Ticker panel polish: freshness up top, radar pad, indicator grid,
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user