From 0b900a54201343db6dc4d138990b95ce9af71004 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Mon, 8 Jun 2026 11:18:06 +0000 Subject: [PATCH] feat(live): SH01 in esecuzione reale shadow (diversificatore decorrelato) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'infrastruttura no-TP esisteva gia': _place_real_tp ritorna subito senza TP, _real_close chiude tutta la quota a market reduce-only (exit a orizzonte H=12). Bastava: (1) _exec_for accetta kind 'ml', (2) SH01 in execution.sleeves. Disaster-bracket on-book = unica protezione di coda di SH01 (esce a H=12 ben prima del -30%). Motivo: SH01 e' il diversificatore piu' decorrelato (corr 0.07) — senza i 5 sleeve PAPER il DD del portafoglio sale 3.96->5.35%, OOS Sharpe 8.58->8.27. Test: SH01 open/close reale senza TP + disaster bracket (93/93). Copertura reale ora ~81% (resta paper solo TR01/ROT02/TSM01, bloccati dal capitale). Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 9 +++++ docs/TODO.md | 39 +++++++++++++-------- portfolios.yml | 6 +++- src/portfolio/runner.py | 8 +++-- tests/portfolio/test_real_close_dsl.py | 47 ++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f2f8798..7e07dd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -370,6 +370,15 @@ queste fade, ma va confermato col paper trader live prima di rischiare capitale - **FIX SH01 wiring (2026-06-01).** SH01 gira come **`StrategyWorker` NORMALE** (NON il vecchio `MLWorkerWrapper` di `multi_runner`, che usava il `SignalEngine` **squeeze SCARTATO**: apriva senza metadata ed usciva a `hold_bars=3`, ignorando del tutto SH01_shape_ml). `SH01_shape_ml.generate_signals` fa il walk-forward (retraining) internamente ad ogni tick ed emette `metadata.max_bars=12` → exit a orizzonte via `StrategyWorker.tick`. Serve ≥4000 barre 1h (`_ML_LOOKBACK_DAYS=365`). Vedi `docs/diary/2026-06-01-sh01-wiring-squeeze-bug.md`. - **Altri fix StrategyWorker (2026-06-01).** Exit a orizzonte puro per strategie senza TP/SL (`elif self.max_bars`, SH01 esce a H=12 non hold_bars=3); `is_win = net > 0` (win NETTO fee, non lordo); filtro `min_tp_frac` (salta micro-scalp col TP entro le fee); loss-guard `hurst_max=0.55` sulle fade (vedi sopra). - **Exit intrabar (fase 3, risolto):** lo `StrategyWorker` ora esce sui TP/SL toccati INTRABAR (high/low della barra, al livello, SL prioritario) come il backtest — non più solo sul close. Allinea fade/DIP01 live al backtest intrabar (`tests/portfolio/test_intrabar_exit.py`). Caveat residuo onesto: nel paper trading l'high/low usato è quello della barra in corso al poll; su un fill reale conterebbe il momento del tocco. +- **ESECUZIONE REALE — pairs 2 gambe + SH01 (2026-06-08).** Estesa oltre i fade: i **5 pairs PR01** + eseguono reale a 2 gambe (`PairsExecutionClient`: open/close long A/short B, leg-risk unwind, MAI + close_position; `pairs_enabled: true` acceso a conto flat, v1.1.12); **SH01** (BTC/ETH) esegue + single-leg con exit a orizzonte H=12 — niente TP/SL, `_place_real_tp` no-op e `_real_close` chiude + tutto market reduce-only, disaster-bracket on-book come unica protezione di coda (v1.1.13). Motivo: + SH01 è il diversificatore più decorrelato (senza i 5 sleeve PAPER il DD del portafoglio sale + 3.96→5.35%). Copertura reale ora ~81% (fade+DIP+pairs+SH01); restano simulati TR01/ROT02/TSM01 + (book multi-asset, bloccati dal capitale: rumore arrotondamento 20-30% a €2k, serve ~€20k). + Diari `docs/diary/2026-06-08-pairs-executor.md`. - **ESECUZIONE REALE — shadow (v1.0.3, 2026-06-03).** I **6 fade** (MR01/MR02/MR07 × BTC/ETH) eseguono ordini **REALI su Deribit testnet** accanto al fill simulato (*shadow*: sim + reale in parallelo, il sim resta la verità che guida le decisioni). `src/live/execution.py` `ExecutionClient`: `open` (market) + `close_amount` (market **reduce-only della SOLA quota del worker** — i 3 fade BTC condividono lo strumento e le posizioni si nettano per conto, quindi NON si usa `close_position` che flatterebbe le quote altrui); **verifica l'esecuzione sul TRADE** (order_id in `get_trade_history`, non sulla size netta aggregata); **fee REALI lette dai `trades[]`**. Strumenti = **lineari USDC** (`BTC_USDC`/`ETH_USDC-PERPETUAL`, amount nel base-coin, step 0.0001/0.001): scelti perché il **payoff lineare == matematica del backtest** (l'inverse `*-PERPETUAL` introdurrebbe una base 1/prezzo) e fee/PnL sono in USDC. Lo `StrategyWorker` tiene un **ledger reale parallelo** (`real_capital`, persistito) e logga `REAL_OPEN`/`REAL_CLOSE` col confronto **slippage** (prezzo sim vs eseguito) e **fee** (assunta 0.10% vs reale). Config: `portfolios.yml` → `overrides.execution {enabled, sleeves:[MR01,MR02,MR07,DIP01], instruments:{BTC:BTC_USDC-PERPETUAL, ETH:ETH_USDC-PERPETUAL}}` (**DIP01 aggiunto il 2026-06-04**: stesso wiring single-leg, TP resting incluso); pairs/rotation/tsmom/shape restano **simulati** (pairs richiede un executor a 2 gambe con gestione leg-risk; shape non ha TP). **Fee reali misurate = 0.05%/lato = 0.10% RT** (== assunto del backtest, su ETH; BTC inverse era ~0.094%). **Alert Telegram:** `REAL_EXEC_LIVE` (primo ordine reale verificato per worker) + `REAL_OPEN_FAIL`. Smoke (testnet, €0): `scripts/analysis/live_exec_smoke.py` (layer: ordine→verifica→fee) e `live_shadow_smoke.py` (catena worker open/close). **Capitale live portato a 2000** (notional fade ~$35) per ridurre il rumore di arrotondamento su BTC (step lineare ~$6.7). NB: ledger reale ≠ ledger sim — i worker già in posizione sim a un restart non hanno quota reale corrispondente; lo shadow reale parte pulito dalla prossima apertura. - **Disaster-bracket on-book + alert outage (v1.1.4, 2026-06-07).** A ogni `REAL_OPEN` dei fade eseguiti il worker piazza uno **STOP_MARKET reduce-only a ~−30%** dall'ingresso (trigger sul mark, diff --git a/docs/TODO.md b/docs/TODO.md index 316da5d..73ffc7c 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,21 +1,32 @@ # TODO — prossimi passi -> Raccolta dei follow-up aperti. Aggiornato 2026-06-07 (fine sessione: roadmap -> improvement-sweep 10/10 completata, v1.1.9 live). +> Raccolta dei follow-up aperti. Aggiornato 2026-06-08 (pairs reali ATTIVI v1.1.12; +> roadmap improvement-sweep 10/10 completata). -## Esecuzione reale — i pezzi grossi mancanti +## Stato esecuzione per sleeve (REALE vs SIMULATO) -- [x] ~~Executor a 2 gambe per i PAIRS~~ — FATTO (2026-06-08): `PairsExecutionClient` - (open/close 2 gambe, leg-risk unwind, ledger shadow resume-safe). Test 92/92 + smoke - testnet end-to-end OK. **Capability PRONTA ma SPENTA** (`pairs_enabled: false`). - Diario `docs/diary/2026-06-08-pairs-executor.md`. -- [ ] **Accendere `pairs_enabled: true`** quando c'è una finestra a conto flat + osservazione. - Poi monitorare REAL_OPEN_PAIR/REAL_CLOSE_PAIR e lo sbilanciamento notional fra gambe. -- [ ] **SH01 in reale**: oggi escluso perché non ha TP/SL (esce a orizzonte H=12) → manca il livello - a cui agganciare il limit resting + disaster-bracket. Due strade: (a) nuovo percorso - "market reduce-only a scadenza orizzonte" nell'executor; (b) valutare TP/SL su SH01 — MA la - ricerca dedicata (`sh01_exit_lab`, 11 famiglie di stop, 0 sopravvissute) dice che ogni stop - rompe l'edge → andrebbe ri-gateato. Prima conviene osservare il PnL live post-fix di oggi. +**REALE (shadow su Deribit testnet) — il "vero" risultato monetario:** +- 6 fade (MR01/MR02/MR07 × BTC/ETH) + DIP01_BTC → single-leg, v1.0.3+ +- 5 pairs PR01 (ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL) → 2 gambe, v1.1.12 (2026-06-08) +- SH01 × BTC/ETH → single-leg, exit a orizzonte (no TP/SL), v1.1.13 (2026-06-08) + +**SIMULATO (paper, NON esegue reale) — categoria distinta:** +- TR01 / ROT02 / TSM01 → book multi-asset, bloccati dal CAPITALE (rumore arrotondamento) + +## Esecuzione reale — pezzi mancanti + +- [x] ~~Executor a 2 gambe per i PAIRS~~ — FATTO + ATTIVO (v1.1.12, 2026-06-08). +- [x] ~~SH01 in reale~~ — FATTO (v1.1.13, 2026-06-08): l'infrastruttura no-TP esisteva già + (_place_real_tp no-op senza TP, _real_close chiude tutto market reduce-only a orizzonte). + Bastava accettare kind 'ml' in _exec_for + SH01 in execution.sleeves. Disaster-bracket + on-book = unica protezione di coda. Test SH01 open/close reale OK. +- [ ] **Multi-asset in reale** (SIMULATO: TR01/ROT02/TSM01) — **SBLOCCATO DA CAPITALE ≥ ~€20k**. + Strumenti USDC tutti esistenti (BNB/DOGE/XRP inclusi), ma a €2k il rumore di arrotondamento + per-asset è 20-30% (DOGE/XRP/SOL step grandi in $, esposizione frammentata su 5-8 asset) → + l'esecuzione reale misurerebbe il rumore dei lotti, non la strategia. Serve un + `BasketExecutionClient` (ribilancio delta multi-strumento, long-only, netting con fade+pairs + sui simboli condivisi). Beneficio limitato (~19% del portafoglio, diversificatori spesso + flat) → bassa priorità anche a capitale alto. Analisi: chat 2026-06-08. ## Capitale (decisione utente, non codice) diff --git a/portfolios.yml b/portfolios.yml index 8704f16..6da237c 100644 --- a/portfolios.yml +++ b/portfolios.yml @@ -29,7 +29,11 @@ overrides: # simulati: pairs richiede executor a 2 gambe, shape non ha TP (orizzonte puro). execution: enabled: true - sleeves: [MR01, MR02, MR07, DIP01] + # SH01 aggiunto 2026-06-08: e' il diversificatore piu' decorrelato (corr 0.07, + # senza di lui il DD del portafoglio sale 3.96->5.35%). Single-leg, esce a + # orizzonte H=12 (no TP/SL) -> _place_real_tp no-op, _real_close market reduce-only; + # disaster-bracket on-book = unica protezione di coda durante outage. + sleeves: [MR01, MR02, MR07, DIP01, SH01] instruments: BTC: BTC_USDC-PERPETUAL ETH: ETH_USDC-PERPETUAL diff --git a/src/portfolio/runner.py b/src/portfolio/runner.py index be6eca7..cb3b271 100644 --- a/src/portfolio/runner.py +++ b/src/portfolio/runner.py @@ -264,8 +264,12 @@ def run(config_path: str = "portfolios.yml"): print(f"[runner] ESECUZIONE REALE PAIRS (2 gambe) attiva — strumenti={exec_instr}") def _exec_for(s): - """(executor, exec_instrument) per uno sleeve, solo se fade single-leg abilitato.""" - if not exec_enabled or s.kind not in ("single",) or s.name not in exec_sleeves: + """(executor, exec_instrument) per uno sleeve single-leg ABILITATO. Kind: + 'single' (fade/DIP01) e 'ml' (SH01). SH01 non ha TP/SL -> _place_real_tp + ritorna subito e _real_close chiude tutto a market reduce-only (orizzonte): + infrastruttura gia' presente. Il disaster-bracket on-book resta l'unica + protezione di coda di SH01 durante un outage (esce a H=12 ben prima del -30%).""" + if not exec_enabled or s.kind not in ("single", "ml") or s.name not in exec_sleeves: return None, None return executor, exec_instr.get(s.asset) diff --git a/tests/portfolio/test_real_close_dsl.py b/tests/portfolio/test_real_close_dsl.py index 4b6e411..c5f392f 100644 --- a/tests/portfolio/test_real_close_dsl.py +++ b/tests/portfolio/test_real_close_dsl.py @@ -31,6 +31,53 @@ class FakeExec: "oid-close", "filled", True) +class FakeFullExec(FakeExec): + """Aggiunge open/place_tp_limit/place_disaster_sl per testare il ciclo SH01 completo.""" + def __init__(self): + super().__init__([{"state": "untriggered"}]) + self.tp_calls = 0 + self.dsl_calls = 0 + self.close_amounts = [] + + def open(self, instrument, side, notional, label=None): + amt = round(notional / 100.0, 6) + return Fill(instrument, side, notional, amt, 100.0, 0.0, 0.05, "oid-open", "filled", True) + + def place_tp_limit(self, *a, **k): + self.tp_calls += 1 # NON deve essere chiamato per SH01 (no TP) + return Fill("x", "sell", 0.0, 0.0, None, 0.0, 0.0, None, None, False) + + def place_disaster_sl(self, instrument, side, amount, stop, label=None): + self.dsl_calls += 1 + return Fill(instrument, "sell", 0.0, amount, None, 0.0, 0.0, "dsl-1", "untriggered", True) + + def close_amount(self, instrument, side, amount, label=None): + self.close_amounts.append(amount) + return Fill(instrument, "sell", 0.0, amount, 105.0, 0.0, 0.05, "oid-close", "filled", True) + + +def test_sh01_real_open_close_no_tp(tmp_path, monkeypatch): + """SH01 (no TP/SL, exit a orizzonte): apre reale, NON piazza TP, piazza disaster, + chiude TUTTA la quota a market reduce-only.""" + from src.live.strategy_worker import StrategyWorker + from src.strategies.base import Signal + notified = [] + monkeypatch.setattr("src.live.strategy_worker.notify_event", + lambda ev, data=None: notified.append(ev)) + fake = FakeFullExec() + w = StrategyWorker(strategy=SimpleNamespace(name="SH01_shape_ml", fee_rt=0.001), + asset="BTC", tf="1h", capital=100.0, position_size=0.5, leverage=2.0, + data_dir=tmp_path, executor=fake, exec_instrument="BTC_USDC-PERPETUAL") + w._open_position(Signal(idx=0, direction=1, entry_price=100.0, + metadata={"max_bars": 12}), 100.0) # niente tp/sl + assert w.real_in_position and fake.tp_calls == 0 # nessun TP per SH01 + assert fake.dsl_calls == 1 and w.real_dsl_order_id # disaster bracket sì + amt = w.real_amount + w._close_position(105.0, "time_limit") + assert not w.real_in_position + assert fake.close_amounts == [amt] # chiude TUTTA la quota a market + + def _worker(tmp_path, fake, monkeypatch, notified): monkeypatch.setattr("src.live.strategy_worker.notify_event", lambda ev, data=None: notified.append(ev))