"""
sf4_batch.py -- [2026-10-02] STAR FOLLOW, WINNERS vs LOSERS (the user's design): winners and losers
stay separate; every minute the stock is compared with the closest days of BOTH:
  share    = % winners among the 150 closest days of the whole playbook (winners + losers)
  BUY      when the WINNERS' buy signal shows (reference buys among the 150 closest winners in the
           last 5 min >= max(6, 2 x normal)) AND the stock looks more like winners -> buy;
           otherwise wait and check again the next minute
  SELL     as soon as it looks more like losers (2 minutes in a row), or the winners' sell signal,
           or 15:55
  variants: "majority"  more like winners = share >= 50%
            "normal"    more like winners = share >= the playbook's normal winner share (~28%)
Features: lib_v2 (price, VWAP, volume, SPY, IWM, gap). Only days before the traded day. 0.1% cost.
    python3 sf4_batch.py 2026-08-27..2026-10-01          (all variants)
"""
import gzip
import json
import sys
from pathlib import Path

import numpy as np

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE)); sys.path.insert(0, "/var/www/screener/trade")
import lib_v2 as L2
import sf3_batch as S3
import reference_trader as RT

K, WIN, MIN_N, RATIO, COST, OUT_MIN = 150, 5, 6, 2.0, 0.1, 2


def run_day(lib, wmask, marks, nb, ns, day, a, rule, base, can_buy=None):
    ok = np.where(lib.dates < day)[0]
    sub = lib.F[ok]; isw = wmask[ok]
    widx = np.where(isw)[0]
    keys = [lib.keys[i] for i in ok]
    D = np.zeros(len(ok))
    need = 0.5 if rule == "majority" else base
    pos, trades, wait = None, [], 0
    for m in range(0, 385):
        D += ((sub[:, m, :] - a["F"][None, m, :]) ** 2).sum(1)
        if m < 5:
            continue
        allk = np.argpartition(D, K)[:K]
        share = float(isw[allk].mean())
        wk = widx[np.argpartition(D[widx], K)[:K]]
        w0 = m - WIN + 1
        buys = sells = 0
        for i in wk:
            for bm, sm in marks.get(keys[i], ()):
                if w0 <= bm <= m:
                    buys += 1
                if bm < w0 and w0 <= sm <= m:
                    sells += 1
        if pos is None:
            if m <= 345 and (can_buy is None or can_buy(m)) and buys >= max(MIN_N, RATIO * nb[m]):
                if share >= need:
                    pos = {"buy_m": m + 1, "buy": float(a["O"][m + 1]), "low": float(a["O"][m + 1]),
                           "share_buy": share, "out": 0, "waited": wait}
                    wait = 0
                else:
                    wait += 1
            continue
        pos["low"] = min(pos["low"], float(a["L"][m]))
        pos["out"] = pos["out"] + 1 if share < need else 0
        why = None
        if pos["out"] >= OUT_MIN:
            why = f"looks more like losers ({share:.0%} winners)"
        elif sells >= max(MIN_N, RATIO * ns[m]):
            why = "winners' sell signal"
        if why:
            trades.append({"buy_m": pos["buy_m"], "buy": pos["buy"], "sell_m": m + 1, "sell": float(a["O"][m + 1]),
                           "why": why, "low": pos["low"], "share_buy": pos["share_buy"]})
            pos = None
    if pos:
        trades.append({"buy_m": pos["buy_m"], "buy": pos["buy"], "sell_m": 385, "sell": float(a["C"][385]),
                       "why": "15:55", "low": pos["low"], "share_buy": pos["share_buy"]})
    for t in trades:
        t["pl"] = (t["sell"] / t["buy"] - 1) * 100 - COST
        t["dd"] = (t["low"] / t["buy"] - 1) * 100
        t["held"] = t["sell_m"] - t["buy_m"]
    return trades


def report(name, T, days):
    if not T:
        print(f"\n== {name}: 0 trades"); return
    p = np.array([t["pl"] for t in T]); half = days[len(days) // 2] if days else ""
    h = np.array([t.get("day", "") < half for t in T])
    byd = {}
    for t in T:
        byd[t.get("day", "")] = byd.get(t.get("day", ""), 0) + t["pl"]
    print(f"\n== {name}: {len(p)} trades, win {(p > 0).mean():.0%}, avg {p.mean():+.2f}%, median {np.median(p):+.2f}%, "
          f"sum {p.sum():+.1f}% (halves {p[h].sum():+.1f}% / {p[~h].sum():+.1f}%), positive days {sum(v > 0 for v in byd.values())}/{len(byd)}, "
          f"worst {p.min():+.1f}%, median held {int(np.median([t['held'] for t in T]))} min")
    for w in sorted({t["why"].split(" (")[0] for t in T}):
        q = np.array([t["pl"] for t in T if t["why"].split(" (")[0] == w])
        print(f"   exit '{w}': {len(q)} trades, win {(q > 0).mean():.0%}, avg {q.mean():+.2f}%, sum {q.sum():+.1f}%")


def main(rng):
    a_, b_ = rng.split("..")
    lib = L2.Lib()
    marks = S3.load()
    wmask = np.array([k in marks for k in lib.keys])
    base = float(wmask.mean())
    nb, ns = S3.normal_rates(marks)
    print(f"playbook: {len(lib.keys)} days, winners {int(wmask.sum())} ({base:.0%} = normal winner share)", flush=True)
    days = sorted({k.split("|")[0] for k in lib.keys if a_ <= k.split("|")[0] <= b_})
    res = {"majority": [], "normal": []}
    for day in days:
        d = json.load(gzip.open(RT.BARS / f"bars_{day}.json.gz", "rt"))
        for s in d["top30_current"]:
            ref = RT.REF.load(day, s)
            if not ref:
                continue
            a = L2.day_arrays(d["symbols"][s]["bars"], ref, day)
            if a is None:
                continue
            for rule in res:
                res[rule] += [{"day": day, "sym": s, **t} for t in run_day(lib, wmask, marks, nb, ns, day, a, rule, base)]
        RT.REF._CACHE.clear()
        print(day, {r: len(v) for r, v in res.items()}, flush=True)
    json.dump(res, open(HERE / f"sf4_batch_{a_}_{b_}.json", "w"), default=float)
    for rule, T in res.items():
        report(f"{rule.upper()} ({'>= 50% winners' if rule == 'majority' else f'>= normal {base:.0%} winners'})", T, days)


if __name__ == "__main__":
    main(sys.argv[1])
