feat: paper trading live su Deribit testnet — squeeze+ML ibrida

Sistema completo: client Cerbero MCP, signal engine (squeeze + GBM),
paper trader con gestione posizioni, stop loss, log JSONL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 09:36:47 +02:00
parent 1617330d10
commit 6e9862c183
5 changed files with 654 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
"""Paper trader: loop principale che monitora, segnala e opera su Deribit testnet."""
from __future__ import annotations
import json
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pandas as pd
from src.live.cerbero_client import CerberoClient
from src.live.signal_engine import SignalEngine
LOG_DIR = Path(__file__).resolve().parents[2] / "data" / "paper_trades"
INSTRUMENT = "ETH-PERPETUAL"
RESOLUTION = "15"
LEVERAGE = 3
POSITION_PCT = 0.15
HOLD_BARS = 3
POLL_SECONDS = 60
LOOKBACK_DAYS = 60
TRAIN_LOOKBACK_DAYS = 365
class PaperTrader:
def __init__(self):
self.client = CerberoClient()
self.engine = SignalEngine(bb_w=14, sq_thr=0.8, ml_thr=0.70)
self.in_position = False
self.position_entry_time: datetime | None = None
self.position_direction: str | None = None
self.position_entry_price: float = 0
self.bars_held = 0
self.last_bar_ts: int = 0
LOG_DIR.mkdir(parents=True, exist_ok=True)
self.log_path = LOG_DIR / f"trades_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jsonl"
self.status_path = LOG_DIR / "status.json"
def log(self, event: str, data: dict | None = None):
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event": event,
**(data or {}),
}
with open(self.log_path, "a") as f:
f.write(json.dumps(entry) + "\n")
print(f" [{entry['timestamp'][:19]}] {event}: {json.dumps(data or {})}")
def save_status(self):
status = {
"in_position": self.in_position,
"direction": self.position_direction,
"entry_price": self.position_entry_price,
"entry_time": self.position_entry_time.isoformat() if self.position_entry_time else None,
"bars_held": self.bars_held,
"last_update": datetime.now(timezone.utc).isoformat(),
}
with open(self.status_path, "w") as f:
json.dump(status, f, indent=2)
def fetch_candles(self, days: int = LOOKBACK_DAYS) -> pd.DataFrame:
end = datetime.now(timezone.utc)
start = end - timedelta(days=days)
candles = self.client.get_historical(
INSTRUMENT,
start.strftime("%Y-%m-%d"),
end.strftime("%Y-%m-%d"),
RESOLUTION,
)
if not candles:
return pd.DataFrame()
df = pd.DataFrame(candles)
df["timestamp"] = df["timestamp"].astype("int64")
df = df.sort_values("timestamp").reset_index(drop=True)
return df
def train_model(self):
self.log("TRAINING", {"lookback_days": TRAIN_LOOKBACK_DAYS})
df = self.fetch_candles(TRAIN_LOOKBACK_DAYS)
if df.empty:
self.log("TRAINING_FAILED", {"reason": "no data"})
return False
result = self.engine.train(df, lookahead=HOLD_BARS)
self.log("TRAINING_DONE", result)
return "error" not in result
def open_position(self, direction: str, signal: dict):
ticker = self.client.get_ticker(INSTRUMENT)
price = ticker["last_price"]
account = self.client.get_account_summary()
equity = account["equity"]
notional = equity * POSITION_PCT
amount = round(notional / price, 1)
amount = max(amount, 1.0)
side = "buy" if direction == "buy" else "sell"
self.log("OPENING", {
"side": side,
"amount": amount,
"price": price,
"equity": equity,
"signal": signal,
})
try:
result = self.client.place_order(
instrument=INSTRUMENT,
side=side,
amount=amount,
order_type="market",
leverage=LEVERAGE,
label="pythagoras-squeeze",
)
self.in_position = True
self.position_direction = side
self.position_entry_price = price
self.position_entry_time = datetime.now(timezone.utc)
self.bars_held = 0
self.log("OPENED", {"order_result": result})
except Exception as e:
self.log("OPEN_FAILED", {"error": str(e)})
def close_current_position(self, reason: str):
if not self.in_position:
return
ticker = self.client.get_ticker(INSTRUMENT)
exit_price = ticker["last_price"]
if self.position_direction == "buy":
pnl_pct = (exit_price - self.position_entry_price) / self.position_entry_price * 100
else:
pnl_pct = (self.position_entry_price - exit_price) / self.position_entry_price * 100
self.log("CLOSING", {
"reason": reason,
"entry_price": self.position_entry_price,
"exit_price": exit_price,
"pnl_pct": round(pnl_pct, 3),
"bars_held": self.bars_held,
})
try:
result = self.client.close_position(INSTRUMENT)
self.log("CLOSED", {"result": result, "pnl_pct": round(pnl_pct, 3)})
except Exception as e:
self.log("CLOSE_FAILED", {"error": str(e)})
self.in_position = False
self.position_direction = None
self.position_entry_price = 0
self.position_entry_time = None
self.bars_held = 0
def check_position_exit(self, df: pd.DataFrame):
if not self.in_position:
return
current_ts = df["timestamp"].iloc[-1]
if current_ts > self.last_bar_ts:
self.bars_held += 1
self.last_bar_ts = current_ts
if self.bars_held >= HOLD_BARS:
self.close_current_position("hold_limit")
return
price = df["close"].iloc[-1]
if self.position_direction == "buy":
pnl_pct = (price - self.position_entry_price) / self.position_entry_price
else:
pnl_pct = (self.position_entry_price - price) / self.position_entry_price
if pnl_pct <= -0.02:
self.close_current_position("stop_loss_2pct")
def run_once(self) -> str:
"""Esegui un singolo ciclo. Ritorna lo stato."""
df = self.fetch_candles(LOOKBACK_DAYS)
if df.empty:
return "no_data"
if self.in_position:
self.check_position_exit(df)
self.save_status()
if self.in_position:
return f"in_position_{self.position_direction}_bar{self.bars_held}"
return "position_closed"
signal = self.engine.check_signal(df)
if signal:
self.log("SIGNAL", signal)
self.open_position(signal["direction"], signal)
self.save_status()
return f"signal_{signal['direction']}"
self.save_status()
return "watching"
def run(self, retrain_hours: int = 24):
"""Loop principale."""
print("=" * 60)
print(f" PAPER TRADER — {INSTRUMENT} {RESOLUTION}m")
print(f" Leva: {LEVERAGE}x, Position: {POSITION_PCT*100:.0f}%, Hold: {HOLD_BARS} barre")
print(f" Poll: ogni {POLL_SECONDS}s")
print(f" Log: {self.log_path}")
print("=" * 60)
account = self.client.get_account_summary()
self.log("STARTUP", {
"equity": account["equity"],
"testnet": account.get("testnet", True),
})
if not self.train_model():
print("Training fallito. Uscita.")
return
last_train = datetime.now(timezone.utc)
while True:
try:
now = datetime.now(timezone.utc)
if (now - last_train).total_seconds() > retrain_hours * 3600:
self.train_model()
last_train = now
status = self.run_once()
if status != "watching":
print(f"{status}")
except KeyboardInterrupt:
self.log("SHUTDOWN", {"reason": "keyboard"})
if self.in_position:
self.close_current_position("shutdown")
break
except Exception as e:
self.log("ERROR", {"error": str(e)})
print(f" ERRORE: {e}")
time.sleep(POLL_SECONDS)
if __name__ == "__main__":
trader = PaperTrader()
trader.run()