← Назад
/* Weather Bot Dashboard v2 — Full rewrite */ const API = '/weather-bot'; let currentTab = 'history'; let allTrades = []; let currentFilter = 'all'; // ============================================================ // INIT // ============================================================ document.addEventListener('DOMContentLoaded', () => { fetchAll(); setInterval(fetchAll, 30000); updateFooterTime(); setInterval(updateFooterTime, 10000); }); function fetchAll() { fetchStatus(); fetchTrades(); if (currentTab === 'markets') fetchMarkets(); if (currentTab === 'performance') fetchPerformance(); } // ============================================================ // API CALLS // ============================================================ async function fetchStatus() { try { const res = await fetch(`${API}/api/status`); const json = await res.json(); if (json.success) updateStatus(json.data); } catch { setOffline(); } } async function fetchTrades() { try { const [allRes, activeRes] = await Promise.all([ fetch(`${API}/api/trades?limit=200`), fetch(`${API}/api/trades/active`), ]); const allJson = await allRes.json(); const activeJson = await activeRes.json(); if (allJson.success) { allTrades = allJson.data || []; renderHistory(allTrades); } if (activeJson.success) { renderOpenPositions(activeJson.data || []); } } catch (e) { console.error('fetchTrades:', e); } } async function fetchMarkets() { try { const res = await fetch(`${API}/api/markets`); const json = await res.json(); if (json.success) renderMarkets(json.data); } catch (e) { console.error('fetchMarkets:', e); } } async function fetchPerformance() { try { const [statsRes, calRes] = await Promise.all([ fetch(`${API}/api/stats/resolution`), fetch(`${API}/api/calibration`), ]); const stats = await statsRes.json(); const cal = await calRes.json(); if (stats.success) renderPerformance(stats.data); if (cal.success) renderCalibration(cal.data); } catch (e) { console.error('fetchPerformance:', e); } } async function triggerScan() { const btn = document.getElementById('scan-btn'); btn.classList.add('loading'); btn.disabled = true; try { await fetch(`${API}/api/scan`, { method: 'POST' }); setTimeout(() => { fetchAll(); btn.classList.remove('loading'); btn.disabled = false; }, 4000); } catch { btn.classList.remove('loading'); btn.disabled = false; } } async function triggerResolve() { try { await fetch(`${API}/api/resolve`, { method: 'POST' }); setTimeout(fetchAll, 5000); } catch (e) { console.error('triggerResolve:', e); } } async function toggleKillSwitch() { const bar = document.getElementById('kill-switch-bar'); const isActive = bar.style.display !== 'none'; const endpoint = isActive ? '/api/kill-switch/deactivate' : '/api/kill-switch/activate'; await fetch(`${API}${endpoint}`, { method: 'POST' }); fetchStatus(); } // ============================================================ // RENDER: STATUS // ============================================================ function updateStatus(data) { // Status indicator const dot = document.getElementById('status-dot'); const text = document.getElementById('status-text'); dot.className = 'status-dot ' + (data.bot_running ? 'online' : 'offline'); text.textContent = data.bot_running ? 'Running' : 'Stopped'; // Mode badge const modeBadge = document.getElementById('mode-badge'); modeBadge.textContent = data.dry_run ? 'DRY RUN' : 'LIVE'; modeBadge.className = 'badge' + (data.dry_run ? '' : ' live'); // KPI values const risk = data.risk || {}; // Show real bankroll or "Virtual" for DRY_RUN with inflated bankroll const bankroll = risk.bankroll || 0; setText('bankroll', bankroll >= 1000 && data.dry_run ? 'Virtual' : `$${bankroll}`); // Use overall P&L (all-time) instead of daily const pnl = risk.overall_pnl != null ? risk.overall_pnl : (risk.daily_pnl || 0); const pnlEl = document.getElementById('daily-pnl'); pnlEl.textContent = `${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)}`; pnlEl.className = `kpi-value ${pnl > 0 ? 'positive' : pnl < 0 ? 'negative' : ''}`; setText('open-positions', risk.open_positions || 0); setText('deployed', `$${(risk.total_deployed || 0).toFixed(0)}`); // Use overall win rate (all resolved trades) instead of daily const wr = risk.overall_win_rate != null ? risk.overall_win_rate : (risk.daily_win_rate || 0); const resolved = risk.overall_resolved || 0; setText('win-rate', resolved > 0 ? `${wr.toFixed(0)}%` : '—'); setText('trades-today', `${risk.daily_trades || 0} (${resolved} resolved)`); // Last scan const scan = data.last_scan || {}; setText('markets-found', scan.markets || 0); setText('last-scan', scan.at ? timeAgo(scan.at) : 'Never'); // Kill switch const ksBar = document.getElementById('kill-switch-bar'); const ksBtn = document.getElementById('kill-btn'); if (risk.kill_switch) { ksBar.style.display = 'flex'; setText('kill-reason', risk.kill_switch_reason || ''); if (ksBtn) { ksBtn.textContent = 'Deactivate'; ksBtn.className = 'btn btn-sm btn-danger'; } } else { ksBar.style.display = 'none'; if (ksBtn) { ksBtn.textContent = 'Activate'; ksBtn.className = 'btn btn-sm'; } } // Settings tab const ctrlMode = document.getElementById('ctrl-mode'); if (ctrlMode) { ctrlMode.textContent = data.dry_run ? 'DRY RUN' : 'LIVE'; ctrlMode.className = data.dry_run ? 'badge' : 'badge live'; } setTextSafe('ctrl-max-loss', `$${risk.max_daily_loss || 20}`); setTextSafe('ctrl-max-pos', risk.max_positions || 20); } // ============================================================ // RENDER: OPEN POSITIONS // ============================================================ function renderOpenPositions(trades) { const container = document.getElementById('open-trades-container'); const countEl = document.getElementById('open-count'); countEl.textContent = trades.length; if (!trades.length) { container.innerHTML = ` <div class="empty-state"> <div class="empty-icon">📭</div> <div>No open positions</div> <div class="empty-sub">Bot will open trades when edge is found</div> </div>`; return; } const html = `<div class="open-grid">${trades.map(t => { const bracket = extractBracket(t.question); const sideClass = (t.side || '').toLowerCase() === 'yes' ? 'side-yes' : 'side-no'; const edgePct = ((t.edge || 0) * 100).toFixed(1); const modelPct = ((t.model_prob || 0) * 100).toFixed(0); const mktPct = ((t.market_prob || 0) * 100).toFixed(0); const statusBadge = getStatusBadge(t.status); const createdAt = formatDateTime(t.created_at); return ` <div class="open-card ${sideClass}"> <div class="open-card-top"> <div> <div class="open-city">${t.city || '?'}</div> <div class="open-bracket">${bracket || t.question || ''}</div> </div> <div>${statusBadge}</div> </div> <div class="open-card-meta"> <div class="open-meta-item"> <span class="open-meta-label">Side</span> <span class="open-meta-value"><span class="badge ${(t.side||'').toLowerCase()}">${t.side || '?'}</span></span> </div> <div class="open-meta-item"> <span class="open-meta-label">Price</span> <span class="open-meta-value">$${(t.price || 0).toFixed(2)}</span> </div> <div class="open-meta-item"> <span class="open-meta-label">Size</span> <span class="open-meta-value">$${(t.size || 0).toFixed(2)}</span> </div> <div class="open-meta-item"> <span class="open-meta-label">Edge</span> <span class="open-meta-value ${edgeClass(t.edge)}">${edgePct}%</span> </div> </div> <div style="margin-top:8px; font-size:11px; color:var(--text-dim)"> Model <span class="prob-model">${modelPct}%</span> <span class="prob-arrow">→</span> Market <span class="prob-market">${mktPct}%</span> <span style="float:right">${createdAt}</span> </div> </div>`; }).join('')}</div>`; container.innerHTML = html; } // ============================================================ // RENDER: TRADE HISTORY // ============================================================ function renderHistory(trades) { const filtered = filterTrades(trades, currentFilter); const tbody = document.getElementById('history-body'); const info = document.getElementById('history-info'); info.textContent = `${filtered.length} trade${filtered.length !== 1 ? 's' : ''}`; if (!filtered.length) { tbody.innerHTML = `<tr><td colspan="10" class="empty">No trades found</td></tr>`; return; } tbody.innerHTML = filtered.map(t => { const date = formatDate(t.created_at); const time = formatTime(t.created_at); const city = t.city || '?'; const bracket = extractBracket(t.question); const side = t.side || '?'; const sideClass = side.toLowerCase(); const price = (t.price || 0).toFixed(2); const size = `$${(t.size || 0).toFixed(2)}`; const edgePct = ((t.edge || 0) * 100).toFixed(1); const eClass = edgeClass(t.edge); const modelPct = ((t.model_prob || 0) * 100).toFixed(0); const mktPct = ((t.market_prob || 0) * 100).toFixed(0); const statusBadge = getStatusBadge(t.status, t.outcome); const pnlStr = formatPnl(t.pnl); const pnlClass = t.pnl > 0 ? 'pnl-positive' : t.pnl < 0 ? 'pnl-negative' : ''; return `<tr> <td><div>${date}</div><div style="font-size:11px;color:var(--text-dim)">${time}</div></td> <td class="cell-city">${city}</td> <td class="cell-bracket">${bracket}</td> <td><span class="badge ${sideClass}">${side}</span></td> <td>$${price}</td> <td>${size}</td> <td class="${eClass}">${edgePct}%</td> <td class="cell-prob"><span class="prob-model">${modelPct}%</span><span class="prob-arrow">→</span><span class="prob-market">${mktPct}%</span></td> <td>${statusBadge}</td> <td class="${pnlClass}">${pnlStr}</td> </tr>`; }).join(''); } function filterTrades(trades, filter) { if (filter === 'all') return trades; if (filter === 'won') return trades.filter(t => t.outcome === 'win'); if (filter === 'lost') return trades.filter(t => t.outcome === 'loss'); if (filter === 'pending') return trades.filter(t => !t.outcome && ['pending','filled','unverified','simulated'].includes(t.status)); return trades; } function filterHistory(filter, el) { currentFilter = filter; document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); el.classList.add('active'); renderHistory(allTrades); } // ============================================================ // RENDER: MARKETS // ============================================================ function renderMarkets(markets) { const tbody = document.getElementById('markets-body'); const info = document.getElementById('markets-info'); info.textContent = `${(markets||[]).length} active markets`; if (!markets || !markets.length) { tbody.innerHTML = '<tr><td colspan="6" class="empty">No markets found</td></tr>'; return; } tbody.innerHTML = markets.map(m => { const city = m.city || '?'; const date = m.date || '?'; const q = (m.question || '').substring(0, 70); const threshold = m.threshold != null ? `${m.threshold}°${m.threshold_unit || 'F'}` : '?'; const yp = parseFloat(m.yes_price); const yesPrice = !isNaN(yp) ? `$${yp.toFixed(2)}` : '?'; const vol = parseFloat(m.volume); const volume = !isNaN(vol) && vol > 0 ? `$${(vol/1000).toFixed(1)}K` : '$0'; const priceClass = yp <= 0.2 ? 'edge-strong' : yp <= 0.5 ? 'edge-medium' : yp >= 0.8 ? 'edge-weak' : ''; return `<tr> <td class="cell-city">${city}</td> <td>${date}</td> <td title="${m.question || ''}">${q}</td> <td>${threshold}</td> <td class="${priceClass}">${yesPrice}</td> <td>${volume}</td> </tr>`; }).join(''); } // ============================================================ // RENDER: PERFORMANCE // ============================================================ function renderPerformance(stats) { setText('perf-total', stats.total || 0); const wrEl = document.getElementById('perf-winrate'); const wr = stats.win_rate || 0; wrEl.textContent = `${wr}%`; wrEl.className = `perf-card-value ${wr >= 50 ? 'positive' : wr > 0 ? 'negative' : ''}`; const pnlEl = document.getElementById('perf-pnl'); const pnl = stats.pnl || 0; pnlEl.textContent = `${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)}`; pnlEl.className = `perf-card-value ${pnl >= 0 ? 'positive' : 'negative'}`; setText('perf-pending', stats.unresolved || 0); // By Tier const tierBody = document.getElementById('perf-tier-body'); if (stats.by_tier && stats.by_tier.length) { tierBody.innerHTML = stats.by_tier.map(t => { const wrClass = t.win_rate >= 60 ? 'pnl-positive' : t.win_rate < 40 ? 'pnl-negative' : ''; const pnlClass = t.pnl >= 0 ? 'pnl-positive' : 'pnl-negative'; return `<tr> <td><strong>${t.tier}</strong></td> <td>${t.total} (W:${t.wins} L:${t.total - t.wins})</td> <td class="${wrClass}">${t.win_rate}%</td> <td>${t.avg_edge}%</td> <td class="${pnlClass}">${t.pnl >= 0?'+':''}$${t.pnl.toFixed(2)}</td> </tr>`; }).join(''); } else { tierBody.innerHTML = '<tr><td colspan="5" class="empty">No resolved trades yet</td></tr>'; } // By City const cityBody = document.getElementById('perf-city-body'); if (stats.by_city && stats.by_city.length) { cityBody.innerHTML = stats.by_city.map(c => { const wrClass = c.win_rate >= 60 ? 'pnl-positive' : c.win_rate < 40 ? 'pnl-negative' : ''; const pnlClass = c.pnl >= 0 ? 'pnl-positive' : 'pnl-negative'; return `<tr> <td class="cell-city">${c.city}</td> <td>${c.total} (W:${c.wins} L:${c.total - c.wins})</td> <td class="${wrClass}">${c.win_rate}%</td> <td class="${pnlClass}">${c.pnl >= 0?'+':''}$${c.pnl.toFixed(2)}</td> </tr>`; }).join(''); } else { cityBody.innerHTML = '<tr><td colspan="4" class="empty">No data yet</td></tr>'; } } function renderCalibration(data) { const tbody = document.getElementById('perf-cal-body'); if (!data || !data.length) { tbody.innerHTML = '<tr><td colspan="5" class="empty">Not enough calibration data (need 3+ samples per city)</td></tr>'; return; } tbody.innerHTML = data.map(c => { const brierClass = c.brier_score < 0.15 ? 'pnl-positive' : c.brier_score > 0.3 ? 'pnl-negative' : ''; const confClass = c.confidence_mult >= 1.0 ? 'pnl-positive' : c.confidence_mult <= 0.6 ? 'pnl-negative' : ''; return `<tr> <td class="cell-city">${c.city}</td> <td class="${brierClass}">${c.brier_score.toFixed(3)}</td> <td>${c.mae.toFixed(1)}°F</td> <td>${c.sample_count}</td> <td class="${confClass}">${c.confidence_mult.toFixed(1)}x</td> </tr>`; }).join(''); } // ============================================================ // TABS // ============================================================ function switchTab(tab, el) { currentTab = tab; document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(t => t.classList.add('hidden')); if (el) el.classList.add('active'); document.getElementById(`tab-${tab}`).classList.remove('hidden'); if (tab === 'markets') fetchMarkets(); if (tab === 'performance') fetchPerformance(); } // ============================================================ // HELPERS // ============================================================ function setText(id, val) { const el = document.getElementById(id); if (el) el.textContent = val; } function setTextSafe(id, val) { const el = document.getElementById(id); if (el) el.textContent = val; } function setOffline() { document.getElementById('status-dot').className = 'status-dot offline'; document.getElementById('status-text').textContent = 'Offline'; } function getStatusBadge(status, outcome) { if (outcome === 'win') return '<span class="badge win">WIN</span>'; if (outcome === 'loss') return '<span class="badge loss">LOSS</span>'; const s = (status || 'pending').toLowerCase(); const cls = {pending:'pending', filled:'filled', simulated:'simulated', unverified:'unverified'}[s] || 'pending'; const label = s.charAt(0).toUpperCase() + s.slice(1); return `<span class="badge ${cls}">${label}</span>`; } function edgeClass(edge) { const e = edge || 0; if (e >= 0.20) return 'edge-strong'; if (e >= 0.10) return 'edge-medium'; return 'edge-weak'; } function formatPnl(pnl) { if (!pnl && pnl !== 0) return '—'; if (pnl === 0) return '$0.00'; return `${pnl > 0 ? '+' : ''}$${pnl.toFixed(2)}`; } function extractBracket(question) { if (!question) return ''; let m = question.match(/be\s+between\s+(\d+)[–-](\d+)\s*°([FC])/i); if (m) return `${m[1]}–${m[2]}°${m[3]}`; m = question.match(/be\s+(\d+)\s*°([FC])\s+or\s+(?:higher|above|more)/i); if (m) return `≥${m[1]}°${m[2]}`; m = question.match(/(\d+)\s*°([FC])\s+or\s+(?:below|lower)/i); if (m) return `≤${m[1]}°${m[2]}`; m = question.match(/be\s+(\d+)\s*°([FC])\s+on/i); if (m) return `=${m[1]}°${m[2]}`; return ''; } function formatDate(isoStr) { if (!isoStr) return '?'; try { const d = new Date(isoStr + 'Z'); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } catch { return '?'; } } function formatTime(isoStr) { if (!isoStr) return ''; try { const d = new Date(isoStr + 'Z'); return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); } catch { return ''; } } function formatDateTime(isoStr) { if (!isoStr) return '?'; try { const d = new Date(isoStr + 'Z'); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); } catch { return '?'; } } function timeAgo(isoStr) { if (!isoStr) return 'Never'; try { const d = new Date(isoStr.endsWith('Z') ? isoStr : isoStr + 'Z'); const sec = Math.floor((Date.now() - d.getTime()) / 1000); if (sec < 0) return 'just now'; if (sec < 60) return `${sec}s ago`; if (sec < 3600) return `${Math.floor(sec / 60)}m ago`; if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`; return `${Math.floor(sec / 86400)}d ago`; } catch { return '?'; } } function updateFooterTime() { const el = document.getElementById('footer-time'); if (el) el.textContent = new Date().toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, month: 'short', day: 'numeric' }); }