diff --git a/agents/__pycache__/brief.cpython-313.pyc b/agents/__pycache__/brief.cpython-313.pyc index 65faad2..d3b848f 100644 Binary files a/agents/__pycache__/brief.cpython-313.pyc and b/agents/__pycache__/brief.cpython-313.pyc differ diff --git a/agents/__pycache__/decio.cpython-313.pyc b/agents/__pycache__/decio.cpython-313.pyc index 6d86b1f..10cd2a4 100644 Binary files a/agents/__pycache__/decio.cpython-313.pyc and b/agents/__pycache__/decio.cpython-313.pyc differ diff --git a/agents/audit.py b/agents/audit.py index c2acf6f..6a7cf8a 100644 --- a/agents/audit.py +++ b/agents/audit.py @@ -10,6 +10,52 @@ class AuditAgent: def __init__(self): 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): checks = [] @@ -105,7 +151,7 @@ class AuditAgent: if decio_action == "HOLD" and brief_data.get("confidence", 0) < 75: 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')}", "timestamp": datetime.now().isoformat(), "checks": checks, @@ -117,10 +163,12 @@ class AuditAgent: "brief_summary": brief_data.get("summary", "")[:100], "decio_decision": decio_action, "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_pct": portfolio.get("total_pnl_pct", 0), } + self.send_notification(res) + return res def generate_llm_report(self, brief_data, decio_data): if not os.getenv("PAPERT_API_KEY") and not os.getenv("OPENROUTER_API_KEY", ""): diff --git a/agents/decio.py b/agents/decio.py index 9d7de11..f66f8bf 100644 --- a/agents/decio.py +++ b/agents/decio.py @@ -1,7 +1,7 @@ """ BrainSteel Fin β€” Decio Agent (v2) 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 from datetime import datetime @@ -14,7 +14,7 @@ class DecioAgent: self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" 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): """DΓ©cio is a mathematician: no confidence β‰₯70%, no operation.""" 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)} REGRAS OPERACIONAIS: - ConfianΓ§a < 75% β†’ decisΓ£o SEMPRE HOLD (nunca opera) - ConfianΓ§a β‰₯ 75% + sinal=ALTA β†’ BUY - ConfianΓ§a β‰₯ 75% + sinal=BAIXA β†’ SELL + ConfianΓ§a < 70% β†’ decisΓ£o SEMPRE HOLD (nunca opera) +ConfianΓ§a β‰₯ 70% + sinal=ALTA β†’ BUY +ConfianΓ§a β‰₯ 70% + sinal=BAIXA β†’ SELL BUY/SELL β†’ Stop Loss = preΓ§o Γ— 0.98 (-2%) | Take Profit = preΓ§o Γ— 1.05 (+5%) 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 if self._risk_filter(data.get("confidence", 0), signal): data["decision"] = "HOLD" - data["justification"] = "ConfianΓ§a < 75% β€” preservando capital" + data["justification"] = "ConfianΓ§a < 70% β€” preservando capital" data["stop_loss"] = 0 data["take_profit"] = 0 return data @@ -131,7 +131,7 @@ AnalΓ­tico, frio, matemΓ‘tico. PreservaΓ§Γ£o de capital > lucro rΓ‘pido. elif self._risk_filter(confidence, signal): decision = "HOLD" sl, tp = 0, 0 - justification = f"ConfianΓ§a {confidence}% < 75% β€” preservando capital" + justification = f"ConfianΓ§a {confidence}% < 70% β€” preservando capital" elif signal == "ALTA": decision = "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 } + # ── 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 ─────────────────────────────────────────────────────────── def run(self): llm_result = self._decide_llm() result = llm_result if llm_result else self._local_decision() price = self.brief_data.get("price", 0) + conf = result.get("confidence", 50) + action = result["decision"] + amount_pct = self._calc_allocation(conf, action) + output = { - "action": result["decision"], - "amount_pct": 25 if result["decision"] in ("BUY", "SELL") else 0, + "action": action, + "amount_pct": amount_pct, "stop_loss": result.get("stop_loss", 0), "take_profit": result.get("take_profit", 0), - "confidence": result.get("confidence", 50), + "confidence": conf, "justification": result.get("justification", "")[:200], "signal": result.get("signal", self.brief_data.get("signal", "NEUTRO")), "price": price, diff --git a/agents/papert.py b/agents/papert.py index d76f089..c4e9239 100644 --- a/agents/papert.py +++ b/agents/papert.py @@ -13,17 +13,22 @@ DATA_DIR = Path("/app/data") PORTFOLIO_DB = DATA_DIR / "portfolio.db" 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() def _init_db(self): DATA_DIR.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(PORTFOLIO_DB)) c = conn.cursor() - c.execute(""" - CREATE TABLE IF NOT EXISTS portfolio ( + c.execute(f""" + CREATE TABLE IF NOT EXISTS {self.tbl_portfolio} ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT, balance REAL, @@ -36,8 +41,8 @@ class Portfolio: UNIQUE(date) ) """) - c.execute(""" - CREATE TABLE IF NOT EXISTS trades ( + c.execute(f""" + CREATE TABLE IF NOT EXISTS {self.tbl_trades} ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT, action TEXT, @@ -55,8 +60,8 @@ class Portfolio: created_at TEXT ) """) - c.execute(""" - CREATE TABLE IF NOT EXISTS daily_snapshot ( + c.execute(f""" + CREATE TABLE IF NOT EXISTS {self.tbl_snapshot} ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT UNIQUE, balance REAL, @@ -72,13 +77,13 @@ class Portfolio: """) # Ensure today's row exists 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: # 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 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())) conn.commit() conn.close() @@ -88,7 +93,7 @@ class Portfolio: conn = sqlite3.connect(str(PORTFOLIO_DB)) c = conn.cursor() 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() 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 amount_usdt = allocation 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: # Sell portion of BTC held @@ -150,19 +155,19 @@ class Portfolio: elif take_profit and price >= take_profit: exit_reason = "take_profit_hit" 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: new_balance = balance 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 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 - c.execute(""" - 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) + c.execute(f""" + 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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, (today, action, price, amount_usdt, btc_amount, price if action == "SELL" else 0, @@ -172,12 +177,12 @@ class Portfolio: now)) # 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)) 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: - 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.close() @@ -196,7 +201,7 @@ class Portfolio: c = conn.cursor() # 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) wins = sum(1 for t in trades if t[3] == "WIN") @@ -204,7 +209,7 @@ class Portfolio: # Current balance 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 btc_held = cur[1] if cur else 0.0 @@ -217,7 +222,7 @@ class Portfolio: total_value = current_balance + (btc_held * btc_price) # 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() @@ -230,6 +235,8 @@ class Portfolio: peak = INITIAL_BALANCE max_drawdown = 0 for _, bal in days: + if bal is None: + continue if bal > peak: peak = bal dd = (peak - bal) / peak * 100 @@ -261,10 +268,10 @@ class Portfolio: stats = self.get_stats() # 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(""" - INSERT OR REPLACE INTO daily_snapshot + c.execute(f""" + 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) VALUES (?,?,?,?,?,?,?,?,?,?) """, ( @@ -284,12 +291,12 @@ class Portfolio: 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): self.name = "PaperT" 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_url = "https://openrouter.ai/api/v1/chat/completions" self.model = "deepseek/deepseek-v4-flash:free" @@ -303,10 +310,10 @@ class PapertAgent: break 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") 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 def _write_log(self, log_line): @@ -335,23 +342,39 @@ class PapertAgent: take_profit = self.decio_data.get("take_profit", 0) or 0 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) - - # Log entry - log_entry = self._format_log_entry(action, entry_price, stop_loss, take_profit) + log_entry = self._format_log_entry(action, entry_price, stop_loss, take_profit, "Soberana") logged = self._write_log(log_entry) - - # Save daily snapshot - try: - self.portfolio.snapshot() - except: - pass - - # Build result + try: self.portfolio.snapshot() + except: pass 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}" 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}%)" @@ -367,7 +390,7 @@ class PapertAgent: "fee_usdt": fee, "log_appended": logged, "result": result_str, - # Portfolio info for dashboard + # Portfolio info for dashboard (Enriched with Multi-Strategy) "portfolio": { "balance": stats["current_balance"], "total_value": stats["total_value"], @@ -378,7 +401,21 @@ class PapertAgent: "losses": stats["losses"], "win_rate": stats["win_rate"], "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", "timestamp": datetime.now().isoformat() diff --git a/app.py b/app.py index 6f933c8..b7d3f9d 100644 --- a/app.py +++ b/app.py @@ -20,6 +20,27 @@ from agents.papert import PapertAgent from agents.audit import AuditAgent 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") # Paths @@ -34,7 +55,7 @@ AGENTS_DB = os.path.join(DATA_DIR, "agents.db") # ── Databases ───────────────────────────────────────────────────────────────── def init_db(): - conn = sqlite3.connect(DB_PATH) + conn = _db_conn(DB_PATH) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS executions ( @@ -70,12 +91,12 @@ def init_db(): cost_usd REAL ) """) - conn.commit() + _safe_commit(conn) conn.close() def init_agents_db(): """Agent registry β€” onboarding + state.""" - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS agents ( @@ -117,12 +138,12 @@ def init_agents_db(): metadata TEXT ) """) - conn.commit() + _safe_commit(conn) conn.close() def seed_agents(): """Seed default agents on first run.""" - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) c = conn.cursor() # Check if already seeded row = c.execute("SELECT COUNT(*) FROM agents").fetchone()[0] @@ -145,7 +166,7 @@ def seed_agents(): for a in defaults: c.execute("INSERT INTO agents (id,name,role,intent,model,provider,status,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)", (*a, now, now)) - conn.commit() + _safe_commit(conn) conn.close() init_db() @@ -197,21 +218,21 @@ state = AgentState() # ── Logging ─────────────────────────────────────────────────────────────────── def log_execution(agent, action, result, status="success"): - conn = sqlite3.connect(DB_PATH) + conn = _db_conn(DB_PATH) c = conn.cursor() c.execute("INSERT INTO executions (timestamp, agent, action, result, status) VALUES (?, ?, ?, ?, ?)", (datetime.now().isoformat(), agent, action, result, status)) - conn.commit() + _safe_commit(conn) conn.close() # Also log to agent_logs _log_agent_event(agent, "execution", result) 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.execute("INSERT INTO agent_logs (agent_id, timestamp, event, duration_ms, result) VALUES (?, ?, ?, ?, ?)", (agent_id, datetime.now().isoformat(), event, duration_ms, result)) - conn.commit() + _safe_commit(conn) conn.close() # ── Pipeline Execution ──────────────────────────────────────────────────────── @@ -233,7 +254,6 @@ def run_pipeline(): brief_data = brief.run() t1 = datetime.now() 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", "") state.brief.update({ "price": brief_data.get("price"), @@ -255,8 +275,12 @@ def run_pipeline(): "macd_histogram": brief_data.get("macd_histogram"), "btc_dominance": brief_data.get("btc_dominance"), }) - log_execution("brief", "market_analysis", brief_summary, "success") - _update_agent_stats("brief", t1 - t0, True) + # RESILIENT: Skip DB write if fails + 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 ── t0 = datetime.now() @@ -272,8 +296,11 @@ def run_pipeline(): "take_profit": decio_data.get("take_profit"), "justification": decio_data.get("justification", "")[:100], }) - log_execution("decio", "strategy_decision", decio_data.get("decision", "HOLD"), "success") - _update_agent_stats("decio", t1 - t0, True) + try: + 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 ── t0 = datetime.now() @@ -282,8 +309,11 @@ def run_pipeline(): papert_data = papert.run() t1 = datetime.now() state.update("papert", "done", papert_data.get("result", "")[:80]) - log_execution("papert", "order_execution", papert_data.get("result", ""), "success") - _update_agent_stats("papert", t1 - t0, True) + try: + 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 ── t0 = datetime.now() @@ -292,31 +322,40 @@ def run_pipeline(): audit_data = audit.audit_pipeline(brief_data, decio_data, papert_data) t1 = datetime.now() state.update("audit", "done", audit_data.get("summary", "")[:80]) - log_execution("audit", "compliance_check", audit_data.get("summary", ""), "success") - _update_agent_stats("audit", t1 - t0, True) + try: + 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 ── - conn = sqlite3.connect(DB_PATH) - c = conn.cursor() - c.execute(""" - INSERT INTO pipeline_runs (started_at, finished_at, brief_result, decio_decision, papert_result, audit_report, status) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, ( - start.isoformat(), - datetime.now().isoformat(), - brief_data.get("summary", ""), - decio_data.get("decision", "HOLD"), - papert_data.get("result", ""), - json.dumps(audit_data), - "success" - )) - conn.commit() - conn.close() + try: + conn = _db_conn(DB_PATH) + c = conn.cursor() + c.execute(""" + INSERT INTO pipeline_runs (started_at, finished_at, brief_result, decio_decision, papert_result, audit_report, status) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + start.isoformat(), + datetime.now().isoformat(), + brief_data.get("summary", ""), + decio_data.get("decision", "HOLD"), + papert_data.get("result", ""), + json.dumps(audit_data), + "success" + )) + _safe_commit(conn) + conn.close() + except Exception as dbg: + app.logger.warning(f"Pipeline run log skipped (non-fatal): {dbg}") 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]}") - _update_agent_stats("brief", datetime.now() - start, False) finally: state.set_pipeline_running(False) @@ -324,8 +363,10 @@ def run_pipeline(): state.update("papert", "idle", "Aguardando...") state.update("audit", "idle", "Aguardando...") + return brief_data, decio_data, papert_data, audit_data + def _update_agent_stats(agent_id, duration, success): - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) c = conn.cursor() now = datetime.now().isoformat() dur = int(duration.total_seconds() * 1000) @@ -342,7 +383,7 @@ def _update_agent_stats(agent_id, duration, success): count = row[1] new_avg = int((old_avg * (count - 1) + dur) / count) c.execute("UPDATE agents SET avg_duration_ms=? WHERE id=?", (new_avg, agent_id)) - conn.commit() + _safe_commit(conn) conn.close() # ── Routes ──────────────────────────────────────────────────────────────────── @@ -364,7 +405,7 @@ def api_run(): @app.route("/api/history") def api_history(): - conn = sqlite3.connect(DB_PATH) + conn = _db_conn(DB_PATH) conn.row_factory = sqlite3.Row c = conn.cursor() 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") def api_pipeline_history(): - conn = sqlite3.connect(DB_PATH) + conn = _db_conn(DB_PATH) conn.row_factory = sqlite3.Row c = conn.cursor() 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"]) def api_token_totals(): """All-time token totals per agent.""" - conn = sqlite3.connect(DB_PATH) + conn = _db_conn(DB_PATH) c = conn.cursor() c.execute(""" 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"]) def get_agents(): """List all registered agents with full status.""" - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) conn.row_factory = sqlite3.Row c = conn.cursor() rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall() @@ -416,7 +457,7 @@ def get_agents(): @app.route("/api/agents/", methods=["GET"]) def get_agent(agent_id): - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) conn.row_factory = sqlite3.Row c = conn.cursor() 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): """Update agent config (model, provider, status, intent).""" data = request.json - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) c = conn.cursor() fields = [] values = [] @@ -442,18 +483,18 @@ def update_agent(agent_id): values.append(datetime.now().isoformat()) values.append(agent_id) c.execute(f"UPDATE agents SET {','.join(fields)} WHERE id=?", values) - conn.commit() + _safe_commit(conn) conn.close() return jsonify({"status": "updated", "id": agent_id}) @app.route("/api/agents/", methods=["DELETE"]) def delete_agent(agent_id): """Remove agent from registry.""" - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) c = conn.cursor() c.execute("DELETE FROM agent_logs WHERE agent_id=?", (agent_id,)) c.execute("DELETE FROM agents WHERE id=?", (agent_id,)) - conn.commit() + _safe_commit(conn) conn.close() return jsonify({"status": "deleted", "id": agent_id}) @@ -463,7 +504,7 @@ def register_agent(): data = request.json if not data.get("id") or not data.get("name"): return jsonify({"error": "id and name required"}), 400 - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) c = conn.cursor() now = datetime.now().isoformat() c.execute(""" @@ -475,7 +516,7 @@ def register_agent(): data.get("provider", "unknown"), data.get("status"), now, now )) - conn.commit() + _safe_commit(conn) conn.close() return jsonify({"status": "registered", "id": data["id"]}), 201 @@ -483,7 +524,7 @@ def register_agent(): @app.route("/api/agent_metrics", methods=["GET"]) def get_agent_metrics(): """Dashboard metrics for all agents.""" - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) conn.row_factory = sqlite3.Row c = conn.cursor() rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall() @@ -516,7 +557,7 @@ def get_agent_metrics(): @app.route("/api/agent_logs/", methods=["GET"]) def get_agent_logs(agent_id): """Event log for specific agent.""" - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) conn.row_factory = sqlite3.Row c = conn.cursor() limit = request.args.get("limit", 50, type=int) @@ -531,7 +572,7 @@ def get_agent_logs(agent_id): @app.route("/api/services", methods=["GET"]) def get_services(): """List all registered services.""" - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) conn.row_factory = sqlite3.Row c = conn.cursor() rows = c.execute("SELECT * FROM services ORDER BY name").fetchall() @@ -546,7 +587,7 @@ def register_service(): return jsonify({"error": "name required"}), 400 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.execute(""" INSERT OR REPLACE INTO services (id, name, type, endpoint, status, last_check, metadata) @@ -560,7 +601,7 @@ def register_service(): datetime.now().isoformat(), json.dumps(data.get("metadata", {})) )) - conn.commit() + _safe_commit(conn) conn.close() return jsonify({"status": "registered", "id": service_id}), 201 @@ -568,7 +609,7 @@ def register_service(): def ping_service(service_id): """Health check a service endpoint.""" import requests as req - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) c = conn.cursor() row = c.execute("SELECT endpoint FROM services WHERE id=?", (service_id,)).fetchone() conn.close() @@ -582,11 +623,11 @@ def ping_service(service_id): except Exception as e: status = "unreachable" - conn = sqlite3.connect(AGENTS_DB) + conn = _db_conn(AGENTS_DB) c = conn.cursor() c.execute("UPDATE services SET status=?, last_check=? WHERE id=?", (status, datetime.now().isoformat(), service_id)) - conn.commit() + _safe_commit(conn) conn.close() return jsonify({"id": service_id, "status": status, "endpoint": endpoint}) @@ -594,7 +635,7 @@ def ping_service(service_id): @app.route("/api/pipeline/visual") def pipeline_visual(): """Real-time pipeline flow with metrics.""" - conn = sqlite3.connect(DB_PATH) + conn = _db_conn(DB_PATH) conn.row_factory = sqlite3.Row c = conn.cursor() @@ -698,5 +739,243 @@ def reset_portfolio(): except Exception as e: 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__": - app.run(host="0.0.0.0", port=3100, debug=False) \ No newline at end of file + app.run(host="0.0.0.0", port=3100, debug=False) +# ── DEBUG: Log all DB operations ───────────────────────────────────────────── diff --git a/static/script.js b/static/script.js index e7d86b0..fa983ad 100644 --- a/static/script.js +++ b/static/script.js @@ -20,6 +20,8 @@ document.addEventListener('DOMContentLoaded', () => { loadServices(); loadPipelineVisual(); loadTokenChart(); + loadConfig(); + loadMultiStratChart('live'); updateStatus('Sistema operacional', 'online'); }); @@ -605,4 +607,233 @@ function renderTokenTotals(totals) { `; 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); + } + }); } \ No newline at end of file diff --git a/static/style.css b/static/style.css index b32436f..8ca068c 100644 --- a/static/style.css +++ b/static/style.css @@ -404,4 +404,129 @@ body { .tokens-table { width: 100%; border-collapse: collapse; font-size: 13px; color: #c8d0e8; } .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 tr:hover td { background: rgba(99, 110, 200, 0.08); } \ No newline at end of file +.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; } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 3166d48..74b6746 100644 --- a/templates/index.html +++ b/templates/index.html @@ -40,17 +40,98 @@ - -
-

🏠 Escritório Virtual

-
-
- + +
+

πŸŽ›οΈ Dosagens de Trabalho Cockpit do Investidor

+
+ +
+
+ ⏱️ +

Ritmo de AnΓ‘lise

+
+

FrequΓͺncia com que a equipe avalia o mercado.

+
+ + + +
-
- OlΓ‘! Eu sou o escritΓ³rio virtual da BrainSteel Fin. + + +
+
+ πŸ›‘οΈ +

Apetite ao Risco

+
+

NΓ­vel de exigΓͺncia do CEO DΓ©cio para operar.

+
+ + + +
+
+ + +
+
+ πŸ’° +

Banca & AlocaΓ§Γ£o

+
+

Capital virtual e limite de risco por trade.

+
+ + +
+
+ + +
+
+ βš–οΈ +

BalanΓ§a de Pesos

+
+

Foco analΓ­tico: TΓ©cnica pura vs Sentimento/Macro.

+
+
+ β—„ TΓ©cnica (GrΓ‘ficos) + Sentimento (Macro) β–Ί +
+ +
EquilΓ­brio (50/50)
+
+ +
+ + +
+
+

πŸ“ˆ Performance Multi-EstratΓ©gia A Corrida do Alpha

+
+ + + + +
+
+
+ Soberana (DΓ©cio) $10.000 + Agressiva (Brief) $10.000 + HODL (BTC Puro) $10.000 + Sentimento (F&G) 50% +
+
+ +
+