← Назад
<!DOCTYPE html> <html lang="en" class="dark"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Options Screener Dashboard</title> <script src="https://cdn.tailwindcss.com"></script> <script> tailwind.config = { darkMode: 'class', theme: { extend: { colors: { background: '#0B0E14', surface: '#1A1E29', surfaceHover: '#232836', border: '#2C3245', primary: '#3B82F6', success: '#10B981', danger: '#EF4444', warning: '#F59E0B', textMain: '#F8FAFC', textMuted: '#94A3B8' } } } } </script> <style> body { background-color: theme('colors.background'); color: theme('colors.textMain'); font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } .glass-card { background: theme('colors.surface'); border: 1px solid theme('colors.border'); } .glass-card:hover { border-color: theme('colors.primary'); } </style> </head> <body class="min-h-screen text-textMain antialiased overflow-x-hidden"> <!-- Header --> <header class="border-b border-border bg-background sticky top-0 z-50"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex flex-col sm:flex-row justify-between items-center gap-4"> <div class="flex items-center gap-3"> <div class="w-8 h-8 rounded bg-primary/20 flex items-center justify-center text-primary font-bold shadow-[0_0_15px_rgba(59,130,246,0.5)]"> &Omega; </div> <h1 class="text-xl font-bold tracking-tight">Options Screener</h1> </div> <div id="header-stats" class="flex gap-6 text-sm"> <div class="flex items-center gap-2"> <span class="text-textMuted">BTC:</span> <span id="btc-price" class="font-mono font-semibold text-textMain animate-pulse">Loading...</span> </div> <div class="flex items-center gap-2"> <span class="text-textMuted">ETH:</span> <span id="eth-price" class="font-mono font-semibold text-textMain animate-pulse">Loading...</span> </div> </div> <div class="text-xs text-textMuted flex items-center gap-2"> <span class="relative flex h-2 w-2"> <span id="live-ping" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75 hidden"></span> <span id="live-dot" class="relative inline-flex rounded-full h-2 w-2 bg-textMuted"></span> </span> <span id="last-update">Connecting...</span> </div> </div> </header> <!-- Main Content --> <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="mb-6 flex justify-between items-end"> <div> <h2 class="text-2xl font-bold mb-1">Trading Signals</h2> <p class="text-textMuted text-sm">Real-time analysis from Unusual Volume, Skew, Gamma, and Strategies</p> </div> <div class="text-sm font-medium text-textMuted"> <span id="signal-count" class="text-white font-bold">0</span> signals found </div> </div> <div id="error-container" class="hidden bg-danger/10 border border-danger/30 text-danger px-4 py-3 rounded-lg mb-6"> <p id="error-message"></p> </div> <!-- Signals Grid --> <div id="signals-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <p class="text-textMuted animate-pulse col-span-full">Loading live dashboard data...</p> </div> </main> <script> const CONFIG = { API_URL: '/api/dashboard' }; const formatCurrency = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(val); const formatDate = (isoStr) => new Date(isoStr).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit' }); const getColors = (signal) => { let bg = 'bg-surfaceHover', text = 'text-textMuted'; if(signal.direction.includes('BULL')) { text = 'text-success'; bg = 'bg-success/10 border-success/20'; } else if(signal.direction.includes('BEAR')) { text = 'text-danger'; bg = 'bg-danger/10 border-danger/20'; } let sevColor = 'text-textMuted bg-surfaceHover'; if(signal.severity === 'EXTREME') sevColor = 'text-white bg-danger shadow-[0_0_8px_rgba(239,68,68,0.6)]'; if(signal.severity === 'HIGH') sevColor = 'text-danger bg-danger/20'; if(signal.severity === 'MEDIUM') sevColor = 'text-warning bg-warning/20'; let confBg = 'bg-textMuted'; if (signal.confidence >= 75) confBg = 'bg-success'; else if (signal.confidence >= 55) confBg = 'bg-warning'; return { dirClass: `${text} ${bg} border`, sevClass: sevColor, confBg }; }; const renderCard = (s) => { const c = getColors(s); return ` <div class="glass-card rounded-xl p-5 flex flex-col transition-all duration-300 relative group"> <div class="flex justify-between items-start mb-3"> <div class="flex items-center gap-2"> <span class="font-bold text-lg text-white">${s.underlying}</span> <span class="text-[10px] px-2 py-0.5 rounded-full uppercase tracking-wider font-semibold ${c.dirClass}">${s.direction.replace(/_/g,' ')}</span> </div> ${s.severity ? `<span class="text-[10px] uppercase font-bold px-2 py-1 rounded tracking-wider ${c.sevClass}">${s.severity}</span>` : ''} </div> <h3 class="font-semibold text-blue-400 text-sm mb-1">${s.strategy}</h3> <p class="text-white text-sm leading-snug mb-3 font-medium">${s.description || 'No description provided'}</p> <p class="text-[12px] text-textMuted flex-grow mb-5 leading-relaxed">${s.rationale}</p> <div class="mt-auto"> <div class="flex justify-between text-xs mb-1"> <span class="text-textMuted font-medium">Confidence Match</span> <span class="text-white font-bold">${s.confidence}%</span> </div> <div class="w-full bg-surfaceHover/80 rounded-full h-1.5 overflow-hidden"> <div class="h-1.5 rounded-full ${c.confBg}" style="width: ${Math.max(5, s.confidence)}%"></div> </div> </div> </div> `; }; const fetchAPI = async () => { try { const res = await fetch(CONFIG.API_URL); const data = await res.json(); if(!data.success) throw new Error(data.error || 'Server returned false'); document.getElementById('btc-price').textContent = formatCurrency(data.spotPrices.BTC); document.getElementById('eth-price').textContent = formatCurrency(data.spotPrices.ETH); const bPriceClasses = document.getElementById('btc-price').classList; bPriceClasses.remove('animate-pulse'); document.getElementById('eth-price').classList.remove('animate-pulse'); document.getElementById('last-update').textContent = 'Live \u2022 Local'; document.getElementById('live-dot').classList.replace('bg-textMuted', 'bg-success'); document.getElementById('live-ping').classList.remove('hidden'); document.getElementById('signal-count').textContent = data.signals ? data.signals.length : 0; const grid = document.getElementById('signals-grid'); grid.innerHTML = data.signals && data.signals.length ? data.signals.map(renderCard).join('') : '<div class="col-span-full py-10 text-center text-textMuted border border-border border-dashed rounded-xl">No active signals found.</div>'; document.getElementById('error-container').classList.add('hidden'); } catch (err) { document.getElementById('error-message').textContent = err.message; document.getElementById('error-container').classList.remove('hidden'); document.getElementById('live-dot').classList.replace('bg-success', 'bg-danger'); } }; fetchAPI(); setInterval(fetchAPI, 10000); </script> </body> </html>