"""Pack technique multi-echelles (15 min, 1 h, 4 h) et declencheurs classiques, calcules localement a partir des
bougies 1 min Binance, uniquement sur des barres CLOTUREES : a l'instant t, rien ne depend d'une bougie future.

Pack (entrees de l'algorithme, avec tous les autres etats du marche) :
  bbp  position dans les bandes de Bollinger (0 = bande basse, 1 = bande haute)
  bbw  largeur des bandes (pb) : compression / expansion de volatilite
  atr  ATR 14 (pb du prix)            adx  force de tendance (ADX 14)
  di   direction de tendance (DI+ - DI-) / (DI+ + DI-)
  don  position dans le canal de Donchian 20    st  sens du Supertrend (10, 3)
  rsi  RSI 14                         vwap ecart au VWAP 20 barres, en ATR
  volz volume inhabituel (z du log-volume vs 20 barres)

Declencheurs (+1 achat / -1 vente, sur la barre qui vient de cloturer) :
  bb_breakout cassure d'une bande de Bollinger   bb_revert retour a l'interieur des bandes
  rsi_revert  sortie de survente / surachat      donchian_breakout plus haut / plus bas des 20 barres
  supertrend_flip inversion du Supertrend        macd_cross croisement MACD / signal
"""
import bisect
import math

TFS = (("15m", 900), ("1h", 3600), ("4h", 14400))
TF_NAMES = tuple(n for n, _ in TFS)
PACK = ("bbp", "bbw", "atr", "adx", "di", "don", "st", "rsi", "vwap", "volz")
RULES = ("bb_breakout", "bb_revert", "rsi_revert", "donchian_breakout", "supertrend_flip", "macd_cross")
PACK_LABELS = {"bbp": "Bollinger %b", "bbw": "largeur Bollinger", "atr": "ATR", "adx": "ADX (force de tendance)",
               "di": "direction de tendance (DI)", "don": "position Donchian", "st": "sens Supertrend", "rsi": "RSI",
               "vwap": "ecart au VWAP", "volz": "volume inhabituel"}
RULE_LABELS = {"bb_breakout": "cassure Bollinger", "bb_revert": "retour dans Bollinger", "rsi_revert": "sortie RSI 30/70",
               "donchian_breakout": "cassure Donchian 20", "supertrend_flip": "inversion Supertrend",
               "macd_cross": "croisement MACD"}
EVENT_MEMORY = 10  # barres pendant lesquelles un declencheur reste visible (attenue) comme entree


def build_bars(kt, o, h, l, c, v, sec):
    """Barres `sec` a partir des bougies 1 min (completes a 80 % au moins ; la barre en cours est exclue)."""
    ends, bo, bh, bl, bc, bv = [], [], [], [], [], []
    per = sec // 60

    def close_bar(cur):
        if cur[6] >= 0.8 * per:
            ends.append((cur[0] + 1) * sec)
            bo.append(cur[1])
            bh.append(cur[2])
            bl.append(cur[3])
            bc.append(cur[4])
            bv.append(cur[5])

    cur = None
    for i, t in enumerate(kt):
        b = t // sec
        if cur is not None and b != cur[0]:
            close_bar(cur)
            cur = None
        if cur is None:
            cur = [b, o[i], h[i], l[i], c[i], v[i], 1]
        else:
            cur[2] = max(cur[2], h[i])
            cur[3] = min(cur[3], l[i])
            cur[4] = c[i]
            cur[5] += v[i]
            cur[6] += 1
    if cur is not None and cur[6] == per:  # derniere barre deja entierement cloturee
        close_bar(cur)
    return ends, bo, bh, bl, bc, bv


def _ema(x, span):
    a, out, e = 2 / (span + 1), [], x[0] if x else 0
    for val in x:
        e = a * val + (1 - a) * e
        out.append(e)
    return out


def _wilder(x, n, start):
    """Moyenne de Wilder de x a partir de l'indice `start` (premiere valeur = moyenne simple de n valeurs)."""
    out = [None] * len(x)
    if len(x) < start + n:
        return out
    a = sum(x[start:start + n]) / n
    out[start + n - 1] = a
    for i in range(start + n, len(x)):
        a = (a * (n - 1) + x[i]) / n
        out[i] = a
    return out


def indicators(h, l, c, v):
    n = len(c)
    feat = {k: [None] * n for k in PACK}
    ev = {k: [0] * n for k in RULES}
    if n < 30:
        return feat, ev
    tr = [h[0] - l[0]] + [max(h[i] - l[i], abs(h[i] - c[i - 1]), abs(l[i] - c[i - 1])) for i in range(1, n)]
    atr = _wilder(tr, 14, 0)
    atr10 = _wilder(tr, 10, 0)
    # Bollinger 20, 2 ecarts-types
    for i in range(19, n):
        w = c[i - 19:i + 1]
        m = sum(w) / 20
        sd = math.sqrt(sum((x - m) ** 2 for x in w) / 20)
        feat["bbp"][i] = (c[i] - (m - 2 * sd)) / (4 * sd) if sd else 0.5
        feat["bbw"][i] = 4 * sd / m * 1e4 if m else None
    # ADX 14
    pdm = [0.0] + [max(h[i] - h[i - 1], 0) if h[i] - h[i - 1] > l[i - 1] - l[i] else 0.0 for i in range(1, n)]
    mdm = [0.0] + [max(l[i - 1] - l[i], 0) if l[i - 1] - l[i] > h[i] - h[i - 1] else 0.0 for i in range(1, n)]
    sp, sm, st_ = _wilder(pdm, 14, 1), _wilder(mdm, 14, 1), _wilder(tr, 14, 1)
    dx = [None] * n
    for i in range(n):
        if st_[i]:
            dip, dim = 100 * sp[i] / st_[i], 100 * sm[i] / st_[i]
            if dip + dim > 0:
                dx[i] = 100 * abs(dip - dim) / (dip + dim)
                feat["di"][i] = (dip - dim) / (dip + dim)
    first = next((i for i in range(n) if dx[i] is not None), None)
    if first is not None:
        adx = _wilder([d if d is not None else 0.0 for d in dx], 14, first)
        for i in range(n):
            feat["adx"][i] = adx[i]
    # Donchian 20, ATR, RSI 14, MACD, VWAP 20, volume
    rsi, g, ls = [None] * n, 0.0, 0.0
    for i in range(1, n):
        d = c[i] - c[i - 1]
        g = (g * 13 + max(d, 0)) / 14 if i > 14 else g + max(d, 0) / 14
        ls = (ls * 13 + max(-d, 0)) / 14 if i > 14 else ls + max(-d, 0) / 14
        if i >= 14:
            rsi[i] = 100.0 if ls == 0 else 100 - 100 / (1 + g / ls)
    macd = [a - b for a, b in zip(_ema(c, 12), _ema(c, 26))]
    hist = [m - s for m, s in zip(macd, _ema(macd, 9))]
    tp = [(a + b + x) / 3 for a, b, x in zip(h, l, c)]
    lv = [math.log(x + 1) for x in v]
    for i in range(n):
        if i >= 19:
            hh, ll = max(h[i - 19:i + 1]), min(l[i - 19:i + 1])
            feat["don"][i] = (c[i] - ll) / (hh - ll) if hh > ll else 0.5
            sv = sum(v[i - 19:i + 1])
            if sv > 0 and atr[i]:
                vw = sum(t * x for t, x in zip(tp[i - 19:i + 1], v[i - 19:i + 1])) / sv
                feat["vwap"][i] = (c[i] - vw) / atr[i]
        if i >= 20:
            w = lv[i - 20:i]
            m = sum(w) / 20
            sd = math.sqrt(sum((x - m) ** 2 for x in w) / 20)
            feat["volz"][i] = (lv[i] - m) / sd if sd else 0.0
        if atr[i]:
            feat["atr"][i] = atr[i] / c[i] * 1e4
        feat["rsi"][i] = rsi[i]
    # Supertrend (10, 3)
    direction, fu, fl = [None] * n, None, None
    for i in range(n):
        if atr10[i] is None:
            continue
        hl2 = (h[i] + l[i]) / 2
        bu, bd = hl2 + 3 * atr10[i], hl2 - 3 * atr10[i]
        if fu is None:
            fu, fl, direction[i] = bu, bd, 1 if c[i] >= hl2 else -1
        else:
            fu = bu if bu < fu or c[i - 1] > fu else fu
            fl = bd if bd > fl or c[i - 1] < fl else fl
            prev = direction[i - 1] or 1
            direction[i] = 1 if prev < 0 and c[i] > fu else (-1 if prev > 0 and c[i] < fl else prev)
        feat["st"][i] = direction[i]
    # declencheurs
    bbp = feat["bbp"]
    for i in range(1, n):
        if bbp[i] is not None and bbp[i - 1] is not None:
            ev["bb_breakout"][i] = 1 if bbp[i] > 1 >= bbp[i - 1] else (-1 if bbp[i] < 0 <= bbp[i - 1] else 0)
            ev["bb_revert"][i] = 1 if bbp[i - 1] < 0 <= bbp[i] else (-1 if bbp[i - 1] > 1 >= bbp[i] else 0)
        if rsi[i] is not None and rsi[i - 1] is not None:
            ev["rsi_revert"][i] = 1 if rsi[i - 1] < 30 <= rsi[i] else (-1 if rsi[i - 1] > 70 >= rsi[i] else 0)
        if i >= 20:
            ev["donchian_breakout"][i] = 1 if c[i] > max(h[i - 20:i]) else (-1 if c[i] < min(l[i - 20:i]) else 0)
        if direction[i] is not None and direction[i - 1] is not None and direction[i] != direction[i - 1]:
            ev["supertrend_flip"][i] = direction[i]
        if i >= 35:
            ev["macd_cross"][i] = 1 if hist[i - 1] <= 0 < hist[i] else (-1 if hist[i - 1] >= 0 > hist[i] else 0)
    return feat, ev


class TechPack:
    """Pack technique et declencheurs de chaque echelle, consultables a la date t (barres closes seulement)."""

    def __init__(self, kt, o, h, l, c, v):
        self.tf = {}
        for name, sec in TFS:
            ends, bo, bh, bl, bc, bv = build_bars(kt, o, h, l, c, v, sec)
            feat, ev = indicators(bh, bl, bc, bv)
            last = {}
            for rule in RULES:
                ld, li, d, k = [0] * len(ends), [-1] * len(ends), 0, -1
                for i, e in enumerate(ev[rule]):
                    if e:
                        d, k = e, i
                    ld[i], li[i] = d, k
                last[rule] = (ld, li)
            self.tf[name] = {"ends": ends, "h": bh, "l": bl, "c": bc, "feat": feat, "ev": ev, "last": last}

    def features(self, t):
        out = {}
        for name in TF_NAMES:
            b = self.tf[name]
            j = bisect.bisect_right(b["ends"], t) - 1
            if j < 0:
                continue
            for k in PACK:
                if b["feat"][k][j] is not None:
                    out[f"tp_{k}_{name}"] = b["feat"][k][j]
            for rule in RULES:
                ld, li = b["last"][rule]
                age = j - li[j]
                # dernier declencheur de la regle, attenue avec son anciennete (0 s'il est trop vieux ou absent)
                out[f"tg_{rule}_{name}"] = ld[j] * math.exp(-age / 3) if li[j] >= 0 and age <= EVENT_MEMORY else 0.0
        return out

    def events(self, t, window):
        """Declencheurs de la barre close la plus recente, si elle s'est terminee dans ]t - window, t]."""
        res = []
        for name in TF_NAMES:
            b = self.tf[name]
            j = bisect.bisect_right(b["ends"], t) - 1
            if j < 0 or t - b["ends"][j] >= window:
                continue
            for rule in RULES:
                if b["ev"][rule][j]:
                    res.append((rule, name, b["ev"][rule][j], b["ends"][j]))
        return res


def pack_names():
    return ([f"tp_{k}_{tf}" for tf in TF_NAMES for k in PACK] +
            [f"tg_{r}_{tf}" for tf in TF_NAMES for r in RULES])


def label(name):
    """Libelle lisible d'une entree du pack (None si ce n'en est pas une)."""
    parts = name.split("_")
    if name.startswith("tp_") and len(parts) == 3 and parts[1] in PACK_LABELS:
        return f"{PACK_LABELS[parts[1]]} {parts[2]}"
    if name.startswith("tg_"):
        rule, tf = "_".join(parts[1:-1]), parts[-1]
        if rule in RULE_LABELS:
            return f"declencheur {RULE_LABELS[rule]} {tf}"
    return None
