Status: BUILT & TESTED (observe-only by default; not yet trading — requires
valid paper keys + EQMOM_TRADE_ENABLED=1).
Source edge: reports/momentum_spectrum.md + reports/momentum_liquid.md.
The single edge that survived the 2026-08 three-bug audit (timezone / look-ahead
/ overlapping-returns). Long-only cross-sectional momentum, OOS 2016–2024 Sharpe
1.44–1.69 (full universe) / 1.25 (liquid subset), bootstrap p < 0.001.
1. Strategy
- Universe: curated liquid large-cap list (
UNIVERSE, 101 names inproduction/equity_momentum_runner.py). Deterministic and auditable. Future enhancement: replace with a point-in-time S&P 500 membership list (seedocs/ROADMAP.mdP3d) to remove any survivorship tilt. - Signal: trailing 6-month (126 trading-day) return of each name's
completed daily close.
ret = close[-1] / close[-1-126] - 1. - Selection: rank eligible names (≥200 days history) by
ret; hold the top decile (10% of universe), equal-weight. - No shorting. Gross long notional =
equity × EQMOM_EQUITY_FRACTION(default 0.95). This is a long-only book — the short leg that killed the long-short factor is simply omitted. - Rebalance: every ~21 calendar days (sticky hold between, matching the
research
hold=21). State trackslast_rebalance_date; non-rebalance decision cycles are HOLD (positions unchanged, no orders).
2. Timing & idempotency (house convention)
- Decision runs post-US-close (16:20 ET) on completed bars only. Orders fill at/near the next open (market order placed after close), matching the backtest's next-open fills.
- One decision per calendar date (
last_decision_dateguard →ALREADY_DONE). - Rebalances are spaced ~21 days apart; between them the book is held.
- All cycles are idempotent and use only data available before the entry.
3. Safety (fail-closed)
Inherited from production/etf_trend_runner.py and the codebase's paper-only
design:
require_paper_env()refuses to start unlessP_LIVE_TRADING=FALSE,P_MODE≠LIVE,ALPACA_BASE_URLis exactly the paper domain, andP_TRADE_MODE ∈ {PAPER_OBSERVE, PAPER_TRADE}.- Default mode
PAPER_OBSERVE: the daily cycle runs fully and logs decisions, but never submits orders. - Orders are gated behind a second switch
EQMOM_TRADE_ENABLED=1AND globalP_TRADE_MODE=PAPER_TRADE. Enabling paper trading globally does NOT make this sleeve trade until the switch is set. - Never forces close on execution discrepancy; respects
configexec limits.
4. Deploy
# observe-only smoke test (no orders, no keys needed for logic; needs valid
# keys only to read account/positions from Alpaca)
python -m production.equity_momentum_runner --once --force
# register / launch with the bot manager (uses .env; observe until enabled)
python start_bots.py --status # confirms 'eqmom' is registered
python start_bots.py # starts eqmom (observe) among the live set
To enable paper trading (after regenerating the dead main .env keys per
docs/ROADMAP.md incident 2026-08-22):
- Set
P_TRADE_MODE=PAPER_TRADEin.env. - Set
EQMOM_TRADE_ENABLED=1(e.g. in theeqmomentry ofstart_bots.py, or export it). - Verify a 32 s startup (
python start_bots.py --status) shows[ OK ].
5. Env knobs
| Var | Default | Meaning |
|---|---|---|
EQMOM_EQUITY_FRACTION | 0.95 | fraction of account equity used as gross long |
EQMOM_TRADE_ENABLED | unset | "1" allows orders when global mode is PAPER_TRADE |
EQMOM_LOOKBACK | 126 | trailing-return window (trading days) |
EQMOM_TOP_DECILE | 0.10 | fraction of universe held (top decile) |
EQMOM_REBALANCE_DAYS | 21 | calendar days between rebalances |
6. Artifacts
- State:
data/state/equity_momentum_state.json - Decision log:
logs/equity_momentum_decisions.jsonl - Bot log:
logs/bots/eqmom.log(viastart_bots.py)
7. Tests
tests/test_equity_momentum_runner.py (4 cases) verifies:
- top-decile selection by trailing return (correct direction, equal weights);
- short-history names are excluded;
- full observe-mode
run_oncewrites state + decision log and never callssubmit_market_order; - idempotency (
ALREADY_DONE) and the between-rebalanceHOLDpath.
8. Open items before live trading
- Keys: main
.envAlpaca keys are dead (401, ~Aug 19) — regenerate. - Costs (RESOLVED in research): the discovery backtest was gross/fee-free
with same-day fills, but
validation/momentum_realistic.pynow re-runs the long-only top-N on the full delisting-inclusive ~500-name universe with correct next-open execution timing AND turnover costs on a grid (5/10/25/50 bps) + a liquidity-tiered cost (top 30% $-volume → 5 bps, mid 40% → 25 bps, bottom 30% → 50 bps). Result: even at 50 bps or tiered, net OOS Sharpe stays ≥ 1.0 (full universe Net@10 ≈ 1.23–1.38, Tiered ≈ 1.19–1.31), OOS p = 0.000. Turnover is heavy (~85% of gross per monthly rebalance ≈ 10×/yr notional) but the edge absorbs it. The 101-name liquid production universe drags less. Useconfig.P_FEE_BPS_PAPER(15 bps) as a conservative floor in sizing; for modeling use 10 bps flat as the base case and 25 bps as stress. No separate slippage model yet — 15–25 bps total is a safe planning buffer. - Universe: promote to a PIT S&P 500 list for a cleaner, larger book.
- Risk: confirm the ~41% maxDD from research is acceptable for the account size; consider a volatility target like the ETF sleeve if not.