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_v1Core 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
- Default .buy() size = 1 share: $100k capital × a ~$100 position → total_return barely reflects alpha (current-state lock; a fix needs a sizer)
slow_periodsilently ignored by MomentumStrategy (kept for UI, no effect)- Long-only: negative ROC only closes
- Sharpe without risk-free deduction
- Max drawdown peak-trough only; no underwater duration
- Equity-curve downsample bug: bars ∈ (200, 400] → step = 1 → no actual downsampling (only kicks in > 400)
- yfinance auto_adjust=True fixed (adjusted prices can shift ROC trigger timing)
- 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-equaldeterminism— 5 reruns byte-identicalresult_structure— 13 fixed top-level keysrounding_contract— metrics round 4, equity_curve round 6equity_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 raiseinsufficient_data— < 30 bars → error "insufficient data"
Changelog
- 1.0.0 (2026-04-20) — First Active (Tier 2 batch 7)

