Merge pull request #131 from gcarq/feature/backtesting-max-open-trades
implement trade count lock for backtesting
This commit is contained in:
		| @@ -79,6 +79,7 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: | |||||||
|  |  | ||||||
|     return dataframe |     return dataframe | ||||||
|  |  | ||||||
|  |  | ||||||
| def populate_sell_trend(dataframe: DataFrame) -> DataFrame: | def populate_sell_trend(dataframe: DataFrame) -> DataFrame: | ||||||
|     """ |     """ | ||||||
|     Based on TA indicators, populates the sell signal for the given dataframe |     Based on TA indicators, populates the sell signal for the given dataframe | ||||||
|   | |||||||
| @@ -146,6 +146,12 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: | |||||||
|         type=int, |         type=int, | ||||||
|         metavar='INT', |         metavar='INT', | ||||||
|     ) |     ) | ||||||
|  |     backtest.add_argument( | ||||||
|  |         '--limit-max-trades', | ||||||
|  |         help='uses max_open_trades from config to simulate real world limitations', | ||||||
|  |         action='store_true', | ||||||
|  |         dest='limit_max_trades', | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def start_backtesting(args) -> None: | def start_backtesting(args) -> None: | ||||||
| @@ -161,6 +167,7 @@ def start_backtesting(args) -> None: | |||||||
|         'BACKTEST_LIVE': 'true' if args.live else '', |         'BACKTEST_LIVE': 'true' if args.live else '', | ||||||
|         'BACKTEST_CONFIG': args.config, |         'BACKTEST_CONFIG': args.config, | ||||||
|         'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval), |         'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval), | ||||||
|  |         'BACKTEST_LIMIT_MAX_TRADES': 'true' if args.limit_max_trades else '', | ||||||
|     }) |     }) | ||||||
|     path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py') |     path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py') | ||||||
|     pytest.main(['-s', path]) |     pytest.main(['-s', path]) | ||||||
|   | |||||||
| @@ -54,6 +54,7 @@ def default_conf(): | |||||||
| @pytest.fixture(scope="module") | @pytest.fixture(scope="module") | ||||||
| def backtest_conf(): | def backtest_conf(): | ||||||
|     return { |     return { | ||||||
|  |         "max_open_trades": 3, | ||||||
|         "stake_currency": "BTC", |         "stake_currency": "BTC", | ||||||
|         "stake_amount": 0.01, |         "stake_amount": 0.01, | ||||||
|         "minimal_roi": { |         "minimal_roi": { | ||||||
|   | |||||||
| @@ -83,24 +83,46 @@ def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currenc | |||||||
|     return tabulate(tabular_data, headers=headers) |     return tabulate(tabular_data, headers=headers) | ||||||
|  |  | ||||||
|  |  | ||||||
| def backtest(backtest_conf, processed, mocker): | def backtest(config: Dict, processed, mocker, max_open_trades=0): | ||||||
|  |     """ | ||||||
|  |     Implements backtesting functionality | ||||||
|  |     :param config: config to use | ||||||
|  |     :param processed: a processed dictionary with format {pair, data} | ||||||
|  |     :param mocker: mocker instance | ||||||
|  |     :param max_open_trades: maximum number of concurrent trades (default: 0, disabled) | ||||||
|  |     :return: DataFrame | ||||||
|  |     """ | ||||||
|     trades = [] |     trades = [] | ||||||
|  |     trade_count_lock = {} | ||||||
|     exchange._API = Bittrex({'key': '', 'secret': ''}) |     exchange._API = Bittrex({'key': '', 'secret': ''}) | ||||||
|     mocker.patch.dict('freqtrade.main._CONF', backtest_conf) |     mocker.patch.dict('freqtrade.main._CONF', config) | ||||||
|     for pair, pair_data in processed.items(): |     for pair, pair_data in processed.items(): | ||||||
|         pair_data['buy'] = 0 |         pair_data['buy'], pair_data['sell'] = 0, 0 | ||||||
|         pair_data['sell'] = 0 |  | ||||||
|         ticker = populate_sell_trend(populate_buy_trend(pair_data)) |         ticker = populate_sell_trend(populate_buy_trend(pair_data)) | ||||||
|         # for each buy point |         # for each buy point | ||||||
|         for row in ticker[ticker.buy == 1].itertuples(index=True): |         for row in ticker[ticker.buy == 1].itertuples(index=True): | ||||||
|  |             if max_open_trades > 0: | ||||||
|  |                 # Check if max_open_trades has already been reached for the given date | ||||||
|  |                 if not trade_count_lock.get(row.date, 0) < max_open_trades: | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |             if max_open_trades > 0: | ||||||
|  |                 # Increase lock | ||||||
|  |                 trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 | ||||||
|  |  | ||||||
|             trade = Trade( |             trade = Trade( | ||||||
|                 open_rate=row.close, |                 open_rate=row.close, | ||||||
|                 open_date=row.date, |                 open_date=row.date, | ||||||
|                 amount=backtest_conf['stake_amount'], |                 amount=config['stake_amount'], | ||||||
|                 fee=exchange.get_fee() * 2 |                 fee=exchange.get_fee() * 2 | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             # calculate win/lose forwards from buy point |             # calculate win/lose forwards from buy point | ||||||
|             for row2 in ticker[row.Index:].itertuples(index=True): |             for row2 in ticker[row.Index + 1:].itertuples(index=True): | ||||||
|  |                 if max_open_trades > 0: | ||||||
|  |                     # Increase trade_count_lock for every iteration | ||||||
|  |                     trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 | ||||||
|  |  | ||||||
|                 if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1: |                 if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1: | ||||||
|                     current_profit = trade.calc_profit(row2.close) |                     current_profit = trade.calc_profit(row2.close) | ||||||
|  |  | ||||||
| @@ -110,6 +132,13 @@ def backtest(backtest_conf, processed, mocker): | |||||||
|     return DataFrame.from_records(trades, columns=labels) |     return DataFrame.from_records(trades, columns=labels) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_max_open_trades(config): | ||||||
|  |     if not os.environ.get('BACKTEST_LIMIT_MAX_TRADES'): | ||||||
|  |         return 0 | ||||||
|  |     print('Using max_open_trades: {} ...'.format(config['max_open_trades'])) | ||||||
|  |     return config['max_open_trades'] | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set") | @pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set") | ||||||
| def test_backtest(backtest_conf, mocker): | def test_backtest(backtest_conf, mocker): | ||||||
|     print('') |     print('') | ||||||
| @@ -147,8 +176,6 @@ def test_backtest(backtest_conf, mocker): | |||||||
|     )) |     )) | ||||||
|  |  | ||||||
|     # Execute backtest and print results |     # Execute backtest and print results | ||||||
|     results = backtest(config, preprocess(data), mocker) |     results = backtest(config, preprocess(data), mocker, get_max_open_trades(config)) | ||||||
|     print('====================== BACKTESTING REPORT ======================================\n\n' |     print('====================== BACKTESTING REPORT ======================================\n\n') | ||||||
|           'NOTE: This Report doesn\'t respect the limits of max_open_trades, \n' |  | ||||||
|           '      so the projected values should be taken with a grain of salt.\n') |  | ||||||
|     print(generate_text_table(data, results, config['stake_currency'])) |     print(generate_text_table(data, results, config['stake_currency'])) | ||||||
|   | |||||||
| @@ -109,6 +109,7 @@ def test_start_backtesting(mocker): | |||||||
|         live=True, |         live=True, | ||||||
|         loglevel=20, |         loglevel=20, | ||||||
|         ticker_interval=1, |         ticker_interval=1, | ||||||
|  |         limit_max_trades=True, | ||||||
|     ) |     ) | ||||||
|     start_backtesting(args) |     start_backtesting(args) | ||||||
|     assert env_mock == { |     assert env_mock == { | ||||||
| @@ -116,6 +117,7 @@ def test_start_backtesting(mocker): | |||||||
|         'BACKTEST_LIVE': 'true', |         'BACKTEST_LIVE': 'true', | ||||||
|         'BACKTEST_CONFIG': 'config.json', |         'BACKTEST_CONFIG': 'config.json', | ||||||
|         'BACKTEST_TICKER_INTERVAL': '1', |         'BACKTEST_TICKER_INTERVAL': '1', | ||||||
|  |         'BACKTEST_LIMIT_MAX_TRADES': 'true', | ||||||
|     } |     } | ||||||
|     assert pytest_mock.call_count == 1 |     assert pytest_mock.call_count == 1 | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user