| /* ============================================= | |
| 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | |
| } | |
| 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.