"""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). """ 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)) 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.capital = capital self.in_position = False self.weights = {a: 0.0 for a in self.universe} self.entry_px = {a: 0.0 for a in self.universe} self.bars_held = 0 self.cooldown = 0 # 1 barra di stacco dopo l'uscita (come l'engine) self.total_trades = 0 self.total_wins = 0 self.last_bar_ts = 0 self._load() # ---------- 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}) return s = json.loads(self.status_path.read_text()) self.capital = s.get("capital", self.initial_capital) self.in_position = s.get("in_position", False) self.weights = {**{a: 0.0 for a in self.universe}, **s.get("weights", {})} self.entry_px = {**{a: 0.0 for a in self.universe}, **s.get("entry_px", {})} self.bars_held = s.get("bars_held", 0) self.cooldown = s.get("cooldown", 0) 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) def _save(self): self.status_path.write_text(json.dumps({ "capital": round(self.capital, 2), "in_position": self.in_position, "weights": {a: round(v, 5) for a, v in self.weights.items()}, "entry_px": self.entry_px, "bars_held": self.bars_held, "cooldown": self.cooldown, "total_trades": self.total_trades, "total_wins": self.total_wins, "last_bar_ts": 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, closes_now): """Realizza il PnL del book corrente al prezzo attuale (log-return netto fee).""" book = 0.0 for k, a in enumerate(self.universe): book += self.weights[a] * np.log(closes_now[k] / self.entry_px[a]) net = book - 2 * self.fee_rt pnl = self.capital * self.position_size * self.leverage * net self.capital = max(self.capital + pnl, 10.0) self.total_trades += 1 self.total_wins += net > 0 acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0 self._log("CLOSE", {"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)}) self.in_position = False self.weights = {a: 0.0 for a in self.universe} def _open_book(self, M, i): cols = list(M.columns) logC = np.log(M.values) w = self._weights(logC[i], logC[i - self.lb]) if w is None: return closes = M.iloc[i].values self.weights = {a: float(w[cols.index(a)]) for a in self.universe} self.entry_px = {a: float(closes[cols.index(a)]) for a in self.universe} self.bars_held = 0 self.in_position = True self._log("OPEN", {"long": [a for a in self.universe if self.weights[a] > 0.05], "short": [a for a in self.universe if self.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 if self.in_position: if new_bar: self.bars_held += 1 self.last_bar_ts = cur_ts # esce dopo HOLD barre; NON rientra nello stesso tick -> entry-to-entry = hold+1 if self.bars_held >= self.hold: self._close_book(M.iloc[i].values) else: self._open_book(M, i) # entra al bar corrente (i = lb alla prima volta) 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 st = "BOOK" if self.in_position else ("COOL" if self.cooldown else "FLAT") return f"{self.worker_id}: €{self.capital:.0f} | {self.total_trades}t {acc:.0f}% | {st}"