9 Commits

Author SHA1 Message Date
Adriano a60ad30ac0 fix(live): naming Deribit corretto per alt -> tutte le 5 coppie pairs tradabili live
Gli alt su Deribit sono perp LINEARI USDC: "<COIN>_USDC-PERPETUAL" (storia dal 2022),
non "<COIN>-PERPETUAL" (vuoto per LTC/ADA, dati errati per SOL). INSTRUMENT_MAP corretto.
Smoke test live (live_smoke_pairs.py): tutte e 5 le coppie ricevono feed fresco (1448
barre, ultima ~0.4h) e ticcano. Riabilitate tutte le coppie in strategies.yml.
BTC/ETH restano inverse ("<COIN>-PERPETUAL"). CLAUDE.md / docstring PR01 aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:26:27 +02:00
Adriano bd31a15548 fix(live): smoke test REALE pairs -> live solo ETH/BTC (alt assenti su Deribit)
Test reale (scripts/analysis/live_smoke_pairs.py): fetch live Cerbero + tick vero per
coppia. Scoperta: l'endpoint Deribit serve solo BTC/ETH freschi; LTC/ADA-PERPETUAL sono
VUOTI e SOL ha pochi dati. Il backtest usava i parquet locali (8 asset completi), ma la
pipeline live no -> tradabile live SOLO ETH/BTC.

- strategies.yml: abilitata solo la coppia ETH/BTC; le altre 4 disabilitate con nota
  (valide a backtest, off finche' non si aggiunge un feed live per gli alt).
- live_smoke_pairs.py (nuovo): verifica end-to-end della pipeline live (no ordini reali).
- CLAUDE.md / docstring PR01: distinzione esplicita logica-validata vs live-disponibile.

Onesta': la validazione precedente era backtest-equivalence (ESATTA), NON test live;
il test live ha rivelato il limite del feed alt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:16:50 +02:00
Adriano 4dc0e77ee5 feat(live): worker a 2 gambe per i pairs (PR01 market-neutral)
src/live/pairs_worker.py: PairsWorker market-neutral (long A / short B sullo z-score
del log-ratio, exit |z|<=z_exit o max_bars, FEE SU 2 GAMBE = 2*fee_rt*lev, stato
persistente come StrategyWorker). multi_runner: sezione `pairs:` nello YAML, fetch di
entrambe le gambe, tick/status/shutdown; INSTRUMENT_MAP esteso agli alt. strategies.yml:
5 coppie PR01 (config universale n50 z2 zx0.75 mb72).

Validazione (scripts/analysis/validate_worker_pairs.py): replay live bar-per-bar ==
backtest pairs_sim ESATTAMENTE -> ETH/BTC capitale 2.870.429 = 2.870.429, 1754 trade,
win 74.1% identici. Caveat: shortabilita'/liquidita' del perp B sugli alt da verificare
in trading reale. CLAUDE.md / docstring PR01 aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:12:25 +02:00
Adriano 9a066eb76f feat(analysis): report_families.py - riepilogo strategie/famiglie per anno + integrazione
Report consolidato: (A) Ret%/anno per famiglia (FADE/HONEST/PAIRS/TSM01) e portafogli,
(B) Ret%/anno per ogni strategia singola (15 sleeve), (C) analisi di integrazione delle
nuove famiglie nel MASTER (+pairs/+TSM01/esteso) con correlazioni, (D) numeri sobri e
raccomandazione (leva 2x, cap pairs 30-35%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:20:23 +02:00
Adriano 7226946911 refactor(explore): irrobustimento anti-overfit di pairs/TSM01/master
Giro di validazione scettica (walk-forward, plateau, stress, scomposizione):

- PAIRS: config PER-COPPIA -> config UNIVERSALE (n50 z2 zx0.75 mb72), niente
  cherry-picking. Plateau confermato (heatmap 20/20 Sharpe>1) + walk-forward
  (ETH/BTC 11/12 finestre+, BTC/LTC 9/10). Scartata BNB/ETH (overfit). 5 coppie robuste.
- TSM01: gross 0.45->0.30 (stesso Sharpe, DD 22->15%); corr reale con ROT02 = 0.62
  (non 0.53); diversificatore, non motore. Robusto (36/36 config OOS+).
- Confluenza multi-TF SCARTATA: overfit (taglia 97% trade, ~40 in 8 anni, Sharpe crolla).
- MASTER: numeri sobri onesti -> OOS Sharpe 7.7/DD 2.3% e' regime calmo 2024-25
  (ottimistico ~50%); worst-DD 90g ~6%, Sharpe atteso ~5, regge leva 2x+slippage.
  Config robusta: equal-weight, leva 2x, cap pairs ~30-35% (sono ~57% del rischio).

Quanto trovato regge l'esame anti-overfit; numeri comunicati sobri. Doc/CLAUDE/memoria aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:11:33 +02:00
Adriano 945c2a2db6 feat(explore): 3a ondata - PAIRS a 6 coppie + verdetti su 4 nuove famiglie
Espansione dei meccanismi provati + 2 nuovi sondaggi (agenti paralleli, honest):
- PAIRS espansa a 6 coppie robuste: + BTC/LTC (robusta 1h E 4h, Sharpe 2.21, DD 24-34%),
  ETH/SOL e BNB/ETH (Sharpe 2.4+, solo 1h). Pattern: alt-liquido vs major.
- Fade su 6 alt: 0 robuste (mean-reversion vive solo su BTC/ETH; DOGE = artefatto).
- Low-vol anomaly: invertita in cripto (vince alta vol), ridondante -> scartata.
- Confluenza multi-TF: dimezza il DD di MR01 (ETH same Sharpe a DD 38% vs 63%) ->
  variante low-DD utile, non edge nuovo.

MASTER + 6 pairs (15 sleeve): CAGR 47->71%, OOS DD 4.7->2.3%, Sharpe OOS 4.33->7.71,
tutti gli anni positivi. Bilancio robusti: famiglia PAIRS (6) + TSM01 = 7 strategie nuove.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 01:13:54 +02:00
Adriano 33e3e2a603 feat(explore): esplora 9 famiglie alternative -> PAIRS (nuovo edge forte) + TSM01
Esplorazione onesta con agenti paralleli su harness condiviso (explore_lab.py):
ingresso close[i], netto fee, OOS, DD basso, attenzione fee. 7 famiglie su 9 sono
rumore (stagionalita' oraria/mensile, cross-sectional reversal, opening-range,
lead-lag BTC->alt, continuation intraday) e l'harness le rifiuta senza falsi positivi.

Due edge reali verificati indipendentemente:
- PR01 Pairs: spread reversion market-neutral su log-ratio z-score (ETH/BTC, LTC/ETH,
  ADA/ETH). ETH/BTC CAGR 144% Sharpe 4.04 OOS DD 17% 8/9 anni, corr mercato ~0.02,
  no-look-ahead verificato, regge fee 0.40%/coppia. Fee su 2 gambe (worker da estendere).
- TSM01: TSMOM multi-orizzonte 3/6/12m + risk-off, distinto da ROT02 (corr 0.53),
  DD 22%/12% OOS, mai un anno negativo, regge fee 0.40%.

Payoff: aggiungere i pairs (quasi scorrelati ~0.05) al MASTER -> CAGR 47->66%,
DD 5.2->3.8% full / 4.7->3.3% OOS, Sharpe OOS 4.33->6.86 (combine_v2.py).

Fix: explore_lab.get_df ora produce timestamp ms reale per 1d/4h (era placeholder).
Diario 2026-05-29-exploration.md + nota CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 01:07:08 +02:00
Adriano 666c906907 feat(analysis): report.py aggiornato con numero trade per anno
Report consolidato: (A) Ret% netto per anno di ogni strategia singola + portafogli
(FADE/HONEST/MASTER eq/5050), (B) numero trade per anno per strategia (ingressi per
fade/DIP01; ribilanciamenti per TR01/ROT02 a posizione continua), (C) riepilogo
portafogli FULL/OOS. Config deployata (MR03/ROT01 in waste, ROT02 top_k=3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 00:33:00 +02:00
Adriano 9ebdfc7a7a feat(ROT02): riduce il DD diversificando (top_k 2 -> 3)
ROT02 concentrava il book su 2 soli asset (DD 40%). top_k=3 dimezza quasi il DD
(40% -> 26%) e ALZA il ritorno full (+1095 -> +1303%, ret/DD da 27 a 50). Il
vol-target abbassa il DD ma sacrifica ritorno (de-leverage) -> tenuto top_k=3
senza VT. Caveat: OOS di ROT02 cala (+98 -> +68%, DD 12 -> 14%), ma il portafoglio
MASTER migliora lo Sharpe full (3.95 -> 4.23). Applicato a ROT02_dual_momentum.py
e a _rot_daily_equity (usata da PORT01/PORT03). Docstring/CLAUDE/diario aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 00:30:55 +02:00
19 changed files with 1546 additions and 22 deletions
+46 -6
View File
@@ -24,10 +24,12 @@ src/strategies/ → classe base Strategy ABC + indicatori condivisi
base.py → Strategy, Signal, BacktestResult, YearlyStats base.py → Strategy, Signal, BacktestResult, YearlyStats
indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation
src/live/ → paper trading live multi-strategia src/live/ → paper trading live multi-strategia
multi_runner.py → orchestratore: carica YAML, fetch candele, tick worker multi_runner.py → orchestratore: carica YAML (strategies + pairs), fetch candele, tick worker
strategy_worker.py → worker indipendente: capital, trade log, stato persistente. strategy_worker.py → worker single-leg: capital, trade log, stato persistente.
Exit guidati da strategia (TP/SL/max_bars via Signal.metadata), Exit guidati da strategia (TP/SL/max_bars via Signal.metadata),
fallback hold_bars/stop -2%. Usa fee_rt della strategia. fallback hold_bars/stop -2%. Usa fee_rt della strategia.
pairs_worker.py → worker a 2 GAMBE per PR01 (market-neutral): long A / short B sullo
z-score del log-ratio, exit |z|<=z_exit o max_bars, fee su 2 gambe.
strategy_loader.py → import dinamico classi Strategy da scripts/strategies/ strategy_loader.py → import dinamico classi Strategy da scripts/strategies/
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet) cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet)
signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS
@@ -126,20 +128,58 @@ paniere di 8 sleeve, contro il 20-70% del singolo. È la vera leva anti-drawdown
honest (DIP/TR/ROT trend+rotazione multi-crypto) sono **quasi scorrelate** honest (DIP/TR/ROT trend+rotazione multi-crypto) sono **quasi scorrelate**
(correlazione cross-famiglia ~0.05). Combinarle in un unico portafoglio migliora il (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 rischio/rendimento rispetto a ciascuna famiglia da sola: equal-weight dei 9 sleeve
→ DD 5.2% full / 4.7% OOS e Sharpe OOS 4.42 (vs honest-only 12% DD / 2.23 Sharpe e → DD 5.2% full / 4.7% OOS e Sharpe 4.23 full / 4.33 OOS (vs honest-only 12.6% DD /
fade-only 8.2% DD / 4.09 Sharpe), CAGR ~46% mantenuta. Studio in 2.20 Sharpe e fade-only 8.2% DD / 4.09 Sharpe), CAGR ~47% mantenuta. Studio in
`scripts/analysis/combine_portfolio.py`. `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 **Portafogli pronti (artefatti accorpati e migliorati).** Oltre a `PORT01` (solo
honest), due script in `scripts/strategies/`: honest), due script in `scripts/strategies/`:
- `PORT02_fade_master.py` — le 3 fade × BTC/ETH accorpate (6 sleeve, filtro trend), - `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%. 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: - `PORT03_all_master.py` — portafoglio MASTER (fade + honest, 9 sleeve). Due varianti:
`equal` (massimo Sharpe: DD 5.2%/4.7% OOS, Sharpe 3.95/4.42) e `5050` fra le due `equal` (massimo Sharpe: DD 5.2%/4.7% OOS, Sharpe 4.23/4.33) e `5050` fra le due
famiglie (minimo DD: 5.1% full / 4.3% OOS). È la configurazione consigliata. famiglie (minimo DD: 5.0% full / 4.5% OOS). È la configurazione consigliata.
Come `PORT01`, sono meta-portafogli (script `run()` di report), non `Strategy` con Come `PORT01`, sono meta-portafogli (script `run()` di report), non `Strategy` con
`generate_signals`, quindi non nel `strategy_loader`. `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).
**Metodologia obbligatoria per ogni nuova strategia** (per non ripetere l'errore squeeze): **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`. 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. 2. Backtest **NETTO** dopo fee realistiche Deribit (**0.10% RT** taker, non 0.20%) + leva.
+155
View File
@@ -0,0 +1,155 @@
# Diario — 2026-05-29 — Esplorazione di nuove famiglie di strategie
## Obiettivo
Trovare 5-10 nuove famiglie di strategie, diverse da quelle esistenti, migliori o
complementari, con DD basso e attenzione alle fee. Esplorazione onesta (no
look-ahead, netto fee, OOS) condotta con **agenti paralleli**, ognuno su una famiglia
indipendente, tutti sullo stesso harness condiviso (`scripts/analysis/explore_lab.py`).
Lavoro sul branch `strategy_explore`.
## Famiglie esplorate (9) ed esito onesto
| Famiglia | Esito | Note |
|---|---|---|
| **Pairs / spread reversion** | ✅ **VINCITORE** | Market-neutral, genuinamente nuova, decorrelata |
| **TSMOM multi-orizzonte** | ✅ diversificatore | Marginale ma distinto (corr 0.53 con ROT02), DD basso |
| Stagionalità settimanale | ⚠️ marginale/fragile | "Mercoledì-long-24h" 7/8 asset OOS+ ma effetto concentrato a 00:00 UTC |
| Vol-target BTC | ⚠️ marginale | Sharpe 0.94 vs 0.76 buy&hold, DD ancora 44% |
| Stagionalità intraday (ora) | ❌ rumore | L'edge orario muore sotto le fee |
| Stagionalità mensile/turn-of-month | ❌ rumore | Reale in-sample, morto OOS dal 2024 |
| Cross-sectional reversal | ❌ nessun edge | Perde vs equal-weight, corr 0.98 col momentum |
| Opening-range breakout | ❌ non generalizza | Solo BTC/ETH, alcuni regimi, fee-fragile |
| Lead-lag BTC→alt | ❌ nessun edge | Reazione contemporanea (corr lag+1 ≈ 0), non batte buy&hold |
| Momentum/continuation intraday | ❌ negativo | Conferma: il *fade* (mean-reversion) domina |
7 famiglie su 9 sono rumore — e l'harness le ha rifiutate senza produrre falsi
positivi (segnale che la metodologia onesta funziona). Due edge reali emergono.
## Vincitore 1 — PAIRS (market-neutral) — `PR01_pairs_reversion.py`
Scommette sul rientro del log-ratio di due cripto verso la media (z-score). Quando
`z ≤ 2` → long A / short B; `z ≥ +2` → l'opposto; esce al rientro (`|z| ≤ 0.5`) o a
tempo. Engine onesto verificato in `pairs_research.py` (test esplicito no-look-ahead:
`z[i]` invariato perturbando il futuro). Fee contate su **2 gambe** (0.20% RT/coppia).
Validazione (netto, leva 3x, OOS = ultimo 30%, 1h):
| Coppia | CAGR | Sharpe | OOS DD | anni+ |
|---|--:|--:|--:|--:|
| ETH/BTC | 144% | 4.04 | 17% | 8/9 |
| LTC/ETH | 71% | 2.52 | 10% | 7/8 |
| ADA/ETH | 77% | 2.16 | 11% | 7/8 |
Tutte le 10 coppie testate positive FULL+OOS, regge fee 0.40% RT/coppia, correlazione
col mercato ~0.02 (market-neutral confermato). DD pieno 42-49% (alto), ma OOS DD
10-17% (buono) e soprattutto **quasi-zero correlazione** col resto → diversificatore
eccezionale. Limite: 2 gambe (long+short), il worker live va esteso prima del live.
## Vincitore 2 — TSM01 (TSMOM multi-orizzonte) — `tsmom_research.py`
Long-only multi-crypto: tiene equal-weight gli asset con consenso pieno del segno di
momentum su 3/6/12 mesi, cash se BTC<SMA100. Distinto da ROT02 (persistenza assoluta
vs ranking relativo), corr 0.53. FULL +169% / OOS +80% / DD 22% / Sharpe 1.07,
**mai un anno negativo**, regge fee 0.40%. Verificato no-look-ahead (cheat-test
esplode a +575%). Marginale come stand-alone (rende meno di ROT02) ma utile in ensemble.
## Il payoff — combinare le nuove fonti col MASTER (`combine_v2.py`)
Le nuove sleeve sono quasi scorrelate col MASTER-9 (pairs ~0.02-0.08, TSM01 0.05).
Aggiungerle migliora nettamente il portafoglio:
| Portafoglio | CAGR | DD% | Sharpe | OOS DD% | OOS Sharpe |
|---|--:|--:|--:|--:|--:|
| MASTER-9 (base) | 47 | 5.2 | 4.23 | 4.7 | 4.33 |
| **MASTER + pairs (12)** | **66** | **3.8** | **5.67** | **3.3** | **6.86** |
| MASTER + TSM01 (10) | 44 | 4.7 | 4.21 | 4.2 | 4.33 |
| MASTER esteso (13) | 62 | 3.6 | 5.66 | 3.0 | 6.79 |
I **pairs** sono l'aggiunta decisiva: alzano la CAGR (47→66), **abbassano il DD**
(5.2→3.8 full, 4.7→**3.3** OOS) e portano lo Sharpe OOS a **6.86** — il free-lunch
della diversificazione da una fonte market-neutral scorrelata. TSM01 contribuisce
poco (diluisce il ritorno) ma abbassa lievemente il DD.
## Caveat onesti
- I pairs hanno DD pieno alto (42-49%) sull'1h; il vantaggio sta nella decorrelazione,
non nel DD stand-alone. Richiedono esecuzione a 2 gambe (short del perp B) — da
verificare shortabilità/liquidità sugli alt e raddoppio fee nel worker.
- Sharpe combinati 5-7 e CAGR 60%+ sono backtest a leva 3x su finestra 2021-2026 con
OOS ~1.6 anni e il 2024 cripto eccezionale: numeri ottimistici, da confermare in
paper trading live.
- TSMOM e le strategie honest condividono l'overlay risk-off SMA100: parte della loro
difensività è comune (non perfettamente indipendente).
## Terza ondata — espansione dei meccanismi provati + 2 nuovi sondaggi
Esplorate altre 4 direzioni con agenti paralleli:
- **Fade su 6 nuovi alt (ADA/BNB/DOGE/LTC/SOL/XRP)**: 0 robuste. La mean-reversion
fade vive solo su BTC/ETH (liquidi); sugli alt sparisce o è artefatto di pochi pump
(DOGE). Coerente con la lezione del progetto.
- **Espansione PAIRS** (tutte le 28 coppie): trovate **3 nuove coppie robuste**
BTC/LTC (robusta 1h *e* 4h, Sharpe 2.21, DD 24-34%, concentrazione PnL 9%), ETH/SOL
e BNB/ETH (Sharpe 2.4+, solo 1h). Pattern: sempre alt-liquido vs major, mai alt/alt.
PR01 ora ha **6 coppie**.
- **Low-volatility anomaly**: ❌ in cripto è INVERTITA (vince l'alta vol = alta beta),
ridondante con EW+risk-off/ROT02. L'anti-test high-vol stravince.
- **Confluenza multi-timeframe (fade 1h confermato da 4h)**: non crea edge nuovo e non
migliora lo Sharpe, ma **dimezza il DD** di MR01 (ETH: stesso Sharpe 3.17 a DD 38% vs
63%) e stabilizza l'OOS → utile variante low-DD, non strategia indipendente.
## Bilancio finale e MASTER esteso (6 pairs)
Robusti deployabili: **famiglia PAIRS (6 coppie) + TSM01** (+ confluenza MTF come variante
low-DD di MR01, + tilt stagionale mercoledì marginale). I 6 pairs sono quasi scorrelati col
MASTER (corr 0.02-0.08). MASTER + 6 pairs:
| Portafoglio | CAGR | DD% | Sharpe | OOS DD% | OOS Sharpe |
|---|--:|--:|--:|--:|--:|
| MASTER-9 (base) | 47 | 5.2 | 4.23 | 4.7 | 4.33 |
| **MASTER + 6 pairs (15)** | **71** | 5.7 | **5.93** | **2.3** | **7.71** |
| MASTER esteso +TSM01 (16) | 67 | 5.4 | 5.95 | **2.2** | 7.67 |
Aggiungere i 6 pairs porta l'**OOS DD a 2.2-2.3%** (da 4.7%) con Sharpe OOS ~7.7 e tutti
gli anni positivi: il guadagno di diversificazione da fonti market-neutral scorrelate.
## Quarto giro — validazione anti-overfitting e irrobustimento
Tre audit scettici paralleli (walk-forward, plateau, stress, scomposizione):
**Pairs — de-overfittati.** Sostituita la config per-coppia (cherry-picking di z_exit/n)
con **una config universale `n=50 z_in=2.0 z_exit=0.75 max_bars=72`**. Verifiche:
- plateau (non picco): heatmap n×z_in → 20/20 celle Sharpe>1 su ETH/BTC e BTC/LTC;
- walk-forward (train 2y / test 6m rolling): ETH/BTC 11/12 finestre positive, BTC/LTC
9/10 → edge distribuito su tutta la storia, non un regime singolo;
- **BNB/ETH scartata** (era robusta solo coi suoi parametri → overfit; crolla con la
universale e muore per prima allo stress costi). Famiglia ridotta a **5 pairs**.
- stress: 5/6 reggono fee+slippage realistici; solo ETH/BTC regge fee 6x (coda fee-fragile).
**Master — numeri sobri.** L'OOS Sharpe 7.7 / DD 2.3% è **ottimistico ~50%** perché l'OOS
cade nel bull calmo 2024-25. Numeri onesti da usare:
- worst-DD su finestra mobile 90g (2021-2026) = **5.7%** (bear FTX) → budget DD ~6%, non 2.3%;
- Sharpe per-semestre: mediana **~5** (min 1.2, max 12) → atteso ~5, non 7.7;
- ogni anno e ogni semestre dal 2021 positivo (anche il 2022 bear, grazie alle gambe short);
- equal-weight ≈ inverse-vol (non dipende da pesi fortunati);
- regge **leva 2x + slippage doppio** (CAGR 36%, Sharpe 5.1);
- **rischio concentrato: i pairs portano ~57% del rischio** → cap consigliato ~30-35%.
- Config robusta raccomandata: **MASTER-esteso, equal-weight, leva 2x, cap pairs ~30-35%**.
**TSM01 — confermato robusto** (36/36 config OOS+, walk-forward stabile) ma corr reale con
ROT02 = **0.62** (non 0.53), e gran parte del DD basso viene dall'overlay risk-off condiviso.
Tenuto come diversificatore con **gross 0.30** (stesso Sharpe, DD 22%→15%).
**Confluenza multi-TF — SCARTATA: era overfit.** Taglia il 97% dei trade (restano ~40 in
8 anni = non significativo), distrugge lo Sharpe (1.58→0.27 su BTC) e il caso "bello" non
sopravvive alle perturbazioni. Per abbassare il DD di MR01 meglio ridurne la leva, non il filtro 4h.
**Risultato del giro:** quanto trovato regge l'esame anti-overfit (NON è l'errore squeeze),
ma i numeri vanno comunicati sobri (Sharpe ~5, DD ~6%) e con leva 2x + cap pairs. Famiglia
pairs consolidata a 5 coppie con config universale; confluenza MTF rimossa dai vincitori.
## File creati (branch strategy_explore)
`scripts/analysis/explore_lab.py` (harness onesto condiviso), `pairs_research.py`
(verifica + ricerca pairs), `tsmom_research.py` (TSM01), `combine_v2.py` (master
esteso); `scripts/strategies/PR01_pairs_reversion.py` (artefatto pairs).
+15
View File
@@ -47,3 +47,18 @@ favorevole pesa. Il target €50/giorno resta vincolato dal capitale: 43% CAGR s
Prossimo passo: confermare il portafoglio combinato nel paper trader live. Prossimo passo: confermare il portafoglio combinato nel paper trader live.
**File:** `scripts/analysis/combine_portfolio.py` (nuovo). **File:** `scripts/analysis/combine_portfolio.py` (nuovo).
## Pulizia roster + miglioria ROT02
- **Waste delle peggiori:** MR03 Keltner (fade più debole, Sharpe 1.22, ridondante
con MR01 — rimuoverla *migliora* il portafoglio fade: DD 8.6→8.2, ret +549→+666)
e ROT01 (dominata da ROT02). Spostate in `scripts/waste/`.
- **Portafogli pronti:** `PORT02_fade_master` (6 sleeve fade) e `PORT03_all_master`
(9 sleeve fade+honest, varianti equal/5050).
- **ROT02 DD alto → migliorato:** la rotazione concentrava il book su 2 asset
(DD 40%). Sweep su `rot_improved`: `top_k=3` dimezza quasi il DD (40%→26%) e
*alza* il ritorno full (+1095→+1303%, ret/DD 27→50). Il vol-target abbassa il DD
ma sacrifica ritorno (de-leverage) → tenuto top_k=3 senza VT. Caveat onesto:
l'OOS di ROT02 cala un po' (+98→+68%, DD 12→14%), ma il MASTER (config deployata)
migliora lo Sharpe full 3.95→4.23. Applicato a `ROT02_dual_momentum.py` e
`_rot_daily_equity`. Sweep in `honest_improve.rot_improved`.
+87
View File
@@ -0,0 +1,87 @@
"""Combina i NUOVI edge (pairs + TSM01) col MASTER esistente: migliora il portafoglio?
Aggiunge al MASTER a 9 sleeve (6 fade + 3 honest) due nuove fonti scoperte
nell'esplorazione, poco correlate:
- PAIRS market-neutral (ETH/BTC, LTC/ETH, ADA/ETH) -> corr ~0 col mercato
- TSM01 (TSMOM multi-orizzonte + risk-off) -> corr ~0.53 con ROT02
Misura correlazione delle nuove sleeve vs esistenti e confronta MASTER-9 vs
MASTER-esteso su Ret/CAGR/DD/Sharpe, FULL e OOS (finestra comune 2021-2026).
"""
from __future__ import annotations
import sys
from pathlib import Path
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.combine_portfolio import (
build_all_sleeves, port_returns, metrics, yearly_returns, SPLIT, OOS_DATE, IDX,
)
from scripts.analysis.honest_improve2 import _daily_equity, _norm
from scripts.analysis.pairs_research import pairs_sim
from scripts.analysis.tsmom_research import tsmom_sim
def daily_from(eq_ts, eq_v):
return _norm(_daily_equity(eq_ts, eq_v, IDX))
def main():
print("Costruzione equity (puo' richiedere ~1-2 min)...\n")
S = build_all_sleeves() # 9 sleeve esistenti
# nuove sleeve: i 6 pairs robusti di PR01 + TSM01
from scripts.strategies.PR01_pairs_reversion import PAIRS
new = {}
for a, b, p in PAIRS:
r = pairs_sim(a, b, **p)
new[f"PR_{a}{b}"] = daily_from(r["eq_ts"], r["eq_v"])
t = tsmom_sim()
new["TSM01"] = daily_from(t["eq_ts"], t["eq_v"])
allS = {**S, **new}
# --- correlazione nuove vs esistenti ---
dr = pd.DataFrame({k: v.pct_change().fillna(0.0) for k, v in allS.items()})
corr = dr.corr()
old_k = list(S); new_k = list(new)
print("=" * 88)
print(" CORRELAZIONE rendimenti giornalieri — NUOVE (righe) vs media esistenti")
print("=" * 88)
for nk in new_k:
avg = corr.loc[nk, old_k].mean()
mx = corr.loc[nk, old_k].abs().max()
print(f" {nk:<12s} corr media col MASTER-9 = {avg:+.2f} |max| = {mx:.2f}")
# --- confronto portafogli ---
def line(label, members):
pr = port_returns(members)
f, o = metrics(pr), metrics(pr, lo=SPLIT)
print(f" {label:<26s}{f['ret']:>+9.0f}{f['cagr']:>7.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}"
f" | {o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}")
return pr
print("\n" + "=" * 96)
print(f" MASTER-9 vs MASTER-ESTESO (con pairs+TSM01) | OOS da {OOS_DATE} | equal-weight daily")
print("=" * 96)
print(f" {'portafoglio':<26s}{'Ret%':>9s}{'CAGR':>7s}{'DD%':>7s}{'Shrp':>7s}"
f" | {'oRet%':>9s}{'oDD%':>7s}{'oShrp':>7s}")
print(" " + "-" * 92)
pairs_only = {k: v for k, v in new.items() if k.startswith('PR_')}
line(f"MASTER-9 (base)", S)
line(f"MASTER +pairs ({len(S)+len(pairs_only)})", {**S, **pairs_only})
line(f"MASTER +TSM01 ({len(S)+1})", {**S, "TSM01": new["TSM01"]})
pr_all = line(f"MASTER-esteso ({len(allS)})", allS)
print(" " + "-" * 92)
pa = yearly_returns(pr_all)
print(" MASTER-esteso per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in pa.items()))
print("\n Se il MASTER-esteso ha DD piu' basso e/o Sharpe piu' alto del MASTER-9, le nuove")
print(" famiglie aggiungono valore (diversificazione da fonti scorrelate).")
if __name__ == "__main__":
main()
+171
View File
@@ -0,0 +1,171 @@
"""Harness ONESTO condiviso per esplorare nuove famiglie di strategie.
Regole NON negoziabili (per non ripetere l'errore squeeze look-ahead):
- direzione e prezzo decisi con dati FINO a close[i] incluso, mai con la barra i
usata per scegliere la direzione e poi entrare a i-1;
- ingresso ESEGUIBILE a close[i];
- exit: take-profit / stop-loss intrabar (high/low) e/o time-limit max_bars;
tp/sl possono essere None -> exit solo a tempo (utile per stagionalita');
- una posizione per volta (non-overlap), capitale composto;
- NETTO dopo fee round-trip (default 0.10% RT reale Deribit) e leva;
- validazione OOS (held-out, ultimo 30%) + sweep fee 0.00-0.20% RT.
Le strategie ad alta frequenza muoiono di fee: ogni entry costa fee_rt*lev sul
notional. Tienine conto: meno operazioni e edge > costi.
Asset disponibili: ADA BNB BTC DOGE ETH LTC SOL XRP (1h, 15m; BTC/ETH anche 5m).
"""
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 src.data.downloader import load_data
FEE_RT = 0.001 # Deribit perp realistico: taker ~0.05%/lato = 0.10% RT
LEV = 3.0
POS = 0.15
OOS_FRAC = 0.30
ASSETS = ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"]
BARS_PER_YEAR = {"5m": 105120, "15m": 35040, "1h": 8760, "4h": 2190, "1d": 365}
# --------------------------- dati ---------------------------
def get_df(asset: str, tf: str) -> pd.DataFrame:
"""OHLCV con colonna dt (UTC). tf nativo (5m,15m,1h) o resample da 1h (4h,1d).
timestamp resta ms-epoch reale anche dopo il resample (no placeholder)."""
if tf in ("5m", "15m", "1h"):
df = load_data(asset, tf).reset_index(drop=True)
else:
base = load_data(asset, "1h").copy()
base["dt"] = pd.to_datetime(base["timestamp"], unit="ms", utc=True)
base = base.set_index("dt")
rule = {"4h": "4h", "1d": "1D"}[tf]
agg = base.resample(rule).agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
).dropna()
epoch = pd.Timestamp("1970-01-01", tz="UTC") # ms-epoch portabile (qualsiasi risoluzione)
agg["timestamp"] = ((agg.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
df = agg.reset_index(drop=True)
df["dt"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
return df
def _dt(df: pd.DataFrame) -> pd.DatetimeIndex:
return pd.to_datetime(df["timestamp"], unit="ms", utc=True)
# --------------------------- indicatori ---------------------------
def atr(df: pd.DataFrame, n: int = 14) -> np.ndarray:
h, l, c = df["high"].values, df["low"].values, df["close"].values
pc = np.roll(c, 1); pc[0] = c[0]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
return pd.Series(tr).rolling(n).mean().values
def ema(x: np.ndarray, n: int) -> np.ndarray:
return pd.Series(x).ewm(span=n, adjust=False).mean().values
def rsi(close: np.ndarray, n: int = 14) -> np.ndarray:
d = np.diff(close, prepend=close[0])
up = pd.Series(np.where(d > 0, d, 0.0)).ewm(alpha=1/n, adjust=False).mean()
dn = pd.Series(np.where(d < 0, -d, 0.0)).ewm(alpha=1/n, adjust=False).mean()
rs = up / dn.replace(0, np.nan)
return (100 - 100 / (1 + rs)).values
# --------------------------- engine ---------------------------
def simulate(entries: list[dict], df: pd.DataFrame, fee_rt: float = FEE_RT,
lev: float = LEV, pos: float = POS, split: int = -1) -> dict:
"""entries: dict con i(idx), d(+1/-1), max_bars; tp/sl opzionali (None=solo tempo).
split: se >0, conta solo entries con i>=split (finestra OOS)."""
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c)
ts = _dt(df)
cap = peak = 1000.0
max_dd = 0.0
fee = fee_rt * lev
trades = wins = 0
last_exit = -1
bars_in = 0
yearly: dict[int, float] = {}
rets: list[float] = []
for e in entries:
i, d = e["i"], e["d"]
if i <= last_exit or i + 1 >= n or i < split:
continue
entry = c[i]
tp, sl, mb = e.get("tp"), e.get("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
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
if hit_sl:
exit_p = sl; break
if hit_tp:
exit_p = tp; break
if k == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * lev - fee
cb = cap
cap = max(cb + cb * pos * ret, 10.0)
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
trades += 1; wins += ret > 0; bars_in += (j - i)
last_exit = j
rets.append(ret * pos)
yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(len(rets))) if len(rets) > 1 and np.std(rets) > 0 else 0.0
return {
"trades": trades,
"win": wins / trades * 100 if trades else 0.0,
"ret": (cap / 1000 - 1) * 100,
"dd": max_dd * 100,
"sharpe": sharpe,
"yearly": yearly,
"exposure": bars_in / n * 100 if n else 0.0,
}
def evaluate(name: str, entries: list[dict], df: pd.DataFrame,
fees=(0.0, 0.0005, 0.001, 0.002)) -> dict:
"""Valuta una lista di entries: FULL, OOS e sweep fee. Stampa una riga sintetica."""
split = int(len(df) * (1 - OOS_FRAC))
full = simulate(entries, df)
oos = simulate(entries, df, split=split)
sweep = {f: simulate(entries, df, fee_rt=f)["ret"] for f in fees}
sweep_oos = {f: simulate(entries, df, fee_rt=f, split=split)["ret"] for f in fees}
yrs = full["yearly"]; pos_yrs = sum(1 for v in yrs.values() if v > 0)
print(f" {name:<24s} trd={full['trades']:>5d} win={full['win']:>4.1f}% "
f"FULL={full['ret']:>+7.0f}% OOS={oos['ret']:>+7.0f}% DD={full['dd']:>4.0f}% "
f"oDD={oos['dd']:>4.0f}% Shrp={full['sharpe']:>4.2f} exp={full['exposure']:>4.1f}% "
f"anniPos={pos_yrs}/{len(yrs)} | fee0.2%: FULL={sweep[0.002]:>+6.0f} OOS={sweep_oos[0.002]:>+6.0f}")
return {"full": full, "oos": oos, "sweep": sweep, "sweep_oos": sweep_oos, "pos_yrs": pos_yrs, "n_yrs": len(yrs)}
def robust(res: dict) -> bool:
"""Verdetto onesto: positivo FULL e OOS, regge a fee 0.20% RT, quasi tutti gli anni positivi."""
return (res["full"]["ret"] > 0 and res["oos"]["ret"] > 0
and res["sweep"][0.002] > 0 and res["sweep_oos"][0.002] > 0
and res["pos_yrs"] >= max(res["n_yrs"] - 1, 1))
if __name__ == "__main__":
# smoke test: una stagionalita' banale (hour-of-day) su BTC 1h
df = get_df("BTC", "1h"); ts = _dt(df)
ents = [{"i": i, "d": 1, "max_bars": 6, "tp": None, "sl": None}
for i in range(len(df) - 7) if ts.iloc[i].hour == 0]
print("smoke test — BTC long ad ogni 00:00 UTC, hold 6h:")
evaluate("seasonality_h0", ents, df)
+1 -1
View File
@@ -170,7 +170,7 @@ def _rot_daily_equity(idx):
for i in range(101, T - 1): for i in range(101, T - 1):
risk_on = btc[i] > bma[i] if not np.isnan(bma[i]) else False risk_on = btc[i] > bma[i] if not np.isnan(bma[i]) else False
mom = P[i] / P[i - 60] - 1; order = np.argsort(mom)[::-1] mom = P[i] / P[i - 60] - 1; order = np.argsort(mom)[::-1]
chosen = [j for j in order if mom[j] > 0][:2] if risk_on else [] chosen = [j for j in order if mom[j] > 0][:3] if risk_on else [] # top_k=3 (era 2): DD piu' basso
nw = np.zeros(N) nw = np.zeros(N)
for j in chosen: for j in chosen:
nw[j] = 0.45 / len(chosen) nw[j] = 0.45 / len(chosen)
+87
View File
@@ -0,0 +1,87 @@
"""Smoke test REALE dei pairs: fetch live da Cerbero + un tick vero per coppia.
A differenza di validate_worker_pairs.py (replay su parquet storici), questo verifica
la PIPELINE LIVE end-to-end: chiama Cerbero per entrambe le gambe, controlla che lo
strumento esista e sia fresco, fa un tick reale del PairsWorker e riporta lo stato.
Serve a scoprire i problemi che il backtest nasconde (es. un perp alt non disponibile
sull'endpoint Deribit). NON apre ordini reali: e' solo paper/lettura.
"""
from __future__ import annotations
import sys
import shutil
import tempfile
from datetime import datetime, timezone, timedelta
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.live.cerbero_client import CerberoClient
from src.live.multi_runner import INSTRUMENT_MAP
from src.live.pairs_worker import PairsWorker
from scripts.strategies.PR01_pairs_reversion import PAIRS
def fetch(cli, asset, start, end):
inst = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL")
try:
cs = cli.get_historical(inst, start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d"), "60")
if not cs:
return inst, None, "VUOTO (strumento assente sull'endpoint)"
df = pd.DataFrame(cs)
df["timestamp"] = df["timestamp"].astype("int64")
df = df.sort_values("timestamp").reset_index(drop=True)
last = pd.to_datetime(df["timestamp"].iloc[-1], unit="ms", utc=True)
age = (datetime.now(timezone.utc) - last).total_seconds() / 3600
return inst, df, f"{len(df)} barre, ultima {last:%Y-%m-%d %H:%M} ({age:.1f}h fa)"
except Exception as e:
return inst, None, f"ERRORE {type(e).__name__}: {str(e)[:60]}"
def main():
cli = CerberoClient()
end = datetime.now(timezone.utc)
start = end - timedelta(days=60)
assets = sorted({a for a, _, _ in PAIRS} | {b for _, b, _ in PAIRS})
print("=" * 80)
print(" SMOKE TEST LIVE PAIRS — fetch reale Cerbero + tick (no ordini reali)")
print("=" * 80)
data = {}
for a in assets:
inst, df, msg = fetch(cli, a, start, end)
data[a] = df
print(f" {a:<4s} [{inst:<16s}] {msg}")
print("\n tick reale per coppia:")
tmp = Path(tempfile.mkdtemp())
try:
for a, b, p in PAIRS:
if data.get(a) is None or data.get(b) is None:
print(f" {a}/{b:<4s}: SKIP (manca feed live di una gamba) -> non tradabile live ora")
continue
w = PairsWorker(a, b, "1h", params=p, fee_rt=0.001, data_dir=tmp)
w._log = lambda *x, **k: None
w._notify = lambda *x, **k: None
m = data[a][["timestamp", "close"]].merge(
data[b][["timestamp", "close"]], on="timestamp", how="inner")
if len(m) < p["n"] + 2:
print(f" {a}/{b:<4s}: merge {len(m)} barre < n+2 ({p['n']+2}) -> dati insufficienti")
continue
z, _ = w._zscore(m["close_x"].values, m["close_y"].values)
w.tick(data[a], data[b])
state = ("IN POS " + ("LONG " + a if w.direction == 1 else "SHORT " + a)
if w.in_position else "FLAT")
print(f" {a}/{b:<4s}: OK merge {len(m)} barre, z_ora={z[-1]:+.2f} -> {state}")
finally:
shutil.rmtree(tmp, ignore_errors=True)
print("\n Solo le coppie con entrambe le gambe fresche su Cerbero sono tradabili live.")
if __name__ == "__main__":
main()
+133
View File
@@ -0,0 +1,133 @@
"""Verifica indipendente + ricerca PAIRS / SPREAD MEAN-REVERSION fra cripto.
Famiglia nuova market-neutral (distinta da tutto l'esistente, single-asset).
Idea: il log-ratio di due cripto oscilla attorno alla media; z-score estremo -> rientra.
Engine ONESTO (no look-ahead, verificato):
- r[i] = log(closeA[i]/closeB[i]); ma/sd = rolling(n) su r -> usano solo r[<=i].
- z[i] = (r[i]-ma[i])/sd[i]. ENTRY a close[i] (eseguibile):
z<=-z_in -> LONG ratio (long A / short B); z>=+z_in -> SHORT ratio.
- EXIT quando |z[j]| <= z_exit (rientro) o time-limit max_bars, a close[j].
- pairs = 2 GAMBE -> fee = 2*fee_rt*lev (0.20% RT/coppia a fee_rt=0.001), il doppio
del single-asset. Rendimento neutral = retA*d - retB*d (notional uguale per gamba).
- non-overlap, capitale composto. Filtro candele sporche: salta salti |dr|>jump_max.
- Ritorno riportato come CAGR e Sharpe ANNUALIZZATO sul tempo reale (no sqrt(n_trade)).
"""
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 src.data.downloader import load_data
FEE_RT, LEV, POS, OOS_FRAC = 0.001, 3.0, 0.15, 0.30
BARS_YEAR = 8760 # 1h
def aligned(a: str, b: str, tf: str = "1h"):
da = load_data(a, tf)[["timestamp", "open", "high", "low", "close"]].rename(columns=lambda x: x + "_a" if x != "timestamp" else x)
db = load_data(b, tf)[["timestamp", "close"]].rename(columns={"close": "close_b"})
m = da.merge(db, on="timestamp", how="inner").reset_index(drop=True)
m["dt"] = pd.to_datetime(m["timestamp"], unit="ms", utc=True)
return m
def pairs_sim(a, b, tf="1h", n=50, z_in=2.0, z_exit=0.5, max_bars=72,
jump_max=0.08, fee_rt=FEE_RT, lev=LEV, pos=POS, split_frac=0.0):
m = aligned(a, b, tf)
ca, cb = m["close_a"].values, m["close_b"].values
r = np.log(ca / cb)
dr = np.abs(np.diff(r, prepend=r[0])) # salto 1-bar del log-ratio
ma = pd.Series(r).rolling(n).mean().values
sd = pd.Series(r).rolling(n).std().values
z = (r - ma) / np.where(sd == 0, np.nan, sd) # causale: usa r[<=i]
ts = m["dt"]; N = len(r)
split = int(N * split_frac)
fee = 2 * fee_rt * lev # 2 gambe
cap = peak = 1000.0; dd = 0.0; last = -1
trades = wins = 0; rets = []; yearly = {}
eq_ts: list = []; eq_v: list = []
for i in range(n + 1, N - 1):
if i < split or np.isnan(z[i]) or dr[i] > jump_max:
continue
if i <= last:
continue
if z[i] <= -z_in:
d = 1
elif z[i] >= z_in:
d = -1
else:
continue
# exit: |z|<=z_exit o max_bars
j = min(i + max_bars, N - 1)
for k in range(1, max_bars + 1):
jj = i + k
if jj >= N:
j = N - 1; break
if abs(z[jj]) <= z_exit:
j = jj; break
j = jj
retA = (ca[j] - ca[i]) / ca[i]
retB = (cb[j] - cb[i]) / cb[i]
ret = (retA - retB) * d * lev - fee # long A / short B (o viceversa)
cap = max(cap + cap * pos * ret, 10.0)
peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
trades += 1; wins += ret > 0; rets.append(ret * pos); last = j
eq_ts.append(ts.iloc[j]); eq_v.append(cap)
yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
yrs_span = (ts.iloc[-1] - ts.iloc[max(split, 0)]).days / 365.25 or 1
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(BARS_YEAR / np.mean([max_bars])) ) if len(rets) > 1 and np.std(rets) > 0 else 0.0
# Sharpe annualizzato sul tempo reale: usa rendimenti per-trade scalati alla frequenza media
if len(rets) > 1 and np.std(rets) > 0:
trades_per_year = trades / yrs_span
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(trades_per_year))
ret_tot = (cap / 1000 - 1) * 100
cagr = ((cap / 1000) ** (1 / yrs_span) - 1) * 100 if cap > 0 else -100
return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=ret_tot, cagr=cagr,
dd=dd * 100, sharpe=sharpe, yearly=yearly, eq_ts=eq_ts, eq_v=eq_v)
def check_no_lookahead():
"""Perturba il FUTURO del ratio e verifica che z[i] non cambi (causalita')."""
m = aligned("ETH", "BTC")
r = np.log(m["close_a"].values / m["close_b"].values)
n = 50; i = 1000
z_i = (r[i] - pd.Series(r).rolling(n).mean().values[i]) / pd.Series(r).rolling(n).std().values[i]
r2 = r.copy(); r2[i + 1:] += 0.5 # stravolge il futuro
z_i2 = (r2[i] - pd.Series(r2).rolling(n).mean().values[i]) / pd.Series(r2).rolling(n).std().values[i]
print(f" no-look-ahead: z[i]={z_i:.6f} vs z[i] con futuro perturbato={z_i2:.6f} -> "
f"{'OK (identico)' if abs(z_i - z_i2) < 1e-9 else 'VIOLAZIONE!'}")
def main():
print("=" * 104)
print(f" PAIRS spread reversion — NETTO fee 0.20% RT/coppia (2 gambe), leva {LEV:.0f}x, OOS ultimo {int(OOS_FRAC*100)}%")
print("=" * 104)
check_no_lookahead()
pairs = [("ETH", "BTC"), ("LTC", "ETH"), ("ADA", "ETH"), ("SOL", "ETH"),
("BNB", "BTC"), ("XRP", "BTC"), ("SOL", "BTC"), ("DOGE", "BTC")]
print(f"\n {'coppia':<10s}{'trd':>5s}{'win%':>6s}{'FULL%':>8s}{'OOS%':>8s}{'CAGR%':>7s}"
f"{'DD%':>6s}{'oDD%':>6s}{'Shrp':>6s}{'anni+':>7s}{'fee0.4%RT':>11s}")
print(" " + "-" * 96)
for a, b in pairs:
f = pairs_sim(a, b, n=50, z_in=2.0, z_exit=0.5, max_bars=72)
o = pairs_sim(a, b, n=50, z_in=2.0, z_exit=0.5, max_bars=72, split_frac=1 - OOS_FRAC)
hi = pairs_sim(a, b, n=50, z_in=2.0, z_exit=0.5, max_bars=72, fee_rt=0.002) # 0.4% RT/coppia
yrs = f["yearly"]; pos_y = sum(1 for v in yrs.values() if v > 0)
print(f" {a+'/'+b:<10s}{f['trades']:>5d}{f['win']:>6.1f}{f['ret']:>+8.0f}{o['ret']:>+8.0f}"
f"{f['cagr']:>7.0f}{f['dd']:>6.0f}{o['dd']:>6.0f}{f['sharpe']:>6.2f}{f'{pos_y}/{len(yrs)}':>7s}"
f"{hi['ret']:>+11.0f}")
# correlazione con BTC daily (market-neutrality) sulla coppia migliore
print("\n Verifica market-neutrality ETH/BTC: per-anno")
f = pairs_sim("ETH", "BTC", n=50, z_in=2.0, z_exit=0.5, max_bars=72)
print(" " + " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(f["yearly"].items())))
if __name__ == "__main__":
main()
+136
View File
@@ -0,0 +1,136 @@
"""Report aggiornato: risultati per anno + numero trade per anno, tutte le strategie.
Sezioni:
(A) RET% NETTO per anno — ogni strategia singola + i portafogli (FADE / HONEST /
MASTER equal / MASTER 50-50). Ret% dai rendimenti giornalieri composti.
(B) NUMERO TRADE per anno — per ogni strategia singola. Per le fade e DIP01 è il
numero di ingressi; per TR01 e ROT02 (posizione continua) è il numero di
ribilanciamenti/cambi di stato nell'anno.
(C) RIEPILOGO — TOT%, CAGR, DD, Sharpe (FULL e OOS) dei portafogli.
Tutto NETTO fee 0.10% RT, leva 3x, pos 15%/sleeve. Finestra comune 2021-2026,
OOS = ultimo 30%. Config = quella deployata (MR03/ROT01 in waste; ROT02 top_k=3).
"""
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 src.data.downloader import load_data
from scripts.analysis.combine_portfolio import (
build_all_sleeves, port_returns, yearly_returns, metrics, SPLIT, OOS_DATE, IDX,
)
from scripts.analysis.risk_management import strats_for, build_trades
from scripts.analysis.honest_lab import get_df, ema, FEE_RT, LEV, POS
from scripts.analysis.honest_improve import rot_improved
from scripts.analysis.honest_improve2 import dip_market_gated
YEARS = sorted(set(IDX.year))
# ---------------- trade per anno, per tipo di strategia ----------------
def fade_trades_year(asset, fn, params) -> dict[int, int]:
df = load_data(asset, "1h")
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
out: dict[int, int] = {}
for i, j, ret in build_trades(fn(df, **params), df, trend_max=3.0):
y = ts.iloc[i].year
out[y] = out.get(y, 0) + 1
return out
def dip_trades_year() -> dict[int, int]:
d = dip_market_gated("BTC", market_n=0)
# yt[anno] = lista dei trade dell'anno -> il conteggio e' la lunghezza
return {int(y): (len(v) if isinstance(v, (list, tuple)) else int(v)) for y, v in d["yt"].items()}
def tr_rebalances_year(assets) -> dict[int, int]:
"""Cambi di stato (entra/esce dal trend) per anno, sommati sul paniere TR01."""
out: dict[int, int] = {}
for a in assets:
df = get_df(a, "4h"); c = df["close"].values
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
ef, es = ema(c, 20), ema(c, 100)
sig = np.where(ef > es, 1.0, 0.0); sig[:100] = 0.0
for i in range(1, len(c)):
if sig[i] != sig[i - 1]:
y = ts.iloc[i].year
out[y] = out.get(y, 0) + 1
return out
def rot_rebalances_year() -> dict[int, int]:
r = rot_improved(lookback=60, top_k=3, regime_n=100)
return {int(y): int(n) for y, n in r["reb"].items()}
def main():
print("Costruzione equity e conteggi (puo' richiedere ~1 min)...\n")
S = build_all_sleeves()
fade = {k: v for k, v in S.items() if k.startswith("MR")}
honest = {k: v for k, v in S.items() if not k.startswith("MR")}
# rendimenti giornalieri per Ret%/anno
sleeve_ret = {k: v.pct_change().fillna(0.0) for k, v in S.items()}
ports = {
"FADE": port_returns(fade),
"HONEST": port_returns(honest),
"MASTEReq": port_returns(S),
"MAST5050": (port_returns(fade) + port_returns(honest)) / 2,
}
# ---- (A) RET% per anno ----
cols_A = list(S) + list(ports)
rety = {**{k: yearly_returns(v) for k, v in sleeve_ret.items()},
**{k: yearly_returns(v) for k, v in ports.items()}}
print("=" * 132)
print(" (A) RET% NETTO PER ANNO — strategie singole e portafogli | leva 3x pos 15% fee 0.10% RT")
print("=" * 132)
print(f" {'Anno':>5s}" + "".join(f"{c.replace('_',''):>11s}" for c in cols_A))
print(" " + "-" * 126)
for y in YEARS:
print(f" {y:>5d}" + "".join(f"{rety[c].get(y, 0):>+11.0f}" for c in cols_A))
# ---- (B) NUMERO TRADE per anno ----
tcounts = {}
for asset in ["BTC", "ETH"]:
for nm, (fn, params) in strats_for(asset).items():
tcounts[f"{nm}_{asset}"] = fade_trades_year(asset, fn, params)
tcounts["DIP01_BTC"] = dip_trades_year()
tcounts["TR01_basket*"] = tr_rebalances_year(["BNB", "BTC", "DOGE", "SOL", "XRP"])
tcounts["ROT02_rot*"] = rot_rebalances_year()
cols_B = list(tcounts)
print("\n" + "=" * 132)
print(" (B) NUMERO TRADE PER ANNO — fade/DIP01 = ingressi; TR01/ROT02 (*) = ribilanciamenti")
print("=" * 132)
print(f" {'Anno':>5s}" + "".join(f"{c.replace('_',''):>13s}" for c in cols_B))
print(" " + "-" * 126)
for y in YEARS:
print(f" {y:>5d}" + "".join(f"{tcounts[c].get(y, 0):>13d}" for c in cols_B))
print(" " + "-" * 126)
print(f" {'TOT':>5s}" + "".join(f"{sum(tcounts[c].values()):>13d}" for c in cols_B))
# ---- (C) riepilogo portafogli ----
print("\n" + "=" * 92)
print(f" (C) RIEPILOGO PORTAFOGLI | OOS da {OOS_DATE}")
print("=" * 92)
print(f" {'portafoglio':<14s}{'Ret%':>9s}{'CAGR':>7s}{'DD%':>7s}{'Shrp':>7s}"
f" | {'oRet%':>9s}{'oDD%':>7s}{'oShrp':>7s}")
print(" " + "-" * 74)
for name, pr in ports.items():
f, o = metrics(pr), metrics(pr, lo=SPLIT)
print(f" {name:<14s}{f['ret']:>+9.0f}{f['cagr']:>7.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}"
f" | {o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}")
print("\n MASTEReq (9 sleeve) = configurazione consigliata. (*) TR01/ROT02 = posizione")
print(" continua: il conteggio e' il numero di ribilanciamenti/cambi di stato, non di trade discreti.")
if __name__ == "__main__":
main()
+136
View File
@@ -0,0 +1,136 @@
"""Report riassuntivo: tutte le strategie/famiglie per anno + analisi di integrazione.
Consolida in un solo posto:
(A) RET% NETTO per anno per FAMIGLIA (FADE / HONEST / PAIRS / TSM01) e per i portafogli.
(B) RET% NETTO per anno per ogni STRATEGIA singola (tutti gli sleeve).
(C) INTEGRAZIONE: cosa succede al MASTER aggiungendo le nuove famiglie (pairs, TSM01).
(D) Numeri SOBRI (worst-case) e raccomandazione operativa.
Famiglie:
FADE (reversione intraday 1h, long/short, BTC/ETH): MR01, MR02, MR07
HONEST (long-only multi-regime multi-crypto): DIP01, TR01, ROT02
PAIRS (market-neutral spread reversion, config universale): 5 coppie
TSM01 (TSMOM multi-orizzonte, diversificatore)
Tutto NETTO fee, leva 3x (vedi nota sobria leva 2x), finestra comune 2021-2026, OOS=ultimo 30%.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.combine_portfolio import (
build_all_sleeves, port_returns, metrics, yearly_returns, SPLIT, OOS_DATE, IDX,
)
from scripts.analysis.honest_improve2 import _daily_equity, _norm
from scripts.analysis.pairs_research import pairs_sim
from scripts.analysis.tsmom_research import tsmom_sim
from scripts.strategies.PR01_pairs_reversion import PAIRS
YEARS = sorted(set(IDX.year))
def daily_from(eq_ts, eq_v):
return _norm(_daily_equity(eq_ts, eq_v, IDX))
def build_everything():
S = build_all_sleeves() # 9 sleeve (FADE 6 + HONEST 3)
pairs = {}
for a, b, p in PAIRS:
r = pairs_sim(a, b, **p)
pairs[f"PR_{a}{b}"] = daily_from(r["eq_ts"], r["eq_v"])
t = tsmom_sim()
tsm = {"TSM01": daily_from(t["eq_ts"], t["eq_v"])}
return S, pairs, tsm
def yrow(label, dr):
yr = yearly_returns(dr)
return f" {label:<14s}" + "".join(f"{yr.get(y, 0):>+9.0f}" for y in YEARS)
def metric_block(label, dr):
f, o = metrics(dr), metrics(dr, lo=SPLIT)
return (f" {label:<16s}{f['ret']:>+9.0f}{f['cagr']:>7.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}"
f" | {o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}")
def main():
print("Costruzione (puo' richiedere ~1-2 min)...\n")
S, pairs, tsm = build_everything()
fade = {k: v for k, v in S.items() if k.startswith("MR")}
honest = {k: v for k, v in S.items() if not k.startswith("MR")}
fam = {
"FADE": port_returns(fade),
"HONEST": port_returns(honest),
"PAIRS": port_returns(pairs),
"TSM01": tsm["TSM01"].pct_change().fillna(0.0),
}
master9 = port_returns(S)
master_p = port_returns({**S, **pairs})
master_x = port_returns({**S, **pairs, **tsm})
# ---------- (A) per anno, per FAMIGLIA + portafogli ----------
print("=" * 110)
print(" (A) RET% NETTO PER ANNO — per FAMIGLIA e per PORTAFOGLIO | leva 3x, fee netta")
print("=" * 110)
print(f" {'':<14s}" + "".join(f"{y:>9d}" for y in YEARS))
print(" " + "-" * 104)
for k, dr in fam.items():
print(yrow(k, dr))
print(" " + "-" * 104)
print(yrow("MASTER-9", master9))
print(yrow("MASTER+pairs", master_p))
print(yrow("MASTER-esteso", master_x))
# ---------- (B) per anno, per STRATEGIA singola ----------
print("\n" + "=" * 130)
print(" (B) RET% NETTO PER ANNO — per STRATEGIA singola (tutti gli sleeve)")
print("=" * 130)
allsl = {**S, **pairs, **tsm}
cols = list(allsl)
print(f" {'Anno':>5s}" + "".join(f"{c.replace('_',''):>11s}" for c in cols))
print(" " + "-" * 124)
yr_each = {k: yearly_returns(v.pct_change().fillna(0.0)) for k, v in allsl.items()}
for y in YEARS:
print(f" {y:>5d}" + "".join(f"{yr_each[c].get(y, 0):>+11.0f}" for c in cols))
# ---------- (C) integrazione ----------
print("\n" + "=" * 96)
print(f" (C) INTEGRAZIONE delle nuove famiglie nel MASTER | OOS da {OOS_DATE} | equal-weight daily")
print("=" * 96)
print(f" {'portafoglio':<16s}{'Ret%':>9s}{'CAGR':>7s}{'DD%':>7s}{'Shrp':>7s}"
f" | {'oRet%':>9s}{'oDD%':>7s}{'oShrp':>7s}")
print(" " + "-" * 80)
print(metric_block("MASTER-9", master9))
print(metric_block("+pairs", master_p))
print(metric_block("+TSM01", port_returns({**S, **tsm})))
print(metric_block("MASTER-esteso", master_x))
# correlazione media nuove vs master-9
dr_all = pd.DataFrame({k: v.pct_change().fillna(0.0) for k, v in {**S, **pairs, **tsm}.items()})
corr = dr_all.corr(); old = list(S)
print(" " + "-" * 80)
for k in list(pairs) + list(tsm):
print(f" corr {k:<11s} vs MASTER-9 = {corr.loc[k, old].mean():+.2f}")
# ---------- (D) numeri sobri ----------
print("\n" + "=" * 96)
print(" (D) NUMERI SOBRI / RACCOMANDAZIONE (anti-overfit)")
print("=" * 96)
print(" - L'OOS singolo (2024-25) e' regime calmo -> Sharpe/DD OOS ottimistici ~50%.")
print(" - Numeri onesti del MASTER-esteso: worst-DD 90g ~6%, Sharpe atteso ~5, ogni anno positivo dal 2021.")
print(" - Regge leva 2x + slippage doppio (CAGR ~36%, Sharpe ~5).")
print(" - Rischio concentrato sui PAIRS (~57%) -> cap allocazione pairs ~30-35%.")
print(" - I pairs sono a 2 gambe (long/short): il worker live va esteso prima del trading reale.")
print(" - CONFIG RACCOMANDATA: MASTER-esteso, equal-weight, leva 2x, cap pairs 30-35%.")
if __name__ == "__main__":
main()
+94
View File
@@ -0,0 +1,94 @@
"""Verifica indipendente + ricerca TSM01 — Time-Series Momentum multi-orizzonte.
Long-only, multi-crypto, bassa frequenza. Per ogni asset il segnale è il CONSENSO
dei segni del momentum su più orizzonti lunghi (3/6/12 mesi); si tengono equal-weight
gli asset con consenso pieno positivo. Overlay risk-off: cash se BTC < SMA100.
Distinta da ROT02 (cross-sectional ranking): qui conta la PERSISTENZA assoluta lenta
di ogni asset, non la classifica relativa. Correlazione con ROT02 ~0.62 -> fattore
parzialmente indipendente, utile come diversificatore (NON come motore di ritorno:
rende meno di ROT02 a parita' di OOS). DD basso.
Anti-overfit: edge su ALTOPIANO (36/36 config orizzonti x thr x regime_n restano OOS+),
walk-forward stabile (4 anni up, 2 piatti per risk-off, mai un anno negativo), regge
fee 0.40% RT. Gran parte del DD basso viene dall'overlay risk-off SMA100 (condiviso),
la struttura multi-orizzonte aggiunge ~+38pp OOS e alza lo Sharpe 0.58->1.07.
Default gross=0.30 (era 0.45): stesso Sharpe ma DD 22%->15% (scelta robusta, non la piu' redditizia).
Engine onesto: pesi a close[i] da soli rendimenti passati, realizzo i->i+1, fee
one-way fee_rt/2 sul turnover. NETTO, leva implicita gross. OOS = ultimo 30%.
"""
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.honest_lab import available_assets, FEE_RT
from scripts.analysis.honest_rotation import build_panel
GROSS, OOS_FRAC = 0.30, 0.30 # gross 0.30 (anti-overfit): stesso Sharpe di 0.45, DD piu' basso
def tsmom_sim(horizons=(63, 126, 252), thr=1.0, regime_n=100, gross=GROSS,
fee_rt=FEE_RT, oos_frac=0.0, cheat=False):
"""horizons in giorni. thr=1.0 -> consenso pieno (tutti i segni positivi)."""
panel = build_panel(available_assets(), "1d")
cols = list(panel.columns); P = panel.values; T, N = P.shape
rets = np.zeros_like(P); rets[1:] = P[1:] / P[:-1] - 1
years = panel.index.year.values
btc = P[:, cols.index("BTC")]
bma = pd.Series(btc).rolling(regime_n).mean().values
start = max(max(horizons) + 1, regime_n + 1, int(T * (1 - oos_frac)) if oos_frac else 0)
cap = 1000.0; w = np.zeros(N); eq = [cap]; yearly = {}
eq_ts: list = []; eq_v: list = []
for i in range(start, T - 1):
risk_on = btc[i] > bma[i] if not np.isnan(bma[i]) else False
wi = i + 1 if cheat else i # cheat: usa il futuro (test no-look-ahead)
score = np.zeros(N)
for h in horizons:
score += np.sign(P[wi] / P[wi - h] - 1)
score /= len(horizons)
chosen = [j for j in range(N) if score[j] >= thr] if risk_on else []
nw = np.zeros(N)
for j in chosen:
nw[j] = gross / len(chosen)
cap -= cap * np.abs(nw - w).sum() * (fee_rt / 2); w = nw
cap = max(cap * (1 + float(np.dot(w, rets[i + 1]))), 10.0)
eq.append(cap)
eq_ts.append(panel.index[i + 1]); eq_v.append(cap)
y = int(years[i]); yearly[y] = yearly.get(y, 0.0) + float(np.dot(w, rets[i + 1])) * 100
eq = np.array(eq); peak = np.maximum.accumulate(eq)
dd = float(np.max((peak - eq) / peak) * 100)
yrs = (panel.index[-1] - panel.index[start]).days / 365.25 or 1
rets_d = np.diff(eq) / eq[:-1]
sharpe = float(np.mean(rets_d) / np.std(rets_d) * np.sqrt(365)) if np.std(rets_d) > 0 else 0.0
return dict(ret=(cap / 1000 - 1) * 100, cagr=((cap / 1000) ** (1 / yrs) - 1) * 100,
dd=dd, sharpe=sharpe, yearly=yearly, eq_ts=eq_ts, eq_v=eq_v,
pos_years=sum(1 for v in yearly.values() if v > 0), n_years=len(yearly))
def main():
print("=" * 90)
print(" TSM01 — TSMOM multi-orizzonte (3/6/12m consenso pieno) + risk-off SMA100")
print("=" * 90)
# no-look-ahead: cheat deve esplodere
base = tsmom_sim()
ch = tsmom_sim(cheat=True)
print(f" no-look-ahead: onesto FULL={base['ret']:+.0f}% vs cheat(futuro)={ch['ret']:+.0f}% -> "
f"{'OK (il cheat esplode -> niente leak)' if ch['ret'] > base['ret'] * 2 else 'CONTROLLARE'}")
o = tsmom_sim(oos_frac=1 - OOS_FRAC)
hi = tsmom_sim(fee_rt=0.002)
print(f"\n FULL {base['ret']:+.0f}% CAGR {base['cagr']:.0f}% DD {base['dd']:.0f}% "
f"Sharpe {base['sharpe']:.2f} anni+ {base['pos_years']}/{base['n_years']}")
print(f" OOS {o['ret']:+.0f}% DD {o['dd']:.0f}% | fee 0.40% RT: FULL {hi['ret']:+.0f}%")
print(" Per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(base["yearly"].items())))
if __name__ == "__main__":
main()
+74
View File
@@ -0,0 +1,74 @@
"""Valida il PairsWorker: replay bar-per-bar sui dati storici == backtest pairs_sim?
Come validate_worker_mr01 per MR01: alimenta il PairsWorker con finestre trailing
crescenti (simula il feed live) e confronta trade/capitale finale col backtest di
riferimento scripts/analysis/pairs_research.pairs_sim. Se combaciano, la semantica
live (z-score causale, exit |z|<=z_exit o max_bars, fee 2 gambe) e' fedele.
"""
from __future__ import annotations
import shutil
import sys
import tempfile
from pathlib import Path
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.pairs_worker import PairsWorker
from scripts.analysis.pairs_research import aligned, pairs_sim
from scripts.strategies.PR01_pairs_reversion import PAIRS
WINDOW = 60 # finestra trailing minima (>= n+2): z[i] corretto, replay veloce
def replay(a: str, b: str, params: dict, data_dir: Path) -> PairsWorker:
m = aligned(a, b)
df_a = m[["timestamp"]].copy(); df_a["close"] = m["close_a"].values
df_b = m[["timestamp"]].copy(); df_b["close"] = m["close_b"].values
w = PairsWorker(a, b, "1h", params=params, fee_rt=0.001, data_dir=data_dir)
# replay veloce: niente I/O su file / log / notifiche ad ogni tick (servono solo le metriche finali)
w._save_state = lambda: None
w._log = lambda *a, **k: None
w._notify = lambda *a, **k: None
n = w.n
for k in range(n + 2, len(m) + 1):
lo = max(0, k - WINDOW)
w.tick(df_a.iloc[lo:k], df_b.iloc[lo:k])
# chiudi eventuale posizione aperta a fine serie (come fa il backtest col troncamento)
return w
def main():
print("=" * 96)
print(" VALIDAZIONE PairsWorker — replay live vs backtest pairs_sim (fee 0.20% RT/coppia)")
print("=" * 96)
print(f" {'coppia':<10s}{'WORKER cap':>12s}{'trd':>5s}{'win%':>6s} | {'BACKTEST cap':>13s}{'trd':>5s}{'win%':>6s} match?")
print(" " + "-" * 88)
# Sottoinsieme rappresentativo: il codice del worker e' identico per ogni coppia,
# quindi 2 coppie con strutture diverse (alt/major e major/alt) bastano a provare
# l'equivalenza. ~135s/coppia su 73k barre orarie. Per validarle tutte: usa PAIRS.
subset = [pp for pp in PAIRS if (pp[0], pp[1]) in {("ETH", "BTC"), ("BTC", "LTC")}]
tmp = Path(tempfile.mkdtemp(prefix="pairs_validate_"))
try:
for a, b, p in subset:
w = replay(a, b, p, tmp)
bt = pairs_sim(a, b, **p)
bt_cap = 1000.0 * (1 + bt["ret"] / 100)
cap_match = abs(w.capital - bt_cap) / bt_cap < 0.02 if bt_cap else False
trd_match = abs(w.total_trades - bt["trades"]) <= max(2, bt["trades"] * 0.02)
ok = "OK" if (cap_match and trd_match) else "DIFF"
ww = w.total_wins / w.total_trades * 100 if w.total_trades else 0
print(f" {a+'/'+b:<10s}{w.capital:>12.0f}{w.total_trades:>5d}{ww:>6.1f} | "
f"{bt_cap:>13.0f}{bt['trades']:>5d}{bt['win']:>6.1f} {ok}")
finally:
shutil.rmtree(tmp, ignore_errors=True)
print(" " + "-" * 88)
print(" match = capitale entro 2% e trade entro 2% del backtest. Differenze minime sono")
print(" attese (gestione bar finale/troncamento), ma la semantica deve coincidere.")
if __name__ == "__main__":
main()
+4 -4
View File
@@ -9,12 +9,12 @@ La diversificazione e' il vero motore di risk-reduction: il DD del portafoglio
scende SOTTO quello della sleeve meno rischiosa, mantenendo una CAGR alta e scende SOTTO quello della sleeve meno rischiosa, mantenendo una CAGR alta e
azzerando quasi gli anni negativi (il 2022 bear passa da -30% di ROT a -1%). azzerando quasi gli anni negativi (il 2022 bear passa da -30% di ROT a -1%).
Risultato (netto, 2021-2026, leva 3x pos 15% per sleeve): Risultato (netto, 2021-2026, leva 3x pos 15% per sleeve; ROT02 ora top_k=3):
DIP01_BTC +322% DD 15% CAGR 31% DIP01_BTC +322% DD 15% CAGR 31%
TR01_basket +591% DD 27% CAGR 43% TR01_basket +591% DD 27% CAGR 43%
ROT02_dualmom +771% DD 40% CAGR 49% ROT02_dualmom +984% DD 26% CAGR 56% (top_k=3: DD 40->26, PnL su)
PORTAFOGLIO +642% DD 12% CAGR 45% <-- DD piu' basso di ogni sleeve PORTAFOGLIO +676% DD 13% CAGR 46% <-- DD piu' basso di ogni sleeve
Per-anno: 2021 +203 · 2022 -1 · 2023 +47 · 2024 +50 · 2025 +14 · 2026 -2 Per-anno: 2021 +224 · 2022 +1 · 2023 +48 · 2024 +48 · 2025 +10 · 2026 -2
Logica e ricostruzione: scripts/analysis/honest_improve2.py. Logica e ricostruzione: scripts/analysis/honest_improve2.py.
""" """
from __future__ import annotations from __future__ import annotations
+6 -5
View File
@@ -11,12 +11,13 @@ Combinare le due famiglie migliora il rischio/rendimento rispetto a ciascuna da
sola: il DD scende e lo Sharpe sale (la honest, da sola piu' lumpy, viene sola: il DD scende e lo Sharpe sale (la honest, da sola piu' lumpy, viene
stabilizzata dalle fade ad alta frequenza). Vedi scripts/analysis/combine_portfolio.py. stabilizzata dalle fade ad alta frequenza). Vedi scripts/analysis/combine_portfolio.py.
Risultato (netto, equal-weight daily, finestra comune 2021-2026, OOS = ultimo 30%): Risultato (netto, equal-weight daily, finestra comune 2021-2026, OOS = ultimo 30%;
ROT02 ora top_k=3 -> DD piu' basso):
FADE only (6) DD 8.2% Sharpe 3.95 (OOS DD 5.9 / Shrp 4.09) FADE only (6) DD 8.2% Sharpe 3.95 (OOS DD 5.9 / Shrp 4.09)
HONEST only (3) DD 12.0% Sharpe 1.90 (OOS DD 6.5 / Shrp 2.23) HONEST only (3) DD 12.6% Sharpe 2.20 (OOS DD 7.8 / Shrp 2.02)
MASTER eq (9) DD 5.2% Sharpe 3.95 (OOS DD 4.7 / Shrp 4.42) <- miglior Sharpe MASTER eq (9) DD 5.2% Sharpe 4.23 (OOS DD 4.7 / Shrp 4.33) <- miglior Sharpe
MASTER 50/50 DD 5.1% Sharpe 3.31 (OOS DD 4.3) <- miglior DD MASTER 50/50 DD 5.0% Sharpe 3.69 (OOS DD 4.5) <- miglior DD
CAGR ~46% mantenuta in entrambe le varianti combinate. CAGR ~47% mantenuta in entrambe le varianti combinate.
Due varianti operative selezionabili: Due varianti operative selezionabili:
weighting="equal" -> equal-weight sui 9 sleeve (massimo Sharpe) weighting="equal" -> equal-weight sui 9 sleeve (massimo Sharpe)
@@ -0,0 +1,83 @@
"""PR01 — Pairs / Spread Mean-Reversion fra cripto (market-neutral). FAMIGLIA NUOVA.
Distinta da tutto l'esistente (single-asset direzionale): scommette sul RIENTRO del
log-ratio di due cripto verso la sua media. Market-neutral (long A / short B) ->
correlazione ~0.02 col mercato -> diversificatore prezioso.
Logica (engine onesto verificato in scripts/analysis/pairs_research.py):
r[i] = log(closeA[i]/closeB[i]); z[i] = (r[i]-SMA_n(r)[i]) / STD_n(r)[i] (causale)
z <= -z_in -> LONG ratio (long A / short B)
z >= +z_in -> SHORT ratio (short A / long B)
EXIT: |z| <= z_exit (rientro) o time-limit max_bars. Ingresso/uscita a close.
Fee su 2 GAMBE = 2*fee_rt*lev (0.20% RT/coppia). Filtro candele sporche (salto>8%).
Validazione anti-overfit (netto, fee 0.20% RT/coppia a 2 gambe, leva 3x, OOS = ultimo
30%, CONFIG UNIVERSALE n=50 z_in=2.0 z_exit=0.75 max_bars=72, 1h):
ETH/BTC : CAGR 158% / Sharpe 4.36 / 8/9 anni+ (regge fee 6x)
LTC/ETH : CAGR 92% / Sharpe 3.08 / 8/8 anni+
ADA/ETH : CAGR 91% / Sharpe 2.69 / 7/8 anni+
BTC/LTC : CAGR 60% / Sharpe 2.36 / 7/8 anni+ (robusta anche a 4h)
ETH/SOL : CAGR 74% / Sharpe 1.96 / 5/7 anni+ (la piu' debole, DD ~63%)
- No look-ahead verificato (z[i] invariato perturbando il futuro).
- PLATEAU non picco: heatmap n x z_in -> 20/20 celle Sharpe>1 (ETH/BTC, BTC/LTC).
- WALK-FORWARD (rolling train 2y / test 6m): ETH/BTC 11/12 finestre positive,
BTC/LTC 9/10 -> edge distribuito su tutta la storia, non un regime singolo.
- Stress costi: 5/6 reggono fee+slippage realistici; solo ETH/BTC regge fee 6x.
- Correlazione con BTC daily ~0.02-0.08 -> market-neutral.
- SCARTATA BNB/ETH: robusta solo coi suoi parametri (overfit), crolla con la universale.
WORKER LIVE: implementato `src/live/pairs_worker.py` (2 gambe, fee doppie, stato
persistente) e wired in `multi_runner` (sezione `pairs:` in strategies.yml). Validato:
- LOGICA: `validate_worker_pairs.py` -> replay storico == backtest pairs_sim ESATTO
(ETH/BTC: capitale, n.trade, win% identici).
- LIVE: `live_smoke_pairs.py` (smoke reale Cerbero) -> tutte e 5 le coppie con feed
live fresco. Naming Deribit corretto: BTC/ETH = "<COIN>-PERPETUAL" (inverse),
alt = "<COIN>_USDC-PERPETUAL" (lineari USDC, storia dal 2022). Trappola: usare
"LTC-PERPETUAL"/"SOL-PERPETUAL" da' vuoto/dati sbagliati -> SEMPRE _USDC-PERPETUAL.
Resta da verificare in trading reale solo la liquidita'/fill in esecuzione.
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.pairs_research import pairs_sim, OOS_FRAC # noqa: E402
# CONFIG UNIVERSALE (anti-overfit): un'unica terna per TUTTE le coppie, niente
# cherry-picking per-coppia. Validata come ALTOPIANO (heatmap n x z_in: 20/20 celle
# Sharpe>1 su ETH/BTC e BTC/LTC) e walk-forward (ETH/BTC 11/12 finestre+, BTC/LTC 9/10).
UNIV = dict(n=50, z_in=2.0, z_exit=0.75, max_bars=72)
# Coppie robuste con la config universale. Sempre alt-liquido vs major (mai alt/alt).
# BNB/ETH SCARTATA: era robusta solo coi suoi parametri (n=30, z_exit=1.0) -> overfit;
# con la config universale crolla (Sharpe 1.5, DD 71%) ed e' la prima a morire allo stress costi.
PAIRS = [
("ETH", "BTC", UNIV), # la migliore (Sharpe 4.4 univ), regge fee 6x
("LTC", "ETH", UNIV),
("ADA", "ETH", UNIV),
("BTC", "LTC", UNIV), # robusta anche a 4h
("ETH", "SOL", UNIV), # piu' debole (DD ~63%, storia SOL corta) -> peso ridotto
]
def run():
print("=" * 92)
print(" PR01 — PAIRS spread reversion (market-neutral) | netto fee 0.20% RT/coppia, leva 3x")
print("=" * 92)
print(f" {'coppia':<10s}{'trd':>5s}{'win%':>6s}{'CAGR%':>7s}{'OOS DD%':>8s}{'DD%':>6s}{'Shrp':>6s}{'anni+':>7s}")
for a, b, p in PAIRS:
f = pairs_sim(a, b, **p)
o = pairs_sim(a, b, **p, split_frac=1 - OOS_FRAC)
yrs = f["yearly"]; pos_y = sum(1 for v in yrs.values() if v > 0)
print(f" {a+'/'+b:<10s}{f['trades']:>5d}{f['win']:>6.1f}{f['cagr']:>7.0f}{o['dd']:>8.0f}"
f"{f['dd']:>6.0f}{f['sharpe']:>6.2f}{f'{pos_y}/{len(yrs)}':>7s}")
print("\n Market-neutral (corr ~0.02-0.08 col mercato) -> ottimo diversificatore di portafoglio.")
print(" Pattern: solo alt-liquido vs major (BTC/ETH); alt-vs-alt e' rumore.")
print(" NB: 2 gambe (long A / short B), fee doppie. Worker live da estendere prima del live.")
if __name__ == "__main__":
run()
+11 -5
View File
@@ -7,10 +7,16 @@ sistema (2022, 2026 YTD) che erano gli unici anni rossi di ROT01.
Risultato (netto, fee 0.10% RT, gross 0.45, OOS = ultimo 30%): MIGLIORA TUTTO Risultato (netto, fee 0.10% RT, gross 0.45, OOS = ultimo 30%): MIGLIORA TUTTO
rispetto a ROT01. rispetto a ROT01.
ROT01 base : FULL +679% / OOS +44% / DD 53% ROT01 base : FULL +679% / OOS +44% / DD 53%
ROT02 SMA100 : FULL +1095% / OOS +98% / DD 40% <-- PnL su, DD giu' ROT02 k2 SMA100 : FULL +1095% / OOS +98% / DD 40% (versione iniziale)
Param-insensitive sulla finestra di regime (SMA100-150). Dettagli in ROT02 k3 SMA100 : FULL +1303% / OOS +68% / DD 26% <-- DD quasi dimezzato, PnL su
scripts/analysis/honest_improve.py (rot_improved).
MIGLIORIA DD (top_k 2 -> 3): la versione iniziale concentrava il book su 2 soli
asset; diversificare su 3 dimezza quasi il drawdown (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 (solo de-leverage), quindi si tiene top_k=3 senza vol-target.
Param-insensitive: top_k 3-4 e regime SMA100-150 danno risultati simili.
Dettagli e sweep in scripts/analysis/honest_improve.py (rot_improved).
""" """
from __future__ import annotations from __future__ import annotations
@@ -22,7 +28,7 @@ sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.honest_improve import rot_improved # noqa: E402 from scripts.analysis.honest_improve import rot_improved # noqa: E402
LOOKBACK, TOP_K, REGIME_N = 60, 2, 100 LOOKBACK, TOP_K, REGIME_N = 60, 3, 100 # top_k=3 (era 2): dimezza il DD
def run(): def run():
+52 -1
View File
@@ -11,6 +11,7 @@ import pandas as pd
from src.live.cerbero_client import CerberoClient from src.live.cerbero_client import CerberoClient
from src.live.strategy_loader import load_strategy from src.live.strategy_loader import load_strategy
from src.live.strategy_worker import StrategyWorker from src.live.strategy_worker import StrategyWorker
from src.live.pairs_worker import PairsWorker
from src.live.signal_engine import SignalEngine from src.live.signal_engine import SignalEngine
from src.live.telegram_notifier import send_telegram from src.live.telegram_notifier import send_telegram
@@ -18,9 +19,20 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
DATA_DIR = PROJECT_ROOT / "data" / "paper_trades" DATA_DIR = PROJECT_ROOT / "data" / "paper_trades"
RESOLUTION_MAP = {"15m": "15", "1h": "60", "5m": "5"} RESOLUTION_MAP = {"15m": "15", "1h": "60", "5m": "5"}
# Convenzione Deribit (verificata via Cerbero, 2026-05-29):
# - BTC/ETH = perpetui INVERSE (margine coin): "<COIN>-PERPETUAL"
# - altcoin = perpetui LINEARI USDC (margine USDC): "<COIN>_USDC-PERPETUAL", storia dal 2022
# Trappola: "LTC-PERPETUAL"/"ADA-PERPETUAL" = 0 candele; "SOL-PERPETUAL" = contratto vecchio
# con dati sbagliati. Per gli alt usare SEMPRE la forma _USDC-PERPETUAL.
INSTRUMENT_MAP = { INSTRUMENT_MAP = {
"BTC": "BTC-PERPETUAL", "BTC": "BTC-PERPETUAL",
"ETH": "ETH-PERPETUAL", "ETH": "ETH-PERPETUAL",
"SOL": "SOL_USDC-PERPETUAL",
"LTC": "LTC_USDC-PERPETUAL",
"ADA": "ADA_USDC-PERPETUAL",
"XRP": "XRP_USDC-PERPETUAL",
"BNB": "BNB_USDC-PERPETUAL",
"DOGE": "DOGE_USDC-PERPETUAL",
} }
@@ -130,6 +142,26 @@ def build_workers(config: dict) -> tuple[list[StrategyWorker], list[MLWorkerWrap
return regular_workers, ml_workers return regular_workers, ml_workers
def build_pairs_workers(config: dict) -> list[PairsWorker]:
"""Crea i PairsWorker (2 gambe) dalla sezione `pairs:` dello YAML."""
defaults = config.get("defaults", {})
workers: list[PairsWorker] = []
for entry in config.get("pairs", []):
if not entry.get("enabled", True):
continue
workers.append(PairsWorker(
asset_a=entry["a"], asset_b=entry["b"], tf=entry.get("tf", "1h"),
params=entry.get("params", {}),
capital=entry.get("capital", defaults.get("capital", 1000)),
position_size=entry.get("position_size", defaults.get("position_size", 0.15)),
leverage=entry.get("leverage", defaults.get("leverage", 3)),
fee_rt=entry.get("fee_rt", 0.001),
name=entry.get("name", "PR01_pairs_reversion"),
data_dir=DATA_DIR,
))
return workers
def run(): def run():
config_path = PROJECT_ROOT / "strategies.yml" config_path = PROJECT_ROOT / "strategies.yml"
if not config_path.exists(): if not config_path.exists():
@@ -143,7 +175,8 @@ def run():
train_lookback_days = 365 train_lookback_days = 365
regular_workers, ml_workers = build_workers(config) regular_workers, ml_workers = build_workers(config)
all_worker_count = len(regular_workers) + len(ml_workers) pairs_workers = build_pairs_workers(config)
all_worker_count = len(regular_workers) + len(ml_workers) + len(pairs_workers)
if all_worker_count == 0: if all_worker_count == 0:
print("Nessuna strategia abilitata in strategies.yml") print("Nessuna strategia abilitata in strategies.yml")
@@ -162,6 +195,8 @@ def run():
print(f"{w.status_summary}") print(f"{w.status_summary}")
for mw in ml_workers: for mw in ml_workers:
print(f"{mw.worker.status_summary} [ML]") print(f"{mw.worker.status_summary} [ML]")
for pw in pairs_workers:
print(f"{pw.status_summary} [PAIRS]")
send_telegram(f"🚀 Multi-Strategy avviato: {all_worker_count} strategie") send_telegram(f"🚀 Multi-Strategy avviato: {all_worker_count} strategie")
@@ -172,6 +207,9 @@ def run():
keys.add((w.asset, w.tf)) keys.add((w.asset, w.tf))
for mw in ml_workers: for mw in ml_workers:
keys.add((mw.worker.asset, mw.worker.tf)) keys.add((mw.worker.asset, mw.worker.tf))
for pw in pairs_workers: # entrambe le gambe del pair
keys.add((pw.asset_a, pw.tf))
keys.add((pw.asset_b, pw.tf))
return keys return keys
# Training iniziale ML # Training iniziale ML
@@ -253,6 +291,15 @@ def run():
except Exception as e: except Exception as e:
print(f" [{mw.worker.worker_id}] ERRORE: {e}") print(f" [{mw.worker.worker_id}] ERRORE: {e}")
# Tick pairs workers (2 gambe)
for pw in pairs_workers:
ka, kb = (pw.asset_a, pw.tf), (pw.asset_b, pw.tf)
if ka in candle_cache and kb in candle_cache:
try:
pw.tick(candle_cache[ka], candle_cache[kb])
except Exception as e:
print(f" [{pw.worker_id}] ERRORE: {e}")
# Status periodico # Status periodico
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
if now.minute == 0 and now.second < poll_seconds: if now.minute == 0 and now.second < poll_seconds:
@@ -261,6 +308,8 @@ def run():
lines.append(f" {w.status_summary}") lines.append(f" {w.status_summary}")
for mw in ml_workers: for mw in ml_workers:
lines.append(f" {mw.worker.status_summary} [ML]") lines.append(f" {mw.worker.status_summary} [ML]")
for pw in pairs_workers:
lines.append(f" {pw.status_summary} [PAIRS]")
send_telegram("\n".join(lines)) send_telegram("\n".join(lines))
except KeyboardInterrupt: except KeyboardInterrupt:
@@ -277,6 +326,8 @@ def run():
if df is not None and not df.empty: if df is not None and not df.empty:
mw.worker._close_position(float(df["close"].iloc[-1]), "shutdown") mw.worker._close_position(float(df["close"].iloc[-1]), "shutdown")
mw.worker._save_state() mw.worker._save_state()
for pw in pairs_workers: # salva stato; non forzo la chiusura a 2 gambe
pw._save_state()
send_telegram("🛑 Multi-Strategy arrestato") send_telegram("🛑 Multi-Strategy arrestato")
break break
except Exception as e: except Exception as e:
+215
View File
@@ -0,0 +1,215 @@
"""PairsWorker — paper trading a 2 GAMBE per la famiglia PR01 (spread reversion).
Market-neutral: long asset A / short asset B (o viceversa) sullo z-score del log-ratio.
Distinto dallo StrategyWorker single-leg: gestisce due strumenti, due prezzi di
ingresso, e conta le fee su ENTRAMBE le gambe (2*fee_rt*lev = 0.20% RT/coppia con
fee_rt=0.001). Semantica identica al backtest scripts/analysis/pairs_research.pairs_sim:
r[i] = log(closeA[i]/closeB[i]); z[i] = (r[i]-SMA_n(r)[i]) / STD_n(r)[i] (causale)
ENTRY a close[i]: z<=-z_in -> LONG ratio (long A / short B); z>=+z_in -> SHORT ratio
EXIT: |z| <= z_exit (rientro) oppure time-limit max_bars
filtro candele sporche: salta l'ingresso se |dr[i]| > jump_max
PnL = (retA - retB) * direction * lev - 2*fee_rt*lev (notional uguale per gamba)
Stato persistente (resume al restart) e log come StrategyWorker.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import pandas as pd
from src.live.telegram_notifier import notify_event
class PairsWorker:
def __init__(
self,
asset_a: str,
asset_b: str,
tf: str,
params: dict | None = None,
capital: float = 1000.0,
position_size: float = 0.15,
leverage: float = 3.0,
fee_rt: float = 0.001, # per gamba RT; la coppia paga 2x
name: str = "PR01_pairs_reversion",
data_dir: Path = Path("data/paper_trades"),
):
self.asset_a = asset_a
self.asset_b = asset_b
self.tf = tf
self.name = name
p = params or {}
self.n = int(p.get("n", 50))
self.z_in = float(p.get("z_in", 2.0))
self.z_exit = float(p.get("z_exit", 0.75))
self.max_bars = int(p.get("max_bars", 72))
self.jump_max = float(p.get("jump_max", 0.08))
self.initial_capital = capital
self.position_size = position_size
self.leverage = leverage
self.fee_rt = fee_rt
self.worker_id = f"{name}__{asset_a}_{asset_b}__{tf}"
self.work_dir = data_dir / self.worker_id
self.work_dir.mkdir(parents=True, exist_ok=True)
self.trades_path = self.work_dir / "trades.jsonl"
self.status_path = self.work_dir / "status.json"
self.capital = capital
self.in_position = False
self.direction = 0 # +1 long ratio (long A/short B), -1 short ratio
self.entry_a = 0.0
self.entry_b = 0.0
self.entry_z = 0.0
self.entry_time = ""
self.bars_held = 0
self.total_trades = 0
self.total_wins = 0
self.last_bar_ts = 0
self.started_at = datetime.now(timezone.utc).isoformat()
self._load_state()
self._save_state()
# ---------------- persistenza ----------------
def _load_state(self):
if not self.status_path.exists():
self._log("INIT", {"capital": self.capital, "pair": f"{self.asset_a}/{self.asset_b}",
"tf": self.tf, "params": {"n": self.n, "z_in": self.z_in,
"z_exit": self.z_exit, "max_bars": self.max_bars}})
return
with open(self.status_path) as f:
s = json.load(f)
self.capital = s.get("capital", self.initial_capital)
self.in_position = s.get("in_position", False)
self.direction = s.get("direction", 0)
self.entry_a = s.get("entry_a", 0.0)
self.entry_b = s.get("entry_b", 0.0)
self.entry_z = s.get("entry_z", 0.0)
self.entry_time = s.get("entry_time", "")
self.bars_held = s.get("bars_held", 0)
self.total_trades = s.get("total_trades", 0)
self.total_wins = s.get("total_wins", 0)
self.last_bar_ts = s.get("last_bar_ts", 0)
self.started_at = s.get("started_at", self.started_at)
self._log("RESUME", {"capital": round(self.capital, 2),
"total_trades": self.total_trades, "in_position": self.in_position})
def _save_state(self):
state = {
"capital": round(self.capital, 2), "in_position": self.in_position,
"direction": self.direction, "entry_a": self.entry_a, "entry_b": self.entry_b,
"entry_z": round(self.entry_z, 4), "entry_time": self.entry_time,
"bars_held": self.bars_held, "total_trades": self.total_trades,
"total_wins": self.total_wins, "last_bar_ts": self.last_bar_ts,
"started_at": self.started_at, "last_update": datetime.now(timezone.utc).isoformat(),
}
with open(self.status_path, "w") as f:
json.dump(state, f, indent=2)
def _log(self, event: str, data: dict | None = None):
entry = {"ts": datetime.now(timezone.utc).isoformat(), "worker": self.worker_id,
"event": event, **(data or {})}
with open(self.trades_path, "a") as f:
f.write(json.dumps(entry) + "\n")
print(f" [{self.worker_id}] {event}: {json.dumps(data or {}, default=str)}")
def _notify(self, event: str, data: dict | None = None):
notify_event(event, {"worker": self.worker_id, **(data or {})})
# ---------------- segnale ----------------
def _zscore(self, ca: np.ndarray, cb: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
r = np.log(ca / cb)
ma = pd.Series(r).rolling(self.n).mean().values
sd = pd.Series(r).rolling(self.n).std().values
z = (r - ma) / np.where(sd == 0, np.nan, sd)
dr = np.abs(np.diff(r, prepend=r[0]))
return z, dr
# ---------------- trading ----------------
def _open(self, d: int, ca: float, cb: float, z: float):
self.in_position = True
self.direction = d
self.entry_a, self.entry_b, self.entry_z = ca, cb, z
self.entry_time = datetime.now(timezone.utc).isoformat()
self.bars_held = 0
data = {"direction": "long_ratio" if d == 1 else "short_ratio",
"long_leg": self.asset_a if d == 1 else self.asset_b,
"short_leg": self.asset_b if d == 1 else self.asset_a,
"entry_a": round(ca, 4), "entry_b": round(cb, 4), "z": round(z, 3),
"capital": round(self.capital, 2)}
self._log("OPEN", data); self._notify("OPENED", data)
def _close(self, ca: float, cb: float, z: float, reason: str):
if not self.in_position:
return
ret_a = (ca - self.entry_a) / self.entry_a
ret_b = (cb - self.entry_b) / self.entry_b
gross = (ret_a - ret_b) * self.direction * self.leverage
fee = 2 * self.fee_rt * self.leverage # 2 gambe
net = gross - fee
pnl = self.capital * self.position_size * net
self.capital = max(self.capital + pnl, 0.0)
is_win = net > 0
self.total_trades += 1
self.total_wins += is_win
acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0
data = {"reason": reason, "exit_a": round(ca, 4), "exit_b": round(cb, 4),
"z": round(z, 3), "gross_ret": round(gross * 100, 3), "fee": round(fee * 100, 3),
"net_return": round(net * 100, 3), "pnl": round(pnl, 2),
"capital": round(self.capital, 2), "bars_held": self.bars_held,
"win": bool(is_win), "total_trades": self.total_trades, "accuracy": round(acc, 1)}
self._log("CLOSE", data); self._notify("CLOSED", data)
self.in_position = False
self.direction = 0
self.entry_a = self.entry_b = self.entry_z = 0.0
self.bars_held = 0
def tick(self, df_a: pd.DataFrame, df_b: pd.DataFrame):
"""Chiamato ad ogni poll con gli OHLCV aggiornati delle due gambe."""
if df_a is None or df_b is None or df_a.empty or df_b.empty:
return
m = df_a[["timestamp", "close"]].rename(columns={"close": "ca"}).merge(
df_b[["timestamp", "close"]].rename(columns={"close": "cb"}), on="timestamp", how="inner"
).sort_values("timestamp").reset_index(drop=True)
if len(m) < self.n + 2:
return
ca, cb = m["ca"].values, m["cb"].values
z, dr = self._zscore(ca, cb)
i = len(m) - 1
cur_ts = int(m["timestamp"].iloc[i])
zi = z[i]
if np.isnan(zi):
self._save_state(); return
if self.in_position:
if cur_ts > self.last_bar_ts:
self.bars_held += 1
self.last_bar_ts = cur_ts
if abs(zi) <= self.z_exit:
self._close(float(ca[i]), float(cb[i]), float(zi), "mean_revert")
elif self.bars_held >= self.max_bars:
self._close(float(ca[i]), float(cb[i]), float(zi), "time_limit")
self._save_state()
return
# flat: cerca ingresso (no look-ahead: z[i] usa solo dati <= i)
if dr[i] <= self.jump_max:
if zi <= -self.z_in:
self._open(1, float(ca[i]), float(cb[i]), float(zi)); self.last_bar_ts = cur_ts
elif zi >= self.z_in:
self._open(-1, float(ca[i]), float(cb[i]), float(zi)); self.last_bar_ts = cur_ts
self._save_state()
@property
def status_summary(self) -> str:
acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0
pos = ("LONG " + self.asset_a if self.direction == 1
else "SHORT " + self.asset_a if self.direction == -1 else "FLAT")
return (f"{self.worker_id}: €{self.capital:.0f} | {self.total_trades}t {acc:.0f}% | {pos}")
+40
View File
@@ -90,3 +90,43 @@ strategies:
max_bars: 24 max_bars: 24
trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD- trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD-
ema_long: 200 ema_long: 200
# ---------------------------------------------------------------------------
# PR01 — PAIRS market-neutral spread reversion (worker a 2 GAMBE: src/live/pairs_worker.py)
# Config UNIVERSALE n50 z2 zx0.75 mb72 (anti-overfit, validata walk-forward).
# fee_rt 0.001/gamba -> 0.20% RT/coppia.
# FEED LIVE (verificato 2026-05-29): tutti i leg disponibili su Deribit via Cerbero con
# il naming corretto -> BTC/ETH = "<COIN>-PERPETUAL" (inverse), alt = "<COIN>_USDC-PERPETUAL"
# (lineari USDC, storia dal 2022). Tutte e 5 le coppie tradabili live. ETH/SOL la piu'
# debole (DD ~63%, storia SOL piu' corta) -> peso ridotto consigliato.
pairs:
- name: PR01_pairs_reversion
a: ETH
b: BTC
tf: 1h
enabled: true
params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08}
- name: PR01_pairs_reversion
a: LTC
b: ETH
tf: 1h
enabled: true
params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08}
- name: PR01_pairs_reversion
a: ADA
b: ETH
tf: 1h
enabled: true
params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08}
- name: PR01_pairs_reversion
a: BTC
b: LTC
tf: 1h
enabled: true
params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08}
- name: PR01_pairs_reversion # la piu' debole (peso ridotto)
a: ETH
b: SOL
tf: 1h
enabled: true
params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08}