feat(live): SH01 in esecuzione reale shadow (diversificatore decorrelato)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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`.
|
- **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).
|
- **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.
|
- **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.
|
- **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
|
- **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,
|
eseguiti il worker piazza uno **STOP_MARKET reduce-only a ~−30%** dall'ingresso (trigger sul mark,
|
||||||
|
|||||||
+25
-14
@@ -1,21 +1,32 @@
|
|||||||
# TODO — prossimi passi
|
# TODO — prossimi passi
|
||||||
|
|
||||||
> Raccolta dei follow-up aperti. Aggiornato 2026-06-07 (fine sessione: roadmap
|
> Raccolta dei follow-up aperti. Aggiornato 2026-06-08 (pairs reali ATTIVI v1.1.12;
|
||||||
> improvement-sweep 10/10 completata, v1.1.9 live).
|
> 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`
|
**REALE (shadow su Deribit testnet) — il "vero" risultato monetario:**
|
||||||
(open/close 2 gambe, leg-risk unwind, ledger shadow resume-safe). Test 92/92 + smoke
|
- 6 fade (MR01/MR02/MR07 × BTC/ETH) + DIP01_BTC → single-leg, v1.0.3+
|
||||||
testnet end-to-end OK. **Capability PRONTA ma SPENTA** (`pairs_enabled: false`).
|
- 5 pairs PR01 (ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL) → 2 gambe, v1.1.12 (2026-06-08)
|
||||||
Diario `docs/diary/2026-06-08-pairs-executor.md`.
|
- SH01 × BTC/ETH → single-leg, exit a orizzonte (no TP/SL), v1.1.13 (2026-06-08)
|
||||||
- [ ] **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.
|
**SIMULATO (paper, NON esegue reale) — categoria distinta:**
|
||||||
- [ ] **SH01 in reale**: oggi escluso perché non ha TP/SL (esce a orizzonte H=12) → manca il livello
|
- TR01 / ROT02 / TSM01 → book multi-asset, bloccati dal CAPITALE (rumore arrotondamento)
|
||||||
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
|
## Esecuzione reale — pezzi mancanti
|
||||||
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.
|
- [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)
|
## Capitale (decisione utente, non codice)
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -29,7 +29,11 @@ overrides:
|
|||||||
# simulati: pairs richiede executor a 2 gambe, shape non ha TP (orizzonte puro).
|
# simulati: pairs richiede executor a 2 gambe, shape non ha TP (orizzonte puro).
|
||||||
execution:
|
execution:
|
||||||
enabled: true
|
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:
|
instruments:
|
||||||
BTC: BTC_USDC-PERPETUAL
|
BTC: BTC_USDC-PERPETUAL
|
||||||
ETH: ETH_USDC-PERPETUAL
|
ETH: ETH_USDC-PERPETUAL
|
||||||
|
|||||||
@@ -264,8 +264,12 @@ def run(config_path: str = "portfolios.yml"):
|
|||||||
print(f"[runner] ESECUZIONE REALE PAIRS (2 gambe) attiva — strumenti={exec_instr}")
|
print(f"[runner] ESECUZIONE REALE PAIRS (2 gambe) attiva — strumenti={exec_instr}")
|
||||||
|
|
||||||
def _exec_for(s):
|
def _exec_for(s):
|
||||||
"""(executor, exec_instrument) per uno sleeve, solo se fade single-leg abilitato."""
|
"""(executor, exec_instrument) per uno sleeve single-leg ABILITATO. Kind:
|
||||||
if not exec_enabled or s.kind not in ("single",) or s.name not in exec_sleeves:
|
'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 None, None
|
||||||
return executor, exec_instr.get(s.asset)
|
return executor, exec_instr.get(s.asset)
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,53 @@ class FakeExec:
|
|||||||
"oid-close", "filled", True)
|
"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):
|
def _worker(tmp_path, fake, monkeypatch, notified):
|
||||||
monkeypatch.setattr("src.live.strategy_worker.notify_event",
|
monkeypatch.setattr("src.live.strategy_worker.notify_event",
|
||||||
lambda ev, data=None: notified.append(ev))
|
lambda ev, data=None: notified.append(ev))
|
||||||
|
|||||||
Reference in New Issue
Block a user