15 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
Adriano bcccfde9a0 feat(strategie): portafogli master (PORT02/PORT03) + waste delle peggiori (MR03, ROT01)
Crea gli artefatti accorpati e migliorati:
- PORT02_fade_master: 3 fade (MR01/MR02/MR07) x BTC/ETH = 6 sleeve, filtro trend,
  equal-weight daily. DD 8.2% full / 5.9% OOS, Sharpe 3.95/4.09, CAGR ~46%.
- PORT03_all_master: portafoglio MASTER fade+honest (9 sleeve), varianti equal
  (max Sharpe: DD 5.2%/4.7% OOS, Sharpe 3.95/4.42) e 50/50 (min DD 5.1%/4.3%).

Sposta in scripts/waste/ le due peggiori:
- MR03 keltner_fade: fade piu' debole (BTC Sharpe 1.22), ridondante con MR01, il
  filtro trend la peggiorava; rimuoverla MIGLIORA il portafoglio fade.
- ROT01 xsect_rotation: strettamente dominata da ROT02 (stesso meccanismo, ROT02
  meglio su tutto), non usata da alcun portafoglio.

Sganciata MR03 da strategy_loader, strategies.yml e dal motore portafogli
(risk_management.STRATS). La funzione keltner_fade resta in strategy_research_v2
come record. CLAUDE.md aggiornato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 00:21:37 +02:00
Adriano 1af4addbdd feat(analysis): studio combinazione strategie fade + honest (diversificazione)
combine_portfolio.py: costruisce l'equity giornaliera di tutte le sleeve (8 fade +
3 honest) su indice comune 2021-2026, misura la correlazione cross-famiglia e
confronta i portafogli FULL/OOS (ret, CAGR, DD, Sharpe).

Risultato: le due famiglie sono quasi scorrelate (corr ~0.05). Combinarle migliora
il rischio/rendimento: equal-weight 11 sleeve -> DD 6.1% full / 4.6% OOS, Sharpe OOS
4.46 (vs honest-only 12% DD / 2.23 e fade-only 8.6% DD / 4.14), CAGR ~43% mantenuta.
Il 50/50 fra famiglie da' il DD piu' basso (5.5% full / 4.0% OOS). Diario 2026-05-29
e nota CLAUDE.md aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 00:14:14 +02:00
Adriano 22c6080873 chore(analysis): pulizia e accorpamento script di analisi (25 -> 15 file)
- accorpa risk_improvements.py + risk_portfolio.py -> risk_management.py
  (sezione A screening leve, sezione B filtro trend + portafoglio)
- rimuove 4 script legacy della famiglia squeeze (ormai in waste, non
  referenziati): compare_strategies, best_yearly, final_report, yearly_market_report
- rimuove 5 script honest_* di diagnostica/iterazione superati da honest_matrix
  (consolidato) e non importati: honest_diag, honest_diag2, honest_candidates,
  honest_yearly, honest_yearly2
- mantiene il core honest (lab/improve/improve2/rotation/trend) + canonici
  (final/matrix), tutta la ricerca fade (strategy_research[_v2]), validazione
  (oos_validation, validate_worker_mr01), intrabar_test (lezione squeeze)
- aggiorna riferimento in CLAUDE.md. Import-check: 14/14 moduli OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 00:08:24 +02:00
Adriano fd547748dd merge: strategy_free -> main (fade MR02/MR03/MR07 + filtro trend Acc/DD)
Integra il lavoro su branch strategy_free, indipendente dalle strategie oneste
(DIP/TR/ROT/PORT) gia' su main: nessun file in comune, merge pulito.

- 3 nuove fade mean-reversion validate OOS fee-aware: MR02 Donchian fade,
  MR03 Keltner fade, MR07 Return reversal (+ base condivisa fade_base).
- Filtro trend (trend_max/ema_long) su tutte le fade: alza Acc e riduce DD
  (drastico su ETH), edge OOS confermato; modello portafoglio equipesato.
2026-05-29 00:00:51 +02:00
Adriano cff0d08fca feat(risk): filtro trend per alzare Acc e ridurre DD + modello portafoglio
Filtro opzionale trend_max/ema_long su tutte le fade (MR01/MR02/MR03/MR07):
salta i segnali quando |close-EMA200|/ATR supera la soglia (non fadare un trend
o crollo estremo). Con trend_max=3.0 (default in strategies.yml): accuratezza su
e DD giu' su 7/8 sleeve, drastico su ETH (MR01 71->26%, MR02 42->25%,
MR03 66->34%, MR07 46->21%); edge OOS confermato. MR03 BTC: filtro disattivo
(unico sleeve dove peggiora entrambe).

Scartate come non robuste: vol-target sizing e skip-alta-volatilita' (peggiorano
sia Acc che DD). Aggiunto modello di portafoglio equipesato su sotto-conti
indipendenti: DD aggregato ~14% full / ~10% OOS sul paniere di 8 sleeve, contro
20-70% del singolo -> vera leva anti-drawdown.

Banco di prova: scripts/analysis/risk_improvements.py, risk_portfolio.py.
Helper trend_distance() in fade_base. CLAUDE.md e diario aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:47:52 +02:00
Adriano 21d3ba609d feat(strategie): 3 nuove fade mean-reversion validate OOS fee-aware (MR02/MR03/MR07)
Trovate e promosse 3 strategie con edge netto distinto da MR01, stessa
metodologia (ingresso close[i], netto fee 0.10% RT + leva 3x, OOS ultimo 30%,
robustezza su griglia + sweep fee 0.00-0.20%):

- MR02 Donchian Fade: fade rottura canale H/L, TP al centro. BTC +172% OOS.
- MR03 Keltner Fade: canale ATR su EMA (indipendente da Bollinger). BTC +112%.
- MR07 Return Reversal: fade movimento di barra estremo (z dei rendimenti). BTC +105%.

Tutte positive netto OOS su entrambi gli asset e su tutto lo sweep fee, anche
0.20% RT pessimista (validate anche via oos_validation live-path). Scartate
MR04 (= MR01 riparametrizzato), MR05 (ADX non robusto), MR06 (RSI2 ETH neg).

Base condivisa fade_base.FadeStrategy (backtest intrabar TP/SL/max_bars).
Aggiunte a strategy_loader e strategies.yml (BTC+ETH 1h). Ricerca in
strategy_research_v2.py. Diario e CLAUDE.md aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:26:21 +02:00
41 changed files with 3029 additions and 1930 deletions
+103 -10
View File
@@ -24,10 +24,12 @@ src/strategies/ → classe base Strategy ABC + indicatori condivisi
base.py → Strategy, Signal, BacktestResult, YearlyStats
indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation
src/live/ → paper trading live multi-strategia
multi_runner.py → orchestratore: carica YAML, fetch candele, tick worker
strategy_worker.py → worker indipendente: capital, trade log, stato persistente.
multi_runner.py → orchestratore: carica YAML (strategies + pairs), fetch candele, tick worker
strategy_worker.py → worker single-leg: capital, trade log, stato persistente.
Exit guidati da strategia (TP/SL/max_bars via Signal.metadata),
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/
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet)
signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS
@@ -82,16 +84,101 @@ Token observer: nel file `secrets/observer.token` del progetto CerberoSuite.
> e `intrabar_test.py`.
Tutte le strategie estendono `src.strategies.base.Strategy`
(`generate_signals() → backtest()`). **Unica strategia con edge netto validato:**
(`generate_signals() → backtest()`). Le strategie mean-reversion condividono
`src.strategies.fade_base.FadeStrategy` (backtest intrabar TP/SL/max_bars).
**Strategie con edge netto validato OOS fee-aware (tutte fade/mean-reversion):**
| Codice | Nome | Tipo | Edge OOS netto | DD | Note |
|--------|------|------|----------------|----|------|
| **MR01** | Bollinger Fade | Mean-reversion | **BTC 1h n50 k2.5: +201% / +196% (worker)** | 15% | Fada la banda, TP alla media, SL ad ATR |
| 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% |
MR01 è robusto su **tutta** la griglia parametri (`n∈{14,20,30,50}` × `k∈{2.0,2.5,3.0}`,
entrambi gli asset → tutte positive OOS) e su **tutte** le fee 0.00-0.20% RT.
Validato col worker reale: BTC +196% / ETH +251% OOS (nov 2023→mag 2026).
Ricerca completa: `scripts/analysis/strategy_research.py`.
> **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).
**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).
**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`.
@@ -101,6 +188,12 @@ Ricerca completa: `scripts/analysis/strategy_research.py`.
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.
## Multi-Strategy Paper Trader
Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna con €1000 USDC virtuali indipendenti.
+97
View File
@@ -94,3 +94,100 @@ risultati riproducibili. La config live di MT01 (ema20+vol) coincide col best do
serve uno scheduling del download (cron/job).
2. **Healthcheck:** valutare un check su mtime di `status.json` (< 180s) per rilevare uno
stallo del loop, non solo l'esistenza del file.
---
### 23:00 — 3 nuove strategie con edge OOS fee-aware (branch `strategy_free`)
**Obiettivo:** trovare almeno 3 nuove strategie (oltre MR01), edge netto validato
out-of-sample e fee-aware, per il target €1.000 → ~€50/giorno.
**Metodologia (invariata dalla lezione squeeze):** ingresso eseguibile a `close[i]`
(nessun look-ahead), backtest netto dopo fee Deribit 0.10% RT + leva 3x, OOS = ultimo
30% held-out, robustezza su griglia parametri + sweep fee 0.000.20% RT, exit
TP/SL intrabar o time-limit, una posizione per volta, capitale composto.
**Candidati** (`scripts/analysis/strategy_research_v2.py`), tutti mean-reversion
(l'edge è sempre il rientro, mai la continuazione):
| Candidato | Esito | Motivo |
|---|---|---|
| **MR02 Donchian Fade** | ✅ | Robusto su tutta la griglia `n × sl_atr` e tutte le fee |
| **MR03 Keltner Fade** | ✅ | Robusto su tutta la griglia `n × k`; banda ATR, indipendente da Bollinger |
| **MR07 Return Reversal** | ✅ | Intero blocco `tp_atr=2.0` positivo full+OOS; esposizione ~8% |
| MR04 Z-score Reversion | ⛔ | Robusto ma è MR01 riparametrizzato (stessa banda std): edge non *nuovo* |
| MR05 Bollinger + filtro ADX | ⛔ | Non robusto: negativo su gran parte della griglia BTC |
| MR06 RSI(2) Connors | ⛔ | ETH 1h negativo; non robusto su entrambi gli asset |
**Risultati** (netto 0.10% RT, leva 3x, OOS, 1h):
| Codice | Meccanismo | BTC OOS | ETH OOS | DD (full) |
|---|---|---|---|---|
| MR02 | estremi canale Donchian H/L | +172% | enorme | 30% / 42% |
| MR03 | canale ATR su EMA | +112% | +886% | 37% / 66% |
| MR07 | z dei rendimenti di barra | +105% | +195% | 25% / 46% |
**Validazione live-path** (`oos_validation.py`, legge `strategies.yml`, exit hold
del worker): tutte e tre positive netto OOS su tutto lo sweep fee, anche al
pessimistico 0.20% RT → edge robusto pure al meccanismo di exit.
**Verifiche:** equivalenza esatta backtest produzione vs research engine (MR02 BTC:
2039 trade, DD 29% identici); le 3 classi si caricano dal `strategy_loader`;
aggiunte a `strategies.yml` (BTC+ETH 1h). Nessuna suite di test nel progetto.
**Onestà sul target:** con 4 fade indipendenti × 2 asset il PnL storico aggregato
supera €50/giorno, ma sono backtest a leva 3x su 8 anni con annate eccezionali
(ETH 2024). Plausibile ma da confermare col paper trader live prima del capitale reale.
DD alto su ETH (MR03 ~66%, come MR01) → leva più bassa consigliata per quell'asset.
**File:** `strategy_research_v2.py`, `src/strategies/fade_base.py`,
`scripts/strategies/MR0{2,3,7}_*.py` (nuovi); `strategy_loader.py`, `strategies.yml`,
`CLAUDE.md` (aggiornati).
**Lezione confermata:** ogni edge robusto trovato finora è mean-reversion; ogni
variante trend/continuation o oscillatore senza filtro perde netto.
---
### 23:45 — Aumentare Acc e ridurre DD (filtro trend + portafoglio)
**Obiettivo:** alzare accuratezza e abbassare drawdown sulle 4 fade, senza
distruggere l'edge né overfittare (ogni leva misurata FULL **e** OOS).
**Diagnosi:** perdite/DD concentrati 20182021 (bear/covid/caos vol), su ETH DD
pieno 6671%. Banco di prova: `scripts/analysis/risk_improvements.py` e
`risk_portfolio.py`.
**Leve testate:**
| Leva | Esito | Motivo |
|---|---|---|
| Sizing vol-target (size ∝ 1/dist-SL) | ⛔ | Over-size sui trade a stop stretto → DD su, ritorno giù |
| Skip alta volatilità (ATR% in coda alta) | ⛔ | L'alta vol è *positiva* per le fade (più reversione): Acc e ritorno giù |
| **Filtro trend** (`\|closeEMA200\|/ATR > soglia` → salta) | ✅ | Non fada trend/crolli estremi: Acc↑ ovunque, DD↓ molto su ETH, OOS regge |
| **Portafoglio** equipesato (sotto-conti indipendenti) | ✅ | Curve poco correlate → DD aggregato 14% (full)/10% (OOS) vs 20-70% singolo |
**Filtro trend — sweep soglia** (assoluta in ATR, regola unica per tutte = niente
overfit): 3.0 ATR è l'equilibrio (2.0 taglia troppo ritorno). Effetto su config
deployata (base → filtro):
| Sleeve | Acc | DD |
|---|---|---|
| MR01 ETH | 46→55 | **71→26** |
| MR02 ETH | 49→55 | 42→25 |
| MR03 ETH | 49→52 | 66→34 |
| MR07 ETH | 48→54 | 46→21 |
| MR01 BTC | 51→54 | 32→34* |
| MR02 BTC | 48→52 | 29→23 |
| MR07 BTC | 49→53 | 25→18 |
| MR03 BTC | 47→47 | 37→37 (filtro OFF) |
\*MR01 BTC: DD full +2pt ma Acc +3.7 e DD OOS piatto (14.8→15.0). **MR03 BTC**:
il filtro peggiora entrambe (unico sleeve) → lasciato disattivo nello yaml.
**Implementazione:** helper `trend_distance()` in `fade_base.py`; param opzionali
`trend_max`/`ema_long` (default None = retro-compatibile) in tutte le strategie
(MR01/02/03/07); `strategies.yml` con `trend_max: 3.0, ema_long: 200` (eccetto
MR03 BTC). Verificato: equivalenza produzione vs ricerca.
**Lezione:** il modo onesto di ridurre il DD non è strozzare il sizing (peggiora),
ma (a) non opporsi a trend estremi e (b) diversificare su strategie scorrelate.
+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).
+64
View File
@@ -0,0 +1,64 @@
# Diario di ricerca — 2026-05-29
## Combinare le strategie migliora i risultati?
**Domanda:** usare insieme le due famiglie di strategie presenti sul repo migliora
il profilo rischio/rendimento rispetto a usarle separatamente?
- **FADE** (mie): reversione intraday 1h, long/short, BTC/ETH — MR01 Bollinger,
MR02 Donchian, MR03 Keltner, MR07 Return-reversal (tutte col filtro trend 3.0 ATR).
- **HONEST** (altra sessione): long-only multi-regime multi-crypto — DIP01 (dip-buy
1h BTC), TR01 (EMA-trend 4h basket), ROT02 (dual-momentum rotation 1d).
**Metodo** (`scripts/analysis/combine_portfolio.py`): per ogni sleeve si costruisce
l'equity **giornaliera** normalizzata su un indice comune (2021-01-01 → 2026-05-26),
si passa ai rendimenti giornalieri, si misura la correlazione cross-famiglia e si
confrontano i portafogli equal-weight (ribilanciati ogni giorno), 50/50 fra famiglie
e inverse-vol. Metriche FULL e OOS (ultimo 30% della finestra comune, da 2024-10-12):
ritorno, CAGR, max DD, Sharpe annualizzato. Le curve honest sono riusate da
`honest_improve2.py`; quelle fade da `risk_management.build_trades`.
**Correlazione:** cross-famiglia **+0.05** (quasi indipendenti). Intra-fade +0.18,
intra-honest +0.05. L'unica coppia un po' correlata è MR01_BTC↔DIP01_BTC (+0.43),
entrambe mean-reversion su BTC. Famiglie scorrelate ⇒ diversificazione quasi ideale.
**Risultati (FULL | OOS):**
| Portafoglio | Ret% | CAGR | DD% | Sharpe | oDD% | oSharpe |
|---|---|---|---|---|---|---|
| FADE only (8) | +549 | 41 | 8.6 | 3.75 | 5.4 | 4.14 |
| HONEST only (3) | +642 | 45 | 12.0 | 1.90 | 6.5 | 2.23 |
| **ALL equal-weight (11)** | +589 | 43 | 6.1 | **3.95** | 4.6 | **4.46** |
| **ALL 50/50 famiglie** | +615 | 44 | **5.5** | 3.18 | **4.0** | 3.87 |
| ALL inverse-vol | +483 | 39 | 5.8 | 3.97 | 4.6 | 4.02 |
**Conclusione: sì, combinare conviene.**
- DD crolla: combinato 5.56.1% full / 4.04.6% OOS, contro 8.6% (fade) e **12%**
(honest) da sole → drawdown ridotto del 3550%.
- Sharpe sale: combinato OOS **4.46** vs honest 2.23 (raddoppia) e batte pure fade (4.14).
- CAGR resta ~4344% (≈ media delle due famiglie) ma con metà del rischio: è il
"free lunch" della diversificazione fra sorgenti di edge scorrelate.
- Best Sharpe = equal-weight degli 11 sleeve; best DD = 50/50 fra le due famiglie.
**Caveat onesti:** la finestra comune è 20212026 (5.4 anni), OOS ~2024-10→oggi
(1.6 anni) — pochi regimi. CAGR e Sharpe sono backtest a leva 3x; il 2024 cripto
favorevole pesa. Il target €50/giorno resta vincolato dal capitale: 43% CAGR su
€1000 non fa €50/giorno a breve, serve compounding pluriennale o più capitale.
Prossimo passo: confermare il portafoglio combinato nel paper trader live.
**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`.
View File
-309
View File
@@ -1,309 +0,0 @@
"""Confronto migliori strategie S1 e S2 — andamento per anno."""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from src.data.downloader import load_data
from src.fractal.patterns import encode_candles
FEE_PERP = 0.002 # 0.1% taker roundtrip perpetual
FEE_OPT = 0.0052 # options roundtrip
INITIAL = 1000
LEVERAGE = 3
def keltner_ratio(close, high, low, window=14):
n = len(close)
r = np.full(n, np.nan)
for i in range(window, n):
wc, wh, wl = close[i-window:i], high[i-window:i], low[i-window:i]
ma = np.mean(wc)
bb_std = np.std(wc)
tr = np.maximum(wh-wl, np.maximum(np.abs(wh-np.roll(wc,1)), np.abs(wl-np.roll(wc,1))))
atr = np.mean(tr[1:])
kc = (ma+1.5*atr)-(ma-1.5*atr)
bb = (ma+2*bb_std)-(ma-2*bb_std)
if kc > 0:
r[i] = bb/kc
return r
def rv_ann(close, window):
lr = np.diff(np.log(np.where(close==0, 1e-10, close)))
r = np.full(len(close), np.nan)
for i in range(window, len(lr)):
r[i+1] = np.std(lr[i-window:i]) * np.sqrt(24*365)
return r
def rsi(close, period=14):
delta = np.diff(close)
gain = np.where(delta>0, delta, 0)
loss = np.where(delta<0, -delta, 0)
result = np.full(len(close), 50.0)
if len(gain) < period:
return result
ag = np.mean(gain[:period])
al = np.mean(loss[:period])
for i in range(period, len(delta)):
ag = (ag*(period-1)+gain[i])/period
al = (al*(period-1)+loss[i])/period
result[i+1] = 100 if al == 0 else 100-100/(1+ag/al)
return result
def ema(arr, period):
r = np.full(len(arr), np.nan)
k = 2/(period+1)
r[period-1] = np.mean(arr[:period])
for i in range(period, len(arr)):
r[i] = arr[i]*k + r[i-1]*(1-k)
return r
# =====================================================================
# S1 BEST: Squeeze Breakout ETH 1h (BBw=14, sq=0.8, brk=3)
# =====================================================================
def run_s1_squeeze(asset, tf):
df = load_data(asset, tf)
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
kcr = keltner_ratio(c, h, l, 14)
yearly = {}
in_sq = False
sq_start = 0
for i in range(15, n):
if np.isnan(kcr[i]):
continue
is_sq = kcr[i] < 0.8
if is_sq and not in_sq:
in_sq = True
sq_start = i
elif not is_sq and in_sq:
in_sq = False
if i - sq_start < 5 or i + 3 >= n:
continue
first_ret = (c[i] - c[i-1]) / c[i-1]
if abs(first_ret) < 0.001:
continue
direction = 1 if first_ret > 0 else -1
actual = (c[i+2] - c[i-1]) / c[i-1]
trade_ret = actual * direction
net = trade_ret * LEVERAGE - FEE_PERP * LEVERAGE
year = ts.iloc[i].year
if year not in yearly:
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
yearly[year]["pnls"].append(net)
yearly[year]["total"] += 1
if trade_ret > 0:
yearly[year]["wins"] += 1
return yearly
# =====================================================================
# S1 BEST ALT: Squeeze+ML hybrid ETH 15m
# =====================================================================
# Troppo complesso da ricalcolare (serve ML training). Uso i dati S1 squeeze puro.
# =====================================================================
# S2 BEST: VRP ETH 48h (con IV stimata, unico disponibile su 8 anni)
# =====================================================================
def run_s2_vrp(asset, dte=48):
df = load_data(asset, "1h")
c = df["close"].values
n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
rv_24 = rv_ann(c, 24)
rv_168 = rv_ann(c, 168)
yearly = {}
for i in range(170, n - dte):
if ts.iloc[i].hour != 8:
continue
rv_s, rv_l = rv_24[i], rv_168[i]
if np.isnan(rv_s) or np.isnan(rv_l) or rv_s < 0.05 or rv_l < 0.05:
continue
regime = rv_s / rv_l
iv_pf = 0.9 if regime > 2 else (1.0 if regime > 1.5 else (1.1 if regime > 1 else 1.2))
iv = rv_l * iv_pf
prem = iv * np.sqrt(dte/(24*365)) * 0.8
spot = c[i]
move = abs(c[min(i+dte, n-1)] - spot) / spot
pos = 0.10
raw = (prem - move) * pos if move <= prem else max(-(move-prem)*pos, -pos*0.05)
net = raw - FEE_OPT * pos
year = ts.iloc[i].year
if year not in yearly:
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
yearly[year]["pnls"].append(net)
yearly[year]["total"] += 1
if raw > 0:
yearly[year]["wins"] += 1
return yearly
# =====================================================================
# S2 BEST PERPETUAL: Multi-TF 15m+1h BTC
# =====================================================================
def run_s2_multitf(asset):
df_1h = load_data(asset, "1h")
df_15m = load_data(asset, "15m")
c1h = df_1h["close"].values
ts1h = pd.to_datetime(df_1h["timestamp"], unit="ms", utc=True)
c15 = df_15m["close"].values
ts15 = df_15m["timestamp"].values
n15 = len(c15)
ema_50 = ema(c1h, 50)
rsi_15m = rsi(c15, 14)
yearly = {}
daily_done = set()
for i in range(100, n15 - 12):
ts_dt = pd.Timestamp(ts15[i], unit="ms", tz="UTC")
day = ts_dt.strftime("%Y-%m-%d")
if day in daily_done:
continue
if rsi_15m[i] > 35 and rsi_15m[i] < 65:
continue
h_idx = np.searchsorted(ts1h.values.astype("int64"), ts15[i]) - 1
if h_idx < 50 or h_idx >= len(c1h) or np.isnan(ema_50[h_idx]):
continue
direction = None
if rsi_15m[i] < 30 and c1h[h_idx] > ema_50[h_idx]:
direction = "long"
elif rsi_15m[i] > 70 and c1h[h_idx] < ema_50[h_idx]:
direction = "short"
if direction is None:
continue
entry = c15[i]
exit_price = c15[min(i+12, n15-1)]
trade_ret = (exit_price-entry)/entry if direction == "long" else (entry-exit_price)/entry
net = trade_ret * LEVERAGE - FEE_PERP * LEVERAGE
year = ts_dt.year
if year not in yearly:
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
yearly[year]["pnls"].append(net)
yearly[year]["total"] += 1
if trade_ret > 0:
yearly[year]["wins"] += 1
daily_done.add(day)
return yearly
# =====================================================================
# REPORT
# =====================================================================
strategies = {
"S1: Squeeze BTC 1h": run_s1_squeeze("BTC", "1h"),
"S1: Squeeze ETH 1h": run_s1_squeeze("ETH", "1h"),
"S1: Squeeze ETH 15m": run_s1_squeeze("ETH", "15m"),
"S2: VRP ETH 48h (IV est)": run_s2_vrp("ETH", 48),
"S2: VRP BTC 48h (IV est)": run_s2_vrp("BTC", 48),
"S2: MultiTF BTC 15m+1h": run_s2_multitf("BTC"),
"S2: MultiTF ETH 15m+1h": run_s2_multitf("ETH"),
}
all_years = sorted(set(y for v in strategies.values() for y in v))
print("=" * 120)
print(" MIGLIORI STRATEGIE — ANDAMENTO PER ANNO")
print(" Fee reali. PnL su €1000 flat (no compounding). Dati OHLCV reali 2018-2026.")
print(" ⚠ VRP usa IV STIMATA (non reale) — fidarsi solo dei dati perpetual per backtest lungo")
print("=" * 120)
# Header
hdr = f" {'Anno':>6s}"
for name in strategies:
short = name.split(": ")[1][:18]
hdr += f" | {short:>18s}"
print(hdr)
print(f" {'-' * (len(hdr) - 2)}")
# Per anno: accuracy / PnL totale
for year in all_years:
row_acc = f" {year:>6d}"
row_pnl = f" {'':>6s}"
for name, yearly in strategies.items():
if year in yearly:
d = yearly[year]
acc = d["wins"]/d["total"]*100 if d["total"] > 0 else 0
pnl = sum(d["pnls"]) * INITIAL
tag = "" if acc >= 75 else "" if acc >= 65 else "" if acc >= 55 else " "
row_acc += f" | {acc:>5.1f}% {tag} {d['total']:>3d}t"
row_pnl += f" | €{pnl:>+8.0f} "
else:
row_acc += f" | {'':>18s}"
row_pnl += f" | {'':>18s}"
print(row_acc)
print(row_pnl)
# Totali
print(f" {'-' * (len(hdr) - 2)}")
row_tot = f" {'TOT':>6s}"
for name, yearly in strategies.items():
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
all_wins = sum(d["wins"] for d in yearly.values())
all_total = sum(d["total"] for d in yearly.values())
acc = all_wins/all_total*100 if all_total > 0 else 0
pnl = sum(all_pnls) * INITIAL
row_tot += f" | {acc:>5.1f}% {all_total:>4d}t"
print(row_tot)
row_pnl_tot = f" {'€TOT':>6s}"
for name, yearly in strategies.items():
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
pnl = sum(all_pnls) * INITIAL
row_pnl_tot += f" | €{pnl:>+8.0f} "
print(row_pnl_tot)
# Compounding
print(f"\n {'':>6s}", end="")
for name in strategies:
short = name.split(": ")[1][:18]
print(f" | {short:>18s}", end="")
print()
row_comp = f" {'COMP':>6s}"
for name, yearly in strategies.items():
cap = float(INITIAL)
for year in sorted(yearly):
for pnl in yearly[year]["pnls"]:
cap += cap * pnl
cap = max(cap, 10)
row_comp += f" | €{cap:>12,.0f} "
print(row_comp)
# Drawdown
row_dd = f" {'MAXDD':>6s}"
for name, yearly in strategies.items():
cap = float(INITIAL)
peak = cap
mdd = 0
for year in sorted(yearly):
for pnl in yearly[year]["pnls"]:
cap += cap * pnl
cap = max(cap, 10)
if cap > peak: peak = cap
dd = (peak - cap) / peak
mdd = max(mdd, dd)
row_dd += f" | {mdd*100:>12.1f}% "
print(row_dd)
# Legenda
print(f"\n Legenda: ▓ ≥75% acc ▒ ≥65% acc ░ ≥55% acc")
print(f" ⚠ S2 VRP: IV stimata (rv_long × 1.0-1.2), NON dati reali opzioni")
print(f" S1 Squeeze e S2 MultiTF: dati OHLCV reali al 100%")
+156
View File
@@ -0,0 +1,156 @@
"""Studio: combinare TUTTE le strategie (fade + honest) migliora i risultati?
Due famiglie con meccanismi e orizzonti diversi:
FADE (intraday 1h, long/short, BTC/ETH): MR01 boll, MR02 donchian, MR07
return-reversal — tutte col filtro trend 3.0 ATR. (MR03 keltner -> waste.)
HONEST (long-only, multi-regime, multi-crypto): DIP01 (dip-buy 1h BTC),
TR01 (EMA trend 4h basket), ROT02 (dual-momentum rotation 1d).
Metodo: per ogni sleeve si costruisce l'equity GIORNALIERA normalizzata su un
indice comune (2021-01-01 -> 2026-05-26), si passa ai rendimenti giornalieri,
si misura la correlazione cross-famiglia e si confrontano i portafogli
equal-weight (ribilanciati ogni giorno) e inverse-vol. Metriche FULL e OOS
(ultimo 30% della finestra comune): ritorno, CAGR, max DD, Sharpe annualizzato.
Tutto NETTO (fee gia' incluse nelle sleeve), leva 3x, pos 15% per sleeve.
"""
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.risk_management import strats_for, build_trades, INIT
# curve daily honest gia' pronte nell'altra famiglia
from scripts.analysis.honest_improve2 import (
_daily_equity, _norm, dip_market_gated, _tr_basket_daily, _rot_daily_equity,
)
IDX = pd.date_range("2021-01-01", "2026-05-26", freq="1D", tz="UTC")
OOS_FRAC = 0.30
SPLIT = int(len(IDX) * (1 - OOS_FRAC)) # confine OOS sulla finestra comune
OOS_DATE = IDX[SPLIT].date()
ANN = 365.0 # giorni/anno per annualizzare
# ---------------- equity giornaliere ----------------
def fade_daily_equity(asset: str, fn, params) -> pd.Series:
"""Equity giornaliera di uno sleeve fade: trade 1h (filtro trend 3.0) -> equity -> daily."""
df = load_data(asset, "1h")
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
trades = build_trades(fn(df, **params), df, trend_max=3.0)
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 * 0.15 * ret, 10.0)
eq[j:] = cap
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
return _norm(s)
def build_all_sleeves() -> dict[str, pd.Series]:
sleeves: dict[str, pd.Series] = {}
# --- FADE: 8 sleeve ---
for asset in ["BTC", "ETH"]:
for nm, (fn, params) in strats_for(asset).items():
sleeves[f"{nm}_{asset}"] = fade_daily_equity(asset, fn, params)
# --- HONEST: 3 sleeve (riuso le funzioni dell'altra famiglia) ---
d = dip_market_gated("BTC", market_n=0, return_equity=True)
sleeves["DIP01_BTC"] = _norm(_daily_equity(d["eq_ts"], d["eq_v"], IDX))
sleeves["TR01_basket"] = _norm(_tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], IDX))
sleeves["ROT02_rot"] = _norm(_rot_daily_equity(IDX))
return sleeves
# ---------------- metriche ----------------
def metrics(daily_ret: pd.Series, lo: int = 0, hi: int | None = None) -> dict:
r = daily_ret.iloc[lo:hi]
eq = (1 + r).cumprod()
peak = eq.cummax(); dd = float(((peak - eq) / peak).max() * 100)
yrs = len(r) / ANN
tot = (eq.iloc[-1] - 1) * 100
cagr = ((eq.iloc[-1]) ** (1 / yrs) - 1) * 100 if yrs > 0 else 0.0
sharpe = float(r.mean() / r.std() * np.sqrt(ANN)) if r.std() > 0 else 0.0
return dict(ret=tot, cagr=cagr, dd=dd, sharpe=sharpe)
def yearly_returns(daily_ret: pd.Series) -> dict[int, float]:
"""Rendimento % netto per anno solare dai rendimenti giornalieri composti."""
g = daily_ret.groupby(daily_ret.index.year).apply(lambda x: ((1 + x).prod() - 1) * 100)
return {int(y): float(v) for y, v in g.items()}
def port_returns(members: dict[str, pd.Series], weights: dict[str, float] | None = None) -> pd.Series:
"""Rendimenti giornalieri di un portafoglio ribilanciato ogni giorno ai pesi dati."""
dr = pd.DataFrame({k: v.pct_change().fillna(0.0) for k, v in members.items()})
if weights is None:
return dr.mean(axis=1)
w = pd.Series(weights); w = w / w.sum()
return (dr * w).sum(axis=1)
def inv_vol_weights(members: dict[str, pd.Series], lo=0, hi=None) -> dict[str, float]:
"""Pesi inversamente proporzionali alla volatilita' (stimata sulla finestra train)."""
vol = {k: v.pct_change().iloc[lo:hi].std() for k, v in members.items()}
inv = {k: (1.0 / s if s and s > 0 else 0.0) for k, s in vol.items()}
tot = sum(inv.values())
return {k: x / tot for k, x in inv.items()}
# ---------------- report ----------------
def row(label, dr):
f = metrics(dr); o = metrics(dr, 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['cagr']:>7.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}")
def main():
print("Costruzione equity giornaliere (puo' richiedere ~1 min)...")
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")}
# --- correlazione cross-famiglia ---
dr = pd.DataFrame({k: v.pct_change().fillna(0.0) for k, v in S.items()})
corr = dr.corr()
fade_k, hon_k = list(fade), list(honest)
cross = corr.loc[fade_k, hon_k]
print("\n" + "=" * 92)
print(f" CORRELAZIONE rendimenti giornalieri — FADE (righe) vs HONEST (colonne) | {IDX[0].date()}->{IDX[-1].date()}")
print("=" * 92)
print(f" {'':<12s}" + "".join(f"{c:>13s}" for c in hon_k))
for f in fade_k:
print(f" {f:<12s}" + "".join(f"{cross.loc[f,c]:>13.2f}" for c in hon_k))
intra_fade = corr.loc[fade_k, fade_k].values[np.triu_indices(len(fade_k), 1)].mean()
intra_hon = corr.loc[hon_k, hon_k].values[np.triu_indices(len(hon_k), 1)].mean()
print(f"\n Corr media intra-FADE {intra_fade:+.2f} | intra-HONEST {intra_hon:+.2f} | "
f"cross-famiglia {cross.values.mean():+.2f} (piu' bassa = piu' diversificazione)")
# --- confronto portafogli ---
print("\n" + "=" * 92)
print(f" PORTAFOGLI equal-weight (ribil. giornaliero) | OOS da {OOS_DATE} | leva3x pos15%/sleeve")
print("=" * 92)
print(f" {'portafoglio':<26s}{'Ret%':>9s}{'CAGR':>7s}{'DD%':>7s}{'Shrp':>7s}"
f" | {'oRet%':>9s}{'oCAGR':>7s}{'oDD%':>7s}{'oShrp':>7s}")
print(" " + "-" * 88)
row("FADE only (8 sleeve)", port_returns(fade))
row("HONEST only (3 sleeve)", port_returns(honest))
row("ALL equal-weight (11)", port_returns(S))
# 50/50 fra le due famiglie (ogni famiglia equipesata al suo interno)
fr, hr = port_returns(fade), port_returns(honest)
row("ALL 50/50 famiglie", (fr + hr) / 2)
# inverse-vol sul train, applicato a tutti gli 11 sleeve
w = inv_vol_weights(S, lo=0, hi=SPLIT)
row("ALL inverse-vol", port_returns(S, w))
print(" " + "-" * 88)
print(" Sharpe annualizzato sui rendimenti giornalieri. Confronta DD e Sharpe:")
print(" se il combinato ha DD piu' basso e Sharpe piu' alto delle singole famiglie, combinare conviene.")
if __name__ == "__main__":
main()
+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()
-559
View File
@@ -1,559 +0,0 @@
"""Analisi finale — S1 (squeeze puro) vs Script 13 (squeeze+ML GBM).
Metriche: PnL, num trades, DD max, tempo medio a mercato, descrizione.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from src.data.downloader import load_data
from src.fractal.patterns import encode_candles
FEE_PERP = 0.002
FEE_ML = 0.001
INITIAL = 1000
LEVERAGE = 3
TF_MINUTES = {"1m": 1, "5m": 5, "15m": 15, "1h": 60, "4h": 240, "1d": 1440}
# ── helpers ──────────────────────────────────────────────────────────
def keltner_ratio(close, high, low, window=14):
n = len(close)
r = np.full(n, np.nan)
for i in range(window, n):
wc, wh, wl = close[i-window:i], high[i-window:i], low[i-window:i]
ma = np.mean(wc)
bb_std = np.std(wc)
tr = np.maximum(wh-wl, np.maximum(np.abs(wh-np.roll(wc,1)), np.abs(wl-np.roll(wc,1))))
atr = np.mean(tr[1:])
kc = (ma+1.5*atr)-(ma-1.5*atr)
bb = (ma+2*bb_std)-(ma-2*bb_std)
if kc > 0:
r[i] = bb/kc
return r
def detect_squeezes(close, high, low, kcr, sq_thr=0.8, min_dur=5):
events = []
in_sq = False
sq_start = 0
for i in range(1, len(close)):
if np.isnan(kcr[i]):
continue
is_sq = kcr[i] < sq_thr
if is_sq and not in_sq:
in_sq = True
sq_start = i
elif not is_sq and in_sq:
in_sq = False
dur = i - sq_start
if dur < min_dur:
continue
events.append({"idx": i, "dur": dur, "sq_start": sq_start,
"avg_vol_squeeze": np.mean(close[sq_start:i]),
"kcr_at_release": kcr[i]})
return events
def _build_result(yearly, capital, max_dd, all_t, all_w, time_pct, avg_dur_h):
acc = all_w / all_t * 100
tot_pnl = sum(p for d in yearly.values() for p in d["pnls"])
years_active = len(yearly)
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
sharpe = np.mean(all_pnls) / np.std(all_pnls) * np.sqrt(252) if len(all_pnls) > 1 and np.std(all_pnls) > 0 else 0
year_details = {}
for y in sorted(yearly):
d = yearly[y]
ya = d["w"] / d["t"] * 100 if d["t"] > 0 else 0
yp = sum(d["pnls"])
year_details[y] = {"trades": d["t"], "acc": ya, "pnl": yp}
valid_years = {y: d for y, d in year_details.items() if d["trades"] >= 10}
if valid_years:
worst_y = min(valid_years, key=lambda y: valid_years[y]["acc"])
worst_acc = valid_years[worst_y]["acc"]
elif year_details:
worst_y = min(year_details, key=lambda y: year_details[y]["acc"])
worst_acc = year_details[worst_y]["acc"]
else:
worst_y = "N/A"
worst_acc = 0
daily_pnl = tot_pnl / (years_active * 365) if years_active > 0 else 0
return {
"trades": all_t, "acc": acc, "pnl": tot_pnl, "capital": capital,
"max_dd": max_dd * 100, "sharpe": sharpe, "daily_pnl": daily_pnl,
"time_in_market_pct": time_pct, "avg_dur_h": avg_dur_h,
"years_active": years_active, "worst_year": str(worst_y),
"worst_acc": worst_acc, "year_details": year_details,
}
# ── S1: Squeeze breakout puro ────────────────────────────────────────
def run_s1_squeeze(asset, tf, hold=3):
df = load_data(asset, tf)
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
kcr = keltner_ratio(c, h, l, 14)
events = detect_squeezes(c, h, l, kcr)
yearly = {}
capital = float(INITIAL)
peak = capital
max_dd = 0
total_bars = 0
for ev in events:
i = ev["idx"]
if i + hold + 1 >= n:
continue
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
if abs(first_ret) < 0.001:
continue
direction = 1 if first_ret > 0 else -1
entry = c[i-1]
exit_price = c[min(i + hold - 1, n - 1)]
actual = (exit_price - entry) / entry * direction
net = actual * LEVERAGE - FEE_PERP * LEVERAGE
capital += capital * 0.15 * net
capital = max(capital, 10)
if capital > peak: peak = capital
dd = (peak - capital) / peak
max_dd = max(max_dd, dd)
total_bars += hold
year = ts.iloc[i].year
if year not in yearly:
yearly[year] = {"w": 0, "t": 0, "pnls": []}
yearly[year]["t"] += 1
if actual > 0: yearly[year]["w"] += 1
yearly[year]["pnls"].append(net * INITIAL)
all_t = sum(d["t"] for d in yearly.values())
all_w = sum(d["w"] for d in yearly.values())
if all_t == 0: return None
return _build_result(yearly, capital, max_dd, all_t, all_w,
total_bars / n * 100, hold * TF_MINUTES.get(tf, 60) / 60)
def run_s1_antifake_vol(asset, tf, hold=3):
df = load_data(asset, tf)
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
kcr = keltner_ratio(c, h, l, 14)
events = detect_squeezes(c, h, l, kcr)
yearly = {}
capital = float(INITIAL)
peak = capital
max_dd = 0
total_bars = 0
for ev in events:
i = ev["idx"]
if i + hold + 1 >= n:
continue
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
if abs(first_ret) < 0.001:
continue
br = h[i] - l[i]
if br > 0:
if c[i] > c[i-1]:
if (h[i] - c[i]) / br > 0.6:
continue
else:
if (c[i] - l[i]) / br > 0.6:
continue
avg_v = np.mean(v[ev["sq_start"]:i])
if avg_v > 0 and v[i] <= avg_v * 1.3:
continue
direction = 1 if first_ret > 0 else -1
entry = c[i-1]
exit_price = c[min(i + hold - 1, n - 1)]
actual = (exit_price - entry) / entry * direction
net = actual * LEVERAGE - FEE_PERP * LEVERAGE
capital += capital * 0.15 * net
capital = max(capital, 10)
if capital > peak: peak = capital
dd = (peak - capital) / peak
max_dd = max(max_dd, dd)
total_bars += hold
year = ts.iloc[i].year
if year not in yearly:
yearly[year] = {"w": 0, "t": 0, "pnls": []}
yearly[year]["t"] += 1
if actual > 0: yearly[year]["w"] += 1
yearly[year]["pnls"].append(net * INITIAL)
all_t = sum(d["t"] for d in yearly.values())
all_w = sum(d["w"] for d in yearly.values())
if all_t == 0: return None
return _build_result(yearly, capital, max_dd, all_t, all_w,
total_bars / n * 100, hold * TF_MINUTES.get(tf, 60) / 60)
# ── Script 13: Squeeze + ML ibrida (GBM walk-forward) ────────────────
def build_features_at(df, i, squeeze_info):
if i < 100:
return None
o = df["open"].values
h = df["high"].values
l = df["low"].values
c = df["close"].values
v = df["volume"].values
feats = []
for w in [12, 24, 48]:
win_c = c[i-w:i]
win_o = o[i-w:i]
win_h = h[i-w:i]
win_l = l[i-w:i]
win_v = v[i-w:i]
mn, mx = win_l.min(), max(win_h.max(), win_c.max())
rng = mx - mn if mx - mn > 0 else 1e-10
total = win_h - win_l
total = np.where(total == 0, 1e-10, total)
body = np.abs(win_c - win_o) / total
direction = np.sign(win_c - win_o)
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
rets = np.diff(log_c)
v_mean = np.mean(win_v)
feats.extend([
np.mean(rets) if len(rets) > 0 else 0,
np.std(rets) if len(rets) > 0 else 0,
np.sum(rets) if len(rets) > 0 else 0,
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
np.mean(body), np.std(body),
np.mean(direction), np.mean(direction[-min(3, w):]),
(win_c[-1] - mn) / rng,
win_v[-1] / v_mean if v_mean > 0 else 1,
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
])
sq = squeeze_info
feats.extend([
sq["dur"], sq["dur"] / 24, sq["kcr_at_release"],
v[i-1] / sq["avg_vol_squeeze"] if sq["avg_vol_squeeze"] > 0 else 1,
np.mean(v[i:min(i+3, len(v))]) / sq["avg_vol_squeeze"] if sq["avg_vol_squeeze"] > 0 else 1,
])
h48 = np.max(h[max(0, i-48):i])
l48 = np.min(l[max(0, i-48):i])
r48 = h48 - l48
feats.append((c[i-1] - l48) / r48 if r48 > 0 else 0.5)
tr = np.maximum(h[i-14:i] - l[i-14:i],
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
atr = np.mean(tr[1:])
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
feats.append(first_ret)
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
def run_s13_hybrid(asset, tf, bb_w, sq_thr, brk_bars, leverage, pos_pct, ml_thr):
df = load_data(asset, tf)
close = df["close"].values
high = df["high"].values
low = df["low"].values
volume = df["volume"].values
n = len(df)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
kcr = keltner_ratio(close, high, low, bb_w)
events = detect_squeezes(close, high, low, kcr, sq_thr)
X_all, y_all, ev_all = [], [], []
for ev in events:
i = ev["idx"]
if i + brk_bars >= n or i < 100:
continue
feats = build_features_at(df, i, ev)
if feats is None:
continue
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
X_all.append(feats)
y_all.append(1 if actual_ret > 0 else 0)
ev_all.append(ev)
if len(X_all) < 50:
return None
X = np.array(X_all)
y = np.array(y_all)
TRAIN_SIZE = max(int(len(X) * 0.5), 50)
STEP_SIZE = max(int(len(X) * 0.1), 10)
yearly = {}
capital = float(INITIAL)
peak = capital
max_dd = 0
total_bars = 0
all_t = 0
all_w = 0
start = 0
while start + TRAIN_SIZE + STEP_SIZE <= len(X):
train_end = start + TRAIN_SIZE
test_end = min(train_end + STEP_SIZE, len(X))
X_tr, y_tr = X[start:train_end], y[start:train_end]
X_te = X[train_end:test_end]
if len(np.unique(y_tr)) < 2:
start += STEP_SIZE
continue
scaler = StandardScaler()
X_tr_s = scaler.fit_transform(X_tr)
X_te_s = scaler.transform(X_te)
model = GradientBoostingClassifier(
n_estimators=150, max_depth=4, min_samples_leaf=10,
learning_rate=0.05, subsample=0.8, random_state=42,
)
model.fit(X_tr_s, y_tr)
up_idx = list(model.classes_).index(1) if 1 in model.classes_ else -1
if up_idx < 0:
start += STEP_SIZE
continue
for j in range(len(X_te)):
proba = model.predict_proba(X_te_s[j:j+1])[0]
p_up = proba[up_idx]
ev = ev_all[train_end + j]
i = ev["idx"]
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
direction = None
if p_up >= ml_thr:
direction = 1
elif p_up <= (1 - ml_thr):
direction = -1
if direction is None:
continue
is_correct = (direction == 1 and actual_ret > 0) or (direction == -1 and actual_ret < 0)
trade_ret = actual_ret * direction
net = trade_ret * leverage - FEE_ML * 2 * leverage
capital += capital * pos_pct * net
capital = max(capital, 10)
if capital > peak: peak = capital
dd = (peak - capital) / peak
max_dd = max(max_dd, dd)
total_bars += brk_bars
all_t += 1
if is_correct: all_w += 1
year = ts.iloc[i].year
if year not in yearly:
yearly[year] = {"w": 0, "t": 0, "pnls": []}
yearly[year]["t"] += 1
if is_correct: yearly[year]["w"] += 1
yearly[year]["pnls"].append(net * INITIAL)
start += STEP_SIZE
if all_t == 0:
return None
return _build_result(yearly, capital, max_dd, all_t, all_w,
total_bars / n * 100, brk_bars * TF_MINUTES.get(tf, 60) / 60)
# ═══════════════════════════════════════════════════════════════════
# ESECUZIONE
# ═══════════════════════════════════════════════════════════════════
print("Calcolo in corso...\n")
strategies = []
def add(name, desc, cat, result):
if result and result["trades"] >= 20:
strategies.append({"name": name, "desc": desc, "cat": cat, **result})
# ── S1: Squeeze puro ────────────────────────────────────────────
add("S1 Squeeze BTC 15m", "Squeeze breakout puro, BBw=14, hold 3×15m, leva 3x",
"S1", run_s1_squeeze("BTC", "15m"))
add("S1 Squeeze ETH 15m", "Squeeze breakout puro, BBw=14, hold 3×15m, leva 3x",
"S1", run_s1_squeeze("ETH", "15m"))
add("S1 Squeeze BTC 1h", "Squeeze breakout puro, BBw=14, hold 3×1h, leva 3x",
"S1", run_s1_squeeze("BTC", "1h"))
add("S1 Squeeze ETH 1h", "Squeeze breakout puro, BBw=14, hold 3×1h, leva 3x",
"S1", run_s1_squeeze("ETH", "1h"))
add("S1 AF+Vol BTC 15m", "Squeeze + antifakeout + volume confirm >1.3× media",
"S1", run_s1_antifake_vol("BTC", "15m"))
add("S1 AF+Vol ETH 15m", "Squeeze + antifakeout + volume confirm >1.3× media",
"S1", run_s1_antifake_vol("ETH", "15m"))
add("S1 AF+Vol BTC 1h", "Squeeze + antifakeout + volume confirm >1.3× media",
"S1", run_s1_antifake_vol("BTC", "1h"))
add("S1 AF+Vol ETH 1h", "Squeeze + antifakeout + volume confirm >1.3× media",
"S1", run_s1_antifake_vol("ETH", "1h"))
# ── Script 13: Squeeze + ML (GBM walk-forward) ─────────────────
print(" Training ML models...")
add("S13 ETH 15m bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 15% pos",
"S13", run_s13_hybrid("ETH", "15m", 14, 0.8, 3, 3, 0.15, 0.70))
add("S13 ETH 15m bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.65, 3x leva 15% pos",
"S13", run_s13_hybrid("ETH", "15m", 14, 0.8, 3, 3, 0.15, 0.65))
add("S13 ETH 15m bb20 ml70", "Squeeze+GBM walk-forward, BBw=20 sq=0.9 ml≥0.70, 3x leva 15% pos",
"S13", run_s13_hybrid("ETH", "15m", 20, 0.9, 3, 3, 0.15, 0.70))
add("S13 BTC 15m bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.9 ml≥0.70, 3x leva 15% pos",
"S13", run_s13_hybrid("BTC", "15m", 14, 0.9, 3, 3, 0.15, 0.70))
add("S13 BTC 15m bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.9 ml≥0.65, 3x leva 15% pos",
"S13", run_s13_hybrid("BTC", "15m", 14, 0.9, 3, 3, 0.15, 0.65))
add("S13 BTC 1h bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 20% pos",
"S13", run_s13_hybrid("BTC", "1h", 14, 0.8, 3, 3, 0.20, 0.70))
add("S13 BTC 1h bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.65, 3x leva 20% pos",
"S13", run_s13_hybrid("BTC", "1h", 14, 0.8, 3, 3, 0.20, 0.65))
add("S13 ETH 1h bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 20% pos",
"S13", run_s13_hybrid("ETH", "1h", 14, 0.8, 3, 3, 0.20, 0.70))
strategies.sort(key=lambda x: x["acc"], reverse=True)
# ═══════════════════════════════════════════════════════════════════
# TABELLA 1: Classifica
# ═══════════════════════════════════════════════════════════════════
W = 150
print("=" * W)
print(" S1 (SQUEEZE PURO) vs S13 (SQUEEZE + GBM) — CLASSIFICA FINALE")
print(f" Fee: 0.2% RT. Dati OHLCV reali 2018-2026. Position 15%. Leva 3x.")
print("=" * W)
hdr = (f" {'#':>2s} {'Cat':>3s} {'Nome':<26s} {'Trades':>6s} {'Acc':>6s} "
f"{'PnL€':>9s} {'DD%':>6s} {'€/day':>7s} {'Sharpe':>7s} "
f"{'Mkt%':>5s} {'Dur':>6s} {'Worst':>12s} {'Anni':>4s}")
print(hdr)
print(f" {''*(W-4)}")
for idx, s in enumerate(strategies, 1):
worst = f"{s['worst_year']}({s['worst_acc']:.0f}%)"
dur_str = f"{s['avg_dur_h']:.0f}h" if s['avg_dur_h'] >= 1 else f"{s['avg_dur_h']*60:.0f}m"
tag = " ★★" if s["acc"] >= 78 else "" if s["acc"] >= 76 else ""
print(f" {idx:>2d} {s['cat']:>3s} {s['name']:<26s} {s['trades']:>6d} {s['acc']:>5.1f}% "
f"{s['pnl']:>+8.0f} {s['max_dd']:>5.1f}% {s['daily_pnl']:>+6.2f} {s['sharpe']:>7.2f} "
f"{s['time_in_market_pct']:>4.1f}% {dur_str:>6s} {worst:>12s} {s['years_active']:>4d}{tag}")
# ═══════════════════════════════════════════════════════════════════
# TABELLA 2: Descrizione
# ═══════════════════════════════════════════════════════════════════
print(f"\n\n{'=' * W}")
print(" DESCRIZIONE")
print(f"{'=' * W}")
print(f" {'#':>2s} {'Nome':<26s} {'Descrizione'}")
print(f" {''*(W-4)}")
for idx, s in enumerate(strategies, 1):
print(f" {idx:>2d} {s['name']:<26s} {s['desc']}")
# ═══════════════════════════════════════════════════════════════════
# TABELLA 3: Breakdown per anno
# ═══════════════════════════════════════════════════════════════════
top_n = min(12, len(strategies))
top = strategies[:top_n]
all_years = sorted(set(y for s in top for y in s["year_details"]))
print(f"\n\n{'=' * W}")
print(f" BREAKDOWN PER ANNO — TOP {top_n} (accuracy% / trades)")
print(f"{'=' * W}")
header = f" {'Nome':<26s}"
for y in all_years:
header += f" {y:>10d}"
print(header)
print(f" {''*(W-4)}")
for s in top:
line = f" {s['name']:<26s}"
for y in all_years:
if y in s["year_details"]:
d = s["year_details"][y]
line += f" {d['acc']:>4.0f}%/{d['trades']:<4d}"
else:
line += f" {'':>10s}"
print(line)
# ═══════════════════════════════════════════════════════════════════
# TABELLA 4: Robustezza
# ═══════════════════════════════════════════════════════════════════
print(f"\n\n{'=' * W}")
print(f" ANALISI ROBUSTEZZA")
print(f"{'=' * W}")
print(f" {'#':>2s} {'Nome':<26s} {'MinAcc':>7s} {'MaxAcc':>7s} {'Spread':>7s} "
f"{'AnniOK':>7s} {'€/trade':>8s} {'Verdict':<12s}")
print(f" {''*90}")
for idx, s in enumerate(strategies, 1):
yd = s["year_details"]
valid = {y: d for y, d in yd.items() if d["trades"] >= 10}
accs = [d["acc"] for d in (valid if valid else yd).values()]
if not accs:
continue
min_a, max_a = min(accs), max(accs)
spread = max_a - min_a
years_ok = sum(1 for a in accs if a >= 70)
avg_pnl = s["pnl"] / s["trades"] if s["trades"] > 0 else 0
n_valid = len(valid if valid else yd)
if n_valid < 4:
verdict = "⚠ CORTO"
elif min_a < 60:
verdict = "⚠ FRAGILE"
elif min_a >= 72 and s["acc"] >= 77:
verdict = "✅ SOLIDO"
elif min_a >= 65 and s["acc"] >= 74:
verdict = "~ BUONO"
else:
verdict = "~ OK"
print(f" {idx:>2d} {s['name']:<26s} {min_a:>6.1f}% {max_a:>6.1f}% {spread:>6.1f}% "
f"{years_ok:>3d}/{n_valid:<3d}{avg_pnl:>+7.1f} {verdict:<12s}")
# ═══════════════════════════════════════════════════════════════════
# VERDETTO
# ═══════════════════════════════════════════════════════════════════
print(f"\n\n{'=' * W}")
print(f" VERDETTO FINALE")
print(f"{'=' * W}")
solidi = [s for s in strategies if s["trades"] >= 200 and s["years_active"] >= 5 and s["worst_acc"] >= 65]
solidi_s1 = [s for s in solidi if s["cat"] == "S1"]
solidi_ml = [s for s in solidi if s["cat"] == "S13"]
solidi_s1.sort(key=lambda x: x["acc"], reverse=True)
solidi_ml.sort(key=lambda x: x["daily_pnl"], reverse=True)
if solidi_s1:
b = solidi_s1[0]
print(f"\n MIGLIORE S1 (regole pure, facile da deployare):")
print(f" {b['name']}{b['acc']:.1f}% acc, {b['trades']} trades, DD {b['max_dd']:.1f}%, €{b['daily_pnl']:+.2f}/day, Sharpe {b['sharpe']:.2f}")
if solidi_ml:
m = solidi_ml[0]
print(f"\n MIGLIORE S13 (squeeze+GBM, più complesso):")
print(f" {m['name']}{m['acc']:.1f}% acc, {m['trades']} trades, DD {m['max_dd']:.1f}%, €{m['daily_pnl']:+.2f}/day, Sharpe {m['sharpe']:.2f}")
max_pnl = max(strategies, key=lambda x: x["pnl"])
print(f"\n MAX PnL: {max_pnl['name']} — €{max_pnl['pnl']:+,.0f}")
+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)
-298
View File
@@ -1,298 +0,0 @@
"""Report finale: TOP 5 metodi + simulazione crescita capitale €1000 → €50/giorno."""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
from src.data.downloader import load_data
print("=" * 70)
print(" REPORT FINALE — TOP 5 METODI")
print(" Target: accuracy >80%, ROI annuo >30%, €50/giorno da €1000")
print("=" * 70)
# Metodo 1: Squeeze Breakout ETH 1h (BBw=20, sqThr=0.8, volume confirmed)
# Metodo 2: Squeeze Breakout ETH 1h (BBw=30, sqThr=0.9, senza vol filter)
# Metodo 3: Squeeze Breakout BTC+ETH combinato
# Metodo 4: Squeeze Breakout 15m (alta frequenza)
# Metodo 5: GBM Structural + Squeeze filter (ibrido ML + strutturale)
FEE = 0.001
LEVERAGE = 3
INITIAL = 1000
def bollinger_bandwidth(close, window=20):
n = len(close)
result = np.full(n, np.nan)
for i in range(window, n):
w = close[i-window:i]
ma = np.mean(w)
std = np.std(w)
if ma > 0:
result[i] = (2 * 2 * std) / ma
return result
def keltner_ratio(close, high, low, window=20):
n = len(close)
result = np.full(n, np.nan)
for i in range(window, n):
wc = close[i-window:i]
wh = high[i-window:i]
wl = low[i-window:i]
ma = np.mean(wc)
bb_std = np.std(wc)
tr = np.maximum(wh - wl, np.maximum(np.abs(wh - np.roll(wc,1)), np.abs(wl - np.roll(wc,1))))
atr = np.mean(tr[1:])
kc_r = (ma + 1.5*atr) - (ma - 1.5*atr)
bb_r = (ma + 2*bb_std) - (ma - 2*bb_std)
if kc_r > 0:
result[i] = bb_r / kc_r
return result
def run_squeeze_backtest(close, high, low, volume, bb_w, sq_thr, brk_bars, vol_filter, split_pct=0.7, leverage=3, pos_pct=0.2):
n = len(close)
split = int(n * split_pct)
kcr = keltner_ratio(close, high, low, bb_w)
in_sq = False
sq_start = 0
capital = float(INITIAL)
equity = [capital]
trades = []
for i in range(bb_w + 1, n):
if np.isnan(kcr[i]):
continue
is_sq = kcr[i] < sq_thr
if is_sq and not in_sq:
in_sq = True
sq_start = i
elif not is_sq and in_sq:
in_sq = False
duration = i - sq_start
if duration < 5 or i < split or i + brk_bars >= n:
continue
# Volume check
if vol_filter:
avg_v = np.mean(volume[sq_start:i])
brk_v = np.mean(volume[i:i+brk_bars])
if avg_v > 0 and brk_v < avg_v * 1.3:
continue
first_ret = (close[i] - close[i-1]) / close[i-1]
if abs(first_ret) < 0.001:
continue
direction = 1 if first_ret > 0 else -1
actual = (close[i+brk_bars-1] - close[i-1]) / close[i-1]
is_correct = (direction == 1 and actual > 0) or (direction == -1 and actual < 0)
trade_ret = actual * direction
net = trade_ret * leverage - FEE * 2 * leverage
pnl = capital * pos_pct * net
capital += pnl
capital = max(capital, 0)
equity.append(capital)
trades.append({
"correct": is_correct,
"actual_ret": actual,
"net_pnl": pnl,
"capital_after": capital,
})
if not trades:
return None
correct = sum(1 for t in trades if t["correct"])
acc = correct / len(trades) * 100
total_ret = (capital - INITIAL) / INITIAL * 100
test_candles = n - split
test_days = test_candles / 24
test_years = test_days / 365.25
ann = ((capital / INITIAL) ** (1/test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
daily_pnl = (capital - INITIAL) / test_days if test_days > 0 else 0
peak = equity[0]
max_dd = 0
for v in equity:
if v > peak: peak = v
dd = (peak - v) / peak if peak > 0 else 0
max_dd = max(max_dd, dd)
return {
"trades": len(trades),
"accuracy": acc,
"total_return": total_ret,
"annualized": ann,
"max_drawdown": max_dd * 100,
"final_capital": capital,
"daily_pnl": daily_pnl,
"trades_per_year": len(trades) / test_years if test_years > 0 else 0,
}
methods = []
# --- Metodo 1: ETH 1h, BBw=20, sqThr=0.8, vol confirmed ---
df_eth = load_data("ETH", "1h")
r1 = run_squeeze_backtest(df_eth["close"].values, df_eth["high"].values, df_eth["low"].values, df_eth["volume"].values,
bb_w=20, sq_thr=0.8, brk_bars=3, vol_filter=True)
methods.append(("M1: ETH 1h Squeeze+Vol (BBw=20,sq=0.8)", r1))
# --- Metodo 2: ETH 1h, BBw=30, sqThr=0.9, no vol ---
r2 = run_squeeze_backtest(df_eth["close"].values, df_eth["high"].values, df_eth["low"].values, df_eth["volume"].values,
bb_w=30, sq_thr=0.9, brk_bars=3, vol_filter=False)
methods.append(("M2: ETH 1h Squeeze (BBw=30,sq=0.9)", r2))
# --- Metodo 3: BTC+ETH combinato ---
df_btc = load_data("BTC", "1h")
r3a = run_squeeze_backtest(df_btc["close"].values, df_btc["high"].values, df_btc["low"].values, df_btc["volume"].values,
bb_w=14, sq_thr=0.8, brk_bars=3, vol_filter=False, pos_pct=0.1)
r3b = run_squeeze_backtest(df_eth["close"].values, df_eth["high"].values, df_eth["low"].values, df_eth["volume"].values,
bb_w=20, sq_thr=0.8, brk_bars=3, vol_filter=False, pos_pct=0.1)
if r3a and r3b:
combined_trades = r3a["trades"] + r3b["trades"]
combined_correct = int(r3a["accuracy"]/100 * r3a["trades"]) + int(r3b["accuracy"]/100 * r3b["trades"])
combined_acc = combined_correct / combined_trades * 100 if combined_trades > 0 else 0
# Simulate portfolio
cap = float(INITIAL)
# Rough estimate: alternate between assets
for r in [r3a, r3b]:
ret_per_trade = r["total_return"] / 100 / r["trades"] if r["trades"] > 0 else 0
for _ in range(r["trades"]):
cap *= (1 + ret_per_trade * 0.5)
r3 = {
"trades": combined_trades,
"accuracy": combined_acc,
"total_return": (cap - INITIAL) / INITIAL * 100,
"annualized": r3a["annualized"] * 0.5 + r3b["annualized"] * 0.5,
"max_drawdown": max(r3a["max_drawdown"], r3b["max_drawdown"]),
"final_capital": cap,
"daily_pnl": r3a["daily_pnl"] + r3b["daily_pnl"],
"trades_per_year": r3a["trades_per_year"] + r3b["trades_per_year"],
}
methods.append(("M3: BTC+ETH 1h Portafoglio Squeeze", r3))
# --- Metodo 4: BTC 15m alta frequenza ---
df_btc_15 = load_data("BTC", "15m")
r4 = run_squeeze_backtest(df_btc_15["close"].values, df_btc_15["high"].values, df_btc_15["low"].values, df_btc_15["volume"].values,
bb_w=14, sq_thr=0.9, brk_bars=3, vol_filter=True)
methods.append(("M4: BTC 15m Squeeze+Vol alta freq", r4))
# --- Metodo 5: ETH 1h squeeze aggressivo ---
r5 = run_squeeze_backtest(df_eth["close"].values, df_eth["high"].values, df_eth["low"].values, df_eth["volume"].values,
bb_w=20, sq_thr=0.8, brk_bars=3, vol_filter=False, leverage=3)
methods.append(("M5: ETH 1h Squeeze aggressivo (no vol)", r5))
# --- Print results ---
print("\n")
for i, (name, r) in enumerate(methods, 1):
if r is None:
print(f" {name}: NO TRADES")
continue
print(f" {'='*65}")
print(f" #{i}{name}")
print(f" {'='*65}")
print(f" Trades: {r['trades']}")
print(f" Accuracy: {r['accuracy']:.1f}% {'' if r['accuracy'] >= 80 else '⚠️' if r['accuracy'] >= 70 else ''}")
print(f" Return totale: {r['total_return']:+.1f}%")
print(f" Return annuo: {r['annualized']:+.1f}% {'' if r['annualized'] >= 30 else '⚠️' if r['annualized'] >= 15 else ''}")
print(f" Max Drawdown: {r['max_drawdown']:.1f}%")
print(f" Capitale finale: €{r['final_capital']:.0f}")
print(f" €/giorno media: €{r['daily_pnl']:.2f}")
print(f" Trades/anno: {r['trades_per_year']:.0f}")
print()
# --- Simulazione crescita 6 mesi ---
print("\n" + "=" * 70)
print(" SIMULAZIONE CRESCITA CAPITALE — 6 MESI")
print(" Metodo: M1 (ETH 1h Squeeze+Vol) — il più preciso (83.9%)")
print("=" * 70)
# M1 params: ~87 trades in ~2.5 anni test = ~35 trades/anno = ~3 al mese
# Accuracy: 83.9%, average return per trade with 3x leverage
# Simulo con dati reali: prendo i trade dal test period
close = df_eth["close"].values
high = df_eth["high"].values
low = df_eth["low"].values
volume = df_eth["volume"].values
n = len(close)
split = int(n * 0.7)
kcr = keltner_ratio(close, high, low, 20)
in_sq = False
sq_start = 0
all_trade_rets = []
for i in range(21, n):
if np.isnan(kcr[i]):
continue
is_sq = kcr[i] < 0.8
if is_sq and not in_sq:
in_sq = True
sq_start = i
elif not is_sq and in_sq:
in_sq = False
if i - sq_start < 5 or i < split or i + 3 >= n:
continue
avg_v = np.mean(volume[sq_start:i])
brk_v = np.mean(volume[i:i+3])
if avg_v > 0 and brk_v < avg_v * 1.3:
continue
first_ret = (close[i] - close[i-1]) / close[i-1]
if abs(first_ret) < 0.001:
continue
direction = 1 if first_ret > 0 else -1
actual = (close[i+2] - close[i-1]) / close[i-1]
trade_ret = actual * direction
all_trade_rets.append(trade_ret)
avg_win = np.mean([r for r in all_trade_rets if r > 0]) if any(r > 0 for r in all_trade_rets) else 0
avg_loss = np.mean([r for r in all_trade_rets if r <= 0]) if any(r <= 0 for r in all_trade_rets) else 0
win_rate = sum(1 for r in all_trade_rets if r > 0) / len(all_trade_rets)
print(f"\n Statistiche trade:")
print(f" Win rate: {win_rate*100:.1f}%")
print(f" Avg win: {avg_win*100:.2f}%")
print(f" Avg loss: {avg_loss*100:.2f}%")
print(f" Trades totali nel test: {len(all_trade_rets)}")
print(f" Trades/mese stimati: ~{len(all_trade_rets) / 30:.0f}")
print(f"\n Crescita simulata mese per mese (€1000 iniziali, leva 3x, 20% per trade):")
capital = 1000.0
monthly_trades = max(len(all_trade_rets) // 30, 3)
# Shuffle trades to simulate different sequences
np.random.seed(42)
for month in range(1, 7):
n_trades = monthly_trades
month_rets = np.random.choice(all_trade_rets, size=n_trades, replace=True)
for ret in month_rets:
net = ret * LEVERAGE - FEE * 2 * LEVERAGE
capital += capital * 0.2 * net
capital = max(capital, 10)
daily_pnl = capital * 0.003 # stima conservativa 0.3% daily basata su performance
print(f" Mese {month}: capitale €{capital:.0f}, €/giorno stima: €{daily_pnl:.1f}")
print(f"\n Capitale dopo 6 mesi: €{capital:.0f}")
print(f" €/giorno necessari: €50")
print(f" €/giorno ottenibili (0.5% daily su capitale): €{capital * 0.005:.1f}")
if capital * 0.005 >= 50:
print(f"\n ✅ TARGET RAGGIUNGIBILE: con €{capital:.0f} di capitale, 0.5% daily = €{capital*0.005:.0f}/giorno")
else:
needed = 50 / 0.005
print(f"\n ⚠️ Servono €{needed:.0f} di capitale per €50/giorno al 0.5% daily")
print(f" Raggiungibile estendendo il periodo di crescita a ~{int(np.log(needed/1000) / np.log(1 + 0.15) + 0.5)} mesi")
-175
View File
@@ -1,175 +0,0 @@
"""Strategie candidate ONESTE + sweep multi-asset/tf con verdetto.
Ogni generatore restituisce una lista di entries {i,d,tp,sl,max_bars} usando
SOLO dati fino a close[i]. L'engine (honest_lab.simulate) entra a close[i].
Famiglie testate (meccanismi distinti, per diversificazione):
MR mean-reversion single-asset (Bollinger fade, RSI revert, Z-score)
XS cross-sectional relative-value (fade della divergenza vs paniere)
MOM time-series momentum / trend su timeframe alto
SES seasonality (ora del giorno UTC)
"""
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 ( # noqa: E402
atr, rsi, ema, get_df, simulate, oos_split, verdict,
available_assets, FEE_RT,
)
# ============================================================================
# MR — mean reversion single-asset
# ============================================================================
def bollinger_fade(df, n=50, k=2.5, sl_atr=2.0, max_bars=24):
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
up, lo = ma + k * sd, ma - k * sd
ents = []
for i in range(n + 14, len(c)):
if np.isnan(up[i]) or np.isnan(a[i]):
continue
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def rsi_revert(df, n=14, lo=25, hi=75, sl_atr=2.5, max_bars=24, ma_n=20):
c = df["close"].values
r = rsi(c, n)
ma = pd.Series(c).rolling(ma_n).mean().values
a = atr(df, 14)
ents = []
for i in range(max(n, ma_n) + 1, len(c)):
if np.isnan(r[i]) or np.isnan(ma[i]) or np.isnan(a[i]):
continue
if r[i - 1] < lo <= r[i]:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif r[i - 1] > hi >= r[i]:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def zscore_revert(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24):
"""Entra quando close e' a |z|>z_in std dalla media; TP alla media."""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
z = (c - ma) / sd
ents = []
for i in range(n + 14, len(c)):
if np.isnan(z[i]) or np.isnan(a[i]) or sd[i] == 0:
continue
if z[i] <= -z_in and z[i - 1] > -z_in:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif z[i] >= z_in and z[i - 1] < z_in:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
# ============================================================================
# MOM — time-series momentum / trend (timeframe alto, niente breakout intrabar)
# ============================================================================
def ema_trend(df, fast=20, slow=50, sl_atr=3.0, tp_atr=10.0, max_bars=240):
"""Trend following: cross EMA fast/slow deciso a close[i], TP/SL ad ATR."""
c = df["close"].values
ef, es = ema(c, fast), ema(c, slow)
a = atr(df, 14)
ents = []
for i in range(slow + 14, len(c)):
if np.isnan(a[i]):
continue
cross_up = ef[i] > es[i] and ef[i - 1] <= es[i - 1]
cross_dn = ef[i] < es[i] and ef[i - 1] >= es[i - 1]
if cross_up:
ents.append({"i": i, "d": 1, "tp": c[i] + tp_atr * a[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif cross_dn:
ents.append({"i": i, "d": -1, "tp": c[i] - tp_atr * a[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
# ============================================================================
# SES — seasonality (ora del giorno UTC). Direzione fissa decisa solo dall'ora.
# ============================================================================
def time_of_day(df, hour_long=None, hour_short=None, hold=6):
"""Entra a close della candela all'ora UTC indicata, esce dopo `hold` barre
(no TP/SL: tp/sl messi a +-inf cosi' esce solo a time-limit)."""
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
c = df["close"].values
hours = ts.dt.hour.values
hour_long = set(hour_long or [])
hour_short = set(hour_short or [])
ents = []
for i in range(1, len(c)):
if hours[i] in hour_long:
ents.append({"i": i, "d": 1, "tp": np.inf, "sl": -np.inf, "max_bars": hold})
elif hours[i] in hour_short:
ents.append({"i": i, "d": -1, "tp": -np.inf, "sl": np.inf, "max_bars": hold})
return ents
# ============================================================================
# sweep
# ============================================================================
def run_sweep(generators: dict, assets: list[str], tfs: list[str]):
print("=" * 130)
print(f" HONEST LAB — NETTO fee {FEE_RT*100:.2f}% RT | leva 3x | pos 15% | OOS ultimo 30%")
print("=" * 130)
print(f" {'Strategia':<26s}{'Asset':>5s}{'TF':>5s}{'Trd':>6s}{'Win%':>7s}"
f"{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'Exp%':>6s}{'AnniPos':>9s}{'OK':>4s}")
print(" " + "-" * 126)
survivors = []
for label, (fn, params) in generators.items():
for asset in assets:
for tf in tfs:
try:
df = get_df(asset, tf)
except Exception:
continue
ents = fn(df, **params)
if len(ents) < 30:
continue
full = simulate(ents, df)
_, oos_e = oos_split(ents, df)
oos = simulate(oos_e, df)
ok = verdict(full, oos)
flag = " OK" if ok else ""
print(f" {label:<26s}{asset:>5s}{tf:>5s}{full.trades:>6d}{full.win:>7.1f}"
f"{full.ret:>+9.0f}{oos.ret:>+9.0f}{full.dd:>6.0f}{full.exposure:>6.0f}"
f"{f'{full.pos_years}/{full.n_years}':>9s}{flag:>4s}")
if ok:
survivors.append((label, asset, tf, full, oos))
print(" " + "-" * 126)
return survivors
GENERATORS = {
"MR_boll n50 k2.5": (bollinger_fade, dict(n=50, k=2.5, sl_atr=2.0, max_bars=24)),
"MR_boll n20 k2.5": (bollinger_fade, dict(n=20, k=2.5, sl_atr=2.0, max_bars=24)),
"MR_rsi 25/75": (rsi_revert, dict(n=14, lo=25, hi=75, sl_atr=2.5, max_bars=24)),
"MR_zscore z2.5": (zscore_revert, dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24)),
"MR_zscore z3": (zscore_revert, dict(n=50, z_in=3.0, sl_atr=2.5, max_bars=24)),
"MOM_ema 20/50": (ema_trend, dict(fast=20, slow=50, sl_atr=3.0, tp_atr=10.0, max_bars=240)),
}
if __name__ == "__main__":
assets = available_assets()
print("Asset disponibili:", assets)
survivors = run_sweep(GENERATORS, assets, ["1h", "4h"])
print(f"\n SOPRAVVISSUTI (FULL+OOS+anni+DD): {len(survivors)}")
for label, a, tf, full, oos in survivors:
print(f" {label:<26s} {a} {tf} FULL {full.ret:+.0f}% OOS {oos.ret:+.0f}% DD {full.dd:.0f}%")
-73
View File
@@ -1,73 +0,0 @@
"""Diagnostica: perche' la mean-reversion simmetrica perde su asset trending?
Test: long-only vs short-only, e MR FILTRATA DAL TREND (buy-dip in uptrend,
sell-rip in downtrend) per evitare di fadeare i trend forti.
"""
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 ( # noqa: E402
atr, ema, get_df, simulate, oos_split, available_assets, FEE_RT,
)
def zscore_entries(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24,
trend_n=0, side="both"):
"""Z-score revert con filtro trend opzionale.
trend_n>0: EMA di lungo periodo. Long solo se close>EMA (uptrend),
short solo se close<EMA (downtrend).
side: 'both' | 'long' | 'short'
"""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
z = (c - ma) / np.where(sd == 0, np.nan, sd)
et = ema(c, trend_n) if trend_n > 0 else None
start = max(n + 14, trend_n + 1 if trend_n else 0)
ents = []
for i in range(start, len(c)):
if np.isnan(z[i]) or np.isnan(a[i]):
continue
long_ok = (et is None or c[i] > et[i]) and side in ("both", "long")
short_ok = (et is None or c[i] < et[i]) and side in ("both", "short")
if z[i] <= -z_in and z[i - 1] > -z_in and long_ok:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif z[i] >= z_in and z[i - 1] < z_in and short_ok:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def row(label, df, ents):
if len(ents) < 20:
print(f" {label:<34s} {'<20 trd':>50s}")
return None
full = simulate(ents, df)
_, oe = oos_split(ents, df)
oos = simulate(oe, df)
print(f" {label:<34s}{full.trades:>6d}{full.win:>7.1f}{full.ret:>+9.0f}"
f"{oos.ret:>+9.0f}{full.dd:>6.0f}{f'{full.pos_years}/{full.n_years}':>8s}")
return full, oos
if __name__ == "__main__":
assets = available_assets()
print(f"HONEST DIAG — z-score revert, fee {FEE_RT*100:.2f}% RT, leva 3x | OOS 30%")
for tf in ["1h"]:
for a in assets:
df = get_df(a, tf)
print(f"\n === {a} {tf} === {'Trd':>5s}{'Win%':>7s}{'FULL%':>8s}{'OOS%':>8s}{'DD%':>6s}{'AnniP':>8s}")
base = dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24)
row("both, no filter", df, zscore_entries(df, **base, side="both"))
row("long-only, no filter", df, zscore_entries(df, **base, side="long"))
row("short-only, no filter", df, zscore_entries(df, **base, side="short"))
row("both + trend200 filter", df, zscore_entries(df, **base, trend_n=200, side="both"))
row("both + trend500 filter", df, zscore_entries(df, **base, trend_n=500, side="both"))
row("long + trend200 filter", df, zscore_entries(df, **base, trend_n=200, side="long"))
-64
View File
@@ -1,64 +0,0 @@
"""Diag2: long-MR sempre + short-MR SOLO in downtrend confermato (close<EMA_t).
Idea: il dip-buying funziona su tutti gli asset (drift rialzista crypto); lo
short funziona solo quando il trend e' gia' giu' -> shortare i rimbalzi in
downtrend, mai i rimbalzi in bull-run.
"""
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 ( # noqa: E402
atr, ema, get_df, simulate, oos_split, available_assets, FEE_RT,
)
def regime_mr(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24, trend_n=200,
allow_short=True):
"""Long su z<=-z_in SEMPRE. Short su z>=+z_in solo se close<EMA(trend_n)."""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
z = (c - ma) / np.where(sd == 0, np.nan, sd)
et = ema(c, trend_n)
start = max(n + 14, trend_n + 1)
ents = []
for i in range(start, len(c)):
if np.isnan(z[i]) or np.isnan(a[i]):
continue
if z[i] <= -z_in and z[i - 1] > -z_in:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif allow_short and z[i] >= z_in and z[i - 1] < z_in and c[i] < et[i]:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def show(label, df, ents):
if len(ents) < 20:
print(f" {label:<30s} <20 trd"); return None
full = simulate(ents, df); _, oe = oos_split(ents, df); oos = simulate(oe, df)
print(f" {label:<30s}{full.trades:>6d}{full.win:>7.1f}{full.ret:>+9.0f}"
f"{oos.ret:>+9.0f}{full.dd:>6.0f}{f'{full.pos_years}/{full.n_years}':>8s}")
return full, oos
if __name__ == "__main__":
assets = available_assets()
print(f"DIAG2 — regime MR (long sempre + short in downtrend) fee {FEE_RT*100:.2f}% leva3x OOS30%")
surv = 0
for a in assets:
df = get_df(a, "1h")
print(f"\n === {a} 1h === {'Trd':>5s}{'Win%':>7s}{'FULL%':>8s}{'OOS%':>8s}{'DD%':>6s}{'AnniP':>8s}")
show("long-only", df, regime_mr(df, allow_short=False))
r = show("long + short@downtrend200", df, regime_mr(df, trend_n=200))
show("long + short@downtrend500", df, regime_mr(df, trend_n=500))
if r and r[0].ret > 0 and r[1].ret > 0:
surv += 1
print(f"\n Asset con regime200 positivo FULL+OOS: {surv}/{len(assets)}")
+1 -1
View File
@@ -170,7 +170,7 @@ def _rot_daily_equity(idx):
for i in range(101, T - 1):
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]
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)
for j in chosen:
nw[j] = 0.45 / len(chosen)
-188
View File
@@ -1,188 +0,0 @@
"""Report PER ANNO (Trade, Acc%, DD%, PnL%) delle 3 strategie oneste.
Acc: DIP01/TR01 = win-rate dei trade chiusi (episodi); ROT01 = % giorni positivi.
DD : drawdown massimo dell'equity DENTRO l'anno solare.
PnL: variazione % dell'equity nell'anno (composta).
Tutto NETTO (fee 0.10% RT, leva 3x, pos 15%). Replica gli engine di honest_*.
"""
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 atr, ema, get_df, available_assets, FEE_RT
from scripts.analysis.honest_final import dip_entries
from scripts.analysis.honest_rotation import build_panel
LEV, POS = 3.0, 0.15
def _yearly_dd(years: np.ndarray, equity: np.ndarray) -> dict[int, float]:
"""DD massimo intra-anno da una serie di equity etichettata per anno."""
out: dict[int, float] = {}
for y in np.unique(years):
eq = equity[years == y]
peak = eq[0]; dd = 0.0
for v in eq:
peak = max(peak, v)
dd = max(dd, (peak - v) / peak if peak > 0 else 0.0)
out[int(y)] = dd * 100
return out
def _print(title, header, rows):
print("\n" + "=" * 78)
print(f" {title}")
print("=" * 78)
print(" " + header)
print(" " + "-" * 74)
for r in rows:
print(" " + r)
# --------------------------- DIP01 (trade-based) ---------------------------
def dip_yearly(asset, tf="1h"):
df = get_df(asset, tf)
ents = dip_entries(df)
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
fee = FEE_RT * LEV
cap = 1000.0
last_exit = -1
eq_y, eq_v = [], []
yt: dict[int, list] = {} # year -> [trades, wins, pnl_start_cap, pnl_end_cap]
for e in ents:
i, d = e["i"], e["d"]
if i <= last_exit or i + 1 >= n:
continue
entry = c[i]; tp, sl, 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:
j = n - 1; exit_p = c[j]; break
if (d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl):
exit_p = sl; break
if (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp):
exit_p = tp; break
if k == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * LEV - fee
cap = max(cap + cap * POS * ret, 10.0)
last_exit = j
y = ts.iloc[i].year
rec = yt.setdefault(y, [0, 0, None, None])
rec[0] += 1; rec[1] += ret > 0
eq_y.append(y); eq_v.append(cap)
dd = _yearly_dd(np.array(eq_y), np.array(eq_v))
# PnL% anno: da equity prima/dopo
rows = []
prev = 1000.0
yrs = sorted(yt)
cum = {}
cprev = 1000.0
# ricostruisci equity di fine anno
end_cap = {}
for y, v in zip(eq_y, eq_v):
end_cap[y] = v
for y in yrs:
t, w = yt[y][0], yt[y][1]
ec = end_cap[y]
pnl = (ec / cprev - 1) * 100
cprev = ec
rows.append(f"{y:>6d}{t:>8d}{(w/t*100 if t else 0):>8.1f}{dd.get(y,0):>8.1f}{pnl:>+10.1f}")
return rows
# --------------------------- TR01 (position episodes) ---------------------------
def tr_yearly(asset, tf="4h", fast=20, slow=100):
df = get_df(asset, tf)
c = df["close"].values; n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
ef, es = ema(c, fast), ema(c, slow)
sig = np.where(ef > es, 1.0, 0.0); sig[:slow] = 0.0
cap = 1000.0; cur = 0.0
fee = FEE_RT / 2 * LEV
ep_start_cap = None; ep_year = None
yt: dict[int, list] = {}
eq_y, eq_v = [], []
for i in range(n - 1):
s = sig[i]
if s != cur:
cap -= cap * POS * fee * abs(s - cur)
if s == 1.0: # apertura long
ep_start_cap = cap; ep_year = ts.iloc[i].year
elif cur == 1.0 and ep_start_cap is not None: # chiusura long
rec = yt.setdefault(ep_year, [0, 0])
rec[0] += 1; rec[1] += cap > ep_start_cap
ep_start_cap = None
cur = s
pr = (c[i + 1] - c[i]) / c[i]
cap = max(cap * (1 + POS * LEV * pr * cur), 10.0)
eq_y.append(ts.iloc[i].year); eq_v.append(cap)
if cur == 1.0 and ep_start_cap is not None:
rec = yt.setdefault(ep_year, [0, 0]); rec[0] += 1; rec[1] += cap > ep_start_cap
dd = _yearly_dd(np.array(eq_y), np.array(eq_v))
end_cap = {}
for y, v in zip(eq_y, eq_v):
end_cap[y] = v
rows = []; cprev = 1000.0
for y in sorted(end_cap):
t, w = yt.get(y, [0, 0])
pnl = (end_cap[y] / cprev - 1) * 100; cprev = end_cap[y]
rows.append(f"{y:>6d}{t:>8d}{(w/t*100 if t else 0):>8.1f}{dd.get(y,0):>8.1f}{pnl:>+10.1f}")
return rows
# --------------------------- ROT01 (daily portfolio) ---------------------------
def rot_yearly(lookback=60, top_k=2, gross=0.45):
panel = build_panel(available_assets(), "1d")
P = panel.values; T, N = P.shape
rets = np.zeros_like(P); rets[1:] = P[1:] / P[:-1] - 1
years = panel.index.year.values
cap = 1000.0; w = np.zeros(N)
yt: dict[int, list] = {} # year -> [rebal, pos_days, days]
eq_y, eq_v = [], []
for i in range(lookback + 1, T - 1):
mom = P[i] / P[i - lookback] - 1
order = np.argsort(mom)[::-1]
chosen = [j for j in order if mom[j] > 0][:top_k]
new_w = np.zeros(N)
for j in chosen:
new_w[j] = gross / len(chosen)
turnover = np.abs(new_w - w).sum()
if turnover > 1e-9:
cap -= cap * turnover * (FEE_RT / 2)
w = new_w
pr = float(np.dot(w, rets[i + 1]))
cap = max(cap * (1 + pr), 10.0)
y = int(years[i])
rec = yt.setdefault(y, [0, 0, 0])
rec[0] += turnover > 1e-9; rec[1] += pr > 0; rec[2] += 1
eq_y.append(y); eq_v.append(cap)
dd = _yearly_dd(np.array(eq_y), np.array(eq_v))
end_cap = {}
for y, v in zip(eq_y, eq_v):
end_cap[y] = v
rows = []; cprev = 1000.0
for y in sorted(end_cap):
reb, pos, days = yt[y]
pnl = (end_cap[y] / cprev - 1) * 100; cprev = end_cap[y]
rows.append(f"{y:>6d}{reb:>8d}{(pos/days*100 if days else 0):>8.1f}{dd.get(y,0):>8.1f}{pnl:>+10.1f}")
return rows
if __name__ == "__main__":
H = f"{'Anno':>6s}{'Trade':>8s}{'Acc%':>8s}{'DD%':>8s}{'PnL%':>10s}"
for a in ["BTC", "ETH", "SOL"]:
_print(f"DIP01 — {a} 1h (Acc = win-rate trade)", H, dip_yearly(a))
for a in ["BNB", "BTC", "DOGE", "SOL", "XRP"]:
_print(f"TR01 — {a} 4h (Trade = episodi long, Acc = win-rate episodi)", H, tr_yearly(a))
_print("ROT01 — paniere 8 crypto 1d (Trade = ribilanciamenti, Acc = % giorni positivi)",
H, rot_yearly())
-74
View File
@@ -1,74 +0,0 @@
"""Tabella per-anno (PnL% e DD% intra-anno) delle versioni MIGLIORATE:
ROT02 (dual-momentum), le 3 sleeve e il PORTAFOGLIO combinato.
Tutto NETTO. Riusa gli engine di honest_improve / honest_improve2.
"""
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_improve2 import ( # noqa: E402
dip_market_gated, _daily_equity, _norm, _tr_basket_daily, _rot_daily_equity,
)
def _year_dd(eq: pd.Series) -> dict[int, float]:
out = {}
for y, g in eq.groupby(eq.index.year):
peak = g.iloc[0]; dd = 0.0
for v in g:
peak = max(peak, v); dd = max(dd, (peak - v) / peak if peak > 0 else 0.0)
out[int(y)] = dd * 100
return out
def _year_pnl(eq: pd.Series) -> dict[int, float]:
out = {}
for y, g in eq.groupby(eq.index.year):
out[int(y)] = (g.iloc[-1] / g.iloc[0] - 1) * 100
return out
def table(name, eq):
eq = _norm(eq)
dd = _year_dd(eq); pnl = _year_pnl(eq)
print(f"\n {name}")
print(f" {'Anno':>6s}{'PnL%':>9s}{'DD%':>7s}")
print(" " + "-" * 22)
for y in sorted(pnl):
print(f" {y:>6d}{pnl[y]:>+9.0f}{dd[y]:>7.0f}")
tot = (eq.iloc[-1] / eq.iloc[0] - 1) * 100
print(f" {'TOT':>6s}{tot:>+9.0f}{_year_dd(eq) and max(_year_dd(eq).values()):>7.0f}(max anno)")
if __name__ == "__main__":
print("=" * 60)
print(" RISULTATI PER ANNO — versioni migliorate (NETTO)")
print("=" * 60)
# ROT02 dal 2020 (dati paniere)
idx_rot = pd.date_range("2020-09-01", "2026-05-26", freq="1D", tz="UTC")
eq_rot = _rot_daily_equity(idx_rot)
table("ROT02 — dual-momentum rotation (1d)", eq_rot)
# sleeve + portafoglio dal 2021
idx = pd.date_range("2021-01-01", "2026-05-26", freq="1D", tz="UTC")
d = dip_market_gated("BTC", market_n=0, return_equity=True)
eq_dip = _norm(_daily_equity(d["eq_ts"], d["eq_v"], idx))
eq_tr = _norm(_tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], idx))
eq_r2 = _norm(_rot_daily_equity(idx))
table("Sleeve DIP01 — BTC (1h)", eq_dip)
table("Sleeve TR01 — basket (4h)", eq_tr)
table("Sleeve ROT02 (1d)", eq_r2)
drets = pd.DataFrame({"DIP": eq_dip.pct_change().fillna(0),
"TR": eq_tr.pct_change().fillna(0),
"ROT": eq_r2.pct_change().fillna(0)})
combo = (1 + drets.mean(axis=1)).cumprod()
table("PORTAFOGLIO equal-weight (daily rebal)", combo)
+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()
+261
View File
@@ -0,0 +1,261 @@
"""Gestione del rischio sulle fade (MR01/MR02/MR03/MR07): alzare Acc, ridurre DD.
Due analisi, ognuna misurata FULL e OOS (ultimo 30%) per non illudersi:
(A) SCREENING LEVE confronta su ogni strategia le leve di rischio:
- vol-target sizing (size ~ 1/distanza-SL) -> SCARTATA (peggiora)
- skip alta volatilita' (ATR% in coda alta) -> SCARTATA (peggiora)
- filtro trend (|close-EMA200|/ATR oltre soglia) -> ADOTTATA (Acc+ DD-)
- combinazione di tutte
(B) FILTRO TREND + PORTAFOGLIO:
- sweep della soglia trend (assoluta in ATR, regola unica = no overfit)
- portafoglio equipesato su sotto-conti indipendenti: curve poco correlate
-> DD aggregato << DD del singolo sleeve (vera leva anti-drawdown)
Engine fedele: ingresso close[i], exit TP/SL intrabar (high/low) o time-limit,
non-overlap, capitale composto. Numeri NETTI fee 0.10% RT, leva 3x.
"""
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.strategy_research import bollinger_fade, atr
from scripts.analysis.strategy_research_v2 import donchian_fade, return_reversal
FEE_RT, LEV, POS, INIT, OOS_FRAC = 0.001, 3.0, 0.15, 1000.0, 0.30
# config base di ogni strategia (come strategies.yml).
# NB: MR03 keltner_fade spostata in scripts/waste/ (fade piu' debole, ridondante
# con MR01); la funzione keltner_fade resta in strategy_research_v2 come record.
STRATS = {
"MR01": (bollinger_fade, dict(n=50, k=2.5, sl_atr=2.0, max_bars=24)),
"MR02": (donchian_fade, dict(n=20, sl_atr=2.0, max_bars=24)),
"MR07": (return_reversal,dict(n=50, k=3.5, tp_atr=2.0, sl_atr=1.5, max_bars=24)),
}
STRATS_ETH = dict(STRATS)
def strats_for(asset: str) -> dict:
return STRATS_ETH if asset == "ETH" else STRATS
# ============================ (A) SCREENING LEVE ============================
def add_context(ents, df, ema_long=200):
"""Aggiunge a ogni entry: sl_dist, atr_pct, trend_dist (|close-EMA|/ATR)."""
c = df["close"].values
a = atr(df, 14)
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
apct = a / c
for e in ents:
i = e["i"]
e["sl_dist"] = abs(c[i] - e["sl"]) / c[i]
e["atr_pct"] = apct[i]
e["trend_dist"] = abs(c[i] - el[i]) / a[i] if a[i] else 0.0
return ents
def simulate(ents, df, fee_rt=FEE_RT, lev=LEV, split=-1,
sizer=None, vol_skip=None, trend_skip=None, max_size=0.30):
"""sizer: funzione(entry)->frazione capitale; default POS fisso.
vol_skip: soglia atr_pct sopra cui salto. trend_skip: soglia trend_dist sopra cui salto."""
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
cap = peak = INIT
dd = 0.0; last = -1; trd = wins = 0
fee = fee_rt * lev
yearly = {}; rets = []
for e in ents:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n or i < split:
continue
if vol_skip is not None and e["atr_pct"] > vol_skip:
continue
if trend_skip is not None and e["trend_dist"] > trend_skip:
continue
entry = c[i]; tp, sl, 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
hs = (d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl)
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if hs: exit_p = sl; break
if ht: exit_p = tp; break
if k == mb: exit_p = c[j]
ret = (exit_p - entry) / entry * d * lev - fee
size = POS if sizer is None else min(sizer(e), max_size)
cap = max(cap + cap * size * ret, 10.0)
peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
trd += 1; wins += ret > 0; last = j; rets.append(ret * size)
y = ts.iloc[i].year; yearly[y] = yearly.get(y, 0.0) + ret * size * INIT
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 dict(trades=trd, acc=wins / trd * 100 if trd else 0.0,
ret=(cap / INIT - 1) * 100, dd=dd * 100, yearly=yearly, sharpe=sharpe)
def vol_target_sizer(target=0.015):
"""size t.c. rischio (size*lev*sl_dist) ~ target; piu' largo lo stop, meno size."""
return lambda e: target / (LEV * max(e["sl_dist"], 1e-4))
def _line(label, full, oos):
print(f" {label:<28s}{full['trades']:>6d}{full['acc']:>7.1f}{full['ret']:>+10.0f}{full['dd']:>7.1f}{full['sharpe']:>7.2f}"
f" | {oos['trades']:>5d}{oos['acc']:>7.1f}{oos['ret']:>+9.0f}{oos['dd']:>7.1f}{oos['sharpe']:>7.2f}")
def screen_levers():
print("=" * 110)
print(" (A) SCREENING LEVE — vol-target / vol-skip / filtro-trend | NETTO fee 0.10% RT, leva 3x")
print("=" * 110)
for asset in ["BTC", "ETH"]:
df = load_data(asset, "1h")
split = int(len(df) * (1 - OOS_FRAC))
print(f"\n {asset} 1h")
print(f" {'config':<28s}{'Trd':>6s}{'Acc%':>7s}{'Ret%':>10s}{'DD%':>7s}{'Shrp':>7s}"
f" | {'oTrd':>5s}{'oAcc':>7s}{'oRet':>9s}{'oDD':>7s}{'oShrp':>7s}")
print(" " + "-" * 106)
for nm, (fn, params) in strats_for(asset).items():
ents = add_context(fn(df, **params), df)
p85 = float(np.quantile([e["atr_pct"] for e in ents], 0.85))
t90 = float(np.quantile([e["trend_dist"] for e in ents], 0.90))
_line(f"{nm} base", simulate(ents, df), simulate(ents, df, split=split))
_line(f"{nm} +volTarget", simulate(ents, df, sizer=vol_target_sizer()),
simulate(ents, df, split=split, sizer=vol_target_sizer()))
_line(f"{nm} +volSkip(p85)", simulate(ents, df, vol_skip=p85),
simulate(ents, df, split=split, vol_skip=p85))
_line(f"{nm} +trendSkip(p90)", simulate(ents, df, trend_skip=t90),
simulate(ents, df, split=split, trend_skip=t90))
_line(f"{nm} +ALL", simulate(ents, df, sizer=vol_target_sizer(), vol_skip=p85, trend_skip=t90),
simulate(ents, df, split=split, sizer=vol_target_sizer(), vol_skip=p85, trend_skip=t90))
print(" " + "-" * 106)
print("\n Esito: vol-target e vol-skip PEGGIORANO; il filtro trend e' l'unica leva utile.")
# ===================== (B) FILTRO TREND + PORTAFOGLIO =====================
def build_trades(ents, df, lev=LEV, fee_rt=FEE_RT, trend_max=None, ema_long=200):
"""Lista trade non-overlap: (entry_idx, exit_idx, ret_netto). Filtro trend opzionale."""
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, sl, 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
hs = (d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl)
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if hs: exit_p = sl; break
if ht: exit_p = tp; 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 metrics_single(trades, pos=POS, split=-1):
cap = peak = INIT; dd = 0.0; trd = wins = 0; rets = []
for i, j, ret in trades:
if i < split:
continue
cap = max(cap + cap * pos * ret, 10.0)
peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
trd += 1; wins += ret > 0; rets.append(ret * pos)
sh = float(np.mean(rets) / np.std(rets) * np.sqrt(len(rets))) if len(rets) > 1 and np.std(rets) > 0 else 0.0
return dict(trades=trd, acc=wins / trd * 100 if trd else 0.0,
ret=(cap / INIT - 1) * 100, dd=dd * 100, sharpe=sh)
def sleeve_equity(trades, n_bars, pos=POS, split=-1):
"""Equity di uno sleeve su sotto-conto indipendente (capitale INIT, pos fissa)."""
eq = np.full(n_bars, INIT, dtype=float)
cap = INIT
for i, j, ret in sorted(trades, key=lambda t: t[1]):
if i < split:
continue
cap = max(cap + cap * pos * ret, 10.0)
eq[j:] = cap
return eq
def metrics_portfolio(strat_trades, n_bars, pos=POS, split=-1):
"""Portafoglio equipesato: media di N sotto-conti indipendenti. DD sull'aggregata."""
sleeves = [sleeve_equity(tr, n_bars, pos=pos, split=split) for tr in strat_trades.values()]
agg = np.mean(sleeves, axis=0)
agg = agg[max(split, 0):]
peak = np.maximum.accumulate(agg)
dd = float(np.max((peak - agg) / peak) * 100)
trd = sum(1 for tr in strat_trades.values() for i, _, _ in tr if i >= split)
wins = sum(1 for tr in strat_trades.values() for i, _, r in tr if i >= split and r > 0)
return dict(trades=trd, acc=wins / trd * 100 if trd else 0.0, ret=(agg[-1] / INIT - 1) * 100, dd=dd)
def trend_and_portfolio():
# --- sweep soglia trend ---
print("\n" + "=" * 104)
print(" (B1) FILTRO TREND |close-EMA200|/ATR > soglia -> SALTA | NETTO fee 0.10% RT, leva 3x")
print("=" * 104)
print(f" {'Strat/Asset':<14s}{'soglia':>8s}{'Trd':>6s}{'Acc%':>7s}{'Ret%':>9s}{'DD%':>7s}"
f" | {'oAcc':>6s}{'oRet':>9s}{'oDD':>7s}{'oShrp':>7s}")
print(" " + "-" * 100)
for asset in ["BTC", "ETH"]:
df = load_data(asset, "1h"); split = int(len(df) * (1 - OOS_FRAC))
for nm, (fn, params) in strats_for(asset).items():
ents = fn(df, **params)
for thr in [None, 4.0, 3.0, 2.5, 2.0]:
tr = build_trades(ents, df, trend_max=thr)
f = metrics_single(tr); o = metrics_single(tr, split=split)
lab = "base" if thr is None else f"{thr}ATR"
print(f" {nm+' '+asset:<14s}{lab:>8s}{f['trades']:>6d}{f['acc']:>7.1f}{f['ret']:>+9.0f}{f['dd']:>7.1f}"
f" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}")
print(" " + "-" * 100)
# --- portafoglio equipesato (filtro trend 3.0 ATR) ---
print("\n" + "=" * 104)
print(" (B2) PORTAFOGLIO equipesato: N sotto-conti indipendenti (pos 0.15, filtro trend 3.0 ATR)")
print("=" * 104)
print(f" {'Universo':<26s}{'Trd':>6s}{'Acc%':>7s}{'Ret%':>10s}{'DD%':>7s}"
f" | {'oAcc':>6s}{'oRet':>9s}{'oDD':>7s}")
print(" " + "-" * 100)
all_trades = {}
for asset in ["BTC", "ETH"]:
df = load_data(asset, "1h"); split = int(len(df) * (1 - OOS_FRAC)); n = len(df)
st = {f"{nm}_{asset}": build_trades(fn(df, **p), df, trend_max=3.0) for nm, (fn, p) in strats_for(asset).items()}
all_trades.update(st)
f = metrics_portfolio(st, n); o = metrics_portfolio(st, n, split=split)
print(f" {'Portafoglio '+asset+' (4 strat)':<26s}{f['trades']:>6d}{f['acc']:>7.1f}{f['ret']:>+10.0f}{f['dd']:>7.1f}"
f" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}")
df0 = load_data("BTC", "1h"); split0 = int(len(df0) * (1 - OOS_FRAC))
f = metrics_portfolio(all_trades, len(df0)); o = metrics_portfolio(all_trades, len(df0), split=split0)
print(" " + "-" * 100)
print(f" {'GLOBALE BTC+ETH (8 sleeve)':<26s}{f['trades']:>6d}{f['acc']:>7.1f}{f['ret']:>+10.0f}{f['dd']:>7.1f}"
f" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}")
print("\n Curve poco correlate => DD aggregato molto piu' basso del singolo sleeve.")
def main():
screen_levers()
trend_and_portfolio()
if __name__ == "__main__":
main()
+306
View File
@@ -0,0 +1,306 @@
"""Ricerca v2 — nuove strategie oltre MR01, stessa metodologia fee-aware OOS.
Lezioni ereditate (vedi strategy_research.py / oos_validation.py):
- mean-reversion ha edge, continuation/trend NO (i breakout rientrano)
- fee = vincolo di prim'ordine -> default Deribit 0.10% RT, poche operazioni meglio
- ingresso ESEGUIBILE a close[i] (mai look-ahead con direzione da barra i)
- ogni numero NETTO dopo fee+leva, su finestra held-out (OOS=ultimo 30%) + per anno
Nuovi candidati (tutti fade/mean-reversion con ingresso onesto):
MR02 donchian_fade - fade rottura canale Donchian (opposto del trend che muore)
MR03 keltner_fade - fade canale Keltner (ATR), TP alla EMA media
MR04 zscore_revert - fade deviazione z-score estrema, TP alla media
MR05 boll_fade_adx - Bollinger fade con filtro regime ADX (solo mercato laterale)
Engine identico a strategy_research.simulate (ingresso close[i], exit TP/SL intrabar
high/low o time-limit, non-overlap, capitale composto).
"""
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))
# riusa engine, dati e indicatori gia' validati
from scripts.analysis.strategy_research import (
FEE_RT, LEV, POS, OOS_FRAC, get_df, atr, rsi, simulate,
)
# --------------------------- indicatori extra ---------------------------
def ema(x: np.ndarray, n: int) -> np.ndarray:
return pd.Series(x).ewm(span=n, adjust=False).mean().values
def adx(df: pd.DataFrame, n: int = 14) -> np.ndarray:
"""Average Directional Index: misura la forza del trend (alto=trend, basso=range)."""
h, l, c = df["high"].values, df["low"].values, df["close"].values
up = h - np.roll(h, 1)
dn = np.roll(l, 1) - l
up[0] = dn[0] = 0.0
plus_dm = np.where((up > dn) & (up > 0), up, 0.0)
minus_dm = np.where((dn > up) & (dn > 0), dn, 0.0)
pc = np.roll(c, 1); pc[0] = c[0]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
atr_n = pd.Series(tr).ewm(alpha=1/n, adjust=False).mean().values
pdi = 100 * pd.Series(plus_dm).ewm(alpha=1/n, adjust=False).mean().values / np.where(atr_n == 0, np.nan, atr_n)
mdi = 100 * pd.Series(minus_dm).ewm(alpha=1/n, adjust=False).mean().values / np.where(atr_n == 0, np.nan, atr_n)
dx = 100 * np.abs(pdi - mdi) / np.where((pdi + mdi) == 0, np.nan, pdi + mdi)
return pd.Series(dx).ewm(alpha=1/n, adjust=False).mean().values
# --------------------------- strategie nuove ---------------------------
def donchian_fade(df, n=20, sl_atr=2.0, max_bars=24):
"""MR02 — fade rottura canale Donchian: rompe sopra max-N => short verso il mid.
Coerente con 'i breakout rientrano': l'opposto di donchian_trend (che fallisce).
Ingresso a close[i] sulla barra che chiude oltre il canale precedente.
TP al centro del canale, SL = sl_atr*ATR oltre l'estremo.
"""
h, l, c = df["high"].values, df["low"].values, df["close"].values
hh = pd.Series(h).rolling(n).max().shift(1).values
ll = pd.Series(l).rolling(n).min().shift(1).values
a = atr(df, 14)
ents = []
for i in range(n + 14, len(c)):
if np.isnan(hh[i]) or np.isnan(a[i]):
continue
mid = (hh[i] + ll[i]) / 2.0
if c[i] > hh[i] and c[i - 1] <= hh[i - 1]: # rottura rialzista => fade short
ents.append({"i": i, "d": -1, "tp": mid, "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
elif c[i] < ll[i] and c[i - 1] >= ll[i - 1]: # rottura ribassista => fade long
ents.append({"i": i, "d": 1, "tp": mid, "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
return ents
def keltner_fade(df, n=20, k=2.0, sl_atr=2.0, max_bars=24):
"""MR03 — fade canale Keltner (EMA +/- k*ATR), TP alla EMA media.
Come Bollinger ma banda basata su ATR (volatilita' di range) invece che std:
reagisce diversamente ai gap. Ingresso quando close esce dalla banda.
"""
c = df["close"].values
e = ema(c, n)
a = atr(df, n)
up, lo = e + k * a, e - k * a
ents = []
for i in range(n + 1, len(c)):
if np.isnan(up[i]) or np.isnan(a[i]):
continue
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
ents.append({"i": i, "d": 1, "tp": e[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
ents.append({"i": i, "d": -1, "tp": e[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def zscore_revert(df, n=50, z=2.0, sl_atr=2.5, max_bars=24):
"""MR04 — fade deviazione z-score estrema dalla media, TP alla media.
z = (close-ma)/std. Entra quando |z| supera la soglia (close fuori); chiude
quando torna alla media. Banda di Bollinger riparametrizzata in z (equivalente
a k=z) ma con SL piu' largo e finestra lunga: poche operazioni, alta selettivita'.
"""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
ents = []
for i in range(n + 14, len(c)):
if np.isnan(ma[i]) or sd[i] == 0 or np.isnan(a[i]):
continue
zi = (c[i] - ma[i]) / sd[i]
zp = (c[i - 1] - ma[i - 1]) / sd[i - 1] if sd[i - 1] else 0.0
if zi <= -z and zp > -z:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif zi >= z and zp < z:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def boll_fade_adx(df, n=50, k=2.5, sl_atr=2.0, max_bars=24, adx_max=25.0):
"""MR05 — Bollinger fade SOLO in regime laterale (ADX < adx_max).
Il fade soffre quando c'e' trend forte (il prezzo continua oltre la banda).
Filtro ADX: opera solo quando la forza del trend e' bassa -> meno trade, edge piu' pulito.
"""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
ax = adx(df, 14)
up, lo = ma + k * sd, ma - k * sd
ents = []
for i in range(n + 14, len(c)):
if np.isnan(up[i]) or np.isnan(a[i]) or np.isnan(ax[i]):
continue
if ax[i] >= adx_max: # trend forte: niente fade
continue
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def rsi2_fade(df, rsi_n=2, lo=10, hi=90, ma_n=200, tp_atr=2.0, sl_atr=3.0, max_bars=24):
"""MR06 — Connors RSI(2) pullback in direzione del trend, TP/SL in ATR.
Meccanismo distinto da MR01/MR03: non usa bande di prezzo ma l'oscillatore
RSI(2), che satura su micro-estremi. Filtro di trend con SMA lunga:
- close SOPRA la SMA (uptrend) + RSI(2) < lo (dip) -> long, target rimbalzo
- close SOTTO la SMA (downtrend) + RSI(2) > hi (pop) -> short
TP = tp_atr*ATR a favore, SL = sl_atr*ATR contro. Compra il ritracciamento
nel trend, non il contro-trend.
"""
c = df["close"].values
r = rsi(c, rsi_n)
ma = pd.Series(c).rolling(ma_n).mean().values
a = atr(df, 14)
ents = []
for i in range(ma_n + 14, len(c)):
if np.isnan(r[i]) or np.isnan(ma[i]) or np.isnan(a[i]):
continue
if r[i] < lo and c[i] > ma[i]: # dip in uptrend -> long
ents.append({"i": i, "d": 1, "tp": c[i] + tp_atr * a[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif r[i] > hi and c[i] < ma[i]: # pop in downtrend -> short
ents.append({"i": i, "d": -1, "tp": c[i] - tp_atr * a[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def return_reversal(df, n=50, k=3.5, tp_atr=2.0, sl_atr=1.5, max_bars=24):
"""MR07 — fade movimento di barra estremo (return reversal).
Misura il rendimento dell'ultima barra in unita' di deviazione standard rolling
dei rendimenti. Se |ret| > k*sigma, fada nella direzione opposta; TP/SL in ATR.
Meccanismo distinto: usa la volatilita' dei RENDIMENTI, non i livelli di prezzo.
Config robusta (k=3.5, tp=2ATR, sl=1.5ATR): positivo full+OOS BTC e ETH 1h,
DD piu' contenuto (BTC 25% / ETH 46%).
"""
c = df["close"].values
ret = np.zeros_like(c)
ret[1:] = np.diff(c) / c[:-1]
sig = pd.Series(ret).rolling(n).std().values
a = atr(df, 14)
ents = []
for i in range(n + 14, len(c)):
if np.isnan(sig[i]) or sig[i] == 0 or np.isnan(a[i]):
continue
z = ret[i] / sig[i]
if z <= -k: # crollo di barra -> fade long
ents.append({"i": i, "d": 1, "tp": c[i] + tp_atr * a[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif z >= k: # spike di barra -> fade short
ents.append({"i": i, "d": -1, "tp": c[i] - tp_atr * a[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
CANDIDATES = {
"MR02 donch_fade n20": (donchian_fade, dict(n=20, sl_atr=2.0, max_bars=24)),
"MR02 donch_fade n50": (donchian_fade, dict(n=50, sl_atr=2.0, max_bars=24)),
"MR03 kelt_fade k2": (keltner_fade, dict(n=20, k=2.0, sl_atr=2.0, max_bars=24)),
"MR03 kelt_fade k2.5": (keltner_fade, dict(n=20, k=2.5, sl_atr=2.0, max_bars=24)),
"MR04 zscore z2 n50": (zscore_revert, dict(n=50, z=2.0, sl_atr=2.5, max_bars=24)),
"MR04 zscore z2.5 n50": (zscore_revert, dict(n=50, z=2.5, sl_atr=2.5, max_bars=24)),
"MR05 boll_adx n50": (boll_fade_adx, dict(n=50, k=2.5, sl_atr=2.0, max_bars=24, adx_max=25)),
"MR05 boll_adx n20": (boll_fade_adx, dict(n=20, k=2.5, sl_atr=2.0, max_bars=24, adx_max=25)),
"MR06 rsi2 10/90": (rsi2_fade, dict(rsi_n=2, lo=10, hi=90, ma_n=200, tp_atr=2.0, sl_atr=3.0, max_bars=24)),
"MR06 rsi2 5/95": (rsi2_fade, dict(rsi_n=2, lo=5, hi=95, ma_n=200, tp_atr=2.0, sl_atr=3.0, max_bars=24)),
"MR07 retrev k3.5": (return_reversal, dict(n=50, k=3.5, tp_atr=2.0, sl_atr=1.5, max_bars=24)),
"MR07 retrev k3.0": (return_reversal, dict(n=50, k=3.0, tp_atr=2.0, sl_atr=1.5, max_bars=24)),
}
def table():
print("=" * 122)
print(f" RICERCA v2 — NETTO dopo fee {FEE_RT*100:.2f}% RT | leva {LEV:.0f}x | pos {POS*100:.0f}% "
f"| OOS = ultimo {int(OOS_FRAC*100)}%")
print("=" * 122)
print(f" {'Strategia':<22s}{'Asset':>5s}{'TF':>5s}{'Trd':>6s}{'Tr/yr':>7s}{'Win%':>7s}"
f"{'Ret%FULL':>10s}{'Ret%OOS':>10s}{'DD%':>7s}{'Exp%':>7s}{'AnniPos':>9s}")
print(" " + "-" * 118)
for label, (fn, params) in CANDIDATES.items():
for asset in ["BTC", "ETH"]:
for tf in ["1h", "4h"]:
df = get_df(asset, tf)
ents = fn(df, **params)
full = simulate(ents, df)
split = int(len(df) * (1 - OOS_FRAC))
oos = simulate([e for e in ents if e["i"] >= split], df)
yrs = full["yearly"]
pos_yrs = sum(1 for v in yrs.values() if v > 0)
tr_yr = full["trades"] / max(len(yrs), 1)
robust = oos["ret"] > 0 and full["ret"] > 0 and pos_yrs >= max(len(yrs) - 1, 1)
flag = " <<<" if robust else ""
print(f" {label:<22s}{asset:>5s}{tf:>5s}{full['trades']:>6d}{tr_yr:>7.0f}{full['win']:>7.1f}"
f"{full['ret']:>+10.1f}{oos['ret']:>+10.1f}{full['dd']:>7.1f}{full['exposure']:>7.1f}"
f"{f'{pos_yrs}/{len(yrs)}':>9s}{flag}")
print(" " + "-" * 118)
print(" <<< = positivo full+OOS e robusto (quasi tutti gli anni positivi).")
def deep_dive():
"""Robustezza dei 3 candidati promossi: fee sweep + griglia parametri OOS."""
split_of = lambda df: int(len(df) * (1 - OOS_FRAC))
fees = [0.0, 0.0005, 0.001, 0.002]
print("\n" + "#" * 122)
print(" APPROFONDIMENTO MR02 / MR03 / MR05 — robustezza fee + griglia (deve restare positivo)")
print("#" * 122)
# --- MR02 Donchian Fade ---
print(f"\n [MR02 donchian_fade] SENSIBILITA' FEE — Ret% FULL/OOS (n=20)")
print(f" {'Asset/TF':<10s}" + "".join(f"{f'{f*100:.2f}%RT':>22s}" for f in fees))
print(f" {'':<10s}" + "".join(f"{'full':>11s}{'oos':>11s}" for _ in fees))
for a, tf in [("BTC", "1h"), ("ETH", "1h"), ("BTC", "4h"), ("ETH", "4h")]:
df = get_df(a, tf); sp = split_of(df)
ents = donchian_fade(df, n=20, sl_atr=2.0, max_bars=24)
oents = [e for e in ents if e["i"] >= sp]
cells = "".join(f"{simulate(ents, df, fee_rt=f)['ret']:>+11.0f}{simulate(oents, df, fee_rt=f)['ret']:>+11.0f}" for f in fees)
print(f" {a+' '+tf:<10s}{cells}")
print(f"\n [MR02] GRIGLIA n x sl_atr — Ret%OOS(DD%) | fee {FEE_RT*100:.2f}% RT")
for a in ["BTC", "ETH"]:
df = get_df(a, "1h"); sp = split_of(df)
print(f"\n {a} 1h " + "".join(f"{f'sl={s}':>16s}" for s in [1.5, 2.0, 3.0]))
for n in [10, 20, 30, 50]:
cells = ""
for s in [1.5, 2.0, 3.0]:
r = simulate([e for e in donchian_fade(df, n=n, sl_atr=s, max_bars=24) if e["i"] >= sp], df)
cell = "%+.0f(%.0f)" % (r["ret"], r["dd"])
cells += f"{cell:>16s}"
print(f" n={n:<4d}{cells}")
# --- MR03 Keltner Fade ---
print(f"\n [MR03 keltner_fade] GRIGLIA n x k — Ret%OOS(DD%) | fee {FEE_RT*100:.2f}% RT")
for a in ["BTC", "ETH"]:
df = get_df(a, "1h"); sp = split_of(df)
print(f"\n {a} 1h " + "".join(f"{f'k={k}':>16s}" for k in [1.5, 2.0, 2.5]))
for n in [14, 20, 30, 50]:
cells = ""
for k in [1.5, 2.0, 2.5]:
r = simulate([e for e in keltner_fade(df, n=n, k=k, sl_atr=2.0, max_bars=24) if e["i"] >= sp], df)
cell = "%+.0f(%.0f)" % (r["ret"], r["dd"])
cells += f"{cell:>16s}"
print(f" n={n:<4d}{cells}")
# --- MR05 Bollinger Fade + ADX ---
print(f"\n [MR05 boll_fade_adx] GRIGLIA n x adx_max — Ret%OOS(DD%) | fee {FEE_RT*100:.2f}% RT")
for a in ["BTC", "ETH"]:
df = get_df(a, "1h"); sp = split_of(df)
print(f"\n {a} 1h " + "".join(f"{f'adx<{x}':>16s}" for x in [20, 25, 30]))
for n in [20, 30, 50]:
cells = ""
for x in [20, 25, 30]:
r = simulate([e for e in boll_fade_adx(df, n=n, k=2.5, sl_atr=2.0, max_bars=24, adx_max=x) if e["i"] >= sp], df)
cell = "%+.0f(%.0f)" % (r["ret"], r["dd"])
cells += f"{cell:>16s}"
print(f" n={n:<4d}{cells}")
if __name__ == "__main__":
table()
deep_dive()
+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()
-169
View File
@@ -1,169 +0,0 @@
"""Report accuracy per ANNO × MERCATO delle strategie migliori.
Esegue ogni strategia vincente su BTC e ETH e produce tabella
accuracy/trades per ogni anno. Permette di vedere robustezza temporale
e differenze tra mercati.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import importlib.util
from pathlib import Path
STRATEGIES_DIR = Path("scripts/strategies")
def load_class(module_file, class_name):
path = STRATEGIES_DIR / f"{module_file}.py"
spec = importlib.util.spec_from_file_location(module_file, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return getattr(mod, class_name)
# (label, module, class, params, hold)
STRATEGIES = [
("SQ02 antifake+vol", "SQ02_squeeze_antifake_vol", "SqueezeAntifakeVol", {}, 3),
("MT01 ema20+vol", "MT01_squeeze_mtf_momentum", "SqueezeMTFMomentum",
{"ema_period": 20, "min_slope": 0.001, "vol_filter": True}, 3),
("PD01 vtb3 vm1.3", "PD01_price_volume_divergence", "PriceVolumeDivergence",
{}, 3),
("CM01 cb6+vol", "CM01_cross_market_momentum", "CrossMarketMomentum",
{"cross_bars": 6, "mom_min": 0.001, "use_vol": True}, 3),
("AD01 lt.65 ht.95", "AD01_adaptive_squeeze", "AdaptiveSqueeze",
{"low_thr": 0.65, "high_thr": 0.95, "use_vol": True}, 3),
]
ASSETS = ["BTC", "ETH"]
TF = "15m"
ALL_YEARS = list(range(2018, 2027))
def run():
results = {} # (label, asset) -> BacktestResult
for label, module, cls_name, params, hold in STRATEGIES:
try:
cls = load_class(module, cls_name)
except Exception as e:
print(f"SKIP {label}: {e}")
continue
strat = cls()
for asset in ASSETS:
try:
r = strat.backtest(asset, TF, hold=hold, **params)
if r:
results[(label, asset)] = r
except Exception as e:
print(f" errore {label} {asset}: {e}")
# ── Tabella ACCURACY per anno × mercato ──────────────────────────
print(f"\n{'=' * 140}")
print(f" ACCURACY PER ANNO × MERCATO — {TF} (fee 0.2% RT, leva 3x, pos 15%)")
print(f"{'=' * 140}")
header = f" {'Strategia':<22s} {'Mkt':>3s}"
for y in ALL_YEARS:
header += f" {y:>7d}"
header += f"{'TOT':>6s} {'DD%':>5s} {'Worst':>10s}"
print(header)
print(f" {'' * 136}")
for label, module, cls_name, params, hold in STRATEGIES:
for asset in ASSETS:
r = results.get((label, asset))
if not r:
continue
yd = {ys.year: ys for ys in r.yearly}
line = f" {label:<22s} {asset:>3s}"
for y in ALL_YEARS:
if y in yd:
line += f" {yd[y].accuracy:>5.0f}%↑" if yd[y].accuracy >= 80 else f" {yd[y].accuracy:>5.0f}% "
else:
line += f" {'':>7s}"
worst = r.worst_year
worst_str = f"{worst.year}({worst.accuracy:.0f}%)" if worst else "N/A"
line += f"{r.accuracy:>5.1f}% {r.max_dd:>4.1f}% {worst_str:>10s}"
print(line)
print(f" {'·' * 136}")
# ── Tabella TRADES per anno × mercato ────────────────────────────
print(f"\n{'=' * 140}")
print(f" NUMERO TRADES PER ANNO × MERCATO")
print(f"{'=' * 140}")
header = f" {'Strategia':<22s} {'Mkt':>3s}"
for y in ALL_YEARS:
header += f" {y:>7d}"
header += f"{'TOT':>6s} {'€/day':>6s}"
print(header)
print(f" {'' * 130}")
for label, module, cls_name, params, hold in STRATEGIES:
for asset in ASSETS:
r = results.get((label, asset))
if not r:
continue
yd = {ys.year: ys for ys in r.yearly}
line = f" {label:<22s} {asset:>3s}"
for y in ALL_YEARS:
if y in yd:
line += f" {yd[y].trades:>7d}"
else:
line += f" {'':>7s}"
line += f"{r.trades:>6d} {r.daily_pnl:>+6.2f}"
print(line)
print(f" {'·' * 130}")
# ── Tabella PnL per anno × mercato ──────────────────────────────
print(f"\n{'=' * 140}")
print(f" PnL € PER ANNO × MERCATO (su €1000, no compounding tra anni)")
print(f"{'=' * 140}")
header = f" {'Strategia':<22s} {'Mkt':>3s}"
for y in ALL_YEARS:
header += f" {y:>7d}"
header += f"{'TOT€':>8s}"
print(header)
print(f" {'' * 132}")
for label, module, cls_name, params, hold in STRATEGIES:
for asset in ASSETS:
r = results.get((label, asset))
if not r:
continue
yd = {ys.year: ys for ys in r.yearly}
line = f" {label:<22s} {asset:>3s}"
for y in ALL_YEARS:
if y in yd:
line += f" {yd[y].pnl:>+7.0f}"
else:
line += f" {'':>7s}"
line += f"{r.pnl:>+8.0f}"
print(line)
print(f" {'·' * 132}")
# ── Sintesi: media per anno (tutte le strategie) ────────────────
print(f"\n{'=' * 140}")
print(f" SINTESI — Accuracy media per anno (tutte le strategie, BTC+ETH)")
print(f"{'=' * 140}")
year_acc = {y: [] for y in ALL_YEARS}
for (label, asset), r in results.items():
for ys in r.yearly:
if ys.trades >= 10:
year_acc[ys.year].append(ys.accuracy)
line_y = f" {'Anno':<22s} "
line_a = f" {'Acc media':<22s} "
for y in ALL_YEARS:
accs = year_acc[y]
avg = sum(accs) / len(accs) if accs else 0
line_y += f" {y:>7d}"
line_a += f" {avg:>6.1f}%"
print(line_y)
print(line_a)
if __name__ == "__main__":
run()
@@ -57,16 +57,21 @@ class BollingerFade(Strategy):
k = params.get("k", 2.5)
sl_atr = params.get("sl_atr", 2.0)
max_bars = params.get("max_bars", 24)
trend_max = params.get("trend_max") # None = filtro disattivo
ema_long = params.get("ema_long", 200)
ma = pd.Series(c).rolling(bb_w).mean().values
sd = pd.Series(c).rolling(bb_w).std().values
a = _atr(df, 14)
up, lo = ma + k * sd, ma - k * sd
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values if trend_max is not None else None
signals: list[Signal] = []
for i in range(bb_w + 14, n_len):
if np.isnan(up[i]) or np.isnan(a[i]):
continue
if el is not None and (a[i] == 0 or np.isnan(el[i]) or abs(c[i] - el[i]) / a[i] > trend_max):
continue
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
d, sl = 1, c[i] - sl_atr * a[i]
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
+82
View File
@@ -0,0 +1,82 @@
"""MR02 — Donchian Fade (mean-reversion sugli estremi del canale).
L'opposto esatto del trend-following Donchian (che PERDE netto: vedi
scripts/analysis/strategy_research.py). Coerente con la lezione squeeze:
i breakout RIENTRANO, quindi si fada la rottura del canale verso il centro.
Logica:
1. Canale Donchian: massimo/minimo delle ultime n barre (escludendo la corrente)
2. ENTRY: close rompe SOPRA il massimo del canale -> SHORT (fade);
close rompe SOTTO il minimo -> LONG. Ingresso a close[i] (eseguibile).
3. EXIT: take-profit al centro del canale (il rientro atteso),
stop-loss a sl_atr*ATR oltre l'estremo, time-limit max_bars.
Validazione (netto, fee 0.10% RT reale Deribit, leva 3x, OOS = ultimo 30%):
BTC 1h n=20: +879% FULL / +171% OOS, DD 30%, 8/9 anni positivi
ETH 1h n=20: enorme FULL / +8452% OOS, DD 42%
Robusto su TUTTA la griglia n in {10,20,30,50} x sl_atr in {1.5,2.0,3.0}
(BTC+ETH 1h sempre positivo OOS) e su tutte le fee 0.00-0.20% RT.
Ricerca completa: scripts/analysis/strategy_research_v2.py.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from src.strategies.base import Signal
from src.strategies.fade_base import FadeStrategy, atr, trend_distance
class DonchianFade(FadeStrategy):
name = "MR02_donchian_fade"
description = "Mean-reversion: fada la rottura del canale Donchian, TP al centro"
default_assets = ["BTC", "ETH"]
default_timeframes = ["1h"]
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
**params) -> list[Signal]:
n = params.get("n", 20)
sl_atr = params.get("sl_atr", 2.0)
max_bars = params.get("max_bars", 24)
trend_max = params.get("trend_max") # None = filtro disattivo
ema_long = params.get("ema_long", 200)
h, l, c = df["high"].values, df["low"].values, df["close"].values
hh = pd.Series(h).rolling(n).max().shift(1).values
ll = pd.Series(l).rolling(n).min().shift(1).values
a = atr(df, 14)
td = trend_distance(df, ema_long) if trend_max is not None else None
signals: list[Signal] = []
for i in range(n + 14, len(c)):
if np.isnan(hh[i]) or np.isnan(a[i]):
continue
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
continue
mid = (hh[i] + ll[i]) / 2.0
if c[i] > hh[i] and c[i - 1] <= hh[i - 1]: # rottura rialzista -> fade short
d, sl = -1, c[i] + sl_atr * a[i]
elif c[i] < ll[i] and c[i - 1] >= ll[i - 1]: # rottura ribassista -> fade long
d, sl = 1, c[i] - sl_atr * a[i]
else:
continue
signals.append(Signal(
idx=i, direction=d, entry_price=c[i],
metadata={"tp": float(mid), "sl": float(sl), "max_bars": max_bars},
))
return signals
if __name__ == "__main__":
strat = DonchianFade()
print("=" * 110)
print(f" MR02 DONCHIAN FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
print("=" * 110)
for asset in ["BTC", "ETH"]:
r = strat.backtest(asset, "1h", n=20, sl_atr=2.0, max_bars=24)
if r:
r.strategy_name = f"MR02 {asset} 1h n20"
r.print_summary()
r.print_yearly()
@@ -0,0 +1,88 @@
"""MR07 — Return Reversal (fade del movimento di barra estremo).
Meccanismo distinto da MR01/MR02/MR03: non guarda i LIVELLI di prezzo (bande,
canali) ma la VOLATILITA' dei rendimenti. Quando una singola barra si muove di
piu' di k deviazioni standard rolling dei rendimenti, e' un'over-reaction che
tende a rientrare: si fada nella direzione opposta. Coerente con la lezione
mean-reversion.
Logica:
1. ret[i] = rendimento dell'ultima barra; sigma = std rolling(n) dei rendimenti
2. z = ret[i]/sigma. Se z <= -k (crollo) -> LONG; se z >= +k (spike) -> SHORT.
Ingresso a close[i] (eseguibile dal vivo, nessun look-ahead).
3. EXIT: take-profit a tp_atr*ATR a favore, stop-loss a sl_atr*ATR contro,
time-limit max_bars.
Validazione (netto, fee 0.10% RT reale Deribit, leva 3x, OOS = ultimo 30%):
config robusta k=3.5 tp=2ATR sl=1.5ATR n=50:
BTC 1h: +447% FULL / +105% OOS, DD 25%
ETH 1h: +335% FULL / +195% OOS, DD 46%
L'intero blocco tp_atr=2.0 (k in {2.5,3.0,3.5} x sl in {1.5,2.0,2.5}) e'
positivo full+OOS su entrambi gli asset 1h.
Ricerca completa: scripts/analysis/strategy_research_v2.py.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from src.strategies.base import Signal
from src.strategies.fade_base import FadeStrategy, atr, trend_distance
class ReturnReversal(FadeStrategy):
name = "MR07_return_reversal"
description = "Mean-reversion: fada il movimento di barra estremo (z dei rendimenti)"
default_assets = ["BTC", "ETH"]
default_timeframes = ["1h"]
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
**params) -> list[Signal]:
n = params.get("n", 50)
k = params.get("k", 3.5)
tp_atr = params.get("tp_atr", 2.0)
sl_atr = params.get("sl_atr", 1.5)
max_bars = params.get("max_bars", 24)
trend_max = params.get("trend_max") # None = filtro disattivo
ema_long = params.get("ema_long", 200)
c = df["close"].values
ret = np.zeros_like(c)
ret[1:] = np.diff(c) / c[:-1]
sig = pd.Series(ret).rolling(n).std().values
a = atr(df, 14)
td = trend_distance(df, ema_long) if trend_max is not None else None
signals: list[Signal] = []
for i in range(n + 14, len(c)):
if np.isnan(sig[i]) or sig[i] == 0 or np.isnan(a[i]):
continue
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
continue
z = ret[i] / sig[i]
if z <= -k: # crollo di barra -> fade long
d, tp, sl = 1, c[i] + tp_atr * a[i], c[i] - sl_atr * a[i]
elif z >= k: # spike di barra -> fade short
d, tp, sl = -1, c[i] - tp_atr * a[i], c[i] + sl_atr * a[i]
else:
continue
signals.append(Signal(
idx=i, direction=d, entry_price=c[i],
metadata={"tp": float(tp), "sl": float(sl), "max_bars": max_bars},
))
return signals
if __name__ == "__main__":
strat = ReturnReversal()
print("=" * 110)
print(f" MR07 RETURN REVERSAL — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
print("=" * 110)
for asset in ["BTC", "ETH"]:
r = strat.backtest(asset, "1h", n=50, k=3.5, tp_atr=2.0, sl_atr=1.5, max_bars=24)
if r:
r.strategy_name = f"MR07 {asset} 1h k3.5"
r.print_summary()
r.print_yearly()
+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
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%
TR01_basket +591% DD 27% CAGR 43%
ROT02_dualmom +771% DD 40% CAGR 49%
PORTAFOGLIO +642% DD 12% CAGR 45% <-- DD piu' basso di ogni sleeve
Per-anno: 2021 +203 · 2022 -1 · 2023 +47 · 2024 +50 · 2025 +14 · 2026 -2
ROT02_dualmom +984% DD 26% CAGR 56% (top_k=3: DD 40->26, PnL su)
PORTAFOGLIO +676% DD 13% CAGR 46% <-- DD piu' basso di ogni sleeve
Per-anno: 2021 +224 · 2022 +1 · 2023 +48 · 2024 +48 · 2025 +10 · 2026 -2
Logica e ricostruzione: scripts/analysis/honest_improve2.py.
"""
from __future__ import annotations
+52
View File
@@ -0,0 +1,52 @@
"""PORT02 — Portafoglio FADE accorpato e migliorato (6 sleeve, equal-weight daily).
Accorpa le 3 strategie fade mean-reversion su BTC e ETH, ciascuna MIGLIORATA con
il filtro trend (salta i fade contro trend estremi: |close-EMA200|/ATR > 3.0):
MR01 Bollinger fade · MR02 Donchian fade · MR07 Return-reversal
x {BTC, ETH} -> 6 sleeve indipendenti, pos 0.15 ciascuna, leva 3x.
(MR03 Keltner spostata in waste: fade piu' debole e ridondante con MR01.)
Le curve sono poco correlate fra loro (corr media intra-fade ~0.18): la
diversificazione abbatte il DD aggregato ben sotto quello del singolo sleeve.
Risultato (netto fee 0.10% RT, equal-weight ribilanciato ogni giorno, finestra
comune 2021-2026, OOS = ultimo 30%):
Ret +666% / CAGR 46% / DD 8.2% FULL · OOS DD 5.9% / Sharpe 3.95 FULL / 4.09 OOS.
Ricostruzione e confronto: scripts/analysis/combine_portfolio.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))
from scripts.analysis.combine_portfolio import ( # noqa: E402
build_all_sleeves, port_returns, metrics, yearly_returns, SPLIT, OOS_DATE, IDX,
)
def run():
sleeves = {k: v for k, v in build_all_sleeves().items() if k.startswith("MR")}
pr = port_returns(sleeves)
full, oos = metrics(pr), metrics(pr, lo=SPLIT)
print("=" * 84)
print(f" PORT02 — FADE master (8 sleeve, equal-weight daily) | {IDX[0].date()} -> {IDX[-1].date()}")
print("=" * 84)
print(f" {'sleeve':<14s}{'Ret%':>9s}{'DD%':>7s}{'Shrp':>7s}")
for name, s in sleeves.items():
m = metrics(s.pct_change().fillna(0.0))
print(f" {name:<14s}{m['ret']:>+9.0f}{m['dd']:>7.1f}{m['sharpe']:>7.2f}")
print(" " + "-" * 80)
print(f" {'PORTAFOGLIO':<14s}{full['ret']:>+9.0f}{full['dd']:>7.1f}{full['sharpe']:>7.2f}"
f" CAGR {full['cagr']:.0f}%")
print(f" {' di cui OOS':<14s}{oos['ret']:>+9.0f}{oos['dd']:>7.1f}{oos['sharpe']:>7.2f}"
f" (da {OOS_DATE})")
pa = yearly_returns(pr)
print(" Per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in pa.items()))
if __name__ == "__main__":
run()
+78
View File
@@ -0,0 +1,78 @@
"""PORT03 — Portafoglio MASTER: accorpa TUTTE le strategie (fade + honest).
Unisce le due famiglie del progetto, quasi scorrelate (correlazione cross-famiglia
~0.05), in un unico portafoglio diversificato:
FADE (reversione intraday 1h, long/short, BTC/ETH, col filtro trend):
MR01 Bollinger · MR02 Donchian · MR07 Return-reversal -> 6 sleeve
HONEST (long-only multi-regime multi-crypto):
DIP01 dip-buy (1h) · TR01 EMA-trend (4h basket) · ROT02 dual-momentum (1d) -> 3 sleeve
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
stabilizzata dalle fade ad alta frequenza). Vedi scripts/analysis/combine_portfolio.py.
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)
HONEST only (3) DD 12.6% Sharpe 2.20 (OOS DD 7.8 / Shrp 2.02)
MASTER eq (9) DD 5.2% Sharpe 4.23 (OOS DD 4.7 / Shrp 4.33) <- miglior Sharpe
MASTER 50/50 DD 5.0% Sharpe 3.69 (OOS DD 4.5) <- miglior DD
CAGR ~47% mantenuta in entrambe le varianti combinate.
Due varianti operative selezionabili:
weighting="equal" -> equal-weight sui 9 sleeve (massimo Sharpe)
weighting="5050" -> 50% famiglia fade + 50% famiglia honest (minimo DD)
"""
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.combine_portfolio import ( # noqa: E402
build_all_sleeves, port_returns, metrics, yearly_returns, SPLIT, OOS_DATE, IDX,
)
def master_returns(sleeves: dict, weighting: str = "equal"):
"""Rendimenti giornalieri del portafoglio master.
equal = equal-weight su tutti gli 11 sleeve; 5050 = media fra le due famiglie."""
fade = {k: v for k, v in sleeves.items() if k.startswith("MR")}
honest = {k: v for k, v in sleeves.items() if not k.startswith("MR")}
if weighting == "5050":
return (port_returns(fade) + port_returns(honest)) / 2
return port_returns(sleeves)
def run():
sleeves = build_all_sleeves()
fade = {k: v for k, v in sleeves.items() if k.startswith("MR")}
honest = {k: v for k, v in sleeves.items() if not k.startswith("MR")}
print("=" * 90)
print(f" PORT03 — MASTER (fade + honest) | {IDX[0].date()} -> {IDX[-1].date()} | OOS da {OOS_DATE}")
print("=" * 90)
print(f" {'portafoglio':<22s}{'Ret%':>9s}{'CAGR':>7s}{'DD%':>7s}{'Shrp':>7s}"
f" | {'oRet%':>9s}{'oDD%':>7s}{'oShrp':>7s}")
print(" " + "-" * 86)
def line(label, pr):
f, o = metrics(pr), metrics(pr, lo=SPLIT)
print(f" {label:<22s}{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}")
line(f"FADE only ({len(fade)})", port_returns(fade))
line(f"HONEST only ({len(honest)})", port_returns(honest))
line(f"MASTER equal ({len(sleeves)})", master_returns(sleeves, "equal"))
line("MASTER 50/50 fam", master_returns(sleeves, "5050"))
print(" " + "-" * 86)
pa = yearly_returns(master_returns(sleeves, "equal"))
print(" MASTER equal per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in pa.items()))
print(" -> combinare le due famiglie scorrelate (~0.05) abbassa il DD e alza lo Sharpe.")
if __name__ == "__main__":
run()
@@ -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()
+10 -4
View File
@@ -8,9 +8,15 @@ 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
rispetto a ROT01.
ROT01 base : FULL +679% / OOS +44% / DD 53%
ROT02 SMA100 : FULL +1095% / OOS +98% / DD 40% <-- PnL su, DD giu'
Param-insensitive sulla finestra di regime (SMA100-150). Dettagli in
scripts/analysis/honest_improve.py (rot_improved).
ROT02 k2 SMA100 : FULL +1095% / OOS +98% / DD 40% (versione iniziale)
ROT02 k3 SMA100 : FULL +1303% / OOS +68% / DD 26% <-- DD quasi dimezzato, PnL su
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
@@ -22,7 +28,7 @@ sys.path.insert(0, str(PROJECT_ROOT))
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():
+82
View File
@@ -0,0 +1,82 @@
"""MR03 — Keltner Fade (mean-reversion sul canale ATR).
Stessa tesi di MR01 (i breakout rientrano) ma con banda costruita su ATR
attorno a una EMA, invece che su deviazione standard attorno a una SMA.
Reagisce diversamente a gap e code: edge indipendente, non ridondante con MR01.
Logica:
1. Canale di Keltner: EMA(n) +/- k*ATR(n)
2. ENTRY: close esce sotto la banda inferiore -> LONG (o sopra la superiore -> SHORT)
Ingresso a close[i] (eseguibile dal vivo, nessun look-ahead).
3. EXIT: take-profit alla EMA centrale (il rientro atteso),
stop-loss a sl_atr*ATR oltre l'estremo, time-limit max_bars.
Validazione (netto, fee 0.10% RT reale Deribit, leva 3x, OOS = ultimo 30%):
BTC 1h n=30 k=2.0: +112% OOS, DD 20%
ETH 1h n=50 k=1.5: +1426% OOS, DD 20%
Robusto su TUTTA la griglia n in {14,20,30,50} x k in {1.5,2.0,2.5}
(BTC+ETH 1h sempre positivo OOS).
Ricerca completa: scripts/analysis/strategy_research_v2.py.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from src.strategies.base import Signal
from src.strategies.fade_base import FadeStrategy, atr, trend_distance
class KeltnerFade(FadeStrategy):
name = "MR03_keltner_fade"
description = "Mean-reversion: fada il canale di Keltner (ATR), TP alla EMA"
default_assets = ["BTC", "ETH"]
default_timeframes = ["1h"]
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
**params) -> list[Signal]:
n = params.get("n", 30)
k = params.get("k", 2.0)
sl_atr = params.get("sl_atr", 2.0)
max_bars = params.get("max_bars", 24)
trend_max = params.get("trend_max") # None = filtro disattivo
ema_long = params.get("ema_long", 200)
c = df["close"].values
e = pd.Series(c).ewm(span=n, adjust=False).mean().values
a = atr(df, n)
up, lo = e + k * a, e - k * a
td = trend_distance(df, ema_long) if trend_max is not None else None
signals: list[Signal] = []
for i in range(n + 1, len(c)):
if np.isnan(up[i]) or np.isnan(a[i]):
continue
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
continue
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
d, sl = 1, c[i] - sl_atr * a[i]
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
d, sl = -1, c[i] + sl_atr * a[i]
else:
continue
signals.append(Signal(
idx=i, direction=d, entry_price=c[i],
metadata={"tp": float(e[i]), "sl": float(sl), "max_bars": max_bars},
))
return signals
if __name__ == "__main__":
strat = KeltnerFade()
print("=" * 110)
print(f" MR03 KELTNER FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
print("=" * 110)
for asset, n, k in [("BTC", 30, 2.0), ("ETH", 50, 1.5)]:
r = strat.backtest(asset, "1h", n=n, k=k, sl_atr=2.0, max_bars=24)
if r:
r.strategy_name = f"MR03 {asset} 1h n{n} k{k}"
r.print_summary()
r.print_yearly()
+52 -1
View File
@@ -11,6 +11,7 @@ import pandas as pd
from src.live.cerbero_client import CerberoClient
from src.live.strategy_loader import load_strategy
from src.live.strategy_worker import StrategyWorker
from src.live.pairs_worker import PairsWorker
from src.live.signal_engine import SignalEngine
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"
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 = {
"BTC": "BTC-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
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():
config_path = PROJECT_ROOT / "strategies.yml"
if not config_path.exists():
@@ -143,7 +175,8 @@ def run():
train_lookback_days = 365
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:
print("Nessuna strategia abilitata in strategies.yml")
@@ -162,6 +195,8 @@ def run():
print(f"{w.status_summary}")
for mw in ml_workers:
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")
@@ -172,6 +207,9 @@ def run():
keys.add((w.asset, w.tf))
for mw in ml_workers:
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
# Training iniziale ML
@@ -253,6 +291,15 @@ def run():
except Exception as 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
now = datetime.now(timezone.utc)
if now.minute == 0 and now.second < poll_seconds:
@@ -261,6 +308,8 @@ def run():
lines.append(f" {w.status_summary}")
for mw in ml_workers:
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))
except KeyboardInterrupt:
@@ -277,6 +326,8 @@ def run():
if df is not None and not df.empty:
mw.worker._close_position(float(df["close"].iloc[-1]), "shutdown")
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")
break
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}")
+2
View File
@@ -18,6 +18,8 @@ _REGISTRY: dict[str, type[Strategy]] = {}
# (vedi scripts/analysis/oos_validation.py).
MODULE_MAP = {
"MR01_bollinger_fade": ("MR01_bollinger_fade", "BollingerFade"),
"MR02_donchian_fade": ("MR02_donchian_fade", "DonchianFade"),
"MR07_return_reversal": ("MR07_return_reversal", "ReturnReversal"),
}
+116
View File
@@ -0,0 +1,116 @@
"""Base condivisa per strategie mean-reversion con exit TP/SL/max_bars.
Tutte le strategie fade (MR02/MR03/MR07) generano Signal con metadata
{tp, sl, max_bars} e usano lo stesso backtest fedele: ingresso a close[i]
(eseguibile dal vivo), uscita su take-profit / stop-loss intrabar (high/low)
o time-limit, una posizione per volta (non-overlap), capitale composto,
fee+leva nette. Identico all'engine di scripts/analysis/strategy_research.py.
Le sottoclassi implementano solo generate_signals().
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from src.strategies.base import Strategy, BacktestResult, YearlyStats, TF_MINUTES
from src.data.downloader import load_data
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 trend_distance(df: pd.DataFrame, ema_long: int = 200) -> np.ndarray:
"""Distanza del close dalla EMA lunga, in multipli di ATR(14).
Misura quanto il prezzo e' esteso rispetto al trend di fondo. Le fade
falliscono quando si oppongono a un trend estremo (crolli/parabolic): il
filtro `trend_max` salta i segnali con distanza > soglia. Riduce DD e alza
l'accuratezza (validato OOS: scripts/analysis/risk_portfolio.py).
"""
c = df["close"].values
a = atr(df, 14)
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
with np.errstate(divide="ignore", invalid="ignore"):
return np.abs(c - el) / np.where(a == 0, np.nan, a)
class FadeStrategy(Strategy):
"""Strategy con backtest intrabar TP/SL/max_bars (exit guidati dai metadata)."""
fee_rt = 0.001 # Deribit perp realistico (taker 0.05%/lato)
leverage = 3.0
position_size = 0.15
initial_capital = 1000.0
def backtest(self, asset: str, tf: str = "1h", hold: int = 3,
**params) -> BacktestResult | None:
df = load_data(asset, tf)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
signals = self.generate_signals(df, ts, **params)
if not signals:
return None
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c)
fee = self.fee_rt * self.leverage
capital = peak = float(self.initial_capital)
max_dd = 0.0
total_bars = 0
last_exit = -1
yearly: dict[int, dict] = {}
for sig in signals:
i, d = sig.idx, sig.direction
if i <= last_exit or i + 1 >= n:
continue
entry = c[i]
tp, sl, mb = sig.metadata["tp"], sig.metadata["sl"], sig.metadata["max_bars"]
exit_p = c[min(i + mb, n - 1)]
j = min(i + mb, n - 1)
for step in range(1, mb + 1):
j = i + step
if j >= n:
j = n - 1; exit_p = c[j]; break
hit_sl = (d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl)
hit_tp = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if hit_sl: # conservativo: SL prima del TP nello stesso bar
exit_p = sl; break
if hit_tp:
exit_p = tp; break
if step == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * self.leverage - fee
capital = max(capital + capital * self.position_size * ret, 10.0)
if capital > peak:
peak = capital
max_dd = max(max_dd, (peak - capital) / peak)
total_bars += (j - i)
last_exit = j
year = ts.iloc[i].year
yr = yearly.setdefault(year, {"w": 0, "t": 0, "pnl": 0.0})
yr["t"] += 1
if ret > 0:
yr["w"] += 1
yr["pnl"] += ret * self.initial_capital
all_t = sum(v["t"] for v in yearly.values())
all_w = sum(v["w"] for v in yearly.values())
if all_t == 0:
return None
yearly_stats = [YearlyStats(y, v["t"], v["w"], v["pnl"]) for y, v in sorted(yearly.items())]
return BacktestResult(
strategy_name=self.name, asset=asset, timeframe=tf, params=params,
trades=all_t, wins=all_w, pnl=sum(v["pnl"] for v in yearly.values()),
capital=capital, initial_capital=self.initial_capital,
max_dd=max_dd * 100, time_in_market_pct=total_bars / n * 100,
avg_trade_duration_h=total_bars / all_t * TF_MINUTES.get(tf, 60) / 60,
years_active=len(yearly), yearly=yearly_stats,
)
+98
View File
@@ -21,6 +21,8 @@ strategies:
k: 2.5
sl_atr: 2.0
max_bars: 24
trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD-
ema_long: 200
# ETH: edge positivo ma DD piu' alto (~70%); leva piu' bassa consigliata
- name: MR01_bollinger_fade
@@ -32,3 +34,99 @@ strategies:
k: 2.5
sl_atr: 2.0
max_bars: 24
trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD-
ema_long: 200
# MR02 Donchian fade: fade rottura canale (estremi H/L). Robusto su tutta la
# griglia n x sl_atr e tutte le fee. BTC +879%/+171% OOS (8/9 anni), ETH enorme.
- name: MR02_donchian_fade
asset: BTC
tf: 1h
enabled: true
params:
n: 20
sl_atr: 2.0
max_bars: 24
trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD-
ema_long: 200
- name: MR02_donchian_fade
asset: ETH
tf: 1h
enabled: true
params:
n: 20
sl_atr: 2.0
max_bars: 24
trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD-
ema_long: 200
# MR03 Keltner fade -> spostata in scripts/waste/ (fade piu' debole e ridondante
# con MR01; il filtro trend la peggiorava su BTC). Vedi CLAUDE.md.
# MR07 Return reversal: fade movimento di barra estremo (z dei rendimenti).
# Meccanismo distinto (volatilita' rendimenti, non livelli). Esposizione bassa
# (~8%). BTC +447%/+105% OOS DD25%, ETH +335%/+195% OOS DD46%.
- name: MR07_return_reversal
asset: BTC
tf: 1h
enabled: true
params:
n: 50
k: 3.5
tp_atr: 2.0
sl_atr: 1.5
max_bars: 24
trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD-
ema_long: 200
- name: MR07_return_reversal
asset: ETH
tf: 1h
enabled: true
params:
n: 50
k: 3.5
tp_atr: 2.0
sl_atr: 1.5
max_bars: 24
trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD-
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}