diff --git a/frontend/src/components/ticker/RecommendationPanel.tsx b/frontend/src/components/ticker/RecommendationPanel.tsx index 2297745..575eb2c 100644 --- a/frontend/src/components/ticker/RecommendationPanel.tsx +++ b/frontend/src/components/ticker/RecommendationPanel.tsx @@ -7,6 +7,8 @@ import { useRiskSettings, type RiskSettings } from '../../hooks/useRiskSettings' import { positionSize } from '../../lib/position'; import { useMarketRegime } from '../../hooks/useMarketRegime'; import { isCounterTrend } from '../../lib/regime'; +import { primaryTargetProbability } from '../../lib/qualification'; +import { PriceRail } from '../charts/horizon'; import type { MarketRegime } from '../../lib/types'; interface RecommendationPanelProps { @@ -15,6 +17,8 @@ interface RecommendationPanelProps { shortSetup?: TradeSetup; currentPrice?: number; nextEarningsDate?: string | null; + /** Render without the section/glass wrapper (inside the unified ticker panel). */ + frameless?: boolean; } /** Whole days from today until an ISO date (negative if past). */ @@ -65,9 +69,9 @@ function notActionableState(setup: TradeSetup, currentPrice?: number) { } function riskClass(risk: TradeSetup['risk_level']) { - if (risk === 'Low') return 'text-emerald-400'; + if (risk === 'Low') return 'text-emerald-300'; if (risk === 'Medium') return 'text-amber-400'; - if (risk === 'High') return 'text-red-400'; + if (risk === 'High') return 'text-red-300'; return 'text-gray-400'; } @@ -77,6 +81,25 @@ function isRecommended(setup: TradeSetup | undefined, action: TradeSetup['recomm return action.startsWith('SHORT'); } +function DirTag({ direction }: { direction: string }) { + const isLong = direction === 'long'; + return ( + + {direction} + + ); +} + +function Chip({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + function TargetTable({ setup }: { setup: TradeSetup }) { if (!setup.targets || setup.targets.length === 0) { return
No target probabilities available.
; @@ -119,7 +142,7 @@ function TargetTable({ setup }: { setup: TradeSetup }) { function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: TradeSetup; action?: TradeSetup['recommended_action']; currentPrice?: number; risk: RiskSettings; regime?: MarketRegime }) { if (!setup) { return ( -+
{invalidated ? `The last ${dir} setup is invalidated — price (${formatPrice(currentPrice!)}) has passed the stop (${formatPrice(setup.stop_loss)}). No fresh ${dir} setup right now; the scanner surfaces a new one when it forms.` : `The last ${dir} setup has played out — price (${formatPrice(currentPrice!)}) is at or past the target (${formatPrice(setup.target)}). No fresh ${dir} setup right now; the scanner surfaces a new one when it forms.`}
-Alternative setup (ticker bias currently favors the opposite direction).
- )} - - {counterTrend && regime && ( -- ⚠ Counter-trend: {setup.direction.toUpperCase()} against a {regime.label} market - ({regime.benchmark ?? 'SPY'}). Lower odds — size down or wait for confirmation. -
- )} - - {drift && drift.status === 'invalidated' && ( -- ⚠ Price ({formatPrice(currentPrice!)}) is past the stop — this setup is invalidated. -
- )} - {drift && drift.status === 'stale' && ( -- {drift.towardTarget - ? `⚠ ${drift.progressPct.toFixed(0)}% of the entry→target move is already gone (${drift.pct >= 0 ? '+' : ''}${drift.pct.toFixed(1)}% from entry) — little reward left.` - : `⚠ Price has moved ${Math.abs(drift.pct).toFixed(1)}% against the setup (toward the stop) — entry may be stale.`} -
- )} - -- Position size · {risk.riskPct}% of {formatPrice(risk.accountSize)} -
-Position exceeds account — needs margin.
+ {/* Warnings — only when they apply */} + {(counterTrend || (!recommended && recommendationActionDirection(action ?? null) !== 'neutral') || drift?.status !== 'fresh') && ( +Alternative setup — the ticker bias currently favors the opposite direction.
+ )} + {counterTrend && regime && ( ++ ⚠ Counter-trend: {setup.direction.toUpperCase()} against a {regime.label} market + ({regime.benchmark ?? 'SPY'}). Lower odds — size down or wait for confirmation. +
+ )} + {drift && drift.status === 'invalidated' && ( ++ ⚠ Price ({formatPrice(currentPrice!)}) is past the stop — this setup is invalidated. +
+ )} + {drift && drift.status === 'stale' && ( ++ {drift.towardTarget + ? `⚠ ${drift.progressPct.toFixed(0)}% of the entry→target move is already gone (${drift.pct >= 0 ? '+' : ''}${drift.pct.toFixed(1)}% from entry) — little reward left.` + : `⚠ Price has moved ${Math.abs(drift.pct).toFixed(1)}% against the setup (toward the stop) — entry may be stale.`} +
)}Set account size below to size this trade.
)} - {!taking ? ( - - ) : ( -- Stop {formatPrice(setup.stop_loss)} · Target {formatPrice(setup.target)} · {setup.direction.toUpperCase()} +
+ Stop {formatPrice(setup.stop_loss)} · Target {formatPrice(setup.target)} · {setup.direction.toUpperCase()} paper trade
{setup.conflict_flags.join(' • ')} -
{summary.reasoning}
+ )} + + {earningsDays != null && earningsDays >= 0 && ( + earningsDays <= EARNINGS_HORIZON_DAYS ? ( ++ ⚠ Earnings in {earningsDays} day{earningsDays === 1 ? '' : 's'} ({nextEarningsDate}) — inside the ~30-day + target horizon. A report can gap price through your stop or target; consider waiting or sizing down. +
+ ) : ( +Next earnings: {nextEarningsDate} ({earningsDays} days).
+ ) + )} + + {preferredDirection !== 'neutral' && preferredSetup ? ( +Recommendation · {symbol.toUpperCase()}
+ {body} +Recommended Action is the ticker-level bias. The preferred setup is shown first; the opposite side is available under Alternative scenario.
- - {summary?.reasoning && !preferredInactive && ( -{summary.reasoning}
- )} - - {earningsDays != null && earningsDays >= 0 && ( - earningsDays <= EARNINGS_HORIZON_DAYS ? ( -- ⚠ Earnings in {earningsDays} day{earningsDays === 1 ? '' : 's'} ({nextEarningsDate}) — inside the ~30-day - target horizon. A report can gap price through your stop or target; consider waiting or sizing down. -
- ) : ( -Next earnings: {nextEarningsDate} ({earningsDays} days).
- ) - )} - - {preferredDirection !== 'neutral' && preferredSetup ? ( -+ Composite{' '} + {Math.round(composite)} + {' '}= Base {Math.round(base)}{' '} + {adj >= 0 ? '+' : '−'} Sentiment{' '} + = 0 ? 'text-emerald-300/80' : 'text-red-300/80'}> + {Math.abs(adj).toFixed(1)} + +
+ ); + })()} + > + )} +- Composite{' '} - {Math.round(composite)} - {' '}= Base {Math.round(base)}{' '} - {adj >= 0 ? '+' : '−'} Sentiment{' '} - = 0 ? 'text-emerald-400/80' : 'text-red-400/80'}> - {Math.abs(adj).toFixed(1)} - -
- ); - })()} - > - )} -