Merge pull request #9057 from freqtrade/feat/stoploss_adjust

"After order" stoploss adjustment
This commit is contained in:
Matthias
2023-08-29 18:06:37 +02:00
committed by GitHub
12 changed files with 185 additions and 62 deletions
+73 -24
View File
@@ -164,6 +164,31 @@ 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). 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. 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: To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
@@ -179,7 +204,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
""" """
Custom stoploss logic, returning the new distance relative to current_rate (as ratio). 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. e.g. returning -0.05 would create a stoploss 5% below current_rate.
@@ -187,7 +213,7 @@ class AwesomeStrategy(IStrategy):
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ 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. Only called when use_custom_stoploss is set to True.
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
@@ -195,25 +221,13 @@ class AwesomeStrategy(IStrategy):
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing. :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 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. :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 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 #### 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. 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.
@@ -229,14 +243,45 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom. # 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: if current_time - timedelta(minutes=120) > trade.open_date_utc:
return -0.05 return -0.05
elif current_time - timedelta(minutes=60) > trade.open_date_utc: elif current_time - timedelta(minutes=60) > trade.open_date_utc:
return -0.10 return -0.10
return 1 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 #### Different stoploss per pair
@@ -255,7 +300,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
if pair in ('ETH/BTC', 'XRP/BTC'): if pair in ('ETH/BTC', 'XRP/BTC'):
return -0.10 return -0.10
@@ -281,7 +327,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
if current_profit < 0.04: if current_profit < 0.04:
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
@@ -314,7 +361,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
# evaluate highest to lowest, so that highest possible stop is used # evaluate highest to lowest, so that highest possible stop is used
if current_profit > 0.40: if current_profit > 0.40:
@@ -325,7 +373,7 @@ class AwesomeStrategy(IStrategy):
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) 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 maximum stoploss value, keeping current stoploss price unchanged
return 1 return None
``` ```
#### Custom stoploss using an indicator from dataframe example #### Custom stoploss using an indicator from dataframe example
@@ -342,7 +390,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze() last_candle = dataframe.iloc[-1].squeeze()
@@ -355,7 +404,7 @@ class AwesomeStrategy(IStrategy):
return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short) return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)
# return maximum stoploss value, keeping current stoploss price unchanged # 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. See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
+4 -2
View File
@@ -901,7 +901,8 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
# once the profit has risen above 10%, keep the stoploss at 7% above the open price # once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10: 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 return dataframe
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
candle = dataframe.iloc[-1].squeeze() candle = dataframe.iloc[-1].squeeze()
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short) return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
+2 -1
View File
@@ -311,7 +311,8 @@ After:
``` python hl_lines="5 7" ``` python hl_lines="5 7"
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, 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) -> Optional[float]:
# once the profit has risen above 10%, keep the stoploss at 7% above the open price # once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10: if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
+19 -8
View File
@@ -1870,15 +1870,23 @@ class FreqtradeBot(LoggingMixin):
trade.update_trade(order_obj) 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 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) trade = self.cancel_stoploss_on_exchange(trade)
if not self.edge: if not self.edge:
# TODO: should shorting/leverage be supported by Edge, # TODO: should shorting/leverage be supported by Edge,
# then this will need to be fixed. # then this will need to be fixed.
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) 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 # Must also run for partial exits
# TODO: Margin will need to use interest_rate as well. # TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate() # interest_rate = self.exchange.get_interest_rate()
@@ -1894,13 +1902,16 @@ class FreqtradeBot(LoggingMixin):
)) ))
except DependencyException: except DependencyException:
logger.warning('Unable to calculate liquidation price') 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 # Updating wallets when order is closed
self.wallets.update() self.wallets.update()
Trade.commit() return trade
self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
return False
def order_close_notify( def order_close_notify(
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool): self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
+8
View File
@@ -579,6 +579,11 @@ class Backtesting:
""" Rate is within candle, therefore filled""" """ Rate is within candle, therefore filled"""
return row[LOW_IDX] <= rate <= row[HIGH_IDX] return row[LOW_IDX] <= rate <= row[HIGH_IDX]
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, # type: ignore
current_date, profit, 0, after_fill=True)
def _try_close_open_order( def _try_close_open_order(
self, order: Optional[Order], trade: LocalTrade, current_date: datetime, self, order: Optional[Order], trade: LocalTrade, current_date: datetime,
row: Tuple) -> bool: row: Tuple) -> bool:
@@ -589,6 +594,9 @@ class Backtesting:
if order and self._get_order_filled(order.ft_price, row): if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_date, trade) order.close_bt_order(current_date, trade)
trade.open_order_id = None 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 True
return False return False
+7 -3
View File
@@ -88,6 +88,9 @@ def migrate_trades_and_orders_table(
stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') 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 = get_column_def(cols, 'initial_stop_loss', '0.0')
initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') 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',
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_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
max_rate = get_column_def(cols, 'max_rate', '0.0') max_rate = get_column_def(cols, 'max_rate', '0.0')
@@ -156,7 +159,7 @@ def migrate_trades_and_orders_table(
open_rate_requested, close_rate, close_rate_requested, close_profit, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, 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, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
timeframe, open_trade_value, close_profit_abs, timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short, trading_mode, leverage, liquidation_price, is_short,
@@ -175,6 +178,7 @@ def migrate_trades_and_orders_table(
{stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
{initial_stop_loss} initial_stop_loss, {initial_stop_loss} initial_stop_loss,
{initial_stop_loss_pct} initial_stop_loss_pct, {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, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {max_rate} max_rate, {min_rate} min_rate,
case when {exit_reason} = 'sell_signal' then 'exit_signal' case when {exit_reason} = 'sell_signal' then 'exit_signal'
@@ -316,8 +320,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# if ('orders' not in previous_tables # if ('orders' not in previous_tables
# or not has_column(cols_orders, 'funding_fee')): # or not has_column(cols_orders, 'funding_fee')):
migrating = False 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 migrating = True
logger.info(f"Running database migration for trades - " logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}") f"backup: {table_back_name}, {order_table_bak_name}")
+18 -6
View File
@@ -240,7 +240,10 @@ class Order(ModelBase):
if (self.ft_order_side == trade.entry_side and self.price): if (self.ft_order_side == trade.entry_side and self.price):
trade.open_rate = self.price trade.open_rate = self.price
trade.recalc_trade_from_orders() trade.recalc_trade_from_orders()
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True) 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 @staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]): def update_orders(orders: List['Order'], order: Dict[str, Any]):
@@ -349,6 +352,7 @@ class LocalTrade:
initial_stop_loss: Optional[float] = 0.0 initial_stop_loss: Optional[float] = 0.0
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct: Optional[float] = None initial_stop_loss_pct: Optional[float] = None
is_stop_loss_trailing: bool = False
# stoploss order id which is on exchange # stoploss order id which is on exchange
stoploss_order_id: Optional[str] = None stoploss_order_id: Optional[str] = None
# last update time of the stoploss order on exchange # last update time of the stoploss order on exchange
@@ -621,18 +625,18 @@ class LocalTrade:
self.stop_loss_pct = -1 * abs(percent) self.stop_loss_pct = -1 * abs(percent)
def adjust_stop_loss(self, current_price: float, stoploss: Optional[float], 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 This adjusts the stop loss to it's most recently observed setting
:param current_price: Current rate the asset is traded :param current_price: Current rate the asset is traded
:param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price). :param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price).
:param initial: Called to initiate stop_loss. :param initial: Called to initiate stop_loss.
Skips everything if self.stop_loss is already set. 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)): 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 # Don't modify if called with initial and nothing to do
return return
refresh = True if refresh and self.nr_of_successful_entries == 1 else False
leverage = self.leverage or 1.0 leverage = self.leverage or 1.0
if self.is_short: if self.is_short:
@@ -643,7 +647,7 @@ class LocalTrade:
stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode, 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) rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
# no stop loss assigned yet # no stop loss assigned yet
if self.initial_stop_loss_pct is None or refresh: if self.initial_stop_loss_pct is None:
self.__set_stop_loss(stop_loss_norm, stoploss) self.__set_stop_loss(stop_loss_norm, stoploss)
self.initial_stop_loss = price_to_precision( self.initial_stop_loss = price_to_precision(
stop_loss_norm, self.price_precision, self.precision_mode, stop_loss_norm, self.price_precision, self.precision_mode,
@@ -658,8 +662,14 @@ class LocalTrade:
# stop losses only walk up, never down!, # stop losses only walk up, never down!,
# ? But adding more to a leveraged trade would create a lower liquidation price, # ? But adding more to a leveraged trade would create a lower liquidation price,
# ? decreasing the minimum stoploss # ? 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...") logger.debug(f"{self.pair} - Adjusting stoploss...")
if not allow_refresh:
self.is_stop_loss_trailing = True
self.__set_stop_loss(stop_loss_norm, stoploss) self.__set_stop_loss(stop_loss_norm, stoploss)
else: else:
logger.debug(f"{self.pair} - Keeping current stoploss...") logger.debug(f"{self.pair} - Keeping current stoploss...")
@@ -1194,7 +1204,7 @@ class LocalTrade:
logger.info(f"Found open trade: {trade}") logger.info(f"Found open trade: {trade}")
# skip case if trailing-stop changed the stoploss already. # 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): and trade.initial_stop_loss_pct != desired_stoploss):
# Stoploss value got changed # Stoploss value got changed
@@ -1267,6 +1277,8 @@ class Trade(ModelBase, LocalTrade):
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column( initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True) # type: ignore 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 which is on exchange
stoploss_order_id: Mapped[Optional[str]] = mapped_column( stoploss_order_id: Mapped[Optional[str]] = mapped_column(
String(255), nullable=True, index=True) # type: ignore String(255), nullable=True, index=True) # type: ignore
+6
View File
@@ -218,6 +218,12 @@ class StrategyResolver(IResolver):
"Please update your strategy to implement " "Please update your strategy to implement "
"`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` " "`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` "
"with the metadata argument. ") "with the metadata argument. ")
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 return strategy
@staticmethod @staticmethod
+19 -10
View File
@@ -373,7 +373,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return True return 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, **kwargs) -> float: current_profit: float, after_fill: bool, **kwargs) -> Optional[float]:
""" """
Custom stoploss logic, returning the new distance relative to current_rate (as ratio). 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. 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_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing. :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 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. :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
""" """
@@ -719,6 +720,8 @@ class IStrategy(ABC, HyperStrategyMixin):
# END - Intended to be overridden by strategy # END - Intended to be overridden by strategy
### ###
_ft_stop_uses_after_fill = False
def __informative_pairs_freqai(self) -> ListPairsWithTimeframes: def __informative_pairs_freqai(self) -> ListPairsWithTimeframes:
""" """
Create informative-pairs needed for FreqAI Create informative-pairs needed for FreqAI
@@ -1160,13 +1163,17 @@ class IStrategy(ABC, HyperStrategyMixin):
def ft_stoploss_adjust(self, current_rate: float, trade: Trade, def ft_stoploss_adjust(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float, current_time: datetime, current_profit: float,
force_stoploss: float, low: Optional[float] = None, 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. Adjust stop-loss dynamically if configured to do so.
:param current_profit: current profit as ratio :param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting :param low: Low value of this candle, only set in backtesting
:param high: High 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 stop_loss_value = force_stoploss if force_stoploss else self.stoploss
# Initiate stoploss with open_rate. Does nothing if stoploss is already set. # Initiate stoploss with open_rate. Does nothing if stoploss is already set.
@@ -1181,18 +1188,20 @@ class IStrategy(ABC, HyperStrategyMixin):
bound = (low if trade.is_short else high) bound = (low if trade.is_short else high)
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound) bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
if self.use_custom_stoploss and dir_correct: if self.use_custom_stoploss and dir_correct:
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None, stop_loss_value_custom = strategy_safe_wrapper(
supress_error=True self.custom_stoploss, default_retval=None, supress_error=True
)(pair=trade.pair, trade=trade, )(pair=trade.pair, trade=trade,
current_time=current_time, current_time=current_time,
current_rate=(bound or current_rate), 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 # Sanity check - error cases will return None
if stop_loss_value: if stop_loss_value_custom:
# logger.info(f"{trade.pair} {stop_loss_value=} {bound_profit=}") stop_loss_value = stop_loss_value_custom
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: 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: if self.trailing_stop and dir_correct:
# trailing stoploss handling # trailing stoploss handling
@@ -1245,7 +1254,7 @@ class IStrategy(ABC, HyperStrategyMixin):
exit_type = ExitType.STOP_LOSS exit_type = ExitType.STOP_LOSS
# If initial stoploss is not the same as current one then it is trailing. # 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 exit_type = ExitType.TRAILING_STOP_LOSS
logger.debug( logger.debug(
f"{trade.pair} - HIT STOP: current price at " f"{trade.pair} - HIT STOP: current price at "
@@ -102,8 +102,8 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
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). 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. 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/ 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. Only called when use_custom_stoploss is set to True.
:param pair: Pair that's currently analyzed :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_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing. :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 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. :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 self.stoploss
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]':
+20
View File
@@ -77,18 +77,28 @@ def test_set_stop_loss_liquidation(fee):
assert trade.liquidation_price == 0.11 assert trade.liquidation_price == 0.11
# Stoploss does not change from liquidation price # Stoploss does not change from liquidation price
assert trade.stop_loss == 1.8 assert trade.stop_loss == 1.8
assert trade.stop_loss_pct == -0.2
assert trade.initial_stop_loss == 1.8 assert trade.initial_stop_loss == 1.8
# lower stop doesn't move stoploss # lower stop doesn't move stoploss
trade.adjust_stop_loss(1.8, 0.2) trade.adjust_stop_loss(1.8, 0.2)
assert trade.liquidation_price == 0.11 assert trade.liquidation_price == 0.11
assert trade.stop_loss == 1.8 assert trade.stop_loss == 1.8
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 assert trade.initial_stop_loss == 1.8
# higher stop does move stoploss # higher stop does move stoploss
trade.adjust_stop_loss(2.1, 0.1) trade.adjust_stop_loss(2.1, 0.1)
assert trade.liquidation_price == 0.11 assert trade.liquidation_price == 0.11
assert pytest.approx(trade.stop_loss) == 1.994999 assert pytest.approx(trade.stop_loss) == 1.994999
assert trade.stop_loss_pct == -0.1
assert trade.initial_stop_loss == 1.8 assert trade.initial_stop_loss == 1.8
assert trade.stoploss_or_liquidation == trade.stop_loss assert trade.stoploss_or_liquidation == trade.stop_loss
@@ -130,12 +140,21 @@ def test_set_stop_loss_liquidation(fee):
assert trade.liquidation_price == 3.8 assert trade.liquidation_price == 3.8
# Stoploss does not change from liquidation price # Stoploss does not change from liquidation price
assert trade.stop_loss == 2.2 assert trade.stop_loss == 2.2
assert trade.stop_loss_pct == -0.2
assert trade.initial_stop_loss == 2.2 assert trade.initial_stop_loss == 2.2
# Stop doesn't move stop higher # Stop doesn't move stop higher
trade.adjust_stop_loss(2.0, 0.3) trade.adjust_stop_loss(2.0, 0.3)
assert trade.liquidation_price == 3.8 assert trade.liquidation_price == 3.8
assert trade.stop_loss == 2.2 assert trade.stop_loss == 2.2
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 assert trade.initial_stop_loss == 2.2
# Stoploss does move lower # Stoploss does move lower
@@ -143,6 +162,7 @@ def test_set_stop_loss_liquidation(fee):
trade.adjust_stop_loss(1.8, 0.1) trade.adjust_stop_loss(1.8, 0.1)
assert trade.liquidation_price == 1.5 assert trade.liquidation_price == 1.5
assert pytest.approx(trade.stop_loss) == 1.89 assert pytest.approx(trade.stop_loss) == 1.89
assert trade.stop_loss_pct == -0.1
assert trade.initial_stop_loss == 2.2 assert trade.initial_stop_loss == 2.2
assert trade.stoploss_or_liquidation == 1.5 assert trade.stoploss_or_liquidation == 1.5
+2 -1
View File
@@ -52,4 +52,5 @@ def test_strategy_test_v3(dataframe_1m, fee, is_short, side):
side=side) is True side=side) is True
assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), 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