Skip to content
Skip to content
← all research
REJECTED

"Production Spec — Equity Momentum Sleeve (`eqmom`)"

2026-08-26

·

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 in production/equity_momentum_runner.py). Deterministic and auditable. Future enhancement: replace with a point-in-time S&P 500 membership list (see docs/ROADMAP.md P3d) 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 tracks last_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_date guard → 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 unless P_LIVE_TRADING=FALSE, P_MODE≠LIVE, ALPACA_BASE_URL is exactly the paper domain, and P_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=1 AND global P_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 config exec 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):

  1. Set P_TRADE_MODE=PAPER_TRADE in .env.
  2. Set EQMOM_TRADE_ENABLED=1 (e.g. in the eqmom entry of start_bots.py, or export it).
  3. Verify a 32 s startup (python start_bots.py --status) shows [ OK ].

#5. Env knobs

VarDefaultMeaning
EQMOM_EQUITY_FRACTION0.95fraction of account equity used as gross long
EQMOM_TRADE_ENABLEDunset"1" allows orders when global mode is PAPER_TRADE
EQMOM_LOOKBACK126trailing-return window (trading days)
EQMOM_TOP_DECILE0.10fraction of universe held (top decile)
EQMOM_REBALANCE_DAYS21calendar 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 (via start_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_once writes state + decision log and never calls submit_market_order;
  • idempotency (ALREADY_DONE) and the between-rebalance HOLD path.

#8. Open items before live trading

  1. Keys: main .env Alpaca keys are dead (401, ~Aug 19) — regenerate.
  2. Costs (RESOLVED in research): the discovery backtest was gross/fee-free with same-day fills, but validation/momentum_realistic.py now 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. Use config.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.
  3. Universe: promote to a PIT S&P 500 list for a cleaner, larger book.
  4. Risk: confirm the ~41% maxDD from research is acceptable for the account size; consider a volatility target like the ETF sleeve if not.

All trading described is paper (simulated). Not investment advice.

← all research