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
This commit is contained in:
@@ -0,0 +1 @@
|
||||
OPENROUTER_API_KEY=sk-or-v1-26c1cbfbe091551a814874f8141a2739b70deef89f464be668fdcac49cfabecd7
|
||||
+16
@@ -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
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
# BrainSteel Fin Agents
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+173
@@ -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"}
|
||||
+497
@@ -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
|
||||
+182
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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/<agent_id>", 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/<agent_id>", 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/<agent_id>", 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/<agent_id>", 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/<service_id>/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)
|
||||
@@ -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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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
flask
|
||||
requests
|
||||
@@ -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 => `
|
||||
<div class="agent-card" id="card-${a.id}">
|
||||
<div class="agent-card-header">
|
||||
<span class="agent-card-emoji">${AGENT_EMOJIS[a.id] || '🤖'}</span>
|
||||
<div>
|
||||
<div class="agent-card-name">${a.name}</div>
|
||||
<div class="agent-card-role">${a.role || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-card-intent">${a.intent || '—'}</div>
|
||||
<div class="agent-card-stats">
|
||||
<span class="stat-badge runs">⏱ ${a.cycle_count || 0} ciclos</span>
|
||||
<span class="stat-badge ok">✓ ${a.success_count || 0}</span>
|
||||
<span class="stat-badge fail">✗ ${a.fail_count || 0}</span>
|
||||
</div>
|
||||
<div class="agent-card-model">${a.model || '—'} @ ${a.provider || '?'}</div>
|
||||
<div class="agent-card-actions">
|
||||
<button onclick="editAgent('${a.id}')">✏️ Editar</button>
|
||||
<button onclick="deleteAgent('${a.id}')" style="color:#ff6b6b">🗑️ Remover</button>
|
||||
</div>
|
||||
</div>
|
||||
`).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 `
|
||||
<div class="metric-card ${m.id}">
|
||||
<div class="metric-name">${AGENT_EMOJIS[m.id] || '🤖'} ${m.name}</div>
|
||||
<div class="metric-value">${rate}%</div>
|
||||
<div class="metric-sub">${m.cycle_count || 0} ciclos · avg ${avg}</div>
|
||||
<div class="metric-bar"><div class="metric-bar-fill" style="width:${rate}%;background:${color}"></div></div>
|
||||
</div>`;
|
||||
}).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 = '<div class="service-item" style="color:var(--text-secondary)">Nenhum serviço registado. Clique em + Novo Serviço.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = services.map(s => `
|
||||
<div class="service-item" id="svc-${s.id}">
|
||||
<span class="service-status-dot ${s.status || 'unknown'}"></span>
|
||||
<span class="service-name">${s.name}</span>
|
||||
<span class="service-type">${s.type || ''}</span>
|
||||
<span class="service-endpoint">${s.endpoint ? s.endpoint.slice(0,40) : ''}</span>
|
||||
<div class="service-actions">
|
||||
<button onclick="pingService('${s.id}')">🏓</button>
|
||||
<button onclick="deleteService('${s.id}')" style="color:#ff6b6b">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`).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) => `
|
||||
<div class="visual-node" id="vis-${n.id}">
|
||||
<div style="font-size:1.4rem">${AGENT_EMOJIS[n.id] || '🤖'}</div>
|
||||
<div class="visual-node-label">${n.label}</div>
|
||||
<div class="visual-node-status">${n.status || 'idle'}</div>
|
||||
${n.total_runs ? `<div style="font-size:0.6rem;color:var(--text-secondary)">${n.total_runs}x</div>` : ''}
|
||||
</div>
|
||||
${i < data.nodes.length - 1 ? '<span class="visual-arrow">→</span>' : ''}
|
||||
`).join('');
|
||||
}
|
||||
// Timeline
|
||||
const tl = document.getElementById('visualTimeline');
|
||||
if (tl) {
|
||||
tl.innerHTML = data.recent_runs.map(r => `
|
||||
<div class="timeline-item">
|
||||
<span class="timeline-ts">${r.started_at ? r.started_at.slice(11,19) : '—'}</span>
|
||||
<span class="timeline-decision ${r.decio_decision}">${r.decio_decision || '?'}</span>
|
||||
<span style="font-size:0.65rem;color:var(--text-secondary)">${r.status}</span>
|
||||
</div>
|
||||
`).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 => `
|
||||
<div class="history-item" style="padding:8px;border-bottom:1px solid rgba(255,255,255,0.05)">
|
||||
<div style="display:flex;justify-content:space-between">
|
||||
<span style="font-weight:700">${r.decio_decision || '?'}</span>
|
||||
<span style="color:var(--text-secondary);font-size:0.75rem">${r.started_at ? r.started_at.slice(0,16).replace('T',' ') : '—'}</span>
|
||||
</div>
|
||||
<div style="font-size:0.75rem;color:var(--text-secondary);margin-top:4px">${(r.brief_result || '').slice(0,80)}</div>
|
||||
</div>
|
||||
`).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 = `<span class="log-time">${ts}</span><span class="log-agent">${agent}</span><span class="log-message">${message}</span>`;
|
||||
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 = '<div style="color:var(--text-secondary);font-size:0.8rem;padding:8px">Nenhum trade ainda. Execute o pipeline para começar.</div>';
|
||||
} 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 `<div style="display:flex;gap:10px;padding:6px 8px;background:var(--bg-card-hover);border-radius:6px;margin-bottom:4px;font-size:0.78rem">
|
||||
<span style="color:var(--text-secondary)">${t.date ? t.date.slice(5) : '—'}</span>
|
||||
<span style="font-weight:700;color:${cls}">${t.action}</span>
|
||||
<span>${t.btc_amount ? t.btc_amount.toFixed(6)+' BTC' : '—'}</span>
|
||||
<span>@ $${Number(t.entry_price).toLocaleString('pt-BR',{minimumFractionDigits:0})}</span>
|
||||
<span style="color:${t.result==='WIN'?'#69f0ae':t.result==='LOSS'?'#ff6b6b':'#888'}">${t.result || ''}</span>
|
||||
</div>`;
|
||||
}).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 = `
|
||||
<td style="color:${AGENT_COLORS[r.agent.toLowerCase()] || '#aab'}">${r.agent}</td>
|
||||
<td>${r.prompt ? r.prompt.toLocaleString() : '-'}</td>
|
||||
<td>${r.completion ? r.completion.toLocaleString() : '-'}</td>
|
||||
<td><b>${r.total ? r.total.toLocaleString() : '-'}</b></td>
|
||||
<td>${r.calls || 0}</td>
|
||||
<td>$${r.cost ? r.cost.toFixed(4) : '0.0000'}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
@@ -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); }
|
||||
@@ -0,0 +1,213 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BrainSteel Fin — Agent Office</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;600;700&family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">🏢</span>
|
||||
<span class="logo-text">BrainSteel <strong>Fin</strong></span>
|
||||
</div>
|
||||
<div class="header-status">
|
||||
<span class="status-dot" id="statusDot"></span>
|
||||
<span id="statusText">Sistema online</span>
|
||||
</div>
|
||||
<div class="header-time" id="clock"></div>
|
||||
</header>
|
||||
|
||||
<!-- Pipeline Flow -->
|
||||
<section class="pipeline-section">
|
||||
<h2>📊 Pipeline de Trading</h2>
|
||||
<div class="pipeline-flow" id="pipelineFlow">
|
||||
<!-- Nodes injected by JS -->
|
||||
</div>
|
||||
<div class="pipeline-controls">
|
||||
<button class="btn btn-primary" id="btnRun" onclick="runPipeline()">
|
||||
▶ Executar Pipeline
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="toggleAuto()">
|
||||
⏱ Auto: <span id="autoStatus">OFF</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="showHistory()">📊 Histórico</button>
|
||||
</div>
|
||||
<div id="lastDecisionBanner" class="decision-banner" style="display:none;"></div>
|
||||
</section>
|
||||
|
||||
<!-- Office Floor -->
|
||||
<section class="office-section">
|
||||
<h2>🏠 Escritório Virtual</h2>
|
||||
<div class="office-floor">
|
||||
<div class="floor-grid" id="agentDesks">
|
||||
<!-- Agent desks injected by JS -->
|
||||
</div>
|
||||
<div class="chat-bubble" id="chatBubble">
|
||||
<span id="chatText">Olá! Eu sou o escritório virtual da BrainSteel Fin.</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Item 1: Agent Registry -->
|
||||
<section class="registry-section">
|
||||
<h2>👥 Cadastro de Agentes</h2>
|
||||
<div class="registry-controls">
|
||||
<button class="btn btn-small" onclick="showAddAgent()">+ Novo Agente</button>
|
||||
<button class="btn btn-small btn-secondary" onclick="loadAgentRegistry()">↻ Atualizar</button>
|
||||
</div>
|
||||
<div class="agent-cards" id="agentCards">
|
||||
<!-- Agent cards injected by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Item 2 & 3: Metrics + Services -->
|
||||
<div class="two-col-section">
|
||||
<section class="metrics-section">
|
||||
<h2>📈 Métricas de Agentes</h2>
|
||||
<div class="metrics-grid" id="metricsGrid">
|
||||
<!-- Metrics injected by JS -->
|
||||
</div>
|
||||
</section>
|
||||
<section class="services-section">
|
||||
<h2>🔌 Serviços</h2>
|
||||
<div class="services-controls">
|
||||
<button class="btn btn-small" onclick="showAddService()">+ Novo Serviço</button>
|
||||
<button class="btn btn-small btn-secondary" onclick="pingAllServices()">🏓 Ping Todos</button>
|
||||
</div>
|
||||
<div class="services-list" id="servicesList">
|
||||
<!-- Services injected by JS -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Item 4: Pipeline Visual -->
|
||||
<section class="visual-section">
|
||||
<h2>🔭 Pipeline Visual em Tempo Real</h2>
|
||||
<div class="visual-container">
|
||||
<div class="visual-flow" id="visualFlow">
|
||||
<!-- Visual pipeline injected by JS -->
|
||||
</div>
|
||||
<div class="visual-timeline" id="visualTimeline">
|
||||
<!-- Timeline injected by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Portfolio Simulator -->
|
||||
<section class="portfolio-section">
|
||||
<div class="portfolio-header">
|
||||
<h2>💹 Portfolio Simulado</h2>
|
||||
<div class="portfolio-summary" id="portfolioSummary">
|
||||
<span class="portfolio-balance" id="pfBalance">$10.000,00</span>
|
||||
<span class="portfolio-pnl" id="pfPnl">P&L: $0,00 (0%)</span>
|
||||
<span class="portfolio-trades" id="pfTrades">0 trades | 0% Win Rate</span>
|
||||
</div>
|
||||
<div class="portfolio-actions">
|
||||
<button class="btn btn-small" onclick="resetPortfolio()">↺ Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="portfolio-chart" id="portfolioChart">
|
||||
<canvas id="chartCanvas" width="800" height="200"></canvas>
|
||||
</div>
|
||||
<div class="portfolio-trades-list" id="portfolioTrades">
|
||||
<!-- Recent trades injected by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Token Tracker -->
|
||||
<section class="tokens-section">
|
||||
<h2>🧮 Tokens por Agente</h2>
|
||||
<div class="tokens-controls">
|
||||
<label>Dias: <select id="tokenDays" onchange="loadTokenChart()">
|
||||
<option value="3">3 dias</option>
|
||||
<option value="7" selected>7 dias</option>
|
||||
<option value="14">14 dias</option>
|
||||
<option value="30">30 dias</option>
|
||||
</select></label>
|
||||
<span id="tokenTotals"></span>
|
||||
</div>
|
||||
<div class="tokens-chart-wrap">
|
||||
<canvas id="tokenChart" width="800" height="200"></canvas>
|
||||
</div>
|
||||
<div class="tokens-table-wrap">
|
||||
<table class="tokens-table" id="tokenTotalsTable">
|
||||
<thead><tr><th>Agente</th><th>Prompt</th><th>Completion</th><th>Total</th><th>Chamadas</th><th>Custo</th></tr></thead>
|
||||
<tbody id="tokenTotalsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Activity Log -->
|
||||
<section class="log-section">
|
||||
<h2>📝 Log de Atividades</h2>
|
||||
<div class="log-container" id="logContainer">
|
||||
<div class="log-entry system">
|
||||
<span class="log-time">--:--</span>
|
||||
<span class="log-agent">SYS</span>
|
||||
<span class="log-message">Aguardando primeira execução...</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modals -->
|
||||
<div class="modal" id="modalHistory" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>📊 Histórico de Execuções</h3>
|
||||
<button class="modal-close" onclick="closeHistory()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="historyContent">
|
||||
<div class="history-item"><span class="history-time">Carregando...</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modalAgent" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalAgentTitle">Novo Agente</h3>
|
||||
<button class="modal-close" onclick="closeAgent()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalAgentBody">
|
||||
<label>ID (slug)</label><input type="text" id="agentId" placeholder="ex: brief">
|
||||
<label>Nome</label><input type="text" id="agentName" placeholder="ex: Brief">
|
||||
<label>Role</label><input type="text" id="agentRole" placeholder="ex: Market Intelligence">
|
||||
<label>Intenção (o que faz)</label><textarea id="agentIntent" rows="3" placeholder="Descrição da intenção do agente..."></textarea>
|
||||
<label>Modelo</label><input type="text" id="agentModel" placeholder="ex: google/gemma-4-31b-it:free">
|
||||
<label>Provider</label><input type="text" id="agentProvider" placeholder="ex: openrouter">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" onclick="saveAgent()">Salvar</button>
|
||||
<button class="btn btn-secondary" onclick="closeAgent()">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modalService" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Novo Serviço</h3>
|
||||
<button class="modal-close" onclick="closeService()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>Nome</label><input type="text" id="serviceName" placeholder="ex: Binance API">
|
||||
<label>Tipo</label><input type="text" id="serviceType" placeholder="ex: exchange, news, data">
|
||||
<label>Endpoint</label><input type="text" id="serviceEndpoint" placeholder="ex: https://api.binance.com">
|
||||
<label>Status</label><input type="text" id="serviceStatus" placeholder="ex: active">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" onclick="saveService()">Salvar</button>
|
||||
<button class="btn btn-secondary" onclick="closeService()">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user