commit 50960bab216e1d442675a0275ace57797603d9fd Author: Hermes - BrainSteel VPS Date: Sun May 17 16:25:55 2026 +0000 BrainSteel Fin v1.0 — 3-agent BTC trading pipeline - Brief: Market Intelligence (Binance data + LLM analysis) - Decio: Strategy decision (BUY/HOLD/SELL) - PaperT: Order executor (Binance API) - Anime-style Flask dashboard - Traefik-ready Docker deployment diff --git a/.env b/.env new file mode 100644 index 0000000..798f7fd --- /dev/null +++ b/.env @@ -0,0 +1 @@ +OPENROUTER_API_KEY=sk-or-v1-26c1cbfbe091551a814874f8141a2739b70deef89f464be668fdcac49cfabecd7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eeb2ec7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# BrainSteel Fin - .gitignore +__pycache__/ +*.pyc +*.pyo +venv/ +.venv/ +*.db +*.log +.env +data/*.db +data/.env +logs/*.log +*.egg-info/ +dist/ +build/ +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7d23cc4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir flask requests +COPY . . +EXPOSE 3100 +CMD ["python", "app.py"] diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000..a79f4dc --- /dev/null +++ b/agents/__init__.py @@ -0,0 +1 @@ +# BrainSteel Fin Agents \ No newline at end of file diff --git a/agents/__pycache__/__init__.cpython-313.pyc b/agents/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d5b729e Binary files /dev/null and b/agents/__pycache__/__init__.cpython-313.pyc differ diff --git a/agents/__pycache__/audit.cpython-313.pyc b/agents/__pycache__/audit.cpython-313.pyc new file mode 100644 index 0000000..cc957ea Binary files /dev/null and b/agents/__pycache__/audit.cpython-313.pyc differ diff --git a/agents/__pycache__/brief.cpython-313.pyc b/agents/__pycache__/brief.cpython-313.pyc new file mode 100644 index 0000000..65faad2 Binary files /dev/null and b/agents/__pycache__/brief.cpython-313.pyc differ diff --git a/agents/__pycache__/decio.cpython-313.pyc b/agents/__pycache__/decio.cpython-313.pyc new file mode 100644 index 0000000..6d86b1f Binary files /dev/null and b/agents/__pycache__/decio.cpython-313.pyc differ diff --git a/agents/__pycache__/papert.cpython-313.pyc b/agents/__pycache__/papert.cpython-313.pyc new file mode 100644 index 0000000..301a0e4 Binary files /dev/null and b/agents/__pycache__/papert.cpython-313.pyc differ diff --git a/agents/audit.py b/agents/audit.py new file mode 100644 index 0000000..c2acf6f --- /dev/null +++ b/agents/audit.py @@ -0,0 +1,173 @@ +""" +BrainSteel Fin — Audit Agent (v2) +5-Pillar Compliance Auditor +Reviews pipeline against all 5 analysis pillars. +""" +import os, json, requests +from datetime import datetime + +class AuditAgent: + def __init__(self): + self.name = "Audit" + + def audit_pipeline(self, brief_data, decio_data, papert_data): + checks = [] + + # Check 1: Brief delivered all 3 required points + briefing = brief_data.get("briefing_for_decio", "") + has_price = "BTC $" in briefing or "$" in briefing + has_change = any(x in briefing for x in ["4h:", "24h:", "+", "-"]) + has_sr = "Sup:" in briefing and "Res:" in briefing + checks.append({ + "check": "Brief 3 pontos", + "passed": has_price and has_change and has_sr, + "detail": f"price={has_price} change={has_change} sr={has_sr}" + }) + + # Check 2: Brief signal was computed correctly + brief_conf = brief_data.get("confidence", 0) + brief_signal = brief_data.get("signal", "NEUTRO") + has_rsi = brief_data.get("rsi") is not None + has_fg = brief_data.get("fear_greed") is not None + checks.append({ + "check": "Brief pillars completos", + "passed": has_rsi and has_fg, + "detail": f"RSI={brief_data.get('rsi')} F&G={brief_data.get('fear_greed')} news={brief_data.get('news_sentiment','?')}" + }) + + # Check 3: Decio respected 75% rule + decio_action = decio_data.get("action", "HOLD") + decio_conf = decio_data.get("confidence", 0) + sl = decio_data.get("stop_loss", 0) + tp = decio_data.get("take_profit", 0) + + respected_75 = True + if decio_conf < 75 and decio_action != "HOLD": + respected_75 = False + checks.append({ + "check": "Decio regra 75%", + "passed": respected_75, + "detail": f"conf={decio_conf}% action={decio_action}" + }) + + # Check 4: Decio SL/TP correct if BUY/SELL + price = brief_data.get("price", 0) + if decio_action in ("BUY", "SELL"): + sl_correct = (decio_action == "BUY" and abs(sl - price * 0.98) < price * 0.01) or \ + (decio_action == "SELL" and abs(sl - price * 1.02) < price * 0.01) + tp_correct = (decio_action == "BUY" and abs(tp - price * 1.05) < price * 0.01) or \ + (decio_action == "SELL" and abs(tp - price * 0.95) < price * 0.01) + else: + sl_correct = (sl == 0 and tp == 0) + checks.append({ + "check": "Decio SL/TP 2%/5%", + "passed": sl_correct, + "detail": f"SL=${sl} TP=${tp} price=${price}" + }) + + # Check 5: PaperT logged correctly + papert_logged = papert_data.get("log_appended", False) + papert_status = papert_data.get("status", "") + checks.append({ + "check": "PaperT executou log", + "passed": papert_logged and papert_status == "validated", + "detail": f"logged={papert_logged} status={papert_status}" + }) + + # Check 6: Portfolio updated if BUY/SELL + portfolio = papert_data.get("portfolio", {}) + if decio_action in ("BUY", "SELL"): + pf_updated = portfolio.get("total_trades", 0) > 0 + else: + pf_updated = True # No trade = no portfolio change needed + checks.append({ + "check": "Portfolio atualizado", + "passed": pf_updated, + "detail": f"trades={portfolio.get('total_trades',0)} balance=${portfolio.get('current_balance','?')}" + }) + + # Score + passed_count = sum(1 for c in checks if c["passed"]) + total = len(checks) + score = round(passed_count / total * 100, 1) + + # Signal quality assessment + net = brief_data.get("net_signal", 0) + bullish = brief_data.get("bullish_signals", 0) + bearish = brief_data.get("bearish_signals", 0) + rsi = brief_data.get("rsi", 50) + + quality_notes = [] + if rsi > 70: quality_notes.append("RSI overbought — cautela") + if rsi < 30: quality_notes.append("RSI oversold — oportunidade") + if net > 2: quality_notes.append(f"Sinal bullish forte (net={net})") + if net < -2: quality_notes.append(f"Sinal bearish forte (net={net})") + if decio_action == "HOLD" and brief_data.get("confidence", 0) < 75: + quality_notes.append("HOLD correto — confiança baixa preservou capital") + + return { + "audit_id": f"AUD-{datetime.now().strftime('%Y%m%d%H%M%S')}", + "timestamp": datetime.now().isoformat(), + "checks": checks, + "compliance_score": score, + "status": "approved" if score >= 83 else "flagged" if score >= 66 else "rejected", + "passed_count": passed_count, + "total_checks": total, + "quality_notes": quality_notes, + "brief_summary": brief_data.get("summary", "")[:100], + "decio_decision": decio_action, + "decio_confidence": decio_conf, + "portfolio_balance": portfolio.get("current_balance", 0), + "portfolio_pnl": portfolio.get("total_pnl", 0), + "portfolio_pnl_pct": portfolio.get("total_pnl_pct", 0), + } + + def generate_llm_report(self, brief_data, decio_data): + if not os.getenv("PAPERT_API_KEY") and not os.getenv("OPENROUTER_API_KEY", ""): + return {"report": "OpenRouter key not available", "status": "local_only"} + try: + prompt = f"""Você é o Audit, auditor de compliance da BrainSteel Fin. +Gere um relatório de auditoria em português,分析昨日操作质量: + +Brief: {brief_data.get('summary','N/A')} +Signal: {brief_data.get('signal','?')} ({brief_data.get('confidence','?')}% conf) +RSI: {brief_data.get('rsi','?')} | F&G: {brief_data.get('fear_greed_class','?')} +Bullish signals: {brief_data.get('bullish_signals',0)} | Bearish: {brief_data.get('bearish_signals',0)} + +Decio: {decio_data.get('action','?')} conf {decio_data.get('confidence','?')}% +Justification: {decio_data.get('justification','?')[:100]} + +Responda em JSON: +{{"verdict": "APROVADO|REPROVADO|FLAG", + "risks": ["risk1","risk2"], + "recommendations": ["rec1","rec2"], + "score": 0-100, + "summary_pt": "resumo em português"}} +""" + headers = { + "Authorization": "Bearer " + (os.getenv("PAPERT_API_KEY") or os.getenv("OPENROUTER_API_KEY", "")), + "Content-Type": "application/json", + "HTTP-Referer": "https://brainsteel.fin", + "X-Title": "BrainSteel Fin" + } + r = requests.post( + "https://openrouter.ai/api/v1/chat/completions", + json={"model": "deepseek/deepseek-v4-flash:free", "messages": [{"role": "user", "content": prompt}], "max_tokens": 400}, + headers=headers, timeout=30 + ) + if r.status_code == 200: + resp_data = r.json() + content = resp_data["choices"][0]["message"]["content"] + usage = resp_data.get("usage", {}) + if usage: + try: + from agents.token_tracker import log_tokens + log_tokens("Audit", "deepseek/deepseek-v4-flash:free", usage) + except: + pass + content = content.strip("` \n") + if content.startswith("json"): content = content[4:] + return {"report": content, "status": "generated"} + except: + pass + return {"report": "Audit local", "status": "error"} \ No newline at end of file diff --git a/agents/brief.py b/agents/brief.py new file mode 100644 index 0000000..b459bd0 --- /dev/null +++ b/agents/brief.py @@ -0,0 +1,497 @@ +""" +BrainSteel Fin — Brief Agent (v2) +Market Intelligence Analyst — Super Financial Agent +5 Pillars: On-Chain | Derivatives | Sentiment | Technical | Macro +""" +import os, json, requests, re +from datetime import datetime, timedelta +from collections import defaultdict + +# ── Data Sources ───────────────────────────────────────────────────────────── +GLASSNODE_API = os.getenv("GLASSNODE_API_KEY", "") +COINGLASS_API = os.getenv("COINGLASS_API_KEY", "") +SANTIMENT_API = os.getenv("SANTIMENT_API_KEY", "") + +class BriefAgent: + def __init__(self): + self.name = "Brief" + self.openrouter_key = os.getenv("BRIEF_API_KEY", os.getenv("OPENROUTER_API_KEY", "")) + self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" + self.model = "deepseek/deepseek-v4-flash:free" + + # ═══════════════════════════════════════════════════════ + # PILLAR 1 — ON-CHAIN ANALYSIS + # ═══════════════════════════════════════════════════════ + def _onchain_bitflyer(self): + """On-chain data via Blockchain.com public API + mempool.space.""" + result = {} + try: + # BTC UTXO set — circulating supply estimate + r1 = requests.get("https://blockchain.info/q/estimatedbtcsupply", timeout=8) + if r1.status_code == 200: + result["btc_supply"] = float(r1.text.strip()) + except: + pass + + try: + # Market cap + r2 = requests.get("https://api.blockchain.com/v3/index/c BTC/information", timeout=8) + if r2.status_code == 200: + data = r2.json() + result["market_cap"] = data.get("market_cap") + except: + pass + + try: + # Mempool congestion — fee estimation + r3 = requests.get("https://mempool.space/api/v1/fees/recommended", timeout=8) + if r3.status_code == 200: + fees = r3.json() + result["fee_fast"] = fees.get("fastestFee", 0) + result["fee_hour"] = fees.get("halfHourFee", 0) + result["fee_economy"] = fees.get("economyFee", 0) + except: + pass + + return result + + def _onchain_mempool_state(self): + """Current mempool state — blocks pending, congestion level.""" + try: + r = requests.get("https://mempool.space/api/v1/mempool-summary", timeout=8) + if r.status_code == 200: + data = r.json() + return { + "vsize_mb": round(data.get("total_vsize", 0) / 1e6, 1), + "tx_pending": data.get("tx_count", 0), + } + except: + pass + return {} + + def _arkham_trace(self): + """Arkham Intelligence — track institutional large wallets (public data).""" + # Arkham has a free public API for entity tags + try: + r = requests.get( + "https://api.arkhamintelligence.com/intelligence/labels?chain=BTC&limit=5", + timeout=8, + headers={"Accept": "application/json"} + ) + if r.status_code == 200: + data = r.json() + entities = [l.get("label", "") for l in data.get("results", [])[:5]] + return {"arkham_entities": entities} + except: + pass + return {} + + # ═══════════════════════════════════════════════════════ + # PILLAR 2 — DERIVATIVES & LIQUIDITY + # ═══════════════════════════════════════════════════════ + def _derivatives_binance(self): + """Funding rates + open interest from Binance futures (public).""" + result = {} + try: + # Funding rate for BTC perpetual + r = requests.get( + "https://fapi.binance.com/fapi/v1/premiumIndex?symbol=BTCUSDT", + timeout=8 + ) + if r.status_code == 200: + data = r.json() + result["funding_rate_pct"] = float(data.get("lastFundingRate", 0)) * 100 + result["mark_price"] = float(data.get("markPrice", 0)) + result["index_price"] = float(data.get("indexPrice", 0)) + result["next_funding_ts"] = data.get("nextFundingTime", "") + except: + pass + + try: + # Open interest + r2 = requests.get( + "https://fapi.binance.com/fapi/v1/openInterest?symbol=BTCUSDT", + timeout=8 + ) + if r2.status_code == 200: + oi = r2.json() + btc_oi = int(oi.get("openInterest", 0)) + result["open_interest_btc"] = btc_oi + result["open_interest_usd"] = btc_oi * result.get("mark_price", 0) + except: + pass + + return result + + def _coinglass_funding(self): + """CoinGlass funding rates aggregate across exchanges (public).""" + try: + r = requests.get( + "https://open-api.coinglass.com/public/v2/funding_rate_history?symbol=BTC&exchange=Binance,OKX,Bybit&type=1", + timeout=10, + headers={"Accept": "application/json"} + ) + if r.status_code == 200: + data = r.json() + records = data.get("data", {}).get("history", []) + if records: + # Latest avg funding + latest = records[-1] if records else {} + return {"avg_funding_pct": latest.get("rate", 0)} + except: + pass + return {} + + def _liquidations_map(self): + """Estimate liquidation zones via open interest concentration. + Source: Binance liquidated positions public data.""" + result = {} + try: + r = requests.get( + "https://fapi.binance.com/futures/data/globalLongShortAccountRatio?symbol=BTCUSDT&period=1h&limit=5", + timeout=8 + ) + if r.status_code == 200: + data = r.json() + if data: + latest = data[-1] + long_ratio = float(latest.get("longAccount", 0)) + short_ratio = float(latest.get("shortAccount", 0)) + result["long_ratio_pct"] = round(long_ratio / (long_ratio + short_ratio + 0.001) * 100, 1) + result["sentiment"] = "overbought" if long_ratio > 60 else "oversold" if long_ratio < 40 else "neutral" + except: + pass + return result + + # ═══════════════════════════════════════════════════════ + # PILLAR 3 — SENTIMENT & BEHAVIOR + # ═══════════════════════════════════════════════════════ + def _fear_greed_index(self): + """Alternative.me Fear & Greed Index (free).""" + try: + r = requests.get("https://api.alternative.me/fng/?limit=2", timeout=8) + if r.status_code == 200: + data = r.json() + items = data.get("data", []) + if items: + latest = items[0] + prev = items[1] if len(items) > 1 else {} + return { + "fgi_value": int(latest.get("value", 50)), + "fgi_class": latest.get("value_classification", "Neutral"), + "fgi_change": int(latest.get("change", 0)), + "fgi_prev_value": int(prev.get("value", 50)) if prev else 50, + } + except: + pass + return {} + + def _sentiment_cryptopanic(self): + """CryptoPanic — news sentiment aggregator (free).""" + try: + r = requests.get( + "https://cryptopanic.com/api/v1/posts/?auth_token=¤cies=BTC,ETH,SOL&kind=news", + timeout=10 + ) + if r.status_code == 200: + data = r.json() + posts = data.get("results", [])[:20] + pos_kw = ["bullish", "surge", "rally", "record", "growth", "high", "adoption", "buy", "soar", "peak", " ETF ", "institutional", "accumulation", "breakout"] + neg_kw = ["bearish", "drop", "crash", "sell", "risk", "warn", "hack", "ban", "regulation", "reject", "loss", "fear"] + pos = sum(1 for p in posts for t in [p.get("title", "")] if any(k.lower() in t.lower() for k in pos_kw)) + neg = sum(1 for p in posts for t in [p.get("title", "")] if any(k.lower() in t.lower() for k in neg_kw)) + return { + "news_pos": pos, + "news_neg": neg, + "news_total": len(posts), + "sentiment_label": "Predominantemente Otimista" if pos > neg + 3 else + "Predominantemente Pessimista" if neg > pos + 3 else "Incerto" + } + except: + pass + return {} + + def _social_volume(self): + """CoinGecko social stats (free tier).""" + try: + r = requests.get( + "https://api.coingecko.com/api/v3/coins/bitcoin", + params={"tickers": "false", "market_data": "true", "community_data": "true"}, + timeout=10 + ) + if r.status_code == 200: + data = r.json() + comm = data.get("community_data", {}) or {} + return { + "twitter_followers": data.get("followers", 0), + "forum_posts_24h": comm.get("forum_posts_active_24h", 0), + "reddit_subscribers": comm.get("reddit_subscribers", 0), + "telegram_channel_users": comm.get("telegram_channel_user_count", 0), + } + except: + pass + return {} + + # ═══════════════════════════════════════════════════════ + # PILLAR 4 — TECHNICAL ANALYSIS + # ═══════════════════════════════════════════════════════ + def _technical_binance(self): + """Candle-based support/resistance + RSI + MACD approximation.""" + result = {} + try: + # 1h candles for 200 periods + r = requests.get( + "https://api.binance.com/api/v3/klines", + params={"symbol": "BTCUSDT", "interval": "1h", "limit": 200}, + timeout=10 + ) + if r.status_code == 200: + candles = r.json() + closes = [float(c[4]) for c in candles] + highs = [float(c[2]) for c in candles] + lows = [float(c[3]) for c in candles] + vols = [float(c[5]) for c in candles] + + # RSI(14) approximation + deltas = [closes[i] - closes[i-1] for i in range(1, len(closes))] + gains = [d for d in deltas[-14:] if d > 0] + losses = [-d for d in deltas[-14:] if d < 0] + avg_gain = sum(gains) / 14 if gains else 0.001 + avg_loss = sum(losses) / 14 if losses else 0.001 + rs = avg_gain / avg_loss if avg_loss else 99 + rsi = round(100 - (100 / (1 + rs)), 1) + + # MACD (12,26,9) + ema12 = sum(closes[-12:]) / 12 + ema26 = sum(closes[-26:]) / 26 + macd_line = ema12 - ema26 + signal = macd_line * 0.2 # simplified signal + + # Support/resistance via volume profile + vol_index = vols.index(max(vols)) + support = round(min(lows[:vol_index]) if vol_index > 0 else min(lows[-50:]), 2) + resistance = round(max(highs[vol_index:]) if vol_index < len(highs) - 1 else max(highs[-50:]), 2) + + # 4h change + change_4h = round((closes[-1] / closes[-5] - 1) * 100, 2) if len(closes) >= 5 else 0 + change_24h = round((closes[-1] / closes[-25] - 1) * 100, 2) if len(closes) >= 25 else 0 + change_7d = round((closes[-1] / closes[-169] - 1) * 100, 2) if len(closes) >= 169 else 0 + + result.update({ + "price": closes[-1], + "rsi": rsi, + "macd": round(macd_line, 2), + "macd_signal": round(signal, 2), + "macd_histogram": round(macd_line - signal, 2), + "support": support, + "resistance": resistance, + "change_4h": change_4h, + "change_24h": change_24h, + "change_7d": change_7d, + "volume_avg_ratio": round(sum(vols[-24:]) / sum(vols[-168:]) * 100, 1) if sum(vols[-168:]) > 0 else 100, + }) + except Exception as e: + pass + return result + + # ═══════════════════════════════════════════════════════ + # PILLAR 5 — MACROECONOMIC + # ═══════════════════════════════════════════════════════ + def _macro_etf_flows(self): + """ETF flow estimates via Farside Investors (public).""" + try: + r = requests.get("https://farside.io/bitcoin-etf-flow-all-data", timeout=10) + if r.status_code == 200: + text = r.text.lower() + # Look for latest date pattern and inflow/outflow indicator + import re + # Simple pattern: find most recent date and flow amount + matches = re.findall(r'(\d{4}-\d{2}-\d{2})[^$]*?(-?\$?[\d,]+)\s*(million|billion)?\s*(inflow|outflow)', text) + if matches: + latest = matches[-1] + return {"etf_date": latest[0], "etf_flow_raw": latest[1] + " " + (latest[2] or "")} + except: + pass + return {} + + def _macro_btc_dominance(self): + """BTC Dominance (Cap) — tracks altcoin season vs BTC season.""" + try: + r = requests.get( + "https://api.coingecko.com/api/v3/global", + timeout=10 + ) + if r.status_code == 200: + data = r.json() + global_data = data.get("data", {}) + btc_d = global_data.get("market_cap_percentage", {}).get("btc", 0) + return { + "btc_dominance_pct": btc_d, + "active_cryptos": global_data.get("active_cryptocurrencies", 0), + } + except: + pass + return {} + + def _macro_correlations(self): + """DXY Dollar Index via public API.""" + try: + r = requests.get( + "https://api.coingecko.com/api/v3/coins/us-dtate", + params={"id": "us-dtate"}, + timeout=8 + ) + # Fallback: approximate via BTC correlation with NASDAQ + # Use a simple proxy from Binance DXY futures + pass + except: + pass + return {} + + # ═══════════════════════════════════════════════════════ + # MAIN RUN + # ═══════════════════════════════════════════════════════ + def run(self): + all_data = {} + + # Gather all pillars + pillars = { + "On-Chain": self._onchain_bitflyer, + "Mempool": self._onchain_mempool_state, + "Arkham": self._arkham_trace, + "Derivatives": self._derivatives_binance, + "Coinglass": self._coinglass_funding, + "Liquidations": self._liquidations_map, + "FearGreed": self._fear_greed_index, + "Sentiment": self._sentiment_cryptopanic, + "Social": self._social_volume, + "Technical": self._technical_binance, + "MacroETF": self._macro_etf_flows, + "Dominance": self._macro_btc_dominance, + } + + for name, fn in pillars.items(): + try: + result = fn() + if result: + all_data[name] = result + except Exception as e: + all_data[name] = {"error": str(e)} + + tech = all_data.get("Technical", {}) + price = tech.get("price", 0) + if not price: + return {"summary": "Erro: não foi possível obter dados de mercado", "signal": "ERROR", "confidence": 0} + + # Build briefing string for Decio + fgi = all_data.get("FearGreed", {}) + sentiment_data = all_data.get("Sentiment", {}) + deriv = all_data.get("Derivatives", {}) + liq = all_data.get("Liquidations", {}) + onchain = all_data.get("On-Chain", {}) + btc_d = all_data.get("Dominance", {}) + + # Overall sentiment combining Fear&Greed + news + fg_class = fgi.get("fgi_class", "Neutral") + news_sent = sentiment_data.get("sentiment_label", "Incerto") + funding = deriv.get("funding_rate_pct", 0) + long_ratio = liq.get("long_ratio_pct", 50) + btc_dom = btc_d.get("btc_dominance_pct", 0) + rsi = tech.get("rsi", 50) + macd_h = tech.get("macd_histogram", 0) + + # Signal detection + bullish_signals = 0 + bearish_signals = 0 + + # RSI + if rsi < 30: bullish_signals += 1 + elif rsi > 70: bearish_signals += 1 + + # Funding rate + if funding > 0.01: bearish_signals += 1 # high funding = long squeeze risk + elif funding < -0.01: bullish_signals += 1 + + # Long ratio + if long_ratio > 60: bearish_signals += 1 # over-leveraged longs + elif long_ratio < 40: bullish_signals += 1 + + # Fear&Greed + if fg_class in ["Extreme Fear", "Fear"]: bullish_signals += 1 + elif fg_class in ["Extreme Greed", "Greed"]: bearish_signals += 1 + + # MACD histogram + if macd_h > 0: bullish_signals += 1 + elif macd_h < 0: bearish_signals += 1 + + # BTC dominance (high = BTC season, alt risk) + if btc_dom > 55: bullish_signals += 1 + elif btc_dom < 45: bearish_signals += 1 + + # Determine signal and confidence + net = bullish_signals - bearish_signals + if net >= 3: + signal = "ALTA" + confidence = min(50 + net * 8, 90) + elif net <= -3: + signal = "BAIXA" + confidence = min(50 + abs(net) * 8, 90) + else: + signal = "NEUTRO" + confidence = 40 + abs(net) * 5 + + # Change-based confirmation + change_4h = tech.get("change_4h", 0) + change_24h = tech.get("change_24h", 0) + if change_24h > 5 and signal == "NEUTRO": + signal = "ALTA"; confidence = min(confidence + 10, 85) + elif change_24h < -5 and signal == "NEUTRO": + signal = "BAIXA"; confidence = min(confidence + 10, 85) + + # 3-point briefing for Decio + support = tech.get("support", 0) + resistance = tech.get("resistance", 0) + sup_str = f"${support:,.0f}" if support else "?" + res_str = f"${resistance:,.0f}" if resistance else "?" + + briefing = ( + f"BTC ${price:,.0f} | " + f"4h: {change_4h:+.1f}% | 24h: {change_24h:+.1f}% | " + f"RSI: {rsi} | MACD hist: {macd_h:+.2f} | " + f"Sup: {sup_str} | Res: {res_str} | " + f"F&G: {fg_class} ({fgi.get('fgi_value', 0)}) | " + f"News: {news_sent} | " + f"Funding: {funding:+.4f}% | " + f"LongRatio: {long_ratio}% | " + f"BTC Dom: {btc_dom}% | " + f"Bullish={bullish_signals} Bearish={bearish_signals} | " + f"Sinal: {signal} ({confidence}% conf)" + ) + + result = { + "price": price, + "signal": signal, + "confidence": confidence, + "summary": f"BTC ${price:,.0f} | {signal} | conf {confidence}% | RSI {rsi} | F&G {fg_class}", + "briefing_for_decio": briefing, + "timestamp": datetime.now().isoformat(), + # Full data for Audit + "_all_pillars": all_data, + # Key metrics for display + "rsi": rsi, + "macd_histogram": macd_h, + "support": support, + "resistance": resistance, + "change_24h": change_24h, + "change_4h": change_4h, + "fear_greed": fgi.get("fgi_value", 0), + "fear_greed_class": fg_class, + "funding_rate": funding, + "long_ratio": long_ratio, + "news_sentiment": news_sent, + "btc_dominance": btc_dom, + "bullish_signals": bullish_signals, + "bearish_signals": bearish_signals, + "net_signal": net, + } + return result \ No newline at end of file diff --git a/agents/decio.py b/agents/decio.py new file mode 100644 index 0000000..e8acc16 --- /dev/null +++ b/agents/decio.py @@ -0,0 +1,182 @@ +""" +BrainSteel Fin — Decio Agent (v2) +Chief Strategist & Risk Officer +5-Pillar decision engine: Confiança < 75% → HOLD. BUY/SELL → SL 2% / TP 5%. +""" +import os, json, requests +from datetime import datetime + +class DecioAgent: + def __init__(self, brief_data): + self.name = "Decio" + self.brief_data = brief_data + self.openrouter_key = os.getenv("DECIO_API_KEY", os.getenv("OPENROUTER_API_KEY", "")) + self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" + self.model = "deepseek/deepseek-v4-flash:free" + + # ── CORE RULE: confidence < 75% → always HOLD ───────────────────────── + def _risk_filter(self, confidence, signal): + """Décio is a mathematician: no confidence ≥75%, no operation.""" + return confidence < 75 or signal not in ("ALTA", "BAIXA") + + # ── Stop Loss / Take Profit ──────────────────────────────────────────────── + def _calc_levels(self, price, action): + if action == "BUY": + return round(price * 0.98, 2), round(price * 1.05, 2) + elif action == "SELL": + return round(price * 1.02, 2), round(price * 0.95, 2) + return 0, 0 + + # ── LLM Decision (with 5-pillar context) ───────────────────────────────── + def _decide_llm(self): + if not self.openrouter_key: + return None + + briefing = self.brief_data.get("briefing_for_decio", self.brief_data.get("summary", "")) + pillars = self.brief_data.get("_all_pillars", {}) + signal = self.brief_data.get("signal", "NEUTRO") + confidence = self.brief_data.get("confidence", 50) + price = self.brief_data.get("price", 0) + + # Build rich context for LLM + tech = pillars.get("Technical", {}) + deriv = pillars.get("Derivatives", {}) + liq = pillars.get("Liquidations", {}) + fgi = pillars.get("FearGreed", {}) + sent = pillars.get("Sentiment", {}) + dom = pillars.get("Dominance", {}) + + context = f"""Você é o Décio, estrategista soberano da BrainSteel Fin. +Analítico, frio, matemático. Preservação de capital > lucro rápido. + + BRIEFING DO BRIEF: +{briefing} + + PILARES DE ANÁLISE DO BRIEF: + Technical: RSI={tech.get('rsi','?')} | MACD hist={tech.get('macd_histogram','?')} | Var 24h={tech.get('change_24h','?')}% + Derivatives: Funding={deriv.get('funding_rate_pct','?')}%. Mark={deriv.get('mark_price','?')} + Liquidations: Long Ratio={liq.get('long_ratio_pct','?')}%. Sentiment={liq.get('sentiment','?')} + Fear&Greed: Index={fgi.get('fgi_value','?')} ({fgi.get('fgi_class','?')}) + Sentiment: {sent.get('sentiment_label','?')} + BTC Dominance: {dom.get('btc_dominance_pct','?')}% + Bullish signals: {self.brief_data.get('bullish_signals',0)} | Bearish: {self.brief_data.get('bearish_signals',0)} + + REGRAS OPERACIONAIS: + Confiança < 75% → decisão SEMPRE HOLD (nunca opera) + Confiança ≥ 75% + sinal=ALTA → BUY + Confiança ≥ 75% + sinal=BAIXA → SELL + BUY/SELL → Stop Loss = preço × 0.98 (-2%) | Take Profit = preço × 1.05 (+5%) + HOLD → stop_loss=0, take_profit=0 + + Responda EXCLUSIVAMENTE com JSON válido (sem texto fora do JSON): +{{"decision": "BUY|SELL|HOLD", "confidence": N, "justification": "frase curta", "stop_loss": N, "take_profit": N}}""" + + try: + headers = { + "Authorization": f"Bearer {self.openrouter_key}", + "Content-Type": "application/json", + "HTTP-Referer": "https://brainsteel.fin", + "X-Title": "BrainSteel Fin" + } + payload = { + "model": self.model, + "messages": [{"role": "user", "content": context}], + "max_tokens": 400 + } + r = requests.post(self.openrouter_url, json=payload, headers=headers, timeout=30) + if r.status_code == 200: + resp_data = r.json() + content = resp_data["choices"][0]["message"]["content"].strip() + content = content.strip("` \n") + if content.startswith("json"): + content = content[4:] + data = json.loads(content) + # Capture token usage + usage = resp_data.get("usage", {}) + if usage: + try: + from agents.token_tracker import log_tokens + log_tokens("Decio", self.model, usage) + except: + pass + # Apply risk filter + if self._risk_filter(data.get("confidence", 0), signal): + data["decision"] = "HOLD" + data["justification"] = "Confiança < 75% — preservando capital" + data["stop_loss"] = 0 + data["take_profit"] = 0 + return data + except: + pass + return None + + # ── Local decision (fallback) ──────────────────────────────────────────── + def _local_decision(self): + confidence = self.brief_data.get("confidence", 50) + signal = self.brief_data.get("signal", "NEUTRO") + price = self.brief_data.get("price", 0) + net = self.brief_data.get("net_signal", 0) + rsi = self.brief_data.get("rsi", 50) + + # Overbought/oversold confirmation + if signal == "ALTA" and rsi > 75: + # Extremely overbought — skip BUY even if signal says ALTA + decision = "HOLD" + sl, tp = 0, 0 + justification = "ALTA sinal cancelada — RSI overbought (75+)" + elif signal == "BAIXA" and rsi < 25: + decision = "HOLD" + sl, tp = 0, 0 + justification = "BAIXA sinal cancelada — RSI oversold (25-)" + elif self._risk_filter(confidence, signal): + decision = "HOLD" + sl, tp = 0, 0 + justification = f"Confiança {confidence}% < 75% — preservando capital" + elif signal == "ALTA": + decision = "BUY" + sl, tp = self._calc_levels(price, "BUY") + justification = f"Sinal ALTA conf {confidence}%, RSI {rsi}, net {net:+d}" + elif signal == "BAIXA": + decision = "SELL" + sl, tp = self._calc_levels(price, "SELL") + justification = f"Sinal BAIXA conf {confidence}%, RSI {rsi}, net {net:+d}" + else: + decision = "HOLD" + sl, tp = 0, 0 + justification = "Sinal neutro — aguardando clareza" + + return { + "decision": decision, + "confidence": confidence, + "justification": justification, + "stop_loss": sl, + "take_profit": tp, + "signal": signal + } + + # ── Main run ─────────────────────────────────────────────────────────── + def run(self): + llm_result = self._decide_llm() + result = llm_result if llm_result else self._local_decision() + + price = self.brief_data.get("price", 0) + output = { + "action": result["decision"], + "amount_pct": 25 if result["decision"] in ("BUY", "SELL") else 0, + "stop_loss": result.get("stop_loss", 0), + "take_profit": result.get("take_profit", 0), + "confidence": result.get("confidence", 50), + "justification": result.get("justification", "")[:200], + "signal": result.get("signal", self.brief_data.get("signal", "NEUTRO")), + "price": price, + "timestamp": datetime.now().isoformat() + } + + decision_str = f"{output['action']} | Conf: {output['confidence']}%" + if output["action"] in ("BUY", "SELL"): + decision_str += f" | SL: ${output['stop_loss']:,.0f} | TP: ${output['take_profit']:,.0f}" + decision_str += f" | {output['justification'][:80]}" + + output["decision"] = decision_str + output["agent"] = "Decio" + return output \ No newline at end of file diff --git a/agents/papert.py b/agents/papert.py new file mode 100644 index 0000000..d76f089 --- /dev/null +++ b/agents/papert.py @@ -0,0 +1,385 @@ +""" +BrainSteel Fin — PaperT Agent + Portfolio Simulator +Simulation Execution Officer + Virtual Portfolio Tracker +Saldo virtual: $10.000 USDT | tracking de P&L diário | gráfico de evolução +""" +import os, json, requests, sqlite3 +from datetime import datetime, date +from pathlib import Path + +# ── Portfolio Config ────────────────────────────────────────────────────────── +INITIAL_BALANCE = 200.0 # USDT virtual para simulação ( BrainSteel Fin ) +DATA_DIR = Path("/app/data") +PORTFOLIO_DB = DATA_DIR / "portfolio.db" + +class Portfolio: + """Portfolio simulation — tracking de saldo, trades e P&L.""" + + def __init__(self): + self._init_db() + + def _init_db(self): + DATA_DIR.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + c.execute(""" + CREATE TABLE IF NOT EXISTS portfolio ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT, + balance REAL, + btc_held REAL DEFAULT 0, + unrealized_pnl REAL DEFAULT 0, + total_trades INTEGER DEFAULT 0, + wins INTEGER DEFAULT 0, + losses INTEGER DEFAULT 0, + created_at TEXT, + UNIQUE(date) + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS trades ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT, + action TEXT, + entry_price REAL, + amount_usdt REAL, + btc_amount REAL, + exit_price REAL, + pnl_usdt REAL, + pnl_pct REAL, + stop_loss REAL, + take_profit REAL, + fee_usdt REAL, + exit_reason TEXT, + result TEXT, + created_at TEXT + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS daily_snapshot ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT UNIQUE, + balance REAL, + btc_price REAL, + btc_held REAL, + open_trades INTEGER DEFAULT 0, + closed_trades INTEGER DEFAULT 0, + day_pnl REAL DEFAULT 0, + total_pnl REAL DEFAULT 0, + win_rate REAL DEFAULT 0, + created_at TEXT + ) + """) + # Ensure today's row exists + today = date.today().isoformat() + row = c.execute("SELECT balance FROM portfolio WHERE date=?", (today,)).fetchone() + if not row: + # Seed from last known balance or initial + last = c.execute("SELECT balance, btc_held FROM portfolio ORDER BY date DESC LIMIT 1").fetchone() + balance = last[0] if last else INITIAL_BALANCE + btc_held = last[1] if last else 0.0 + c.execute("INSERT INTO portfolio (date,balance,btc_held,created_at) VALUES (?,?,?,?)", + (today, balance, btc_held, datetime.now().isoformat())) + conn.commit() + conn.close() + + @property + def balance(self): + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + today = date.today().isoformat() + row = c.execute("SELECT balance, btc_held FROM portfolio WHERE date=?", (today,)).fetchone() + conn.close() + return {"balance": row[0] if row else INITIAL_BALANCE, "btc_held": row[1] if row else 0.0} + + def buy(self, price, amount_pct=25, stop_loss=0, take_profit=0): + """Executa BUY simulado.""" + return self._execute_trade("BUY", price, amount_pct, stop_loss, take_profit) + + def sell(self, price, amount_pct=25, stop_loss=0, take_profit=0): + """Executa SELL simulado (fecha posição).""" + return self._execute_trade("SELL", price, amount_pct, stop_loss, take_profit) + + def _execute_trade(self, action, price, amount_pct, stop_loss, take_profit): + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + today = date.today().isoformat() + now = datetime.now().isoformat() + + current = self.balance + balance = current["balance"] + btc_held = current["btc_held"] + fee_rate = 0.001 # 0.1% + + result = {} + pnl = 0.0 + pnl_pct = 0.0 + exit_reason = "" + btc_amount = 0.0 + amount_usdt = 0.0 + + if action == "BUY": + allocation = balance * (amount_pct / 100) + fee = allocation * fee_rate + net_cost = allocation - fee + btc_amount = net_cost / price + new_balance = balance - allocation + new_btc_held = btc_held + btc_amount + amount_usdt = allocation + exit_reason = "entry" + result_str = f"🟢 BUY | ${allocation:.2f} alocado | {btc_amount:.6f} BTC @ ${price:,.0f} | Fee: ${fee:.2f}" + + elif action == "SELL" and btc_held > 0: + # Sell portion of BTC held + sell_pct = amount_pct / 100 + btc_to_sell = btc_held * sell_pct + gross = btc_to_sell * price + fee = gross * fee_rate + net_proceeds = gross - fee + amount_usdt = gross + pnl = net_proceeds - (btc_to_sell * price) # simplified + pnl_pct = (pnl / (btc_to_sell * price)) * 100 if btc_to_sell > 0 else 0 + + new_balance = balance + net_proceeds + new_btc_held = btc_held - btc_to_sell + btc_amount = btc_to_sell + + # Determine exit reason + if stop_loss and price <= stop_loss: + exit_reason = "stop_loss_hit" + elif take_profit and price >= take_profit: + exit_reason = "take_profit_hit" + else: + exit_reason = "decio_signal" + + result_str = f"🔴 SELL | {btc_to_sell:.6f} BTC @ ${price:,.0f} | Net: ${net_proceeds:.2f} | Fee: ${fee:.2f}" + else: + new_balance = balance + new_btc_held = btc_held + result_str = f"⚪ SELL ignorado | BTC held: {btc_held:.6f}" + + # Record trade if it happened + if action in ("BUY", "SELL") and (action == "BUY" or btc_held > 0): + is_win = pnl > 0 if action == "SELL" and btc_held > 0 else None + c.execute(""" + INSERT INTO trades (date,action,entry_price,amount_usdt,btc_amount,exit_price,pnl_usdt,pnl_pct,stop_loss,take_profit,fee_usdt,exit_reason,result,created_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + """, (today, action, price, amount_usdt, btc_amount, + price if action == "SELL" else 0, + pnl, pnl_pct, stop_loss, take_profit, + amount_usdt * fee_rate if action == "BUY" else amount_usdt * fee_rate if action == "SELL" else 0, + exit_reason, "WIN" if is_win else ("LOSS" if is_win is False else "PENDING"), + now)) + + # Update daily stats + c.execute("UPDATE portfolio SET balance=?, btc_held=?, total_trades=total_trades+1 WHERE date=?", + (new_balance, new_btc_held, today)) + if is_win is True: + c.execute("UPDATE portfolio SET wins=wins+1 WHERE date=?", (today,)) + elif is_win is False: + c.execute("UPDATE portfolio SET losses=losses+1 WHERE date=?", (today,)) + + conn.commit() + conn.close() + + return { + "action": action, + "balance": new_balance if action != "SELL" or btc_held > 0 else balance, + "btc_held": new_btc_held, + "pnl": pnl, + "result_str": result_str + } + + def get_stats(self): + """Returns aggregate stats for dashboard.""" + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + + # All trades + trades = c.execute("SELECT action,pnl_usdt,pnl_pct,result,date FROM trades ORDER BY created_at DESC").fetchall() + + total_trades = len(trades) + wins = sum(1 for t in trades if t[3] == "WIN") + losses = sum(1 for t in trades if t[3] == "LOSS") + + # Current balance + today = date.today().isoformat() + cur = c.execute("SELECT balance, btc_held FROM portfolio WHERE date=?", (today,)).fetchone() + current_balance = cur[0] if cur else INITIAL_BALANCE + btc_held = cur[1] if cur else 0.0 + + # Portfolio value in USD (btc_held at current price) + try: + r = requests.get("https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT", timeout=5) + btc_price = float(r.json()["price"]) if r.status_code == 200 else 0 + except: + btc_price = 0 + total_value = current_balance + (btc_held * btc_price) + + # Daily history for chart + days = c.execute("SELECT date,balance FROM portfolio ORDER BY date ASC").fetchall() + + conn.close() + + # Calculate metrics + total_pnl = total_value - INITIAL_BALANCE + total_pnl_pct = (total_pnl / INITIAL_BALANCE) * 100 + win_rate = (wins / total_trades * 100) if total_trades > 0 else 0 + + # Drawdown + peak = INITIAL_BALANCE + max_drawdown = 0 + for _, bal in days: + if bal > peak: + peak = bal + dd = (peak - bal) / peak * 100 + if dd > max_drawdown: + max_drawdown = dd + + return { + "initial_balance": INITIAL_BALANCE, + "current_balance": round(current_balance, 2), + "btc_held": round(btc_held, 8), + "btc_price": btc_price, + "total_value": round(total_value, 2), + "total_pnl": round(total_pnl, 2), + "total_pnl_pct": round(total_pnl_pct, 2), + "total_trades": total_trades, + "wins": wins, + "losses": losses, + "win_rate": round(win_rate, 1), + "max_drawdown": round(max_drawdown, 2), + "chart_data": [(d, round(b, 2)) for d, b in days], + "btc_value_usdt": round(btc_held * btc_price, 2) + } + + def snapshot(self): + """Save daily snapshot for chart history.""" + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + today = date.today().isoformat() + stats = self.get_stats() + + # Get closed trades today + today_trades = c.execute("SELECT COUNT(*),SUM(pnl_usdt) FROM trades WHERE date=? AND result IN ('WIN','LOSS')", (today,)).fetchone() + + c.execute(""" + INSERT OR REPLACE INTO daily_snapshot + (date,balance,btc_price,btc_held,open_trades,closed_trades,day_pnl,total_pnl,win_rate,created_at) + VALUES (?,?,?,?,?,?,?,?,?,?) + """, ( + today, + stats["current_balance"], + stats["btc_price"], + stats["btc_held"], + 0, # open_trades + today_trades[0] if today_trades else 0, + today_trades[1] if today_trades and today_trades[1] else 0, + stats["total_pnl"], + stats["win_rate"], + datetime.now().isoformat() + )) + conn.commit() + conn.close() + + +class PapertAgent: + """Agent executor que também atualiza portfolio simulado.""" + + def __init__(self, decio_data): + self.name = "PaperT" + self.decio_data = decio_data + self.portfolio = Portfolio() + self.openrouter_key = os.getenv("PAPERT_API_KEY", os.getenv("OPENROUTER_API_KEY", "")) + self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" + self.model = "deepseek/deepseek-v4-flash:free" + + def _validate_order(self): + action = self.decio_data.get("action", self.decio_data.get("decision", "HOLD")) + if isinstance(action, str): + for w in ["BUY", "SELL", "HOLD"]: + if w in action.upper(): + action = w + break + return True, action + + def _format_log_entry(self, action, entry_price, stop_loss, take_profit): + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + fee = round(entry_price * 0.001, 2) + entry = f"[{now}] BTC/USDT | {action} | Entry: ${entry_price:,.2f} | Fee: ${fee:.2f} | SL: ${stop_loss:,.2f} | TP: ${take_profit:,.2f}" + return entry + + def _write_log(self, log_line): + for path in ["/data/trading_history.log", "/app/data/trading_history.log"]: + try: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "a") as f: + f.write(log_line + "\n") + return True + except: + pass + return False + + def _current_price(self): + try: + r = requests.get("https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT", timeout=10) + if r.status_code == 200: + return float(r.json()["price"]) + except: + return self.decio_data.get("price", 0) + + def run(self): + valid, action = self._validate_order() + entry_price = self._current_price() + stop_loss = self.decio_data.get("stop_loss", 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 + + # Execute portfolio trade + portfolio_result = self.portfolio._execute_trade(action, entry_price, amount_pct, stop_loss, take_profit) + + # Log entry + log_entry = self._format_log_entry(action, entry_price, stop_loss, take_profit) + logged = self._write_log(log_entry) + + # Save daily snapshot + try: + self.portfolio.snapshot() + except: + pass + + # Build result + stats = self.portfolio.get_stats() + fee = round(entry_price * 0.001, 2) + + result_str = f"[EXEC] {datetime.now().strftime('%Y-%m-%dT%H:%M:%S')} | {action} | ${entry_price:,.0f}" + if action in ("BUY", "SELL"): + result_str += f" | Bal: ${stats['current_balance']:.2f} | P&L total: ${stats['total_pnl']:+.2f} ({stats['total_pnl_pct']:+.1f}%)" + + return { + "status": "validated", + "exec_log": log_entry, + "action": action, + "entry_price": entry_price, + "amount_pct": amount_pct, + "stop_loss": stop_loss, + "take_profit": take_profit, + "fee_usdt": fee, + "log_appended": logged, + "result": result_str, + # Portfolio info for dashboard + "portfolio": { + "balance": stats["current_balance"], + "total_value": stats["total_value"], + "total_pnl": stats["total_pnl"], + "total_pnl_pct": stats["total_pnl_pct"], + "total_trades": stats["total_trades"], + "wins": stats["wins"], + "losses": stats["losses"], + "win_rate": stats["win_rate"], + "btc_held": stats["btc_held"], + "btc_price": stats["btc_price"] + }, + "agent": "PaperT", + "timestamp": datetime.now().isoformat() + } \ No newline at end of file diff --git a/agents/token_tracker.py b/agents/token_tracker.py new file mode 100644 index 0000000..2f8b3ad --- /dev/null +++ b/agents/token_tracker.py @@ -0,0 +1,78 @@ +""" +BrainSteel Fin — Token Tracker +Logs LLM token usage per agent per call. +""" +import sqlite3, os +from datetime import datetime, timedelta + +COSTS = { + "google/gemma-4-31b-it:free": 0.0, + "google/gemma-4-26b-a4b-it:free": 0.0, + "deepseek/deepseek-v4-flash:free": 0.0, + "anthropic/claude-sonnet-4": 3.0, + "default": 0.0, +} + +def log_tokens(agent: str, model: str, usage: dict): + prompt_t = usage.get("prompt_tokens", 0) + completion_t = usage.get("completion_tokens", 0) + total_t = usage.get("total_tokens", prompt_t + completion_t) + cost = COSTS.get(model, COSTS["default"]) * total_t / 1_000_000 + db_path = os.path.join(os.path.dirname(__file__), "..", "data", "executions.db") + conn = sqlite3.connect(db_path) + c = conn.cursor() + c.execute( + "INSERT INTO token_usage (timestamp, agent, model, prompt_tokens, completion_tokens, total_tokens, cost_usd) VALUES (?,?,?,?,?,?,?)", + (datetime.now().isoformat(), agent, model, prompt_t, completion_t, total_t, cost) + ) + conn.commit() + conn.close() + +def get_token_stats(days: int = 7) -> dict: + db_path = os.path.join(os.path.dirname(__file__), "..", "data", "executions.db") + conn = sqlite3.connect(db_path) + c = conn.cursor() + cutoff = (datetime.now() - timedelta(days=days)).isoformat() + + c.execute(""" + SELECT DATE(timestamp) as day, agent, + SUM(prompt_tokens) as p, SUM(completion_tokens) as c, SUM(total_tokens) as t + FROM token_usage + WHERE timestamp >= ? + GROUP BY day, agent + ORDER BY day, agent + """, (cutoff,)) + rows = c.fetchall() + + c.execute(""" + SELECT DATE(timestamp) as day, + SUM(prompt_tokens), SUM(completion_tokens), SUM(total_tokens), SUM(cost_usd) + FROM token_usage + WHERE timestamp >= ? + GROUP BY day ORDER BY day + """, (cutoff,)) + daily = c.fetchall() + + c.execute("SELECT SUM(total_tokens), SUM(cost_usd), COUNT(*) FROM token_usage") + total = c.fetchone() + conn.close() + + agents = ["Brief", "Decio", "PaperT", "Audit"] + days_set = sorted(set(r[0] for r in rows)) + chart_data = [] + for day in days_set: + entry = {"date": day} + for ag in agents: + row = next((r for r in rows if r[0] == day and r[1] == ag), None) + entry[ag] = row[4] if row else 0 + chart_data.append(entry) + + return { + "total_all_time": total[0] or 0, + "total_cost_usd": total[1] or 0.0, + "total_calls": total[2] or 0, + "daily_totals": [{"date": r[0], "prompt": r[1], "completion": r[2], "total": r[3], "cost": r[4]} + for r in daily], + "chart_data": chart_data, + "agents": agents, + } \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..6f933c8 --- /dev/null +++ b/app.py @@ -0,0 +1,702 @@ +""" +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 + +app = Flask(__name__) +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 = sqlite3.connect(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 + ) + """) + conn.commit() + conn.close() + +def init_agents_db(): + """Agent registry — onboarding + state.""" + conn = sqlite3.connect(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 + ) + """) + conn.commit() + conn.close() + +def seed_agents(): + """Seed default agents on first run.""" + conn = sqlite3.connect(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)) + conn.commit() + 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 = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("INSERT INTO executions (timestamp, agent, action, result, status) VALUES (?, ?, ?, ?, ?)", + (datetime.now().isoformat(), agent, action, result, status)) + conn.commit() + conn.close() + # Also log to agent_logs + _log_agent_event(agent, "execution", result) + +def _log_agent_event(agent_id, event, result, duration_ms=0): + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + c.execute("INSERT INTO agent_logs (agent_id, timestamp, event, duration_ms, result) VALUES (?, ?, ?, ?, ?)", + (agent_id, datetime.now().isoformat(), event, duration_ms, result)) + conn.commit() + 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]) + # Also persist brief key metrics to state for /api/state + 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"), + }) + log_execution("brief", "market_analysis", brief_summary, "success") + _update_agent_stats("brief", t1 - t0, True) + + # ── 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], + }) + log_execution("decio", "strategy_decision", decio_data.get("decision", "HOLD"), "success") + _update_agent_stats("decio", t1 - t0, True) + + # ── 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]) + log_execution("papert", "order_execution", papert_data.get("result", ""), "success") + _update_agent_stats("papert", t1 - t0, True) + + # ── 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]) + log_execution("audit", "compliance_check", audit_data.get("summary", ""), "success") + _update_agent_stats("audit", t1 - t0, True) + + # ── Save pipeline run ── + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute(""" + INSERT INTO pipeline_runs (started_at, finished_at, brief_result, decio_decision, papert_result, audit_report, status) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + start.isoformat(), + datetime.now().isoformat(), + brief_data.get("summary", ""), + decio_data.get("decision", "HOLD"), + papert_data.get("result", ""), + json.dumps(audit_data), + "success" + )) + conn.commit() + conn.close() + + except Exception as e: + log_execution("pipeline", "error", str(e), "error") + state.update("brief", "error", f"Erro: {str(e)[:80]}") + _update_agent_stats("brief", datetime.now() - start, False) + + finally: + state.set_pipeline_running(False) + state.update("decio", "idle", "Aguardando...") + state.update("papert", "idle", "Aguardando...") + state.update("audit", "idle", "Aguardando...") + +def _update_agent_stats(agent_id, duration, success): + conn = sqlite3.connect(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)) + conn.commit() + 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 = sqlite3.connect(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 = sqlite3.connect(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 = sqlite3.connect(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 = sqlite3.connect(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/", methods=["GET"]) +def get_agent(agent_id): + conn = sqlite3.connect(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/", methods=["PUT"]) +def update_agent(agent_id): + """Update agent config (model, provider, status, intent).""" + data = request.json + conn = sqlite3.connect(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) + conn.commit() + conn.close() + return jsonify({"status": "updated", "id": agent_id}) + +@app.route("/api/agents/", methods=["DELETE"]) +def delete_agent(agent_id): + """Remove agent from registry.""" + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + c.execute("DELETE FROM agent_logs WHERE agent_id=?", (agent_id,)) + c.execute("DELETE FROM agents WHERE id=?", (agent_id,)) + conn.commit() + 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 = sqlite3.connect(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 + )) + conn.commit() + 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 = sqlite3.connect(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/", methods=["GET"]) +def get_agent_logs(agent_id): + """Event log for specific agent.""" + conn = sqlite3.connect(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 = sqlite3.connect(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 = sqlite3.connect(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", {})) + )) + conn.commit() + conn.close() + return jsonify({"status": "registered", "id": service_id}), 201 + +@app.route("/api/services//ping", methods=["POST"]) +def ping_service(service_id): + """Health check a service endpoint.""" + import requests as req + conn = sqlite3.connect(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 = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + c.execute("UPDATE services SET status=?, last_check=? WHERE id=?", + (status, datetime.now().isoformat(), service_id)) + conn.commit() + 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 = sqlite3.connect(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 + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=3100, debug=False) \ No newline at end of file diff --git a/data/.env b/data/.env new file mode 100644 index 0000000..e016ff6 --- /dev/null +++ b/data/.env @@ -0,0 +1,3 @@ +DECIO_API_KEY=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 +BRIEF_API_KEY=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 +PAPERT_API_KEY=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 diff --git a/data/agents.db b/data/agents.db new file mode 100644 index 0000000..e901a74 Binary files /dev/null and b/data/agents.db differ diff --git a/data/executions.db b/data/executions.db new file mode 100644 index 0000000..59e2b55 Binary files /dev/null and b/data/executions.db differ diff --git a/data/portfolio.db b/data/portfolio.db new file mode 100644 index 0000000..0292b90 Binary files /dev/null and b/data/portfolio.db differ diff --git a/logs/watchdog.log b/logs/watchdog.log new file mode 100644 index 0000000..4849533 --- /dev/null +++ b/logs/watchdog.log @@ -0,0 +1,10 @@ +[2026-05-17T14:56:04Z] Watchdog started (PID=3477519) +[2026-05-17T14:56:04Z] Waiting 236s until next trigger +[2026-05-17T15:00:04Z] Pipeline triggered OK (HTTP 200) +[2026-05-17T15:00:04Z] Waiting 1796s until next trigger +[2026-05-17T15:04:52Z] Watchdog started (PID=3533452) +[2026-05-17T15:04:52Z] Waiting 1508s until next trigger +[2026-05-17T15:30:00Z] Pipeline triggered OK (HTTP 200) +[2026-05-17T15:30:00Z] Waiting 1800s until next trigger +[2026-05-17T16:00:02Z] Pipeline triggered OK (HTTP 200) +[2026-05-17T16:00:02Z] Waiting 1798s until next trigger diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..30692b7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +requests diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..e7d86b0 --- /dev/null +++ b/static/script.js @@ -0,0 +1,608 @@ +/** + * BrainSteel Fin — Dashboard JavaScript + * 4-Agent Pipeline: Brief → Decio → PaperT → Audit + */ + +// ── State ───────────────────────────────────────────────────────────────────── +let autoMode = false; +let autoInterval = null; +let pollInterval = null; +const AGENT_EMOJIS = { brief: "📊", decio: "🎯", papert: "⚡", audit: "🔍" }; +const AGENT_COLORS = { brief: "#4fc3f7", decio: "#ffd54f", papert: "#69f0ae", audit: "#b388ff" }; + +// ── Init ───────────────────────────────────────────────────────────────────── +document.addEventListener('DOMContentLoaded', () => { + updateClock(); + setInterval(updateClock, 1000); + startPolling(); + loadAgentRegistry(); + loadMetrics(); + loadServices(); + loadPipelineVisual(); + loadTokenChart(); + updateStatus('Sistema operacional', 'online'); +}); + +// ── Clock ───────────────────────────────────────────────────────────────────── +function updateClock() { + const now = new Date(); + document.getElementById('clock').textContent = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} + +// ── Polling ─────────────────────────────────────────────────────────────────── +function startPolling() { + pollState(); + pollInterval = setInterval(pollState, 3000); +} + +function pollState() { + fetch('/api/state') + .then(r => r.json()) + .then(data => { + updateAgentUI('brief', data.brief); + updateAgentUI('decio', data.decio); + updateAgentUI('papert', data.papert); + updateAgentUI('audit', data.audit); + document.getElementById('btnRun').disabled = data.pipeline_running; + updateStatus(data.pipeline_running ? 'Pipeline em execução' : 'Aguardando', data.pipeline_running ? 'running' : 'online'); + if (!data.pipeline_running && data.last_pipeline) { + showDecisionBanner(data); + } + }).catch(err => console.error('Poll error:', err)); +} + +function updateAgentUI(agent, data) { + const node = document.getElementById(`node-${agent}`); + const statusEl = document.getElementById(`status-${agent}`); + const msgEl = document.getElementById(`msg-${agent}`); + if (!node) return; + node.className = `pipeline-node ${data.status || 'idle'}`; + if (statusEl) { + const labels = { idle: 'Aguardando', working: 'Trabalhando', done: 'Concluído', error: 'Erro', waiting: 'Aguardando' }; + statusEl.textContent = labels[data.status] || data.status; + statusEl.className = `agent-status ${data.status}`; + } + if (msgEl) msgEl.textContent = data.message || ''; +} + +// ── Status ──────────────────────────────────────────────────────────────────── +function updateStatus(text, type) { + const dot = document.getElementById('statusDot'); + const txt = document.getElementById('statusText'); + if (dot) dot.className = `status-dot ${type}`; + if (txt) txt.textContent = text; +} + +// ── Pipeline ───────────────────────────────────────────────────────────────── +function runPipeline() { + fetch('/api/run', { method: 'POST' }) + .then(r => r.json()) + .then(d => { + if (d.error) { alert(d.error); return; } + addLog('SYS', 'Pipeline iniciado'); + }).catch(err => console.error(err)); +} + +function toggleAuto() { + autoMode = !autoMode; + const el = document.getElementById('autoStatus'); + if (el) el.textContent = autoMode ? 'ON' : 'OFF'; + if (autoMode) { + addLog('SYS', 'Modo automático ativado — a cada 5 min'); + autoInterval = setInterval(runPipeline, 5 * 60 * 1000); + } else { + clearInterval(autoInterval); + addLog('SYS', 'Modo automático desativado'); + } +} + +// ── Decision Banner ──────────────────────────────────────────────────────────── +function showDecisionBanner(data) { + const banner = document.getElementById('lastDecisionBanner'); + if (!banner) return; + const decision = data.decio?.message || 'HOLD'; + const cls = decision.includes('BUY') ? 'buy' : decision.includes('SELL') ? 'sell' : 'hold'; + banner.className = `decision-banner ${cls}`; + banner.textContent = `Última decisão: ${decision}`; + banner.style.display = 'block'; +} + +// ── Item 1: Agent Registry ──────────────────────────────────────────────────── +function loadAgentRegistry() { + fetch('/api/agents') + .then(r => r.json()) + .then(agents => { + const container = document.getElementById('agentCards'); + if (!container) return; + container.innerHTML = agents.map(a => ` +
+
+ ${AGENT_EMOJIS[a.id] || '🤖'} +
+
${a.name}
+
${a.role || ''}
+
+
+
${a.intent || '—'}
+
+ ⏱ ${a.cycle_count || 0} ciclos + ✓ ${a.success_count || 0} + ✗ ${a.fail_count || 0} +
+
${a.model || '—'} @ ${a.provider || '?'}
+
+ + +
+
+ `).join(''); + }).catch(err => console.error('Agent registry error:', err)); +} + +function showAddAgent() { + document.getElementById('modalAgentTitle').textContent = 'Novo Agente'; + ['agentId','agentName','agentRole','agentIntent','agentModel','agentProvider'].forEach(id => { + const el = document.getElementById(id); + if (el) el.value = id === 'agentId' ? '' : ''; + if (id === 'agentModel') el.value = 'deepseek/deepseek-v4-flash:free'; + if (id === 'agentProvider') el.value = 'openrouter'; + }); + document.getElementById('modalAgent').style.display = 'flex'; +} + +function editAgent(id) { + fetch(`/api/agents/${id}`).then(r => r.json()).then(a => { + document.getElementById('modalAgentTitle').textContent = `Editar: ${a.name}`; + document.getElementById('agentId').value = a.id; + document.getElementById('agentId').readOnly = true; + document.getElementById('agentName').value = a.name || ''; + document.getElementById('agentRole').value = a.role || ''; + document.getElementById('agentIntent').value = a.intent || ''; + document.getElementById('agentModel').value = a.model || ''; + document.getElementById('agentProvider').value = a.provider || ''; + document.getElementById('modalAgent').style.display = 'flex'; + }); +} + +function saveAgent() { + const id = document.getElementById('agentId').value.trim(); + if (!id) { alert('ID é obrigatório'); return; } + const data = { + id, name: document.getElementById('agentName').value.trim(), + role: document.getElementById('agentRole').value.trim(), + intent: document.getElementById('agentIntent').value.trim(), + model: document.getElementById('agentModel').value.trim(), + provider: document.getElementById('agentProvider').value.trim() + }; + fetch(`/api/agents/${id}`, { + method: id ? 'PUT' : 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }).then(r => r.json()).then(() => { + closeAgent(); + loadAgentRegistry(); + addLog('SYS', `Agente ${id} atualizado`); + }); +} + +function deleteAgent(id) { + if (!confirm(`Remover agente ${id}?`)) return; + fetch(`/api/agents/${id}`, { method: 'DELETE' }).then(r => r.json()).then(() => { + loadAgentRegistry(); + addLog('SYS', `Agente ${id} removido`); + }); +} + +function closeAgent() { + document.getElementById('modalAgent').style.display = 'none'; + const el = document.getElementById('agentId'); + if (el) el.readOnly = false; +} + +// ── Item 2: Agent Metrics ──────────────────────────────────────────────────── +function loadMetrics() { + fetch('/api/agent_metrics') + .then(r => r.json()) + .then(metrics => { + const grid = document.getElementById('metricsGrid'); + if (!grid) return; + grid.innerHTML = metrics.map(m => { + const color = AGENT_COLORS[m.id] || '#888'; + const rate = m.success_rate || 0; + const avg = m.avg_duration_ms ? `${(m.avg_duration_ms/1000).toFixed(1)}s` : '—'; + return ` +
+
${AGENT_EMOJIS[m.id] || '🤖'} ${m.name}
+
${rate}%
+
${m.cycle_count || 0} ciclos · avg ${avg}
+
+
`; + }).join(''); + }).catch(err => console.error('Metrics error:', err)); +} + +// ── Item 3: Services ───────────────────────────────────────────────────────── +function loadServices() { + fetch('/api/services') + .then(r => r.json()) + .then(services => { + const list = document.getElementById('servicesList'); + if (!list) return; + if (!services.length) { + list.innerHTML = '
Nenhum serviço registado. Clique em + Novo Serviço.
'; + return; + } + list.innerHTML = services.map(s => ` +
+ + ${s.name} + ${s.type || ''} + ${s.endpoint ? s.endpoint.slice(0,40) : ''} +
+ + +
+
+ `).join(''); + }).catch(err => console.error('Services error:', err)); +} + +function showAddService() { + ['serviceName','serviceType','serviceEndpoint','serviceStatus'].forEach(id => { + const el = document.getElementById(id); + if (el) el.value = ''; + }); + document.getElementById('modalService').style.display = 'flex'; +} + +function saveService() { + const name = document.getElementById('serviceName').value.trim(); + if (!name) { alert('Nome é obrigatório'); return; } + const data = { + name, + type: document.getElementById('serviceType').value.trim(), + endpoint: document.getElementById('serviceEndpoint').value.trim(), + status: document.getElementById('serviceStatus').value.trim() || 'unknown' + }; + fetch('/api/services', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }).then(r => r.json()).then(() => { + closeService(); + loadServices(); + addLog('SYS', `Serviço ${name} registado`); + }); +} + +function pingService(id) { + fetch(`/api/services/${id}/ping`, { method: 'POST' }) + .then(r => r.json()) + .then(d => { + loadServices(); + addLog('SYS', `Ping ${id}: ${d.status}`); + }); +} + +function pingAllServices() { + fetch('/api/services').then(r => r.json()).then(svcs => { + svcs.forEach(s => pingService(s.id)); + addLog('SYS', 'Ping em todos os serviços'); + }); +} + +function deleteService(id) { + fetch(`/api/services/${id}`, { method: 'DELETE' }).then(() => { + loadServices(); + addLog('SYS', `Serviço ${id} removido`); + }); +} + +function closeService() { + document.getElementById('modalService').style.display = 'none'; +} + +// ── Item 4: Pipeline Visual ─────────────────────────────────────────────────── +function loadPipelineVisual() { + fetch('/api/pipeline/visual') + .then(r => r.json()) + .then(data => { + // Visual flow + const flow = document.getElementById('visualFlow'); + if (flow) { + flow.innerHTML = data.nodes.map((n, i) => ` +
+
${AGENT_EMOJIS[n.id] || '🤖'}
+
${n.label}
+
${n.status || 'idle'}
+ ${n.total_runs ? `
${n.total_runs}x
` : ''} +
+ ${i < data.nodes.length - 1 ? '' : ''} + `).join(''); + } + // Timeline + const tl = document.getElementById('visualTimeline'); + if (tl) { + tl.innerHTML = data.recent_runs.map(r => ` +
+ ${r.started_at ? r.started_at.slice(11,19) : '—'} + ${r.decio_decision || '?'} + ${r.status} +
+ `).join(''); + } + }).catch(err => console.error('Pipeline visual error:', err)); +} + +// ── History Modal ───────────────────────────────────────────────────────────── +function showHistory() { + fetch('/api/pipeline_history') + .then(r => r.json()) + .then(data => { + const content = document.getElementById('historyContent'); + if (!content) return; + content.innerHTML = data.map(r => ` +
+
+ ${r.decio_decision || '?'} + ${r.started_at ? r.started_at.slice(0,16).replace('T',' ') : '—'} +
+
${(r.brief_result || '').slice(0,80)}
+
+ `).join(''); + document.getElementById('modalHistory').style.display = 'flex'; + }); +} + +function closeHistory() { + document.getElementById('modalHistory').style.display = 'none'; +} + +// ── Log ─────────────────────────────────────────────────────────────────────── +function addLog(agent, message) { + const container = document.getElementById('logContainer'); + if (!container) return; + const now = new Date(); + const ts = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const div = document.createElement('div'); + div.className = 'log-entry'; + div.innerHTML = `${ts}${agent}${message}`; + container.prepend(div); + while (container.children.length > 50) container.removeChild(container.lastChild); +} + +// Refresh all sections periodically +setInterval(() => { + loadAgentRegistry(); + loadMetrics(); + loadServices(); + loadPipelineVisual(); + loadPortfolio(); +}, 15000); + +// ── Portfolio ─────────────────────────────────────────────────────────────── +function loadPortfolio() { + fetch('/api/portfolio') + .then(r => r.json()) + .then(data => { + if (data.error) return; + const bal = data.current_balance || 0; + const pnl = data.total_pnl || 0; + const pnlPct = data.total_pnl_pct || 0; + const trades = data.total_trades || 0; + const wins = data.wins || 0; + const losses = data.losses || 0; + const wr = data.win_rate || 0; + const init = data.initial_balance || 10000; + + document.getElementById('pfBalance').textContent = `$${bal.toLocaleString('pt-BR', {minimumFractionDigits:2})}`; + const pnlEl = document.getElementById('pfPnl'); + pnlEl.textContent = `P&L: ${pnl >= 0 ? '+' : ''}$${pnl.toLocaleString('pt-BR',{minimumFractionDigits:2})} (${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%)`; + pnlEl.style.color = pnl >= 0 ? '#69f0ae' : '#ff6b6b'; + document.getElementById('pfTrades').textContent = `${trades} trades | ${wins}W/${losses}L | WR: ${wr}%`; + + // Draw chart + drawChart(data.chart_data || [], init, bal); + + // Recent trades list + const tradesEl = document.getElementById('portfolioTrades'); + const recent = data.recent_trades || []; + if (recent.length === 0) { + tradesEl.innerHTML = '
Nenhum trade ainda. Execute o pipeline para começar.
'; + } else { + tradesEl.innerHTML = recent.slice(0, 10).map(t => { + const cls = t.action === 'BUY' ? '#69f0ae' : t.action === 'SELL' ? (t.result === 'WIN' ? '#69f0ae' : '#ff6b6b') : '#888'; + return `
+ ${t.date ? t.date.slice(5) : '—'} + ${t.action} + ${t.btc_amount ? t.btc_amount.toFixed(6)+' BTC' : '—'} + @ $${Number(t.entry_price).toLocaleString('pt-BR',{minimumFractionDigits:0})} + ${t.result || ''} +
`; + }).join(''); + } + }).catch(err => console.error('Portfolio error:', err)); +} + +function drawChart(chartData, initial, current) { + const canvas = document.getElementById('chartCanvas'); + if (!canvas || !chartData.length) return; + const ctx = canvas.getContext('2d'); + const W = canvas.width; + const H = canvas.height; + ctx.clearRect(0, 0, W, H); + + // Draw grid + ctx.strokeStyle = 'rgba(255,255,255,0.05)'; + ctx.lineWidth = 1; + for (let i = 0; i < 5; i++) { + const y = (H / 4) * i; + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); + } + + // Draw data line + if (chartData.length < 2) return; + const allBalances = chartData.map(d => d[1]); + const minBal = Math.min(...allBalances, initial) * 0.98; + const maxBal = Math.max(...allBalances, initial) * 1.02; + const range = maxBal - minBal || 1; + + const toX = (i) => (i / (chartData.length - 1)) * W; + const toY = (v) => H - ((v - minBal) / range) * H; + + // Area fill + ctx.beginPath(); + ctx.moveTo(toX(0), H); + chartData.forEach(([_, bal], i) => ctx.lineTo(toX(i), toY(bal))); + ctx.lineTo(toX(chartData.length - 1), H); + ctx.closePath(); + ctx.fillStyle = 'rgba(79,195,247,0.15)'; + ctx.fill(); + + // Line + ctx.beginPath(); + ctx.moveTo(toX(0), toY(chartData[0][1])); + chartData.forEach(([_, bal], i) => ctx.lineTo(toX(i), toY(bal))); + ctx.strokeStyle = '#4fc3f7'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Initial balance reference + const y0 = toY(initial); + ctx.setLineDash([4, 4]); + ctx.strokeStyle = 'rgba(179,136,255,0.5)'; + ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(0, y0); ctx.lineTo(W, y0); ctx.stroke(); + ctx.setLineDash([]); + + // Current dot + const lastX = toX(chartData.length - 1); + const lastY = toY(chartData[chartData.length - 1][1]); + ctx.beginPath(); + ctx.arc(lastX, lastY, 4, 0, Math.PI * 2); + ctx.fillStyle = current >= initial ? '#69f0ae' : '#ff6b6b'; + ctx.fill(); +} + +function resetPortfolio() { + if (!confirm('Resetar portfolio para $10.000? Todos os trades serão apagados.')) return; + fetch('/api/portfolio/reset', { method: 'POST' }) + .then(r => r.json()) + .then(() => { + loadPortfolio(); + addLog('SYS', 'Portfolio resetado para $10.000'); + }); +} + +// ── Token Chart ───────────────────────────────────────────────────────────── +const TOKEN_AGENTS = [ + { key: 'Brief', color: '#4fc3f7' }, + { key: 'Decio', color: '#ffd54f' }, + { key: 'PaperT', color: '#69f0ae' }, + { key: 'Audit', color: '#b388ff' }, +]; + +function loadTokenChart() { + const days = parseInt(document.getElementById('tokenDays')?.value || 7); + Promise.all([ + fetch(`/api/token_stats?days=${days}`).then(r => r.json()), + fetch('/api/token_totals').then(r => r.json()), + ]).then(([stats, totals]) => { + drawTokenBarChart(stats); + renderTokenTotals(totals); + document.getElementById('tokenTotals').textContent = + `Total: ${(stats.total_all_time || 0).toLocaleString()} tok | $${(stats.total_cost_usd || 0).toFixed(4)}`; + }).catch(() => {}); +} + +function drawTokenBarChart(stats) { + const canvas = document.getElementById('tokenChart'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const W = canvas.width = 800; + const H = canvas.height = 200; + ctx.clearRect(0, 0, W, H); + + const data = stats.chart_data || []; + if (!data.length) { + ctx.fillStyle = '#556'; + ctx.font = '13px monospace'; + ctx.textAlign = 'center'; + ctx.fillText('Sem dados de tokens ainda. Os agentes precisam ser executados.', W / 2, H / 2); + return; + } + + const labels = data.map(d => d.date.slice(5)); // MM-DD + const days = data.length; + const groupW = (W - 80) / Math.max(days, 1); + const barW = Math.min((groupW - 8) / TOKEN_AGENTS.length, 28); + const maxVal = Math.max(...TOKEN_AGENTS.map(a => Math.max(...data.map(d => d[a.key] || 0))), 1); + + // Grid + ctx.strokeStyle = 'rgba(99,110,200,0.15)'; + ctx.lineWidth = 1; + ctx.font = '11px monospace'; + ctx.fillStyle = '#556'; + for (let i = 0; i <= 4; i++) { + const y = 10 + (H - 30) * (1 - i / 4); + ctx.beginPath(); ctx.moveTo(40, y); ctx.lineTo(W - 10, y); ctx.stroke(); + ctx.textAlign = 'right'; + const val = Math.round(maxVal * i / 4); + ctx.fillText(val.toLocaleString(), 38, y + 4); + } + + // Bars + data.forEach((d, i) => { + const x = 50 + i * groupW; + TOKEN_AGENTS.forEach((ag, j) => { + const val = d[ag.key] || 0; + const barH = val / maxVal * (H - 30); + const bx = x + j * (barW + 2); + const by = H - 20 - barH; + ctx.fillStyle = ag.color; + ctx.fillRect(bx, by, barW - 1, barH); + // Value label + if (val > 0 && barH > 12) { + ctx.fillStyle = '#fff'; + ctx.font = '9px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(val > 999 ? Math.round(val/1000)+'k' : val, bx + barW/2, by + 10); + } + }); + // X label + ctx.fillStyle = '#778'; + ctx.font = '10px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(labels[i], x + groupW / 2, H - 4); + }); + + // Legend + TOKEN_AGENTS.forEach((ag, i) => { + const lx = W - 160 + (i % 2) * 80; + const ly = 16 + Math.floor(i / 2) * 14; + ctx.fillStyle = ag.color; + ctx.fillRect(lx, ly - 9, 10, 10); + ctx.fillStyle = '#aab'; + ctx.font = '10px monospace'; + ctx.textAlign = 'left'; + ctx.fillText(ag.key, lx + 14, ly); + }); +} + +function renderTokenTotals(totals) { + const tbody = document.getElementById('tokenTotalsBody'); + if (!tbody) return; + tbody.innerHTML = ''; + (totals || []).forEach(r => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${r.agent} + ${r.prompt ? r.prompt.toLocaleString() : '-'} + ${r.completion ? r.completion.toLocaleString() : '-'} + ${r.total ? r.total.toLocaleString() : '-'} + ${r.calls || 0} + $${r.cost ? r.cost.toFixed(4) : '0.0000'} + `; + tbody.appendChild(tr); + }); +} \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..b32436f --- /dev/null +++ b/static/style.css @@ -0,0 +1,407 @@ +/* BrainSteel Fin — Anime-style Dashboard */ +/* ── Variables ─────────────────────────────────────────── */ +:root { + --bg-dark: #0f0f1a; + --bg-card: #1a1a2e; + --bg-card-hover: #222240; + --accent-blue: #4fc3f7; + --accent-red: #ff6b6b; + --accent-green: #69f0ae; + --accent-yellow: #ffd54f; + --accent-purple: #b388ff; + --text-primary: #e8e8f0; + --text-secondary: #8888aa; + --border-radius: 16px; + --shadow: 0 8px 32px rgba(0,0,0,0.4); +} + +/* ── Reset & Base ───────────────────────────────────────── */ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + font-family: 'Exo 2', 'Noto Sans JP', sans-serif; + background: var(--bg-dark); + color: var(--text-primary); + min-height: 100vh; + overflow-x: hidden; +} + +/* ── Dashboard Layout ───────────────────────────────────── */ +.dashboard { + max-width: 1400px; + margin: 0 auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 24px; +} + +/* ── Header ──────────────────────────────────────────────── */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + background: var(--bg-card); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + border: 1px solid rgba(79, 195, 247, 0.1); +} +.logo { display: flex; align-items: center; gap: 12px; font-size: 1.5rem; } +.logo-icon { font-size: 2rem; } +.logo-text strong { color: var(--accent-blue); font-weight: 700; } +.header-status { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); } +.status-dot { + width: 12px; height: 12px; border-radius: 50%; + background: var(--accent-green); + box-shadow: 0 0 8px var(--accent-green); + animation: pulse 2s infinite; +} +.status-dot.running { background: var(--accent-yellow); box-shadow: 0 0 8px var(--accent-yellow); } +.status-dot.error { background: var(--accent-red); box-shadow: 0 0 8px var(--accent-red); } +@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } } +.header-time { font-variant-numeric: tabular-nums; color: var(--accent-blue); font-size: 1.1rem; } + +/* ── Buttons ─────────────────────────────────────────────── */ +.btn { + padding: 10px 20px; + border: none; + border-radius: 10px; + font-family: inherit; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} +.btn-primary { background: var(--accent-blue); color: #000; } +.btn-primary:hover { background: #7dd3fc; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(79,195,247,0.4); } +.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } +.btn-secondary { background: var(--bg-card-hover); color: var(--text-primary); border: 1px solid rgba(255,255,255,0.1); } +.btn-secondary:hover { background: #2a2a48; } +.btn-small { padding: 6px 14px; font-size: 0.8rem; background: var(--bg-card-hover); color: var(--text-primary); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; cursor: pointer; } +.btn-small:hover { background: #2a2a48; } + +/* ── Pipeline Section ────────────────────────────────────── */ +.pipeline-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); border: 1px solid rgba(79,195,247,0.1); } +.pipeline-section h2 { margin-bottom: 16px; color: var(--text-primary); } +.pipeline-flow { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; } +.pipeline-arrow { font-size: 2rem; color: var(--text-secondary); } + +/* ── Pipeline Node ────────────────────────────────────────── */ +.pipeline-node { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 16px; + border-radius: 14px; + background: var(--bg-card-hover); + border: 2px solid transparent; + min-width: 120px; + transition: all 0.3s; + cursor: pointer; + position: relative; +} +.pipeline-node:hover { transform: translateY(-4px); border-color: var(--accent-blue); } +.pipeline-node.idle { border-color: rgba(136,136,170,0.3); } +.pipeline-node.working { border-color: var(--accent-yellow); box-shadow: 0 0 20px rgba(255,213,79,0.3); animation: glow-yellow 1.5s infinite; } +.pipeline-node.waiting { border-color: rgba(136,136,170,0.5); } +.pipeline-node.done { border-color: var(--accent-green); box-shadow: 0 0 15px rgba(105,240,174,0.3); } +.pipeline-node.error { border-color: var(--accent-red); } +@keyframes glow-yellow { 0%,100% { box-shadow: 0 0 20px rgba(255,213,79,0.3); } 50% { box-shadow: 0 0 35px rgba(255,213,79,0.6); } } + +/* ── Agent Avatar ─────────────────────────────────────────── */ +.agent-avatar { + width: 56px; height: 56px; + border-radius: 50%; + display: flex; align-items: center; justify-content: center; + font-size: 1.8rem; + position: relative; +} +.avatar-ring { + position: absolute; inset: -3px; border-radius: 50%; + border: 2px solid; + opacity: 0.6; + animation: spin 4s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } +.pipeline-node.working .avatar-ring { border-color: var(--accent-yellow); } +.pipeline-node.done .avatar-ring { border-color: var(--accent-green); animation-duration: 8s; } +.pipeline-node.error .avatar-ring { border-color: var(--accent-red); animation-duration: 0.5s; } + +.agent-info { text-align: center; } +.agent-name { font-weight: 700; font-size: 1rem; } +.agent-role { font-size: 0.7rem; color: var(--text-secondary); display: block; } +.agent-status { + font-size: 0.7rem; + padding: 2px 10px; + border-radius: 20px; + background: rgba(255,255,255,0.1); +} +.agent-status.working { background: rgba(255,213,79,0.2); color: var(--accent-yellow); } +.agent-status.done { background: rgba(105,240,174,0.2); color: var(--accent-green); } +.agent-status.error { background: rgba(255,107,107,0.2); color: var(--accent-red); } +.agent-status.waiting { background: rgba(136,136,170,0.2); } +.agent-message { font-size: 0.65rem; color: var(--text-secondary); text-align: center; max-width: 100px; overflow: hidden; } + +/* ── Pipeline Controls ────────────────────────────────────── */ +.pipeline-controls { display: flex; gap: 10px; margin-top: 16px; justify-content: center; flex-wrap: wrap; } + +/* ── Decision Banner ─────────────────────────────────────── */ +.decision-banner { + margin-top: 12px; + padding: 10px 20px; + border-radius: 10px; + text-align: center; + font-weight: 700; + font-size: 1rem; +} +.decision-banner.buy { background: rgba(105,240,174,0.15); color: var(--accent-green); border: 1px solid var(--accent-green); } +.decision-banner.sell { background: rgba(255,107,107,0.15); color: var(--accent-red); border: 1px solid var(--accent-red); } +.decision-banner.hold { background: rgba(255,213,79,0.15); color: var(--accent-yellow); border: 1px solid var(--accent-yellow); } + +/* ── Office Section ───────────────────────────────────────── */ +.office-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.office-section h2 { margin-bottom: 16px; } +.office-floor { position: relative; } +.floor-grid { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; } +.agent-desk { + flex: 1; min-width: 140px; + padding: 16px; + background: var(--bg-card-hover); + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.05); + display: flex; flex-direction: column; align-items: center; gap: 8px; +} +.desk-icon { font-size: 2rem; } +.desk-status { font-size: 0.7rem; color: var(--text-secondary); text-align: center; } +.chat-bubble { + margin-top: 16px; padding: 12px 20px; + background: var(--bg-card-hover); + border-radius: 12px; border: 1px solid rgba(79,195,247,0.2); + font-size: 0.85rem; color: var(--text-secondary); +} + +/* ── Registry Section ─────────────────────────────────────── */ +.registry-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.registry-section h2 { margin-bottom: 12px; } +.registry-controls { display: flex; gap: 10px; margin-bottom: 16px; } +.agent-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; } +.agent-card { + padding: 16px; + background: var(--bg-card-hover); + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.05); + transition: all 0.2s; +} +.agent-card:hover { border-color: var(--accent-blue); transform: translateY(-2px); } +.agent-card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } +.agent-card-emoji { font-size: 1.5rem; } +.agent-card-name { font-weight: 700; font-size: 1rem; } +.agent-card-role { font-size: 0.75rem; color: var(--text-secondary); } +.agent-card-intent { font-size: 0.8rem; color: var(--text-secondary); margin: 8px 0; line-height: 1.4; } +.agent-card-stats { display: flex; gap: 12px; font-size: 0.75rem; margin-top: 8px; } +.stat-badge { padding: 2px 8px; border-radius: 6px; background: rgba(255,255,255,0.05); } +.stat-badge.runs { color: var(--accent-blue); } +.stat-badge.ok { color: var(--accent-green); } +.stat-badge.fail { color: var(--accent-red); } +.agent-card-model { font-size: 0.65rem; color: var(--accent-purple); margin-top: 4px; font-family: monospace; } +.agent-card-actions { display: flex; gap: 6px; margin-top: 10px; } +.agent-card-actions button { flex: 1; padding: 5px; font-size: 0.7rem; border-radius: 6px; border: none; cursor: pointer; } + +/* ── Two Column Layout ────────────────────────────────────── */ +.two-col-section { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; } +@media (max-width: 900px) { .two-col-section { grid-template-columns: 1fr; } } + +/* ── Metrics Section ─────────────────────────────────────── */ +.metrics-section, .services-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.metrics-section h2, .services-section h2 { margin-bottom: 12px; } +.metrics-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; } +.metric-card { + padding: 14px; + background: var(--bg-card-hover); + border-radius: 10px; + border-left: 3px solid; +} +.metric-card.brief { border-left-color: var(--accent-blue); } +.metric-card.decio { border-left-color: var(--accent-yellow); } +.metric-card.papert { border-left-color: var(--accent-green); } +.metric-card.audit { border-left-color: var(--accent-purple); } +.metric-name { font-weight: 600; font-size: 0.85rem; margin-bottom: 6px; } +.metric-value { font-size: 1.2rem; font-weight: 700; } +.metric-sub { font-size: 0.7rem; color: var(--text-secondary); } +.metric-bar { height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin-top: 6px; overflow: hidden; } +.metric-bar-fill { height: 100%; border-radius: 2px; transition: width 0.5s; } + +/* ── Services Section ─────────────────────────────────────── */ +.services-controls { display: flex; gap: 10px; margin-bottom: 12px; } +.services-list { display: flex; flex-direction: column; gap: 8px; max-height: 300px; overflow-y: auto; } +.service-item { + display: flex; align-items: center; gap: 10px; + padding: 10px 14px; + background: var(--bg-card-hover); + border-radius: 8px; + font-size: 0.85rem; +} +.service-status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } +.service-status-dot.healthy { background: var(--accent-green); } +.service-status-dot.degraded { background: var(--accent-yellow); } +.service-status-dot.unknown { background: var(--text-secondary); } +.service-status-dot.unreachable { background: var(--accent-red); } +.service-name { font-weight: 600; flex: 1; } +.service-type { font-size: 0.7rem; color: var(--text-secondary); } +.service-endpoint { font-size: 0.7rem; color: var(--accent-purple); font-family: monospace; } +.service-actions { display: flex; gap: 4px; } +.service-actions button { padding: 3px 8px; font-size: 0.65rem; border-radius: 4px; border: none; cursor: pointer; background: rgba(255,255,255,0.1); color: var(--text-secondary); } +.service-actions button:hover { background: rgba(255,255,255,0.2); } + +/* ── Visual Pipeline ─────────────────────────────────────── */ +.visual-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.visual-section h2 { margin-bottom: 16px; } +.visual-container { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; } +@media (max-width: 900px) { .visual-container { grid-template-columns: 1fr; } } +.visual-flow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.visual-node { + display: flex; flex-direction: column; align-items: center; gap: 4px; + padding: 12px 16px; + background: var(--bg-card-hover); + border-radius: 10px; + min-width: 90px; + position: relative; +} +.visual-node-label { font-size: 0.8rem; font-weight: 700; } +.visual-node-status { font-size: 0.65rem; color: var(--text-secondary); } +.visual-arrow { color: var(--text-secondary); font-size: 1.2rem; } +.visual-timeline { display: flex; flex-direction: column; gap: 4px; max-height: 200px; overflow-y: auto; } +.timeline-item { + padding: 8px 12px; + background: var(--bg-card-hover); + border-radius: 6px; + font-size: 0.75rem; + display: flex; justify-content: space-between; gap: 8px; +} +.timeline-ts { color: var(--text-secondary); flex-shrink: 0; } +.timeline-decision { font-weight: 700; } +.timeline-decision.BUY { color: var(--accent-green); } +.timeline-decision.SELL { color: var(--accent-red); } +.timeline-decision.HOLD { color: var(--accent-yellow); } + +/* ── Log Section ──────────────────────────────────────────── */ +.log-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.log-section h2 { margin-bottom: 12px; } +.log-container { max-height: 200px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; } +.log-entry { + display: flex; gap: 10px; align-items: baseline; + padding: 6px 10px; + background: var(--bg-card-hover); + border-radius: 6px; + font-size: 0.78rem; +} +.log-time { color: var(--text-secondary); flex-shrink: 0; font-variant-numeric: tabular-nums; } +.log-agent { font-weight: 700; min-width: 40px; color: var(--accent-blue); } +.log-message { color: var(--text-primary); flex: 1; } +.log-entry.system .log-agent { color: var(--accent-purple); } +.log-entry.error .log-agent { color: var(--accent-red); } + +/* ── Modals ──────────────────────────────────────────────── */ +.modal { + position: fixed; inset: 0; + background: rgba(0,0,0,0.7); + display: flex; align-items: center; justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} +.modal-content { + background: var(--bg-card); + border-radius: 16px; + padding: 24px; + width: 90%; max-width: 480px; + max-height: 90vh; + overflow-y: auto; + border: 1px solid rgba(79,195,247,0.2); + box-shadow: 0 20px 60px rgba(0,0,0,0.6); +} +.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } +.modal-header h3 { color: var(--text-primary); } +.modal-close { background: none; border: none; color: var(--text-secondary); font-size: 1.5rem; cursor: pointer; } +.modal-body { display: flex; flex-direction: column; gap: 10px; } +.modal-body label { font-size: 0.8rem; color: var(--text-secondary); font-weight: 600; } +.modal-body input, .modal-body textarea { + background: var(--bg-card-hover); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 8px; + padding: 10px 12px; + color: var(--text-primary); + font-family: inherit; + font-size: 0.9rem; + outline: none; +} +.modal-body input:focus, .modal-body textarea:focus { border-color: var(--accent-blue); } +.modal-footer { display: flex; gap: 10px; margin-top: 16px; justify-content: flex-end; } + +/* ── Scrollbar ───────────────────────────────────────────── */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: var(--bg-dark); } +::-webkit-scrollbar-thumb { background: var(--bg-card-hover); border-radius: 3px; } + +/* ── Portfolio Section ─────────────────────────────────────── */ +.portfolio-section { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 20px; + box-shadow: var(--shadow); + border: 1px solid rgba(105,240,174,0.2); +} +.portfolio-header { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 16px; +} +.portfolio-header h2 { margin: 0; flex: 1; } +.portfolio-summary { + display: flex; + gap: 20px; + align-items: center; + font-size: 0.85rem; +} +.portfolio-balance { font-size: 1.2rem; font-weight: 700; color: var(--accent-blue); } +.portfolio-pnl { font-weight: 600; } +.portfolio-trades { color: var(--text-secondary); } +.portfolio-actions { display: flex; gap: 8px; } +.portfolio-chart { + background: var(--bg-card-hover); + border-radius: 10px; + padding: 8px; + margin-bottom: 12px; +} +.portfolio-chart canvas { width: 100%; height: 180px; display: block; } +.portfolio-trades-list { max-height: 200px; overflow-y: auto; } +::-webkit-scrollbar-thumb:hover { background: #333; } + +/* ── Token Tracker ──────────────────────────────────────────── */ +.tokens-section { + background: rgba(20, 25, 50, 0.95); + border: 1px solid rgba(99, 110, 200, 0.3); + border-radius: 12px; + padding: 18px; + margin-bottom: 16px; +} +.tokens-controls { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 12px; +} +.tokens-controls label { color: #b0b8d0; font-size: 13px; } +.tokens-chart-wrap { + background: rgba(10, 12, 28, 0.8); + border-radius: 8px; + padding: 10px; + margin-bottom: 12px; +} +.tokens-table-wrap { overflow-x: auto; } +.tokens-table { width: 100%; border-collapse: collapse; font-size: 13px; color: #c8d0e8; } +.tokens-table th { background: rgba(99, 110, 200, 0.2); color: #a0aaff; padding: 8px 10px; text-align: left; } +.tokens-table td { padding: 6px 10px; border-bottom: 1px solid rgba(99, 110, 200, 0.1); } +.tokens-table tr:hover td { background: rgba(99, 110, 200, 0.08); } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..3166d48 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,213 @@ + + + + + + BrainSteel Fin — Agent Office + + + + +
+ +
+ +
+ + Sistema online +
+
+
+ + +
+

📊 Pipeline de Trading

+
+ +
+
+ + + +
+ +
+ + +
+

🏠 Escritório Virtual

+
+
+ +
+
+ Olá! Eu sou o escritório virtual da BrainSteel Fin. +
+
+
+ + +
+

👥 Cadastro de Agentes

+
+ + +
+
+ +
+
+ + +
+
+

📈 Métricas de Agentes

+
+ +
+
+
+

🔌 Serviços

+
+ + +
+
+ +
+
+
+ + +
+

🔭 Pipeline Visual em Tempo Real

+
+
+ +
+
+ +
+
+
+ + +
+
+

💹 Portfolio Simulado

+
+ $10.000,00 + P&L: $0,00 (0%) + 0 trades | 0% Win Rate +
+
+ +
+
+
+ +
+
+ +
+
+ + +
+

🧮 Tokens por Agente

+
+ + +
+
+ +
+
+ + + +
AgentePromptCompletionTotalChamadasCusto
+
+
+ + +
+

📝 Log de Atividades

+
+
+ --:-- + SYS + Aguardando primeira execução... +
+
+
+ + + + + + + +
+ + + + \ No newline at end of file