"""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()
