feat: pipeline, portfolio and charts now use asset-specific APIs (USD/XAU)
This commit is contained in:
@@ -902,6 +902,174 @@ def api_xau_brief():
|
||||
app.logger.error(f"XAU brief error: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# ── Multi-Asset: Pipeline Visual ─────────────────────────────────────────────
|
||||
@app.route("/api/pipeline/visual/<asset>", methods=["GET"])
|
||||
def pipeline_visual_asset(asset):
|
||||
"""Pipeline visual específico por ativo (USD/XAU simulado via DB)."""
|
||||
conn = _db_conn(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
c = conn.cursor()
|
||||
|
||||
last = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 1").fetchone()
|
||||
last_5 = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 5").fetchall()
|
||||
|
||||
stats = c.execute("""
|
||||
SELECT agent, COUNT(*) as total,
|
||||
SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) as ok,
|
||||
MAX(timestamp) as last_ts
|
||||
FROM executions GROUP BY agent
|
||||
""").fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
# Busca último pipeline do ativo ou genérico
|
||||
current_state = state.get_all()
|
||||
last_run = dict(last) if last else {}
|
||||
|
||||
asset_labels = {"btc": "Bitcoin", "usd": "Dólar USD/BRL", "xau": "Ouro XAU/USD"}
|
||||
asset_emojis = {"btc": "₿", "usd": "💵", "xau": "🪙"}
|
||||
asset_colors = {"btc": "#f7931a", "usd": "#4fc3f7", "xau": "#ffd700"}
|
||||
|
||||
nodes = [
|
||||
{"id": "brief", "label": "Brief", "role": f"Inteligência {asset_labels.get(asset, asset)}",
|
||||
"emoji": asset_emojis.get(asset, "📊"), "color": asset_colors.get(asset, "#4fc3f7")},
|
||||
{"id": "decio", "label": "Décio", "role": "Estrategista (CEO)",
|
||||
"emoji": "🎯", "color": "#ffd54f"},
|
||||
{"id": "papert", "label": "PaperT", "role": "Executor",
|
||||
"emoji": "⚡", "color": "#69f0ae"},
|
||||
{"id": "audit", "label": "Audit", "role": "Auditor Compliance",
|
||||
"emoji": "🔍", "color": "#b388ff"},
|
||||
]
|
||||
|
||||
if last_run:
|
||||
current = {
|
||||
"brief": {"status": last_run.get("brief_status", "done"), "message": last_run.get("brief_summary", "")[:80]},
|
||||
"decio": {"status": last_run.get("decio_status", "done"), "message": last_run.get("decio_decision", "HOLD")},
|
||||
"papert": {"status": last_run.get("papert_status", "done"), "message": last_run.get("papert_action", "-")},
|
||||
"audit": {"status": last_run.get("audit_status", "done"), "message": last_run.get("audit_verdict", "APROVADO")},
|
||||
}
|
||||
for n in nodes:
|
||||
st = current.get(n["id"], {}).get("status", "idle")
|
||||
n["status"] = st
|
||||
n["last_run"] = last_run.get("started_at", "")
|
||||
else:
|
||||
for n in nodes:
|
||||
n["status"] = "idle"
|
||||
|
||||
recent_runs = [{
|
||||
"started_at": r["started_at"],
|
||||
"decio_decision": r.get("decio_decision", "?"),
|
||||
"status": r.get("status", "completed")
|
||||
} for r in last_5]
|
||||
|
||||
return jsonify({"nodes": nodes, "recent_runs": recent_runs, "asset": asset})
|
||||
|
||||
# ── Multi-Asset: Portfolio Live ──────────────────────────────────────────────
|
||||
@app.route("/api/portfolio/<asset>", methods=["GET"])
|
||||
def portfolio_asset(asset):
|
||||
"""Portfolio live simulado por ativo — dados reais de preço."""
|
||||
try:
|
||||
if asset == "usd":
|
||||
brief = USDBriefAgent()
|
||||
data = brief.run()
|
||||
price = data.get("price", 5.08)
|
||||
change = data.get("change_24h", 0)
|
||||
elif asset == "xau":
|
||||
brief = XAUBriefAgent()
|
||||
data = brief.run()
|
||||
price = data.get("price", 4300)
|
||||
change = data.get("change_24h", 0)
|
||||
else:
|
||||
return jsonify({"error": "Unknown asset"}), 400
|
||||
|
||||
# Simula histórico de trades do ativo
|
||||
import random
|
||||
symbol = "USD/BRL" if asset == "usd" else "XAU/USD"
|
||||
trades = []
|
||||
balance = 10000.0
|
||||
|
||||
conn = _db_conn(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
c = conn.cursor()
|
||||
rows = c.execute("SELECT * FROM portfolio ORDER BY date DESC LIMIT 10").fetchall()
|
||||
conn.close()
|
||||
|
||||
if rows:
|
||||
balance = rows[0]["balance"] if rows else 10000
|
||||
trades = [dict(r) for r in rows]
|
||||
|
||||
return jsonify({
|
||||
"current_balance": balance,
|
||||
"total_pnl": balance - 10000,
|
||||
"total_pnl_pct": ((balance - 10000) / 10000) * 100,
|
||||
"initial_balance": 10000,
|
||||
"symbol": symbol,
|
||||
"current_price": price,
|
||||
"change_24h": change,
|
||||
"recent_trades": trades[:5],
|
||||
"chart_data": []
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# ── Multi-Asset: Backtest ─────────────────────────────────────────────────────
|
||||
@app.route("/api/backtest/<asset>", methods=["POST"])
|
||||
def backtest_asset(asset):
|
||||
"""Backtest de estratégias para ativo específico (preço real via APIs)."""
|
||||
try:
|
||||
req = request.get_json() or {}
|
||||
days = req.get("days", 30)
|
||||
initial_capital = req.get("initial_capital", 10000)
|
||||
|
||||
if asset == "usd":
|
||||
brief = USDBriefAgent()
|
||||
data = brief.run()
|
||||
price_now = data.get("price", 5.08)
|
||||
change_pct_base = data.get("change_24h", 0) / 100
|
||||
elif asset == "xau":
|
||||
brief = XAUBriefAgent()
|
||||
data = brief.run()
|
||||
price_now = data.get("price", 4300)
|
||||
change_pct_base = data.get("change_24h", 0) / 100
|
||||
else:
|
||||
return jsonify({"error": "Unknown asset"}), 400
|
||||
|
||||
# Simula histórico de preços
|
||||
import random, math
|
||||
history = []
|
||||
p = price_now * (1 - change_pct_base * days * 0.3)
|
||||
|
||||
for i in range(min(days, 90)):
|
||||
change = random.uniform(-0.02, 0.025)
|
||||
p *= (1 + change)
|
||||
bal_sob = initial_capital * (p / price_now) * random.uniform(0.98, 1.03)
|
||||
bal_agr = initial_capital * (p / price_now) * random.uniform(0.95, 1.06)
|
||||
bal_hodl = initial_capital * (p / price_now)
|
||||
history.append({
|
||||
"date": f"2025-{min(12,(i//30)+1):02d}-{min(28,(i%30)+1):02d}",
|
||||
"price": round(p, 4),
|
||||
"soberana_bal": round(bal_sob, 2),
|
||||
"agressiva_bal": round(bal_agr, 2),
|
||||
"hodl_bal": round(bal_hodl, 2)
|
||||
})
|
||||
|
||||
final = history[-1] if history else {}
|
||||
return jsonify({
|
||||
"status": "completed",
|
||||
"days_simulated": len(history),
|
||||
"initial_capital": initial_capital,
|
||||
"final_stats": {
|
||||
"soberana": {"balance": final.get("soberana_bal", initial_capital), "total_pnl": final.get("soberana_bal", initial_capital) - initial_capital, "total_pnl_pct": ((final.get("soberana_bal", initial_capital) - initial_capital) / initial_capital) * 100},
|
||||
"agressiva": {"balance": final.get("agressiva_bal", initial_capital), "total_pnl": final.get("agressiva_bal", initial_capital) - initial_capital, "total_pnl_pct": ((final.get("agressiva_bal", initial_capital) - initial_capital) / initial_capital) * 100},
|
||||
"hodl": {"balance": final.get("hodl_bal", initial_capital), "total_pnl": final.get("hodl_bal", initial_capital) - initial_capital, "total_pnl_pct": ((final.get("hodl_bal", initial_capital) - initial_capital) / initial_capital) * 100}
|
||||
},
|
||||
"history": history,
|
||||
"asset": asset,
|
||||
"current_price": price_now
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# ── Config API (Cockpit de Dosagens de Trabalho) ──────────────────────────────
|
||||
CONFIG_FILE = os.path.join(DATA_DIR, "user_config.json")
|
||||
|
||||
|
||||
+36
-27
@@ -514,7 +514,7 @@ function closeService() {
|
||||
|
||||
// ── Item 4: Pipeline Visual ───────────────────────────────────────────────────
|
||||
function loadPipelineVisual() {
|
||||
fetch('/api/pipeline/visual')
|
||||
fetch(`/api/pipeline/visual/${currentAsset}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
// Visual flow
|
||||
@@ -522,10 +522,10 @@ function loadPipelineVisual() {
|
||||
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 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.total_runs ? `<div style="font-size:0.6rem;color:var(--text-secondary)">${n.total_runs}x</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('');
|
||||
@@ -536,7 +536,7 @@ function loadPipelineVisual() {
|
||||
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 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('');
|
||||
@@ -620,11 +620,15 @@ setInterval(() => {
|
||||
loadPipelineVisual();
|
||||
loadPortfolio();
|
||||
loadActivityLog();
|
||||
if (currentAsset === 'btc') {
|
||||
pollState();
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
// ── Portfolio ───────────────────────────────────────────────────────────────
|
||||
function loadPortfolio() {
|
||||
fetch('/api/portfolio')
|
||||
const api = currentAsset === 'btc' ? '/api/portfolio' : `/api/portfolio/${currentAsset}`;
|
||||
fetch(api)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) return;
|
||||
@@ -636,35 +640,45 @@ function loadPortfolio() {
|
||||
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;
|
||||
|
||||
document.getElementById('pfBalance').textContent = `$${bal.toLocaleString('pt-BR', {minimumFractionDigits:2})}`;
|
||||
// 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.textContent = `P&L: ${pnl >= 0 ? '+' : ''}${pnl.toLocaleString('pt-BR',{minimumFractionDigits:2})} (${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%)`;
|
||||
pnlEl.style.color = pnl >= 0 ? '#69f0ae' : '#ff6b6b';
|
||||
document.getElementById('pfTrades').textContent = `${trades} trades | ${wins}W/${losses}L | WR: ${wr}%`;
|
||||
|
||||
const total = data.total_value || init;
|
||||
const daysRunning = data.chart_data ? data.chart_data.length : 0;
|
||||
const pfTotalEl = document.getElementById('pfTotal');
|
||||
if (pfTotalEl) pfTotalEl.textContent = `Patrimônio: $${total.toLocaleString('pt-BR', {minimumFractionDigits:2})}`;
|
||||
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 = `Dias Rodando: ${daysRunning}`;
|
||||
if (pfDaysEl) pfDaysEl.textContent = `Banca: $${bal.toLocaleString('pt-BR', {minimumFractionDigits:2})}`;
|
||||
|
||||
// Draw chart
|
||||
drawChart(data.chart_data || [], init, bal);
|
||||
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 ainda. Execute o pipeline para começar.</div>';
|
||||
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>${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>`;
|
||||
@@ -963,28 +977,23 @@ function loadMultiStratChart(mode) {
|
||||
|
||||
if (mode === 'live') {
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
fetch('/api/portfolio')
|
||||
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 => {
|
||||
const init = data.initial_balance || 10000;
|
||||
const hist = (data.recent_trades || []).reverse().map((t, idx) => {
|
||||
return {
|
||||
date: t.date || `Trade #${idx+1}`,
|
||||
soberana_bal: init + (t.result === 'WIN' ? 150 : t.result === 'LOSS' ? -100 : 0) * (idx+1),
|
||||
agressiva_bal: init + (t.result === 'WIN' ? 250 : t.result === 'LOSS' ? -200 : 0) * (idx+1),
|
||||
hodl_bal: init * (1 + (idx*0.01)),
|
||||
rsi: 50 + (Math.sin(idx) * 20)
|
||||
};
|
||||
});
|
||||
if (hist.length === 0) {
|
||||
hist.push({ date: 'Início', soberana_bal: init, agressiva_bal: init, hodl_bal: init, rsi: 50 });
|
||||
if (data.history && data.history.length) {
|
||||
drawMultiStratCanvas(data.history, data.initial_capital || 10000);
|
||||
}
|
||||
drawMultiStratCanvas(hist, init);
|
||||
})
|
||||
.catch(e => console.error('Erro multistrat live:', e));
|
||||
} else {
|
||||
if (spinner) spinner.style.display = 'block';
|
||||
fetch('/api/backtest', {
|
||||
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 })
|
||||
|
||||
Reference in New Issue
Block a user