Skip to content

feat(strategy): value+momentum composite — second-edge probe - #2

Open
Dukotah wants to merge 2 commits into
mainfrom
feat/second-edge-value-momentum
Open

feat(strategy): value+momentum composite — second-edge probe#2
Dukotah wants to merge 2 commits into
mainfrom
feat/second-edge-value-momentum

Conversation

@Dukotah

@Dukotah Dukotah commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Pursues the second uncorrelated edge (the real Phase 6 blocker) — the documented next probe from DECISIONS Session 22.

Adds

  • apex/strategy/library/value_momentum.pyValueMomentumStrategy: per-asset composite of a value leg (long-horizon reversal z-score) and a momentum leg (blended TSMOM over 1/3/6/12mo), each cross-sectionally z-scored then combined (AQR "Value & Momentum Everywhere" score-level approach). Holds top-K, long/flat.
  • Configurable universe; defaults to the broad 10-ETF expanded pool (SPY/EFA/EEM/TLT/IEF/LQD/GLD/SLV/DBC/VNQ).
  • tests/test_value_momentum.py — hand-derived leg math, composite ranking, degenerate-case safety, end-to-end signals, determinism.

Notes

  • Emits SignalEvents only (no orders/IO/wall-clock), Decimal throughout, frozen-model safe.
  • Intentionally NOT deployed/registered — must clear the Gauntlet (esp. Sharpe@2× cost) AND the correlation gate before any capital.
  • Built with no local Python — CI is first execution.

🤖 Generated with Claude Code

Dukotah and others added 2 commits June 5, 2026 22:49
… probe

Add ValueMomentumStrategy: a combined per-asset VALUE + MOMENTUM composite-score
strategy, the 'next probe' flagged in DECISIONS Session 22 / ROADMAP Phase 6 for
the second uncorrelated edge.

apex/strategy/library/value_momentum.py
  - Per-asset composite = value_weight * z(value) + momentum_weight * z(momentum),
    cross-sectionally Z-scored so the two legs share a scale and neither dominates
    by units; degrades gracefully (Z=0) with a single live asset / zero dispersion.
  - VALUE leg = long-horizon reversal -(return value_period->skip_recent) (5y, skip
    last 1y) — the cheap-vs-multi-year-ago laggard scores high.
  - MOMENTUM leg = weighted blend of trailing returns over multiple lookbacks
    (default 21/63/126/252 ~ 1/3/6/12 months); only scored once EVERY lookback has
    history so scores stay comparable across assets.
  - Holds top-K by composite, long/flat, position-aware via broker-reconciled
    context (cold-start correct, no pyramiding), inverse-vol sized via signal
    strength only (RiskManager remains sole sizer). Optional trend-trap filter,
    OFF by default to measure the pure composite's correlation to trend first.
  - Configurable universe: symbols arg + DEFAULT_VALMOM_UNIVERSE (the broad 10-ETF
    'expanded' pool: SPY/EFA/EEM/TLT/IEF/LQD/GLD/SLV/DBC/VNQ) + default_universe()
    helper — runs on the richer 10-13 ETF pool per Session 22 next probe (a).
  - Emits SignalEvents with strength 0..1, a reason string and suggested_stop_loss;
    no orders, no I/O, no wall-clock; stdlib-only deterministic math.
  - Docstring states it MUST clear the Gauntlet (esp. Sharpe@2x-cost) AND the
    correlation gate before any capital; NOT marked deployed.

tests/test_value_momentum.py
  - Validation; hand-computed value/momentum leg metrics; composite Z-score math and
    the 'both cheap AND trending wins' ranking; missing-leg skipping; single-asset
    graceful degradation; end-to-end buy of the cheap-recovering asset (and veto of
    the long-run winner); stop/reason/strength on the signal; trend-filter veto of a
    falling composite leader; default-universe shape; determinism.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…entions

- Metric tests had miscounted negative indices: a k-bar return uses close[-(k+1)],
  so the equal/weighted momentum blends are 0.2121.. / 0.1515.. and the skip_recent
  value window reads close[-5]->close[-2]. Implementation was correct.
- composite ranking test now sets top_k=2 so the worst-on-both asset is excluded.
- end-to-end test now asserts the true behavior: the cheap-AND-recovering asset is
  the composite's FIRST pick and the expensive long-run winner never leads it
  (the 50/50 blend does not categorically veto it, so 'never bought' was too strong).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant