From 150c5510c74390641fd723f6a70b5f67f53ec9cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Mar 2023 19:46:07 +0100 Subject: [PATCH 1/3] Don''t fully fail bot when invalid price value is reached closes #8300 --- freqtrade/exchange/exchange.py | 6 +++++- freqtrade/freqtradebot.py | 14 +++++++++----- tests/exchange/test_binance.py | 4 ++-- tests/exchange/test_huobi.py | 6 +++--- tests/exchange/test_kucoin.py | 6 +++--- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 99551b054..104eaa221 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1135,7 +1135,11 @@ class Exchange: "sell" else (stop_price >= limit_rate)) # Ensure rate is less than stop price if bad_stop_price: - raise OperationalException( + # This can for example happen if the stop / liquidation price is set to 0 + # Which is possible if a market-order closes right away. + # The InvalidOrderException will bubble up to exit_positions, where it will be + # handled gracefully. + raise InvalidOrderException( "In stoploss limit order, stop price should be more than limit price. " f"Stop price: {stop_price}, Limit price: {limit_rate}, " f"Limit Price pct: {limit_price_pct}" diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 06c8831f5..00bfc1ee2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1021,12 +1021,16 @@ class FreqtradeBot(LoggingMixin): trades_closed = 0 for trade in trades: try: + try: + if (self.strategy.order_types.get('stoploss_on_exchange') and + self.handle_stoploss_on_exchange(trade)): + trades_closed += 1 + Trade.commit() + continue - if (self.strategy.order_types.get('stoploss_on_exchange') and - self.handle_stoploss_on_exchange(trade)): - trades_closed += 1 - Trade.commit() - continue + except InvalidOrderException as exception: + logger.warning( + f'Unable to handle stoploss on exchange for {trade.pair}: {exception}') # Check if we can sell our current pair if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): trades_closed += 1 diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 8ada089bd..273860e15 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -52,7 +52,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss( pair='ETH/BTC', amount=1, @@ -131,7 +131,7 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss( pair='ETH/BTC', amount=1, diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py index 5e4fd7316..85d2ced9d 100644 --- a/tests/exchange/test_huobi.py +++ b/tests/exchange/test_huobi.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -31,7 +31,7 @@ def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss_on_exchange_limit_ratio': 1.05}, side=side, @@ -84,7 +84,7 @@ def test_create_stoploss_order_dry_run_huobi(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss_on_exchange_limit_ratio': 1.05}, side='sell', leverage=1.0) diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index e0bb32b7c..07f3fb6a3 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -31,7 +31,7 @@ def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') if order_type == 'limit': - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={ 'stoploss': order_type, @@ -92,7 +92,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss': 'limit', 'stoploss_on_exchange_limit_ratio': 1.05}, From 469166636c9794afb309875d87be66e76518430e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 07:27:45 +0100 Subject: [PATCH 2/3] Set initial stoploss when creating the order This ensures that a trade never has "None" as stoploss --- freqtrade/freqtradebot.py | 3 +++ tests/rpc/test_rpc.py | 11 ----------- tests/test_integration.py | 18 +++++++++--------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 00bfc1ee2..9d402b6a6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -810,6 +810,9 @@ class FreqtradeBot(LoggingMixin): precision_mode=self.exchange.precisionMode, contract_size=self.exchange.get_contract_size(pair), ) + stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair) + trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True) + else: # This is additional buy, we reset fee_open_currency so timeout checking can work trade.is_open = True diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 7d829bdb6..4e2dc94ae 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -125,17 +125,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_pct': 0.0, 'profit_abs': 0.0, 'total_profit_abs': 0.0, - 'stop_loss_abs': 0.0, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, - 'stoploss_current_dist': -1.099e-05, - 'stoploss_current_dist_ratio': -1.0, - 'stoploss_current_dist_pct': pytest.approx(-100.0), - 'stoploss_entry_dist': -0.0010025, - 'stoploss_entry_dist_ratio': -1.0, - 'initial_stop_loss_abs': 0.0, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, 'open_order': '(limit buy rem=91.07468123)', }) response_unfilled['orders'][0].update({ diff --git a/tests/test_integration.py b/tests/test_integration.py index 922285309..5cbedd818 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -386,12 +386,12 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert trade.open_order_id is not None assert pytest.approx(trade.stake_amount) == 60 assert trade.open_rate == 1.96 - assert trade.stop_loss_pct is None - assert trade.stop_loss == 0.0 + assert trade.stop_loss_pct == -0.1 + assert pytest.approx(trade.stop_loss) == trade.open_rate * (1 - 0.1 / leverage) + assert pytest.approx(trade.initial_stop_loss) == trade.open_rate * (1 - 0.1 / leverage) + assert trade.initial_stop_loss_pct == -0.1 assert trade.leverage == leverage assert trade.stake_amount == 60 - assert trade.initial_stop_loss == 0.0 - assert trade.initial_stop_loss_pct is None # No adjustment freqtrade.process() trade = Trade.get_trades().first() @@ -407,11 +407,11 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert trade.open_order_id is not None # Open rate is not adjusted yet assert trade.open_rate == 1.96 - assert trade.stop_loss_pct is None - assert trade.stop_loss == 0.0 + assert trade.stop_loss_pct == -0.1 + assert pytest.approx(trade.stop_loss) == trade.open_rate * (1 - 0.1 / leverage) + assert pytest.approx(trade.initial_stop_loss) == trade.open_rate * (1 - 0.1 / leverage) assert trade.stake_amount == 60 - assert trade.initial_stop_loss == 0.0 - assert trade.initial_stop_loss_pct is None + assert trade.initial_stop_loss_pct == -0.1 # Fill order mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) @@ -424,7 +424,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert pytest.approx(trade.stake_amount) == 60 assert trade.stop_loss_pct == -0.1 assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage) - assert pytest.approx(trade.initial_stop_loss) == 1.99 * (1 - 0.1 / leverage) + assert pytest.approx(trade.initial_stop_loss) == 1.96 * (1 - 0.1 / leverage) assert trade.initial_stop_loss_pct == -0.1 # 2nd order - not filling From b317524ed70883a5d440827aeb52434b57a1602e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 20:27:45 +0100 Subject: [PATCH 3/3] protect adjust_trade_position from crashing in case of unsafe code --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9d402b6a6..623d39c09 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -594,7 +594,7 @@ class FreqtradeBot(LoggingMixin): stake_available = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None)( + default_retval=None, supress_error=True)( trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_entry_rate, current_profit=current_entry_profit, min_stake=min_entry_stake, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 315b3b9db..fe6667ad9 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -522,7 +522,7 @@ class Backtesting: max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) stake_available = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None)( + default_retval=None, supress_error=True)( trade=trade, # type: ignore[arg-type] current_time=current_date, current_rate=current_rate, current_profit=current_profit, min_stake=min_stake,