Download dashboard.html from raghava4u/Trading-Bot-M20: direct link, hf CLI and curl.
- Browser
- Download file 42.5 kB
-
https://huggingface.co/raghava4u/Trading-Bot-M20/resolve/main/dashboard.html
- Command line
-
hf download hf://raghava4u/Trading-Bot-M20/dashboard.html
-
curl -L -o dashboard.html https://huggingface.co/raghava4u/Trading-Bot-M20/resolve/main/dashboard.html
42.5 kB
| <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> | |