Ticker panel polish: freshness up top, radar pad, indicator grid,
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m4s
Deploy / deploy (push) Successful in 35s

verdict de-dup, target picker on take

Five fixes from prod review:

1. Data freshness row moves from the panel foot to directly under the
   header - data age is a first-look concern.
2. Radar fingerprint labels were clipped left ("Momentum") and right
   (value digits): label padding widened to fit "momentum 100" on both
   anchored sides.
3. Indicators tab: the one-at-a-time dropdown (which also clipped) is
   gone; all indicators load in parallel as quiet outlined cards - EMA
   cross with its colored signal, RSI/ADX with a one-line human read
   (overbought / trending / ...), values formatted to 2 decimals
   instead of 4, normalized score demoted to a corner note.
4. Recommendation verdict line de-duplicated: the backend reasoning
   already starts with the action label, so it now replaces the
   headline instead of repeating under it.
5. Mark-as-taken: the take form gains a target selector listing every
   detected target with price / probability / classification (primary
   preselected), and the card warns when the primary target sits below
   15% probability. Why the scanner promotes such a target to primary
   is a separate (backend) question.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 12:20:23 +02:00
co-authored by Claude Fable 5
parent d806ede07e
commit 03bdcbd745
4 changed files with 180 additions and 114 deletions
@@ -165,6 +165,7 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
const [taking, setTaking] = useState(false);
const [takeShares, setTakeShares] = useState<number>(sizing?.shares ?? 0);
const [takeEntry, setTakeEntry] = useState<number>(currentPrice ?? setup.entry_price);
const [takeTarget, setTakeTarget] = useState<number>(setup.target);
const confirmTake = () => {
createTrade.mutate(
@@ -174,7 +175,7 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
entry_price: takeEntry,
shares: takeShares,
stop_loss: setup.stop_loss,
target: setup.target,
target: takeTarget,
},
{ onSuccess: () => setTaking(false) },
);
@@ -245,6 +246,11 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
)}
</div>
)}
{prob != null && prob < 15 && (
<p className="mt-2.5 text-[11px] text-amber-400">
The primary target has only a {Math.round(prob)}% probability pick a nearer target from the list when taking the trade.
</p>
)}
{/* The setup, spatially — stop → entry → now → target */}
<PriceRail
@@ -270,6 +276,7 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
onClick={() => {
setTakeShares(sizing?.shares ?? 0);
setTakeEntry(currentPrice ?? setup.entry_price);
setTakeTarget(setup.target);
setTaking(true);
}}
className="rounded-lg border border-blue-500/35 bg-blue-500/15 px-3.5 py-1.5 text-xs font-semibold text-blue-300 transition-colors hover:bg-blue-500/25"
@@ -304,8 +311,24 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
/>
</label>
</div>
{setup.targets && setup.targets.length > 1 ? (
<label className="block space-y-1">
<span className="num text-[10px] uppercase tracking-wider text-gray-500">Target</span>
<select
value={takeTarget}
onChange={(e) => setTakeTarget(Number(e.target.value))}
className="input-glass w-full px-2 py-1.5 text-sm num"
>
{setup.targets.map((t) => (
<option key={`${t.sr_level_id}-${t.price}`} value={t.price} className="bg-[#14161f]">
{formatPrice(t.price)} · {t.probability.toFixed(0)}% · {t.classification}{t.is_primary ? ' · primary' : ''}
</option>
))}
</select>
</label>
) : null}
<p className="num text-[10px] text-gray-500">
Stop {formatPrice(setup.stop_loss)} · Target {formatPrice(setup.target)} · {setup.direction.toUpperCase()} paper trade
Stop {formatPrice(setup.stop_loss)} · Target {formatPrice(takeTarget)} · {setup.direction.toUpperCase()} paper trade
</p>
<div className="flex gap-2">
<button
@@ -421,11 +444,15 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
const body = (
<div className="space-y-4">
{/* One verdict line — the reasoning already contains the action label,
so it replaces it instead of repeating it. */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
{preferredInactive ? (
<span className="text-sm font-semibold text-gray-400">
No current setup <span className="font-normal text-gray-500">(last {preferredDirection} bias {recommendationActionLabel(action).toLowerCase()} {preferredInactive.invalidated ? 'invalidated' : 'played out'})</span>
</span>
) : summary?.reasoning ? (
<span className="text-sm leading-relaxed text-gray-200">{summary.reasoning}</span>
) : (
<span className="text-sm font-semibold text-blue-300">{recommendationActionLabel(action)}</span>
)}
@@ -437,10 +464,6 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
</div>
</div>
{summary?.reasoning && !preferredInactive && (
<p className="text-sm leading-relaxed text-gray-300">{summary.reasoning}</p>
)}
{earningsDays != null && earningsDays >= 0 && (
earningsDays <= EARNINGS_HORIZON_DAYS ? (
<p className="rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] text-amber-300">