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 && (
+ {
+ 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
+
+ )}
+
{/* 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 && (
- {
- 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
-
- )}
-
-
{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 }))
+ }
/>
)}