"""
volatility.py

[2026-09-23] Per-symbol volatility class and the minimum entry-stop
distance that goes with it (user request: "recognize when a stock is
volatile from the ones that has a low volatility and adjust the minimum
stop distance accordingly"). Motivated by 9/23's quiet names getting
3-7c stops from 3 x 1-min ATR (BLMN, LTRX: 3c) -- inside the normal
post-entry dip every trade makes.

Volatility = daily ATR(14) as % of price, from the prior completed
sessions' daily bars (scanner.compute_range_20d_high already fetches
them; simulate.py fetches the same as of the replayed date). Carried
into smart_engine as resistance_levels["daily_atr"] (a price distance,
not a level -- every level consumer skips it).

Floor = max(volatility floor, spread_mult x current spread), where the
volatility floor is either
  style "k":     k x daily ATR
  style "tiers": a % of price by class (low/medium/high daily ATR%).
The stop is then min(stop from the caller's own rule, price - floor):
a floor only ever WIDENS a stop, never tightens one.
"""

DEFAULT_MIN_STOP = {
    "enabled": False,
    "style": "tiers",
    "k": 0.15,
    # [max daily ATR%, floor % of price, class]
    "tiers": [[3.0, 0.6, "low"], [6.0, 1.0, "medium"], [1e9, 1.5, "high"]],
    "spread_mult": 2.0,
    "unknown_floor_pct": 1.0,
}


def merge_cfg(cfg: dict) -> dict:
    return {**DEFAULT_MIN_STOP, **(cfg or {})}


def daily_atr(daily_bars: list, period: int = 14):
    """Wilder-free simple ATR over the last `period` true ranges of
    completed daily bars (oldest first). None if not enough history."""
    if len(daily_bars) < 2:
        return None
    trs = []
    for prev, b in zip(daily_bars, daily_bars[1:]):
        trs.append(max(b["h"] - b["l"], abs(b["h"] - prev["c"]), abs(b["l"] - prev["c"])))
    trs = trs[-period:]
    return sum(trs) / len(trs)


def classify(price: float, datr, cfg: dict):
    if not datr or not price:
        return "unknown", None
    pct = datr / price * 100.0
    for max_pct, _floor, name in cfg["tiers"]:
        if pct < max_pct:
            return name, pct
    return cfg["tiers"][-1][2], pct


def stop_floor(price: float, datr, spread: float, cfg: dict) -> dict:
    """Returns {"floor", "class", "daily_atr_pct"} -- floor is a price
    distance (dollars) the stop must be at least this far below price."""
    cls, pct = classify(price, datr, cfg)
    if cls == "unknown":
        vol_floor = price * cfg["unknown_floor_pct"] / 100.0
    elif cfg["style"] == "k":
        vol_floor = cfg["k"] * datr
    else:
        vol_floor = next(price * f / 100.0 for m, f, n in cfg["tiers"] if n == cls)
    floor = max(vol_floor, cfg["spread_mult"] * (spread or 0.0))
    return {"floor": floor, "class": cls, "daily_atr_pct": round(pct, 2) if pct else None}


def five_day_reference(daily_bars: list) -> dict:
    """[2026-09-25] User's 5-day reference frame, from the last 5 COMPLETED
    sessions (oldest first): extreme high/low (the ceiling/floor traders
    defend), average daily high/low, and the average daily range (the
    stock's typical one-day move). Keys are prefixed ref_ so every level
    consumer skips them unless setup_plan.five_day_levels is on."""
    last5 = daily_bars[-5:]
    if len(last5) < 2:
        return {}
    return {
        "ref_5d_high": max(b["h"] for b in last5),
        "ref_5d_low": min(b["l"] for b in last5),
        "ref_5d_avg_high": round(sum(b["h"] for b in last5) / len(last5), 4),
        "ref_5d_avg_low": round(sum(b["l"] for b in last5) / len(last5), 4),
        "ref_5d_avg_range": round(sum(b["h"] - b["l"] for b in last5) / len(last5), 4),
    }


def is_level_key(key: str) -> bool:
    """Keys in a resistance-levels dict that are NOT price levels."""
    return key != "daily_atr" and not key.startswith("ref_")


# ---------------------------------------------------------------------------
# [2026-09-28] 14-day support / resistance reference (user request).
# RECORDING ONLY for now -- logged with every entry check, no rule reads it.
# ---------------------------------------------------------------------------
def sr_levels(daily_bars: list, lookback: int = 14, tol_atr: float = 0.25, min_touches: int = 2) -> dict:
    """Levels from the last `lookback` COMPLETED daily sessions (oldest first).
    Every daily high and low is a touch; touches are grouped while a group's
    width (highest - lowest) stays within tol_atr x daily ATR (no drifting
    chains). A group with >= min_touches is a level: a band lo..hi, its
    touch count and how many sessions ago it was last touched. Support vs
    resistance is decided against the current price (sr_position)."""
    bars = daily_bars[-lookback:]
    if len(bars) < 3:
        return {}
    atr = daily_atr(daily_bars)
    if not atr:
        return {}
    tol = tol_atr * atr
    n = len(bars)
    pts = sorted([(b["h"], n - 1 - i) for i, b in enumerate(bars)] +
                 [(b["l"], n - 1 - i) for i, b in enumerate(bars)])
    groups, cur = [], [pts[0]]
    for p in pts[1:]:
        if p[0] - cur[0][0] <= tol:
            cur.append(p)
        else:
            groups.append(cur)
            cur = [p]
    groups.append(cur)
    levels = [{"lo": round(g[0][0], 4), "hi": round(g[-1][0], 4),
               "mid": round(sum(x[0] for x in g) / len(g), 4),
               "touches": len(g), "last_touch_days_ago": min(x[1] for x in g)}
              for g in groups if len(g) >= min_touches]
    return {"levels": levels, "high": max(b["h"] for b in bars), "low": min(b["l"] for b in bars),
            "atr": round(atr, 4), "sessions": n, "tol": round(tol, 4)}


def sr_position(sr: dict, price: float) -> dict:
    """Where `price` sits against the 14-day levels: nearest resistance band
    above (lo > price) and support band below (hi < price) -- a band the
    price is inside is reported as inside_level -- distances in % and daily
    ATRs, and the position in the 14-day range (0 = low, 100 = high)."""
    if not sr or not price:
        return {}
    atr = sr["atr"]
    above = [l for l in sr["levels"] if l["lo"] > price]
    below = [l for l in sr["levels"] if l["hi"] < price]
    inside = [l for l in sr["levels"] if l["lo"] <= price <= l["hi"]]
    res = min(above, key=lambda l: l["lo"]) if above else None
    sup = max(below, key=lambda l: l["hi"]) if below else None
    rng = sr["high"] - sr["low"]
    return {"high_14d": sr["high"], "low_14d": sr["low"],
            "pos_in_range_pct": round((price - sr["low"]) / rng * 100, 1) if rng > 0 else None,
            "inside_level": inside[0] if inside else None,
            "resistance": res, "support": sup,
            "dist_to_resistance_pct": round((res["lo"] / price - 1) * 100, 3) if res else None,
            "dist_to_resistance_atr": round((res["lo"] - price) / atr, 3) if res else None,
            "dist_to_support_pct": round((price / sup["hi"] - 1) * 100, 3) if sup else None,
            "dist_to_support_atr": round((price - sup["hi"]) / atr, 3) if sup else None,
            "above_14d_high": price > sr["high"], "below_14d_low": price < sr["low"]}
