β ΠΠ°Π·Π°Π΄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