From 89aae71c32c79d3cb3e21382ad237f86406d5cb2 Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Tue, 15 Mar 2022 11:41:39 +0530 Subject: [PATCH 01/26] correcting docs for pricing of ask strategy --- docs/includes/pricing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md index ed8a45e68..4505d7fec 100644 --- a/docs/includes/pricing.md +++ b/docs/includes/pricing.md @@ -88,9 +88,9 @@ When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Fr #### Sell price without Orderbook enabled -When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. +The following section uses `side` as the configured `ask_strategy.price_side`. -When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. +When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price. The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price. From f03f586eeb829314d3ba491be706d43408f50747 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 21 Mar 2022 05:01:18 -0600 Subject: [PATCH 02/26] funding_fee tests --- tests/exchange/test_exchange.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9bb9db58f..aed58a368 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3877,6 +3877,7 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf): @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ + ('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0), ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 1, 2, "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0002493), @@ -3891,9 +3892,11 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf): # ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0012443999999999999), # ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759), # ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289), + ('ftx', 0, 2, "2021-09-01 00:10:00", "2021-09-01 00:30:00", 30.0, 0.0), ('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008000000000003), ('ftx', 0, 13, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691), ('ftx', 1, 9, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0016656000000000002), + ('gateio', 0, 2, "2021-09-01 00:10:00", "2021-09-01 04:00:00", 30.0, 0.0), ('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('gateio', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), @@ -3966,7 +3969,7 @@ def test__fetch_and_calculate_funding_fees( exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( - return_value=['1h', '4h', '8h'])) + return_value=['1h', '4h', '8h'])) funding_fees = exchange._fetch_and_calculate_funding_fees( pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == expected_fees From 2c89da6bf7b5c07c1a93a9ec76610857c931681f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 21 Mar 2022 19:30:24 +0100 Subject: [PATCH 03/26] Update code to properly behave when rounding open_date for funding fees --- freqtrade/exchange/exchange.py | 3 +-- tests/exchange/test_exchange.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fe47ca4d1..93190fb2a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2302,11 +2302,10 @@ class Exchange: timeframe = self._ft_has['mark_ohlcv_timeframe'] timeframe_ff = self._ft_has.get('funding_fee_timeframe', self._ft_has['mark_ohlcv_timeframe']) - open_date = timeframe_to_prev_date(timeframe, open_date) if not close_date: close_date = datetime.now(timezone.utc) - open_timestamp = int(open_date.timestamp()) * 1000 + open_timestamp = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000 # close_timestamp = int(close_date.timestamp()) * 1000 mark_comb: PairWithTimeframe = ( diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index aed58a368..265d24569 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3878,13 +3878,13 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf): @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ ('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0), - ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), - ('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), + ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.00091409999), + ('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', 1, 2, "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', 1, 2, "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493), - ('binance', 0, 1, "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999), - ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), - ('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), + ('binance', 0, 1, "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.00066479999), + ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.00091409999), + ('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0014937), # ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0008289), @@ -3893,17 +3893,17 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf): # ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759), # ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289), ('ftx', 0, 2, "2021-09-01 00:10:00", "2021-09-01 00:30:00", 30.0, 0.0), - ('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008000000000003), + ('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008), ('ftx', 0, 13, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691), - ('ftx', 1, 9, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0016656000000000002), + ('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.001668), + ('ftx', 1, 9, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0019932), ('gateio', 0, 2, "2021-09-01 00:10:00", "2021-09-01 04:00:00", 30.0, 0.0), - ('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), - ('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), + ('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999), + ('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999), ('gateio', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), - ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001), + ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235), # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895), - ('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002), ]) def test__fetch_and_calculate_funding_fees( mocker, From 247635db79896e07f64241b20a9b47cec28fe577 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Mar 2022 19:28:13 +0100 Subject: [PATCH 04/26] Fix tests --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2d6b46745..2d63cc77f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -276,7 +276,7 @@ class FreqtradeBot(LoggingMixin): pair=trade.pair, amount=trade.amount, is_short=trade.is_short, - open_date=trade.open_date + open_date=trade.open_date_utc ) trade.funding_fees = funding_fees else: @@ -1358,7 +1358,7 @@ class FreqtradeBot(LoggingMixin): pair=trade.pair, amount=trade.amount, is_short=trade.is_short, - open_date=trade.open_date, + open_date=trade.open_date_utc, ) exit_type = 'exit' if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6e8b1afbf..dc6c0b838 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -5034,9 +5034,9 @@ def test_update_funding_fees( default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' - date_midnight = arrow.get('2021-09-01 00:00:00') - date_eight = arrow.get('2021-09-01 08:00:00') - date_sixteen = arrow.get('2021-09-01 16:00:00') + date_midnight = arrow.get('2021-09-01 00:00:00').datetime + date_eight = arrow.get('2021-09-01 08:00:00').datetime + date_sixteen = arrow.get('2021-09-01 16:00:00').datetime columns = ['date', 'open', 'high', 'low', 'close', 'volume'] # 16:00 entry is actually never used # But should be kept in the test to ensure we're filtering correctly. @@ -5119,11 +5119,7 @@ def test_update_funding_fees( trades = Trade.get_open_trades() assert len(trades) == 3 for trade in trades: - assert pytest.approx(trade.funding_fees) == ( - trade.amount * - mark_prices[trade.pair].iloc[0]['open'] * - funding_rates[trade.pair].iloc[0]['open'] * multipl - ) + assert pytest.approx(trade.funding_fees) == 0 mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order) time_machine.move_to("2021-09-01 08:00:00 +00:00") if schedule_off: @@ -5136,8 +5132,8 @@ def test_update_funding_fees( ) assert trade.funding_fees == pytest.approx(sum( trade.amount * - mark_prices[trade.pair].iloc[0:2]['open'] * - funding_rates[trade.pair].iloc[0:2]['open'] * multipl + mark_prices[trade.pair].iloc[1:2]['open'] * + funding_rates[trade.pair].iloc[1:2]['open'] * multipl )) else: @@ -5147,8 +5143,8 @@ def test_update_funding_fees( for trade in trades: assert trade.funding_fees == pytest.approx(sum( trade.amount * - mark_prices[trade.pair].iloc[0:2]['open'] * - funding_rates[trade.pair].iloc[0:2]['open'] * + mark_prices[trade.pair].iloc[1:2]['open'] * + funding_rates[trade.pair].iloc[1:2]['open'] * multipl )) From dae9f4d877b921c935ea71a095ff85f9a0fffe80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Mar 2022 06:48:59 +0100 Subject: [PATCH 05/26] Update doc clarity, partially revert prior commit --- docs/includes/pricing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md index 4505d7fec..103df6cd3 100644 --- a/docs/includes/pricing.md +++ b/docs/includes/pricing.md @@ -51,9 +51,9 @@ When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Fre #### Buy price without Orderbook enabled -The following section uses `side` as the configured `bid_strategy.price_side`. +The following section uses `side` as the configured `bid_strategy.price_side` (defaults to `"bid"`). -When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. +When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `bid_strategy.ask_last_balance`.. The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. @@ -88,9 +88,9 @@ When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Fr #### Sell price without Orderbook enabled -The following section uses `side` as the configured `ask_strategy.price_side`. +The following section uses `side` as the configured `ask_strategy.price_side` (defaults to `"ask"`). -When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price. +When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `ask_strategy.bid_last_balance`. The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price. From 46ca773c25f6151c9271787732cb35b3eedb5d7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Mar 2022 19:58:53 +0100 Subject: [PATCH 06/26] Simplify some rpc code --- freqtrade/rpc/rpc.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 23132fcd7..753db0d25 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -156,7 +156,7 @@ class RPC: """ # Fetch open trades if trade_ids: - trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() + trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() else: trades = Trade.get_open_trades() @@ -171,9 +171,8 @@ class RPC: # calculate profit and send message to user if trade.is_open: try: - closing_side = trade.exit_side current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=closing_side) + trade.pair, refresh=False, side=trade.exit_side) except (ExchangeError, PricingError): current_rate = NAN else: @@ -223,7 +222,7 @@ class RPC: def _rpc_status_table(self, stake_currency: str, fiat_display_currency: str) -> Tuple[List, List, float]: - trades = Trade.get_open_trades() + trades: List[Trade] = Trade.get_open_trades() if not trades: raise RPCException('no active trade') else: @@ -232,9 +231,8 @@ class RPC: for trade in trades: # calculate profit and send message to user try: - closing_side = "buy" if trade.is_short else "sell" current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=closing_side) + trade.pair, refresh=False, side=trade.exit_side) except (PricingError, ExchangeError): current_rate = NAN trade_profit = trade.calc_profit(current_rate) @@ -458,7 +456,7 @@ class RPC: """ Returns cumulative profit statistics """ trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True)) - trades = Trade.get_trades(trade_filter).order_by(Trade.id).all() + trades: List[Trade] = Trade.get_trades(trade_filter).order_by(Trade.id).all() profit_all_coin = [] profit_all_ratio = [] @@ -487,9 +485,8 @@ class RPC: else: # Get current rate try: - closing_side = "buy" if trade.is_short else "sell" current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=closing_side) + trade.pair, refresh=False, side=trade.exit_side) except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) From d7f76ee452d6488a6d4e7e426667bf9dca5f729a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 06:37:40 +0100 Subject: [PATCH 07/26] Update confirm_trade_exit to use sell_reason terminology --- docs/strategy-advanced.md | 4 +-- docs/strategy-callbacks.md | 6 ++-- docs/strategy_migration.md | 29 +++++++++++++++++-- freqtrade/freqtradebot.py | 3 +- freqtrade/optimize/backtesting.py | 3 +- freqtrade/strategy/interface.py | 4 +-- .../subtemplates/strategy_methods_advanced.j2 | 4 +-- tests/strategy/test_default_strategy.py | 3 +- 8 files changed, 42 insertions(+), 14 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 74de614f6..374c675a2 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -49,7 +49,7 @@ from freqtrade.exchange import timeframe_to_prev_date class AwesomeStrategy(IStrategy): def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, + rate: float, time_in_force: str, exit_reason: str, current_time: 'datetime', **kwargs) -> bool: # Obtain pair dataframe. dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) @@ -125,7 +125,7 @@ def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame The provided exit-tag is then used as sell-reason - and shown as such in backtest results. !!! Note - `sell_reason` is limited to 100 characters, remaining data will be truncated. + `exit_reason` is limited to 100 characters, remaining data will be truncated. ## Strategy version diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 7f819d5d0..6474ffcaa 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -546,7 +546,7 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, + rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool: """ Called right before placing a regular sell order. @@ -562,7 +562,7 @@ class AwesomeStrategy(IStrategy): :param amount: Amount in quote currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param sell_reason: Sell reason. + :param exit_reason: Exit reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] :param current_time: datetime object, containing the current datetime @@ -570,7 +570,7 @@ class AwesomeStrategy(IStrategy): :return bool: When True is returned, then the sell-order is placed on the exchange. False aborts the process """ - if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: + if exit_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: # Reject force-sells with negative profit # This is just a sample, please adjust to your needs # (this does not necessarily make sense, assuming you know when you're force-selling) diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 4d6de440f..3a66ae9b3 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -20,6 +20,7 @@ If you intend on using markets other than spot markets, please migrate your stra * New `side` argument to callbacks without trade object * `custom_stake_amount` * `confirm_trade_entry` +* Changed argument name in `confirm_trade_exit` * Renamed `trade.nr_of_successful_buys` to `trade.nr_of_successful_entries` (mostly relevant for `adjust_trade_position()`). * Introduced new [`leverage` callback](strategy-callbacks.md#leverage-callback). * Informative pairs can now pass a 3rd element in the Tuple, defining the candle type. @@ -149,16 +150,17 @@ class AwesomeStrategy(IStrategy): New string argument `side` - which can be either `"long"` or `"short"`. -``` python hl_lines="5" +``` python hl_lines="4" class AwesomeStrategy(IStrategy): def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], **kwargs) -> bool: return True ``` + After: -``` python hl_lines="5" +``` python hl_lines="4" class AwesomeStrategy(IStrategy): def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], @@ -166,6 +168,29 @@ class AwesomeStrategy(IStrategy): return True ``` +### `confirm_trade_exit` + +Changed argument `sell_reason` to `exit_reason`. +For compatibility, `sell_reason` will still be provided for a limited time. + +``` python hl_lines="3" +class AwesomeStrategy(IStrategy): + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, + current_time: datetime, **kwargs) -> bool: + return True +``` + +After: + +``` python hl_lines="3" +class AwesomeStrategy(IStrategy): + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, exit_reason: str, + current_time: datetime, **kwargs) -> bool: + return True +``` + ### Adjust trade position changes While adjust-trade-position itself did not change, you should no longer use `trade.nr_of_successful_buys` - and instead use `trade.nr_of_successful_entries`, which will also include short entries. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0d87ca706..534fa8425 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1385,7 +1385,8 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, + time_in_force=time_in_force, exit_reason=sell_reason.sell_reason, + sell_reason=sell_reason.sell_reason, # sellreason -> compatibility current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of exiting {trade.pair}") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7d482ba99..b4726f753 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -553,7 +553,8 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_reason, + sell_reason=sell.sell_reason, # deprecated + exit_reason=sell.sell_reason, current_time=sell_candle_time): return None diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 69dc80128..7b07d82c1 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -290,7 +290,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, + rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool: """ Called right before placing a regular exit order. @@ -307,7 +307,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param amount: Amount in quote currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param sell_reason: Exit reason. + :param exit_reason: Exit reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] :param current_time: datetime object, containing the current datetime diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index d98adfa07..0ceeca982 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -143,7 +143,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f return True def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, + rate: float, time_in_force: str, exit_reason: str, current_time: 'datetime', **kwargs) -> bool: """ Called right before placing a regular sell order. @@ -160,7 +160,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: :param amount: Amount in quote currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param sell_reason: Sell reason. + :param exit_reason: Exit reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] :param current_time: datetime object, containing the current datetime diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index a9d11e52f..5cb8fce16 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -46,7 +46,8 @@ def test_strategy_test_v3(result, fee, is_short, side): current_time=datetime.utcnow(), side=side, entry_tag=None) is True assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, - rate=20000, time_in_force='gtc', sell_reason='roi', + rate=20000, time_in_force='gtc', exit_reason='roi', + sell_reason='roi', current_time=datetime.utcnow(), side=side) is True From 62e8c7b5b728e53019df70440b0ada216b4064bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 06:38:58 +0100 Subject: [PATCH 08/26] Rename parameter to avoid ambiguity --- freqtrade/freqtradebot.py | 18 +++++++++--------- freqtrade/rpc/rpc.py | 4 ++-- tests/test_freqtradebot.py | 22 +++++++++++----------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 534fa8425..f2aca0d09 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -976,7 +976,7 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( + self.execute_trade_exit(trade, trade.stop_loss, exit_check=SellCheckTuple( sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: @@ -1158,7 +1158,7 @@ class FreqtradeBot(LoggingMixin): try: self.execute_trade_exit( trade, order.get('price'), - sell_reason=SellCheckTuple(sell_type=SellType.EMERGENCY_SELL)) + exit_check=SellCheckTuple(sell_type=SellType.EMERGENCY_SELL)) except DependencyException as exception: logger.warning( f'Unable to emergency sell trade {trade.pair}: {exception}') @@ -1333,7 +1333,7 @@ class FreqtradeBot(LoggingMixin): self, trade: Trade, limit: float, - sell_reason: SellCheckTuple, + exit_check: SellCheckTuple, *, exit_tag: Optional[str] = None, ordertype: Optional[str] = None, @@ -1342,7 +1342,7 @@ class FreqtradeBot(LoggingMixin): Executes a trade exit for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order - :param sell_reason: Reason the sell was triggered + :param exit_check: CheckTuple with signal and reason :return: True if it succeeds (supported) False (not supported) """ trade.funding_fees = self.exchange.get_funding_fees( @@ -1352,7 +1352,7 @@ class FreqtradeBot(LoggingMixin): open_date=trade.open_date, ) exit_type = 'exit' - if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if exit_check.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): exit_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, @@ -1376,7 +1376,7 @@ class FreqtradeBot(LoggingMixin): trade = self.cancel_stoploss_on_exchange(trade) order_type = ordertype or self.strategy.order_types[exit_type] - if sell_reason.sell_type == SellType.EMERGENCY_SELL: + if exit_check.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencyexit", "market") @@ -1385,8 +1385,8 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, exit_reason=sell_reason.sell_reason, - sell_reason=sell_reason.sell_reason, # sellreason -> compatibility + time_in_force=time_in_force, exit_reason=exit_check.sell_reason, + sell_reason=exit_check.sell_reason, # sellreason -> compatibility current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of exiting {trade.pair}") return False @@ -1415,7 +1415,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = exit_tag or sell_reason.sell_reason + trade.sell_reason = exit_tag or exit_check.sell_reason # Lock pair for one candle to prevent immediate re-trading self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 753db0d25..2c61c39ad 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -707,12 +707,12 @@ class RPC: # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side=trade.exit_side) - sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) + exit_check = SellCheckTuple(sell_type=SellType.FORCE_SELL) order_type = ordertype or self._freqtrade.strategy.order_types.get( "forceexit", self._freqtrade.strategy.order_types["exit"]) self._freqtrade.execute_trade_exit( - trade, current_rate, sell_reason, ordertype=order_type) + trade, current_rate, exit_check, ordertype=order_type) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ebc1a7b2d..feed74a49 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3100,7 +3100,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), - sell_reason=SellCheckTuple(sell_type=SellType.ROI) + exit_check=SellCheckTuple(sell_type=SellType.ROI) ) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -3112,7 +3112,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), - sell_reason=SellCheckTuple(sell_type=SellType.ROI) + exit_check=SellCheckTuple(sell_type=SellType.ROI) ) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -3173,7 +3173,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd ) freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down)()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -3248,7 +3248,7 @@ def test_execute_trade_exit_custom_exit_price( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL) + exit_check=SellCheckTuple(sell_type=SellType.SELL_SIGNAL) ) # Sell price must be different to default bid price @@ -3319,7 +3319,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99 freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down())['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -3379,7 +3379,7 @@ def test_execute_trade_exit_sloe_cancel_exception( trade.stoploss_order_id = "abcd" freqtrade.execute_trade_exit(trade=trade, limit=1234, - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert create_order_mock.call_count == 2 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -3434,7 +3434,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS) + exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS) ) trade = Trade.query.first() @@ -3579,7 +3579,7 @@ def test_execute_trade_exit_market_order( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - sell_reason=SellCheckTuple(sell_type=SellType.ROI) + exit_check=SellCheckTuple(sell_type=SellType.ROI) ) assert not trade.is_open @@ -3647,7 +3647,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u assert not freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - sell_reason=sell_reason + exit_check=sell_reason ) assert mock_insuf.call_count == 1 @@ -3816,7 +3816,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_down()['ask' if is_short else 'bid'], - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS) + exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS) ) trade.close(ticker_usdt_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) @@ -5145,7 +5145,7 @@ def test_update_funding_fees( trade=trade, # The values of the next 2 params are irrelevant for this test limit=ticker_usdt_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.ROI) + exit_check=SellCheckTuple(sell_type=SellType.ROI) ) assert trade.funding_fees == pytest.approx(sum( trade.amount * From 8d111d357afa0c44214c9990798dae2918733880 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 06:46:29 +0100 Subject: [PATCH 09/26] Update SellCheckTuple to new naming --- freqtrade/freqtradebot.py | 26 ++++++++++----------- freqtrade/optimize/backtesting.py | 24 +++++++++---------- freqtrade/rpc/rpc.py | 4 ++-- freqtrade/strategy/interface.py | 38 +++++++++++++++---------------- tests/strategy/test_interface.py | 38 +++++++++++++++---------------- tests/test_freqtradebot.py | 28 +++++++++++------------ tests/test_integration.py | 16 ++++++------- 7 files changed, 87 insertions(+), 87 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2aca0d09..eae776546 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -27,7 +27,7 @@ from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager -from freqtrade.strategy.interface import IStrategy, SellCheckTuple +from freqtrade.strategy.interface import IStrategy, ExitCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -976,8 +976,8 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, trade.stop_loss, exit_check=SellCheckTuple( - sell_type=SellType.EMERGENCY_SELL)) + self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple( + exit_type=SellType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None @@ -1101,7 +1101,7 @@ class FreqtradeBot(LoggingMixin): """ Check and execute trade exit """ - should_exit: SellCheckTuple = self.strategy.should_exit( + should_exit: ExitCheckTuple = self.strategy.should_exit( trade, exit_rate, datetime.now(timezone.utc), @@ -1110,8 +1110,8 @@ class FreqtradeBot(LoggingMixin): force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) - if should_exit.sell_flag: - logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}' + if should_exit.exit_flag: + logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}' f'Tag: {exit_tag if exit_tag is not None else "None"}') self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag) return True @@ -1158,7 +1158,7 @@ class FreqtradeBot(LoggingMixin): try: self.execute_trade_exit( trade, order.get('price'), - exit_check=SellCheckTuple(sell_type=SellType.EMERGENCY_SELL)) + exit_check=ExitCheckTuple(exit_type=SellType.EMERGENCY_SELL)) except DependencyException as exception: logger.warning( f'Unable to emergency sell trade {trade.pair}: {exception}') @@ -1333,7 +1333,7 @@ class FreqtradeBot(LoggingMixin): self, trade: Trade, limit: float, - exit_check: SellCheckTuple, + exit_check: ExitCheckTuple, *, exit_tag: Optional[str] = None, ordertype: Optional[str] = None, @@ -1352,7 +1352,7 @@ class FreqtradeBot(LoggingMixin): open_date=trade.open_date, ) exit_type = 'exit' - if exit_check.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if exit_check.exit_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): exit_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, @@ -1376,7 +1376,7 @@ class FreqtradeBot(LoggingMixin): trade = self.cancel_stoploss_on_exchange(trade) order_type = ordertype or self.strategy.order_types[exit_type] - if exit_check.sell_type == SellType.EMERGENCY_SELL: + if exit_check.exit_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencyexit", "market") @@ -1385,8 +1385,8 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, exit_reason=exit_check.sell_reason, - sell_reason=exit_check.sell_reason, # sellreason -> compatibility + time_in_force=time_in_force, exit_reason=exit_check.exit_reason, + sell_reason=exit_check.exit_reason, # sellreason -> compatibility current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of exiting {trade.pair}") return False @@ -1415,7 +1415,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = exit_tag or exit_check.sell_reason + trade.sell_reason = exit_tag or exit_check.exit_reason # Lock pair for one candle to prevent immediate re-trading self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b4726f753..d4c0c4e48 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -31,7 +31,7 @@ from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.strategy.interface import IStrategy, SellCheckTuple +from freqtrade.strategy.interface import IStrategy, ExitCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -352,20 +352,20 @@ class Backtesting: data[pair] = df_analyzed[headers].values.tolist() return data - def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, + def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, trade_dur: int) -> float: """ Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI - if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if sell.exit_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur) - elif sell.sell_type == (SellType.ROI): + elif sell.exit_type == (SellType.ROI): return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur) else: return sell_row[OPEN_IDX] - def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, + def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, trade_dur: int) -> float: # our stoploss was already lower than candle high, # possibly due to a cancelled trade exit. @@ -383,7 +383,7 @@ class Backtesting: # Special case: trailing triggers within same candle as trade opened. Assume most # pessimistic price movement, which is moving just enough to arm stoploss and # immediately going down to stop price. - if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0: + if sell.exit_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0: if ( not self.strategy.use_custom_stoploss and self.strategy.trailing_stop and self.strategy.trailing_only_offset_is_reached @@ -413,7 +413,7 @@ class Backtesting: # Set close_rate to stoploss return trade.stop_loss - def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, + def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, trade_dur: int) -> float: is_short = trade.is_short or False leverage = trade.leverage or 1.0 @@ -521,7 +521,7 @@ class Backtesting: low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX] ) - if sell.sell_flag: + if sell.exit_flag: trade.close_date = sell_candle_time trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) @@ -532,7 +532,7 @@ class Backtesting: # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) order_type = self.strategy.order_types['exit'] - if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): + if sell.exit_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): # Custom exit pricing only for sell-signals if order_type == 'limit': closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, @@ -553,12 +553,12 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_reason, # deprecated - exit_reason=sell.sell_reason, + sell_reason=sell.exit_reason, # deprecated + exit_reason=sell.exit_reason, current_time=sell_candle_time): return None - trade.sell_reason = sell.sell_reason + trade.sell_reason = sell.exit_reason # Checks and adds an exit tag, after checking that the length of the # sell_row has the length for an exit tag column diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2c61c39ad..ed279e303 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -27,7 +27,7 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter -from freqtrade.strategy.interface import SellCheckTuple +from freqtrade.strategy.interface import ExitCheckTuple from freqtrade.wallets import PositionWallet, Wallet @@ -707,7 +707,7 @@ class RPC: # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side=trade.exit_side) - exit_check = SellCheckTuple(sell_type=SellType.FORCE_SELL) + exit_check = ExitCheckTuple(exit_type=SellType.FORCE_SELL) order_type = ordertype or self._freqtrade.strategy.order_types.get( "forceexit", self._freqtrade.strategy.order_types["exit"]) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7b07d82c1..fc12334fc 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -32,20 +32,20 @@ logger = logging.getLogger(__name__) CUSTOM_EXIT_MAX_LENGTH = 64 -class SellCheckTuple: +class ExitCheckTuple: """ - NamedTuple for Sell type + reason + NamedTuple for Exit type + reason """ - sell_type: SellType - sell_reason: str = '' + exit_type: SellType + exit_reason: str = '' - def __init__(self, sell_type: SellType, sell_reason: str = ''): - self.sell_type = sell_type - self.sell_reason = sell_reason or sell_type.value + def __init__(self, exit_type: SellType, exit_reason: str = ''): + self.exit_type = exit_type + self.exit_reason = exit_reason or exit_type.value @property - def sell_flag(self): - return self.sell_type != SellType.NONE + def exit_flag(self): + return self.exit_type != SellType.NONE class IStrategy(ABC, HyperStrategyMixin): @@ -848,7 +848,7 @@ class IStrategy(ABC, HyperStrategyMixin): def should_exit(self, trade: Trade, rate: float, current_time: datetime, *, enter: bool, exit_: bool, low: float = None, high: float = None, - force_stoploss: float = 0) -> SellCheckTuple: + force_stoploss: float = 0) -> ExitCheckTuple: """ This function evaluates if one of the conditions required to trigger an exit order has been reached, which can either be a stop-loss, ROI or exit-signal. @@ -908,29 +908,29 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"{trade.pair} - Sell signal received. " f"sell_type=SellType.{sell_signal.name}" + (f", custom_reason={custom_reason}" if custom_reason else "")) - return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) + return ExitCheckTuple(exit_type=sell_signal, exit_reason=custom_reason) # Sequence: # Exit-signal # ROI (if not stoploss) # Stoploss - if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: + if roi_reached and stoplossflag.exit_type != SellType.STOP_LOSS: logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") - return SellCheckTuple(sell_type=SellType.ROI) + return ExitCheckTuple(exit_type=SellType.ROI) - if stoplossflag.sell_flag: + if stoplossflag.exit_flag: - logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}") + logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.exit_type}") return stoplossflag # This one is noisy, commented out... # logger.debug(f"{trade.pair} - No exit signal.") - return SellCheckTuple(sell_type=SellType.NONE) + return ExitCheckTuple(exit_type=SellType.NONE) def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, low: float = None, - high: float = None) -> SellCheckTuple: + high: float = None) -> ExitCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to exit or not @@ -1008,9 +1008,9 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"{trade.pair} - Trailing stop saved " f"{new_stoploss:.6f}") - return SellCheckTuple(sell_type=sell_type) + return ExitCheckTuple(exit_type=sell_type) - return SellCheckTuple(sell_type=SellType.NONE) + return ExitCheckTuple(exit_type=SellType.NONE) def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 18af215a3..3d75b36ac 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -18,7 +18,7 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) -from freqtrade.strategy.interface import SellCheckTuple +from freqtrade.strategy.interface import ExitCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re @@ -455,23 +455,23 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=now, current_profit=profit, force_stoploss=0, high=None) - assert isinstance(sl_flag, SellCheckTuple) - assert sl_flag.sell_type == expected + assert isinstance(sl_flag, ExitCheckTuple) + assert sl_flag.exit_type == expected if expected == SellType.NONE: - assert sl_flag.sell_flag is False + assert sl_flag.exit_flag is False else: - assert sl_flag.sell_flag is True + assert sl_flag.exit_flag is True assert round(trade.stop_loss, 2) == adjusted current_rate2 = trade.open_rate * (1 + profit2) sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade, current_time=now, current_profit=profit2, force_stoploss=0, high=None) - assert sl_flag.sell_type == expected2 + assert sl_flag.exit_type == expected2 if expected2 == SellType.NONE: - assert sl_flag.sell_flag is False + assert sl_flag.exit_flag is False else: - assert sl_flag.sell_flag is True + assert sl_flag.exit_flag is True assert round(trade.stop_loss, 2) == adjusted2 strategy.custom_stoploss = original_stopvalue @@ -496,34 +496,34 @@ def test_custom_exit(default_conf, fee, caplog) -> None: enter=False, exit_=False, low=None, high=None) - assert res.sell_flag is False - assert res.sell_type == SellType.NONE + assert res.exit_flag is False + assert res.exit_type == SellType.NONE strategy.custom_exit = MagicMock(return_value=True) res = strategy.should_exit(trade, 1, now, enter=False, exit_=False, low=None, high=None) - assert res.sell_flag is True - assert res.sell_type == SellType.CUSTOM_SELL - assert res.sell_reason == 'custom_sell' + assert res.exit_flag is True + assert res.exit_type == SellType.CUSTOM_SELL + assert res.exit_reason == 'custom_sell' strategy.custom_exit = MagicMock(return_value='hello world') res = strategy.should_exit(trade, 1, now, enter=False, exit_=False, low=None, high=None) - assert res.sell_type == SellType.CUSTOM_SELL - assert res.sell_flag is True - assert res.sell_reason == 'hello world' + assert res.exit_type == SellType.CUSTOM_SELL + assert res.exit_flag is True + assert res.exit_reason == 'hello world' caplog.clear() strategy.custom_exit = MagicMock(return_value='h' * 100) res = strategy.should_exit(trade, 1, now, enter=False, exit_=False, low=None, high=None) - assert res.sell_type == SellType.CUSTOM_SELL - assert res.sell_flag is True - assert res.sell_reason == 'h' * 64 + assert res.exit_type == SellType.CUSTOM_SELL + assert res.exit_flag is True + assert res.exit_reason == 'h' * 64 assert log_has_re('Custom sell reason returned from custom_exit is too long.*', caplog) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index feed74a49..6f7b8f9b0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -20,7 +20,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock -from freqtrade.strategy.interface import SellCheckTuple +from freqtrade.strategy.interface import ExitCheckTuple from freqtrade.worker import Worker from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, @@ -3100,7 +3100,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), - exit_check=SellCheckTuple(sell_type=SellType.ROI) + exit_check=ExitCheckTuple(exit_type=SellType.ROI) ) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -3112,7 +3112,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), - exit_check=SellCheckTuple(sell_type=SellType.ROI) + exit_check=ExitCheckTuple(exit_type=SellType.ROI) ) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -3173,7 +3173,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd ) freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down)()['bid'], - exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -3248,7 +3248,7 @@ def test_execute_trade_exit_custom_exit_price( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - exit_check=SellCheckTuple(sell_type=SellType.SELL_SIGNAL) + exit_check=ExitCheckTuple(exit_type=SellType.SELL_SIGNAL) ) # Sell price must be different to default bid price @@ -3319,7 +3319,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99 freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down())['bid'], - exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -3379,7 +3379,7 @@ def test_execute_trade_exit_sloe_cancel_exception( trade.stoploss_order_id = "abcd" freqtrade.execute_trade_exit(trade=trade, limit=1234, - exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS)) assert create_order_mock.call_count == 2 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -3434,7 +3434,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS) + exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS) ) trade = Trade.query.first() @@ -3579,7 +3579,7 @@ def test_execute_trade_exit_market_order( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - exit_check=SellCheckTuple(sell_type=SellType.ROI) + exit_check=ExitCheckTuple(exit_type=SellType.ROI) ) assert not trade.is_open @@ -3643,7 +3643,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u fetch_ticker=ticker_usdt_sell_up ) - sell_reason = SellCheckTuple(sell_type=SellType.ROI) + sell_reason = ExitCheckTuple(exit_type=SellType.ROI) assert not freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], @@ -3696,8 +3696,8 @@ def test_sell_profit_only( if sell_type == SellType.SELL_SIGNAL.value: freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) else: - freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_type=SellType.NONE)) + freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple( + exit_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() @@ -3816,7 +3816,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_down()['ask' if is_short else 'bid'], - exit_check=SellCheckTuple(sell_type=SellType.STOP_LOSS) + exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS) ) trade.close(ticker_usdt_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) @@ -5145,7 +5145,7 @@ def test_update_funding_fees( trade=trade, # The values of the next 2 params are irrelevant for this test limit=ticker_usdt_sell_up()['bid'], - exit_check=SellCheckTuple(sell_type=SellType.ROI) + exit_check=ExitCheckTuple(exit_type=SellType.ROI) ) assert trade.funding_fees == pytest.approx(sum( trade.amount * diff --git a/tests/test_integration.py b/tests/test_integration.py index 9115b431b..3e9c81a80 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,7 +6,7 @@ from freqtrade.enums import SellType from freqtrade.persistence import Trade from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC -from freqtrade.strategy.interface import SellCheckTuple +from freqtrade.strategy.interface import ExitCheckTuple from tests.conftest import get_patched_freqtradebot, patch_get_signal @@ -53,8 +53,8 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) # Sell 3rd trade (not called for the first trade) should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_type=SellType.NONE), - SellCheckTuple(sell_type=SellType.SELL_SIGNAL)] + ExitCheckTuple(exit_type=SellType.NONE), + ExitCheckTuple(exit_type=SellType.SELL_SIGNAL)] ) cancel_order_mock = MagicMock() mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) @@ -161,11 +161,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati _notify_exit=MagicMock(), ) should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_type=SellType.NONE), - SellCheckTuple(sell_type=SellType.SELL_SIGNAL), - SellCheckTuple(sell_type=SellType.NONE), - SellCheckTuple(sell_type=SellType.NONE), - SellCheckTuple(sell_type=SellType.NONE)] + ExitCheckTuple(exit_type=SellType.NONE), + ExitCheckTuple(exit_type=SellType.SELL_SIGNAL), + ExitCheckTuple(exit_type=SellType.NONE), + ExitCheckTuple(exit_type=SellType.NONE), + ExitCheckTuple(exit_type=SellType.NONE)] ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) From c07883b1f9044927977ccc6ceb1db66efaae67f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 06:50:18 +0100 Subject: [PATCH 10/26] Move ExitCheckTuple to enums --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/exitchecktuple.py | 17 +++++++++++++++++ freqtrade/freqtradebot.py | 5 +++-- freqtrade/optimize/backtesting.py | 5 +++-- freqtrade/strategy/interface.py | 20 ++------------------ tests/strategy/test_interface.py | 3 +-- tests/test_freqtradebot.py | 4 ++-- tests/test_integration.py | 3 +-- 8 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 freqtrade/enums/exitchecktuple.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 49f2f1a60..840290c90 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa: F401 from freqtrade.enums.backteststate import BacktestState from freqtrade.enums.candletype import CandleType +from freqtrade.enums.exitchecktuple import ExitCheckTuple from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.ordertypevalue import OrderTypeValues from freqtrade.enums.rpcmessagetype import RPCMessageType diff --git a/freqtrade/enums/exitchecktuple.py b/freqtrade/enums/exitchecktuple.py new file mode 100644 index 000000000..3fec52e0a --- /dev/null +++ b/freqtrade/enums/exitchecktuple.py @@ -0,0 +1,17 @@ +from freqtrade.enums.selltype import SellType + + +class ExitCheckTuple: + """ + NamedTuple for Exit type + reason + """ + exit_type: SellType + exit_reason: str = '' + + def __init__(self, exit_type: SellType, exit_reason: str = ''): + self.exit_type = exit_type + self.exit_reason = exit_reason or exit_type.value + + @property + def exit_flag(self): + return self.exit_type != SellType.NONE diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eae776546..c8f11559c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,8 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State, TradingMode +from freqtrade.enums import (ExitCheckTuple, RPCMessageType, RunMode, SellType, SignalDirection, + State, TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -27,7 +28,7 @@ from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager -from freqtrade.strategy.interface import IStrategy, ExitCheckTuple +from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d4c0c4e48..27ca11a89 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,7 +19,8 @@ from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import BacktestState, CandleType, MarginMode, SellType, TradingMode +from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, MarginMode, SellType, + TradingMode) from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import get_strategy_run_id @@ -31,7 +32,7 @@ from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.strategy.interface import IStrategy, ExitCheckTuple +from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index fc12334fc..e78043b44 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -13,8 +13,8 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (CandleType, SellType, SignalDirection, SignalTagType, SignalType, - TradingMode) +from freqtrade.enums import (CandleType, ExitCheckTuple, SellType, SignalDirection, SignalTagType, + SignalType, TradingMode) from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date @@ -32,22 +32,6 @@ logger = logging.getLogger(__name__) CUSTOM_EXIT_MAX_LENGTH = 64 -class ExitCheckTuple: - """ - NamedTuple for Exit type + reason - """ - exit_type: SellType - exit_reason: str = '' - - def __init__(self, exit_type: SellType, exit_reason: str = ''): - self.exit_type = exit_type - self.exit_reason = exit_reason or exit_type.value - - @property - def exit_flag(self): - return self.exit_type != SellType.NONE - - class IStrategy(ABC, HyperStrategyMixin): """ Interface for freqtrade strategies diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 3d75b36ac..5b7519d82 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -11,14 +11,13 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.enums import SellType, SignalDirection +from freqtrade.enums import ExitCheckTuple, SellType, SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.optimize.space import SKDecimal from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) -from freqtrade.strategy.interface import ExitCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6f7b8f9b0..eaf6044d0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -13,14 +13,14 @@ import pytest from pandas import DataFrame from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT -from freqtrade.enums import CandleType, RPCMessageType, RunMode, SellType, SignalDirection, State +from freqtrade.enums import (CandleType, ExitCheckTuple, RPCMessageType, RunMode, SellType, + SignalDirection, State) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock -from freqtrade.strategy.interface import ExitCheckTuple from freqtrade.worker import Worker from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, diff --git a/tests/test_integration.py b/tests/test_integration.py index 3e9c81a80..692886cf3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -2,11 +2,10 @@ from unittest.mock import MagicMock import pytest -from freqtrade.enums import SellType +from freqtrade.enums import ExitCheckTuple, SellType from freqtrade.persistence import Trade from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC -from freqtrade.strategy.interface import ExitCheckTuple from tests.conftest import get_patched_freqtradebot, patch_get_signal From dcfa3e86487d9e9b5901bcaca65a0cae82baca0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 06:55:37 +0100 Subject: [PATCH 11/26] Update SellType to ExitType --- freqtrade/edge/edge_positioning.py | 6 +- freqtrade/enums/__init__.py | 2 +- freqtrade/enums/exitchecktuple.py | 8 +- freqtrade/enums/{selltype.py => exittype.py} | 2 +- freqtrade/freqtradebot.py | 12 +-- freqtrade/optimize/backtesting.py | 12 +-- freqtrade/persistence/models.py | 4 +- .../plugins/protections/stoploss_guard.py | 10 +- freqtrade/rpc/rpc.py | 4 +- freqtrade/strategy/interface.py | 26 ++--- tests/edge/test_edge.py | 12 +-- tests/optimize/__init__.py | 4 +- tests/optimize/conftest.py | 4 +- tests/optimize/test_backtest_detail.py | 98 +++++++++---------- tests/optimize/test_backtesting.py | 20 ++-- .../test_backtesting_adjust_position.py | 4 +- tests/optimize/test_hyperopt.py | 10 +- tests/optimize/test_optimize_reports.py | 14 +-- tests/plugins/test_protections.py | 46 ++++----- tests/rpc/test_rpc_telegram.py | 16 +-- tests/rpc/test_rpc_webhook.py | 8 +- tests/strategy/test_interface.py | 36 +++---- tests/test_freqtradebot.py | 74 +++++++------- tests/test_integration.py | 20 ++-- 24 files changed, 226 insertions(+), 226 deletions(-) rename freqtrade/enums/{selltype.py => exittype.py} (95%) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index f38d25188..d6df86dea 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -13,7 +13,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data -from freqtrade.enums import RunMode, SellType +from freqtrade.enums import RunMode, ExitType from freqtrade.enums.candletype import CandleType from freqtrade.exceptions import OperationalException from freqtrade.exchange.exchange import timeframe_to_seconds @@ -461,7 +461,7 @@ class Edge: if stop_index <= sell_index: exit_index = open_trade_index + stop_index - exit_type = SellType.STOP_LOSS + exit_type = ExitType.STOP_LOSS exit_price = stop_price elif stop_index > sell_index: # If exit is SELL then we exit at the next candle @@ -471,7 +471,7 @@ class Edge: if len(ohlc_columns) - 1 < exit_index: break - exit_type = SellType.SELL_SIGNAL + exit_type = ExitType.SELL_SIGNAL exit_price = ohlc_columns[exit_index, 0] trade = {'pair': pair, diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 840290c90..e50ebc4a4 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -2,11 +2,11 @@ from freqtrade.enums.backteststate import BacktestState from freqtrade.enums.candletype import CandleType from freqtrade.enums.exitchecktuple import ExitCheckTuple +from freqtrade.enums.exittype import ExitType from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.ordertypevalue import OrderTypeValues from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode -from freqtrade.enums.selltype import SellType from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode diff --git a/freqtrade/enums/exitchecktuple.py b/freqtrade/enums/exitchecktuple.py index 3fec52e0a..c245a05da 100644 --- a/freqtrade/enums/exitchecktuple.py +++ b/freqtrade/enums/exitchecktuple.py @@ -1,17 +1,17 @@ -from freqtrade.enums.selltype import SellType +from freqtrade.enums.exittype import ExitType class ExitCheckTuple: """ NamedTuple for Exit type + reason """ - exit_type: SellType + exit_type: ExitType exit_reason: str = '' - def __init__(self, exit_type: SellType, exit_reason: str = ''): + def __init__(self, exit_type: ExitType, exit_reason: str = ''): self.exit_type = exit_type self.exit_reason = exit_reason or exit_type.value @property def exit_flag(self): - return self.exit_type != SellType.NONE + return self.exit_type != ExitType.NONE diff --git a/freqtrade/enums/selltype.py b/freqtrade/enums/exittype.py similarity index 95% rename from freqtrade/enums/selltype.py rename to freqtrade/enums/exittype.py index 015c30186..36d2a4f9e 100644 --- a/freqtrade/enums/selltype.py +++ b/freqtrade/enums/exittype.py @@ -1,7 +1,7 @@ from enum import Enum -class SellType(Enum): +class ExitType(Enum): """ Enum to distinguish between sell reasons """ diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8f11559c..cf4c6231f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import (ExitCheckTuple, RPCMessageType, RunMode, SellType, SignalDirection, +from freqtrade.enums import (ExitCheckTuple, RPCMessageType, RunMode, ExitType, SignalDirection, State, TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) @@ -978,7 +978,7 @@ class FreqtradeBot(LoggingMixin): logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple( - exit_type=SellType.EMERGENCY_SELL)) + exit_type=ExitType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None @@ -1009,7 +1009,7 @@ class FreqtradeBot(LoggingMixin): # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): - trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + trade.sell_reason = ExitType.STOPLOSS_ON_EXCHANGE.value self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys @@ -1159,7 +1159,7 @@ class FreqtradeBot(LoggingMixin): try: self.execute_trade_exit( trade, order.get('price'), - exit_check=ExitCheckTuple(exit_type=SellType.EMERGENCY_SELL)) + exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_SELL)) except DependencyException as exception: logger.warning( f'Unable to emergency sell trade {trade.pair}: {exception}') @@ -1353,7 +1353,7 @@ class FreqtradeBot(LoggingMixin): open_date=trade.open_date, ) exit_type = 'exit' - if exit_check.exit_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): exit_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, @@ -1377,7 +1377,7 @@ class FreqtradeBot(LoggingMixin): trade = self.cancel_stoploss_on_exchange(trade) order_type = ordertype or self.strategy.order_types[exit_type] - if exit_check.exit_type == SellType.EMERGENCY_SELL: + if exit_check.exit_type == ExitType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencyexit", "market") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 27ca11a89..082effdf2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,7 +19,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, MarginMode, SellType, +from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, MarginMode, ExitType, TradingMode) from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -359,9 +359,9 @@ class Backtesting: Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI - if sell.exit_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur) - elif sell.exit_type == (SellType.ROI): + elif sell.exit_type == (ExitType.ROI): return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur) else: return sell_row[OPEN_IDX] @@ -384,7 +384,7 @@ class Backtesting: # Special case: trailing triggers within same candle as trade opened. Assume most # pessimistic price movement, which is moving just enough to arm stoploss and # immediately going down to stop price. - if sell.exit_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0: + if sell.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0: if ( not self.strategy.use_custom_stoploss and self.strategy.trailing_stop and self.strategy.trailing_only_offset_is_reached @@ -533,7 +533,7 @@ class Backtesting: # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) order_type = self.strategy.order_types['exit'] - if sell.exit_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): + if sell.exit_type in (ExitType.SELL_SIGNAL, ExitType.CUSTOM_SELL): # Custom exit pricing only for sell-signals if order_type == 'limit': closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, @@ -814,7 +814,7 @@ class Backtesting: sell_row = data[pair][-1] trade.close_date = sell_row[DATE_IDX].to_pydatetime() - trade.sell_reason = SellType.FORCE_SELL.value + trade.sell_reason = ExitType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) LocalTrade.close_bt_trade(trade) # Deepcopy object to have wallets update correctly diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 67f76a005..e4559d1f6 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -14,7 +14,7 @@ from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES -from freqtrade.enums import SellType, TradingMode +from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.leverage import interest from freqtrade.persistence.migrations import check_migrate @@ -625,7 +625,7 @@ class LocalTrade(): elif order.ft_order_side == 'stoploss': self.stoploss_order_id = None self.close_rate_requested = self.stop_loss - self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + self.sell_reason = ExitType.STOPLOSS_ON_EXCHANGE.value if self.is_open: logger.info(f'{order.order_type.upper()} is hit for {self}.') self.close(order.safe_price) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 40edf1204..7a29c20b1 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -3,7 +3,7 @@ import logging from datetime import datetime, timedelta from typing import Any, Dict -from freqtrade.enums import SellType +from freqtrade.enums import ExitType from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn @@ -44,8 +44,8 @@ class StoplossGuard(IProtection): # filters = [ # Trade.is_open.is_(False), # Trade.close_date > look_back_until, - # or_(Trade.sell_reason == SellType.STOP_LOSS.value, - # and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, + # or_(Trade.sell_reason == ExitType.STOP_LOSS.value, + # and_(Trade.sell_reason == ExitType.TRAILING_STOP_LOSS.value, # Trade.close_profit < 0)) # ] # if pair: @@ -54,8 +54,8 @@ class StoplossGuard(IProtection): trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) 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) + ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value, + ExitType.STOPLOSS_ON_EXCHANGE.value) and trade.close_profit and trade.close_profit < 0)] if len(trades) < self._trade_limit: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ed279e303..4934343b6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -18,7 +18,7 @@ from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data -from freqtrade.enums import SellType, SignalDirection, State, TradingMode +from freqtrade.enums import ExitType, SignalDirection, State, TradingMode from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -707,7 +707,7 @@ class RPC: # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side=trade.exit_side) - exit_check = ExitCheckTuple(exit_type=SellType.FORCE_SELL) + exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL) order_type = ordertype or self._freqtrade.strategy.order_types.get( "forceexit", self._freqtrade.strategy.order_types["exit"]) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e78043b44..8df8c88a0 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -13,7 +13,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (CandleType, ExitCheckTuple, SellType, SignalDirection, SignalTagType, +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, SignalTagType, SignalType, TradingMode) from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -861,7 +861,7 @@ class IStrategy(ABC, HyperStrategyMixin): and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=current_time)) - sell_signal = SellType.NONE + sell_signal = ExitType.NONE custom_reason = '' # use provided rate in backtesting, not high/low. current_rate = rate @@ -872,14 +872,14 @@ class IStrategy(ABC, HyperStrategyMixin): pass elif self.use_sell_signal and not enter: if exit_: - sell_signal = SellType.SELL_SIGNAL + sell_signal = ExitType.SELL_SIGNAL else: trade_type = "exit_short" if trade.is_short else "sell" custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)( pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, current_profit=current_profit) if custom_reason: - sell_signal = SellType.CUSTOM_SELL + sell_signal = ExitType.CUSTOM_SELL if isinstance(custom_reason, str): if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH: logger.warning(f'Custom {trade_type} reason returned from ' @@ -888,9 +888,9 @@ class IStrategy(ABC, HyperStrategyMixin): custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH] else: custom_reason = None - if sell_signal in (SellType.CUSTOM_SELL, SellType.SELL_SIGNAL): + if sell_signal in (ExitType.CUSTOM_SELL, ExitType.SELL_SIGNAL): logger.debug(f"{trade.pair} - Sell signal received. " - f"sell_type=SellType.{sell_signal.name}" + + f"sell_type=ExitType.{sell_signal.name}" + (f", custom_reason={custom_reason}" if custom_reason else "")) return ExitCheckTuple(exit_type=sell_signal, exit_reason=custom_reason) @@ -898,9 +898,9 @@ class IStrategy(ABC, HyperStrategyMixin): # Exit-signal # ROI (if not stoploss) # Stoploss - if roi_reached and stoplossflag.exit_type != SellType.STOP_LOSS: - logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") - return ExitCheckTuple(exit_type=SellType.ROI) + if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS: + logger.debug(f"{trade.pair} - Required profit reached. sell_type=ExitType.ROI") + return ExitCheckTuple(exit_type=ExitType.ROI) if stoplossflag.exit_flag: @@ -909,7 +909,7 @@ class IStrategy(ABC, HyperStrategyMixin): # This one is noisy, commented out... # logger.debug(f"{trade.pair} - No exit signal.") - return ExitCheckTuple(exit_type=SellType.NONE) + return ExitCheckTuple(exit_type=ExitType.NONE) def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, @@ -973,11 +973,11 @@ class IStrategy(ABC, HyperStrategyMixin): if ((sl_higher_long or sl_lower_short) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): - sell_type = SellType.STOP_LOSS + sell_type = ExitType.STOP_LOSS # If initial stoploss is not the same as current one then it is trailing. if trade.initial_stop_loss != trade.stop_loss: - sell_type = SellType.TRAILING_STOP_LOSS + sell_type = ExitType.TRAILING_STOP_LOSS logger.debug( f"{trade.pair} - HIT STOP: current price at " f"{((high if trade.is_short else low) or current_rate):.6f}, " @@ -994,7 +994,7 @@ class IStrategy(ABC, HyperStrategyMixin): return ExitCheckTuple(exit_type=sell_type) - return ExitCheckTuple(exit_type=SellType.NONE) + return ExitCheckTuple(exit_type=ExitType.NONE) def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: """ diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 7bdc940df..4ac27adc0 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -12,7 +12,7 @@ from pandas import DataFrame, to_datetime from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo -from freqtrade.enums import SellType +from freqtrade.enums import ExitType from freqtrade.exceptions import OperationalException from tests.conftest import get_patched_freqtradebot, log_has from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, @@ -95,8 +95,8 @@ tc1 = BTContainer(data=[ [6, 5000, 5025, 4975, 4987, 6172, 0, 0], # should sell ], stop_loss=-0.99, roi={"0": float('inf')}, profit_perc=0.00, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=2), - BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=4, close_tick=6)] + trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=2), + BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=4, close_tick=6)] ) # 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss @@ -107,7 +107,7 @@ tc2 = BTContainer(data=[ [2, 5000, 5025, 4975, 4987, 6172, 0, 0], ], stop_loss=-0.01, roi={"0": float('inf')}, profit_perc=-0.01, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)] ) # 4) Entered, sl 3 %, candle drops 4%, recovers to 1 % = > Trade closed, 3 % loss @@ -118,7 +118,7 @@ tc3 = BTContainer(data=[ [2, 5000, 5025, 4975, 4987, 6172, 0, 0], ], stop_loss=-0.03, roi={"0": float('inf')}, profit_perc=-0.03, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)] ) # 5) Stoploss and sell are hit. should sell on stoploss @@ -129,7 +129,7 @@ tc4 = BTContainer(data=[ [2, 5000, 5025, 4975, 4987, 6172, 0, 0], ], stop_loss=-0.03, roi={"0": float('inf')}, profit_perc=-0.03, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)] ) TESTS = [ diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 43ad6ecd8..ad14125b5 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -3,7 +3,7 @@ from typing import Dict, List, NamedTuple, Optional import arrow from pandas import DataFrame -from freqtrade.enums import SellType +from freqtrade.enums import ExitType from freqtrade.exchange import timeframe_to_minutes @@ -15,7 +15,7 @@ class BTrade(NamedTuple): """ Minimalistic Trade result used for functional backtesting """ - sell_reason: SellType + sell_reason: ExitType open_tick: int close_tick: int enter_tag: Optional[str] = None diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 8c7fa3ac9..019e367da 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -5,7 +5,7 @@ from pathlib import Path import pandas as pd import pytest -from freqtrade.enums import RunMode, SellType +from freqtrade.enums import RunMode, ExitType from freqtrade.optimize.hyperopt import Hyperopt from tests.conftest import patch_exchange @@ -44,7 +44,7 @@ def hyperopt_results(): 'profit_abs': [-0.2, 0.4, -0.2, 0.6], 'trade_duration': [10, 30, 10, 10], 'amount': [0.1, 0.1, 0.1, 0.1], - 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.STOP_LOSS, SellType.ROI], + 'sell_reason': [ExitType.STOP_LOSS, ExitType.ROI, ExitType.STOP_LOSS, ExitType.ROI], 'open_date': [ datetime(2019, 1, 1, 9, 15, 0), diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 7ede1adc3..c5aaab5e6 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from freqtrade.data.history import get_timerange -from freqtrade.enums import SellType +from freqtrade.enums import ExitType from freqtrade.optimize.backtesting import Backtesting from tests.conftest import patch_exchange from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, @@ -23,7 +23,7 @@ tc0 = BTContainer(data=[ [4, 5010, 5011, 4977, 4995, 6172, 0, 0], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] + trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) # Test 1: Stop-Loss Triggered 1% loss @@ -37,7 +37,7 @@ tc1 = BTContainer(data=[ [4, 4977, 4995, 4977, 4995, 6172, 0, 0], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2)] ) @@ -52,7 +52,7 @@ tc2 = BTContainer(data=[ [4, 4962, 4987, 4937, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.03, roi={"0": 1}, profit_perc=-0.03, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=3)] ) @@ -72,8 +72,8 @@ tc3 = BTContainer(data=[ [5, 4962, 4987, 4000, 4000, 6172, 0, 0], # exit with stoploss hit [6, 4950, 4975, 4950, 4950, 6172, 0, 0]], stop_loss=-0.02, roi={"0": 1}, profit_perc=-0.04, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2), - BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2), + BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=4, close_tick=5)] ) # Test 4: Minus 3% / recovery +15% @@ -89,7 +89,7 @@ tc4 = BTContainer(data=[ [4, 4962, 4987, 4937, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.02, roi={"0": 0.06}, profit_perc=-0.02, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2)] ) # Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain @@ -103,7 +103,7 @@ tc5 = BTContainer(data=[ [4, 4962, 4987, 4962, 4972, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 0.03}, profit_perc=0.03, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) # Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss @@ -117,7 +117,7 @@ tc6 = BTContainer(data=[ [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.02, roi={"0": 0.05}, profit_perc=-0.02, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2)] ) # Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain @@ -131,7 +131,7 @@ tc7 = BTContainer(data=[ [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.02, roi={"0": 0.03}, profit_perc=0.03, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2)] ) @@ -145,7 +145,7 @@ tc8 = BTContainer(data=[ [3, 4850, 5050, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.055, trailing_stop=True, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) @@ -159,7 +159,7 @@ tc9 = BTContainer(data=[ [3, 5000, 5200, 4550, 4850, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.064, trailing_stop=True, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) # Test 10: trailing_stop should raise so candle 3 causes a stoploss @@ -175,7 +175,7 @@ tc10 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.1, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.10, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=4)] ) # Test 11: trailing_stop should raise so candle 3 causes a stoploss @@ -191,7 +191,7 @@ tc11 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) # Test 12: trailing_stop should raise in candle 2 and cause a stoploss in the same candle @@ -207,7 +207,7 @@ tc12 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] ) # Test 13: Buy and sell ROI on same candle @@ -220,7 +220,7 @@ tc13 = BTContainer(data=[ [3, 4850, 5050, 4750, 4750, 6172, 0, 0], [4, 4750, 4950, 4750, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=1)] ) # Test 14 - Buy and Stoploss on same candle @@ -233,7 +233,7 @@ tc14 = BTContainer(data=[ [3, 4850, 5050, 4750, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.05, roi={"0": 0.10}, profit_perc=-0.05, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)] ) @@ -247,8 +247,8 @@ tc15 = BTContainer(data=[ [3, 4850, 5050, 4750, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.05, roi={"0": 0.01}, profit_perc=-0.04, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1), - BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=1), + BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=2, close_tick=2)] ) # Test 16: Buy, hold for 65 min, then forcesell using roi=-1 @@ -263,7 +263,7 @@ tc16 = BTContainer(data=[ [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "65": -1}, profit_perc=-0.012, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) # Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 @@ -279,7 +279,7 @@ tc17 = BTContainer(data=[ [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) @@ -295,7 +295,7 @@ tc18 = BTContainer(data=[ [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.04, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) # Test 19: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. @@ -310,7 +310,7 @@ tc19 = BTContainer(data=[ [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4550, 4975, 4550, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) # Test 20: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. @@ -325,7 +325,7 @@ tc20 = BTContainer(data=[ [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4925, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) # Test 21: trailing_stop ROI collision. @@ -342,7 +342,7 @@ tc21 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2)] ) # Test 22: trailing_stop Raises in candle 2 - but ROI applies at the same time. @@ -358,7 +358,7 @@ tc22 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2)] ) @@ -375,7 +375,7 @@ tc22s = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2, is_short=True)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2, is_short=True)] ) # Test 23: trailing_stop Raises in candle 2 (does not trigger) @@ -394,7 +394,7 @@ tc23 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.1, "119": 0.03}, profit_perc=0.03, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) # Test 24: Sell with signal sell in candle 3 (stoploss also triggers on this candle) @@ -409,7 +409,7 @@ tc24 = BTContainer(data=[ [4, 5010, 5010, 4977, 4995, 6172, 0, 0], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True, - trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=3)] ) # Test 25: Sell with signal sell in candle 3 (stoploss also triggers on this candle) @@ -424,7 +424,7 @@ tc25 = BTContainer(data=[ [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] + trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) # Test 25l: (copy of test25 with leverage) @@ -441,7 +441,7 @@ tc25l = BTContainer(data=[ [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, leverage=5.0, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] + trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) # Test 25s: (copy of test25 with leverage and as short) @@ -458,7 +458,7 @@ tc25s = BTContainer(data=[ [5, 4995, 4995, 4950, 4950, 6172, 0, 0, 0, 0]], stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, leverage=5.0, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4, is_short=True)] + trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4, is_short=True)] ) # Test 26: Sell with signal sell in candle 3 (ROI at signal candle) # Stoploss at 10% (irrelevant), ROI at 5% (will trigger) @@ -472,7 +472,7 @@ tc26 = BTContainer(data=[ [4, 5010, 5010, 4855, 4995, 6172, 0, 0], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) # Test 27: Sell with signal sell in candle 3 (ROI at signal candle) @@ -486,7 +486,7 @@ tc27 = BTContainer(data=[ [4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.002, use_sell_signal=True, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] + trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) # Test 28: trailing_stop should raise so candle 3 causes a stoploss @@ -503,7 +503,7 @@ tc28 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.03, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) # Test 28s: trailing_stop should raise so candle 3 causes a stoploss @@ -521,7 +521,7 @@ tc28s = BTContainer(data=[ trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_stop_positive=0.03, trades=[ - BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True) + BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True) ] ) @@ -537,7 +537,7 @@ tc29 = BTContainer(data=[ [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.02, trailing_stop=True, trailing_stop_positive=0.03, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] ) # Test 30: trailing_stop should be triggered immediately on trade open candle. @@ -551,7 +551,7 @@ tc30 = BTContainer(data=[ [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, trailing_stop_positive=0.01, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] ) # Test 31: trailing_stop should be triggered immediately on trade open candle. @@ -566,7 +566,7 @@ tc31 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.01, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_stop_positive=0.01, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] ) # Test 32: trailing_stop should be triggered immediately on trade open candle. @@ -581,7 +581,7 @@ tc32 = BTContainer(data=[ stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_stop_positive=0.01, use_custom_stoploss=True, - trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] ) # Test 33: trailing_stop should be triggered immediately on trade open candle. @@ -597,7 +597,7 @@ tc33 = BTContainer(data=[ trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_stop_positive=0.01, use_custom_stoploss=True, trades=[BTrade( - sell_reason=SellType.TRAILING_STOP_LOSS, + sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1, enter_tag='buy_signal_01' @@ -617,7 +617,7 @@ tc33s = BTContainer(data=[ trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_stop_positive=0.01, use_custom_stoploss=True, trades=[BTrade( - sell_reason=SellType.TRAILING_STOP_LOSS, + sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1, enter_tag='short_signal_01', @@ -647,7 +647,7 @@ tc35 = BTContainer(data=[ [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, custom_entry_price=7200, trades=[ - BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1) + BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1) ]) # Test 35s: Custom-entry-price above all candles should have rate adjusted to "entry candle high" @@ -661,7 +661,7 @@ tc35s = BTContainer(data=[ stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, custom_entry_price=4000, trades=[ - BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True) + BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True) ] ) @@ -678,7 +678,7 @@ tc36 = BTContainer(data=[ [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, custom_entry_price=4952, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2)] ) # Test 37: Custom-entry-price around candle low @@ -693,7 +693,7 @@ tc37 = BTContainer(data=[ [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, custom_entry_price=4952, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=1)] ) # Test 38: Custom exit price below all candles @@ -708,7 +708,7 @@ tc38 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, use_sell_signal=True, custom_exit_price=4552, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)] + trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=3)] ) # Test 39: Custom exit price above all candles @@ -723,7 +723,7 @@ tc39 = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, use_sell_signal=True, custom_exit_price=6052, - trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] + trades=[BTrade(sell_reason=ExitType.FORCE_SELL, open_tick=1, close_tick=4)] ) # Test 39: Custom short exit price above below candles @@ -738,7 +738,7 @@ tc39a = BTContainer(data=[ stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, use_sell_signal=True, custom_exit_price=4700, - trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4, is_short=True)] + trades=[BTrade(sell_reason=ExitType.FORCE_SELL, open_tick=1, close_tick=4, is_short=True)] ) # Test 40: Colliding long and short signal diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 6f2e95985..c8ea515f1 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -19,7 +19,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.enums import RunMode, SellType +from freqtrade.enums import RunMode, ExitType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.misc import get_strategy_run_id @@ -713,7 +713,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: # No data available. res = backtesting._get_sell_trade_entry(trade, row_sell) assert res is not None - assert res.sell_reason == SellType.ROI.value + assert res.sell_reason == ExitType.ROI.value assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc) # Enter new trade @@ -732,7 +732,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: res = backtesting._get_sell_trade_entry(trade, row_sell) assert res is not None - assert res.sell_reason == SellType.ROI.value + assert res.sell_reason == ExitType.ROI.value # Sell at minute 3 (not available above!) assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc) sell_order = res.select_order('sell', True) @@ -781,7 +781,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.value, SellType.ROI.value], + 'sell_reason': [ExitType.ROI.value, ExitType.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], @@ -1219,7 +1219,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'close_rate': [0.104969, 0.103541], "is_short": [False, False], - 'sell_reason': [SellType.ROI, SellType.ROI] + 'sell_reason': [ExitType.ROI, ExitType.ROI] }) result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], 'profit_ratio': [0.03, 0.01, 0.1], @@ -1237,7 +1237,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'open_rate': [0.104445, 0.10302485, 0.122541], 'close_rate': [0.104969, 0.103541, 0.123541], "is_short": [False, False, False], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + 'sell_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS] }) backtestmock = MagicMock(side_effect=[ { @@ -1337,7 +1337,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, 'stake_amount': [0.01, 0.01], 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], - 'sell_reason': [SellType.ROI, SellType.ROI] + 'sell_reason': [ExitType.ROI, ExitType.ROI] }) result2 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT', 'XRP/USDT'], 'profit_ratio': [0.03, 0.01, 0.1], @@ -1355,7 +1355,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, '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] + 'sell_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS] }) backtestmock = MagicMock(side_effect=[ { @@ -1440,7 +1440,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, 'stake_amount': [0.01, 0.01], 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], - 'sell_reason': [SellType.ROI, SellType.ROI] + 'sell_reason': [ExitType.ROI, ExitType.ROI] }) result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], 'profit_ratio': [0.03, 0.01, 0.1], @@ -1458,7 +1458,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, '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] + 'sell_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS] }) backtestmock = MagicMock(side_effect=[ { diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index f8586ffb6..95847c660 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -8,7 +8,7 @@ from arrow import Arrow from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import get_timerange -from freqtrade.enums import SellType +from freqtrade.enums import ExitType from freqtrade.optimize.backtesting import Backtesting from tests.conftest import patch_exchange @@ -60,7 +60,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> 'trade_duration': [200, 40], 'profit_ratio': [0.0, 0.0], 'profit_abs': [0.0, 0.0], - 'sell_reason': [SellType.ROI.value, SellType.ROI.value], + 'sell_reason': [ExitType.ROI.value, ExitType.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_hyperopt.py b/tests/optimize/test_hyperopt.py index 2b437ad92..d1a9134cf 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -10,7 +10,7 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data -from freqtrade.enums import RunMode, SellType +from freqtrade.enums import RunMode, ExitType from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto @@ -357,8 +357,8 @@ def test_hyperopt_format_results(hyperopt): "is_open": [False, False, False, True], "is_short": [False, False, False, False], "stake_amount": [0.01, 0.01, 0.01, 0.01], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] + "sell_reason": [ExitType.ROI, ExitType.STOP_LOSS, + ExitType.ROI, ExitType.FORCE_SELL] }), 'config': hyperopt.config, 'locks': [], @@ -428,8 +428,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "is_open": [False, False, False, True], "is_short": [False, False, False, False], "stake_amount": [0.01, 0.01, 0.01, 0.01], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] + "sell_reason": [ExitType.ROI, ExitType.STOP_LOSS, + ExitType.ROI, ExitType.FORCE_SELL] }), 'config': hyperopt_conf, 'locks': [], diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 4a6155441..3ff8d5870 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -12,7 +12,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, load_backtest_stats) from freqtrade.edge import PairInfo -from freqtrade.enums import SellType +from freqtrade.enums import ExitType from freqtrade.optimize.optimize_reports import (_get_resample_from_period, generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, @@ -77,8 +77,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): "is_open": [False, False, False, True], "is_short": [False, False, False, False], "stake_amount": [0.01, 0.01, 0.01, 0.01], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] + "sell_reason": [ExitType.ROI, ExitType.STOP_LOSS, + ExitType.ROI, ExitType.FORCE_SELL] }), 'config': default_conf, 'locks': [], @@ -129,8 +129,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): "is_open": [False, False, False, True], "is_short": [False, False, False, False], "stake_amount": [0.01, 0.01, 0.01, 0.01], - "sell_reason": [SellType.ROI, SellType.ROI, - SellType.STOP_LOSS, SellType.FORCE_SELL] + "sell_reason": [ExitType.ROI, ExitType.ROI, + ExitType.STOP_LOSS, ExitType.FORCE_SELL] }), 'config': default_conf, 'locks': [], @@ -276,7 +276,7 @@ def test_text_table_sell_reason(): 'wins': [2, 0, 0], 'draws': [0, 0, 0], 'losses': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + 'sell_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS] } ) @@ -308,7 +308,7 @@ def test_generate_sell_reason_stats(): 'wins': [2, 0, 0], 'draws': [0, 0, 0], 'losses': [0, 0, 1], - 'sell_reason': [SellType.ROI.value, SellType.ROI.value, SellType.STOP_LOSS.value] + 'sell_reason': [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value] } ) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index a3cb29c9d..69c42c93d 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -4,14 +4,14 @@ from datetime import datetime, timedelta import pytest from freqtrade import constants -from freqtrade.enums import SellType +from freqtrade.enums import ExitType from freqtrade.persistence import PairLocks, Trade from freqtrade.plugins.protectionmanager import ProtectionManager from tests.conftest import get_patched_freqtradebot, log_has_re def generate_mock_trade(pair: str, fee: float, is_open: bool, - sell_reason: str = SellType.SELL_SIGNAL, + sell_reason: str = ExitType.SELL_SIGNAL, min_ago_open: int = None, min_ago_close: int = None, profit_rate: float = 0.9 ): @@ -91,7 +91,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): caplog.clear() Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, )) @@ -100,12 +100,12 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): caplog.clear() # This trade does not count, as it's closed too long ago Trade.query.session.add(generate_mock_trade( - 'BCH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'BCH/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=250, min_ago_close=100, )) Trade.query.session.add(generate_mock_trade( - 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'ETH/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=240, min_ago_close=30, )) # 3 Trades closed - but the 2nd has been closed too long ago. @@ -114,7 +114,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): caplog.clear() Trade.query.session.add(generate_mock_trade( - 'LTC/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'LTC/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=180, min_ago_close=30, )) @@ -148,7 +148,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair caplog.clear() Trade.query.session.add(generate_mock_trade( - pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + pair, fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, profit_rate=0.9, )) @@ -158,12 +158,12 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair caplog.clear() # This trade does not count, as it's closed too long ago Trade.query.session.add(generate_mock_trade( - pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + pair, fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=250, min_ago_close=100, profit_rate=0.9, )) # Trade does not count for per pair stop as it's the wrong pair. Trade.query.session.add(generate_mock_trade( - 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'ETH/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=240, min_ago_close=30, profit_rate=0.9, )) # 3 Trades closed - but the 2nd has been closed too long ago. @@ -178,7 +178,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair # 2nd Trade that counts with correct pair Trade.query.session.add(generate_mock_trade( - pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + pair, fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=180, min_ago_close=30, profit_rate=0.9, )) @@ -203,7 +203,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): caplog.clear() Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, )) @@ -213,7 +213,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() Trade.query.session.add(generate_mock_trade( - 'ETH/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + 'ETH/BTC', fee.return_value, False, sell_reason=ExitType.ROI.value, min_ago_open=205, min_ago_close=35, )) @@ -242,7 +242,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): caplog.clear() Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=800, min_ago_close=450, profit_rate=0.9, )) @@ -253,7 +253,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=200, min_ago_close=120, profit_rate=0.9, )) @@ -265,14 +265,14 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): # Add positive trade Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.ROI.value, min_ago_open=20, min_ago_close=10, profit_rate=1.15, )) assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not PairLocks.is_pair_locked('XRP/BTC') Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=110, min_ago_close=20, profit_rate=0.8, )) @@ -300,15 +300,15 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): caplog.clear() Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) Trade.query.session.add(generate_mock_trade( - 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'ETH/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) Trade.query.session.add(generate_mock_trade( - 'NEO/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'NEO/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) # No losing trade yet ... so max_drawdown will raise exception @@ -316,7 +316,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not freqtrade.protections.stop_per_pair('XRP/BTC') Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=500, min_ago_close=400, profit_rate=0.9, )) # Not locked with one trade @@ -326,7 +326,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value, min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, )) @@ -339,7 +339,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): # Winning trade ... (should not lock, does not change drawdown!) Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.ROI.value, min_ago_open=320, min_ago_close=410, profit_rate=1.5, )) assert not freqtrade.protections.global_stop() @@ -349,7 +349,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): # Add additional negative trade, causing a loss of > 15% Trade.query.session.add(generate_mock_trade( - 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.ROI.value, min_ago_open=20, min_ago_close=10, profit_rate=0.8, )) assert not freqtrade.protections.stop_per_pair('XRP/BTC') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 70651e5cc..27b0074dd 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -18,7 +18,7 @@ from telegram.error import BadRequest, NetworkError, TelegramError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo -from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State +from freqtrade.enums import RPCMessageType, RunMode, ExitType, SignalDirection, State from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging @@ -1059,7 +1059,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'fiat_currency': 'USD', 'buy_tag': ANY, 'enter_tag': ANY, - 'sell_reason': SellType.FORCE_SELL.value, + 'sell_reason': ExitType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, @@ -1127,7 +1127,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'fiat_currency': 'USD', 'buy_tag': ANY, 'enter_tag': ANY, - 'sell_reason': SellType.FORCE_SELL.value, + 'sell_reason': ExitType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, @@ -1185,7 +1185,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'fiat_currency': 'USD', 'buy_tag': ANY, 'enter_tag': ANY, - 'sell_reason': SellType.FORCE_SELL.value, + 'sell_reason': ExitType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, @@ -1932,7 +1932,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'stake_currency': 'ETH', 'fiat_currency': 'USD', 'enter_tag': 'buy_signal1', - 'sell_reason': SellType.STOP_LOSS.value, + 'sell_reason': ExitType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(hours=-1), 'close_date': arrow.utcnow(), }) @@ -1966,7 +1966,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', 'enter_tag': 'buy_signal1', - 'sell_reason': SellType.STOP_LOSS.value, + 'sell_reason': ExitType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'close_date': arrow.utcnow(), }) @@ -2045,7 +2045,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction, 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', 'enter_tag': enter_signal, - 'sell_reason': SellType.STOP_LOSS.value, + 'sell_reason': ExitType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'close_date': arrow.utcnow(), }) @@ -2169,7 +2169,7 @@ def test_send_msg_sell_notification_no_fiat( 'stake_currency': 'ETH', 'fiat_currency': 'USD', 'enter_tag': enter_signal, - 'sell_reason': SellType.STOP_LOSS.value, + 'sell_reason': ExitType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'close_date': arrow.utcnow(), }) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 69a2d79fb..90d72252e 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from requests import RequestException -from freqtrade.enums import RPCMessageType, SellType +from freqtrade.enums import RPCMessageType, ExitType from freqtrade.rpc import RPC from freqtrade.rpc.webhook import Webhook from tests.conftest import get_patched_freqtradebot, log_has @@ -244,7 +244,7 @@ def test_send_msg_webhook(default_conf, mocker): 'profit_amount': 0.001, 'profit_ratio': 0.20, 'stake_currency': 'BTC', - 'sell_reason': SellType.STOP_LOSS.value + 'sell_reason': ExitType.STOP_LOSS.value } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 @@ -269,7 +269,7 @@ def test_send_msg_webhook(default_conf, mocker): 'profit_amount': 0.001, 'profit_ratio': 0.20, 'stake_currency': 'BTC', - 'sell_reason': SellType.STOP_LOSS.value + 'sell_reason': ExitType.STOP_LOSS.value } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 @@ -294,7 +294,7 @@ def test_send_msg_webhook(default_conf, mocker): 'profit_amount': 0.001, 'profit_ratio': 0.20, 'stake_currency': 'BTC', - 'sell_reason': SellType.STOP_LOSS.value + 'sell_reason': ExitType.STOP_LOSS.value } webhook.send_msg(msg=msg) assert msg_mock.call_count == 1 diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5b7519d82..a5325a680 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -11,7 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.enums import ExitCheckTuple, SellType, SignalDirection +from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.optimize.space import SKDecimal from freqtrade.persistence import PairLocks, Trade @@ -409,22 +409,22 @@ def test_min_roi_reached3(default_conf, fee) -> None: 'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, # enable custom stoploss, expected after 1st call, expected after 2nd call - (0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE, None), - (0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS, None), - (0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS, None), - (0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE, None), - (0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS, None), + (0.2, 0.9, ExitType.NONE, False, False, 0.3, 0.9, ExitType.NONE, None), + (0.2, 0.9, ExitType.NONE, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None), + (0.2, 1.14, ExitType.NONE, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, None), + (0.01, 0.96, ExitType.NONE, True, False, 0.05, 1, ExitType.NONE, None), + (0.05, 1, ExitType.NONE, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None), # Default custom case - trails with 10% - (0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE, None), - (0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS, None), - (0.05, 1, SellType.NONE, False, True, -0.06, 1, SellType.TRAILING_STOP_LOSS, + (0.05, 0.95, ExitType.NONE, False, True, -0.02, 0.95, ExitType.NONE, None), + (0.05, 0.95, ExitType.NONE, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, None), + (0.05, 1, ExitType.NONE, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS, lambda **kwargs: -0.05), - (0.05, 1, SellType.NONE, False, True, 0.09, 1.04, SellType.NONE, + (0.05, 1, ExitType.NONE, False, True, 0.09, 1.04, ExitType.NONE, lambda **kwargs: -0.05), - (0.05, 0.95, SellType.NONE, False, True, 0.09, 0.98, SellType.NONE, + (0.05, 0.95, ExitType.NONE, False, True, 0.09, 0.98, ExitType.NONE, lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)), # Error case - static stoploss in place - (0.05, 0.9, SellType.NONE, False, True, 0.09, 0.9, SellType.NONE, + (0.05, 0.9, ExitType.NONE, False, True, 0.09, 0.9, ExitType.NONE, lambda **kwargs: None), ]) def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, @@ -456,7 +456,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili force_stoploss=0, high=None) assert isinstance(sl_flag, ExitCheckTuple) assert sl_flag.exit_type == expected - if expected == SellType.NONE: + if expected == ExitType.NONE: assert sl_flag.exit_flag is False else: assert sl_flag.exit_flag is True @@ -467,7 +467,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili current_time=now, current_profit=profit2, force_stoploss=0, high=None) assert sl_flag.exit_type == expected2 - if expected2 == SellType.NONE: + if expected2 == ExitType.NONE: assert sl_flag.exit_flag is False else: assert sl_flag.exit_flag is True @@ -496,14 +496,14 @@ def test_custom_exit(default_conf, fee, caplog) -> None: low=None, high=None) assert res.exit_flag is False - assert res.exit_type == SellType.NONE + assert res.exit_type == ExitType.NONE strategy.custom_exit = MagicMock(return_value=True) res = strategy.should_exit(trade, 1, now, enter=False, exit_=False, low=None, high=None) assert res.exit_flag is True - assert res.exit_type == SellType.CUSTOM_SELL + assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_reason == 'custom_sell' strategy.custom_exit = MagicMock(return_value='hello world') @@ -511,7 +511,7 @@ def test_custom_exit(default_conf, fee, caplog) -> None: res = strategy.should_exit(trade, 1, now, enter=False, exit_=False, low=None, high=None) - assert res.exit_type == SellType.CUSTOM_SELL + assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_flag is True assert res.exit_reason == 'hello world' @@ -520,7 +520,7 @@ def test_custom_exit(default_conf, fee, caplog) -> None: res = strategy.should_exit(trade, 1, now, enter=False, exit_=False, low=None, high=None) - assert res.exit_type == SellType.CUSTOM_SELL + assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_flag is True assert res.exit_reason == 'h' * 64 assert log_has_re('Custom sell reason returned from custom_exit is too long.*', caplog) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index eaf6044d0..5e5f1cc3c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -13,7 +13,7 @@ import pytest from pandas import DataFrame from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT -from freqtrade.enums import (CandleType, ExitCheckTuple, RPCMessageType, RunMode, SellType, +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RPCMessageType, RunMode, SignalDirection, State) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, @@ -236,7 +236,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, assert freqtrade.handle_trade(trade) is not ignore_strat_sl if not ignore_strat_sl: assert log_has_re('Exit for NEO/BTC detected. Reason: stop_loss.*', caplog) - assert trade.sell_reason == SellType.STOP_LOSS.value + assert trade.sell_reason == ExitType.STOP_LOSS.value def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -> None: @@ -1209,7 +1209,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id is None assert trade.is_open is False - assert trade.sell_reason == str(SellType.EMERGENCY_SELL) + assert trade.sell_reason == str(ExitType.EMERGENCY_SELL) @pytest.mark.parametrize("is_short", [False, True]) @@ -1292,7 +1292,7 @@ def test_create_stoploss_order_invalid_order( caplog.clear() freqtrade.create_stoploss_order(trade, 200) assert trade.stoploss_order_id is None - assert trade.sell_reason == SellType.EMERGENCY_SELL.value + assert trade.sell_reason == ExitType.EMERGENCY_SELL.value assert log_has("Unable to place a stoploss order on exchange. ", caplog) assert log_has("Exiting the trade forcefully", caplog) @@ -1304,7 +1304,7 @@ def test_create_stoploss_order_invalid_order( # Rpc is sending first buy, then sell assert rpc_mock.call_count == 2 - assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == SellType.EMERGENCY_SELL.value + assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == ExitType.EMERGENCY_SELL.value assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' @@ -2274,7 +2274,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee, caplog.clear() patch_get_signal(freqtrade) assert freqtrade.handle_trade(trade) - assert log_has("ETH/USDT - Required profit reached. sell_type=SellType.ROI", + assert log_has("ETH/USDT - Required profit reached. sell_type=ExitType.ROI", caplog) @@ -2316,7 +2316,7 @@ def test_handle_trade_use_sell_signal( else: patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) - assert log_has("ETH/USDT - Sell signal received. sell_type=SellType.SELL_SIGNAL", + assert log_has("ETH/USDT - Sell signal received. sell_type=ExitType.SELL_SIGNAL", caplog) @@ -3100,7 +3100,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), - exit_check=ExitCheckTuple(exit_type=SellType.ROI) + exit_check=ExitCheckTuple(exit_type=ExitType.ROI) ) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -3112,7 +3112,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), - exit_check=ExitCheckTuple(exit_type=SellType.ROI) + exit_check=ExitCheckTuple(exit_type=ExitType.ROI) ) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -3137,7 +3137,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'profit_ratio': 0.00493809 if is_short else 0.09451372, 'stake_currency': 'USDT', 'fiat_currency': 'USD', - 'sell_reason': SellType.ROI.value, + 'sell_reason': ExitType.ROI.value, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, @@ -3173,7 +3173,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd ) freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down)()['bid'], - exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS)) + exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -3196,7 +3196,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'profit_ratio': -0.0945681 if is_short else -1.247e-05, 'stake_currency': 'USDT', 'fiat_currency': 'USD', - 'sell_reason': SellType.STOP_LOSS.value, + 'sell_reason': ExitType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, @@ -3248,7 +3248,7 @@ def test_execute_trade_exit_custom_exit_price( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - exit_check=ExitCheckTuple(exit_type=SellType.SELL_SIGNAL) + exit_check=ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL) ) # Sell price must be different to default bid price @@ -3276,7 +3276,7 @@ def test_execute_trade_exit_custom_exit_price( 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', 'fiat_currency': 'USD', - 'sell_reason': SellType.SELL_SIGNAL.value, + 'sell_reason': ExitType.SELL_SIGNAL.value, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, @@ -3319,7 +3319,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99 freqtrade.execute_trade_exit( trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down())['bid'], - exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS)) + exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -3343,7 +3343,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'profit_ratio': -0.00501253 if is_short else -0.01493766, 'stake_currency': 'USDT', 'fiat_currency': 'USD', - 'sell_reason': SellType.STOP_LOSS.value, + 'sell_reason': ExitType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, @@ -3379,7 +3379,7 @@ def test_execute_trade_exit_sloe_cancel_exception( trade.stoploss_order_id = "abcd" freqtrade.execute_trade_exit(trade=trade, limit=1234, - exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS)) + exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) assert create_order_mock.call_count == 2 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -3434,7 +3434,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS) + exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS) ) trade = Trade.query.first() @@ -3510,7 +3510,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None assert trade.is_open is False - assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value + assert trade.sell_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 3 if is_short: assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.SHORT @@ -3579,7 +3579,7 @@ def test_execute_trade_exit_market_order( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - exit_check=ExitCheckTuple(exit_type=SellType.ROI) + exit_check=ExitCheckTuple(exit_type=ExitType.ROI) ) assert not trade.is_open @@ -3606,7 +3606,7 @@ def test_execute_trade_exit_market_order( 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', 'fiat_currency': 'USD', - 'sell_reason': SellType.ROI.value, + 'sell_reason': ExitType.ROI.value, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, @@ -3643,7 +3643,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u fetch_ticker=ticker_usdt_sell_up ) - sell_reason = ExitCheckTuple(exit_type=SellType.ROI) + sell_reason = ExitCheckTuple(exit_type=ExitType.ROI) assert not freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], @@ -3654,18 +3654,18 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u @pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type,is_short', [ # Enable profit - (True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value, False), - (True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value, True), + (True, 2.18, 2.2, False, True, ExitType.SELL_SIGNAL.value, False), + (True, 2.18, 2.2, False, True, ExitType.SELL_SIGNAL.value, True), # # Disable profit - (False, 3.19, 3.2, True, False, SellType.SELL_SIGNAL.value, False), - (False, 3.19, 3.2, True, False, SellType.SELL_SIGNAL.value, True), + (False, 3.19, 3.2, True, False, ExitType.SELL_SIGNAL.value, False), + (False, 3.19, 3.2, True, False, ExitType.SELL_SIGNAL.value, True), # # Enable loss - # # * Shouldn't this be SellType.STOP_LOSS.value + # # * Shouldn't this be ExitType.STOP_LOSS.value (True, 0.21, 0.22, False, False, None, False), (True, 2.41, 2.42, False, False, None, True), # Disable loss - (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value, False), - (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value, True), + (False, 0.10, 0.22, True, False, ExitType.SELL_SIGNAL.value, False), + (False, 0.10, 0.22, True, False, ExitType.SELL_SIGNAL.value, True), ]) def test_sell_profit_only( default_conf_usdt, limit_order, limit_order_open, is_short, @@ -3693,11 +3693,11 @@ def test_sell_profit_only( }) freqtrade = FreqtradeBot(default_conf_usdt) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - if sell_type == SellType.SELL_SIGNAL.value: + if sell_type == ExitType.SELL_SIGNAL.value: freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) else: freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple( - exit_type=SellType.NONE)) + exit_type=ExitType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() @@ -3816,7 +3816,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_down()['ask' if is_short else 'bid'], - exit_check=ExitCheckTuple(exit_type=SellType.STOP_LOSS) + exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS) ) trade.close(ticker_usdt_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) @@ -3874,7 +3874,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_op else: patch_get_signal(freqtrade, enter_long=False, exit_long=False) assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.ROI.value + assert trade.sell_reason == ExitType.ROI.value @pytest.mark.parametrize("is_short,val1,val2", [ @@ -3936,7 +3936,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open, f"stoploss is {(2.0 * val1 * stop_multi):6f}, " f"initial stoploss was at {(2.0 * stop_multi):6f}, trade opened at 2.000000", caplog) - assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value + assert trade.sell_reason == ExitType.TRAILING_STOP_LOSS.value @pytest.mark.parametrize('offset,trail_if_reached,second_sl,is_short', [ @@ -4042,7 +4042,7 @@ def test_trailing_stop_loss_positive( f"initial stoploss was at {'2.42' if is_short else '1.80'}0000, " f"trade opened at {2.2 if is_short else 2.0}00000", caplog) - assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value + assert trade.sell_reason == ExitType.TRAILING_STOP_LOSS.value @pytest.mark.parametrize("is_short", [False, True]) @@ -4088,7 +4088,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_ # Test if entry-signal is absent patch_get_signal(freqtrade) assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.ROI.value + assert trade.sell_reason == ExitType.ROI.value def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fee, fee, caplog, @@ -5145,7 +5145,7 @@ def test_update_funding_fees( trade=trade, # The values of the next 2 params are irrelevant for this test limit=ticker_usdt_sell_up()['bid'], - exit_check=ExitCheckTuple(exit_type=SellType.ROI) + exit_check=ExitCheckTuple(exit_type=ExitType.ROI) ) assert trade.funding_fees == pytest.approx(sum( trade.amount * diff --git a/tests/test_integration.py b/tests/test_integration.py index 692886cf3..606290495 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.enums import ExitCheckTuple, SellType +from freqtrade.enums import ExitCheckTuple, ExitType from freqtrade.persistence import Trade from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC @@ -52,8 +52,8 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) # Sell 3rd trade (not called for the first trade) should_sell_mock = MagicMock(side_effect=[ - ExitCheckTuple(exit_type=SellType.NONE), - ExitCheckTuple(exit_type=SellType.SELL_SIGNAL)] + ExitCheckTuple(exit_type=ExitType.NONE), + ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL)] ) cancel_order_mock = MagicMock() mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) @@ -115,7 +115,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, assert wallets_mock.call_count == 4 trade = trades[0] - assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value + assert trade.sell_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert not trade.is_open trade = trades[1] @@ -123,7 +123,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, assert trade.is_open trade = trades[2] - assert trade.sell_reason == SellType.SELL_SIGNAL.value + assert trade.sell_reason == ExitType.SELL_SIGNAL.value assert not trade.is_open @@ -160,11 +160,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati _notify_exit=MagicMock(), ) should_sell_mock = MagicMock(side_effect=[ - ExitCheckTuple(exit_type=SellType.NONE), - ExitCheckTuple(exit_type=SellType.SELL_SIGNAL), - ExitCheckTuple(exit_type=SellType.NONE), - ExitCheckTuple(exit_type=SellType.NONE), - ExitCheckTuple(exit_type=SellType.NONE)] + ExitCheckTuple(exit_type=ExitType.NONE), + ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL), + ExitCheckTuple(exit_type=ExitType.NONE), + ExitCheckTuple(exit_type=ExitType.NONE), + ExitCheckTuple(exit_type=ExitType.NONE)] ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) From 5f71232d6f488a3b3d9d6e4384ab05cd24e9e424 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 07:00:35 +0100 Subject: [PATCH 12/26] Fix doc typo --- docs/strategy-customization.md | 2 +- freqtrade/edge/edge_positioning.py | 3 +-- freqtrade/rpc/rpc.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4b505d400..900c6d1b4 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -908,7 +908,7 @@ In some situations it may be confusing to deal with stops relative to current ra ??? Example "Returning a stoploss using absolute price from the custom stoploss function" - If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)`. + If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)`. ``` python diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index d6df86dea..8116949cf 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -13,8 +13,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data -from freqtrade.enums import RunMode, ExitType -from freqtrade.enums.candletype import CandleType +from freqtrade.enums import CandleType, ExitType, RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4934343b6..5b59da1fc 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -18,7 +18,7 @@ from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data -from freqtrade.enums import ExitType, SignalDirection, State, TradingMode +from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection, State, TradingMode from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -27,7 +27,6 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter -from freqtrade.strategy.interface import ExitCheckTuple from freqtrade.wallets import PositionWallet, Wallet From a55bc9c1e4898b970f85ae477094ccb9c98212d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 08:02:27 +0100 Subject: [PATCH 13/26] Pin jinja in docs requirements --- docs/requirements-docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 01ce559ff..3afc212d3 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -2,3 +2,4 @@ mkdocs==1.2.3 mkdocs-material==8.2.5 mdx_truly_sane_lists==1.2 pymdown-extensions==9.3 +jinja2==3.0.3 From a004bcf00f7740c16577da59de41d4fb65f0b4ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 08:03:32 +0100 Subject: [PATCH 14/26] Small refactor to backtesting --- docs/strategy-customization.md | 2 +- freqtrade/optimize/backtesting.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 900c6d1b4..fd2119753 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -1092,7 +1092,7 @@ When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are The following rules apply, and entry signals will be ignored if more than one of the 3 signals is set: -- `enter_long` -> `exit_long`, `exit_short` +- `enter_long` -> `exit_long`, `enter_short` - `enter_short` -> `exit_short`, `enter_long` ## Further strategy ideas diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 082effdf2..6c48c841a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,7 +19,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, MarginMode, ExitType, +from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType, MarginMode, TradingMode) from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -129,12 +129,9 @@ class Backtesting: self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) - self.margin_mode: MarginMode = config.get('margin_mode', MarginMode.NONE) # strategies which define "can_short=True" will fail to load in Spot mode. self._can_short = self.trading_mode != TradingMode.SPOT - self.progress = BTProgress() - self.abort = False self.init_backtest() def __del__(self): From 054b63700180913d88535e79c5e6d3bde244ae81 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 25 Mar 2022 06:56:05 -0600 Subject: [PATCH 15/26] Add amount_to_contracts and order_contracts_to_amount to stoploss --- freqtrade/exchange/exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fe47ca4d1..6ce6e3d54 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1103,11 +1103,12 @@ class Exchange: if self.trading_mode == TradingMode.FUTURES: params['reduceOnly'] = True - amount = self.amount_to_precision(pair, amount) + amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) self._lev_prep(pair, leverage, side) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=rate, params=params) + order = self._order_contracts_to_amount(order) logger.info(f"stoploss {user_order_type} order added for {pair}. " f"stop price: {stop_price}. limit: {rate}") self._log_exchange_response('create_stoploss_order', order) From d3ea14de68c62d54798c668e7aee77ee182e5f5b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 25 Mar 2022 07:21:31 -0600 Subject: [PATCH 16/26] test_stoploss_contract_size --- tests/exchange/test_exchange.py | 45 ++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9bb9db58f..23ff5a5c9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3966,7 +3966,7 @@ def test__fetch_and_calculate_funding_fees( exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( - return_value=['1h', '4h', '8h'])) + return_value=['1h', '4h', '8h'])) funding_fees = exchange._fetch_and_calculate_funding_fees( pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == expected_fees @@ -4787,3 +4787,46 @@ def test_get_liquidation_price( buffer_amount = liquidation_buffer * abs(open_rate - expected_liq) expected_liq = expected_liq - buffer_amount if is_short else expected_liq + buffer_amount isclose(expected_liq, liq) + + +@pytest.mark.parametrize('contract_size,order_amount', [ + (10, 10), + (0.01, 10000), +]) +def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amount): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + }, + 'amount': order_amount, + 'cost': order_amount, + 'filled': order_amount, + 'remaining': order_amount, + 'symbol': 'ETH/BTC', + }) + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange._get_contract_size = MagicMock(return_value=contract_size) + + api_mock.create_order.reset_mock() + order = exchange.stoploss( + pair='ETH/BTC', + amount=100, + stop_price=220, + order_types={}, + side='buy', + leverage=1.0 + ) + + assert api_mock.create_order.call_args_list[0][1]['amount'] == order_amount + assert order['amount'] == 100 + assert order['cost'] == 100 + assert order['filled'] == 100 + assert order['remaining'] == 100 From 1ab6773257704bc47e6f52f38b68a662adbb7906 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 15:17:46 +0100 Subject: [PATCH 17/26] Update todo-lev to todo --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4491d4e18..f2cd5e5ad 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -990,7 +990,8 @@ class FreqtradeBot(LoggingMixin): Check if trade is fulfilled in which case the stoploss on exchange should be added immediately if stoploss on exchange is enabled. - # TODO-lev: liquidation price always on exchange, even without stoploss_on_exchange + # TODO: liquidation price always on exchange, even without stoploss_on_exchange + # Therefore fetching account liquidations for open pairs may make sense. """ logger.debug('Handling stoploss on exchange %s ...', trade) From cd11ba3489a77da6cc35ee8ea15b2e828e3fab3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 15:36:30 +0100 Subject: [PATCH 18/26] Fix naming in interface.py --- freqtrade/strategy/interface.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8df8c88a0..da7f02912 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -136,8 +136,7 @@ class IStrategy(ABC, HyperStrategyMixin): cls_method = getattr(self.__class__, attr_name) if not callable(cls_method): continue - informative_data_list = getattr( - cls_method, '_ft_informative', None) + informative_data_list = getattr(cls_method, '_ft_informative', None) if not isinstance(informative_data_list, list): # Type check is required because mocker would return a mock object that evaluates to # True, confusing this code. @@ -945,9 +944,9 @@ class IStrategy(ABC, HyperStrategyMixin): else: logger.warning("CustomStoploss function did not return valid stoploss") - sl_lower_short = (trade.stop_loss < (low or current_rate) and not trade.is_short) - sl_higher_long = (trade.stop_loss > (high or current_rate) and trade.is_short) - if self.trailing_stop and (sl_lower_short or sl_higher_long): + sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short) + sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short) + if self.trailing_stop and (sl_lower_long or sl_higher_short): # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset From b419d0043c2fed1c4872f8d60100596ab0fa2b3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 15:36:52 +0100 Subject: [PATCH 19/26] Don't run CI directly on feat/short --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd3e9330f..216a53bc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,6 @@ on: branches: - stable - develop - - feat/short - ci/* tags: release: From 1c0946833da746b480f6ef88d4866d6a87824e17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 16:04:28 +0100 Subject: [PATCH 20/26] Fix bug in exit-count detection --- freqtrade/persistence/models.py | 6 ++---- tests/test_persistence.py | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e4559d1f6..f5dd5f1f2 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -688,7 +688,7 @@ class LocalTrade(): Get amount of failed exiting orders assumes full exits. """ - return len([o for o in self.orders if o.ft_order_side == 'sell']) + return len([o for o in self.orders if o.ft_order_side == self.exit_side]) def _calc_open_trade_value(self) -> float: """ @@ -706,16 +706,14 @@ class LocalTrade(): """ Recalculate open_trade_value. Must be called whenever open_rate, fee_open or is_short is changed. - """ self.open_trade_value = self._calc_open_trade_value() def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal: """ - : param interest_rate: interest_charge for borrowing this coin(optional). + :param interest_rate: interest_charge for borrowing this coin(optional). If interest_rate is not set self.interest_rate will be used """ - zero = Decimal(0.0) # If nothing was borrowed if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 207a37313..34fc98fef 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -2057,10 +2057,11 @@ def test_get_best_pair_lev(fee): @pytest.mark.usefixtures("init_persistence") -def test_get_exit_order_count(fee): +@pytest.mark.parametrize('is_short', [True, False]) +def test_get_exit_order_count(fee, is_short): - create_mock_trades_usdt(fee) - trade = Trade.get_trades([Trade.pair == 'ETC/USDT']).first() + create_mock_trades(fee, is_short=is_short) + trade = Trade.get_trades([Trade.pair == 'ETC/BTC']).first() assert trade.get_exit_order_count() == 1 From 50ba20ec9fe1cab05a2b9a4e652d26e7157fa97b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 16:14:18 +0100 Subject: [PATCH 21/26] Remove some unused test methods --- tests/conftest.py | 6 +----- tests/test_persistence.py | 8 ++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 05ff39358..117aeaaed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import re from copy import deepcopy from datetime import datetime, timedelta from pathlib import Path -from typing import Optional, Tuple +from typing import Optional from unittest.mock import MagicMock, Mock, PropertyMock import arrow @@ -360,10 +360,6 @@ def create_mock_trades_usdt(fee, use_db: bool = True): Trade.commit() -def get_sides(is_short: bool) -> Tuple[str, str]: - return ("sell", "buy") if is_short else ("buy", "sell") - - @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: """ diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 34fc98fef..b83b75cc2 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -15,8 +15,8 @@ from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids -from tests.conftest import (create_mock_trades, create_mock_trades_usdt, - create_mock_trades_with_leverage, get_sides, log_has, log_has_re) +from tests.conftest import (create_mock_trades, + create_mock_trades_with_leverage, log_has, log_has_re) spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES @@ -77,7 +77,7 @@ def test_init_dryrun_db(default_conf, tmpdir): @pytest.mark.parametrize('is_short', [False, True]) @pytest.mark.usefixtures("init_persistence") def test_enter_exit_side(fee, is_short): - enter_side, exit_side = get_sides(is_short) + enter_side, exit_side = ("sell", "buy") if is_short else ("buy", "sell") trade = Trade( id=2, pair='ADA/USDT', @@ -456,7 +456,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt - enter_side, exit_side = get_sides(is_short) + enter_side, exit_side = ("sell", "buy") if is_short else ("buy", "sell") trade = Trade( id=2, From 46f4227329a7493a12886b38f7f64391e6565ef6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 18:09:18 +0100 Subject: [PATCH 22/26] Check if symbol is not None --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6ce6e3d54..10e7056dc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -402,7 +402,7 @@ class Exchange: return trades def _order_contracts_to_amount(self, order: Dict) -> Dict: - if 'symbol' in order: + if 'symbol' in order and order['symbol'] is not None: contract_size = self._get_contract_size(order['symbol']) if contract_size != 1: for prop in ['amount', 'cost', 'filled', 'remaining']: @@ -1108,10 +1108,10 @@ class Exchange: self._lev_prep(pair, leverage, side) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=rate, params=params) + self._log_exchange_response('create_stoploss_order', order) order = self._order_contracts_to_amount(order) logger.info(f"stoploss {user_order_type} order added for {pair}. " f"stop price: {stop_price}. limit: {rate}") - self._log_exchange_response('create_stoploss_order', order) return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 23ff5a5c9..711b46e3e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4111,10 +4111,36 @@ def test__order_contracts_to_amount( 'trades': None, 'info': {}, }, + { + # Realistic stoploss order on gateio. + 'id': '123456380', + 'clientOrderId': '12345638203', + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'status': None, + 'symbol': None, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'side': None, + 'price': None, + 'stopPrice': None, + 'average': None, + 'amount': None, + 'cost': None, + 'filled': None, + 'remaining': None, + 'fee': None, + 'fees': [], + 'trades': None, + 'info': {}, + }, ] order1 = exchange._order_contracts_to_amount(orders[0]) order2 = exchange._order_contracts_to_amount(orders[1]) + exchange._order_contracts_to_amount(orders[2]) assert order1['amount'] == 30.0 * contract_size assert order2['amount'] == 40.0 * contract_size From 973644de6675b5ae222b6636405408a91c675089 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 19:25:43 +0100 Subject: [PATCH 23/26] Fix bad import --- tests/test_persistence.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b83b75cc2..80364a3e7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -15,8 +15,7 @@ from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids -from tests.conftest import (create_mock_trades, - create_mock_trades_with_leverage, log_has, log_has_re) +from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES From 6f1b14c01393e387e3f6ad8b903313cd56c0c66d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Mar 2022 19:46:56 +0100 Subject: [PATCH 24/26] Update buy_timeout and sell_timeout methods --- docs/bot-basics.md | 6 +- docs/strategy-callbacks.md | 16 ++-- docs/strategy_migration.md | 28 +++++++ freqtrade/freqtradebot.py | 3 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/resolvers/strategy_resolver.py | 81 +++++++++++-------- freqtrade/strategy/interface.py | 28 +++++-- .../subtemplates/strategy_methods_advanced.j2 | 16 ++-- .../broken_futures_strategies.py | 18 +++++ tests/strategy/test_strategy_loading.py | 11 ++- tests/test_freqtradebot.py | 72 +++++++---------- 11 files changed, 175 insertions(+), 106 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index f8d85a711..a44b611f3 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -32,8 +32,8 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and * Call `populate_entry_trend()` * Call `populate_exit_trend()` * Check timeouts for open orders. - * Calls `check_buy_timeout()` strategy callback for open entry orders. - * Calls `check_sell_timeout()` strategy callback for open exit orders. + * Calls `check_entry_timeout()` strategy callback for open entry orders. + * Calls `check_exit_timeout()` strategy callback for open exit orders. * Verifies existing positions and eventually places exit orders. * Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`. * Determine exit-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback. @@ -64,7 +64,7 @@ This loop will be repeated again and again until the bot is stopped. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Call `custom_stoploss()` and `custom_exit()` to find custom exit points. * For exits based on exit-signal and custom-exit: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). - * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks. + * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks. * Generate backtest report output !!! Note diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 6474ffcaa..31d52e30c 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -12,7 +12,7 @@ Currently available callbacks: * [`custom_exit()`](#custom-exit-signal) * [`custom_stoploss()`](#custom-stoploss) * [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules) -* [`check_buy_timeout()` and `check_sell_timeout()`](#custom-order-timeout-rules) +* [`check_entry_timeout()` and `check_exit_timeout()`](#custom-order-timeout-rules) * [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation) * [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation) * [`adjust_trade_position()`](#adjust-trade-position) @@ -408,7 +408,7 @@ However, freqtrade also offers a custom callback for both order types, which all ### Custom order timeout example Called for every open order until that order is either filled or cancelled. -`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders. +`check_entry_timeout()` is called for trade entries, while `check_exit_timeout()` is called for trade exit orders. A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. @@ -429,8 +429,8 @@ class AwesomeStrategy(IStrategy): 'sell': 60 * 25 } - def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, - current_time: datetime, **kwargs) -> bool: + def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): return True elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3): @@ -440,7 +440,7 @@ class AwesomeStrategy(IStrategy): return False - def check_sell_timeout(self, pair: str, trade: Trade, order: dict, + def check_exit_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): return True @@ -470,8 +470,8 @@ class AwesomeStrategy(IStrategy): 'sell': 60 * 25 } - def check_buy_timeout(self, pair: str, trade: Trade, order: dict, - current_time: datetime, **kwargs) -> bool: + def check_entry_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: ob = self.dp.orderbook(pair, 1) current_price = ob['bids'][0][0] # Cancel buy order if price is more than 2% above the order. @@ -480,7 +480,7 @@ class AwesomeStrategy(IStrategy): return False - def check_sell_timeout(self, pair: str, trade: Trade, order: dict, + def check_exit_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: ob = self.dp.orderbook(pair, 1) current_price = ob['asks'][0][0] diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 3a66ae9b3..19bd20880 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -11,6 +11,8 @@ If you intend on using markets other than spot markets, please migrate your stra * `populate_buy_trend()` -> `populate_entry_trend()` * `populate_sell_trend()` -> `populate_exit_trend()` * `custom_sell()` -> `custom_exit()` + * `check_buy_timeout()` -> `check_entry_timeout()` + * `check_sell_timeout()` -> `check_exit_timeout()` * Dataframe columns: * `buy` -> `enter_long` * `sell` -> `exit_long` @@ -124,6 +126,32 @@ class AwesomeStrategy(IStrategy): # ... ``` +### `custom_entry_timeout` + +`check_buy_timeout()` has been renamed to `check_entry_timeout()`, and `check_sell_timeout()` has been renamed to `check_exit_timeout()`. + +``` python hl_lines="2 6" +class AwesomeStrategy(IStrategy): + def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + return False + + def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + return False +``` + +``` python hl_lines="2 6" +class AwesomeStrategy(IStrategy): + def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + return False + + def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + return False +``` + ### Custom-stake-amount New string argument `side` - which can be either `"long"` or `"short"`. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2cd5e5ad..089a5804a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1138,13 +1138,12 @@ class FreqtradeBot(LoggingMixin): fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) is_entering = order['side'] == trade.enter_side not_closed = order['status'] == 'open' or fully_cancelled - time_method = 'sell' if order['side'] == 'sell' else 'buy' max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) order_obj = trade.select_order_by_order_id(trade.open_order_id) if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( - time_method, trade, order_obj, datetime.now(timezone.utc))) + trade, order_obj, datetime.now(timezone.utc))) ): if is_entering: self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b50932cab..808e94bad 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -853,7 +853,7 @@ class Backtesting: """ for order in [o for o in trade.orders if o.ft_is_open]: - timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time) + timedout = self.strategy.ft_check_timed_out(trade, order, current_time) if timedout: if order.side == trade.enter_side: self.timedout_entry_orders += 1 diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index c630fa967..87a9cc4b3 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -169,6 +169,51 @@ class StrategyResolver(IResolver): " in your strategy. Please note that short signals will be ignored in that case." ) + @staticmethod + def validate_strategy(strategy: IStrategy) -> IStrategy: + if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + # Require new method + if not check_override(strategy, IStrategy, 'populate_entry_trend'): + raise OperationalException("`populate_entry_trend` must be implemented.") + if not check_override(strategy, IStrategy, 'populate_exit_trend'): + raise OperationalException("`populate_exit_trend` must be implemented.") + if check_override(strategy, IStrategy, 'check_buy_timeout'): + raise OperationalException("Please migrate your implementation " + "of `check_buy_timeout` to `check_entry_timeout`.") + if check_override(strategy, IStrategy, 'check_sell_timeout'): + raise OperationalException("Please migrate your implementation " + "of `check_sell_timeout` to `check_exit_timeout`.") + + if check_override(strategy, IStrategy, 'custom_sell'): + raise OperationalException( + "Please migrate your implementation of `custom_sell` to `custom_exit`.") + else: + # TODO: Implementing one of the following methods should show a deprecation warning + # buy_trend and sell_trend, custom_sell + if ( + not check_override(strategy, IStrategy, 'populate_buy_trend') + and not check_override(strategy, IStrategy, 'populate_entry_trend') + ): + raise OperationalException( + "`populate_entry_trend` or `populate_buy_trend` must be implemented.") + if ( + not check_override(strategy, IStrategy, 'populate_sell_trend') + and not check_override(strategy, IStrategy, 'populate_exit_trend') + ): + raise OperationalException( + "`populate_exit_trend` or `populate_sell_trend` must be implemented.") + + strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) + strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) + strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) + if any(x == 2 for x in [ + strategy._populate_fun_len, + strategy._buy_fun_len, + strategy._sell_fun_len + ]): + strategy.INTERFACE_VERSION = 1 + return strategy + @staticmethod def _load_strategy(strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: @@ -208,42 +253,8 @@ class StrategyResolver(IResolver): ) if strategy: - if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - # Require new method - if not check_override(strategy, IStrategy, 'populate_entry_trend'): - raise OperationalException("`populate_entry_trend` must be implemented.") - if not check_override(strategy, IStrategy, 'populate_exit_trend'): - raise OperationalException("`populate_exit_trend` must be implemented.") - if check_override(strategy, IStrategy, 'custom_sell'): - raise OperationalException( - "Please migrate your implementation of `custom_sell` to `custom_exit`.") - else: - # TODO: Implementing one of the following methods should show a deprecation warning - # buy_trend and sell_trend, custom_sell - if ( - not check_override(strategy, IStrategy, 'populate_buy_trend') - and not check_override(strategy, IStrategy, 'populate_entry_trend') - ): - raise OperationalException( - "`populate_entry_trend` or `populate_buy_trend` must be implemented.") - if ( - not check_override(strategy, IStrategy, 'populate_sell_trend') - and not check_override(strategy, IStrategy, 'populate_exit_trend') - ): - raise OperationalException( - "`populate_exit_trend` or `populate_sell_trend` must be implemented.") - strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) - strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) - strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) - if any(x == 2 for x in [ - strategy._populate_fun_len, - strategy._buy_fun_len, - strategy._sell_fun_len - ]): - strategy.INTERFACE_VERSION = 1 - - return strategy + return StrategyResolver.validate_strategy(strategy) raise OperationalException( f"Impossible to load Strategy '{strategy_name}'. This class does not exist " diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index da7f02912..a61483e1d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -209,7 +209,14 @@ class IStrategy(ABC, HyperStrategyMixin): def check_buy_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: """ - Check buy timeout function callback. + DEPRECATED: Please use `check_entry_timeout` instead. + """ + return False + + def check_entry_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: + """ + Check entry timeout function callback. This method can be used to override the enter-timeout. It is called whenever a limit entry order has been created, and is not yet fully filled. @@ -224,11 +231,19 @@ class IStrategy(ABC, HyperStrategyMixin): :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the entry order is cancelled. """ - return False + return self.check_buy_timeout( + pair=pair, trade=trade, order=order, current_time=current_time) def check_sell_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: """ + DEPRECATED: Please use `check_exit_timeout` instead. + """ + return False + + def check_exit_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: + """ Check sell timeout function callback. This method can be used to override the exit-timeout. It is called whenever a (long) limit sell order or (short) limit buy @@ -244,7 +259,8 @@ class IStrategy(ABC, HyperStrategyMixin): :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled. """ - return False + return self.check_exit_timeout( + pair=pair, trade=trade, order=order, current_time=current_time) def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], @@ -1023,12 +1039,13 @@ class IStrategy(ABC, HyperStrategyMixin): else: return current_profit > roi - def ft_check_timed_out(self, side: str, trade: LocalTrade, order: Order, + def ft_check_timed_out(self, trade: LocalTrade, order: Order, current_time: datetime) -> bool: """ FT Internal method. Check if timeout is active, and if the order is still open and timed out """ + side = 'buy' if order.side == 'buy' else 'sell' timeout = self.config.get('unfilledtimeout', {}).get(side) if timeout is not None: timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') @@ -1038,7 +1055,8 @@ class IStrategy(ABC, HyperStrategyMixin): and order.order_date_utc < timeout_threshold) if timedout: return True - time_method = self.check_sell_timeout if order.side == 'sell' else self.check_buy_timeout + time_method = (self.check_exit_timeout if order.side == trade.exit_side + else self.check_entry_timeout) return strategy_safe_wrapper(time_method, default_retval=False)( diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 0ceeca982..d3178740b 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -170,11 +170,11 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: """ return True -def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: +def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: """ - Check buy timeout function callback. - This method can be used to override the buy-timeout. - It is called whenever a limit buy order has been created, + Check entry timeout function callback. + This method can be used to override the entry-timeout. + It is called whenever a limit entry order has been created, and is not yet fully filled. Configuration options in `unfilledtimeout` will be verified before this, so ensure to set these timeouts high enough. @@ -190,11 +190,11 @@ def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> """ return False -def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: +def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: """ - Check sell timeout function callback. - This method can be used to override the sell-timeout. - It is called whenever a limit sell order has been created, + Check exit timeout function callback. + This method can be used to override the exit-timeout. + It is called whenever a limit exit order has been created, and is not yet fully filled. Configuration options in `unfilledtimeout` will be verified before this, so ensure to set these timeouts high enough. diff --git a/tests/strategy/strats/broken_strats/broken_futures_strategies.py b/tests/strategy/strats/broken_strats/broken_futures_strategies.py index 4a84b7491..7e6955d37 100644 --- a/tests/strategy/strats/broken_strats/broken_futures_strategies.py +++ b/tests/strategy/strats/broken_strats/broken_futures_strategies.py @@ -29,3 +29,21 @@ class TestStrategyImplementCustomSell(TestStrategyNoImplementSell): current_rate: float, current_profit: float, **kwargs): return False + + +class TestStrategyImplementBuyTimeout(TestStrategyNoImplementSell): + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return super().populate_exit_trend(dataframe, metadata) + + def check_buy_timeout(self, pair: str, trade, order: dict, + current_time: datetime, **kwargs) -> bool: + return False + + +class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell): + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return super().populate_exit_trend(dataframe, metadata) + + def check_sell_timeout(self, pair: str, trade, order: dict, + current_time: datetime, **kwargs) -> bool: + return False diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 89803e15d..b1b67dcf0 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -418,11 +418,20 @@ def test_missing_implements(default_conf): StrategyResolver.load_strategy(default_conf) default_conf['strategy'] = 'TestStrategyImplementCustomSell' - with pytest.raises(OperationalException, match=r"Please migrate your implementation of `custom_sell`.*"): StrategyResolver.load_strategy(default_conf) + default_conf['strategy'] = 'TestStrategyImplementBuyTimeout' + with pytest.raises(OperationalException, + match=r"Please migrate your implementation of `check_buy_timeout`.*"): + StrategyResolver.load_strategy(default_conf) + + default_conf['strategy'] = 'TestStrategyImplementSellTimeout' + with pytest.raises(OperationalException, + match=r"Please migrate your implementation of `check_sell_timeout`.*"): + StrategyResolver.load_strategy(default_conf) + @pytest.mark.filterwarnings("ignore:deprecated") def test_call_deprecated_function(result, default_conf, caplog): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 304f525bd..9637a45e6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2370,7 +2370,7 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog): @pytest.mark.parametrize("is_short", [False, True]) -def test_check_handle_timedout_buy_usercustom( +def test_check_handle_timedout_entry_usercustom( default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, limit_sell_order_old, fee, mocker, is_short ) -> None: @@ -2406,34 +2406,23 @@ def test_check_handle_timedout_buy_usercustom( assert cancel_order_mock.call_count == 0 # Return false - trade remains open - if is_short: - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) - else: - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 1 - if is_short: - assert freqtrade.strategy.check_sell_timeout.call_count == 1 - # Raise Keyerror ... (no impact on trade) - freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError) - else: - assert freqtrade.strategy.check_buy_timeout.call_count == 1 - freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError) + assert freqtrade.strategy.check_entry_timeout.call_count == 1 + freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError) freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 1 - if is_short: - assert freqtrade.strategy.check_sell_timeout.call_count == 1 - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True) - else: - assert freqtrade.strategy.check_buy_timeout.call_count == 1 - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True) + assert freqtrade.strategy.check_entry_timeout.call_count == 1 + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=True) + # Trade should be closed since the function returns true freqtrade.check_handle_timedout() assert cancel_order_wr_mock.call_count == 1 @@ -2441,10 +2430,7 @@ def test_check_handle_timedout_buy_usercustom( trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 0 - if is_short: - assert freqtrade.strategy.check_sell_timeout.call_count == 1 - else: - assert freqtrade.strategy.check_buy_timeout.call_count == 1 + assert freqtrade.strategy.check_entry_timeout.call_count == 1 @pytest.mark.parametrize("is_short", [False, True]) @@ -2472,9 +2458,9 @@ def test_check_handle_timedout_buy( Trade.query.session.add(open_trade) if is_short: - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False) else: - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 @@ -2484,9 +2470,9 @@ def test_check_handle_timedout_buy( assert nb_trades == 0 # Custom user buy-timeout is never called if is_short: - assert freqtrade.strategy.check_sell_timeout.call_count == 0 + assert freqtrade.strategy.check_exit_timeout.call_count == 0 else: - assert freqtrade.strategy.check_buy_timeout.call_count == 0 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 @pytest.mark.parametrize("is_short", [False, True]) @@ -2553,7 +2539,7 @@ def test_check_handle_timedout_buy_exception( @pytest.mark.parametrize("is_short", [False, True]) -def test_check_handle_timedout_sell_usercustom( +def test_check_handle_timedout_exit_usercustom( default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, is_short, open_trade_usdt, caplog ) -> None: @@ -2585,35 +2571,35 @@ def test_check_handle_timedout_sell_usercustom( freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) # Return false - No impact freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 0 assert open_trade_usdt.is_open is False - assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) - assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) + assert freqtrade.strategy.check_exit_timeout.call_count == 1 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 - freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError) - freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError) + freqtrade.strategy.check_exit_timeout = MagicMock(side_effect=KeyError) + freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError) # Return Error - No impact freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 0 assert open_trade_usdt.is_open is False - assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) - assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) + assert freqtrade.strategy.check_exit_timeout.call_count == 1 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 # Return True - sells! - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True) - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True) + freqtrade.strategy.check_exit_timeout = MagicMock(return_value=True) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=True) freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 assert open_trade_usdt.is_open is True - assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) - assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) + assert freqtrade.strategy.check_exit_timeout.call_count == 1 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 # 2nd canceled trade - Fail execute sell caplog.clear() @@ -2665,16 +2651,16 @@ def test_check_handle_timedout_sell( Trade.query.session.add(open_trade_usdt) - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 assert open_trade_usdt.is_open is True # Custom user sell-timeout is never called - assert freqtrade.strategy.check_sell_timeout.call_count == 0 - assert freqtrade.strategy.check_buy_timeout.call_count == 0 + assert freqtrade.strategy.check_exit_timeout.call_count == 0 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 @pytest.mark.parametrize("is_short", [False, True]) From 06248172426ff4a626ef75dd3ff5ce4f9a2ca41c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Mar 2022 11:55:11 +0100 Subject: [PATCH 25/26] update unfilledtimeout settings to entry/exit --- config_examples/config_binance.example.json | 4 +-- config_examples/config_bittrex.example.json | 4 +-- config_examples/config_ftx.example.json | 4 +-- config_examples/config_full.example.json | 4 +-- config_examples/config_kraken.example.json | 4 +-- docs/configuration.md | 4 +-- docs/strategy-callbacks.md | 8 +++--- docs/strategy_migration.md | 25 ++++++++++++++++++ freqtrade/configuration/config_validation.py | 21 +++++++++++++++ freqtrade/constants.py | 4 +-- freqtrade/rpc/api_server/api_schemas.py | 4 +-- freqtrade/strategy/interface.py | 8 +++--- freqtrade/templates/base_config.json.j2 | 4 +-- tests/conftest.py | 4 +-- tests/test_configuration.py | 27 +++++++++++++++++++- tests/test_freqtradebot.py | 6 ++--- 16 files changed, 103 insertions(+), 32 deletions(-) diff --git a/config_examples/config_binance.example.json b/config_examples/config_binance.example.json index c6faf506c..ae84af420 100644 --- a/config_examples/config_binance.example.json +++ b/config_examples/config_binance.example.json @@ -8,8 +8,8 @@ "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/config_examples/config_bittrex.example.json b/config_examples/config_bittrex.example.json index 9fe99c835..1f55d43ed 100644 --- a/config_examples/config_bittrex.example.json +++ b/config_examples/config_bittrex.example.json @@ -8,8 +8,8 @@ "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/config_examples/config_ftx.example.json b/config_examples/config_ftx.example.json index 4f7c2af54..fbdff3333 100644 --- a/config_examples/config_ftx.example.json +++ b/config_examples/config_ftx.example.json @@ -8,8 +8,8 @@ "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index bbdafa805..9e4c342ed 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -30,8 +30,8 @@ }, "stoploss": -0.10, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/config_examples/config_kraken.example.json b/config_examples/config_kraken.example.json index 5ac3a9255..27a4979d4 100644 --- a/config_examples/config_kraken.example.json +++ b/config_examples/config_kraken.example.json @@ -8,8 +8,8 @@ "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/docs/configuration.md b/docs/configuration.md index 147f0b672..3f3086833 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -102,8 +102,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md).
*Defaults to `"spot"`.*
**Datatype:** String | `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md).
**Datatype:** String | `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md).
*Defaults to `0.05`.*
**Datatype:** Float -| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 31d52e30c..6baf38c41 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -425,8 +425,8 @@ class AwesomeStrategy(IStrategy): # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. unfilledtimeout = { - 'buy': 60 * 25, - 'sell': 60 * 25 + 'entry': 60 * 25, + 'exit': 60 * 25 } def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, @@ -466,8 +466,8 @@ class AwesomeStrategy(IStrategy): # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. unfilledtimeout = { - 'buy': 60 * 25, - 'sell': 60 * 25 + 'entry': 60 * 25, + 'exit': 60 * 25 } def check_entry_timeout(self, pair: str, trade: Trade, order: dict, diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 19bd20880..ee6f1a494 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -32,6 +32,7 @@ If you intend on using markets other than spot markets, please migrate your stra * Strategy/Configuration settings. * `order_time_in_force` buy -> entry, sell -> exit. * `order_types` buy -> entry, sell -> exit. + * `unfilledtimeout` buy -> entry, sell -> exit. ## Extensive explanation @@ -287,6 +288,7 @@ This should be given the value of `trade.is_short`. "stoploss": "market", "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 + } ``` ``` python hl_lines="2-6" @@ -299,4 +301,27 @@ This should be given the value of `trade.is_short`. "stoploss": "market", "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 + } +``` + +#### `unfilledtimeout` + +`unfilledtimeout` have changed all wordings from `buy` to `entry` - and `sell` to `exit`. + +``` python hl_lines="2-3" +unfilledtimeout = { + "buy": 10, + "sell": 10, + "exit_timeout_count": 0, + "unit": "minutes" + } +``` + +``` python hl_lines="2-3" +unfilledtimeout = { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + } ``` diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 267509b43..3ebb18cd6 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -216,6 +216,7 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None: _validate_time_in_force(conf) _validate_order_types(conf) + _validate_unfilledtimeout(conf) def _validate_time_in_force(conf: Dict[str, Any]) -> None: @@ -258,3 +259,23 @@ def _validate_order_types(conf: Dict[str, Any]) -> None: ]: process_deprecated_setting(conf, 'order_types', o, 'order_types', n) + + +def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None: + unfilledtimeout = conf.get('unfilledtimeout', {}) + if any(x in unfilledtimeout for x in ['buy', 'sell']): + if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + raise OperationalException( + "Please migrate your unfilledtimeout settings to use the new wording.") + else: + + logger.warning( + "DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated." + "Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording." + ) + for o, n in [ + ('buy', 'entry'), + ('sell', 'exit'), + ]: + + process_deprecated_setting(conf, 'unfilledtimeout', o, 'unfilledtimeout', n) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index fabac5830..6441559ad 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -165,8 +165,8 @@ CONF_SCHEMA = { 'unfilledtimeout': { 'type': 'object', 'properties': { - 'buy': {'type': 'number', 'minimum': 1}, - 'sell': {'type': 'number', 'minimum': 1}, + 'entry': {'type': 'number', 'minimum': 1}, + 'exit': {'type': 'number', 'minimum': 1}, 'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0}, 'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'} } diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index b6d175c0f..07772647f 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -131,8 +131,8 @@ class Daily(BaseModel): class UnfilledTimeout(BaseModel): - buy: Optional[int] - sell: Optional[int] + entry: Optional[int] + exit: Optional[int] unit: Optional[str] exit_timeout_count: Optional[int] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a61483e1d..c00eb238a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -259,7 +259,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled. """ - return self.check_exit_timeout( + return self.check_sell_timeout( pair=pair, trade=trade, order=order, current_time=current_time) def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, @@ -1045,14 +1045,14 @@ class IStrategy(ABC, HyperStrategyMixin): FT Internal method. Check if timeout is active, and if the order is still open and timed out """ - side = 'buy' if order.side == 'buy' else 'sell' + side = 'entry' if order.ft_order_side == trade.enter_side else 'exit' + timeout = self.config.get('unfilledtimeout', {}).get(side) if timeout is not None: timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') timeout_kwargs = {timeout_unit: -timeout} timeout_threshold = current_time + timedelta(**timeout_kwargs) - timedout = (order.status == 'open' and order.side == side - and order.order_date_utc < timeout_threshold) + timedout = (order.status == 'open' and order.order_date_utc < timeout_threshold) if timedout: return True time_method = (self.check_exit_timeout if order.side == trade.exit_side diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 60f4b4fd7..e5f8f5efe 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -16,8 +16,8 @@ "trading_mode": "{{ trading_mode }}", "margin_mode": "{{ margin_mode }}", "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/tests/conftest.py b/tests/conftest.py index 117aeaaed..898945370 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -416,8 +416,8 @@ def get_default_conf(testdatadir): "dry_run_wallet": 1000, "stoploss": -0.10, "unfilledtimeout": { - "buy": 10, - "sell": 30 + "entry": 10, + "exit": 30 }, "bid_strategy": { "ask_last_balance": 0.0, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index e4a30c958..44187104a 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -963,7 +963,7 @@ def test_validate_time_in_force(default_conf, caplog) -> None: validate_config_consistency(conf) -def test_validate_order_types(default_conf, caplog) -> None: +def test__validate_order_types(default_conf, caplog) -> None: conf = deepcopy(default_conf) conf['order_types'] = { 'buy': 'limit', @@ -998,6 +998,31 @@ def test_validate_order_types(default_conf, caplog) -> None: validate_config_consistency(conf) +def test__validate_unfilledtimeout(default_conf, caplog) -> None: + conf = deepcopy(default_conf) + conf['unfilledtimeout'] = { + 'buy': 30, + 'sell': 35, + } + validate_config_consistency(conf) + assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is.*", caplog) + assert conf['unfilledtimeout']['entry'] == 30 + assert conf['unfilledtimeout']['exit'] == 35 + assert 'buy' not in conf['unfilledtimeout'] + assert 'sell' not in conf['unfilledtimeout'] + + conf = deepcopy(default_conf) + conf['unfilledtimeout'] = { + 'buy': 30, + 'sell': 35, + } + conf['trading_mode'] = 'futures' + with pytest.raises( + OperationalException, + match=r"Please migrate your unfilledtimeout settings to use the new wording\."): + validate_config_consistency(conf) + + def test_load_config_test_comments() -> None: """ Load config with comments diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9637a45e6..37a43eab4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2378,8 +2378,8 @@ def test_check_handle_timedout_entry_usercustom( old_order = limit_sell_order_old if is_short else limit_buy_order_old old_order['id'] = open_trade.open_order_id - default_conf_usdt["unfilledtimeout"] = {"buy": 30, - "sell": 1400} if is_short else {"buy": 1400, "sell": 30} + default_conf_usdt["unfilledtimeout"] = {"entry": 30, + "exit": 1400} if is_short else {"entry": 1400, "exit": 30} rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=old_order) @@ -2543,7 +2543,7 @@ def test_check_handle_timedout_exit_usercustom( default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, is_short, open_trade_usdt, caplog ) -> None: - default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1} + default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1} limit_sell_order_old['id'] = open_trade_usdt.open_order_id if is_short: limit_sell_order_old['side'] = 'buy' From 4424dcc2df3e97b14997e7d2dc22bcb951e49174 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Mar 2022 12:01:28 +0100 Subject: [PATCH 26/26] Fix odd test --- tests/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 37a43eab4..624399884 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2378,8 +2378,7 @@ def test_check_handle_timedout_entry_usercustom( old_order = limit_sell_order_old if is_short else limit_buy_order_old old_order['id'] = open_trade.open_order_id - default_conf_usdt["unfilledtimeout"] = {"entry": 30, - "exit": 1400} if is_short else {"entry": 1400, "exit": 30} + default_conf_usdt["unfilledtimeout"] = {"entry": 1400, "exit": 30} rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=old_order) @@ -2399,6 +2398,7 @@ def test_check_handle_timedout_entry_usercustom( freqtrade = FreqtradeBot(default_conf_usdt) open_trade.is_short = is_short open_trade.orders[0].side = 'sell' if is_short else 'buy' + open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' Trade.query.session.add(open_trade) # Ensure default is to return empty (so not mocked yet)