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,171 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user