5 Commits

Author SHA1 Message Date
Adriano a51129acf6 feat(analysis): matrice PnL/anno consolidata (confronto strategie + portafoglio)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:53:37 +02:00
Adriano 1b099bb47b feat(analysis): tabella per-anno (PnL/DD) versioni migliorate + portafoglio
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:52:25 +02:00
Adriano 783fa5546f feat(analysis): miglioramenti - ROT02 dual-momentum + portafoglio (DD 12%)
Obiettivo: alzare Acc, ridurre DD, migliorare PnL. Leve oneste, no tuning per-anno.

- ROT02: overlay absolute-momentum (cash se BTC<SMA100) su ROT01. Domina su tutte
  le metriche: FULL +679->+1095%, OOS +44->+98%, DD 53->40%.
- DIP01 market-gate (variante low-DD): alza Acc (ETH 52->57, SOL 49->52) e dimezza
  il DD (ETH 53->23), al costo di PnL. De-risking opzionale; su BTC il gate va evitato.
- PORT01: portafoglio equal-weight giornaliero delle 3 sleeve anti-correlate
  (DIP01+TR01+ROT02). DD 12% (sotto ogni sleeve), CAGR 45%, 2022 bear -1% (era -30%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:49:14 +02:00
Adriano ad141f080c feat(analysis): report per-anno (Trade/Acc/DD/PnL) delle 3 strategie
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:42:04 +02:00
Adriano 212427ffa1 feat(analysis): 3 strategie oneste validate OOS multi-crypto (DIP/TR/ROT)
Ricerca onesta post-squeeze su 8 crypto (2018-2026), engine fee-aware con
ingresso eseguibile a close[i], uscita TP/SL intrabar, OOS held-out, sweep fee.

Lezione madre: shortare cripto perde OOS sistematicamente (campione net-bull)
-> tutte le strategie robuste sono long-biased.

Tre meccanismi distinti e complementari:
- DIP01  dip-buy z-score reversion (long-only, 1h)  robusto BTC/ETH/SOL
- TR01   EMA 20/100 trend-following (long-only, 4h) robusto su 5/8 asset
- ROT01  rotazione cross-sectional momentum sul paniere (1d) OOS +44%, param-insensitive

Engine e validazione: scripts/analysis/honest_lab.py + honest_final.py
(+ honest_candidates/diag/diag2/trend/rotation). Diario in docs/diary/.

Onesto sull'obiettivo: €50/giorno su €1000 in pochi mesi non e' raggiungibile a
rischio sano (~1825%/anno); edge reali 30-60% OOS pluriennale. Via realistica:
portafoglio delle 3, leva moderata, crescita composta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:28:00 +02:00
18 changed files with 2004 additions and 0 deletions
+136
View File
@@ -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à.
+175
View File
@@ -0,0 +1,175 @@
"""Strategie candidate ONESTE + sweep multi-asset/tf con verdetto.
Ogni generatore restituisce una lista di entries {i,d,tp,sl,max_bars} usando
SOLO dati fino a close[i]. L'engine (honest_lab.simulate) entra a close[i].
Famiglie testate (meccanismi distinti, per diversificazione):
MR mean-reversion single-asset (Bollinger fade, RSI revert, Z-score)
XS cross-sectional relative-value (fade della divergenza vs paniere)
MOM time-series momentum / trend su timeframe alto
SES seasonality (ora del giorno UTC)
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.honest_lab import ( # noqa: E402
atr, rsi, ema, get_df, simulate, oos_split, verdict,
available_assets, FEE_RT,
)
# ============================================================================
# MR — mean reversion single-asset
# ============================================================================
def bollinger_fade(df, n=50, k=2.5, sl_atr=2.0, max_bars=24):
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
up, lo = ma + k * sd, ma - k * sd
ents = []
for i in range(n + 14, len(c)):
if np.isnan(up[i]) or np.isnan(a[i]):
continue
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def rsi_revert(df, n=14, lo=25, hi=75, sl_atr=2.5, max_bars=24, ma_n=20):
c = df["close"].values
r = rsi(c, n)
ma = pd.Series(c).rolling(ma_n).mean().values
a = atr(df, 14)
ents = []
for i in range(max(n, ma_n) + 1, len(c)):
if np.isnan(r[i]) or np.isnan(ma[i]) or np.isnan(a[i]):
continue
if r[i - 1] < lo <= r[i]:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif r[i - 1] > hi >= r[i]:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def zscore_revert(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24):
"""Entra quando close e' a |z|>z_in std dalla media; TP alla media."""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
z = (c - ma) / sd
ents = []
for i in range(n + 14, len(c)):
if np.isnan(z[i]) or np.isnan(a[i]) or sd[i] == 0:
continue
if z[i] <= -z_in and z[i - 1] > -z_in:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif z[i] >= z_in and z[i - 1] < z_in:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
# ============================================================================
# MOM — time-series momentum / trend (timeframe alto, niente breakout intrabar)
# ============================================================================
def ema_trend(df, fast=20, slow=50, sl_atr=3.0, tp_atr=10.0, max_bars=240):
"""Trend following: cross EMA fast/slow deciso a close[i], TP/SL ad ATR."""
c = df["close"].values
ef, es = ema(c, fast), ema(c, slow)
a = atr(df, 14)
ents = []
for i in range(slow + 14, len(c)):
if np.isnan(a[i]):
continue
cross_up = ef[i] > es[i] and ef[i - 1] <= es[i - 1]
cross_dn = ef[i] < es[i] and ef[i - 1] >= es[i - 1]
if cross_up:
ents.append({"i": i, "d": 1, "tp": c[i] + tp_atr * a[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif cross_dn:
ents.append({"i": i, "d": -1, "tp": c[i] - tp_atr * a[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
# ============================================================================
# SES — seasonality (ora del giorno UTC). Direzione fissa decisa solo dall'ora.
# ============================================================================
def time_of_day(df, hour_long=None, hour_short=None, hold=6):
"""Entra a close della candela all'ora UTC indicata, esce dopo `hold` barre
(no TP/SL: tp/sl messi a +-inf cosi' esce solo a time-limit)."""
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
c = df["close"].values
hours = ts.dt.hour.values
hour_long = set(hour_long or [])
hour_short = set(hour_short or [])
ents = []
for i in range(1, len(c)):
if hours[i] in hour_long:
ents.append({"i": i, "d": 1, "tp": np.inf, "sl": -np.inf, "max_bars": hold})
elif hours[i] in hour_short:
ents.append({"i": i, "d": -1, "tp": -np.inf, "sl": np.inf, "max_bars": hold})
return ents
# ============================================================================
# sweep
# ============================================================================
def run_sweep(generators: dict, assets: list[str], tfs: list[str]):
print("=" * 130)
print(f" HONEST LAB — NETTO fee {FEE_RT*100:.2f}% RT | leva 3x | pos 15% | OOS ultimo 30%")
print("=" * 130)
print(f" {'Strategia':<26s}{'Asset':>5s}{'TF':>5s}{'Trd':>6s}{'Win%':>7s}"
f"{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'Exp%':>6s}{'AnniPos':>9s}{'OK':>4s}")
print(" " + "-" * 126)
survivors = []
for label, (fn, params) in generators.items():
for asset in assets:
for tf in tfs:
try:
df = get_df(asset, tf)
except Exception:
continue
ents = fn(df, **params)
if len(ents) < 30:
continue
full = simulate(ents, df)
_, oos_e = oos_split(ents, df)
oos = simulate(oos_e, df)
ok = verdict(full, oos)
flag = " OK" if ok else ""
print(f" {label:<26s}{asset:>5s}{tf:>5s}{full.trades:>6d}{full.win:>7.1f}"
f"{full.ret:>+9.0f}{oos.ret:>+9.0f}{full.dd:>6.0f}{full.exposure:>6.0f}"
f"{f'{full.pos_years}/{full.n_years}':>9s}{flag:>4s}")
if ok:
survivors.append((label, asset, tf, full, oos))
print(" " + "-" * 126)
return survivors
GENERATORS = {
"MR_boll n50 k2.5": (bollinger_fade, dict(n=50, k=2.5, sl_atr=2.0, max_bars=24)),
"MR_boll n20 k2.5": (bollinger_fade, dict(n=20, k=2.5, sl_atr=2.0, max_bars=24)),
"MR_rsi 25/75": (rsi_revert, dict(n=14, lo=25, hi=75, sl_atr=2.5, max_bars=24)),
"MR_zscore z2.5": (zscore_revert, dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24)),
"MR_zscore z3": (zscore_revert, dict(n=50, z_in=3.0, sl_atr=2.5, max_bars=24)),
"MOM_ema 20/50": (ema_trend, dict(fast=20, slow=50, sl_atr=3.0, tp_atr=10.0, max_bars=240)),
}
if __name__ == "__main__":
assets = available_assets()
print("Asset disponibili:", assets)
survivors = run_sweep(GENERATORS, assets, ["1h", "4h"])
print(f"\n SOPRAVVISSUTI (FULL+OOS+anni+DD): {len(survivors)}")
for label, a, tf, full, oos in survivors:
print(f" {label:<26s} {a} {tf} FULL {full.ret:+.0f}% OOS {oos.ret:+.0f}% DD {full.dd:.0f}%")
+73
View File
@@ -0,0 +1,73 @@
"""Diagnostica: perche' la mean-reversion simmetrica perde su asset trending?
Test: long-only vs short-only, e MR FILTRATA DAL TREND (buy-dip in uptrend,
sell-rip in downtrend) per evitare di fadeare i trend forti.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.honest_lab import ( # noqa: E402
atr, ema, get_df, simulate, oos_split, available_assets, FEE_RT,
)
def zscore_entries(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24,
trend_n=0, side="both"):
"""Z-score revert con filtro trend opzionale.
trend_n>0: EMA di lungo periodo. Long solo se close>EMA (uptrend),
short solo se close<EMA (downtrend).
side: 'both' | 'long' | 'short'
"""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
z = (c - ma) / np.where(sd == 0, np.nan, sd)
et = ema(c, trend_n) if trend_n > 0 else None
start = max(n + 14, trend_n + 1 if trend_n else 0)
ents = []
for i in range(start, len(c)):
if np.isnan(z[i]) or np.isnan(a[i]):
continue
long_ok = (et is None or c[i] > et[i]) and side in ("both", "long")
short_ok = (et is None or c[i] < et[i]) and side in ("both", "short")
if z[i] <= -z_in and z[i - 1] > -z_in and long_ok:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif z[i] >= z_in and z[i - 1] < z_in and short_ok:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def row(label, df, ents):
if len(ents) < 20:
print(f" {label:<34s} {'<20 trd':>50s}")
return None
full = simulate(ents, df)
_, oe = oos_split(ents, df)
oos = simulate(oe, df)
print(f" {label:<34s}{full.trades:>6d}{full.win:>7.1f}{full.ret:>+9.0f}"
f"{oos.ret:>+9.0f}{full.dd:>6.0f}{f'{full.pos_years}/{full.n_years}':>8s}")
return full, oos
if __name__ == "__main__":
assets = available_assets()
print(f"HONEST DIAG — z-score revert, fee {FEE_RT*100:.2f}% RT, leva 3x | OOS 30%")
for tf in ["1h"]:
for a in assets:
df = get_df(a, tf)
print(f"\n === {a} {tf} === {'Trd':>5s}{'Win%':>7s}{'FULL%':>8s}{'OOS%':>8s}{'DD%':>6s}{'AnniP':>8s}")
base = dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24)
row("both, no filter", df, zscore_entries(df, **base, side="both"))
row("long-only, no filter", df, zscore_entries(df, **base, side="long"))
row("short-only, no filter", df, zscore_entries(df, **base, side="short"))
row("both + trend200 filter", df, zscore_entries(df, **base, trend_n=200, side="both"))
row("both + trend500 filter", df, zscore_entries(df, **base, trend_n=500, side="both"))
row("long + trend200 filter", df, zscore_entries(df, **base, trend_n=200, side="long"))
+64
View File
@@ -0,0 +1,64 @@
"""Diag2: long-MR sempre + short-MR SOLO in downtrend confermato (close<EMA_t).
Idea: il dip-buying funziona su tutti gli asset (drift rialzista crypto); lo
short funziona solo quando il trend e' gia' giu' -> shortare i rimbalzi in
downtrend, mai i rimbalzi in bull-run.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.honest_lab import ( # noqa: E402
atr, ema, get_df, simulate, oos_split, available_assets, FEE_RT,
)
def regime_mr(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24, trend_n=200,
allow_short=True):
"""Long su z<=-z_in SEMPRE. Short su z>=+z_in solo se close<EMA(trend_n)."""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
z = (c - ma) / np.where(sd == 0, np.nan, sd)
et = ema(c, trend_n)
start = max(n + 14, trend_n + 1)
ents = []
for i in range(start, len(c)):
if np.isnan(z[i]) or np.isnan(a[i]):
continue
if z[i] <= -z_in and z[i - 1] > -z_in:
ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars})
elif allow_short and z[i] >= z_in and z[i - 1] < z_in and c[i] < et[i]:
ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars})
return ents
def show(label, df, ents):
if len(ents) < 20:
print(f" {label:<30s} <20 trd"); return None
full = simulate(ents, df); _, oe = oos_split(ents, df); oos = simulate(oe, df)
print(f" {label:<30s}{full.trades:>6d}{full.win:>7.1f}{full.ret:>+9.0f}"
f"{oos.ret:>+9.0f}{full.dd:>6.0f}{f'{full.pos_years}/{full.n_years}':>8s}")
return full, oos
if __name__ == "__main__":
assets = available_assets()
print(f"DIAG2 — regime MR (long sempre + short in downtrend) fee {FEE_RT*100:.2f}% leva3x OOS30%")
surv = 0
for a in assets:
df = get_df(a, "1h")
print(f"\n === {a} 1h === {'Trd':>5s}{'Win%':>7s}{'FULL%':>8s}{'OOS%':>8s}{'DD%':>6s}{'AnniP':>8s}")
show("long-only", df, regime_mr(df, allow_short=False))
r = show("long + short@downtrend200", df, regime_mr(df, trend_n=200))
show("long + short@downtrend500", df, regime_mr(df, trend_n=500))
if r and r[0].ret > 0 and r[1].ret > 0:
surv += 1
print(f"\n Asset con regime200 positivo FULL+OOS: {surv}/{len(assets)}")
+103
View File
@@ -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)
+175
View File
@@ -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}")
+184
View File
@@ -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][:2] if risk_on else []
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()
+192
View File
@@ -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
+80
View File
@@ -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))
+96
View File
@@ -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}")
+109
View File
@@ -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))
+188
View File
@@ -0,0 +1,188 @@
"""Report PER ANNO (Trade, Acc%, DD%, PnL%) delle 3 strategie oneste.
Acc: DIP01/TR01 = win-rate dei trade chiusi (episodi); ROT01 = % giorni positivi.
DD : drawdown massimo dell'equity DENTRO l'anno solare.
PnL: variazione % dell'equity nell'anno (composta).
Tutto NETTO (fee 0.10% RT, leva 3x, pos 15%). Replica gli engine di honest_*.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.honest_lab import atr, ema, get_df, available_assets, FEE_RT
from scripts.analysis.honest_final import dip_entries
from scripts.analysis.honest_rotation import build_panel
LEV, POS = 3.0, 0.15
def _yearly_dd(years: np.ndarray, equity: np.ndarray) -> dict[int, float]:
"""DD massimo intra-anno da una serie di equity etichettata per anno."""
out: dict[int, float] = {}
for y in np.unique(years):
eq = equity[years == y]
peak = eq[0]; dd = 0.0
for v in eq:
peak = max(peak, v)
dd = max(dd, (peak - v) / peak if peak > 0 else 0.0)
out[int(y)] = dd * 100
return out
def _print(title, header, rows):
print("\n" + "=" * 78)
print(f" {title}")
print("=" * 78)
print(" " + header)
print(" " + "-" * 74)
for r in rows:
print(" " + r)
# --------------------------- DIP01 (trade-based) ---------------------------
def dip_yearly(asset, tf="1h"):
df = get_df(asset, tf)
ents = dip_entries(df)
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
fee = FEE_RT * LEV
cap = 1000.0
last_exit = -1
eq_y, eq_v = [], []
yt: dict[int, list] = {} # year -> [trades, wins, pnl_start_cap, pnl_end_cap]
for e in ents:
i, d = e["i"], e["d"]
if i <= last_exit or i + 1 >= n:
continue
entry = c[i]; tp, sl, mb = e["tp"], e["sl"], e["max_bars"]
exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
j = n - 1; exit_p = c[j]; break
if (d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl):
exit_p = sl; break
if (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp):
exit_p = tp; break
if k == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * LEV - fee
cap = max(cap + cap * POS * ret, 10.0)
last_exit = j
y = ts.iloc[i].year
rec = yt.setdefault(y, [0, 0, None, None])
rec[0] += 1; rec[1] += ret > 0
eq_y.append(y); eq_v.append(cap)
dd = _yearly_dd(np.array(eq_y), np.array(eq_v))
# PnL% anno: da equity prima/dopo
rows = []
prev = 1000.0
yrs = sorted(yt)
cum = {}
cprev = 1000.0
# ricostruisci equity di fine anno
end_cap = {}
for y, v in zip(eq_y, eq_v):
end_cap[y] = v
for y in yrs:
t, w = yt[y][0], yt[y][1]
ec = end_cap[y]
pnl = (ec / cprev - 1) * 100
cprev = ec
rows.append(f"{y:>6d}{t:>8d}{(w/t*100 if t else 0):>8.1f}{dd.get(y,0):>8.1f}{pnl:>+10.1f}")
return rows
# --------------------------- TR01 (position episodes) ---------------------------
def tr_yearly(asset, tf="4h", fast=20, slow=100):
df = get_df(asset, tf)
c = df["close"].values; n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
ef, es = ema(c, fast), ema(c, slow)
sig = np.where(ef > es, 1.0, 0.0); sig[:slow] = 0.0
cap = 1000.0; cur = 0.0
fee = FEE_RT / 2 * LEV
ep_start_cap = None; ep_year = None
yt: dict[int, list] = {}
eq_y, eq_v = [], []
for i in range(n - 1):
s = sig[i]
if s != cur:
cap -= cap * POS * fee * abs(s - cur)
if s == 1.0: # apertura long
ep_start_cap = cap; ep_year = ts.iloc[i].year
elif cur == 1.0 and ep_start_cap is not None: # chiusura long
rec = yt.setdefault(ep_year, [0, 0])
rec[0] += 1; rec[1] += cap > ep_start_cap
ep_start_cap = None
cur = s
pr = (c[i + 1] - c[i]) / c[i]
cap = max(cap * (1 + POS * LEV * pr * cur), 10.0)
eq_y.append(ts.iloc[i].year); eq_v.append(cap)
if cur == 1.0 and ep_start_cap is not None:
rec = yt.setdefault(ep_year, [0, 0]); rec[0] += 1; rec[1] += cap > ep_start_cap
dd = _yearly_dd(np.array(eq_y), np.array(eq_v))
end_cap = {}
for y, v in zip(eq_y, eq_v):
end_cap[y] = v
rows = []; cprev = 1000.0
for y in sorted(end_cap):
t, w = yt.get(y, [0, 0])
pnl = (end_cap[y] / cprev - 1) * 100; cprev = end_cap[y]
rows.append(f"{y:>6d}{t:>8d}{(w/t*100 if t else 0):>8.1f}{dd.get(y,0):>8.1f}{pnl:>+10.1f}")
return rows
# --------------------------- ROT01 (daily portfolio) ---------------------------
def rot_yearly(lookback=60, top_k=2, gross=0.45):
panel = build_panel(available_assets(), "1d")
P = panel.values; T, N = P.shape
rets = np.zeros_like(P); rets[1:] = P[1:] / P[:-1] - 1
years = panel.index.year.values
cap = 1000.0; w = np.zeros(N)
yt: dict[int, list] = {} # year -> [rebal, pos_days, days]
eq_y, eq_v = [], []
for i in range(lookback + 1, T - 1):
mom = P[i] / P[i - lookback] - 1
order = np.argsort(mom)[::-1]
chosen = [j for j in order if mom[j] > 0][:top_k]
new_w = np.zeros(N)
for j in chosen:
new_w[j] = gross / len(chosen)
turnover = np.abs(new_w - w).sum()
if turnover > 1e-9:
cap -= cap * turnover * (FEE_RT / 2)
w = new_w
pr = float(np.dot(w, rets[i + 1]))
cap = max(cap * (1 + pr), 10.0)
y = int(years[i])
rec = yt.setdefault(y, [0, 0, 0])
rec[0] += turnover > 1e-9; rec[1] += pr > 0; rec[2] += 1
eq_y.append(y); eq_v.append(cap)
dd = _yearly_dd(np.array(eq_y), np.array(eq_v))
end_cap = {}
for y, v in zip(eq_y, eq_v):
end_cap[y] = v
rows = []; cprev = 1000.0
for y in sorted(end_cap):
reb, pos, days = yt[y]
pnl = (end_cap[y] / cprev - 1) * 100; cprev = end_cap[y]
rows.append(f"{y:>6d}{reb:>8d}{(pos/days*100 if days else 0):>8.1f}{dd.get(y,0):>8.1f}{pnl:>+10.1f}")
return rows
if __name__ == "__main__":
H = f"{'Anno':>6s}{'Trade':>8s}{'Acc%':>8s}{'DD%':>8s}{'PnL%':>10s}"
for a in ["BTC", "ETH", "SOL"]:
_print(f"DIP01 — {a} 1h (Acc = win-rate trade)", H, dip_yearly(a))
for a in ["BNB", "BTC", "DOGE", "SOL", "XRP"]:
_print(f"TR01 — {a} 4h (Trade = episodi long, Acc = win-rate episodi)", H, tr_yearly(a))
_print("ROT01 — paniere 8 crypto 1d (Trade = ribilanciamenti, Acc = % giorni positivi)",
H, rot_yearly())
+74
View File
@@ -0,0 +1,74 @@
"""Tabella per-anno (PnL% e DD% intra-anno) delle versioni MIGLIORATE:
ROT02 (dual-momentum), le 3 sleeve e il PORTAFOGLIO combinato.
Tutto NETTO. Riusa gli engine di honest_improve / honest_improve2.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.honest_improve2 import ( # noqa: E402
dip_market_gated, _daily_equity, _norm, _tr_basket_daily, _rot_daily_equity,
)
def _year_dd(eq: pd.Series) -> dict[int, float]:
out = {}
for y, g in eq.groupby(eq.index.year):
peak = g.iloc[0]; dd = 0.0
for v in g:
peak = max(peak, v); dd = max(dd, (peak - v) / peak if peak > 0 else 0.0)
out[int(y)] = dd * 100
return out
def _year_pnl(eq: pd.Series) -> dict[int, float]:
out = {}
for y, g in eq.groupby(eq.index.year):
out[int(y)] = (g.iloc[-1] / g.iloc[0] - 1) * 100
return out
def table(name, eq):
eq = _norm(eq)
dd = _year_dd(eq); pnl = _year_pnl(eq)
print(f"\n {name}")
print(f" {'Anno':>6s}{'PnL%':>9s}{'DD%':>7s}")
print(" " + "-" * 22)
for y in sorted(pnl):
print(f" {y:>6d}{pnl[y]:>+9.0f}{dd[y]:>7.0f}")
tot = (eq.iloc[-1] / eq.iloc[0] - 1) * 100
print(f" {'TOT':>6s}{tot:>+9.0f}{_year_dd(eq) and max(_year_dd(eq).values()):>7.0f}(max anno)")
if __name__ == "__main__":
print("=" * 60)
print(" RISULTATI PER ANNO — versioni migliorate (NETTO)")
print("=" * 60)
# ROT02 dal 2020 (dati paniere)
idx_rot = pd.date_range("2020-09-01", "2026-05-26", freq="1D", tz="UTC")
eq_rot = _rot_daily_equity(idx_rot)
table("ROT02 — dual-momentum rotation (1d)", eq_rot)
# sleeve + portafoglio dal 2021
idx = pd.date_range("2021-01-01", "2026-05-26", freq="1D", tz="UTC")
d = dip_market_gated("BTC", market_n=0, return_equity=True)
eq_dip = _norm(_daily_equity(d["eq_ts"], d["eq_v"], idx))
eq_tr = _norm(_tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], idx))
eq_r2 = _norm(_rot_daily_equity(idx))
table("Sleeve DIP01 — BTC (1h)", eq_dip)
table("Sleeve TR01 — basket (4h)", eq_tr)
table("Sleeve ROT02 (1d)", eq_r2)
drets = pd.DataFrame({"DIP": eq_dip.pct_change().fillna(0),
"TR": eq_tr.pct_change().fillna(0),
"ROT": eq_r2.pct_change().fillna(0)})
combo = (1 + drets.mean(axis=1)).cumprod()
table("PORTAFOGLIO equal-weight (daily rebal)", combo)
+152
View File
@@ -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()
+65
View File
@@ -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):
DIP01_BTC +322% DD 15% CAGR 31%
TR01_basket +591% DD 27% CAGR 43%
ROT02_dualmom +771% DD 40% CAGR 49%
PORTAFOGLIO +642% DD 12% CAGR 45% <-- DD piu' basso di ogni sleeve
Per-anno: 2021 +203 · 2022 -1 · 2023 +47 · 2024 +50 · 2025 +14 · 2026 -2
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,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()
+40
View File
@@ -0,0 +1,40 @@
"""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 SMA100 : FULL +1095% / OOS +98% / DD 40% <-- PnL su, DD giu'
Param-insensitive sulla finestra di regime (SMA100-150). Dettagli in
scripts/analysis/honest_improve.py (rot_improved).
"""
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, 2, 100
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()
+50
View File
@@ -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()