From f0a154692de57912c1d5d9596133c17808f152f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jan 2021 06:37:42 +0100 Subject: [PATCH 01/44] Wallets should use trade_proxy --- freqtrade/wallets.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index d7dcfd487..078bcd07e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -11,6 +11,7 @@ from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import Trade +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -26,13 +27,14 @@ class Wallet(NamedTuple): class Wallets: - def __init__(self, config: dict, exchange: Exchange) -> None: + def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: self._config = config self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] self._last_wallet_refresh = 0 - self.update() + if not skip_update: + self.update() def get_free(self, currency: str) -> float: balance = self._wallets.get(currency) @@ -64,8 +66,8 @@ class Wallets: """ # Recreate _wallets to reset closed trade balances _wallets = {} - closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() - open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() + closed_trades = Trade.get_trades_proxy(is_open=False) + open_trades = Trade.get_trades_proxy(is_open=True) tot_profit = sum([trade.calc_profit() for trade in closed_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades]) @@ -102,7 +104,7 @@ class Wallets: if currency not in balances: del self._wallets[currency] - def update(self, require_update: bool = True) -> None: + def update(self, require_update: bool = True, log: bool = True) -> None: """ Updates wallets from the configured version. By default, updates from the exchange. @@ -111,11 +113,12 @@ class Wallets: :param require_update: Allow skipping an update if balances were recently refreshed """ if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)): - if self._config['dry_run']: - self._update_dry() - else: + if (not self._config['dry_run'] or self._config.get('runmode') == RunMode.LIVE): self._update_live() - logger.info('Wallets synced.') + else: + self._update_dry() + if log: + logger.info('Wallets synced.') self._last_wallet_refresh = arrow.utcnow().int_timestamp def get_all_balances(self) -> Dict[str, Any]: From 9361aa1c95d5a408a4015e4ae5b04d29fd129b58 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jan 2021 07:06:58 +0100 Subject: [PATCH 02/44] Add wallets to backtesting --- freqtrade/optimize/backtesting.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3186313e1..b68732d5c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -28,6 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) @@ -114,6 +115,8 @@ class Backtesting: if self.config.get('enable_protections', False): self.protections = ProtectionManager(self.config) + self.wallets = Wallets(self.config, self.exchange) + # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) # Load one (first) strategy @@ -176,6 +179,10 @@ class Backtesting: PairLocks.reset_locks() Trade.reset_trades() + def update_wallets(self): + if self.wallets: + self.wallets.update(log=False) + def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -276,8 +283,10 @@ class Backtesting: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = SellType.FORCE_SELL trade.close(sell_row[OPEN_IDX], show_msg=False) - trade.is_open = True - trades.append(trade) + # Deepcopy object to have wallets update correctly + trade1 = deepcopy(trade) + trade1.is_open = True + trades.append(trade1) return trades def backtest(self, processed: Dict, stake_amount: float, @@ -346,6 +355,7 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): + self.update_wallets() # Enter trade trade = Trade( pair=pair, @@ -372,6 +382,7 @@ class Backtesting: trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: + self.update_wallets() # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) @@ -384,6 +395,7 @@ class Backtesting: tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) + self.update_wallets() return trade_list_to_dataframe(trades) @@ -425,6 +437,7 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) + print(self.wallets.get_all_balances()) self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, From 4ce4eadc2366f6f58ab0811bc2fc01e2018627e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jan 2021 07:15:04 +0100 Subject: [PATCH 03/44] remove only ccxt objects when hyperopting --- freqtrade/optimize/hyperopt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index eee0f13b3..9cc5f2059 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -661,7 +661,9 @@ class Hyperopt: dump(preprocessed, self.data_pickle_file) # We don't need exchange instance anymore while running hyperopt - self.backtesting.exchange = None # type: ignore + self.backtesting.exchange._api = None # type: ignore + self.backtesting.exchange._api_async = None # type: ignore + # self.backtesting.exchange = None # type: ignore self.backtesting.pairlists = None # type: ignore self.backtesting.strategy.dp = None # type: ignore IStrategy.dp = None # type: ignore From b5177eadabe2a349382d7ee537aaae423942435f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Feb 2021 10:22:59 +0100 Subject: [PATCH 04/44] Extract close method for exchange --- freqtrade/exchange/exchange.py | 3 +++ freqtrade/optimize/hyperopt.py | 1 + 2 files changed, 4 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 617cd6c26..0e9a90548 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -147,6 +147,9 @@ class Exchange: """ Destructor - clean up async stuff """ + self.close() + + def close(self): logger.debug("Exchange object destroyed, closing async loop") if self._api_async and inspect.iscoroutinefunction(self._api_async.close): asyncio.get_event_loop().run_until_complete(self._api_async.close()) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9cc5f2059..155f1e69b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -661,6 +661,7 @@ class Hyperopt: dump(preprocessed, self.data_pickle_file) # We don't need exchange instance anymore while running hyperopt + self.backtesting.exchange.close() self.backtesting.exchange._api = None # type: ignore self.backtesting.exchange._api_async = None # type: ignore # self.backtesting.exchange = None # type: ignore From 712d503e6ca51acde5a676f833f073018d465cab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Feb 2021 10:30:50 +0100 Subject: [PATCH 05/44] Use sell-reason value in backtesting, not the enum object --- freqtrade/optimize/backtesting.py | 9 +++++---- freqtrade/optimize/optimize_reports.py | 2 +- freqtrade/persistence/models.py | 12 ++++++++++-- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_backtesting.py | 2 +- tests/optimize/test_optimize_reports.py | 2 +- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b68732d5c..718fd2c42 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -259,11 +259,11 @@ class Backtesting: sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: - trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60) - closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_type + trade.sell_reason = sell.sell_type.value + trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) trade.close(closerate, show_msg=False) return trade @@ -281,7 +281,7 @@ class Backtesting: sell_row = data[pair][-1] trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = SellType.FORCE_SELL + trade.sell_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) # Deepcopy object to have wallets update correctly trade1 = deepcopy(trade) @@ -366,6 +366,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, + exchange='backtesting', ) # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 88b2028ba..6338b1d71 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -132,7 +132,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List tabular_data.append( { - 'sell_reason': reason.value, + 'sell_reason': reason, 'trades': count, 'wins': len(result[result['profit_abs'] > 0]), 'draws': len(result[result['profit_abs'] == 0]), diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dff59819c..a05aa2c96 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -268,6 +268,14 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') + @property + def open_date_utc(self): + return self.open_date.replace(tzinfo=timezone.utc) + + @property + def close_date_utc(self): + return self.close_date.replace(tzinfo=timezone.utc) + def to_json(self) -> Dict[str, Any]: return { 'trade_id': self.id, @@ -306,9 +314,9 @@ class Trade(_DECL_BASE): 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'close_profit_abs': self.close_profit_abs, # Deprecated - 'trade_duration_s': (int((self.close_date - self.open_date).total_seconds()) + 'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds()) if self.close_date else None), - 'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60) + 'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60) if self.close_date else None), 'profit_ratio': self.close_profit, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index daf7c2053..c9499cc42 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -514,6 +514,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] - assert res.sell_reason == trade.sell_reason + assert res.sell_reason == trade.sell_reason.value assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index c8d4338af..db14749c3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -486,7 +486,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'trade_duration': [235, 40], 'profit_ratio': [0.0, 0.0], 'profit_abs': [0.0, 0.0], - 'sell_reason': [SellType.ROI, SellType.ROI], + 'sell_reason': [SellType.ROI.value, SellType.ROI.value], 'initial_stop_loss_abs': [0.0940005, 0.09272236], 'initial_stop_loss_ratio': [-0.1, -0.1], 'stop_loss_abs': [0.0940005, 0.09272236], diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 51a78c7cc..8b64c2764 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -265,7 +265,7 @@ def test_generate_sell_reason_stats(): 'wins': [2, 0, 0], 'draws': [0, 0, 0], 'losses': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + 'sell_reason': [SellType.ROI.value, SellType.ROI.value, SellType.STOP_LOSS.value] } ) From e32b2097f0010127f0bbb095a3968ccc5c71f6c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Feb 2021 10:20:43 +0100 Subject: [PATCH 06/44] Use timestamp in UTC timezone for ROI comparisons --- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2f64f3dac..fd2f8bdd0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -932,7 +932,7 @@ class FreqtradeBot(LoggingMixin): Check and execute sell """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.utcnow(), buy, sell, + trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8a0b27e96..6d40e56cc 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -649,7 +649,7 @@ class IStrategy(ABC): :return: True if bot should sell at current rate """ # Check if time matches and current rate is above threshold - trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) + trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60) _, roi = self.min_roi_reached_entry(trade_dur) if roi is None: return False From 081b9be45c072d4d39f5672122d3c5dbbbf5aa07 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Feb 2021 10:49:47 +0100 Subject: [PATCH 07/44] use get_all_locks to get locks for backtest result --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 718fd2c42..f37107767 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -442,7 +442,7 @@ class Backtesting: self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, - 'locks': PairLocks.locks, + 'locks': PairLocks.get_all_locks(), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), } diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 8644146d8..f0048bb52 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -123,3 +123,11 @@ class PairLocks(): now = datetime.now(timezone.utc) return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now) + + @staticmethod + def get_all_locks() -> List[PairLock]: + + if PairLocks.use_db: + return PairLock.query.all() + else: + return PairLocks.locks From 20455de2a9533ebbf3990b9efe241a1bec1543e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Feb 2021 20:22:33 +0100 Subject: [PATCH 08/44] Small enhancements to docs --- docs/strategy-customization.md | 2 +- freqtrade/persistence/migrations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index fdc95a3c1..fd733c88e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -709,7 +709,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished. !!! Warning - Locking pairs is not available during backtesting. + Manually locking pairs is not available during backtesting, only locks via Protections are allowed. #### Pair locking example diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index ed976c2a9..961363b0e 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -141,7 +141,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: inspector = inspect(engine) cols = inspector.get_columns('trades') - if 'orders' not in previous_tables: + if 'orders' not in previous_tables and 'trades' in previous_tables: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) else: From 0faa6f84dcd6ee8d5990fd8f2bbe0c7fed80dac9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:23:11 +0100 Subject: [PATCH 09/44] Improve Wallet logging disabling for backtesting --- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/wallets.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f37107767..7f2ba60f2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -116,6 +116,7 @@ class Backtesting: self.protections = ProtectionManager(self.config) self.wallets = Wallets(self.config, self.exchange) + self.wallets._log = False # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -181,7 +182,7 @@ class Backtesting: def update_wallets(self): if self.wallets: - self.wallets.update(log=False) + self.wallets.update() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 078bcd07e..9562f34e6 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -29,6 +29,7 @@ class Wallets: def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: self._config = config + self._log = True self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] @@ -104,7 +105,7 @@ class Wallets: if currency not in balances: del self._wallets[currency] - def update(self, require_update: bool = True, log: bool = True) -> None: + def update(self, require_update: bool = True) -> None: """ Updates wallets from the configured version. By default, updates from the exchange. @@ -117,7 +118,7 @@ class Wallets: self._update_live() else: self._update_dry() - if log: + if self._log: logger.info('Wallets synced.') self._last_wallet_refresh = arrow.utcnow().int_timestamp From 0754a7a78f36d379d9332de0bfef3124780debe0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:33:39 +0100 Subject: [PATCH 10/44] total_open_trades_stake should support no-db mode --- freqtrade/persistence/models.py | 10 +++++++--- tests/conftest.py | 20 +++++++++++++------- tests/conftest_trades.py | 4 ++++ tests/test_persistence.py | 8 ++++++-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a05aa2c96..f72705c34 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -671,9 +671,13 @@ class Trade(_DECL_BASE): Calculates total invested amount in open trades in stake currency """ - total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ - .filter(Trade.is_open.is_(True))\ - .scalar() + if Trade.use_db: + total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ + .filter(Trade.is_open.is_(True))\ + .scalar() + else: + total_open_stake_amount = sum( + t.stake_amount for t in Trade.get_trades_proxy(is_open=True)) return total_open_stake_amount or 0 @staticmethod diff --git a/tests/conftest.py b/tests/conftest.py index 61899dd53..946ae1fb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -183,28 +183,34 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -def create_mock_trades(fee): +def create_mock_trades(fee, use_db: bool = True): """ Create some fake trades ... """ + def add_trade(trade): + if use_db: + Trade.session.add(trade) + else: + Trade.trades.append(trade) + # Simulate dry_run entries trade = mock_trade_1(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_2(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_3(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_4(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_5(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_6(fee) - Trade.session.add(trade) + add_trade(trade) @pytest.fixture(autouse=True) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index fa9910b8d..6a42d04e3 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -28,6 +28,7 @@ def mock_trade_1(fee): amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', open_order_id='dry_run_buy_12345', @@ -180,6 +181,7 @@ def mock_trade_4(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', open_order_id='prod_buy_12345', @@ -230,6 +232,7 @@ def mock_trade_5(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', strategy='SampleStrategy', @@ -281,6 +284,7 @@ def mock_trade_6(fee): amount_requested=2.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.15, exchange='bittrex', strategy='SampleStrategy', diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d0d29f142..1fced3e16 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1039,14 +1039,18 @@ def test_fee_updated(fee): @pytest.mark.usefixtures("init_persistence") -def test_total_open_trades_stakes(fee): +@pytest.mark.parametrize('use_db', [True, False]) +def test_total_open_trades_stakes(fee, use_db): + Trade.use_db = use_db res = Trade.total_open_trades_stakes() assert res == 0 - create_mock_trades(fee) + create_mock_trades(fee, use_db) res = Trade.total_open_trades_stakes() assert res == 0.004 + Trade.use_db = True + @pytest.mark.usefixtures("init_persistence") def test_get_overall_performance(fee): From 959ff990460acd0e137c0c2aaccbb6cfc1efd932 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:45:59 +0100 Subject: [PATCH 11/44] Add Dry-run wallet CLI option --- docs/backtesting.md | 4 ++++ docs/bot-usage.md | 4 ++++ docs/configuration.md | 2 +- docs/hyperopt.md | 6 +++++- freqtrade/commands/arguments.py | 6 +++--- freqtrade/commands/cli_options.py | 5 +++++ freqtrade/configuration/configuration.py | 4 +++- freqtrade/wallets.py | 1 + 8 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index a14c8f2e4..38d1af45a 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -16,6 +16,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--eps] [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -48,6 +49,9 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to backtest. Please note that ticker-interval needs to be diff --git a/docs/bot-usage.md b/docs/bot-usage.md index c7fe8634d..4ff6168a0 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -56,6 +56,7 @@ optional arguments: usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [--db-url PATH] [--sd-notify] [--dry-run] + [--dry-run-wallet DRY_RUN_WALLET] optional arguments: -h, --help show this help message and exit @@ -66,6 +67,9 @@ optional arguments: --sd-notify Notify systemd service manager. --dry-run Enforce dry-run for trading (removes Exchange secrets and simulates trades). + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/configuration.md b/docs/configuration.md index 0163e1671..663d9c5b2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -49,7 +49,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
**Datatype:** String | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
**Datatype:** Boolean -| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float +| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float | `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions.
*Defaults to `false`.*
**Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ec155062f..ee3d75d0b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -43,7 +43,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] [-e INT] + [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] [--random-state INT] [--min-trades INT] @@ -82,6 +83,9 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. -e INT, --epochs INT Specify number of epochs (default: 100). --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] Specify which parameters to hyperopt. Space-separated diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index c64c11a18..88cec7b3e 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -14,18 +14,18 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] +ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", ] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", - "enable_protections", + "enable_protections", "dry_run_wallet", "strategy_list", "export", "exportfilename"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "position_stacking", "use_max_market_positions", - "enable_protections", + "enable_protections", "dry_run_wallet", "epochs", "spaces", "print_all", "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 7dc85377d..90ebb5e6a 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -110,6 +110,11 @@ AVAILABLE_CLI_OPTIONS = { help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).', action='store_true', ), + "dry_run_wallet": Arg( + '--dry-run-wallet', + help='Starting balance, used for backtesting / hyperopt and dry-runs.', + type=float, + ), # Optimize common "timeframe": Arg( '-i', '--timeframe', '--ticker-interval', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 7bf3e6bf2..6295d01d4 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -232,7 +232,9 @@ class Configuration: self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') - + self._args_to_config(config, argname='dry_run_wallet', + logstring='Parameter --dry-run-wallet detected, ' + 'overriding dry_run_wallet to: {} ...') self._args_to_config(config, argname='fee', logstring='Parameter --fee detected, ' 'setting fee to: {} ...') diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 9562f34e6..f5ce4c102 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -158,6 +158,7 @@ class Wallets: Check if stake amount can be fulfilled with the available balance for the stake currency :return: float: Stake amount + :raise: DependencyException if balance is lower than stake-amount """ available_amount = self._get_available_stake_amount() From e4abe902fc924b30c41ddc67edb744eb2095951e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 20:37:55 +0100 Subject: [PATCH 12/44] Enable compounding for backtesting --- freqtrade/optimize/backtesting.py | 66 ++++++++++++++------------ freqtrade/optimize/hyperopt.py | 1 - tests/optimize/test_backtest_detail.py | 1 - tests/optimize/test_backtesting.py | 6 --- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7f2ba60f2..7ed5064e7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, @@ -270,6 +270,30 @@ class Backtesting: return None + def _enter_trade(self, pair: str, row, max_open_trades: int, + open_trade_count: int) -> Optional[Trade]: + self.update_wallets() + try: + stake_amount = self.wallets.get_trade_stake_amount( + pair, max_open_trades - open_trade_count, None) + except DependencyException: + stake_amount = 0 + if stake_amount: + # Enter trade + trade = Trade( + pair=pair, + open_rate=row[OPEN_IDX], + open_date=row[DATE_IDX], + stake_amount=stake_amount, + amount=round(stake_amount / row[OPEN_IDX], 8), + fee_open=self.fee, + fee_close=self.fee, + is_open=True, + exchange='backtesting', + ) + return trade + return None + def handle_left_open(self, open_trades: Dict[str, List[Trade]], data: Dict[str, List[Tuple]]) -> List[Trade]: """ @@ -290,7 +314,7 @@ class Backtesting: trades.append(trade1) return trades - def backtest(self, processed: Dict, stake_amount: float, + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, enable_protections: bool = False) -> DataFrame: @@ -302,7 +326,6 @@ class Backtesting: Avoid extensive logging in this method and functions it calls. :param processed: a processed dictionary with format {pair, data} - :param stake_amount: amount to use for each trade :param start_date: backtesting timerange start datetime :param end_date: backtesting timerange end datetime :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited @@ -310,10 +333,6 @@ class Backtesting: :param enable_protections: Should protections be enabled? :return: DataFrame with trades (results of backtesting) """ - logger.debug(f"Run backtest, stake_amount: {stake_amount}, " - f"start_date: {start_date}, end_date: {end_date}, " - f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" - ) trades: List[Trade] = [] self.prepare_backtest(enable_protections) @@ -356,30 +375,18 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): - self.update_wallets() - # Enter trade - trade = Trade( - pair=pair, - open_rate=row[OPEN_IDX], - open_date=row[DATE_IDX], - stake_amount=stake_amount, - amount=round(stake_amount / row[OPEN_IDX], 8), - fee_open=self.fee, - fee_close=self.fee, - is_open=True, - exchange='backtesting', - ) - # TODO: hacky workaround to avoid opening > max_open_trades - # This emulates previous behaviour - not sure if this is correct - # Prevents buying if the trade-slot was freed in this candle - open_trade_count_start += 1 - open_trade_count += 1 - # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") - open_trades[pair].append(trade) - Trade.trades.append(trade) + trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start) + if trade: + # TODO: hacky workaround to avoid opening > max_open_trades + # This emulates previous behaviour - not sure if this is correct + # Prevents buying if the trade-slot was freed in this candle + open_trade_count_start += 1 + open_trade_count += 1 + # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") + open_trades[pair].append(trade) + Trade.trades.append(trade) for trade in open_trades[pair]: - # since indexes has been incremented before, we need to go one step back to # also check the buying candle for sell conditions. trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured @@ -431,7 +438,6 @@ class Backtesting: # Execute backtest and store results results = self.backtest( processed=preprocessed, - stake_amount=self.config['stake_amount'], start_date=min_date.datetime, end_date=max_date.datetime, max_open_trades=max_open_trades, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 155f1e69b..79ecb6052 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -537,7 +537,6 @@ class Hyperopt: backtesting_results = self.backtesting.backtest( processed=processed, - stake_amount=self.config['stake_amount'], start_date=min_date.datetime, end_date=max_date.datetime, max_open_trades=self.max_open_trades, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index c9499cc42..4d6605b9f 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -503,7 +503,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: min_date, max_date = get_timerange({pair: frame}) results = backtesting.backtest( processed=data_processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=10, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index db14749c3..620bd1df5 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -90,7 +90,6 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None: assert isinstance(processed, dict) results = backtesting.backtest( processed=processed, - stake_amount=config['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=1, @@ -111,7 +110,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): min_date, max_date = get_timerange(processed) return { 'processed': processed, - 'stake_amount': conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 10, @@ -461,7 +459,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: min_date, max_date = get_timerange(processed) results = backtesting.backtest( processed=processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=10, @@ -523,7 +520,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None min_date, max_date = get_timerange(processed) results = backtesting.backtest( processed=processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=1, @@ -678,7 +674,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) min_date, max_date = get_timerange(processed) backtest_conf = { 'processed': processed, - 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 3, @@ -694,7 +689,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtest_conf = { 'processed': processed, - 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 1, From 8d61a263823943cdbdb911d2c40bc283ba415903 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Feb 2021 20:20:32 +0100 Subject: [PATCH 13/44] Allow dynamic stake for backtesting and hyperopt --- freqtrade/commands/optimize_commands.py | 14 +++++++++----- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 7 ++++--- tests/optimize/test_hyperopt.py | 8 ++++---- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 7411ca9c6..bf36972c4 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -3,7 +3,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -23,10 +23,14 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ RunMode.HYPEROPT: 'hyperoptimization', } if (method in no_unlimited_runmodes.keys() and - config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT): - raise DependencyException( - f'The value of `stake_amount` cannot be set as "{constants.UNLIMITED_STAKE_AMOUNT}" ' - f'for {no_unlimited_runmodes[method]}') + config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT and + config['max_open_trades'] != float('inf')): + pass + # config['dry_run_wallet'] = config['stake_amount'] * \ + # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) + + # logger.warning(f"Changing dry-run-wallet to {config['dry_run_wallet']} " + # "(max_open_trades * stake_amount).") return config diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7ed5064e7..29559126b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -445,11 +445,11 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - print(self.wallets.get_all_balances()) self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), + 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), } diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 620bd1df5..061bcbaa0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -242,8 +242,9 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con '--strategy', 'DefaultStrategy', ] - with pytest.raises(DependencyException, match=r'.`stake_amount`.*'): - setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + # TODO: does this test still make sense? + conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + assert isinstance(conf, dict) def test_start(mocker, fee, default_conf, caplog) -> None: diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 68eb3d6f7..88a4cea2d 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,7 +15,7 @@ from filelock import Timeout from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -140,9 +140,9 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', ] - - with pytest.raises(DependencyException, match=r'.`stake_amount`.*'): - setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + # TODO: does this test still make sense? + conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + assert isinstance(conf, dict) def test_hyperoptresolver(mocker, default_conf, caplog) -> None: From 35e6a9ab3aa70cad59d8cc75b50bedecdce56e0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Feb 2021 09:01:05 +0100 Subject: [PATCH 14/44] Backtest-reports should calculate total gains based on starting capital --- docs/backtesting.md | 18 +++++++++++--- freqtrade/optimize/optimize_reports.py | 32 +++++++++++++++++-------- tests/conftest.py | 1 + tests/optimize/test_optimize_reports.py | 10 ++++---- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 38d1af45a..eab64a7a9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -252,7 +252,10 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| Total Profit % | 152.41% | +| Starting capital | 0.01000000 BTC | +| End capital | 0.01762792 BTC | +| Absolute profit | 0.00762792 BTC | +| Total Profit % | 76.2% | | Trades per day | 3.575 | | | | | Best Pair | LSK/BTC 26.26% | @@ -261,6 +264,7 @@ A backtesting result will look like that: | Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | +| Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | @@ -328,7 +332,10 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| Total Profit % | 152.41% | +| Starting capital | 0.01000000 BTC | +| End capital | 0.01762792 BTC | +| Absolute profit | 0.00762792 BTC | +| Total Profit % | 76.2% | | Trades per day | 3.575 | | | | | Best Pair | LSK/BTC 26.26% | @@ -337,6 +344,7 @@ It contains some useful key metrics about performance of your strategy on backte | Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | +| Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | @@ -351,11 +359,15 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. -- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. +- `Starting capital`: Start capital - as given by dry-run-wallet (config or command line). +- `End capital`: Final capital - starting capital + absolute profit. +- `Absolute profit`: Profit made in stake currency. +- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. +- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6338b1d71..d6adfdf50 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -56,12 +56,13 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: 'Wins', 'Draws', 'Losses'] -def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict: +def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: """ Generate one result dict, with "first_column" as key. """ profit_sum = result['profit_ratio'].sum() - profit_total = profit_sum / max_open_trades + # (end-capital - starting capital) / starting capital + profit_total = result['profit_abs'].sum() / starting_balance return { 'key': first_column, @@ -88,13 +89,13 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: } -def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, +def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_balance: int, results: DataFrame, skip_nan: bool = False) -> List[Dict]: """ Generates and returns a list for the given backtest data and the results dataframe :param data: Dict of containing data that was used during backtesting. :param stake_currency: stake-currency - used to correctly name headers - :param max_open_trades: Maximum allowed open trades + :param starting_balance: Starting balance :param results: Dataframe containing the backtest results :param skip_nan: Print "left open" open trades :return: List of Dicts containing the metrics per pair @@ -107,10 +108,10 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t if skip_nan and result['profit_abs'].isnull().all(): continue - tabular_data.append(_generate_result_line(result, max_open_trades, pair)) + tabular_data.append(_generate_result_line(result, starting_balance, pair)) # Append Total - tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL')) + tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data @@ -159,7 +160,7 @@ def generate_strategy_metrics(all_results: Dict) -> List[Dict]: tabular_data = [] for strategy, results in all_results.items(): tabular_data.append(_generate_result_line( - results['results'], results['config']['max_open_trades'], strategy) + results['results'], results['config']['dry_run_wallet'], strategy) ) return tabular_data @@ -246,15 +247,16 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], continue config = content['config'] max_open_trades = min(config['max_open_trades'], len(btdata.keys())) + starting_balance = config['dry_run_wallet'] stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - max_open_trades=max_open_trades, + starting_balance=starting_balance, results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, results=results) left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - max_open_trades=max_open_trades, + starting_balance=starting_balance, results=results.loc[results['is_open']], skip_nan=True) daily_stats = generate_daily_stats(results) @@ -276,7 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_ratio'].sum() / max_open_trades, + 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.int_timestamp * 1000, @@ -292,6 +294,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], + 'starting_balance': starting_balance, + 'dry_run_wallet': starting_balance, + 'final_balance': content['final_balance'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -431,6 +436,13 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), + ('Starting capital', round_coin_value(strat_results['starting_balance'], + strat_results['stake_currency'])), + ('End capital', round_coin_value(strat_results['final_balance'], + strat_results['stake_currency'])), + ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], + strat_results['stake_currency'])), + ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), ('', ''), # Empty line to improve readability diff --git a/tests/conftest.py b/tests/conftest.py index 946ae1fb5..6e70603b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -261,6 +261,7 @@ def get_default_conf(testdatadir): "20": 0.02, "0": 0.04 }, + "dry_run_wallet": 1000, "stoploss": -0.10, "unfilledtimeout": { "buy": 10, diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8b64c2764..405cc599b 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -48,7 +48,7 @@ def test_text_table_bt_results(): ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', - max_open_trades=2, results=results) + starting_balance=4, results=results) assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str @@ -78,6 +78,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): }), 'config': default_conf, 'locks': [], + 'final_balance': 1000.02, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } @@ -189,7 +190,7 @@ def test_generate_pair_metrics(): ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', - max_open_trades=2, results=results) + starting_balance=2, results=results) assert isinstance(pair_results, list) assert len(pair_results) == 2 assert pair_results[-1]['key'] == 'TOTAL' @@ -291,6 +292,7 @@ def test_generate_sell_reason_stats(): def test_text_table_strategy(default_conf): default_conf['max_open_trades'] = 2 + default_conf['dry_run_wallet'] = 3 results = {} results['TestStrategy1'] = {'results': pd.DataFrame( { @@ -323,9 +325,9 @@ def test_text_table_strategy(default_conf): '|---------------+--------+----------------+----------------+------------------+' '----------------+----------------+--------+---------+----------|\n' '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' - ' 30.00 | 0:17:00 | 3 | 0 | 0 |\n' + ' 36.67 | 0:17:00 | 3 | 0 | 0 |\n' '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' - ' 45.00 | 0:20:00 | 3 | 0 | 0 |' + ' 43.33 | 0:20:00 | 3 | 0 | 0 |' ) strategy_results = generate_strategy_metrics(all_results=results) From 72f21fc5ec90ae15f622c362d2a24bd31f068613 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 13:08:49 +0100 Subject: [PATCH 15/44] Add trade-volume metric --- docs/backtesting.md | 3 +++ freqtrade/optimize/optimize_reports.py | 5 ++++- tests/optimize/test_backtesting.py | 2 ++ tests/optimize/test_optimize_reports.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index eab64a7a9..ada788da9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -257,6 +257,7 @@ A backtesting result will look like that: | Absolute profit | 0.00762792 BTC | | Total Profit % | 76.2% | | Trades per day | 3.575 | +| Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | | Worst Pair | ZEC/BTC -10.18% | @@ -337,6 +338,7 @@ It contains some useful key metrics about performance of your strategy on backte | Absolute profit | 0.00762792 BTC | | Total Profit % | 76.2% | | Trades per day | 3.575 | +| Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | | Worst Pair | ZEC/BTC -10.18% | @@ -364,6 +366,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Absolute profit`: Profit made in stake currency. - `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d6adfdf50..dde0f8dd2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -277,6 +277,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), + 'total_volume': results['stake_amount'].sum(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), @@ -442,9 +443,11 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('Total trade volume', round_coin_value(strat_results['total_volume'], + strat_results['stake_currency'])), + ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 061bcbaa0..8fba8724b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -817,6 +817,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '2018-01-30 05:35:00', ], utc=True), 'trade_duration': [235, 40], 'is_open': [False, False], + 'stake_amount': [0.01, 0.01], 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], 'sell_reason': [SellType.ROI, SellType.ROI] @@ -833,6 +834,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '2018-01-30 08:30:00'], utc=True), 'trade_duration': [47, 40, 20], 'is_open': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], 'open_rate': [0.104445, 0.10302485, 0.122541], 'close_rate': [0.104969, 0.103541, 0.123541], 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 405cc599b..ca6a4ab01 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -73,6 +73,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "trade_duration": [123, 34, 31, 14], "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], "sell_reason": [SellType.ROI, SellType.STOP_LOSS, SellType.ROI, SellType.FORCE_SELL] }), From 74fc4bdab5e108c72015f80112df8ff7aa12bdcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 07:56:35 +0100 Subject: [PATCH 16/44] Shorten debug log --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 29559126b..c60cfa9b7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -382,7 +382,7 @@ class Backtesting: # Prevents buying if the trade-slot was freed in this candle open_trade_count_start += 1 open_trade_count += 1 - # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") + # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) Trade.trades.append(trade) From 0d2f877e77b17dc87c4efede8097dc90fd6202a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 19:30:17 +0100 Subject: [PATCH 17/44] Use absolute drawdown calc --- freqtrade/data/btanalysis.py | 10 ++++++--- freqtrade/optimize/optimize_reports.py | 17 ++++++++++++++- freqtrade/plot/plotting.py | 2 +- .../protections/max_drawdown_protection.py | 2 +- tests/data/test_btanalysis.py | 21 ++++++++++++------- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 8e851a8e8..117278585 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -360,13 +360,14 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', value_col: str = 'profit_ratio' - ) -> Tuple[float, pd.Timestamp, pd.Timestamp]: + ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]: """ Calculate max drawdown and the corresponding close dates :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :param date_col: Column in DataFrame to use for dates (defaults to 'close_date') :param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio') - :return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time + :return: Tuple (float, highdate, lowdate, highvalue, lowvalue) with absolute max drawdown, + high and low time and high and low value. :raise: ValueError if trade-dataframe was found empty. """ if len(trades) == 0: @@ -382,7 +383,10 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' raise ValueError("No losing trade, therefore no drawdown.") high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] low_date = profit_results.loc[idxmin, date_col] - return abs(min(max_drawdown_df['drawdown'])), high_date, low_date + high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin] + ['high_value'].idxmax(), 'cumulative'] + low_val = max_drawdown_df.loc[idxmin, 'cumulative'] + return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index dde0f8dd2..5b3f813f2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -322,14 +322,20 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], result['strategy'][strategy] = strat_stats try: - max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( + max_drawdown, _, _, _, _ = calculate_max_drawdown( results, value_col='profit_ratio') + drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( + results, value_col='profit_abs') strat_stats.update({ 'max_drawdown': max_drawdown, + 'max_drawdown_abs': drawdown_abs, 'drawdown_start': drawdown_start, 'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, + + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, }) csum_min, csum_max = calculate_csum(results) @@ -341,6 +347,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], except ValueError: strat_stats.update({ 'max_drawdown': 0.0, + 'max_drawdown_abs': 0.0, + 'max_drawdown_low': 0.0, + 'max_drawdown_high': 0.0, 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start_ts': 0, 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), @@ -471,6 +480,12 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), + ('Max Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + strat_results['stake_currency'])), + ('Max Drawdown high', round_coin_value(strat_results['max_drawdown_high'], + strat_results['stake_currency'])), + ('Max Drawdown low', round_coin_value(strat_results['max_drawdown_low'], + strat_results['stake_currency'])), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 4325e537e..682c2b018 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -145,7 +145,7 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, Add scatter points indicating max drawdown """ try: - max_drawdown, highdate, lowdate = calculate_max_drawdown(trades) + max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades) drawdown = go.Scatter( x=[highdate, lowdate], diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index d54e6699b..d1c6b192d 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -55,7 +55,7 @@ class MaxDrawdown(IProtection): # Drawdown is always positive try: - drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') + drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') except ValueError: return False, None, None diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 3c4687745..555808679 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -274,15 +274,17 @@ def test_create_cum_profit1(testdatadir): def test_calculate_max_drawdown(testdatadir): filename = testdatadir / "backtest-result_test.json" bt_data = load_backtest_data(filename) - drawdown, h, low = calculate_max_drawdown(bt_data) + drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(bt_data) assert isinstance(drawdown, float) assert pytest.approx(drawdown) == 0.21142322 - assert isinstance(h, Timestamp) - assert isinstance(low, Timestamp) - assert h == Timestamp('2018-01-24 14:25:00', tz='UTC') - assert low == Timestamp('2018-01-30 04:45:00', tz='UTC') + assert isinstance(hdate, Timestamp) + assert isinstance(lowdate, Timestamp) + assert isinstance(hval, float) + assert isinstance(lval, float) + assert hdate == Timestamp('2018-01-24 14:25:00', tz='UTC') + assert lowdate == Timestamp('2018-01-30 04:45:00', tz='UTC') with pytest.raises(ValueError, match='Trade dataframe empty.'): - drawdown, h, low = calculate_max_drawdown(DataFrame()) + drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(DataFrame()) def test_calculate_csum(testdatadir): @@ -310,13 +312,16 @@ def test_calculate_max_drawdown2(): # sort by profit and reset index df = df.sort_values('profit').reset_index(drop=True) df1 = df.copy() - drawdown, h, low = calculate_max_drawdown(df, date_col='open_date', value_col='profit') + drawdown, hdate, ldate, hval, lval = calculate_max_drawdown( + df, date_col='open_date', value_col='profit') # Ensure df has not been altered. assert df.equals(df1) assert isinstance(drawdown, float) # High must be before low - assert h < low + assert hdate < ldate + # High value must be higher than low value + assert hval > lval assert drawdown == 0.091755 df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date']) From aed23d55c280806af19fe9c7927fe1088ff1e0b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 20:12:59 +0100 Subject: [PATCH 18/44] Add starting balance to profit cumsum calculation --- freqtrade/data/btanalysis.py | 7 ++++--- tests/data/test_btanalysis.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 117278585..3adee8775 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -389,10 +389,11 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val -def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: +def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]: """ Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane :param trades: DataFrame containing trades (requires columns close_date and profit_percent) + :param starting_balance: Add starting balance to results, to show the wallets high / low points :return: Tuple (float, float) with cumsum of profit_abs :raise: ValueError if trade-dataframe was found empty. """ @@ -401,7 +402,7 @@ def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: csum_df = pd.DataFrame() csum_df['sum'] = trades['profit_abs'].cumsum() - csum_min = csum_df['sum'].min() - csum_max = csum_df['sum'].max() + csum_min = csum_df['sum'].min() + starting_balance + csum_max = csum_df['sum'].max() + starting_balance return csum_min, csum_max diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 555808679..538c89a90 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -296,6 +296,11 @@ def test_calculate_csum(testdatadir): assert isinstance(csum_max, float) assert csum_min < 0.01 assert csum_max > 0.02 + csum_min1, csum_max1 = calculate_csum(bt_data, 5) + + assert csum_min1 == csum_min + 5 + assert csum_max1 == csum_max + 5 + with pytest.raises(ValueError, match='Trade dataframe empty.'): csum_min, csum_max = calculate_csum(DataFrame()) From f367375e5b6240625ddd0ba51120ad48faa41409 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 20:39:50 +0100 Subject: [PATCH 19/44] ABS drawdown should show wallet high and low values --- freqtrade/optimize/optimize_reports.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 5b3f813f2..1ac0ae1d6 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -334,11 +334,11 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - 'max_drawdown_low': low_val, - 'max_drawdown_high': high_val, + 'max_drawdown_low': low_val + starting_balance, + 'max_drawdown_high': high_val + starting_balance, }) - csum_min, csum_max = calculate_csum(results) + csum_min, csum_max = calculate_csum(results, starting_balance) strat_stats.update({ 'csum_min': csum_min, 'csum_max': csum_max @@ -493,7 +493,15 @@ def text_table_add_metrics(strat_results: Dict) -> str: return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") else: - return '' + start_balance = round_coin_value(strat_results['starting_balance'], + strat_results['stake_currency']) + stake_amount = round_coin_value(strat_results['stake_amount'], + strat_results['stake_currency']) + message = ("No trades made. " + f"Your starting balance was {start_balance}, " + f"and your stake was {stake_amount}." + ) + return message def show_backtest_results(config: Dict, backtest_stats: Dict): From 37d7d2afd5c953c413024964d0078172ba1e3e1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 19:50:10 +0100 Subject: [PATCH 20/44] Wallets should not recalculate close_profit for closed trades --- freqtrade/wallets.py | 2 +- tests/conftest_trades.py | 2 ++ tests/test_freqtradebot.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f5ce4c102..c2085641e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -69,7 +69,7 @@ class Wallets: _wallets = {} closed_trades = Trade.get_trades_proxy(is_open=False) open_trades = Trade.get_trades_proxy(is_open=True) - tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades]) current_stake = self.start_cap + tot_profit - tot_in_trades diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 6a42d04e3..025aac1b6 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -82,6 +82,7 @@ def mock_trade_2(fee): open_rate=0.123, close_rate=0.128, close_profit=0.005, + close_profit_abs=0.000584127, exchange='bittrex', is_open=False, open_order_id='dry_run_sell_12345', @@ -141,6 +142,7 @@ def mock_trade_3(fee): open_rate=0.05, close_rate=0.06, close_profit=0.01, + close_profit_abs=0.000155, exchange='bittrex', is_open=False, strategy='DefaultStrategy', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3bd2f5607..d7d2e19f6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2243,6 +2243,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade.close_profit_abs = 0.001 open_trade.is_open = False Trade.session.add(open_trade) @@ -2290,6 +2291,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade.close_profit_abs = 0.001 open_trade.is_open = False Trade.session.add(open_trade) From 7913166453518733fcd793879d0150d1339796d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 20:07:27 +0100 Subject: [PATCH 21/44] Improve performance by updating wallets only when necessary --- freqtrade/optimize/backtesting.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c60cfa9b7..f921f64c3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -180,10 +180,6 @@ class Backtesting: PairLocks.reset_locks() Trade.reset_trades() - def update_wallets(self): - if self.wallets: - self.wallets.update() - def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -272,7 +268,6 @@ class Backtesting: def _enter_trade(self, pair: str, row, max_open_trades: int, open_trade_count: int) -> Optional[Trade]: - self.update_wallets() try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) @@ -391,7 +386,6 @@ class Backtesting: trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: - self.update_wallets() # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) @@ -404,7 +398,7 @@ class Backtesting: tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) - self.update_wallets() + self.wallets.update() return trade_list_to_dataframe(trades) From f04f07299c7689841eaf7eab15c574c09c5774b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 20:19:03 +0100 Subject: [PATCH 22/44] Improve backtesting metrics --- docs/backtesting.md | 39 ++++++++++++++++++-------- freqtrade/optimize/optimize_reports.py | 36 +++++++++++++----------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index ada788da9..bac12dae0 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -252,11 +252,12 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| Starting capital | 0.01000000 BTC | -| End capital | 0.01762792 BTC | +| Starting balance | 0.01000000 BTC | +| Final balance | 0.01762792 BTC | | Absolute profit | 0.00762792 BTC | -| Total Profit % | 76.2% | +| Total profit % | 76.2% | | Trades per day | 3.575 | +| Avg. stake amount | 0.001 | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -269,7 +270,12 @@ A backtesting result will look like that: | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | -| Max Drawdown | 50.63% | +| Min balance | 0.00945123 BTC | +| Max balance | 0.01846651 BTC | +| Drawdown | 50.63% | +| Drawdown | 0.0015 BTC | +| Drawdown high | 0.0013 BTC | +| Drawdown low | -0.0002 BTC | | Drawdown Start | 2019-02-15 14:10:00 | | Drawdown End | 2019-04-11 18:15:00 | | Market change | -5.88% | @@ -333,11 +339,12 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| Starting capital | 0.01000000 BTC | -| End capital | 0.01762792 BTC | +| Starting balance | 0.01000000 BTC | +| Final balance | 0.01762792 BTC | | Absolute profit | 0.00762792 BTC | -| Total Profit % | 76.2% | +| Total profit % | 76.2% | | Trades per day | 3.575 | +| Avg. stake amount | 0.001 | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -350,7 +357,12 @@ It contains some useful key metrics about performance of your strategy on backte | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | -| Max Drawdown | 50.63% | +| Min balance | 0.00945123 BTC | +| Max balance | 0.01846651 BTC | +| Drawdown | 50.63% | +| Drawdown | 0.0015 BTC | +| Drawdown high | 0.0013 BTC | +| Drawdown low | -0.0002 BTC | | Drawdown Start | 2019-02-15 14:10:00 | | Drawdown End | 2019-04-11 18:15:00 | | Market change | -5.88% | @@ -361,18 +373,21 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. -- `Starting capital`: Start capital - as given by dry-run-wallet (config or command line). -- `End capital`: Final capital - starting capital + absolute profit. +- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line). +- `End balance`: Final balance - starting balance + absolute profit. - `Absolute profit`: Profit made in stake currency. -- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. +- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. -- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). +- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. +- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). +- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 1ac0ae1d6..cee0bb1ce 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -278,6 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'total_volume': results['stake_amount'].sum(), + 'avg_stake_amount': results['stake_amount'].mean(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), @@ -295,6 +296,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], + 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], @@ -334,8 +336,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - 'max_drawdown_low': low_val + starting_balance, - 'max_drawdown_high': high_val + starting_balance, + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, }) csum_min, csum_max = calculate_csum(results, starting_balance) @@ -446,14 +448,16 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), - ('Starting capital', round_coin_value(strat_results['starting_balance'], + ('Starting balance', round_coin_value(strat_results['starting_balance'], strat_results['stake_currency'])), - ('End capital', round_coin_value(strat_results['final_balance'], - strat_results['stake_currency'])), + ('Final balance', round_coin_value(strat_results['final_balance'], + strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), + ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'], + strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), @@ -474,18 +478,18 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('', ''), # Empty line to improve readability - ('Abs Profit Min', round_coin_value(strat_results['csum_min'], - strat_results['stake_currency'])), - ('Abs Profit Max', round_coin_value(strat_results['csum_max'], - strat_results['stake_currency'])), + ('Min balance', round_coin_value(strat_results['csum_min'], + strat_results['stake_currency'])), + ('Max balance', round_coin_value(strat_results['csum_max'], + strat_results['stake_currency'])), - ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), - ('Max Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + ('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), + ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + strat_results['stake_currency'])), + ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], + strat_results['stake_currency'])), + ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'], strat_results['stake_currency'])), - ('Max Drawdown high', round_coin_value(strat_results['max_drawdown_high'], - strat_results['stake_currency'])), - ('Max Drawdown low', round_coin_value(strat_results['max_drawdown_low'], - strat_results['stake_currency'])), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), From 52acacbed5b43f5cec2f07af74336998c3e51523 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 07:20:51 +0100 Subject: [PATCH 23/44] Check min-trade-stake in backtesting --- freqtrade/optimize/backtesting.py | 4 +++- tests/optimize/test_backtest_detail.py | 3 ++- tests/optimize/test_backtesting.py | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f921f64c3..bd185234f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,7 +273,9 @@ class Backtesting: pair, max_open_trades - open_trade_count, None) except DependencyException: stake_amount = 0 - if stake_amount: + min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) + if stake_amount and stake_amount > min_stake_amount: + # print(f"{pair}, {stake_amount}") # Enter trade trade = Trade( pair=pair, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 4d6605b9f..a56e024f7 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -489,7 +489,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal} - mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0)) + mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8fba8724b..eda8aac9d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -450,6 +450,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' @@ -510,6 +511,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) backtesting = Backtesting(default_conf) @@ -555,6 +557,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad default_conf['enable_protections'] = True mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) tests = [ ['sine', 9], ['raise', 10], @@ -586,6 +589,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, default_conf['protections'] = protections default_conf['enable_protections'] = True + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results @@ -623,6 +627,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC', datadir=testdatadir) @@ -655,6 +660,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) return dataframe + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) From 394a6bbf2a86c8c4990c1f38e566ebaaa2ea2561 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:21:30 +0100 Subject: [PATCH 24/44] Fix some type errors --- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/optimize_reports.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bd185234f..7028a38cd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -274,7 +274,7 @@ class Backtesting: except DependencyException: stake_amount = 0 min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) - if stake_amount and stake_amount > min_stake_amount: + if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") # Enter trade trade = Trade( @@ -341,7 +341,7 @@ class Backtesting: indexes: Dict = {} tmp = start_date + timedelta(minutes=self.timeframe_min) - open_trades: Dict[str, List] = defaultdict(list) + open_trades: Dict[str, List[Trade]] = defaultdict(list) open_trade_count = 0 # Loop timerange and get candle for each pair at that point in time diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index cee0bb1ce..e7111f20c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -479,9 +479,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], - strat_results['stake_currency'])), + strat_results['stake_currency'])), ('Max balance', round_coin_value(strat_results['csum_max'], - strat_results['stake_currency'])), + strat_results['stake_currency'])), ('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], From 03eb23a4ce7cdf54ffbc3595814ca2029f979262 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 19:29:04 +0100 Subject: [PATCH 25/44] 2 levels of Trade models, one with and one without sqlalchemy Fixes a performance issue when backtesting with sqlalchemy, as that uses descriptors for all properties. --- freqtrade/optimize/backtesting.py | 11 +- freqtrade/persistence/__init__.py | 3 +- freqtrade/persistence/models.py | 244 ++++++++++++++++++++---------- 3 files changed, 170 insertions(+), 88 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7028a38cd..322a3f00b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -23,6 +23,7 @@ from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.persistence import PairLocks, Trade +from freqtrade.persistence.models import LocalTrade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -267,7 +268,7 @@ class Backtesting: return None def _enter_trade(self, pair: str, row, max_open_trades: int, - open_trade_count: int) -> Optional[Trade]: + open_trade_count: int) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) @@ -277,7 +278,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") # Enter trade - trade = Trade( + trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], open_date=row[DATE_IDX], @@ -291,8 +292,8 @@ class Backtesting: return trade return None - def handle_left_open(self, open_trades: Dict[str, List[Trade]], - data: Dict[str, List[Tuple]]) -> List[Trade]: + def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]], + data: Dict[str, List[Tuple]]) -> List[LocalTrade]: """ Handling of left open trades at the end of backtesting """ @@ -381,7 +382,7 @@ class Backtesting: open_trade_count += 1 # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) - Trade.trades.append(trade) + LocalTrade.trades.append(trade) for trade in open_trades[pair]: # also check the buying candle for sell conditions. diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 35f2bc406..d1fcac0ba 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db +from freqtrade.persistence.models import (LocalTrade, Order, Trade, clean_dry_run_db, cleanup_db, + init_db) from freqtrade.persistence.pairlock_middleware import PairLocks diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f72705c34..48ae8bb40 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -199,67 +199,67 @@ class Order(_DECL_BASE): return Order.query.filter(Order.ft_is_open.is_(True)).all() -class Trade(_DECL_BASE): +class LocalTrade(): """ Trade database model. - Also handles updating and querying trades + Used in backtesting - must be aligned to Trade model! + """ - __tablename__ = 'trades' - - use_db: bool = True + use_db: bool = False # Trades container for backtesting - trades: List['Trade'] = [] + trades: List['LocalTrade'] = [] - id = Column(Integer, primary_key=True) + id: int = 0 - orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") + orders: List[Order] = [] - exchange = Column(String, nullable=False) - pair = Column(String, nullable=False, index=True) - is_open = Column(Boolean, nullable=False, default=True, index=True) - fee_open = Column(Float, nullable=False, default=0.0) - fee_open_cost = Column(Float, nullable=True) - fee_open_currency = Column(String, nullable=True) - fee_close = Column(Float, nullable=False, default=0.0) - fee_close_cost = Column(Float, nullable=True) - fee_close_currency = Column(String, nullable=True) - open_rate = Column(Float) - open_rate_requested = Column(Float) + exchange: str = '' + pair: str = '' + is_open: bool = True + fee_open: float = 0.0 + fee_open_cost: Optional[float] = None + fee_open_currency: str = '' + fee_close: float = 0.0 + fee_close_cost: Optional[float] = None + fee_close_currency: str = '' + open_rate: float + open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value - open_trade_value = Column(Float) - close_rate = Column(Float) - close_rate_requested = Column(Float) - close_profit = Column(Float) - close_profit_abs = Column(Float) - stake_amount = Column(Float, nullable=False) - amount = Column(Float) - amount_requested = Column(Float) - open_date = Column(DateTime, nullable=False, default=datetime.utcnow) - close_date = Column(DateTime) - open_order_id = Column(String) + open_trade_value: float + close_rate: Optional[float] = None + close_rate_requested: Optional[float] = None + close_profit: Optional[float] = None + close_profit_abs: Optional[float] = None + stake_amount: float + amount: float + amount_requested: Optional[float] = None + open_date: datetime + close_date: Optional[datetime] = None + open_order_id: Optional[str] = None # absolute value of the stop loss - stop_loss = Column(Float, nullable=True, default=0.0) + stop_loss: float = 0.0 # percentage value of the stop loss - stop_loss_pct = Column(Float, nullable=True) + stop_loss_pct: float = 0.0 # absolute value of the initial stop loss - initial_stop_loss = Column(Float, nullable=True, default=0.0) + initial_stop_loss: float = 0.0 # percentage value of the initial stop loss - initial_stop_loss_pct = Column(Float, nullable=True) + initial_stop_loss_pct: float = 0.0 # stoploss order id which is on exchange - stoploss_order_id = Column(String, nullable=True, index=True) + stoploss_order_id: Optional[str] = None # last update time of the stoploss order on exchange - stoploss_last_update = Column(DateTime, nullable=True) + stoploss_last_update: Optional[datetime] = None # absolute value of the highest reached price - max_rate = Column(Float, nullable=True, default=0.0) + max_rate: float = 0.0 # Lowest price reached - min_rate = Column(Float, nullable=True) - sell_reason = Column(String, nullable=True) - sell_order_status = Column(String, nullable=True) - strategy = Column(String, nullable=True) - timeframe = Column(Integer, nullable=True) + min_rate: float = 0.0 + sell_reason: str = '' + sell_order_status: str = '' + strategy: str = '' + timeframe: Optional[int] = None def __init__(self, **kwargs): - super().__init__(**kwargs) + for key in kwargs: + setattr(self, key, kwargs[key]) self.recalc_open_trade_value() def __repr__(self): @@ -349,8 +349,7 @@ class Trade(_DECL_BASE): """ Resets all trades. Only active for backtesting mode. """ - if not Trade.use_db: - Trade.trades = [] + LocalTrade.trades = [] def adjust_min_max_rates(self, current_price: float) -> None: """ @@ -418,8 +417,8 @@ class Trade(_DECL_BASE): if order_type in ('market', 'limit') and order['side'] == 'buy': # Update open rate and actual amount - self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) - self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) + self.open_rate = float(safe_value_fallback(order, 'average', 'price')) + self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_value() if self.is_open: logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') @@ -443,7 +442,7 @@ class Trade(_DECL_BASE): Sets close_rate to the given rate, calculates total profit and marks trade as closed """ - self.close_rate = Decimal(rate) + self.close_rate = rate self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() self.close_date = self.close_date or datetime.utcnow() @@ -488,14 +487,6 @@ class Trade(_DECL_BASE): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) - def delete(self) -> None: - - for order in self.orders: - Order.session.delete(order) - - Trade.session.delete(self) - Trade.session.flush() - def _calc_open_trade_value(self) -> float: """ Calculate the open_rate including open_fee. @@ -525,7 +516,7 @@ class Trade(_DECL_BASE): if rate is None and not self.close_rate: return 0.0 - sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) + sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = sell_trade * Decimal(fee or self.fee_close) return float(sell_trade - fees) @@ -597,7 +588,7 @@ class Trade(_DECL_BASE): @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, - ) -> List['Trade']: + ) -> List['LocalTrade']: """ Helper function to query Trades. Returns a List of trades, filtered on the parameters given. @@ -606,30 +597,19 @@ class Trade(_DECL_BASE): :return: unsorted List[Trade] """ - if Trade.use_db: - trade_filter = [] - if pair: - trade_filter.append(Trade.pair == pair) - if open_date: - trade_filter.append(Trade.open_date > open_date) - if close_date: - trade_filter.append(Trade.close_date > close_date) - if is_open is not None: - trade_filter.append(Trade.is_open.is_(is_open)) - return Trade.get_trades(trade_filter).all() - else: - # Offline mode - without database - sel_trades = [trade for trade in Trade.trades] - if pair: - sel_trades = [trade for trade in sel_trades if trade.pair == pair] - if open_date: - sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] - if close_date: - sel_trades = [trade for trade in sel_trades if trade.close_date - and trade.close_date > close_date] - if is_open is not None: - sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] - return sel_trades + + # Offline mode - without database + sel_trades = [trade for trade in LocalTrade.trades] + if pair: + sel_trades = [trade for trade in sel_trades if trade.pair == pair] + if open_date: + sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] + if close_date: + sel_trades = [trade for trade in sel_trades if trade.close_date + and trade.close_date > close_date] + if is_open is not None: + sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] + return sel_trades @staticmethod def get_open_trades() -> List[Any]: @@ -735,6 +715,106 @@ class Trade(_DECL_BASE): logger.info(f"New stoploss: {trade.stop_loss}.") +class Trade(_DECL_BASE, LocalTrade): + """ + Trade database model. + Also handles updating and querying trades + """ + __tablename__ = 'trades' + + use_db: bool = True + + id = Column(Integer, primary_key=True) + + orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") + + exchange = Column(String, nullable=False) + pair = Column(String, nullable=False, index=True) + is_open = Column(Boolean, nullable=False, default=True, index=True) + fee_open = Column(Float, nullable=False, default=0.0) + fee_open_cost = Column(Float, nullable=True) + fee_open_currency = Column(String, nullable=True) + fee_close = Column(Float, nullable=False, default=0.0) + fee_close_cost = Column(Float, nullable=True) + fee_close_currency = Column(String, nullable=True) + open_rate = Column(Float) + open_rate_requested = Column(Float) + # open_trade_value - calculated via _calc_open_trade_value + open_trade_value = Column(Float) + close_rate = Column(Float) + close_rate_requested = Column(Float) + close_profit = Column(Float) + close_profit_abs = Column(Float) + stake_amount = Column(Float, nullable=False) + amount = Column(Float) + amount_requested = Column(Float) + open_date = Column(DateTime, nullable=False, default=datetime.utcnow) + close_date = Column(DateTime) + open_order_id = Column(String) + # absolute value of the stop loss + stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the stop loss + stop_loss_pct = Column(Float, nullable=True) + # absolute value of the initial stop loss + initial_stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the initial stop loss + initial_stop_loss_pct = Column(Float, nullable=True) + # stoploss order id which is on exchange + stoploss_order_id = Column(String, nullable=True, index=True) + # last update time of the stoploss order on exchange + stoploss_last_update = Column(DateTime, nullable=True) + # absolute value of the highest reached price + max_rate = Column(Float, nullable=True, default=0.0) + # Lowest price reached + min_rate = Column(Float, nullable=True) + sell_reason = Column(String, nullable=True) + sell_order_status = Column(String, nullable=True) + strategy = Column(String, nullable=True) + timeframe = Column(Integer, nullable=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.recalc_open_trade_value() + + def delete(self) -> None: + + for order in self.orders: + Order.session.delete(order) + + Trade.session.delete(self) + Trade.session.flush() + + @staticmethod + def get_trades_proxy(*, pair: str = None, is_open: bool = None, + open_date: datetime = None, close_date: datetime = None, + ) -> List['LocalTrade']: + """ + Helper function to query Trades. + Returns a List of trades, filtered on the parameters given. + In live mode, converts the filter to a database query and returns all rows + In Backtest mode, uses filters on Trade.trades to get the result. + + :return: unsorted List[Trade] + """ + if Trade.use_db: + trade_filter = [] + if pair: + trade_filter.append(Trade.pair == pair) + if open_date: + trade_filter.append(Trade.open_date > open_date) + if close_date: + trade_filter.append(Trade.close_date > close_date) + if is_open is not None: + trade_filter.append(Trade.is_open.is_(is_open)) + return Trade.get_trades(trade_filter).all() + else: + return LocalTrade.get_trades_proxy( + pair=pair, is_open=is_open, + open_date=open_date, + close_date=close_date + ) + + class PairLock(_DECL_BASE): """ Pair Locks database model. From 53a57f2c81f05c6bf7f2ce3cf5bd5cc95d591464 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:22:00 +0100 Subject: [PATCH 26/44] Change some types Fix types of new model object --- freqtrade/data/btanalysis.py | 4 ++-- freqtrade/optimize/backtesting.py | 12 ++++++------ freqtrade/plugins/protections/cooldown_period.py | 3 ++- freqtrade/plugins/protections/iprotection.py | 6 +++--- freqtrade/plugins/protections/low_profit_pairs.py | 2 +- freqtrade/plugins/protections/stoploss_guard.py | 2 +- tests/conftest.py | 4 ++-- tests/optimize/test_backtest_detail.py | 1 - 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3adee8775..c98477f4e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -10,7 +10,7 @@ import pandas as pd from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import json_load -from freqtrade.persistence import Trade, init_db +from freqtrade.persistence import LocalTrade, Trade, init_db logger = logging.getLogger(__name__) @@ -224,7 +224,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str, return df_final[df_final['open_trades'] > max_open_trades] -def trade_list_to_dataframe(trades: List[Trade]) -> pd.DataFrame: +def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame: """ Convert list of Trade objects to pandas Dataframe :param trades: List of trade objects diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 322a3f00b..aeafaffd3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -211,7 +211,7 @@ class Backtesting: data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)] return data - def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple, + def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, trade_dur: int) -> float: """ Get close rate for backtesting result @@ -251,10 +251,10 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[Trade]: + def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX], - sell_row[BUY_IDX], sell_row[SELL_IDX], + sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore + sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: @@ -331,7 +331,7 @@ class Backtesting: :param enable_protections: Should protections be enabled? :return: DataFrame with trades (results of backtesting) """ - trades: List[Trade] = [] + trades: List[LocalTrade] = [] self.prepare_backtest(enable_protections) # Use dict of lists with data for performance @@ -342,7 +342,7 @@ class Backtesting: indexes: Dict = {} tmp = start_date + timedelta(minutes=self.timeframe_min) - open_trades: Dict[str, List[Trade]] = defaultdict(list) + open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trade_count = 0 # Loop timerange and get candle for each pair at that point in time diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 2d7d7b4c7..f74f83885 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -44,7 +44,8 @@ class CooldownPeriod(IProtection): trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) if trades: # Get latest trade - trade = sorted(trades, key=lambda t: t.close_date)[-1] + # Ignore type error as we know we only get closed trades. + trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 684bf6cd3..d034beefc 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import plural from freqtrade.mixins import LoggingMixin -from freqtrade.persistence import Trade +from freqtrade.persistence import LocalTrade logger = logging.getLogger(__name__) @@ -93,11 +93,11 @@ class IProtection(LoggingMixin, ABC): """ @staticmethod - def calculate_lock_end(trades: List[Trade], stop_minutes: int) -> datetime: + def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime: """ Get lock end time """ - max_date: datetime = max([trade.close_date for trade in trades]) + max_date: datetime = max([trade.close_date for trade in trades if trade.close_date]) # comming from Database, tzinfo is not set. if max_date.tzinfo is None: max_date = max_date.replace(tzinfo=timezone.utc) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 9d5ed35b4..7822ce73c 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -53,7 +53,7 @@ class LowProfitPairs(IProtection): # Not enough trades in the relevant period return False, None, None - profit = sum(trade.close_profit for trade in trades) + profit = sum(trade.close_profit for trade in trades if trade.close_profit) if profit < self._required_profit: self.log_once( f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 5a9b9ddd0..635c0be04 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -56,7 +56,7 @@ class StoplossGuard(IProtection): trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, SellType.STOPLOSS_ON_EXCHANGE.value) - and trade.close_profit < 0)] + and trade.close_profit and trade.close_profit < 0)] if len(trades) < self._trade_limit: return False, None, None diff --git a/tests/conftest.py b/tests/conftest.py index 6e70603b1..793ba83b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade, init_db +from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, @@ -191,7 +191,7 @@ def create_mock_trades(fee, use_db: bool = True): if use_db: Trade.session.add(trade) else: - Trade.trades.append(trade) + LocalTrade.trades.append(trade) # Simulate dry_run entries trade = mock_trade_1(fee) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a56e024f7..0ba6f4a7f 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument import logging -from unittest.mock import MagicMock import pytest From 60db6ccf454715aa9d8b2ba56e4676006e8fb1fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:07:00 +0100 Subject: [PATCH 27/44] Add test for subclassing --- freqtrade/persistence/models.py | 8 ++++---- tests/test_persistence.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 48ae8bb40..51a48c246 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -222,16 +222,16 @@ class LocalTrade(): fee_close: float = 0.0 fee_close_cost: Optional[float] = None fee_close_currency: str = '' - open_rate: float + open_rate: float = 0.0 open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value - open_trade_value: float + open_trade_value: float = 0.0 close_rate: Optional[float] = None close_rate_requested: Optional[float] = None close_profit: Optional[float] = None close_profit_abs: Optional[float] = None - stake_amount: float - amount: float + stake_amount: float = 0.0 + amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime close_date: Optional[datetime] = None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1fced3e16..18a377ca3 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,14 +1,16 @@ # pragma pylint: disable=missing-docstring, C0103 +from types import FunctionType import logging from unittest.mock import MagicMock import arrow import pytest from sqlalchemy import create_engine +from sqlalchemy.sql.schema import Column from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db +from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re @@ -1176,3 +1178,25 @@ def test_select_order(fee): assert order.ft_order_side == 'stoploss' order = trades[4].select_order('sell', False) assert order is None + + +def test_Trade_object_idem(): + + assert issubclass(Trade, LocalTrade) + + trade = vars(Trade) + localtrade = vars(LocalTrade) + + # Parent (LocalTrade) should have the same attributes + for item in trade: + # Exclude private attributes and open_date (as it's not assigned a default) + if (not item.startswith('_') + and item not in ('delete', 'session', 'query', 'open_date')): + assert item in localtrade + + # Fails if only a column is added without corresponding parent field + for item in localtrade: + if (not item.startswith('__') + and item not in ('trades', ) + and type(getattr(LocalTrade, item)) not in (property, FunctionType)): + assert item in trade From fc256749af4a29ee30353a2ae4edc17f9b3a4021 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Feb 2021 06:54:33 +0100 Subject: [PATCH 28/44] Add test for backtesting _enter_trade --- freqtrade/optimize/backtesting.py | 7 +++-- tests/data/test_btanalysis.py | 1 - tests/optimize/test_backtesting.py | 41 +++++++++++++++++++++++++++++- tests/test_persistence.py | 3 +-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aeafaffd3..9a4a3787a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -22,8 +22,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.persistence import PairLocks, Trade -from freqtrade.persistence.models import LocalTrade +from freqtrade.persistence import LocalTrade, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -267,13 +266,13 @@ class Backtesting: return None - def _enter_trade(self, pair: str, row, max_open_trades: int, + def _enter_trade(self, pair: str, row: List, max_open_trades: int, open_trade_count: int) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) except DependencyException: - stake_amount = 0 + return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 538c89a90..e42c13e18 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -301,7 +301,6 @@ def test_calculate_csum(testdatadir): assert csum_min1 == csum_min + 5 assert csum_max1 == csum_max + 5 - with pytest.raises(ValueError, match='Trade dataframe empty.'): csum_min, csum_max = calculate_csum(DataFrame()) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index eda8aac9d..354b3f6b0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -17,8 +17,9 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.backtesting import Backtesting +from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType @@ -447,6 +448,44 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) +def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: + default_conf['ask_strategy']['use_sell_signal'] = False + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) + patch_exchange(mocker) + default_conf['stake_amount'] = 'unlimited' + backtesting = Backtesting(default_conf) + pair = 'UNITTEST/BTC' + row = [ + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 1, # Sell + 0.001, # Open + 0.0011, # Close + 0, # Sell + 0.00099, # Low + 0.0012, # High + ] + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert isinstance(trade, LocalTrade) + assert trade.stake_amount == 495 + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2) + assert trade is None + + # Stake-amount too high! + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert trade is None + + # Stake-amount too high! + mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", + side_effect=DependencyException) + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert trade is None + + def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 18a377ca3..1a8124b00 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,12 +1,11 @@ # pragma pylint: disable=missing-docstring, C0103 -from types import FunctionType import logging +from types import FunctionType from unittest.mock import MagicMock import arrow import pytest from sqlalchemy import create_engine -from sqlalchemy.sql.schema import Column from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException From d3fb473e578e0f1ea5b7275d7023e6f8088d2583 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Feb 2021 20:21:50 +0100 Subject: [PATCH 29/44] Improve backtesting documentation --- docs/backtesting.md | 86 ++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index bac12dae0..9fa9025d8 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -95,8 +95,7 @@ Strategy arguments: ## Test your strategy with Backtesting Now you have good Buy and Sell strategies and some historic data, you want to test it against -real data. This is what we call -[backtesting](https://en.wikipedia.org/wiki/Backtesting). +real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/` by default. If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`. @@ -104,6 +103,8 @@ For details on downloading, please refer to the [Data Downloading](data-download The result of backtesting will confirm if your bot has better odds of making a profit than a loss. +All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation. + !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. @@ -111,38 +112,46 @@ The result of backtesting will confirm if your bot has better odds of making a p To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. -### Run a backtesting against the currencies listed in your config file +### Example backtesting commands -#### With 5 min candle (OHLCV) data (per default) +With 5 min candle (OHLCV) data (per default) ```bash -freqtrade backtesting +freqtrade backtesting --strategy AwesomeStrategy ``` -#### With 1 min candle (OHLCV) data +Where `--strategy AwesomeStrategy` / `-s AwesomeStrategy` refers to the class name of the strategy, which is within a python file in the `user_data/strategies` directory. + +--- + +With 1 min candle (OHLCV) data ```bash -freqtrade backtesting --timeframe 1m +freqtrade backtesting --strategy AwesomeStrategy --timeframe 1m ``` -#### Using a different on-disk historical candle (OHLCV) data source +--- + +Providing a custom starting balance of 1000 (in stake currency) + +```bash +freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000 +``` + +--- + +Using a different on-disk historical candle (OHLCV) data source Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory. You can then use this data for backtesting as follows: ```bash -freqtrade --datadir user_data/data/bittrex-20180101 backtesting +freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101 ``` -#### With a (custom) strategy file +--- -```bash -freqtrade backtesting -s SampleStrategy -``` - -Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory. - -#### Comparing multiple Strategies +Comparing multiple Strategies ```bash freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m @@ -150,23 +159,29 @@ freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timefram Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies. -#### Exporting trades to file +--- + +Exporting trades to file ```bash -freqtrade backtesting --export trades --config config.json --strategy SampleStrategy +freqtrade backtesting --strategy backtesting --export trades --config config.json ``` The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. -#### Exporting trades to file specifying a custom filename +--- + +Exporting trades to file specifying a custom filename ```bash -freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json +freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json ``` Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period). -#### Supplying custom fee value +--- + +Supplying custom fee value Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting. @@ -181,26 +196,26 @@ freqtrade backtesting --fee 0.001 !!! Note Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info. -#### Running backtest with smaller testset by using timerange +--- -Use the `--timerange` argument to change how much of the testset you want to use. +Running backtest with smaller test-set by using timerange +Use the `--timerange` argument to change how much of the test-set you want to use. -For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata. +For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your input data. ```bash freqtrade backtesting --timerange=20190501- ``` -You can also specify particular dates or a range span indexed by start and stop. +You can also specify particular date ranges. The full timerange specification: -- Use tickframes till 2018/01/31: `--timerange=-20180131` -- Use tickframes since 2018/01/31: `--timerange=20180131-` -- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` -- Use tickframes between POSIX timestamps 1527595200 1527618600: - `--timerange=1527595200-1527618600` +- Use data until 2018/01/31: `--timerange=-20180131` +- Use data since 2018/01/31: `--timerange=20180131-` +- Use data since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` +- Use data between POSIX / epoch timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600` ## Understand the backtesting result @@ -296,9 +311,9 @@ here: The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. -The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums up all the profits/losses. -The column `tot profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). -In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `tot_profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. +The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses. +The column `Tot Profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). +In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `Tot Profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set. @@ -452,6 +467,5 @@ Detailed output for all strategies one after the other will be available, so mak ## Next step -Great, your strategy is profitable. What if the bot can give your the -optimal parameters to use for your strategy? +Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy? Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md) From 86f9409fd293604e03408e89beb460078768d103 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Feb 2021 20:14:33 +0100 Subject: [PATCH 30/44] fix --stake-amount parameter --- freqtrade/commands/cli_options.py | 1 - freqtrade/configuration/configuration.py | 7 +++++++ tests/test_configuration.py | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 90ebb5e6a..3b27237da 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -133,7 +133,6 @@ AVAILABLE_CLI_OPTIONS = { "stake_amount": Arg( '--stake-amount', help='Override the value of the `stake_amount` configuration setting.', - type=float, ), # Backtesting "position_stacking": Arg( diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 6295d01d4..88447e490 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -229,6 +229,13 @@ class Configuration: elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + if self.args.get('stake_amount', None): + # Convert explicitly to float to support CLI argument for both unlimited and value + try: + self.args['stake_amount'] = float(self.args['stake_amount']) + except ValueError: + pass + self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 94c3e24f6..6b3df392b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -430,7 +430,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non '--enable-position-stacking', '--disable-max-market-positions', '--timerange', ':100', - '--export', '/bar/foo' + '--export', '/bar/foo', + '--stake-amount', 'unlimited' ] args = Arguments(arglist).get_parsed_arg() @@ -463,6 +464,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'export' in config assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) + assert 'stake_amount' in config + assert config['stake_amount'] == 'unlimited' def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: From 98f3142b30e2067b4ead4e3dec51848d56a9c0cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 19:48:06 +0100 Subject: [PATCH 31/44] Improve handling of backtesting params --- freqtrade/commands/cli_options.py | 2 +- freqtrade/commands/optimize_commands.py | 11 ++++++++--- freqtrade/configuration/configuration.py | 6 +++--- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 17 +++++++++++++---- tests/optimize/test_hyperopt.py | 17 +++++++++++++---- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 3b27237da..15c13cec9 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -111,7 +111,7 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), "dry_run_wallet": Arg( - '--dry-run-wallet', + '--dry-run-wallet', '--starting-balance', help='Starting balance, used for backtesting / hyperopt and dry-runs.', type=float, ), diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index bf36972c4..130743f68 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -4,6 +4,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_coin_value from freqtrade.state import RunMode @@ -22,9 +23,13 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ RunMode.BACKTEST: 'backtesting', RunMode.HYPEROPT: 'hyperoptimization', } - if (method in no_unlimited_runmodes.keys() and - config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT and - config['max_open_trades'] != float('inf')): + if method in no_unlimited_runmodes.keys(): + if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT + and config['stake_amount'] > config['dry_run_wallet']): + wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency']) + stake = round_coin_value(config['stake_amount'], config['stake_currency']) + raise OperationalException(f"Starting balance ({wallet}) " + f"is smaller than stake_amount {stake}.") pass # config['dry_run_wallet'] = config['stake_amount'] * \ # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 88447e490..a40a4fd83 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -214,9 +214,6 @@ class Configuration: self._args_to_config( config, argname='enable_protections', logstring='Parameter --enable-protections detected, enabling Protections. ...') - # Setting max_open_trades to infinite if -1 - if config.get('max_open_trades') == -1: - config['max_open_trades'] = float('inf') if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: config.update({'use_max_market_positions': False}) @@ -228,6 +225,9 @@ class Configuration: 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + # Setting max_open_trades to infinite if -1 + if config.get('max_open_trades') == -1: + config['max_open_trades'] = float('inf') if self.args.get('stake_amount', None): # Convert explicitly to float to support CLI argument for both unlimited and value diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9a4a3787a..13ffc1d25 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -128,7 +128,7 @@ class Backtesting: PairLocks.use_db = True Trade.use_db = True - def _set_strategy(self, strategy): + def _set_strategy(self, strategy: IStrategy): """ Load strategy into backtesting """ diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 354b3f6b0..4bbfe8a78 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -9,7 +9,6 @@ import pandas as pd import pytest from arrow import Arrow -from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting from freqtrade.configuration import TimeRange from freqtrade.data import history @@ -232,8 +231,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog) -def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT +def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -241,12 +239,23 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '2' ] - # TODO: does this test still make sense? conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) assert isinstance(conf, dict) + args = [ + 'backtesting', + '--config', 'config.json', + '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '0.5' + ] + with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"): + setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + def test_start(mocker, fee, default_conf, caplog) -> None: start_mock = MagicMock() diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 88a4cea2d..9ebdad2b5 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -12,7 +12,6 @@ import pytest from arrow import Arrow from filelock import Timeout -from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException @@ -130,8 +129,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo assert log_has('Parameter --print-all detected ...', caplog) -def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_conf) -> None: - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT +def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -139,11 +137,22 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--stake-amount', '1', + '--starting-balance', '2' ] - # TODO: does this test still make sense? conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) assert isinstance(conf, dict) + args = [ + 'hyperopt', + '--config', 'config.json', + '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '0.5' + ] + with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"): + setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + def test_hyperoptresolver(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) From f5bb5f56f1aeefe13075f418d3ea15f24969fdbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 19:53:29 +0100 Subject: [PATCH 32/44] Update documentation with backtesting compounding possibilities --- docs/backtesting.md | 15 ++++++++++++--- docs/bot-usage.md | 2 +- docs/configuration.md | 7 ++++--- docs/hyperopt.md | 2 +- freqtrade/commands/optimize_commands.py | 6 ------ 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 9fa9025d8..96911763e 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -49,7 +49,7 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] @@ -108,10 +108,19 @@ All profit calculations include fees, and freqtrade will use the exchange's defa !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. - Please read the [pairlists documentation](plugins.md#pairlists) for more information. - + Please read the [pairlists documentation](plugins.md#pairlists) for more information. To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. +### Starting balance + +Backtesting will require a starting balance, which can be given as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. +This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade. + +### Dynamic stake amount + +Backtesting supports [dynamic stake amount](configuration.md#dynamic-stake-amount) by configuring `stake_amount` as `"unlimited"`, which will split the starting balance into `max_open_trades` pieces. +Profits from early trades will result in subsequent higher stake amounts, resulting in compounding of profits over the backtesting period. + ### Example backtesting commands With 5 min candle (OHLCV) data (per default) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4ff6168a0..b65220722 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -67,7 +67,7 @@ optional arguments: --sd-notify Notify systemd service manager. --dry-run Enforce dry-run for trading (removes Exchange secrets and simulates trades). - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. diff --git a/docs/configuration.md b/docs/configuration.md index 663d9c5b2..2cc22d6ec 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -218,11 +218,12 @@ To allow the bot to trade all the available `stake_currency` in your account (mi "tradable_balance_ratio": 0.99, ``` -!!! Note - This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available). +!!! Tip "Compounding profits" + This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available), and will result in profit compounding. !!! Note "When using Dry-Run Mode" - When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. + When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. + It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. --8<-- "includes/pricing.md" diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ee3d75d0b..d6959b457 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -83,7 +83,7 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. -e INT, --epochs INT Specify number of epochs (default: 100). diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 130743f68..6323bc2b1 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -30,12 +30,6 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ stake = round_coin_value(config['stake_amount'], config['stake_currency']) raise OperationalException(f"Starting balance ({wallet}) " f"is smaller than stake_amount {stake}.") - pass - # config['dry_run_wallet'] = config['stake_amount'] * \ - # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) - - # logger.warning(f"Changing dry-run-wallet to {config['dry_run_wallet']} " - # "(max_open_trades * stake_amount).") return config From fb489c11c921b77bf6f029c59ecb71f4d8712486 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:07:02 +0100 Subject: [PATCH 33/44] Improve test-coverage of pairlocks --- tests/plugins/test_pairlocks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index dfcbff0ed..fce3a8cd1 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -73,9 +73,13 @@ def test_PairLocks(use_db): assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) if use_db: - assert len(PairLock.query.all()) > 0 + locks = PairLocks.get_all_locks() + locks_db = PairLock.query.all() + assert len(locks) == len(locks_db) + assert len(locks_db) > 0 else: # Nothing was pushed to the database + assert len(PairLocks.get_all_locks()) > 0 assert len(PairLock.query.all()) == 0 # Reset use-db variable PairLocks.reset_locks() From f65092459a39ccd7238550a9518f989bab41feb7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:14:25 +0100 Subject: [PATCH 34/44] Fix optimize_reports test --- tests/optimize/test_optimize_reports.py | 32 ++++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index ca6a4ab01..8119c732b 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -102,6 +102,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): # Above sample had no loosing trade assert strat_stats['max_drawdown'] == 0.0 + # Retry with losing trade results = {'DefStrat': { 'results': pd.DataFrame( {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], @@ -118,18 +119,31 @@ def test_generate_backtest_stats(default_conf, testdatadir): "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.ROI, + SellType.STOP_LOSS, SellType.FORCE_SELL] }), - 'config': default_conf} + 'config': default_conf, + 'locks': [], + 'final_balance': 1000.02, + 'backtest_start_time': Arrow.utcnow().int_timestamp, + 'backtest_end_time': Arrow.utcnow().int_timestamp, + } } - assert strat_stats['max_drawdown'] == 0.0 - assert strat_stats['drawdown_start'] == datetime(1970, 1, 1, tzinfo=timezone.utc) - assert strat_stats['drawdown_end'] == datetime(1970, 1, 1, tzinfo=timezone.utc) - assert strat_stats['drawdown_end_ts'] == 0 - assert strat_stats['drawdown_start_ts'] == 0 + stats = generate_backtest_stats(btdata, results, min_date, max_date) + assert isinstance(stats, dict) + assert 'strategy' in stats + assert 'DefStrat' in stats['strategy'] + assert 'strategy_comparison' in stats + strat_stats = stats['strategy']['DefStrat'] + + assert strat_stats['max_drawdown'] == 0.013803 + assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc) + assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc) + assert strat_stats['drawdown_end_ts'] == 1510699380000 + assert strat_stats['drawdown_start_ts'] == 1510697400000 assert strat_stats['pairlist'] == ['UNITTEST/BTC'] # Test storing stats From 324b9dbdff126f53470919398081b2374d30c8b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:31:21 +0100 Subject: [PATCH 35/44] Simplify wallet code --- freqtrade/optimize/backtesting.py | 3 +-- freqtrade/wallets.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 13ffc1d25..b9ae096e2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -115,8 +115,7 @@ class Backtesting: if self.config.get('enable_protections', False): self.protections = ProtectionManager(self.config) - self.wallets = Wallets(self.config, self.exchange) - self.wallets._log = False + self.wallets = Wallets(self.config, self.exchange, log=False) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index c2085641e..553f7c61d 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -27,15 +27,14 @@ class Wallet(NamedTuple): class Wallets: - def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: + def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None: self._config = config - self._log = True + self._log = log self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] self._last_wallet_refresh = 0 - if not skip_update: - self.update() + self.update() def get_free(self, currency: str) -> float: balance = self._wallets.get(currency) From 6018a0534367bff3895778a50cec945d03d1a0a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:45:22 +0100 Subject: [PATCH 36/44] Improve backtest documentation --- docs/backtesting.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 96911763e..29ddb494b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -108,12 +108,13 @@ All profit calculations include fees, and freqtrade will use the exchange's defa !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. - Please read the [pairlists documentation](plugins.md#pairlists) for more information. + Please read the [pairlists documentation](plugins.md#pairlists) for more information. + To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. ### Starting balance -Backtesting will require a starting balance, which can be given as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. +Backtesting will require a starting balance, which can be provided as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade. ### Dynamic stake amount @@ -281,7 +282,7 @@ A backtesting result will look like that: | Absolute profit | 0.00762792 BTC | | Total profit % | 76.2% | | Trades per day | 3.575 | -| Avg. stake amount | 0.001 | +| Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -368,7 +369,7 @@ It contains some useful key metrics about performance of your strategy on backte | Absolute profit | 0.00762792 BTC | | Total profit % | 76.2% | | Trades per day | 3.575 | -| Avg. stake amount | 0.001 | +| Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -398,7 +399,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. - `Starting balance`: Start balance - as given by dry-run-wallet (config or command line). -- `End balance`: Final balance - starting balance + absolute profit. +- `Final balance`: Final balance - starting balance + absolute profit. - `Absolute profit`: Profit made in stake currency. - `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). From b2e9295d7f86688e40278ebe253c81b7b6a6450e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 19:57:42 +0100 Subject: [PATCH 37/44] Small stylistic fixes --- freqtrade/optimize/backtesting.py | 1 - freqtrade/persistence/models.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b9ae096e2..1b6d2e89c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -274,7 +274,6 @@ class Backtesting: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): - # print(f"{pair}, {stake_amount}") # Enter trade trade = LocalTrade( pair=pair, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 51a48c246..3a6474696 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -652,9 +652,8 @@ class LocalTrade(): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ - .filter(Trade.is_open.is_(True))\ - .scalar() + total_open_stake_amount = Trade.session.query( + func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() else: total_open_stake_amount = sum( t.stake_amount for t in Trade.get_trades_proxy(is_open=True)) @@ -719,6 +718,8 @@ class Trade(_DECL_BASE, LocalTrade): """ Trade database model. Also handles updating and querying trades + + Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' From d9d5617432cc991a2de976f8e84cb107913ea0d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 20:26:13 +0100 Subject: [PATCH 38/44] UPdate backtesting doc for total profit calc --- docs/backtesting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 29ddb494b..2e91b6e74 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -322,8 +322,8 @@ The bot has made `429` trades for an average duration of `4:12:00`, with a perfo earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses. -The column `Tot Profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). -In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `Tot Profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. +The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. +In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`. Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set. From 9cb37409fda6b0d9235ec7069489fbd062f0b873 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Feb 2021 09:56:29 +0100 Subject: [PATCH 39/44] Explicitly convert starting-balance to float --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index e7111f20c..0de0c16a0 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -277,7 +277,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), - 'total_volume': results['stake_amount'].sum(), + 'total_volume': float(results['stake_amount'].sum()), 'avg_stake_amount': results['stake_amount'].mean(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, From 55a315be14c00072983bd15e5e6c1ec21ce430c3 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 2 Mar 2021 13:34:14 +0100 Subject: [PATCH 40/44] fix: avg_stake_amount should not be `NaN` if df is empty --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0de0c16a0..47ddfc9fc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -278,7 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'total_volume': float(results['stake_amount'].sum()), - 'avg_stake_amount': results['stake_amount'].mean(), + 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), From 078b77d41be278208b9bc4fb5ddfe224aa562e68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Mar 2021 16:12:22 +0100 Subject: [PATCH 41/44] Fix crash when using unlimited stake and no trades are made --- freqtrade/optimize/optimize_reports.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 47ddfc9fc..52ae09ad1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -8,7 +8,7 @@ from numpy import int64 from pandas import DataFrame from tabulate import tabulate -from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN +from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change, calculate_max_drawdown) from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value @@ -499,8 +499,10 @@ def text_table_add_metrics(strat_results: Dict) -> str: else: start_balance = round_coin_value(strat_results['starting_balance'], strat_results['stake_currency']) - stake_amount = round_coin_value(strat_results['stake_amount'], - strat_results['stake_currency']) + stake_amount = round_coin_value( + strat_results['stake_amount'], strat_results['stake_currency'] + ) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited' + message = ("No trades made. " f"Your starting balance was {start_balance}, " f"and your stake was {stake_amount}." From bc05d03126aa7a9622b2fa75552c1e026c17d0f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 19:21:09 +0100 Subject: [PATCH 42/44] Make best / worst day absolute --- docs/backtesting.md | 10 +++++----- freqtrade/optimize/optimize_reports.py | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2e91b6e74..d02c59f05 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -289,8 +289,8 @@ A backtesting result will look like that: | Worst Pair | ZEC/BTC -10.18% | | Best Trade | LSK/BTC 4.25% | | Worst Trade | ZEC/BTC -10.25% | -| Best day | 25.27% | -| Worst day | -30.67% | +| Best day | 0.00076 BTC | +| Worst day | -0.00036 BTC | | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | @@ -376,8 +376,8 @@ It contains some useful key metrics about performance of your strategy on backte | Worst Pair | ZEC/BTC -10.18% | | Best Trade | LSK/BTC 4.25% | | Worst Trade | ZEC/BTC -10.25% | -| Best day | 25.27% | -| Worst day | -30.67% | +| Best day | 0.00076 BTC | +| Worst day | -0.00036 BTC | | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | @@ -406,7 +406,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. -- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade +- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade. - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 52ae09ad1..099976aa9 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -196,13 +196,18 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: return { 'backtest_best_day': 0, 'backtest_worst_day': 0, + 'backtest_best_day_abs': 0, + 'backtest_worst_day_abs': 0, 'winning_days': 0, 'draw_days': 0, 'losing_days': 0, 'winner_holding_avg': timedelta(), 'loser_holding_avg': timedelta(), } - daily_profit = results.resample('1d', on='close_date')['profit_ratio'].sum() + daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum() + daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10) + worst_rel = min(daily_profit_rel) + best_rel = max(daily_profit_rel) worst = min(daily_profit) best = max(daily_profit) winning_days = sum(daily_profit > 0) @@ -213,8 +218,10 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: losing_trades = results.loc[results['profit_ratio'] < 0] return { - 'backtest_best_day': best, - 'backtest_worst_day': worst, + 'backtest_best_day': best_rel, + 'backtest_worst_day': worst_rel, + 'backtest_best_day_abs': best, + 'backtest_worst_day_abs': worst, 'winning_days': winning_days, 'draw_days': draw_days, 'losing_days': losing_days, @@ -470,8 +477,10 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Worst trade', f"{worst_trade['pair']} " f"{round(worst_trade['profit_ratio'] * 100, 2)}%"), - ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), - ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), + ('Best day', round_coin_value(strat_results['backtest_best_day_abs'], + strat_results['stake_currency'])), + ('Worst day', round_coin_value(strat_results['backtest_worst_day_abs'], + strat_results['stake_currency'])), ('Days win/draw/lose', f"{strat_results['winning_days']} / " f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), From 0b81b58d287cad3ffbd628ec221abd44f53b41ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Mar 2021 11:28:54 +0100 Subject: [PATCH 43/44] Use pandas.values.tolist instead of itertuples speeds up backtesting closes #4494 --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1b6d2e89c..bb90fedce 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -206,7 +206,7 @@ class Backtesting: # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)] + data[pair] = df_analyzed.values.tolist() return data def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, From 4b550dab17d8121dd79545f6d809f38e67f0a5f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Mar 2021 19:40:29 +0100 Subject: [PATCH 44/44] Always reset fake-databases Otherwise results may stick around for the next strategy --- freqtrade/optimize/backtesting.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bb90fedce..aa289dc2b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -174,10 +174,8 @@ class Backtesting: PairLocks.use_db = False PairLocks.timeframe = self.config['timeframe'] Trade.use_db = False - if enable_protections: - # Reset persisted data - used for protections only - PairLocks.reset_locks() - Trade.reset_trades() + PairLocks.reset_locks() + Trade.reset_trades() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """