"""r0702_ell_fibconfluence.py — FIBONACCI CONFLUENCE: il prezzo reagisce ai livelli Fib piu' che a livelli QUALSIASI? (2026-07-02, filone ELL-B) CLAIM (video didattico, Casario): proiettando su swing di prezzo griglie di ritracciamento (0.382/0.5/0.618) ed estensione (1.272/1.414/1.618/2.618) di Fibonacci, i livelli — e in particolare le CONFLUENZE dove due griglie convergono — sarebbero zone di reazione del prezzo. Claim testabile: la reazione ai livelli Fib batte quella a livelli con rapporti CASUALI proiettati sugli STESSI swing? IL NULL E' TUTTO: senza placebo, "il prezzo ha reagito al 61.8%" e' solo apofenia (qualunque livello dentro il range viene "toccato e rispettato" a volte). METODO (tutto a priori, dichiarato PRIMA di guardare i numeri): * Swing MECCANICI e causali: zigzag a soglia k*ATR14 (k in {2,4}), NON-REPAINTING — un pivot e' confermato solo quando close si e' mosso k*ATR dall'estremo; i livelli dello swing (A->B) sono utilizzabili solo da confirm_idx+1 (timestamp di conferma esplicito). * Griglia attiva per finestra [confirm(B)+1, confirm(pivot successivo)]: ritracciamenti L = pB - r*M e estensioni L = pA + e*M (M = pB-pA). Finestre disgiunte per costruzione. * TOCCO = low<=L<=high con la barra precedente che NON conteneva L (fresh touch); direzione attesa dal lato di approccio (close[i-1]>L -> supporto -> atteso rimbalzo SU; giu'). Decisione a close[i] (high/low noti a fine barra), reazione misurata da close[i] a close[i+H], H in {5,20} barre — ESEGUIBILE (niente entry sugli estremi). La variante "fill al livello" (da L invece che da close) e' riportata SOLO come lens ottimista. * Statistica primaria: media della reazione segnata NORMALIZZATA per ATR (react/(ATR/close)) sui tocchi IN-SAMPLE (pre-2025); raw bps riportati. Il drift da buy-the-dip e' identico per Fib e placebo -> il percentile vs placebo lo neutralizza. * NULL (a): 100 set di rapporti PLACEBO fissi — 3 ritracciamenti uniformi in [0.2,0.9] + 4 estensioni uniformi in [1.05,3.0], esclusa banda max(0.02, 2%rel) attorno ai Fib veri — proiettati sugli STESSI swing. NULL (b): 100 repliche con rapporti casuali RI-SORTEGGIATI per ogni swing (stessa densita', nessuna coerenza cross-swing). NULL (c) — AGGIUNTO dopo il run 1 come indurimento dichiarato: 100 set LOCATION-MATCHED (ogni rapporto = Fib vero +/- jitter appena fuori la banda esclusa, entro ~6-8%) — il null uniforme (a) non e' density-matched (la reazione media dipende da DOVE sta il rapporto, non solo da quale numero e'), quindi (c) e' il test affilato di "0.618 e' speciale vs 0.58/0.66". Verdetto = percentile del Fib vero nelle tre distribuzioni placebo. * CONFLUENZA: ritracciamenti dello swing corrente x estensioni dello swing s-2 (stesso verso, il precedente ha verso opposto e proietta sempre FUORI dal range corrente — bug geometrico del run 1, corretto e dichiarato) entro eps=0.25*ATR(w0); reazione in zona confluente vs livelli singoli delle stesse griglie, stesso placebo. * MULTIPLE TESTING: 16 celle (2 asset x 2 TF x 2 k x 2 H). Soglia single-cell Bonferroni ~0.997; verdetto famiglia (a priori): esiste (k,H) con percentile POOLED IS >= 0.99 su ENTRAMBI i null E tutte le 4 celle asset x TF a pctl >= 0.90. Solo allora si strategizza (fade ai livelli, fee 0.10% RT) con al.study_family_honest + al.study_marginal. * Hold-out 2025-26 MAI usato per selezionare (qui non si seleziona nulla: rapporti fissati dal claim; k/H riportati per cella, il verdetto e' sul pooled IS). CAUSALITA': pivot ricalcolati su prefisso troncato == pivot full con confirm list: """Pivot alternati H/L: (pivot_idx, price, type +1=high/-1=low, confirm_idx). Un pivot e' confermato quando close si muove k*ATR14[i] oltre l'estremo corrente. Causale: usa solo dati <= i; il pivot NON viene mai spostato dopo la conferma.""" h = df["high"].values.astype(float) l = df["low"].values.astype(float) c = df["close"].values.astype(float) a = al.atr(df, atr_win) n = len(c) piv: list = [] start = atr_win if n <= start + 2: return piv ehi, ehi_i = h[start], start elo, elo_i = l[start], start dir_ = 0 i = start + 1 while i < n and dir_ == 0: # bootstrap: direzione ignota if h[i] > ehi: ehi, ehi_i = h[i], i if l[i] < elo: elo, elo_i = l[i], i if c[i] <= ehi - k_atr * a[i]: piv.append((ehi_i, float(ehi), +1, i)) dir_ = -1 seg = l[ehi_i:i + 1] elo_i = ehi_i + int(np.argmin(seg)); elo = float(seg.min()) elif c[i] >= elo + k_atr * a[i]: piv.append((elo_i, float(elo), -1, i)) dir_ = +1 seg = h[elo_i:i + 1] ehi_i = elo_i + int(np.argmax(seg)); ehi = float(seg.max()) i += 1 while i < n: if dir_ == -1: # in discesa: cerco swing low if l[i] < elo: elo, elo_i = l[i], i if c[i] >= elo + k_atr * a[i]: piv.append((elo_i, float(elo), -1, i)) dir_ = +1 seg = h[elo_i:i + 1] ehi_i = elo_i + int(np.argmax(seg)); ehi = float(seg.max()) else: # in salita: cerco swing high if h[i] > ehi: ehi, ehi_i = h[i], i if c[i] <= ehi - k_atr * a[i]: piv.append((ehi_i, float(ehi), +1, i)) dir_ = -1 seg = l[ehi_i:i + 1] elo_i = ehi_i + int(np.argmin(seg)); elo = float(seg.min()) i += 1 return piv def build_segments(piv: list, n: int) -> list: """Segmenti: swing A->B (pivot s-1 -> s), griglia attiva in [confirm(B)+1, confirm(next)]. prev2 = (qA, qB) dello swing s-2 (STESSO verso del corrente, per la confluenza: lo swing s-1 ha verso opposto e le sue estensioni proiettano sempre fuori dal range corrente).""" segs = [] for s in range(1, len(piv)): A, B = piv[s - 1], piv[s] w0 = B[3] + 1 w1 = piv[s + 1][3] if s + 1 < len(piv) else n - 1 prev2 = (piv[s - 3][1], piv[s - 2][1]) if s >= 3 else None segs.append(dict(w0=w0, w1=w1, pA=A[1], pB=B[1], prev2=prev2)) return segs # =========================================================================== # RATIO SET (vero + placebo) # =========================================================================== def _draw_valid(rng, lo: float, hi: float, size, forbid) -> np.ndarray: """Uniformi in [lo,hi] con banda esclusa max(0.02, 2% relativo) attorno ai Fib veri.""" out = rng.uniform(lo, hi, size=size) for _ in range(200): bad = np.zeros(out.shape, bool) for f in forbid: bad |= np.abs(out - f) < max(0.02, 0.02 * f) if not bad.any(): break out[bad] = rng.uniform(lo, hi, size=int(bad.sum())) return out def make_placebo_a(rng): """(N_PA,3) ritracciamenti + (N_PA,4) estensioni: set FISSI cross-swing.""" r = _draw_valid(rng, *RET_RANGE, (N_PA, len(RET_TRUE)), RET_TRUE) e = _draw_valid(rng, *EXT_RANGE, (N_PA, len(EXT_TRUE)), EXT_TRUE) return r, e def make_placebo_b(rng, n_seg: int): """(n_seg,N_PB,3) + (n_seg,N_PB,4): rapporti RI-SORTEGGIATI per ogni swing.""" r = _draw_valid(rng, *RET_RANGE, (n_seg, N_PB, len(RET_TRUE)), RET_TRUE) e = _draw_valid(rng, *EXT_RANGE, (n_seg, N_PB, len(EXT_TRUE)), EXT_TRUE) return r, e def make_placebo_c(rng): """Set fissi LOCATION-MATCHED: ogni rapporto = Fib vero +/- jitter appena FUORI la banda esclusa (max(0.02,2%rel)) ed entro ~6-8% -> vicini di casa dei Fib. Se i Fib sono davvero speciali (il mercato ancora ESATTAMENTE a 0.618), devono battere anche questi.""" def jit(true_vals, lo, hi): f = np.array(true_vals, float)[None, :] band = np.maximum(0.02, 0.02 * f) delta = band + rng.uniform(0.0, 0.06 * np.maximum(1.0, f), size=(N_PC, f.shape[1])) sign = rng.choice([-1.0, 1.0], size=(N_PC, f.shape[1])) return np.clip(f + sign * delta, lo, hi) return jit(RET_TRUE, *RET_RANGE), jit(EXT_TRUE, *EXT_RANGE) # =========================================================================== # TOUCH SCAN vettoriale + accumulo # =========================================================================== def _touch_scan(h, l, c, w0: int, w1: int, L: np.ndarray, ids: np.ndarray): """Fresh touch dei livelli L nella finestra [w0,w1]: ritorna (bar globali, direzione attesa dal lato di approccio, livello, id-set). Direzione: sign(close[i-1] - L).""" lo = l[w0:w1 + 1]; hi = h[w0:w1 + 1] contains = (lo[:, None] <= L[None, :]) & (hi[:, None] >= L[None, :]) fresh = contains.copy() fresh[1:] &= ~contains[:-1] fresh[0] &= ~((l[w0 - 1] <= L) & (h[w0 - 1] >= L)) tt, kk = np.nonzero(fresh) if not len(tt): z = np.zeros(0) return z.astype(int), z, z, z.astype(int) gi = w0 + tt Lk = L[kk] dirs = np.sign(c[gi - 1] - Lk) m = dirs != 0 return gi[m], dirs[m], Lk[m], ids[kk[m]] def _new_store(): return dict( acc={(H, s): dict(sn=np.zeros(N_SETS), sr=np.zeros(N_SETS), sl=np.zeros(N_SETS), c=np.zeros(N_SETS)) for H in HORIZONS for s in ("IS", "FULL")}, brk={s: dict(b=np.zeros(N_SETS), t=np.zeros(N_SETS)) for s in ("IS", "FULL")}, ) def _acc_touches(store, gi, dirs, Lk, sid, c, a14, is_pre, n): if not len(gi): return relatr = a14[gi] / c[gi] ok = np.isfinite(relatr) & (relatr > 0) gi, dirs, Lk, sid, relatr = gi[ok], dirs[ok], Lk[ok], sid[ok], relatr[ok] if not len(gi): return broke = ((dirs > 0) & (c[gi] < Lk)) | ((dirs < 0) & (c[gi] > Lk)) for scope, sm in (("FULL", np.ones(len(gi), bool)), ("IS", is_pre[gi])): b = store["brk"][scope] np.add.at(b["t"], sid[sm], 1.0) np.add.at(b["b"], sid[sm & broke], 1.0) for H in HORIZONS: m = sm & ((gi + H) < n) if not m.any(): continue g2, d2 = gi[m], dirs[m] react = d2 * (c[g2 + H] / c[g2] - 1.0) a = store["acc"][(H, scope)] np.add.at(a["sn"], sid[m], react / relatr[m]) np.add.at(a["sr"], sid[m], react) np.add.at(a["sl"], sid[m], d2 * (c[g2 + H] / Lk[m] - 1.0)) np.add.at(a["c"], sid[m], 1.0) def scan_config(df: pd.DataFrame, segs: list, PA_r, PA_e, PB_r, PB_e, PC_r, PC_e) -> dict: """Scansione completa di un config (asset,tf,k): test principale (griglia dello swing corrente: ret+ext) e confluenza (ret corrente x ext dello swing s-2, stesso verso).""" h = df["high"].values.astype(float) l = df["low"].values.astype(float) c = df["close"].values.astype(float) a14 = al.atr(df, ATR_WIN) n = len(c) is_pre = (pd.to_datetime(df["datetime"], utc=True) < al.HOLDOUT).values R_true_r = np.array(RET_TRUE)[None, :] R_true_e = np.array(EXT_TRUE)[None, :] main, conf, sing = _new_store(), _new_store(), _new_store() czc = np.zeros(N_SETS) # numero di zone confluenti per set set_rep = np.repeat(np.arange(N_SETS), NL) for si, seg in enumerate(segs): w0, w1 = seg["w0"], seg["w1"] if w0 < 1 or w1 < w0 or w0 >= n: continue w1 = min(w1, n - 1) pA, pB = seg["pA"], seg["pB"] M = pB - pA if abs(M) < 1e-12: continue Rr = np.vstack([R_true_r, PA_r, PB_r[si], PC_r]) # (N_SETS,3) Re = np.vstack([R_true_e, PA_e, PB_e[si], PC_e]) # (N_SETS,4) L = np.concatenate([pB - Rr * M, pA + Re * M], axis=1).ravel() _acc_touches(main, *_touch_scan(h, l, c, w0, w1, L, set_rep), c, a14, is_pre, n) # ------- confluenza: ret(cur) x ext(swing s-2, stesso verso) entro eps ------- if seg["prev2"] is None: continue qA, qB = seg["prev2"] Mp = qB - qA if abs(Mp) < 1e-12: continue eps = CONF_EPS_ATR * a14[w0] if not np.isfinite(eps) or eps <= 0: continue Lr = pB - Rr * M # (N_SETS,3) Le = qA + Re * Mp # (N_SETS,4) dmat = np.abs(Lr[:, :, None] - Le[:, None, :]) # (N_SETS,3,4) cs, cr, ce = np.nonzero(dmat <= eps) if len(cs): Lc = 0.5 * (Lr[cs, cr] + Le[cs, ce]) np.add.at(czc, cs, 1.0) _acc_touches(conf, *_touch_scan(h, l, c, w0, w1, Lc, cs), c, a14, is_pre, n) used_r = np.zeros((N_SETS, Lr.shape[1]), bool); used_r[cs, cr] = True used_e = np.zeros((N_SETS, Le.shape[1]), bool); used_e[cs, ce] = True sr_, rr_ = np.nonzero(~used_r) se_, ee_ = np.nonzero(~used_e) Ls = np.concatenate([Lr[sr_, rr_], Le[se_, ee_]]) ids_s = np.concatenate([sr_, se_]) _acc_touches(sing, *_touch_scan(h, l, c, w0, w1, Ls, ids_s), c, a14, is_pre, n) return dict(main=main, conf=conf, sing=sing, czc=czc, n_seg=len(segs)) # =========================================================================== # STATISTICHE dai contatori # =========================================================================== def _means(store, H: int, scope: str, field: str = "sn"): a = store["acc"][(H, scope)] with np.errstate(invalid="ignore", divide="ignore"): return np.where(a["c"] > 0, a[field] / a["c"], np.nan) def _pctl(true_val: float, plc: np.ndarray): v = plc[np.isfinite(plc)] if len(v) < 20 or not np.isfinite(true_val): return None return float(np.mean(v <= true_val)) IDX_A = slice(1, 1 + N_PA) IDX_B = slice(1 + N_PA, 1 + N_PA + N_PB) IDX_C = slice(1 + N_PA + N_PB, N_SETS) def _cell_stats(store, H: int): out = {} for scope in ("IS", "FULL"): mn = _means(store, H, scope) mr = _means(store, H, scope, "sr") ml = _means(store, H, scope, "sl") cnt = store["acc"][(H, scope)]["c"] out[scope] = dict( n=int(cnt[0]), raw_bps=float(mr[0] * 1e4) if np.isfinite(mr[0]) else None, lvl_bps=float(ml[0] * 1e4) if np.isfinite(ml[0]) else None, norm=float(mn[0]) if np.isfinite(mn[0]) else None, plcA_med=float(np.nanmedian(mn[IDX_A])), plcC_med=float(np.nanmedian(mn[IDX_C])), pctlA=_pctl(mn[0], mn[IDX_A]), pctlB=_pctl(mn[0], mn[IDX_B]), pctlC=_pctl(mn[0], mn[IDX_C]), ) b = store["brk"]["IS"] with np.errstate(invalid="ignore", divide="ignore"): rate = np.where(b["t"] > 0, b["b"] / b["t"], np.nan) out["brk_rate"] = float(rate[0]) if np.isfinite(rate[0]) else None # "il livello tiene" = break-rate BASSO -> pctl_hold alto se true < placebo v = rate[IDX_A][np.isfinite(rate[IDX_A])] out["brk_pctl_hold"] = (float(np.mean(v >= rate[0])) if len(v) >= 20 and np.isfinite(rate[0]) else None) return out def _pool_stores(stores: list): """Somma i contatori di piu' config (stesso significato dei set -> pooling coerente).""" pooled = _new_store() for st in stores: for key, a in st["acc"].items(): for f in ("sn", "sr", "sl", "c"): pooled["acc"][key][f] += a[f] for s, b in st["brk"].items(): pooled["brk"][s]["b"] += b["b"] pooled["brk"][s]["t"] += b["t"] return pooled # =========================================================================== # STRATEGIA (solo se il claim batte il placebo) — fade ai livelli Fib # =========================================================================== def fib_fade_positions(df: pd.DataFrame, k_atr: float, H: int) -> np.ndarray: """Posizione long/short: al fresh-touch di un livello Fib vero prendi la direzione del rimbalzo atteso e tienila H barre (tocchi sovrapposti sommati, clip a +/-1). Decisione a close[i] -> eval_weights la applica dalla barra i+1 (nessun leak).""" piv = zigzag_pivots(df, k_atr) n = len(df) segs = build_segments(piv, n) h = df["high"].values.astype(float) l = df["low"].values.astype(float) c = df["close"].values.astype(float) sig = np.zeros(n) ids0 = np.zeros(NL, dtype=int) for seg in segs: w0, w1 = seg["w0"], min(seg["w1"], n - 1) if w0 < 1 or w1 < w0: continue pA, pB = seg["pA"], seg["pB"] M = pB - pA if abs(M) < 1e-12: continue L = np.concatenate([pB - np.array(RET_TRUE) * M, pA + np.array(EXT_TRUE) * M]) gi, dirs, _, _ = _touch_scan(h, l, c, w0, w1, L, ids0) np.add.at(sig, gi, dirs) return np.clip(pd.Series(sig).rolling(H, min_periods=1).sum().values, -1.0, 1.0) def make_fib_target(tf: str = "1d", k_atr: float = 2.0, H: int = 20): def target(df): return fib_fade_positions(df, k_atr, H) return target # =========================================================================== # MAIN # =========================================================================== def main(): print("=" * 104) print("r0702 ELL-B FIBONACCI CONFLUENCE — reazione ai livelli Fib vs rapporti placebo " "(il null e' tutto)") print(f"zigzag k*ATR14 k={K_GRID}, ratios ret={RET_TRUE} ext={EXT_TRUE}, H={HORIZONS} barre," f" placebo A={N_PA} set fissi / B={N_PB} per-swing / C={N_PC} location-matched," f" seed={SEED}") print("statistica = media reazione segnata/ATR sui tocchi IS (pre-2025); " "pctl alto = Fib meglio del placebo") print("=" * 104) # ---------- [0] CAUSALITY: pivot su prefisso troncato + target strategy ---------- print("\n[0] CAUSALITY CHECK zigzag (pivot confermati < cut identici su prefisso 90%)") for a in ASSETS: for tf in TFS: df = al.get(a, tf) cut = int(len(df) * 0.9) pf = [p for p in zigzag_pivots(df, K_GRID[0]) if p[3] < cut] pp = [p for p in zigzag_pivots(df.iloc[:cut].reset_index(drop=True), K_GRID[0]) if p[3] < cut] ok = pf == pp print(f" {a} {tf}: pivots full single-cell sig ~>=0.997") hdr = (f" {'asset':<5s} {'tf':<3s} {'k':>2s} {'H':>3s} {'nIS':>6s} {'nFULL':>6s} " f"{'rawIS(bps)':>10s} {'lvlIS(bps)':>10s} {'normIS':>8s} {'plcAmed':>8s} " f"{'plcCmed':>8s} {'pctlA_IS':>8s} {'pctlB_IS':>8s} {'pctlC_IS':>8s} " f"{'pctlA_F':>8s} {'brkIS':>6s} {'brkPctl':>7s}") print(hdr) cells = {} for (a, tf, k), res in results.items(): for H in HORIZONS: st = _cell_stats(res["main"], H) cells[(a, tf, k, H)] = st i_, f_ = st["IS"], st["FULL"] print(f" {a:<5s} {tf:<3s} {k:>2.0f} {H:>3d} {i_['n']:>6d} {f_['n']:>6d} " f"{i_['raw_bps']:>10.1f} {i_['lvl_bps']:>10.1f} {i_['norm']:>8.3f} " f"{i_['plcA_med']:>8.3f} {i_['plcC_med']:>8.3f} " f"{i_['pctlA']:>8.2f} {i_['pctlB']:>8.2f} {i_['pctlC']:>8.2f} " f"{f_['pctlA']:>8.2f} {st['brk_rate']:>6.2f} " f"{st['brk_pctl_hold'] if st['brk_pctl_hold'] is not None else float('nan'):>7.2f}") # ---------- [3] POOLED per (k,H) su asset x TF ---------- print("\n[3] POOLED (somma tocchi su 2 asset x 2 TF) — il test famiglia") pooled_pass = {} for k in K_GRID: pooled = _pool_stores([results[(a, tf, k)]["main"] for a in ASSETS for tf in TFS]) for H in HORIZONS: st = _cell_stats(pooled, H) i_ = st["IS"] cell_ps = [cells[(a, tf, k, H)]["IS"]["pctlA"] for a in ASSETS for tf in TFS if cells[(a, tf, k, H)]["IS"]["n"] >= MIN_CELL_TOUCH] min_cell = min(cell_ps) if cell_ps else None ok = (i_["pctlA"] is not None and i_["pctlA"] >= 0.99 and i_["pctlB"] is not None and i_["pctlB"] >= 0.99 and i_["pctlC"] is not None and i_["pctlC"] >= 0.95 and min_cell is not None and min_cell >= 0.90) pooled_pass[(k, H)] = ok print(f" k={k:.0f} H={H:>2d}: nIS={i_['n']:>6d} normIS={i_['norm']:+.3f} " f"(plcA med {i_['plcA_med']:+.3f} / plcC med {i_['plcC_med']:+.3f}) " f"pctlA_IS={i_['pctlA']:.2f} pctlB_IS={i_['pctlB']:.2f} " f"pctlC_IS={i_['pctlC']:.2f} pctlA_FULL={st['FULL']['pctlA']:.2f} " f"min_cell_pctlA={min_cell if min_cell is not None else 'n/a'} " f"PASS(A,B>=0.99 & C>=0.95 & cells>=0.90)={ok}") # ---------- [4] CONFLUENZA vs livello singolo ---------- print("\n[4] CONFLUENZA (ret swing corrente x ext swing s-2 stesso verso, eps=0.25*ATR)") print(" diff = norm(conf) - norm(single); pctl del diff vero vs placebo A") conf_ok_cells = {} for (a, tf, k), res in results.items(): for H in HORIZONS: mc = _means(res["conf"], H, "IS") ms = _means(res["sing"], H, "IS") nc = int(res["conf"]["acc"][(H, "IS")]["c"][0]) ns = int(res["sing"]["acc"][(H, "IS")]["c"][0]) diff = mc - ms p = _pctl(diff[0], diff[IDX_A]) czc_true = int(res["czc"][0]) czc_plc = float(np.mean(res["czc"][IDX_A])) conf_ok_cells[(a, tf, k, H)] = (p is not None and p >= 0.95 and nc >= MIN_CONF_TOUCH) d0 = f"{diff[0]:+.3f}" if np.isfinite(diff[0]) else " n/a" print(f" {a} {tf} k={k:.0f} H={H:>2d}: zone true={czc_true} (plcA med~{czc_plc:.0f}) " f"tocchiIS conf={nc} sing={ns} " f"norm conf={mc[0] if np.isfinite(mc[0]) else float('nan'):+.3f} " f"sing={ms[0] if np.isfinite(ms[0]) else float('nan'):+.3f} diff={d0} " f"pctl_diff={p if p is not None else 'n/a'}") conf_real = {} for k in K_GRID: for H in HORIZONS: conf_real[(k, H)] = all(conf_ok_cells[(a, tf, k, H)] for a in ASSETS for tf in TFS) print(" verdetto confluenza per (k,H) [tutte le 4 celle pctl>=0.95 e n>=30]: " + " ".join(f"k={k:.0f},H={H}:{'PASS' if v else 'FAIL'}" for (k, H), v in conf_real.items())) # ---------- [5] VERDETTO + eventuale strategizzazione ---------- fib_real = any(pooled_pass.values()) any_conf = any(conf_real.values()) print("\n[5] VERDETTO") print(f" fib_real (pooled IS: A,B>=0.99, C>=0.95, tutte le celle >=0.90, " f"per qualche k,H): {fib_real} -> {pooled_pass}") print(f" confluenza > singolo livello (robusto 4/4 celle): {any_conf}") if fib_real: print("\n[6] STRATEGIA (il claim regge il placebo) — fade ai livelli, " "study_family_honest + marginal") grid = [dict(k_atr=k, H=H) for k in K_GRID for H in HORIZONS] fam = al.study_family_honest("FIB-FADE", make_fib_target, grid, tfs=TFS) ch = fam.get("chosen") print(f" chosen(IS)={ch} n_cells={fam.get('n_cells')} " f"DSR={fam.get('deflated_sharpe')} (null-max {fam.get('expected_null_max')}) " f"dsr_pass={fam.get('dsr_pass')}") print(f" earns_slot_marginal={fam.get('earns_slot_marginal')} " f"EARNS_SLOT_HONEST={fam.get('earns_slot_honest')}") if fam.get("marginal"): print(al.fmt_marginal(fam["marginal"])) else: print("\n[6] STRATEGIA: SALTATA — la reazione ai livelli Fib NON batte i rapporti " "casuali in modo robusto (regola a priori). Nessun edge da strategizzare.") print("\nFine. Selezione: nessuna (rapporti dal claim, verdetto su pooled IS); " "hold-out mai usato per scegliere.") if __name__ == "__main__": main()