feat(live): Stage 2 — GridWorker (Price Ladder) come PAPER sleeve nel runner
Wire del Price Ladder come sleeve PAPER (sim-only, fuori dal pool €500, NESSUN ordine reale): - runner: kind="grid" -> GridWorker in build_worker_for; _spec_assets_tf grid; tick branch (w.tick(res[asset])); meccanismo PAPER_EXTRA (sleeve paper letti da overrides.paper_extra, NON da _defs.py -> NON entrano nel backtest canonico/regression-lock: PORT06 resta 19 sleeve). Parsing difensivo (un errore non crasha il runner mainnet). Loop dati estesi a paper_extra. - GridWorker: bootstrap storia FULL (start fisso, come SH01) + mappatura capitale forward dal deploy (capital = initial*eq[-1]/base_norm) -> niente salti da finestra mobile; base_norm persistito (resume). grid_mtm robusto al df live (timestamp senza datetime; param df). - portfolios.yml: GRID_BTC in paper_extra (regime range1.5, rd0.20/ru0.06, L6, sl0.10/tp0.03, position_size 0.15 PINNATO). Gira in data/portfolio_paper_stats/GRID_BTC/. Validazione (validate_grid_worker.py): [A] logica n_trades==backtest, [B] forward-tracking esatto, [C] resume esatto. Dry-test integrazione runner: import OK, build OK, tick OK, pos 0.15. SICUREZZA: kind=grid mai eseguito reale (runner avvia ordini solo per single/ml); €500 intatti. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+41
-10
@@ -45,7 +45,7 @@ class GridWorker:
|
||||
|
||||
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):
|
||||
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
|
||||
@@ -59,6 +59,19 @@ class GridWorker:
|
||||
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"
|
||||
@@ -66,6 +79,17 @@ class GridWorker:
|
||||
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})
|
||||
@@ -76,14 +100,16 @@ class GridWorker:
|
||||
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._log("RESUME", {"capital": round(self.capital, 2), "n_trades": self.n_trades})
|
||||
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,
|
||||
"in_position": self.in_position, "params": self.p,
|
||||
"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))
|
||||
|
||||
@@ -97,28 +123,33 @@ class GridWorker:
|
||||
pass
|
||||
|
||||
def tick(self, df: pd.DataFrame):
|
||||
"""df = OHLCV live (open/high/low/close[/datetime]) fino ad ora. Ricomputa la griglia
|
||||
col motore canonico e aggiorna capital = initial * equity_norm. SIM only (no ordini)."""
|
||||
"""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(df, p.get("ema_n", 200), p.get("trend_max", 2.0))
|
||||
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=df)
|
||||
flat_skip=True, deploy_mask=mask, df=full)
|
||||
if eqd is None or len(eqd) == 0:
|
||||
return
|
||||
new_cap = self.initial_capital * float(eqd.iloc[-1])
|
||||
self.capital = max(new_cap, 0.0)
|
||||
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(df.iloc[-1].get("datetime", df.iloc[-1].get("timestamp", "")))
|
||||
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"),
|
||||
|
||||
Reference in New Issue
Block a user