"""Edge nella FORMA discreta delle candele -> distribuzione condizionale dell'esito. Famiglia: encoding DISCRETO della morfologia di una finestra di L candele (sequenza UP/DOWN/DOJI, opzionalmente arricchita con bucket di body-ratio e shadow-ratio) -> codice intero. Per ogni codice si stima la distribuzione del rendimento a H barre usando SOLO le occorrenze PASSATE il cui esito era gia' realizzato prima della barra di decisione i (expanding window causale). Se un codice mostra bias direzionale forte e statisticamente solido (n campioni >= soglia, win-rate o |media| oltre soglia) si ENTRA a close[i] nella direzione del bias; exit a H barre o TP/SL ATR. VINCOLI ANTI-LOOK-AHEAD (l'errore squeeze e' nato qui): - il codice a i usa SOLO open/high/low/close fino alla barra i inclusa; - la statistica condizionale a i conta SOLO occorrenze del codice terminate in e con e+H <= i-1 -> il loro esito H e' interamente noto PRIMA di i; - direzione decisa dal CODICE (forma fino a close[i]) + STATISTICHE PASSATE, ingresso eseguibile a close[i]. - check_no_lookahead() perturba il futuro: ne' il codice a i ne' le stat usate devono cambiare. Riusa l'engine netto-fee + OOS di explore_lab (simulate/evaluate/robust). Implementazione causale O(N) per codice via accumulatori incrementali (niente ricalcolo dell'intera storia ad ogni barra). Asset: ADA BNB BTC DOGE ETH LTC SOL XRP (1h, 15m). Default 1h. """ from __future__ import annotations import sys from pathlib import Path import numpy as np PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) from scripts.analysis.explore_lab import ( # noqa: E402 get_df, evaluate, robust, simulate, atr, OOS_FRAC, ) # --------------------------- encoding discreto della forma --------------------------- def candle_codes(df, L: int, body_buckets: int = 1, shadow_buckets: int = 1) -> np.ndarray: """Codice intero della forma per la finestra di L candele terminante in i. Componenti per ogni candela: - direzione UP/DOWN/DOJI (sempre): 3 stati. - bucket del body-ratio |c-o|/(h-l) (se body_buckets>1): quantizzazione fissa in body_buckets livelli (corpo piccolo/medio/grande...). - bucket dello shadow-ratio (h-max(o,c)-(min(o,c)-l))/(h-l) in [-1,1] (se shadow_buckets>1): ombra sup vs inf. Quantizzazione a SOGLIE FISSE (non quantili): non dipende dal futuro ne' dal dataset globale -> causale per costruzione. codes[i] dipende solo da barre [i-L+1 .. i]. Per i < L-1 -> -1 (non valido). """ o = df["open"].values; c = df["close"].values h = df["high"].values; l = df["low"].values n = len(c) rng = np.where((h - l) == 0, 1e-12, h - l) body = np.abs(c - o) / rng # [0,1] direction = np.where(body < 0.1, 0, # DOJI np.where(c > o, 1, 2)) # UP=1, DOWN=2 (3 stati: 0,1,2) # shadow asymmetry in [-1,1]: >0 ombra sup dominante, <0 ombra inf up_sh = (h - np.maximum(o, c)) / rng lo_sh = (np.minimum(o, c) - l) / rng shadow = up_sh - lo_sh # bucket body (soglie fisse su frazioni del range): 0..body_buckets-1 if body_buckets > 1: edges_b = np.linspace(0.0, 1.0, body_buckets + 1)[1:-1] bbk = np.digitize(body, edges_b) # 0..body_buckets-1 else: bbk = np.zeros(n, dtype=int) if shadow_buckets > 1: edges_s = np.linspace(-1.0, 1.0, shadow_buckets + 1)[1:-1] sbk = np.digitize(shadow, edges_s) else: sbk = np.zeros(n, dtype=int) # simbolo per candela: dir * (body_buckets*shadow_buckets) + bbk*shadow_buckets + sbk nbb, nsb = body_buckets, shadow_buckets per_dir = nbb * nsb sym = direction * per_dir + bbk * nsb + sbk # 0 .. 3*per_dir-1 base = 3 * per_dir codes = np.full(n, -1, dtype=np.int64) # codice della finestra L: base-L polinomiale sui simboli [i-L+1 .. i] acc = np.zeros(n, dtype=np.int64) for k in range(L): # contributo della candela a posizione (i-L+1+k): peso base**(L-1-k) shifted = np.full(n, 0, dtype=np.int64) shifted[L - 1 - k:] = sym[: n - (L - 1 - k)] if (L - 1 - k) > 0 else sym acc += shifted * (base ** (L - 1 - k)) codes[L - 1:] = acc[L - 1:] return codes def fwd_return(close: np.ndarray, H: int) -> np.ndarray: out = np.full(len(close), np.nan) out[: len(close) - H] = (close[H:] - close[:-H]) / close[:-H] return out # --------------------------- stima condizionale causale --------------------------- def shape_entries(df, L=3, H=12, body_buckets=1, shadow_buckets=1, min_n=30, edge=0.55, min_lib=500, tp_atr=None, sl_atr=None, fade=False) -> list[dict]: """Entries dal bias condizionale del codice di forma (causale, no look-ahead). L: lunghezza finestra-forma. H: orizzonte = max_bars. body_buckets/shadow_buckets: granularita' dell'encoding (1 = solo direzione). min_n: occorrenze passate minime del codice (con esito noto) per fidarsi. edge: win-rate minimo (frazione di esiti concordi col segno della media) per entrare; |edge-0.5| e' il margine direzionale. min_lib: barre minime di storia prima di iniziare a operare. tp_atr/sl_atr: TP/SL in multipli di ATR (None = solo time-limit H). Causalita': a barra di decisione i si aggiorna lo stato del codice della finestra terminata in e = i-1-H (il cui esito fr[e] e' ora noto). Le statistiche usate per decidere a i derivano quindi solo da occorrenze con e+H <= i-1. """ close = df["close"].values n = len(close) a = atr(df, 14) codes = candle_codes(df, L, body_buckets, shadow_buckets) fr = fwd_return(close, H) # accumulatori per codice: somma rendimenti, n positivi, n totali from collections import defaultdict cnt = defaultdict(int) pos = defaultdict(int) ssum = defaultdict(float) entries: list[dict] = [] for i in range(min_lib, n - 1): # aggiorna lo stato col codice la cui finestra termina in e = i-1-H e = i - 1 - H if e >= L - 1: ce = codes[e] re = fr[e] if ce >= 0 and not np.isnan(re): cnt[ce] += 1 pos[ce] += 1 if re > 0 else 0 ssum[ce] += re ci = codes[i] if ci < 0: continue ntot = cnt.get(ci, 0) if ntot < min_n: continue mean = ssum[ci] / ntot wr_up = pos[ci] / ntot # frazione esiti positivi nel passato d = 1 if mean > 0 else -1 # win-rate nella direzione scelta wr = wr_up if d == 1 else (1.0 - wr_up) if wr < edge: continue if fade: d = -d # FADE: entra contro il bias storico ent = {"i": i, "d": d, "max_bars": H} if tp_atr is not None and a[i] > 0: ent["tp"] = close[i] + d * tp_atr * a[i] if sl_atr is not None and a[i] > 0: ent["sl"] = close[i] - d * sl_atr * a[i] entries.append(ent) return entries # --------------------------- verifica no look-ahead --------------------------- def check_no_lookahead(df, L=3, H=12, body_buckets=2, shadow_buckets=2) -> bool: """Perturbare il FUTURO (>i) non cambia (a) il codice a i, (b) le stat usate a i. (a) codes[i] dipende solo da barre <= i. (b) le entries fino a i (incluse) non cambiano se stravolgo le barre > i+1. """ n = len(df) i = n // 2 codes0 = candle_codes(df, L, body_buckets, shadow_buckets) df2 = df.copy() fut = slice(i + 1, n) for col in ("open", "high", "low", "close"): df2.loc[df2.index[i + 1:], col] = df2[col].values[i + 1:] * 1.5 codes1 = candle_codes(df2, L, body_buckets, shadow_buckets) ok_code = bool(codes0[i] == codes1[i] and np.array_equal(codes0[: i + 1], codes1[: i + 1])) # entries: confronta quelle con indice <= i-1-H (decise con stat tutte note prima del futuro) e0 = shape_entries(df, L=L, H=H, body_buckets=body_buckets, shadow_buckets=shadow_buckets, min_n=5, edge=0.50, min_lib=200) e1 = shape_entries(df2, L=L, H=H, body_buckets=body_buckets, shadow_buckets=shadow_buckets, min_n=5, edge=0.50, min_lib=200) cutoff = i - 1 - H s0 = {(x["i"], x["d"]) for x in e0 if x["i"] <= cutoff} s1 = {(x["i"], x["d"]) for x in e1 if x["i"] <= cutoff} ok_ent = (s0 == s1) print(f" no-lookahead codice a i={i}: {'OK' if ok_code else 'VIOLATO'}; " f"entries<=i-1-H invarianti: {'OK' if ok_ent else 'VIOLATO'} " f"({len(s0)} vs {len(s1)})") return ok_code and ok_ent # --------------------------- run riproducibile --------------------------- def predictive_power(df, L=3, H=12, body_buckets=1, shadow_buckets=1, min_n=30, min_lib=500): """Diagnostica ONESTA: la direzione predetta dal bias storico (causale) anticipa il segno del rendimento realizzato? Misura hit-rate aggregato della predizione (segno media passata del codice) vs realizzato, su tutte le barre operabili. Niente fee: pura capacita' predittiva del codice di forma.""" close = df["close"].values n = len(close) codes = candle_codes(df, L, body_buckets, shadow_buckets) fr = fwd_return(close, H) from collections import defaultdict cnt = defaultdict(int); pos = defaultdict(int); ssum = defaultdict(float) hits = tot = 0 pred_ret = 0.0 for i in range(min_lib, n - 1): e = i - 1 - H if e >= L - 1: ce = codes[e]; re = fr[e] if ce >= 0 and not np.isnan(re): cnt[ce] += 1; pos[ce] += 1 if re > 0 else 0; ssum[ce] += re ci = codes[i] if ci < 0 or cnt.get(ci, 0) < min_n or np.isnan(fr[i]): continue d = 1 if ssum[ci] / cnt[ci] > 0 else -1 actual = fr[i] hits += (np.sign(actual) == d); tot += 1 pred_ret += d * actual # PnL teorico senza fee hr = hits / tot * 100 if tot else 0.0 print(f" predittivita' L{L}H{H} b{body_buckets}s{shadow_buckets}: " f"hit={hr:.2f}% su {tot} (50%=rumore) | PnL_grezzo_noFee={pred_ret*100:+.0f}%") return hr def run(): print("=" * 100) print(" SHAPE_CANDLE_RESEARCH — encoding discreto forma -> bias condizionale | netto fee, OOS") print("=" * 100) df_btc = get_df("BTC", "1h") print("\n[causalita']") check_no_lookahead(df_btc, L=3, H=12, body_buckets=2, shadow_buckets=2) # ----- sweep base BTC/ETH 1h: solo direzione (body=shadow=1) ----- print("\n[BTC/ETH 1h] solo direzione UP/DOWN/DOJI (body=1,shadow=1), time-exit a H:") for asset in ("BTC", "ETH"): df = get_df(asset, "1h") print(f" -- {asset} 1h --") for L in (2, 3, 4): for H in (6, 12, 24): ents = shape_entries(df, L=L, H=H, min_n=30, edge=0.55) evaluate(f"dir L{L}H{H}", ents, df) # ----- encoding arricchito body+shadow ----- print("\n[BTC/ETH 1h] encoding arricchito (body=2,shadow=2), time-exit a H:") for asset in ("BTC", "ETH"): df = get_df(asset, "1h") print(f" -- {asset} 1h --") for L in (2, 3): for H in (6, 12, 24): ents = shape_entries(df, L=L, H=H, body_buckets=2, shadow_buckets=2, min_n=30, edge=0.55) evaluate(f"rich L{L}H{H} b2s2", ents, df) # ----- selettivita': soglie edge piu' alte, meno trade ----- print("\n[BTC/ETH 1h] selettivo (edge>=0.58, min_n>=50), dir-only:") for asset in ("BTC", "ETH"): df = get_df(asset, "1h") print(f" -- {asset} 1h --") for L in (3, 4, 5): for H in (12, 24): ents = shape_entries(df, L=L, H=H, min_n=50, edge=0.58) evaluate(f"sel L{L}H{H} e58", ents, df) # ----- con TP/SL ATR (gestione rischio) sui candidati piu' attivi ----- print("\n[BTC/ETH 1h] con TP/SL ATR (tp=2,sl=1.5), dir-only L3:") for asset in ("BTC", "ETH"): df = get_df(asset, "1h") for H in (12, 24): ents = shape_entries(df, L=3, H=H, min_n=30, edge=0.55, tp_atr=2.0, sl_atr=1.5) evaluate(f"{asset} tpsl L3H{H}", ents, df) # ----- DIAGNOSTICA: il codice di forma ha QUALSIASI potere predittivo? ----- print("\n[DIAGNOSTICA] hit-rate predizione (segno bias storico vs realizzato), senza fee:") for asset in ("BTC", "ETH"): df = get_df(asset, "1h") print(f" -- {asset} 1h --") for L in (2, 3, 4): for H in (6, 12, 24): predictive_power(df, L=L, H=H, min_n=30) for L in (2, 3): predictive_power(df, L=L, H=12, body_buckets=2, shadow_buckets=2, min_n=30) # ----- IPOTESI FADE: il bias e' anti-predittivo (mean-reversion)? ----- print("\n[FADE del bias] entra CONTRO il bias storico (dir-only):") for asset in ("BTC", "ETH"): df = get_df(asset, "1h") print(f" -- {asset} 1h --") for L in (2, 3): for H in (6, 12, 24): ents = shape_entries(df, L=L, H=H, min_n=30, edge=0.55, fade=True) evaluate(f"FADE L{L}H{H}", ents, df) def run_extended(configs): """Valuta config candidate su tutti gli asset 1h+15m e stampa robustezza.""" assets = ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"] for cfg in configs: print(f"\n[ESTESO] config {cfg}") for tf in ("1h", "15m"): print(f" -- timeframe {tf} --") nrob = 0 for asset in assets: try: df = get_df(asset, tf) except Exception as ex: print(f" {asset}: skip ({ex})") continue ents = shape_entries(df, **cfg) res = evaluate(f"{asset} {tf}", ents, df) nrob += robust(res) print(f" -> robuste {nrob}/{len(assets)} su {tf}") if __name__ == "__main__": run()