TRAXOR
Trading Monitor
P&L Global Hoy
+$0.00
Cuentas Activas
0
Dashboard
P&L Total
$0.00
Total acumulado
Max Drawdown
0.00%
Peor racha
Operaciones
0
Total histórico
Win Rate
0%
Promedio cuentas
Mis Cuentas
📊
Sin cuentas todavía
Agrega tu primera cuenta de trading para comenzar
Calendario Global
P&L por día · Todas las cuentas combinadas
Mayo 2026
Estadísticas Globales
Resumen de todas las cuentas
P&L Total
$0
Ops Totales
0
Win Rate
0%
Drawdown Máx
0%
Progreso Combinado
➕ Nueva Cuenta
📈 Nueva Operación
Cuenta
Resultado del día
$
Actualizar Axi SELECT
Actualiza tus métricas actuales
IC `, 'FundedNext': ` F N `, 'Darwinex': ` ZERO `, 'Axi': ` axi `, }; const cfg = BROKER_CONFIG[broker] || {}; if(logos[broker]) return logos[broker]; // Generic fallback with abbr const abbr = cfg.abbr || (broker||'?').substring(0,2).toUpperCase(); const color = cfg.color || '#00e5a0'; const textFill = isLight(color) ? '#000' : '#fff'; return ` ${abbr} `; } // ─── DELETE ACCOUNT ─── function deleteAccount(accId, e) { if(e) e.stopPropagation(); const acc = accounts.find(a => a.id === accId); if(!acc) return; if(acc.id === 'ic_demo_8y') { if(!confirm('¿Eliminar la cuenta demo?\nPerderás el historial de 8 años.')) return; } const name = acc.broker === 'Otro' ? (acc.customName||'Cuenta') : acc.broker; if(acc.id !== 'ic_demo_8y' && !confirm(`¿Eliminar la cuenta de ${name}?\n\nEsta acción no se puede deshacer.`)) return; accounts = accounts.filter(a => a.id !== accId); localStorage.removeItem('traxor_challenge_' + accId); saveAccounts(); if(currentAccountId === accId) closePanel(); renderAccounts(); showToast(`🗑️ Cuenta ${name} eliminada`); } // ─── INIT (moved to economic calendar section below) ─── function setTodayDate() { const d = new Date(); const opts = { weekday:'long', year:'numeric', month:'long', day:'numeric' }; document.getElementById('today-date').textContent = d.toLocaleDateString('es-ES', opts); } // ─── VIEWS (handled in economic calendar section below) ─── // ─── RENDER ACCOUNTS ─── function renderAccounts() { const grid = document.getElementById('accounts-grid'); const empty = document.getElementById('empty-state'); // Remove old cards (keep empty state) grid.querySelectorAll('.account-card').forEach(c => c.remove()); if(accounts.length === 0) { empty.style.display = ''; updateTopbar(); return; } empty.style.display = 'none'; accounts.forEach(acc => { const card = createAccountCard(acc); grid.appendChild(card); }); updateTopbar(); updateOverviewStats(); } function createAccountCard(acc) { const totalPnl = acc.trades ? acc.trades.reduce((s,t) => s + t.pnl, 0) : acc.pnl || 0; const todayPnl = getTodayPnl(acc); const winRate = calcWinRate(acc); const sparkPath = generateSparkPath(acc); const cfg = BROKER_CONFIG[acc.broker] || {}; const logoColor = acc.color || cfg.color || '#00e5a0'; const pos = totalPnl >= 0; const brokerName = acc.broker === 'Otro' ? (acc.customName||'Custom') : acc.broker; const div = document.createElement('div'); div.className = 'account-card'; div.setAttribute('data-id', acc.id); div.innerHTML = `
${getBrokerLogoHTML(acc.broker, 44)}
${brokerName}
P&L Total
${pos ? '+' : ''}$${Math.abs(totalPnl).toFixed(2)}
Hoy
${todayPnl >= 0 ? '+' : ''}$${todayPnl.toFixed(2)}
Balance
$${formatNum(acc.balance || 0)}
Win Rate
${winRate}%
Trades
${acc.trades ? acc.trades.length : 0}
${getChallengeCardHTML(acc)} ${getAxiSelectCardHTML(acc)}
`; div.addEventListener('click', () => openPanel(acc.id)); return div; } // ─── SPARK PATH ─── function generateSparkPath(acc) { const trades = acc.trades || []; if(trades.length < 2) { return { line: 'M0,25 L260,25', area: 'M0,25 L260,25 L260,50 L0,50 Z' }; } let cumulative = 0; const values = [0, ...trades.map(t => { cumulative += t.pnl; return cumulative; })]; const min = Math.min(...values); const max = Math.max(...values); const range = max - min || 1; const pts = values.map((v, i) => { const x = (i / (values.length - 1)) * 260; const y = 45 - ((v - min) / range) * 40; return [x, y]; }); const line = 'M' + pts.map(p => p.join(',')).join(' L'); const area = line + ` L260,50 L0,50 Z`; return { line, area }; } // ─── HELPERS ─── function getTodayPnl(acc) { if(!acc.trades) return 0; const today = new Date().toISOString().split('T')[0]; return acc.trades.filter(t => t.date === today).reduce((s,t) => s+t.pnl, 0); } function calcWinRate(acc) { if(!acc.trades || !acc.trades.length) return 0; const wins = acc.trades.filter(t => t.pnl > 0).length; return Math.round((wins / acc.trades.length) * 100); } function calcDrawdown(acc) { if(!acc.trades || !acc.trades.length) return 0; let peak = 0, dd = 0, cum = 0; acc.trades.forEach(t => { cum += t.pnl; if(cum > peak) peak = cum; const d = peak > 0 ? ((peak - cum) / peak) * 100 : 0; if(d > dd) dd = d; }); return Math.round(dd * 10) / 10; } function formatNum(n) { if(n >= 1000) return (n/1000).toFixed(1)+'K'; return n.toFixed(0); } function isLight(hex) { const c = hex.replace('#',''); const r = parseInt(c.substring(0,2),16); const g = parseInt(c.substring(2,4),16); const b = parseInt(c.substring(4,6),16); return (0.299*r + 0.587*g + 0.114*b) > 128; } // ─── STATS UPDATES ─── function updateTopbar() { const todayTotal = accounts.reduce((s,a) => s + getTodayPnl(a), 0); const pos = todayTotal >= 0; const gtEl = document.getElementById('global-today'); const acEl = document.getElementById('active-count'); if(gtEl) { gtEl.textContent = (pos?'+':'') + '$' + todayTotal.toFixed(2); gtEl.style.color = pos ? 'var(--green)' : 'var(--red)'; } if(acEl) acEl.textContent = accounts.filter(a => a.active !== false).length; } function updateOverviewStats() { const totalPnl = accounts.reduce((s,a) => { const tp = (a.trades||[]).reduce((x,t)=>x+t.pnl,0); return s + tp; }, 0); const maxDD = accounts.reduce((m,a) => Math.max(m, calcDrawdown(a)), 0); const totalOps = accounts.reduce((s,a) => s + (a.trades||[]).length, 0); const avgWR = accounts.length ? Math.round(accounts.reduce((s,a)=>s+calcWinRate(a),0)/accounts.length) : 0; const safeSet = (id, val) => { const el = document.getElementById(id); if(el) el.textContent = val; }; safeSet('stat-total-pnl', (totalPnl>=0?'+':'') + '$' + Math.abs(totalPnl).toFixed(2)); safeSet('stat-drawdown', maxDD.toFixed(1) + '%'); safeSet('stat-ops', totalOps); safeSet('stat-winrate', avgWR + '%'); } function updateGlobalStats() { const totalPnl = accounts.reduce((s,a) => s + (a.trades||[]).reduce((x,t)=>x+t.pnl,0), 0); const totalOps = accounts.reduce((s,a) => s + (a.trades||[]).length, 0); const avgWR = accounts.length ? Math.round(accounts.reduce((s,a)=>s+calcWinRate(a),0)/accounts.length) : 0; const maxDD = accounts.reduce((m,a) => Math.max(m, calcDrawdown(a)), 0); const safeSet = (id, val) => { const el = document.getElementById(id); if(el) el.textContent = val; }; safeSet('gs-pnl', (totalPnl>=0?'+':'')+'$'+Math.abs(totalPnl).toFixed(2)); safeSet('gs-ops', totalOps); safeSet('gs-wr', avgWR+'%'); safeSet('gs-dd', maxDD.toFixed(1)+'%'); } // ─── PANEL ─── function openPanel(id) { currentAccountId = id; const acc = accounts.find(a => a.id === id); if(!acc) return; const safeEl = (id) => document.getElementById(id); const safeSet = (id, prop, val) => { const e = safeEl(id); if(e) e[prop] = val; }; const logoEl = safeEl('panel-logo'); if(logoEl) logoEl.innerHTML = getBrokerLogoHTML(acc.broker, 44); safeSet('panel-broker-name','textContent', acc.broker === 'Otro' ? (acc.customName||'Custom') : acc.broker); safeSet('panel-account-num','textContent', `${acc.accountNum||'#------'} · ${acc.type||'Live'} · Balance: $${(acc.balance||0).toLocaleString()}`); // Reset tabs document.querySelectorAll('.panel-tab').forEach((t,i) => t.classList.toggle('active', i===0)); document.querySelectorAll('.tab-content').forEach((t,i) => t.classList.toggle('active', i===0)); // Populate overview renderPanelMetrics(acc); drawPanelChart(acc); // Populate trades renderTradesTable(acc); // Populate calendar renderCalendar(acc); // Populate drawdown renderDrawdown(acc); // Show/hide challenge tab const challengeTab = document.querySelector('.challenge-tab'); if(challengeTab) challengeTab.style.display = isChallengeBroker(acc) ? '' : 'none'; // Show/hide Axi SELECT tab const axiTab = document.querySelector('.axiselect-tab'); if(axiTab) axiTab.style.display = acc.broker === 'Axi' ? '' : 'none'; // Populate challenge renderChallenge(acc); renderYearly(acc); // Populate Axi SELECT renderAxiSelect(acc); const overlay = document.getElementById('panel-overlay'); if(overlay) { overlay.classList.add('open'); document.body.style.overflow = 'hidden'; } } function closePanel() { const overlay = document.getElementById('panel-overlay'); if(overlay) overlay.classList.remove('open'); document.body.style.overflow = ''; currentAccountId = null; } function closePanelOnOverlay(e) { if(e.target === document.getElementById('panel-overlay')) closePanel(); } // ─── PANEL METRICS ─── function renderPanelMetrics(acc) { const totalPnl = (acc.trades||[]).reduce((s,t)=>s+t.pnl, 0); const todayPnl = getTodayPnl(acc); const wr = calcWinRate(acc); const dd = calcDrawdown(acc); const pos = totalPnl >= 0; const todayPos = todayPnl >= 0; const pmEl = document.getElementById('panel-metrics'); if(!pmEl) return; pmEl.innerHTML = `
P&L Total
${pos?'+':''}$${Math.abs(totalPnl).toFixed(2)}
P&L Hoy
${todayPos?'+':''}$${todayPnl.toFixed(2)}
Win Rate
${wr}%
Drawdown
${dd}%
`; } // ─── PANEL CHART ─── function drawPanelChart(acc) { const svg = document.getElementById('panel-chart-svg'); drawLineChart(svg, acc, 600, 140); } function drawGlobalChart() { const svg = document.getElementById('global-chart-svg'); const allTrades = accounts.flatMap(a => (a.trades||[]).map(t => ({...t}))); allTrades.sort((a,b) => a.date.localeCompare(b.date)); drawLineChart(svg, { trades: allTrades }, 600, 140); } function drawLineChart(svg, acc, w, h) { if(!svg) return; const trades = acc.trades || []; // Need at least 1 trade if(trades.length === 0) { svg.innerHTML = `Sin datos aún`; return; } const pad = { top: 22, right: 16, bottom: 22, left: 52 }; const cw = w - pad.left - pad.right; const ch = h - pad.top - pad.bottom; // Build cumulative values starting from 0 let cum = 0; const values = [0, ...trades.map(t => { cum += t.pnl; return cum; })]; const min = Math.min(...values); const max = Math.max(...values); // Add 8% padding above/below so line doesn't touch edges const rawRange = max - min || 1; const padV = rawRange * 0.08; const vMin = min - padV; const vMax = max + padV; const vRange = vMax - vMin; const toX = i => pad.left + (i / (values.length - 1 || 1)) * cw; const toY = v => pad.top + ch - ((v - vMin) / vRange) * ch; // Zero line Y position const zeroY = toY(0); const zeroVisible = zeroY >= pad.top && zeroY <= pad.top + ch; // Build points const pts = values.map((v, i) => [toX(i), toY(v)]); // Build SVG path segments colored by sign // We split the path at zero crossings function buildSegments() { const segs = []; // { color, points[] } let current = []; let currentSign = values[0] >= 0 ? 'pos' : 'neg'; for(let i = 0; i < pts.length; i++) { const sign = values[i] >= 0 ? 'pos' : 'neg'; if(sign !== currentSign && i > 0) { // Find zero crossing between i-1 and i const v0 = values[i-1], v1 = values[i]; const t = v0 / (v0 - v1); // interpolation factor const cx = pts[i-1][0] + t * (pts[i][0] - pts[i-1][0]); const cy = zeroY; current.push([cx, cy]); segs.push({ sign: currentSign, points: current }); current = [[cx, cy]]; currentSign = sign; } current.push(pts[i]); } if(current.length) segs.push({ sign: currentSign, points: current }); return segs; } const segments = buildSegments(); const GREEN = '#00e5a0'; const RED = '#ff4d6d'; // Build filled areas per segment function segArea(points, sign) { if(points.length < 2) return ''; const col = sign === 'pos' ? GREEN : RED; const linePath = 'M' + points.map(p => p.join(',')).join(' L'); // Close area down to zero line const areaPath = linePath + ` L${points[points.length-1][0]},${zeroY}` + ` L${points[0][0]},${zeroY} Z`; return ` `; } // Grid lines (3 horizontal) const gridVals = [vMin + vRange*0.25, vMin + vRange*0.5, vMin + vRange*0.75]; const gridLines = gridVals.map(v => { const gy = toY(v); return ``; }).join(''); // Labels const labelStyle = `font-family="Space Mono" font-size="9" fill="#4a5568"`; const totalPnl = values[values.length - 1]; const totalColor = totalPnl >= 0 ? GREEN : RED; const totalLabel = (totalPnl >= 0 ? '+' : '') + '$' + Math.abs(totalPnl).toFixed(2); // Dot at last point const lastPt = pts[pts.length - 1]; const lastColor = values[values.length-1] >= 0 ? GREEN : RED; svg.innerHTML = ` ${gridLines} ${zeroVisible ? ` $0` : ''} ${max>=0?'+':''}$${Math.abs(max).toFixed(0)} ${min>=0?'+':''}$${Math.abs(min).toFixed(0)} ${segments.map(s => segArea(s.points, s.sign)).join('')} ${totalLabel} `; } // ─── TRADES TABLE ─── function renderTradesTable(acc) { const tbody = document.getElementById('trades-tbody'); const countEl = document.getElementById('trades-count'); if(!tbody) return; const trades = acc.trades || []; if(countEl) countEl.textContent = `${trades.length} operación${trades.length !== 1 ? 'es' : ''}`; if(!trades.length) { tbody.innerHTML = `Sin operaciones registradas. Agrega una manualmente.`; return; } tbody.innerHTML = [...trades].reverse().map((t,i) => ` ${t.date||'—'} ${t.pair||'—'} ${t.type||'—'} ${t.lots||'—'} ${t.entry||'—'} ${t.exit||'—'} ${t.pnl>=0?'+':''}$${Math.abs(t.pnl).toFixed(2)} `).join(''); } // ─── CALENDAR ─── function renderCalendar(acc) { const year = 2026, month = 4; // May 2026 (0-indexed) const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month+1, 0).getDate(); const today = new Date(); // Build day→pnl map const dayMap = {}; (acc.trades||[]).forEach(t => { const d = t.date; if(!dayMap[d]) dayMap[d] = 0; dayMap[d] += t.pnl; }); const grid = document.getElementById('cal-grid'); grid.innerHTML = ''; ['D','L','M','X','J','V','S'].forEach(d => { const h = document.createElement('div'); h.className = 'cal-day-header'; h.textContent = d; grid.appendChild(h); }); // Adjust: 0=Sun=last col const startOffset = firstDay === 0 ? 6 : firstDay - 1; for(let i = 0; i < startOffset; i++) { const e = document.createElement('div'); e.className = 'cal-day empty'; grid.appendChild(e); } for(let d = 1; d <= daysInMonth; d++) { const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`; const pnl = dayMap[dateStr]; const isToday = today.getFullYear()===year && today.getMonth()===month && today.getDate()===d; const el = document.createElement('div'); el.className = 'cal-day' + (pnl !== undefined ? (pnl >= 0 ? ' profit' : ' loss') : '') + (isToday ? ' today' : ''); el.innerHTML = `${d}${pnl !== undefined ? `${pnl>=0?'+':''}${pnl.toFixed(0)}` : ''}`; if(pnl !== undefined) el.title = `${dateStr}: ${pnl>=0?'+':''}$${pnl.toFixed(2)}`; grid.appendChild(el); } } function renderGlobalCalendar() { const year = 2026, month = 4; const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month+1, 0).getDate(); const today = new Date(); const dayMap = {}; accounts.forEach(acc => { (acc.trades||[]).forEach(t => { if(!dayMap[t.date]) dayMap[t.date] = 0; dayMap[t.date] += t.pnl; }); }); const grid = document.getElementById('global-calendar'); grid.innerHTML = ''; grid.className = 'cal-grid'; ['D','L','M','X','J','V','S'].forEach(d => { const h = document.createElement('div'); h.className = 'cal-day-header'; h.textContent = d; grid.appendChild(h); }); const startOffset = firstDay === 0 ? 6 : firstDay - 1; for(let i = 0; i < startOffset; i++) { const e = document.createElement('div'); e.className = 'cal-day empty'; grid.appendChild(e); } for(let d = 1; d <= daysInMonth; d++) { const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`; const pnl = dayMap[dateStr]; const isToday = today.getDate()===d && today.getMonth()===month; const el = document.createElement('div'); el.className = 'cal-day' + (pnl!==undefined?(pnl>=0?' profit':' loss'):'') + (isToday?' today':''); el.innerHTML = `${d}${pnl!==undefined?`${pnl>=0?'+':''}${pnl.toFixed(0)}`:''}`; grid.appendChild(el); } } // ─── DRAWDOWN ─── function renderDrawdown(acc) { const trades = acc.trades || []; const totalPnl = trades.reduce((s,t)=>s+t.pnl,0); const dd = calcDrawdown(acc); const balance = (acc.balance||10000) + totalPnl; const safeSet = (id, v) => { const e = document.getElementById(id); if(e) e.textContent = v; }; safeSet('dd-current', dd.toFixed(1) + '%'); safeSet('dd-max', dd.toFixed(1) + '%'); safeSet('dd-balance', '$' + balance.toFixed(2)); const bars = document.getElementById('dd-bars'); if(!bars) return; // Weekly groups const weekMap = {}; trades.forEach(t => { const d = new Date(t.date); const week = `W${Math.ceil(d.getDate()/7)}`; if(!weekMap[week]) weekMap[week] = 0; weekMap[week] += t.pnl; }); if(Object.keys(weekMap).length === 0) { bars.innerHTML = '
Sin datos de operaciones
'; return; } bars.innerHTML = Object.entries(weekMap).map(([w, pnl]) => { const pct = Math.min(Math.abs(pnl) / (acc.balance||10000) * 100, 100); const cls = pct < 3 ? 'safe' : pct < 7 ? 'warn' : 'danger'; return `
${w} ${pnl>=0?'+':''}$${pnl.toFixed(2)}
`; }).join(''); } // ─── TABS ─── function switchTab(name, el) { document.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active')); if(el) el.classList.add('active'); const tabEl = document.getElementById('tab-'+name); if(tabEl) tabEl.classList.add('active'); } // ─── ADD ACCOUNT ─── function openAddModal() { const el = document.getElementById('add-modal-overlay'); if(el) el.classList.add('open'); } function closeAddModal() { const el = document.getElementById('add-modal-overlay'); if(el) el.classList.remove('open'); } function updateLogoPreview() { const el = document.getElementById('new-broker'); const grp = document.getElementById('custom-name-group'); const val = el ? el.value : ''; if(grp) grp.style.display = val === 'Otro' ? '' : 'none'; } function selectColor(el) { document.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected')); el.classList.add('selected'); selectedColor = el.dataset.color; } function addAccount() { const g = (id) => { const e = document.getElementById(id); return e ? e.value : ''; }; const broker = g('new-broker'); if(!broker) { showToast('⚠️ Selecciona un broker'); return; } const customName = g('new-custom-name'); const accountNum = g('new-account-num') || '#' + Math.floor(Math.random()*99999); const balance = parseFloat(g('new-balance')) || 10000; const type = g('new-type') || 'Live'; // Demo account if(broker === '__DEMO__') { loadICDemo(); return; } const acc = { id: Date.now().toString(), broker, customName, accountNum, balance, type, color: selectedColor, active: true, trades: [], screenshots: [], createdAt: new Date().toISOString() }; accounts.push(acc); saveAccounts(); renderAccounts(); closeAddModal(); showToast(`✅ Cuenta ${broker} agregada`); ['new-broker','new-account-num','new-balance'].forEach(id => { const e = document.getElementById(id); if(e) e.value = ''; }); } // ─── ADD TRADE ─── function openAddTradeModal() { const el = document.getElementById('add-trade-modal-overlay'); if(el) el.classList.add('open'); } function closeAddTradeModal() { const el = document.getElementById('add-trade-modal-overlay'); if(el) el.classList.remove('open'); } function addTrade() { const acc = accounts.find(a => a.id === currentAccountId); if(!acc) return; const g = (id) => { const e = document.getElementById(id); return e ? e.value : ''; }; const pair = g('t-pair') || '—'; const type = g('t-type') || 'BUY'; const lots = g('t-lots') || '0.01'; const date = g('t-date') || new Date().toISOString().split('T')[0]; const entry = g('t-entry') || '0'; const exit = g('t-exit') || '0'; const pnl = parseFloat(g('t-pnl')) || 0; if(!acc.trades) acc.trades = []; acc.trades.push({ pair, type, lots: parseFloat(lots), date, entry: parseFloat(entry), exit: parseFloat(exit), pnl }); saveAccounts(); renderTradesTable(acc); renderCalendar(acc); renderPanelMetrics(acc); drawPanelChart(acc); renderDrawdown(acc); renderChallenge(acc); renderAccounts(); closeAddTradeModal(); showToast(`✅ Operación agregada · ${pnl>=0?'+':''}$${pnl.toFixed(2)}`); ['t-pair','t-lots','t-entry','t-exit','t-pnl'].forEach(id => { const e = document.getElementById(id); if(e) e.value = ''; }); } // ─── SCREENSHOT ─── // ─── SAVE ─── function saveAccounts() { localStorage.setItem('traxor_accounts', JSON.stringify(accounts)); } // ─── TOAST ─── function showToast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 3000); } // Initial date for trade form document.getElementById('t-date').value = new Date().toISOString().split('T')[0]; // ═══════════════════════════════════════════════ // ─── PERSONAL CHALLENGE DATA ─── // ═══════════════════════════════════════════════ const CHALLENGE_STEPS = [ { step:1, target:6, risk:4, balance:170 }, { step:2, target:8, risk:6, balance:178 }, { step:3, target:10, risk:8, balance:188 }, { step:4, target:14, risk:10, balance:202 }, { step:5, target:18, risk:14, balance:220 }, { step:6, target:22, risk:18, balance:242 }, { step:7, target:28, risk:22, balance:270 }, { step:8, target:38, risk:28, balance:308 }, { step:9, target:48, risk:38, balance:356 }, { step:10, target:64, risk:48, balance:420 }, { step:11, target:82, risk:64, balance:502 }, { step:12, target:108, risk:82, balance:610 }, { step:13, target:140, risk:108, balance:750 }, { step:14, target:182, risk:140, balance:932 }, { step:15, target:236, risk:182, balance:1187 }, { step:16, target:308, risk:236, balance:1476 }, { step:17, target:400, risk:308, balance:1876 }, { step:18, target:520, risk:400, balance:2396 }, { step:19, target:674, risk:520, balance:3070 }, { step:20, target:878, risk:674, balance:3948 }, { step:21, target:1140, risk:878, balance:5088 }, { step:22, target:1482, risk:1140, balance:6570 }, { step:23, target:1928, risk:1482, balance:8489 }, { step:24, target:2506, risk:1928, balance:11004 }, { step:25, target:3256, risk:2506, balance:14260 }, { step:26, target:4234, risk:3256, balance:18496 }, { step:27, target:5504, risk:4234, balance:23993 }, { step:28, target:7156, risk:5504, balance:31154 }, { step:29, target:9302, risk:7156, balance:40456 }, { step:30, target:12092,risk:9302, balance:52548 }, ]; function isChallengeBroker(acc) { const b = (acc.broker || '').toLowerCase(); return b.includes('ic') || b === 'axi'; } function getChallengeState(accId) { const raw = localStorage.getItem('traxor_challenge_' + accId); if(raw) return JSON.parse(raw); return { completedSteps: [] }; // array of step indexes (0-based) marked as done } function saveChallengeState(accId, state) { localStorage.setItem('traxor_challenge_' + accId, JSON.stringify(state)); } // Current active step = first step not marked as completed function getCurrentStepIndex(state) { for(let i = 0; i < CHALLENGE_STEPS.length; i++) { if(!state.completedSteps.includes(i)) return i; } return CHALLENGE_STEPS.length; // all done } // ─── MARK STEP COMPLETE (called from button) ─── function markStepComplete(accId, stepIndex) { const state = getChallengeState(accId); if(!state.completedSteps.includes(stepIndex)) { state.completedSteps.push(stepIndex); saveChallengeState(accId, state); } const acc = accounts.find(a => a.id === accId); if(acc) { renderChallenge(acc); renderAccounts(); // refresh card mini bar } } // ─── UNDO STEP (unmark last completed) ─── function undoStepComplete(accId, stepIndex) { const state = getChallengeState(accId); state.completedSteps = state.completedSteps.filter(i => i !== stepIndex); saveChallengeState(accId, state); const acc = accounts.find(a => a.id === accId); if(acc) { renderChallenge(acc); renderAccounts(); } } // ─── RENDER CHALLENGE TAB ─── function renderChallenge(acc) { const el = document.getElementById('challenge-content'); if(!el) return; if(!isChallengeBroker(acc)) { el.innerHTML = ''; return; } const state = getChallengeState(acc.id); const currentIdx = getCurrentStepIndex(state); const isFinished = currentIdx >= CHALLENGE_STEPS.length; const completedCount = state.completedSteps.length; const progressPct = Math.round((completedCount / CHALLENGE_STEPS.length) * 100); const current = isFinished ? CHALLENGE_STEPS[CHALLENGE_STEPS.length-1] : CHALLENGE_STEPS[currentIdx]; el.innerHTML = `
Step Actual
${isFinished ? '🏆' : currentIdx + 1}
${isFinished ? '
¡COMPLETADO!
' : ''}
Riesgo por Op.
${current.risk}€
Target del Step
${current.target}€
${completedCount} de ${CHALLENGE_STEPS.length} steps completados ${progressPct}%
${!isFinished ? `
` : ''}
Tabla de Steps
${completedCount} / ${CHALLENGE_STEPS.length} completados
${CHALLENGE_STEPS.map((s, i) => { const isDone = state.completedSteps.includes(i); const isCurrent = i === currentIdx && !isFinished; const cls = isDone ? 'done' : isCurrent ? 'current' : 'pending'; const actionBtn = isDone ? `` : isCurrent ? `` : ``; const statusLabel = isDone ? `` : isCurrent ? `▶ Aquí` : ''; return ` `; }).join('')}
Step Target Riesgo Step Balance Estado
${s.step} ${s.target}€ ${s.risk}€ ${s.balance.toLocaleString()}€
${statusLabel} ${actionBtn}
`; } // ─── CHALLENGE MINI WIDGET ON CARD ─── function getChallengeCardHTML(acc) { if(!isChallengeBroker(acc)) return ''; const state = getChallengeState(acc.id); const currentIdx = getCurrentStepIndex(state); const isFinished = currentIdx >= CHALLENGE_STEPS.length; const current = isFinished ? CHALLENGE_STEPS[CHALLENGE_STEPS.length-1] : CHALLENGE_STEPS[currentIdx]; const stepNum = isFinished ? '🏆' : (currentIdx + 1); const completedCount = state.completedSteps.length; const progressPct = Math.round((completedCount / CHALLENGE_STEPS.length) * 100); return `
Step${stepNum}
${completedCount}/${CHALLENGE_STEPS.length} done
Progreso global ${progressPct}%
Risk ${current.risk}€
Target ${current.target}€
`; } // ═══════════════════════════════════════════════ // ─── QUICK DAILY P&L ─── // ═══════════════════════════════════════════════ let quickPnlAccId = null; function openQuickPnl(accId) { quickPnlAccId = accId; const acc = accounts.find(a => a.id === accId); if(!acc) return; const brokerName = acc.broker === 'Otro' ? (acc.customName||'Custom') : acc.broker; // Set logo const logoEl = document.getElementById('qpnl-logo'); if(logoEl) logoEl.innerHTML = getBrokerLogoHTML(acc.broker, 38); // Set name const bEl = document.getElementById('qpnl-broker'); const aEl = document.getElementById('qpnl-account'); if(bEl) bEl.textContent = brokerName; if(aEl) aEl.textContent = `${acc.accountNum||'#------'} · ${acc.type||'Live'}`; // Reset fields const amtEl = document.getElementById('qpnl-amount'); const dateEl = document.getElementById('qpnl-date'); const noteEl = document.getElementById('qpnl-note'); const confirmBtn = document.getElementById('qpnl-confirm-btn'); if(amtEl) { amtEl.value = ''; amtEl.style.color = 'var(--text)'; amtEl.style.borderColor = 'var(--border2)'; } if(dateEl) dateEl.value = new Date().toISOString().split('T')[0]; if(noteEl) noteEl.value = ''; if(confirmBtn) { confirmBtn.style.background = 'var(--accent)'; confirmBtn.style.color = '#000'; } const overlay = document.getElementById('quick-pnl-overlay'); if(overlay) overlay.classList.add('open'); document.body.style.overflow = 'hidden'; setTimeout(() => { if(amtEl) amtEl.focus(); }, 100); } function closeQuickPnl() { const overlay = document.getElementById('quick-pnl-overlay'); if(overlay) overlay.classList.remove('open'); document.body.style.overflow = ''; quickPnlAccId = null; } function updateQpnlColor(input) { const val = parseFloat(input.value); if(isNaN(val) || val === 0) { input.style.color = 'var(--text)'; input.style.borderColor = 'var(--border2)'; const btn = document.getElementById('qpnl-confirm-btn'); if(btn) { btn.style.background = 'var(--accent)'; btn.style.color = '#000'; } } else if(val > 0) { input.style.color = 'var(--green)'; input.style.borderColor = 'rgba(0,229,160,0.5)'; const btn = document.getElementById('qpnl-confirm-btn'); if(btn) { btn.style.background = 'var(--green)'; btn.style.color = '#000'; } } else { input.style.color = 'var(--red)'; input.style.borderColor = 'rgba(255,77,109,0.5)'; const btn = document.getElementById('qpnl-confirm-btn'); if(btn) { btn.style.background = 'var(--red)'; btn.style.color = '#fff'; } } } function setQpnlPreset(btn, sign) { const input = document.getElementById('qpnl-amount'); if(!input) return; const current = parseFloat(input.value) || 0; // If already has a value, just flip sign; else set to 0 to prompt typing if(current !== 0) { input.value = (Math.abs(current) * sign).toFixed(2); } updateQpnlColor(input); input.focus(); } function addToQpnl(amount) { const input = document.getElementById('qpnl-amount'); if(!input) return; const current = parseFloat(input.value) || 0; input.value = (current + amount).toFixed(2); updateQpnlColor(input); } function saveQuickPnl() { const acc = accounts.find(a => a.id === quickPnlAccId); if(!acc) return; const amtEl = document.getElementById('qpnl-amount'); const dateEl = document.getElementById('qpnl-date'); const noteEl = document.getElementById('qpnl-note'); const pnl = parseFloat(amtEl?.value); if(isNaN(pnl) || pnl === 0) { if(amtEl) { amtEl.style.borderColor = 'var(--red)'; amtEl.focus(); } showToast('⚠️ Introduce un valor distinto de 0'); return; } const date = dateEl?.value || new Date().toISOString().split('T')[0]; const note = noteEl?.value || ''; // Save as a "daily" trade entry if(!acc.trades) acc.trades = []; acc.trades.push({ pair: note || 'Diario', type: pnl >= 0 ? 'BUY' : 'SELL', lots: 0, date, entry: 0, exit: 0, pnl, source: 'manual' }); saveAccounts(); // Refresh everything renderAccounts(); if(currentAccountId === quickPnlAccId) { const a = accounts.find(x => x.id === quickPnlAccId); if(a) { renderTradesTable(a); renderCalendar(a); renderPanelMetrics(a); drawPanelChart(a); renderDrawdown(a); renderChallenge(a); } } closeQuickPnl(); const sign = pnl >= 0 ? '+' : ''; showToast(`${pnl >= 0 ? '🟢' : '🔴'} ${brokerName(acc)} · ${sign}$${Math.abs(pnl).toFixed(2)} guardado`); } function brokerName(acc) { return acc.broker === 'Otro' ? (acc.customName||'Cuenta') : acc.broker; } // Close on overlay click // ═══════════════════════════════════════════════ // ─── AXI SELECT ENGINE ─── // ═══════════════════════════════════════════════ const AXI_STAGES = [ { id: 'semilla', name: 'Semilla', icon: '🌱', funding: 5000, minEquity: 500, edgeScore: 50, profitSplit: 0, leverage: '1:1000', multiplier: 'x10', profitSplitAlloc: 7, minDays: 30, minOps: 20, maxLoss: -7, }, { id: 'incubacion', name: 'Incubación', icon: '⚡', funding: 20000, minEquity: 1000, edgeScore: 60, profitSplit: 40, leverage: '1:100', multiplier: 'x10', profitSplitAlloc: 7, minDays: 60, minOps: 40, maxLoss: -7, }, { id: 'aceleracion', name: 'Aceleración', icon: '🚀', funding: 100000, minEquity: 2000, edgeScore: 70, profitSplit: 50, leverage: '1:100', multiplier: 'x25', profitSplitAlloc: 7, minDays: 60, minOps: 50, maxLoss: -7, }, { id: 'pro', name: 'Pro', icon: '💎', funding: 200000, minEquity: 5000, edgeScore: 90, profitSplit: 60, leverage: '1:100', multiplier: 'x40', profitSplitAlloc: 7, minDays: 60, minOps: 50, maxLoss: -7, }, { id: 'pro500', name: 'Pro 500', icon: '🏆', funding: 500000, minEquity: 10000, edgeScore: 90, profitSplit: 70, leverage: '1:100', multiplier: 'x50', profitSplitAlloc: 7, minDays: 60, minOps: 50, maxLoss: -7, }, { id: 'proM', name: 'Pro M', icon: '👑', funding: 1000000, minEquity: 20000, edgeScore: 90, profitSplit: 80, leverage: '1:100', multiplier: 'x50', profitSplitAlloc: 0, minDays: 0, minOps: 0, maxLoss: -10, }, ]; function getAxiState(accId) { const raw = localStorage.getItem('traxor_axiselect_' + accId); if(raw) return JSON.parse(raw); return { currentStage: 0, completedStages: [], equity: 379.52, edgeScore: 56, ops: 44, days: 0, pnlPct: 0, drawdownPct: 0, startDate: new Date().toISOString().split('T')[0], lastUpdated: null, }; } function saveAxiState(accId, state) { localStorage.setItem('traxor_axiselect_' + accId, JSON.stringify(state)); } // ─── RENDER AXI SELECT TAB ─── function renderAxiSelect(acc) { const el = document.getElementById('axiselect-content'); if(!el) return; if(acc.broker !== 'Axi') { el.innerHTML = ''; return; } const state = getAxiState(acc.id); const stageIdx = Math.min(state.currentStage, AXI_STAGES.length - 1); const stage = AXI_STAGES[stageIdx]; const isLastStage = stageIdx === AXI_STAGES.length - 1; // Compute progress values const opsTarget = stage.minOps || 1; const daysTarget = stage.minDays || 1; const opsPct = stage.minOps === 0 ? 100 : Math.min(100, Math.round((state.ops / opsTarget) * 100)); const daysPct = stage.minDays === 0 ? 100 : Math.min(100, Math.round((state.days / daysTarget) * 100)); const equityPct = Math.min(100, Math.round((state.equity / stage.minEquity) * 100)); const edgePct = Math.min(100, Math.round((state.edgeScore / stage.edgeScore) * 100)); const equityOk = state.equity >= stage.minEquity; const edgeOk = state.edgeScore >= stage.edgeScore; const opsOk = stage.minOps === 0 || state.ops >= stage.minOps; const daysOk = stage.minDays === 0 || state.days >= stage.minDays; const canAdvance = equityOk && edgeOk && opsOk && daysOk && !isLastStage; // Compute staleness let lastUpdatedStr = 'Nunca actualizado'; let isStale = true; if(state.lastUpdated) { const lu = new Date(state.lastUpdated); const now = new Date(); const diffH = Math.floor((now - lu) / 36e5); const diffD = Math.floor(diffH / 24); if(diffH < 1) { lastUpdatedStr = 'Hace menos de 1 hora'; isStale = false; } else if(diffH < 24) { lastUpdatedStr = `Hace ${diffH}h`; isStale = false; } else { lastUpdatedStr = `Hace ${diffD} día${diffD>1?'s':''}`; isStale = diffD >= 1; } } el.innerHTML = `
axiSELECT Ruta
🕒 ${lastUpdatedStr} ${isStale ? '⚠️ Actualiza tus datos' : '✓ Al día'}
${isStale ? `
⚠️
Datos posiblemente desactualizados
Entra en my.axi.com, consulta tu Edge Score y operaciones, y pulsa "Actualizar datos".
` : ''}
${AXI_STAGES.map((s, i) => { const isDone = state.completedStages.includes(i); const isCurrent = i === stageIdx; const isLocked = i > stageIdx; const cls = isDone ? 'done' : isCurrent ? 'current' : isLocked ? 'locked' : 'done'; const badge = isDone ? `
` : isCurrent ? `
Aquí
` : ''; const arrow = i < AXI_STAGES.length - 1 ? `
` : ''; return `
${badge}
${s.icon}
${s.name}
$${(s.funding/1000).toFixed(0)}K
${arrow}
`; }).join('')}
${stage.icon} Etapa: ${stage.name}
$${stage.funding.toLocaleString()}
Patrimonio
$${state.equity.toFixed(2)}
Mín: $${stage.minEquity}
Edge Score
${state.edgeScore}
Mín: ${stage.edgeScore}
Operaciones
${state.ops}${stage.minOps>0?'/'+stage.minOps:''}
${stage.minOps===0?'Sin límite':'Requeridas: '+stage.minOps}
Días activo
${state.days}${stage.minDays>0?'/'+stage.minDays:''}
${stage.minDays===0?'Sin límite':'Días mín: '+stage.minDays}
P&L %
${state.pnlPct>=0?'+':''}${state.pnlPct.toFixed(2)}%
Pérd. máx: ${stage.maxLoss}%
Profit Split
${stage.profitSplit}%
Asignac.: ${stage.profitSplitAlloc}%
Patrimonio mínimo $${state.equity.toFixed(0)} / $${stage.minEquity}
Edge Score ${state.edgeScore} / ${stage.edgeScore}
${stage.minOps > 0 ? `
Operaciones ${state.ops} / ${stage.minOps}
` : ''} ${stage.minDays > 0 ? `
Días en etapa ${state.days} / ${stage.minDays}
` : ''}
Detalles de la Etapa
${stage.minDays>0?``:''} ${stage.minOps>0?``:''}
Financiamiento máximo$${stage.funding.toLocaleString()}
División de ganancias${stage.profitSplit}%
Apalancamiento${stage.leverage}
Multiplicador máx.${stage.multiplier}
Patrimonio mínimo$${stage.minEquity.toLocaleString()}
Puntuación Edge mín.${stage.edgeScore}
Div. ganancias (asignac.)${stage.profitSplitAlloc}%
Duración mínima${stage.minDays} días
Operaciones por etapa${stage.minOps}
Pérdida máxima${stage.maxLoss}%
${canAdvance ? ` ` : isLastStage ? `
👑 ¡Nivel máximo alcanzado! Pro M · $1.000.000
` : `
⏳ Completa los requisitos de la etapa para avanzar: ${!equityOk ? `
• Patrimonio insuficiente ($${state.equity.toFixed(0)} / $${stage.minEquity})
` : ''} ${!edgeOk ? `
• Edge Score insuficiente (${state.edgeScore} / ${stage.edgeScore})
` : ''} ${!opsOk ? `
• Operaciones insuficientes (${state.ops} / ${stage.minOps})
` : ''} ${!daysOk ? `
• Días insuficientes (${state.days} / ${stage.minDays})
` : ''}
`} `; } // ─── ADVANCE STAGE ─── function axiAdvanceStage(accId) { const state = getAxiState(accId); if(!state.completedStages.includes(state.currentStage)) { state.completedStages.push(state.currentStage); } state.currentStage = Math.min(state.currentStage + 1, AXI_STAGES.length - 1); // Reset tracking for new stage state.ops = 0; state.days = 0; state.pnlPct = 0; state.startDate = new Date().toISOString().split('T')[0]; saveAxiState(accId, state); const acc = accounts.find(a => a.id === accId); if(acc) { renderAxiSelect(acc); renderAccounts(); } showToast(`⚡ ¡Avanzaste a ${AXI_STAGES[state.currentStage].name}!`); } // ─── JUMP TO STAGE (click on road) ─── function axiJumpStage(accId, idx) { const state = getAxiState(accId); // Only allow jumping to completed or current stages if(idx > state.currentStage) return; state.currentStage = idx; saveAxiState(accId, state); const acc = accounts.find(a => a.id === accId); if(acc) renderAxiSelect(acc); } // ─── EDIT MODAL ─── let axiEditAccId = null; function openAxiEditModal(accId) { axiEditAccId = accId; const state = getAxiState(accId); const setVal = (id, v) => { const e = document.getElementById(id); if(e) e.value = v; }; setVal('axi-equity', state.equity); setVal('axi-edge', state.edgeScore); setVal('axi-ops', state.ops); setVal('axi-days', state.days); setVal('axi-pnl', state.pnlPct); const overlay = document.getElementById('axi-edit-overlay'); if(overlay) overlay.classList.add('open'); document.body.style.overflow = 'hidden'; } function closeAxiEditModal() { const overlay = document.getElementById('axi-edit-overlay'); if(overlay) overlay.classList.remove('open'); document.body.style.overflow = ''; axiEditAccId = null; } function saveAxiEdit() { if(!axiEditAccId) return; const state = getAxiState(axiEditAccId); const g = (id) => parseFloat(document.getElementById(id)?.value) || 0; state.equity = g('axi-equity'); state.edgeScore = g('axi-edge'); state.ops = Math.round(g('axi-ops')); state.days = Math.round(g('axi-days')); state.pnlPct = g('axi-pnl'); state.lastUpdated = new Date().toISOString(); saveAxiState(axiEditAccId, state); const acc = accounts.find(a => a.id === axiEditAccId); if(acc) { renderAxiSelect(acc); renderAccounts(); } closeAxiEditModal(); showToast('✅ Axi SELECT actualizado'); } // ─── MINI CARD WIDGET ─── function getAxiSelectCardHTML(acc) { if(acc.broker !== 'Axi') return ''; const state = getAxiState(acc.id); const stage = AXI_STAGES[Math.min(state.currentStage, AXI_STAGES.length-1)]; // Staleness let isStale = !state.lastUpdated; if(state.lastUpdated) { const diffH = Math.floor((new Date() - new Date(state.lastUpdated)) / 36e5); isStale = diffH >= 24; } return `
Axi SELECT${stage.icon} ${stage.name}
EDGE SCORE
${state.edgeScore}
Financiación$${stage.funding.toLocaleString()}
${isStale ? `
⚠️ Actualizar
` : `
✓ Al día
`}
`; } // ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════ // ─── FIREBASE ─── // ═══════════════════════════════════════════════ const FIREBASE_CONFIG = { apiKey: "AIzaSyCcjG_a_Vrz-oeQdFzy48E7meMREzGKmPk", authDomain: "traxor-37ba4.firebaseapp.com", projectId: "traxor-37ba4", storageBucket: "traxor-37ba4.firebasestorage.app", messagingSenderId: "494589785507", appId: "1:494589785507:web:f01bc099160e25959d5616" }; let db = null, auth = null, currentUser = null, syncTimeout = null; function initFirebase() { try { firebase.initializeApp(FIREBASE_CONFIG); auth = firebase.auth(); db = firebase.firestore(); return true; } catch(e) { return false; } } function switchAuthTab(tab) { const isLogin = tab==='login'; document.getElementById('auth-login-form').style.display = isLogin?'':'none'; document.getElementById('auth-register-form').style.display = isLogin?'none':''; document.getElementById('tab-login-btn').classList.toggle('active', isLogin); document.getElementById('tab-register-btn').classList.toggle('active', !isLogin); const e1=document.getElementById('auth-error'), e2=document.getElementById('auth-reg-error'); if(e1) e1.textContent=''; if(e2) e2.textContent=''; } function setAuthLoading(form, on) { const btn=document.getElementById('auth-'+form+'-btn'); const txt=document.getElementById('auth-'+form+'-text'); const spn=document.getElementById('auth-'+form+'-spinner'); if(btn) btn.disabled=on; if(txt) txt.style.display=on?'none':''; if(spn) spn.style.display=on?'block':'none'; } async function doLogin() { const email=document.getElementById('auth-email').value.trim(); const pass=document.getElementById('auth-password').value; const err=document.getElementById('auth-error'); err.textContent=''; if(!email||!pass){err.textContent='Rellena todos los campos';return;} setAuthLoading('login',true); try { const c=await auth.signInWithEmailAndPassword(email,pass); await onLogin(c.user); } catch(e){err.textContent=authErr(e.code);} finally{setAuthLoading('login',false);} } async function doRegister() { const email=document.getElementById('auth-reg-email').value.trim(); const pass=document.getElementById('auth-reg-password').value; const conf=document.getElementById('auth-reg-confirm').value; const err=document.getElementById('auth-reg-error'); err.textContent=''; if(!email||!pass){err.textContent='Rellena todos los campos';return;} if(pass.length<6){err.textContent='Mínimo 6 caracteres';return;} if(pass!==conf){err.textContent='Las contraseñas no coinciden';return;} setAuthLoading('reg',true); try { const c=await auth.createUserWithEmailAndPassword(email,pass); await onLogin(c.user); } catch(e){err.textContent=authErr(e.code);} finally{setAuthLoading('reg',false);} } async function doForgotPassword() { const email=document.getElementById('auth-email').value.trim(); if(!email){document.getElementById('auth-error').textContent='Introduce tu email';return;} await auth.sendPasswordResetEmail(email); const e=document.getElementById('auth-error'); e.style.color='var(--green)'; e.textContent='✅ Email enviado'; } async function doLogout() { toggleUserMenu(false); accounts=[]; Object.keys(localStorage).forEach(k=>{if(k.startsWith('traxor_'))localStorage.removeItem(k);}); await auth.signOut(); } function toggleUserMenu(force) { const dd=document.getElementById('user-dropdown'); if(!dd) return; dd.style.display=(force!==undefined?force:dd.style.display==='none')?'block':'none'; } document.addEventListener('click', e=>{ const w=document.getElementById('user-menu-wrap'); if(w&&!w.contains(e.target)) toggleUserMenu(false); }); function authErr(code) { return({'auth/user-not-found':'Email no registrado','auth/wrong-password':'Contraseña incorrecta', 'auth/email-already-in-use':'Email ya registrado','auth/invalid-email':'Email no válido', 'auth/weak-password':'Contraseña muy débil','auth/too-many-requests':'Demasiados intentos', 'auth/invalid-credential':'Email o contraseña incorrectos'}[code]||'Error: '+code); } async function onLogin(user) { currentUser=user; const ie=document.getElementById('user-initial'); const ee=document.getElementById('user-email-label'); if(ie) ie.textContent=(user.email||'?')[0].toUpperCase(); if(ee) ee.textContent=user.email; accounts=[]; Object.keys(localStorage).forEach(k=>{if(k.startsWith('traxor_'))localStorage.removeItem(k);}); hideAuthScreen(); await loadFromCloud(); } function showAuthScreen() { const ids=['auth-screen']; const hide=['.topbar','.app','.mobile-nav','.mobile-fab','.mobile-pnl-fab']; const s=document.getElementById('auth-screen'); if(s) s.style.display='block'; hide.forEach(sel=>{const el=document.querySelector(sel);if(el)el.style.display='none';}); } function hideAuthScreen() { const s=document.getElementById('auth-screen'); const t=document.querySelector('.topbar'); const a=document.querySelector('.app'); if(s) s.style.display='none'; if(t) t.style.display=''; if(a) a.style.display=''; } function setSyncStatus(status) { const dot=document.getElementById('sync-dot'); if(!dot) return; const colors={syncing:'#ffd60a',ok:'var(--green)',error:'var(--red)',offline:'var(--text3)'}; dot.style.background=colors[status]||colors.offline; } function saveAccounts() { localStorage.setItem('traxor_accounts',JSON.stringify(accounts)); clearTimeout(syncTimeout); syncTimeout=setTimeout(syncToCloud,1000); } async function syncToCloud() { if(!db||!currentUser) return; setSyncStatus('syncing'); try { const snap={accounts,challenges:{},axiselect:{}}; accounts.forEach(a=>{ const ch=localStorage.getItem('traxor_challenge_'+a.id); const ax=localStorage.getItem('traxor_axiselect_'+a.id); if(ch) snap.challenges[a.id]=JSON.parse(ch); if(ax) snap.axiselect[a.id]=JSON.parse(ax); }); await db.collection('userData').doc(currentUser.uid).set({ data:JSON.stringify(snap), updatedAt:firebase.firestore.FieldValue.serverTimestamp() }); setSyncStatus('ok'); } catch(e){console.error(e);setSyncStatus('error');} } async function loadFromCloud() { if(!db||!currentUser) return; setSyncStatus('syncing'); try { const doc=await db.collection('userData').doc(currentUser.uid).get(); if(doc.exists&&doc.data().data) { const snap=JSON.parse(doc.data().data); if(snap.accounts){accounts=snap.accounts;localStorage.setItem('traxor_accounts',JSON.stringify(accounts));} if(snap.challenges) Object.entries(snap.challenges).forEach(([id,v])=>localStorage.setItem('traxor_challenge_'+id,JSON.stringify(v))); if(snap.axiselect) Object.entries(snap.axiselect).forEach(([id,v])=>localStorage.setItem('traxor_axiselect_'+id,JSON.stringify(v))); } setSyncStatus('ok'); } catch(e){console.error(e);setSyncStatus('error');accounts=JSON.parse(localStorage.getItem('traxor_accounts')||'[]');} setTodayDate();renderAccounts();renderGlobalCalendar();updateGlobalStats(); } async function doSync(){toggleUserMenu(false);await loadFromCloud();showToast('🔄 Sincronizado');} function saveChallengeState(accId,state){ localStorage.setItem('traxor_challenge_'+accId,JSON.stringify(state)); clearTimeout(syncTimeout);syncTimeout=setTimeout(syncToCloud,1000); } function saveAxiState(accId,state){ localStorage.setItem('traxor_axiselect_'+accId,JSON.stringify(state)); clearTimeout(syncTimeout);syncTimeout=setTimeout(syncToCloud,1000); } function mobileNav(name, el) { document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.querySelectorAll('.sidebar-icon').forEach(i => i.classList.remove('active')); document.querySelectorAll('.mobile-nav-item').forEach(i => i.classList.remove('active')); const view = document.getElementById('view-'+name); if(view) view.classList.add('active'); if(el) el.classList.add('active'); if(name === 'stats-global') { updateGlobalStats(); drawGlobalChart(); } if(name === 'calendar-global') { renderGlobalCalendar(); } } // Mobile P&L picker function openMobilePnlPicker() { if(accounts.length === 0) { showToast('⚠️ Primero agrega una cuenta'); return; } if(accounts.length === 1) { openQuickPnl(accounts[0].id); return; } const existing = document.getElementById('mobile-pnl-picker'); if(existing) existing.remove(); const sheet = document.createElement('div'); sheet.id = 'mobile-pnl-picker'; sheet.style.cssText = 'position:fixed;inset:0;z-index:600;background:rgba(0,0,0,0.7);display:flex;align-items:flex-end;'; sheet.innerHTML = `
¿En qué cuenta?
${accounts.map(a => `
${getBrokerLogoHTML(a.broker, 36)}
${a.broker==='Otro'?(a.customName||'Custom'):a.broker}
${a.accountNum||'#------'} · ${a.type||'Live'}
`).join('')}
`; sheet.addEventListener('click', e => { if(e.target === sheet) sheet.remove(); }); document.body.appendChild(sheet); } document.addEventListener('DOMContentLoaded', () => { const td=document.getElementById('t-date'); if(td) td.value=new Date().toISOString().split('T')[0]; const qo=document.getElementById('quick-pnl-overlay'); if(qo) qo.addEventListener('click',e=>{if(e.target===qo)closeQuickPnl();}); const ao=document.getElementById('axi-edit-overlay'); if(ao) ao.addEventListener('click',e=>{if(e.target===ao)closeAxiEditModal();}); if(!initFirebase()) { // Offline mode accounts=JSON.parse(localStorage.getItem('traxor_accounts')||'[]'); setTodayDate();renderAccounts();renderGlobalCalendar();updateGlobalStats(); setSyncStatus('offline'); return; } showAuthScreen(); let done=false; // Safety timeout 6s — if Firebase hangs, go offline setTimeout(()=>{ if(done) return; done=true; accounts=JSON.parse(localStorage.getItem('traxor_accounts')||'[]'); setTodayDate();renderAccounts();renderGlobalCalendar();updateGlobalStats(); hideAuthScreen();setSyncStatus('offline'); showToast('⚠️ Modo offline activado'); },6000); auth.onAuthStateChanged(async user=>{ if(done&&!user) return; done=true; if(user) await onLogin(user); else showAuthScreen(); }); }); // ─── VIEWS ─── function showView(name, el) { document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.querySelectorAll('.sidebar-icon').forEach(i => i.classList.remove('active')); document.querySelectorAll('.mobile-nav-item').forEach(i => i.classList.remove('active')); const view = document.getElementById('view-'+name); if(view) view.classList.add('active'); if(el) el.classList.add('active'); // Sync mobile nav highlight const mobileMap = { 'dashboard':'mnav-dashboard', 'calendar-global':'mnav-calendar', 'stats-global':'mnav-stats' }; const mItem = document.getElementById(mobileMap[name]); if(mItem) mItem.classList.add('active'); if(name === 'stats-global') { updateGlobalStats(); drawGlobalChart(); } if(name === 'calendar-global') { renderGlobalCalendar(); } } // ─── PWA INSTALL BANNER ─── let deferredInstallPrompt = null; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredInstallPrompt = e; // Show install banner after 2 seconds setTimeout(showInstallBanner, 2000); }); function showInstallBanner() { if(!deferredInstallPrompt) return; const existing = document.getElementById('pwa-banner'); if(existing) return; const banner = document.createElement('div'); banner.id = 'pwa-banner'; banner.style.cssText = ` position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:999; background:var(--surface2);border:1px solid var(--accent);border-radius:14px; padding:14px 18px;display:flex;align-items:center;gap:12px; box-shadow:0 8px 30px rgba(0,0,0,0.5),0 0 20px rgba(0,229,160,0.15); min-width:280px;max-width:340px;animation:fadeUp 0.3s ease; `; banner.innerHTML = `
Instalar TRAXOR
Añadir a pantalla de inicio
`; document.body.appendChild(banner); // Auto-dismiss after 8s setTimeout(() => banner.remove(), 8000); } async function installPWA() { if(!deferredInstallPrompt) return; deferredInstallPrompt.prompt(); const { outcome } = await deferredInstallPrompt.userChoice; deferredInstallPrompt = null; const banner = document.getElementById('pwa-banner'); if(banner) banner.remove(); if(outcome === 'accepted') showToast('✅ TRAXOR instalado en tu dispositivo'); } // Register service worker if('serviceWorker' in navigator) { navigator.serviceWorker.register('traxor-sw.js').catch(() => {}); } // ─── RENDER YEARLY ─── function renderYearly(acc) { const el = document.getElementById('yearly-content'); if(!el) return; // Build yearly from trades if no yearlyStats let yearly = acc.yearlyStats || {}; if(!Object.keys(yearly).length) { const byY = {}; (acc.trades||[]).forEach(t => { const y = (t.date||'').split('-')[0]; if(!y) return; if(!byY[y]) byY[y] = {pnl:0,wins:0,losses:0}; byY[y].pnl += t.pnl; if(t.pnl>0) byY[y].wins++; else byY[y].losses++; }); yearly = byY; } const years = Object.keys(yearly).sort(); if(!years.length) { el.innerHTML='
Sin datos
'; return; } const totalPnl = Object.values(yearly).reduce((s,v)=>s+v.pnl,0); const totalWins = Object.values(yearly).reduce((s,v)=>s+v.wins,0); const totalOps = Object.values(yearly).reduce((s,v)=>s+v.wins+v.losses,0); const wr = totalOps ? Math.round(totalWins/totalOps*100) : 0; const best = years.reduce((b,y) => yearly[y].pnl > yearly[b].pnl ? y : b, years[0]); const maxPnl = Math.max(...Object.values(yearly).map(v=>Math.abs(v.pnl))); function fmtBig(n) { const a=Math.abs(n); if(a>=1e9) return (n/1e9).toFixed(2)+'B'; if(a>=1e6) return (n/1e6).toFixed(2)+'M'; if(a>=1e3) return (n/1e3).toFixed(1)+'K'; return n.toFixed(2); } let running = acc.balance || 20000; const tableRows = years.map(y => { const v = yearly[y]; running += v.pnl; const tot = v.wins+v.losses; const wr2 = tot ? Math.round(v.wins/tot*100) : 0; const pos = v.pnl >= 0; return ` ${y} ${pos?'+':''}${fmtBig(v.pnl)}€ ${tot} ${v.wins} ${v.losses} ${wr2}% €${fmtBig(running)} `; }).join(''); el.innerHTML = `
P&L Total
+€${fmtBig(totalPnl)}
Win Rate
${wr}%
Mejor Año
${best}
Total Ops
${totalOps.toLocaleString()}
Ganancia por Año
${years.map(y => { const v = yearly[y]; const pos = v.pnl>=0; const pct = Math.round(Math.abs(v.pnl)/maxPnl*100); const col = pos ? 'var(--green)' : 'var(--red)'; const tot = v.wins+v.losses; const wr2 = tot ? Math.round(v.wins/tot*100) : 0; return `
${y}
${tot} ops · WR ${wr2}%
${pos?'+':''}${fmtBig(v.pnl)}€
`; }).join('')}
Detalle por Año
${tableRows}
AñoP&LOpsWinsLossWRBal. Final
`; } // ─── RENDER YEARLY ─── function renderYearly(acc) { const el = document.getElementById('yearly-content'); if(!el) return; // Build yearly from trades if no yearlyStats let yearly = acc.yearlyStats || {}; if(!Object.keys(yearly).length) { const byY = {}; (acc.trades||[]).forEach(t => { const y = (t.date||'').split('-')[0]; if(!y) return; if(!byY[y]) byY[y] = {pnl:0,wins:0,losses:0}; byY[y].pnl += t.pnl; if(t.pnl>0) byY[y].wins++; else byY[y].losses++; }); yearly = byY; } const years = Object.keys(yearly).sort(); if(!years.length) { el.innerHTML='
Sin datos
'; return; } const totalPnl = Object.values(yearly).reduce((s,v)=>s+v.pnl,0); const totalWins = Object.values(yearly).reduce((s,v)=>s+v.wins,0); const totalOps = Object.values(yearly).reduce((s,v)=>s+v.wins+v.losses,0); const wr = totalOps ? Math.round(totalWins/totalOps*100) : 0; const best = years.reduce((b,y) => yearly[y].pnl > yearly[b].pnl ? y : b, years[0]); const maxPnl = Math.max(...Object.values(yearly).map(v=>Math.abs(v.pnl))); function fmtBig(n) { const a=Math.abs(n); if(a>=1e9) return (n/1e9).toFixed(2)+'B'; if(a>=1e6) return (n/1e6).toFixed(2)+'M'; if(a>=1e3) return (n/1e3).toFixed(1)+'K'; return n.toFixed(2); } let running = acc.balance || 20000; const tableRows = years.map(y => { const v = yearly[y]; running += v.pnl; const tot = v.wins+v.losses; const wr2 = tot ? Math.round(v.wins/tot*100) : 0; const pos = v.pnl >= 0; return ` ${y} ${pos?'+':''}${fmtBig(v.pnl)}€ ${tot} ${v.wins} ${v.losses} ${wr2}% €${fmtBig(running)} `; }).join(''); el.innerHTML = `
P&L Total
+€${fmtBig(totalPnl)}
Win Rate
${wr}%
Mejor Año
${best}
Total Ops
${totalOps.toLocaleString()}
Ganancia por Año
${years.map(y => { const v = yearly[y]; const pos = v.pnl>=0; const pct = Math.round(Math.abs(v.pnl)/maxPnl*100); const col = pos ? 'var(--green)' : 'var(--red)'; const tot = v.wins+v.losses; const wr2 = tot ? Math.round(v.wins/tot*100) : 0; return `
${y}
${tot} ops · WR ${wr2}%
${pos?'+':''}${fmtBig(v.pnl)}€
`; }).join('')}
Detalle por Año
${tableRows}
AñoP&LOpsWinsLossWRBal. Final
`; } // ═══════════════════════════════════════════════ >