"""Basket rule v4 (3 Oct 2026): phases BUY / HOLD / HOLD HALF / RISK OFF for every basket, plus rotation groups.
Chosen after 70 iterations (it_a .. it_f, rh_build, it_e in /home/claude/rh50; raw data published with the handover).

TURNS BULLISH (signal) on a daily close above the basket's highest close of the prior NIN days, while
  BTC closes above its 100 day average, the majors basket is risk on, at least 25% of the other tested baskets
  are risk on, and (Solana baskets only) SOL has not gained 20%+ on BTC in 4 weeks (froth).
PHASES: BUY = the signal close and the next close if price is still within 10% of the breakout level
  (buying 2+ days late lost money in 2025 to 2026); HOLD = risk on, no new money; TAKE HALF = first HOT close
  (20 day z score >= 2 and top 20% of the 90 day range); HOLD HALF after that; SELL = a daily close below the
  lowest close of the prior NOUT days, then WAIT (risk off).
WINDOWS: long tested baskets 30/15; young baskets (Robinhood, Ink, Arc, Bankr, o1) the FAST rule 10/5, which
  made money on the same 14 long baskets in both halves (2023 to 2024: $3.14 won per $1 lost; 2025 to 2026: $2.10)
  and on the rebuilt Robinhood baskets caught the late August run and was out by 10 Sep, before the crash
  (the slow rule bought the top and lost 34 to 36%).
ROTATION: rotate into = baskets in the BUY phase, strongest 30 day move first, up to 3; hold = risk on;
  rotate out of = SELL or TAKE HALF today; wait = risk off."""
import math, statistics as S, datetime as dt
from stages import stage_series

SLOW, FAST, COST, TOPK = (30, 15), (10, 5), 0.01, 3
BUY_DAYS, BUY_EXT = 1, 0.10
SOLBTC_FROTH, BREADTH = 0.20, 0.25


def sma_on(days, px, n=100):
    ds = [d for d in days if px.get(d)]; out = {}
    for i, d in enumerate(ds):
        w = [px[x] for x in ds[max(0, i - n + 1):i + 1]]
        if len(w) >= int(n * 0.9): out[d] = px[d] > sum(w) / len(w)
    return out


def donchian_on(days, L, nin=30, nout=15):
    s = False; out = {}
    for i, d in enumerate(days):
        if i >= nin and L[i] > max(L[i - nin:i]): s = True
        elif i >= nout and L[i] < min(L[i - nout:i]): s = False
        out[d] = s
    return out


def to_daily(line, stamp_open=False):
    by = {}
    for t, v in line:
        if not v: continue
        if stamp_open: t += 4 * 3600
        d = dt.datetime.fromtimestamp(t - 1, dt.timezone.utc).date() + dt.timedelta(days=1)
        by[d.isoformat()] = v
    return sorted(by.items())


def gate_ok(d, gates):
    """gates: dict with btc_on, maj_on, breadth (date -> share of other baskets on), froth (date -> bool)."""
    if not gates["btc_on"].get(d, False): return False, "BTC below its 100 day average"
    if not gates.get("is_majors") and not gates["maj_on"].get(d, False): return False, "majors basket risk off"
    b = gates.get("breadth", {}).get(d)
    if b is not None and b < BREADTH: return False, "fewer than 25% of other baskets risk on"
    if gates.get("froth", {}).get(d): return False, "SOL up 20%+ on BTC in 4 weeks (froth)"
    return True, ""


def run(days, L, gates, nin, nout, cost=COST):
    st = stage_series(L) if len(L) >= 26 else [("WARMUP", {})] * len(L)
    trades = []; w = [0.0] * len(L); i = nin
    while i < len(L):
        d = days[i]
        if not (L[i] > max(L[i - nin:i]) and gate_ok(d, gates)[0]): i += 1; continue
        ent = L[i]; brk = max(L[i - nin:i]); cur = 1.0; trim = None; acc = 0.0; ex = None; w[i] = 1.0
        for j in range(i + 1, len(L)):
            if L[j] < min(L[j - nout:j]): ex = j; break
            if cur == 1.0 and st[j][0] == "HOT": trim = j; acc += 0.5 * (L[j] / ent - 1); cur = 0.5
            w[j] = cur
        last = ex if ex is not None else len(L) - 1
        ret = acc + cur * (L[last] / ent - 1) - (cost if ex is not None else cost / 2)
        trades.append(dict(entry=days[i], entry_lvl=ent, brk=brk, trim=days[trim] if trim is not None else None,
                           exit=days[ex] if ex is not None else None, last=days[last], ret=ret, bars=last - i,
                           status="closed" if ex is not None else "open", weight=cur))
        if ex is None: break
        i = ex + 1
    return trades, w


def stats(ts):
    xs = [t["ret"] for t in ts if t["status"] == "closed"]
    if not xs: return dict(n=0)
    wn = [x for x in xs if x > 0]; ls = [x for x in xs if x <= 0]
    return dict(n=len(xs), mean=round(S.mean(xs) * 100, 1), med=round(S.median(xs) * 100, 1), win=round(len(wn) / len(xs) * 100),
                pf=round(sum(wn) / -sum(ls), 2) if ls and sum(ls) < 0 else None)


def read(days, L, gates, fast=False, live_start="2026-10-04"):
    n = len(L)
    if n < 12: return {"state": "TOO NEW", "word": "TOO NEW", "phase": "too_new", "days_of_data": n}
    nin, nout = FAST if fast else SLOW
    if n <= nin + 1:
        nin, nout = FAST
        fast = True
    trades, w = run(days, L, gates, nin, nout)
    last = days[-1]; cur = trades[-1] if trades else None
    hi_next = max(L[-nin:]); lo_next = min(L[-nout:])
    st = stage_series(L)[-1] if n >= 26 else ("WARMUP", {})
    ok, why_blocked = gate_ok(last, gates)
    o = {"fast": fast, "window": [nin, nout], "days_of_data": n, "gate_ok": ok, "blocked_by": why_blocked,
         "gates": {"btc": bool(gates["btc_on"].get(last)), "majors": True if gates.get("is_majors") else bool(gates["maj_on"].get(last)),
                   "breadth": gates.get("breadth", {}).get(last), "froth": bool(gates.get("froth", {}).get(last))},
         "stage": st[0], "r30": round((L[-1] / L[-31] - 1) * 100, 1) if n > 30 else None,
         "r7": round((L[-1] / L[-8] - 1) * 100, 1) if n > 7 else None,
         "up_at": round((hi_next / L[-1] - 1) * 100, 1), "down_at": round((lo_next / L[-1] - 1) * 100, 1),
         "record": stats([t for t in trades if t["entry"] < live_start]),
         "trades": [dict(t, ret=round(t["ret"] * 100, 1)) for t in trades if t["entry"] >= live_start or t["status"] == "open"],
         "line": [[d, round(v, 10)] for d, v in zip(days[-120:], L[-120:])], "w": w[-120:]}
    on = bool(cur and cur["status"] == "open")
    o["state"] = "RISK ON" if on else "RISK OFF"
    if on:
        age = cur["bars"]; ext = L[-1] / cur["brk"] - 1
        o["open"] = dict(entry=cur["entry"], entry_lvl=cur["entry_lvl"], brk=cur["brk"], trim=cur["trim"], weight=cur["weight"],
                         ret=round((L[-1] / cur["entry_lvl"] - 1) * 100, 1), days=age, above_breakout=round(ext * 100, 1))
        if cur["trim"] == last: o["word"], o["phase"] = "TAKE HALF", "out"
        elif cur["trim"]: o["word"], o["phase"] = "HOLD HALF", "hold"
        elif age <= BUY_DAYS and ext <= BUY_EXT: o["word"], o["phase"] = "BUY", "into"
        else: o["word"], o["phase"] = "HOLD", "hold"
    else:
        if cur and cur["exit"] == last:
            o["word"], o["phase"] = "SELL", "out"; o["closed"] = dict(entry=cur["entry"], ret=round(cur["ret"] * 100, 1))
        elif L[-1] > max(L[-nin - 1:-1]) and not ok: o["word"], o["phase"] = "WAIT: BLOCKED", "wait"
        elif o["up_at"] <= 5: o["word"], o["phase"] = "WAIT: CLOSE", "wait"
        else: o["word"], o["phase"] = "WAIT", "wait"
    return o
