def test_CooldownPeriod(mocker, default_conf, fee, caplog):
    default_conf['protections'] = [{
        "method": "CooldownPeriod",
        "stop_duration": 60,
    }]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to .*"
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')

    assert not log_has_re(message, caplog)
    caplog.clear()

    Trade.query.session.add(generate_mock_trade(
        'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
        min_ago_open=200, min_ago_close=30,
    ))

    assert not freqtrade.protections.global_stop()
    assert freqtrade.protections.stop_per_pair('XRP/BTC')
    assert PairLocks.is_pair_locked('XRP/BTC')
    assert not PairLocks.is_global_lock()

    Trade.query.session.add(generate_mock_trade(
        'ETH/BTC', fee.return_value, False, sell_reason=SellType.ROI.value,
        min_ago_open=205, min_ago_close=35,
    ))

    assert not freqtrade.protections.global_stop()
    assert not PairLocks.is_pair_locked('ETH/BTC')
    assert freqtrade.protections.stop_per_pair('ETH/BTC')
    assert PairLocks.is_pair_locked('ETH/BTC')
    assert not PairLocks.is_global_lock()
Exemple #2
0
def test_PairLocks_getlongestlock(use_db):
    PairLocks.timeframe = '5m'
    # No lock should be present
    PairLocks.use_db = use_db
    if use_db:
        assert len(PairLock.query.all()) == 0

    assert PairLocks.use_db == use_db

    pair = 'ETH/BTC'
    assert not PairLocks.is_pair_locked(pair)
    PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
    # ETH/BTC locked for 4 minutes
    assert PairLocks.is_pair_locked(pair)
    lock = PairLocks.get_pair_longest_lock(pair)

    assert lock.lock_end_time.replace(
        tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=3)
    assert lock.lock_end_time.replace(
        tzinfo=timezone.utc) < arrow.utcnow().shift(minutes=14)

    PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=15).datetime)
    assert PairLocks.is_pair_locked(pair)

    lock = PairLocks.get_pair_longest_lock(pair)
    # Must be longer than above
    assert lock.lock_end_time.replace(
        tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=14)

    PairLocks.reset_locks()
    PairLocks.use_db = True
def test_LowProfitPairs(mocker, default_conf, fee, caplog):
    default_conf['protections'] = [{
        "method": "LowProfitPairs",
        "lookback_period": 400,
        "stop_duration": 60,
        "trade_limit": 2,
        "required_profit": 0.0,
    }]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to .*"
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')

    assert not log_has_re(message, caplog)
    caplog.clear()

    Trade.query.session.add(generate_mock_trade(
        'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
        min_ago_open=800, min_ago_close=450, profit_rate=0.9,
    ))

    # Not locked with 1 trade
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')
    assert not PairLocks.is_pair_locked('XRP/BTC')
    assert not PairLocks.is_global_lock()

    Trade.query.session.add(generate_mock_trade(
        'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
        min_ago_open=200, min_ago_close=120, profit_rate=0.9,
    ))

    # Not locked with 1 trade (first trade is outside of lookback_period)
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')
    assert not PairLocks.is_pair_locked('XRP/BTC')
    assert not PairLocks.is_global_lock()

    # Add positive trade
    Trade.query.session.add(generate_mock_trade(
        'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value,
        min_ago_open=20, min_ago_close=10, profit_rate=1.15,
    ))
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')
    assert not PairLocks.is_pair_locked('XRP/BTC')

    Trade.query.session.add(generate_mock_trade(
        'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
        min_ago_open=110, min_ago_close=20, profit_rate=0.8,
    ))

    # Locks due to 2nd trade
    assert not freqtrade.protections.global_stop()
    assert freqtrade.protections.stop_per_pair('XRP/BTC')
    assert PairLocks.is_pair_locked('XRP/BTC')
    assert not PairLocks.is_global_lock()
Exemple #4
0
    def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
        """
        Checks if a pair is currently locked
        The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
        and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap
        of 2 seconds for a buy to happen on an old signal.
        :param: pair: "Pair to check"
        :param candle_date: Date of the last candle. Optional, defaults to current date
        :returns: locking state of the pair in question.
        """

        if not candle_date:
            # Simple call ...
            return PairLocks.is_pair_locked(pair)
        else:
            lock_time = timeframe_to_next_date(self.timeframe, candle_date)
            return PairLocks.is_pair_locked(pair, lock_time)
def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair):
    default_conf['protections'] = [{
        "method": "StoplossGuard",
        "lookback_period": 60,
        "trade_limit": 2,
        "stop_duration": 60,
        "only_per_pair": only_per_pair
    }]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to .*"
    pair = 'XRP/BTC'
    assert not freqtrade.protections.stop_per_pair(pair)
    assert not freqtrade.protections.global_stop()
    assert not log_has_re(message, caplog)
    caplog.clear()

    Trade.query.session.add(generate_mock_trade(
        pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
        min_ago_open=200, min_ago_close=30, profit_rate=0.9,
        ))

    assert not freqtrade.protections.stop_per_pair(pair)
    assert not freqtrade.protections.global_stop()
    assert not log_has_re(message, caplog)
    caplog.clear()
    # This trade does not count, as it's closed too long ago
    Trade.query.session.add(generate_mock_trade(
        pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
        min_ago_open=250, min_ago_close=100, profit_rate=0.9,
    ))
    # Trade does not count for per pair stop as it's the wrong pair.
    Trade.query.session.add(generate_mock_trade(
        'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
        min_ago_open=240, min_ago_close=30, profit_rate=0.9,
    ))
    # 3 Trades closed - but the 2nd has been closed too long ago.
    assert not freqtrade.protections.stop_per_pair(pair)
    assert freqtrade.protections.global_stop() != only_per_pair
    if not only_per_pair:
        assert log_has_re(message, caplog)
    else:
        assert not log_has_re(message, caplog)

    caplog.clear()

    # 2nd Trade that counts with correct pair
    Trade.query.session.add(generate_mock_trade(
        pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
        min_ago_open=180, min_ago_close=30, profit_rate=0.9,
    ))

    assert freqtrade.protections.stop_per_pair(pair)
    assert freqtrade.protections.global_stop() != only_per_pair
    assert PairLocks.is_pair_locked(pair)
    assert PairLocks.is_global_lock() != only_per_pair
 def stop_per_pair(self, pair, now: Optional[datetime] = None) -> Optional[PairLock]:
     if not now:
         now = datetime.now(timezone.utc)
     result = None
     for protection_handler in self._protection_handlers:
         if protection_handler.has_local_stop:
             lock, until, reason = protection_handler.stop_per_pair(pair, now)
             if lock and until:
                 if not PairLocks.is_pair_locked(pair, until):
                     result = PairLocks.lock_pair(pair, until, reason, now=now)
     return result
def test_PairLocks_reason(use_db):
    PairLocks.timeframe = '5m'
    PairLocks.use_db = use_db
    # No lock should be present
    if use_db:
        assert len(PairLock.query.all()) == 0

    assert PairLocks.use_db == use_db

    PairLocks.lock_pair('XRP/USDT',
                        arrow.utcnow().shift(minutes=4).datetime, 'TestLock1')
    PairLocks.lock_pair('ETH/USDT',
                        arrow.utcnow().shift(minutes=4).datetime, 'TestLock2')

    assert PairLocks.is_pair_locked('XRP/USDT')
    assert PairLocks.is_pair_locked('ETH/USDT')

    PairLocks.unlock_reason('TestLock1')
    assert not PairLocks.is_pair_locked('XRP/USDT')
    assert PairLocks.is_pair_locked('ETH/USDT')

    PairLocks.reset_locks()
    PairLocks.use_db = True
    def backtest(self,
                 processed: Dict,
                 start_date: datetime,
                 end_date: datetime,
                 max_open_trades: int = 0,
                 position_stacking: bool = False,
                 enable_protections: bool = False) -> Dict[str, Any]:
        """
        Implement backtesting functionality

        NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
        Of course try to not have ugly code. By some accessor are sometime slower than functions.
        Avoid extensive logging in this method and functions it calls.

        :param processed: a processed dictionary with format {pair, data}, which gets cleared to
        optimize memory usage!
        :param start_date: backtesting timerange start datetime
        :param end_date: backtesting timerange end datetime
        :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
        :param position_stacking: do we allow position stacking?
        :param enable_protections: Should protections be enabled?
        :return: DataFrame with trades (results of backtesting)
        """
        trades: List[LocalTrade] = []
        self.prepare_backtest(enable_protections)

        # Use dict of lists with data for performance
        # (looping lists is a lot faster than pandas DataFrames)
        data: Dict = self._get_ohlcv_as_lists(processed)

        # Indexes per pair, so some pairs are allowed to have a missing start.
        indexes: Dict = defaultdict(int)
        tmp = start_date + timedelta(minutes=self.timeframe_min)

        open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
        open_trade_count = 0

        self.progress.init_step(
            BacktestState.BACKTEST,
            int((end_date - start_date) /
                timedelta(minutes=self.timeframe_min)))

        # Loop timerange and get candle for each pair at that point in time
        while tmp <= end_date:
            open_trade_count_start = open_trade_count
            self.check_abort()
            for i, pair in enumerate(data):
                row_index = indexes[pair]
                try:
                    # Row is treated as "current incomplete candle".
                    # Buy / sell signals are shifted by 1 to compensate for this.
                    row = data[pair][row_index]
                except IndexError:
                    # missing Data for one pair at the end.
                    # Warnings for this are shown during data loading
                    continue

                # Waits until the time-counter reaches the start of the data for this pair.
                if row[DATE_IDX] > tmp:
                    continue

                row_index += 1
                indexes[pair] = row_index
                self.dataprovider._set_dataframe_max_index(row_index)

                # without positionstacking, we can only have one open trade per pair.
                # max_open_trades must be respected
                # don't open on the last 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 not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
                    trade = self._enter_trade(pair, row)
                    if trade:
                        # TODO: hacky workaround to avoid opening > max_open_trades
                        # This emulates previous behaviour - not sure if this is correct
                        # Prevents buying if the trade-slot was freed in this candle
                        open_trade_count_start += 1
                        open_trade_count += 1
                        # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
                        open_trades[pair].append(trade)
                        LocalTrade.add_bt_trade(trade)

                for trade in list(open_trades[pair]):
                    # also check the buying candle for sell conditions.
                    trade_entry = self._get_sell_trade_entry(trade, row)
                    # Sell occurred
                    if trade_entry:
                        # logger.debug(f"{pair} - Backtesting sell {trade}")
                        open_trade_count -= 1
                        open_trades[pair].remove(trade)

                        LocalTrade.close_bt_trade(trade)
                        trades.append(trade_entry)
                        if enable_protections:
                            self.protections.stop_per_pair(pair, row[DATE_IDX])
                            self.protections.global_stop(tmp)

            # Move time one configured time_interval ahead.
            self.progress.increment()
            tmp += timedelta(minutes=self.timeframe_min)

        trades += self.handle_left_open(open_trades, data=data)
        self.wallets.update()

        results = trade_list_to_dataframe(trades)
        return {
            'results':
            results,
            'config':
            self.strategy.config,
            'locks':
            PairLocks.get_all_locks(),
            'rejected_signals':
            self.rejected_trades,
            'final_balance':
            self.wallets.get_total(self.strategy.config['stake_currency']),
        }
def test_MaxDrawdown(mocker, default_conf, fee, caplog):
    default_conf['protections'] = [{
        "method": "MaxDrawdown",
        "lookback_period": 1000,
        "stop_duration": 60,
        "trade_limit": 3,
        "max_allowed_drawdown": 0.15
    }]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to Max.*"

    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')
    caplog.clear()

    Trade.session.add(
        generate_mock_trade(
            'XRP/BTC',
            fee.return_value,
            False,
            sell_reason=SellType.STOP_LOSS.value,
            min_ago_open=1000,
            min_ago_close=900,
            profit_rate=1.1,
        ))
    Trade.session.add(
        generate_mock_trade(
            'ETH/BTC',
            fee.return_value,
            False,
            sell_reason=SellType.STOP_LOSS.value,
            min_ago_open=1000,
            min_ago_close=900,
            profit_rate=1.1,
        ))
    Trade.session.add(
        generate_mock_trade(
            'NEO/BTC',
            fee.return_value,
            False,
            sell_reason=SellType.STOP_LOSS.value,
            min_ago_open=1000,
            min_ago_close=900,
            profit_rate=1.1,
        ))
    # No losing trade yet ... so max_drawdown will raise exception
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')

    Trade.session.add(
        generate_mock_trade(
            'XRP/BTC',
            fee.return_value,
            False,
            sell_reason=SellType.STOP_LOSS.value,
            min_ago_open=500,
            min_ago_close=400,
            profit_rate=0.9,
        ))
    # Not locked with one trade
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')
    assert not PairLocks.is_pair_locked('XRP/BTC')
    assert not PairLocks.is_global_lock()

    Trade.session.add(
        generate_mock_trade(
            'XRP/BTC',
            fee.return_value,
            False,
            sell_reason=SellType.STOP_LOSS.value,
            min_ago_open=1200,
            min_ago_close=1100,
            profit_rate=0.5,
        ))

    # Not locked with 1 trade (2nd trade is outside of lookback_period)
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')
    assert not PairLocks.is_pair_locked('XRP/BTC')
    assert not PairLocks.is_global_lock()
    assert not log_has_re(message, caplog)

    # Winning trade ... (should not lock, does not change drawdown!)
    Trade.session.add(
        generate_mock_trade(
            'XRP/BTC',
            fee.return_value,
            False,
            sell_reason=SellType.ROI.value,
            min_ago_open=320,
            min_ago_close=410,
            profit_rate=1.5,
        ))
    assert not freqtrade.protections.global_stop()
    assert not PairLocks.is_global_lock()

    caplog.clear()

    # Add additional negative trade, causing a loss of > 15%
    Trade.session.add(
        generate_mock_trade(
            'XRP/BTC',
            fee.return_value,
            False,
            sell_reason=SellType.ROI.value,
            min_ago_open=20,
            min_ago_close=10,
            profit_rate=0.8,
        ))
    assert not freqtrade.protections.stop_per_pair('XRP/BTC')
    # local lock not supported
    assert not PairLocks.is_pair_locked('XRP/BTC')
    assert freqtrade.protections.global_stop()
    assert PairLocks.is_global_lock()
    assert log_has_re(message, caplog)
Exemple #10
0
def test_PairLocks(use_db):
    PairLocks.timeframe = '5m'
    # No lock should be present
    if use_db:
        assert len(PairLock.query.all()) == 0
    else:
        PairLocks.use_db = False

    assert PairLocks.use_db == use_db

    pair = 'ETH/BTC'
    assert not PairLocks.is_pair_locked(pair)
    PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
    # ETH/BTC locked for 4 minutes
    assert PairLocks.is_pair_locked(pair)

    # XRP/BTC should not be locked now
    pair = 'XRP/BTC'
    assert not PairLocks.is_pair_locked(pair)
    # Unlocking a pair that's not locked should not raise an error
    PairLocks.unlock_pair(pair)

    PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
    assert PairLocks.is_pair_locked(pair)

    # Get both locks from above
    locks = PairLocks.get_pair_locks(None)
    assert len(locks) == 2

    # Unlock original pair
    pair = 'ETH/BTC'
    PairLocks.unlock_pair(pair)
    assert not PairLocks.is_pair_locked(pair)
    assert not PairLocks.is_global_lock()

    pair = 'BTC/USDT'
    # Lock until 14:30
    lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)
    PairLocks.lock_pair(pair, lock_time)

    assert not PairLocks.is_pair_locked(pair)
    assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-10))
    assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-10))
    assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50))
    assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50))

    # Should not be locked after time expired
    assert not PairLocks.is_pair_locked(pair,
                                        lock_time + timedelta(minutes=10))

    locks = PairLocks.get_pair_locks(pair, lock_time + timedelta(minutes=-2))
    assert len(locks) == 1
    assert 'PairLock' in str(locks[0])

    # Unlock all
    PairLocks.unlock_pair(pair, lock_time + timedelta(minutes=-2))
    assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50))

    # Global lock
    PairLocks.lock_pair('*', lock_time)
    assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50))
    # Global lock also locks every pair seperately
    assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50))
    assert PairLocks.is_pair_locked('XRP/USDT',
                                    lock_time + timedelta(minutes=-50))

    if use_db:
        assert len(PairLock.query.all()) > 0
    else:
        # Nothing was pushed to the database
        assert len(PairLock.query.all()) == 0
    # Reset use-db variable
    PairLocks.use_db = True
Exemple #11
0
    def backtest(self,
                 processed: Dict,
                 stake_amount: float,
                 start_date: datetime,
                 end_date: datetime,
                 max_open_trades: int = 0,
                 position_stacking: bool = False,
                 enable_protections: bool = False) -> DataFrame:
        """
        Implement backtesting functionality

        NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
        Of course try to not have ugly code. By some accessor are sometime slower than functions.
        Avoid extensive logging in this method and functions it calls.

        :param processed: a processed dictionary with format {pair, data}
        :param stake_amount: amount to use for each trade
        :param start_date: backtesting timerange start datetime
        :param end_date: backtesting timerange end datetime
        :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
        :param position_stacking: do we allow position stacking?
        :param enable_protections: Should protections be enabled?
        :return: DataFrame with trades (results of backtesting)
        """
        logger.debug(
            f"Run backtest, stake_amount: {stake_amount}, "
            f"start_date: {start_date}, end_date: {end_date}, "
            f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
        )
        trades = []
        self.prepare_backtest(enable_protections)

        # Use dict of lists with data for performance
        # (looping lists is a lot faster than pandas DataFrames)
        data: Dict = self._get_ohlcv_as_lists(processed)

        # Indexes per pair, so some pairs are allowed to have a missing start.
        indexes: Dict = {}
        tmp = start_date + timedelta(minutes=self.timeframe_min)

        open_trades: Dict[str, List] = defaultdict(list)
        open_trade_count = 0

        # Loop timerange and get candle for each pair at that point in time
        while tmp <= end_date:
            open_trade_count_start = open_trade_count

            for i, pair in enumerate(data):
                if pair not in indexes:
                    indexes[pair] = 0

                try:
                    row = data[pair][indexes[pair]]
                except IndexError:
                    # missing Data for one pair at the end.
                    # Warnings for this are shown during data loading
                    continue

                # Waits until the time-counter reaches the start of the data for this pair.
                if row[DATE_IDX] > tmp:
                    continue
                indexes[pair] += 1

                # without positionstacking, we can only have one open trade per pair.
                # max_open_trades must be respected
                # don't open on the last row
                if ((position_stacking or len(open_trades[pair]) == 0)
                        and (max_open_trades <= 0
                             or open_trade_count_start < max_open_trades)
                        and tmp != end_date and row[BUY_IDX] == 1
                        and row[SELL_IDX] != 1
                        and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
                    # Enter trade
                    trade = Trade(
                        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,
                    )
                    # TODO: hacky workaround to avoid opening > max_open_trades
                    # This emulates previous behaviour - not sure if this is correct
                    # Prevents buying if the trade-slot was freed in this candle
                    open_trade_count_start += 1
                    open_trade_count += 1
                    # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
                    open_trades[pair].append(trade)
                    Trade.trades.append(trade)

                for trade in open_trades[pair]:
                    # since indexes has been incremented before, we need to go one step back to
                    # also check the buying candle for sell conditions.
                    trade_entry = self._get_sell_trade_entry(trade, row)
                    # Sell occured
                    if trade_entry:
                        # logger.debug(f"{pair} - Backtesting sell {trade}")
                        open_trade_count -= 1
                        open_trades[pair].remove(trade)
                        trades.append(trade_entry)
                        if enable_protections:
                            self.protections.stop_per_pair(pair, row[DATE_IDX])
                            self.protections.global_stop(tmp)

            # Move time one configured time_interval ahead.
            tmp += timedelta(minutes=self.timeframe_min)

        trades += self.handle_left_open(open_trades, data=data)

        return DataFrame.from_records(trades, columns=BacktestResult._fields)
Exemple #12
0
    def backtest(self,
                 processed: Dict,
                 start_date: datetime,
                 end_date: datetime,
                 max_open_trades: int = 0,
                 position_stacking: bool = False,
                 enable_protections: bool = False) -> Dict[str, Any]:
        """
        Implement backtesting functionality

        NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
        Of course try to not have ugly code. By some accessor are sometime slower than functions.
        Avoid extensive logging in this method and functions it calls.

        :param processed: a processed dictionary with format {pair, data}, which gets cleared to
        optimize memory usage!
        :param start_date: backtesting timerange start datetime
        :param end_date: backtesting timerange end datetime
        :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
        :param position_stacking: do we allow position stacking?
        :param enable_protections: Should protections be enabled?
        :return: DataFrame with trades (results of backtesting)
        """
        trades: List[LocalTrade] = []
        self.prepare_backtest(enable_protections)
        # Ensure wallets are uptodate (important for --strategy-list)
        self.wallets.update()
        # Use dict of lists with data for performance
        # (looping lists is a lot faster than pandas DataFrames)
        data: Dict = self._get_ohlcv_as_lists(processed)

        # Indexes per pair, so some pairs are allowed to have a missing start.
        indexes: Dict = defaultdict(int)
        current_time = start_date + timedelta(minutes=self.timeframe_min)

        open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
        open_trade_count = 0

        self.progress.init_step(
            BacktestState.BACKTEST,
            int((end_date - start_date) /
                timedelta(minutes=self.timeframe_min)))

        # Loop timerange and get candle for each pair at that point in time
        while current_time <= end_date:
            open_trade_count_start = open_trade_count
            self.check_abort()
            for i, pair in enumerate(data):
                row_index = indexes[pair]
                row = self.validate_row(data, pair, row_index, current_time)
                if not row:
                    continue

                row_index += 1
                indexes[pair] = row_index
                self.dataprovider._set_dataframe_max_index(row_index)

                # 1. Process buys.
                # without positionstacking, we can only have one open trade per pair.
                # max_open_trades must be respected
                # don't open on the last row
                if ((position_stacking or len(open_trades[pair]) == 0)
                        and self.trade_slot_available(max_open_trades,
                                                      open_trade_count_start)
                        and current_time != end_date and row[BUY_IDX] == 1
                        and row[SELL_IDX] != 1
                        and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
                    trade = self._enter_trade(pair, row)
                    if trade:
                        # TODO: hacky workaround to avoid opening > max_open_trades
                        # This emulates previous behavior - not sure if this is correct
                        # Prevents buying if the trade-slot was freed in this candle
                        open_trade_count_start += 1
                        open_trade_count += 1
                        # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
                        open_trades[pair].append(trade)

                for trade in list(open_trades[pair]):
                    # 2. Process buy orders.
                    order = trade.select_order('buy', is_open=True)
                    if order and self._get_order_filled(order.price, row):
                        order.close_bt_order(current_time)
                        trade.open_order_id = None
                        LocalTrade.add_bt_trade(trade)
                        self.wallets.update()

                    # 3. Create sell orders (if any)
                    if not trade.open_order_id:
                        self._get_sell_trade_entry(
                            trade, row)  # Place sell order if necessary

                    # 4. Process sell orders.
                    order = trade.select_order('sell', is_open=True)
                    if order and self._get_order_filled(order.price, row):
                        trade.open_order_id = None
                        trade.close_date = current_time
                        trade.close(order.price, show_msg=False)

                        # logger.debug(f"{pair} - Backtesting sell {trade}")
                        open_trade_count -= 1
                        open_trades[pair].remove(trade)
                        LocalTrade.close_bt_trade(trade)
                        trades.append(trade)
                        self.wallets.update()
                        self.run_protections(enable_protections, pair,
                                             current_time)

                    # 5. Cancel expired buy/sell orders.
                    if self.check_order_cancel(trade, current_time):
                        # Close trade due to buy timeout expiration.
                        open_trade_count -= 1
                        open_trades[pair].remove(trade)
                        self.wallets.update()

            # Move time one configured time_interval ahead.
            self.progress.increment()
            current_time += timedelta(minutes=self.timeframe_min)

        trades += self.handle_left_open(open_trades, data=data)
        self.wallets.update()

        results = trade_list_to_dataframe(trades)
        return {
            'results':
            results,
            'config':
            self.strategy.config,
            'locks':
            PairLocks.get_all_locks(),
            'rejected_signals':
            self.rejected_trades,
            'timedout_entry_orders':
            self.timedout_entry_orders,
            'timedout_exit_orders':
            self.timedout_exit_orders,
            'final_balance':
            self.wallets.get_total(self.strategy.config['stake_currency']),
        }