import type { TradeSetup } from '../../lib/types'; import { formatPrice, formatPercent } from '../../lib/format'; import { recommendationActionDirection, recommendationActionLabel } from '../../lib/recommendation'; interface RecommendationPanelProps { symbol: string; longSetup?: TradeSetup; shortSetup?: TradeSetup; currentPrice?: number; } /** * How far current price has drifted from the setup's entry. A setup whose * entry is far from the live price (price already ran toward target, or fell * through the stop) is stale — entering now changes the risk/reward. */ function entryDrift(setup: TradeSetup, currentPrice?: number) { if (currentPrice == null || !setup.entry_price) return null; const pct = ((currentPrice - setup.entry_price) / setup.entry_price) * 100; const towardTarget = setup.direction === 'long' ? currentPrice >= setup.entry_price : currentPrice <= setup.entry_price; // Past the stop entirely = invalidated; moved >1/3 of the way to target = stale const span = Math.abs(setup.target - setup.entry_price); const moved = Math.abs(currentPrice - setup.entry_price); const beyondStop = setup.direction === 'long' ? currentPrice <= setup.stop_loss : currentPrice >= setup.stop_loss; let status: 'fresh' | 'stale' | 'invalidated' = 'fresh'; if (beyondStop) status = 'invalidated'; else if (span > 0 && moved / span > 0.33) status = 'stale'; return { pct, towardTarget, status }; } function riskClass(risk: TradeSetup['risk_level']) { if (risk === 'Low') return 'text-emerald-400'; if (risk === 'Medium') return 'text-amber-400'; if (risk === 'High') return 'text-red-400'; return 'text-gray-400'; } function isRecommended(setup: TradeSetup | undefined, action: TradeSetup['recommended_action'] | undefined) { if (!setup || !action) return false; if (setup.direction === 'long') return action.startsWith('LONG'); return action.startsWith('SHORT'); } function TargetTable({ setup }: { setup: TradeSetup }) { if (!setup.targets || setup.targets.length === 0) { return
No target probabilities available.
; } return (| Classification | Price | Distance | R:R | Probability |
|---|---|---|---|---|
| {target.classification} | {formatPrice(target.price)} | {formatPercent((target.distance_from_entry / setup.entry_price) * 100)} | {target.rr_ratio.toFixed(2)} | {target.probability.toFixed(1)}% |
Alternative setup (ticker bias currently favors the opposite direction).
)} {drift && drift.status === 'invalidated' && (⚠ Price ({formatPrice(currentPrice!)}) is past the stop — this setup is invalidated.
)} {drift && drift.status === 'stale' && (⚠ Price has moved {drift.pct >= 0 ? '+' : ''}{drift.pct.toFixed(1)}% from entry{drift.towardTarget ? ' toward target' : ' against the setup'} — entry may be stale.
)}Recommended Action is the ticker-level bias. The preferred setup is shown first; the opposite side is available under Alternative scenario.
{summary?.reasoning && ({summary.reasoning}
)} {preferredDirection !== 'neutral' && preferredSetup ? (