Files
PythagorasGoal/Old/src/live/grid_worker.py
T
Adriano Dal Pastro 14522262e6 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>
2026-06-19 15:20:59 +00:00

158 lines
7.5 KiB
Python

"""GridWorker — Price Ladder (griglia) live SIM/PAPER, shadow-stage 1.
Worker live per la strategia Price Ladder (griglia geometrica con regime-gate + SL/TP,
config vincente del branch price_ladder_research). STAGE 1 = SIM/PAPER: gira sul feed LIVE
Deribit (stessi dati di decisione degli altri worker) e contabilizza l'equity mark-to-market
col MOTORE CANONICO `grid_mtm` (parita' col backtest per costruzione), MA non piazza ordini
reali. Accumula un track record paper per validare live-vs-backtest prima dello shadow reale.
NON esegue ordini: l'esecuzione reale (griglia di LIMIT resting su Deribit, gestione fill
parziali/episodi) e' lo STAGE 2, dietro testnet + autorizzazione esplicita (soldi veri,
siamo su mainnet). Per costruzione il runner avvia ordini reali solo per kind in
('single','ml'); kind='grid' resta sim.
Stato persistente (status.json): capital, peak, max_dd, n_trades, last_ts -> resume al restart.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import pandas as pd
from scripts.analysis.grid_game_gate import grid_mtm
def _regime_mask(df: pd.DataFrame, ema_n: int, trend_max: float) -> np.ndarray:
"""Mask CAUSALE 'range-bound' allineata a df (== ladder_search.regime_mask, ma su df live)."""
c = df["close"].to_numpy(float)
h = df["high"].to_numpy(float); l = df["low"].to_numpy(float)
ema = pd.Series(c).ewm(span=ema_n, adjust=False).mean().to_numpy()
pc = np.roll(c, 1); pc[0] = c[0]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
atr = pd.Series(tr).rolling(14).mean().to_numpy()
with np.errstate(invalid="ignore", divide="ignore"):
dist = np.abs(c - ema) / np.where(atr == 0, np.nan, atr)
m = dist < trend_max
m[~np.isfinite(dist)] = False
return m
class GridWorker:
KIND = "grid"
def __init__(self, sid: str, asset: str, params: dict, capital: float,
work_dir: Path, leverage: float = 3.0, position_size: float = 0.15,
fee_side: float = 0.0005, notifier=None, hist: pd.DataFrame | None = None):
self.sid = sid
self.asset = asset
self.p = dict(params) # tf,range_down,range_up,levels,sl_buf,tp_buf,max_bars,regime,trend_max
self.leverage = leverage
self.position_size = position_size
self.fee_side = fee_side
self.notifier = notifier
self.initial_capital = capital
self.capital = capital
self.peak = capital
self.max_dd = 0.0
self.n_trades = 0
self.last_ts = ""
# base_norm = valore dell'equity-norm (cumulata da inizio storia) al DEPLOY: la
# capital forward = initial * eq[-1]/base_norm -> parte da `initial` e segue il
# ritorno della griglia DA QUEL MOMENTO (start FISSO: niente salti da finestra mobile).
self.base_norm = None
# bootstrap STORIA FULL (start fisso, come SH01): il feed live e' una finestra mobile,
# ma normalizzando su una serie a start fisso l'equity forward e' stabile.
if hist is None:
try:
from src.data.downloader import load_data
hist = load_data(asset, self.p.get("tf", "1h"))
except Exception:
hist = None
self.hist = hist
self.work_dir = Path(work_dir)
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.in_position = False # compat dashboard (la griglia non ha una posizione singola)
self._load_state()
def _merge(self, live_df: pd.DataFrame) -> pd.DataFrame:
"""Storia bootstrap + feed live, dedup su timestamp (il live prevale), start FISSO."""
if self.hist is None or len(self.hist) == 0:
return live_df
cols = ["timestamp", "open", "high", "low", "close", "volume"]
h = self.hist[[c for c in cols if c in self.hist.columns]]
l = live_df[[c for c in cols if c in live_df.columns]]
m = pd.concat([h, l], ignore_index=True)
m = m.drop_duplicates(subset="timestamp", keep="last").sort_values("timestamp")
return m.reset_index(drop=True)
def _load_state(self):
if not self.status_path.exists():
self._log("INIT", {"capital": round(self.capital, 2), "sid": self.sid})
return
s = json.loads(self.status_path.read_text())
self.capital = s.get("capital", self.initial_capital)
self.peak = s.get("peak", self.capital)
self.max_dd = s.get("max_dd", 0.0)
self.n_trades = s.get("n_trades", 0)
self.last_ts = s.get("last_ts", "")
self.base_norm = s.get("base_norm")
self._log("RESUME", {"capital": round(self.capital, 2), "n_trades": self.n_trades,
"base_norm": self.base_norm})
def _save_state(self):
self.status_path.write_text(json.dumps({
"sid": self.sid, "kind": self.KIND, "asset": self.asset,
"capital": round(self.capital, 4), "peak": round(self.peak, 4),
"max_dd": round(self.max_dd, 4), "n_trades": self.n_trades,
"base_norm": self.base_norm, "in_position": self.in_position, "params": self.p,
"last_ts": self.last_ts, "ts": datetime.now(timezone.utc).isoformat(),
}, indent=2))
def _log(self, event: str, extra: dict):
row = {"ts": datetime.now(timezone.utc).isoformat(), "sid": getattr(self, "sid", "?"),
"event": event, **extra}
try:
with open(self.work_dir / "trades.jsonl", "a") as f:
f.write(json.dumps(row) + "\n")
except Exception:
pass
def tick(self, df: pd.DataFrame):
"""df = OHLCV live (finestra mobile) fino ad ora. Merge con la storia bootstrap
(start FISSO), ricomputa la griglia col motore canonico, e mappa il capitale forward:
capital = initial * eq[-1]/base_norm (parte da `initial` al deploy, segue la griglia
da li' in poi). SIM only (nessun ordine reale)."""
if df is None or len(df) < 40:
return
full = self._merge(df)
p = self.p
regime = p.get("regime", "none")
mask = (_regime_mask(full, p.get("ema_n", 200), p.get("trend_max", 2.0))
if regime == "range" else None)
eqd, st = grid_mtm(
self.asset, tf=p["tf"], range_down=p["range_down"], range_up=p["range_up"],
levels=p["levels"], sl_buf=p["sl_buf"], tp_buf=p["tp_buf"], max_bars=p["max_bars"],
pos=self.position_size, lev=self.leverage, fee_side=self.fee_side,
flat_skip=True, deploy_mask=mask, df=full)
if eqd is None or len(eqd) == 0:
return
cur = float(eqd.iloc[-1])
if self.base_norm is None or self.base_norm <= 0:
self.base_norm = cur # baseline al primo tick (deploy)
self.capital = max(self.initial_capital * cur / self.base_norm, 0.0)
self.peak = max(self.peak, self.capital)
if self.peak > 0:
self.max_dd = max(self.max_dd, (self.peak - self.capital) / self.peak)
self.n_trades = int(st.get("trades", self.n_trades))
self.last_ts = str(full.iloc[-1].get("timestamp", ""))
self._save_state()
self._log("GRID_MTM", {"capital": round(self.capital, 2), "n_trades": self.n_trades,
"win": st.get("win"), "stops": st.get("stops"),
"pnl_source": "sim"})
return self.capital