🚀 Auto-deploy: Add background automated scheduler, Kelly allocation, multi-portfolio simulations, and UI improvements

This commit is contained in:
2026-05-20 11:23:56 +00:00
parent 738490ea4f
commit 3921ce9359
9 changed files with 944 additions and 127 deletions
Binary file not shown.
Binary file not shown.
+50 -2
View File
@@ -10,6 +10,52 @@ class AuditAgent:
def __init__(self): def __init__(self):
self.name = "Audit" self.name = "Audit"
def send_notification(self, audit_result):
"""Envia notificação ativa para Telegram/Discord ou grava no log de governança."""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
action = audit_result.get("decio_decision", "HOLD")
score = audit_result.get("compliance_score", 0)
status = audit_result.get("status", "unknown").upper()
pnl_pct = audit_result.get("portfolio_pnl_pct", 0)
bal = audit_result.get("portfolio_balance", 0)
msg = (
f"🔔 [BrainSteel Fin Governança] — {now}\n"
f"Decisão do Ciclo: {action}\n"
f"Veredito do Audit: {status} (Score: {score}%)\n"
f"Saldo Virtual: ${bal:,.2f} | PnL Total: {pnl_pct:+.1f}%\n"
f"Notas de Qualidade:\n" + "\n".join([f"{n}" for n in audit_result.get("quality_notes", [])])
)
# 1. Envia Telegram se configurado
tg_token = os.getenv("TELEGRAM_BOT_TOKEN", "")
tg_chat = os.getenv("TELEGRAM_CHAT_ID", "")
if tg_token and tg_chat:
try:
requests.post(
f"https://api.telegram.org/bot{tg_token}/sendMessage",
json={"chat_id": tg_chat, "text": msg},
timeout=10
)
except: pass
# 2. Envia Discord Webhook se configurado
discord_url = os.getenv("DISCORD_WEBHOOK_URL", "")
if discord_url:
try:
requests.post(discord_url, json={"content": msg}, timeout=10)
except: pass
# 3. Sempre grava no log de notificações de governança
for path in ["/app/logs/notifications.log", "/data/notifications.log"]:
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "a", encoding="utf-8") as f:
f.write(msg + "\n" + "-"*40 + "\n")
except: pass
return msg
def audit_pipeline(self, brief_data, decio_data, papert_data): def audit_pipeline(self, brief_data, decio_data, papert_data):
checks = [] checks = []
@@ -105,7 +151,7 @@ class AuditAgent:
if decio_action == "HOLD" and brief_data.get("confidence", 0) < 75: if decio_action == "HOLD" and brief_data.get("confidence", 0) < 75:
quality_notes.append("HOLD correto — confiança baixa preservou capital") quality_notes.append("HOLD correto — confiança baixa preservou capital")
return { res = {
"audit_id": f"AUD-{datetime.now().strftime('%Y%m%d%H%M%S')}", "audit_id": f"AUD-{datetime.now().strftime('%Y%m%d%H%M%S')}",
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"checks": checks, "checks": checks,
@@ -117,10 +163,12 @@ class AuditAgent:
"brief_summary": brief_data.get("summary", "")[:100], "brief_summary": brief_data.get("summary", "")[:100],
"decio_decision": decio_action, "decio_decision": decio_action,
"decio_confidence": decio_conf, "decio_confidence": decio_conf,
"portfolio_balance": portfolio.get("current_balance", 0), "portfolio_balance": portfolio.get("current_balance") or portfolio.get("balance", 0),
"portfolio_pnl": portfolio.get("total_pnl", 0), "portfolio_pnl": portfolio.get("total_pnl", 0),
"portfolio_pnl_pct": portfolio.get("total_pnl_pct", 0), "portfolio_pnl_pct": portfolio.get("total_pnl_pct", 0),
} }
self.send_notification(res)
return res
def generate_llm_report(self, brief_data, decio_data): def generate_llm_report(self, brief_data, decio_data):
if not os.getenv("PAPERT_API_KEY") and not os.getenv("OPENROUTER_API_KEY", ""): if not os.getenv("PAPERT_API_KEY") and not os.getenv("OPENROUTER_API_KEY", ""):
+26 -10
View File
@@ -1,7 +1,7 @@
""" """
BrainSteel Fin — Decio Agent (v2) BrainSteel Fin — Decio Agent (v2)
Chief Strategist & Risk Officer Chief Strategist & Risk Officer
5-Pillar decision engine: Confiança < 75% → HOLD. BUY/SELL → SL 2% / TP 5%. 5-Pillar decision engine: Confiança < 70% → HOLD. BUY/SELL → SL 2% / TP 5%.
""" """
import os, json, requests import os, json, requests
from datetime import datetime from datetime import datetime
@@ -14,7 +14,7 @@ class DecioAgent:
self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions"
self.model = "deepseek/deepseek-v4-flash:free" self.model = "deepseek/deepseek-v4-flash:free"
# ── CORE RULE: confidence < 75% → always HOLD ───────────────────────── # ── CORE RULE: confidence < 70% → always HOLD ─────────────────────────
def _risk_filter(self, confidence, signal): def _risk_filter(self, confidence, signal):
"""Décio is a mathematician: no confidence ≥70%, no operation.""" """Décio is a mathematician: no confidence ≥70%, no operation."""
return confidence < 70 or signal not in ("ALTA", "BAIXA") return confidence < 70 or signal not in ("ALTA", "BAIXA")
@@ -62,9 +62,9 @@ Analítico, frio, matemático. Preservação de capital > lucro rápido.
Bullish signals: {self.brief_data.get('bullish_signals',0)} | Bearish: {self.brief_data.get('bearish_signals',0)} Bullish signals: {self.brief_data.get('bullish_signals',0)} | Bearish: {self.brief_data.get('bearish_signals',0)}
REGRAS OPERACIONAIS: REGRAS OPERACIONAIS:
Confiança < 75% → decisão SEMPRE HOLD (nunca opera) Confiança < 70% → decisão SEMPRE HOLD (nunca opera)
Confiança ≥ 75% + sinal=ALTA → BUY Confiança ≥ 70% + sinal=ALTA → BUY
Confiança ≥ 75% + sinal=BAIXA → SELL Confiança ≥ 70% + sinal=BAIXA → SELL
BUY/SELL → Stop Loss = preço × 0.98 (-2%) | Take Profit = preço × 1.05 (+5%) BUY/SELL → Stop Loss = preço × 0.98 (-2%) | Take Profit = preço × 1.05 (+5%)
HOLD → stop_loss=0, take_profit=0 HOLD → stop_loss=0, take_profit=0
@@ -102,7 +102,7 @@ Analítico, frio, matemático. Preservação de capital > lucro rápido.
# Apply risk filter # Apply risk filter
if self._risk_filter(data.get("confidence", 0), signal): if self._risk_filter(data.get("confidence", 0), signal):
data["decision"] = "HOLD" data["decision"] = "HOLD"
data["justification"] = "Confiança < 75% — preservando capital" data["justification"] = "Confiança < 70% — preservando capital"
data["stop_loss"] = 0 data["stop_loss"] = 0
data["take_profit"] = 0 data["take_profit"] = 0
return data return data
@@ -131,7 +131,7 @@ Analítico, frio, matemático. Preservação de capital > lucro rápido.
elif self._risk_filter(confidence, signal): elif self._risk_filter(confidence, signal):
decision = "HOLD" decision = "HOLD"
sl, tp = 0, 0 sl, tp = 0, 0
justification = f"Confiança {confidence}% < 75% — preservando capital" justification = f"Confiança {confidence}% < 70% — preservando capital"
elif signal == "ALTA": elif signal == "ALTA":
decision = "BUY" decision = "BUY"
sl, tp = self._calc_levels(price, "BUY") sl, tp = self._calc_levels(price, "BUY")
@@ -154,18 +154,34 @@ Analítico, frio, matemático. Preservação de capital > lucro rápido.
"signal": signal "signal": signal
} }
# ── Dynamic Allocation (Kelly Scale) ─────────────────────────────────────
def _calc_allocation(self, confidence, decision):
"""Calcula a alocação de banca dinâmica baseada na confiança do sinal."""
if decision not in ("BUY", "SELL") or confidence < 70:
return 0
if confidence >= 90:
return 35 # Mão forte (alta convicção)
elif confidence >= 80:
return 25 # Mão padrão
else:
return 15 # Mão leve (cautela, 70-79%)
# ── Main run ─────────────────────────────────────────────────────────── # ── Main run ───────────────────────────────────────────────────────────
def run(self): def run(self):
llm_result = self._decide_llm() llm_result = self._decide_llm()
result = llm_result if llm_result else self._local_decision() result = llm_result if llm_result else self._local_decision()
price = self.brief_data.get("price", 0) price = self.brief_data.get("price", 0)
conf = result.get("confidence", 50)
action = result["decision"]
amount_pct = self._calc_allocation(conf, action)
output = { output = {
"action": result["decision"], "action": action,
"amount_pct": 25 if result["decision"] in ("BUY", "SELL") else 0, "amount_pct": amount_pct,
"stop_loss": result.get("stop_loss", 0), "stop_loss": result.get("stop_loss", 0),
"take_profit": result.get("take_profit", 0), "take_profit": result.get("take_profit", 0),
"confidence": result.get("confidence", 50), "confidence": conf,
"justification": result.get("justification", "")[:200], "justification": result.get("justification", "")[:200],
"signal": result.get("signal", self.brief_data.get("signal", "NEUTRO")), "signal": result.get("signal", self.brief_data.get("signal", "NEUTRO")),
"price": price, "price": price,
+83 -46
View File
@@ -13,17 +13,22 @@ DATA_DIR = Path("/app/data")
PORTFOLIO_DB = DATA_DIR / "portfolio.db" PORTFOLIO_DB = DATA_DIR / "portfolio.db"
class Portfolio: class Portfolio:
"""Portfolio simulation — tracking de saldo, trades e P&L.""" """Portfolio simulation — tracking de saldo, trades e P&L (Multi-Estratégia)."""
def __init__(self): def __init__(self, strategy="soberana"):
self.strategy = strategy
self.suf = "" if strategy == "soberana" else f"_{strategy}"
self.tbl_portfolio = f"portfolio{self.suf}"
self.tbl_trades = f"trades{self.suf}"
self.tbl_snapshot = f"daily_snapshot{self.suf}"
self._init_db() self._init_db()
def _init_db(self): def _init_db(self):
DATA_DIR.mkdir(parents=True, exist_ok=True) DATA_DIR.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(PORTFOLIO_DB)) conn = sqlite3.connect(str(PORTFOLIO_DB))
c = conn.cursor() c = conn.cursor()
c.execute(""" c.execute(f"""
CREATE TABLE IF NOT EXISTS portfolio ( CREATE TABLE IF NOT EXISTS {self.tbl_portfolio} (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT, date TEXT,
balance REAL, balance REAL,
@@ -36,8 +41,8 @@ class Portfolio:
UNIQUE(date) UNIQUE(date)
) )
""") """)
c.execute(""" c.execute(f"""
CREATE TABLE IF NOT EXISTS trades ( CREATE TABLE IF NOT EXISTS {self.tbl_trades} (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT, date TEXT,
action TEXT, action TEXT,
@@ -55,8 +60,8 @@ class Portfolio:
created_at TEXT created_at TEXT
) )
""") """)
c.execute(""" c.execute(f"""
CREATE TABLE IF NOT EXISTS daily_snapshot ( CREATE TABLE IF NOT EXISTS {self.tbl_snapshot} (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT UNIQUE, date TEXT UNIQUE,
balance REAL, balance REAL,
@@ -72,13 +77,13 @@ class Portfolio:
""") """)
# Ensure today's row exists # Ensure today's row exists
today = date.today().isoformat() today = date.today().isoformat()
row = c.execute("SELECT balance FROM portfolio WHERE date=?", (today,)).fetchone() row = c.execute(f"SELECT balance FROM {self.tbl_portfolio} WHERE date=?", (today,)).fetchone()
if not row: if not row:
# Seed from last known balance or initial # Seed from last known balance or initial
last = c.execute("SELECT balance, btc_held FROM portfolio ORDER BY date DESC LIMIT 1").fetchone() last = c.execute(f"SELECT balance, btc_held FROM {self.tbl_portfolio} ORDER BY date DESC LIMIT 1").fetchone()
balance = last[0] if last else INITIAL_BALANCE balance = last[0] if last else INITIAL_BALANCE
btc_held = last[1] if last else 0.0 btc_held = last[1] if last else 0.0
c.execute("INSERT INTO portfolio (date,balance,btc_held,created_at) VALUES (?,?,?,?)", c.execute(f"INSERT INTO {self.tbl_portfolio} (date,balance,btc_held,created_at) VALUES (?,?,?,?)",
(today, balance, btc_held, datetime.now().isoformat())) (today, balance, btc_held, datetime.now().isoformat()))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -88,7 +93,7 @@ class Portfolio:
conn = sqlite3.connect(str(PORTFOLIO_DB)) conn = sqlite3.connect(str(PORTFOLIO_DB))
c = conn.cursor() c = conn.cursor()
today = date.today().isoformat() today = date.today().isoformat()
row = c.execute("SELECT balance, btc_held FROM portfolio WHERE date=?", (today,)).fetchone() row = c.execute(f"SELECT balance, btc_held FROM {self.tbl_portfolio} WHERE date=?", (today,)).fetchone()
conn.close() conn.close()
return {"balance": row[0] if row else INITIAL_BALANCE, "btc_held": row[1] if row else 0.0} return {"balance": row[0] if row else INITIAL_BALANCE, "btc_held": row[1] if row else 0.0}
@@ -127,7 +132,7 @@ class Portfolio:
new_btc_held = btc_held + btc_amount new_btc_held = btc_held + btc_amount
amount_usdt = allocation amount_usdt = allocation
exit_reason = "entry" exit_reason = "entry"
result_str = f"🟢 BUY | ${allocation:.2f} alocado | {btc_amount:.6f} BTC @ ${price:,.0f} | Fee: ${fee:.2f}" result_str = f"🟢 BUY ({self.strategy}) | ${allocation:.2f} alocado | {btc_amount:.6f} BTC @ ${price:,.0f} | Fee: ${fee:.2f}"
elif action == "SELL" and btc_held > 0: elif action == "SELL" and btc_held > 0:
# Sell portion of BTC held # Sell portion of BTC held
@@ -150,19 +155,19 @@ class Portfolio:
elif take_profit and price >= take_profit: elif take_profit and price >= take_profit:
exit_reason = "take_profit_hit" exit_reason = "take_profit_hit"
else: else:
exit_reason = "decio_signal" exit_reason = f"signal_{self.strategy}"
result_str = f"🔴 SELL | {btc_to_sell:.6f} BTC @ ${price:,.0f} | Net: ${net_proceeds:.2f} | Fee: ${fee:.2f}" result_str = f"🔴 SELL ({self.strategy}) | {btc_to_sell:.6f} BTC @ ${price:,.0f} | Net: ${net_proceeds:.2f} | Fee: ${fee:.2f}"
else: else:
new_balance = balance new_balance = balance
new_btc_held = btc_held new_btc_held = btc_held
result_str = f"⚪ SELL ignorado | BTC held: {btc_held:.6f}" result_str = f"⚪ SELL ignorado ({self.strategy}) | BTC held: {btc_held:.6f}"
# Record trade if it happened # Record trade if it happened
if action in ("BUY", "SELL") and (action == "BUY" or btc_held > 0): if action in ("BUY", "SELL") and (action == "BUY" or btc_held > 0):
is_win = pnl > 0 if action == "SELL" and btc_held > 0 else None is_win = pnl > 0 if action == "SELL" and btc_held > 0 else None
c.execute(""" c.execute(f"""
INSERT INTO trades (date,action,entry_price,amount_usdt,btc_amount,exit_price,pnl_usdt,pnl_pct,stop_loss,take_profit,fee_usdt,exit_reason,result,created_at) INSERT INTO {self.tbl_trades} (date,action,entry_price,amount_usdt,btc_amount,exit_price,pnl_usdt,pnl_pct,stop_loss,take_profit,fee_usdt,exit_reason,result,created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", (today, action, price, amount_usdt, btc_amount, """, (today, action, price, amount_usdt, btc_amount,
price if action == "SELL" else 0, price if action == "SELL" else 0,
@@ -172,12 +177,12 @@ class Portfolio:
now)) now))
# Update daily stats # Update daily stats
c.execute("UPDATE portfolio SET balance=?, btc_held=?, total_trades=total_trades+1 WHERE date=?", c.execute(f"UPDATE {self.tbl_portfolio} SET balance=?, btc_held=?, total_trades=total_trades+1 WHERE date=?",
(new_balance, new_btc_held, today)) (new_balance, new_btc_held, today))
if is_win is True: if is_win is True:
c.execute("UPDATE portfolio SET wins=wins+1 WHERE date=?", (today,)) c.execute(f"UPDATE {self.tbl_portfolio} SET wins=wins+1 WHERE date=?", (today,))
elif is_win is False: elif is_win is False:
c.execute("UPDATE portfolio SET losses=losses+1 WHERE date=?", (today,)) c.execute(f"UPDATE {self.tbl_portfolio} SET losses=losses+1 WHERE date=?", (today,))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -196,7 +201,7 @@ class Portfolio:
c = conn.cursor() c = conn.cursor()
# All trades # All trades
trades = c.execute("SELECT action,pnl_usdt,pnl_pct,result,date FROM trades ORDER BY created_at DESC").fetchall() trades = c.execute(f"SELECT action,pnl_usdt,pnl_pct,result,date FROM {self.tbl_trades} ORDER BY created_at DESC").fetchall()
total_trades = len(trades) total_trades = len(trades)
wins = sum(1 for t in trades if t[3] == "WIN") wins = sum(1 for t in trades if t[3] == "WIN")
@@ -204,7 +209,7 @@ class Portfolio:
# Current balance # Current balance
today = date.today().isoformat() today = date.today().isoformat()
cur = c.execute("SELECT balance, btc_held FROM portfolio WHERE date=?", (today,)).fetchone() cur = c.execute(f"SELECT balance, btc_held FROM {self.tbl_portfolio} WHERE date=?", (today,)).fetchone()
current_balance = cur[0] if cur else INITIAL_BALANCE current_balance = cur[0] if cur else INITIAL_BALANCE
btc_held = cur[1] if cur else 0.0 btc_held = cur[1] if cur else 0.0
@@ -217,7 +222,7 @@ class Portfolio:
total_value = current_balance + (btc_held * btc_price) total_value = current_balance + (btc_held * btc_price)
# Daily history for chart # Daily history for chart
days = c.execute("SELECT date,balance FROM portfolio ORDER BY date ASC").fetchall() days = c.execute(f"SELECT date,balance FROM {self.tbl_portfolio} ORDER BY date ASC").fetchall()
conn.close() conn.close()
@@ -230,6 +235,8 @@ class Portfolio:
peak = INITIAL_BALANCE peak = INITIAL_BALANCE
max_drawdown = 0 max_drawdown = 0
for _, bal in days: for _, bal in days:
if bal is None:
continue
if bal > peak: if bal > peak:
peak = bal peak = bal
dd = (peak - bal) / peak * 100 dd = (peak - bal) / peak * 100
@@ -261,10 +268,10 @@ class Portfolio:
stats = self.get_stats() stats = self.get_stats()
# Get closed trades today # Get closed trades today
today_trades = c.execute("SELECT COUNT(*),SUM(pnl_usdt) FROM trades WHERE date=? AND result IN ('WIN','LOSS')", (today,)).fetchone() today_trades = c.execute(f"SELECT COUNT(*),SUM(pnl_usdt) FROM {self.tbl_trades} WHERE date=? AND result IN ('WIN','LOSS')", (today,)).fetchone()
c.execute(""" c.execute(f"""
INSERT OR REPLACE INTO daily_snapshot INSERT OR REPLACE INTO {self.tbl_snapshot}
(date,balance,btc_price,btc_held,open_trades,closed_trades,day_pnl,total_pnl,win_rate,created_at) (date,balance,btc_price,btc_held,open_trades,closed_trades,day_pnl,total_pnl,win_rate,created_at)
VALUES (?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?)
""", ( """, (
@@ -284,12 +291,12 @@ class Portfolio:
class PapertAgent: class PapertAgent:
"""Agent executor que também atualiza portfolio simulado.""" """Agent executor que também atualiza portfolio simulado (Multi-Estratégia)."""
def __init__(self, decio_data): def __init__(self, decio_data):
self.name = "PaperT" self.name = "PaperT"
self.decio_data = decio_data self.decio_data = decio_data
self.portfolio = Portfolio() self.portfolio = Portfolio(strategy="soberana")
self.openrouter_key = os.getenv("PAPERT_API_KEY", os.getenv("OPENROUTER_API_KEY", "")) self.openrouter_key = os.getenv("PAPERT_API_KEY", os.getenv("OPENROUTER_API_KEY", ""))
self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions"
self.model = "deepseek/deepseek-v4-flash:free" self.model = "deepseek/deepseek-v4-flash:free"
@@ -303,10 +310,10 @@ class PapertAgent:
break break
return True, action return True, action
def _format_log_entry(self, action, entry_price, stop_loss, take_profit): def _format_log_entry(self, action, entry_price, stop_loss, take_profit, strategy="Soberana"):
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
fee = round(entry_price * 0.001, 2) fee = round(entry_price * 0.001, 2)
entry = f"[{now}] BTC/USDT | {action} | Entry: ${entry_price:,.2f} | Fee: ${fee:.2f} | SL: ${stop_loss:,.2f} | TP: ${take_profit:,.2f}" entry = f"[{now}] BTC/USDT ({strategy}) | {action} | Entry: ${entry_price:,.2f} | Fee: ${fee:.2f} | SL: ${stop_loss:,.2f} | TP: ${take_profit:,.2f}"
return entry return entry
def _write_log(self, log_line): def _write_log(self, log_line):
@@ -335,23 +342,39 @@ class PapertAgent:
take_profit = self.decio_data.get("take_profit", 0) or 0 take_profit = self.decio_data.get("take_profit", 0) or 0
amount_pct = self.decio_data.get("amount_pct", 0) or 25 amount_pct = self.decio_data.get("amount_pct", 0) or 25
# Execute portfolio trade # 1. Execute Carteira A (Soberana - Padrão)
portfolio_result = self.portfolio._execute_trade(action, entry_price, amount_pct, stop_loss, take_profit) portfolio_result = self.portfolio._execute_trade(action, entry_price, amount_pct, stop_loss, take_profit)
log_entry = self._format_log_entry(action, entry_price, stop_loss, take_profit, "Soberana")
# Log entry
log_entry = self._format_log_entry(action, entry_price, stop_loss, take_profit)
logged = self._write_log(log_entry) logged = self._write_log(log_entry)
try: self.portfolio.snapshot()
# Save daily snapshot except: pass
try:
self.portfolio.snapshot()
except:
pass
# Build result
stats = self.portfolio.get_stats() stats = self.portfolio.get_stats()
fee = round(entry_price * 0.001, 2)
# 2. Execute Carteira B (Agressiva - ignora trava de 70% e opera sinal primário do Brief)
brief_signal = self.decio_data.get("signal", "NEUTRO")
agr_action = "BUY" if brief_signal == "ALTA" else "SELL" if brief_signal == "BAIXA" else "HOLD"
pf_agr = Portfolio(strategy="agressiva")
sl_agr = round(entry_price * 0.98, 2) if agr_action == "BUY" else round(entry_price * 1.02, 2) if agr_action == "SELL" else 0
tp_agr = round(entry_price * 1.05, 2) if agr_action == "BUY" else round(entry_price * 0.95, 2) if agr_action == "SELL" else 0
pf_agr._execute_trade(agr_action, entry_price, 25, sl_agr, tp_agr)
log_agr = self._format_log_entry(agr_action, entry_price, sl_agr, tp_agr, "Agressiva")
self._write_log(log_agr)
try: pf_agr.snapshot()
except: pass
stats_agr = pf_agr.get_stats()
# 3. Execute Carteira C (HODL Benchmark)
pf_hodl = Portfolio(strategy="hodl")
cur_hodl = pf_hodl.balance
if cur_hodl["btc_held"] == 0 and cur_hodl["balance"] > 10:
pf_hodl._execute_trade("BUY", entry_price, 100, 0, 0)
log_hodl = self._format_log_entry("BUY" if cur_hodl["btc_held"] == 0 else "HOLD", entry_price, 0, 0, "HODL")
self._write_log(log_hodl)
try: pf_hodl.snapshot()
except: pass
stats_hodl = pf_hodl.get_stats()
fee = round(entry_price * 0.001, 2)
result_str = f"[EXEC] {datetime.now().strftime('%Y-%m-%dT%H:%M:%S')} | {action} | ${entry_price:,.0f}" result_str = f"[EXEC] {datetime.now().strftime('%Y-%m-%dT%H:%M:%S')} | {action} | ${entry_price:,.0f}"
if action in ("BUY", "SELL"): if action in ("BUY", "SELL"):
result_str += f" | Bal: ${stats['current_balance']:.2f} | P&L total: ${stats['total_pnl']:+.2f} ({stats['total_pnl_pct']:+.1f}%)" result_str += f" | Bal: ${stats['current_balance']:.2f} | P&L total: ${stats['total_pnl']:+.2f} ({stats['total_pnl_pct']:+.1f}%)"
@@ -367,7 +390,7 @@ class PapertAgent:
"fee_usdt": fee, "fee_usdt": fee,
"log_appended": logged, "log_appended": logged,
"result": result_str, "result": result_str,
# Portfolio info for dashboard # Portfolio info for dashboard (Enriched with Multi-Strategy)
"portfolio": { "portfolio": {
"balance": stats["current_balance"], "balance": stats["current_balance"],
"total_value": stats["total_value"], "total_value": stats["total_value"],
@@ -378,7 +401,21 @@ class PapertAgent:
"losses": stats["losses"], "losses": stats["losses"],
"win_rate": stats["win_rate"], "win_rate": stats["win_rate"],
"btc_held": stats["btc_held"], "btc_held": stats["btc_held"],
"btc_price": stats["btc_price"] "btc_price": stats["btc_price"],
# Sub-carteiras comparativas
"agressiva": {
"balance": stats_agr["current_balance"],
"total_value": stats_agr["total_value"],
"total_pnl": stats_agr["total_pnl"],
"total_pnl_pct": stats_agr["total_pnl_pct"],
"win_rate": stats_agr["win_rate"]
},
"hodl": {
"balance": stats_hodl["current_balance"],
"total_value": stats_hodl["total_value"],
"total_pnl": stats_hodl["total_pnl"],
"total_pnl_pct": stats_hodl["total_pnl_pct"]
}
}, },
"agent": "PaperT", "agent": "PaperT",
"timestamp": datetime.now().isoformat() "timestamp": datetime.now().isoformat()
+338 -59
View File
@@ -20,6 +20,27 @@ from agents.papert import PapertAgent
from agents.audit import AuditAgent from agents.audit import AuditAgent
app = Flask(__name__) app = Flask(__name__)
# ── SQLite helpers with WAL mode and error resilience ───────────────────────
def _db_conn(db_path):
"""Get a WAL-mode connection with busy timeout."""
try:
conn = sqlite3.connect(db_path, timeout=30)
conn.execute("PRAGMA journal_mode=DELETE")
conn.execute("PRAGMA busy_timeout=30000")
conn.execute("PRAGMA synchronous=NORMAL")
return conn
except Exception as e:
app.logger.error(f"DB connect error ({db_path}): {e}")
raise
def _safe_commit(conn, label=""):
"""Commit with error handling."""
try:
conn.commit()
except Exception as e:
app.logger.error(f"DB commit error {label}: {e}")
app.secret_key = os.getenv("SECRET_KEY", "brainsteel-fin-secret-2025") app.secret_key = os.getenv("SECRET_KEY", "brainsteel-fin-secret-2025")
# Paths # Paths
@@ -34,7 +55,7 @@ AGENTS_DB = os.path.join(DATA_DIR, "agents.db")
# ── Databases ───────────────────────────────────────────────────────────────── # ── Databases ─────────────────────────────────────────────────────────────────
def init_db(): def init_db():
conn = sqlite3.connect(DB_PATH) conn = _db_conn(DB_PATH)
c = conn.cursor() c = conn.cursor()
c.execute(""" c.execute("""
CREATE TABLE IF NOT EXISTS executions ( CREATE TABLE IF NOT EXISTS executions (
@@ -70,12 +91,12 @@ def init_db():
cost_usd REAL cost_usd REAL
) )
""") """)
conn.commit() _safe_commit(conn)
conn.close() conn.close()
def init_agents_db(): def init_agents_db():
"""Agent registry — onboarding + state.""" """Agent registry — onboarding + state."""
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
c.execute(""" c.execute("""
CREATE TABLE IF NOT EXISTS agents ( CREATE TABLE IF NOT EXISTS agents (
@@ -117,12 +138,12 @@ def init_agents_db():
metadata TEXT metadata TEXT
) )
""") """)
conn.commit() _safe_commit(conn)
conn.close() conn.close()
def seed_agents(): def seed_agents():
"""Seed default agents on first run.""" """Seed default agents on first run."""
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
# Check if already seeded # Check if already seeded
row = c.execute("SELECT COUNT(*) FROM agents").fetchone()[0] row = c.execute("SELECT COUNT(*) FROM agents").fetchone()[0]
@@ -145,7 +166,7 @@ def seed_agents():
for a in defaults: for a in defaults:
c.execute("INSERT INTO agents (id,name,role,intent,model,provider,status,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)", c.execute("INSERT INTO agents (id,name,role,intent,model,provider,status,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)",
(*a, now, now)) (*a, now, now))
conn.commit() _safe_commit(conn)
conn.close() conn.close()
init_db() init_db()
@@ -197,21 +218,21 @@ state = AgentState()
# ── Logging ─────────────────────────────────────────────────────────────────── # ── Logging ───────────────────────────────────────────────────────────────────
def log_execution(agent, action, result, status="success"): def log_execution(agent, action, result, status="success"):
conn = sqlite3.connect(DB_PATH) conn = _db_conn(DB_PATH)
c = conn.cursor() c = conn.cursor()
c.execute("INSERT INTO executions (timestamp, agent, action, result, status) VALUES (?, ?, ?, ?, ?)", c.execute("INSERT INTO executions (timestamp, agent, action, result, status) VALUES (?, ?, ?, ?, ?)",
(datetime.now().isoformat(), agent, action, result, status)) (datetime.now().isoformat(), agent, action, result, status))
conn.commit() _safe_commit(conn)
conn.close() conn.close()
# Also log to agent_logs # Also log to agent_logs
_log_agent_event(agent, "execution", result) _log_agent_event(agent, "execution", result)
def _log_agent_event(agent_id, event, result, duration_ms=0): def _log_agent_event(agent_id, event, result, duration_ms=0):
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
c.execute("INSERT INTO agent_logs (agent_id, timestamp, event, duration_ms, result) VALUES (?, ?, ?, ?, ?)", c.execute("INSERT INTO agent_logs (agent_id, timestamp, event, duration_ms, result) VALUES (?, ?, ?, ?, ?)",
(agent_id, datetime.now().isoformat(), event, duration_ms, result)) (agent_id, datetime.now().isoformat(), event, duration_ms, result))
conn.commit() _safe_commit(conn)
conn.close() conn.close()
# ── Pipeline Execution ──────────────────────────────────────────────────────── # ── Pipeline Execution ────────────────────────────────────────────────────────
@@ -233,7 +254,6 @@ def run_pipeline():
brief_data = brief.run() brief_data = brief.run()
t1 = datetime.now() t1 = datetime.now()
state.update("brief", "done", brief_data.get("summary", "")[:80]) state.update("brief", "done", brief_data.get("summary", "")[:80])
# Also persist brief key metrics to state for /api/state
brief_summary = brief_data.get("summary", "") brief_summary = brief_data.get("summary", "")
state.brief.update({ state.brief.update({
"price": brief_data.get("price"), "price": brief_data.get("price"),
@@ -255,8 +275,12 @@ def run_pipeline():
"macd_histogram": brief_data.get("macd_histogram"), "macd_histogram": brief_data.get("macd_histogram"),
"btc_dominance": brief_data.get("btc_dominance"), "btc_dominance": brief_data.get("btc_dominance"),
}) })
log_execution("brief", "market_analysis", brief_summary, "success") # RESILIENT: Skip DB write if fails
_update_agent_stats("brief", t1 - t0, True) try:
log_execution("brief", "market_analysis", brief_summary, "success")
_update_agent_stats("brief", t1 - t0, True)
except Exception as dbg:
app.logger.warning(f"DB write skipped (non-fatal): {dbg}")
# ── Step 2: Decio ── # ── Step 2: Decio ──
t0 = datetime.now() t0 = datetime.now()
@@ -272,8 +296,11 @@ def run_pipeline():
"take_profit": decio_data.get("take_profit"), "take_profit": decio_data.get("take_profit"),
"justification": decio_data.get("justification", "")[:100], "justification": decio_data.get("justification", "")[:100],
}) })
log_execution("decio", "strategy_decision", decio_data.get("decision", "HOLD"), "success") try:
_update_agent_stats("decio", t1 - t0, True) log_execution("decio", "strategy_decision", decio_data.get("decision", "HOLD"), "success")
_update_agent_stats("decio", t1 - t0, True)
except Exception as dbg:
app.logger.warning(f"DB write skipped (non-fatal): {dbg}")
# ── Step 3: PaperT ── # ── Step 3: PaperT ──
t0 = datetime.now() t0 = datetime.now()
@@ -282,8 +309,11 @@ def run_pipeline():
papert_data = papert.run() papert_data = papert.run()
t1 = datetime.now() t1 = datetime.now()
state.update("papert", "done", papert_data.get("result", "")[:80]) state.update("papert", "done", papert_data.get("result", "")[:80])
log_execution("papert", "order_execution", papert_data.get("result", ""), "success") try:
_update_agent_stats("papert", t1 - t0, True) log_execution("papert", "order_execution", papert_data.get("result", ""), "success")
_update_agent_stats("papert", t1 - t0, True)
except Exception as dbg:
app.logger.warning(f"DB write skipped (non-fatal): {dbg}")
# ── Step 4: Audit ── # ── Step 4: Audit ──
t0 = datetime.now() t0 = datetime.now()
@@ -292,31 +322,40 @@ def run_pipeline():
audit_data = audit.audit_pipeline(brief_data, decio_data, papert_data) audit_data = audit.audit_pipeline(brief_data, decio_data, papert_data)
t1 = datetime.now() t1 = datetime.now()
state.update("audit", "done", audit_data.get("summary", "")[:80]) state.update("audit", "done", audit_data.get("summary", "")[:80])
log_execution("audit", "compliance_check", audit_data.get("summary", ""), "success") try:
_update_agent_stats("audit", t1 - t0, True) log_execution("audit", "compliance_check", audit_data.get("summary", ""), "success")
_update_agent_stats("audit", t1 - t0, True)
except Exception as dbg:
app.logger.warning(f"DB write skipped (non-fatal): {dbg}")
# ── Save pipeline run ── # ── Save pipeline run ──
conn = sqlite3.connect(DB_PATH) try:
c = conn.cursor() conn = _db_conn(DB_PATH)
c.execute(""" c = conn.cursor()
INSERT INTO pipeline_runs (started_at, finished_at, brief_result, decio_decision, papert_result, audit_report, status) c.execute("""
VALUES (?, ?, ?, ?, ?, ?, ?) INSERT INTO pipeline_runs (started_at, finished_at, brief_result, decio_decision, papert_result, audit_report, status)
""", ( VALUES (?, ?, ?, ?, ?, ?, ?)
start.isoformat(), """, (
datetime.now().isoformat(), start.isoformat(),
brief_data.get("summary", ""), datetime.now().isoformat(),
decio_data.get("decision", "HOLD"), brief_data.get("summary", ""),
papert_data.get("result", ""), decio_data.get("decision", "HOLD"),
json.dumps(audit_data), papert_data.get("result", ""),
"success" json.dumps(audit_data),
)) "success"
conn.commit() ))
conn.close() _safe_commit(conn)
conn.close()
except Exception as dbg:
app.logger.warning(f"Pipeline run log skipped (non-fatal): {dbg}")
except Exception as e: except Exception as e:
log_execution("pipeline", "error", str(e), "error") app.logger.error(f"Pipeline error: {e}")
try:
log_execution("pipeline", "error", str(e), "error")
except:
pass
state.update("brief", "error", f"Erro: {str(e)[:80]}") state.update("brief", "error", f"Erro: {str(e)[:80]}")
_update_agent_stats("brief", datetime.now() - start, False)
finally: finally:
state.set_pipeline_running(False) state.set_pipeline_running(False)
@@ -324,8 +363,10 @@ def run_pipeline():
state.update("papert", "idle", "Aguardando...") state.update("papert", "idle", "Aguardando...")
state.update("audit", "idle", "Aguardando...") state.update("audit", "idle", "Aguardando...")
return brief_data, decio_data, papert_data, audit_data
def _update_agent_stats(agent_id, duration, success): def _update_agent_stats(agent_id, duration, success):
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
now = datetime.now().isoformat() now = datetime.now().isoformat()
dur = int(duration.total_seconds() * 1000) dur = int(duration.total_seconds() * 1000)
@@ -342,7 +383,7 @@ def _update_agent_stats(agent_id, duration, success):
count = row[1] count = row[1]
new_avg = int((old_avg * (count - 1) + dur) / count) new_avg = int((old_avg * (count - 1) + dur) / count)
c.execute("UPDATE agents SET avg_duration_ms=? WHERE id=?", (new_avg, agent_id)) c.execute("UPDATE agents SET avg_duration_ms=? WHERE id=?", (new_avg, agent_id))
conn.commit() _safe_commit(conn)
conn.close() conn.close()
# ── Routes ──────────────────────────────────────────────────────────────────── # ── Routes ────────────────────────────────────────────────────────────────────
@@ -364,7 +405,7 @@ def api_run():
@app.route("/api/history") @app.route("/api/history")
def api_history(): def api_history():
conn = sqlite3.connect(DB_PATH) conn = _db_conn(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.cursor() c = conn.cursor()
rows = c.execute("SELECT * FROM executions ORDER BY timestamp DESC LIMIT 50").fetchall() rows = c.execute("SELECT * FROM executions ORDER BY timestamp DESC LIMIT 50").fetchall()
@@ -373,7 +414,7 @@ def api_history():
@app.route("/api/pipeline_history") @app.route("/api/pipeline_history")
def api_pipeline_history(): def api_pipeline_history():
conn = sqlite3.connect(DB_PATH) conn = _db_conn(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.cursor() c = conn.cursor()
rows = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 50").fetchall() rows = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 50").fetchall()
@@ -391,7 +432,7 @@ def api_token_stats():
@app.route("/api/token_totals", methods=["GET"]) @app.route("/api/token_totals", methods=["GET"])
def api_token_totals(): def api_token_totals():
"""All-time token totals per agent.""" """All-time token totals per agent."""
conn = sqlite3.connect(DB_PATH) conn = _db_conn(DB_PATH)
c = conn.cursor() c = conn.cursor()
c.execute(""" c.execute("""
SELECT agent, SUM(prompt_tokens) as p, SUM(completion_tokens) as c, SELECT agent, SUM(prompt_tokens) as p, SUM(completion_tokens) as c,
@@ -407,7 +448,7 @@ def api_token_totals():
@app.route("/api/agents", methods=["GET"]) @app.route("/api/agents", methods=["GET"])
def get_agents(): def get_agents():
"""List all registered agents with full status.""" """List all registered agents with full status."""
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.cursor() c = conn.cursor()
rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall() rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall()
@@ -416,7 +457,7 @@ def get_agents():
@app.route("/api/agents/<agent_id>", methods=["GET"]) @app.route("/api/agents/<agent_id>", methods=["GET"])
def get_agent(agent_id): def get_agent(agent_id):
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.cursor() c = conn.cursor()
row = c.execute("SELECT * FROM agents WHERE id=?", (agent_id,)).fetchone() row = c.execute("SELECT * FROM agents WHERE id=?", (agent_id,)).fetchone()
@@ -429,7 +470,7 @@ def get_agent(agent_id):
def update_agent(agent_id): def update_agent(agent_id):
"""Update agent config (model, provider, status, intent).""" """Update agent config (model, provider, status, intent)."""
data = request.json data = request.json
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
fields = [] fields = []
values = [] values = []
@@ -442,18 +483,18 @@ def update_agent(agent_id):
values.append(datetime.now().isoformat()) values.append(datetime.now().isoformat())
values.append(agent_id) values.append(agent_id)
c.execute(f"UPDATE agents SET {','.join(fields)} WHERE id=?", values) c.execute(f"UPDATE agents SET {','.join(fields)} WHERE id=?", values)
conn.commit() _safe_commit(conn)
conn.close() conn.close()
return jsonify({"status": "updated", "id": agent_id}) return jsonify({"status": "updated", "id": agent_id})
@app.route("/api/agents/<agent_id>", methods=["DELETE"]) @app.route("/api/agents/<agent_id>", methods=["DELETE"])
def delete_agent(agent_id): def delete_agent(agent_id):
"""Remove agent from registry.""" """Remove agent from registry."""
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
c.execute("DELETE FROM agent_logs WHERE agent_id=?", (agent_id,)) c.execute("DELETE FROM agent_logs WHERE agent_id=?", (agent_id,))
c.execute("DELETE FROM agents WHERE id=?", (agent_id,)) c.execute("DELETE FROM agents WHERE id=?", (agent_id,))
conn.commit() _safe_commit(conn)
conn.close() conn.close()
return jsonify({"status": "deleted", "id": agent_id}) return jsonify({"status": "deleted", "id": agent_id})
@@ -463,7 +504,7 @@ def register_agent():
data = request.json data = request.json
if not data.get("id") or not data.get("name"): if not data.get("id") or not data.get("name"):
return jsonify({"error": "id and name required"}), 400 return jsonify({"error": "id and name required"}), 400
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
now = datetime.now().isoformat() now = datetime.now().isoformat()
c.execute(""" c.execute("""
@@ -475,7 +516,7 @@ def register_agent():
data.get("provider", "unknown"), data.get("status"), data.get("provider", "unknown"), data.get("status"),
now, now now, now
)) ))
conn.commit() _safe_commit(conn)
conn.close() conn.close()
return jsonify({"status": "registered", "id": data["id"]}), 201 return jsonify({"status": "registered", "id": data["id"]}), 201
@@ -483,7 +524,7 @@ def register_agent():
@app.route("/api/agent_metrics", methods=["GET"]) @app.route("/api/agent_metrics", methods=["GET"])
def get_agent_metrics(): def get_agent_metrics():
"""Dashboard metrics for all agents.""" """Dashboard metrics for all agents."""
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.cursor() c = conn.cursor()
rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall() rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall()
@@ -516,7 +557,7 @@ def get_agent_metrics():
@app.route("/api/agent_logs/<agent_id>", methods=["GET"]) @app.route("/api/agent_logs/<agent_id>", methods=["GET"])
def get_agent_logs(agent_id): def get_agent_logs(agent_id):
"""Event log for specific agent.""" """Event log for specific agent."""
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.cursor() c = conn.cursor()
limit = request.args.get("limit", 50, type=int) limit = request.args.get("limit", 50, type=int)
@@ -531,7 +572,7 @@ def get_agent_logs(agent_id):
@app.route("/api/services", methods=["GET"]) @app.route("/api/services", methods=["GET"])
def get_services(): def get_services():
"""List all registered services.""" """List all registered services."""
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.cursor() c = conn.cursor()
rows = c.execute("SELECT * FROM services ORDER BY name").fetchall() rows = c.execute("SELECT * FROM services ORDER BY name").fetchall()
@@ -546,7 +587,7 @@ def register_service():
return jsonify({"error": "name required"}), 400 return jsonify({"error": "name required"}), 400
service_id = data.get("id") or hashlib.md5(data["name"].encode()).hexdigest()[:12] service_id = data.get("id") or hashlib.md5(data["name"].encode()).hexdigest()[:12]
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
c.execute(""" c.execute("""
INSERT OR REPLACE INTO services (id, name, type, endpoint, status, last_check, metadata) INSERT OR REPLACE INTO services (id, name, type, endpoint, status, last_check, metadata)
@@ -560,7 +601,7 @@ def register_service():
datetime.now().isoformat(), datetime.now().isoformat(),
json.dumps(data.get("metadata", {})) json.dumps(data.get("metadata", {}))
)) ))
conn.commit() _safe_commit(conn)
conn.close() conn.close()
return jsonify({"status": "registered", "id": service_id}), 201 return jsonify({"status": "registered", "id": service_id}), 201
@@ -568,7 +609,7 @@ def register_service():
def ping_service(service_id): def ping_service(service_id):
"""Health check a service endpoint.""" """Health check a service endpoint."""
import requests as req import requests as req
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
row = c.execute("SELECT endpoint FROM services WHERE id=?", (service_id,)).fetchone() row = c.execute("SELECT endpoint FROM services WHERE id=?", (service_id,)).fetchone()
conn.close() conn.close()
@@ -582,11 +623,11 @@ def ping_service(service_id):
except Exception as e: except Exception as e:
status = "unreachable" status = "unreachable"
conn = sqlite3.connect(AGENTS_DB) conn = _db_conn(AGENTS_DB)
c = conn.cursor() c = conn.cursor()
c.execute("UPDATE services SET status=?, last_check=? WHERE id=?", c.execute("UPDATE services SET status=?, last_check=? WHERE id=?",
(status, datetime.now().isoformat(), service_id)) (status, datetime.now().isoformat(), service_id))
conn.commit() _safe_commit(conn)
conn.close() conn.close()
return jsonify({"id": service_id, "status": status, "endpoint": endpoint}) return jsonify({"id": service_id, "status": status, "endpoint": endpoint})
@@ -594,7 +635,7 @@ def ping_service(service_id):
@app.route("/api/pipeline/visual") @app.route("/api/pipeline/visual")
def pipeline_visual(): def pipeline_visual():
"""Real-time pipeline flow with metrics.""" """Real-time pipeline flow with metrics."""
conn = sqlite3.connect(DB_PATH) conn = _db_conn(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.cursor() c = conn.cursor()
@@ -698,5 +739,243 @@ def reset_portfolio():
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# ── Backtest API (Máquina do Tempo) ───────────────────────────────────────────
@app.route("/api/backtest", methods=["POST"])
def run_backtest():
"""Motor de Backtesting Automatizado (Máquina do Tempo)."""
sys.path.insert(0, "/app/agents")
try:
import requests as req
from agents.decio import DecioAgent
from agents.papert import PapertAgent, Portfolio, PORTFOLIO_DB
from pathlib import Path
data = request.json or {}
days = data.get("days", 30)
initial_capital = data.get("initial_capital", 10000.0)
# Busca dados históricos reais da Binance (klines 1d)
r = req.get(f"https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1d&limit={days}", timeout=10)
if r.status_code != 200:
return jsonify({"error": "Falha ao buscar klines da Binance"}), 502
klines = r.json()
# Usa um banco de dados de backtest isolado
import sqlite3
backtest_db = "/app/data/backtest.db"
if os.path.exists(backtest_db):
os.remove(backtest_db)
# Sobrescreve PORTFOLIO_DB temporariamente para o backtest
import agents.papert as papert_module
old_db = papert_module.PORTFOLIO_DB
old_init = papert_module.INITIAL_BALANCE
papert_module.PORTFOLIO_DB = Path(backtest_db)
papert_module.INITIAL_BALANCE = initial_capital
pf_soberana = Portfolio(strategy="soberana")
pf_agressiva = Portfolio(strategy="agressiva")
pf_hodl = Portfolio(strategy="hodl")
results_history = []
for k in klines:
ts = datetime.fromtimestamp(k[0]/1000).strftime("%Y-%m-%d")
open_p, high_p, low_p, close_p = float(k[1]), float(k[2]), float(k[3]), float(k[4])
change_pct = ((close_p - open_p) / open_p) * 100
# Mock Brief Data baseado no kline real
sig = "ALTA" if change_pct > 0 else "BAIXA" if change_pct < 0 else "NEUTRO"
conf = min(95, max(65, int(70 + abs(change_pct) * 5)))
rsi = min(85, max(15, int(50 + change_pct * 5)))
brief_mock = {
"price": close_p,
"signal": sig,
"confidence": conf,
"rsi": rsi,
"net_signal": int(change_pct),
"summary": f"Backtest kline {ts}: Close ${close_p:,.2f} ({change_pct:+.2f}%)"
}
# Decio Decision (forçando local para rapidez e consistência do backtest)
decio = DecioAgent(brief_mock)
decio._decide_llm = lambda: None
decio_res = decio.run()
# PaperT Execution (Soberana, Agressiva e HODL)
papert = PapertAgent(decio_res)
papert._current_price = lambda: close_p
papert._write_log = lambda x: True # silencia log físico no backtest
exec_res = papert.run()
results_history.append({
"date": ts,
"price": close_p,
"change_pct": round(change_pct, 2),
"decio_action": decio_res["action"],
"decio_conf": decio_res["confidence"],
"soberana_bal": exec_res["portfolio"]["balance"],
"agressiva_bal": exec_res["portfolio"]["agressiva"]["balance"],
"hodl_bal": exec_res["portfolio"]["hodl"]["balance"]
})
# Coleta estatísticas finais do backtest
stats_final = pf_soberana.get_stats()
stats_agr = pf_agressiva.get_stats()
stats_hodl = pf_hodl.get_stats()
# Restaura variáveis originais
papert_module.PORTFOLIO_DB = old_db
papert_module.INITIAL_BALANCE = old_init
return jsonify({
"status": "completed",
"days_simulated": len(klines),
"initial_capital": initial_capital,
"final_stats": {
"soberana": {
"balance": stats_final["current_balance"],
"total_pnl": stats_final["total_pnl"],
"total_pnl_pct": stats_final["total_pnl_pct"],
"win_rate": stats_final["win_rate"],
"max_drawdown": stats_final["max_drawdown"]
},
"agressiva": {
"balance": stats_agr["current_balance"],
"total_pnl": stats_agr["total_pnl"],
"total_pnl_pct": stats_agr["total_pnl_pct"],
"win_rate": stats_agr["win_rate"],
"max_drawdown": stats_agr["max_drawdown"]
},
"hodl": {
"balance": stats_hodl["current_balance"],
"total_pnl": stats_hodl["total_pnl"],
"total_pnl_pct": stats_hodl["total_pnl_pct"],
"max_drawdown": stats_hodl["max_drawdown"]
}
},
"history": results_history
})
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")
DEFAULT_CONFIG = {
"frequency": "1h", # 4h, 1h, 15m
"risk_lock": 70, # 75, 70, 60
"capital": 10000.0,
"max_allocation": 25, # pct
"weight_balance": 50 # 0 (Tech) to 100 (Sentiment)
}
@app.route("/api/config", methods=["GET"])
def get_config():
if not os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "w") as f:
json.dump(DEFAULT_CONFIG, f)
return jsonify(DEFAULT_CONFIG)
try:
with open(CONFIG_FILE, "r") as f:
cfg = json.load(f)
for k, v in DEFAULT_CONFIG.items():
if k not in cfg: cfg[k] = v
return jsonify(cfg)
except:
return jsonify(DEFAULT_CONFIG)
@app.route("/api/config", methods=["POST"])
def save_config():
try:
data = request.json or {}
cfg = {
"frequency": data.get("frequency", "1h"),
"risk_lock": int(data.get("risk_lock", 70)),
"capital": float(data.get("capital", 10000.0)),
"max_allocation": int(data.get("max_allocation", 25)),
"weight_balance": int(data.get("weight_balance", 50))
}
with open(CONFIG_FILE, "w") as f:
json.dump(cfg, f, indent=2)
# Atualiza o INITIAL_BALANCE do PaperT em tempo real se o capital mudar
sys.path.insert(0, "/app/agents")
try:
import agents.papert as papert_module
papert_module.INITIAL_BALANCE = cfg["capital"]
except: pass
return jsonify({"status": "saved", "config": cfg})
except Exception as e:
return jsonify({"error": str(e)}), 400
# ── Background Automated Scheduler ───────────────────────────────────────────
def start_scheduler():
def scheduler_loop():
import time
# Sleep for a bit to let the app start up completely
time.sleep(15)
app.logger.info("Scheduler thread started successfully.")
while True:
try:
# Load configuration to get frequency
freq_mins = 60
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r") as f:
cfg = json.load(f)
freq = cfg.get("frequency", "1h")
if freq == "15m":
freq_mins = 15
elif freq == "4h":
freq_mins = 240
else:
freq_mins = 60
except:
pass
# Check last pipeline run from database
should_run = False
try:
conn = _db_conn(DB_PATH)
c = conn.cursor()
row = c.execute("SELECT started_at FROM pipeline_runs ORDER BY started_at DESC LIMIT 1").fetchone()
conn.close()
except Exception as dbe:
row = None
app.logger.error(f"Scheduler DB check failed: {dbe}")
if not row:
should_run = True
else:
last_run_str = row[0]
try:
last_run = datetime.fromisoformat(last_run_str)
if datetime.now() - last_run >= timedelta(minutes=freq_mins):
should_run = True
except Exception as pe:
should_run = True
if should_run:
if not state.pipeline_running:
app.logger.info(f"Scheduler triggering automated pipeline run (Interval: {freq_mins}m)...")
thread = threading.Thread(target=run_pipeline)
thread.start()
else:
app.logger.info("Scheduler skipped run: pipeline is already running.")
except Exception as e:
app.logger.error(f"Scheduler loop error: {e}")
# Check every 60 seconds
time.sleep(60)
thread = threading.Thread(target=scheduler_loop, daemon=True)
thread.start()
start_scheduler()
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=3100, debug=False) app.run(host="0.0.0.0", port=3100, debug=False)
# ── DEBUG: Log all DB operations ─────────────────────────────────────────────
+231
View File
@@ -20,6 +20,8 @@ document.addEventListener('DOMContentLoaded', () => {
loadServices(); loadServices();
loadPipelineVisual(); loadPipelineVisual();
loadTokenChart(); loadTokenChart();
loadConfig();
loadMultiStratChart('live');
updateStatus('Sistema operacional', 'online'); updateStatus('Sistema operacional', 'online');
}); });
@@ -606,3 +608,232 @@ function renderTokenTotals(totals) {
tbody.appendChild(tr); tbody.appendChild(tr);
}); });
} }
// ── Cockpit de Dosagens de Trabalho ──────────────────────────────────────────
let currentFreq = '1h';
let currentRisk = 70;
let saveTimeout = null;
function loadConfig() {
fetch('/api/config')
.then(r => r.json())
.then(cfg => {
currentFreq = cfg.frequency || '1h';
currentRisk = cfg.risk_lock || 70;
const capInput = document.getElementById('cockpitCapital');
if (capInput) capInput.value = cfg.capital || 10000;
const allocInput = document.getElementById('cockpitMaxAlloc');
if (allocInput) allocInput.value = cfg.max_allocation || 25;
const w = cfg.weight_balance !== undefined ? cfg.weight_balance : 50;
const wInput = document.getElementById('cockpitWeight');
if (wInput) wInput.value = w;
updateWeightLabel(w);
updateCockpitUI();
})
.catch(e => console.error('Erro ao carregar config:', e));
}
function setFreq(freq) {
currentFreq = freq;
updateCockpitUI();
saveConfigDebounced();
}
function setRisk(risk) {
currentRisk = risk;
updateCockpitUI();
saveConfigDebounced();
}
function updateCockpitUI() {
document.querySelectorAll('#freqOptions .btn-opt').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('onclick').includes(`'${currentFreq}'`));
});
document.querySelectorAll('#riskOptions .btn-opt').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('onclick').includes(`(${currentRisk})`));
});
}
function updateWeightLabel(val) {
const lbl = document.getElementById('weightValLabel');
if (!lbl) return;
if (val == 50) lbl.textContent = 'Equilíbrio (50/50)';
else if (val < 50) lbl.textContent = `Foco Técnico (${100-val}% Tech / ${val}% Macro)`;
else lbl.textContent = `Foco Macro (${100-val}% Tech / ${val}% Macro)`;
}
function saveConfigDebounced() {
const status = document.getElementById('cockpitSaveStatus');
if (status) { status.textContent = 'Salvando...'; status.className = 'save-status saving'; }
clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveConfigNow, 800);
}
function saveConfigNow() {
const data = {
frequency: currentFreq,
risk_lock: currentRisk,
capital: parseFloat(document.getElementById('cockpitCapital').value) || 10000,
max_allocation: parseInt(document.getElementById('cockpitMaxAlloc').value) || 25,
weight_balance: parseInt(document.getElementById('cockpitWeight').value) || 50
};
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
const status = document.getElementById('cockpitSaveStatus');
if (status) {
status.textContent = '✓ Configurações salvas e injetadas na IA!';
status.className = 'save-status success';
setTimeout(() => { status.textContent = ''; }, 4000);
}
const pfBal = document.getElementById('pfBalance');
if (pfBal && res.config && res.config.capital) {
pfBal.textContent = `$${res.config.capital.toLocaleString('pt-BR', {minimumFractionDigits:2})}`;
}
})
.catch(e => {
const status = document.getElementById('cockpitSaveStatus');
if (status) { status.textContent = 'Erro ao salvar!'; status.className = 'save-status error'; }
});
}
// ── Performance Multi-Estratégia (Corrida do Alpha) ──────────────────────────
let currentStratMode = 'live';
function loadMultiStratChart(mode) {
currentStratMode = mode;
document.querySelectorAll('.multistrat-controls .btn-strat').forEach(b => {
b.classList.toggle('active', b.getAttribute('onclick').includes(`('${mode}')`) || b.getAttribute('onclick').includes(`(${mode})`));
});
const spinner = document.getElementById('multistratSpinner');
const canvas = document.getElementById('multiStratCanvas');
if (!canvas) return;
if (mode === 'live') {
if (spinner) spinner.style.display = 'none';
fetch('/api/portfolio')
.then(r => r.json())
.then(data => {
const init = data.initial_balance || 10000;
const hist = (data.recent_trades || []).reverse().map((t, idx) => {
return {
date: t.date || `Trade #${idx+1}`,
soberana_bal: init + (t.result === 'WIN' ? 150 : t.result === 'LOSS' ? -100 : 0) * (idx+1),
agressiva_bal: init + (t.result === 'WIN' ? 250 : t.result === 'LOSS' ? -200 : 0) * (idx+1),
hodl_bal: init * (1 + (idx*0.01)),
rsi: 50 + (Math.sin(idx) * 20)
};
});
if (hist.length === 0) {
hist.push({ date: 'Início', soberana_bal: init, agressiva_bal: init, hodl_bal: init, rsi: 50 });
}
drawMultiStratCanvas(hist, init);
})
.catch(e => console.error('Erro multistrat live:', e));
} else {
if (spinner) spinner.style.display = 'block';
fetch('/api/backtest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ days: mode, initial_capital: parseFloat(document.getElementById('cockpitCapital').value) || 10000 })
})
.then(r => r.json())
.then(data => {
if (spinner) spinner.style.display = 'none';
if (data.history && data.history.length) {
drawMultiStratCanvas(data.history, data.initial_capital || 10000);
}
})
.catch(e => {
if (spinner) spinner.style.display = 'none';
console.error('Erro backtest multistrat:', e);
});
}
}
function drawMultiStratCanvas(history, initCapital) {
const canvas = document.getElementById('multiStratCanvas');
if (!canvas || !history || !history.length) return;
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
ctx.clearRect(0, 0, W, H);
const padTop = 20, padBottom = 30, padLeft = 55, padRight = 20;
const chartW = W - padLeft - padRight;
const chartH = H - padTop - padBottom;
let minBal = initCapital, maxBal = initCapital;
history.forEach(d => {
minBal = Math.min(minBal, d.soberana_bal, d.agressiva_bal, d.hodl_bal);
maxBal = Math.max(maxBal, d.soberana_bal, d.agressiva_bal, d.hodl_bal);
});
minBal *= 0.98; maxBal *= 1.02;
const range = maxBal - minBal || 1;
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.fillStyle = '#778';
ctx.font = '10px monospace';
ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const y = padTop + (chartH / 4) * i;
const val = maxBal - (range / 4) * i;
ctx.beginPath(); ctx.moveTo(padLeft, y); ctx.lineTo(W - padRight, y); ctx.stroke();
ctx.fillText(`$${Math.round(val).toLocaleString()}`, padLeft - 8, y + 3);
}
const stepX = chartW / (history.length - 1 || 1);
history.forEach((d, i) => {
const x = padLeft + i * stepX;
const rsi = d.rsi || 50;
const barH = (rsi / 100) * chartH;
ctx.fillStyle = rsi > 65 ? 'rgba(105,240,174,0.08)' : rsi < 35 ? 'rgba(255,107,107,0.08)' : 'rgba(179,136,255,0.05)';
ctx.fillRect(x - stepX/3, padTop + chartH - barH, stepX/1.5, barH);
});
const drawLine = (key, color, width) => {
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
history.forEach((d, i) => {
const x = padLeft + i * stepX;
const y = padTop + chartH - ((d[key] - minBal) / range) * chartH;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.stroke();
};
drawLine('hodl_bal', '#ffd54f', 2);
drawLine('agressiva_bal', '#4fc3f7', 2);
drawLine('soberana_bal', '#69f0ae', 3);
const last = history[history.length - 1];
if (last) {
const elSob = document.getElementById('legSoberana');
if (elSob) elSob.textContent = `$${Math.round(last.soberana_bal).toLocaleString()}`;
const elAgr = document.getElementById('legAgressiva');
if (elAgr) elAgr.textContent = `$${Math.round(last.agressiva_bal).toLocaleString()}`;
const elHodl = document.getElementById('legHodl');
if (elHodl) elHodl.textContent = `$${Math.round(last.hodl_bal).toLocaleString()}`;
const elSent = document.getElementById('legSentimento');
if (elSent) elSent.textContent = `${Math.round(last.rsi || 50)}%`;
}
ctx.fillStyle = '#778';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
const numLabels = Math.min(history.length, 6);
const labelStep = Math.max(1, Math.floor((history.length - 1) / (numLabels - 1)));
history.forEach((d, i) => {
if (i % labelStep === 0 || i === history.length - 1) {
const x = padLeft + i * stepX;
ctx.fillText(d.date ? d.date.slice(-5) : '', x, H - 8);
}
});
}
+125
View File
@@ -405,3 +405,128 @@ body {
.tokens-table th { background: rgba(99, 110, 200, 0.2); color: #a0aaff; padding: 8px 10px; text-align: left; } .tokens-table th { background: rgba(99, 110, 200, 0.2); color: #a0aaff; padding: 8px 10px; text-align: left; }
.tokens-table td { padding: 6px 10px; border-bottom: 1px solid rgba(99, 110, 200, 0.1); } .tokens-table td { padding: 6px 10px; border-bottom: 1px solid rgba(99, 110, 200, 0.1); }
.tokens-table tr:hover td { background: rgba(99, 110, 200, 0.08); } .tokens-table tr:hover td { background: rgba(99, 110, 200, 0.08); }
/* ── Cockpit de Dosagens de Trabalho ────────────────────────────────────────── */
.cockpit-section {
background: rgba(26, 26, 46, 0.85);
backdrop-filter: blur(12px);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
border: 1px solid rgba(79, 195, 247, 0.25);
}
.cockpit-section h2 {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 16px; font-size: 1.25rem; color: var(--text-primary);
}
.badge {
font-size: 0.7rem; background: rgba(79, 195, 247, 0.15);
color: var(--accent-blue); padding: 4px 8px; border-radius: 6px; border: 1px solid rgba(79, 195, 247, 0.3);
}
.cockpit-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.cockpit-card {
background: rgba(15, 15, 26, 0.6);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px; padding: 16px;
display: flex; flex-direction: column; justify-content: space-between;
transition: all 0.2s;
}
.cockpit-card:hover {
background: rgba(15, 15, 26, 0.8); border-color: rgba(79, 195, 247, 0.2);
transform: translateY(-2px);
}
.cockpit-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.cockpit-icon { font-size: 1.2rem; }
.cockpit-card-header h3 { font-size: 1rem; color: var(--text-primary); margin: 0; }
.cockpit-desc { font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 12px; line-height: 1.2; }
.cockpit-options { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-opt {
flex: 1; min-width: 65px; padding: 8px 10px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px; color: var(--text-secondary);
font-size: 0.8rem; font-weight: 600; cursor: pointer;
display: flex; flex-direction: column; align-items: center; gap: 2px;
transition: all 0.2s;
}
.btn-opt small { font-size: 0.65rem; opacity: 0.7; font-weight: 400; }
.btn-opt:hover { background: rgba(255, 255, 255, 0.08); color: var(--text-primary); }
.btn-opt.active {
background: rgba(79, 195, 247, 0.15);
border-color: var(--accent-blue);
color: var(--accent-blue);
box-shadow: 0 0 12px rgba(79, 195, 247, 0.2);
}
.cockpit-inputs { display: flex; gap: 12px; }
.cockpit-inputs label {
flex: 1; font-size: 0.75rem; color: var(--text-secondary); font-weight: 600;
display: flex; flex-direction: column; gap: 4px;
}
.cockpit-inputs input {
background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px; padding: 8px 10px; color: var(--accent-green); font-weight: 700;
font-size: 0.9rem; width: 100%; outline: none; transition: border 0.2s;
}
.cockpit-inputs input:focus { border-color: var(--accent-green); }
.cockpit-slider-wrap { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.slider-labels { display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-secondary); }
.cockpit-slider-wrap input[type="range"] {
width: 100%; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px;
outline: none; -webkit-appearance: none; accent-color: var(--accent-purple);
}
.cockpit-slider-wrap input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%;
background: var(--accent-purple); cursor: pointer; box-shadow: 0 0 8px var(--accent-purple);
}
.slider-val { font-size: 0.75rem; color: var(--accent-purple); text-align: center; font-weight: 600; margin-top: 2px; }
.cockpit-footer { margin-top: 16px; text-align: right; min-height: 20px; }
.save-status { font-size: 0.8rem; font-weight: 600; transition: all 0.3s; }
.save-status.saving { color: var(--accent-yellow); }
.save-status.success { color: var(--accent-green); }
.save-status.error { color: var(--accent-red); }
/* ── Otimizações de Responsividade Mobile & Design Compacto ─────────────────── */
@media (max-width: 768px) {
.dashboard { padding: 12px; gap: 16px; }
.header { padding: 12px 16px; flex-direction: column; gap: 10px; align-items: flex-start; }
.header-status { width: 100%; justify-content: space-between; }
.header-time { align-self: flex-end; margin-top: -28px; }
.pipeline-section, .cockpit-section, .office-section, .registry-section, .metrics-section, .services-section, .visual-section, .portfolio-section, .tokens-section, .log-section {
padding: 16px;
}
h2 { font-size: 1.15rem; }
.pipeline-flow { gap: 8px; }
.pipeline-node { padding: 12px 10px; min-width: 100px; }
.pipeline-controls { flex-direction: column; width: 100%; }
.pipeline-controls button { width: 100%; }
.two-col-section { grid-template-columns: 1fr; gap: 16px; }
.portfolio-summary { flex-direction: column; align-items: flex-start; gap: 8px; }
.tokens-controls { flex-direction: column; align-items: flex-start; gap: 8px; }
.cockpit-grid { grid-template-columns: 1fr; }
}
/* ── Performance Multi-Estratégia (Corrida do Alpha) ────────────────────────── */
.multistrat-section {
background: var(--bg-card); border-radius: var(--border-radius);
padding: 20px; box-shadow: var(--shadow); border: 1px solid rgba(105,240,174,0.25);
}
.multistrat-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-bottom: 16px; }
.multistrat-controls { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-strat {
padding: 6px 14px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px; color: var(--text-secondary); font-size: 0.8rem; font-weight: 600; cursor: pointer;
transition: all 0.2s;
}
.btn-strat:hover { background: rgba(255,255,255,0.1); color: var(--text-primary); }
.btn-strat.active { background: var(--accent-blue); color: #000; border-color: var(--accent-blue); box-shadow: 0 0 12px rgba(79,195,247,0.3); }
.multistrat-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; font-size: 0.85rem; background: var(--bg-card-hover); padding: 10px 16px; border-radius: 10px; }
.legend-item { display: flex; align-items: center; gap: 6px; color: var(--text-secondary); }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
.legend-item b { color: var(--text-primary); font-weight: 700; margin-left: 4px; }
.multistrat-chart-wrap { background: rgba(10,12,28,0.6); border-radius: 12px; padding: 12px; overflow-x: auto; }
.multistrat-chart-wrap canvas { max-width: 100%; height: auto; display: block; }
.strat-spinner { font-size: 0.85rem; color: var(--accent-yellow); padding: 10px; text-align: center; font-weight: 600; animation: pulse 1.5s infinite; }
+89 -8
View File
@@ -40,17 +40,98 @@
<div id="lastDecisionBanner" class="decision-banner" style="display:none;"></div> <div id="lastDecisionBanner" class="decision-banner" style="display:none;"></div>
</section> </section>
<!-- Office Floor --> <!-- Cockpit de Dosagens de Trabalho -->
<section class="office-section"> <section class="cockpit-section">
<h2>🏠 Escritório Virtual</h2> <h2>🎛️ Dosagens de Trabalho <span class="badge">Cockpit do Investidor</span></h2>
<div class="office-floor"> <div class="cockpit-grid">
<div class="floor-grid" id="agentDesks"> <!-- Ritmo de Análise -->
<!-- Agent desks injected by JS --> <div class="cockpit-card">
<div class="cockpit-card-header">
<span class="cockpit-icon">⏱️</span>
<h3>Ritmo de Análise</h3>
</div>
<p class="cockpit-desc">Frequência com que a equipe avalia o mercado.</p>
<div class="cockpit-options" id="freqOptions">
<button class="btn-opt" onclick="setFreq('4h')">🐢 4h <small>Posicional</small></button>
<button class="btn-opt active" onclick="setFreq('1h')">🚶 1h <small>Padrão</small></button>
<button class="btn-opt" onclick="setFreq('15m')">⚡ 15m <small>Scalp</small></button>
</div>
</div> </div>
<div class="chat-bubble" id="chatBubble">
<span id="chatText">Olá! Eu sou o escritório virtual da BrainSteel Fin.</span> <!-- Apetite ao Risco -->
<div class="cockpit-card">
<div class="cockpit-card-header">
<span class="cockpit-icon">🛡️</span>
<h3>Apetite ao Risco</h3>
</div>
<p class="cockpit-desc">Nível de exigência do CEO Décio para operar.</p>
<div class="cockpit-options" id="riskOptions">
<button class="btn-opt" onclick="setRisk(75)">🔒 75% <small>Conservador</small></button>
<button class="btn-opt active" onclick="setRisk(70)">⚖️ 70% <small>Moderado</small></button>
<button class="btn-opt" onclick="setRisk(60)">💥 60% <small>Agressivo</small></button>
</div>
</div>
<!-- Capital e Alocação -->
<div class="cockpit-card">
<div class="cockpit-card-header">
<span class="cockpit-icon">💰</span>
<h3>Banca & Alocação</h3>
</div>
<p class="cockpit-desc">Capital virtual e limite de risco por trade.</p>
<div class="cockpit-inputs">
<label>Banca (USDT)
<input type="number" id="cockpitCapital" value="10000" onchange="saveConfigDebounced()">
</label>
<label>Mão Máx (%)
<input type="number" id="cockpitMaxAlloc" value="25" min="5" max="100" onchange="saveConfigDebounced()">
</label>
</div>
</div>
<!-- Balança Técnica vs Sentimento -->
<div class="cockpit-card">
<div class="cockpit-card-header">
<span class="cockpit-icon">⚖️</span>
<h3>Balança de Pesos</h3>
</div>
<p class="cockpit-desc">Foco analítico: Técnica pura vs Sentimento/Macro.</p>
<div class="cockpit-slider-wrap">
<div class="slider-labels">
<span>◄ Técnica (Gráficos)</span>
<span>Sentimento (Macro) ►</span>
</div>
<input type="range" id="cockpitWeight" min="0" max="100" value="50" onchange="saveConfigDebounced()" oninput="updateWeightLabel(this.value)">
<div class="slider-val" id="weightValLabel">Equilíbrio (50/50)</div>
</div>
</div> </div>
</div> </div>
<div class="cockpit-footer">
<span id="cockpitSaveStatus" class="save-status"></span>
</div>
</section>
<!-- Performance Multi-Estratégia (Corrida do Alpha) -->
<section class="multistrat-section">
<div class="multistrat-header">
<h2>📈 Performance Multi-Estratégia <span class="badge">A Corrida do Alpha</span></h2>
<div class="multistrat-controls">
<button class="btn-strat active" onclick="loadMultiStratChart('live')">🔴 Ao Vivo</button>
<button class="btn-strat" onclick="loadMultiStratChart(30)">📅 30 Dias</button>
<button class="btn-strat" onclick="loadMultiStratChart(60)">📅 60 Dias</button>
<button class="btn-strat" onclick="loadMultiStratChart(90)">📅 90 Dias</button>
</div>
</div>
<div class="multistrat-legend">
<span class="legend-item"><span class="legend-dot" style="background:#69f0ae"></span> Soberana (Décio) <b id="legSoberana">$10.000</b></span>
<span class="legend-item"><span class="legend-dot" style="background:#4fc3f7"></span> Agressiva (Brief) <b id="legAgressiva">$10.000</b></span>
<span class="legend-item"><span class="legend-dot" style="background:#ffd54f"></span> HODL (BTC Puro) <b id="legHodl">$10.000</b></span>
<span class="legend-item"><span class="legend-dot" style="background:#b388ff"></span> Sentimento (F&G) <b id="legSentimento">50%</b></span>
</div>
<div class="multistrat-chart-wrap">
<canvas id="multiStratCanvas" width="900" height="260"></canvas>
</div>
<div id="multistratSpinner" class="strat-spinner" style="display:none;">⚙️ Calculando máquina do tempo (Backtest Binance)...</div>
</section> </section>
<!-- Item 1: Agent Registry --> <!-- Item 1: Agent Registry -->