Skip to content

Commit

Permalink
Add system to run backtesting
Browse files Browse the repository at this point in the history
  • Loading branch information
j-waters authored and edeng23 committed Apr 2, 2021
1 parent e619c9d commit 395b871
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 21 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ If you are interested in running a Telegram bot, more information can be found a

### Run

`python -m binance_trade_bot`
```shell
python -m binance_trade_bot
```

### Docker

Expand All @@ -108,6 +110,16 @@ If you only want to start the SQLite browser
docker-compose up -d sqlitebrowser
```

## Backtesting

You can test the bot on historic data to see how it performs.

```shell
python backtest.py
```

Feel free to modify that file to test and compare different settings and time periods

## Developing

To make sure your code is properly formatted before making a pull request,
Expand Down
7 changes: 7 additions & 0 deletions backtest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from datetime import datetime

from binance_trade_bot import backtest

if __name__ == "__main__":
resulting_balances = backtest(datetime(2021, 1, 1), datetime.now())
print(resulting_balances)
3 changes: 3 additions & 0 deletions binance_trade_bot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .backtest import backtest, download_market_data
from .binance_api_manager import BinanceAPIManager
from .crypto_trading import main as run_trader
90 changes: 70 additions & 20 deletions binance_trade_bot/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
from datetime import datetime, timedelta
from multiprocessing import Process, Value
from random import randint
from typing import Dict

import requests.exceptions
from binance.exceptions import BinanceAPIException
from diskcache import Cache

Expand All @@ -25,21 +27,31 @@ def get_price(self, ticker_symbol):


class MockBinanceManager(BinanceAPIManager):
def __init__(self, config: Config, db: Database, logger: Logger):
def __init__(
self,
config: Config,
db: Database,
logger: Logger,
start_date: datetime = None,
start_balances: Dict[str, float] = None,
):
super().__init__(config, db, logger)
self.config = config
self.datetime = datetime(2021, 1, 1)
self.balances = {config.BRIDGE.symbol: 100}
self.datetime = start_date or datetime(2021, 1, 1)
self.balances = start_balances or {config.BRIDGE.symbol: 100}

def increment(self):
self.datetime += timedelta(minutes=1)
def increment(self, interval=1):
self.datetime += timedelta(minutes=interval)

def get_all_market_tickers(self):
"""
Get ticker price of all coins
"""
return FakeAllTickers(self)

def get_fee(self, origin_coin: Coin, target_coin: Coin, selling: bool):
return 0.0075

def get_market_ticker_price(self, ticker_symbol: str):
"""
Get ticker price of a specific coin
Expand All @@ -48,8 +60,15 @@ def get_market_ticker_price(self, ticker_symbol: str):
key = f"{ticker_symbol}_{dt}"
val = cache.get(key, None)
if val is None:
val = float(self.binance_client.get_historical_klines(ticker_symbol, "1m", dt, dt)[0][1])
cache.set(key, val)
try:
val = float(self.binance_client.get_historical_klines(ticker_symbol, "1m", dt, dt)[0][1])
cache.set(key, val)
except requests.exceptions.ConnectionError:
time.sleep(randint(5, 10))
return self.get_market_ticker_price(ticker_symbol)
except IndexError:
return None

return val

def get_currency_balance(self, currency_symbol: str):
Expand Down Expand Up @@ -89,36 +108,67 @@ def sell_alt(self, origin_coin: Coin, target_coin: Coin):
return {"price": from_coin_price}


def backtest():
config = Config()
def backtest(
start_date: datetime = None,
end_date: datetime = None,
interval=1,
start_balances: Dict[str, float] = None,
starting_coin: str = None,
config: Config = None,
) -> Dict[str, float]:
"""
:param config: Configuration object to use
:param start_date: Date to backtest from
:param end_date: Date to backtest up to
:param interval: Number of virtual minutes between each scout
:param start_balances: A dictionary of initial coin values. Default: {BRIDGE: 100}
:param starting_coin: The coin to start on. Default: first coin in coin list
:return: The final coin balances
"""
config = config or Config()
logger = Logger()

end_date = end_date or datetime.today()

db = Database(logger, config, "sqlite://")
db.create_database()
db.set_coins(config.SUPPORTED_COIN_LIST)

manager = MockBinanceManager(config, db, logger)
manager = MockBinanceManager(config, db, logger, start_date, start_balances)

starting_coin = db.get_coin(config.SUPPORTED_COIN_LIST[0])
manager.buy_alt(starting_coin, config.BRIDGE, manager.get_all_market_tickers())
starting_coin = db.get_coin(starting_coin or config.SUPPORTED_COIN_LIST[0])
if manager.get_currency_balance(starting_coin.symbol) == 0:
manager.buy_alt(starting_coin, config.BRIDGE, manager.get_all_market_tickers())
db.set_current_coin(starting_coin)

trader = AutoTrader(manager, db, logger, config)
trader.initialize_trade_thresholds()

while True:
print(manager.datetime)
trader.scout()
manager.increment()
try:
while manager.datetime < end_date:
print(manager.datetime)
trader.scout()
manager.increment(interval)
except KeyboardInterrupt:
pass
return manager.balances


def download_market_data(start_date: datetime = None, end_date: datetime = None, interval=1):
"""
:param start_date: Date to backtest from
:param end_date: Date to backtest up to
:param interval: Number of virtual minutes between each scout
"""

def download_market_data():
def _thread(symbol, counter: Value):
manager = MockBinanceManager(config, None, None)
while True:
manager = MockBinanceManager(config, None, None, start_date)
while manager.datetime < end_date:
try:
manager.get_market_ticker_price(symbol)
manager.increment()
manager.increment(interval)
counter.value += 1
except BinanceAPIException:
time.sleep(randint(10, 30))
Expand Down

0 comments on commit 395b871

Please sign in to comment.