trader.strategies.macd module¶
MACD trend-confirmed momentum, with an optional per-bar stop-loss.
Found while surveying ../backtesting (issue #35, 2026-08-20): MACD-based strategies were the best-performing entries in that repo’s own grid-search database. This ports two variants together, since the second is a strict superset of the first rather than a separate strategy type.
Core signal (standard 12/26/9 MACD):
MACD line = EMA(close, fast) - EMA(close, slow).
Signal line = EMA(MACD line, signal_period) – an EMA of a derived series, not of the closes themselves.
momentum_positive = MACD > signal AND signal > 0.
momentum_negative = MACD < signal AND signal < 0.
Trend filter: an N-period SMA of the close. trend_upward = close > MA; trend_downward = close < MA.
Buy: momentum_positive AND trend_upward. Sell: momentum_negative AND trend_downward. The MA trend filter is a confirmation gate on top of a bare MACD crossover, not a second independent signal – dropping it entirely is a different, out-of-scope variant (issue #35’s “Out of scope”).
Optional stop-loss, a per-bar exit check independent of the MACD/MA signal above: sell if close < entry_price * (1 - risk) OR close < previous_close * (1 - risk), where risk is stop_loss_pct. It is a modifier on this strategy’s signal, not a separate strategy type, and it is checked before the MACD/trend logic and can fire even while those indicators are still warming up – a held position needs protection from the first bar it exists, not only once warmup_bars() worth of history has accumulated.
Ships mode: shadow (see config/strategies.yaml); Decimal throughout, no pandas/numpy crossing into strategies/, matching every other rule strategy in this package.
Issue #109 (`requirements.md` §4.1): this strategy used to hand-roll its own EMA/MACD recursion internally rather than going through trader/indicators/ at all – the exact “same indicator, reinvented twice” gap the issue found. It now calls trader.indicators.macd, which delegates to talib.MACD (see that module for the boundary-conversion details). The underlying algorithm is unchanged (an SMA-seeded EMA-of-EMA-difference, the same construction the old hand-rolled _ema/_macd_and_signal used); only the engine computing it moved. One real, new constraint from that move: talib.MACD requires fast_period/slow_period/signal_period each >= 2 (a period of 1 is undefined for an EMA difference), enforced in __post_init__ below with this strategy’s own ConfigError, rather than surfacing as talib’s undecorated exception the first time evaluate() runs.
- class trader.strategies.macd.MacdStrategy(id, fast_period=12, slow_period=26, signal_period=9, trend_period=50, stop_loss_pct=None)[source]¶
Bases:
objectMACD momentum, confirmed by a moving-average trend filter, with an optional per-bar stop-loss.
See the module docstring for the buy/sell rule and the stop-loss semantics.
- Parameters:
id (str)
fast_period (int)
slow_period (int)
signal_period (int)
trend_period (int)
stop_loss_pct (Decimal | None)
- id: str¶
- fast_period: int¶
- slow_period: int¶
- signal_period: int¶
- trend_period: int¶
- stop_loss_pct: Decimal | None¶
- backtestable: ClassVar[bool] = True¶