"""HYPERION — indicators (pure Python, candle dicts: {t,o,h,l,c,v})"""
from __future__ import annotations
import math


def sma(vals: list[float], n: int) -> float | None:
    if len(vals) < n:
        return None
    return sum(vals[-n:]) / n


def ema_series(vals: list[float], n: int) -> list[float]:
    if not vals:
        return []
    k = 2 / (n + 1)
    out = [vals[0]]
    for v in vals[1:]:
        out.append(v * k + out[-1] * (1 - k))
    return out


def ema(vals: list[float], n: int) -> float | None:
    if len(vals) < n:
        return None
    return ema_series(vals, n)[-1]


def rsi(closes: list[float], n: int = 14) -> float | None:
    if len(closes) < n + 1:
        return None
    gains, losses = [], []
    for i in range(1, len(closes)):
        d = closes[i] - closes[i - 1]
        gains.append(max(d, 0.0))
        losses.append(max(-d, 0.0))
    ag = sum(gains[:n]) / n
    al = sum(losses[:n]) / n
    for i in range(n, len(gains)):
        ag = (ag * (n - 1) + gains[i]) / n
        al = (al * (n - 1) + losses[i]) / n
    if al == 0:
        return 100.0
    return 100 - 100 / (1 + ag / al)


def true_ranges(candles: list[dict]) -> list[float]:
    trs = []
    for i, c in enumerate(candles):
        if i == 0:
            trs.append(c["h"] - c["l"])
        else:
            pc = candles[i - 1]["c"]
            trs.append(max(c["h"] - c["l"], abs(c["h"] - pc), abs(c["l"] - pc)))
    return trs


def atr(candles: list[dict], n: int = 14) -> float | None:
    trs = true_ranges(candles)
    if len(trs) < n:
        return None
    a = sum(trs[:n]) / n
    for tr in trs[n:]:
        a = (a * (n - 1) + tr) / n
    return a


def adx(candles: list[dict], n: int = 14) -> float | None:
    if len(candles) < 2 * n + 1:
        return None
    plus_dm, minus_dm = [], []
    for i in range(1, len(candles)):
        up = candles[i]["h"] - candles[i - 1]["h"]
        dn = candles[i - 1]["l"] - candles[i]["l"]
        plus_dm.append(up if (up > dn and up > 0) else 0.0)
        minus_dm.append(dn if (dn > up and dn > 0) else 0.0)
    trs = true_ranges(candles)[1:]

    def wilder(vals):
        s = sum(vals[:n])
        out = [s]
        for v in vals[n:]:
            s = s - s / n + v
            out.append(s)
        return out

    atr_s, pdm_s, mdm_s = wilder(trs), wilder(plus_dm), wilder(minus_dm)
    dxs = []
    for a, p, m in zip(atr_s, pdm_s, mdm_s):
        if a == 0:
            continue
        pdi, mdi = 100 * p / a, 100 * m / a
        if pdi + mdi == 0:
            continue
        dxs.append(100 * abs(pdi - mdi) / (pdi + mdi))
    if len(dxs) < n:
        return None
    a = sum(dxs[:n]) / n
    for d in dxs[n:]:
        a = (a * (n - 1) + d) / n
    return a


def zscore(vals: list[float], n: int = 20) -> float | None:
    if len(vals) < n:
        return None
    w = vals[-n:]
    mu = sum(w) / n
    var = sum((v - mu) ** 2 for v in w) / n
    sd = math.sqrt(var)
    if sd == 0:
        return 0.0
    return (vals[-1] - mu) / sd


def realized_vol(closes: list[float], n: int = 20) -> float | None:
    """Std of log returns over n bars (per-bar, not annualized)."""
    if len(closes) < n + 1:
        return None
    rets = [math.log(closes[i] / closes[i - 1]) for i in range(len(closes) - n, len(closes))]
    mu = sum(rets) / len(rets)
    return math.sqrt(sum((r - mu) ** 2 for r in rets) / len(rets))


def donchian(candles: list[dict], n: int = 20) -> tuple[float, float] | None:
    if len(candles) < n + 1:
        return None
    w = candles[-n - 1:-1]  # exclude current bar
    return max(c["h"] for c in w), min(c["l"] for c in w)
