Skip to main content
Many trading strategies combine signals from multiple time frames — for example, a weekly trend filter with daily entry signals. backtesting.py supports this via resample_apply() from backtesting.lib, which handles resampling, indicator application, and back-alignment in a single call.

The core concept

You always pass the lowest time frame data to Backtest. To use a higher time frame, you resample up, apply an indicator, then forward-fill the values back down to the original frequency. The critical detail is label='right': each resampled bar is labeled with the end of its period, not the start. This ensures the indicator value for a given week is only available after that week closes — preventing look-ahead bias.

resample_apply()

from backtesting.lib import resample_apply

resample_apply(rule, func, series, *args, agg=None, **kwargs)
ParameterDescription
rulePandas offset string: 'W-FRI', 'D', '4H', 'ME', etc.
funcIndicator function to apply on the resampled series
seriesA Strategy.data.* array or pd.Series with a DatetimeIndex
*argsExtra positional arguments forwarded to func
aggAggregation function for resampling. Defaults to OHLCV_AGG for DataFrames, or the appropriate entry from OHLCV_AGG for named Series, otherwise 'last'
**kwargsExtra keyword arguments forwarded to func (except plot, overlay, color, scatter which go to self.I())
When called from inside Strategy.init(), the result is automatically wrapped with self.I(), so it is plotted and managed like any other indicator.

Complete example: weekly RSI with daily MAs

This strategy uses RSI computed on daily and weekly close prices, plus four daily moving averages, to decide when to enter and exit long trades:
import pandas as pd
from backtesting import Strategy, Backtest
from backtesting.lib import resample_apply
from backtesting.test import GOOG


def SMA(array, n):
    """Simple moving average."""
    return pd.Series(array).rolling(n).mean()


def RSI(array, n):
    """Relative strength index (approximate)."""
    gain = pd.Series(array).diff()
    loss = gain.copy()
    gain[gain < 0] = 0
    loss[loss > 0] = 0
    rs = gain.ewm(n).mean() / loss.abs().ewm(n).mean()
    return 100 - 100 / (1 + rs)


class System(Strategy):
    d_rsi = 30   # Daily RSI lookback
    w_rsi = 30   # Weekly RSI lookback
    level = 70   # RSI entry threshold

    def init(self):
        # Daily moving averages — standard self.I() usage
        self.ma10  = self.I(SMA, self.data.Close, 10)
        self.ma20  = self.I(SMA, self.data.Close, 20)
        self.ma50  = self.I(SMA, self.data.Close, 50)
        self.ma100 = self.I(SMA, self.data.Close, 100)

        # Daily RSI computed on the original (daily) time frame
        self.daily_rsi = self.I(RSI, self.data.Close, self.d_rsi)

        # Weekly RSI: resample daily Close to weekly (Friday end),
        # apply RSI, then forward-fill back to daily index.
        # resample_apply() handles all of this automatically.
        self.weekly_rsi = resample_apply(
            'W-FRI', RSI, self.data.Close, self.w_rsi)

    def next(self):
        price = self.data.Close[-1]

        if (not self.position
                and self.daily_rsi[-1]  > self.level
                and self.weekly_rsi[-1] > self.level
                and self.weekly_rsi[-1] > self.daily_rsi[-1]
                and self.ma10[-1] > self.ma20[-1] > self.ma50[-1] > self.ma100[-1]
                and price > self.ma10[-1]):
            # Enter long with an 8% stop-loss
            self.buy(sl=.92 * price)

        elif price < .98 * self.ma10[-1]:
            self.position.close()


backtest = Backtest(GOOG, System, commission=.002)
stats = backtest.run()
print(stats)

How look-ahead bias is prevented

Internally, resample_apply() resamples with label='right':
resampled = series.resample(rule, label='right').agg(agg).dropna()
This labels each weekly bar with the Friday close date (for 'W-FRI'), not the Monday open. The resampled indicator is then forward-filled back to the original daily index:
result = result.reindex(
    index=series.index.union(resampled.index),
    method='ffill'
).reindex(series.index)
The net effect: on any given daily bar, self.weekly_rsi[-1] reflects the RSI value of the last completed week, never the current in-progress week.

Manual equivalent

For complete transparency, here is what resample_apply('D', SMA, self.data.Close, 10) does internally when working with hourly data:
class System(Strategy):
    def init(self):
        # 1. Get closing prices as a pandas Series with DatetimeIndex
        close = self.data.Close.s

        # 2. Resample to daily; label='right' prevents look-ahead bias
        daily = close.resample('D', label='right').agg('last')

        # 3. Apply indicator on the resampled series,
        #    then reindex back to the original hourly frequency
        def daily_sma(series, n):
            from backtesting.test import SMA
            return SMA(series, n).reindex(close.index).ffill()

        # 4. Wrap in self.I() so it is plotted and properly managed
        self.sma = self.I(daily_sma, daily, 10, plot=False)
Using resample_apply() reduces all of the above to a single line.

OHLCV aggregation

When resampling a full OHLCV DataFrame rather than a single Series, use the OHLCV_AGG dictionary for correct aggregation:
from backtesting.lib import OHLCV_AGG

# Manually resample a DataFrame
weekly_df = data.resample('W-FRI', label='right').agg(OHLCV_AGG).dropna()
OHLCV_AGG is defined as:
OHLCV_AGG = {
    'Open':   'first',
    'High':   'max',
    'Low':    'min',
    'Close':  'last',
    'Volume': 'sum',
}
This is the default when resample_apply() receives a DataFrame or a Series named after one of these columns.

Common Pandas offset strings

RuleMeaning
'D'Calendar day
'W-FRI'Week ending Friday
'ME'Month end
'QE'Quarter end
'4H'4-hour bars
'2W-MON'Bi-weekly, ending Monday
Always pass data in the finest time frame your strategy uses. If you need both hourly and daily signals, pass hourly OHLCV data and use resample_apply('D', ...) to compute daily indicators.
resample_apply() requires that series has a DatetimeIndex. If your data uses a plain integer index, the function will raise an assertion error.

Build docs developers (and LLMs) love