trader.strategies.resistance_breakout module

Resistance-cluster breakout: a validated-band alternative to Turtle’s naive N-day-high/low channel (issue #36).

Ported from ../moirai’s unmerged n_hit_resistance_breakthrough (sup_res.py, branch learn-graphing-prediction) – a research prototype that was never wired into moirai’s own framework, but whose construction is a real refinement over TurtleStrategy’s Donchian channel:

  1. Find swing highs: a candle whose High equals the rolling max over a CENTERED alpha-bar window (a fractal detector – local_extrema in the source), never the current/last bar, which has no future bars to confirm it with.

  2. Take the n most recent swing highs and compute their mean High.

  3. VALIDATE the level is real: if any of those n highs deviates from the mean by more than half the configured band width, the highs are too scattered to call a real resistance level – no signal, regardless of where the price sits. This is the piece Turtle’s single N-day extreme cannot express at all: Turtle’s channel is real by construction (it is just “the highest high”), so it has no way to say “there have been highs near here, but they don’t agree with each other.”

  4. Otherwise the resistance band is [mean - half_width, mean + half_width].

  5. BREAKOUT: the latest close is the first one past band_high + buffer since the band’s own most recent swing high – never a re-break. A candle that already closed past the threshold earlier (while the level was already valid) means today’s close above it is old news, not a new signal; Turtle’s channel has no equivalent guard and fires again on every bar the price happens to stay above the old high.

Only the resistance (upside) side existed in the source. The downside/exit half – swing LOWS, a support band, and a breakDOWN threshold – is built here by mirroring the same construction onto Bar.low, so this strategy has an entry/exit shape comparable to Turtle’s entry_period/exit_period channel: BUY on a fresh resistance breakout while flat, SELL on a fresh support breakdown while holding.

Design decision not specified by the source: `region_width` and `breakout_buffer` are expressed here as PERCENTAGES OF THE CLUSTER’S OWN MEAN (`region_width_pct`, `breakout_buffer_pct`), not the source’s raw absolute-price parameters. The source was a single-symbol research prototype; this app trades symbols from roughly $1 to $500 in the same configured universe, and one fixed dollar band cannot be simultaneously sane for both ends of that range. A mean-relative percentage is the same scale-free convention this codebase already uses elsewhere for exactly this reason (trail_pct/floor_pct on TrailingStopFloorStrategy, reuse_price_band_pct on LlmStrategy).

`n`, `alpha`, `region_width_pct`, and `breakout_buffer_pct` are conservative starting values, not validated ones – the source algorithm was never tuned against a real dataset. See config/strategies.yaml for the reasoning behind each chosen default; a future backtest sweep (the same kind docs/ideas.md already flags rsi_revert’s oversold needed) should revisit all four before this strategy is ever considered for mode: trading.

Ships mode: shadow. turtle_20_10 is untouched – this is an additional, comparable alternative, not a replacement, so the shadow-portfolio simulator (issue #33) can measure which construction actually performs better.

class trader.strategies.resistance_breakout.ResistanceBreakoutStrategy(id, n=3, alpha=5, region_width_pct=Decimal('2.0'), breakout_buffer_pct=Decimal('0.5'))[source]

Bases: object

BUY on a fresh resistance-cluster breakout while flat; SELL on a fresh support-cluster breakdown while holding. See the module docstring for the full algorithm and the reasoning behind the default parameters.

Parameters:
  • id (str)

  • n (int)

  • alpha (int)

  • region_width_pct (Decimal)

  • breakout_buffer_pct (Decimal)

id: str
n: int

Swing points required before a resistance/support level is considered real. 3 is the classic technical-analysis minimum – 2 touches is easily coincidental.

alpha: int

Width (in bars) of the centered window a candle must be the extreme of to count as a swing point; alpha // 2 bars of context on each side. 5 needs 2 bars either side: enough to reject single-bar noise without an unrealistically long confirmation lag.

region_width_pct: Decimal

Total resistance/support band width, as a percent of the cluster’s own mean (see the module docstring for why this is relative, not the source’s absolute region_width). 2.0% is a conservative starting guess.

breakout_buffer_pct: Decimal

How far past the band edge a close must sit before it counts as a breakout/breakdown, as a percent of the cluster’s own mean. 0.5% is a conservative starting guess – enough to filter a single noisy tick at the band edge without requiring a large move.

backtestable: ClassVar[bool] = True
warmup_bars()[source]
Return type:

int

evaluate(bars, position)[source]
Parameters:
Return type:

Signal