chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"""CrossSectionalWorker — paper/live worker per XS01 (reversione cross-sectional, 8 asset).
|
||||
|
||||
Mirror ESATTO di scripts.strategies.XS01_cross_sectional.xsec_sim: ogni HOLD barre
|
||||
classifica gli asset per rendimento su LB barre, pesi w = -(ret - media)/gross (market-
|
||||
neutral gross 1), entra al close, esce dopo HOLD barre, riallinea (1 barra di stacco fra
|
||||
uscita e nuovo ingresso, come l'engine). PnL su book log-return netto fee 0.10% RT.
|
||||
Stato persistente (resume). Solo SIM (esecuzione reale a 8 gambe non implementata).
|
||||
|
||||
PHASE-TRANCHING (2026-06-11, gate xs01_tranche_gate.py): param `tranches`=K divide il
|
||||
book in K sub-book sfasati di hold/K barre, capitale comune (PnL/K per tranche). La fase
|
||||
del roll non-sovrapposto e' arbitraria e da sola muove Sharpe FULL daily 1.52-2.33 e DD
|
||||
13.8-33.1% (timing-luck): l'ensemble di fase la elimina SENZA parametri fittati (plateau
|
||||
K=2 e K=3 entrambi promossi; PORT06 OOS Sh 10.07->10.15, DD 1.48->1.38). Solo path live,
|
||||
come disp_min: il backtest canonico resta single-phase. K=1 = comportamento storico.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.live.telegram_notifier import notify_event
|
||||
|
||||
|
||||
class CrossSectionalWorker:
|
||||
def __init__(self, universe, tf="1h", params=None, capital=1000.0,
|
||||
position_size=0.15, leverage=3.0, fee_rt=0.0005,
|
||||
name="XS01", data_dir=Path("data/portfolio_paper")):
|
||||
self.universe = list(universe)
|
||||
p = params or {}
|
||||
self.lb = int(p.get("lb", 48))
|
||||
self.hold = int(p.get("hold", 12))
|
||||
# dispersion-gate (2026-06-10): entra solo se la std cross-section del
|
||||
# momentum lb supera disp_min — senza dispersione da far rientrare i
|
||||
# trade sono fee. None = off (parita' col backtest canonico non filtrato).
|
||||
self.disp_min = p.get("disp_min")
|
||||
self.tf = tf
|
||||
self.initial_capital = capital
|
||||
self.position_size = position_size
|
||||
self.leverage = leverage
|
||||
self.fee_rt = fee_rt
|
||||
self.worker_id = f"{name}__{tf}"
|
||||
self.work_dir = Path(data_dir) / self.worker_id
|
||||
self.work_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.status_path = self.work_dir / "status.json"
|
||||
self.trades_path = self.work_dir / "trades.jsonl"
|
||||
|
||||
self.k = max(1, int(p.get("tranches", 1)))
|
||||
self._step = max(1, round(self.hold / self.k)) # sfasamento iniziale fra tranche
|
||||
self.capital = capital
|
||||
self.books = [self._flat_book(j * self._step) for j in range(self.k)]
|
||||
self.total_trades = 0
|
||||
self.total_wins = 0
|
||||
self.last_bar_ts = 0
|
||||
self._load()
|
||||
|
||||
def _flat_book(self, wait: int = 0):
|
||||
return {"weights": {a: 0.0 for a in self.universe},
|
||||
"entry_px": {a: 0.0 for a in self.universe},
|
||||
"bars_held": 0, "in_position": False, "wait": int(wait)}
|
||||
|
||||
@property
|
||||
def in_position(self) -> bool:
|
||||
return any(b["in_position"] for b in self.books)
|
||||
|
||||
# ---------- persistenza ----------
|
||||
def _load(self):
|
||||
if not self.status_path.exists():
|
||||
self._log("INIT", {"capital": self.capital, "universe": self.universe,
|
||||
"lb": self.lb, "hold": self.hold, "tranches": self.k})
|
||||
return
|
||||
s = json.loads(self.status_path.read_text())
|
||||
self.capital = s.get("capital", self.initial_capital)
|
||||
self.total_trades = s.get("total_trades", 0)
|
||||
self.total_wins = s.get("total_wins", 0)
|
||||
self.last_bar_ts = s.get("last_bar_ts", 0)
|
||||
if "books" in s:
|
||||
for j, bs in enumerate(s["books"][: self.k]):
|
||||
b = self.books[j]
|
||||
b["weights"] = {**{a: 0.0 for a in self.universe}, **bs.get("weights", {})}
|
||||
b["entry_px"] = {**{a: 0.0 for a in self.universe}, **bs.get("entry_px", {})}
|
||||
b["bars_held"] = int(bs.get("bars_held", 0))
|
||||
b["in_position"] = bool(bs.get("in_position", False))
|
||||
b["wait"] = int(bs.get("wait", 0))
|
||||
elif s.get("in_position") or s.get("weights"):
|
||||
# migrazione dallo schema legacy single-book: il vecchio book diventa la
|
||||
# tranche 0; le altre partono flat col loro sfasamento (gia' in __init__)
|
||||
b = self.books[0]
|
||||
b["weights"] = {**{a: 0.0 for a in self.universe}, **s.get("weights", {})}
|
||||
b["entry_px"] = {**{a: 0.0 for a in self.universe}, **s.get("entry_px", {})}
|
||||
b["bars_held"] = int(s.get("bars_held", 0))
|
||||
b["in_position"] = bool(s.get("in_position", False))
|
||||
b["wait"] = 0
|
||||
|
||||
def _save(self):
|
||||
self.status_path.write_text(json.dumps({
|
||||
"capital": round(float(self.capital), 2), "in_position": bool(self.in_position),
|
||||
"tranches": int(self.k),
|
||||
"books": [{"weights": {a: round(float(v), 5) for a, v in b["weights"].items()},
|
||||
"entry_px": {a: float(v) for a, v in b["entry_px"].items()},
|
||||
"bars_held": int(b["bars_held"]), "in_position": bool(b["in_position"]),
|
||||
"wait": int(b["wait"])} for b in self.books],
|
||||
"total_trades": int(self.total_trades), "total_wins": int(self.total_wins),
|
||||
"last_bar_ts": int(self.last_bar_ts),
|
||||
"last_update": datetime.now(timezone.utc).isoformat(),
|
||||
}, indent=2))
|
||||
|
||||
def _log(self, event, data=None):
|
||||
entry = {"ts": datetime.now(timezone.utc).isoformat(), "worker": self.worker_id,
|
||||
"event": event, **(data or {})}
|
||||
with open(self.trades_path, "a") as f:
|
||||
f.write(json.dumps(entry, default=str) + "\n")
|
||||
print(f" [{self.worker_id}] {event}: {json.dumps(data or {}, default=str)[:160]}")
|
||||
|
||||
def _notify(self, event, data=None):
|
||||
notify_event(event, {"worker": self.worker_id, **(data or {})})
|
||||
|
||||
# ---------- pannello allineato ----------
|
||||
def _panel(self, data: dict):
|
||||
frames = []
|
||||
for a in self.universe:
|
||||
df = data.get(a)
|
||||
if df is None or df.empty:
|
||||
return None
|
||||
frames.append(df[["timestamp", "close"]].rename(columns={"close": a}).set_index("timestamp"))
|
||||
M = pd.concat(frames, axis=1, join="inner").sort_index()
|
||||
# scarta la barra IN FORMAZIONE (close non settled) — come gli altri worker
|
||||
from src.live.bars import last_bar_is_forming
|
||||
ts = M.index.to_numpy()
|
||||
if len(ts) and last_bar_is_forming(ts):
|
||||
M = M.iloc[:-1]
|
||||
return M
|
||||
|
||||
# ---------- weights (identici all'engine) ----------
|
||||
def _weights(self, logC_row, logC_lb_row):
|
||||
dm = logC_row - logC_lb_row
|
||||
dm = dm - dm.mean()
|
||||
w = -dm
|
||||
gw = np.sum(np.abs(w))
|
||||
return w / gw if gw > 1e-9 else None
|
||||
|
||||
def _close_book(self, b, closes_now, tranche: int):
|
||||
"""Realizza il PnL del book della tranche al prezzo attuale (log-return netto fee).
|
||||
Capitale comune: il notional della tranche e' 1/K del book virtuale."""
|
||||
book = 0.0
|
||||
for k, a in enumerate(self.universe):
|
||||
book += b["weights"][a] * np.log(closes_now[k] / b["entry_px"][a])
|
||||
# cast a tipi Python: i numpy (float64/int64/bool_) rompono json.dumps in _save
|
||||
net = float(book - 2 * self.fee_rt)
|
||||
pnl = float(self.capital * self.position_size * self.leverage * net / self.k)
|
||||
self.capital = max(self.capital + pnl, 10.0)
|
||||
self.total_trades += 1
|
||||
self.total_wins += 1 if net > 0 else 0
|
||||
acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0
|
||||
self._log("CLOSE", {"tranche": tranche, "book_ret": round(book * 100, 3),
|
||||
"net": round(net * 100, 3),
|
||||
"pnl": round(pnl, 2), "capital": round(self.capital, 2),
|
||||
"trades": self.total_trades, "acc": round(acc, 1)})
|
||||
b["in_position"] = False
|
||||
b["weights"] = {a: 0.0 for a in self.universe}
|
||||
|
||||
def _open_book(self, M, i, b, tranche: int):
|
||||
cols = list(M.columns)
|
||||
logC = np.log(M.values)
|
||||
if self.disp_min is not None:
|
||||
disp = float(np.nanstd(logC[i] - logC[i - self.lb]))
|
||||
if disp < float(self.disp_min):
|
||||
return # regime senza dispersione: skip entry
|
||||
w = self._weights(logC[i], logC[i - self.lb])
|
||||
if w is None:
|
||||
return
|
||||
closes = M.iloc[i].values
|
||||
b["weights"] = {a: float(w[cols.index(a)]) for a in self.universe}
|
||||
b["entry_px"] = {a: float(closes[cols.index(a)]) for a in self.universe}
|
||||
b["bars_held"] = 0
|
||||
b["in_position"] = True
|
||||
self._log("OPEN", {"tranche": tranche,
|
||||
"long": [a for a in self.universe if b["weights"][a] > 0.05],
|
||||
"short": [a for a in self.universe if b["weights"][a] < -0.05],
|
||||
"capital": round(self.capital, 2)})
|
||||
|
||||
# ---------- tick ----------
|
||||
def tick(self, data: dict):
|
||||
M = self._panel(data)
|
||||
if M is None or len(M) < self.lb + 1: # serve close[i] e close[i-lb] -> lb+1 barre
|
||||
return
|
||||
i = len(M) - 1
|
||||
cur_ts = int(M.index[i])
|
||||
new_bar = cur_ts > self.last_bar_ts
|
||||
|
||||
for j, b in enumerate(self.books):
|
||||
if b["in_position"]:
|
||||
if new_bar:
|
||||
b["bars_held"] += 1
|
||||
# esce dopo HOLD barre; NON rientra nello stesso tick -> entry-to-entry = hold+1
|
||||
if b["bars_held"] >= self.hold:
|
||||
self._close_book(b, M.iloc[i].values, j)
|
||||
elif b["wait"] > 0:
|
||||
if new_bar:
|
||||
b["wait"] -= 1 # sfasamento iniziale della tranche
|
||||
else:
|
||||
self._open_book(M, i, b, j) # entra al bar corrente (i = lb alla prima volta)
|
||||
# solo avanti: se il panel si accorcia per un feed in ritardo (inner join),
|
||||
# non si regredisce — una barra gia' contata non va ricontata
|
||||
self.last_bar_ts = max(self.last_bar_ts, cur_ts)
|
||||
self._save()
|
||||
|
||||
@property
|
||||
def status_summary(self) -> str:
|
||||
acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0
|
||||
nb = sum(1 for b in self.books if b["in_position"])
|
||||
st = f"BOOK {nb}/{self.k}" if nb else "FLAT"
|
||||
return f"{self.worker_id}: €{self.capital:.0f} | {self.total_trades}t {acc:.0f}% | {st}"
|
||||
Reference in New Issue
Block a user