diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 1ad8074d4..70f99e54b 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -42,7 +42,7 @@ docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_I docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM # Run backtest -docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2 +docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index dd6ac841e..fd5f0ef93 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -53,7 +53,7 @@ docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT # Run backtest -docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2 +docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2b9517f3b..dc1e2831a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -539,9 +539,10 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods 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, + side: str, **kwargs) -> bool: """ - Called right before placing a buy order. + Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -549,12 +550,13 @@ class AwesomeStrategy(IStrategy): 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 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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :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 placed on the exchange. False aborts the process @@ -617,7 +619,7 @@ It is possible to manage your risk by reducing or increasing stake amount when p class AwesomeStrategy(IStrategy): def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + side: str, **kwargs) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) current_candle = dataframe.iloc[-1].squeeze() @@ -642,6 +644,34 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an !!! Tip Returning `0` or `None` will prevent trades from being placed. +## Leverage Callback + +When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage). + +Assuming a capital of 500USDT, a trade with leverage=3 would result in a position with 500 x 3 = 1500 USDT. + +Values that are above `max_leverage` will be adjusted to `max_leverage`. +For markets / exchanges that don't support leverage, this method is ignored. + +``` python +class AwesomeStrategy(IStrategy): + def leverage(self, pair: str, current_time: 'datetime', current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + """ + Customize leverage for each new trade. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_leverage: A leverage proposed by the bot. + :param max_leverage: Max leverage allowed on this pair + :param side: 'long' or 'short' - indicating the direction of the proposed trade + :return: A leverage amount, which is between 1.0 and max_leverage. + """ + return 1.0 +``` + --- ## Derived strategies diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 7d97661c4..e8d878838 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -31,6 +31,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag'] +# TODO-lev: usage of the above might need compatibility code (buy_tag, is_short?, ...?) def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 1950f0d08..e08b3df2f 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -159,7 +159,8 @@ class Edge: logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days)..') - headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] + # TODO-lev: Should edge support shorts? needs to be investigated further... + headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long'] trades: list = [] for pair, pair_data in preprocessed.items(): @@ -167,8 +168,13 @@ class Edge: pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - df_analyzed = self.strategy.advise_sell( - self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() + df_analyzed = self.strategy.advise_exit( + dataframe=self.strategy.advise_entry( + dataframe=pair_data, + metadata={'pair': pair} + ), + metadata={'pair': pair} + )[headers].copy() trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range) @@ -382,8 +388,8 @@ class Edge: return final def _find_trades_for_stoploss_range(self, df, pair, stoploss_range): - buy_column = df['buy'].values - sell_column = df['sell'].values + buy_column = df['enter_long'].values + sell_column = df['exit_long'].values date_column = df['date'].values ohlc_columns = df[['open', 'high', 'low', 'close']].values diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 692a7fcb6..e9d166258 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -4,6 +4,6 @@ from freqtrade.enums.collateral import Collateral from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType -from freqtrade.enums.signaltype import SignalTagType, SignalType +from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index fc57e1ce7..fc585318c 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -5,12 +5,19 @@ class SignalType(Enum): """ Enum to distinguish between enter and exit signals """ - BUY = "buy" - SELL = "sell" + ENTER_LONG = "enter_long" + EXIT_LONG = "exit_long" + ENTER_SHORT = "enter_short" + EXIT_SHORT = "exit_short" class SignalTagType(Enum): """ Enum for signal columns """ - BUY_TAG = "buy_tag" + ENTER_TAG = "enter_tag" + + +class SignalDirection(Enum): + LONG = 'long' + SHORT = 'short' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ae4970ea3..32edd8588 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -422,24 +422,25 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (buy, sell, buy_tag) = self.strategy.get_signal( - pair, - self.strategy.timeframe, - analyzed_df - ) + (signal, enter_tag) = self.strategy.get_entry_signal( + pair, self.strategy.timeframe, analyzed_df + ) - if buy and not sell: + if signal: stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): + # TODO-lev: Does the below need to be adjusted for shorts? if self._check_depth_of_market_buy(pair, bid_check_dom): - return self.execute_entry(pair, stake_amount, buy_tag=buy_tag) + # TODO-lev: pass in "enter" as side. + + return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) else: return False - return self.execute_entry(pair, stake_amount, buy_tag=buy_tag) + return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) else: return False @@ -468,7 +469,7 @@ class FreqtradeBot(LoggingMixin): return False def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: + forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -501,7 +502,9 @@ class FreqtradeBot(LoggingMixin): default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), current_rate=enter_limit_requested, proposed_stake=stake_amount, - min_stake=min_stake_amount, max_stake=max_stake_amount) + min_stake=min_stake_amount, max_stake=max_stake_amount, side='long') + # TODO-lev: Add non-hardcoded "side" parameter + stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: @@ -518,9 +521,12 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types.get('forcebuy', order_type) # TODO-lev: Will this work for shorting? + # TODO-lev: Add non-hardcoded "side" parameter if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): + time_in_force=time_in_force, current_time=datetime.now(timezone.utc), + side='long' + ): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) @@ -579,7 +585,8 @@ class FreqtradeBot(LoggingMixin): exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), - buy_tag=buy_tag, + # TODO-lev: compatibility layer for buy_tag (!) + buy_tag=enter_tag, timeframe=timeframe_to_minutes(self.config['timeframe']) ) trade.orders.append(order_obj) @@ -703,22 +710,23 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) - (buy, sell) = (False, False) + (enter, exit_) = (False, False) + # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal if (self.config.get('use_sell_signal', True) or self.config.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( + (enter, exit_) = self.strategy.get_exit_signal( trade.pair, self.strategy.timeframe, - analyzed_df + analyzed_df, is_short=trade.is_short ) - logger.debug('checking sell') + # TODO-lev: side should depend on trade side. exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_exit(trade, exit_rate, buy, sell): + if self._check_and_execute_exit(trade, exit_rate, enter, exit_): return True logger.debug('Found no sell signal for %s.', trade) @@ -864,18 +872,18 @@ class FreqtradeBot(LoggingMixin): f"for pair {trade.pair}.") def _check_and_execute_exit(self, trade: Trade, exit_rate: float, - buy: bool, sell: bool) -> bool: + enter: bool, exit_: bool) -> bool: """ - Check and execute exit + Check and execute trade exit """ - should_sell = self.strategy.should_sell( - trade, exit_rate, datetime.now(timezone.utc), buy, sell, + should_exit: SellCheckTuple = self.strategy.should_exit( + trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_, 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_trade_exit(trade, exit_rate, should_sell) + if should_exit.sell_flag: + logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}') + self.execute_trade_exit(trade, exit_rate, should_exit) return True return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d4964746a..4890c20aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -37,13 +37,15 @@ logger = logging.getLogger(__name__) # Indexes for backtest tuples DATE_IDX = 0 -BUY_IDX = 1 -OPEN_IDX = 2 -CLOSE_IDX = 3 -SELL_IDX = 4 -LOW_IDX = 5 -HIGH_IDX = 6 -BUY_TAG_IDX = 7 +OPEN_IDX = 1 +HIGH_IDX = 2 +LOW_IDX = 3 +CLOSE_IDX = 4 +LONG_IDX = 5 +ELONG_IDX = 6 # Exit long +SHORT_IDX = 7 +ESHORT_IDX = 8 # Exit short +ENTER_TAG_IDX = 9 class Backtesting: @@ -64,8 +66,8 @@ class Backtesting: config['dry_run'] = True self.strategylist: List[IStrategy] = [] self.all_results: Dict[str, Dict] = {} - - self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) + self._exchange_name = self.config['exchange']['name'] + self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config) self.dataprovider = DataProvider(self.config, None) if self.config.get('strategy_list', None): @@ -136,6 +138,10 @@ class Backtesting: self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) + # TODO-lev: This should come from the configuration setting or better a + # TODO-lev: combination of config/strategy "use_shorts"(?) and "can_short" from the exchange + self._can_short = False + self.progress = BTProgress() self.abort = False @@ -245,7 +251,8 @@ class Backtesting: """ # Every change to this headers list must evaluate further usages of the resulting tuple # and eventually change the constants for indexes at the top - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag'] + headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', + 'enter_short', 'exit_short', 'enter_tag'] data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -253,13 +260,13 @@ class Backtesting: for pair, pair_data in processed.items(): self.check_abort() self.progress.increment() - if not pair_data.empty: - pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist - pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist - pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist - df_analyzed = self.strategy.advise_sell( - self.strategy.advise_buy(pair_data, {'pair': pair}), + if not pair_data.empty: + # Cleanup from prior runs + pair_data.drop(headers[5:] + ['buy', 'sell'], axis=1, errors='ignore') + + df_analyzed = self.strategy.advise_exit( + self.strategy.advise_entry(pair_data, {'pair': pair}), {'pair': pair} ).copy() # Trim startup period from analyzed dataframe @@ -267,9 +274,11 @@ class Backtesting: startup_candles=self.required_startup) # To avoid using data from future, we use buy/sell signals shifted # from the previous candle - df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) - df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) - df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) + for col in headers[5:]: + if col in df_analyzed.columns: + df_analyzed.loc[:, col] = df_analyzed.loc[:, col].shift(1) + else: + df_analyzed.loc[:, col] = 0 if col != 'enter_tag' else None # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) @@ -350,10 +359,13 @@ class Backtesting: def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: sell_candle_time = sell_row[DATE_IDX].to_pydatetime() - sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore - sell_candle_time, sell_row[BUY_IDX], - sell_row[SELL_IDX], - low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) + enter = sell_row[SHORT_IDX] if trade.is_short else sell_row[LONG_IDX] + exit_ = sell_row[ESHORT_IDX] if trade.is_short else sell_row[ELONG_IDX] + sell = self.strategy.should_exit( + trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore + enter=enter, exit_=exit_, + low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX] + ) if sell.sell_flag: trade.close_date = sell_candle_time @@ -389,9 +401,12 @@ class Backtesting: if len(detail_data) == 0: # Fall back to "regular" data if no detail data was found for this candle return self._get_sell_trade_entry_for_candle(trade, sell_row) - detail_data['buy'] = sell_row[BUY_IDX] - detail_data['sell'] = sell_row[SELL_IDX] - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] + detail_data['enter_long'] = sell_row[LONG_IDX] + detail_data['exit_long'] = sell_row[ELONG_IDX] + detail_data['enter_short'] = sell_row[SHORT_IDX] + detail_data['exit_short'] = sell_row[ESHORT_IDX] + headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', + 'enter_short', 'exit_short'] for det_row in detail_data[headers].values.tolist(): res = self._get_sell_trade_entry_for_candle(trade, det_row) if res: @@ -402,7 +417,7 @@ class Backtesting: else: return self._get_sell_trade_entry_for_candle(trade, sell_row) - def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: + def _enter_trade(self, pair: str, row: List, direction: str) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: @@ -414,7 +429,8 @@ class Backtesting: stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], - proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) + proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount, + side=direction) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: @@ -425,12 +441,13 @@ class Backtesting: # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], - time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): + time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime(), + side=direction): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade - has_buy_tag = len(row) >= BUY_TAG_IDX + 1 + has_enter_tag = len(row) >= ENTER_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -440,8 +457,9 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, - buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, - exchange='backtesting', + buy_tag=row[ENTER_TAG_IDX] if has_enter_tag else None, + exchange=self._exchange_name, + is_short=(direction == 'short'), ) return trade return None @@ -475,6 +493,20 @@ class Backtesting: self.rejected_trades += 1 return False + def check_for_trade_entry(self, row) -> Optional[str]: + enter_long = row[LONG_IDX] == 1 + exit_long = row[ELONG_IDX] == 1 + enter_short = self._can_short and row[SHORT_IDX] == 1 + exit_short = self._can_short and row[ESHORT_IDX] == 1 + + if enter_long == 1 and not any([exit_long, enter_short]): + # Long + return 'long' + if enter_short == 1 and not any([exit_short, enter_long]): + # Short + return 'short' + return None + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, @@ -537,15 +569,15 @@ class Backtesting: # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row + trade_dir = self.check_for_trade_entry(row) if ( (position_stacking or len(open_trades[pair]) == 0) and self.trade_slot_available(max_open_trades, open_trade_count_start) and tmp != end_date - and row[BUY_IDX] == 1 - and row[SELL_IDX] != 1 + and trade_dir is not None and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) ): - trade = self._enter_trade(pair, row) + trade = self._enter_trade(pair, row, trade_dir) if trade: # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 509c03e90..43b61cf67 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -386,8 +386,9 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra ) fig.add_trace(candles, 1, 1) - if 'buy' in data.columns: - df_buy = data[data['buy'] == 1] + # TODO-lev: Needs short equivalent + if 'enter_long' in data.columns: + df_buy = data[data['enter_long'] == 1] if len(df_buy) > 0: buys = go.Scatter( x=df_buy.date, @@ -405,8 +406,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra else: logger.warning("No buy-signals found.") - if 'sell' in data.columns: - df_sell = data[data['sell'] == 1] + if 'exit_long' in data.columns: + df_sell = data[data['exit_long'] == 1] if len(df_sell) > 0: sells = go.Scatter( x=df_sell.date, diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index bdfe16d28..a22a0b6b8 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -13,7 +13,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import SellType, SignalTagType, SignalType +from freqtrade.enums import SellType, SignalDirection, SignalTagType, SignalType from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date @@ -187,7 +187,7 @@ class IStrategy(ABC, HyperStrategyMixin): def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: """ - Check buy enter timeout function callback. + Check buy timeout function callback. This method can be used to override the enter-timeout. It is called whenever a limit entry order has been created, and is not yet fully filled. @@ -231,7 +231,8 @@ class IStrategy(ABC, HyperStrategyMixin): pass 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, + side: str, **kwargs) -> bool: """ Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or @@ -247,6 +248,7 @@ class IStrategy(ABC, HyperStrategyMixin): :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 current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :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 placed on the exchange. False aborts the process @@ -366,10 +368,9 @@ class IStrategy(ABC, HyperStrategyMixin): def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + side: str, **kwargs) -> float: """ - Customize stake size for each new trade. This method is not called when edge module is - enabled. + Customize stake size for each new trade. :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime @@ -377,10 +378,28 @@ class IStrategy(ABC, HyperStrategyMixin): :param proposed_stake: A stake amount proposed by the bot. :param min_stake: Minimal stake size allowed by exchange. :param max_stake: Balance available for trading. + :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A stake size, which is between min_stake and max_stake. """ return proposed_stake + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + """ + Customize leverage for each new trade. This method is not called when edge module is + enabled. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_leverage: A leverage proposed by the bot. + :param max_leverage: Max leverage allowed on this pair + :param side: 'long' or 'short' - indicating the direction of the proposed trade + :return: A leverage amount, which is between 1.0 and max_leverage. + """ + return 1.0 + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. @@ -471,8 +490,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ logger.debug("TA Analysis Launched") dataframe = self.advise_indicators(dataframe, metadata) - dataframe = self.advise_buy(dataframe, metadata) - dataframe = self.advise_sell(dataframe, metadata) + dataframe = self.advise_entry(dataframe, metadata) + dataframe = self.advise_exit(dataframe, metadata) return dataframe def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -497,9 +516,11 @@ class IStrategy(ABC, HyperStrategyMixin): self.dp._set_cached_df(pair, self.timeframe, dataframe) else: logger.debug("Skipping TA Analysis for already analyzed candle") - dataframe['buy'] = 0 - dataframe['sell'] = 0 - dataframe['buy_tag'] = None + dataframe[SignalType.ENTER_LONG.value] = 0 + dataframe[SignalType.EXIT_LONG.value] = 0 + dataframe[SignalType.ENTER_SHORT.value] = 0 + dataframe[SignalType.EXIT_SHORT.value] = 0 + dataframe[SignalTagType.ENTER_TAG.value] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -558,8 +579,8 @@ class IStrategy(ABC, HyperStrategyMixin): message = "" if dataframe is None: message = "No dataframe returned (return statement missing?)." - elif 'buy' not in dataframe: - message = "Buy column not set." + elif 'enter_long' not in dataframe: + message = "enter_long/buy column not set." elif df_len != len(dataframe): message = message_template.format("length") elif df_close != dataframe["close"].iloc[-1]: @@ -572,12 +593,12 @@ class IStrategy(ABC, HyperStrategyMixin): else: raise StrategyError(message) - def get_signal( + def get_latest_candle( self, pair: str, timeframe: str, - dataframe: DataFrame - ) -> Tuple[bool, bool, Optional[str]]: + dataframe: DataFrame, + ) -> Tuple[Optional[DataFrame], Optional[arrow.Arrow]]: """ Calculates current signal based based on the entry order or exit order columns of the dataframe. @@ -585,12 +606,11 @@ class IStrategy(ABC, HyperStrategyMixin): :param pair: pair in format ANT/BTC :param timeframe: timeframe to use :param dataframe: Analyzed dataframe to get signal from. - :return: (Buy, Sell)/(Short, Exit_short) A bool-tuple indicating - (buy/sell)/(short/exit_short) signal + :return: (None, None) or (Dataframe, latest_date) - corresponding to the last candle """ if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning(f'Empty candle (OHLCV) data for pair {pair}') - return False, False, None + return None, None latest_date = dataframe['date'].max() latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] @@ -605,27 +625,89 @@ class IStrategy(ABC, HyperStrategyMixin): 'Outdated history for pair %s. Last tick is %s minutes old', pair, int((arrow.utcnow() - latest_date).total_seconds() // 60) ) - return False, False, None + return None, None + return latest, latest_date - enter = latest[SignalType.BUY.value] == 1 + def get_exit_signal( + self, + pair: str, + timeframe: str, + dataframe: DataFrame, + is_short: bool = None + ) -> Tuple[bool, bool]: + """ + Calculates current exit signal based based on the buy/short or sell/exit_short + columns of the dataframe. + Used by Bot to get the signal to exit. + depending on is_short, looks at "short" or "long" columns. + :param pair: pair in format ANT/BTC + :param timeframe: timeframe to use + :param dataframe: Analyzed dataframe to get signal from. + :param is_short: Indicating existing trade direction. + :return: (enter, exit) A bool-tuple with enter / exit values. + """ + latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe) + if latest is None: + return False, False - exit = False - if SignalType.SELL.value in latest: - exit = latest[SignalType.SELL.value] == 1 + if is_short: + enter = latest.get(SignalType.ENTER_SHORT.value, 0) == 1 + exit_ = latest.get(SignalType.EXIT_SHORT.value, 0) == 1 + else: + enter = latest[SignalType.ENTER_LONG.value] == 1 + exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1 - buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) + logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) " + f"enter={enter} exit={exit_}") + + return enter, exit_ + + def get_entry_signal( + self, + pair: str, + timeframe: str, + dataframe: DataFrame, + ) -> Tuple[Optional[SignalDirection], Optional[str]]: + """ + Calculates current entry signal based based on the buy/short or sell/exit_short + 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 timeframe: timeframe to use + :param dataframe: Analyzed dataframe to get signal from. + :return: (SignalDirection, entry_tag) + """ + latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe) + if latest is None or latest_date is None: + return None, None + + enter_long = latest[SignalType.ENTER_LONG.value] == 1 + exit_long = latest.get(SignalType.EXIT_LONG.value, 0) == 1 + enter_short = latest.get(SignalType.ENTER_SHORT.value, 0) == 1 + exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1 + + enter_signal: Optional[SignalDirection] = None + enter_tag_value: Optional[str] = None + if enter_long == 1 and not any([exit_long, enter_short]): + enter_signal = SignalDirection.LONG + enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) + if enter_short == 1 and not any([exit_short, enter_long]): + enter_signal = SignalDirection.SHORT + enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) - logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', - latest['date'], pair, str(enter), str(exit)) timeframe_seconds = timeframe_to_seconds(timeframe) + if self.ignore_expired_candle( - latest_date=latest_date, + latest_date=latest_date.datetime, current_time=datetime.now(timezone.utc), timeframe_seconds=timeframe_seconds, - enter=enter + enter=bool(enter_signal) ): - return False, exit, buy_tag - return enter, exit, buy_tag + return None, enter_tag_value + + logger.debug(f"entry trigger: {latest['date']} (pair={pair}) " + f"enter={enter_long} enter_tag_value={enter_tag_value}") + return enter_signal, enter_tag_value def ignore_expired_candle( self, @@ -640,8 +722,9 @@ 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_exit(self, trade: Trade, rate: float, date: datetime, *, + enter: bool, exit_: bool, + low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger an exit order @@ -651,6 +734,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param force_stoploss: Externally provided stoploss :return: True if trade should be exited, False otherwise """ + current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) @@ -665,7 +749,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_profit = trade.calc_profit_ratio(current_rate) # 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, current_time=date)) @@ -678,10 +762,11 @@ class IStrategy(ABC, HyperStrategyMixin): 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 pass - elif self.use_sell_signal and not buy: - if sell: + elif self.use_sell_signal and not enter: + if exit_: sell_signal = SellType.SELL_SIGNAL else: + trade_type = "exit_short" if trade.is_short else "sell" 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) @@ -689,9 +774,9 @@ class IStrategy(ABC, HyperStrategyMixin): 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.') + logger.warning(f'Custom {trade_type} reason returned from ' + f'custom_{trade_type} is too long and was trimmed' + f'to {CUSTOM_SELL_MAX_LENGTH} characters.') custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH] else: custom_reason = None @@ -737,7 +822,12 @@ class IStrategy(ABC, HyperStrategyMixin): # Initiate stoploss with open_rate. Does nothing if stoploss is already set. 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) + if not trade.is_short else + trade.stop_loss > (high or current_rate) + ) + + if self.use_custom_stoploss and dir_correct: stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, @@ -755,6 +845,7 @@ class IStrategy(ABC, HyperStrategyMixin): sl_offset = self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. + # TODO-lev: Check this function - high / low usage must be inversed for short trades! high_profit = current_profit if not high else trade.calc_profit_ratio(high) # Don't update stoploss if trailing_only_offset_is_reached is true. @@ -821,7 +912,7 @@ class IStrategy(ABC, HyperStrategyMixin): def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ Populates indicators for given candle (OHLCV) data (for multiple pairs) - Does not run advise_buy or advise_sell! + Does not run advise_entry or advise_exit! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show. @@ -853,7 +944,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: return self.populate_indicators(dataframe, metadata) - def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the entry order signal for the given dataframe This method should not be overridden. @@ -868,11 +959,15 @@ class IStrategy(ABC, HyperStrategyMixin): if self._buy_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) - return self.populate_buy_trend(dataframe) # type: ignore + df = self.populate_buy_trend(dataframe) # type: ignore else: - return self.populate_buy_trend(dataframe, metadata) + df = self.populate_buy_trend(dataframe, metadata) + if 'enter_long' not in df.columns: + df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns') - def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return df + + def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the exit order signal for the given dataframe This method should not be overridden. @@ -886,6 +981,9 @@ class IStrategy(ABC, HyperStrategyMixin): if self._sell_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) - return self.populate_sell_trend(dataframe) # type: ignore + df = self.populate_sell_trend(dataframe) # type: ignore else: - return self.populate_sell_trend(dataframe, metadata) + df = self.populate_sell_trend(dataframe, metadata) + if 'exit_long' not in df.columns: + df = df.rename({'sell': 'exit_long'}, axis='columns') + return df diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index de88de33b..126a9c6c5 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,5 +1,6 @@ import pandas as pd +from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes @@ -66,7 +67,11 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.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, @@ -87,10 +92,19 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa if current_profit == -1: return 1 - stoploss = 1-((1+open_relative_stop)/(1+current_profit)) + if for_short is True: + # TODO-lev: How would this be calculated for short + raise OperationalException( + "Freqtrade hasn't figured out how to calculated stoploss on shorts") + # stoploss = 1-((1+open_relative_stop)/(1+current_profit)) + else: + stoploss = 1-((1+open_relative_stop)/(1+current_profit)) # 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) def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 06d7cbc5c..3feff75c6 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -122,7 +122,7 @@ class {{ strategy }}(IStrategy): {{ buy_trend | indent(16) }} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'buy'] = 1 + 'enter_long'] = 1 return dataframe @@ -138,6 +138,6 @@ class {{ strategy }}(IStrategy): {{ sell_trend | indent(16) }} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'sell'] = 1 + 'exit_long'] = 1 return dataframe {{ additional_methods | indent(4) }} diff --git a/freqtrade/templates/sample_short_strategy.py b/freqtrade/templates/sample_short_strategy.py new file mode 100644 index 000000000..bdd0054e8 --- /dev/null +++ b/freqtrade/templates/sample_short_strategy.py @@ -0,0 +1,379 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 +# isort: skip_file +# --- Do not remove these libs --- +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame + +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +# This class is a sample. Feel free to customize it. +class SampleStrategy(IStrategy): + """ + This is a sample strategy to inspire you. + More information in https://www.freqtrade.io/en/latest/strategy-customization/ + + You can: + :return: a Dataframe with all mandatory indicators for the strategies + - Rename the class name (Do not forget to update class_name) + - Add any methods you want to build your strategy + - Add any lib you need to build your strategy + + You must keep: + - the lib in the section "Do not remove these libs" + - the methods: populate_indicators, populate_buy_trend, populate_sell_trend + You should keep: + - timeframe, minimal_roi, stoploss, trailing_* + """ + # Strategy interface version - allow new iterations of the strategy interface. + # Check the documentation or the Sample strategy to get the latest version. + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi". + minimal_roi = { + "60": 0.01, + "30": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy. + # This attribute will be overridden if the config file contains "stoploss". + stoploss = -0.10 + + # Trailing stoploss + trailing_stop = False + # trailing_only_offset_is_reached = False + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured + + # Hyperoptable parameters + 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. + timeframe = '5m' + + # Run "populate_indicators()" only for new candle. + process_only_new_candles = False + + # These values can be overridden in the "ask_strategy" section in the config. + use_sell_signal = True + sell_profit_only = False + ignore_roi_if_buy_signal = False + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 30 + + # Optional order type mapping. + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + # Optional order time in force. + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc' + } + + plot_config = { + 'main_plot': { + 'tema': {}, + 'sar': {'color': 'white'}, + }, + 'subplots': { + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + "RSI": { + 'rsi': {'color': 'red'}, + } + } + } + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # Momentum Indicators + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # # Plus Directional Indicator / Movement + # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + # dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # # Minus Directional Indicator / Movement + # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # # Aroon, Aroon Oscillator + # aroon = ta.AROON(dataframe) + # dataframe['aroonup'] = aroon['aroonup'] + # dataframe['aroondown'] = aroon['aroondown'] + # dataframe['aroonosc'] = ta.AROONOSC(dataframe) + + # # Awesome Oscillator + # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # # Keltner Channel + # keltner = qtpylib.keltner_channel(dataframe) + # dataframe["kc_upperband"] = keltner["upper"] + # dataframe["kc_lowerband"] = keltner["lower"] + # dataframe["kc_middleband"] = keltner["mid"] + # dataframe["kc_percent"] = ( + # (dataframe["close"] - dataframe["kc_lowerband"]) / + # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) + # ) + # dataframe["kc_width"] = ( + # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"] + # ) + + # # Ultimate Oscillator + # dataframe['uo'] = ta.ULTOSC(dataframe) + + # # Commodity Channel Index: values [Oversold:-100, Overbought:100] + # dataframe['cci'] = ta.CCI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) + # rsi = 0.1 * (dataframe['rsi'] - 50) + # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + + # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # # Stochastic Slow + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] + + # Stochastic Fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # # Stochastic RSI + # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. + # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # # ROC + # dataframe['roc'] = ta.ROC(dataframe) + + # Overlap Studies + # ------------------------------------ + + # Bollinger Bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + dataframe["bb_percent"] = ( + (dataframe["close"] - dataframe["bb_lowerband"]) / + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) + ) + dataframe["bb_width"] = ( + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"] + ) + + # Bollinger Bands - Weighted (EMA based instead of SMA) + # weighted_bollinger = qtpylib.weighted_bollinger_bands( + # qtpylib.typical_price(dataframe), window=20, stds=2 + # ) + # dataframe["wbb_upperband"] = weighted_bollinger["upper"] + # dataframe["wbb_lowerband"] = weighted_bollinger["lower"] + # dataframe["wbb_middleband"] = weighted_bollinger["mid"] + # dataframe["wbb_percent"] = ( + # (dataframe["close"] - dataframe["wbb_lowerband"]) / + # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) + # ) + # dataframe["wbb_width"] = ( + # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) / + # dataframe["wbb_middleband"] + # ) + + # # EMA - Exponential Moving Average + # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + # dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21) + # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # # SMA - Simple Moving Average + # dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3) + # dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5) + # dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10) + # dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21) + # dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50) + # dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100) + + # Parabolic SAR + dataframe['sar'] = ta.SAR(dataframe) + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + # # Hammer: values [0, 100] + # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # # Inverted Hammer: values [0, 100] + # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # # Dragonfly Doji: values [0, 100] + # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # # Piercing Line: values [0, 100] + # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # # Morningstar: values [0, 100] + # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # # Three White Soldiers: values [0, 100] + # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + # # Hanging Man: values [0, 100] + # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # # Shooting Star: values [0, 100] + # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # # Gravestone Doji: values [0, 100] + # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # # Dark Cloud Cover: values [0, 100] + # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # # Evening Doji Star: values [0, 100] + # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # # Evening Star: values [0, 100] + # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + # # Three Line Strike: values [0, -100, 100] + # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # # Spinning Top: values [0, -100, 100] + # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # # Engulfing: values [0, -100, 100] + # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # # Harami: values [0, -100, 100] + # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # # Three Outside Up/Down: values [0, -100, 100] + # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # # Three Inside Up/Down: values [0, -100, 100] + # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + + # # Chart type + # # ------------------------------------ + # # Heikin Ashi Strategy + # heikinashi = qtpylib.heikinashi(dataframe) + # dataframe['ha_open'] = heikinashi['open'] + # dataframe['ha_close'] = heikinashi['close'] + # dataframe['ha_high'] = heikinashi['high'] + # dataframe['ha_low'] = heikinashi['low'] + + # Retrieve best bid and best ask from the orderbook + # ------------------------------------ + """ + # first check if dataprovider is available + if self.dp: + if self.dp.runmode.value in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] + """ + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy 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 + ), + 'enter_short'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with sell column + """ + + dataframe.loc[ + ( + # Signal: RSI crosses above 30 + (qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) & + # Guard: tema below BB middle + (dataframe['tema'] <= dataframe['bb_middleband']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'exit_short'] = 1 + + return dataframe diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 574819949..80fa7cdae 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -352,7 +352,7 @@ class SampleStrategy(IStrategy): (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'buy'] = 1 + 'enter_long'] = 1 return dataframe @@ -371,5 +371,5 @@ class SampleStrategy(IStrategy): (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'sell'] = 1 + 'exit_long'] = 1 return dataframe diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 2df23f365..1f064f88e 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -12,12 +12,11 @@ def bot_loop_start(self, **kwargs) -> None: """ pass -def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float, +def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + side: str, **kwargs) -> float: """ - Customize stake size for each new trade. This method is not called when edge module is - enabled. + Customize stake size for each new trade. :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime @@ -25,6 +24,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: :param proposed_stake: A stake amount proposed by the bot. :param min_stake: Minimal stake size allowed by exchange. :param max_stake: Balance available for trading. + :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A stake size, which is between min_stake and max_stake. """ return proposed_stake @@ -80,9 +80,10 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre return None 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, + side: str, **kwargs) -> bool: """ - Called right before placing a buy order. + Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -90,12 +91,13 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f 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 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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :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 placed on the exchange. False aborts the process diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 135510b38..a1d89d7d3 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -19,8 +19,8 @@ from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_in from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_args, log_has, + log_has_re, patch_exchange, patched_configuration_load_config_file) from tests.conftest_trades import MOCK_TRADE_COUNT @@ -774,7 +774,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" not in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out # Test regular output args = [ @@ -789,7 +789,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out # Test color output args = [ @@ -803,7 +803,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out assert "LOAD FAILED" in captured.out diff --git a/tests/conftest.py b/tests/conftest.py index d2f24fa69..b35ff17d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from copy import deepcopy from datetime import datetime, timedelta from functools import reduce from pathlib import Path -from typing import Tuple +from typing import Optional, Tuple from unittest.mock import MagicMock, Mock, PropertyMock import arrow @@ -19,6 +19,7 @@ from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.enums import Collateral, RunMode, TradingMode +from freqtrade.enums.signaltype import SignalDirection from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db @@ -34,6 +35,9 @@ logging.getLogger('').setLevel(logging.INFO) # Do not mask numpy errors as warnings that no one read, raise the exсeption np.seterr(all='raise') +CURRENT_TEST_STRATEGY = 'StrategyTestV3' +TRADE_SIDES = ('long', 'short') + def pytest_addoption(parser): parser.addoption('--longrun', action='store_true', dest="longrun", @@ -201,13 +205,35 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None)) -> None: +def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False, + enter_short=False, exit_short=False, enter_tag: Optional[str] = None) -> None: """ :param mocker: mocker to patch IStrategy class :param value: which value IStrategy.get_signal() must return + (buy, sell, buy_tag) :return: None """ - freqtrade.strategy.get_signal = lambda e, s, x: value + # returns (Signal-direction, signaname) + def patched_get_entry_signal(*args, **kwargs): + direction = None + if enter_long and not any([exit_long, enter_short]): + direction = SignalDirection.LONG + if enter_short and not any([exit_short, enter_long]): + direction = SignalDirection.SHORT + + return direction, enter_tag + + freqtrade.strategy.get_entry_signal = patched_get_entry_signal + + def patched_get_exit_signal(pair, timeframe, dataframe, is_short): + if is_short: + return enter_short, exit_short + else: + return enter_long, exit_long + + # returns (enter, exit) + freqtrade.strategy.get_exit_signal = patched_get_exit_signal + freqtrade.exchange.refresh_latest_ohlcv = lambda p: None @@ -383,7 +409,7 @@ def get_default_conf(testdatadir): "user_data_dir": Path("user_data"), "verbosity": 3, "strategy_path": str(Path(__file__).parent / "strategy" / "strats"), - "strategy": "StrategyTestV2", + "strategy": CURRENT_TEST_STRATEGY, "disableparamexport": True, "internals": {}, "export": "none", diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 700cd3fa7..cf3c970f6 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -33,7 +33,7 @@ def mock_trade_1(fee): open_rate=0.123, exchange='binance', open_order_id='dry_run_buy_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') @@ -87,7 +87,7 @@ def mock_trade_2(fee): exchange='binance', is_open=False, open_order_id='dry_run_sell_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, sell_reason='sell_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), @@ -146,7 +146,7 @@ def mock_trade_3(fee): close_profit_abs=0.000155, exchange='binance', is_open=False, - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, sell_reason='roi', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), @@ -189,7 +189,7 @@ def mock_trade_4(fee): open_rate=0.123, exchange='binance', open_order_id='prod_buy_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 1dcd04a80..e7b8c5b2f 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -16,7 +16,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_ get_latest_hyperopt_file, load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history -from tests.conftest import create_mock_trades +from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT @@ -128,7 +128,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): for col in BT_DATA_COLUMNS: if col not in ['index', 'open_at_end']: assert col in trades.columns - trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='StrategyTestV2') + trades = load_trades_from_db(db_url=default_conf['db_url'], strategy=CURRENT_TEST_STRATEGY) assert len(trades) == 4 trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy') assert len(trades) == 0 @@ -186,7 +186,7 @@ def test_load_trades(default_conf, mocker): db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), no_trades=False, - strategy="StrategyTestV2", + strategy=CURRENT_TEST_STRATEGY, ) assert db_mock.call_count == 1 diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 575a590e7..73ceabbbf 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -26,7 +26,8 @@ from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHa from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver -from tests.conftest import get_patched_exchange, log_has, log_has_re, patch_exchange +from tests.conftest import (CURRENT_TEST_STRATEGY, get_patched_exchange, log_has, log_has_re, + patch_exchange) # Change this if modifying UNITTEST/BTC testdatafile @@ -380,7 +381,7 @@ def test_file_dump_json_tofile(testdatadir) -> None: def test_get_timerange(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) data = strategy.advise_all_indicators( @@ -398,7 +399,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None: def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) data = strategy.advise_all_indicators( @@ -422,7 +423,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange('index', 'index', 200, 250) diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 6ad2d300b..10518758c 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -18,7 +18,7 @@ class BTrade(NamedTuple): sell_reason: SellType open_tick: int close_tick: int - buy_tag: Optional[str] = None + enter_tag: Optional[str] = None class BTContainer(NamedTuple): @@ -44,14 +44,18 @@ def _get_frame_time_from_offset(offset): def _build_backtest_dataframe(data): - columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell'] - columns = columns + ['buy_tag'] if len(data[0]) == 9 else columns + columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'enter_long', 'exit_long', + 'enter_short', 'exit_short'] + if len(data[0]) == 8: + # No short columns + data = [d + [0, 0] for d in data] + columns = columns + ['enter_tag'] if len(data[0]) == 11 else columns frame = DataFrame.from_records(data, columns=columns) frame['date'] = frame['date'].apply(_get_frame_time_from_offset) # Ensure floats are in place for column in ['open', 'high', 'low', 'close', 'volume']: frame[column] = frame[column].astype('float64') - if 'buy_tag' not in columns: - frame['buy_tag'] = None + if 'enter_tag' not in columns: + frame['enter_tag'] = None return frame diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e5c037f3e..227d778af 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -519,12 +519,12 @@ tc32 = BTContainer(data=[ # Test 33: trailing_stop should be triggered immediately on trade open candle. # stop-loss: 1%, ROI: 10% (should not apply) tc33 = BTContainer(data=[ - # D O H L C V B S BT - [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 'buy_signal_01'], - [1, 5000, 5500, 5000, 4900, 6172, 0, 0, None], # enter trade (signal on last candle) and stop - [2, 4900, 5250, 4500, 5100, 6172, 0, 0, None], - [3, 5100, 5100, 4650, 4750, 6172, 0, 0, None], - [4, 4750, 4950, 4350, 4750, 6172, 0, 0, None]], + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 0, 0, 'buy_signal_01'], + [1, 5000, 5500, 5000, 4900, 6172, 0, 0, 0, 0, None], # enter trade and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0, 0, 0, None], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0, None], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0, None]], stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_stop_positive=0.01, use_custom_stoploss=True, @@ -532,7 +532,7 @@ tc33 = BTContainer(data=[ sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1, - buy_tag='buy_signal_01' + enter_tag='buy_signal_01' )] ) @@ -571,6 +571,7 @@ TESTS = [ tc31, tc32, tc33, + # TODO-lev: Add tests for short here ] @@ -597,8 +598,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) backtesting.required_startup = 0 - backtesting.strategy.advise_buy = lambda a, m: frame - backtesting.strategy.advise_sell = lambda a, m: frame + backtesting.strategy.advise_entry = lambda a, m: frame + backtesting.strategy.advise_exit = lambda a, m: frame backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss caplog.set_level(logging.DEBUG) @@ -620,6 +621,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.sell_reason == trade.sell_reason.value - assert res.buy_tag == trade.buy_tag + assert res.buy_tag == trade.enter_tag assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 2248cd4c1..662ca0193 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -123,12 +123,14 @@ def _trend(signals, buy_value, sell_value): n = len(signals['low']) buy = np.zeros(n) sell = np.zeros(n) - for i in range(0, len(signals['buy'])): + for i in range(0, len(signals['date'])): if random.random() > 0.5: # Both buy and sell signals at same timeframe buy[i] = buy_value sell[i] = sell_value - signals['buy'] = buy - signals['sell'] = sell + signals['enter_long'] = buy + signals['exit_long'] = sell + signals['enter_short'] = 0 + signals['exit_short'] = 0 return signals @@ -143,8 +145,10 @@ def _trend_alternate(dataframe=None, metadata=None): buy[i] = 1 else: sell[i] = 1 - signals['buy'] = buy - signals['sell'] = sell + signals['enter_long'] = buy + signals['exit_long'] = sell + signals['enter_short'] = 0 + signals['exit_short'] = 0 return dataframe @@ -155,7 +159,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--export', 'none' ] @@ -190,7 +194,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--timeframe', '1m', '--enable-position-stacking', @@ -240,7 +244,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '2' ] @@ -251,7 +255,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '0.5' ] @@ -269,7 +273,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] pargs = get_args(args) start_backtesting(pargs) @@ -291,8 +295,8 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert backtesting.config == default_conf assert backtesting.timeframe == '5m' assert callable(backtesting.strategy.advise_all_indicators) - assert callable(backtesting.strategy.advise_buy) - assert callable(backtesting.strategy.advise_sell) + assert callable(backtesting.strategy.advise_entry) + assert callable(backtesting.strategy.advise_exit) assert isinstance(backtesting.strategy.dp, DataProvider) get_fee.assert_called() assert backtesting.fee == 0.5 @@ -302,7 +306,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: patch_exchange(mocker) del default_conf['timeframe'] - default_conf['strategy_list'] = ['StrategyTestV2', + default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'SampleStrategy'] mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) @@ -340,7 +344,6 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: assert len(processed['UNITTEST/BTC']) == 102 # Load strategy to compare the result between Backtesting function and strategy are the same - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) processed2 = strategy.advise_all_indicators(data) @@ -482,7 +485,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) # Multiple strategies - default_conf['strategy_list'] = ['StrategyTestV2', 'TestStrategyLegacyV1'] + default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1'] with pytest.raises(OperationalException, match='PrecisionFilter not allowed for backtesting multiple strategies.'): Backtesting(default_conf) @@ -508,41 +511,47 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: 0.0012, # High '', # Buy Signal Name ] - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 # Fake 2 trades, so there's not enough amount for the next trade left. LocalTrade.trades_open.append(trade) LocalTrade.trades_open.append(trade) - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None LocalTrade.trades_open.pop() - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is not None backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5 - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade assert trade.stake_amount == 123.5 # In case of error - use proposed stake backtesting.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade assert trade.stake_amount == 495 + assert trade.is_short is False + + trade = backtesting._enter_trade(pair, row=row, direction='short') + assert trade + assert trade.stake_amount == 495 + assert trade.is_short is True # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None # Stake-amount throwing error mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException) - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None backtesting.cleanup() @@ -560,47 +569,54 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: pair = 'UNITTEST/BTC' row = [ pd.Timestamp(year=2020, month=1, day=1, hour=4, minute=55, tzinfo=timezone.utc), - 1, # Buy 200, # Open - 201, # Close - 0, # Sell - 195, # Low 201.5, # High - '', # Buy Signal Name + 195, # Low + 201, # Close + 1, # enter_long + 0, # exit_long + 0, # enter_short + 0, # exit_hsort + '', # Long Signal Name + '', # Short Signal Name ] - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert isinstance(trade, LocalTrade) row_sell = [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc), - 0, # Buy 200, # Open - 201, # Close - 0, # Sell - 195, # Low 210.5, # High - '', # Buy Signal Name + 195, # Low + 201, # Close + 0, # enter_long + 0, # exit_long + 0, # enter_short + 0, # exit_short + '', # long Signal Name + '', # Short Signal Name ] row_detail = pd.DataFrame( [ [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc), - 1, 200, 199, 0, 197, 200.1, '', + 200, 200.1, 197, 199, 1, 0, 0, 0, '', '', ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc), - 0, 199, 199.5, 0, 199, 199.7, '', + 199, 199.7, 199, 199.5, 0, 0, 0, 0, '', '' ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc), - 0, 199.5, 200.5, 0, 199, 200.8, '', + 199.5, 200.8, 199, 200.9, 0, 0, 0, 0, '', '' ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc), - 0, 200.5, 210.5, 0, 193, 210.5, '', # ROI sell (?) + 200.5, 210.5, 193, 210.5, 0, 0, 0, 0, '', '' # ROI sell (?) ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc), - 0, 200, 199, 0, 193, 200.1, '', + 200, 200.1, 193, 199, 0, 0, 0, 0, '', '' ], - ], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"] + ], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', + 'enter_short', 'exit_short', 'long_tag', 'short_tag'] ) # No data available. @@ -610,11 +626,12 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc) # Enter new trade - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert isinstance(trade, LocalTrade) # Assign empty ... no result. backtesting.detail_data[pair] = pd.DataFrame( - [], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"]) + [], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', + 'enter_short', 'exit_short', 'long_tag', 'short_tag']) res = backtesting._get_sell_trade_entry(trade, row) assert res is None @@ -785,7 +802,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): - # Override the default buy trend function in our StrategyTestV2 + # Override the default buy trend function in our StrategyTest def fun(dataframe=None, pair=None): buy_value = 1 sell_value = 1 @@ -794,14 +811,14 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = fun # Override - backtesting.strategy.advise_sell = fun # Override + backtesting.strategy.advise_entry = fun # Override + backtesting.strategy.advise_exit = fun # Override result = backtesting.backtest(**backtest_conf) assert result['results'].empty def test_backtest_only_sell(mocker, default_conf, testdatadir): - # Override the default buy trend function in our StrategyTestV2 + # Override the default buy trend function in our StrategyTest def fun(dataframe=None, pair=None): buy_value = 0 sell_value = 1 @@ -810,8 +827,8 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = fun # Override - backtesting.strategy.advise_sell = fun # Override + backtesting.strategy.advise_entry = fun # Override + backtesting.strategy.advise_exit = fun # Override result = backtesting.backtest(**backtest_conf) assert result['results'].empty @@ -825,8 +842,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): backtesting = Backtesting(default_conf) backtesting.required_startup = 0 backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = _trend_alternate # Override - backtesting.strategy.advise_sell = _trend_alternate # Override + backtesting.strategy.advise_entry = _trend_alternate # Override + backtesting.strategy.advise_exit = _trend_alternate # Override result = backtesting.backtest(**backtest_conf) # 200 candles in backtest data # won't buy on first (shifted by 1) @@ -857,8 +874,10 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) multi = 20 else: multi = 18 - dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0) - dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) + dataframe['enter_long'] = np.where(dataframe.index % multi == 0, 1, 0) + dataframe['exit_long'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) + dataframe['enter_short'] = 0 + dataframe['exit_short'] = 0 return dataframe mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) @@ -877,8 +896,8 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = _trend_alternate_hold # Override - backtesting.strategy.advise_sell = _trend_alternate_hold # Override + backtesting.strategy.advise_entry = _trend_alternate_hold # Override + backtesting.strategy.advise_exit = _trend_alternate_hold # Override processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) @@ -928,7 +947,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', str(testdatadir), '--timeframe', '1m', '--timerange', '1510694220-1510700340', @@ -999,7 +1018,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): '--enable-position-stacking', '--disable-max-market-positions', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1', ] args = get_args(args) @@ -1022,7 +1041,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'Backtesting with data from 2017-11-14 21:17:00 ' 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy TestStrategyLegacyV1', ] @@ -1103,7 +1122,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '--enable-position-stacking', '--disable-max-market-positions', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1', ] args = get_args(args) @@ -1120,7 +1139,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'Backtesting with data from 2017-11-14 21:17:00 ' 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy TestStrategyLegacyV1', ] @@ -1208,7 +1227,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, '--timeframe', '5m', '--timeframe-detail', '1m', '--strategy-list', - 'StrategyTestV2' + CURRENT_TEST_STRATEGY ] args = get_args(args) start_backtesting(args) @@ -1222,7 +1241,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, 'up to 2019-10-13 11:10:00 (2 days).', 'Backtesting with data from 2019-10-11 01:40:00 ' 'up to 2019-10-13 11:10:00 (2 days).', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', ] for line in exists: diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 18d5f1c76..e091c9c53 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge from freqtrade.enums import RunMode from freqtrade.optimize.edge_cli import EdgeCli -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -16,7 +16,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] config = setup_optimize_configuration(get_args(args), RunMode.EDGE) @@ -46,7 +46,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--timeframe', '1m', '--timerange', ':100', @@ -80,7 +80,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] pargs = get_args(args) start_edge(pargs) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index e4ce29d44..57d10d048 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -18,10 +18,13 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy.hyper import IntParameter -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) +# TODO-lev: This file + + def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -122,7 +125,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None args = [ 'hyperopt', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '0.5' ] @@ -315,8 +318,8 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: # Should be called for historical candle data assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -695,8 +698,8 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -769,8 +772,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: assert dumper.called assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -818,8 +821,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: assert dumper.called assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 9c2b2e8fc..17e8248c3 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -10,7 +10,7 @@ import rapidjson from freqtrade.constants import FTHYPT_FILEVERSION from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer -from tests.conftest import log_has +from tests.conftest import CURRENT_TEST_STRATEGY, log_has # Functions for recurrent object patching @@ -167,9 +167,9 @@ def test__pprint_dict(): def test_get_strategy_filename(default_conf): - x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV2') + x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV3') assert isinstance(x, Path) - assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v2.py' + assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v3.py' x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy') assert x is None @@ -177,7 +177,7 @@ def test_get_strategy_filename(default_conf): def test_export_params(tmpdir): - filename = Path(tmpdir) / "StrategyTestV2.json" + filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json" assert not filename.is_file() params = { "params_details": { @@ -205,12 +205,12 @@ def test_export_params(tmpdir): } } - HyperoptTools.export_params(params, "StrategyTestV2", filename) + HyperoptTools.export_params(params, CURRENT_TEST_STRATEGY, filename) assert filename.is_file() content = rapidjson.load(filename.open('r')) - assert content['strategy_name'] == 'StrategyTestV2' + assert content['strategy_name'] == CURRENT_TEST_STRATEGY assert 'params' in content assert "buy" in content["params"] assert "sell" in content["params"] @@ -223,7 +223,7 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker): default_conf['disableparamexport'] = False export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params") - filename = Path(tmpdir) / "StrategyTestV2.json" + filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json" assert not filename.is_file() params = { "params_details": { @@ -252,17 +252,17 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker): FTHYPT_FILEVERSION: 2, } - HyperoptTools.try_export_params(default_conf, "StrategyTestV222", params) + HyperoptTools.try_export_params(default_conf, "StrategyTestVXXX", params) assert log_has("Strategy not found, not exporting parameter file.", caplog) assert export_mock.call_count == 0 caplog.clear() - HyperoptTools.try_export_params(default_conf, "StrategyTestV2", params) + HyperoptTools.try_export_params(default_conf, CURRENT_TEST_STRATEGY, params) assert export_mock.call_count == 1 - assert export_mock.call_args_list[0][0][1] == 'StrategyTestV2' - assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v2.json' + assert export_mock.call_args_list[0][0][1] == CURRENT_TEST_STRATEGY + assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v3.json' def test_params_print(capsys): diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 83caefd2d..b8cf0c682 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -21,6 +21,7 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver +from tests.conftest import CURRENT_TEST_STRATEGY from tests.data.test_history import _backup_file, _clean_test_file @@ -52,7 +53,7 @@ def test_text_table_bt_results(): def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) StrategyResolver.load_strategy(default_conf) results = {'DefStrat': { diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7c98b2df7..afce87b88 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -24,8 +24,8 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer -from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, - log_has_re, patch_get_signal) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_mock_coro, + get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) BASE_URI = "/api/v1" @@ -885,7 +885,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'open_trade_value': 15.1668225, 'sell_reason': None, 'sell_order_status': None, - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', @@ -990,7 +990,7 @@ def test_api_forcebuy(botclient, mocker, fee): close_rate=0.265441, id=22, timeframe=5, - strategy="StrategyTestV2" + strategy=CURRENT_TEST_STRATEGY )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) @@ -1040,7 +1040,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'open_trade_value': 0.24605460, 'sell_reason': None, 'sell_order_status': None, - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', @@ -1107,7 +1107,7 @@ def test_api_pair_candles(botclient, ohlcv_history): f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) assert 'strategy' in rc.json() - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert 'columns' in rc.json() assert 'data_start_ts' in rc.json() assert 'data_start' in rc.json() @@ -1145,19 +1145,19 @@ def test_api_pair_history(botclient, ohlcv_history): # No pair rc = client_get(client, f"{BASE_URI}/pair_history?timeframe={timeframe}" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No Timeframe rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No timerange rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&strategy=StrategyTestV2") + f"&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No strategy @@ -1169,14 +1169,14 @@ def test_api_pair_history(botclient, ohlcv_history): # Working rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 200) assert rc.json()['length'] == 289 assert len(rc.json()['data']) == rc.json()['length'] assert 'columns' in rc.json() assert 'data' in rc.json() assert rc.json()['pair'] == 'UNITTEST/BTC' - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' assert rc.json()['data_start_ts'] == 1515628800000 assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00' @@ -1185,7 +1185,7 @@ def test_api_pair_history(botclient, ohlcv_history): # No data found rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&timerange=20200111-20200112&strategy=StrategyTestV2") + f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 502) assert rc.json()['error'] == ("Error querying /api/v1/pair_history: " "No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") @@ -1226,19 +1226,20 @@ def test_api_strategies(botclient): 'HyperoptableStrategy', 'InformativeDecoratorTest', 'StrategyTestV2', - 'TestStrategyLegacyV1' + 'StrategyTestV3', + 'TestStrategyLegacyV1', ]} def test_api_strategy(botclient): ftbot, client = botclient - rc = client_get(client, f"{BASE_URI}/strategy/StrategyTestV2") + rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}") assert_response(rc) - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY - data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v2.py").read_text() + data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text() assert rc.json()['code'] == data rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") @@ -1295,7 +1296,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog): # start backtesting data = { - "strategy": "StrategyTestV2", + "strategy": CURRENT_TEST_STRATEGY, "timeframe": "5m", "timerange": "20180110-20180111", "max_open_trades": 3, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 21f1cd000..23ccadca0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -25,8 +25,8 @@ from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC from freqtrade.rpc.telegram import Telegram, authorized_only -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, - patch_exchange, patch_get_signal, patch_whitelist) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot, + log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -1238,7 +1238,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] - assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0] + assert f'*Strategy:* `{CURRENT_TEST_STRATEGY}`' in msg_mock.call_args_list[0][0][0] assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -1247,7 +1247,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] - assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0] + assert f'*Strategy:* `{CURRENT_TEST_STRATEGY}`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py index a32ad79e8..68f8651c2 100644 --- a/tests/strategy/strats/informative_decorator_strategy.py +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -2,8 +2,7 @@ from pandas import DataFrame -from freqtrade.strategy import informative, merge_informative_pair -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy, informative, merge_informative_pair class InformativeDecoratorTest(IStrategy): diff --git a/tests/strategy/strats/legacy_strategy_v1.py b/tests/strategy/strats/legacy_strategy_v1.py index ebfce632b..adb75c33e 100644 --- a/tests/strategy/strats/legacy_strategy_v1.py +++ b/tests/strategy/strats/legacy_strategy_v1.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy # -------------------------------- diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 53e39526f..428ecc8c0 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy class StrategyTestV2(IStrategy): diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py new file mode 100644 index 000000000..115211a7c --- /dev/null +++ b/tests/strategy/strats/strategy_test_v3.py @@ -0,0 +1,181 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from datetime import datetime + +import talib.abstract as ta +from pandas import DataFrame + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy, + RealParameter) + + +class StrategyTestV3(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 3 + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal timeframe for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False + } + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional time in force for orders + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc', + } + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74, + 'sell_minusdi': 0.4 + } + + buy_rsi = IntParameter([0, 50], default=30, 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_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) + protection_enabled = BooleanParameter(default=True) + protection_cooldown_lookback = IntParameter([0, 50], default=30) + + # TODO-lev: Can we make this work with protection tests? + # TODO-lev: (Would replace HyperoptableStrategy implicitly ... ) + # @property + # def protections(self): + # prot = [] + # if self.protection_enabled.value: + # prot.append({ + # "method": "CooldownPeriod", + # "stop_duration_candles": self.protection_cooldown_lookback.value + # }) + # return prot + + def informative_pairs(self): + + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # Minus Directional Indicator / Movement + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + dataframe.loc[ + ( + (dataframe['rsi'] < self.buy_rsi.value) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ), + 'enter_long'] = 1 + dataframe.loc[ + ( + qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value) + ), + 'enter_short'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > self.sell_minusdi.value) + ), + 'exit_long'] = 1 + + dataframe.loc[ + ( + qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value) + ), + 'exit_short'] = 1 + + # TODO-lev: Add short logic + return dataframe + + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + # Return 3.0 in all cases. + # Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly. + + return 3.0 diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 6426ebe5f..a995491f2 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -4,20 +4,20 @@ from pandas import DataFrame from freqtrade.persistence.models import Trade -from .strats.strategy_test_v2 import StrategyTestV2 +from .strats.strategy_test_v3 import StrategyTestV3 def test_strategy_test_v2_structure(): - assert hasattr(StrategyTestV2, 'minimal_roi') - assert hasattr(StrategyTestV2, 'stoploss') - assert hasattr(StrategyTestV2, 'timeframe') - assert hasattr(StrategyTestV2, 'populate_indicators') - assert hasattr(StrategyTestV2, 'populate_buy_trend') - assert hasattr(StrategyTestV2, 'populate_sell_trend') + assert hasattr(StrategyTestV3, 'minimal_roi') + assert hasattr(StrategyTestV3, 'stoploss') + assert hasattr(StrategyTestV3, 'timeframe') + assert hasattr(StrategyTestV3, 'populate_indicators') + assert hasattr(StrategyTestV3, 'populate_buy_trend') + assert hasattr(StrategyTestV3, 'populate_sell_trend') def test_strategy_test_v2(result, fee): - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) metadata = {'pair': 'ETH/BTC'} assert type(strategy.minimal_roi) is dict @@ -37,10 +37,11 @@ def test_strategy_test_v2(result, fee): assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', - current_time=datetime.utcnow()) is True + current_time=datetime.utcnow(), side='long') is True 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', current_time=datetime.utcnow()) is True + # TODO-lev: Test for shorts? assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), current_rate=20_000, current_profit=0.05) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index d3c876782..a9334c616 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -12,6 +12,7 @@ from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.enums import SellType +from freqtrade.enums.signaltype import SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.optimize.space import SKDecimal from freqtrade.persistence import PairLocks, Trade @@ -20,38 +21,68 @@ from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, Categoric DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from tests.conftest import log_has, log_has_re +from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re -from .strats.strategy_test_v2 import StrategyTestV2 +from .strats.strategy_test_v3 import StrategyTestV3 # Avoid to reinit the same object again and again -_STRATEGY = StrategyTestV2(config={}) +_STRATEGY = StrategyTestV3(config={}) _STRATEGY.dp = DataProvider({}, None, None) -def test_returns_latest_signal(mocker, default_conf, ohlcv_history): +def test_returns_latest_signal(ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow() # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() - mocked_history['sell'] = 0 - mocked_history['buy'] = 0 - mocked_history.loc[1, 'sell'] = 1 + mocked_history['enter_long'] = 0 + mocked_history['exit_long'] = 0 + mocked_history['enter_short'] = 0 + mocked_history['exit_short'] = 0 + mocked_history.loc[1, 'exit_long'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None) - mocked_history.loc[1, 'sell'] = 0 - mocked_history.loc[1, 'buy'] = 1 + assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, True) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) + mocked_history.loc[1, 'exit_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None) - mocked_history.loc[1, 'sell'] = 0 - mocked_history.loc[1, 'buy'] = 0 + assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history + ) == (SignalDirection.LONG, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) + mocked_history.loc[1, 'exit_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 0 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None) - mocked_history.loc[1, 'sell'] = 0 - mocked_history.loc[1, 'buy'] = 1 - mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' + assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) + mocked_history.loc[1, 'exit_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 1 + mocked_history.loc[1, 'enter_tag'] = 'buy_signal_01' - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01') + assert _STRATEGY.get_entry_signal( + 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, 'buy_signal_01') + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) + + mocked_history.loc[1, 'exit_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 0 + mocked_history.loc[1, 'enter_short'] = 1 + mocked_history.loc[1, 'exit_short'] = 0 + mocked_history.loc[1, 'enter_tag'] = 'sell_signal_01' + + assert _STRATEGY.get_entry_signal( + 'ETH/BTC', '5m', mocked_history) == (SignalDirection.SHORT, 'sell_signal_01') + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (True, False) + + mocked_history.loc[1, 'enter_short'] = 0 + mocked_history.loc[1, 'exit_short'] = 1 + assert _STRATEGY.get_entry_signal( + 'ETH/BTC', '5m', mocked_history) == (None, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, True) def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): @@ -67,18 +98,18 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): assert log_has('Empty dataframe for pair ETH/BTC', caplog) -def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False, None) == _STRATEGY.get_signal( +def test_get_signal_empty(default_conf, caplog): + assert (None, None) == _STRATEGY.get_latest_candle( 'foo', default_conf['timeframe'], DataFrame() ) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) + assert (None, None) == _STRATEGY.get_latest_candle('bar', default_conf['timeframe'], None) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) caplog.clear() - assert (False, False, None) == _STRATEGY.get_signal( + assert (None, None) == _STRATEGY.get_latest_candle( 'baz', default_conf['timeframe'], DataFrame([]) @@ -86,7 +117,7 @@ def test_get_signal_empty(default_conf, mocker, caplog): assert log_has('Empty candle (OHLCV) data for pair baz', caplog) -def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history): +def test_get_signal_exception_valueerror(mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) mocker.patch.object( @@ -111,14 +142,14 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() - mocked_history['sell'] = 0 - mocked_history['buy'] = 0 - mocked_history.loc[1, 'buy'] = 1 + mocked_history['exit_long'] = 0 + mocked_history['enter_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 1 caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False, None) == _STRATEGY.get_signal( + assert (None, None) == _STRATEGY.get_latest_candle( 'xyz', default_conf['timeframe'], mocked_history @@ -134,13 +165,13 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history): mocked_history = ohlcv_history.copy() # Intentionally don't set sell column # mocked_history['sell'] = 0 - mocked_history['buy'] = 0 - mocked_history.loc[1, 'buy'] = 1 + mocked_history['enter_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 1 caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (True, False, None) == _STRATEGY.get_signal( + assert (SignalDirection.LONG, None) == _STRATEGY.get_entry_signal( 'xyz', default_conf['timeframe'], mocked_history @@ -148,7 +179,6 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history): def test_ignore_expired_candle(default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.ignore_buying_expired_candle_after = 60 @@ -195,8 +225,8 @@ def test_assert_df_raise(mocker, caplog, ohlcv_history): def test_assert_df(ohlcv_history, caplog): df_len = len(ohlcv_history) - 1 - ohlcv_history.loc[:, 'buy'] = 0 - ohlcv_history.loc[:, 'sell'] = 0 + ohlcv_history.loc[:, 'enter_long'] = 0 + ohlcv_history.loc[:, 'exit_long'] = 0 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) @@ -219,8 +249,8 @@ def test_assert_df(ohlcv_history, caplog): _STRATEGY.assert_df(None, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) with pytest.raises(StrategyError, - match="Buy column not set"): - _STRATEGY.assert_df(ohlcv_history.drop('buy', axis=1), len(ohlcv_history), + match="enter_long/buy column not set."): + _STRATEGY.assert_df(ohlcv_history.drop('enter_long', axis=1), len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) _STRATEGY.disable_dataframe_checks = True @@ -233,7 +263,6 @@ def test_assert_df(ohlcv_history, caplog): def test_advise_all_indicators(default_conf, testdatadir) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange.parse_timerange('1510694220-1510700340') @@ -244,7 +273,6 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None: def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') timerange = TimeRange.parse_timerange('1510694220-1510700340') @@ -262,7 +290,6 @@ def test_min_roi_reached(default_conf, fee) -> None: min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1}, {0: 0.1, 20: 0.05, 55: 0.01}] for roi in min_roi_list: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = roi trade = Trade( @@ -301,7 +328,6 @@ def test_min_roi_reached2(default_conf, fee) -> None: }, ] for roi in min_roi_list: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = roi trade = Trade( @@ -336,7 +362,6 @@ def test_min_roi_reached3(default_conf, fee) -> None: 30: 0.05, 55: 0.30, } - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = min_roi trade = Trade( @@ -389,8 +414,6 @@ def test_min_roi_reached3(default_conf, fee) -> None: def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, profit2, adjusted2, expected2, custom_stop) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) - strategy = StrategyResolver.load_strategy(default_conf) trade = Trade( pair='ETH/BTC', @@ -437,8 +460,6 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili def test_custom_sell(default_conf, fee, caplog) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) - strategy = StrategyResolver.load_strategy(default_conf) trade = Trade( pair='ETH/BTC', @@ -452,50 +473,84 @@ def test_custom_sell(default_conf, fee, caplog) -> None: ) now = arrow.utcnow().datetime - res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) + res = strategy.should_exit(trade, 1, now, + enter=False, exit_=False, + low=None, high=None) assert res.sell_flag is False assert res.sell_type == SellType.NONE strategy.custom_sell = MagicMock(return_value=True) - res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) + res = strategy.should_exit(trade, 1, now, + enter=False, exit_=False, + low=None, high=None) assert res.sell_flag is True assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_reason == 'custom_sell' strategy.custom_sell = MagicMock(return_value='hello world') - res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) + res = strategy.should_exit(trade, 1, now, + enter=False, exit_=False, + low=None, high=None) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True assert res.sell_reason == 'hello world' caplog.clear() strategy.custom_sell = MagicMock(return_value='h' * 100) - res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) + res = strategy.should_exit(trade, 1, now, + enter=False, exit_=False, + low=None, high=None) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True assert res.sell_reason == 'h' * 64 assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog) +@pytest.mark.parametrize('side', TRADE_SIDES) +def test_leverage_callback(default_conf, side) -> None: + default_conf['strategy'] = 'StrategyTestV2' + strategy = StrategyResolver.load_strategy(default_conf) + + assert strategy.leverage( + pair='XRP/USDT', + current_time=datetime.now(timezone.utc), + current_rate=2.2, + proposed_leverage=1.0, + max_leverage=5.0, + side=side, + ) == 1 + + default_conf['strategy'] = CURRENT_TEST_STRATEGY + strategy = StrategyResolver.load_strategy(default_conf) + assert strategy.leverage( + pair='XRP/USDT', + current_time=datetime.now(timezone.utc), + current_rate=2.2, + proposed_leverage=1.0, + max_leverage=5.0, + side=side, + ) == 3 + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) - buy_mock = MagicMock(side_effect=lambda x, meta: x) - sell_mock = MagicMock(side_effect=lambda x, meta: x) + entry_mock = MagicMock(side_effect=lambda x, meta: x) + exit_mock = MagicMock(side_effect=lambda x, meta: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_buy=buy_mock, - advise_sell=sell_mock, + advise_entry=entry_mock, + advise_exit=exit_mock, ) - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert entry_mock.call_count == 1 + assert entry_mock.call_count == 1 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -504,8 +559,8 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 2 - assert buy_mock.call_count == 2 - assert buy_mock.call_count == 2 + assert entry_mock.call_count == 2 + assert entry_mock.call_count == 2 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -513,16 +568,16 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) - buy_mock = MagicMock(side_effect=lambda x, meta: x) - sell_mock = MagicMock(side_effect=lambda x, meta: x) + entry_mock = MagicMock(side_effect=lambda x, meta: x) + exit_mock = MagicMock(side_effect=lambda x, meta: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_buy=buy_mock, - advise_sell=sell_mock, + advise_entry=entry_mock, + advise_exit=exit_mock, ) - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) strategy.dp = DataProvider({}, None, None) strategy.process_only_new_candles = True @@ -532,8 +587,8 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> assert 'close' in ret.columns assert isinstance(ret, DataFrame) assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert entry_mock.call_count == 1 + assert entry_mock.call_count == 1 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() @@ -541,20 +596,19 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert entry_mock.call_count == 1 + assert entry_mock.call_count == 1 # only skipped analyze adds buy and sell columns, otherwise it's all mocked - assert 'buy' in ret.columns - assert 'sell' in ret.columns - assert ret['buy'].sum() == 0 - assert ret['sell'].sum() == 0 + assert 'enter_long' in ret.columns + assert 'exit_long' in ret.columns + assert ret['enter_long'].sum() == 0 + assert ret['exit_long'].sum() == 0 assert not log_has('TA Analysis Launched', caplog) assert log_has('Skipping TA Analysis for already analyzed candle', caplog) @pytest.mark.usefixtures("init_persistence") def test_is_pair_locked(default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) PairLocks.timeframe = default_conf['timeframe'] PairLocks.use_db = True strategy = StrategyResolver.load_strategy(default_conf) diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 8b7505883..e18a3710b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -10,7 +10,7 @@ from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.interface import IStrategy -from tests.conftest import log_has, log_has_re +from tests.conftest import CURRENT_TEST_STRATEGY, log_has, log_has_re def test_search_strategy(): @@ -18,7 +18,7 @@ def test_search_strategy(): s, _ = StrategyResolver._search_object( directory=default_location, - object_name='StrategyTestV2', + object_name=CURRENT_TEST_STRATEGY, add_source=True, ) assert issubclass(s, IStrategy) @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 4 + assert len(strategies) == 5 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 5 + assert len(strategies) == 6 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 4 + assert len([x for x in strategies if x['class'] is not None]) == 5 assert len([x for x in strategies if x['class'] is None]) == 1 @@ -74,10 +74,10 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): - default_conf['strategy'] = 'StrategyTestV2' + default_conf['strategy'] = 'StrategyTestV3' extra_dir = Path.cwd() / 'some/path' with pytest.raises(OperationalException): - StrategyResolver._load_strategy('StrategyTestV2', config=default_conf, + StrategyResolver._load_strategy(CURRENT_TEST_STRATEGY, config=default_conf, extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) @@ -99,8 +99,10 @@ def test_load_strategy_noname(default_conf): StrategyResolver.load_strategy(default_conf) -def test_strategy(result, default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) +@pytest.mark.filterwarnings("ignore:deprecated") +@pytest.mark.parametrize('strategy_name', ['StrategyTestV2', 'TestStrategyLegacyV1']) +def test_strategy_pre_v3(result, default_conf, strategy_name): + default_conf.update({'strategy': strategy_name}) strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} @@ -117,17 +119,19 @@ def test_strategy(result, default_conf): df_indicators = strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators - dataframe = strategy.advise_buy(df_indicators, metadata=metadata) - assert 'buy' in dataframe.columns + dataframe = strategy.advise_entry(df_indicators, metadata=metadata) + assert 'buy' not in dataframe.columns + assert 'enter_long' in dataframe.columns - dataframe = strategy.advise_sell(df_indicators, metadata=metadata) - assert 'sell' in dataframe.columns + dataframe = strategy.advise_exit(df_indicators, metadata=metadata) + assert 'sell' not in dataframe.columns + assert 'exit_long' in dataframe.columns def test_strategy_override_minimal_roi(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'minimal_roi': { "20": 0.1, "0": 0.5 @@ -144,7 +148,7 @@ def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_stoploss(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'stoploss': -0.5 }) strategy = StrategyResolver.load_strategy(default_conf) @@ -156,7 +160,7 @@ def test_strategy_override_stoploss(caplog, default_conf): def test_strategy_override_trailing_stop(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'trailing_stop': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -169,7 +173,7 @@ def test_strategy_override_trailing_stop(caplog, default_conf): def test_strategy_override_trailing_stop_positive(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'trailing_stop_positive': -0.1, 'trailing_stop_positive_offset': -0.2 @@ -189,7 +193,7 @@ def test_strategy_override_timeframe(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'timeframe': 60, 'stake_currency': 'ETH' }) @@ -205,7 +209,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'process_only_new_candles': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -225,7 +229,7 @@ def test_strategy_override_order_types(caplog, default_conf): 'stoploss_on_exchange': True, } default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'order_types': order_types }) strategy = StrategyResolver.load_strategy(default_conf) @@ -239,12 +243,12 @@ def test_strategy_override_order_types(caplog, default_conf): " 'stoploss_on_exchange': True}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'order_types': {'buy': 'market'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV2'. " + match=r"Impossible to load Strategy '" + CURRENT_TEST_STRATEGY + "'. " r"Order-types mapping is incomplete."): StrategyResolver.load_strategy(default_conf) @@ -258,7 +262,7 @@ def test_strategy_override_order_tif(caplog, default_conf): } default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'order_time_in_force': order_time_in_force }) strategy = StrategyResolver.load_strategy(default_conf) @@ -271,20 +275,20 @@ def test_strategy_override_order_tif(caplog, default_conf): " {'buy': 'fok', 'sell': 'gtc'}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'order_time_in_force': {'buy': 'fok'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV2'. " - r"Order-time-in-force mapping is incomplete."): + match=f"Impossible to load Strategy '{CURRENT_TEST_STRATEGY}'. " + "Order-time-in-force mapping is incomplete."): StrategyResolver.load_strategy(default_conf) def test_strategy_override_use_sell_signal(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, }) strategy = StrategyResolver.load_strategy(default_conf) assert strategy.use_sell_signal @@ -294,7 +298,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): assert default_conf['use_sell_signal'] default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'use_sell_signal': False, }) strategy = StrategyResolver.load_strategy(default_conf) @@ -307,7 +311,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_sell_profit_only(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, }) strategy = StrategyResolver.load_strategy(default_conf) assert not strategy.sell_profit_only @@ -317,7 +321,7 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): assert not default_conf['sell_profit_only'] default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'sell_profit_only': True, }) strategy = StrategyResolver.load_strategy(default_conf) @@ -345,7 +349,7 @@ def test_deprecate_populate_indicators(result, default_conf): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) + strategy.advise_entry(indicators, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -354,7 +358,7 @@ def test_deprecate_populate_indicators(result, default_conf): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) + strategy.advise_exit(indicators, {'pair': 'ETH_BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -362,7 +366,7 @@ def test_deprecate_populate_indicators(result, default_conf): @pytest.mark.filterwarnings("ignore:deprecated") -def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): +def test_call_deprecated_function(result, default_conf, caplog): default_location = Path(__file__).parent / "strats" del default_conf['timeframe'] default_conf.update({'strategy': 'TestStrategyLegacyV1', @@ -382,19 +386,19 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - enterdf = strategy.advise_buy(result, metadata=metadata) + enterdf = strategy.advise_entry(result, metadata=metadata) assert isinstance(enterdf, DataFrame) - assert 'buy' in enterdf.columns + assert 'enter_long' in enterdf.columns - exitdf = strategy.advise_sell(result, metadata=metadata) + exitdf = strategy.advise_exit(result, metadata=metadata) assert isinstance(exitdf, DataFrame) - assert 'sell' in exitdf + assert 'exit_long' in exitdf assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.", caplog) -def test_strategy_interface_versioning(result, monkeypatch, default_conf): +def test_strategy_interface_versioning(result, default_conf): default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} @@ -409,10 +413,13 @@ def test_strategy_interface_versioning(result, monkeypatch, default_conf): assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - enterdf = strategy.advise_buy(result, metadata=metadata) + enterdf = strategy.advise_entry(result, metadata=metadata) assert isinstance(enterdf, DataFrame) - assert 'buy' in enterdf.columns - exitdf = strategy.advise_sell(result, metadata=metadata) + assert 'buy' not in enterdf.columns + assert 'enter_long' in enterdf.columns + + exitdf = strategy.advise_exit(result, metadata=metadata) assert isinstance(exitdf, DataFrame) - assert 'sell' in exitdf + assert 'sell' not in exitdf + assert 'exit_long' in exitdf diff --git a/tests/test_arguments.py b/tests/test_arguments.py index fca5c6ab9..c2ddaf0ff 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -7,6 +7,7 @@ import pytest from freqtrade.commands import Arguments from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive +from tests.conftest import CURRENT_TEST_STRATEGY # Parse common command-line-arguments. Used for all tools @@ -123,7 +124,7 @@ def test_parse_args_backtesting_custom() -> None: '-c', 'test_conf.json', '--ticker-interval', '1m', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'SampleStrategy' ] call_args = Arguments(args).get_parsed_arg() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 1ce45e4d5..e25cd800d 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -23,7 +23,8 @@ from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_ from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre -from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file +from tests.conftest import (CURRENT_TEST_STRATEGY, log_has, log_has_re, + patched_configuration_load_config_file) @pytest.fixture(scope="function") @@ -403,7 +404,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> arglist = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] args = Arguments(arglist).get_parsed_arg() @@ -440,7 +441,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non arglist = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--userdir', "/tmp/freqtrade", '--ticker-interval', '1m', @@ -497,7 +498,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non '--ticker-interval', '1m', '--export', 'trades', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategy' ] diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index bb9527011..71926f9b7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -234,7 +234,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, # stoploss shoud be hit assert freqtrade.handle_trade(trade) is not ignore_strat_sl if not ignore_strat_sl: - assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) + assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog) assert trade.sell_reason == SellType.STOP_LOSS.value @@ -427,7 +427,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: ) default_conf['stake_amount'] = 10 freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, enter_long=False) Trade.query = MagicMock() Trade.query.filter = MagicMock() @@ -648,9 +648,10 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: refresh_latest_ohlcv=refresh_mock, ) inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")]) - mocker.patch( - 'freqtrade.strategy.interface.IStrategy.get_signal', - return_value=(False, False, '') + mocker.patch.multiple( + 'freqtrade.strategy.interface.IStrategy', + get_exit_signal=MagicMock(return_value=(False, False)), + get_entry_signal=MagicMock(return_value=(None, None)) ) mocker.patch('time.sleep', return_value=None) @@ -1802,7 +1803,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi assert trade.is_open is True freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.open_order_id == limit_sell_order['id'] @@ -1830,7 +1831,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, ) freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, enter_long=True, exit_long=True) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() @@ -1849,7 +1850,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Buy and Sell are not triggering, so doing nothing ... - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, enter_long=False) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1857,7 +1858,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Buy and Sell are triggering, so doing nothing ... - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1865,7 +1866,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Sell is triggering, guess what : we are Selling! - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) trades = Trade.query.all() assert freqtrade.handle_trade(trades[0]) is True @@ -1899,7 +1900,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, # we might just want to check if we are in a sell condition without # executing # if ROI is reached we must sell - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI", caplog) @@ -1928,10 +1929,10 @@ def test_handle_trade_use_sell_signal(default_conf, ticker, limit_buy_order_open trade = Trade.query.first() trade.is_open = True - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=False) assert not freqtrade.handle_trade(trade) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) @@ -3058,7 +3059,7 @@ def test_sell_profit_only( trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is handle_first if handle_second: @@ -3095,7 +3096,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ trade = Trade.query.first() amnt = trade.amount trade.update(limit_buy_order) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) assert freqtrade.handle_trade(trade) is True @@ -3203,11 +3204,11 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trade) is False # Test if buy-signal is absent (should sell due to roi = true) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.ROI.value @@ -3484,11 +3485,11 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b trade = Trade.query.first() trade.update(limit_buy_order) # Sell due to min_roi_reached - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trade) is True # Test if buy-signal is absent - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -4016,7 +4017,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o freqtrade.wallets.update() assert trade.is_open is True - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0] diff --git a/tests/test_integration.py b/tests/test_integration.py index a3484d438..1395012d3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -72,7 +72,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, create_stoploss_order=MagicMock(return_value=True), _notify_exit=MagicMock(), ) - mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) + mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1000)) @@ -163,7 +163,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc SellCheckTuple(sell_type=SellType.NONE), SellCheckTuple(sell_type=SellType.NONE)] ) - mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) + mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) freqtrade = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtrade) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 51301a464..b2bd75db7 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -201,8 +201,8 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t timerange = TimeRange(None, 'line', 0, -1000) data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) - data['buy'] = 0 - data['sell'] = 0 + data['enter_long'] = 0 + data['exit_long'] = 0 indicators1 = [] indicators2 = [] @@ -261,12 +261,12 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir) buy = find_trace_in_fig_data(figure.data, "buy") assert isinstance(buy, go.Scatter) # All buy-signals should be plotted - assert int(data.buy.sum()) == len(buy.x) + assert int(data['enter_long'].sum()) == len(buy.x) sell = find_trace_in_fig_data(figure.data, "sell") assert isinstance(sell, go.Scatter) # All buy-signals should be plotted - assert int(data.sell.sum()) == len(sell.x) + assert int(data['exit_long'].sum()) == len(sell.x) assert find_trace_in_fig_data(figure.data, "Bollinger Band")