Files
ForexBot2/web/static/index.html
T
harwinder 98daf71a52 Add web dashboard (Redis + FastAPI + WebSocket), scope XAU penalties
Signal daemon (signal_web.py): candle-boundary scanner mirroring
telegram_bot.py, publishes serialized SignalReport JSON to Redis.
Web API (web/app.py): thin FastAPI reader behind Apache, serves
/api/signals REST + /ws WebSocket from Redis pub/sub.
Frontend (web/static/index.html): 3-pair grid, color-coded signals,
indicator breakdown per timeframe, trade setup display.

Also: XAU-specific penalty scoping in signal_aggregator (softer
confidence penalties for gold, original strict values for forex),
pair-aware session filter routing XAU to its own blocked hours.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 23:15:37 -04:00

483 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ForexBot2 — Live Dashboard</title>
<style>
:root {
--bg: #0f1117;
--card-bg: #1a1d27;
--border: #2a2d3a;
--text: #e1e4eb;
--text-dim: #8b8fa3;
--buy: #22c55e;
--buy-bg: rgba(34, 197, 94, 0.08);
--sell: #ef4444;
--sell-bg: rgba(239, 68, 68, 0.08);
--neutral: #6b7280;
--neutral-bg: rgba(107, 114, 128, 0.06);
--strong-buy: #16a34a;
--strong-sell: #dc2626;
--accent: #6366f1;
--gold: #eab308;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
header {
padding: 20px 24px 12px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
header h1 {
font-size: 18px;
font-weight: 600;
letter-spacing: 0.5px;
}
.status-bar {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: var(--text-dim);
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--sell);
display: inline-block;
}
.status-dot.connected { background: var(--buy); }
#last-update { color: var(--text-dim); }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 20px 24px;
max-width: 1600px;
margin: 0 auto;
}
@media (max-width: 1100px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
.pair-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
transition: border-color 0.3s;
}
.pair-card.buy { border-color: var(--buy); background: var(--buy-bg); }
.pair-card.sell { border-color: var(--sell); background: var(--sell-bg); }
.pair-card.neutral { border-color: var(--border); }
.card-header {
padding: 16px 18px 12px;
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1px solid var(--border);
}
.pair-name {
font-size: 20px;
font-weight: 700;
letter-spacing: 1px;
}
.pair-price {
font-size: 14px;
color: var(--text-dim);
margin-top: 2px;
}
.signal-badge {
padding: 4px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.signal-badge.buy, .signal-badge.strong_buy { background: var(--buy); color: #000; }
.signal-badge.sell, .signal-badge.strong_sell { background: var(--sell); color: #fff; }
.signal-badge.neutral { background: var(--neutral); color: #fff; }
.card-meta {
padding: 10px 18px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
border-bottom: 1px solid var(--border);
font-size: 11px;
}
.meta-item { display: flex; flex-direction: column; gap: 2px; }
.meta-label { color: var(--text-dim); text-transform: uppercase; font-size: 9px; letter-spacing: 0.5px; }
.meta-value { font-weight: 600; font-size: 13px; }
.quality-A\+, .quality-A { color: var(--buy); }
.quality-B { color: var(--gold); }
.quality-C { color: #f97316; }
.quality-D, .quality-F { color: var(--sell); }
.tf-section {
padding: 8px 18px;
border-bottom: 1px solid var(--border);
}
.tf-section:last-of-type { border-bottom: none; }
.tf-header {
font-size: 11px;
font-weight: 700;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.tf-signal-tag {
font-size: 10px;
padding: 1px 6px;
border-radius: 3px;
font-weight: 600;
}
.tf-signal-tag.buy { background: rgba(34,197,94,0.2); color: var(--buy); }
.tf-signal-tag.sell { background: rgba(239,68,68,0.2); color: var(--sell); }
.tf-signal-tag.neutral { background: rgba(107,114,128,0.2); color: var(--neutral); }
.indicator-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 0;
font-size: 12px;
}
.ind-name { color: var(--text-dim); text-transform: uppercase; font-size: 10px; min-width: 80px; }
.ind-desc { flex: 1; margin: 0 8px; color: var(--text); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ind-signal { font-weight: 600; font-size: 11px; min-width: 50px; text-align: right; }
.ind-signal.buy, .ind-signal.strong_buy { color: var(--buy); }
.ind-signal.sell, .ind-signal.strong_sell { color: var(--sell); }
.ind-signal.neutral { color: var(--neutral); }
.trade-setup {
padding: 12px 18px;
border-top: 2px solid var(--border);
background: rgba(99, 102, 241, 0.05);
}
.trade-setup h4 {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--accent);
margin-bottom: 8px;
}
.trade-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
font-size: 12px;
}
.trade-grid .label { color: var(--text-dim); }
.trade-grid .val { font-weight: 600; text-align: right; }
.trade-grid .val.buy-dir { color: var(--buy); }
.trade-grid .val.sell-dir { color: var(--sell); }
.reasons-section {
padding: 8px 18px 12px;
font-size: 11px;
}
.reasons-section .bull { color: var(--buy); }
.reasons-section .bear { color: var(--sell); }
.reasons-section .warn { color: var(--gold); }
.reason-line { padding: 1px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loading {
text-align: center;
padding: 60px 20px;
color: var(--text-dim);
font-size: 14px;
}
.loading .spinner {
width: 32px; height: 32px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.flash { animation: pulse 0.6s ease-in-out; }
.mtf-bar {
display: flex;
gap: 6px;
padding: 6px 18px 10px;
font-size: 10px;
}
.mtf-chip {
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
letter-spacing: 0.3px;
}
.mtf-chip.buy { background: rgba(34,197,94,0.15); color: var(--buy); }
.mtf-chip.sell { background: rgba(239,68,68,0.15); color: var(--sell); }
.mtf-chip.neutral { background: rgba(107,114,128,0.12); color: var(--neutral); }
</style>
</head>
<body>
<header>
<h1>ForexBot2 Live Dashboard</h1>
<div class="status-bar">
<span><span class="status-dot" id="ws-dot"></span> <span id="ws-status">Connecting…</span></span>
<span id="last-update"></span>
</div>
</header>
<div class="grid" id="grid">
<div class="loading" id="loading">
<div class="spinner"></div>
Running initial scan — this takes 30–60 seconds…
</div>
</div>
<script>
const PAIRS = ["EUR/USD", "GBP/USD", "XAU/USD"];
const TF_ORDER = ["15min", "1h", "4h"];
const TF_LABELS = {"5min":"M5","15min":"M15","1h":"H1","4h":"H4","1d":"D1"};
let ws = null;
let reconnectDelay = 1000;
function signalClass(sig) {
if (!sig) return "neutral";
const s = sig.toLowerCase();
if (s === "strong_buy" || s === "buy") return "buy";
if (s === "strong_sell" || s === "sell") return "sell";
return "neutral";
}
function signalLabel(sig) {
if (!sig) return "NEUTRAL";
return sig.replace("_", " ");
}
function fmtPrice(price, pair) {
if (!price) return "—";
if (pair === "XAU/USD") return price.toFixed(2);
return price.toFixed(5);
}
function buildIndicatorRows(indicators) {
if (!indicators || Object.keys(indicators).length === 0) return "";
let html = "";
for (const [name, ind] of Object.entries(indicators)) {
const cls = signalClass(ind.signal);
html += `<div class="indicator-row">
<span class="ind-name">${name.replace(/_/g, " ")}</span>
<span class="ind-desc">${ind.description || ""}</span>
<span class="ind-signal ${cls}">${signalLabel(ind.signal)}</span>
</div>`;
}
return html;
}
function buildCard(pair, tfData) {
if (!tfData || Object.keys(tfData).length === 0) return "";
let best = null;
for (const tf of TF_ORDER) {
if (tfData[tf]) { best = tfData[tf]; break; }
}
if (!best) return "";
const sc = signalClass(best.signal);
const priceStr = fmtPrice(best.price, pair);
let mtfHtml = "";
for (const tf of TF_ORDER) {
const d = tfData[tf];
if (!d) continue;
const cls = signalClass(d.signal);
mtfHtml += `<span class="mtf-chip ${cls}">${TF_LABELS[tf] || tf} ${signalLabel(d.signal)}</span>`;
}
let metaHtml = `<div class="card-meta">
<div class="meta-item"><span class="meta-label">Score</span><span class="meta-value">${best.score >= 0 ? "+" : ""}${best.score}</span></div>
<div class="meta-item"><span class="meta-label">Confidence</span><span class="meta-value">${best.confidence}%</span></div>
<div class="meta-item"><span class="meta-label">Quality</span><span class="meta-value quality-${best.quality}">${best.quality || "—"}</span></div>
</div>
<div class="card-meta">
<div class="meta-item"><span class="meta-label">Regime</span><span class="meta-value">${(best.regime || "—").replace(/_/g, " ")}</span></div>
<div class="meta-item"><span class="meta-label">Volatility</span><span class="meta-value">${best.volatility || "—"}</span></div>
<div class="meta-item"><span class="meta-label">MTF</span><span class="meta-value">${best.mtf_agreement ? "✓ Aligned" : "✗ Diverged"}</span></div>
</div>`;
let tfSections = "";
for (const tf of TF_ORDER) {
const d = tfData[tf];
if (!d) continue;
let indicators = d.indicators || {};
const tfCls = signalClass(d.signal);
tfSections += `<div class="tf-section">
<div class="tf-header">
<span>${TF_LABELS[tf] || tf} — Score: ${d.score >= 0 ? "+" : ""}${d.score} | ${d.confidence}%</span>
<span class="tf-signal-tag ${tfCls}">${signalLabel(d.signal)}</span>
</div>
${buildIndicatorRows(indicators)}`;
if (d.confirmations) {
for (const cf of d.confirmations) {
const cfCls = signalClass(cf.signal);
tfSections += `<div class="tf-header" style="margin-top:6px;">
<span>${TF_LABELS[cf.timeframe] || cf.timeframe} (confirm) — ${cf.score >= 0 ? "+" : ""}${cf.score} | ${cf.confidence}%</span>
<span class="tf-signal-tag ${cfCls}">${signalLabel(cf.signal)}</span>
</div>`;
tfSections += buildIndicatorRows(cf.indicators);
}
}
tfSections += `</div>`;
}
let tradeHtml = "";
if (best.trade_setup) {
const t = best.trade_setup;
const dirCls = t.direction === "BUY" ? "buy-dir" : "sell-dir";
tradeHtml = `<div class="trade-setup">
<h4>Trade Setup</h4>
<div class="trade-grid">
<span class="label">Direction</span><span class="val ${dirCls}">${t.direction}</span>
<span class="label">Entry</span><span class="val">${t.entry}</span>
<span class="label">Stop Loss</span><span class="val">${t.sl}</span>
<span class="label">Take Profit</span><span class="val">${t.tp1}</span>
<span class="label">Risk/Reward</span><span class="val">1:${t.rr}</span>
<span class="label">Risk</span><span class="val">${t.risk_pips} pips</span>
</div>
</div>`;
}
let reasonsHtml = "";
const bulls = best.bullish_reasons || [];
const bears = best.bearish_reasons || [];
const warns = best.warnings || [];
if (bulls.length + bears.length + warns.length > 0) {
reasonsHtml = `<div class="reasons-section">`;
for (const r of bulls.slice(0, 3)) reasonsHtml += `<div class="reason-line bull">+ ${r}</div>`;
for (const r of bears.slice(0, 3)) reasonsHtml += `<div class="reason-line bear">- ${r}</div>`;
for (const w of warns.slice(0, 2)) reasonsHtml += `<div class="reason-line warn">⚠ ${w}</div>`;
reasonsHtml += `</div>`;
}
return `<div class="pair-card ${sc}" id="card-${pair.replace("/","")}">
<div class="card-header">
<div>
<div class="pair-name">${pair}</div>
<div class="pair-price">${priceStr}</div>
</div>
<span class="signal-badge ${sc}">${signalLabel(best.signal)}</span>
</div>
${metaHtml}
<div class="mtf-bar">${mtfHtml}</div>
${tfSections}
${tradeHtml}
${reasonsHtml}
</div>`;
}
function render(data) {
const grid = document.getElementById("grid");
const loading = document.getElementById("loading");
if (loading) loading.remove();
let html = "";
for (const pair of PAIRS) {
const tfData = data[pair] || {};
html += buildCard(pair, tfData);
}
grid.innerHTML = html;
grid.querySelectorAll(".pair-card").forEach(c => {
c.classList.add("flash");
setTimeout(() => c.classList.remove("flash"), 600);
});
}
function connect() {
const proto = location.protocol === "https:" ? "wss:" : "ws:";
ws = new WebSocket(`${proto}//${location.host}/ws`);
ws.onopen = () => {
document.getElementById("ws-dot").classList.add("connected");
document.getElementById("ws-status").textContent = "Connected";
reconnectDelay = 1000;
};
ws.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
if (msg.type === "update" && msg.data) {
render(msg.data);
document.getElementById("last-update").textContent = msg.timestamp || "";
}
} catch (err) {
console.error("Parse error:", err);
}
};
ws.onclose = () => {
document.getElementById("ws-dot").classList.remove("connected");
document.getElementById("ws-status").textContent = "Reconnecting…";
setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
};
ws.onerror = () => { ws.close(); };
}
connect();
</script>
</body>
</html>