Skip to content

research.research_strategies.bollinger_band_mean_reversion.runner

research.research_strategies.bollinger_band_mean_reversion.runner

build_config

build_config() -> RunConfig[YfinanceConfig]

Builds the backtest configuration for a Bollinger Band mean-reversion parameter sweep on AAPL daily bars, 2000-2026.

Source code in src\contango\research\research_strategies\bollinger_band_mean_reversion\runner.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def build_config() -> RunConfig[YfinanceConfig]:
    """
    Builds the backtest configuration for a Bollinger Band mean-reversion
    parameter sweep on AAPL daily bars, 2000-2026.
    """
    ticker = "AAPL"
    interval = Interval.DAY_1
    start = datetime(2000, 1, 1)
    end = datetime(2026, 1, 1)

    initial_cash = 1_000
    initial_position = 0
    fill_behavior = FillBehavior.INSTANT  # fills orders immediately at the bar's price, no partial fills
    slippage = 0.001                      # 0.1% price slippage applied per fill, to simulate imperfect execution
    commission_per_unit = 0.0             # fee per share bought

    start_unix_ms = int(start.timestamp() * 1000)
    end_unix_ms = int(end.timestamp() * 1000)

    # ResearchRunner will grid-search every combination of these values,
    # instantiating BollingerBandMeanReversion(**params) for each one and
    # running a full backtest per combination. Total runs = product of list
    # lengths below (11 * 5 * 4 * 1 = 220 backtests in this case).
    param_space: dict[str, list[int | float | str]] = {
        # Lookback window (in bars) used to compute the Bollinger Band's
        # moving average and standard deviation. Swept from 5 to 25 in
        # steps of 2 -> [5, 7, 9, ..., 25].
        "bollinger_bands_period": list(range(5, 26, 2)),

        # Band width, in standard deviations, above/below the moving
        # average. Smaller values -> tighter bands -> more frequent,
        # lower-conviction signals. Larger values -> wider bands -> rarer signals.
        "bollinger_bands_stdev": [0.5, 1.0, 1.5, 2.0, 2.5],

        # Fraction of available cash committed to each buy signal.
        # 1.0 means trade 100% of cash on every entry; lower values leave cash
        # in portfolio (less risky, as shown when graphing).
        "allocation": [0.25, 0.5, 0.75, 1.0],

        # Held fixed at a single value here since this run only trades
        # one ticker, but keeping it in param_space keeps the strategy
        # constructor signature uniform for the grid search.
        "symbol": [ticker],
    }

    return RunConfig[YfinanceConfig](
        broker=Yfinance(NYSECalendar()),
        broker_config=YfinanceConfig(ticker, interval, start_unix_ms, end_unix_ms),
        strategy_factory=BollingerBandMeanReversion,
        param_space=param_space,
        initial_cash=initial_cash,
        initial_position=initial_position,
        fill_behavior=fill_behavior,
        slippage=slippage,
        commission_per_unit=commission_per_unit,
    )