diff --git a/frontend/src/components/signals/BacktestPanel.tsx b/frontend/src/components/signals/BacktestPanel.tsx index 3b76fad..c1e314e 100644 --- a/frontend/src/components/signals/BacktestPanel.tsx +++ b/frontend/src/components/signals/BacktestPanel.tsx @@ -9,35 +9,8 @@ import { Disclosure } from '../ui/Disclosure'; import { Dropdown } from '../ui/Dropdown'; import { Section } from '../ui/Section'; import { useToast } from '../ui/Toast'; -import type { BacktestCurvePoint, BacktestPortfolioMonitorRun } from '../../lib/types'; - -function fmtR(v: number | null | undefined): string { - if (v === null || v === undefined) return '—'; - return `${v > 0 ? '+' : ''}${v.toFixed(2)}R`; -} -function fmtPct(v: number | null): string { - return v === null ? '—' : `${v.toFixed(1)}%`; -} -function fmtMoney(v: number | null | undefined): string { - if (v === null || v === undefined) return '—'; - return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} -function fmtSignedPct(v: number | null | undefined): string { - if (v === null || v === undefined) return '—'; - return `${v > 0 ? '+' : ''}${v.toFixed(1)}%`; -} -function fmtDrawdown(v: number | null | undefined): string { - return v === null || v === undefined ? '—' : `-${Math.abs(v).toFixed(1)}%`; -} -function fmtDays(v: number | null | undefined): string { - return v === null || v === undefined ? '—' : `${v.toFixed(1)}d`; -} -function rColor(v: number | null): string { - if (v === null) return 'text-gray-400'; - if (v > 0) return 'text-emerald-400'; - if (v < 0) return 'text-red-400'; - return 'text-gray-300'; -} +import { BacktestRecommendationCard } from './BacktestRecommendationCard'; +import { PortfolioMonitorPanel } from './PortfolioMonitorPanel'; function timeAgo(iso: string): string { const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000); @@ -48,95 +21,14 @@ function timeAgo(iso: string): string { return `${Math.floor(hrs / 24)}d ago`; } -function Stat({ label, value, valueClass = 'text-gray-100', sub }: { - label: string; value: string; valueClass?: string; sub?: string; -}) { - return ( -
{label}
-{value}
- {sub &&{sub}
} -{run.label}
-{run.start_date} - {run.end_date}
-+ Live GTL is the exact target path the scanner and the + scheduled backtest use; Structural S/R is a comparison + arm sourcing targets from chart structure. Weekly steps + five sessions at a time and is what the server runs; Daily + {' '}is roughly 5× the replay work. +
-Daily replays ~5× the work — prefer the offline snapshot runner.
+ )} + {targetModel === 'structural_sr' && ( +Comparison arm — not the live scanner's target path.
+ )} +Portfolio monitor
-- Simulated book for the selected strategy and lookback, compared with the S&P 500. -
-- Avg hold {fmtDays(monitorRun.avg_hold_days)} · Best {fmtR(monitorRun.best_trade_r)} / Worst{' '} - {fmtR(monitorRun.worst_trade_r)} · Avg P&L per trade {fmtMoney(monitorRun.avg_trade_pnl)} - {monitorRun.reentry_policy === 'gate_reset' ? ( - <> · Re-entry after gate failure and fresh qualification> - ) : null} -
- - {monitorRun.yearly_returns && monitorRun.yearly_returns.length > 0 && ( -Per-year returns
-{monitor.note}
} -What this backtest recommends
- {report.recommendation.headline && ( -- {report.recommendation.headline} -
- )} -{report.recommendation.note}
- )} -- 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; your realized results up top show what it is actually delivering. -
> )} diff --git a/frontend/src/components/signals/BacktestRecommendationCard.tsx b/frontend/src/components/signals/BacktestRecommendationCard.tsx new file mode 100644 index 0000000..42d4095 --- /dev/null +++ b/frontend/src/components/signals/BacktestRecommendationCard.tsx @@ -0,0 +1,92 @@ +import { Disclosure } from '../ui/Disclosure'; +import type { BacktestRecommendation } from '../../lib/types'; + +/** + * The verdict, ahead of the tuning detail. + * + * All eight findings used to render as equal-weight bullets, so "does this + * strategy work" sat in the same visual register as "which cutoff scored best". + * `topic` splits them: the three that answer the question stay inline, the rest + * collapse. + * + * No topic chips — every backend string already self-prefixes ("Gate: …", + * "Robustness: …"), so a chip would render "GATE │ Gate: …", and stripping the + * prefix would drop real information ("(3y)" carries the lookback, "Legacy" + * qualifies the diagnostic). + */ +const PRIMARY_TOPICS = new Set(['production', 'benchmark', 'robustness']); + +/** + * Mirrors how the backend phrases a bad result — `_build_recommendation` emits + * "Robustness WARNING: …" and "Book vs SPY: LAGS …". There is deliberately no + * `severity` field on the payload; if that changes, this is the one place to fix. + */ +function isWarning(text: string): boolean { + return text.includes('WARNING') || text.includes('LAGS'); +} + +export function BacktestRecommendationCard({ + recommendation, +}: { + recommendation: BacktestRecommendation; +}) { + const items = recommendation.items; + if (items.length === 0) return null; + + // A warning is always visible, whatever its topic — burying "the edge + // disappears without the top 5% of winners" behind a disclosure would defeat + // the point of surfacing it at all. + const primary = items.filter((i) => PRIMARY_TOPICS.has(i.topic) || isWarning(i.text)); + const secondary = items.filter((i) => !PRIMARY_TOPICS.has(i.topic) && !isWarning(i.text)); + const warningCount = items.filter((i) => isWarning(i.text)).length; + + return ( +What this backtest recommends
+ {warningCount > 0 && ( + + ⚠ {warningCount} warning{warningCount > 1 ? 's' : ''} + + )} +{recommendation.headline}
+ )} + + {primary.length > 0 && ( +{recommendation.note}
+ )} +{run.label}
+{run.start_date} - {run.end_date}
+Diagnostic only — not production P&L.{' '} Grades gate-level touch vs stop (the rejected take-profit model). Production exits are - initial stop / ATR trail / max hold — see paper trades and the portfolio monitor above. - Target before stop = win, stop first = loss (same-bar both = loss), neither in 30 trading - days = expired at 0R. Only matured windows count. Scores{' '} + initial stop / ATR trail / max hold — see the Paper Trades tab and the portfolio monitor + above. Target before stop = win, stop first = loss (same-bar both = loss), neither in 30 + trading days = expired at 0R. Only matured windows count. Scores{' '} all setups as a control group; runs nightly.
diff --git a/frontend/src/components/signals/MyTradesPanel.tsx b/frontend/src/components/signals/MyTradesPanel.tsx index 1678270..b890e8c 100644 --- a/frontend/src/components/signals/MyTradesPanel.tsx +++ b/frontend/src/components/signals/MyTradesPanel.tsx @@ -2,22 +2,10 @@ import { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { usePaperTrades } from '../../hooks/usePaperTrades'; import { tradePnl } from '../../lib/paperTrade'; -import { formatPrice } from '../../lib/format'; +import { formatPrice, fmtR, fmtSignedMoney, rColor } from '../../lib/format'; import { Section } from '../ui/Section'; import { Callout } from '../ui/Callout'; - -function money(v: number): string { - return `${v >= 0 ? '+' : '−'}$${Math.abs(v).toFixed(2)}`; -} -function fmtR(v: number | null): string { - return v === null ? '—' : `${v > 0 ? '+' : ''}${v.toFixed(2)}R`; -} -function color(v: number | null): string { - if (v === null) return 'text-gray-400'; - if (v > 0) return 'text-emerald-400'; - if (v < 0) return 'text-red-400'; - return 'text-gray-300'; -} +import { StatTile } from '../ui/StatTile'; // How the trade was closed — useful context on real trades at almost no cost. function reasonMeta(reason: string | null): { label: string; cls: string } { @@ -31,18 +19,6 @@ function reasonMeta(reason: string | null): { label: string; cls: string } { } } -function Stat({ label, value, valueClass = 'text-gray-100', sub }: { - label: string; value: string; valueClass?: string; sub?: string; -}) { - return ( -{label}
-{value}
- {sub &&{sub}
} -Portfolio monitor
++ Simulated book for the selected strategy and lookback, compared with the S&P 500. +
++ Risk-adjusted quality metrics appear after the next backtest run. +
+ ) : ( +Risk-adjusted quality
++ Avg hold {fmtDays(monitorRun.avg_hold_days)} · Best {fmtR(monitorRun.best_trade_r)} / Worst{' '} + {fmtR(monitorRun.worst_trade_r)} + {monitorRun.reentry_policy === 'gate_reset' ? ( + <> · Re-entry after gate failure and fresh qualification> + ) : null} +
+ + {monitorRun.yearly_returns && monitorRun.yearly_returns.length > 0 && ( +Per-year returns
+{monitor.note}
} +{label}
+{value}
+ {sub &&{sub}
} +