/**
* 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();
loadPipelineVisual(); // Refresh pipeline visual for new asset
loadConfig();
loadPortfolio();
loadMultiStratChart('live');
// Update pipeline header
updatePipelineHeader();
addLog('SYS', `Ativo alterado para ${ASSET_LABELS[asset]} (${ASSET_PAIRS[asset]})`);
}
function clearAssetData() {
// Clear portfolio stale BTC data
const pfTrades = document.getElementById('portfolioTrades');
if (pfTrades) pfTrades.innerHTML = `
Carregando ${ASSET_LABELS[currentAsset]}...
`;
// Clear portfolio header values
const pfPnl = document.getElementById('pfPnl');
if (pfPnl) { pfPnl.textContent = 'P&L: —'; pfPnl.style.color = 'var(--text-secondary)'; }
const pfTrades2 = document.getElementById('pfTrades');
if (pfTrades2) pfTrades2.textContent = '—';
const pfTotal = document.getElementById('pfTotal');
if (pfTotal) pfTotal.textContent = ASSET_PAIRS[currentAsset];
const pfDays = document.getElementById('pfDays');
if (pfDays) pfDays.textContent = ASSET_LABELS[currentAsset];
// Clear multi-strat canvas
const canvas = document.getElementById('multiStratCanvas');
if (canvas) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); }
}
function updatePipelineHeader() {
const h2 = document.querySelector('.pipeline-section h2');
if (h2) {
const emoji = currentAsset === 'btc' ? '📊' : currentAsset === 'usd' ? '💵' : '🪙';
h2.innerHTML = `${emoji} Pipeline ${ASSET_LABELS[currentAsset]}`;
}
// Update run button text
const btnRun = document.getElementById('btnRun');
if (btnRun) {
if (currentAsset === 'btc') {
btnRun.innerHTML = '▶ Executar Pipeline';
btnRun.disabled = false;
} else {
btnRun.innerHTML = `▶ Executar ${ASSET_LABELS[currentAsset]}`;
btnRun.disabled = false;
}
}
}
// ── Asset Brief Loader ───────────────────────────────────────────────────────
function loadAssetBrief() {
if (currentAsset === 'btc') {
// BTC uses the existing pipeline via pollState
pollState();
showBTCPanels();
return;
}
// Clear stale BTC data from portfolio/timeline when switching to USD/XAU
clearAssetData();
const apiMap = { usd: '/api/usd/brief', xau: '/api/xau/brief' };
const api = apiMap[currentAsset];
if (!api) return;
// Show loading state on agent cards
updateAgentUI('brief', { status: 'working', message: `Carregando ${ASSET_LABELS[currentAsset]}...` });
updateAgentUI('decio', { status: 'waiting', message: 'Aguardando...' });
updateAgentUI('papert', { status: 'idle', message: 'Aguardando...' });
updateAgentUI('audit', { status: 'idle', message: 'Aguardando...' });
fetch(api)
.then(r => r.json())
.then(data => {
if (data.error) {
addLog('SYS', `Erro: ${data.error}`);
return;
}
const brief = data.brief;
const decio = data.decio;
// Format price based on asset type
let priceStr = '-';
if (brief.price && brief.price > 0) {
if (currentAsset === 'usd') {
priceStr = `R$ ${brief.price.toFixed(4)}`;
} else if (currentAsset === 'xau') {
priceStr = `US$ ${brief.price.toLocaleString('pt-BR', {minimumFractionDigits: 2})}`;
}
}
// Update header with price badge
const headerStatus = document.querySelector('.header-status');
if (headerStatus) {
headerStatus.innerHTML = `
${ASSET_LABELS[currentAsset]}: ${priceStr}
`;
}
// Update brief agent card
let signalColor = '#888';
if (brief.signal === 'ALTA') signalColor = '#69f0ae';
else if (brief.signal === 'BAIXA') signalColor = '#ff6b6b';
updateAgentUI('brief', {
status: 'done',
message: `${ASSET_PAIRS[currentAsset]}: ${priceStr}`
});
// Update decio agent card
let decisionColor = '#888';
if (decio.decision === 'BUY') decisionColor = '#69f0ae';
else if (decio.decision === 'SELL') decisionColor = '#ff6b6b';
updateAgentUI('decio', {
status: 'done',
message: `Decisão: ${decio.decision} (${decio.confidence}%)`
});
// Update papert
updateAgentUI('papert', {
status: 'done',
message: `SL: ${decio.stop_loss || 0} | TP: ${decio.take_profit || 0}`
});
// Update audit
updateAgentUI('audit', {
status: 'done',
message: `${brief.signal} | RSI: ${brief.rsi || '-'}`
});
// Show decision banner
const banner = document.getElementById('lastDecisionBanner');
if (banner) {
banner.style.display = 'block';
banner.className = `decision-banner ${decio.decision.toLowerCase()}`;
banner.innerHTML = `
${decio.decision}
Confiança: ${decio.confidence}%
${decio.stop_loss ? `Stop Loss: ${decio.stop_loss}` : ''}
${decio.take_profit ? `Take Profit: ${decio.take_profit}` : ''}
Signal: ${brief.signal}
RSI: ${brief.rsi || '-'}
${brief.change_24h !== undefined ? `24h: ${brief.change_24h >= 0 ? '+' : ''}${brief.change_24h}%` : ''}
`;
}
// Update sidebar status
const sidebarDot = document.getElementById('sidebarStatusDot');
const sidebarText = document.getElementById('sidebarStatusText');
if (sidebarDot) sidebarDot.style.background = decisionColor;
if (sidebarText) sidebarText.textContent = `${decio.decision} ${decio.confidence}%`;
addLog('SYS', `${ASSET_LABELS[currentAsset]} | ${decio.decision} | Conf: ${decio.confidence}%`);
})
.catch(err => {
addLog('SYS', `Erro API ${currentAsset}: ${err.message}`);
updateAgentUI('brief', { status: 'error', message: 'Erro ao carregar' });
});
}
function showBTCPanels() {
// Restore BTC panels
const banner = document.getElementById('lastDecisionBanner');
if (banner) banner.style.display = 'none';
const headerStatus = document.querySelector('.header-status');
if (headerStatus) {
headerStatus.innerHTML = `
Sistema online
`;
}
const sidebarDot = document.getElementById('sidebarStatusDot');
const sidebarText = document.getElementById('sidebarStatusText');
if (sidebarDot) sidebarDot.style.background = '#69f0ae';
if (sidebarText) sidebarText.textContent = 'Online';
}
// ── 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');
updatePipelineHeader();
// 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() {
if (currentAsset === 'btc') {
pollState();
pollInterval = setInterval(() => {
if (currentAsset === 'btc') pollState();
}, 3000);
} else {
// For USD/XAU, poll the asset brief
loadAssetBrief();
pollInterval = setInterval(() => {
if (currentAsset !== 'btc') loadAssetBrief();
else pollState();
}, 10000);
}
}
function pollState() {
if (currentAsset !== 'btc') return; // Don't poll BTC state when on other assets
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 => `
${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/${currentAsset}`)
.then(r => r.json())
.then(data => {
// Visual flow
const flow = document.getElementById('visualFlow');
if (flow) {
flow.innerHTML = data.nodes.map((n, i) => `
${n.emoji || AGENT_EMOJIS[n.id] || '🤖'}
${n.label}
${n.status || 'idle'}
${n.last_run ? `
${n.last_run.slice(11,19)}
` : ''}
${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();
if (currentAsset === 'btc') {
pollState();
}
}, 15000);
// ── Portfolio ───────────────────────────────────────────────────────────────
function loadPortfolio() {
const api = currentAsset === 'btc' ? '/api/portfolio' : `/api/portfolio/${currentAsset}`;
fetch(api)
.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;
const symbol = data.symbol || 'BTC/USD';
const price = data.current_price;
const change = data.change_24h || 0;
// Header: show price + symbol
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) {
let totalText = `${symbol}: ${price ? (price > 100 ? price.toLocaleString('pt-BR',{minimumFractionDigits:2}) : price.toFixed(4)) : '-'}`;
if (change !== 0) totalText += ` (${change >= 0 ? '+' : ''}${change.toFixed(2)}%)`;
pfTotalEl.textContent = totalText;
pfTotalEl.style.color = change >= 0 ? '#69f0ae' : '#ff6b6b';
}
const pfDaysEl = document.getElementById('pfDays');
if (pfDaysEl) pfDaysEl.textContent = `Banca: $${bal.toLocaleString('pt-BR', {minimumFractionDigits:2})}`;
// Draw chart
if (data.chart_data && data.chart_data.length) {
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 para ${symbol}.
`;
} 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';
const api = currentAsset === 'btc' ? '/api/backtest/live' : `/api/backtest/${currentAsset}`;
fetch(api, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ days: 1, initial_capital: parseFloat(document.getElementById('cockpitCapital').value) || 10000 })
})
.then(r => r.json())
.then(data => {
if (data.history && data.history.length) {
drawMultiStratCanvas(data.history, data.initial_capital || 10000);
}
})
.catch(e => console.error('Erro multistrat live:', e));
} else {
if (spinner) spinner.style.display = 'block';
const api = currentAsset === 'btc' ? '/api/backtest' : `/api/backtest/${currentAsset}`;
fetch(api, {
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);
}
});
}