🚀 Auto-deploy: Add background automated scheduler, Kelly allocation, multi-portfolio simulations, and UI improvements
This commit is contained in:
Binary file not shown.
Binary file not shown.
+50
-2
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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 ─────────────────────────────────────────────
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
loadServices();
|
loadServices();
|
||||||
loadPipelineVisual();
|
loadPipelineVisual();
|
||||||
loadTokenChart();
|
loadTokenChart();
|
||||||
|
loadConfig();
|
||||||
|
loadMultiStratChart('live');
|
||||||
updateStatus('Sistema operacional', 'online');
|
updateStatus('Sistema operacional', 'online');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -605,4 +607,233 @@ 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
+126
-1
@@ -404,4 +404,129 @@ body {
|
|||||||
.tokens-table { width: 100%; border-collapse: collapse; font-size: 13px; color: #c8d0e8; }
|
.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 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
@@ -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 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user