436 lines
18 KiB
Python
436 lines
18 KiB
Python
"""
|
|
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 (Multi-Estratégia)."""
|
|
|
|
def __init__(self, strategy="soberana"):
|
|
self.strategy = strategy
|
|
self.suf = "" if strategy == "soberana" else f"_{strategy}"
|
|
self.tbl_portfolio = f"portfolio{self.suf}"
|
|
self.tbl_trades = f"trades{self.suf}"
|
|
self.tbl_snapshot = f"daily_snapshot{self.suf}"
|
|
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(f"""
|
|
CREATE TABLE IF NOT EXISTS {self.tbl_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(f"""
|
|
CREATE TABLE IF NOT EXISTS {self.tbl_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(f"""
|
|
CREATE TABLE IF NOT EXISTS {self.tbl_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(f"SELECT balance FROM {self.tbl_portfolio} WHERE date=?", (today,)).fetchone()
|
|
if not row:
|
|
# Seed from last known balance or initial
|
|
last = c.execute(f"SELECT balance, btc_held FROM {self.tbl_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(f"INSERT INTO {self.tbl_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(f"SELECT balance, btc_held FROM {self.tbl_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 ({self.strategy}) | ${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
|
|
|
|
# Calculate average entry price from OPEN BUYs
|
|
c.execute(f"SELECT entry_price, btc_amount FROM {self.tbl_trades} WHERE action='BUY' AND result IN ('PENDING', 'OPEN')")
|
|
buy_trades = c.fetchall()
|
|
total_cost = sum(t[0] * t[1] for t in buy_trades)
|
|
total_btc = sum(t[1] for t in buy_trades)
|
|
avg_entry = total_cost / total_btc if total_btc > 0 else price
|
|
|
|
pnl = net_proceeds - (btc_to_sell * avg_entry)
|
|
pnl_pct = (pnl / (btc_to_sell * avg_entry)) * 100 if (btc_to_sell * avg_entry) > 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 = f"signal_{self.strategy}"
|
|
|
|
result_str = f"🔴 SELL ({self.strategy}) | {btc_to_sell:.6f} BTC @ ${price:,.0f} | Net: ${net_proceeds:.2f} | PNL: ${pnl:.2f} | Fee: ${fee:.2f}"
|
|
else:
|
|
new_balance = balance
|
|
new_btc_held = btc_held
|
|
result_str = f"⚪ SELL ignorado ({self.strategy}) | 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" else None
|
|
trade_result = "WIN" if is_win is True else ("LOSS" if is_win is False else "OPEN")
|
|
|
|
c.execute(f"""
|
|
INSERT INTO {self.tbl_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,
|
|
exit_reason, trade_result,
|
|
now))
|
|
|
|
if action == "SELL":
|
|
# Mark previous OPEN buys as CLOSED
|
|
c.execute(f"UPDATE {self.tbl_trades} SET result='CLOSED' WHERE action='BUY' AND result IN ('PENDING', 'OPEN')")
|
|
|
|
# Update daily stats
|
|
c.execute(f"UPDATE {self.tbl_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(f"UPDATE {self.tbl_portfolio} SET wins=wins+1 WHERE date=?", (today,))
|
|
elif is_win is False:
|
|
c.execute(f"UPDATE {self.tbl_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(f"SELECT action,pnl_usdt,pnl_pct,result,date FROM {self.tbl_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(f"SELECT balance, btc_held FROM {self.tbl_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(f"SELECT date,balance FROM {self.tbl_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 is None:
|
|
continue
|
|
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(f"SELECT COUNT(*),SUM(pnl_usdt) FROM {self.tbl_trades} WHERE date=? AND result IN ('WIN','LOSS')", (today,)).fetchone()
|
|
|
|
c.execute(f"""
|
|
INSERT OR REPLACE INTO {self.tbl_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 (Multi-Estratégia)."""
|
|
|
|
def __init__(self, decio_data):
|
|
self.name = "PaperT"
|
|
self.decio_data = decio_data
|
|
self.portfolio = Portfolio(strategy="soberana")
|
|
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, strategy="Soberana"):
|
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
fee = round(entry_price * 0.001, 2)
|
|
entry = f"[{now}] BTC/USDT ({strategy}) | {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
|
|
|
|
# 1. Execute Carteira A (Soberana - Padrão)
|
|
portfolio_result = self.portfolio._execute_trade(action, entry_price, amount_pct, stop_loss, take_profit)
|
|
log_entry = self._format_log_entry(action, entry_price, stop_loss, take_profit, "Soberana")
|
|
logged = self._write_log(log_entry)
|
|
try: self.portfolio.snapshot()
|
|
except: pass
|
|
stats = self.portfolio.get_stats()
|
|
|
|
# 2. Execute Carteira B (Agressiva - ignora trava de 70% e opera sinal primário do Brief)
|
|
brief_signal = self.decio_data.get("signal", "NEUTRO")
|
|
agr_action = "BUY" if brief_signal == "ALTA" else "SELL" if brief_signal == "BAIXA" else "HOLD"
|
|
pf_agr = Portfolio(strategy="agressiva")
|
|
sl_agr = round(entry_price * 0.98, 2) if agr_action == "BUY" else round(entry_price * 1.02, 2) if agr_action == "SELL" else 0
|
|
tp_agr = round(entry_price * 1.05, 2) if agr_action == "BUY" else round(entry_price * 0.95, 2) if agr_action == "SELL" else 0
|
|
pf_agr._execute_trade(agr_action, entry_price, 25, sl_agr, tp_agr)
|
|
log_agr = self._format_log_entry(agr_action, entry_price, sl_agr, tp_agr, "Agressiva")
|
|
self._write_log(log_agr)
|
|
try: pf_agr.snapshot()
|
|
except: pass
|
|
stats_agr = pf_agr.get_stats()
|
|
|
|
# 3. Execute Carteira C (HODL Benchmark)
|
|
pf_hodl = Portfolio(strategy="hodl")
|
|
cur_hodl = pf_hodl.balance
|
|
if cur_hodl["btc_held"] == 0 and cur_hodl["balance"] > 10:
|
|
pf_hodl._execute_trade("BUY", entry_price, 100, 0, 0)
|
|
log_hodl = self._format_log_entry("BUY" if cur_hodl["btc_held"] == 0 else "HOLD", entry_price, 0, 0, "HODL")
|
|
self._write_log(log_hodl)
|
|
try: pf_hodl.snapshot()
|
|
except: pass
|
|
stats_hodl = pf_hodl.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 (Enriched with Multi-Strategy)
|
|
"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"],
|
|
# Sub-carteiras comparativas
|
|
"agressiva": {
|
|
"balance": stats_agr["current_balance"],
|
|
"total_value": stats_agr["total_value"],
|
|
"total_pnl": stats_agr["total_pnl"],
|
|
"total_pnl_pct": stats_agr["total_pnl_pct"],
|
|
"win_rate": stats_agr["win_rate"]
|
|
},
|
|
"hodl": {
|
|
"balance": stats_hodl["current_balance"],
|
|
"total_value": stats_hodl["total_value"],
|
|
"total_pnl": stats_hodl["total_pnl"],
|
|
"total_pnl_pct": stats_hodl["total_pnl_pct"]
|
|
}
|
|
},
|
|
"agent": "PaperT",
|
|
"timestamp": datetime.now().isoformat()
|
|
} |