feat(live): alert Telegram esecuzione reale + doc shadow-execution in CLAUDE.md
- telegram_notifier: eventi REAL_EXEC_LIVE (primo ordine reale verificato per worker) e REAL_OPEN_FAIL (apertura reale non verificata) - StrategyWorker: notifica una-tantum al primo REAL_OPEN verificato + ogni FAIL; flag real_first_notified persistito - CLAUDE.md: documentato lo shadow-execution (ExecutionClient, lineari USDC, verifica sul trade, fee reali, config, alert, smoke) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,11 @@ src/live/ → paper trading live multi-strategia
|
|||||||
pairs_worker.py → worker a 2 GAMBE per PR01 (market-neutral): long A / short B sullo
|
pairs_worker.py → worker a 2 GAMBE per PR01 (market-neutral): long A / short B sullo
|
||||||
z-score del log-ratio, exit |z|<=z_exit o max_bars, fee su 2 gambe.
|
z-score del log-ratio, exit |z|<=z_exit o max_bars, fee su 2 gambe.
|
||||||
strategy_loader.py → import dinamico classi Strategy da scripts/strategies/
|
strategy_loader.py → import dinamico classi Strategy da scripts/strategies/
|
||||||
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet)
|
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet): dati + ordini
|
||||||
|
(place_order market/reduce_only, get_trade_history, get_positions)
|
||||||
|
execution.py → ExecutionClient: esecuzione REALE su Deribit (shadow). notional→
|
||||||
|
amount (lineari USDC + inverse), open/close_amount reduce-only,
|
||||||
|
verifica sul trade (order_id), fee reali dai trades[]
|
||||||
signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS
|
signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS
|
||||||
telegram_notifier.py → notifiche Telegram per trade
|
telegram_notifier.py → notifiche Telegram per trade
|
||||||
src/portfolio/ → portafogli di prima classe (capitale-pool, backtest+live)
|
src/portfolio/ → portafogli di prima classe (capitale-pool, backtest+live)
|
||||||
@@ -76,6 +80,8 @@ uv run python -m src.live.multi_runner # paper trading live multi
|
|||||||
uv run python scripts/portfolios/PORT06_master_shape.py # report backtest portafoglio (default)
|
uv run python scripts/portfolios/PORT06_master_shape.py # report backtest portafoglio (default)
|
||||||
uv run python -m src.portfolio.runner # paper trading a PORTAFOGLIO (capitale pool)
|
uv run python -m src.portfolio.runner # paper trading a PORTAFOGLIO (capitale pool)
|
||||||
uv run python scripts/analysis/smoke_portfolio.py # smoke live data layer Cerbero v2
|
uv run python scripts/analysis/smoke_portfolio.py # smoke live data layer Cerbero v2
|
||||||
|
uv run python scripts/analysis/live_exec_smoke.py # smoke ESECUZIONE reale (ordine→verifica→fee, testnet)
|
||||||
|
uv run python scripts/analysis/live_shadow_smoke.py # smoke catena shadow nel worker (open/close reali)
|
||||||
uv run python scripts/analysis/regime_fetcher.py # fetch DVOL+funding (Deribit mainnet) -> data/regime/
|
uv run python scripts/analysis/regime_fetcher.py # fetch DVOL+funding (Deribit mainnet) -> data/regime/
|
||||||
./scripts/deploy.sh [patch|minor|major] # DEPLOY: bump versione + commit + rebuild Docker
|
./scripts/deploy.sh [patch|minor|major] # DEPLOY: bump versione + commit + rebuild Docker
|
||||||
uv run pytest # test
|
uv run pytest # test
|
||||||
@@ -309,6 +315,7 @@ 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 — 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], instruments:{BTC:BTC_USDC-PERPETUAL, ETH:ETH_USDC-PERPETUAL}}`; pairs/rotation/tsmom/shape/dip restano **simulati**. **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.
|
||||||
- **Limite noto:** al ribilancio le posizioni APERTE restano sul loro notional (non travasate); fedele al backtest daily-rebalanced entro il turnover infragiornaliero.
|
- **Limite noto:** al ribilancio le posizioni APERTE restano sul loro notional (non travasate); fedele al backtest daily-rebalanced entro il turnover infragiornaliero.
|
||||||
|
|
||||||
## Multi-Strategy Paper Trader
|
## Multi-Strategy Paper Trader
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class StrategyWorker:
|
|||||||
self.real_entry_notional = 0.0 # USD effettivi esposti all'entrata
|
self.real_entry_notional = 0.0 # USD effettivi esposti all'entrata
|
||||||
self.real_order_id = ""
|
self.real_order_id = ""
|
||||||
self.real_trades = 0
|
self.real_trades = 0
|
||||||
|
self.real_first_notified = False # alert Telegram "esecuzione viva" una tantum
|
||||||
|
|
||||||
self.worker_id = f"{strategy.name}__{asset}__{tf}"
|
self.worker_id = f"{strategy.name}__{asset}__{tf}"
|
||||||
self.work_dir = data_dir / self.worker_id
|
self.work_dir = data_dir / self.worker_id
|
||||||
@@ -116,6 +117,7 @@ class StrategyWorker:
|
|||||||
self.real_entry_notional = state.get("real_entry_notional", 0.0)
|
self.real_entry_notional = state.get("real_entry_notional", 0.0)
|
||||||
self.real_order_id = state.get("real_order_id", "")
|
self.real_order_id = state.get("real_order_id", "")
|
||||||
self.real_trades = state.get("real_trades", 0)
|
self.real_trades = state.get("real_trades", 0)
|
||||||
|
self.real_first_notified = state.get("real_first_notified", False)
|
||||||
|
|
||||||
self._log("RESUME", {"capital": round(self.capital, 2),
|
self._log("RESUME", {"capital": round(self.capital, 2),
|
||||||
"total_trades": self.total_trades,
|
"total_trades": self.total_trades,
|
||||||
@@ -147,6 +149,7 @@ class StrategyWorker:
|
|||||||
"real_entry_notional": self.real_entry_notional,
|
"real_entry_notional": self.real_entry_notional,
|
||||||
"real_order_id": self.real_order_id,
|
"real_order_id": self.real_order_id,
|
||||||
"real_trades": self.real_trades,
|
"real_trades": self.real_trades,
|
||||||
|
"real_first_notified": self.real_first_notified,
|
||||||
"last_update": datetime.now(timezone.utc).isoformat(),
|
"last_update": datetime.now(timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
with open(self.status_path, "w") as f:
|
with open(self.status_path, "w") as f:
|
||||||
@@ -228,8 +231,12 @@ class StrategyWorker:
|
|||||||
if linear else fill.amount)
|
if linear else fill.amount)
|
||||||
self.real_order_id = fill.order_id or ""
|
self.real_order_id = fill.order_id or ""
|
||||||
self._log("REAL_OPEN", data)
|
self._log("REAL_OPEN", data)
|
||||||
|
if not self.real_first_notified: # conferma una-tantum: l'esecuzione reale e' viva
|
||||||
|
self._notify("REAL_EXEC_LIVE", data)
|
||||||
|
self.real_first_notified = True
|
||||||
else:
|
else:
|
||||||
self._log("REAL_OPEN_FAIL", {**data, "note": fill.notes})
|
self._log("REAL_OPEN_FAIL", {**data, "note": fill.notes})
|
||||||
|
self._notify("REAL_OPEN_FAIL", {**data, "note": fill.notes})
|
||||||
|
|
||||||
def _real_close(self, sim_exit: float, reason: str, sim_pnl: float):
|
def _real_close(self, sim_exit: float, reason: str, sim_pnl: float):
|
||||||
"""Chiusura REALE (reduce-only della quota worker) + confronto col sim."""
|
"""Chiusura REALE (reduce-only della quota worker) + confronto col sim."""
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
|||||||
NOTIFY_EVENTS = {
|
NOTIFY_EVENTS = {
|
||||||
"SIGNAL", "OPENED", "CLOSED", "OPEN_FAILED", "CLOSE_FAILED",
|
"SIGNAL", "OPENED", "CLOSED", "OPEN_FAILED", "CLOSE_FAILED",
|
||||||
"ERROR", "STARTUP", "SHUTDOWN", "TRAINING_FAILED",
|
"ERROR", "STARTUP", "SHUTDOWN", "TRAINING_FAILED",
|
||||||
|
# esecuzione REALE (shadow su Deribit testnet)
|
||||||
|
"REAL_EXEC_LIVE", # primo ordine reale verificato di un worker (conferma "e' vivo")
|
||||||
|
"REAL_OPEN_FAIL", # un'apertura reale NON si e' verificata (problema da guardare)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user