def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair): def _trend_alternate_hold(dataframe=None, metadata=None): """ Buy every xth candle - sell every other xth -2 (hold on to pairs a bit) """ if metadata['pair'] in ('ETH/BTC', 'LTC/BTC'): multi = 20 else: multi = 18 dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0) dataframe['sell'] = np.where( (dataframe.index + multi - 2) % multi == 0, 1, 0) return dataframe mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] data = history.load_data(datadir=None, ticker_interval='5m', pairs=pairs) # Only use 500 lines to increase performance data = trim_dictlist(data, -500) # Remove data for one pair from the beginning of the data data[pair] = data[pair][tres:] # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} default_conf['ticker_interval'] = '5m' backtesting = Backtesting(default_conf) backtesting.advise_buy = _trend_alternate_hold # Override backtesting.advise_sell = _trend_alternate_hold # Override data_processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timeframe(data_processed) backtest_conf = { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 3, 'position_stacking': False, 'start_date': min_date, 'end_date': max_date, } results = backtesting.backtest(backtest_conf) # Make sure we have parallel trades assert len(evaluate_result_multi(results, '5min', 2)) > 0 # make sure we don't have trades with more than configured max_open_trades assert len(evaluate_result_multi(results, '5min', 3)) == 0 backtest_conf = { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 1, 'position_stacking': False, 'start_date': min_date, 'end_date': max_date, } results = backtesting.backtest(backtest_conf) assert len(evaluate_result_multi(results, '5min', 1)) == 0
def test_backtest(default_conf, fee, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' timerange = TimeRange(None, 'line', 0, -201) data = history.load_data(datadir=None, ticker_interval='5m', pairs=['UNITTEST/BTC'], timerange=timerange) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timeframe(data_processed) results = backtesting.backtest({ 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 10, 'position_stacking': False, 'start_date': min_date, 'end_date': max_date, }) assert not results.empty assert len(results) == 2 expected = pd.DataFrame({ 'pair': [pair, pair], 'profit_percent': [0.0, 0.0], 'profit_abs': [0.0, 0.0], 'open_time': pd.to_datetime([ Arrow(2018, 1, 29, 18, 40, 0).datetime, Arrow(2018, 1, 30, 3, 30, 0).datetime ], utc=True), 'close_time': pd.to_datetime([ Arrow(2018, 1, 29, 22, 35, 0).datetime, Arrow(2018, 1, 30, 4, 10, 0).datetime ], utc=True), 'open_index': [78, 184], 'close_index': [125, 192], 'trade_duration': [235, 40], 'open_at_end': [False, False], 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], 'sell_reason': [SellType.ROI, SellType.ROI] }) pd.testing.assert_frame_equal(results, expected) data_pair = data_processed[pair] for _, t in results.iterrows(): ln = data_pair.loc[data_pair["date"] == t["open_time"]] # Check open trade rate alignes to open rate assert ln is not None assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6) # check close trade rate alignes to close rate or is between high and low ln = data_pair.loc[data_pair["date"] == t["close_time"]] assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or round(ln.iloc[0]["low"], 6) < round( t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
def generate_optimizer(self, _params: Dict) -> Dict: params = self.get_args(_params) if self.has_space('roi'): self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table( params) if self.has_space('buy'): self.advise_buy = self.custom_hyperopt.buy_strategy_generator( params) elif hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore if self.has_space('sell'): self.advise_sell = self.custom_hyperopt.sell_strategy_generator( params) elif hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore if self.has_space('stoploss'): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) min_date, max_date = get_timeframe(processed) self.min_date = min_date self.max_date = max_date results = self.backtest({ 'stake_amount': self.config['stake_amount'], 'processed': processed, 'position_stacking': self.config.get('position_stacking', True), 'start_date': min_date, 'end_date': max_date, }) result_explanation = self.format_results(results) #total_profit = results.profit_percent.sum() total_profit = results.profit_percent trade_count = len(results.index) trade_duration = results.trade_duration.mean() if trade_count == 0: return { 'loss': MAX_LOSS, 'params': params, 'result': result_explanation, } loss = self.calculate_loss(total_profit, trade_count) return { 'loss': loss, 'params': params, 'result': result_explanation, }
def test_get_timeframe(default_conf, mocker) -> None: patch_exchange(mocker) strategy = DefaultStrategy(default_conf) data = strategy.tickerdata_to_dataframe( history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC'])) min_date, max_date = optimize.get_timeframe(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): data = history.load_data(datadir=None, ticker_interval='1m', pairs=[pair]) data = trim_dictlist(data, -201) patch_exchange(mocker) backtesting = Backtesting(conf) processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timeframe(processed) return { 'stake_amount': conf['stake_amount'], 'processed': processed, 'max_open_trades': 10, 'position_stacking': False, 'record': record, 'start_date': min_date, 'end_date': max_date, }
def test_validate_backtest_data(default_conf, mocker, caplog) -> None: patch_exchange(mocker) strategy = DefaultStrategy(default_conf) timerange = TimeRange('index', 'index', 200, 250) data = strategy.tickerdata_to_dataframe( history.load_data(datadir=None, ticker_interval='5m', pairs=['UNITTEST/BTC'], timerange=timerange)) min_date, max_date = optimize.get_timeframe(data) caplog.clear() assert not optimize.validate_backtest_data( data, min_date, max_date, constants.TICKER_INTERVAL_MINUTES["5m"]) assert len(caplog.record_tuples) == 0
def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: patch_exchange(mocker) strategy = DefaultStrategy(default_conf) data = strategy.tickerdata_to_dataframe( history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC'])) min_date, max_date = optimize.get_timeframe(data) caplog.clear() assert optimize.validate_backtest_data( data, min_date, max_date, constants.TICKER_INTERVAL_MINUTES["1m"]) assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", caplog.record_tuples)
def simple_backtest(config, contour, num_results, mocker) -> None: patch_exchange(mocker) config['ticker_interval'] = '1m' backtesting = Backtesting(config) data = load_data_test(contour) processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timeframe(processed) assert isinstance(processed, dict) results = backtesting.backtest({ 'stake_amount': config['stake_amount'], 'processed': processed, 'max_open_trades': 1, 'position_stacking': False, 'start_date': min_date, 'end_date': max_date, }) # results :: <class 'pandas.core.frame.DataFrame'> assert len(results) == num_results
def test_ohlcv_fill_up_missing_data(caplog): data = load_pair_history(datadir=None, ticker_interval='1m', refresh_pairs=False, pair='UNITTEST/BTC', fill_up_missing=False) caplog.set_level(logging.DEBUG) data2 = ohlcv_fill_up_missing_data(data, '1m') assert len(data2) > len(data) # Column names should not change assert (data.columns == data2.columns).all() assert log_has( f"Missing data fillup: before: {len(data)} - after: {len(data2)}", caplog.record_tuples) # Test fillup actually fixes invalid backtest data min_date, max_date = get_timeframe({'UNITTEST/BTC': data}) assert validate_backtest_data({'UNITTEST/BTC': data}, min_date, max_date, 1) assert not validate_backtest_data({'UNITTEST/BTC': data2}, min_date, max_date, 1)
def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: """ run functional tests """ default_conf["stoploss"] = data.stop_loss default_conf["minimal_roi"] = {"0": data.roi} default_conf['ticker_interval'] = tests_ticker_interval mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0)) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) backtesting.advise_buy = lambda a, m: frame backtesting.advise_sell = lambda a, m: frame caplog.set_level(logging.DEBUG) pair = 'UNITTEST/BTC' # Dummy data as we mock the analyze functions data_processed = {pair: DataFrame()} min_date, max_date = get_timeframe({pair: frame}) results = backtesting.backtest({ 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 10, 'start_date': min_date, 'end_date': max_date, }) print(results.T) assert len(results) == len(data.trades) assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3) for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.sell_reason == trade.sell_reason assert res.open_time == _get_frame_time_from_offset(trade.open_tick) assert res.close_time == _get_frame_time_from_offset(trade.close_tick)
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) backtesting = Backtesting(default_conf) # Run a backtesting for an exiting 1min ticker_interval timerange = TimeRange(None, 'line', 0, -200) data = history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC'], timerange=timerange) processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timeframe(processed) results = backtesting.backtest({ 'stake_amount': default_conf['stake_amount'], 'processed': processed, 'max_open_trades': 1, 'position_stacking': False, 'start_date': min_date, 'end_date': max_date, }) assert not results.empty assert len(results) == 1
def test_backtest_multi_pair(default_conf, fee, mocker): def evaluate_result_multi(results, freq, max_open_trades): # Find overlapping trades by expanding each trade once per period # and then counting overlaps dates = [ pd.Series( pd.date_range(row[1].open_time, row[1].close_time, freq=freq)) for row in results[['open_time', 'close_time']].iterrows() ] deltas = [len(x) for x in dates] dates = pd.Series(pd.concat(dates).values, name='date') df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) df2 = df2.astype(dtype={ "open_time": "datetime64", "close_time": "datetime64" }) df2 = pd.concat([dates, df2], axis=1) df2 = df2.set_index('date') df_final = df2.resample(freq)[['pair']].count() return df_final[df_final['pair'] > max_open_trades] def _trend_alternate_hold(dataframe=None, metadata=None): """ Buy every 8th candle - sell every other 8th -2 (hold on to pairs a bit) """ multi = 8 dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0) dataframe['sell'] = np.where( (dataframe.index + multi - 2) % multi == 0, 1, 0) if metadata['pair'] in ('ETH/BTC', 'LTC/BTC'): dataframe['buy'] = dataframe['buy'].shift(-4) dataframe['sell'] = dataframe['sell'].shift(-4) return dataframe mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] data = history.load_data(datadir=None, ticker_interval='5m', pairs=pairs) data = trim_dictlist(data, -500) # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} default_conf['ticker_interval'] = '5m' backtesting = Backtesting(default_conf) backtesting.advise_buy = _trend_alternate_hold # Override backtesting.advise_sell = _trend_alternate_hold # Override data_processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timeframe(data_processed) backtest_conf = { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 3, 'position_stacking': False, 'start_date': min_date, 'end_date': max_date, } results = backtesting.backtest(backtest_conf) # Make sure we have parallel trades assert len(evaluate_result_multi(results, '5min', 2)) > 0 # make sure we don't have trades with more than configured max_open_trades assert len(evaluate_result_multi(results, '5min', 3)) == 0 backtest_conf = { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 1, 'position_stacking': False, 'start_date': min_date, 'end_date': max_date, } results = backtesting.backtest(backtest_conf) assert len(evaluate_result_multi(results, '5min', 1)) == 0
def start(self) -> None: """ Run a backtesting end-to-end :return: None """ data: Dict[str, Any] = {} pairs = self.config['exchange']['pair_whitelist'] logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') self.exchange.refresh_tickers(pairs, self.ticker_interval) data = self.exchange._klines else: logger.info( 'Using local backtesting data (using whitelist in given config) ...' ) timerange = Arguments.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) data = history.load_data(datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=pairs, ticker_interval=self.ticker_interval, refresh_pairs=self.config.get( 'refresh_pairs', False), exchange=self.exchange, timerange=timerange) if not data: logger.critical("No data found. Terminating.") return # Use max_open_trades in backtesting, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): max_open_trades = self.config['max_open_trades'] else: logger.info( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...' ) max_open_trades = 0 all_results = {} for strat in self.strategylist: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) min_date, max_date = optimize.get_timeframe(data) # Validate dataframe for missing values (mainly at start and end, as fillup is called) optimize.validate_backtest_data( data, min_date, max_date, constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]) logger.info('Measuring data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days) # need to reprocess data every time to populate signals preprocessed = self.strategy.tickerdata_to_dataframe(data) # Execute backtest and print results all_results[self.strategy.get_strategy_name()] = self.backtest({ 'stake_amount': self.config.get('stake_amount'), 'processed': preprocessed, 'max_open_trades': max_open_trades, 'position_stacking': self.config.get('position_stacking', False), 'start_date': min_date, 'end_date': max_date, }) for strategy, results in all_results.items(): if self.config.get('export', False): self._store_backtest_result( self.config['exportfilename'], results, strategy if len(self.strategylist) > 1 else None) print(f"Result for strategy {strategy}") print(' BACKTESTING REPORT '.center(119, '=')) print(self._generate_text_table(data, results)) print(' SELL REASON STATS '.center(119, '=')) print(self._generate_text_table_sell_reason(data, results)) print(' LEFT OPEN TRADES REPORT '.center(119, '=')) print( self._generate_text_table(data, results.loc[results.open_at_end], True)) print() if len(all_results) > 1: # Print Strategy summary table print(' Strategy Summary '.center(119, '=')) print(self._generate_text_table_strategy(all_results)) print('\nFor more details, please look at the detail tables above')