feat: add strategy variant lab and signal context snapshots
Backtest report now includes research-only hold-to-horizon portfolio variants comparing raw vs residual 12-1 momentum, cutoff 80 vs 90, max 10 vs 15 positions, and SPY-200 risk scaling. A dynamic research recommendation panel flags residual momentum, cutoff 90, or regime scaling only when transparent promotion rules pass. Adds signal_context_snapshots with migration 016 and captures one point-in-time context row per newly generated TradeSetup: setup fields, composite/dimensions, latest sentiment, latest fundamentals, and strategy_version=momentum_12_1_rr_time_v1. This is forward-only; no historical sentiment/fundamental backfill is attempted. No live gate, paper-trade exit, or production ranking behavior changes. Verification: 458 backend tests pass, ruff check app/ clean, frontend npm run build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import { Callout } from '../ui/Callout';
|
||||
import { Disclosure } from '../ui/Disclosure';
|
||||
import { Section } from '../ui/Section';
|
||||
import { useToast } from '../ui/Toast';
|
||||
import type { BacktestBucket, BacktestPortfolioPolicy } from '../../lib/types';
|
||||
import type { BacktestBucket, BacktestPortfolioPolicy, BacktestStrategyVariant } from '../../lib/types';
|
||||
|
||||
function fmtR(v: number | null | undefined): string {
|
||||
if (v === null || v === undefined) return '—';
|
||||
@@ -206,6 +206,25 @@ export function BacktestPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report.research_recommendation && report.research_recommendation.items.length > 0 && (
|
||||
<div className="glass border border-emerald-400/15 p-4">
|
||||
<p className="section-index">Research candidates</p>
|
||||
<ul className="mt-2 space-y-1">
|
||||
{report.research_recommendation.items.map((item) => (
|
||||
<li
|
||||
key={item.topic + item.text}
|
||||
className={`text-xs ${item.candidate ? 'text-emerald-400' : 'text-gray-400'}`}
|
||||
>
|
||||
{item.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{report.research_recommendation.note && (
|
||||
<p className="mt-2 text-[11px] text-gray-600">{report.research_recommendation.note}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Stat
|
||||
label="Qualified Hit Rate"
|
||||
@@ -516,6 +535,60 @@ export function BacktestPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report.strategy_variants && report.strategy_variants.variants.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-widest text-gray-500">
|
||||
Strategy variants
|
||||
</p>
|
||||
<p className="mb-2 text-[11px] text-gray-500">
|
||||
{report.strategy_variants.note ?? 'Research-only portfolio variants.'}{' '}
|
||||
<span className="text-gray-300">
|
||||
Residual momentum stays research-only until a variant beats production under the promotion rules.
|
||||
</span>
|
||||
</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">Variant</th>
|
||||
<th className="px-4 py-2.5 text-right">Rank</th>
|
||||
<th className="px-4 py-2.5 text-right">Cutoff</th>
|
||||
<th className="px-4 py-2.5 text-right">Max Pos</th>
|
||||
<th className="px-4 py-2.5 text-right">Risk</th>
|
||||
<th className="px-4 py-2.5 text-right">CAGR</th>
|
||||
<th className="px-4 py-2.5 text-right">Max DD</th>
|
||||
<th className="px-4 py-2.5 text-right">Sharpe</th>
|
||||
<th className="px-4 py-2.5 text-right">Total Ret</th>
|
||||
<th className="px-4 py-2.5 text-right">Trades</th>
|
||||
<th className="px-4 py-2.5 text-right">Skipped</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{report.strategy_variants.variants.map((row: BacktestStrategyVariant) => (
|
||||
<tr key={row.variant} className="border-b border-white/[0.04]">
|
||||
<td className="px-4 py-2.5 font-medium text-gray-200">{row.label}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-300">{row.ranking}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-300">{row.cutoff.toFixed(0)}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-300">{row.max_positions}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-300">
|
||||
{row.risk_scale === 'spy_200' ? '0.5-1.0%' : `${row.risk_per_trade_pct.toFixed(1)}%`}
|
||||
</td>
|
||||
<td className={`num px-4 py-2.5 text-right ${rColor(row.cagr_pct)}`}>{fmtSignedPct(row.cagr_pct)}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-amber-400">−{row.max_drawdown_pct.toFixed(1)}%</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-200">
|
||||
{row.sharpe === null ? '—' : row.sharpe.toFixed(2)}
|
||||
</td>
|
||||
<td className={`num px-4 py-2.5 text-right ${rColor(row.total_return_pct)}`}>{fmtSignedPct(row.total_return_pct)}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-300">{row.trades}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-500">{row.skipped_book_full}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report.signal_eval && report.signal_eval.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-widest text-gray-500">
|
||||
|
||||
@@ -294,6 +294,11 @@ export interface BacktestRecommendation {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface BacktestResearchRecommendation {
|
||||
items: { topic: string; text: string; candidate?: boolean }[];
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface BacktestPortfolioSim {
|
||||
params: {
|
||||
starting_capital: number;
|
||||
@@ -307,6 +312,21 @@ export interface BacktestPortfolioSim {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface BacktestStrategyVariant extends BacktestPortfolioPolicy {
|
||||
variant: string;
|
||||
label: string;
|
||||
ranking: 'raw' | 'residual' | string;
|
||||
cutoff: number;
|
||||
max_positions: number;
|
||||
risk_per_trade_pct: number;
|
||||
risk_scale: string | null;
|
||||
}
|
||||
|
||||
export interface BacktestStrategyVariants {
|
||||
variants: BacktestStrategyVariant[];
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface BacktestGateAblationRow extends BacktestBucket {
|
||||
variant: string;
|
||||
// The same variant graded under the hold-to-horizon time exit.
|
||||
@@ -347,7 +367,9 @@ export interface BacktestReport {
|
||||
gate_ablation_note?: string;
|
||||
time_exit_sweep?: BacktestTimeExitRow[];
|
||||
portfolio_sim?: BacktestPortfolioSim;
|
||||
strategy_variants?: BacktestStrategyVariants;
|
||||
recommendation?: BacktestRecommendation;
|
||||
research_recommendation?: BacktestResearchRecommendation;
|
||||
signal_eval?: BacktestSignalEvalRow[];
|
||||
signal_eval_note?: string;
|
||||
note: string;
|
||||
|
||||
Reference in New Issue
Block a user