14522262e6
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>
172 lines
7.2 KiB
Python
172 lines
7.2 KiB
Python
"""Harness ONESTO condiviso per esplorare nuove famiglie di strategie.
|
|
|
|
Regole NON negoziabili (per non ripetere l'errore squeeze look-ahead):
|
|
- direzione e prezzo decisi con dati FINO a close[i] incluso, mai con la barra i
|
|
usata per scegliere la direzione e poi entrare a i-1;
|
|
- ingresso ESEGUIBILE a close[i];
|
|
- exit: take-profit / stop-loss intrabar (high/low) e/o time-limit max_bars;
|
|
tp/sl possono essere None -> exit solo a tempo (utile per stagionalita');
|
|
- una posizione per volta (non-overlap), capitale composto;
|
|
- NETTO dopo fee round-trip (default 0.10% RT reale Deribit) e leva;
|
|
- validazione OOS (held-out, ultimo 30%) + sweep fee 0.00-0.20% RT.
|
|
|
|
Le strategie ad alta frequenza muoiono di fee: ogni entry costa fee_rt*lev sul
|
|
notional. Tienine conto: meno operazioni e edge > costi.
|
|
|
|
Asset disponibili: ADA BNB BTC DOGE ETH LTC SOL XRP (1h, 15m; BTC/ETH anche 5m).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from src.data.downloader import load_data
|
|
|
|
FEE_RT = 0.001 # Deribit perp realistico: taker ~0.05%/lato = 0.10% RT
|
|
LEV = 3.0
|
|
POS = 0.15
|
|
OOS_FRAC = 0.30
|
|
ASSETS = ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"]
|
|
BARS_PER_YEAR = {"5m": 105120, "15m": 35040, "1h": 8760, "4h": 2190, "1d": 365}
|
|
|
|
|
|
# --------------------------- dati ---------------------------
|
|
def get_df(asset: str, tf: str) -> pd.DataFrame:
|
|
"""OHLCV con colonna dt (UTC). tf nativo (5m,15m,1h) o resample da 1h (4h,1d).
|
|
timestamp resta ms-epoch reale anche dopo il resample (no placeholder)."""
|
|
if tf in ("5m", "15m", "1h"):
|
|
df = load_data(asset, tf).reset_index(drop=True)
|
|
else:
|
|
base = load_data(asset, "1h").copy()
|
|
base["dt"] = pd.to_datetime(base["timestamp"], unit="ms", utc=True)
|
|
base = base.set_index("dt")
|
|
rule = {"4h": "4h", "1d": "1D"}[tf]
|
|
agg = base.resample(rule).agg(
|
|
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
|
|
).dropna()
|
|
epoch = pd.Timestamp("1970-01-01", tz="UTC") # ms-epoch portabile (qualsiasi risoluzione)
|
|
agg["timestamp"] = ((agg.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
|
|
df = agg.reset_index(drop=True)
|
|
df["dt"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
|
return df
|
|
|
|
|
|
def _dt(df: pd.DataFrame) -> pd.DatetimeIndex:
|
|
return pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
|
|
|
|
|
# --------------------------- indicatori ---------------------------
|
|
def atr(df: pd.DataFrame, n: int = 14) -> np.ndarray:
|
|
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
|
pc = np.roll(c, 1); pc[0] = c[0]
|
|
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
|
|
return pd.Series(tr).rolling(n).mean().values
|
|
|
|
|
|
def ema(x: np.ndarray, n: int) -> np.ndarray:
|
|
return pd.Series(x).ewm(span=n, adjust=False).mean().values
|
|
|
|
|
|
def rsi(close: np.ndarray, n: int = 14) -> np.ndarray:
|
|
d = np.diff(close, prepend=close[0])
|
|
up = pd.Series(np.where(d > 0, d, 0.0)).ewm(alpha=1/n, adjust=False).mean()
|
|
dn = pd.Series(np.where(d < 0, -d, 0.0)).ewm(alpha=1/n, adjust=False).mean()
|
|
rs = up / dn.replace(0, np.nan)
|
|
return (100 - 100 / (1 + rs)).values
|
|
|
|
|
|
# --------------------------- engine ---------------------------
|
|
def simulate(entries: list[dict], df: pd.DataFrame, fee_rt: float = FEE_RT,
|
|
lev: float = LEV, pos: float = POS, split: int = -1) -> dict:
|
|
"""entries: dict con i(idx), d(+1/-1), max_bars; tp/sl opzionali (None=solo tempo).
|
|
split: se >0, conta solo entries con i>=split (finestra OOS)."""
|
|
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
|
n = len(c)
|
|
ts = _dt(df)
|
|
cap = peak = 1000.0
|
|
max_dd = 0.0
|
|
fee = fee_rt * lev
|
|
trades = wins = 0
|
|
last_exit = -1
|
|
bars_in = 0
|
|
yearly: dict[int, float] = {}
|
|
rets: list[float] = []
|
|
|
|
for e in entries:
|
|
i, d = e["i"], e["d"]
|
|
if i <= last_exit or i + 1 >= n or i < split:
|
|
continue
|
|
entry = c[i]
|
|
tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
|
|
exit_p = c[min(i + mb, n - 1)]
|
|
j = min(i + mb, n - 1)
|
|
for k in range(1, mb + 1):
|
|
j = i + k
|
|
if j >= n:
|
|
exit_p = c[n - 1]; break
|
|
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
|
|
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
|
|
if hit_sl:
|
|
exit_p = sl; break
|
|
if hit_tp:
|
|
exit_p = tp; break
|
|
if k == mb:
|
|
exit_p = c[j]
|
|
ret = (exit_p - entry) / entry * d * lev - fee
|
|
cb = cap
|
|
cap = max(cb + cb * pos * ret, 10.0)
|
|
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
|
|
trades += 1; wins += ret > 0; bars_in += (j - i)
|
|
last_exit = j
|
|
rets.append(ret * pos)
|
|
yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
|
|
|
|
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(len(rets))) if len(rets) > 1 and np.std(rets) > 0 else 0.0
|
|
return {
|
|
"trades": trades,
|
|
"win": wins / trades * 100 if trades else 0.0,
|
|
"ret": (cap / 1000 - 1) * 100,
|
|
"dd": max_dd * 100,
|
|
"sharpe": sharpe,
|
|
"yearly": yearly,
|
|
"exposure": bars_in / n * 100 if n else 0.0,
|
|
}
|
|
|
|
|
|
def evaluate(name: str, entries: list[dict], df: pd.DataFrame,
|
|
fees=(0.0, 0.0005, 0.001, 0.002)) -> dict:
|
|
"""Valuta una lista di entries: FULL, OOS e sweep fee. Stampa una riga sintetica."""
|
|
split = int(len(df) * (1 - OOS_FRAC))
|
|
full = simulate(entries, df)
|
|
oos = simulate(entries, df, split=split)
|
|
sweep = {f: simulate(entries, df, fee_rt=f)["ret"] for f in fees}
|
|
sweep_oos = {f: simulate(entries, df, fee_rt=f, split=split)["ret"] for f in fees}
|
|
yrs = full["yearly"]; pos_yrs = sum(1 for v in yrs.values() if v > 0)
|
|
print(f" {name:<24s} trd={full['trades']:>5d} win={full['win']:>4.1f}% "
|
|
f"FULL={full['ret']:>+7.0f}% OOS={oos['ret']:>+7.0f}% DD={full['dd']:>4.0f}% "
|
|
f"oDD={oos['dd']:>4.0f}% Shrp={full['sharpe']:>4.2f} exp={full['exposure']:>4.1f}% "
|
|
f"anniPos={pos_yrs}/{len(yrs)} | fee0.2%: FULL={sweep[0.002]:>+6.0f} OOS={sweep_oos[0.002]:>+6.0f}")
|
|
return {"full": full, "oos": oos, "sweep": sweep, "sweep_oos": sweep_oos, "pos_yrs": pos_yrs, "n_yrs": len(yrs)}
|
|
|
|
|
|
def robust(res: dict) -> bool:
|
|
"""Verdetto onesto: positivo FULL e OOS, regge a fee 0.20% RT, quasi tutti gli anni positivi."""
|
|
return (res["full"]["ret"] > 0 and res["oos"]["ret"] > 0
|
|
and res["sweep"][0.002] > 0 and res["sweep_oos"][0.002] > 0
|
|
and res["pos_yrs"] >= max(res["n_yrs"] - 1, 1))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# smoke test: una stagionalita' banale (hour-of-day) su BTC 1h
|
|
df = get_df("BTC", "1h"); ts = _dt(df)
|
|
ents = [{"i": i, "d": 1, "max_bars": 6, "tp": None, "sl": None}
|
|
for i in range(len(df) - 7) if ts.iloc[i].hour == 0]
|
|
print("smoke test — BTC long ad ogni 00:00 UTC, hold 6h:")
|
|
evaluate("seasonality_h0", ents, df)
|