institutional-trader/frontend/src/components/tables/TradeAnalysisModal.jsx

583 lines
27 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="w-full max-w-6xl max-h-[90vh] bg-slate-900 border border-slate-800 rounded-lg shadow-2xl overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-800 bg-slate-950/50">
<div className="flex items-center gap-4">
<div>
<h2 className="text-2xl font-bold text-slate-100">{symbol}</h2>
<div className="flex items-center gap-2 mt-1">
<Badge
variant="default"
className={cn(
"text-xs",
callPut === 'CALL'
? "bg-blue-500/20 text-blue-400 border-blue-500/50"
: "bg-red-500/20 text-red-400 border-red-500/50"
)}
>
{callPut} ${typeof strike === 'number' ? strike.toFixed(2) : 'N/A'}
</Badge>
{isGamma && (
<Badge className="bg-purple-500/20 text-purple-400 border-purple-500/50 text-xs">
GAMMA SQUEEZE
</Badge>
)}
{signal?.type && (
<Badge variant="outline" className="text-xs">
{signal.type.replace(/_/g, ' ')}
</Badge>
)}
</div>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="text-slate-400 hover:text-slate-200"
>
<X className="w-5 h-5" />
</Button>
</div>
{/* Content - Scrollable */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* Quick Stats Bar */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-800/50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">🚀 Score</div>
<div className="text-2xl font-bold text-slate-100">{score.toFixed(1)}</div>
</div>
<div className="bg-slate-800/50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">🔥 Momentum</div>
<div className="text-2xl font-bold text-slate-100">{momentum}</div>
</div>
<div className="bg-slate-800/50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">📊 Confidence</div>
<div className="text-2xl font-bold text-slate-100">{winProb}%</div>
</div>
<div className="bg-slate-800/50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">💰 Premium</div>
<div className="text-2xl font-bold text-slate-100">
${(premium / 1000000).toFixed(2)}M
</div>
</div>
</div>
{/* Trade Signal Card */}
{signal && signal.signal !== 'NEUTRAL' && signal.signal !== 'WAIT' && (
<div className="bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-500/30 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-slate-100">🎯 TRADE PLAN</h3>
<Badge
variant={signal.signal === 'BUY' ? 'success' : 'destructive'}
className="text-sm px-3 py-1"
>
{signal.signal === 'BUY' ? '🚀 BUY' : '🔴 SHORT'}
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{entryPrice && typeof entryPrice === 'number' && (
<div className="bg-slate-800/70 rounded-lg p-4">
<div className="text-xs text-slate-400 mb-1">Entry</div>
<div className="text-xl font-bold text-green-400">${entryPrice.toFixed(2)}</div>
{signal.entry?.notes && (
<div className="text-xs text-slate-500 mt-1">{signal.entry.notes}</div>
)}
</div>
)}
{stopPrice && typeof stopPrice === 'number' && (
<div className="bg-slate-800/70 rounded-lg p-4">
<div className="text-xs text-slate-400 mb-1">Stop Loss</div>
<div className="text-xl font-bold text-red-400">${stopPrice.toFixed(2)}</div>
{risk && typeof risk === 'number' && entryPrice && typeof entryPrice === 'number' && (
<div className="text-xs text-slate-500 mt-1">
Risk: ${risk.toFixed(2)} ({((risk / entryPrice) * 100).toFixed(1)}%)
</div>
)}
</div>
)}
{target1Price && typeof target1Price === 'number' && (
<div className="bg-slate-800/70 rounded-lg p-4">
<div className="text-xs text-slate-400 mb-1">Target 1</div>
<div className="text-xl font-bold text-blue-400">${target1Price.toFixed(2)}</div>
<div className="text-xs text-slate-500 mt-1">
{riskReward1 && `R:R ${riskReward1}:1`} Take 50%
</div>
</div>
)}
{target2Price && typeof target2Price === 'number' && (
<div className="bg-slate-800/70 rounded-lg p-4">
<div className="text-xs text-slate-400 mb-1">Target 2</div>
<div className="text-xl font-bold text-blue-400">${target2Price.toFixed(2)}</div>
<div className="text-xs text-slate-500 mt-1">Take 30%</div>
</div>
)}
{target3Price && typeof target3Price === 'number' && (
<div className="bg-slate-800/70 rounded-lg p-4">
<div className="text-xs text-slate-400 mb-1">Target 3</div>
<div className="text-xl font-bold text-blue-400">${target3Price.toFixed(2)}</div>
<div className="text-xs text-slate-500 mt-1">Runner 20%</div>
</div>
)}
</div>
{signal.reasoning && (
<div className="mt-4 p-4 bg-slate-800/50 rounded-lg">
<div className="text-xs text-slate-400 mb-2">Reasoning:</div>
<div className="text-sm text-slate-200">{signal.reasoning}</div>
</div>
)}
{signal.invalidation && (
<div className="mt-4 p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
<div className="text-xs text-red-400 mb-2 font-semibold"> Invalidation Rules:</div>
<div className="text-sm text-slate-300">
{Array.isArray(signal.invalidation) ? (
<ul className="list-disc list-inside space-y-1">
{signal.invalidation.map((rule, idx) => (
<li key={idx}>{rule}</li>
))}
</ul>
) : (
<div>{signal.invalidation}</div>
)}
</div>
</div>
)}
</div>
)}
{/* Flow Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-slate-800/50 rounded-lg p-4">
<h4 className="text-sm font-semibold text-slate-300 mb-3">🔹 FLOW DETAILS</h4>
{/* Flow Trend Visual */}
{flowTrend && (
<div className={cn(
"mb-4 p-4 border-2 rounded-lg",
flowTrend.color === 'red' && flowTrend.label === 'DEAD'
? "bg-red-900/30 border-red-500"
: flowTrend.color === 'red' && flowTrend.label === 'SURGING'
? "bg-red-900/30 border-red-500"
: flowTrend.color === 'yellow'
? "bg-yellow-900/30 border-yellow-500"
: "bg-orange-900/30 border-orange-500"
)}>
<div className="flex items-center gap-3">
<span className="text-5xl">{flowTrend.icon}</span>
<div>
<div className={cn(
"text-2xl font-bold",
flowTrend.color === 'red' ? "text-red-400" :
flowTrend.color === 'yellow' ? "text-yellow-400" :
"text-orange-400"
)}>
{flowTrend.label} FLOW
</div>
<div className="text-sm text-gray-300">
Last activity: <span className={cn(
"font-bold",
flowTrend.color === 'red' ? "text-red-300" :
flowTrend.color === 'yellow' ? "text-yellow-300" :
"text-orange-300"
)}>
{lastFlowMinutesAgo} minutes ago {hoursAgo > 0 && `(${hoursAgo} ${hoursAgo === 1 ? 'hour' : 'hours'})`}
</span>
</div>
<div className={cn(
"text-sm mt-1",
flowTrend.color === 'red' ? "text-yellow-300" :
flowTrend.color === 'yellow' ? "text-yellow-200" :
"text-orange-200"
)}>
{flowTrend.message}
</div>
</div>
</div>
</div>
)}
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Net Premium:</span>
<span className={cn(
"font-semibold",
netPremium > 0 ? "text-green-400" : "text-red-400"
)}>
${(Math.abs(netPremium) / 1000000).toFixed(2)}M
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Bull Premium:</span>
<span className="text-green-400">${(bullTotal / 1000000).toFixed(2)}M</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Bear Premium:</span>
<span className="text-red-400">${(bearTotal / 1000000).toFixed(2)}M</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Volume:</span>
<span className="text-slate-300">{volume.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Open Interest:</span>
<span className="text-slate-300">{oi.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Moneyness:</span>
<span className="text-slate-300">
{row.moneynessLabel || `${moneyness.distancePct || 0}% ${moneyness.category || ''}`}
</span>
</div>
</div>
</div>
<div className="bg-slate-800/50 rounded-lg p-4">
<h4 className="text-sm font-semibold text-slate-300 mb-3">📊 PRICE CONTEXT</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Current Price:</span>
<span className="text-slate-200 font-semibold">
${typeof currentPrice === 'number' ? currentPrice.toFixed(2) : 'N/A'}
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Strike:</span>
<span className="text-slate-200">
${typeof strike === 'number' ? strike.toFixed(2) : 'N/A'}
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Distance:</span>
<span className={cn(
"font-semibold",
moneyness.distancePct > 0 ? "text-red-400" : "text-green-400"
)}>
{moneyness.distancePct ? `${moneyness.distancePct > 0 ? '+' : ''}${moneyness.distancePct.toFixed(1)}%` : 'N/A'}
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">vs RTH Open:</span>
<span className={cn(
"font-semibold",
(row.PctVsRthOpen || 0) > 0 ? "text-green-400" : "text-red-400"
)}>
{(row.PctVsRthOpen || 0) > 0 ? '+' : ''}{(row.PctVsRthOpen || 0).toFixed(2)}%
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Flow Trend:</span>
<span className={cn(
"font-semibold",
trend?.trend === 'SURGING' ? "text-green-400" :
trend?.trend === 'FADING' ? "text-red-400" :
"text-yellow-400"
)}>
{trend?.status || 'STABLE'}
</span>
</div>
</div>
{/* Tape Alignment Visual */}
{tapeAlign !== '—' ? (
<div className="mt-4 p-4 bg-green-900/30 border-2 border-green-500 rounded-lg">
<div className="flex items-center gap-3">
<span className="text-4xl"></span>
<div>
<div className="text-xl font-bold text-green-400">TAPE ALIGNED</div>
<div className="text-sm text-gray-300">Price confirming flow direction </div>
</div>
</div>
</div>
) : (
<div className="mt-4 p-4 bg-orange-900/30 border-2 border-orange-500 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-4xl"></span>
<div>
<div className="text-xl font-bold text-orange-400">NO TAPE ALIGNMENT</div>
<div className="text-sm text-gray-300">Price not confirming flow direction</div>
</div>
</div>
<div className="text-right">
<div className="text-xs text-gray-400">Current Price</div>
<div className="text-2xl font-bold text-orange-300">${typeof currentPrice === 'number' ? currentPrice.toFixed(2) : 'N/A'}</div>
<div className="text-xs text-gray-400">Expected: Upward momentum</div>
<div className="text-xs text-red-300">Reality: Flat/down</div>
</div>
</div>
</div>
)}
</div>
</div>
{/* Badges & Catalyst */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-slate-800/50 rounded-lg p-4">
<h4 className="text-sm font-semibold text-slate-300 mb-3">🏆 BADGES</h4>
<div className="flex flex-wrap gap-2">
{badges.round && (
<Badge variant="outline" className="text-lg">{badges.round}</Badge>
)}
{badges.more && badges.more.split(' ').map((badge, idx) => (
<Badge key={idx} variant="outline" className="text-lg">{badge}</Badge>
))}
{badges.flash && (
<Badge variant="outline" className="text-lg">{badges.flash}</Badge>
)}
</div>
</div>
<div className="bg-slate-800/50 rounded-lg p-4">
<h4 className="text-sm font-semibold text-slate-300 mb-3"> CATALYST</h4>
{catalyst !== 'None' ? (
<div>
<Badge variant="warning" className="text-sm mb-2">{catalyst}</Badge>
{trend?.lastFlowMinutesAgo !== null && (
<div className="text-sm text-slate-400 mt-2">
Last flow: {trend.lastFlowMinutesAgo}min ago
</div>
)}
</div>
) : (
<div className="text-slate-500 text-sm">No alerts detected</div>
)}
</div>
</div>
{/* AI Analysis */}
<div className="bg-slate-800/50 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-semibold text-slate-300">🤖 AI ANALYSIS</h4>
{!showAIAnalysis && (
<Button
variant="outline"
size="sm"
onClick={() => {
setShowAIAnalysis(true);
fetchAnalysis(row);
}}
disabled={aiLoading}
className="text-xs"
>
{aiLoading ? 'Analyzing...' : 'Get AI Analysis'}
</Button>
)}
</div>
{showAIAnalysis && (
<div className="space-y-3">
{aiLoading && (
<div className="text-sm text-slate-400">Analyzing with Claude AI...</div>
)}
{aiError && (
<div className="text-sm text-red-400">Error: {aiError}</div>
)}
{aiAnalysis && aiAnalysis.analysis && (
<div className="space-y-4">
<div className="text-sm text-slate-200 bg-slate-700/30 p-3 rounded border border-slate-600/50">
<strong>Summary:</strong> {aiAnalysis.analysis.summary}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Long Term */}
{aiAnalysis.analysis.longTerm && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Long-Term Investor</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.longTerm.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.longTerm.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.longTerm.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.longTerm.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.longTerm.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.longTerm.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.longTerm.moatAndFundamentals}</p>
</div>
)}
{/* Swing Trade */}
{aiAnalysis.analysis.swing && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Swing Trader</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.swing.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.swing.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.swing.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.swing.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.swing.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.swing.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.swing.technicalSetup}</p>
</div>
)}
{/* Day Trade */}
{aiAnalysis.analysis.dayTrade && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Day Trader</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.dayTrade.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.dayTrade.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.dayTrade.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.dayTrade.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.dayTrade.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.dayTrade.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.dayTrade.volatilityAndLiquidity}</p>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Footer Actions */}
<div className="flex items-center justify-between p-4 border-t border-slate-800 bg-slate-950/50">
<div className="text-xs text-slate-500">
Time Horizon: {signal?.timeHorizon || 'INTRADAY'}
Expiration: {expiration ? new Date(expiration).toLocaleDateString() : 'N/A'}
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={onClose}>
Close
</Button>
<Button
variant="default"
size="sm"
className="bg-blue-600 hover:bg-blue-700"
>
Add to Watchlist
</Button>
</div>
</div>
</div>
</div>
);
}