Thanhanh9's picture
download
raw
21.9 kB
/* =============================================
Forex Backtest Tracker — app.js
============================================= */
const API = 'http://localhost:1000';
// ─── State ───────────────────────────────────
let strategies = [];
let currentStrategyId = null;
let currentDetail = null;
let comparisonChart = null;
let winrateChart = null;
// ─── Bootstrap ───────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
if (localStorage.getItem('sidebarCollapsed') === '1') {
document.body.classList.add('sidebar-collapsed');
}
loadStrategies().then(() => {
handleHash();
});
});
window.addEventListener('hashchange', handleHash);
function handleHash() {
const hash = window.location.hash.slice(1);
if (hash.startsWith('strategy-')) {
const id = hash.replace('strategy-', '');
if (strategies.length > 0) {
const s = strategies.find(x => x.id == id);
if (s) openDetail(id);
else showPage('dashboard');
}
} else if (hash === 'strategies') {
showPage('strategies');
} else {
showPage('dashboard');
}
}
function toggleSidebar() {
const collapsed = document.body.classList.toggle('sidebar-collapsed');
localStorage.setItem('sidebarCollapsed', collapsed ? '1' : '0');
}
// ─── API Helpers ─────────────────────────────
async function api(path, options = {}) {
try {
const res = await fetch(`${API}${path}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${res.status}`);
}
if (res.status === 204) return null;
return res.json();
} catch (e) {
showToast(e.message, 'error');
throw e;
}
}
// ─── Load Data ───────────────────────────────
async function loadStrategies() {
strategies = await api('/strategies') || [];
renderDashboard();
renderStrategiesGrid();
}
// ─── Page Navigation ─────────────────────────
function showPage(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById(`page-${pageId}`).classList.add('active');
const navEl = document.getElementById(`nav-${pageId}`);
if (navEl) navEl.classList.add('active');
const titles = { dashboard: 'Dashboard', strategies: 'Strategies', detail: 'Strategy Detail' };
document.getElementById('page-title').textContent = titles[pageId] || '';
if (pageId === 'dashboard') window.location.hash = '';
if (pageId === 'strategies') window.location.hash = 'strategies';
const topBtn = document.getElementById('topbar-btn');
if (pageId === 'detail') {
topBtn.textContent = '+ Add Backtest';
topBtn.onclick = openBacktestModal;
} else {
topBtn.textContent = '+ New Strategy';
topBtn.onclick = openStrategyModal;
}
}
// ─── Dashboard ───────────────────────────────
function renderDashboard() {
const total = strategies.length;
const active = strategies.filter(s => s.status === 'active').length;
const testing = strategies.filter(s => s.status === 'testing').length;
const totalBt = strategies.reduce((acc, s) => acc + (s.aggregated?.count || 0), 0);
document.getElementById('sum-total').textContent = total;
document.getElementById('sum-active').textContent = active;
document.getElementById('sum-testing').textContent = testing;
document.getElementById('sum-backtests').textContent = totalBt;
renderComparisonChart();
renderWinrateChart();
renderRankingTable();
}
function renderComparisonChart() {
const metric = document.getElementById('chart-metric-select')?.value || 'net_profit';
const labels = strategies.map(s => s.name);
const data = strategies.map(s => s.aggregated?.[metric] ?? 0);
const ctx = document.getElementById('comparison-chart');
if (!ctx) return;
if (comparisonChart) comparisonChart.destroy();
comparisonChart = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{
label: metricLabel(metric),
data,
backgroundColor: data.map(v => v >= 0 ? 'rgba(99,102,241,0.7)' : 'rgba(239,68,68,0.7)'),
borderColor: data.map(v => v >= 0 ? '#6366f1' : '#ef4444'),
borderWidth: 1,
borderRadius: 6,
}]
},
options: chartOptions(metricLabel(metric))
});
}
function renderWinrateChart() {
const ctx = document.getElementById('winrate-chart');
if (!ctx) return;
const labels = strategies.map(s => s.name);
const winTrades = strategies.map(s => s.aggregated?.profit_trade ?? 0);
const lossTrades = strategies.map(s => s.aggregated?.loss_trade ?? 0);
if (winrateChart) winrateChart.destroy();
winrateChart = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [
{
label: 'Profit Trades',
data: winTrades,
backgroundColor: 'rgba(34,197,94,0.7)',
borderColor: '#22c55e',
borderWidth: 1,
borderRadius: 6,
},
{
label: 'Loss Trades',
data: lossTrades,
backgroundColor: 'rgba(239,68,68,0.7)',
borderColor: '#ef4444',
borderWidth: 1,
borderRadius: 6,
}
]
},
options: chartOptions('Trades', true)
});
}
function renderRankingTable() {
const field = document.getElementById('rank-sort-field')?.value || 'net_profit';
const dir = document.getElementById('rank-sort-dir')?.value || 'desc';
const sorted = [...strategies].sort((a, b) => {
const av = a.aggregated?.[field] ?? -Infinity;
const bv = b.aggregated?.[field] ?? -Infinity;
return dir === 'desc' ? bv - av : av - bv;
});
const tbody = document.getElementById('ranking-tbody');
if (!tbody) return;
if (sorted.length === 0) {
tbody.innerHTML = `<tr><td colspan="11" class="empty-state" style="padding:40px;text-align:center;color:var(--text-muted)">No strategies yet</td></tr>`;
return;
}
tbody.innerHTML = sorted.map((s, i) => {
const a = s.aggregated || {};
const rankClass = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
return `<tr>
<td><span class="rank-num ${rankClass}">${i + 1}</span></td>
<td><strong style="cursor:pointer;color:var(--accent)" onclick="openDetail(${s.id})">${esc(s.name)}</strong></td>
<td>${statusBadge(s.status)}</td>
<td>${a.count ?? 0}</td>
<td>${fmtNum(a.net_profit)}%</td>
<td>${fmtNum(a.profit_factor)}</td>
<td>${fmtNum(a.max_dd_pct)}%</td>
<td>${fmtNum(a.rr)}</td>
<td>${fmtNum(a.total_return)}</td>
<td>${fmtNum(a.carg)}%</td>
<td>
<button class="btn btn-ghost btn-icon" onclick="openDetail(${s.id})" title="View">👁</button>
<button class="btn btn-ghost btn-icon" onclick="confirmDelete('strategy',${s.id})" title="Delete">🗑</button>
</td>
</tr>`;
}).join('');
}
// ─── Strategies Grid ─────────────────────────
function renderStrategiesGrid() {
const grid = document.getElementById('strategies-grid');
if (!grid) return;
if (strategies.length === 0) {
grid.innerHTML = `<div class="empty-state">
<div class="empty-state-icon">📊</div>
<h3>No strategies yet</h3>
<p>Click "+ New Strategy" to get started</p>
</div>`;
return;
}
grid.innerHTML = strategies.map(s => {
const a = s.aggregated || {};
return `<div class="strategy-card" onclick="openDetail(${s.id})">
<div class="strategy-card-header">
<div>
<div class="strategy-card-name">${esc(s.name)}</div>
${statusBadge(s.status)}
</div>
<div style="display:flex;gap:6px" onclick="event.stopPropagation()">
<button class="btn btn-ghost btn-icon" onclick="openEditStrategyModal(${s.id})">✏️</button>
<button class="btn btn-ghost btn-icon" onclick="confirmDelete('strategy',${s.id})">🗑</button>
</div>
</div>
<p class="strategy-card-desc">${esc(s.description || 'No description')}</p>
<div class="strategy-card-metrics">
<div class="metric-mini">
<div class="metric-mini-label">Net P%</div>
<div class="metric-mini-value">${fmtNum(a.net_profit) ?? '—'}</div>
</div>
<div class="metric-mini">
<div class="metric-mini-label">PF</div>
<div class="metric-mini-value">${fmtNum(a.profit_factor) ?? '—'}</div>
</div>
<div class="metric-mini">
<div class="metric-mini-label">Backtests</div>
<div class="metric-mini-value">${a.count ?? 0}</div>
</div>
</div>
</div>`;
}).join('');
}
// ─── Strategy Detail ─────────────────────────
async function openDetail(strategyId) {
currentStrategyId = strategyId;
window.location.hash = 'strategy-' + strategyId;
currentDetail = await api(`/strategies/${strategyId}`);
renderDetailPage();
showPage('detail');
}
function renderDetailPage() {
const s = currentDetail;
const a = s.aggregated || {};
document.getElementById('detail-name').textContent = s.name;
const badge = document.getElementById('detail-status-badge');
badge.textContent = s.status === 'active' ? '✅ Active' : '🔬 Testing';
badge.className = `badge badge-${s.status}`;
document.getElementById('detail-description').textContent = s.description || '';
const detailSec = document.getElementById('detail-details-section');
if (s.details) {
document.getElementById('detail-details').textContent = s.details;
detailSec.style.display = '';
} else {
detailSec.style.display = 'none';
}
document.getElementById('detail-agg-count').textContent = `${a.count || 0} backtest(s)`;
// Metrics grid
const metricsGrid = document.getElementById('detail-metrics-grid');
const DOLLAR_KEYS = new Set(['gross_profit', 'gross_loss', 'initial_balance', 'total_return',
'avg_profit_trade', 'avg_loss_trade', 'largest_profit_trade', 'largest_loss_trade']);
const metricDefs = [
{ key: 'net_profit', label: 'Net Profit %', suffix: '%' },
{ key: 'gross_profit', label: 'Gross Profit', dollar: true },
{ key: 'gross_loss', label: 'Gross Loss', dollar: true },
{ key: 'initial_balance', label: 'Initial Balance', dollar: true },
{ key: 'total_return', label: 'Total Return', dollar: true },
{ key: 'total_trades', label: 'Total Trades' },
{ key: 'profit_factor', label: 'Profit Factor' },
{ key: 'max_dd_pct', label: 'Max DD %', suffix: '%' },
{ key: 'ar_mdd', label: 'AR/MDD' },
{ key: 'avg_profit_trade', label: 'Avg Profit', dollar: true },
{ key: 'avg_loss_trade', label: 'Avg Loss', dollar: true },
{ key: 'recover_factor', label: 'Recover Factor' },
{ key: 'largest_profit_trade', label: 'Largest Profit', dollar: true },
{ key: 'largest_loss_trade', label: 'Largest Loss', dollar: true },
{ key: 'carg', label: 'CARG %', suffix: '%' },
{ key: 'rr', label: 'RR' },
];
metricsGrid.innerHTML = metricDefs.map(m => {
const val = a[m.key];
let cls = '';
if (val !== null && val !== undefined) {
if (['net_profit', 'gross_profit', 'total_return', 'profit_factor', 'recover_factor', 'carg'].includes(m.key)) {
cls = val >= 0 ? 'positive' : 'negative';
}
}
const display = val !== null && val !== undefined
? (m.dollar ? fmtDollar(val) : `${fmtNum(val)}${m.suffix || ''}`)
: '—';
return `<div class="metric-cell">
<div class="metric-cell-label">${m.label}</div>
<div class="metric-cell-value ${cls}">${display}</div>
</div>`;
}).join('');
// Table
renderBacktestTable(s.backtests || []);
}
function renderBacktestTable(backtests) {
const tbody = document.getElementById('backtest-entries-tbody');
if (!tbody) return;
if (backtests.length === 0) {
tbody.innerHTML = `<tr><td colspan="21" style="padding:40px;text-align:center;color:var(--text-muted)">No backtest entries yet</td></tr>`;
return;
}
tbody.innerHTML = backtests.map(b => `<tr>
<td><strong>${esc(b.pair)}</strong></td>
<td><span class="badge" style="background:var(--bg-secondary);color:var(--text-secondary)">${esc(b.timeframe)}</span></td>
<td style="color:var(--text-muted)">${b.date_from || ''}${b.date_from && b.date_to ? ' → ' : ''}${b.date_to || ''}</td>
<td class="${colorVal(b.net_profit)}">${fmtNum(b.net_profit) ?? '—'}${b.net_profit != null ? '%' : ''}</td>
<td>${fmtDollar(b.total_return)}</td>
<td>${b.total_trades ?? '—'}</td>
<td class="${b.profit_factor >= 1 ? 'positive' : ''}">${fmtNum(b.profit_factor) ?? '—'}</td>
<td style="color:var(--red)">${fmtNum(b.max_dd_pct) ?? '—'}${b.max_dd_pct != null ? '%' : ''}</td>
<td>${fmtNum(b.ar_mdd) ?? '—'}</td>
<td>${fmtDollar(b.avg_profit_trade)}</td>
<td>${fmtDollar(b.avg_loss_trade)}</td>
<td>${fmtNum(b.recover_factor) ?? '—'}</td>
<td style="color:var(--green)">${fmtDollar(b.largest_profit_trade)}</td>
<td style="color:var(--red)">${fmtDollar(b.largest_loss_trade)}</td>
<td>${fmtNum(b.carg) ?? '—'}${b.carg != null ? '%' : ''}</td>
<td>${fmtNum(b.rr) ?? '—'}</td>
<td>
<button class="btn btn-ghost btn-icon" onclick="openEditBacktestModal(${b.id})" title="Edit">✏️</button>
<button class="btn btn-ghost btn-icon" onclick="confirmDelete('backtest',${b.id})" title="Delete">🗑</button>
</td>
</tr>`).join('');
}
// ─── Strategy Modal ───────────────────────────
function openStrategyModal(id = null) {
document.getElementById('strategy-id').value = '';
document.getElementById('strategy-modal-title').textContent = 'New Strategy';
document.getElementById('strategy-form').reset();
if (id) {
const s = strategies.find(x => x.id === id);
if (s) {
document.getElementById('strategy-id').value = s.id;
document.getElementById('strategy-modal-title').textContent = 'Edit Strategy';
document.getElementById('strategy-name').value = s.name;
document.getElementById('strategy-status').value = s.status;
document.getElementById('strategy-description').value = s.description || '';
document.getElementById('strategy-details').value = s.details || '';
}
}
openModal('strategy-modal');
}
function openEditStrategyModal(id) {
const sid = id || currentStrategyId;
openStrategyModal(sid);
}
async function saveStrategy() {
const id = document.getElementById('strategy-id').value;
const payload = {
name: document.getElementById('strategy-name').value.trim(),
status: document.getElementById('strategy-status').value,
description: document.getElementById('strategy-description').value.trim(),
details: document.getElementById('strategy-details').value.trim(),
};
if (!payload.name) { showToast('Name is required', 'error'); return; }
try {
if (id) {
await api(`/strategies/${id}`, { method: 'PUT', body: JSON.stringify(payload) });
showToast('Strategy updated!', 'success');
} else {
await api('/strategies', { method: 'POST', body: JSON.stringify(payload) });
showToast('Strategy created!', 'success');
}
closeModal('strategy-modal');
await loadStrategies();
if (id && currentStrategyId == id) await openDetail(id);
} catch { }
}
// ─── Backtest Modal ───────────────────────────
const BT_FIELDS = [
'pair', 'timeframe', 'date_from', 'date_to', 'notes',
'net_profit', 'initial_balance', 'total_return',
'total_trades', 'profit_factor', 'max_dd_pct',
'ar_mdd', 'avg_profit_trade', 'avg_loss_trade', 'recover_factor',
'largest_profit_trade', 'largest_loss_trade', 'carg', 'rr'
];
function openBacktestModal() {
document.getElementById('backtest-id').value = '';
document.getElementById('backtest-modal-title').textContent = 'Add Backtest Entry';
document.getElementById('backtest-form').reset();
openModal('backtest-modal');
}
function openEditBacktestModal(entryId) {
const entry = currentDetail.backtests.find(b => b.id === entryId);
if (!entry) return;
document.getElementById('backtest-id').value = entryId;
document.getElementById('backtest-modal-title').textContent = 'Edit Backtest Entry';
BT_FIELDS.forEach(f => {
const el = document.getElementById(`bt-${f.replace(/_/g, '_')}`);
if (el && entry[f] !== null && entry[f] !== undefined) el.value = entry[f];
else if (el) el.value = '';
});
openModal('backtest-modal');
}
async function saveBacktest() {
const id = document.getElementById('backtest-id').value;
const payload = {};
BT_FIELDS.forEach(f => {
const el = document.getElementById(`bt-${f}`);
if (!el) return;
const val = el.value.trim();
if (val === '') { payload[f] = null; return; }
if (['pair', 'timeframe', 'date_from', 'date_to', 'notes'].includes(f)) {
payload[f] = val;
} else if (['total_trades', 'profit_trade', 'loss_trade'].includes(f)) {
payload[f] = parseInt(val, 10);
} else {
payload[f] = parseFloat(val);
}
});
if (!payload.pair) { showToast('Currency pair is required', 'error'); return; }
if (!payload.timeframe) { showToast('Timeframe is required', 'error'); return; }
try {
if (id) {
await api(`/backtests/${id}`, { method: 'PUT', body: JSON.stringify(payload) });
showToast('Backtest updated!', 'success');
} else {
await api(`/strategies/${currentStrategyId}/backtests`, { method: 'POST', body: JSON.stringify(payload) });
showToast('Backtest added!', 'success');
}
closeModal('backtest-modal');
await openDetail(currentStrategyId);
await loadStrategies();
} catch { }
}
// ─── Delete Confirmation ──────────────────────
function confirmDelete(type, id) {
const msg = type === 'strategy'
? 'Delete this strategy and all its backtest entries? This cannot be undone.'
: 'Delete this backtest entry? This cannot be undone.';
document.getElementById('confirm-message').textContent = msg;
document.getElementById('confirm-action-btn').onclick = () => doDelete(type, id);
openModal('confirm-modal');
}
async function doDelete(type, id) {
try {
if (type === 'strategy') {
await api(`/strategies/${id}`, { method: 'DELETE' });
showToast('Strategy deleted', 'success');
closeModal('confirm-modal');
await loadStrategies();
showPage('strategies');
} else {
await api(`/backtests/${id}`, { method: 'DELETE' });
showToast('Backtest deleted', 'success');
closeModal('confirm-modal');
await openDetail(currentStrategyId);
await loadStrategies();
}
} catch { }
}
// ─── Modal Helpers ────────────────────────────
function openModal(id) {
document.getElementById(id).classList.add('open');
}
function closeModal(id) {
document.getElementById(id).classList.remove('open');
}
// ─── Toast ────────────────────────────────────
function showToast(msg, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = type === 'success' ? `✅ ${msg}` : `❌ ${msg}`;
toast.className = `toast ${type} show`;
setTimeout(() => toast.classList.remove('show'), 3000);
}
// ─── Chart Options ────────────────────────────
function chartOptions(label, stacked = false) {
return {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: stacked, labels: { color: '#8b91a8', font: { size: 11 } } },
tooltip: {
backgroundColor: '#181c27',
borderColor: '#2a2f3d',
borderWidth: 1,
titleColor: '#e8eaf0',
bodyColor: '#8b91a8',
padding: 10,
}
},
scales: {
x: {
stacked,
ticks: { color: '#8b91a8', font: { size: 11 } },
grid: { color: 'rgba(42,47,61,0.6)' },
},
y: {
stacked,
ticks: { color: '#8b91a8', font: { size: 11 } },
grid: { color: 'rgba(42,47,61,0.6)' },
}
}
};
}
// ─── Utility ─────────────────────────────────
function fmtNum(v) {
if (v === null || v === undefined) return null;
return Number.isInteger(v) ? v.toString() : v.toFixed(2);
}
function fmtDollar(v) {
if (v === null || v === undefined) return '—';
return '$' + Math.round(v).toLocaleString('en-US');
}
function colorVal(v) {
if (v === null || v === undefined) return '';
return v >= 0 ? 'positive' : 'negative';
}
function esc(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function statusBadge(status) {
return status === 'active'
? `<span class="badge badge-active">✅ Active</span>`
: `<span class="badge badge-testing">🔬 Testing</span>`;
}
function metricLabel(key) {
const map = {
net_profit: 'Net Profit %', profit_factor: 'Profit Factor',
max_dd_pct: 'Max DD %', rr: 'RR',
total_return: 'Total Return', carg: 'CARG %'
};
return map[key] || key;
}

Xet Storage Details

Size:
21.9 kB
·
Xet hash:
649de2cad7d6f7a7f18655959d5e65e3cd2b6e958ddcbd40b6e0c4839ff69f12

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.