← Назад/* 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'
});
}