212427ffa1
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>
176 lines
7.4 KiB
Python
176 lines
7.4 KiB
Python
"""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}%")
|