momentum gate: long-only + wire the percentile onto live setups
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 47s
Deploy / deploy (push) Successful in 24s

Part 1 — long-only. The momentum edge is long top-momentum; the gate was
qualifying shorts on high-momentum names (fighting the trend), which showed as
the -0.13R Short(qual.) drag. While the gate is active, shorts no longer qualify
(backend qualification, backtest _momentum_qualifies, and the frontend mirror).

Part 2 — production wiring. Live setups now carry a real momentum rank, so the
dashboard, the Track Record's qualified stats, and outcome evaluation all gate on
the same value instead of deferring to floors:
- new momentum_service.compute_momentum_percentiles: 12-1 momentum per ticker,
  ranked across the universe into a {symbol: percentile} map.
- the daily R:R scan ranks the universe up front and stores each setup's
  percentile (new trade_setups.momentum_percentile column, migration 010).
- enhance_trade_setup mutates the same row, so the percentile is preserved;
  _trade_setup_to_dict + TradeSetupResponse expose it to the API.

Until a fresh scan runs, pre-existing setups have a null percentile and the gate
falls back to floors for them (longs) / excludes them (shorts) — they fill in on
the next scan. 341 backend tests pass; frontend build clean.

Needs the alembic upgrade (migration 010) on deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 07:07:38 +02:00
parent 7060b9a019
commit 605f95098c
10 changed files with 221 additions and 11 deletions
+8 -5
View File
@@ -33,11 +33,14 @@ export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boo
return false;
}
if ((setup.confidence_score ?? 0) < config.min_confidence) return false;
// Cross-sectional momentum is the core selection — only enforced when a
// percentile is attached and a threshold is set; otherwise defer to the floors.
if (config.min_momentum_percentile > 0 && setup.momentum_percentile != null
&& setup.momentum_percentile < config.min_momentum_percentile) {
return false;
// Cross-sectional momentum is the core selection (long-only). While the gate is
// active, shorts never qualify; the percentile floor is enforced only when a
// percentile is attached, otherwise defer to the floors.
if (config.min_momentum_percentile > 0) {
if (setup.direction === 'short') return false;
if (setup.momentum_percentile != null && setup.momentum_percentile < config.min_momentum_percentile) {
return false;
}
}
if (config.require_high_conviction && !HIGH_CONVICTION_ACTIONS.has(setup.recommended_action ?? '')) {
return false;