Source code for trader.strategies.rsi_revert

"""Mean reversion on RSI: buy oversold, exit when it recovers."""

from collections.abc import Sequence
from dataclasses import dataclass
from typing import ClassVar

from trader.domain import Bar, Position
from trader.indicators import rsi
from trader.strategies.base import Action, Signal

__all__ = ["RsiRevertStrategy"]


[docs] @dataclass(frozen=True, slots=True) class RsiRevertStrategy: """Buy when RSI falls below `oversold`; sell when it rises above `overbought`.""" id: str period: int = 14 oversold: float = 30.0 overbought: float = 70.0 # 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: # RSI's first defined value sits at index `period`, so `period + 1` bars. return self.period + 1
[docs] def evaluate(self, bars: Sequence[Bar], position: Position | None) -> Signal: series = rsi(bars, self.period) value = series[-1] if series else None if value is None: return Signal(Action.HOLD, "RSI not yet defined") indicators = {"rsi": value, "close": bars[-1].close} if position is None and value < self.oversold: return Signal( Action.BUY, f"RSI {value:.1f} below oversold threshold {self.oversold:g}", indicators, ) if position is not None and value > self.overbought: return Signal( Action.SELL, f"RSI {value:.1f} above overbought threshold {self.overbought:g}", indicators, ) # This branch is reached from two structurally different states, and the # reason has to say which — it used to read "RSI {value} within # thresholds", which is false whenever the *other* threshold is the one # being crossed. Measured live 2026-08-11: a flat PLTR at RSI 72.2 # recorded "RSI 72.2 within thresholds". The behaviour was right (no # position, so the exit cannot fire) but `decisions.reasoning` is the one # record of *why* nothing happened, and it was asserting something untrue. if position is None: return Signal( Action.HOLD, f"Flat, and RSI {value:.1f} is not below the oversold entry " f"threshold {self.oversold:g}", indicators, ) return Signal( Action.HOLD, f"Holding, and RSI {value:.1f} is not above the overbought exit " f"threshold {self.overbought:g}", indicators, )