From 935e8f49de98bbddc4504e9dd9860997d67a620f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Apr 2024 15:36:26 +0200 Subject: [PATCH 1/5] Type-check fee from configuration ... --- freqtrade/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 37e2d849c..b4ab907ff 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -171,6 +171,7 @@ CONF_SCHEMA = { 'use_exit_signal': {'type': 'boolean'}, 'exit_profit_only': {'type': 'boolean'}, 'exit_profit_offset': {'type': 'number'}, + 'fee': {'type': 'number', 'minimum': 0, 'maximum': 0.1}, 'ignore_roi_if_entry_signal': {'type': 'boolean'}, 'ignore_buying_expired_candle_after': {'type': 'number'}, 'trading_mode': {'type': 'string', 'enum': TRADING_MODES}, From 3f2f2a1dbd739f9816a14bd778cac3c5b9c46073 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Apr 2024 15:51:58 +0200 Subject: [PATCH 2/5] Use worst case of maker / taker fee for backtest --- freqtrade/optimize/backtesting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1952c4fe7..2b7e048bd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -134,8 +134,12 @@ class Backtesting: if config.get('fee', None) is not None: self.fee = config['fee'] + logger.info(f"Using fee {self.fee:.4%} from config.") else: - self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) + fees = [self.exchange.get_fee(symbol=self.pairlists.whitelist[0], taker_or_maker=mot) + for mot in ('taker', 'maker')] + self.fee = max(fee for fee in fees if fee is not None) + logger.info(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).") self.precision_mode = self.exchange.precisionMode if self.config.get('freqai_backtest_live_models', False): From 3a2e3215b9dba5a150f62c03b57d627a33ed7e9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Apr 2024 18:26:43 +0200 Subject: [PATCH 3/5] Ensure get_fee returns something in tests --- tests/conftest.py | 1 + tests/exchange/test_exchange.py | 2 +- tests/optimize/test_backtest_detail.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b46f30f8f..93fc8a70c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -241,6 +241,7 @@ def patch_exchange( if api_mock: mocker.patch(f'{EXMS}._init_ccxt', return_value=api_mock) else: + mocker.patch(f'{EXMS}.get_fee', return_value=0.025) mocker.patch(f'{EXMS}._init_ccxt', MagicMock()) mocker.patch(f'{EXMS}.timeframes', PropertyMock( return_value=['5m', '15m', '1h', '1d'])) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c8293965a..e3440ad9b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1121,12 +1121,12 @@ def test_create_dry_run_order_fees( price_side, fee, ): + exchange = get_patched_exchange(mocker, default_conf) mocker.patch( f'{EXMS}.get_fee', side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 ) mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=price_side == 'other') - exchange = get_patched_exchange(mocker, default_conf) order = exchange.create_dry_run_order( pair='LTC/USDT', diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 54468910c..21f2d069c 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -921,12 +921,12 @@ def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> No default_conf["use_exit_signal"] = data.use_exit_signal default_conf["max_open_trades"] = 10 + patch_exchange(mocker) mocker.patch(f"{EXMS}.get_fee", return_value=0.0) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch(f"{EXMS}.get_max_leverage", return_value=100) mocker.patch(f"{EXMS}.calculate_funding_fees", return_value=0) - patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) # TODO: Should we initialize this properly?? From f259270e9c7da7d8266ee1ce8c06d96b4dc1aa91 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Apr 2024 19:52:48 +0200 Subject: [PATCH 4/5] Update tests to properly mock fee --- tests/conftest.py | 2 +- tests/optimize/test_backtesting.py | 29 ++++++++++------------- tests/optimize/test_edge_cli.py | 4 ++-- tests/optimize/test_lookahead_analysis.py | 2 +- tests/optimize/test_recursive_analysis.py | 5 ++-- tests/rpc/test_rpc.py | 2 +- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 93fc8a70c..4eabbe7fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -241,7 +241,7 @@ def patch_exchange( if api_mock: mocker.patch(f'{EXMS}._init_ccxt', return_value=api_mock) else: - mocker.patch(f'{EXMS}.get_fee', return_value=0.025) + mocker.patch(f'{EXMS}.get_fee', return_value=0.0025) mocker.patch(f'{EXMS}._init_ccxt', MagicMock()) mocker.patch(f'{EXMS}.timeframes', PropertyMock( return_value=['5m', '15m', '1h', '1d'])) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4163e9606..64dfd1613 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -298,12 +298,12 @@ def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: def test_data_with_fee(default_conf, mocker) -> None: patch_exchange(mocker) - default_conf['fee'] = 0.1234 + default_conf['fee'] = 0.01234 fee_mock = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - assert backtesting.fee == 0.1234 + assert backtesting.fee == 0.01234 assert fee_mock.call_count == 0 default_conf['fee'] = 0.0 @@ -620,12 +620,11 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: assert trade is None -def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None: +def test_backtest__check_trade_exit(default_conf, mocker) -> None: default_conf['use_exit_signal'] = False - mocker.patch(f'{EXMS}.get_fee', fee) + patch_exchange(mocker) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) - patch_exchange(mocker) default_conf['timeframe_detail'] = '1m' default_conf['max_open_trades'] = 2 backtesting = Backtesting(default_conf) @@ -683,14 +682,13 @@ def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None: assert res is None -def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: +def test_backtest_one(default_conf, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False default_conf['max_open_trades'] = 10 - mocker.patch(f'{EXMS}.get_fee', fee) + patch_exchange(mocker) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) - patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) pair = 'UNITTEST/BTC' @@ -777,14 +775,13 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: @pytest.mark.parametrize('use_detail', [True, False]) -def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None: +def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail) -> None: default_conf_usdt['use_exit_signal'] = False - mocker.patch(f'{EXMS}.get_fee', fee) + patch_exchange(mocker) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) if use_detail: default_conf_usdt['timeframe_detail'] = '1m' - patch_exchange(mocker) def advise_entry(df, *args, **kwargs): # Mock function to force several entries @@ -864,14 +861,14 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de (False, -0.01780296, 5), ]) def test_backtest_one_detail_futures( - default_conf_usdt, fee, mocker, testdatadir, use_detail, exp_funding_fee, + default_conf_usdt, mocker, testdatadir, use_detail, exp_funding_fee, exp_ff_updates) -> None: default_conf_usdt['use_exit_signal'] = False default_conf_usdt['trading_mode'] = 'futures' default_conf_usdt['margin_mode'] = 'isolated' default_conf_usdt['candle_type_def'] = CandleType.FUTURES - mocker.patch(f'{EXMS}.get_fee', fee) + patch_exchange(mocker) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', @@ -881,7 +878,6 @@ def test_backtest_one_detail_futures( default_conf_usdt['timeframe'] = '1h' if use_detail: default_conf_usdt['timeframe_detail'] = '5m' - patch_exchange(mocker) def advise_entry(df, *args, **kwargs): # Mock function to force several entries @@ -1237,19 +1233,18 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad ([{"method": "CooldownPeriod", "stop_duration": 3}], 'sine', 9), ([{"method": "CooldownPeriod", "stop_duration": 3}], 'raise', 10), ]) -def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, +def test_backtest_pricecontours(default_conf, mocker, testdatadir, protections, contour, expected) -> None: if protections: default_conf['protections'] = protections default_conf['enable_protections'] = True + patch_exchange(mocker) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch(f'{EXMS}.get_fee', fee) # While entry-signals are unrealistic, running backtesting # over and over again should not cause different results - patch_exchange(mocker) default_conf['timeframe'] = '1m' backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 64172bf1c..3a059fce5 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -99,11 +99,11 @@ def test_edge_init(mocker, edge_conf) -> None: def test_edge_init_fee(mocker, edge_conf) -> None: patch_exchange(mocker) - edge_conf['fee'] = 0.1234 + edge_conf['fee'] = 0.01234 edge_conf['stake_amount'] = 20 fee_mock = mocker.patch(f'{EXMS}.get_fee', return_value=0.5) edge_cli = EdgeCli(edge_conf) - assert edge_cli.edge.fee == 0.1234 + assert edge_cli.edge.fee == 0.01234 assert fee_mock.call_count == 0 diff --git a/tests/optimize/test_lookahead_analysis.py b/tests/optimize/test_lookahead_analysis.py index 6c84663b6..c19a4ba4d 100644 --- a/tests/optimize/test_lookahead_analysis.py +++ b/tests/optimize/test_lookahead_analysis.py @@ -343,11 +343,11 @@ def test_initialize_single_lookahead_analysis(lookahead_conf, mocker, caplog): 'no_bias', 'bias1' ]) def test_biased_strategy(lookahead_conf, mocker, caplog, scenario) -> None: + patch_exchange(mocker) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch(f'{EXMS}.get_fee', return_value=0.0) mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.00001) mocker.patch(f'{EXMS}.get_max_pair_stake_amount', return_value=float('inf')) - patch_exchange(mocker) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) lookahead_conf['pairs'] = ['UNITTEST/USDT'] diff --git a/tests/optimize/test_recursive_analysis.py b/tests/optimize/test_recursive_analysis.py index 95a96c9f5..f02356e56 100644 --- a/tests/optimize/test_recursive_analysis.py +++ b/tests/optimize/test_recursive_analysis.py @@ -10,7 +10,7 @@ from freqtrade.data.history import get_timerange from freqtrade.exceptions import OperationalException from freqtrade.optimize.analysis.recursive import RecursiveAnalysis from freqtrade.optimize.analysis.recursive_helpers import RecursiveAnalysisSubFunctions -from tests.conftest import get_args, log_has_re, patch_exchange +from tests.conftest import EXMS, get_args, log_has_re, patch_exchange @pytest.fixture @@ -152,8 +152,9 @@ def test_initialize_single_recursive_analysis(recursive_conf, mocker, caplog): 'no_bias', 'bias1', 'bias2' ]) def test_recursive_biased_strategy(recursive_conf, mocker, caplog, scenario) -> None: - mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) + mocker.patch(f'{EXMS}.get_fee', return_value=0.0) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) recursive_conf['pairs'] = ['UNITTEST/BTC'] diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5c8602c2f..427b46dfa 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -103,6 +103,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'funding_fee': ANY, 'ft_order_tag': None, }], } + freqtradebot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( EXMS, @@ -111,7 +112,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: _dry_is_price_crossed=MagicMock(side_effect=[False, True]), ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) From 997db6c70620961efc28078f18ff76d04af8f67a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Apr 2024 19:59:53 +0200 Subject: [PATCH 5/5] Type-ignore we can't type variables of the list-comprehension ... --- freqtrade/optimize/backtesting.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2b7e048bd..ee85ac711 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -136,8 +136,13 @@ class Backtesting: self.fee = config['fee'] logger.info(f"Using fee {self.fee:.4%} from config.") else: - fees = [self.exchange.get_fee(symbol=self.pairlists.whitelist[0], taker_or_maker=mot) - for mot in ('taker', 'maker')] + fees = [ + self.exchange.get_fee( + symbol=self.pairlists.whitelist[0], + taker_or_maker=mt, # type: ignore + ) + for mt in ('taker', 'maker') + ] self.fee = max(fee for fee in fees if fee is not None) logger.info(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).") self.precision_mode = self.exchange.precisionMode