feat: Smart Autonomous 3-Phase Optimizer — complete redesign
PROBLEM: Old system repeated identical params, all scores flat at 0.2500,
user had zero control over symbol/TF/dates. Not smart, not dynamic.
NEW ARCHITECTURE:
optimizer/ (NEW package)
├── __init__.py
├── session_config.py User choices (EA, symbol, TF, dates, budget, objective)
├── lhs_sampler.py Latin Hypercube Sampling — diverse exploration
├── result_ranker.py Relative scoring (best in session=1.0, worst=0.0)
├── budget.py Time budget tracker
└── pipeline.py 3-phase orchestrator
Phase 1 — Broad Discovery (LHS, 20-26 runs):
Samples FULL parameter space, not just defaults±tiny step
LHS guarantees coverage: all 17 optimizable LEGSTECH params explored
Relative ranking: profitable configs float top, losers score 0
Phase 2 — Refinement (9 runs):
Neighbor search around top 3 configs at ±20% range (not ±0.5 step)
Keeps best of Phase1 vs Phase2 — never regresses
Phase 3 — Validation (5 runs):
OOS backtest on unseen data period
Sensitivity test: nudge params ±20%, detect fragility
Verdict: RECOMMENDED / RISKY / NOT_RELIABLE
Output: Clean downloadable .set file via /download_set/<run_id>
UI REDESIGN:
ui/templates/landing.html New / homepage (was old dashboard)
ui/templates/setup.html New /setup — EA, symbol, TF, dates, budget, objective
ui/templates/dashboard.html Updated /dashboard with:
- 5-step phase indicator
- Real progress bar per run
- Phase 1 results table (top 5 after phase1)
- Verdict banner with download button
- No-profitable-config warning
ui/static/js/dashboard.js Handles 8 new pipeline SocketIO events
app.py New routes: /, /setup, /dashboard
/api/start accepts full SessionConfig JSON
/download_set/<id> serves optimized .set
ea/registry.py +list_all() for setup page dropdown
ui/templates/reports_index.html Back to Dashboard → /dashboard (was /)
ui/static/css/style.css +dot-warn, dot-done, profit-pos/neg, aliases
FIXES:
Score no longer flat 0.2500 (was: absolute thresholds on losing EA)
User now controls: symbol, timeframe, dates, budget, objective
Parameters now span full range (was: tiny step from defaults)
Verdict is actionable: RECOMMENDED / RISKY / NOT_RELIABLE with reason
TESTED:
8/8 pre-flight checks pass
Browser test: landing ✓, setup form ✓, /dashboard ✓,
phase indicator active ✓, /reports ✓, back link ✓
This commit is contained in:
@@ -71,8 +71,16 @@ body {
|
||||
.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-warn { background: var(--yellow); box-shadow: 0 0 6px var(--yellow); }
|
||||
.dot-done { background: var(--green); }
|
||||
.dot-error { background: var(--red); }
|
||||
|
||||
/* Alias for dashboard pipeline inline styles */
|
||||
:root { --card-bg: var(--bg-card); --border-color: var(--border); }
|
||||
.profit-pos { color: var(--green); }
|
||||
.profit-neg { color: var(--red); }
|
||||
.log-warning { color: var(--yellow); }
|
||||
|
||||
@keyframes pulse {
|
||||
0%,100% { opacity: 1; } 50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
+389
-310
@@ -1,387 +1,466 @@
|
||||
/* dashboard.js — Real-time MT5 Optimizer Dashboard */
|
||||
/**
|
||||
* dashboard.js — Smart Optimizer Live Dashboard
|
||||
* Handles all SocketIO events from the 3-phase pipeline.
|
||||
*/
|
||||
|
||||
// ── 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, {
|
||||
// ── Chart Setup ───────────────────────────────────────────────────────────────
|
||||
const ctx = document.getElementById('scoreChart').getContext('2d');
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Composite Score',
|
||||
label: 'Session Score',
|
||||
data: [],
|
||||
borderColor: '#22d3a5',
|
||||
backgroundColor: 'rgba(34,211,165,0.08)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#22d3a5',
|
||||
borderColor: '#00d4aa',
|
||||
backgroundColor: 'rgba(0,212,170,0.08)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointBackgroundColor: ctx => {
|
||||
const v = ctx.raw;
|
||||
return (v !== null && v > 0) ? '#00d4aa' : '#ef4444';
|
||||
},
|
||||
tension: 0.35,
|
||||
fill: true,
|
||||
},
|
||||
{
|
||||
label: 'Calmar Ratio',
|
||||
data: [],
|
||||
borderColor: '#4f8ef7',
|
||||
backgroundColor: 'rgba(79,142,247,0.05)',
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
pointRadius: 3,
|
||||
borderColor: '#7c6dfa',
|
||||
borderWidth: 1.5,
|
||||
borderDash: [4, 2],
|
||||
},
|
||||
pointRadius: 2,
|
||||
borderDash: [4, 3],
|
||||
tension: 0.35,
|
||||
fill: false,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 500 },
|
||||
animation: { duration: 400 },
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: '#6b7fa3', font: { size: 11, family: 'Inter' } }
|
||||
labels: { color: '#94a3b8', font: { family: 'Inter', size: 11 }, boxWidth: 12 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: '#111621',
|
||||
borderColor: '#2a3a6e',
|
||||
backgroundColor: '#141b27',
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
borderWidth: 1,
|
||||
titleColor: '#e2e8f8',
|
||||
bodyColor: '#6b7fa3',
|
||||
titleColor: '#e2e8f0',
|
||||
bodyColor: '#94a3b8',
|
||||
callbacks: {
|
||||
label: ctx => {
|
||||
if (ctx.dataset.label === 'Session Score') return ` Score: ${(ctx.raw || 0).toFixed(4)}`;
|
||||
return ` Calmar: ${(ctx.raw || 0).toFixed(3)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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,
|
||||
}
|
||||
x: { ticks: { color: '#475569', font: { size: 10 }, maxTicksLimit: 12 }, grid: { color: 'rgba(255,255,255,0.04)' } },
|
||||
y: { min: 0, max: 1, ticks: { color: '#475569', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,0.04)' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Controls ───────────────────────────────────────────────────────────────
|
||||
// ── State ─────────────────────────────────────────────────────────────────────
|
||||
let startTs = null;
|
||||
let elapsedTimer = null;
|
||||
let findingsCount = 0;
|
||||
let runCount = 0;
|
||||
let bestScore = 0;
|
||||
let candCount = 0;
|
||||
let totalRuns = 0;
|
||||
|
||||
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...');
|
||||
}
|
||||
// ── SocketIO ──────────────────────────────────────────────────────────────────
|
||||
const socket = io();
|
||||
|
||||
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...');
|
||||
});
|
||||
}
|
||||
socket.on('connect', () => addLog('info', '🔗 Connected to optimizer server'));
|
||||
socket.on('disconnect', () => addLog('warning', '⚡ Disconnected from server'));
|
||||
|
||||
// Status sync on connect
|
||||
socket.on('status_sync', (s) => {
|
||||
setStatus(s.state, s.phase);
|
||||
if (s.ea_name && s.symbol && s.timeframe) {
|
||||
document.getElementById('session-label').textContent =
|
||||
`${s.ea_name} · ${s.symbol} · ${s.timeframe}`;
|
||||
}
|
||||
totalRuns = s.total_runs || 0;
|
||||
updateRunCount(s.run_count || 0);
|
||||
if (s.state === 'running') {
|
||||
document.getElementById('progress-wrap').style.display = '';
|
||||
document.getElementById('btn-stop').classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('status_change', (d) => {
|
||||
setStatus(d.state, d.phase);
|
||||
if (d.state === 'running') {
|
||||
startTs = Date.now();
|
||||
startElapsedTimer();
|
||||
document.getElementById('progress-wrap').style.display = '';
|
||||
document.getElementById('btn-stop').classList.remove('hidden');
|
||||
}
|
||||
if (d.state === 'idle' || d.state === 'stopping') {
|
||||
document.getElementById('btn-stop').classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Each run completes
|
||||
socket.on('run_complete', (d) => {
|
||||
updateRunCount((d.run_number != null) ? null : runCount + 1, d);
|
||||
|
||||
// Update metrics
|
||||
const profit = d.net_profit || 0;
|
||||
el('m-profit').textContent = `$${profit.toFixed(0)}`;
|
||||
el('m-profit').className = 'metric-val ' + (profit >= 0 ? 'profit-pos' : 'profit-neg');
|
||||
el('m-calmar').textContent = (d.calmar || 0).toFixed(3);
|
||||
el('m-pf').textContent = d.profit_factor != null ? (d.profit_factor).toFixed(2) : '—';
|
||||
el('m-dd').textContent = d.max_drawdown != null ? `${d.max_drawdown.toFixed(1)}%` : '—';
|
||||
el('m-wr').textContent = d.win_rate != null ? `${d.win_rate.toFixed(1)}%` : '—';
|
||||
el('m-trades').textContent = d.total_trades != null ? d.total_trades : '—';
|
||||
el('current-run-id').textContent = d.run_id || '—';
|
||||
|
||||
// Session score (0–1)
|
||||
const score = d.score || 0;
|
||||
const scoreStr = score.toFixed(4);
|
||||
el('m-score').textContent = d.passing ? scoreStr : 'failing';
|
||||
el('score-bar-fill').style.width = `${Math.round(score * 100)}%`;
|
||||
|
||||
// Update best score header
|
||||
if (d.passing && score > bestScore) {
|
||||
bestScore = score;
|
||||
el('hdr-score').textContent = scoreStr;
|
||||
}
|
||||
|
||||
// Add to chart
|
||||
const label = d.run_id ? d.run_id.slice(-6) : `${runCount}`;
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(d.passing ? score : 0);
|
||||
|
||||
// Calmar normalized to 0-1 range (cap at 3)
|
||||
const calmarNorm = Math.min(Math.max((d.calmar || 0) / 3.0, 0), 1);
|
||||
chart.data.datasets[1].data.push(d.passing ? calmarNorm : 0);
|
||||
|
||||
chart.update('none');
|
||||
el('chart-badge').textContent = `${chart.data.labels.length} runs`;
|
||||
|
||||
// Progress bar
|
||||
if (d.progress_pct != null) {
|
||||
el('progress-bar-fill').style.width = `${d.progress_pct}%`;
|
||||
}
|
||||
if (d.phase) updatePhaseLabel(d.phase, d.run_number, d.total);
|
||||
if (d.budget_summary) el('progress-budget').textContent = d.budget_summary;
|
||||
|
||||
// Add to best configs panel if profitable
|
||||
if (d.passing) {
|
||||
addCandidate(d);
|
||||
}
|
||||
});
|
||||
|
||||
// Phase start
|
||||
socket.on('phase_start', (d) => {
|
||||
const labels = { phase1: 'Phase 1: Broad Discovery', phase2: 'Phase 2: Refinement', phase3: 'Phase 3: Validation' };
|
||||
el('progress-phase').textContent = labels[d.phase] || d.phase;
|
||||
el('progress-count').textContent = `0 / ${d.total || '?'}`;
|
||||
setPhaseActive(d.phase);
|
||||
if (d.phase === 'phase2') {
|
||||
markPhaseDone('phase1');
|
||||
el('phase1-results-card').style.display = '';
|
||||
}
|
||||
if (d.phase === 'phase3') {
|
||||
markPhaseDone('phase2');
|
||||
}
|
||||
});
|
||||
|
||||
// Phase 1 complete — show results table
|
||||
socket.on('phase1_complete', (d) => {
|
||||
el('phase1-results-card').style.display = '';
|
||||
el('phase1-badge').textContent = `${d.n_passing}/${d.total_tested} profitable`;
|
||||
renderPhase1Table(d.top_results || []);
|
||||
addLog('info', `━━ Phase 1 done: ${d.n_passing}/${d.total_tested} profitable configs found. Starting refinement...`);
|
||||
});
|
||||
|
||||
// Phase 2 complete
|
||||
socket.on('phase2_complete', (d) => {
|
||||
addLog('success', `✅ Phase 2 done. Best: ${d.best_run_id} · Profit: $${d.best_profit} · Calmar: ${d.best_calmar}`);
|
||||
});
|
||||
|
||||
// No profitable configuration found
|
||||
socket.on('no_profitable_config', (d) => {
|
||||
el('no-profit-banner').style.display = '';
|
||||
el('no-profit-msg').textContent = d.msg || 'No profitable configuration found.';
|
||||
el('progress-wrap').style.display = 'none';
|
||||
addLog('error', '❌ ' + (d.msg || 'No profitable config found'));
|
||||
});
|
||||
|
||||
// Optimization complete — show verdict
|
||||
socket.on('optimization_complete', (d) => {
|
||||
markPhaseDone('phase3');
|
||||
markPhaseDone('done');
|
||||
setStatus('idle', 'done');
|
||||
stopElapsedTimer();
|
||||
|
||||
el('progress-wrap').style.display = 'none';
|
||||
el('btn-stop').classList.add('hidden');
|
||||
|
||||
renderVerdict(d);
|
||||
addLog(d.verdict === 'RECOMMENDED' ? 'success' : 'warning',
|
||||
`🏁 Optimization complete! ${d.verdict} · `+
|
||||
`Profit: $${d.net_profit} · Calmar: ${d.calmar} · Runs: ${d.total_runs} · ${d.elapsed_min}min`
|
||||
);
|
||||
});
|
||||
|
||||
// Log
|
||||
socket.on('log', (d) => addLog(d.level, d.msg));
|
||||
|
||||
// Error
|
||||
socket.on('error', (d) => addLog('error', '❌ ' + (d.msg || 'Unknown error')));
|
||||
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function stopOptimizer() {
|
||||
fetch('/api/stop', { method: 'POST' });
|
||||
clearInterval(timerInterval);
|
||||
setRunning(false);
|
||||
setDot('idle', 'Stopped');
|
||||
addLog('warn', 'Optimizer stopped by user.');
|
||||
fetch('/api/stop', { method: 'POST' }).catch(() => {});
|
||||
addLog('warning', '⏹ Stop requested...');
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('log-feed').innerHTML = '';
|
||||
el('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 ──────────────────────────────────────────────────────────────────
|
||||
// ── Rendering helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
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');
|
||||
function renderPhase1Table(results) {
|
||||
const tbody = el('phase1-tbody');
|
||||
tbody.innerHTML = '';
|
||||
Object.entries(h.delta || {}).forEach(([param, newVal]) => {
|
||||
const oldVal = currentParams[param];
|
||||
results.forEach((r, i) => {
|
||||
const tr = document.createElement('tr');
|
||||
if (i === 0) tr.className = 'rank-1';
|
||||
const profitClass = r.net_profit >= 0 ? 'profit-pos' : 'profit-neg';
|
||||
const badge = r.passing
|
||||
? `<span class="badge-pass">✓ pass</span>`
|
||||
: `<span class="badge-fail">fail</span>`;
|
||||
tr.innerHTML = `
|
||||
<td class="param-changed">${escHtml(param)}</td>
|
||||
<td class="param-old">${oldVal !== undefined ? oldVal : '—'}</td>
|
||||
<td class="param-new">${newVal}</td>
|
||||
<td>#${r.rank || i+1} ${i === 0 ? '🥇' : ''}</td>
|
||||
<td class="${profitClass}">$${(r.net_profit||0).toFixed(0)}</td>
|
||||
<td>${(r.calmar||0).toFixed(3)}</td>
|
||||
<td>${(r.profit_factor||0).toFixed(2)}</td>
|
||||
<td>${(r.win_rate||0).toFixed(1)}%</td>
|
||||
<td>${(r.max_drawdown||0).toFixed(1)}%</td>
|
||||
<td>${r.total_trades||0}</td>
|
||||
<td>${(r.score||0).toFixed(4)}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Log ────────────────────────────────────────────────────────────────────
|
||||
function renderVerdict(d) {
|
||||
const banner = el('verdict-banner');
|
||||
const icons = { RECOMMENDED: '✅', RISKY: '⚠️', NOT_RELIABLE: '❌' };
|
||||
const labels = {
|
||||
RECOMMENDED: 'Recommended',
|
||||
RISKY: 'Risky',
|
||||
NOT_RELIABLE: 'Not Reliable',
|
||||
};
|
||||
const subs = {
|
||||
RECOMMENDED: 'Consistent returns with controlled drawdown. Ready to deploy.',
|
||||
RISKY: 'Profitable in-sample but shows signs of fragility or OOS degradation. Use with caution.',
|
||||
NOT_RELIABLE: 'Results are inconsistent or not profitable enough. Consider different settings.',
|
||||
};
|
||||
|
||||
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);
|
||||
const profitClass = d.net_profit >= 0 ? 'profit-pos' : 'profit-neg';
|
||||
const oos = d.oos_profit != null
|
||||
? `<div class="verdict-metric"><div class="verdict-metric-val ${d.oos_profit>=0?'profit-pos':'profit-neg'}" >$${d.oos_profit.toFixed(0)}</div><div class="verdict-metric-lbl">OOS Profit</div></div>`
|
||||
: '';
|
||||
|
||||
banner.className = `verdict-banner ${d.verdict}`;
|
||||
banner.innerHTML = `
|
||||
<div class="verdict-header verdict-${d.verdict}">
|
||||
<div class="verdict-icon">${icons[d.verdict] || '?'}</div>
|
||||
<div>
|
||||
<div class="verdict-title">${labels[d.verdict] || d.verdict}</div>
|
||||
<div class="verdict-sub">${subs[d.verdict] || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verdict-metrics">
|
||||
<div class="verdict-metric">
|
||||
<div class="verdict-metric-val ${profitClass}">$${(d.net_profit||0).toFixed(0)}</div>
|
||||
<div class="verdict-metric-lbl">Net Profit</div>
|
||||
</div>
|
||||
<div class="verdict-metric">
|
||||
<div class="verdict-metric-val">${(d.calmar||0).toFixed(2)}</div>
|
||||
<div class="verdict-metric-lbl">Calmar</div>
|
||||
</div>
|
||||
<div class="verdict-metric">
|
||||
<div class="verdict-metric-val">${(d.win_rate||0).toFixed(1)}%</div>
|
||||
<div class="verdict-metric-lbl">Win Rate</div>
|
||||
</div>
|
||||
<div class="verdict-metric">
|
||||
<div class="verdict-metric-val">${(d.max_drawdown||0).toFixed(1)}%</div>
|
||||
<div class="verdict-metric-lbl">Max DD</div>
|
||||
</div>
|
||||
${oos}
|
||||
</div>
|
||||
<div class="verdict-actions">
|
||||
${d.set_file_url ? `<a href="${d.set_file_url}" class="btn-download" download>⬇ Download .set File</a>` : ''}
|
||||
<a href="/reports" class="btn-new-run">📁 View Full Report</a>
|
||||
<a href="/setup" class="btn-new-run">⚡ New Optimization</a>
|
||||
</div>
|
||||
`;
|
||||
banner.style.display = '';
|
||||
}
|
||||
|
||||
// ── Candidates ─────────────────────────────────────────────────────────────
|
||||
|
||||
function addCandidate(d) {
|
||||
candidatesCount++;
|
||||
const list = document.getElementById('candidates-list');
|
||||
const list = el('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 ? '+' : '';
|
||||
candCount++;
|
||||
el('cand-count').textContent = `${candCount} config${candCount !== 1 ? 's' : ''}`;
|
||||
|
||||
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>
|
||||
// Keep only top 10
|
||||
if (list.children.length >= 10) {
|
||||
list.removeChild(list.lastChild);
|
||||
}
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'cand-card';
|
||||
const profitClass = (d.net_profit||0) >= 0 ? 'profit-pos' : 'profit-neg';
|
||||
card.innerHTML = `
|
||||
<div class="cand-header">
|
||||
<span class="cand-rank">${d.phase?.toUpperCase() || 'RUN'} · Score ${(d.score||0).toFixed(4)}</span>
|
||||
<span class="cand-run-id">${d.run_id}</span>
|
||||
</div>
|
||||
<div class="cand-score-row">
|
||||
<div class="cand-metric">
|
||||
<div class="cand-metric-lbl">Profit</div>
|
||||
<div class="cand-metric-val ${profitClass}">$${(d.net_profit||0).toFixed(0)}</div>
|
||||
</div>
|
||||
<div class="cand-metric">
|
||||
<div class="cand-metric-lbl">Calmar</div>
|
||||
<div class="cand-metric-val">${(d.calmar||0).toFixed(3)}</div>
|
||||
</div>
|
||||
<div class="cand-metric">
|
||||
<div class="cand-metric-lbl">Win%</div>
|
||||
<div class="cand-metric-val">${(d.win_rate||0).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div class="cand-metric">
|
||||
<div class="cand-metric-lbl">DD%</div>
|
||||
<div class="cand-metric-val">${(d.max_drawdown||0).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div class="cand-metric">
|
||||
<div class="cand-metric-lbl">Trades</div>
|
||||
<div class="cand-metric-val">${d.total_trades||0}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
list.insertBefore(chip, list.firstChild);
|
||||
document.getElementById('cand-count').textContent = `${candidatesCount} candidates`;
|
||||
list.insertBefore(card, list.firstChild);
|
||||
}
|
||||
|
||||
// ── Chart update ───────────────────────────────────────────────────────────
|
||||
|
||||
function pushChartPoint(d) {
|
||||
const labels = scoreChart.data.labels;
|
||||
// Label: 'Baseline' for first point, 'Iter N · run_id' for hypothesis runs
|
||||
const lbl = d.run_id
|
||||
? (d.run_id.startsWith('baseline') ? 'Baseline' : `It${d.iteration}·${d.run_id.split('_').pop()}`)
|
||||
: `Iter ${d.iteration}`;
|
||||
labels.push(lbl);
|
||||
scoreChart.data.datasets[0].data.push(d.score);
|
||||
// Normalize calmar: clamp -0.5..2.0 → 0..1 for display
|
||||
const calmarNorm = Math.max(0, Math.min(1, (d.calmar + 0.5) / 2.5));
|
||||
scoreChart.data.datasets[1].data.push(calmarNorm);
|
||||
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)}`;
|
||||
function addLog(level, msg) {
|
||||
const feed = el('log-feed');
|
||||
const div = document.createElement('div');
|
||||
div.className = `log-line log-${level}`;
|
||||
const ts = new Date().toLocaleTimeString('en-GB', { hour12: false });
|
||||
div.textContent = `[${ts}] ${msg}`;
|
||||
feed.appendChild(div);
|
||||
feed.scrollTop = feed.scrollHeight;
|
||||
// Max 200 lines
|
||||
while (feed.children.length > 200) feed.removeChild(feed.firstChild);
|
||||
}
|
||||
|
||||
// ── Utility ────────────────────────────────────────────────────────────────
|
||||
// ── Phase indicator helpers ───────────────────────────────────────────────────
|
||||
|
||||
function escHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
function setPhaseActive(phase) {
|
||||
const map = { setup: 'phase-setup', phase1: 'phase-phase1', phase2: 'phase-phase2', phase3: 'phase-phase3' };
|
||||
const id = map[phase];
|
||||
if (!id) return;
|
||||
document.querySelectorAll('.phase-step').forEach(s => s.classList.remove('active'));
|
||||
const step = el(id);
|
||||
if (step) step.classList.add('active');
|
||||
}
|
||||
|
||||
// ── Socket events ──────────────────────────────────────────────────────────
|
||||
function markPhaseDone(phase) {
|
||||
const map = { phase1: 'phase-phase1', phase2: 'phase-phase2', phase3: 'phase-phase3', done: 'phase-done' };
|
||||
const step = el(map[phase]);
|
||||
if (step) { step.classList.remove('active'); step.classList.add('done'); }
|
||||
}
|
||||
|
||||
socket.on('connect', () => addLog('info', 'Connected to optimizer.'));
|
||||
// ── Status helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
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);
|
||||
});
|
||||
function setStatus(state, phase) {
|
||||
const dot = el('state-dot');
|
||||
const label = el('state-label');
|
||||
|
||||
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);
|
||||
});
|
||||
const states = {
|
||||
running: { cls: 'dot-running', label: phase ? `${phase.replace('phase','Phase ')}` : 'Running' },
|
||||
stopping: { cls: 'dot-warn', label: 'Stopping...' },
|
||||
idle: { cls: 'dot-idle', label: 'Ready' },
|
||||
done: { cls: 'dot-done', label: 'Complete' },
|
||||
};
|
||||
const s = states[state] || states.idle;
|
||||
dot.className = `dot ${s.cls}`;
|
||||
label.textContent = s.label;
|
||||
}
|
||||
|
||||
socket.on('iteration_start', d => {
|
||||
document.getElementById('hdr-iter').textContent = d.iteration;
|
||||
addLog('info', `━━ Starting Iteration ${d.iteration} ━━`);
|
||||
setPhase('analyze');
|
||||
});
|
||||
function updateRunCount(count, d) {
|
||||
if (count !== null) runCount = count;
|
||||
else if (d) runCount++;
|
||||
el('hdr-iter').textContent = totalRuns > 0 ? `${runCount}/${totalRuns}` : runCount;
|
||||
}
|
||||
|
||||
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 ? '\u2713' : '\u2022';
|
||||
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}%`
|
||||
);
|
||||
// Push baseline run to chart immediately (hypothesis runs pushed via score_update)
|
||||
if (d.run_id && d.run_id.startsWith('baseline')) {
|
||||
pushChartPoint({ iteration: 0, run_id: d.run_id, score: d.score, calmar: d.calmar });
|
||||
function updatePhaseLabel(phase, runNum, total) {
|
||||
const labels = { phase1: 'Phase 1: Broad Search', phase2: 'Phase 2: Refinement', phase3: 'Phase 3: Validation', phase3_oos: 'Phase 3: OOS Test', phase3_sens: 'Phase 3: Sensitivity' };
|
||||
el('progress-phase').textContent = labels[phase] || phase;
|
||||
if (runNum != null && total != null) {
|
||||
el('progress-count').textContent = `${runNum} / ${total}`;
|
||||
el('progress-bar-fill').style.width = `${Math.round(runNum / total * 100)}%`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('run_failed', d => {
|
||||
addLog('error', `✗ Run failed: ${d.run_id} — ${d.error}`);
|
||||
});
|
||||
// ── Elapsed timer ─────────────────────────────────────────────────────────────
|
||||
|
||||
socket.on('finding', f => addFinding(f));
|
||||
function startElapsedTimer() {
|
||||
if (elapsedTimer) clearInterval(elapsedTimer);
|
||||
if (!startTs) startTs = Date.now();
|
||||
elapsedTimer = setInterval(() => {
|
||||
const secs = Math.floor((Date.now() - startTs) / 1000);
|
||||
const mm = Math.floor(secs / 60).toString().padStart(2, '0');
|
||||
const ss = (secs % 60).toString().padStart(2, '0');
|
||||
el('hdr-elapsed').textContent = `${mm}:${ss}`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
socket.on('hypotheses', d => {
|
||||
setPhase('explore');
|
||||
showHypotheses(d.items);
|
||||
addLog('info', `💡 ${d.items.length} hypothesis(es) proposed`);
|
||||
});
|
||||
function stopElapsedTimer() {
|
||||
if (elapsedTimer) { clearInterval(elapsedTimer); elapsedTimer = null; }
|
||||
}
|
||||
|
||||
socket.on('hypothesis_testing', d => {
|
||||
addLog('info', `🧪 Testing H${d.idx}: ${d.desc.substring(0, 60)}...`);
|
||||
});
|
||||
// ── Utility ───────────────────────────────────────────────────────────────────
|
||||
|
||||
socket.on('score_update', d => {
|
||||
pushChartPoint(d);
|
||||
// Update best score header only when a candidate is actually promoted
|
||||
if (d.promoted) {
|
||||
document.getElementById('hdr-score').textContent = d.score.toFixed(4);
|
||||
function el(id) { return document.getElementById(id); }
|
||||
|
||||
// Init: mark Setup phase as active
|
||||
setPhaseActive('setup');
|
||||
|
||||
// Check if run already in progress
|
||||
fetch('/api/status').then(r => r.json()).then(s => {
|
||||
if (s.state === 'running') {
|
||||
setStatus(s.state, s.phase);
|
||||
startTs = Date.now() - (s.elapsed_s || 0) * 1000;
|
||||
startElapsedTimer();
|
||||
totalRuns = s.total_runs || 0;
|
||||
updateRunCount(s.run_count || 0, null);
|
||||
document.getElementById('progress-wrap').style.display = '';
|
||||
document.getElementById('btn-stop').classList.remove('hidden');
|
||||
setPhaseActive(s.phase);
|
||||
if (s.ea_name) {
|
||||
document.getElementById('session-label').textContent =
|
||||
`${s.ea_name} · ${s.symbol} · ${s.timeframe}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MT5 Smart 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">
|
||||
<style>
|
||||
/* ── Progress bar ─────────────────────────────────────── */
|
||||
.progress-wrap {
|
||||
background: var(--card-bg, #0d1117);
|
||||
border: 1px solid var(--border-color, rgba(255,255,255,0.08));
|
||||
border-radius: 14px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
|
||||
.progress-phase { font-size: 0.85rem; font-weight: 600; color: #00d4aa; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
.progress-count { font-size: 0.82rem; color: #64748b; font-family: 'JetBrains Mono', monospace; }
|
||||
.progress-bar-track { height: 6px; background: rgba(255,255,255,0.06); border-radius: 999px; overflow: hidden; }
|
||||
.progress-bar-fill { height: 100%; background: linear-gradient(90deg, #00d4aa, #7c6dfa); border-radius: 999px; transition: width 0.5s ease; width: 0%; }
|
||||
.progress-budget { margin-top: 0.5rem; font-size: 0.75rem; color: #64748b; }
|
||||
|
||||
/* ── Phase steps ──────────────────────────────────────── */
|
||||
.phase-steps { display: flex; align-items: center; gap: 0; }
|
||||
.phase-step { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; flex: 1; position: relative; }
|
||||
.phase-dot { width: 28px; height: 28px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.12); background: #0d1117; display: grid; place-items: center; font-size: 0.75rem; font-weight: 700; color: #64748b; transition: all 0.4s; z-index: 1; }
|
||||
.phase-name { font-size: 0.7rem; color: #64748b; font-weight: 500; text-align: center; }
|
||||
.phase-line { flex: 1; height: 2px; background: rgba(255,255,255,0.08); margin-top: -16px; }
|
||||
.phase-step.active .phase-dot { border-color: #00d4aa; color: #00d4aa; background: rgba(0,212,170,0.1); box-shadow: 0 0 12px rgba(0,212,170,0.3); }
|
||||
.phase-step.active .phase-name { color: #00d4aa; }
|
||||
.phase-step.done .phase-dot { border-color: #00d4aa; background: #00d4aa; color: #051a14; }
|
||||
.phase-step.done .phase-name { color: #e2e8f0; }
|
||||
|
||||
/* ── Phase 1 results table ────────────────────────────── */
|
||||
.results-table-wrap { overflow-x: auto; }
|
||||
.results-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
|
||||
.results-table th { color: #64748b; font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; padding: 0.5rem 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.07); text-align: right; }
|
||||
.results-table th:first-child { text-align: left; }
|
||||
.results-table td { padding: 0.6rem 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.04); text-align: right; }
|
||||
.results-table td:first-child { text-align: left; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; }
|
||||
.results-table tr:hover td { background: rgba(255,255,255,0.02); }
|
||||
.results-table .rank-1 td { color: #00d4aa; }
|
||||
.results-table .rank-1 td:first-child { font-weight: 600; }
|
||||
.profit-pos { color: #00d4aa; }
|
||||
.profit-neg { color: #ef4444; }
|
||||
.badge-pass { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 4px; background: rgba(0,212,170,0.15); color: #00d4aa; font-size: 0.7rem; font-weight: 600; }
|
||||
.badge-fail { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 4px; background: rgba(239,68,68,0.12); color: #ef4444; font-size: 0.7rem; }
|
||||
|
||||
/* ── Verdict banner ───────────────────────────────────── */
|
||||
.verdict-banner {
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
animation: fadeUp 0.5s ease both;
|
||||
}
|
||||
@keyframes fadeUp { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:translateY(0)} }
|
||||
.verdict-banner.RECOMMENDED { background: linear-gradient(135deg, rgba(0,212,170,0.12), rgba(0,212,170,0.04)); border: 1px solid rgba(0,212,170,0.3); }
|
||||
.verdict-banner.RISKY { background: linear-gradient(135deg, rgba(245,158,11,0.12), rgba(245,158,11,0.04)); border: 1px solid rgba(245,158,11,0.3); }
|
||||
.verdict-banner.NOT_RELIABLE{ background: linear-gradient(135deg, rgba(239,68,68,0.12), rgba(239,68,68,0.04)); border: 1px solid rgba(239,68,68,0.3); }
|
||||
.verdict-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.25rem; }
|
||||
.verdict-icon { font-size: 2.5rem; }
|
||||
.verdict-title { font-size: 1.6rem; font-weight: 800; letter-spacing: -0.03em; }
|
||||
.verdict-sub { font-size: 0.85rem; color: #94a3b8; margin-top: 0.2rem; }
|
||||
.verdict-RECOMMENDED .verdict-title { color: #00d4aa; }
|
||||
.verdict-RISKY .verdict-title { color: #f59e0b; }
|
||||
.verdict-NOT_RELIABLE .verdict-title { color: #ef4444; }
|
||||
.verdict-metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-bottom: 1.5rem; }
|
||||
.verdict-metric { background: rgba(255,255,255,0.04); border-radius: 10px; padding: 0.75rem; text-align: center; }
|
||||
.verdict-metric-val { font-size: 1.3rem; font-weight: 700; }
|
||||
.verdict-metric-lbl { font-size: 0.7rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.07em; margin-top: 0.2rem; }
|
||||
.verdict-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
||||
.btn-download {
|
||||
display: inline-flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.8rem 1.75rem; border-radius: 10px;
|
||||
background: linear-gradient(135deg, #00d4aa, #00b890);
|
||||
color: #051a14; font-weight: 700; font-family: inherit;
|
||||
font-size: 0.95rem; cursor: pointer; border: none;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 20px rgba(0,212,170,0.3);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-download:hover { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(0,212,170,0.4); }
|
||||
.btn-new-run {
|
||||
display: inline-flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.8rem 1.5rem; border-radius: 10px;
|
||||
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
|
||||
color: #e2e8f0; font-weight: 600; font-family: inherit;
|
||||
font-size: 0.9rem; cursor: pointer; text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-new-run:hover { background: rgba(255,255,255,0.1); }
|
||||
|
||||
/* ── No-profit warning ────────────────────────────────── */
|
||||
.no-profit-banner {
|
||||
background: rgba(239,68,68,0.08);
|
||||
border: 1px solid rgba(239,68,68,0.2);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
display: none;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.no-profit-banner h3 { color: #ef4444; margin-bottom: 0.5rem; }
|
||||
.no-profit-banner p { color: #94a3b8; font-size: 0.875rem; line-height: 1.6; }
|
||||
|
||||
/* ── Updated candidate card ───────────────────────────── */
|
||||
.cand-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07); border-radius: 12px; padding: 1rem; margin-bottom: 0.75rem; }
|
||||
.cand-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
|
||||
.cand-rank { font-size: 0.75rem; font-weight: 700; color: #00d4aa; }
|
||||
.cand-run-id { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: #64748b; }
|
||||
.cand-score-row { display: flex; gap: 1.5rem; font-size: 0.82rem; flex-wrap: wrap; }
|
||||
.cand-metric { display: flex; flex-direction: column; gap: 0.1rem; }
|
||||
.cand-metric-lbl { font-size: 0.68rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.cand-metric-val { font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── HEADER ── -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">⚡</span>
|
||||
<div>
|
||||
<div class="logo-title">MT5 Smart Optimizer</div>
|
||||
<div class="logo-sub" id="session-label">Live Dashboard</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">Runs</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">
|
||||
<a href="/" class="btn btn-secondary">🏠 Home</a>
|
||||
<a href="/reports" class="btn btn-secondary">📁 Reports</a>
|
||||
<button class="btn btn-stop hidden" id="btn-stop" onclick="stopOptimizer()">⏹ Stop</button>
|
||||
<a href="/setup" class="btn btn-start" id="btn-start">⚡ New Run</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── MAIN GRID ── -->
|
||||
<main class="main-grid">
|
||||
|
||||
<!-- LEFT COLUMN -->
|
||||
<div class="col-left">
|
||||
|
||||
<!-- Verdict banner (hidden until complete) -->
|
||||
<div class="verdict-banner" id="verdict-banner"></div>
|
||||
|
||||
<!-- No-profit warning (hidden until needed) -->
|
||||
<div class="no-profit-banner" id="no-profit-banner">
|
||||
<h3>⚠️ No Profitable Configuration Found</h3>
|
||||
<p id="no-profit-msg">Phase 1 found no profitable configuration. Try a different timeframe, date range, or check EA settings.</p>
|
||||
<div style="margin-top:1rem">
|
||||
<a href="/setup" class="btn-new-run">⚡ Try Again with Different Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress -->
|
||||
<div class="progress-wrap" id="progress-wrap" style="display:none">
|
||||
<div class="progress-header">
|
||||
<span class="progress-phase" id="progress-phase">Phase 1: Broad Discovery</span>
|
||||
<span class="progress-count" id="progress-count">0 / 0</span>
|
||||
</div>
|
||||
<div class="progress-bar-track">
|
||||
<div class="progress-bar-fill" id="progress-bar-fill"></div>
|
||||
</div>
|
||||
<div class="progress-budget" id="progress-budget"></div>
|
||||
</div>
|
||||
|
||||
<!-- 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">🎯 Latest 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>
|
||||
<div class="score-bar-wrap">
|
||||
<div class="score-bar-label">
|
||||
<span>Session 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 steps -->
|
||||
<div class="card phase-card">
|
||||
<div class="phase-steps">
|
||||
<div class="phase-step" id="phase-setup">
|
||||
<div class="phase-dot">1</div>
|
||||
<div class="phase-name">Setup</div>
|
||||
</div>
|
||||
<div class="phase-line"></div>
|
||||
<div class="phase-step" id="phase-phase1">
|
||||
<div class="phase-dot">2</div>
|
||||
<div class="phase-name">Broad Search</div>
|
||||
</div>
|
||||
<div class="phase-line"></div>
|
||||
<div class="phase-step" id="phase-phase2">
|
||||
<div class="phase-dot">3</div>
|
||||
<div class="phase-name">Refine</div>
|
||||
</div>
|
||||
<div class="phase-line"></div>
|
||||
<div class="phase-step" id="phase-phase3">
|
||||
<div class="phase-dot">4</div>
|
||||
<div class="phase-name">Validate</div>
|
||||
</div>
|
||||
<div class="phase-line"></div>
|
||||
<div class="phase-step" id="phase-done">
|
||||
<div class="phase-dot">✓</div>
|
||||
<div class="phase-name">Done</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 1 results table -->
|
||||
<div class="card" id="phase1-results-card" style="display:none">
|
||||
<div class="card-header">
|
||||
<span class="card-title">🏆 Phase 1 — Top Configurations</span>
|
||||
<span class="card-badge" id="phase1-badge">—</span>
|
||||
</div>
|
||||
<div class="results-table-wrap">
|
||||
<table class="results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Profit</th>
|
||||
<th>Calmar</th>
|
||||
<th>PF</th>
|
||||
<th>Win%</th>
|
||||
<th>DD%</th>
|
||||
<th>Trades</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="phase1-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. <a href="/setup" style="color:inherit;opacity:0.7">Start a new optimization →</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Best candidates so far -->
|
||||
<div class="card candidates-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">⭐ Best Configs Found</span>
|
||||
<span class="card-badge" id="cand-count">0 configs</span>
|
||||
</div>
|
||||
<div class="candidates-list" id="candidates-list">
|
||||
<div class="cand-empty">Profitable configurations will appear here as the optimizer runs...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── 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,267 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MT5 Smart Optimizer</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;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #07090f;
|
||||
--bg2: #0d1117;
|
||||
--bg3: #141b27;
|
||||
--border: rgba(255,255,255,0.07);
|
||||
--accent: #00d4aa;
|
||||
--accent2: #7c6dfa;
|
||||
--warn: #f59e0b;
|
||||
--text: #e2e8f0;
|
||||
--muted: #64748b;
|
||||
--glow: 0 0 40px rgba(0,212,170,0.15);
|
||||
}
|
||||
|
||||
html, body { height: 100%; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Animated background grid */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed; inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0,212,170,0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0,212,170,0.03) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
animation: gridScroll 20s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
@keyframes gridScroll { to { background-position: 60px 60px; } }
|
||||
|
||||
/* Glowing orbs */
|
||||
.orb {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(120px);
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
animation: orb-float 8s ease-in-out infinite;
|
||||
}
|
||||
.orb-1 { width: 600px; height: 600px; background: var(--accent); top: -200px; left: -100px; animation-delay: 0s; }
|
||||
.orb-2 { width: 500px; height: 500px; background: var(--accent2); bottom: -200px; right: -100px; animation-delay: 4s; }
|
||||
@keyframes orb-float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-30px)} }
|
||||
|
||||
.landing {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
max-width: 700px;
|
||||
padding: 2rem;
|
||||
animation: fadeUp 0.8s ease both;
|
||||
}
|
||||
@keyframes fadeUp { from{opacity:0;transform:translateY(30px)} to{opacity:1;transform:translateY(0)} }
|
||||
|
||||
.logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.logo-icon {
|
||||
width: 52px; height: 52px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
border-radius: 14px;
|
||||
display: grid; place-items: center;
|
||||
font-size: 1.6rem;
|
||||
box-shadow: var(--glow);
|
||||
}
|
||||
.logo-name {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: white;
|
||||
}
|
||||
.logo-sub { font-size: 0.75rem; color: var(--muted); margin-top: 2px; }
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 5vw, 3.5rem);
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 1.25rem;
|
||||
background: linear-gradient(135deg, #ffffff 0%, var(--accent) 60%, var(--accent2) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 3rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.9rem 2rem;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent), #00b890);
|
||||
color: #051a14;
|
||||
box-shadow: 0 4px 24px rgba(0,212,170,0.35);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 36px rgba(0,212,170,0.5);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg3);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #1e2a3f;
|
||||
border-color: rgba(255,255,255,0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
margin-top: 3.5rem;
|
||||
padding-top: 2.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.stat { text-align: center; }
|
||||
.stat-val {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 800;
|
||||
color: var(--accent);
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.stat-label { font-size: 0.75rem; color: var(--muted); margin-top: 0.2rem; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.8rem;
|
||||
background: rgba(0,212,170,0.1);
|
||||
border: 1px solid rgba(0,212,170,0.2);
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--accent);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.pulse {
|
||||
width: 7px; height: 7px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease infinite;
|
||||
}
|
||||
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(0.8)} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="orb orb-1"></div>
|
||||
<div class="orb orb-2"></div>
|
||||
|
||||
<div class="landing">
|
||||
|
||||
<div class="logo">
|
||||
<div class="logo-icon">⚡</div>
|
||||
<div>
|
||||
<div class="logo-name">MT5 Smart Optimizer</div>
|
||||
<div class="logo-sub">Autonomous EA Optimization Engine</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-pill" id="status-pill">
|
||||
<div class="pulse"></div>
|
||||
<span id="status-text">Ready</span>
|
||||
</div>
|
||||
|
||||
<h1>Find the Best Settings<br>for Any MT5 EA</h1>
|
||||
|
||||
<p class="subtitle">
|
||||
The Smart Optimizer tests dozens of configurations across your full parameter space,
|
||||
validates the winner on unseen data, and delivers a ready-to-use .set file — in under an hour.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a href="/setup" class="btn btn-primary">
|
||||
⚡ New Optimization
|
||||
</a>
|
||||
<a href="/dashboard" class="btn btn-secondary" id="dashboard-btn">
|
||||
📊 Live Dashboard
|
||||
</a>
|
||||
<a href="/reports" class="btn btn-secondary">
|
||||
📁 View Reports
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-val">3</div>
|
||||
<div class="stat-label">Phases</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-val" id="stat-runs">20–50</div>
|
||||
<div class="stat-label">Config Tests</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-val"><1hr</div>
|
||||
<div class="stat-label">Typical Time</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-val">OOS</div>
|
||||
<div class="stat-label">Validated</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check if optimizer is running
|
||||
fetch('/api/status').then(r => r.json()).then(s => {
|
||||
if (s.state === 'running') {
|
||||
document.getElementById('status-text').textContent = `Running — ${s.phase} (${s.run_count}/${s.total_runs})`;
|
||||
document.getElementById('dashboard-btn').style.background = 'linear-gradient(135deg,#7c6dfa,#5b4fe8)';
|
||||
document.getElementById('dashboard-btn').style.color = 'white';
|
||||
}
|
||||
}).catch(() => {});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,7 +38,7 @@
|
||||
<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="/" onclick="if(document.referrer){history.back();return false;}">← Back to Dashboard</a>
|
||||
<a class="back-link" href="/dashboard">← Back to Dashboard</a>
|
||||
</header>
|
||||
|
||||
{% if not runs %}
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>New Optimization — MT5 Smart Optimizer</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&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #07090f;
|
||||
--bg2: #0d1117;
|
||||
--bg3: #141b27;
|
||||
--bg4: #1a2335;
|
||||
--border: rgba(255,255,255,0.08);
|
||||
--accent: #00d4aa;
|
||||
--accent2:#7c6dfa;
|
||||
--warn: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--text: #e2e8f0;
|
||||
--muted: #64748b;
|
||||
--glow: 0 0 30px rgba(0,212,170,0.12);
|
||||
}
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1rem 4rem;
|
||||
}
|
||||
body::before {
|
||||
content:''; position:fixed; inset:0;
|
||||
background-image: linear-gradient(rgba(0,212,170,0.025) 1px,transparent 1px),
|
||||
linear-gradient(90deg,rgba(0,212,170,0.025) 1px,transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
pointer-events:none; z-index:0;
|
||||
}
|
||||
|
||||
.page { position:relative; z-index:1; max-width: 780px; margin: 0 auto; }
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
margin-bottom: 2.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.back-btn {
|
||||
display: inline-flex; align-items: center; gap: 0.4rem;
|
||||
color: var(--muted); text-decoration: none;
|
||||
font-size: 0.875rem; transition: color 0.2s;
|
||||
}
|
||||
.back-btn:hover { color: var(--text); }
|
||||
.header-title { font-size: 1.5rem; font-weight: 800; letter-spacing:-0.03em; }
|
||||
.header-sub { font-size: 0.85rem; color: var(--muted); margin-top: 0.1rem; }
|
||||
|
||||
/* Section cards */
|
||||
.section {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.section:hover { border-color: rgba(255,255,255,0.13); }
|
||||
.section-title {
|
||||
font-size: 0.8rem; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.1em;
|
||||
color: var(--accent); margin-bottom: 1.25rem;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Form grid */
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; }
|
||||
|
||||
.field { display: flex; flex-direction: column; gap: 0.45rem; }
|
||||
.field label { font-size: 0.8rem; color: var(--muted); font-weight: 500; }
|
||||
|
||||
input, select {
|
||||
background: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(0,212,170,0.12);
|
||||
}
|
||||
select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position: right 0.9rem center; padding-right: 2.2rem; cursor:pointer; }
|
||||
|
||||
/* Budget pills */
|
||||
.pill-group { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.pill {
|
||||
padding: 0.55rem 1.2rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg3);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
color: var(--muted);
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
}
|
||||
.pill:hover { border-color: var(--accent); color: var(--text); }
|
||||
.pill.active {
|
||||
background: rgba(0,212,170,0.15);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.pill .badge {
|
||||
position: absolute; top: -7px; right: -4px;
|
||||
background: var(--accent2); color: white;
|
||||
font-size: 0.6rem; padding: 0 5px; border-radius: 999px; font-weight:600;
|
||||
}
|
||||
|
||||
/* Objective cards */
|
||||
.obj-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.75rem; }
|
||||
.obj-card {
|
||||
background: var(--bg3);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
.obj-card:hover { border-color: rgba(255,255,255,0.2); }
|
||||
.obj-card.active { border-color: var(--accent); background: rgba(0,212,170,0.08); }
|
||||
.obj-icon { font-size: 1.6rem; margin-bottom: 0.4rem; }
|
||||
.obj-name { font-size: 0.85rem; font-weight: 600; }
|
||||
.obj-desc { font-size: 0.72rem; color: var(--muted); margin-top: 0.2rem; }
|
||||
|
||||
/* Advanced toggle */
|
||||
.adv-toggle {
|
||||
display: flex; align-items: center; gap: 0.6rem;
|
||||
cursor: pointer;
|
||||
color: var(--muted); font-size: 0.85rem;
|
||||
padding: 0.6rem 0;
|
||||
user-select: none;
|
||||
width: fit-content;
|
||||
}
|
||||
.adv-toggle:hover { color: var(--text); }
|
||||
.adv-toggle svg { transition: transform 0.2s; }
|
||||
.adv-toggle.open svg { transform: rotate(90deg); }
|
||||
.adv-body { display: none; margin-top: 1rem; }
|
||||
.adv-body.open { display: block; }
|
||||
|
||||
/* Param checkboxes */
|
||||
.param-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.5rem; }
|
||||
.param-check { display: flex; align-items: center; gap: 0.6rem; cursor: pointer; padding: 0.4rem 0.6rem; border-radius: 8px; transition: background 0.15s; }
|
||||
.param-check:hover { background: var(--bg4); }
|
||||
.param-check input { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; }
|
||||
.param-check span { font-size: 0.82rem; }
|
||||
|
||||
/* Submit */
|
||||
.submit-row {
|
||||
display: flex; gap: 1rem; align-items: center;
|
||||
margin-top: 2rem; padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.btn-start {
|
||||
padding: 0.95rem 2.5rem;
|
||||
background: linear-gradient(135deg, var(--accent), #00b890);
|
||||
color: #051a14;
|
||||
border: none; border-radius: 12px;
|
||||
font-size: 1rem; font-weight: 700;
|
||||
cursor: pointer; font-family: inherit;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 24px rgba(0,212,170,0.3);
|
||||
display: flex; align-items: center; gap: 0.6rem;
|
||||
}
|
||||
.btn-start:hover { transform: translateY(-2px); box-shadow: 0 8px 36px rgba(0,212,170,0.45); }
|
||||
.btn-start:active { transform: translateY(0); }
|
||||
.btn-start:disabled { opacity: 0.5; pointer-events: none; }
|
||||
|
||||
.estimate { color: var(--muted); font-size: 0.85rem; }
|
||||
.estimate strong { color: var(--accent); }
|
||||
|
||||
/* Hint row */
|
||||
.hint {
|
||||
background: rgba(0,212,170,0.06);
|
||||
border: 1px solid rgba(0,212,170,0.15);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid-2, .grid-3, .obj-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<a href="/" class="back-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M3.83 7H14a.5.5 0 010 1H3.83l3.58 3.59a.5.5 0 01-.7.71l-4.5-4.5a.5.5 0 010-.71l4.5-4.5a.5.5 0 11.7.71L3.83 7z"/></svg>
|
||||
Back
|
||||
</a>
|
||||
<div>
|
||||
<div class="header-title">⚡ New Optimization</div>
|
||||
<div class="header-sub">Configure and launch a smart EA optimization session</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="setup-form" novalidate>
|
||||
|
||||
<!-- EA Identity -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12l2 2 4-4"/></svg>
|
||||
EA Configuration
|
||||
</div>
|
||||
<div class="grid-3">
|
||||
<div class="field">
|
||||
<label for="ea_name">Expert Advisor</label>
|
||||
<select id="ea_name" name="ea_name">
|
||||
{% for ea in registered_eas %}
|
||||
<option value="{{ ea.name }}" {% if ea.name == default_ea %}selected{% endif %}>{{ ea.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="symbol">Symbol</label>
|
||||
<select id="symbol" name="symbol">
|
||||
<option value="XAUUSD" selected>XAUUSD (Gold)</option>
|
||||
<option value="EURUSD">EURUSD</option>
|
||||
<option value="GBPUSD">GBPUSD</option>
|
||||
<option value="USDJPY">USDJPY</option>
|
||||
<option value="USDCHF">USDCHF</option>
|
||||
<option value="USDCAD">USDCAD</option>
|
||||
<option value="AUDUSD">AUDUSD</option>
|
||||
<option value="BTCUSD">BTCUSD</option>
|
||||
<option value="NASDAQ">NASDAQ</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="timeframe">Timeframe</label>
|
||||
<select id="timeframe" name="timeframe">
|
||||
<option value="M15">M15</option>
|
||||
<option value="M30">M30</option>
|
||||
<option value="H1" selected>H1</option>
|
||||
<option value="H4">H4</option>
|
||||
<option value="D1">D1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Periods -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
|
||||
Date Ranges
|
||||
</div>
|
||||
<div class="grid-2" style="margin-bottom:1rem">
|
||||
<div class="field">
|
||||
<label for="train_start">Training From</label>
|
||||
<input type="date" id="train_start" name="train_start" value="2022-01-01">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="train_end">Training To</label>
|
||||
<input type="date" id="train_end" name="train_end" value="2023-12-31">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="val_start">Validation From (Out-of-Sample)</label>
|
||||
<input type="date" id="val_start" name="val_start" value="2024-01-01">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="val_end">Validation To</label>
|
||||
<input type="date" id="val_end" name="val_end" value="2024-06-30">
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:1rem">
|
||||
<strong>Tip:</strong> Training period is where the optimizer searches for settings.
|
||||
Validation is unseen data — the optimizer never touches it until Phase 3.
|
||||
Keep validation at least 6 months.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Budget -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
Time Budget
|
||||
</div>
|
||||
<div class="pill-group">
|
||||
<button type="button" class="pill" data-budget="30" onclick="setBudget(30)">
|
||||
⚡ 30 min
|
||||
<span class="badge">~15 tests</span>
|
||||
</button>
|
||||
<button type="button" class="pill active" data-budget="60" onclick="setBudget(60)">
|
||||
⏱ 1 hour
|
||||
<span class="badge">~35 tests</span>
|
||||
</button>
|
||||
<button type="button" class="pill" data-budget="120" onclick="setBudget(120)">
|
||||
🌙 2 hours
|
||||
<span class="badge">~70 tests</span>
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="budget_minutes" name="budget_minutes" value="60">
|
||||
</div>
|
||||
|
||||
<!-- Objective -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
Optimize For
|
||||
</div>
|
||||
<div class="obj-grid">
|
||||
<div class="obj-card active" data-obj="balanced" onclick="setObj('balanced')">
|
||||
<div class="obj-icon">⚖️</div>
|
||||
<div class="obj-name">Balanced</div>
|
||||
<div class="obj-desc">Good profit + Low drawdown</div>
|
||||
</div>
|
||||
<div class="obj-card" data-obj="max_profit" onclick="setObj('max_profit')">
|
||||
<div class="obj-icon">📈</div>
|
||||
<div class="obj-name">Max Profit</div>
|
||||
<div class="obj-desc">Highest return, any risk</div>
|
||||
</div>
|
||||
<div class="obj-card" data-obj="min_drawdown" onclick="setObj('min_drawdown')">
|
||||
<div class="obj-icon">🛡️</div>
|
||||
<div class="obj-name">Low Risk</div>
|
||||
<div class="obj-desc">Minimum drawdown first</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="objective" name="objective" value="balanced">
|
||||
</div>
|
||||
|
||||
<!-- Advanced: Param Selection -->
|
||||
<div class="section">
|
||||
<div class="adv-toggle" id="adv-toggle" onclick="toggleAdv()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 18l6-6-6-6"/></svg>
|
||||
Advanced: Select Parameters to Optimize
|
||||
<span style="color:var(--muted);font-size:0.78rem;margin-left:0.25rem">(optional)</span>
|
||||
</div>
|
||||
<div class="adv-body" id="adv-body">
|
||||
<p style="font-size:0.82rem;color:var(--muted);margin-bottom:1rem">
|
||||
Leave all checked to let the optimizer decide. Uncheck parameters you want to keep fixed.
|
||||
</p>
|
||||
<div class="param-grid" id="param-grid">
|
||||
{% for p in params %}
|
||||
<label class="param-check">
|
||||
<input type="checkbox" name="selected_params" value="{{ p.name }}"
|
||||
{% if p.optimize %}checked{% endif %}
|
||||
{% if p.type == 'fixed' %}disabled{% endif %}>
|
||||
<span title="{{ p.range_label }}">{{ p.name.replace('Inp','') }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="submit-row">
|
||||
<button type="submit" class="btn-start" id="start-btn">
|
||||
<span>⚡</span> Start Optimization
|
||||
</button>
|
||||
<div class="estimate" id="estimate">
|
||||
Estimated: <strong id="est-runs">~35 tests</strong> in <strong id="est-time">~1 hour</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let budget = 60;
|
||||
let objective = 'balanced';
|
||||
|
||||
const estimates = {
|
||||
30: {runs: '~15 tests', time: '~30 min'},
|
||||
60: {runs: '~35 tests', time: '~1 hour'},
|
||||
120: {runs: '~70 tests', time: '~2 hours'},
|
||||
};
|
||||
|
||||
function setBudget(val) {
|
||||
budget = val;
|
||||
document.getElementById('budget_minutes').value = val;
|
||||
document.querySelectorAll('.pill').forEach(p => {
|
||||
p.classList.toggle('active', parseInt(p.dataset.budget) === val);
|
||||
});
|
||||
const e = estimates[val] || estimates[60];
|
||||
document.getElementById('est-runs').textContent = e.runs;
|
||||
document.getElementById('est-time').textContent = e.time;
|
||||
}
|
||||
|
||||
function setObj(val) {
|
||||
objective = val;
|
||||
document.getElementById('objective').value = val;
|
||||
document.querySelectorAll('.obj-card').forEach(c => {
|
||||
c.classList.toggle('active', c.dataset.obj === val);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleAdv() {
|
||||
const toggle = document.getElementById('adv-toggle');
|
||||
const body = document.getElementById('adv-body');
|
||||
toggle.classList.toggle('open');
|
||||
body.classList.toggle('open');
|
||||
}
|
||||
|
||||
document.getElementById('setup-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('start-btn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>⏳</span> Starting...';
|
||||
|
||||
const form = e.target;
|
||||
const data = {
|
||||
ea_name: form.ea_name.value,
|
||||
symbol: form.symbol.value,
|
||||
timeframe: form.timeframe.value,
|
||||
train_start: form.train_start.value.replace(/-/g, '.'),
|
||||
train_end: form.train_end.value.replace(/-/g, '.'),
|
||||
val_start: form.val_start.value.replace(/-/g, '.'),
|
||||
val_end: form.val_end.value.replace(/-/g, '.'),
|
||||
objective: form.objective.value,
|
||||
budget_minutes: parseInt(form.budget_minutes.value),
|
||||
selected_params: [...form.querySelectorAll('input[name=selected_params]:checked')]
|
||||
.map(cb => cb.value),
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/start', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.ok) {
|
||||
window.location.href = '/dashboard';
|
||||
} else {
|
||||
alert('Could not start: ' + (result.msg || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>⚡</span> Start Optimization';
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>⚡</span> Start Optimization';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user