Initial commit: MT5 EA Optimizer v1.0
Full optimization system for LEGSTECH_EA_V2: - Flask + SocketIO live dashboard (dark premium UI) - MT5 process control (auto-kill, clean launch, retry) - HTML report parser (UTF-16 LE, 597 trades, metrics) - Pre-run validation and actionable error messages - Analysis engines: Reversal, TimePerfomance, EntryExit, EquityCurve - Composite scoring (Calmar-primary) - Mutation engine with knowledge_base.yaml - Validation gate: IS + Walk-Forward - Reports folder with HTML/CSV per run - Double-click launcher batch file
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
/* style.css — MT5 Optimizer Dashboard */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
/* ── Variables ────────────────────────────────────────────────────────────── */
|
||||
:root {
|
||||
--bg: #0a0d14;
|
||||
--bg-card: #111621;
|
||||
--bg-card2: #161c2a;
|
||||
--border: #1e2740;
|
||||
--border-glow: #2a3a6e;
|
||||
--text: #e2e8f8;
|
||||
--text-dim: #6b7fa3;
|
||||
--text-muted: #3d4f70;
|
||||
--accent: #4f8ef7;
|
||||
--accent2: #7c3aed;
|
||||
--green: #22d3a5;
|
||||
--red: #f44;
|
||||
--yellow: #fbbf24;
|
||||
--orange: #f97316;
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── Header ───────────────────────────────────────────────────────────────── */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 24px;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo { display: flex; align-items: center; gap: 12px; }
|
||||
.logo-icon { font-size: 28px; }
|
||||
.logo-title { font-weight: 800; font-size: 16px; letter-spacing: -0.3px; }
|
||||
.logo-sub { font-size: 11px; color: var(--text-dim); font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
.header-center { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
|
||||
.stat-pill {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
background: var(--bg-card2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pill-label { color: var(--text-dim); }
|
||||
.pill-val { font-weight: 700; font-family: 'JetBrains Mono', monospace; color: var(--accent); }
|
||||
.score-val { color: var(--green); font-size: 14px; }
|
||||
|
||||
.dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.dot-idle { background: var(--text-muted); }
|
||||
.dot-running { background: var(--green); box-shadow: 0 0 8px var(--green); animation: pulse 1.5s infinite; }
|
||||
.dot-paused { background: var(--yellow); }
|
||||
.dot-error { background: var(--red); }
|
||||
|
||||
@keyframes pulse {
|
||||
0%,100% { opacity: 1; } 50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
.header-right { display: flex; gap: 8px; }
|
||||
|
||||
/* ── Buttons ──────────────────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-start { background: linear-gradient(135deg, #4f8ef7, #7c3aed); color: #fff; }
|
||||
.btn-start:hover { opacity: 0.85; transform: translateY(-1px); }
|
||||
.btn-pause { background: var(--yellow); color: #000; }
|
||||
.btn-stop { background: var(--red); color: #fff; }
|
||||
.btn-secondary { background: var(--bg-card2); color: var(--text-dim); border: 1px solid var(--border); }
|
||||
.btn-secondary:hover { color: var(--text); border-color: var(--accent); }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* ── Main Grid ────────────────────────────────────────────────────────────── */
|
||||
.main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 20px 24px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.main-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.col-left, .col-right { display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
/* ── Cards ────────────────────────────────────────────────────────────────── */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.card:hover { border-color: var(--border-glow); }
|
||||
|
||||
.card-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.card-title { font-weight: 700; font-size: 13px; }
|
||||
.card-badge {
|
||||
background: var(--bg-card2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 3px 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.run-id-badge {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--accent);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ── Chart ────────────────────────────────────────────────────────────────── */
|
||||
.chart-wrap { height: 200px; position: relative; }
|
||||
#scoreChart { width: 100% !important; height: 100% !important; }
|
||||
|
||||
/* ── Metrics Grid ─────────────────────────────────────────────────────────── */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.metric-card {
|
||||
background: var(--bg-card2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.metric-label { font-size: 10px; color: var(--text-dim); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.metric-val { font-size: 18px; font-weight: 700; font-family: 'JetBrains Mono', monospace; color: var(--text); }
|
||||
|
||||
.score-bar-wrap { margin-top: 4px; }
|
||||
.score-bar-label {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px; color: var(--text-dim);
|
||||
}
|
||||
.score-big { font-size: 22px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: var(--green); }
|
||||
.score-bar-track {
|
||||
height: 6px; background: var(--bg-card2);
|
||||
border-radius: 3px; overflow: hidden;
|
||||
}
|
||||
.score-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent), var(--green));
|
||||
border-radius: 3px;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
/* ── Phase Tracker ────────────────────────────────────────────────────────── */
|
||||
.phase-card { padding: 14px 20px; }
|
||||
.phase-steps {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.phase-step {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||
opacity: 0.35; transition: opacity 0.3s;
|
||||
}
|
||||
.phase-step.active { opacity: 1; }
|
||||
.phase-step.done { opacity: 0.6; }
|
||||
.phase-dot {
|
||||
width: 12px; height: 12px; border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
border: 2px solid var(--border);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.phase-step.active .phase-dot {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 10px var(--accent);
|
||||
}
|
||||
.phase-step.done .phase-dot { background: var(--green); border-color: var(--green); }
|
||||
.phase-name { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.phase-line { flex: 1; height: 1px; background: var(--border); margin: 0 4px; margin-bottom: 18px; }
|
||||
|
||||
/* ── Findings ─────────────────────────────────────────────────────────────── */
|
||||
.findings-feed {
|
||||
max-height: 220px; overflow-y: auto;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.finding-empty { color: var(--text-muted); font-size: 12px; text-align: center; padding: 20px 0; }
|
||||
.finding-row {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
background: var(--bg-card2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px 12px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
.finding-sev {
|
||||
font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 4px;
|
||||
flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 1px;
|
||||
}
|
||||
.sev-high { background: rgba(244,68,68,0.2); color: #f44; border: 1px solid rgba(244,68,68,0.3); }
|
||||
.sev-medium { background: rgba(251,191,36,0.15); color: var(--yellow); border: 1px solid rgba(251,191,36,0.3); }
|
||||
.sev-low { background: rgba(79,142,247,0.1); color: var(--accent); border: 1px solid rgba(79,142,247,0.2); }
|
||||
.finding-text { flex: 1; font-size: 12px; line-height: 1.5; color: var(--text); }
|
||||
.finding-conf { font-size: 10px; color: var(--text-dim); flex-shrink: 0; margin-top: 2px; }
|
||||
|
||||
/* ── Params Table ─────────────────────────────────────────────────────────── */
|
||||
.hyp-desc {
|
||||
font-size: 12px; color: var(--text-dim); margin-bottom: 10px;
|
||||
padding: 8px 10px; background: var(--bg-card2);
|
||||
border-radius: var(--radius-sm); border: 1px solid var(--border);
|
||||
font-style: italic;
|
||||
}
|
||||
.params-table-wrap { overflow-x: auto; max-height: 180px; overflow-y: auto; }
|
||||
.params-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.params-table thead th {
|
||||
text-align: left; padding: 6px 12px;
|
||||
color: var(--text-dim); font-weight: 600;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.params-table tbody td {
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid rgba(30,39,64,0.5);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.params-table tbody tr:last-child td { border-bottom: none; }
|
||||
.param-changed { color: var(--green); }
|
||||
.param-old { color: var(--text-dim); text-decoration: line-through; }
|
||||
.param-new { color: var(--green); font-weight: 600; }
|
||||
.param-arrow { color: var(--text-muted); padding: 0 4px; }
|
||||
|
||||
/* ── Log ──────────────────────────────────────────────────────────────────── */
|
||||
.log-card { max-height: 220px; display: flex; flex-direction: column; }
|
||||
.log-feed {
|
||||
flex: 1; overflow-y: auto; max-height: 160px;
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 11px;
|
||||
}
|
||||
.log-line { padding: 2px 4px; border-radius: 3px; }
|
||||
.log-info { color: var(--text-dim); }
|
||||
.log-success { color: var(--green); }
|
||||
.log-warn { color: var(--yellow); }
|
||||
.log-error { color: var(--red); }
|
||||
.clear-btn {
|
||||
background: none; border: none; color: var(--text-muted);
|
||||
cursor: pointer; font-size: 11px; padding: 2px 6px;
|
||||
}
|
||||
.clear-btn:hover { color: var(--text-dim); }
|
||||
|
||||
/* ── Candidates Section ───────────────────────────────────────────────────── */
|
||||
.candidates-section {
|
||||
padding: 0 24px 24px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.candidates-card {}
|
||||
.candidates-list {
|
||||
display: flex; gap: 12px; flex-wrap: wrap;
|
||||
}
|
||||
.cand-empty { color: var(--text-muted); font-size: 12px; padding: 10px 0; text-align: center; width: 100%; }
|
||||
.candidate-chip {
|
||||
background: linear-gradient(135deg, rgba(34,211,165,0.1), rgba(79,142,247,0.1));
|
||||
border: 1px solid rgba(34,211,165,0.3);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 16px;
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
min-width: 200px;
|
||||
animation: slideIn 0.4s ease;
|
||||
}
|
||||
.cand-score {
|
||||
font-size: 22px; font-weight: 800;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--green);
|
||||
}
|
||||
.cand-id { font-size: 10px; color: var(--text-dim); font-family: 'JetBrains Mono', monospace; }
|
||||
.cand-delta { font-size: 11px; font-weight: 600; }
|
||||
.delta-up { color: var(--green); }
|
||||
.delta-dn { color: var(--red); }
|
||||
|
||||
/* ── Scrollbar styling ────────────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border-glow); border-radius: 2px; }
|
||||
|
||||
/* ── Animations ───────────────────────────────────────────────────────────── */
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
/* dashboard.js — Real-time MT5 Optimizer Dashboard */
|
||||
|
||||
// ── Socket.IO connection ───────────────────────────────────────────────────
|
||||
const socket = io();
|
||||
let startTime = null;
|
||||
let timerInterval = null;
|
||||
let findingsCount = 0;
|
||||
let candidatesCount = 0;
|
||||
let currentParams = {};
|
||||
|
||||
// ── Chart setup ────────────────────────────────────────────────────────────
|
||||
const ctx = document.getElementById('scoreChart').getContext('2d');
|
||||
const scoreChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Composite Score',
|
||||
data: [],
|
||||
borderColor: '#22d3a5',
|
||||
backgroundColor: 'rgba(34,211,165,0.08)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#22d3a5',
|
||||
borderWidth: 2,
|
||||
},
|
||||
{
|
||||
label: 'Calmar Ratio',
|
||||
data: [],
|
||||
borderColor: '#4f8ef7',
|
||||
backgroundColor: 'rgba(79,142,247,0.05)',
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
pointRadius: 3,
|
||||
borderWidth: 1.5,
|
||||
borderDash: [4, 2],
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 500 },
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: '#6b7fa3', font: { size: 11, family: 'Inter' } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: '#111621',
|
||||
borderColor: '#2a3a6e',
|
||||
borderWidth: 1,
|
||||
titleColor: '#e2e8f8',
|
||||
bodyColor: '#6b7fa3',
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#6b7fa3', font: { size: 10 } },
|
||||
grid: { color: 'rgba(30,39,64,0.6)' },
|
||||
},
|
||||
y: {
|
||||
ticks: { color: '#6b7fa3', font: { size: 10 } },
|
||||
grid: { color: 'rgba(30,39,64,0.6)' },
|
||||
min: 0, max: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Controls ───────────────────────────────────────────────────────────────
|
||||
|
||||
function startOptimizer() {
|
||||
fetch('/api/start', { method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ auto: true })
|
||||
});
|
||||
startTime = Date.now();
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
setRunning(true);
|
||||
addLog('info', 'Starting optimizer...');
|
||||
}
|
||||
|
||||
function pauseOptimizer() {
|
||||
fetch('/api/pause', { method: 'POST' }).then(r => r.json()).then(d => {
|
||||
const btn = document.getElementById('btn-pause');
|
||||
btn.textContent = d.paused ? '▶ Resume' : '⏸ Pause';
|
||||
setDot(d.paused ? 'paused' : 'running', d.paused ? 'Paused' : 'Running...');
|
||||
});
|
||||
}
|
||||
|
||||
function stopOptimizer() {
|
||||
fetch('/api/stop', { method: 'POST' });
|
||||
clearInterval(timerInterval);
|
||||
setRunning(false);
|
||||
setDot('idle', 'Stopped');
|
||||
addLog('warn', 'Optimizer stopped by user.');
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('log-feed').innerHTML = '';
|
||||
}
|
||||
|
||||
function setRunning(on) {
|
||||
document.getElementById('btn-start').classList.toggle('hidden', on);
|
||||
document.getElementById('btn-pause').classList.toggle('hidden', !on);
|
||||
document.getElementById('btn-stop').classList.toggle('hidden', !on);
|
||||
if (on) setDot('running', 'Running...');
|
||||
}
|
||||
|
||||
// ── Timer ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function updateTimer() {
|
||||
if (!startTime) return;
|
||||
const s = Math.floor((Date.now() - startTime) / 1000);
|
||||
const m = Math.floor(s / 60);
|
||||
const ss = String(s % 60).padStart(2, '0');
|
||||
document.getElementById('hdr-elapsed').textContent = `${m}:${ss}`;
|
||||
}
|
||||
|
||||
// ── State dot ──────────────────────────────────────────────────────────────
|
||||
|
||||
function setDot(state, label) {
|
||||
const dot = document.getElementById('state-dot');
|
||||
dot.className = `dot dot-${state}`;
|
||||
document.getElementById('state-label').textContent = label;
|
||||
}
|
||||
|
||||
// ── Phase tracker ──────────────────────────────────────────────────────────
|
||||
|
||||
const PHASES = ['baseline', 'analyze', 'explore', 'validate', 'oos'];
|
||||
|
||||
function setPhase(phase) {
|
||||
const idx = PHASES.indexOf(phase);
|
||||
PHASES.forEach((p, i) => {
|
||||
const el = document.getElementById(`phase-${p}`);
|
||||
if (!el) return;
|
||||
el.classList.remove('active', 'done');
|
||||
if (i < idx) el.classList.add('done');
|
||||
else if (i === idx) el.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
// ── Metrics update ─────────────────────────────────────────────────────────
|
||||
|
||||
function updateMetrics(d) {
|
||||
const set = (id, val) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = val;
|
||||
};
|
||||
set('m-profit', d.net_profit !== undefined ? `$${d.net_profit.toLocaleString()}` : '—');
|
||||
set('m-calmar', d.calmar !== undefined ? d.calmar.toFixed(3) : '—');
|
||||
set('m-pf', d.profit_factor !== undefined ? d.profit_factor.toFixed(3) : '—');
|
||||
set('m-dd', d.drawdown_pct !== undefined ? `${d.drawdown_pct}%` : '—');
|
||||
set('m-wr', d.win_rate !== undefined ? `${d.win_rate.toFixed(1)}%` : '—');
|
||||
set('m-trades', d.total_trades ?? '—');
|
||||
set('m-mfe', d.mfe_capture !== undefined ? `${d.mfe_capture.toFixed(1)}%` : '—');
|
||||
set('m-rev', d.reversal_rate !== undefined ? `${d.reversal_rate.toFixed(1)}%` : '—');
|
||||
set('m-score', d.score !== undefined ? d.score.toFixed(4) : '—');
|
||||
set('current-run-id', d.run_id || '—');
|
||||
|
||||
if (d.score !== undefined) {
|
||||
const fill = document.getElementById('score-bar-fill');
|
||||
if (fill) fill.style.width = `${Math.min(100, d.score * 100)}%`;
|
||||
set('hdr-score', d.score.toFixed(4));
|
||||
}
|
||||
|
||||
// Color profit
|
||||
const pEl = document.getElementById('m-profit');
|
||||
if (pEl && d.net_profit !== undefined) {
|
||||
pEl.style.color = d.net_profit >= 0 ? 'var(--green)' : 'var(--red)';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Findings ───────────────────────────────────────────────────────────────
|
||||
|
||||
function addFinding(f) {
|
||||
findingsCount++;
|
||||
const feed = document.getElementById('findings-feed');
|
||||
const empty = feed.querySelector('.finding-empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
const sevClass = { high: 'sev-high', medium: 'sev-medium', low: 'sev-low' }[f.severity] || 'sev-low';
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'finding-row';
|
||||
row.innerHTML = `
|
||||
<span class="finding-sev ${sevClass}">${f.severity}</span>
|
||||
<span class="finding-text">${escHtml(f.description)}</span>
|
||||
<span class="finding-conf">${(f.confidence * 100).toFixed(0)}%</span>
|
||||
`;
|
||||
feed.insertBefore(row, feed.firstChild);
|
||||
|
||||
// Keep max 20 findings visible
|
||||
while (feed.children.length > 20) feed.removeChild(feed.lastChild);
|
||||
|
||||
document.getElementById('findings-count').textContent = `${findingsCount} findings`;
|
||||
}
|
||||
|
||||
// ── Hypotheses / Params ────────────────────────────────────────────────────
|
||||
|
||||
function showHypotheses(items) {
|
||||
if (!items || !items.length) return;
|
||||
const h = items[0]; // show first (highest priority)
|
||||
document.getElementById('hyp-desc').textContent = h.desc || '';
|
||||
document.getElementById('hyp-badge').textContent = `${items.length} hypothesis(es)`;
|
||||
|
||||
const tbody = document.getElementById('params-tbody');
|
||||
tbody.innerHTML = '';
|
||||
Object.entries(h.delta || {}).forEach(([param, newVal]) => {
|
||||
const oldVal = currentParams[param];
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td class="param-changed">${escHtml(param)}</td>
|
||||
<td class="param-old">${oldVal !== undefined ? oldVal : '—'}</td>
|
||||
<td class="param-new">${newVal}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Log ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function addLog(level, msg) {
|
||||
const feed = document.getElementById('log-feed');
|
||||
const line = document.createElement('div');
|
||||
line.className = `log-line log-${level}`;
|
||||
const ts = new Date().toLocaleTimeString('en-GB', { hour12: false });
|
||||
line.textContent = `[${ts}] ${msg}`;
|
||||
feed.insertBefore(line, feed.firstChild);
|
||||
while (feed.children.length > 200) feed.removeChild(feed.lastChild);
|
||||
}
|
||||
|
||||
// ── Candidates ─────────────────────────────────────────────────────────────
|
||||
|
||||
function addCandidate(d) {
|
||||
candidatesCount++;
|
||||
const list = document.getElementById('candidates-list');
|
||||
const empty = list.querySelector('.cand-empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
const deltaClass = d.delta >= 0 ? 'delta-up' : 'delta-dn';
|
||||
const deltaSign = d.delta >= 0 ? '+' : '';
|
||||
|
||||
const chip = document.createElement('div');
|
||||
chip.className = 'candidate-chip';
|
||||
chip.innerHTML = `
|
||||
<div class="cand-score">${d.score.toFixed(4)}</div>
|
||||
<div class="cand-delta ${deltaClass}">${deltaSign}${d.delta.toFixed(4)} vs prev</div>
|
||||
<div class="cand-id">ID: ${d.candidate_id}</div>
|
||||
`;
|
||||
list.insertBefore(chip, list.firstChild);
|
||||
document.getElementById('cand-count').textContent = `${candidatesCount} candidates`;
|
||||
}
|
||||
|
||||
// ── Chart update ───────────────────────────────────────────────────────────
|
||||
|
||||
function pushChartPoint(d) {
|
||||
const labels = scoreChart.data.labels;
|
||||
labels.push(`Iter ${d.iteration}`);
|
||||
scoreChart.data.datasets[0].data.push(d.score);
|
||||
scoreChart.data.datasets[1].data.push(d.calmar > 1 ? 1 : d.calmar / 4); // normalize calmar to 0-1
|
||||
if (labels.length > 50) {
|
||||
labels.shift();
|
||||
scoreChart.data.datasets.forEach(ds => ds.data.shift());
|
||||
}
|
||||
scoreChart.update('none');
|
||||
document.getElementById('chart-badge').textContent = `Score: ${d.score.toFixed(4)}`;
|
||||
}
|
||||
|
||||
// ── Utility ────────────────────────────────────────────────────────────────
|
||||
|
||||
function escHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ── Socket events ──────────────────────────────────────────────────────────
|
||||
|
||||
socket.on('connect', () => addLog('info', 'Connected to optimizer.'));
|
||||
|
||||
socket.on('status_sync', d => {
|
||||
document.getElementById('hdr-iter').textContent = d.iteration;
|
||||
if (d.state === 'running') { setRunning(true); setDot('running', 'Running...'); }
|
||||
if (d.best_score) document.getElementById('hdr-score').textContent = d.best_score.toFixed(4);
|
||||
});
|
||||
|
||||
socket.on('status_change', d => {
|
||||
if (d.state === 'running') setDot('running', 'Running...');
|
||||
if (d.state === 'paused') setDot('paused', 'Paused');
|
||||
if (d.state === 'idle') setDot('idle', 'Ready');
|
||||
if (d.phase) setPhase(d.phase);
|
||||
});
|
||||
|
||||
socket.on('iteration_start', d => {
|
||||
document.getElementById('hdr-iter').textContent = d.iteration;
|
||||
addLog('info', `━━ Starting Iteration ${d.iteration} ━━`);
|
||||
setPhase('analyze');
|
||||
});
|
||||
|
||||
socket.on('run_started', d => {
|
||||
setPhase(d.phase === 'baseline' ? 'baseline' : 'explore');
|
||||
document.getElementById('current-run-id').textContent = d.run_id;
|
||||
addLog('info', `▶ MT5 started: ${d.run_id} [${d.period}]`);
|
||||
currentParams = d.params || {};
|
||||
});
|
||||
|
||||
socket.on('run_complete', d => {
|
||||
updateMetrics(d);
|
||||
const sign = d.score > 0 ? '✓' : '•';
|
||||
addLog(d.score > 0.3 ? 'success' : 'info',
|
||||
`${sign} ${d.run_id}: Score=${d.score} | Calmar=${d.calmar} | PF=${d.profit_factor} | DD=${d.drawdown_pct}%`
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('run_failed', d => {
|
||||
addLog('error', `✗ Run failed: ${d.run_id} — ${d.error}`);
|
||||
});
|
||||
|
||||
socket.on('finding', f => addFinding(f));
|
||||
|
||||
socket.on('hypotheses', d => {
|
||||
setPhase('explore');
|
||||
showHypotheses(d.items);
|
||||
addLog('info', `💡 ${d.items.length} hypothesis(es) proposed`);
|
||||
});
|
||||
|
||||
socket.on('hypothesis_testing', d => {
|
||||
addLog('info', `🧪 Testing H${d.idx}: ${d.desc.substring(0, 60)}...`);
|
||||
});
|
||||
|
||||
socket.on('score_update', d => {
|
||||
pushChartPoint(d);
|
||||
document.getElementById('hdr-score').textContent = d.score.toFixed(4);
|
||||
});
|
||||
|
||||
socket.on('candidate_promoted', d => {
|
||||
addCandidate(d);
|
||||
addLog('success', `🏆 Candidate promoted! Score: ${d.score}`);
|
||||
});
|
||||
|
||||
socket.on('optimization_complete', d => {
|
||||
clearInterval(timerInterval);
|
||||
setRunning(false);
|
||||
setDot('idle', 'Complete');
|
||||
addLog('success', `🏁 Done! ${d.iterations} iterations, ${d.candidates} candidate(s), best score: ${d.best_score}`);
|
||||
addLog('info', '📁 Reports saved to MT5_Optimizer\\Reports\\');
|
||||
});
|
||||
|
||||
socket.on('error', d => {
|
||||
addLog('error', `Error: ${d.msg}`);
|
||||
setDot('error', 'Error');
|
||||
});
|
||||
|
||||
socket.on('log', d => addLog(d.level, d.msg));
|
||||
|
||||
// ── Init: restore chart from history ──────────────────────────────────────
|
||||
|
||||
fetch('/api/history').then(r => r.json()).then(history => {
|
||||
history.forEach(d => pushChartPoint(d));
|
||||
});
|
||||
|
||||
fetch('/api/status').then(r => r.json()).then(d => {
|
||||
document.getElementById('hdr-iter').textContent = d.iteration;
|
||||
if (d.best_score) document.getElementById('hdr-score').textContent = d.best_score.toFixed(4);
|
||||
if (d.state === 'running') {
|
||||
setRunning(true);
|
||||
setDot('running', 'Running...');
|
||||
startTime = Date.now() - d.elapsed_s * 1000;
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MT5 EA Optimizer — Live Dashboard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- ── HEADER ── -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">⚡</span>
|
||||
<div>
|
||||
<div class="logo-title">MT5 EA Optimizer</div>
|
||||
<div class="logo-sub">LEGSTECH_EA_V2 · XAUUSD · H1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<div class="stat-pill" id="pill-state">
|
||||
<span class="dot dot-idle" id="state-dot"></span>
|
||||
<span id="state-label">Ready</span>
|
||||
</div>
|
||||
<div class="stat-pill">
|
||||
<span class="pill-label">Iteration</span>
|
||||
<span class="pill-val" id="hdr-iter">0</span>
|
||||
</div>
|
||||
<div class="stat-pill">
|
||||
<span class="pill-label">Best Score</span>
|
||||
<span class="pill-val score-val" id="hdr-score">—</span>
|
||||
</div>
|
||||
<div class="stat-pill">
|
||||
<span class="pill-label">Elapsed</span>
|
||||
<span class="pill-val" id="hdr-elapsed">0:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="btn btn-secondary" id="btn-reports" onclick="window.open('/reports','_blank')">
|
||||
📁 Reports
|
||||
</button>
|
||||
<button class="btn btn-pause hidden" id="btn-pause" onclick="pauseOptimizer()">
|
||||
⏸ Pause
|
||||
</button>
|
||||
<button class="btn btn-stop hidden" id="btn-stop" onclick="stopOptimizer()">
|
||||
⏹ Stop
|
||||
</button>
|
||||
<button class="btn btn-start" id="btn-start" onclick="startOptimizer()">
|
||||
▶ Start Optimizer
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── MAIN GRID ── -->
|
||||
<main class="main-grid">
|
||||
|
||||
<!-- LEFT COLUMN -->
|
||||
<div class="col-left">
|
||||
|
||||
<!-- Score Chart -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">📈 Score History</span>
|
||||
<span class="card-badge" id="chart-badge">Waiting...</span>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<canvas id="scoreChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Run Metrics -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">🎯 Current Run Metrics</span>
|
||||
<span class="run-id-badge" id="current-run-id">—</span>
|
||||
</div>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Net Profit</div>
|
||||
<div class="metric-val" id="m-profit">—</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Calmar Ratio</div>
|
||||
<div class="metric-val" id="m-calmar">—</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Profit Factor</div>
|
||||
<div class="metric-val" id="m-pf">—</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Max Drawdown</div>
|
||||
<div class="metric-val" id="m-dd">—</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Win Rate</div>
|
||||
<div class="metric-val" id="m-wr">—</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Total Trades</div>
|
||||
<div class="metric-val" id="m-trades">—</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">MFE Capture</div>
|
||||
<div class="metric-val" id="m-mfe">—</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Reversal Rate</div>
|
||||
<div class="metric-val" id="m-rev">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-bar-wrap">
|
||||
<div class="score-bar-label">
|
||||
<span>Composite Score</span>
|
||||
<span class="score-big" id="m-score">—</span>
|
||||
</div>
|
||||
<div class="score-bar-track">
|
||||
<div class="score-bar-fill" id="score-bar-fill" style="width:0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div class="col-right">
|
||||
|
||||
<!-- Phase tracker -->
|
||||
<div class="card phase-card">
|
||||
<div class="phase-steps">
|
||||
<div class="phase-step" id="phase-baseline">
|
||||
<div class="phase-dot"></div>
|
||||
<div class="phase-name">Baseline</div>
|
||||
</div>
|
||||
<div class="phase-line"></div>
|
||||
<div class="phase-step" id="phase-analyze">
|
||||
<div class="phase-dot"></div>
|
||||
<div class="phase-name">Analyze</div>
|
||||
</div>
|
||||
<div class="phase-line"></div>
|
||||
<div class="phase-step" id="phase-explore">
|
||||
<div class="phase-dot"></div>
|
||||
<div class="phase-name">Test</div>
|
||||
</div>
|
||||
<div class="phase-line"></div>
|
||||
<div class="phase-step" id="phase-validate">
|
||||
<div class="phase-dot"></div>
|
||||
<div class="phase-name">Validate</div>
|
||||
</div>
|
||||
<div class="phase-line"></div>
|
||||
<div class="phase-step" id="phase-oos">
|
||||
<div class="phase-dot"></div>
|
||||
<div class="phase-name">OOS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Findings feed -->
|
||||
<div class="card findings-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">🔍 Analysis Findings</span>
|
||||
<span class="card-badge" id="findings-count">0 findings</span>
|
||||
</div>
|
||||
<div class="findings-feed" id="findings-feed">
|
||||
<div class="finding-empty">Findings will appear here after each analysis run...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameter changes -->
|
||||
<div class="card params-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">🔧 Parameter Changes</span>
|
||||
<span class="card-badge" id="hyp-badge">Waiting...</span>
|
||||
</div>
|
||||
<div class="hyp-desc" id="hyp-desc">No hypothesis being tested yet.</div>
|
||||
<div class="params-table-wrap" id="params-table-wrap">
|
||||
<table class="params-table">
|
||||
<thead><tr><th>Parameter</th><th>Before</th><th>After</th></tr></thead>
|
||||
<tbody id="params-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live log -->
|
||||
<div class="card log-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">📋 Live Log</span>
|
||||
<button class="clear-btn" onclick="clearLog()">Clear</button>
|
||||
</div>
|
||||
<div class="log-feed" id="log-feed">
|
||||
<div class="log-line log-info">System ready. Click Start Optimizer to begin.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── CANDIDATES PANEL ── -->
|
||||
<section class="candidates-section">
|
||||
<div class="card candidates-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">🏆 Promoted Candidates</span>
|
||||
<span class="card-badge" id="cand-count">0 candidates</span>
|
||||
</div>
|
||||
<div class="candidates-list" id="candidates-list">
|
||||
<div class="cand-empty">Validated candidates will appear here...</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── SCRIPTS ── -->
|
||||
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,192 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Run {{ run_id }} — MT5 Optimizer Report</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #0a0d14; color: #e2e8f8; font-family: 'Inter', sans-serif; font-size: 13px; padding: 32px; }
|
||||
h1 { font-size: 22px; font-weight: 800; margin-bottom: 4px; }
|
||||
.sub { color: #6b7fa3; font-size: 12px; font-family: 'JetBrains Mono', monospace; margin-bottom: 24px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 28px; }
|
||||
.card { background: #111621; border: 1px solid #1e2740; border-radius: 10px; padding: 14px 18px; }
|
||||
.card-label { font-size: 10px; color: #6b7fa3; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
||||
.card-val { font-size: 24px; font-weight: 700; font-family: 'JetBrains Mono', monospace; }
|
||||
.green { color: #22d3a5; } .red { color: #f44; } .blue { color: #4f8ef7; }
|
||||
.section-title { font-size: 14px; font-weight: 700; margin: 24px 0 12px; border-left: 3px solid #4f8ef7; padding-left: 10px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 24px; }
|
||||
th { text-align: left; padding: 8px 12px; color: #6b7fa3; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid #1e2740; }
|
||||
td { padding: 8px 12px; border-bottom: 1px solid rgba(30,39,64,0.4); font-family: 'JetBrains Mono', monospace; }
|
||||
tr:hover td { background: rgba(79,142,247,0.04); }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; }
|
||||
.high { background: rgba(244,68,68,0.15); color: #f44; }
|
||||
.medium { background: rgba(251,191,36,0.15); color: #fbbf24; }
|
||||
.low { background: rgba(79,142,247,0.1); color: #4f8ef7; }
|
||||
.win { color: #22d3a5; } .loss { color: #f44; } .reversal { color: #fbbf24; }
|
||||
.score-big { font-size: 48px; font-weight: 800; color: #22d3a5; text-align: center; padding: 20px 0; }
|
||||
.back-link { display: inline-block; margin-bottom: 20px; color: #4f8ef7; text-decoration: none; font-size: 12px; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="back-link" href="javascript:history.back()">← Back to Dashboard</a>
|
||||
<h1>Run Report: {{ run_id }}</h1>
|
||||
<div class="sub">Generated: {{ generated }} · Phase: {{ summary.phase }}</div>
|
||||
|
||||
<!-- Score -->
|
||||
<div class="score-big">{{ "%.4f"|format(summary.score) }}</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-label">Net Profit</div>
|
||||
<div class="card-val {% if metrics.net_profit >= 0 %}green{% else %}red{% endif %}">
|
||||
${{ "%.2f"|format(metrics.net_profit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Calmar Ratio</div>
|
||||
<div class="card-val blue">{{ "%.3f"|format(metrics.calmar_ratio) }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Profit Factor</div>
|
||||
<div class="card-val {% if metrics.profit_factor >= 1.3 %}green{% else %}red{% endif %}">
|
||||
{{ "%.3f"|format(metrics.profit_factor) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Max Drawdown</div>
|
||||
<div class="card-val red">{{ "%.1f"|format(metrics.max_drawdown_pct * 100) }}%</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Win Rate</div>
|
||||
<div class="card-val">{{ "%.1f"|format(metrics.win_rate * 100) }}%</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Total Trades</div>
|
||||
<div class="card-val">{{ metrics.total_trades }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Sharpe Ratio</div>
|
||||
<div class="card-val">{{ "%.3f"|format(metrics.sharpe_ratio) }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Recovery Factor</div>
|
||||
<div class="card-val">{{ "%.2f"|format(metrics.recovery_factor) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if metrics.avg_mfe_capture is not none %}
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-label">MFE Capture</div>
|
||||
<div class="card-val green">{{ "%.1f"|format(metrics.avg_mfe_capture * 100) }}%</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Reversal Rate</div>
|
||||
<div class="card-val {% if (metrics.reversal_rate or 0) > 0.2 %}red{% else %}green{% endif %}">
|
||||
{{ "%.1f"|format((metrics.reversal_rate or 0) * 100) }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Avg MFE Pips</div>
|
||||
<div class="card-val">{{ "%.1f"|format(metrics.avg_mfe_pips or 0) }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">Avg MAE Pips</div>
|
||||
<div class="card-val">{{ "%.1f"|format(metrics.avg_mae_pips or 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Score vs Baseline -->
|
||||
{% if baseline_score %}
|
||||
<div class="card" style="margin-bottom:24px">
|
||||
<div class="card-label">Score vs Baseline</div>
|
||||
<div style="font-size:18px;font-weight:700;font-family:'JetBrains Mono',monospace;margin-top:6px">
|
||||
{{ "%.4f"|format(baseline_score) }} →
|
||||
<span class="{% if summary.score > baseline_score %}green{% else %}red{% endif %}">
|
||||
{{ "%.4f"|format(summary.score) }}
|
||||
({{ "%+.4f"|format(summary.score - baseline_score) }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Hypothesis info -->
|
||||
{% if hypothesis %}
|
||||
<div class="section-title">Hypothesis</div>
|
||||
<div class="card" style="margin-bottom:24px">
|
||||
<div style="margin-bottom:8px;font-weight:600">{{ hypothesis.description }}</div>
|
||||
<div style="color:#6b7fa3;font-size:11px">Strategy: {{ hypothesis.strategy }} · Rule: {{ hypothesis.kb_rule_id }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Findings -->
|
||||
{% if findings %}
|
||||
<div class="section-title">Analysis Findings ({{ findings|length }})</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Analyzer</th><th>Severity</th><th>Confidence</th><th>Finding</th><th>Est. Impact</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in findings %}
|
||||
<tr>
|
||||
<td>{{ f.analyzer }}</td>
|
||||
<td><span class="badge {{ f.severity }}">{{ f.severity.upper() }}</span></td>
|
||||
<td>{{ "%.0f"|format(f.confidence * 100) }}%</td>
|
||||
<td style="max-width:400px;white-space:normal;font-family:'Inter',sans-serif">{{ f.description }}</td>
|
||||
<td>${{ "%.0f"|format(f.impact_estimate_pnl) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<!-- Parameters -->
|
||||
{% if params %}
|
||||
<div class="section-title">Parameter Configuration</div>
|
||||
<table>
|
||||
<thead><tr><th>Parameter</th><th>Value</th><th>Changed</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in params %}
|
||||
<tr>
|
||||
<td {% if p.changed %}class="green"{% endif %}>{{ p.name }}</td>
|
||||
<td {% if p.changed %}class="green"{% endif %}>{{ p.new }}</td>
|
||||
<td>{% if p.changed %}✅ {{ p.old }} → {{ p.new }}{% else %}—{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<!-- Trade sample -->
|
||||
{% if trades_sample %}
|
||||
<div class="section-title">Trade Sample (first 50)</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>#</th><th>Open</th><th>Close</th><th>Dir</th><th>Net $</th><th>MFE pip</th><th>MAE pip</th><th>Session</th><th>Result</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in trades_sample %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ t.open_time }}</td>
|
||||
<td>{{ t.close_time }}</td>
|
||||
<td>{{ t.direction }}</td>
|
||||
<td class="{% if t.net_money >= 0 %}win{% else %}loss{% endif %}">
|
||||
${{ "%.2f"|format(t.net_money) }}
|
||||
</td>
|
||||
<td>{{ t.mfe_pips or '—' }}</td>
|
||||
<td>{{ t.mae_pips or '—' }}</td>
|
||||
<td>{{ t.session or '—' }}</td>
|
||||
<td class="{{ t.result_class or '' }}">{{ t.result_class or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Reports — MT5 Optimizer</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #0a0d14; color: #e2e8f8; font-family: 'Inter', sans-serif; font-size: 13px; padding: 32px; }
|
||||
header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; }
|
||||
h1 { font-size: 22px; font-weight: 800; }
|
||||
.sub { color: #6b7fa3; font-size: 12px; margin-top: 4px; }
|
||||
.back-link { color: #4f8ef7; text-decoration: none; font-size: 12px; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
.empty { color: #3d4f70; text-align: center; padding: 80px 0; font-size: 16px; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
|
||||
.run-card {
|
||||
background: #111621; border: 1px solid #1e2740; border-radius: 12px; padding: 18px 20px;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
text-decoration: none; color: inherit; display: block;
|
||||
}
|
||||
.run-card:hover { border-color: #4f8ef7; transform: translateY(-2px); }
|
||||
.run-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
||||
.run-id { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #6b7fa3; }
|
||||
.run-score { font-size: 28px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: #22d3a5; }
|
||||
.run-phase { font-size: 10px; padding: 2px 8px; border-radius: 4px; background: rgba(79,142,247,0.15); color: #4f8ef7; font-weight: 700; text-transform: uppercase; }
|
||||
.metrics-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 10px; }
|
||||
.m { text-align: center; background: #161c2a; border-radius: 6px; padding: 6px 8px; }
|
||||
.m-label { font-size: 9px; color: #6b7fa3; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.m-val { font-size: 14px; font-weight: 700; font-family: 'JetBrains Mono', monospace; margin-top: 2px; }
|
||||
.delta-up { color: #22d3a5; } .delta-dn { color: #f44; }
|
||||
.run-ts { font-size: 10px; color: #3d4f70; margin-top: 10px; font-family: 'JetBrains Mono', monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<h1>📁 Optimization Reports</h1>
|
||||
<div class="sub">{{ runs|length }} run(s) recorded · Click any card to open the full report</div>
|
||||
</div>
|
||||
<a class="back-link" href="/">← Back to Dashboard</a>
|
||||
</header>
|
||||
|
||||
{% if not runs %}
|
||||
<div class="empty">No reports yet.<br>Start the optimizer to generate your first run report.</div>
|
||||
{% else %}
|
||||
<div class="cards">
|
||||
{% for r in runs %}
|
||||
<a class="run-card" href="/reports/{{ r.run_id }}/summary.html" target="_blank">
|
||||
<div class="run-header">
|
||||
<div>
|
||||
<div class="run-id">{{ r.run_id }}</div>
|
||||
<div class="run-score">{{ "%.4f"|format(r.score) }}</div>
|
||||
</div>
|
||||
<span class="run-phase">{{ r.phase }}</span>
|
||||
</div>
|
||||
<div class="metrics-row">
|
||||
<div class="m">
|
||||
<div class="m-label">Net Profit</div>
|
||||
<div class="m-val {% if r.net_profit >= 0 %}delta-up{% else %}delta-dn{% endif %}">
|
||||
${{ "%.0f"|format(r.net_profit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="m">
|
||||
<div class="m-label">Calmar</div>
|
||||
<div class="m-val">{{ "%.2f"|format(r.calmar) }}</div>
|
||||
</div>
|
||||
<div class="m">
|
||||
<div class="m-label">Drawdown</div>
|
||||
<div class="m-val delta-dn">{{ "%.1f"|format(r.drawdown_pct) }}%</div>
|
||||
</div>
|
||||
<div class="m">
|
||||
<div class="m-label">PF</div>
|
||||
<div class="m-val">{{ "%.2f"|format(r.profit_factor) }}</div>
|
||||
</div>
|
||||
<div class="m">
|
||||
<div class="m-label">Trades</div>
|
||||
<div class="m-val">{{ r.total_trades }}</div>
|
||||
</div>
|
||||
<div class="m">
|
||||
<div class="m-label">Score Δ</div>
|
||||
<div class="m-val {% if r.score_delta >= 0 %}delta-up{% else %}delta-dn{% endif %}">
|
||||
{{ "%+.4f"|format(r.score_delta) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="run-ts">{{ r.ts[:19].replace("T"," ") }} UTC</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user