← Назад
import { useState, useEffect, useMemo } from 'react' import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query' import { Activity, ShieldAlert, ArrowUpRight, ArrowDownRight, Info, AlertTriangle, TrendingUp, TrendingDown, AlignVerticalJustifyCenter, Bell, BellRing, BellOff, Table2, BarChart3 } from 'lucide-react' import { registerSW } from 'virtual:pwa-register' // Create a react-query client const queryClient = new QueryClient({ defaultOptions: { queries: { refetchInterval: 30000, // auto refresh every 30s refetchOnWindowFocus: true, }, }, }) // Main App wrapped in QueryProvider function App() { return ( <QueryClientProvider client={queryClient}> <Dashboard /> </QueryClientProvider> ) } // ═══════════════════════════════════════════════════════════════ // P&L CALCULATOR MODAL // ═══════════════════════════════════════════════════════════════ function PnLCalculator({ isOpen, onClose, prefill }) { const [optionType, setOptionType] = useState(prefill?.type || 'CALL') const [strike, setStrike] = useState(prefill?.strike || 0) const [premium, setPremium] = useState(prefill?.premium || 0) // per-unit premium const [qty, setQty] = useState(prefill?.qty || 1) // contracts const [spot, setSpot] = useState(prefill?.spot || 0) const unit = prefill?.unit || 1 // XRP=100, DOGE=1000, others=1 // Update when prefill changes useEffect(() => { if (prefill) { setOptionType(prefill.type || 'CALL') setStrike(prefill.strike || 0) setPremium(prefill.premium || 0) setQty(prefill.qty || 1) setSpot(prefill.spot || 0) } }, [prefill]) if (!isOpen) return null const isCall = optionType === 'CALL' // Premium is per-unit, breakeven uses per-unit math const breakeven = isCall ? strike + premium : strike - premium // Total cost = premium_per_unit Γ— unit Γ— qty_contracts const maxLoss = premium * unit * qty const totalCost = premium * unit * qty // Generate P&L at different spot prices const spotRange = [] const center = spot || strike const step = center * 0.01 // 1% steps for (let i = -15; i <= 15; i++) { const s = center + (i * step) if (s <= 0) continue let pnl if (isCall) { const intrinsic = Math.max(0, s - strike) pnl = (intrinsic - premium) * unit * qty } else { const intrinsic = Math.max(0, strike - s) pnl = (intrinsic - premium) * unit * qty } spotRange.push({ spot: s, pnl }) } const maxProfit = Math.max(...spotRange.map(r => r.pnl)) const maxPnL = Math.max(Math.abs(maxProfit), Math.abs(maxLoss)) // Bar chart helpers const barWidth = (pnl) => Math.min(100, Math.abs(pnl) / maxPnL * 100) const barColor = (pnl) => pnl >= 0 ? 'bg-green-500' : 'bg-red-500' return ( <div className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4" onClick={onClose}> <div className="bg-darkBg border border-darkBorder rounded-xl max-w-lg w-full max-h-[90vh] overflow-y-auto shadow-2xl" onClick={e => e.stopPropagation()}> {/* Header */} <div className="flex justify-between items-center px-4 py-3 border-b border-darkBorder"> <h3 className="text-lg font-bold text-white flex items-center gap-2">πŸ“Š P&L Calculator</h3> <button onClick={onClose} className="text-gray-400 hover:text-white text-xl">βœ•</button> </div> {/* Inputs */} <div className="px-4 py-3 grid grid-cols-2 gap-3"> <div> <label className="text-[10px] text-gray-500 uppercase">Type</label> <div className="flex gap-1 mt-1"> <button onClick={() => setOptionType('CALL')} className={`flex-1 py-1.5 rounded text-xs font-bold ${isCall ? 'bg-green-600 text-white' : 'bg-darkSurface text-gray-400 border border-darkBorder'}`}>CALL</button> <button onClick={() => setOptionType('PUT')} className={`flex-1 py-1.5 rounded text-xs font-bold ${!isCall ? 'bg-red-600 text-white' : 'bg-darkSurface text-gray-400 border border-darkBorder'}`}>PUT</button> </div> </div> <div> <label className="text-[10px] text-gray-500 uppercase">Qty</label> <input type="number" value={qty} onChange={e => setQty(e.target.value === '' ? '' : parseFloat(e.target.value))} onBlur={() => { if (!qty || qty < 0.01) setQty(0.01) }} step="0.01" min="0.01" className="w-full mt-1 bg-darkSurface border border-darkBorder rounded px-2 py-1.5 text-white text-sm font-mono" /> </div> <div> <label className="text-[10px] text-gray-500 uppercase">Strike ($)</label> <input type="number" value={strike} onChange={e => setStrike(parseFloat(e.target.value) || 0)} step="any" className="w-full mt-1 bg-darkSurface border border-darkBorder rounded px-2 py-1.5 text-white text-sm font-mono" /> </div> <div> <label className="text-[10px] text-gray-500 uppercase">Premium {unit > 1 ? `($/unit, 1ct=${unit})` : '($)'}</label> <input type="number" value={premium} onChange={e => setPremium(parseFloat(e.target.value) || 0)} step="any" className="w-full mt-1 bg-darkSurface border border-darkBorder rounded px-2 py-1.5 text-white text-sm font-mono" /> </div> <div className="col-span-2"> <label className="text-[10px] text-gray-500 uppercase">Current Spot ($)</label> <input type="number" value={spot} onChange={e => setSpot(parseFloat(e.target.value) || 0)} step="any" className="w-full mt-1 bg-darkSurface border border-darkBorder rounded px-2 py-1.5 text-white text-sm font-mono" /> </div> </div> {/* Key Metrics */} <div className="px-4 py-2 grid grid-cols-3 gap-2 text-xs font-mono"> <div className="bg-darkSurface rounded p-2 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">BREAKEVEN</div> <div className="text-yellow-400 font-bold">${breakeven.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div> </div> <div className="bg-darkSurface rounded p-2 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">MAX LOSS</div> <div className="text-red-400 font-bold">-${maxLoss.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div> </div> <div className="bg-darkSurface rounded p-2 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">TOTAL COST</div> <div className="text-white font-bold">${totalCost.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div> </div> </div> {/* Payoff Diagram (horizontal bar chart) */} <div className="px-4 py-3"> <div className="text-[10px] text-gray-500 uppercase mb-2">Payoff at Expiration</div> <div className="space-y-px"> {spotRange.map((r, i) => { const isBreakeven = Math.abs(r.spot - breakeven) < step * 0.6 const isCurrentSpot = spot > 0 && Math.abs(r.spot - spot) < step * 0.6 return ( <div key={i} className={`flex items-center gap-1 text-[10px] font-mono py-0.5 ${isCurrentSpot ? 'bg-blue-500/10 rounded' : ''} ${isBreakeven ? 'bg-yellow-500/10 rounded' : ''}`}> <span className={`w-16 text-right flex-shrink-0 ${isCurrentSpot ? 'text-blue-400 font-bold' : isBreakeven ? 'text-yellow-400' : 'text-gray-500'}`}> ${r.spot >= 1000 ? (r.spot/1000).toFixed(1) + 'K' : r.spot.toFixed(2)} {isCurrentSpot ? ' β—„' : ''} </span> <div className="flex-1 flex items-center h-3"> {r.pnl >= 0 ? ( <div className="flex items-center w-full"> <div className="w-1/2"></div> <div className={`h-2.5 rounded-r ${barColor(r.pnl)}`} style={{ width: `${barWidth(r.pnl) / 2}%` }}></div> </div> ) : ( <div className="flex items-center justify-end w-full"> <div className={`h-2.5 rounded-l ${barColor(r.pnl)}`} style={{ width: `${barWidth(r.pnl) / 2}%` }}></div> <div className="w-1/2"></div> </div> )} </div> <span className={`w-16 text-right flex-shrink-0 ${r.pnl >= 0 ? 'text-green-400' : 'text-red-400'}`}> {r.pnl >= 0 ? '+' : ''}{r.pnl < 1000 && r.pnl > -1000 ? r.pnl.toFixed(2) : (r.pnl/1000).toFixed(1) + 'K'} </span> </div> ) })} </div> {/* Legend */} <div className="flex gap-4 mt-2 text-[9px] text-gray-500"> <span>β—„ = current spot</span> <span className="text-yellow-400/50">β–ˆ = breakeven zone</span> <span className="text-green-400/50">β–ˆ = profit</span> <span className="text-red-400/50">β–ˆ = loss</span> </div> </div> </div> </div> ) } // ═══════════════════════════════════════════════════════════════ // WHALE FLOW FEED // ═══════════════════════════════════════════════════════════════ function WhaleFlow() { const [wfAsset, setWfAsset] = useState('ALL') const [minPremium, setMinPremium] = useState(1000) const assets = ['ALL', 'BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'DOGE'] const premiumFilters = [1000, 5000, 10000, 50000] const baseUrl = import.meta.env.DEV ? 'http://localhost:8080' : '' const { data, isLoading } = useQuery({ queryKey: ['whaleFlow', wfAsset, minPremium], queryFn: async () => { const params = new URLSearchParams({ minPremium: minPremium.toString(), limit: '50' }) if (wfAsset !== 'ALL') params.set('underlying', wfAsset) const res = await fetch(`${baseUrl}/api/whale-flow?${params}`) if (!res.ok) throw new Error('Failed') return res.json() }, refetchInterval: 30000, }) const flows = data?.data || [] const summary = data?.summary || {} const fmtUsd = (n) => { if (n >= 1000000) return `$${(n/1000000).toFixed(1)}M` if (n >= 1000) return `$${(n/1000).toFixed(1)}K` return `$${n?.toFixed(0)}` } const sizeEmoji = (size) => { if (size === 'WHALE') return 'πŸ‹' if (size === 'LARGE') return '🦈' if (size === 'NOTABLE') return '🐟' return 'Β·' } const sentimentColor = (s) => s === 'BULLISH' ? 'text-green-400' : 'text-red-400' return ( <div> {/* Controls */} <div className="flex flex-wrap gap-3 mb-4 items-center"> <div className="flex gap-1 bg-darkSurface p-1 rounded-lg border border-darkBorder"> {assets.map(a => ( <button key={a} onClick={() => setWfAsset(a)} className={`px-3 py-1 rounded text-xs font-bold transition-colors ${ wfAsset === a ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white' }`}>{a}</button> ))} </div> <div className="flex gap-1 bg-darkSurface p-1 rounded-lg border border-darkBorder"> <span className="text-[10px] text-gray-500 flex items-center px-1">Min:</span> {premiumFilters.map(p => ( <button key={p} onClick={() => setMinPremium(p)} className={`px-2 py-1 rounded text-[10px] font-mono transition-colors ${ minPremium === p ? 'bg-amber-600 text-white' : 'text-gray-400 hover:text-white' }`}>{fmtUsd(p)}</button> ))} </div> </div> {/* Summary Bar */} {summary.totalPremium > 0 && ( <div className="flex gap-3 mb-4 text-xs font-mono"> <div className="bg-darkSurface rounded-lg px-3 py-2 border border-darkBorder flex-1 text-center"> <div className="text-gray-500 text-[9px]">TOTAL FLOW</div> <div className="text-white font-bold">{fmtUsd(summary.totalPremium)}</div> </div> <div className="bg-green-500/10 rounded-lg px-3 py-2 border border-green-500/20 flex-1 text-center"> <div className="text-green-500/70 text-[9px]">🟒 BULLISH</div> <div className="text-green-400 font-bold">{fmtUsd(summary.bullishPremium)}</div> </div> <div className="bg-red-500/10 rounded-lg px-3 py-2 border border-red-500/20 flex-1 text-center"> <div className="text-red-500/70 text-[9px]">πŸ”΄ BEARISH</div> <div className="text-red-400 font-bold">{fmtUsd(summary.bearishPremium)}</div> </div> <div className="bg-darkSurface rounded-lg px-3 py-2 border border-darkBorder flex-1 text-center"> <div className="text-gray-500 text-[9px]">BULL/BEAR</div> <div className={`font-bold ${summary.bullBearRatio > 1 ? 'text-green-400' : 'text-red-400'}`}> {summary.bullBearRatio?.toFixed(2)}x </div> </div> </div> )} {isLoading && ( <div className="flex justify-center py-12"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500"></div> </div> )} {!isLoading && flows.length > 0 && ( <div className="overflow-x-auto rounded-lg border border-darkBorder"> <table className="w-full text-[11px] font-mono border-collapse"> <thead> <tr className="bg-darkSurface text-gray-400 border-b border-darkBorder"> <th className="px-2 py-2 text-left">Size</th> <th className="px-2 py-2 text-left">Asset</th> <th className="px-2 py-2 text-left">Contract</th> <th className="px-2 py-2 text-right">Premium</th> <th className="px-2 py-2 text-right">Vol</th> <th className="px-2 py-2 text-right">Price</th> <th className="px-2 py-2 text-right">V/OI</th> <th className="px-2 py-2 text-right">IV</th> <th className="px-2 py-2 text-right">Ξ”</th> <th className="px-2 py-2 text-right">DTE</th> <th className="px-2 py-2 text-center">Dir</th> </tr> </thead> <tbody> {flows.map((f, i) => ( <tr key={i} className={`border-b border-darkBorder/30 hover:bg-darkSurface/50 ${ f.size === 'WHALE' ? 'bg-amber-500/5' : '' }`}> <td className="px-2 py-1.5">{sizeEmoji(f.size)}</td> <td className="px-2 py-1.5 text-white font-bold">{f.underlying}</td> <td className="px-2 py-1.5"> <span className={f.type === 'CALL' ? 'text-green-400' : 'text-red-400'}>{f.type}</span> <span className="text-gray-400 ml-1">${f.strike.toLocaleString()}</span> <span className="text-gray-600 ml-1">{f.expiry.substring(2,4)}/{f.expiry.substring(4,6)}</span> </td> <td className="px-2 py-1.5 text-right"> <span className={`font-bold ${f.size === 'WHALE' ? 'text-amber-400' : f.size === 'LARGE' ? 'text-amber-300' : 'text-white'}`}> {fmtUsd(f.premium)} </span> </td> <td className="px-2 py-1.5 text-right text-gray-300">{f.volume.toFixed(1)}</td> <td className="px-2 py-1.5 text-right text-gray-400">${f.lastPrice < 1 ? f.lastPrice.toFixed(4) : f.lastPrice.toFixed(2)}</td> <td className="px-2 py-1.5 text-right"> <span className={f.voiRatio > 5 ? 'text-amber-400 font-bold' : 'text-gray-500'}> {f.voiRatio > 100 ? '∞' : f.voiRatio.toFixed(1)}x </span> </td> <td className="px-2 py-1.5 text-right text-gray-400">{f.iv ? (f.iv * 100).toFixed(0) + '%' : 'β€”'}</td> <td className="px-2 py-1.5 text-right text-gray-300">{f.delta?.toFixed(3) || 'β€”'}</td> <td className="px-2 py-1.5 text-right"> <span className={f.dte <= 3 ? 'text-red-400' : f.dte <= 7 ? 'text-yellow-400' : 'text-gray-400'}> {f.dte}d </span> </td> <td className="px-2 py-1.5 text-center"> <span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${ f.sentiment === 'BULLISH' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400' }`}> {f.sentiment === 'BULLISH' ? '🟒' : 'πŸ”΄'} </span> </td> </tr> ))} </tbody> </table> </div> )} {!isLoading && flows.length === 0 && ( <div className="text-center text-gray-500 py-12 bg-darkSurface rounded-xl border border-darkBorder"> No whale activity above {fmtUsd(minPremium)} threshold. Try lowering the filter. </div> )} </div> ) } // ═══════════════════════════════════════════════════════════════ // OPTIONS CHAIN VIEW // ═══════════════════════════════════════════════════════════════ function ChainView() { const [chainAsset, setChainAsset] = useState('BTC') const [chainExpiry, setChainExpiry] = useState(null) const assets = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'DOGE'] const baseUrl = import.meta.env.DEV ? 'http://localhost:8080' : '' const { data, isLoading } = useQuery({ queryKey: ['chain', chainAsset, chainExpiry], queryFn: async () => { const params = new URLSearchParams({ underlying: chainAsset }) if (chainExpiry) params.set('expiry', chainExpiry) const res = await fetch(`${baseUrl}/api/chain?${params}`) if (!res.ok) throw new Error('Failed to fetch chain') return res.json() }, refetchInterval: 30000, }) // When asset changes, reset expiry to let API pick nearest useEffect(() => { setChainExpiry(null) }, [chainAsset]) // When data arrives and no expiry set, default to first useEffect(() => { if (data?.expiry && !chainExpiry) setChainExpiry(data.expiry) }, [data?.expiry]) const chain = data?.chain || [] const spot = data?.spotApprox || 0 // Find ATM index for scroll-to const atmIndex = useMemo(() => { return chain.findIndex(r => r.moneyness === 'ATM') }, [chain]) // Auto-scroll to ATM on load useEffect(() => { if (atmIndex >= 0) { const el = document.getElementById(`strike-${atmIndex}`) if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }) } }, [atmIndex, chainExpiry]) const fmtNum = (n, d = 2) => n != null ? Number(n).toFixed(d) : 'β€”' const fmtK = (n) => n >= 1000 ? `${(n/1000).toFixed(1)}K` : n?.toString() || '0' // Color for IV relative (green = low, red = high) const ivColor = (iv) => { if (!iv) return 'text-gray-500' if (iv < 0.3) return 'text-green-400' if (iv < 0.6) return 'text-yellow-400' return 'text-red-400' } // Highlight rows near ATM const rowBg = (row) => { if (row.moneyness === 'ATM') return 'bg-blue-500/10 border-l-2 border-r-2 border-blue-500/50' return 'hover:bg-darkSurface/50' } // OI bar width (relative to max) const maxOI = useMemo(() => Math.max(...chain.map(r => r.totalOI), 1), [chain]) return ( <div> {/* Controls */} <div className="flex flex-wrap gap-3 mb-4 items-center"> {/* Asset selector */} <div className="flex gap-1 bg-darkSurface p-1 rounded-lg border border-darkBorder"> {assets.map(a => ( <button key={a} onClick={() => setChainAsset(a)} className={`px-3 py-1 rounded text-xs font-bold transition-colors ${ chainAsset === a ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white' }`}>{a}</button> ))} </div> {/* Expiry selector */} {data?.expiries && ( <div className="flex gap-1 bg-darkSurface p-1 rounded-lg border border-darkBorder overflow-x-auto max-w-[500px]"> {data.expiries.map(exp => { // Calculate DTE for label const y = 2000 + parseInt(exp.substring(0, 2)) const m = parseInt(exp.substring(2, 4)) - 1 const d = parseInt(exp.substring(4, 6)) const dte = Math.max(0, Math.ceil((new Date(Date.UTC(y, m, d, 8)) - Date.now()) / 86400000)) return ( <button key={exp} onClick={() => setChainExpiry(exp)} className={`px-2 py-1 rounded text-[10px] font-mono whitespace-nowrap transition-colors ${ chainExpiry === exp ? 'bg-purple-600 text-white' : 'text-gray-400 hover:text-white' }`}> {exp.substring(2,4)}/{exp.substring(4,6)} <span className="text-gray-500">({dte}d)</span> </button> ) })} </div> )} {/* Info badges */} {data && ( <div className="flex gap-2 text-[10px] text-gray-500"> <span>Spot β‰ˆ <span className="text-white font-bold">${spot?.toLocaleString()}</span></span> <span>DTE <span className="text-white font-bold">{data.dte}d</span></span> <span>Strikes <span className="text-white font-bold">{data.strikesCount}</span></span> </div> )} </div> {isLoading && ( <div className="flex justify-center py-12"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> </div> )} {!isLoading && chain.length > 0 && ( <div className="overflow-x-auto rounded-lg border border-darkBorder"> <table className="w-full text-[11px] font-mono border-collapse"> <thead> <tr className="bg-darkSurface text-gray-400 border-b border-darkBorder"> {/* CALLS side */} <th className="px-2 py-2 text-right text-green-400/70">Vol</th> <th className="px-2 py-2 text-right text-green-400/70">OI</th> <th className="px-2 py-2 text-right text-green-400/70">IV</th> <th className="px-2 py-2 text-right text-green-400/70">Ξ”</th> <th className="px-2 py-2 text-right text-green-400/70">Bid</th> <th className="px-2 py-2 text-right text-green-400/70">Ask</th> {/* Strike */} <th className="px-3 py-2 text-center bg-darkBg font-bold text-white border-x border-darkBorder">Strike</th> {/* PUTS side */} <th className="px-2 py-2 text-left text-red-400/70">Bid</th> <th className="px-2 py-2 text-left text-red-400/70">Ask</th> <th className="px-2 py-2 text-left text-red-400/70">Ξ”</th> <th className="px-2 py-2 text-left text-red-400/70">IV</th> <th className="px-2 py-2 text-left text-red-400/70">OI</th> <th className="px-2 py-2 text-left text-red-400/70">Vol</th> </tr> <tr className="bg-darkSurface/50 text-[9px] text-gray-500 border-b border-darkBorder"> <td colSpan="6" className="text-center py-0.5 text-green-500/50">── CALLS ──</td> <td className="text-center bg-darkBg border-x border-darkBorder"></td> <td colSpan="6" className="text-center py-0.5 text-red-500/50">── PUTS ──</td> </tr> </thead> <tbody> {chain.map((row, i) => { const c = row.call const p = row.put const isAtm = row.moneyness === 'ATM' const isCallItm = row.moneyness === 'ITM_CALL' const isPutItm = row.moneyness === 'ITM_PUT' return ( <tr key={row.strike} id={`strike-${i}`} className={`border-b border-darkBorder/30 transition-colors ${rowBg(row)} ${isAtm ? 'font-bold' : ''}`}> {/* CALL side */} <td className={`px-2 py-1.5 text-right ${isCallItm ? 'bg-green-500/5' : ''}`}> <span className={c?.volume > 0 ? 'text-green-400' : 'text-gray-600'}>{fmtK(c?.volume)}</span> </td> <td className={`px-2 py-1.5 text-right ${isCallItm ? 'bg-green-500/5' : ''}`}> <span className="text-gray-400">{fmtK(c?.openInterest)}</span> </td> <td className={`px-2 py-1.5 text-right ${isCallItm ? 'bg-green-500/5' : ''}`}> <span className={ivColor(c?.iv)}>{c?.iv ? (c.iv * 100).toFixed(0) + '%' : 'β€”'}</span> </td> <td className={`px-2 py-1.5 text-right ${isCallItm ? 'bg-green-500/5' : ''}`}> <span className="text-gray-300">{c?.delta != null ? fmtNum(c.delta, 3) : 'β€”'}</span> </td> <td className={`px-2 py-1.5 text-right ${isCallItm ? 'bg-green-500/5' : ''}`}> <span className="text-green-400">{c ? '$' + fmtNum(c.markPrice, c.markPrice < 1 ? 4 : 2) : 'β€”'}</span> </td> <td className={`px-2 py-1.5 text-right ${isCallItm ? 'bg-green-500/5' : ''}`}> <span className="text-green-300">{c ? '$' + fmtNum(c.lastPrice, c.lastPrice < 1 ? 4 : 2) : 'β€”'}</span> </td> {/* STRIKE center */} <td className={`px-3 py-1.5 text-center bg-darkBg border-x border-darkBorder ${isAtm ? 'text-blue-400 text-sm' : 'text-white'}`}> {row.strike.toLocaleString()} {isAtm && <span className="ml-1 text-[8px] text-blue-500">ATM</span>} </td> {/* PUT side */} <td className={`px-2 py-1.5 text-left ${isPutItm ? 'bg-red-500/5' : ''}`}> <span className="text-red-400">{p ? '$' + fmtNum(p.markPrice, p.markPrice < 1 ? 4 : 2) : 'β€”'}</span> </td> <td className={`px-2 py-1.5 text-left ${isPutItm ? 'bg-red-500/5' : ''}`}> <span className="text-red-300">{p ? '$' + fmtNum(p.lastPrice, p.lastPrice < 1 ? 4 : 2) : 'β€”'}</span> </td> <td className={`px-2 py-1.5 text-left ${isPutItm ? 'bg-red-500/5' : ''}`}> <span className="text-gray-300">{p?.delta != null ? fmtNum(p.delta, 3) : 'β€”'}</span> </td> <td className={`px-2 py-1.5 text-left ${isPutItm ? 'bg-red-500/5' : ''}`}> <span className={ivColor(p?.iv)}>{p?.iv ? (p.iv * 100).toFixed(0) + '%' : 'β€”'}</span> </td> <td className={`px-2 py-1.5 text-left ${isPutItm ? 'bg-red-500/5' : ''}`}> <span className="text-gray-400">{fmtK(p?.openInterest)}</span> </td> <td className={`px-2 py-1.5 text-left ${isPutItm ? 'bg-red-500/5' : ''}`}> <span className={p?.volume > 0 ? 'text-red-400' : 'text-gray-600'}>{fmtK(p?.volume)}</span> </td> </tr> ) })} </tbody> </table> </div> )} {!isLoading && chain.length === 0 && ( <div className="text-center text-gray-500 py-12 bg-darkSurface rounded-xl border border-darkBorder"> No options data for {chainAsset}. Try another asset. </div> )} </div> ) } // ═══════════════════════════════════════════════════════════════ // IV SURFACE / TERM STRUCTURE // ═══════════════════════════════════════════════════════════════ function IvSurface() { const [asset, setAsset] = useState('BTC') const [optType, setOptType] = useState('CALL') const [selectedExpiry, setSelectedExpiry] = useState(null) const assets = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'DOGE'] const { data, isLoading } = useQuery({ queryKey: ['iv-surface', asset, optType], queryFn: () => fetch(`/api/iv-surface?underlying=${asset}&type=${optType}`) .then(r => r.json()), refetchInterval: 30000, }) const smiles = data?.smiles || [] const termStructure = data?.termStructure || [] const spot = data?.spot || 0 const shape = data?.shape || 'FLAT' // Pick which expiry smiles to show (up to 4) const visibleSmiles = selectedExpiry ? smiles.filter(s => s.expiry === selectedExpiry) : smiles.slice(0, 4) // Colors for expiry lines const lineColors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'] // Find IV range across visible smiles for chart scaling β€” add 10% padding const allIvs = visibleSmiles.flatMap(s => s.points.map(p => p.iv)) const rawIvMin = allIvs.length ? Math.min(...allIvs) : 0 const rawIvMax = allIvs.length ? Math.max(...allIvs) : 1 const ivPad = (rawIvMax - rawIvMin) * 0.1 || 0.01 const ivMin = rawIvMin - ivPad const ivMax = rawIvMax + ivPad const ivRange = ivMax - ivMin || 0.01 // Term structure chart scaling const tsIvs = termStructure.map(t => t.atmIv) const tsMin = tsIvs.length ? Math.min(...tsIvs) : 0 const tsMax = tsIvs.length ? Math.max(...tsIvs) : 1 const tsRange = tsMax - tsMin || 0.01 const shapeLabel = { CONTANGO: { text: 'Contango (normal)', color: 'text-green-400', desc: 'Π”Π°Π»ΡŒΠ½ΠΈΠ΅ Π΄ΠΎΡ€ΠΎΠΆΠ΅ β€” Ρ€Ρ‹Π½ΠΎΠΊ спокоСн' }, BACKWARDATION: { text: 'Backwardation (inverted)', color: 'text-red-400', desc: 'Π‘Π»ΠΈΠΆΠ½ΠΈΠ΅ Π΄ΠΎΡ€ΠΎΠΆΠ΅ β€” Ρ€Ρ‹Π½ΠΎΠΊ Π½Π΅Ρ€Π²Π½ΠΈΡ‡Π°Π΅Ρ‚' }, FLAT: { text: 'Flat', color: 'text-gray-400', desc: 'IV одинаковая ΠΏΠΎ срокам' }, }[shape] || { text: shape, color: 'text-gray-400', desc: '' } return ( <div> {/* Controls */} <div className="flex flex-wrap gap-3 mb-4"> <div className="flex gap-1 bg-darkSurface p-1 rounded-lg border border-darkBorder"> {assets.map(a => ( <button key={a} onClick={() => { setAsset(a); setSelectedExpiry(null) }} className={`px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${ asset === a ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white' }`}>{a}</button> ))} </div> <div className="flex gap-1 bg-darkSurface p-1 rounded-lg border border-darkBorder"> <button onClick={() => setOptType('CALL')} className={`px-3 py-1.5 rounded-md text-xs font-medium ${ optType === 'CALL' ? 'bg-green-600 text-white' : 'text-gray-400 hover:text-white' }`}>Calls</button> <button onClick={() => setOptType('PUT')} className={`px-3 py-1.5 rounded-md text-xs font-medium ${ optType === 'PUT' ? 'bg-red-600 text-white' : 'text-gray-400 hover:text-white' }`}>Puts</button> </div> {smiles.length > 0 && ( <select value={selectedExpiry || ''} onChange={e => setSelectedExpiry(e.target.value || null)} className="bg-darkSurface text-gray-300 text-xs border border-darkBorder rounded-lg px-3 py-1.5"> <option value="">All expiries (top 4)</option> {smiles.map(s => ( <option key={s.expiry} value={s.expiry}> {s.expiry.substring(2,4)}/{s.expiry.substring(4,6)} ({s.dte}d) β€” {s.points.length} strikes </option> ))} </select> )} </div> {isLoading && ( <div className="flex justify-center items-center h-48"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> </div> )} {!isLoading && ( <div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> {/* IV SMILE CHART */} <div className="bg-darkSurface rounded-xl p-4 border border-darkBorder"> <h3 className="text-white font-bold mb-1">IV Smile / Skew</h3> <p className="text-gray-500 text-xs mb-3"> IV ΠΏΠΎ страйкам β€” ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚ ΠΊΠ°ΠΊ Ρ€Ρ‹Π½ΠΎΠΊ ΠΎΡ†Π΅Π½ΠΈΠ²Π°Π΅Ρ‚ риски Π½Π° Ρ€Π°Π·Π½Ρ‹Ρ… уровнях Ρ†Π΅Π½Ρ‹ </p> {visibleSmiles.length === 0 ? ( <div className="text-gray-500 text-center py-8">No IV data</div> ) : (() => { // Compute unified strike range across all visible smiles const allStrikes = visibleSmiles.flatMap(s => s.points.map(p => p.strike)) const strikeMin = Math.min(...allStrikes) const strikeMax = Math.max(...allStrikes) const sRange = strikeMax - strikeMin || 1 // X-axis labels: 5 evenly spaced strikes const fmtStrike = (v) => v >= 1000 ? `$${(v / 1000).toFixed(0)}K` : `$${v.toFixed(0)}` const xLabels = [0, 0.25, 0.5, 0.75, 1].map(pct => fmtStrike(strikeMin + pct * sRange)) return ( <div className="relative" style={{ height: '300px' }}> {/* Y axis labels */} <div className="absolute left-0 top-0 bottom-6 w-10 flex flex-col justify-between text-[9px] text-gray-500 pr-1 text-right"> <span>{(ivMax * 100).toFixed(0)}%</span> <span>{((ivMin + ivRange * 0.75) * 100).toFixed(0)}%</span> <span>{((ivMin + ivRange * 0.5) * 100).toFixed(0)}%</span> <span>{((ivMin + ivRange * 0.25) * 100).toFixed(0)}%</span> <span>{(ivMin * 100).toFixed(0)}%</span> </div> {/* Chart area β€” div clips overflow on mobile */} <div className="absolute left-10 top-0 right-0 bottom-6 overflow-hidden rounded"> <svg className="w-full h-full" viewBox="0 0 600 270" preserveAspectRatio="none"> {/* Grid lines */} {[0, 0.25, 0.5, 0.75, 1].map(pct => ( <line key={pct} x1="0" y1={270 - pct * 260} x2="600" y2={270 - pct * 260} stroke="#374151" strokeWidth="0.5" strokeDasharray="4,4" /> ))} {/* Spot price vertical line */} {spot > 0 && (() => { const x = ((spot - strikeMin) / sRange) * 600 if (x > 0 && x < 600) { return <line x1={x} y1="0" x2={x} y2="270" stroke="#6b7280" strokeWidth="1" strokeDasharray="6,3" /> } return null })()} {/* IV curves per expiry β€” unified X scale */} {visibleSmiles.map((smile, idx) => { const pts = smile.points if (pts.length < 2) return null const pathD = pts.map((p, i) => { const x = ((p.strike - strikeMin) / sRange) * 600 const y = 270 - ((p.iv - ivMin) / ivRange) * 260 return `${i === 0 ? 'M' : 'L'} ${x} ${y}` }).join(' ') return <path key={smile.expiry} d={pathD} fill="none" stroke={lineColors[idx % lineColors.length]} strokeWidth="2.5" /> })} </svg> </div> {/* X axis labels (strikes) */} <div className="absolute left-10 right-0 bottom-0 flex justify-between text-[9px] text-gray-500 px-1"> {xLabels.map((label, i) => <span key={i}>{label}</span>)} </div> {/* Legend */} <div className="absolute right-1 top-1 bg-darkBg/80 rounded px-1.5 py-1 flex flex-col gap-0.5"> {visibleSmiles.map((s, i) => ( <div key={s.expiry} className="flex items-center gap-1 text-[9px]"> <div className="w-2.5 h-0.5 rounded" style={{ backgroundColor: lineColors[i % lineColors.length] }} /> <span className="text-gray-400">{s.expiry.substring(2,4)}/{s.expiry.substring(4,6)} ({s.dte}d)</span> </div> ))} </div> </div> ) })()} {spot > 0 && <div className="text-[9px] text-gray-500 mt-1">Spot: ${spot.toLocaleString()} (dashed line) | X = Strike price</div>} </div> {/* TERM STRUCTURE CHART */} <div className="bg-darkSurface rounded-xl p-4 border border-darkBorder"> <h3 className="text-white font-bold mb-1"> Term Structure <span className={`ml-2 text-xs font-normal ${shapeLabel.color}`}>{shapeLabel.text}</span> </h3> <p className="text-gray-500 text-xs mb-3"> ATM IV ΠΏΠΎ срокам экспирации β€” {shapeLabel.desc} </p> {termStructure.length === 0 ? ( <div className="text-gray-500 text-center py-8">No term structure data</div> ) : ( <div className="overflow-x-auto"> {/* Bar chart β€” scrollable on mobile */} <div className="flex items-end gap-2 pb-1" style={{ height: '240px', minWidth: `${Math.max(termStructure.length * 52, 300)}px` }}> {termStructure.map((t, i) => { const pct = tsRange > 0 ? (t.atmIv - tsMin) / tsRange : 0.5 const height = 30 + pct * 170 const isMin = t.atmIv === tsMin const isMax = t.atmIv === tsMax return ( <div key={t.expiry} className="flex flex-col items-center gap-1" style={{ width: '44px', flexShrink: 0 }}> <span className={`text-[9px] ${isMax ? 'text-red-400 font-bold' : isMin ? 'text-green-400 font-bold' : 'text-gray-400'}`}> {(t.atmIv * 100).toFixed(1)}% </span> <div className={`w-8 rounded-t ${ i === 0 ? 'bg-blue-500' : shape === 'BACKWARDATION' ? 'bg-red-500/60' : 'bg-green-500/60' }`} style={{ height: `${height}px` }} title={`ATM IV ${(t.atmIv * 100).toFixed(2)}% | Strike $${t.atmStrike?.toLocaleString()}`} /> <div className="text-center leading-tight"> <div className="text-[9px] text-gray-300"> {t.expiry.substring(2,4)}/{t.expiry.substring(4,6)} </div> <div className="text-[8px] text-gray-500">{t.dte}d</div> </div> </div> ) })} </div> </div> )} </div> </div> )} {/* Educational tooltip */} <div className="mt-4 bg-darkSurface rounded-xl p-3 border border-darkBorder text-xs text-gray-400"> <strong className="text-gray-300">Как Ρ‡ΠΈΡ‚Π°Ρ‚ΡŒ:</strong> <span className="ml-1"> <strong>IV Smile:</strong> U-образная кривая = Ρ€Ρ‹Π½ΠΎΠΊ боится Ρ€Π΅Π·ΠΊΠΈΡ… Π΄Π²ΠΈΠΆΠ΅Π½ΠΈΠΉ Π² ΠΎΠ±Π΅ стороны. Бкос Π²ΠΏΡ€Π°Π²ΠΎ = страх роста, Π²Π»Π΅Π²ΠΎ = страх падСния. <strong className="ml-2">Term Structure:</strong> Contango (Π·Π΅Π»Ρ‘Π½Ρ‹ΠΉ) = Π½ΠΎΡ€ΠΌΠ°, Ρ€Ρ‹Π½ΠΎΠΊ спокоСн. Backwardation (красный) = Ρ€Ρ‹Π½ΠΎΠΊ ΠΆΠ΄Ρ‘Ρ‚ Π΄Π²ΠΈΠΆΠ΅Π½ΠΈΠ΅ БЕЙЧАБ β†’ Π±Π»ΠΈΠΆΠ½ΠΈΠ΅ ΠΎΠΏΡ†ΠΈΠΎΠ½Ρ‹ Π΄ΠΎΡ€ΠΎΠΆΠ΅. </span> </div> </div> ) } // ═══════════════════════════════════════════════════════════════ // TRADING TAB β€” Positions, Orders, Account // ═══════════════════════════════════════════════════════════════ function TradingTab() { const baseUrl = import.meta.env.DEV ? 'http://localhost:8080' : '' const [statsPeriod, setStatsPeriod] = useState(null) // null = ALL const { data: posData, isLoading: posLoading, refetch: refetchPos } = useQuery({ queryKey: ['tradingPositions'], queryFn: async () => { const res = await fetch(`${baseUrl}/api/trading/positions`) if (!res.ok) throw new Error('Failed') return res.json() }, refetchInterval: 15000, }) const { data: ordersData, refetch: refetchOrders } = useQuery({ queryKey: ['tradingOrders'], queryFn: async () => { const res = await fetch(`${baseUrl}/api/trading/orders`) if (!res.ok) throw new Error('Failed') return res.json() }, refetchInterval: 15000, }) const { data: statusData } = useQuery({ queryKey: ['tradingStatus'], queryFn: async () => { const res = await fetch(`${baseUrl}/api/trading/status`) if (!res.ok) throw new Error('Failed') return res.json() }, staleTime: 30000, }) const { data: statsData, refetch: refetchStats } = useQuery({ queryKey: ['tradingStats', statsPeriod], queryFn: async () => { const params = statsPeriod ? `&days=${statsPeriod}` : '' const res = await fetch(`${baseUrl}/api/trading/stats?_=1${params}`) if (!res.ok) throw new Error('Failed') return res.json() }, refetchInterval: 60000, }) const { data: journalData, refetch: refetchJournal } = useQuery({ queryKey: ['tradingJournal', statsPeriod], queryFn: async () => { const params = statsPeriod ? `&days=${statsPeriod}` : '' const res = await fetch(`${baseUrl}/api/trading/journal?_=1${params}&limit=50`) if (!res.ok) throw new Error('Failed') return res.json() }, refetchInterval: 60000, }) const [syncing, setSyncing] = useState(false) const handleSync = async () => { setSyncing(true) try { await fetch(`${baseUrl}/api/trading/sync`, { method: 'POST' }) refetchStats() refetchJournal() refetchPos() } catch (e) { /* ignore */ } setSyncing(false) } const positions = posData?.data || [] const orders = ordersData?.data || [] const account = statusData?.data?.account || {} const usdtBal = account.usdtBalance const stats = statsData?.data || null const cancelOrder = async (symbol, orderId) => { await fetch(`${baseUrl}/api/trading/order?symbol=${symbol}&orderId=${orderId}`, { method: 'DELETE' }) refetchOrders() } const fmtPrice = (v) => { const n = parseFloat(v) if (isNaN(n)) return '-' return n >= 100 ? n.toLocaleString(undefined, { maximumFractionDigits: 2 }) : n.toFixed(4) } const fmtHold = (mins) => { if (!mins) return '-' if (mins < 60) return `${mins}m` if (mins < 1440) return `${Math.round(mins / 60)}h` return `${(mins / 1440).toFixed(1)}d` } const periodBtns = [ { label: '7D', value: 7 }, { label: '30D', value: 30 }, { label: 'ALL', value: null }, ] return ( <div> {/* ── Stats Bar ── */} {stats && stats.totalTrades > 0 && ( <div className="mb-4"> <div className="flex items-center justify-between mb-2"> <h3 className="text-white font-bold text-sm">Performance</h3> <div className="flex gap-1"> {periodBtns.map(b => ( <button key={b.label} onClick={() => setStatsPeriod(b.value)} className={`px-2 py-0.5 rounded text-[10px] font-mono transition-colors ${statsPeriod === b.value ? 'bg-orange-600 text-white' : 'text-gray-500 hover:text-white bg-darkSurface border border-darkBorder'}`}> {b.label} </button> ))} </div> </div> {/* Row 1: Core metrics */} <div className="grid grid-cols-3 sm:grid-cols-6 gap-2 mb-2"> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">TOTAL P&L</div> <div className={`font-bold text-xs font-mono ${stats.totalPnl >= 0 ? 'text-green-400' : 'text-red-400'}`}> {stats.totalPnl >= 0 ? '+' : ''}${stats.totalPnl.toFixed(2)} </div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">WIN RATE</div> <div className={`font-bold text-xs font-mono ${stats.winRate >= 50 ? 'text-green-400' : 'text-red-400'}`}> {stats.winRate}% </div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">TRADES</div> <div className="text-white font-bold text-xs font-mono">{stats.wins}W / {stats.losses}L</div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">AVG WIN</div> <div className="text-green-400 font-bold text-xs font-mono">+${stats.avgWin.toFixed(2)}</div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">AVG LOSS</div> <div className="text-red-400 font-bold text-xs font-mono">${stats.avgLoss.toFixed(2)}</div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">PROFIT FACTOR</div> <div className={`font-bold text-xs font-mono ${stats.profitFactor >= 1 ? 'text-green-400' : 'text-red-400'}`}> {stats.profitFactor >= 999 ? '∞' : stats.profitFactor.toFixed(2)} </div> </div> </div> {/* Row 2: Extended metrics */} <div className="grid grid-cols-2 sm:grid-cols-5 gap-2"> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">BEST TRADE</div> <div className="text-green-400 font-bold text-[10px] font-mono truncate"> {stats.bestTrade ? `${stats.bestTrade.symbol.split('-')[0]} +$${stats.bestTrade.pnl.toFixed(2)}` : '-'} </div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">WORST TRADE</div> <div className="text-red-400 font-bold text-[10px] font-mono truncate"> {stats.worstTrade ? `${stats.worstTrade.symbol.split('-')[0]} $${stats.worstTrade.pnl.toFixed(2)}` : '-'} </div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">STREAK</div> <div className={`font-bold text-xs font-mono ${stats.currentStreakType === 'WIN' ? 'text-green-400' : 'text-red-400'}`}> {stats.currentStreak}{stats.currentStreakType === 'WIN' ? 'W' : 'L'} </div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">MAX STREAKS</div> <div className="text-white font-bold text-[10px] font-mono"> <span className="text-green-400">{stats.winStreak}W</span> / <span className="text-red-400">{stats.lossStreak}L</span> </div> </div> <div className="bg-darkSurface rounded-lg px-2 py-1.5 border border-darkBorder text-center"> <div className="text-gray-500 text-[9px]">AVG HOLD</div> <div className="text-white font-bold text-xs font-mono">{fmtHold(stats.avgHoldMinutes)}</div> </div> </div> </div> )} {/* Account Summary */} {usdtBal && ( <div className="flex gap-3 mb-4 text-xs font-mono"> <div className="bg-darkSurface rounded-lg px-3 py-2 border border-darkBorder flex-1 text-center"> <div className="text-gray-500 text-[9px]">BALANCE</div> <div className="text-white font-bold">${parseFloat(usdtBal.marginBalance || usdtBal.balance || 0).toFixed(2)}</div> </div> <div className="bg-darkSurface rounded-lg px-3 py-2 border border-darkBorder flex-1 text-center"> <div className="text-gray-500 text-[9px]">AVAILABLE</div> <div className="text-green-400 font-bold">${parseFloat(usdtBal.available || usdtBal.availableBalance || 0).toFixed(2)}</div> </div> <div className="bg-darkSurface rounded-lg px-3 py-2 border border-darkBorder flex-1 text-center"> <div className="text-gray-500 text-[9px]">POSITIONS</div> <div className="text-white font-bold">{positions.length}</div> </div> <div className="bg-darkSurface rounded-lg px-3 py-2 border border-darkBorder flex-1 text-center"> <div className="text-gray-500 text-[9px]">OPEN ORDERS</div> <div className="text-white font-bold">{orders.length}</div> </div> </div> )} {/* Open Positions */} <div className="mb-6"> <div className="flex items-center justify-between mb-2"> <h3 className="text-white font-bold text-sm">Open Positions</h3> <button onClick={() => refetchPos()} className="text-[10px] text-gray-500 hover:text-white">↻ Refresh</button> </div> {posLoading && <div className="flex justify-center py-8"><div className="animate-spin rounded-full h-6 w-6 border-b-2 border-orange-500"></div></div>} {!posLoading && positions.length === 0 && ( <div className="text-center py-8 text-gray-600 text-sm">No open positions</div> )} {positions.length > 0 && ( <div className="overflow-x-auto rounded-lg border border-darkBorder"> <table className="w-full text-[11px] font-mono border-collapse"> <thead> <tr className="bg-darkSurface text-gray-400 border-b border-darkBorder"> <th className="px-2 py-2 text-left">Symbol</th> <th className="px-2 py-2 text-right">Qty</th> <th className="px-2 py-2 text-right">Entry</th> <th className="px-2 py-2 text-right">Mark</th> <th className="px-2 py-2 text-right">PnL</th> <th className="px-2 py-2 text-right">ROI%</th> </tr> </thead> <tbody> {positions.map((p, i) => { const qty = parseFloat(p.quantity || 0) const entry = parseFloat(p.entryPrice || 0) const mark = parseFloat(p.markPrice || p.markValue || 0) const pnl = parseFloat(p.unrealizedPNL || p.unrealizedPnL || 0) const roi = entry > 0 ? ((mark - entry) / entry * 100) : 0 return ( <tr key={i} className="border-b border-darkBorder/30 hover:bg-darkSurface/50"> <td className="px-2 py-1.5 text-white font-bold">{p.symbol}</td> <td className={`px-2 py-1.5 text-right ${qty > 0 ? 'text-green-400' : 'text-red-400'}`}>{qty}</td> <td className="px-2 py-1.5 text-right text-gray-300">{fmtPrice(entry)}</td> <td className="px-2 py-1.5 text-right text-white">{fmtPrice(mark)}</td> <td className={`px-2 py-1.5 text-right font-bold ${pnl >= 0 ? 'text-green-400' : 'text-red-400'}`}> {pnl >= 0 ? '+' : ''}{pnl.toFixed(2)} </td> <td className={`px-2 py-1.5 text-right ${roi >= 0 ? 'text-green-400' : 'text-red-400'}`}> {roi >= 0 ? '+' : ''}{roi.toFixed(1)}% </td> </tr> ) })} </tbody> </table> </div> )} </div> {/* Open Orders */} <div> <div className="flex items-center justify-between mb-2"> <h3 className="text-white font-bold text-sm">Open Orders</h3> <button onClick={() => refetchOrders()} className="text-[10px] text-gray-500 hover:text-white">↻ Refresh</button> </div> {orders.length === 0 && ( <div className="text-center py-8 text-gray-600 text-sm">No open orders</div> )} {orders.length > 0 && ( <div className="overflow-x-auto rounded-lg border border-darkBorder"> <table className="w-full text-[11px] font-mono border-collapse"> <thead> <tr className="bg-darkSurface text-gray-400 border-b border-darkBorder"> <th className="px-2 py-2 text-left">Symbol</th> <th className="px-2 py-2 text-center">Side</th> <th className="px-2 py-2 text-right">Qty</th> <th className="px-2 py-2 text-right">Price</th> <th className="px-2 py-2 text-right">Filled</th> <th className="px-2 py-2 text-center">Action</th> </tr> </thead> <tbody> {orders.map((o, i) => ( <tr key={i} className="border-b border-darkBorder/30 hover:bg-darkSurface/50"> <td className="px-2 py-1.5 text-white font-bold">{o.symbol}</td> <td className={`px-2 py-1.5 text-center font-bold ${o.side === 'BUY' ? 'text-green-400' : 'text-red-400'}`}>{o.side}</td> <td className="px-2 py-1.5 text-right text-gray-300">{o.quantity}</td> <td className="px-2 py-1.5 text-right text-white">{fmtPrice(o.price)}</td> <td className="px-2 py-1.5 text-right text-gray-400">{o.executedQty || 0}/{o.quantity}</td> <td className="px-2 py-1.5 text-center"> <button onClick={() => cancelOrder(o.symbol, o.orderId)} className="px-2 py-0.5 bg-red-600/20 border border-red-500/30 rounded text-red-400 text-[10px] hover:bg-red-600/40 transition-colors"> Cancel </button> </td> </tr> ))} </tbody> </table> </div> )} </div> {/* ── Trade Journal ── */} <div className="mt-6"> <div className="flex items-center justify-between mb-2"> <h3 className="text-white font-bold text-sm">Trade Journal</h3> <button onClick={handleSync} disabled={syncing} className="text-[10px] text-gray-500 hover:text-white disabled:opacity-50 flex items-center gap-1"> {syncing ? <span className="animate-spin">⟳</span> : '⟳'} Sync Binance </button> </div> {(!journalData?.data?.trades || journalData.data.trades.length === 0) && ( <div className="text-center py-6 text-gray-600 text-sm border border-dashed border-darkBorder rounded-lg"> No closed trades yet. Close a position and hit Sync. </div> )} {journalData?.data?.trades?.length > 0 && ( <div className="overflow-x-auto rounded-lg border border-darkBorder"> <table className="w-full text-[11px] font-mono border-collapse"> <thead> <tr className="bg-darkSurface text-gray-400 border-b border-darkBorder"> <th className="px-2 py-2 text-left">Date</th> <th className="px-2 py-2 text-left">Asset</th> <th className="px-2 py-2 text-center">Type</th> <th className="px-2 py-2 text-right">Strike</th> <th className="px-2 py-2 text-right">Entry</th> <th className="px-2 py-2 text-right">Exit</th> <th className="px-2 py-2 text-right">P&L</th> <th className="px-2 py-2 text-right">ROI%</th> <th className="px-2 py-2 text-center">Exit</th> <th className="px-2 py-2 text-right">Hold</th> </tr> </thead> <tbody> {journalData.data.trades.map((t, i) => ( <tr key={t.id || i} className="border-b border-darkBorder/30 hover:bg-darkSurface/50"> <td className="px-2 py-1.5 text-gray-400"> {new Date(t.exitTime).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' })} </td> <td className="px-2 py-1.5 text-white font-bold">{t.underlying}</td> <td className={`px-2 py-1.5 text-center ${t.optionType === 'CALL' ? 'text-green-400' : 'text-red-400'}`}> {t.optionType === 'CALL' ? 'C' : 'P'} </td> <td className="px-2 py-1.5 text-right text-gray-300"> {t.strike >= 1000 ? t.strike.toLocaleString() : t.strike} </td> <td className="px-2 py-1.5 text-right text-gray-300">${t.entryPrice}</td> <td className="px-2 py-1.5 text-right text-white">${t.exitPrice}</td> <td className={`px-2 py-1.5 text-right font-bold ${t.pnl >= 0 ? 'text-green-400' : 'text-red-400'}`}> {t.pnl >= 0 ? '+' : ''}${t.pnl.toFixed(2)} </td> <td className={`px-2 py-1.5 text-right ${t.roi >= 0 ? 'text-green-400' : 'text-red-400'}`}> {t.roi >= 0 ? '+' : ''}{t.roi.toFixed(0)}% </td> <td className="px-2 py-1.5 text-center"> {t.exitReason === 'TP_HIT' ? <span className="text-green-400 text-[10px]">TP</span> : t.exitReason === 'SL_HIT' ? <span className="text-red-400 text-[10px]">SL</span> : t.exitReason === 'EXPIRED_WORTHLESS' ? <span className="text-orange-400 text-[10px]">EXP-0</span> : t.exitReason === 'EXERCISED' ? <span className="text-cyan-400 text-[10px]">EXERC</span> : t.exitReason === 'MANUAL_CLOSE' ? <span className="text-gray-400 text-[10px]">MANUAL</span> : t.status === 'EXPIRED' ? <span className="text-orange-400 text-[10px]">EXP</span> : <span className="text-gray-600 text-[10px]">-</span>} </td> <td className="px-2 py-1.5 text-right text-gray-400">{fmtHold(t.holdMinutes)}</td> </tr> ))} </tbody> </table> {journalData.data.total > journalData.data.trades.length && ( <div className="text-center py-2 text-gray-500 text-[10px]"> Showing {journalData.data.trades.length} of {journalData.data.total} trades </div> )} </div> )} </div> </div> ) } // ═══════════════════════════════════════════════════════════════ // CUSTOM ALERTS TAB // ═══════════════════════════════════════════════════════════════ function CustomAlerts() { const baseUrl = import.meta.env.DEV ? 'http://localhost:8080' : '' const [showForm, setShowForm] = useState(false) const [editingAlert, setEditingAlert] = useState(null) const [form, setForm] = useState({ name: '', underlying: 'BTC', metric: 'iv_rank', condition: 'lt', threshold: '', cooldownMinutes: 30, telegramEnabled: true, pushEnabled: true }) const [expandedId, setExpandedId] = useState(null) // Fetch alerts const { data: alertsData, isLoading, refetch } = useQuery({ queryKey: ['customAlerts'], queryFn: async () => { const res = await fetch(`${baseUrl}/api/custom-alerts`) if (!res.ok) throw new Error('Failed') return res.json() }, refetchInterval: 30000, }) // Fetch available metrics const { data: metricsData } = useQuery({ queryKey: ['alertMetrics'], queryFn: async () => { const res = await fetch(`${baseUrl}/api/custom-alerts/metrics`) if (!res.ok) throw new Error('Failed') return res.json() }, staleTime: Infinity, }) // Fetch trigger history for expanded alert const { data: triggerData } = useQuery({ queryKey: ['alertTriggers', expandedId], queryFn: async () => { if (!expandedId) return null const res = await fetch(`${baseUrl}/api/custom-alerts/${expandedId}/triggers?limit=10`) if (!res.ok) throw new Error('Failed') return res.json() }, enabled: !!expandedId, refetchInterval: 30000, }) const alerts = alertsData?.data || [] const metrics = metricsData?.data?.metrics || [] const conditions = metricsData?.data?.conditions || [] const underlyings = metricsData?.data?.underlyings || ['BTC', 'ETH', 'SOL', 'DOGE', 'XRP', 'BNB', 'ALL'] const condLabels = { gt: '>', lt: '<', gte: '>=', lte: '<=', eq: '=', crosses_above: 'Crosses Above', crosses_below: 'Crosses Below' } const metricLabel = (id) => metrics.find(m => m.id === id)?.label || id const metricUnit = (id) => metrics.find(m => m.id === id)?.unit || '' // API helpers const apiCall = async (url, method = 'GET', body = null) => { const opts = { method, headers: { 'Content-Type': 'application/json' } } if (body) opts.body = JSON.stringify(body) const res = await fetch(`${baseUrl}${url}`, opts) return res.json() } const handleCreate = async () => { if (!form.name || form.threshold === '') return const body = { ...form, threshold: parseFloat(form.threshold) } if (editingAlert) { await apiCall(`/api/custom-alerts/${editingAlert.id}`, 'PUT', body) } else { await apiCall('/api/custom-alerts', 'POST', body) } setShowForm(false) setEditingAlert(null) setForm({ name: '', underlying: 'BTC', metric: 'iv_rank', condition: 'lt', threshold: '', cooldownMinutes: 30, telegramEnabled: true, pushEnabled: true }) refetch() } const handleToggle = async (id) => { await apiCall(`/api/custom-alerts/${id}/toggle`, 'POST') refetch() } const handleDelete = async (id) => { if (!confirm('Delete this alert?')) return await apiCall(`/api/custom-alerts/${id}`, 'DELETE') refetch() } const handleEdit = (alert) => { setForm({ name: alert.name, underlying: alert.underlying, metric: alert.metric, condition: alert.condition, threshold: alert.threshold, cooldownMinutes: alert.cooldownMinutes, telegramEnabled: alert.telegramEnabled, pushEnabled: alert.pushEnabled, }) setEditingAlert(alert) setShowForm(true) } const timeSince = (dateStr) => { if (!dateStr) return 'never' const diff = Date.now() - new Date(dateStr).getTime() const mins = Math.floor(diff / 60000) if (mins < 1) return 'just now' if (mins < 60) return `${mins}m ago` const hrs = Math.floor(mins / 60) if (hrs < 24) return `${hrs}h ago` return `${Math.floor(hrs / 24)}d ago` } return ( <div> {/* Header */} <div className="flex items-center justify-between mb-4"> <div className="text-sm text-gray-400"> {alerts.length} alert{alerts.length !== 1 ? 's' : ''} configured <span className="text-gray-600 ml-2">({alerts.filter(a => a.enabled).length} active)</span> </div> <button onClick={() => { setShowForm(!showForm); setEditingAlert(null); setForm({ name: '', underlying: 'BTC', metric: 'iv_rank', condition: 'lt', threshold: '', cooldownMinutes: 30, telegramEnabled: true, pushEnabled: true }) }} className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white transition-colors"> {showForm ? 'βœ• Cancel' : '+ New Alert'} </button> </div> {/* Create/Edit Form */} {showForm && ( <div className="bg-darkSurface border border-darkBorder rounded-xl p-4 mb-4"> <h3 className="text-white font-bold text-sm mb-3">{editingAlert ? 'Edit Alert' : 'Create Alert'}</h3> <div className="grid grid-cols-2 md:grid-cols-3 gap-3"> {/* Name */} <div className="col-span-2 md:col-span-3"> <label className="text-[10px] text-gray-500 uppercase">Alert Name</label> <input type="text" value={form.name} onChange={e => setForm({...form, name: e.target.value})} placeholder="e.g. BTC IV Rank Low" className="w-full mt-1 bg-darkBg border border-darkBorder rounded px-3 py-2 text-white text-sm" /> </div> {/* Underlying */} <div> <label className="text-[10px] text-gray-500 uppercase">Asset</label> <select value={form.underlying} onChange={e => setForm({...form, underlying: e.target.value})} className="w-full mt-1 bg-darkBg border border-darkBorder rounded px-3 py-2 text-white text-sm"> {underlyings.map(u => <option key={u} value={u}>{u}</option>)} </select> </div> {/* Metric */} <div> <label className="text-[10px] text-gray-500 uppercase">Metric</label> <select value={form.metric} onChange={e => setForm({...form, metric: e.target.value})} className="w-full mt-1 bg-darkBg border border-darkBorder rounded px-3 py-2 text-white text-sm"> {metrics.map(m => <option key={m.id} value={m.id}>{m.label}</option>)} </select> </div> {/* Condition */} <div> <label className="text-[10px] text-gray-500 uppercase">Condition</label> <select value={form.condition} onChange={e => setForm({...form, condition: e.target.value})} className="w-full mt-1 bg-darkBg border border-darkBorder rounded px-3 py-2 text-white text-sm"> {conditions.map(c => <option key={c} value={c}>{condLabels[c] || c}</option>)} </select> </div> {/* Threshold */} <div> <label className="text-[10px] text-gray-500 uppercase">Threshold {metricUnit(form.metric) ? `(${metricUnit(form.metric)})` : ''}</label> <input type="number" value={form.threshold} onChange={e => setForm({...form, threshold: e.target.value})} step="any" placeholder="20" className="w-full mt-1 bg-darkBg border border-darkBorder rounded px-3 py-2 text-white text-sm font-mono" /> </div> {/* Cooldown */} <div> <label className="text-[10px] text-gray-500 uppercase">Cooldown (min)</label> <input type="number" value={form.cooldownMinutes} onChange={e => setForm({...form, cooldownMinutes: parseInt(e.target.value) || 30})} min="1" className="w-full mt-1 bg-darkBg border border-darkBorder rounded px-3 py-2 text-white text-sm font-mono" /> </div> {/* Delivery toggles */} <div className="flex items-end gap-4 pb-1"> <label className="flex items-center gap-2 text-xs text-gray-300 cursor-pointer"> <input type="checkbox" checked={form.telegramEnabled} onChange={e => setForm({...form, telegramEnabled: e.target.checked})} className="accent-blue-500" /> Telegram </label> <label className="flex items-center gap-2 text-xs text-gray-300 cursor-pointer"> <input type="checkbox" checked={form.pushEnabled} onChange={e => setForm({...form, pushEnabled: e.target.checked})} className="accent-blue-500" /> Push </label> </div> </div> {/* Description hint */} {form.metric && ( <div className="mt-2 text-[11px] text-gray-500 italic"> {metrics.find(m => m.id === form.metric)?.description} </div> )} {/* Preview */} {form.name && form.threshold !== '' && ( <div className="mt-3 bg-darkBg rounded-lg px-3 py-2 border border-darkBorder/50 text-xs text-gray-300"> Preview: <span className="text-white font-bold">{form.underlying}</span> {metricLabel(form.metric)} <span className="text-yellow-400">{condLabels[form.condition]}</span> <span className="text-white font-mono">{form.threshold}{metricUnit(form.metric)}</span> </div> )} <button onClick={handleCreate} className="mt-3 w-full py-2 rounded-lg text-sm font-bold bg-green-600 hover:bg-green-700 text-white transition-colors"> {editingAlert ? 'Save Changes' : 'Create Alert'} </button> </div> )} {/* Loading */} {isLoading && ( <div className="flex justify-center py-12"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> </div> )} {/* Empty state */} {!isLoading && alerts.length === 0 && !showForm && ( <div className="text-center py-16 text-gray-500"> <Bell size={48} className="mx-auto mb-3 opacity-30" /> <p className="text-lg font-medium">No alerts configured</p> <p className="text-sm mt-1">Create your first alert to get notified when market conditions change</p> </div> )} {/* Alert Cards */} {!isLoading && alerts.length > 0 && ( <div className="space-y-2"> {alerts.map(alert => ( <div key={alert.id} className={`bg-darkSurface border rounded-xl overflow-hidden transition-colors ${ alert.enabled ? 'border-darkBorder' : 'border-darkBorder/50 opacity-60' }`}> {/* Main row */} <div className="flex items-center gap-3 px-4 py-3"> {/* Toggle */} <button onClick={() => handleToggle(alert.id)} className={`flex-shrink-0 w-10 h-5 rounded-full relative transition-colors ${alert.enabled ? 'bg-green-600' : 'bg-gray-700'}`}> <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${alert.enabled ? 'left-5' : 'left-0.5'}`} /> </button> {/* Info */} <div className="flex-1 min-w-0" onClick={() => setExpandedId(expandedId === alert.id ? null : alert.id)} style={{ cursor: 'pointer' }}> <div className="flex items-center gap-2"> <span className="text-white font-bold text-sm truncate">{alert.name}</span> <span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-600/20 text-blue-400 font-mono">{alert.underlying}</span> </div> <div className="text-[11px] text-gray-400 mt-0.5 font-mono"> {metricLabel(alert.metric)} {condLabels[alert.condition]} {alert.threshold}{metricUnit(alert.metric)} </div> </div> {/* Delivery indicators */} <div className="flex gap-1 flex-shrink-0"> {alert.telegramEnabled && <span title="Telegram" className="text-[10px] bg-blue-500/20 text-blue-400 px-1.5 py-0.5 rounded">TG</span>} {alert.pushEnabled && <span title="Push" className="text-[10px] bg-purple-500/20 text-purple-400 px-1.5 py-0.5 rounded">Push</span>} </div> {/* Last triggered */} <div className="flex-shrink-0 text-right"> <div className="text-[10px] text-gray-500">Last trigger</div> <div className={`text-[11px] font-mono ${alert.lastTriggeredAt ? 'text-yellow-400' : 'text-gray-600'}`}> {timeSince(alert.lastTriggeredAt)} </div> </div> {/* Actions */} <div className="flex gap-1 flex-shrink-0"> <button onClick={() => handleEdit(alert)} className="text-gray-500 hover:text-white p-1 transition-colors" title="Edit">✏️</button> <button onClick={() => handleDelete(alert.id)} className="text-gray-500 hover:text-red-400 p-1 transition-colors" title="Delete">πŸ—‘οΈ</button> </div> </div> {/* Expanded: Trigger History */} {expandedId === alert.id && ( <div className="border-t border-darkBorder/50 px-4 py-3 bg-darkBg/50"> <div className="text-[10px] text-gray-500 uppercase mb-2">Recent Triggers</div> {triggerData?.data?.length > 0 ? ( <div className="space-y-1.5"> {triggerData.data.map((t, i) => ( <div key={i} className="flex items-center gap-3 text-[11px] font-mono"> <span className="text-gray-500">{new Date(t.createdAt).toLocaleString()}</span> <span className="text-yellow-400">value: {typeof t.value === 'number' ? t.value.toFixed(2) : t.value}</span> </div> ))} </div> ) : ( <div className="text-[11px] text-gray-600 italic">No triggers yet</div> )} </div> )} </div> ))} </div> )} </div> ) } // ═══════════════════════════════════════════════════════════════ // SIGNALS DASHBOARD (existing) // ═══════════════════════════════════════════════════════════════ function TradeModal({ signal: sig, tradeResult, setTradeResult, onClose }) { const t = sig.trade || {} const isCombo = !!(t.contracts && t.contracts.length > 1) const contracts = isCombo ? t.contracts : [{ symbol: t.symbol || t.contract || sig.parameters?.symbol || '', entry: t.entry || parseFloat(sig.parameters?.costUsd) || 0, type: t.action?.includes('CALL') ? 'CALL' : 'PUT', tpPct: t.target?.returnPct || 100, qty: t.qty || 0.01 }] const defaultQty = contracts[0]?.qty || t.qty || 0.01 const [qty, setQtyLocal] = useState(defaultQty) const [submitting, setSubmitting] = useState(false) const [legResults, setLegResults] = useState([]) const totalCost = contracts.reduce((s, c) => s + (c.entry || 0) * qty, 0) const sizing = t.sizing || null const handleExecute = async () => { setSubmitting(true) setTradeResult(null) setLegResults([]) const base = import.meta.env.DEV ? 'http://localhost:8080' : '' const results = [] for (const c of contracts) { try { const tpPct = c.tpPct || t.target?.returnPct || 100 const body = { symbol: c.symbol, side: 'BUY', quantity: qty, price: c.entry, tpPct } const res = await fetch(`${base}/api/trading/order`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const json = await res.json() results.push({ symbol: c.symbol, type: c.type, ...json }) } catch (err) { results.push({ symbol: c.symbol, type: c.type, success: false, error: err.message }) } } setLegResults(results) const allOk = results.every(r => r.success) setTradeResult({ success: allOk, data: { legs: results } }) if (allOk) setTimeout(() => onClose(), 3000) setSubmitting(false) } return ( <div className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4" onClick={onClose}> <div className="bg-darkBg border border-darkBorder rounded-xl max-w-md w-full shadow-2xl" onClick={e => e.stopPropagation()}> <div className="flex justify-between items-center px-4 py-3 border-b border-darkBorder"> <h3 className="text-lg font-bold text-white flex items-center gap-2">⚑ Execute {isCombo ? 'Combo' : 'Trade'}</h3> <button onClick={onClose} className="text-gray-400 hover:text-white text-xl">βœ•</button> </div> <div className="px-4 py-3 space-y-3"> <div className="bg-darkSurface rounded-lg p-3 border border-darkBorder"> <div className="flex items-center gap-2 mb-1"> <span className={`text-xs font-bold px-2 py-0.5 rounded ${sig.direction === 'BULLISH' ? 'bg-green-600/20 text-green-400' : sig.direction === 'BEARISH' ? 'bg-red-600/20 text-red-400' : 'bg-blue-600/20 text-blue-400'}`}> {sig.direction} </span> <span className="text-white font-bold text-sm">{sig.underlying} β€” {sig.strategy}</span> </div> {contracts.map((c, i) => ( <div key={i} className="flex justify-between text-[11px] text-gray-400 font-mono mt-1"> <span>{c.type === 'CALL' ? 'πŸ“ˆ' : 'πŸ“‰'} {c.type} {c.symbol}</span> <span className="text-white">${c.entry?.toFixed(4)}</span> </div> ))} </div> {/* Qty input β€” pre-filled from sizing */} <div> <label className="text-[10px] text-gray-500 uppercase">Quantity (both legs)</label> <input type="number" value={qty} onChange={e => setQtyLocal(e.target.value === '' ? '' : parseFloat(e.target.value))} onBlur={() => { if (!qty || qty < 0.01) setQtyLocal(0.01) }} step="0.01" min="0.01" className="w-full mt-1 bg-darkSurface border border-darkBorder rounded px-2 py-1.5 text-white text-sm font-mono" /> {sizing && ( <div className="text-[10px] text-gray-500 mt-1"> Suggested: {defaultQty} ({sizing.riskPct}% of ${sizing.balance} = ${sizing.riskAmount} risk) </div> )} </div> {/* Cost + TP summary per leg */} <div className="bg-darkSurface rounded-lg px-3 py-2 border border-darkBorder text-xs font-mono space-y-1"> <div className="flex justify-between text-gray-400"> <span>Total Cost (max loss):</span> <span className="text-white font-bold">${totalCost.toFixed(4)}</span> </div> {sizing && sizing.riskOfDeposit > 5 && ( <div className="text-yellow-400">⚠️ {sizing.riskOfDeposit.toFixed(1)}% of deposit β€” high risk!</div> )} {contracts.map((c, i) => { const tp = c.tpPct || t.target?.returnPct || 100 return ( <div key={i} className="flex justify-between text-gray-400 pt-1 border-t border-darkBorder/30"> <span>{c.type} TP ({tp}%):</span> <span className="text-green-400">${(c.entry * (1 + tp / 100)).toFixed(4)}</span> </div> ) })} <div className="flex justify-between text-gray-400 pt-1 border-t border-darkBorder/30"> <span>Profit if winning leg hits TP:</span> <span className="text-green-400 font-bold">+${(contracts[0].entry * qty * (contracts[0].tpPct || t.target?.returnPct || 100) / 100).toFixed(4)}</span> </div> </div> {/* Results per leg */} {legResults.length > 0 && legResults.map((r, i) => ( <div key={i} className={`rounded-lg px-3 py-2 text-sm ${r.success ? 'bg-green-500/10 border border-green-500/30 text-green-400' : 'bg-red-500/10 border border-red-500/30 text-red-400'}`}> {r.success ? `βœ… ${r.type} placed: ${r.symbol}` : `❌ ${r.type}: ${r.error}`} </div> ))} <button onClick={handleExecute} disabled={submitting} className={`w-full py-2.5 rounded-lg text-sm font-bold transition-colors ${ submitting ? 'bg-gray-700 text-gray-500' : 'bg-green-600 hover:bg-green-700 text-white' }`}> {submitting ? '⏳ Placing...' : `⚑ BUY ${isCombo ? `${contracts.length} legs` : contracts[0]?.symbol} + auto TP`} </button> <div className="text-[10px] text-gray-600 text-center">Premium = max loss. TP SELL auto-placed on fill per leg.</div> </div> </div> </div> ) } // ─── BACKTEST TAB ────────────────────────────────────────── function SortHeader({ label, field, sort, setSort, align = 'right' }) { const active = sort.field === field const arrow = active ? (sort.dir === 'asc' ? ' β–²' : ' β–Ό') : '' return ( <th className={`${align === 'left' ? 'text-left' : 'text-right'} px-3 py-2 cursor-pointer select-none hover:text-gray-300 transition-colors ${active ? 'text-cyan-400' : ''}`} onClick={() => setSort(s => s.field === field ? { field, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { field, dir: 'desc' })}> {label}{arrow} </th> ) } function BacktestTab() { const [days, setDays] = useState(30) const [sigFilter, setSigFilter] = useState({ strategy: '', underlying: '', outcome: '' }) const [sigPage, setSigPage] = useState(0) const [stratSort, setStratSort] = useState({ field: 'total', dir: 'desc' }) const [assetSort, setAssetSort] = useState({ field: 'total', dir: 'desc' }) const [expandedSig, setExpandedSig] = useState(null) const base = import.meta.env.DEV ? 'http://localhost:8080' : '' const { data: stats, isLoading: statsLoading } = useQuery({ queryKey: ['backtestStats', days], queryFn: async () => { const res = await fetch(`${base}/api/backtest/stats?days=${days}`) const json = await res.json() return json.data || json }, staleTime: 60000, }) const sigParams = new URLSearchParams({ limit: '20', offset: String(sigPage * 20) }) if (sigFilter.strategy) sigParams.set('strategy', sigFilter.strategy) if (sigFilter.underlying) sigParams.set('underlying', sigFilter.underlying) if (sigFilter.outcome) sigParams.set('outcome', sigFilter.outcome) const { data: signals, isLoading: sigLoading } = useQuery({ queryKey: ['backtestSignals', sigPage, sigFilter], queryFn: async () => { const res = await fetch(`${base}/api/backtest/signals?${sigParams}`) const json = await res.json() return json.data || json }, staleTime: 30000, }) const fmtPnl = (v) => { if (v == null || isNaN(v)) return 'β€”' const n = Number(v) const color = n > 0 ? 'text-green-400' : n < 0 ? 'text-red-400' : 'text-gray-400' return <span className={color}>{n > 0 ? '+' : ''}{n.toFixed(2)}%</span> } const fmtWr = (v) => { if (v == null) return 'β€”' const n = Number(v) const color = n >= 60 ? 'text-green-400' : n >= 45 ? 'text-yellow-400' : 'text-red-400' return <span className={color}>{n.toFixed(1)}%</span> } const sortRows = (rows, sort) => { return [...rows].sort((a, b) => { const av = a[sort.field] ?? 0, bv = b[sort.field] ?? 0 return sort.dir === 'asc' ? av - bv : bv - av }) } const strategies = sortRows(stats?.strategies || [], stratSort) const underlyings = sortRows(stats?.underlyings || [], assetSort) const sigList = signals?.signals || [] const sigTotal = signals?.total || 0 return ( <div className="space-y-6"> {/* Period selector */} <div className="flex items-center gap-3"> <span className="text-gray-400 text-sm">Period:</span> {[7, 14, 30, 90].map(d => ( <button key={d} onClick={() => { setDays(d); setSigPage(0) }} className={`px-3 py-1 rounded text-sm font-medium transition-colors ${ days === d ? 'bg-cyan-600 text-white' : 'bg-darkSurface text-gray-400 hover:text-white border border-darkBorder' }`}>{d}D</button> ))} </div> {statsLoading ? ( <div className="flex justify-center py-12"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-cyan-500" /></div> ) : stats ? ( <> {/* Overview cards */} <div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> {[ { label: 'Total Signals', value: stats.totalSignals?.toLocaleString(), sub: `${stats.completed} completed` }, { label: 'Win Rate', value: fmtWr(stats.overallWinRate), sub: `${stats.pending} pending` }, { label: 'Avg PnL 24h', value: fmtPnl(stats.avgPnl24h), sub: 'option-based' }, { label: 'Completed', value: `${stats.completed}`, sub: `${((stats.completed / (stats.totalSignals || 1)) * 100).toFixed(0)}% tracked` }, ].map((c, i) => ( <div key={i} className="bg-darkSurface border border-darkBorder rounded-lg p-4"> <div className="text-xs text-gray-500 uppercase tracking-wider">{c.label}</div> <div className="text-xl font-bold mt-1">{c.value}</div> <div className="text-xs text-gray-500 mt-1">{c.sub}</div> </div> ))} </div> {/* Strategy performance table */} <div className="bg-darkSurface border border-darkBorder rounded-lg overflow-hidden"> <div className="px-4 py-3 border-b border-darkBorder"> <h3 className="font-semibold text-sm">Strategy Performance</h3> </div> <div className="overflow-x-auto"> <table className="w-full text-sm"> <thead> <tr className="text-xs text-gray-500 uppercase border-b border-darkBorder"> <th className="text-left px-4 py-2">Strategy</th> <SortHeader label="Signals" field="total" sort={stratSort} setSort={setStratSort} /> <SortHeader label="Wins" field="wins" sort={stratSort} setSort={setStratSort} /> <SortHeader label="Losses" field="losses" sort={stratSort} setSort={setStratSort} /> <SortHeader label="Win Rate" field="winRate" sort={stratSort} setSort={setStratSort} /> <SortHeader label="Avg 1h" field="avgPnl1h" sort={stratSort} setSort={setStratSort} /> <SortHeader label="Avg 4h" field="avgPnl4h" sort={stratSort} setSort={setStratSort} /> <SortHeader label="Avg 24h" field="avgPnl24h" sort={stratSort} setSort={setStratSort} /> </tr> </thead> <tbody> {strategies.map((s, i) => ( <tr key={i} className="border-b border-darkBorder/50 hover:bg-darkBorder/30 cursor-pointer" onClick={() => { setSigFilter(f => ({ ...f, strategy: f.strategy === s.strategy ? '' : s.strategy })); setSigPage(0) }}> <td className="px-4 py-2 font-medium whitespace-nowrap"> {sigFilter.strategy === s.strategy && <span className="text-cyan-400 mr-1">●</span>} {s.strategy} </td> <td className="text-right px-3 py-2 text-gray-400">{s.total}</td> <td className="text-right px-3 py-2 text-green-400">{s.wins}</td> <td className="text-right px-3 py-2 text-red-400">{s.losses}</td> <td className="text-right px-3 py-2 font-medium">{fmtWr(s.winRate)}</td> <td className="text-right px-3 py-2">{fmtPnl(s.avgPnl1h)}</td> <td className="text-right px-3 py-2">{fmtPnl(s.avgPnl4h)}</td> <td className="text-right px-3 py-2 font-medium">{fmtPnl(s.avgPnl24h)}</td> </tr> ))} </tbody> </table> </div> </div> {/* Underlying performance table */} <div className="bg-darkSurface border border-darkBorder rounded-lg overflow-hidden"> <div className="px-4 py-3 border-b border-darkBorder"> <h3 className="font-semibold text-sm">Performance by Asset</h3> </div> <div className="overflow-x-auto"> <table className="w-full text-sm"> <thead> <tr className="text-xs text-gray-500 uppercase border-b border-darkBorder"> <th className="text-left px-4 py-2">Asset</th> <SortHeader label="Signals" field="total" sort={assetSort} setSort={setAssetSort} /> <SortHeader label="Wins" field="wins" sort={assetSort} setSort={setAssetSort} /> <SortHeader label="Win Rate" field="winRate" sort={assetSort} setSort={setAssetSort} /> <SortHeader label="Avg 24h PnL" field="avgPnl24h" sort={assetSort} setSort={setAssetSort} /> </tr> </thead> <tbody> {underlyings.map((u, i) => ( <tr key={i} className="border-b border-darkBorder/50 hover:bg-darkBorder/30 cursor-pointer" onClick={() => { setSigFilter(f => ({ ...f, underlying: f.underlying === u.underlying ? '' : u.underlying })); setSigPage(0) }}> <td className="px-4 py-2 font-medium"> {sigFilter.underlying === u.underlying && <span className="text-cyan-400 mr-1">●</span>} {u.underlying} </td> <td className="text-right px-3 py-2 text-gray-400">{u.total}</td> <td className="text-right px-3 py-2 text-green-400">{u.wins}</td> <td className="text-right px-3 py-2 font-medium">{fmtWr(u.winRate)}</td> <td className="text-right px-3 py-2 font-medium">{fmtPnl(u.avgPnl24h)}</td> </tr> ))} </tbody> </table> </div> </div> </> ) : null} {/* Signal Log */} <div className="bg-darkSurface border border-darkBorder rounded-lg overflow-hidden"> <div className="px-4 py-3 border-b border-darkBorder flex items-center justify-between flex-wrap gap-2"> <h3 className="font-semibold text-sm">Signal Log ({sigTotal} total)</h3> <div className="flex gap-2"> {['WIN', 'LOSS', 'PENDING'].map(o => ( <button key={o} onClick={() => { setSigFilter(f => ({ ...f, outcome: f.outcome === o ? '' : o })); setSigPage(0) }} className={`px-2 py-0.5 rounded text-xs font-medium transition-colors ${ sigFilter.outcome === o ? o === 'WIN' ? 'bg-green-600 text-white' : o === 'LOSS' ? 'bg-red-600 text-white' : 'bg-gray-600 text-white' : 'bg-darkBorder text-gray-400 hover:text-white' }`}>{o}</button> ))} {(sigFilter.strategy || sigFilter.underlying || sigFilter.outcome) && ( <button onClick={() => { setSigFilter({ strategy: '', underlying: '', outcome: '' }); setSigPage(0) }} className="px-2 py-0.5 rounded text-xs text-gray-500 hover:text-white">Clear</button> )} </div> </div> {sigLoading ? ( <div className="flex justify-center py-8"><div className="animate-spin rounded-full h-6 w-6 border-b-2 border-cyan-500" /></div> ) : ( <> <div className="overflow-x-auto"> <table className="w-full text-sm"> <thead> <tr className="text-xs text-gray-500 uppercase border-b border-darkBorder"> <th className="text-left px-4 py-2">Time</th> <th className="text-left px-3 py-2">Strategy</th> <th className="text-left px-3 py-2">Asset</th> <th className="text-center px-3 py-2">Dir</th> <th className="text-right px-3 py-2">Conf</th> <th className="text-right px-3 py-2">Spot</th> <th className="text-right px-3 py-2">1h</th> <th className="text-right px-3 py-2">4h</th> <th className="text-right px-3 py-2">24h</th> <th className="text-center px-3 py-2">Result</th> </tr> </thead> <tbody> {sigList.map((s, i) => { const t = new Date(s.createdAt) const dir = s.direction const isExpanded = expandedSig === s.id const params = s.parameters || {} return ( <> <tr key={s.id || i} className={`border-b border-darkBorder/50 hover:bg-darkBorder/30 cursor-pointer ${isExpanded ? 'bg-darkBorder/20' : ''}`} onClick={() => setExpandedSig(isExpanded ? null : s.id)}> <td className="px-4 py-2 text-gray-400 whitespace-nowrap text-xs"> <span className="text-gray-600 mr-1">{isExpanded ? 'β–Ύ' : 'β–Έ'}</span> {t.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} {t.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })} </td> <td className="px-3 py-2 whitespace-nowrap">{s.strategy}</td> <td className="px-3 py-2 font-medium">{s.underlying}</td> <td className="px-3 py-2 text-center"> {dir === 'BULLISH' ? <span className="text-green-400">β–²</span> : dir === 'BEARISH' ? <span className="text-red-400">β–Ό</span> : <span className="text-gray-400">β—†</span>} </td> <td className="text-right px-3 py-2 text-gray-400">{s.confidence}</td> <td className="text-right px-3 py-2 text-gray-400">${Number(s.spotAtSignal).toLocaleString(undefined, { maximumFractionDigits: 2 })}</td> <td className="text-right px-3 py-2">{fmtPnl(s.pnlPct1h)}</td> <td className="text-right px-3 py-2">{fmtPnl(s.pnlPct4h)}</td> <td className="text-right px-3 py-2 font-medium">{fmtPnl(s.pnlPct24h)}</td> <td className="px-3 py-2 text-center"> {s.outcome === 'WIN' ? <span className="text-green-400 font-medium">WIN</span> : s.outcome === 'LOSS' ? <span className="text-red-400 font-medium">LOSS</span> : <span className="text-gray-500 text-xs">pending</span>} </td> </tr> {isExpanded && ( <tr key={`${s.id}-detail`} className="bg-darkBorder/10"> <td colSpan={10} className="px-6 py-3"> <div className="text-xs space-y-2"> {s.description && ( <p className="text-gray-300 leading-relaxed">{s.description}</p> )} <div className="flex flex-wrap gap-x-6 gap-y-1 text-gray-500"> {s.severity && <span>Severity: <span className={ s.severity === 'EXTREME' ? 'text-red-400' : s.severity === 'HIGH' ? 'text-orange-400' : 'text-gray-400' }>{s.severity}</span></span>} {s.spotAfter1h != null && <span>Spot 1h: <span className="text-gray-300">${Number(s.spotAfter1h).toLocaleString(undefined, { maximumFractionDigits: 2 })}</span></span>} {s.spotAfter4h != null && <span>Spot 4h: <span className="text-gray-300">${Number(s.spotAfter4h).toLocaleString(undefined, { maximumFractionDigits: 2 })}</span></span>} {s.spotAfter24h != null && <span>Spot 24h: <span className="text-gray-300">${Number(s.spotAfter24h).toLocaleString(undefined, { maximumFractionDigits: 2 })}</span></span>} </div> {Object.keys(params).length > 0 && ( <div className="flex flex-wrap gap-x-4 gap-y-1 text-gray-500 pt-1 border-t border-darkBorder/50"> {Object.entries(params).map(([k, v]) => ( <span key={k}>{k}: <span className="text-gray-400">{typeof v === 'number' ? v.toFixed?.(4) ?? v : String(v)}</span></span> ))} </div> )} </div> </td> </tr> )} </> ) })} {sigList.length === 0 && ( <tr><td colSpan={10} className="text-center py-8 text-gray-500">No signals found</td></tr> )} </tbody> </table> </div> {/* Pagination */} {sigTotal > 20 && ( <div className="flex items-center justify-between px-4 py-3 border-t border-darkBorder"> <span className="text-xs text-gray-500"> {sigPage * 20 + 1}–{Math.min((sigPage + 1) * 20, sigTotal)} of {sigTotal} </span> <div className="flex gap-2"> <button disabled={sigPage === 0} onClick={() => setSigPage(p => p - 1)} className="px-3 py-1 rounded text-xs bg-darkBorder text-gray-400 hover:text-white disabled:opacity-30">Prev</button> <button disabled={(sigPage + 1) * 20 >= sigTotal} onClick={() => setSigPage(p => p + 1)} className="px-3 py-1 rounded text-xs bg-darkBorder text-gray-400 hover:text-white disabled:opacity-30">Next</button> </div> </div> )} </> )} </div> </div> ) } function Dashboard() { const [activeTab, setActiveTab] = useState('signals') // 'signals' | 'chain' | 'whale' | 'iv-surface' | 'alerts' const [selectedAsset, setSelectedAsset] = useState('ALL') const [selectedTimeframe, setSelectedTimeframe] = useState('ALL') // New state const [pushStatus, setPushStatus] = useState(typeof Notification !== 'undefined' ? Notification.permission : 'default') const [isSubscribed, setIsSubscribed] = useState(false); const [isTestingPush, setIsTestingPush] = useState(false); const [highlightedSignal, setHighlightedSignal] = useState(null); const [pnlCalc, setPnlCalc] = useState({ isOpen: false, prefill: null }) const [tradeModal, setTradeModal] = useState({ isOpen: false, signal: null }) const [tradeResult, setTradeResult] = useState(null) // { success, data/error } // Fetch trading status const { data: tradingStatus } = useQuery({ queryKey: ['tradingStatus'], queryFn: async () => { const base = import.meta.env.DEV ? 'http://localhost:8080' : ''; const res = await fetch(`${base}/api/trading/status`) if (!res.ok) return { data: { enabled: false } } return res.json() }, staleTime: 60000, }) const tradingEnabled = tradingStatus?.data?.enabled || false // Open P&L calculator with prefilled data from a trade signal function openPnlCalc(trade) { if (!trade) { setPnlCalc({ isOpen: true, prefill: null }) return } const tradeUnit = trade.unit || 1 const isCombo = !!(trade.contracts && trade.contracts.length > 1) const rawPremium = isCombo ? (trade.totalEntry || 0) : (trade.entry || 0) setPnlCalc({ isOpen: true, prefill: { type: isCombo ? 'CALL' : (trade.action?.includes('CALL') ? 'CALL' : 'PUT'), strike: trade.strike || trade.contracts?.[0]?.strike || 0, premium: trade.premiumPerUnit || (rawPremium / tradeUnit), qty: trade.qty || trade.contracts?.[0]?.qty || 1, unit: tradeUnit, spot: trade.spot || 0, isCombo, } }) } // Check current subscription status on load useEffect(() => { // Force PWA to aggressively fetch the absolute latest version from the server // and skip the "waiting" lifecycle phase so the Bug icon appears immediately. registerSW({ immediate: true, onNeedRefresh() { // Automatically reload the page when a new version (with the Bug icon) is available window.location.reload(true); } }); async function checkSub() { if ('serviceWorker' in navigator && 'PushManager' in window && typeof Notification !== 'undefined' && Notification.permission === 'granted') { const registration = await navigator.serviceWorker.ready; const sub = await registration.pushManager.getSubscription(); setIsSubscribed(!!sub); } } checkSub(); }, []); async function handleSubscribe() { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { alert('Π’Π°Ρˆ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€ ΠΈΠ»ΠΈ ОБ Π½Π΅ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ Push-увСдомлСния.'); return; } try { const registration = await navigator.serviceWorker.ready; let permission = Notification.permission; if (isSubscribed) { const subscription = await registration.pushManager.getSubscription(); if (subscription) { await subscription.unsubscribe(); const baseUrl = import.meta.env.DEV ? 'http://localhost:8080' : ''; await fetch(`${baseUrl}/api/notifications/unsubscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) }); } setIsSubscribed(false); alert('πŸ”• УвСдомлСния Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½Ρ‹.'); return; } if (permission !== 'granted') { permission = await Notification.requestPermission(); } setPushStatus(permission); if (permission === 'granted') { const baseUrl = import.meta.env.DEV ? 'http://localhost:8080' : ''; const vapidRes = await fetch(`${baseUrl}/api/notifications/vapidPublicKey`); const { publicKey } = await vapidRes.json(); const base64ToUint8Array = base64String => { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; }; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64ToUint8Array(publicKey) }); await fetch(`${baseUrl}/api/notifications/subscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) }); setIsSubscribed(true); alert('βœ… УвСдомлСния ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ Π²ΠΊΠ»ΡŽΡ‡Π΅Π½Ρ‹!'); } } catch (error) { console.error('Push Subscription Error:', error); alert('Ошибка подписки Π½Π° Push-увСдомлСния. Π£Π±Π΅Π΄ΠΈΡ‚Π΅ΡΡŒ, Ρ‡Ρ‚ΠΎ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€ Ρ€Π°Π·Ρ€Π΅ΡˆΠ°Π΅Ρ‚ увСдомлСния для этого сайта.'); } } async function handleTestNotification() { if (isTestingPush) return; setIsTestingPush(true); try { const baseUrl = import.meta.env.DEV ? 'http://localhost:8080' : ''; const res = await fetch(`${baseUrl}/api/notifications/test`, { method: 'POST' }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || 'Failed to send test push'); } } catch (e) { console.error('Ошибка ΠΏΡ€ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ΅ тСстового ΠΏΡƒΡˆΠ°:', e); alert('Ошибка ΠΏΡ€ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ΅ тСстового ΠΏΡƒΡˆΠ°: ' + e.message); } finally { // Disable the button for 5 seconds to prevent spamming Chrome's push heuristic setTimeout(() => setIsTestingPush(false), 5000); } } const { data, isLoading, error } = useQuery({ queryKey: ['dashboardData', selectedTimeframe], queryFn: async () => { // Determine the API base URL based on the environment const baseUrl = import.meta.env.DEV ? 'http://localhost:8080' : ''; const res = await fetch(`${baseUrl}/api/dashboard?timeframe=${selectedTimeframe}`) if (!res.ok) throw new Error('Failed to fetch data') return res.json() } }) // Deep Link Highlighting Logic useEffect(() => { if (data?.signals && data.signals.length > 0) { const params = new URLSearchParams(window.location.search); const targetId = params.get('signalId'); if (targetId) { // Switch to signals tab and show ALL assets so the card is visible setActiveTab('signals'); setSelectedAsset('ALL'); setHighlightedSignal(targetId); // Allow React a tick to render the 'ALL' list if it was previously filtered setTimeout(() => { const el = document.getElementById(targetId); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } // Remove URL param cleanly without reloading the page window.history.replaceState({}, document.title, window.location.pathname); }, 150); // Turn off the glowing effect after 3.5 seconds setTimeout(() => { setHighlightedSignal(null); }, 3500); } } }, [data?.signals]); const assets = ['ALL', 'BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'DOGE'] const timeframes = ['ALL', '1W', '2W', '1M'] function renderIcon(type, direction) { if (type === 'COMBO') { if (direction === 'NEUTRAL' || direction === 'NEUTRAL_VOLATILE') return <AlignVerticalJustifyCenter className="text-volatilityText" size={20} /> if (direction === 'NEUTRAL_RANGE') return <Activity className="text-gray-400" size={20} /> return <Activity className="text-blue-500" size={20} /> } if (type === 'ANOMALY') return <ShieldAlert className="text-red-500" size={20} /> if (direction === 'BULLISH') return <TrendingUp className="text-bullishText" size={20} /> if (direction === 'BEARISH') return <TrendingDown className="text-bearishText" size={20} /> return <Activity size={20} /> } function renderColor(type, direction) { if (type === 'COMBO') return 'border-volatilityText bg-volatilityBg' if (type === 'ANOMALY') return 'border-red-500 bg-red-500/10' if (direction === 'BULLISH') return 'border-bullishText bg-bullishBg' if (direction === 'BEARISH') return 'border-bearishText bg-bearishBg' return 'border-darkBorder bg-darkSurface' } const signals = data?.signals || [] const filteredSignals = selectedAsset === 'ALL' ? signals : signals.filter(s => s.underlying === selectedAsset) return ( <div className="min-h-screen bg-darkBg text-gray-200 p-6 font-sans overflow-x-hidden"> {/* HEADER SECTION */} <header className="flex flex-col xl:flex-row justify-between items-start xl:items-center gap-4 mb-8 border-b border-darkBorder pb-4 w-full"> <div> <h1 className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500"> Options Screener V2 </h1> <p className="text-sm text-gray-400 mt-1"> Advanced Auto-Trading Signals & Flow </p> </div> <div className="flex flex-col xl:items-end gap-3 w-full xl:w-auto mt-2 xl:mt-0"> <div className="flex gap-2 w-full justify-start xl:justify-end items-center flex-wrap"> <button onClick={handleSubscribe} className={`p-2 rounded-lg border flex-shrink-0 flex items-center justify-center transition-colors shadow-sm ${isSubscribed ? 'bg-emerald-500/10 border-emerald-500/50 text-emerald-400 hover:bg-emerald-500/20' : 'bg-darkSurface border-darkBorder text-gray-400 hover:text-white hover:border-gray-500' }`} title={isSubscribed ? "Π’Ρ‹ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ увСдомлСния" : "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ увСдомлСния"} > {isSubscribed ? <BellRing size={18} /> : <BellOff size={18} />} </button> {/* Active Asset Filter */} <div className="flex gap-2 bg-darkSurface p-1 rounded-lg border border-darkBorder flex-wrap"> {assets.map(a => ( <button key={`asset-${a}`} onClick={() => setSelectedAsset(a)} className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors whitespace-nowrap ${selectedAsset === a ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`} > {a} </button> ))} </div> </div> {/* Expiry Timeframe Filter */} <div className="flex gap-2 bg-darkSurface p-1 rounded-lg border border-darkBorder w-full xl:w-auto justify-start flex-wrap"> <span className="text-sm text-gray-500 flex items-center px-2">Expiry:</span> {timeframes.map(tf => ( <button key={`tf-${tf}`} onClick={() => setSelectedTimeframe(tf)} className={`px-3 py-1 rounded-md text-xs font-bold transition-colors ${selectedTimeframe === tf ? 'bg-purple-600 outline outline-1 outline-purple-400 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`} > {tf} </button> ))} </div> </div> </header> {/* TAB NAVIGATION */} <div className="flex gap-1 mb-6 bg-darkSurface p-1 rounded-lg border border-darkBorder w-fit"> <button onClick={() => setActiveTab('signals')} className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${ activeTab === 'signals' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`}> <BarChart3 size={16} /> Signals </button> <button onClick={() => setActiveTab('chain')} className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${ activeTab === 'chain' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`}> <Table2 size={16} /> Options Chain </button> <button onClick={() => setActiveTab('whale')} className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${ activeTab === 'whale' ? 'bg-amber-600 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`}> πŸ‹ Whale Flow </button> <button onClick={() => setActiveTab('iv-surface')} className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${ activeTab === 'iv-surface' ? 'bg-purple-600 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`}> <TrendingUp size={16} /> IV Surface </button> <button onClick={() => setActiveTab('alerts')} className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${ activeTab === 'alerts' ? 'bg-green-600 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`}> <Bell size={16} /> Alerts </button> <button onClick={() => setActiveTab('backtest')} className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${ activeTab === 'backtest' ? 'bg-cyan-600 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`}> πŸ“Š Backtest </button> {tradingEnabled && ( <button onClick={() => setActiveTab('trading')} className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${ activeTab === 'trading' ? 'bg-orange-600 text-white' : 'text-gray-400 hover:text-white hover:bg-darkBorder' }`}> ⚑ Trading </button> )} </div> {/* CHAIN VIEW TAB */} {activeTab === 'chain' && <ChainView />} {/* WHALE FLOW TAB */} {activeTab === 'whale' && <WhaleFlow />} {/* IV SURFACE TAB */} {activeTab === 'iv-surface' && <IvSurface />} {/* CUSTOM ALERTS TAB */} {activeTab === 'alerts' && <CustomAlerts />} {/* BACKTEST TAB */} {activeTab === 'backtest' && <BacktestTab />} {/* TRADING TAB β€” Positions + Orders + Account */} {activeTab === 'trading' && <TradingTab />} {/* SIGNALS TAB */} {activeTab === 'signals' && <> {/* ERROR / LOADING STATE */} {isLoading && ( <div className="flex justify-center items-center h-64"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> </div> )} {error && ( <div className="bg-red-500/10 border border-red-500 text-red-400 p-4 rounded-lg flex items-center gap-3"> <AlertTriangle /> <div> <p className="font-bold">Error connecting to server</p> <p className="text-sm text-red-300">Please try again later or check your connection.</p> </div> </div> )} {/* SIGNALS GRID */} {!isLoading && !error && ( <div> <div className="flex justify-between items-end mb-4"> <h2 className="text-xl font-semibold flex items-center gap-2"> <Activity className="text-blue-500" /> Actionable Signals </h2> <div className="text-sm text-gray-500"> Showing {filteredSignals.length} high-confidence setups </div> </div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> {filteredSignals.length === 0 ? ( <div className="col-span-full py-12 text-center text-gray-500 bg-darkSurface rounded-xl border border-darkBorder"> No active high-confidence signals for {selectedAsset}. Market is quiet. </div> ) : ( filteredSignals.map(sig => { const sigCardClasses = `flex flex-col rounded-xl border p-4 shadow-lg transition-all duration-500 hover:-translate-y-1 bg-darkSurface border-l-4 ${renderColor(sig.type, sig.direction)} ${highlightedSignal === sig.id ? 'ring-4 ring-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.3)] border-emerald-500 scale-[1.02]' : 'border-t-darkBorder border-r-darkBorder border-b-darkBorder' }` return ( <div key={sig.id} id={sig.id} className={sigCardClasses} > <div className="flex justify-between items-start mb-3"> <div className="flex items-center gap-2"> <div className={`p-1.5 rounded-md border ${ sig.signal === 'BUY_CALL' ? 'bg-green-600/20 border-green-500/40' : sig.signal === 'BUY_PUT' ? 'bg-red-600/20 border-red-500/40' : sig.strategy === 'Gamma Play' ? 'bg-purple-600/20 border-purple-500/40' : sig.strategy === 'Unusual Volume' ? 'bg-amber-600/20 border-amber-500/40' : (sig.strategy?.includes('Straddle') || sig.strategy?.includes('Strangle') || sig.strategy?.includes('Weekend')) ? 'bg-blue-600/20 border-blue-500/40' : 'bg-darkBg border-darkBorder' }`}> {renderIcon(sig.type, sig.direction)} </div> <div> <span className="text-xs font-bold text-gray-400 tracking-wider"> {sig.underlying} </span> <h3 className={`text-lg font-bold leading-tight mt-0.5 ${ sig.signal === 'BUY_CALL' ? 'text-green-400' : sig.signal === 'BUY_PUT' ? 'text-red-400' : sig.strategy === 'Gamma Play' ? 'text-purple-400' : sig.strategy === 'Unusual Volume' ? 'text-amber-400' : (sig.strategy?.includes('Straddle') || sig.strategy?.includes('Strangle') || sig.strategy?.includes('Weekend')) ? 'text-blue-400' : 'text-white' }`}> {sig.strategy} </h3> </div> </div> {/* Confidence Badge */} <div className={`px-2 py-0.5 rounded text-xs font-bold ${sig.confidence > 80 ? 'bg-green-500/20 text-green-400' : sig.confidence > 60 ? 'bg-yellow-500/20 text-yellow-400' : 'bg-gray-500/20 text-gray-400' }`}> {sig.confidence}% </div> </div> <div className="text-sm text-gray-300 mb-4 flex-grow"> {sig.rationale} </div> {/* Tooltip / Helper text */} {sig.tooltip && ( <div className="mt-auto pt-3 border-t border-darkBorder/50 flex items-start gap-2 text-xs text-gray-400"> <Info size={14} className="mt-0.5 flex-shrink-0 text-blue-400" /> <p className="whitespace-pre-line">{sig.tooltip}</p> </div> )} {/* Explicit Legs Selection */} {sig.legs && sig.legs.length > 0 && ( <div className="mt-3 flex flex-col gap-1 text-xs font-mono bg-darkBg p-2 rounded-md border border-darkBorder text-gray-300"> {sig.legs.map((leg, i) => ( <div key={i} className={`py-0.5 ${leg.startsWith('BUY') ? 'text-blue-400' : 'text-red-400'}`}> {leg} </div> ))} </div> )} {/* Trade Recommendation Card */} {sig.trade && ( <div className="mt-3 text-xs font-mono bg-darkBg rounded-md border border-darkBorder overflow-hidden"> {/* Header */} <div className={`flex justify-between items-center px-3 py-1.5 border-b border-darkBorder ${ sig.signal === 'BUY_CALL' ? 'bg-green-600/20' : sig.signal === 'BUY_PUT' ? 'bg-red-600/20' : sig.strategy === 'Gamma Play' ? 'bg-purple-600/20' : sig.strategy === 'Unusual Volume' ? 'bg-amber-600/20' : 'bg-blue-600/20' }`}> <span className={`font-bold text-[11px] ${ sig.signal === 'BUY_CALL' ? 'text-green-400' : sig.signal === 'BUY_PUT' ? 'text-red-400' : sig.strategy === 'Gamma Play' ? 'text-purple-400' : sig.strategy === 'Unusual Volume' ? 'text-amber-400' : 'text-blue-400' }`}>{sig.trade.action || sig.strategy}</span> <span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${ sig.trade.timeDecayRisk === 'CRITICAL' ? 'bg-red-500/20 text-red-400' : sig.trade.timeDecayRisk === 'HIGH' ? 'bg-orange-500/20 text-orange-400' : sig.trade.timeDecayRisk === 'MEDIUM' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-green-500/20 text-green-400' }`}> ⏳ {sig.trade.dte}d ({sig.trade.timeDecayRisk}) </span> </div> <div className="px-3 py-2 space-y-1.5"> {/* Contract & Entry */} {sig.trade.contract && ( <> <div className="flex justify-between"> <span className="text-gray-500">ΠšΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚:</span> <span className="text-blue-400 font-bold">{sig.trade.contract}</span> </div> <div className="flex justify-between"> <span className="text-gray-500">Π‘Ρ‚Ρ€Π°ΠΉΠΊ:</span> <span className="text-white">${sig.trade.strike?.toLocaleString()}</span> </div> <div className="flex justify-between"> <span className="text-gray-500">Spot сСйчас:</span> <span className="text-white">${sig.trade.spot?.toLocaleString()}</span> </div> </> )} {/* Combo contracts */} {sig.trade.contracts && sig.trade.contracts.map((c, i) => ( <div key={i} className="flex justify-between"> <span className="text-gray-500">{c.type} {c.qty > 1 ? `Γ—${c.qty}` : ''}:</span> <span className="text-blue-400 font-bold text-[10px]">{c.symbol} @ ${c.entry}</span> </div> ))} {/* Entry */} <div className="flex justify-between items-center border-t border-darkBorder/50 pt-1.5 mt-1"> <span className="text-gray-400 font-bold">πŸ’° ΠŸΡ€Π΅ΠΌΠΈΡƒΠΌ:</span> <span className="text-white font-bold"> ${sig.trade.entry?.toFixed(4) || sig.trade.totalEntry?.toFixed(4)}/ct {sig.trade.unit > 1 && <span className="text-gray-500 text-[10px] ml-1">(${sig.trade.premiumPerUnit?.toFixed(4)}/unit, 1ct={sig.trade.unit})</span>} </span> </div> {/* Breakeven */} {sig.trade.breakeven && ( <div className="flex justify-between"> <span className="text-gray-500">πŸ“ Π‘Π΅Π·ΡƒΠ±Ρ‹Ρ‚ΠΎΠΊ:</span> <span className="text-yellow-400">Spot @ ${sig.trade.breakeven?.toLocaleString()}</span> </div> )} {sig.trade.breakevenUp && ( <div className="flex justify-between"> <span className="text-gray-500">πŸ“ Π‘Π΅Π·ΡƒΠ±Ρ‹Ρ‚ΠΎΠΊ:</span> <span className="text-yellow-400 text-[10px]">↑${sig.trade.breakevenUp?.toLocaleString()} / ↓${sig.trade.breakevenDown?.toLocaleString()}</span> </div> )} {/* Target */} {sig.trade.target && ( <div className="flex justify-between items-center bg-green-500/10 border border-green-500/20 px-1.5 py-1 rounded"> <span className="text-green-500/80 font-bold">🎯 Π’Π΅ΠΉΠΊ (+{sig.trade.target.returnPct}%):</span> {sig.trade.target.spotPrice && ( <span className="text-green-400 font-bold">Spot @ ${sig.trade.target.spotPrice?.toLocaleString()}</span> )} {sig.trade.target.spotUp && ( <span className="text-green-400 font-bold text-[10px]">↑${sig.trade.target.spotUp?.toLocaleString()} / ↓${sig.trade.target.spotDown?.toLocaleString()}</span> )} </div> )} {/* Stop Loss β€” only show if concrete spotPrice exists (not "premium=max loss" note) */} {sig.trade.stopLoss?.spotPrice && ( <div className="flex justify-between items-center bg-red-500/10 border border-red-500/20 px-1.5 py-1 rounded"> <span className="text-red-500/80 font-bold">πŸ›‘ Π‘Ρ‚ΠΎΠΏ (-{sig.trade.stopLoss.lossPct}%):</span> <span className="text-red-400 font-bold">Spot @ ${sig.trade.stopLoss.spotPrice?.toLocaleString()}</span> </div> )} {/* Whale Info (Unusual Volume) */} {sig.trade.whaleInfo && ( <div className="flex justify-between items-center bg-amber-500/10 border border-amber-500/20 px-1.5 py-1 rounded"> <span className="text-amber-500/80 font-bold">πŸ‹ ΠšΠΈΡ‚ Π²Π»ΠΈΠ»:</span> <span className="text-amber-400 font-bold">${Number(sig.trade.whaleInfo.totalPremium).toLocaleString('en-US', { maximumFractionDigits: 0 })} ({sig.trade.whaleInfo.voiRatio}x V/OI)</span> </div> )} {/* Gamma Info */} {sig.trade.gammaInfo && ( <div className="flex justify-between items-center bg-purple-500/10 border border-purple-500/20 px-1.5 py-1 rounded"> <span className="text-purple-400/80 font-bold">⚑ Gamma:</span> <span className="text-purple-400 font-bold">1% spot β†’ {sig.trade.gammaInfo.premiumMoveFor1PctSpot?.toFixed(0)}% ΠΏΡ€Π΅ΠΌΠΈΡƒΠΌ ({sig.trade.gammaInfo.moneyness})</span> </div> )} {/* Spot move needed for target (gamma plays) */} {sig.trade.target?.spotMovePct && ( <div className="flex justify-between text-[10px]"> <span className="text-gray-500">Spot Π½ΡƒΠΆΠ½ΠΎ ΠΏΡ€ΠΎΠΉΡ‚ΠΈ:</span> <span className="text-purple-400 font-bold">{sig.trade.target.spotMovePct}%</span> </div> )} {/* Max Loss & Risk/Reward */} <div className="flex justify-between items-center border-t border-darkBorder/50 pt-1.5 mt-1"> <span className="text-gray-500">Макс. ΡƒΠ±Ρ‹Ρ‚ΠΎΠΊ:</span> <span className="text-red-400">${sig.trade.maxLoss?.toFixed(4)}</span> </div> <div className="flex justify-between items-center"> <span className="text-gray-500">Risk/Reward:</span> <div className="flex items-center gap-2"> <span className="text-green-400">{sig.trade.riskReward}</span> <button onClick={() => openPnlCalc(sig.trade)} className="px-1.5 py-0.5 bg-blue-600/20 border border-blue-500/30 rounded text-blue-400 text-[10px] font-bold hover:bg-blue-600/40 transition-colors"> πŸ“Š Calc </button> {tradingEnabled && (sig.trade.symbol || sig.trade.contract || sig.trade.contracts?.[0]?.symbol) && ( <button onClick={() => setTradeModal({ isOpen: true, signal: sig })} className="px-1.5 py-0.5 bg-green-600/20 border border-green-500/30 rounded text-green-400 text-[10px] font-bold hover:bg-green-600/40 transition-colors"> ⚑ Execute </button> )} </div> </div> {/* Greeks footer */} <div className="flex justify-start gap-3 items-center mt-1 pt-1.5 border-t border-darkBorder/30"> {sig.trade.delta && <span className="text-gray-500 text-[10px]">Ξ” <span className="text-gray-300">{sig.trade.delta}</span></span>} {sig.trade.gamma && <span className="text-gray-500 text-[10px]">Ξ“ <span className="text-gray-300">{Number(sig.trade.gamma).toFixed(5)}</span></span>} {sig.trade.theta && <span className="text-gray-500 text-[10px]">Θ <span className="text-gray-300">{Number(sig.trade.theta).toFixed(4)}</span></span>} {sig.trade.dte && <span className="text-gray-500 text-[10px]">DTE <span className="text-gray-300">{sig.trade.dte}d</span></span>} {sig.trade.qty && sig.trade.qty > 1 && <span className="text-gray-500 text-[10px]">QTY <span className="text-gray-300">{sig.trade.qty}</span></span>} </div> </div> </div> )} {/* Legacy Metrics (for non-strategy signals like Unusual Volume, Gamma Play) */} {!sig.trade && sig.parameters && ( <div className="mt-3 grid grid-cols-2 gap-2 text-xs font-mono bg-darkBg px-2 py-2 rounded-md border border-darkBorder text-gray-400"> {sig.parameters.symbol && <div className="col-span-2 flex justify-between"><span className="text-gray-500">Contract:</span> <span className="text-blue-400 font-bold">{sig.parameters.symbol}</span></div>} {sig.parameters.spot && <div className="col-span-2 flex justify-between"><span className="text-gray-500">Spot:</span> <span className="text-white">${parseFloat(sig.parameters.spot).toLocaleString()}</span></div>} {sig.parameters.costUsd !== undefined && ( <div className="col-span-2 flex justify-between items-center border-t border-darkBorder border-dashed pt-1 mt-1"> <span className="text-gray-400">ΠŸΡ€Π΅ΠΌΠΈΡƒΠΌ (ΠšΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚):</span> <span className="font-bold text-white">${Number(sig.parameters.costUsd).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 })}</span> </div> )} {sig.parameters.totalWhalePremium !== undefined && ( <div className="col-span-2 flex justify-between items-center mt-1 pt-1 bg-amber-500/10 border border-amber-500/20 px-1.5 py-0.5 rounded"> <span className="text-amber-500/80 font-bold text-[11px]">πŸ’° Whale Volume (Total):</span> <span className="font-bold text-amber-400 text-[11px]">${Number(sig.parameters.totalWhalePremium).toLocaleString('en-US', { maximumFractionDigits: 0 })}</span> </div> )} {(sig.parameters.ivRank || sig.parameters.delta || sig.parameters.dte) && ( <div className="col-span-2 flex justify-start gap-4 items-center mt-2 pt-2 border-t border-darkBorder/30"> {sig.parameters.ivRank && <span className="text-gray-500 text-[10px] uppercase">IV Rank: <span className="text-gray-300 font-mono">{sig.parameters.ivRank}</span></span>} {sig.parameters.delta && <span className="text-gray-500 text-[10px] uppercase">Delta: <span className="text-gray-300 font-mono">{sig.parameters.delta}</span></span>} {sig.parameters.dte && <span className="text-gray-500 text-[10px] uppercase">DTE: <span className="text-gray-300 font-mono">{sig.parameters.dte}d</span></span>} </div> )} </div> )} </div> )}) )} </div> </div> )} </>} {/* Trade Execution Modal */} {tradeModal.isOpen && tradeModal.signal && ( <TradeModal signal={tradeModal.signal} tradeResult={tradeResult} setTradeResult={setTradeResult} onClose={() => { setTradeModal({ isOpen: false, signal: null }); setTradeResult(null) }} /> )} {/* P&L Calculator Modal */} <PnLCalculator isOpen={pnlCalc.isOpen} onClose={() => setPnlCalc({ isOpen: false, prefill: null })} prefill={pnlCalc.prefill} /> {/* Floating Calc Button */} <button onClick={() => openPnlCalc(null)} className="fixed bottom-6 right-6 bg-blue-600 hover:bg-blue-700 text-white rounded-full w-12 h-12 flex items-center justify-center shadow-lg transition-all hover:scale-110 z-40" title="P&L Calculator"> πŸ“Š </button> </div> ) } export default App