diff --git a/frontend/src/components/signals/BacktestPanel.tsx b/frontend/src/components/signals/BacktestPanel.tsx index 4a191c2..9371ec8 100644 --- a/frontend/src/components/signals/BacktestPanel.tsx +++ b/frontend/src/components/signals/BacktestPanel.tsx @@ -1,7 +1,6 @@ import { useMemo, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useBacktestReport } from '../../hooks/useMarketRegime'; -import { usePerformance } from '../../hooks/usePerformance'; import { triggerJob } from '../../api/admin'; import { Button } from '../ui/Button'; import { Callout } from '../ui/Callout'; @@ -10,14 +9,6 @@ import { Section } from '../ui/Section'; import { useToast } from '../ui/Toast'; import type { BacktestCurvePoint, BacktestPortfolioMonitorRun } from '../../lib/types'; -// Need at least this many matured setups before a live-vs-backtest verdict means -// anything; below it the live sample is too noisy to compare. -const MIN_MATURED = 20; -// Live expectancy this far (in R) below the backtest counts as drift, not noise. -const DRIFT_TOLERANCE_R = 0.2; - -type TrackingStatus = 'building' | 'tracking' | 'drift' | 'no-backtest'; - function fmtR(v: number | null | undefined): string { if (v === null || v === undefined) return '—'; return `${v > 0 ? '+' : ''}${v.toFixed(2)}R`; @@ -67,17 +58,6 @@ function Stat({ label, value, valueClass = 'text-gray-100', sub }: { ); } -function VerdictChip({ status }: { status: TrackingStatus }) { - const styles: Record = { - tracking: { cls: 'border-emerald-500/30 bg-emerald-500/15 text-emerald-300', label: '✓ tracking' }, - drift: { cls: 'border-amber-500/30 bg-amber-500/15 text-amber-300', label: '⚠ drift' }, - building: { cls: 'border-white/10 bg-white/[0.05] text-gray-400', label: 'building' }, - 'no-backtest': { cls: 'border-white/10 bg-white/[0.05] text-gray-400', label: 'no backtest' }, - }; - const s = styles[status]; - return {s.label}; -} - function curvePath( points: BacktestCurvePoint[], min: number, @@ -158,7 +138,6 @@ function EquityCurveChart({ run }: { run: BacktestPortfolioMonitorRun }) { export function BacktestPanel() { const { data: report, isLoading } = useBacktestReport(); - const { data: perf } = usePerformance({ qualified_only: true }); const queryClient = useQueryClient(); const toast = useToast(); const [selectedStrategy, setSelectedStrategy] = useState(''); @@ -178,22 +157,6 @@ export function BacktestPanel() { [monitor, activeStrategy, activeLookback], ); - // Live matured qualified cohort vs the backtest's qualified expectancy — the - // out-of-sample check that the running system faithfully implements the backtest. - const liveAvgR = perf?.overall.avg_r ?? null; - const liveN = perf?.overall.total ?? 0; - const btAvgR = report?.overall_qualified.avg_r ?? null; - let status: TrackingStatus = 'building'; - if (liveAvgR != null && liveN >= MIN_MATURED) { - status = btAvgR == null ? 'no-backtest' : liveAvgR >= btAvgR - DRIFT_TOLERANCE_R ? 'tracking' : 'drift'; - } - const verdictNote: Record = { - building: `Fewer than ~${MIN_MATURED} matured setups so far — until then the backtest is the edge estimate. This turns into a live check as setups age past their ~30-day window.`, - 'no-backtest': 'Run the backtest to get a baseline to compare the live record against.', - tracking: 'Live setups are resolving in line with the backtest — the running system is faithfully implementing it.', - drift: 'Live expectancy is running materially below the backtest — small-sample noise, a regime shift, or a live/backtest gap. Worth a look.', - }; - const run = useMutation({ mutationFn: () => triggerJob('backtest'), onSuccess: (res) => { @@ -208,7 +171,7 @@ export function BacktestPanel() { }); return ( -
+
@@ -320,25 +283,6 @@ export function BacktestPanel() {
)} - {/* Live-vs-backtest validation: does the running system realize what the backtest promised? */} -
-
-
- - Live {fmtR(liveAvgR)} - - - Backtest {fmtR(btAvgR)} - - - {liveN} matured{perf ? ` · ${perf.maturing} maturing` : ''} · qualified expectancy - -
- -
-

{verdictNote[status]}

-
- {monitor.note &&

{monitor.note}

}
) : ( @@ -374,7 +318,7 @@ export function BacktestPanel() {

Strategy research — gate tuning, exit sweeps, factor rank-IC — now runs locally against a database snapshot (see README). This page keeps only what says whether the promoted strategy - is worth trading and being delivered live. + is worth trading; your realized results up top show what it is actually delivering.

)} diff --git a/frontend/src/components/signals/MyTradesPanel.tsx b/frontend/src/components/signals/MyTradesPanel.tsx index 286bdc2..2c03cf9 100644 --- a/frontend/src/components/signals/MyTradesPanel.tsx +++ b/frontend/src/components/signals/MyTradesPanel.tsx @@ -19,6 +19,18 @@ function color(v: number | null): string { return 'text-gray-300'; } +// How the trade was closed — useful context on real trades at almost no cost. +function reasonMeta(reason: string | null): { label: string; cls: string } { + switch (reason) { + case 'stop': return { label: 'Stop', cls: 'text-red-400' }; + case 'trailing': return { label: 'Trail', cls: 'text-amber-400' }; + case 'target': return { label: 'Target', cls: 'text-emerald-400' }; + case 'time': return { label: 'Time', cls: 'text-gray-400' }; + case 'manual': return { label: 'Manual', cls: 'text-blue-300' }; + default: return { label: '—', cls: 'text-gray-500' }; + } +} + function Stat({ label, value, valueClass = 'text-gray-100', sub }: { label: string; value: string; valueClass?: string; sub?: string; }) { @@ -85,6 +97,7 @@ export function MyTradesPanel() { P&L R Alpha + Exit Closed @@ -102,6 +115,11 @@ export function MyTradesPanel() { {p ? money(p.pnl) : '—'} {p?.r != null ? fmtR(p.r) : '—'} {t.alpha_pct != null ? `${t.alpha_pct >= 0 ? '+' : ''}${t.alpha_pct.toFixed(1)}%` : '—'} + + + {reasonMeta(t.close_reason).label} + + {t.closed_at ? new Date(t.closed_at).toLocaleDateString() : '—'} ))} diff --git a/frontend/src/components/signals/TrackRecordPanel.tsx b/frontend/src/components/signals/TrackRecordPanel.tsx index 5ff9eca..76a5e77 100644 --- a/frontend/src/components/signals/TrackRecordPanel.tsx +++ b/frontend/src/components/signals/TrackRecordPanel.tsx @@ -1,4 +1,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { usePerformance } from '../../hooks/usePerformance'; +import { useBacktestReport } from '../../hooks/useMarketRegime'; import { triggerJob, resetTrackRecord } from '../../api/admin'; import { Button } from '../ui/Button'; import { Disclosure } from '../ui/Disclosure'; @@ -6,10 +8,63 @@ import { useToast } from '../ui/Toast'; import { BacktestPanel } from './BacktestPanel'; import { MyTradesPanel } from './MyTradesPanel'; +// Need at least this many matured setups before the pipeline check means anything; +// below it the live sample is too noisy to compare. +const MIN_MATURED = 20; +// Live expectancy this far (in R) below the backtest counts as drift, not noise. +const DRIFT_TOLERANCE_R = 0.2; + +type PipelineStatus = 'building' | 'tracking' | 'drift' | 'no-backtest'; + +function fmtR(value: number | null): string { + if (value === null) return '—'; + return `${value > 0 ? '+' : ''}${value.toFixed(2)}R`; +} + +function rColor(value: number | null): string { + if (value === null) return 'text-gray-400'; + if (value > 0) return 'text-emerald-400'; + if (value < 0) return 'text-red-400'; + return 'text-gray-300'; +} + +function StatusChip({ status }: { status: PipelineStatus }) { + const styles: Record = { + tracking: { cls: 'border-emerald-500/30 bg-emerald-500/15 text-emerald-300', label: '✓ in sync' }, + drift: { cls: 'border-amber-500/30 bg-amber-500/15 text-amber-300', label: '⚠ drift' }, + building: { cls: 'border-white/10 bg-white/[0.05] text-gray-400', label: 'building' }, + 'no-backtest': { cls: 'border-white/10 bg-white/[0.05] text-gray-400', label: 'no backtest' }, + }; + const s = styles[status]; + return {s.label}; +} + export function TrackRecordPanel() { const queryClient = useQueryClient(); const toast = useToast(); + // Setup-outcome pipeline check: does the live outcome evaluator reproduce the + // backtest's target/stop grading? Both sides use the SAME target/stop/expired + // model — this is a plumbing/QA signal (no look-ahead, config or data drift), + // NOT validation of the ATR-trail production strategy shown in the monitor. + const { data: perf } = usePerformance({ qualified_only: true }); + const { data: report } = useBacktestReport(); + const liveAvgR = perf?.overall.avg_r ?? null; + const liveN = perf?.overall.total ?? 0; + const btAvgR = report?.overall_qualified.avg_r ?? null; + let status: PipelineStatus = 'building'; + if (liveAvgR != null && liveN >= MIN_MATURED) { + status = btAvgR == null ? 'no-backtest' : liveAvgR >= btAvgR - DRIFT_TOLERANCE_R ? 'tracking' : 'drift'; + } + const statusNote: Record = { + building: `Fewer than ~${MIN_MATURED} matured setups so far — too few to compare.`, + 'no-backtest': 'Run the backtest to get a target/stop baseline to check against.', + tracking: + "Live setup outcomes are resolving in line with the backtest's target/stop model — the outcome-evaluation pipeline shows no look-ahead, config or data drift. (Checks the setup-grading pipeline, not the ATR-trail production book above.)", + drift: + "Live setup outcomes are running materially below the backtest's target/stop model — small-sample noise, a regime shift, or a live/backtest pipeline gap. Worth a look.", + }; + const evaluateMutation = useMutation({ mutationFn: () => triggerJob('outcome_evaluator'), onSuccess: () => { @@ -46,22 +101,42 @@ export function TrackRecordPanel() { return (
- {/* Your real, realized results come first; the strategy validation follows. */} + {/* Your real, realized results come first; the strategy simulation follows. */}
-
+

The live check replays every setup against the daily bars after detection: target before stop = win, stop first = loss (both in one bar counts conservatively as a loss), neither within 30 trading days = expired at 0R. Only setups whose full window has elapsed count; younger ones are still maturing (near stops resolve fast, far targets need time, so early numbers skew negative). The evaluator scores all setups — qualified or not, so - unqualified ones stay a control group — and runs nightly. Reset permanently clears all setups and - their outcomes; live setups regenerate on the next scan. + unqualified ones stay a control group — and runs nightly.

+ + {/* Diagnostic, not strategy validation: live target/stop outcomes vs the backtest's target/stop model. */} +
+
+
+ Setup-outcome pipeline check + + Live {fmtR(liveAvgR)} + + + Backtest {fmtR(btAvgR)} + + + {liveN} matured{perf ? ` · ${perf.maturing} maturing` : ''} · qualified target/stop + +
+ +
+

{statusNote[status]}

+
+