import { useState } from 'react'; import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/button'; import { cn } from '@/utils/cn'; import { useAIAnalysis } from '@/hooks/useAIAnalysis'; import { X } from 'lucide-react'; /** * Trade Analysis Modal * Full-screen detailed analysis for evaluating individual trades */ export function TradeAnalysisModal({ row, isOpen, onClose }) { const [showAIAnalysis, setShowAIAnalysis] = useState(false); const { loading: aiLoading, error: aiError, analysis: aiAnalysis, fetchAnalysis } = useAIAnalysis(); if (!isOpen || !row) return null; const signal = row.tradeSignal; const symbol = row.Symbol || row.symbol_norm; const score = row.rocketScore || row.rocket_score || 0; const momentum = row.momentumScore || 0; const netPremium = (row.bull_total || 0) - (row.bear_total || 0); const bullTotal = row.bull_total || 0; const bearTotal = row.bear_total || 0; const premium = row.premium_num || 0; const volume = row.Volume || row.vol_num || 0; const oi = row.OI || row.oi_num || 0; const currentPrice = parseFloat(row.u_close || row.spot_num || row.Price || 0) || 0; // Ensure strike is always a number - handle string values like "150.00" or "$150" const strikeValue = row.Strike || 0; const strike = typeof strikeValue === 'string' ? parseFloat(strikeValue.replace(/[^0-9.-]/g, '')) || 0 : parseFloat(strikeValue) || 0; const callPut = row.CallPut || row.cp_norm || ''; const expiration = row.ExpirationDate; const moneyness = row.moneynessContext || {}; const isGamma = row.isGammaSqueezeSetup; const tapeAlign = row.TapeAlign || row.tapeArrow || '—'; const trend = row.flowTrendRaw || row.flowTrend?.full; const decay = row.flowDecayRaw || row.flowDecay?.full; const catalyst = row.near_alert_type || 'None'; const badges = row.badges || row.badgesRaw || {}; // Extract prices const extractPrice = (value) => { if (!value) return null; if (typeof value === 'string') { const match = value.match(/\$?([\d.]+)/); return match ? parseFloat(match[1]) : null; } if (typeof value === 'object') { const primary = value.primary || value.aggressive || value.tight || value.wide || value.t1; if (typeof primary === 'string') { const match = primary.match(/\$?([\d.]+)/); return match ? parseFloat(match[1]) : null; } return primary; } return null; }; const entryPrice = signal?.entry ? extractPrice(signal.entry) : currentPrice; const stopPrice = signal?.stop ? extractPrice(signal.stop) : null; const target1Price = signal?.target?.t1 ? extractPrice(signal.target.t1) : null; const target2Price = signal?.target?.t2 ? extractPrice(signal.target.t2) : null; const target3Price = signal?.target?.t3 ? extractPrice(signal.target.t3) : null; // Calculate R:R const risk = entryPrice && stopPrice ? Math.abs(entryPrice - stopPrice) : null; const reward1 = entryPrice && target1Price ? Math.abs(target1Price - entryPrice) : null; const riskReward1 = risk && reward1 ? (reward1 / risk).toFixed(2) : null; // Calculate win probability estimate const winProb = signal?.confidence || 50; // Get flow trend status const getFlowTrend = (lastFlowMinutes) => { if (lastFlowMinutes === null || lastFlowMinutes === undefined) { return null; } if (lastFlowMinutes <= 5) { return { icon: '🔥', label: 'SURGING', color: 'red', message: 'Institutions actively buying RIGHT NOW' }; } else if (lastFlowMinutes <= 30) { return { icon: '🟡', label: 'STABLE', color: 'yellow', message: 'Recent flow - institutions still positioned' }; } else if (lastFlowMinutes <= 60) { return { icon: '⚠️', label: 'FADING', color: 'orange', message: 'Flow slowing down - watch for reversal' }; } else { return { icon: '💀', label: 'DEAD', color: 'red', message: 'Flow too stale - institutions likely exited' }; } }; const lastFlowMinutesAgo = trend?.lastFlowMinutesAgo ?? decay?.lastFlowMinutesAgo; const flowTrend = getFlowTrend(lastFlowMinutesAgo); const hoursAgo = lastFlowMinutesAgo ? Math.floor(lastFlowMinutesAgo / 60) : 0; return (
{/* Header */}

{symbol}

{callPut} ${typeof strike === 'number' ? strike.toFixed(2) : 'N/A'} {isGamma && ( ⚡ GAMMA SQUEEZE )} {signal?.type && ( {signal.type.replace(/_/g, ' ')} )}
{/* Content - Scrollable */}
{/* Quick Stats Bar */}
🚀 Score
{score.toFixed(1)}
🔥 Momentum
{momentum}
📊 Confidence
{winProb}%
💰 Premium
${(premium / 1000000).toFixed(2)}M
{/* Trade Signal Card */} {signal && signal.signal !== 'NEUTRAL' && signal.signal !== 'WAIT' && (

🎯 TRADE PLAN

{signal.signal === 'BUY' ? '🚀 BUY' : '🔴 SHORT'}
{entryPrice && typeof entryPrice === 'number' && (
Entry
${entryPrice.toFixed(2)}
{signal.entry?.notes && (
{signal.entry.notes}
)}
)} {stopPrice && typeof stopPrice === 'number' && (
Stop Loss
${stopPrice.toFixed(2)}
{risk && typeof risk === 'number' && entryPrice && typeof entryPrice === 'number' && (
Risk: ${risk.toFixed(2)} ({((risk / entryPrice) * 100).toFixed(1)}%)
)}
)} {target1Price && typeof target1Price === 'number' && (
Target 1
${target1Price.toFixed(2)}
{riskReward1 && `R:R ${riskReward1}:1`} • Take 50%
)} {target2Price && typeof target2Price === 'number' && (
Target 2
${target2Price.toFixed(2)}
Take 30%
)} {target3Price && typeof target3Price === 'number' && (
Target 3
${target3Price.toFixed(2)}
Runner 20%
)}
{signal.reasoning && (
Reasoning:
{signal.reasoning}
)} {signal.invalidation && (
⚠️ Invalidation Rules:
{Array.isArray(signal.invalidation) ? (
    {signal.invalidation.map((rule, idx) => (
  • {rule}
  • ))}
) : (
{signal.invalidation}
)}
)}
)} {/* Flow Details */}

🔹 FLOW DETAILS

{/* Flow Trend Visual */} {flowTrend && (
{flowTrend.icon}
{flowTrend.label} FLOW
Last activity: {lastFlowMinutesAgo} minutes ago {hoursAgo > 0 && `(${hoursAgo} ${hoursAgo === 1 ? 'hour' : 'hours'})`}
⚠️ {flowTrend.message}
)}
Net Premium: 0 ? "text-green-400" : "text-red-400" )}> ${(Math.abs(netPremium) / 1000000).toFixed(2)}M
Bull Premium: ${(bullTotal / 1000000).toFixed(2)}M
Bear Premium: ${(bearTotal / 1000000).toFixed(2)}M
Volume: {volume.toLocaleString()}
Open Interest: {oi.toLocaleString()}
Moneyness: {row.moneynessLabel || `${moneyness.distancePct || 0}% ${moneyness.category || ''}`}

📊 PRICE CONTEXT

Current Price: ${typeof currentPrice === 'number' ? currentPrice.toFixed(2) : 'N/A'}
Strike: ${typeof strike === 'number' ? strike.toFixed(2) : 'N/A'}
Distance: 0 ? "text-red-400" : "text-green-400" )}> {moneyness.distancePct ? `${moneyness.distancePct > 0 ? '+' : ''}${moneyness.distancePct.toFixed(1)}%` : 'N/A'}
vs RTH Open: 0 ? "text-green-400" : "text-red-400" )}> {(row.PctVsRthOpen || 0) > 0 ? '+' : ''}{(row.PctVsRthOpen || 0).toFixed(2)}%
Flow Trend: {trend?.status || 'STABLE'}
{/* Tape Alignment Visual */} {tapeAlign !== '—' ? (
TAPE ALIGNED
Price confirming flow direction ↑
) : (
⚠️
NO TAPE ALIGNMENT
Price not confirming flow direction
Current Price
${typeof currentPrice === 'number' ? currentPrice.toFixed(2) : 'N/A'}
Expected: Upward momentum
Reality: Flat/down
)}
{/* Badges & Catalyst */}

🏆 BADGES

{badges.round && ( {badges.round} )} {badges.more && badges.more.split(' ').map((badge, idx) => ( {badge} ))} {badges.flash && ( {badges.flash} )}

⚡ CATALYST

{catalyst !== 'None' ? (
{catalyst} {trend?.lastFlowMinutesAgo !== null && (
Last flow: {trend.lastFlowMinutesAgo}min ago
)}
) : (
No alerts detected
)}
{/* AI Analysis */}

🤖 AI ANALYSIS

{!showAIAnalysis && ( )}
{showAIAnalysis && (
{aiLoading && (
Analyzing with Claude AI...
)} {aiError && (
Error: {aiError}
)} {aiAnalysis && aiAnalysis.analysis && (
Summary: {aiAnalysis.analysis.summary}
{/* Long Term */} {aiAnalysis.analysis.longTerm && (
Long-Term Investor
{aiAnalysis.analysis.longTerm.assessment} {aiAnalysis.analysis.longTerm.recommendation}

{aiAnalysis.analysis.longTerm.moatAndFundamentals}

)} {/* Swing Trade */} {aiAnalysis.analysis.swing && (
Swing Trader
{aiAnalysis.analysis.swing.assessment} {aiAnalysis.analysis.swing.recommendation}

{aiAnalysis.analysis.swing.technicalSetup}

)} {/* Day Trade */} {aiAnalysis.analysis.dayTrade && (
Day Trader
{aiAnalysis.analysis.dayTrade.assessment} {aiAnalysis.analysis.dayTrade.recommendation}

{aiAnalysis.analysis.dayTrade.volatilityAndLiquidity}

)}
)}
)}
{/* Footer Actions */}
Time Horizon: {signal?.timeHorizon || 'INTRADAY'} • Expiration: {expiration ? new Date(expiration).toLocaleDateString() : 'N/A'}
); }