Beispiel #1
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
    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']),
        }
Beispiel #3
0
def test_PairLocks(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

    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:
        locks = PairLocks.get_all_locks()
        locks_db = PairLock.query.all()
        assert len(locks) == len(locks_db)
        assert len(locks_db) > 0
    else:
        # Nothing was pushed to the database
        assert len(PairLocks.get_all_locks()) > 0
        assert len(PairLock.query.all()) == 0
    # Reset use-db variable
    PairLocks.reset_locks()
    PairLocks.use_db = True
Beispiel #4
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']),
        }