Merge pull request #4750 from rokups/rk/custom_sell
Add IStrategy.custom_sell method which allows per-trade sell signal evaluation
This commit is contained in:
@@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from cachetools import TTLCache
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
@@ -28,7 +29,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||
from freqtrade.state import State
|
||||
from freqtrade.strategy.interface import IStrategy, SellType
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -783,10 +784,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||
self.strategy.timeframe)
|
||||
if (config_ask_strategy.get('use_sell_signal', True) or
|
||||
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||
self.strategy.timeframe)
|
||||
|
||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
|
||||
|
||||
@@ -813,13 +814,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
# resulting in outdated RPC messages
|
||||
self._sell_rate_cache[trade.pair] = sell_rate
|
||||
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
else:
|
||||
logger.debug('checking sell')
|
||||
sell_rate = self.get_sell_rate(trade.pair, True)
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
logger.debug('Found no sell signal for %s.', trade)
|
||||
@@ -850,7 +851,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.stoploss_order_id = None
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Selling the trade forcefully')
|
||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
|
||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||
sell_type=SellType.EMERGENCY_SELL))
|
||||
|
||||
except ExchangeError:
|
||||
trade.stoploss_order_id = None
|
||||
@@ -949,19 +951,19 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
||||
def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float,
|
||||
buy: bool, sell: bool) -> bool:
|
||||
"""
|
||||
Check and execute sell
|
||||
"""
|
||||
should_sell = self.strategy.should_sell(
|
||||
trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
||||
dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
|
||||
if should_sell.sell_flag:
|
||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
||||
self.execute_sell(trade, sell_rate, should_sell)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1150,16 +1152,16 @@ class FreqtradeBot(LoggingMixin):
|
||||
raise DependencyException(
|
||||
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||
|
||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool:
|
||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
||||
"""
|
||||
Executes a limit sell for the given trade and limit
|
||||
:param trade: Trade instance
|
||||
:param limit: limit rate for the sell order
|
||||
:param sellreason: Reason the sell was triggered
|
||||
:param sell_reason: Reason the sell was triggered
|
||||
:return: True if it succeeds (supported) False (not supported)
|
||||
"""
|
||||
sell_type = 'sell'
|
||||
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||
sell_type = 'stoploss'
|
||||
|
||||
# if stoploss is on exchange and we are on dry_run mode,
|
||||
@@ -1176,10 +1178,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||
|
||||
order_type = self.strategy.order_types[sell_type]
|
||||
if sell_reason == SellType.EMERGENCY_SELL:
|
||||
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
||||
# Emergency sells (default to market!)
|
||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||
if sell_reason == SellType.FORCE_SELL:
|
||||
if sell_reason.sell_type == SellType.FORCE_SELL:
|
||||
# Force sells (default to the sell_type defined in the strategy,
|
||||
# but we allow this value to be changed)
|
||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||
@@ -1190,7 +1192,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
||||
time_in_force=time_in_force,
|
||||
sell_reason=sell_reason.value):
|
||||
sell_reason=sell_reason.sell_reason):
|
||||
logger.info(f"User requested abortion of selling {trade.pair}")
|
||||
return False
|
||||
|
||||
@@ -1213,7 +1215,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.open_order_id = order['id']
|
||||
trade.sell_order_status = ''
|
||||
trade.close_rate_requested = limit
|
||||
trade.sell_reason = sell_reason.value
|
||||
trade.sell_reason = sell_reason.sell_reason
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') == 'closed':
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
|
@@ -247,15 +247,17 @@ class Backtesting:
|
||||
else:
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade,
|
||||
sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
|
||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
||||
sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
|
||||
sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore
|
||||
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX],
|
||||
sell_row[SELL_IDX],
|
||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||
|
||||
if sell.sell_flag:
|
||||
trade.close_date = sell_row[DATE_IDX]
|
||||
trade.sell_reason = sell.sell_type.value
|
||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
||||
trade.sell_reason = sell.sell_reason
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
|
||||
@@ -265,7 +267,7 @@ class Backtesting:
|
||||
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
||||
rate=closerate,
|
||||
time_in_force=time_in_force,
|
||||
sell_reason=sell.sell_type.value):
|
||||
sell_reason=sell.sell_reason):
|
||||
return None
|
||||
|
||||
trade.close(closerate, show_msg=False)
|
||||
@@ -293,7 +295,7 @@ class Backtesting:
|
||||
trade = LocalTrade(
|
||||
pair=pair,
|
||||
open_rate=row[OPEN_IDX],
|
||||
open_date=row[DATE_IDX],
|
||||
open_date=row[DATE_IDX].to_pydatetime(),
|
||||
stake_amount=stake_amount,
|
||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
||||
fee_open=self.fee,
|
||||
@@ -315,7 +317,7 @@ class Backtesting:
|
||||
for trade in open_trades[pair]:
|
||||
sell_row = data[pair][-1]
|
||||
|
||||
trade.close_date = sell_row[DATE_IDX]
|
||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
||||
trade.sell_reason = SellType.FORCE_SELL.value
|
||||
trade.close(sell_row[OPEN_IDX], show_msg=False)
|
||||
LocalTrade.close_bt_trade(trade)
|
||||
@@ -396,7 +398,7 @@ class Backtesting:
|
||||
|
||||
for trade in open_trades[pair]:
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||
trade_entry = self._get_sell_trade_entry(processed[pair], trade, row)
|
||||
# Sell occured
|
||||
if trade_entry:
|
||||
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
||||
|
@@ -24,7 +24,7 @@ from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.state import State
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -554,7 +554,8 @@ class RPC:
|
||||
if not fully_canceled:
|
||||
# Get current rate and execute sell
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
|
||||
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
|
||||
# ---- EOF def _exec_forcesell ----
|
||||
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
|
@@ -7,7 +7,7 @@ import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
@@ -24,6 +24,7 @@ from freqtrade.wallets import Wallets
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
CUSTOM_SELL_MAX_LENGTH = 64
|
||||
|
||||
|
||||
class SignalType(Enum):
|
||||
@@ -45,6 +46,7 @@ class SellType(Enum):
|
||||
SELL_SIGNAL = "sell_signal"
|
||||
FORCE_SELL = "force_sell"
|
||||
EMERGENCY_SELL = "emergency_sell"
|
||||
CUSTOM_SELL = "custom_sell"
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
@@ -52,12 +54,20 @@ class SellType(Enum):
|
||||
return self.value
|
||||
|
||||
|
||||
class SellCheckTuple(NamedTuple):
|
||||
class SellCheckTuple(object):
|
||||
"""
|
||||
NamedTuple for Sell type + reason
|
||||
"""
|
||||
sell_flag: bool
|
||||
sell_type: SellType
|
||||
sell_reason: str = ''
|
||||
|
||||
def __init__(self, sell_type: SellType, sell_reason: str = ''):
|
||||
self.sell_type = sell_type
|
||||
self.sell_reason = sell_reason or sell_type.value
|
||||
|
||||
@property
|
||||
def sell_flag(self):
|
||||
return self.sell_type != SellType.NONE
|
||||
|
||||
|
||||
class IStrategy(ABC, HyperStrategyMixin):
|
||||
@@ -264,7 +274,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, dataframe: DataFrame, **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.
|
||||
@@ -280,11 +290,37 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New stoploss value, relative to the currentrate
|
||||
"""
|
||||
return self.stoploss
|
||||
|
||||
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||
current_profit: float, dataframe: DataFrame,
|
||||
**kwargs) -> Optional[Union[str, bool]]:
|
||||
"""
|
||||
Custom sell signal logic indicating that specified position should be sold. Returning a
|
||||
string or True from this method is equal to setting sell signal on a candle at specified
|
||||
time. This method is not called when sell signal is set.
|
||||
|
||||
This method should be overridden to create sell signals that depend on trade parameters. For
|
||||
example you could implement a stoploss relative to candle when trade was opened, or a custom
|
||||
1:2 risk-reward ROI.
|
||||
|
||||
Custom sell reason max length is 64. Exceeding this limit will raise OperationalException.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: trade object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return: To execute sell, return a string with custom sell reason or True. Otherwise return
|
||||
None or False.
|
||||
"""
|
||||
return None
|
||||
|
||||
def informative_pairs(self) -> ListPairsWithTimeframes:
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
@@ -500,8 +536,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
return False
|
||||
|
||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
||||
sell: bool, low: float = None, high: float = None,
|
||||
def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime,
|
||||
buy: bool, sell: bool, low: float = None, high: float = None,
|
||||
force_stoploss: float = 0) -> SellCheckTuple:
|
||||
"""
|
||||
This function evaluates if one of the conditions required to trigger a sell
|
||||
@@ -517,8 +553,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
trade.adjust_min_max_rates(high or current_rate)
|
||||
|
||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=date, current_profit=current_profit,
|
||||
stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate,
|
||||
trade=trade, current_time=date,
|
||||
current_profit=current_profit,
|
||||
force_stoploss=force_stoploss, high=high)
|
||||
|
||||
# Set current rate to high for backtesting sell
|
||||
@@ -531,12 +568,29 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
and self.min_roi_reached(trade=trade, current_profit=current_profit,
|
||||
current_time=date))
|
||||
|
||||
sell_signal = SellType.NONE
|
||||
custom_reason = ''
|
||||
if (ask_strategy.get('sell_profit_only', False)
|
||||
and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
|
||||
# sell_profit_only and profit doesn't reach the offset - ignore sell signal
|
||||
sell_signal = False
|
||||
else:
|
||||
sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True)
|
||||
pass
|
||||
elif ask_strategy.get('use_sell_signal', True) and not buy:
|
||||
if sell:
|
||||
sell_signal = SellType.SELL_SIGNAL
|
||||
else:
|
||||
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
|
||||
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate,
|
||||
current_profit=current_profit, dataframe=dataframe)
|
||||
if custom_reason:
|
||||
sell_signal = SellType.CUSTOM_SELL
|
||||
if isinstance(custom_reason, str):
|
||||
if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH:
|
||||
logger.warning(f'Custom sell reason returned from custom_sell is too '
|
||||
f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} '
|
||||
f'characters.')
|
||||
custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
|
||||
else:
|
||||
custom_reason = None
|
||||
# TODO: return here if sell-signal should be favored over ROI
|
||||
|
||||
# Start evaluations
|
||||
@@ -545,26 +599,25 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# Sell-signal
|
||||
# Stoploss
|
||||
if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
|
||||
logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, "
|
||||
f"sell_type=SellType.ROI")
|
||||
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
|
||||
logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI")
|
||||
return SellCheckTuple(sell_type=SellType.ROI)
|
||||
|
||||
if sell_signal:
|
||||
logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
|
||||
f"sell_type=SellType.SELL_SIGNAL")
|
||||
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
|
||||
if sell_signal != SellType.NONE:
|
||||
logger.debug(f"{trade.pair} - Sell signal received. "
|
||||
f"sell_type=SellType.{sell_signal.name}" +
|
||||
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
||||
return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason)
|
||||
|
||||
if stoplossflag.sell_flag:
|
||||
|
||||
logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
|
||||
f"sell_type={stoplossflag.sell_type}")
|
||||
logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}")
|
||||
return stoplossflag
|
||||
|
||||
# This one is noisy, commented out...
|
||||
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
# logger.debug(f"{trade.pair} - No sell signal.")
|
||||
return SellCheckTuple(sell_type=SellType.NONE)
|
||||
|
||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
||||
def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade,
|
||||
current_time: datetime, current_profit: float,
|
||||
force_stoploss: float, high: float = None) -> SellCheckTuple:
|
||||
"""
|
||||
@@ -582,7 +635,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
)(pair=trade.pair, trade=trade,
|
||||
current_time=current_time,
|
||||
current_rate=current_rate,
|
||||
current_profit=current_profit)
|
||||
current_profit=current_profit,
|
||||
dataframe=dataframe)
|
||||
# Sanity check - error cases will return None
|
||||
if stop_loss_value:
|
||||
# logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}")
|
||||
@@ -626,9 +680,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
logger.debug(f"{trade.pair} - Trailing stop saved "
|
||||
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
|
||||
|
||||
return SellCheckTuple(sell_flag=True, sell_type=sell_type)
|
||||
return SellCheckTuple(sell_type=sell_type)
|
||||
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
return SellCheckTuple(sell_type=SellType.NONE)
|
||||
|
||||
def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:
|
||||
"""
|
||||
|
@@ -14,8 +14,9 @@ def bot_loop_start(self, **kwargs) -> None:
|
||||
|
||||
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, dataframe: DataFrame,
|
||||
**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.
|
||||
@@ -31,6 +32,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', c
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New stoploss value, relative to the currentrate
|
||||
"""
|
||||
|
Reference in New Issue
Block a user