Skip to content

What (One-liner)

Single-name momentum backtest: ROC(period=fast_period) > 0 → buy, < 0 → sell. Backtrader event-driven + real yfinance history + 10bps commission. Returns total_return / Sharpe / max_drawdown / win_rate / normalized equity curve.

How to use

Drag momentum_strategy_v1 onto the Canvas, wire an upstream price_factor_v1 node (or fill symbol) + optional fast_period=10 (ROC window). Date range defaults to the last 365 days.

Typical wiring:

upstream price_factor → momentum_strategy_v1 (fast_period=10) → portfolio_optimization

                                                  risk_management_v1

Core formulas

# Strategy (Backtrader event-driven):
if ROC(fast_period) > 0 and flat: .buy()     # default size = 1 share
if ROC(fast_period) < 0 and long: .sell()

# Post-run metrics:
total_return       = (final_equity - 100k) / 100k       [round 4]
annualized_return  = (1 + total_return) ** (252/bars)-1 [round 4]
sharpe_ratio       = mean(daily_rtn)/std * √252         [round 4]
max_drawdown       = max((peak - eq)/peak)              [round 4]
win_rate           = won/total on exits                 [round 4]
equity_curve       = [round(v/100k, 6) for v in vals]   [normalized]
                   step = len//200 if len>200 else 1 (gotcha: ≤400 → 1)

Rounding contract: metrics → 4; equity_curve → 6.

Assumptions & applicability

Assumptions: daily frequency + ≥30 bars + long-only + $100k capital + 10bps commission + default .buy() size=1 share + no slippage + yfinance auto_adjust=True.

Fits: single-name momentum backtests, Canvas strategy-graph overlays, post-institution-decision direction adjustment, teaching demos.

Does not fit: intraday / shorting / multi-name portfolios / fine-sized production / pre-2000 data.

Input / Output contract

{symbol?, start_date?, end_date?, fast_period?=10, slow_period?=30 (unused), data_source?='yfinance'}{symbol, strategy, start_date, end_date, total_return, annualized_return, sharpe_ratio, max_drawdown, win_rate, num_trades, equity_curve, bars, data_source, error?}

Canvas additionally wraps signal (long_bias/neutral/short_bias), fast_ma, roc_lookback.

Known limitations

  1. Default .buy() size = 1 share: $100k capital × a ~$100 position → total_return barely reflects alpha (current-state lock; a fix needs a sizer)
  2. slow_period silently ignored by MomentumStrategy (kept for UI, no effect)
  3. Long-only: negative ROC only closes
  4. Sharpe without risk-free deduction
  5. Max drawdown peak-trough only; no underwater duration
  6. Equity-curve downsample bug: bars ∈ (200, 400] → step = 1 → no actual downsampling (only kicks in > 400)
  7. yfinance auto_adjust=True fixed (adjusted prices can shift ROC trigger timing)
  8. akshare only for symbol='GOLD'

References

Jegadeesh-Titman (1993) cross-sectional momentum + Moskowitz-Ooi-Pedersen (2012) time-series momentum + AQR trend-following reviews. Full citations in the frontmatter.

Golden Test

tests/golden/fixtures/tier2/momentum_strategy/monkey-patched fetch_price_data + 300-day synthetic OHLC (seed=42), 1e-10 tolerance, 8 tests:

  • matches_snapshot — full output byte-equal
  • determinism — 5 reruns byte-identical
  • result_structure — 13 fixed top-level keys
  • rounding_contract — metrics round 4, equity_curve round 6
  • equity_curve_downsampling_contract — locks the downsample bug's current behaviour (len=290 stays 290)
  • win_rate_bounds — ∈ [0, 1]
  • fetch_error_handled — fetch failure → error field set, no raise
  • insufficient_data — < 30 bars → error "insufficient data"

Changelog

  • 1.0.0 (2026-04-20) — First Active (Tier 2 batch 7)

Verifiable intelligence for the decisions that demand scrutiny.