diff --git a/README.md b/README.md index 05a25a322..925f22588 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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, diff --git a/backtest.py b/backtest.py new file mode 100644 index 000000000..ee9fcad03 --- /dev/null +++ b/backtest.py @@ -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) diff --git a/binance_trade_bot/__init__.py b/binance_trade_bot/__init__.py index e69de29bb..86854d146 100644 --- a/binance_trade_bot/__init__.py +++ b/binance_trade_bot/__init__.py @@ -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 diff --git a/binance_trade_bot/backtest.py b/binance_trade_bot/backtest.py index 7992a0666..819b23e80 100644 --- a/binance_trade_bot/backtest.py +++ b/binance_trade_bot/backtest.py @@ -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 @@ -25,14 +27,21 @@ 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): """ @@ -40,6 +49,9 @@ def get_all_market_tickers(self): """ 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 @@ -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): @@ -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))