/** * BrainSteel Fin — Dashboard JavaScript * 4-Agent Pipeline: Brief → Decio → PaperT → Audit */ // ── State ───────────────────────────────────────────────────────────────────── let autoMode = false; let autoInterval = null; let pollInterval = null; let currentAsset = 'btc'; // 'btc', 'usd', 'xau' const AGENT_EMOJIS = { brief: "📊", decio: "🎯", papert: "⚡", audit: "🔍" }; const AGENT_COLORS = { brief: "#4fc3f7", decio: "#ffd54f", papert: "#69f0ae", audit: "#b388ff" }; // ── Asset Switching ─────────────────────────────────────────────────────────── const ASSET_SYMBOLS = { btc: '₿', usd: '💵', xau: '🪙' }; const ASSET_LABELS = { btc: 'Bitcoin', usd: 'Dólar', xau: 'Ouro' }; const ASSET_PAIRS = { btc: 'BTC/USD', usd: 'USD/BRL', xau: 'XAU/USD' }; function switchAsset(asset, btnEl) { if (asset === currentAsset) return; currentAsset = asset; // Update sidebar buttons document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active')); if (btnEl) btnEl.classList.add('active'); // Update header title dynamically const titleEl = document.querySelector('.logo-text strong'); if (titleEl) titleEl.textContent = asset.toUpperCase(); // Reload data for new asset loadAssetBrief(); loadConfig(); loadPortfolio(); loadMultiStratChart('live'); addLog('SYS', `Ativo alterado para ${ASSET_LABELS[asset]} (${ASSET_PAIRS[asset]})`); } // ── Asset Brief Loader ─────────────────────────────────────────────────────── function loadAssetBrief() { if (currentAsset === 'btc') { // BTC uses the existing pipeline via pollState pollState(); return; } const apiMap = { usd: '/api/usd/brief', xau: '/api/xau/brief' }; const api = apiMap[currentAsset]; if (!api) return; fetch(api) .then(r => r.json()) .then(data => { if (data.error) { addLog('SYS', `Erro ao carregar ${ASSET_LABELS[currentAsset]}: ${data.error}`); return; } const brief = data.brief; const decio = data.decio; // Update brief agent card const priceStr = brief.price ? (brief.price > 1000 ? brief.price.toLocaleString('pt-BR', {minimumFractionDigits: 2}) : brief.price.toFixed(4)) : '-'; updateAgentUI('brief', { status: 'done', message: `${ASSET_PAIRS[currentAsset]}: ${priceStr}` }); // Update decio agent card updateAgentUI('decio', { status: 'done', message: `Decisão: ${decio.decision} (${decio.confidence}%)` }); addLog('SYS', `${ASSET_LABELS[currentAsset]} | ${decio.decision} | Conf: ${decio.confidence}%`); }) .catch(err => { addLog('SYS', `Erro API ${currentAsset}: ${err.message}`); }); } // ── Init ───────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { updateClock(); setInterval(updateClock, 1000); // Sidebar clock setInterval(() => { const now = new Date(); const el = document.getElementById('sidebarClock'); if (el) el.textContent = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); }, 1000); startPolling(); loadAgentRegistry(); loadMetrics(); loadServices(); loadPipelineVisual(); loadTokenChart(); loadConfig(); loadMultiStratChart('live'); loadPortfolio(); loadActivityLog(); updateStatus('Sistema operacional', 'online'); // Load asset-specific data if (currentAsset === 'btc') { pollState(); } else { loadAssetBrief(); } }); // ── Clock ───────────────────────────────────────────────────────────────────── function updateClock() { const now = new Date(); document.getElementById('clock').textContent = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } // ── Polling ─────────────────────────────────────────────────────────────────── function startPolling() { pollState(); pollInterval = setInterval(pollState, 3000); } function pollState() { fetch('/api/state') .then(r => r.json()) .then(data => { updateAgentUI('brief', data.brief); updateAgentUI('decio', data.decio); updateAgentUI('papert', data.papert); updateAgentUI('audit', data.audit); document.getElementById('btnRun').disabled = data.pipeline_running; updateStatus(data.pipeline_running ? 'Pipeline em execução' : 'Aguardando', data.pipeline_running ? 'running' : 'online'); if (!data.pipeline_running && data.last_pipeline) { showDecisionBanner(data); } }).catch(err => console.error('Poll error:', err)); } function updateAgentUI(agent, data) { const node = document.getElementById(`node-${agent}`); const statusEl = document.getElementById(`status-${agent}`); const msgEl = document.getElementById(`msg-${agent}`); if (!node) return; node.className = `pipeline-node ${data.status || 'idle'}`; if (statusEl) { const labels = { idle: 'Aguardando', working: 'Trabalhando', done: 'Concluído', error: 'Erro', waiting: 'Aguardando' }; statusEl.textContent = labels[data.status] || data.status; statusEl.className = `agent-status ${data.status}`; } if (msgEl) msgEl.textContent = data.message || ''; } // ── Status ──────────────────────────────────────────────────────────────────── function updateStatus(text, type) { const dot = document.getElementById('statusDot'); const txt = document.getElementById('statusText'); if (dot) dot.className = `status-dot ${type}`; if (txt) txt.textContent = text; // Sync sidebar status const sidebarDot = document.getElementById('sidebarStatusDot'); const sidebarTxt = document.getElementById('sidebarStatusText'); if (sidebarDot) sidebarDot.className = `status-dot ${type}`; if (sidebarTxt) sidebarTxt.textContent = text; } // ── Pipeline ───────────────────────────────────────────────────────────────── function runPipeline() { fetch('/api/run', { method: 'POST' }) .then(r => r.json()) .then(d => { if (d.error) { alert(d.error); return; } addLog('SYS', 'Pipeline iniciado'); }).catch(err => console.error(err)); } function toggleAuto() { autoMode = !autoMode; const el = document.getElementById('autoStatus'); if (el) el.textContent = autoMode ? 'ON' : 'OFF'; if (autoMode) { addLog('SYS', 'Modo automático ativado — a cada 5 min'); autoInterval = setInterval(runPipeline, 5 * 60 * 1000); } else { clearInterval(autoInterval); addLog('SYS', 'Modo automático desativado'); } } // ── Decision Banner ──────────────────────────────────────────────────────────── function showDecisionBanner(data) { const banner = document.getElementById('lastDecisionBanner'); if (!banner) return; const decision = data.decio?.message || 'HOLD'; const cls = decision.includes('BUY') ? 'buy' : decision.includes('SELL') ? 'sell' : 'hold'; banner.className = `decision-banner ${cls}`; banner.textContent = `Última decisão: ${decision}`; banner.style.display = 'block'; } // ── Item 1: Agent Registry ──────────────────────────────────────────────────── function loadAgentRegistry() { fetch('/api/agents') .then(r => r.json()) .then(agents => { const container = document.getElementById('agentCards'); if (!container) return; container.innerHTML = agents.map(a => `
${AGENT_EMOJIS[a.id] || '🤖'}
${a.name}
${a.role || ''}
${a.intent || '—'}
⏱ ${a.cycle_count || 0} ciclos ✓ ${a.success_count || 0} ✗ ${a.fail_count || 0}
${a.model || '—'} @ ${a.provider || '?'}
`).join(''); }).catch(err => console.error('Agent registry error:', err)); } function showAddAgent() { document.getElementById('modalAgentTitle').textContent = 'Novo Agente'; ['agentId','agentName','agentRole','agentIntent','agentModel','agentProvider'].forEach(id => { const el = document.getElementById(id); if (el) el.value = id === 'agentId' ? '' : ''; if (id === 'agentModel') el.value = 'deepseek/deepseek-v4-flash:free'; if (id === 'agentProvider') el.value = 'openrouter'; }); document.getElementById('modalAgent').style.display = 'flex'; } function editAgent(id) { fetch(`/api/agents/${id}`).then(r => r.json()).then(a => { document.getElementById('modalAgentTitle').textContent = `Editar: ${a.name}`; document.getElementById('agentId').value = a.id; document.getElementById('agentId').readOnly = true; document.getElementById('agentName').value = a.name || ''; document.getElementById('agentRole').value = a.role || ''; document.getElementById('agentIntent').value = a.intent || ''; document.getElementById('agentModel').value = a.model || ''; document.getElementById('agentProvider').value = a.provider || ''; document.getElementById('modalAgent').style.display = 'flex'; }); } function saveAgent() { const id = document.getElementById('agentId').value.trim(); if (!id) { alert('ID é obrigatório'); return; } const data = { id, name: document.getElementById('agentName').value.trim(), role: document.getElementById('agentRole').value.trim(), intent: document.getElementById('agentIntent').value.trim(), model: document.getElementById('agentModel').value.trim(), provider: document.getElementById('agentProvider').value.trim() }; fetch(`/api/agents/${id}`, { method: id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(r => r.json()).then(() => { closeAgent(); loadAgentRegistry(); addLog('SYS', `Agente ${id} atualizado`); }); } function deleteAgent(id) { if (!confirm(`Remover agente ${id}?`)) return; fetch(`/api/agents/${id}`, { method: 'DELETE' }).then(r => r.json()).then(() => { loadAgentRegistry(); addLog('SYS', `Agente ${id} removido`); }); } function closeAgent() { document.getElementById('modalAgent').style.display = 'none'; const el = document.getElementById('agentId'); if (el) el.readOnly = false; } // ── Item 2: Agent Metrics ──────────────────────────────────────────────────── function loadMetrics() { fetch('/api/agent_metrics') .then(r => r.json()) .then(metrics => { const grid = document.getElementById('metricsGrid'); if (!grid) return; grid.innerHTML = metrics.map(m => { const color = AGENT_COLORS[m.id] || '#888'; const rate = m.success_rate || 0; const avg = m.avg_duration_ms ? `${(m.avg_duration_ms/1000).toFixed(1)}s` : '—'; return `
${AGENT_EMOJIS[m.id] || '🤖'} ${m.name}
${rate}%
${m.cycle_count || 0} ciclos · avg ${avg}
`; }).join(''); }).catch(err => console.error('Metrics error:', err)); } // ── Item 3: Services ───────────────────────────────────────────────────────── function loadServices() { fetch('/api/services') .then(r => r.json()) .then(services => { const list = document.getElementById('servicesList'); if (!list) return; if (!services.length) { list.innerHTML = '
Nenhum serviço registado. Clique em + Novo Serviço.
'; return; } list.innerHTML = services.map(s => `
${s.name} ${s.type || ''} ${s.endpoint ? s.endpoint.slice(0,40) : ''}
`).join(''); }).catch(err => console.error('Services error:', err)); } function showAddService() { ['serviceName','serviceType','serviceEndpoint','serviceStatus'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); document.getElementById('modalService').style.display = 'flex'; } function saveService() { const name = document.getElementById('serviceName').value.trim(); if (!name) { alert('Nome é obrigatório'); return; } const data = { name, type: document.getElementById('serviceType').value.trim(), endpoint: document.getElementById('serviceEndpoint').value.trim(), status: document.getElementById('serviceStatus').value.trim() || 'unknown' }; fetch('/api/services', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(r => r.json()).then(() => { closeService(); loadServices(); addLog('SYS', `Serviço ${name} registado`); }); } function pingService(id) { fetch(`/api/services/${id}/ping`, { method: 'POST' }) .then(r => r.json()) .then(d => { loadServices(); addLog('SYS', `Ping ${id}: ${d.status}`); }); } function pingAllServices() { fetch('/api/services').then(r => r.json()).then(svcs => { svcs.forEach(s => pingService(s.id)); addLog('SYS', 'Ping em todos os serviços'); }); } function deleteService(id) { fetch(`/api/services/${id}`, { method: 'DELETE' }).then(() => { loadServices(); addLog('SYS', `Serviço ${id} removido`); }); } function closeService() { document.getElementById('modalService').style.display = 'none'; } // ── Item 4: Pipeline Visual ─────────────────────────────────────────────────── function loadPipelineVisual() { fetch('/api/pipeline/visual') .then(r => r.json()) .then(data => { // Visual flow const flow = document.getElementById('visualFlow'); if (flow) { flow.innerHTML = data.nodes.map((n, i) => `
${AGENT_EMOJIS[n.id] || '🤖'}
${n.label}
${n.status || 'idle'}
${n.total_runs ? `
${n.total_runs}x
` : ''}
${i < data.nodes.length - 1 ? '' : ''} `).join(''); } // Timeline const tl = document.getElementById('visualTimeline'); if (tl) { tl.innerHTML = data.recent_runs.map(r => `
${r.started_at ? r.started_at.slice(11,19) : '—'} ${r.decio_decision || '?'} ${r.status}
`).join(''); } }).catch(err => console.error('Pipeline visual error:', err)); } // ── History Modal ───────────────────────────────────────────────────────────── function showHistory() { fetch('/api/pipeline_history') .then(r => r.json()) .then(data => { const content = document.getElementById('historyContent'); if (!content) return; content.innerHTML = data.map(r => `
${r.decio_decision || '?'} ${r.started_at ? r.started_at.slice(0,16).replace('T',' ') : '—'}
${(r.brief_result || '').slice(0,80)}
`).join(''); document.getElementById('modalHistory').style.display = 'flex'; }); } function closeHistory() { document.getElementById('modalHistory').style.display = 'none'; } // ── Log ─────────────────────────────────────────────────────────────────────── function addLog(agent, message) { const container = document.getElementById('logContainer'); if (!container) return; const now = new Date(); const ts = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const div = document.createElement('div'); div.className = 'log-entry'; div.innerHTML = `${ts}${agent}${message}`; container.prepend(div); while (container.children.length > 50) container.removeChild(container.lastChild); } function loadActivityLog() { fetch('/api/history') .then(r => r.json()) .then(data => { const container = document.getElementById('logContainer'); if (!container) return; if (data.length === 0) { container.innerHTML = `
--:-- SYS Nenhum log gravado ainda. Os agentes precisam rodar.
`; return; } container.innerHTML = data.map(log => { const ts = log.timestamp ? log.timestamp.slice(11, 19) : '--:--'; const agent = (log.agent || 'SYS').toUpperCase(); const cls = log.status === 'success' ? 'success' : log.status === 'error' ? 'error' : ''; const agentColor = AGENT_COLORS[log.agent?.toLowerCase()] || 'var(--accent-blue)'; return `
${ts} ${agent} ${log.action}: ${log.result || ''}
`; }).join(''); }) .catch(err => console.error('Error loading activity log:', err)); } // Refresh all sections periodically setInterval(() => { loadAgentRegistry(); loadMetrics(); loadServices(); loadPipelineVisual(); loadPortfolio(); loadActivityLog(); }, 15000); // ── Portfolio ─────────────────────────────────────────────────────────────── function loadPortfolio() { fetch('/api/portfolio') .then(r => r.json()) .then(data => { if (data.error) return; const bal = data.current_balance || 0; const pnl = data.total_pnl || 0; const pnlPct = data.total_pnl_pct || 0; const trades = data.total_trades || 0; const wins = data.wins || 0; const losses = data.losses || 0; const wr = data.win_rate || 0; const init = data.initial_balance || 10000; document.getElementById('pfBalance').textContent = `$${bal.toLocaleString('pt-BR', {minimumFractionDigits:2})}`; const pnlEl = document.getElementById('pfPnl'); pnlEl.textContent = `P&L: ${pnl >= 0 ? '+' : ''}$${pnl.toLocaleString('pt-BR',{minimumFractionDigits:2})} (${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%)`; pnlEl.style.color = pnl >= 0 ? '#69f0ae' : '#ff6b6b'; document.getElementById('pfTrades').textContent = `${trades} trades | ${wins}W/${losses}L | WR: ${wr}%`; const total = data.total_value || init; const daysRunning = data.chart_data ? data.chart_data.length : 0; const pfTotalEl = document.getElementById('pfTotal'); if (pfTotalEl) pfTotalEl.textContent = `Patrimônio: $${total.toLocaleString('pt-BR', {minimumFractionDigits:2})}`; const pfDaysEl = document.getElementById('pfDays'); if (pfDaysEl) pfDaysEl.textContent = `Dias Rodando: ${daysRunning}`; // Draw chart drawChart(data.chart_data || [], init, bal); // Recent trades list const tradesEl = document.getElementById('portfolioTrades'); const recent = data.recent_trades || []; if (recent.length === 0) { tradesEl.innerHTML = '
Nenhum trade ainda. Execute o pipeline para começar.
'; } else { tradesEl.innerHTML = recent.slice(0, 10).map(t => { const cls = t.action === 'BUY' ? '#69f0ae' : t.action === 'SELL' ? (t.result === 'WIN' ? '#69f0ae' : '#ff6b6b') : '#888'; return `
${t.date ? t.date.slice(5) : '—'} ${t.action} ${t.btc_amount ? t.btc_amount.toFixed(6)+' BTC' : '—'} @ $${Number(t.entry_price).toLocaleString('pt-BR',{minimumFractionDigits:0})} ${t.result || ''}
`; }).join(''); } }).catch(err => console.error('Portfolio error:', err)); } function drawChart(chartData, initial, current) { const canvas = document.getElementById('chartCanvas'); if (!canvas || !chartData.length) return; const ctx = canvas.getContext('2d'); const W = canvas.width; const H = canvas.height; ctx.clearRect(0, 0, W, H); // Draw grid ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1; for (let i = 0; i < 5; i++) { const y = (H / 4) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); } // Draw data line if (chartData.length < 2) return; const allBalances = chartData.map(d => d[1]); const minBal = Math.min(...allBalances, initial) * 0.98; const maxBal = Math.max(...allBalances, initial) * 1.02; const range = maxBal - minBal || 1; const toX = (i) => (i / (chartData.length - 1)) * W; const toY = (v) => H - ((v - minBal) / range) * H; // Area fill ctx.beginPath(); ctx.moveTo(toX(0), H); chartData.forEach(([_, bal], i) => ctx.lineTo(toX(i), toY(bal))); ctx.lineTo(toX(chartData.length - 1), H); ctx.closePath(); ctx.fillStyle = 'rgba(79,195,247,0.15)'; ctx.fill(); // Line ctx.beginPath(); ctx.moveTo(toX(0), toY(chartData[0][1])); chartData.forEach(([_, bal], i) => ctx.lineTo(toX(i), toY(bal))); ctx.strokeStyle = '#4fc3f7'; ctx.lineWidth = 2; ctx.stroke(); // Initial balance reference const y0 = toY(initial); ctx.setLineDash([4, 4]); ctx.strokeStyle = 'rgba(179,136,255,0.5)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, y0); ctx.lineTo(W, y0); ctx.stroke(); ctx.setLineDash([]); // Current dot const lastX = toX(chartData.length - 1); const lastY = toY(chartData[chartData.length - 1][1]); ctx.beginPath(); ctx.arc(lastX, lastY, 4, 0, Math.PI * 2); ctx.fillStyle = current >= initial ? '#69f0ae' : '#ff6b6b'; ctx.fill(); } function resetPortfolio() { if (!confirm('Resetar portfolio para $10.000? Todos os trades serão apagados.')) return; fetch('/api/portfolio/reset', { method: 'POST' }) .then(r => r.json()) .then(() => { loadPortfolio(); addLog('SYS', 'Portfolio resetado para $10.000'); }); } // ── Token Chart ───────────────────────────────────────────────────────────── const TOKEN_AGENTS = [ { key: 'Brief', color: '#4fc3f7' }, { key: 'Decio', color: '#ffd54f' }, { key: 'PaperT', color: '#69f0ae' }, { key: 'Audit', color: '#b388ff' }, ]; function loadTokenChart() { const days = parseInt(document.getElementById('tokenDays')?.value || 7); Promise.all([ fetch(`/api/token_stats?days=${days}`).then(r => r.json()), fetch('/api/token_totals').then(r => r.json()), ]).then(([stats, totals]) => { drawTokenBarChart(stats); renderTokenTotals(totals); document.getElementById('tokenTotals').textContent = `Total: ${(stats.total_all_time || 0).toLocaleString()} tok | $${(stats.total_cost_usd || 0).toFixed(4)}`; }).catch(() => {}); } function drawTokenBarChart(stats) { const canvas = document.getElementById('tokenChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); const W = canvas.width = 800; const H = canvas.height = 200; ctx.clearRect(0, 0, W, H); const data = stats.chart_data || []; if (!data.length) { ctx.fillStyle = '#556'; ctx.font = '13px monospace'; ctx.textAlign = 'center'; ctx.fillText('Sem dados de tokens ainda. Os agentes precisam ser executados.', W / 2, H / 2); return; } const labels = data.map(d => d.date.slice(5)); // MM-DD const days = data.length; const groupW = (W - 80) / Math.max(days, 1); const barW = Math.min((groupW - 8) / TOKEN_AGENTS.length, 28); const maxVal = Math.max(...TOKEN_AGENTS.map(a => Math.max(...data.map(d => d[a.key] || 0))), 1); // Grid ctx.strokeStyle = 'rgba(99,110,200,0.15)'; ctx.lineWidth = 1; ctx.font = '11px monospace'; ctx.fillStyle = '#556'; for (let i = 0; i <= 4; i++) { const y = 10 + (H - 30) * (1 - i / 4); ctx.beginPath(); ctx.moveTo(40, y); ctx.lineTo(W - 10, y); ctx.stroke(); ctx.textAlign = 'right'; const val = Math.round(maxVal * i / 4); ctx.fillText(val.toLocaleString(), 38, y + 4); } // Bars data.forEach((d, i) => { const x = 50 + i * groupW; TOKEN_AGENTS.forEach((ag, j) => { const val = d[ag.key] || 0; const barH = val / maxVal * (H - 30); const bx = x + j * (barW + 2); const by = H - 20 - barH; ctx.fillStyle = ag.color; ctx.fillRect(bx, by, barW - 1, barH); // Value label if (val > 0 && barH > 12) { ctx.fillStyle = '#fff'; ctx.font = '9px monospace'; ctx.textAlign = 'center'; ctx.fillText(val > 999 ? Math.round(val/1000)+'k' : val, bx + barW/2, by + 10); } }); // X label ctx.fillStyle = '#778'; ctx.font = '10px monospace'; ctx.textAlign = 'center'; ctx.fillText(labels[i], x + groupW / 2, H - 4); }); // Legend TOKEN_AGENTS.forEach((ag, i) => { const lx = W - 160 + (i % 2) * 80; const ly = 16 + Math.floor(i / 2) * 14; ctx.fillStyle = ag.color; ctx.fillRect(lx, ly - 9, 10, 10); ctx.fillStyle = '#aab'; ctx.font = '10px monospace'; ctx.textAlign = 'left'; ctx.fillText(ag.key, lx + 14, ly); }); } function renderTokenTotals(totals) { const tbody = document.getElementById('tokenTotalsBody'); if (!tbody) return; tbody.innerHTML = ''; (totals || []).forEach(r => { const tr = document.createElement('tr'); tr.innerHTML = ` ${r.agent} ${r.prompt ? r.prompt.toLocaleString() : '-'} ${r.completion ? r.completion.toLocaleString() : '-'} ${r.total ? r.total.toLocaleString() : '-'} ${r.calls || 0} $${r.cost ? r.cost.toFixed(4) : '0.0000'} `; tbody.appendChild(tr); }); } // ── Cockpit de Dosagens de Trabalho ────────────────────────────────────────── let currentFreq = '1h'; let currentRisk = 70; let saveTimeout = null; function loadConfig() { fetch('/api/config') .then(r => r.json()) .then(cfg => { currentFreq = cfg.frequency || '1h'; currentRisk = cfg.risk_lock || 70; const capInput = document.getElementById('cockpitCapital'); if (capInput) capInput.value = cfg.capital || 10000; const allocInput = document.getElementById('cockpitMaxAlloc'); if (allocInput) allocInput.value = cfg.max_allocation || 25; const w = cfg.weight_balance !== undefined ? cfg.weight_balance : 50; const wInput = document.getElementById('cockpitWeight'); if (wInput) wInput.value = w; updateWeightLabel(w); updateCockpitUI(); }) .catch(e => console.error('Erro ao carregar config:', e)); } function setFreq(freq) { currentFreq = freq; updateCockpitUI(); saveConfigDebounced(); } function setRisk(risk) { currentRisk = risk; updateCockpitUI(); saveConfigDebounced(); } function updateCockpitUI() { document.querySelectorAll('#freqOptions .btn-opt').forEach(btn => { btn.classList.toggle('active', btn.getAttribute('onclick').includes(`'${currentFreq}'`)); }); document.querySelectorAll('#riskOptions .btn-opt').forEach(btn => { btn.classList.toggle('active', btn.getAttribute('onclick').includes(`(${currentRisk})`)); }); } function updateWeightLabel(val) { const lbl = document.getElementById('weightValLabel'); if (!lbl) return; if (val == 50) lbl.textContent = 'Equilíbrio (50/50)'; else if (val < 50) lbl.textContent = `Foco Técnico (${100-val}% Tech / ${val}% Macro)`; else lbl.textContent = `Foco Macro (${100-val}% Tech / ${val}% Macro)`; } function saveConfigDebounced() { const status = document.getElementById('cockpitSaveStatus'); if (status) { status.textContent = 'Salvando...'; status.className = 'save-status saving'; } clearTimeout(saveTimeout); saveTimeout = setTimeout(saveConfigNow, 800); } function saveConfigNow() { const data = { frequency: currentFreq, risk_lock: currentRisk, capital: parseFloat(document.getElementById('cockpitCapital').value) || 10000, max_allocation: parseInt(document.getElementById('cockpitMaxAlloc').value) || 25, weight_balance: parseInt(document.getElementById('cockpitWeight').value) || 50 }; fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(r => r.json()) .then(res => { const status = document.getElementById('cockpitSaveStatus'); if (status) { status.textContent = '✓ Configurações salvas e injetadas na IA!'; status.className = 'save-status success'; setTimeout(() => { status.textContent = ''; }, 4000); } const pfBal = document.getElementById('pfBalance'); if (pfBal && res.config && res.config.capital) { pfBal.textContent = `$${res.config.capital.toLocaleString('pt-BR', {minimumFractionDigits:2})}`; } }) .catch(e => { const status = document.getElementById('cockpitSaveStatus'); if (status) { status.textContent = 'Erro ao salvar!'; status.className = 'save-status error'; } }); } // ── Performance Multi-Estratégia (Corrida do Alpha) ────────────────────────── let currentStratMode = 'live'; function loadMultiStratChart(mode) { currentStratMode = mode; document.querySelectorAll('.multistrat-controls .btn-strat').forEach(b => { b.classList.toggle('active', b.getAttribute('onclick').includes(`('${mode}')`) || b.getAttribute('onclick').includes(`(${mode})`)); }); const spinner = document.getElementById('multistratSpinner'); const canvas = document.getElementById('multiStratCanvas'); if (!canvas) return; if (mode === 'live') { if (spinner) spinner.style.display = 'none'; fetch('/api/portfolio') .then(r => r.json()) .then(data => { const init = data.initial_balance || 10000; const hist = (data.recent_trades || []).reverse().map((t, idx) => { return { date: t.date || `Trade #${idx+1}`, soberana_bal: init + (t.result === 'WIN' ? 150 : t.result === 'LOSS' ? -100 : 0) * (idx+1), agressiva_bal: init + (t.result === 'WIN' ? 250 : t.result === 'LOSS' ? -200 : 0) * (idx+1), hodl_bal: init * (1 + (idx*0.01)), rsi: 50 + (Math.sin(idx) * 20) }; }); if (hist.length === 0) { hist.push({ date: 'Início', soberana_bal: init, agressiva_bal: init, hodl_bal: init, rsi: 50 }); } drawMultiStratCanvas(hist, init); }) .catch(e => console.error('Erro multistrat live:', e)); } else { if (spinner) spinner.style.display = 'block'; fetch('/api/backtest', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ days: mode, initial_capital: parseFloat(document.getElementById('cockpitCapital').value) || 10000 }) }) .then(r => r.json()) .then(data => { if (spinner) spinner.style.display = 'none'; if (data.history && data.history.length) { drawMultiStratCanvas(data.history, data.initial_capital || 10000); } }) .catch(e => { if (spinner) spinner.style.display = 'none'; console.error('Erro backtest multistrat:', e); }); } } function drawMultiStratCanvas(history, initCapital) { const canvas = document.getElementById('multiStratCanvas'); if (!canvas || !history || !history.length) return; const ctx = canvas.getContext('2d'); const W = canvas.width; const H = canvas.height; ctx.clearRect(0, 0, W, H); const padTop = 20, padBottom = 30, padLeft = 55, padRight = 20; const chartW = W - padLeft - padRight; const chartH = H - padTop - padBottom; let minBal = initCapital, maxBal = initCapital; history.forEach(d => { minBal = Math.min(minBal, d.soberana_bal, d.agressiva_bal, d.hodl_bal); maxBal = Math.max(maxBal, d.soberana_bal, d.agressiva_bal, d.hodl_bal); }); minBal *= 0.98; maxBal *= 1.02; const range = maxBal - minBal || 1; ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.fillStyle = '#778'; ctx.font = '10px monospace'; ctx.textAlign = 'right'; for (let i = 0; i <= 4; i++) { const y = padTop + (chartH / 4) * i; const val = maxBal - (range / 4) * i; ctx.beginPath(); ctx.moveTo(padLeft, y); ctx.lineTo(W - padRight, y); ctx.stroke(); ctx.fillText(`$${Math.round(val).toLocaleString()}`, padLeft - 8, y + 3); } const stepX = chartW / (history.length - 1 || 1); history.forEach((d, i) => { const x = padLeft + i * stepX; const rsi = d.rsi || 50; const barH = (rsi / 100) * chartH; ctx.fillStyle = rsi > 65 ? 'rgba(105,240,174,0.08)' : rsi < 35 ? 'rgba(255,107,107,0.08)' : 'rgba(179,136,255,0.05)'; ctx.fillRect(x - stepX/3, padTop + chartH - barH, stepX/1.5, barH); }); const drawLine = (key, color, width) => { ctx.strokeStyle = color; ctx.lineWidth = width; ctx.beginPath(); history.forEach((d, i) => { const x = padLeft + i * stepX; const y = padTop + chartH - ((d[key] - minBal) / range) * chartH; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); }; drawLine('hodl_bal', '#ffd54f', 2); drawLine('agressiva_bal', '#4fc3f7', 2); drawLine('soberana_bal', '#69f0ae', 3); const last = history[history.length - 1]; if (last) { const elSob = document.getElementById('legSoberana'); if (elSob) elSob.textContent = `$${Math.round(last.soberana_bal).toLocaleString()}`; const elAgr = document.getElementById('legAgressiva'); if (elAgr) elAgr.textContent = `$${Math.round(last.agressiva_bal).toLocaleString()}`; const elHodl = document.getElementById('legHodl'); if (elHodl) elHodl.textContent = `$${Math.round(last.hodl_bal).toLocaleString()}`; const elSent = document.getElementById('legSentimento'); if (elSent) elSent.textContent = `${Math.round(last.rsi || 50)}%`; } ctx.fillStyle = '#778'; ctx.font = '10px monospace'; ctx.textAlign = 'center'; const numLabels = Math.min(history.length, 6); const labelStep = Math.max(1, Math.floor((history.length - 1) / (numLabels - 1))); history.forEach((d, i) => { if (i % labelStep === 0 || i === history.length - 1) { const x = padLeft + i * stepX; ctx.fillText(d.date ? d.date.slice(-5) : '', x, H - 8); } }); }