🚀 Auto-deploy: Add background automated scheduler, Kelly allocation, multi-portfolio simulations, and UI improvements

This commit is contained in:
2026-05-20 11:23:56 +00:00
parent 738490ea4f
commit 3921ce9359
9 changed files with 944 additions and 127 deletions
+83 -46
View File
@@ -13,17 +13,22 @@ DATA_DIR = Path("/app/data")
PORTFOLIO_DB = DATA_DIR / "portfolio.db"
class Portfolio:
"""Portfolio simulation — tracking de saldo, trades e P&L."""
"""Portfolio simulation — tracking de saldo, trades e P&L (Multi-Estratégia)."""
def __init__(self):
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("""
CREATE TABLE IF NOT EXISTS portfolio (
c.execute(f"""
CREATE TABLE IF NOT EXISTS {self.tbl_portfolio} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT,
balance REAL,
@@ -36,8 +41,8 @@ class Portfolio:
UNIQUE(date)
)
""")
c.execute("""
CREATE TABLE IF NOT EXISTS trades (
c.execute(f"""
CREATE TABLE IF NOT EXISTS {self.tbl_trades} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT,
action TEXT,
@@ -55,8 +60,8 @@ class Portfolio:
created_at TEXT
)
""")
c.execute("""
CREATE TABLE IF NOT EXISTS daily_snapshot (
c.execute(f"""
CREATE TABLE IF NOT EXISTS {self.tbl_snapshot} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT UNIQUE,
balance REAL,
@@ -72,13 +77,13 @@ class Portfolio:
""")
# Ensure today's row exists
today = date.today().isoformat()
row = c.execute("SELECT balance FROM portfolio WHERE date=?", (today,)).fetchone()
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("SELECT balance, btc_held FROM portfolio ORDER BY date DESC LIMIT 1").fetchone()
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("INSERT INTO portfolio (date,balance,btc_held,created_at) VALUES (?,?,?,?)",
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()
@@ -88,7 +93,7 @@ class Portfolio:
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()
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}
@@ -127,7 +132,7 @@ class Portfolio:
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}"
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
@@ -150,19 +155,19 @@ class Portfolio:
elif take_profit and price >= take_profit:
exit_reason = "take_profit_hit"
else:
exit_reason = "decio_signal"
exit_reason = f"signal_{self.strategy}"
result_str = f"🔴 SELL | {btc_to_sell:.6f} BTC @ ${price:,.0f} | Net: ${net_proceeds:.2f} | Fee: ${fee:.2f}"
result_str = f"🔴 SELL ({self.strategy}) | {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}"
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" 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)
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,
@@ -172,12 +177,12 @@ class Portfolio:
now))
# Update daily stats
c.execute("UPDATE portfolio SET balance=?, btc_held=?, total_trades=total_trades+1 WHERE date=?",
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("UPDATE portfolio SET wins=wins+1 WHERE date=?", (today,))
c.execute(f"UPDATE {self.tbl_portfolio} SET wins=wins+1 WHERE date=?", (today,))
elif is_win is False:
c.execute("UPDATE portfolio SET losses=losses+1 WHERE date=?", (today,))
c.execute(f"UPDATE {self.tbl_portfolio} SET losses=losses+1 WHERE date=?", (today,))
conn.commit()
conn.close()
@@ -196,7 +201,7 @@ class Portfolio:
c = conn.cursor()
# All trades
trades = c.execute("SELECT action,pnl_usdt,pnl_pct,result,date FROM trades ORDER BY created_at DESC").fetchall()
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")
@@ -204,7 +209,7 @@ class Portfolio:
# Current balance
today = date.today().isoformat()
cur = c.execute("SELECT balance, btc_held FROM portfolio WHERE date=?", (today,)).fetchone()
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
@@ -217,7 +222,7 @@ class Portfolio:
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()
days = c.execute(f"SELECT date,balance FROM {self.tbl_portfolio} ORDER BY date ASC").fetchall()
conn.close()
@@ -230,6 +235,8 @@ class Portfolio:
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
@@ -261,10 +268,10 @@ class Portfolio:
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()
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("""
INSERT OR REPLACE INTO daily_snapshot
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 (?,?,?,?,?,?,?,?,?,?)
""", (
@@ -284,12 +291,12 @@ class Portfolio:
class PapertAgent:
"""Agent executor que também atualiza portfolio simulado."""
"""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()
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"
@@ -303,10 +310,10 @@ class PapertAgent:
break
return True, action
def _format_log_entry(self, action, entry_price, stop_loss, take_profit):
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 | {action} | Entry: ${entry_price:,.2f} | Fee: ${fee:.2f} | SL: ${stop_loss:,.2f} | TP: ${take_profit:,.2f}"
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):
@@ -335,23 +342,39 @@ class PapertAgent:
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
# 1. Execute Carteira A (Soberana - Padrão)
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)
log_entry = self._format_log_entry(action, entry_price, stop_loss, take_profit, "Soberana")
logged = self._write_log(log_entry)
# Save daily snapshot
try:
self.portfolio.snapshot()
except:
pass
# Build result
try: self.portfolio.snapshot()
except: pass
stats = self.portfolio.get_stats()
fee = round(entry_price * 0.001, 2)
# 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}%)"
@@ -367,7 +390,7 @@ class PapertAgent:
"fee_usdt": fee,
"log_appended": logged,
"result": result_str,
# Portfolio info for dashboard
# Portfolio info for dashboard (Enriched with Multi-Strategy)
"portfolio": {
"balance": stats["current_balance"],
"total_value": stats["total_value"],
@@ -378,7 +401,21 @@ class PapertAgent:
"losses": stats["losses"],
"win_rate": stats["win_rate"],
"btc_held": stats["btc_held"],
"btc_price": stats["btc_price"]
"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()