"""
reference.py -- [2026-09-28] Fixed 14-day reference data per stock (entry rules v2).

Built once per trading day after the close (cron ~16:35) for every stock that
could make the watchlist (universe filters + last close $4-16 + 14-day average
volume >= 500k), and saved as one small file per stock:

    data/reference/<for_date>/<SYMBOL>.json      for_date = the NEXT trading day

The live bot and the simulator load the same files, so tests and live trading
use identical references. Nothing here changes a trade; it is data.

Per stock (from the previous 14 completed sessions):
  minute profile (390 regular-session minutes, 9:30 = index 0):
    vol_per_min[m]     average volume in minute m            -> per-minute RVOL
    cum_vol[m]         average total volume by the end of m  -> RVOL so far
    abs_move_pct[m]    average |close(m) / open - 1| in %    -> move vs normal (noise area)
    open5_vol          average 9:30-9:35 volume              -> opening RVOL
  daily: atr14, adv14 (avg daily volume), avg_range_pct14, prev close/high/low,
         high_20d, ref_5d_* (5-day frame), sr14 (14-day support/resistance levels)

    python3 reference.py                 build for the next trading day (after the close)
    python3 reference.py --for 2026-09-29 [--symbols AGEN,TGB]
Load:  reference.load(for_date, symbol) -> dict or None
"""
import gc
import json
import sys
import time
from datetime import datetime, timedelta, date
from pathlib import Path
from zoneinfo import ZoneInfo

BASE = Path(__file__).resolve().parent
sys.path.insert(0, str(BASE))

ET = ZoneInfo("America/New_York")
OUT = BASE / "data" / "reference"
SESSIONS = 14
MINUTES = 390
PRICE_BAND = (4.0, 16.0)          # a little wider than the $5-15 universe: stocks move into range
MIN_ADV = 500_000


def next_weekday(d: date) -> date:
    d += timedelta(days=1)
    while d.weekday() >= 5:
        d += timedelta(days=1)
    return d


def _minute_profile(days: dict) -> dict:
    """days: {date: [(minute_index, open, high, low, close, volume), ...]} for regular-session bars."""
    n = len(days)
    vol = [0.0] * MINUTES
    cum = [0.0] * MINUTES
    mv = [0.0] * MINUTES
    open5 = 0.0
    for d, bars in days.items():
        by_m = {b[0]: b for b in bars}
        day_open = bars[0][1] if bars else None
        run, last_close = 0.0, day_open
        for m in range(MINUTES):
            b = by_m.get(m)
            v = b[5] if b else 0.0
            if b:
                last_close = b[4]
            run += v
            vol[m] += v
            cum[m] += run
            if day_open and last_close:
                mv[m] += abs(last_close / day_open - 1) * 100
            if m < 5:
                open5 += v
    return {"vol_per_min": [round(x / n, 1) for x in vol], "cum_vol": [round(x / n, 1) for x in cum],
            "abs_move_pct": [round(x / n, 4) for x in mv], "open5_vol": round(open5 / n, 1), "days": n}


def build(for_date: date, symbols: list = None, chunk: int = 25, verbose: bool = True) -> dict:
    """Build reference files for `for_date` from the 14 sessions before it."""
    from alpaca.data.requests import StockBarsRequest
    from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
    from alpaca_client import get_client
    import universe
    import volatility

    t0 = time.time()
    client = get_client()
    day0 = datetime(for_date.year, for_date.month, for_date.day, tzinfo=ET)
    explicit = symbols is not None       # named stocks (rescan adds, tests): no price/volume filter --
    if symbols is None:                  # a stock can gap into range overnight (CLRO 9/28: $3.45 -> $5.83)
        symbols = universe.get_universe_symbols(client)
    daily = client.get_daily_bars_bulk(symbols, day0 - timedelta(days=45), day0 - timedelta(seconds=1))
    keep = {}
    for s, bl in daily.items():
        bl = [b for b in bl if b["t"] < day0]
        if len(bl) < 3:
            continue
        adv = sum(b["v"] for b in bl[-SESSIONS:]) / len(bl[-SESSIONS:])
        # same volume rule as the scanner (yesterday >= 500k) OR a 14-day average >= 500k
        if explicit or (PRICE_BAND[0] <= bl[-1]["c"] <= PRICE_BAND[1] and (adv >= MIN_ADV or bl[-1]["v"] >= MIN_ADV)):
            keep[s] = bl
    for bench in ("SPY", "IWM"):
        if bench not in keep:
            bl = [b for b in client.get_daily_bars_bulk([bench], day0 - timedelta(days=45),
                                                        day0 - timedelta(seconds=1)).get(bench, []) if b["t"] < day0]
            if bl:
                keep[bench] = bl
    sess = sorted({b["t"].astimezone(ET).date() for bl in keep.values() for b in bl})[-SESSIONS:]
    if verbose:
        print(f"{len(keep)} stocks qualify (of {len(symbols)}); sessions {sess[0]} .. {sess[-1]}", flush=True)
    outdir = OUT / for_date.isoformat()
    outdir.mkdir(parents=True, exist_ok=True)
    start = datetime(sess[0].year, sess[0].month, sess[0].day, 9, 30, tzinfo=ET)
    end = datetime(sess[-1].year, sess[-1].month, sess[-1].day, 16, 0, tzinfo=ET)
    syms = sorted(keep)
    done = 0
    for i in range(0, len(syms), chunk):
        part = syms[i:i + chunk]
        try:
            req = StockBarsRequest(symbol_or_symbols=part, timeframe=TimeFrame(1, TimeFrameUnit.Minute),
                                   start=start, end=end, limit=None, feed=client._feed)
            data = client.hist_data.get_stock_bars(req).data
        except Exception as e:
            print(f"  chunk {i} failed: {e}", flush=True)
            continue
        for s in part:
            days = {}
            for b in data.get(s, []):
                t = b.timestamp.astimezone(ET)
                m = (t.hour - 9) * 60 + t.minute - 30
                if 0 <= m < MINUTES and t.date() in sess:
                    days.setdefault(t.date(), []).append((m, float(b.open), float(b.high), float(b.low),
                                                          float(b.close), float(b.volume)))
            if not days:
                continue
            for d in days:
                days[d].sort()
            bl = keep[s]
            last14 = bl[-SESSIONS:]
            ref = {"symbol": s, "for_date": for_date.isoformat(), "built": datetime.now(ET).isoformat(timespec="seconds"),
                   "sessions": [d.isoformat() for d in sorted(days)],
                   **_minute_profile(days),
                   "atr14": volatility.daily_atr(bl), "adv14": round(sum(b["v"] for b in last14) / len(last14)),
                   "avg_range_pct14": round(sum((b["h"] / b["l"] - 1) * 100 for b in last14) / len(last14), 3),
                   "prev_close": bl[-1]["c"], "prev_high": bl[-1]["h"], "prev_low": bl[-1]["l"],
                   "high_20d": max(b["h"] for b in bl[-20:]),
                   **volatility.five_day_reference(bl),
                   "sr14": volatility.sr_levels(bl)}
            (outdir / f"{s}.json").write_text(json.dumps(ref, separators=(",", ":")))
            done += 1
        del data
        gc.collect()
        if verbose:
            print(f"  {min(i + chunk, len(syms))}/{len(syms)} stocks, {time.time() - t0:.0f}s", flush=True)
    (outdir / "_index.json").write_text(json.dumps({"for_date": for_date.isoformat(), "stocks": done,
                                                     "sessions": [d.isoformat() for d in sess],
                                                     "seconds": round(time.time() - t0)}))
    return {"stocks": done, "seconds": round(time.time() - t0)}


_CACHE = {}


def load(for_date, symbol: str):
    """The reference for `symbol` on `for_date` (date or 'YYYY-MM-DD'), or None."""
    key = (str(for_date), symbol)
    if key not in _CACHE:
        p = OUT / str(for_date) / f"{symbol}.json"
        _CACHE[key] = json.loads(p.read_text()) if p.exists() else None
    return _CACHE[key]


if __name__ == "__main__":
    args = sys.argv[1:]
    fd = next_weekday(datetime.now(ET).date()) if "--for" not in args else date.fromisoformat(args[args.index("--for") + 1])
    syms = args[args.index("--symbols") + 1].split(",") if "--symbols" in args else None
    print(build(fd, syms))
