"""
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}
