BrainSteel Fin v1.0 — 3-agent BTC trading pipeline
- Brief: Market Intelligence (Binance data + LLM analysis) - Decio: Strategy decision (BUY/HOLD/SELL) - PaperT: Order executor (Binance API) - Anime-style Flask dashboard - Traefik-ready Docker deployment
This commit is contained in:
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* 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();
|
||||
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);
|
||||
}
|
||||
|
||||
// Refresh all sections periodically
|
||||
setInterval(() => {
|
||||
loadAgentRegistry();
|
||||
loadMetrics();
|
||||
loadServices();
|
||||
loadPipelineVisual();
|
||||
loadPortfolio();
|
||||
}, 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user