def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame], timerange: TimeRange): self.progress.init_step(BacktestState.ANALYZE, 0) logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() # Use max_open_trades in backtesting, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): # Must come from strategy config, as the strategy may modify this setting. max_open_trades = self.strategy.config['max_open_trades'] else: logger.info( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...' ) max_open_trades = 0 # need to reprocess data every time to populate signals preprocessed = self.strategy.advise_all_indicators(data) # Trim startup period from analyzed dataframe preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup) if not preprocessed_tmp: raise OperationalException( "No data left after adjusting for startup candles.") # Use preprocessed_tmp for date generation (the trimmed dataframe). # Backtesting will re-trim the dataframes after buy/sell signal generation. min_date, max_date = history.get_timerange(preprocessed_tmp) logger.info( f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days).') # Execute backtest and store results results = self.backtest( processed=preprocessed, start_date=min_date, end_date=max_date, max_open_trades=max_open_trades, position_stacking=self.config.get('position_stacking', False), enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), }) self.all_results[self.strategy.get_strategy_name()] = results return min_date, max_date
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() # Use max_open_trades in backtesting, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): # Must come from strategy config, as the strategy may modify this setting. max_open_trades = self.strategy.config['max_open_trades'] else: logger.info( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...' ) max_open_trades = 0 # need to reprocess data every time to populate signals preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe( df, timerange, startup_candles=self.required_startup) min_date, max_date = history.get_timerange(preprocessed) logger.info( f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days)..') # Execute backtest and store results results = self.backtest( processed=preprocessed, start_date=min_date.datetime, end_date=max_date.datetime, max_open_trades=max_open_trades, position_stacking=self.config.get('position_stacking', False), enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), } return min_date, max_date
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return None min_stake_amount = self.exchange.get_min_pair_stake_amount( pair, row[OPEN_IDX], -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() 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) stake_amount = self.wallets._validate_stake_amount( pair, stake_amount, min_stake_amount) if not stake_amount: return None order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['sell'] # 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()): 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 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], open_date=row[DATE_IDX].to_pydatetime(), stake_amount=stake_amount, amount=round(stake_amount / row[OPEN_IDX], 8), 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', ) return trade return None
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() # Use max_open_trades in backtesting, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): # Must come from strategy config, as the strategy may modify this setting. max_open_trades = self.strategy.config['max_open_trades'] else: logger.info( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...') max_open_trades = 0 # need to reprocess data every time to populate signals preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe for pair in list(preprocessed): df = preprocessed[pair] df = trim_dataframe(df, timerange, startup_candles=self.required_startup) if len(df) > 0: preprocessed[pair] = df else: logger.warning(f'{pair} has no data left after adjusting for startup candles, ' f'skipping.') del preprocessed[pair] if not preprocessed: raise OperationalException( "No data left after adjusting for startup candles.") min_date, max_date = history.get_timerange(preprocessed) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days).') # Execute backtest and store results results = self.backtest( processed=preprocessed, start_date=min_date, end_date=max_date, max_open_trades=max_open_trades, position_stacking=self.config.get('position_stacking', False), enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), }) self.all_results[self.strategy.get_strategy_name()] = results return min_date, max_date
def analyze_pair(self, pair: str) -> None: """ Fetch data for this pair from dataprovider and analyze. Stores the dataframe into the dataprovider. The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`. :param pair: Pair to analyze. """ if not self.dp: raise OperationalException("DataProvider not found.") dataframe = self.dp.ohlcv(pair, self.timeframe) if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning('Empty candle (OHLCV) data for pair %s', pair) return try: df_len, df_close, df_date = self.preserve_df(dataframe) dataframe = strategy_safe_wrapper(self._analyze_ticker_internal, message="")(dataframe, { 'pair': pair }) self.assert_df(dataframe, df_len, df_close, df_date) except StrategyError as error: logger.warning( f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}" ) return if dataframe.empty: logger.warning('Empty dataframe for pair %s', pair) return
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['sell'] # 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): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], open_date=row[DATE_IDX], stake_amount=stake_amount, amount=round(stake_amount / row[OPEN_IDX], 8), fee_open=self.fee, fee_close=self.fee, is_open=True, exchange='backtesting', ) return trade return None
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['sell'] if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, sell_reason=sell.sell_type.value): return None trade.close(closerate, show_msg=False) return trade return None
def test_strategy_safe_wrapper_error(caplog, error): def failing_method(): raise error('This is an error.') def working_method(argumentpassedin): return argumentpassedin with pytest.raises(StrategyError, match=r'This is an error.'): strategy_safe_wrapper(failing_method, message='DeadBeef')() assert log_has_re(r'DeadBeef.*', caplog) ret = strategy_safe_wrapper(failing_method, message='DeadBeef', default_retval=True)() assert isinstance(ret, bool) assert ret
def test_strategy_safe_wrapper(value): def working_method(argumentpassedin): return argumentpassedin ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value) assert type(ret) == type(value) assert ret == value
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators :param pair: pair in format ANT/BTC :param interval: Interval to use (in min) :param dataframe: Dataframe to analyze :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning('Empty candle (OHLCV) data for pair %s', pair) return False, False try: df_len, df_close, df_date = self.preserve_df(dataframe) dataframe = strategy_safe_wrapper( self._analyze_ticker_internal, message="" )(dataframe, {'pair': pair}) self.assert_df(dataframe, df_len, df_close, df_date) except StrategyError as error: logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}") return False, False if dataframe.empty: logger.warning('Empty dataframe for pair %s', pair) return False, False latest_date = dataframe['date'].max() latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] # Explicitly convert to arrow object to ensure the below comparison does not fail latest_date = arrow.get(latest_date) # Check if dataframe is out of date interval_minutes = timeframe_to_minutes(interval) offset = self.config.get('exchange', {}).get('outdated_offset', 5) if latest_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', pair, (arrow.utcnow() - latest_date).seconds // 60 ) return False, False (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug( 'trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell) ) return buy, sell
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]) if sell.sell_flag: trade.close_date = sell_candle_time trade_dur = int( (trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['sell'] if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, sell_reason=sell.sell_reason, current_time=sell_candle_time): return None trade.sell_reason = sell.sell_reason # Checks and adds an exit tag, after checking that the length of the # sell_row has the length for an exit tag column if (len(sell_row) > EXIT_TAG_IDX and sell_row[EXIT_TAG_IDX] is not None and len(sell_row[EXIT_TAG_IDX]) > 0): trade.sell_reason = sell_row[EXIT_TAG_IDX] trade.close(closerate, show_msg=False) return trade return None
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple ) -> LocalTrade: current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, row[OPEN_IDX], -0.1) max_stake = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], current_profit=current_profit, min_stake=min_stake, max_stake=max_stake) # Check if we should increase our position if stake_amount is not None and stake_amount > 0.0: pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade) if pos_trade is not None: self.wallets.update() return pos_trade return trade
def ft_check_timed_out(self, side: str, trade: Trade, order: Dict, current_time: datetime) -> bool: """ FT Internal method. Check if timeout is active, and if the order is still open and timed out """ timeout = self.config.get('unfilledtimeout', {}).get(side) ordertime = arrow.get(order['datetime']).datetime if timeout is not None: timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') timeout_kwargs = {timeout_unit: -timeout} timeout_threshold = current_time + timedelta(**timeout_kwargs) timedout = (order['status'] == 'open' and order['side'] == side and ordertime < timeout_threshold) if timedout: return True time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout return strategy_safe_wrapper(time_method, default_retval=False)( pair=trade.pair, trade=trade, order=order, current_time=current_time)
def _get_sell_trade_entry_for_candle( self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: # Check if we need to adjust our current positions if self.strategy.position_adjustment_enable: trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) 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]) if sell.sell_flag: trade.close_date = sell_candle_time trade_dur = int( (trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): # Custom exit pricing only for sell-signals closerate = strategy_safe_wrapper( self.strategy.custom_exit_price, default_retval=closerate)(pair=trade.pair, trade=trade, current_time=sell_row[DATE_IDX], proposed_rate=closerate, current_profit=current_profit) # Use the maximum between close_rate and low as we cannot sell outside of a candle. closerate = min(max(closerate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['sell'] if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, sell_reason=sell.sell_reason, current_time=sell_candle_time): return None trade.sell_reason = sell.sell_reason # Checks and adds an exit tag, after checking that the length of the # sell_row has the length for an exit tag column if (len(sell_row) > EXIT_TAG_IDX and sell_row[EXIT_TAG_IDX] is not None and len(sell_row[EXIT_TAG_IDX]) > 0): trade.sell_reason = sell_row[EXIT_TAG_IDX] trade.close(closerate, show_msg=False) return trade return None
def _enter_trade( self, pair: str, row: Tuple, stake_amount: Optional[float] = None, trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: # let's call the custom entry price, using the open price as default price propose_rate = strategy_safe_wrapper( self.strategy.custom_entry_price, default_retval=row[OPEN_IDX])( pair=pair, current_time=row[DATE_IDX].to_pydatetime(), proposed_rate=row[OPEN_IDX]) # default value is the open rate # Move rate to within the candle's low/high rate propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX]) min_stake_amount = self.exchange.get_min_pair_stake_amount( pair, propose_rate, -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() pos_adjust = trade is not None if not pos_adjust: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return trade 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=propose_rate, proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) stake_amount = self.wallets.validate_stake_amount( pair, stake_amount, min_stake_amount) if not stake_amount: # In case of pos adjust, still return the original trade # If not pos adjust, trade is None return trade order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['sell'] # Confirm trade entry: if not pos_adjust: if not strategy_safe_wrapper( self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): amount = round(stake_amount / propose_rate, 8) if trade is None: # Enter trade has_buy_tag = len(row) >= BUY_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=propose_rate, open_date=row[DATE_IDX].to_pydatetime(), stake_amount=stake_amount, amount=amount, 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', orders=[]) order = Order(ft_is_open=False, ft_pair=trade.pair, symbol=trade.pair, ft_order_side="buy", side="buy", order_type="market", status="closed", price=propose_rate, average=propose_rate, amount=amount, filled=amount, cost=stake_amount + trade.fee_open) trade.orders.append(order) if pos_adjust: trade.recalc_trade_from_orders() return trade
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return None # let's call the custom entry price, using the open price as default price propose_rate = strategy_safe_wrapper( self.strategy.custom_entry_price, default_retval=row[OPEN_IDX])( pair=pair, current_time=row[DATE_IDX].to_pydatetime(), proposed_rate=row[OPEN_IDX]) # default value is the open rate # Move rate to within the candle's low/high rate propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX]) min_stake_amount = self.exchange.get_min_pair_stake_amount( pair, propose_rate, -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() 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=propose_rate, proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) stake_amount = self.wallets.validate_stake_amount( pair, stake_amount, min_stake_amount) if not stake_amount: return None order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['sell'] # 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=propose_rate, time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): 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 trade = LocalTrade( pair=pair, open_rate=propose_rate, open_date=row[DATE_IDX].to_pydatetime(), stake_amount=stake_amount, amount=round(stake_amount / propose_rate, 8), 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', ) return trade return None
def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not :param current_profit: current profit as ratio """ stop_loss_value = force_stoploss if force_stoploss else self.stoploss # 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: stop_loss_value = strategy_safe_wrapper( self.custom_stoploss, default_retval=None)(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, current_profit=current_profit, dataframe=dataframe) # Sanity check - error cases will return None if stop_loss_value: # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") trade.adjust_stop_loss(current_rate, stop_loss_value) else: logger.warning( "CustomStoploss function did not return valid stoploss") if self.trailing_stop: # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. 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. if not (self.trailing_only_offset_is_reached and high_profit < sl_offset): # Specific handling for trailing_stop_positive if self.trailing_stop_positive is not None and high_profit > sl_offset: stop_loss_value = self.trailing_stop_positive logger.debug( f"{trade.pair} - Using positive stoploss: {stop_loss_value} " f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%" ) trade.adjust_stop_loss(high or current_rate, stop_loss_value) # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. if ((trade.stop_loss >= current_rate) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS # If initial stoploss is not the same as current one then it is trailing. if trade.initial_stop_loss != trade.stop_loss: sell_type = SellType.TRAILING_STOP_LOSS logger.debug( f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, " f"stoploss is {trade.stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"trade opened at {trade.open_rate:.6f}") logger.debug( f"{trade.pair} - Trailing stop saved " f"{trade.stop_loss - trade.initial_stop_loss:.6f}") return SellCheckTuple(sell_type=sell_type) return SellCheckTuple(sell_type=SellType.NONE)
def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell has been reached, which can either be a stop-loss, ROI or sell-signal. :param low: Only used during backtesting to simulate stoploss :param high: Only used during backtesting, to simulate ROI :param force_stoploss: Externally provided stoploss :return: True if trade should be sold, False otherwise """ # Set current rate to low for backtesting sell current_rate = low or rate current_profit = trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate) stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) ask_strategy = self.config.get('ask_strategy', {}) # if buy signal and ignore_roi is set, we don't need to evaluate min_roi. roi_reached = ( not (buy and ask_strategy.get('ignore_roi_if_buy_signal', False)) and self.min_roi_reached( trade=trade, current_profit=current_profit, current_time=date)) sell_signal = SellType.NONE custom_reason = '' if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal pass elif ask_strategy.get('use_sell_signal', True) and not buy: if sell: sell_signal = SellType.SELL_SIGNAL else: custom_reason = strategy_safe_wrapper( self.custom_sell, default_retval=False)(pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, current_profit=current_profit, dataframe=dataframe) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: logger.warning( f'Custom sell reason returned from custom_sell is too ' f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} ' f'characters.') custom_reason = custom_reason[: CUSTOM_SELL_MAX_LENGTH] else: custom_reason = None # TODO: return here if sell-signal should be favored over ROI # Start evaluations # Sequence: # ROI (if not stoploss) # Sell-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: logger.debug( f"{trade.pair} - Required profit reached. sell_type=SellType.ROI" ) return SellCheckTuple(sell_type=SellType.ROI) if sell_signal != SellType.NONE: logger.debug( f"{trade.pair} - Sell signal received. " f"sell_type=SellType.{sell_signal.name}" + (f", custom_reason={custom_reason}" if custom_reason else "")) return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: logger.debug( f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}" ) return stoplossflag # This one is noisy, commented out... # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE)
def _get_sell_trade_entry_for_candle( self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: # Check if we need to adjust our current positions if self.strategy.position_adjustment_enable: check_adjust_buy = True if self.strategy.max_entry_position_adjustment > -1: count_of_buys = trade.nr_of_successful_buys check_adjust_buy = ( count_of_buys <= self.strategy.max_entry_position_adjustment) if check_adjust_buy: trade = self._get_adjust_trade_entry_for_candle( trade, sell_row) 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]) if sell.sell_flag: trade.close_date = sell_candle_time trade_dur = int( (trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) try: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) except ValueError: return None # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) order_type = self.strategy.order_types['sell'] if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): # Custom exit pricing only for sell-signals if order_type == 'limit': closerate = strategy_safe_wrapper( self.strategy.custom_exit_price, default_retval=closerate)( pair=trade.pair, trade=trade, current_time=sell_candle_time, proposed_rate=closerate, current_profit=current_profit) # We can't place orders lower than current low. # freqtrade does not support this in live, and the order would fill immediately closerate = max(closerate, sell_row[LOW_IDX]) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['sell'] if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, sell_reason=sell.sell_reason, current_time=sell_candle_time): return None trade.sell_reason = sell.sell_reason # Checks and adds an exit tag, after checking that the length of the # sell_row has the length for an exit tag column if (len(sell_row) > EXIT_TAG_IDX and sell_row[EXIT_TAG_IDX] is not None and len(sell_row[EXIT_TAG_IDX]) > 0): trade.sell_reason = sell_row[EXIT_TAG_IDX] self.order_id_counter += 1 order = Order( id=self.order_id_counter, ft_trade_id=trade.id, order_date=sell_candle_time, order_update_date=sell_candle_time, ft_is_open=True, ft_pair=trade.pair, order_id=str(self.order_id_counter), symbol=trade.pair, ft_order_side="sell", side="sell", order_type=order_type, status="open", price=closerate, average=closerate, amount=trade.amount, filled=0, remaining=trade.amount, cost=trade.amount * closerate, ) trade.orders.append(order) return trade return None
def _enter_trade( self, pair: str, row: Tuple, stake_amount: Optional[float] = None, trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: current_time = row[DATE_IDX].to_pydatetime() entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None # let's call the custom entry price, using the open price as default price order_type = self.strategy.order_types['buy'] propose_rate = row[OPEN_IDX] if order_type == 'limit': propose_rate = strategy_safe_wrapper( self.strategy.custom_entry_price, default_retval=row[OPEN_IDX])( pair=pair, current_time=current_time, proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate # We can't place orders higher than current high (otherwise it'd be a stop limit buy) # which freqtrade does not support in live. propose_rate = min(propose_rate, row[HIGH_IDX]) min_stake_amount = self.exchange.get_min_pair_stake_amount( pair, propose_rate, -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() pos_adjust = trade is not None if not pos_adjust: try: stake_amount = self.wallets.get_trade_stake_amount( pair, None, update=False) except DependencyException: return None stake_amount = strategy_safe_wrapper( self.strategy.custom_stake_amount, default_retval=stake_amount)(pair=pair, current_time=current_time, current_rate=propose_rate, proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount, entry_tag=entry_tag) stake_amount = self.wallets.validate_stake_amount( pair, stake_amount, min_stake_amount) if not stake_amount: # In case of pos adjust, still return the original trade # If not pos adjust, trade is None return trade time_in_force = self.strategy.order_time_in_force['buy'] # Confirm trade entry: if not pos_adjust: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, time_in_force=time_in_force, current_time=current_time, entry_tag=entry_tag): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): self.order_id_counter += 1 amount = round(stake_amount / propose_rate, 8) if trade is None: # Enter trade self.trade_id_counter += 1 trade = LocalTrade(id=self.trade_id_counter, open_order_id=self.order_id_counter, pair=pair, open_rate=propose_rate, open_rate_requested=propose_rate, open_date=current_time, stake_amount=stake_amount, amount=amount, amount_requested=amount, fee_open=self.fee, fee_close=self.fee, is_open=True, buy_tag=entry_tag, exchange='backtesting', orders=[]) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) order = Order( id=self.order_id_counter, ft_trade_id=trade.id, ft_is_open=True, ft_pair=trade.pair, order_id=str(self.order_id_counter), symbol=trade.pair, ft_order_side="buy", side="buy", order_type=order_type, status="open", order_date=current_time, order_filled_date=current_time, order_update_date=current_time, price=propose_rate, average=propose_rate, amount=amount, filled=0, remaining=amount, cost=stake_amount + trade.fee_open, ) if pos_adjust and self._get_order_filled(order.price, row): order.close_bt_order(current_time) else: trade.open_order_id = str(self.order_id_counter) trade.orders.append(order) trade.recalc_trade_from_orders() return trade