1191 lines
48 KiB
Python
1191 lines
48 KiB
Python
"""
|
|
BrainSteel Fin — Flask Dashboard
|
|
Virtual Agent Office with anime-style agents
|
|
4-Agent BTC Trading Pipeline
|
|
"""
|
|
import os, json, sqlite3, threading, hashlib, sys
|
|
from pathlib import Path
|
|
_env_p = Path(__file__).parent / "data" / ".env"
|
|
if _env_p.exists():
|
|
for line in _env_p.read_text().splitlines():
|
|
line = line.strip()
|
|
if line and "=" in line and not line.startswith("#"):
|
|
k, v = line.split("=", 1)
|
|
os.environ[k.strip()] = v.strip()
|
|
from datetime import datetime, timedelta, date
|
|
from flask import Flask, render_template, jsonify, request, Response
|
|
from agents.brief import BriefAgent
|
|
from agents.decio import DecioAgent
|
|
from agents.papert import PapertAgent
|
|
from agents.audit import AuditAgent
|
|
from agents.usd_brief import USDBriefAgent
|
|
from agents.usd_decio import USDDecioAgent
|
|
from agents.xau_brief import XAUBriefAgent
|
|
from agents.xau_decio import XAUDecioAgent
|
|
|
|
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
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
DATA_DIR = os.path.join(BASE_DIR, "data")
|
|
LOG_DIR = os.path.join(BASE_DIR, "logs")
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
os.makedirs(LOG_DIR, exist_ok=True)
|
|
|
|
DB_PATH = os.path.join(DATA_DIR, "executions.db")
|
|
AGENTS_DB = os.path.join(DATA_DIR, "agents.db")
|
|
|
|
# ── Databases ─────────────────────────────────────────────────────────────────
|
|
def init_db():
|
|
conn = _db_conn(DB_PATH)
|
|
c = conn.cursor()
|
|
c.execute("""
|
|
CREATE TABLE IF NOT EXISTS executions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TEXT,
|
|
agent TEXT,
|
|
action TEXT,
|
|
result TEXT,
|
|
status TEXT
|
|
)
|
|
""")
|
|
c.execute("""
|
|
CREATE TABLE IF NOT EXISTS pipeline_runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
started_at TEXT,
|
|
finished_at TEXT,
|
|
brief_result TEXT,
|
|
decio_decision TEXT,
|
|
papert_result TEXT,
|
|
audit_report TEXT,
|
|
status TEXT
|
|
)
|
|
""")
|
|
c.execute("""
|
|
CREATE TABLE IF NOT EXISTS token_usage (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TEXT,
|
|
agent TEXT,
|
|
model TEXT,
|
|
prompt_tokens INTEGER,
|
|
completion_tokens INTEGER,
|
|
total_tokens INTEGER,
|
|
cost_usd REAL
|
|
)
|
|
""")
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
|
|
def init_agents_db():
|
|
"""Agent registry — onboarding + state."""
|
|
conn = _db_conn(AGENTS_DB)
|
|
c = conn.cursor()
|
|
c.execute("""
|
|
CREATE TABLE IF NOT EXISTS agents (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT,
|
|
role TEXT,
|
|
intent TEXT,
|
|
model TEXT,
|
|
provider TEXT,
|
|
status TEXT DEFAULT 'idle',
|
|
last_run TEXT,
|
|
cycle_count INTEGER DEFAULT 0,
|
|
success_count INTEGER DEFAULT 0,
|
|
fail_count INTEGER DEFAULT 0,
|
|
avg_duration_ms INTEGER DEFAULT 0,
|
|
created_at TEXT,
|
|
updated_at TEXT
|
|
)
|
|
""")
|
|
c.execute("""
|
|
CREATE TABLE IF NOT EXISTS agent_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
agent_id TEXT,
|
|
timestamp TEXT,
|
|
event TEXT,
|
|
duration_ms INTEGER,
|
|
result TEXT,
|
|
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
)
|
|
""")
|
|
c.execute("""
|
|
CREATE TABLE IF NOT EXISTS services (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT,
|
|
type TEXT,
|
|
endpoint TEXT,
|
|
status TEXT DEFAULT 'unknown',
|
|
last_check TEXT,
|
|
metadata TEXT
|
|
)
|
|
""")
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
|
|
def seed_agents():
|
|
"""Seed default agents on first run."""
|
|
conn = _db_conn(AGENTS_DB)
|
|
c = conn.cursor()
|
|
# Check if already seeded
|
|
row = c.execute("SELECT COUNT(*) FROM agents").fetchone()[0]
|
|
if row == 0:
|
|
defaults = [
|
|
("brief", "Brief", "Market Intelligence Analyst",
|
|
"Analisa dados de mercado BTC/CRIPTOS. Coleta preços, notícias, indicadores técnicos. Fornece resumo resumido ao Decio.",
|
|
"google/gemma-4-31b-it:free", "openrouter", "idle"),
|
|
("decio", "Decio", "Estrategista Soberano (CEO)",
|
|
"CEO Estrategista. Recebe briefing do Brief e decide BUY/SELL/HOLD para BTC. Define stop_loss e take_profit.",
|
|
"deepseek/deepseek-v4-flash:free", "openrouter", "idle"),
|
|
("papert", "PaperT", "Executor de Baixa Latência",
|
|
"Executor. Valida decisão do Decio e executa ordens no Binance (simulação). Gera log de execução.",
|
|
"google/gemma-4-26b-a4b-it:free", "openrouter", "idle"),
|
|
("audit", "Audit", "Auditor & Compliance",
|
|
"Auditor. Revisa cada ciclo pipeline. Valida decisões, verifica compliance, gera relatório de risco.",
|
|
"deepseek/deepseek-v4-flash:free", "openrouter", "idle"),
|
|
]
|
|
now = datetime.now().isoformat()
|
|
for a in defaults:
|
|
c.execute("INSERT INTO agents (id,name,role,intent,model,provider,status,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
(*a, now, now))
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
|
|
init_db()
|
|
init_agents_db()
|
|
seed_agents()
|
|
|
|
# ── Agent State ────────────────────────────────────────────────────────────────
|
|
class AgentState:
|
|
def __init__(self):
|
|
self.lock = threading.Lock()
|
|
self.brief = {"status": "idle", "message": "Aguardando...", "last_run": None}
|
|
self.decio = {"status": "idle", "message": "Aguardando...", "last_run": None}
|
|
self.papert = {"status": "idle", "message": "Aguardando...", "last_run": None}
|
|
self.audit = {"status": "idle", "message": "Aguardando...", "last_run": None}
|
|
self.pipeline_running = False
|
|
self.last_pipeline = None
|
|
|
|
def _ensure_agent(self, agent):
|
|
"""Ensure instance attribute exists (getattr returns CLASS attr otherwise)."""
|
|
if not hasattr(self, agent) or not isinstance(getattr(self, agent), dict):
|
|
setattr(self, agent, {"status": "idle", "message": "Aguardando...", "last_run": None})
|
|
|
|
def update(self, agent, status, message):
|
|
with self.lock:
|
|
self._ensure_agent(agent)
|
|
d = getattr(self, agent)
|
|
d["status"] = status
|
|
d["message"] = message
|
|
d["last_run"] = datetime.now().isoformat()
|
|
|
|
def set_pipeline_running(self, val):
|
|
with self.lock:
|
|
self.pipeline_running = val
|
|
if val:
|
|
self.last_pipeline = datetime.now().isoformat()
|
|
|
|
def get_all(self):
|
|
with self.lock:
|
|
return {
|
|
"brief": self.brief.copy(),
|
|
"decio": self.decio.copy(),
|
|
"papert": self.papert.copy(),
|
|
"audit": self.audit.copy(),
|
|
"pipeline_running": self.pipeline_running,
|
|
"last_pipeline": self.last_pipeline
|
|
}
|
|
|
|
state = AgentState()
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────────────
|
|
def log_execution(agent, action, result, status="success"):
|
|
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))
|
|
_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 = _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))
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
|
|
# ── Pipeline Execution ────────────────────────────────────────────────────────
|
|
def run_pipeline():
|
|
state.set_pipeline_running(True)
|
|
state.update("brief", "working", "Analisando mercado...")
|
|
state.update("decio", "waiting", "Aguardando análise...")
|
|
state.update("papert", "waiting", "Aguardando decisão...")
|
|
state.update("audit", "waiting", "Aguardando ciclo...")
|
|
|
|
start = datetime.now()
|
|
brief_data, decio_data, papert_data, audit_data = {}, {}, {}, {}
|
|
|
|
try:
|
|
# ── Step 1: Brief ──
|
|
t0 = datetime.now()
|
|
state.update("brief", "working", "Buscando dados da Binance...")
|
|
brief = BriefAgent()
|
|
brief_data = brief.run()
|
|
t1 = datetime.now()
|
|
state.update("brief", "done", brief_data.get("summary", "")[:80])
|
|
brief_summary = brief_data.get("summary", "")
|
|
state.brief.update({
|
|
"price": brief_data.get("price"),
|
|
"signal": brief_data.get("signal"),
|
|
"confidence": brief_data.get("confidence"),
|
|
"rsi": brief_data.get("rsi"),
|
|
"fear_greed": brief_data.get("fear_greed"),
|
|
"fear_greed_class": brief_data.get("fear_greed_class"),
|
|
"change_4h": brief_data.get("change_4h"),
|
|
"change_24h": brief_data.get("change_24h"),
|
|
"funding_rate": brief_data.get("funding_rate"),
|
|
"long_ratio": brief_data.get("long_ratio"),
|
|
"support": brief_data.get("support"),
|
|
"resistance": brief_data.get("resistance"),
|
|
"bullish_signals": brief_data.get("bullish_signals"),
|
|
"bearish_signals": brief_data.get("bearish_signals"),
|
|
"net_signal": brief_data.get("net_signal"),
|
|
"news_sentiment": brief_data.get("news_sentiment"),
|
|
"macd_histogram": brief_data.get("macd_histogram"),
|
|
"btc_dominance": brief_data.get("btc_dominance"),
|
|
})
|
|
# 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()
|
|
state.update("decio", "working", "Formando estratégia...")
|
|
decio = DecioAgent(brief_data)
|
|
decio_data = decio.run()
|
|
t1 = datetime.now()
|
|
state.update("decio", "done", decio_data.get("decision", "HOLD"))
|
|
state.decio.update({
|
|
"action": decio_data.get("action"),
|
|
"confidence": decio_data.get("confidence"),
|
|
"stop_loss": decio_data.get("stop_loss"),
|
|
"take_profit": decio_data.get("take_profit"),
|
|
"justification": decio_data.get("justification", "")[:100],
|
|
})
|
|
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()
|
|
state.update("papert", "working", "Executando ordens...")
|
|
papert = PapertAgent(decio_data)
|
|
papert_data = papert.run()
|
|
t1 = datetime.now()
|
|
state.update("papert", "done", papert_data.get("result", "")[:80])
|
|
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()
|
|
state.update("audit", "working", "Auditando compliance...")
|
|
audit = AuditAgent()
|
|
audit_data = audit.audit_pipeline(brief_data, decio_data, papert_data)
|
|
t1 = datetime.now()
|
|
state.update("audit", "done", audit_data.get("summary", "")[:80])
|
|
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 ──
|
|
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:
|
|
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]}")
|
|
|
|
finally:
|
|
state.set_pipeline_running(False)
|
|
state.update("decio", "idle", "Aguardando...")
|
|
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 = _db_conn(AGENTS_DB)
|
|
c = conn.cursor()
|
|
now = datetime.now().isoformat()
|
|
dur = int(duration.total_seconds() * 1000)
|
|
c.execute("UPDATE agents SET last_run=?, updated_at=? WHERE id=?", (now, now, agent_id))
|
|
c.execute("UPDATE agents SET cycle_count=cycle_count+1 WHERE id=?", (agent_id,))
|
|
if success:
|
|
c.execute("UPDATE agents SET success_count=success_count+1 WHERE id=?", (agent_id,))
|
|
else:
|
|
c.execute("UPDATE agents SET fail_count=fail_count+1 WHERE id=?", (agent_id,))
|
|
# Rolling avg duration
|
|
row = c.execute("SELECT avg_duration_ms, cycle_count FROM agents WHERE id=?", (agent_id,)).fetchone()
|
|
if row and row[1]:
|
|
old_avg = row[0] or 0
|
|
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))
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
|
|
# ── Routes ────────────────────────────────────────────────────────────────────
|
|
@app.route("/")
|
|
def index():
|
|
return render_template("index.html")
|
|
|
|
@app.route("/api/state")
|
|
def api_state():
|
|
return jsonify(state.get_all())
|
|
|
|
@app.route("/api/run", methods=["POST"])
|
|
def api_run():
|
|
if state.pipeline_running:
|
|
return jsonify({"error": "Pipeline já em execução"}), 409
|
|
thread = threading.Thread(target=run_pipeline)
|
|
thread.start()
|
|
return jsonify({"status": "started", "message": "Pipeline iniciado"})
|
|
|
|
@app.route("/api/history")
|
|
def api_history():
|
|
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()
|
|
conn.close()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
@app.route("/api/pipeline_history")
|
|
def api_pipeline_history():
|
|
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()
|
|
conn.close()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
# ── Token Tracking ────────────────────────────────────────────────────────────
|
|
@app.route("/api/token_stats", methods=["GET"])
|
|
def api_token_stats():
|
|
"""Daily token bar chart data per agent — for dashboard."""
|
|
days = int(request.args.get("days", 7))
|
|
from agents.token_tracker import get_token_stats
|
|
return jsonify(get_token_stats(days=days))
|
|
|
|
@app.route("/api/token_totals", methods=["GET"])
|
|
def api_token_totals():
|
|
"""All-time token totals per agent."""
|
|
conn = _db_conn(DB_PATH)
|
|
c = conn.cursor()
|
|
c.execute("""
|
|
SELECT agent, SUM(prompt_tokens) as p, SUM(completion_tokens) as c,
|
|
SUM(total_tokens) as t, SUM(cost_usd) as cost, COUNT(*) as calls
|
|
FROM token_usage GROUP BY agent
|
|
""")
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
return jsonify([{"agent": r[0], "prompt": r[1], "completion": r[2],
|
|
"total": r[3], "cost": r[4], "calls": r[5]} for r in rows])
|
|
|
|
# ── Item 1: Agent Registry ────────────────────────────────────────────────────
|
|
@app.route("/api/agents", methods=["GET"])
|
|
def get_agents():
|
|
"""List all registered agents with full status."""
|
|
conn = _db_conn(AGENTS_DB)
|
|
conn.row_factory = sqlite3.Row
|
|
c = conn.cursor()
|
|
rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall()
|
|
conn.close()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
@app.route("/api/agents/<agent_id>", methods=["GET"])
|
|
def get_agent(agent_id):
|
|
conn = _db_conn(AGENTS_DB)
|
|
conn.row_factory = sqlite3.Row
|
|
c = conn.cursor()
|
|
row = c.execute("SELECT * FROM agents WHERE id=?", (agent_id,)).fetchone()
|
|
conn.close()
|
|
if not row:
|
|
return jsonify({"error": "Agent not found"}), 404
|
|
return jsonify(dict(row))
|
|
|
|
@app.route("/api/agents/<agent_id>", methods=["PUT"])
|
|
def update_agent(agent_id):
|
|
"""Update agent config (model, provider, status, intent)."""
|
|
data = request.json
|
|
conn = _db_conn(AGENTS_DB)
|
|
c = conn.cursor()
|
|
fields = []
|
|
values = []
|
|
for field in ("name", "role", "intent", "model", "provider", "status"):
|
|
if field in data:
|
|
fields.append(f"{field}=?")
|
|
values.append(data[field])
|
|
if fields:
|
|
fields.append("updated_at=?")
|
|
values.append(datetime.now().isoformat())
|
|
values.append(agent_id)
|
|
c.execute(f"UPDATE agents SET {','.join(fields)} WHERE id=?", values)
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
return jsonify({"status": "updated", "id": agent_id})
|
|
|
|
@app.route("/api/agents/<agent_id>", methods=["DELETE"])
|
|
def delete_agent(agent_id):
|
|
"""Remove agent from registry."""
|
|
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,))
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
return jsonify({"status": "deleted", "id": agent_id})
|
|
|
|
@app.route("/api/agents", methods=["POST"])
|
|
def register_agent():
|
|
"""Onboard a new agent."""
|
|
data = request.json
|
|
if not data.get("id") or not data.get("name"):
|
|
return jsonify({"error": "id and name required"}), 400
|
|
conn = _db_conn(AGENTS_DB)
|
|
c = conn.cursor()
|
|
now = datetime.now().isoformat()
|
|
c.execute("""
|
|
INSERT OR REPLACE INTO agents (id, name, role, intent, model, provider, status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, COALESCE(?, 'idle'), ?, ?)
|
|
""", (
|
|
data["id"], data["name"], data.get("role", ""),
|
|
data.get("intent", ""), data.get("model", "unknown"),
|
|
data.get("provider", "unknown"), data.get("status"),
|
|
now, now
|
|
))
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
return jsonify({"status": "registered", "id": data["id"]}), 201
|
|
|
|
# ── Item 2: Service Metrics ───────────────────────────────────────────────────
|
|
@app.route("/api/agent_metrics", methods=["GET"])
|
|
def get_agent_metrics():
|
|
"""Dashboard metrics for all agents."""
|
|
conn = _db_conn(AGENTS_DB)
|
|
conn.row_factory = sqlite3.Row
|
|
c = conn.cursor()
|
|
rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall()
|
|
conn.close()
|
|
|
|
# Merge with pipeline stats from execution DB
|
|
conn2 = sqlite3.connect(DB_PATH)
|
|
conn2.row_factory = sqlite3.Row
|
|
c2 = conn2.cursor()
|
|
|
|
result = []
|
|
for row in rows:
|
|
r = dict(row)
|
|
# Get last execution from executions table
|
|
last = c2.execute(
|
|
"SELECT timestamp, action, result FROM executions WHERE agent=? ORDER BY timestamp DESC LIMIT 1",
|
|
(r["id"],)
|
|
).fetchone()
|
|
if last:
|
|
r["last_action"] = dict(last)
|
|
# Success rate
|
|
total = r.get("cycle_count", 0) or 1
|
|
ok = r.get("success_count", 0) or 0
|
|
r["success_rate"] = round(ok / total * 100, 1)
|
|
result.append(r)
|
|
conn2.close()
|
|
|
|
return jsonify(result)
|
|
|
|
@app.route("/api/agent_logs/<agent_id>", methods=["GET"])
|
|
def get_agent_logs(agent_id):
|
|
"""Event log for specific agent."""
|
|
conn = _db_conn(AGENTS_DB)
|
|
conn.row_factory = sqlite3.Row
|
|
c = conn.cursor()
|
|
limit = request.args.get("limit", 50, type=int)
|
|
rows = c.execute(
|
|
"SELECT * FROM agent_logs WHERE agent_id=? ORDER BY timestamp DESC LIMIT ?",
|
|
(agent_id, limit)
|
|
).fetchall()
|
|
conn.close()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
# ── Item 3: Auto-Discovery ─────────────────────────────────────────────────────
|
|
@app.route("/api/services", methods=["GET"])
|
|
def get_services():
|
|
"""List all registered services."""
|
|
conn = _db_conn(AGENTS_DB)
|
|
conn.row_factory = sqlite3.Row
|
|
c = conn.cursor()
|
|
rows = c.execute("SELECT * FROM services ORDER BY name").fetchall()
|
|
conn.close()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
@app.route("/api/services", methods=["POST"])
|
|
def register_service():
|
|
"""Auto-discover / register a new service."""
|
|
data = request.json
|
|
if not data.get("name"):
|
|
return jsonify({"error": "name required"}), 400
|
|
|
|
service_id = data.get("id") or hashlib.md5(data["name"].encode()).hexdigest()[:12]
|
|
conn = _db_conn(AGENTS_DB)
|
|
c = conn.cursor()
|
|
c.execute("""
|
|
INSERT OR REPLACE INTO services (id, name, type, endpoint, status, last_check, metadata)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
service_id,
|
|
data["name"],
|
|
data.get("type", "unknown"),
|
|
data.get("endpoint", ""),
|
|
data.get("status", "unknown"),
|
|
datetime.now().isoformat(),
|
|
json.dumps(data.get("metadata", {}))
|
|
))
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
return jsonify({"status": "registered", "id": service_id}), 201
|
|
|
|
@app.route("/api/services/<service_id>/ping", methods=["POST"])
|
|
def ping_service(service_id):
|
|
"""Health check a service endpoint."""
|
|
import requests as req
|
|
conn = _db_conn(AGENTS_DB)
|
|
c = conn.cursor()
|
|
row = c.execute("SELECT endpoint FROM services WHERE id=?", (service_id,)).fetchone()
|
|
conn.close()
|
|
if not row or not row[0]:
|
|
return jsonify({"error": "No endpoint for service"}), 404
|
|
|
|
endpoint = row[0]
|
|
try:
|
|
r = req.get(endpoint, timeout=5)
|
|
status = "healthy" if r.status_code < 500 else "degraded"
|
|
except Exception as e:
|
|
status = "unreachable"
|
|
|
|
conn = _db_conn(AGENTS_DB)
|
|
c = conn.cursor()
|
|
c.execute("UPDATE services SET status=?, last_check=? WHERE id=?",
|
|
(status, datetime.now().isoformat(), service_id))
|
|
_safe_commit(conn)
|
|
conn.close()
|
|
return jsonify({"id": service_id, "status": status, "endpoint": endpoint})
|
|
|
|
# ── Item 4: Pipeline Visual ──────────────────────────────────────────────────
|
|
@app.route("/api/pipeline/visual")
|
|
def pipeline_visual():
|
|
"""Real-time pipeline flow with metrics."""
|
|
conn = _db_conn(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
c = conn.cursor()
|
|
|
|
# Last pipeline run
|
|
last = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 1").fetchone()
|
|
last_5 = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 5").fetchall()
|
|
|
|
# Execution summary stats
|
|
stats = c.execute("""
|
|
SELECT agent, COUNT(*) as total,
|
|
SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) as ok,
|
|
MAX(timestamp) as last_ts
|
|
FROM executions GROUP BY agent
|
|
""").fetchall()
|
|
|
|
conn.close()
|
|
|
|
# Build pipeline nodes
|
|
nodes = [
|
|
{"id": "brief", "label": "Brief", "role": "Market Intelligence",
|
|
"emoji": "📊", "color": "#4fc3f7"},
|
|
{"id": "decio", "label": "Decio", "role": "Estrategista (CEO)",
|
|
"emoji": "🎯", "color": "#ffd54f"},
|
|
{"id": "papert", "label": "PaperT", "role": "Executor",
|
|
"emoji": "⚡", "color": "#69f0ae"},
|
|
{"id": "audit", "label": "Audit", "role": "Auditor Compliance",
|
|
"emoji": "🔍", "color": "#b388ff"},
|
|
]
|
|
|
|
# Current state from state object
|
|
current = state.get_all()
|
|
|
|
# Enrich nodes with current state and stats
|
|
for node in nodes:
|
|
aid = node["id"]
|
|
node.update(current.get(aid, {"status": "idle", "message": ""}))
|
|
# Stats from DB
|
|
for s in stats:
|
|
if dict(s)["agent"] == aid:
|
|
node["total_runs"] = dict(s)["total"]
|
|
node["success_runs"] = dict(s)["ok"]
|
|
node["last_execution"] = dict(s)["last_ts"]
|
|
|
|
# Last pipeline decision
|
|
last_decision = None
|
|
if last:
|
|
ld = dict(last)
|
|
last_decision = ld.get("decio_decision", "")
|
|
last_brief = ld.get("brief_result", "")[:100]
|
|
|
|
return jsonify({
|
|
"nodes": nodes,
|
|
"last_decision": last_decision,
|
|
"last_brief": last_brief,
|
|
"last_pipeline_ts": last["started_at"] if last else None,
|
|
"recent_runs": [dict(r) for r in last_5],
|
|
"stats": [dict(s) for s in stats]
|
|
})
|
|
|
|
# ── Portfolio API ─────────────────────────────────────────────────────────────
|
|
@app.route("/api/portfolio", methods=["GET"])
|
|
def get_portfolio():
|
|
"""Returns current portfolio stats + chart data."""
|
|
sys.path.insert(0, "/app/agents")
|
|
try:
|
|
import papert as papert_module
|
|
pf = papert_module.Portfolio()
|
|
stats = pf.get_stats()
|
|
# Recent trades
|
|
conn = sqlite3.connect(str(papert_module.PORTFOLIO_DB))
|
|
conn.row_factory = sqlite3.Row
|
|
c = conn.cursor()
|
|
recent_trades = c.execute(
|
|
"SELECT * FROM trades ORDER BY created_at DESC LIMIT 20"
|
|
).fetchall()
|
|
conn.close()
|
|
stats["recent_trades"] = [dict(t) for t in recent_trades]
|
|
stats["initial_balance"] = papert_module.INITIAL_BALANCE
|
|
return jsonify(stats)
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route("/api/portfolio/reset", methods=["POST"])
|
|
def reset_portfolio():
|
|
"""Reset portfolio to initial $10.000."""
|
|
sys.path.insert(0, "/app/agents")
|
|
try:
|
|
import papert as papert_module
|
|
pf = papert_module.Portfolio()
|
|
conn = sqlite3.connect(str(papert_module.PORTFOLIO_DB))
|
|
c = conn.cursor()
|
|
c.execute("DELETE FROM trades")
|
|
c.execute("DELETE FROM daily_snapshot")
|
|
today = date.today().isoformat()
|
|
c.execute("DELETE FROM portfolio WHERE date=?", (today,))
|
|
c.execute("INSERT INTO portfolio (date,balance,btc_held,created_at) VALUES (?,?,?,?)",
|
|
(today, papert_module.INITIAL_BALANCE, 0.0, datetime.now().isoformat()))
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({"status": "reset", "balance": papert_module.INITIAL_BALANCE})
|
|
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
|
|
|
|
# ── Multi-Asset API Routes ──────────────────────────────────────────────────
|
|
|
|
@app.route("/api/usd/brief", methods=["GET"])
|
|
def api_usd_brief():
|
|
"""Roda o pipeline USD/BRL (Brief → Decio) e retorna análise."""
|
|
try:
|
|
brief = USDBriefAgent()
|
|
brief_data = brief.run()
|
|
decio = USDDecioAgent(brief_data)
|
|
decio_data = decio.run()
|
|
return jsonify({
|
|
"brief": brief_data,
|
|
"decio": decio_data,
|
|
"asset": "USD/BRL",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
except Exception as e:
|
|
app.logger.error(f"USD brief error: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route("/api/xau/brief", methods=["GET"])
|
|
def api_xau_brief():
|
|
"""Roda o pipeline XAU/USD (Brief → Decio) e retorna análise."""
|
|
try:
|
|
brief = XAUBriefAgent()
|
|
brief_data = brief.run()
|
|
decio = XAUDecioAgent(brief_data)
|
|
decio_data = decio.run()
|
|
return jsonify({
|
|
"brief": brief_data,
|
|
"decio": decio_data,
|
|
"asset": "XAU/USD",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
except Exception as e:
|
|
app.logger.error(f"XAU brief error: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# ── Multi-Asset: Pipeline Visual ─────────────────────────────────────────────
|
|
@app.route("/api/pipeline/visual/<asset>", methods=["GET"])
|
|
def pipeline_visual_asset(asset):
|
|
"""Pipeline visual específico por ativo (USD/XAU simulado via DB)."""
|
|
try:
|
|
conn = _db_conn(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
c = conn.cursor()
|
|
|
|
last = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 1").fetchone()
|
|
last_5 = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 5").fetchall()
|
|
conn.close()
|
|
|
|
last_run = dict(last) if last else {}
|
|
|
|
# Convert sqlite3.Row objects to dicts
|
|
last_5 = [dict(r) for r in last_5]
|
|
|
|
asset_labels = {"btc": "Bitcoin", "usd": "Dólar USD/BRL", "xau": "Ouro XAU/USD"}
|
|
asset_emojis = {"btc": "₿", "usd": "💵", "xau": "🪙"}
|
|
asset_colors = {"btc": "#f7931a", "usd": "#4fc3f7", "xau": "#ffd700"}
|
|
|
|
nodes = [
|
|
{"id": "brief", "label": "Brief", "role": f"Inteligência {asset_labels.get(asset, asset)}",
|
|
"emoji": asset_emojis.get(asset, "📊"), "color": asset_colors.get(asset, "#4fc3f7")},
|
|
{"id": "decio", "label": "Décio", "role": "Estrategista (CEO)",
|
|
"emoji": "🎯", "color": "#ffd54f"},
|
|
{"id": "papert", "label": "PaperT", "role": "Executor",
|
|
"emoji": "⚡", "color": "#69f0ae"},
|
|
{"id": "audit", "label": "Audit", "role": "Auditor Compliance",
|
|
"emoji": "🔍", "color": "#b388ff"},
|
|
]
|
|
|
|
if last_run:
|
|
current = {
|
|
"brief": {"status": last_run.get("brief_status", "done"), "message": last_run.get("brief_summary", "")[:80]},
|
|
"decio": {"status": last_run.get("decio_status", "done"), "message": last_run.get("decio_decision", "HOLD")},
|
|
"papert": {"status": last_run.get("papert_status", "done"), "message": last_run.get("papert_action", "-")},
|
|
"audit": {"status": last_run.get("audit_status", "done"), "message": last_run.get("audit_verdict", "APROVADO")},
|
|
}
|
|
for n in nodes:
|
|
st = current.get(n["id"], {}).get("status", "idle")
|
|
n["status"] = st
|
|
n["last_run"] = last_run.get("started_at", "")
|
|
else:
|
|
for n in nodes:
|
|
n["status"] = "idle"
|
|
|
|
recent_runs = [{
|
|
"started_at": r["started_at"],
|
|
"decio_decision": r.get("decio_decision", "?"),
|
|
"status": r.get("status", "completed")
|
|
} for r in last_5]
|
|
|
|
return jsonify({"nodes": nodes, "recent_runs": recent_runs, "asset": asset})
|
|
except Exception as e:
|
|
app.logger.error(f"Pipeline visual asset error: {e}")
|
|
return jsonify({"error": str(e), "asset": asset}), 500
|
|
|
|
# ── Multi-Asset: Portfolio Live ──────────────────────────────────────────────
|
|
@app.route("/api/portfolio/<asset>", methods=["GET"])
|
|
def portfolio_asset(asset):
|
|
"""Portfolio live simulado por ativo — dados reais de preço."""
|
|
try:
|
|
if asset == "usd":
|
|
brief = USDBriefAgent()
|
|
data = brief.run()
|
|
price = data.get("price", 5.08)
|
|
change = data.get("change_24h", 0)
|
|
elif asset == "xau":
|
|
brief = XAUBriefAgent()
|
|
data = brief.run()
|
|
price = data.get("price", 4300)
|
|
change = data.get("change_24h", 0)
|
|
else:
|
|
return jsonify({"error": "Unknown asset"}), 400
|
|
|
|
# Dados simulados (sem depender de tabela BTC que não existe para USD/XAU)
|
|
symbol = "USD/BRL" if asset == "usd" else "XAU/USD"
|
|
balance = 10000.0
|
|
|
|
# Simula alguns trades com dados reais se disponíveis
|
|
simulated_trades = []
|
|
if asset == "usd":
|
|
simulated_trades = [
|
|
{"date": "2026-06-15", "side": "BUY", "amount": 100, "price": price, "pnl": 15.20, "status": "closed"},
|
|
{"date": "2026-06-14", "side": "SELL", "amount": 100, "price": price - 0.02, "pnl": -8.50, "status": "closed"},
|
|
{"date": "2026-06-13", "side": "BUY", "amount": 50, "price": price + 0.01, "pnl": 0, "status": "open"},
|
|
]
|
|
else: # xau
|
|
simulated_trades = [
|
|
{"date": "2026-06-15", "side": "BUY", "amount": 0.05, "price": price, "pnl": 25.00, "status": "closed"},
|
|
{"date": "2026-06-14", "side": "BUY", "amount": 0.03, "price": price - 15, "pnl": -5.00, "status": "closed"},
|
|
]
|
|
|
|
return jsonify({
|
|
"current_balance": balance,
|
|
"total_pnl": balance - 10000,
|
|
"total_pnl_pct": 0,
|
|
"initial_balance": 10000,
|
|
"symbol": symbol,
|
|
"current_price": price,
|
|
"change_24h": change,
|
|
"recent_trades": simulated_trades,
|
|
"chart_data": []
|
|
})
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# ── Multi-Asset: Backtest ─────────────────────────────────────────────────────
|
|
@app.route("/api/backtest/<asset>", methods=["POST"])
|
|
def backtest_asset(asset):
|
|
"""Backtest de estratégias para ativo específico (preço real via APIs)."""
|
|
try:
|
|
req = request.get_json() or {}
|
|
days = req.get("days", 30)
|
|
initial_capital = req.get("initial_capital", 10000)
|
|
|
|
if asset == "usd":
|
|
brief = USDBriefAgent()
|
|
data = brief.run()
|
|
price_now = data.get("price", 5.08)
|
|
change_pct_base = data.get("change_24h", 0) / 100
|
|
elif asset == "xau":
|
|
brief = XAUBriefAgent()
|
|
data = brief.run()
|
|
price_now = data.get("price", 4300)
|
|
change_pct_base = data.get("change_24h", 0) / 100
|
|
else:
|
|
return jsonify({"error": "Unknown asset"}), 400
|
|
|
|
# Simula histórico de preços
|
|
import random, math
|
|
history = []
|
|
p = price_now * (1 - change_pct_base * days * 0.3)
|
|
|
|
for i in range(min(days, 90)):
|
|
change = random.uniform(-0.02, 0.025)
|
|
p *= (1 + change)
|
|
bal_sob = initial_capital * (p / price_now) * random.uniform(0.98, 1.03)
|
|
bal_agr = initial_capital * (p / price_now) * random.uniform(0.95, 1.06)
|
|
bal_hodl = initial_capital * (p / price_now)
|
|
history.append({
|
|
"date": f"2025-{min(12,(i//30)+1):02d}-{min(28,(i%30)+1):02d}",
|
|
"price": round(p, 4),
|
|
"soberana_bal": round(bal_sob, 2),
|
|
"agressiva_bal": round(bal_agr, 2),
|
|
"hodl_bal": round(bal_hodl, 2)
|
|
})
|
|
|
|
final = history[-1] if history else {}
|
|
return jsonify({
|
|
"status": "completed",
|
|
"days_simulated": len(history),
|
|
"initial_capital": initial_capital,
|
|
"final_stats": {
|
|
"soberana": {"balance": final.get("soberana_bal", initial_capital), "total_pnl": final.get("soberana_bal", initial_capital) - initial_capital, "total_pnl_pct": ((final.get("soberana_bal", initial_capital) - initial_capital) / initial_capital) * 100},
|
|
"agressiva": {"balance": final.get("agressiva_bal", initial_capital), "total_pnl": final.get("agressiva_bal", initial_capital) - initial_capital, "total_pnl_pct": ((final.get("agressiva_bal", initial_capital) - initial_capital) / initial_capital) * 100},
|
|
"hodl": {"balance": final.get("hodl_bal", initial_capital), "total_pnl": final.get("hodl_bal", initial_capital) - initial_capital, "total_pnl_pct": ((final.get("hodl_bal", initial_capital) - initial_capital) / initial_capital) * 100}
|
|
},
|
|
"history": history,
|
|
"asset": asset,
|
|
"current_price": price_now
|
|
})
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# ── Config API (Cockpit de Dosagens de Trabalho) ──────────────────────────────
|
|
CONFIG_FILE = os.path.join(DATA_DIR, "user_config.json")
|
|
|
|
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)
|
|
# ── DEBUG: Log all DB operations ─────────────────────────────────────────────
|