1117 lines
48 KiB
JavaScript
1117 lines
48 KiB
JavaScript
/**
|
||
* 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 = `<div style="color:var(--text-secondary);font-size:0.8rem;padding:8px">Carregando ${ASSET_LABELS[currentAsset]}...</div>`;
|
||
// 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 = `
|
||
<span class="status-dot" id="statusDot" style="background:#69f0ae"></span>
|
||
<span id="statusText">${ASSET_LABELS[currentAsset]}: ${priceStr}</span>
|
||
`;
|
||
}
|
||
|
||
// 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 = `
|
||
<div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap">
|
||
<span style="font-size:1.5rem;font-weight:700;color:${decisionColor}">${decio.decision}</span>
|
||
<span style="color:var(--text-secondary)">Confiança: <b style="color:${decisionColor}">${decio.confidence}%</b></span>
|
||
${decio.stop_loss ? `<span style="color:var(--text-secondary)">Stop Loss: <b>${decio.stop_loss}</b></span>` : ''}
|
||
${decio.take_profit ? `<span style="color:var(--text-secondary)">Take Profit: <b>${decio.take_profit}</b></span>` : ''}
|
||
<span style="color:var(--text-secondary)">Signal: <b style="color:${signalColor}">${brief.signal}</b></span>
|
||
<span style="color:var(--text-secondary)">RSI: <b>${brief.rsi || '-'}</b></span>
|
||
${brief.change_24h !== undefined ? `<span style="color:var(--text-secondary)">24h: <b style="color:${brief.change_24h >= 0 ? '#69f0ae' : '#ff6b6b'}">${brief.change_24h >= 0 ? '+' : ''}${brief.change_24h}%</b></span>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 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 = `
|
||
<span class="status-dot" id="statusDot"></span>
|
||
<span id="statusText">Sistema online</span>
|
||
`;
|
||
}
|
||
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 => `
|
||
<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/${currentAsset}`)
|
||
.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">${n.emoji || AGENT_EMOJIS[n.id] || '🤖'}</div>
|
||
<div class="visual-node-label">${n.label}</div>
|
||
<div class="visual-node-status">${n.status || 'idle'}</div>
|
||
${n.last_run ? `<div style="font-size:0.6rem;color:var(--text-secondary)">${n.last_run.slice(11,19)}</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?.toLowerCase() || ''}">${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();
|
||
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 = `<div style="color:var(--text-secondary);font-size:0.8rem;padding:8px">Nenhum trade para ${symbol}.</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';
|
||
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);
|
||
}
|
||
});
|
||
} |