← Back
/* 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 || {};

    // Bankroll = exchange cash (available for new trades)
    const cash = risk.bankroll || 0;
    const deployed = risk.total_deployed || 0;
    const total = risk.total_capital || (cash + deployed);
    setText('bankroll', data.dry_run && total >= 1000 ? 'Virtual' : `$${cash.toFixed(2)}`);

    // 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', `$${deployed.toFixed(0)} / $${total.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)}%` : '—');
    const totalTrades = risk.overall_total_trades != null ? risk.overall_total_trades : (risk.daily_trades || 0);
    setText('trades-today', `${totalTrades} (${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 limit = parseInt(document.getElementById('history-limit').value) || 0;
    const display = limit > 0 ? filtered.slice(0, limit) : filtered;
    const tbody = document.getElementById('history-body');
    const info = document.getElementById('history-info');
    info.textContent = limit > 0 && filtered.length > limit
        ? `${display.length} of ${filtered.length} trades`
        : `${filtered.length} trade${filtered.length !== 1 ? 's' : ''}`;

    if (!display.length) {
        tbody.innerHTML = `<tr><td colspan="10" class="empty">No trades found</td></tr>`;
        return;
    }

    tbody.innerHTML = display.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 hasTz = isoStr.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(isoStr);
        const d = new Date(hasTz ? 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'
    });
}

📜 Git History

8fca132chore: initial commit — version control setup5 weeks ago
Show last diff
Loading...