backtesting.py icon indicating copy to clipboard operation
backtesting.py copied to clipboard

Unable to compute indicator values in the next method

Open lgrawet opened this issue 3 weeks ago • 4 comments

Expected behavior

Hi,

I'm trying to compute the following ichimoku indicator in next(), everything is fine if I do it in init(). The error is :

ValueError: Indicators must return (optionally a tuple of) numpy.arrays of same length as `data` (data shape: (2,); indicator "ichimoku" shape: (2, 5), returned value: [[nan nan nan nan nan]
 [nan nan nan nan nan]])

It looks like the culprit is line 152 in backtesting.py https://github.com/kernc/backtesting.py/blob/1af1dda0e9f31fa3123ae179bc71c001b6b487f1/backtesting/backtesting.py#L152-L154

Code sample

The ichimoku indicator and required midprice method:

    def ichimoku(self, high, low, close):
        df = pd.DataFrame()

        # Calculate indicators
        tenkan_sen = self.midprice(high=high, low=low, length=self.tenkan)
        kijun_sen = self.midprice(high=high, low=low, length=self.kijun)
        span_a = 0.5 * (tenkan_sen + kijun_sen)
        span_b = self.midprice(high=high, low=low, length=self.senkou)

        span_a = span_a.shift(self.kijun)
        span_b = span_b.shift(self.kijun)
        chikou_span = close.shift(-self.kijun)

        # Append Ichimoku indicators to main df
        df[f"ISA_{self.tenkan}"] = span_a
        df[f"ISB_{self.kijun}"] = span_b
        df[f"ITS_{self.tenkan}"] = tenkan_sen
        df[f"IKS_{self.kijun}"] = kijun_sen
        df[f"ICS_{self.kijun}"] = chikou_span
        
        return df.to_numpy().T[[0, 1, 2, 3, 4]]

    def midprice(self, high: pd.Series, low: pd.Series, length: int = 2) -> pd.Series:
        lowest_low = low.rolling(length, min_periods=length).min()
        highest_high = high.rolling(length, min_periods=length).max()
        midprice = 0.5 * (lowest_low + highest_high)
        
        return midprice

Actual behavior

ValueError: Indicators must return (optionally a tuple of) numpy.arrays of same length as `data` (data shape: (2,); indicator "ichimoku" shape: (2, 5), returned value: [[nan nan nan nan nan]
 [nan nan nan nan nan]])

Additional info, steps to reproduce, full crash traceback, screenshots

Just call the indicator in next()

    def next(self):
        im = self.I(self.ichimoku, high=self.data.High.s, low=self.data.Low.s, close=self.data.Close.s, overlay=True, plot=False, name="ichimoku")
    ...

Software versions

  • backtesting 0.6.5
  • pandas 2.3.3
  • numpy 2.3.4
  • bokeh 3.8.0
  • OS: Debian 13 amd64

lgrawet avatar Nov 26 '25 10:11 lgrawet

The code is assuming length of data will be greater than the number of indicators returned. I 'd say this needs a bit stricter condition check.

kernc avatar Nov 26 '25 23:11 kernc

Well I don't start with one candle with my bots in real life. I start with an history of 2*senkou = 104 candles for standard ichimoku. Starting with one candle makes many indicators throwing an error and stops the backtesting. Allowing a customisable candle history would solve two problem at once.

Thanks a lot!

lgrawet avatar Nov 27 '25 08:11 lgrawet

Does this mean changing the condition heuristic to something like:

# Assuming no more than 10 indicator lines are computed at once
if is_arraylike and np.argmax(value.shape) == 0 and len(self._data) > 10:

would work in your case or no?

How about:

if is_arraylike and value.shape[0] == len(self._data):

?

kernc avatar Nov 27 '25 16:11 kernc

# Assuming no more than 10 indicator lines are computed at once
if is_arraylike and np.argmax(value.shape) == 0 and len(self._data) > 10:

I suppose this would solve the original issue but

if is_arraylike and value.shape[0] == len(self._data):

makes more sense to me as we want to ensure indicator returns the same number of rows as self._data

lgrawet avatar Nov 27 '25 21:11 lgrawet