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:
@@ -12,7 +12,7 @@ import { OpenTradesPanel } from '../components/dashboard/OpenTradesPanel';
|
||||
import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton';
|
||||
import { formatPrice } from '../lib/format';
|
||||
import { recommendationActionLabel } from '../lib/recommendation';
|
||||
import { qualifiesSetup, activationSummary, primaryTargetProbability, expectedValueR } from '../lib/qualification';
|
||||
import { qualifiesSetup, activationSummary, primaryTargetProbability } from '../lib/qualification';
|
||||
import type { TradeSetup } from '../lib/types';
|
||||
|
||||
function fmtR(value: number | null): string {
|
||||
@@ -69,12 +69,12 @@ export default function DashboardPage() {
|
||||
);
|
||||
|
||||
// Show qualified setups first; fall back to the full list when none qualify.
|
||||
// Rank by expected value (R) so the best opportunity sits at the top.
|
||||
// Rank by 12-1 momentum percentile so the strongest names sit at the top.
|
||||
const showingQualified = qualifiedSetups.length > 0;
|
||||
const topSetups: TradeSetup[] = useMemo(() => {
|
||||
const pool = showingQualified ? qualifiedSetups : trades.data ?? [];
|
||||
return [...pool]
|
||||
.sort((a, b) => (expectedValueR(b) ?? -Infinity) - (expectedValueR(a) ?? -Infinity))
|
||||
.sort((a, b) => (b.momentum_percentile ?? -Infinity) - (a.momentum_percentile ?? -Infinity))
|
||||
.slice(0, 5);
|
||||
}, [showingQualified, qualifiedSetups, trades.data]);
|
||||
|
||||
@@ -176,13 +176,12 @@ export default function DashboardPage() {
|
||||
<th className="px-4 py-3 text-right">Entry</th>
|
||||
<th className="px-4 py-3 text-right">R:R</th>
|
||||
<th className="px-4 py-3 text-right">Target Prob</th>
|
||||
<th className="px-4 py-3 text-right">Exp. Value</th>
|
||||
<th className="px-4 py-3 text-right">Momentum</th>
|
||||
<th className="hidden px-4 py-3 md:table-cell">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topSetups.map((setup, i) => {
|
||||
const ev = expectedValueR(setup);
|
||||
const isTopPick = i === 0;
|
||||
return (
|
||||
<tr
|
||||
@@ -212,8 +211,8 @@ export default function DashboardPage() {
|
||||
return p != null ? `${Math.round(p)}%` : '—';
|
||||
})()}
|
||||
</td>
|
||||
<td className={`num px-4 py-3 text-right font-semibold ${rColor(ev)}`}>
|
||||
{fmtR(ev)}
|
||||
<td className="num px-4 py-3 text-right font-semibold text-gray-200">
|
||||
{setup.momentum_percentile != null ? `${Math.round(setup.momentum_percentile)}%ile` : '—'}
|
||||
</td>
|
||||
<td className="hidden px-4 py-3 text-xs text-gray-400 md:table-cell">
|
||||
{recommendationActionLabel(setup.recommended_action)}
|
||||
@@ -225,7 +224,7 @@ export default function DashboardPage() {
|
||||
</table>
|
||||
<div className="flex items-center justify-between border-t border-white/[0.04] px-4 py-2.5">
|
||||
<span className="text-[11px] text-gray-500">
|
||||
Exp. Value = probability-weighted payoff per unit of risk
|
||||
Momentum = ticker's 12-1 month rank across the universe (higher = stronger)
|
||||
</span>
|
||||
<Link to="/signals" className="text-xs font-medium text-blue-300 hover:text-blue-200 transition-colors">
|
||||
All setups →
|
||||
|
||||
Reference in New Issue
Block a user