Added short and exit_short to strategy

This commit is contained in:
Sam Germain 2021-08-08 03:38:34 -06:00
parent 98fe3e73de
commit d4a7d2d444
24 changed files with 862 additions and 152 deletions

View File

@ -167,8 +167,15 @@ class Edge:
pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.sort_values(by=['date'])
pair_data = pair_data.reset_index(drop=True) pair_data = pair_data.reset_index(drop=True)
df_analyzed = self.strategy.advise_sell( df_analyzed = self.strategy.advise_exit(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() dataframe=self.strategy.advise_enter(
dataframe=pair_data,
metadata={'pair': pair},
is_short=False
),
metadata={'pair': pair},
is_short=False
)[headers].copy()
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range) trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)

View File

@ -7,6 +7,8 @@ class SignalType(Enum):
""" """
BUY = "buy" BUY = "buy"
SELL = "sell" SELL = "sell"
SHORT = "short"
EXIT_SHORT = "exit_short"
class SignalTagType(Enum): class SignalTagType(Enum):
@ -14,3 +16,4 @@ class SignalTagType(Enum):
Enum for signal columns Enum for signal columns
""" """
BUY_TAG = "buy_tag" BUY_TAG = "buy_tag"
SELL_TAG = "sell_tag"

View File

@ -231,8 +231,8 @@ class Backtesting:
if has_buy_tag: if has_buy_tag:
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
df_analyzed = self.strategy.advise_sell( df_analyzed = self.strategy.advise_exit(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() self.strategy.advise_enter(pair_data, {'pair': pair}), {'pair': pair}).copy()
# Trim startup period from analyzed dataframe # Trim startup period from analyzed dataframe
df_analyzed = trim_dataframe(df_analyzed, self.timerange, df_analyzed = trim_dataframe(df_analyzed, self.timerange,
startup_candles=self.required_startup) startup_candles=self.required_startup)

View File

@ -110,7 +110,7 @@ class Hyperopt:
self.backtesting.strategy.advise_indicators = ( # type: ignore self.backtesting.strategy.advise_indicators = ( # type: ignore
self.custom_hyperopt.populate_indicators) # type: ignore self.custom_hyperopt.populate_indicators) # type: ignore
if hasattr(self.custom_hyperopt, 'populate_buy_trend'): if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.backtesting.strategy.advise_buy = ( # type: ignore self.backtesting.strategy.advise_enter = ( # type: ignore
self.custom_hyperopt.populate_buy_trend) # type: ignore self.custom_hyperopt.populate_buy_trend) # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'): if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.backtesting.strategy.advise_sell = ( # type: ignore self.backtesting.strategy.advise_sell = ( # type: ignore
@ -283,12 +283,13 @@ class Hyperopt:
params_dict = self._get_params_dict(self.dimensions, raw_params) params_dict = self._get_params_dict(self.dimensions, raw_params)
# Apply parameters # Apply parameters
# TODO-lev: These don't take a side, how can I pass is_short=True/False to it
if HyperoptTools.has_space(self.config, 'buy'): if HyperoptTools.has_space(self.config, 'buy'):
self.backtesting.strategy.advise_buy = ( # type: ignore self.backtesting.strategy.advise_enter = ( # type: ignore
self.custom_hyperopt.buy_strategy_generator(params_dict)) self.custom_hyperopt.buy_strategy_generator(params_dict))
if HyperoptTools.has_space(self.config, 'sell'): if HyperoptTools.has_space(self.config, 'sell'):
self.backtesting.strategy.advise_sell = ( # type: ignore self.backtesting.strategy.advise_exit = ( # type: ignore
self.custom_hyperopt.sell_strategy_generator(params_dict)) self.custom_hyperopt.sell_strategy_generator(params_dict))
if HyperoptTools.has_space(self.config, 'protection'): if HyperoptTools.has_space(self.config, 'protection'):

View File

@ -51,6 +51,7 @@ class HyperOptResolver(IResolver):
if not hasattr(hyperopt, 'populate_sell_trend'): if not hasattr(hyperopt, 'populate_sell_trend'):
logger.info("Hyperopt class does not provide populate_sell_trend() method. " logger.info("Hyperopt class does not provide populate_sell_trend() method. "
"Using populate_sell_trend from the strategy.") "Using populate_sell_trend from the strategy.")
# TODO-lev: Short equivelents?
return hyperopt return hyperopt

View File

@ -202,9 +202,14 @@ class StrategyResolver(IResolver):
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
strategy._short_fun_len = len(getfullargspec(strategy.populate_short_trend).args)
strategy._exit_short_fun_len = len(
getfullargspec(strategy.populate_exit_short_trend).args)
if any(x == 2 for x in [strategy._populate_fun_len, if any(x == 2 for x in [strategy._populate_fun_len,
strategy._buy_fun_len, strategy._buy_fun_len,
strategy._sell_fun_len]): strategy._sell_fun_len,
strategy._short_fun_len,
strategy._exit_short_fun_len]):
strategy.INTERFACE_VERSION = 1 strategy.INTERFACE_VERSION = 1
return strategy return strategy

View File

@ -44,5 +44,5 @@ class UvicornServer(uvicorn.Server):
time.sleep(1e-3) time.sleep(1e-3)
def cleanup(self): def cleanup(self):
self.should_exit = True self.should_sell = True
self.thread.join() self.thread.join()

View File

@ -22,6 +22,8 @@ from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# TODO-lev: This file
class BaseParameter(ABC): class BaseParameter(ABC):
""" """

View File

@ -62,6 +62,8 @@ class IStrategy(ABC, HyperStrategyMixin):
_populate_fun_len: int = 0 _populate_fun_len: int = 0
_buy_fun_len: int = 0 _buy_fun_len: int = 0
_sell_fun_len: int = 0 _sell_fun_len: int = 0
_short_fun_len: int = 0
_exit_short_fun_len: int = 0
_ft_params_from_file: Dict = {} _ft_params_from_file: Dict = {}
# associated minimal roi # associated minimal roi
minimal_roi: Dict minimal_roi: Dict
@ -135,7 +137,7 @@ class IStrategy(ABC, HyperStrategyMixin):
@abstractmethod @abstractmethod
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Populate indicators that will be used in the Buy and Sell strategy Populate indicators that will be used in the Buy, Sell, Short, Exit_short strategy
:param dataframe: DataFrame with data from the exchange :param dataframe: DataFrame with data from the exchange
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
@ -143,7 +145,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return dataframe return dataframe
@abstractmethod @abstractmethod
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_enter_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
@ -153,7 +155,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return dataframe return dataframe
@abstractmethod @abstractmethod
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
@ -164,9 +166,9 @@ class IStrategy(ABC, HyperStrategyMixin):
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
""" """
Check buy timeout function callback. Check enter timeout function callback.
This method can be used to override the buy-timeout. This method can be used to override the enter-timeout.
It is called whenever a limit buy order has been created, It is called whenever a limit buy/short order has been created,
and is not yet fully filled. and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this, Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough. so ensure to set these timeouts high enough.
@ -176,16 +178,16 @@ class IStrategy(ABC, HyperStrategyMixin):
:param trade: trade object. :param trade: trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order dictionary as returned from CCXT.
: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 bool: When True is returned, then the buy-order is cancelled. :return bool: When True is returned, then the buy/short-order is cancelled.
""" """
return False return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
""" """
Check sell timeout function callback. Check exit timeout function callback.
This method can be used to override the sell-timeout. This method can be used to override the exit-timeout.
It is called whenever a limit sell order has been created, It is called whenever a (long) limit sell order or (short) limit buy
and is not yet fully filled. has been created, and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this, Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough. so ensure to set these timeouts high enough.
@ -194,7 +196,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param trade: trade object. :param trade: trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order dictionary as returned from CCXT.
: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 bool: When True is returned, then the sell-order is cancelled. :return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled.
""" """
return False return False
@ -210,7 +212,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool: time_in_force: str, current_time: datetime, **kwargs) -> bool:
""" """
Called right before placing a buy order. Called right before placing a buy/short order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
network requests in this method. network requests in this method.
@ -218,7 +220,7 @@ class IStrategy(ABC, HyperStrategyMixin):
When not implemented by a strategy, returns True (always confirming). When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought. :param pair: Pair that's about to be bought/shorted.
:param order_type: Order type (as configured in order_types). usually limit or market. :param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded. :param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
@ -234,7 +236,7 @@ class IStrategy(ABC, HyperStrategyMixin):
rate: float, time_in_force: str, sell_reason: str, rate: float, time_in_force: str, sell_reason: str,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Called right before placing a regular sell order. Called right before placing a regular sell/exit_short order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
network requests in this method. network requests in this method.
@ -242,18 +244,18 @@ class IStrategy(ABC, HyperStrategyMixin):
When not implemented by a strategy, returns True (always confirming). When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be sold. :param pair: Pair for trade that's about to be exited.
:param trade: trade object. :param trade: trade object.
:param order_type: Order type (as configured in order_types). usually limit or market. :param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in quote currency. :param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason. :param sell_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
: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 bool: When True is returned, then the sell-order is placed on the exchange. :return bool: When True, then the sell-order/exit_short-order is placed on the exchange.
False aborts the process False aborts the process
""" """
return True return True
@ -283,15 +285,15 @@ class IStrategy(ABC, HyperStrategyMixin):
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, def custom_sell(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]]:
""" """
Custom sell signal logic indicating that specified position should be sold. Returning a Custom exit 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 string or True from this method is equal to setting exit signal on a candle at specified
time. This method is not called when sell signal is set. time. This method is not called when exit signal is set.
This method should be overridden to create sell signals that depend on trade parameters. For This method should be overridden to create exit signals that depend on trade parameters. For
example you could implement a sell relative to the candle when the trade was opened, example you could implement an exit relative to the candle when the trade was opened,
or a custom 1:2 risk-reward ROI. or a custom 1:2 risk-reward ROI.
Custom sell reason max length is 64. Exceeding characters will be removed. Custom exit reason max length is 64. Exceeding characters will be removed.
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
:param trade: trade object. :param trade: trade object.
@ -299,7 +301,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_rate: Rate, calculated based on pricing settings in ask_strategy. :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 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. :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 :return: To execute exit, return a string with custom sell reason or True. Otherwise return
None or False. None or False.
""" """
return None return None
@ -371,7 +373,7 @@ class IStrategy(ABC, HyperStrategyMixin):
Checks if a pair is currently locked Checks if a pair is currently locked
The 2nd, optional parameter ensures that locks are applied until the new candle arrives, The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap
of 2 seconds for a buy to happen on an old signal. of 2 seconds for a buy/short to happen on an old signal.
:param pair: "Pair to check" :param pair: "Pair to check"
:param candle_date: Date of the last candle. Optional, defaults to current date :param candle_date: Date of the last candle. Optional, defaults to current date
:returns: locking state of the pair in question. :returns: locking state of the pair in question.
@ -387,15 +389,17 @@ class IStrategy(ABC, HyperStrategyMixin):
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Parses the given candle (OHLCV) data and returns a populated DataFrame Parses the given candle (OHLCV) data and returns a populated DataFrame
add several TA indicators and buy signal to it add several TA indicators and buy/short signal to it
:param dataframe: Dataframe containing data from exchange :param dataframe: Dataframe containing data from exchange
:param metadata: Metadata dictionary with additional data (e.g. 'pair') :param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame of candle (OHLCV) data with indicator data and signals added :return: DataFrame of candle (OHLCV) data with indicator data and signals added
""" """
logger.debug("TA Analysis Launched") logger.debug("TA Analysis Launched")
dataframe = self.advise_indicators(dataframe, metadata) dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.advise_buy(dataframe, metadata) dataframe = self.advise_enter(dataframe, metadata, is_short=False)
dataframe = self.advise_sell(dataframe, metadata) dataframe = self.advise_exit(dataframe, metadata, is_short=False)
dataframe = self.advise_enter(dataframe, metadata, is_short=True)
dataframe = self.advise_exit(dataframe, metadata, is_short=True)
return dataframe return dataframe
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@ -422,7 +426,10 @@ class IStrategy(ABC, HyperStrategyMixin):
logger.debug("Skipping TA Analysis for already analyzed candle") logger.debug("Skipping TA Analysis for already analyzed candle")
dataframe['buy'] = 0 dataframe['buy'] = 0
dataframe['sell'] = 0 dataframe['sell'] = 0
dataframe['short'] = 0
dataframe['exit_short'] = 0
dataframe['buy_tag'] = None dataframe['buy_tag'] = None
dataframe['short_tag'] = None
# Other Defs in strategy that want to be called every loop here # Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata) # twitter_sell = self.watch_twitter_feed(dataframe, metadata)
@ -482,6 +489,7 @@ class IStrategy(ABC, HyperStrategyMixin):
if dataframe is None: if dataframe is None:
message = "No dataframe returned (return statement missing?)." message = "No dataframe returned (return statement missing?)."
elif 'buy' not in dataframe: elif 'buy' not in dataframe:
# TODO-lev: Something?
message = "Buy column not set." message = "Buy column not set."
elif df_len != len(dataframe): elif df_len != len(dataframe):
message = message_template.format("length") message = message_template.format("length")
@ -499,15 +507,18 @@ class IStrategy(ABC, HyperStrategyMixin):
self, self,
pair: str, pair: str,
timeframe: str, timeframe: str,
dataframe: DataFrame dataframe: DataFrame,
is_short: bool = False
) -> Tuple[bool, bool, Optional[str]]: ) -> Tuple[bool, bool, Optional[str]]:
""" """
Calculates current signal based based on the buy / sell columns of the dataframe. Calculates current signal based based on the buy/short or sell/exit_short
Used by Bot to get the signal to buy or sell columns of the dataframe.
Used by Bot to get the signal to buy, sell, short, or exit_short
:param pair: pair in format ANT/BTC :param pair: pair in format ANT/BTC
:param timeframe: timeframe to use :param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from. :param dataframe: Analyzed dataframe to get signal from.
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal :return: (Buy, Sell)/(Short, Exit_short) A bool-tuple indicating
(buy/sell)/(short/exit_short) signal
""" """
if not isinstance(dataframe, DataFrame) or dataframe.empty: if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning(f'Empty candle (OHLCV) data for pair {pair}') logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
@ -528,42 +539,49 @@ class IStrategy(ABC, HyperStrategyMixin):
) )
return False, False, None return False, False, None
buy = latest[SignalType.BUY.value] == 1 (enter_type, enter_tag) = (
(SignalType.SHORT, SignalTagType.SHORT_TAG)
if is_short else
(SignalType.BUY, SignalTagType.BUY_TAG)
)
exit_type = SignalType.EXIT_SHORT if is_short else SignalType.SELL
sell = False enter = latest[enter_type.value] == 1
if SignalType.SELL.value in latest:
sell = latest[SignalType.SELL.value] == 1
buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) exit = False
if exit_type.value in latest:
exit = latest[exit_type.value] == 1
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', enter_tag_value = latest.get(enter_tag.value, None)
latest['date'], pair, str(buy), str(sell))
logger.debug(f'trigger: %s (pair=%s) {enter_type.value}=%s {exit_type.value}=%s',
latest['date'], pair, str(enter), str(exit))
timeframe_seconds = timeframe_to_seconds(timeframe) timeframe_seconds = timeframe_to_seconds(timeframe)
if self.ignore_expired_candle(latest_date=latest_date, if self.ignore_expired_candle(latest_date=latest_date,
current_time=datetime.now(timezone.utc), current_time=datetime.now(timezone.utc),
timeframe_seconds=timeframe_seconds, timeframe_seconds=timeframe_seconds,
buy=buy): enter=enter):
return False, sell, buy_tag return False, exit, enter_tag_value
return buy, sell, buy_tag return enter, exit, enter_tag_value
def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, def ignore_expired_candle(self, latest_date: datetime, current_time: datetime,
timeframe_seconds: int, buy: bool): timeframe_seconds: int, enter: bool):
if self.ignore_buying_expired_candle_after and buy: if self.ignore_buying_expired_candle_after and enter:
time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds)) time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds))
return time_delta.total_seconds() > self.ignore_buying_expired_candle_after return time_delta.total_seconds() > self.ignore_buying_expired_candle_after
else: else:
return False return False
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, def should_sell(self, trade: Trade, rate: float, date: datetime, enter: bool,
sell: bool, low: float = None, high: float = None, exit: bool, low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple: force_stoploss: float = 0) -> SellCheckTuple:
""" """
This function evaluates if one of the conditions required to trigger a sell This function evaluates if one of the conditions required to trigger a sell/exit_short
has been reached, which can either be a stop-loss, ROI or sell-signal. has been reached, which can either be a stop-loss, ROI or exit-signal.
:param low: Only used during backtesting to simulate stoploss :param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
:param high: Only used during backtesting, to simulate ROI :param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
:param force_stoploss: Externally provided stoploss :param force_stoploss: Externally provided stoploss
:return: True if trade should be sold, False otherwise :return: True if trade should be exited, False otherwise
""" """
current_rate = rate current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
@ -578,8 +596,8 @@ class IStrategy(ABC, HyperStrategyMixin):
current_rate = high or rate current_rate = high or rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
# if buy signal and ignore_roi is set, we don't need to evaluate min_roi. # if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (buy and self.ignore_roi_if_buy_signal) roi_reached = (not (enter and self.ignore_roi_if_buy_signal)
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date)) current_time=date))
@ -592,10 +610,11 @@ class IStrategy(ABC, HyperStrategyMixin):
if (self.sell_profit_only and current_profit <= self.sell_profit_offset): if (self.sell_profit_only and current_profit <= self.sell_profit_offset):
# sell_profit_only and profit doesn't reach the offset - ignore sell signal # sell_profit_only and profit doesn't reach the offset - ignore sell signal
pass pass
elif self.use_sell_signal and not buy: elif self.use_sell_signal and not enter:
if sell: if exit:
sell_signal = SellType.SELL_SIGNAL sell_signal = SellType.SELL_SIGNAL
else: else:
trade_type = "exit_short" if trade.is_short else "sell"
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate,
current_profit=current_profit) current_profit=current_profit)
@ -603,18 +622,18 @@ class IStrategy(ABC, HyperStrategyMixin):
sell_signal = SellType.CUSTOM_SELL sell_signal = SellType.CUSTOM_SELL
if isinstance(custom_reason, str): if isinstance(custom_reason, str):
if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH:
logger.warning(f'Custom sell reason returned from custom_sell is too ' logger.warning(f'Custom {trade_type} reason returned from '
f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} ' f'custom_{trade_type} is too long and was trimmed'
f'characters.') f'to {CUSTOM_SELL_MAX_LENGTH} characters.')
custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH] custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
else: else:
custom_reason = None custom_reason = None
# TODO: return here if sell-signal should be favored over ROI # TODO: return here if exit-signal should be favored over ROI
# Start evaluations # Start evaluations
# Sequence: # Sequence:
# ROI (if not stoploss) # ROI (if not stoploss)
# Sell-signal # Exit-signal
# Stoploss # Stoploss
if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI")
@ -632,7 +651,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return stoplossflag return stoplossflag
# This one is noisy, commented out... # This one is noisy, commented out...
# logger.debug(f"{trade.pair} - No sell signal.") # logger.debug(f"{trade.pair} - No exit signal.")
return SellCheckTuple(sell_type=SellType.NONE) return SellCheckTuple(sell_type=SellType.NONE)
def stop_loss_reached(self, current_rate: float, trade: Trade, def stop_loss_reached(self, current_rate: float, trade: Trade,
@ -641,7 +660,7 @@ class IStrategy(ABC, HyperStrategyMixin):
high: float = None) -> SellCheckTuple: high: float = None) -> SellCheckTuple:
""" """
Based on current profit of the trade and configured (trailing) stoploss, Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not decides to exit or not
: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
@ -651,7 +670,12 @@ class IStrategy(ABC, HyperStrategyMixin):
# Initiate stoploss with open_rate. Does nothing if stoploss is already set. # Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
if self.use_custom_stoploss and trade.stop_loss < (low or current_rate): dir_correct = (
trade.stop_loss < (low or current_rate) and not trade.is_short or
trade.stop_loss > (low or current_rate) and trade.is_short
)
if self.use_custom_stoploss and dir_correct:
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
)(pair=trade.pair, trade=trade, )(pair=trade.pair, trade=trade,
current_time=current_time, current_time=current_time,
@ -735,7 +759,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
""" """
Populates indicators for given candle (OHLCV) data (for multiple pairs) Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advise_buy or advise_sell! Does not run advise_enter or advise_exit!
Used by optimize operations only, not during dry / live runs. Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run. Using .copy() to get a fresh copy of the dataframe for every strategy run.
Has positive effects on memory usage for whatever reason - also when Has positive effects on memory usage for whatever reason - also when
@ -746,7 +770,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Populate indicators that will be used in the Buy and Sell strategy Populate indicators that will be used in the Buy, Sell, short, exit_short strategy
This method should not be overridden. This method should not be overridden.
:param dataframe: Dataframe with data from the exchange :param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
@ -760,37 +784,60 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return self.populate_indicators(dataframe, metadata) return self.populate_indicators(dataframe, metadata)
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def advise_enter(
self,
dataframe: DataFrame,
metadata: dict,
is_short: bool = False
) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy/short signal for the given dataframe
This method should not be overridden. This method should not be overridden.
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information dictionary, with details like the :param metadata: Additional information dictionary, with details like the
currently traded pair currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.") (type, fun_len) = (
("short", self._short_fun_len)
if is_short else
("buy", self._buy_fun_len)
)
if self._buy_fun_len == 2: logger.debug(f"Populating {type} signals for pair {metadata.get('pair')}.")
if fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see " warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning) "the current function headers!", DeprecationWarning)
return self.populate_buy_trend(dataframe) # type: ignore return self.populate_enter_trend(dataframe) # type: ignore
else: else:
return self.populate_buy_trend(dataframe, metadata) return self.populate_enter_trend(dataframe, metadata)
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def advise_exit(
self,
dataframe: DataFrame,
metadata: dict,
is_short: bool = False
) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell/exit_short signal for the given dataframe
This method should not be overridden. This method should not be overridden.
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information dictionary, with details like the :param metadata: Additional information dictionary, with details like the
currently traded pair currently traded pair
:return: DataFrame with sell column :return: DataFrame with sell column
""" """
logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.")
if self._sell_fun_len == 2: (type, fun_len) = (
("exit_short", self._exit_short_fun_len)
if is_short else
("sell", self._sell_fun_len)
)
logger.debug(f"Populating {type} signals for pair {metadata.get('pair')}.")
if fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see " warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning) "the current function headers!", DeprecationWarning)
return self.populate_sell_trend(dataframe) # type: ignore return self.populate_exit_trend(dataframe) # type: ignore
else: else:
return self.populate_sell_trend(dataframe, metadata) return self.populate_exit_trend(dataframe, metadata)

View File

@ -58,7 +58,11 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
return dataframe return dataframe
def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: def stoploss_from_open(
open_relative_stop: float,
current_profit: float,
for_short: bool = False
) -> float:
""" """
Given the current profit, and a desired stop loss value relative to the open price, Given the current profit, and a desired stop loss value relative to the open price,
@ -72,14 +76,17 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa
:param open_relative_stop: Desired stop loss percentage relative to open price :param open_relative_stop: Desired stop loss percentage relative to open price
:param current_profit: The current profit percentage :param current_profit: The current profit percentage
:return: Positive stop loss value relative to current price :return: Stop loss value relative to current price
""" """
# formula is undefined for current_profit -1, return maximum value # formula is undefined for current_profit -1, return maximum value
if current_profit == -1: if current_profit == -1:
return 1 return 1
stoploss = 1-((1+open_relative_stop)/(1+current_profit)) stoploss = 1-((1+open_relative_stop)/(1+current_profit)) # TODO-lev: Is this right?
# negative stoploss values indicate the requested stop price is higher than the current price # negative stoploss values indicate the requested stop price is higher than the current price
return max(stoploss, 0.0) if for_short:
return min(stoploss, 0.0)
else:
return max(stoploss, 0.0)

View File

@ -172,3 +172,125 @@ class SampleHyperOpt(IHyperOpt):
return dataframe return dataframe
return populate_sell_trend return populate_sell_trend
@staticmethod
def short_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the short strategy parameters to be used by Hyperopt.
"""
def populate_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Buy strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
if 'mfi-enabled' in params and params['mfi-enabled']:
conditions.append(dataframe['mfi'] > params['mfi-value'])
if 'fastd-enabled' in params and params['fastd-enabled']:
conditions.append(dataframe['fastd'] > params['fastd-value'])
if 'adx-enabled' in params and params['adx-enabled']:
conditions.append(dataframe['adx'] < params['adx-value'])
if 'rsi-enabled' in params and params['rsi-enabled']:
conditions.append(dataframe['rsi'] > params['rsi-value'])
# TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'bb_upper':
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_below(
dataframe['macd'], dataframe['macdsignal']
))
if params['trigger'] == 'sar_reversal':
conditions.append(qtpylib.crossed_below(
dataframe['close'], dataframe['sar']
))
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'short'] = 1
return dataframe
return populate_short_trend
@staticmethod
def short_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching short strategy parameters.
"""
return [
Integer(75, 90, name='mfi-value'),
Integer(55, 85, name='fastd-value'),
Integer(50, 80, name='adx-value'),
Integer(60, 80, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_upper', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod
def exit_short_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the exit_short strategy parameters to be used by Hyperopt.
"""
def populate_exit_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Exit_short strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
if 'exit-short-mfi-enabled' in params and params['exit-short-mfi-enabled']:
conditions.append(dataframe['mfi'] < params['exit-short-mfi-value'])
if 'exit-short-fastd-enabled' in params and params['exit-short-fastd-enabled']:
conditions.append(dataframe['fastd'] < params['exit-short-fastd-value'])
if 'exit-short-adx-enabled' in params and params['exit-short-adx-enabled']:
conditions.append(dataframe['adx'] > params['exit-short-adx-value'])
if 'exit-short-rsi-enabled' in params and params['exit-short-rsi-enabled']:
conditions.append(dataframe['rsi'] < params['exit-short-rsi-value'])
# TRIGGERS
if 'exit-short-trigger' in params:
if params['exit-short-trigger'] == 'exit-short-bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['exit-short-trigger'] == 'exit-short-macd_cross_signal':
conditions.append(qtpylib.crossed_below(
dataframe['macdsignal'], dataframe['macd']
))
if params['exit-short-trigger'] == 'exit-short-sar_reversal':
conditions.append(qtpylib.crossed_below(
dataframe['sar'], dataframe['close']
))
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'exit_short'] = 1
return dataframe
return populate_exit_short_trend
@staticmethod
def exit_short_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching exit short strategy parameters.
"""
return [
Integer(1, 25, name='exit_short-mfi-value'),
Integer(1, 50, name='exit_short-fastd-value'),
Integer(1, 50, name='exit_short-adx-value'),
Integer(1, 40, name='exit_short-rsi-value'),
Categorical([True, False], name='exit_short-mfi-enabled'),
Categorical([True, False], name='exit_short-fastd-enabled'),
Categorical([True, False], name='exit_short-adx-enabled'),
Categorical([True, False], name='exit_short-rsi-enabled'),
Categorical(['exit_short-bb_lower',
'exit_short-macd_cross_signal',
'exit_short-sar_reversal'], name='exit_short-trigger')
]

View File

@ -187,9 +187,132 @@ class AdvancedSampleHyperOpt(IHyperOpt):
return populate_sell_trend return populate_sell_trend
@staticmethod
def short_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the short strategy parameters to be used by Hyperopt.
"""
def populate_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Buy strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
if 'mfi-enabled' in params and params['mfi-enabled']:
conditions.append(dataframe['mfi'] > params['mfi-value'])
if 'fastd-enabled' in params and params['fastd-enabled']:
conditions.append(dataframe['fastd'] > params['fastd-value'])
if 'adx-enabled' in params and params['adx-enabled']:
conditions.append(dataframe['adx'] < params['adx-value'])
if 'rsi-enabled' in params and params['rsi-enabled']:
conditions.append(dataframe['rsi'] > params['rsi-value'])
# TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'bb_upper':
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_below(
dataframe['macd'], dataframe['macdsignal']
))
if params['trigger'] == 'sar_reversal':
conditions.append(qtpylib.crossed_below(
dataframe['close'], dataframe['sar']
))
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'short'] = 1
return dataframe
return populate_short_trend
@staticmethod
def short_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching short strategy parameters.
"""
return [
Integer(75, 90, name='mfi-value'),
Integer(55, 85, name='fastd-value'),
Integer(50, 80, name='adx-value'),
Integer(60, 80, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_upper', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod
def exit_short_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the exit_short strategy parameters to be used by Hyperopt.
"""
def populate_exit_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Exit_short strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
if 'exit-short-mfi-enabled' in params and params['exit-short-mfi-enabled']:
conditions.append(dataframe['mfi'] < params['exit-short-mfi-value'])
if 'exit-short-fastd-enabled' in params and params['exit-short-fastd-enabled']:
conditions.append(dataframe['fastd'] < params['exit-short-fastd-value'])
if 'exit-short-adx-enabled' in params and params['exit-short-adx-enabled']:
conditions.append(dataframe['adx'] > params['exit-short-adx-value'])
if 'exit-short-rsi-enabled' in params and params['exit-short-rsi-enabled']:
conditions.append(dataframe['rsi'] < params['exit-short-rsi-value'])
# TRIGGERS
if 'exit-short-trigger' in params:
if params['exit-short-trigger'] == 'exit-short-bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['exit-short-trigger'] == 'exit-short-macd_cross_signal':
conditions.append(qtpylib.crossed_below(
dataframe['macdsignal'], dataframe['macd']
))
if params['exit-short-trigger'] == 'exit-short-sar_reversal':
conditions.append(qtpylib.crossed_below(
dataframe['sar'], dataframe['close']
))
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'exit_short'] = 1
return dataframe
return populate_exit_short_trend
@staticmethod
def exit_short_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching exit short strategy parameters.
"""
return [
Integer(1, 25, name='exit_short-mfi-value'),
Integer(1, 50, name='exit_short-fastd-value'),
Integer(1, 50, name='exit_short-adx-value'),
Integer(1, 40, name='exit_short-rsi-value'),
Categorical([True, False], name='exit_short-mfi-enabled'),
Categorical([True, False], name='exit_short-fastd-enabled'),
Categorical([True, False], name='exit_short-adx-enabled'),
Categorical([True, False], name='exit_short-rsi-enabled'),
Categorical(['exit_short-bb_lower',
'exit_short-macd_cross_signal',
'exit_short-sar_reversal'], name='exit_short-trigger')
]
@staticmethod @staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:
""" """
# TODO-lev?
Generate the ROI table that will be used by Hyperopt Generate the ROI table that will be used by Hyperopt
This implementation generates the default legacy Freqtrade ROI tables. This implementation generates the default legacy Freqtrade ROI tables.
@ -211,6 +334,7 @@ class AdvancedSampleHyperOpt(IHyperOpt):
@staticmethod @staticmethod
def roi_space() -> List[Dimension]: def roi_space() -> List[Dimension]:
""" """
# TODO-lev?
Values to search for each ROI steps Values to search for each ROI steps
Override it if you need some different ranges for the parameters in the Override it if you need some different ranges for the parameters in the
@ -231,6 +355,7 @@ class AdvancedSampleHyperOpt(IHyperOpt):
@staticmethod @staticmethod
def stoploss_space() -> List[Dimension]: def stoploss_space() -> List[Dimension]:
""" """
# TODO-lev?
Stoploss Value to search Stoploss Value to search
Override it if you need some different range for the parameter in the Override it if you need some different range for the parameter in the
@ -243,6 +368,7 @@ class AdvancedSampleHyperOpt(IHyperOpt):
@staticmethod @staticmethod
def trailing_space() -> List[Dimension]: def trailing_space() -> List[Dimension]:
""" """
# TODO-lev?
Create a trailing stoploss space. Create a trailing stoploss space.
You may override it in your custom Hyperopt class. You may override it in your custom Hyperopt class.

View File

@ -29,7 +29,7 @@ class SampleStrategy(IStrategy):
You must keep: You must keep:
- the lib in the section "Do not remove these libs" - the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_buy_trend, populate_sell_trend - the methods: populate_indicators, populate_buy_trend, populate_sell_trend, populate_short_trend, populate_exit_short_trend
You should keep: You should keep:
- timeframe, minimal_roi, stoploss, trailing_* - timeframe, minimal_roi, stoploss, trailing_*
""" """
@ -58,6 +58,8 @@ class SampleStrategy(IStrategy):
# Hyperoptable parameters # Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True)
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
# Optimal timeframe for the strategy. # Optimal timeframe for the strategy.
timeframe = '5m' timeframe = '5m'
@ -373,3 +375,40 @@ class SampleStrategy(IStrategy):
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
def populate_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the short signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with short column
"""
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) &
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'short'] = 1
return dataframe
def populate_exit_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit_short signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit_short column
"""
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) &
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_short'] = 1
return dataframe

View File

@ -105,6 +105,66 @@ class DefaultHyperOpt(IHyperOpt):
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
] ]
@staticmethod
def short_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the short strategy parameters to be used by Hyperopt.
"""
def populate_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Buy strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
if 'mfi-enabled' in params and params['mfi-enabled']:
conditions.append(dataframe['mfi'] > params['mfi-value'])
if 'fastd-enabled' in params and params['fastd-enabled']:
conditions.append(dataframe['fastd'] > params['fastd-value'])
if 'adx-enabled' in params and params['adx-enabled']:
conditions.append(dataframe['adx'] < params['adx-value'])
if 'rsi-enabled' in params and params['rsi-enabled']:
conditions.append(dataframe['rsi'] > params['rsi-value'])
# TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'bb_upper':
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_below(
dataframe['macd'], dataframe['macdsignal']
))
if params['trigger'] == 'sar_reversal':
conditions.append(qtpylib.crossed_below(
dataframe['close'], dataframe['sar']
))
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'short'] = 1
return dataframe
return populate_short_trend
@staticmethod
def short_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching short strategy parameters.
"""
return [
Integer(75, 90, name='mfi-value'),
Integer(55, 85, name='fastd-value'),
Integer(50, 80, name='adx-value'),
Integer(60, 80, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_upper', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod @staticmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable: def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
@ -148,6 +208,49 @@ class DefaultHyperOpt(IHyperOpt):
return populate_sell_trend return populate_sell_trend
@staticmethod
def exit_short_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the exit_short strategy parameters to be used by Hyperopt.
"""
def populate_exit_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Exit_short strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
if 'exit-short-mfi-enabled' in params and params['exit-short-mfi-enabled']:
conditions.append(dataframe['mfi'] < params['exit-short-mfi-value'])
if 'exit-short-fastd-enabled' in params and params['exit-short-fastd-enabled']:
conditions.append(dataframe['fastd'] < params['exit-short-fastd-value'])
if 'exit-short-adx-enabled' in params and params['exit-short-adx-enabled']:
conditions.append(dataframe['adx'] > params['exit-short-adx-value'])
if 'exit-short-rsi-enabled' in params and params['exit-short-rsi-enabled']:
conditions.append(dataframe['rsi'] < params['exit-short-rsi-value'])
# TRIGGERS
if 'exit-short-trigger' in params:
if params['exit-short-trigger'] == 'exit-short-bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['exit-short-trigger'] == 'exit-short-macd_cross_signal':
conditions.append(qtpylib.crossed_below(
dataframe['macdsignal'], dataframe['macd']
))
if params['exit-short-trigger'] == 'exit-short-sar_reversal':
conditions.append(qtpylib.crossed_below(
dataframe['sar'], dataframe['close']
))
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'exit_short'] = 1
return dataframe
return populate_exit_short_trend
@staticmethod @staticmethod
def sell_indicator_space() -> List[Dimension]: def sell_indicator_space() -> List[Dimension]:
""" """
@ -167,6 +270,25 @@ class DefaultHyperOpt(IHyperOpt):
'sell-sar_reversal'], name='sell-trigger') 'sell-sar_reversal'], name='sell-trigger')
] ]
@staticmethod
def exit_short_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching exit short strategy parameters.
"""
return [
Integer(1, 25, name='exit_short-mfi-value'),
Integer(1, 50, name='exit_short-fastd-value'),
Integer(1, 50, name='exit_short-adx-value'),
Integer(1, 40, name='exit_short-rsi-value'),
Categorical([True, False], name='exit_short-mfi-enabled'),
Categorical([True, False], name='exit_short-fastd-enabled'),
Categorical([True, False], name='exit_short-adx-enabled'),
Categorical([True, False], name='exit_short-rsi-enabled'),
Categorical(['exit_short-bb_lower',
'exit_short-macd_cross_signal',
'exit_short-sar_reversal'], name='exit_short-trigger')
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators. Should be a copy of same method from strategy. Based on TA indicators. Should be a copy of same method from strategy.
@ -200,3 +322,37 @@ class DefaultHyperOpt(IHyperOpt):
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
def populate_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include short space.
"""
dataframe.loc[
(
(dataframe['close'] > dataframe['bb_upperband']) &
(dataframe['mfi'] < 84) &
(dataframe['adx'] > 75) &
(dataframe['rsi'] < 79)
),
'buy'] = 1
return dataframe
def populate_exit_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include exit_short space.
"""
dataframe.loc[
(
(qtpylib.crossed_below(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] < 46)
),
'sell'] = 1
return dataframe

View File

@ -597,8 +597,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.required_startup = 0 backtesting.required_startup = 0
backtesting.strategy.advise_buy = lambda a, m: frame backtesting.strategy.advise_enter = lambda a, m: frame
backtesting.strategy.advise_sell = lambda a, m: frame backtesting.strategy.advise_exit = lambda a, m: frame
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)

View File

@ -290,8 +290,8 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert backtesting.timeframe == '5m' assert backtesting.timeframe == '5m'
assert callable(backtesting.strategy.ohlcvdata_to_dataframe) assert callable(backtesting.strategy.ohlcvdata_to_dataframe)
assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_enter)
assert callable(backtesting.strategy.advise_sell) assert callable(backtesting.strategy.advise_exit)
assert isinstance(backtesting.strategy.dp, DataProvider) assert isinstance(backtesting.strategy.dp, DataProvider)
get_fee.assert_called() get_fee.assert_called()
assert backtesting.fee == 0.5 assert backtesting.fee == 0.5
@ -700,8 +700,8 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_enter = fun # Override
backtesting.strategy.advise_sell = fun # Override backtesting.strategy.advise_exit = fun # Override
result = backtesting.backtest(**backtest_conf) result = backtesting.backtest(**backtest_conf)
assert result['results'].empty assert result['results'].empty
@ -716,8 +716,8 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_enter = fun # Override
backtesting.strategy.advise_sell = fun # Override backtesting.strategy.advise_exit = fun # Override
result = backtesting.backtest(**backtest_conf) result = backtesting.backtest(**backtest_conf)
assert result['results'].empty assert result['results'].empty
@ -731,8 +731,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.required_startup = 0 backtesting.required_startup = 0
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_enter = _trend_alternate # Override
backtesting.strategy.advise_sell = _trend_alternate # Override backtesting.strategy.advise_exit = _trend_alternate # Override
result = backtesting.backtest(**backtest_conf) result = backtesting.backtest(**backtest_conf)
# 200 candles in backtest data # 200 candles in backtest data
# won't buy on first (shifted by 1) # won't buy on first (shifted by 1)
@ -777,8 +777,8 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = _trend_alternate_hold # Override backtesting.strategy.advise_enter = _trend_alternate_hold # Override
backtesting.strategy.advise_sell = _trend_alternate_hold # Override backtesting.strategy.advise_exit = _trend_alternate_hold # Override
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)

View File

@ -25,6 +25,9 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
from .hyperopts.default_hyperopt import DefaultHyperOpt from .hyperopts.default_hyperopt import DefaultHyperOpt
# TODO-lev: This file
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -363,8 +366,8 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
# Should be called for historical candle data # Should be called for historical candle data
assert dumper.call_count == 1 assert dumper.call_count == 1
assert dumper2.call_count == 1 assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_enter")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")
@ -822,8 +825,8 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
assert dumper.call_count == 1 assert dumper.call_count == 1
assert dumper2.call_count == 1 assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_enter")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")
@ -903,8 +906,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
assert dumper.called assert dumper.called
assert dumper.call_count == 1 assert dumper.call_count == 1
assert dumper2.call_count == 1 assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_enter")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")
@ -957,8 +960,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
assert dumper.called assert dumper.called
assert dumper.call_count == 1 assert dumper.call_count == 1
assert dumper2.call_count == 1 assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_enter")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")

View File

@ -264,7 +264,7 @@ def test_api_UvicornServer(mocker):
assert thread_mock.call_count == 1 assert thread_mock.call_count == 1
s.cleanup() s.cleanup()
assert s.should_exit is True assert s.should_sell is True
def test_api_UvicornServer_run(mocker): def test_api_UvicornServer_run(mocker):

View File

@ -154,3 +154,48 @@ class DefaultStrategy(IStrategy):
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
def populate_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the short signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with short column
"""
dataframe.loc[
(
(dataframe['rsi'] > 65) &
(dataframe['fastd'] > 65) &
(dataframe['adx'] < 70) &
(dataframe['plus_di'] < 0.5) # TODO-lev: What to do here
) |
(
(dataframe['adx'] < 35) &
(dataframe['plus_di'] < 0.5) # TODO-lev: What to do here
),
'short'] = 1
return dataframe
def populate_exit_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit_short signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit_short column
"""
dataframe.loc[
(
(
(qtpylib.crossed_below(dataframe['rsi'], 30)) |
(qtpylib.crossed_below(dataframe['fastd'], 30))
) &
(dataframe['adx'] < 90) &
(dataframe['minus_di'] < 0) # TODO-lev: what to do here
) |
(
(dataframe['adx'] > 30) &
(dataframe['minus_di'] < 0.5) # TODO-lev: what to do here
),
'exit_short'] = 1
return dataframe

View File

@ -60,6 +60,15 @@ class HyperoptableStrategy(IStrategy):
'sell_minusdi': 0.4 'sell_minusdi': 0.4
} }
short_params = {
'short_rsi': 65,
}
exit_short_params = {
'exit_short_rsi': 26,
'exit_short_minusdi': 0.6
}
buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_rsi = IntParameter([0, 50], default=30, space='buy')
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
@ -78,6 +87,12 @@ class HyperoptableStrategy(IStrategy):
}) })
return prot return prot
short_rsi = IntParameter([50, 100], default=70, space='sell')
short_plusdi = RealParameter(low=0, high=1, default=0.5, space='sell')
exit_short_rsi = IntParameter(low=0, high=50, default=30, space='buy')
exit_short_minusdi = DecimalParameter(low=0, high=1, default=0.4999, decimals=3, space='buy',
load=False)
def informative_pairs(self): def informative_pairs(self):
""" """
Define additional, informative pair/interval combinations to be cached from the exchange. Define additional, informative pair/interval combinations to be cached from the exchange.
@ -167,7 +182,7 @@ class HyperoptableStrategy(IStrategy):
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with sell column
""" """
dataframe.loc[ dataframe.loc[
( (
@ -184,3 +199,48 @@ class HyperoptableStrategy(IStrategy):
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
def populate_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the short signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with short column
"""
dataframe.loc[
(
(dataframe['rsi'] > self.short_rsi.value) &
(dataframe['fastd'] > 65) &
(dataframe['adx'] < 70) &
(dataframe['plus_di'] < self.short_plusdi.value)
) |
(
(dataframe['adx'] < 35) &
(dataframe['plus_di'] < self.short_plusdi.value)
),
'short'] = 1
return dataframe
def populate_exit_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit_short signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit_short column
"""
dataframe.loc[
(
(
(qtpylib.crossed_below(dataframe['rsi'], self.exit_short_rsi.value)) |
(qtpylib.crossed_below(dataframe['fastd'], 30))
) &
(dataframe['adx'] < 90) &
(dataframe['minus_di'] < 0) # TODO-lev: What should this be
) |
(
(dataframe['adx'] < 30) &
(dataframe['minus_di'] < self.exit_short_minusdi.value)
),
'exit_short'] = 1
return dataframe

View File

@ -85,3 +85,34 @@ class TestStrategyLegacy(IStrategy):
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
def populate_short_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 30) &
(dataframe['tema'] > dataframe['tema'].shift(1)) &
(dataframe['volume'] > 0)
),
'buy'] = 1
return dataframe
def populate_exit_short_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 70) &
(dataframe['tema'] < dataframe['tema'].shift(1)) &
(dataframe['volume'] > 0)
),
'sell'] = 1
return dataframe

View File

@ -14,6 +14,8 @@ def test_default_strategy_structure():
assert hasattr(DefaultStrategy, 'populate_indicators') assert hasattr(DefaultStrategy, 'populate_indicators')
assert hasattr(DefaultStrategy, 'populate_buy_trend') assert hasattr(DefaultStrategy, 'populate_buy_trend')
assert hasattr(DefaultStrategy, 'populate_sell_trend') assert hasattr(DefaultStrategy, 'populate_sell_trend')
assert hasattr(DefaultStrategy, 'populate_short_trend')
assert hasattr(DefaultStrategy, 'populate_exit_short_trend')
def test_default_strategy(result, fee): def test_default_strategy(result, fee):
@ -27,6 +29,10 @@ def test_default_strategy(result, fee):
assert type(indicators) is DataFrame assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
# TODO-lev: I think these two should be commented out in the strategy by default
# TODO-lev: so they can be tested, but the tests can't really remain
assert type(strategy.populate_short_trend(indicators, metadata)) is DataFrame
assert type(strategy.populate_exit_short_trend(indicators, metadata)) is DataFrame
trade = Trade( trade = Trade(
open_rate=19_000, open_rate=19_000,
@ -37,10 +43,28 @@ def test_default_strategy(result, fee):
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', rate=20000, time_in_force='gtc',
current_time=datetime.utcnow()) is True is_short=False, current_time=datetime.utcnow()) is True
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', sell_reason='roi', rate=20000, time_in_force='gtc', sell_reason='roi',
current_time=datetime.utcnow()) is True is_short=False, current_time=datetime.utcnow()) is True
# TODO-lev: Test for shorts?
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) == strategy.stoploss
short_trade = Trade(
open_rate=21_000,
amount=0.1,
pair='ETH/BTC',
fee_open=fee.return_value
)
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc',
is_short=True, current_time=datetime.utcnow()) is True
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=short_trade, order_type='limit',
amount=0.1, rate=20000, time_in_force='gtc',
sell_reason='roi', is_short=True,
current_time=datetime.utcnow()) is True

View File

@ -156,17 +156,21 @@ def test_ignore_expired_candle(default_conf):
# Add 1 candle length as the "latest date" defines candle open. # Add 1 candle length as the "latest date" defines candle open.
current_time = latest_date + timedelta(seconds=80 + 300) current_time = latest_date + timedelta(seconds=80 + 300)
assert strategy.ignore_expired_candle(latest_date=latest_date, assert strategy.ignore_expired_candle(
current_time=current_time, latest_date=latest_date,
timeframe_seconds=300, current_time=current_time,
buy=True) is True timeframe_seconds=300,
enter=True
) is True
current_time = latest_date + timedelta(seconds=30 + 300) current_time = latest_date + timedelta(seconds=30 + 300)
assert not strategy.ignore_expired_candle(latest_date=latest_date, assert not strategy.ignore_expired_candle(
current_time=current_time, latest_date=latest_date,
timeframe_seconds=300, current_time=current_time,
buy=True) is True timeframe_seconds=300,
enter=True
) is True
def test_assert_df_raise(mocker, caplog, ohlcv_history): def test_assert_df_raise(mocker, caplog, ohlcv_history):
@ -478,20 +482,20 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x) enter_mock = MagicMock(side_effect=lambda x, meta, is_short: x)
sell_mock = MagicMock(side_effect=lambda x, meta: x) exit_mock = MagicMock(side_effect=lambda x, meta, is_short: x)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.strategy.interface.IStrategy', 'freqtrade.strategy.interface.IStrategy',
advise_indicators=ind_mock, advise_indicators=ind_mock,
advise_buy=buy_mock, advise_enter=enter_mock,
advise_sell=sell_mock, advise_exit=exit_mock,
) )
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert enter_mock.call_count == 2
assert buy_mock.call_count == 1 assert enter_mock.call_count == 2
assert log_has('TA Analysis Launched', caplog) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
@ -500,8 +504,8 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true # No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 2 assert ind_mock.call_count == 2
assert buy_mock.call_count == 2 assert enter_mock.call_count == 4
assert buy_mock.call_count == 2 assert enter_mock.call_count == 4
assert log_has('TA Analysis Launched', caplog) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
@ -509,13 +513,13 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None: def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x) enter_mock = MagicMock(side_effect=lambda x, meta, is_short: x)
sell_mock = MagicMock(side_effect=lambda x, meta: x) exit_mock = MagicMock(side_effect=lambda x, meta, is_short: x)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.strategy.interface.IStrategy', 'freqtrade.strategy.interface.IStrategy',
advise_indicators=ind_mock, advise_indicators=ind_mock,
advise_buy=buy_mock, advise_enter=enter_mock,
advise_sell=sell_mock, advise_exit=exit_mock,
) )
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
@ -528,8 +532,8 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
assert 'close' in ret.columns assert 'close' in ret.columns
assert isinstance(ret, DataFrame) assert isinstance(ret, DataFrame)
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert enter_mock.call_count == 2 # Once for buy, once for short
assert buy_mock.call_count == 1 assert enter_mock.call_count == 2
assert log_has('TA Analysis Launched', caplog) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.clear() caplog.clear()
@ -537,8 +541,8 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true # No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert enter_mock.call_count == 2
assert buy_mock.call_count == 1 assert enter_mock.call_count == 2
# only skipped analyze adds buy and sell columns, otherwise it's all mocked # only skipped analyze adds buy and sell columns, otherwise it's all mocked
assert 'buy' in ret.columns assert 'buy' in ret.columns
assert 'sell' in ret.columns assert 'sell' in ret.columns
@ -743,10 +747,10 @@ def test_auto_hyperopt_interface(default_conf):
assert strategy.sell_minusdi.value == 0.5 assert strategy.sell_minusdi.value == 0.5
all_params = strategy.detect_all_parameters() all_params = strategy.detect_all_parameters()
assert isinstance(all_params, dict) assert isinstance(all_params, dict)
assert len(all_params['buy']) == 2 # TODO-lev: Should these be 4,4 and 10?
assert len(all_params['sell']) == 2 assert len(all_params['buy']) == 4
# Number of Hyperoptable parameters assert len(all_params['sell']) == 4
assert all_params['count'] == 6 assert all_params['count'] == 10
strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy') strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy')

View File

@ -117,12 +117,18 @@ def test_strategy(result, default_conf):
df_indicators = strategy.advise_indicators(result, metadata=metadata) df_indicators = strategy.advise_indicators(result, metadata=metadata)
assert 'adx' in df_indicators assert 'adx' in df_indicators
dataframe = strategy.advise_buy(df_indicators, metadata=metadata) dataframe = strategy.advise_enter(df_indicators, metadata=metadata, is_short=False)
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
dataframe = strategy.advise_sell(df_indicators, metadata=metadata) dataframe = strategy.advise_exit(df_indicators, metadata=metadata, is_short=False)
assert 'sell' in dataframe.columns assert 'sell' in dataframe.columns
dataframe = strategy.advise_enter(df_indicators, metadata=metadata, is_short=True)
assert 'short' in dataframe.columns
dataframe = strategy.advise_exit(df_indicators, metadata=metadata, is_short=True)
assert 'exit_short' in dataframe.columns
def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
@ -218,6 +224,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf):
def test_strategy_override_order_types(caplog, default_conf): def test_strategy_override_order_types(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
# TODO-lev: Maybe change
order_types = { order_types = {
'buy': 'market', 'buy': 'market',
'sell': 'limit', 'sell': 'limit',
@ -345,7 +352,7 @@ def test_deprecate_populate_indicators(result, default_conf):
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) strategy.advise_enter(indicators, {'pair': 'ETH/BTC'}, is_short=False) # TODO-lev
assert len(w) == 1 assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning) assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \ assert "deprecated - check out the Sample strategy to see the current function headers!" \
@ -354,7 +361,7 @@ def test_deprecate_populate_indicators(result, default_conf):
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) strategy.advise_exit(indicators, {'pair': 'ETH_BTC'}, is_short=False) # TODO-lev
assert len(w) == 1 assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning) assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \ assert "deprecated - check out the Sample strategy to see the current function headers!" \
@ -374,6 +381,8 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog):
assert strategy._populate_fun_len == 2 assert strategy._populate_fun_len == 2
assert strategy._buy_fun_len == 2 assert strategy._buy_fun_len == 2
assert strategy._sell_fun_len == 2 assert strategy._sell_fun_len == 2
# assert strategy._short_fun_len == 2
# assert strategy._exit_short_fun_len == 2
assert strategy.INTERFACE_VERSION == 1 assert strategy.INTERFACE_VERSION == 1
assert strategy.timeframe == '5m' assert strategy.timeframe == '5m'
assert strategy.ticker_interval == '5m' assert strategy.ticker_interval == '5m'
@ -382,14 +391,22 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog):
assert isinstance(indicator_df, DataFrame) assert isinstance(indicator_df, DataFrame)
assert 'adx' in indicator_df.columns assert 'adx' in indicator_df.columns
buydf = strategy.advise_buy(result, metadata=metadata) buydf = strategy.advise_enter(result, metadata=metadata, is_short=False)
assert isinstance(buydf, DataFrame) assert isinstance(buydf, DataFrame)
assert 'buy' in buydf.columns assert 'buy' in buydf.columns
selldf = strategy.advise_sell(result, metadata=metadata) selldf = strategy.advise_exit(result, metadata=metadata, is_short=False)
assert isinstance(selldf, DataFrame) assert isinstance(selldf, DataFrame)
assert 'sell' in selldf assert 'sell' in selldf
# shortdf = strategy.advise_enter(result, metadata=metadata, is_short=True)
# assert isinstance(shortdf, DataFrame)
# assert 'short' in shortdf.columns
# exit_shortdf = strategy.advise_exit(result, metadata=metadata, is_short=True)
# assert isinstance(exit_shortdf, DataFrame)
# assert 'exit_short' in exit_shortdf
assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.", assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.",
caplog) caplog)
@ -403,16 +420,26 @@ def test_strategy_interface_versioning(result, monkeypatch, default_conf):
assert strategy._populate_fun_len == 3 assert strategy._populate_fun_len == 3
assert strategy._buy_fun_len == 3 assert strategy._buy_fun_len == 3
assert strategy._sell_fun_len == 3 assert strategy._sell_fun_len == 3
assert strategy._short_fun_len == 3
assert strategy._exit_short_fun_len == 3
assert strategy.INTERFACE_VERSION == 2 assert strategy.INTERFACE_VERSION == 2
indicator_df = strategy.advise_indicators(result, metadata=metadata) indicator_df = strategy.advise_indicators(result, metadata=metadata)
assert isinstance(indicator_df, DataFrame) assert isinstance(indicator_df, DataFrame)
assert 'adx' in indicator_df.columns assert 'adx' in indicator_df.columns
buydf = strategy.advise_buy(result, metadata=metadata) buydf = strategy.advise_enter(result, metadata=metadata, is_short=False)
assert isinstance(buydf, DataFrame) assert isinstance(buydf, DataFrame)
assert 'buy' in buydf.columns assert 'buy' in buydf.columns
selldf = strategy.advise_sell(result, metadata=metadata) selldf = strategy.advise_exit(result, metadata=metadata, is_short=False)
assert isinstance(selldf, DataFrame) assert isinstance(selldf, DataFrame)
assert 'sell' in selldf assert 'sell' in selldf
shortdf = strategy.advise_enter(result, metadata=metadata, is_short=True)
assert isinstance(shortdf, DataFrame)
assert 'short' in shortdf.columns
exit_shortdf = strategy.advise_exit(result, metadata=metadata, is_short=True)
assert isinstance(exit_shortdf, DataFrame)
assert 'exit_short' in exit_shortdf