Пример #1
0
    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
Пример #2
0
    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
Пример #3
0
    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
Пример #4
0
    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
Пример #5
0
    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
Пример #6
0
    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
Пример #7
0
    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
Пример #8
0
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
Пример #9
0
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
Пример #10
0
    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
Пример #11
0
    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
Пример #12
0
    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
Пример #13
0
    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)
Пример #14
0
    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
Пример #15
0
    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
Пример #16
0
    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
Пример #17
0
    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)
Пример #18
0
    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)
Пример #19
0
    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
Пример #20
0
    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