Files

198 lines
8.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
BrainSteel Fin — Decio Agent (v2)
Chief Strategist & Risk Officer
5-Pillar decision engine: Confiança < 70% → 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 < 70% → always HOLD ─────────────────────────
def _risk_filter(self, confidence, signal):
"""Décio is a mathematician: no confidence ≥70%, no operation."""
return confidence < 70 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 < 70% → decisão SEMPRE HOLD (nunca opera)
Confiança ≥ 70% + sinal=ALTA → BUY
Confiança ≥ 70% + 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 < 70% — 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}% < 70% — 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
}
# ── Dynamic Allocation (Kelly Scale) ─────────────────────────────────────
def _calc_allocation(self, confidence, decision):
"""Calcula a alocação de banca dinâmica baseada na confiança do sinal."""
if decision not in ("BUY", "SELL") or confidence < 70:
return 0
if confidence >= 90:
return 35 # Mão forte (alta convicção)
elif confidence >= 80:
return 25 # Mão padrão
else:
return 15 # Mão leve (cautela, 70-79%)
# ── Main run ───────────────────────────────────────────────────────────
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)
conf = result.get("confidence", 50)
action = result["decision"]
amount_pct = self._calc_allocation(conf, action)
output = {
"action": action,
"amount_pct": amount_pct,
"stop_loss": result.get("stop_loss", 0),
"take_profit": result.get("take_profit", 0),
"confidence": conf,
"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