Files
BrainsteelFin/static/script.js
T

873 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* BrainSteel Fin — Dashboard JavaScript
* 4-Agent Pipeline: Brief → Decio → PaperT → Audit
*/
// ── State ─────────────────────────────────────────────────────────────────────
let autoMode = false;
let autoInterval = null;
let pollInterval = null;
const AGENT_EMOJIS = { brief: "📊", decio: "🎯", papert: "⚡", audit: "🔍" };
const AGENT_COLORS = { brief: "#4fc3f7", decio: "#ffd54f", papert: "#69f0ae", audit: "#b388ff" };
// ── Init ─────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
updateClock();
setInterval(updateClock, 1000);
startPolling();
loadAgentRegistry();
loadMetrics();
loadServices();
loadPipelineVisual();
loadTokenChart();
loadConfig();
loadMultiStratChart('live');
loadPortfolio();
loadActivityLog();
updateStatus('Sistema operacional', 'online');
});
// ── 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;
}
// ── 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 => `
<div class="agent-card" id="card-${a.id}">
<div class="agent-card-header">
<span class="agent-card-emoji">${AGENT_EMOJIS[a.id] || '🤖'}</span>
<div>
<div class="agent-card-name">${a.name}</div>
<div class="agent-card-role">${a.role || ''}</div>
</div>
</div>
<div class="agent-card-intent">${a.intent || '—'}</div>
<div class="agent-card-stats">
<span class="stat-badge runs">⏱ ${a.cycle_count || 0} ciclos</span>
<span class="stat-badge ok">✓ ${a.success_count || 0}</span>
<span class="stat-badge fail">✗ ${a.fail_count || 0}</span>
</div>
<div class="agent-card-model">${a.model || '—'} @ ${a.provider || '?'}</div>
<div class="agent-card-actions">
<button onclick="editAgent('${a.id}')">✏️ Editar</button>
<button onclick="deleteAgent('${a.id}')" style="color:#ff6b6b">🗑️ Remover</button>
</div>
</div>
`).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 `
<div class="metric-card ${m.id}">
<div class="metric-name">${AGENT_EMOJIS[m.id] || '🤖'} ${m.name}</div>
<div class="metric-value">${rate}%</div>
<div class="metric-sub">${m.cycle_count || 0} ciclos · avg ${avg}</div>
<div class="metric-bar"><div class="metric-bar-fill" style="width:${rate}%;background:${color}"></div></div>
</div>`;
}).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 = '<div class="service-item" style="color:var(--text-secondary)">Nenhum serviço registado. Clique em + Novo Serviço.</div>';
return;
}
list.innerHTML = services.map(s => `
<div class="service-item" id="svc-${s.id}">
<span class="service-status-dot ${s.status || 'unknown'}"></span>
<span class="service-name">${s.name}</span>
<span class="service-type">${s.type || ''}</span>
<span class="service-endpoint">${s.endpoint ? s.endpoint.slice(0,40) : ''}</span>
<div class="service-actions">
<button onclick="pingService('${s.id}')">🏓</button>
<button onclick="deleteService('${s.id}')" style="color:#ff6b6b">×</button>
</div>
</div>
`).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) => `
<div class="visual-node" id="vis-${n.id}">
<div style="font-size:1.4rem">${AGENT_EMOJIS[n.id] || '🤖'}</div>
<div class="visual-node-label">${n.label}</div>
<div class="visual-node-status">${n.status || 'idle'}</div>
${n.total_runs ? `<div style="font-size:0.6rem;color:var(--text-secondary)">${n.total_runs}x</div>` : ''}
</div>
${i < data.nodes.length - 1 ? '<span class="visual-arrow">→</span>' : ''}
`).join('');
}
// Timeline
const tl = document.getElementById('visualTimeline');
if (tl) {
tl.innerHTML = data.recent_runs.map(r => `
<div class="timeline-item">
<span class="timeline-ts">${r.started_at ? r.started_at.slice(11,19) : '—'}</span>
<span class="timeline-decision ${r.decio_decision}">${r.decio_decision || '?'}</span>
<span style="font-size:0.65rem;color:var(--text-secondary)">${r.status}</span>
</div>
`).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 => `
<div class="history-item" style="padding:8px;border-bottom:1px solid rgba(255,255,255,0.05)">
<div style="display:flex;justify-content:space-between">
<span style="font-weight:700">${r.decio_decision || '?'}</span>
<span style="color:var(--text-secondary);font-size:0.75rem">${r.started_at ? r.started_at.slice(0,16).replace('T',' ') : '—'}</span>
</div>
<div style="font-size:0.75rem;color:var(--text-secondary);margin-top:4px">${(r.brief_result || '').slice(0,80)}</div>
</div>
`).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 = `<span class="log-time">${ts}</span><span class="log-agent">${agent}</span><span class="log-message">${message}</span>`;
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 = `
<div class="log-entry system">
<span class="log-time">--:--</span>
<span class="log-agent">SYS</span>
<span class="log-message">Nenhum log gravado ainda. Os agentes precisam rodar.</span>
</div>`;
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 `
<div class="log-entry ${cls}">
<span class="log-time">${ts}</span>
<span class="log-agent" style="color: ${agentColor}">${agent}</span>
<span class="log-message"><b>${log.action}</b>: ${log.result || ''}</span>
</div>`;
}).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}%`;
// 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 = '<div style="color:var(--text-secondary);font-size:0.8rem;padding:8px">Nenhum trade ainda. Execute o pipeline para começar.</div>';
} 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 `<div style="display:flex;gap:10px;padding:6px 8px;background:var(--bg-card-hover);border-radius:6px;margin-bottom:4px;font-size:0.78rem">
<span style="color:var(--text-secondary)">${t.date ? t.date.slice(5) : '—'}</span>
<span style="font-weight:700;color:${cls}">${t.action}</span>
<span>${t.btc_amount ? t.btc_amount.toFixed(6)+' BTC' : '—'}</span>
<span>@ $${Number(t.entry_price).toLocaleString('pt-BR',{minimumFractionDigits:0})}</span>
<span style="color:${t.result==='WIN'?'#69f0ae':t.result==='LOSS'?'#ff6b6b':'#888'}">${t.result || ''}</span>
</div>`;
}).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 = `
<td style="color:${AGENT_COLORS[r.agent.toLowerCase()] || '#aab'}">${r.agent}</td>
<td>${r.prompt ? r.prompt.toLocaleString() : '-'}</td>
<td>${r.completion ? r.completion.toLocaleString() : '-'}</td>
<td><b>${r.total ? r.total.toLocaleString() : '-'}</b></td>
<td>${r.calls || 0}</td>
<td>$${r.cost ? r.cost.toFixed(4) : '0.0000'}</td>
`;
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);
}
});
}