Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a066eb76f | |||
| 7226946911 | |||
| 945c2a2db6 | |||
| 33e3e2a603 | |||
| 666c906907 | |||
| 9ebdfc7a7a | |||
| bcccfde9a0 | |||
| 1af4addbdd | |||
| 22c6080873 | |||
| fd547748dd | |||
| a51129acf6 | |||
| 1b099bb47b | |||
| 783fa5546f | |||
| ad141f080c | |||
| 212427ffa1 |
@@ -90,9 +90,13 @@ Tutte le strategie estendono `src.strategies.base.Strategy`
|
||||
|--------|------|-----------|-----------------------------------|----|------|
|
||||
| **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 |
|
||||
| **MR03** | Keltner Fade | canale ATR attorno a EMA | BTC +112% / ETH +886% | 20-66% | Banda indipendente da Bollinger |
|
||||
| **MR07** | Return Reversal | z dei rendimenti di barra | BTC +105% / ETH +195% | 25-46% | Fada il movimento estremo, exit in ATR; esposizione ~8% |
|
||||
|
||||
> **MR03 Keltner Fade** spostata in `scripts/waste/`: era la fade più debole
|
||||
> (BTC Sharpe 1.22, il filtro trend la peggiorava) e ridondante con MR01 (stessa
|
||||
> idea di banda). Rimuoverla dal portafoglio ne ha *migliorato* le metriche.
|
||||
> La funzione `keltner_fade` resta in `strategy_research_v2.py` come record.
|
||||
|
||||
**Lezione confermata:** l'edge è sempre *mean-reversion* (i breakout rientrano).
|
||||
Il trend-following (Donchian trend, RSI cross) e gli oscillatori senza filtro
|
||||
(RSI revert, ADX-filtered fade) perdono netti → restano scartati.
|
||||
@@ -110,7 +114,7 @@ esteso rispetto al trend di fondo (`|close − EMA(ema_long)| / ATR(14) > trend_
|
||||
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_portfolio.py`).
|
||||
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).
|
||||
|
||||
@@ -118,6 +122,54 @@ Leva non robusta scartate: vol-target sizing e skip-alta-volatilità (peggiorano
|
||||
× 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 da estendere). Verifica: `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 e non ancora
|
||||
validati col worker live a 2 gambe). 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`.
|
||||
2. Backtest **NETTO** dopo fee realistiche Deribit (**0.10% RT** taker, non 0.20%) + leva.
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# 2026-05-28 — Ricerca onesta di nuove strategie (post-squeeze)
|
||||
|
||||
## Contesto e mandato
|
||||
|
||||
Dopo aver scoperto che l'intera famiglia squeeze-breakout era un artefatto di
|
||||
look-ahead (accuratezze 76-82% svanite sotto ingresso eseguibile), il mandato è
|
||||
stato: trovare in modo **onesto** almeno 3 strategie attendibili, testate su ~8
|
||||
anni e su più criptovalute, con le fee incluse nella valutazione, partendo da
|
||||
€1.000 con l'obiettivo (aspirazionale) di €50/giorno. Esplorare anche idee fuori
|
||||
dal comune e l'uso combinato di più crypto e timeframe.
|
||||
|
||||
## Metodologia (engine onesto)
|
||||
|
||||
Tutto il lavoro usa un unico engine condiviso (`scripts/analysis/honest_lab.py`)
|
||||
con questi vincoli anti-illusione:
|
||||
|
||||
1. **Ingresso eseguibile.** Ogni segnale alla barra `i` usa solo dati fino a
|
||||
`close[i]` e l'ingresso avviene a `close[i]` (ciò che il worker live vede e
|
||||
può eseguire). Disponibile anche l'ingresso più conservativo a `open[i+1]`.
|
||||
2. **Uscita realistica.** Take-profit / stop-loss valutati intrabar su `high`/`low`,
|
||||
in modo conservativo (SL prima del TP nello stesso bar), più time-limit.
|
||||
Una posizione per volta (non-overlap), capitale composto.
|
||||
3. **Fee di prim'ordine.** Tutto è NETTO dopo fee round-trip realistiche Deribit
|
||||
(0.10% RT) moltiplicate per la leva (3x), con sweep fino a 0.20% RT.
|
||||
4. **Validazione severa.** FULL + out-of-sample (ultimo 30%) + conteggio anni
|
||||
positivi + sweep fee + griglia parametri + test su **8 crypto**
|
||||
(BTC, ETH, SOL, BNB, XRP, LTC, DOGE, ADA, 2018→2026).
|
||||
|
||||
## Lezione madre
|
||||
|
||||
**Shortare le crypto perde OOS in modo sistematico in questo campione.** Sia la
|
||||
mean-reversion sul lato short, sia il momentum short, crollano fuori campione: il
|
||||
periodo 2018-2026 è net-bull e ogni rialzo "estremo" tende a continuare invece di
|
||||
rientrare. Tutte le configurazioni che sopravvivono oneste sono **long-biased**.
|
||||
È un fatto da dichiarare: parte della performance OOS è correlata al beta rialzista
|
||||
delle crypto. Le strategie aggiungono *timing* sopra quel beta, non lo eliminano.
|
||||
|
||||
## Le 3 strategie selezionate (meccanismi distinti)
|
||||
|
||||
| Codice | Meccanismo | TF | Asset robusti | OOS netto (fee 0.10% RT) | DD | Anni+ |
|
||||
|--------|-----------|----|---------------|--------------------------|----|-------|
|
||||
| **DIP01** | Dip-buy z-score reversion (long-only) | 1h | BTC, ETH, SOL | BTC +59% · ETH +224% · SOL +13% | 23-55% | 6-7/9 |
|
||||
| **TR01** | EMA 20/100 trend-following (long-only) | 4h | BNB, BTC, DOGE, SOL, XRP | BTC +27% · DOGE +53% · XRP +29% | 29-53% | 4-6/8 |
|
||||
| **ROT01** | Rotazione cross-sectional momentum sul paniere | 1d | intero paniere (8) | **+44%** | 53% | 5/7 |
|
||||
|
||||
Dettagli e riproducibilità: `scripts/analysis/honest_final.py` (tabella di
|
||||
validazione unica), `honest_rotation.py`, `honest_trend.py`, `honest_candidates.py`,
|
||||
`honest_diag.py`/`honest_diag2.py` (diagnostica long/short e filtro trend).
|
||||
|
||||
### DIP01 — compra le capitolazioni
|
||||
Long-only: entra quando lo z-score del prezzo rispetto alla media a 50 barre scende
|
||||
sotto −2.5 (capitolazione), prende profitto al rientro verso la media, SL a 2.5·ATR.
|
||||
È la versione robusta e onesta della famiglia mean-reversion: regge lo sweep fee
|
||||
fino a 0.20% RT (BTC +45% OOS anche a 0.20%). Funziona sui major (BTC/ETH/SOL); sugli
|
||||
alt molto parabolici (DOGE/BNB) un dip fisso continua a scendere e non ha edge.
|
||||
|
||||
### TR01 — cavalca i trend
|
||||
Long-only: in posizione quando EMA(20) > EMA(100) sul 4h, altrimenti cash. Poche
|
||||
operazioni (≈200 flip in 8 anni) ⇒ le fee non sono letali. È **complementare** a
|
||||
DIP01: guadagna nei regimi di trend, dove la reversione soffre.
|
||||
|
||||
### ROT01 — la più affidabile e "fuori dal comune"
|
||||
Una sola strategia che usa **tutto il paniere** in un unico book: ogni giorno ordina
|
||||
le 8 crypto per momentum (rendimento a 60 giorni) e alloca a parti uguali alle 2
|
||||
migliori con momentum positivo, il resto in cash. Cattura la *dispersione* tra
|
||||
crypto (gli alt forti corrono molto più di BTC nei bull) senza shortare nulla.
|
||||
È **param-insensitive** (tutte le combinazioni lookback/top-k sono positive OOS) e
|
||||
regge le fee fino a 0.20% RT (+41% OOS). Risponde direttamente alla richiesta di
|
||||
combinare più crypto e un timeframe diverso in un'unica strategia. Per-anno:
|
||||
2020 +33% · 2021 +181% · 2022 −29% (bear) · 2023 +43% · 2024 +59% · 2025 +6% · 2026 −10% (YTD).
|
||||
|
||||
## Diversificazione
|
||||
|
||||
I tre meccanismi coprono regimi diversi e in larga misura anti-correlati:
|
||||
reversione (DIP01), momentum di singolo asset (TR01), forza relativa cross-asset
|
||||
(ROT01). Eseguirli insieme produce una curva di equity più liscia del singolo.
|
||||
|
||||
## Onestà sull'obiettivo €50/giorno
|
||||
|
||||
Va detto chiaramente: **€50/giorno su €1.000 in pochi mesi non è raggiungibile a
|
||||
rischio sano.** Significa ~€18.250/anno, cioè ~1.825%/anno; gli edge onesti qui
|
||||
trovati rendono il 30-60% OOS su orizzonti pluriennali. Le strade per avvicinare
|
||||
quel numero sono: (a) far crescere il capitale per anni con interesse composto —
|
||||
€50/giorno diventa plausibile solo quando il capitale è molto più grande; (b) alzare
|
||||
la leva, che però aumenta proporzionalmente il drawdown (già 23-55%) ed espone a
|
||||
rovina; (c) aggiungere capitale. Nessuna di queste è una scorciatoia. La proposta
|
||||
onesta è un portafoglio delle 3 strategie a leva moderata, puntando alla
|
||||
**sopravvivenza e alla crescita composta**, non al target giornaliero immediato.
|
||||
|
||||
## Miglioramenti (alzare Acc, ridurre DD, migliorare PnL)
|
||||
|
||||
Leve oneste e documentate, senza tuning sui singoli anni
|
||||
(`scripts/analysis/honest_improve.py`, `honest_improve2.py`):
|
||||
|
||||
### ROT02 — dual-momentum overlay (migliora TUTTO)
|
||||
Alla rotazione cross-sectional di ROT01 si aggiunge un overlay di *absolute
|
||||
momentum*: cash quando BTC è sotto la sua media a 100 giorni (mercato risk-off).
|
||||
Taglia i bear di sistema (gli unici anni rossi di ROT01).
|
||||
|
||||
| | FULL% | OOS% | DD% |
|
||||
|---|---|---|---|
|
||||
| ROT01 base | +679 | +44 | 53 |
|
||||
| **ROT02 (SMA100)** | **+1095** | **+98** | **40** |
|
||||
|
||||
PnL su, DD giù: dominanza su tutte e tre le metriche. Param-insensitive (SMA100-150).
|
||||
|
||||
### DIP01 — market-gate (variante low-DD)
|
||||
Comprare i dip solo quando BTC è risk-on alza l'**Acc** (ETH 52→57%, SOL 49→52%) e
|
||||
**dimezza il DD** (ETH 53→23%, SOL 25→13%), al costo di parte della PnL (meno trade).
|
||||
È de-risking, non un pasto gratis: utile per chi vuole una curva più liscia. Su BTC
|
||||
il gate va evitato (i dip migliori di BTC arrivano proprio quando BTC è sotto la
|
||||
propria SMA), quindi DIP01 base resta la versione di riferimento per BTC.
|
||||
|
||||
### PORT01 — portafoglio combinato (il vero motore di risk-reduction)
|
||||
Equal-weight giornaliero ribilanciato delle 3 sleeve anti-correlate
|
||||
(DIP01 BTC + TR01 basket + ROT02). La diversificazione porta il DD del portafoglio
|
||||
**sotto** quello della sleeve meno rischiosa, mantenendo una CAGR alta.
|
||||
|
||||
| Sleeve | ret% | DD% | CAGR% |
|
||||
|--------|------|-----|-------|
|
||||
| DIP01 BTC | +322 | 15 | 31 |
|
||||
| TR01 basket | +591 | 27 | 43 |
|
||||
| ROT02 dual-mom | +771 | 40 | 49 |
|
||||
| **PORTAFOGLIO** | **+642** | **12** | **45** |
|
||||
|
||||
Per-anno portafoglio: 2021 +203% · 2022 **−1%** (bear neutralizzato, era −30% su ROT) ·
|
||||
2023 +47% · 2024 +50% · 2025 +14% · 2026 −2% (YTD). Nessun anno realmente negativo,
|
||||
DD massimo 12%, CAGR 45%. È la configurazione di deployment raccomandata.
|
||||
|
||||
## Prossimi passi
|
||||
|
||||
- Integrare DIP01 nel worker (già compatibile: Signal con tp/sl/max_bars).
|
||||
- Trailing-stop ad ATR per TR01 (per alzarne l'Acc e ridurne ulteriormente il DD).
|
||||
- Estendere il worker per strategie position-based (TR01) e di portafoglio (ROT01).
|
||||
- Backtest del portafoglio combinato con ribilanciamento del capitale.
|
||||
- Walk-forward rolling (oltre al singolo split 70/30) per confermare la stabilità.
|
||||
@@ -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).
|
||||
@@ -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.5–6.1% full / 4.0–4.6% OOS, contro 8.6% (fade) e **12%**
|
||||
(honest) da sole → drawdown ridotto del 35–50%.
|
||||
- Sharpe sale: combinato OOS **4.46** vs honest 2.23 (raddoppia) e batte pure fade (4.14).
|
||||
- CAGR resta ~43–44% (≈ 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 è 2021–2026 (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`.
|
||||
@@ -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%")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -0,0 +1,171 @@
|
||||
"""Harness ONESTO condiviso per esplorare nuove famiglie di strategie.
|
||||
|
||||
Regole NON negoziabili (per non ripetere l'errore squeeze look-ahead):
|
||||
- direzione e prezzo decisi con dati FINO a close[i] incluso, mai con la barra i
|
||||
usata per scegliere la direzione e poi entrare a i-1;
|
||||
- ingresso ESEGUIBILE a close[i];
|
||||
- exit: take-profit / stop-loss intrabar (high/low) e/o time-limit max_bars;
|
||||
tp/sl possono essere None -> exit solo a tempo (utile per stagionalita');
|
||||
- una posizione per volta (non-overlap), capitale composto;
|
||||
- NETTO dopo fee round-trip (default 0.10% RT reale Deribit) e leva;
|
||||
- validazione OOS (held-out, ultimo 30%) + sweep fee 0.00-0.20% RT.
|
||||
|
||||
Le strategie ad alta frequenza muoiono di fee: ogni entry costa fee_rt*lev sul
|
||||
notional. Tienine conto: meno operazioni e edge > costi.
|
||||
|
||||
Asset disponibili: ADA BNB BTC DOGE ETH LTC SOL XRP (1h, 15m; BTC/ETH anche 5m).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.data.downloader import load_data
|
||||
|
||||
FEE_RT = 0.001 # Deribit perp realistico: taker ~0.05%/lato = 0.10% RT
|
||||
LEV = 3.0
|
||||
POS = 0.15
|
||||
OOS_FRAC = 0.30
|
||||
ASSETS = ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"]
|
||||
BARS_PER_YEAR = {"5m": 105120, "15m": 35040, "1h": 8760, "4h": 2190, "1d": 365}
|
||||
|
||||
|
||||
# --------------------------- dati ---------------------------
|
||||
def get_df(asset: str, tf: str) -> pd.DataFrame:
|
||||
"""OHLCV con colonna dt (UTC). tf nativo (5m,15m,1h) o resample da 1h (4h,1d).
|
||||
timestamp resta ms-epoch reale anche dopo il resample (no placeholder)."""
|
||||
if tf in ("5m", "15m", "1h"):
|
||||
df = load_data(asset, tf).reset_index(drop=True)
|
||||
else:
|
||||
base = load_data(asset, "1h").copy()
|
||||
base["dt"] = pd.to_datetime(base["timestamp"], unit="ms", utc=True)
|
||||
base = base.set_index("dt")
|
||||
rule = {"4h": "4h", "1d": "1D"}[tf]
|
||||
agg = base.resample(rule).agg(
|
||||
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
|
||||
).dropna()
|
||||
epoch = pd.Timestamp("1970-01-01", tz="UTC") # ms-epoch portabile (qualsiasi risoluzione)
|
||||
agg["timestamp"] = ((agg.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
|
||||
df = agg.reset_index(drop=True)
|
||||
df["dt"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
return df
|
||||
|
||||
|
||||
def _dt(df: pd.DataFrame) -> pd.DatetimeIndex:
|
||||
return pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
|
||||
# --------------------------- indicatori ---------------------------
|
||||
def atr(df: pd.DataFrame, n: int = 14) -> np.ndarray:
|
||||
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
||||
pc = np.roll(c, 1); pc[0] = c[0]
|
||||
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
|
||||
return pd.Series(tr).rolling(n).mean().values
|
||||
|
||||
|
||||
def ema(x: np.ndarray, n: int) -> np.ndarray:
|
||||
return pd.Series(x).ewm(span=n, adjust=False).mean().values
|
||||
|
||||
|
||||
def rsi(close: np.ndarray, n: int = 14) -> np.ndarray:
|
||||
d = np.diff(close, prepend=close[0])
|
||||
up = pd.Series(np.where(d > 0, d, 0.0)).ewm(alpha=1/n, adjust=False).mean()
|
||||
dn = pd.Series(np.where(d < 0, -d, 0.0)).ewm(alpha=1/n, adjust=False).mean()
|
||||
rs = up / dn.replace(0, np.nan)
|
||||
return (100 - 100 / (1 + rs)).values
|
||||
|
||||
|
||||
# --------------------------- engine ---------------------------
|
||||
def simulate(entries: list[dict], df: pd.DataFrame, fee_rt: float = FEE_RT,
|
||||
lev: float = LEV, pos: float = POS, split: int = -1) -> dict:
|
||||
"""entries: dict con i(idx), d(+1/-1), max_bars; tp/sl opzionali (None=solo tempo).
|
||||
split: se >0, conta solo entries con i>=split (finestra OOS)."""
|
||||
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
||||
n = len(c)
|
||||
ts = _dt(df)
|
||||
cap = peak = 1000.0
|
||||
max_dd = 0.0
|
||||
fee = fee_rt * lev
|
||||
trades = wins = 0
|
||||
last_exit = -1
|
||||
bars_in = 0
|
||||
yearly: dict[int, float] = {}
|
||||
rets: list[float] = []
|
||||
|
||||
for e in entries:
|
||||
i, d = e["i"], e["d"]
|
||||
if i <= last_exit or i + 1 >= n or i < split:
|
||||
continue
|
||||
entry = c[i]
|
||||
tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
|
||||
exit_p = c[min(i + mb, n - 1)]
|
||||
j = min(i + mb, n - 1)
|
||||
for k in range(1, mb + 1):
|
||||
j = i + k
|
||||
if j >= n:
|
||||
exit_p = c[n - 1]; break
|
||||
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
|
||||
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
|
||||
if hit_sl:
|
||||
exit_p = sl; break
|
||||
if hit_tp:
|
||||
exit_p = tp; break
|
||||
if k == mb:
|
||||
exit_p = c[j]
|
||||
ret = (exit_p - entry) / entry * d * lev - fee
|
||||
cb = cap
|
||||
cap = max(cb + cb * pos * ret, 10.0)
|
||||
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
|
||||
trades += 1; wins += ret > 0; bars_in += (j - i)
|
||||
last_exit = j
|
||||
rets.append(ret * pos)
|
||||
yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
|
||||
|
||||
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(len(rets))) if len(rets) > 1 and np.std(rets) > 0 else 0.0
|
||||
return {
|
||||
"trades": trades,
|
||||
"win": wins / trades * 100 if trades else 0.0,
|
||||
"ret": (cap / 1000 - 1) * 100,
|
||||
"dd": max_dd * 100,
|
||||
"sharpe": sharpe,
|
||||
"yearly": yearly,
|
||||
"exposure": bars_in / n * 100 if n else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def evaluate(name: str, entries: list[dict], df: pd.DataFrame,
|
||||
fees=(0.0, 0.0005, 0.001, 0.002)) -> dict:
|
||||
"""Valuta una lista di entries: FULL, OOS e sweep fee. Stampa una riga sintetica."""
|
||||
split = int(len(df) * (1 - OOS_FRAC))
|
||||
full = simulate(entries, df)
|
||||
oos = simulate(entries, df, split=split)
|
||||
sweep = {f: simulate(entries, df, fee_rt=f)["ret"] for f in fees}
|
||||
sweep_oos = {f: simulate(entries, df, fee_rt=f, split=split)["ret"] for f in fees}
|
||||
yrs = full["yearly"]; pos_yrs = sum(1 for v in yrs.values() if v > 0)
|
||||
print(f" {name:<24s} trd={full['trades']:>5d} win={full['win']:>4.1f}% "
|
||||
f"FULL={full['ret']:>+7.0f}% OOS={oos['ret']:>+7.0f}% DD={full['dd']:>4.0f}% "
|
||||
f"oDD={oos['dd']:>4.0f}% Shrp={full['sharpe']:>4.2f} exp={full['exposure']:>4.1f}% "
|
||||
f"anniPos={pos_yrs}/{len(yrs)} | fee0.2%: FULL={sweep[0.002]:>+6.0f} OOS={sweep_oos[0.002]:>+6.0f}")
|
||||
return {"full": full, "oos": oos, "sweep": sweep, "sweep_oos": sweep_oos, "pos_yrs": pos_yrs, "n_yrs": len(yrs)}
|
||||
|
||||
|
||||
def robust(res: dict) -> bool:
|
||||
"""Verdetto onesto: positivo FULL e OOS, regge a fee 0.20% RT, quasi tutti gli anni positivi."""
|
||||
return (res["full"]["ret"] > 0 and res["oos"]["ret"] > 0
|
||||
and res["sweep"][0.002] > 0 and res["sweep_oos"][0.002] > 0
|
||||
and res["pos_yrs"] >= max(res["n_yrs"] - 1, 1))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# smoke test: una stagionalita' banale (hour-of-day) su BTC 1h
|
||||
df = get_df("BTC", "1h"); ts = _dt(df)
|
||||
ents = [{"i": i, "d": 1, "max_bars": 6, "tp": None, "sl": None}
|
||||
for i in range(len(df) - 7) if ts.iloc[i].hour == 0]
|
||||
print("smoke test — BTC long ad ogni 00:00 UTC, hold 6h:")
|
||||
evaluate("seasonality_h0", ents, df)
|
||||
@@ -1,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")
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Validazione FINALE delle 3 strategie oneste selezionate.
|
||||
Per ciascuna: per-asset FULL/OOS/DD/anni-positivi + sweep fee (0/0.05/0.10/0.20% RT).
|
||||
Tutto NETTO, ingresso eseguibile, OOS = ultimo 30%, leva 3x.
|
||||
|
||||
S1 DIP — long-only dip-buy z-score reversion (1h) [regime: reversione]
|
||||
S2 TREND — long-only EMA 20/100 trend-following (4h) [regime: momentum singolo]
|
||||
S3 ROT — rotazione cross-sectional momentum sul paniere (1d) [regime: forza relativa]
|
||||
"""
|
||||
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, simulate, oos_split, available_assets
|
||||
from scripts.analysis.honest_trend import simulate_position, ema_dual_signal, oos as trend_oos
|
||||
from scripts.analysis.honest_rotation import build_panel, simulate_rotation
|
||||
|
||||
FEES = [0.0, 0.0005, 0.001, 0.002]
|
||||
|
||||
|
||||
# ---- S1 DIP ----
|
||||
def dip_entries(df, n=50, z_in=2.5, sl_atr=2.5, 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)
|
||||
z = (c - ma) / np.where(sd == 0, np.nan, sd)
|
||||
ents = []
|
||||
for i in range(n + 14, 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})
|
||||
return ents
|
||||
|
||||
|
||||
def validate_dip(assets):
|
||||
print("\n" + "=" * 100)
|
||||
print(" S1 DIP — long-only dip-buy z-score reversion | 1h | n=50 z=2.5 sl=2.5ATR mb=24")
|
||||
print("=" * 100)
|
||||
print(f" {'Asset':<6s}{'Trd':>6s}{'Win%':>7s}{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'Exp%':>6s}{'AnniP':>8s}"
|
||||
f"{' fee-sweep OOS% (0/0.05/0.10/0.20)':<40s}")
|
||||
ok = 0
|
||||
for a in assets:
|
||||
df = get_df(a, "1h"); ents = dip_entries(df)
|
||||
if len(ents) < 30:
|
||||
continue
|
||||
full = simulate(ents, df); _, oe = oos_split(ents, df); oos = simulate(oe, df)
|
||||
sweep = " ".join(f"{simulate(oe, df, fee_rt=f).ret:+.0f}" for f in FEES)
|
||||
good = full.ret > 0 and oos.ret > 0
|
||||
ok += good
|
||||
print(f" {a:<6s}{full.trades:>6d}{full.win:>7.1f}{full.ret:>+9.0f}{oos.ret:>+9.0f}"
|
||||
f"{full.dd:>6.0f}{full.exposure:>6.0f}{f'{full.pos_years}/{full.n_years}':>8s} [{sweep}]"
|
||||
f"{' OK' if good else ''}")
|
||||
print(f" -> robusto (FULL+OOS>0) su {ok}/{len(assets)} asset")
|
||||
|
||||
|
||||
def validate_trend(assets):
|
||||
print("\n" + "=" * 100)
|
||||
print(" S2 TREND — long-only EMA 20/100 trend | 4h")
|
||||
print("=" * 100)
|
||||
print(f" {'Asset':<6s}{'Flip':>6s}{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'Exp%':>6s}{'AnniP':>8s}")
|
||||
ok = 0
|
||||
for a in assets:
|
||||
df = get_df(a, "4h"); sig = ema_dual_signal(df, 20, 100, long_only=True)
|
||||
full = simulate_position(sig, df); oos = trend_oos(sig, df)
|
||||
good = full["ret"] > 0 and oos["ret"] > 0
|
||||
ok += good
|
||||
print(f" {a:<6s}{full['flips']:>6d}{full['ret']:>+9.0f}{oos['ret']:>+9.0f}"
|
||||
f"{full['dd']:>6.0f}{full['exposure']:>6.0f}{(str(full['pos_years'])+'/'+str(full['n_years'])):>8s}"
|
||||
f"{' OK' if good else ''}")
|
||||
print(f" -> robusto su {ok}/{len(assets)} asset")
|
||||
|
||||
|
||||
def validate_rot(assets):
|
||||
print("\n" + "=" * 100)
|
||||
print(" S3 ROT — rotazione cross-sectional momentum | 1d | lb=60 top2 su tutto il paniere")
|
||||
print("=" * 100)
|
||||
panel = build_panel(assets, "1d")
|
||||
print(f" Paniere {list(panel.columns)} {panel.shape[0]} barre {panel.index[0].date()}->{panel.index[-1].date()}")
|
||||
print(f" {'fee RT':<10s}{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'AnniP':>8s}")
|
||||
for f in FEES:
|
||||
full = simulate_rotation(panel, lookback=60, top_k=2, fee_rt=f)
|
||||
oos = simulate_rotation(panel, lookback=60, top_k=2, fee_rt=f, oos_frac=0.30)
|
||||
anni = str(full['pos_years']) + '/' + str(full['n_years'])
|
||||
print(f" {f*100:>5.2f}%RT {full['ret']:>+9.0f}{oos['ret']:>+9.0f}{full['dd']:>6.0f}{anni:>8s}")
|
||||
# per-anno alla fee reale
|
||||
full = simulate_rotation(panel, lookback=60, top_k=2, fee_rt=0.001)
|
||||
print(" per-anno (fee 0.10%): " + " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(full["yearly"].items())))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
assets = available_assets()
|
||||
print(f"VALIDAZIONE FINALE — asset disponibili: {assets}")
|
||||
validate_dip(assets)
|
||||
validate_trend(assets)
|
||||
validate_rot(assets)
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Miglioramenti ONESTI: alzare Acc, ridurre DD, migliorare PnL senza overfitting.
|
||||
|
||||
Leve usate (tutte robuste e documentate, niente tuning sui singoli anni):
|
||||
1. ABSOLUTE-MOMENTUM overlay (dual momentum): vai in CASH quando il "mercato"
|
||||
(BTC) e' sotto la sua media di lungo periodo -> taglia i bear (2022/2026).
|
||||
2. VOL-TARGETING: scala l'esposizione per puntare a una volatilita' costante
|
||||
-> riduce il DD e liscia la PnL.
|
||||
3. TRAILING STOP ad ATR per il trend (TR01) -> blocca i profitti.
|
||||
Confronto base vs migliorata su FULL + OOS + DD pieno + per-anno.
|
||||
"""
|
||||
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_rotation import build_panel
|
||||
|
||||
LEV, POS = 3.0, 0.15
|
||||
|
||||
|
||||
def _dd(eq: np.ndarray) -> float:
|
||||
peak = eq[0]; mx = 0.0
|
||||
for v in eq:
|
||||
peak = max(peak, v); mx = max(mx, (peak - v) / peak if peak > 0 else 0.0)
|
||||
return mx * 100
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ROT01 migliorata: dual-momentum (cash se BTC < SMA) + vol-target
|
||||
# ============================================================================
|
||||
def rot_improved(lookback=60, top_k=2, gross=0.45, regime_n=100,
|
||||
target_vol=0.0, vol_n=20, fee_rt=FEE_RT, oos_frac=0.0):
|
||||
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")]
|
||||
use_regime = regime_n and regime_n > 1
|
||||
btc_ma = pd.Series(btc).rolling(max(regime_n, 2)).mean().values
|
||||
# vol realizzata del portafoglio equal-weight come proxy di scala
|
||||
mkt_ret = rets.mean(axis=1)
|
||||
rv = pd.Series(mkt_ret).rolling(vol_n).std().values * np.sqrt(365)
|
||||
start = max(lookback + 1, (regime_n + 1) if use_regime else 0, int(T * (1 - oos_frac)) if oos_frac else 0)
|
||||
cap = 1000.0; w = np.zeros(N)
|
||||
eq = [cap]; yearly: dict[int, float] = {}; pos_days = {}; days = {}; reb = {}
|
||||
for i in range(start, T - 1):
|
||||
if use_regime:
|
||||
risk_on = btc[i] > btc_ma[i] if not np.isnan(btc_ma[i]) else False
|
||||
else:
|
||||
risk_on = True
|
||||
mom = P[i] / P[i - lookback] - 1
|
||||
order = np.argsort(mom)[::-1]
|
||||
chosen = [j for j in order if mom[j] > 0][:top_k] if risk_on else []
|
||||
g = gross
|
||||
if target_vol > 0 and not np.isnan(rv[i]) and rv[i] > 0:
|
||||
g = min(gross, gross * target_vol / rv[i]) # solo riduzione (no leva extra)
|
||||
new_w = np.zeros(N)
|
||||
for j in chosen:
|
||||
new_w[j] = g / 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)
|
||||
eq.append(cap)
|
||||
y = int(years[i])
|
||||
yearly[y] = yearly.get(y, 0.0) + pr * 100
|
||||
pos_days[y] = pos_days.get(y, 0) + (pr > 0); days[y] = days.get(y, 0) + 1
|
||||
reb[y] = reb.get(y, 0) + (turnover > 1e-9)
|
||||
return {"ret": (cap / 1000 - 1) * 100, "dd": _dd(np.array(eq)), "yearly": yearly,
|
||||
"pos_years": sum(1 for v in yearly.values() if v > 0), "n_years": len(yearly),
|
||||
"pos_days": pos_days, "days": days, "reb": reb}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DIP01 migliorata: filtro regime (no dip in bear forte) + vol-target sizing
|
||||
# ============================================================================
|
||||
def dip_improved(asset, tf="1h", n=50, z_in=2.5, sl_atr=2.5, max_bars=24,
|
||||
regime_n=200, vol_target=0.0, fee_rt=FEE_RT, oos_frac=0.0):
|
||||
df = get_df(asset, tf)
|
||||
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)
|
||||
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)
|
||||
sma_r = pd.Series(c).rolling(regime_n).mean().values
|
||||
atr_pct = a / c # volatilita' relativa
|
||||
base_vol = np.nanmedian(atr_pct[regime_n:regime_n * 2]) if N > regime_n * 2 else np.nanmedian(atr_pct)
|
||||
fee = fee_rt * LEV
|
||||
cap = 1000.0; last_exit = -1
|
||||
eq = [cap]; yt: dict[int, list] = {}
|
||||
start = max(n + 14, regime_n + 1) if regime_n else n + 14
|
||||
split = int(N * (1 - oos_frac)) if oos_frac else 0
|
||||
for i in range(start, N):
|
||||
if i < split or np.isnan(z[i]) or np.isnan(a[i]):
|
||||
continue
|
||||
if not (z[i] <= -z_in and z[i - 1] > -z_in):
|
||||
continue
|
||||
# filtro regime: salta i dip in bear forte (prezzo molto sotto SMA lunga)
|
||||
if regime_n and not np.isnan(sma_r[i]) and c[i] < sma_r[i] * 0.90:
|
||||
continue
|
||||
if i <= last_exit or i + 1 >= N:
|
||||
continue
|
||||
# vol-target: riduci posizione se ATR% > base (no leva extra)
|
||||
psize = POS
|
||||
if vol_target > 0 and not np.isnan(atr_pct[i]) and atr_pct[i] > 0:
|
||||
psize = POS * min(1.0, base_vol / atr_pct[i])
|
||||
entry = c[i]; tp, sl, mb = ma[i], c[i] - sl_atr * a[i], 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 l[j] <= sl:
|
||||
exit_p = sl; break
|
||||
if h[j] >= tp:
|
||||
exit_p = tp; break
|
||||
if k == mb:
|
||||
exit_p = c[j]
|
||||
ret = (exit_p - entry) / entry * LEV - fee
|
||||
cap = max(cap + cap * psize * ret, 10.0)
|
||||
last_exit = j
|
||||
y = ts.iloc[i].year
|
||||
rec = yt.setdefault(y, [0, 0]); rec[0] += 1; rec[1] += ret > 0
|
||||
eq.append(cap)
|
||||
t = sum(v[0] for v in yt.values()); w = sum(v[1] for v in yt.values())
|
||||
return {"ret": (cap / 1000 - 1) * 100, "dd": _dd(np.array(eq)),
|
||||
"trades": t, "acc": w / t * 100 if t else 0.0,
|
||||
"yt": yt, "pos_years": sum(1 for v in yt.values() if v[1] / max(v[0],1) and v[1]>v[0]*0 and (v[1]>0)), "n_years": len(yt)}
|
||||
|
||||
|
||||
def dip_acc_pnl(asset, **kw):
|
||||
"""ritorna anche FULL e OOS."""
|
||||
full = dip_improved(asset, **kw)
|
||||
oos = dip_improved(asset, oos_frac=0.30, **kw)
|
||||
return full, oos
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 92)
|
||||
print(" ROT01 — BASE vs MIGLIORATA (dual-momentum cash + vol-target)")
|
||||
print("=" * 92)
|
||||
print(f" {'config':<40s}{'FULL%':>9s}{'OOS%':>9s}{'DD%pieno':>10s}{'AnniP':>8s}")
|
||||
b = rot_improved(regime_n=0); bo = rot_improved(regime_n=0, oos_frac=0.30)
|
||||
print(f" {'BASE (no overlay)':<40s}{b['ret']:>+9.0f}{bo['ret']:>+9.0f}{b['dd']:>10.0f}"
|
||||
f"{str(b['pos_years'])+'/'+str(b['n_years']):>8s}")
|
||||
for rn in [100, 150, 200]:
|
||||
f = rot_improved(regime_n=rn); o = rot_improved(regime_n=rn, oos_frac=0.30)
|
||||
print(f" {'+ dual-mom cash (BTC<SMA'+str(rn)+')':<40s}{f['ret']:>+9.0f}{o['ret']:>+9.0f}"
|
||||
f"{f['dd']:>10.0f}{str(f['pos_years'])+'/'+str(f['n_years']):>8s}")
|
||||
for tv in [0.6, 0.8]:
|
||||
f = rot_improved(regime_n=150, target_vol=tv); o = rot_improved(regime_n=150, target_vol=tv, oos_frac=0.30)
|
||||
print(f" {'+ dual-mom150 + volTarget'+str(tv):<40s}{f['ret']:>+9.0f}{o['ret']:>+9.0f}"
|
||||
f"{f['dd']:>10.0f}{str(f['pos_years'])+'/'+str(f['n_years']):>8s}")
|
||||
|
||||
print("\n" + "=" * 92)
|
||||
print(" DIP01 — BASE vs MIGLIORATA (filtro regime + vol-target)")
|
||||
print("=" * 92)
|
||||
print(f" {'asset / config':<34s}{'Trd':>6s}{'Acc%':>7s}{'FULL%':>9s}{'OOS%':>9s}{'DD%pieno':>10s}")
|
||||
for a in ["BTC", "ETH", "SOL"]:
|
||||
for label, kw in [("base", dict(regime_n=0, vol_target=0)),
|
||||
("+regime+volTgt", dict(regime_n=200, vol_target=0.5))]:
|
||||
f, o = dip_acc_pnl(a, **kw)
|
||||
print(f" {a+' '+label:<34s}{f['trades']:>6d}{f['acc']:>7.1f}{f['ret']:>+9.0f}"
|
||||
f"{o['ret']:>+9.0f}{f['dd']:>10.0f}")
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Miglioramenti v2: market-regime gate su DIP01 + PORTAFOGLIO combinato.
|
||||
|
||||
- DIP01 con gate di mercato: compra i dip solo quando BTC e' risk-on (BTC>SMA),
|
||||
cosi' si evitano le capitolazioni dei bear (2018/2022) che peggiorano Acc/DD/PnL.
|
||||
- Portafoglio: equal-weight giornaliero delle 3 strategie migliorate -> la
|
||||
diversificazione taglia il DD mantenendo la PnL (migliora il risk-adjusted).
|
||||
Tutto NETTO, con DD pieno e per-anno.
|
||||
"""
|
||||
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_improve import rot_improved, _dd
|
||||
|
||||
LEV, POS = 3.0, 0.15
|
||||
|
||||
|
||||
def _daily_equity(ts_list, cap_list, idx):
|
||||
"""serie di equity giornaliera (ffill) su un DatetimeIndex comune."""
|
||||
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
|
||||
s = s[~s.index.duplicated(keep="last")].sort_index()
|
||||
daily = s.resample("1D").last().reindex(idx).ffill().bfill()
|
||||
return daily
|
||||
|
||||
|
||||
# ---------- DIP01 con market-regime gate ----------
|
||||
def dip_market_gated(asset, n=50, z_in=2.5, sl_atr=2.5, max_bars=24,
|
||||
market_n=100, fee_rt=FEE_RT, oos_frac=0.0, return_equity=False):
|
||||
df = get_df(asset, "1h")
|
||||
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)
|
||||
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)
|
||||
# regime di mercato: BTC 1h > SMA(market_n in giorni -> *24 barre)
|
||||
btc = get_df("BTC", "1h")
|
||||
bser = pd.Series(btc["close"].values,
|
||||
index=pd.to_datetime(btc["timestamp"], unit="ms", utc=True))
|
||||
bser = bser[~bser.index.duplicated()]
|
||||
bma = bser.rolling(market_n * 24).mean()
|
||||
risk_on = (bser > bma).reindex(ts, method="ffill").fillna(False).values
|
||||
fee = fee_rt * LEV
|
||||
cap = 1000.0; last_exit = -1
|
||||
eq_ts, eq_v = [], []
|
||||
yt: dict[int, list] = {}; ypnl: dict[int, float] = {}
|
||||
split = int(N * (1 - oos_frac)) if oos_frac else 0
|
||||
for i in range(n + 14, N):
|
||||
if i < split or np.isnan(z[i]) or np.isnan(a[i]):
|
||||
continue
|
||||
if not (z[i] <= -z_in and z[i - 1] > -z_in):
|
||||
continue
|
||||
if market_n and not risk_on[i]:
|
||||
continue
|
||||
if i <= last_exit or i + 1 >= N:
|
||||
continue
|
||||
entry = c[i]; tp, sl, mb = ma[i], c[i] - sl_atr * a[i], 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 l[j] <= sl:
|
||||
exit_p = sl; break
|
||||
if h[j] >= tp:
|
||||
exit_p = tp; break
|
||||
if k == mb:
|
||||
exit_p = c[j]
|
||||
ret = (exit_p - entry) / entry * LEV - fee
|
||||
cap = max(cap + cap * POS * ret, 10.0)
|
||||
last_exit = j
|
||||
y = ts.iloc[i].year
|
||||
rec = yt.setdefault(y, [0, 0]); rec[0] += 1; rec[1] += ret > 0
|
||||
ypnl[y] = ypnl.get(y, 0.0) + ret * 100
|
||||
eq_ts.append(ts.iloc[j]); eq_v.append(cap)
|
||||
t = sum(v[0] for v in yt.values()); w = sum(v[1] for v in yt.values())
|
||||
out = {"ret": (cap / 1000 - 1) * 100, "dd": _dd(np.array(eq_v)) if eq_v else 0.0,
|
||||
"trades": t, "acc": w / t * 100 if t else 0.0, "yt": yt, "ypnl": ypnl,
|
||||
"pos_years": sum(1 for v in ypnl.values() if v > 0), "n_years": len(ypnl)}
|
||||
if return_equity:
|
||||
out["eq_ts"], out["eq_v"] = eq_ts, eq_v
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 96)
|
||||
print(" DIP01 — base vs MARKET-GATE (compra dip solo se BTC>SMA100)")
|
||||
print("=" * 96)
|
||||
print(f" {'asset / config':<30s}{'Trd':>6s}{'Acc%':>7s}{'FULL%':>9s}{'OOS%':>9s}{'DD%':>7s}{'AnniP':>8s}")
|
||||
for a in ["BTC", "ETH", "SOL"]:
|
||||
b = dip_market_gated(a, market_n=0); bo = dip_market_gated(a, market_n=0, oos_frac=0.30)
|
||||
g = dip_market_gated(a, market_n=100); go = dip_market_gated(a, market_n=100, oos_frac=0.30)
|
||||
print(f" {a+' base':<30s}{b['trades']:>6d}{b['acc']:>7.1f}{b['ret']:>+9.0f}{bo['ret']:>+9.0f}"
|
||||
f"{b['dd']:>7.0f}{str(b['pos_years'])+'/'+str(b['n_years']):>8s}")
|
||||
print(f" {a+' +gate100':<30s}{g['trades']:>6d}{g['acc']:>7.1f}{g['ret']:>+9.0f}{go['ret']:>+9.0f}"
|
||||
f"{g['dd']:>7.0f}{str(g['pos_years'])+'/'+str(g['n_years']):>8s}")
|
||||
|
||||
# ---------- PORTAFOGLIO combinato (3 sleeve diversificate) ----------
|
||||
print("\n" + "=" * 96)
|
||||
print(" PORTAFOGLIO equal-weight giornaliero (ribilanciato): DIP01 + TR01-basket + ROT02")
|
||||
print("=" * 96)
|
||||
idx = pd.date_range("2021-01-01", "2026-05-26", freq="1D", tz="UTC")
|
||||
# sleeve 1: DIP01 base su BTC (la migliore)
|
||||
d = dip_market_gated("BTC", market_n=0, return_equity=True)
|
||||
eq_dip = _norm(_daily_equity(d["eq_ts"], d["eq_v"], idx))
|
||||
# sleeve 2: TR01 equal-weight su {BNB,BTC,DOGE,SOL,XRP}
|
||||
eq_tr = _norm(_tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], idx))
|
||||
# sleeve 3: ROT02 dual-momentum
|
||||
eq_rot = _norm(_rot_daily_equity(idx))
|
||||
members = {"DIP01_BTC": eq_dip, "TR01_basket": eq_tr, "ROT02_dualmom": eq_rot}
|
||||
# ribilanciamento giornaliero equal-weight: media dei rendimenti giornalieri
|
||||
drets = pd.DataFrame({k: v.pct_change().fillna(0) for k, v in members.items()})
|
||||
port_ret = drets.mean(axis=1)
|
||||
combo = (1 + port_ret).cumprod()
|
||||
print(f" Periodo {idx[0].date()} -> {idx[-1].date()} (leva/pos gia' incluse nelle sleeve)")
|
||||
print(f" {'sleeve':<16s}{'ret%':>9s}{'DD%':>7s}{'CAGR%':>8s}")
|
||||
yrs = (idx[-1] - idx[0]).days / 365.25
|
||||
for name, s in members.items():
|
||||
r = (s.iloc[-1] / s.iloc[0] - 1) * 100
|
||||
cagr = ((s.iloc[-1] / s.iloc[0]) ** (1 / yrs) - 1) * 100
|
||||
print(f" {name:<16s}{r:>+9.0f}{_dd(s.values):>7.0f}{cagr:>8.0f}")
|
||||
r = (combo.iloc[-1] / combo.iloc[0] - 1) * 100
|
||||
cagr = ((combo.iloc[-1] / combo.iloc[0]) ** (1 / yrs) - 1) * 100
|
||||
print(f" {'PORTAFOGLIO':<16s}{r:>+9.0f}{_dd(combo.values):>7.0f}{cagr:>8.0f} <-- DD molto piu' basso, CAGR solida")
|
||||
# per-anno del portafoglio
|
||||
pa = (port_ret.groupby(port_ret.index.year).apply(lambda x: ((1 + x).prod() - 1) * 100))
|
||||
print(" Portafoglio per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in pa.items()))
|
||||
|
||||
|
||||
def _norm(s):
|
||||
return s / s.iloc[0]
|
||||
|
||||
|
||||
def _tr_basket_daily(assets, idx):
|
||||
"""equity giornaliera media di TR01 (EMA20/100 long-only, 4h) sul paniere."""
|
||||
eqs = []
|
||||
for a in assets:
|
||||
df = get_df(a, "4h"); c = df["close"].values; n = len(c)
|
||||
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
|
||||
cap = 1000.0; cur = 0.0; fee = FEE_RT / 2 * LEV
|
||||
tl, cl = [], []
|
||||
for i in range(n - 1):
|
||||
s = sig[i]
|
||||
if s != cur:
|
||||
cap -= cap * POS * fee * abs(s - cur); cur = s
|
||||
cap = max(cap * (1 + POS * LEV * (c[i + 1] - c[i]) / c[i] * cur), 10.0)
|
||||
tl.append(ts.iloc[i]); cl.append(cap)
|
||||
eqs.append(_norm(_daily_equity(tl, cl, idx)))
|
||||
return _norm(pd.concat(eqs, axis=1).mean(axis=1))
|
||||
|
||||
|
||||
def _rot_daily_equity(idx):
|
||||
"""equity giornaliera della ROT01 dual-momentum (ricostruita bar-by-bar)."""
|
||||
from scripts.analysis.honest_rotation import build_panel
|
||||
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
|
||||
btc = P[:, cols.index("BTC")]; bma = pd.Series(btc).rolling(100).mean().values
|
||||
cap = 1000.0; w = np.zeros(N); ts_list = []; cap_list = []
|
||||
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][: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)
|
||||
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)
|
||||
ts_list.append(panel.index[i]); cap_list.append(cap)
|
||||
s = _daily_equity(ts_list, cap_list, idx); return s / s.iloc[0]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,192 @@
|
||||
"""honest_lab — laboratorio di ricerca strategie ONESTO e fee-aware.
|
||||
|
||||
Principi (per non ripetere l'errore look-ahead della famiglia squeeze):
|
||||
1. Ogni segnale a barra i usa SOLO dati fino a close[i]. Ingresso a close[i]
|
||||
(eseguibile dal vivo: il worker vede la candela chiusa ed entra). Opzione
|
||||
di robustezza: ingresso a open[i+1] (ancora piu' conservativo).
|
||||
2. Uscita TP/SL valutata intrabar su high/low, conservativa: SL prima del TP
|
||||
nello stesso bar. Time-limit max_bars. Una posizione per volta (non-overlap).
|
||||
3. Tutto NETTO dopo fee round-trip realistiche (0.10% Deribit) * leva.
|
||||
4. Validazione: FULL + OOS (held-out ultimo 30%) + per-anno + sweep fee
|
||||
+ griglia parametri + su PIU' asset. Niente di tutto cio' -> scartata.
|
||||
|
||||
Engine condiviso riusabile da tutte le strategie candidate.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
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 # noqa: E402
|
||||
|
||||
FEE_RT = 0.001 # Deribit perp realistico: taker ~0.05%/lato = 0.10% RT
|
||||
LEV = 3.0
|
||||
POS = 0.15
|
||||
OOS_FRAC = 0.30
|
||||
DATA_DIR = PROJECT_ROOT / "data" / "raw"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# dati
|
||||
# ----------------------------------------------------------------------------
|
||||
_CACHE: dict[tuple[str, str], pd.DataFrame] = {}
|
||||
|
||||
|
||||
def available_assets() -> list[str]:
|
||||
out = []
|
||||
for p in sorted(DATA_DIR.glob("*_1h.parquet")):
|
||||
name = p.stem.replace("_1h", "").upper()
|
||||
if name not in ("BTC_DVOL", "ETH_DVOL"):
|
||||
out.append(name)
|
||||
return out
|
||||
|
||||
|
||||
def get_df(asset: str, tf: str) -> pd.DataFrame:
|
||||
"""tf nativo (15m,1h) o resample da 1h (2h,4h,6h,12h,1d)."""
|
||||
key = (asset, tf)
|
||||
if key in _CACHE:
|
||||
return _CACHE[key]
|
||||
if tf in ("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 = {"2h": "2h", "4h": "4h", "6h": "6h", "12h": "12h", "1d": "1D"}[tf]
|
||||
agg = base.resample(rule).agg(
|
||||
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
|
||||
).dropna()
|
||||
# l'indice puo' essere datetime64[ms] o [ns]: forza ms in modo robusto
|
||||
agg["timestamp"] = agg.index.values.astype("datetime64[ms]").astype("int64")
|
||||
df = agg.reset_index(drop=True)
|
||||
df = df[["timestamp", "open", "high", "low", "close", "volume"]].copy()
|
||||
_CACHE[key] = df
|
||||
return df
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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 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
|
||||
|
||||
|
||||
def ema(close: np.ndarray, n: int) -> np.ndarray:
|
||||
return pd.Series(close).ewm(span=n, adjust=False).mean().values
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# engine
|
||||
# ----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class SimResult:
|
||||
trades: int
|
||||
win: float
|
||||
ret: float # ritorno % netto composto su 1000
|
||||
dd: float
|
||||
exposure: float
|
||||
yearly: dict[int, float]
|
||||
|
||||
@property
|
||||
def pos_years(self) -> int:
|
||||
return sum(1 for v in self.yearly.values() if v > 0)
|
||||
|
||||
@property
|
||||
def n_years(self) -> int:
|
||||
return len(self.yearly)
|
||||
|
||||
|
||||
def simulate(entries: list[dict], df: pd.DataFrame, fee_rt: float = FEE_RT,
|
||||
lev: float = LEV, pos: float = POS, entry_on_open: bool = False) -> SimResult:
|
||||
"""entries: dict {i, d(+1/-1), tp, sl, max_bars}.
|
||||
|
||||
entry_on_open=True -> ingresso a open[i+1] invece di close[i] (robustezza).
|
||||
"""
|
||||
o, h, l, c = (df["open"].values, df["high"].values,
|
||||
df["low"].values, df["close"].values)
|
||||
n = len(c)
|
||||
cap = peak = 1000.0
|
||||
max_dd = 0.0
|
||||
fee = fee_rt * lev
|
||||
trades = wins = 0
|
||||
last_exit = -1
|
||||
bars_in = 0
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
yearly: dict[int, float] = {}
|
||||
|
||||
for e in entries:
|
||||
i, d = e["i"], e["d"]
|
||||
ei = i + 1 if entry_on_open else i # barra di ingresso
|
||||
if ei <= last_exit or ei + 1 >= n:
|
||||
continue
|
||||
entry = o[ei] if entry_on_open else c[i]
|
||||
tp, sl, mb = e["tp"], e["sl"], e["max_bars"]
|
||||
exit_p = c[min(ei + mb, n - 1)]
|
||||
j = min(ei + mb, n - 1)
|
||||
for k in range(1, mb + 1):
|
||||
j = ei + k
|
||||
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 k == mb:
|
||||
exit_p = c[j]
|
||||
ret = (exit_p - entry) / entry * d * lev - fee
|
||||
cap = max(cap + cap * pos * ret, 10.0)
|
||||
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
|
||||
trades += 1; wins += ret > 0; bars_in += (j - ei)
|
||||
last_exit = j
|
||||
yr = ts.iloc[i].year
|
||||
yearly[yr] = yearly.get(yr, 0.0) + ret * 100
|
||||
return SimResult(
|
||||
trades=trades,
|
||||
win=wins / trades * 100 if trades else 0.0,
|
||||
ret=(cap / 1000 - 1) * 100,
|
||||
dd=max_dd * 100,
|
||||
exposure=bars_in / n * 100,
|
||||
yearly=yearly,
|
||||
)
|
||||
|
||||
|
||||
def oos_split(entries: list[dict], df: pd.DataFrame, frac: float = OOS_FRAC):
|
||||
split = int(len(df) * (1 - frac))
|
||||
ins = [e for e in entries if e["i"] < split]
|
||||
oos = [e for e in entries if e["i"] >= split]
|
||||
return ins, oos
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# criterio di accettazione
|
||||
# ----------------------------------------------------------------------------
|
||||
def verdict(full: SimResult, oos: SimResult) -> bool:
|
||||
"""Strategia attendibile su un singolo asset/tf."""
|
||||
if full.trades < 30:
|
||||
return False
|
||||
if full.ret <= 0 or oos.ret <= 0:
|
||||
return False
|
||||
if full.pos_years < max(full.n_years - 1, 1):
|
||||
return False
|
||||
if full.dd > 45:
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tabella unica consolidata: PnL% NETTO per anno, tutte le strategie a confronto.
|
||||
Colonne: DIP01(BTC) · TR01(basket) · ROT01(base) · ROT02(dual-mom) · PORTAFOGLIO.
|
||||
Ultima riga: TOT e DD full-period.
|
||||
"""
|
||||
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_improve import _dd
|
||||
from scripts.analysis.honest_improve2 import (
|
||||
dip_market_gated, _daily_equity, _norm, _tr_basket_daily,
|
||||
)
|
||||
from scripts.analysis.honest_rotation import build_panel
|
||||
|
||||
LEV, POS = 3.0, 0.15
|
||||
|
||||
|
||||
def rot_daily(idx, regime_n=0, lookback=60, top_k=2, gross=0.45):
|
||||
"""equity giornaliera della rotazione, con/senza overlay dual-momentum."""
|
||||
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
|
||||
btc = P[:, cols.index("BTC")]
|
||||
bma = pd.Series(btc).rolling(max(regime_n, 2)).mean().values
|
||||
use_reg = regime_n and regime_n > 1
|
||||
cap = 1000.0; w = np.zeros(N); tl, cl = [], []
|
||||
start = max(lookback + 1, regime_n + 1 if use_reg else 0)
|
||||
for i in range(start, T - 1):
|
||||
risk_on = (btc[i] > bma[i]) if (use_reg and not np.isnan(bma[i])) else True
|
||||
mom = P[i] / P[i - lookback] - 1; order = np.argsort(mom)[::-1]
|
||||
chosen = [j for j in order if mom[j] > 0][:top_k] 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)
|
||||
tl.append(panel.index[i]); cl.append(cap)
|
||||
return _norm(_daily_equity(tl, cl, idx))
|
||||
|
||||
|
||||
def year_pnl(eq):
|
||||
return {int(y): (g.iloc[-1] / g.iloc[0] - 1) * 100 for y, g in _norm(eq).groupby(eq.index.year)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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)
|
||||
cols = {
|
||||
"DIP01(BTC)": _norm(_daily_equity(d["eq_ts"], d["eq_v"], idx)),
|
||||
"TR01(bskt)": _norm(_tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], idx)),
|
||||
"ROT01": rot_daily(idx, regime_n=0),
|
||||
"ROT02": rot_daily(idx, regime_n=100),
|
||||
}
|
||||
drets = pd.DataFrame({k: v.pct_change().fillna(0) for k, v in {
|
||||
"DIP01(BTC)": cols["DIP01(BTC)"], "TR01(bskt)": cols["TR01(bskt)"], "ROT02": cols["ROT02"]
|
||||
}.items()})
|
||||
cols["PORTAF."] = (1 + drets.mean(axis=1)).cumprod()
|
||||
|
||||
names = list(cols)
|
||||
py = {n: year_pnl(cols[n]) for n in names}
|
||||
years = sorted({y for n in names for y in py[n]})
|
||||
|
||||
print("=" * 78)
|
||||
print(" PnL% NETTO PER ANNO — confronto strategie (leva 3x, fee 0.10% RT)")
|
||||
print("=" * 78)
|
||||
print(f" {'Anno':>6s}" + "".join(f"{n:>12s}" for n in names))
|
||||
print(" " + "-" * 72)
|
||||
for y in years:
|
||||
print(f" {y:>6d}" + "".join(f"{py[n].get(y, float('nan')):>+12.0f}" if y in py[n] else f"{'-':>12s}" for n in names))
|
||||
print(" " + "-" * 72)
|
||||
print(f" {'TOT%':>6s}" + "".join(f"{(cols[n].iloc[-1]/cols[n].iloc[0]-1)*100:>+12.0f}" for n in names))
|
||||
print(f" {'DDfull':>6s}" + "".join(f"{_dd(cols[n].values):>12.0f}" for n in names))
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Strategia #3 candidata: ROTAZIONE cross-sectional momentum (multi-crypto).
|
||||
Una sola strategia che usa l'INTERO paniere: ad ogni ribilanciamento alloca il
|
||||
capitale agli asset con momentum migliore (long-only). Cattura la dispersione tra
|
||||
crypto (gli alt forti corrono molto piu' di BTC nei bull) senza shortare nulla.
|
||||
|
||||
Onesto: i pesi a close[i] usano solo rendimenti passati; il rendimento del bar
|
||||
i->i+1 e' realizzato con quei pesi. Fee sul turnover. Allineamento per timestamp.
|
||||
"""
|
||||
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 get_df, available_assets, FEE_RT # noqa: E402
|
||||
|
||||
LEV = 3.0
|
||||
GROSS = 0.45 # esposizione lorda = LEV*POS del singolo (0.15*3) per confronto equo
|
||||
|
||||
|
||||
def build_panel(assets: list[str], tf: str) -> pd.DataFrame:
|
||||
"""Matrice close allineata per timestamp (inner join)."""
|
||||
closes = {}
|
||||
for a in assets:
|
||||
df = get_df(a, tf)
|
||||
s = pd.Series(df["close"].values,
|
||||
index=pd.to_datetime(df["timestamp"], unit="ms", utc=True))
|
||||
closes[a] = s[~s.index.duplicated()]
|
||||
panel = pd.DataFrame(closes).dropna()
|
||||
return panel
|
||||
|
||||
|
||||
def simulate_rotation(panel: pd.DataFrame, lookback=30, top_k=2,
|
||||
fee_rt=FEE_RT, gross=GROSS, abs_filter=True,
|
||||
oos_frac=0.0) -> dict:
|
||||
"""Ad ogni barra: ranking per rendimento passato `lookback`; pesi uguali sui
|
||||
top_k con momentum>0 (se abs_filter); altrimenti cash. gross = esposizione tot.
|
||||
oos_frac>0: parte a investire solo dall'ultimo frac del campione."""
|
||||
P = panel.values
|
||||
T, N = P.shape
|
||||
rets = np.zeros_like(P)
|
||||
rets[1:] = P[1:] / P[:-1] - 1
|
||||
years = panel.index.year.values
|
||||
start = max(lookback + 1, int(T * (1 - oos_frac)) if oos_frac else lookback + 1)
|
||||
cap = peak = 1000.0
|
||||
max_dd = 0.0
|
||||
w = np.zeros(N)
|
||||
yearly: dict[int, float] = {}
|
||||
turn_total = 0.0
|
||||
for i in range(start, T - 1):
|
||||
mom = P[i] / P[i - lookback] - 1
|
||||
order = np.argsort(mom)[::-1]
|
||||
new_w = np.zeros(N)
|
||||
chosen = [j for j in order if (mom[j] > 0 or not abs_filter)][:top_k]
|
||||
if chosen:
|
||||
for j in chosen:
|
||||
new_w[j] = gross / len(chosen)
|
||||
# fee sul turnover (one-way = fee_rt/2 su ogni variazione di peso)
|
||||
turnover = np.abs(new_w - w).sum()
|
||||
cap -= cap * turnover * (fee_rt / 2)
|
||||
turn_total += turnover
|
||||
w = new_w
|
||||
port_ret = float(np.dot(w, rets[i + 1])) # rendimento bar i->i+1
|
||||
cap = max(cap * (1 + port_ret), 10.0)
|
||||
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
|
||||
yearly[years[i]] = yearly.get(years[i], 0.0) + port_ret * 100
|
||||
return {
|
||||
"ret": (cap / 1000 - 1) * 100,
|
||||
"dd": max_dd * 100,
|
||||
"turnover": turn_total,
|
||||
"yearly": yearly,
|
||||
"pos_years": sum(1 for v in yearly.values() if v > 0),
|
||||
"n_years": len(yearly),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
assets = available_assets()
|
||||
print(f"ROTATION cross-sectional momentum — fee {FEE_RT*100:.2f}% RT, gross {GROSS} | OOS 30%")
|
||||
print(f" Paniere: {assets}")
|
||||
for tf in ["1d", "4h"]:
|
||||
panel = build_panel(assets, tf)
|
||||
print(f"\n === {tf} === panel {panel.shape[0]} barre, {panel.index[0].date()} -> {panel.index[-1].date()}")
|
||||
print(f" {'config':<22s}{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'Turn':>7s}{'AnniP':>8s}")
|
||||
for lb in [20, 30, 60, 90]:
|
||||
for k in [1, 2, 3]:
|
||||
full = simulate_rotation(panel, lookback=lb, top_k=k)
|
||||
oos = simulate_rotation(panel, lookback=lb, top_k=k, oos_frac=0.30)
|
||||
anni = f"{full['pos_years']}/{full['n_years']}"
|
||||
print(f" lb{lb:<3d} top{k:<14d}{full['ret']:>+9.0f}{oos['ret']:>+9.0f}"
|
||||
f"{full['dd']:>6.0f}{full['turnover']:>7.0f}{anni:>8s}")
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Strategia #3 candidata: time-series momentum / trend (TSMOM).
|
||||
Posizione continua decisa a close[i] dai dati passati; fee SOLO sui cambi di
|
||||
posizione (poche operazioni su TF alto = fee non letali). Niente look-ahead:
|
||||
il rendimento del bar i->i+1 usa la direzione decisa a close[i].
|
||||
"""
|
||||
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 ema, get_df, available_assets, FEE_RT # noqa: E402
|
||||
|
||||
LEV = 3.0
|
||||
POS = 0.15
|
||||
|
||||
|
||||
def simulate_position(sig: np.ndarray, df: pd.DataFrame, fee_rt: float = FEE_RT,
|
||||
lev: float = LEV, pos: float = POS) -> dict:
|
||||
"""sig[i] in {-1,0,1} = direzione tenuta nel bar i->i+1, decisa a close[i].
|
||||
Fee one-way = fee_rt/2 su ogni unita' di variazione posizione."""
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
cap = peak = 1000.0
|
||||
max_dd = 0.0
|
||||
cur = 0.0
|
||||
flips = 0
|
||||
bars_in = 0
|
||||
yearly: dict[int, float] = {}
|
||||
for i in range(n - 1):
|
||||
s = sig[i]
|
||||
if not np.isfinite(s):
|
||||
s = 0.0
|
||||
if s != cur:
|
||||
cap -= cap * pos * (fee_rt / 2) * lev * abs(s - cur)
|
||||
flips += abs(s - cur) > 0
|
||||
cur = s
|
||||
pr = (c[i + 1] - c[i]) / c[i]
|
||||
bar_ret = pos * lev * pr * cur
|
||||
cap = max(cap * (1 + bar_ret), 10.0)
|
||||
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
|
||||
if cur != 0:
|
||||
bars_in += 1
|
||||
yr = ts.iloc[i].year
|
||||
yearly[yr] = yearly.get(yr, 0.0) + bar_ret * 100
|
||||
return {
|
||||
"ret": (cap / 1000 - 1) * 100,
|
||||
"dd": max_dd * 100,
|
||||
"flips": flips,
|
||||
"exposure": bars_in / n * 100,
|
||||
"yearly": yearly,
|
||||
"pos_years": sum(1 for v in yearly.values() if v > 0),
|
||||
"n_years": len(yearly),
|
||||
}
|
||||
|
||||
|
||||
def tsmom_signal(df, lookback=30, long_only=False):
|
||||
"""+1 se close>close[-lookback], -1 (o 0 se long_only) altrimenti."""
|
||||
c = df["close"].values
|
||||
sig = np.zeros(len(c))
|
||||
for i in range(lookback, len(c)):
|
||||
up = c[i] > c[i - lookback]
|
||||
sig[i] = 1.0 if up else (0.0 if long_only else -1.0)
|
||||
return sig
|
||||
|
||||
|
||||
def ema_dual_signal(df, fast=20, slow=100, long_only=False):
|
||||
"""+1 se EMA_fast>EMA_slow."""
|
||||
c = df["close"].values
|
||||
ef, es = ema(c, fast), ema(c, slow)
|
||||
sig = np.where(ef > es, 1.0, 0.0 if long_only else -1.0)
|
||||
sig[:slow] = 0.0
|
||||
return sig
|
||||
|
||||
|
||||
def oos(sig, df, frac=0.30):
|
||||
split = int(len(df) * (1 - frac))
|
||||
s2 = sig.copy(); s2[:split] = 0.0
|
||||
return simulate_position(s2, df)
|
||||
|
||||
|
||||
def show(label, df, sig):
|
||||
full = simulate_position(sig, df)
|
||||
o = oos(sig, df)
|
||||
anni = f"{full['pos_years']}/{full['n_years']}"
|
||||
print(f" {label:<26s}{full['flips']:>6d}{full['ret']:>+9.0f}{o['ret']:>+9.0f}"
|
||||
f"{full['dd']:>6.0f}{full['exposure']:>6.0f}{anni:>8s}")
|
||||
return full, o
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
assets = available_assets()
|
||||
print(f"TSMOM / trend — fee {FEE_RT*100:.2f}% RT, leva3x pos15% | OOS30%")
|
||||
for tf in ["1d", "4h"]:
|
||||
print(f"\n ###### TF {tf} ######")
|
||||
for a in assets:
|
||||
df = get_df(a, tf)
|
||||
print(f"\n === {a} {tf} === {'Flip':>5s}{'FULL%':>8s}{'OOS%':>8s}{'DD%':>6s}{'Exp%':>6s}{'AnniP':>8s}")
|
||||
show("TSMOM lb30 long/short", df, tsmom_signal(df, 30))
|
||||
show("TSMOM lb30 long-only", df, tsmom_signal(df, 30, long_only=True))
|
||||
show("TSMOM lb90 long/short", df, tsmom_signal(df, 90))
|
||||
show("EMA 20/100 long/short", df, ema_dual_signal(df, 20, 100))
|
||||
show("EMA 20/100 long-only", df, ema_dual_signal(df, 20, 100, long_only=True))
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -1,139 +0,0 @@
|
||||
"""Migliorare Acc e ridurre DD sulle fade (MR01/MR02/MR03/MR07) senza overfit.
|
||||
|
||||
Leve testate, ognuna misurata FULL e OOS (ultimo 30%) per non illudersi:
|
||||
- vol-target sizing: size per trade ~ 1/distanza-SL -> rischio costante, DD piu' liscio
|
||||
- filtro vol regime: salta i trade con ATR% in coda alta (periodi caotici)
|
||||
- filtro anti-trend: non fadare contro un trend forte (|close-EMA_long|/ATR grande)
|
||||
- portfolio: equity curve combinata delle 4 strategie su un conto unico
|
||||
|
||||
Engine fedele (ingresso close[i], exit TP/SL intrabar o time-limit, non-overlap,
|
||||
capitale composto) con sizing per-trade. 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, keltner_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)
|
||||
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)),
|
||||
"MR03": (keltner_fade, dict(n=30, k=2.0, 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_ETH3 = dict(STRATS); STRATS_ETH3["MR03"] = (keltner_fade, dict(n=50, k=2.0, sl_atr=2.0, max_bars=24))
|
||||
|
||||
|
||||
def add_context(ents, df, ema_long=200):
|
||||
"""Aggiunge a ogni entry: sl_dist_pct, 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 main():
|
||||
for asset in ["BTC", "ETH"]:
|
||||
df = load_data(asset, "1h")
|
||||
split = int(len(df) * (1 - OOS_FRAC))
|
||||
table = STRATS_ETH3 if asset == "ETH" else STRATS
|
||||
# quantili vol globali per la soglia (p90)
|
||||
print("\n" + "=" * 110)
|
||||
print(f" {asset} 1h — leve di riduzione DD / aumento Acc | NETTO fee 0.10% RT, leva 3x")
|
||||
print("=" * 110)
|
||||
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 table.items():
|
||||
ents = add_context(fn(df, **params), df)
|
||||
apct = np.array([e["atr_pct"] for e in ents])
|
||||
p85 = float(np.quantile(apct, 0.85))
|
||||
tdist = np.array([e["trend_dist"] for e in ents])
|
||||
t90 = float(np.quantile(tdist, 0.90))
|
||||
|
||||
base_f = simulate(ents, df); base_o = simulate(ents, df, split=split)
|
||||
line(f"{nm} base", base_f, base_o)
|
||||
vt_f = simulate(ents, df, sizer=vol_target_sizer()); vt_o = simulate(ents, df, split=split, sizer=vol_target_sizer())
|
||||
line(f"{nm} +volTarget", vt_f, vt_o)
|
||||
vs_f = simulate(ents, df, vol_skip=p85); vs_o = simulate(ents, df, split=split, vol_skip=p85)
|
||||
line(f"{nm} +volSkip(p85)", vs_f, vs_o)
|
||||
ts_f = simulate(ents, df, trend_skip=t90); ts_o = simulate(ents, df, split=split, trend_skip=t90)
|
||||
line(f"{nm} +trendSkip(p90)", ts_f, ts_o)
|
||||
allf = simulate(ents, df, sizer=vol_target_sizer(), vol_skip=p85, trend_skip=t90)
|
||||
allo = simulate(ents, df, split=split, sizer=vol_target_sizer(), vol_skip=p85, trend_skip=t90)
|
||||
line(f"{nm} +ALL", allf, allo)
|
||||
print(" " + "-" * 106)
|
||||
print("\n Shrp = Sharpe annuo-naive sui ritorni per-trade. oXxx = stessa metrica su OOS (ultimo 30%).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
@@ -1,163 +0,0 @@
|
||||
"""Affina il filtro trend (soglia assoluta ATR) e costruisce il portafoglio combinato.
|
||||
|
||||
Due risultati:
|
||||
(1) trend filter: salta le fade quando |close-EMA200|/ATR > soglia (non fadare un
|
||||
trend estremo). Soglia ASSOLUTA in multipli di ATR -> stessa regola per tutte
|
||||
le strategie/asset, basso rischio di overfit. Sweep soglie, FULL e OOS.
|
||||
(2) portafoglio: equity curve combinata delle 4 strategie sullo stesso conto
|
||||
(rischio diviso fra N posizioni). Curve poco correlate -> DD aggregato << DD
|
||||
della singola strategia. Confronto singola vs portafoglio, con/senza filtro.
|
||||
"""
|
||||
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, keltner_fade, return_reversal
|
||||
|
||||
FEE_RT, LEV, INIT, OOS_FRAC = 0.001, 3.0, 1000.0, 0.30
|
||||
|
||||
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)),
|
||||
"MR03": (keltner_fade, dict(n=30, k=2.0, 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); STRATS_ETH["MR03"] = (keltner_fade, dict(n=50, k=2.0, sl_atr=2.0, max_bars=24))
|
||||
|
||||
|
||||
def build_trades(ents, df, lev=LEV, fee_rt=FEE_RT, trend_max=None, ema_long=200):
|
||||
"""Ritorna 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, ts, pos=0.15, 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=0.15, split=-1):
|
||||
"""Equity curve di uno sleeve su sotto-conto indipendente (capitale INIT, pos fissa).
|
||||
Ritorna array lungo n_bars (step aggiornato alla chiusura di ogni trade)."""
|
||||
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 # da j in poi il sotto-conto vale cap
|
||||
return eq
|
||||
|
||||
|
||||
def metrics_portfolio(strat_trades, n_bars, ts, pos=0.15, split=-1):
|
||||
"""Portafoglio equipesato: capitale diviso in N sotto-conti indipendenti, ciascuno
|
||||
con la sua strategia a `pos` fisso. Equity aggregata = media dei sotto-conti (somma
|
||||
normalizzata a base INIT). DD misurato sull'equity aggregata. Niente leva sovrapposta."""
|
||||
sleeves = [sleeve_equity(tr, n_bars, pos=pos, split=split) for tr in strat_trades.values()]
|
||||
agg = np.mean(sleeves, axis=0) # media -> base INIT, diversificazione reale
|
||||
# restringi alla finestra effettiva (da split in poi se OOS)
|
||||
lo = max(split, 0)
|
||||
agg = agg[lo:]
|
||||
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, sharpe=0.0)
|
||||
|
||||
|
||||
def main():
|
||||
# ---------- (1) sweep soglia trend ----------
|
||||
print("=" * 104)
|
||||
print(" (1) 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)
|
||||
thresholds = [None, 4.0, 3.0, 2.5, 2.0]
|
||||
for asset in ["BTC", "ETH"]:
|
||||
df = load_data(asset, "1h"); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
split = int(len(df) * (1 - OOS_FRAC))
|
||||
table = STRATS_ETH if asset == "ETH" else STRATS
|
||||
for nm, (fn, params) in table.items():
|
||||
ents = fn(df, **params)
|
||||
for thr in thresholds:
|
||||
tr = build_trades(ents, df, trend_max=thr)
|
||||
f = metrics_single(tr, ts); o = metrics_single(tr, ts, 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)
|
||||
|
||||
# ---------- (2) portafoglio combinato ----------
|
||||
print("\n" + "=" * 104)
|
||||
print(" (2) PORTAFOGLIO equipesato: capitale diviso in N sotto-conti indipendenti")
|
||||
print(" (pos 0.15 ciascuno, filtro trend 3.0 ATR). DD aggregato vs singola strategia.")
|
||||
print("=" * 104)
|
||||
print(f" {'Universo':<26s}{'Trd':>6s}{'Acc%':>7s}{'Ret%':>10s}{'DD%':>7s}{'':>7s}"
|
||||
f" | {'oAcc':>6s}{'oRet':>9s}{'oDD':>7s}{'':>7s}")
|
||||
print(" " + "-" * 100)
|
||||
all_trades = {}
|
||||
for asset in ["BTC", "ETH"]:
|
||||
df = load_data(asset, "1h"); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
split = int(len(df) * (1 - OOS_FRAC)); n = len(df)
|
||||
table = STRATS_ETH if asset == "ETH" else STRATS
|
||||
st = {f"{nm}_{asset}": build_trades(fn(df, **p), df, trend_max=3.0) for nm, (fn, p) in table.items()}
|
||||
all_trades.update(st)
|
||||
f = metrics_portfolio(st, n, ts); o = metrics_portfolio(st, n, ts, 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['sharpe']:>7.2f}"
|
||||
f" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}")
|
||||
# globale 8 sleeve
|
||||
df0 = load_data("BTC", "1h"); ts0 = pd.to_datetime(df0["timestamp"], unit="ms", utc=True)
|
||||
split0 = int(len(df0) * (1 - OOS_FRAC))
|
||||
f = metrics_portfolio(all_trades, len(df0), ts0); o = metrics_portfolio(all_trades, len(df0), ts0, 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['sharpe']:>7.2f}"
|
||||
f" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}")
|
||||
print("\n Nota: ogni sleeve gira su un sotto-conto indipendente (pos 0.15); l'equity di")
|
||||
print(" portafoglio e' la media dei sotto-conti. Curve poco correlate => DD aggregato")
|
||||
print(" molto piu' basso del DD del singolo sleeve (la vera leva anti-drawdown).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,152 @@
|
||||
"""DIP01 — Dip-Buy Z-Score Reversion (long-only).
|
||||
|
||||
Variante robusta e ONESTA della famiglia mean-reversion: compra SOLO i dip
|
||||
(close a z<=-z_in deviazioni sotto la media mobile) e prende profitto al rientro
|
||||
verso la media. Niente short: nel campione 2018-2026 shortare cripto perde OOS
|
||||
sistematicamente (vedi scripts/analysis/honest_final.py).
|
||||
|
||||
Logica:
|
||||
1. z-score = (close - SMA(n)) / STD(n)
|
||||
2. ENTRY long quando z attraversa al ribasso -z_in (capitolazione)
|
||||
3. EXIT: take-profit alla media mobile, stop-loss a sl_atr*ATR sotto l'entry,
|
||||
o time-limit max_bars
|
||||
4. ingresso a close[i] (eseguibile dal vivo, nessun look-ahead)
|
||||
|
||||
Validazione (netto, fee 0.10% RT Deribit, leva 3x, OOS = ultimo 30%):
|
||||
BTC 1h: FULL +298% / OOS +59% / DD 23% / 7-9 anni positivi
|
||||
ETH 1h: FULL +190% / OOS +224% / DD 54%
|
||||
SOL 1h: FULL +50% / OOS +13% / DD 25%
|
||||
Regge lo sweep fee fino a 0.20% RT (BTC OOS +45% anche a 0.20%).
|
||||
Robusto su BTC/ETH/SOL (asset major); sugli alt molto parabolici (DOGE/BNB)
|
||||
non ha edge -> usare solo su BTC/ETH/SOL.
|
||||
|
||||
Compatibile con StrategyWorker: ogni Signal porta tp/sl/max_bars in metadata.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal, 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
|
||||
|
||||
|
||||
class DipReversion(Strategy):
|
||||
name = "DIP01_dip_reversion"
|
||||
description = "Long-only dip-buy z-score reversion, TP alla media"
|
||||
default_assets = ["BTC", "ETH", "SOL"]
|
||||
default_timeframes = ["1h"]
|
||||
fee_rt = 0.001
|
||||
leverage = 3.0
|
||||
position_size = 0.15
|
||||
initial_capital = 1000.0
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
n = params.get("n", 50)
|
||||
z_in = params.get("z_in", 2.5)
|
||||
sl_atr = params.get("sl_atr", 2.5)
|
||||
max_bars = params.get("max_bars", 24)
|
||||
|
||||
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)
|
||||
|
||||
signals: list[Signal] = []
|
||||
for i in range(n + 14, len(c)):
|
||||
if np.isnan(z[i]) or np.isnan(a[i]):
|
||||
continue
|
||||
if z[i] <= -z_in and z[i - 1] > -z_in:
|
||||
signals.append(Signal(
|
||||
idx=i, direction=1, entry_price=c[i],
|
||||
metadata={"tp": float(ma[i]), "sl": float(c[i] - sl_atr * a[i]),
|
||||
"max_bars": max_bars},
|
||||
))
|
||||
return signals
|
||||
|
||||
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:
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strat = DipReversion()
|
||||
print(f"{'=' * 100}")
|
||||
print(f" DIP01 DIP-BUY REVERSION — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
|
||||
print(f"{'=' * 100}")
|
||||
for asset in ["BTC", "ETH", "SOL"]:
|
||||
r = strat.backtest(asset, "1h", n=50, z_in=2.5, sl_atr=2.5, max_bars=24)
|
||||
if r:
|
||||
r.strategy_name = f"DIP01 {asset} 1h"
|
||||
r.print_summary()
|
||||
@@ -0,0 +1,65 @@
|
||||
"""PORT01 — Portafoglio combinato delle 3 strategie oneste (equal-weight, daily rebal).
|
||||
|
||||
Sleeve (meccanismi anti-correlati):
|
||||
DIP01 dip-buy reversion su BTC (1h) regime: reversione
|
||||
TR01 EMA 20/100 trend su paniere (4h) regime: momentum singolo
|
||||
ROT02 dual-momentum rotation (1d) regime: forza relativa + risk-off
|
||||
|
||||
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; ROT02 ora top_k=3):
|
||||
DIP01_BTC +322% DD 15% CAGR 31%
|
||||
TR01_basket +591% DD 27% CAGR 43%
|
||||
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
|
||||
|
||||
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.honest_improve import _dd # noqa: E402
|
||||
from scripts.analysis.honest_improve2 import ( # noqa: E402
|
||||
dip_market_gated, _daily_equity, _norm, _tr_basket_daily, _rot_daily_equity,
|
||||
)
|
||||
|
||||
|
||||
def run():
|
||||
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)
|
||||
members = {
|
||||
"DIP01_BTC": _norm(_daily_equity(d["eq_ts"], d["eq_v"], idx)),
|
||||
"TR01_basket": _norm(_tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], idx)),
|
||||
"ROT02_dualmom": _norm(_rot_daily_equity(idx)),
|
||||
}
|
||||
drets = pd.DataFrame({k: v.pct_change().fillna(0) for k, v in members.items()})
|
||||
port_ret = drets.mean(axis=1)
|
||||
combo = (1 + port_ret).cumprod()
|
||||
yrs = (idx[-1] - idx[0]).days / 365.25
|
||||
|
||||
print("=" * 80)
|
||||
print(f" PORT01 — portafoglio equal-weight (daily rebal) | {idx[0].date()} -> {idx[-1].date()}")
|
||||
print("=" * 80)
|
||||
print(f" {'sleeve':<16s}{'ret%':>9s}{'DD%':>7s}{'CAGR%':>8s}")
|
||||
for name, s in members.items():
|
||||
r = (s.iloc[-1] / s.iloc[0] - 1) * 100
|
||||
cagr = ((s.iloc[-1] / s.iloc[0]) ** (1 / yrs) - 1) * 100
|
||||
print(f" {name:<16s}{r:>+9.0f}{_dd(s.values):>7.0f}{cagr:>8.0f}")
|
||||
r = (combo.iloc[-1] / combo.iloc[0] - 1) * 100
|
||||
cagr = ((combo.iloc[-1] / combo.iloc[0]) ** (1 / yrs) - 1) * 100
|
||||
print(f" {'PORTAFOGLIO':<16s}{r:>+9.0f}{_dd(combo.values):>7.0f}{cagr:>8.0f}")
|
||||
pa = port_ret.groupby(port_ret.index.year).apply(lambda x: ((1 + x).prod() - 1) * 100)
|
||||
print(" Per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in pa.items()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -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()
|
||||
@@ -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,78 @@
|
||||
"""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.
|
||||
|
||||
LIMITE OPERATIVO: e' una strategia a 2 gambe (long un perp + short l'altro), il worker
|
||||
attuale e' single-leg. Per tradarla serve: (a) eseguibilita' short del perp B su
|
||||
Deribit/Bybit, (b) gestione 2 ordini + fee doppie. Finche' il worker non supporta
|
||||
2 gambe, PR01 resta validata in backtest ma non wired nel paper trader.
|
||||
"""
|
||||
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()
|
||||
@@ -0,0 +1,46 @@
|
||||
"""ROT02 — Dual-Momentum Rotation (ROT01 + overlay di absolute momentum).
|
||||
|
||||
Evoluzione di ROT01: alla rotazione cross-sectional (forza relativa) aggiunge un
|
||||
overlay di ABSOLUTE momentum sul mercato: se BTC e' sotto la sua media a `regime_n`
|
||||
giorni (mercato risk-off), va completamente in CASH. Cosi' si evitano i bear di
|
||||
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 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
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.analysis.honest_improve import rot_improved # noqa: E402
|
||||
|
||||
LOOKBACK, TOP_K, REGIME_N = 60, 3, 100 # top_k=3 (era 2): dimezza il DD
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 90)
|
||||
print(f" ROT02 DUAL-MOMENTUM | 1d lb={LOOKBACK} top{TOP_K} + cash se BTC<SMA{REGIME_N} | netto fee 0.10% RT")
|
||||
print("=" * 90)
|
||||
full = rot_improved(lookback=LOOKBACK, top_k=TOP_K, regime_n=REGIME_N)
|
||||
oos = rot_improved(lookback=LOOKBACK, top_k=TOP_K, regime_n=REGIME_N, oos_frac=0.30)
|
||||
print(f" FULL: {full['ret']:+.0f}% DD {full['dd']:.0f}% ({full['pos_years']}/{full['n_years']} anni positivi)")
|
||||
print(f" OOS : {oos['ret']:+.0f}% DD {oos['dd']:.0f}%")
|
||||
print(" Per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(full["yearly"].items())))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,50 @@
|
||||
"""TR01 — EMA Trend Following (long-only), timeframe 4h.
|
||||
|
||||
Cavalca i trend rialzisti, si mette in cash nei downtrend. Niente short
|
||||
(shortare cripto perde OOS nel campione 2018-2026). Complementare a DIP01:
|
||||
DIP01 guadagna nei regimi di reversione, TR01 nei regimi di trend.
|
||||
|
||||
Logica:
|
||||
1. EMA fast (20) e EMA slow (100) sul close
|
||||
2. LONG quando EMA_fast > EMA_slow (uptrend), altrimenti CASH
|
||||
3. posizione continua, decisione a close[i] (no look-ahead);
|
||||
fee solo sui cambi di stato (poche operazioni = fee non letali)
|
||||
|
||||
Validazione (netto, fee 0.10% RT, leva 3x, pos 15%, OOS = ultimo 30%):
|
||||
robusto FULL+OOS su 5/8 asset: BNB(+14), BTC(+27), DOGE(+53), SOL(+7), XRP(+29) OOS.
|
||||
ETH ~flat, ADA/LTC negativi OOS -> preferire BNB/BTC/DOGE/SOL/XRP.
|
||||
Dettagli in scripts/analysis/honest_final.py / honest_trend.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.honest_trend import ( # noqa: E402
|
||||
simulate_position, ema_dual_signal, oos as trend_oos,
|
||||
)
|
||||
from scripts.analysis.honest_lab import get_df
|
||||
|
||||
ASSETS = ["BNB", "BTC", "DOGE", "SOL", "XRP"]
|
||||
FAST, SLOW, TF = 20, 100, "4h"
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 90)
|
||||
print(f" TR01 EMA TREND {FAST}/{SLOW} long-only | {TF} | netto fee 0.10% RT leva 3x pos 15%")
|
||||
print("=" * 90)
|
||||
print(f" {'Asset':<6s}{'Flip':>6s}{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'Exp%':>6s}{'AnniPos':>9s}")
|
||||
for a in ASSETS:
|
||||
df = get_df(a, TF)
|
||||
sig = ema_dual_signal(df, FAST, SLOW, long_only=True)
|
||||
f = simulate_position(sig, df)
|
||||
o = trend_oos(sig, df)
|
||||
print(f" {a:<6s}{f['flips']:>6d}{f['ret']:>+9.0f}{o['ret']:>+9.0f}"
|
||||
f"{f['dd']:>6.0f}{f['exposure']:>6.0f}{str(f['pos_years'])+'/'+str(f['n_years']):>9s}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,48 @@
|
||||
"""ROT01 — Cross-Sectional Momentum Rotation (multi-crypto, long-only), 1d.
|
||||
|
||||
UNA strategia che usa l'INTERO paniere di crypto in un solo book: ogni giorno
|
||||
ordina gli asset per momentum (rendimento sugli ultimi `lookback` giorni) e alloca
|
||||
il capitale in parti uguali ai `top_k` con momentum positivo; il resto in cash.
|
||||
Cattura la dispersione tra crypto (gli alt forti corrono molto piu' di BTC nei bull)
|
||||
senza shortare nulla. Meccanismo distinto da DIP01/TR01 -> vera diversificazione.
|
||||
|
||||
Onesto: i pesi a close[i] usano solo rendimenti passati; il rendimento del giorno
|
||||
i->i+1 e' realizzato con quei pesi. Fee sul turnover. Allineamento per timestamp.
|
||||
|
||||
Validazione (netto, fee 0.10% RT, gross 0.45, OOS = ultimo 30%):
|
||||
lb=60 top2 -> FULL +679% / OOS +44% / DD 53% / 5-7 anni positivi.
|
||||
Param-insensitive (tutte le lb/k positive) e regge fee fino 0.20% RT (OOS +41%).
|
||||
Per-anno: 2020+33 2021+181 2022-29 2023+43 2024+59 2025+6 2026-10 (i negativi = bear).
|
||||
Dettagli in scripts/analysis/honest_rotation.py / honest_final.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.honest_rotation import build_panel, simulate_rotation # noqa: E402
|
||||
from scripts.analysis.honest_lab import available_assets
|
||||
|
||||
LOOKBACK, TOP_K, TF = 60, 2, "1d"
|
||||
|
||||
|
||||
def run():
|
||||
assets = available_assets()
|
||||
panel = build_panel(assets, TF)
|
||||
print("=" * 90)
|
||||
print(f" ROT01 ROTAZIONE cross-sectional momentum | {TF} lb={LOOKBACK} top{TOP_K} | netto fee 0.10% RT")
|
||||
print("=" * 90)
|
||||
print(f" Paniere: {list(panel.columns)}")
|
||||
print(f" Periodo: {panel.index[0].date()} -> {panel.index[-1].date()} ({panel.shape[0]} barre)")
|
||||
full = simulate_rotation(panel, lookback=LOOKBACK, top_k=TOP_K, fee_rt=0.001)
|
||||
oos = simulate_rotation(panel, lookback=LOOKBACK, top_k=TOP_K, fee_rt=0.001, oos_frac=0.30)
|
||||
print(f"\n FULL: {full['ret']:+.0f}% DD {full['dd']:.0f}% turnover {full['turnover']:.0f}")
|
||||
print(f" OOS : {oos['ret']:+.0f}% DD {oos['dd']:.0f}% ({full['pos_years']}/{full['n_years']} anni positivi)")
|
||||
print(" Per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(full["yearly"].items())))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -19,7 +19,6 @@ _REGISTRY: dict[str, type[Strategy]] = {}
|
||||
MODULE_MAP = {
|
||||
"MR01_bollinger_fade": ("MR01_bollinger_fade", "BollingerFade"),
|
||||
"MR02_donchian_fade": ("MR02_donchian_fade", "DonchianFade"),
|
||||
"MR03_keltner_fade": ("MR03_keltner_fade", "KeltnerFade"),
|
||||
"MR07_return_reversal": ("MR07_return_reversal", "ReturnReversal"),
|
||||
}
|
||||
|
||||
|
||||
+2
-24
@@ -60,30 +60,8 @@ strategies:
|
||||
trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD-
|
||||
ema_long: 200
|
||||
|
||||
# MR03 Keltner fade: fade canale ATR su EMA (banda indipendente da Bollinger).
|
||||
# Robusto su tutta la griglia n x k. BTC n30 k2.0 +112% OOS DD20%.
|
||||
# ETH: edge ampio ma DD pieno ~65% (tratto dell'asset, come MR01) -> leva bassa.
|
||||
- name: MR03_keltner_fade
|
||||
asset: BTC
|
||||
tf: 1h
|
||||
enabled: true
|
||||
params:
|
||||
n: 30
|
||||
k: 2.0
|
||||
sl_atr: 2.0
|
||||
max_bars: 24
|
||||
# NB: su MR03 BTC il filtro trend PEGGIORA Acc e DD (unico sleeve) -> disattivo.
|
||||
- name: MR03_keltner_fade
|
||||
asset: ETH
|
||||
tf: 1h
|
||||
enabled: true
|
||||
params:
|
||||
n: 50
|
||||
k: 2.0
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user