From a40c920f8d4565a4eb09de141069e2ca0e973dc1 Mon Sep 17 00:00:00 2001 From: Fergus Date: Tue, 5 May 2020 15:25:05 +0100 Subject: [PATCH 01/15] V2 Issue 1: Rearchitecture and new build process This - mammoth - Pull Request introduces a whole new structure to the service, in addition to a new build process, better dependency manag- ement, new tests, and more extensibility. In essence: this is a new service, and this is commit #1. --- .behaverc | 3 + .flake8 | 3 + .github/workflows/pythonapp.yml | 28 +- Dockerfile | 14 + Makefile | 21 +- README.md | 52 +- algorunner/__init__.py | 0 algorunner/abstract/__init__.py | 2 + algorunner/abstract/calculator.py | 11 + algorunner/abstract/strategy.py | 24 + algorunner/adapters/__init__.py | 9 + algorunner/adapters/_binance.py | 169 ++++ algorunner/adapters/base.py | 34 + algorunner/events.py | 52 ++ algorunner/exceptions.py | 26 + algorunner/runner.py | 28 + algorunner/strategy.py | 51 ++ algorunner/trader.py | 73 ++ example.py | 38 - lib/__init__.py | 4 - lib/account.py | 92 -- lib/runner.py | 83 -- plain.output | 55 ++ poetry.lock | 827 ++++++++++++++++++ pyproject.toml | 21 + run.py | 81 ++ setup.sh | 7 + strategies/__init__.py | 0 strategies/example.py | 23 + test/TESTING.md | 19 + test/__init__.py | 3 - test/adapters/__init__.py | 0 test/adapters/binance/__init__.py | 0 test/adapters/binance/fixtures.py | 8 + .../binance/test_user_transformations.py | 75 ++ test/fixtures/__init__.py | 0 test/fixtures/{ => binance}/account.json | 0 .../{ => binance}/balance_update.json | 0 .../{ => binance}/execution_report.json | 0 .../{ => binance}/outbound_account_info.json | 0 .../outbound_account_position.json | 0 test/fixtures/invalid_strategy.py | 4 + test/fixtures/valid_strategy.py | 5 + test/helpers.py | 14 + test/scenarios/account_updates.feature | 50 ++ test/scenarios/steps/account.py | 94 ++ test/test_account.py | 3 + test/{account.py => test_account.pyold} | 0 test/test_runner.py | 3 + test/{runner.py => test_runner.pyold} | 0 test/test_strategy.py | 27 + 51 files changed, 1860 insertions(+), 276 deletions(-) create mode 100644 .behaverc create mode 100644 .flake8 create mode 100644 Dockerfile create mode 100644 algorunner/__init__.py create mode 100644 algorunner/abstract/__init__.py create mode 100644 algorunner/abstract/calculator.py create mode 100644 algorunner/abstract/strategy.py create mode 100644 algorunner/adapters/__init__.py create mode 100644 algorunner/adapters/_binance.py create mode 100644 algorunner/adapters/base.py create mode 100644 algorunner/events.py create mode 100644 algorunner/exceptions.py create mode 100644 algorunner/runner.py create mode 100644 algorunner/strategy.py create mode 100644 algorunner/trader.py delete mode 100644 example.py delete mode 100644 lib/__init__.py delete mode 100644 lib/account.py delete mode 100644 lib/runner.py create mode 100644 plain.output create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 run.py create mode 100644 setup.sh create mode 100644 strategies/__init__.py create mode 100644 strategies/example.py create mode 100644 test/TESTING.md create mode 100644 test/adapters/__init__.py create mode 100644 test/adapters/binance/__init__.py create mode 100644 test/adapters/binance/fixtures.py create mode 100644 test/adapters/binance/test_user_transformations.py create mode 100644 test/fixtures/__init__.py rename test/fixtures/{ => binance}/account.json (100%) rename test/fixtures/{ => binance}/balance_update.json (100%) rename test/fixtures/{ => binance}/execution_report.json (100%) rename test/fixtures/{ => binance}/outbound_account_info.json (100%) rename test/fixtures/{ => binance}/outbound_account_position.json (100%) create mode 100644 test/fixtures/invalid_strategy.py create mode 100644 test/fixtures/valid_strategy.py create mode 100644 test/helpers.py create mode 100644 test/scenarios/account_updates.feature create mode 100644 test/scenarios/steps/account.py create mode 100644 test/test_account.py rename test/{account.py => test_account.pyold} (100%) create mode 100644 test/test_runner.py rename test/{runner.py => test_runner.pyold} (100%) create mode 100644 test/test_strategy.py diff --git a/.behaverc b/.behaverc new file mode 100644 index 0000000..303b787 --- /dev/null +++ b/.behaverc @@ -0,0 +1,3 @@ +[behave] +format=plain +paths=test/scenarios \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..d02b1e1 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 120 +exclude=test/* \ No newline at end of file diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 8513108..f4e4384 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -1,7 +1,7 @@ # 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: @@ -10,26 +10,26 @@ on: 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: Lint with flake8 + - name: Run linter run: | - pip install flake8 - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest + make lint + - name: Run tests run: | make test diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7e69ebd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.9-slim + +RUN apt-get update && apt-get install build-essential -y + +WORKDIR /algorunner +COPY poetry.lock pyproject.toml Makefile setup.sh /algorunner/ + +RUN make env-check + +# @todo --no-dev --no-ansi +RUN poetry config virtualenvs.create false && make deps + +COPY . /code +ENTRYPOINT [ "make" "run" ] diff --git a/Makefile b/Makefile index 351f855..3e20980 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,20 @@ -.PHONY: deps test +.PHONY: env-check build lint deps test run + +env-check: + @sh setup.sh + +build: + echo "build docker container" + +lint: + poetry run flake8 deps: - pip install pandas - pip install python-binance + poetry install --no-interaction test: - python -m test.account - python -m test.runner \ No newline at end of file + poetry run pytest + poetry run behave + +run: + poetry run python run.py \ No newline at end of file diff --git a/README.md b/README.md index 442ea13..8f888ed 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,27 @@ -# Runner ![Runner](https://github.com/FergusInLondon/Runner/workflows/Runner/badge.svg) +# ... @todo? -A massive WIP that may or may not be worth an actual README at this point in time. It has no tests, and the account functionality is still being baked in. +## Running -Currently it does invoke a strategy and provides it with real-time streamed data from Binance though. +@todo -### Note -This is *vaguely* related to my form of Enigma Catalyst, as (a) I really want to brush up on my Python, and (b) Binance seems like the best exchange to implement streaming trades on - so I'd like to get used to interacting with them. +### Configuration -## Example +@todo -Check `example.py` for a runnable version of this strategy: +## Development -```python -class ExampleStrategy(object): - """ - A simple example strategy that computes the average price change over - the previous 5 2000ms updates. - """ +### Docker +There's also a `Dockerfile` contained in this repository; this installs all the requirements to commence development. - def start(self, control): - self.series = pd.DataFrame() - self.control = control +### Finding Tasks - def process(self, kline): - self.series = self.series.append(kline) +The codebase is littered with `@todo` tags where low-hanging fruit is marked when discovered/encountered. - if self.series.shape[0] > 5: - print("Average price change over past 5 windows: ", pd.to_numeric(self.series[-5:]["PriceChange"]).mean()) ``` +➜ adapters git:(huge-refactor) ✗ grep -r '@todo' . + ./binance/test_user_transformations.py: pass # @todo - transformation not implemented +➜ adapters git:(huge-refactor) ✗ ../.. -When executed via the runner, this will calculate the average price change over the past 5 2000ms updates, and display it to the user. - -``` -python example.py -Average price change over past 5 windows: 26.694 -Average price change over past 5 windows: 26.356 -Average price change over past 5 windows: 26.444 -Average price change over past 5 windows: 26.246000000000002 -Average price change over past 5 windows: 26.272000000000002 -Average price change over past 5 windows: 26.706 -Average price change over past 5 windows: 27.142000000000003 -Average price change over past 5 windows: 27.182 -Average price change over past 5 windows: 27.562 -Average price change over past 5 windows: 28.002 -Average price change over past 5 windows: 28.246000000000002 -Average price change over past 5 windows: 28.49 -Average price change over past 5 windows: 28.754 +➜ Runner git:(huge-refactor) ✗ grep -r '@todo' . | wc -l + 17 ``` diff --git a/algorunner/__init__.py b/algorunner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/algorunner/abstract/__init__.py b/algorunner/abstract/__init__.py new file mode 100644 index 0000000..c891bd1 --- /dev/null +++ b/algorunner/abstract/__init__.py @@ -0,0 +1,2 @@ +from algorunner.abstract.strategy import Strategy # noqa: F401 +from algorunner.abstract.calculator import Calculator # noqa: F401 diff --git a/algorunner/abstract/calculator.py b/algorunner/abstract/calculator.py new file mode 100644 index 0000000..a9d6f32 --- /dev/null +++ b/algorunner/abstract/calculator.py @@ -0,0 +1,11 @@ +from abc import ABC + + +class Calculator(ABC): + """ + A `Calculator` is responsible for determining whether a position should + be opened, and how those positions should be sized/placed. + + @todo: interface TBD + """ + pass diff --git a/algorunner/abstract/strategy.py b/algorunner/abstract/strategy.py new file mode 100644 index 0000000..6d5bca2 --- /dev/null +++ b/algorunner/abstract/strategy.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod + +import pandas as pd + + +class Strategy(ABC): + """ + A `Strategy` is the container for an algorithm, it simply needs to respond + to incoming market payloads and be able to generate events for the `Account` + Actor. + """ + @abstractmethod + def process(self, tick: pd.DataFrame): + """ + @todo - accept Union[pd.DataFrame, RawMarketPayload] + where RawMarketPayload is a TypedDict w/ no pandas processing. + """ + pass + + def dispatch(self): + """ + @todo - fire events to the Actor queue. Concrete implementation. + """ + pass diff --git a/algorunner/adapters/__init__.py b/algorunner/adapters/__init__.py new file mode 100644 index 0000000..db2603b --- /dev/null +++ b/algorunner/adapters/__init__.py @@ -0,0 +1,9 @@ +from algorunner.adapters._binance import BinanceAdapter +from algorunner.adapters.base import ( # noqa: F401 + Credentials, + InvalidPayloadRecieved +) + +ADAPTERS = { + "binance": BinanceAdapter +} diff --git a/algorunner/adapters/_binance.py b/algorunner/adapters/_binance.py new file mode 100644 index 0000000..965067e --- /dev/null +++ b/algorunner/adapters/_binance.py @@ -0,0 +1,169 @@ +from logging import getLogger +from typing import Tuple + +from binance.client import Client +from binance import BinanceSocketManager +import pandas as pd + + +from algorunner.abstract.strategy import Strategy +from algorunner.adapters.base import ( + Adapter, Credentials, InvalidPayloadRecieved +) +from algorunner.trader import Trader +from algorunner.events import ( + AccountStatus, + BalanceUpdate, + Position, + PositionStatus, + UpdateEvent, + UpdateType +) + + +logger = getLogger() + + +class BinanceAdapter(Adapter): + """ """ + + class MarketStreamPandasTransformer: + def __call__(self, tick) -> pd.DataFrame: + """Converts the inbound tick to something exchange-agnostic.""" + df = pd.DataFrame([tick]) + df.rename(columns=lambda col: { + 'e': "24hrTicker", + 'E': "EventTime", + 's': "Symbol", + 'p': "PriceChange", + 'P': "PriceChangePercent", + 'w': "WeightedAveragePrice", + 'x': "FirstTradePrice", + 'c': "LastPrice", + 'Q': "LastQuantity", + 'b': "BestBidPrice", + 'B': "BestBidQuantity", + 'a': "BestAskPrice", + 'A': "BestAskQuantity", + 'o': "OpenPrice", + 'h': "HighPrice", + 'l': "LowPrice", + 'v': "TotalTradedBaseAssetVolume", + 'q': "TotalTradedQuoteAssetVolume", + 'O': "StatisticsOpenTime", + 'C': "StatisticsCloseTime", + 'F': "FirstTradeId", + 'L': "LastTradeId", + 'n': "TotalNumberOfTrades", + }[col], + inplace=True) + df.set_index('EventTime', inplace=True) + df.index = pd.to_datetime(df.index, unit='ms') + return df + + class UserStreamEventTransformer: + """ """ + + def __call__(self, payload) -> Tuple[str, UpdateEvent]: + try: + message_map = { + "outboundAccountInfo": self.account_update, + "outboundAccountPosition": self.position_update, + "balanceUpdate": self.balance_update, + "executionReport": self.order_report + } + + return message_map[payload["e"]](payload) + except KeyError: + msg = "unknown payload type {p}".format(p=payload.get("e")) + raise InvalidPayloadRecieved(msg) + except Exception as e: + raise Exception("unknown error occured in user stream", e) + + def initial_rest_payload(self, payload) -> AccountStatus: + # @todo - there is so much data in this payload that we're missing + # out on, like commission rates etc. + return AccountStatus( + CanWithdraw=payload["canWithdraw"], + CanTrade=payload["canTrade"], + CanDeposit=payload["canDeposit"], + Positions={ + balance["asset"]: Position( + Locked=float(balance["locked"]), + Free=float(balance["free"]) + ) for balance in payload["balances"] + } + ) + + def account_update(self, payload) -> Tuple[str, AccountStatus]: + return UpdateType.ACCOUNT, AccountStatus( + CanWithdraw=payload["W"], + CanTrade=payload["T"], + CanDeposit=payload["D"], + Positions={ + balance["a"]: Position( + Locked=float(balance["l"]), + Free=float(balance["f"]) + ) for balance in payload["B"] + } + ) + + def balance_update(self, payload) -> Tuple[str, BalanceUpdate]: + return UpdateType.BALANCE, BalanceUpdate( + Asset=payload["a"], Update=float(payload["d"]) + ) + + def position_update(self, payload) -> Tuple[str, PositionStatus]: + return UpdateType.POSITION, { + balance["a"]: Position( + Locked=float(balance["l"]), + Free=float(balance["f"]) + ) for balance in payload["B"] + } + + def order_report(self, payload): + # @todo - never did work out how to handle these. + pass + + def connect(self, creds: Credentials, trader: Trader): + self.trader = trader + self.client = Client(creds['key'], creds['secret']) + self.socket_manager = BinanceSocketManager(self.client) + + self.user_transformer = self.UserStreamEventTransformer() + self.market_transformer = self.MarketStreamPandasTransformer() + update = self.transformer.initial_rest_payload( + self.client.get_account() + ) + self.trader(UpdateType.ACCOUNT, update) + + self.socket_manager.start_user_socket(self.handle_user_stream) + + def run(self, strategy: Strategy, symbol: str): + self.strategy = strategy + self.socket_manager.start_symbol_ticker_socket( + symbol, self._handle_ticker + ) + + def _handle_ticker(self, tick): + """Given an incoming payload from the market websocket stream, + prepare it for the `Strategy` and then execute the strategy + against it.""" + try: + parsed_data = self.market_transformer(tick) + self.strategy.process(parsed_data) + except InvalidPayloadRecieved as e: + logger.warn( + "received exception when handling market stream. ignoring tick.", + e + ) + + def _handle_user_stream(self, payload): + try: + update_type, transformed = self.user_transformer(payload) + self.account(update_type, transformed) + except Exception as e: + logger.warn( + "recieved exception handling user stream payload. ignoring message.", + e + ) diff --git a/algorunner/adapters/base.py b/algorunner/adapters/base.py new file mode 100644 index 0000000..37f4b79 --- /dev/null +++ b/algorunner/adapters/base.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from typing import TypedDict + +from algorunner.abstract.strategy import Strategy +from algorunner.trader import Trader + + +class InvalidPayloadRecieved(Exception): + """InvalidPayloadRecieved is thrown when an invalid message is recieved + from the exchange via a websocket stream.""" + pass + + +class Credentials(TypedDict): + """Required credentials to authenticate with a given exchange.""" + exchange: str + key: str + secret: str + + +class Adapter(ABC): + """Required interface that an exchange adapter must implement.""" + + @abstractmethod + def connect(self, creds: Credentials, trader: Trader): + """connect authenticates with the exchange, and also populates + the associated `Trader` object with the latest state.""" + pass + + @abstractmethod + def run(self, strategy: Strategy): + """run executes the underlying strategy, ensuring that any data + transformation required is carried out correctly.""" + pass diff --git a/algorunner/events.py b/algorunner/events.py new file mode 100644 index 0000000..7244ae1 --- /dev/null +++ b/algorunner/events.py @@ -0,0 +1,52 @@ +from enum import Enum +from typing import ( + Dict, NamedTuple, TypedDict, Union +) + + +class UpdateType(Enum): + """ """ + BALANCE = 1 + ACCOUNT = 2 + POSITION = 3 + + +class Position(NamedTuple): + """ """ + Free: float + Locked: float + + +PositionStatus = Dict[str, Position] + + +class AccountStatus(TypedDict): + """ """ + CanWithdraw: bool + CanTrade: bool + CanDeposit: bool + Positions: PositionStatus + + +class BalanceUpdate(TypedDict): + """ """ + Asset: str + Update: float + + +class AccountEventAction(Enum): + """ """ + NO_ACTION = 1 + BUY = 2 + SELL = 3 + + +UpdateEvent = Union[AccountStatus, BalanceUpdate, PositionStatus] + + +class CalculationResult(Enum): + """ """ + INSUFFICIENT_FUNDS = 1 + TRANSACTION_REJECTED = 2 + POSITION_UPDATED = 3 + SUCCESSFUL_REBALANCE = 4 diff --git a/algorunner/exceptions.py b/algorunner/exceptions.py new file mode 100644 index 0000000..d424f47 --- /dev/null +++ b/algorunner/exceptions.py @@ -0,0 +1,26 @@ +from typing import Optional, List + +MSG_INVALID_CONFIG = "unable to parse all required values from configuration" +MSG_INVALID_CONFIG_W_FIELDS = "unable to parse [{fields}] from configuration" +MSG_UNKNOWN_EXCHANGE = "unable to find adapter for exchange '{name}'" + + +class InvalidConfiguration(Exception): + """ + Raised when there's an issue with the provided configuration; either an + option not being specified, or an option being specified incorrectly. + """ + def __init__(self, invalid_fields: Optional[List[str]]): + self.message = ( + MSG_INVALID_CONFIG if not invalid_fields + else MSG_INVALID_CONFIG_W_FIELDS.format(fields=invalid_fields.join(", ")) + ) + + +class UnknownExchange(Exception): + """ + Raised when the exchange specified in the configuration is unknown. + """ + def __init__(self, exchange_name: str, exception: Optional[Exception]): + self.message = MSG_UNKNOWN_EXCHANGE.format(name=exchange_name) + self.exc = exception diff --git a/algorunner/runner.py b/algorunner/runner.py new file mode 100644 index 0000000..6b1a106 --- /dev/null +++ b/algorunner/runner.py @@ -0,0 +1,28 @@ +from algorunner.trader import Trader +from algorunner.adapters import ADAPTERS, Credentials +from algorunner.exceptions import UnknownExchange +from algorunner.abstract.strategy import Strategy + + +class Runner(object): + """ + The Runner is responsible for configuring all components required to execute + a trading algorithm. It's called by the entrypoint of the app, located in + `run.py`. + """ + + def __init__(self, creds: Credentials, symbol: str, strategy: Strategy): + adapter_cls = ADAPTERS.get(creds["exchange"]) + if not adapter_cls: + raise UnknownExchange(creds["exchange"]) + + self.account = Trader() + self.symbol = symbol + self.strategy = strategy + self.adapter = adapter_cls() + + self.adapter.connect(creds, self.account) + + def run(self): + """ """ + self.adapter.run(self.strategy, self.symbol) diff --git a/algorunner/strategy.py b/algorunner/strategy.py new file mode 100644 index 0000000..eb64faa --- /dev/null +++ b/algorunner/strategy.py @@ -0,0 +1,51 @@ +from importlib import import_module +from typing import Optional + +from algorunner.abstract import Strategy + + +class FailureLoadingStrategy(Exception): + """ + Raised when a Strategy cannot be instantiated; this may be down to + loading the Strategy, or errors that render it unexecutable. Also + stores the original exception if available. + """ + def __init__(self, strategy_name: str, exception: Optional[Exception]): + self.message = "unable to instantiate strategy '{name}'".format(name=strategy_name) + self.exc = exception + + +class InvalidStrategyProvided(Exception): + """Raised when the loaded strategy does no inherit from the base class.""" + pass + + +class StrategyNotFound(Exception): + """Raised when the module loader is unable to retrieve the strategy.""" + pass + + +_DEFAULT_STRATEGY_PARENT_MODULE = 'strategies.{module}' + + +def load_strategy(strategy_name: str, module_name: Optional[str] = None) -> Strategy: + """Dynamically load strategies located in the `/strategies` directory""" + if not module_name: + module_name = _DEFAULT_STRATEGY_PARENT_MODULE.format( + module=strategy_name.lower() + ) + + try: + module = import_module(module_name) + + _class = getattr(module, strategy_name) + if not issubclass(_class, Strategy): + raise InvalidStrategyProvided() + + return _class() + except InvalidStrategyProvided as e: + raise e + except ModuleNotFoundError: + raise StrategyNotFound() + except Exception as e: + raise FailureLoadingStrategy(strategy_name, e) diff --git a/algorunner/trader.py b/algorunner/trader.py new file mode 100644 index 0000000..ba5932f --- /dev/null +++ b/algorunner/trader.py @@ -0,0 +1,73 @@ +from algorunner.events import ( + AccountStatus, UpdateEvent, UpdateType +) + + +class Trader: + """ + The Trader is a model of a real "trader" - i.e it monitors for indicators + generated by it's strategy, applies calculations from rules defined by it's + `Calculator` instance to determine whether an order should be made, and if + so, at what rate/quantity. + """ + + def __call__(self, update_type: str, updated_props: UpdateEvent): + """Sets account state - i.e. balance and positions.""" + + if update_type == UpdateType.ACCOUNT: + self.status = updated_props + + """ + # This required python > 3.10.*; alas there's a bug with pip + # on this version. So this will likely need to be refactored + # to use simple if/else comparisons until pip 21.2.3 is released. + # @see https://github.com/pypa/pip/pull/10252 + # @todo + match update_type: + case UpdateType.BALANCE: + # @todo Eh, look at whatever fuckery is involved. + # surely the "Locked" balance needs updating to..?! + asset = updated_props["Asset"] + self.status.Positions[asset]["Free"] += updated_props["Update"] + case UpdateType.ACCOUNT: + self.status = updated_props + case UpdateType.POSITION: + self.status["Positions"] = updated_props + """ + pass + + def initial_state(self, status: AccountStatus): + self.status = status + + def start(self, handler): + pass + + def balance(self, asset): + return self.status.get(asset, None) + + +""" + # @todo - these will be events. + def buy(self, asset, amount, limit=False, price=0): + if limit: + self.binance.order_limit_buy( + symbol=asset, + quantity=amount, + price=price) + else: + self.binance.order_market_buy( + symbol=asset, + quantity=amount) + + # @todo - these will be events. + def sell(self, asset, amount, limit=False, price=0): + if limit: + self.binance.order_limit_sell( + symbol=asset, + quantity=amount, + price=price) + else: + self.binance.order_market_sell( + symbol=asset, + quantity=amount) +""" diff --git a/example.py b/example.py deleted file mode 100644 index e8cc76a..0000000 --- a/example.py +++ /dev/null @@ -1,38 +0,0 @@ -import pandas as pd -from lib.runner import Runner -import configparser - -class ExampleStrategy(object): - """ - A simple example strategy that computes the average price change over - the previous 5 2000ms updates. - """ - - def start(self, control): - self.series = pd.DataFrame() - self.control = control - - def process(self, kline): - self.series = self.series.append(kline) - - if self.series.shape[0] > 5: - print("Average price change over past 5 windows: ", pd.to_numeric(self.series[-5:]["PriceChange"]).mean()) - - -if __name__ == "__main__": - # Example Usage: - # - # Instantiate a `Runner`; providing API credentials, the symbol you wish to - # run your strategy against, and the actual strategy itself. Then simply call - # `.run()` to execute the strategy. - - cfg = configparser.ConfigParser() - cfg.read('bot.ini') - - strategy = ExampleStrategy() - runner = Runner( - apiKey = cfg['credentials']['ApiKey'], - apiSecret = cfg['credentials']['ApiSecret'], - symbol = cfg['strategy']['Symbol'], - runnable=strategy) - runner.run() diff --git a/lib/__init__.py b/lib/__init__.py deleted file mode 100644 index cc2c489..0000000 --- a/lib/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -import os -import sys - -sys.path.append(os.path.dirname(os.path.realpath(__file__))) diff --git a/lib/account.py b/lib/account.py deleted file mode 100644 index d715bc1..0000000 --- a/lib/account.py +++ /dev/null @@ -1,92 +0,0 @@ - -class Account(object): - """ - The Account is responsible for keeping track of user balances, it's - updated via a UserSocket. - """ - - can_withdraw = None - can_trade = None - can_deposit = None - balances = {} - - def __init__(self, payload): - self.can_withdraw = payload["canWithdraw"] - self.can_trade = payload["canTrade"] - self.can_deposit = payload["canDeposit"] - - for asset in payload["balances"]: - self.balances[asset["asset"]] = ( - float(asset["free"]), float(asset["locked"])) - - def __call__(self, payload): - """ - For simplicity Account is callable. - """ - - action = { - 'outboundAccountInfo': self.account_update, - 'outboundAccountPosition': self.position_update, - 'balanceUpdate': self.balance_update, - 'executionReport': self.order_report - }.get(payload["e"], - lambda p: print("unhandled account event ", p["e"])) - - action(payload) - - def account_update(self, payload): - self.can_withdraw = payload["W"] - self.can_trade = payload["T"] - self.can_deposit = payload["D"] - - for asset in payload["B"]: - self.balances[asset["a"]] = (float(asset["f"]), float(asset["l"])) - - def position_update(self, payload): - for asset in payload["B"]: - self.balances[asset["a"]] = (float(asset["f"]), float(asset["l"])) - - def balance_update(self, payload): - self.balances[payload["a"]] = ( - self.balances[payload["a"]][0] + float(payload["d"]), - self.balances[payload["a"]][1] - ) - - def order_report(self, payload): - # Not entirely sure what, if anything other than logging, we should do - # here. After all, actual account updates/state are already handled. - pass - - def capability_trade(self): - return self.can_trade - - def capability_withdraw(self): - return self.can_withdraw - - def capability_deposit(self): - return self.can_deposit - - def balance(self, asset): - return self.balances.get(asset, None) - - def buy(self, asset, amount, limit=False, price=0): - if limit: - self.binance.order_limit_buy( - symbol=asset, - quantity=amount, - price=price) - else: - self.binance.order_market_buy( - symbol=asset, - quantity=amount) - - def sell(self, asset, amount, limit=False, price=0): - if limit: - self.binance.order_limit_sell( - symbol=asset, - quantity=amount, - price=price) - else: - self.binance.order_market_sell( - symbol=asset, - quantity=amount) diff --git a/lib/runner.py b/lib/runner.py deleted file mode 100644 index d927f01..0000000 --- a/lib/runner.py +++ /dev/null @@ -1,83 +0,0 @@ -import pandas as pd - -from account import Account -from binance.client import Client -from binance.websockets import BinanceSocketManager - - -class Runner(object): - """ - The Runner is responsible for handling any exchange interactions, - and running a given `Strategy` against the data that is provided - from the exchange. - - A `Strategy` is expected to have two methods (a) `start()` - which - performs any initialisation - such as configuring instance attributes, - and (b) `process(runner, tick_dataframe)`, which computes any actions - to run based upon the incoming data from the exchange. - """ - - def __init__(self, apiKey, apiSecret, symbol, runnable): - self.client = Client(apiKey, apiSecret) - - account_info = self.client.get_account() - self.account = Account(account_info) - - self.bm = BinanceSocketManager(self.client) - self.bm.start_user_socket(self.account) - - self.symbol = symbol - self.runnable = runnable - - def run(self): - """ - Run is the main event loop, where data taken from the exchange - is subsequently passed on to the underlying strategy. - - By passing this instance in to the strategy - via the `run(r, d)` - method - it's possible for the strategy to invoke methods that - interact with the exchange, via this runner; i.e to make buy/sell - calls. - """ - try: - self.runnable.start(self) - except AttributeError: - print("invalid runnable: no 'start' method") - self.bm.start_symbol_ticker_socket( - self.symbol, - lambda kline: self.runnable.process(self.parse_dataframe(kline))) - self.bm.start() - - def parse_dataframe(self, kline): - """ - """ - df = pd.DataFrame([kline]) - df.rename(columns=lambda col: { - 'e': "24hrTicker", - 'E': "EventTime", - 's': "Symbol", - 'p': "PriceChange", - 'P': "PriceChangePercent", - 'w': "WeightedAveragePrice", - 'x': "FirstTradePrice", - 'c': "LastPrice", - 'Q': "LastQuantity", - 'b': "BestBidPrice", - 'B': "BestBidQuantity", - 'a': "BestAskPrice", - 'A': "BestAskQuantity", - 'o': "OpenPrice", - 'h': "HighPrice", - 'l': "LowPrice", - 'v': "TotalTradedBaseAssetVolume", - 'q': "TotalTradedQuoteAssetVolume", - 'O': "StatisticsOpenTime", - 'C': "StatisticsCloseTime", - 'F': "FirstTradeId", - 'L': "LastTradeId", - 'n': "TotalNumberOfTrades", - }[col], - inplace=True) - df.set_index('EventTime', inplace=True) - df.index = pd.to_datetime(df.index, unit='ms') - return df diff --git a/plain.output b/plain.output new file mode 100644 index 0000000..5232138 --- /dev/null +++ b/plain.output @@ -0,0 +1,55 @@ +Feature: Account State + Background: + + Scenario: Stay synchronised with account updates + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given an account update with full capabilities ... passed in 0.000s + When that account update is processed ... passed in 0.000s + Then the account should have full capabilities ... passed in 0.000s + Given an account update with minimal capabilities ... passed in 0.000s + When that account update is processed ... passed in 0.000s + Then the account should have minimal capabilities ... passed in 0.000s + + Scenario: Stay synchronised with balance updates + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given a BTC balance of 50 free and 25 locked ... passed in 0.000s + And a balance update of 20 BTC ... passed in 0.000s + When that balance update is processed ... passed in 0.000s + Then the account should have a balance of 70 BTC free ... passed in 0.000s + Given a balance update of -30 BTC ... passed in 0.000s + When that balance update is processed ... passed in 0.000s + Then the account should have a balance of 40 BTC free ... passed in 0.000s + + Scenario: Stay synchronised with position updates + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given an account position of BTC at 10 free and 25 locked ... passed in 0.000s + And a position update of ETH at 20 free and 50 locked ... passed in 0.000s + When that position update is processed ... passed in 0.000s + Then there should be a BTC balance of 10 free and 25 locked ... passed in 0.000s + And there should be a ETH balance of 20 free and 50 locked ... passed in 0.000s + And there should be a total of 2 balances ... passed in 0.000s + Given a position update of BTC at 5 free and 30 locked ... passed in 0.000s + When that position update is processed ... passed in 0.000s + Then there should be a BTC balance of 5 free and 30 locked ... passed in 0.000s + And there should be a ETH balance of 20 free and 50 locked ... passed in 0.000s + And there should be a total of 2 balances ... passed in 0.000s + + Scenario: Process orders requests approved by the calculator + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given a market order to buy BTC ... passed in 0.000s + And the calculator will accept the order with a size of 0.0032 ... passed in 0.000s + When the order event is processed ... passed in 0.000s + Then the API should recieve an order of 0.0032 BTC ... passed in 0.000s + + Scenario: Skip order requests rejected by the calculator + Given an account with no initial state ... passed in 0.000s + And that account is currently awaiting messages ... passed in 0.000s + Given a market order to buy BTC ... passed in 0.000s + And the calculator will reject the order ... passed in 0.000s + When the order event is processed ... passed in 0.000s + Then the API should not recieve any orders ... passed in 0.000s + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..259a864 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,827 @@ +[[package]] +name = "aiohttp" +version = "3.7.4.post0" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +async-timeout = ">=3.0,<4.0" +attrs = ">=17.3.0" +chardet = ">=2.0,<5.0" +multidict = ">=4.5,<7.0" +typing-extensions = ">=3.6.5" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["aiodns", "brotlipy", "cchardet"] + +[[package]] +name = "async-timeout" +version = "3.0.1" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.5.3" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "behave" +version = "1.2.6" +description = "behave is behaviour-driven development, Python style" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +parse = ">=1.8.2" +parse-type = ">=0.4.2" +six = ">=1.11" + +[package.extras] +develop = ["coverage", "pytest (>=3.0)", "pytest-cov", "tox", "invoke (>=0.21.0)", "path.py (>=8.1.2)", "pycmd", "pathlib", "modernize (>=0.5)", "pylint"] +docs = ["sphinx (>=1.6)", "sphinx-bootstrap-theme (>=0.6)"] + +[[package]] +name = "certifi" +version = "2021.5.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "charset-normalizer" +version = "2.0.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.1" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "dateparser" +version = "1.0.0" +description = "Date parsing library designed to parse dates from HTML pages" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +python-dateutil = "*" +pytz = "*" +regex = "!=2019.02.19" +tzlocal = "*" + +[package.extras] +calendars = ["convertdate", "hijri-converter", "convertdate"] + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "idna" +version = "3.2" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "multidict" +version = "5.1.0" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "numpy" +version = "1.21.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pandas" +version = "1.3.1" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.7.1" + +[package.dependencies] +numpy = ">=1.17.3" +python-dateutil = ">=2.7.3" +pytz = ">=2017.3" + +[package.extras] +test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"] + +[[package]] +name = "parse" +version = "1.19.0" +description = "parse() is the opposite of format()" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "parse-type" +version = "0.5.2" +description = "Simplifies to build parse types based on the parse module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" + +[package.dependencies] +parse = ">=1.8.4" +six = ">=1.11" + +[package.extras] +develop = ["coverage (>=4.4)", "pytest (>=3.2)", "pytest-cov", "tox (>=2.8)"] +docs = ["sphinx (>=1.2)"] + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "python-binance" +version = "1.0.12" +description = "Binance REST API python implementation" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +aiohttp = "*" +dateparser = "*" +requests = "*" +six = "*" +ujson = "*" +websockets = "*" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2021.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "regex" +version = "2021.8.3" +description = "Alternative regular expression module, to replace re." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "tzlocal" +version = "2.1" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pytz = "*" + +[[package]] +name = "ujson" +version = "4.0.2" +description = "Ultra fast JSON encoder and decoder for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.6" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "websockets" +version = "9.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "yarl" +version = "1.6.3" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "78dd347ad4cf94fcfb10cb9987109c1a20b0b27c2c7dbfc049fc5a7cda5c3716" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, + {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, +] +async-timeout = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +behave = [ + {file = "behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"}, + {file = "behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86"}, +] +certifi = [ + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, +] +click = [ + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, +] +dateparser = [ + {file = "dateparser-1.0.0-py2.py3-none-any.whl", hash = "sha256:17202df32c7a36e773136ff353aa3767e987f8b3e27374c39fd21a30a803d6f8"}, + {file = "dateparser-1.0.0.tar.gz", hash = "sha256:159cc4e01a593706a15cd4e269a0b3345edf3aef8bf9278a57dac8adf5bf1e4a"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +idna = [ + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +multidict = [ + {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"}, + {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"}, + {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"}, + {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"}, + {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"}, + {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"}, + {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"}, + {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"}, + {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"}, + {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"}, + {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"}, + {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"}, + {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, +] +numpy = [ + {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, + {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, + {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, + {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, + {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, + {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, + {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, + {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, + {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, +] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] +pandas = [ + {file = "pandas-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1ee8418d0f936ff2216513aa03e199657eceb67690995d427a4a7ecd2e68f442"}, + {file = "pandas-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d9acfca191140a518779d1095036d842d5e5bc8e8ad8b5eaad1aff90fe1870d"}, + {file = "pandas-1.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e323028ab192fcfe1e8999c012a0fa96d066453bb354c7e7a4a267b25e73d3c8"}, + {file = "pandas-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d06661c6eb741ae633ee1c57e8c432bb4203024e263fe1a077fa3fda7817fdb"}, + {file = "pandas-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:23c7452771501254d2ae23e9e9dac88417de7e6eff3ce64ee494bb94dc88c300"}, + {file = "pandas-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7150039e78a81eddd9f5a05363a11cadf90a4968aac6f086fd83e66cf1c8d1d6"}, + {file = "pandas-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5c09a2538f0fddf3895070579082089ff4ae52b6cb176d8ec7a4dacf7e3676c1"}, + {file = "pandas-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905fc3e0fcd86b0a9f1f97abee7d36894698d2592b22b859f08ea5a8fe3d3aab"}, + {file = "pandas-1.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ee927c70794e875a59796fab8047098aa59787b1be680717c141cd7873818ae"}, + {file = "pandas-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c976e023ed580e60a82ccebdca8e1cc24d8b1fbb28175eb6521025c127dab66"}, + {file = "pandas-1.3.1-cp38-cp38-win32.whl", hash = "sha256:22f3fcc129fb482ef44e7df2a594f0bd514ac45aabe50da1a10709de1b0f9d84"}, + {file = "pandas-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45656cd59ae9745a1a21271a62001df58342b59c66d50754390066db500a8362"}, + {file = "pandas-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:114c6789d15862508900a25cb4cb51820bfdd8595ea306bab3b53cd19f990b65"}, + {file = "pandas-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:527c43311894aff131dea99cf418cd723bfd4f0bcf3c3da460f3b57e52a64da5"}, + {file = "pandas-1.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb3b33dde260b1766ea4d3c6b8fbf6799cee18d50a2a8bc534cf3550b7c819a"}, + {file = "pandas-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c28760932283d2c9f6fa5e53d2f77a514163b9e67fd0ee0879081be612567195"}, + {file = "pandas-1.3.1-cp39-cp39-win32.whl", hash = "sha256:be12d77f7e03c40a2466ed00ccd1a5f20a574d3c622fe1516037faa31aa448aa"}, + {file = "pandas-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9e1fe6722cbe27eb5891c1977bca62d456c19935352eea64d33956db46139364"}, + {file = "pandas-1.3.1.tar.gz", hash = "sha256:341935a594db24f3ff07d1b34d1d231786aa9adfa84b76eab10bf42907c8aed3"}, +] +parse = [ + {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"}, +] +parse-type = [ + {file = "parse_type-0.5.2-py2.py3-none-any.whl", hash = "sha256:089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e"}, + {file = "parse_type-0.5.2.tar.gz", hash = "sha256:7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +python-binance = [ + {file = "python-binance-1.0.12.tar.gz", hash = "sha256:f3eaa3e93ce6f227073bc6cd131fb1fe9c9bd22195d3305ba75d523511474d49"}, + {file = "python_binance-1.0.12-py2.py3-none-any.whl", hash = "sha256:941f0aa9420559e175612fcb2b9f836b98ffe06a812c01dd8ef7bdb132635435"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytz = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, +] +regex = [ + {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, + {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"}, + {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"}, + {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, + {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"}, + {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"}, + {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, + {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"}, + {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"}, + {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"}, + {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"}, + {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"}, + {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, +] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] +tzlocal = [ + {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, + {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, +] +ujson = [ + {file = "ujson-4.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e390df0dcc7897ffb98e17eae1f4c442c39c91814c298ad84d935a3c5c7a32fa"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:84b1dca0d53b0a8d58835f72ea2894e4d6cf7a5dd8f520ab4cbd698c81e49737"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:91396a585ba51f84dc71c8da60cdc86de6b60ba0272c389b6482020a1fac9394"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:eb6b25a7670c7537a5998e695fa62ff13c7f9c33faf82927adf4daa460d5f62e"}, + {file = "ujson-4.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f8aded54c2bc554ce20b397f72101737dd61ee7b81c771684a7dd7805e6cca0c"}, + {file = "ujson-4.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:30962467c36ff6de6161d784cd2a6aac1097f0128b522d6e9291678e34fb2b47"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:fc51e545d65689c398161f07fd405104956ec27f22453de85898fa088b2cd4bb"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e6e90330670c78e727d6637bb5a215d3e093d8e3570d439fd4922942f88da361"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:5e1636b94c7f1f59a8ead4c8a7bab1b12cc52d4c21ababa295ffec56b445fd2a"}, + {file = "ujson-4.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e2cadeb0ddc98e3963bea266cc5b884e5d77d73adf807f0bda9eca64d1c509d5"}, + {file = "ujson-4.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a214ba5a21dad71a43c0f5aef917cd56a2d70bc974d845be211c66b6742a471c"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0190d26c0e990c17ad072ec8593647218fe1c675d11089cd3d1440175b568967"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f273a875c0b42c2a019c337631bc1907f6fdfbc84210cc0d1fff0e2019bbfaec"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d3a87888c40b5bfcf69b4030427cd666893e826e82cc8608d1ba8b4b5e04ea99"}, + {file = "ujson-4.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:7333e8bc45ea28c74ae26157eacaed5e5629dbada32e0103c23eb368f93af108"}, + {file = "ujson-4.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b3a6dcc660220539aa718bcc9dbd6dedf2a01d19c875d1033f028f212e36d6bb"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0ea07fe57f9157118ca689e7f6db72759395b99121c0ff038d2e38649c626fb1"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d6d061563470cac889c0a9fd367013a5dbd8efc36ad01ab3e67a57e56cad720"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b5c70704962cf93ec6ea3271a47d952b75ae1980d6c56b8496cec2a722075939"}, + {file = "ujson-4.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:aad6d92f4d71e37ea70e966500f1951ecd065edca3a70d3861b37b176dd6702c"}, + {file = "ujson-4.0.2.tar.gz", hash = "sha256:c615a9e9e378a7383b756b7e7a73c38b22aeb8967a8bfbffd4741f7ffd043c4d"}, +] +urllib3 = [ + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, +] +websockets = [ + {file = "websockets-9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da"}, + {file = "websockets-9.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4"}, + {file = "websockets-9.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58"}, + {file = "websockets-9.1-cp36-cp36m-win32.whl", hash = "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a"}, + {file = "websockets-9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b"}, + {file = "websockets-9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af"}, + {file = "websockets-9.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd"}, + {file = "websockets-9.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2"}, + {file = "websockets-9.1-cp37-cp37m-win32.whl", hash = "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a"}, + {file = "websockets-9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857"}, + {file = "websockets-9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe"}, + {file = "websockets-9.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0"}, + {file = "websockets-9.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02"}, + {file = "websockets-9.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f"}, + {file = "websockets-9.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec"}, + {file = "websockets-9.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc"}, + {file = "websockets-9.1-cp38-cp38-win32.whl", hash = "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e"}, + {file = "websockets-9.1-cp38-cp38-win_amd64.whl", hash = "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077"}, + {file = "websockets-9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2"}, + {file = "websockets-9.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d"}, + {file = "websockets-9.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d"}, + {file = "websockets-9.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c"}, + {file = "websockets-9.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135"}, + {file = "websockets-9.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd"}, + {file = "websockets-9.1-cp39-cp39-win32.whl", hash = "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20"}, + {file = "websockets-9.1-cp39-cp39-win_amd64.whl", hash = "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0"}, + {file = "websockets-9.1.tar.gz", hash = "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3"}, +] +yarl = [ + {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"}, + {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"}, + {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"}, + {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"}, + {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"}, + {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"}, + {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"}, + {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"}, + {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"}, + {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"}, + {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"}, + {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"}, + {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c2afae0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "AlgoRunner" +version = "0.0.1" +description = "Trading Algorithm execution for cryptocurrency exchanges." +authors = ["Fergus In London "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.9" +click = "^8.0.1" +pandas = "^1.3.1" +python-binance = "^1.0.12" + +[tool.poetry.dev-dependencies] +behave = "^1.2.6" +pytest = "^6.2.4" +flake8 = "^3.9.2" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/run.py b/run.py new file mode 100644 index 0000000..507ed95 --- /dev/null +++ b/run.py @@ -0,0 +1,81 @@ +import configparser +from logging import getLogger + +import click + +from algorunner import exceptions +from algorunner.runner import ( + Credentials, Runner +) +from algorunner.strategy import load_strategy + + +logger = getLogger() + + +@click.command() +@click.option('-c', '--config', 'config_file', default='bot.ini', short_help='Configuration file.') +@click.option('-s', '--strategy', 'strategy_name', short_help='Name of Strategy to run') +@click.option('--testing', isflag=True, default=True, short_help='Run in testing mode, NOT live') +@click.option('--exchange', envvar='ALGORUNNER_EXCHANGE', short_help='Crypto exchange to execute strategy against') +@click.option('--api-key', envvar='ALGORUNNER_API_KEY', short_help='API Key for exchange/broker') +@click.option('--api-secret', envvar='ALGORUNNER_API_SECRET', short_help='API Secret for exchange/broker') +@click.option('--trading-symbol', envvar='ALGORUNNER_TRADING_SYMBOL', short_help='Symbol to execute strategy against') +def entrypoint( + config_file: str, + strategy_name: str, + testing: bool, + exchange: str, + apiKey: str, + apiSecret: str, + trading_symbol: str +): + """AlgoRunner is a simple runner for executing trading strategies against + cryptocurrency exchanges, with support for executing backtests. By default + AlgoRunner will run in BACKTEST mode. + + All configuration can be done through a .INI file, although some parameters + can be passed as CLI arguments and/or environment variables. + + For full details see https://github.com/fergusinlondon/algorunner + """ + + if not testing: + logger.warn("WARNING: Running in LIVE trading mode.") + + cfg = configparser.ConfigParser() + cfg.read(config_file) + + try: + apiKey = apiKey if apiKey else cfg['credentials']['api_key'] + apiSecret = apiSecret if apiSecret else cfg['credentials']['api_secret'] + strategy_name = strategy_name if strategy_name else cfg['strategy']['name'] + exchange = exchange if exchange else cfg['credentials']['exchange'] + trading_symbol = trading_symbol if trading_symbol else cfg['credentials']['symbol'] + except KeyError: + raise exceptions.InvalidConfiguration(exceptions.MSG_MISSING_CONFIG) + + # Filter out any empty config options + if not all([apiKey, apiSecret, strategy_name, exchange, trading_symbol]): + raise exceptions.InvalidConfiguration(exceptions.MSG_MISSING_CONFIG) + + strategy = load_strategy(strategy_name) + runner = Runner(Credentials( + exchange=exchange, + key=apiKey, + secret=apiSecret + ), trading_symbol, strategy) + runner.run() + + +if __name__ == "__main__": + try: + entrypoint() + except exceptions.InvalidConfiguration as e: + logger.critical("CRITICAL FAILURE: incorrect configuration provided", e.message) + except exceptions.FailureLoadingStrategy as e: + logger.critical("CRITICAL FAILURE: unable to instantiate strategy", e.message) + except exceptions.UnknownExchange as e: + logger.critical("CRITICAL FAILURE: invalid exchange specified in config", e.message) + except Exception as e: + logger.critical("CRITICAL FAILURE: Terminating...", e) diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..396cec2 --- /dev/null +++ b/setup.sh @@ -0,0 +1,7 @@ +if ! command -v poetry &> /dev/null +then + echo "Poetry not found, installing from pip..." + pip install poetry +else + echo "Poetry is already installed, you're good to go!" +fi diff --git a/strategies/__init__.py b/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/strategies/example.py b/strategies/example.py new file mode 100644 index 0000000..97986d7 --- /dev/null +++ b/strategies/example.py @@ -0,0 +1,23 @@ +import pandas as pd + +from algorunner.strategy import Strategy + + +class Example(Strategy): + """ + A simple example strategy that computes the average price change over + the previous 5 2000ms updates. + """ + + # this tag is used in a unit test targeting the strategy loader. ignore! + _testing_tag = True + + def __init__(self): + self.series = pd.DataFrame() + + def process(self, tick): + self.series = self.series.append(tick) + + if self.series.shape[0] > 5: + recent_window = pd.to_numeric(self.series[-5:]["PriceChange"]) + print("Average price change over past 5 windows: ", recent_window.mean()) diff --git a/test/TESTING.md b/test/TESTING.md new file mode 100644 index 0000000..9bbded7 --- /dev/null +++ b/test/TESTING.md @@ -0,0 +1,19 @@ +# Testing + +The living document linked to from the `README` outlines the tests expected/required to run. + +## Execution + +To run the tests use the `test` make target - i.e `$ make test`. + +## Test Types + +### BDD Scenarios +The `scenarios` folder contains some BDD style tests - using `behave` - for testing the `Trader` object. + +The decision to introduce additional BDD style tests was taken as `Trader` is based around a rough interpretation of the `Actor` model. In real terms this means all interactions go through a concurrent message-style interface; the implementation in this repository utilises `TypedDict` objects named `*Update`, that get dispatched through a `queue.Queue`. + + +### Unit Tests + +This directory contains the unit tests for application, there are notes as to which components require testing - and the various cases that need to be accounted for - detailed in the notion.so document. diff --git a/test/__init__.py b/test/__init__.py index 6b214be..e69de29 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,3 +0,0 @@ -import os -import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lib'))) diff --git a/test/adapters/__init__.py b/test/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/adapters/binance/__init__.py b/test/adapters/binance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/adapters/binance/fixtures.py b/test/adapters/binance/fixtures.py new file mode 100644 index 0000000..7b4602c --- /dev/null +++ b/test/adapters/binance/fixtures.py @@ -0,0 +1,8 @@ +from pytest import fixture + +from algorunner.adapters import BinanceAdapter + + +@fixture +def user_transformer(): + return BinanceAdapter.UserStreamEventTransformer() \ No newline at end of file diff --git a/test/adapters/binance/test_user_transformations.py b/test/adapters/binance/test_user_transformations.py new file mode 100644 index 0000000..d833186 --- /dev/null +++ b/test/adapters/binance/test_user_transformations.py @@ -0,0 +1,75 @@ +from algorunner.events import UpdateType + +from test.helpers import * +from test.adapters.binance.fixtures import * + + +_FIXTURE_PATTERN = "test/fixtures/binance/{fixture}.json" +ACCOUNT_UPDATE_PATTERN = _FIXTURE_PATTERN.format(fixture="account") +BALANCE_UPDATE_PATTERN = _FIXTURE_PATTERN.format(fixture="balance_update") +EXECUTION_REPORT_PATTERN = _FIXTURE_PATTERN.format(fixture="execution_report") +ACCOUNT_INFORMATION_PATTERN = _FIXTURE_PATTERN.format(fixture="outbound_account_info") +ACCOUNT_POSITION_PATTERN = _FIXTURE_PATTERN.format(fixture="outbound_account_position") + + +def check_position(positions, symbol, free, locked): + assert symbol in positions + assert positions[symbol].Free == free + assert positions[symbol].Locked == locked + + +def test_account_transformation(user_transformer, load_fixture): + with load_fixture(ACCOUNT_UPDATE_PATTERN) as account_payload: + transformed = user_transformer.initial_rest_payload(account_payload) + + assert not transformed['CanDeposit'] + assert transformed['CanTrade'] + assert transformed['CanWithdraw'] + + assert len(transformed['Positions']) == 2 + check_position(transformed['Positions'], 'BTC', 4723846.89208129, 0.0) + check_position(transformed['Positions'], 'LTC', 4763368.68006011, 0.0) + + +def test_stream_balance_update_transformation(user_transformer, load_fixture): + with load_fixture(BALANCE_UPDATE_PATTERN) as balance: + update_type, update_object = user_transformer(balance) + + assert update_type == UpdateType.BALANCE + assert update_object['Asset'] == 'BTC' + assert update_object['Update'] == 100.0 + + +def stream_execution_report_transformation(user_transformer, load_fixture): + with load_fixture(EXECUTION_REPORT_PATTERN) as execution: + update_type, update_object = user_transformer(execution) + + pass # @todo - transformation not implemented + + +def test_stream_account_info_transformation(user_transformer, load_fixture): + with load_fixture(ACCOUNT_INFORMATION_PATTERN) as information: + update_type, update_object = user_transformer(information) + + assert update_type == UpdateType.ACCOUNT + + assert update_object['CanDeposit'] + assert update_object['CanTrade'] + assert update_object['CanWithdraw'] + + assert len(update_object['Positions']) == 5 + check_position(update_object['Positions'], 'LTC', 17366.18538083, 0.0) + check_position(update_object['Positions'], 'BTC', 10537.85314051, 2.19464093) + check_position(update_object['Positions'], 'ETH', 17902.35190619, 0.0) + check_position(update_object['Positions'], 'BNC', 1114503.29769312, 0.0) + check_position(update_object['Positions'], 'NEO', 0.0, 0.0) + + +def stream_account_position_transformation(user_transformer, load_fixture): + with load_fixture(ACCOUNT_POSITION_PATTERN) as position: + update_type, update_object = user_transformer(position) + + assert update_type == UpdateType.POSITION + assert "BTC" in update_object + assert update_object["BTC"].Free + assert update_object["BTC"].Locked diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/account.json b/test/fixtures/binance/account.json similarity index 100% rename from test/fixtures/account.json rename to test/fixtures/binance/account.json diff --git a/test/fixtures/balance_update.json b/test/fixtures/binance/balance_update.json similarity index 100% rename from test/fixtures/balance_update.json rename to test/fixtures/binance/balance_update.json diff --git a/test/fixtures/execution_report.json b/test/fixtures/binance/execution_report.json similarity index 100% rename from test/fixtures/execution_report.json rename to test/fixtures/binance/execution_report.json diff --git a/test/fixtures/outbound_account_info.json b/test/fixtures/binance/outbound_account_info.json similarity index 100% rename from test/fixtures/outbound_account_info.json rename to test/fixtures/binance/outbound_account_info.json diff --git a/test/fixtures/outbound_account_position.json b/test/fixtures/binance/outbound_account_position.json similarity index 100% rename from test/fixtures/outbound_account_position.json rename to test/fixtures/binance/outbound_account_position.json diff --git a/test/fixtures/invalid_strategy.py b/test/fixtures/invalid_strategy.py new file mode 100644 index 0000000..61af4a8 --- /dev/null +++ b/test/fixtures/invalid_strategy.py @@ -0,0 +1,4 @@ + +class InvalidStrategy(): + # strategy doesn't implement required base class or methods + pass \ No newline at end of file diff --git a/test/fixtures/valid_strategy.py b/test/fixtures/valid_strategy.py new file mode 100644 index 0000000..6cd8386 --- /dev/null +++ b/test/fixtures/valid_strategy.py @@ -0,0 +1,5 @@ +from algorunner.abstract.strategy import Strategy + +class ValidStrategy(Strategy): + def process(self, tick): + return True \ No newline at end of file diff --git a/test/helpers.py b/test/helpers.py new file mode 100644 index 0000000..9af5c26 --- /dev/null +++ b/test/helpers.py @@ -0,0 +1,14 @@ +from contextlib import contextmanager +from contextlib import contextmanager +from json import load +from pytest import fixture + + +@fixture +def load_fixture(): + @contextmanager + def open_fixture(payload_file): + with open(payload_file) as json_file: + yield load(json_file) + + return open_fixture diff --git a/test/scenarios/account_updates.feature b/test/scenarios/account_updates.feature new file mode 100644 index 0000000..e23beaa --- /dev/null +++ b/test/scenarios/account_updates.feature @@ -0,0 +1,50 @@ +Feature: Account State + The Account object manages state associated with an exchange user. + The Account must be able to handle updates taken from user data, as well as + coordinate with the Calculator and API Adapter to make transactions. + + Background: + Given an account with no initial state + and that account is currently awaiting messages + + Scenario: Stay synchronised with account updates + Given an account update with full capabilities + When that account update is processed + Then the account should have full capabilities + Given an account update with minimal capabilities + When that account update is processed + Then the account should have minimal capabilities + + Scenario: Stay synchronised with balance updates + Given a BTC balance of 50 free and 25 locked + And a balance update of 20 BTC + When that balance update is processed + Then the account should have a balance of 70 BTC free + Given a balance update of -30 BTC + When that balance update is processed + Then the account should have a balance of 40 BTC free + + Scenario: Stay synchronised with position updates + Given an account position of BTC at 10 free and 25 locked + and a position update of ETH at 20 free and 50 locked + When that position update is processed + Then there should be a BTC balance of 10 free and 25 locked + and there should be a ETH balance of 20 free and 50 locked + and there should be a total of 2 balances + Given a position update of BTC at 5 free and 30 locked + When that position update is processed + Then there should be a BTC balance of 5 free and 30 locked + and there should be a ETH balance of 20 free and 50 locked + and there should be a total of 2 balances + + Scenario: Process orders requests approved by the calculator + Given a market order to buy BTC + and the calculator will accept the order with a size of 0.0032 + When the order event is processed + Then the API should recieve an order of 0.0032 BTC + + Scenario: Skip order requests rejected by the calculator + Given a market order to buy BTC + and the calculator will reject the order + When the order event is processed + Then the API should not recieve any orders diff --git a/test/scenarios/steps/account.py b/test/scenarios/steps/account.py new file mode 100644 index 0000000..882b0f3 --- /dev/null +++ b/test/scenarios/steps/account.py @@ -0,0 +1,94 @@ +from behave import * + +from algorunner.events import ( + AccountStatus, UpdateType +) +from algorunner.trader import Trader + +@given("an account with no initial state") +def fresh_account(context): + context.trader = Trader() + +@given("that account is currently awaiting messages") +def account_running(context): + pass + +@given("an account update with {capabilities} capabilities") +def account_update_full_capabilities(context, capabilities): + hasPermission = (capabilities == "full") + context.account_update = AccountStatus( + CanWithdraw=hasPermission, + CanTrade=hasPermission, + CanDeposit=hasPermission + ) + +@when("that account update is processed") +def account_update_processed(context): + context.trader(UpdateType.ACCOUNT, context.account_update) + +@then("the account should have {capabilities} capabilities") +def account_has_full_capabilities(context, capabilities): + hasPermission = (capabilities == "full") + assert context.trader.status["CanWithdraw"] == hasPermission + assert context.trader.status["CanTrade"] == hasPermission + assert context.trader.status["CanDeposit"] == hasPermission + +@given("a {symbol} balance of {free} free and {locked} locked") +def current_balance(context, symbol, free, locked): + pass + +@given("a balance update of {quantity:g} {symbol}") +def balance_update(context, quantity, symbol): + pass + +@when("that balance update is processed") +def balance_updated_processed(context): + pass + +@then("the account should have a balance of {balance:d} {symbol} free") +def balance_for_symbol(context, balance, symbol): + pass + +@given("an account position of {symbol} at {free:d} free and {locked:d} locked") +def account_with_balance(context, symbol, free, locked): + pass + +@given("a position update of {symbol} at {free:d} free and {locked:d} locked") +def position_update(context, symbol, free, locked): + pass + +@when("that position update is processed") +def position_update_processed(context): + pass + +@then("there should be a {symbol} balance of {free:d} free and {locked:d} locked") +def check_symbol_balance(context, symbol, free, locked): + pass + +@then("there should be a total of {count:d} balances") +def check_balance_count(context, count): + pass + +@given("a market order to buy {symbol}") +def market_order(context, symbol): + pass + +@given("the calculator will reject the order") +def calculator_rejection(context): + pass + +@given("the calculator will accept the order with a size of {size:g}") +def calculator_accepted(context, size): + pass + +@when("the order event is processed") +def order_event_process(context): + pass + +@then("the API should recieve an order of {quantity:g} {symbol}") +def check_for_order(context, quantity, symbol): + pass + +@then("the API should not recieve any orders") +def check_no_orders_made(context): + pass diff --git a/test/test_account.py b/test/test_account.py new file mode 100644 index 0000000..91263b4 --- /dev/null +++ b/test/test_account.py @@ -0,0 +1,3 @@ + +def test_tests_are_running(): + assert True == True diff --git a/test/account.py b/test/test_account.pyold similarity index 100% rename from test/account.py rename to test/test_account.pyold diff --git a/test/test_runner.py b/test/test_runner.py new file mode 100644 index 0000000..91263b4 --- /dev/null +++ b/test/test_runner.py @@ -0,0 +1,3 @@ + +def test_tests_are_running(): + assert True == True diff --git a/test/runner.py b/test/test_runner.pyold similarity index 100% rename from test/runner.py rename to test/test_runner.pyold diff --git a/test/test_strategy.py b/test/test_strategy.py new file mode 100644 index 0000000..b8a5fb9 --- /dev/null +++ b/test/test_strategy.py @@ -0,0 +1,27 @@ +import pytest + +from algorunner.strategy import * + + +def test_default_strategies_module(): + strategy = load_strategy('Example') + assert strategy._testing_tag + + +def test_custom_strategies_module(): + strategy = load_strategy('ValidStrategy', 'test.fixtures.valid_strategy') + assert strategy.process(None) + + +@pytest.mark.parametrize("module, strategy, exception", [ + ('Ehe', 'definitely.not.a.real.module', StrategyNotFound), + ('InvalidStrategy', 'test.fixtures.invalid_strategy', InvalidStrategyProvided), +]) +def test_no_strategy_module_availabe(module, strategy, exception): + correct_exception = False + try: + load_strategy(module, strategy) + except exception: + correct_exception = True + + assert correct_exception From f139548f97df0962bfeb6d86aa8a0f99eff55f02 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Fri, 6 Aug 2021 18:58:29 +0100 Subject: [PATCH 02/15] incidental: enable CI on develop branch --- .github/workflows/pythonapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index f4e4384..391171f 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -5,7 +5,7 @@ name: AlgoRunner on: push: - branches: [ master ] + branches: [ master, develop ] pull_request: branches: [ master ] From dcf47d19662cc1fc6470467197f99923dfa5f62c Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Fri, 6 Aug 2021 19:07:09 +0100 Subject: [PATCH 03/15] incidental: line endings and .gitignore rule --- .behaverc | 2 +- .flake8 | 2 +- .gitignore | 3 +- Makefile | 2 +- plain.output | 55 ------------------------------- test/adapters/binance/fixtures.py | 2 +- test/fixtures/invalid_strategy.py | 2 +- test/fixtures/valid_strategy.py | 2 +- 8 files changed, 8 insertions(+), 62 deletions(-) delete mode 100644 plain.output diff --git a/.behaverc b/.behaverc index 303b787..e66490e 100644 --- a/.behaverc +++ b/.behaverc @@ -1,3 +1,3 @@ [behave] format=plain -paths=test/scenarios \ No newline at end of file +paths=test/scenarios diff --git a/.flake8 b/.flake8 index d02b1e1..558ad7f 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] max-line-length = 120 -exclude=test/* \ No newline at end of file +exclude=test/* diff --git a/.gitignore b/.gitignore index 1307c5e..2581463 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ __pycache__/ -bot.ini \ No newline at end of file +bot.ini +plain.output \ No newline at end of file diff --git a/Makefile b/Makefile index 3e20980..65a2184 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,4 @@ test: poetry run behave run: - poetry run python run.py \ No newline at end of file + poetry run python run.py diff --git a/plain.output b/plain.output deleted file mode 100644 index 5232138..0000000 --- a/plain.output +++ /dev/null @@ -1,55 +0,0 @@ -Feature: Account State - Background: - - Scenario: Stay synchronised with account updates - Given an account with no initial state ... passed in 0.000s - And that account is currently awaiting messages ... passed in 0.000s - Given an account update with full capabilities ... passed in 0.000s - When that account update is processed ... passed in 0.000s - Then the account should have full capabilities ... passed in 0.000s - Given an account update with minimal capabilities ... passed in 0.000s - When that account update is processed ... passed in 0.000s - Then the account should have minimal capabilities ... passed in 0.000s - - Scenario: Stay synchronised with balance updates - Given an account with no initial state ... passed in 0.000s - And that account is currently awaiting messages ... passed in 0.000s - Given a BTC balance of 50 free and 25 locked ... passed in 0.000s - And a balance update of 20 BTC ... passed in 0.000s - When that balance update is processed ... passed in 0.000s - Then the account should have a balance of 70 BTC free ... passed in 0.000s - Given a balance update of -30 BTC ... passed in 0.000s - When that balance update is processed ... passed in 0.000s - Then the account should have a balance of 40 BTC free ... passed in 0.000s - - Scenario: Stay synchronised with position updates - Given an account with no initial state ... passed in 0.000s - And that account is currently awaiting messages ... passed in 0.000s - Given an account position of BTC at 10 free and 25 locked ... passed in 0.000s - And a position update of ETH at 20 free and 50 locked ... passed in 0.000s - When that position update is processed ... passed in 0.000s - Then there should be a BTC balance of 10 free and 25 locked ... passed in 0.000s - And there should be a ETH balance of 20 free and 50 locked ... passed in 0.000s - And there should be a total of 2 balances ... passed in 0.000s - Given a position update of BTC at 5 free and 30 locked ... passed in 0.000s - When that position update is processed ... passed in 0.000s - Then there should be a BTC balance of 5 free and 30 locked ... passed in 0.000s - And there should be a ETH balance of 20 free and 50 locked ... passed in 0.000s - And there should be a total of 2 balances ... passed in 0.000s - - Scenario: Process orders requests approved by the calculator - Given an account with no initial state ... passed in 0.000s - And that account is currently awaiting messages ... passed in 0.000s - Given a market order to buy BTC ... passed in 0.000s - And the calculator will accept the order with a size of 0.0032 ... passed in 0.000s - When the order event is processed ... passed in 0.000s - Then the API should recieve an order of 0.0032 BTC ... passed in 0.000s - - Scenario: Skip order requests rejected by the calculator - Given an account with no initial state ... passed in 0.000s - And that account is currently awaiting messages ... passed in 0.000s - Given a market order to buy BTC ... passed in 0.000s - And the calculator will reject the order ... passed in 0.000s - When the order event is processed ... passed in 0.000s - Then the API should not recieve any orders ... passed in 0.000s - diff --git a/test/adapters/binance/fixtures.py b/test/adapters/binance/fixtures.py index 7b4602c..8e18045 100644 --- a/test/adapters/binance/fixtures.py +++ b/test/adapters/binance/fixtures.py @@ -5,4 +5,4 @@ @fixture def user_transformer(): - return BinanceAdapter.UserStreamEventTransformer() \ No newline at end of file + return BinanceAdapter.UserStreamEventTransformer() diff --git a/test/fixtures/invalid_strategy.py b/test/fixtures/invalid_strategy.py index 61af4a8..5b04a81 100644 --- a/test/fixtures/invalid_strategy.py +++ b/test/fixtures/invalid_strategy.py @@ -1,4 +1,4 @@ class InvalidStrategy(): # strategy doesn't implement required base class or methods - pass \ No newline at end of file + pass diff --git a/test/fixtures/valid_strategy.py b/test/fixtures/valid_strategy.py index 6cd8386..c222b17 100644 --- a/test/fixtures/valid_strategy.py +++ b/test/fixtures/valid_strategy.py @@ -2,4 +2,4 @@ class ValidStrategy(Strategy): def process(self, tick): - return True \ No newline at end of file + return True From 213befffe4334bb345df6260d566a9cfa843d4a7 Mon Sep 17 00:00:00 2001 From: Fergus Date: Thu, 12 Aug 2021 18:24:55 +0100 Subject: [PATCH 04/15] V2: Actor implementation for "Trader" (WIP) (#4) * Trader: Rough implementation of the Actor pattern * (V2) Concurrent Exchange<->Strategy synchronisation w/ messages This PR introduces a WIP implementation of the `SyncAgent`, an Actor-based component that is responsible for handling messages from both the strategy and the exchange. Inbound data from the exchange - via the user stream - is used to maintain a snapshot of the status of the account. Whilst messages originating from the strategy are used to generate transactions destined for the exchange. The SyncAgent runs within the context of it's own thread, and communication is managed via a Queue and associated message types. There are associated tests for the SyncAgent, written in Gherkin and using the `behave` python package. Incidental Work: - All changes to the `AccountStatus` entity are now done via combined update/mutation objects, available in the `mutations` module. - Trader/Account and Strategy entities are now combined, this allows more customisation when writing strategies, but also removes some moving parts. - User defined `Strategy` objects are now expected to implement any risk calculations - as opposed to having seperate `Calculator` objects. - The Runner is now capable of handling graceful shutdowns, and terminating the SyncAgent and API Adapter. - Unit test coverage has improved, although the BDD tests need work for compatibility with the new `SyncAgent` - Various amendments and tweaks to interface/abstract classes. - The `Makefile` now has a 'help' target. As usual, these changes are driven by the living design document on notion.so * Incidental: Clean up logging - stop passing logger around --- .gitignore | 2 +- Dockerfile | 2 + Makefile | 21 ++- README.md | 20 ++- algorunner/abstract/__init__.py | 3 +- algorunner/abstract/base_strategy.py | 141 ++++++++++++++++ algorunner/abstract/calculator.py | 11 -- algorunner/abstract/strategy.py | 24 --- algorunner/adapters/__init__.py | 1 + algorunner/adapters/_binance.py | 155 +++++++++--------- algorunner/adapters/base.py | 31 +++- algorunner/events.py | 52 ------ algorunner/exceptions.py | 45 ++++- algorunner/mutations.py | 136 +++++++++++++++ algorunner/runner.py | 52 ++++-- algorunner/strategy.py | 31 +--- algorunner/trader.py | 73 --------- bot.example.ini | 8 +- poetry.lock | 36 +++- pyproject.toml | 1 + run.py | 56 +++---- strategies/example.py | 13 +- .../binance/{fixtures.py => fixtures.pyold} | 0 .../binance/test_user_transformations.py | 75 --------- .../binance/test_user_transformations.pyold | 83 ++++++++++ test/fixtures/valid_strategy.py | 11 +- test/helpers.py | 1 - test/scenarios/account_updates.feature | 50 ------ test/scenarios/steps/account.py | 94 ----------- test/scenarios/steps/sync_agent.py | 126 ++++++++++++++ test/scenarios/sync_agent.feature | 62 +++++++ test/test_account.py | 3 - test/test_account.pyold | 63 ------- test/test_mutations.py | 69 ++++++++ test/test_runner.py | 57 ++++++- test/test_runner.pyold | 104 ------------ test/test_strategy.py | 7 +- 37 files changed, 991 insertions(+), 728 deletions(-) create mode 100644 algorunner/abstract/base_strategy.py delete mode 100644 algorunner/abstract/calculator.py delete mode 100644 algorunner/abstract/strategy.py delete mode 100644 algorunner/events.py create mode 100644 algorunner/mutations.py delete mode 100644 algorunner/trader.py rename test/adapters/binance/{fixtures.py => fixtures.pyold} (100%) delete mode 100644 test/adapters/binance/test_user_transformations.py create mode 100644 test/adapters/binance/test_user_transformations.pyold delete mode 100644 test/scenarios/account_updates.feature delete mode 100644 test/scenarios/steps/account.py create mode 100644 test/scenarios/steps/sync_agent.py create mode 100644 test/scenarios/sync_agent.feature delete mode 100644 test/test_account.py delete mode 100644 test/test_account.pyold create mode 100644 test/test_mutations.py delete mode 100644 test/test_runner.pyold diff --git a/.gitignore b/.gitignore index 2581463..a93e8f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .idea/ __pycache__/ bot.ini -plain.output \ No newline at end of file +plain.output diff --git a/Dockerfile b/Dockerfile index 7e69ebd..37865cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,3 +12,5 @@ RUN poetry config virtualenvs.create false && make deps COPY . /code ENTRYPOINT [ "make" "run" ] + +# @todo - secondary layer with development dependencies removed diff --git a/Makefile b/Makefile index 65a2184..4914b0f 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,27 @@ -.PHONY: env-check build lint deps test run +.PHONY: env-check build lint deps test run todo -env-check: +help: ## Show this help. + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' + +env-check: ## Check that the current environment is capable of running AlgoRunner. @sh setup.sh -build: +build: ## Build docker image, tagged "algorunner:" echo "build docker container" -lint: +lint: ## Run code quality checks poetry run flake8 -deps: +deps: ## Install all required dependencies (including for development) poetry install --no-interaction -test: +test: ## Run all tests - including both unit tests and BDD scenarios poetry run pytest poetry run behave -run: +run: ## Run AlgoRunner poetry run python run.py + +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 e342521..8a2e7dc 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,26 @@ ## Development +### Make Targets + +The `Makefile` contains a selection of useful targets for simplfying the development workflow. + +``` +➜ Runner git:(v2/trader-actor) ✗ make help +help: Show this help. +env-check: Check that the current environment is capable of running AlgoRunner. +build: Build docker image, tagged "algorunner:" +lint: Run code quality checks +deps: Install all required dependencies (including for development) +test: Run all tests - including both unit tests and BDD feature tests +run: Run AlgoRunner +todo: Scan the codebase for items tagged with "@todo" + +``` + ### Docker -There's also a `Dockerfile` contained in this repository; this installs all the requirements to commence development. + +There's also a `Dockerfile` contained in this repository; this builds a `python:3.9-slim` based Docker Image, with all development dependencies. This can be built using the aforementioned `Makefile`. ### Finding Tasks diff --git a/algorunner/abstract/__init__.py b/algorunner/abstract/__init__.py index c891bd1..ab0bca2 100644 --- a/algorunner/abstract/__init__.py +++ b/algorunner/abstract/__init__.py @@ -1,2 +1 @@ -from algorunner.abstract.strategy import Strategy # noqa: F401 -from algorunner.abstract.calculator import Calculator # noqa: F401 +from algorunner.abstract.base_strategy import * # noqa: F401 F403 diff --git a/algorunner/abstract/base_strategy.py b/algorunner/abstract/base_strategy.py new file mode 100644 index 0000000..2235467 --- /dev/null +++ b/algorunner/abstract/base_strategy.py @@ -0,0 +1,141 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from queue import Queue +from threading import Thread +from typing import Callable, Optional + +from loguru import logger +import pandas as pd + +from algorunner.exceptions import StrategyExceptionThresholdBreached +from algorunner.mutations import AccountState, is_update +from algorunner.adapters.base import Adapter, TransactionParams + + +@dataclass +class TransactionRequest: + """Dispatched by `process` this triggers risk calculation via `authorise` + and potential dispatch of a transaction to the exchange.""" + symbol: str + order_type: str + + +@dataclass +class AuthorisationDecision: + """Returned by `authorise` and determines whether a transaction can be + made, and the appropriate parameters for that transaction.""" + accepted: bool + params: Optional[TransactionParams] + + +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 # count exceptions over past 5 mins. + 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("trader thread has completed") + + def _transaction_handler(self, trx: TransactionRequest): + decision = self.authorisation_guard(self.state, trx) + if not decision.accepted: + logger.info("transaction rejected: failed defined auth rules") + return + + logger.info("transaction accepted: passing to API adapter for dispatch") + self.api.execute(decision.params) + + def start_sync(self, queue: Queue, adapter: Adapter): + self.sync_agent = self.SyncAgent(queue, adapter, self.log) + self.sync_queue = queue + + def open_position(self, symbol: str): + logger.debug(f"requesting to open new position ({symbol})") + self.sync_queue.put(TransactionRequest(symbol=symbol, order_type="BUY")) + + def close_position(self, symbol: str): + logger.debug(f"requesting to close position ({symbol})") + self.sync_queue.put(TransactionRequest(symbol=symbol, order_type="SELL")) + + def shutdown(self): + self.sync_agent.stop("shutdown requested") + + @abstractmethod + def process(self, tick: pd.DataFrame): + """ + @todo - accept Union[pd.DataFrame, RawMarketPayload] + where RawMarketPayload is a TypedDict w/ no pandas processing. + """ + pass + + @abstractmethod + def authorise(self, + state: AccountState, + trx: TransactionRequest) -> AuthorisationDecision: + """ + @todo - define params. + """ + pass diff --git a/algorunner/abstract/calculator.py b/algorunner/abstract/calculator.py deleted file mode 100644 index a9d6f32..0000000 --- a/algorunner/abstract/calculator.py +++ /dev/null @@ -1,11 +0,0 @@ -from abc import ABC - - -class Calculator(ABC): - """ - A `Calculator` is responsible for determining whether a position should - be opened, and how those positions should be sized/placed. - - @todo: interface TBD - """ - pass diff --git a/algorunner/abstract/strategy.py b/algorunner/abstract/strategy.py deleted file mode 100644 index 6d5bca2..0000000 --- a/algorunner/abstract/strategy.py +++ /dev/null @@ -1,24 +0,0 @@ -from abc import ABC, abstractmethod - -import pandas as pd - - -class Strategy(ABC): - """ - A `Strategy` is the container for an algorithm, it simply needs to respond - to incoming market payloads and be able to generate events for the `Account` - Actor. - """ - @abstractmethod - def process(self, tick: pd.DataFrame): - """ - @todo - accept Union[pd.DataFrame, RawMarketPayload] - where RawMarketPayload is a TypedDict w/ no pandas processing. - """ - pass - - def dispatch(self): - """ - @todo - fire events to the Actor queue. Concrete implementation. - """ - pass diff --git a/algorunner/adapters/__init__.py b/algorunner/adapters/__init__.py index db2603b..b27fa19 100644 --- a/algorunner/adapters/__init__.py +++ b/algorunner/adapters/__init__.py @@ -1,5 +1,6 @@ from algorunner.adapters._binance import BinanceAdapter from algorunner.adapters.base import ( # noqa: F401 + Adapter, Credentials, InvalidPayloadRecieved ) diff --git a/algorunner/adapters/_binance.py b/algorunner/adapters/_binance.py index 965067e..c3bbd6f 100644 --- a/algorunner/adapters/_binance.py +++ b/algorunner/adapters/_binance.py @@ -1,32 +1,25 @@ -from logging import getLogger -from typing import Tuple +from typing import Callable from binance.client import Client from binance import BinanceSocketManager -import pandas as pd +import pandas as pd -from algorunner.abstract.strategy import Strategy from algorunner.adapters.base import ( Adapter, Credentials, InvalidPayloadRecieved ) -from algorunner.trader import Trader -from algorunner.events import ( - AccountStatus, - BalanceUpdate, - Position, - PositionStatus, - UpdateEvent, - UpdateType -) - -logger = getLogger() +from algorunner.mutations import ( + AccountUpdate, BaseUpdate, BalanceUpdate, CapabilitiesUpdate, Position +) class BinanceAdapter(Adapter): """ """ + class MarketStreamRawTransformer: + pass + class MarketStreamPandasTransformer: def __call__(self, tick) -> pd.DataFrame: """Converts the inbound tick to something exchange-agnostic.""" @@ -64,7 +57,7 @@ def __call__(self, tick) -> pd.DataFrame: class UserStreamEventTransformer: """ """ - def __call__(self, payload) -> Tuple[str, UpdateEvent]: + def __call__(self, payload) -> BaseUpdate: try: message_map = { "outboundAccountInfo": self.account_update, @@ -73,6 +66,7 @@ def __call__(self, payload) -> Tuple[str, UpdateEvent]: "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")) @@ -80,90 +74,97 @@ def __call__(self, payload) -> Tuple[str, UpdateEvent]: except Exception as e: raise Exception("unknown error occured in user stream", e) - def initial_rest_payload(self, payload) -> AccountStatus: + 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 AccountStatus( - CanWithdraw=payload["canWithdraw"], - CanTrade=payload["canTrade"], - CanDeposit=payload["canDeposit"], - Positions={ - balance["asset"]: Position( - Locked=float(balance["locked"]), - Free=float(balance["free"]) - ) for balance in payload["balances"] - } + 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) -> Tuple[str, AccountStatus]: - return UpdateType.ACCOUNT, AccountStatus( - CanWithdraw=payload["W"], - CanTrade=payload["T"], - CanDeposit=payload["D"], - Positions={ + 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"] } ) - - def balance_update(self, payload) -> Tuple[str, BalanceUpdate]: - return UpdateType.BALANCE, BalanceUpdate( - Asset=payload["a"], Update=float(payload["d"]) - ) - - def position_update(self, payload) -> Tuple[str, PositionStatus]: - return UpdateType.POSITION, { - balance["a"]: Position( - Locked=float(balance["l"]), - Free=float(balance["f"]) - ) for balance in payload["B"] - } + """ + pass def order_report(self, payload): # @todo - never did work out how to handle these. pass - def connect(self, creds: Credentials, trader: Trader): - self.trader = trader + 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() - update = self.transformer.initial_rest_payload( - self.client.get_account() + self.market_transformer = ( + self.MarketStreamPandasTransformer() if self.use_pandas + else self.MarketStreamRawTransformer() ) - self.trader(UpdateType.ACCOUNT, update) - self.socket_manager.start_user_socket(self.handle_user_stream) + 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() + )) - def run(self, strategy: Strategy, symbol: str): - self.strategy = strategy + # subscribe to all subsequent user events + self.socket_manager.start_user_socket( + lambda p: self.sync_queue.put(self.user_transformer(p)) + ) + + def run(self, symbol: str, process: Callable): self.socket_manager.start_symbol_ticker_socket( - symbol, self._handle_ticker + symbol, lambda p: process(self.market_transformer(p)) ) - def _handle_ticker(self, tick): - """Given an incoming payload from the market websocket stream, - prepare it for the `Strategy` and then execute the strategy - against it.""" - try: - parsed_data = self.market_transformer(tick) - self.strategy.process(parsed_data) - except InvalidPayloadRecieved as e: - logger.warn( - "received exception when handling market stream. ignoring tick.", - e - ) - def _handle_user_stream(self, payload): - try: - update_type, transformed = self.user_transformer(payload) - self.account(update_type, transformed) - except Exception as e: - logger.warn( - "recieved exception handling user stream payload. ignoring message.", - e - ) +""" + # @todo - these will come via execute(TransactionParams) + 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) + + do we want to return an identifier associated with the transaction + to allow monitoring via the event stream? I think so? + + # @todo - these will be events. + def sell(self, asset, amount, limit=False, price=0): + if limit: + self.binance.order_limit_sell( + symbol=asset, + quantity=amount, + price=price) + else: + self.binance.order_market_sell( + symbol=asset, + quantity=amount) +""" diff --git a/algorunner/adapters/base.py b/algorunner/adapters/base.py index 37f4b79..59a2e9d 100644 --- a/algorunner/adapters/base.py +++ b/algorunner/adapters/base.py @@ -1,8 +1,6 @@ from abc import ABC, abstractmethod -from typing import TypedDict - -from algorunner.abstract.strategy import Strategy -from algorunner.trader import Trader +from typing import Callable, TypedDict +from queue import Queue class InvalidPayloadRecieved(Exception): @@ -18,17 +16,38 @@ class Credentials(TypedDict): secret: str +class TransactionParams(TypedDict): + """Parameters detailing an execution to execute on an exchange.""" + pass + + 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: Credentials, trader: Trader): + def connect(self, creds: Credentials): """connect authenticates with the exchange, and also populates the associated `Trader` object with the latest state.""" pass @abstractmethod - def run(self, strategy: Strategy): + def monitor_user(self): + """ @todo """ + pass + + @abstractmethod + def run(self, process: Callable, terminated: bool): """run executes the underlying strategy, ensuring that any data transformation required is carried out correctly.""" pass + + @abstractmethod + def execute(self, trx: TransactionParams) -> bool: + pass + + @abstractmethod + def disconnect(self): + pass diff --git a/algorunner/events.py b/algorunner/events.py deleted file mode 100644 index 7244ae1..0000000 --- a/algorunner/events.py +++ /dev/null @@ -1,52 +0,0 @@ -from enum import Enum -from typing import ( - Dict, NamedTuple, TypedDict, Union -) - - -class UpdateType(Enum): - """ """ - BALANCE = 1 - ACCOUNT = 2 - POSITION = 3 - - -class Position(NamedTuple): - """ """ - Free: float - Locked: float - - -PositionStatus = Dict[str, Position] - - -class AccountStatus(TypedDict): - """ """ - CanWithdraw: bool - CanTrade: bool - CanDeposit: bool - Positions: PositionStatus - - -class BalanceUpdate(TypedDict): - """ """ - Asset: str - Update: float - - -class AccountEventAction(Enum): - """ """ - NO_ACTION = 1 - BUY = 2 - SELL = 3 - - -UpdateEvent = Union[AccountStatus, BalanceUpdate, PositionStatus] - - -class CalculationResult(Enum): - """ """ - INSUFFICIENT_FUNDS = 1 - TRANSACTION_REJECTED = 2 - POSITION_UPDATED = 3 - SUCCESSFUL_REBALANCE = 4 diff --git a/algorunner/exceptions.py b/algorunner/exceptions.py index d424f47..27740b1 100644 --- a/algorunner/exceptions.py +++ b/algorunner/exceptions.py @@ -10,7 +10,7 @@ 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]]): + 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(", ")) @@ -24,3 +24,46 @@ class UnknownExchange(Exception): def __init__(self, exchange_name: str, exception: Optional[Exception]): self.message = MSG_UNKNOWN_EXCHANGE.format(name=exchange_name) self.exc = exception + + +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}'" + + +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/mutations.py b/algorunner/mutations.py new file mode 100644 index 0000000..abcd6c6 --- /dev/null +++ b/algorunner/mutations.py @@ -0,0 +1,136 @@ +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/runner.py b/algorunner/runner.py index 6b1a106..15c2063 100644 --- a/algorunner/runner.py +++ b/algorunner/runner.py @@ -1,7 +1,19 @@ -from algorunner.trader import Trader -from algorunner.adapters import ADAPTERS, Credentials +from queue import Queue +from signal import SIGTERM, signal + +from loguru import logger + +from algorunner import abstract +from algorunner.adapters import ADAPTERS, Credentials, Adapter from algorunner.exceptions import UnknownExchange -from algorunner.abstract.strategy import Strategy + + +def get_adapter(exchange: str, *args, **kwargs) -> Adapter: + adapter_cls = ADAPTERS.get(exchange) + if not adapter_cls: + raise UnknownExchange(exchange) + + return adapter_cls(*args, **kwargs) class Runner(object): @@ -11,18 +23,32 @@ class Runner(object): `run.py`. """ - def __init__(self, creds: Credentials, symbol: str, strategy: Strategy): - adapter_cls = ADAPTERS.get(creds["exchange"]) - if not adapter_cls: - raise UnknownExchange(creds["exchange"]) - - self.account = Trader() - self.symbol = symbol + def __init__(self, + creds: Credentials, + strategy: abstract.BaseStrategy): + self.sync_queue = Queue() + self.adapter = get_adapter(creds["exchange"], self.sync_queue) self.strategy = strategy - self.adapter = adapter_cls() - self.adapter.connect(creds, self.account) + self.strategy.start_sync(self.sync_queue, self.adapter) + self.adapter.connect(creds) + signal(SIGTERM, self._handle_sigterm()) + logger.debug("finished initialising runner") + + def _handle_sigterm(self): + def _handler(signum, frame): + logger.warning("caught SIGTERM: attempting graceful termination") + self.stop() + + return _handler def run(self): """ """ - self.adapter.run(self.strategy, self.symbol) + self.adapter.monitor_user(self.trader_queue) + self.adapter.run(self.strategy, self.strategy.process) + logger.info("monitoring user stream and executing strategy") + + def stop(self): + logger.info("attempting to shutdown strategy execution and disconnect from exchange") + self.strategy.shutdown() + self.adapter.disconnect() diff --git a/algorunner/strategy.py b/algorunner/strategy.py index eb64faa..c683482 100644 --- a/algorunner/strategy.py +++ b/algorunner/strategy.py @@ -1,36 +1,21 @@ from importlib import import_module from typing import Optional -from algorunner.abstract import Strategy +from loguru import logger - -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 +from algorunner.abstract import BaseStrategy +from algorunner.exceptions import ( + FailureLoadingStrategy, InvalidStrategyProvided, StrategyNotFound +) _DEFAULT_STRATEGY_PARENT_MODULE = 'strategies.{module}' -def load_strategy(strategy_name: str, module_name: Optional[str] = None) -> Strategy: +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() ) @@ -39,7 +24,7 @@ def load_strategy(strategy_name: str, module_name: Optional[str] = None) -> Stra module = import_module(module_name) _class = getattr(module, strategy_name) - if not issubclass(_class, Strategy): + if not issubclass(_class, BaseStrategy): raise InvalidStrategyProvided() return _class() diff --git a/algorunner/trader.py b/algorunner/trader.py deleted file mode 100644 index ba5932f..0000000 --- a/algorunner/trader.py +++ /dev/null @@ -1,73 +0,0 @@ -from algorunner.events import ( - AccountStatus, UpdateEvent, UpdateType -) - - -class Trader: - """ - The Trader is a model of a real "trader" - i.e it monitors for indicators - generated by it's strategy, applies calculations from rules defined by it's - `Calculator` instance to determine whether an order should be made, and if - so, at what rate/quantity. - """ - - def __call__(self, update_type: str, updated_props: UpdateEvent): - """Sets account state - i.e. balance and positions.""" - - if update_type == UpdateType.ACCOUNT: - self.status = updated_props - - """ - # This required python > 3.10.*; alas there's a bug with pip - # on this version. So this will likely need to be refactored - # to use simple if/else comparisons until pip 21.2.3 is released. - # @see https://github.com/pypa/pip/pull/10252 - # @todo - match update_type: - case UpdateType.BALANCE: - # @todo Eh, look at whatever fuckery is involved. - # surely the "Locked" balance needs updating to..?! - asset = updated_props["Asset"] - self.status.Positions[asset]["Free"] += updated_props["Update"] - case UpdateType.ACCOUNT: - self.status = updated_props - case UpdateType.POSITION: - self.status["Positions"] = updated_props - """ - pass - - def initial_state(self, status: AccountStatus): - self.status = status - - def start(self, handler): - pass - - def balance(self, asset): - return self.status.get(asset, None) - - -""" - # @todo - these will be events. - def buy(self, asset, amount, limit=False, price=0): - if limit: - self.binance.order_limit_buy( - symbol=asset, - quantity=amount, - price=price) - else: - self.binance.order_market_buy( - symbol=asset, - quantity=amount) - - # @todo - these will be events. - def sell(self, asset, amount, limit=False, price=0): - if limit: - self.binance.order_limit_sell( - symbol=asset, - quantity=amount, - price=price) - else: - self.binance.order_market_sell( - symbol=asset, - quantity=amount) -""" diff --git a/bot.example.ini b/bot.example.ini index 9675d50..26480f4 100644 --- a/bot.example.ini +++ b/bot.example.ini @@ -1,6 +1,8 @@ [credentials] -ApiKey = binanceAPIKey -ApiSecret = binanceAPISecret +exchange = binance +api_key = binanceAPIKey +api_secret = binanceAPISecret [strategy] -Symbol = BTCUSDT \ No newline at end of file +name = Example +symbol = BTCUSDT \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 259a864..c1b4ebc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -156,6 +156,21 @@ category = "dev" optional = false python-versions = "*" +[[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 = "mccabe" version = "0.6.1" @@ -420,6 +435,17 @@ 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" @@ -435,7 +461,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "78dd347ad4cf94fcfb10cb9987109c1a20b0b27c2c7dbfc049fc5a7cda5c3716" +content-hash = "288c288e00be59e89ae280bb07007a531aca8a5f84610b7b7e6783d953c945c9" [metadata.files] aiohttp = [ @@ -527,6 +553,10 @@ idna = [ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +loguru = [ + {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, + {file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -786,6 +816,10 @@ websockets = [ {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"}, diff --git a/pyproject.toml b/pyproject.toml index c2afae0..0c193b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ python = "^3.9" click = "^8.0.1" pandas = "^1.3.1" python-binance = "^1.0.12" +loguru = "^0.5.3" [tool.poetry.dev-dependencies] behave = "^1.2.6" diff --git a/run.py b/run.py index 507ed95..dd2cdff 100644 --- a/run.py +++ b/run.py @@ -1,33 +1,30 @@ import configparser -from logging import getLogger import click +from loguru import logger -from algorunner import exceptions +from algorunner.exceptions import InvalidConfiguration from algorunner.runner import ( Credentials, Runner ) from algorunner.strategy import load_strategy -logger = getLogger() - - @click.command() -@click.option('-c', '--config', 'config_file', default='bot.ini', short_help='Configuration file.') -@click.option('-s', '--strategy', 'strategy_name', short_help='Name of Strategy to run') -@click.option('--testing', isflag=True, default=True, short_help='Run in testing mode, NOT live') -@click.option('--exchange', envvar='ALGORUNNER_EXCHANGE', short_help='Crypto exchange to execute strategy against') -@click.option('--api-key', envvar='ALGORUNNER_API_KEY', short_help='API Key for exchange/broker') -@click.option('--api-secret', envvar='ALGORUNNER_API_SECRET', short_help='API Secret for exchange/broker') -@click.option('--trading-symbol', envvar='ALGORUNNER_TRADING_SYMBOL', short_help='Symbol to execute strategy against') +@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, - apiKey: str, - apiSecret: str, + api_key: str, + api_secret: str, trading_symbol: str ): """AlgoRunner is a simple runner for executing trading strategies against @@ -41,29 +38,28 @@ def entrypoint( """ if not testing: - logger.warn("WARNING: Running in LIVE trading mode.") + logger.warning("running in LIVE trading mode") cfg = configparser.ConfigParser() cfg.read(config_file) try: - apiKey = apiKey if apiKey else cfg['credentials']['api_key'] - apiSecret = apiSecret if apiSecret else cfg['credentials']['api_secret'] - strategy_name = strategy_name if strategy_name else cfg['strategy']['name'] exchange = exchange if exchange else cfg['credentials']['exchange'] - trading_symbol = trading_symbol if trading_symbol else cfg['credentials']['symbol'] - except KeyError: - raise exceptions.InvalidConfiguration(exceptions.MSG_MISSING_CONFIG) + 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'] - # Filter out any empty config options - if not all([apiKey, apiSecret, strategy_name, exchange, trading_symbol]): - raise exceptions.InvalidConfiguration(exceptions.MSG_MISSING_CONFIG) + 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=apiKey, - secret=apiSecret + key=api_key, + secret=api_secret ), trading_symbol, strategy) runner.run() @@ -71,11 +67,5 @@ def entrypoint( if __name__ == "__main__": try: entrypoint() - except exceptions.InvalidConfiguration as e: - logger.critical("CRITICAL FAILURE: incorrect configuration provided", e.message) - except exceptions.FailureLoadingStrategy as e: - logger.critical("CRITICAL FAILURE: unable to instantiate strategy", e.message) - except exceptions.UnknownExchange as e: - logger.critical("CRITICAL FAILURE: invalid exchange specified in config", e.message) except Exception as e: - logger.critical("CRITICAL FAILURE: Terminating...", e) + logger.exception("encountered unrecoverable error. terminating algorunner.", e.message) diff --git a/strategies/example.py b/strategies/example.py index 97986d7..191a624 100644 --- a/strategies/example.py +++ b/strategies/example.py @@ -1,9 +1,12 @@ import pandas as pd -from algorunner.strategy import Strategy +from algorunner.abstract import BaseStrategy +from algorunner.abstract.base_strategy import ( + AccountState, TransactionRequest, AuthorisationDecision +) -class Example(Strategy): +class Example(BaseStrategy): """ A simple example strategy that computes the average price change over the previous 5 2000ms updates. @@ -13,7 +16,8 @@ class Example(Strategy): _testing_tag = True def __init__(self): - self.series = pd.DataFrame() + self.series = pd.DataFrame + super().__init__() def process(self, tick): self.series = self.series.append(tick) @@ -21,3 +25,6 @@ def process(self, tick): if self.series.shape[0] > 5: recent_window = pd.to_numeric(self.series[-5:]["PriceChange"]) print("Average price change over past 5 windows: ", recent_window.mean()) + + def authorise(self, state: AccountState, trx: TransactionRequest) -> AuthorisationDecision: + pass diff --git a/test/adapters/binance/fixtures.py b/test/adapters/binance/fixtures.pyold similarity index 100% rename from test/adapters/binance/fixtures.py rename to test/adapters/binance/fixtures.pyold diff --git a/test/adapters/binance/test_user_transformations.py b/test/adapters/binance/test_user_transformations.py deleted file mode 100644 index d833186..0000000 --- a/test/adapters/binance/test_user_transformations.py +++ /dev/null @@ -1,75 +0,0 @@ -from algorunner.events import UpdateType - -from test.helpers import * -from test.adapters.binance.fixtures import * - - -_FIXTURE_PATTERN = "test/fixtures/binance/{fixture}.json" -ACCOUNT_UPDATE_PATTERN = _FIXTURE_PATTERN.format(fixture="account") -BALANCE_UPDATE_PATTERN = _FIXTURE_PATTERN.format(fixture="balance_update") -EXECUTION_REPORT_PATTERN = _FIXTURE_PATTERN.format(fixture="execution_report") -ACCOUNT_INFORMATION_PATTERN = _FIXTURE_PATTERN.format(fixture="outbound_account_info") -ACCOUNT_POSITION_PATTERN = _FIXTURE_PATTERN.format(fixture="outbound_account_position") - - -def check_position(positions, symbol, free, locked): - assert symbol in positions - assert positions[symbol].Free == free - assert positions[symbol].Locked == locked - - -def test_account_transformation(user_transformer, load_fixture): - with load_fixture(ACCOUNT_UPDATE_PATTERN) as account_payload: - transformed = user_transformer.initial_rest_payload(account_payload) - - assert not transformed['CanDeposit'] - assert transformed['CanTrade'] - assert transformed['CanWithdraw'] - - assert len(transformed['Positions']) == 2 - check_position(transformed['Positions'], 'BTC', 4723846.89208129, 0.0) - check_position(transformed['Positions'], 'LTC', 4763368.68006011, 0.0) - - -def test_stream_balance_update_transformation(user_transformer, load_fixture): - with load_fixture(BALANCE_UPDATE_PATTERN) as balance: - update_type, update_object = user_transformer(balance) - - assert update_type == UpdateType.BALANCE - assert update_object['Asset'] == 'BTC' - assert update_object['Update'] == 100.0 - - -def stream_execution_report_transformation(user_transformer, load_fixture): - with load_fixture(EXECUTION_REPORT_PATTERN) as execution: - update_type, update_object = user_transformer(execution) - - pass # @todo - transformation not implemented - - -def test_stream_account_info_transformation(user_transformer, load_fixture): - with load_fixture(ACCOUNT_INFORMATION_PATTERN) as information: - update_type, update_object = user_transformer(information) - - assert update_type == UpdateType.ACCOUNT - - assert update_object['CanDeposit'] - assert update_object['CanTrade'] - assert update_object['CanWithdraw'] - - assert len(update_object['Positions']) == 5 - check_position(update_object['Positions'], 'LTC', 17366.18538083, 0.0) - check_position(update_object['Positions'], 'BTC', 10537.85314051, 2.19464093) - check_position(update_object['Positions'], 'ETH', 17902.35190619, 0.0) - check_position(update_object['Positions'], 'BNC', 1114503.29769312, 0.0) - check_position(update_object['Positions'], 'NEO', 0.0, 0.0) - - -def stream_account_position_transformation(user_transformer, load_fixture): - with load_fixture(ACCOUNT_POSITION_PATTERN) as position: - update_type, update_object = user_transformer(position) - - assert update_type == UpdateType.POSITION - assert "BTC" in update_object - assert update_object["BTC"].Free - assert update_object["BTC"].Locked diff --git a/test/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/fixtures/valid_strategy.py b/test/fixtures/valid_strategy.py index c222b17..887ebae 100644 --- a/test/fixtures/valid_strategy.py +++ b/test/fixtures/valid_strategy.py @@ -1,5 +1,10 @@ -from algorunner.abstract.strategy import Strategy - -class ValidStrategy(Strategy): +from algorunner.abstract import BaseStrategy +from algorunner.abstract.base_strategy import ( + AccountState, TransactionRequest, AuthorisationDecision +) +class ValidStrategy(BaseStrategy): def process(self, tick): return True + + def authorise(self, state: AccountState, trx: TransactionRequest) -> AuthorisationDecision: + pass diff --git a/test/helpers.py b/test/helpers.py index 9af5c26..75a4f55 100644 --- a/test/helpers.py +++ b/test/helpers.py @@ -1,5 +1,4 @@ from contextlib import contextmanager -from contextlib import contextmanager from json import load from pytest import fixture diff --git a/test/scenarios/account_updates.feature b/test/scenarios/account_updates.feature deleted file mode 100644 index e23beaa..0000000 --- a/test/scenarios/account_updates.feature +++ /dev/null @@ -1,50 +0,0 @@ -Feature: Account State - The Account object manages state associated with an exchange user. - The Account must be able to handle updates taken from user data, as well as - coordinate with the Calculator and API Adapter to make transactions. - - Background: - Given an account with no initial state - and that account is currently awaiting messages - - Scenario: Stay synchronised with account updates - Given an account update with full capabilities - When that account update is processed - Then the account should have full capabilities - Given an account update with minimal capabilities - When that account update is processed - Then the account should have minimal capabilities - - Scenario: Stay synchronised with balance updates - Given a BTC balance of 50 free and 25 locked - And a balance update of 20 BTC - When that balance update is processed - Then the account should have a balance of 70 BTC free - Given a balance update of -30 BTC - When that balance update is processed - Then the account should have a balance of 40 BTC free - - Scenario: Stay synchronised with position updates - Given an account position of BTC at 10 free and 25 locked - and a position update of ETH at 20 free and 50 locked - When that position update is processed - Then there should be a BTC balance of 10 free and 25 locked - and there should be a ETH balance of 20 free and 50 locked - and there should be a total of 2 balances - Given a position update of BTC at 5 free and 30 locked - When that position update is processed - Then there should be a BTC balance of 5 free and 30 locked - and there should be a ETH balance of 20 free and 50 locked - and there should be a total of 2 balances - - Scenario: Process orders requests approved by the calculator - Given a market order to buy BTC - and the calculator will accept the order with a size of 0.0032 - When the order event is processed - Then the API should recieve an order of 0.0032 BTC - - Scenario: Skip order requests rejected by the calculator - Given a market order to buy BTC - and the calculator will reject the order - When the order event is processed - Then the API should not recieve any orders diff --git a/test/scenarios/steps/account.py b/test/scenarios/steps/account.py deleted file mode 100644 index 882b0f3..0000000 --- a/test/scenarios/steps/account.py +++ /dev/null @@ -1,94 +0,0 @@ -from behave import * - -from algorunner.events import ( - AccountStatus, UpdateType -) -from algorunner.trader import Trader - -@given("an account with no initial state") -def fresh_account(context): - context.trader = Trader() - -@given("that account is currently awaiting messages") -def account_running(context): - pass - -@given("an account update with {capabilities} capabilities") -def account_update_full_capabilities(context, capabilities): - hasPermission = (capabilities == "full") - context.account_update = AccountStatus( - CanWithdraw=hasPermission, - CanTrade=hasPermission, - CanDeposit=hasPermission - ) - -@when("that account update is processed") -def account_update_processed(context): - context.trader(UpdateType.ACCOUNT, context.account_update) - -@then("the account should have {capabilities} capabilities") -def account_has_full_capabilities(context, capabilities): - hasPermission = (capabilities == "full") - assert context.trader.status["CanWithdraw"] == hasPermission - assert context.trader.status["CanTrade"] == hasPermission - assert context.trader.status["CanDeposit"] == hasPermission - -@given("a {symbol} balance of {free} free and {locked} locked") -def current_balance(context, symbol, free, locked): - pass - -@given("a balance update of {quantity:g} {symbol}") -def balance_update(context, quantity, symbol): - pass - -@when("that balance update is processed") -def balance_updated_processed(context): - pass - -@then("the account should have a balance of {balance:d} {symbol} free") -def balance_for_symbol(context, balance, symbol): - pass - -@given("an account position of {symbol} at {free:d} free and {locked:d} locked") -def account_with_balance(context, symbol, free, locked): - pass - -@given("a position update of {symbol} at {free:d} free and {locked:d} locked") -def position_update(context, symbol, free, locked): - pass - -@when("that position update is processed") -def position_update_processed(context): - pass - -@then("there should be a {symbol} balance of {free:d} free and {locked:d} locked") -def check_symbol_balance(context, symbol, free, locked): - pass - -@then("there should be a total of {count:d} balances") -def check_balance_count(context, count): - pass - -@given("a market order to buy {symbol}") -def market_order(context, symbol): - pass - -@given("the calculator will reject the order") -def calculator_rejection(context): - pass - -@given("the calculator will accept the order with a size of {size:g}") -def calculator_accepted(context, size): - pass - -@when("the order event is processed") -def order_event_process(context): - pass - -@then("the API should recieve an order of {quantity:g} {symbol}") -def check_for_order(context, quantity, symbol): - pass - -@then("the API should not recieve any orders") -def check_no_orders_made(context): - pass diff --git a/test/scenarios/steps/sync_agent.py b/test/scenarios/steps/sync_agent.py new file mode 100644 index 0000000..1c6e467 --- /dev/null +++ b/test/scenarios/steps/sync_agent.py @@ -0,0 +1,126 @@ +from queue import Queue +from unittest import mock +from time import sleep + +from loguru import logger +from behave import * + +from algorunner.abstract import ( + AuthorisationDecision, + BaseStrategy, + ShutdownRequest, + TransactionRequest, +) +from algorunner.adapters.base import Adapter, TransactionParams +from algorunner.mutations import ( + Position, BalanceUpdate, AccountUpdate, CapabilitiesUpdate +) + + +@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() + +@when("the sync agent is stopped") +def stop_sync_agent(context): + context.agent_params["queue"].put(ShutdownRequest(reason="bdd tests")) + sleep(.25) + +@then("it should no longer be running") +def sync_agent_not_running(context): + assert not 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=[] + )) + +@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 = [] + +@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 + +@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 + )) + +@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 + +@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)] + )) + +@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 + +@given("a request to buy {symbol}") +def market_order(context, symbol): + context.message_list.append(TransactionRequest( + symbol=symbol, order_type="buy" + )) + +@given("the order is declined") +def calculator_rejection(context): + context.agent_params["auth"].return_value = AuthorisationDecision( + accepted=False, params=None + ) + +@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 = AuthorisationDecision( + accepted=True, params=TransactionParams() + ) + +@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_account.py b/test/test_account.py deleted file mode 100644 index 91263b4..0000000 --- a/test/test_account.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_tests_are_running(): - assert True == True diff --git a/test/test_account.pyold b/test/test_account.pyold deleted file mode 100644 index 874a42c..0000000 --- a/test/test_account.pyold +++ /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/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 index 91263b4..767bc66 100644 --- a/test/test_runner.py +++ b/test/test_runner.py @@ -1,3 +1,56 @@ +from signal import SIGTERM +from unittest.mock import MagicMock, patch -def test_tests_are_running(): - assert True == True +import pytest + +from algorunner.abstract import BaseStrategy +from algorunner.adapters import Credentials +from algorunner.exceptions import UnknownExchange +from algorunner.runner import Runner + + +@pytest.fixture +def mock_adapter() -> MagicMock: + with patch('algorunner.runner.get_adapter') 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 + + +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"), + 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 UnknownExchange: + have_exception = True + + assert have_exception diff --git a/test/test_runner.pyold b/test/test_runner.pyold deleted file mode 100644 index 9cc563d..0000000 --- a/test/test_runner.pyold +++ /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/test_strategy.py b/test/test_strategy.py index b8a5fb9..f44be05 100644 --- a/test/test_strategy.py +++ b/test/test_strategy.py @@ -1,7 +1,10 @@ import pytest -from algorunner.strategy import * - +from algorunner.strategy import ( + load_strategy, + StrategyNotFound, + InvalidStrategyProvided +) def test_default_strategies_module(): strategy = load_strategy('Example') From bb765fe788b63bd5e9e87972c168d56a8373995f Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Fri, 13 Aug 2021 17:20:37 +0100 Subject: [PATCH 05/15] docs wip --- DEVELOPMENT.md | 1 + Dockerfile | 2 +- LICENSE.txt | 7 +++ Makefile | 23 +++++----- README.md | 116 +++++++++++++++++++++++++++++++++++------------- bot.example.ini | 10 ++--- 6 files changed, 113 insertions(+), 46 deletions(-) create mode 100644 DEVELOPMENT.md create mode 100644 LICENSE.txt 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 index 37865cb..22d83ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,6 @@ RUN make env-check RUN poetry config virtualenvs.create false && make deps COPY . /code -ENTRYPOINT [ "make" "run" ] +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 4914b0f..ebe8e7d 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,30 @@ .PHONY: env-check build lint deps test run todo -help: ## Show this help. +help: ## Show this help. @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' -env-check: ## Check that the current environment is capable of running AlgoRunner. +env-check: ## Check that the current environment is capable of running AlgoRunner. @sh setup.sh -build: ## Build docker image, tagged "algorunner:" - echo "build docker container" +build: ## Build docker image, tagged "algorunner:" and "algorunner:latest" + docker build -t algorunner:latest -t algorunner:`git rev-parse --short HEAD` . -lint: ## Run code quality checks +docker: build ## Run the docker image after it's built. + docker run algorunner:latest + +lint: ## Run code quality checks poetry run flake8 -deps: ## Install all required dependencies (including for development) +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 +test: ## Run all tests - including both unit tests and BDD scenarios poetry run pytest poetry run behave -run: ## Run AlgoRunner +local: ## Run AlgoRunner locally via Poetry poetry run python run.py -todo: ## Scan the codebase for items tagged with "@todo" +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`." + @echo "\nTotal items marked '@todo': `grep --exclude=\*.pyc -r '@todo' . | wc -l | xargs`." diff --git a/README.md b/README.md index 8a2e7dc..d16ce77 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,109 @@ -# ... @todo? +# AlgoRunner -## Running +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. -@todo +All development is done against the `develop` branch, although at this time that's likely the branch you actually want to browse. -### Configuration +| 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) | +| 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) | -@todo +## Defining a strategy -## Development +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 +import pandas as pd + +from algorunner.abstract import BaseStrategy +from algorunner.abstract.base_strategy import ( + AccountState, TransactionRequest, AuthorisationDecision +) -### Make Targets -The `Makefile` contains a selection of useful targets for simplfying the development workflow. +class Example(BaseStrategy): + def __init__(self): + self.series = pd.DataFrame + super().__init__() + 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: + 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 ``` -➜ Runner git:(v2/trader-actor) ✗ make help -help: Show this help. -env-check: Check that the current environment is capable of running AlgoRunner. -build: Build docker image, tagged "algorunner:" -lint: Run code quality checks -deps: Install all required dependencies (including for development) -test: Run all tests - including both unit tests and BDD feature tests -run: Run AlgoRunner -todo: Scan the codebase for items tagged with "@todo" + +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. + +## Required Configuration + +Configuration can be done via: a `.ini` file, environment variables, or a combination of both. +``` +[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 ``` -### Docker +By default AlgoRunner will try and read a file named `bot.ini`, but this can be overridden by the `--config` flag: -There's also a `Dockerfile` contained in this repository; this builds a `python:3.9-slim` based Docker Image, with all development dependencies. This can be built using the aforementioned `Makefile`. +``` +$ python run.py --config [config .ini file] +``` -### Finding Tasks +Alternatively, configuration can also be done via: a `.ini` file, environment variables, or a combination of both. -The codebase is littered with `@todo` tags where low-hanging fruit is marked when discovered/encountered. +| `.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. ``` -➜ adapters git:(huge-refactor) ✗ grep -r '@todo' . - ./binance/test_user_transformations.py: pass # @todo - transformation not implemented -➜ adapters git:(huge-refactor) ✗ ../.. -➜ Runner git:(huge-refactor) ✗ grep -r '@todo' . | wc -l - 17 +### 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/bot.example.ini b/bot.example.ini index 26480f4..f9441c9 100644 --- a/bot.example.ini +++ b/bot.example.ini @@ -1,8 +1,8 @@ [credentials] -exchange = binance -api_key = binanceAPIKey -api_secret = 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] -name = Example -symbol = BTCUSDT \ No newline at end of file +name = Example # Strategy to execute +symbol = BTCUSDT # Symbol to execute the strategy against From 4aaeb59bb1d32b7a9d8e099604629c757454ddf1 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Fri, 13 Aug 2021 20:54:27 +0100 Subject: [PATCH 06/15] automatic adapter registration --- algorunner/adapters/__init__.py | 10 +++---- algorunner/adapters/_binance.py | 11 +++++++- algorunner/adapters/base.py | 33 +++++++++++++++++++++++ algorunner/exceptions.py | 10 ------- algorunner/runner.py | 13 ++------- test/test_adapter.py | 47 +++++++++++++++++++++++++++++++++ test/test_runner.py | 7 ++--- 7 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 test/test_adapter.py diff --git a/algorunner/adapters/__init__.py b/algorunner/adapters/__init__.py index b27fa19..a57eb95 100644 --- a/algorunner/adapters/__init__.py +++ b/algorunner/adapters/__init__.py @@ -1,10 +1,8 @@ -from algorunner.adapters._binance import BinanceAdapter +from algorunner.adapters._binance import BinanceAdapter # noqa: F401 from algorunner.adapters.base import ( # noqa: F401 Adapter, + AdapterError, Credentials, - InvalidPayloadRecieved + InvalidPayloadRecieved, + factory, ) - -ADAPTERS = { - "binance": BinanceAdapter -} diff --git a/algorunner/adapters/_binance.py b/algorunner/adapters/_binance.py index c3bbd6f..cf7dd9b 100644 --- a/algorunner/adapters/_binance.py +++ b/algorunner/adapters/_binance.py @@ -6,7 +6,7 @@ import pandas as pd from algorunner.adapters.base import ( - Adapter, Credentials, InvalidPayloadRecieved + Adapter, Credentials, InvalidPayloadRecieved, TransactionParams, register_adapter ) from algorunner.mutations import ( @@ -14,9 +14,12 @@ ) +@register_adapter class BinanceAdapter(Adapter): """ """ + identifier = "binance" + class MarketStreamRawTransformer: pass @@ -139,6 +142,12 @@ def run(self, symbol: str, process: Callable): symbol, lambda p: process(self.market_transformer(p)) ) + def execute(self, trx: TransactionParams): + pass + + def disconnect(self): + pass + """ # @todo - these will come via execute(TransactionParams) diff --git a/algorunner/adapters/base.py b/algorunner/adapters/base.py index 59a2e9d..2fdfeac 100644 --- a/algorunner/adapters/base.py +++ b/algorunner/adapters/base.py @@ -2,6 +2,12 @@ from typing import Callable, TypedDict from queue import Queue +from loguru import logger + + +class AdapterError(Exception): + pass + class InvalidPayloadRecieved(Exception): """InvalidPayloadRecieved is thrown when an invalid message is recieved @@ -51,3 +57,30 @@ def execute(self, trx: TransactionParams) -> bool: @abstractmethod def disconnect(self): pass + + +_available_adapters = {} + + +def register_adapter(cls): + logger.debug("registering new adapter...") + try: + identifier = getattr(cls, "identifier") + except AttributeError: + raise AdapterError(f"cannot find identifier for adapter class: {cls.__name__}") + + if hasattr(_available_adapters, identifier): + raise 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 AdapterError(f"no adapter registered for identifier {requested_adapter}") + + return cls(*args, **kwargs) diff --git a/algorunner/exceptions.py b/algorunner/exceptions.py index 27740b1..437a5e4 100644 --- a/algorunner/exceptions.py +++ b/algorunner/exceptions.py @@ -2,7 +2,6 @@ MSG_INVALID_CONFIG = "unable to parse all required values from configuration" MSG_INVALID_CONFIG_W_FIELDS = "unable to parse [{fields}] from configuration" -MSG_UNKNOWN_EXCHANGE = "unable to find adapter for exchange '{name}'" class InvalidConfiguration(Exception): @@ -17,15 +16,6 @@ def __init__(self, invalid_fields: Optional[List[str]] = None): ) -class UnknownExchange(Exception): - """ - Raised when the exchange specified in the configuration is unknown. - """ - def __init__(self, exchange_name: str, exception: Optional[Exception]): - self.message = MSG_UNKNOWN_EXCHANGE.format(name=exchange_name) - self.exc = exception - - class NoBalanceAvailable(Exception): """ Triggered when the an attempt to access a balance that does not exist diff --git a/algorunner/runner.py b/algorunner/runner.py index 15c2063..14c147d 100644 --- a/algorunner/runner.py +++ b/algorunner/runner.py @@ -4,16 +4,7 @@ from loguru import logger from algorunner import abstract -from algorunner.adapters import ADAPTERS, Credentials, Adapter -from algorunner.exceptions import UnknownExchange - - -def get_adapter(exchange: str, *args, **kwargs) -> Adapter: - adapter_cls = ADAPTERS.get(exchange) - if not adapter_cls: - raise UnknownExchange(exchange) - - return adapter_cls(*args, **kwargs) +from algorunner.adapters import Credentials, factory class Runner(object): @@ -27,7 +18,7 @@ def __init__(self, creds: Credentials, strategy: abstract.BaseStrategy): self.sync_queue = Queue() - self.adapter = get_adapter(creds["exchange"], self.sync_queue) + self.adapter = factory(creds["exchange"], self.sync_queue) self.strategy = strategy self.strategy.start_sync(self.sync_queue, self.adapter) diff --git a/test/test_adapter.py b/test/test_adapter.py new file mode 100644 index 0000000..1936e03 --- /dev/null +++ b/test/test_adapter.py @@ -0,0 +1,47 @@ +from algorunner.adapters.base import ( + AdapterError, + factory, + register_adapter +) +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_runner.py b/test/test_runner.py index 767bc66..e17d6d1 100644 --- a/test/test_runner.py +++ b/test/test_runner.py @@ -1,3 +1,4 @@ + from signal import SIGTERM from unittest.mock import MagicMock, patch @@ -5,13 +6,13 @@ from algorunner.abstract import BaseStrategy from algorunner.adapters import Credentials -from algorunner.exceptions import UnknownExchange +from algorunner.adapters.base import AdapterError from algorunner.runner import Runner @pytest.fixture def mock_adapter() -> MagicMock: - with patch('algorunner.runner.get_adapter') as mock: + with patch('algorunner.runner.factory') as mock: mock.return_value = MagicMock() yield mock.return_value @@ -50,7 +51,7 @@ def invalid_exchange_should_trigger_exception(mock_strategy): have_exception = False try: Runner(Credentials(exchange="lolnoexchange"), mock_strategy) - except UnknownExchange: + except AdapterError: have_exception = True assert have_exception From a5b80292d660274e19836a11e3e8bdad96bd948b Mon Sep 17 00:00:00 2001 From: Fergus Date: Sat, 14 Aug 2021 18:31:03 +0100 Subject: [PATCH 07/15] v2: include hooks functionality for logging/monitoring (#9) --- algorunner/hooks.py | 75 ++++++++++++++++ algorunner/monitoring.py | 22 +++++ test/scenarios/environment.py | 10 +++ test/scenarios/hooks.feature | 53 +++++++++++ test/scenarios/steps/hooks.py | 139 +++++++++++++++++++++++++++++ test/scenarios/steps/sync_agent.py | 1 - test/test_monitoring.py | 27 ++++++ 7 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 algorunner/hooks.py create mode 100644 algorunner/monitoring.py create mode 100644 test/scenarios/environment.py create mode 100644 test/scenarios/hooks.feature create mode 100644 test/scenarios/steps/hooks.py create mode 100644 test/test_monitoring.py diff --git a/algorunner/hooks.py b/algorunner/hooks.py new file mode 100644 index 0000000..b165369 --- /dev/null +++ b/algorunner/hooks.py @@ -0,0 +1,75 @@ +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.""" + PROCESS_DURATION = 1 + API_EXECUTE_DURATION = 2 + + +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], +} + +# @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}") + + 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.API_EXECUTE_DURATION) +def handle_api_duration(duration: float): + logger.debug(f"api execution duration: {duration}ms") + + +@hook_handler(hook=Hook.PROCESS_DURATION) +def handle_process_duration(duration: float): + logger.debug(f"tick process duration: {duration}ms") diff --git a/algorunner/monitoring.py b/algorunner/monitoring.py new file mode 100644 index 0000000..7422141 --- /dev/null +++ b/algorunner/monitoring.py @@ -0,0 +1,22 @@ +from time import time + +from loguru import logger + + +class Timer: + """Simple timer based context manager, used for performance monitoring + in conjunction with hooks.""" + def __init__(self): + self.duration = None + + 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})") + + def ms(self) -> float: + return self.duration * 1000 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 index 1c6e467..82265b9 100644 --- a/test/scenarios/steps/sync_agent.py +++ b/test/scenarios/steps/sync_agent.py @@ -2,7 +2,6 @@ from unittest import mock from time import sleep -from loguru import logger from behave import * from algorunner.abstract import ( diff --git a/test/test_monitoring.py b/test/test_monitoring.py new file mode 100644 index 0000000..3cf74ee --- /dev/null +++ b/test/test_monitoring.py @@ -0,0 +1,27 @@ +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 From 438cce128baf70564e3fb60a1112add438b26a60 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Sat, 14 Aug 2021 18:32:18 +0100 Subject: [PATCH 08/15] wip: documentation --- docs/swimlanes/README.md | 7 +++++ docs/swimlanes/order_flow.txt | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 docs/swimlanes/README.md create mode 100644 docs/swimlanes/order_flow.txt 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. + + + From e6989adebffbdc83a79c9673236463f948b35082 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Fri, 13 Aug 2021 20:54:27 +0100 Subject: [PATCH 09/15] v2 - work towards exchange interactions and order processing --- algorunner/abstract/base_strategy.py | 66 ++++++++++---------- algorunner/adapters/__init__.py | 11 ++-- algorunner/adapters/_binance.py | 70 +++++++++++---------- algorunner/adapters/base.py | 91 +++++++++++++++++++++++++--- algorunner/exceptions.py | 10 --- algorunner/runner.py | 15 +---- strategies/example.py | 4 +- test/fixtures/valid_strategy.py | 4 +- test/scenarios/steps/sync_agent.py | 21 +++---- test/test_adapter.py | 47 ++++++++++++++ test/test_runner.py | 9 +-- 11 files changed, 228 insertions(+), 120 deletions(-) create mode 100644 test/test_adapter.py diff --git a/algorunner/abstract/base_strategy.py b/algorunner/abstract/base_strategy.py index 2235467..15f656d 100644 --- a/algorunner/abstract/base_strategy.py +++ b/algorunner/abstract/base_strategy.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from dataclasses import dataclass from queue import Queue from threading import Thread from typing import Callable, Optional @@ -8,24 +7,9 @@ import pandas as pd from algorunner.exceptions import StrategyExceptionThresholdBreached +from algorunner.monitoring import Timer from algorunner.mutations import AccountState, is_update -from algorunner.adapters.base import Adapter, TransactionParams - - -@dataclass -class TransactionRequest: - """Dispatched by `process` this triggers risk calculation via `authorise` - and potential dispatch of a transaction to the exchange.""" - symbol: str - order_type: str - - -@dataclass -class AuthorisationDecision: - """Returned by `authorise` and determines whether a transaction can be - made, and the appropriate parameters for that transaction.""" - accepted: bool - params: Optional[TransactionParams] +from algorunner.adapters.base import Adapter, InvalidOrder, TransactionRequest, Tick class ShutdownRequest: @@ -70,7 +54,7 @@ def is_running(self) -> bool: def _listen(self): logger.info("listening for events and inbound messages") - exception_count = 0 # count exceptions over past 5 mins. + 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) @@ -97,16 +81,29 @@ def _listen(self): logger.critical("exception rate has breached threshold, failing..") raise StrategyExceptionThresholdBreached("too many exceptions encountered!") - logger.warn("trader thread has completed") + logger.warn("syncagent has completed") def _transaction_handler(self, trx: TransactionRequest): - decision = self.authorisation_guard(self.state, trx) - if not decision.accepted: - logger.info("transaction rejected: failed defined auth rules") + trx = self.authorisation_guard(self.state, trx) + if not trx.approved: + logger.info(f"transaction rejected: {trx.reason}") return - logger.info("transaction accepted: passing to API adapter for dispatch") - self.api.execute(decision.params) + t = Timer() + with t: + try: + logger.info("transaction accepted: passing to API adapter for dispatch") + self.api.execute(trx) + except InvalidOrder: + pass + # @todo hook(API_PROCESS) + + def __call__(self, tick: Tick): + t = Timer() + with t: + self.process(tick) + + # @todo call hook def start_sync(self, queue: Queue, adapter: Adapter): self.sync_agent = self.SyncAgent(queue, adapter, self.log) @@ -122,20 +119,23 @@ def close_position(self, symbol: str): def shutdown(self): self.sync_agent.stop("shutdown requested") - + + def account_state(self) -> AccountState: + return self.sync_agent.account_state + @abstractmethod - def process(self, tick: pd.DataFrame): + def authorise(self, + state: AccountState, + trx: TransactionRequest) -> TransactionRequest: """ - @todo - accept Union[pd.DataFrame, RawMarketPayload] - where RawMarketPayload is a TypedDict w/ no pandas processing. + @todo - define params. """ pass @abstractmethod - def authorise(self, - state: AccountState, - trx: TransactionRequest) -> AuthorisationDecision: + def process(self, tick: Tick): """ - @todo - define params. + @todo - accept Union[pd.DataFrame, RawMarketPayload] + where RawMarketPayload is a TypedDict w/ no pandas processing. """ pass diff --git a/algorunner/adapters/__init__.py b/algorunner/adapters/__init__.py index b27fa19..bb8b393 100644 --- a/algorunner/adapters/__init__.py +++ b/algorunner/adapters/__init__.py @@ -1,10 +1,9 @@ -from algorunner.adapters._binance import BinanceAdapter +from algorunner.adapters._binance import BinanceAdapter # noqa: F401 from algorunner.adapters.base import ( # noqa: F401 Adapter, + AdapterError, Credentials, - InvalidPayloadRecieved + InvalidOrder, + InvalidPayloadError, + factory, ) - -ADAPTERS = { - "binance": BinanceAdapter -} diff --git a/algorunner/adapters/_binance.py b/algorunner/adapters/_binance.py index c3bbd6f..255db65 100644 --- a/algorunner/adapters/_binance.py +++ b/algorunner/adapters/_binance.py @@ -6,7 +6,7 @@ import pandas as pd from algorunner.adapters.base import ( - Adapter, Credentials, InvalidPayloadRecieved + Adapter, Credentials, InvalidOrder, InvalidPayloadError, OrderType, TransactionRequest, register_adapter ) from algorunner.mutations import ( @@ -14,9 +14,12 @@ ) +@register_adapter class BinanceAdapter(Adapter): """ """ + identifier = "binance" + class MarketStreamRawTransformer: pass @@ -70,7 +73,7 @@ def __call__(self, payload) -> BaseUpdate: return message_map[payload["e"]](payload) except KeyError: msg = "unknown payload type {p}".format(p=payload.get("e")) - raise InvalidPayloadRecieved(msg) + raise InvalidPayloadError(msg) except Exception as e: raise Exception("unknown error occured in user stream", e) @@ -113,6 +116,10 @@ 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) @@ -130,41 +137,40 @@ def monitor_user(self): )) # subscribe to all subsequent user events - self.socket_manager.start_user_socket( + 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.socket_manager.start_symbol_ticker_socket( + 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) -""" - # @todo - these will come via execute(TransactionParams) - 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) - - do we want to return an identifier associated with the transaction - to allow monitoring via the event stream? I think so? - - # @todo - these will be events. - def sell(self, asset, amount, limit=False, price=0): - if limit: - self.binance.order_limit_sell( - symbol=asset, - quantity=amount, - price=price) - else: - self.binance.order_market_sell( - symbol=asset, - quantity=amount) -""" + 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/base.py b/algorunner/adapters/base.py index 59a2e9d..ec41123 100644 --- a/algorunner/adapters/base.py +++ b/algorunner/adapters/base.py @@ -1,23 +1,73 @@ from abc import ABC, abstractmethod -from typing import Callable, TypedDict +from dataclasses import dataclass +from enum import Enum +from typing import Callable, Optional, Union from queue import Queue +from loguru import logger +import pandas as pd -class InvalidPayloadRecieved(Exception): - """InvalidPayloadRecieved is thrown when an invalid message is recieved + + +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 -class Credentials(TypedDict): +@dataclass +class Credentials(): """Required credentials to authenticate with a given exchange.""" exchange: str key: str secret: str -class TransactionParams(TypedDict): - """Parameters detailing an execution to execute on an exchange.""" +@dataclass +class TransactionRequest: + """Dispatched by `process` this triggers risk calculation via `authorise` + and potential dispatch of a transaction to the exchange.""" + reason: str + symbol: str + order_type: OrderType + quantity: float + price: Optional[float] + approved: bool = False + + 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 @@ -45,9 +95,36 @@ def run(self, process: Callable, terminated: bool): pass @abstractmethod - def execute(self, trx: TransactionParams) -> bool: + def execute(self, trx: 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 AdapterError(f"cannot find identifier for adapter class: {cls.__name__}") + + if hasattr(_available_adapters, identifier): + raise 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 AdapterError(f"no adapter registered for identifier {requested_adapter}") + + return cls(*args, **kwargs) diff --git a/algorunner/exceptions.py b/algorunner/exceptions.py index 27740b1..437a5e4 100644 --- a/algorunner/exceptions.py +++ b/algorunner/exceptions.py @@ -2,7 +2,6 @@ MSG_INVALID_CONFIG = "unable to parse all required values from configuration" MSG_INVALID_CONFIG_W_FIELDS = "unable to parse [{fields}] from configuration" -MSG_UNKNOWN_EXCHANGE = "unable to find adapter for exchange '{name}'" class InvalidConfiguration(Exception): @@ -17,15 +16,6 @@ def __init__(self, invalid_fields: Optional[List[str]] = None): ) -class UnknownExchange(Exception): - """ - Raised when the exchange specified in the configuration is unknown. - """ - def __init__(self, exchange_name: str, exception: Optional[Exception]): - self.message = MSG_UNKNOWN_EXCHANGE.format(name=exchange_name) - self.exc = exception - - class NoBalanceAvailable(Exception): """ Triggered when the an attempt to access a balance that does not exist diff --git a/algorunner/runner.py b/algorunner/runner.py index 15c2063..99f27ed 100644 --- a/algorunner/runner.py +++ b/algorunner/runner.py @@ -4,16 +4,7 @@ from loguru import logger from algorunner import abstract -from algorunner.adapters import ADAPTERS, Credentials, Adapter -from algorunner.exceptions import UnknownExchange - - -def get_adapter(exchange: str, *args, **kwargs) -> Adapter: - adapter_cls = ADAPTERS.get(exchange) - if not adapter_cls: - raise UnknownExchange(exchange) - - return adapter_cls(*args, **kwargs) +from algorunner.adapters import Credentials, factory class Runner(object): @@ -27,7 +18,7 @@ def __init__(self, creds: Credentials, strategy: abstract.BaseStrategy): self.sync_queue = Queue() - self.adapter = get_adapter(creds["exchange"], self.sync_queue) + self.adapter = factory(creds.exchange, self.sync_queue) self.strategy = strategy self.strategy.start_sync(self.sync_queue, self.adapter) @@ -45,7 +36,7 @@ def _handler(signum, frame): def run(self): """ """ self.adapter.monitor_user(self.trader_queue) - self.adapter.run(self.strategy, self.strategy.process) + self.adapter.run(self.strategy, self.strategy) logger.info("monitoring user stream and executing strategy") def stop(self): diff --git a/strategies/example.py b/strategies/example.py index 191a624..ffa4078 100644 --- a/strategies/example.py +++ b/strategies/example.py @@ -2,7 +2,7 @@ from algorunner.abstract import BaseStrategy from algorunner.abstract.base_strategy import ( - AccountState, TransactionRequest, AuthorisationDecision + AccountState, TransactionRequest ) @@ -26,5 +26,5 @@ def process(self, tick): 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: + def authorise(self, state: AccountState, trx: TransactionRequest) -> TransactionRequest: pass diff --git a/test/fixtures/valid_strategy.py b/test/fixtures/valid_strategy.py index 887ebae..741e54e 100644 --- a/test/fixtures/valid_strategy.py +++ b/test/fixtures/valid_strategy.py @@ -1,10 +1,10 @@ from algorunner.abstract import BaseStrategy from algorunner.abstract.base_strategy import ( - AccountState, TransactionRequest, AuthorisationDecision + AccountState, TransactionRequest ) class ValidStrategy(BaseStrategy): def process(self, tick): return True - def authorise(self, state: AccountState, trx: TransactionRequest) -> AuthorisationDecision: + def authorise(self, state: AccountState, trx: TransactionRequest) -> TransactionRequest: pass diff --git a/test/scenarios/steps/sync_agent.py b/test/scenarios/steps/sync_agent.py index 1c6e467..3e9253b 100644 --- a/test/scenarios/steps/sync_agent.py +++ b/test/scenarios/steps/sync_agent.py @@ -5,13 +5,10 @@ from loguru import logger from behave import * -from algorunner.abstract import ( - AuthorisationDecision, - BaseStrategy, - ShutdownRequest, - TransactionRequest, +from algorunner.abstract.base_strategy import ( + ShutdownRequest, BaseStrategy ) -from algorunner.adapters.base import Adapter, TransactionParams +from algorunner.adapters.base import Adapter, OrderType, TransactionRequest from algorunner.mutations import ( Position, BalanceUpdate, AccountUpdate, CapabilitiesUpdate ) @@ -101,21 +98,21 @@ def check_balance_count(context, count): @given("a request to buy {symbol}") def market_order(context, symbol): context.message_list.append(TransactionRequest( - symbol=symbol, order_type="buy" + reason="", symbol="", quantity="", price="", order_type=OrderType.MARKET_SELL )) @given("the order is declined") def calculator_rejection(context): - context.agent_params["auth"].return_value = AuthorisationDecision( - accepted=False, params=None + 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 = AuthorisationDecision( - accepted=True, params=TransactionParams() + context.agent_params["auth"].return_value = TransactionRequest( + approved=True, reason="", symbol="", quantity="", price="", order_type=OrderType.MARKET_SELL ) - @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() diff --git a/test/test_adapter.py b/test/test_adapter.py new file mode 100644 index 0000000..1936e03 --- /dev/null +++ b/test/test_adapter.py @@ -0,0 +1,47 @@ +from algorunner.adapters.base import ( + AdapterError, + factory, + register_adapter +) +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_runner.py b/test/test_runner.py index 767bc66..7865343 100644 --- a/test/test_runner.py +++ b/test/test_runner.py @@ -1,3 +1,4 @@ + from signal import SIGTERM from unittest.mock import MagicMock, patch @@ -5,13 +6,13 @@ from algorunner.abstract import BaseStrategy from algorunner.adapters import Credentials -from algorunner.exceptions import UnknownExchange +from algorunner.adapters.base import AdapterError from algorunner.runner import Runner @pytest.fixture def mock_adapter() -> MagicMock: - with patch('algorunner.runner.get_adapter') as mock: + with patch('algorunner.runner.factory') as mock: mock.return_value = MagicMock() yield mock.return_value @@ -28,7 +29,7 @@ def mock_strategy() -> MagicMock: 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"), + creds=Credentials(exchange="binance", key="", secret=""), strategy=mock_strategy ) @@ -50,7 +51,7 @@ def invalid_exchange_should_trigger_exception(mock_strategy): have_exception = False try: Runner(Credentials(exchange="lolnoexchange"), mock_strategy) - except UnknownExchange: + except AdapterError: have_exception = True assert have_exception From 8fa8ace29e968ed36d2ae043f83c1ba697e87228 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Sat, 14 Aug 2021 18:51:48 +0100 Subject: [PATCH 10/15] some readme updates --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d16ce77..08ada69 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ All development is done against the `develop` branch, although at this time that | 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) | | 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) | +### Living Design Doc + +... + ## 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`. From c2f2bfe472295bb9c83e11aca13433f2146beb0c Mon Sep 17 00:00:00 2001 From: Fergus Date: Sat, 14 Aug 2021 23:24:45 +0100 Subject: [PATCH 11/15] Support Order Flow and define API abstraction (#7) * v2 - work towards exchange interactions and order processing * v2: various amendments for order flow and exchange interactions Incidentally allows the timer to trigger hooks. --- algorunner/__init__.py | 4 + algorunner/abstract/__init__.py | 1 - algorunner/adapters/__init__.py | 5 +- algorunner/adapters/_binance.py | 73 +++++------ algorunner/adapters/_sample.py | 31 +++++ algorunner/adapters/base.py | 34 ++--- algorunner/adapters/messages.py | 70 +++++++++++ algorunner/exceptions.py | 25 ---- algorunner/hooks.py | 28 ++++- algorunner/monitoring.py | 10 +- algorunner/runner.py | 15 +-- algorunner/strategy/__init__.py | 8 ++ .../base_strategy.py => strategy/base.py} | 113 ++++++++++------- algorunner/strategy/exceptions.py | 26 ++++ .../{strategy.py => strategy/loader.py} | 4 +- strategies/example.py | 9 +- test/fixtures/valid_strategy.py | 11 +- test/scenarios/steps/sync_agent.py | 119 ++++++++++-------- test/test_adapter.py | 2 +- test/test_monitoring.py | 13 ++ test/test_runner.py | 6 +- 21 files changed, 394 insertions(+), 213 deletions(-) delete mode 100644 algorunner/abstract/__init__.py create mode 100644 algorunner/adapters/_sample.py create mode 100644 algorunner/adapters/messages.py create mode 100644 algorunner/strategy/__init__.py rename algorunner/{abstract/base_strategy.py => strategy/base.py} (55%) create mode 100644 algorunner/strategy/exceptions.py rename algorunner/{strategy.py => strategy/loader.py} (92%) diff --git a/algorunner/__init__.py b/algorunner/__init__.py index e69de29..01e5d1b 100644 --- a/algorunner/__init__.py +++ b/algorunner/__init__.py @@ -0,0 +1,4 @@ +# +# @todo - really need to work out what needs to be exposed +# for user defined strategies - so we can use `pdoc`. +# diff --git a/algorunner/abstract/__init__.py b/algorunner/abstract/__init__.py deleted file mode 100644 index ab0bca2..0000000 --- a/algorunner/abstract/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from algorunner.abstract.base_strategy import * # noqa: F401 F403 diff --git a/algorunner/adapters/__init__.py b/algorunner/adapters/__init__.py index a57eb95..b950717 100644 --- a/algorunner/adapters/__init__.py +++ b/algorunner/adapters/__init__.py @@ -1,8 +1,7 @@ from algorunner.adapters._binance import BinanceAdapter # noqa: F401 +from algorunner.adapters._sample import SampleAdapter # noqa: F401 +from algorunner.adapters.messages import * # noqa: F401, F403 from algorunner.adapters.base import ( # noqa: F401 Adapter, - AdapterError, - Credentials, - InvalidPayloadRecieved, factory, ) diff --git a/algorunner/adapters/_binance.py b/algorunner/adapters/_binance.py index cf7dd9b..3b11874 100644 --- a/algorunner/adapters/_binance.py +++ b/algorunner/adapters/_binance.py @@ -6,7 +6,11 @@ import pandas as pd from algorunner.adapters.base import ( - Adapter, Credentials, InvalidPayloadRecieved, TransactionParams, register_adapter + register_adapter, Adapter +) + +from algorunner.adapters.messages import ( + Credentials, InvalidOrder, InvalidPayloadError, OrderType, TransactionRequest ) from algorunner.mutations import ( @@ -73,7 +77,7 @@ def __call__(self, payload) -> BaseUpdate: return message_map[payload["e"]](payload) except KeyError: msg = "unknown payload type {p}".format(p=payload.get("e")) - raise InvalidPayloadRecieved(msg) + raise InvalidPayloadError(msg) except Exception as e: raise Exception("unknown error occured in user stream", e) @@ -116,6 +120,10 @@ 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) @@ -133,47 +141,40 @@ def monitor_user(self): )) # subscribe to all subsequent user events - self.socket_manager.start_user_socket( + 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.socket_manager.start_symbol_ticker_socket( + self.market_conn_key = self.socket_manager.start_symbol_ticker_socket( symbol, lambda p: process(self.market_transformer(p)) ) - def execute(self, trx: TransactionParams): - pass + def execute(self, trx: TransactionRequest): + trx.validate() - def disconnect(self): - pass + 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") -""" - # @todo - these will come via execute(TransactionParams) - 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) - - do we want to return an identifier associated with the transaction - to allow monitoring via the event stream? I think so? - - # @todo - these will be events. - def sell(self, asset, amount, limit=False, price=0): - if limit: - self.binance.order_limit_sell( - symbol=asset, - quantity=amount, - price=price) - else: - self.binance.order_market_sell( - symbol=asset, - quantity=amount) -""" + 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..c9fd128 --- /dev/null +++ b/algorunner/adapters/_sample.py @@ -0,0 +1,31 @@ + +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 index 2fdfeac..f7ee1bc 100644 --- a/algorunner/adapters/base.py +++ b/algorunner/adapters/base.py @@ -1,30 +1,10 @@ from abc import ABC, abstractmethod -from typing import Callable, TypedDict +from typing import Callable from queue import Queue from loguru import logger - -class AdapterError(Exception): - pass - - -class InvalidPayloadRecieved(Exception): - """InvalidPayloadRecieved is thrown when an invalid message is recieved - from the exchange via a websocket stream.""" - pass - - -class Credentials(TypedDict): - """Required credentials to authenticate with a given exchange.""" - exchange: str - key: str - secret: str - - -class TransactionParams(TypedDict): - """Parameters detailing an execution to execute on an exchange.""" - pass +from algorunner.adapters import messages class Adapter(ABC): @@ -34,7 +14,7 @@ def __init__(self, sync_queue: Queue): self.sync_queue = sync_queue @abstractmethod - def connect(self, creds: Credentials): + def connect(self, creds: messages.Credentials): """connect authenticates with the exchange, and also populates the associated `Trader` object with the latest state.""" pass @@ -51,7 +31,7 @@ def run(self, process: Callable, terminated: bool): pass @abstractmethod - def execute(self, trx: TransactionParams) -> bool: + def execute(self, trx: messages.TransactionRequest) -> bool: pass @abstractmethod @@ -67,10 +47,10 @@ def register_adapter(cls): try: identifier = getattr(cls, "identifier") except AttributeError: - raise AdapterError(f"cannot find identifier for adapter class: {cls.__name__}") + raise messages.AdapterError(f"cannot find identifier for adapter class: {cls.__name__}") if hasattr(_available_adapters, identifier): - raise AdapterError(f"attempt at registering duplicate adapter for identifier: {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}'") @@ -81,6 +61,6 @@ 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 AdapterError(f"no adapter registered for identifier {requested_adapter}") + 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..d39b6cc --- /dev/null +++ b/algorunner/adapters/messages.py @@ -0,0 +1,70 @@ +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 + + 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 index 437a5e4..9e3ceab 100644 --- a/algorunner/exceptions.py +++ b/algorunner/exceptions.py @@ -32,28 +32,3 @@ class InvalidUpdate(Exception): """ def __init__(self, prop: str, update_type: str): self.message = f"missing '{prop}' in update on update type '{update_type}'" - - -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/hooks.py b/algorunner/hooks.py index b165369..c79b858 100644 --- a/algorunner/hooks.py +++ b/algorunner/hooks.py @@ -1,3 +1,4 @@ +from algorunner.adapters.messages import TransactionRequest from enum import Enum from typing import Callable, Optional @@ -7,8 +8,12 @@ class Hook(Enum): """Hook represents valid hooks for user-defined functions to listen for.""" - PROCESS_DURATION = 1 - API_EXECUTE_DURATION = 2 + RUNNER_INITIALISED = 1 + RUNNER_STARTING = 2 + RUNNER_STOPPING = 3 + ORDER_REQUEST = 4 + API_EXECUTE_DURATION = 5 + PROCESS_DURATION = 6 class InvalidHookHandler(Exception): @@ -19,6 +24,10 @@ class InvalidHookHandler(Exception): 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? @@ -73,3 +82,18 @@ def handle_api_duration(duration: float): @hook_handler(hook=Hook.PROCESS_DURATION) def handle_process_duration(duration: float): logger.debug(f"tick process duration: {duration}ms") + + +@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 index 7422141..74694cf 100644 --- a/algorunner/monitoring.py +++ b/algorunner/monitoring.py @@ -1,4 +1,6 @@ +from algorunner.hooks import Hook, hook from time import time +from typing import Optional from loguru import logger @@ -6,8 +8,9 @@ class Timer: """Simple timer based context manager, used for performance monitoring in conjunction with hooks.""" - def __init__(self): + def __init__(self, trigger_hook: Optional[Hook] = None): self.duration = None + self.hook = trigger_hook def __enter__(self): self.start = time() @@ -18,5 +21,8 @@ def __exit__(self, exc_type, exc_val, traceback): 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 self.duration * 1000 + return round(self.duration * 1000) diff --git a/algorunner/runner.py b/algorunner/runner.py index 14c147d..6e89fb4 100644 --- a/algorunner/runner.py +++ b/algorunner/runner.py @@ -1,9 +1,10 @@ +from algorunner.hooks import Hook, hook from queue import Queue from signal import SIGTERM, signal from loguru import logger -from algorunner import abstract +from algorunner.strategy import BaseStrategy from algorunner.adapters import Credentials, factory @@ -16,15 +17,15 @@ class Runner(object): def __init__(self, creds: Credentials, - strategy: abstract.BaseStrategy): + strategy: BaseStrategy): self.sync_queue = Queue() - self.adapter = factory(creds["exchange"], self.sync_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()) - logger.debug("finished initialising runner") + hook(Hook.RUNNER_INITIALISED) def _handle_sigterm(self): def _handler(signum, frame): @@ -36,10 +37,10 @@ def _handler(signum, frame): def run(self): """ """ self.adapter.monitor_user(self.trader_queue) - self.adapter.run(self.strategy, self.strategy.process) - logger.info("monitoring user stream and executing strategy") + self.adapter.run(self.strategy, self.strategy) + hook(Hook.RUNNER_STARTING) def stop(self): - logger.info("attempting to shutdown strategy execution and disconnect from exchange") + 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..764154c --- /dev/null +++ b/algorunner/strategy/__init__.py @@ -0,0 +1,8 @@ +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 +) diff --git a/algorunner/abstract/base_strategy.py b/algorunner/strategy/base.py similarity index 55% rename from algorunner/abstract/base_strategy.py rename to algorunner/strategy/base.py index 2235467..daf087b 100644 --- a/algorunner/abstract/base_strategy.py +++ b/algorunner/strategy/base.py @@ -1,31 +1,17 @@ from abc import ABC, abstractmethod -from dataclasses import dataclass +from algorunner.hooks import Hook, hook from queue import Queue from threading import Thread from typing import Callable, Optional from loguru import logger -import pandas as pd -from algorunner.exceptions import StrategyExceptionThresholdBreached -from algorunner.mutations import AccountState, is_update -from algorunner.adapters.base import Adapter, TransactionParams - - -@dataclass -class TransactionRequest: - """Dispatched by `process` this triggers risk calculation via `authorise` - and potential dispatch of a transaction to the exchange.""" - symbol: str - order_type: str +from algorunner.strategy.exceptions import StrategyExceptionThresholdBreached - -@dataclass -class AuthorisationDecision: - """Returned by `authorise` and determines whether a transaction can be - made, and the appropriate parameters for that transaction.""" - accepted: bool - params: Optional[TransactionParams] +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: @@ -70,7 +56,7 @@ def is_running(self) -> bool: def _listen(self): logger.info("listening for events and inbound messages") - exception_count = 0 # count exceptions over past 5 mins. + 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) @@ -97,45 +83,86 @@ def _listen(self): logger.critical("exception rate has breached threshold, failing..") raise StrategyExceptionThresholdBreached("too many exceptions encountered!") - logger.warn("trader thread has completed") + logger.warn("syncagent has completed") def _transaction_handler(self, trx: TransactionRequest): - decision = self.authorisation_guard(self.state, trx) - if not decision.accepted: - logger.info("transaction rejected: failed defined auth rules") + trx = self.authorisation_guard(self.state, trx) + if not trx.approved: + logger.info(f"transaction rejected: {trx.reason}") return - logger.info("transaction accepted: passing to API adapter for dispatch") - self.api.execute(decision.params) + 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 open_position(self, symbol: str): - logger.debug(f"requesting to open new position ({symbol})") - self.sync_queue.put(TransactionRequest(symbol=symbol, order_type="BUY")) + 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 close_position(self, symbol: str): - logger.debug(f"requesting to close position ({symbol})") - self.sync_queue.put(TransactionRequest(symbol=symbol, order_type="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") - @abstractmethod - def process(self, tick: pd.DataFrame): - """ - @todo - accept Union[pd.DataFrame, RawMarketPayload] - where RawMarketPayload is a TypedDict w/ no pandas processing. - """ - pass + def account_state(self) -> AccountState: + return self.sync_agent.account_state - @abstractmethod def authorise(self, state: AccountState, - trx: TransactionRequest) -> AuthorisationDecision: + trx: TransactionRequest) -> TransactionRequest: + logger.info("no authorisation guard set: automatically authorising order") + trx.approved = True + return trx + + @abstractmethod + def process(self, tick: Tick): """ - @todo - define params. """ pass diff --git a/algorunner/strategy/exceptions.py b/algorunner/strategy/exceptions.py new file mode 100644 index 0000000..41d84b7 --- /dev/null +++ b/algorunner/strategy/exceptions.py @@ -0,0 +1,26 @@ +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.py b/algorunner/strategy/loader.py similarity index 92% rename from algorunner/strategy.py rename to algorunner/strategy/loader.py index c683482..3ca8b5f 100644 --- a/algorunner/strategy.py +++ b/algorunner/strategy/loader.py @@ -3,8 +3,8 @@ from loguru import logger -from algorunner.abstract import BaseStrategy -from algorunner.exceptions import ( +from algorunner.strategy import BaseStrategy +from algorunner.strategy.exceptions import ( FailureLoadingStrategy, InvalidStrategyProvided, StrategyNotFound ) diff --git a/strategies/example.py b/strategies/example.py index 191a624..fc23273 100644 --- a/strategies/example.py +++ b/strategies/example.py @@ -1,9 +1,8 @@ import pandas as pd -from algorunner.abstract import BaseStrategy -from algorunner.abstract.base_strategy import ( - AccountState, TransactionRequest, AuthorisationDecision -) +from algorunner.adapters import TransactionRequest +from algorunner.mutations import AccountState +from algorunner.strategy import BaseStrategy class Example(BaseStrategy): @@ -26,5 +25,5 @@ def process(self, tick): 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: + def authorise(self, state: AccountState, trx: TransactionRequest) -> TransactionRequest: pass diff --git a/test/fixtures/valid_strategy.py b/test/fixtures/valid_strategy.py index 887ebae..d7b6ac2 100644 --- a/test/fixtures/valid_strategy.py +++ b/test/fixtures/valid_strategy.py @@ -1,10 +1,11 @@ -from algorunner.abstract import BaseStrategy -from algorunner.abstract.base_strategy import ( - AccountState, TransactionRequest, AuthorisationDecision -) +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) -> AuthorisationDecision: + def authorise(self, state: AccountState, trx: TransactionRequest) -> TransactionRequest: pass diff --git a/test/scenarios/steps/sync_agent.py b/test/scenarios/steps/sync_agent.py index 82265b9..af782e8 100644 --- a/test/scenarios/steps/sync_agent.py +++ b/test/scenarios/steps/sync_agent.py @@ -1,21 +1,25 @@ -from queue import Queue -from unittest import mock from time import sleep +from unittest import mock +from queue import Queue from behave import * -from algorunner.abstract import ( - AuthorisationDecision, - BaseStrategy, - ShutdownRequest, - TransactionRequest, +from algorunner.strategy import ( + ShutdownRequest, BaseStrategy +) +from algorunner.adapters import ( + Adapter, TransactionRequest, OrderType ) -from algorunner.adapters.base import Adapter, TransactionParams from algorunner.mutations import ( Position, BalanceUpdate, AccountUpdate, CapabilitiesUpdate ) +# +# GIVEN +# + + @given("a running sync agent awaiting messages") def new_running_sync_agent(context): Adapter.__abstractmethods__ = {} @@ -31,15 +35,6 @@ def new_running_sync_agent(context): context.agent.start() assert context.agent.is_running() -@when("the sync agent is stopped") -def stop_sync_agent(context): - context.agent_params["queue"].put(ShutdownRequest(reason="bdd tests")) - sleep(.25) - -@then("it should no longer be running") -def sync_agent_not_running(context): - assert not 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 @@ -48,20 +43,6 @@ def account_update_full_capabilities(context, capabilities): positions=[] )) -@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 = [] - -@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 - @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) @@ -72,11 +53,6 @@ def balance_update(context, quantity, symbol): asset=symbol, delta=quantity )) -@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 - @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) @@ -87,34 +63,75 @@ def position_update(context, symbol, free, locked): balances=[Position(symbol, free=free, locked=locked)] )) -@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 - @given("a request to buy {symbol}") def market_order(context, symbol): context.message_list.append(TransactionRequest( - symbol=symbol, order_type="buy" + reason="", symbol="", quantity="", price="", order_type=OrderType.MARKET_SELL )) @given("the order is declined") def calculator_rejection(context): - context.agent_params["auth"].return_value = AuthorisationDecision( - accepted=False, params=None + 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 = AuthorisationDecision( - accepted=True, params=TransactionParams() + 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() diff --git a/test/test_adapter.py b/test/test_adapter.py index 1936e03..699d070 100644 --- a/test/test_adapter.py +++ b/test/test_adapter.py @@ -1,8 +1,8 @@ from algorunner.adapters.base import ( - AdapterError, factory, register_adapter ) +from algorunner.adapters.messages import AdapterError from algorunner.adapters._binance import BinanceAdapter from queue import Queue diff --git a/test/test_monitoring.py b/test/test_monitoring.py index 3cf74ee..39d50cc 100644 --- a/test/test_monitoring.py +++ b/test/test_monitoring.py @@ -1,3 +1,4 @@ +from algorunner.hooks import Hook from unittest.mock import patch from algorunner.monitoring import Timer @@ -25,3 +26,15 @@ def test_timer_bubbles_exceptions(): 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()) diff --git a/test/test_runner.py b/test/test_runner.py index e17d6d1..8d7e758 100644 --- a/test/test_runner.py +++ b/test/test_runner.py @@ -4,9 +4,9 @@ import pytest -from algorunner.abstract import BaseStrategy from algorunner.adapters import Credentials -from algorunner.adapters.base import AdapterError +from algorunner.adapters.messages import AdapterError +from algorunner.strategy import BaseStrategy from algorunner.runner import Runner @@ -29,7 +29,7 @@ def mock_strategy() -> MagicMock: 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"), + creds=Credentials(exchange="binance", key="", secret=""), strategy=mock_strategy ) From 3b6ee464030b629e598705957ed183a04d6f5f02 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Sat, 14 Aug 2021 18:48:15 +0100 Subject: [PATCH 12/15] v2: various amendments aimed at enabling future features - improvements to the test suite - added hook mechanism for monitoring/observing - added automatic registration of adapters, and hook handlers - improvements to strategy loading - update api adapter interface - changes to linting and build tasks - unified logging - changes to exceptions --- .github/workflows/pythonapp.yml | 7 +- .gitignore | 1 + Makefile | 8 +- README.md | 4 +- algorunner/__init__.py | 4 + algorunner/abstract/__init__.py | 1 - algorunner/adapters/__init__.py | 6 +- algorunner/adapters/_backtest.py | 50 +++++++ algorunner/adapters/_binance.py | 112 ++++++++------- algorunner/adapters/base.py | 92 +++--------- algorunner/adapters/messages.py | 77 ++++++++++ algorunner/exceptions.py | 39 ++---- algorunner/hooks.py | 38 +++-- algorunner/monitoring.py | 32 ++++- algorunner/mutations.py | 46 +++--- algorunner/runner.py | 13 +- algorunner/strategy/__init__.py | 8 ++ .../base_strategy.py => strategy/base.py} | 132 +++++++++++++----- algorunner/strategy/exceptions.py | 31 ++++ .../{strategy.py => strategy/loader.py} | 18 ++- poetry.lock | 76 +++++++++- pyproject.toml | 12 +- strategies/example.py | 28 +++- test/conftest.py | 35 +++++ test/fixtures/valid_strategy.py | 9 +- test/helpers.py | 13 -- test/scenarios/steps/sync_agent.py | 104 ++++++++------ test/test_adapter.py | 2 +- test/test_monitoring.py | 19 +++ test/test_runner.py | 21 +-- 30 files changed, 698 insertions(+), 340 deletions(-) delete mode 100644 algorunner/abstract/__init__.py create mode 100644 algorunner/adapters/_backtest.py create mode 100644 algorunner/adapters/messages.py create mode 100644 algorunner/strategy/__init__.py rename algorunner/{abstract/base_strategy.py => strategy/base.py} (51%) create mode 100644 algorunner/strategy/exceptions.py rename algorunner/{strategy.py => strategy/loader.py} (63%) create mode 100644 test/conftest.py delete mode 100644 test/helpers.py diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 391171f..6925ef6 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -27,9 +27,6 @@ jobs: - name: Install dependencies run: | make deps - - name: Run linter + - name: Run linting and tests run: | - make lint - - name: Run tests - run: | - make test + make ci diff --git a/.gitignore b/.gitignore index a93e8f9..440b3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +.todo __pycache__/ bot.ini plain.output diff --git a/Makefile b/Makefile index 4914b0f..8a27dc0 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: env-check build lint deps test run todo +.PHONY: help env-check build lint deps test ci run todo -help: ## Show this help. +help: ## Show this help. @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' env-check: ## Check that the current environment is capable of running AlgoRunner. @@ -10,6 +10,7 @@ build: ## Build docker image, tagged "algorunner:" echo "build docker container" lint: ## Run code quality checks + poetry run black algorunner poetry run flake8 deps: ## Install all required dependencies (including for development) @@ -19,6 +20,9 @@ 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 diff --git a/README.md b/README.md index 8a2e7dc..2fd1830 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# ... @todo? +# AlgoRunner +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + ## Running diff --git a/algorunner/__init__.py b/algorunner/__init__.py index e69de29..01e5d1b 100644 --- a/algorunner/__init__.py +++ b/algorunner/__init__.py @@ -0,0 +1,4 @@ +# +# @todo - really need to work out what needs to be exposed +# for user defined strategies - so we can use `pdoc`. +# diff --git a/algorunner/abstract/__init__.py b/algorunner/abstract/__init__.py deleted file mode 100644 index ab0bca2..0000000 --- a/algorunner/abstract/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from algorunner.abstract.base_strategy import * # noqa: F401 F403 diff --git a/algorunner/adapters/__init__.py b/algorunner/adapters/__init__.py index bb8b393..8229973 100644 --- a/algorunner/adapters/__init__.py +++ b/algorunner/adapters/__init__.py @@ -1,9 +1,5 @@ -from algorunner.adapters._binance import BinanceAdapter # noqa: F401 +from algorunner.adapters.messages import * # noqa: F401, F403 from algorunner.adapters.base import ( # noqa: F401 Adapter, - AdapterError, - Credentials, - InvalidOrder, - InvalidPayloadError, 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 index ec30524..a5f5880 100644 --- a/algorunner/adapters/_binance.py +++ b/algorunner/adapters/_binance.py @@ -5,16 +5,22 @@ import pandas as pd -from algorunner.adapters.base import ( -<<<<<<< HEAD - Adapter, Credentials, InvalidOrder, InvalidPayloadError, OrderType, TransactionRequest, register_adapter -======= - Adapter, Credentials, InvalidPayloadRecieved, TransactionParams, register_adapter ->>>>>>> develop +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 + AccountUpdate, + BaseUpdate, + BalanceUpdate, + CapabilitiesUpdate, + Position, ) @@ -31,34 +37,36 @@ 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", + 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') + inplace=True, + ) + df.set_index("EventTime", inplace=True) + df.index = pd.to_datetime(df.index, unit="ms") return df class UserStreamEventTransformer: @@ -70,7 +78,7 @@ def __call__(self, payload) -> BaseUpdate: "outboundAccountInfo": self.account_update, "outboundAccountPosition": self.position_update, "balanceUpdate": self.balance_update, - "executionReport": self.order_report + "executionReport": self.order_report, } # what if we need to return multiple? i.e CapabilitiesUpdate AND AccountUpdate @@ -88,22 +96,29 @@ def initial_rest_payload(self, payload) -> 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"]] + 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"] - ]) + 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 + """@todo return Message( Type=MessageType.UPDATE_POSITION, Msg={ @@ -125,20 +140,21 @@ def order_endpoint(self, payload): pass def connect(self, creds: Credentials): - self.client = Client(creds['key'], creds['secret']) + 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 + 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() - )) + 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( diff --git a/algorunner/adapters/base.py b/algorunner/adapters/base.py index ec41123..804bb08 100644 --- a/algorunner/adapters/base.py +++ b/algorunner/adapters/base.py @@ -1,74 +1,10 @@ from abc import ABC, abstractmethod -from dataclasses import dataclass -from enum import Enum -from typing import Callable, Optional, Union +from typing import Callable from queue import Queue from loguru import logger -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: str - symbol: str - order_type: OrderType - quantity: float - price: Optional[float] - approved: bool = False - - 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 +from algorunner.adapters import messages class Adapter(ABC): @@ -78,24 +14,24 @@ def __init__(self, sync_queue: Queue): self.sync_queue = sync_queue @abstractmethod - def connect(self, creds: Credentials): + def connect(self, creds: messages.Credentials): """connect authenticates with the exchange, and also populates - the associated `Trader` object with the latest state.""" + the associated `Trader` object with the latest state.""" pass @abstractmethod def monitor_user(self): - """ @todo """ + """@todo""" pass @abstractmethod - def run(self, process: Callable, terminated: bool): + def run(self, symbol: str, process: Callable): """run executes the underlying strategy, ensuring that any data - transformation required is carried out correctly.""" + transformation required is carried out correctly.""" pass @abstractmethod - def execute(self, trx: TransactionRequest) -> bool: + def execute(self, trx: messages.TransactionRequest) -> bool: pass @abstractmethod @@ -111,10 +47,14 @@ def register_adapter(cls): try: identifier = getattr(cls, "identifier") except AttributeError: - raise AdapterError(f"cannot find identifier for adapter class: {cls.__name__}") + raise messages.AdapterError( + f"cannot find identifier for adapter class: {cls.__name__}" + ) if hasattr(_available_adapters, identifier): - raise AdapterError(f"attempt at registering duplicate adapter for identifier: {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}'") @@ -125,6 +65,8 @@ 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 AdapterError(f"no adapter registered for identifier {requested_adapter}") + 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 index 437a5e4..d2ea81c 100644 --- a/algorunner/exceptions.py +++ b/algorunner/exceptions.py @@ -9,10 +9,14 @@ 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(", ")) + MSG_INVALID_CONFIG + if not invalid_fields + else MSG_INVALID_CONFIG_W_FIELDS.format( + fields=invalid_fields.join(", ") + ) ) @@ -21,6 +25,7 @@ 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}'" @@ -30,30 +35,8 @@ 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}'" - - -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 + 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 index b165369..ded0848 100644 --- a/algorunner/hooks.py +++ b/algorunner/hooks.py @@ -1,3 +1,4 @@ +from algorunner.adapters.messages import TransactionRequest from enum import Enum from typing import Callable, Optional @@ -6,19 +7,29 @@ class Hook(Enum): """Hook represents valid hooks for user-defined functions to listen - for.""" - PROCESS_DURATION = 1 - API_EXECUTE_DURATION = 2 + 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? @@ -27,6 +38,7 @@ class InvalidHookHandler(Exception): 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}'") @@ -38,6 +50,7 @@ def register(fn): 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 @@ -57,7 +70,7 @@ def hook(hook: Hook, *args, **kwargs): def clear_handlers(hook: Optional[Hook] = None): """`clear_handlers` clears registered handlers; optionally for - a specific hook""" + a specific hook""" if hook and _registered_hooks.get(hook): _registered_hooks[hook] = [] return @@ -65,11 +78,16 @@ def clear_handlers(hook: Optional[Hook] = None): _registered_hooks.clear() -@hook_handler(hook=Hook.API_EXECUTE_DURATION) -def handle_api_duration(duration: float): - logger.debug(f"api execution duration: {duration}ms") +@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.PROCESS_DURATION) -def handle_process_duration(duration: float): - logger.debug(f"tick process duration: {duration}ms") +@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 index 7422141..a874199 100644 --- a/algorunner/monitoring.py +++ b/algorunner/monitoring.py @@ -1,22 +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): + 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) + self.duration = time() - self.start if exc_type: - logger.error(f"detected exception during monitoring: {exc_type} ({exc_val})") + logger.error( + f"detected exception during monitoring: {exc_type} ({exc_val})" + ) + + if self.hook: + hook(self.hook, self.ms()) def ms(self) -> float: - return self.duration * 1000 + 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 index abcd6c6..c2e6398 100644 --- a/algorunner/mutations.py +++ b/algorunner/mutations.py @@ -9,14 +9,15 @@ 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 + "can_withdraw": True, + "can_deposit": True, + "can_trade": True, } def balance(self, asset: str) -> Tuple[float, float]: @@ -32,14 +33,16 @@ def capability(self, capability: str) -> bool: 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 - }) + logger.error( + "invalid arguments supplied to update object!", + {"expected": self.REQUIRED_PROPS, "actual": kwargs}, + ) raise InvalidUpdate(prop, self.__class__) self.__dict__.update(kwargs) @@ -52,6 +55,7 @@ def handle(self, state: AccountState) -> AccountState: @dataclass class Position: """Position signifies the position associated with a given asset.""" + asset: str free: float locked: float @@ -73,21 +77,22 @@ def is_update(cls): class OrderUpdate(BaseUpdate): """Recieved when there are changes to an order associated with the account.""" - REQUIRED_PROPS = [ - "symbol", "orderId", "side", "type", "status", "quantity" - ] + 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 - }} + state.orders[self.order_id] = { + **order, + **{ + "symbol": self.symbol, + "side": self.side, + "type": self.type, + "status": self.status, + "quantity": self.quantity, + }, + } return state @@ -95,13 +100,14 @@ def handle(self, state: AccountState) -> AccountState: 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 = state.balances.get( + self.asset, Position(asset=self.asset, free=0, locked=0) + ) asset_balance.free += self.delta state.balances[self.asset] = asset_balance @@ -111,6 +117,7 @@ def handle(self, state: AccountState) -> AccountState: @register_update class AccountUpdate(BaseUpdate): """An update containing any balances which have changed.""" + REQUIRED_PROPS = ["balances"] # List[Position] def handle(self, state: AccountState) -> AccountState: @@ -124,6 +131,7 @@ def handle(self, state: AccountState) -> AccountState: @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: diff --git a/algorunner/runner.py b/algorunner/runner.py index 99f27ed..577bd0b 100644 --- a/algorunner/runner.py +++ b/algorunner/runner.py @@ -1,9 +1,10 @@ +from algorunner.hooks import Hook, hook from queue import Queue from signal import SIGTERM, signal from loguru import logger -from algorunner import abstract +from algorunner.strategy import BaseStrategy from algorunner.adapters import Credentials, factory @@ -14,9 +15,7 @@ class Runner(object): `run.py`. """ - def __init__(self, - creds: Credentials, - strategy: abstract.BaseStrategy): + def __init__(self, creds: Credentials, strategy: BaseStrategy): self.sync_queue = Queue() self.adapter = factory(creds.exchange, self.sync_queue) self.strategy = strategy @@ -24,7 +23,7 @@ def __init__(self, self.strategy.start_sync(self.sync_queue, self.adapter) self.adapter.connect(creds) signal(SIGTERM, self._handle_sigterm()) - logger.debug("finished initialising runner") + hook(Hook.RUNNER_INITIALISED) def _handle_sigterm(self): def _handler(signum, frame): @@ -37,9 +36,9 @@ def run(self): """ """ self.adapter.monitor_user(self.trader_queue) self.adapter.run(self.strategy, self.strategy) - logger.info("monitoring user stream and executing strategy") + hook(Hook.RUNNER_STARTING) def stop(self): - logger.info("attempting to shutdown strategy execution and disconnect from exchange") + 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..d7fc3dd --- /dev/null +++ b/algorunner/strategy/__init__.py @@ -0,0 +1,8 @@ +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, +) diff --git a/algorunner/abstract/base_strategy.py b/algorunner/strategy/base.py similarity index 51% rename from algorunner/abstract/base_strategy.py rename to algorunner/strategy/base.py index 15f656d..c2e4bbb 100644 --- a/algorunner/abstract/base_strategy.py +++ b/algorunner/strategy/base.py @@ -1,19 +1,27 @@ 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, Optional +from typing import Callable, Dict, Optional from loguru import logger -import pandas as pd -from algorunner.exceptions import StrategyExceptionThresholdBreached +from algorunner.strategy.exceptions import StrategyExceptionThresholdBreached + from algorunner.monitoring import Timer from algorunner.mutations import AccountState, is_update -from algorunner.adapters.base import Adapter, InvalidOrder, TransactionRequest, Tick +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 @@ -61,10 +69,14 @@ def _listen(self): try: if message_type == ShutdownRequest: - logger.warning(f"terminating trader thread ({message.reason}).") + logger.warning( + f"terminating trader thread ({message.reason})." + ) break elif message_type == TransactionRequest: - logger.info("request recieved from strategy to execute a transaction") + logger.info( + "request recieved from strategy to execute a transaction" + ) self._transaction_handler(message) continue elif not is_update(message_type): @@ -73,13 +85,21 @@ def _listen(self): 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, - }) + 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.critical( + "exception rate has breached threshold, failing.." + ) + raise StrategyExceptionThresholdBreached( + "too many exceptions encountered!" + ) logger.warn("syncagent has completed") @@ -89,53 +109,89 @@ def _transaction_handler(self, trx: TransactionRequest): logger.info(f"transaction rejected: {trx.reason}") return - t = Timer() + t = Timer(Hook.API_EXECUTE_DURATION) with t: try: - logger.info("transaction accepted: passing to API adapter for dispatch") + logger.info( + "transaction accepted: passing to API adapter for dispatch" + ) self.api.execute(trx) except InvalidOrder: pass - # @todo hook(API_PROCESS) - + def __call__(self, tick: Tick): - t = Timer() + t = Timer(Hook.PROCESS_DURATION) with t: self.process(tick) - - # @todo call hook def start_sync(self, queue: Queue, adapter: Adapter): self.sync_agent = self.SyncAgent(queue, adapter, self.log) self.sync_queue = queue - def open_position(self, symbol: str): - logger.debug(f"requesting to open new position ({symbol})") - self.sync_queue.put(TransactionRequest(symbol=symbol, order_type="BUY")) + 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 close_position(self, symbol: str): - logger.debug(f"requesting to close position ({symbol})") - self.sync_queue.put(TransactionRequest(symbol=symbol, order_type="SELL")) + 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 - - @abstractmethod - def authorise(self, - state: AccountState, - trx: TransactionRequest) -> TransactionRequest: - """ - @todo - define params. - """ - pass + + def authorise( + self, state: AccountState, trx: TransactionRequest + ) -> TransactionRequest: + logger.info( + "no authorisation guard set: automatically authorising order" + ) + trx.approved = True + return trx + + 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])) @abstractmethod def process(self, tick: Tick): - """ - @todo - accept Union[pd.DataFrame, RawMarketPayload] - where RawMarketPayload is a TypedDict w/ no pandas processing. - """ + """ """ 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.py b/algorunner/strategy/loader.py similarity index 63% rename from algorunner/strategy.py rename to algorunner/strategy/loader.py index c683482..f4708f1 100644 --- a/algorunner/strategy.py +++ b/algorunner/strategy/loader.py @@ -3,19 +3,25 @@ from loguru import logger -from algorunner.abstract import BaseStrategy -from algorunner.exceptions import ( - FailureLoadingStrategy, InvalidStrategyProvided, StrategyNotFound +from algorunner.strategy import BaseStrategy +from algorunner.strategy.exceptions import ( + FailureLoadingStrategy, + InvalidStrategyProvided, + StrategyNotFound, ) -_DEFAULT_STRATEGY_PARENT_MODULE = 'strategies.{module}' +_DEFAULT_STRATEGY_PARENT_MODULE = "strategies.{module}" -def load_strategy(strategy_name: str, module_name: Optional[str] = None) -> BaseStrategy: +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") + logger.debug( + "using default module name - looking in strategies directory" + ) module_name = _DEFAULT_STRATEGY_PARENT_MODULE.format( module=strategy_name.lower() ) diff --git a/poetry.lock b/poetry.lock index c1b4ebc..9a3cb11 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,6 +17,14 @@ 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" @@ -64,6 +72,28 @@ six = ">=1.11" 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" @@ -187,6 +217,14 @@ 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" @@ -246,6 +284,14 @@ six = ">=1.11" 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 = "pluggy" version = "0.13.1" @@ -387,6 +433,14 @@ 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" @@ -461,7 +515,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "288c288e00be59e89ae280bb07007a531aca8a5f84610b7b7e6783d953c945c9" +content-hash = "5ea8b1f3269e8c1c0ea35e5e3517262750cfdf424915f411b96c53dcf4974f26" [metadata.files] aiohttp = [ @@ -503,6 +557,10 @@ aiohttp = [ {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"}, @@ -519,6 +577,10 @@ 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"}, @@ -600,6 +662,10 @@ multidict = [ {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"}, @@ -662,6 +728,10 @@ 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"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -745,6 +815,10 @@ 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"}, diff --git a/pyproject.toml b/pyproject.toml index 0c193b2..956609a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + [tool.poetry] name = "AlgoRunner" version = "0.0.1" @@ -16,7 +20,9 @@ loguru = "^0.5.3" behave = "^1.2.6" pytest = "^6.2.4" flake8 = "^3.9.2" +black = "^21.7b0" -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.black] +line-length = 80 +target-version = ['py37'] +include = '\.pyi?$' diff --git a/strategies/example.py b/strategies/example.py index ffa4078..4291687 100644 --- a/strategies/example.py +++ b/strategies/example.py @@ -1,9 +1,11 @@ import pandas as pd -from algorunner.abstract import BaseStrategy -from algorunner.abstract.base_strategy import ( - AccountState, TransactionRequest -) +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): @@ -16,15 +18,29 @@ class Example(BaseStrategy): _testing_tag = True def __init__(self): - self.series = pd.DataFrame 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"]) - print("Average price change over past 5 windows: ", recent_window.mean()) + 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/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/valid_strategy.py b/test/fixtures/valid_strategy.py index 741e54e..d7b6ac2 100644 --- a/test/fixtures/valid_strategy.py +++ b/test/fixtures/valid_strategy.py @@ -1,7 +1,8 @@ -from algorunner.abstract import BaseStrategy -from algorunner.abstract.base_strategy import ( - AccountState, TransactionRequest -) +from algorunner.adapters import TransactionRequest +from algorunner.mutations import AccountState +from algorunner.strategy import BaseStrategy + + class ValidStrategy(BaseStrategy): def process(self, tick): return True diff --git a/test/helpers.py b/test/helpers.py deleted file mode 100644 index 75a4f55..0000000 --- a/test/helpers.py +++ /dev/null @@ -1,13 +0,0 @@ -from contextlib import contextmanager -from json import load -from pytest import fixture - - -@fixture -def load_fixture(): - @contextmanager - def open_fixture(payload_file): - with open(payload_file) as json_file: - yield load(json_file) - - return open_fixture diff --git a/test/scenarios/steps/sync_agent.py b/test/scenarios/steps/sync_agent.py index 2d2e146..af782e8 100644 --- a/test/scenarios/steps/sync_agent.py +++ b/test/scenarios/steps/sync_agent.py @@ -1,18 +1,25 @@ -from queue import Queue -from unittest import mock from time import sleep +from unittest import mock +from queue import Queue from behave import * -from algorunner.abstract.base_strategy import ( +from algorunner.strategy import ( ShutdownRequest, BaseStrategy ) -from algorunner.adapters.base import Adapter, OrderType, TransactionRequest +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__ = {} @@ -28,15 +35,6 @@ def new_running_sync_agent(context): context.agent.start() assert context.agent.is_running() -@when("the sync agent is stopped") -def stop_sync_agent(context): - context.agent_params["queue"].put(ShutdownRequest(reason="bdd tests")) - sleep(.25) - -@then("it should no longer be running") -def sync_agent_not_running(context): - assert not 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 @@ -45,20 +43,6 @@ def account_update_full_capabilities(context, capabilities): positions=[] )) -@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 = [] - -@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 - @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) @@ -69,11 +53,6 @@ def balance_update(context, quantity, symbol): asset=symbol, delta=quantity )) -@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 - @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) @@ -84,16 +63,6 @@ def position_update(context, symbol, free, locked): balances=[Position(symbol, free=free, locked=locked)] )) -@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 - @given("a request to buy {symbol}") def market_order(context, symbol): context.message_list.append(TransactionRequest( @@ -112,6 +81,57 @@ 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() diff --git a/test/test_adapter.py b/test/test_adapter.py index 1936e03..699d070 100644 --- a/test/test_adapter.py +++ b/test/test_adapter.py @@ -1,8 +1,8 @@ from algorunner.adapters.base import ( - AdapterError, factory, register_adapter ) +from algorunner.adapters.messages import AdapterError from algorunner.adapters._binance import BinanceAdapter from queue import Queue diff --git a/test/test_monitoring.py b/test/test_monitoring.py index 3cf74ee..d36177b 100644 --- a/test/test_monitoring.py +++ b/test/test_monitoring.py @@ -1,5 +1,7 @@ +from algorunner.hooks import Hook, hook from unittest.mock import patch + from algorunner.monitoring import Timer def test_timer_returns_duration_in_ms(): @@ -25,3 +27,20 @@ def test_timer_bubbles_exceptions(): 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") \ No newline at end of file diff --git a/test/test_runner.py b/test/test_runner.py index 7865343..4418c08 100644 --- a/test/test_runner.py +++ b/test/test_runner.py @@ -2,30 +2,11 @@ from signal import SIGTERM from unittest.mock import MagicMock, patch -import pytest - -from algorunner.abstract import BaseStrategy from algorunner.adapters import Credentials -from algorunner.adapters.base import AdapterError +from algorunner.adapters.messages import AdapterError from algorunner.runner import Runner -@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 - - def test_handle_graceful_shutdown(mock_adapter: MagicMock, mock_strategy: MagicMock): with patch('algorunner.runner.signal') as mock_signal: r = Runner( From e870493d746e21fb5460559cf55aa15e45dc7e45 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Sun, 22 Aug 2021 00:43:43 +0100 Subject: [PATCH 13/15] docs: pdoc integration --- Makefile | 11 +- README.md | 6 - algorunner/__init__.py | 25 +- algorunner/adapters/__init__.py | 6 + algorunner/exceptions.py | 6 + algorunner/pdoc.md | 26 + algorunner/strategy/__init__.py | 6 + algorunner/strategy/base.py | 4 +- docs/algorunner.html | 1082 +++++++++++++++++++++++++++++++ docs/index.html | 59 ++ docs/search.json | 1 + poetry.lock | 95 ++- pyproject.toml | 1 + 13 files changed, 1311 insertions(+), 17 deletions(-) create mode 100644 algorunner/pdoc.md create mode 100644 docs/algorunner.html create mode 100644 docs/index.html create mode 100644 docs/search.json diff --git a/Makefile b/Makefile index 6868e21..fe9a6c4 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,10 @@ env-check: ## Check that the current environment is capable of running AlgoRun 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` +fix: ## Attempt to fix linting issues with `black` poetry run black algorunner -lint: ## Run code quality checks +lint: ## Run code quality checks poetry run black --check algorunner poetry run flake8 @@ -23,12 +23,15 @@ 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 +ci: lint test ## Run both linting and testing @echo "finished running CI tasks" -run: ## Run AlgoRunner +run: ## Run AlgoRunner poetry run python run.py +docs: ## Generate API documentation using "pdoc" + poetry run pdoc -o ./docs algorunner + 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 bfe43d9..c91847f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,4 @@ -<<<<<<< HEAD -# AlgoRunner -======= # AlgoRunner -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) - ->>>>>>> develop 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. diff --git a/algorunner/__init__.py b/algorunner/__init__.py index 01e5d1b..63a447d 100644 --- a/algorunner/__init__.py +++ b/algorunner/__init__.py @@ -1,4 +1,21 @@ -# -# @todo - really need to work out what needs to be exposed -# for user defined strategies - so we can use `pdoc`. -# +""" + .. 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 index 8229973..1ba3d99 100644 --- a/algorunner/adapters/__init__.py +++ b/algorunner/adapters/__init__.py @@ -1,3 +1,9 @@ +""" + +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, diff --git a/algorunner/exceptions.py b/algorunner/exceptions.py index d2ea81c..599e386 100644 --- a/algorunner/exceptions.py +++ b/algorunner/exceptions.py @@ -1,3 +1,9 @@ +""" +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" diff --git a/algorunner/pdoc.md b/algorunner/pdoc.md new file mode 100644 index 0000000..07cf1cd --- /dev/null +++ b/algorunner/pdoc.md @@ -0,0 +1,26 @@ +> @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. + +> **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/strategy/__init__.py b/algorunner/strategy/__init__.py index d7fc3dd..788ced4 100644 --- a/algorunner/strategy/__init__.py +++ b/algorunner/strategy/__init__.py @@ -1,3 +1,7 @@ +""" +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 @@ -6,3 +10,5 @@ StrategyExceptionThresholdBreached, StrategyNotFound, ) + +__all__ = ['BaseStrategy'] \ No newline at end of file diff --git a/algorunner/strategy/base.py b/algorunner/strategy/base.py index 415d484..ce59650 100644 --- a/algorunner/strategy/base.py +++ b/algorunner/strategy/base.py @@ -35,7 +35,7 @@ class BaseStrategy(ABC): positions) """ - class SyncAgent: + class _SyncAgent: def __init__(self, queue: Queue, adapter: Adapter, auth: Callable): self.queue = queue self.api = adapter @@ -125,7 +125,7 @@ def __call__(self, tick: Tick): self.process(tick) def start_sync(self, queue: Queue, adapter: Adapter): - self.sync_agent = self.SyncAgent(queue, adapter, self.log) + self.sync_agent = self._SyncAgent(queue, adapter, self.log) self.sync_queue = queue def _place_order(self, params: dict): diff --git a/docs/algorunner.html b/docs/algorunner.html new file mode 100644 index 0000000..34b2830 --- /dev/null +++ b/docs/algorunner.html @@ -0,0 +1,1082 @@ + + + + + + + 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.

+ +
+

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/index.html b/docs/index.html new file mode 100644 index 0000000..c646eb3 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,59 @@ + + + + + + + Module List – pdoc 7.4.0 + + + + + + + + + + +
+ + pdoc logo + + +
+
+
+ + \ No newline at end of file diff --git a/docs/search.json b/docs/search.json new file mode 100644 index 0000000..3adb638 --- /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
\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": 93}, "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}}, "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}}}}, "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}}}}}, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.Hook": {"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, "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}}}}, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"algorunner": {"tf": 1.7320508075688772}}, "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}}}}}}}}}, "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}}}}}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"algorunner": {"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/poetry.lock b/poetry.lock index 9a3cb11..c72fc7f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -186,6 +186,20 @@ 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" @@ -201,6 +215,14 @@ 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" @@ -292,6 +314,22 @@ 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" @@ -327,6 +365,14 @@ 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" @@ -515,7 +561,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "5ea8b1f3269e8c1c0ea35e5e3517262750cfdf424915f411b96c53dcf4974f26" +content-hash = "006e1cf93de1feda94acf8ecdf10f8cb5628bd5c078ed81e9fecf0ea0ad5db75" [metadata.files] aiohttp = [ @@ -615,10 +661,50 @@ idna = [ 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"}, @@ -732,6 +818,9 @@ 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"}, @@ -748,6 +837,10 @@ 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"}, diff --git a/pyproject.toml b/pyproject.toml index 956609a..17ce5e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ 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" From 573a0024337eb7dec22155c3469b40cdf4e643a5 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Sun, 22 Aug 2021 00:46:44 +0100 Subject: [PATCH 14/15] incidental: tweak docs Make task --- Makefile | 4 +- docs/algorunner.html | 1082 ----------------------------------------- docs/index.html | 1109 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 1069 insertions(+), 1126 deletions(-) delete mode 100644 docs/algorunner.html diff --git a/Makefile b/Makefile index fe9a6c4..5574922 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help env-check build lint deps test ci run todo +.PHONY: help env-check build lint deps test ci run todo docs help: ## Show this help. @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' @@ -31,6 +31,8 @@ run: ## Run AlgoRunner 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 diff --git a/docs/algorunner.html b/docs/algorunner.html deleted file mode 100644 index 34b2830..0000000 --- a/docs/algorunner.html +++ /dev/null @@ -1,1082 +0,0 @@ - - - - - - - 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.

- -
-

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/index.html b/docs/index.html index c646eb3..34b2830 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,56 +4,1079 @@ - Module List – pdoc 7.4.0 + algorunner API documentation - + - - - -
- - pdoc logo - - -
+
+
+

+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.

+ +
+

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 From f0f582dbdf1be6e9b14338617254672bb83fd170 Mon Sep 17 00:00:00 2001 From: Fergus Morrow Date: Sun, 22 Aug 2021 00:50:06 +0100 Subject: [PATCH 15/15] update doc links --- README.md | 2 +- algorunner/pdoc.md | 2 ++ docs/index.html | 2 ++ docs/search.json | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c91847f..c9c88c3 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ class Example(BaseStrategy): 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. +For information on the classes used - i.e. `AuthorisationDecision`, `TransactionRequest`, and `AccountState` - please see the [API documentation](https://fergusinlondon.github.io/Runner/). ## Required Configuration diff --git a/algorunner/pdoc.md b/algorunner/pdoc.md index 07cf1cd..73a73cf 100644 --- a/algorunner/pdoc.md +++ b/algorunner/pdoc.md @@ -3,6 +3,8 @@ 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 diff --git a/docs/index.html b/docs/index.html index 34b2830..4fc0bbd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -162,6 +162,8 @@

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.

diff --git a/docs/search.json b/docs/search.json index 3adb638..7affc01 100644 --- a/docs/search.json +++ b/docs/search.json @@ -1 +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
\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": 93}, "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}}, "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}}}}, "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}}}}}, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"algorunner.Hook": {"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, "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}}}}, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"algorunner": {"tf": 1.7320508075688772}}, "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}}}}}}}}}, "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}}}}}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"algorunner": {"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 +{"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