738490ea4f
- RSI threshold: 35/65 → 38/62 - Funding: 0.001 → 0.0001 (any positive = bearish signal) - Long ratio: 48-52 → 47-53 (tighter) - MACD: 10 → 5 (more sensitive) - BTC dominance: 57.5 → 55 - 4h change: 1.5% → 1% - Decio risk filter: 75% → 70% to allow 74% signals through - Fix UnboundLocalError on change_4h (defined earlier in run())
182 lines
8.1 KiB
Python
182 lines
8.1 KiB
Python
"""
|
||
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 ≥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 < 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 |