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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user