Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b34e11476 | |||
| ae7f3d17f2 | |||
| 3b6ff02197 | |||
| 8dbdadd509 | |||
| 33267584d9 | |||
| dc2b5697da | |||
| 6b9c469832 | |||
| 14522262e6 | |||
| 8401a280b9 | |||
| b4d4961d77 | |||
| 264b9200ea | |||
| b3d4ab7150 | |||
| 587fbc0f61 | |||
| 8abeb7a83f | |||
| b9e3388f76 | |||
| e631e1b89c | |||
| d733196564 | |||
| 57758ffcb1 | |||
| c8b956ab56 | |||
| 3b60289cbd | |||
| e328836614 | |||
| a0842a61cc | |||
| 3a66907f43 | |||
| aee53161c3 | |||
| 7b3e554424 | |||
| b576ee66ac | |||
| ef797f1ff0 | |||
| 4c184d5cbc | |||
| c21e1dc635 | |||
| 47e92a0425 | |||
| faf08b3988 | |||
| 3ac8a46e6a | |||
| 7ee5b1767f | |||
| d5a83075f8 | |||
| 183209dbd4 | |||
| e311d00fe4 | |||
| 37ffe92498 | |||
| f6287fcd36 | |||
| b6b465878a | |||
| 22af19c7f9 | |||
| 31f08ddf32 | |||
| dc22256e0e | |||
| 6fe45867f0 | |||
| ab9899b0c4 | |||
| 432c1ad790 | |||
| 85043bf2d3 | |||
| 643ff7f943 | |||
| 12e71d4c8b | |||
| 378ec7fb43 | |||
| f78380596a | |||
| 82a1c60e08 | |||
| 515193a203 | |||
| 612f2bfced | |||
| a2d581691a | |||
| 5fe53841e9 | |||
| e0257c6c88 | |||
| 6f294e3889 | |||
| 1a0b601fab | |||
| 2b9adaeea3 | |||
| e7fd286fe0 | |||
| 003875f2c3 | |||
| e18adba4a6 | |||
| 36ac2426a1 | |||
| 82c05f6f81 | |||
| 3f1feb1a6f | |||
| cfdd8f6a72 | |||
| 844a9d0e22 | |||
| 22b77668f1 | |||
| 521447fce6 | |||
| 429fa01c97 | |||
| 8a2b065dd7 | |||
| 51baed22e4 | |||
| 12625b3315 | |||
| 15520dda70 | |||
| 2ae6d7afe6 | |||
| b208417f2d | |||
| 9d15506b05 | |||
| ba5ec7bd6b | |||
| 8d69a0cef5 | |||
| 8adf388e86 | |||
| e25d5db6ad | |||
| d1180ef25b | |||
| 61fcc29c5b | |||
| 5e557fe998 | |||
| cc39c36c08 | |||
| d2948d582b | |||
| a85289d7c7 | |||
| d3dab57532 | |||
| abc1c1cb80 | |||
| 1a9032feaa | |||
| 90c4726a31 | |||
| be72b157cf | |||
| d25d897fd1 | |||
| 5d45f4ef6e | |||
| cfc48cdef4 | |||
| aa4361ddde |
+12
@@ -18,7 +18,10 @@ data/processed/
|
|||||||
notebooks/.ipynb_checkpoints/
|
notebooks/.ipynb_checkpoints/
|
||||||
data/paper_trades/
|
data/paper_trades/
|
||||||
data/portfolio_paper/
|
data/portfolio_paper/
|
||||||
|
data/portfolio_paper_stats/
|
||||||
data/portfolios/
|
data/portfolios/
|
||||||
|
# watermark fondi del reconciler (stato runtime, contiene il balance)
|
||||||
|
data/funds_watch.json
|
||||||
|
|
||||||
# stato locale di tooling (non condiviso)
|
# stato locale di tooling (non condiviso)
|
||||||
.claude/
|
.claude/
|
||||||
@@ -32,3 +35,12 @@ data/regime/dispersion_features.parquet
|
|||||||
# storico catena opzioni importato da cerbero-bite (rigenerabile: options_fetcher.py)
|
# storico catena opzioni importato da cerbero-bite (rigenerabile: options_fetcher.py)
|
||||||
data/options/
|
data/options/
|
||||||
data/_reset_backup/
|
data/_reset_backup/
|
||||||
|
|
||||||
|
# game artifacts (log/json di scripts/games e gate)
|
||||||
|
data/games/
|
||||||
|
.env.mainnet
|
||||||
|
|
||||||
|
# archived data (mirrors top-level data/ ignores, which are top-level-anchored)
|
||||||
|
Old/data/
|
||||||
|
Old/**/__pycache__/
|
||||||
|
.cache_trackE_*.npy
|
||||||
|
|||||||
@@ -1,463 +1,164 @@
|
|||||||
# PythagorasGoal — Istruzioni per agenti
|
# PythagorasGoal — Istruzioni per agenti
|
||||||
|
|
||||||
## Panoramica
|
## Stato del progetto — v2.0.0 RESET (2026-06-19)
|
||||||
|
|
||||||
Progetto di ricerca: riconoscimento pattern frattali per trading algoritmico su criptovalute (BTC, ETH). L'obiettivo è arrivare a €50/giorno di profitto partendo da €1.000, entro 6–8 mesi.
|
**LEGGERE PRIMA DI TUTTO.** Il progetto è stato resettato dopo aver scoperto che l'intera
|
||||||
|
libreria di strategie "validata OOS" (FADE, PAIRS, DIP01, TR01, ROT02, TSM01, XS01, SH01) era
|
||||||
|
**artefatto di uno storico contaminato** — print fantasma del feed Cerbero **testnet** + storico
|
||||||
|
**Binance/USDT**. Ri-testate sul feed reale, tutte perdono ogni anno (vedi
|
||||||
|
`docs/diary/2026-06-19-deribit-history.md`, il documento di fondazione).
|
||||||
|
|
||||||
|
Cosa è cambiato:
|
||||||
|
- Lo storico è stato **ricostruito da Deribit mainnet** e **certificato**. Universo affidabile =
|
||||||
|
**solo BTC/ETH** (tutti i TF). Gli alt sono esclusi (illiquidi/divergenti/non certificabili).
|
||||||
|
- Tutto il codice vecchio (strategie, stack live, ~100 script di ricerca/gate, dati non
|
||||||
|
certificati, 60+ diari) è **archiviato in `Old/`** (preservato in git, non cancellato).
|
||||||
|
- L'esecuzione è **DISABILITATA**, il conto mainnet è flat. **Non c'è trading live attivo.**
|
||||||
|
- Si riparte dalla ricerca di strategie NUOVE, su dati certi, con la metodologia qui sotto.
|
||||||
|
|
||||||
|
### Ricerca post-reset (2026-06-19) — esito
|
||||||
|
|
||||||
|
Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condiviso
|
||||||
|
`src/backtest/harness.py`). Sintesi in `docs/diary/2026-06-19-research-synthesis.md`.
|
||||||
|
|
||||||
|
- **VINCITRICE (l'unica robusta e profittevole): TP01 Trend Portfolio** —
|
||||||
|
`src/strategies/trend_portfolio.py`. TSMOM multi-orizzonte (1-3-6 mesi) vol-targeted,
|
||||||
|
50/50 BTC+ETH. Config canonica **PORT LF4h** (4h, long-flat, vol-target 20%, leva cap 2x):
|
||||||
|
**CAGR ~16.6%, Sharpe ~1.32-1.36, maxDD ~12-14%, positiva ogni anno 2019-2026**.
|
||||||
|
Robusta su tutti i TF (15m-1d), regge fee fino a 0.40% RT, su entrambi gli asset.
|
||||||
|
Paper trader: `scripts/live/paper_trend.py`. Test: `tests/test_trend_portfolio.py`.
|
||||||
|
- **Edge deboli ma reali** (NON standalone, NON migliorano il portafoglio): ML walk-forward
|
||||||
|
su BTC (Sharpe ~0.57), trend 1h long-short (Sharpe ~1.0), relative-value market-neutral
|
||||||
|
ETH/BTC (scorrelato ~0.05 ma Sharpe solo 0.27 → troppo debole per alzare lo Sharpe).
|
||||||
|
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero su dati
|
||||||
|
certi — la vecchia libreria +201%/+1238% era pura contaminazione); trend 5m/15m (fee).
|
||||||
|
- **Soffitto strutturale:** con i soli BTC/ETH lo Sharpe di portafoglio si ferma a **~1.3**.
|
||||||
|
Combinare TF o aggiungere la RV non aiuta (ridondanza/edge troppo debole).
|
||||||
|
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
|
||||||
|
capitale o un DD da rovina). La leva non è la scorciatoia; la via è target-vol + capitale +
|
||||||
|
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
|
||||||
|
|
||||||
|
Script ricerca: `scripts/research/track{A,B,C,D,E}_*.py` + `trackD_timing.py`.
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
Ricerca: riconoscimento pattern frattali per trading algoritmico su crypto. Target dichiarato
|
||||||
|
€50/giorno partendo da €1.000. **Onestà prima di tutto**: nessun numero va creduto finché non è
|
||||||
|
netto fee, out-of-sample, robusto su griglia, e su dati certificati + liquidi + eseguibili.
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- **Linguaggio:** Python 3.11+
|
- **Linguaggio:** Python 3.11+ — **Package manager:** uv (`pyproject.toml`, `uv.lock`)
|
||||||
- **Package manager:** uv (dipendenze in `pyproject.toml`, lock in `uv.lock`)
|
- **Dati:** Parquet in `data/raw/` (gitignored). Solo BTC/ETH (5m/15m/1h).
|
||||||
- **Dati:** Parquet in `data/raw/` (non committati, ~70 MB)
|
- **Analisi/ML:** numpy, pandas, scipy, scikit-learn
|
||||||
- **ML:** scikit-learn (GradientBoostingClassifier)
|
- **Fonte dati storici:** Deribit mainnet via `ccxt` (pubblico, tokenless)
|
||||||
- **Analisi:** numpy, pandas, scipy
|
|
||||||
- **API dati:** Cerbero MCP su `cerbero-mcp.tielogic.xyz` (Deribit, Bybit, Hyperliquid), ccxt/Binance come fallback
|
|
||||||
- **Config:** pyyaml per `strategies.yml`
|
|
||||||
|
|
||||||
## Struttura
|
## Struttura (post-reset)
|
||||||
|
|
||||||
```
|
```
|
||||||
src/data/ → download e caricamento dati
|
src/data/downloader.py → load_data(asset, tf): legge i parquet certificati da data/raw/
|
||||||
downloader.py → download/caricamento parquet (gate: solo strumenti validati)
|
src/strategies/base.py → Strategy (ABC), Signal, BacktestResult, YearlyStats
|
||||||
instruments.py → discovery + validazione strumenti per exchange, registry
|
src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...)
|
||||||
|
src/strategies/trend_portfolio.py → TP01: strategia VINCENTE (PORT LF4h), causale, deployabile
|
||||||
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
|
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
|
||||||
src/backtest/ → engine di backtesting (engine.py)
|
src/backtest/engine.py → engine di backtesting riusabile
|
||||||
src/strategies/ → classe base Strategy ABC + indicatori condivisi
|
src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals no-leakage, OOS)
|
||||||
base.py → Strategy, Signal, BacktestResult, YearlyStats
|
src/version.py → APP_VERSION (legge il file VERSION)
|
||||||
indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation
|
scripts/research/ → ricerca post-reset: track{A-E}_*.py (trend/ML/MR/portfolio/xsec)
|
||||||
src/live/ → paper trading live multi-strategia
|
scripts/live/paper_trend.py → paper trader forward-only di TP01 (no esecuzione reale)
|
||||||
multi_runner.py → orchestratore: carica YAML (strategies + pairs), fetch candele, tick worker
|
scripts/analysis/ → SOLO i tool dati certificati:
|
||||||
strategy_worker.py → worker single-leg: capital, trade log, stato persistente.
|
rebuild_history.py → (ri)costruisce lo storico da Deribit mainnet (base 5m + resample)
|
||||||
Exit guidati da strategia (TP/SL/max_bars via Signal.metadata),
|
certify_feed.py → certifica il feed (integrità, coerenza resample, spike, cross-venue)
|
||||||
fallback hold_bars/stop -2%. Usa fee_rt della strategia.
|
audit_feed.py → audit per-barra vs riferimento esterno
|
||||||
pairs_worker.py → worker a 2 GAMBE per PR01 (market-neutral): long A / short B sullo
|
multi_source_check.py → cross-check multi-venue (quale venue è "vero")
|
||||||
z-score del log-ratio, exit |z|<=z_exit o max_bars, fee su 2 gambe.
|
data/raw/ → btc/eth × {5m,15m,1h} (gitignored). UNICO dato attivo.
|
||||||
strategy_loader.py → import dinamico classi Strategy da scripts/strategies/
|
data/instruments_registry.json → registry strumenti (reference)
|
||||||
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet): dati + ordini
|
docs/diary/ → diario di ricerca (1 voce: il reset; aggiungere dopo ogni esperimento)
|
||||||
(place_order market/reduce_only, get_trade_history, get_positions)
|
Old/ → ARCHIVIO: tutto il vecchio (strategie, live, ricerca, dati, diari)
|
||||||
execution.py → ExecutionClient: esecuzione REALE su Deribit (shadow). notional→
|
VERSION → semver (2.0.0)
|
||||||
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
|
|
||||||
telegram_notifier.py → notifiche Telegram per trade
|
|
||||||
src/portfolio/ → portafogli di prima classe (capitale-pool, backtest+live)
|
|
||||||
base.py → SleeveSpec, Portfolio (.backtest), load_active_portfolio
|
|
||||||
weighting.py → schemi pesi: equal/cap/inverse_vol/cluster_rp/manual
|
|
||||||
sleeves.py → builder unificato equity-per-sleeve (fonte unica, parità report)
|
|
||||||
ledger.py → PortfolioLedger: capitale/PnL/DD/persistenza+resume
|
|
||||||
runner.py → PortfolioRunner live (data Cerbero v2, sizing, ribilancio)
|
|
||||||
src/version.py → APP_VERSION (legge il file VERSION) — mostrato nei msg Telegram
|
|
||||||
src/strategies/fade_base.py → FadeStrategy + helper: atr, trend_distance, hurst_skip_mask (loss-guard),
|
|
||||||
exit close-confirm SL via param sl_confirm_atr (EXIT-16)
|
|
||||||
scripts/strategies/ → strategie con edge validato OOS: FADE (MR01/MR02/MR07),
|
|
||||||
HONEST (DIP01/TR01/ROT02), PAIRS (PR01), TSMOM + portafogli (PORT01/02/03);
|
|
||||||
FR01_hurst_calm_fade.py = record ricerca (robusto ma NON deployato)
|
|
||||||
scripts/portfolios/ → definizioni PORT01-06 (_defs.py) + report run() + hourly_report.py (Telegram)
|
|
||||||
scripts/waste/ → strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD)
|
|
||||||
scripts/analysis/ → ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...);
|
|
||||||
regime_fetcher.py + regime_lab.py (DVOL/funding/feature regime per la ricerca);
|
|
||||||
exit_lab.py + exit_policies/ (harness ricerca exit: cache segnali, train/OOS);
|
|
||||||
options_fetcher.py + options_chain.py (storico opzioni REALE da cerbero-bite);
|
|
||||||
option_overlay_lab.py (overlay opzioni prezzato; mr02eth_port06_gate.py = gate swap-sleeve)
|
|
||||||
scripts/bump_version.py, scripts/deploy.sh → versionamento e deploy (bump+commit+rebuild)
|
|
||||||
VERSION → versione semver (cotta nell'immagine, +1 ad ogni deploy)
|
|
||||||
strategies.yml → config multi-strategy paper trader
|
|
||||||
docs/diary/ → diario di ricerca giornaliero
|
|
||||||
docs/specs/ → specifiche di design
|
|
||||||
data/raw/ → file .parquet OHLCV (gitignored) | data/regime/ → DVOL+funding+feature (gitignored)
|
|
||||||
data/options/ → storico catena opzioni per-strike (bid/ask/IV/greche/OI) importato da cerbero-bite (gitignored)
|
|
||||||
data/instruments_registry.json → allowlist strumenti validati (gate del downloader)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Comandi
|
## Comandi
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync # installa dipendenze
|
uv sync # installa dipendenze
|
||||||
uv run python -m src.data.downloader # scarica dati storici (solo strumenti validati)
|
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # (ri)costruisci storico da Deribit mainnet
|
||||||
uv run python -m src.data.instruments # (ri)costruisci il registry strumenti validati
|
uv run python scripts/analysis/certify_feed.py # certifica i feed (locale + cross-venue)
|
||||||
uv run python scripts/strategies/MR01_bollinger_fade.py # backtest una strategia (es. fade)
|
uv run python scripts/analysis/certify_feed.py --local # solo check locali (veloce)
|
||||||
uv run python scripts/strategies/PR01_pairs_reversion.py # backtest pairs market-neutral
|
uv run python scripts/research/trackD_trendport.py # backtest strategia vincente (full report)
|
||||||
uv run python scripts/analysis/strategy_research.py # ricerca strategie fee-aware OOS
|
uv run python scripts/research/trackD_timing.py # vincitrice su 15m/1h/4h/1d + PnL/DD/trade per anno
|
||||||
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
|
uv run python scripts/live/paper_trend.py # avanza il paper trader TP01 (forward-only)
|
||||||
uv run python scripts/analysis/report_families.py # report per anno di tutte le famiglie
|
|
||||||
uv run python scripts/analysis/validate_worker_pairs.py # replay worker 2 gambe == backtest
|
|
||||||
uv run python -m src.live.multi_runner # paper trading live multi-strategia (strategie + pairs)
|
|
||||||
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 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/exit_lab.py # (ri)costruisci cache segnali exit-lab + parity check
|
|
||||||
./scripts/deploy.sh [patch|minor|major] # DEPLOY: bump versione + commit + rebuild Docker
|
|
||||||
uv run pytest # test
|
uv run pytest # test
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Deploy.** Il sorgente è **COPY nell'immagine Docker** (non montato) → `docker compose restart`
|
|
||||||
> NON ricarica il codice: serve **`docker compose up -d --build`** (o `./scripts/deploy.sh`, che bumpa
|
|
||||||
> la versione, committa e rebuilda). Il volume `data/` persiste → i worker fanno RESUME dello stato.
|
|
||||||
> La **versione** (file `VERSION`, semver, +1 ad ogni deploy via `deploy.sh`) compare nei messaggi
|
|
||||||
> Telegram (notifiche trade + report orario) → correli ogni msg al codice che l'ha generato.
|
|
||||||
|
|
||||||
## Dati storici
|
|
||||||
|
|
||||||
Scaricati e salvati localmente in Parquet. Per rigenerarli:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from src.data.downloader import download_all, load_data
|
from src.data.downloader import load_data
|
||||||
download_all() # scarica BTC + ETH su 5m/15m/1h dal 2018
|
df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError (guardrail: solo dati certi)
|
||||||
df = load_data("ETH", "15m") # carica un asset/timeframe
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Fonte primaria: Cerbero MCP (endpoint `/mcp-deribit/tools/get_historical`).
|
## IL DATO — fonte di verità (regola di prim'ordine)
|
||||||
Token observer: nel file `secrets/observer.token` del progetto CerberoSuite.
|
|
||||||
|
|
||||||
### Strumenti & validazione (gate raccolta dati)
|
- **La verità è Deribit mainnet**, perché è dove (in futuro) eseguiamo. Cross-check multi-venue:
|
||||||
|
Deribit mainnet è a 0-1 bps dal consenso. **Binance NON è la verità** (è USDT, ~10 bps fuori, e
|
||||||
|
sotto depeg USDT fino al 3% off) → usare Binance/Coinbase SOLO come audit indipendente, mai come
|
||||||
|
ancora per "ripulire" i dati.
|
||||||
|
- **Aggiornare lo storico SOLO con `rebuild_history.py`** (ccxt Deribit mainnet, base 5m unica +
|
||||||
|
resample → coerenza interna garantita). **MAI** il vecchio downloader Cerbero (token testnet =
|
||||||
|
feed farlocco: è la causa della contaminazione).
|
||||||
|
- **Certificare sempre** dopo un rebuild con `certify_feed.py` (integrità OHLC, zero gap, coerenza
|
||||||
|
resample maxΔ≈0, spike = solo crash reali, accordo cross-venue per-anno vs Coinbase USD).
|
||||||
|
|
||||||
`src/data/instruments.py` scopre e **valida** gli strumenti per ogni exchange
|
### Universo ricercabile certificato
|
||||||
implementato — **Deribit** e **Hyperliquid** (esclusi Alpaca/stocks e **Bybit**,
|
- **BTC / ETH**: puliti (2-6 bps vs Coinbase USD su tutta la storia), liquidi (~0% barre flat a 1h),
|
||||||
il cui feed testnet è farlocco). Per ogni perpetuo enumera via `get_instruments`
|
storia lunga (2018/2019→oggi) → **ogni timeframe (5m/15m/1h)**. È l'unico dato in `data/raw`.
|
||||||
/`get_markets` e verifica sui **dati storici realmente raccoglibili**:
|
- **Alt (SOL/XRP/ADA/LTC/DOGE/BNB): FUORI.** Illiquidi (LTC 5m 82% barre flat O=H=L=C, run fino a
|
||||||
esistenza, congruenza OHLC, not-flat (scarta contratti morti), liquidità (volume
|
~3 giorni), divergenti (LTC/DOGE >1% su 10-21% delle barre 2022-23), o non certificabili
|
||||||
daily) e **congruenza prezzo cross-exchange** (scostamento dalla mediana del
|
(XRP delistato da Coinbase per causa SEC; BNB non listato + storia da 2024-10). Sono archiviati in
|
||||||
base-coin ≤ 5% → scarta outlier come `SOL-PERPETUAL`=9.6 vs SOL reale ~82).
|
`Old/data/raw`. Riammetterne uno richiede prima una ricertificazione che dimostri liquidità + accordo.
|
||||||
|
|
||||||
Output: `data/instruments_registry.json` (strumenti validi, timeframe, start-date).
|
## Metodologia obbligatoria per ogni nuova strategia
|
||||||
**Gate:** `_download_cerbero_range` rifiuta gli strumenti non validati (override
|
|
||||||
`allow_unvalidated=True` solo per casi eccezionali). Rigenera con
|
|
||||||
`python -m src.data.instruments`.
|
|
||||||
|
|
||||||
> **NB testnet.** Il token Cerbero punta a testnet; la congruenza cross-exchange
|
1. **Ingresso eseguibile**: direzione e prezzo decisi con dati **fino a `close[i]`**, mai
|
||||||
> è il filtro che distingue i feed realistici (Deribit, Hyperliquid) da quelli
|
`close[i-1]` con direzione presa da `i`; mai entry sull'estremo (high/low) di una candela.
|
||||||
> farlocchi (Bybit). Simboli Deribit: BTC/ETH = `<COIN>-PERPETUAL` (inverse);
|
2. **Backtest NETTO** dopo fee realistiche Deribit (**0.10% RT** taker; maker ~0%) + leva.
|
||||||
> alt = `<COIN>_USDC-PERPETUAL` (lineari USDC). Registry attuale: Deribit 18/106,
|
3. **Out-of-sample** held-out + robustezza su **griglia parametri** (entrambi gli asset, tutte le
|
||||||
> Hyperliquid 66/74 validi (major liquidi: BTC dal 2018, alt dal 2022).
|
celle positive) + **sweep fee** (0.00-0.20% RT, margine ampio).
|
||||||
|
4. **Liquidità & plausibilità** (lezione v2.0.0): incrociare ogni edge con la liquidità reale del
|
||||||
|
book (quota di barre flat) e con la plausibilità del prezzo (cross-venue). Un edge full+OOS
|
||||||
|
robusto su un book fermo o su wick fantasma NON è un edge.
|
||||||
|
5. Strategia in `scripts/strategies/` (codice univoco), test in `tests/`, diario aggiornato.
|
||||||
|
|
||||||
## Strategie attive
|
## Lezioni critiche (da NON ripetere — la storia di questo progetto)
|
||||||
|
|
||||||
> **LEZIONE CRITICA (2026-05-28).** L'intera famiglia squeeze-breakout (SQ01-04,
|
- **Feed contaminato → libreria fasulla (v2.0.0).** Print fantasma testnet + Binance/USDT hanno
|
||||||
> MT01, ML01, AD01, CM01, PD01) è stata **scartata in `scripts/waste/`**: le
|
prodotto edge inesistenti (+201%/+1238%/+16492% "OOS"). Tutti spariti sul feed reale. Lezione: il
|
||||||
> accuratezze storiche 76-82% erano un **artefatto di look-ahead**. Quei backtest
|
dato viene prima della strategia; certificare sempre.
|
||||||
> decidono la direzione con `sign(close[i]-close[i-1])` (la candela di breakout `i`)
|
- **Look-ahead squeeze (storico).** L'intera famiglia squeeze-breakout aveva accuratezze 76-82% che
|
||||||
> ma entrano a `close[i-1]` — cioè comprano *prima* della candela che usano per
|
erano artefatto: decideva la direzione con la candela di breakout `i` ma entrava a `close[i-1]`.
|
||||||
> scegliere la direzione. Dal vivo il worker scopre il breakout solo a `close[i]`
|
Con ingresso onesto: lancio di moneta. (Dettagli nei diari in `Old/`.)
|
||||||
> ed entra lì: l'edge sparisce (win-rate ~47%, lancio di moneta). Sotto ingresso
|
- **Entry sugli estremi di candela.** Strategie che entrano a `close` quando `close` è all'estremo
|
||||||
> onesto e fee reali **tutte perdono, anche a fee zero**. Inoltre i breakout
|
del range (≤0.1% o ≥99.9%) gonfiano i ritorni in modo irrealistico (ETH 2024: +30.848% → +2.725%
|
||||||
> *rientrano* (mean-reversion > continuation). Vedi `scripts/analysis/oos_validation.py`
|
rimuovendoli). Spesso è un artefatto di dato o di entry non eseguibile.
|
||||||
> e `intrabar_test.py`.
|
- **Mean-reversion vs breakout.** Sui dati storici l'unica direzione che mostrava edge era la
|
||||||
|
mean-reversion (i breakout rientrano) — MA anche quegli edge erano per lo più artefatto del feed:
|
||||||
Tutte le strategie estendono `src.strategies.base.Strategy`
|
da riverificare da zero su dati certi.
|
||||||
(`generate_signals() → backtest()`). Le strategie mean-reversion condividono
|
- **Fee** = vincolo di prim'ordine. 0.10% RT baseline. Molte operazioni = morte per fee.
|
||||||
`src.strategies.fade_base.FadeStrategy` (backtest intrabar TP/SL/max_bars).
|
- **Leva**: testare 3x; 5x raddoppia il drawdown. I numeri a leva alta NON sono il caso base.
|
||||||
**Strategie con edge netto validato OOS fee-aware (tutte fade/mean-reversion):**
|
- **Data leakage** con rendimenti log: `returns[k] = log(close[k+1]/close[k])` usa `close[k+1]`. I
|
||||||
|
feature devono fermarsi a `returns[i-2]` se il prezzo corrente è `close[i-1]`. Verificare SEMPRE.
|
||||||
| Codice | Nome | Meccanismo | Edge OOS netto (1h, fee 0.10% RT) | DD | Note |
|
|
||||||
|--------|------|-----------|-----------------------------------|----|------|
|
|
||||||
| **MR01** | Bollinger Fade | banda std attorno a SMA | BTC +201% / ETH +1238% | 15-72% | Fada la banda, TP alla media, SL ad ATR |
|
|
||||||
| **MR02** | Donchian Fade | estremi canale H/L | BTC +172% / ETH enorme | 30-42% | Fada la rottura del canale, TP al centro |
|
|
||||||
| **MR07** | Return Reversal | z dei rendimenti di barra | BTC +105% / ETH +195% | 25-46% | Fada il movimento estremo, exit in ATR; esposizione ~8% |
|
|
||||||
|
|
||||||
> **MR03 Keltner Fade** spostata in `scripts/waste/`: era la fade più debole
|
|
||||||
> (BTC Sharpe 1.22, il filtro trend la peggiorava) e ridondante con MR01 (stessa
|
|
||||||
> idea di banda). Rimuoverla dal portafoglio ne ha *migliorato* le metriche.
|
|
||||||
> La funzione `keltner_fade` resta in `strategy_research_v2.py` come record.
|
|
||||||
|
|
||||||
**Lezione confermata:** l'edge è sempre *mean-reversion* (i breakout rientrano).
|
|
||||||
Il trend-following (Donchian trend, RSI cross) e gli oscillatori senza filtro
|
|
||||||
(RSI revert, ADX-filtered fade) perdono netti → restano scartati.
|
|
||||||
|
|
||||||
Ogni strategia è robusta su **tutta** la sua griglia parametri (entrambi gli asset
|
|
||||||
→ tutte positive OOS) e su **tutte** le fee 0.00-0.20% RT (margine ampio).
|
|
||||||
MR01 validato col worker reale: BTC +196% / ETH +251% OOS (nov 2023→mag 2026).
|
|
||||||
Ricerca completa: `scripts/analysis/strategy_research.py` (MR01) e
|
|
||||||
`scripts/analysis/strategy_research_v2.py` (MR02/MR03/MR07).
|
|
||||||
Validazione live-path: `scripts/analysis/oos_validation.py`.
|
|
||||||
|
|
||||||
**Filtro trend (riduzione DD + aumento Acc).** Tutte le fade accettano i parametri
|
|
||||||
opzionali `trend_max` / `ema_long`: saltano i segnali quando il prezzo è troppo
|
|
||||||
esteso rispetto al trend di fondo (`|close − EMA(ema_long)| / ATR(14) > trend_max`),
|
|
||||||
cioè quando si starebbe fadando un trend/crollo estremo. Con `trend_max=3.0`,
|
|
||||||
`ema_long=200` (default in `strategies.yml`): accuratezza su tutti gli sleeve
|
|
||||||
e DD giù drasticamente su ETH (MR01 71%→26%, MR02 42%→25%, MR03 66%→34%,
|
|
||||||
MR07 46%→21%), edge OOS confermato (vedi `scripts/analysis/risk_management.py`).
|
|
||||||
Unica eccezione: MR03 BTC, dove il filtro peggiora entrambe → lasciato disattivo.
|
|
||||||
Leva non robusta scartate: vol-target sizing e skip-alta-volatilità (peggiorano).
|
|
||||||
|
|
||||||
**SWAP filtri fade: hurst→trend (2026-06-07).** Il gate PORT06 sul path live
|
|
||||||
(`scripts/analysis/trendmax_port06_impact.py`, parità 1.00000 col canonico) ha mostrato che
|
|
||||||
**post-EXIT-16 il loss-guard Hurst è ridondante-dannoso**: EXIT-16 ha eliminato i wick-stop che
|
|
||||||
hurst evitava → gli ingressi saltati (66% delle barre!) sono in maggioranza tornati vincenti.
|
|
||||||
Su PORT06: LIVE hurst-only FULL Sh 7.23 / OOS 9.35-DD 1.68 vs **TREND-ONLY 7.89 / 9.91-DD 1.20**
|
|
||||||
(domina su tutte le metriche; hurst+trend insieme over-filtra: 7.11, metà dei trade; plateau
|
|
||||||
trend_max 2.5/3.0/3.5 robusto). TREND-ONLY è la config che la ricerca EXIT-16 aveva davvero
|
|
||||||
promosso (entries trend-filtrate, no hurst) e che il live non aveva mai eseguito. **Live ora:
|
|
||||||
`trend_max=3.0, ema_long=200` nelle 6 fade di `_defs.py`, `hurst_max` rimosso** (la maschera resta
|
|
||||||
in `fade_base`). Monitor: `hourly_report` traccia lo stop-rate per epoca PRE→HURST→TREND.
|
|
||||||
Lezione: ri-gateare ogni filtro quando cambia l'exit engine. Diario
|
|
||||||
`docs/diary/2026-06-07-trendmax-gate.md`. Il paragrafo sotto resta come record storico:
|
|
||||||
|
|
||||||
**Loss-guard Hurst (storico: live dal 2026-06-02 al 2026-06-07, poi sostituito dal filtro trend).** Le fade accettano `hurst_max`: saltano i segnali in
|
|
||||||
regime PERSISTENTE/trending (rolling-Hurst ≥ soglia), dove si concentrano stop-loss e perdite
|
|
||||||
(diagnosi: stop-rate 43% per hurst>0.55 vs 21% anti-persistente; i peggiori 1% trade hanno hurst
|
|
||||||
medio 0.61). Helper `src.strategies.fade_base.hurst_skip_mask` (rolling-Hurst causale **dalle sole
|
|
||||||
close** → nessun feed esterno; step=6 per velocità live). **`hurst_max=0.55` attivo sulle 6 fade in
|
|
||||||
`_defs.py`**: il test decisivo a livello PORT06 lo conferma — **FULL DD 4.10%→2.39% (quasi dimezzato),
|
|
||||||
Sharpe 6.62→6.76, OOS Sharpe 8.89→9.15**. È l'UNICO meccanismo anti-perdite che supera il gate (ADX,
|
|
||||||
vol-expansion/vratio, efficiency-ratio, time-stop, vol-target FALLISCONO: tagliano i winner insieme ai
|
|
||||||
loser; i claim esterni ADX/ATR-ratio non si replicano su queste fade crypto). NB: il filtro agisce solo
|
|
||||||
sul path LIVE (`spec.params`); il backtest canonico (`build_everything`/regression-lock) NON è filtrato
|
|
||||||
→ il live farà MEGLIO del backtest sul DD. Ricerca: `scripts/analysis/fade_lossguard_workflow.js`,
|
|
||||||
diagnosi `fade_loss_by_regime.py`, diario `docs/diary/2026-06-02-fade-lossguard.md`.
|
|
||||||
**Effetto misurato (backtest):** stop-loss fade −67% in numero (1881→621), perdite totali −68%, coda
|
|
||||||
−61%→−48% (lo stop-RATE per-trade scende poco, 42→38%: il guard lavora riducendo l'ESPOSIZIONE nel
|
|
||||||
regime tossico, non rendendo sicuro ogni trade). **Monitor live:** `hourly_report.py` traccia lo
|
|
||||||
stop-rate fade PRIMA/DOPO l'attivazione (14:34 UTC del 2026-06-02) e dà il verdetto su Telegram quando
|
|
||||||
il campione DOPO ≥30 (già confermato: stop-rate live PRIMA 42% == backtest 42.1%).
|
|
||||||
|
|
||||||
**FIX EXIT-16 live — confirm su barra COMPLETATA (2026-06-05).** Il worker valutava il confirm-SL
|
|
||||||
sul prezzo della candela IN FORMAZIONE ad ogni poll → reintroduceva la wick-sensitivity che EXIT-16
|
|
||||||
elimina (audit: 2 stop su 3 del crash ETH erano wick-stop che il backtest non avrebbe preso in quel
|
|
||||||
momento). Ora `tick` valuta il confirm SOLO sul close dell'ultima barra completata (detection: la
|
|
||||||
riga -1 del df è la candela in corso finché `now < ts[-1]+bar_ms`), buf dall'ATR della stessa barra;
|
|
||||||
fill al prezzo corrente (≈ stress lag_close_exit, OK in exit-lab); TP intrabar invariato. La concausa
|
|
||||||
feed-gap NON è mitigabile lato exit (fill reali ≈ sim) e l'entry-guard post-flat è BOCCIATA (skippare
|
|
||||||
i segnali dopo barre flat PEGGIORA tutti gli sleeve ETH: la candela-gap è l'overshoot che la fade
|
|
||||||
fada). Aggiunto alert Telegram `STALE_FEED` (≥2 barre 1h flat → notifica + gap % al risveglio, solo
|
|
||||||
osservabilità). Diario `docs/diary/2026-06-05-confirm-sl-forming-bar.md`.
|
|
||||||
|
|
||||||
**EXIT-16 close-confirm SL (ATTIVO LIVE, 2026-06-04; esteso a DIP01 il 2026-06-07).** Le fade E
|
|
||||||
DIP01 accettano `sl_confirm_atr` (live: 0.5 in `_defs.py`): lo SL **intrabar è disattivato** e lo stop scatta solo se il **CLOSE** della barra
|
|
||||||
sfonda `sl ∓ 0.5·ATR14`, con uscita al close (TP intrabar al livello e max_bars invariati; in modalità
|
|
||||||
confirm il TP ha priorità nel bar). Scoperta della ricerca exit-lab (34 agenti, 23 famiglie esplorate +
|
|
||||||
10 verifiche avversariali + test PORT06): **gli stop intrabar da wick sono falsi negativi** — l'overshoot
|
|
||||||
che buca lo stop e rientra è esattamente il movimento che la fade fada. Verificato: indipendente dal
|
|
||||||
loss-guard Hurst, plateau buffer 0.4-1.0, regge fee 2x/lag/slippage, coda ≈ base nei crash veri (FTX:
|
|
||||||
+2.4% vs −39% del no-SL puro). **PORT06: FULL Sharpe 6.47→7.84 DD 4.10→2.60, OOS Sharpe 8.82→10.06 DD
|
|
||||||
1.30→1.15.** La famiglia "cavalca il prezzo" (trailing/ride/partial-runner, 15 varianti) è invece tutta
|
|
||||||
SCARTATA: oltre il TP=media non c'è coda (4ª conferma). Collaterali: l'engine intrabar filla gli stop
|
|
||||||
"al livello" anche su gap-through (54% dei casi per stop tight) → bias PRO stop-stretti nelle ricerche
|
|
||||||
future; mai deployare strategie con `sl=0` (il fallback −2% del worker non si applicherebbe). Harness
|
|
||||||
riusabile `scripts/analysis/exit_lab.py` + policy in `exit_policies/`. Implementazione: `fade_base.backtest`
|
|
||||||
+ `StrategyWorker.tick` (param `sl_confirm_atr`, None = comportamento storico; il backtest canonico
|
|
||||||
`build_everything` resta NON filtrato → il live farà meglio del backtest, come per il loss-guard).
|
|
||||||
Diario `docs/diary/2026-06-04-exit-lab.md`.
|
|
||||||
|
|
||||||
**Portafoglio.** Diversificare su sotto-conti indipendenti equipesati (le 4 strategie
|
|
||||||
× BTC/ETH, pos 0.15 ciascuno) abbatte il DD aggregato: ~14% full / ~10% OOS sul
|
|
||||||
paniere di 8 sleeve, contro il 20-70% del singolo. È la vera leva anti-drawdown.
|
|
||||||
|
|
||||||
**Combinare le due famiglie (fade + honest).** Le fade (reversione intraday 1h) e le
|
|
||||||
honest (DIP/TR/ROT trend+rotazione multi-crypto) sono **quasi scorrelate**
|
|
||||||
(correlazione cross-famiglia ~0.05). Combinarle in un unico portafoglio migliora il
|
|
||||||
rischio/rendimento rispetto a ciascuna famiglia da sola: equal-weight dei 9 sleeve
|
|
||||||
→ DD 5.2% full / 4.7% OOS e Sharpe 4.23 full / 4.33 OOS (vs honest-only 12.6% DD /
|
|
||||||
2.20 Sharpe e fade-only 8.2% DD / 4.09 Sharpe), CAGR ~47% mantenuta. Studio in
|
|
||||||
`scripts/analysis/combine_portfolio.py`.
|
|
||||||
|
|
||||||
**ROT02 — riduzione DD (top_k 2→3).** La rotazione dual-momentum honest concentrava
|
|
||||||
il book su 2 soli asset (DD 40%). Diversificare su 3 (`top_k=3`) dimezza quasi il DD
|
|
||||||
(40%→26%) e *alza* pure il ritorno full (+1095%→+1303%, ret/DD da 27 a 50); il
|
|
||||||
vol-target abbassa il DD ma sacrifica ritorno, quindi si tiene top_k=3 senza VT.
|
|
||||||
Applicato a `ROT02_dual_momentum.py` e a `_rot_daily_equity` (usata dai portafogli).
|
|
||||||
|
|
||||||
**Portafogli pronti (artefatti accorpati e migliorati).** Oltre a `PORT01` (solo
|
|
||||||
honest), due script in `scripts/strategies/`:
|
|
||||||
- `PORT02_fade_master.py` — le 3 fade × BTC/ETH accorpate (6 sleeve, filtro trend),
|
|
||||||
equal-weight daily: DD ~8.2% full / 5.9% OOS, Sharpe 3.95/4.09, CAGR ~46%.
|
|
||||||
- `PORT03_all_master.py` — portafoglio MASTER (fade + honest, 9 sleeve). Due varianti:
|
|
||||||
`equal` (massimo Sharpe: DD 5.2%/4.7% OOS, Sharpe 4.23/4.33) e `5050` fra le due
|
|
||||||
famiglie (minimo DD: 5.0% full / 4.5% OOS). È la configurazione consigliata.
|
|
||||||
Come `PORT01`, sono meta-portafogli (script `run()` di report), non `Strategy` con
|
|
||||||
`generate_signals`, quindi non nel `strategy_loader`.
|
|
||||||
|
|
||||||
**Esplorazione famiglie alternative (branch `strategy_explore`, 2026-05-29).** Esplorate
|
|
||||||
9 famiglie nuove con agenti paralleli su harness onesto condiviso
|
|
||||||
(`scripts/analysis/explore_lab.py`). 7 sono rumore (rifiutate: stagionalità oraria/mensile,
|
|
||||||
cross-sectional reversal, opening-range breakout, lead-lag BTC→alt, continuation intraday —
|
|
||||||
quest'ultima riconferma la dominanza mean-reversion). Due edge reali:
|
|
||||||
- **PR01 Pairs** (`scripts/strategies/PR01_pairs_reversion.py`): spread reversion
|
|
||||||
market-neutral sul log-ratio z-score, **config UNIVERSALE** `n=50 z_in=2.0 z_exit=0.75
|
|
||||||
max_bars=72` (anti-overfit, niente tuning per-coppia). **5 coppie robuste**: ETH/BTC
|
|
||||||
(Sharpe 4.36), LTC/ETH (3.08), ADA/ETH (2.69), BTC/LTC (2.36, robusta anche 4h), ETH/SOL
|
|
||||||
(1.96, la più debole). Pattern: sempre alt-liquido vs major. Plateau confermato
|
|
||||||
(heatmap 20/20 Sharpe>1) + walk-forward (ETH/BTC 11/12 finestre+). **BNB/ETH scartata**
|
|
||||||
(overfit). Corr col mercato ~0.02-0.08. Fee su **2 gambe**: worker live implementato
|
|
||||||
(`src/live/pairs_worker.py`, sezione `pairs:` in `strategies.yml`). LOGICA validata
|
|
||||||
(`validate_worker_pairs.py`: replay == backtest ESATTO). LIVE (`live_smoke_pairs.py`,
|
|
||||||
smoke reale Cerbero): **tutte e 5 le coppie con feed live fresco**. Naming Deribit:
|
|
||||||
BTC/ETH = `<COIN>-PERPETUAL` (inverse); alt = `<COIN>_USDC-PERPETUAL` (lineari USDC,
|
|
||||||
storia dal 2022). Trappola: `LTC-PERPETUAL`/`SOL-PERPETUAL` danno vuoto/dati errati →
|
|
||||||
usare sempre `_USDC-PERPETUAL`. Resta da verificare solo liquidità/fill in esecuzione.
|
|
||||||
Verifica edge: `pairs_research.py`.
|
|
||||||
- **TSM01** (`scripts/analysis/tsmom_research.py`): TSMOM multi-orizzonte 3/6/12m + risk-off,
|
|
||||||
**gross 0.30**, distinto da ROT02 (corr 0.62), DD 15-22%, mai un anno negativo. Robusto
|
|
||||||
(36/36 config OOS+) ma diversificatore, non motore di ritorno (rende meno di ROT02).
|
|
||||||
|
|
||||||
Aggiungere i **5 pairs** al MASTER (quasi scorrelati, ~0.02-0.09) è il free-lunch più
|
|
||||||
grande (`scripts/analysis/combine_v2.py`). **Numeri sobri onesti** (l'OOS singolo 2024-25
|
|
||||||
è regime calmo → ottimistico ~50%): worst-DD su 90g rolling **~6%** (non 2.3%), Sharpe
|
|
||||||
atteso **~5** (mediana semestrale), ogni anno positivo dal 2021, regge **leva 2x +
|
|
||||||
slippage doppio** (CAGR 36%, Sharpe 5.1). Config robusta raccomandata: **MASTER-esteso
|
|
||||||
equal-weight, leva 2x, cap pairs ~30-35%** (i pairs sono ~57% del rischio; worker live a
|
|
||||||
2 gambe implementato, validato e con feed live su tutte e 5 le coppie — resta da
|
|
||||||
verificare liquidità/fill in esecuzione reale). La confluenza multi-TF è stata SCARTATA (overfit).
|
|
||||||
|
|
||||||
**Pattern del segnale per FORMA (branch `shape_patterns`, 2026-05-29).** Esplorate 5 famiglie
|
|
||||||
di *shape forecasting* con agenti paralleli su harness onesto (`scripts/analysis/shape_lab.py`:
|
|
||||||
analog kNN causale, no-look-ahead verificato). **4/5 sono RUMORE** (riconfermano la dominanza
|
|
||||||
mean-reversion): analog kNN sulla forma grezza (solo BTC-overfit), encoding candele
|
|
||||||
UP/DOWN/DOJI+body/shadow (hit-rate ~50%), DTW+template geometrici (DTW *peggiora* l'euclidea;
|
|
||||||
template overfit), PIP/pivot/zig-zag (0/48 robuste). Vedi `scripts/analysis/shape_*_research.py`.
|
|
||||||
- **SH01 Shape-ML** (`scripts/strategies/SH01_shape_ml.py`): UNICO edge. Una LogisticRegression
|
|
||||||
legge 17 feature di forma (body/shadow, rendimenti, pendenza/curvatura, pos max/min, RSI,
|
|
||||||
estensione) e predice il segno del rendimento a H barre in **walk-forward** (scaler+modello
|
|
||||||
solo sul passato, no leakage). Config **W24 H12 th0.58**. A differenza dello squeeze
|
|
||||||
**regge fee 0.20% RT**. Win-rate ~50% → l'edge è nell'**asimmetria**, non nella frequenza.
|
|
||||||
Validazione (`scripts/analysis/shape_ml_validate.py`): BTC robusto OVUNQUE (expanding +219%/
|
|
||||||
OOS +42% Sharpe 2.72 8-9 anni; rolling 2y +166%/+96%; stress 2x+slippage OK), ETH/ADA
|
|
||||||
robusti solo expanding (secondari), LTC/SOL/XRP scartati. Griglia: **5/27 celle robuste su
|
|
||||||
cresta stretta W24/H8-12** → overfit moderato, scelta la config conservativa. **Valore vero:
|
|
||||||
diversificatore** (corr +0.08 col MASTER); aggiungerlo migliora l'OOS del MASTER (Sharpe
|
|
||||||
4.33→5.10, DD 4.7%→4.2%). NON motore standalone. **LIVE (2026-06-01): gira come StrategyWorker
|
|
||||||
reale** (vedi fix wiring sotto in SCOPE LIVE). Diario: `docs/diary/2026-05-29-shape.md`.
|
|
||||||
|
|
||||||
**ARGO / GEX opzioni (analisi 2026-06-01, SCARTATO).** Valutato ARGO (lettura del gamma-exposure dei
|
|
||||||
dealer Deribit) come filtro di regime. Esito **NO-GO**: il net-GEX si calcola live (Deribit mainnet
|
|
||||||
public API, OI reale ~368k contratti, DVOL/funding storici gratis) ma **lo storico per-strike dell'OI
|
|
||||||
non è gratuito → non backtestabile OOS** (stesso muro delle opzioni W18/19/21). Niente evidenza crypto,
|
|
||||||
segno fragile, mercato dominato dai perp. Diario `docs/diary/2026-06-01-argo-gex-feasibility.md`.
|
|
||||||
|
|
||||||
**Storico opzioni REALE da cerbero-bite (2026-06-09, il muro ARGO è caduto per il periodo recente).**
|
|
||||||
Il container **cerbero-bite** (accanto, `/opt/docker/cerbero-bite`) accumula in continuo lo **storico
|
|
||||||
per-strike** della catena opzioni Deribit (BTC+ETH) nella tabella `option_chain_snapshots` del suo
|
|
||||||
SQLite (`bite-data:/app/data/state.sqlite`, root-only): bid/ask/mid/**IV per-strike**/greche/OI/volume,
|
|
||||||
**dal 2026-05-01**, cadenza ~12 min (~110k righe/asset). È esattamente il dato che ARGO/W18-21
|
|
||||||
credevano non-gratuito. `scripts/analysis/options_fetcher.py` lo importa via `docker exec` →
|
|
||||||
`data/options/{eth,btc}_chain.parquet`; `scripts/analysis/options_chain.py` (`OptionChain`) espone
|
|
||||||
loader + `skew_curve()` / `premium_levels()` (aggregati ROBUSTI) + `quote()` causale best-effort.
|
|
||||||
**Caveat granularità:** cerbero-bite snapshotta una **fetta rotante** (~1 scadenza per ciclo) → ottimo
|
|
||||||
per skew/premi **aggregati**, limitato per il pricing **per-trade** preciso (usare la finestra di
|
|
||||||
staleness). **Pannello regime `market_snapshots`** (anch'esso importato → `data/options/market_snapshots.parquet`,
|
|
||||||
loader `options_chain.load_market(asset)`; merge causale su prezzo via `options_chain.attach_market(df, asset)`):
|
|
||||||
feature REALI pre-calcolate — **spot, dvol, realized_vol_30d, iv_minus_rv (VRP), funding perp/cross,
|
|
||||||
`dealer_net_gamma` (net-GEX dealer!), `gamma_flip_level`, oi_delta_pct_4h, liquidation_long/short_risk**.
|
|
||||||
È il segnale ARGO/GEX che il progetto dichiarò non-backtestabile: ora accumula, reale e allineato (lo
|
|
||||||
`spot` elimina il proxy ATM). **Copertura reale:** spot/dvol dal 2026-03-26 (sparse/daily fino a fine
|
|
||||||
aprile), **net-GEX denso orario solo da ~2026-05-01** → ~5-6 settimane, **un singolo regime calmo**:
|
|
||||||
analisi esplorativa OK ma NIENTE edge validabile ora (i prior GEX/VRP non si replicano su 9 sett.;
|
|
||||||
`liquidation_risk` costante 'low'). Valore FORWARD: rivalutare quando il pannello attraversa
|
|
||||||
gamma-flip/liquidazioni/crash con held-out. **NB look-ahead:** usare `attach_market` (merge_asof
|
|
||||||
causale, NaN prima della copertura), MAI `astype(int64)` su un timestamp datetime (darebbe ns →
|
|
||||||
match all'ultimo snapshot = leak). **Numeri reali misurati (ETH):** skew put 10% OTM = **×1.1** (liquido, spread ~7%, NON ×1.6
|
|
||||||
come sembrava da un singolo snapshot a 1g, illiquido); premio reale put 10% OTM ≈ **1.0%/mese** (0.65%/sett),
|
|
||||||
catastrofe 15%+ OTM ≈ 0.33%/mese. **Vincolo strutturale:** gli strike **10% OTM a 24h NON esistono**
|
|
||||||
(Deribit lista solo near-ATM sui tenor corti) → un overlay **per-trade a 24h è infattibile**; l'unica
|
|
||||||
struttura eseguibile è una **put settimanale/mensile standing** (catastrofe-cap di sleeve), da gateare
|
|
||||||
coi premi reali. Vedi `docs/diary/2026-06-08-mr02eth-replace-search.md`.
|
|
||||||
|
|
||||||
**Frattali del segnale × Regime ARGO (ricerca 100 agenti, 2026-06-02, RECORD).** Cercata una strategia
|
|
||||||
che combini un segnale frattale (Hurst/Higuchi/Williams/analog) con un gate di regime (DVOL/VRP/funding).
|
|
||||||
Infrastruttura riusabile: `scripts/analysis/regime_fetcher.py` (DVOL+funding da Deribit mainnet →
|
|
||||||
**`data/regime/`**, NON `data/raw/` che è solo OHLCV) e `regime_lab.py` (feature regime+frattali causali,
|
|
||||||
cache, harness netto-OOS). Esito: **15 strategie robuste e causali, ma NESSUNA batte/migliora il PORT06**
|
|
||||||
(diversificatori sovrapposti alle fade). **Finding: il prior ARGO "VRP>0=range=fade" è SMENTITO** —
|
|
||||||
l'edge è su **VRP<0 + DVOL bassa**. Il vincitore `FR01_hurst_calm_fade.py` è robusto ma DILUISCE il
|
|
||||||
PORT06 (OOS Sharpe 8.89→8.72) → **non deployato** (in `scripts/strategies/` ma NON in MODULE_MAP/yml).
|
|
||||||
Il sottoprodotto utile è stato il **loss-guard Hurst** (vedi sopra), che invece MIGLIORA il PORT06.
|
|
||||||
Diario `docs/diary/2026-06-02-fractal-argo-search.md`.
|
|
||||||
|
|
||||||
**Metodologia obbligatoria per ogni nuova strategia** (per non ripetere l'errore squeeze):
|
|
||||||
1. Ingresso eseguibile: direzione e prezzo decisi con dati **fino a `close[i]`**, mai `close[i-1]` con direzione da `i`.
|
|
||||||
2. Backtest **NETTO** dopo fee realistiche Deribit (**0.10% RT** taker, non 0.20%) + leva.
|
|
||||||
3. Validazione **out-of-sample** (held-out) + robustezza su griglia parametri + sweep fee.
|
|
||||||
4. Crea script in `scripts/strategies/`, aggiungi a `MODULE_MAP` (`strategy_loader.py`) e a `strategies.yml`.
|
|
||||||
|
|
||||||
Strategie scartate storiche in `scripts/waste/` (W01-W28 + la famiglia squeeze).
|
|
||||||
|
|
||||||
**Verso €50/giorno.** Con 4 strategie indipendenti (MR01/MR02/MR03/MR07) × 2 asset
|
|
||||||
(BTC/ETH) su €1000 ciascuna, il PnL medio storico aggregato è ben oltre €50/giorno;
|
|
||||||
ma quei numeri sono backtest a leva 3x su 8 anni e includono anni eccezionali (es.
|
|
||||||
ETH 2024). Stima onesta: il target è *plausibile* su un portafoglio diversificato di
|
|
||||||
queste fade, ma va confermato col paper trader live prima di rischiare capitale reale.
|
|
||||||
|
|
||||||
## Portafogli
|
|
||||||
|
|
||||||
- Un `Portfolio` è un oggetto di prima classe (`src/portfolio/`) con definizione (sleeve + schema pesi) e due facce sulla **STESSA** definizione: `.backtest()` (riusa il builder unico di `sleeves.py` → parità esatta con `report_families`) e live (`PortfolioRunner`: capitale pool condiviso, sizing per peso, ribilancio giornaliero, ledger aggregato in `data/portfolios/{code}/`).
|
|
||||||
- **Schemi peso:** `equal` (default), `cap` (tetto per famiglia, es. pairs 33% — config raccomandata), `inverse_vol`, `cluster_rp` (equal fra cluster naturali poi inverse-vol intra-cluster), `manual`. Definiti in `weighting.py`; la chiave cap è la famiglia (PAIRS/FADE/HONEST/SHAPE/TSM).
|
|
||||||
- **Default `portfolios.yml`:** PORT06 (master+shape), `weighting=cap pairs 0.33 + shape 0.0588`, leva 2x, ribilancio 1D. Backtest PORT06 canonico (dati al 2026-05-28, pre-cap-shape): FULL Sharpe 6.47 DD 4.10% / OOS Sharpe 8.82 DD 1.30%; **con EXIT-16 close-confirm (config live attuale): FULL 7.84 / 2.60%, OOS 10.06 / 1.15%** (i vecchi 6.07/8.19 erano pre-loss-guard/pre-refresh dati). Col cap SHAPE (2026-06-05): FULL 6.43 / 3.96%, OOS 8.58 / 1.36% — assicurazione sulla coda SH01, vedi sotto.
|
|
||||||
- **SH01 SENZA STOP-LOSS — by design, CONFERMATO da ricerca (2026-06-05).** Dopo il crash ETH (−15.6% su un trade SH01 live), ricerca multi-agente con harness dedicato `scripts/analysis/sh01_exit_lab.py` (cache segnali walk-forward, engine con **fill gap-aware** `worse(livello, open)`, parity esatta con explore_lab, protocollo train≤2023-11-01/OOS): **11 famiglie di stop testate (ATR intrabar/close-confirm, %, chandelier, breakeven, giveback, loser-timestop, disaster-cap close+intrabar, swing, vol-regime), 0 sopravvissute** al gate (ETH migliorato senza degradare BTC, train E oos, plateau). Pattern: ogni stop stretto abbastanza da toccare la coda ETH rompe BTC; ogni stop largo non arriva alla coda; nei crash il fill è al gap, non al livello (lo stop "protettivo" PEGGIORA la coda OOS). Mitigazione adottata: **cap famiglia SHAPE a 0.0588 in PORT06** (≈ dimezzata; costo OOS Sharpe −0.24, FULL DD −0.14pp) — la prossima coda impatta il conto per metà. NON impostare mai `sl`/`sl_confirm_atr` su SH_BTC/SH_ETH. Direzione futura: liquidity-gate sull'entry (skip dopo feed flat). Diario `docs/diary/2026-06-05-sh01-sl-research.md`.
|
|
||||||
- **Data layer Cerbero v2:** `get_historical_v2` unificato + `get_instruments` (naming robusto) + `get_ticker_batch`. Trading su Deribit.
|
|
||||||
- **SCOPE LIVE (fase 2 completata):** il runner esegue TUTTI gli sleeve di PORT06. Worker: single `StrategyWorker` (fade MR01/02/07, DIP01, **e SH01**), `PairsWorker` (PR01 2 gambe), e i multi-asset dedicati `BasketTrendWorker` (TR01 4h), `RotationWorker` (ROT02 1d), `TsmomWorker` (TSM01 1d). Il runner fetcha 1h da Cerbero v2 e **resampla a 4h/1d** (lookback dimensionato sui daily: TSM01 usa 252g). Validazione: runner pool/ribilancio/ledger == backtest (`validate_portfolio_runner.py`, identico); worker multi-asset == reference (`validate_honest_workers.py`: TSM01 esatto, ROT02 +1303% canonico, TR01 stesso ordine — differenza di convenzione capitale-unico vs media-equity).
|
|
||||||
- **SH01 bootstrap full-history (punto-10, 2026-06-07).** La ri-validazione col train-window del
|
|
||||||
regime live (`sh01_trainwindow_validate.py`) ha mostrato che SH01 a train 365g **NON è robusto**
|
|
||||||
(BTC fee-2x FULL −42%, ETH Sharpe −0.02, trade-rate 22-26% vs 10% validato: LogReg over-confident,
|
|
||||||
soglia inerte — la diagnosi sweep era esatta). L'edge è MONOTONO nella memoria: solo l'expanding
|
|
||||||
full-history passa il gate. Fix live: il runner passa agli sleeve `ml` la storia FULL
|
|
||||||
(`_with_history`: parquet locale + feed, gap-guard con WARN) e `ml_wf_entries(last_block_only=True)`
|
|
||||||
fitta SOLO l'ultimo blocco del walk-forward → segnali **identici per costruzione** al WF completo
|
|
||||||
(parity test esatto), 0.6 s/tick su 73k barre. Manutenzione: tenere fresco il parquet
|
|
||||||
(`download_all()`). MAI ri-tunare la soglia nel regime corto (instabile/incoerente fra asset).
|
|
||||||
Diario `docs/diary/2026-06-07-sh01-trainwindow.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).
|
|
||||||
- **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,
|
|
||||||
`ExecutionClient.place_disaster_sl`, cancellato in `_real_close`): assicurazione per gli outage
|
|
||||||
(poll-loop fermo = exit non valutati), in operatività normale non scatta mai → 0 costo Sharpe.
|
|
||||||
Config `overrides.execution.disaster_sl_pct` (0.30; 0=off). NB: il `set_stop_loss` di cerbero-mcp
|
|
||||||
è un `private/edit` (solo ordini APERTI) → inutilizzabile su market fillati; la cancel di un
|
|
||||||
trigger order risponde `untriggered` (= successo, verificato testnet). Alert Telegram `FEED_OUTAGE`
|
|
||||||
dopo 5 poll falliti consecutivi (con elenco posizioni reali aperte) + notifica di ripresa. I fix
|
|
||||||
di parità della stessa tornata (v1.1.3): TR01 fee×leva + forming-bar TR01/Pairs + WARN
|
|
||||||
`PANEL_SHORT` su TSM01/ROT02; `hourly_report` ora mostra i multi-asset (sezione MULTI-ASSET).
|
|
||||||
Diario `docs/diary/2026-06-07-sweep-fixes.md`.
|
|
||||||
- **TP reale = LIMIT reduce-only AL LIVELLO (2026-06-04).** Misurati +235 bps di slippage medio sulle uscite take-profit market-on-poll (sim esce al livello intrabar, il reale chiudeva al poll post-rimbalzo: sim +11.85 vs reale +0.62 USD sui primi 7 close). Fix: a ogni `REAL_OPEN` il worker piazza un **limit reduce-only al TP** (`ExecutionClient.place_tp_limit`, prezzo quantizzato al tick, SOLA quota del worker) → `REAL_TP_RESTING`; a ogni chiusura sim `_real_close` **cancella il resting → riconcilia i fill (anche parziali) via `get_trade_history` per order_id → market reduce-only solo del residuo** → ledger su prezzo combinato. `real_tp_order_id` persistito in `status.json` (resume-safe). Lo **SL resta market-on-poll** (deliberato: i trigger Deribit generano un nuovo order_id al trigger → fill non verificabile per order_id; e sul SL il rimbalzo lavora a favore). Fill da resting = fee **maker ~0%**. Smoke: `live_shadow_smoke.py` (2 scenari, testnet). Diario `docs/diary/2026-06-04-shadow-divergence.md`.
|
|
||||||
- **position_size per-famiglia (2026-06-07).** `portfolios.yml` accetta `position_size_family`
|
|
||||||
(chiave = `weighting.family_of`); plumbing `runner.pos_for_spec`. **PAIRS a 0.20** (esposizione
|
|
||||||
0.40 ≈ il validato 0.45): la famiglia è senza stop e col globale 0.5×lev2 girava a ~2.2x il
|
|
||||||
validato (ETH/BTC DD grezzo 78% a quella taglia). PORT06 OOS DD 3.40→1.26% al costo di OOS
|
|
||||||
Sharpe 9.05→8.43 — assicurazione come il cap SHAPE. Gate `pairspos_port06_impact.py`,
|
|
||||||
diario `docs/diary/2026-06-07-pairspos-gate.md`.
|
|
||||||
- **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
|
|
||||||
|
|
||||||
Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna con €1000 USDC virtuali indipendenti.
|
|
||||||
|
|
||||||
**Config:** `strategies.yml` — due sezioni: `strategies` (single-leg: fade/honest) e
|
|
||||||
`pairs` (a 2 gambe). Attive: 6 fade (MR01/MR02/MR07 × BTC/ETH) + 5 coppie PR01.
|
|
||||||
**Due worker:** `strategy_worker.py` (single-leg) e `pairs_worker.py` (2 gambe,
|
|
||||||
long A / short B sullo z-score del log-ratio, fee su 2 gambe).
|
|
||||||
**Persistenza:** `data/paper_trades/{worker_id}/` con `trades.jsonl` (append-only) + `status.json` (resume al restart).
|
|
||||||
**Hot-add:** aggiungi riga YAML → `docker compose restart` → storico intatto.
|
|
||||||
**Exit strategia:** se un `Signal` porta `tp`/`sl`/`max_bars` in `metadata` (come le fade), il worker esce su take-profit/stop-loss/time-limit; i pairs escono su |z|≤z_exit o max_bars.
|
|
||||||
**Naming Deribit (feed live):** major = `<COIN>-PERPETUAL` (inverse); alt = `<COIN>_USDC-PERPETUAL` (lineari USDC). Vedi INSTRUMENT_MAP in `multi_runner.py`.
|
|
||||||
**Notifiche:** Telegram per ogni trade (richiede `.env` con `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID`).
|
|
||||||
|
|
||||||
## Convenzioni
|
## Convenzioni
|
||||||
|
|
||||||
- Strategie in `scripts/strategies/` con codice univoco (MR01, ...).
|
- Strategie in `scripts/strategies/` con codice univoco; scartate documentate nel diario.
|
||||||
- Script scartati in `scripts/waste/` (W01-W28 + famiglia squeeze).
|
- Diario in `docs/diary/YYYY-MM-DD.md`, aggiornato dopo ogni esperimento significativo.
|
||||||
- Diario in `docs/diary/YYYY-MM-DD.md`. Aggiornare dopo ogni esperimento significativo.
|
- **Nessun segreto nei commit** (token/chiavi). `.env` e `.env.mainnet` sono gitignored.
|
||||||
- Nessun dato sensibile nei commit (token, chiavi API). Usare `.gitignore`.
|
- Versionamento: `VERSION` (semver) + `scripts/bump_version.py`. `src/version.py` lo legge.
|
||||||
- Verificare sempre assenza di data leakage prima di fidarsi dei risultati. In particolare: `returns[i-w : i]` include `close[i]` che è un candle nel futuro — usare `returns[i-w : i-1]`.
|
|
||||||
|
|
||||||
## Attenzione
|
## Archivio `Old/`
|
||||||
|
|
||||||
- **Data leakage:** è stata trovata e corretta nello script 05. Ogni volta che si usano rendimenti logaritmici (`np.diff(np.log(close))`), ricordare che `returns[k]` usa `close[k+1]`. I feature devono fermarsi a `returns[i-2]` se il prezzo corrente è `close[i-1]`.
|
Tutto il lavoro pre-reset (preservato in git per consultazione storica): strategie
|
||||||
- **Fee:** Deribit perp reale = taker ~0.05%/lato (**0.10% round-trip**), maker ~0%. Usare 0.10% RT come baseline (lo 0.20% storico era pessimista 2x). Includere SEMPRE nel backtest: sono vincolo di prim'ordine, molte operazioni = morte per fee. Il worker usa `strategy.fee_rt` (MR01 = 0.001).
|
(`Old/scripts/strategies`), stack live e portafogli (`Old/src/live`, `Old/src/portfolio`,
|
||||||
- **Leva:** testato con 3x. Aumentare a 5x migliora i rendimenti ma raddoppia il drawdown.
|
`Old/scripts/portfolios`), ricerca/gate (`Old/scripts/analysis`), dati non certificati
|
||||||
- **GBM:** GradientBoostingClassifier di scikit-learn. Ensemble di alberi decisionali sequenziali. Walk-forward per evitare leakage temporale.
|
(`Old/data`), 60+ diari (`Old/docs/diary`), test (`Old/tests`). Consultabile come riferimento
|
||||||
- **Cerbero `get_historical` (fix 2026-05-28):** `end_date` come data nuda è inclusivo dell'intera giornata fino all'ultima candela chiusa (es. `end=oggi` arriva fino ad ora, non più a mezzanotte); accettati anche timestamp con orario (`...T14:00:00`, naive=UTC); nessun cap a ~5000 righe (paginazione interna). Il client passa già `end=oggi`, ora corretto. Prima del fix il paper trader restava a zero trade perché il feed era fermo a mezzanotte.
|
("come facevamo X"), ma **nessun edge lì dentro è fidato** finché non è ri-validato su dati certi.
|
||||||
- **Dati ETH Deribit 15m:** 14-30%/anno di candele *flat* (O=H=L=C, volume 0, run fino a ~54h) per bassa liquidità del perpetuo. Verificato (2026-05-28): escluderle NON cambia i backtest (Δacc ≤0.5pp) → edge robusto. Resta un caveat operativo (slippage/fill in trading reale, irrilevante per paper). BTC pulito eccetto picco ~8% nel 2024.
|
```
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Strategia di Grid Trading — Versione Corretta
|
||||||
|
|
||||||
|
> Documento di specifica della strategia. Descrive *cosa* deve fare il bot e *perché*,
|
||||||
|
> non l'implementazione. È il riferimento da cui partire per riscrivere il codice in
|
||||||
|
> modo sicuro e testabile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Obiettivo
|
||||||
|
|
||||||
|
Estrarre profitto dalla **volatilità di un asset all'interno di un intervallo di prezzo
|
||||||
|
(range)**, comprando automaticamente quando il prezzo scende e vendendo quando risale,
|
||||||
|
secondo livelli predefiniti (la "griglia").
|
||||||
|
|
||||||
|
La griglia **non prevede** la direzione del mercato: monetizza le oscillazioni. Funziona
|
||||||
|
quando il prezzo oscilla; perde quando il prezzo prende un trend deciso. Tutta la
|
||||||
|
progettazione che segue serve a massimizzare il primo caso e a limitare i danni nel
|
||||||
|
secondo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Principi corretti (cosa cambia rispetto al bot originale)
|
||||||
|
|
||||||
|
| Aspetto | Bot originale (sbagliato) | Versione corretta |
|
||||||
|
|---|---|---|
|
||||||
|
| Asset | Shitcoin illiquida (LAND) | Coppia liquida (es. ETH/USDT, BNB/USDT) |
|
||||||
|
| Passo griglia | Assoluto in USDT (`GRID_STEP=3`) | **Percentuale** sul prezzo |
|
||||||
|
| Livelli | Mobili, inseguono il prezzo | **Fissi** dentro un range definito |
|
||||||
|
| Protezione perdite | Nessuna | **Stop-loss** sotto il range + **take-profit** sopra |
|
||||||
|
| Slippage | `amountOutMin = 0` (nessuna) | Calcolato da `getAmountsOut` − tolleranza |
|
||||||
|
| Break-even fee | Ignorato | Passo griglia **> costo round-trip** |
|
||||||
|
| Capitale | Tutto, senza limiti | Allocazione fissa, suddivisa per livello |
|
||||||
|
| Chiave privata | In chiaro nel `.env` | Keystore cifrato o input a runtime |
|
||||||
|
| Validazione | Nessuna | **Backtest** + **testnet** prima del capitale reale |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Definizione della griglia
|
||||||
|
|
||||||
|
### 3.1 Parametri di ingresso
|
||||||
|
|
||||||
|
| Parametro | Significato | Esempio |
|
||||||
|
|---|---|---|
|
||||||
|
| `PAIR` | Coppia da tradare (base/quote) | `ETH/USDT` |
|
||||||
|
| `RANGE_LOW` | Estremo inferiore del range | `2800` |
|
||||||
|
| `RANGE_HIGH` | Estremo superiore del range | `3400` |
|
||||||
|
| `GRID_LEVELS` | Numero di livelli nella griglia | `12` |
|
||||||
|
| `CAPITAL_QUOTE` | Capitale totale in quote (USDT) | `1200` |
|
||||||
|
| `STOP_LOSS` | Prezzo sotto cui il bot chiude tutto e si ferma | `2650` |
|
||||||
|
| `TAKE_PROFIT` | Prezzo sopra cui il bot chiude tutto e si ferma | `3550` |
|
||||||
|
| `SLIPPAGE_BPS` | Tolleranza slippage in basis point (50 = 0,5%) | `50` |
|
||||||
|
| `FEE_BPS` | Fee del DEX in basis point (PancakeSwap = 25) | `25` |
|
||||||
|
|
||||||
|
### 3.2 Costruzione dei livelli (griglia geometrica)
|
||||||
|
|
||||||
|
I livelli vanno spaziati in **percentuale**, non in valore assoluto. Una griglia
|
||||||
|
geometrica mantiene lo stesso rendimento percentuale per ogni gradino, indipendentemente
|
||||||
|
dal prezzo.
|
||||||
|
|
||||||
|
```
|
||||||
|
ratio = (RANGE_HIGH / RANGE_LOW) ^ (1 / GRID_LEVELS)
|
||||||
|
livello[i] = RANGE_LOW * ratio ^ i per i = 0 .. GRID_LEVELS
|
||||||
|
```
|
||||||
|
|
||||||
|
Esempio (`RANGE_LOW=2800`, `RANGE_HIGH=3400`, `GRID_LEVELS=12`):
|
||||||
|
`ratio ≈ 1,0163` → passo di circa **1,63% per gradino**.
|
||||||
|
|
||||||
|
### 3.3 Capitale per livello
|
||||||
|
|
||||||
|
```
|
||||||
|
quote_per_livello = CAPITAL_QUOTE / GRID_LEVELS
|
||||||
|
```
|
||||||
|
|
||||||
|
Ogni livello di acquisto impegna `quote_per_livello`. Il capitale è suddiviso in anticipo:
|
||||||
|
il bot **non** può comprare più di quanto allocato, e non scende mai sotto zero.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Vincolo di break-even (regola anti-fee)
|
||||||
|
|
||||||
|
Una griglia con passi troppo fitti perde: le fee di ogni round-trip (compra + vendi) si
|
||||||
|
mangiano il profitto. **Il passo percentuale della griglia deve superare il costo totale
|
||||||
|
di un round-trip.**
|
||||||
|
|
||||||
|
```
|
||||||
|
costo_round_trip ≈ 2 * (FEE_BPS + SLIPPAGE_BPS) / 10000 (in frazione)
|
||||||
|
passo_griglia = ratio - 1
|
||||||
|
|
||||||
|
VINCOLO: passo_griglia > costo_round_trip * margine_sicurezza (margine ≥ 1,5)
|
||||||
|
```
|
||||||
|
|
||||||
|
Esempio con `FEE_BPS=25`, `SLIPPAGE_BPS=50`:
|
||||||
|
`costo_round_trip ≈ 2 * (25+50)/10000 = 1,5%`. Con margine 1,5 → il passo deve essere
|
||||||
|
**≥ 2,25%**. Se la griglia geometrica dà 1,63%, **è troppo fitta**: vanno ridotti i
|
||||||
|
`GRID_LEVELS` o allargato il range finché il vincolo è rispettato. Il bot deve rifiutarsi
|
||||||
|
di partire se il vincolo non è soddisfatto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Logica operativa
|
||||||
|
|
||||||
|
### 5.1 Inizializzazione
|
||||||
|
1. Validare i parametri: `RANGE_LOW < prezzo_attuale < RANGE_HIGH`, vincolo di break-even
|
||||||
|
rispettato, capitale disponibile sul wallet.
|
||||||
|
2. Verificare la coppia: liquidità sufficiente, contratto non honeypot (controllo
|
||||||
|
obbligatorio, non opzionale), token con decimali noti.
|
||||||
|
3. Costruire i livelli (§3.2) e marcare ognuno come `attivo`/`riempito`.
|
||||||
|
4. Allocare il capitale (§3.3).
|
||||||
|
|
||||||
|
### 5.2 Ciclo principale
|
||||||
|
A ogni tick (es. ogni nuovo blocco, o ogni N secondi):
|
||||||
|
|
||||||
|
```
|
||||||
|
prezzo = prezzo_corrente(PAIR)
|
||||||
|
|
||||||
|
# --- guardie di uscita: hanno priorità su tutto ---
|
||||||
|
SE prezzo <= STOP_LOSS:
|
||||||
|
vendi_tutta_la_posizione() # con slippage protetto
|
||||||
|
ferma il bot, log "STOP-LOSS"
|
||||||
|
SE prezzo >= TAKE_PROFIT:
|
||||||
|
vendi_tutta_la_posizione()
|
||||||
|
ferma il bot, log "TAKE-PROFIT"
|
||||||
|
|
||||||
|
# --- logica griglia ---
|
||||||
|
PER ogni livello L:
|
||||||
|
SE prezzo attraversa L verso il basso E L non è ancora riempito:
|
||||||
|
compra quote_per_livello # con amountOutMin protetto
|
||||||
|
marca L come riempito
|
||||||
|
SE prezzo attraversa L verso l'alto E il livello sotto è riempito:
|
||||||
|
vendi la quantità di quel livello # con amountOutMin protetto
|
||||||
|
marca quel livello come libero
|
||||||
|
```
|
||||||
|
|
||||||
|
I livelli sono **fissi** (calcolati una volta), non inseguono il prezzo. Questo rende il
|
||||||
|
comportamento prevedibile e backtestabile.
|
||||||
|
|
||||||
|
### 5.3 Calcolo `amountOutMin` (protezione slippage — obbligatoria)
|
||||||
|
|
||||||
|
Mai passare `0`. Prima di ogni swap:
|
||||||
|
|
||||||
|
```
|
||||||
|
atteso = router.getAmountsOut(amountIn, path)[ultimo]
|
||||||
|
amountOutMin = atteso * (10000 - SLIPPAGE_BPS) / 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
Se la transazione non rientra nella tolleranza, deve **fallire** (revert), non eseguire a
|
||||||
|
qualsiasi prezzo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Gestione del rischio
|
||||||
|
|
||||||
|
1. **Stop-loss obbligatorio** sotto `RANGE_LOW`. È la differenza tra "strategia" e
|
||||||
|
"gambling": senza stop-loss un trend ribassista svuota il wallet.
|
||||||
|
2. **Take-profit** sopra `RANGE_HIGH` per chiudere quando il prezzo esce dal range al
|
||||||
|
rialzo (la griglia avrebbe già venduto tutto; il take-profit evita di restare esposti).
|
||||||
|
3. **Capitale segregato**: usare un wallet dedicato, con solo il capitale destinato alla
|
||||||
|
strategia. Mai il wallet principale.
|
||||||
|
4. **Limite di gas e prezzo gas** ragionevoli, ricalcolati dinamicamente (no valori fissi
|
||||||
|
obsoleti).
|
||||||
|
5. **Kill-switch manuale**: comando per fermare il bot e liquidare in qualsiasi momento.
|
||||||
|
6. **Idempotenza/recupero**: se il bot si riavvia, deve ricostruire lo stato dei livelli
|
||||||
|
(riempiti/liberi) dal saldo on-chain, non ripartire da zero.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Validazione prima del capitale reale
|
||||||
|
|
||||||
|
Nessun fondo reale prima di aver superato, in ordine:
|
||||||
|
|
||||||
|
1. **Backtest** su dati storici della coppia (almeno alcuni mesi, includendo sia fasi
|
||||||
|
laterali sia un trend marcato), misurando: PnL netto **dopo** fee e slippage,
|
||||||
|
max drawdown, numero di trade, comportamento allo stop-loss.
|
||||||
|
2. **Paper trading / simulazione** in tempo reale, senza eseguire ordini veri.
|
||||||
|
3. **Testnet** (BSC testnet) con la stessa logica e router di test, per verificare
|
||||||
|
l'esecuzione on-chain end-to-end.
|
||||||
|
4. **Mainnet con capitale minimo** (es. l'equivalente di pochi euro) per la prima
|
||||||
|
settimana, poi scalare solo se i risultati combaciano col backtest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Quando NON usare questa strategia
|
||||||
|
|
||||||
|
- Asset illiquido o a rischio rug-pull/honeypot.
|
||||||
|
- Mercato in trend forte e prolungato (la griglia perde: lo stop-loss limita il danno ma
|
||||||
|
non genera profitto).
|
||||||
|
- Passo griglia che non rispetta il vincolo di break-even (§4).
|
||||||
|
- Capitale che non puoi permetterti di perdere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Parametri di esempio (configurazione di partenza prudente)
|
||||||
|
|
||||||
|
```
|
||||||
|
PAIR = BNB/USDT # coppia liquida su PancakeSwap
|
||||||
|
RANGE_LOW = 580
|
||||||
|
RANGE_HIGH = 720
|
||||||
|
GRID_LEVELS = 8 # passo ≈ 2,7% > break-even
|
||||||
|
CAPITAL_QUOTE = 400 # USDT, su wallet dedicato
|
||||||
|
STOP_LOSS = 545 # ~6% sotto RANGE_LOW
|
||||||
|
TAKE_PROFIT = 760 # ~5,5% sopra RANGE_HIGH
|
||||||
|
SLIPPAGE_BPS = 50 # 0,5%
|
||||||
|
FEE_BPS = 25 # PancakeSwap v2
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifica break-even: passo ≈ 2,7% > `1,5 × (2×0,75%) = 2,25%` ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Questo documento descrive la strategia. L'implementazione (ethers v6, gestione sicura
|
||||||
|
della chiave, calcolo slippage, stato persistente, backtester) va sviluppata a parte,
|
||||||
|
con test, e validata secondo §7 prima di qualsiasi capitale reale.*
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
build: .
|
||||||
|
container_name: pythagoras-portfolio
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["uv", "run", "python", "-m", "src.portfolio.runner"]
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./portfolios.yml:/app/portfolios.yml:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
- .env.mainnet # token MAINNET (soldi veri); prevale su .env (vedi micro-test plan)
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import os; assert any(f.endswith('status.json') for r,d,fs in os.walk('/app/data/portfolios') for f in fs)"]
|
||||||
|
interval: 120s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.enable=false
|
||||||
|
|
||||||
|
# Dashboard web read-only (stato live, PnL totale/per-strategia, grafico equity,
|
||||||
|
# trade attivi+chiusi). Stessa immagine del runner, monta gli stessi data/ in sola
|
||||||
|
# lettura logica (legge equity.jsonl + status/trades dei worker). Porta 8787.
|
||||||
|
# NB: nessuna auth -> non esporre su internet pubblico, solo rete interna/VPN.
|
||||||
|
dashboard:
|
||||||
|
build: .
|
||||||
|
container_name: pythagoras-dashboard
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["uv", "run", "python", "-m", "src.live.dashboard", "--port", "8787"]
|
||||||
|
ports:
|
||||||
|
- "8787:8787"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./portfolios.yml:/app/portfolios.yml:ro
|
||||||
|
- ./docs/report:/app/docs/report:ro # scheda strategie_attive.html (modal "scheda dettagliata")
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
- .env.mainnet # token MAINNET (soldi veri); prevale su .env (vedi micro-test plan)
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8787/api/state', timeout=5)"]
|
||||||
|
interval: 120s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.enable=false
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# TODO — prossimi passi
|
||||||
|
|
||||||
|
> Raccolta dei follow-up aperti. Aggiornato 2026-06-11 (XS01 dispersion-gate live v1.1.20;
|
||||||
|
> real-truth ledger attivo; reset portafoglio 2026-06-10).
|
||||||
|
|
||||||
|
## Stato esecuzione per sleeve (REALE vs SIMULATO)
|
||||||
|
|
||||||
|
**REALE (shadow su Deribit testnet, REAL-TRUTH: il ledger segue i fill reali) — 15 sleeve:**
|
||||||
|
- 6 fade (MR01/MR02/MR07 × BTC/ETH) + DIP01_BTC → single-leg, v1.0.3+
|
||||||
|
- 6 pairs PR01 (ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL + ETH/BTC 15m) → 2 gambe,
|
||||||
|
v1.1.12 (2026-06-08); il 15m dal v1.1.16 (2026-06-09)
|
||||||
|
- SH01 × BTC/ETH → single-leg, exit a orizzonte (no TP/SL), v1.1.13 (2026-06-08)
|
||||||
|
|
||||||
|
**SIMULATO (paper, NON esegue reale) — 4 book multi-asset, fuori dal capitale-pool:**
|
||||||
|
- TR01 / ROT02 / TSM01 / XS01 → 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/XS01) — **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)
|
||||||
|
|
||||||
|
- [ ] **Valutare aumento capitale a €5.000-10.000** per togliere il rumore di arrotondamento.
|
||||||
|
A €2.000 il rumore BTC per trade è ~2.4% (morde); soglie: ~€4.800 (<1%), ~€9.500 (<0.5%,
|
||||||
|
"punto dolce"), ~€19.000 (trascurabile). Le commissioni invece sono % e neutre al capitale.
|
||||||
|
Col real-truth ledger il rumore di arrotondamento entra DIRETTAMENTE nell'equity → più
|
||||||
|
capitale = ledger più fedele alla strategia. Riguarda i 15 sleeve che eseguono reale.
|
||||||
|
|
||||||
|
## Follow-up dal code-review (bassa priorità, non urgenti)
|
||||||
|
|
||||||
|
- [x] ~~forming-bar su ROT02/TSM01~~ — GIÀ FIXATO (v1.1.10, 2026-06-08): `_panel` condiviso
|
||||||
|
scarta la barra in formazione via `last_bar_is_forming`. Item rimasto stantio nel TODO.
|
||||||
|
- [ ] **ancora bfill di `_daily_equity`** — QUANTIFICATO 2026-06-11 (`daily_equity_bfill_impact.py`):
|
||||||
|
**NON materiale, lasciare documentato**. PORT06 OOS invariato per costruzione (il bias vive
|
||||||
|
in testa alla serie, pre-SPLIT; ΔSharpe +0.001, ΔDD 0.000); FULL DD leggermente OTTIMISTICO
|
||||||
|
(3.46→3.67 col fix: il primo trade DIP01 2021, −3.75%, è nascosto dal bfill). Nessun verdetto
|
||||||
|
di gate a rischio (bias identico nei due bracci A-vs-B, si cancella). Unica eccezione OOS:
|
||||||
|
TSM01 (primo punto equity post-SPLIT, −0.014 Sh). Correggere SOLO in un eventuale refactor
|
||||||
|
del builder daily, OVUNQUE in un colpo (~12 file di gate replicano la convenzione) e
|
||||||
|
ri-baselinando i canonici nello stesso commit. CAVEAT per gate futuri: finestre IDX che
|
||||||
|
partono a metà storia amplificano il bug → usare lì la convenzione carry-forward corretta.
|
||||||
|
- [x] ~~convenzione TR01 worker vs reference~~ — ERA UN BUG, FIXATO (2026-06-11): `mean(rets)`
|
||||||
|
sui soli asset in posizione sovrappesava N/k a paniere parziale (con 1 long: 0.45 del
|
||||||
|
capitale invece di 0.09). Fix: `sum(rets)/len(universe)` (equal-weight 1/N canonico).
|
||||||
|
Replay post-fix: +32% vs reference +42% (residuo = convenzione capitale-unico vs
|
||||||
|
media-equity, dichiarata). Diario `2026-06-11-stability-sweep.md`.
|
||||||
|
- [x] ~~engine duplicato nei 3 gate~~ — FATTO (2026-06-11): fattorizzato in
|
||||||
|
`scripts/analysis/_port06_gate_common.py` (`build_trades_variant` versione trendmax =
|
||||||
|
superset con hurst_mask/trend_max parametrici, `equity_from_trades`, `port_metrics`,
|
||||||
|
`dd`); i 3 gate importano da lì. Nessuna drift di matematica trovata fra le copie
|
||||||
|
(solo firme/docstring). Output dei 3 gate verificato BYTE-IDENTICO prima/dopo.
|
||||||
|
`dip_trades` (DIP01) NON è una copia ma un sibling deliberato (long-only, orig_gap,
|
||||||
|
j clampato) → resta nel suo script, documentato nel modulo comune.
|
||||||
|
- [ ] **epoche hardcoded in `hourly_report.lossguard_section`** (LOSSGUARD_SINCE, TRENDSWAP_SINCE):
|
||||||
|
ogni nuova epoca-filtro richiede di editare la funzione. Derivarle da deploy history/config.
|
||||||
|
|
||||||
|
## Code-review 2026-06-11 sera (8a2b065..) — finding DEFERITI (i confermati critici sono fixati in v1.1.26)
|
||||||
|
|
||||||
|
- [ ] **RESTING reduce-only esposti al netting** (TP limit + disaster-SL): se un worker opposto
|
||||||
|
apre DOPO il piazzamento, il resting può fillare parziale o essere respinto — il fallback
|
||||||
|
netting copre solo i close sincroni. È il pezzo "position manager / sotto-conti" già noto;
|
||||||
|
lo scenario peggiore è il disaster-SL cappato proprio nel crash per cui esiste.
|
||||||
|
- [ ] **Lifecycle `orphan_legs`**: append-only — un orfano risolto a mano (o da reset_flatten)
|
||||||
|
resta nello status e MASCHERA per compensazione un drift vero futuro (falso negativo del
|
||||||
|
reconciler). Serve un comando di risoluzione (`--resolve-orphan`) e/o reset_flatten che
|
||||||
|
azzeri anche lo stato reale nei status.json. Idem: il PnL della chiusura manuale di un
|
||||||
|
orfano non viene mai bookato in real_capital (diagnostica shadow divergente).
|
||||||
|
- [ ] **TP_PHANTOM residuo**: `resting_fills` guarda solo le ultime 100 righe di trade-history
|
||||||
|
per strumento — su conto molto attivo un fill TP reale può scivolare fuori finestra →
|
||||||
|
falso phantom persistente (sim resta in posizione, reale flat). Mitigato dal quantize
|
||||||
|
conservativo (v1.1.26); fix vero = endpoint order-state in cerbero-mcp.
|
||||||
|
- [ ] **Validazione feed a monte** (altitudine): TP_PHANTOM copre solo i tocchi TP dei single-leg;
|
||||||
|
le ENTRY spike-driven, lo SL close-confirm su close spike e lo z dei pairs restano esposti
|
||||||
|
ai wick fantasma del feed testnet. Un validatore barre nel data layer coprirebbe tutti i
|
||||||
|
consumer con un meccanismo solo (su mainnet il fenomeno non esiste: priorità bassa finché
|
||||||
|
si resta su testnet, gli eventi TP_PHANTOM/NET_CLOSE ne misurano la frequenza).
|
||||||
|
- [ ] **Contratto dello schema status.json**: reconcile (`src/live/books.py`), hourly_report e i
|
||||||
|
worker condividono lo schema per convenzione implicita — books.py è ora la fonte unica per
|
||||||
|
i campi `real_*`, ma un helper `worker.real_book()` usato da _save e dai reader chiuderebbe
|
||||||
|
la classe di bug. Pulizia: `_tp_hit` helper per i 4 siti di tocco TP duplicati; port_metrics
|
||||||
|
ri-biforcato in xs01_tranche_gate/pairs30m_gate (importare da _port06_gate_common);
|
||||||
|
splits3/metriche duplicate nei games engine (estrarre in scripts/games/engine.py).
|
||||||
|
|
||||||
|
## Ricerca dispersion/correlation (2026-06-08, 165 agenti) — follow-up opzionale
|
||||||
|
|
||||||
|
- [x] ~~Gate PORT06 di `index_comp_disp` W=168~~ — FATTO (2026-06-08): PROMOSSO MARGINALE.
|
||||||
|
Decorrela bene (corr 0.06 col MASTER) ma OOS PIATTO (Sharpe −0.01). **Documentato e
|
||||||
|
rimandato** (non deployato): gate in `dispersion_edges/gate_index_comp_disp.py`,
|
||||||
|
riprendere solo se si costruisce una famiglia DISP più ampia. Diario aggiornato.
|
||||||
|
|
||||||
|
## Monitoraggio (osservare, non agire subito)
|
||||||
|
|
||||||
|
- [ ] **Churn da spike-print testnet su ETH (2026-06-11)**: il feed testnet stampa wick
|
||||||
|
anomali sulla barra 1h ETH che (a) generano segnali short MR01/MR07 (lo spike È lo
|
||||||
|
z-score estremo) e (b) "toccano" il TP intrabar della stessa barra → il sim booka
|
||||||
|
+4% fantasma a bars_held=0, il reale apre+chiude pagando solo fee/spread (~−0.17€
|
||||||
|
a giro, 14 giri oggi ≈ −2.3€). Il real-truth ledger contabilizza GIUSTO (per questo
|
||||||
|
esiste) e il report orario ora conta i win dal flag reale. MITIGATO in v1.1.23:
|
||||||
|
gate `TP_PHANTOM` (il tocco TP deve essere confermato dal fill del resting sul book
|
||||||
|
reale, o dal prezzo oltre il livello) → niente più chiusure a mercato su wick
|
||||||
|
fantasma. Resta l'ENTRY spike-driven (il segnale stesso nasce dal wick): NON
|
||||||
|
filtrarlo nei segnali (= fit su artefatto testnet); osservare la frequenza dei log
|
||||||
|
TP_PHANTOM — se cresce, valutare un gate di QUALITÀ FEED nel data layer.
|
||||||
|
|
||||||
|
- [ ] **FADE in coda storica (2026-06-11)**: il rolling 120g equal-weight delle 6 fade è al
|
||||||
|
**2° percentile** della propria storia (−1.0% vs p5 +0.4%); il PORT06 complessivo resta
|
||||||
|
in variazione normale (19-28° pct). NESSUN ritocco parametri (= fit sul regime corrente);
|
||||||
|
osservare se rientra. Follow-up tecnico: alert di drift per-FAMIGLIA nel `hourly_report`
|
||||||
|
(precomputare la distribuzione storica dei rolling-return di famiglia dal backtest
|
||||||
|
canonico → confrontare il rolling live; notifica sotto p5).
|
||||||
|
- [ ] **PnL live post-swap trend** (epoca TREND in `hourly_report`): verdetto stop-rate fade a n>=30.
|
||||||
|
- [ ] **SH01 post-bootstrap**: il trade-rate live deve scendere da ~25% a ~10% delle barre
|
||||||
|
(selettività della soglia ripristinata). Controllare nel report orario.
|
||||||
|
- [ ] **MR02_ETH**: ultima posizione aperta SENZA disaster-SL (aperta pre-v1.1.4). Si chiuderà
|
||||||
|
normalmente; le prossime aperture avranno il bracket.
|
||||||
|
- [ ] **parquet fresco per SH01 bootstrap**: oggi al 2026-05-28, il feed copre 365g → margine ~11
|
||||||
|
mesi. Rilanciare `download_all()` periodicamente (WARN automatico se il gap supera il lookback).
|
||||||
+15
@@ -107,6 +107,21 @@ Gate PORT06 (`eth_collar_gate.py`, BS calibrato skew_put 1.12 / skew_call 1.0, s
|
|||||||
−4% aggregato). La cap auto-finanziata era giusta sul *costo* (~break-even) ma la protezione non
|
−4% aggregato). La cap auto-finanziata era giusta sul *costo* (~break-even) ma la protezione non
|
||||||
vale il costo Sharpe/OOS-DD. Le opzioni si chiudono come **NO-GO empirico su prezzi reali**.
|
vale il costo Sharpe/OOS-DD. Le opzioni si chiudono come **NO-GO empirico su prezzi reali**.
|
||||||
|
|
||||||
|
## Deploy della miglioria (2026-06-09): MR02/ETH stop-largo sl_atr 3.0
|
||||||
|
Nessun SOSTITUTO batte il live; la MIGLIORIA deployabile e' allargare lo stop EXIT-16.
|
||||||
|
Gate stop-width (EXIT-16 close-confirm, swap solo MR02_ETH):
|
||||||
|
| sl_atr | FULL Sh | FULL DD | OOS Sh | OOS DD | worst-trade |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2.0 (LIVE) | 6.73 | 3.67 | 8.80 | 1.23 | -48% |
|
||||||
|
| **3.0** | **6.77** | **3.30** | **8.82** | 1.23 | **-50%** |
|
||||||
|
| 4.0 | 6.78 | 3.49 | 8.82 | 1.23 | -62% |
|
||||||
|
| no-SL (sl=0) | 6.76 | 3.68 | 8.87 | 1.23 | -65% (vietato) |
|
||||||
|
`sl_atr=3.0` domina il live (FULL DD -0.37pp, Sh +0.04/+0.02) e cattura il beneficio del no-SL
|
||||||
|
(gli stop di meta'-discesa sono falsi negativi) MANTENENDO lo stop di catastrofe (worst -50%, non
|
||||||
|
-65%) -> regola "mai sl=0" rispettata; 4.0 inizia a perdere la coda. Plateau 2.5-4.0 (tutti > live
|
||||||
|
su Sharpe). Override SCOPED a MR02/ETH in `_defs.py` (`_fade_params`); gli altri 5 fade non
|
||||||
|
ri-gateati -> invariati. 93/93 test passano. Deploy: `./scripts/deploy.sh` (cambio live).
|
||||||
|
|
||||||
## Artefatti riusabili
|
## Artefatti riusabili
|
||||||
- `scripts/analysis/option_overlay_lab.py` — overlay opzioni (pricing BS sintetico su DVOL).
|
- `scripts/analysis/option_overlay_lab.py` — overlay opzioni (pricing BS sintetico su DVOL).
|
||||||
- `scripts/analysis/options_fetcher.py` — import storico opzioni reale da cerbero-bite → data/options/.
|
- `scripts/analysis/options_fetcher.py` — import storico opzioni reale da cerbero-bite → data/options/.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 2026-06-09 — Gioco "Blind Traders": 100 agenti ciechi
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
100 agenti LLM (haiku) ricevono due serie anonime **X** e **Y** — in realta'
|
||||||
|
**BTC** e **ETH** 1h/15m/5m, mai etichettate — e devono proporre UNA regola che
|
||||||
|
"anticipi" i movimenti per un PnL netto positivo (fee 0.10% RT) con **>=10
|
||||||
|
trade/mese**. Non sanno cosa siano i dati. L'orchestratore (engine deterministico)
|
||||||
|
valuta ogni strategia, assegna un punteggio su **PNL + %win**, da' **90 epoche di
|
||||||
|
elaborazione** (hill-climb dei parametri) e **ogni 10 epoche blocca il 10% meno
|
||||||
|
profittevole** -> restano i **10 piu' profittevoli**.
|
||||||
|
|
||||||
|
Infrastruttura in `scripts/games/`:
|
||||||
|
- `engine.py` — dati anonimizzati, 6 famiglie segnale (zscore/breakout/ma_cross/
|
||||||
|
rsi/momentum/pairs), backtester causale fee-aware, scoring (>=10 tpm o squalifica).
|
||||||
|
- `agent_brief.py` — digest ANONIMO (stat aggregate + finestra normalizzata) + menu.
|
||||||
|
- `arena.py` — torneo a **3 finestre**: TRAIN (hill-climb), VALID (cull+rank
|
||||||
|
dell'orchestratore), TEST (OOS puro, mai ottimizzato). Anti-overfit.
|
||||||
|
- `run_game.py` — carica le 100 spec degli agenti e lancia il torneo.
|
||||||
|
|
||||||
|
## Risultato emergente
|
||||||
|
I 100 agenti ciechi, leggendo SOLO le statistiche anonime (autocorrelazione
|
||||||
|
negativa, "after_big_move_continues_pct" ~30-40% => le mosse estreme rientrano),
|
||||||
|
hanno **riscoperto da soli che il mercato e' mean-reverting**: 100/100 reversion,
|
||||||
|
67 hanno scelto il detector pairs, 30 zscore. Esattamente la lezione storica del
|
||||||
|
progetto (edge = reversione; pairs ETH/BTC il piu' robusto) — senza sapere che
|
||||||
|
fosse crypto.
|
||||||
|
|
||||||
|
## Classifica finale (top 10) — tutti PAIRS su 15m
|
||||||
|
Vincitore **agente #91** (15m, pairs market-neutral sul log-ratio X/Y):
|
||||||
|
- TEST/OOS puro: **PnL +3126%**, **win 77%**, **108.9 trade/mese**, **Sharpe 20.3**
|
||||||
|
- Full-period: PnL +8052%, win 70%, 94 tpm, Sharpe 12.2 (9604 trade)
|
||||||
|
- params: lookback 66, entry 1.67σ, exit 1.0σ, max_bars 35
|
||||||
|
- ipotesi (cieca): "Y altamente reversivo, X/Y log-ratio strong mean-reversion
|
||||||
|
(-0.43 autocorr), bassa correlazione cross-asset -> pairs market-neutral".
|
||||||
|
|
||||||
|
Tutti i 10 finalisti: pairs 15m, TEST Sharpe medio 19.9, tpm 66-109 (>>10).
|
||||||
|
|
||||||
|
## Caveat onesti
|
||||||
|
- Numeri OOS ottimistici: PnL additivo a notional fisso, **niente slippage sulle 2
|
||||||
|
gambe**, finestra OOS calma, 15m molti trade. Coerente col caveat PR01 del
|
||||||
|
progetto (Sharpe reale atteso ~4-5, non 20). Il valore del gioco e' il **metodo**
|
||||||
|
(scoperta cieca + selezione anti-overfit), non il livello assoluto di Sharpe.
|
||||||
|
- La convergenza su pairs conferma robustezza ma riduce la diversita': i 10 finalisti
|
||||||
|
sono varianti della stessa idea (ETH/BTC spread). Per un portafoglio servirebbe
|
||||||
|
diversificare (gia' fatto altrove: fade + honest + shape).
|
||||||
|
|
||||||
|
## Re-run "sobrio" con slippage (0.05%/lato)
|
||||||
|
`GAME_SLIP=0.0005` -> i pairs pagano +0.20% RT extra (4 lati). Lo slippage spinge
|
||||||
|
l'ottimizzatore verso **meno churn**: tpm dei finalisti 66-109 -> **40-47**, Sharpe
|
||||||
|
top-10 ~20 -> ~13.5. Vincitore **#43** (15m pairs): TEST PnL **+2091%**, win 77%,
|
||||||
|
**46.9 tpm**, Sharpe **15.6**. La gerarchia (pairs 15m domina) e la robustezza
|
||||||
|
reggono lo stress; lo Sharpe reale atteso resta ~4-5 (OOS calmo + PnL additivo).
|
||||||
|
Log: `data/games/game_slip.log`.
|
||||||
|
|
||||||
|
Artefatti: `data/games/tournament_result.json`, `data/games/specs/agent_*.json`,
|
||||||
|
`engine.set_slippage()` (env `GAME_SLIP`).
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 2026-06-09 — Gioco "Blind Traders" sessione 2: timing diversi (30m/2h/4h)
|
||||||
|
|
||||||
|
Seconda sessione del gioco (vedi `2026-06-09-blind-traders-game.md`), stesso protocollo
|
||||||
|
(100 agenti ciechi su BTC/ETH anonimi, scoring PNL+%win, ≥10 trade/mese, 90 epoche, cull
|
||||||
|
10% ogni 10 epoche → 10 finalisti, split a 3 anti-overfit) ma su **timeframe diversi**:
|
||||||
|
game 1 = 5m/15m/1h; **game 2 = 30m/2h/4h** (medio-lunghi). Engine con resampling aggiunto
|
||||||
|
(`engine._RESAMPLE`: 30m←15m, 2h/4h←1h). Specs in `data/games/specs2/`, risultato
|
||||||
|
`data/games/tournament_result2.json`.
|
||||||
|
|
||||||
|
## Diversita' proposte (di nuovo: riscoperta cieca della mean-reversion)
|
||||||
|
100 agenti: **74 pairs, 25 zscore, 1 breakout; 100% reversion**; tf 34/33/33. Come nel
|
||||||
|
game 1, leggendo solo le statistiche anonime (autocorrelazione negativa del log-ratio,
|
||||||
|
continuazione post-mossa ~40%) gli agenti convergono sulla reversione senza sapere che
|
||||||
|
sia crypto.
|
||||||
|
|
||||||
|
## Classifica finale — tutti 30m pairs
|
||||||
|
Vincitore **agente #36** (30m, pairs ETH/BTC):
|
||||||
|
- TEST/OOS: **PnL +1451%, win 77%, 43.4 trade/mese, Sharpe 12.3**.
|
||||||
|
- I 10 finalisti sono TUTTI 30m pairs (TEST Sharpe ~12, win 76-77%, tpm 43-49).
|
||||||
|
|
||||||
|
## Finding chiave: la regola ≥10 trade/mese e' un FILTRO sul timeframe
|
||||||
|
Quanti agenti per tf superano la soglia di attivita' + qualita':
|
||||||
|
| tf | agenti | ≥10 trade/mese | positivi OOS | miglior OOS Sharpe (pnl/win/tpm) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 30m | 34 | **34 (100%)** | 26 | **11.6** (1405% 76% 56) |
|
||||||
|
| 2h | 33 | 29 (88%) | 17 | 6.1 (512% 79% 17) |
|
||||||
|
| 4h | 33 | **4 (12%)** | 6 | 1.4 (103% 68% 14) |
|
||||||
|
|
||||||
|
A 4h solo 4/33 agenti riescono a fare ≥10 trade/mese (le barre sono troppo rade per
|
||||||
|
la reversione pairs); e l'edge cala col timeframe (Sharpe 11.6→6.1→1.4). Per questo i
|
||||||
|
finalisti sono tutti 30m.
|
||||||
|
|
||||||
|
## Lezione cross-game (game 1 + game 2)
|
||||||
|
Esiste una **frontiera frequenza-vs-edge**: la regola ≥10 trade/mese mette un *pavimento*
|
||||||
|
sul timeframe (i lunghi non fanno abbastanza trade), il costo/edge mette un *soffitto* (i
|
||||||
|
cortissimi sono cost-fragili). Il punto ottimo e' **il timeframe piu' corto con edge ancora
|
||||||
|
robusto**: game 1 (con 15m disponibile) → vince 15m; game 2 (senza 15m) → vince 30m. Sempre
|
||||||
|
**ETH/BTC spread reversion**. Coerente con l'analisi di robustezza del 15m
|
||||||
|
(`2026-06-09-pairs15m-robustezza.md`): piu' corto = piu' trade = piu' edge di backtest, ma
|
||||||
|
piu' fragile ai costi. Il gioco trova l'edge; la prudenza di deploy (mezza size) gestisce
|
||||||
|
la fragilita'. Artefatti: `scripts/games/`, `data/games/tournament_result2.json`.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# 2026-06-09 — Validazione edge credit-spread cerbero-bite (prezzi reali)
|
||||||
|
|
||||||
|
## Contesto
|
||||||
|
cerbero-bite (container accanto, `/opt/docker/cerbero-bite`) vende credit-spread su
|
||||||
|
ETH (bull-put primario, short delta ~0.18, DTE 18, Quarter-Kelly 13%, PT 50% / stop
|
||||||
|
2.5x credito / delta-breach 0.30 / vol-stop +10 DVOL / time-stop 7 DTE; testnet,
|
||||||
|
propose-only). Tune "Profilo B" del 2026-06-09: short delta alzato a 0.18 (da 0.10-0.15)
|
||||||
|
e `credit_to_width_ratio_min` 0.30->0.08 perche' a delta basso 0 spread erano eleggibili.
|
||||||
|
Domanda: l'edge regge su un ciclo ETH completo, o e' profittevole solo nei campioni calmi?
|
||||||
|
|
||||||
|
Validato con l'infrastruttura opzioni REALE (data/options/, importata da cerbero-bite).
|
||||||
|
Script riprendibile: `scripts/analysis/cerbero_bite_credit_spread.py`.
|
||||||
|
|
||||||
|
## Risultati
|
||||||
|
1. **Economia d'ingresso reale** (chain, 3145 spread): cw a delta 0.18 = **0.106** (p25 0.085),
|
||||||
|
eleggibilita' **65%**, short strike **~9.4% OTM** (NON 18% — quello era il vecchio delta basso),
|
||||||
|
**max-loss/credito = 8.4x**.
|
||||||
|
2. **Tail model-free** (8 anni ETH reali, cw 0.106, hold-to-expiry, niente modello opzioni):
|
||||||
|
win-rate 74%, **EV -1.0 crediti/trade, 7/9 anni NEGATIVI**, ETH <-13.4% a 17g (max-loss) il
|
||||||
|
**17.8%** delle volte. Un max-loss (-8.4cr) cancella 17 PT-winner. Gap 1g p5 = -25% (salta lo stop).
|
||||||
|
3. **Managed (skew calibrato sulle IV reali)**: win-rate **37%** (delta-breach esce sul 62% dei
|
||||||
|
trade a piccola perdita), **EV -0.02 cr/trade** (gia' a economia favorevole), worst -4.6 (2023).
|
||||||
|
2021+ EV -0.022.
|
||||||
|
|
||||||
|
## Caveat di calibrazione (TODO aperto)
|
||||||
|
Il mark mid+skew da **cw 0.228 vs 0.106 reale** -> sovrastima il credito ~2x (manca bid/ask
|
||||||
|
incrociato sulle 2 gambe + griglia strike reale). Quindi l'EV managed mostrato (-0.02) e' a
|
||||||
|
economia **2x troppo favorevole**: l'EV vero e' **<=**. Per il numero esatto: modellare bid/ask
|
||||||
|
reale + griglia (entrambi nella chain) cosi' entry cw -> 0.106. RIPRENDERE da qui.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
- **NON edge robusto su ciclo completo.** Il "+0.48%/mese netto" citato era **artefatto di
|
||||||
|
finestra calma** (mag-giu 2026, nessun crash): li' il body vince (EV +0.6), ma su ciclo
|
||||||
|
completo (mesi -13% al 18% di frequenza) e' breakeven-to-negativo in 2 stime indipendenti.
|
||||||
|
- **L'82% PoP e' ingannevole**: o esci presto sul delta-breach (win 37%, grind di piccole
|
||||||
|
perdite), o tieni e prendi la coda. Non c'e' la "macchina da 82% di vittorie".
|
||||||
|
- **Il tune Profilo B PEGGIORA la coda** (vendere a 9.4% OTM mette il max-loss in zona di
|
||||||
|
pullback ordinario). Strutturale: il mercato non paga per vendere lontano, vendere vicino
|
||||||
|
espone alla coda.
|
||||||
|
- **Coda CONCENTRATA con PythagorasGoal**: il gap che salta lo stop = il crash ETH, lo stesso
|
||||||
|
evento che colpisce lo sleeve fade. I due sistemi non diversificano, concentrano.
|
||||||
|
- **Azione**: cerbero-bite resta testnet/paper finche' non c'e' un campione che include un crash
|
||||||
|
vero; valutare un long deep-OTM put (0.33%/mese reale) come cap della coda del bull-put-spread.
|
||||||
|
|
||||||
|
## Stato
|
||||||
|
DA RIPRENDERE: calibrazione esatta credito (bid/ask + griglia) -> EV managed definitivo.
|
||||||
|
Tutto il resto e' chiuso e documentato.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# 2026-06-09 — Percorso live 15m per ETH/BTC pairs: COSTRUITO e VALIDATO
|
||||||
|
|
||||||
|
Seguito di `2026-06-09-pairs15m-port06-gate.md` (il gate passa, edge reale e non
|
||||||
|
artefatto flat). Qui si costruisce e VALIDA l'infrastruttura per eseguire il pairs
|
||||||
|
ETH/BTC a 15m con flat-skip, alla pari del backtest (disciplina validate_worker_pairs).
|
||||||
|
|
||||||
|
## 1. Engine canonico (regression-locked)
|
||||||
|
`scripts/analysis/pairs_research.py`: aggiunti `aligned_ohlc`, `is_flat_ohlc`,
|
||||||
|
`pairs_sim_flat(..., flat_skip, scan_buffer)`. Regola di uscita **LIVE-REALIZABLE**:
|
||||||
|
la condizione (|z|<=z_exit O bars>=max_bars) ARMA `exit_ready`; si esce al CLOSE della
|
||||||
|
PRIMA barra PULITA successiva (mai a un prezzo passato come faceva il prototipo push-back).
|
||||||
|
- **Regression-lock**: `pairs_sim_flat(flat_skip=False)` == `pairs_sim` ESATTO
|
||||||
|
(ETH/BTC 1h 1756 trade, 15m 9388 trade, ret/dd/sharpe identici al bit).
|
||||||
|
|
||||||
|
## 2. PairsWorker esteso (retrocompatibile)
|
||||||
|
`src/live/pairs_worker.py`: param `flat_skip`, stato `exit_ready` (persistito), tick
|
||||||
|
ora fa merge OHLC e rileva le candele flat (O=H=L=C in UNA gamba). Entry saltato su barra
|
||||||
|
stale; uscita con la stessa regola exit_ready dell'engine. **Default off = comportamento
|
||||||
|
1h storico invariato** (se mancano le colonne OHLC, flat=False).
|
||||||
|
|
||||||
|
## 3. Runner: fetch sub-orario (inerte finche' non c'e' uno sleeve 15m)
|
||||||
|
`src/portfolio/runner.py`: `_SUBHOURLY={5m,15m,30m}`, `_LOOKBACK_DAYS` esteso; il loop
|
||||||
|
fetcha DIRETTO da Cerbero i timeframe sub-orari per (asset,tf) (non resamplabili dal 1h) e
|
||||||
|
un router `_series_for` instrada la serie giusta a ogni worker. Zero impatto sul live
|
||||||
|
attuale: nessuno sleeve e' 15m → `subhourly_needs` vuoto → ramo morto.
|
||||||
|
|
||||||
|
## 4. VALIDAZIONE (validate_worker_pairs.py) — TUTTO OK
|
||||||
|
Replay bar-per-bar del worker == backtest:
|
||||||
|
| caso | worker | backtest | match |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ETH/BTC 1h | 1756 trd, cap 2.886.616 | 1756, 2.886.616 | **OK esatto** |
|
||||||
|
| BTC/LTC 1h | 599 trd, cap 16.861 | 599, 16.861 | **OK esatto** |
|
||||||
|
| **ETH/BTC 15m-flat** | **8452 trd** | **8453 trd** (cap entro 0.15%) | **OK** |
|
||||||
|
(1 trade di differenza = posizione finale aperta non chiusa nel replay, atteso.)
|
||||||
|
|
||||||
|
## 5. Gate finale (engine == worker) — PROMOSSO
|
||||||
|
`pairs15m_gate_final.py` (corr 1h vs 15m = 0.372, 3201 ingressi flat saltati):
|
||||||
|
| variante ETH/BTC | FULL Sh | FULL DD | OOS Sh | OOS DD |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| baseline 1h | 6.43 | 3.96 | 8.58 | 1.36 |
|
||||||
|
| **SWAP 15m-flat** | 7.31 | 3.55 | **9.95** | **1.26** |
|
||||||
|
| **BLEND 1h+15m** | 7.03 | 3.66 | 9.57 | 1.24 |
|
||||||
|
Entrambi PROMOSSI (a fee backtest). Caveat slippage del gate precedente invariato → il
|
||||||
|
BLEND e' la forma raccomandata (meta' allocazione sul 1h pulito, slippage-robusto).
|
||||||
|
|
||||||
|
## Stato e attivazione (NON fatta — decisione di deploy)
|
||||||
|
Tutto il PERCORSO e' pronto e validato, ma il 15m **non e' attivo nel portafoglio live**:
|
||||||
|
attivarlo cambia il trading reale e va deciso esplicitamente. Per accenderlo:
|
||||||
|
1. `_defs.py`: aggiungere SleeveSpec pairs ETH/BTC a 15m (tf="15m",
|
||||||
|
params={n:66,z_in:1.674,z_exit:1.0,max_bars:35,flat_skip:True}) — come SWAP della 1h o
|
||||||
|
come 2a sleeve (BLEND) sotto il cap PAIRS.
|
||||||
|
2. `report_families.build_everything` / `sleeves`: l'equity del nuovo sleeve dal
|
||||||
|
`pairs_sim_flat(tf=15m, flat_skip=True)` (per parita' backtest==report).
|
||||||
|
3. Shadow smoke su testnet (come `live_smoke_pairs.py`) prima del paper reale.
|
||||||
|
4. `deploy.sh` (bump+rebuild) — il runner gia' fetcha 15m e passa flat_skip via spec.params.
|
||||||
|
|
||||||
|
Test suite: nessuna regressione (1h byte-exact). Artefatti: pairs_research.py,
|
||||||
|
pairs_worker.py, runner.py, validate_worker_pairs.py, pairs15m_gate_final.py.
|
||||||
|
|
||||||
|
## ATTIVAZIONE IN REALE (2026-06-09) — BLEND, mezza size
|
||||||
|
Deciso: BLEND (sleeve 15m ACCANTO al 1h, non swap). Implementato:
|
||||||
|
- `_defs.py`: SleeveSpec `PR_ETHBTC_15M` (tf=15m, flat_skip, params.position_size=0.10
|
||||||
|
= meta' del family PAIRS 0.20) in PAIRS -> entra in PORT04/05/06.
|
||||||
|
- `report_families.build_everything`: equity da `pairs_sim_flat(tf=15m, flat_skip=True, pos=0.075)`
|
||||||
|
(mezza size, == intento live) con sid PR_ETHBTC_15M.
|
||||||
|
- `runner.pos_for_spec`: override PER-SLEEVE (params.position_size) > famiglia > globale.
|
||||||
|
- **Mezza size perche'** a peso pieno il 15m pesava il 25.8% del rischio PORT06 (vs 9.5% del
|
||||||
|
1h): dimezzato -> 11.5% vs 10.6%, bilanciato. Disciplina come la cap SHAPE; rispetta il
|
||||||
|
caveat slippage (il 15m non domina il book).
|
||||||
|
|
||||||
|
**PORT06 col BLEND (mezza size)**: FULL Sharpe **6.43->7.20** DD **3.96->3.68**,
|
||||||
|
OOS Sharpe **8.58->9.66** DD **1.36->1.31**. Migliora tutto.
|
||||||
|
|
||||||
|
**Smoke live 15m** (`pairs15m_live_smoke.py`): Cerbero serve candele 15m FRESCHE per
|
||||||
|
ETH e BTC (ultima barra 0 min fa, flat live 2-3%), worker flat-skip ticca OK. Esecuzione
|
||||||
|
reale a 2 gambe gia' coperta da `live_pairs_smoke.py` (livello strumento, tf-indipendente).
|
||||||
|
|
||||||
|
**Regression-lock aggiornati** (miglioria attesa, non regressione): test_definitions
|
||||||
|
(17->18 sleeve), test_backtest_parity_cap (FULL 6.47->7.20, OOS 8.82->9.66). Suite verde.
|
||||||
|
|
||||||
|
Live: il runner fetcha 15m diretto, costruisce il PairsWorker(flat_skip) col pos 0.10,
|
||||||
|
e lo esegue reale a 2 gambe (pairs_enabled). Attivazione via deploy (bump+rebuild).
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 2026-06-09 — ETH/BTC pairs a 15m: gate PORT06 (dal gioco Blind Traders)
|
||||||
|
|
||||||
|
## Origine
|
||||||
|
Il gioco "Blind Traders" (100 agenti ciechi) ha eletto come vincitore una variante
|
||||||
|
ETH/BTC pairs su **15m** (config #43: n=66 z_in=1.67 z_exit=1.0 max_bars=35). Domanda:
|
||||||
|
e' un vero miglioramento o un duplicato piu' veloce della sleeve PR01 ETH/BTC gia'
|
||||||
|
deployata a 1h? Testato sul serio con l'engine di PRODUZIONE `pairs_sim` + gate PORT06.
|
||||||
|
Script: `scripts/analysis/pairs15m_port06_gate.py`.
|
||||||
|
|
||||||
|
## Risultati
|
||||||
|
- **Parita' OK** (corr 1.00000): l'harness riproduce esattamente il sleeve canonico
|
||||||
|
PR_ETHBTC → gate affidabile.
|
||||||
|
- **CORRELAZIONE 1h vs 15m = 0.349** (rendimenti giornalieri). **SMENTISCE la mia
|
||||||
|
ipotesi iniziale "duplicato ridondante"**: a 15m cattura eventi di reversione DIVERSI
|
||||||
|
→ e' un diversificatore reale, non una doppia scommessa sullo stesso spread.
|
||||||
|
- **Robustezza 15m**: griglia n×z_in → **16/16 celle Sharpe>1** (9-12), plateau non picco.
|
||||||
|
Non e' un punto overfit del gioco.
|
||||||
|
- **Standalone**: 15m fa 9388 trade (vs 1756 a 1h), Sharpe 11.7 (vs 4.36), DD 54% (vs 48%),
|
||||||
|
8/9 anni+ . (Le % FULL sono esplose dal compounding pos0.15·lev3 su 9k trade → guardare
|
||||||
|
Sharpe/DD/anni, non il livello %.)
|
||||||
|
|
||||||
|
## Gate PORT06 (pos0.15 lev3 canonico, OOS da 2024-10-12)
|
||||||
|
| variante ETH/BTC | FULL Sh | FULL DD | OOS Sh | OOS DD |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **baseline 1h** | 6.43 | 3.96 | 8.58 | 1.36 |
|
||||||
|
| **SWAP 15m** | 7.64 | 3.49 | **10.39** | **1.26** |
|
||||||
|
| **BLEND 1h+15m** | 7.30 | 3.63 | 9.95 | 1.24 |
|
||||||
|
|
||||||
|
A fee di backtest (0.20% RT/coppia) **entrambe PROMOSSE**: Sharpe su e DD giu' ovunque.
|
||||||
|
|
||||||
|
## Stress slippage a livello PORT06 (il vero rischio: 15m = 5× i trade)
|
||||||
|
| fee_rt | RT/coppia | PORT06 FULL Sh | FULL DD | OOS Sh | OOS DD | std Sh | std oDD |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| baseline 1h | 0.20% | 6.43 | 3.96 | 8.58 | 1.36 | 4.36 | 16% |
|
||||||
|
| 15m | 0.20% | 7.64 | 3.49 | 10.39 | 1.26 | 11.7 | 13% |
|
||||||
|
| 15m | 0.40% | 7.04 | 4.08 | 9.78 | 1.45 | 8.5 | 27% |
|
||||||
|
| 15m | 0.60% | 6.43 | 4.67 | 9.15 | 1.66 | 5.3 | 47% |
|
||||||
|
|
||||||
|
**Degradazione graziosa ma reale**: il vantaggio di **Sharpe** sopravvive fino a slippage
|
||||||
|
pessimista (OOS 9.15 > 8.58 anche a 0.60%), ma il vantaggio di **DD si perde gia' a 0.40%**
|
||||||
|
(FULL DD 4.08 > 3.96 baseline; standalone oDD esplode 13→27→47%). La regola del progetto
|
||||||
|
("ri-gateare ogni filtro quando cambiano i costi") qui taglia: la frequenza 5× rende la
|
||||||
|
sleeve slippage-sensitive.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
- **NON un duplicato** (corr 0.35) e **NON overfit** (16/16 robusto) → la mia liquidazione
|
||||||
|
iniziale era SBAGLIATA, lo dico chiaro.
|
||||||
|
- **Passa il gate a fee di backtest, marginale sotto slippage**: migliora Sharpe sempre, ma
|
||||||
|
sotto slippage realistico (≥0.40% RT) peggiora leggermente il DD di portafoglio.
|
||||||
|
- **Due rischi di produzione NON ancora quantificati**: (a) qualita' dati ETH 15m (14-30%/anno
|
||||||
|
candele flat O=H=L=C → fill non eseguibili che gonfierebbero il backtest), (b) fill/liquidita'
|
||||||
|
reale a 2 gambe a 15m (5× ordini). Il worker pairs e' validato a 1h, non a 15m.
|
||||||
|
|
||||||
|
**Raccomandazione**: NON swap diretto in live. Candidato promettente → percorso forward:
|
||||||
|
preferire il **BLEND 1h+15m** (tiene il DD pulito del 1h e raccoglie il rendimento
|
||||||
|
decorrelato del 15m) **dopo** un check sull'impatto delle candele flat 15m sui pairs.
|
||||||
|
Allineato a come il progetto tratta FR01 (robusto ma non deployato finche' non domina pulito).
|
||||||
|
Resta come record di ricerca; deploy solo se il check flat-candle e' pulito.
|
||||||
|
|
||||||
|
## CHECK FLAT-CANDLE (pairs15m_flatcheck.py) — PULITO
|
||||||
|
Rischio: ETH 15m ha molte candele flat (O=H=L=C) → close stale che gonfia z-score →
|
||||||
|
reversione FINTA non eseguibile. Test:
|
||||||
|
- **Prevalenza**: ETH 15m **16.4% medio** (fino 30% nel 2022); BTC 15m solo 3.5%. Reale.
|
||||||
|
- **Fill toccati**: 12.9% degli entry e 15.2% degli exit cadono su una barra flat.
|
||||||
|
- **Test decisivo** (entry/exit SOLO su barre pulite, non-flat in entrambe le gambe):
|
||||||
|
rimuove 11.2% dei trade, **Sharpe trattenuto all'83%** (11.74→9.70; OOS Sharpe 18.4).
|
||||||
|
Se l'edge fosse un artefatto flat, filtrando crollerebbe → **NON crolla. NON e' artefatto.**
|
||||||
|
- **Gate PORT06 col 15m FLAT-FILTRATO** (corr 1h vs 15m-flat = 0.366, ancora decorrelato):
|
||||||
|
- SWAP 15m-flat: FULL 7.32/3.55, OOS **9.99/1.26** → PROMOSSO
|
||||||
|
- BLEND 1h+15m-flat: FULL 7.05/3.66, OOS **9.60/1.24** → PROMOSSO
|
||||||
|
|
||||||
|
## Conclusione (3 box su 4 puliti)
|
||||||
|
✅ NON duplicato (corr 0.35-0.37) ✅ robusto (16/16) ✅ NON artefatto flat (83% Sharpe)
|
||||||
|
⚠️ slippage-sensitive: a fee backtest passa pulito; a slippage ≥0.40% RT il vantaggio di
|
||||||
|
Sharpe regge ma il DD-edge si assottiglia. Il **BLEND** mitiga (meta' allocazione resta sul
|
||||||
|
1h pulito e slippage-robusto) → e' la forma deployabile.
|
||||||
|
|
||||||
|
## Realta' del deploy (perche' NON tocco ancora il live)
|
||||||
|
Il gate passa a livello BACKTEST. Ma il live NON puo' eseguire un sleeve 15m oggi:
|
||||||
|
- la live pairs gira SOLO a 1h (`PairsWorker`, validato da `validate_worker_pairs` a 1h);
|
||||||
|
il runner risampla a 1h/4h/1d, non gestisce un leg pairs a 15m.
|
||||||
|
- un BLEND richiede DUE sotto-sleeve ETH/BTC (1h + 15m) dentro il cap PAIRS, e il
|
||||||
|
**flat-skip va replicato nel worker live** (altrimenti il live tradera' le barre stale che
|
||||||
|
il backtest esclude → divergenza backtest-vs-live, la classe di bug che il progetto teme).
|
||||||
|
Editare `_defs.py` cambierebbe solo il backtest/report, NON il live → sarebbe ingannevole.
|
||||||
|
**Percorso deploy corretto** (da confermare): (1) estendere `PairsWorker`/runner al 15m +
|
||||||
|
flat-skip; (2) `validate_worker_pairs` a 15m (replay == backtest esatto); (3) aggiungere lo
|
||||||
|
sleeve 15m sotto il cap PAIRS; (4) shadow su testnet prima del paper. Finche' (1)-(2) non
|
||||||
|
sono fatti e validati, resta **record di ricerca PROMOSSO ma non live**.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 2026-06-09 — ETH/BTC 15m: analisi di robustezza (il limite è il COSTO)
|
||||||
|
|
||||||
|
Domanda: lo sleeve `PR_ETHBTC_15M` (attivato in real, v1.1.16) è robusto? Risposta dai
|
||||||
|
dati: **sì su parametri, tempo e qualità-dati; fragile sui COSTI** — e quel limite è
|
||||||
|
strutturale (alta frequenza). Engine `pairs_research.pairs_sim_flat` (live-realizable).
|
||||||
|
|
||||||
|
## ✅ Robusto su parametri, tempo, dati
|
||||||
|
- **Plateau parametrico**: griglia n×z_in (40-80 × 1.5-2.5) → **16/16 celle Sharpe>1**
|
||||||
|
(range 9-12). Non un picco overfit. (`pairs15m_port06_gate.py`)
|
||||||
|
- **Consistenza temporale**: 2018-2021 +1874% (3/4 anni+, solo 2018 negativo);
|
||||||
|
2022-2026 +12958% (**5/5 anni+**); **OOS ultimo 30% Sharpe 17.6, DD 13%, +**. L'edge
|
||||||
|
è distribuito, non un singolo regime.
|
||||||
|
- **Non artefatto dei dati**: filtrando le candele flat ETH 15m (16% storico) resta
|
||||||
|
l'**83% dello Sharpe** (`pairs15m_flatcheck.py`).
|
||||||
|
- **Decorrelato dal 1h** (corr rendimenti giornalieri **0.37**): segnale diverso, non un relabel.
|
||||||
|
|
||||||
|
## ❌ Fragile sui COSTI — il vero tallone (frequenza 5× il 1h)
|
||||||
|
Sharpe in funzione del costo all-in RT/coppia (fee + slippage):
|
||||||
|
|
||||||
|
| costo RT/coppia | **15m Sharpe** | 1h Sharpe |
|
||||||
|
|---|---|---|
|
||||||
|
| 0.20% (reale, 1×) | **9.34** | 4.36 |
|
||||||
|
| 0.40% (2×) | 6.15 | 3.52 |
|
||||||
|
| 0.60% (3×) | 2.95 | 2.68 |
|
||||||
|
| 0.80% (4×) | **−0.24** | 1.84 |
|
||||||
|
| 1.20% (6×) | −6.63 | 0.16 |
|
||||||
|
|
||||||
|
**Il 1h regge ~6× i costi; il 15m va negativo già a ~4×.** Tanti trade piccoli (8453 vs
|
||||||
|
1756) → margine di costo sottile. Lo **Sharpe 9.34 è un numero a costo basso**: appena lo
|
||||||
|
slippage reale a 2 gambe porta l'all-in verso 0.40-0.60%, scende a 3-6 (ancora positivo,
|
||||||
|
ma l'edge enorme del backtest è in gran parte illusione da bassi costi).
|
||||||
|
|
||||||
|
## Implicazioni (coerenti con la config deployata)
|
||||||
|
1. **La mezza size era giusta**: non si dà al 15m il peso che il backtest a costo basso
|
||||||
|
suggerirebbe. È un blend-tilt, non una scommessa.
|
||||||
|
2. **NO allo swap** (togliere il 1h, tenere solo 15m): sostituirebbe l'àncora cost-robust
|
||||||
|
(1h, regge 6×) con la sleeve cost-fragile. Confronto PORT06: swap OOS 9.95 vs blend 9.66
|
||||||
|
— +0.3 di Sharpe di backtest pagati in robustezza reale. Non vale.
|
||||||
|
3. **Giudice finale = ledger reale shadow**: misurerà lo slippage vero a 15m. Soglia di
|
||||||
|
lettura: all-in <0.40% (Sharpe ≥6) → ottimo diversificatore, valutare size piena;
|
||||||
|
verso 0.60%+ → vale appena la mezza-size che ha (già protetto).
|
||||||
|
|
||||||
|
**In una riga**: robusto come *segnale*, fragile come *esecuzione* → sta nel portafoglio a
|
||||||
|
metà size accanto al 1h, non al suo posto. Riproducibile: fee-sweep + sub-periodo +
|
||||||
|
OOS via `pairs_sim_flat`; plateau/flat-check negli script `pairs15m_*.py`.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# 2026-06-09 — Statistiche per-anno di tutte le sleeve attive in REAL
|
||||||
|
|
||||||
|
Snapshot post-attivazione BLEND ETH/BTC 15m (v1.1.16). Breakdown per-anno delle **15
|
||||||
|
sleeve che eseguono ordini reali** su Deribit testnet (escluse le 3 PAPER multi-asset
|
||||||
|
TR01/ROT02/TSM01). Engine **path-live**: EXIT-16 + filtro trend 3.0 per le fade,
|
||||||
|
walk-forward expanding per SH01, flat-skip per il pairs 15m. **PnL% = somma dei
|
||||||
|
rendimenti netti per-trade levered ×3, fee 0.10-0.20% RT incluse** — NON il contributo
|
||||||
|
al portafoglio (che applica sizing pos, cap-weighting, leva 2x, ribilancio 1D); serve a
|
||||||
|
confrontare edge grezzo e DD per sleeve.
|
||||||
|
|
||||||
|
Riproducibile: famiglie pairs via `pairs_research.pairs_sim`/`pairs_sim_flat`; fade/DIP/SH01
|
||||||
|
via le funzioni `stats_fades`/`stats_dip`/`stats_sh01` di `scripts/analysis/make_strategy_doc.py`.
|
||||||
|
|
||||||
|
## FADE (6) + DIP01 — PnL% per anno (n trade)
|
||||||
|
| anno | MR01 BTC | MR01 ETH | MR02 BTC | MR02 ETH | MR07 BTC | MR07 ETH | DIP01 BTC |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 2018 | −78 (48) | −85 (42) | +69 (127) | +10 (127) | −12 (38) | +15 (25) | −78 (79) |
|
||||||
|
| 2019 | −84 (57) | +21 (46) | +18 (118) | +120 (114) | +55 (52) | −40 (35) | −104 (62) |
|
||||||
|
| 2020 | −29 (68) | −35 (55) | +100 (112) | −94 (135) | −11 (53) | +39 (43) | +67 (56) |
|
||||||
|
| 2021 | +326 (82) | +238 (60) | +332 (136) | +160 (127) | +245 (45) | +72 (24) | +315 (69) |
|
||||||
|
| 2022 | +567 (108) | +749 (76) | +728 (106) | +650 (91) | +446 (82) | +472 (58) | +407 (83) |
|
||||||
|
| 2023 | +318 (122) | +248 (73) | +235 (96) | +524 (146) | +168 (67) | +337 (58) | +228 (83) |
|
||||||
|
| 2024 | +413 (113) | +888 (126) | +868 (149) | +1952 (180) | +377 (75) | +645 (87) | +332 (81) |
|
||||||
|
| 2025 | +368 (90) | +361 (86) | +386 (136) | +931 (151) | +190 (60) | +226 (55) | +303 (85) |
|
||||||
|
| 2026* | +69 (29) | −9 (24) | +88 (37) | +25 (41) | +58 (17) | +40 (17) | +7 (29) |
|
||||||
|
| **TOT** | +1870 (717) | +2376 (588) | +2823 (1017) | +4278 (1112) | +1517 (489) | +1806 (402) | +1476 (627) |
|
||||||
|
| **maxDD%** | 32 | 23 | 19 | 31 | 12 | 23 | 37 |
|
||||||
|
|
||||||
|
## SHAPE SH01 (2) — PnL% per anno (n trade)
|
||||||
|
| anno | SH01 BTC | SH01 ETH |
|
||||||
|
|---|---|---|
|
||||||
|
| 2018 | −66 (237) | +74 (239) |
|
||||||
|
| 2019 | +88 (318) | −19 (365) |
|
||||||
|
| 2020 | +194 (240) | −293 (219) |
|
||||||
|
| 2021 | +301 (224) | +67 (146) |
|
||||||
|
| 2022 | +64 (142) | +79 (91) |
|
||||||
|
| 2023 | +17 (118) | +21 (17) |
|
||||||
|
| 2024 | +110 (144) | +108 (47) |
|
||||||
|
| 2025 | +77 (85) | +540 (108) |
|
||||||
|
| 2026* | +59 (23) | −30 (25) |
|
||||||
|
| **TOT** | +845 (1531) | +547 (1257) |
|
||||||
|
| **maxDD%** | 23 | 61 |
|
||||||
|
|
||||||
|
## PAIRS (6) — PnL% per anno (n trade) | 15m a mezza size (pos 0.075)
|
||||||
|
| anno | ETH/BTC 1h | LTC/ETH | ADA/ETH | BTC/LTC | ETH/SOL | ETH/BTC 15m |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 2018 | −343 (177) | — | — | — | — | −492 (913) |
|
||||||
|
| 2019 | +233 (198) | — | — | — | — | +792 (963) |
|
||||||
|
| 2020 | +323 (211) | — | — | — | — | +452 (867) |
|
||||||
|
| 2021 | +549 (202) | — | — | — | — | +1122 (949) |
|
||||||
|
| 2022 | +1344 (206) | +292 (45) | +899 (157) | +328 (58) | +1243 (147) | +2136 (898) |
|
||||||
|
| 2023 | +464 (213) | +56 (94) | +341 (173) | +61 (106) | +125 (168) | +689 (976) |
|
||||||
|
| 2024 | +1661 (253) | +1024 (195) | +1078 (225) | +638 (186) | +1302 (199) | +6313 (1353) |
|
||||||
|
| 2025 | +1173 (225) | +962 (196) | +1046 (187) | +686 (178) | +1264 (202) | +3664 (1123) |
|
||||||
|
| 2026* | +61 (71) | +279 (76) | +140 (71) | +265 (71) | +270 (88) | +155 (411) |
|
||||||
|
| **TOT** | +5464 (1756) | +2614 (606) | +3504 (813) | +1978 (599) | +4204 (804) | +14832 (8453) |
|
||||||
|
| **maxDD%** | 48 | 14 | 19 | 21 | 24 | 34 |
|
||||||
|
| **Sharpe** | 4.36 | 4.22 | 4.90 | 2.72 | 4.61 | 9.34 |
|
||||||
|
|
||||||
|
\* 2026 parziale (fino al 28 mag). Coppie alt (LTC/ADA/SOL) hanno storia solo dal 2022.
|
||||||
|
|
||||||
|
## Aggregato PORT06 (BLEND attivo, 18 sleeve def.)
|
||||||
|
**FULL Sharpe 7.20 / DD 3.68% / CAGR 76% — OOS Sharpe 9.66 / DD 1.31%**. Per anno:
|
||||||
|
2021 +19.0% · 2022 +73.3% · 2023 +39.8% · 2024 +220.1% · 2025 +110.5% · 2026* +7.7%.
|
||||||
|
Nessun anno negativo dal 2021.
|
||||||
|
|
||||||
|
## Letture
|
||||||
|
- Il PnL% grezzo del 15m e' grande perche' fa ~5x i trade del 1h (8453 vs 1756), non
|
||||||
|
per-trade: il confronto equo e' Sharpe (9.34 vs 4.36) e DD (34% vs 48%). Nel portafoglio
|
||||||
|
entra a mezza size -> ~11.5% del rischio (== il 1h).
|
||||||
|
- MR02 ETH = singolo motore piu' forte (+4278%, 2024 +1952%); MR07 il piu' selettivo
|
||||||
|
(DD 12-23%); SH01 ETH il piu' rischioso (DD 61%, coda 2020 −293%) -> mitigato dal cap SHAPE.
|
||||||
|
- I 3 book multi-asset (TR01/ROT02/TSM01) restano PAPER, fuori da questo elenco.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# 2026-06-09 — XS01: reversione cross-sectional (famiglia nuova, trovata + deployata PAPER)
|
||||||
|
|
||||||
|
## Origine
|
||||||
|
Dopo aver scartato (alla cieca, coi giochi) trend/breakout/seasonal/opzioni/funding come
|
||||||
|
rumore o −EV, ho cercato io un meccanismo *diverso* dalla mean-reversion pairwise. Trovato:
|
||||||
|
**XS01 — reversione CROSS-SECTIONAL** su 8 asset (BTC/ETH/LTC/ADA/SOL/BNB/XRP/DOGE).
|
||||||
|
|
||||||
|
## Meccanismo
|
||||||
|
Ogni HOLD=12 ore: classifica gli 8 asset per rendimento su LB=48 ore, pesi
|
||||||
|
w = −(ret − media_cross-section), normalizzati a gross 1 → **long i perdenti relativi /
|
||||||
|
short i vincenti**, market-neutral. Roll non sovrapposto (entry-to-entry = hold+1 barre).
|
||||||
|
Fee 0.10% RT/book. Cattura il FATTORE reversione trasversale, distinto dai pairs (pairwise).
|
||||||
|
|
||||||
|
## Verifica (engine canonico `scripts/strategies/XS01_cross_sectional.py`)
|
||||||
|
- **No look-ahead** verificato (segnale invariato perturbando il futuro).
|
||||||
|
- **Robusto**: plateau OOS Sharpe **2–3.9** su lb 12–72 × hold 6–24.
|
||||||
|
- **Scorrelato**: corr **−0.006 / 0.035** da PR01 ETH/BTC, −0.028 dai fade → diversificatore.
|
||||||
|
- Per-anno (entry): 2022 +34, 2023 +6, 2024 +21, **2025 +225**, 2026 +85 (5/5 anni+).
|
||||||
|
- **Caveat**: edge concentrato sul 2025; cost-sensitive (muore ~0.35% RT/book); 8 gambe;
|
||||||
|
storia dal 2022 (no 2018-2020).
|
||||||
|
|
||||||
|
## Worker validato (== backtest esatto)
|
||||||
|
`src/live/xsec_worker.py` `CrossSectionalWorker`: book market-neutral che rolla ogni HOLD
|
||||||
|
barre, stessa formula pesi e cadenza dell'engine. `validate_xsec_worker.py`: replay
|
||||||
|
bar-per-bar == backtest **ESATTO** (worker 4993/1427 trade/49.8% == backtest 4993/1427/49.8%).
|
||||||
|
Bug risolto: il primo prototipo rollava 1 barra troppo tardi (cooldown extra) → rimosso,
|
||||||
|
guard a lb+1, entry-to-entry = hold+1.
|
||||||
|
|
||||||
|
## Gate PORT06 — PROMOSSO (con asterisco)
|
||||||
|
| | corr | FULL Sh | FULL DD | OOS Sh | OOS DD |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| ATTUALE (19→ senza XS01) | — | 7.20 | 3.68 | 9.66 | 1.31 |
|
||||||
|
| **+XS01** | −0.006 | **7.34** | **3.46** | **10.07** | 1.48 |
|
||||||
|
|
||||||
|
Migliora 3 metriche su 4 (OOS Sharpe **+0.41**, il salto più grande dal 15m; FULL DD giù).
|
||||||
|
Unico neo: OOS DD +0.17pp. Risk-contrib XS01 solo **2.2%** (diversificatore a bassa vol).
|
||||||
|
|
||||||
|
## Deploy (v?, 2026-06-09) — PAPER
|
||||||
|
8 gambe → niente esecuzione reale (come TR01/ROT02/TSM01) → XS01 gira **PAPER**
|
||||||
|
(`paper_sleeves`), fuori dal pool, raccoglie statistica forward. Wiring: `_defs.XSEC` in
|
||||||
|
PORT06 (19 sleeve, family XSEC via prefix "XS"), `build_everything` (equity da xsec_sim),
|
||||||
|
`runner` kind="xsec" → CrossSectionalWorker, `asset_days` ora include i paper (fix: gli alt
|
||||||
|
BNB/DOGE/XRP ora vengono fetchati anche per TR01/ROT02/TSM01). Regression-lock aggiornati
|
||||||
|
(18→19 sleeve, FULL 7.20→7.34, OOS 9.66→10.07, DD 3.68→3.46). 93 test verdi.
|
||||||
|
|
||||||
|
**Direzione futura:** se la statistica forward conferma, costruire l'esecuzione reale a
|
||||||
|
N gambe (oggi inesistente) per portarlo nel pool. Per ora: candidato validato che gira
|
||||||
|
PAPER e si osserva. Artefatti: `scripts/strategies/XS01_cross_sectional.py`,
|
||||||
|
`src/live/xsec_worker.py`, `scripts/analysis/{validate_xsec_worker,xsec_port06_gate}.py`.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 2026-06-10 — FC01 funding-carry market-neutral: SCARTATA (il carry non paga le fee)
|
||||||
|
|
||||||
|
## Idea
|
||||||
|
|
||||||
|
Carry NEUTRALE sul funding Deribit (meccanismo mai esplorato: W12 era lo short
|
||||||
|
direzionale su funding alto, bocciato): short della gamba col funding alto /
|
||||||
|
long della gamba bassa (ETH vs BTC, dollar-neutral), incassando il differenziale
|
||||||
|
di funding con esposizione residua = solo lo spread ETH/BTC.
|
||||||
|
|
||||||
|
Dati REALI: `data/regime/{btc,eth}_funding.parquet` — funding orario effettivo
|
||||||
|
dic 2019 → giu 2026 (56.938 ore), `interest_1h` + index_price.
|
||||||
|
Harness: `scripts/analysis/funding_carry_research.py` (causale: decisione al
|
||||||
|
close t, accrual da t+1; fee 0.10% RT per gamba; TRAIN ≤2023-11-01 / OOS dopo).
|
||||||
|
|
||||||
|
## Numeri che uccidono l'idea (prima ancora del backtest)
|
||||||
|
|
||||||
|
- Funding annualizzato: BTC mean +7.2% / med +0.3%; ETH +5.2% / +0.05% —
|
||||||
|
la MEDIANA è ~zero: il funding alto è episodico, non strutturale.
|
||||||
|
- Spread ETH−BTC: p10 −19% / p90 +12.6% annualizzato, MA autocorr del
|
||||||
|
24h-smooth a 72h = 0.20 → poca persistenza.
|
||||||
|
- **Episodi |spread|>10% ann: durata mediana 19h** (p90 60h) → carry
|
||||||
|
incassabile per episodio ≈ 10%/8760×19h ≈ **0.02-0.05%**, contro **0.20%**
|
||||||
|
di fee (2 gambe). Strutturalmente morto: il carry è un ordine di grandezza
|
||||||
|
sotto i costi, e il price-leg dello spread ETH/BTC (vol ben maggiore del
|
||||||
|
carry) domina il PnL.
|
||||||
|
|
||||||
|
## Backtest (conferma)
|
||||||
|
|
||||||
|
- FC-A spread-carry, griglia smooth {24,72,168}h × thr {5,10,20}% ann:
|
||||||
|
TRAIN quasi tutto negativo (Sharpe −0.6…+0.03), OOS negativo ovunque
|
||||||
|
tranne una cella (s24 thr5: OOS +38% con TRAIN −45% = rumore/sign-flip).
|
||||||
|
Sweep fee: negativa anche a 0.05% RT/gamba. Annuale: 2021 −93%, nessuna
|
||||||
|
stabilità.
|
||||||
|
- FC-B direzionale single-asset (confronto onesto): negativa ovunque,
|
||||||
|
riconferma W12.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
SCARTATA. Su Deribit BTC/ETH il funding non è un carry harvestabile: troppo
|
||||||
|
piccolo, troppo breve, e l'hedge cross-asset introduce più rischio del carry
|
||||||
|
che raccoglie. Eventuale rivisita SOLO se: (a) si aggiungono alt ad alto
|
||||||
|
funding cronico (Hyperliquid, registry già validato) dove i livelli sono
|
||||||
|
5-10x, o (b) si usa il funding come FEATURE/gate di strategie esistenti
|
||||||
|
(es. filtro sulle entry PR01) invece che come fonte di PnL.
|
||||||
|
|
||||||
|
Script: `scripts/analysis/funding_carry_research.py` (resta come record negativo).
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# 2026-06-10 — Gioco "Blind Traders" sessione 3: GRID TRADERS (regola: STRATEGIA_GRIGLIA.md)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Terza sessione del gioco dei trader ciechi, con una regola nuova: ogni agente deve
|
||||||
|
implementare la strategia descritta in `STRATEGIA_GRIGLIA.md` (grid trading a griglia
|
||||||
|
geometrica). 100 agenti (haiku, via Workflow) ricevono SOLO un digest anonimo di due
|
||||||
|
serie X/Y (in realtà BTC/ETH, mai rivelato) sul loro timing assegnato
|
||||||
|
(25×15m, 20×30m, 20×1h, 15×2h, 12×4h, 8×1d) + 5 archetipi di stile a rotazione
|
||||||
|
(prudente / aggressivo / asimmetrico-rialzista / asimmetrico-ribassista / data-driven),
|
||||||
|
e propongono la CONFIGURAZIONE della griglia: `range_down/up %`, `grid_levels`,
|
||||||
|
`sl_buf`, `tp_buf`, `max_bars`, serie.
|
||||||
|
|
||||||
|
Infrastruttura nuova in `scripts/games/`:
|
||||||
|
- **`grid_engine.py`** — backtest deterministico, causale, fee-aware della spec:
|
||||||
|
griglia geometrica `ratio=((1+ru)/(1-rd))^(1/L)` costruita sul close di deploy,
|
||||||
|
capitale 1/L per livello (§3.3), buy su attraversamento ↓ di un livello non
|
||||||
|
riempito, sell del livello su attraversamento ↑ del successivo (§5.2), SL sotto
|
||||||
|
il range e TP sopra che liquidano tutto (§6), redeploy a fine episodio
|
||||||
|
(SL/TP/max_bars). **Vincolo break-even §4 implementato alla lettera**: passo
|
||||||
|
≤ 1.5×costo RT → il motore si rifiuta di partire (`refused`, fitness −2e6);
|
||||||
|
`_normalize` dell'arena riduce i livelli al massimo legale. Fill intrabar lungo
|
||||||
|
il percorso O→L→H→C / O→H→L→C; fee 0.10% RT per round-trip + slippage opzionale.
|
||||||
|
- **`grid_arena.py`** — torneo identico alle sessioni 1-2: split 60/20/20
|
||||||
|
TRAIN/VALID/TEST, 90 epoche di hill-climb sul TRAIN, cull del 10% in VALID ogni
|
||||||
|
10 epoche → 10 superstiti; TEST = OOS puro mai ottimizzato.
|
||||||
|
- **`grid_brief.py`** — digest anonimo con statistiche per dimensionare una griglia:
|
||||||
|
escursione max/min rolling (w100/500/2000, mediana e p90) e probabilità di fuga
|
||||||
|
da un range ±5/10/20% entro 500 barre.
|
||||||
|
|
||||||
|
## Esito
|
||||||
|
|
||||||
|
`data/games/grid_result.json` (+ log `grid_tournament.log`). 100/100 spec da agenti
|
||||||
|
reali (nessun sostituto random).
|
||||||
|
|
||||||
|
- **Alla proposta, 80/100 agenti scelgono X (=BTC)**: dai soli numeri anonimi capiscono
|
||||||
|
che la griglia sopravvive meglio sulla serie meno volatile (escape ±20% in 500 barre:
|
||||||
|
BTC 34.5% vs ETH 53.2% a 1h).
|
||||||
|
- **L'evoluzione ribalta la scelta: tutti i 10 superstiti finiscono su Y (=ETH)** —
|
||||||
|
nel periodo VALID/TEST la vol più alta di ETH paga di più i round-trip, e il rischio
|
||||||
|
trend è gestito non dal range stretto ma dalla FORMA della griglia (sotto).
|
||||||
|
- **Convergenza fortissima della forma** (9/10 superstiti): griglia **asimmetrica
|
||||||
|
ribassista** — range profondo sotto (−13/−20%), corto sopra (+4/+8%), **livelli al
|
||||||
|
minimo (4)** → passo largo ~4.5-5.5%, SL buffer profondo (5-15%), max_bars lunghi.
|
||||||
|
Tradotto: **compra i dip di ETH in 4 tranche distanziate ~5%, rivendi ogni tranche
|
||||||
|
al rimbalzo di un passo, stop catastrofale a ~−25/−30%**. Il gioco ha ri-scoperto
|
||||||
|
per la terza volta la mean-reversion (qui in forma di dip-buying a tranche), e ha
|
||||||
|
imparato da solo la lezione anti-fee: meglio pochi passi larghi che griglie fitte.
|
||||||
|
|
||||||
|
**Vincitore: agente #11, ETH 15m**, griglia −17.1%/+4.6% × 4 livelli, SL buf 12.4%,
|
||||||
|
TP buf 4.8%, max 2143 barre. TEST(OOS): **PnL +891% (additivo), win 97%, 38.5
|
||||||
|
trade/mese, Sharpe 10.1**. FULL 2018-2026 (include bear 2018 e 2022): +4284%, Sharpe 9.6.
|
||||||
|
Stress slippage TEST: 0.05%/lato → 871%, 0.10%/lato → 850% (il passo ~5.4% ⋙ costi).
|
||||||
|
|
||||||
|
## Caveat onesti (perché NON è un candidato deploy così com'è)
|
||||||
|
|
||||||
|
1. **Il PnL è additivo per-trade e non misura il drawdown UNREALIZED**: l'engine
|
||||||
|
somma i round-trip realizzati; mentre la griglia tiene 4 tranche dentro un
|
||||||
|
drawdown −17%, l'equity vera è sott'acqua (fino a ~−15% di episodio + SL −25/30%
|
||||||
|
quando scatta). Il win-rate 97% è il profilo classico della griglia: tante
|
||||||
|
micro-vincite, perdite rare ma grandi (stessa famiglia di rischio del
|
||||||
|
short-vol/martingala). Per un gate serio servirebbe l'equity curve mark-to-market.
|
||||||
|
2. **Long-only su ETH con VALID/TEST 2023-2026** (regime rialzista/oscillante):
|
||||||
|
l'asimmetria −17/+4.6 è anche un fit al regime. Il FULL positivo (bear inclusi)
|
||||||
|
è incoraggiante ma il grosso del PnL resta nei periodi di reversione rialzista.
|
||||||
|
3. Le candele flat ETH 15m (14-30%/anno) e i fill intrabar "al livello" condividono
|
||||||
|
i caveat noti del progetto (bias pro-stop-stretti dell'engine intrabar; qui gli
|
||||||
|
stop sono larghi, quindi l'effetto è minore).
|
||||||
|
|
||||||
|
## Lezioni
|
||||||
|
|
||||||
|
- La spec STRATEGIA_GRIGLIA.md è implementabile in modo causale e onesto; il suo
|
||||||
|
vincolo break-even (§4) è esattamente la "lezione fee" del progetto, e il torneo
|
||||||
|
l'ha confermato spingendo i livelli al minimo (passo massimo).
|
||||||
|
- La griglia evoluta è un parente povero delle fade MR già in live: stesso edge
|
||||||
|
(reversione ETH), ma incassato con inventory risk crescente invece che con
|
||||||
|
TP/SL per-trade. Non sostituisce le MR; eventuale interesse solo come variante
|
||||||
|
"a tranche" da gateare con equity mark-to-market (e confronto con MR01/MR02 a
|
||||||
|
parità di rischio) PRIMA di pensarci per il PORT06.
|
||||||
|
|
||||||
|
## GATE "si puo' inserire?" (stessa sera) — NO-GO: edge = artefatto delle wick testnet
|
||||||
|
|
||||||
|
`scripts/analysis/grid_game_gate.py`: engine **mark-to-market** dedicato (equity
|
||||||
|
per barra = capitale + inventario al close, SL gap-aware, flat-skip, fee 0.10% RT),
|
||||||
|
metriche standard del progetto + gate PORT06. Primo passaggio ingannevole: standalone
|
||||||
|
WINNER 15m FULL Sharpe 5.61 DD 15.8%, corr max coi sleeve 0.34, plateau 16/16,
|
||||||
|
e il criterio formale promuoveva il half-size (OOS Sh 10.07->10.12, DD 1.48->1.36).
|
||||||
|
MA il breakdown annuale mostrava **+4946% nel 2022** (ETH −70%): impossibile.
|
||||||
|
|
||||||
|
**Verifica avversariale decisiva**: il feed Deribit (testnet) e' pieno di **spike
|
||||||
|
print** — ETH 15m ha 1064 barre con wick ≥6% sotto i close adiacenti che rientrano
|
||||||
|
subito (fino a −19% in 15 minuti; BTC ha spike a −54% nel feb 2024). La griglia
|
||||||
|
intrabar compra su quelle wick FINTE e rivende al rimbalzo: free money in sim,
|
||||||
|
infillabile dal vivo. Stress **close-only** (fill solo su attraversamento del
|
||||||
|
close): WINNER 15m CAGR 1544%->21%, Sharpe 5.61->0.92, OOS +2738%->+32%,
|
||||||
|
trades 3803->1156; top3 1h CAGR -1.7%, OOS −18%. **~99% dell'edge vive nelle
|
||||||
|
wick.** La versione onesta (Sharpe 0.92, DD 27%) e' molto sotto ogni sleeve
|
||||||
|
deployato → **NON inseribile**, in nessuna size.
|
||||||
|
|
||||||
|
Lezioni aggiuntive:
|
||||||
|
- Il torneo ha massimizzato esattamente l'artefatto: la convergenza su ETH 15m
|
||||||
|
range profondo era la firma del **wick harvesting**, non di un edge.
|
||||||
|
- I motori intrabar dei giochi (`grid_engine`, e in misura minore i TP intrabar
|
||||||
|
di `engine.py`) vanno SEMPRE stressati con una variante close-only prima di
|
||||||
|
promuovere qualunque vincitore: aggiungere il check ai prossimi giochi.
|
||||||
|
- Gli sleeve in produzione NON sono toccati da questo artefatto nello stesso modo:
|
||||||
|
pairs entra/esce sui close, le fade hanno EXIT-16 close-confirm sullo SL, e
|
||||||
|
soprattutto il ledger REAL-TRUTH usa i fill reali (che gli spike non fillano).
|
||||||
|
|
||||||
|
Artefatti: `scripts/games/grid_{engine,arena,brief}.py`, spec agenti in
|
||||||
|
`data/games/specs_grid/`, digest `data/games/grid_digests.json`, risultato
|
||||||
|
`data/games/grid_result.json`, log `data/games/grid_tournament.log`, gate
|
||||||
|
`scripts/analysis/grid_game_gate.py` (param `close_only` per lo stress anti-wick).
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 2026-06-10 — REAL-TRUTH: il ledger segue i fill reali (sim → diagnostica)
|
||||||
|
|
||||||
|
## Richiesta
|
||||||
|
|
||||||
|
L'utente vuole che sim e reale coincidano — o meglio, che il sim non esista come
|
||||||
|
verità: «voglio vedere dati che hanno avuto movimento reale su exchange». La
|
||||||
|
ricognizione dei 77 eventi reali (dal 2026-06-03) ha mostrato che il gap
|
||||||
|
sim/reale NON era slippage (piccolo, es. −0.15 bps su un TP) ma **contabile**:
|
||||||
|
|
||||||
|
1. **Ledger separati.** Il `capital` che guida portafoglio/ribilanci/sizing era
|
||||||
|
aggiornato dal PnL SIM; il PnL reale finiva in un `real_capital` parallelo che
|
||||||
|
non guidava nulla. MR01 BTC: sim +28.93 vs reale +1.98 sugli stessi 4 trade.
|
||||||
|
2. **Prezzi sim da candele testnet** (spike print, es. 2026-06-07 sim short BTC a
|
||||||
|
65266.5 con mark reale 62395): il sim bookava PnL che il reale non vede. Con
|
||||||
|
il ledger sim come verità, l'equity del portafoglio accumulava questa fantasia.
|
||||||
|
|
||||||
|
Nota: il *notional* reale era GIÀ derivato dalla formula sim
|
||||||
|
(`capital·ps·lev` passato a `_real_open`/`_real_open_pair`) — il punto 1 della
|
||||||
|
richiesta (sizing allineato) si chiude da solo una volta che `capital` è reale.
|
||||||
|
|
||||||
|
## Implementazione (REAL-TRUTH)
|
||||||
|
|
||||||
|
- `StrategyWorker` e `PairsWorker` accettano `real_truth: bool` (default False =
|
||||||
|
shadow storico). Con flag attivo e esecuzione abilitata:
|
||||||
|
- `_real_close`/`_real_close_pair` ritornano `(real_pnl, applied)`;
|
||||||
|
`applied=True` se ci sono fill reali (o chiusura verificata).
|
||||||
|
- `_close_position`/`_close` chiamano la chiusura reale PRIMA dell'update
|
||||||
|
ledger: `capital += real_pnl` (fee reali incluse); `is_win = pnl_reale > 0`.
|
||||||
|
- Il sim resta nel log CLOSE come diagnostica: `pnl_source` ("real" |
|
||||||
|
"sim_fallback"), `sim_pnl`, `real_pnl`.
|
||||||
|
- **Fallback al sim** SOLO se il trade reale non è mai esistito/fillato
|
||||||
|
(REAL_OPEN_FAIL, fill zero) — dichiarato nel log, mai silenzioso.
|
||||||
|
- Runner: `overrides.execution.real_truth` (yml) → `build_worker_for(...,
|
||||||
|
real_truth=)`. `portfolios.yml`: **`real_truth: true`**.
|
||||||
|
- Conseguenza a catena: equity ledger → pesi → allocazioni → notional dei
|
||||||
|
prossimi ordini derivano ora dai soldi veri sul conto. Il `real_capital`
|
||||||
|
parallelo resta come ledger puro-reale di confronto.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
`tests/portfolio/test_real_truth.py` (6 test): capital segue i fill reali
|
||||||
|
(single-leg e pairs), una divergenza sim-win/reale-loss viene contata come LOSS,
|
||||||
|
fallback sim dichiarato su REAL_OPEN_FAIL/leg-fail, e modalità shadow invariata
|
||||||
|
senza flag. Suite completa: 99 passed.
|
||||||
|
|
||||||
|
## Limiti onesti
|
||||||
|
|
||||||
|
- I multi-asset (TR01/ROT02/TSM01/XS01) restano sim per costruzione (paper
|
||||||
|
sleeves fuori dal pool, capitale insufficiente per i book multi-leg).
|
||||||
|
- La STORIA del ledger non è riscritta: l'equity attuale (~2154) ingloba il PnL
|
||||||
|
sim accumulato fino a oggi (reale realizzato era ≈ −15.6 dal 3/6). Da ora la
|
||||||
|
divergenza smette di accumularsi; se si vuole un azzeramento (reset equity al
|
||||||
|
conto reale) è un'operazione separata e deliberata.
|
||||||
|
- Le DECISIONI di trading (entry/exit) restano guidate dai prezzi del feed
|
||||||
|
(candele testnet): real-truth corregge la contabilità, non i segnali. Gli
|
||||||
|
spike print del feed possono ancora generare entry/exit subottimali — ma ora
|
||||||
|
il loro effetto si misura in PnL reale, non in PnL immaginario.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 2026-06-10 — XS01 dispersion-gate: PROMOSSO e LIVE (entry solo con dispersione da fare rientrare)
|
||||||
|
|
||||||
|
## Domanda
|
||||||
|
|
||||||
|
L'edge di XS01 (reversione cross-sectional 8 asset) era concentrato (2025 domina,
|
||||||
|
2023 quasi piatto) e cost-sensitive. La reversione cross-sezionale va accesa solo
|
||||||
|
quando c'e' dispersione da far rientrare?
|
||||||
|
|
||||||
|
## Metodo (anti multiple-testing): `scripts/analysis/xs01_dispersion_gate.py`
|
||||||
|
|
||||||
|
3 feature di regime CAUSALI calcolate dallo stesso panel closes (nessun feed
|
||||||
|
esterno): `g_disp` = std cross-section del momentum lb (la grandezza che si fada),
|
||||||
|
`g_corr` = correlazione media pairwise 72h (identita' della varianza dell'indice),
|
||||||
|
`g_vol` = vol BTC 168h. Diagnostica per quintili (quintili dal TRAIN, 70/30) sul
|
||||||
|
net per-trade dell'engine canonico NON gateato, TRAIN e OOS separati: si procede
|
||||||
|
solo con relazione monotona e concorde nelle due finestre.
|
||||||
|
|
||||||
|
## Esito diagnostica
|
||||||
|
|
||||||
|
- **g_disp: monotona e concorde** — Q1 NEGATIVO (−10 bps TRAIN / −8 OOS) →
|
||||||
|
Q5 +26/+280. Senza dispersione i trade sono solo fee. PROMOSSA.
|
||||||
|
- g_corr e g_vol: non monotone / segno incoerente → BOCCIATE (niente fishing).
|
||||||
|
|
||||||
|
## Gate (sweep soglie = percentili TRAIN, side dal TRAIN)
|
||||||
|
|
||||||
|
Plateau pieno p30-p70, niente picco: TRAIN Sh 1.51 → 2.0-2.3, OOS Sh 5.73 →
|
||||||
|
6.2-7.5. Scelta **p50 (disp_min = 0.0313)**, ~47% delle ore aperte:
|
||||||
|
|
||||||
|
- Standalone: trade 1427→859 (−40% turnover → meta' fee), win 50→53%,
|
||||||
|
**Sharpe 2.50→3.46**, DD 16.2→15.8%. **Ogni anno migliora**: 2022 +34→+40,
|
||||||
|
2023 +6→+28, 2024 +21→+44, 2025 +225→+237, 2026 +85→+108 — risolve la
|
||||||
|
concentrazione, il punto debole della validazione originale.
|
||||||
|
- Fee stress 2x (0.20% RT/book): OOS Sh 6.76 — la cost-sensitivity e' mitigata
|
||||||
|
(il gate taglia proprio i trade che pagavano fee senza edge).
|
||||||
|
- **Gate PORT06** (swap equity sleeve): FULL Sh 7.34→7.41 DD pari,
|
||||||
|
**OOS Sh 10.07→10.37 DD 1.48→1.47** → PROMOSSO (criterio standard).
|
||||||
|
|
||||||
|
## Implementazione (solo path LIVE, come trend/hurst sulle fade)
|
||||||
|
|
||||||
|
- `src/live/xsec_worker.py`: param opzionale `disp_min` (None = off), check in
|
||||||
|
`_open_book` su `nanstd(logC[i] − logC[i−lb])`. Default off → la validazione
|
||||||
|
`validate_xsec_worker` (replay == backtest) resta esatta.
|
||||||
|
- `src/portfolio/runner.py`: pass-through di `disp_min` (il runner costruiva il
|
||||||
|
dict params esplicitamente e l'avrebbe perso).
|
||||||
|
- `scripts/portfolios/_defs.py`: `disp_min: 0.0313` nella spec XS01.
|
||||||
|
- Il backtest canonico (`build_everything`) resta NON filtrato → il live fara'
|
||||||
|
meglio del backtest, coerente con le altre guardie.
|
||||||
|
|
||||||
|
Unit check: gate blocca panel piatto / apre panel disperso / default off invariato.
|
||||||
|
99/99 test. Nota macro della giornata: FC01 funding-carry SCARTATA
|
||||||
|
(diario separato) — il protocollo promuove ~1 idea su molte, come deve.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# 2026-06-11 — Sweep di stabilità su tutte le strategie (anti-overfit)
|
||||||
|
|
||||||
|
Obiettivo: analisi di tutte le 19 sleeve cercando miglioramenti/correzioni/protezioni che
|
||||||
|
aumentino la STABILITÀ, con disciplina anti-overfit (ipotesi pre-registrate, griglie fissate
|
||||||
|
prima di guardare i numeri, verdetti su TRAIN E OOS con plateau, gate PORT06; nessun re-test
|
||||||
|
di idee già bocciate: ADX/vol-target/time-stop/hurst/stop SH01/multi-TF/entry-guard ecc.).
|
||||||
|
|
||||||
|
## Audit di drift (nessun fitting)
|
||||||
|
|
||||||
|
- `report_families` sui dati correnti: correlazioni cross-famiglia ancora ≈0 (pairs 0.03-0.11,
|
||||||
|
XS01 0.01, SH 0.03-0.04 vs MASTER-9) → la diversificazione NON è driftata.
|
||||||
|
- **Regression-lock trend_max** (`trendmax_port06_impact`) rieseguito su dati freschi:
|
||||||
|
parità 1.00000 su tutte e 6 le fade (engine live-path integro) e **plateau trend_max
|
||||||
|
2.5/3.0/3.5 confermato** (OOS Sh 11.14/11.24/10.98, DD 1.33 identico).
|
||||||
|
- **Percentile del rolling-return PORT06** (cap weights, storia 2021+): finestra corrente
|
||||||
|
60g al 19°, 120g al 21°, 160g al 28° percentile → periodo fiacco ma in variazione normale.
|
||||||
|
- ⚠️ **FADE in coda storica**: il rolling 120g equal-weight delle 6 fade è a **−1.0% =
|
||||||
|
2° percentile** della propria storia (p5 = +0.4%). È il tratto peggiore mai attraversato
|
||||||
|
dalla famiglia. Decisione esplicita: NESSUN ritocco ai parametri (sarebbe fit sul regime
|
||||||
|
corrente, l'errore che il progetto evita da sempre); la protezione è la diversificazione
|
||||||
|
(già in atto: il portafoglio regge al 19-28° pct) + monitoraggio. Follow-up in TODO:
|
||||||
|
alert di drift per-famiglia nel hourly_report (distribuzione storica precomputata).
|
||||||
|
|
||||||
|
## Correzione: bug contabile TR01 worker (FIXATO)
|
||||||
|
|
||||||
|
`BasketTrendWorker.tick` usava `mean(rets)` sui SOLI asset in posizione → con paniere
|
||||||
|
parziale sovrappesa N/k (con 1 solo long: 0.45 del capitale invece di 0.09). Era l'origine
|
||||||
|
della divergenza replay −44% vs reference +42% annotata nel TODO. La convenzione canonica
|
||||||
|
(backtest PORT06 via `_tr_basket_daily`) è equal-weight 1/N sull'universo: fix 1 riga
|
||||||
|
(`sum(rets)/len(universe)`). Replay post-fix: **TR01 +32% vs reference +42%** (stesso segno
|
||||||
|
e ordine di grandezza = gate del validatore; il residuo è la differenza dichiarata
|
||||||
|
capitale-unico vs media-equity). ROT02 +171%==ref, TSM01 +5%==ref invariati. Solo
|
||||||
|
statistica PAPER: nessun effetto su pool/ordini. NB: il forming-bar su ROT02/TSM01 segnato
|
||||||
|
nel TODO era GIÀ fixato (v1.1.10, `_panel` scarta la barra in formazione) — TODO aggiornato.
|
||||||
|
|
||||||
|
## Protezione 1 (pre-registrata): disaster-cap z sui pairs → **NO-GO**
|
||||||
|
|
||||||
|
Ipotesi: exit immediata se |z| ≥ z_stop dopo l'ingresso taglia la coda da structural-break
|
||||||
|
senza toccare i trade normali. Griglia fissata: z_stop {3.0,3.5,4.0,5.0} × 5 coppie 1h +
|
||||||
|
{2.5,3.0,3.5,4.0} sul 15m, train <2023-11-01 / OOS, engine con regression-lock ESATTO su
|
||||||
|
`pairs_sim`/`pairs_sim_flat`. Esito (potere statistico AMPIO, centinaia di trigger):
|
||||||
|
**bocciata su tutti e 3 i criteri** — il DD peggiora quasi ovunque, il worst-trade OOS
|
||||||
|
peggiora su 4 coppie su 6 (ETH/BTC −62→−168%!), Sharpe OOS cala oltre il −10% relativo in
|
||||||
|
TUTTE le celle, e il "plateau" esiste solo per il danno (monotono: più stretto, peggio).
|
||||||
|
Meccanismi: (i) lo stop realizza la perdita al massimo overshoot — il movimento che la
|
||||||
|
strategia fada (stessa lezione EXIT-16/SH01, **5ª conferma**); (ii) l'engine non-overlap
|
||||||
|
rientra subito nello spread ancora divergente → churn di fee a 2 gambe e stop ripetuti.
|
||||||
|
I pairs restano senza stop by design; la mitigazione resta la taglia
|
||||||
|
(`position_size_family` PAIRS 0.20). Record: `scripts/analysis/pairs_zstop_research.py`.
|
||||||
|
|
||||||
|
## Protezione 2 (pre-registrata): phase-tranching XS01 → **PROMOSSA e LIVE**
|
||||||
|
|
||||||
|
Diagnosi: il roll non-sovrapposto di XS01 ha una FASE arbitraria (dipende da quando il
|
||||||
|
worker parte) e l'esito ne dipende parecchio — sulle 12 fasi possibili: Sharpe daily FULL
|
||||||
|
1.52-2.33, DD per-trade 13.8-33.1% (`xs01_tranche_research.py`). È timing-luck puro: il
|
||||||
|
backtest canonico (fase 0) è una delle estrazioni FORTUNATE sul FULL (DD 15.4 vs mediana
|
||||||
|
~21). Rimedio senza parametri fittati: **ensemble di fase** — K sub-book sfasati di hold/K
|
||||||
|
barre su capitale comune (PnL/K). Gate onesto su equity daily (`xs01_tranche_gate.py`):
|
||||||
|
|
||||||
|
- standalone OOS: Sharpe 3.79→4.27 (K=2) →4.85 (K=3), DD 7.99→7.25→5.56;
|
||||||
|
- FULL standalone: il DD sale verso la mediana di fase (~21%) — il "peggioramento" è la
|
||||||
|
RIMOZIONE della fortuna della fase 0, non un costo;
|
||||||
|
- PORT06 swap-sleeve: FULL invariato (7.33/3.46), OOS Sh 10.07→10.11 (K=2) →10.15 (K=3),
|
||||||
|
OOS DD 1.48→1.43→1.38. **Plateau: K=2 E K=3 entrambi promossi** (non best-pick).
|
||||||
|
|
||||||
|
Implementazione: `CrossSectionalWorker` param `tranches` (default 1 = storico; live K=3 in
|
||||||
|
`_defs.py`), books indipendenti con sfasamento iniziale `wait`, capitale comune, migrazione
|
||||||
|
automatica dello status legacy (il vecchio book → tranche 0), `last_bar_ts` solo-avanti
|
||||||
|
(robustezza ai panel accorciati da feed in ritardo). Solo path LIVE come `disp_min` (il
|
||||||
|
backtest canonico resta single-phase → il confronto live/backtest va letto con la mediana
|
||||||
|
di fase in mente). **Validatore esteso e PASSATO**: K=1 replay == `xsec_sim` ESATTO
|
||||||
|
(1427 trade, cap 4993==4993); K=3 replay == unione fasi 0/4/8 ESATTO (4279 trade,
|
||||||
|
4512==4512). Osservabilità: `hourly_report` aggrega i sub-book nel book medio.
|
||||||
|
|
||||||
|
## Non toccati (per evidenza, non per pigrizia)
|
||||||
|
|
||||||
|
- **Fade/DIP01**: exit-lab già esaustivo (23+11 famiglie); plateau e parità riconfermati oggi.
|
||||||
|
- **SH01**: 11 famiglie di stop già bocciate; cap famiglia + monitor trade-rate in essere.
|
||||||
|
- **ROT02/TSM01**: replay == reference; forming-bar già fixato.
|
||||||
|
- **TR01 come sleeve** (3 anni negativi su 6): rimosso? NO — sarebbe una decisione guidata
|
||||||
|
dal regime recente (recency bias); il suo ruolo è catturare i trend che le fade non
|
||||||
|
prendono. Resta, con la statistica ora contabilizzata correttamente.
|
||||||
|
|
||||||
|
## Esito test
|
||||||
|
|
||||||
|
`pytest`: 99 passed. Validatori: honest workers OK, xsec K=1/K=3 OK, trendmax lock OK.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# 2026-06-11 — Audit di sistema + verità contabile sul netting (v1.1.24)
|
||||||
|
|
||||||
|
Seconda tornata della giornata (dopo lo sweep strategie): analisi del SISTEMA live
|
||||||
|
(esecuzione, stato, feed, processo) con 3 indagini parallele + fix.
|
||||||
|
|
||||||
|
## Audit live (dal reset 2026-06-10T21:24Z)
|
||||||
|
|
||||||
|
- **Churn phantom-TP quantificato**: 20 round-trip fantasma sui fade ETH (2 finestre,
|
||||||
|
13:06Z e 17:32-17:58Z, feed che stampava wick ~1640 con mercato a 1675-90 per 25 min).
|
||||||
|
Somma real_pnl −2.35 USD vs sim_pnl +80 USD: il real-truth ledger ha contenuto, il gate
|
||||||
|
TP_PHANTOM (v1.1.23) copre il pattern da stasera.
|
||||||
|
- **Il conto NON quadrava coi libri**: short 0.027 ETH in più (l'hedge long ETH del pair
|
||||||
|
ETH/SOL mangiato dai close reduce-only dei fade) e BTC flat con MR02_BTC convinto di
|
||||||
|
essere short (TP resting fillato da uno spike REALE del book a −3.8%, +6.6$ non bookati,
|
||||||
|
che il worker riconcilierà alla chiusura sim) col **disaster-SL residuo sul book a
|
||||||
|
posizione flat**. Bonifica eseguita: DSL `USDC-SLMB-26521` cancellato, riallineo
|
||||||
|
+0.027 ETH reduce-only (fill 1682.5) → conto == libri (verificato).
|
||||||
|
|
||||||
|
## Root-cause strutturale
|
||||||
|
|
||||||
|
Le **quote per-worker con ordini reduce-only su un conto a NETTING si rompono quando due
|
||||||
|
worker hanno direzioni opposte sullo stesso strumento** (pairs long ETH vs fade short ETH):
|
||||||
|
- un close reduce-only può essere **cappato** (Deribit riduce l'amount in silenzio) → il
|
||||||
|
ledger bookava la chiusura PIENA perché `Fill.amount` era il richiesto, non il fillato;
|
||||||
|
- un close reduce-only nel verso "sbagliato" rispetto al netto viene **respinto** → la
|
||||||
|
gamba pairs resta orfana sul conto, ma il worker bookava il PnL al prezzo sim e azzerava
|
||||||
|
lo stato (3 volte oggi: PnL fantasma nel ledger real-truth, ETH/SOL di fatto short nudo).
|
||||||
|
|
||||||
|
## Fix (v1.1.24) — verità, non workaround
|
||||||
|
|
||||||
|
1. **`Fill.filled_amount`** (da `order.filled_amount`, fallback trades/history): tutti i
|
||||||
|
ledger usano il fillato; nota "FILL PARZIALE" nel Fill.
|
||||||
|
2. **`REAL_CLOSE_PARTIAL`** (log + Telegram): close che filla meno del residuo → residuo
|
||||||
|
orfano dichiarato, `REAL_CLOSE verified=false`.
|
||||||
|
3. **Pairs per-gamba**: PnL bookato SOLO per gambe con fill verificato; gamba respinta →
|
||||||
|
record in `orphan_legs` (persistito nello status) + alert `PAIR_LEG_ORPHAN`;
|
||||||
|
`applied=True` (real-truth) solo con ENTRAMBE le gambe → altrimenti fallback sim
|
||||||
|
DICHIARATO (meglio del numero mezzo-reale di prima).
|
||||||
|
4. **`REAL_DIVERGENCE` anche su jsonl** (prima solo Telegram: l'audit ha dovuto
|
||||||
|
ricostruire gli episodi dai REAL_CLOSE).
|
||||||
|
5. **Runner: tick isolato per-worker** — un'eccezione in un worker non salta più gli
|
||||||
|
altri né l'update equity; streak per-worker con alert `WORKER_ERROR_STREAK` a 5.
|
||||||
|
|
||||||
|
Test: 2 nuovi (partial-close, orphan-leg) + fixture aggiornate → 106 passed.
|
||||||
|
|
||||||
|
## Decisione di design APERTA (per l'utente)
|
||||||
|
|
||||||
|
Finché pairs e fade condividono strumenti in direzioni opposte, le chiusure possono
|
||||||
|
orfanizzarsi: ora è VISIBILE e contabilizzato giusto, ma non eliminato. Opzioni:
|
||||||
|
A) **Position-manager centrale per strumento** (il runner netta i delta di tutti i
|
||||||
|
worker e manda UN ordine; ricostruisce le quote contabilmente) — corretto ma
|
||||||
|
invasivo, da progettare con calma;
|
||||||
|
B) **Sotto-conti Deribit separati** per famiglia (pairs vs fade) — pulito, richiede
|
||||||
|
setup conto e client multi-token;
|
||||||
|
C) **Status quo monitorato**: alert PAIR_LEG_ORPHAN + riallineo manuale (oggi: 1 giro
|
||||||
|
in 2 minuti) — accettabile su testnet, NON per capitale vero.
|
||||||
|
Raccomandazione: C ora, A prima di passare a capitale reale.
|
||||||
|
|
||||||
|
**Secondo passo — FATTO (v1.1.25): NETTING delle chiusure market.** `close_amount`
|
||||||
|
tenta il reduce-only e riesegue il residuo cappato/respinto in market puro (= il
|
||||||
|
netting contro le quote opposte: il conto si muove del delta esatto del libro).
|
||||||
|
Un solo punto di fix (anche `close_pair` ci passa). Fill combinato per il chiamante
|
||||||
|
(prezzo pesato, fee sommate), evento `NET_CLOSE` su log+Telegram a ogni fallback,
|
||||||
|
4 test dedicati. Niente più orfani per costruzione; `orphan_legs` resta come ultima
|
||||||
|
difesa se fallisce anche il market puro. Effetto collaterale benefico: la chiusura
|
||||||
|
futura della gamba ETH di ETH_SOL#2 (che sarebbe stata respinta di nuovo) ora
|
||||||
|
eseguirà correttamente. La scelta A-vs-B-vs-C resta aperta solo per la parte
|
||||||
|
RESTING (TP/DSL su book condiviso) e per i multi-asset.
|
||||||
|
|
||||||
|
**Primo passo verso A — FATTO (sera stessa): reconciler read-only.**
|
||||||
|
`scripts/analysis/reconcile_account.py`: per ogni strumento USDC confronta
|
||||||
|
atteso (Σ quote reali dai status.json: single-leg + pairs 2 gambe + orphan_legs
|
||||||
|
registrati = drift SPIEGATO) vs conto reale (`get_positions`, size/mark → coin),
|
||||||
|
tolleranza 1.5×step, anti-race (ricontrollo a 10s prima di segnalare). In crontab
|
||||||
|
host ORARIO (:40) con alert Telegram `ACCOUNT_DRIFT`. Al primo run ha beccato un
|
||||||
|
vero positivo: BTC libro short 0.0028 vs conto flat (il TP di MR02_BTC fillato
|
||||||
|
dallo spike reale delle 12:22Z, che il worker riconcilierà alla chiusura sim) —
|
||||||
|
esattamente la classe di divergenza che prima restava invisibile per ore.
|
||||||
|
|
||||||
|
## Altri esiti della tornata
|
||||||
|
|
||||||
|
- **Dedup engine gate** (TODO chiuso): `_port06_gate_common.py`, output 3 gate
|
||||||
|
byte-identici, nessun copy-drift trovato (la paura era fondata ma non ancora avverata).
|
||||||
|
- **Bug bfill `_daily_equity` quantificato** (TODO aggiornato): NON materiale — OOS
|
||||||
|
invariato per costruzione, FULL DD 3.46→3.67 col fix (l'attuale è lievemente ottimista),
|
||||||
|
nessun verdetto di gate a rischio. Lasciato documentato.
|
||||||
|
- **Drift monitor in produzione**: `drift_monitor.py` in crontab host (07:15 UTC,
|
||||||
|
Telegram): rolling 60/120g per famiglia vs distribuzione storica propria, warn < p5.
|
||||||
|
Oggi: FADE 120g al p2 (coda storica, nessun intervento), resto normale, XSEC p84.
|
||||||
|
- **Dati cerbero-bite refreshati**: catena opzioni a ~153k righe/asset (fino a oggi
|
||||||
|
19:30), pannello regime denso (net-GEX 644/673 ultimi 7g). Sempre un solo regime:
|
||||||
|
niente validazione edge, valore forward.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 2026-06-12 — ACCEL50: cosa accelera davvero verso €50/giorno
|
||||||
|
|
||||||
|
**Domanda.** Quali strategie/leve accorciano il tempo per arrivare a €50/g da ~€2k?
|
||||||
|
Script: `scripts/analysis/accel50_research.py`.
|
||||||
|
|
||||||
|
## Il quadro onesto
|
||||||
|
|
||||||
|
A €2k, anche col CAGR OOS del PORT06 (~111% a lev 2), il PnL atteso è ~€4/g: il
|
||||||
|
collo di bottiglia NON è l'edge (Sharpe OOS 10), è la **taglia**. Le vie testate,
|
||||||
|
in ordine di impatto:
|
||||||
|
|
||||||
|
## 1. LEVA — l'acceleratore dominante (nessuna ricerca nuova)
|
||||||
|
|
||||||
|
Frontiera su daily return canonici PORT06 (scala lineare, fee pro-quota):
|
||||||
|
|
||||||
|
| lev | CAGR full | DD full | CAGR OOS | DD OOS | anni a €50/g da 2k |
|
||||||
|
|-----|-----------|---------|----------|--------|--------------------|
|
||||||
|
| 2 (attuale) | 74% | 3.5% | 111% | 1.5% | 3.3 |
|
||||||
|
| 3 | 128% | 5.2% | 206% | 2.2% | 1.9 |
|
||||||
|
| 4 | 200% | 6.9% | 343% | 2.9% | 1.2 |
|
||||||
|
| 5 | 293% | 8.6% | 539% | 3.7% | 0.9 |
|
||||||
|
|
||||||
|
Anche scontando l'OOS del 50% (regime calmo, caveat noto), lev 3-4 dimezza i
|
||||||
|
tempi tenendo il DD sotto il 10%. Caveat: il modello è lineare — non cattura
|
||||||
|
margine, code grasse, slippage che cresce col notional, e gli sleeve senza stop
|
||||||
|
(PAIRS/SH01) a leva alta hanno code peggiori del modello. Proposta sobria:
|
||||||
|
**lev 2→3 subito** (DD full 5.2% = ancora metà del PORT02 storico), rivalutare
|
||||||
|
4 dopo un mese di ledger reale pulito.
|
||||||
|
|
||||||
|
## 2. FADE 15m — la candidata nuova che PASSA il probe
|
||||||
|
|
||||||
|
MR01/02/07 a 15m, parametri live 1h non ri-tunati (anti-overfit), fee 0.10% RT:
|
||||||
|
|
||||||
|
- **Tutti e 6 gli sleeve positivi**, OOS 2025-26 positivo ovunque, fee 2x OK
|
||||||
|
(Sh 1.6-2.9 — margine ampio).
|
||||||
|
- **BTC 15m domina il suo 1h**: MR01 Sh 3.37 vs 2.76 con META' del DD
|
||||||
|
(15.1% vs 31.7%); MR02 3.49 vs 3.29 (DD 10.8 vs 18.7).
|
||||||
|
- ETH 15m leggermente sotto il 1h in Sharpe ma OOS molto più grande in valore
|
||||||
|
assoluto (4x trade = compounding più veloce; MR02 +22052 vs +9560).
|
||||||
|
- Infrastruttura live 15m GIA' esistente (fetch sub-orario del BLEND pairs).
|
||||||
|
|
||||||
|
**Prossimi passi obbligati prima del deploy** (metodologia standard):
|
||||||
|
gate PORT06 con correlazione 15m↔1h (se ~1 sostituire, se bassa aggiungere),
|
||||||
|
griglia parametri al 15m, validazione worker, caveat flat ETH 15m (14-30%
|
||||||
|
storico — per le fade single-leg il fill flat è meno tossico che per i pairs,
|
||||||
|
ma va guardato con flat-aware engine).
|
||||||
|
|
||||||
|
## 3. PAIRS nuove — BOCCIATE (stale-print illusion, di nuovo)
|
||||||
|
|
||||||
|
Sweep onesto delle 19 coppie mai testate (config universale pre-registrata
|
||||||
|
n=50 z2.0/0.5 max72): 8 candidate con Sh 1.5-4.3... MA le gambe alt hanno
|
||||||
|
88-98% barre flat (ADA 98%, LTC 97%, DOGE 91%, XRP 88%, BNB 88%) e con
|
||||||
|
`flat_skip=True` muoiono quasi tutte (BTC/ADA 4.33→0.17, ETH/DOGE 3.79→0.46;
|
||||||
|
migliore superstite ETH/XRP 1.34 < le 5 deployate). Identica classe di
|
||||||
|
illusione del XEX su DOGE/SOL (stessa giornata, `xex_divergence_research.py`).
|
||||||
|
**PAXG idem**: 92% flat su Deribit → chiuso anche il ramo "oro".
|
||||||
|
|
||||||
|
NB: questo getta anche una luce nuova sulle gambe alt dei pairs GIA' deployati
|
||||||
|
(ADA/LTC/SOL hanno flat share altissime nel parquet) — il loro ledger reale
|
||||||
|
shadow è il banco di prova giusto e finora regge, ma teniamolo d'occhio.
|
||||||
|
|
||||||
|
## 4. CAPITALE — domina tutto
|
||||||
|
|
||||||
|
A config attuale: €50/g ≈ €24k di capitale. Ogni € aggiunto accorcia
|
||||||
|
linearmente; nessuna ricerca batte un deposito. In più, a ~€20k si sbloccano
|
||||||
|
in esecuzione reale i 4 book multi-asset oggi solo paper (TR01/ROT02/TSM01/XS01).
|
||||||
|
|
||||||
|
## Sintesi operativa
|
||||||
|
|
||||||
|
Ordine d'impatto: **capitale > leva (2→3) > fade 15m (da gateare) >>** tutto il
|
||||||
|
resto. Le vie "nuova strategia esotica" (pairs nuove, PAXG, XEX) sono tutte
|
||||||
|
morte oggi sotto il test di esecuzione realistica — la lezione del giorno è che
|
||||||
|
su questo testnet ogni edge va validato col **book/flat-aware engine** prima di
|
||||||
|
crederci.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 2026-06-12 — FADE TF SWEEP: 1m / 2m / 5m / 10m / 30m (post-swap 15m)
|
||||||
|
|
||||||
|
Richiesta utente: estendere l'analisi timeframe dei fade oltre il 15m appena
|
||||||
|
deployato (v1.1.30). Script: `scripts/analysis/fade_tf_sweep.py`.
|
||||||
|
Dati: parquet locale (5m/15m/30m full-history; 10m = resample dal 5m, unit-safe);
|
||||||
|
1m/2m da Cerbero (120 giorni recenti — la storia 1m locale non esiste: esclusa
|
||||||
|
dal refresh notturno per costo, 2m/10m non sono intervalli nativi del v2).
|
||||||
|
|
||||||
|
## A. Storia completa (engine canonico, OOS da 2024-10, fee 0.10% RT)
|
||||||
|
|
||||||
|
OOS Sharpe per timeframe (e OOS Sharpe a fee 2x del peggiore):
|
||||||
|
|
||||||
|
| tf | MR01_BTC | MR02_BTC | MR07_BTC | MR01_ETH | MR02_ETH | MR07_ETH | worst f2x |
|
||||||
|
|----|---------|---------|---------|---------|---------|---------|-----------|
|
||||||
|
| 5m | 3.66 | 1.90 | 4.12 | 5.31 | 6.54 | 5.52 | **MR02_BTC −1.70** |
|
||||||
|
| 10m | 2.62 | 2.69 | 3.31 | 5.32 | 6.49 | 5.59 | MR02_BTC 0.32 |
|
||||||
|
| 15m (live) | 1.94 | 2.30 | 2.37 | 4.94 | 6.40 | 4.44 | MR02_BTC 0.60 |
|
||||||
|
| 30m | 1.35 | 2.32 | 1.56 | 3.25 | 5.23 | 2.81 | MR02_BTC 1.40 |
|
||||||
|
|
||||||
|
**La frontiera è monotona**: più il tf scende, più Sharpe sale (MR01/MR07)… e più
|
||||||
|
il margine fee si assottiglia. A fee 2x MR02_BTC muore a 5m e resta fragile a 10m.
|
||||||
|
MR02 (donchian) fa 3-6x i trade degli altri: è la strategia più esposta al churn.
|
||||||
|
|
||||||
|
## B. Finestra comune recente (2026-02-12 → 06-12, il regime CORRENTE)
|
||||||
|
|
||||||
|
- **MR02 sotto i 15m è un disastro**: 1m −64%, 2m −44%, 5m −22% (fee-death).
|
||||||
|
- **MR01 a 1m brilla** (ETH +60.6%, Sh 5.7; BTC +33.5%) ma **muore a fee 2x**
|
||||||
|
(unico sopravvissuto MR01_ETH +16.5%): margine troppo sottile per fidarsi.
|
||||||
|
- Flat share a 1m: ETH 25.6%, BTC 13.3% → rischio stale-print alto (la lezione
|
||||||
|
del giorno: pairs-alt/XEX/PAXG).
|
||||||
|
- Il regime recente è CALMO: anche il 5m vi è fiacco (+4.8/−22.9/+3.7 BTC).
|
||||||
|
I tf veloci pagano nella volatilità, non nella calma — il loro vantaggio
|
||||||
|
full-history viene dai regimi mossi (2021-22, 2024).
|
||||||
|
|
||||||
|
## C. Correlazione col 15m live (daily, storia completa)
|
||||||
|
|
||||||
|
5m↔15m media **0.46**, 10m↔15m media **0.53** (range 0.28-0.81). Diversificazione
|
||||||
|
parziale: un eventuale ADD del 10m avrebbe senso ma è meno pulito del salto
|
||||||
|
1h→15m (che era a 0.26).
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
- **1m / 2m: CHIUSI.** Fee-margin nullo a stress, microstruttura flat pesante,
|
||||||
|
validazione full-history impraticabile. Non deployare mai MR02 sotto i 15m.
|
||||||
|
- **5m: no-swap.** L'edge c'è ma MR02_BTC muore a fee 2x — viola il criterio di
|
||||||
|
robustezza fee che tutte le strategie deployate rispettano.
|
||||||
|
- **10m: in WATCHLIST.** Quasi l'edge del 5m con più margine (f2x 0.32 resta
|
||||||
|
sotto la soglia di comfort per MR02_BTC; MR01/MR07 reggono bene). Possibile
|
||||||
|
ADD selettivo (solo MR01/MR07?) da gateare su PORT06 più avanti — NON ora:
|
||||||
|
il 15m è live da poche ore, un cambio alla volta e si lascia parlare il
|
||||||
|
ledger reale.
|
||||||
|
- **15m: confermato** come ginocchio della frontiera margine-fee/rendimento.
|
||||||
|
|
||||||
|
Collaterale tecnico: bug di resample scoperto e fixato nello sweep — pandas 2.x
|
||||||
|
conserva `datetime64[ms]` da `to_datetime(unit="ms")`, quindi `.view(int64)//10**6`
|
||||||
|
divide due volte e manda i timestamp nel 1970 (equity piatta silenziosa). Usare
|
||||||
|
`(index - EPOCH) // pd.Timedelta(milliseconds=1)`.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 2026-06-12 — GATE PORT06: fade 15m PROMOSSI (tutte e tre le varianti)
|
||||||
|
|
||||||
|
Seguito del probe ACCEL50: i 6 sleeve fade (MR01/02/07 × BTC/ETH) a 15m,
|
||||||
|
parametri live 1h NON ri-tunati, engine canonico `build_trades`/`fade_daily_equity`
|
||||||
|
parametrizzato sul timeframe. Script: `scripts/analysis/fade15m_port06_gate.py`.
|
||||||
|
|
||||||
|
## Risultati
|
||||||
|
|
||||||
|
**[1] Parità.** Builder locale a 1h == sleeve canonico, diff 0.00 esatto su tutti e 6.
|
||||||
|
|
||||||
|
**[2] Standalone (daily, pos 0.15 lev 3, fee 0.10% RT).** Il 15m batte il twin 1h
|
||||||
|
quasi ovunque; OOS Sharpe: MR01_ETH 4.94 (vs 1.10), MR02_ETH 6.40 (vs 4.72),
|
||||||
|
MR07_ETH 4.44 (vs 1.97), MR07_BTC 2.37 (vs 1.59). Fee 2x: regge ovunque tranne
|
||||||
|
MR02_BTC (OOS 0.60 — il piu' fee-sensitive, ma a fee reali e' il migliore BTC).
|
||||||
|
|
||||||
|
**[3] Correlazione 15m↔1h: media 0.26** (range 0.16-0.41) — più decorrelato del
|
||||||
|
pairs 15m promosso a 0.37. NON è la stessa scommessa più veloce: è un edge a
|
||||||
|
orizzonte diverso.
|
||||||
|
|
||||||
|
**[4] Gate PORT06** (cap PAIRS 0.33 / SHAPE 0.0588):
|
||||||
|
|
||||||
|
| variante | FULL CAGR | FULL DD | FULL Sh | OOS CAGR | OOS DD | OOS Sh |
|
||||||
|
|----------|-----------|---------|---------|----------|--------|--------|
|
||||||
|
| BASELINE (1h) | 74% | 3.46% | 7.34 | 111% | 1.48% | 10.07 |
|
||||||
|
| **ADD (+6 sleeve 15m)** | 85% | **2.73%** | 8.02 | 125% | **1.38%** | **10.48** |
|
||||||
|
| SWAP (15m al posto 1h) | **101%** | 2.47% | 8.13 | **163%** | 2.09% | 10.86 |
|
||||||
|
| BLEND 50/50 | 87% | 2.31% | 8.06 | 136% | 1.77% | 10.82 |
|
||||||
|
|
||||||
|
Tutte e tre PROMOSSE dal criterio standard (OOS Sharpe non peggiora E DD scende).
|
||||||
|
|
||||||
|
**[5] Anti-illusione flat (il check che ha ucciso pairs-alt e XEX/DOGE).**
|
||||||
|
ETH 15m ha 14-30%/anno di candele flat. Con flat-entry-skip l'edge SOPRAVVIVE:
|
||||||
|
MR01_ETH Sh 3.11→2.85 (OOS 4.94→4.60), MR02_ETH 4.72→4.63, MR07_ETH 3.76→3.55;
|
||||||
|
BTC invariato (≤0.01). Coerente col finding registrato 2026-05-28. La quota di
|
||||||
|
ritorno persa (~30-40% del FULL ETH) è il caveat slippage onesto: il ledger
|
||||||
|
reale shadow sarà il giudice, come per il pairs 15m.
|
||||||
|
|
||||||
|
## Raccomandazione
|
||||||
|
|
||||||
|
**ADD** è la promozione più pulita: migliora TUTTE le metriche senza buttare il
|
||||||
|
track record live dei 6 sleeve 1h (SWAP rinuncia alla config validata dal vivo;
|
||||||
|
il suo OOS DD peggiora 1.48→2.09). In subordine BLEND (miglior FULL DD 2.31%).
|
||||||
|
|
||||||
|
## Per il deploy (non fatto qui)
|
||||||
|
|
||||||
|
1. `_defs.py`: 6 SleeveSpec nuovi `MR0x_{asset}_15M` con `tf: 15m` nei params.
|
||||||
|
2. Runner: estendere il fetch sub-orario (già esistente per PR_ETHBTC_15M) ai
|
||||||
|
fade 15m; verificare lookback (EMA200 a 15m = 50h → bastano ~2200 barre).
|
||||||
|
3. Validazione worker a 15m (replay == backtest, come validate_worker_pairs).
|
||||||
|
4. Sizing: a peso pieno i 6 sleeve 15m raddoppiano la famiglia FADE; valutare
|
||||||
|
position_size ridotto come il blend pairs (il gate ADD qui è già a peso pieno
|
||||||
|
e migliora comunque, ma il caveat slippage 15m suggerisce prudenza).
|
||||||
|
5. MR02_BTC 15m è il più fee-sensitive (fee2x OOS 0.60): monitorare le fee reali.
|
||||||
|
|
||||||
|
## Addendum — DEPLOY: SWAP SECCO (stessa giornata, scelta utente)
|
||||||
|
|
||||||
|
L'utente ha scelto **SWAP** (la variante a CAGR più alto), consapevole dei due
|
||||||
|
costi discussi: OOS DD 1.48→2.09% e perdita della maturità operativa 1h (la
|
||||||
|
meccanica live — EXIT-16 confirm, TP_PHANTOM, TP resting — gira 4x più veloce
|
||||||
|
su un feed testnet con wick anomali; il gate misura l'edge, non la robustezza
|
||||||
|
operativa). Proposta alternativa staged (ADD temporaneo 1-2 settimane, poi
|
||||||
|
spegnere il 1h) declinata: "swap secco".
|
||||||
|
|
||||||
|
Implementazione (deliberatamente minima, sid INVARIATI → pesi/alloc/epoche
|
||||||
|
intatti):
|
||||||
|
- `_defs.py`: FADE specs → `tf="15m"` (DIP01 resta 1h: non era nel gate).
|
||||||
|
- `combine_portfolio.py`: `FADE_TF="15m"` nel builder canonico → le due facce
|
||||||
|
(backtest/live) restano sulla STESSA definizione; il lock di parità
|
||||||
|
(`test_backtest_parity`) confronta le facce fra loro e resta verde.
|
||||||
|
- Runner: NESSUNA modifica (il fetch sub-orario BTC/ETH 15m esisteva già per il
|
||||||
|
blend pairs; `_spec_assets_tf`/`_series_for` sono generici).
|
||||||
|
- Worker: nuove dir `*__15m` (stato 1h preservato in `*__1h`, worker flat al
|
||||||
|
momento dello switch). Esecuzione reale invariata (match per nome MR01/02/07).
|
||||||
|
- max_bars=24 barre ora = 6h; EXIT-16 confirm sulla barra 15m completata.
|
||||||
|
|
||||||
|
Monitor post-deploy: stop-rate e divergenza sim/reale dei fade 15m
|
||||||
|
nell'hourly report; fee reali su MR02_BTC; STALE_FEED (le barre flat 15m
|
||||||
|
sono più frequenti che a 1h).
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 2026-06-12 — Sweep migliorie/strategie nuove (sera, post-swap 15m)
|
||||||
|
|
||||||
|
Richiesta: cercare altre migliorie da implementare o strategie nuove. Tre
|
||||||
|
esperimenti + una miglioria di codice.
|
||||||
|
|
||||||
|
## 1. INIT_LINEAGE — eredità capitale al cambio timeframe (IMPLEMENTATO)
|
||||||
|
|
||||||
|
`StrategyWorker._inherit_lineage_capital`: al primo avvio (niente status.json)
|
||||||
|
il worker eredita `capital`/`real_capital` dal worker più recente di stessa
|
||||||
|
strategia+asset su altro tf (glob `{strategy}__{asset}__*`). MAI la posizione.
|
||||||
|
Nato dallo swap 1h→15m di oggi: i worker nuovi partivano dall'allocazione del
|
||||||
|
pool scartando il PnL del gemello (−16.8 di equity fantasma, riallineata a mano
|
||||||
|
col seed). Il prossimo swap non avrà bisogno di seed manuale.
|
||||||
|
Test: `tests/portfolio/test_capital_lineage.py` (eredita / no-sibling / resume).
|
||||||
|
|
||||||
|
## 2. Gate 10m ADD selettivo (MR01/MR07) — BOCCIATO, watchlist chiusa
|
||||||
|
|
||||||
|
Baseline aggiornata al PORT06 post-swap (fade 15m): ADD di 4 sleeve 10m
|
||||||
|
(MR01/MR07 × BTC/ETH, MR02 escluso perché fee-fragile) dà FULL Sh 8.13→8.33 e
|
||||||
|
DD 2.47→2.28, ma **OOS Sharpe 10.86→10.76** → fallisce il criterio standard.
|
||||||
|
Il 15m cattura già quasi tutto l'alpha veloce (corr 10m↔15m 0.53). Chiuso.
|
||||||
|
|
||||||
|
## 3. XSEC breadth (universo 8 → 14/15) — direzione GIUSTA, venue SBAGLIATO
|
||||||
|
|
||||||
|
La breadth è la leva classica delle strategie cross-sectional. Due banchi:
|
||||||
|
|
||||||
|
- **Hyperliquid 15 coin** (dati REALI, ma profondità v2 limitata a ~207 giorni,
|
||||||
|
regime calmo recente): CORE-8 **−6.2%** (Sh −1.38, coerente col
|
||||||
|
dispersion-gate live che tiene XS01 fuori in questo regime) vs FULL-15
|
||||||
|
**+9.4%** (Sh 1.58, WR 41→51%). La breadth trasforma un book perdente in
|
||||||
|
vincente — **sui prezzi veri**.
|
||||||
|
- **Deribit 14 coin** (storia piena 2022-10→2026-06, +AVAX/DOT/TRX/LINK/BCH/UNI,
|
||||||
|
parity del sim verificata ESATTA vs `xsec_sim`): FULL Sh 1.48→**1.22**, OOS
|
||||||
|
4.66→**3.41**, fee 2x da −0.33 a **−1.45**. PEGGIO dell'8: le 6 gambe nuove
|
||||||
|
hanno chiusure flat al **91-99%** e il loro "momentum" è rumore stale che
|
||||||
|
corrompe il ranking cross-section (3ª conferma della lezione del giorno:
|
||||||
|
pairs-alt, XEX/DOGE, ora XSEC).
|
||||||
|
|
||||||
|
**Conclusione:** l'espansione dell'universo XS01 è promettente ma bloccata dalla
|
||||||
|
qualità dati del venue. Sblocco strategico: **routing dati Hyperliquid** nel
|
||||||
|
runner (il client v2 già supporta `exchange="hyperliquid"`) + accumulo storia
|
||||||
|
HL in avanti. Rivalutare quando HL avrà ≥12-18 mesi di storia utile.
|
||||||
|
|
||||||
|
## Direzioni aperte residue (non attaccate oggi)
|
||||||
|
|
||||||
|
- **Put settimanale standing** (catastrofe-cap): unica struttura opzioni
|
||||||
|
eseguibile, da gateare coi premi reali cerbero-bite (~1%/mese il 10% OTM).
|
||||||
|
Harness `option_overlay_lab.py` pronto.
|
||||||
|
- **Hyperliquid come venue di esecuzione** (oltre che dati): aprirebbe fades su
|
||||||
|
alt liquidi con fill realistici; lavoro infrastrutturale grosso.
|
||||||
|
- 10m fade: chiuso oggi. 1m/2m/5m: chiusi oggi. Pairs nuove/PAXG/XEX: chiusi oggi.
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# 2026-06-12 — Reconcile resting + guard feed↔book + epoca report (v1.1.27)
|
||||||
|
|
||||||
|
## Contesto: la notte ha validato (di nuovo) il tema osservabilità
|
||||||
|
|
||||||
|
Due eventi nelle 24h hanno motivato la tornata:
|
||||||
|
|
||||||
|
1. **MR02_BTC (06:00):** il TP resting LIMIT ha fillato sul **book reale** a 60481
|
||||||
|
nella notte (+6.55 USD reali) mentre il **feed sim** non è mai sceso sotto 63285
|
||||||
|
(−443 bps di divergenza, sim chiuso a time_limit con −1.25). Il disaster-SL è
|
||||||
|
risultato `order_not_found` alla cancel (probabile auto-cancel a posizione flat).
|
||||||
|
Caso SPECULARE del TP_PHANTOM: lì il feed stampa wick che il book non ha; qui il
|
||||||
|
book si muove e il feed non lo vede. Real-truth ha bookato il reale (corretto),
|
||||||
|
ma il sistema se n'è accorto solo ORE dopo, al close sim.
|
||||||
|
2. **Incidente venue (dalle ~09:47):** il conto Deribit testnet ha iniziato a
|
||||||
|
rifiutare ordini (`locked_by_admin`, `invalid_reduce_only_order`) e le posizioni
|
||||||
|
sul conto sono cambiate da sole (ETH LONG 0.141 con avg 1849.5 — prezzo vecchio
|
||||||
|
di settimane): **rollback/reset admin del testnet**, non un nostro bug. Difese OK:
|
||||||
|
`REAL_CLOSE_PARTIAL`, leg-fail con unwind, orfani registrati (ETH −0.026,
|
||||||
|
SOL +0.7), `sim_fallback` dichiarato. MA: il reconciler delle 11:40 è **morto in
|
||||||
|
silenzio** su un 502 — il guardiano che non suona è indistinguibile dal tutto-ok.
|
||||||
|
|
||||||
|
## Fatto
|
||||||
|
|
||||||
|
### 1. Reconcile degli ordini RESTING (cron :40, read-only)
|
||||||
|
|
||||||
|
- **cerbero-mcp 9a74052:** nuovo endpoint Deribit `get_open_orders`
|
||||||
|
(`private/get_open_orders_by_currency`; per i trigger untriggered interrogare
|
||||||
|
anche `type='trigger_all'` e merge per order_id). Pattern già presente per
|
||||||
|
Hyperliquid/IBKR. Rebuild + smoke su testnet (book vuoto == stato atteso).
|
||||||
|
- **`books.expected_resting()`:** TP/DSL attesi dai libri dei worker single-leg
|
||||||
|
in posizione reale (i pairs non hanno resting).
|
||||||
|
- **`reconcile_account.compute_resting_drift()`** — tre classi:
|
||||||
|
`FILLED_UNBOOKED` (atteso non in book + fill nel trade history col worker ancora
|
||||||
|
in posizione = il caso MR02), `MISSING` (atteso, non in book, zero fill — per il
|
||||||
|
DSL triggered il fill ha order_id NUOVO → appare qui e il drift posizioni
|
||||||
|
completa), `STALE` (in book con label di un nostro worker ma nessun libro:
|
||||||
|
fillerebbe a sorpresa). Anti-race come per le posizioni; alert `RESTING_DRIFT`.
|
||||||
|
- **`RECONCILE_FAIL`:** il main è ora guardato — su eccezione alert Telegram +
|
||||||
|
exit 2 (fix del crash silenzioso delle 11:40).
|
||||||
|
|
||||||
|
### 2. Guard FEED_BOOK_GAP nel runner
|
||||||
|
|
||||||
|
`_check_feed_book_gap`: ad ogni poll confronta il close della candela in corso col
|
||||||
|
**mark dello strumento d'ESECUZIONE** (USDC, `get_ticker_batch`, 1 chiamata);
|
||||||
|
oltre soglia (`overrides.feed_book_gap_bps`, default 150) → alert per episodio,
|
||||||
|
recovery con isteresi a soglia/2, fail-open su errori di rete. Le decisioni
|
||||||
|
restano sul feed (il sim è la verità che guida): questo dice solo QUANDO i fill
|
||||||
|
reali possono divergere dal sim. Avrebbe segnalato il caso MR02 in tempo reale.
|
||||||
|
|
||||||
|
### 3. Epoca v1.1.26 nell'hourly_report
|
||||||
|
|
||||||
|
I conteggi CHIUSI cumulativi erano inquinati dal churn TP-fantasma dell'11-06
|
||||||
|
17:32–17:58 (~24 giri pre-fix). Nuova riga `epoca v1.1.26+ (TP_PHANTOM attivo)`
|
||||||
|
da `EPOCH_V1126 = 2026-06-11T21:40` (deploy del gate): oggi legge **7/1** contro
|
||||||
|
il 18/17 storico — è il numero da confrontare col backtest da qui in poi.
|
||||||
|
|
||||||
|
## Test e deploy
|
||||||
|
|
||||||
|
`pytest` 121 passed (114 + 7 nuovi: `test_reconcile_resting.py`,
|
||||||
|
`test_feed_book_gap.py`). Deploy **v1.1.27** (runner nel container; reconciler e
|
||||||
|
hourly_report girano da host → già attivi). cerbero-mcp rebuildato (endpoint).
|
||||||
|
|
||||||
|
## Aperto / lezioni
|
||||||
|
|
||||||
|
- **Drift ETH +0.167 non spiegato** dal rollback testnet: quando il conto si
|
||||||
|
sblocca serve `reset_flatten` + riallineamento ledger (decisione utente).
|
||||||
|
- I **residui dei close single-leg falliti** (`REAL_CLOSE_PARTIAL`, es. MR07
|
||||||
|
0.102) NON finiscono in `orphan_legs` → il reconciler li vede come drift NON
|
||||||
|
spiegato. Estendere la registrazione orfani anche al single-leg è il naturale
|
||||||
|
passo successivo (punto in analisi sviluppi).
|
||||||
|
- Il testnet può **riscrivere il conto sotto i piedi** (lock + rollback): nessuna
|
||||||
|
invariante "il conto cambia solo per nostri ordini" è affidabile. Il reconciler
|
||||||
|
orario è l'unico rilevatore; valutare cadenza più fitta in presenza di
|
||||||
|
posizioni reali aperte.
|
||||||
|
|
||||||
|
## Addendum (pomeriggio, v1.1.28): orfani single-leg + circuit-breaker venue-lock
|
||||||
|
|
||||||
|
I due punti promossi dall'analisi sviluppi, entrambi figli dell'incidente di oggi:
|
||||||
|
|
||||||
|
1. **Orfani anche per i close single-leg.** `REAL_CLOSE_PARTIAL` ora registra la
|
||||||
|
quota residua in `StrategyWorker.orphan_legs` (stessa semantica dei pairs,
|
||||||
|
persistita in status.json, letta da `books.real_books`) → il reconciler vede
|
||||||
|
il drift come SPIEGATO invece che anomalo (il caso MR07 0.102 ETH delle 09:47
|
||||||
|
restava invisibile ai libri). Inoltre `_close_position` ora persiste lo stato
|
||||||
|
a fine booking (prima solo al save del tick: un crash li' in mezzo perdeva
|
||||||
|
capital e orfano). NB: nessun save a meta' chiusura — con `real_in_position`
|
||||||
|
ancora true l'orfano conterebbe doppio nei libri.
|
||||||
|
2. **Circuit-breaker venue-lock in `ExecutionClient`.** Dopo `lock_trip=3` reject
|
||||||
|
'locked' consecutivi (es. `locked_by_admin` di oggi) le APERTURE sono sospese
|
||||||
|
senza toccare l'API (Fill failed → path REAL_OPEN_FAIL/sim_fallback esistente;
|
||||||
|
per i pairs entrambe le gambe rifiutate localmente → niente leg parziali da
|
||||||
|
unwindare, niente fee sprecate). Le CHIUSURE si tentano SEMPRE (path gia'
|
||||||
|
sicuro: partial/orphan/netting) e un loro reject 'locked' rinfresca il
|
||||||
|
cooldown. Riarmo: dopo `lock_cooldown_s=900` la prossima apertura fa da probe.
|
||||||
|
Alert `VENUE_LOCK` al trip + RIENTRATO al primo ordine accettato.
|
||||||
|
|
||||||
|
Test: `test_single_leg_orphan.py`, `test_venue_lock_breaker.py` (126 passed).
|
||||||
|
|
||||||
|
## Chiusura incidente (13:05): testnet sbloccato, conto riallineato
|
||||||
|
|
||||||
|
Sequenza eseguita a venue sbloccato: stop runner → `reset_flatten` (chiusi i due
|
||||||
|
artefatti del rollback: ETH long 0.141 @1664.25, SOL long 0.7 @66.651, conto FLAT)
|
||||||
|
→ svuotati gli `orphan_legs` di ETH_SOL (le quote orfane non esistono piu' sul
|
||||||
|
conto post-flatten; edit a container fermo, altrimenti il worker li riscrive
|
||||||
|
dalla memoria) → restart. **Reconciler: OK — conto allineato ai libri** (posizioni
|
||||||
|
e resting), runner healthy, log puliti. Il ledger NON e' stato toccato: i booking
|
||||||
|
dell'incidente erano gia' dichiarati (`sim_fallback`/real) e l'equity (€2020.82)
|
||||||
|
resta la verita' contabile del paper.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 2026-06-12 — XEX: discordanze Deribit testnet vs Hyperliquid
|
||||||
|
|
||||||
|
**Domanda.** I prezzi Deribit (testnet, dove eseguiamo) e Hyperliquid (feed
|
||||||
|
realistico, proxy della realtà) divergono? La divergenza è usabile per fare trade?
|
||||||
|
|
||||||
|
**Strumenti comuni validati:** 9 base-coin (ADA, AVAX, BNB, BTC, DOGE, ETH, PAXG,
|
||||||
|
SOL, TRUMP). Analisi su BTC/ETH (inverse, liquidi) + SOL/DOGE (lineari USDC).
|
||||||
|
Script: `scripts/analysis/xex_divergence_research.py`.
|
||||||
|
|
||||||
|
## Misure (1h, 2026-03-01 → 2026-06-12, ~2470 barre)
|
||||||
|
|
||||||
|
| Coin | spread medio | std | half-life | flat Deribit | Δbook−HL live |
|
||||||
|
|------|--------------|-----|-----------|--------------|----------------|
|
||||||
|
| BTC | −0.29% | 1.25% | 7.3h | 6% | **−0.97%** |
|
||||||
|
| ETH | −0.55% | 1.33% | 5.1h | 9% | **−1.54…−2.16%** |
|
||||||
|
| SOL | −0.38% | 3.60% | 5.2h | 35% | −0.05% (allineato) |
|
||||||
|
| DOGE | +0.34% | 1.88% | 4.2h | 87% | +0.16% (allineato) |
|
||||||
|
|
||||||
|
Per standard mainnet questi spread sono enormi (reale <0.05%): la divergenza è
|
||||||
|
l'artefatto del feed testnet che periodicamente si stacca dalla realtà e rientra.
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
1. **Lo spread è mean-reverting e il gap viene chiuso ANCHE da Deribit** (il lato
|
||||||
|
che possiamo tradare): beta del ritorno futuro Deribit sullo spread negativo e
|
||||||
|
crescente con l'orizzonte (ETH −0.36, BTC −0.23 a 24h). Non è solo HL che si
|
||||||
|
muove.
|
||||||
|
2. **Trappola smascherata — stale print arb.** Il backtest su DOGE dava Sharpe
|
||||||
|
6.7 (OOS 9.1!) e SOL 2.7, MA il book live di entrambi sta attaccato a HL
|
||||||
|
(±0.16%) mentre i print restano vecchi (DOGE 87% barre flat): l'edge era
|
||||||
|
**finzione** — i fill reali avverrebbero al prezzo vero, non al print stantio.
|
||||||
|
Stessa classe di illusione del look-ahead squeeze: il backtest compra un
|
||||||
|
prezzo a cui nessuno fa fill.
|
||||||
|
3. **Su BTC/ETH inverse la dislocazione è REALE: è il book stesso a essere
|
||||||
|
spostato** (misurato live: bid/ask −0.97% / −1.54…−2.16% sotto HL con depth
|
||||||
|
>$1M sui primi 5 livelli, spread bid/ask 1-3 bps). Lì si può davvero comprare
|
||||||
|
sotto/vendere sopra la realtà.
|
||||||
|
4. **Edge netto moderato e timing-sensitive** (fee 0.10% RT, entry |s|≥1%, exit
|
||||||
|
|s|≤0.25% o 24 barre): BTC FULL Sh 1.0 / OOS 2.05; ETH FULL 2.28 / OOS 1.37.
|
||||||
|
Con entry ritardato di 1 barra (stress staleness) BTC FULL→0, ETH OOS→0.1:
|
||||||
|
con half-life di 3-7 ore, un'ora di ritardo erode quasi tutto. Un'eventuale
|
||||||
|
implementazione live deve leggere il **book in tempo reale** (non il close 1h)
|
||||||
|
— a quel punto il lag reale è minuti, non un'ora, e l'edge eseguibile sta fra
|
||||||
|
lag0 e lag1.
|
||||||
|
|
||||||
|
## Segnale live al momento dell'analisi
|
||||||
|
|
||||||
|
ETH book Deribit **−2.16%** sotto HL (z=−1.7, 6.7° percentile storico), BTC
|
||||||
|
−0.97%: entrambi in zona "long Deribit" per il fade dello spread. (Solo
|
||||||
|
osservazione: nessun deploy senza gate.)
|
||||||
|
|
||||||
|
## Verdetto e prossimi passi
|
||||||
|
|
||||||
|
- Discordanza tradabile trovata: **fade dello spread D/H su BTC/ETH-PERPETUAL,
|
||||||
|
segnale dal book, soglia ~1%, exit a convergenza o 24h**. Da NON estendere a
|
||||||
|
DOGE/SOL (illusione stale-print).
|
||||||
|
- È un **edge di testnet** (non trasferibile a mainnet): legittimo per il
|
||||||
|
paper/shadow corrente, ma va dichiarato per quello che è.
|
||||||
|
- Prima di qualsiasi deploy, metodologia standard: gate PORT06 (correlazione con
|
||||||
|
le fade esistenti, che già fadano in parte questi stessi spike testnet —
|
||||||
|
rischio doppio conteggio), robustezza griglia, validazione worker.
|
||||||
|
|
||||||
|
Script: `scripts/analysis/xex_divergence_research.py` (fetch v2 due exchange,
|
||||||
|
tabella convergenza, backtest lag0/lag1, book reality-check live).
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 2026-06-13 — Dashboard web PORT06 (stato live + PnL + grafici + trade)
|
||||||
|
|
||||||
|
Richiesta utente: frontend per visualizzare lo stato con PnL totale e per-strategia,
|
||||||
|
grafici, e liste trade (attivi in tempo reale + chiusi).
|
||||||
|
|
||||||
|
## Cosa
|
||||||
|
|
||||||
|
`src/live/dashboard.py` — server **stdlib `http.server`** (zero nuove dipendenze),
|
||||||
|
legge i file `data/` e serve:
|
||||||
|
- `GET /api/state` → JSON con tutto lo stato calcolato
|
||||||
|
- `GET /` → single-page HTML (vanilla JS, polling ogni 5s)
|
||||||
|
|
||||||
|
Contenuto della pagina:
|
||||||
|
- **KPI**: equity, PnL totale (€ e %), max DD, peak
|
||||||
|
- **Grafico equity** (Chart.js da CDN, fallback testuale se offline) dalla
|
||||||
|
`equity.jsonl` del ledger (downsample a 400 punti)
|
||||||
|
- **PnL per strategia** (barre verdi/rosse): realizzato netto fee = Σ `pnl` reali
|
||||||
|
dai CLOSE (REAL-TRUTH), n trade, win-rate, capitale; tag `paper` per i
|
||||||
|
multi-asset non eseguiti, `•aperta` se in posizione
|
||||||
|
- **Trade attivi in tempo reale**: lato, entry, **mark corrente** (Cerbero
|
||||||
|
best-effort, cache 20s), **PnL non realizzato** (€ e %, da `real_entry_notional`),
|
||||||
|
barre/max_bars, distanza al TP, età dello status (⚠ se >15min = stantio)
|
||||||
|
- **Trade chiusi** (ultimi 50): ora, strategia, motivo, PnL reale, sim, esito
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
Servizio docker-compose `dashboard` (stessa immagine del runner, monta gli stessi
|
||||||
|
`data/`, porta **8787**), `restart: unless-stopped` + healthcheck sull'API.
|
||||||
|
Accesso: `http://<host>:8787`. **Nessuna auth** → solo rete interna/VPN, non
|
||||||
|
esporre pubblicamente. Avvio: `docker compose up -d --build dashboard` (il runner
|
||||||
|
non viene toccato).
|
||||||
|
|
||||||
|
uv run python -m src.live.dashboard --port 8787 # anche standalone su host
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- Il PnL per-strategia usa il PnL REALE (real_truth), coerente col report orario.
|
||||||
|
- I 6 fade 1h ritirati dallo swap restano in lista (hanno storico CLOSE): flat,
|
||||||
|
mostrano il loro PnL realizzato storico accanto ai gemelli 15m attivi.
|
||||||
|
- Unrealized € sui pairs non mostrato (posizione a 2 gambe, z-based) → "pairs (z)".
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# 2026-06-13 — Report ricorrente LEDGER REALE vs BACKTEST (il gate per scalare)
|
||||||
|
|
||||||
|
## Perché
|
||||||
|
|
||||||
|
Domanda dell'utente: "come cresco il capitale a 12k". Risposta: il prerequisito
|
||||||
|
prima di mettere soldi veri è che il **ledger reale combaci col backtest** —
|
||||||
|
soprattutto lo slippage del 15m appena deployato. Questo report rende quel gate
|
||||||
|
un dato osservabile, non un'opinione.
|
||||||
|
|
||||||
|
Insight chiave: per gli sleeve eseguiti (6 fade 15m, DIP01, 6 pairs, SH01) il
|
||||||
|
**sim del worker == backtest canonico PER COSTRUZIONE** (validato). Quindi
|
||||||
|
"reale vs backtest" = "reale vs sim" = la **fuga di esecuzione**: slippage +
|
||||||
|
fee reali vs assunte + effetti netting/phantom/sim_fallback.
|
||||||
|
|
||||||
|
## Cosa misura
|
||||||
|
|
||||||
|
`scripts/analysis/ledger_vs_backtest.py` (read-only: solo trades.jsonl +
|
||||||
|
status.json, nessuna rete → affidabile in cron):
|
||||||
|
- PnL realizzato sim vs reale (Σ e per-trade) → **LEAKAGE** € e per-trade (bottom line)
|
||||||
|
- slippage ingressi (REAL_OPEN) e uscite-a-mercato (REAL_CLOSE; escluse le uscite
|
||||||
|
da TP resting, fill maker al livello = no slippage)
|
||||||
|
- fee reali vs assunte (0.10% RT)
|
||||||
|
- trade `sim_fallback` (reale mai eseguito/fillato) = quota NON coperta dal reale
|
||||||
|
- ledger per-sleeve: real_capital vs capital (sim)
|
||||||
|
- **verdetto** 🟢/🟡/🔴: <10 trade = campione piccolo; leakage basso+slippage ≤15bps
|
||||||
|
= verde (si può pensare a scalare); slippage >40bps = rosso (edge erode, NON scalare)
|
||||||
|
|
||||||
|
## Clean-start (importante)
|
||||||
|
|
||||||
|
Una finestra mobile pura includerebbe l'**incidente testnet pre-fix**: a 7g il
|
||||||
|
report dà sim +82 vs reale +5 (🔴) — ma è gonfiato dai +4% FANTASMA che il sim
|
||||||
|
bookava e il reale no, prima di TP_PHANTOM (v1.1.23), netting (v1.1.25) e
|
||||||
|
ribilancio-conservativo (v1.1.31). Lo scheduler usa **`--since 2026-06-13`** →
|
||||||
|
accumula SOLO dati post-fix, e diventa statisticamente significativo coi giorni.
|
||||||
|
Finestra pulita oggi: 1 trade, leakage +0.07, slippage ingresso 12-29 bps.
|
||||||
|
|
||||||
|
## Scheduling
|
||||||
|
|
||||||
|
Cron host (come reconcile/hourly_report), **giornaliero 08:30 UTC**:
|
||||||
|
`ledger_vs_backtest.py --since 2026-06-13 --telegram` → Telegram + log in
|
||||||
|
`~/port06_ledger_vs_backtest.log`. Invio verificato.
|
||||||
|
|
||||||
|
## Come usarlo
|
||||||
|
|
||||||
|
Quando il campione supera ~10-20 trade reali e il verdetto è 🟢 stabile per
|
||||||
|
qualche giorno (leakage per-trade piccolo, slippage medio ≤15 bps), allora il
|
||||||
|
15m regge l'esecuzione e si può passare da testnet a piccolo reale → poi scalare.
|
||||||
|
Se resta 🔴/🟡, l'edge si erode sui fill e NON va scalato: prima si capisce dove
|
||||||
|
perde (slippage ingressi? uscite a mercato? sim_fallback frequenti?).
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 2026-06-13 — Orfano da swap: incidente + guard nel reconciler
|
||||||
|
|
||||||
|
## Incidente
|
||||||
|
|
||||||
|
Lo `stato trades` del mattino ha scoperto una **posizione reale orfana**: il
|
||||||
|
worker fade **MR02_BTC 1h** aveva aperto uno short reale (0.0028 BTC @ 64135.5)
|
||||||
|
ieri alle 15:01; lo **swap a 15m (v1.1.30, ~20:48) lo ha rimosso dal config
|
||||||
|
mentre era ancora in posizione** → da allora nessun runner lo gestiva. Stato:
|
||||||
|
|
||||||
|
- conto Deribit: short 0.0028 BTC (il long di apertura del gemello 15m aveva
|
||||||
|
aperto e chiuso nettando via il resto)
|
||||||
|
- il **TP limit (63387.75) era sparito** dal book (cancellato durante il netting
|
||||||
|
della chiusura 15m) → short NUDO, protetto solo dal disaster-SL a +30%
|
||||||
|
- il **reconciler NON allarmava**: lo `status.json` del worker morto dichiarava
|
||||||
|
ancora `real_in_position: true` → conto == libri. Punto cieco: il reconciler
|
||||||
|
leggeva i libri dagli status ma non sapeva quali worker fossero VIVI.
|
||||||
|
|
||||||
|
Chiusura manuale (testnet): buy 0.0028 reduce-only @63766.5 (~+$0.85 netto sullo
|
||||||
|
short), cancel disaster-SL, worker marcato flat, PnL bookato (real_capital
|
||||||
|
181.18→182.03). Conto verificato flat su BTC; SH01_ETH short intatto.
|
||||||
|
|
||||||
|
## Guard implementato
|
||||||
|
|
||||||
|
`reconcile_account.compute_stale_real_positions(max_age_min=15)`: segnala i
|
||||||
|
worker che dichiarano `real_in_position` ma il cui `status.json` è fermo da
|
||||||
|
oltre 15 min. Un worker vivo riscrive lo status a ogni poll (~60s) → la
|
||||||
|
**staleness** è il discriminante robusto e venue-agnostico (cattura
|
||||||
|
ritirati-da-swap, crashati, worker rimossi dal config). Alert Telegram
|
||||||
|
`STALE_REAL_POSITION` (con `--telegram`), incluso nell'exit code e nel verdetto.
|
||||||
|
Gira già al prossimo cron host (:40) — nessun rebuild (lo script gira dal
|
||||||
|
working tree). Test: `tests/portfolio/test_reconcile_resting.py` (stantio
|
||||||
|
flaggato / fresco no / flat-vecchio no).
|
||||||
|
|
||||||
|
## Causa radice e direzione
|
||||||
|
|
||||||
|
La feature `INIT_LINEAGE` di ieri trasferisce il *capitale* al gemello del nuovo
|
||||||
|
timeframe, ma non la *posizione*. Il guard di oggi è la **rete di sicurezza**
|
||||||
|
(rileva e allarma entro un'ora). La **prevenzione** vera — flattare/consegnare la
|
||||||
|
posizione reale del worker ritirato al boot del runner — resta da implementare
|
||||||
|
(va fatta lato runner, con cautela: piazza ordini reali all'avvio). Per ora:
|
||||||
|
swap a conto fade-flat quando possibile, e il reconciler copre il resto.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 2026-06-13 — Equity gonfiata dal ribilancio: +4.77 fantasma (causa + fix)
|
||||||
|
|
||||||
|
## Sintomo
|
||||||
|
|
||||||
|
Report "dall'ultimo reset": equity 2020.53 → 2025.80 (+5.27), ma un solo trade
|
||||||
|
chiuso (MR02_BTC 15m, reale +0.50). Il +4.77 residuo è comparso di colpo a
|
||||||
|
00:01:52 (primo poll dopo mezzanotte UTC = ribilancio giornaliero), senza alcuna
|
||||||
|
chiusura dietro.
|
||||||
|
|
||||||
|
## Causa — doppio conteggio del capitale degli in-position al ribilancio
|
||||||
|
|
||||||
|
`rebalance_allocations` faceva:
|
||||||
|
1. `total_capital = Σ capital di tutti i worker`
|
||||||
|
2. `alloc[sid] = peso × total_capital` per OGNI sid
|
||||||
|
3. ai worker in posizione NON applicava l'alloc (la posizione tiene il suo notional)
|
||||||
|
4. ai flat: `capital = alloc[sid]`
|
||||||
|
|
||||||
|
`update_equity` (ogni poll) = Σ capital. Dopo il ribilancio la somma diventa:
|
||||||
|
|
||||||
|
Σcapital = Σ(peso_i × total) [flat] + Σ(capital_j) [in-pos]
|
||||||
|
= total − Σalloc_j + Σcapital_j
|
||||||
|
= total + Σ(capital_j − alloc_j) ← inflazione
|
||||||
|
|
||||||
|
Cioè i flat si dividevano l'INTERO total (che includeva anche il capitale degli
|
||||||
|
in-position), e gli in-position lo tenevano in più → doppio conteggio.
|
||||||
|
|
||||||
|
Al ribilancio delle 00:01, **MR02_BTC 15m era in posizione** col capitale seedato
|
||||||
|
**181.19** (eredità del gemello 1h, INIT_LINEAGE di ieri) mentre la sua allocazione
|
||||||
|
era ~176.42 → **+4.77** di equity dal nulla. Lo scatenante è stato il seed (capitale
|
||||||
|
ben sopra l'allocazione media) combinato con la posizione aperta al ribilancio; ma
|
||||||
|
il bug era latente da sempre (ogni worker in-pos che devia dall'alloc gonfiava/sgonfiava
|
||||||
|
l'equity al ribilancio successivo).
|
||||||
|
|
||||||
|
## Fix — conservazione (ledger.allocate `reserved`)
|
||||||
|
|
||||||
|
`allocate(weights, reserved={sid: capitale})`: i worker in posizione TRATTENGONO il
|
||||||
|
loro capitale (deployato, non spostabile); i flat si dividono `total − Σreserved`
|
||||||
|
per peso **rinormalizzato sui soli flat**. Così `Σalloc == total_capital` sempre →
|
||||||
|
**l'equity è conservata dal ribilancio** (un ribilancio sposta capitale fra sleeve,
|
||||||
|
non crea valore). `runner.rebalance_allocations` calcola `reserved` dai worker
|
||||||
|
`in_position` e lo passa. Senza `reserved` (default) = comportamento storico
|
||||||
|
(corretto solo a worker tutti flat, es. allocazione iniziale).
|
||||||
|
Test: `tests/portfolio/test_ledger.py::test_alloc_conserves_total_with_reserved`.
|
||||||
|
|
||||||
|
## Correzione dello stato (rimozione del +4.77 già accumulato)
|
||||||
|
|
||||||
|
Equity riportata a **2021.03** = baseline post-seed verificata (2020.53) + unico
|
||||||
|
trade reale (MR02_BTC 15m +0.50). Capitali dei worker del pool scalati ×
|
||||||
|
(2021.03/2025.79) e peak del ledger resettato a 2021.03 (il 2025.80 non è mai
|
||||||
|
stato un massimo legittimo → niente DD fantasma). L'orfano MR02_BTC 1h chiuso a
|
||||||
|
parte (+0.85) resta sul worker ritirato, fuori dal pool.
|
||||||
|
|
||||||
|
## Esito
|
||||||
|
|
||||||
|
Equity = soldi veri: dal reset il trading ha prodotto **+0.50** (1 trade), non +5.27.
|
||||||
|
Deploy con codice fixato (rebuild) così il prossimo ribilancio conserva.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# 2026-06-14 — Timing sweep: PAIRS & HONEST su 5/10/15/30m (vs live)
|
||||||
|
|
||||||
|
## Domanda (goal utente)
|
||||||
|
|
||||||
|
Dopo lo swap fade 1h→15m (v1.1.30), i **pairs** e le **honest** beneficiano dello stesso
|
||||||
|
trattamento — girare su timeframe più veloci (5/10/15/30 min)?
|
||||||
|
|
||||||
|
Script: `scripts/analysis/timing_sweep_pairs_honest.py` (riusa i motori canonici
|
||||||
|
`pairs_sim_flat`, replica DIP intrabar verificata == `dip_market_gated(market_n=0)`, gate
|
||||||
|
PORT06 == `pairs30m_gate`/`dip01`). Niente re-tuning dei parametri al cambio TF (anti-overfit,
|
||||||
|
come lo swap fade). Tutto netto, leva 3x, OOS held-out (da 2024-10).
|
||||||
|
|
||||||
|
## Vincolo dati (hard — definisce lo scope)
|
||||||
|
|
||||||
|
Solo **BTC/ETH** hanno 5m/15m/30m in locale (10m = resample causale da 5m). **TUTTI gli alt**
|
||||||
|
(ADA/BNB/DOGE/LTC/SOL/XRP) sono **SOLO 1h**. Quindi:
|
||||||
|
- **PAIRS:** solo **ETH/BTC** è sweepabile sub-orario. Gli altri 4 pair (gambe alt: LTC/ETH,
|
||||||
|
ADA/ETH, BTC/LTC, ETH/SOL) restano 1h per sempre senza dati alt sub-orari.
|
||||||
|
- **HONEST:** solo **DIP01** (BTC, mean-reversion) ha senso + dati. **TR01** (trend EMA20/100 su
|
||||||
|
4h, basket alt) e **ROT02** (rotazione dual-momentum 1d, momentum 60g, universo alt) sono lente
|
||||||
|
(orizzonte multi-giorno/mese) E multi-asset-su-alt → sub-orario **infattibile** (dati) e
|
||||||
|
**insensato** (momentum a 60 giorni su barre da 5 min). Nessuno sweep.
|
||||||
|
|
||||||
|
## Flat-share (print stale O=H=L=C, = rischio fill) — sale al scendere del TF
|
||||||
|
|
||||||
|
| asset | 5m | 10m | 15m | 30m | 1h |
|
||||||
|
|-------|----|-----|-----|-----|-----|
|
||||||
|
| BTC | 10.0% | 4.8% | 3.5% | 1.5% | 0.9% |
|
||||||
|
| ETH | **29.0%** | 17.3% | 16.4% | 9.0% | 6.7% |
|
||||||
|
|
||||||
|
ETH 5m al 29% è un allarme operativo serio. Nota: **30m ETH (9%) < 15m (16.4%)** → il 30m ha
|
||||||
|
*meno* rischio stale-print del 15m già live.
|
||||||
|
|
||||||
|
## PAIRS ETH/BTC — il vero free-lunch
|
||||||
|
|
||||||
|
Config UNIVERSALE 1h (n=50, z_in=2.0, z_exit=0.75, max_bars=72), flat_skip live-realizable:
|
||||||
|
|
||||||
|
| tf | Sharpe FULL | OOS Sh | OOS Sh fee2x | CAGR | win% |
|
||||||
|
|----|-------------|--------|--------------|------|------|
|
||||||
|
| 5m | 9.54 | 17.19 | 11.98 | 2071% | 56.0 |
|
||||||
|
| 10m | 9.26 | 16.54 | **13.03** | 1214% | 64.1 |
|
||||||
|
| 15m (live) | 8.10 | 14.40 | 11.74 | 673% | 66.7 |
|
||||||
|
| 30m | 6.20 | 11.17 | 9.59 | 312% | 71.7 |
|
||||||
|
| 1h (live) | 3.74 | 7.11 | 6.19 | 119% | 72.4 |
|
||||||
|
|
||||||
|
(I CAGR a quattro cifre sono compounding leva su singolo sleeve, irrealistici — il portafoglio
|
||||||
|
normalizza/cappa; il segnale robusto è lo **Sharpe** e il **gate PORT06**.)
|
||||||
|
|
||||||
|
- Sharpe **monotono** al scendere del TF, e **regge le fee 2x** (a differenza delle fade, dove
|
||||||
|
MR02 muore a 5m). Pairs market-neutral: lo spread rientra più spesso ad alta frequenza, e
|
||||||
|
ETH/BTC è la coppia più liquida.
|
||||||
|
- Corr daily fra TF: 5m↔1h 0.28, 10m↔1h 0.32, 30m↔1h 0.51; 10m↔15m 0.67 → i TF veloci sono
|
||||||
|
**fasi diverse dello stesso edge**, diversificanti, non duplicati.
|
||||||
|
|
||||||
|
**Gate PORT06 (add half-size al BLEND 1h+15m, OOS da 2024-10):**
|
||||||
|
|
||||||
|
| config | FULL Sh | FULL DD% | OOS Sh | OOS DD% | verdetto |
|
||||||
|
|--------|---------|----------|--------|---------|----------|
|
||||||
|
| ATTUALE (1h+15m) | 8.13 | 2.47 | 10.86 | 2.09 | — |
|
||||||
|
| +30m | 8.24 | 2.28 | 10.87 | 1.97 | **MIGLIORA** (già `pairs30m_gate`, mai deployato) |
|
||||||
|
| +10m | 8.44 | 2.28 | 11.04 | **1.92** | **MIGLIORA** (domina tutto) |
|
||||||
|
| +5m | 8.52 | 2.30 | 11.00 | 1.95 | **MIGLIORA** (FULL Sh max) |
|
||||||
|
|
||||||
|
Tutte e tre dominano su FULL Sh, FULL DD, OOS Sh, OOS DD. **10m** è il candidato più pulito
|
||||||
|
(miglior OOS DD, flat-share moderato 17%); **30m** è il più sicuro (flat-share 9% < 15m live).
|
||||||
|
|
||||||
|
## DIP01 (BTC) — più veloce, ma lo swap NON domina
|
||||||
|
|
||||||
|
Engine canonico (n=50, z_in=2.5, sl_atr=2.5, max_bars=24), parità 1h verificata:
|
||||||
|
|
||||||
|
| tf | OOS Sh BTC | OOS Sh fee2x BTC | corr↔1h | OOS Sh ETH |
|
||||||
|
|----|------------|------------------|---------|------------|
|
||||||
|
| 5m | 4.91 | 2.41 | 0.13 | 8.43 |
|
||||||
|
| 10m | 3.47 | 1.99 | 0.17 | 6.51 |
|
||||||
|
| 15m | 2.40 | 1.40 | 0.22 | 4.49 |
|
||||||
|
| 30m | 1.34 | **0.77** | 0.41 | 3.54 |
|
||||||
|
| 1h (live) | 1.69 | 1.36 | — | 2.68 |
|
||||||
|
|
||||||
|
Standalone Sharpe sale al scendere del TF e i TF veloci sono **fortemente diversificanti**
|
||||||
|
(corr 0.13–0.22 con l'1h). MA: **BTC fee-fragile** (30m f2x 0.77), e il **gate-swap non domina**:
|
||||||
|
|
||||||
|
| config | FULL Sh | FULL DD% | OOS Sh | OOS DD% |
|
||||||
|
|--------|---------|----------|--------|---------|
|
||||||
|
| DIP01 1h (live) | 8.13 | 2.47 | 10.86 | 2.09 |
|
||||||
|
| DIP01 30m | 8.16 | 2.27 | 10.78 | 2.03 |
|
||||||
|
| DIP01 15m | 8.16 | 2.30 | 10.82 | 2.14 |
|
||||||
|
| DIP01 10m | 8.31 | 2.49 | 10.85 | 2.15 |
|
||||||
|
| DIP01 5m | 8.46 | 2.27 | 11.02 | 2.16 |
|
||||||
|
|
||||||
|
Tutti migliorano i FULL ma **alzano l'OOS DD** (e 30m abbassa l'OOS Sharpe) → nessuno domina
|
||||||
|
sui 4 criteri. DIP01 5m dà il miglior OOS Sharpe (11.02) ma OOS DD 2.16 vs 2.09. La bassa corr
|
||||||
|
suggerirebbe un **ADD** (DIP01 5m come secondo sleeve diversificante) più che uno swap, ma
|
||||||
|
aumenta la concentrazione BTC-rev e non è il free-lunch netto che sono i pairs.
|
||||||
|
|
||||||
|
## Caveat (perché backtest ≠ via libera a scalare)
|
||||||
|
|
||||||
|
1. **Slippage su feed flat-alto** = il vero banco di prova, già flaggato per il 15m blend
|
||||||
|
("il vero banco di prova è il ledger reale shadow"). A 5m/10m è peggio (ETH 5m 29% flat).
|
||||||
|
Il backtest usa flat_skip (timing live-realizable) ma NON modella lo slippage attorno ai
|
||||||
|
print stali. → gateare via shadow ledger PRIMA di sizing, specie il 5m.
|
||||||
|
2. **OOS = un solo regime** (2024-10→, calmo). Lo Sharpe monotono-col-TF può essere in parte
|
||||||
|
un artefatto di regime calmo (la MR veloce prospera nel range a bassa vol; lo sweep fade
|
||||||
|
trovò "i tf veloci pagano nella volatilità, non nella calma"). I pairs sono market-neutral
|
||||||
|
→ meno esposti dei fade ai crash direzionali, ma il caveat resta.
|
||||||
|
|
||||||
|
## Verdetto del backtest (gate full + OOS 2024→)
|
||||||
|
|
||||||
|
- **PAIRS ETH/BTC: il gate diceva SÌ.** Estendere il BLEND con TF più veloci migliorava PORT06
|
||||||
|
su tutte le metriche (10m domina, 30m il più sicuro, 5m FULL Sh max), fee-2x robusto.
|
||||||
|
- **DIP01: marginale.** Più veloce è diversificante ma lo swap non domina e BTC è fee-fragile.
|
||||||
|
- **TR01/ROT02: NO.** Dati alt 1h-only + orizzonte lento → fuori scope per costruzione.
|
||||||
|
|
||||||
|
## Tentativo di deploy → due muri, NESSUN deploy (2026-06-14)
|
||||||
|
|
||||||
|
L'utente ha scelto di deployare un blend pairs ETH/BTC più veloce. Il wiring è stato fatto e
|
||||||
|
validato (sleeve def, backtest face, worker replay == backtest esatto, regression-lock
|
||||||
|
aggiornato) PRIMA di scoprire due blocchi che hanno ribaltato la decisione:
|
||||||
|
|
||||||
|
1. **MURO FEED (30m e 10m).** Il feed live **Cerbero v2 (`/mcp/tools/get_historical`) serve
|
||||||
|
solo 5m/15m/1h** — il 30m e il 10m danno 400 in OGNI formato (30m/30/30min/1800), e
|
||||||
|
l'endpoint legacy è 404 (rimosso). La voce `"30m"` in `runner._SUBHOURLY` era **speculativa,
|
||||||
|
mai testata live**. → un blend 30m/10m NON è drop-in: richiede di derivarlo resamplando il
|
||||||
|
feed 15m/5m nel runner (modifica al data-path live condiviso). **Lezione: il _SUBHOURLY del
|
||||||
|
runner ≠ ciò che il feed serve davvero — smoke-testare il fetch prima di promettere un TF.**
|
||||||
|
|
||||||
|
2. **MURO REGIME (5m).** Il 5m è l'unico TF veloce nativo (drop-in), ma il gate full-history
|
||||||
|
(Sharpe 9.54) è un **relitto dei regimi volatili vecchi**. Nel regime ATTUALE è il PEGGIORE
|
||||||
|
(ETH/BTC pairs half-size, ret%/Sharpe/DD):
|
||||||
|
|
||||||
|
| tf | ultimi 30g | ultimi 90g | ultimi 180g |
|
||||||
|
|----|-----------|-----------|------------|
|
||||||
|
| 5m | −10.5% / −11.5 / 11.6% | −2.0% / −0.2 / 14.1% | +78% / 2.93 / **19.5%** |
|
||||||
|
| 15m | −4.4% / −6.2 / 5.8% | −3.1% / −0.9 / 5.8% | +56% / 3.80 / 7.9% |
|
||||||
|
| 1h | −1.0% / −1.1 / 3.0% | +3.0% / +1.3 / 3.0% | +25% / **3.95** / 3.0% |
|
||||||
|
|
||||||
|
Il 5m perde su ogni finestra recente e ha **DD 3-6× il 1h**; persino sui 180g il suo Sharpe
|
||||||
|
(2.93) è SOTTO 15m/1h. Sommato al **flat-share ETH 5m 29%** (slippage reale, non modellato
|
||||||
|
dal backtest) e al fatto che girerebbe **reale**, il guadagno modesto di backtest (FULL
|
||||||
|
Sharpe +0.39) non vale il rischio. **5m: non conviene.**
|
||||||
|
|
||||||
|
**ESITO: nessun deploy. Config live invariata (19 sleeve, pairs 1h+15m).** I file tracciati
|
||||||
|
ripristinati a HEAD; restano solo questo diario + `timing_sweep_pairs_honest.py` come record.
|
||||||
|
|
||||||
|
**Lezione di metodo:** il gate PORT06 full+OOS è necessario ma non sufficiente — va incrociato
|
||||||
|
con (a) la fattibilità del FEED live e (b) il regime RECENTE. Un edge full-history può essere
|
||||||
|
un relitto di volatilità passata; "più veloce = più Sharpe storico" si rovescia in "più veloce
|
||||||
|
= più DD nel regime calmo attuale". Se in futuro si vorrà un blend più veloce: il **30m** resta
|
||||||
|
il migliore on-paper (flat 9%), ma serve prima derivarlo dal 15m nel runner + ri-verificarlo
|
||||||
|
nel regime del momento.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# 2026-06-15 — Gate del feed CONGELATO (freeze gate)
|
||||||
|
|
||||||
|
## Problema (da "stato ordini")
|
||||||
|
|
||||||
|
Audit degli ordini live: 5 sleeve in posizione reale, **4-5 contaminati dal feed ETH
|
||||||
|
congelato**. `ETH-PERPETUAL` (inverse, il feed di DECISIONE del runner) è fermo a
|
||||||
|
**1661.95 da 36h+** (100% barre flat, 57/57 nel windows 2g, **1 solo valore distinto**),
|
||||||
|
mentre il book reale dove si ESEGUE (`ETH_USDC-PERPETUAL`, lineare) si muove ~1717-1722.
|
||||||
|
|
||||||
|
Effetto misurato sui soldi reali (log container):
|
||||||
|
- **SH01_ETH** ha aperto short a sim 1661.95 / real 1717.10 (slippage 332 bps), chiuso a
|
||||||
|
time_limit con `sim_exit 1661.95` ma `real_fill 1719.21` → **−2.83 USD reali vs −0.09 sim**
|
||||||
|
→ e ha **riaperto subito** lo stesso short sulla trappola.
|
||||||
|
- I 4 pairs con gamba ETH (ETH/BTC 1h+15m, ETH/SOL, LTC/ETH, ADA/ETH) sono entrati con
|
||||||
|
z-score **estremi** (−3.10, −2.96, +5.58, −2.56): artefatto MECCANICO del log-ratio con
|
||||||
|
ETH pinnato a 1661.95 mentre gli alt si muovono → falsa divergenza → carico di spread long-ETH.
|
||||||
|
|
||||||
|
È un GUASTO testnet, non di strategia: in mainnet l'arbitraggio tiene inverse ≈ lineare ≈ realtà.
|
||||||
|
|
||||||
|
## Decisione utente
|
||||||
|
|
||||||
|
> "congela gli sleeve ETH-leg finché il feed non si sblocca"
|
||||||
|
|
||||||
|
## Soluzione: gate del feed congelato nel runner (auto-guarente)
|
||||||
|
|
||||||
|
`src/portfolio/runner.py`: quando il feed di decisione 1h di un asset è **congelato**, gli
|
||||||
|
sleeve CONCENTRATI (single/ml/pairs) che ne dipendono **saltano il tick** (entry E exit)
|
||||||
|
finché il feed non si sblocca. Come un outage (i worker non valutano gli exit, protezione =
|
||||||
|
disaster-SL on-book). Si rilascia DA SOLO alla prima barra completa non-flat.
|
||||||
|
|
||||||
|
### Distinguere il GUASTO dall'ILLIQUIDITÀ (il punto critico)
|
||||||
|
|
||||||
|
Un detector flat-bar ingenuo sarebbe SBAGLIATO: sul feed reale (2026-06-15) gli alt sono
|
||||||
|
flat ma VIVI. Firma misurata (run finale di close invariate / valori distinti in 48h):
|
||||||
|
|
||||||
|
| Asset | run | distinti/48h | natura |
|
||||||
|
|-------|-----|--------------|--------|
|
||||||
|
| ETH | 64 | **1** | MORTO (guasto) |
|
||||||
|
| BNB | 64 | 2 | MORTO (testnet, solo paper) |
|
||||||
|
| DOGE | 42 | 4 | MORTO (testnet, solo paper) |
|
||||||
|
| XRP | 12 | 3 | illiquido |
|
||||||
|
| ADA | 11 | 5 | illiquido ma VIVO |
|
||||||
|
| LTC | 10 | 5 | illiquido ma VIVO |
|
||||||
|
| SOL | 1 | 31 | VIVO |
|
||||||
|
|
||||||
|
→ il detector conta la **run di close INVARIATE** (prezzo che non cambia MAI), non le barre
|
||||||
|
flat. Soglia **24** (`feed_freeze_gate_bars`, configurabile, 0=off): un giorno intero di
|
||||||
|
prezzo immobile separa nettamente le due popolazioni. ETH/BNB/DOGE (run 40-64) gateati,
|
||||||
|
ADA/LTC/XRP/SOL (run ≤12) no. **PR_BTCLTC resta attivo** (BTC vivo + LTC solo illiquido NON
|
||||||
|
deve sospendere il pair: si perderebbero segnali reali sulla gamba BTC).
|
||||||
|
|
||||||
|
### NON è l'entry-guard post-flat (BOCCIATA)
|
||||||
|
|
||||||
|
Si gatea **DURANTE** il freeze (ultima barra completa ferma). La barra di RIPRESA è non-flat
|
||||||
|
→ la run si azzera → il tick riprende SU di essa. NON si salta la candela-gap di ripresa
|
||||||
|
(quella è l'overshoot che la fade fada con profitto — l'entry-guard post-flat è bocciata,
|
||||||
|
CLAUDE.md / 2026-06-05). Rilascio solo su barra **completata** non-flat (la forming non basta,
|
||||||
|
coerente con EXIT-16).
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
Solo sleeve `single`/`ml`/`pairs`. I multi-asset (basket/rotation/tsmom/xsec, tutti PAPER)
|
||||||
|
NON sono gateati: diversificati su 8 asset, un feed fermo non li compromette (e ROT02 ha ETH
|
||||||
|
nell'universo ma resta attivo). Osservabilità: log `FEED_FROZEN_GATE` + alert Telegram
|
||||||
|
GATED/RIPRESO, dedup per episodio.
|
||||||
|
|
||||||
|
## Esito (smoke feed reale, soglia 24)
|
||||||
|
|
||||||
|
- CONGELATI: ETH, BNB, DOGE.
|
||||||
|
- **GATEATI: le 9 gambe ETH esatte** — MR01/MR02/MR07_ETH, SH_ETH, PR_ETHBTC, PR_ETHBTC_15M,
|
||||||
|
PR_LTCETH, PR_ADAETH, PR_ETHSOL. BTC-only, PR_BTCLTC e i multi-asset restano attivi.
|
||||||
|
|
||||||
|
Le 5 posizioni ETH già aperte NON vengono flattate (freeze = pausa, non liquidazione): un
|
||||||
|
close forzato passerebbe per lo stesso feed congelato (= il close −2.83 di SH01). Restano
|
||||||
|
gestite alla ripresa del feed; protezione di coda = disaster-SL on-book a −30%.
|
||||||
|
|
||||||
|
Test: `tests/portfolio/test_freeze_gate.py` (detector guasto-vs-illiquido, rilascio su barra
|
||||||
|
completata, scope sui veri sleeve PORT06). Suite portfolio: 140/140 verdi.
|
||||||
|
|
||||||
|
## Lezione
|
||||||
|
|
||||||
|
Tre livelli di osservabilità del feed ora distinti: STALE_FEED (≥2 barre flat, alert umano),
|
||||||
|
FEED_BOOK_GAP (feed≠book d'esecuzione, alert), **FEED_FROZEN_GATE (≥24 barre invariate, AGISCE)**.
|
||||||
|
La firma che separa guasto da illiquidità è la **run di prezzo invariato**, non la barra flat:
|
||||||
|
gli alt illiquidi stampano barre flat ma il prezzo si muove ogni ~10 barre.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 2026-06-16 — Guard TP-invertito (churn MR02_BTC 15m)
|
||||||
|
|
||||||
|
## Sintomo
|
||||||
|
Burst di **8 trade in 15 minuti** su `MR02_donchian_fade__BTC__15m` (09:00–09:15 UTC),
|
||||||
|
tutti `long`, tutti chiusi `take_profit` a `bars_held=0-1` **in perdita**, accuracy
|
||||||
|
crollata 60%→27%. TP **fisso** a 66189.25, **sotto** ogni entry (66700–66834):
|
||||||
|
|
||||||
|
```
|
||||||
|
09:00 OPEN long 66780.5 tp=66189.25 sl=64218.21 ← tp E sl ENTRAMBI sotto l'entry
|
||||||
|
09:02 CLOSE take_profit exit=66189.25 held=0 sim=-2.63 real=-0.12
|
||||||
|
... ×8 ... +7× REAL_TP_FAIL: invalid_reduce_only_order
|
||||||
|
```
|
||||||
|
|
||||||
|
I trade MR02 sani prima del cluster hanno il TP dal lato giusto (long → tp>entry:
|
||||||
|
65033>63414, 64148>63775, 66079>65717). Il cluster delle 09:00 è l'unico **invertito**.
|
||||||
|
|
||||||
|
## Causa radice — segnale da wick transitorio (NON feed congelato, NON spike d'ingresso)
|
||||||
|
BTC alle 09:00 era **davvero** ~66780 (MR01 short@66919, MR07 short@66780.5 nello stesso
|
||||||
|
istante, slippage reale 0.7–2.5 bps → feed sano). Il difetto è a monte:
|
||||||
|
|
||||||
|
1. Una barra con **wick verso il basso** (~64218 = il livello SL) fa scattare il donchian
|
||||||
|
"rottura canale basso" → segnale **LONG**, con `tp`=centro canale (66189) e `sl`=64218
|
||||||
|
calcolati **dal contesto wickato**.
|
||||||
|
2. Il wick rientra; il worker entra `long` al prezzo **recuperato** 66780 — cioè **sopra
|
||||||
|
il proprio TP=66189**.
|
||||||
|
3. L'exit intrabar (`bar_high >= tp`) è banalmente vera all'istante → chiude "take_profit"
|
||||||
|
a 66189 = **perdita garantita a held=0**. Si ripete 8× perché il livello donchian
|
||||||
|
wickato resta nel lookback.
|
||||||
|
|
||||||
|
Differenza chiave da TP_PHANTOM: lì il wick è sull'**exit** (tocco fantasma del TP);
|
||||||
|
qui il wick produce un **tp dal lato sbagliato dell'entry** → la posizione non doveva
|
||||||
|
proprio aprirsi.
|
||||||
|
|
||||||
|
## Perché i guard esistenti non l'hanno preso
|
||||||
|
- **TP_PHANTOM** richiede un limit reduce-only resting come oracolo. Qui il limit è
|
||||||
|
**respinto** (`invalid_reduce_only_order`: una sell reduce-only a 66189 sotto il mercato
|
||||||
|
è invalida) → niente `real_tp_order_id` → fail-open → l'exit passa. E il prezzo corrente
|
||||||
|
(66780) ha **realmente** superato il livello → non è un tocco fantasma.
|
||||||
|
- **Freeze-gate (v1.1.32)**: BTC non è congelato (prezzi che si muovono) → non scatta.
|
||||||
|
|
||||||
|
## Danno reale: contenuto (il real-truth ledger ha funzionato)
|
||||||
|
- Σ **reale** del cluster ≈ **−€2.29** (solo slippage+fee: il fill reale è al prezzo vero
|
||||||
|
~66700-66730, non al TP fantasma).
|
||||||
|
- Σ **sim** sarebbe stato ≈ **−€17.9** → il real-truth ledger ha evitato che ~**€15.6** di
|
||||||
|
perdita fantasma colpisse l'equity.
|
||||||
|
- Costi collaterali: 8 round-trip di fee, 7 TP reduce-only respinti, 8 disaster-SL da
|
||||||
|
piazzare/cancellare, sim accuracy/report inquinati.
|
||||||
|
- Auto-esaurito alle 09:15 (sleeve poi flat).
|
||||||
|
|
||||||
|
## Fix — guard "TP invertito" (v1.1.33, zero parametri)
|
||||||
|
`StrategyWorker._open_position`: **prima** di aprire, se il TP è già sfondato all'ingresso
|
||||||
|
(`long & tp<=entry` oppure `short & tp>=entry`) il segnale è malformato (artefatto wick)
|
||||||
|
→ **NON si apre** (niente ordine reale, niente disaster-SL). Verità d'esecuzione, non un
|
||||||
|
filtro di strategia (stessa filosofia di TP_PHANTOM). Log `INVERTED_TP_SKIP` (dedup
|
||||||
|
per-barra) + alert Telegram una-tantum per processo. Agisce **solo sul path live**: il
|
||||||
|
backtest canonico entra al close del bar del segnale (entry == base di tp/sl → mai
|
||||||
|
invertito) e resta non filtrato → il live farà meglio del backtest, come loss-guard /
|
||||||
|
trend / TP_PHANTOM.
|
||||||
|
|
||||||
|
Test: `tests/portfolio/test_inverted_tp.py` (long/short invertiti skippati, controlli
|
||||||
|
positivi long/short validi aprono, no-tp apre, dedup per-barra). Suite portfolio 146/146.
|
||||||
|
|
||||||
|
## Lezione
|
||||||
|
Cerotto testnet (il feed wicka): il fix vero è **mainnet**, dove l'arbitraggio elimina i
|
||||||
|
wick-print. Ma il guard è corretto in assoluto — un segnale con TP dal lato sbagliato
|
||||||
|
dell'entry non va mai eseguito, su qualsiasi venue.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 2026-06-16 — Fase 0 smoke mainnet VERDE (ETH→USDC, limit non market)
|
||||||
|
|
||||||
|
## Contesto
|
||||||
|
Il conto Deribit mainnet, fino al 2026-06-14 **vuoto** (€0, unico blocco al micro-test), ha
|
||||||
|
ricevuto un deposito iniziale di **~$50**: 25.00 USDC + 0.013878 ETH. Occasione per eseguire la
|
||||||
|
**Fase 0** del piano (`docs/specs/mainnet-microtest-plan.md`): validare la catena d'esecuzione su
|
||||||
|
soldi VERI con rischio ~0, abbinandola a un'operazione utile — **consolidare l'ETH stranded in
|
||||||
|
margine USDC**.
|
||||||
|
|
||||||
|
Perché consolidare: noi eseguiamo sui perp **lineari USDC** (`*_USDC-PERPETUAL`), marginati in
|
||||||
|
**USDC**. I 0.0138 ETH stavano nel wallet ETH (collateral dell'inverse, che NON usiamo per la
|
||||||
|
matematica lineare del backtest) → margine effettivo solo i $25 USDC. Convertendo, l'intero ~$50
|
||||||
|
diventa USDC utilizzabile.
|
||||||
|
|
||||||
|
## Ordine (LIMIT, non market — istruzione utente)
|
||||||
|
Check capability read-only prima: spot `ETH_USDC` esiste (bid 1780.3/ask 1780.9, `testnet:false`,
|
||||||
|
price tick 0.1). Il listing strumenti NON espone lo spot (kind=spot vuoto, endpoint v1 404) ma il
|
||||||
|
ticker basta. `place_order` supporta `order_type="limit"` + `price`.
|
||||||
|
|
||||||
|
- **LIMIT SELL `ETH_USDC` 0.0138 ETH @ floor 1776.3** (~0.1% sotto il bid) — *marketable limit con
|
||||||
|
cap di slippage*: crossa e filla al bid corrente, ma con un floor sotto cui NON può fillare.
|
||||||
|
Soddisfa "non a market" (prezzo protetto, niente slippage incontrollato) garantendo il fill.
|
||||||
|
- Step 0.0001 ok al primo colpo (niente fallback 0.013); fallback previsti 0.013/0.012.
|
||||||
|
|
||||||
|
### Esito
|
||||||
|
```
|
||||||
|
order_state = filled filled_amount = 0.0138
|
||||||
|
average_price = 1778.1 (= bid al momento dell'invio; il bid era sceso 1780.3->1778.1 tra
|
||||||
|
quote e invio, ma > floor 1776.3 -> zero slippage negativo)
|
||||||
|
fee = 0.0 (maker, costo nullo)
|
||||||
|
order_id = ETH_USDC-8488088107
|
||||||
|
```
|
||||||
|
Riconciliazione conto (before→after):
|
||||||
|
| Wallet | Prima | Dopo |
|
||||||
|
|---|---|---|
|
||||||
|
| USDC | 25.00 | **49.538** (+24.538) |
|
||||||
|
| ETH | 0.013878 | 0.000078 (dust ~$0.14, sotto il min → non convertibile) |
|
||||||
|
| BTC | 0 | 0 |
|
||||||
|
| posizioni / ordini resting | — | nessuna / nessuno |
|
||||||
|
|
||||||
|
## Cosa è dimostrato
|
||||||
|
Catena end-to-end su **mainnet** (`testnet:false`), a costo ~0: routing live + auth + `place_order`
|
||||||
|
limit + fill + verifica via `get_trade_history` + riconciliazione `get_account_summary`. La
|
||||||
|
meccanica d'esecuzione reale FUNZIONA su soldi veri. **Fase 0 ✅.**
|
||||||
|
|
||||||
|
## Limite residuo (resta il blocco vero)
|
||||||
|
~$49.54 USDC è **sotto il minimo €500** del piano: a questo margine il rumore di arrotondamento
|
||||||
|
BTC (step lineare ~$6.6) soffoca il segnale → NON si può ancora testare l'**edge**. Per la **Fase 1**
|
||||||
|
(fade-only, €1000, 2-4 settimane, verdetto ledger-vs-backtest) serve un **top-up a ~€500-1000 USDC**.
|
||||||
|
Stato piano aggiornato: token+smoke ✅, funding 🟡 parziale, Fase 1 in attesa di capitale.
|
||||||
|
|
||||||
|
## Note operative
|
||||||
|
- Operazione MANUALE one-off (non il runner): `.env.mainnet` sourcato a mano, client puntato a
|
||||||
|
mainnet solo per questa sessione. Il runner di produzione resta su **testnet** (`.env` condiviso
|
||||||
|
invariato) → nessun rischio di flip accidentale.
|
||||||
|
- La marketable-limit con floor è il pattern giusto per conversioni one-shot a prezzo controllato
|
||||||
|
(vs il TP resting/limit-al-livello degli sleeve): fill immediato + cap di slippage, fee maker.
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
# 2026-06-18 — Price Ladder (griglia) vs gate PORT06
|
||||||
|
|
||||||
|
**Branch:** `price_ladder_research` · **Harness:** `scripts/analysis/ladder_search.py`
|
||||||
|
(engine `grid_mtm`: mark-to-market onesto, SL gap-aware, flat-skip, fee 0.10% RT taker).
|
||||||
|
**Baseline PORT06:** FULL Sharpe 8.13 / DD 2.47% — OOS Sharpe 10.86 / DD 2.09%.
|
||||||
|
**Metodo:** caccia multi-agente su asset × TF (15m/30m/1h) × regime-gate (none / range
|
||||||
|
trend_max 1.5/2.0/2.5), poi `verify` avversariale sui finalisti (plateau, fee 2x, corr,
|
||||||
|
FULL DD di portafoglio). **Goal di progetto:** trovare un Price Ladder che *diversifichi*
|
||||||
|
(PASSI il gate) senza accendere la coda di trend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Cosa era già noto
|
||||||
|
|
||||||
|
Il gioco-griglia precedente aveva trovato una **griglia ETH fortissima standalone**
|
||||||
|
(Sharpe ~5.61) ma **BOCCIATA al gate PORT06**: corr ~0.40 con le fade ETH già deployate
|
||||||
|
→ **ridondante**, non un diversificatore. La lezione: lo standalone Sharpe non basta, conta
|
||||||
|
quanto il ladder *aggiunge ortogonalmente* al portafoglio. Questa ricerca cerca proprio un
|
||||||
|
ladder che PASSI il gate = che abbia corr bassa con gli sleeve esistenti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Risultato NUOVO — i ladder PASSANO il gate
|
||||||
|
|
||||||
|
Sì. Su **tutti gli asset liquidi** (BTC e ETH) e su quasi tutte le combinazioni TF×regime,
|
||||||
|
i ladder migliorano l'OOS Sharpe di portafoglio (10.86 → ~11.0–11.35) **con corr bassa**:
|
||||||
|
|
||||||
|
- **BTC** (15m/30m/1h, range o none): corr **0.15–0.27**, molto più ortogonale della
|
||||||
|
griglia ETH bocciata (0.40). OOS di portafoglio 11.1–11.35 (il guadagno OOS più grosso).
|
||||||
|
- **ETH** (15m/30m/1h): corr **0.21–0.57** (15m la peggiore, fino a 0.57; a 1h scende a
|
||||||
|
0.22–0.39). OOS di portafoglio ~10.9–11.1.
|
||||||
|
- **SOL/LTC**: SOL passa solo 2 celle, half-only, edge standalone quasi nullo
|
||||||
|
(full_sh ~0.2); **LTC è negativo ovunque** (full_sh −0.4…−0.8, bocciato) → solo BTC/ETH
|
||||||
|
liquidi danno un ladder vivo, come atteso.
|
||||||
|
|
||||||
|
Il gate **misura corr + OOS**, e su questi due assi i ladder BTC vincono nettamente.
|
||||||
|
|
||||||
|
### Migliori candidati (dai `verify` avversariali)
|
||||||
|
|
||||||
|
| Asset/TF/regime/struttura | OOS base→new | FULL DD standalone | FULL DD portafoglio (full / half) | corr | fee2x OOS | robust? |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **ETH 1h range2 rd0.16 ru0.04 L4** | 10.86→**11.00** | 11.79% | **2.70** / 2.52 | 0.305 | 4.89 | ✅ sì |
|
||||||
|
| **ETH 1h range2 rd0.16 ru0.06 L6** | 10.86→11.01 | 12.09% | 2.74 / 2.54 | 0.328 (al filo) | 5.05 | ✅ sì |
|
||||||
|
| **ETH 1h range2 rd0.16 ru0.06 L4** | 10.86→10.98 | 12.64% | **2.68** / 2.51 | 0.249 | 4.44 | ✅ sì |
|
||||||
|
| **ETH 30m range2 rd0.16 ru0.04 L3** | 10.86→10.93 | 12.56% | **2.48** / 2.41 | 0.223 | 4.21 | ✅ sì |
|
||||||
|
| **ETH 1h none rd0.20 ru0.06 L6** | 10.86→11.03 | 17.89% | 2.59 / 2.46 | 0.271 | 5.43 | ✅ sì |
|
||||||
|
| **ETH 30m none rd0.16 ru0.06 L4** | 10.86→10.92 | 18.68% | **2.37** / 2.35 | 0.319 | 5.42 | ✅ sì |
|
||||||
|
| **ETH 1h none rd0.16 ru0.06 L4** | 10.86→10.96 | 23.13% | **2.40** / 2.37 | 0.224 | 4.59 | ✅ sì |
|
||||||
|
| **BTC 1h none rd0.08 ru0.06 L3** | 10.86→**11.34** | **53.69%** | 3.11 / **2.48** | 0.216 | 5.78 | ⚠ half-only |
|
||||||
|
| **BTC 30m range2.5 rd0.08 ru0.06 L3** | 10.86→11.22 | 55.33% | 3.27 / **2.50** | 0.265 | 5.19 | ⚠ half-only |
|
||||||
|
| **BTC 30m none rd0.08 ru0.06 L3** | 10.86→11.32 | 55.33% | 3.27 / **2.44** | 0.220 | 5.37 | ⚠ half-only (tail) |
|
||||||
|
| **BTC 1h range1.5 rd0.20 ru0.06 L6** | 10.86→11.13 | 54.43% | 3.19 / **2.33** | **0.161** | 5.46 | ⚠ half-only (tail) |
|
||||||
|
|
||||||
|
`rd` = spacing down, `ru` = spacing up, `L` = livelli. Tutti i finalisti hanno plateau
|
||||||
|
robusto (4–7 vicini di struttura restano PROMOSSO) e fee2x ~= OOS standalone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. IL NODO — la FULL DD (che il gate IGNORA)
|
||||||
|
|
||||||
|
**Il gate verdict NON guarda la FULL DD.** Misura solo corr + OOS Sharpe/DD. Una griglia
|
||||||
|
long-only ha una coda di trend 2021/22 (viene comprata fino in fondo mentre il prezzo crolla
|
||||||
|
in un trend persistente) → la **FULL DD standalone** è il vero rischio nascosto. Il filtro
|
||||||
|
onesto è la **FULL DD di PORTAFOGLIO** (`full_full_dd` / `half_full_dd`), con soglia di
|
||||||
|
tail-risk **~+0.8pp sopra il baseline 2.47%** (cioè restare sotto ~3.3%).
|
||||||
|
|
||||||
|
Due famiglie nette emergono:
|
||||||
|
|
||||||
|
- **BTC** — FULL DD standalone **~54–67%** (la coda long-only è devastante, come temuto).
|
||||||
|
Il regime-gate range *taglia gli ingressi nei trend* ma **non immunizza** dalle posizioni
|
||||||
|
già aperte → a FULL size la FULL DD di portafoglio sale a **3.1–6.6%** (+0.6/+4.0pp,
|
||||||
|
**oltre la soglia tail** in quasi tutte le celle). **A HALF size invece rientra a 2.33–2.54%**
|
||||||
|
(~baseline) preservando l'OOS ~11.0–11.3. → **BTC deployabile SOLO a half-size**, e il
|
||||||
|
guadagno OOS più grosso (11.3+) viene proprio da queste celle a coda alta.
|
||||||
|
|
||||||
|
- **ETH** — FULL DD standalone **molto più bassa** del previsto: **12–33%** (1h range scende
|
||||||
|
a **~12%**), NON il ~60% temuto. Lo SL-buffer + flat-skip + il regime-gate range tengono la
|
||||||
|
griglia fuori dai trend estesi. Conseguenza: la FULL DD di portafoglio resta **2.37–2.74%**
|
||||||
|
(+0.0/+0.27pp, **sotto soglia anche a FULL size**) — in diversi casi *scende* sotto il
|
||||||
|
baseline (es. ETH 30m none L4 → 2.37%). → **ETH è la famiglia tail-safe.** Il prezzo è una
|
||||||
|
corr più alta (0.22–0.33 sui finalisti 1h/30m, fino a 0.57 sul 15m da scartare).
|
||||||
|
|
||||||
|
**Lettura chiave:** il regime-gate *abbassa* la FULL DD ma su BTC **non a sufficienza** a
|
||||||
|
full size (la coda 54% è troppo grande); è l'**half-size** a fare il lavoro. Su ETH la coda
|
||||||
|
è già piccola e il portafoglio l'assorbe. Quindi il discriminante reale è **asset (ETH vs
|
||||||
|
BTC) + size**, non solo il regime-gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Vantaggio Deribit — fill MAKER
|
||||||
|
|
||||||
|
Un Price Ladder è una griglia di **ordini LIMIT ai livelli** → su Deribit i fill sono
|
||||||
|
**MAKER (~0% fee)**, non taker. L'harness assume **0.10% RT taker** ovunque → è
|
||||||
|
**CONSERVATIVA**: la stessa griglia live pagherebbe meno fee e potrebbe rendere PIÙ del
|
||||||
|
backtest. Lo confermano i `verify`: il **fee2x** (raddoppio fee) lascia l'OOS Sharpe quasi
|
||||||
|
intatto (~98% trattenuto su quasi tutti i finalisti) → nessun fee-cliff, e con i fill maker
|
||||||
|
reali c'è upside, non downside, sul lato costi. (Il rischio resta la coda di trend e lo
|
||||||
|
slippage/fill in esecuzione reale, non le fee.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Verdetto operativo
|
||||||
|
|
||||||
|
**C'è un Price Ladder deployabile.** Il candidato più pulito su tutti gli assi
|
||||||
|
(PROMOSSO + FULL DD di portafoglio controllata a full size + plateau robusto + corr genuina):
|
||||||
|
|
||||||
|
> **ETH 1h, regime=range trend_max 2.0, rd=0.16 / ru=0.04 / 4 livelli, sl_buf 0.12 / tp_buf 0.05, max_bars 720.**
|
||||||
|
> OOS 10.86→11.00, FULL DD portafoglio **2.70% a full / 2.52% a half** (standalone 11.79%),
|
||||||
|
> corr 0.305, fee2x OOS 4.89, plateau 4 vicini PROMOSSO. **Deploy raccomandato a HALF size**
|
||||||
|
> (FULL DD +0.05pp, marginale; il caveat slippage live giustifica la prudenza).
|
||||||
|
|
||||||
|
**Alternative ETH equivalenti** (stessa banda di sicurezza, scegliere per corr/coda):
|
||||||
|
`ETH 1h range2 rd0.16 ru0.06 L4` (corr 0.249, la più bassa) e `ETH 30m range2 rd0.16 ru0.04 L3`
|
||||||
|
(FULL DD portafoglio 2.48, ~baseline). Tutte ETH 1h/30m **range-gated rd≥0.16** → NON scendere
|
||||||
|
sotto rd 0.16 (rd 0.12 fa risalire la corr verso 0.38 = ricomincia a somigliare alle fade ETH;
|
||||||
|
il 15m ha corr 0.43–0.57 = troppo ridondante, scartare).
|
||||||
|
|
||||||
|
**I BTC NON a full size.** Danno il guadagno OOS più grande (fino a 11.34) e la corr più bassa
|
||||||
|
(0.16–0.22 = la diversificazione migliore), MA la FULL DD standalone ~54% li rende
|
||||||
|
**tail-risky a full size** (FULL DD portafoglio 3.1–6.6%, oltre soglia). **Deployabili SOLO a
|
||||||
|
half-size** (FULL DD portafoglio 2.33–2.50), dove restano un diversificatore eccellente. Il
|
||||||
|
migliore BTC half-size è `BTC 1h range1.5 rd0.20 ru0.06 L6` (corr **0.161**, FULL DD portafoglio
|
||||||
|
half **2.33**).
|
||||||
|
|
||||||
|
**BLOCCO prima di qualsiasi deploy:** il gate PORT06 (corr + OOS) è **necessario ma non
|
||||||
|
sufficiente** — non controlla la FULL DD, e l'OOS held-out è un **regime calmo** (2024–25). La
|
||||||
|
coda di trend 2021/22 è reale e standalone è enorme su BTC. **Nessun deploy senza un gate
|
||||||
|
stressato sul 2022** (verificare la FULL DD di portafoglio attraverso il trend duro, non solo
|
||||||
|
l'OOS calmo) e una conferma su **ledger reale shadow** per slippage/fill maker. Il verdetto del
|
||||||
|
gate va sempre incrociato con la FULL DD di portafoglio (filtro vero) + fattibilità feed live +
|
||||||
|
regime recente — la stessa lezione del timing-sweep pairs/honest (2026-06-14).
|
||||||
|
|
||||||
|
**Sintesi:** ETH 1h/30m range-gated rd≥0.16 a half-size è il **deploy-candidate sano e
|
||||||
|
tail-safe**; BTC è il diversificatore migliore ma **solo a half-size** per la coda di trend.
|
||||||
|
Prossimo passo obbligatorio: stress-test 2022 + shadow ledger reale (i fill maker possono solo
|
||||||
|
migliorare il risultato sul lato fee).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. ⚠️ RETTIFICA CRITICA — il verdetto §5 è ROVESCIATO (critico + verifica dati)
|
||||||
|
|
||||||
|
Il critico di completezza + una verifica diretta dei dati **ribaltano la raccomandazione di
|
||||||
|
deploy della §5**. Messo a verbale: la §5 è SUPERATA da quanto segue.
|
||||||
|
|
||||||
|
**(1) I risultati BTC poggiano su DATI CORROTTI (spike-print), verificato.** La "coda di trend
|
||||||
|
BTC ~54%" su cui ruota la §3 NON è un crollo di mercato: è **feed sporco**. Misurato su `btc_1h`:
|
||||||
|
**37 barre con range intrabar >20%**, concentrate a **feb-2024** (es. 2024-02-13: low=38.580 con
|
||||||
|
close~49.968 = wick fasullo −23% che rimbalza nella stessa barra), con min 22.706 / max 104.688 in
|
||||||
|
un trimestre dove BTC stava ~47–57k. È lo **stesso spike-print testnet documentato ovunque in
|
||||||
|
CLAUDE.md** (TP_PHANTOM / feed congelato). L'engine SL-gap-aware filla su questi wick fantasma e la
|
||||||
|
griglia "compra" i dip fasulli → la coda BTC ~54% e parte dell'edge OOS sono **artefatto, non
|
||||||
|
mercato**. Conseguenza: i candidati BTC NON sono affidabili, e lo "stress 2022" proposto NON
|
||||||
|
toccherebbe la coda vera (che è a feb-2024, da dati sporchi). Serve `close_only` + pulizia feed.
|
||||||
|
|
||||||
|
**(2) Il guadagno OOS è RUMORE al peso deployabile.** +0.14 OOS Sharpe su base 10.86, con il ladder
|
||||||
|
che pesa **~5.3% full / ~2.7% half** in un portafoglio già a Sharpe ~11, su **592 giorni di un
|
||||||
|
solo regime calmo** (2024-10→2026-05, niente 2022). Nessun test di significatività
|
||||||
|
(block-bootstrap). A half-size il contributo è quasi inerte (a volte l'OOS DD *migliora*): è dentro
|
||||||
|
l'errore di campionamento, non un edge dimostrato.
|
||||||
|
|
||||||
|
**(3) La diversificazione ETH è più sottile di quanto venduto.** Il finalista ETH "raccomandato" è
|
||||||
|
**corr 0.305** (non 0.15–0.20: quella è BTC half-only non deployabile), già vicino allo **0.40 che
|
||||||
|
fece bocciare la griglia ETH originale**. Peggio: il `regime_mask` usa **lo stesso filtro
|
||||||
|
trend-distance** (|close−EMA200|/ATR) delle fade ETH già live → la "diversificazione" è in parte
|
||||||
|
illusoria (gate e sleeve condividono il filtro). La corr misurata è **daily in regime calmo**, NON
|
||||||
|
co-tail: una griglia long-only e una fade reversion possono co-crollare in un trend (entrambe
|
||||||
|
"comprano il calo") — la metrica che conterebbe (co-movimento nella coda) non è calcolata.
|
||||||
|
|
||||||
|
**(4) La FULL DD standalone è mascherata dal compounding.** Il finalista ETH compone a **~31x
|
||||||
|
(+3.1M%) in 5.4 anni**: una "FULL DD 11.79%" accanto a un ritorno di milioni-% è artefatto di
|
||||||
|
notional reinvestito, e il daily-resample **nasconde l'inventory-risk intrabar** che il docstring
|
||||||
|
di `grid_game_gate.py` (righe 5–7) avverte esplicitamente di non vedere.
|
||||||
|
|
||||||
|
**(5) Stress disponibile NON usato.** `ladder_search`/`grid_mtm` hanno `close_only=True` (filla solo
|
||||||
|
sul close, neutralizza gli spike-print): NON è stato girato sui finalisti — proprio il test che
|
||||||
|
smonterebbe la coda BTC fasulla.
|
||||||
|
|
||||||
|
### Verdetto rettificato
|
||||||
|
|
||||||
|
**NON c'è un Price Ladder deployabile dimostrato.** La ricerca ha prodotto (a) una **harness onesta
|
||||||
|
e validata** (`ladder_search.py` + regime-gate, riproduce esatto il gate noto), (b) un **risultato
|
||||||
|
negativo/guardingo** robusto: ogni candidato o è ridondante (ETH, corr→0.40), o poggia su dati
|
||||||
|
sporchi (BTC spike-print), o è rumore al peso deployabile. **Il multi-agente + critico ha evitato
|
||||||
|
un deploy ingiustificato** — esattamente la lezione anti-squeeze del progetto.
|
||||||
|
|
||||||
|
**Prima di riconsiderare QUALSIASI ladder servono, in ordine:** (1) **pulire il feed BTC** dagli
|
||||||
|
spike-print + girare `close_only` sui finalisti; (2) **test di significatività** del delta OOS
|
||||||
|
(block-bootstrap) al peso reale; (3) **co-tail / corr nei crash** (2022), non corr daily calma;
|
||||||
|
(4) isolare e spiegare l'**evento feb-2024**. Config live **invariata**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. RE-GATE su DATI PULITI (post clean_feed.py) — quadro onesto finale
|
||||||
|
|
||||||
|
Dopo `clean_feed.py` (254 spike-print riparati con Binance) ho ri-gateato i top candidati con
|
||||||
|
le verifiche che il critico chiedeva (`ladder_regate_clean.py`): gate PORT06 + stress
|
||||||
|
`close_only` + **DD per anno** sulla storia completa.
|
||||||
|
|
||||||
|
| Candidato | DD (gate, 2021+) | OOS Sh | corr | fee2x | close_only OOS | gate½ | **DD 2018 (tail vero)** |
|
||||||
|
|---|---:|---:|---:|---:|---:|---|---:|
|
||||||
|
| ETH 1h rd0.16 ru0.04 L4 range2 | 12.4% | 4.70 | 0.29 | 4.62 | 0.24 | PROMOSSO | −52.4% |
|
||||||
|
| ETH 1h rd0.16 ru0.06 L4 range2 | 12.6% | 4.25 | 0.23 | 4.17 | 0.20 | PROMOSSO | −49.9% |
|
||||||
|
| BTC 1h rd0.08 ru0.06 L3 none | 10.8% | 5.90 | 0.24 | 5.78 | 0.60 | PROMOSSO | −50.3% |
|
||||||
|
| **BTC 1h rd0.20 ru0.06 L6 range1.5** | 14.8% | 5.58 | 0.23 | 5.46 | 0.89 | PROMOSSO | **−27.7%** |
|
||||||
|
| BTC 30m rd0.08 ru0.06 L3 none | 14.8% | 5.47 | 0.25 | 5.37 | 1.07 | PROMOSSO | −36.6% |
|
||||||
|
|
||||||
|
**Cosa cambia coi dati puliti:**
|
||||||
|
1. ✅ **L'obiezione "coda artefatto" CADE.** Il 54% del BTC era spike-print 2024: ora la DD del
|
||||||
|
gate (2021+) è **~11-15%** e tutti i candidati restano PROMOSSO a half-size, corr 0.22-0.29,
|
||||||
|
reggono fee2x. Sono candidati *veri*, non artefatti.
|
||||||
|
2. 🔴 **Emerge il tail VERO: il 2018** (−44/−52% standalone), che **il gate NON VEDE** perché
|
||||||
|
l'IDX del progetto parte dal 2021-01-01. Una griglia long-only sarebbe stata sventrata nel
|
||||||
|
bear 2018 (BTC −84% sull'anno). È il nuovo rischio portante — e una **lacuna metodologica
|
||||||
|
generale**: tutte le metriche "FULL" del progetto escludono il 2018. Mitigante reale: il
|
||||||
|
**regime-gate stretto** (BTC rd0.20 L6 **range1.5**) dimezza il tail 2018 a **−27.7%** (vs
|
||||||
|
−50% senza gate) → il gate-di-regime fa il suo lavoro contro i bear sostenuti.
|
||||||
|
3. 🟡 **L'edge dipende dai fill INTRABAR.** `close_only` (fill solo sul close) fa crollare l'OOS
|
||||||
|
(4.7→0.24 ETH, 5.9→0.6 BTC). Per ordini LIMIT i fill intrabar sono legittimi (close_only è
|
||||||
|
troppo severo come stress), ma il gap segnala forte **sensibilità all'ipotesi di fill** →
|
||||||
|
il ledger shadow reale è indispensabile prima di fidarsi.
|
||||||
|
4. 🟡 Restano: corr 0.22-0.29 (moderata), guadagno OOS ai limiti del rumore al peso deployabile
|
||||||
|
(~2.7% half), OOS singolo regime calmo.
|
||||||
|
|
||||||
|
**Verdetto finale (clean data):** la pulizia ha rimosso l'obiezione FALSA (coda artefatto) e
|
||||||
|
promosso i candidati a *genuini*, ma ha anche scoperto il tail REALE (2018, gate-cieco) e
|
||||||
|
confermato due cautele (fill intrabar, OOS rumoroso). **Il candidato più sensato è ora
|
||||||
|
`BTC 1h rd0.20 ru0.06 L6 regime=range trend_max1.5`**: PROMOSSO, corr 0.23, e la **miglior coda
|
||||||
|
2018 (−27.7%)** grazie al regime-gate. **Prima di QUALSIASI deploy**: (1) backtest che INCLUDA il
|
||||||
|
2018 nel gate (non solo IDX 2021+), (2) ledger shadow reale per i fill intrabar/maker, (3) half-size.
|
||||||
|
La pulizia del feed resta il guadagno trasversale più grande (irrobustisce ogni backtest del progetto).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. I 3 PASSI PRE-DEPLOY + STUDIO SL/TP (ladder_sltp_study.py)
|
||||||
|
|
||||||
|
**[1] Valutazione 2018-INCLUSIVE** (standalone, tutta la storia; il gate IDX2021+ è cieco al 2018):
|
||||||
|
| Candidato (sl0.12/tp0.05) | full_sh | full_dd | oos_sh | DD 2018 | DD 2021 |
|
||||||
|
|---|---:|---:|---:|---:|---:|
|
||||||
|
| BTC 1h L6 **range1.5** | 3.18 | −27.7% | 4.95 | **−27.7%** | −14.8% |
|
||||||
|
| BTC 1h L3 none | 2.93 | −60.2% | 5.51 | **−50.3%** | −10.8% |
|
||||||
|
|
||||||
|
Il regime-gate già dimezza il tail vero (2018: −50%→−27.7%). Senza gate la griglia long-only è insostenibile (−60%).
|
||||||
|
|
||||||
|
**Studio SL/TP** (sweep sl_buf × tp_buf sul candidato regime-gated, full 2018+):
|
||||||
|
- **Sweet spot: `sl_buf=0.10, tp_buf=0.03` → DD 2018 −23.5%** (da −27.7%), oos_sh 5.06, full_sh 2.92. Migliora il tail SENZA intaccare l'edge.
|
||||||
|
- **Lezione (conferma il prior del progetto):** un SL **troppo stretto PEGGIORA** (sl 0.06 → −33/−35%): stoppa e rideploya più in basso nel coltello che cade = la trappola del falso-negativo mean-reversion. Un SL **troppo largo** (0.20) idem (−29/−31%, oos più basso). Il bene sta in mezzo (0.10).
|
||||||
|
- **Il SL da solo (senza regime-gate) è ERRATICO** (tabella L3 none non-monotona: −20% → −52% → −43% al variare di pochi punti): è il **regime-gate** il controllo primario della coda; il SL moderato fine-tuna. Catastrophe-control = "non deployare nel bear" (regime) + "cappa l'episodio" (SL 0.10), NON uno stop stretto.
|
||||||
|
|
||||||
|
**[2] Fill maker vs taker** (Deribit: i livelli sono LIMIT → maker):
|
||||||
|
| fee | oos_sh | DD 2018 | gate½ |
|
||||||
|
|---|---:|---:|---|
|
||||||
|
| taker 0.10% RT (harness) | 5.06 | −23.5% | PROMOSSO (OOS 10.86→11.0, corr 0.195) |
|
||||||
|
| **maker 0%** (Deribit reale) | 5.18 | −22.7% | PROMOSSO (OOS→11.0, corr 0.194) |
|
||||||
|
|
||||||
|
Il maker è leggermente MIGLIORE → la harness taker è conservativa, nessun fee-cliff. (La parte live = shadow ledger reale resta il passo operativo finale.)
|
||||||
|
|
||||||
|
**[3] Half-size:** il gate "half" è già a metà size e PROMOSSO; la coda 2018 standalone −23.5% si dimezza sul book (~−12% in un 2018-repeat a half-size) = reale ma gestibile.
|
||||||
|
|
||||||
|
### CONFIG FINALE raccomandata (con SL/TP aggiunti)
|
||||||
|
> **BTC 1h · regime=range trend_max 1.5 · rd 0.20 / ru 0.06 / 6 livelli · sl_buf 0.10 / tp_buf 0.03 · HALF-size.**
|
||||||
|
> - Tail 2018-inclusive: −23.5% standalone (~−12% sul book a half) — reale ma controllato dal regime-gate + SL moderato.
|
||||||
|
> - Gate PORT06: PROMOSSO (OOS 10.86→11.0, corr 0.195); maker Deribit lo migliora; nessun fee-cliff.
|
||||||
|
> - **Unico passo residuo: shadow ledger reale** (fill intrabar/maker su Deribit) — operativo, non backtestabile.
|
||||||
|
|
||||||
|
Caveat onesti residui: l'OOS gain al peso deployabile è modesto (10.86→11.0); il tail 2018 non è zero (−12% a half); l'edge dipende dai fill intrabar (per i LIMIT è legittimo, ma lo shadow lo deve confermare).
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,181 @@
|
|||||||
|
# Piano operativo — Micro-test MAINNET (Deribit)
|
||||||
|
|
||||||
|
**Obiettivo.** Rispondere all'unica domanda che testnet e backtest non possono:
|
||||||
|
*quando si passa ai fill VERI, quanto edge sopravvive?* Si misura con poco denaro
|
||||||
|
reale (€500–1500) per 2–4 settimane, leggendo il report **ledger-vs-backtest**.
|
||||||
|
Costo del verdetto: poche centinaia di € invece di scoprirlo a €5k.
|
||||||
|
|
||||||
|
**Perché serve (e perché testnet non basta).** Testnet ha feed farlocchi — il
|
||||||
|
2026-06-13 `ETH-PERPETUAL` inverse era **congelato a 1661.95 da 12h+** mentre il
|
||||||
|
prezzo reale si muoveva (gap −1.3%); BTC era vivo. Su mainnet l'arbitraggio tiene
|
||||||
|
inverse ≈ lineare ≈ realtà (entro bps): **il micro-test risolve il problema feed
|
||||||
|
per costruzione**, oltre a dare fill/slippage/fee reali. (Il 2026-06-14 `ETH-PERPETUAL`
|
||||||
|
testnet era ancora congelato a 1661.95 da 36h+ → conferma definitiva: testnet è inaffidabile.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STATO (2026-06-16)
|
||||||
|
|
||||||
|
- ✅ **Token mainnet wired e VERIFICATO.** `MAINNET_TOKEN` (da cerbero-mcp, che ha già le chiavi
|
||||||
|
Deribit LIVE) copiato in **`.env.mainnet`** (chmod 600, gitignored), SEPARATO dal `.env`
|
||||||
|
condiviso. Verifica read-only: `is_mainnet()=True`, auth OK su `get_account_summary` (nessun
|
||||||
|
ordine). Il `.env` condiviso resta testnet → il runner attuale NON flippa a mainnet a un riavvio.
|
||||||
|
- ✅ **FASE 0 SMOKE VERDE (2026-06-16).** Catena d'esecuzione validata su mainnet con soldi VERI a
|
||||||
|
costo ~0: LIMIT SELL `ETH_USDC` spot 0.0138 ETH @ floor 1776.3 (marketable, NON market) →
|
||||||
|
`order_state=filled`, average_price **1778.1** (= bid, zero slippage negativo), **fee 0.0**.
|
||||||
|
Riconciliazione conto: USDC 25.00→**49.54** (+24.54), ETH→0.000078 dust, nessun ordine resting.
|
||||||
|
Provato end-to-end: routing live + auth + place_order limit + fill + verifica trade + balance.
|
||||||
|
(Doppio scopo: consolidare l'ETH stranded in margine USDC + smoke.) Diario
|
||||||
|
`docs/diary/2026-06-16-mainnet-fase0-smoke.md`.
|
||||||
|
- 🟡 **FINANZIAMENTO PARZIALE: ~$49.54 USDC** (deposito iniziale ~$50: 25 USDC + 0.0138 ETH, ora
|
||||||
|
consolidati). **Sotto il minimo €500** del piano → basta per la meccanica (Fase 0 ✅) ma NON per
|
||||||
|
testare l'edge (a $50 di margine il rumore di arrotondamento BTC soffoca il segnale). **Serve
|
||||||
|
top-up a ~€500-1000 USDC** prima della Fase 1.
|
||||||
|
- ⏭️ Appena finanziato a ~€1000: avviare il micro-test come **servizio separato** (env_file
|
||||||
|
`.env.mainnet`, dir dati pulita, `portfolios.yml` fade-only) → Fase 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisiti (umani, fuori dal codice)
|
||||||
|
|
||||||
|
1. **Conto Deribit MAINNET** (KYC) finanziato con **~€1000** in USDC (vedi sizing).
|
||||||
|
2. **MAINNET_TOKEN Cerbero** abilitato al trading mainnet sul conto (lato CerberoSuite).
|
||||||
|
3. Decidere il capitale: **€1000 raccomandato** (vedi sezione Sizing).
|
||||||
|
|
||||||
|
> NB sicurezza: il MAINNET_TOKEN dà accesso a soldi veri. Va in `.env` (gitignored),
|
||||||
|
> mai committato. Un bot-tag dedicato (`pythagoras-mainnet`) per isolarlo nell'audit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Come si punta a mainnet (già pronto, 1 sola modifica .env)
|
||||||
|
|
||||||
|
Il client legge il token da env (default = testnet). Il token mainnet vive in un file
|
||||||
|
**`.env.mainnet`** DEDICATO (NON nel `.env` condiviso, che resta testnet → il runner attuale
|
||||||
|
non flippa a mainnet a un riavvio):
|
||||||
|
|
||||||
|
```
|
||||||
|
# .env.mainnet (gitignored, chmod 600) — caricato SOLO dal servizio micro-test mainnet
|
||||||
|
CERBERO_TOKEN=<MAINNET_TOKEN>
|
||||||
|
CERBERO_BOT_TAG=pythagoras-mainnet
|
||||||
|
```
|
||||||
|
|
||||||
|
Il servizio micro-test usa `env_file: [.env, .env.mainnet]` (le chiavi di `.env.mainnet`
|
||||||
|
prevalgono → Telegram da `.env`, token mainnet da `.env.mainnet`).
|
||||||
|
|
||||||
|
**Nessuna modifica di codice.** `CerberoClient.is_mainnet()` lo rileva; il runner,
|
||||||
|
l'ExecutionClient e gli smoke usano lo stesso client. Per tornare a testnet: togliere
|
||||||
|
le due righe. (Verificato: default invariato testnet, override flippa il rilevamento.)
|
||||||
|
|
||||||
|
> Consiglio: **non riusare il container/data testnet**. Avviare il micro-test su una
|
||||||
|
> dir dati pulita (`data/` separata o nuovo volume) così i ledger reali mainnet non
|
||||||
|
> si mescolano allo storico testnet, e il report parte clean-start.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sizing & taglia — perché €1000 e SOLO le fade in fase 1
|
||||||
|
|
||||||
|
Notional = `alloc_sleeve × position_size × leverage`; amount = notional/prezzo,
|
||||||
|
quantizzato allo step Deribit. Rumore di arrotondamento = step/amount.
|
||||||
|
|
||||||
|
**Fades-only (6 sleeve equal, pos 0.5 × leva 3) — rumore arrotondamento:**
|
||||||
|
|
||||||
|
| Capitale | notional/fade | round% BTC | round% ETH |
|
||||||
|
|----------|---------------|-----------|-----------|
|
||||||
|
| €500 | $125 | 5.2% | 1.3% |
|
||||||
|
| **€1000** | **$250** | **2.6%** | **0.7%** |
|
||||||
|
| €1500 | $375 | 1.7% | 0.4% |
|
||||||
|
|
||||||
|
→ **€1000 è il punto giusto**: BTC ~2.6% di rumore (leggibile), abbastanza piccolo
|
||||||
|
da non far male. €500 funziona ma BTC sale al 5%; €1500 è il più pulito.
|
||||||
|
|
||||||
|
**Perché NIENTE pairs/SH01 in fase 1:**
|
||||||
|
- **Pairs** (pos 0.13, 2 gambe): a €1000 il notional/gamba è ~$21 → arrotondamento
|
||||||
|
**~30%** (la gamba alt anche peggio): a questa taglia misureresti l'arrotondamento,
|
||||||
|
non l'edge. Servono ~€5–8k per pairs puliti. Inoltre l'executor a 2 gambe ha
|
||||||
|
leg-risk: più superfici d'errore. → fuori dalla fase 1.
|
||||||
|
- **SH01**: bootstrap full-history dal parquet inverse; su mainnet va ri-derivato il
|
||||||
|
parquet da prezzi mainnet. Diversificatore, non core. → fuori dalla fase 1.
|
||||||
|
|
||||||
|
Le **6 fade single-leg (MR01/02/07 × BTC/ETH)** sono il **core edge**, l'esecuzione
|
||||||
|
più semplice (open market + TP limit resting + disaster-SL), e quelle col track
|
||||||
|
record validato col worker reale. Sono il test giusto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fasi
|
||||||
|
|
||||||
|
### Fase 0 — Smoke mainnet (€0 di rischio reale, 1 ordine minimo)
|
||||||
|
Con il token mainnet attivo, eseguire `live_exec_smoke.py` (ordine→verifica→fee) e
|
||||||
|
`live_shadow_smoke.py` su **size minima** per confermare: auth mainnet OK, strumenti
|
||||||
|
USDC risolti, fill verificato per order_id, fee reali lette. Se uno fallisce, STOP.
|
||||||
|
|
||||||
|
### Fase 1 — Solo fade, €1000, 2–4 settimane
|
||||||
|
`portfolios.yml` per il micro-test:
|
||||||
|
```yaml
|
||||||
|
overrides:
|
||||||
|
total_capital: 1000
|
||||||
|
leverage: 3 # config che vorremmo deployare (misura il vero)
|
||||||
|
position_size: 0.5
|
||||||
|
paper_sleeves: [TR01, ROT02, TSM01, XS01, PR01..., SH01...] # tutto tranne le 6 fade -> paper
|
||||||
|
execution:
|
||||||
|
enabled: true
|
||||||
|
sleeves: [MR01, MR02, MR07] # solo fade (DIP01 opz.: 1h BTC, core honest)
|
||||||
|
pairs_enabled: false # niente 2-gambe in fase 1
|
||||||
|
disaster_sl_pct: 0.30 # rete di coda invariata
|
||||||
|
real_truth: true
|
||||||
|
```
|
||||||
|
> Alternativa prudente settimana-1: `leverage: 2` per dimezzare il DD mentre si
|
||||||
|
> verifica l'esecuzione, poi 3.
|
||||||
|
|
||||||
|
Avviare il runner mainnet (container separato o stessa immagine con `.env` mainnet),
|
||||||
|
+ reconciler orario + report ledger-vs-backtest giornaliero `--since <data-start>`.
|
||||||
|
|
||||||
|
### Fase 2 — Verdetto (dal report ledger-vs-backtest)
|
||||||
|
Aspettare **≥10–20 trade reali** (su mainnet le fade ETH/BTC tradano regolarmente, no
|
||||||
|
feed congelato). Criterio (lo stesso del report, soglie già nel verdetto 🟢/🟡/🔴):
|
||||||
|
- 🟢 **slippage medio ≤15 bps E leakage per-trade < €0.30** in relativo → l'edge regge,
|
||||||
|
si scala.
|
||||||
|
- 🟡 leakage moderato → estendere il campione, capire dove perde.
|
||||||
|
- 🔴 slippage >40 bps o leakage che mangia l'edge → **NON scalare**; l'edge non
|
||||||
|
sopravvive all'esecuzione (hai perso ~€20–50, non €5k).
|
||||||
|
|
||||||
|
### Fase 3 — Espansione (solo se 🟢)
|
||||||
|
1. Alzare il capitale (€1000→€2k→€5k) — i pairs diventano puliti sopra ~€5k.
|
||||||
|
2. Riaccendere pairs (a conto flat) e SH01 (con parquet mainnet).
|
||||||
|
3. Continuare a leggere il report ad ogni step.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rischi & safeguard (già implementati)
|
||||||
|
|
||||||
|
- **Disaster-SL on-book −30%** ad ogni apertura → protegge da outage del runner.
|
||||||
|
- **Reconciler orario** (`reconcile_account --telegram`) → drift conto-vs-libri +
|
||||||
|
guard `STALE_REAL_POSITION` (worker non gestito).
|
||||||
|
- **Netting close** (v1.1.25) → niente gambe orfane/close cappati.
|
||||||
|
- **Real-truth ledger** → equity = soldi veri sul conto.
|
||||||
|
- **Kill-switch**: `execution.enabled: false` (smette di aprire reali, gestisce le
|
||||||
|
uscite) oppure stop del container + flatten manuale (procedura già rodata).
|
||||||
|
- Perdita massima plausibile in fase 1: qualche % di €1000 = **€20–80**. È il prezzo
|
||||||
|
del verdetto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cosa NON fare
|
||||||
|
|
||||||
|
- Non partire con pairs/SH01 (rumore/complessità).
|
||||||
|
- Non scalare prima del verdetto 🟢 stabile per più giorni.
|
||||||
|
- Non mescolare i dati testnet e mainnet nello stesso ledger.
|
||||||
|
- Non committare mai il MAINNET_TOKEN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist di avvio
|
||||||
|
|
||||||
|
- [x] MAINNET_TOKEN Cerbero abilitato + verificato (auth OK, is_mainnet=True) — 2026-06-14
|
||||||
|
- [x] Token in `.env.mainnet` (separato, gitignored, chmod 600) + `CERBERO_BOT_TAG=pythagoras-mainnet`
|
||||||
|
- [x] Fase 0 smoke mainnet VERDE (LIMIT SELL ETH_USDC filled, fill@bid, fee 0, balance riconciliato) — 2026-06-16
|
||||||
|
- [~] Conto Deribit mainnet finanziato: **~$49.54 USDC** (parziale, sotto il min €500) ← **TOP-UP a ~€1000 = BLOCCO Fase 1**
|
||||||
|
- [ ] Servizio micro-test separato (env_file `.env.mainnet`, dir dati pulita)
|
||||||
|
- [ ] `portfolios.yml` micro-test (fade-only, €1000, exec sleeves MR01/02/07)
|
||||||
|
- [ ] runner mainnet + reconciler + report ledger-vs-backtest schedulati
|
||||||
|
- [ ] dopo ≥10–20 trade: leggere il verdetto → scala / aspetta / stop
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Config LIVE del trader a portafoglio. Seleziona UN portafoglio attivo
|
||||||
|
# (definito in scripts/portfolios/_defs.py) e ne fa l'override dei parametri operativi.
|
||||||
|
#
|
||||||
|
# ============ MICRO-TEST MAINNET — soldi VERI (Fase 1, 2026-06-17) ============
|
||||||
|
# Conversione da testnet a mainnet Deribit (vedi docs/specs/mainnet-microtest-plan.md).
|
||||||
|
# Capitale REALE €500 (periodo di prova; poi si scala col verdetto ledger-vs-backtest).
|
||||||
|
# Eseguono reale SOLO le 6 fade (MR01/MR02/MR07 x BTC/ETH, 15m) + DIP01 (BTC 1h);
|
||||||
|
# pairs/SH01/multi-asset -> PAPER (sola statistica, fuori dal pool/conto). Il token
|
||||||
|
# mainnet arriva da .env.mainnet (env_file del servizio). Ledger ripartito da ZERO:
|
||||||
|
# lo storico testnet e' archiviato in data/_reset_backup/pre_mainnet_20260617-205943.tgz.
|
||||||
|
active: PORT06
|
||||||
|
overrides:
|
||||||
|
# CAPITALE REALE del micro-test (era 2000 su testnet). €500 = minimo del piano
|
||||||
|
# (rumore arrotondamento BTC ~5-6%); si sale dopo il verdetto ledger-vs-backtest.
|
||||||
|
total_capital: 500
|
||||||
|
# equal-weight: nel pool REALE restano solo FADE (6) + DIP01 (1) -> 1/7 ciascuno.
|
||||||
|
# I cap PAIRS/SHAPE non servono piu' (quelle famiglie sono PAPER, fuori dal pool).
|
||||||
|
weighting: equal
|
||||||
|
# Leva 3x (scelta utente 2026-06-17 per il micro-test = config che vogliamo deployare,
|
||||||
|
# frontiera ACCEL50). NB su soldi veri al capitale minimo: DD pieno (alt prudente 2x).
|
||||||
|
leverage: 3
|
||||||
|
rebalance: 1D
|
||||||
|
poll_seconds: 60
|
||||||
|
# Gate feed CONGELATO: su mainnet l'arbitraggio tiene il feed vivo (niente freeze come
|
||||||
|
# sul testnet), lo lascio attivo come rete di sicurezza (non scattera' su BTC/ETH
|
||||||
|
# liquidi). 0 = disattivo.
|
||||||
|
feed_freeze_gate_bars: 24
|
||||||
|
# SLEEVE PAPER (fuori dal pool/pesi/ledger): i €500 si dividono SOLO tra i 7 sleeve
|
||||||
|
# REALI (6 fade + DIP01). Pairs (PR01, 2 gambe) e SH01 fuori dalla Fase 1: a €500 il
|
||||||
|
# rumore di arrotondamento li soffoca (pairs ~30%/gamba; servono ~€5-8k) e aggiungono
|
||||||
|
# superfici d'errore (leg-risk). Multi-asset (TR01/ROT02/TSM01/XS01) paper come sempre.
|
||||||
|
paper_sleeves: [TR01, ROT02, TSM01, XS01, PR01, SH01]
|
||||||
|
# Frazione di capitale-sleeve per posizione (0.5 con leva 3 = 1.5x la fetta impegnata).
|
||||||
|
position_size: 0.5
|
||||||
|
# Override per-famiglia: irrilevante per il conto reale (i pairs sono PAPER), tenuto
|
||||||
|
# solo perche' i worker pairs in sola-statistica dimensionino come da gate storico.
|
||||||
|
position_size_family: {PAIRS: 0.13}
|
||||||
|
# PAPER_EXTRA (2026-06-18): sleeve paper definiti SOLO qui (NON in _defs.py/PORT06) ->
|
||||||
|
# NON entrano nel backtest canonico/regression-lock. Shadow STAGE 1 del Price Ladder:
|
||||||
|
# GridWorker SIM-only su feed Deribit BTC 1h (NESSUN ordine reale; kind=grid non e' mai
|
||||||
|
# eseguito reale per costruzione). Config = re-gate su dati puliti (branch
|
||||||
|
# price_ladder_research): regime-gate range trend_max 1.5, rd0.20/ru0.06, 6 livelli,
|
||||||
|
# sl0.10/tp0.03. position_size 0.15 PINNATO (canonico validato; senza, erediterebbe il
|
||||||
|
# 0.5 globale del micro-test). Gira in data/portfolio_paper_stats/GRID_BTC/.
|
||||||
|
paper_extra:
|
||||||
|
- sid: GRID_BTC
|
||||||
|
kind: grid
|
||||||
|
name: GRID
|
||||||
|
asset: BTC
|
||||||
|
tf: "1h"
|
||||||
|
cluster: BTC-rev
|
||||||
|
params: {tf: "1h", range_down: 0.20, range_up: 0.06, levels: 6,
|
||||||
|
sl_buf: 0.10, tp_buf: 0.03, max_bars: 720,
|
||||||
|
regime: range, trend_max: 1.5, position_size: 0.15}
|
||||||
|
# Esecuzione REALE su Deribit MAINNET. Solo i 7 single-leg con TP/SL in metadata:
|
||||||
|
# 6 fade (MR01/MR02/MR07 x BTC/ETH 15m) + DIP01 (BTC 1h). Ordini sui LINEARI USDC
|
||||||
|
# (payoff lineare = matematica del backtest; fee/PnL in USDC).
|
||||||
|
execution:
|
||||||
|
# DISATTIVATA 2026-06-18: la verifica su dati REALI (Binance spot + perp Deribit
|
||||||
|
# MAINNET) ha mostrato che le 6 fade NON hanno edge sui prezzi veri (tutte negative
|
||||||
|
# ogni anno, Sharpe -3..-5) -> il +1000% "validato OOS" era un artefatto dei print
|
||||||
|
# fantasma del feed testnet Cerbero. Micro-test fermato e conto flattato a mano.
|
||||||
|
# Tenuta a false come blindatura: un 'docker compose up'/deploy NON deve riprendere
|
||||||
|
# a tradere reale finche' non ri-validiamo l'intera libreria su dati reali.
|
||||||
|
enabled: false
|
||||||
|
# SOLO fade + DIP01 in Fase 1 (SH01 e pairs -> paper, vedi paper_sleeves).
|
||||||
|
sleeves: [MR01, MR02, MR07, DIP01]
|
||||||
|
instruments:
|
||||||
|
BTC: BTC_USDC-PERPETUAL
|
||||||
|
ETH: ETH_USDC-PERPETUAL
|
||||||
|
# niente esecuzione a 2 gambe in Fase 1 (pairs sono paper).
|
||||||
|
pairs_enabled: false
|
||||||
|
# Disaster-bracket on-book (~-30%) a ogni apertura: assicurazione per gli outage
|
||||||
|
# del runner. In operativita' normale non scatta mai -> 0 costo. 0 = disattivo.
|
||||||
|
disaster_sl_pct: 0.30
|
||||||
|
# REAL-TRUTH: equity/ribilanci/sizing derivano dai FILL REALI (fee reali incluse),
|
||||||
|
# non dal sim. Il sim resta solo diagnostica nel log CLOSE.
|
||||||
|
real_truth: true
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"""REGRESSION-LOCK COMUNE dei gate PORT06 live (exit16 / trendmax / dip01).
|
||||||
|
|
||||||
|
Queste funzioni erano copiate quasi-verbatim in exit16_port06_impact.py,
|
||||||
|
trendmax_port06_impact.py e dip01_exit16_impact.py. Sono il regression-lock
|
||||||
|
delle DECISIONI LIVE (EXIT-16, swap hurst->trend, DIP01 EXIT-16): la copy-drift
|
||||||
|
fra le copie avrebbe corrotto i verdetti, quindi vivono qui in un'unica copia.
|
||||||
|
|
||||||
|
NON cambiare la matematica: i gate devono restare riproducibili byte-a-byte.
|
||||||
|
Se un nuovo gate richiede un comportamento diverso, PARAMETRIZZARE (come fu
|
||||||
|
fatto per hurst_mask/trend_max), mai biforcare una copia.
|
||||||
|
|
||||||
|
Contenuto:
|
||||||
|
build_trades_variant : replay ESATTO di risk_management.build_trades sulle
|
||||||
|
fade (mode="orig" == canonico), con i rami varianti
|
||||||
|
EXIT-16 (mode="exit16"), filtro trend (trend_max) e
|
||||||
|
loss-guard Hurst (hurst_mask) parametrici.
|
||||||
|
equity_from_trades : trade -> equity giornaliera normalizzata su IDX
|
||||||
|
(stesso flusso di combine_portfolio.fade_daily_equity).
|
||||||
|
port_metrics : metriche FULL/OOS del portafoglio con la STESSA
|
||||||
|
matematica pesi di Portfolio.backtest (weight_vector
|
||||||
|
su tutti gli sleeve, ribilancio come port_returns).
|
||||||
|
dd : max drawdown % di una equity.
|
||||||
|
|
||||||
|
NB: l'engine DIP01 (dip_trades in dip01_exit16_impact.py) NON e' una copia di
|
||||||
|
build_trades_variant ma un sibling deliberatamente diverso (long-only, mode
|
||||||
|
"orig_gap" gap-aware, j clampato a n-1 a fine serie, niente filtri trend/hurst)
|
||||||
|
-> resta nel suo script.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from scripts.analysis.strategy_research import atr
|
||||||
|
from scripts.analysis.risk_management import FEE_RT, LEV, POS, INIT
|
||||||
|
from scripts.analysis.combine_portfolio import (
|
||||||
|
_norm, IDX, port_returns, metrics, SPLIT,
|
||||||
|
)
|
||||||
|
from src.portfolio import weighting as W
|
||||||
|
|
||||||
|
BUFFER = 0.5 # EXIT-16 close-confirm (come in produzione)
|
||||||
|
EMA_LONG = 200
|
||||||
|
|
||||||
|
|
||||||
|
def build_trades_variant(ents, df, mode, trend_max, hurst_mask=None,
|
||||||
|
buffer=BUFFER, lev=LEV, fee_rt=FEE_RT, ema_long=EMA_LONG):
|
||||||
|
"""Replica ESATTA di risk_management.build_trades, con i rami varianti.
|
||||||
|
|
||||||
|
mode="orig" : SL intrabar al livello (SL prima del TP) == canonico.
|
||||||
|
mode="exit16" : SL intrabar OFF; TP intrabar al livello (priorita' nel bar);
|
||||||
|
SL solo se il CLOSE sfonda sl0 -/+ buffer*ATR14[j], fill a close[j].
|
||||||
|
trend_max : None = filtro OFF; 3.0 = config live.
|
||||||
|
hurst_mask : bool[i]=True -> salta l'ingresso (loss-guard storico).
|
||||||
|
"""
|
||||||
|
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
||||||
|
n = len(c)
|
||||||
|
a = atr(df, 14)
|
||||||
|
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
|
||||||
|
fee = fee_rt * lev
|
||||||
|
out = []
|
||||||
|
last = -1
|
||||||
|
for e in ents:
|
||||||
|
i, d = e["i"], e["d"]
|
||||||
|
if i <= last or i + 1 >= n:
|
||||||
|
continue
|
||||||
|
if hurst_mask is not None and hurst_mask[i]:
|
||||||
|
continue
|
||||||
|
if trend_max is not None and a[i] and abs(c[i] - el[i]) / a[i] > trend_max:
|
||||||
|
continue
|
||||||
|
entry = c[i]
|
||||||
|
tp, sl0, mb = e["tp"], e["sl"], e["max_bars"]
|
||||||
|
exit_p = c[min(i + mb, n - 1)]
|
||||||
|
j = min(i + mb, n - 1)
|
||||||
|
for k in range(1, mb + 1):
|
||||||
|
j = i + k
|
||||||
|
if j >= n:
|
||||||
|
exit_p = c[n - 1]
|
||||||
|
break
|
||||||
|
if mode == "orig":
|
||||||
|
hs = (d == 1 and l[j] <= sl0) or (d == -1 and h[j] >= sl0)
|
||||||
|
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
|
||||||
|
if hs:
|
||||||
|
exit_p = sl0
|
||||||
|
break
|
||||||
|
if ht:
|
||||||
|
exit_p = tp
|
||||||
|
break
|
||||||
|
if k == mb:
|
||||||
|
exit_p = c[j]
|
||||||
|
else: # exit16
|
||||||
|
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
|
||||||
|
if ht:
|
||||||
|
exit_p = tp
|
||||||
|
break
|
||||||
|
aj = a[j] if np.isfinite(a[j]) else 0.0
|
||||||
|
confirm = (d == 1 and c[j] < sl0 - buffer * aj) or \
|
||||||
|
(d == -1 and c[j] > sl0 + buffer * aj)
|
||||||
|
if confirm:
|
||||||
|
exit_p = c[j]
|
||||||
|
break
|
||||||
|
if k == mb:
|
||||||
|
exit_p = c[j]
|
||||||
|
ret = (exit_p - entry) / entry * d * lev - fee
|
||||||
|
out.append((i, j, ret))
|
||||||
|
last = j
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def equity_from_trades(df, trades):
|
||||||
|
"""Trade -> equity giornaliera su IDX (flusso di combine_portfolio.fade_daily_equity)."""
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
n = len(df)
|
||||||
|
eq = np.full(n, INIT, dtype=float)
|
||||||
|
cap = INIT
|
||||||
|
for i, j, ret in sorted(trades, key=lambda t: t[1]):
|
||||||
|
cap = max(cap + cap * POS * ret, 10.0)
|
||||||
|
eq[j:] = cap
|
||||||
|
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
|
||||||
|
return _norm(s)
|
||||||
|
|
||||||
|
|
||||||
|
def port_metrics(members: dict[str, pd.Series], p):
|
||||||
|
"""Metriche (FULL, OOS) del portafoglio p con la STESSA matematica pesi cap
|
||||||
|
di Portfolio.backtest."""
|
||||||
|
ids = p.sleeve_ids
|
||||||
|
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
||||||
|
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
|
||||||
|
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
||||||
|
drp = port_returns({i: members[i] for i in ids}, w)
|
||||||
|
return metrics(drp), metrics(drp, lo=SPLIT)
|
||||||
|
|
||||||
|
|
||||||
|
def dd(s):
|
||||||
|
"""Max drawdown % di una serie equity."""
|
||||||
|
pk = s.cummax()
|
||||||
|
return float(((pk - s) / pk).max() * 100)
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""ACCEL50 — Ricerca acceleratori verso l'obiettivo €50/giorno (2026-06-12).
|
||||||
|
|
||||||
|
Domanda: quali strategie/leve portano PIU' VELOCEMENTE a €50/g partendo da ~€2k?
|
||||||
|
Diario: docs/diary/2026-06-12-accel50.md. Esiti:
|
||||||
|
|
||||||
|
1. LEVA su PORT06 (acceleratore dominante, zero ricerca nuova).
|
||||||
|
La frontiera (scala lineare dei daily return canonici, fee pro-quota) mostra
|
||||||
|
che a Sharpe ~7-10 il vincolo non e' il rischio ma la taglia: lev 2->4 porta
|
||||||
|
gli anni-a-target da 3.3 a 1.2 con FULL DD 3.5->6.9%. Vedi lev_frontier().
|
||||||
|
|
||||||
|
2. FADE 15m (candidata NUOVA, validazione preliminare PASSATA).
|
||||||
|
MR01/MR02/MR07 a 15m con i parametri live 1h (trend_max=3, ema_long=200,
|
||||||
|
sl_confirm_atr=0.5, fee 0.10% RT): tutti e 6 gli sleeve positivi, OOS
|
||||||
|
2025-26 positivo ovunque (spesso > del 1h: 4x trade = compounding piu'
|
||||||
|
rapido), reggono fee 2x (Sh 1.6-2.9). BTC 15m MIGLIORA il 1h (MR01 Sh
|
||||||
|
3.37 vs 2.76 con meta' DD). Prossimo passo obbligato: gate PORT06
|
||||||
|
(correlazione col gemello 1h, parita' worker — infra 15m gia' esistente
|
||||||
|
dal BLEND pairs). Vedi fade15m_probe().
|
||||||
|
|
||||||
|
3. PAIRS NUOVE: BOCCIATE (stale-print illusion).
|
||||||
|
Lo sweep delle 19 coppie mai testate (config universale pre-registrata)
|
||||||
|
dava 8 candidate con Sharpe 1.5-4.3, MA le gambe alt hanno 88-98% di barre
|
||||||
|
flat (LTC 97%, ADA 98%, DOGE 91%, XRP 88%, BNB 88%) e con flat_skip=True
|
||||||
|
(fill solo su barre pulite) muoiono quasi tutte (BTC/ADA 4.33->0.17,
|
||||||
|
ETH/DOGE 3.79->0.46). Migliore superstite ETH/XRP a 1.34: inferiore alle
|
||||||
|
5 deployate -> niente. Stessa classe di illusione del XEX su DOGE/SOL
|
||||||
|
(vedi xex_divergence_research.py). PAXG idem: 92% flat su Deribit.
|
||||||
|
|
||||||
|
4. CAPITALE: a config attuale servono ~€24k per €50/g; ogni € aggiunto
|
||||||
|
accorcia linearmente (non e' una strategia ma domina ogni altra leva).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
def lev_frontier() -> None:
|
||||||
|
"""Frontiera di leva su PORT06: CAGR/DD/Sharpe e anni-a-€50/g per lev 1-6.
|
||||||
|
Modello: scala lineare dei daily return del backtest canonico (strumenti
|
||||||
|
lineari, fee proporzionali al notional). NON modella margine/code grasse."""
|
||||||
|
from scripts.portfolios._defs import PORTFOLIOS
|
||||||
|
from scripts.analysis.combine_portfolio import port_returns, SPLIT
|
||||||
|
from src.portfolio.sleeves import all_sleeve_equities, sleeve_returns_df
|
||||||
|
|
||||||
|
p = PORTFOLIOS["PORT06"]
|
||||||
|
eq = all_sleeve_equities()
|
||||||
|
members = {sid: eq[sid] for sid in p.sleeve_ids}
|
||||||
|
w = p.weight_vector(sleeve_returns_df(p.sleeve_ids))
|
||||||
|
base = port_returns(members, w) # == live a lev 2 (parita' validata)
|
||||||
|
|
||||||
|
def dd(x):
|
||||||
|
c = (1 + x).cumprod()
|
||||||
|
return ((c - c.cummax()) / c.cummax()).min() * 100
|
||||||
|
|
||||||
|
def cagr(x):
|
||||||
|
c = (1 + x).cumprod()
|
||||||
|
return ((c.iloc[-1]) ** (365 / len(x)) - 1) * 100
|
||||||
|
|
||||||
|
print("lev CAGR_full% DD_full% CAGR_oos% DD_oos% K_per_50/g anni_da_2k")
|
||||||
|
for f, lev in [(0.5, 1), (1.0, 2), (1.5, 3), (2.0, 4), (2.5, 5), (3.0, 6)]:
|
||||||
|
r = base * f
|
||||||
|
roos = r.iloc[SPLIT:]
|
||||||
|
co = cagr(roos)
|
||||||
|
daily = (1 + co / 100) ** (1 / 365) - 1
|
||||||
|
k = 50 / daily if daily > 0 else float("inf")
|
||||||
|
anni = np.log(k / 2020) / np.log(1 + co / 100) if co > 0 else float("inf")
|
||||||
|
print(f"{lev:>3} {cagr(r):>11.0f} {dd(r):>9.2f} {co:>10.0f} {dd(roos):>8.2f} "
|
||||||
|
f"{k:>11,.0f} {max(anni, 0):>11.1f}")
|
||||||
|
|
||||||
|
|
||||||
|
def fade15m_probe() -> None:
|
||||||
|
"""MR01/02/07 a 15m vs 1h, parametri live, fee 0.10% e stress 2x."""
|
||||||
|
import importlib.util
|
||||||
|
import inspect
|
||||||
|
from src.strategies.base import Strategy
|
||||||
|
|
||||||
|
LIVEP = dict(trend_max=3.0, ema_long=200, sl_confirm_atr=0.5)
|
||||||
|
paths = {
|
||||||
|
"MR01": "scripts/strategies/MR01_bollinger_fade.py",
|
||||||
|
"MR02": "scripts/strategies/MR02_donchian_fade.py",
|
||||||
|
"MR07": "scripts/strategies/MR07_return_reversal.py",
|
||||||
|
}
|
||||||
|
for code, rel in paths.items():
|
||||||
|
spec = importlib.util.spec_from_file_location(code.lower(), PROJECT_ROOT / rel)
|
||||||
|
m = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(m)
|
||||||
|
cls = next(o for _, o in vars(m).items()
|
||||||
|
if inspect.isclass(o) and issubclass(o, Strategy) and o.__module__ == m.__name__)
|
||||||
|
s = cls()
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
line = f"{code} {asset}: "
|
||||||
|
for tf in ("1h", "15m"):
|
||||||
|
r = s.backtest(asset, tf, **LIVEP)
|
||||||
|
if r is None:
|
||||||
|
line += f"{tf}: no-sig | "
|
||||||
|
continue
|
||||||
|
oos = sum(y.pnl for y in r.yearly if y.year >= 2025)
|
||||||
|
old = s.fee_rt
|
||||||
|
s.fee_rt = 0.002
|
||||||
|
r2 = s.backtest(asset, tf, **LIVEP)
|
||||||
|
s.fee_rt = old
|
||||||
|
line += (f"{tf}: Sh{r.sharpe:5.2f} DD{r.max_dd:5.1f}% n={r.trades:4d} "
|
||||||
|
f"oos25-26={oos:+8.0f} fee2x_Sh{r2.sharpe:5.2f} | ")
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=== 1. Frontiera di leva PORT06 ===")
|
||||||
|
lev_frontier()
|
||||||
|
print("\n=== 2. Fade 15m probe ===")
|
||||||
|
fade15m_probe()
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""CONFERMA su feed PURO Binance spot — la fade ha edge reale o era artefatto-print?
|
||||||
|
|
||||||
|
Il clean close-aware ha spliciato barre Binance-spot dentro la serie Deribit-perp:
|
||||||
|
il crollo del backtest potrebbe (a) rivelare la verita' (l'edge era print) o (b) essere
|
||||||
|
un artefatto dello splice (basis perp/spot ai punti di giunzione). Test decisivo:
|
||||||
|
girare lo STESSO engine fade su una serie 100% Binance spot (sorgente coerente, niente
|
||||||
|
splice). Se anche qui la fade e' negativa -> edge confermato finto.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np, pandas as pd, ccxt
|
||||||
|
from scripts.analysis.risk_management import build_trades, strats_for
|
||||||
|
|
||||||
|
EX = ccxt.binance({"enableRateLimit": True})
|
||||||
|
SYM = {"BTC": "BTC/USDT", "ETH": "ETH/USDT"}
|
||||||
|
START = "2020-06-01" # warmup per EMA200/ATR; il report usa 2021+
|
||||||
|
YEARS = [2021, 2022, 2023, 2024, 2025, 2026]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(asset, tf="15m"):
|
||||||
|
start_ms = int(pd.Timestamp(START, tz="UTC").timestamp() * 1000)
|
||||||
|
end_ms = int(pd.Timestamp("2026-05-26", tz="UTC").timestamp() * 1000)
|
||||||
|
tf_ms = 15 * 60 * 1000
|
||||||
|
rows = []
|
||||||
|
since = start_ms
|
||||||
|
while since <= end_ms:
|
||||||
|
r = EX.fetch_ohlcv(SYM[asset], tf, since=since, limit=1000)
|
||||||
|
if not r:
|
||||||
|
break
|
||||||
|
rows += r
|
||||||
|
nxt = int(r[-1][0]) + tf_ms
|
||||||
|
if nxt <= since:
|
||||||
|
break
|
||||||
|
since = nxt
|
||||||
|
df = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||||
|
df = df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
|
||||||
|
return df[df["timestamp"] <= end_ms]
|
||||||
|
|
||||||
|
|
||||||
|
def yearly(rows_by_year_ret, ts, trades, pos=0.15):
|
||||||
|
# per-anno compound
|
||||||
|
yr = {y: 1000.0 for y in YEARS}
|
||||||
|
# ricostruisco compound per anno separato (reset capitale ogni anno per ret% annuo)
|
||||||
|
by = {y: [] for y in YEARS}
|
||||||
|
for i, j, r in trades:
|
||||||
|
y = ts.iloc[i].year
|
||||||
|
if y in by:
|
||||||
|
by[y].append(r)
|
||||||
|
out = {}
|
||||||
|
for y in YEARS:
|
||||||
|
cap = 1000.0
|
||||||
|
for r in by[y]:
|
||||||
|
cap = max(cap + cap * pos * r, 10.0)
|
||||||
|
out[y] = (cap / 1000 - 1) * 100
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def full_oos(ts, trades, pos=0.15, split_date="2024-10-12"):
|
||||||
|
sd = pd.Timestamp(split_date, tz="UTC")
|
||||||
|
def comp(sub):
|
||||||
|
cap = 1000.0; rets = []
|
||||||
|
for i, j, r in sub:
|
||||||
|
cap = max(cap + cap * pos * r, 10.0); rets.append(r * pos)
|
||||||
|
return cap, rets
|
||||||
|
capF, rF = comp(trades)
|
||||||
|
oos = [(i, j, r) for i, j, r in trades if ts.iloc[i] >= sd]
|
||||||
|
capO, rO = comp(oos)
|
||||||
|
shF = float(np.mean(rF) / np.std(rF) * np.sqrt(len(rF))) if len(rF) > 1 and np.std(rF) > 0 else 0.0
|
||||||
|
shO = float(np.mean(rO) / np.std(rO) * np.sqrt(len(rO))) if len(rO) > 1 and np.std(rO) > 0 else 0.0
|
||||||
|
return (capF / 1000 - 1) * 100, shF, (capO / 1000 - 1) * 100, shO
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"Fetch Binance 15m (da {START})...\n")
|
||||||
|
data = {a: fetch(a) for a in ("BTC", "ETH")}
|
||||||
|
print("=" * 92)
|
||||||
|
print(" FADE su PURO Binance spot 15m | RET% per anno (pos 0.15, leva 3x, trend 3.0)")
|
||||||
|
print("=" * 92)
|
||||||
|
print(f" {'sleeve':<12s}" + "".join(f"{y:>9d}" for y in YEARS) + " | FULL% Shrp | OOS% Shrp")
|
||||||
|
print(" " + "-" * 88)
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
df = data[asset].copy()
|
||||||
|
df = df[pd.to_datetime(df["timestamp"], unit="ms", utc=True).dt.year >= 2021].reset_index(drop=True) \
|
||||||
|
if False else df # tengo il warmup, filtro nei trade
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
for code in ("MR01", "MR02", "MR07"):
|
||||||
|
fn, params = strats_for(asset)[code]
|
||||||
|
trades = build_trades(fn(df, **params), df, trend_max=3.0)
|
||||||
|
trades = [(i, j, r) for i, j, r in trades if ts.iloc[i].year >= 2021]
|
||||||
|
yr = yearly(None, ts, trades)
|
||||||
|
fF, shF, fO, shO = full_oos(ts, trades)
|
||||||
|
print(f" {code+'_'+asset:<12s}" + "".join(f"{yr[y]:>+9.0f}" for y in YEARS) +
|
||||||
|
f" | {fF:>+8.0f} {shF:>5.2f} | {fO:>+6.0f} {shO:>5.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""Validazione dell'edge del credit-spread di cerbero-bite sui PREZZI REALI.
|
||||||
|
|
||||||
|
cerbero-bite (container accanto) vende credit spread su ETH (bull-put primario,
|
||||||
|
short delta ~0.18, DTE 18, PT 50% / stop 2.5x credito / delta-breach 0.30 / vol-stop
|
||||||
|
+10 DVOL / time-stop 7 DTE). Domanda: l'edge regge su un CICLO ETH completo, o e'
|
||||||
|
profittevole solo nei campioni calmi?
|
||||||
|
|
||||||
|
Tre analisi (riprendibili):
|
||||||
|
1) entry_economics() -> economia d'ingresso REALE dalla chain (data/options/eth_chain.parquet):
|
||||||
|
credit/width effettivo a delta 0.18 dai bid/ask veri, eleggibilita' sotto i gate liquidita'.
|
||||||
|
2) tail_model_free() -> esito terminale dai prezzi ETH reali (2018-2026), cw reale 0.106,
|
||||||
|
NESSUN modello opzioni (niente errore BS): win-rate, EV, frequenza max-loss.
|
||||||
|
3) managed_backtest() -> lifecycle CON management; mark con skew calibrato sulle IV reali.
|
||||||
|
|
||||||
|
ESITO (2026-06-09):
|
||||||
|
- cw reale a delta 0.18 = 0.106 (short ~9.4% OTM, NON 18%), max-loss/credito = 8.4x, eleggibilita' 65%.
|
||||||
|
- hold-to-expiry @0.106: EV -1.0 crediti/trade, 7/9 anni NEGATIVI, max-loss 17.8% delle volte.
|
||||||
|
- managed (skew): EV -0.02 cr/trade, win-rate 37% (delta-breach esce sul 62% dei trade a piccola perdita).
|
||||||
|
- VERDETTO: NON edge robusto su ciclo completo. Il "+0.48%/mese" era artefatto di finestra calma
|
||||||
|
(mag-giu 2026, no crash). Premium-selling a skew negativo: vince nei campioni calmi, restituisce
|
||||||
|
tutto (o piu') nei crash. Tune "Profilo B" (vendere a 9.4% OTM) PEGGIORA la frequenza di max-loss.
|
||||||
|
Coda CONCENTRATA col fade ETH di PythagorasGoal (stesso crash colpisce entrambi).
|
||||||
|
|
||||||
|
TODO APERTO (per nail-are l'EV managed esatto): la calibrazione non e' ancora perfetta
|
||||||
|
(mark mid+skew da cw 0.228 vs 0.106 reale -> sovrastima il credito ~2x). Manca: modellare
|
||||||
|
bid/ask reale incrociato sulle 2 gambe + griglia strike reale (entrambi nella chain) cosi'
|
||||||
|
l'entry cw scende a 0.106 e l'EV managed diventa esatto. Allora chiudere il sì/no definitivo.
|
||||||
|
|
||||||
|
uv run python scripts/analysis/cerbero_bite_credit_spread.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys, math, collections
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np, pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
from scripts.analysis.options_chain import OptionChain, load_market
|
||||||
|
from scripts.analysis.explore_lab import get_df
|
||||||
|
from scripts.analysis.option_overlay_lab import bs_put, _ncdf, dvol_for
|
||||||
|
|
||||||
|
SHORT_OTM, LONG_OTM, DTE = 0.094, 0.134, 17 # da chain reale (delta 0.18, width 4%)
|
||||||
|
CW_REAL = 0.106
|
||||||
|
|
||||||
|
|
||||||
|
def entry_economics():
|
||||||
|
oc = OptionChain("ETH"); ch = oc.df
|
||||||
|
mk = load_market("ETH")[["ts_ms", "spot"]].dropna().sort_values("ts_ms")
|
||||||
|
p = ch[ch["option_type"] == "P"].copy()
|
||||||
|
p = pd.merge_asof(p.sort_values("ts_ms"), mk, on="ts_ms", direction="backward")
|
||||||
|
cand = p[(p["tenor_d"] >= 14) & (p["tenor_d"] <= 21)].dropna(subset=["delta", "bid", "ask", "strike", "spot"])
|
||||||
|
rows = []
|
||||||
|
for (ts, exp), g in cand.groupby(["timestamp", "expiry"]):
|
||||||
|
spot = g["spot"].iloc[0]
|
||||||
|
sc = g[(g["delta"] <= -0.12) & (g["delta"] >= -0.22)]
|
||||||
|
if sc.empty: continue
|
||||||
|
short = sc.iloc[(sc["delta"] + 0.18).abs().argmin()]
|
||||||
|
Ks = short["strike"]; longc = g[g["strike"] < Ks]
|
||||||
|
if longc.empty: continue
|
||||||
|
longp = longc.iloc[(longc["strike"] - (Ks - spot * 0.04)).abs().argmin()]
|
||||||
|
W = Ks - longp["strike"]
|
||||||
|
if W <= 0: continue
|
||||||
|
credit = short["bid"] - longp["ask"]
|
||||||
|
def ok(o):
|
||||||
|
sp = (o["ask"] - o["bid"]) / ((o["ask"] + o["bid"]) / 2) if (o["ask"] + o["bid"]) > 0 else 9
|
||||||
|
return (o["open_interest"] or 0) >= 100 and sp <= 0.15 and o["bid"] > 0
|
||||||
|
cw = credit / (W / spot)
|
||||||
|
rows.append(dict(cw=cw, credit=credit, elig=ok(short) and ok(longp) and cw >= 0.08 and credit > 0,
|
||||||
|
short_otm=(spot - Ks) / spot, delta=short["delta"]))
|
||||||
|
r = pd.DataFrame(rows)
|
||||||
|
print(f"[ENTRY] {len(r)} spread | eleggibili {r['elig'].mean()*100:.0f}% | cw mediano {r['cw'].median():.3f} "
|
||||||
|
f"| short OTM {r['short_otm'].median()*100:.1f}% | max-loss/credito {((1-r['cw'].median())/r['cw'].median()):.1f}x")
|
||||||
|
|
||||||
|
|
||||||
|
def tail_model_free():
|
||||||
|
df = get_df("ETH", "1h"); c = df["close"].values; n = len(c)
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True); H = DTE * 24
|
||||||
|
res = []
|
||||||
|
for i in range(200, n - H - 1, 24 * 2):
|
||||||
|
S0 = c[i]; Ks = S0 * (1 - SHORT_OTM); Kl = S0 * (1 - LONG_OTM); W = Ks - Kl
|
||||||
|
Sx = c[i + H]; intr = min(max(Ks - Sx, 0.0), W); credit = CW_REAL * W
|
||||||
|
res.append((ts.iloc[i].year, 1 - intr / credit, Sx < Kl))
|
||||||
|
R = pd.DataFrame(res, columns=["y", "pnl", "maxloss"]); P = R["pnl"].values
|
||||||
|
print(f"[TAIL model-free @cw0.106] win {(P>0).mean()*100:.0f}% | EV {P.mean():+.2f}cr | max-loss {R['maxloss'].mean()*100:.0f}% "
|
||||||
|
f"| anni neg {(R.groupby('y')['pnl'].mean()<0).sum()}/{R['y'].nunique()}")
|
||||||
|
|
||||||
|
|
||||||
|
def _skew_fit():
|
||||||
|
oc = OptionChain("ETH"); ch = oc.df
|
||||||
|
mk = load_market("ETH")[["ts_ms", "spot"]].dropna().sort_values("ts_ms")
|
||||||
|
p = ch[ch["option_type"] == "P"].copy()
|
||||||
|
p = pd.merge_asof(p.sort_values("ts_ms"), mk, on="ts_ms", direction="backward")
|
||||||
|
p = p.dropna(subset=["iv", "strike", "spot", "delta", "tenor_d"])
|
||||||
|
p = p[(p["tenor_d"] >= 7) & (p["tenor_d"] <= 35) & (p["iv"] > 0)]
|
||||||
|
p["dd"] = (p["delta"] + 0.5).abs()
|
||||||
|
atm = p.sort_values("dd").groupby("timestamp")["iv"].first()
|
||||||
|
p["atm_iv"] = p["timestamp"].map(atm); p = p.dropna(subset=["atm_iv"])
|
||||||
|
p["k"] = np.log(p["strike"] / p["spot"]); p["ratio"] = p["iv"] / p["atm_iv"]
|
||||||
|
p = p[(p["k"] > -0.35) & (p["k"] < 0.15) & (p["ratio"] > 0.5) & (p["ratio"] < 3)]
|
||||||
|
coef, *_ = np.linalg.lstsq(np.c_[p["k"], p["k"]**2], p["ratio"] - 1.0, rcond=None)
|
||||||
|
return coef # a, b
|
||||||
|
|
||||||
|
|
||||||
|
def managed_backtest():
|
||||||
|
a, b = _skew_fit()
|
||||||
|
def ivol(S, K, atm):
|
||||||
|
k = math.log(K / S); return max(atm * (1 + a * k + b * k * k), 0.05)
|
||||||
|
def put_delta(S, K, T, sig):
|
||||||
|
if T <= 0 or sig <= 0: return -1.0 if S < K else 0.0
|
||||||
|
return _ncdf((math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T))) - 1.0
|
||||||
|
def mark(S, Ks, Kl, T, atm):
|
||||||
|
return bs_put(S, Ks, T, ivol(S, Ks, atm)) - bs_put(S, Kl, T, ivol(S, Kl, atm))
|
||||||
|
df = get_df("ETH", "1h"); c = df["close"].values; n = len(c)
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True); dvol = dvol_for(df, "ETH")
|
||||||
|
H = DTE * 24; STEP = 6; cw = []; tr = []
|
||||||
|
for i in range(200, n - H - 1, 24 * 2):
|
||||||
|
S0 = c[i]; atm0 = dvol[i] if not np.isnan(dvol[i]) else 0.6
|
||||||
|
Ks = S0 * (1 - SHORT_OTM); Kl = S0 * (1 - LONG_OTM); W = Ks - Kl
|
||||||
|
credit = mark(S0, Ks, Kl, DTE / 365.0, atm0)
|
||||||
|
if credit <= 0: continue
|
||||||
|
cw.append(credit / W); pnl = why = None
|
||||||
|
for k in range(STEP, H + 1, STEP):
|
||||||
|
j = i + k; Trem = max((H - k) / (24 * 365.0), 1e-6); Sj = c[j]
|
||||||
|
atmj = dvol[j] if not np.isnan(dvol[j]) else atm0; mk = mark(Sj, Ks, Kl, Trem, atmj)
|
||||||
|
if mk <= 0.5 * credit: pnl, why = 1 - mk / credit, "PT"; break
|
||||||
|
if mk >= 2.5 * credit: pnl, why = 1 - mk / credit, "stop"; break
|
||||||
|
if put_delta(Sj, Ks, Trem, ivol(Sj, Ks, atmj)) <= -0.30: pnl, why = 1 - mk / credit, "delta"; break
|
||||||
|
if atmj - atm0 >= 0.10: pnl, why = 1 - mk / credit, "vol"; break
|
||||||
|
if k >= (DTE - 7) * 24: pnl, why = 1 - mk / credit, "time"; break
|
||||||
|
if pnl is None:
|
||||||
|
Sx = c[i + H]; intr = min(max(Ks - Sx, 0), W); pnl, why = 1 - intr / credit, "expiry"
|
||||||
|
tr.append((ts.iloc[i].year, pnl, why))
|
||||||
|
P = np.array([t[1] for t in tr])
|
||||||
|
print(f"[MANAGED skew] cw@entry {np.median(cw):.3f} (vs 0.106 reale: sovrastima ~2x, EV vero <=) | "
|
||||||
|
f"win {(P>0).mean()*100:.0f}% | EV {P.mean():+.3f}cr | worst {P.min():.1f} | "
|
||||||
|
f"uscite {dict(collections.Counter(t[2] for t in tr))}")
|
||||||
|
R = pd.DataFrame({"y": [t[0] for t in tr], "p": P})
|
||||||
|
print(f" 2021+: EV {R[R.y>=2021]['p'].mean():+.3f}cr/trade")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
entry_economics()
|
||||||
|
tail_model_free()
|
||||||
|
managed_backtest()
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""PROBE CERBERO MCP — quali exchange/fonti serve davvero? (cerca IBKR & alt)
|
||||||
|
|
||||||
|
Non si fida del commento nel codice: interroga il server v2 (/mcp/tools/get_historical
|
||||||
|
con `exchange=...`) su una matrice di nomi exchange + naming strumento, e riporta chi
|
||||||
|
risponde con candele vere. Cerca in particolare IBKR e Alpaca (spot US reale).
|
||||||
|
|
||||||
|
uv run python scripts/analysis/cerbero_probe.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import requests
|
||||||
|
from src.live.cerbero_client import CerberoClient, is_mainnet
|
||||||
|
|
||||||
|
C = CerberoClient()
|
||||||
|
START, END, INTERVAL = "2026-05-20", "2026-05-27", "1h"
|
||||||
|
|
||||||
|
# exchange -> naming strumento BTC da provare (varianti)
|
||||||
|
EXCHANGES = {
|
||||||
|
"deribit": ["BTC-PERPETUAL"],
|
||||||
|
"hyperliquid": ["BTC"],
|
||||||
|
"bybit": ["BTCUSDT", "BTC-PERPETUAL", "BTCUSD"],
|
||||||
|
"alpaca": ["BTC/USD", "BTCUSD", "BTC/USDT", "BTC"],
|
||||||
|
"ibkr": ["BTC", "BTC.USD", "BTCUSD"],
|
||||||
|
"interactivebrokers": ["BTC", "BTCUSD"],
|
||||||
|
"binance": ["BTCUSDT", "BTC/USDT", "BTC-PERPETUAL"],
|
||||||
|
"coinbase": ["BTC-USD", "BTC/USD", "BTCUSD"],
|
||||||
|
"kraken": ["XBTUSD", "BTC/USD", "BTCUSD"],
|
||||||
|
"okx": ["BTC-USDT", "BTC-USD-SWAP"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def try_v2(exchange: str, instrument: str) -> str:
|
||||||
|
try:
|
||||||
|
candles = C.get_historical_v2(instrument, START, END, INTERVAL, exchange=exchange)
|
||||||
|
if candles:
|
||||||
|
c0, c1 = candles[0], candles[-1]
|
||||||
|
return f"OK {len(candles):>4} candele close {c1.get('close')}"
|
||||||
|
return "vuoto (0 candele)"
|
||||||
|
except requests.HTTPError as e:
|
||||||
|
code = e.response.status_code if e.response is not None else "?"
|
||||||
|
return f"HTTP {code}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"{type(e).__name__}: {str(e)[:50]}"
|
||||||
|
|
||||||
|
|
||||||
|
def list_tools() -> None:
|
||||||
|
"""Tenta di enumerare i tool/endpoint del server (best-effort)."""
|
||||||
|
for path in ("/mcp/tools", "/mcp/tools/list", "/tools", "/mcp"):
|
||||||
|
try:
|
||||||
|
r = requests.post(f"{C.base_url}{path}", headers=C._headers(), json={}, timeout=10)
|
||||||
|
print(f" POST {path:<18} -> HTTP {r.status_code} {str(r.text)[:200]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" POST {path:<18} -> {type(e).__name__}: {str(e)[:60]}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 80)
|
||||||
|
print(f" PROBE CERBERO MCP @ {C.base_url} (mainnet={is_mainnet()})")
|
||||||
|
print("=" * 80)
|
||||||
|
print("\n[1] Enumerazione endpoint/tool (best-effort):")
|
||||||
|
list_tools()
|
||||||
|
print(f"\n[2] get_historical_v2 BTC {INTERVAL} {START}->{END} per exchange:")
|
||||||
|
print(f" {'exchange':<20s}{'instrument':<16s}esito")
|
||||||
|
print(" " + "-" * 70)
|
||||||
|
for ex, syms in EXCHANGES.items():
|
||||||
|
for sym in syms:
|
||||||
|
res = try_v2(ex, sym)
|
||||||
|
print(f" {ex:<20s}{sym:<16s}{res}")
|
||||||
|
if res.startswith("OK"):
|
||||||
|
break # trovato il naming giusto, basta
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""RI-ESECUZIONE FADE sul feed PULITO (data/raw ricostruito da Deribit mainnet, 2026-06-19).
|
||||||
|
|
||||||
|
Dopo il rebuild (scripts/analysis/rebuild_history.py) i parquet canonici in data/raw
|
||||||
|
sono storia Deribit mainnet reale (ccxt pubblico), certificata vs Coinbase USD. Qui giro
|
||||||
|
le 6 fade (MR01/MR02/MR07 x BTC/ETH) con l'ENGINE CANONICO (risk_management.build_trades,
|
||||||
|
strats_for) sul feed pulito, su ENTRAMBI i timeframe:
|
||||||
|
- 1h = config dei claim storici "validati OOS" (CLAUDE.md: MR01 BTC +201% / ETH +1238%)
|
||||||
|
- 15m = config LIVE attuale (swap 1h->15m, v1.1.30)
|
||||||
|
|
||||||
|
Stessi parametri del live: pos 0.15, leva 3x, trend_max 3.0, fee 0.10% RT. OOS = ultimo 30%
|
||||||
|
per indice (convenzione OOS_FRAC del progetto). Read-only, nessuna scrittura.
|
||||||
|
|
||||||
|
uv run python scripts/analysis/clean_fade_rerun.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
from scripts.analysis.risk_management import strats_for, build_trades, POS, LEV, OOS_FRAC
|
||||||
|
|
||||||
|
TFS = ["1h", "15m"]
|
||||||
|
YEARS = list(range(2018, 2027))
|
||||||
|
|
||||||
|
|
||||||
|
def metrics(ts, trades, split_idx, pos=POS):
|
||||||
|
"""trades = [(i, j, r_netto)]. Ritorna (per-anno%, FULL%, FULL Sharpe, OOS%, OOS Sharpe)."""
|
||||||
|
by = {y: 0.0 for y in YEARS}
|
||||||
|
capF = capO = 1000.0
|
||||||
|
rF, rO = [], []
|
||||||
|
for i, j, r in trades:
|
||||||
|
y = ts.iloc[i].year
|
||||||
|
if y in by:
|
||||||
|
by[y] += r * pos * 1000.0 # contributo lineare per la riga annuale
|
||||||
|
capF = max(capF + capF * pos * r, 10.0)
|
||||||
|
rF.append(r * pos)
|
||||||
|
if i >= split_idx:
|
||||||
|
capO = max(capO + capO * pos * r, 10.0)
|
||||||
|
rO.append(r * pos)
|
||||||
|
yr = {y: by[y] / 1000.0 * 100 for y in YEARS}
|
||||||
|
shF = float(np.mean(rF) / np.std(rF) * np.sqrt(len(rF))) if len(rF) > 1 and np.std(rF) > 0 else 0.0
|
||||||
|
shO = float(np.mean(rO) / np.std(rO) * np.sqrt(len(rO))) if len(rO) > 1 and np.std(rO) > 0 else 0.0
|
||||||
|
return yr, (capF / 1000 - 1) * 100, shF, (capO / 1000 - 1) * 100, shO
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
years_present = set()
|
||||||
|
results = {}
|
||||||
|
for tf in TFS:
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
df = load_data(asset, tf)
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
years_present |= set(ts.dt.year.unique().tolist())
|
||||||
|
split_idx = int(len(df) * (1 - OOS_FRAC))
|
||||||
|
cov = f"{ts.iloc[0].date()} -> {ts.iloc[-1].date()} ({len(df)} barre, OOS da {ts.iloc[split_idx].date()})"
|
||||||
|
for code in ("MR01", "MR02", "MR07"):
|
||||||
|
fn, params = strats_for(asset)[code]
|
||||||
|
trades = build_trades(fn(df, **params), df, trend_max=3.0)
|
||||||
|
results[(tf, asset, code)] = (metrics(ts, trades, split_idx), len(trades), cov)
|
||||||
|
|
||||||
|
years = [y for y in YEARS if y in years_present]
|
||||||
|
for tf in TFS:
|
||||||
|
print("\n" + "=" * (62 + 9 * len(years)))
|
||||||
|
print(f" FADE su FEED PULITO (Deribit mainnet) — {tf} | pos {POS}, leva {LEV:.0f}x, trend 3.0, fee 0.10% RT")
|
||||||
|
# mostra la copertura una volta per asset
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
print(f" {asset}: {results[(tf, asset, 'MR01')][2]}")
|
||||||
|
print("=" * (62 + 9 * len(years)))
|
||||||
|
print(f" {'sleeve':<11s}" + "".join(f"{y:>9d}" for y in years) +
|
||||||
|
f"{'Trd':>7s}{'FULL%':>9s}{'Shrp':>7s}{'OOS%':>8s}{'Shrp':>7s}")
|
||||||
|
print(" " + "-" * (60 + 9 * len(years)))
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
for code in ("MR01", "MR02", "MR07"):
|
||||||
|
(yr, fF, shF, fO, shO), ntr, _ = results[(tf, asset, code)]
|
||||||
|
print(f" {code+'_'+asset:<11s}" + "".join(f"{yr[y]:>+9.0f}" for y in years) +
|
||||||
|
f"{ntr:>7d}{fF:>+9.0f}{shF:>7.2f}{fO:>+8.0f}{shO:>7.2f}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
"""CLEAN FEED — ripara gli spike-print del feed Deribit/Cerbero coi dati reali di Binance.
|
||||||
|
|
||||||
|
Motivo (2026-06-18): la ricerca Price Ladder ha rivelato che data/raw/btc_1h.parquet (e gli
|
||||||
|
altri TF/asset) contengono barre con WICK FASULLI (es. BTC 2024-02-13: low 38.580 con
|
||||||
|
close ~49.968, BTC reale ~50k) — lo stesso spike-print testnet documentato in CLAUDE.md
|
||||||
|
(TP_PHANTOM / feed congelato). Sono pochi (decine per file) ma avvelenano i backtest
|
||||||
|
(stop/entry su prezzi mai avvenuti) e gonfiano le code (la "FULL DD BTC ~54%" del ladder era
|
||||||
|
in gran parte questo).
|
||||||
|
|
||||||
|
Metodo (conservativo, fonte di verita' = Binance spot via ccxt, gia' cablato nel progetto):
|
||||||
|
1. DETECT: barra sospetta = high/low che sfora >15% il cluster di close locale [i-1,i,i+1]
|
||||||
|
(close sano + wick fasullo). Soglia larga: tanto e' Binance ad arbitrare.
|
||||||
|
2. ARBITRA: per ogni sospetta, scarica la barra Binance reale (BTC/USDT, ETH/USDT) allo
|
||||||
|
stesso tf/timestamp. Sostituisce O/H/L/C SOLO se Binance dissente materialmente (>2% su
|
||||||
|
high o low) -> un wick VERO confermato da Binance resta intatto. Volume/timestamp invariati.
|
||||||
|
3. BACKUP (data/_feed_backup/) + scrittura atomica + VALIDAZIONE (re-scan = 0 sospette,
|
||||||
|
n righe invariato). Log dettagliato di ogni barra riparata (old OHLC -> new).
|
||||||
|
|
||||||
|
uv run python scripts/analysis/clean_feed.py [ASSET_TF ...] # default: tutti BTC/ETH x TF
|
||||||
|
uv run python scripts/analysis/clean_feed.py BTC_1h # un solo file
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from src.data.downloader import _parquet_path, DATA_DIR
|
||||||
|
|
||||||
|
BACKUP = PROJECT_ROOT / "data" / "_feed_backup"
|
||||||
|
SYMBOL = {"BTC": "BTC/USDT", "ETH": "ETH/USDT"}
|
||||||
|
WICK_THR = 0.15 # detect: wick oltre 15% il cluster di close locale
|
||||||
|
REPLACE_THR = 0.02 # arbitra: sostituisci solo se Binance dissente >2% su high/low
|
||||||
|
CLOSE_THR = 0.01 # close-aware: sostituisci la barra se il CLOSE Deribit dista >1% da Binance
|
||||||
|
TF_MS = {"5m": 5, "8m": 8, "13m": 13, "15m": 15, "19m": 19, "30m": 30, "1h": 60}
|
||||||
|
_EX = None
|
||||||
|
|
||||||
|
|
||||||
|
def _binance():
|
||||||
|
global _EX
|
||||||
|
if _EX is None:
|
||||||
|
import ccxt
|
||||||
|
_EX = ccxt.binance({"enableRateLimit": True})
|
||||||
|
return _EX
|
||||||
|
|
||||||
|
|
||||||
|
def suspect_mask(df: pd.DataFrame) -> np.ndarray:
|
||||||
|
c = df["close"].to_numpy(float); h = df["high"].to_numpy(float); l = df["low"].to_numpy(float)
|
||||||
|
cp = np.roll(c, 1); cp[0] = c[0]; cn = np.roll(c, -1); cn[-1] = c[-1]
|
||||||
|
locmax = np.maximum.reduce([c, cp, cn]); locmin = np.minimum.reduce([c, cp, cn])
|
||||||
|
return (h > locmax * (1 + WICK_THR)) | (l < locmin * (1 - WICK_THR))
|
||||||
|
|
||||||
|
|
||||||
|
def _binance_bar(symbol: str, tf: str, ts_ms: int):
|
||||||
|
"""OHLC reale Binance alla barra ts_ms (None se assente)."""
|
||||||
|
try:
|
||||||
|
rows = _binance().fetch_ohlcv(symbol, tf, since=ts_ms - 1, limit=3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ! binance err: {type(e).__name__}: {str(e)[:80]}")
|
||||||
|
return None
|
||||||
|
for r in rows:
|
||||||
|
if int(r[0]) == ts_ms:
|
||||||
|
return float(r[1]), float(r[2]), float(r[3]), float(r[4])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def clean_file(asset: str, tf: str) -> dict:
|
||||||
|
path = _parquet_path(asset, tf)
|
||||||
|
if not path.exists():
|
||||||
|
return {"file": f"{asset}_{tf}", "skip": "no-file"}
|
||||||
|
df = pd.read_parquet(path)
|
||||||
|
mask = suspect_mask(df)
|
||||||
|
idx = np.where(mask)[0]
|
||||||
|
n0 = len(df)
|
||||||
|
if len(idx) == 0:
|
||||||
|
return {"file": f"{asset}_{tf}", "suspect": 0, "repaired": 0, "kept_real": 0,
|
||||||
|
"missing_binance": 0, "rows_before": n0, "rows_after": n0,
|
||||||
|
"still_suspect": 0, "log": []}
|
||||||
|
repaired, kept, missing = 0, 0, 0
|
||||||
|
log = []
|
||||||
|
for i in idx:
|
||||||
|
ts = int(df.iloc[i]["timestamp"])
|
||||||
|
b = _binance_bar(SYMBOL[asset], tf, ts)
|
||||||
|
oh, ol = float(df.iloc[i]["high"]), float(df.iloc[i]["low"])
|
||||||
|
if b is None:
|
||||||
|
missing += 1
|
||||||
|
continue
|
||||||
|
bo, bh, bl, bc = b
|
||||||
|
if abs(oh - bh) / bh > REPLACE_THR or abs(ol - bl) / max(bl, 1e-9) > REPLACE_THR:
|
||||||
|
df.iat[i, df.columns.get_loc("open")] = bo
|
||||||
|
df.iat[i, df.columns.get_loc("high")] = bh
|
||||||
|
df.iat[i, df.columns.get_loc("low")] = bl
|
||||||
|
df.iat[i, df.columns.get_loc("close")] = bc
|
||||||
|
repaired += 1
|
||||||
|
ts_s = pd.to_datetime(ts, unit="ms", utc=True).strftime("%Y-%m-%d %H:%M")
|
||||||
|
log.append(f" {ts_s} H {oh:,.0f}->{bh:,.0f} L {ol:,.0f}->{bl:,.0f}")
|
||||||
|
else:
|
||||||
|
kept += 1 # Binance conferma il wick: barra reale, intatta
|
||||||
|
if repaired:
|
||||||
|
BACKUP.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(path, BACKUP / f"{asset.lower()}_{tf}.parquet.bak")
|
||||||
|
tmp = path.with_suffix(".parquet.tmp")
|
||||||
|
df.to_parquet(tmp, index=False)
|
||||||
|
tmp.replace(path)
|
||||||
|
# validazione
|
||||||
|
df2 = pd.read_parquet(path)
|
||||||
|
still = int(suspect_mask(df2).sum())
|
||||||
|
return {"file": f"{asset}_{tf}", "suspect": len(idx), "repaired": repaired,
|
||||||
|
"kept_real": kept, "missing_binance": missing, "rows_before": n0,
|
||||||
|
"rows_after": len(df2), "still_suspect": still, "log": log}
|
||||||
|
|
||||||
|
|
||||||
|
def _binance_series(asset: str, tf: str, start_ms: int, end_ms: int) -> dict:
|
||||||
|
"""OHLC reale Binance per l'intero range -> dict ts_ms -> (o,h,l,c). Bulk paginato."""
|
||||||
|
ex = _binance()
|
||||||
|
tf_ms = TF_MS[tf] * 60 * 1000
|
||||||
|
out: dict[int, tuple] = {}
|
||||||
|
since = start_ms
|
||||||
|
while since <= end_ms:
|
||||||
|
try:
|
||||||
|
rows = ex.fetch_ohlcv(SYMBOL[asset], tf, since=since, limit=1000)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ! binance err: {type(e).__name__}: {str(e)[:80]}")
|
||||||
|
break
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for r in rows:
|
||||||
|
out[int(r[0])] = (float(r[1]), float(r[2]), float(r[3]), float(r[4]))
|
||||||
|
nxt = int(rows[-1][0]) + tf_ms
|
||||||
|
if nxt <= since:
|
||||||
|
break
|
||||||
|
since = nxt
|
||||||
|
if len(rows) < 1000 and since > end_ms:
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def clean_file_close(asset: str, tf: str, thr: float = CLOSE_THR, backup_dir: Path | None = None) -> dict:
|
||||||
|
"""CLOSE-AWARE: sostituisce O/H/L/C con Binance per ogni barra il cui CLOSE Deribit
|
||||||
|
dista > thr da Binance (1% default). Cattura i print 'silenziosi' che il wick-check
|
||||||
|
>15% non vede (close fantasma su barra di range piccolo). Fonte di verita' = Binance
|
||||||
|
spot (il feed storico e' perp testnet -> inaffidabile; lo spot ~ mainnet via arbitraggio)."""
|
||||||
|
if tf not in TF_MS:
|
||||||
|
return {"file": f"{asset}_{tf}", "skip": "tf-non-binance"}
|
||||||
|
path = _parquet_path(asset, tf)
|
||||||
|
if not path.exists():
|
||||||
|
return {"file": f"{asset}_{tf}", "skip": "no-file"}
|
||||||
|
df = pd.read_parquet(path)
|
||||||
|
n0 = len(df)
|
||||||
|
tms = df["timestamp"].to_numpy("int64")
|
||||||
|
c = df["close"].to_numpy(float)
|
||||||
|
bz = _binance_series(asset, tf, int(tms[0]), int(tms[-1]))
|
||||||
|
col = {k: df.columns.get_loc(k) for k in ("open", "high", "low", "close")}
|
||||||
|
fixed, by_year, missing = 0, {}, 0
|
||||||
|
log = []
|
||||||
|
for i in range(n0):
|
||||||
|
b = bz.get(int(tms[i]))
|
||||||
|
if b is None:
|
||||||
|
missing += 1
|
||||||
|
continue
|
||||||
|
bo, bh, bl, bc = b
|
||||||
|
if bc <= 0:
|
||||||
|
continue
|
||||||
|
orig = float(c[i]) # cattura PRIMA della scrittura (to_numpy puo' essere una view)
|
||||||
|
if abs(orig - bc) / bc > thr:
|
||||||
|
df.iat[i, col["open"]] = bo
|
||||||
|
df.iat[i, col["high"]] = bh
|
||||||
|
df.iat[i, col["low"]] = bl
|
||||||
|
df.iat[i, col["close"]] = bc
|
||||||
|
fixed += 1
|
||||||
|
y = pd.to_datetime(int(tms[i]), unit="ms", utc=True).year
|
||||||
|
by_year[y] = by_year.get(y, 0) + 1
|
||||||
|
if len(log) < 10:
|
||||||
|
ts_s = pd.to_datetime(int(tms[i]), unit="ms", utc=True).strftime("%Y-%m-%d %H:%M")
|
||||||
|
log.append(f" {ts_s} C {orig:,.2f}->{bc:,.2f} ({abs(orig-bc)/bc*100:.1f}%)")
|
||||||
|
if fixed:
|
||||||
|
bdir = backup_dir or BACKUP
|
||||||
|
bdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(path, bdir / f"{asset.lower()}_{tf}.parquet.bak")
|
||||||
|
tmp = path.with_suffix(".parquet.tmp")
|
||||||
|
df.to_parquet(tmp, index=False)
|
||||||
|
tmp.replace(path)
|
||||||
|
# validazione: ri-scan, 0 barre residue oltre soglia (fra quelle coperte da Binance)
|
||||||
|
df2 = pd.read_parquet(path)
|
||||||
|
c2 = df2["close"].to_numpy(float)
|
||||||
|
still = sum(1 for i in range(len(df2))
|
||||||
|
if (b := bz.get(int(tms[i]))) and b[3] > 0 and abs(c2[i] - b[3]) / b[3] > thr)
|
||||||
|
return {"file": f"{asset}_{tf}", "covered": n0 - missing, "fixed": fixed,
|
||||||
|
"missing_binance": missing, "rows_before": n0, "rows_after": len(df2),
|
||||||
|
"still_over_thr": still, "by_year": by_year, "log": log}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||||
|
close_mode = "--close" in sys.argv
|
||||||
|
dry = "--dry" in sys.argv
|
||||||
|
if close_mode:
|
||||||
|
targets = args or [f"{a}_{tf}" for a in ("BTC", "ETH") for tf in ("5m", "15m", "1h")]
|
||||||
|
stamp = time.strftime("%Y%m%d-%H%M%S")
|
||||||
|
bdir = BACKUP / f"pre_close_clean_{stamp}"
|
||||||
|
print(f"CLEAN FEED (close-aware vs Binance, thr={CLOSE_THR*100:.0f}%) — "
|
||||||
|
f"{'DRY-RUN (nessuna scrittura)' if dry else f'backup in {bdir}'}\n")
|
||||||
|
grand = 0
|
||||||
|
for t in targets:
|
||||||
|
asset, tf = t.split("_", 1)
|
||||||
|
if dry:
|
||||||
|
# dry: conta soltanto, niente scrittura
|
||||||
|
r = _dry_close(asset, tf)
|
||||||
|
else:
|
||||||
|
r = clean_file_close(asset, tf, backup_dir=bdir)
|
||||||
|
if r.get("skip"):
|
||||||
|
print(f" {t:<9} SKIP ({r['skip']})"); continue
|
||||||
|
grand += r.get("fixed", 0)
|
||||||
|
yr = " ".join(f"{y}:{n}" for y, n in sorted(r.get("by_year", {}).items()))
|
||||||
|
print(f" {r['file']:<9} coperte={r.get('covered',0):>7} riparate={r.get('fixed',0):>4} "
|
||||||
|
f"no-binance={r.get('missing_binance',0):>5} | righe {r['rows_before']}=={r.get('rows_after',r['rows_before'])} "
|
||||||
|
f"residue>thr={r.get('still_over_thr','-')}")
|
||||||
|
if yr:
|
||||||
|
print(f" per anno: {yr}")
|
||||||
|
for line in r.get("log", []):
|
||||||
|
print(line)
|
||||||
|
print(f"\n TOTALE barre riparate (close-aware): {grand}")
|
||||||
|
return
|
||||||
|
targets = args or [f"{a}_{tf}" for a in ("BTC", "ETH") for tf in ("5m", "15m", "30m", "1h")]
|
||||||
|
print(f"CLEAN FEED — backup in {BACKUP}\n")
|
||||||
|
grand = 0
|
||||||
|
for t in targets:
|
||||||
|
asset, tf = t.split("_", 1)
|
||||||
|
r = clean_file(asset, tf)
|
||||||
|
if r.get("skip"):
|
||||||
|
print(f" {t:<9} SKIP ({r['skip']})"); continue
|
||||||
|
grand += r.get("repaired", 0)
|
||||||
|
print(f" {r['file']:<9} sospette={r['suspect']:>3} riparate={r['repaired']:>3} "
|
||||||
|
f"reali-tenute={r.get('kept_real',0):>3} no-binance={r.get('missing_binance',0):>2} "
|
||||||
|
f"| righe {r['rows_before']}=={r['rows_after']} residue-sospette={r['still_suspect']}")
|
||||||
|
for line in r.get("log", [])[:8]:
|
||||||
|
print(line)
|
||||||
|
if len(r.get("log", [])) > 8:
|
||||||
|
print(f" ... (+{len(r['log'])-8} altre)")
|
||||||
|
print(f"\n TOTALE barre riparate: {grand}")
|
||||||
|
|
||||||
|
|
||||||
|
def _dry_close(asset: str, tf: str, thr: float = CLOSE_THR) -> dict:
|
||||||
|
"""Conta soltanto quante barre verrebbero riparate (nessuna scrittura)."""
|
||||||
|
if tf not in TF_MS:
|
||||||
|
return {"file": f"{asset}_{tf}", "skip": "tf-non-binance"}
|
||||||
|
path = _parquet_path(asset, tf)
|
||||||
|
if not path.exists():
|
||||||
|
return {"file": f"{asset}_{tf}", "skip": "no-file"}
|
||||||
|
df = pd.read_parquet(path)
|
||||||
|
tms = df["timestamp"].to_numpy("int64"); c = df["close"].to_numpy(float)
|
||||||
|
bz = _binance_series(asset, tf, int(tms[0]), int(tms[-1]))
|
||||||
|
fixed, by_year, missing = 0, {}, 0
|
||||||
|
for i in range(len(df)):
|
||||||
|
b = bz.get(int(tms[i]))
|
||||||
|
if b is None:
|
||||||
|
missing += 1; continue
|
||||||
|
if b[3] > 0 and abs(c[i] - b[3]) / b[3] > thr:
|
||||||
|
fixed += 1
|
||||||
|
y = pd.to_datetime(int(tms[i]), unit="ms", utc=True).year
|
||||||
|
by_year[y] = by_year.get(y, 0) + 1
|
||||||
|
return {"file": f"{asset}_{tf}", "covered": len(df) - missing, "fixed": fixed,
|
||||||
|
"missing_binance": missing, "rows_before": len(df), "by_year": by_year, "log": []}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -40,9 +40,17 @@ ANN = 365.0 # giorni/anno per annualizzare
|
|||||||
|
|
||||||
|
|
||||||
# ---------------- equity giornaliere ----------------
|
# ---------------- equity giornaliere ----------------
|
||||||
def fade_daily_equity(asset: str, fn, params) -> pd.Series:
|
# SWAP fade 1h -> 15m (2026-06-12, scelta utente). Gate fade15m_port06_gate.py: parametri
|
||||||
"""Equity giornaliera di uno sleeve fade: trade 1h (filtro trend 3.0) -> equity -> daily."""
|
# 1h NON ri-tunati trasferiti a 15m, corr 15m-1h media 0.26, SWAP promosso (FULL CAGR
|
||||||
df = load_data(asset, "1h")
|
# 74->101% DD 3.46->2.47%, OOS Sh 10.07->10.86; OOS DD 1.48->2.09 accettato), edge ETH
|
||||||
|
# regge il flat-entry-skip. Il canonico segue il deployato per tenere la parita' delle
|
||||||
|
# due facce. Diario docs/diary/2026-06-12-fade15m-gate.md.
|
||||||
|
FADE_TF = "15m"
|
||||||
|
|
||||||
|
|
||||||
|
def fade_daily_equity(asset: str, fn, params, tf: str = FADE_TF) -> pd.Series:
|
||||||
|
"""Equity giornaliera di uno sleeve fade: trade (filtro trend 3.0) -> equity -> daily."""
|
||||||
|
df = load_data(asset, tf)
|
||||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
trades = build_trades(fn(df, **params), df, trend_max=3.0)
|
trades = build_trades(fn(df, **params), df, trend_max=3.0)
|
||||||
n = len(df); eq = np.full(n, INIT, dtype=float); cap = INIT
|
n = len(df); eq = np.full(n, INIT, dtype=float); cap = INIT
|
||||||
@@ -55,7 +63,7 @@ def fade_daily_equity(asset: str, fn, params) -> pd.Series:
|
|||||||
|
|
||||||
def build_all_sleeves() -> dict[str, pd.Series]:
|
def build_all_sleeves() -> dict[str, pd.Series]:
|
||||||
sleeves: dict[str, pd.Series] = {}
|
sleeves: dict[str, pd.Series] = {}
|
||||||
# --- FADE: 8 sleeve ---
|
# --- FADE: 6 sleeve (15m dal 2026-06-12, vedi FADE_TF) ---
|
||||||
for asset in ["BTC", "ETH"]:
|
for asset in ["BTC", "ETH"]:
|
||||||
for nm, (fn, params) in strats_for(asset).items():
|
for nm, (fn, params) in strats_for(asset).items():
|
||||||
sleeves[f"{nm}_{asset}"] = fade_daily_equity(asset, fn, params)
|
sleeves[f"{nm}_{asset}"] = fade_daily_equity(asset, fn, params)
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
"""ANALISI DI IMPATTO (sola lettura, da docs/TODO.md): bug bfill di `_daily_equity`.
|
||||||
|
|
||||||
|
IL BUG (scripts/analysis/honest_improve2.py:30):
|
||||||
|
daily = s.resample("1D").last().reindex(idx).ffill().bfill()
|
||||||
|
La serie `s` e' a PUNTI-TRADE (un valore di capitale per ogni exit). Il `reindex(idx)`
|
||||||
|
taglia PRIMA di forward-fillare: i giorni di IDX precedenti al primo trade DENTRO la
|
||||||
|
finestra restano NaN (il ffill non ha un valore precedente in-finestra da propagare) e
|
||||||
|
il `.bfill()` finale li riempie col capitale DOPO il primo trade in-finestra. Effetti:
|
||||||
|
1. l'ancora a idx[0] e' il capitale post-primo-trade-in-finestra, NON il capitale
|
||||||
|
portato avanti dall'ultimo trade PRIMA della finestra;
|
||||||
|
2. il rendimento del primo trade in-finestra viene CANCELLATO dalla serie daily
|
||||||
|
(la testa e' piatta al valore post-trade -> pct_change = 0 anche il giorno del trade).
|
||||||
|
|
||||||
|
CORREZIONE (qui, solo per confronto): ffill PRIMA del reindex (carry-forward su tutta la
|
||||||
|
storia trade) + testa pre-primo-trade-assoluto = capitale iniziale 1000. MAI valori dal futuro.
|
||||||
|
|
||||||
|
Sleeve canonici interessati (serie a punti-trade -> testa di IDX scoperta):
|
||||||
|
DIP01_BTC, PR_ETHBTC, PR_ETHBTC_15M, PR_LTCETH, PR_ADAETH, PR_BTCLTC, PR_ETHSOL,
|
||||||
|
TSM01, XS01 (questi due quasi-densi: punti daily/12h -> impatto atteso ~0).
|
||||||
|
TR01_basket / ROT02_rot passano da _daily_equity ma con punti PER-BARRA (densi dal
|
||||||
|
2018) -> verificati comunque qui via monkeypatch runtime (nessun file canonico toccato).
|
||||||
|
I fade (combine_portfolio.py:52) e SH01 (shape_ml_validate.py:124) usano lo stesso
|
||||||
|
pattern reindex+bfill ma su equity PER-BARRA con dati che iniziano prima di IDX[0]
|
||||||
|
-> il bfill e' un no-op (verificato: nessun NaN in testa).
|
||||||
|
|
||||||
|
NB: le metriche OOS canoniche affettano la STESSA serie daily a SPLIT (metrics(dr,
|
||||||
|
lo=SPLIT)); la distorsione sta solo in testa (2021) -> l'OOS e' invariato per
|
||||||
|
costruzione se il primo trade in-finestra precede lo SPLIT. Questo script lo misura.
|
||||||
|
|
||||||
|
Uso: uv run python scripts/analysis/daily_equity_bfill_impact.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
import scripts.analysis.honest_improve2 as hi2
|
||||||
|
from scripts.analysis.honest_improve2 import _norm, dip_market_gated
|
||||||
|
from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns
|
||||||
|
from scripts.analysis.pairs_research import pairs_sim, pairs_sim_flat
|
||||||
|
from scripts.analysis.tsmom_research import tsmom_sim
|
||||||
|
from scripts.strategies.PR01_pairs_reversion import PAIRS as PAIR_DEFS
|
||||||
|
from scripts.strategies.XS01_cross_sectional import xsec_sim
|
||||||
|
from scripts.portfolios._defs import PORTFOLIOS
|
||||||
|
from src.portfolio import weighting as W
|
||||||
|
|
||||||
|
INIT = 1000.0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- le due convenzioni ----------------
|
||||||
|
def daily_equity_buggy(ts_list, cap_list, idx):
|
||||||
|
"""Replica ESATTA di honest_improve2._daily_equity (per parity-check)."""
|
||||||
|
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
|
||||||
|
s = s[~s.index.duplicated(keep="last")].sort_index()
|
||||||
|
return s.resample("1D").last().reindex(idx).ffill().bfill()
|
||||||
|
|
||||||
|
|
||||||
|
def daily_equity_fixed(ts_list, cap_list, idx, init=INIT):
|
||||||
|
"""CORRETTA: ancora = capitale portato avanti dall'ultimo trade PRIMA della
|
||||||
|
finestra (ffill prima del reindex); pre-primo-trade assoluto = capitale iniziale."""
|
||||||
|
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
|
||||||
|
s = s[~s.index.duplicated(keep="last")].sort_index()
|
||||||
|
daily = s.resample("1D").last().ffill() # carry-forward su TUTTA la storia
|
||||||
|
daily = daily.reindex(idx).ffill() # coda oltre l'ultimo trade
|
||||||
|
return daily.fillna(init) # testa pre-primo-trade: capitale iniziale
|
||||||
|
|
||||||
|
|
||||||
|
def head_info(ts_list, cap_list, idx):
|
||||||
|
"""(primo giorno con trade dentro IDX, rendimento di testa perso dal bfill %)."""
|
||||||
|
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
|
||||||
|
s = s[~s.index.duplicated(keep="last")].sort_index()
|
||||||
|
raw = s.resample("1D").last().reindex(idx) # senza fill: NaN = nessun trade quel giorno
|
||||||
|
first = raw.first_valid_index()
|
||||||
|
if first is None:
|
||||||
|
return None, 0.0
|
||||||
|
fixed = daily_equity_fixed(ts_list, cap_list, idx)
|
||||||
|
lost = (fixed.loc[first] / fixed.iloc[0] - 1) * 100 # ritorno idx[0]->primo trade-day
|
||||||
|
return first.date(), float(lost)
|
||||||
|
|
||||||
|
|
||||||
|
def m2(eq: pd.Series):
|
||||||
|
dr = eq.pct_change().fillna(0.0)
|
||||||
|
return metrics(dr), metrics(dr, lo=SPLIT)
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_pair(label, b, f):
|
||||||
|
d_sh = f["sharpe"] - b["sharpe"]
|
||||||
|
d_dd = f["dd"] - b["dd"]
|
||||||
|
d_rt = f["ret"] - b["ret"]
|
||||||
|
return (f" {label:<22s}"
|
||||||
|
f"Sh {b['sharpe']:6.2f}->{f['sharpe']:6.2f} ({d_sh:+.3f}) "
|
||||||
|
f"DD {b['dd']:6.2f}->{f['dd']:6.2f} ({d_dd:+.3f}pp) "
|
||||||
|
f"ret {b['ret']:+9.1f}->{f['ret']:+9.1f} ({d_rt:+8.2f}pp)")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 110)
|
||||||
|
print(" IMPATTO bug bfill _daily_equity (honest_improve2.py:30) — attuale vs corretto")
|
||||||
|
print(f" IDX {IDX[0].date()} -> {IDX[-1].date()} | OOS da {OOS_DATE} (slice a SPLIT={SPLIT} sui rendimenti daily)")
|
||||||
|
print("=" * 110)
|
||||||
|
|
||||||
|
# ---------------- [1] baseline canonica (bfill cosi' com'e') ----------------
|
||||||
|
print("\n[1] build_everything() canonico (2-3 min)...")
|
||||||
|
from scripts.analysis.report_families import build_everything
|
||||||
|
S, pairs, tsm, shape = build_everything()
|
||||||
|
base = {**S, **pairs, **tsm, **shape}
|
||||||
|
|
||||||
|
# ---------------- [2] ri-simula gli sleeve a punti-trade ----------------
|
||||||
|
print("[2] ri-simulazione sleeve a punti-trade (parity-check + versione corretta)...")
|
||||||
|
raw: dict[str, tuple] = {}
|
||||||
|
d = dip_market_gated("BTC", market_n=0, return_equity=True)
|
||||||
|
raw["DIP01_BTC"] = (d["eq_ts"], d["eq_v"])
|
||||||
|
for a, b_, p in PAIR_DEFS:
|
||||||
|
r = pairs_sim(a, b_, **p)
|
||||||
|
raw[f"PR_{a}{b_}"] = (r["eq_ts"], r["eq_v"])
|
||||||
|
r15 = pairs_sim_flat("ETH", "BTC", tf="15m", n=66, z_in=1.674, z_exit=1.0,
|
||||||
|
max_bars=35, flat_skip=True, pos=0.075)
|
||||||
|
raw["PR_ETHBTC_15M"] = (r15["eq_ts"], r15["eq_v"])
|
||||||
|
t = tsmom_sim()
|
||||||
|
raw["TSM01"] = (t["eq_ts"], t["eq_v"])
|
||||||
|
x = xsec_sim()
|
||||||
|
raw["XS01"] = (x["eq_ts"], x["eq_v"])
|
||||||
|
|
||||||
|
fixed: dict[str, pd.Series] = {}
|
||||||
|
print(f"\n {'sleeve':<16s}{'parity(max|diff|)':>18s}{'1o trade in IDX':>17s}{'ret testa perso%':>18s}")
|
||||||
|
for k, (ts, v) in raw.items():
|
||||||
|
bug = _norm(daily_equity_buggy(ts, v, IDX))
|
||||||
|
par = float((bug - base[k]).abs().max())
|
||||||
|
fixed[k] = _norm(daily_equity_fixed(ts, v, IDX))
|
||||||
|
first, lost = head_info(ts, v, IDX)
|
||||||
|
flag = "" if par < 1e-9 else " <-- PARITY FAIL"
|
||||||
|
print(f" {k:<16s}{par:>18.2e}{str(first):>17s}{lost:>+18.3f}{flag}")
|
||||||
|
|
||||||
|
# TR01/ROT02: passano da _daily_equity ma con punti per-barra (densi) ->
|
||||||
|
# ricalcolo con monkeypatch RUNTIME della funzione (nessun file toccato).
|
||||||
|
orig_de = hi2._daily_equity
|
||||||
|
try:
|
||||||
|
hi2._daily_equity = daily_equity_fixed
|
||||||
|
tr_f = _norm(hi2._tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], IDX))
|
||||||
|
rot_f = _norm(hi2._rot_daily_equity(IDX))
|
||||||
|
finally:
|
||||||
|
hi2._daily_equity = orig_de
|
||||||
|
for k, sf in (("TR01_basket", tr_f), ("ROT02_rot", rot_f)):
|
||||||
|
diff = float((sf - base[k]).abs().max())
|
||||||
|
print(f" {k:<16s}{'(denso)':>18s}{'—':>17s}{diff:>18.2e} (diff fixed-vs-base: atteso ~0)")
|
||||||
|
fixed[k] = sf
|
||||||
|
|
||||||
|
# ---------------- [3] metriche per sleeve: attuale vs corretto ----------------
|
||||||
|
print("\n" + "=" * 110)
|
||||||
|
print(" (3) SLEEVE a punti-trade — FULL e OOS, attuale(bfill) -> corretto(carry-forward)")
|
||||||
|
print("=" * 110)
|
||||||
|
rows_oos_delta = {}
|
||||||
|
for k in fixed:
|
||||||
|
bf, bo = m2(base[k])
|
||||||
|
ff, fo = m2(fixed[k])
|
||||||
|
print(fmt_pair(f"{k} FULL", bf, ff))
|
||||||
|
print(fmt_pair(f"{k} OOS ", bo, fo))
|
||||||
|
rows_oos_delta[k] = (ff["sharpe"] - bf["sharpe"], ff["dd"] - bf["dd"],
|
||||||
|
fo["sharpe"] - bo["sharpe"], fo["dd"] - bo["dd"])
|
||||||
|
|
||||||
|
# ---------------- [4] PORT06: attuale vs corretto ----------------
|
||||||
|
print("\n" + "=" * 110)
|
||||||
|
print(" (4) PORT06 (cap PAIRS 0.33 + SHAPE 0.0588) — attuale vs corretto")
|
||||||
|
print("=" * 110)
|
||||||
|
p = PORTFOLIOS["PORT06"]
|
||||||
|
|
||||||
|
def port_m(members):
|
||||||
|
ids = p.sleeve_ids
|
||||||
|
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
||||||
|
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
|
||||||
|
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
||||||
|
drp = port_returns({i: members[i] for i in ids}, w)
|
||||||
|
return metrics(drp), metrics(drp, lo=SPLIT)
|
||||||
|
|
||||||
|
members_fix = {**base, **fixed}
|
||||||
|
bf, bo = port_m(base)
|
||||||
|
ff, fo = port_m(members_fix)
|
||||||
|
print(fmt_pair("PORT06 FULL", bf, ff))
|
||||||
|
print(fmt_pair("PORT06 OOS ", bo, fo))
|
||||||
|
|
||||||
|
# ---------------- [5] verdetto ----------------
|
||||||
|
print("\n" + "=" * 110)
|
||||||
|
print(" (5) VERDETTO (soglie materialita': >0.1 Sharpe o >0.5pp DD su PORT06 OOS)")
|
||||||
|
print("=" * 110)
|
||||||
|
d_sh_oos = abs(fo["sharpe"] - bo["sharpe"])
|
||||||
|
d_dd_oos = abs(fo["dd"] - bo["dd"])
|
||||||
|
d_sh_full = abs(ff["sharpe"] - bf["sharpe"])
|
||||||
|
d_dd_full = abs(ff["dd"] - bf["dd"])
|
||||||
|
materiale = d_sh_oos > 0.1 or d_dd_oos > 0.5
|
||||||
|
print(f" PORT06 OOS : dSharpe {fo['sharpe']-bo['sharpe']:+.4f} dDD {fo['dd']-bo['dd']:+.4f}pp"
|
||||||
|
f" -> {'MATERIALE' if materiale else 'NON materiale'}")
|
||||||
|
print(f" PORT06 FULL: dSharpe {ff['sharpe']-bf['sharpe']:+.4f} dDD {ff['dd']-bf['dd']:+.4f}pp")
|
||||||
|
worst = sorted(rows_oos_delta.items(), key=lambda kv: -abs(kv[1][0]) - abs(kv[1][1]) / 10)
|
||||||
|
print(" Sleeve piu' toccati (dSharpe FULL, dDD FULL, dSharpe OOS, dDD OOS):")
|
||||||
|
for k, (ds, dd_, dso, ddo) in worst[:5]:
|
||||||
|
print(f" {k:<16s} FULL {ds:+.3f} / {dd_:+.3f}pp OOS {dso:+.3f} / {ddo:+.3f}pp")
|
||||||
|
print("\n Nota strutturale: l'OOS canonico e' uno slice a SPLIT della stessa serie daily;")
|
||||||
|
print(" la distorsione bfill vive solo in testa (prima del primo trade in IDX) -> se il")
|
||||||
|
print(" primo trade in-finestra precede lo SPLIT, l'OOS e' INVARIATO per costruzione.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"""PIANO STORICO DERIBIT — quanta storia copre davvero il venue dove ESEGUIAMO?
|
||||||
|
|
||||||
|
Obiettivo: scegliere la fonte migliore per ricostruire lo storico di backtest, dato
|
||||||
|
che si esegue su Deribit. Principio (gia' misurato in multi_source_check): l'ancora
|
||||||
|
giusta e' il VENUE DI ESECUZIONE, non Binance/USDT. Qui rispondo con i numeri a:
|
||||||
|
|
||||||
|
1. COPERTURA: da quando esiste OHLCV su Deribit MAINNET (ccxt pubblico, no token) per
|
||||||
|
gli strumenti che tradiamo — inverse (BTC/ETH-PERPETUAL) e lineari USDC.
|
||||||
|
2. TIMEFRAME nativi disponibili su Deribit.
|
||||||
|
3. FEDELTA' inverse-vs-lineare (stesso indice? -> posso usare l'inverse, storia lunga,
|
||||||
|
come price-series e i lineari recenti sono ridondanti per il PREZZO).
|
||||||
|
4. GAP pre-Deribit: quanto indietro vanno le strategie e cosa manca -> da gap-fillare
|
||||||
|
con Coinbase USD (spot, NON USDT).
|
||||||
|
|
||||||
|
Tutto via ccxt pubblico Deribit (= api.deribit.com mainnet, reale). Non modifica nulla.
|
||||||
|
|
||||||
|
uv run python scripts/analysis/deribit_history_plan.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import ccxt
|
||||||
|
|
||||||
|
DERIBIT = ccxt.deribit({"enableRateLimit": True})
|
||||||
|
COINBASE = ccxt.coinbase({"enableRateLimit": True})
|
||||||
|
|
||||||
|
|
||||||
|
def earliest(symbol: str, tf: str = "1d") -> tuple[str | None, int, str | None]:
|
||||||
|
"""Trova la prima candela disponibile (probe since 2016) + n candele totali stimate."""
|
||||||
|
since = int(pd.Timestamp("2016-01-01", tz="UTC").timestamp() * 1000)
|
||||||
|
try:
|
||||||
|
rows = DERIBIT.fetch_ohlcv(symbol, tf, since=since, limit=5000)
|
||||||
|
except Exception as e:
|
||||||
|
return None, 0, f"{type(e).__name__}: {str(e)[:60]}"
|
||||||
|
if not rows:
|
||||||
|
return None, 0, "no-data"
|
||||||
|
first = pd.to_datetime(int(rows[0][0]), unit="ms", utc=True)
|
||||||
|
last = pd.to_datetime(int(rows[-1][0]), unit="ms", utc=True)
|
||||||
|
return f"{first.date()} -> {last.date()}", len(rows), None
|
||||||
|
|
||||||
|
|
||||||
|
def list_perps() -> dict:
|
||||||
|
"""Risolve i simboli ccxt reali dei perp Deribit per BTC/ETH (inverse + lineari)."""
|
||||||
|
DERIBIT.load_markets()
|
||||||
|
found = {}
|
||||||
|
for sym, m in DERIBIT.markets.items():
|
||||||
|
if not m.get("swap"):
|
||||||
|
continue
|
||||||
|
base = m.get("base")
|
||||||
|
if base not in ("BTC", "ETH"):
|
||||||
|
continue
|
||||||
|
settle = m.get("settle")
|
||||||
|
kind = "inverse" if m.get("inverse") else "linear"
|
||||||
|
found[f"{base}-{kind}({settle})"] = sym
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_series(ex, symbol, tf, start, end, limit=1000):
|
||||||
|
start_ms = int(pd.Timestamp(start, tz="UTC").timestamp() * 1000)
|
||||||
|
end_ms = int(pd.Timestamp(end, tz="UTC").timestamp() * 1000)
|
||||||
|
tf_ms = ex.parse_timeframe(tf) * 1000
|
||||||
|
out, since = {}, start_ms
|
||||||
|
while since <= end_ms:
|
||||||
|
try:
|
||||||
|
rows = ex.fetch_ohlcv(symbol, tf, since=since, limit=limit)
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for r in rows:
|
||||||
|
if start_ms <= int(r[0]) <= end_ms and r[4]:
|
||||||
|
out[int(r[0])] = float(r[4])
|
||||||
|
nxt = int(rows[-1][0]) + tf_ms
|
||||||
|
if nxt <= since:
|
||||||
|
break
|
||||||
|
since = nxt
|
||||||
|
if len(rows) < limit and since > end_ms:
|
||||||
|
break
|
||||||
|
return pd.Series(out)
|
||||||
|
|
||||||
|
|
||||||
|
def dev_bps(a: pd.Series, b: pd.Series) -> tuple[int, float, float, float]:
|
||||||
|
df = pd.concat([a.rename("a"), b.rename("b")], axis=1, join="inner")
|
||||||
|
if len(df) == 0:
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
d = (df["a"] - df["b"]).abs() / df["b"] * 1e4
|
||||||
|
return len(df), float(d.median()), float(d.quantile(.95)), float(d.max())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 84)
|
||||||
|
print(" PIANO STORICO DERIBIT MAINNET (ccxt pubblico, reale)")
|
||||||
|
print("=" * 84)
|
||||||
|
|
||||||
|
print("\n[1] Simboli perp Deribit BTC/ETH risolti:")
|
||||||
|
perps = list_perps()
|
||||||
|
for k, v in perps.items():
|
||||||
|
print(f" {k:<22s} -> {v}")
|
||||||
|
|
||||||
|
print("\n[2] COPERTURA storica (1d, probe da 2016):")
|
||||||
|
print(f" {'strumento':<22s}{'range disponibile':<28s}{'giorni':>8s}")
|
||||||
|
cov = {}
|
||||||
|
for k, sym in perps.items():
|
||||||
|
rng, n, err = earliest(sym, "1d")
|
||||||
|
cov[k] = (sym, rng, n)
|
||||||
|
print(f" {k:<22s}{(rng or err or '-'):<28s}{n:>8d}")
|
||||||
|
|
||||||
|
print("\n[3] TIMEFRAME nativi Deribit (test su BTC inverse, oggi):")
|
||||||
|
bsym = next((s for k, s in perps.items() if k.startswith("BTC-inverse")), "BTC/USD:BTC")
|
||||||
|
tfs = []
|
||||||
|
for tf in ("1m", "3m", "5m", "10m", "15m", "30m", "1h", "2h", "3h", "4h", "6h", "12h", "1d"):
|
||||||
|
try:
|
||||||
|
r = DERIBIT.fetch_ohlcv(bsym, tf, limit=3)
|
||||||
|
tfs.append(tf if r else f"{tf}:vuoto")
|
||||||
|
except Exception:
|
||||||
|
tfs.append(f"{tf}:NO")
|
||||||
|
print(f" ok: {[t for t in tfs if ':' not in t]}")
|
||||||
|
print(f" ko: {[t for t in tfs if ':' in t]}")
|
||||||
|
|
||||||
|
print("\n[4] FEDELTA' inverse-vs-lineare USDC (close 1h, ultimi ~40g):")
|
||||||
|
for base in ("BTC", "ETH"):
|
||||||
|
inv = next((s for k, s in perps.items() if k.startswith(f"{base}-inverse")), None)
|
||||||
|
lin = next((s for k, s in perps.items() if k.startswith(f"{base}-linear")), None)
|
||||||
|
if not inv or not lin:
|
||||||
|
print(f" {base}: manca inverse o lineare"); continue
|
||||||
|
a = fetch_series(DERIBIT, inv, "1h", "2026-04-15", "2026-05-27")
|
||||||
|
b = fetch_series(DERIBIT, lin, "1h", "2026-04-15", "2026-05-27")
|
||||||
|
n, med, p95, mx = dev_bps(a, b)
|
||||||
|
print(f" {base}: barre={n} inverse-vs-lineare med {med:.1f} bps p95 {p95:.1f} max {mx:.1f}")
|
||||||
|
|
||||||
|
print("\n[5] GAP pre-Deribit: Deribit inverse vs Coinbase USD su finestra PROFONDA (2020-06, 1d):")
|
||||||
|
for base in ("BTC", "ETH"):
|
||||||
|
inv = next((s for k, s in perps.items() if k.startswith(f"{base}-inverse")), None)
|
||||||
|
a = fetch_series(DERIBIT, inv, "1d", "2020-06-01", "2020-09-01")
|
||||||
|
b = fetch_series(COINBASE, f"{base}/USD", "1d", "2020-06-01", "2020-09-01", limit=300)
|
||||||
|
n, med, p95, mx = dev_bps(a, b)
|
||||||
|
cov_first = cov.get(f"{base}-inverse(BTC)" if base == "BTC" else f"{base}-inverse(ETH)", (None, "?", 0))[1]
|
||||||
|
print(f" {base}: Deribit-vs-Coinbase barre={n} med {med:.1f} bps p95 {p95:.1f} max {mx:.1f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+2
-13
@@ -33,11 +33,9 @@ sys.path.insert(0, str(PROJECT_ROOT))
|
|||||||
|
|
||||||
from src.data.downloader import load_data
|
from src.data.downloader import load_data
|
||||||
from scripts.analysis.strategy_research import atr
|
from scripts.analysis.strategy_research import atr
|
||||||
from scripts.analysis.combine_portfolio import (
|
from scripts.analysis.combine_portfolio import _norm, IDX, metrics, SPLIT, OOS_DATE
|
||||||
_norm, IDX, port_returns, metrics, SPLIT, OOS_DATE,
|
from scripts.analysis._port06_gate_common import port_metrics
|
||||||
)
|
|
||||||
from scripts.portfolios._defs import PORTFOLIOS
|
from scripts.portfolios._defs import PORTFOLIOS
|
||||||
from src.portfolio import weighting as W
|
|
||||||
|
|
||||||
FEE_RT, LEV, POS, INIT = 0.001, 3.0, 0.15, 1000.0
|
FEE_RT, LEV, POS, INIT = 0.001, 3.0, 0.15, 1000.0
|
||||||
BUFFER = 0.5
|
BUFFER = 0.5
|
||||||
@@ -139,15 +137,6 @@ def cell_metrics(eq):
|
|||||||
return metrics(dr), metrics(dr, lo=SPLIT)
|
return metrics(dr), metrics(dr, lo=SPLIT)
|
||||||
|
|
||||||
|
|
||||||
def port_metrics(members, p):
|
|
||||||
ids = p.sleeve_ids
|
|
||||||
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
|
||||||
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
|
|
||||||
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
|
||||||
drp = port_returns({i: members[i] for i in ids}, w)
|
|
||||||
return metrics(drp), metrics(drp, lo=SPLIT)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
p = PORTFOLIOS["PORT06"]
|
p = PORTFOLIOS["PORT06"]
|
||||||
print("=" * 104)
|
print("=" * 104)
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""Drift monitor per-famiglia — il rolling-return corrente di ogni famiglia vs la
|
||||||
|
DISTRIBUZIONE STORICA dei propri rolling-return (stessa finestra, storia 2021+).
|
||||||
|
|
||||||
|
Non è un filtro di trading: è OSSERVABILITÀ (la protezione giusta contro il drift è
|
||||||
|
accorgersene presto, non ritoccare i parametri — lezione 2026-06-11: le FADE al 2°
|
||||||
|
percentile sul 120g sono state trovate a mano; questo script lo rende ripetibile).
|
||||||
|
|
||||||
|
Percentile basso = la famiglia sta attraversando uno dei suoi tratti peggiori:
|
||||||
|
- sotto P_WARN (5%): segnalato — coerente con la coda storica, OSSERVARE;
|
||||||
|
- il PORT06 complessivo sotto P_WARN è più serio (la diversificazione non copre).
|
||||||
|
Equity dal builder canonico (all_sleeve_equities → parità coi gate).
|
||||||
|
|
||||||
|
uv run python scripts/analysis/drift_monitor.py # stampa
|
||||||
|
uv run python scripts/analysis/drift_monitor.py --telegram # + invio Telegram
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from scripts.analysis.combine_portfolio import port_returns
|
||||||
|
from scripts.portfolios._defs import PORTFOLIOS
|
||||||
|
from src.portfolio.sleeves import all_sleeve_equities
|
||||||
|
from src.portfolio import weighting as W
|
||||||
|
|
||||||
|
WINDOWS = (60, 120) # giorni
|
||||||
|
P_WARN = 5.0 # percentile sotto cui segnalare
|
||||||
|
|
||||||
|
|
||||||
|
def family_returns():
|
||||||
|
"""Rendimenti daily per famiglia (equal-weight intra-famiglia) + PORT06 (pesi cap)."""
|
||||||
|
p = PORTFOLIOS["PORT06"]
|
||||||
|
eq = dict(all_sleeve_equities())
|
||||||
|
ids = list(p.sleeve_ids)
|
||||||
|
fams: dict[str, list] = {}
|
||||||
|
for i in ids:
|
||||||
|
fams.setdefault(W.family_of(i), []).append(i)
|
||||||
|
out = {}
|
||||||
|
for f, members in sorted(fams.items()):
|
||||||
|
out[f] = port_returns({i: eq[i] for i in members},
|
||||||
|
{i: 1 / len(members) for i in members})
|
||||||
|
dr = pd.DataFrame({i: eq[i].pct_change().fillna(0.0) for i in ids})
|
||||||
|
w = W.weight_vector("cap", ids, dr, caps=p.caps, clusters=p.clusters)
|
||||||
|
out["PORT06"] = port_returns({i: eq[i] for i in ids}, w)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def drift_rows():
|
||||||
|
rows = []
|
||||||
|
for name, r in family_returns().items():
|
||||||
|
for win in WINDOWS:
|
||||||
|
# vettoriale (log1p+rolling sum) invece di apply(np.prod): identico
|
||||||
|
# numericamente, ~100x piu' veloce del callback Python per-finestra
|
||||||
|
roll = np.expm1(np.log1p(r).rolling(win).sum())
|
||||||
|
roll = roll.dropna()
|
||||||
|
if len(roll) < 100:
|
||||||
|
continue
|
||||||
|
cur = float(roll.iloc[-1])
|
||||||
|
pct = float((roll < cur).mean() * 100)
|
||||||
|
rows.append(dict(name=name, win=win, cur=cur * 100, pct=pct,
|
||||||
|
p5=float(roll.quantile(0.05) * 100),
|
||||||
|
med=float(roll.median() * 100)))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def build_report(rows) -> tuple[str, bool]:
|
||||||
|
warn = [r for r in rows if r["pct"] < P_WARN]
|
||||||
|
L = ["📉 <b>Drift monitor</b> — rolling-return vs storia propria (2021+)"]
|
||||||
|
L.append("<pre>" + f"{'famiglia':<9}{'win':>5}{'corr%':>8}{'pct':>6}{'p5%':>8}{'med%':>7}")
|
||||||
|
for r in rows:
|
||||||
|
flag = " ⚠️" if r["pct"] < P_WARN else ""
|
||||||
|
L.append(f"{r['name']:<9}{r['win']:>4}g{r['cur']:>+8.1f}{r['pct']:>5.0f}%"
|
||||||
|
f"{r['p5']:>+8.1f}{r['med']:>+7.1f}{flag}")
|
||||||
|
L.append("</pre>")
|
||||||
|
if warn:
|
||||||
|
names = ", ".join(f"{r['name']} {r['win']}g (p{r['pct']:.0f})" for r in warn)
|
||||||
|
L.append(f"⚠️ sotto il p{P_WARN:.0f} storico: {names} — coda storica della famiglia: "
|
||||||
|
"OSSERVARE, non ritoccare i parametri (drift ≠ rottura; "
|
||||||
|
"vedi docs/diary/2026-06-11-stability-sweep.md)")
|
||||||
|
else:
|
||||||
|
L.append(f"✅ tutte le famiglie sopra il p{P_WARN:.0f} storico")
|
||||||
|
return "\n".join(L), bool(warn)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rows = drift_rows()
|
||||||
|
report, warned = build_report(rows)
|
||||||
|
import re
|
||||||
|
print(re.sub(r"</?(b|pre)>", "", report))
|
||||||
|
if "--telegram" in sys.argv:
|
||||||
|
from src.live.telegram_notifier import send_telegram
|
||||||
|
ok = send_telegram(report)
|
||||||
|
print(f"[telegram] inviato: {ok}")
|
||||||
|
return warned
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# exit code 1 su warn: utilizzabile da cron/script come canale d'allarme
|
||||||
|
# (coerente con reconcile_account; prima il bool era calcolato e buttato via)
|
||||||
|
sys.exit(1 if main() else 0)
|
||||||
+8
-101
@@ -24,106 +24,23 @@ from __future__ import annotations
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
from src.data.downloader import load_data
|
from src.data.downloader import load_data
|
||||||
from scripts.analysis.strategy_research import atr
|
from scripts.analysis.risk_management import strats_for
|
||||||
from scripts.analysis.risk_management import strats_for, FEE_RT, LEV, POS, INIT
|
from scripts.analysis.combine_portfolio import OOS_DATE
|
||||||
from scripts.analysis.combine_portfolio import (
|
from scripts.analysis._port06_gate_common import (
|
||||||
fade_daily_equity, _norm, IDX, port_returns, metrics, SPLIT, OOS_DATE,
|
build_trades_variant, equity_from_trades, port_metrics, dd as _dd,
|
||||||
)
|
)
|
||||||
from scripts.portfolios._defs import PORTFOLIOS
|
from scripts.portfolios._defs import PORTFOLIOS
|
||||||
from src.portfolio import weighting as W
|
|
||||||
|
|
||||||
BUFFER = 0.5 # EXIT-16 promossa: close-confirm con buffer 0.5 ATR
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- engine replay
|
|
||||||
def build_trades_variant(ents, df, mode, buffer=BUFFER,
|
|
||||||
lev=LEV, fee_rt=FEE_RT, trend_max=3.0, ema_long=200):
|
|
||||||
"""Replica ESATTA di risk_management.build_trades, cambiando SOLO il ramo SL.
|
|
||||||
|
|
||||||
mode="orig" : SL intrabar al livello (SL prima del TP) == canonico.
|
|
||||||
mode="exit16" : SL intrabar DISATTIVATO; close-confirm sul close[j]:
|
|
||||||
long esci a close[j] se close[j] < sl0 - buffer*atr14[j]
|
|
||||||
short esci a close[j] se close[j] > sl0 + buffer*atr14[j]
|
|
||||||
TP intrabar al livello e max_bars al close INVARIATI.
|
|
||||||
"""
|
|
||||||
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
|
||||||
n = len(c)
|
|
||||||
a = atr(df, 14)
|
|
||||||
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
|
|
||||||
fee = fee_rt * lev
|
|
||||||
out = []
|
|
||||||
last = -1
|
|
||||||
for e in ents:
|
|
||||||
i, d = e["i"], e["d"]
|
|
||||||
if i <= last or i + 1 >= n:
|
|
||||||
continue
|
|
||||||
if trend_max is not None and a[i] and abs(c[i] - el[i]) / a[i] > trend_max:
|
|
||||||
continue
|
|
||||||
entry = c[i]
|
|
||||||
tp, sl0, mb = e["tp"], e["sl"], e["max_bars"]
|
|
||||||
exit_p = c[min(i + mb, n - 1)]
|
|
||||||
j = min(i + mb, n - 1)
|
|
||||||
for k in range(1, mb + 1):
|
|
||||||
j = i + k
|
|
||||||
if j >= n:
|
|
||||||
exit_p = c[n - 1]
|
|
||||||
break
|
|
||||||
if mode == "orig":
|
|
||||||
hs = (d == 1 and l[j] <= sl0) or (d == -1 and h[j] >= sl0)
|
|
||||||
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
|
|
||||||
if hs:
|
|
||||||
exit_p = sl0
|
|
||||||
break
|
|
||||||
if ht:
|
|
||||||
exit_p = tp
|
|
||||||
break
|
|
||||||
if k == mb:
|
|
||||||
exit_p = c[j]
|
|
||||||
else: # exit16: no SL intrabar; TP intrabar; poi close-confirm SL al close[j]
|
|
||||||
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
|
|
||||||
if ht:
|
|
||||||
exit_p = tp
|
|
||||||
break
|
|
||||||
aj = a[j] if np.isfinite(a[j]) else 0.0
|
|
||||||
confirm = (d == 1 and c[j] < sl0 - buffer * aj) or \
|
|
||||||
(d == -1 and c[j] > sl0 + buffer * aj)
|
|
||||||
if confirm:
|
|
||||||
exit_p = c[j]
|
|
||||||
break
|
|
||||||
if k == mb:
|
|
||||||
exit_p = c[j]
|
|
||||||
ret = (exit_p - entry) / entry * d * lev - fee
|
|
||||||
out.append((i, j, ret))
|
|
||||||
last = j
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def fade_equity_variant(asset, fn, params, mode):
|
def fade_equity_variant(asset, fn, params, mode):
|
||||||
"""Stesso flusso di combine_portfolio.fade_daily_equity ma con build_trades_variant."""
|
"""Stesso flusso di combine_portfolio.fade_daily_equity ma con build_trades_variant."""
|
||||||
df = load_data(asset, "1h")
|
df = load_data(asset, "1h")
|
||||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
|
||||||
trades = build_trades_variant(fn(df, **params), df, mode=mode, trend_max=3.0)
|
trades = build_trades_variant(fn(df, **params), df, mode=mode, trend_max=3.0)
|
||||||
n = len(df)
|
return equity_from_trades(df, trades)
|
||||||
eq = np.full(n, INIT, dtype=float)
|
|
||||||
cap = INIT
|
|
||||||
for i, j, ret in sorted(trades, key=lambda t: t[1]):
|
|
||||||
cap = max(cap + cap * POS * ret, 10.0)
|
|
||||||
eq[j:] = cap
|
|
||||||
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
|
|
||||||
return _norm(s)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- pesi PORT06
|
|
||||||
def port_metrics(members: dict[str, pd.Series], weights: dict[str, float]):
|
|
||||||
dr = port_returns(members, weights)
|
|
||||||
return metrics(dr), metrics(dr, lo=SPLIT)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -174,17 +91,9 @@ def main():
|
|||||||
for sid in fade_ids:
|
for sid in fade_ids:
|
||||||
members_e16[sid] = eq_e16[sid] # sostituisco SOLO le 6 colonne fade
|
members_e16[sid] = eq_e16[sid] # sostituisco SOLO le 6 colonne fade
|
||||||
|
|
||||||
ids = p.sleeve_ids
|
# pesi cap canonici (gli stessi che usa Portfolio.backtest) dentro port_metrics
|
||||||
# pesi cap canonici (gli stessi che usa Portfolio.backtest)
|
f_b, o_b = port_metrics(members_base, p)
|
||||||
dr_base = pd.DataFrame({i: members_base[i].pct_change().fillna(0.0) for i in ids})
|
f_e, o_e = port_metrics(members_e16, p)
|
||||||
w_base = W.weight_vector(p.weighting, ids, dr_base, weights=p.weights,
|
|
||||||
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
|
||||||
dr_e16 = pd.DataFrame({i: members_e16[i].pct_change().fillna(0.0) for i in ids})
|
|
||||||
w_e16 = W.weight_vector(p.weighting, ids, dr_e16, weights=p.weights,
|
|
||||||
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
|
||||||
|
|
||||||
f_b, o_b = port_metrics({i: members_base[i] for i in ids}, w_base)
|
|
||||||
f_e, o_e = port_metrics({i: members_e16[i] for i in ids}, w_e16)
|
|
||||||
|
|
||||||
print("\n" + "=" * 96)
|
print("\n" + "=" * 96)
|
||||||
print(f" [3] PORT06 — pesi={p.weighting} caps={p.caps} | OOS da {OOS_DATE} | leva3x interna fade, pos0.15")
|
print(f" [3] PORT06 — pesi={p.weighting} caps={p.caps} | OOS da {OOS_DATE} | leva3x interna fade, pos0.15")
|
||||||
@@ -207,8 +116,6 @@ def main():
|
|||||||
f"{'orig DD%':>10s}{'e16 DD%':>10s}")
|
f"{'orig DD%':>10s}{'e16 DD%':>10s}")
|
||||||
for sid in fade_ids:
|
for sid in fade_ids:
|
||||||
ro = eq_orig[sid]; re = eq_e16[sid]
|
ro = eq_orig[sid]; re = eq_e16[sid]
|
||||||
def _dd(s):
|
|
||||||
pk = s.cummax(); return float(((pk - s) / pk).max() * 100)
|
|
||||||
rro = (ro.iloc[-1] / ro.iloc[0] - 1) * 100
|
rro = (ro.iloc[-1] / ro.iloc[0] - 1) * 100
|
||||||
rre = (re.iloc[-1] / re.iloc[0] - 1) * 100
|
rre = (re.iloc[-1] / re.iloc[0] - 1) * 100
|
||||||
print(f" {sid:<10s}{rro:>12.1f}{rre:>14.1f}{rre-rro:>+10.1f}"
|
print(f" {sid:<10s}{rro:>12.1f}{rre:>14.1f}{rre-rro:>+10.1f}"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user