diff --git a/frontend/src/components/charts/horizon.tsx b/frontend/src/components/charts/horizon.tsx index bc33274..e69d16a 100644 --- a/frontend/src/components/charts/horizon.tsx +++ b/frontend/src/components/charts/horizon.tsx @@ -43,15 +43,18 @@ export function RBar({ r, max = 1.6 }: { r: number | null; max?: number }) { /* ------------------------------------------------------------------ */ export function PriceRail({ - direction, entry, stop, target, current, + direction, entry, stop, target, current, scaleTo, }: { direction: string; entry: number; stop: number; target: number; current: number | null; + /** Extra prices the scale must span (e.g. every target in the ladder), so + * switching targets moves the target marker, not stop/entry/now. */ + scaleTo?: number[]; }) { - const points = [entry, stop, target, ...(current != null ? [current] : [])]; + const points = [entry, stop, target, ...(current != null ? [current] : []), ...(scaleTo ?? [])]; const span = Math.max(...points) - Math.min(...points) || 1; const lo = Math.min(...points) - span * 0.06; const hi = Math.max(...points) + span * 0.06; diff --git a/frontend/src/components/ticker/RecommendationPanel.tsx b/frontend/src/components/ticker/RecommendationPanel.tsx index cd05d13..646934c 100644 --- a/frontend/src/components/ticker/RecommendationPanel.tsx +++ b/frontend/src/components/ticker/RecommendationPanel.tsx @@ -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 (
@@ -195,8 +207,14 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad const [takeTarget, setTakeTarget] = useState(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(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(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 */}
{recommended && ( @@ -254,6 +272,32 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad R:R {activeRR.toFixed(1)}:1 {activeProb != null && target prob {Math.round(activeProb)}%} {selected && !selected.is_primary && custom target} + + {sizing ? ( + + {sizing.shares} sh · {formatPrice(sizing.positionValue)} · risk {formatPrice(sizing.dollarRisk)} + {sizing.exceedsAccount && } + + ) : ( + set account size to size this + )} + {!taking && ( + + )} +
{/* Warnings — only when they apply */} @@ -288,40 +332,17 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad

)} - {/* 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 */} t.price)} /> - {/* Sizing + take, one line */} -
- {sizing ? ( - - {sizing.shares} shares · {formatPrice(sizing.positionValue)} position · max loss {formatPrice(sizing.dollarRisk)} - {sizing.exceedsAccount && exceeds account — needs margin} - - ) : ( - Set account size above to size this trade. - )} - {!taking && ( - - )} -
- {taking && (
@@ -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 ? (
- + {alternativeSetup && (
@@ -538,15 +565,15 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric Alternative scenario ({alternativeSetup.direction.toUpperCase()})
- +
)}
) : (
- - + +
)}
diff --git a/frontend/src/pages/TickerDetailPage.tsx b/frontend/src/pages/TickerDetailPage.tsx index ede919b..a187244 100644 --- a/frontend/src/pages/TickerDetailPage.tsx +++ b/frontend/src/pages/TickerDetailPage.tsx @@ -251,6 +251,13 @@ export default function TickerDetailPage() { // Which setup the chart overlays. 'auto' = the ticker's preferred direction. const [overlayChoice, setOverlayChoice] = useState<'auto' | 'long' | 'short' | 'none'>('auto'); + // Target chosen in the recommendation ladder, per direction — the chart + // overlay follows it. + const [chosenTargets, setChosenTargets] = useState<{ long: number | null; short: number | null }>({ + long: null, + short: null, + }); + const action = (longSetup ?? shortSetup)?.recommended_action ?? null; const overlaySetup: TradeSetup | undefined = useMemo(() => { if (overlayChoice === 'none') return undefined; @@ -263,6 +270,15 @@ export default function TickerDetailPage() { return candidates.sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0]; }, [overlayChoice, longSetup, shortSetup, action]); + // Chart overlay with the ladder-selected target applied (R:R from the ladder). + const overlayWithTarget: TradeSetup | undefined = useMemo(() => { + if (!overlaySetup) return undefined; + const chosen = chosenTargets[overlaySetup.direction as 'long' | 'short']; + if (chosen == null || chosen === overlaySetup.target) return overlaySetup; + const ladder = overlaySetup.targets?.find((t) => t.price === chosen); + return { ...overlaySetup, target: chosen, rr_ratio: ladder?.rr_ratio ?? overlaySetup.rr_ratio }; + }, [overlaySetup, chosenTargets]); + // Sort visible S/R levels by strength for the table (only levels within chart zones) const sortedLevels = useMemo(() => { if (!srLevels.data?.visible_levels) return []; @@ -429,7 +445,7 @@ export default function TickerDetailPage() { data={ohlcv.data} srLevels={srLevels.data?.levels} zones={srLevels.data?.zones} - tradeSetup={overlaySetup} + tradeSetup={overlayWithTarget} currentPrice={priceInfo?.price} />

@@ -447,6 +463,10 @@ export default function TickerDetailPage() { shortSetup={shortSetup} currentPrice={priceInfo?.price} nextEarningsDate={fundamentals.data?.next_earnings_date} + selectedTargets={chosenTargets} + onSelectTarget={(direction, price) => + setChosenTargets((s) => ({ ...s, [direction]: price })) + } />

)}