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:
Adriano Dal Pastro
2026-06-18 16:18:53 +00:00
parent b3d4ab7150
commit 264b9200ea
5 changed files with 137 additions and 51 deletions
+29 -2
View File
@@ -23,6 +23,7 @@ from src.live.basket_trend_worker import BasketTrendWorker
from src.live.rotation_worker import RotationWorker
from src.live.tsmom_worker import TsmomWorker
from src.live.xsec_worker import CrossSectionalWorker
from src.live.grid_worker import GridWorker
from src.live.strategy_loader import load_strategy
# Codice-breve sleeve -> nome modulo Strategy in scripts/strategies/ (worker single/ml)
@@ -102,6 +103,13 @@ def build_worker_for(spec: SleeveSpec, alloc_capital: float, leverage: float,
capital=alloc_capital, position_size=position_size, leverage=leverage,
data_dir=data_dir,
)
if spec.kind == "grid":
# Price Ladder (griglia) — SIM/PAPER (shadow stage 1): nessun ordine reale.
return GridWorker(
sid=spec.sid, asset=spec.asset, params=spec.params, capital=alloc_capital,
work_dir=Path(data_dir) / spec.sid, leverage=leverage,
position_size=position_size, fee_side=0.0005,
)
module = _STRAT_MODULE.get(spec.name)
if module is None:
raise ValueError(f"sleeve live non supportato: {spec.name} (kind={spec.kind})")
@@ -191,6 +199,8 @@ def _spec_assets_tf(spec: SleeveSpec):
return [spec.a, spec.b], spec.tf
if spec.kind in _MULTI_KINDS:
return list(spec.params["universe"]), spec.params.get("tf", "1d" if spec.kind != "basket" else "4h")
if spec.kind == "grid":
return [spec.asset], spec.params.get("tf", spec.tf)
return [spec.asset], spec.tf
@@ -375,6 +385,20 @@ def run(config_path: str = "portfolios.yml"):
paper_codes = {str(c).upper() for c in (_ov.get("paper_sleeves") or [])}
live_specs = [s for s in supported if s.name.upper() not in paper_codes]
paper_specs = [s for s in supported if s.name.upper() in paper_codes]
# PAPER_EXTRA: sleeve paper definiti SOLO in config (NON in p.sleeves) -> NON entrano
# nel backtest canonico/regression-lock (all_sleeve_equities non sa costruirli). Nato
# per il Price Ladder (kind=grid, shadow stage 1 sim). Parsing DIFENSIVO: un errore qui
# non deve crashare il runner mainnet.
for _ex in (_ov.get("paper_extra") or []):
try:
paper_specs.append(SleeveSpec(
kind=str(_ex["kind"]), name=str(_ex.get("name", _ex["sid"])),
sid=str(_ex["sid"]), asset=_ex.get("asset"),
tf=str(_ex.get("tf", "1h")), params=dict(_ex.get("params", {})),
cluster=str(_ex.get("cluster", "")),
))
except Exception as e:
print(f"[runner] WARN paper_extra ignorato ({_ex}): {e}")
if paper_specs:
print(f"[runner] sleeve PAPER (solo statistica, fuori dal pool): "
f"{[s.sid for s in paper_specs]}")
@@ -467,7 +491,7 @@ def run(config_path: str = "portfolios.yml"):
# lookback (giorni) richiesto per ogni asset = max sui worker che lo usano
asset_days: dict[str, int] = {}
for s in supported: # live + PAPER (anche XS01/TR01/ROT02/TSM01)
for s in live_specs + paper_specs: # live + PAPER (incl. paper_extra grid)
assets, tf = _spec_assets_tf(s)
days = _LOOKBACK_DAYS.get(tf, 90)
if s.kind == "ml": # SH01 ha bisogno di molta storia 1h
@@ -478,7 +502,7 @@ def run(config_path: str = "portfolios.yml"):
# timeframe SUB-orari (es. pairs 15m, flat-skip): non resamplabili dal 1h ->
# fetch DIRETTO da Cerbero per (asset, tf). Inerte se nessuno sleeve e' sub-orario.
subhourly_needs: dict[tuple[str, str], int] = {}
for s in supported: # live + paper
for s in live_specs + paper_specs: # live + paper (incl. paper_extra grid)
assets, tf = _spec_assets_tf(s)
if tf in _SUBHOURLY:
for a in assets:
@@ -603,6 +627,9 @@ def run(config_path: str = "portfolios.yml"):
# interno fitta solo l'ultimo blocco (last_block_only nei params).
w.tick(_with_history(ml_hist.get(s.asset), res[s.asset],
ml_gap_warned, s.asset))
elif s.kind == "grid":
# Price Ladder SIM/PAPER: ricomputa la griglia sul feed live (BTC 1h).
w.tick(res[s.asset])
else:
# single (fade/dip): StrategyWorker su feed live.
w.tick(res[s.asset])