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,156 @@
|
||||
"""GATE PORT06 del candidato index_comp_disp W=168 (ricerca dispersion 2026-06-08).
|
||||
|
||||
Edge confermato avversarialmente: fade della componente idiosincratica di BTC verso
|
||||
l'indice EW, gated da alta dispersione. Config: rel_len=12, z_win=336, z_thr=1.5,
|
||||
disp_168 >= quantile rolling 0.7 (win 720), TP=1.0*ATR14, SL=1.5*ATR14, max_bars=24.
|
||||
|
||||
Domanda del gate (lezione FR01: robusto != migliora-il-portafoglio):
|
||||
1) correlazione daily col MASTER e con le fade BTC esistenti (e' un diversificatore?)
|
||||
2) PORT06 BASE (17 sleeve) vs +DISP (18 sleeve) con pesi cap: DeltaSharpe/DeltaDD FULL e OOS.
|
||||
PROMOSSO solo se decorrela E migliora (o non degrada) l'OOS.
|
||||
|
||||
uv run python scripts/analysis/dispersion_edges/gate_index_comp_disp.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[3]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.analysis.dispersion_lab import features, align_to
|
||||
from scripts.analysis.explore_lab import get_df, atr
|
||||
from scripts.analysis.combine_portfolio import _norm, IDX, port_returns, metrics, SPLIT, OOS_DATE
|
||||
from scripts.analysis.honest_improve2 import _daily_equity
|
||||
from scripts.portfolios._defs import PORTFOLIOS
|
||||
from src.portfolio import weighting as W
|
||||
|
||||
FEE_RT, LEV, POS, INIT = 0.001, 3.0, 0.15, 1000.0
|
||||
CFG = dict(rel_len=12, z_win=336, z_thr=1.5, disp_q=0.7, disp_q_win=720,
|
||||
tp_atr=1.0, sl_atr=1.5, max_bars=24)
|
||||
|
||||
|
||||
def _last_rank(x):
|
||||
if x.shape[0] < 2:
|
||||
return np.nan
|
||||
return float((x[:-1] < x[-1]).mean())
|
||||
|
||||
|
||||
def build_trades(asset="BTC"):
|
||||
"""Entries CAUSALI + exit intrabar (TP/SL/max_bars) -> [(i, j, ret_netto)]."""
|
||||
df = get_df(asset, "1h")
|
||||
F = features()
|
||||
fa = align_to(F, df)
|
||||
c, h, l = df["close"].values, df["high"].values, df["low"].values
|
||||
n = len(c)
|
||||
a14 = atr(df, 14)
|
||||
rel = fa[f"rel_{asset}"].values.astype(float)
|
||||
disp = fa["disp_168"].values.astype(float)
|
||||
# somma rolling rel su rel_len, z-score causale (mean/std rolling z_win shift 1)
|
||||
rs = pd.Series(rel).rolling(CFG["rel_len"]).sum()
|
||||
rmean = rs.rolling(CFG["z_win"]).mean().shift(1)
|
||||
rstd = rs.rolling(CFG["z_win"]).std().shift(1)
|
||||
z = ((rs - rmean) / rstd.replace(0, np.nan)).values
|
||||
dpct = pd.Series(disp).rolling(CFG["disp_q_win"]).apply(_last_rank, raw=True).values
|
||||
fee = FEE_RT * LEV
|
||||
out = []
|
||||
last = -1
|
||||
for i in range(n - 1):
|
||||
if i <= last or not np.isfinite(z[i]) or not np.isfinite(dpct[i]):
|
||||
continue
|
||||
if dpct[i] < CFG["disp_q"] or abs(z[i]) < CFG["z_thr"]:
|
||||
continue
|
||||
ai = a14[i]
|
||||
if not np.isfinite(ai) or ai <= 0:
|
||||
continue
|
||||
d = -1 if z[i] > 0 else 1
|
||||
tp = c[i] + d * CFG["tp_atr"] * ai
|
||||
sl = c[i] - d * CFG["sl_atr"] * ai
|
||||
mb = CFG["max_bars"]
|
||||
j = min(i + mb, n - 1)
|
||||
exit_p = c[j]
|
||||
for k in range(1, mb + 1):
|
||||
j = i + k
|
||||
if j >= n:
|
||||
j = n - 1; exit_p = c[j]; break
|
||||
if d == 1:
|
||||
if l[j] <= sl: exit_p = sl; break
|
||||
if h[j] >= tp: exit_p = tp; break
|
||||
else:
|
||||
if h[j] >= sl: exit_p = sl; break
|
||||
if l[j] <= tp: exit_p = tp; break
|
||||
if k == mb: exit_p = c[j]
|
||||
out.append((i, j, (exit_p - c[i]) / c[i] * d * LEV - fee))
|
||||
last = j
|
||||
return df, out
|
||||
|
||||
|
||||
def daily_equity(df, trades):
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
cap = INIT; eq_ts, eq_v = [], []
|
||||
for i, j, ret in sorted(trades, key=lambda t: t[1]):
|
||||
cap = max(cap + cap * POS * ret, 10.0)
|
||||
eq_ts.append(ts.iloc[j]); eq_v.append(cap)
|
||||
return _norm(_daily_equity(eq_ts, eq_v, IDX))
|
||||
|
||||
|
||||
def pmetrics(members, p, extra=None):
|
||||
ids = list(p.sleeve_ids) + ([extra] if extra else [])
|
||||
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
||||
if extra:
|
||||
caps = dict(p.caps); caps["DISP"] = caps.get("DISP", None)
|
||||
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
|
||||
caps=p.caps, clusters={**{i:(p.clusters or {}).get(i,i) for i in p.sleeve_ids},
|
||||
**({extra:"disp"} if extra else {})},
|
||||
lookback=p.vol_lookback)
|
||||
drp = port_returns({i: members[i] for i in ids}, w)
|
||||
return metrics(drp), metrics(drp, lo=SPLIT)
|
||||
|
||||
|
||||
def main():
|
||||
p = PORTFOLIOS["PORT06"]
|
||||
print("=" * 100)
|
||||
print(" GATE PORT06 — candidato index_comp_disp W=168 (BTC) | famiglia DISP nuova")
|
||||
print(f" config {CFG} | OOS da {OOS_DATE}")
|
||||
print("=" * 100)
|
||||
|
||||
from src.portfolio.sleeves import all_sleeve_equities
|
||||
eq_base = dict(all_sleeve_equities())
|
||||
|
||||
df, trades = build_trades("BTC")
|
||||
disp_eq = daily_equity(df, trades)
|
||||
fr = (disp_eq.iloc[-1] / disp_eq.iloc[0] - 1) * 100
|
||||
o = disp_eq.iloc[SPLIT:]; ofr = (o.iloc[-1] / o.iloc[0] - 1) * 100
|
||||
print(f"\n[1] candidato standalone: {len(trades)} trade | FULL {fr:+.0f}% | OOS {ofr:+.0f}%")
|
||||
|
||||
# correlazione daily col MASTER e con le fade BTC
|
||||
dr_cand = disp_eq.pct_change().fillna(0.0)
|
||||
print("\n[2] correlazione daily col candidato (decorrela?):")
|
||||
for sid in ["MR01_BTC", "MR02_BTC", "MR07_BTC", "DIP01_BTC"]:
|
||||
corr = dr_cand.corr(eq_base[sid].pct_change().fillna(0.0))
|
||||
print(f" {sid:<12} corr {corr:+.3f}")
|
||||
master_dr = pd.DataFrame({i: eq_base[i].pct_change().fillna(0.0) for i in p.sleeve_ids}).mean(axis=1)
|
||||
print(f" {'MASTER(EW)':<12} corr {dr_cand.corr(master_dr):+.3f}")
|
||||
|
||||
# PORT06 base vs +DISP
|
||||
f_b, o_b = pmetrics(eq_base, p)
|
||||
members = dict(eq_base); members["DISP_BTC"] = disp_eq
|
||||
f_e, o_e = pmetrics(members, p, extra="DISP_BTC")
|
||||
print("\n[3] PORT06 BASE (17) vs +DISP (18):")
|
||||
print(f" {'':<10}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}")
|
||||
print(f" {'BASE':<10}{f_b['sharpe']:>9.2f}{f_b['dd']:>10.2f}{o_b['sharpe']:>9.2f}{o_b['dd']:>9.2f}")
|
||||
print(f" {'+DISP':<10}{f_e['sharpe']:>9.2f}{f_e['dd']:>10.2f}{o_e['sharpe']:>9.2f}{o_e['dd']:>9.2f}")
|
||||
print(f" {'DELTA':<10}{f_e['sharpe']-f_b['sharpe']:>+9.2f}{f_e['dd']-f_b['dd']:>+10.2f}"
|
||||
f"{o_e['sharpe']-o_b['sharpe']:>+9.2f}{o_e['dd']-o_b['dd']:>+9.2f}")
|
||||
|
||||
promoted = (o_e['sharpe'] >= o_b['sharpe'] - 0.02 and o_e['dd'] <= o_b['dd'] + 0.20
|
||||
and f_e['sharpe'] >= f_b['sharpe'] - 0.02)
|
||||
print("\n VERDETTO: " + (">>> PROMOSSO <<<" if promoted else ">>> BOCCIATO (diluisce, come FR01) <<<"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,191 @@
|
||||
"""FAMIGLIA index_comp_disp (W=24) — dispersion trading REALIZZATO.
|
||||
|
||||
Idea: l'indice EW vs le componenti. Quando la dispersione cross-sectional rolling
|
||||
(disp_24) e' ALTA, le componenti idiosincratiche estreme (rel_A = ret_A - ret_idx)
|
||||
tendono a RIENTRARE verso l'indice -> fade della componente idiosincratica estrema:
|
||||
- se rel_A e' molto positivo (A ha sovraperformato l'indice oltre soglia) -> SHORT A
|
||||
- se rel_A e' molto negativo (A ha sottoperformato l'indice oltre soglia) -> LONG A
|
||||
condizionato a disp_24 sopra una soglia (regime di alta dispersione).
|
||||
|
||||
CAUSALE: la decisione a close[i] usa SOLO feature note a i:
|
||||
- rel_A[i] = log-ret di A meno log-ret indice nella barra [i-1->i] (nota a close[i])
|
||||
- disp_24[i] = media rolling 24 della disp cross-sectional fino a i (nota a close[i])
|
||||
Ingresso eseguibile a close[i]. Niente uso di i+1 nella decisione.
|
||||
|
||||
Per rendere le soglie comparabili fra asset/tempo, rel_A si normalizza con la sua
|
||||
deviazione standard rolling CAUSALE (rolling 168h, shiftata di 1 per non includere i).
|
||||
disp_24 si normalizza col suo quantile rolling causale (percentile rolling).
|
||||
|
||||
Exit: time-stop max_bars (con TP/SL ATR opzionali). Il fade verso l'indice e' un
|
||||
ritorno alla media -> orizzonte breve.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.analysis.dispersion_lab import features, align_to, UNIVERSE # noqa: E402
|
||||
from scripts.analysis.explore_lab import get_df, evaluate, robust, atr # noqa: E402
|
||||
|
||||
W = 24 # finestra famiglia
|
||||
|
||||
|
||||
def _last_rank(x: np.ndarray) -> float:
|
||||
"""Frazione dei valori (esclusi l'ultimo) strettamente < dell'ultimo. Causale:
|
||||
l'ultimo elemento e' la barra i, confrontata coi 719 valori precedenti."""
|
||||
if x.shape[0] < 2:
|
||||
return np.nan
|
||||
return float((x[:-1] < x[-1]).mean())
|
||||
|
||||
|
||||
def _causal_signals(asset: str, df: pd.DataFrame, fa: pd.DataFrame):
|
||||
"""Precalcola (una volta per asset) rel_z, disp_pctl, atr — feature CAUSALI."""
|
||||
a14 = atr(df, 14)
|
||||
rel = fa[f"rel_{asset}"].values.astype(float)
|
||||
disp = fa["disp_24"].values.astype(float)
|
||||
|
||||
# z-score CAUSALE di rel: media/std rolling 168h SHIFTATA di 1 (solo barre <= i-1)
|
||||
rel_s = pd.Series(rel)
|
||||
rmean = rel_s.rolling(168).mean().shift(1)
|
||||
rstd = rel_s.rolling(168).std().shift(1)
|
||||
rel_z = ((rel_s - rmean) / rstd.replace(0, np.nan)).values
|
||||
|
||||
# percentile rolling CAUSALE di disp_24 (rank di disp[i] vs i 720 valori fino a i).
|
||||
# vettoriale via rank rolling: pos dell'ultimo elemento / (win-1).
|
||||
win = 720
|
||||
dr = pd.Series(disp).rolling(win).apply(_last_rank, raw=True)
|
||||
disp_pctl = dr.values
|
||||
return rel_z, disp_pctl, a14
|
||||
|
||||
|
||||
def build_entries(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
|
||||
rel_z_thr: float, disp_pctl_thr: float,
|
||||
max_bars: int, tp_atr: float | None, sl_atr: float | None,
|
||||
precomp=None) -> list[dict]:
|
||||
"""Costruisce entries CAUSALI per il fade della componente idiosincratica."""
|
||||
n = len(df)
|
||||
c = df["close"].values
|
||||
if precomp is None:
|
||||
rel_z, disp_pctl, a14 = _causal_signals(asset, df, fa)
|
||||
else:
|
||||
rel_z, disp_pctl, a14 = precomp
|
||||
|
||||
entries: list[dict] = []
|
||||
for i in range(n - 1):
|
||||
z = rel_z[i]
|
||||
dp = disp_pctl[i]
|
||||
if not np.isfinite(z) or not np.isfinite(dp):
|
||||
continue
|
||||
if dp < disp_pctl_thr: # solo regime di alta dispersione
|
||||
continue
|
||||
if abs(z) < rel_z_thr: # solo componente idio estrema
|
||||
continue
|
||||
d = -1 if z > 0 else +1 # FADE: rel alto -> short A; rel basso -> long A
|
||||
a = a14[i]
|
||||
if not np.isfinite(a) or a <= 0:
|
||||
tp = sl = None
|
||||
else:
|
||||
tp = c[i] + d * tp_atr * a if tp_atr else None
|
||||
sl = c[i] - d * sl_atr * a if sl_atr else None
|
||||
entries.append({"i": i, "d": d, "max_bars": max_bars, "tp": tp, "sl": sl})
|
||||
return entries
|
||||
|
||||
|
||||
def check_no_lookahead(asset: str, df: pd.DataFrame, fa: pd.DataFrame) -> bool:
|
||||
"""Perturba i PREZZI dopo un indice T e verifica che le entries con i<=T non cambino.
|
||||
Qui ricostruiamo la entry-rule su una copia di df/fa col futuro alterato e confrontiamo
|
||||
le entries (i, d) con i<=T."""
|
||||
n = len(df)
|
||||
T = int(n * 0.6)
|
||||
|
||||
base = build_entries(asset, df, fa, rel_z_thr=2.0, disp_pctl_thr=0.6,
|
||||
max_bars=12, tp_atr=None, sl_atr=None)
|
||||
|
||||
# perturba i prezzi dopo T: alza del 50% close/high/low/open
|
||||
df2 = df.copy()
|
||||
for col in ("open", "high", "low", "close"):
|
||||
df2.loc[df2.index > T, col] = df2.loc[df2.index > T, col] * 1.5
|
||||
# perturba anche le feature riferite a barre > T (rel_<asset>, disp_24)
|
||||
fa2 = fa.copy()
|
||||
for col in (f"rel_{asset}", "disp_24"):
|
||||
fa2.loc[fa2.index > T, col] = fa2.loc[fa2.index > T, col] * 1.5
|
||||
|
||||
pert = build_entries(asset, df2, fa2, rel_z_thr=2.0, disp_pctl_thr=0.6,
|
||||
max_bars=12, tp_atr=None, sl_atr=None)
|
||||
|
||||
base_le = {(e["i"], e["d"]) for e in base if e["i"] <= T}
|
||||
pert_le = {(e["i"], e["d"]) for e in pert if e["i"] <= T}
|
||||
ok = base_le == pert_le
|
||||
print(f"[no-look-ahead] entries con i<=T={T} invarianti al futuro: "
|
||||
f"{'OK' if ok else 'VIOLATO'} (base={len(base_le)} pert={len(pert_le)})")
|
||||
if not ok:
|
||||
diff = (base_le ^ pert_le)
|
||||
print(f" differenze: {sorted(diff)[:10]}")
|
||||
return ok
|
||||
|
||||
|
||||
def main():
|
||||
F = features()
|
||||
print(f"feature caricate: {F.shape[0]} barre")
|
||||
|
||||
# asset single-asset da esplorare (i piu' liquidi + qualche alt)
|
||||
assets = ["BTC", "ETH", "SOL", "ADA", "BNB", "DOGE", "LTC", "XRP"]
|
||||
|
||||
# griglia piccola di soglie
|
||||
rel_z_grid = [1.5, 2.0, 2.5]
|
||||
disp_pctl_grid = [0.5, 0.7]
|
||||
mb_grid = [6, 12, 24]
|
||||
# exit: prima senza tp/sl (puro time-stop), poi con un TP/SL ATR moderato
|
||||
exit_grid = [
|
||||
(None, None),
|
||||
(1.5, 2.0),
|
||||
]
|
||||
|
||||
best = None
|
||||
# no-look-ahead check una volta (su ETH)
|
||||
df_eth = get_df("ETH", "1h")
|
||||
fa_eth = align_to(F, df_eth)
|
||||
la_ok = check_no_lookahead("ETH", df_eth, fa_eth)
|
||||
print()
|
||||
|
||||
for asset in assets:
|
||||
df = get_df(asset, "1h")
|
||||
fa = align_to(F, df)
|
||||
precomp = _causal_signals(asset, df, fa) # una volta per asset (costoso)
|
||||
for rz in rel_z_grid:
|
||||
for dp in disp_pctl_grid:
|
||||
for mb in mb_grid:
|
||||
for (tp_a, sl_a) in exit_grid:
|
||||
ents = build_entries(asset, df, fa, rz, dp, mb, tp_a, sl_a,
|
||||
precomp=precomp)
|
||||
if len(ents) < 30:
|
||||
continue
|
||||
tag = f"{asset} rz{rz} dp{dp} mb{mb} tp{tp_a} sl{sl_a}"
|
||||
res = evaluate(tag, ents, df)
|
||||
rb = robust(res)
|
||||
# criterio "migliore": OOS ret, poi sharpe; preferisci robuste
|
||||
score = (rb, res["oos"]["ret"], res["full"]["sharpe"])
|
||||
if best is None or score > best[0]:
|
||||
best = (score, tag, res, rb,
|
||||
dict(asset=asset, rz=rz, dp=dp, mb=mb, tp=tp_a, sl=sl_a))
|
||||
|
||||
print("\n=== MIGLIORE ===")
|
||||
if best is None:
|
||||
print("nessuna cella con abbastanza trade")
|
||||
return
|
||||
score, tag, res, rb, cfg = best
|
||||
print(f"config: {tag}")
|
||||
print(f"robust={rb} lookahead_ok={la_ok}")
|
||||
print(f"FULL ret={res['full']['ret']:+.0f}% OOS ret={res['oos']['ret']:+.0f}% "
|
||||
f"DD={res['full']['dd']:.0f}% Sharpe={res['full']['sharpe']:.2f}")
|
||||
print(f"fee0.2% OOS={res['sweep_oos'][0.002]:+.0f}% anniPos={res['pos_yrs']}/{res['n_yrs']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,152 @@
|
||||
"""rel_idio_fade (W=24): fade della componente idiosincratica rel_A vs indice.
|
||||
|
||||
Idea: rel_A = ret(A) - ret(indice EW) e' il rendimento idiosincratico (residuo di
|
||||
mercato). Quando l'asset diverge troppo dall'indice (z-score di rel_A su finestra
|
||||
W=24 elevato), si fada il residuo verso l'indice: se A ha sovraperformato troppo
|
||||
(z alto) -> SHORT A; se ha sottoperformato (z basso) -> LONG A. Mean-reversion del
|
||||
residuo.
|
||||
|
||||
ENTRY CAUSALE: la decisione a close[i] usa SOLO rel_A fino a i incluso. Lo z-score
|
||||
e' costruito con media/std rolling su [i-W+1 .. i] (causale). Ingresso eseguibile a
|
||||
close[i]; exit a tempo (max_bars), opzionale TP/SL ad ATR.
|
||||
|
||||
Esegui: cd /opt/docker/PythagorasGoal && PYTHONPATH=. uv run python \
|
||||
scripts/analysis/_disp_scratch/rel_idio_fade_24.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[3]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.analysis.dispersion_lab import features, align_to, UNIVERSE # noqa: E402
|
||||
from scripts.analysis.explore_lab import get_df, evaluate, robust, atr # noqa: E402
|
||||
|
||||
W = 24 # finestra correlazione/dispersione (richiesta dalla famiglia)
|
||||
ASSETS = ["BTC", "ETH", "ADA", "BNB", "DOGE", "LTC", "SOL", "XRP"]
|
||||
Z_GRID = [1.5, 2.0, 2.5, 3.0]
|
||||
MB_GRID = [12, 24, 48]
|
||||
TP_ATR = None # exit a tempo puro per il primo screening
|
||||
SL_ATR = None
|
||||
|
||||
|
||||
def build_entries(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
|
||||
z_thr: float, max_bars: int,
|
||||
tp_atr=None, sl_atr=None) -> list[dict]:
|
||||
"""Entries CAUSALI per il fade del residuo idiosincratico.
|
||||
|
||||
z[i] = (rel[i] - mean(rel[i-W+1..i])) / std(rel[i-W+1..i]) -> usa solo dati <= i.
|
||||
rel[i] e' gia' causale (deriva da log-ret fino a close[i]). Quando |z[i]|>=thr:
|
||||
z>0 (A ha sovraperformato l'indice) -> SHORT (d=-1), fade verso l'indice
|
||||
z<0 (A ha sottoperformato) -> LONG (d=+1)
|
||||
"""
|
||||
rel = fa[f"rel_{asset}"].values.astype(float)
|
||||
s = pd.Series(rel)
|
||||
mu = s.rolling(W).mean().values
|
||||
sd = s.rolling(W).std(ddof=0).values
|
||||
z = (rel - mu) / np.where(sd > 0, sd, np.nan)
|
||||
|
||||
a = atr(df, 14)
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
entries: list[dict] = []
|
||||
for i in range(W, n - 1):
|
||||
zi = z[i]
|
||||
if not np.isfinite(zi) or abs(zi) < z_thr:
|
||||
continue
|
||||
d = -1 if zi > 0 else 1 # fade del residuo
|
||||
e = {"i": i, "d": d, "max_bars": max_bars}
|
||||
if tp_atr is not None and np.isfinite(a[i]):
|
||||
e["tp"] = c[i] + d * tp_atr * a[i] # TP nella direzione del fade
|
||||
if sl_atr is not None and np.isfinite(a[i]):
|
||||
e["sl"] = c[i] - d * sl_atr * a[i]
|
||||
entries.append(e)
|
||||
return entries
|
||||
|
||||
|
||||
def check_no_lookahead(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
|
||||
z_thr: float, max_bars: int) -> bool:
|
||||
"""Perturba i prezzi DOPO un indice T e verifica che le entries con i<=T non
|
||||
cambino (la entry-rule usa solo dati <= close[i])."""
|
||||
rel = fa[f"rel_{asset}"].values.astype(float)
|
||||
n = len(rel)
|
||||
T = int(n * 0.6)
|
||||
|
||||
def z_of(relv):
|
||||
s = pd.Series(relv)
|
||||
mu = s.rolling(W).mean().values
|
||||
sd = s.rolling(W).std(ddof=0).values
|
||||
return (relv - mu) / np.where(sd > 0, sd, np.nan)
|
||||
|
||||
z0 = z_of(rel)
|
||||
rel2 = rel.copy()
|
||||
rel2[T + 1:] = rel2[T + 1:] + 0.05 # shock del futuro
|
||||
z2 = z_of(rel2)
|
||||
|
||||
def ents_from(z):
|
||||
out = []
|
||||
for i in range(W, n - 1):
|
||||
if i > T:
|
||||
break
|
||||
zi = z[i]
|
||||
if np.isfinite(zi) and abs(zi) >= z_thr:
|
||||
out.append((i, -1 if zi > 0 else 1))
|
||||
return out
|
||||
|
||||
ok = ents_from(z0) == ents_from(z2)
|
||||
print(f" [no-look-ahead {asset}] entries i<=T={T} invarianti al futuro: "
|
||||
f"{'OK' if ok else 'VIOLATO'}")
|
||||
return ok
|
||||
|
||||
|
||||
def main():
|
||||
F = features()
|
||||
print(f"feature caricate: {F.shape[0]} barre, {F.shape[1]} colonne")
|
||||
|
||||
best = None
|
||||
look_ok_all = True
|
||||
for asset in ASSETS:
|
||||
df = get_df(asset, "1h")
|
||||
fa = align_to(F, df)
|
||||
# un check no-look-ahead per asset (config centrale)
|
||||
look_ok_all &= check_no_lookahead(asset, df, fa, z_thr=2.0, max_bars=24)
|
||||
print(f"--- {asset} ---")
|
||||
for z_thr in Z_GRID:
|
||||
for mb in MB_GRID:
|
||||
ents = build_entries(asset, df, fa, z_thr, mb, TP_ATR, SL_ATR)
|
||||
if len(ents) < 30:
|
||||
continue
|
||||
name = f"{asset} z{z_thr} mb{mb}"
|
||||
res = evaluate(name, ents, df)
|
||||
rb = robust(res)
|
||||
score = res["oos"]["ret"] + res["full"]["ret"]
|
||||
cand = {
|
||||
"asset": asset, "z": z_thr, "mb": mb,
|
||||
"full": res["full"]["ret"], "oos": res["oos"]["ret"],
|
||||
"fee02_oos": res["sweep_oos"][0.002],
|
||||
"dd": res["full"]["dd"], "sharpe": res["full"]["sharpe"],
|
||||
"pos_yrs": res["pos_yrs"], "n_yrs": res["n_yrs"],
|
||||
"robust": rb, "score": score, "trades": res["full"]["trades"],
|
||||
}
|
||||
# preferisci robuste; a parita' di robustezza, score piu' alto
|
||||
if best is None or (cand["robust"], cand["score"]) > (best["robust"], best["score"]):
|
||||
best = cand
|
||||
|
||||
print("\n=== CELLA MIGLIORE ===")
|
||||
if best:
|
||||
print(f" asset={best['asset']} z={best['z']} mb={best['mb']} trades={best['trades']}")
|
||||
print(f" FULL={best['full']:+.0f}% OOS={best['oos']:+.0f}% "
|
||||
f"fee0.2%OOS={best['fee02_oos']:+.0f}% DD={best['dd']:.0f}% "
|
||||
f"Sharpe={best['sharpe']:.2f} anniPos={best['pos_yrs']}/{best['n_yrs']} "
|
||||
f"robust={best['robust']}")
|
||||
print(f" no-look-ahead tutti gli asset: {'OK' if look_ok_all else 'VIOLATO'}")
|
||||
return best
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user