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:
Hermes - BrainSteel VPS
2026-05-17 16:25:55 +00:00
commit 50960bab21
24 changed files with 3285 additions and 0 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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=&currencies=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
View File
@@ -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
+385
View File
@@ -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()
}
+78
View File
@@ -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,
}