Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a40c920
V2 Issue 1: Rearchitecture and new build process
FergusInLondon May 5, 2020
f139548
incidental: enable CI on develop branch
FergusInLondon Aug 6, 2021
b36df0a
Merge branch 'develop' into huge-refactor
FergusInLondon Aug 6, 2021
268308f
Merge pull request #1 from FergusInLondon/huge-refactor
FergusInLondon Aug 6, 2021
dcf47d1
incidental: line endings and .gitignore rule
FergusInLondon Aug 6, 2021
213beff
V2: Actor implementation for "Trader" (WIP) (#4)
FergusInLondon Aug 12, 2021
785838d
Merge branch 'master' into develop
FergusInLondon Aug 13, 2021
bb765fe
docs wip
FergusInLondon Aug 13, 2021
4aaeb59
automatic adapter registration
FergusInLondon Aug 13, 2021
a5b8029
v2: include hooks functionality for logging/monitoring (#9)
FergusInLondon Aug 14, 2021
438cce1
wip: documentation
FergusInLondon Aug 14, 2021
e6989ad
v2 - work towards exchange interactions and order processing
FergusInLondon Aug 13, 2021
742ad2a
Merge branch 'develop' into v2/order-placement-exchange-interactions
FergusInLondon Aug 14, 2021
8fa8ace
some readme updates
FergusInLondon Aug 14, 2021
c2f2bfe
Support Order Flow and define API abstraction (#7)
FergusInLondon Aug 14, 2021
3b6ee46
v2: various amendments aimed at enabling future features
FergusInLondon Aug 14, 2021
e0f0bd7
Merge branch 'develop' of github.com:FergusInLondon/Runner into develop
FergusInLondon Aug 19, 2021
9e0147e
Merge branch 'develop' into v2/docs
FergusInLondon Aug 19, 2021
e870493
docs: pdoc integration
FergusInLondon Aug 21, 2021
573a002
incidental: tweak docs Make task
FergusInLondon Aug 21, 2021
f0f582d
update doc links
FergusInLondon Aug 21, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .behaverc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[behave]
format=plain
paths=test/scenarios
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
exclude=test/*
25 changes: 14 additions & 11 deletions .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Runner
name: AlgoRunner

on:
push:
branches: [ master ]
branches: [ master, develop ]
pull_request:
branches: [ master ]

jobs:
build:
ci:
strategy:
fail-fast: true
matrix:
python-version: [3.9] # Py3 only, and latest release UP.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
- uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.0.0
with:
python-version: 3.8
poetry-version: 1.1.7
- name: Install dependencies
run: |
make deps
- name: Run linter
run: |
make lint
- name: Run tests
- name: Run linting and tests
run: |
make test
make ci
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.idea/
.todo
__pycache__/
bot.ini
bot.ini
plain.output
1 change: 1 addition & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.9-slim

RUN apt-get update && apt-get install build-essential -y

WORKDIR /algorunner
COPY poetry.lock pyproject.toml Makefile setup.sh /algorunner/

RUN make env-check

# @todo --no-dev --no-ansi
RUN poetry config virtualenvs.create false && make deps

COPY . /code
ENTRYPOINT [ "make", "local" ]

# @todo - secondary layer with development dependencies removed
7 changes: 7 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2021 [@FergusInLondon <fergus@fergus.london>]

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.
47 changes: 36 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
.PHONY: deps test
.PHONY: help env-check build lint deps test ci run todo docs

lint:
flake8 ./lib --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 ./lib --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
help: ## Show this help.
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'

deps:
pip install pandas
pip install python-binance
pip install flake8
env-check: ## Check that the current environment is capable of running AlgoRunner.
@sh setup.sh

test:
python -m test.account
python -m test.runner
build: ## Build docker image, tagged "algorunner:<commit>" and "algorunner:latest"
docker build -t algorunner:latest -t algorunner:`git rev-parse --short HEAD` .

fix: ## Attempt to fix linting issues with `black`
poetry run black algorunner

lint: ## Run code quality checks
poetry run black --check algorunner
poetry run flake8

deps: env-check ## Install all required dependencies (including for development)
poetry install --no-interaction

test: ## Run all tests - including both unit tests and BDD scenarios
poetry run pytest
poetry run behave

ci: lint test ## Run both linting and testing
@echo "finished running CI tasks"

run: ## Run AlgoRunner
poetry run python run.py

docs: ## Generate API documentation using "pdoc"
poetry run pdoc -o ./docs algorunner
rm docs/index.html
mv docs/algorunner.html docs/index.html

todo: ## Scan the codebase for items tagged with "@todo"
@grep -r "@todo" --exclude=\*.pyc algorunner
@echo "\nTotal items marked '@todo': `grep --exclude=\*.pyc -r '@todo' . | wc -l | xargs`."
130 changes: 93 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,113 @@
# Runner ![Runner](https://github.com/FergusInLondon/Runner/workflows/Runner/badge.svg)
# AlgoRunner

A massive WIP that may or may not be worth an actual README at this point in time. It has no tests, and the account functionality is still being baked in.
A lightweight service for running algorithmic trading strategies against cryptocurrency exchanges. Currently under heavy development and defining the exchange interactions, as well moving towards support for multiple exchanges.

Currently it does invoke a strategy and provides it with real-time streamed data from Binance though.
All development is done against the `develop` branch, although at this time that's likely the branch you actually want to browse.

### Note
This is *vaguely* related to my form of Enigma Catalyst, as (a) I really want to brush up on my Python, and (b) Binance seems like the best exchange to implement streaming trades on - so I'd like to get used to interacting with them.
| Branch | Status |
| ------- | ------------------------------------------------------------ |
| Master | ![Unit Tests & Build](https://github.com/FergusInLondon/Runner/actions/workflows/pythonapp.yml/badge.svg)![CodeQL](https://github.com/FergusInLondon/Runner/actions/workflows/codeql-analysis.yml/badge.svg)[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) |
| Develop | ![Unit Tests & Build](https://github.com/FergusInLondon/Runner/actions/workflows/pythonapp.yml/badge.svg?branch=develop)![CodeQL](https://github.com/FergusInLondon/Runner/actions/workflows/codeql-analysis.yml/badge.svg?branch=develop)[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) |

## Example
### Living Design Doc

Check `example.py` for a runnable version of this strategy:
...

## Defining a strategy

To define a strategy to execute you simply need to define a `Strategy` class and place it in the `./strategies` folder where it can be loaded. A strategy **must** inherit from `BaseStrategy` and **must** implement two methods: `process` and `authorise`.

```python
class ExampleStrategy(object):
"""
A simple example strategy that computes the average price change over
the previous 5 2000ms updates.
"""
import pandas as pd

from algorunner.abstract import BaseStrategy
from algorunner.abstract.base_strategy import (
AccountState, TransactionRequest, AuthorisationDecision
)


def start(self, control):
self.series = pd.DataFrame()
self.control = control
class Example(BaseStrategy):
def __init__(self):
self.series = pd.DataFrame
super().__init__()

def process(self, kline):
self.series = self.series.append(kline)
def process(self, tick: pd.DataFrame):
"""process accepts a DataFrame containing the latest tick data."""
self.series = self.series.append(tick)

if self.series.shape[0] > 5:
print("Average price change over past 5 windows: ", pd.to_numeric(self.series[-5:]["PriceChange"]).mean())
recent_window = pd.to_numeric(self.series[-5:]["PriceChange"])
print("Average price change over past 5 windows: ", recent_window.mean())

def authorise(self, state: AccountState, trx: TransactionRequest) -> AuthorisationDecision:
"""authorise is used to perform any risk calculations and position sizing."""
pass
```

When executed via the runner, this will calculate the average price change over the past 5 2000ms updates, and display it to the user.
From the `BaseStrategy` class you can interact with the market via calling `self.open_position(symbol: str)` and `self.close_position(symbol: str)` - this will subsequently be passed through to the `authorise(...)` call which will determine whether that interaction is allowed, whether it fits in with the users defined approach to risk, and what size the that position should be. Under the hood this is all handles via events.

For information on the classes used - i.e. `AuthorisationDecision`, `TransactionRequest`, and `AccountState` - please see the [API documentation](https://fergusinlondon.github.io/Runner/).

## Required Configuration

Configuration can be done via: a `.ini` file, environment variables, or a combination of both.

```
python example.py
Average price change over past 5 windows: 26.694
Average price change over past 5 windows: 26.356
Average price change over past 5 windows: 26.444
Average price change over past 5 windows: 26.246000000000002
Average price change over past 5 windows: 26.272000000000002
Average price change over past 5 windows: 26.706
Average price change over past 5 windows: 27.142000000000003
Average price change over past 5 windows: 27.182
Average price change over past 5 windows: 27.562
Average price change over past 5 windows: 28.002
Average price change over past 5 windows: 28.246000000000002
Average price change over past 5 windows: 28.49
Average price change over past 5 windows: 28.754
[credentials]
exchange = binance # Identifier of the target exchange.
api_key = binanceAPIKey # API Key for the exchange
api_secret = binanceAPISecret # API Secret for the exchange

[strategy]
name = Example # Strategy to execute
symbol = BTCUSDT # Symbol to execute the strategy against
```

By default AlgoRunner will try and read a file named `bot.ini`, but this can be overridden by the `--config` flag:

```
$ python run.py --config [config .ini file]
```

Alternatively, configuration can also be done via: a `.ini` file, environment variables, or a combination of both.

| `.ini` variable | environment variable | CLI flag |
| ---------------------- | --------------------- | ---------------- |
| credentials.exchange | ALGORUNNER_EXCHANGE | --exchange |
| credentials.api_key | ALGORUNNER_API_KEY | --api-key |
| credentials.api_secret | ALGORUNNER_API_SECRET | --api-secret |
| strategy.name | - | -s / --strategy |
| strategy.symbol | - | --trading-symbol |

**It's advisable not to pass any exchange details - i.e. `credentials.*` variables - via either the configuration file or the CLI!**

## Executing AlgoRunner

There are to methods to run AlgoRunner: the recommended way is via Docker.

### Using Docker

To build a Docker Image simply run `make docker` from the root of this repository; this will create an image with the tags `algorunner:<commit-hash>` and `algorunner:latest`, before running the image. **There are no pre-built Docker Images available.**

```
$ make docker # that's genuinely it, I promise.
```

### Running Locally

To run the service locally it's also relatively trivial:

```
$ make deps # this will install all dependencies
$ make local # this will run the service in the environment provided by poetry
```

Note: you **must** have `poetry` - a python dependency manager - installed on your system to run AlgoRunner. If you don't then the `deps` target of the Makefile will *attempt* to install it on your behalf.

---
## Development

## August 2021 Update: AlgoRunner V2.0
For details on development please see `DEVELOPMENT.md`, and for details on the automated test suite please see `test/TESTING.md`.

A preview of this release is available in the [`develop`](https://github.com/FergusInLondon/Runner/tree/develop) branch; and progress tracking is available via the [Version 2 Project Board](https://github.com/FergusInLondon/Runner/projects/1).
## License

This version aims to introduce a new design to allow easier exchange API interactions, concurrent processing of market orders, user definable algorithms *and* risk calculations, and improved dependency management.
This software is licensed by the terms outlined in the [*The MIT License*](https://opensource.org/licenses/MIT). For the full and entire text of this license please see `LICENSE.txt`.
21 changes: 21 additions & 0 deletions algorunner/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
.. include:: pdoc.md
"""

from algorunner.strategy.base import BaseStrategy
from algorunner.adapters.base import Adapter
from algorunner.hooks import (
Hook, InvalidHookHandler, hook, clear_handlers
)
from algorunner.monitoring import Timer

__docformat__ = "restructuredtext"
__all__ = [
'BaseStrategy',
'Adapter',
'Hook',
'InvalidHookHandler',
'hook',
'clear_handlers',
'Timer',
]
11 changes: 11 additions & 0 deletions algorunner/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""

Verification of the functionality of an Adapter should be done in a similar fashion to the
Gherkin specification for the binance adapter.
"""

from algorunner.adapters.messages import * # noqa: F401, F403
from algorunner.adapters.base import ( # noqa: F401
Adapter,
factory,
)
50 changes: 50 additions & 0 deletions algorunner/adapters/_backtest.py
Original file line number Diff line number Diff line change
@@ -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()
Loading