replace EV activation gate with cross-sectional 12-1 momentum ranking
The 5-year backtest confirmed the EV gate adds negative value (high threshold = worst expectancy) and that 12-1 month momentum is the one price signal with a plausible, right-signed cross-sectional IC (~0.05). So "qualified" now means: clears the R:R + confidence floors AND the ticker ranks in the top `min_momentum_percentile` of the universe by 12-1 momentum that week. - qualification.py: drop expected_value_r / the EV gate; add a momentum-percentile gate (duck-typed `momentum_percentile`, only enforced when attached + threshold set, else defers to floors). Mirrored in frontend qualification.ts. - activation config/schema: min_expected_value -> min_momentum_percentile (default 80 = top quintile). ActivationSettings, DashboardPage (ranks/【shows】 momentum instead of EV), and the BacktestPanel sweep follow. - backtest: rank each ISO week's universe by 12-1 momentum, assign a percentile, and qualify the top slice; the sweep now sweeps the percentile cutoff. Also offload the backtest's per-ticker compute to a worker thread so the heavy ~5y run no longer blocks the API event loop (the "backend offline" flicker). Production setups don't carry momentum_percentile yet — wiring the scanner to attach it (a universe momentum-rank step) is the next step; until then the live gate defers to floors while the backtest measures the momentum selection. 330 backend tests pass; frontend build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -184,19 +184,19 @@ export function BacktestPanel() {
|
||||
{report.sweep && report.sweep.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-widest text-gray-500">
|
||||
Min expected-value sweep
|
||||
Momentum-percentile sweep
|
||||
</p>
|
||||
<p className="mb-2 text-[11px] text-gray-500">
|
||||
How many setups qualify — and how they perform — at each expected-value gate (other
|
||||
gate conditions held fixed). EV is in R: 0.15 means +0.15× your risk per trade on
|
||||
average. Lower = more trades, watch that expectancy holds. Your current setting is
|
||||
How many setups qualify — and how they perform — at each momentum-rank cutoff (floors
|
||||
held fixed). 80 = only the top 20% of the universe by 12-1 momentum each week; 0 =
|
||||
floors only. Lower = more trades, watch that expectancy holds. Your current setting is
|
||||
highlighted; set it in Admin → Settings → Activation.
|
||||
</p>
|
||||
<div className="glass overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] text-left text-xs uppercase tracking-wider text-gray-500">
|
||||
<th className="px-4 py-2.5">Min EV (R)</th>
|
||||
<th className="px-4 py-2.5">Min momentum %ile</th>
|
||||
<th className="px-4 py-2.5 text-right">Qualified</th>
|
||||
<th className="px-4 py-2.5 text-right">Wins</th>
|
||||
<th className="px-4 py-2.5 text-right">Losses</th>
|
||||
@@ -207,12 +207,12 @@ export function BacktestPanel() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{report.sweep.map((row) => {
|
||||
const current = Math.abs(row.min_expected_value - report.min_expected_value) < 0.001;
|
||||
const current = Math.abs(row.min_momentum_percentile - report.min_momentum_percentile) < 0.001;
|
||||
return (
|
||||
<tr key={row.min_expected_value} className={`border-b border-white/[0.04] ${current ? 'bg-blue-400/10' : ''}`}>
|
||||
<tr key={row.min_momentum_percentile} className={`border-b border-white/[0.04] ${current ? 'bg-blue-400/10' : ''}`}>
|
||||
<td className="num px-4 py-2.5 text-gray-200">
|
||||
{current && <span className="mr-1 text-blue-300">★</span>}
|
||||
{row.min_expected_value.toFixed(2)}
|
||||
{row.min_momentum_percentile.toFixed(0)}
|
||||
</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-200">{row.total}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-emerald-400">{row.wins}</td>
|
||||
|
||||
Reference in New Issue
Block a user