/home/devscfvi/AblePro/js/analytics_ablePro.js
// ============================================================
// ANALYTICS — AblePro Bootstrap 5 UI override
// Keeps all chart init logic from analytics.js
// Adds TraderVue-style detailed stats, win/loss day comparison,
// ApexCharts equity curve, day-of-week heatmap, and more.
// ============================================================
// ── Helper: resolve P&L for any trade ────────────────────────
function _pnl(t) {
if (t.actual_pnl != null && t.actual_pnl !== '') return parseFloat(t.actual_pnl);
return t.status === 'win' ? parseFloat(t.profit_amount || 0) : -parseFloat(t.loss_amount || 0);
}
// ── Helper: format currency ───────────────────────────────────
function _fc(v) {
const abs = Math.abs(parseFloat(v) || 0);
const sign = parseFloat(v) >= 0 ? '+' : '-';
return sign + '$' + abs.toFixed(2);
}
// ── Helper: colour class ──────────────────────────────────────
function _cc(v) { return parseFloat(v) >= 0 ? 'text-success' : 'text-danger'; }
// ── Main render override ──────────────────────────────────────
window.renderAnalytics = function() {
if (!state.analytics) {
return `<div class="d-flex align-items-center justify-content-center py-5">
<div class="spinner-border text-primary"></div>
<span class="ms-3 text-muted">Loading analytics…</span>
</div>`;
}
const hasData = state.trades && state.trades.length > 0;
if (!hasData) {
return `<div class="card"><div class="card-body text-center py-5">
<i class="ti ti-chart-bar f-48 d-block mb-3 text-muted opacity-25"></i>
<h5 class="text-muted">No trades yet</h5>
<p class="text-muted mb-4">Import or log trades to unlock analytics</p>
<button class="btn btn-primary" onclick="navigateTo('calculator')">
<i class="ti ti-plus me-2"></i>Add First Trade
</button>
</div></div>`;
}
const tabs = [
{ id:'overview', icon:'ti-layout-dashboard', label:'Overview' },
{ id:'reports', icon:'ti-report-analytics', label:'Reports' },
{ id:'days-times', icon:'ti-clock', label:'Days/Times' },
{ id:'strategy', icon:'ti-target', label:'Strategy' },
{ id:'psychology', icon:'ti-brain', label:'Psychology' },
];
return `
<!-- Tab nav -->
<div class="card mb-4">
<div class="card-body py-2 px-3">
<ul class="nav nav-tabs border-0 gap-1" id="analytics-tabs">
${tabs.map((t, i) => `
<li class="nav-item">
<button class="nav-link ${i===0?'active':''} d-flex align-items-center gap-2 px-3 py-2 f-13 fw-semibold"
onclick="switchAnalyticsTabAP('${t.id}', this)">
<i class="ti ${t.icon} f-15"></i>
<span class="d-none d-md-inline">${t.label}</span>
</button>
</li>`).join('')}
</ul>
</div>
</div>
<!-- Tab content -->
<div id="analytics-content"></div>`;
};
// ── Tab switcher ──────────────────────────────────────────────
window.switchAnalyticsTabAP = function(tab, btnEl) {
document.querySelectorAll('#analytics-tabs .nav-link').forEach(b => b.classList.remove('active'));
if (btnEl) btnEl.classList.add('active');
const container = document.getElementById('analytics-content');
if (!container) return;
switch(tab) {
case 'overview': container.innerHTML = renderOverviewTabAP(); setTimeout(initOverviewChartsAP, 120); break;
case 'reports': container.innerHTML = renderReportsTabAP(); setTimeout(initReportsChartsAP, 120); break;
case 'days-times': container.innerHTML = renderDaysTimesTabAP(); setTimeout(initDaysTimesChartsAP, 120); break;
case 'strategy': container.innerHTML = renderCategoryTab(); setTimeout(initCategoryCharts, 120); break;
case 'psychology': container.innerHTML = renderPsychologyTab(); setTimeout(initPsychologyCharts, 120); break;
default: container.innerHTML = renderOverviewTabAP(); setTimeout(initOverviewChartsAP, 120); break;
}
};
// ── initAnalyticsCharts override ──────────────────────────────
window.initAnalyticsCharts = function() {
// Boot on the Overview tab
const firstBtn = document.querySelector('#analytics-tabs .nav-link');
switchAnalyticsTabAP('overview', firstBtn);
};
// ─────────────────────────────────────────────────────────────
// OVERVIEW TAB
// ─────────────────────────────────────────────────────────────
function renderOverviewTabAP() {
const stats = state.stats || {};
const analytics = state.analytics || {};
const trades = state.trades || [];
const closed = trades.filter(t => t.status==='win'||t.status==='loss');
const withPnl = closed.map(t => ({...t, _pnl: _pnl(t)}));
const totalPnl = withPnl.reduce((s,t)=>s+t._pnl,0);
const wins = withPnl.filter(t=>t._pnl>0);
const losses = withPnl.filter(t=>t._pnl<0);
const profitFactor= losses.length>0 ? (wins.reduce((s,t)=>s+t._pnl,0)/Math.abs(losses.reduce((s,t)=>s+t._pnl,0))).toFixed(2) : wins.length>0?'∞':'0.00';
const expectancy = withPnl.length>0 ? totalPnl/withPnl.length : 0;
const winRate = withPnl.length>0 ? Math.round(wins.length/withPnl.length*100) : 0;
const streaks = analytics.streaks || {};
const kpis = [
{ label:'Net P&L', val:_fc(totalPnl), cls:_cc(totalPnl), icon:'ti-currency-dollar', bg:'bg-light-'+(totalPnl>=0?'success':'danger') },
{ label:'Win Rate', val:winRate+'%', cls:_cc(winRate-50), icon:'ti-chart-pie', bg:'bg-light-'+(winRate>=50?'success':'danger') },
{ label:'Profit Factor', val:profitFactor, cls:_cc(parseFloat(profitFactor)-1), icon:'ti-trending-up', bg:'bg-light-primary' },
{ label:'Expectancy', val:_fc(expectancy), cls:_cc(expectancy), icon:'ti-target', bg:'bg-light-'+(expectancy>=0?'success':'danger') },
{ label:'Total Trades', val:withPnl.length, cls:'', icon:'ti-list-details', bg:'bg-light-primary' },
{ label:'Avg R:R', val:(()=>{ const rrs=withPnl.filter(t=>parseFloat(t.reward_risk_ratio||0)>0).map(t=>parseFloat(t.reward_risk_ratio)); return rrs.length>0?'1:'+(rrs.reduce((a,b)=>a+b,0)/rrs.length).toFixed(2):'—'; })(), cls:'text-primary', icon:'ti-scale', bg:'bg-light-primary' },
];
return `
<!-- KPI row -->
<div class="row g-3 mb-4">
${kpis.map(k=>`
<div class="col-6 col-md-4 col-lg-2">
<div class="card mb-0 h-100">
<div class="card-body py-3 px-3">
<div class="d-flex align-items-center gap-2 mb-2">
<span class="avtar avtar-xs ${k.bg} rounded"><i class="ti ${k.icon} f-15"></i></span>
<span class="text-muted f-11">${k.label}</span>
</div>
<div class="fw-bold f-20 ${k.cls}">${k.val}</div>
</div>
</div>
</div>`).join('')}
</div>
<div class="row g-4 mb-4">
<!-- Equity curve -->
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between py-3">
<h6 class="mb-0 fw-semibold">Equity Curve</h6>
<span class="badge ${totalPnl>=0?'bg-light-success text-success':'bg-light-danger text-danger'}">${_fc(totalPnl)}</span>
</div>
<div class="card-body p-2">
<div id="apex-equity" style="height:220px"></div>
</div>
</div>
</div>
<!-- Streaks -->
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header py-3"><h6 class="mb-0 fw-semibold">Streaks</h6></div>
<div class="card-body">
<div class="row g-3">
<div class="col-6">
<div class="p-3 rounded-3 text-center bg-light-success">
<div class="f-28 fw-black text-success">${streaks.current_win_streak||0}</div>
<div class="f-11 text-muted">Current Win</div>
<div class="f-11 text-success mt-1">Best: ${streaks.max_win_streak||0}</div>
</div>
</div>
<div class="col-6">
<div class="p-3 rounded-3 text-center bg-light-danger">
<div class="f-28 fw-black text-danger">${streaks.current_loss_streak||0}</div>
<div class="f-11 text-muted">Current Loss</div>
<div class="f-11 text-danger mt-1">Worst: ${streaks.max_loss_streak||0}</div>
</div>
</div>
<div class="col-12">
<div class="p-3 rounded-3" style="background:#f8fafc">
<div class="d-flex justify-content-between mb-2">
<span class="f-12 text-muted">Win / Loss / Open</span>
<span class="f-12 fw-semibold">${wins.length} / ${losses.length} / ${trades.filter(t=>!t.status||t.status==='pending').length}</span>
</div>
<div style="height:8px;background:#e2e8f0;border-radius:4px;overflow:hidden">
<div style="height:100%;width:${winRate}%;background:linear-gradient(90deg,#10b981,#34d399);border-radius:4px"></div>
</div>
<div class="d-flex justify-content-between mt-1">
<span class="f-10 text-success">${winRate}% wins</span>
<span class="f-10 text-danger">${100-winRate}% losses</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Performance by coin -->
<div class="card">
<div class="card-header py-3"><h6 class="mb-0 fw-semibold">Performance by Symbol</h6></div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 f-13">
<thead class="table-light">
<tr><th>Symbol</th><th class="text-end">Trades</th><th class="text-end">Win Rate</th><th class="text-end">Net P&L</th><th class="text-end">Avg R:R</th></tr>
</thead>
<tbody>
${(analytics.byCoin||[]).slice(0,10).map(c=>`
<tr>
<td class="fw-semibold">${c.coin}</td>
<td class="text-end">${c.total||c.total_trades||0}</td>
<td class="text-end ${parseFloat(c.win_rate||0)>=50?'text-success':'text-danger'}">${parseFloat(c.win_rate||0).toFixed(1)}%</td>
<td class="text-end fw-semibold ${_cc(c.net_pnl)}">${_fc(c.net_pnl)}</td>
<td class="text-end text-primary">—</td>
</tr>`).join('')||'<tr><td colspan="5" class="text-center text-muted py-3">No data</td></tr>'}
</tbody>
</table>
</div>
</div>
</div>`;
}
function initOverviewChartsAP() {
const trades = state.trades || [];
const closed = trades.filter(t=>t.status==='win'||t.status==='loss');
const withPnl = closed.map(t=>({...t,_pnl:_pnl(t)}));
// Build cumulative by trade date
const sorted = [...withPnl].sort((a,b)=>{
const da = (a.trade_date||a.created_at||'').substring(0,10);
const db = (b.trade_date||b.created_at||'').substring(0,10);
return da.localeCompare(db);
});
let cum=0;
const series = sorted.map(t=>{ cum+=t._pnl; return parseFloat(cum.toFixed(2)); });
const labels = sorted.map(t=>(t.trade_date||t.created_at||'').substring(0,10));
const finalVal = series[series.length-1]||0;
const lineColor = finalVal>=0?'#10b981':'#ef4444';
if (window.ApexCharts && document.getElementById('apex-equity')) {
if (window._apexEquity) { try{window._apexEquity.destroy();}catch(e){} }
window._apexEquity = new ApexCharts(document.getElementById('apex-equity'), {
chart:{ type:'area', height:220, toolbar:{show:false}, sparkline:{enabled:false}, animations:{enabled:true,speed:600} },
series:[{ name:'Equity', data:series }],
xaxis:{ categories:labels, labels:{show:labels.length<40, style:{colors:'#94a3b8',fontSize:'10px'}}, axisBorder:{show:false}, axisTicks:{show:false} },
yaxis:{ labels:{ style:{colors:'#94a3b8',fontSize:'11px'}, formatter:v=>'$'+v.toFixed(0) } },
stroke:{ curve:'smooth', width:2.5, colors:[lineColor] },
fill:{ type:'gradient', gradient:{ shadeIntensity:1, opacityFrom:.35, opacityTo:0, stops:[0,100], colorStops:[{offset:0,color:lineColor,opacity:.35},{offset:100,color:lineColor,opacity:0}] } },
tooltip:{ theme:'light', y:{formatter:v=>(v>=0?'+':'')+`$${v.toFixed(2)}`} },
grid:{ borderColor:'#f1f5f9', strokeDashArray:3 },
colors:[lineColor],
});
window._apexEquity.render();
}
}
// ─────────────────────────────────────────────────────────────
// REPORTS TAB — TraderVue-style full stats table
// ─────────────────────────────────────────────────────────────
function renderReportsTabAP() {
const trades = state.trades || [];
const closed = trades.filter(t=>t.status==='win'||t.status==='loss');
if (closed.length===0) return `<div class="card"><div class="card-body text-center py-5 text-muted">Close some trades to see reports.</div></div>`;
const withPnl = closed.map(t=>({...t,_pnl:_pnl(t)}));
// Core stats
const netPnl = withPnl.reduce((s,t)=>s+t._pnl,0);
const wins = withPnl.filter(t=>t._pnl>0);
const losses = withPnl.filter(t=>t._pnl<=0);
const totalWinAmt = wins.reduce((s,t)=>s+t._pnl,0);
const totalLossAmt= Math.abs(losses.reduce((s,t)=>s+t._pnl,0));
const profitFactor= totalLossAmt>0?(totalWinAmt/totalLossAmt).toFixed(2):totalWinAmt>0?'∞':'0.00';
const winRate = withPnl.length>0?Math.round(wins.length/withPnl.length*100):0;
const avgWin = wins.length>0?totalWinAmt/wins.length:0;
const avgLoss = losses.length>0?totalLossAmt/losses.length:0;
const expectancy = withPnl.length>0?netPnl/withPnl.length:0;
const largestGain = wins.length>0?Math.max(...wins.map(t=>t._pnl)):0;
const largestLoss = losses.length>0?Math.max(...losses.map(t=>Math.abs(t._pnl))):0;
// Standard deviation
const mean = expectancy;
const variance = withPnl.length>1?withPnl.reduce((s,t)=>s+Math.pow(t._pnl-mean,2),0)/(withPnl.length-1):0;
const stdDev = Math.sqrt(variance);
// Daily breakdown — win days vs loss days
const dailyMap = {};
withPnl.forEach(t=>{
const day = (t.trade_date||t.created_at||'').substring(0,10);
if(!day||day==='') return;
if(!dailyMap[day]) dailyMap[day]={pnl:0,trades:0,wins:0};
dailyMap[day].pnl+=t._pnl; dailyMap[day].trades++; if(t._pnl>0) dailyMap[day].wins++;
});
const dailyArr = Object.values(dailyMap);
const winDays = dailyArr.filter(d=>d.pnl>0);
const lossDays = dailyArr.filter(d=>d.pnl<=0);
const avgDailyPnl = dailyArr.length>0?netPnl/dailyArr.length:0;
// Kelly %
const p = winRate/100;
const b = avgLoss>0?avgWin/avgLoss:0;
const kelly = b>0?Math.max(0,((p*b-(1-p))/b)*100).toFixed(1):'—';
// Max consecutive
let maxWinStreak=0, maxLossStreak=0, curW=0, curL=0;
withPnl.forEach(t=>{ if(t._pnl>0){curW++;curL=0;maxWinStreak=Math.max(maxWinStreak,curW);}else{curL++;curW=0;maxLossStreak=Math.max(maxLossStreak,curL);} });
// Cumulative for drawdown
let cum=0, peak=0, maxDD=0;
withPnl.forEach(t=>{ cum+=t._pnl; if(cum>peak)peak=cum; const dd=peak-cum; if(dd>maxDD)maxDD=dd; });
const statRow = (label, val, cls='') => `
<tr>
<td class="f-13 text-muted py-2">${label}</td>
<td class="f-13 fw-semibold py-2 text-end ${cls}">${val}</td>
</tr>`;
// Best/worst trades
const sorted = [...withPnl].sort((a,b)=>b._pnl-a._pnl);
const best5 = sorted.slice(0,5);
const worst5 = sorted.slice(-5).reverse();
const fmtDate = d => d?(new Date((d||'').substring(0,10)+'T00:00:00')).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'2-digit'}):'—';
return `
<!-- Charts row -->
<div class="row g-4 mb-4">
<div class="col-md-8">
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between py-3">
<h6 class="mb-0 fw-semibold">Daily P&L</h6>
<div class="d-flex gap-2">
<span class="badge bg-light-success text-success">${winDays.length} green days</span>
<span class="badge bg-light-danger text-danger">${lossDays.length} red days</span>
</div>
</div>
<div class="card-body p-2"><div id="apex-daily" style="height:220px"></div></div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-header py-3"><h6 class="mb-0 fw-semibold">Cumulative P&L</h6></div>
<div class="card-body p-2"><div id="apex-cum" style="height:220px"></div></div>
</div>
</div>
</div>
<!-- Full stats table — TraderVue style -->
<div class="card mb-4">
<div class="card-header d-flex align-items-center gap-2 py-3">
<i class="ti ti-report-analytics text-primary f-18"></i>
<h6 class="mb-0 fw-semibold">Detailed Statistics</h6>
</div>
<div class="card-body p-0">
<div class="row g-0">
<!-- Column 1 -->
<div class="col-md-4 border-end">
<div class="px-3 py-2 border-bottom bg-light">
<span class="f-11 fw-bold text-muted text-uppercase tracking-wider">P&L Summary</span>
</div>
<table class="table table-sm mb-0">
<tbody>
${statRow('Total Gain / Loss', _fc(netPnl), _cc(netPnl))}
${statRow('Profit Factor', profitFactor, _cc(parseFloat(profitFactor)-1))}
${statRow('Expectancy (per trade)', _fc(expectancy), _cc(expectancy))}
${statRow('Largest Gain', '+$'+largestGain.toFixed(2), 'text-success')}
${statRow('Largest Loss', '-$'+largestLoss.toFixed(2), 'text-danger')}
${statRow('Max Drawdown', '-$'+maxDD.toFixed(2), 'text-danger')}
${statRow('Std Deviation', '$'+stdDev.toFixed(2))}
</tbody>
</table>
<div class="px-3 py-2 border-bottom border-top bg-light mt-1">
<span class="f-11 fw-bold text-muted text-uppercase">Winning Days</span>
</div>
<table class="table table-sm mb-0">
<tbody>
${statRow('Total Win Days', winDays.length, 'text-success')}
${statRow('Avg Win Day P&L', winDays.length>0?_fc(winDays.reduce((s,d)=>s+d.pnl,0)/winDays.length):'—', 'text-success')}
${statRow('Best Day', '+$'+Math.max(0,...winDays.map(d=>d.pnl)).toFixed(2), 'text-success')}
</tbody>
</table>
</div>
<!-- Column 2 -->
<div class="col-md-4 border-end">
<div class="px-3 py-2 border-bottom bg-light">
<span class="f-11 fw-bold text-muted text-uppercase">Win / Loss Breakdown</span>
</div>
<table class="table table-sm mb-0">
<tbody>
${statRow('Win Rate', winRate+'%', _cc(winRate-50))}
${statRow('Winning Trades', wins.length+' ('+winRate+'%)', 'text-success')}
${statRow('Losing Trades', losses.length+' ('+(100-winRate)+'%)', 'text-danger')}
${statRow('Avg Winning Trade', '+$'+avgWin.toFixed(2), 'text-success')}
${statRow('Avg Losing Trade', '-$'+avgLoss.toFixed(2), 'text-danger')}
${statRow('Avg Trade (all)', _fc(expectancy), _cc(expectancy))}
${statRow('Total Trades', withPnl.length)}
</tbody>
</table>
<div class="px-3 py-2 border-bottom border-top bg-light mt-1">
<span class="f-11 fw-bold text-muted text-uppercase">Losing Days</span>
</div>
<table class="table table-sm mb-0">
<tbody>
${statRow('Total Loss Days', lossDays.length, 'text-danger')}
${statRow('Avg Loss Day P&L', lossDays.length>0?_fc(lossDays.reduce((s,d)=>s+d.pnl,0)/lossDays.length):'—', 'text-danger')}
${statRow('Worst Day', '-$'+Math.abs(Math.min(0,...lossDays.map(d=>d.pnl))).toFixed(2), 'text-danger')}
</tbody>
</table>
</div>
<!-- Column 3 -->
<div class="col-md-4">
<div class="px-3 py-2 border-bottom bg-light">
<span class="f-11 fw-bold text-muted text-uppercase">Advanced Metrics</span>
</div>
<table class="table table-sm mb-0">
<tbody>
${statRow('Avg Daily Gain/Loss', _fc(avgDailyPnl), _cc(avgDailyPnl))}
${statRow('Trading Days', dailyArr.length)}
${statRow('Max Consec. Wins', maxWinStreak, 'text-success')}
${statRow('Max Consec. Losses', maxLossStreak, 'text-danger')}
${statRow('Kelly Percentage', kelly!=='—'?kelly+'%':'—')}
${statRow('Total Fees', '$'+(withPnl.reduce((s,t)=>s+parseFloat(t.fees||0),0)).toFixed(2), 'text-muted')}
${(()=>{ const rrs=(state.trades||[]).filter(t=>t.status==='win'||t.status==='loss').filter(t=>parseFloat(t.reward_risk_ratio||0)>0).map(t=>parseFloat(t.reward_risk_ratio)); const avgRR=rrs.length>0?(rrs.reduce((a,b)=>a+b,0)/rrs.length).toFixed(2):'—'; return statRow('Avg R:R', rrs.length>0?'1:'+avgRR:'—', 'text-primary'); })()}
</tbody>
</table>
<div class="px-3 py-2 border-bottom border-top bg-light mt-1">
<span class="f-11 fw-bold text-muted text-uppercase">Win / Loss Day Ratio</span>
</div>
<div class="px-3 py-3">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="f-12 text-muted" style="width:80px">Win Days</span>
<div class="flex-fill" style="background:#f1f5f9;border-radius:4px;height:10px">
<div style="background:#10b981;height:10px;border-radius:4px;width:${dailyArr.length>0?Math.round(winDays.length/dailyArr.length*100):0}%"></div>
</div>
<span class="f-12 text-success fw-semibold">${dailyArr.length>0?Math.round(winDays.length/dailyArr.length*100):0}%</span>
</div>
<div class="d-flex align-items-center gap-2">
<span class="f-12 text-muted" style="width:80px">Loss Days</span>
<div class="flex-fill" style="background:#f1f5f9;border-radius:4px;height:10px">
<div style="background:#ef4444;height:10px;border-radius:4px;width:${dailyArr.length>0?Math.round(lossDays.length/dailyArr.length*100):0}%"></div>
</div>
<span class="f-12 text-danger fw-semibold">${dailyArr.length>0?Math.round(lossDays.length/dailyArr.length*100):0}%</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Best & Worst Trades -->
<div class="row g-4">
<div class="col-md-6">
<div class="card">
<div class="card-header py-3 d-flex align-items-center gap-2">
<i class="ti ti-trophy text-warning"></i>
<h6 class="mb-0 fw-semibold">Best Trades</h6>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0 f-13">
<thead class="table-light"><tr><th>Symbol</th><th>Date</th><th class="text-end">P&L</th></tr></thead>
<tbody>
${best5.map(t=>`<tr style="cursor:pointer" onclick="openTradeDetailModal(window._tradeRegistry[${t.id}])">
<td class="fw-semibold">${t.coin||'—'}</td>
<td class="text-muted">${fmtDate(t.trade_date||t.created_at)}</td>
<td class="text-end fw-bold text-success">${_fc(t._pnl)}</td>
</tr>`).join('')}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header py-3 d-flex align-items-center gap-2">
<i class="ti ti-alert-triangle text-danger"></i>
<h6 class="mb-0 fw-semibold">Worst Trades</h6>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0 f-13">
<thead class="table-light"><tr><th>Symbol</th><th>Date</th><th class="text-end">P&L</th></tr></thead>
<tbody>
${worst5.map(t=>`<tr style="cursor:pointer" onclick="openTradeDetailModal(window._tradeRegistry[${t.id}])">
<td class="fw-semibold">${t.coin||'—'}</td>
<td class="text-muted">${fmtDate(t.trade_date||t.created_at)}</td>
<td class="text-end fw-bold text-danger">${_fc(t._pnl)}</td>
</tr>`).join('')}
</tbody>
</table>
</div>
</div>
</div>
</div>`;
}
function initReportsChartsAP() {
const trades = state.trades||[];
const closed = trades.filter(t=>t.status==='win'||t.status==='loss');
if(!closed.length) return;
const withPnl = closed.map(t=>({...t,_pnl:_pnl(t)}));
// Daily map using trade_date
const dailyMap={};
withPnl.forEach(t=>{
const day=(t.trade_date||t.created_at||'').substring(0,10);
if(!day) return;
dailyMap[day]=(dailyMap[day]||0)+t._pnl;
});
const dailyEntries=Object.entries(dailyMap).sort((a,b)=>a[0].localeCompare(b[0]));
const dailyLabels=dailyEntries.map(([d])=>d);
const dailyValues=dailyEntries.map(([,v])=>+v.toFixed(2));
let cum=0;
const cumValues=dailyValues.map(v=>+(cum+=v).toFixed(2));
const finalVal=cumValues[cumValues.length-1]||0;
if(!window.ApexCharts) return;
// Daily P&L
if(document.getElementById('apex-daily')) {
if(window._apexDaily){try{window._apexDaily.destroy();}catch(e){}}
window._apexDaily=new ApexCharts(document.getElementById('apex-daily'),{
chart:{type:'bar',height:220,toolbar:{show:false}},
series:[{name:'Daily P&L',data:dailyValues}],
xaxis:{categories:dailyLabels,labels:{style:{colors:'#94a3b8',fontSize:'10px'},rotate:-45},axisBorder:{show:false}},
yaxis:{labels:{style:{colors:'#94a3b8',fontSize:'11px'},formatter:v=>'$'+v.toFixed(0)}},
colors:[function({value}){return value>=0?'#10b981':'#ef4444';}],
plotOptions:{bar:{borderRadius:4,columnWidth:'60%'}},
tooltip:{theme:'light',y:{formatter:v=>(v>=0?'+':'')+`$${v.toFixed(2)}`}},
grid:{borderColor:'#f1f5f9',strokeDashArray:3},
});
window._apexDaily.render();
}
// Cumulative
if(document.getElementById('apex-cum')) {
if(window._apexCum){try{window._apexCum.destroy();}catch(e){}}
const lc=finalVal>=0?'#10b981':'#ef4444';
window._apexCum=new ApexCharts(document.getElementById('apex-cum'),{
chart:{type:'area',height:220,toolbar:{show:false}},
series:[{name:'Cumulative',data:cumValues}],
xaxis:{categories:dailyLabels,labels:{show:false},axisBorder:{show:false}},
yaxis:{labels:{style:{colors:'#94a3b8',fontSize:'11px'},formatter:v=>'$'+v.toFixed(0)}},
stroke:{curve:'smooth',width:2,colors:[lc]},
fill:{type:'gradient',gradient:{shadeIntensity:1,opacityFrom:.3,opacityTo:0,stops:[0,100]}},
colors:[lc],
tooltip:{theme:'light',y:{formatter:v=>(v>=0?'+':'')+`$${v.toFixed(2)}`}},
grid:{borderColor:'#f1f5f9',strokeDashArray:3},
});
window._apexCum.render();
}
}
// ─────────────────────────────────────────────────────────────
// DAYS / TIMES TAB
// ─────────────────────────────────────────────────────────────
function renderDaysTimesTabAP() {
return `
<div class="row g-4">
<div class="col-md-6">
<div class="card"><div class="card-header py-3"><h6 class="mb-0 fw-semibold">P&L by Day of Week</h6></div>
<div class="card-body p-2"><div id="apex-dow-pnl" style="height:240px"></div></div></div>
</div>
<div class="col-md-6">
<div class="card"><div class="card-header py-3"><h6 class="mb-0 fw-semibold">Win Rate by Day of Week</h6></div>
<div class="card-body p-2"><div id="apex-dow-wr" style="height:240px"></div></div></div>
</div>
<div class="col-md-6">
<div class="card"><div class="card-header py-3"><h6 class="mb-0 fw-semibold">P&L by Hour of Day</h6></div>
<div class="card-body p-2"><div id="apex-hour-pnl" style="height:260px"></div></div></div>
</div>
<div class="col-md-6">
<div class="card"><div class="card-header py-3"><h6 class="mb-0 fw-semibold">Trade Count by Hour</h6></div>
<div class="card-body p-2"><div id="apex-hour-count" style="height:260px"></div></div></div>
</div>
</div>`;
}
function initDaysTimesChartsAP() {
const trades = state.trades||[];
const closed = trades.filter(t=>t.status==='win'||t.status==='loss');
if(!closed.length||!window.ApexCharts) return;
const withPnl = closed.map(t=>({...t,_pnl:_pnl(t)}));
// Day of week
const dowNames=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const dowPnl =Array(7).fill(0), dowCount=Array(7).fill(0), dowWins=Array(7).fill(0);
withPnl.forEach(t=>{
const raw=(t.trade_date||t.created_at||'').substring(0,10);
if(!raw) return;
const d=new Date(raw+'T00:00:00').getDay();
dowPnl[d]+=t._pnl; dowCount[d]++; if(t._pnl>0) dowWins[d]++;
});
const dowWr=dowCount.map((c,i)=>c>0?Math.round(dowWins[i]/c*100):null);
// DOW P&L bar
if(document.getElementById('apex-dow-pnl')){
if(window._apexDowPnl){try{window._apexDowPnl.destroy();}catch(e){}}
window._apexDowPnl=new ApexCharts(document.getElementById('apex-dow-pnl'),{
chart:{type:'bar',height:240,toolbar:{show:false}},
series:[{name:'P&L',data:dowPnl.map(v=>+v.toFixed(2))}],
xaxis:{categories:dowNames,labels:{style:{colors:'#94a3b8'}}},
yaxis:{labels:{style:{colors:'#94a3b8'},formatter:v=>'$'+v.toFixed(0)}},
colors:[function({value}){return value>=0?'#10b981':'#ef4444';}],
plotOptions:{bar:{borderRadius:5}},
tooltip:{theme:'light',y:{formatter:v=>(v>=0?'+':'')+`$${v.toFixed(2)}`}},
grid:{borderColor:'#f1f5f9',strokeDashArray:3},
});
window._apexDowPnl.render();
}
// DOW win rate
if(document.getElementById('apex-dow-wr')){
if(window._apexDowWr){try{window._apexDowWr.destroy();}catch(e){}}
window._apexDowWr=new ApexCharts(document.getElementById('apex-dow-wr'),{
chart:{type:'bar',height:240,toolbar:{show:false}},
series:[{name:'Win Rate',data:dowWr}],
xaxis:{categories:dowNames,labels:{style:{colors:'#94a3b8'}}},
yaxis:{min:0,max:100,labels:{style:{colors:'#94a3b8'},formatter:v=>v+'%'}},
colors:[function({value}){return value===null?'#cbd5e1':value>=50?'#10b981':'#ef4444';}],
plotOptions:{bar:{borderRadius:5}},
tooltip:{theme:'light',y:{formatter:v=>v!==null?v+'% win rate':'No trades'}},
grid:{borderColor:'#f1f5f9',strokeDashArray:3},
});
window._apexDowWr.render();
}
// Hour stats
const hourPnl=Array(24).fill(0), hourCount=Array(24).fill(0);
withPnl.forEach(t=>{
const raw=(t.created_at||'');
const hMatch=raw.match(/T?(\d{2}):/)||raw.match(/\s(\d{2}):/);
if(!hMatch) return;
const h=parseInt(hMatch[1]);
hourPnl[h]+=t._pnl; hourCount[h]++;
});
const activeHours=hourPnl.map((v,i)=>({h:i,pnl:v,count:hourCount[i]})).filter(x=>x.count>0);
if(document.getElementById('apex-hour-pnl')&&activeHours.length>0){
if(window._apexHourPnl){try{window._apexHourPnl.destroy();}catch(e){}}
window._apexHourPnl=new ApexCharts(document.getElementById('apex-hour-pnl'),{
chart:{type:'bar',height:260,toolbar:{show:false}},
series:[{name:'P&L',data:activeHours.map(x=>+x.pnl.toFixed(2))}],
xaxis:{categories:activeHours.map(x=>x.h+':00'),labels:{style:{colors:'#94a3b8',fontSize:'10px'}}},
yaxis:{labels:{style:{colors:'#94a3b8'},formatter:v=>'$'+v.toFixed(0)}},
colors:[function({value}){return value>=0?'#10b981':'#ef4444';}],
plotOptions:{bar:{borderRadius:4,horizontal:true}},
tooltip:{theme:'light',y:{formatter:v=>(v>=0?'+':'')+`$${v.toFixed(2)}`}},
grid:{borderColor:'#f1f5f9',strokeDashArray:3},
});
window._apexHourPnl.render();
}
if(document.getElementById('apex-hour-count')&&activeHours.length>0){
if(window._apexHourCount){try{window._apexHourCount.destroy();}catch(e){}}
window._apexHourCount=new ApexCharts(document.getElementById('apex-hour-count'),{
chart:{type:'bar',height:260,toolbar:{show:false}},
series:[{name:'Trades',data:activeHours.map(x=>x.count)}],
xaxis:{categories:activeHours.map(x=>x.h+':00'),labels:{style:{colors:'#94a3b8',fontSize:'10px'}}},
yaxis:{labels:{style:{colors:'#94a3b8'},formatter:v=>Math.round(v)+''}},
colors:['#4680FF'],
plotOptions:{bar:{borderRadius:4,horizontal:true}},
tooltip:{theme:'light'},
grid:{borderColor:'#f1f5f9',strokeDashArray:3},
});
window._apexHourCount.render();
}
}
// ── switchAnalyticsTab compatibility shim ─────────────────────
// (some buttons in analytics.js still call switchAnalyticsTab)
window.switchAnalyticsTab = function(tab) {
const map = {overview:'overview',category:'strategy',psychology:'psychology',timing:'days-times',reports:'reports'};
const apTab = map[tab]||tab;
const btn = [...document.querySelectorAll('#analytics-tabs .nav-link')]
.find(b=>b.getAttribute('onclick')&&b.getAttribute('onclick').includes(`'${apTab}'`));
switchAnalyticsTabAP(apTab, btn||null);
};