chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""DIP01 — Dip-buy mean-reversion single-asset (z-score sotto-banda). Honest family.
|
||||
|
||||
Replica live della logica validata in scripts/analysis/honest_improve2.dip_market_gated
|
||||
(con market_n=0, come lo sleeve DIP01_BTC del portafoglio): compra quando lo z-score del
|
||||
prezzo rispetto a SMA(n) incrocia sotto -z_in; esce a TP=SMA, SL=close-sl_atr*ATR, o max_bars.
|
||||
"""
|
||||
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.strategies.base import Strategy, Signal # noqa: E402
|
||||
|
||||
|
||||
def _atr(df, n=14):
|
||||
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 Dip01DipBuy(Strategy):
|
||||
name = "DIP01_dip_buy"
|
||||
description = "Dip-buy mean-reversion single-asset (z-score), exit TP=SMA/SL=ATR/max_bars"
|
||||
default_assets = ["BTC"]
|
||||
default_timeframes = ["1h"]
|
||||
fee_rt = 0.001
|
||||
leverage = 3.0
|
||||
position_size = 0.15
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
n: int = 50, z_in: float = 2.5, sl_atr: float = 2.5,
|
||||
max_bars: int = 24, **params) -> list[Signal]:
|
||||
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)
|
||||
# Edge minimo: salta i dip il cui TP (la media) è entro il costo round-trip. 0 = off.
|
||||
min_tp_frac = params.get("min_tp_frac", 0.0)
|
||||
out: list[Signal] = []
|
||||
for i in range(n + 14, len(c)):
|
||||
if np.isnan(z[i]) or np.isnan(a[i]) or np.isnan(ma[i]):
|
||||
continue
|
||||
if z[i] <= -z_in and z[i - 1] > -z_in:
|
||||
if min_tp_frac > 0 and abs(ma[i] - c[i]) / c[i] <= min_tp_frac:
|
||||
continue # TP entro le fee -> non eseguibile in utile
|
||||
out.append(Signal(idx=i, direction=1, entry_price=float(c[i]),
|
||||
metadata={"tp": float(ma[i]),
|
||||
"sl": float(c[i] - sl_atr * a[i]),
|
||||
"max_bars": int(max_bars)}))
|
||||
return out
|
||||
@@ -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,123 @@
|
||||
"""FR01 — Hurst-Calm Fade (FRATTALE x REGIME). Esito della ricerca a 100 agenti (2026-06-02).
|
||||
|
||||
Fade della banda di Bollinger (k=2.5 su SMA50, TP=media, SL=2*ATR, max_bars=24) ATTIVATO
|
||||
SOLO quando coincidono due condizioni di regime:
|
||||
- FRATTALE: rolling Hurst < 0.55 (regime anti-persistente -> la mean-reversion ha senso
|
||||
fratalmente; con Hurst>0.55 il fade peggiora, il momentum perde comunque).
|
||||
- VOLATILITA: dvol_pct < 0.40 (DVOL nel terzile basso del suo storico -> regime calmo/range).
|
||||
|
||||
Doppio gate frattale x regime: l'INTERAZIONE e' l'ingrediente attivo, non il fade di per se'
|
||||
(ablation: senza gate Sharpe ~0.8 e muore a fee 0.2% RT; col doppio gate OOS Sharpe ~3.7).
|
||||
|
||||
VALIDAZIONE (netto fee 0.10% RT, leva 3x, OOS ultimo 30%, ricerca fractal_argo_workflow):
|
||||
BTC 1h: 198 trade, FULL +100% / OOS +54% / Sharpe OOS 3.73 / DD OOS 5.1% / 6/6 anni positivi,
|
||||
regge fee 0.2% RT. Confermato avversarialmente (no look-ahead, split alternativo).
|
||||
Generalizza a ETH 1h (Sharpe ~2.6, secondario). 4h/1d = rumore (pochi trade).
|
||||
Correlazione coi fade esistenti BASSA: MR01 +0.17, MR02 +0.08, MR07 -0.03 -> DIVERSIFICATORE
|
||||
quasi-ortogonale (profilo SH01/pairs), NON ridondante. Esposizione ~1-9% -> low-frequency.
|
||||
|
||||
RUOLO: diversificatore a basso DD per il MASTER/PORT06, NON motore standalone (non batte il
|
||||
portafoglio da solo). Coerente coi priori: i frattali da soli sono rumore; il valore e' nel
|
||||
gating del regime. NB il prior ARGO "VRP>0=range=fade" e' SMENTITO: l'edge robusto e' su VRP<0
|
||||
e su DVOL bassa (questo gate dvol_pct<0.4), non su vol alta.
|
||||
|
||||
DIPENDENZA REGIME (caveat deploy): il gate usa DVOL/dvol_pct. Per il BACKTEST le feature
|
||||
arrivano da regime_lab (cache da Deribit mainnet). Per il LIVE serve un feed DVOL in produzione
|
||||
(regime_fetcher + allineamento causale nel runner) -> wiring NON ancora fatto. Finche' manca,
|
||||
FR01 e' validata-in-ricerca ma non deployabile live.
|
||||
|
||||
Run backtest: uv run python scripts/strategies/FR01_hurst_calm_fade.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.strategies.base import Strategy, Signal # noqa: E402
|
||||
from src.fractal.indicators import rolling_hurst # noqa: E402
|
||||
|
||||
|
||||
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 HurstCalmFade(Strategy):
|
||||
name = "FR01_hurst_calm_fade"
|
||||
description = "Fade Bollinger gateato da Hurst<0.55 (anti-persistente) + DVOL bassa (calm)"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
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]:
|
||||
bb_w = params.get("bb_window", 50)
|
||||
k = params.get("k", 2.5)
|
||||
sl_atr = params.get("sl_atr", 2.0)
|
||||
max_bars = params.get("max_bars", 24)
|
||||
hurst_thr = params.get("hurst_thr", 0.55)
|
||||
hurst_win = params.get("hurst_win", 100)
|
||||
dvol_pct_thr = params.get("dvol_pct_thr", 0.40)
|
||||
|
||||
c = df["close"].values.astype(float)
|
||||
n = len(c)
|
||||
ma = pd.Series(c).rolling(bb_w).mean().values
|
||||
sd = pd.Series(c).rolling(bb_w).std().values
|
||||
a = _atr(df, 14)
|
||||
hurst = rolling_hurst(c, window=hurst_win) # causale (returns[i-win:i])
|
||||
# dvol_pct: dalla colonna se presente (regime_lab.load_features), altrimenti gate OFF
|
||||
dvol_pct = df["dvol_pct"].values if "dvol_pct" in df.columns else np.full(n, np.nan)
|
||||
|
||||
signals: list[Signal] = []
|
||||
for i in range(bb_w + 14, n):
|
||||
if np.isnan(sd[i]) or np.isnan(a[i]) or sd[i] == 0:
|
||||
continue
|
||||
# GATE FRATTALE x REGIME (tutto noto a i)
|
||||
if hurst[i] >= hurst_thr:
|
||||
continue
|
||||
if "dvol_pct" in df.columns:
|
||||
if np.isnan(dvol_pct[i]) or dvol_pct[i] >= dvol_pct_thr:
|
||||
continue
|
||||
up, lo = ma[i] + k * sd[i], ma[i] - k * sd[i]
|
||||
if c[i] < lo:
|
||||
d, sl = 1, c[i] - sl_atr * a[i]
|
||||
elif c[i] > up:
|
||||
d, sl = -1, c[i] + sl_atr * a[i]
|
||||
else:
|
||||
continue
|
||||
signals.append(Signal(
|
||||
idx=i, direction=d, entry_price=float(c[i]),
|
||||
metadata={"tp": float(ma[i]), "sl": float(sl), "max_bars": int(max_bars)},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# backtest via l'harness onesto + feature di regime_lab (DVOL reale)
|
||||
from scripts.analysis.regime_lab import load_features, report
|
||||
from scripts.analysis.explore_lab import robust
|
||||
|
||||
strat = HurstCalmFade()
|
||||
print(f"{'=' * 100}")
|
||||
print(f" FR01 HURST-CALM FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
|
||||
print(f" gate: hurst<0.55 (anti-persistente) + dvol_pct<0.40 (DVOL bassa)")
|
||||
print(f"{'=' * 100}")
|
||||
for asset in ("BTC", "ETH"):
|
||||
df = load_features(asset, "1h")
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
sigs = strat.generate_signals(df, ts)
|
||||
ent = [{"i": s.idx, "d": s.direction, "tp": s.metadata["tp"],
|
||||
"sl": s.metadata["sl"], "max_bars": s.metadata["max_bars"]} for s in sigs]
|
||||
res = report(f"FR01_{asset}_1h", ent, df)
|
||||
print(f" -> {asset}: robust={robust(res)} OOS Sharpe={res['oos']['sharpe']:.2f} "
|
||||
f"OOS ret={res['oos']['ret']:+.0f}% DD={res['full']['dd']:.0f}%")
|
||||
@@ -0,0 +1,195 @@
|
||||
"""MR01 — Bollinger Fade (mean-reversion).
|
||||
|
||||
L'UNICA famiglia con edge netto reale dopo l'analisi out-of-sample fee-aware
|
||||
(vedi scripts/analysis/strategy_research.py). Contrario della tesi squeeze:
|
||||
i breakout RIENTRANO, quindi si fada l'estremo verso la media.
|
||||
|
||||
Logica:
|
||||
1. Bollinger Band (window n, k deviazioni) sul close
|
||||
2. ENTRY: close esce sotto la banda inferiore -> long (o sopra la superiore -> short)
|
||||
3. EXIT: take-profit alla media mobile (il rientro atteso),
|
||||
stop-loss a sl_atr*ATR oltre l'estremo, oppure time-limit max_bars
|
||||
4. ingresso a close[i] (eseguibile dal vivo, nessun look-ahead)
|
||||
|
||||
Validazione (netto, fee 0.10% RT reale Deribit, leva 3x, OOS = ultimo 30%):
|
||||
BTC 1h n=50 k=2.5: +201% OOS, DD 15%, ~tutti gli anni positivi
|
||||
ETH 1h n=50 k=2.0: +1238% OOS, DD 23%
|
||||
Robusto su TUTTA la griglia n in {14,20,30,50} x k in {2.0,2.5,3.0}
|
||||
e su tutte le fee 0.00-0.20% RT (margine di sicurezza ampio).
|
||||
|
||||
NOTA LIVE: usa TP alla media + SL ad ATR + max_bars. Lo StrategyWorker attuale
|
||||
esce solo a hold_bars/stop -2% fisso: per tradarla come validata il worker deve
|
||||
supportare gli exit TP/SL passati in metadata (vedi metadata di ogni Signal).
|
||||
"""
|
||||
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
|
||||
from src.strategies.fade_base import hurst_skip_mask
|
||||
|
||||
|
||||
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 BollingerFade(Strategy):
|
||||
name = "MR01_bollinger_fade"
|
||||
description = "Mean-reversion: fada la banda di Bollinger, TP alla media"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["1h"]
|
||||
fee_rt = 0.001 # Deribit perp realistico (taker 0.05%/lato)
|
||||
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_len = len(c)
|
||||
bb_w = params.get("bb_window", 50)
|
||||
k = params.get("k", 2.5)
|
||||
sl_atr = params.get("sl_atr", 2.0)
|
||||
max_bars = params.get("max_bars", 24)
|
||||
trend_max = params.get("trend_max") # None = filtro disattivo
|
||||
ema_long = params.get("ema_long", 200)
|
||||
# Edge minimo: salta i segnali il cui TP (la media) è più vicino dell'entry del
|
||||
# costo round-trip -> perdenti garantiti anche colpendo il TP. 0 = off.
|
||||
min_tp_frac = params.get("min_tp_frac", 0.0)
|
||||
# Loss-guard Hurst: salta in regime persistente/trending (hurst >= soglia). None = off.
|
||||
hurst_max = params.get("hurst_max")
|
||||
|
||||
ma = pd.Series(c).rolling(bb_w).mean().values
|
||||
sd = pd.Series(c).rolling(bb_w).std().values
|
||||
a = _atr(df, 14)
|
||||
up, lo = ma + k * sd, ma - k * sd
|
||||
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values if trend_max is not None else None
|
||||
skip = hurst_skip_mask(df, hurst_max, params.get("hurst_win", 100))
|
||||
|
||||
signals: list[Signal] = []
|
||||
for i in range(bb_w + 14, n_len):
|
||||
if np.isnan(up[i]) or np.isnan(a[i]):
|
||||
continue
|
||||
if skip[i]:
|
||||
continue # loss-guard: regime persistente
|
||||
if el is not None and (a[i] == 0 or np.isnan(el[i]) or abs(c[i] - el[i]) / a[i] > trend_max):
|
||||
continue
|
||||
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
|
||||
d, sl = 1, c[i] - sl_atr * a[i]
|
||||
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
|
||||
d, sl = -1, c[i] + sl_atr * a[i]
|
||||
else:
|
||||
continue
|
||||
if min_tp_frac > 0 and abs(ma[i] - c[i]) / c[i] <= min_tp_frac:
|
||||
continue # TP entro le fee -> non eseguibile in utile
|
||||
signals.append(Signal(
|
||||
idx=i, direction=d, entry_price=c[i],
|
||||
metadata={"tp": float(ma[i]), "sl": float(sl), "max_bars": max_bars},
|
||||
))
|
||||
return signals
|
||||
|
||||
def backtest(self, asset: str, tf: str = "1h", hold: int = 3,
|
||||
**params) -> BacktestResult | None:
|
||||
"""Backtest fedele: TP alla media / SL ad ATR / time-limit, fee+leva nette."""
|
||||
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)
|
||||
# EXIT-16 close-confirm SL (2026-06-04): come in fade_base.FadeStrategy.backtest.
|
||||
# None = comportamento storico. Vedi docs/diary/2026-06-04-exit-lab.md.
|
||||
sl_confirm = params.get("sl_confirm_atr")
|
||||
a14 = _atr(df, 14) if sl_confirm else None
|
||||
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_tp = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
|
||||
if sl_confirm is None:
|
||||
hit_sl = (d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl)
|
||||
if hit_sl:
|
||||
exit_p = sl; break
|
||||
if hit_tp:
|
||||
exit_p = tp; break
|
||||
else:
|
||||
# close-confirm: TP intrabar al livello; SL valutato sul CLOSE
|
||||
if hit_tp:
|
||||
exit_p = tp; break
|
||||
buf = sl_confirm * a14[j]
|
||||
if (d == 1 and c[j] < sl - buf) or (d == -1 and c[j] > sl + buf):
|
||||
exit_p = c[j]; 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 = BollingerFade()
|
||||
print(f"{'=' * 110}")
|
||||
print(f" MR01 BOLLINGER FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
|
||||
print(f"{'=' * 110}")
|
||||
results = []
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for k in [2.0, 2.5]:
|
||||
r = strat.backtest(asset, "1h", bb_window=50, k=k, sl_atr=2.0, max_bars=24)
|
||||
if r:
|
||||
r.strategy_name = f"MR01 {asset} 1h n50 k{k}"
|
||||
results.append(r)
|
||||
for r in results:
|
||||
r.print_summary()
|
||||
if results:
|
||||
results[0].print_yearly()
|
||||
@@ -0,0 +1,91 @@
|
||||
"""MR02 — Donchian Fade (mean-reversion sugli estremi del canale).
|
||||
|
||||
L'opposto esatto del trend-following Donchian (che PERDE netto: vedi
|
||||
scripts/analysis/strategy_research.py). Coerente con la lezione squeeze:
|
||||
i breakout RIENTRANO, quindi si fada la rottura del canale verso il centro.
|
||||
|
||||
Logica:
|
||||
1. Canale Donchian: massimo/minimo delle ultime n barre (escludendo la corrente)
|
||||
2. ENTRY: close rompe SOPRA il massimo del canale -> SHORT (fade);
|
||||
close rompe SOTTO il minimo -> LONG. Ingresso a close[i] (eseguibile).
|
||||
3. EXIT: take-profit al centro del canale (il rientro atteso),
|
||||
stop-loss a sl_atr*ATR oltre l'estremo, time-limit max_bars.
|
||||
|
||||
Validazione (netto, fee 0.10% RT reale Deribit, leva 3x, OOS = ultimo 30%):
|
||||
BTC 1h n=20: +879% FULL / +171% OOS, DD 30%, 8/9 anni positivi
|
||||
ETH 1h n=20: enorme FULL / +8452% OOS, DD 42%
|
||||
Robusto su TUTTA la griglia n in {10,20,30,50} x sl_atr in {1.5,2.0,3.0}
|
||||
(BTC+ETH 1h sempre positivo OOS) e su tutte le fee 0.00-0.20% RT.
|
||||
Ricerca completa: scripts/analysis/strategy_research_v2.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Signal
|
||||
from src.strategies.fade_base import FadeStrategy, atr, trend_distance, hurst_skip_mask
|
||||
|
||||
|
||||
class DonchianFade(FadeStrategy):
|
||||
name = "MR02_donchian_fade"
|
||||
description = "Mean-reversion: fada la rottura del canale Donchian, TP al centro"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["1h"]
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
n = params.get("n", 20)
|
||||
sl_atr = params.get("sl_atr", 2.0)
|
||||
max_bars = params.get("max_bars", 24)
|
||||
trend_max = params.get("trend_max") # None = filtro disattivo
|
||||
ema_long = params.get("ema_long", 200)
|
||||
# Edge minimo: salta i fade il cui TP (midpoint canale) è entro il costo RT. 0 = off.
|
||||
min_tp_frac = params.get("min_tp_frac", 0.0)
|
||||
# Loss-guard Hurst: salta in regime persistente/trending (hurst >= soglia). None = off.
|
||||
hurst_max = params.get("hurst_max")
|
||||
|
||||
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
||||
hh = pd.Series(h).rolling(n).max().shift(1).values
|
||||
ll = pd.Series(l).rolling(n).min().shift(1).values
|
||||
a = atr(df, 14)
|
||||
td = trend_distance(df, ema_long) if trend_max is not None else None
|
||||
skip = hurst_skip_mask(df, hurst_max, params.get("hurst_win", 100))
|
||||
|
||||
signals: list[Signal] = []
|
||||
for i in range(n + 14, len(c)):
|
||||
if np.isnan(hh[i]) or np.isnan(a[i]):
|
||||
continue
|
||||
if skip[i]:
|
||||
continue # loss-guard: regime persistente
|
||||
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
|
||||
continue
|
||||
mid = (hh[i] + ll[i]) / 2.0
|
||||
if c[i] > hh[i] and c[i - 1] <= hh[i - 1]: # rottura rialzista -> fade short
|
||||
d, sl = -1, c[i] + sl_atr * a[i]
|
||||
elif c[i] < ll[i] and c[i - 1] >= ll[i - 1]: # rottura ribassista -> fade long
|
||||
d, sl = 1, c[i] - sl_atr * a[i]
|
||||
else:
|
||||
continue
|
||||
if min_tp_frac > 0 and abs(mid - c[i]) / c[i] <= min_tp_frac:
|
||||
continue # TP entro le fee -> non eseguibile in utile
|
||||
signals.append(Signal(
|
||||
idx=i, direction=d, entry_price=c[i],
|
||||
metadata={"tp": float(mid), "sl": float(sl), "max_bars": max_bars},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strat = DonchianFade()
|
||||
print("=" * 110)
|
||||
print(f" MR02 DONCHIAN FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
|
||||
print("=" * 110)
|
||||
for asset in ["BTC", "ETH"]:
|
||||
r = strat.backtest(asset, "1h", n=20, sl_atr=2.0, max_bars=24)
|
||||
if r:
|
||||
r.strategy_name = f"MR02 {asset} 1h n20"
|
||||
r.print_summary()
|
||||
r.print_yearly()
|
||||
@@ -0,0 +1,97 @@
|
||||
"""MR07 — Return Reversal (fade del movimento di barra estremo).
|
||||
|
||||
Meccanismo distinto da MR01/MR02/MR03: non guarda i LIVELLI di prezzo (bande,
|
||||
canali) ma la VOLATILITA' dei rendimenti. Quando una singola barra si muove di
|
||||
piu' di k deviazioni standard rolling dei rendimenti, e' un'over-reaction che
|
||||
tende a rientrare: si fada nella direzione opposta. Coerente con la lezione
|
||||
mean-reversion.
|
||||
|
||||
Logica:
|
||||
1. ret[i] = rendimento dell'ultima barra; sigma = std rolling(n) dei rendimenti
|
||||
2. z = ret[i]/sigma. Se z <= -k (crollo) -> LONG; se z >= +k (spike) -> SHORT.
|
||||
Ingresso a close[i] (eseguibile dal vivo, nessun look-ahead).
|
||||
3. EXIT: take-profit a tp_atr*ATR a favore, stop-loss a sl_atr*ATR contro,
|
||||
time-limit max_bars.
|
||||
|
||||
Validazione (netto, fee 0.10% RT reale Deribit, leva 3x, OOS = ultimo 30%):
|
||||
config robusta k=3.5 tp=2ATR sl=1.5ATR n=50:
|
||||
BTC 1h: +447% FULL / +105% OOS, DD 25%
|
||||
ETH 1h: +335% FULL / +195% OOS, DD 46%
|
||||
L'intero blocco tp_atr=2.0 (k in {2.5,3.0,3.5} x sl in {1.5,2.0,2.5}) e'
|
||||
positivo full+OOS su entrambi gli asset 1h.
|
||||
Ricerca completa: scripts/analysis/strategy_research_v2.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Signal
|
||||
from src.strategies.fade_base import FadeStrategy, atr, trend_distance, hurst_skip_mask
|
||||
|
||||
|
||||
class ReturnReversal(FadeStrategy):
|
||||
name = "MR07_return_reversal"
|
||||
description = "Mean-reversion: fada il movimento di barra estremo (z dei rendimenti)"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["1h"]
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
n = params.get("n", 50)
|
||||
k = params.get("k", 3.5)
|
||||
tp_atr = params.get("tp_atr", 2.0)
|
||||
sl_atr = params.get("sl_atr", 1.5)
|
||||
max_bars = params.get("max_bars", 24)
|
||||
trend_max = params.get("trend_max") # None = filtro disattivo
|
||||
ema_long = params.get("ema_long", 200)
|
||||
# Edge minimo: salta i fade il cui TP (ATR-scaled) è entro il costo RT. 0 = off.
|
||||
min_tp_frac = params.get("min_tp_frac", 0.0)
|
||||
# Loss-guard Hurst: salta in regime persistente/trending (hurst >= soglia). None = off.
|
||||
hurst_max = params.get("hurst_max")
|
||||
|
||||
c = df["close"].values
|
||||
ret = np.zeros_like(c)
|
||||
ret[1:] = np.diff(c) / c[:-1]
|
||||
sig = pd.Series(ret).rolling(n).std().values
|
||||
a = atr(df, 14)
|
||||
td = trend_distance(df, ema_long) if trend_max is not None else None
|
||||
skip = hurst_skip_mask(df, hurst_max, params.get("hurst_win", 100))
|
||||
|
||||
signals: list[Signal] = []
|
||||
for i in range(n + 14, len(c)):
|
||||
if np.isnan(sig[i]) or sig[i] == 0 or np.isnan(a[i]):
|
||||
continue
|
||||
if skip[i]:
|
||||
continue # loss-guard: regime persistente
|
||||
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
|
||||
continue
|
||||
z = ret[i] / sig[i]
|
||||
if z <= -k: # crollo di barra -> fade long
|
||||
d, tp, sl = 1, c[i] + tp_atr * a[i], c[i] - sl_atr * a[i]
|
||||
elif z >= k: # spike di barra -> fade short
|
||||
d, tp, sl = -1, c[i] - tp_atr * a[i], c[i] + sl_atr * a[i]
|
||||
else:
|
||||
continue
|
||||
if min_tp_frac > 0 and abs(tp - c[i]) / c[i] <= min_tp_frac:
|
||||
continue # TP entro le fee -> non eseguibile in utile
|
||||
signals.append(Signal(
|
||||
idx=i, direction=d, entry_price=c[i],
|
||||
metadata={"tp": float(tp), "sl": float(sl), "max_bars": max_bars},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strat = ReturnReversal()
|
||||
print("=" * 110)
|
||||
print(f" MR07 RETURN REVERSAL — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
|
||||
print("=" * 110)
|
||||
for asset in ["BTC", "ETH"]:
|
||||
r = strat.backtest(asset, "1h", n=50, k=3.5, tp_atr=2.0, sl_atr=1.5, max_bars=24)
|
||||
if r:
|
||||
r.strategy_name = f"MR07 {asset} 1h k3.5"
|
||||
r.print_summary()
|
||||
r.print_yearly()
|
||||
@@ -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,83 @@
|
||||
"""PR01 — Pairs / Spread Mean-Reversion fra cripto (market-neutral). FAMIGLIA NUOVA.
|
||||
|
||||
Distinta da tutto l'esistente (single-asset direzionale): scommette sul RIENTRO del
|
||||
log-ratio di due cripto verso la sua media. Market-neutral (long A / short B) ->
|
||||
correlazione ~0.02 col mercato -> diversificatore prezioso.
|
||||
|
||||
Logica (engine onesto verificato in scripts/analysis/pairs_research.py):
|
||||
r[i] = log(closeA[i]/closeB[i]); z[i] = (r[i]-SMA_n(r)[i]) / STD_n(r)[i] (causale)
|
||||
z <= -z_in -> LONG ratio (long A / short B)
|
||||
z >= +z_in -> SHORT ratio (short A / long B)
|
||||
EXIT: |z| <= z_exit (rientro) o time-limit max_bars. Ingresso/uscita a close.
|
||||
Fee su 2 GAMBE = 2*fee_rt*lev (0.20% RT/coppia). Filtro candele sporche (salto>8%).
|
||||
|
||||
Validazione anti-overfit (netto, fee 0.20% RT/coppia a 2 gambe, leva 3x, OOS = ultimo
|
||||
30%, CONFIG UNIVERSALE n=50 z_in=2.0 z_exit=0.75 max_bars=72, 1h):
|
||||
ETH/BTC : CAGR 158% / Sharpe 4.36 / 8/9 anni+ (regge fee 6x)
|
||||
LTC/ETH : CAGR 92% / Sharpe 3.08 / 8/8 anni+
|
||||
ADA/ETH : CAGR 91% / Sharpe 2.69 / 7/8 anni+
|
||||
BTC/LTC : CAGR 60% / Sharpe 2.36 / 7/8 anni+ (robusta anche a 4h)
|
||||
ETH/SOL : CAGR 74% / Sharpe 1.96 / 5/7 anni+ (la piu' debole, DD ~63%)
|
||||
- No look-ahead verificato (z[i] invariato perturbando il futuro).
|
||||
- PLATEAU non picco: heatmap n x z_in -> 20/20 celle Sharpe>1 (ETH/BTC, BTC/LTC).
|
||||
- WALK-FORWARD (rolling train 2y / test 6m): ETH/BTC 11/12 finestre positive,
|
||||
BTC/LTC 9/10 -> edge distribuito su tutta la storia, non un regime singolo.
|
||||
- Stress costi: 5/6 reggono fee+slippage realistici; solo ETH/BTC regge fee 6x.
|
||||
- Correlazione con BTC daily ~0.02-0.08 -> market-neutral.
|
||||
- SCARTATA BNB/ETH: robusta solo coi suoi parametri (overfit), crolla con la universale.
|
||||
|
||||
WORKER LIVE: implementato `src/live/pairs_worker.py` (2 gambe, fee doppie, stato
|
||||
persistente) e wired in `multi_runner` (sezione `pairs:` in strategies.yml). Validato:
|
||||
- LOGICA: `validate_worker_pairs.py` -> replay storico == backtest pairs_sim ESATTO
|
||||
(ETH/BTC: capitale, n.trade, win% identici).
|
||||
- LIVE: `live_smoke_pairs.py` (smoke reale Cerbero) -> tutte e 5 le coppie con feed
|
||||
live fresco. Naming Deribit corretto: BTC/ETH = "<COIN>-PERPETUAL" (inverse),
|
||||
alt = "<COIN>_USDC-PERPETUAL" (lineari USDC, storia dal 2022). Trappola: usare
|
||||
"LTC-PERPETUAL"/"SOL-PERPETUAL" da' vuoto/dati sbagliati -> SEMPRE _USDC-PERPETUAL.
|
||||
Resta da verificare in trading reale solo la liquidita'/fill in esecuzione.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.analysis.pairs_research import pairs_sim, OOS_FRAC # noqa: E402
|
||||
|
||||
# CONFIG UNIVERSALE (anti-overfit): un'unica terna per TUTTE le coppie, niente
|
||||
# cherry-picking per-coppia. Validata come ALTOPIANO (heatmap n x z_in: 20/20 celle
|
||||
# Sharpe>1 su ETH/BTC e BTC/LTC) e walk-forward (ETH/BTC 11/12 finestre+, BTC/LTC 9/10).
|
||||
UNIV = dict(n=50, z_in=2.0, z_exit=0.75, max_bars=72)
|
||||
|
||||
# Coppie robuste con la config universale. Sempre alt-liquido vs major (mai alt/alt).
|
||||
# BNB/ETH SCARTATA: era robusta solo coi suoi parametri (n=30, z_exit=1.0) -> overfit;
|
||||
# con la config universale crolla (Sharpe 1.5, DD 71%) ed e' la prima a morire allo stress costi.
|
||||
PAIRS = [
|
||||
("ETH", "BTC", UNIV), # la migliore (Sharpe 4.4 univ), regge fee 6x
|
||||
("LTC", "ETH", UNIV),
|
||||
("ADA", "ETH", UNIV),
|
||||
("BTC", "LTC", UNIV), # robusta anche a 4h
|
||||
("ETH", "SOL", UNIV), # piu' debole (DD ~63%, storia SOL corta) -> peso ridotto
|
||||
]
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 92)
|
||||
print(" PR01 — PAIRS spread reversion (market-neutral) | netto fee 0.20% RT/coppia, leva 3x")
|
||||
print("=" * 92)
|
||||
print(f" {'coppia':<10s}{'trd':>5s}{'win%':>6s}{'CAGR%':>7s}{'OOS DD%':>8s}{'DD%':>6s}{'Shrp':>6s}{'anni+':>7s}")
|
||||
for a, b, p in PAIRS:
|
||||
f = pairs_sim(a, b, **p)
|
||||
o = pairs_sim(a, b, **p, split_frac=1 - OOS_FRAC)
|
||||
yrs = f["yearly"]; pos_y = sum(1 for v in yrs.values() if v > 0)
|
||||
print(f" {a+'/'+b:<10s}{f['trades']:>5d}{f['win']:>6.1f}{f['cagr']:>7.0f}{o['dd']:>8.0f}"
|
||||
f"{f['dd']:>6.0f}{f['sharpe']:>6.2f}{f'{pos_y}/{len(yrs)}':>7s}")
|
||||
print("\n Market-neutral (corr ~0.02-0.08 col mercato) -> ottimo diversificatore di portafoglio.")
|
||||
print(" Pattern: solo alt-liquido vs major (BTC/ETH); alt-vs-alt e' rumore.")
|
||||
print(" NB: 2 gambe (long A / short B), fee doppie. Worker live da estendere prima del live.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -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,109 @@
|
||||
"""SH01 — Shape-ML: direzione predetta dalla FORMA del segnale (ML walk-forward).
|
||||
|
||||
FAMIGLIA NUOVA, distinta da tutto l'esistente. Non e' una regola fissa su bande/canali
|
||||
(fade) ne' momentum/rotazione (honest) ne' spread (pairs): una LogisticRegression legge
|
||||
la MORFOLOGIA della finestra recente (body/shadow delle candele, rendimenti, pendenza,
|
||||
curvatura, posizione di max/min, RSI, estensione) e predice il segno del rendimento a H
|
||||
barre. Entra a close[i] solo se la probabilita' supera una soglia (selettivita').
|
||||
|
||||
E' l'UNICO edge sopravvissuto alla ricerca sui pattern-di-forma (2026-05-29): le altre
|
||||
4 famiglie testate con agenti paralleli su harness onesto sono RUMORE (analog kNN forma
|
||||
grezza, encoding candele UP/DOWN/DOJI, DTW+template geometrici, PIP/pivot) — confermano
|
||||
la dominanza mean-reversion. Vedi scripts/analysis/shape_*_research.py e docs/diary.
|
||||
|
||||
Logica (engine onesto verificato in scripts/analysis/shape_ml_research.py):
|
||||
feature di forma X[i] da o/h/l/c[i-W+1..i] (causali: solo dati fino a close[i])
|
||||
walk-forward a blocchi: scaler+modello fittati SOLO sul passato con esito noto
|
||||
(finestre e con e+H <= inizio_blocco-1), poi predicono il blocco corrente
|
||||
proba(classe) >= thresh -> entra a close[i] nella direzione predetta, exit a H barre
|
||||
fee 0.10% RT (single-leg). NESSUN look-ahead (check espliciti: perturbare il futuro
|
||||
non cambia ne' le feature a i ne' le predizioni fino a i).
|
||||
|
||||
VALIDAZIONE DURA (netto fee, leva 3x, pos 0.15, OOS = ultimo 30%, config W24 H12 th0.58,
|
||||
scripts/analysis/shape_ml_validate.py):
|
||||
- MULTI-ASSET expanding: robusti BTC, ETH, ADA; scartati LTC/SOL/XRP.
|
||||
BTC : FULL +219% / OOS +42% / Sharpe 2.72 / DD 23% / 8-9 anni+ / accOOS 56% (regge fee 0.2%: +60/+26)
|
||||
ETH : FULL +80% / OOS +144% / Sharpe 1.21 / DD 61% / 6/9 anni+ / accOOS 55% (piu' volatile -> secondario)
|
||||
ADA : FULL +707% / OOS +57% / Sharpe 3.22 / DD 39% / 7/8 anni+ (robusto solo expanding)
|
||||
- WALK-FORWARD ROLLING (train fisso 2 anni): regge solo BTC (FULL +166% / OOS +96% / Sharpe 2.05).
|
||||
-> l'edge si appoggia in parte alla memoria lunga: BTC e' il piu' solido.
|
||||
- STRESS leva 2x + slippage doppio (fee 0.20% RT): BTC OK (FULL +40% / OOS +17% / Sharpe 1.24),
|
||||
ETH marginale (FULL +7% / OOS +73% / Sharpe 0.37).
|
||||
- GRIGLIA (W,H,thresh) su BTC: 5/27 celle robuste a fee 0.2%, su una CRESTA stretta
|
||||
(W24, H8-12), non altopiano largo -> rischio overfit moderato. Per prudenza si sceglie
|
||||
la config robusta sul PIU' ALTO numero di test (W24 H12 th0.58), non il PnL massimo
|
||||
(W24 H8 rende di piu' ma accOOS ~49% = piu' drift che segnale).
|
||||
|
||||
USO CONSIGLIATO: NON motore standalone (per-asset e' troppo stretto/fragile fuori da BTC),
|
||||
ma DIVERSIFICATORE di portafoglio. Corr daily col MASTER +0.08 (quasi scorrelato).
|
||||
Aggiungere lo sleeve shape (BTC+ETH) al MASTER migliora l'OOS: Sharpe 4.33->5.10,
|
||||
DD 4.7%->4.2% (scripts/analysis/shape_ml_validate.py sez. 5).
|
||||
|
||||
LIVE: richiede un worker capace di RIALLENARE periodicamente il modello (come il legacy
|
||||
signal_engine ML, non lo StrategyWorker a regola fissa). Da wirare prima del paper live.
|
||||
"""
|
||||
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 src.strategies.base import Strategy, Signal # noqa: E402
|
||||
from scripts.analysis.explore_lab import get_df, evaluate, robust # noqa: E402
|
||||
from scripts.analysis.shape_ml_research import ml_wf_entries # noqa: E402
|
||||
|
||||
# Config robusta scelta (cresta W24 H8-12; H12 th0.58 = la piu' robusta sui test).
|
||||
CONFIG = dict(W=24, H=12, model="logit", thresh=0.58)
|
||||
|
||||
# Asset con edge robusto. BTC primario (regge ogni stress); ETH secondario (diversificatore
|
||||
# piu' volatile). ADA robusto solo expanding -> tenuto fuori dal set live conservativo.
|
||||
ASSETS = ["BTC", "ETH"]
|
||||
|
||||
|
||||
class ShapeMLStrategy(Strategy):
|
||||
name = "SH01_shape_ml"
|
||||
description = "Direzione predetta dalla forma del segnale (LogisticRegression walk-forward), exit a H barre"
|
||||
default_assets = ASSETS
|
||||
default_timeframes = ["1h"]
|
||||
fee_rt = 0.001
|
||||
leverage = 3.0
|
||||
position_size = 0.15
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex, **params) -> list[Signal]:
|
||||
cfg = {**CONFIG, **{k: params[k] for k in ("W", "H", "model", "thresh") if k in params}}
|
||||
# last_block_only (live, 2026-06-07): il runner passa la storia FULL (parquet
|
||||
# bootstrap + feed) e qui si fitta/predice solo l'ultimo blocco del walk-forward
|
||||
# (segnali identici al WF completo per costruzione, 1 fit invece di ~140).
|
||||
# La ri-validazione punto-10 ha mostrato che il regime corto (train 365g) NON
|
||||
# e' robusto (BTC negativo a fee 2x, trade-rate 22% vs 10%): l'edge richiede
|
||||
# il training expanding full-history. Vedi sh01_trainwindow_validate.py.
|
||||
ents = ml_wf_entries(df, W=cfg["W"], H=cfg["H"], model=cfg["model"], thresh=cfg["thresh"],
|
||||
last_block_only=bool(params.get("last_block_only")))
|
||||
c = df["close"].values
|
||||
out: list[Signal] = []
|
||||
for e in ents:
|
||||
out.append(Signal(idx=e["i"], direction=e["d"], entry_price=float(c[e["i"]]),
|
||||
metadata={"max_bars": e["max_bars"]}))
|
||||
return out
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 96)
|
||||
print(" SH01 — Shape-ML | direzione dalla FORMA (LogisticRegression walk-forward) | netto fee 0.10% RT, leva 3x")
|
||||
print("=" * 96)
|
||||
print(f" config: {CONFIG} (W=finestra forma, H=orizzonte/exit, thresh=soglia proba)")
|
||||
for a in ASSETS:
|
||||
df = get_df(a, "1h")
|
||||
ents = ml_wf_entries(df, **CONFIG)
|
||||
res = evaluate(f"{a}", ents, df)
|
||||
print(f" ^ {'ROBUSTO (FULL+OOS+, regge fee 0.2%, ~tutti anni+)' if robust(res) else 'edge presente ma con anni negativi (diversificatore)'}")
|
||||
print("\n Uso: diversificatore di portafoglio (corr ~0.08 col MASTER), non motore standalone.")
|
||||
print(" Live: serve worker con retraining periodico del modello (vedi docstring).")
|
||||
|
||||
|
||||
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,105 @@
|
||||
"""XS01 — Cross-Sectional Reversion (market-neutral su 8 cripto). FAMIGLIA NUOVA.
|
||||
|
||||
Distinta dai pairs PR01 (pairwise) e dai fade (single-asset): ogni HOLD ore classifica
|
||||
gli 8 asset per rendimento su LB ore e va LONG i perdenti relativi / SHORT i vincenti
|
||||
(peso ∝ -(ret - media_cross-section)), market-neutral gross 1. Cattura il FATTORE
|
||||
reversione cross-sezionale. Scorrelato (~0) da pairs e fade -> diversificatore.
|
||||
|
||||
Engine ONESTO (no look-ahead, verificato): pesi a barra i da close[<=i]; ingresso a
|
||||
close[i], uscita a close[i+HOLD]; roll NON sovrapposto (riallinea ogni HOLD barre).
|
||||
Fee = 0.10% RT/book (turnover gross 1 -> 2*fee_rt). PnL su capitale composto (pos, lev).
|
||||
|
||||
Validazione (sessione 2026-06-09, lb48 hold12, fee 0.10% RT, OOS ultimo 30%):
|
||||
FULL Sharpe ~3.3 / OOS ~3.4, plateau lb 12-72 x hold 6-24 (OOS 2-3.9), 4/5 anni+.
|
||||
Decorrelato (-0.006 da PR01 ETH/BTC). Cost-sensitive: muore ~0.35% RT/book.
|
||||
Gate PORT06: +XS01 -> OOS Sharpe 9.66->10.07, FULL DD 3.68->3.46 (OOS DD +0.06pp a mezza size).
|
||||
"""
|
||||
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
|
||||
|
||||
UNIVERSE = ["BTC", "ETH", "LTC", "ADA", "SOL", "BNB", "XRP", "DOGE"]
|
||||
FEE_RT, LEV, POS, OOS_FRAC = 0.0005, 3.0, 0.15, 0.30
|
||||
LB, HOLD = 48, 12
|
||||
|
||||
|
||||
def aligned_panel(assets=UNIVERSE, tf="1h"):
|
||||
dfs = {a: load_data(a, tf)[["timestamp", "close"]].rename(columns={"close": a}).set_index("timestamp")
|
||||
for a in assets}
|
||||
M = pd.concat(dfs.values(), axis=1, join="inner").sort_index()
|
||||
return M[assets]
|
||||
|
||||
|
||||
def xsec_sim(assets=UNIVERSE, tf="1h", lb=LB, hold=HOLD, fee_rt=FEE_RT, lev=LEV,
|
||||
pos=POS, split_frac=0.0):
|
||||
M = aligned_panel(assets, tf)
|
||||
C = M[assets].values
|
||||
ts = pd.to_datetime(M.index, unit="ms", utc=True)
|
||||
n = len(C); logC = np.log(C)
|
||||
split = int(n * split_frac)
|
||||
cap = peak = 1000.0; dd = 0.0
|
||||
trades = wins = 0; rets = []; yearly = {}; yearly_n = {}
|
||||
eq_ts, eq_v = [], []
|
||||
last = -1; i = max(lb, split)
|
||||
fee = 2 * fee_rt # gross 1 -> turnover 2 (entra+esce)
|
||||
while i < n - hold:
|
||||
if i <= last:
|
||||
i += 1; continue
|
||||
dm = (logC[i] - logC[i - lb]); dm = dm - dm.mean()
|
||||
w = -dm; gw = np.sum(np.abs(w))
|
||||
if gw < 1e-9:
|
||||
i += 1; continue
|
||||
w = w / gw # market-neutral, gross 1
|
||||
book = float(np.sum(w * (logC[i + hold] - logC[i])))
|
||||
net = book - fee
|
||||
cap = max(cap + cap * pos * lev * net, 10.0)
|
||||
peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
|
||||
trades += 1; wins += net > 0; rets.append(net * pos); last = i + hold
|
||||
eq_ts.append(ts[i + hold]); eq_v.append(cap)
|
||||
yearly[ts[i].year] = yearly.get(ts[i].year, 0.0) + net * 100
|
||||
yearly_n[ts[i].year] = yearly_n.get(ts[i].year, 0) + 1
|
||||
i += 1
|
||||
yrs_span = (ts[-1] - ts[max(split, 0)]).days / 365.25 or 1
|
||||
sharpe = 0.0
|
||||
if len(rets) > 1 and np.std(rets) > 0:
|
||||
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(trades / yrs_span))
|
||||
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, yearly_n=yearly_n,
|
||||
eq_ts=eq_ts, eq_v=eq_v)
|
||||
|
||||
|
||||
def check_no_lookahead():
|
||||
M = aligned_panel(); logC = np.log(M.values); i = 1000
|
||||
a = (logC[i] - logC[i - LB])
|
||||
Cp = logC.copy(); Cp[i + 1:] += 0.5
|
||||
b = (Cp[i] - Cp[i - LB])
|
||||
print(f" no-look-ahead: segnale invariato col futuro perturbato -> "
|
||||
f"{'OK' if np.allclose(a, b) else 'VIOLAZIONE'}")
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 84)
|
||||
print(" XS01 — Cross-Sectional Reversion (8 asset, market-neutral) | netto fee 0.10% RT/book")
|
||||
print("=" * 84)
|
||||
check_no_lookahead()
|
||||
f = xsec_sim()
|
||||
o = xsec_sim(split_frac=1 - OOS_FRAC)
|
||||
yrs = f["yearly"]; pos_y = sum(1 for v in yrs.values() if v > 0)
|
||||
print(f" trade {f['trades']} | win {f['win']:.1f}% | CAGR {f['cagr']:.0f}% | DD {f['dd']:.0f}% | "
|
||||
f"Sharpe FULL {f['sharpe']:.2f} / OOS {o['sharpe']:.2f} | anni+ {pos_y}/{len(yrs)}")
|
||||
print(" per anno:", " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(yrs.items())))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user