Promote production portfolio strategy
This commit is contained in:
@@ -6,14 +6,16 @@ import { SkeletonCard } from '../ui/Skeleton';
|
||||
export function ExitPolicySettings() {
|
||||
const { data, isLoading } = useExitPolicy();
|
||||
const update = useUpdateExitPolicy();
|
||||
const [mode, setMode] = useState<ExitPolicy['mode']>('time');
|
||||
const [mode, setMode] = useState<ExitPolicy['mode']>('atr_trailing');
|
||||
const [pct, setPct] = useState(12);
|
||||
const [atrMultiplier, setAtrMultiplier] = useState(3);
|
||||
const [holdDays, setHoldDays] = useState(30);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setMode(data.mode);
|
||||
setPct(data.trailing_pct);
|
||||
setAtrMultiplier(data.atr_multiplier ?? 3);
|
||||
setHoldDays(data.hold_days ?? 30);
|
||||
}
|
||||
}, [data]);
|
||||
@@ -26,14 +28,15 @@ export function ExitPolicySettings() {
|
||||
<h3 className="text-sm font-semibold text-gray-200">Paper-Trade Exit</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
How open paper trades auto-close (in the nightly/intraday outcome job).{' '}
|
||||
<span className="text-gray-300">Hold</span> keeps the initial stop and exits at the Nth trading
|
||||
day's close — the backtest-validated exit (classic momentum: hold ~a month, re-rank);{' '}
|
||||
<span className="text-gray-300">Trailing</span> rides a trailing stop;{' '}
|
||||
<span className="text-gray-300">ATR trail</span> is the promoted production exit: initial stop,
|
||||
ATR trailing stop, and a max N-trading-day hold;{' '}
|
||||
<span className="text-gray-300">Hold</span> keeps only the initial stop until the Nth trading
|
||||
day's close; <span className="text-gray-300">Percent trail</span> is the older trailing mode;{' '}
|
||||
<span className="text-gray-300">Target / stop</span> closes at the setup's target or stop.
|
||||
The setup's initial stop is always the floor.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-gray-400">Exit mode</span>
|
||||
<select
|
||||
@@ -41,8 +44,9 @@ export function ExitPolicySettings() {
|
||||
onChange={(e) => setMode(e.target.value as ExitPolicy['mode'])}
|
||||
className="w-full input-glass px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="atr_trailing">ATR trail + max hold</option>
|
||||
<option value="time">Hold N days + stop</option>
|
||||
<option value="trailing">Trailing stop</option>
|
||||
<option value="trailing">Percent trailing stop</option>
|
||||
<option value="target">Target / stop</option>
|
||||
</select>
|
||||
</label>
|
||||
@@ -55,10 +59,24 @@ export function ExitPolicySettings() {
|
||||
step={1}
|
||||
value={holdDays}
|
||||
onChange={(e) => setHoldDays(Number(e.target.value))}
|
||||
disabled={mode !== 'time'}
|
||||
disabled={mode !== 'time' && mode !== 'atr_trailing'}
|
||||
className="w-full input-glass px-3 py-2 text-sm disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-[11px] text-gray-600">Backtest optimum: 30 (its evaluation horizon).</span>
|
||||
<span className="text-[11px] text-gray-600">Production max hold: 30 trading days.</span>
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-gray-400">ATR multiplier</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0.5}
|
||||
max={10}
|
||||
step={0.25}
|
||||
value={atrMultiplier}
|
||||
onChange={(e) => setAtrMultiplier(Number(e.target.value))}
|
||||
disabled={mode !== 'atr_trailing'}
|
||||
className="w-full input-glass px-3 py-2 text-sm disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-[11px] text-gray-600">Promoted strategy: 3x ATR.</span>
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-gray-400">Trailing width (%)</span>
|
||||
@@ -72,13 +90,18 @@ export function ExitPolicySettings() {
|
||||
disabled={mode !== 'trailing'}
|
||||
className="w-full input-glass px-3 py-2 text-sm disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-[11px] text-gray-600">Give-back from the peak. ≥15% ≈ the hold exit.</span>
|
||||
<span className="text-[11px] text-gray-600">Legacy percent trail from the peak.</span>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
className="btn-primary px-4 py-2 text-sm disabled:opacity-50"
|
||||
disabled={update.isPending}
|
||||
onClick={() => update.mutate({ mode, trailing_pct: pct, hold_days: holdDays })}
|
||||
onClick={() => update.mutate({
|
||||
mode,
|
||||
trailing_pct: pct,
|
||||
atr_multiplier: atrMultiplier,
|
||||
hold_days: holdDays,
|
||||
})}
|
||||
>
|
||||
{update.isPending ? 'Saving…' : 'Save Exit Policy'}
|
||||
</button>
|
||||
|
||||
@@ -24,9 +24,13 @@ export function OpenTradesPanel() {
|
||||
const close = useClosePaperTrade();
|
||||
|
||||
const exitLabel = policy
|
||||
? policy.mode === 'trailing'
|
||||
? `auto-exit: trailing ${Math.round(policy.trailing_pct)}%`
|
||||
: 'auto-exit: target/stop'
|
||||
? policy.mode === 'atr_trailing'
|
||||
? `auto-exit: ${(policy.atr_multiplier ?? 3).toFixed(1)}x ATR trail / ${policy.hold_days}d max`
|
||||
: policy.mode === 'trailing'
|
||||
? `auto-exit: trailing ${Math.round(policy.trailing_pct)}%`
|
||||
: policy.mode === 'time'
|
||||
? `auto-exit: ${policy.hold_days}d hold`
|
||||
: 'auto-exit: target/stop'
|
||||
: null;
|
||||
|
||||
const totals = useMemo(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useBacktestReport } from '../../hooks/useMarketRegime';
|
||||
import { triggerJob } from '../../api/admin';
|
||||
@@ -6,7 +7,13 @@ import { Callout } from '../ui/Callout';
|
||||
import { Disclosure } from '../ui/Disclosure';
|
||||
import { Section } from '../ui/Section';
|
||||
import { useToast } from '../ui/Toast';
|
||||
import type { BacktestBucket, BacktestPortfolioPolicy, BacktestStrategyVariant } from '../../lib/types';
|
||||
import type {
|
||||
BacktestBucket,
|
||||
BacktestCurvePoint,
|
||||
BacktestPortfolioMonitorRun,
|
||||
BacktestPortfolioPolicy,
|
||||
BacktestStrategyVariant,
|
||||
} from '../../lib/types';
|
||||
|
||||
function fmtR(v: number | null | undefined): string {
|
||||
if (v === null || v === undefined) return '—';
|
||||
@@ -23,6 +30,9 @@ 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`;
|
||||
}
|
||||
@@ -120,16 +130,109 @@ function BucketRow({ label, b }: { label: string; b: BacktestBucket }) {
|
||||
);
|
||||
}
|
||||
|
||||
function curvePath(
|
||||
points: BacktestCurvePoint[],
|
||||
min: number,
|
||||
max: number,
|
||||
w: number,
|
||||
h: number,
|
||||
pad: number,
|
||||
startMs: number,
|
||||
endMs: number,
|
||||
): string {
|
||||
if (points.length < 2) return '';
|
||||
const span = Math.max(max - min, 1);
|
||||
const timeSpan = Math.max(endMs - startMs, 1);
|
||||
return points
|
||||
.map((p, i) => {
|
||||
const t = new Date(p.date).getTime();
|
||||
const x = pad + ((t - startMs) / timeSpan) * (w - pad * 2);
|
||||
const value = p.return_pct ?? 0;
|
||||
const y = pad + (1 - (value - min) / span) * (h - pad * 2);
|
||||
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function EquityCurveChart({ run }: { run: BacktestPortfolioMonitorRun }) {
|
||||
const portfolio = run.equity_curve ?? [];
|
||||
const benchmark = run.benchmark_curve ?? [];
|
||||
const values = [...portfolio, ...benchmark]
|
||||
.map((p) => p.return_pct)
|
||||
.filter((v): v is number => v !== null && v !== undefined);
|
||||
if (portfolio.length < 2 || values.length === 0) {
|
||||
return <Callout variant="empty">No equity curve points for this selection.</Callout>;
|
||||
}
|
||||
|
||||
const min = Math.min(0, ...values);
|
||||
const max = Math.max(0, ...values);
|
||||
const times = [...portfolio, ...benchmark]
|
||||
.map((p) => new Date(p.date).getTime())
|
||||
.filter((v) => Number.isFinite(v));
|
||||
if (times.length === 0) {
|
||||
return <Callout variant="empty">No dated equity curve points for this selection.</Callout>;
|
||||
}
|
||||
const startMs = Math.min(...times);
|
||||
const endMs = Math.max(...times);
|
||||
const w = 720;
|
||||
const h = 240;
|
||||
const pad = 28;
|
||||
const portfolioPath = curvePath(portfolio, min, max, w, h, pad, startMs, endMs);
|
||||
const benchmarkPath = curvePath(benchmark, min, max, w, h, pad, startMs, endMs);
|
||||
const lastPortfolio = portfolio[portfolio.length - 1]?.return_pct ?? null;
|
||||
const lastBenchmark = benchmark[benchmark.length - 1]?.return_pct ?? run.spy_return_pct;
|
||||
|
||||
return (
|
||||
<div className="glass overflow-hidden">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-white/[0.05] px-4 py-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-100">{run.label}</p>
|
||||
<p className="text-[11px] text-gray-500">{run.start_date} - {run.end_date}</p>
|
||||
</div>
|
||||
<div className="flex gap-4 text-xs">
|
||||
<span className="text-blue-300">Portfolio {fmtSignedPct(lastPortfolio)}</span>
|
||||
<span className="text-gray-400">S&P 500 {fmtSignedPct(lastBenchmark)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox={`0 0 ${w} ${h}`} className="h-64 w-full" role="img" aria-label="Portfolio return compared with S&P 500">
|
||||
<line x1={pad} y1={h - pad} x2={w - pad} y2={h - pad} stroke="rgba(255,255,255,0.12)" />
|
||||
<line x1={pad} y1={pad} x2={pad} y2={h - pad} stroke="rgba(255,255,255,0.12)" />
|
||||
{benchmarkPath && (
|
||||
<path d={benchmarkPath} fill="none" stroke="rgba(156,163,175,0.9)" strokeWidth="2" strokeDasharray="5 5" />
|
||||
)}
|
||||
<path d={portfolioPath} fill="none" stroke="rgb(96,165,250)" strokeWidth="3" />
|
||||
<text x={pad} y={pad - 8} className="fill-gray-500 text-[10px]">{fmtSignedPct(max)}</text>
|
||||
<text x={pad} y={h - 8} className="fill-gray-500 text-[10px]">{fmtSignedPct(min)}</text>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BacktestPanel() {
|
||||
const { data: report, isLoading } = useBacktestReport();
|
||||
const queryClient = useQueryClient();
|
||||
const toast = useToast();
|
||||
const [selectedStrategy, setSelectedStrategy] = useState('');
|
||||
const [selectedLookback, setSelectedLookback] = useState('');
|
||||
|
||||
const bestTimeAvgR =
|
||||
report?.time_exit_sweep && report.time_exit_sweep.length > 0
|
||||
? Math.max(...report.time_exit_sweep.map((r) => netOrGross(r) ?? -Infinity))
|
||||
: null;
|
||||
const sim = report?.portfolio_sim ?? null;
|
||||
const monitor = report?.portfolio_monitor ?? null;
|
||||
const activeStrategy =
|
||||
selectedStrategy || monitor?.production_strategy || monitor?.strategies[0]?.strategy || '';
|
||||
const activeLookback =
|
||||
selectedLookback || (monitor?.lookbacks.some((l) => l.lookback === '3y') ? '3y' : monitor?.lookbacks[0]?.lookback) || '';
|
||||
const monitorRun = useMemo(
|
||||
() =>
|
||||
monitor?.runs.find((row) => row.strategy === activeStrategy && row.lookback === activeLookback) ??
|
||||
monitor?.runs.find((row) => row.strategy === activeStrategy) ??
|
||||
monitor?.runs[0] ??
|
||||
null,
|
||||
[monitor, activeStrategy, activeLookback],
|
||||
);
|
||||
|
||||
const run = useMutation({
|
||||
mutationFn: () => triggerJob('backtest'),
|
||||
@@ -182,6 +285,58 @@ export function BacktestPanel() {
|
||||
)}
|
||||
</p>
|
||||
|
||||
{monitor && monitorRun && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<p className="section-index">Portfolio monitor</p>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Cached portfolio simulation for supported strategies, compared with S&P 500.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<label className="flex flex-col gap-1 text-[11px] uppercase tracking-wider text-gray-500">
|
||||
Strategy
|
||||
<select
|
||||
value={activeStrategy}
|
||||
onChange={(e) => setSelectedStrategy(e.target.value)}
|
||||
className="rounded border border-white/10 bg-slate-950 px-3 py-2 text-xs normal-case tracking-normal text-gray-200"
|
||||
>
|
||||
{monitor.strategies.map((s) => (
|
||||
<option key={s.strategy} value={s.strategy}>
|
||||
{s.is_production ? 'Production: ' : ''}{s.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-[11px] uppercase tracking-wider text-gray-500">
|
||||
Lookback
|
||||
<select
|
||||
value={activeLookback}
|
||||
onChange={(e) => setSelectedLookback(e.target.value)}
|
||||
className="rounded border border-white/10 bg-slate-950 px-3 py-2 text-xs normal-case tracking-normal text-gray-200"
|
||||
>
|
||||
{monitor.lookbacks.map((l) => (
|
||||
<option key={l.lookback} value={l.lookback}>{l.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<Stat label="CAGR" value={fmtSignedPct(monitorRun.cagr_pct)} valueClass={rColor(monitorRun.cagr_pct)} />
|
||||
<Stat label="Sharpe" value={monitorRun.sharpe == null ? '—' : monitorRun.sharpe.toFixed(2)} />
|
||||
<Stat label="Max Drawdown" value={fmtDrawdown(monitorRun.max_drawdown_pct)} valueClass="text-amber-400" />
|
||||
<Stat label="Total Return" value={fmtSignedPct(monitorRun.total_return_pct)} valueClass={rColor(monitorRun.total_return_pct)} />
|
||||
<Stat label="Trades" value={String(monitorRun.trades)} sub={`${fmtPct(monitorRun.win_rate)} win rate`} />
|
||||
</div>
|
||||
|
||||
<EquityCurveChart run={monitorRun} />
|
||||
{monitor.note && <p className="text-[11px] text-gray-600">{monitor.note}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report.recommendation && report.recommendation.items.length > 0 && (
|
||||
<div className="glass border border-blue-400/20 p-4">
|
||||
<p className="section-index">What this backtest recommends</p>
|
||||
@@ -487,8 +642,8 @@ export function BacktestPanel() {
|
||||
<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">Metric</th>
|
||||
{sim.policies.map((p) => (
|
||||
<th key={p.policy} className="px-4 py-2.5 text-right">
|
||||
{POLICY_LABELS[p.policy] ?? p.policy}
|
||||
<th key={p.policy ?? 'policy'} className="px-4 py-2.5 text-right">
|
||||
{POLICY_LABELS[p.policy ?? ''] ?? p.policy ?? 'Policy'}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
@@ -523,7 +678,7 @@ export function BacktestPanel() {
|
||||
<tr key={label} className="border-b border-white/[0.04]">
|
||||
<td className="px-4 py-2.5 font-medium text-gray-200">{label}</td>
|
||||
{sim.policies.map((p) => (
|
||||
<td key={p.policy} className={`num px-4 py-2.5 text-right ${color(p)}`}>
|
||||
<td key={p.policy ?? label} className={`num px-4 py-2.5 text-right ${color(p)}`}>
|
||||
{fmt(p)}
|
||||
</td>
|
||||
))}
|
||||
|
||||
@@ -75,7 +75,9 @@ export function topPickSymbol(
|
||||
if (all.length === 0) return null;
|
||||
const qualified = activation ? all.filter((t) => qualifiesSetup(t, activation)) : [];
|
||||
const top = [...qualified].sort(
|
||||
(a, b) => (b.momentum_percentile ?? -Infinity) - (a.momentum_percentile ?? -Infinity),
|
||||
(a, b) =>
|
||||
(b.strategy_rank ?? b.momentum_percentile ?? -Infinity) -
|
||||
(a.strategy_rank ?? a.momentum_percentile ?? -Infinity),
|
||||
)[0];
|
||||
return top?.symbol ?? null;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface WatchlistEntry {
|
||||
rr_ratio: number | null;
|
||||
rr_direction: string | null;
|
||||
momentum_percentile: number | null;
|
||||
strategy_rank: number | null;
|
||||
sr_levels: SRLevelSummary[];
|
||||
last_close: number | null;
|
||||
change_pct: number | null;
|
||||
@@ -141,6 +142,8 @@ export interface TradeSetup {
|
||||
evaluated_at: string | null;
|
||||
current_price: number | null;
|
||||
momentum_percentile?: number | null;
|
||||
strategy_rank?: number | null;
|
||||
volatility_percentile?: number | null;
|
||||
context_as_of?: TradeSetupContextAsOf | null;
|
||||
recommendation_summary?: RecommendationSummary;
|
||||
}
|
||||
@@ -227,8 +230,9 @@ export interface PaperTrade {
|
||||
}
|
||||
|
||||
export interface ExitPolicy {
|
||||
mode: 'time' | 'trailing' | 'target';
|
||||
mode: 'time' | 'trailing' | 'atr_trailing' | 'target';
|
||||
trailing_pct: number;
|
||||
atr_multiplier: number;
|
||||
hold_days: number;
|
||||
}
|
||||
|
||||
@@ -276,7 +280,7 @@ export interface BacktestTimeExitRow {
|
||||
}
|
||||
|
||||
export interface BacktestPortfolioPolicy {
|
||||
policy: string;
|
||||
policy?: string;
|
||||
starting_capital: number;
|
||||
final_equity: number;
|
||||
total_return_pct: number;
|
||||
@@ -294,10 +298,19 @@ export interface BacktestPortfolioPolicy {
|
||||
skipped_book_full: number;
|
||||
spy_return_pct: number | null;
|
||||
yearly_returns?: { year: number; return_pct: number | null }[];
|
||||
exit_reasons?: Record<string, number>;
|
||||
equity_curve?: BacktestCurvePoint[];
|
||||
benchmark_curve?: BacktestCurvePoint[];
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
}
|
||||
|
||||
export interface BacktestCurvePoint {
|
||||
date: string;
|
||||
equity: number;
|
||||
return_pct: number | null;
|
||||
}
|
||||
|
||||
export interface BacktestRecommendation {
|
||||
headline: string | null;
|
||||
items: { topic: string; text: string }[];
|
||||
@@ -337,6 +350,25 @@ export interface BacktestStrategyVariants {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface BacktestPortfolioMonitorRun extends BacktestPortfolioPolicy {
|
||||
strategy: string;
|
||||
label: string;
|
||||
description: string;
|
||||
is_production: boolean;
|
||||
entry_variant: string;
|
||||
exit_policy: string;
|
||||
lookback: string;
|
||||
lookback_label: string;
|
||||
}
|
||||
|
||||
export interface BacktestPortfolioMonitor {
|
||||
production_strategy: string;
|
||||
strategies: { strategy: string; label: string; description: string; is_production: boolean }[];
|
||||
lookbacks: { lookback: string; label: string }[];
|
||||
runs: BacktestPortfolioMonitorRun[];
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface BacktestGateAblationRow extends BacktestBucket {
|
||||
variant: string;
|
||||
// The same variant graded under the hold-to-horizon time exit.
|
||||
@@ -378,6 +410,8 @@ export interface BacktestReport {
|
||||
time_exit_sweep?: BacktestTimeExitRow[];
|
||||
portfolio_sim?: BacktestPortfolioSim;
|
||||
strategy_variants?: BacktestStrategyVariants;
|
||||
exit_policy_variants?: { variants: BacktestStrategyVariant[]; note?: string };
|
||||
portfolio_monitor?: BacktestPortfolioMonitor | null;
|
||||
recommendation?: BacktestRecommendation;
|
||||
research_recommendation?: BacktestResearchRecommendation;
|
||||
signal_eval?: BacktestSignalEvalRow[];
|
||||
|
||||
@@ -76,10 +76,16 @@ export default function DashboardPage() {
|
||||
[trades.data, activation.data],
|
||||
);
|
||||
|
||||
// Rank only actionable/qualified setups by residual 12-1 momentum percentile.
|
||||
// Rank only actionable/qualified setups by the production strategy score.
|
||||
// Residual momentum still gates qualification; strategy_rank is the promoted
|
||||
// 80/20 residual-momentum/high-vol ordering score when available.
|
||||
const topSetups: TradeSetup[] = useMemo(() => {
|
||||
return [...qualifiedSetups]
|
||||
.sort((a, b) => (b.momentum_percentile ?? -Infinity) - (a.momentum_percentile ?? -Infinity))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(b.strategy_rank ?? b.momentum_percentile ?? -Infinity) -
|
||||
(a.strategy_rank ?? a.momentum_percentile ?? -Infinity),
|
||||
)
|
||||
.slice(0, 5);
|
||||
}, [qualifiedSetups]);
|
||||
|
||||
@@ -194,7 +200,7 @@ export default function DashboardPage() {
|
||||
<div className="xl:col-span-3">
|
||||
<Section
|
||||
title="Top Setups"
|
||||
hint="qualified and ranked by residual momentum"
|
||||
hint="qualified by residual momentum, ranked by production score"
|
||||
>
|
||||
{trades.isLoading && <SkeletonTable rows={5} cols={5} />}
|
||||
{trades.isError && <Callout variant="error">Failed to load setups</Callout>}
|
||||
@@ -211,7 +217,8 @@ 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">Residual Mom.</th>
|
||||
<th className="px-4 py-3 text-right">Prod. Rank</th>
|
||||
<th className="px-4 py-3 text-right">Residual</th>
|
||||
<th className="hidden px-4 py-3 md:table-cell">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -252,6 +259,9 @@ export default function DashboardPage() {
|
||||
})()}
|
||||
</td>
|
||||
<td className="num px-4 py-3 text-right font-semibold text-gray-200">
|
||||
{setup.strategy_rank != null ? `${Math.round(setup.strategy_rank)}%ile` : '—'}
|
||||
</td>
|
||||
<td className="num px-4 py-3 text-right text-gray-400">
|
||||
{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">
|
||||
@@ -264,7 +274,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">
|
||||
Momentum = ticker's 12-1 month rank across the universe (higher = stronger)
|
||||
Production rank = 80% residual momentum + 20% realized volatility; residual still gates qualification.
|
||||
</span>
|
||||
<Link to="/signals" className="text-xs font-medium text-blue-300 hover:text-blue-200 transition-colors">
|
||||
All setups →
|
||||
|
||||
@@ -296,7 +296,7 @@ export default function TickerDetailPage() {
|
||||
<StatusPill
|
||||
tone="blue"
|
||||
label="★ Top Pick"
|
||||
title="Current top pick — highest residual-momentum qualified setup right now"
|
||||
title="Current top pick - highest production-ranked qualified setup right now"
|
||||
/>
|
||||
)}
|
||||
{hasOpenTrade && (
|
||||
|
||||
Reference in New Issue
Block a user