diff --git a/.behaverc b/.behaverc new file mode 100644 index 0000000..303b787 --- /dev/null +++ b/.behaverc @@ -0,0 +1,3 @@ +[behave] +format=plain +paths=test/scenarios \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..d02b1e1 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 120 +exclude=test/* \ No newline at end of file diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index bb475ac..391171f 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -1,23 +1,29 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Runner +name: AlgoRunner on: push: - branches: [ master ] + branches: [ master, develop ] pull_request: branches: [ master ] jobs: - build: + ci: + strategy: + fail-fast: true + matrix: + python-version: [3.9] # Py3 only, and latest release UP. runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v1 + - uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - uses: abatilo/actions-poetry@v2.0.0 with: - python-version: 3.8 + poetry-version: 1.1.7 - name: Install dependencies run: | make deps diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7e69ebd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.9-slim + +RUN apt-get update && apt-get install build-essential -y + +WORKDIR /algorunner +COPY poetry.lock pyproject.toml Makefile setup.sh /algorunner/ + +RUN make env-check + +# @todo --no-dev --no-ansi +RUN poetry config virtualenvs.create false && make deps + +COPY . /code +ENTRYPOINT [ "make" "run" ] diff --git a/Makefile b/Makefile index 9e7dd32..3e20980 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,20 @@ -.PHONY: deps test +.PHONY: env-check build lint deps test run + +env-check: + @sh setup.sh + +build: + echo "build docker container" lint: - flake8 ./lib --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 ./lib --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + poetry run flake8 deps: - pip install pandas - pip install python-binance - pip install flake8 + poetry install --no-interaction test: - python -m test.account - python -m test.runner \ No newline at end of file + poetry run pytest + poetry run behave + +run: + poetry run python run.py \ No newline at end of file diff --git a/README.md b/README.md index bc7fb6f..e342521 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,29 @@ -# Runner ![Runner](https://github.com/FergusInLondon/Runner/workflows/Runner/badge.svg) +# ... @todo? -A massive WIP that may or may not be worth an actual README at this point in time. It has no tests, and the account functionality is still being baked in. +## Running -Currently it does invoke a strategy and provides it with real-time streamed data from Binance though. +@todo -### Note -This is *vaguely* related to my form of Enigma Catalyst, as (a) I really want to brush up on my Python, and (b) Binance seems like the best exchange to implement streaming trades on - so I'd like to get used to interacting with them. +### Configuration -## Example +@todo -Check `example.py` for a runnable version of this strategy: +## Development -```python -class ExampleStrategy(object): - """ - A simple example strategy that computes the average price change over - the previous 5 2000ms updates. - """ +### Docker +There's also a `Dockerfile` contained in this repository; this installs all the requirements to commence development. - def start(self, control): - self.series = pd.DataFrame() - self.control = control +### Finding Tasks - def process(self, kline): - self.series = self.series.append(kline) +The codebase is littered with `@todo` tags where low-hanging fruit is marked when discovered/encountered. - if self.series.shape[0] > 5: - print("Average price change over past 5 windows: ", pd.to_numeric(self.series[-5:]["PriceChange"]).mean()) ``` +➜ adapters git:(huge-refactor) ✗ grep -r '@todo' . + ./binance/test_user_transformations.py: pass # @todo - transformation not implemented +➜ adapters git:(huge-refactor) ✗ ../.. -When executed via the runner, this will calculate the average price change over the past 5 2000ms updates, and display it to the user. - -``` -python example.py -Average price change over past 5 windows: 26.694 -Average price change over past 5 windows: 26.356 -Average price change over past 5 windows: 26.444 -Average price change over past 5 windows: 26.246000000000002 -Average price change over past 5 windows: 26.272000000000002 -Average price change over past 5 windows: 26.706 -Average price change over past 5 windows: 27.142000000000003 -Average price change over past 5 windows: 27.182 -Average price change over past 5 windows: 27.562 -Average price change over past 5 windows: 28.002 -Average price change over past 5 windows: 28.246000000000002 -Average price change over past 5 windows: 28.49 -Average price change over past 5 windows: 28.754 +➜ Runner git:(huge-refactor) ✗ grep -r '@todo' . | wc -l + 17 ``` --- diff --git a/algorunner/__init__.py b/algorunner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/algorunner/abstract/__init__.py b/algorunner/abstract/__init__.py new file mode 100644 index 0000000..c891bd1 --- /dev/null +++ b/algorunner/abstract/__init__.py @@ -0,0 +1,2 @@ +from algorunner.abstract.strategy import Strategy # noqa: F401 +from algorunner.abstract.calculator import Calculator # noqa: F401 diff --git a/algorunner/abstract/calculator.py b/algorunner/abstract/calculator.py new file mode 100644 index 0000000..a9d6f32 --- /dev/null +++ b/algorunner/abstract/calculator.py @@ -0,0 +1,11 @@ +from abc import ABC + + +class Calculator(ABC): + """ + A `Calculator` is responsible for determining whether a position should + be opened, and how those positions should be sized/placed. + + @todo: interface TBD + """ + pass diff --git a/algorunner/abstract/strategy.py b/algorunner/abstract/strategy.py new file mode 100644 index 0000000..6d5bca2 --- /dev/null +++ b/algorunner/abstract/strategy.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod + +import pandas as pd + + +class Strategy(ABC): + """ + A `Strategy` is the container for an algorithm, it simply needs to respond + to incoming market payloads and be able to generate events for the `Account` + Actor. + """ + @abstractmethod + def process(self, tick: pd.DataFrame): + """ + @todo - accept Union[pd.DataFrame, RawMarketPayload] + where RawMarketPayload is a TypedDict w/ no pandas processing. + """ + pass + + def dispatch(self): + """ + @todo - fire events to the Actor queue. Concrete implementation. + """ + pass diff --git a/algorunner/adapters/__init__.py b/algorunner/adapters/__init__.py new file mode 100644 index 0000000..db2603b --- /dev/null +++ b/algorunner/adapters/__init__.py @@ -0,0 +1,9 @@ +from algorunner.adapters._binance import BinanceAdapter +from algorunner.adapters.base import ( # noqa: F401 + Credentials, + InvalidPayloadRecieved +) + +ADAPTERS = { + "binance": BinanceAdapter +} diff --git a/algorunner/adapters/_binance.py b/algorunner/adapters/_binance.py new file mode 100644 index 0000000..965067e --- /dev/null +++ b/algorunner/adapters/_binance.py @@ -0,0 +1,169 @@ +from logging import getLogger +from typing import Tuple + +from binance.client import Client +from binance import BinanceSocketManager +import pandas as pd + + +from algorunner.abstract.strategy import Strategy +from algorunner.adapters.base import ( + Adapter, Credentials, InvalidPayloadRecieved +) +from algorunner.trader import Trader +from algorunner.events import ( + AccountStatus, + BalanceUpdate, + Position, + PositionStatus, + UpdateEvent, + UpdateType +) + + +logger = getLogger() + + +class BinanceAdapter(Adapter): + """ """ + + class MarketStreamPandasTransformer: + def __call__(self, tick) -> pd.DataFrame: + """Converts the inbound tick to something exchange-agnostic.""" + df = pd.DataFrame([tick]) + df.rename(columns=lambda col: { + 'e': "24hrTicker", + 'E': "EventTime", + 's': "Symbol", + 'p': "PriceChange", + 'P': "PriceChangePercent", + 'w': "WeightedAveragePrice", + 'x': "FirstTradePrice", + 'c': "LastPrice", + 'Q': "LastQuantity", + 'b': "BestBidPrice", + 'B': "BestBidQuantity", + 'a': "BestAskPrice", + 'A': "BestAskQuantity", + 'o': "OpenPrice", + 'h': "HighPrice", + 'l': "LowPrice", + 'v': "TotalTradedBaseAssetVolume", + 'q': "TotalTradedQuoteAssetVolume", + 'O': "StatisticsOpenTime", + 'C': "StatisticsCloseTime", + 'F': "FirstTradeId", + 'L': "LastTradeId", + 'n': "TotalNumberOfTrades", + }[col], + inplace=True) + df.set_index('EventTime', inplace=True) + df.index = pd.to_datetime(df.index, unit='ms') + return df + + class UserStreamEventTransformer: + """ """ + + def __call__(self, payload) -> Tuple[str, UpdateEvent]: + try: + message_map = { + "outboundAccountInfo": self.account_update, + "outboundAccountPosition": self.position_update, + "balanceUpdate": self.balance_update, + "executionReport": self.order_report + } + + return message_map[payload["e"]](payload) + except KeyError: + msg = "unknown payload type {p}".format(p=payload.get("e")) + raise InvalidPayloadRecieved(msg) + except Exception as e: + raise Exception("unknown error occured in user stream", e) + + def initial_rest_payload(self, payload) -> AccountStatus: + # @todo - there is so much data in this payload that we're missing + # out on, like commission rates etc. + return AccountStatus( + CanWithdraw=payload["canWithdraw"], + CanTrade=payload["canTrade"], + CanDeposit=payload["canDeposit"], + Positions={ + balance["asset"]: Position( + Locked=float(balance["locked"]), + Free=float(balance["free"]) + ) for balance in payload["balances"] + } + ) + + def account_update(self, payload) -> Tuple[str, AccountStatus]: + return UpdateType.ACCOUNT, AccountStatus( + CanWithdraw=payload["W"], + CanTrade=payload["T"], + CanDeposit=payload["D"], + Positions={ + balance["a"]: Position( + Locked=float(balance["l"]), + Free=float(balance["f"]) + ) for balance in payload["B"] + } + ) + + def balance_update(self, payload) -> Tuple[str, BalanceUpdate]: + return UpdateType.BALANCE, BalanceUpdate( + Asset=payload["a"], Update=float(payload["d"]) + ) + + def position_update(self, payload) -> Tuple[str, PositionStatus]: + return UpdateType.POSITION, { + balance["a"]: Position( + Locked=float(balance["l"]), + Free=float(balance["f"]) + ) for balance in payload["B"] + } + + def order_report(self, payload): + # @todo - never did work out how to handle these. + pass + + def connect(self, creds: Credentials, trader: Trader): + self.trader = trader + self.client = Client(creds['key'], creds['secret']) + self.socket_manager = BinanceSocketManager(self.client) + + self.user_transformer = self.UserStreamEventTransformer() + self.market_transformer = self.MarketStreamPandasTransformer() + update = self.transformer.initial_rest_payload( + self.client.get_account() + ) + self.trader(UpdateType.ACCOUNT, update) + + self.socket_manager.start_user_socket(self.handle_user_stream) + + def run(self, strategy: Strategy, symbol: str): + self.strategy = strategy + self.socket_manager.start_symbol_ticker_socket( + symbol, self._handle_ticker + ) + + def _handle_ticker(self, tick): + """Given an incoming payload from the market websocket stream, + prepare it for the `Strategy` and then execute the strategy + against it.""" + try: + parsed_data = self.market_transformer(tick) + self.strategy.process(parsed_data) + except InvalidPayloadRecieved as e: + logger.warn( + "received exception when handling market stream. ignoring tick.", + e + ) + + def _handle_user_stream(self, payload): + try: + update_type, transformed = self.user_transformer(payload) + self.account(update_type, transformed) + except Exception as e: + logger.warn( + "recieved exception handling user stream payload. ignoring message.", + e + ) diff --git a/algorunner/adapters/base.py b/algorunner/adapters/base.py new file mode 100644 index 0000000..37f4b79 --- /dev/null +++ b/algorunner/adapters/base.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from typing import TypedDict + +from algorunner.abstract.strategy import Strategy +from algorunner.trader import Trader + + +class InvalidPayloadRecieved(Exception): + """InvalidPayloadRecieved is thrown when an invalid message is recieved + from the exchange via a websocket stream.""" + pass + + +class Credentials(TypedDict): + """Required credentials to authenticate with a given exchange.""" + exchange: str + key: str + secret: str + + +class Adapter(ABC): + """Required interface that an exchange adapter must implement.""" + + @abstractmethod + def connect(self, creds: Credentials, trader: Trader): + """connect authenticates with the exchange, and also populates + the associated `Trader` object with the latest state.""" + pass + + @abstractmethod + def run(self, strategy: Strategy): + """run executes the underlying strategy, ensuring that any data + transformation required is carried out correctly.""" + pass diff --git a/algorunner/events.py b/algorunner/events.py new file mode 100644 index 0000000..7244ae1 --- /dev/null +++ b/algorunner/events.py @@ -0,0 +1,52 @@ +from enum import Enum +from typing import ( + Dict, NamedTuple, TypedDict, Union +) + + +class UpdateType(Enum): + """ """ + BALANCE = 1 + ACCOUNT = 2 + POSITION = 3 + + +class Position(NamedTuple): + """ """ + Free: float + Locked: float + + +PositionStatus = Dict[str, Position] + + +class AccountStatus(TypedDict): + """ """ + CanWithdraw: bool + CanTrade: bool + CanDeposit: bool + Positions: PositionStatus + + +class BalanceUpdate(TypedDict): + """ """ + Asset: str + Update: float + + +class AccountEventAction(Enum): + """ """ + NO_ACTION = 1 + BUY = 2 + SELL = 3 + + +UpdateEvent = Union[AccountStatus, BalanceUpdate, PositionStatus] + + +class CalculationResult(Enum): + """ """ + INSUFFICIENT_FUNDS = 1 + TRANSACTION_REJECTED = 2 + POSITION_UPDATED = 3 + SUCCESSFUL_REBALANCE = 4 diff --git a/algorunner/exceptions.py b/algorunner/exceptions.py new file mode 100644 index 0000000..d424f47 --- /dev/null +++ b/algorunner/exceptions.py @@ -0,0 +1,26 @@ +from typing import Optional, List + +MSG_INVALID_CONFIG = "unable to parse all required values from configuration" +MSG_INVALID_CONFIG_W_FIELDS = "unable to parse [{fields}] from configuration" +MSG_UNKNOWN_EXCHANGE = "unable to find adapter for exchange '{name}'" + + +class InvalidConfiguration(Exception): + """ + Raised when there's an issue with the provided configuration; either an + option not being specified, or an option being specified incorrectly. + """ + def __init__(self, invalid_fields: Optional[List[str]]): + self.message = ( + MSG_INVALID_CONFIG if not invalid_fields + else MSG_INVALID_CONFIG_W_FIELDS.format(fields=invalid_fields.join(", ")) + ) + + +class UnknownExchange(Exception): + """ + Raised when the exchange specified in the configuration is unknown. + """ + def __init__(self, exchange_name: str, exception: Optional[Exception]): + self.message = MSG_UNKNOWN_EXCHANGE.format(name=exchange_name) + self.exc = exception diff --git a/algorunner/runner.py b/algorunner/runner.py new file mode 100644 index 0000000..6b1a106 --- /dev/null +++ b/algorunner/runner.py @@ -0,0 +1,28 @@ +from algorunner.trader import Trader +from algorunner.adapters import ADAPTERS, Credentials +from algorunner.exceptions import UnknownExchange +from algorunner.abstract.strategy import Strategy + + +class Runner(object): + """ + The Runner is responsible for configuring all components required to execute + a trading algorithm. It's called by the entrypoint of the app, located in + `run.py`. + """ + + def __init__(self, creds: Credentials, symbol: str, strategy: Strategy): + adapter_cls = ADAPTERS.get(creds["exchange"]) + if not adapter_cls: + raise UnknownExchange(creds["exchange"]) + + self.account = Trader() + self.symbol = symbol + self.strategy = strategy + self.adapter = adapter_cls() + + self.adapter.connect(creds, self.account) + + def run(self): + """ """ + self.adapter.run(self.strategy, self.symbol) diff --git a/algorunner/strategy.py b/algorunner/strategy.py new file mode 100644 index 0000000..eb64faa --- /dev/null +++ b/algorunner/strategy.py @@ -0,0 +1,51 @@ +from importlib import import_module +from typing import Optional + +from algorunner.abstract import Strategy + + +class FailureLoadingStrategy(Exception): + """ + Raised when a Strategy cannot be instantiated; this may be down to + loading the Strategy, or errors that render it unexecutable. Also + stores the original exception if available. + """ + def __init__(self, strategy_name: str, exception: Optional[Exception]): + self.message = "unable to instantiate strategy '{name}'".format(name=strategy_name) + self.exc = exception + + +class InvalidStrategyProvided(Exception): + """Raised when the loaded strategy does no inherit from the base class.""" + pass + + +class StrategyNotFound(Exception): + """Raised when the module loader is unable to retrieve the strategy.""" + pass + + +_DEFAULT_STRATEGY_PARENT_MODULE = 'strategies.{module}' + + +def load_strategy(strategy_name: str, module_name: Optional[str] = None) -> Strategy: + """Dynamically load strategies located in the `/strategies` directory""" + if not module_name: + module_name = _DEFAULT_STRATEGY_PARENT_MODULE.format( + module=strategy_name.lower() + ) + + try: + module = import_module(module_name) + + _class = getattr(module, strategy_name) + if not issubclass(_class, Strategy): + raise InvalidStrategyProvided() + + return _class() + except InvalidStrategyProvided as e: + raise e + except ModuleNotFoundError: + raise StrategyNotFound() + except Exception as e: + raise FailureLoadingStrategy(strategy_name, e) diff --git a/algorunner/trader.py b/algorunner/trader.py new file mode 100644 index 0000000..ba5932f --- /dev/null +++ b/algorunner/trader.py @@ -0,0 +1,73 @@ +from algorunner.events import ( + AccountStatus, UpdateEvent, UpdateType +) + + +class Trader: + """ + The Trader is a model of a real "trader" - i.e it monitors for indicators + generated by it's strategy, applies calculations from rules defined by it's + `Calculator` instance to determine whether an order should be made, and if + so, at what rate/quantity. + """ + + def __call__(self, update_type: str, updated_props: UpdateEvent): + """Sets account state - i.e. balance and positions.""" + + if update_type == UpdateType.ACCOUNT: + self.status = updated_props + + """ + # This required python > 3.10.*; alas there's a bug with pip + # on this version. So this will likely need to be refactored + # to use simple if/else comparisons until pip 21.2.3 is released. + # @see https://github.com/pypa/pip/pull/10252 + # @todo + match update_type: + case UpdateType.BALANCE: + # @todo Eh, look at whatever fuckery is involved. + # surely the "Locked" balance needs updating to..?! + asset = updated_props["Asset"] + self.status.Positions[asset]["Free"] += updated_props["Update"] + case UpdateType.ACCOUNT: + self.status = updated_props + case UpdateType.POSITION: + self.status["Positions"] = updated_props + """ + pass + + def initial_state(self, status: AccountStatus): + self.status = status + + def start(self, handler): + pass + + def balance(self, asset): + return self.status.get(asset, None) + + +""" + # @todo - these will be events. + def buy(self, asset, amount, limit=False, price=0): + if limit: + self.binance.order_limit_buy( + symbol=asset, + quantity=amount, + price=price) + else: + self.binance.order_market_buy( + symbol=asset, + quantity=amount) + + # @todo - these will be events. + def sell(self, asset, amount, limit=False, price=0): + if limit: + self.binance.order_limit_sell( + symbol=asset, + quantity=amount, + price=price) + else: + self.binance.order_market_sell( + symbol=asset, + quantity=amount) +""" diff --git a/example.py b/example.py deleted file mode 100644 index e8cc76a..0000000 --- a/example.py +++ /dev/null @@ -1,38 +0,0 @@ -import pandas as pd -from lib.runner import Runner -import configparser - -class ExampleStrategy(object): - """ - A simple example strategy that computes the average price change over - the previous 5 2000ms updates. - """ - - def start(self, control): - self.series = pd.DataFrame() - self.control = control - - def process(self, kline): - self.series = self.series.append(kline) - - if self.series.shape[0] > 5: - print("Average price change over past 5 windows: ", pd.to_numeric(self.series[-5:]["PriceChange"]).mean()) - - -if __name__ == "__main__": - # Example Usage: - # - # Instantiate a `Runner`; providing API credentials, the symbol you wish to - # run your strategy against, and the actual strategy itself. Then simply call - # `.run()` to execute the strategy. - - cfg = configparser.ConfigParser() - cfg.read('bot.ini') - - strategy = ExampleStrategy() - runner = Runner( - apiKey = cfg['credentials']['ApiKey'], - apiSecret = cfg['credentials']['ApiSecret'], - symbol = cfg['strategy']['Symbol'], - runnable=strategy) - runner.run() diff --git a/lib/__init__.py b/lib/__init__.py deleted file mode 100644 index cc2c489..0000000 --- a/lib/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -import os -import sys - -sys.path.append(os.path.dirname(os.path.realpath(__file__))) diff --git a/lib/account.py b/lib/account.py deleted file mode 100644 index d715bc1..0000000 --- a/lib/account.py +++ /dev/null @@ -1,92 +0,0 @@ - -class Account(object): - """ - The Account is responsible for keeping track of user balances, it's - updated via a UserSocket. - """ - - can_withdraw = None - can_trade = None - can_deposit = None - balances = {} - - def __init__(self, payload): - self.can_withdraw = payload["canWithdraw"] - self.can_trade = payload["canTrade"] - self.can_deposit = payload["canDeposit"] - - for asset in payload["balances"]: - self.balances[asset["asset"]] = ( - float(asset["free"]), float(asset["locked"])) - - def __call__(self, payload): - """ - For simplicity Account is callable. - """ - - action = { - 'outboundAccountInfo': self.account_update, - 'outboundAccountPosition': self.position_update, - 'balanceUpdate': self.balance_update, - 'executionReport': self.order_report - }.get(payload["e"], - lambda p: print("unhandled account event ", p["e"])) - - action(payload) - - def account_update(self, payload): - self.can_withdraw = payload["W"] - self.can_trade = payload["T"] - self.can_deposit = payload["D"] - - for asset in payload["B"]: - self.balances[asset["a"]] = (float(asset["f"]), float(asset["l"])) - - def position_update(self, payload): - for asset in payload["B"]: - self.balances[asset["a"]] = (float(asset["f"]), float(asset["l"])) - - def balance_update(self, payload): - self.balances[payload["a"]] = ( - self.balances[payload["a"]][0] + float(payload["d"]), - self.balances[payload["a"]][1] - ) - - def order_report(self, payload): - # Not entirely sure what, if anything other than logging, we should do - # here. After all, actual account updates/state are already handled. - pass - - def capability_trade(self): - return self.can_trade - - def capability_withdraw(self): - return self.can_withdraw - - def capability_deposit(self): - return self.can_deposit - - def balance(self, asset): - return self.balances.get(asset, None) - - def buy(self, asset, amount, limit=False, price=0): - if limit: - self.binance.order_limit_buy( - symbol=asset, - quantity=amount, - price=price) - else: - self.binance.order_market_buy( - symbol=asset, - quantity=amount) - - def sell(self, asset, amount, limit=False, price=0): - if limit: - self.binance.order_limit_sell( - symbol=asset, - quantity=amount, - price=price) - else: - self.binance.order_market_sell( - symbol=asset, - quantity=amount) diff --git a/lib/runner.py b/lib/runner.py deleted file mode 100644 index d927f01..0000000 --- a/lib/runner.py +++ /dev/null @@ -1,83 +0,0 @@ -import pandas as pd - -from account import Account -from binance.client import Client -from binance.websockets import BinanceSocketManager - - -class Runner(object): - """ - The Runner is responsible for handling any exchange interactions, - and running a given `Strategy` against the data that is provided - from the exchange. - - A `Strategy` is expected to have two methods (a) `start()` - which - performs any initialisation - such as configuring instance attributes, - and (b) `process(runner, tick_dataframe)`, which computes any actions - to run based upon the incoming data from the exchange. - """ - - def __init__(self, apiKey, apiSecret, symbol, runnable): - self.client = Client(apiKey, apiSecret) - - account_info = self.client.get_account() - self.account = Account(account_info) - - self.bm = BinanceSocketManager(self.client) - self.bm.start_user_socket(self.account) - - self.symbol = symbol - self.runnable = runnable - - def run(self): - """ - Run is the main event loop, where data taken from the exchange - is subsequently passed on to the underlying strategy. - - By passing this instance in to the strategy - via the `run(r, d)` - method - it's possible for the strategy to invoke methods that - interact with the exchange, via this runner; i.e to make buy/sell - calls. - """ - try: - self.runnable.start(self) - except AttributeError: - print("invalid runnable: no 'start' method") - self.bm.start_symbol_ticker_socket( - self.symbol, - lambda kline: self.runnable.process(self.parse_dataframe(kline))) - self.bm.start() - - def parse_dataframe(self, kline): - """ - """ - df = pd.DataFrame([kline]) - df.rename(columns=lambda col: { - 'e': "24hrTicker", - 'E': "EventTime", - 's': "Symbol", - 'p': "PriceChange", - 'P': "PriceChangePercent", - 'w': "WeightedAveragePrice", - 'x': "FirstTradePrice", - 'c': "LastPrice", - 'Q': "LastQuantity", - 'b': "BestBidPrice", - 'B': "BestBidQuantity", - 'a': "BestAskPrice", - 'A': "BestAskQuantity", - 'o': "OpenPrice", - 'h': "HighPrice", - 'l': "LowPrice", - 'v': "TotalTradedBaseAssetVolume", - 'q': "TotalTradedQuoteAssetVolume", - 'O': "StatisticsOpenTime", - 'C': "StatisticsCloseTime", - 'F': "FirstTradeId", - 'L': "LastTradeId", - 'n': "TotalNumberOfTrades", - }[col], - inplace=True) - df.set_index('EventTime', inplace=True) - df.index = pd.to_datetime(df.index, unit='ms') - return df diff --git a/plain.output b/plain.output new file mode 100644 index 0000000..5232138 --- /dev/null +++ b/plain.output @@ -0,0 +1,55 @@ +Feature: Account State + Background: + + Scenario: Stay synchronised with account updates + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given an account update with full capabilities ... passed in 0.000s + When that account update is processed ... passed in 0.000s + Then the account should have full capabilities ... passed in 0.000s + Given an account update with minimal capabilities ... passed in 0.000s + When that account update is processed ... passed in 0.000s + Then the account should have minimal capabilities ... passed in 0.000s + + Scenario: Stay synchronised with balance updates + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given a BTC balance of 50 free and 25 locked ... passed in 0.000s + And a balance update of 20 BTC ... passed in 0.000s + When that balance update is processed ... passed in 0.000s + Then the account should have a balance of 70 BTC free ... passed in 0.000s + Given a balance update of -30 BTC ... passed in 0.000s + When that balance update is processed ... passed in 0.000s + Then the account should have a balance of 40 BTC free ... passed in 0.000s + + Scenario: Stay synchronised with position updates + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given an account position of BTC at 10 free and 25 locked ... passed in 0.000s + And a position update of ETH at 20 free and 50 locked ... passed in 0.000s + When that position update is processed ... passed in 0.000s + Then there should be a BTC balance of 10 free and 25 locked ... passed in 0.000s + And there should be a ETH balance of 20 free and 50 locked ... passed in 0.000s + And there should be a total of 2 balances ... passed in 0.000s + Given a position update of BTC at 5 free and 30 locked ... passed in 0.000s + When that position update is processed ... passed in 0.000s + Then there should be a BTC balance of 5 free and 30 locked ... passed in 0.000s + And there should be a ETH balance of 20 free and 50 locked ... passed in 0.000s + And there should be a total of 2 balances ... passed in 0.000s + + Scenario: Process orders requests approved by the calculator + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given a market order to buy BTC ... passed in 0.000s + And the calculator will accept the order with a size of 0.0032 ... passed in 0.000s + When the order event is processed ... passed in 0.000s + Then the API should recieve an order of 0.0032 BTC ... passed in 0.000s + + Scenario: Skip order requests rejected by the calculator + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given a market order to buy BTC ... passed in 0.000s + And the calculator will reject the order ... passed in 0.000s + When the order event is processed ... passed in 0.000s + Then the API should not recieve any orders ... passed in 0.000s + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..259a864 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,827 @@ +[[package]] +name = "aiohttp" +version = "3.7.4.post0" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +async-timeout = ">=3.0,<4.0" +attrs = ">=17.3.0" +chardet = ">=2.0,<5.0" +multidict = ">=4.5,<7.0" +typing-extensions = ">=3.6.5" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["aiodns", "brotlipy", "cchardet"] + +[[package]] +name = "async-timeout" +version = "3.0.1" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.5.3" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "behave" +version = "1.2.6" +description = "behave is behaviour-driven development, Python style" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +parse = ">=1.8.2" +parse-type = ">=0.4.2" +six = ">=1.11" + +[package.extras] +develop = ["coverage", "pytest (>=3.0)", "pytest-cov", "tox", "invoke (>=0.21.0)", "path.py (>=8.1.2)", "pycmd", "pathlib", "modernize (>=0.5)", "pylint"] +docs = ["sphinx (>=1.6)", "sphinx-bootstrap-theme (>=0.6)"] + +[[package]] +name = "certifi" +version = "2021.5.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "charset-normalizer" +version = "2.0.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.1" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "dateparser" +version = "1.0.0" +description = "Date parsing library designed to parse dates from HTML pages" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +python-dateutil = "*" +pytz = "*" +regex = "!=2019.02.19" +tzlocal = "*" + +[package.extras] +calendars = ["convertdate", "hijri-converter", "convertdate"] + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "idna" +version = "3.2" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "multidict" +version = "5.1.0" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "numpy" +version = "1.21.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pandas" +version = "1.3.1" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.7.1" + +[package.dependencies] +numpy = ">=1.17.3" +python-dateutil = ">=2.7.3" +pytz = ">=2017.3" + +[package.extras] +test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"] + +[[package]] +name = "parse" +version = "1.19.0" +description = "parse() is the opposite of format()" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "parse-type" +version = "0.5.2" +description = "Simplifies to build parse types based on the parse module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" + +[package.dependencies] +parse = ">=1.8.4" +six = ">=1.11" + +[package.extras] +develop = ["coverage (>=4.4)", "pytest (>=3.2)", "pytest-cov", "tox (>=2.8)"] +docs = ["sphinx (>=1.2)"] + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "python-binance" +version = "1.0.12" +description = "Binance REST API python implementation" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +aiohttp = "*" +dateparser = "*" +requests = "*" +six = "*" +ujson = "*" +websockets = "*" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2021.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "regex" +version = "2021.8.3" +description = "Alternative regular expression module, to replace re." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "tzlocal" +version = "2.1" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pytz = "*" + +[[package]] +name = "ujson" +version = "4.0.2" +description = "Ultra fast JSON encoder and decoder for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.6" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "websockets" +version = "9.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "yarl" +version = "1.6.3" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "78dd347ad4cf94fcfb10cb9987109c1a20b0b27c2c7dbfc049fc5a7cda5c3716" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, + {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, +] +async-timeout = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +behave = [ + {file = "behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"}, + {file = "behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86"}, +] +certifi = [ + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, +] +click = [ + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, +] +dateparser = [ + {file = "dateparser-1.0.0-py2.py3-none-any.whl", hash = "sha256:17202df32c7a36e773136ff353aa3767e987f8b3e27374c39fd21a30a803d6f8"}, + {file = "dateparser-1.0.0.tar.gz", hash = "sha256:159cc4e01a593706a15cd4e269a0b3345edf3aef8bf9278a57dac8adf5bf1e4a"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +idna = [ + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +multidict = [ + {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"}, + {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"}, + {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"}, + {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"}, + {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"}, + {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"}, + {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"}, + {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"}, + {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"}, + {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"}, + {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"}, + {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"}, + {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, +] +numpy = [ + {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, + {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, + {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, + {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, + {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, + {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, + {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, + {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, + {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, +] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] +pandas = [ + {file = "pandas-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1ee8418d0f936ff2216513aa03e199657eceb67690995d427a4a7ecd2e68f442"}, + {file = "pandas-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d9acfca191140a518779d1095036d842d5e5bc8e8ad8b5eaad1aff90fe1870d"}, + {file = "pandas-1.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e323028ab192fcfe1e8999c012a0fa96d066453bb354c7e7a4a267b25e73d3c8"}, + {file = "pandas-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d06661c6eb741ae633ee1c57e8c432bb4203024e263fe1a077fa3fda7817fdb"}, + {file = "pandas-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:23c7452771501254d2ae23e9e9dac88417de7e6eff3ce64ee494bb94dc88c300"}, + {file = "pandas-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7150039e78a81eddd9f5a05363a11cadf90a4968aac6f086fd83e66cf1c8d1d6"}, + {file = "pandas-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5c09a2538f0fddf3895070579082089ff4ae52b6cb176d8ec7a4dacf7e3676c1"}, + {file = "pandas-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905fc3e0fcd86b0a9f1f97abee7d36894698d2592b22b859f08ea5a8fe3d3aab"}, + {file = "pandas-1.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ee927c70794e875a59796fab8047098aa59787b1be680717c141cd7873818ae"}, + {file = "pandas-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c976e023ed580e60a82ccebdca8e1cc24d8b1fbb28175eb6521025c127dab66"}, + {file = "pandas-1.3.1-cp38-cp38-win32.whl", hash = "sha256:22f3fcc129fb482ef44e7df2a594f0bd514ac45aabe50da1a10709de1b0f9d84"}, + {file = "pandas-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45656cd59ae9745a1a21271a62001df58342b59c66d50754390066db500a8362"}, + {file = "pandas-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:114c6789d15862508900a25cb4cb51820bfdd8595ea306bab3b53cd19f990b65"}, + {file = "pandas-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:527c43311894aff131dea99cf418cd723bfd4f0bcf3c3da460f3b57e52a64da5"}, + {file = "pandas-1.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb3b33dde260b1766ea4d3c6b8fbf6799cee18d50a2a8bc534cf3550b7c819a"}, + {file = "pandas-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c28760932283d2c9f6fa5e53d2f77a514163b9e67fd0ee0879081be612567195"}, + {file = "pandas-1.3.1-cp39-cp39-win32.whl", hash = "sha256:be12d77f7e03c40a2466ed00ccd1a5f20a574d3c622fe1516037faa31aa448aa"}, + {file = "pandas-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9e1fe6722cbe27eb5891c1977bca62d456c19935352eea64d33956db46139364"}, + {file = "pandas-1.3.1.tar.gz", hash = "sha256:341935a594db24f3ff07d1b34d1d231786aa9adfa84b76eab10bf42907c8aed3"}, +] +parse = [ + {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"}, +] +parse-type = [ + {file = "parse_type-0.5.2-py2.py3-none-any.whl", hash = "sha256:089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e"}, + {file = "parse_type-0.5.2.tar.gz", hash = "sha256:7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +python-binance = [ + {file = "python-binance-1.0.12.tar.gz", hash = "sha256:f3eaa3e93ce6f227073bc6cd131fb1fe9c9bd22195d3305ba75d523511474d49"}, + {file = "python_binance-1.0.12-py2.py3-none-any.whl", hash = "sha256:941f0aa9420559e175612fcb2b9f836b98ffe06a812c01dd8ef7bdb132635435"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytz = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, +] +regex = [ + {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, + {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"}, + {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"}, + {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, + {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"}, + {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"}, + {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, + {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"}, + {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"}, + {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"}, + {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"}, + {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"}, + {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, +] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] +tzlocal = [ + {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, + {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, +] +ujson = [ + {file = "ujson-4.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e390df0dcc7897ffb98e17eae1f4c442c39c91814c298ad84d935a3c5c7a32fa"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:84b1dca0d53b0a8d58835f72ea2894e4d6cf7a5dd8f520ab4cbd698c81e49737"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:91396a585ba51f84dc71c8da60cdc86de6b60ba0272c389b6482020a1fac9394"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:eb6b25a7670c7537a5998e695fa62ff13c7f9c33faf82927adf4daa460d5f62e"}, + {file = "ujson-4.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f8aded54c2bc554ce20b397f72101737dd61ee7b81c771684a7dd7805e6cca0c"}, + {file = "ujson-4.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:30962467c36ff6de6161d784cd2a6aac1097f0128b522d6e9291678e34fb2b47"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:fc51e545d65689c398161f07fd405104956ec27f22453de85898fa088b2cd4bb"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e6e90330670c78e727d6637bb5a215d3e093d8e3570d439fd4922942f88da361"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:5e1636b94c7f1f59a8ead4c8a7bab1b12cc52d4c21ababa295ffec56b445fd2a"}, + {file = "ujson-4.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e2cadeb0ddc98e3963bea266cc5b884e5d77d73adf807f0bda9eca64d1c509d5"}, + {file = "ujson-4.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a214ba5a21dad71a43c0f5aef917cd56a2d70bc974d845be211c66b6742a471c"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0190d26c0e990c17ad072ec8593647218fe1c675d11089cd3d1440175b568967"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f273a875c0b42c2a019c337631bc1907f6fdfbc84210cc0d1fff0e2019bbfaec"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d3a87888c40b5bfcf69b4030427cd666893e826e82cc8608d1ba8b4b5e04ea99"}, + {file = "ujson-4.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:7333e8bc45ea28c74ae26157eacaed5e5629dbada32e0103c23eb368f93af108"}, + {file = "ujson-4.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b3a6dcc660220539aa718bcc9dbd6dedf2a01d19c875d1033f028f212e36d6bb"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0ea07fe57f9157118ca689e7f6db72759395b99121c0ff038d2e38649c626fb1"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d6d061563470cac889c0a9fd367013a5dbd8efc36ad01ab3e67a57e56cad720"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b5c70704962cf93ec6ea3271a47d952b75ae1980d6c56b8496cec2a722075939"}, + {file = "ujson-4.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:aad6d92f4d71e37ea70e966500f1951ecd065edca3a70d3861b37b176dd6702c"}, + {file = "ujson-4.0.2.tar.gz", hash = "sha256:c615a9e9e378a7383b756b7e7a73c38b22aeb8967a8bfbffd4741f7ffd043c4d"}, +] +urllib3 = [ + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, +] +websockets = [ + {file = "websockets-9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da"}, + {file = "websockets-9.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4"}, + {file = "websockets-9.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58"}, + {file = "websockets-9.1-cp36-cp36m-win32.whl", hash = "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a"}, + {file = "websockets-9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b"}, + {file = "websockets-9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af"}, + {file = "websockets-9.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd"}, + {file = "websockets-9.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2"}, + {file = "websockets-9.1-cp37-cp37m-win32.whl", hash = "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a"}, + {file = "websockets-9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857"}, + {file = "websockets-9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe"}, + {file = "websockets-9.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0"}, + {file = "websockets-9.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02"}, + {file = "websockets-9.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f"}, + {file = "websockets-9.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec"}, + {file = "websockets-9.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc"}, + {file = "websockets-9.1-cp38-cp38-win32.whl", hash = "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e"}, + {file = "websockets-9.1-cp38-cp38-win_amd64.whl", hash = "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077"}, + {file = "websockets-9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2"}, + {file = "websockets-9.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d"}, + {file = "websockets-9.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d"}, + {file = "websockets-9.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c"}, + {file = "websockets-9.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135"}, + {file = "websockets-9.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd"}, + {file = "websockets-9.1-cp39-cp39-win32.whl", hash = "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20"}, + {file = "websockets-9.1-cp39-cp39-win_amd64.whl", hash = "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0"}, + {file = "websockets-9.1.tar.gz", hash = "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3"}, +] +yarl = [ + {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"}, + {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"}, + {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"}, + {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"}, + {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"}, + {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"}, + {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"}, + {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"}, + {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"}, + {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"}, + {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"}, + {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"}, + {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c2afae0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "AlgoRunner" +version = "0.0.1" +description = "Trading Algorithm execution for cryptocurrency exchanges." +authors = ["Fergus In London "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.9" +click = "^8.0.1" +pandas = "^1.3.1" +python-binance = "^1.0.12" + +[tool.poetry.dev-dependencies] +behave = "^1.2.6" +pytest = "^6.2.4" +flake8 = "^3.9.2" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/run.py b/run.py new file mode 100644 index 0000000..507ed95 --- /dev/null +++ b/run.py @@ -0,0 +1,81 @@ +import configparser +from logging import getLogger + +import click + +from algorunner import exceptions +from algorunner.runner import ( + Credentials, Runner +) +from algorunner.strategy import load_strategy + + +logger = getLogger() + + +@click.command() +@click.option('-c', '--config', 'config_file', default='bot.ini', short_help='Configuration file.') +@click.option('-s', '--strategy', 'strategy_name', short_help='Name of Strategy to run') +@click.option('--testing', isflag=True, default=True, short_help='Run in testing mode, NOT live') +@click.option('--exchange', envvar='ALGORUNNER_EXCHANGE', short_help='Crypto exchange to execute strategy against') +@click.option('--api-key', envvar='ALGORUNNER_API_KEY', short_help='API Key for exchange/broker') +@click.option('--api-secret', envvar='ALGORUNNER_API_SECRET', short_help='API Secret for exchange/broker') +@click.option('--trading-symbol', envvar='ALGORUNNER_TRADING_SYMBOL', short_help='Symbol to execute strategy against') +def entrypoint( + config_file: str, + strategy_name: str, + testing: bool, + exchange: str, + apiKey: str, + apiSecret: str, + trading_symbol: str +): + """AlgoRunner is a simple runner for executing trading strategies against + cryptocurrency exchanges, with support for executing backtests. By default + AlgoRunner will run in BACKTEST mode. + + All configuration can be done through a .INI file, although some parameters + can be passed as CLI arguments and/or environment variables. + + For full details see https://github.com/fergusinlondon/algorunner + """ + + if not testing: + logger.warn("WARNING: Running in LIVE trading mode.") + + cfg = configparser.ConfigParser() + cfg.read(config_file) + + try: + apiKey = apiKey if apiKey else cfg['credentials']['api_key'] + apiSecret = apiSecret if apiSecret else cfg['credentials']['api_secret'] + strategy_name = strategy_name if strategy_name else cfg['strategy']['name'] + exchange = exchange if exchange else cfg['credentials']['exchange'] + trading_symbol = trading_symbol if trading_symbol else cfg['credentials']['symbol'] + except KeyError: + raise exceptions.InvalidConfiguration(exceptions.MSG_MISSING_CONFIG) + + # Filter out any empty config options + if not all([apiKey, apiSecret, strategy_name, exchange, trading_symbol]): + raise exceptions.InvalidConfiguration(exceptions.MSG_MISSING_CONFIG) + + strategy = load_strategy(strategy_name) + runner = Runner(Credentials( + exchange=exchange, + key=apiKey, + secret=apiSecret + ), trading_symbol, strategy) + runner.run() + + +if __name__ == "__main__": + try: + entrypoint() + except exceptions.InvalidConfiguration as e: + logger.critical("CRITICAL FAILURE: incorrect configuration provided", e.message) + except exceptions.FailureLoadingStrategy as e: + logger.critical("CRITICAL FAILURE: unable to instantiate strategy", e.message) + except exceptions.UnknownExchange as e: + logger.critical("CRITICAL FAILURE: invalid exchange specified in config", e.message) + except Exception as e: + logger.critical("CRITICAL FAILURE: Terminating...", e) diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..396cec2 --- /dev/null +++ b/setup.sh @@ -0,0 +1,7 @@ +if ! command -v poetry &> /dev/null +then + echo "Poetry not found, installing from pip..." + pip install poetry +else + echo "Poetry is already installed, you're good to go!" +fi diff --git a/strategies/__init__.py b/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/strategies/example.py b/strategies/example.py new file mode 100644 index 0000000..97986d7 --- /dev/null +++ b/strategies/example.py @@ -0,0 +1,23 @@ +import pandas as pd + +from algorunner.strategy import Strategy + + +class Example(Strategy): + """ + A simple example strategy that computes the average price change over + the previous 5 2000ms updates. + """ + + # this tag is used in a unit test targeting the strategy loader. ignore! + _testing_tag = True + + def __init__(self): + self.series = pd.DataFrame() + + def process(self, tick): + self.series = self.series.append(tick) + + if self.series.shape[0] > 5: + recent_window = pd.to_numeric(self.series[-5:]["PriceChange"]) + print("Average price change over past 5 windows: ", recent_window.mean()) diff --git a/test/TESTING.md b/test/TESTING.md new file mode 100644 index 0000000..9bbded7 --- /dev/null +++ b/test/TESTING.md @@ -0,0 +1,19 @@ +# Testing + +The living document linked to from the `README` outlines the tests expected/required to run. + +## Execution + +To run the tests use the `test` make target - i.e `$ make test`. + +## Test Types + +### BDD Scenarios +The `scenarios` folder contains some BDD style tests - using `behave` - for testing the `Trader` object. + +The decision to introduce additional BDD style tests was taken as `Trader` is based around a rough interpretation of the `Actor` model. In real terms this means all interactions go through a concurrent message-style interface; the implementation in this repository utilises `TypedDict` objects named `*Update`, that get dispatched through a `queue.Queue`. + + +### Unit Tests + +This directory contains the unit tests for application, there are notes as to which components require testing - and the various cases that need to be accounted for - detailed in the notion.so document. diff --git a/test/__init__.py b/test/__init__.py index 6b214be..e69de29 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,3 +0,0 @@ -import os -import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lib'))) diff --git a/test/adapters/__init__.py b/test/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/adapters/binance/__init__.py b/test/adapters/binance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/adapters/binance/fixtures.py b/test/adapters/binance/fixtures.py new file mode 100644 index 0000000..7b4602c --- /dev/null +++ b/test/adapters/binance/fixtures.py @@ -0,0 +1,8 @@ +from pytest import fixture + +from algorunner.adapters import BinanceAdapter + + +@fixture +def user_transformer(): + return BinanceAdapter.UserStreamEventTransformer() \ No newline at end of file diff --git a/test/adapters/binance/test_user_transformations.py b/test/adapters/binance/test_user_transformations.py new file mode 100644 index 0000000..d833186 --- /dev/null +++ b/test/adapters/binance/test_user_transformations.py @@ -0,0 +1,75 @@ +from algorunner.events import UpdateType + +from test.helpers import * +from test.adapters.binance.fixtures import * + + +_FIXTURE_PATTERN = "test/fixtures/binance/{fixture}.json" +ACCOUNT_UPDATE_PATTERN = _FIXTURE_PATTERN.format(fixture="account") +BALANCE_UPDATE_PATTERN = _FIXTURE_PATTERN.format(fixture="balance_update") +EXECUTION_REPORT_PATTERN = _FIXTURE_PATTERN.format(fixture="execution_report") +ACCOUNT_INFORMATION_PATTERN = _FIXTURE_PATTERN.format(fixture="outbound_account_info") +ACCOUNT_POSITION_PATTERN = _FIXTURE_PATTERN.format(fixture="outbound_account_position") + + +def check_position(positions, symbol, free, locked): + assert symbol in positions + assert positions[symbol].Free == free + assert positions[symbol].Locked == locked + + +def test_account_transformation(user_transformer, load_fixture): + with load_fixture(ACCOUNT_UPDATE_PATTERN) as account_payload: + transformed = user_transformer.initial_rest_payload(account_payload) + + assert not transformed['CanDeposit'] + assert transformed['CanTrade'] + assert transformed['CanWithdraw'] + + assert len(transformed['Positions']) == 2 + check_position(transformed['Positions'], 'BTC', 4723846.89208129, 0.0) + check_position(transformed['Positions'], 'LTC', 4763368.68006011, 0.0) + + +def test_stream_balance_update_transformation(user_transformer, load_fixture): + with load_fixture(BALANCE_UPDATE_PATTERN) as balance: + update_type, update_object = user_transformer(balance) + + assert update_type == UpdateType.BALANCE + assert update_object['Asset'] == 'BTC' + assert update_object['Update'] == 100.0 + + +def stream_execution_report_transformation(user_transformer, load_fixture): + with load_fixture(EXECUTION_REPORT_PATTERN) as execution: + update_type, update_object = user_transformer(execution) + + pass # @todo - transformation not implemented + + +def test_stream_account_info_transformation(user_transformer, load_fixture): + with load_fixture(ACCOUNT_INFORMATION_PATTERN) as information: + update_type, update_object = user_transformer(information) + + assert update_type == UpdateType.ACCOUNT + + assert update_object['CanDeposit'] + assert update_object['CanTrade'] + assert update_object['CanWithdraw'] + + assert len(update_object['Positions']) == 5 + check_position(update_object['Positions'], 'LTC', 17366.18538083, 0.0) + check_position(update_object['Positions'], 'BTC', 10537.85314051, 2.19464093) + check_position(update_object['Positions'], 'ETH', 17902.35190619, 0.0) + check_position(update_object['Positions'], 'BNC', 1114503.29769312, 0.0) + check_position(update_object['Positions'], 'NEO', 0.0, 0.0) + + +def stream_account_position_transformation(user_transformer, load_fixture): + with load_fixture(ACCOUNT_POSITION_PATTERN) as position: + update_type, update_object = user_transformer(position) + + assert update_type == UpdateType.POSITION + assert "BTC" in update_object + assert update_object["BTC"].Free + assert update_object["BTC"].Locked diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/account.json b/test/fixtures/binance/account.json similarity index 100% rename from test/fixtures/account.json rename to test/fixtures/binance/account.json diff --git a/test/fixtures/balance_update.json b/test/fixtures/binance/balance_update.json similarity index 100% rename from test/fixtures/balance_update.json rename to test/fixtures/binance/balance_update.json diff --git a/test/fixtures/execution_report.json b/test/fixtures/binance/execution_report.json similarity index 100% rename from test/fixtures/execution_report.json rename to test/fixtures/binance/execution_report.json diff --git a/test/fixtures/outbound_account_info.json b/test/fixtures/binance/outbound_account_info.json similarity index 100% rename from test/fixtures/outbound_account_info.json rename to test/fixtures/binance/outbound_account_info.json diff --git a/test/fixtures/outbound_account_position.json b/test/fixtures/binance/outbound_account_position.json similarity index 100% rename from test/fixtures/outbound_account_position.json rename to test/fixtures/binance/outbound_account_position.json diff --git a/test/fixtures/invalid_strategy.py b/test/fixtures/invalid_strategy.py new file mode 100644 index 0000000..61af4a8 --- /dev/null +++ b/test/fixtures/invalid_strategy.py @@ -0,0 +1,4 @@ + +class InvalidStrategy(): + # strategy doesn't implement required base class or methods + pass \ No newline at end of file diff --git a/test/fixtures/valid_strategy.py b/test/fixtures/valid_strategy.py new file mode 100644 index 0000000..6cd8386 --- /dev/null +++ b/test/fixtures/valid_strategy.py @@ -0,0 +1,5 @@ +from algorunner.abstract.strategy import Strategy + +class ValidStrategy(Strategy): + def process(self, tick): + return True \ No newline at end of file diff --git a/test/helpers.py b/test/helpers.py new file mode 100644 index 0000000..9af5c26 --- /dev/null +++ b/test/helpers.py @@ -0,0 +1,14 @@ +from contextlib import contextmanager +from contextlib import contextmanager +from json import load +from pytest import fixture + + +@fixture +def load_fixture(): + @contextmanager + def open_fixture(payload_file): + with open(payload_file) as json_file: + yield load(json_file) + + return open_fixture diff --git a/test/scenarios/account_updates.feature b/test/scenarios/account_updates.feature new file mode 100644 index 0000000..e23beaa --- /dev/null +++ b/test/scenarios/account_updates.feature @@ -0,0 +1,50 @@ +Feature: Account State + The Account object manages state associated with an exchange user. + The Account must be able to handle updates taken from user data, as well as + coordinate with the Calculator and API Adapter to make transactions. + + Background: + Given an account with no initial state + and that account is currently awaiting messages + + Scenario: Stay synchronised with account updates + Given an account update with full capabilities + When that account update is processed + Then the account should have full capabilities + Given an account update with minimal capabilities + When that account update is processed + Then the account should have minimal capabilities + + Scenario: Stay synchronised with balance updates + Given a BTC balance of 50 free and 25 locked + And a balance update of 20 BTC + When that balance update is processed + Then the account should have a balance of 70 BTC free + Given a balance update of -30 BTC + When that balance update is processed + Then the account should have a balance of 40 BTC free + + Scenario: Stay synchronised with position updates + Given an account position of BTC at 10 free and 25 locked + and a position update of ETH at 20 free and 50 locked + When that position update is processed + Then there should be a BTC balance of 10 free and 25 locked + and there should be a ETH balance of 20 free and 50 locked + and there should be a total of 2 balances + Given a position update of BTC at 5 free and 30 locked + When that position update is processed + Then there should be a BTC balance of 5 free and 30 locked + and there should be a ETH balance of 20 free and 50 locked + and there should be a total of 2 balances + + Scenario: Process orders requests approved by the calculator + Given a market order to buy BTC + and the calculator will accept the order with a size of 0.0032 + When the order event is processed + Then the API should recieve an order of 0.0032 BTC + + Scenario: Skip order requests rejected by the calculator + Given a market order to buy BTC + and the calculator will reject the order + When the order event is processed + Then the API should not recieve any orders diff --git a/test/scenarios/steps/account.py b/test/scenarios/steps/account.py new file mode 100644 index 0000000..882b0f3 --- /dev/null +++ b/test/scenarios/steps/account.py @@ -0,0 +1,94 @@ +from behave import * + +from algorunner.events import ( + AccountStatus, UpdateType +) +from algorunner.trader import Trader + +@given("an account with no initial state") +def fresh_account(context): + context.trader = Trader() + +@given("that account is currently awaiting messages") +def account_running(context): + pass + +@given("an account update with {capabilities} capabilities") +def account_update_full_capabilities(context, capabilities): + hasPermission = (capabilities == "full") + context.account_update = AccountStatus( + CanWithdraw=hasPermission, + CanTrade=hasPermission, + CanDeposit=hasPermission + ) + +@when("that account update is processed") +def account_update_processed(context): + context.trader(UpdateType.ACCOUNT, context.account_update) + +@then("the account should have {capabilities} capabilities") +def account_has_full_capabilities(context, capabilities): + hasPermission = (capabilities == "full") + assert context.trader.status["CanWithdraw"] == hasPermission + assert context.trader.status["CanTrade"] == hasPermission + assert context.trader.status["CanDeposit"] == hasPermission + +@given("a {symbol} balance of {free} free and {locked} locked") +def current_balance(context, symbol, free, locked): + pass + +@given("a balance update of {quantity:g} {symbol}") +def balance_update(context, quantity, symbol): + pass + +@when("that balance update is processed") +def balance_updated_processed(context): + pass + +@then("the account should have a balance of {balance:d} {symbol} free") +def balance_for_symbol(context, balance, symbol): + pass + +@given("an account position of {symbol} at {free:d} free and {locked:d} locked") +def account_with_balance(context, symbol, free, locked): + pass + +@given("a position update of {symbol} at {free:d} free and {locked:d} locked") +def position_update(context, symbol, free, locked): + pass + +@when("that position update is processed") +def position_update_processed(context): + pass + +@then("there should be a {symbol} balance of {free:d} free and {locked:d} locked") +def check_symbol_balance(context, symbol, free, locked): + pass + +@then("there should be a total of {count:d} balances") +def check_balance_count(context, count): + pass + +@given("a market order to buy {symbol}") +def market_order(context, symbol): + pass + +@given("the calculator will reject the order") +def calculator_rejection(context): + pass + +@given("the calculator will accept the order with a size of {size:g}") +def calculator_accepted(context, size): + pass + +@when("the order event is processed") +def order_event_process(context): + pass + +@then("the API should recieve an order of {quantity:g} {symbol}") +def check_for_order(context, quantity, symbol): + pass + +@then("the API should not recieve any orders") +def check_no_orders_made(context): + pass diff --git a/test/test_account.py b/test/test_account.py new file mode 100644 index 0000000..91263b4 --- /dev/null +++ b/test/test_account.py @@ -0,0 +1,3 @@ + +def test_tests_are_running(): + assert True == True diff --git a/test/account.py b/test/test_account.pyold similarity index 100% rename from test/account.py rename to test/test_account.pyold diff --git a/test/test_runner.py b/test/test_runner.py new file mode 100644 index 0000000..91263b4 --- /dev/null +++ b/test/test_runner.py @@ -0,0 +1,3 @@ + +def test_tests_are_running(): + assert True == True diff --git a/test/runner.py b/test/test_runner.pyold similarity index 100% rename from test/runner.py rename to test/test_runner.pyold diff --git a/test/test_strategy.py b/test/test_strategy.py new file mode 100644 index 0000000..b8a5fb9 --- /dev/null +++ b/test/test_strategy.py @@ -0,0 +1,27 @@ +import pytest + +from algorunner.strategy import * + + +def test_default_strategies_module(): + strategy = load_strategy('Example') + assert strategy._testing_tag + + +def test_custom_strategies_module(): + strategy = load_strategy('ValidStrategy', 'test.fixtures.valid_strategy') + assert strategy.process(None) + + +@pytest.mark.parametrize("module, strategy, exception", [ + ('Ehe', 'definitely.not.a.real.module', StrategyNotFound), + ('InvalidStrategy', 'test.fixtures.invalid_strategy', InvalidStrategyProvided), +]) +def test_no_strategy_module_availabe(module, strategy, exception): + correct_exception = False + try: + load_strategy(module, strategy) + except exception: + correct_exception = True + + assert correct_exception