Target selection syncs to chart; stable rail scale; sizing top-right
- The ladder target choice is lifted to the ticker page (per direction), so selecting a target updates the candlestick overlay's target line and zone too, with the R:R for the tooltip taken from the ladder row. - Price rail: the scale now always spans the entire target ladder, so stop / entry / now hold their positions and only the target marker moves when a different target is selected. The furthest target sits at the right edge. - Setup card layout: position sizing (shares / value / max loss, details in the tooltip) and the Mark-as-taken button move to the top right of the card header row, next to the stats they belong with - the dangling bottom row is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,9 @@ interface RecommendationPanelProps {
|
||||
nextEarningsDate?: string | null;
|
||||
/** Render without the section/glass wrapper (inside the unified ticker panel). */
|
||||
frameless?: boolean;
|
||||
/** Lifted target selection per direction, so the candlestick overlay follows. */
|
||||
selectedTargets?: { long: number | null; short: number | null };
|
||||
onSelectTarget?: (direction: 'long' | 'short', price: number) => void;
|
||||
}
|
||||
|
||||
/** Whole days from today until an ISO date (negative if past). */
|
||||
@@ -166,7 +169,16 @@ function TargetTable({ setup, selectedPrice, onSelect }: {
|
||||
);
|
||||
}
|
||||
|
||||
function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: TradeSetup; action?: TradeSetup['recommended_action']; currentPrice?: number; risk: RiskSettings; regime?: MarketRegime }) {
|
||||
function SetupCard({ setup, action, currentPrice, risk, regime, selectedPrice, onSelectPrice }: {
|
||||
setup?: TradeSetup;
|
||||
action?: TradeSetup['recommended_action'];
|
||||
currentPrice?: number;
|
||||
risk: RiskSettings;
|
||||
regime?: MarketRegime;
|
||||
/** Controlled target selection (lifted so the candlestick chart can follow). */
|
||||
selectedPrice?: number | null;
|
||||
onSelectPrice?: (price: number) => void;
|
||||
}) {
|
||||
if (!setup) {
|
||||
return (
|
||||
<div className="rounded-xl border border-white/[0.07] p-4 text-xs text-gray-500">
|
||||
@@ -195,8 +207,14 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
|
||||
const [takeTarget, setTakeTarget] = useState<number>(setup.target);
|
||||
|
||||
// Target choice from the ladder drives the rail, the chips, and the take
|
||||
// flow — the scanner's primary is just the default.
|
||||
const [selPrice, setSelPrice] = useState<number | null>(null);
|
||||
// flow — the scanner's primary is just the default. Controlled by the page
|
||||
// when provided (so the candlestick overlay follows), else local.
|
||||
const [internalSel, setInternalSel] = useState<number | null>(null);
|
||||
const selPrice = selectedPrice !== undefined ? selectedPrice : internalSel;
|
||||
const selectTargetPrice = (p: number) => {
|
||||
if (onSelectPrice) onSelectPrice(p);
|
||||
else setInternalSel(p);
|
||||
};
|
||||
const selected = selPrice != null ? (setup.targets ?? []).find((t) => t.price === selPrice) ?? null : null;
|
||||
const activePrice = selected?.price ?? setup.target;
|
||||
const activeRR = selected?.rr_ratio ?? setup.rr_ratio;
|
||||
@@ -242,7 +260,7 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
|
||||
data-direction={setup.direction}
|
||||
className={`rounded-xl border p-4 ${recommended ? 'border-blue-400/25' : 'border-white/[0.07] opacity-80'}`}
|
||||
>
|
||||
{/* Identity + key stats in one quiet row */}
|
||||
{/* Identity + key stats left, sizing + take top right */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DirTag direction={setup.direction} />
|
||||
{recommended && (
|
||||
@@ -254,6 +272,32 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
|
||||
<Chip>R:R {activeRR.toFixed(1)}:1</Chip>
|
||||
{activeProb != null && <Chip>target prob {Math.round(activeProb)}%</Chip>}
|
||||
{selected && !selected.is_primary && <Chip>custom target</Chip>}
|
||||
<span className="ml-auto flex flex-wrap items-center gap-3">
|
||||
{sizing ? (
|
||||
<span
|
||||
className="num text-xs text-gray-400"
|
||||
title={`Position size at ${risk.riskPct}% of ${formatPrice(risk.accountSize)}: ${sizing.shares} shares · ${formatPrice(sizing.positionValue)} position value · ${formatPrice(sizing.dollarRisk)} max loss at the stop`}
|
||||
>
|
||||
{sizing.shares} sh · {formatPrice(sizing.positionValue)} · risk {formatPrice(sizing.dollarRisk)}
|
||||
{sizing.exceedsAccount && <span className="ml-1.5 text-amber-400" title="Position exceeds account — needs margin">⚠</span>}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[11px] text-gray-600">set account size to size this</span>
|
||||
)}
|
||||
{!taking && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setTakeShares(sizing?.shares ?? 0);
|
||||
setTakeEntry(currentPrice ?? setup.entry_price);
|
||||
setTakeTarget(activePrice);
|
||||
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"
|
||||
>
|
||||
Mark as taken
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Warnings — only when they apply */}
|
||||
@@ -288,40 +332,17 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* The setup, spatially — stop → entry → now → the *selected* target */}
|
||||
{/* The setup, spatially — stop/entry/now hold still, the *selected*
|
||||
target moves along a scale that always spans the whole ladder */}
|
||||
<PriceRail
|
||||
direction={setup.direction}
|
||||
entry={setup.entry_price}
|
||||
stop={setup.stop_loss}
|
||||
target={activePrice}
|
||||
current={currentPrice ?? null}
|
||||
scaleTo={(setup.targets ?? []).map((t) => t.price)}
|
||||
/>
|
||||
|
||||
{/* Sizing + take, one line */}
|
||||
<div className="mt-1 flex flex-wrap items-center justify-between gap-3 border-t border-white/[0.06] pt-3">
|
||||
{sizing ? (
|
||||
<span className="num text-xs text-gray-400">
|
||||
{sizing.shares} shares · {formatPrice(sizing.positionValue)} position · max loss {formatPrice(sizing.dollarRisk)}
|
||||
{sizing.exceedsAccount && <span className="ml-2 text-amber-400">exceeds account — needs margin</span>}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[11px] text-gray-600">Set account size above to size this trade.</span>
|
||||
)}
|
||||
{!taking && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setTakeShares(sizing?.shares ?? 0);
|
||||
setTakeEntry(currentPrice ?? setup.entry_price);
|
||||
setTakeTarget(activePrice);
|
||||
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"
|
||||
>
|
||||
Mark as taken
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{taking && (
|
||||
<div className="mt-3 rounded-lg border border-white/[0.08] p-3 space-y-2.5">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -395,7 +416,7 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
|
||||
setup={setup}
|
||||
selectedPrice={activePrice}
|
||||
onSelect={(t) => {
|
||||
setSelPrice(t.price);
|
||||
selectTargetPrice(t.price);
|
||||
setTakeTarget(t.price);
|
||||
}}
|
||||
/>
|
||||
@@ -454,7 +475,13 @@ function RiskControls({ risk, update }: { risk: RiskSettings; update: (p: Partia
|
||||
);
|
||||
}
|
||||
|
||||
export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPrice, nextEarningsDate, frameless = false }: RecommendationPanelProps) {
|
||||
export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPrice, nextEarningsDate, frameless = false, selectedTargets, onSelectTarget }: RecommendationPanelProps) {
|
||||
const selFor = (setup?: TradeSetup) =>
|
||||
setup && selectedTargets ? selectedTargets[setup.direction as 'long' | 'short'] : undefined;
|
||||
const onSelFor = (setup?: TradeSetup) =>
|
||||
setup && onSelectTarget
|
||||
? (price: number) => onSelectTarget(setup.direction as 'long' | 'short', price)
|
||||
: undefined;
|
||||
const { settings: risk, update: updateRisk } = useRiskSettings();
|
||||
const regime = useMarketRegime().data;
|
||||
const summary = longSetup?.recommendation_summary ?? shortSetup?.recommendation_summary;
|
||||
@@ -530,7 +557,7 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
|
||||
|
||||
{preferredDirection !== 'neutral' && preferredSetup ? (
|
||||
<div className="space-y-3">
|
||||
<SetupCard setup={preferredSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} />
|
||||
<SetupCard setup={preferredSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} selectedPrice={selFor(preferredSetup)} onSelectPrice={onSelFor(preferredSetup)} />
|
||||
|
||||
{alternativeSetup && (
|
||||
<details>
|
||||
@@ -538,15 +565,15 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
|
||||
Alternative scenario ({alternativeSetup.direction.toUpperCase()})
|
||||
</summary>
|
||||
<div className="mt-3">
|
||||
<SetupCard setup={alternativeSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} />
|
||||
<SetupCard setup={alternativeSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} selectedPrice={selFor(alternativeSetup)} onSelectPrice={onSelFor(alternativeSetup)} />
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<SetupCard setup={longSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} />
|
||||
<SetupCard setup={shortSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} />
|
||||
<SetupCard setup={longSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} selectedPrice={selFor(longSetup)} onSelectPrice={onSelFor(longSetup)} />
|
||||
<SetupCard setup={shortSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} selectedPrice={selFor(shortSetup)} onSelectPrice={onSelFor(shortSetup)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user