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,157 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user