From 966eb59fd303a9261880f59b2bc56879f688829b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Dec 2023 11:52:40 +0100 Subject: [PATCH 1/8] Extract funding fee calculation to separate method --- freqtrade/optimize/backtesting.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d5caf7070..bbafd0650 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -725,16 +725,7 @@ class Backtesting: self, trade: LocalTrade, row: Tuple, current_time: datetime ) -> Optional[LocalTrade]: - if self.trading_mode == TradingMode.FUTURES: - trade.set_funding_fees( - self.exchange.calculate_funding_fees( - self.futures_data[trade.pair], - amount=trade.amount, - is_short=trade.is_short, - open_date=trade.date_last_filled_utc, - close_date=current_time - ) - ) + self._run_funding_fees(trade, current_time) # Check if we need to adjust our current positions if self.strategy.position_adjustment_enable: @@ -753,6 +744,21 @@ class Backtesting: return t return None + def _run_funding_fees(self, trade: Trade, current_time: datetime): + """ + Calculate funding fees if necessary and add them to the trade. + """ + if self.trading_mode == TradingMode.FUTURES: + trade.set_funding_fees( + self.exchange.calculate_funding_fees( + self.futures_data[trade.pair], + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.date_last_filled_utc, + close_date=current_time + ) + ) + def get_valid_price_and_stake( self, pair: str, row: Tuple, propose_rate: float, stake_amount: float, direction: LongShort, current_time: datetime, entry_tag: Optional[str], From 074343f0f1f365afb176ce0b28b9d25f40394eae Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Dec 2023 13:23:40 +0100 Subject: [PATCH 2/8] Don't calculate funding_fees on every iteration --- freqtrade/optimize/backtesting.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bbafd0650..77efbf35c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -283,11 +283,13 @@ class Backtesting: else: self.detail_data = {} if self.trading_mode == TradingMode.FUTURES: + self.funding_fee_timeframe: str = self.exchange.get_option('mark_ohlcv_timeframe') + self.funding_fee_timeframe_secs: int = timeframe_to_seconds(self.funding_fee_timeframe) # Load additional futures data. funding_rates_dict = history.load_data( datadir=self.config['datadir'], pairs=self.pairlists.whitelist, - timeframe=self.exchange.get_option('mark_ohlcv_timeframe'), + timeframe=self.funding_fee_timeframe, timerange=self.timerange, startup_candles=0, fail_without_data=True, @@ -299,7 +301,7 @@ class Backtesting: mark_rates_dict = history.load_data( datadir=self.config['datadir'], pairs=self.pairlists.whitelist, - timeframe=self.exchange.get_option('mark_ohlcv_timeframe'), + timeframe=self.funding_fee_timeframe, timerange=self.timerange, startup_candles=0, fail_without_data=True, @@ -744,20 +746,26 @@ class Backtesting: return t return None - def _run_funding_fees(self, trade: Trade, current_time: datetime): + def _run_funding_fees(self, trade: Trade, current_time: datetime, force: bool = False): """ Calculate funding fees if necessary and add them to the trade. """ if self.trading_mode == TradingMode.FUTURES: - trade.set_funding_fees( - self.exchange.calculate_funding_fees( - self.futures_data[trade.pair], - amount=trade.amount, - is_short=trade.is_short, - open_date=trade.date_last_filled_utc, - close_date=current_time + + if ( + force + or (current_time.timestamp() % self.funding_fee_timeframe_secs) == 0 + ): + # Funding fee interval. + trade.set_funding_fees( + self.exchange.calculate_funding_fees( + self.futures_data[trade.pair], + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.date_last_filled_utc, + close_date=current_time + ) ) - ) def get_valid_price_and_stake( self, pair: str, row: Tuple, propose_rate: float, stake_amount: float, From ef23f0fcbac3795ca2d60cce9941fa35b0214d31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Dec 2023 13:47:17 +0100 Subject: [PATCH 3/8] Fix test to account for new funding_fee count --- tests/optimize/test_backtesting.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index b1e2a47ad..c6941d7c7 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -947,12 +947,13 @@ def test_backtest_one_detail_futures( # assert late_entry > 0 -@pytest.mark.parametrize('use_detail,entries,max_stake,expected_ff', [ - (True, 50, 3000, -1.18038144), - (False, 6, 360, -0.14679994)]) +@pytest.mark.parametrize('use_detail,entries,max_stake,ff_updates,expected_ff', [ + (True, 50, 3000, 54, -1.18038144), + (False, 6, 360, 10, -0.14679994), +]) def test_backtest_one_detail_futures_funding_fees( default_conf_usdt, fee, mocker, testdatadir, use_detail, entries, max_stake, - expected_ff, + ff_updates, expected_ff, ) -> None: """ Funding fees are expected to differ, as the maximum position size differs. @@ -1015,8 +1016,10 @@ def test_backtest_one_detail_futures_funding_fees( assert len(results) == 1 assert 'orders' in results.columns - # funding_fees have been calculated for each candle - assert ff_spy.call_count == (324 if use_detail else 27) + # funding_fees have been calculated for each funding-fee candle + # the trade is open for 26 hours - hence we expect the 8h fee to apply 4 times. + # Additional counts will happen due each successful entry, which needs to call this, too. + assert ff_spy.call_count == ff_updates for t in Trade.trades: # At least 6 adjustment orders From 8964c138f15b2321cba878d89e4d62266cdbb5c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Dec 2023 13:47:43 +0100 Subject: [PATCH 4/8] Call funding fee calculation whenever a trade is closed --- freqtrade/optimize/backtesting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 77efbf35c..3ea1c5a78 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -606,6 +606,8 @@ class Backtesting: """ if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_date, trade) + self._run_funding_fees(trade, current_date, force=True) + if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): # trade is still open trade.set_liquidation_price(self.exchange.get_liquidation_price( From 3f6cd9ee5119942286ff9f14e2f03b5c1ce2c5f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Dec 2023 13:51:18 +0100 Subject: [PATCH 5/8] Patch funding_fee calculation for unrelated test --- tests/optimize/test_backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index c6941d7c7..17d1c8192 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -549,6 +549,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: default_conf_usdt['exchange']['pair_whitelist'] = ['.*'] backtesting = Backtesting(default_conf_usdt) backtesting._set_strategy(backtesting.strategylist[0]) + mocker.patch('freqtrade.optimize.backtesting.Backtesting._run_funding_fees') pair = 'ETH/USDT:USDT' row = [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), From 0bd513012a6cf1165ec3affd24afb28090092792 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Dec 2023 13:57:19 +0100 Subject: [PATCH 6/8] enhance further test for funding-fee checking --- tests/optimize/test_backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 17d1c8192..fd594e94b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -853,8 +853,8 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de @pytest.mark.parametrize('use_detail,exp_funding_fee, exp_ff_updates', [ - (True, -0.018054162, 44), - (False, -0.01780296, 8), + (True, -0.018054162, 11), + (False, -0.01780296, 5), ]) def test_backtest_one_detail_futures( default_conf_usdt, fee, mocker, testdatadir, use_detail, exp_funding_fee, From eac5d53a647c7f955704d3ee7ed54bbd559d019c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Dec 2023 06:48:05 +0100 Subject: [PATCH 7/8] Add mock to backtest adjust position --- tests/optimize/test_backtesting_adjust_position.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index ad1f31068..9b40b3a9d 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -104,6 +104,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.1, 0.1)) + mocker.patch('freqtrade.optimize.backtesting.Backtesting._run_funding_fees') patch_exchange(mocker) default_conf.update({ From 30f94ef5b77b0f52b99c0da7652f72a1311c1fc0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Dec 2023 19:12:08 +0100 Subject: [PATCH 8/8] Use LocalTrade for typehint --- 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 3ea1c5a78..36673d2e8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -748,7 +748,7 @@ class Backtesting: return t return None - def _run_funding_fees(self, trade: Trade, current_time: datetime, force: bool = False): + def _run_funding_fees(self, trade: LocalTrade, current_time: datetime, force: bool = False): """ Calculate funding fees if necessary and add them to the trade. """