Trading-Bot-M20 / dashboard.html
raghava4u's picture
Upload folder using huggingface_hub
d53dc44 verified
Raw History Blame Contribute Delete
42.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trading System — Live Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4/build/global/luxon.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1/dist/chartjs-adapter-luxon.umd.min.js"></script>
<style>
/* ── Reset & Base ─────────────────────────────────────────────────────── */
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#e6edf3;
--dim:#8b949e;--green:#3fb950;--red:#f85149;--yellow:#d29922;
--blue:#58a6ff;--purple:#bc8cff;--cyan:#39d2c0;--orange:#f0883e;
--accent:#1f6feb;
}
html{font-size:14px}
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,-apple-system,sans-serif;overflow-x:hidden}
/* ── Layout ───────────────────────────────────────────────────────────── */
.header{background:linear-gradient(135deg,#161b22 0%,#1c2432 100%);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;backdrop-filter:blur(8px)}
.header h1{font-size:1.25rem;font-weight:600;letter-spacing:-.02em}
.header h1 span{color:var(--blue)}
.header-right{display:flex;align-items:center;gap:16px;font-size:.85rem}
.mode-badge{padding:4px 12px;border-radius:4px;font-weight:700;font-size:.75rem;text-transform:uppercase;letter-spacing:.05em}
.mode-dry{background:#b62324;color:#fff}
.mode-paper{background:#9e6a00;color:#fff}
.mode-live{background:#238636;color:#fff}
.safe-badge{background:#f85149;color:#fff;animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
.clock{color:var(--dim);font-variant-numeric:tabular-nums}
.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px}
.dot-green{background:var(--green)}
.dot-red{background:var(--red)}
.dot-yellow{background:var(--yellow)}
/* ── Banner ───────────────────────────────────────────────────────────── */
.banner{padding:10px 24px;font-size:.85rem;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
.banner-error{background:#b6232422;color:var(--red)}
.banner-info{background:#1f6feb22;color:var(--blue)}
.banner .dismiss{margin-left:auto;cursor:pointer;opacity:.6;font-size:1.1rem;border:none;background:transparent;color:inherit}
.banner .dismiss:hover{opacity:1}
.grid{display:grid;gap:16px;padding:16px 24px;grid-template-columns:1fr 1fr 1fr 1fr;grid-template-rows:auto auto auto auto auto}
@media(max-width:1400px){.grid{grid-template-columns:1fr 1fr}}
@media(max-width:800px){.grid{grid-template-columns:1fr}}
/* ── Cards ────────────────────────────────────────────────────────────── */
.card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:16px;position:relative;overflow:hidden}
.card-title{font-size:.8rem;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin-bottom:12px;display:flex;align-items:center;gap:6px;position:sticky;top:0;background:var(--card);z-index:2;padding-bottom:4px}
.card-title .icon{font-size:1rem}
/* Scrollable card content */
.card-scroll{max-height:380px;overflow-y:auto}
.card-scroll::-webkit-scrollbar{width:5px}
.card-scroll::-webkit-scrollbar-track{background:transparent}
.card-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
.card-scroll::-webkit-scrollbar-thumb:hover{background:var(--dim)}
/* P&L Card */
.pnl-card{grid-column:1/2}
.pnl-value{font-size:2.4rem;font-weight:700;font-variant-numeric:tabular-nums;line-height:1.1}
.pnl-value.positive{color:var(--green)}
.pnl-value.negative{color:var(--red)}
.pnl-value.zero{color:var(--dim)}
.pnl-sub{display:flex;gap:24px;margin-top:8px;font-size:.85rem;color:var(--dim)}
.pnl-sub span strong{color:var(--text)}
.progress-bar{margin-top:12px;height:8px;background:#21262d;border-radius:4px;overflow:hidden;position:relative}
.progress-fill{height:100%;border-radius:4px;transition:width .6s ease}
.progress-label{display:flex;justify-content:space-between;font-size:.75rem;color:var(--dim);margin-top:4px}
/* Risk Card */
.risk-card{grid-column:2/3}
.risk-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.risk-item{padding:8px;border-radius:6px;background:#21262d}
.risk-item .label{font-size:.7rem;text-transform:uppercase;color:var(--dim);letter-spacing:.05em}
.risk-item .value{font-size:1.1rem;font-weight:600;margin-top:2px}
.circuit-list{margin-top:10px;display:flex;flex-wrap:wrap;gap:6px}
.circuit-chip{padding:2px 8px;border-radius:12px;font-size:.7rem;font-weight:600;background:#f8514933;color:var(--red);border:1px solid #f8514966}
.circuit-chip.ok{background:#3fb95022;color:var(--green);border-color:#3fb95044}
/* Sentiment Card */
.sentiment-card{grid-column:3/4}
.sent-bars{display:flex;flex-direction:column;gap:6px;margin-top:4px}
.sent-row{display:flex;align-items:center;gap:8px}
.sent-sym{width:44px;font-weight:600;font-size:.85rem}
.sent-bar-container{flex:1;height:14px;background:#21262d;border-radius:3px;position:relative;overflow:hidden}
.sent-bar-fill{height:100%;border-radius:3px;transition:width .4s ease}
.sent-val{width:42px;text-align:right;font-size:.8rem;font-variant-numeric:tabular-nums}
/* Config Card */
.config-card{grid-column:4/5}
.config-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 12px;font-size:.82rem}
.config-grid dt{color:var(--dim)}
.config-grid dd{font-weight:600;text-align:right}
/* ── Model & Metrics Card ─────────────────────────────────────────────── */
.model-card{grid-column:1/3}
.model-section{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.model-info-list{display:flex;flex-direction:column;gap:6px}
.model-row{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #21262d;font-size:.82rem}
.model-row:last-child{border:none}
.model-row .mlabel{color:var(--dim)}
.model-row .mval{font-weight:600;text-align:right}
.model-row .mval.good{color:var(--green)}
.model-row .mval.warn{color:var(--yellow)}
.model-row .mval.bad{color:var(--red)}
/* Indicators table */
.ind-table{width:100%;font-size:.82rem;border-collapse:collapse}
.ind-table th{text-align:left;padding:4px 8px;color:var(--dim);font-weight:600;font-size:.72rem;text-transform:uppercase;border-bottom:1px solid var(--border)}
.ind-table td{padding:4px 8px;border-bottom:1px solid #21262d}
.ind-weight{display:inline-block;width:40px;height:6px;background:#21262d;border-radius:3px;overflow:hidden;vertical-align:middle;margin-left:6px}
.ind-weight-fill{height:100%;border-radius:3px;background:var(--blue)}
.ind-type-tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:.7rem;font-weight:600;background:#21262d;color:var(--dim)}
.ind-type-tag.momentum{color:var(--orange);background:#f0883e22}
.ind-type-tag.trend{color:var(--blue);background:#58a6ff22}
.ind-type-tag.volatility{color:var(--purple);background:#bc8cff22}
.ind-type-tag.volume-price{color:var(--cyan);background:#39d2c022}
/* ── Data Status Card ─────────────────────────────────────────────────── */
.data-card{grid-column:3/5}
.data-table{width:100%;font-size:.82rem;border-collapse:collapse}
.data-table th{text-align:left;padding:6px 8px;color:var(--dim);font-weight:600;font-size:.72rem;text-transform:uppercase;border-bottom:1px solid var(--border)}
.data-table td{padding:6px 8px;border-bottom:1px solid #21262d}
.bar-count{font-weight:700;font-variant-numeric:tabular-nums;color:var(--text)}
.bar-count.zero{color:var(--red)}
.bar-count.low{color:var(--yellow)}
.bar-count.good{color:var(--green)}
.last-update{font-size:.75rem;color:var(--dim)}
.total-bars-badge{display:inline-block;background:var(--accent);color:#fff;padding:3px 10px;border-radius:4px;font-weight:700;font-size:.85rem;margin-left:8px}
/* ── Metrics Card ─────────────────────────────────────────────────────── */
.metrics-card{grid-column:1/-1}
.metrics-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(140px, 1fr));gap:10px}
.metric-box{background:#21262d;border-radius:6px;padding:10px 12px;text-align:center}
.metric-box .metric-val{font-size:1.3rem;font-weight:700;font-variant-numeric:tabular-nums;margin-bottom:2px}
.metric-box .metric-label{font-size:.7rem;text-transform:uppercase;color:var(--dim);letter-spacing:.04em}
.metric-box .metric-val.positive{color:var(--green)}
.metric-box .metric-val.negative{color:var(--red)}
.metric-box .metric-val.neutral{color:var(--blue)}
.no-metrics{color:var(--dim);font-style:italic;text-align:center;padding:20px;font-size:.9rem}
/* Predictions Table */
.predictions-card{grid-column:1/-1}
table{width:100%;border-collapse:collapse;font-size:.85rem;font-variant-numeric:tabular-nums}
th{text-align:left;padding:8px 10px;border-bottom:2px solid var(--border);color:var(--dim);font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.05em}
td{padding:8px 10px;border-bottom:1px solid #21262d}
tr:hover td{background:#1c2432}
.dir-up{color:var(--green);font-weight:600}
.dir-down{color:var(--red);font-weight:600}
.dir-neutral{color:var(--dim)}
.decision-buy{color:#fff;background:var(--green);padding:2px 8px;border-radius:3px;font-weight:700;font-size:.75rem}
.decision-sell{color:#fff;background:var(--red);padding:2px 8px;border-radius:3px;font-weight:700;font-size:.75rem}
.decision-hold{color:var(--dim);font-size:.8rem}
.confidence-bar{width:60px;height:6px;background:#21262d;border-radius:3px;display:inline-block;vertical-align:middle;overflow:hidden}
.confidence-fill{height:100%;border-radius:3px}
/* Positions Table */
.positions-card{grid-column:1/3}
.pnl-cell.positive{color:var(--green)}
.pnl-cell.negative{color:var(--red)}
/* Charts */
.charts-card{grid-column:3/-1}
.chart-tabs{display:flex;gap:4px;margin-bottom:8px;flex-wrap:wrap}
.chart-tab{padding:4px 12px;border-radius:4px;font-size:.78rem;cursor:pointer;border:1px solid var(--border);background:transparent;color:var(--dim);transition:all .15s}
.chart-tab:hover{border-color:var(--blue);color:var(--blue)}
.chart-tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}
.chart-wrapper{position:relative;height:260px}
/* Trade Log */
.log-card{grid-column:1/-1}
.log-list{max-height:200px;overflow-y:auto;font-size:.8rem;font-family:'Cascadia Code','Fira Code',monospace}
.log-list::-webkit-scrollbar{width:6px}
.log-list::-webkit-scrollbar-track{background:transparent}
.log-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
.log-entry{padding:4px 0;border-bottom:1px solid #21262d;display:flex;gap:12px}
.log-time{color:var(--dim);white-space:nowrap}
.log-msg{flex:1}
.log-buy{color:var(--green)}
.log-sell{color:var(--red)}
.log-info{color:var(--blue)}
.log-warn{color:var(--yellow)}
/* Connection status */
.conn-ok{color:var(--green)}
.conn-err{color:var(--red)}
/* Empty table state */
.empty-row td{text-align:center;color:var(--dim);padding:24px;font-style:italic}
</style>
</head>
<body>
<!-- ═══════════════════════════════ HEADER ═══════════════════════════════ -->
<div class="header">
<h1>📊 <span>Trading System</span> Dashboard</h1>
<div class="header-right">
<span id="modeBadge" class="mode-badge mode-dry">DRY RUN</span>
<span id="safeBadge" class="safe-badge" style="display:none">⚠ SAFE MODE</span>
<span id="marketStatus"><span class="status-dot dot-yellow"></span>Loading…</span>
<span id="clock" class="clock">--:--:-- ET</span>
<span id="connStatus" class="conn-err" title="Connecting…">●</span>
</div>
</div>
<!-- ═══════════════════════════════ BANNERS ══════════════════════════════ -->
<div id="bannerArea"></div>
<!-- ═══════════════════════════════ GRID ═════════════════════════════════ -->
<div class="grid">
<!-- ─── P&L Card ──────────────────────────────────────────────────── -->
<div class="card pnl-card">
<div class="card-title"><span class="icon">💰</span> Daily P&L</div>
<div id="pnlValue" class="pnl-value zero">$0.00</div>
<div class="pnl-sub">
<span>Realized: <strong id="pnlRealized">$0.00</strong></span>
<span>Unrealized: <strong id="pnlUnrealized">$0.00</strong></span>
</div>
<div class="pnl-sub" style="margin-top:6px;border-top:1px solid var(--border);padding-top:6px">
<span>Equity: <strong id="acctEquity" style="color:var(--green)">—</strong></span>
<span>Buying Power: <strong id="acctBuyingPower" style="color:var(--blue)">—</strong></span>
<span>Cash: <strong id="acctCash">—</strong></span>
</div>
<div class="progress-bar"><div id="pnlProgress" class="progress-fill" style="width:0%;background:var(--green)"></div></div>
<div class="progress-label"><span id="pnlPct">0%</span><span id="pnlTarget">Target: $20.00</span></div>
</div>
<!-- ─── Risk Card ─────────────────────────────────────────────────── -->
<div class="card risk-card">
<div class="card-title"><span class="icon">🛡️</span> Risk Status</div>
<div class="risk-grid">
<div class="risk-item"><div class="label">PDT Trades</div><div class="value" id="pdtCount">—</div></div>
<div class="risk-item"><div class="label">Open Positions</div><div class="value" id="posCount">0</div></div>
<div class="risk-item"><div class="label">Loss Streak</div><div class="value" id="lossStreak">0</div></div>
<div class="risk-item"><div class="label">Sentiment</div><div class="value" id="sentStatus">—</div></div>
</div>
<div class="circuit-list" id="circuitList"><span class="circuit-chip ok">All Clear</span></div>
</div>
<!-- ─── Sentiment Card ────────────────────────────────────────────── -->
<div class="card sentiment-card">
<div class="card-title"><span class="icon">🧠</span> Sentiment Scores</div>
<div class="card-scroll"><div class="sent-bars" id="sentBars"><!-- filled by JS --></div></div>
</div>
<!-- ─── Config Card ───────────────────────────────────────────────── -->
<div class="card config-card">
<div class="card-title"><span class="icon">⚙️</span> Configuration</div>
<div class="card-scroll"><dl class="config-grid" id="configGrid"><!-- filled by JS --></dl></div>
</div>
<!-- ─── Model & System Card ───────────────────────────────────────── -->
<div class="card model-card">
<div class="card-title"><span class="icon">🤖</span> Model & Indicators</div>
<div class="card-scroll">
<div class="model-section">
<div>
<div class="model-info-list" id="modelInfo">
<div class="model-row"><span class="mlabel">Loading model info…</span></div>
</div>
</div>
<div>
<table class="ind-table" id="indTable">
<thead><tr><th>Indicator</th><th>Weight</th><th>Type</th></tr></thead>
<tbody id="indBody"><tr><td colspan="3" style="color:var(--dim);text-align:center">Loading…</td></tr></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ─── Data Status Card ──────────────────────────────────────────── -->
<div class="card data-card">
<div class="card-title"><span class="icon">💾</span> Data Downloaded <span id="totalBarsBadge" class="total-bars-badge" style="display:none">0 bars</span></div>
<div class="card-scroll">
<table class="data-table">
<thead><tr><th>Symbol</th><th>Daily Bars</th><th>Last Daily</th><th>Hourly Bars</th><th>Last Hourly</th><th>5min Bars</th><th>Last 5min</th></tr></thead>
<tbody id="dataBody"><tr><td colspan="7" style="color:var(--dim);text-align:center">Loading…</td></tr></tbody>
</table>
</div>
</div>
<!-- ─── Performance Metrics Card ──────────────────────────────────── -->
<div class="card metrics-card">
<div class="card-title"><span class="icon">📊</span> Performance Metrics (Backtest)</div>
<div id="metricsGrid" class="metrics-grid">
<div class="no-metrics">Run a backtest to see metrics — <code>python -m backtester.engine</code></div>
</div>
</div>
<!-- ─── Predictions Table ─────────────────────────────────────────── -->
<div class="card predictions-card">
<div class="card-title"><span class="icon">🔮</span> Predictions & Signals</div>
<div class="card-scroll">
<table>
<thead>
<tr>
<th>Symbol</th><th>Price</th><th>Direction</th><th>Signal</th>
<th>Score</th><th>Tech</th><th>Sent</th>
<th>Entry</th><th>Stop</th><th>Target</th><th>Est P&L</th><th>Confidence</th>
</tr>
</thead>
<tbody id="predBody">
<tr class="empty-row"><td colspan="12">Market closed — predictions generated during trading hours (9:30 AM – 4:00 PM ET)</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ─── Positions Table ───────────────────────────────────────────── -->
<div class="card positions-card">
<div class="card-title"><span class="icon">📋</span> Open Positions</div>
<div class="card-scroll">
<table>
<thead>
<tr><th>Symbol</th><th>Side</th><th>Qty</th><th>Entry</th><th>Current</th><th>P&L</th><th>Value</th></tr>
</thead>
<tbody id="posBody">
<tr class="empty-row"><td colspan="7">No open positions</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ─── Chart ─────────────────────────────────────────────────────── -->
<div class="card charts-card">
<div class="card-title"><span class="icon">📈</span> Price Chart</div>
<div class="chart-tabs" id="chartTabs"><!-- filled by JS --></div>
<div class="chart-wrapper"><canvas id="priceChart"></canvas></div>
</div>
<!-- ─── Trade Log ─────────────────────────────────────────────────── -->
<div class="card log-card">
<div class="card-title"><span class="icon">📜</span> Trade Activity Log</div>
<div class="log-list" id="tradeLog"><div class="log-entry"><span class="log-time">—</span><span class="log-msg log-info">Waiting for events…</span></div></div>
</div>
</div><!-- /grid -->
<!-- ══════════════════════════════ SCRIPT ════════════════════════════════ -->
<script>
// ── API base URL: auto-detect file:// vs served ─────────────────────
const API = (function(){
if(location.protocol === 'file:' || location.port === '') return 'http://localhost:5000';
return location.origin;
})();
const $ = s => document.querySelector(s);
const $$ = s => document.querySelectorAll(s);
let universe = [];
let selectedChart = '';
let priceChart = null;
let configLoaded = false;
let systemLoaded = false;
let target = 20;
let apiConnected = false;
// ── Helpers ──────────────────────────────────────────────────────────
function fmt(v, d=2){ return v != null ? Number(v).toFixed(d) : '—'; }
function fmtUSD(v){ return v != null ? '$'+Number(v).toFixed(2) : '—'; }
function pnlClass(v){ return v > 0 ? 'positive' : v < 0 ? 'negative' : 'zero'; }
function dirClass(d){ return d==='UP'?'dir-up':d==='DOWN'?'dir-down':'dir-neutral'; }
function decClass(d){ return d==='BUY'?'decision-buy':d==='SELL'?'decision-sell':'decision-hold'; }
function showBanner(id, type, msg){
const area = $('#bannerArea');
if(document.getElementById('banner-'+id)) return;
const div = document.createElement('div');
div.id = 'banner-'+id;
div.className = 'banner banner-'+type;
div.innerHTML = `<span>${msg}</span><button class="dismiss" onclick="this.parentElement.remove()">✕</button>`;
area.appendChild(div);
}
function removeBanner(id){
const el = document.getElementById('banner-'+id);
if(el) el.remove();
}
function fmtTimestamp(iso){
if(!iso) return '—';
const d = new Date(iso);
const now = new Date();
const diffH = (now - d) / 3600000;
const dateStr = d.toLocaleDateString('en-US',{month:'short',day:'numeric'});
const timeStr = d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:false});
let age = '';
if(diffH < 1) age = Math.round(diffH*60)+'m ago';
else if(diffH < 24) age = Math.round(diffH)+'h ago';
else age = Math.round(diffH/24)+'d ago';
return `${dateStr} ${timeStr} <span style="color:var(--dim);font-size:.7rem">(${age})</span>`;
}
function barCountClass(n){
if(n===0) return 'zero';
if(n<50) return 'low';
return 'good';
}
// ── API Fetch ────────────────────────────────────────────────────────
async function api(path){
try{
const r = await fetch(API+path, {signal: AbortSignal.timeout(5000)});
if(!r.ok) throw new Error(r.status);
if(!apiConnected){
apiConnected = true;
removeBanner('noserver');
}
$('#connStatus').className = 'conn-ok';
$('#connStatus').title = 'Connected to '+API;
return await r.json();
} catch(e){
if(!apiConnected){
showBanner('noserver','error',
`⚠️ Cannot reach API server at <strong>${API}</strong>. Start the trading system: <code>python main.py</code> then refresh this page.`
);
}
$('#connStatus').className = 'conn-err';
$('#connStatus').title = 'Disconnected: '+e.message;
return null;
}
}
// ── Config (once) ────────────────────────────────────────────────────
async function loadConfig(){
const c = await api('/api/config');
if(!c) return;
configLoaded = true;
universe = c.universe;
target = c.target_daily_profit;
selectedChart = universe[0] || 'AAPL';
$('#pnlTarget').textContent = `Target: ${fmtUSD(target)}`;
// Mode badge
const b = $('#modeBadge');
if(c.dry_run){ b.textContent='DRY RUN'; b.className='mode-badge mode-dry'; }
else if(c.trading_mode==='paper'){ b.textContent='PAPER'; b.className='mode-badge mode-paper'; }
else{ b.textContent='LIVE'; b.className='mode-badge mode-live'; }
// Config grid
const g = $('#configGrid');
const items = [
['Mode', c.trading_mode.toUpperCase() + (c.dry_run?' (DRY)':'')],
['GPU', c.gpu?'✅ Enabled':'❌ Off'],
['Risk/Trade', c.risk_per_trade_pct+'%'],
['Max Loss', fmtUSD(c.max_daily_loss)],
['Max Pos', c.max_positions],
['Tech Wt', (c.technical_weight*100).toFixed(0)+'%'],
['Sent Wt', (c.sentiment_weight*100).toFixed(0)+'%'],
['Short', c.allow_short?'Yes':'No'],
['Overnight', c.allow_overnight?'Yes':'No'],
['Buy Thresh', c.buy_threshold],
['Sell Thresh', c.sell_threshold],
['Above $25k', c.above_25k?'Yes':'No'],
];
g.innerHTML = items.map(([k,v])=>`<dt>${k}</dt><dd>${v}</dd>`).join('');
// Chart tabs
const tabs = $('#chartTabs');
tabs.innerHTML = universe.map(s=>
`<button class="chart-tab${s===selectedChart?' active':''}" data-sym="${s}">${s}</button>`
).join('');
tabs.addEventListener('click', e=>{
const sym = e.target.dataset.sym;
if(!sym) return;
selectedChart = sym;
$$('.chart-tab').forEach(t=>t.classList.toggle('active',t.dataset.sym===sym));
loadChart(sym);
});
initChart();
loadChart(selectedChart);
}
// ── System Info (once) ───────────────────────────────────────────────
async function loadSystemInfo(){
const d = await api('/api/system_info');
if(!d) return;
systemLoaded = true;
const m = d.model;
// Model info
const mi = $('#modelInfo');
const gpuClass = m.gpu_available ? 'good' : 'warn';
const gpuText = m.gpu_available ? `✅ ${m.gpu_name} (${m.gpu_vram_gb}GB)` : '❌ CPU only';
const newsText = m.news_sources.length ? m.news_sources.join(' + ') : '⚠ No API keys';
const newsClass = m.news_sources.length ? 'good' : 'bad';
mi.innerHTML = [
['Model', `<strong>${m.name}</strong>`, ''],
['Type', m.type, ''],
['Device', m.device, gpuClass],
['GPU', gpuText, gpuClass],
['Labels', m.labels.join(', '), ''],
['Score', m.score_formula, ''],
['Batch / Tokens', `${m.batch_size} / ${m.max_tokens}`, ''],
['News Sources', newsText, newsClass],
['Fallback', m.fallback, 'warn'],
].map(([l,v,c])=>`<div class="model-row"><span class="mlabel">${l}</span><span class="mval ${c}">${v}</span></div>`).join('');
// Indicators table
const ib = $('#indBody');
const typeMap = {'Momentum':'momentum','Trend':'trend','Volatility':'volatility','Volume-Price':'volume-price','Volatility Filter':'volatility'};
ib.innerHTML = d.indicators.map(i=>{
const pct = (i.weight * 100).toFixed(0);
const cls = typeMap[i.type]||'';
return `<tr>
<td><strong>${i.name}</strong></td>
<td>${pct}% <div class="ind-weight"><div class="ind-weight-fill" style="width:${pct*5}%"></div></div></td>
<td><span class="ind-type-tag ${cls}">${i.type}</span></td>
</tr>`;
}).join('');
// Data status table
const db = $('#dataBody');
const syms = d.universe || Object.keys(d.data_status);
db.innerHTML = syms.map(s=>{
const sd = d.data_status[s] || {};
const daily = sd.daily || {bars:0};
const hourly = sd.hourly || {bars:0};
const fivemin = sd['5min'] || {bars:0};
return `<tr>
<td><strong>${s}</strong></td>
<td><span class="bar-count ${barCountClass(daily.bars)}">${daily.bars.toLocaleString()}</span></td>
<td class="last-update">${fmtTimestamp(daily.last_update)}</td>
<td><span class="bar-count ${barCountClass(hourly.bars)}">${hourly.bars.toLocaleString()}</span></td>
<td class="last-update">${fmtTimestamp(hourly.last_update)}</td>
<td><span class="bar-count ${barCountClass(fivemin.bars)}">${fivemin.bars.toLocaleString()}</span></td>
<td class="last-update">${fmtTimestamp(fivemin.last_update)}</td>
</tr>`;
}).join('');
// Total bars badge
const badge = $('#totalBarsBadge');
badge.textContent = d.total_bars.toLocaleString() + ' bars';
badge.style.display = 'inline-block';
// Metrics
updateMetricsDisplay(d.metrics);
}
// ── Performance Metrics ──────────────────────────────────────────────
function updateMetricsDisplay(metrics){
const container = $('#metricsGrid');
if(!metrics || Object.keys(metrics).length === 0){
container.innerHTML = '<div class="no-metrics">No backtest results yet — run <code>python -m backtester.engine</code> to generate metrics (win rate, Sharpe, drawdown, etc.)</div>';
return;
}
const m = metrics;
const items = [
['Win Rate', m.win_rate_pct != null ? m.win_rate_pct.toFixed(1)+'%' : '—', m.win_rate_pct >= 50 ? 'positive' : 'negative'],
['Total Return', m.total_return_pct != null ? m.total_return_pct.toFixed(2)+'%' : '—', m.total_return_pct >= 0 ? 'positive' : 'negative'],
['Sharpe', m.sharpe_ratio != null ? m.sharpe_ratio.toFixed(2) : '—', m.sharpe_ratio >= 1 ? 'positive' : m.sharpe_ratio >= 0 ? 'neutral' : 'negative'],
['Sortino', m.sortino_ratio != null ? m.sortino_ratio.toFixed(2) : '—', m.sortino_ratio >= 1 ? 'positive' : 'neutral'],
['Max Drawdown', m.max_drawdown_pct != null ? m.max_drawdown_pct.toFixed(2)+'%' : '—', Math.abs(m.max_drawdown_pct||0) < 5 ? 'positive' : 'negative'],
['CAGR', m.cagr_pct != null ? m.cagr_pct.toFixed(2)+'%' : '—', m.cagr_pct >= 0 ? 'positive' : 'negative'],
['Expectancy', m.expectancy_usd != null ? '$'+m.expectancy_usd.toFixed(2) : '—', m.expectancy_usd >= 0 ? 'positive' : 'negative'],
['Total Trades', m.total_trades != null ? m.total_trades : '—', 'neutral'],
['Avg Win', m.avg_win_pct != null ? m.avg_win_pct.toFixed(2)+'%' : '—', 'positive'],
['Avg Loss', m.avg_loss_pct != null ? m.avg_loss_pct.toFixed(2)+'%' : '—', 'negative'],
['Calmar', m.calmar_ratio != null ? m.calmar_ratio.toFixed(2) : '—', m.calmar_ratio >= 1 ? 'positive' : 'neutral'],
['Avg Duration', m.avg_trade_duration_min != null ? Math.round(m.avg_trade_duration_min)+'m' : '—', 'neutral'],
];
container.innerHTML = items.map(([label, val, cls])=>
`<div class="metric-box"><div class="metric-val ${cls}">${val}</div><div class="metric-label">${label}</div></div>`
).join('');
}
// ── Portfolio ────────────────────────────────────────────────────────
async function updatePortfolio(){
const d = await api('/api/portfolio');
if(!d) return;
const total = d.total || 0;
const el = $('#pnlValue');
el.textContent = (total>=0?'+':'')+fmtUSD(total);
el.className = 'pnl-value ' + pnlClass(total);
$('#pnlRealized').textContent = fmtUSD(d.realized);
$('#pnlUnrealized').textContent = fmtUSD(d.unrealized);
if(d.equity != null) $('#acctEquity').textContent = fmtUSD(d.equity);
if(d.buying_power != null) $('#acctBuyingPower').textContent = fmtUSD(d.buying_power);
if(d.cash != null) $('#acctCash').textContent = fmtUSD(d.cash);
const pct = target > 0 ? Math.min(Math.max(total/target*100,0),100) : 0;
const bar = $('#pnlProgress');
bar.style.width = pct+'%';
bar.style.background = total >= target ? 'var(--green)' : total >= 0 ? 'var(--blue)' : 'var(--red)';
$('#pnlPct').textContent = pct.toFixed(1)+'%';
const pos = d.positions;
const keys = Object.keys(pos);
const body = $('#posBody');
if(keys.length===0){
body.innerHTML = '<tr class="empty-row"><td colspan="7">No open positions</td></tr>';
} else {
body.innerHTML = keys.map(s=>{
const p = pos[s];
const pl = p.unrealized_pl||0;
return `<tr>
<td><strong>${p.symbol}</strong></td>
<td>${(p.side||'').toUpperCase()}</td>
<td>${fmt(p.qty,4)}</td>
<td>${fmtUSD(p.entry_price)}</td>
<td>${fmtUSD(p.current_price)}</td>
<td class="pnl-cell ${pnlClass(pl)}">${(pl>=0?'+':'')+fmtUSD(pl)}</td>
<td>${fmtUSD(p.market_value)}</td>
</tr>`;
}).join('');
}
$('#posCount').textContent = d.count;
}
// ── Predictions ──────────────────────────────────────────────────────
async function updatePredictions(){
const d = await api('/api/predictions');
if(!d) return;
const body = $('#predBody');
const syms = universe.length ? universe : Object.keys(d);
if(syms.length===0){
body.innerHTML = '<tr class="empty-row"><td colspan="12">Market closed — predictions generated during trading hours (9:30 AM – 4:00 PM ET)</td></tr>';
return;
}
const hasData = syms.some(s => d[s] && d[s].direction !== '—');
if(!hasData){
body.innerHTML = '<tr class="empty-row"><td colspan="12">Market closed — predictions generated during trading hours (9:30 AM – 4:00 PM ET)</td></tr>';
return;
}
body.innerHTML = syms.map(s=>{
const p = d[s];
if(!p || p.direction==='—') return `<tr><td><strong>${s}</strong></td>${'<td>—</td>'.repeat(11)}</tr>`;
const conf = (p.confidence||0)*100;
const confColor = conf > 60 ? 'var(--green)' : conf > 30 ? 'var(--yellow)' : 'var(--dim)';
return `<tr>
<td><strong>${s}</strong></td>
<td>${fmtUSD(p.current_price)}</td>
<td class="${dirClass(p.direction)}">${p.direction==='UP'?'▲ UP':p.direction==='DOWN'?'▼ DOWN':'● NEUTRAL'}</td>
<td><span class="${decClass(p.decision)}">${p.decision}</span></td>
<td>${fmt(p.final_score,3)}</td>
<td>${fmt(p.technical,3)}</td>
<td>${fmt(p.sentiment,3)}</td>
<td>${p.entry_price?fmtUSD(p.entry_price):'—'}</td>
<td>${p.stop_loss?fmtUSD(p.stop_loss):'—'}</td>
<td>${p.take_profit?fmtUSD(p.take_profit):'—'}</td>
<td style="color:${p.expected_profit>0?'var(--green)':'var(--dim)'}">${fmtUSD(p.expected_profit)}</td>
<td><div class="confidence-bar"><div class="confidence-fill" style="width:${conf}%;background:${confColor}"></div></div> ${conf.toFixed(0)}%</td>
</tr>`;
}).join('');
}
// ── Risk ─────────────────────────────────────────────────────────────
async function updateRisk(){
const d = await api('/api/risk');
if(!d) return;
$('#clock').textContent = d.time_et + ' ET';
const ms = $('#marketStatus');
if(d.market_open) ms.innerHTML = '<span class="status-dot dot-green"></span>Market Open';
else ms.innerHTML = '<span class="status-dot dot-red"></span>Market Closed';
$('#safeBadge').style.display = d.safe_mode ? 'inline' : 'none';
if(d.pdt_count >= 0){
const pdtEl = $('#pdtCount');
pdtEl.textContent = d.pdt_count + '/' + d.pdt_max;
pdtEl.style.color = d.pdt_count >= d.pdt_max ? 'var(--red)' : d.pdt_count >= d.pdt_max-1 ? 'var(--yellow)' : 'var(--green)';
} else {
$('#pdtCount').textContent = 'N/A';
$('#pdtCount').style.color = 'var(--dim)';
}
$('#lossStreak').textContent = d.consecutive_losses;
$('#lossStreak').style.color = d.consecutive_losses>=3?'var(--red)':d.consecutive_losses>=2?'var(--yellow)':'var(--green)';
const se = $('#sentStatus');
if(d.sentiment_status==='fresh'){ se.textContent='Fresh'; se.style.color='var(--green)'; }
else if(d.sentiment_status==='stale'){ se.textContent='Stale ('+(d.sentiment_age_min||0).toFixed(0)+'m)'; se.style.color='var(--yellow)'; }
else if(d.sentiment_status==='warming_up'){ se.textContent='Warming…'; se.style.color='var(--yellow)'; }
else{ se.textContent='No Data'; se.style.color='var(--red)'; }
const cl = $('#circuitList');
if(d.circuits_active.length === 0){
cl.innerHTML = '<span class="circuit-chip ok">All Clear</span>';
} else {
cl.innerHTML = d.circuits_active.map(c=>`<span class="circuit-chip">${c.replace(/_/g,' ').toUpperCase()}</span>`).join('');
}
}
// ── Sentiment ────────────────────────────────────────────────────────
async function updateSentiment(){
const d = await api('/api/sentiment');
if(!d) return;
const container = $('#sentBars');
const syms = universe.length ? universe : Object.keys(d);
container.innerHTML = syms.map(s=>{
const sc = d[s] || {};
const v = sc.score || 0;
const pct = ((v + 1) / 2) * 100;
const color = v > 0.15 ? 'var(--green)' : v < -0.15 ? 'var(--red)' : 'var(--dim)';
const staleTag = sc.stale ? ' <span style="color:var(--yellow);font-size:.65rem">STALE</span>' : '';
return `<div class="sent-row">
<span class="sent-sym">${s}</span>
<div class="sent-bar-container">
<div class="sent-bar-fill" style="width:${pct}%;background:${color}"></div>
</div>
<span class="sent-val" style="color:${color}">${v>=0?'+':''}${v.toFixed(2)}${staleTag}</span>
</div>`;
}).join('');
}
// ── Trade Log ────────────────────────────────────────────────────────
async function updateTradeLog(){
const d = await api('/api/trades');
if(!d || d.length===0) return;
const container = $('#tradeLog');
container.innerHTML = d.slice().reverse().map(e=>{
const t = e.time ? new Date(e.time).toLocaleTimeString('en-US',{hour12:false}) : '—';
const cls = e.side==='buy'?'log-buy':e.side==='sell'?'log-sell':e.type==='warning'?'log-warn':'log-info';
const msg = e.message || `${(e.side||'').toUpperCase()} ${e.symbol||''} ${e.qty||''} @ ${fmtUSD(e.price)}`;
return `<div class="log-entry"><span class="log-time">${t}</span><span class="log-msg ${cls}">${msg}</span></div>`;
}).join('');
}
// ── Chart ────────────────────────────────────────────────────────────
function initChart(){
const ctx = $('#priceChart').getContext('2d');
priceChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{label:'Close', data:[], borderColor:'#58a6ff', borderWidth:1.5, pointRadius:0, tension:0.1, fill:false},
{label:'VWAP', data:[], borderColor:'#bc8cff', borderWidth:1, pointRadius:0, borderDash:[4,4], tension:0.1, fill:false}
]
},
options: {
responsive:true, maintainAspectRatio:false,
animation:{duration:0},
interaction:{mode:'index',intersect:false},
scales:{
x:{display:true,grid:{color:'#21262d'},ticks:{color:'#8b949e',maxTicksLimit:10,font:{size:10}}},
y:{display:true,grid:{color:'#21262d'},ticks:{color:'#8b949e',font:{size:10},callback:v=>'$'+v.toFixed(2)}}
},
plugins:{
legend:{labels:{color:'#8b949e',boxWidth:12,font:{size:11}}},
tooltip:{
backgroundColor:'#161b22',borderColor:'#30363d',borderWidth:1,titleColor:'#e6edf3',bodyColor:'#e6edf3',
callbacks:{label:ctx=>`${ctx.dataset.label}: $${ctx.parsed.y.toFixed(2)}`}
}
}
}
});
}
async function loadChart(symbol){
const d = await api('/api/chart/'+symbol);
if(!d || !d.bars || d.bars.length===0){
// Try daily chart if 5min not available
const dd = await api('/api/chart_daily/'+symbol);
if(dd && dd.bars && dd.bars.length > 0){
const labels = dd.bars.map(b=>b.t);
priceChart.data.labels = labels;
priceChart.data.datasets[0].data = dd.bars.map(b=>b.c);
priceChart.data.datasets[1].data = dd.bars.map(b=>+((b.h+b.l+b.c)/3).toFixed(2));
priceChart.options.plugins.title = {display:true, text:symbol+' — Daily', color:'#8b949e', font:{size:12}};
priceChart.update();
}
return;
}
const labels = d.bars.map(b=>{
const dt = new Date(b.t);
return dt.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:false});
});
priceChart.data.labels = labels;
priceChart.data.datasets[0].data = d.bars.map(b=>b.c);
priceChart.data.datasets[1].data = d.bars.map(b=>{
return +((b.h+b.l+b.c)/3).toFixed(2);
});
priceChart.options.plugins.title = {display:true, text:symbol+' — 5min', color:'#8b949e', font:{size:12}};
priceChart.update();
}
// ── Update Loop ──────────────────────────────────────────────────────
async function tick(){
if(!configLoaded) await loadConfig();
if(!systemLoaded) await loadSystemInfo();
await Promise.all([
updatePortfolio(),
updatePredictions(),
updateRisk(),
updateSentiment(),
updateTradeLog(),
]);
}
// Auto-refresh every 5 seconds
loadConfig().then(()=>{
loadSystemInfo();
tick();
setInterval(tick, 5000);
setInterval(()=>loadChart(selectedChart), 30000);
// Refresh system info every 2 minutes
setInterval(loadSystemInfo, 120000);
});
</script>
</body>
</html>