diff --git a/.behaverc b/.behaverc new file mode 100644 index 0000000..e66490e --- /dev/null +++ b/.behaverc @@ -0,0 +1,3 @@ +[behave] +format=plain +paths=test/scenarios diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..558ad7f --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 120 +exclude=test/* diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index bb475ac..6925ef6 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -1,29 +1,32 @@ # 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 - - name: Run linter - run: | - make lint - - name: Run tests + - name: Run linting and tests run: | - make test + make ci diff --git a/.gitignore b/.gitignore index 1307c5e..440b3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/ +.todo __pycache__/ -bot.ini \ No newline at end of file +bot.ini +plain.output diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1 @@ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..22d83ed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +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", "local" ] + +# @todo - secondary layer with development dependencies removed diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0d9e89d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2021 [@FergusInLondon ] + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index 9e7dd32..5574922 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,39 @@ -.PHONY: deps test +.PHONY: help env-check build lint deps test ci run todo docs -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 +help: ## Show this help. + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' -deps: - pip install pandas - pip install python-binance - pip install flake8 +env-check: ## Check that the current environment is capable of running AlgoRunner. + @sh setup.sh -test: - python -m test.account - python -m test.runner \ No newline at end of file +build: ## Build docker image, tagged "algorunner:" and "algorunner:latest" + docker build -t algorunner:latest -t algorunner:`git rev-parse --short HEAD` . + +fix: ## Attempt to fix linting issues with `black` + poetry run black algorunner + +lint: ## Run code quality checks + poetry run black --check algorunner + poetry run flake8 + +deps: env-check ## Install all required dependencies (including for development) + poetry install --no-interaction + +test: ## Run all tests - including both unit tests and BDD scenarios + poetry run pytest + poetry run behave + +ci: lint test ## Run both linting and testing + @echo "finished running CI tasks" + +run: ## Run AlgoRunner + poetry run python run.py + +docs: ## Generate API documentation using "pdoc" + poetry run pdoc -o ./docs algorunner + rm docs/index.html + mv docs/algorunner.html docs/index.html + +todo: ## Scan the codebase for items tagged with "@todo" + @grep -r "@todo" --exclude=\*.pyc algorunner + @echo "\nTotal items marked '@todo': `grep --exclude=\*.pyc -r '@todo' . | wc -l | xargs`." diff --git a/README.md b/README.md index bc7fb6f..c9c88c3 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,113 @@ -# Runner ![Runner](https://github.com/FergusInLondon/Runner/workflows/Runner/badge.svg) +# AlgoRunner -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. +A lightweight service for running algorithmic trading strategies against cryptocurrency exchanges. Currently under heavy development and defining the exchange interactions, as well moving towards support for multiple exchanges. -Currently it does invoke a strategy and provides it with real-time streamed data from Binance though. +All development is done against the `develop` branch, although at this time that's likely the branch you actually want to browse. -### 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. +| Branch | Status | +| ------- | ------------------------------------------------------------ | +| Master | ![Unit Tests & Build](https://github.com/FergusInLondon/Runner/actions/workflows/pythonapp.yml/badge.svg)![CodeQL](https://github.com/FergusInLondon/Runner/actions/workflows/codeql-analysis.yml/badge.svg)[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) | +| Develop | ![Unit Tests & Build](https://github.com/FergusInLondon/Runner/actions/workflows/pythonapp.yml/badge.svg?branch=develop)![CodeQL](https://github.com/FergusInLondon/Runner/actions/workflows/codeql-analysis.yml/badge.svg?branch=develop)[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) | -## Example +### Living Design Doc -Check `example.py` for a runnable version of this strategy: +... + +## Defining a strategy + +To define a strategy to execute you simply need to define a `Strategy` class and place it in the `./strategies` folder where it can be loaded. A strategy **must** inherit from `BaseStrategy` and **must** implement two methods: `process` and `authorise`. ```python -class ExampleStrategy(object): - """ - A simple example strategy that computes the average price change over - the previous 5 2000ms updates. - """ +import pandas as pd + +from algorunner.abstract import BaseStrategy +from algorunner.abstract.base_strategy import ( + AccountState, TransactionRequest, AuthorisationDecision +) + - def start(self, control): - self.series = pd.DataFrame() - self.control = control +class Example(BaseStrategy): + def __init__(self): + self.series = pd.DataFrame + super().__init__() - def process(self, kline): - self.series = self.series.append(kline) + def process(self, tick: pd.DataFrame): + """process accepts a DataFrame containing the latest tick data.""" + self.series = self.series.append(tick) if self.series.shape[0] > 5: - print("Average price change over past 5 windows: ", pd.to_numeric(self.series[-5:]["PriceChange"]).mean()) + recent_window = pd.to_numeric(self.series[-5:]["PriceChange"]) + print("Average price change over past 5 windows: ", recent_window.mean()) + + def authorise(self, state: AccountState, trx: TransactionRequest) -> AuthorisationDecision: + """authorise is used to perform any risk calculations and position sizing.""" + pass ``` -When executed via the runner, this will calculate the average price change over the past 5 2000ms updates, and display it to the user. +From the `BaseStrategy` class you can interact with the market via calling `self.open_position(symbol: str)` and `self.close_position(symbol: str)` - this will subsequently be passed through to the `authorise(...)` call which will determine whether that interaction is allowed, whether it fits in with the users defined approach to risk, and what size the that position should be. Under the hood this is all handles via events. + +For information on the classes used - i.e. `AuthorisationDecision`, `TransactionRequest`, and `AccountState` - please see the [API documentation](https://fergusinlondon.github.io/Runner/). + +## Required Configuration + +Configuration can be done via: a `.ini` file, environment variables, or a combination of both. ``` -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 +[credentials] +exchange = binance # Identifier of the target exchange. +api_key = binanceAPIKey # API Key for the exchange +api_secret = binanceAPISecret # API Secret for the exchange + +[strategy] +name = Example # Strategy to execute +symbol = BTCUSDT # Symbol to execute the strategy against +``` + +By default AlgoRunner will try and read a file named `bot.ini`, but this can be overridden by the `--config` flag: + ``` +$ python run.py --config [config .ini file] +``` + +Alternatively, configuration can also be done via: a `.ini` file, environment variables, or a combination of both. + +| `.ini` variable | environment variable | CLI flag | +| ---------------------- | --------------------- | ---------------- | +| credentials.exchange | ALGORUNNER_EXCHANGE | --exchange | +| credentials.api_key | ALGORUNNER_API_KEY | --api-key | +| credentials.api_secret | ALGORUNNER_API_SECRET | --api-secret | +| strategy.name | - | -s / --strategy | +| strategy.symbol | - | --trading-symbol | + +**It's advisable not to pass any exchange details - i.e. `credentials.*` variables - via either the configuration file or the CLI!** + +## Executing AlgoRunner + +There are to methods to run AlgoRunner: the recommended way is via Docker. + +### Using Docker + +To build a Docker Image simply run `make docker` from the root of this repository; this will create an image with the tags `algorunner:` and `algorunner:latest`, before running the image. **There are no pre-built Docker Images available.** + +``` +$ make docker # that's genuinely it, I promise. +``` + +### Running Locally + +To run the service locally it's also relatively trivial: + +``` +$ make deps # this will install all dependencies +$ make local # this will run the service in the environment provided by poetry +``` + +Note: you **must** have `poetry` - a python dependency manager - installed on your system to run AlgoRunner. If you don't then the `deps` target of the Makefile will *attempt* to install it on your behalf. ---- +## Development -## August 2021 Update: AlgoRunner V2.0 +For details on development please see `DEVELOPMENT.md`, and for details on the automated test suite please see `test/TESTING.md`. -A preview of this release is available in the [`develop`](https://github.com/FergusInLondon/Runner/tree/develop) branch; and progress tracking is available via the [Version 2 Project Board](https://github.com/FergusInLondon/Runner/projects/1). +## License -This version aims to introduce a new design to allow easier exchange API interactions, concurrent processing of market orders, user definable algorithms *and* risk calculations, and improved dependency management. +This software is licensed by the terms outlined in the [*The MIT License*](https://opensource.org/licenses/MIT). For the full and entire text of this license please see `LICENSE.txt`. diff --git a/algorunner/__init__.py b/algorunner/__init__.py new file mode 100644 index 0000000..63a447d --- /dev/null +++ b/algorunner/__init__.py @@ -0,0 +1,21 @@ +""" + .. include:: pdoc.md +""" + +from algorunner.strategy.base import BaseStrategy +from algorunner.adapters.base import Adapter +from algorunner.hooks import ( + Hook, InvalidHookHandler, hook, clear_handlers +) +from algorunner.monitoring import Timer + +__docformat__ = "restructuredtext" +__all__ = [ + 'BaseStrategy', + 'Adapter', + 'Hook', + 'InvalidHookHandler', + 'hook', + 'clear_handlers', + 'Timer', +] \ No newline at end of file diff --git a/algorunner/adapters/__init__.py b/algorunner/adapters/__init__.py new file mode 100644 index 0000000..1ba3d99 --- /dev/null +++ b/algorunner/adapters/__init__.py @@ -0,0 +1,11 @@ +""" + +Verification of the functionality of an Adapter should be done in a similar fashion to the +Gherkin specification for the binance adapter. +""" + +from algorunner.adapters.messages import * # noqa: F401, F403 +from algorunner.adapters.base import ( # noqa: F401 + Adapter, + factory, +) diff --git a/algorunner/adapters/_backtest.py b/algorunner/adapters/_backtest.py new file mode 100644 index 0000000..8d60fd2 --- /dev/null +++ b/algorunner/adapters/_backtest.py @@ -0,0 +1,50 @@ +from csv import reader +from threading import Thread +from typing import Callable, List + +from algorunner.adapters.base import Adapter, register_adapter +from algorunner.adapters.messages import ( + Credentials, + TransactionRequest, +) + + +def transform_csv(row: List[str]) -> any: + pass + + +@register_adapter +class BacktestAdapter(Adapter): + + identifier = "backtest" + + def connect(self, creds: Credentials): + # We abuse the Credentials object here: and use the given parameters + # to determine filenames. + self.datafile = creds.exchange + self.outfile = creds.key + + def monitor_user(self): + # We wont provide user updates in backtest mode. This may be cause + # some functionality problems though (i.e. when `authorise()` relies + # upon account status). + pass + + def run(self, symbol: str, process: Callable): + # + # + # + def process_file(): + with open(self.datafile, "r") as csv: + for row in reader(csv): + process(transform_csv(row)) + + self.thread = Thread(target=process_file, daemon=True) + self.thread.start() + + def execute(self, trx: TransactionRequest) -> bool: + pass + + def disconnect(self): + if self.thread.is_alive(): + self.thread.join() diff --git a/algorunner/adapters/_binance.py b/algorunner/adapters/_binance.py new file mode 100644 index 0000000..a5f5880 --- /dev/null +++ b/algorunner/adapters/_binance.py @@ -0,0 +1,196 @@ +from typing import Callable + +from binance.client import Client +from binance import BinanceSocketManager + +import pandas as pd + +from algorunner.adapters.base import register_adapter, Adapter + +from algorunner.adapters.messages import ( + Credentials, + InvalidOrder, + InvalidPayloadError, + OrderType, + TransactionRequest, +) + +from algorunner.mutations import ( + AccountUpdate, + BaseUpdate, + BalanceUpdate, + CapabilitiesUpdate, + Position, +) + + +@register_adapter +class BinanceAdapter(Adapter): + """ """ + + identifier = "binance" + + class MarketStreamRawTransformer: + pass + + 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) -> BaseUpdate: + try: + message_map = { + "outboundAccountInfo": self.account_update, + "outboundAccountPosition": self.position_update, + "balanceUpdate": self.balance_update, + "executionReport": self.order_report, + } + + # what if we need to return multiple? i.e CapabilitiesUpdate AND AccountUpdate + return message_map[payload["e"]](payload) + except KeyError: + msg = "unknown payload type {p}".format(p=payload.get("e")) + raise InvalidPayloadError(msg) + except Exception as e: + raise Exception("unknown error occured in user stream", e) + + def initial_rest_payload(self, payload) -> CapabilitiesUpdate: + # @todo - there is so much data in this payload that we're missing + # out on, like commission rates etc. + return CapabilitiesUpdate( + can_withdraw=payload["canWithdraw"], + can_trade=payload["canTrade"], + can_deposit=payload["canDeposit"], + positions=[ + Position( + asset=b["asset"], free=b["free"], locked=b["locked"] + ) + for b in payload["balances"] + ], + ) + + def account_update(self, payload) -> AccountUpdate: + return AccountUpdate( + balances=[ + Position( + asset=b["asset"], free=b["free"], locked=b["locked"] + ) + for b in payload["B"] + ] + ) + + def balance_update(self, payload) -> BalanceUpdate: + return BalanceUpdate(asset=payload["a"], delta=payload["d"]) + + def position_update(self, payload): + """@todo + return Message( + Type=MessageType.UPDATE_POSITION, + Msg={ + balance["a"]: Position( + Locked=float(balance["l"]), + Free=float(balance["f"]) + ) for balance in payload["B"] + } + ) + """ + pass + + def order_report(self, payload): + # @todo - never did work out how to handle these. + pass + + def order_endpoint(self, payload): + # @todo - parse the result from the order endpoint. + pass + + def connect(self, creds: Credentials): + self.client = Client(creds["key"], creds["secret"]) + self.socket_manager = BinanceSocketManager(self.client) + + self.user_transformer = self.UserStreamEventTransformer() + self.market_transformer = ( + self.MarketStreamPandasTransformer() + if self.use_pandas + else self.MarketStreamRawTransformer() + ) + + def monitor_user(self): + # get initial account state from the API and dispatch associated event + self.sync_queue.put( + self.transformer.initial_rest_payload(self.client.get_account()) + ) + + # subscribe to all subsequent user events + self.user_conn_key = self.socket_manager.start_user_socket( + lambda p: self.sync_queue.put(self.user_transformer(p)) + ) + + def run(self, symbol: str, process: Callable): + self.market_conn_key = self.socket_manager.start_symbol_ticker_socket( + symbol, lambda p: process(self.market_transformer(p)) + ) + + def execute(self, trx: TransactionRequest): + trx.validate() + + kwargs = { + "symbol": trx.symbol, + "quantity": trx.quantity, + } + + if trx.is_limit(): + kwargs["price"] = trx.price + + dispatcher = { + OrderType.LIMIT_BUY: self.binance.order_limit_buy, + OrderType.LIMIT_SELL: self.binance.order_limit_sell, + OrderType.MARKET_BUY: self.binance.order_market_buy, + OrderType.MARKET_SELL: self.binance.order_market_sell, + }.get(trx.order_type) + + if not dispatcher: + raise InvalidOrder("invalid order type") + + order_response = dispatcher(**kwargs) + return self.user_transformer.order_endpoint(order_response) + + def disconnect(self): + self.socket_manager.stop_socket(self.market_conn_key) + self.socket_manager.stop_socket(self.user_conn_key) + self.socket_manager.close() diff --git a/algorunner/adapters/_sample.py b/algorunner/adapters/_sample.py new file mode 100644 index 0000000..fcf31f0 --- /dev/null +++ b/algorunner/adapters/_sample.py @@ -0,0 +1,27 @@ +from typing import Callable +from algorunner.adapters.base import Adapter, register_adapter +from algorunner.adapters.messages import ( + Credentials, + TransactionRequest, +) + + +@register_adapter +class SampleAdapter(Adapter): + + identifier = "sample" + + def connect(self, creds: Credentials): + pass + + def monitor_user(self): + pass + + def run(self, process: Callable, terminated: bool): + pass + + def execute(self, trx: TransactionRequest) -> bool: + pass + + def disconnect(self): + pass diff --git a/algorunner/adapters/base.py b/algorunner/adapters/base.py new file mode 100644 index 0000000..804bb08 --- /dev/null +++ b/algorunner/adapters/base.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod +from typing import Callable +from queue import Queue + +from loguru import logger + +from algorunner.adapters import messages + + +class Adapter(ABC): + """Required interface that an exchange adapter must implement.""" + + def __init__(self, sync_queue: Queue): + self.sync_queue = sync_queue + + @abstractmethod + def connect(self, creds: messages.Credentials): + """connect authenticates with the exchange, and also populates + the associated `Trader` object with the latest state.""" + pass + + @abstractmethod + def monitor_user(self): + """@todo""" + pass + + @abstractmethod + def run(self, symbol: str, process: Callable): + """run executes the underlying strategy, ensuring that any data + transformation required is carried out correctly.""" + pass + + @abstractmethod + def execute(self, trx: messages.TransactionRequest) -> bool: + pass + + @abstractmethod + def disconnect(self): + pass + + +_available_adapters = {} + + +def register_adapter(cls): + logger.debug("registering new adapter...") + try: + identifier = getattr(cls, "identifier") + except AttributeError: + raise messages.AdapterError( + f"cannot find identifier for adapter class: {cls.__name__}" + ) + + if hasattr(_available_adapters, identifier): + raise messages.AdapterError( + f"attempt at registering duplicate adapter for identifier: {identifier}" + ) + + _available_adapters[cls.identifier] = cls + logger.debug(f"registered new adapter: '{identifier}'") + return cls + + +def factory(requested_adapter: str, *args, **kwargs) -> Adapter: + logger.info(f"instantiating adapter for '{requested_adapter}'") + cls = _available_adapters.get(requested_adapter) + if not cls: + raise messages.AdapterError( + f"no adapter registered for identifier {requested_adapter}" + ) + + return cls(*args, **kwargs) diff --git a/algorunner/adapters/messages.py b/algorunner/adapters/messages.py new file mode 100644 index 0000000..61711ad --- /dev/null +++ b/algorunner/adapters/messages.py @@ -0,0 +1,77 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Optional, Union + +import pandas as pd + + +class OrderType(Enum): + MARKET_SELL = 1 + LIMIT_SELL = 2 + MARKET_BUY = 3 + LIMIT_BUY = 4 + + +@dataclass +class RawTickPayload: + pass + + +Tick = Union[pd.DataFrame, RawTickPayload] + + +class AdapterError(Exception): + pass + + +class InvalidOrder(Exception): + """Is LIMIT without price? @todo""" + + pass + + +class InvalidPayloadError(Exception): + """InvalidPayloadError is thrown when an invalid message is recieved + from the exchange via a websocket stream.""" + + pass + + +@dataclass +class Credentials: + """Required credentials to authenticate with a given exchange.""" + + exchange: str + key: str + secret: str + + +@dataclass +class TransactionRequest: + """Dispatched by `process` this triggers risk calculation via `authorise` + and potential dispatch of a transaction to the exchange.""" + + reason: Optional[str] + symbol: str + order_type: OrderType + quantity: float + price: Optional[float] + approved: bool = False + run_test: bool = True + + def is_limit(self) -> bool: + return self.order_type in [OrderType.LIMIT_SELL, OrderType.LIMIT_BUY] + + def validate(self): + if self.is_limit() and not self.price: + raise InvalidOrder("limit order requires a price") + + if not all([self.symbol, self.order_type, self.quantity]): + raise InvalidOrder( + "order requires all of symbol, order_type, and quantity" + ) + + +@dataclass +class OrderStatus: + pass diff --git a/algorunner/exceptions.py b/algorunner/exceptions.py new file mode 100644 index 0000000..599e386 --- /dev/null +++ b/algorunner/exceptions.py @@ -0,0 +1,48 @@ +""" +Very few - if any - of these are relevant to user-defined strategies; so +are perhaps worthy of *not* including in the `pdoc` docs? Alternatively, +do we import *all* user required objects in to `algorunner/__init__.py` +and coordinate the docs in one place, *and* simplify imports for strategies? +""" +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" + + +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]] = None): + self.message = ( + MSG_INVALID_CONFIG + if not invalid_fields + else MSG_INVALID_CONFIG_W_FIELDS.format( + fields=invalid_fields.join(", ") + ) + ) + + +class NoBalanceAvailable(Exception): + """ + Triggered when the an attempt to access a balance that does not exist + occurs. + """ + + def __init__(self, symbol): + self.message = f"no balance available for '{symbol}'" + + +class InvalidUpdate(Exception): + """ + Triggered when an Update is recieved but there's it's missing a required + property + """ + + def __init__(self, prop: str, update_type: str): + self.message = ( + f"missing '{prop}' in update on update type '{update_type}'" + ) diff --git a/algorunner/hooks.py b/algorunner/hooks.py new file mode 100644 index 0000000..ded0848 --- /dev/null +++ b/algorunner/hooks.py @@ -0,0 +1,93 @@ +from algorunner.adapters.messages import TransactionRequest +from enum import Enum +from typing import Callable, Optional + +from loguru import logger + + +class Hook(Enum): + """Hook represents valid hooks for user-defined functions to listen + for.""" + + RUNNER_INITIALISED = 1 + RUNNER_STARTING = 2 + RUNNER_STOPPING = 3 + ORDER_REQUEST = 4 + API_EXECUTE_DURATION = 5 + PROCESS_DURATION = 6 + + +class InvalidHookHandler(Exception): + """Raised when `hook_handler` is unable to register a given hook.""" + + pass + + +CALLBACK_TYPES = { + Hook.PROCESS_DURATION: Callable[[float], None], + Hook.API_EXECUTE_DURATION: Callable[[float], None], + Hook.ORDER_REQUEST: Callable[[TransactionRequest], None], + Hook.RUNNER_STOPPING: Callable[[], None], + Hook.RUNNER_STARTING: Callable[[], None], + Hook.RUNNER_INITIALISED: Callable[[], None], +} + +# @todo We have a few of these registry decorations now; place in one class? +_registered_hooks = {} + + +def hook_handler(hook: Hook): + """`hook_handler` is a decorator to go wrap around a hook handler.""" + + def register(fn): + if not hook: + raise InvalidHookHandler(f"no hook specified for '{fn.__name}'") + + expected_callback = CALLBACK_TYPES.get(hook) + if not expected_callback: + raise InvalidHookHandler(f"unknown hook specified ('{hook}')") + + if not callable(fn): + raise InvalidHookHandler(f"invalid hook supplied for '{hook}") + + fn.__hook_handler__ = True + callbacks = _registered_hooks.get(hook, []) + callbacks.append(fn) + _registered_hooks[hook] = callbacks + + return register + + +def hook(hook: Hook, *args, **kwargs): + """`hook(...)` calls any handlers associated with a given Hook.""" + callbacks = _registered_hooks.get(hook, []) + for cb in callbacks: + try: + cb(*args, **kwargs) + except TypeError: + logger.error(f"invalid handler ({cb.__name__}) for hook ({hook})") + + +def clear_handlers(hook: Optional[Hook] = None): + """`clear_handlers` clears registered handlers; optionally for + a specific hook""" + if hook and _registered_hooks.get(hook): + _registered_hooks[hook] = [] + return + + _registered_hooks.clear() + + +@hook_handler(hook=Hook.RUNNER_STARTING) +def handle_runner_starting(): + logger.info("runner initiation: monitoring streams and executing strategy") + + +@hook_handler(hook=Hook.RUNNER_STOPPING) +def handle_runner_stopping(): + logger.info("runner termination: closing streams and terminating strategy") + + +@hook_handler(hook=Hook.RUNNER_INITIALISED) +def handle_runner_initialisation(): + logger.info("runner is ready for execution") diff --git a/algorunner/monitoring.py b/algorunner/monitoring.py new file mode 100644 index 0000000..a874199 --- /dev/null +++ b/algorunner/monitoring.py @@ -0,0 +1,44 @@ +from algorunner.hooks import Hook, hook, hook_handler +from time import time +from typing import Optional + +from loguru import logger + +_PERFORMANCE_LOG_LEVEL = "performance" +logger.level(_PERFORMANCE_LOG_LEVEL, no=25, color="") + + +class Timer: + """Simple timer based context manager, used for performance monitoring + in conjunction with hooks.""" + + def __init__(self, trigger_hook: Optional[Hook] = None): + self.duration = None + self.hook = trigger_hook + + def __enter__(self): + self.start = time() + + def __exit__(self, exc_type, exc_val, traceback): + self.duration = time() - self.start + + if exc_type: + logger.error( + f"detected exception during monitoring: {exc_type} ({exc_val})" + ) + + if self.hook: + hook(self.hook, self.ms()) + + def ms(self) -> float: + return round(self.duration * 1000) + + +@hook_handler(hook=Hook.API_EXECUTE_DURATION) +def log_api_duration(duration: float): + logger.log(_PERFORMANCE_LOG_LEVEL, f"api call duration: {duration}") + + +@hook_handler(hook=Hook.PROCESS_DURATION) +def log_process_duration(duration: float): + logger.log(_PERFORMANCE_LOG_LEVEL, f"tick process duration: {duration}") diff --git a/algorunner/mutations.py b/algorunner/mutations.py new file mode 100644 index 0000000..c2e6398 --- /dev/null +++ b/algorunner/mutations.py @@ -0,0 +1,144 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Tuple + +from loguru import logger + +from algorunner.exceptions import NoBalanceAvailable, InvalidUpdate + + +class AccountState: + """Stores a snapshot of the state of the account linked to the current exchange.""" + + def __init__(self): + self.balances = {} + self.orders = {} + self.permissions = { + # assume true - as most likely case - until updated otherwise. + "can_withdraw": True, + "can_deposit": True, + "can_trade": True, + } + + def balance(self, asset: str) -> Tuple[float, float]: + balance = self.balances.get(asset) + if not balance: + raise NoBalanceAvailable(asset) + + return (balance.free, balance.locked) + + def capability(self, capability: str) -> bool: + return self.permissions.get(capability, False) + + +class BaseUpdate(ABC): + """BaseUpdate defines the interface an Update must adhere to.""" + + REQUIRED_PROPS = [] + + def __init__(self, **kwargs): + for prop in self.REQUIRED_PROPS: + if prop not in kwargs: + logger.error( + "invalid arguments supplied to update object!", + {"expected": self.REQUIRED_PROPS, "actual": kwargs}, + ) + raise InvalidUpdate(prop, self.__class__) + + self.__dict__.update(kwargs) + + @abstractmethod + def handle(self, state: AccountState) -> AccountState: + pass + + +@dataclass +class Position: + """Position signifies the position associated with a given asset.""" + + asset: str + free: float + locked: float + + +_available_updates = set() + + +def register_update(cls): + _available_updates.add(cls) + return cls + + +def is_update(cls): + return cls in _available_updates + + +@register_update +class OrderUpdate(BaseUpdate): + """Recieved when there are changes to an order associated with the account.""" + + REQUIRED_PROPS = ["symbol", "orderId", "side", "type", "status", "quantity"] + + def handle(self, state: AccountState) -> AccountState: + logger.info("recieved inbound update for pending transaction") + order = state.orders.get(self.order_id, {}) + # @todo use the | operator when Py 3.9 is fixed. + state.orders[self.order_id] = { + **order, + **{ + "symbol": self.symbol, + "side": self.side, + "type": self.type, + "status": self.status, + "quantity": self.quantity, + }, + } + return state + + +@register_update +class BalanceUpdate(BaseUpdate): + """An update containing an individual balance that has changed, + expressed as delta between balances.""" + + REQUIRED_PROPS = ["asset", "delta"] + + def handle(self, state: AccountState) -> AccountState: + logger.info(f"recieved inbound balance update for '{self.asset}'") + asset_balance = state.balances.get( + self.asset, Position(asset=self.asset, free=0, locked=0) + ) + + asset_balance.free += self.delta + state.balances[self.asset] = asset_balance + return state + + +@register_update +class AccountUpdate(BaseUpdate): + """An update containing any balances which have changed.""" + + REQUIRED_PROPS = ["balances"] # List[Position] + + def handle(self, state: AccountState) -> AccountState: + logger.info("recieved inbound user update") + for p in self.balances: + state.balances[p.asset] = p + + return state + + +@register_update +class CapabilitiesUpdate(BaseUpdate): + """The first update an account will recieve upon initialisation.""" + + REQUIRED_PROPS = ["can_withdraw", "can_trade", "can_deposit", "positions"] + + def handle(self, state: AccountState) -> AccountState: + logger.info("recieved inbound capabilities update for current user") + state.permissions["can_withdraw"] = self.can_withdraw + state.permissions["can_trade"] = self.can_trade + state.permissions["can_deposit"] = self.can_deposit + + position_update = AccountUpdate(balances=self.positions) + return position_update.handle(state) diff --git a/algorunner/pdoc.md b/algorunner/pdoc.md new file mode 100644 index 0000000..73a73cf --- /dev/null +++ b/algorunner/pdoc.md @@ -0,0 +1,28 @@ +> @todo: define *all* user required objects, and then import to `algorunner/__init__.py` +> to simplify document generation *and* developer experience. + +AlgoRunner is a simple framework that can be used to build algorithmic trading strategies against cryptocurrency exchanges. + +For information on the development, check the [repository](https://github.com/FergusInLondon/Runner). + +> **This documentation covers the objects that you'll need to interact with when using AlgoRunner; it doesn't necessarily cover the internals of the system.** + +## Using AlgoRunner + +AlgoRunner is simple: **Strategy** objects are executed against an exchange via an API **Adapter**. + +## Writing Strategies + +The abstract methods required to be implemented are located in `algorunner.BaseStrategy`. + +### Integration with "Hooks" + +AlgoRunner has the concept of "*hooks*": simple events that are dispatched containing performance metrics or status updates. See `algorunner.hooks` for information information about these. + +### Exceptions and Error Handling + +Check out the `algorunner.exceptions` module for more information. + +## Writing Adapters + +Adapters must inherit from `algorunner.adapters.base`. diff --git a/algorunner/runner.py b/algorunner/runner.py new file mode 100644 index 0000000..577bd0b --- /dev/null +++ b/algorunner/runner.py @@ -0,0 +1,44 @@ +from algorunner.hooks import Hook, hook +from queue import Queue +from signal import SIGTERM, signal + +from loguru import logger + +from algorunner.strategy import BaseStrategy +from algorunner.adapters import Credentials, factory + + +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, strategy: BaseStrategy): + self.sync_queue = Queue() + self.adapter = factory(creds.exchange, self.sync_queue) + self.strategy = strategy + + self.strategy.start_sync(self.sync_queue, self.adapter) + self.adapter.connect(creds) + signal(SIGTERM, self._handle_sigterm()) + hook(Hook.RUNNER_INITIALISED) + + def _handle_sigterm(self): + def _handler(signum, frame): + logger.warning("caught SIGTERM: attempting graceful termination") + self.stop() + + return _handler + + def run(self): + """ """ + self.adapter.monitor_user(self.trader_queue) + self.adapter.run(self.strategy, self.strategy) + hook(Hook.RUNNER_STARTING) + + def stop(self): + hook(Hook.RUNNER_STOPPING) + self.strategy.shutdown() + self.adapter.disconnect() diff --git a/algorunner/strategy/__init__.py b/algorunner/strategy/__init__.py new file mode 100644 index 0000000..788ced4 --- /dev/null +++ b/algorunner/strategy/__init__.py @@ -0,0 +1,14 @@ +""" +The `strategy` packages contains +""" + +from algorunner.strategy.base import BaseStrategy, ShutdownRequest # noqa: F401 +from algorunner.strategy.loader import load_strategy # noqa: F401 +from algorunner.strategy.exceptions import ( # noqa: F401 + FailureLoadingStrategy, + InvalidStrategyProvided, + StrategyExceptionThresholdBreached, + StrategyNotFound, +) + +__all__ = ['BaseStrategy'] \ No newline at end of file diff --git a/algorunner/strategy/base.py b/algorunner/strategy/base.py new file mode 100644 index 0000000..ce59650 --- /dev/null +++ b/algorunner/strategy/base.py @@ -0,0 +1,197 @@ +from abc import ABC, abstractmethod +from algorunner.hooks import Hook, hook, hook_handler +from queue import Queue +from threading import Thread +from typing import Callable, Dict, Optional + +from loguru import logger + +from algorunner.strategy.exceptions import StrategyExceptionThresholdBreached + +from algorunner.monitoring import Timer +from algorunner.mutations import AccountState, is_update +from algorunner.adapters.base import Adapter +from algorunner.adapters.messages import ( + InvalidOrder, + OrderType, + TransactionRequest, + Tick, +) + + +class ShutdownRequest: + """When recieved by the Sync Agent this triggers thread termination.""" + + def __init__(self, reason: str = "unknown reason"): + self.reason = reason + + +class BaseStrategy(ABC): + """ + A `BaseStrategy` is the container for an algorithm, it simply needs to respond + to incoming market payloads and be able to generate events for the internal + `SyncAgent` Actor which is responsible for synchronising state between the API + and the algorithm. (In this context "state" means transactions, balances and + positions) + """ + + class _SyncAgent: + def __init__(self, queue: Queue, adapter: Adapter, auth: Callable): + self.queue = queue + self.api = adapter + self.state = AccountState() + self.authorisation_guard = auth + + def start(self): + # @todo - do we *really* want it as a daemon; I see two arguments here. + # tests obviously *must* be run against a daemon. + self.thread = Thread(target=self._listen, daemon=True) + self.thread.start() + logger.debug("initiated sync agent") + + def stop(self, reason: Optional[str] = None): + logger.info(f"sync agent termination requested: '{reason}'") + self.queue.put(ShutdownRequest(reason)) + + self.thread.join() + logger.info("sync agent has halted.") + + def is_running(self) -> bool: + return self.thread.is_alive() + + def _listen(self): + logger.info("listening for events and inbound messages") + + exception_count = 0 # @todo count exceptions over past 5 mins. Probs a job for a contextmanager. + while True: + message = self.queue.get() + message_type = type(message) + + try: + if message_type == ShutdownRequest: + logger.warning( + f"terminating trader thread ({message.reason})." + ) + break + elif message_type == TransactionRequest: + logger.info( + "request recieved from strategy to execute a transaction" + ) + self._transaction_handler(message) + continue + elif not is_update(message_type): + logger.error("recieved message without known handler") + continue + + message.handle(self.state) + except Exception as e: + logger.error( + "sync agent has caught an exception. will try to continue.", + { + "exc": e, + "exc_count": exception_count, + }, + ) + + if exception_count > 5: + logger.critical( + "exception rate has breached threshold, failing.." + ) + raise StrategyExceptionThresholdBreached( + "too many exceptions encountered!" + ) + + logger.warn("syncagent has completed") + + def _transaction_handler(self, trx: TransactionRequest): + trx = self.authorisation_guard(self.state, trx) + if not trx.approved: + logger.info(f"transaction rejected: {trx.reason}") + return + + t = Timer(Hook.API_EXECUTE_DURATION) + with t: + try: + logger.info( + "transaction accepted: passing to API adapter for dispatch" + ) + self.api.execute(trx) + except InvalidOrder: + pass + + def __call__(self, tick: Tick): + t = Timer(Hook.PROCESS_DURATION) + with t: + self.process(tick) + + def start_sync(self, queue: Queue, adapter: Adapter): + self.sync_agent = self._SyncAgent(queue, adapter, self.log) + self.sync_queue = queue + + def _place_order(self, params: dict): + try: + params = TransactionRequest(**params) + self.sync_queue.put(params) + hook(Hook.ORDER_REQUEST, params) + except TypeError: + raise InvalidOrder( + "invalid parameters supplied when attempting order" + ) + + def order_sell_limit(self, **kwargs): + if not all([kwargs["symbol"], kwargs["price"], kwargs["quantity"]]): + raise InvalidOrder( + "order_sell_limit requires symbol, price, and quantity" + ) + + self._place_order({**kwargs, **{"order_type": OrderType.LIMIT_SELL}}) + + def order_sell_market(self, **kwargs): + if not all([kwargs["symbol"], kwargs["quantity"]]): + raise InvalidOrder("order_sell_market requires symbol and quantity") + + self._place_order({**kwargs, **{"order_type": OrderType.MARKET_SELL}}) + + def order_buy_limit(self, **kwargs): + if not all([kwargs["symbol"], kwargs["price"], kwargs["quantity"]]): + raise InvalidOrder( + "order_buy_limit requires symbol, price, and quantity" + ) + + self._place_order({**kwargs, **{"order_type": OrderType.LIMIT_BUY}}) + + def order_buy_market(self, **kwargs): + if not all([kwargs["symbol"], kwargs["quantity"]]): + raise InvalidOrder("order_buy_market requires symbol and quantity") + + self._place_order({**kwargs, **{"order_type": OrderType.MARKET_SELL}}) + + def shutdown(self): + self.sync_agent.stop("shutdown requested") + + def account_state(self) -> AccountState: + return self.sync_agent.account_state + + def register_hooks(self, hooks: Dict[Hook, Callable]): + def wrapper(fn): + def _wrapped(*args, **kwargs): + fn(*args, **kwargs) + + return _wrapped + + for h in hooks: + hook_handler(h)(wrapper(hooks[h])) + + def authorise( + self, state: AccountState, trx: TransactionRequest + ) -> TransactionRequest: + logger.info( + "no authorisation guard set: automatically authorising order" + ) + trx.approved = True + return trx + + @abstractmethod + def process(self, tick: Tick): + """ """ + pass diff --git a/algorunner/strategy/exceptions.py b/algorunner/strategy/exceptions.py new file mode 100644 index 0000000..d0446fb --- /dev/null +++ b/algorunner/strategy/exceptions.py @@ -0,0 +1,31 @@ +from typing import Optional + + +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 + + +class StrategyExceptionThresholdBreached(Exception): + pass diff --git a/algorunner/strategy/loader.py b/algorunner/strategy/loader.py new file mode 100644 index 0000000..f4708f1 --- /dev/null +++ b/algorunner/strategy/loader.py @@ -0,0 +1,42 @@ +from importlib import import_module +from typing import Optional + +from loguru import logger + +from algorunner.strategy import BaseStrategy +from algorunner.strategy.exceptions import ( + FailureLoadingStrategy, + InvalidStrategyProvided, + StrategyNotFound, +) + + +_DEFAULT_STRATEGY_PARENT_MODULE = "strategies.{module}" + + +def load_strategy( + strategy_name: str, module_name: Optional[str] = None +) -> BaseStrategy: + """Dynamically load strategies located in the `/strategies` directory""" + if not module_name: + logger.debug( + "using default module name - looking in strategies directory" + ) + 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, BaseStrategy): + 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/bot.example.ini b/bot.example.ini index 9675d50..f9441c9 100644 --- a/bot.example.ini +++ b/bot.example.ini @@ -1,6 +1,8 @@ [credentials] -ApiKey = binanceAPIKey -ApiSecret = binanceAPISecret +exchange = binance # Identifier of the target exchange. +api_key = binanceAPIKey # API Key for the exchange +api_secret = binanceAPISecret # API Secret for the exchange [strategy] -Symbol = BTCUSDT \ No newline at end of file +name = Example # Strategy to execute +symbol = BTCUSDT # Symbol to execute the strategy against diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..4fc0bbd --- /dev/null +++ b/docs/index.html @@ -0,0 +1,1084 @@ + + + + + + + algorunner API documentation + + + + + + + + +
+
+

+algorunner

+ +
+

@todo: define all user required objects, and then import to algorunner/__init__.py + to simplify document generation and developer experience.

+
+ +

AlgoRunner is a simple framework that can be used to build algorithmic trading strategies against cryptocurrency exchanges.

+ +

For information on the development, check the repository.

+ +
+

This documentation covers the objects that you'll need to interact with when using AlgoRunner; it doesn't necessarily cover the internals of the system.

+
+ +

Using AlgoRunner

+ +

AlgoRunner is simple: Strategy objects are executed against an exchange via an API Adapter.

+ +

Writing Strategies

+ +

The abstract methods required to be implemented are located in algorunner.BaseStrategy.

+ +

Integration with "Hooks"

+ +

AlgoRunner has the concept of "hooks": simple events that are dispatched containing performance metrics or status updates. See algorunner.hooks for information information about these.

+ +

Exceptions and Error Handling

+ +

Check out the algorunner.exceptions module for more information.

+ +

Writing Adapters

+ +

Adapters must inherit from algorunner.adapters.base.

+
+ +
+ View Source +
"""
+    .. include:: pdoc.md
+"""
+
+from algorunner.strategy.base import BaseStrategy
+from algorunner.adapters.base import Adapter
+from algorunner.hooks import (
+    Hook, InvalidHookHandler, hook, clear_handlers
+)
+from algorunner.monitoring import Timer
+
+__docformat__ = "restructuredtext"
+__all__ = [
+    'BaseStrategy',
+    'Adapter',
+    'Hook',
+    'InvalidHookHandler',
+    'hook',
+    'clear_handlers',
+    'Timer',
+]
+
+ +
+ +
+
+
+ #   + + + class + BaseStrategy(abc.ABC): +
+ +
+ View Source +
class BaseStrategy(ABC):
+    """
+    A `BaseStrategy` is the container for an algorithm, it simply needs to respond
+    to incoming market payloads and be able to generate events for the internal
+    `SyncAgent` Actor which is responsible for synchronising state between the API
+    and the algorithm. (In this context "state" means transactions, balances and
+    positions)
+    """
+
+    class _SyncAgent:
+        def __init__(self, queue: Queue, adapter: Adapter, auth: Callable):
+            self.queue = queue
+            self.api = adapter
+            self.state = AccountState()
+            self.authorisation_guard = auth
+
+        def start(self):
+            # @todo - do we *really* want it as a daemon; I see two arguments here.
+            # tests obviously *must* be run against a daemon.
+            self.thread = Thread(target=self._listen, daemon=True)
+            self.thread.start()
+            logger.debug("initiated sync agent")
+
+        def stop(self, reason: Optional[str] = None):
+            logger.info(f"sync agent termination requested: '{reason}'")
+            self.queue.put(ShutdownRequest(reason))
+
+            self.thread.join()
+            logger.info("sync agent has halted.")
+
+        def is_running(self) -> bool:
+            return self.thread.is_alive()
+
+        def _listen(self):
+            logger.info("listening for events and inbound messages")
+
+            exception_count = 0  # @todo count exceptions over past 5 mins. Probs a job for a contextmanager.
+            while True:
+                message = self.queue.get()
+                message_type = type(message)
+
+                try:
+                    if message_type == ShutdownRequest:
+                        logger.warning(
+                            f"terminating trader thread ({message.reason})."
+                        )
+                        break
+                    elif message_type == TransactionRequest:
+                        logger.info(
+                            "request recieved from strategy to execute a transaction"
+                        )
+                        self._transaction_handler(message)
+                        continue
+                    elif not is_update(message_type):
+                        logger.error("recieved message without known handler")
+                        continue
+
+                    message.handle(self.state)
+                except Exception as e:
+                    logger.error(
+                        "sync agent has caught an exception. will try to continue.",
+                        {
+                            "exc": e,
+                            "exc_count": exception_count,
+                        },
+                    )
+
+                    if exception_count > 5:
+                        logger.critical(
+                            "exception rate has breached threshold, failing.."
+                        )
+                        raise StrategyExceptionThresholdBreached(
+                            "too many exceptions encountered!"
+                        )
+
+            logger.warn("syncagent has completed")
+
+        def _transaction_handler(self, trx: TransactionRequest):
+            trx = self.authorisation_guard(self.state, trx)
+            if not trx.approved:
+                logger.info(f"transaction rejected: {trx.reason}")
+                return
+
+            t = Timer(Hook.API_EXECUTE_DURATION)
+            with t:
+                try:
+                    logger.info(
+                        "transaction accepted: passing to API adapter for dispatch"
+                    )
+                    self.api.execute(trx)
+                except InvalidOrder:
+                    pass
+
+    def __call__(self, tick: Tick):
+        t = Timer(Hook.PROCESS_DURATION)
+        with t:
+            self.process(tick)
+
+    def start_sync(self, queue: Queue, adapter: Adapter):
+        self.sync_agent = self._SyncAgent(queue, adapter, self.log)
+        self.sync_queue = queue
+
+    def _place_order(self, params: dict):
+        try:
+            params = TransactionRequest(**params)
+            self.sync_queue.put(params)
+            hook(Hook.ORDER_REQUEST, params)
+        except TypeError:
+            raise InvalidOrder(
+                "invalid parameters supplied when attempting order"
+            )
+
+    def order_sell_limit(self, **kwargs):
+        if not all([kwargs["symbol"], kwargs["price"], kwargs["quantity"]]):
+            raise InvalidOrder(
+                "order_sell_limit requires symbol, price, and quantity"
+            )
+
+        self._place_order({**kwargs, **{"order_type": OrderType.LIMIT_SELL}})
+
+    def order_sell_market(self, **kwargs):
+        if not all([kwargs["symbol"], kwargs["quantity"]]):
+            raise InvalidOrder("order_sell_market requires symbol and quantity")
+
+        self._place_order({**kwargs, **{"order_type": OrderType.MARKET_SELL}})
+
+    def order_buy_limit(self, **kwargs):
+        if not all([kwargs["symbol"], kwargs["price"], kwargs["quantity"]]):
+            raise InvalidOrder(
+                "order_buy_limit requires symbol, price, and quantity"
+            )
+
+        self._place_order({**kwargs, **{"order_type": OrderType.LIMIT_BUY}})
+
+    def order_buy_market(self, **kwargs):
+        if not all([kwargs["symbol"], kwargs["quantity"]]):
+            raise InvalidOrder("order_buy_market requires symbol and quantity")
+
+        self._place_order({**kwargs, **{"order_type": OrderType.MARKET_SELL}})
+
+    def shutdown(self):
+        self.sync_agent.stop("shutdown requested")
+
+    def account_state(self) -> AccountState:
+        return self.sync_agent.account_state
+
+    def register_hooks(self, hooks: Dict[Hook, Callable]):
+        def wrapper(fn):
+            def _wrapped(*args, **kwargs):
+                fn(*args, **kwargs)
+
+            return _wrapped
+
+        for h in hooks:
+            hook_handler(h)(wrapper(hooks[h]))
+
+    def authorise(
+        self, state: AccountState, trx: TransactionRequest
+    ) -> TransactionRequest:
+        logger.info(
+            "no authorisation guard set: automatically authorising order"
+        )
+        trx.approved = True
+        return trx
+
+    @abstractmethod
+    def process(self, tick: Tick):
+        """ """
+        pass
+
+ +
+ +

A BaseStrategy is the container for an algorithm, it simply needs to respond +to incoming market payloads and be able to generate events for the internal +SyncAgent Actor which is responsible for synchronising state between the API +and the algorithm. (In this context "state" means transactions, balances and +positions)

+
+ + +
+
#   + + + def + start_sync(self, queue: queue.Queue, adapter: algorunner.adapters.base.Adapter): +
+ +
+ View Source +
    def start_sync(self, queue: Queue, adapter: Adapter):
+        self.sync_agent = self._SyncAgent(queue, adapter, self.log)
+        self.sync_queue = queue
+
+ +
+ + + +
+
+
#   + + + def + order_sell_limit(self, **kwargs): +
+ +
+ View Source +
    def order_sell_limit(self, **kwargs):
+        if not all([kwargs["symbol"], kwargs["price"], kwargs["quantity"]]):
+            raise InvalidOrder(
+                "order_sell_limit requires symbol, price, and quantity"
+            )
+
+        self._place_order({**kwargs, **{"order_type": OrderType.LIMIT_SELL}})
+
+ +
+ + + +
+
+
#   + + + def + order_sell_market(self, **kwargs): +
+ +
+ View Source +
    def order_sell_market(self, **kwargs):
+        if not all([kwargs["symbol"], kwargs["quantity"]]):
+            raise InvalidOrder("order_sell_market requires symbol and quantity")
+
+        self._place_order({**kwargs, **{"order_type": OrderType.MARKET_SELL}})
+
+ +
+ + + +
+
+
#   + + + def + order_buy_limit(self, **kwargs): +
+ +
+ View Source +
    def order_buy_limit(self, **kwargs):
+        if not all([kwargs["symbol"], kwargs["price"], kwargs["quantity"]]):
+            raise InvalidOrder(
+                "order_buy_limit requires symbol, price, and quantity"
+            )
+
+        self._place_order({**kwargs, **{"order_type": OrderType.LIMIT_BUY}})
+
+ +
+ + + +
+
+
#   + + + def + order_buy_market(self, **kwargs): +
+ +
+ View Source +
    def order_buy_market(self, **kwargs):
+        if not all([kwargs["symbol"], kwargs["quantity"]]):
+            raise InvalidOrder("order_buy_market requires symbol and quantity")
+
+        self._place_order({**kwargs, **{"order_type": OrderType.MARKET_SELL}})
+
+ +
+ + + +
+
+
#   + + + def + shutdown(self): +
+ +
+ View Source +
    def shutdown(self):
+        self.sync_agent.stop("shutdown requested")
+
+ +
+ + + +
+
+
#   + + + def + account_state(self) -> algorunner.mutations.AccountState: +
+ +
+ View Source +
    def account_state(self) -> AccountState:
+        return self.sync_agent.account_state
+
+ +
+ + + +
+
+
#   + + + def + register_hooks(self, hooks: Dict[algorunner.hooks.Hook, Callable]): +
+ +
+ View Source +
    def register_hooks(self, hooks: Dict[Hook, Callable]):
+        def wrapper(fn):
+            def _wrapped(*args, **kwargs):
+                fn(*args, **kwargs)
+
+            return _wrapped
+
+        for h in hooks:
+            hook_handler(h)(wrapper(hooks[h]))
+
+ +
+ + + +
+
+ + +
+ View Source +
    def authorise(
+        self, state: AccountState, trx: TransactionRequest
+    ) -> TransactionRequest:
+        logger.info(
+            "no authorisation guard set: automatically authorising order"
+        )
+        trx.approved = True
+        return trx
+
+ +
+ + + +
+
+
#   + +
@abstractmethod
+ + def + process( + self, + tick: Union[pandas.core.frame.DataFrame, algorunner.adapters.messages.RawTickPayload] +): +
+ +
+ View Source +
    @abstractmethod
+    def process(self, tick: Tick):
+        """ """
+        pass
+
+ +
+ + + +
+
+
+
+ #   + + + class + Adapter(abc.ABC): +
+ +
+ View Source +
class Adapter(ABC):
+    """Required interface that an exchange adapter must implement."""
+
+    def __init__(self, sync_queue: Queue):
+        self.sync_queue = sync_queue
+
+    @abstractmethod
+    def connect(self, creds: messages.Credentials):
+        """connect authenticates with the exchange, and also populates
+        the associated `Trader` object with the latest state."""
+        pass
+
+    @abstractmethod
+    def monitor_user(self):
+        """@todo"""
+        pass
+
+    @abstractmethod
+    def run(self, symbol: str, process: Callable):
+        """run executes the underlying strategy, ensuring that any data
+        transformation required is carried out correctly."""
+        pass
+
+    @abstractmethod
+    def execute(self, trx: messages.TransactionRequest) -> bool:
+        pass
+
+    @abstractmethod
+    def disconnect(self):
+        pass
+
+ +
+ +

Required interface that an exchange adapter must implement.

+
+ + +
+
#   + +
@abstractmethod
+ + def + connect(self, creds: algorunner.adapters.messages.Credentials): +
+ +
+ View Source +
    @abstractmethod
+    def connect(self, creds: messages.Credentials):
+        """connect authenticates with the exchange, and also populates
+        the associated `Trader` object with the latest state."""
+        pass
+
+ +
+ +

connect authenticates with the exchange, and also populates +the associated Trader object with the latest state.

+
+ + +
+
+
#   + +
@abstractmethod
+ + def + monitor_user(self): +
+ +
+ View Source +
    @abstractmethod
+    def monitor_user(self):
+        """@todo"""
+        pass
+
+ +
+ +

@todo

+
+ + +
+
+
#   + +
@abstractmethod
+ + def + run(self, symbol: str, process: Callable): +
+ +
+ View Source +
    @abstractmethod
+    def run(self, symbol: str, process: Callable):
+        """run executes the underlying strategy, ensuring that any data
+        transformation required is carried out correctly."""
+        pass
+
+ +
+ +

run executes the underlying strategy, ensuring that any data +transformation required is carried out correctly.

+
+ + +
+
+
#   + +
@abstractmethod
+ + def + execute(self, trx: algorunner.adapters.messages.TransactionRequest) -> bool: +
+ +
+ View Source +
    @abstractmethod
+    def execute(self, trx: messages.TransactionRequest) -> bool:
+        pass
+
+ +
+ + + +
+
+
#   + +
@abstractmethod
+ + def + disconnect(self): +
+ +
+ View Source +
    @abstractmethod
+    def disconnect(self):
+        pass
+
+ +
+ + + +
+
+
+
+ #   + + + class + Hook(enum.Enum): +
+ +
+ View Source +
class Hook(Enum):
+    """Hook represents valid hooks for user-defined functions to listen
+    for."""
+
+    RUNNER_INITIALISED = 1
+    RUNNER_STARTING = 2
+    RUNNER_STOPPING = 3
+    ORDER_REQUEST = 4
+    API_EXECUTE_DURATION = 5
+    PROCESS_DURATION = 6
+
+ +
+ +

Hook represents valid hooks for user-defined functions to listen +for.

+
+ + +
+
#   + + RUNNER_INITIALISED = <Hook.RUNNER_INITIALISED: 1> +
+ + + +
+
+
#   + + RUNNER_STARTING = <Hook.RUNNER_STARTING: 2> +
+ + + +
+
+
#   + + RUNNER_STOPPING = <Hook.RUNNER_STOPPING: 3> +
+ + + +
+
+
#   + + ORDER_REQUEST = <Hook.ORDER_REQUEST: 4> +
+ + + +
+
+
#   + + API_EXECUTE_DURATION = <Hook.API_EXECUTE_DURATION: 5> +
+ + + +
+
+
#   + + PROCESS_DURATION = <Hook.PROCESS_DURATION: 6> +
+ + + +
+
+
Inherited Members
+
+
enum.Enum
+
name
+
value
+ +
+
+
+
+
+
+ #   + + + class + InvalidHookHandler(builtins.Exception): +
+ +
+ View Source +
class InvalidHookHandler(Exception):
+    """Raised when `hook_handler` is unable to register a given hook."""
+
+    pass
+
+ +
+ +

Raised when hook_handler is unable to register a given hook.

+
+ + +
+
Inherited Members
+
+
builtins.Exception
+
Exception
+ +
+
builtins.BaseException
+
with_traceback
+
args
+ +
+
+
+
+
+
#   + + + def + hook(hook: algorunner.hooks.Hook, *args, **kwargs): +
+ +
+ View Source +
def hook(hook: Hook, *args, **kwargs):
+    """`hook(...)` calls any handlers associated with a given Hook."""
+    callbacks = _registered_hooks.get(hook, [])
+    for cb in callbacks:
+        try:
+            cb(*args, **kwargs)
+        except TypeError:
+            logger.error(f"invalid handler ({cb.__name__}) for hook ({hook})")
+
+ +
+ +

hook(...) calls any handlers associated with a given Hook.

+
+ + +
+
+
#   + + + def + clear_handlers(hook: Optional[algorunner.hooks.Hook] = None): +
+ +
+ View Source +
def clear_handlers(hook: Optional[Hook] = None):
+    """`clear_handlers` clears registered handlers; optionally for
+    a specific hook"""
+    if hook and _registered_hooks.get(hook):
+        _registered_hooks[hook] = []
+        return
+
+    _registered_hooks.clear()
+
+ +
+ +

clear_handlers clears registered handlers; optionally for +a specific hook

+
+ + +
+
+
+ #   + + + class + Timer: +
+ +
+ View Source +
class Timer:
+    """Simple timer based context manager, used for performance monitoring
+    in conjunction with hooks."""
+
+    def __init__(self, trigger_hook: Optional[Hook] = None):
+        self.duration = None
+        self.hook = trigger_hook
+
+    def __enter__(self):
+        self.start = time()
+
+    def __exit__(self, exc_type, exc_val, traceback):
+        self.duration = time() - self.start
+
+        if exc_type:
+            logger.error(
+                f"detected exception during monitoring: {exc_type} ({exc_val})"
+            )
+
+        if self.hook:
+            hook(self.hook, self.ms())
+
+    def ms(self) -> float:
+        return round(self.duration * 1000)
+
+ +
+ +

Simple timer based context manager, used for performance monitoring +in conjunction with hooks.

+
+ + +
+
#   + + + Timer(trigger_hook: Optional[algorunner.hooks.Hook] = None) +
+ +
+ View Source +
    def __init__(self, trigger_hook: Optional[Hook] = None):
+        self.duration = None
+        self.hook = trigger_hook
+
+ +
+ + + +
+
+
#   + + + def + ms(self) -> float: +
+ +
+ View Source +
    def ms(self) -> float:
+        return round(self.duration * 1000)
+
+ +
+ + + +
+
+
+ + \ No newline at end of file diff --git a/docs/search.json b/docs/search.json new file mode 100644 index 0000000..7affc01 --- /dev/null +++ b/docs/search.json @@ -0,0 +1 @@ +{"version": "0.9.5", "fields": ["qualname", "fullname", "doc"], "ref": "fullname", "documentStore": {"docs": {"algorunner": {"fullname": "algorunner", "modulename": "algorunner", "qualname": "", "type": "module", "doc": "
\n

@todo: define all user required objects, and then import to algorunner/__init__.py\n to simplify document generation and developer experience.

\n
\n\n

AlgoRunner is a simple framework that can be used to build algorithmic trading strategies against cryptocurrency exchanges.

\n\n

For information on the development, check the repository.

\n\n
\n

This documentation covers the objects that you'll need to interact with when using AlgoRunner; it doesn't necessarily cover the internals of the system.

\n
\n\n

Using AlgoRunner

\n\n

AlgoRunner is simple: Strategy objects are executed against an exchange via an API Adapter.

\n\n

Writing Strategies

\n\n

The abstract methods required to be implemented are located in algorunner.BaseStrategy.

\n\n

Integration with \"Hooks\"

\n\n

AlgoRunner has the concept of \"hooks\": simple events that are dispatched containing performance metrics or status updates. See algorunner.hooks for information information about these.

\n\n

Exceptions and Error Handling

\n\n

Check out the algorunner.exceptions module for more information.

\n\n

Writing Adapters

\n\n

Adapters must inherit from algorunner.adapters.base.

\n"}, "algorunner.BaseStrategy": {"fullname": "algorunner.BaseStrategy", "modulename": "algorunner", "qualname": "BaseStrategy", "type": "class", "doc": "

A BaseStrategy is the container for an algorithm, it simply needs to respond\nto incoming market payloads and be able to generate events for the internal\nSyncAgent Actor which is responsible for synchronising state between the API\nand the algorithm. (In this context \"state\" means transactions, balances and\npositions)

\n"}, "algorunner.BaseStrategy.start_sync": {"fullname": "algorunner.BaseStrategy.start_sync", "modulename": "algorunner", "qualname": "BaseStrategy.start_sync", "type": "function", "doc": "

\n", "parameters": ["self", "queue", "adapter"], "funcdef": "def"}, "algorunner.BaseStrategy.order_sell_limit": {"fullname": "algorunner.BaseStrategy.order_sell_limit", "modulename": "algorunner", "qualname": "BaseStrategy.order_sell_limit", "type": "function", "doc": "

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "algorunner.BaseStrategy.order_sell_market": {"fullname": "algorunner.BaseStrategy.order_sell_market", "modulename": "algorunner", "qualname": "BaseStrategy.order_sell_market", "type": "function", "doc": "

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "algorunner.BaseStrategy.order_buy_limit": {"fullname": "algorunner.BaseStrategy.order_buy_limit", "modulename": "algorunner", "qualname": "BaseStrategy.order_buy_limit", "type": "function", "doc": "

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "algorunner.BaseStrategy.order_buy_market": {"fullname": "algorunner.BaseStrategy.order_buy_market", "modulename": "algorunner", "qualname": "BaseStrategy.order_buy_market", "type": "function", "doc": "

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "algorunner.BaseStrategy.shutdown": {"fullname": "algorunner.BaseStrategy.shutdown", "modulename": "algorunner", "qualname": "BaseStrategy.shutdown", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "algorunner.BaseStrategy.account_state": {"fullname": "algorunner.BaseStrategy.account_state", "modulename": "algorunner", "qualname": "BaseStrategy.account_state", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "algorunner.BaseStrategy.register_hooks": {"fullname": "algorunner.BaseStrategy.register_hooks", "modulename": "algorunner", "qualname": "BaseStrategy.register_hooks", "type": "function", "doc": "

\n", "parameters": ["self", "hooks"], "funcdef": "def"}, "algorunner.BaseStrategy.authorise": {"fullname": "algorunner.BaseStrategy.authorise", "modulename": "algorunner", "qualname": "BaseStrategy.authorise", "type": "function", "doc": "

\n", "parameters": ["self", "state", "trx"], "funcdef": "def"}, "algorunner.BaseStrategy.process": {"fullname": "algorunner.BaseStrategy.process", "modulename": "algorunner", "qualname": "BaseStrategy.process", "type": "function", "doc": "

\n", "parameters": ["self", "tick"], "funcdef": "def"}, "algorunner.Adapter": {"fullname": "algorunner.Adapter", "modulename": "algorunner", "qualname": "Adapter", "type": "class", "doc": "

Required interface that an exchange adapter must implement.

\n"}, "algorunner.Adapter.connect": {"fullname": "algorunner.Adapter.connect", "modulename": "algorunner", "qualname": "Adapter.connect", "type": "function", "doc": "

connect authenticates with the exchange, and also populates\nthe associated Trader object with the latest state.

\n", "parameters": ["self", "creds"], "funcdef": "def"}, "algorunner.Adapter.monitor_user": {"fullname": "algorunner.Adapter.monitor_user", "modulename": "algorunner", "qualname": "Adapter.monitor_user", "type": "function", "doc": "

@todo

\n", "parameters": ["self"], "funcdef": "def"}, "algorunner.Adapter.run": {"fullname": "algorunner.Adapter.run", "modulename": "algorunner", "qualname": "Adapter.run", "type": "function", "doc": "

run executes the underlying strategy, ensuring that any data\ntransformation required is carried out correctly.

\n", "parameters": ["self", "symbol", "process"], "funcdef": "def"}, "algorunner.Adapter.execute": {"fullname": "algorunner.Adapter.execute", "modulename": "algorunner", "qualname": "Adapter.execute", "type": "function", "doc": "

\n", "parameters": ["self", "trx"], "funcdef": "def"}, "algorunner.Adapter.disconnect": {"fullname": "algorunner.Adapter.disconnect", "modulename": "algorunner", "qualname": "Adapter.disconnect", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "algorunner.Hook": {"fullname": "algorunner.Hook", "modulename": "algorunner", "qualname": "Hook", "type": "class", "doc": "

Hook represents valid hooks for user-defined functions to listen\nfor.

\n"}, "algorunner.Hook.RUNNER_INITIALISED": {"fullname": "algorunner.Hook.RUNNER_INITIALISED", "modulename": "algorunner", "qualname": "Hook.RUNNER_INITIALISED", "type": "variable", "doc": "

\n"}, "algorunner.Hook.RUNNER_STARTING": {"fullname": "algorunner.Hook.RUNNER_STARTING", "modulename": "algorunner", "qualname": "Hook.RUNNER_STARTING", "type": "variable", "doc": "

\n"}, "algorunner.Hook.RUNNER_STOPPING": {"fullname": "algorunner.Hook.RUNNER_STOPPING", "modulename": "algorunner", "qualname": "Hook.RUNNER_STOPPING", "type": "variable", "doc": "

\n"}, "algorunner.Hook.ORDER_REQUEST": {"fullname": "algorunner.Hook.ORDER_REQUEST", "modulename": "algorunner", "qualname": "Hook.ORDER_REQUEST", "type": "variable", "doc": "

\n"}, "algorunner.Hook.API_EXECUTE_DURATION": {"fullname": "algorunner.Hook.API_EXECUTE_DURATION", "modulename": "algorunner", "qualname": "Hook.API_EXECUTE_DURATION", "type": "variable", "doc": "

\n"}, "algorunner.Hook.PROCESS_DURATION": {"fullname": "algorunner.Hook.PROCESS_DURATION", "modulename": "algorunner", "qualname": "Hook.PROCESS_DURATION", "type": "variable", "doc": "

\n"}, "algorunner.InvalidHookHandler": {"fullname": "algorunner.InvalidHookHandler", "modulename": "algorunner", "qualname": "InvalidHookHandler", "type": "class", "doc": "

Raised when hook_handler is unable to register a given hook.

\n"}, "algorunner.hook": {"fullname": "algorunner.hook", "modulename": "algorunner", "qualname": "hook", "type": "function", "doc": "

hook(...) calls any handlers associated with a given Hook.

\n", "parameters": ["hook", "args", "kwargs"], "funcdef": "def"}, "algorunner.clear_handlers": {"fullname": "algorunner.clear_handlers", "modulename": "algorunner", "qualname": "clear_handlers", "type": "function", "doc": "

clear_handlers clears registered handlers; optionally for\na specific hook

\n", "parameters": ["hook"], "funcdef": "def"}, "algorunner.Timer": {"fullname": "algorunner.Timer", "modulename": "algorunner", "qualname": "Timer", "type": "class", "doc": "

Simple timer based context manager, used for performance monitoring\nin conjunction with hooks.

\n"}, "algorunner.Timer.__init__": {"fullname": "algorunner.Timer.__init__", "modulename": "algorunner", "qualname": "Timer.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "trigger_hook"], "funcdef": "def"}, "algorunner.Timer.ms": {"fullname": "algorunner.Timer.ms", "modulename": "algorunner", "qualname": "Timer.ms", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}}, "docInfo": {"algorunner": {"qualname": 0, "fullname": 1, "doc": 97}, "algorunner.BaseStrategy": {"qualname": 1, "fullname": 2, "doc": 26}, "algorunner.BaseStrategy.start_sync": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.order_sell_limit": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.order_sell_market": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.order_buy_limit": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.order_buy_market": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.shutdown": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.account_state": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.register_hooks": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.authorise": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.BaseStrategy.process": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Adapter": {"qualname": 1, "fullname": 2, "doc": 5}, "algorunner.Adapter.connect": {"qualname": 2, "fullname": 3, "doc": 9}, "algorunner.Adapter.monitor_user": {"qualname": 2, "fullname": 3, "doc": 1}, "algorunner.Adapter.run": {"qualname": 2, "fullname": 3, "doc": 11}, "algorunner.Adapter.execute": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Adapter.disconnect": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Hook": {"qualname": 1, "fullname": 2, "doc": 8}, "algorunner.Hook.RUNNER_INITIALISED": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Hook.RUNNER_STARTING": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Hook.RUNNER_STOPPING": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Hook.ORDER_REQUEST": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Hook.API_EXECUTE_DURATION": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Hook.PROCESS_DURATION": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.InvalidHookHandler": {"qualname": 1, "fullname": 2, "doc": 6}, "algorunner.hook": {"qualname": 1, "fullname": 2, "doc": 6}, "algorunner.clear_handlers": {"qualname": 1, "fullname": 2, "doc": 7}, "algorunner.Timer": {"qualname": 1, "fullname": 2, "doc": 10}, "algorunner.Timer.__init__": {"qualname": 2, "fullname": 3, "doc": 0}, "algorunner.Timer.ms": {"qualname": 2, "fullname": 3, "doc": 0}}, "length": 31, "save": true}, "index": {"qualname": {"root": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {"algorunner.BaseStrategy": {"tf": 1}, "algorunner.BaseStrategy.start_sync": {"tf": 1}, "algorunner.BaseStrategy.order_sell_limit": {"tf": 1}, "algorunner.BaseStrategy.order_sell_market": {"tf": 1}, "algorunner.BaseStrategy.order_buy_limit": {"tf": 1}, "algorunner.BaseStrategy.order_buy_market": {"tf": 1}, "algorunner.BaseStrategy.shutdown": {"tf": 1}, "algorunner.BaseStrategy.account_state": {"tf": 1}, "algorunner.BaseStrategy.register_hooks": {"tf": 1}, "algorunner.BaseStrategy.authorise": {"tf": 1}, "algorunner.BaseStrategy.process": {"tf": 1}}, "df": 11}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"algorunner.BaseStrategy.start_sync": {"tf": 1}}, "df": 1}}}}}}}}}, "h": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.BaseStrategy.shutdown": {"tf": 1}}, "df": 1}}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.order_sell_limit": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.order_sell_market": {"tf": 1}}, "df": 1}}}}}}}}}}}, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.order_buy_limit": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.order_buy_market": {"tf": 1}}, "df": 1}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Hook.ORDER_REQUEST": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.account_state": {"tf": 1}}, "df": 1}}}}}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.BaseStrategy.authorise": {"tf": 1}}, "df": 1}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter": {"tf": 1}, "algorunner.Adapter.connect": {"tf": 1}, "algorunner.Adapter.monitor_user": {"tf": 1}, "algorunner.Adapter.run": {"tf": 1}, "algorunner.Adapter.execute": {"tf": 1}, "algorunner.Adapter.disconnect": {"tf": 1}}, "df": 6}}}}, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Hook.API_EXECUTE_DURATION": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"algorunner.BaseStrategy.register_hooks": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.Hook.RUNNER_INITIALISED": {"tf": 1}}, "df": 1}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Hook.RUNNER_STARTING": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {"algorunner.Hook.RUNNER_STOPPING": {"tf": 1}}, "df": 1}}}}}}}}}}}, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.BaseStrategy.process": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Hook.PROCESS_DURATION": {"tf": 1}}, "df": 1}}}}}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.connect": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.clear_handlers": {"tf": 1}}, "df": 1}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.Adapter.monitor_user": {"tf": 1}}, "df": 1}}}}}}}}}, "s": {"docs": {"algorunner.Timer.ms": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.execute": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.disconnect": {"tf": 1}}, "df": 1}}}}}}}}}}, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"algorunner.Hook": {"tf": 1}, "algorunner.Hook.RUNNER_INITIALISED": {"tf": 1}, "algorunner.Hook.RUNNER_STARTING": {"tf": 1}, "algorunner.Hook.RUNNER_STOPPING": {"tf": 1}, "algorunner.Hook.ORDER_REQUEST": {"tf": 1}, "algorunner.Hook.API_EXECUTE_DURATION": {"tf": 1}, "algorunner.Hook.PROCESS_DURATION": {"tf": 1}, "algorunner.hook": {"tf": 1}}, "df": 8}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.InvalidHookHandler": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Timer": {"tf": 1}, "algorunner.Timer.__init__": {"tf": 1}, "algorunner.Timer.ms": {"tf": 1}}, "df": 3}}}}}, "_": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "_": {"docs": {"algorunner.Timer.__init__": {"tf": 1}}, "df": 1}}}}}}}}}}, "fullname": {"root": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1}, "algorunner.BaseStrategy.start_sync": {"tf": 1}, "algorunner.BaseStrategy.order_sell_limit": {"tf": 1}, "algorunner.BaseStrategy.order_sell_market": {"tf": 1}, "algorunner.BaseStrategy.order_buy_limit": {"tf": 1}, "algorunner.BaseStrategy.order_buy_market": {"tf": 1}, "algorunner.BaseStrategy.shutdown": {"tf": 1}, "algorunner.BaseStrategy.account_state": {"tf": 1}, "algorunner.BaseStrategy.register_hooks": {"tf": 1}, "algorunner.BaseStrategy.authorise": {"tf": 1}, "algorunner.BaseStrategy.process": {"tf": 1}, "algorunner.Adapter": {"tf": 1}, "algorunner.Adapter.connect": {"tf": 1}, "algorunner.Adapter.monitor_user": {"tf": 1}, "algorunner.Adapter.run": {"tf": 1}, "algorunner.Adapter.execute": {"tf": 1}, "algorunner.Adapter.disconnect": {"tf": 1}, "algorunner.Hook": {"tf": 1}, "algorunner.Hook.RUNNER_INITIALISED": {"tf": 1}, "algorunner.Hook.RUNNER_STARTING": {"tf": 1}, "algorunner.Hook.RUNNER_STOPPING": {"tf": 1}, "algorunner.Hook.ORDER_REQUEST": {"tf": 1}, "algorunner.Hook.API_EXECUTE_DURATION": {"tf": 1}, "algorunner.Hook.PROCESS_DURATION": {"tf": 1}, "algorunner.InvalidHookHandler": {"tf": 1}, "algorunner.hook": {"tf": 1}, "algorunner.clear_handlers": {"tf": 1}, "algorunner.Timer": {"tf": 1}, "algorunner.Timer.__init__": {"tf": 1}, "algorunner.Timer.ms": {"tf": 1}}, "df": 31}}}}}}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.account_state": {"tf": 1}}, "df": 1}}}}}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.BaseStrategy.authorise": {"tf": 1}}, "df": 1}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter": {"tf": 1}, "algorunner.Adapter.connect": {"tf": 1}, "algorunner.Adapter.monitor_user": {"tf": 1}, "algorunner.Adapter.run": {"tf": 1}, "algorunner.Adapter.execute": {"tf": 1}, "algorunner.Adapter.disconnect": {"tf": 1}}, "df": 6}}}}, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Hook.API_EXECUTE_DURATION": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {"algorunner.BaseStrategy": {"tf": 1}, "algorunner.BaseStrategy.start_sync": {"tf": 1}, "algorunner.BaseStrategy.order_sell_limit": {"tf": 1}, "algorunner.BaseStrategy.order_sell_market": {"tf": 1}, "algorunner.BaseStrategy.order_buy_limit": {"tf": 1}, "algorunner.BaseStrategy.order_buy_market": {"tf": 1}, "algorunner.BaseStrategy.shutdown": {"tf": 1}, "algorunner.BaseStrategy.account_state": {"tf": 1}, "algorunner.BaseStrategy.register_hooks": {"tf": 1}, "algorunner.BaseStrategy.authorise": {"tf": 1}, "algorunner.BaseStrategy.process": {"tf": 1}}, "df": 11}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"algorunner.BaseStrategy.start_sync": {"tf": 1}}, "df": 1}}}}}}}}}, "h": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.BaseStrategy.shutdown": {"tf": 1}}, "df": 1}}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.order_sell_limit": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.order_sell_market": {"tf": 1}}, "df": 1}}}}}}}}}}}, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.order_buy_limit": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy.order_buy_market": {"tf": 1}}, "df": 1}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Hook.ORDER_REQUEST": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"algorunner.BaseStrategy.register_hooks": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.Hook.RUNNER_INITIALISED": {"tf": 1}}, "df": 1}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Hook.RUNNER_STARTING": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {"algorunner.Hook.RUNNER_STOPPING": {"tf": 1}}, "df": 1}}}}}}}}}}}, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.BaseStrategy.process": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Hook.PROCESS_DURATION": {"tf": 1}}, "df": 1}}}}}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.connect": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.clear_handlers": {"tf": 1}}, "df": 1}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.Adapter.monitor_user": {"tf": 1}}, "df": 1}}}}}}}}}, "s": {"docs": {"algorunner.Timer.ms": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.execute": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.disconnect": {"tf": 1}}, "df": 1}}}}}}}}}}, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"algorunner.Hook": {"tf": 1}, "algorunner.Hook.RUNNER_INITIALISED": {"tf": 1}, "algorunner.Hook.RUNNER_STARTING": {"tf": 1}, "algorunner.Hook.RUNNER_STOPPING": {"tf": 1}, "algorunner.Hook.ORDER_REQUEST": {"tf": 1}, "algorunner.Hook.API_EXECUTE_DURATION": {"tf": 1}, "algorunner.Hook.PROCESS_DURATION": {"tf": 1}, "algorunner.hook": {"tf": 1}}, "df": 8}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.InvalidHookHandler": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Timer": {"tf": 1}, "algorunner.Timer.__init__": {"tf": 1}, "algorunner.Timer.ms": {"tf": 1}}, "df": 3}}}}}, "_": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "_": {"docs": {"algorunner.Timer.__init__": {"tf": 1}}, "df": 1}}}}}}}}}}, "doc": {"root": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {"algorunner": {"tf": 1}, "algorunner.Adapter.monitor_user": {"tf": 1}}, "df": 2}}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"algorunner": {"tf": 1}}, "df": 1, "r": {"docs": {"algorunner.Adapter.connect": {"tf": 1}}, "df": 1}}}, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1}}}}}}}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Timer": {"tf": 1}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"algorunner": {"tf": 1}, "algorunner.Hook": {"tf": 1}}, "df": 2}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {"algorunner": {"tf": 1.4142135623730951}}, "df": 1}}}}}}, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1.4142135623730951}}, "df": 1}}}}}}, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "'": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {"algorunner": {"tf": 1.7320508075688772}, "algorunner.Timer": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "r": {"docs": {"algorunner": {"tf": 1}, "algorunner.Hook": {"tf": 1}}, "df": 2}}}, "p": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.InvalidHookHandler": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {"algorunner": {"tf": 1.4142135623730951}, "algorunner.Adapter": {"tf": 1}, "algorunner.Adapter.run": {"tf": 1}}, "df": 3}}}}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.Hook": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}, "s": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}}}, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.InvalidHookHandler": {"tf": 1}, "algorunner.clear_handlers": {"tf": 1}}, "df": 2}}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1}}, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.InvalidHookHandler": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1.7320508075688772}, "algorunner.Adapter.connect": {"tf": 1}}, "df": 2}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}, "algorunner.Adapter.run": {"tf": 1}}, "df": 2}}, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.clear_handlers": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}, "algorunner.Adapter": {"tf": 1}}, "df": 2}}}}}}}}, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"algorunner": {"tf": 2}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}, "n": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1}}, "df": 2}, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {"algorunner.Adapter": {"tf": 1}}, "df": 1}}}}, "g": {"docs": {}, "df": 0, "r": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {"algorunner": {"tf": 3}}, "df": 1, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "_": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "m": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1.4142135623730951}}, "df": 2}}}}}}}}, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1.4142135623730951}}, "df": 1}}}}}}, "p": {"docs": {}, "df": 0, "i": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1}}, "df": 2}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 2}, "algorunner.Adapter": {"tf": 1}}, "df": 2}}}}, "b": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.connect": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {"algorunner.Adapter.connect": {"tf": 1}, "algorunner.hook": {"tf": 1}}, "df": 2}}}}}}, "p": {"docs": {}, "df": 0, "y": {"docs": {"algorunner": {"tf": 1}}, "df": 1}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"algorunner": {"tf": 1}, "algorunner.Timer": {"tf": 1}}, "df": 2}}}}}}, "a": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}, "p": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.Adapter.connect": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {"algorunner": {"tf": 1.7320508075688772}, "algorunner.Timer": {"tf": 1}}, "df": 2, "i": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1, "f": {"docs": {}, "df": 0, "i": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {"algorunner": {"tf": 1.7320508075688772}, "algorunner.Adapter.run": {"tf": 1}}, "df": 2}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {"algorunner": {"tf": 1}}, "df": 1}, "e": {"docs": {"algorunner.BaseStrategy": {"tf": 1.4142135623730951}, "algorunner.Adapter.connect": {"tf": 1}}, "df": 2}}}}, "y": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}}}}}}}, "e": {"docs": {}, "df": 0, "e": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {"algorunner.clear_handlers": {"tf": 1}}, "df": 1}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1}}, "df": 2}}}}, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.InvalidHookHandler": {"tf": 1}, "algorunner.hook": {"tf": 1}}, "df": 2}}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"algorunner": {"tf": 1.4142135623730951}, "algorunner.Adapter": {"tf": 1}, "algorunner.Adapter.connect": {"tf": 1}}, "df": 3}}}}, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1.4142135623730951}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}, "algorunner.Adapter.run": {"tf": 1}}, "df": 2}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1}}, "df": 2}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1}}}}}, "f": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.Hook": {"tf": 1}}, "df": 1}}}}}}}}, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"algorunner": {"tf": 1}, "algorunner.Timer": {"tf": 1}}, "df": 2, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1}}, "df": 2}}}}}}}}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}}}}}, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"algorunner": {"tf": 1.4142135623730951}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"algorunner": {"tf": 1.4142135623730951}}, "df": 1}}}, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1}}, "df": 2}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy": {"tf": 1}, "algorunner.Timer": {"tf": 1}}, "df": 2}}}}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.connect": {"tf": 1}}, "df": 1}}}}, "j": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Timer": {"tf": 1}}, "df": 1}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1}}}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {"algorunner.Adapter.run": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.hook": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.clear_handlers": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.clear_handlers": {"tf": 1}}, "df": 1}}}}}}}}}}}, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "'": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"algorunner": {"tf": 1}, "algorunner.BaseStrategy": {"tf": 1}}, "df": 2}}, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"algorunner.Hook": {"tf": 1}}, "df": 1}}}}}, "w": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"algorunner": {"tf": 1.4142135623730951}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.Timer": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.BaseStrategy": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"algorunner.Timer": {"tf": 1}}, "df": 1}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"algorunner": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"algorunner.Adapter.connect": {"tf": 1}}, "df": 1}}}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"algorunner.Hook": {"tf": 1}}, "df": 1}}}}}}, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"algorunner": {"tf": 1.7320508075688772}, "algorunner.Hook": {"tf": 1.4142135623730951}, "algorunner.InvalidHookHandler": {"tf": 1}, "algorunner.hook": {"tf": 1.4142135623730951}, "algorunner.clear_handlers": {"tf": 1}, "algorunner.Timer": {"tf": 1}}, "df": 6, "_": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {"algorunner.InvalidHookHandler": {"tf": 1}}, "df": 1}}}}}}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {"algorunner": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "r": {"docs": {"algorunner.hook": {"tf": 1}, "algorunner.clear_handlers": {"tf": 1}}, "df": 2}}}}}}}}}}, "pipeline": ["trimmer", "stopWordFilter", "stemmer"], "_isPrebuiltIndex": true} \ No newline at end of file diff --git a/docs/swimlanes/README.md b/docs/swimlanes/README.md new file mode 100644 index 0000000..cda72f0 --- /dev/null +++ b/docs/swimlanes/README.md @@ -0,0 +1,7 @@ +# Docs: Swimlanes + +These files are to be ran using [swimlanes.io](https://swimlanes.io); and show various interactions between components and/or APIs. + +## Contents + +- **`order_flow.txt`**: the flow from the strategy to the external exchange, detailing interactions between the `Strategy`, `SyncAgent`, `Adapter` and external exchange API. \ No newline at end of file diff --git a/docs/swimlanes/order_flow.txt b/docs/swimlanes/order_flow.txt new file mode 100644 index 0000000..cf10e98 --- /dev/null +++ b/docs/swimlanes/order_flow.txt @@ -0,0 +1,48 @@ +// swimlanes.io: Order Flow + +autonumber + +Strategy -> SyncAgent: OrderRequest + +note: +When the **Strategy** determines that an order should be placed, it triggers the dispatch of an **OrderRequest** to the **SyncAgent** queue. + + +SyncAgent -> Authorise: OrderRequest, AccountState + +Authorise -> SyncAgent: OrderRequest + +note: +The **SyncAgent** calls the **Authorise** method; this will determine whether the order should be placed, in addition to the type of order and the quantities involved. + + +--: Transaction Approved + +SyncAgent -> Adapter: OrderRequest + +Adapter --> Exchange: REST: Test Order + +note: If supported by the exchange, and it doesn't result in too much of a performance decrease, then a test order can be dispatched to verify the validity of the request. + +Adapter -> Exchange: REST: Place Order + +Adapter -> SyncAgent: OrderStatus + +note: +The **Adapter** returns an **OrderStatus** object built using the response from the underlying API request. At this point the **OrderStatus** will likely contain an ID, status, amount, and percentage "filled". + +The **SyncAgent** can then maintain a registry of orders and their status. + +--: Transaction Updates + +...: {fas-spinner} + +Exchange -> Adapter: Stream: Order Update + +Adapter -> SyncAgent: OrderUpdate + +note: +Subsequent updates to the order arrive via the *user stream*, which result in an **OrderUpdate** being sent to the **SyncAgent** message queue - resulting in an update to the **OrderStatus** registry. + + + 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/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c72fc7f --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1028 @@ +[[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 = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[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 = "black" +version = "21.7b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.8.1,<1" +regex = ">=2020.1.8" +tomli = ">=0.2.6,<2.0.0" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] +python2 = ["typed-ast (>=1.4.2)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[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 = "jinja2" +version = "3.0.1" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "loguru" +version = "0.5.3" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.10b0)", "isort (>=5.1.1)"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[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 = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[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 = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "pdoc" +version = "7.4.0" +description = "API Documentation for Python Projects" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Jinja2 = ">=2.11.0" +MarkupSafe = "*" +pygments = "*" + +[package.extras] +dev = ["flake8", "hypothesis", "mypy", "pytest", "pytest-cov", "pytest-timeout", "tox"] + +[[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 = "pygments" +version = "2.10.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.5" + +[[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 = "tomli" +version = "1.2.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[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 = "win32-setctime" +version = "1.0.3" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] + +[[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 = "006e1cf93de1feda94acf8ecdf10f8cb5628bd5c078ed81e9fecf0ea0ad5db75" + +[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"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +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"}, +] +black = [ + {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"}, + {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, +] +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"}, +] +jinja2 = [ + {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, + {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, +] +loguru = [ + {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, + {file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +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"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +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"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +pdoc = [ + {file = "pdoc-7.4.0-py3-none-any.whl", hash = "sha256:681a2f243e4ca51bedd0645c2d18275b8b83444e9b6e42b502882ec45369e679"}, +] +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"}, +] +pygments = [ + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, +] +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"}, +] +tomli = [ + {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, + {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, +] +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"}, +] +win32-setctime = [ + {file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"}, + {file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"}, +] +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..17ce5e5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[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" +loguru = "^0.5.3" +pdoc = "^7.4.0" + +[tool.poetry.dev-dependencies] +behave = "^1.2.6" +pytest = "^6.2.4" +flake8 = "^3.9.2" +black = "^21.7b0" + +[tool.black] +line-length = 80 +target-version = ['py37'] +include = '\.pyi?$' diff --git a/run.py b/run.py new file mode 100644 index 0000000..dd2cdff --- /dev/null +++ b/run.py @@ -0,0 +1,71 @@ +import configparser + +import click +from loguru import logger + +from algorunner.exceptions import InvalidConfiguration +from algorunner.runner import ( + Credentials, Runner +) +from algorunner.strategy import load_strategy + + +@click.command() +@click.option('-c', '--config', 'config_file', default='bot.ini', help='Configuration file.') +@click.option('-s', '--strategy', 'strategy_name', help='Name of Strategy to run') +@click.option('--testing', is_flag=True, default=True, help='Run in testing mode, NOT live') +@click.option('--exchange', envvar='ALGORUNNER_EXCHANGE', help='Crypto exchange to execute strategy against') +@click.option('--api-key', envvar='ALGORUNNER_API_KEY', help='API Key for exchange/broker') +@click.option('--api-secret', envvar='ALGORUNNER_API_SECRET', help='API Secret for exchange/broker') +@click.option('--trading-symbol', envvar='ALGORUNNER_TRADING_SYMBOL', help='Symbol to execute strategy against') +def entrypoint( + config_file: str, + strategy_name: str, + testing: bool, + exchange: str, + api_key: str, + api_secret: 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.warning("running in LIVE trading mode") + + cfg = configparser.ConfigParser() + cfg.read(config_file) + + try: + exchange = exchange if exchange else cfg['credentials']['exchange'] + api_key = api_key if api_key else cfg['credentials']['api_key'] + api_secret = api_secret if api_secret else cfg['credentials']['api_secret'] + strategy_name = strategy_name if strategy_name else cfg['strategy']['name'] + trading_symbol = trading_symbol if trading_symbol else cfg['strategy']['symbol'] + + if not all([api_key, api_secret, strategy_name, exchange, trading_symbol]): + raise KeyError + except KeyError: + raise InvalidConfiguration() + + strategy = load_strategy(strategy_name) + runner = Runner(Credentials( + exchange=exchange, + key=api_key, + secret=api_secret + ), trading_symbol, strategy) + runner.run() + + +if __name__ == "__main__": + try: + entrypoint() + except Exception as e: + logger.exception("encountered unrecoverable error. terminating algorunner.", e.message) 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..4291687 --- /dev/null +++ b/strategies/example.py @@ -0,0 +1,46 @@ +import pandas as pd + +from algorunner.adapters import TransactionRequest +from algorunner.hooks import Hook +from algorunner.mutations import AccountState +from algorunner.strategy import BaseStrategy + +from logging import getLogger + + +class Example(BaseStrategy): + """ + 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): + super().__init__() + self.series = pd.DataFrame + self.logger = getLogger() + self.register_hooks({ + Hook.API_EXECUTE_DURATION: self.handle_api_duration, + Hook.PROCESS_DURATION: self.handle_process_duration, + }) + + 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"]) + self.logger.info("Average price change over past 5 windows: ", recent_window.mean()) + + def authorise(self, state: AccountState, trx: TransactionRequest) -> TransactionRequest: + pass + + # AlgoRunner additionally provides hooks for monitoring execution + # performance and being able to respond to internal events. + # @see BaseStrategy.register_hooks + def handle_api_duration(self, duration: float): + self.logger.info(f"api call duration: {duration}ms") + + def handle_process_duration(self, duration: float): + self.logger.info(f"tick process duration: {duration}ms") 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/account.py b/test/account.py deleted file mode 100644 index 874a42c..0000000 --- a/test/account.py +++ /dev/null @@ -1,63 +0,0 @@ -import unittest -import json -from account import Account - -def get_payload(fixture): - with open(f"test/fixtures/{fixture}.json") as json_file: - data = json.load(json_file) - return data - -class TestAccount(unittest.TestCase): - def test_account_init(self): - acc = Account(get_payload("account")) - - self.assertTrue(acc.capability_trade()) - self.assertTrue(acc.capability_withdraw()) - self.assertFalse(acc.capability_deposit()) - - self.assertIsNone(acc.balance("NONEXISTANT"), None) - self.assertEqual(acc.balance("BTC"), ( - float("4723846.89208129"), float(0))) - self.assertEqual(acc.balance("LTC"), ( - float("4763368.68006011"), float(0))) - - def test_account_update(self): - acc = Account(get_payload("account")) - acc(get_payload("outbound_account_info")) - - self.assertTrue(acc.capability_trade()) - self.assertTrue(acc.capability_withdraw()) - self.assertTrue(acc.capability_deposit()) - - self.assertIsNone(acc.balance("NONEXISTANT"), None) - self.assertEqual(acc.balance("BTC"), ( - float("10537.85314051"), float("2.19464093"))) - self.assertEqual(acc.balance("LTC"), ( - float("17366.18538083"), float(0))) - self.assertEqual(acc.balance("ETH"), ( - float("17902.35190619"), float(0))) - self.assertEqual(acc.balance("BNC"), ( - float("1114503.29769312"), float(0))) - self.assertEqual(acc.balance("NEO"), ( - float(0), float(0))) - - def test_position_update(self): - acc = Account(get_payload("account")) - acc(get_payload("outbound_account_info")) - acc(get_payload("outbound_account_position")) - - self.assertEqual(acc.balance("ETH"), ( - float("10000.000000"), float(0) - )) - - def test_balance_update(self): - acc = Account(get_payload("account")) - acc(get_payload("outbound_account_info")) - acc(get_payload("balance_update")) - - self.assertEqual(acc.balance("BTC"), ( - (float("10537.85314051") + float("100.00000000")), - float("2.19464093"))) - -if __name__ == "__main__": - unittest.main() 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.pyold b/test/adapters/binance/fixtures.pyold new file mode 100644 index 0000000..8e18045 --- /dev/null +++ b/test/adapters/binance/fixtures.pyold @@ -0,0 +1,8 @@ +from pytest import fixture + +from algorunner.adapters import BinanceAdapter + + +@fixture +def user_transformer(): + return BinanceAdapter.UserStreamEventTransformer() diff --git a/test/adapters/binance/test_user_transformations.pyold b/test/adapters/binance/test_user_transformations.pyold new file mode 100644 index 0000000..8c062ee --- /dev/null +++ b/test/adapters/binance/test_user_transformations.pyold @@ -0,0 +1,83 @@ +from algorunner.messages import MessageType + +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: + message = user_transformer.initial_rest_payload(account_payload) + + assert message.Type == MessageType.UPDATE_ACCOUNT + + msg = message.Msg + assert not msg['CanDeposit'] + assert msg['CanTrade'] + assert msg['CanWithdraw'] + + assert len(msg['Positions']) == 2 + check_position(msg['Positions'], 'BTC', 4723846.89208129, 0.0) + check_position(msg['Positions'], 'LTC', 4763368.68006011, 0.0) + + +def test_stream_balance_update_transformation(user_transformer, load_fixture): + with load_fixture(BALANCE_UPDATE_PATTERN) as balance: + message = user_transformer(balance) + + assert message.Type == MessageType.UPDATE_BALANCE + + msg = message.Msg + assert msg['Asset'] == 'BTC' + assert msg['Update'] == 100.0 + + +def stream_execution_report_transformation(user_transformer, load_fixture): + with load_fixture(EXECUTION_REPORT_PATTERN) as execution: + message = 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: + message = user_transformer(information) + + assert message.Type == MessageType.UPDATE_ACCOUNT + + msg = message.Msg + assert msg['CanDeposit'] + assert msg['CanTrade'] + assert msg['CanWithdraw'] + + assert len(msg['Positions']) == 5 + check_position(msg['Positions'], 'LTC', 17366.18538083, 0.0) + check_position(msg['Positions'], 'BTC', 10537.85314051, 2.19464093) + check_position(msg['Positions'], 'ETH', 17902.35190619, 0.0) + check_position(msg['Positions'], 'BNC', 1114503.29769312, 0.0) + check_position(msg['Positions'], 'NEO', 0.0, 0.0) + + +def stream_account_position_transformation(user_transformer, load_fixture): + with load_fixture(ACCOUNT_POSITION_PATTERN) as position: + message = user_transformer(position) + + assert message.Type == MessageType.UPDATE_POSITION + + msg = message.Msg + assert "BTC" in msg + assert msg["BTC"].Free + assert msg["BTC"].Locked diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..fc0ec5f --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,35 @@ +from contextlib import contextmanager +from json import load +from unittest.mock import MagicMock, patch + +import pytest + +from algorunner.strategy import BaseStrategy, load_strategy + +@pytest.fixture +def mock_adapter() -> MagicMock: + with patch('algorunner.runner.factory') as mock: + mock.return_value = MagicMock() + yield mock.return_value + +@pytest.fixture +def mock_strategy() -> MagicMock: + abstractmethods = BaseStrategy.__abstractmethods__ + BaseStrategy.__abstractmethods__ = {} + + yield MagicMock() + + BaseStrategy.__abstractmethods__ = abstractmethods + +@pytest.fixture +def example_strategy() -> BaseStrategy: + yield load_strategy('Example') + +@pytest.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/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..5b04a81 --- /dev/null +++ b/test/fixtures/invalid_strategy.py @@ -0,0 +1,4 @@ + +class InvalidStrategy(): + # strategy doesn't implement required base class or methods + pass diff --git a/test/fixtures/valid_strategy.py b/test/fixtures/valid_strategy.py new file mode 100644 index 0000000..d7b6ac2 --- /dev/null +++ b/test/fixtures/valid_strategy.py @@ -0,0 +1,11 @@ +from algorunner.adapters import TransactionRequest +from algorunner.mutations import AccountState +from algorunner.strategy import BaseStrategy + + +class ValidStrategy(BaseStrategy): + def process(self, tick): + return True + + def authorise(self, state: AccountState, trx: TransactionRequest) -> TransactionRequest: + pass diff --git a/test/runner.py b/test/runner.py deleted file mode 100644 index 9cc563d..0000000 --- a/test/runner.py +++ /dev/null @@ -1,104 +0,0 @@ -import unittest -import json -import pandas as pd -from unittest.mock import patch -from runner import Runner - - -def get_payload(fixture): - with open(f"test/fixtures/{fixture}.json") as json_file: - data = json.load(json_file) - return data - - -class MockStrategy(object): - def start(self, control): - self.control = control - - -example_kline = { - 'e': '24hrTicker', - 'E': 1580770073221, - 's': 'BTCUSDT', - 'p': '-117.45000000', - 'P': '-1.250', - 'w': '9362.96596369', - 'x': '9394.08000000', - 'c': '9276.63000000', - 'Q': '0.01075500', - 'b': '9275.46000000', - 'B': '0.26951100', - 'a': '9276.63000000', - 'A': '0.00002400', - 'o': '9394.08000000', - 'h': '9618.79000000', - 'l': '9234.00000000', - 'v': '52160.24254000', - 'q': '488374575.55996477', - 'O': 1580683673213, - 'C': 1580770073213, - 'F': 238122539, - 'L': 238624515, - 'n': 501977 -} - - -class TestRunner(unittest.TestCase): - - @patch('runner.Client', autospec=True) - @patch('runner.BinanceSocketManager', autospec=True) - def test_runner_init(self, socket_mock, client_mock): - """ - Ensure that the runner initialises by (a) creating a Binance API client, - (b) retrieving account details, and (c) starting a User Websocket Conn. - """ - client_instance = client_mock.return_value - client_instance.get_account.return_value = get_payload("account") - - r = Runner("apiKey", "apiSecret", "symbol", None) - - client_mock.assert_called_once_with("apiKey", "apiSecret") - client_instance.get_account.assert_called_once() - socket_mock.return_value.start_user_socket.assert_called_once() - - @patch('runner.Client', autospec=True) - @patch('runner.BinanceSocketManager', autospec=True) - def test_runner_run(self, socket_mock, client_mock): - """ - Ensure that `.start()` is called on the strategy, that the correct args - are provided to the new streaming ticker socket, and that the socket is - correctly started. - """ - mock_strategy = MockStrategy() - client_instance = client_mock.return_value - client_instance.get_account.return_value = get_payload("account") - - r = Runner("apiKey", "apiSecret", "symbolToMonitor", mock_strategy) - r.run() - - self.assertEqual(mock_strategy.control, r) - - call = socket_mock.return_value.start_symbol_ticker_socket.call_args_list - self.assertEqual(call[0][0][0], "symbolToMonitor") - socket_mock.return_value.start.assert_called_once() - - @patch('runner.Client', autospec=True) - @patch('runner.BinanceSocketManager', autospec=True) - def test_parse_dataframe(self, socket_mock, client_mock): - """ - Ensure that inbound kline messages are correctly parsed in to Pandas - dataframes. - """ - - df = Runner("apiKey", "apiSecret", "symbolToMonitor", MockStrategy()).parse_dataframe(example_kline) - self.assertFalse(df.empty) - self.assertEqual(df.shape, (1, 22)) - self.assertEqual(df.index.name, "EventTime") - - # a few random columns - self.assertEqual(df["OpenPrice"][0], '9394.08000000') - self.assertEqual(df["PriceChangePercent"][0], '-1.250') - self.assertEqual(df["LastQuantity"][0], '0.01075500') - -if __name__ == "__main__": - unittest.main() diff --git a/test/scenarios/environment.py b/test/scenarios/environment.py new file mode 100644 index 0000000..1a8f3ed --- /dev/null +++ b/test/scenarios/environment.py @@ -0,0 +1,10 @@ +from unittest.mock import patch + +def before_feature(context, feature): + if 'mock_logger' in feature.tags: + context._mock_logger = patch("algorunner.hooks.logger") + context.mock_logger = context._mock_logger.__enter__() + +def after_feature(context, feature): + if 'mock_logger' in feature.tags: + context._mock_logger.__exit__((None,)) diff --git a/test/scenarios/hooks.feature b/test/scenarios/hooks.feature new file mode 100644 index 0000000..4e4c957 --- /dev/null +++ b/test/scenarios/hooks.feature @@ -0,0 +1,53 @@ +@mock_logger +Feature: Hooks + Hooks allow user-defined handlers to internal events in the AlgoRunner. + + Scenario: An invalid handler should be rejected + Given no handlers are registered + and a handler that isn't callable + When that handler is registered + Then an InvalidHookHandler exception is raised + + Scenario: Hook type should be validated when registering handler + Given no handlers are registered + and a valid handler for an unknown hook + When that handler is registered + Then an InvalidHookHandler exception is raised + + @intercepts_error + Scenario: Invalid handler signactures trigger logging warning + Given no handlers are registered + Given an invalid handler with a different signature + and logging is enabled + When that handler is registered + and the process_duration hook is triggered + Then the logger recieves an error message + + Scenario: A valid hook should be registered and triggered + Given no handlers are registered + Given a valid handler for process_duration + When that handler is registered + and the process_duration hook is triggered + Then no exception should be raised + and the logger recieves no errors + and the handler should be called + and the handler should have the correct argument + + Scenario: Multiple hooks should all be called + Given no handlers are registered + and 5 valid handlers for process_duration + When those handlers are registered + and the process_duration hook is triggered + Then no exception should be raised + and the logger recieves no errors + and the handlers should all be called + and the handlers should have the correct argument + + Scenario: Handlers should be called per-hook trigger + Given no handlers are registered + and a valid handler for process_duration + When that handler is registered + and the process_duration hook is triggered 5 times + Then no exception should be raised + and the logger recieves no errors + and the handler should be called 5 times diff --git a/test/scenarios/steps/hooks.py b/test/scenarios/steps/hooks.py new file mode 100644 index 0000000..b4bb75f --- /dev/null +++ b/test/scenarios/steps/hooks.py @@ -0,0 +1,139 @@ +from behave import * + +from algorunner.hooks import ( + hook_handler, clear_handlers, hook, + Hook, InvalidHookHandler +) + + +class not_callable(): + pass + + +def has_wrong_signature(ex: str, am: int, ple: dict) -> int: + return 5 + + +class valid_handler: + def __init__(self): + self.was_called = False + self.arg = None + self.call_times = 0 + + def __call__(self, duration: float) -> None: + self.call_times += 1 + self.was_called = True + self.arg = duration + + +## GIVEN + + +@given(u'no handlers are registered') +def no_registered_handlers(context): + clear_handlers() + +@given(u'a handler that isn\'t callable') +def non_callable_handler(context): + context.handlers = [not_callable()] + +@given(u'{num:d} valid handlers for process_duration') +def multiple_valid_handlers(context, num): + context.handlers = [valid_handler() for _ in range(num)] + +@given(u'an invalid handler with a different signature') +def wrong_signature_handler(context): + context.handlers = [has_wrong_signature] + +@given(u'a valid handler for an unknown hook') +def valid_handler_unknown_hook(context): + context.hook = "ewfgheruihuier" + context.handlers = [valid_handler()] + +@given(u'a valid handler for process_duration') +def valid_process_duration_handler(context): + context.handlers = [valid_handler()] + + +## WHEN + + +@when(u'that handler is registered') +def handler_registered(context): + handlers_registered(context) + +@when(u'the process_duration hook is triggered') +def trigger_hook(context): + context.logger_has_error = False + context.hook_param = 3.242 + + context.mock_logger.error.call_count = 0 + hook(Hook.PROCESS_DURATION, context.hook_param) + context.logger_has_error = (context.mock_logger.error.call_count == 1) + +@when(u'those handlers are registered') +def handlers_registered(context): + context.have_exception = False + context.have_hook_exception = False + + try: + register_fn = hook_handler(getattr(context, "hook", Hook.PROCESS_DURATION)) + for handler in context.handlers: + register_fn(handler) + except InvalidHookHandler: + context.have_exception = True + context.have_hook_exception = True + except Exception: + context.have_exception = True + +@when(u'the process_duration hook is triggered {times:d} times') +def step_impl(context, times): + for _ in range(times): + trigger_hook(context) + +@given(u'logging is enabled') +def logging_enabled(context): + pass + + +## THEN + + +@then(u'an InvalidHookHandler exception is raised') +def hook_handler_exception_raised(context): + assert context.have_hook_exception + +@then(u'the handlers should have the correct argument') +def handlers_argument(context): + for handler in context.handlers: + assert handler.arg == context.hook_param + +@then(u'the handler should have the correct argument') +def handler_argument(context): + handlers_argument(context) + +@then(u'no exception should be raised') +def no_exception_raised(context): + assert not context.have_exception + +@then(u'the handlers should all be called') +def handlers_called(context): + for handler in context.handlers: + assert handler.was_called + +@then(u'the handler should be called') +def handler_called(context): + handlers_called(context) + +@then(u'the handler should be called {times:d} times') +def handler_called_multiple_times(context, times): + for handler in context.handlers: + assert handler.call_times == times + +@then(u'the logger recieves no errors') +def logger_has_no_errors(context): + assert context.logger_has_error == False + +@then(u'the logger recieves an error message') +def logger_has_errors(context): + assert context.logger_has_error \ No newline at end of file diff --git a/test/scenarios/steps/sync_agent.py b/test/scenarios/steps/sync_agent.py new file mode 100644 index 0000000..af782e8 --- /dev/null +++ b/test/scenarios/steps/sync_agent.py @@ -0,0 +1,142 @@ +from time import sleep +from unittest import mock +from queue import Queue + +from behave import * + +from algorunner.strategy import ( + ShutdownRequest, BaseStrategy +) +from algorunner.adapters import ( + Adapter, TransactionRequest, OrderType +) +from algorunner.mutations import ( + Position, BalanceUpdate, AccountUpdate, CapabilitiesUpdate +) + + +# +# GIVEN +# + + +@given("a running sync agent awaiting messages") +def new_running_sync_agent(context): + Adapter.__abstractmethods__ = {} + + context.agent_params = { + "queue": Queue(), + "adapter": mock.MagicMock(), + "auth": mock.MagicMock() + } + + context.message_list = [] + context.agent = BaseStrategy.SyncAgent(**context.agent_params) + context.agent.start() + assert context.agent.is_running() + +@given("an account update with {capabilities} capabilities") +def account_update_full_capabilities(context, capabilities): + hasPermission = (capabilities == "full") # todo - just a straight payload swap + context.message_list.append(CapabilitiesUpdate( + can_withdraw=hasPermission, can_trade=hasPermission, can_deposit=hasPermission, + positions=[] + )) + +@given("a {symbol} balance of {free:d} free and {locked:d} locked") +def current_balance(context, symbol, free, locked): + context.agent.state.balances[symbol] = Position(symbol, free=free, locked=locked) + +@given("a balance update of {quantity:g} {symbol}") +def balance_update(context, quantity, symbol): + context.message_list.append(BalanceUpdate( + asset=symbol, delta=quantity + )) + +@given("an account position of {symbol} at {free:d} free and {locked:d} locked") +def account_with_balance(context, symbol, free, locked): + context.agent.state.balances[symbol] = Position(symbol, free=free, locked=locked) + +@given("a position update of {symbol} at {free:d} free and {locked:d} locked") +def position_update(context, symbol, free, locked): + context.message_list.append(AccountUpdate( + balances=[Position(symbol, free=free, locked=locked)] + )) + +@given("a request to buy {symbol}") +def market_order(context, symbol): + context.message_list.append(TransactionRequest( + reason="", symbol="", quantity="", price="", order_type=OrderType.MARKET_SELL + )) + +@given("the order is declined") +def calculator_rejection(context): + context.agent_params["auth"].return_value = TransactionRequest( + approved=False, reason="", symbol="", quantity="", price="", order_type=OrderType.MARKET_SELL + ) + + +@given("the order of {symbol} is accepted with a size of {size:g}") +def calculator_accepted(context, symbol, size): + context.agent_params["auth"].return_value = TransactionRequest( + approved=True, reason="", symbol="", quantity="", price="", order_type=OrderType.MARKET_SELL + ) + + +# +# WHEN +# + + +@when("all messages are processed") +def account_update_processed(context): + for msg in context.message_list: + context.agent_params["queue"].put(msg) + + sleep(.5) + context.message_list = [] + +@when("the sync agent is stopped") +def stop_sync_agent(context): + context.agent_params["queue"].put(ShutdownRequest(reason="bdd tests")) + sleep(.25) + + +# +# THEN +# + + +@then("it should no longer be running") +def sync_agent_not_running(context): + assert not context.agent.is_running() + +@then("the account should have {capabilities} capabilities") +def account_has_full_capabilities(context, capabilities): + hasPermission = (capabilities == "full") + for perm in ['can_withdraw', 'can_deposit', 'can_trade']: + assert context.agent.state.capability(perm) == hasPermission + +@then("the account should have a balance of {balance:d} {symbol} free") +def balance_for_symbol(context, balance, symbol): + (free, _) = context.agent.state.balance(symbol) + assert free == balance + +@then("there should be a {symbol} balance of {free:d} free and {locked:d} locked") +def check_symbol_balance(context, symbol, free, locked): + (_free, _locked) = context.agent.state.balance(symbol) + assert free == _free + assert locked == _locked + +@then("there should be a total of {count:d} balances") +def check_balance_count(context, count): + assert len(context.agent.state.balances.keys()) == count + +@then("the API should recieve an order of {quantity:g} {symbol}") +def check_for_order(context, quantity, symbol): + context.agent_params["adapter"].execute.assert_called_once() + # @todo adapter not called with trx params matching mock calc return + +@then("the API should not recieve any orders") +def check_no_orders_made(context): + context.agent_params["adapter"].execute.assert_not_called() diff --git a/test/scenarios/sync_agent.feature b/test/scenarios/sync_agent.feature new file mode 100644 index 0000000..2eb792f --- /dev/null +++ b/test/scenarios/sync_agent.feature @@ -0,0 +1,62 @@ +Feature: Sync Agent + The Sync Agent runs as an Actor, embedded within the Strategy. + The agent is responsible for maintaining a snapshot of the current state + of an account - meaning all orders and balances - whilst also coordinating + any market orders that are required. + + Scenario: Stay synchronised with account updates + Given a running sync agent awaiting messages + and an account update with full capabilities + When all messages are processed + Then the account should have full capabilities + Given an account update with minimal capabilities + When all messages are processed + Then the account should have minimal capabilities + When the sync agent is stopped + Then it should no longer be running + + Scenario: Stay synchronised with balance updates + Given a running sync agent awaiting messages + and a BTC balance of 50 free and 25 locked + And a balance update of 20 BTC + When all messages are processed + Then the account should have a balance of 70 BTC free + Given a balance update of -30 BTC + When all messages are processed + Then the account should have a balance of 40 BTC free + When the sync agent is stopped + Then it should no longer be running + + Scenario: Stay synchronised with position updates + Given a running sync agent awaiting messages + and an account position of BTC at 10 free and 25 locked + and a position update of ETH at 20 free and 50 locked + When all messages are 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 all messages are 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 + When the sync agent is stopped + Then it should no longer be running + + Scenario: Process authorised transaction requests + Given a running sync agent awaiting messages + Given a request to buy BTC + and the order of BTC is accepted with a size of 0.0032 + When all messages are processed + Then the API should recieve an order of 0.0032 BTC + When the sync agent is stopped + Then it should no longer be running + + Scenario: Skip declined transaction requests + Given a running sync agent awaiting messages + and a request to buy BTC + and the order is declined + When all messages are processed + Then the API should not recieve any orders + When the sync agent is stopped + Then it should no longer be running diff --git a/test/test_adapter.py b/test/test_adapter.py new file mode 100644 index 0000000..699d070 --- /dev/null +++ b/test/test_adapter.py @@ -0,0 +1,47 @@ +from algorunner.adapters.base import ( + factory, + register_adapter +) +from algorunner.adapters.messages import AdapterError +from algorunner.adapters._binance import BinanceAdapter +from queue import Queue + + +class invalid_adapter: + """ i do not have an identifier """ + pass + +class duplicate_adapter: + identifier = "binance" + pass + +def test_registry_requires_valid_identifier(): + have_exception = False + try: + register_adapter(invalid_adapter) + except AdapterError: + have_exception = True + + assert have_exception + +def test_registry_rejects_duplicate_adapters(): + have_exception = False + try: + register_adapter(invalid_adapter) + except AdapterError: + have_exception = True + + assert have_exception + +def test_factory_fails_when_adapter_is_unknown(): + have_exception = False + try: + factory("notarealadapter", sync_queue=Queue()) + except AdapterError: + have_exception = True + + assert have_exception + +def test_factory_returns_valid_adapter(): + adapter = factory("binance", sync_queue=Queue()) + assert isinstance(adapter, BinanceAdapter) diff --git a/test/test_monitoring.py b/test/test_monitoring.py new file mode 100644 index 0000000..387bf38 --- /dev/null +++ b/test/test_monitoring.py @@ -0,0 +1,46 @@ +from algorunner.hooks import Hook, hook +from unittest.mock import patch + + +from algorunner.monitoring import Timer + +def test_timer_returns_duration_in_ms(): + with patch('algorunner.monitoring.time') as time_mock: + time_mock.side_effect = [2, 3] + + t = Timer() + with t: + pass + + assert t.ms() == 1000 + +def test_timer_bubbles_exceptions(): + have_exc = False + + with patch('algorunner.monitoring.logger') as logger_mock: + try: + t = Timer() + with t: + raise Exception() + except Exception: + have_exc = True + + assert logger_mock.error.call_count == 1 + assert have_exc + +def test_timer_triggers_hooks(): + with patch('algorunner.monitoring.hook') as hook_mock: + with patch('algorunner.monitoring.time') as time_mock: + time_mock.side_effect = [2.3, 3.0] + + t = Timer(Hook.PROCESS_DURATION) + with t: + pass + + assert t.ms() == 700 + hook_mock.assert_called_once_with(Hook.PROCESS_DURATION, t.ms()) + +def test_user_provided_hooks_execute(example_strategy): + with patch.object(example_strategy, 'logger') as logger: + hook(Hook.API_EXECUTE_DURATION, 123.4) + logger.info.assert_called_once_with(f"api call duration: 123.4ms") diff --git a/test/test_mutations.py b/test/test_mutations.py new file mode 100644 index 0000000..7bb57a7 --- /dev/null +++ b/test/test_mutations.py @@ -0,0 +1,69 @@ +import pytest + +from algorunner.mutations import * + + +class NotAnEvent: + pass + + +@pytest.mark.parametrize("cls, is_valid", [ + (OrderUpdate, True), (BalanceUpdate, True), (AccountUpdate, True), + (AccountState, False), (NotAnEvent, False), (NoBalanceAvailable, False) +]) +def test_update_objects_registered(cls, is_valid): + assert is_update(cls) == is_valid + + +balance_update_cases = [ + (BalanceUpdate(asset="BTC", delta=100), [("BTC", 100.0)]), + (BalanceUpdate(asset="ETH", delta=33.12), [("BTC", 100.0), ("ETH", 33.12)]), + (BalanceUpdate(asset="BTC", delta=-53.2), [("BTC", 46.8), ("ETH", 33.12)]) +] +def test_balance_update(): + account = AccountState() + + for test in balance_update_cases: + update, expectations = test + update.handle(account) + + for e in expectations: + (free, _) = account.balance(e[0]) + assert free == e[1] + + +account_update_cases = [ + ( + AccountUpdate(balances=[Position(asset="BTC", free=523.1, locked=32)]), + [("BTC", 523.1, 32)] + ), + ( + AccountUpdate(balances=[Position(asset="ETH", free=32.1, locked=0)]), + [("BTC", 523.1, 32), ("ETH", 32.1, 0)] + ), + ( + AccountUpdate(balances=[Position(asset="BTC", free=555.1, locked=0)]), + [("BTC", 555.1, 0), ("ETH", 32.1, 0)] + ), + ( + AccountUpdate(balances=[ + Position(asset="BTC", free=500.1, locked=55.1), + Position(asset="ETH", free=20, locked=12.1) + ]), [("BTC", 500.1, 55.1), ("ETH", 20, 12.1)] + ) +] +def test_account_update(): + account = AccountState() + + for test in account_update_cases: + test[0].handle(account) + + for balance in test[1]: + (symbol, free, locked) = balance + assert (free, locked) == account.balance(symbol) + + +def test_order_update(): + # @todo - we need to really investigate the underlying payload + # and exactly what we want to do here. + pass diff --git a/test/test_runner.py b/test/test_runner.py new file mode 100644 index 0000000..4418c08 --- /dev/null +++ b/test/test_runner.py @@ -0,0 +1,38 @@ + +from signal import SIGTERM +from unittest.mock import MagicMock, patch + +from algorunner.adapters import Credentials +from algorunner.adapters.messages import AdapterError +from algorunner.runner import Runner + + +def test_handle_graceful_shutdown(mock_adapter: MagicMock, mock_strategy: MagicMock): + with patch('algorunner.runner.signal') as mock_signal: + r = Runner( + creds=Credentials(exchange="binance", key="", secret=""), + strategy=mock_strategy + ) + + # check components are ran + mock_strategy.start_sync.assert_called_once() + mock_adapter.connect.assert_called_once() + + # check signal handler is configured + mock_signal.assert_called_once() + signal, handler = mock_signal.call_args.args + + # check signal handler terminates components + handler(SIGTERM, None) + mock_adapter.disconnect.assert_called_once() + mock_strategy.shutdown.assert_called_once() + + +def invalid_exchange_should_trigger_exception(mock_strategy): + have_exception = False + try: + Runner(Credentials(exchange="lolnoexchange"), mock_strategy) + except AdapterError: + have_exception = True + + assert have_exception diff --git a/test/test_strategy.py b/test/test_strategy.py new file mode 100644 index 0000000..f44be05 --- /dev/null +++ b/test/test_strategy.py @@ -0,0 +1,30 @@ +import pytest + +from algorunner.strategy import ( + load_strategy, + StrategyNotFound, + InvalidStrategyProvided +) + +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