research(wave-0702): ondata timing + CRT — 8 filoni, 0 nuovi sleeve, finding anchor timing-luck TP01
Goal: "altre strategie su Deribit con timing differenti". 8 filoni multi-agente + scettico: - event-clock bars, expiry calendar Deribit, clock lenti/bande, regime-speed: SCARTATI - CRT (Candle Range Theory) base/multi-TF/contesto: SCARTATA 3/3 (DSR~0, ritest = informazione negativa; sottoprodotto: FOLLOW>FADE sui livelli prior-day ogni anno, conferma il lead prevday) - FINDING (confermato da scettico indipendente): hold-out 0.31 di TP01 = migliore delle 24 ancore orarie (mediana 0.04, banda [-0.13,+0.30]) -> narrativa corretta in CLAUDE.md e docstring: l'hold-out non risolve l'edge di ritorno, regge il taglio DD a ogni ancora. Tranching K=2/4 = solo varianza della stima, no deploy a $600. Audit d'ancora pendente su XS01/SKH01. Book live e portafoglio INVARIATI. Test 168/168. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
"""r0702_crt_base — CRT "Candle Range Theory", versione BASE single-TF (pattern meccanizzato).
|
||||
|
||||
FILONE 2026-07-02. Falso breakout codificato in 3 candele (turtle soup / spring-upthrust):
|
||||
C1 (range): candela direzionale forte -> body/range >= b AND range >= k*ATR14 (griglia b,k)
|
||||
C2 (manipolazione): rompe un estremo di C1 di almeno s*ATR14 (griglia s) ma CHIUDE DENTRO
|
||||
il range di C1. Flag colore opzionale (short: C2 rossa che apre sopra il close di C1;
|
||||
long: C2 verde che apre sotto il close di C1).
|
||||
C3 (ingresso): entry a open C3 = close C2 (deciso con dati <= close C2 -> causale).
|
||||
SL = estremo di C2 (punto dello sweep). TP = estremo OPPOSTO del range di C1.
|
||||
Filtro R:R >= 1.3 a entry. Direzioni: short su sweep dell'alto, long su sweep del basso.
|
||||
|
||||
OVERLAP DICHIARATO con la ricerca esistente (grep dei docstring runs/MRV*.py + MIC07):
|
||||
- MRV01-11 = mean-reversion su INDICATORI (RSI2, BB, z-score, IBS, W%R, consec-down,
|
||||
gap-fill, CCI, stochastic, VWAP-dev, %b) — nessuna testa il pattern 3-candele
|
||||
sweep+close-back-inside con SL/TP strutturali. La famiglia MR generica e' MORTA sul
|
||||
feed certificato: CRT e' una MR *condizionata da un evento di liquidita'*, quindi il
|
||||
prior e' fortemente negativo — serve battere il null del fade incondizionato.
|
||||
- MIC07 (pin-bar rejection al supporto) e' il parente piu' vicino: rejection candle
|
||||
single-bar a un N-bar low. CRT differisce: riferimento = range di C1 forte (1 barra),
|
||||
sweep quantificato in ATR, close-back-inside esplicito, TP strutturale (estremo opposto
|
||||
di C1) e non R-multiple. Overlap concettuale parziale, meccanica diversa.
|
||||
|
||||
GATES: selezione cella SOLO in-sample pre-2025; deflated Sharpe su TUTTI i trial
|
||||
(cella x tf x direzione); ANCHOR-SHIFT (+1/+2/+4h) sul resample 4h/12h/1d; fee sweep
|
||||
0.00-0.20% RT; marginal_vs_tp01 se Sharpe standalone >= 0.5.
|
||||
NULL decisivi: (i) fade INCONDIZIONATO dello stesso estremo (senza close-back-inside);
|
||||
(ii) condizione INVERTITA (C2 chiude FUORI = breakout confermato, trade col breakout).
|
||||
|
||||
Motore trade-level CONSERVATIVO (specchia src/backtest/harness.backtest_signals):
|
||||
entry a close[i]; exit scan da i+1; SL/TP fillati AL LIVELLO su high/low; se nella stessa
|
||||
barra sono toccati entrambi scatta PRIMA lo STOP (worst-case); time-exit a close dopo
|
||||
max_hold barre; nessun overlap (una posizione alla volta per asset). Fee 0.10% RT.
|
||||
Equity mark-to-market per barra (lente Sharpe daily-compounded, convenzione progetto).
|
||||
|
||||
Run: uv run python scripts/research/r0702_crt_base.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from itertools import product
|
||||
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
||||
import altlib as al # noqa: E402
|
||||
|
||||
import numpy as np # noqa: E402
|
||||
import pandas as pd # noqa: E402
|
||||
|
||||
HOLDOUT = al.HOLDOUT # 2025-01-01 UTC
|
||||
FEE_RT = 2 * al.FEE_SIDE # 0.10% round-trip
|
||||
RR_MIN = 1.3
|
||||
TFS = ("1h", "4h", "12h", "1d")
|
||||
RULES = {"4h": "4h", "12h": "12h", "1d": "1D"}
|
||||
ASSETS = al.CERTIFIED # BTC, ETH
|
||||
MIN_IS_TRADES = 25 # trade combinati minimi in-sample per cella eleggibile
|
||||
|
||||
GRID = [dict(b=b, k=k, s=s, color=col, mh=mh)
|
||||
for b, k, s, col, mh in product((0.5, 0.7), (1.0, 1.5),
|
||||
(0.0, 0.1, 0.25), (False, True), (5, 10, 20))]
|
||||
DIRS = ("long", "short", "both")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# DATI (anchor 00:00 UTC di default; anchor spostabile per il gate anchor-shift)
|
||||
# ===========================================================================
|
||||
def resample_anchor(df_1h: pd.DataFrame, rule: str, offset_hours: int) -> pd.DataFrame:
|
||||
"""Come trend_portfolio.resample_tf (label/closed='left') ma con ancora spostata di
|
||||
+offset_hours. Niente .view('int64'): epoca esplicita via // Timedelta (tz-aware safe)."""
|
||||
g = df_1h.copy()
|
||||
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
|
||||
idx.name = "dt"
|
||||
g.index = idx
|
||||
out = g.resample(rule, label="left", closed="left",
|
||||
offset=pd.Timedelta(hours=offset_hours)).agg(
|
||||
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
|
||||
out = out.dropna(subset=["open"])
|
||||
out["datetime"] = out.index
|
||||
epoch = pd.Timestamp("1970-01-01", tz="UTC")
|
||||
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
|
||||
return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close",
|
||||
"volume", "datetime"]]
|
||||
|
||||
|
||||
_PREP_CACHE: dict = {}
|
||||
|
||||
|
||||
def prep(asset: str, tf: str, anchor: int = 0) -> dict:
|
||||
key = (asset, tf, anchor)
|
||||
if key in _PREP_CACHE:
|
||||
return _PREP_CACHE[key]
|
||||
if anchor == 0 or tf == "1h":
|
||||
df = al.get(asset, tf)
|
||||
else:
|
||||
df = resample_anchor(al.get(asset, "1h"), RULES[tf], anchor)
|
||||
d = dict(
|
||||
df=df,
|
||||
o=df["open"].values.astype(float), h=df["high"].values.astype(float),
|
||||
l=df["low"].values.astype(float), c=df["close"].values.astype(float),
|
||||
atr=al.atr(df, 14),
|
||||
idx=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)),
|
||||
)
|
||||
_PREP_CACHE[key] = d
|
||||
return d
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# DETECTION (vettoriale, causale: tutto deciso con OHLC fino alla barra i = C2;
|
||||
# l'ATR usato e' quello di C1 (i-1) -> ancora piu' conservativo)
|
||||
# ===========================================================================
|
||||
def _shift1(x: np.ndarray) -> np.ndarray:
|
||||
out = np.empty_like(x); out[0] = np.nan; out[1:] = x[:-1]
|
||||
return out
|
||||
|
||||
|
||||
def detect(d: dict, b: float, k: float, s: float, color: bool, variant: str) -> dict:
|
||||
"""Ritorna {dir: (indici C2, sl, tp)} per variant in {'crt','fade','breakout'}.
|
||||
Indice i = barra C2; C1 = i-1. Entry (gestita dal motore) = close[i]."""
|
||||
o, h, l, c = d["o"], d["h"], d["l"], d["c"]
|
||||
h1, l1, o1, c1 = _shift1(h), _shift1(l), _shift1(o), _shift1(c)
|
||||
atr1 = _shift1(d["atr"])
|
||||
rng1 = h1 - l1
|
||||
body1 = np.abs(c1 - o1)
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
strong = (np.isfinite(atr1) & (atr1 > 0) & (rng1 > 0)
|
||||
& (body1 / rng1 >= b) & (rng1 >= k * atr1))
|
||||
sweep_up = strong & (h > h1 + s * atr1) # C2 rompe l'alto di C1
|
||||
sweep_dn = strong & (l < l1 - s * atr1) # C2 rompe il basso di C1
|
||||
out = {}
|
||||
if variant == "crt":
|
||||
sh = sweep_up & (c < h1) & (c > l1) # chiude DENTRO il range di C1
|
||||
lg = sweep_dn & (c > l1) & (c < h1)
|
||||
if color:
|
||||
sh &= (c < o) & (o > c1) # rossa che apre sopra il close di C1
|
||||
lg &= (c > o) & (o < c1) # verde che apre sotto il close di C1
|
||||
sh &= (h > c) & ((c - l1) / np.where(h - c > 0, h - c, np.nan) >= RR_MIN)
|
||||
lg &= (c > l) & ((h1 - c) / np.where(c - l > 0, c - l, np.nan) >= RR_MIN)
|
||||
out["short"] = (np.where(sh)[0], h, l1) # SL=high C2, TP=low C1
|
||||
out["long"] = (np.where(lg)[0], l, h1) # SL=low C2, TP=high C1
|
||||
elif variant == "fade":
|
||||
# NULL (i): stesso sweep, NESSUNA richiesta di close-back-inside (no colore).
|
||||
# Solo validita' geometrica (TP dal lato giusto dell'entry).
|
||||
sh = sweep_up & (c > l1)
|
||||
lg = sweep_dn & (c < h1)
|
||||
sh &= (h > c) & ((c - l1) / np.where(h - c > 0, h - c, np.nan) >= RR_MIN)
|
||||
lg &= (c > l) & ((h1 - c) / np.where(c - l > 0, c - l, np.nan) >= RR_MIN)
|
||||
out["short"] = (np.where(sh)[0], h, l1)
|
||||
out["long"] = (np.where(lg)[0], l, h1)
|
||||
elif variant == "breakout":
|
||||
# NULL (ii): condizione INVERTITA — C2 chiude FUORI dal range di C1 =
|
||||
# breakout confermato, trade IN DIREZIONE del breakout.
|
||||
# SL = livello rotto (rientro nel range = fallimento), TP = measured move
|
||||
# (range di C1 proiettato oltre il livello). Stesso filtro R:R.
|
||||
lg = sweep_up & (c > h1) # rompe l'alto e chiude sopra -> LONG
|
||||
sh = sweep_dn & (c < l1) # rompe il basso e chiude sotto -> SHORT
|
||||
tp_lg = h1 + rng1
|
||||
tp_sh = l1 - rng1
|
||||
lg &= (tp_lg > c) & ((tp_lg - c) / np.where(c - h1 > 0, c - h1, np.nan) >= RR_MIN)
|
||||
sh &= (c > tp_sh) & ((c - tp_sh) / np.where(l1 - c > 0, l1 - c, np.nan) >= RR_MIN)
|
||||
out["long"] = (np.where(lg)[0], h1, tp_lg) # SL=high C1, TP=measured move
|
||||
out["short"] = (np.where(sh)[0], l1, tp_sh) # SL=low C1
|
||||
else:
|
||||
raise ValueError(variant)
|
||||
return out
|
||||
|
||||
|
||||
def merge_dirs(sig: dict, which: str):
|
||||
"""Lista ordinata di (i, dir, sl, tp) per direzione 'long'/'short'/'both'."""
|
||||
rows = []
|
||||
if which in ("long", "both"):
|
||||
ii, sl, tp = sig["long"]
|
||||
rows += [(int(i), 1, float(sl[i]), float(tp[i])) for i in ii]
|
||||
if which in ("short", "both"):
|
||||
ii, sl, tp = sig["short"]
|
||||
rows += [(int(i), -1, float(sl[i]), float(tp[i])) for i in ii]
|
||||
rows.sort(key=lambda r: r[0])
|
||||
return rows
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# MOTORE TRADE-LEVEL (conservativo; specchia backtest_signals: SL prioritario)
|
||||
# ===========================================================================
|
||||
def run_trades(d: dict, rows: list, mh: int, fee_rt: float = FEE_RT):
|
||||
"""Ritorna (trades, barnet). trades: (i_entry, i_exit, dir, net, R, gross).
|
||||
barnet: rendimento netto per-barra mark-to-market (fee 50/50 su entry/exit bar)."""
|
||||
c, h, l = d["c"], d["h"], d["l"]
|
||||
n = len(c)
|
||||
barnet = np.zeros(n)
|
||||
trades = []
|
||||
busy_until = -1
|
||||
for i, dr, sl, tp in rows:
|
||||
if i <= busy_until or i >= n - 1:
|
||||
continue
|
||||
entry = c[i]
|
||||
exit_idx = min(i + mh, n - 1)
|
||||
exit_price = c[exit_idx]
|
||||
for j in range(i + 1, min(i + mh, n - 1) + 1):
|
||||
if dr == 1:
|
||||
if l[j] <= sl: # STOP prima (worst-case)
|
||||
exit_price, exit_idx = sl, j; break
|
||||
if h[j] >= tp:
|
||||
exit_price, exit_idx = tp, j; break
|
||||
else:
|
||||
if h[j] >= sl:
|
||||
exit_price, exit_idx = sl, j; break
|
||||
if l[j] <= tp:
|
||||
exit_price, exit_idx = tp, j; break
|
||||
exit_price, exit_idx = c[j], j
|
||||
gross = dr * (exit_price / entry - 1.0)
|
||||
net = gross - fee_rt
|
||||
risk = abs(sl - entry) / entry
|
||||
R = net / risk if risk > 0 else np.nan
|
||||
trades.append((i, exit_idx, dr, net, R, gross))
|
||||
pp = entry
|
||||
for j in range(i + 1, exit_idx + 1):
|
||||
pj = exit_price if j == exit_idx else c[j]
|
||||
barnet[j] += dr * (pj / pp - 1.0)
|
||||
pp = pj
|
||||
barnet[i] -= fee_rt / 2
|
||||
barnet[exit_idx] -= fee_rt / 2
|
||||
busy_until = exit_idx
|
||||
return trades, barnet
|
||||
|
||||
|
||||
def daily_series(d: dict, barnet: np.ndarray) -> pd.Series:
|
||||
return al._to_daily(pd.Series(barnet, index=d["idx"]))
|
||||
|
||||
|
||||
def combo_daily(dailies: dict) -> pd.Series:
|
||||
J = pd.concat(dailies, axis=1, join="inner").fillna(0.0)
|
||||
return 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]
|
||||
|
||||
|
||||
def series_metrics(daily: pd.Series) -> dict:
|
||||
def _dd(s):
|
||||
eq = np.cumprod(1.0 + s.values); pk = np.maximum.accumulate(eq)
|
||||
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
|
||||
ins, hold = daily[daily.index < HOLDOUT], daily[daily.index >= HOLDOUT]
|
||||
yearly = {int(y): round(float(np.prod(1 + g.values) - 1), 4)
|
||||
for y, g in daily.groupby(daily.index.year)}
|
||||
return dict(full_sh=round(al._sh(daily), 3), is_sh=round(al._sh(ins), 3),
|
||||
hold_sh=round(al._sh(hold), 3), full_dd=round(_dd(daily), 4),
|
||||
hold_dd=round(_dd(hold), 4), full_ret=round(float(np.prod(1 + daily.values) - 1), 4),
|
||||
hold_ret=round(float(np.prod(1 + hold.values) - 1), 4), yearly=yearly)
|
||||
|
||||
|
||||
def trade_stats(trades: list, idx: pd.DatetimeIndex) -> dict:
|
||||
if not trades:
|
||||
return dict(n=0, n_is=0, n_hold=0, wr=None, avg_R=None, exp_net=None)
|
||||
t_entry = idx[[t[0] for t in trades]]
|
||||
net = np.array([t[3] for t in trades]); R = np.array([t[4] for t in trades])
|
||||
is_m = np.asarray(t_entry < HOLDOUT)
|
||||
|
||||
def _blk(m):
|
||||
if m.sum() == 0:
|
||||
return dict(n=0, wr=None, avg_R=None, exp_net=None)
|
||||
return dict(n=int(m.sum()), wr=round(float(np.mean(net[m] > 0) * 100), 1),
|
||||
avg_R=round(float(np.nanmean(R[m])), 3),
|
||||
exp_net=round(float(np.mean(net[m]) * 100), 3))
|
||||
full = _blk(np.ones(len(net), bool))
|
||||
per_year = {}
|
||||
for y in sorted(set(t_entry.year)):
|
||||
per_year[int(y)] = _blk(np.asarray(t_entry.year == y))
|
||||
return dict(n=full["n"], n_is=int(is_m.sum()), n_hold=int((~is_m).sum()),
|
||||
wr=full["wr"], avg_R=full["avg_R"], exp_net=full["exp_net"],
|
||||
is_blk=_blk(is_m), hold_blk=_blk(~is_m), per_year=per_year)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# RUNNER di un trial (cella x tf x direzione) su entrambi gli asset
|
||||
# ===========================================================================
|
||||
def run_trial(tf: str, p: dict, which: str, variant: str = "crt",
|
||||
fee_rt: float = FEE_RT, anchor: int = 0):
|
||||
dailies, all_trades, all_stats = {}, {}, {}
|
||||
for a in ASSETS:
|
||||
d = prep(a, tf, anchor)
|
||||
sig = detect(d, p["b"], p["k"], p["s"], p["color"], variant)
|
||||
rows = merge_dirs(sig, which)
|
||||
trades, barnet = run_trades(d, rows, p["mh"], fee_rt)
|
||||
dailies[a] = daily_series(d, barnet)
|
||||
all_trades[a] = trades
|
||||
all_stats[a] = trade_stats(trades, d["idx"])
|
||||
daily = combo_daily(dailies)
|
||||
sm = series_metrics(daily)
|
||||
n_is = sum(st["n_is"] for st in all_stats.values())
|
||||
n_full = sum(st["n"] for st in all_stats.values())
|
||||
return dict(tf=tf, params=p, dir=which, variant=variant, daily=daily,
|
||||
metrics=sm, per_asset_stats=all_stats, n_is=n_is, n_full=n_full)
|
||||
|
||||
|
||||
def pooled_trade_stats(trial: dict) -> dict:
|
||||
"""Statistiche trade POOLED sui due asset (per il report della cella scelta)."""
|
||||
trades, idxs = [], []
|
||||
for a in ASSETS:
|
||||
d = prep(a, trial["tf"])
|
||||
for t in trial["_raw_trades"][a]:
|
||||
trades.append(t); idxs.append(d["idx"][t[0]])
|
||||
if not trades:
|
||||
return dict(n=0)
|
||||
order = np.argsort(np.array([i.value for i in idxs]))
|
||||
trades = [trades[k] for k in order]
|
||||
idx = pd.DatetimeIndex([idxs[k] for k in order])
|
||||
return _pooled(trades, idx)
|
||||
|
||||
|
||||
def _pooled(trades, idx):
|
||||
net = np.array([t[3] for t in trades]); R = np.array([t[4] for t in trades])
|
||||
is_m = np.asarray(idx < HOLDOUT)
|
||||
|
||||
def _blk(m):
|
||||
if m.sum() == 0:
|
||||
return dict(n=0, wr=None, avg_R=None, exp_net=None)
|
||||
return dict(n=int(m.sum()), wr=round(float(np.mean(net[m] > 0) * 100), 1),
|
||||
avg_R=round(float(np.nanmean(R[m])), 3),
|
||||
exp_net=round(float(np.mean(net[m]) * 100), 3))
|
||||
out = dict(full=_blk(np.ones(len(net), bool)), is_blk=_blk(is_m), hold_blk=_blk(~is_m),
|
||||
per_year={int(y): _blk(np.asarray(idx.year == y)) for y in sorted(set(idx.year))})
|
||||
return out
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# MAIN
|
||||
# ===========================================================================
|
||||
def main():
|
||||
t0 = time.time()
|
||||
print("=" * 96)
|
||||
print("r0702 CRT — Candle Range Theory BASE single-TF | fee 0.10% RT | hold-out 2025-01-01+")
|
||||
print("Griglia: b(0.5,0.7) x k(1.0,1.5) x s(0.0,0.1,0.25) x color(off,on) x max_hold(5,10,20)")
|
||||
print(f"= {len(GRID)} celle x {len(TFS)} TF x {len(DIRS)} direzioni = "
|
||||
f"{len(GRID) * len(TFS) * len(DIRS)} trial (tutti contati nel DSR)")
|
||||
print("=" * 96)
|
||||
|
||||
for a in ASSETS:
|
||||
d = prep(a, "1d")
|
||||
print(f" dati {a} 1d: {d['idx'][0].date()} -> {d['idx'][-1].date()} ({len(d['c'])} barre)")
|
||||
|
||||
# ---- 1) griglia completa (righe leggere; il daily si ricalcola per la scelta) ----
|
||||
rows = []
|
||||
freq_by_tf = {tf: [] for tf in TFS}
|
||||
for tf in TFS:
|
||||
years = {}
|
||||
for a in ASSETS:
|
||||
d = prep(a, tf)
|
||||
years[a] = (d["idx"][-1] - d["idx"][0]).total_seconds() / 86400 / 365.25
|
||||
span_y = float(np.mean(list(years.values())))
|
||||
for p in GRID:
|
||||
# detection condivisa fra direzioni e mh (mh influenza solo il motore)
|
||||
for which in DIRS:
|
||||
tr = run_trial(tf, p, which)
|
||||
m = tr["metrics"]
|
||||
rows.append(dict(tf=tf, **p, dir=which, is_sh=m["is_sh"], full_sh=m["full_sh"],
|
||||
hold_sh=m["hold_sh"], n_is=tr["n_is"], n_full=tr["n_full"]))
|
||||
if which == "both":
|
||||
freq_by_tf[tf].append(tr["n_full"] / (2 * span_y)) # trade/anno per asset
|
||||
print(f" [grid] tf={tf} fatto ({time.time() - t0:.0f}s)")
|
||||
|
||||
R = pd.DataFrame(rows)
|
||||
print("\n--- FREQUENZA PATTERN (CRT, entrambe le direzioni, trade/anno PER ASSET) ---")
|
||||
for tf in TFS:
|
||||
f = np.array(freq_by_tf[tf])
|
||||
print(f" {tf:>4s}: mediana {np.median(f):6.1f} min {f.min():6.1f} max {f.max():6.1f} "
|
||||
f"(su {len(f)} celle)")
|
||||
|
||||
# ---- 2) selezione cella SOLO in-sample (pre-2025) ----
|
||||
elig = R[(R.n_is >= MIN_IS_TRADES) & np.isfinite(R.is_sh)].copy()
|
||||
print(f"\n--- SELEZIONE IN-SAMPLE: {len(elig)}/{len(R)} trial eleggibili "
|
||||
f"(>= {MIN_IS_TRADES} trade IS combinati) ---")
|
||||
top = elig.sort_values("is_sh", ascending=False).head(12)
|
||||
cols = ["tf", "b", "k", "s", "color", "mh", "dir", "is_sh", "hold_sh", "full_sh", "n_is", "n_full"]
|
||||
print(top[cols].to_string(index=False))
|
||||
|
||||
if len(elig) == 0:
|
||||
print("\nVERDETTO: FAIL — nessuna cella con abbastanza trade in-sample.")
|
||||
return
|
||||
|
||||
best = elig.sort_values("is_sh", ascending=False).iloc[0]
|
||||
p = dict(b=float(best.b), k=float(best.k), s=float(best.s),
|
||||
color=bool(best.color), mh=int(best.mh))
|
||||
tf, which = str(best.tf), str(best.dir)
|
||||
print(f"\n=== CELLA SCELTA (max Sharpe IN-SAMPLE, hold-out solo riportato) ===")
|
||||
print(f" tf={tf} dir={which} {p}")
|
||||
|
||||
# ricalcolo completo della cella scelta (con trade grezzi per il pooled report)
|
||||
chosen = run_trial(tf, p, which)
|
||||
chosen["_raw_trades"] = {}
|
||||
for a in ASSETS:
|
||||
d = prep(a, tf)
|
||||
sig = detect(d, p["b"], p["k"], p["s"], p["color"], "crt")
|
||||
trades, _ = run_trades(d, merge_dirs(sig, which), p["mh"])
|
||||
chosen["_raw_trades"][a] = trades
|
||||
m = chosen["metrics"]
|
||||
print(f" COMBINED 50/50: FULL Sh {m['full_sh']} IS Sh {m['is_sh']} HOLD Sh {m['hold_sh']} "
|
||||
f"| FULL ret {m['full_ret'] * 100:+.1f}% DD {m['full_dd'] * 100:.1f}% "
|
||||
f"| HOLD ret {m['hold_ret'] * 100:+.1f}% DD {m['hold_dd'] * 100:.1f}%")
|
||||
print(f" per-anno (ret combo): " + " ".join(f"{y}:{v * 100:+.1f}%" for y, v in m["yearly"].items()))
|
||||
ps = pooled_trade_stats(chosen)
|
||||
if ps.get("full", {}).get("n", 0) > 0:
|
||||
f_, i_, h_ = ps["full"], ps["is_blk"], ps["hold_blk"]
|
||||
print(f" trade POOLED: n={f_['n']} WR={f_['wr']}% avgR={f_['avg_R']} exp={f_['exp_net']}%"
|
||||
f" | IS n={i_['n']} WR={i_['wr']}% avgR={i_['avg_R']} exp={i_['exp_net']}%"
|
||||
f" | HOLD n={h_['n']} WR={h_['wr']}% avgR={h_['avg_R']} exp={h_['exp_net']}%")
|
||||
print(" trade per anno: " + " ".join(
|
||||
f"{y}:n{b['n']}/wr{b['wr']}/exp{b['exp_net']}%" for y, b in ps["per_year"].items()))
|
||||
for a in ASSETS:
|
||||
st = chosen["per_asset_stats"][a]
|
||||
print(f" {a}: n={st['n']} (IS {st['n_is']}/HOLD {st['n_hold']}) WR={st['wr']}% "
|
||||
f"avgR={st['avg_R']} exp={st['exp_net']}%")
|
||||
|
||||
# per-direzione della cella scelta (stessi parametri)
|
||||
print("\n--- CELLA SCELTA per DIREZIONE (stessi parametri) ---")
|
||||
for wdir in DIRS:
|
||||
tr = run_trial(tf, p, wdir)
|
||||
mm = tr["metrics"]
|
||||
print(f" {wdir:>5s}: IS {mm['is_sh']:+.2f} HOLD {mm['hold_sh']:+.2f} FULL {mm['full_sh']:+.2f} "
|
||||
f"n={tr['n_full']} (IS {tr['n_is']})")
|
||||
|
||||
# ---- sanity cross-check vs harness ufficiale (al.eval_signals) ----
|
||||
print("\n--- CROSS-CHECK vs al.eval_signals (harness ufficiale, stessa convenzione) ---")
|
||||
for a in ASSETS:
|
||||
d = prep(a, tf)
|
||||
sig = detect(d, p["b"], p["k"], p["s"], p["color"], "crt")
|
||||
entries = [None] * len(d["c"])
|
||||
for i, dr, sl, tp in merge_dirs(sig, which):
|
||||
entries[i] = dict(dir=dr, sl=sl, tp=tp, max_bars=p["mh"])
|
||||
ev = al.eval_signals(d["df"], entries, fee_rt=FEE_RT, asset=a, tf=tf)
|
||||
mine = chosen["_raw_trades"][a]
|
||||
my_ret = float(np.prod([1 + t[3] for t in mine]) - 1)
|
||||
print(f" {a}: harness n={ev['n_trades']} ret={ev['full']['ret'] * 100:+.1f}% "
|
||||
f"| mio n={len(mine)} ret={my_ret * 100:+.1f}% "
|
||||
f"{'OK' if ev['n_trades'] == len(mine) else 'MISMATCH!'}")
|
||||
|
||||
# ---- 3) DSR su TUTTI i trial ----
|
||||
all_sr = [r["full_sh"] for r in rows if np.isfinite(r["full_sh"]) and r["n_full"] >= 1]
|
||||
dsr, sr0 = al.deflated_sharpe(m["full_sh"], all_sr, chosen["daily"])
|
||||
print(f"\n--- DEFLATED SHARPE: DSR={dsr:.3f} (PASS>=0.95) expected-null-max Sh={sr0:.2f} "
|
||||
f"trial contati={len(all_sr)} (su {len(rows)} totali; esclusi 0-trade) ---")
|
||||
|
||||
# ---- 4) ANCHOR-SHIFT (+1/+2/+4h) ----
|
||||
print("\n--- ANCHOR-SHIFT (ancora resample spostata; pattern vero ~invariante) ---")
|
||||
anchor_rows = {}
|
||||
if tf == "1h":
|
||||
print(" tf=1h nativo: nessuna dipendenza dall'ancora del resample (N/A).")
|
||||
alt = elig[elig.tf != "1h"].sort_values("is_sh", ascending=False)
|
||||
if len(alt):
|
||||
b2 = alt.iloc[0]
|
||||
p2 = dict(b=float(b2.b), k=float(b2.k), s=float(b2.s), color=bool(b2.color), mh=int(b2.mh))
|
||||
print(f" (test eseguito sulla miglior cella IS a tf>=4h: tf={b2.tf} dir={b2.dir} {p2})")
|
||||
for off in (0, 1, 2, 4):
|
||||
tr = run_trial(str(b2.tf), p2, str(b2.dir), anchor=off)
|
||||
mm = tr["metrics"]
|
||||
anchor_rows[off] = mm
|
||||
print(f" anchor +{off}h: IS {mm['is_sh']:+.2f} HOLD {mm['hold_sh']:+.2f} "
|
||||
f"FULL {mm['full_sh']:+.2f} n={tr['n_full']}")
|
||||
else:
|
||||
for off in (0, 1, 2, 4):
|
||||
tr = run_trial(tf, p, which, anchor=off)
|
||||
mm = tr["metrics"]
|
||||
anchor_rows[off] = mm
|
||||
print(f" anchor +{off}h: IS {mm['is_sh']:+.2f} HOLD {mm['hold_sh']:+.2f} "
|
||||
f"FULL {mm['full_sh']:+.2f} n={tr['n_full']}")
|
||||
if anchor_rows:
|
||||
fulls = [v["full_sh"] for v in anchor_rows.values()]
|
||||
flip = (max(fulls) > 0) and (min(fulls) < 0)
|
||||
print(f" spread FULL Sh = {max(fulls) - min(fulls):+.2f} "
|
||||
f"{'SIGN-FLIP -> ARTIFACT-RISK' if flip else 'nessun sign-flip'}")
|
||||
|
||||
# ---- 5) FEE SWEEP 0.00-0.20% RT ----
|
||||
print("\n--- FEE SWEEP (cella scelta) ---")
|
||||
for fr in (0.0, 0.0005, 0.001, 0.0015, 0.002):
|
||||
tr = run_trial(tf, p, which, fee_rt=fr)
|
||||
mm = tr["metrics"]
|
||||
print(f" fee {fr * 100:.2f}%RT: IS {mm['is_sh']:+.2f} HOLD {mm['hold_sh']:+.2f} "
|
||||
f"FULL {mm['full_sh']:+.2f}")
|
||||
|
||||
# ---- 6) NULL DECISIVI ----
|
||||
print("\n--- NULL (i): FADE INCONDIZIONATO dello stesso estremo (no close-back-inside) ---")
|
||||
p_null = dict(p, color=False)
|
||||
for var, lbl in (("fade", "fade-incond"), ("breakout", "breakout-conf")):
|
||||
tr = run_trial(tf, p_null, which, variant=var)
|
||||
mm = tr["metrics"]
|
||||
print(f" {lbl:>14s}: IS {mm['is_sh']:+.2f} HOLD {mm['hold_sh']:+.2f} FULL {mm['full_sh']:+.2f} "
|
||||
f"n={tr['n_full']} (IS {tr['n_is']}) per-anno " +
|
||||
" ".join(f"{y}:{v * 100:+.0f}%" for y, v in mm["yearly"].items()))
|
||||
tr = run_trial(tf, p, which, variant="crt")
|
||||
mm = tr["metrics"]
|
||||
print(f" {'CRT (rif.)':>14s}: IS {mm['is_sh']:+.2f} HOLD {mm['hold_sh']:+.2f} FULL {mm['full_sh']:+.2f} "
|
||||
f"n={tr['n_full']} (IS {tr['n_is']})")
|
||||
|
||||
# ---- 7) MARGINAL vs TP01 (solo se standalone >= 0.5) ----
|
||||
if max(m["full_sh"], m["is_sh"]) >= 0.5:
|
||||
print("\n--- MARGINAL vs TP01 (standalone >= 0.5) ---")
|
||||
marg = al.marginal_vs_tp01(chosen["daily"])
|
||||
keys = ("marginal_verdict", "corr_full", "corr_hold", "cand_insample_sharpe",
|
||||
"has_insample_edge", "is_hedge", "multicut_uplift", "multicut_persistent",
|
||||
"robust_oos", "beta_to_tp01", "resid_sharpe_full")
|
||||
for kk in keys:
|
||||
print(f" {kk}: {marg.get(kk)}")
|
||||
for w, dd in marg.get("blends", {}).items():
|
||||
print(f" blend {w}: full {dd['full']} (uplift {dd['uplift_full']:+.3f}) "
|
||||
f"hold {dd['hold']} (uplift {dd['uplift_hold']})")
|
||||
else:
|
||||
print(f"\n--- MARGINAL vs TP01: SALTATO (standalone FULL {m['full_sh']} / IS {m['is_sh']} < 0.5) ---")
|
||||
|
||||
print(f"\n[done in {time.time() - t0:.0f}s]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user