UI: frame the setup as what it is — a momentum signal with a trailing exit
The UI told a swing-trade story (entry -> target -> stop) while the engine runs a momentum portfolio (buy strength, trail out, re-rank). The selection was honest; everything around it was borrowed from a strategy we don't run. The target is never an exit under `atr_trailing`: `_atr_trailing_close()` does not even take it as a parameter. It exists only to compute the R:R and touch odds that admit a setup through the activation gate. Backtested exit reasons for the production strategy: 144 initial stop, 98 trailing stop, 78 max hold — target 0. See docs/research/sr-levels-and-exits.md. What changed: - New ExitPlanPanel on every setup card states the rules that actually close the trade: initial stop (1R), the price at which the 3x ATR trail takes over from it, the trail width in R, and the max hold. Derived in lib/exitPlan.ts from the live exit policy, so it follows Admin rather than hardcoding the default. - New BaseRatesPanel replaces per-target "probability" as the answer to "what usually happens": win rate, average hold, best/worst R, and how trades actually ended — measured under the real exit, from the backtest report. - "Target"/"target probability" relabelled to "level"/"touch odds" and grouped as gate metrics, with the R:R. On the dashboard focus card, residual momentum (the actual signal) takes the headline stat those two used to occupy. - The take-trade dialog no longer offers a target dropdown whose value the exit ignores; it states the trailing plan instead. The picker returns only when the live policy is mode='target', where the choice is real. The stored target is now the setup's own, not whichever row was last clicked while exploring. - "Played out" is gone. A setup was declared dead once price reached the target — backwards under a trailing exit, where reaching a level is the good case and the trade keeps running. Only the stop invalidates a setup now; running past the entry is an "extended" warning, measured in R (you'd be chasing). The levels ladder, the price rail and the chart overlay all stay fully explorable — clicking a level still drives them. It is framed as overhead structure, which is what it is, rather than a menu of exits. Adds a parity guard: the UI recovers ATR as |entry - stop| / 1.5, so the test fails if the scanner's stop width ever moves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -92,7 +92,7 @@ function RadarSetupRow({ setup, rank, reason, name, selected, onSelect }: RadarR
|
||||
className={`grid cursor-pointer grid-cols-[20px_minmax(92px,116px)_44px_1fr_auto] items-center gap-2.5 rounded-lg px-2 py-2.5 transition-colors ${
|
||||
selected ? 'bg-blue-400/[0.08]' : 'hover:bg-white/[0.03]'
|
||||
} ${qualified ? '' : 'opacity-60'}`}
|
||||
title={`R:R ${setup.rr_ratio.toFixed(1)}:1${prob != null ? ` · target prob ${Math.round(prob)}%` : ''} · click to focus`}
|
||||
title={`gate: R:R ${setup.rr_ratio.toFixed(1)}:1${prob != null ? ` · touch odds ${Math.round(prob)}%` : ''} (screening, not an exit) · click to focus`}
|
||||
>
|
||||
<span className="num text-[11px] text-gray-500">{rank}</span>
|
||||
<span className="min-w-0">
|
||||
@@ -164,34 +164,41 @@ function FocusCard({ setup, name, badge, badgeTone, footNote, onReset }: {
|
||||
<span className="rounded-full border border-white/[0.09] px-2.5 py-0.5 text-[11.5px] text-gray-400">
|
||||
conviction {convictionLabel(setup.recommended_action)}
|
||||
</span>
|
||||
{setup.momentum_percentile != null && (
|
||||
<span className="rounded-full border border-white/[0.09] px-2.5 py-0.5 text-[11.5px] text-gray-400">
|
||||
residual momentum {Math.round(setup.momentum_percentile)}th %ile
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-10 text-right">
|
||||
<div>
|
||||
<p className="section-index">reward / risk</p>
|
||||
<p className="font-display mt-1 text-3xl font-semibold text-gray-100">
|
||||
{setup.rr_ratio.toFixed(1)}
|
||||
<span className="text-lg text-gray-400"> : 1</span>
|
||||
</p>
|
||||
</div>
|
||||
{prob != null && (
|
||||
<div>
|
||||
<p className="section-index">target probability</p>
|
||||
{/* The headline stat is the signal that actually selected this ticker.
|
||||
R:R and touch odds are gate inputs computed from an S/R level the
|
||||
trade never exits at — they get quiet, labelled treatment. */}
|
||||
<div className="flex items-start gap-10 text-right">
|
||||
{setup.momentum_percentile != null && (
|
||||
<div title="Residual 12-1 month momentum percentile across the universe. This is why the ticker was selected.">
|
||||
<p className="section-index">residual momentum</p>
|
||||
<p className="font-display mt-1 text-3xl font-semibold text-gray-100">
|
||||
{Math.round(prob)}
|
||||
top {Math.max(1, Math.round(100 - setup.momentum_percentile))}
|
||||
<span className="text-lg text-gray-400">%</span>
|
||||
</p>
|
||||
<div className="ml-auto mt-2 h-1 w-28 rounded-full bg-blue-500/20">
|
||||
<span className="block h-full rounded-full bg-blue-500" style={{ width: `${Math.round(prob)}%` }} />
|
||||
<span
|
||||
className="block h-full rounded-full bg-blue-500"
|
||||
style={{ width: `${Math.min(100, Math.round(setup.momentum_percentile))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="max-w-[13rem]"
|
||||
title="Gate metrics. The reward/risk and touch odds of the nearest S/R level are what admitted this setup through the activation gate. The trade does NOT exit at that level — it exits on the trailing stop."
|
||||
>
|
||||
<p className="section-index">gate metrics</p>
|
||||
<p className="num mt-1.5 text-sm text-gray-300">
|
||||
R:R {setup.rr_ratio.toFixed(1)}:1
|
||||
{prob != null && <> · touch {Math.round(prob)}%</>}
|
||||
</p>
|
||||
<p className="mt-1 text-[10.5px] leading-relaxed text-gray-500">
|
||||
screening only — exits on the trailing stop, not at the level
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user