From d09193127917130ebc846d0d18fe7f8de0a53ac7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 10:54:31 +0200 Subject: [PATCH 01/21] Ease meaning of "refresh" param for adjust_stoploss --- freqtrade/persistence/trade_model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 7b17bef8d..3fd93ef82 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -240,7 +240,8 @@ class Order(ModelBase): if (self.ft_order_side == trade.entry_side and self.price): trade.open_rate = self.price trade.recalc_trade_from_orders() - trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True) + trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, + refresh=trade.nr_of_successful_entries == 1) @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): @@ -628,11 +629,12 @@ class LocalTrade: :param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price). :param initial: Called to initiate stop_loss. Skips everything if self.stop_loss is already set. + :param refresh: Called to refresh stop_loss, allows adjustment in both directions """ if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)): # Don't modify if called with initial and nothing to do return - refresh = True if refresh and self.nr_of_successful_entries == 1 else False + refresh = True if refresh else False leverage = self.leverage or 1.0 if self.is_short: From e2274e813a2c8ca49bb41624c0b922e9b5a2ee34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 11:01:00 +0200 Subject: [PATCH 02/21] Rename adjust_stoploss parameter to allow_refresh --- freqtrade/persistence/trade_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 3fd93ef82..3e4fcc939 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -241,7 +241,7 @@ class Order(ModelBase): trade.open_rate = self.price trade.recalc_trade_from_orders() trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, - refresh=trade.nr_of_successful_entries == 1) + allow_refresh=trade.nr_of_successful_entries == 1) @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): @@ -622,7 +622,7 @@ class LocalTrade: self.stop_loss_pct = -1 * abs(percent) def adjust_stop_loss(self, current_price: float, stoploss: Optional[float], - initial: bool = False, refresh: bool = False) -> None: + initial: bool = False, allow_refresh: bool = False) -> None: """ This adjusts the stop loss to it's most recently observed setting :param current_price: Current rate the asset is traded @@ -634,7 +634,7 @@ class LocalTrade: if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)): # Don't modify if called with initial and nothing to do return - refresh = True if refresh else False + allow_refresh = True if allow_refresh else False leverage = self.leverage or 1.0 if self.is_short: @@ -645,7 +645,7 @@ class LocalTrade: stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode, rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) # no stop loss assigned yet - if self.initial_stop_loss_pct is None or refresh: + if self.initial_stop_loss_pct is None or allow_refresh: self.__set_stop_loss(stop_loss_norm, stoploss) self.initial_stop_loss = price_to_precision( stop_loss_norm, self.price_precision, self.precision_mode, From 147cc4f0b6691fe2d10e55b1491f606c4e5ca834 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 11:09:26 +0200 Subject: [PATCH 03/21] Initial version of stop "after_fill" --- freqtrade/freqtradebot.py | 7 +++++++ freqtrade/strategy/interface.py | 12 +++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index beca1f09c..f5a6c38b2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1894,6 +1894,13 @@ class FreqtradeBot(LoggingMixin): )) except DependencyException: logger.warning('Unable to calculate liquidation price') + if self.strategy.use_custom_stoploss: + current_rate = self.exchange.get_rate( + trade.pair, side='exit', is_short=trade.is_short, refresh=True) + profit = trade.calc_profit_ratio(current_rate) + self.strategy.ft_stoploss_adjust(current_rate, trade, + datetime.now(timezone.utc), profit, 0, + after_fill=True) # Updating wallets when order is closed self.wallets.update() Trade.commit() diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index eca3e3ede..feb561743 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -373,7 +373,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + current_profit: float, after_fill: bool, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -389,6 +389,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param after_fill: True if the stoploss is called after the order was filled. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the current_rate """ @@ -1160,7 +1161,7 @@ class IStrategy(ABC, HyperStrategyMixin): def ft_stoploss_adjust(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, low: Optional[float] = None, - high: Optional[float] = None) -> None: + high: Optional[float] = None, after_fill: bool = False) -> None: """ Adjust stop-loss dynamically if configured to do so. :param current_profit: current profit as ratio @@ -1186,11 +1187,12 @@ class IStrategy(ABC, HyperStrategyMixin): )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=(bound or current_rate), - current_profit=bound_profit) + current_profit=bound_profit, + after_fill=after_fill) # Sanity check - error cases will return None if stop_loss_value: - # logger.info(f"{trade.pair} {stop_loss_value=} {bound_profit=}") - trade.adjust_stop_loss(bound or current_rate, stop_loss_value) + trade.adjust_stop_loss(bound or current_rate, stop_loss_value, + allow_refresh=after_fill) else: logger.warning("CustomStoploss function did not return valid stoploss") From ae9f73062446b6671896fabbcb534f380676386d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 11:31:17 +0200 Subject: [PATCH 04/21] Add explicit "is_trailing_stop" field to database --- freqtrade/persistence/migrations.py | 9 ++++++--- freqtrade/persistence/trade_model.py | 6 +++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 87b172846..32972e7ff 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -88,6 +88,8 @@ def migrate_trades_and_orders_table( stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') + is_stop_loss_trailing = get_column_def(cols, 'is_stop_loss_trailing', + 'stop_loss_pct <> initial_stop_loss_pct') stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') @@ -156,7 +158,7 @@ def migrate_trades_and_orders_table( open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, - stoploss_order_id, stoploss_last_update, + is_stop_loss_trailing, stoploss_order_id, stoploss_last_update, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, timeframe, open_trade_value, close_profit_abs, trading_mode, leverage, liquidation_price, is_short, @@ -175,6 +177,7 @@ def migrate_trades_and_orders_table( {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, + {is_stop_loss_trailing} is_stop_loss_trailing, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, case when {exit_reason} = 'sell_signal' then 'exit_signal' @@ -316,8 +319,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # if ('orders' not in previous_tables # or not has_column(cols_orders, 'funding_fee')): migrating = False - # if not has_column(cols_trades, 'max_stake_amount'): - if not has_column(cols_orders, 'ft_price'): + # if not has_column(cols_orders, 'ft_price'): + if not has_column(cols_trades, 'is_stop_loss_trailing'): migrating = True logger.info(f"Running database migration for trades - " f"backup: {table_back_name}, {order_table_bak_name}") diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 3e4fcc939..0e72c0519 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -350,6 +350,7 @@ class LocalTrade: initial_stop_loss: Optional[float] = 0.0 # percentage value of the initial stop loss initial_stop_loss_pct: Optional[float] = None + is_stop_loss_trailing: bool = False # stoploss order id which is on exchange stoploss_order_id: Optional[str] = None # last update time of the stoploss order on exchange @@ -662,6 +663,7 @@ class LocalTrade: # ? decreasing the minimum stoploss if (higher_stop and not self.is_short) or (lower_stop and self.is_short): logger.debug(f"{self.pair} - Adjusting stoploss...") + self.is_stop_loss_trailing = True self.__set_stop_loss(stop_loss_norm, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") @@ -1195,7 +1197,7 @@ class LocalTrade: logger.info(f"Found open trade: {trade}") # skip case if trailing-stop changed the stoploss already. - if (trade.stop_loss == trade.initial_stop_loss + if (not trade.is_stop_loss_trailing and trade.initial_stop_loss_pct != desired_stoploss): # Stoploss value got changed @@ -1268,6 +1270,8 @@ class Trade(ModelBase, LocalTrade): # percentage value of the initial stop loss initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column( Float(), nullable=True) # type: ignore + is_stop_loss_trailing: Mapped[bool] = mapped_column( + nullable=False, default=False) # type: ignore # stoploss order id which is on exchange stoploss_order_id: Mapped[Optional[str]] = mapped_column( String(255), nullable=True, index=True) # type: ignore From 6b9547a9ad354da5495d4279a96e76b31f1aac01 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 14:56:05 +0200 Subject: [PATCH 05/21] Improve migrations --- freqtrade/persistence/migrations.py | 5 +++-- tests/strategy/test_default_strategy.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 32972e7ff..1270b85ac 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -88,8 +88,9 @@ def migrate_trades_and_orders_table( stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') - is_stop_loss_trailing = get_column_def(cols, 'is_stop_loss_trailing', - 'stop_loss_pct <> initial_stop_loss_pct') + is_stop_loss_trailing = get_column_def( + cols, 'is_stop_loss_trailing', + f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)') stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index b5b07e0cd..afe7fc97a 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -52,4 +52,5 @@ def test_strategy_test_v3(dataframe_1m, fee, is_short, side): side=side) is True assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), - current_rate=20_000, current_profit=0.05) == strategy.stoploss + current_rate=20_000, current_profit=0.05, after_fill=False + ) == strategy.stoploss From ec8ba821ed16c91a18d78f4175a45f79cf4e1e23 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 15:43:19 +0200 Subject: [PATCH 06/21] Simplify stop adjustment code --- freqtrade/persistence/trade_model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 0e72c0519..b3bd48bda 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -240,8 +240,9 @@ class Order(ModelBase): if (self.ft_order_side == trade.entry_side and self.price): trade.open_rate = self.price trade.recalc_trade_from_orders() - trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, - allow_refresh=trade.nr_of_successful_entries == 1) + if trade.nr_of_successful_entries == 1: + trade.initial_stop_loss_pct = None + trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct) @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): @@ -646,7 +647,7 @@ class LocalTrade: stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode, rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) # no stop loss assigned yet - if self.initial_stop_loss_pct is None or allow_refresh: + if self.initial_stop_loss_pct is None: self.__set_stop_loss(stop_loss_norm, stoploss) self.initial_stop_loss = price_to_precision( stop_loss_norm, self.price_precision, self.precision_mode, From 4da8c911612aefa27d074b1d75a016d7d98326e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 15:55:46 +0200 Subject: [PATCH 07/21] Improve stop adjustment tests --- tests/persistence/test_persistence.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 958db8c72..a80b54244 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -77,18 +77,21 @@ def test_set_stop_loss_liquidation(fee): assert trade.liquidation_price == 0.11 # Stoploss does not change from liquidation price assert trade.stop_loss == 1.8 + assert trade.stop_loss_pct == -0.2 assert trade.initial_stop_loss == 1.8 # lower stop doesn't move stoploss trade.adjust_stop_loss(1.8, 0.2) assert trade.liquidation_price == 0.11 assert trade.stop_loss == 1.8 + assert trade.stop_loss_pct == -0.2 assert trade.initial_stop_loss == 1.8 # higher stop does move stoploss trade.adjust_stop_loss(2.1, 0.1) assert trade.liquidation_price == 0.11 assert pytest.approx(trade.stop_loss) == 1.994999 + assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 1.8 assert trade.stoploss_or_liquidation == trade.stop_loss @@ -130,12 +133,14 @@ def test_set_stop_loss_liquidation(fee): assert trade.liquidation_price == 3.8 # Stoploss does not change from liquidation price assert trade.stop_loss == 2.2 + assert trade.stop_loss_pct == -0.2 assert trade.initial_stop_loss == 2.2 # Stop doesn't move stop higher trade.adjust_stop_loss(2.0, 0.3) assert trade.liquidation_price == 3.8 assert trade.stop_loss == 2.2 + assert trade.stop_loss_pct == -0.2 assert trade.initial_stop_loss == 2.2 # Stoploss does move lower @@ -143,6 +148,7 @@ def test_set_stop_loss_liquidation(fee): trade.adjust_stop_loss(1.8, 0.1) assert trade.liquidation_price == 1.5 assert pytest.approx(trade.stop_loss) == 1.89 + assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 2.2 assert trade.stoploss_or_liquidation == 1.5 From 6249392526d3343e8b9867dd56008e7477b636cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 15:56:43 +0200 Subject: [PATCH 08/21] Add test for "allow adjustment in other direction" --- tests/persistence/test_persistence.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index a80b54244..63c3ec321 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -87,6 +87,13 @@ def test_set_stop_loss_liquidation(fee): assert trade.stop_loss_pct == -0.2 assert trade.initial_stop_loss == 1.8 + # Lower stop with "allow_refresh" does move stoploss + trade.adjust_stop_loss(1.8, 0.22, allow_refresh=True) + assert trade.liquidation_price == 0.11 + assert trade.stop_loss == 1.602 + assert trade.stop_loss_pct == -0.22 + assert trade.initial_stop_loss == 1.8 + # higher stop does move stoploss trade.adjust_stop_loss(2.1, 0.1) assert trade.liquidation_price == 0.11 @@ -143,6 +150,13 @@ def test_set_stop_loss_liquidation(fee): assert trade.stop_loss_pct == -0.2 assert trade.initial_stop_loss == 2.2 + # Stop does move stop higher with "allow_refresh" + trade.adjust_stop_loss(2.0, 0.3, allow_refresh=True) + assert trade.liquidation_price == 3.8 + assert trade.stop_loss == 2.3 + assert trade.stop_loss_pct == -0.3 + assert trade.initial_stop_loss == 2.2 + # Stoploss does move lower trade.set_liquidation_price(1.5) trade.adjust_stop_loss(1.8, 0.1) From e1eeaa24d274e9dfb007eb9d0ab3883599ef802d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 15:57:21 +0200 Subject: [PATCH 09/21] Implement "adjust lower" correctly --- freqtrade/persistence/trade_model.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index b3bd48bda..607d1d2bd 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -636,7 +636,6 @@ class LocalTrade: if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)): # Don't modify if called with initial and nothing to do return - allow_refresh = True if allow_refresh else False leverage = self.leverage or 1.0 if self.is_short: @@ -662,7 +661,11 @@ class LocalTrade: # stop losses only walk up, never down!, # ? But adding more to a leveraged trade would create a lower liquidation price, # ? decreasing the minimum stoploss - if (higher_stop and not self.is_short) or (lower_stop and self.is_short): + if ( + allow_refresh + or (higher_stop and not self.is_short) + or (lower_stop and self.is_short) + ): logger.debug(f"{self.pair} - Adjusting stoploss...") self.is_stop_loss_trailing = True self.__set_stop_loss(stop_loss_norm, stoploss) From 62d83b8dbde696049a777c78e995e2e657cdb5c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 15:57:47 +0200 Subject: [PATCH 10/21] Use is_stop_trailing for actual trailing detection --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index feb561743..19092b0f4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1247,7 +1247,7 @@ class IStrategy(ABC, HyperStrategyMixin): exit_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: + if trade.is_stop_loss_trailing: exit_type = ExitType.TRAILING_STOP_LOSS logger.debug( f"{trade.pair} - HIT STOP: current price at " From fc60c0df1990ae726cf08453169b87e597ec2ee0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 16:00:33 +0200 Subject: [PATCH 11/21] Add call to stoploss-adjust for backtesting --- freqtrade/optimize/backtesting.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bdd04ba7f..6a3e1949c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -578,6 +578,11 @@ class Backtesting: """ Rate is within candle, therefore filled""" return row[LOW_IDX] <= rate <= row[HIGH_IDX] + def _call_adjust_stop(self, current_date: datetime, trade: Trade, current_rate: float): + profit = trade.calc_profit_ratio(current_rate) + self.strategy.ft_stoploss_adjust(current_rate, trade, current_date, profit, 0, + after_fill=True) + def _try_close_open_order( self, order: Optional[Order], trade: LocalTrade, current_date: datetime, row: Tuple) -> bool: @@ -588,6 +593,9 @@ class Backtesting: if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_date, trade) trade.open_order_id = None + if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): + self._call_adjust_stop(current_date, trade, order.ft_price) + # pass return True return False From bef5e191a480e64e436f43cc310a46ecd0c550a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 16:12:04 +0200 Subject: [PATCH 12/21] Don't surprise people with "after_fill" calls --- freqtrade/resolvers/strategy_resolver.py | 5 +++++ freqtrade/strategy/interface.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 6f5b6655d..50605af72 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -218,6 +218,11 @@ class StrategyResolver(IResolver): "Please update your strategy to implement " "`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` " "with the metadata argument. ") + + after_fill = 'after_fill' in getfullargspec(strategy.custom_stoploss).args + if after_fill: + strategy._ft_stop_uses_after_fill = True + return strategy @staticmethod diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 19092b0f4..cbd241a02 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -720,6 +720,8 @@ class IStrategy(ABC, HyperStrategyMixin): # END - Intended to be overridden by strategy ### + _ft_stop_uses_after_fill = False + def __informative_pairs_freqai(self) -> ListPairsWithTimeframes: """ Create informative-pairs needed for FreqAI @@ -1168,6 +1170,10 @@ class IStrategy(ABC, HyperStrategyMixin): :param low: Low value of this candle, only set in backtesting :param high: High value of this candle, only set in backtesting """ + if after_fill and not self._ft_stop_uses_after_fill: + # Skip if the strategy doesn't support after fill. + return + stop_loss_value = force_stoploss if force_stoploss else self.stoploss # Initiate stoploss with open_rate. Does nothing if stoploss is already set. From 6e32f172be15e7326a9699ee4eabd1f45923de70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 16:16:23 +0200 Subject: [PATCH 13/21] Update samples in the documentation --- docs/strategy-callbacks.md | 23 ++++++++++++------- docs/strategy-customization.md | 6 +++-- docs/strategy_migration.md | 2 +- .../strategy_methods_advanced.j2 | 8 +++---- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index ab8eb9f98..93be196d4 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -179,7 +179,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -187,7 +188,7 @@ class AwesomeStrategy(IStrategy): For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ - When not implemented by a strategy, returns the initial stoploss value + When not implemented by a strategy, returns the initial stoploss value. Only called when use_custom_stoploss is set to True. :param pair: Pair that's currently analyzed @@ -195,8 +196,9 @@ class AwesomeStrategy(IStrategy): :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param after_fill: True if the stoploss is called after the order was filled. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New stoploss value, relative to the current rate + :return float: New stoploss value, relative to the current_rate """ return -0.04 ``` @@ -229,7 +231,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date_utc: @@ -255,7 +258,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -281,7 +285,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float: if current_profit < 0.04: return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss @@ -314,7 +319,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float: # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: @@ -342,7 +348,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) last_candle = dataframe.iloc[-1].squeeze() diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 8913d787b..f81e07c7a 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -901,7 +901,8 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float: # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: @@ -943,7 +944,8 @@ In some situations it may be confusing to deal with stops relative to current ra return dataframe def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1].squeeze() return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short) diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index d00349d1d..87ccf7667 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -311,7 +311,7 @@ After: ``` python hl_lines="5 7" def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> float: # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index bfbb20ec1..d30588be3 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -102,8 +102,8 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f use_custom_stoploss = True -def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', - current_rate: float, current_profit: float, **kwargs) -> float: +def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, after_fill: bool, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -111,7 +111,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ - When not implemented by a strategy, returns the initial stoploss value + When not implemented by a strategy, returns the initial stoploss value. Only called when use_custom_stoploss is set to True. :param pair: Pair that's currently analyzed @@ -119,10 +119,10 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param after_fill: True if the stoploss is called after the order was filled. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the current_rate """ - return self.stoploss def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': From 3ed682a9c630dd1d2899f12258f20052a2469e58 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 16:20:54 +0200 Subject: [PATCH 14/21] Allow None from custom_stop --- docs/strategy-callbacks.md | 19 ++++++++++--------- docs/strategy-customization.md | 4 ++-- docs/strategy_migration.md | 3 ++- freqtrade/strategy/interface.py | 4 ++-- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 93be196d4..a64c392fa 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -164,6 +164,7 @@ E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoplo During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades). The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. +Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss. To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: @@ -180,7 +181,7 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, - **kwargs) -> float: + **kwargs) -> Optional[float]: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -232,14 +233,14 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, - **kwargs) -> float: + **kwargs) -> Optional[float]: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date_utc: return -0.05 elif current_time - timedelta(minutes=60) > trade.open_date_utc: return -0.10 - return 1 + return None ``` #### Different stoploss per pair @@ -259,7 +260,7 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, - **kwargs) -> float: + **kwargs) -> Optional[float]: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -286,7 +287,7 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, - **kwargs) -> float: + **kwargs) -> Optional[float]: if current_profit < 0.04: return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss @@ -320,7 +321,7 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, - **kwargs) -> float: + **kwargs) -> Optional[float]: # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: @@ -331,7 +332,7 @@ class AwesomeStrategy(IStrategy): return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) # return maximum stoploss value, keeping current stoploss price unchanged - return 1 + return None ``` #### Custom stoploss using an indicator from dataframe example @@ -349,7 +350,7 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, - **kwargs) -> float: + **kwargs) -> Optional[float]: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) last_candle = dataframe.iloc[-1].squeeze() @@ -362,7 +363,7 @@ class AwesomeStrategy(IStrategy): return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short) # return maximum stoploss value, keeping current stoploss price unchanged - return 1 + return None ``` See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index f81e07c7a..c77b59788 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -902,7 +902,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, - **kwargs) -> float: + **kwargs) -> Optional[float]: # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: @@ -945,7 +945,7 @@ In some situations it may be confusing to deal with stops relative to current ra def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, - **kwargs) -> float: + **kwargs) -> Optional[float]: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1].squeeze() return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short) diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 87ccf7667..0af901bb3 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -311,7 +311,8 @@ After: ``` python hl_lines="5 7" def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> float: + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> Optional[float]: # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index cbd241a02..5010f3db7 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -373,7 +373,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, after_fill: bool, **kwargs) -> float: + current_profit: float, after_fill: bool, **kwargs) -> Optional[float]: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -1200,7 +1200,7 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_stop_loss(bound or current_rate, stop_loss_value, allow_refresh=after_fill) else: - logger.warning("CustomStoploss function did not return valid stoploss") + logger.debug("CustomStoploss function did not return valid stoploss") if self.trailing_stop and dir_correct: # trailing stoploss handling From ddf79088fb1994954d1ca482df5c615a22262e47 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 16:31:01 +0200 Subject: [PATCH 15/21] Update custom stop documenttaion --- docs/strategy-callbacks.md | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index a64c392fa..9f75bd656 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -166,6 +166,30 @@ During backtesting, `current_rate` (and `current_profit`) are provided against t The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss. +Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). + +!!! Note "Use of dates" + All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. + +!!! Tip "Trailing stoploss" + It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. + +### Adjust stoploss after position adjustments + +Depending on your strategy, you may encounter the need to adjust the stoploss in both directions after a [position adjustment](#adjust-trade-position). +For this, freqtrade will make an additional call with `after_fill=True` after an order fills, which will allow the strategy to move the stoploss in any direction (also widening the gap between stoploss and current price, which is otherwise forbidden). + +!!! Note "backwards compatibility" + This call will only be made if the `after_fill` parameter is part of the function definition of your `custom_stoploss` function. + As such, this will not impact (and with that, surprise) existing, running strategies. + +### Custom stoploss examples + +The next section will show some examples on what's possible with the custom stoploss function. +Of course, many more things are possible, and all examples can be combined at will. + +#### Trailing stop via custom stoploss + To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: ``` python @@ -204,19 +228,6 @@ class AwesomeStrategy(IStrategy): return -0.04 ``` -Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). - -!!! Note "Use of dates" - All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. - -!!! Tip "Trailing stoploss" - It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. - -### Custom stoploss examples - -The next section will show some examples on what's possible with the custom stoploss function. -Of course, many more things are possible, and all examples can be combined at will. - #### Time based trailing stop Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. From 070a1990e8e85dc1e43d962dd21d4d668c7aaf2c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 16:46:25 +0200 Subject: [PATCH 16/21] Improve handling of None values from custom_stoploss --- freqtrade/optimize/backtesting.py | 6 +++--- freqtrade/strategy/interface.py | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6a3e1949c..21075a1df 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -578,10 +578,10 @@ class Backtesting: """ Rate is within candle, therefore filled""" return row[LOW_IDX] <= rate <= row[HIGH_IDX] - def _call_adjust_stop(self, current_date: datetime, trade: Trade, current_rate: float): + def _call_adjust_stop(self, current_date: datetime, trade: LocalTrade, current_rate: float): profit = trade.calc_profit_ratio(current_rate) - self.strategy.ft_stoploss_adjust(current_rate, trade, current_date, profit, 0, - after_fill=True) + self.strategy.ft_stoploss_adjust(current_rate, trade, # type: ignore + current_date, profit, 0, after_fill=True) def _try_close_open_order( self, order: Optional[Order], trade: LocalTrade, current_date: datetime, diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5010f3db7..43d2a0baf 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1188,15 +1188,16 @@ class IStrategy(ABC, HyperStrategyMixin): bound = (low if trade.is_short else high) bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound) if self.use_custom_stoploss and dir_correct: - stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None, - supress_error=True - )(pair=trade.pair, trade=trade, - current_time=current_time, - current_rate=(bound or current_rate), - current_profit=bound_profit, - after_fill=after_fill) + stop_loss_value_custom = strategy_safe_wrapper( + self.custom_stoploss, default_retval=None, supress_error=True + )(pair=trade.pair, trade=trade, + current_time=current_time, + current_rate=(bound or current_rate), + current_profit=bound_profit, + after_fill=after_fill) # Sanity check - error cases will return None - if stop_loss_value: + if stop_loss_value_custom: + stop_loss_value = stop_loss_value_custom trade.adjust_stop_loss(bound or current_rate, stop_loss_value, allow_refresh=after_fill) else: From 106dffe2c53d0272abc49d160fa821b8df341b76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 17:07:34 +0200 Subject: [PATCH 17/21] split update_trade --- freqtrade/freqtradebot.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f5a6c38b2..681bc71c7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1870,15 +1870,23 @@ class FreqtradeBot(LoggingMixin): trade.update_trade(order_obj) - if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES: + trade = self._update_trade_after_fill(trade, order_obj) + Trade.commit() + + self.order_close_notify(trade, order_obj, stoploss_order, send_msg) + + return False + + def _update_trade_after_fill(self, trade: Trade, order: Order) -> Trade: + if order.status in constants.NON_OPEN_EXCHANGE_STATES: # If a entry order was closed, force update on stoploss on exchange - if order.get('side') == trade.entry_side: + if order.ft_order_side == trade.entry_side: trade = self.cancel_stoploss_on_exchange(trade) if not self.edge: # TODO: should shorting/leverage be supported by Edge, # then this will need to be fixed. trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - if order.get('side') == trade.entry_side or (trade.amount > 0 and trade.is_open): + if order.ft_order_side == trade.entry_side or (trade.amount > 0 and trade.is_open): # Must also run for partial exits # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() @@ -1903,11 +1911,7 @@ class FreqtradeBot(LoggingMixin): after_fill=True) # Updating wallets when order is closed self.wallets.update() - Trade.commit() - - self.order_close_notify(trade, order_obj, stoploss_order, send_msg) - - return False + return trade def order_close_notify( self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool): From 7f1a81eeedadcfd6c0c1c4adba5508c773327a0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 17:08:01 +0200 Subject: [PATCH 18/21] Fix stop switching to trailing if order is replaced in backtesting --- freqtrade/persistence/trade_model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 607d1d2bd..b3bd16817 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -242,6 +242,7 @@ class Order(ModelBase): trade.recalc_trade_from_orders() if trade.nr_of_successful_entries == 1: trade.initial_stop_loss_pct = None + trade.is_stop_loss_trailing = False trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct) @staticmethod @@ -667,7 +668,8 @@ class LocalTrade: or (lower_stop and self.is_short) ): logger.debug(f"{self.pair} - Adjusting stoploss...") - self.is_stop_loss_trailing = True + if not allow_refresh: + self.is_stop_loss_trailing = True self.__set_stop_loss(stop_loss_norm, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") From cb85a530425c306ed533b1a931009bbc50db2219 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 17:08:37 +0200 Subject: [PATCH 19/21] Improve "uses_after_fill" detection (short-circuits some logic, resulting in less code being executed in interface.py) --- freqtrade/resolvers/strategy_resolver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 50605af72..7e0204c0e 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -219,8 +219,9 @@ class StrategyResolver(IResolver): "`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` " "with the metadata argument. ") - after_fill = 'after_fill' in getfullargspec(strategy.custom_stoploss).args - if after_fill: + has_after_fill = ('after_fill' in getfullargspec(strategy.custom_stoploss).args + and check_override(strategy, IStrategy, 'custom_stoploss')) + if has_after_fill: strategy._ft_stop_uses_after_fill = True return strategy From a78d70499860b523dfbeed7b0858d3a3630be2e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 17:29:49 +0200 Subject: [PATCH 20/21] Fix strategy template typng --- .../strategy_subtemplates/strategy_methods_advanced.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index d30588be3..95c6df2ea 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -102,7 +102,7 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f use_custom_stoploss = True -def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, +def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). From 5ed5907809cc1c8f23df61c9a9b013170bb3e3c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Aug 2023 10:14:32 +0200 Subject: [PATCH 21/21] Add explicit example for "after_fill" handling --- docs/strategy-callbacks.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 9f75bd656..f44e7ae0c 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -254,6 +254,36 @@ class AwesomeStrategy(IStrategy): return None ``` +#### Time based trailing stop with after-fill adjustments + +Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. +If an additional order fills, set stoploss to -10% below the new `open_rate` ([Averaged across all entries](#position-adjust-calculations)). + +``` python +from datetime import datetime, timedelta +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> Optional[float]: + + if after_fill: + # After an additional order, start with a stoploss of 10% below the new open rate + return stoploss_from_open(0.10, current_profit, is_short=trade.is_short, leverage=trade.leverage) + # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. + if current_time - timedelta(minutes=120) > trade.open_date_utc: + return -0.05 + elif current_time - timedelta(minutes=60) > trade.open_date_utc: + return -0.10 + return None +``` + #### Different stoploss per pair Use a different stoploss depending on the pair.