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:
@@ -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()
|
||||
Reference in New Issue
Block a user