Files
PythagorasGoal/scripts/analysis/honest_candidates.py
T
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

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}%")