Source code for trader.strategies.bollinger_revert
"""Mean reversion on Bollinger bands: buy the lower band, exit at the mean."""
from collections.abc import Sequence
from dataclasses import dataclass
from typing import ClassVar
from trader.domain import Bar, Position
from trader.indicators import bollinger
from trader.strategies.base import Action, Signal
__all__ = ["BollingerRevertStrategy"]
[docs]
@dataclass(frozen=True, slots=True)
class BollingerRevertStrategy:
"""Buy below the lower band; sell once price recovers to the middle band.
Exiting at the middle rather than the upper band is deliberate: this is a
reversion-to-the-mean trade, and the mean is the target.
"""
id: str
period: int = 20
num_std: int = 2
# See TurtleStrategy: `Strategy.backtestable`'s Protocol default is not
# structurally inherited, so the attribute must actually exist here.
# `ClassVar` keeps it out of the generated `__init__`/`__repr__`/`__eq__`.
backtestable: ClassVar[bool] = True
[docs]
def warmup_bars(self) -> int:
return self.period
[docs]
def evaluate(self, bars: Sequence[Bar], position: Position | None) -> Signal:
bands = bollinger(bars, self.period, self.num_std)
band = bands[-1] if bands else None
if band is None:
return Signal(Action.HOLD, "Bollinger bands not yet defined")
close = bars[-1].close
indicators = {
"close": close,
"upper": band.upper,
"middle": band.middle,
"lower": band.lower,
}
if position is None and close < band.lower:
return Signal(
Action.BUY, f"Close {close} below lower band {band.lower}", indicators
)
if position is not None and close >= band.middle:
return Signal(
Action.SELL,
f"Close {close} recovered to middle band {band.middle}",
indicators,
)
# Two structurally different states reach this branch, so the reason has
# to say which. It used to read "Close {close} inside the bands", which is
# false when the close is outside the band the *other* branch watches.
# Measured live 2026-08-11: a flat ITRG at close 2.6000 against an upper
# band of 2.5921 — above it — reported "inside the bands". Same defect as
# `rsi_revert`'s, fixed the same way.
if position is None:
return Signal(
Action.HOLD,
f"Flat, and close {close} is not below the lower band {band.lower}",
indicators,
)
return Signal(
Action.HOLD,
f"Holding, and close {close} has not recovered to the middle band "
f"{band.middle}",
indicators,
)