mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-02-03 21:00:25 +00:00
Merge pull request #9057 from freqtrade/feat/stoploss_adjust
"After order" stoploss adjustment
This commit is contained in:
@@ -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).
|
||||
|
||||
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:
|
||||
|
||||
@@ -179,7 +204,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) -> 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.
|
||||
@@ -187,7 +213,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,25 +221,13 @@ 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
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -229,14 +243,45 @@ 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) -> 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
|
||||
```
|
||||
|
||||
#### 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
|
||||
@@ -255,7 +300,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) -> Optional[float]:
|
||||
|
||||
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||
return -0.10
|
||||
@@ -281,7 +327,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) -> Optional[float]:
|
||||
|
||||
if current_profit < 0.04:
|
||||
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
|
||||
|
||||
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
|
||||
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 maximum stoploss value, keeping current stoploss price unchanged
|
||||
return 1
|
||||
return None
|
||||
```
|
||||
|
||||
#### Custom stoploss using an indicator from dataframe example
|
||||
@@ -342,7 +390,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) -> Optional[float]:
|
||||
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
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 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.
|
||||
|
||||
@@ -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) -> Optional[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) -> 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)
|
||||
|
||||
@@ -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, **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)
|
||||
|
||||
@@ -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()
|
||||
@@ -1894,13 +1902,16 @@ 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()
|
||||
|
||||
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):
|
||||
|
||||
@@ -579,6 +579,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: 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(
|
||||
self, order: Optional[Order], trade: LocalTrade, current_date: datetime,
|
||||
row: Tuple) -> bool:
|
||||
@@ -589,6 +594,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
|
||||
|
||||
|
||||
@@ -88,6 +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',
|
||||
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')
|
||||
@@ -156,7 +159,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 +178,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 +320,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}")
|
||||
|
||||
@@ -240,7 +240,10 @@ 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)
|
||||
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
|
||||
def update_orders(orders: List['Order'], order: Dict[str, Any]):
|
||||
@@ -349,6 +352,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
|
||||
@@ -621,18 +625,18 @@ 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
|
||||
: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
|
||||
|
||||
leverage = self.leverage or 1.0
|
||||
if self.is_short:
|
||||
@@ -643,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 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,
|
||||
@@ -658,8 +662,14 @@ 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...")
|
||||
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...")
|
||||
@@ -1194,7 +1204,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
|
||||
|
||||
@@ -1267,6 +1277,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
|
||||
|
||||
@@ -218,6 +218,12 @@ class StrategyResolver(IResolver):
|
||||
"Please update your strategy to implement "
|
||||
"`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` "
|
||||
"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
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -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) -> 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.
|
||||
@@ -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
|
||||
"""
|
||||
@@ -719,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
|
||||
@@ -1160,13 +1163,17 @@ 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
|
||||
: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.
|
||||
@@ -1181,18 +1188,20 @@ 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)
|
||||
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:
|
||||
# logger.info(f"{trade.pair} {stop_loss_value=} {bound_profit=}")
|
||||
trade.adjust_stop_loss(bound or current_rate, 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:
|
||||
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
|
||||
@@ -1245,7 +1254,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 "
|
||||
|
||||
@@ -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]]':
|
||||
|
||||
@@ -77,18 +77,28 @@ 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
|
||||
|
||||
# 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
|
||||
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 +140,21 @@ 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
|
||||
|
||||
# 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
|
||||
@@ -143,6 +162,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user