Exemple #1
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']),
        }
    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']),
        }
Exemple #3
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: List[Trade] = []
        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 trade_list_to_dataframe(trades)