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:
LEGSTECH Optimizer
2026-04-13 02:28:09 +00:00
commit 7a3e13a734
40 changed files with 7182 additions and 0 deletions
+318
View File
@@ -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); }
}
+374
View File
@@ -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, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ── 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);
}
});
+218
View File
@@ -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>
+192
View File
@@ -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>
+93
View File
@@ -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>