Demote/relabel the setup-outcome check; add exit reason to My Trades

The "tracking/drift" chip compared the live target/stop/expired outcome cohort
against the backtest's target/stop bucket (overall_qualified) — a like-for-like
pipeline check — but sat directly under the portfolio monitor, which shows the
promoted 3x-ATR-trailing book. That juxtaposition (plus "faithfully implementing
it" copy) made a plumbing/QA signal read as validation of the ATR-trail strategy
you actually trade. It validates neither the trailing-stop book nor real trades.

- Move the check out of the monitor block into the "Track-record maintenance"
  disclosure, relabelled "Setup-outcome pipeline check" with copy that says it
  checks the setup-grading pipeline (no look-ahead/config/data drift), NOT the
  ATR-trail production book. The genuine live validation stays My Trades (real
  paper trades, same ATR-trail exits) up top.
- Add a compact "Exit" column to My Trades showing close_reason
  (Stop/Trail/Target/Time/Manual) — the field was already plumbed to the
  frontend PaperTrade type, so this is frontend-only.

tsc -b && vite build pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 09:51:13 +02:00
co-authored by Claude Opus 4.8
parent 02b28f5ea6
commit 2a4bdd16a8
3 changed files with 99 additions and 62 deletions
@@ -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<TrackingStatus, { cls: string; label: string }> = {
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 <span className={`shrink-0 rounded-full border px-2.5 py-1 text-xs font-medium ${s.cls}`}>{s.label}</span>;
}
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<TrackingStatus, string> = {
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 (
<Section title="Is the strategy working?" hint="portfolio simulation vs S&P 500, validated against the live record">
<Section title="Is the strategy working?" hint="portfolio simulation of the promoted strategy vs S&P 500">
<div className="space-y-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<Disclosure summary="How this is measured">
@@ -320,25 +283,6 @@ export function BacktestPanel() {
</div>
)}
{/* Live-vs-backtest validation: does the running system realize what the backtest promised? */}
<div className="glass-sm space-y-2 p-4">
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-2">
<div className="flex flex-wrap items-baseline gap-x-5 gap-y-1">
<span className="text-sm text-gray-400">
Live <span className={`num font-semibold ${rColor(liveAvgR)}`}>{fmtR(liveAvgR)}</span>
</span>
<span className="text-sm text-gray-400">
Backtest <span className={`num font-semibold ${rColor(btAvgR)}`}>{fmtR(btAvgR)}</span>
</span>
<span className="text-xs text-gray-500">
{liveN} matured{perf ? ` · ${perf.maturing} maturing` : ''} · qualified expectancy
</span>
</div>
<VerdictChip status={status} />
</div>
<p className="text-[11px] leading-relaxed text-gray-500">{verdictNote[status]}</p>
</div>
{monitor.note && <p className="text-[11px] text-gray-600">{monitor.note}</p>}
</div>
) : (
@@ -374,7 +318,7 @@ export function BacktestPanel() {
<p className="text-[11px] text-gray-600">
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.
</p>
</>
)}