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:
Adriano Dal Pastro
2026-06-19 15:16:03 +00:00
parent 8401a280b9
commit 14522262e6
383 changed files with 1971 additions and 779 deletions
+216
View File
@@ -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}"