"""
build_open_window_dataset.py

[2026-09-25] User request: for each of the last 20 trading days, find every
stock that met the trade bot's basic universe criteria, and save all its
1-minute bars from 9:25 to 10:00 ET.

Universe criteria, reconstructed per day exactly as the bot applies them at
its 9:28 scan (universe.get_universe_symbols + universe.prefilter_by_snapshot):
  - asset filters: tradable US equity, no ETFs/funds, no leveraged/inverse,
    name complexity <= max_name_syllables  (today's asset list -- see caveat)
  - price: last trade before 9:28 ET that morning in [price_min, price_max]
    (premarket; if no premarket trade, the previous session's close)
  - volume: previous session's volume >= min_avg_daily_volume

Output (this folder, reports/open_window/):
  open_window_bars_<first>_<last>.csv.gz   one row per 1-min bar, 9:25-9:59 bar starts
  open_window_universe_<first>_<last>.csv  one row per qualifying symbol-day

Caveat: asset filters use the CURRENT asset list, so symbols delisted since
(or listed after) a given day are handled as of today.
"""
import csv
import gzip
import json
import sys
from datetime import datetime, time, timedelta, timezone
from pathlib import Path
from zoneinfo import ZoneInfo

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from alpaca.data.requests import StockBarsRequest
from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
from alpaca_client import get_client
from config_loader import get_config
import universe

ET = ZoneInfo("America/New_York")
UTC = timezone.utc
N_DAYS = 20
OUT = Path(__file__).resolve().parent
OUT.mkdir(exist_ok=True)


def at(d, h, m):
    return datetime.combine(d, time(h, m), tzinfo=ET)


def minute_bars(client, symbols, start, end, chunk=100):
    out = {}
    for i in range(0, len(symbols), chunk):
        req = StockBarsRequest(symbol_or_symbols=symbols[i:i + chunk],
                               timeframe=TimeFrame(1, TimeFrameUnit.Minute),
                               start=start, end=end, limit=None, feed=client._feed)
        try:
            data = client.hist_data.get_stock_bars(req).data
        except Exception as e:
            print(f"  chunk {i} failed: {e}", flush=True)
            continue
        out.update(data)
    return out


def main():
    ucfg = get_config()["universe"]
    client = get_client()
    syms = universe.get_universe_symbols(client)
    print(f"asset-filtered universe (today's list): {len(syms)}", flush=True)

    end = datetime.now(ET).replace(hour=0, minute=0, second=0, microsecond=0)
    daily = client.get_daily_bars_bulk(syms, end - timedelta(days=45), end - timedelta(seconds=1), limit=40)
    dates = sorted({b["t"].astimezone(ET).date() for bl in daily.values() for b in bl})
    days = dates[-N_DAYS:]
    print(f"trading days: {days[0]} .. {days[-1]} ({len(days)})", flush=True)

    by_sym = {s: {b["t"].astimezone(ET).date(): b for b in bl} for s, bl in daily.items()}
    tag = f"{days[0]:%Y%m%d}_{days[-1]:%Y%m%d}"
    bars_path = OUT / f"open_window_bars_{tag}.csv.gz"
    uni_path = OUT / f"open_window_universe_{tag}.csv"
    summary = []

    with gzip.open(bars_path, "wt", newline="") as bf, open(uni_path, "w", newline="") as uf:
        bw, uw = csv.writer(bf), csv.writer(uf)
        bw.writerow(["date", "symbol", "time_et", "open", "high", "low", "close", "volume", "vwap", "trades"])
        uw.writerow(["date", "symbol", "price_0928", "price_source", "prev_close", "prev_volume",
                     "gap_pct_0928", "open_0930", "close_0959", "high_0930_1000", "low_0930_1000",
                     "chg_0930_1000_pct", "run_up_pct", "volume_0930_1000", "bars"])
        for d in days:
            prev = dates[dates.index(d) - 1]
            # volume rule + a loose price band to limit minute-bar requests
            pre = [s for s in syms
                   if prev in by_sym.get(s, {}) and by_sym[s][prev]["v"] >= ucfg["min_avg_daily_volume"]
                   and ucfg["price_min"] * 0.6 <= by_sym[s][prev]["c"] <= ucfg["price_max"] * 1.6]
            mb = minute_bars(client, pre, at(d, 4, 0), at(d, 10, 0))
            n_ok = 0
            for s in pre:
                bars = [b for b in mb.get(s, [])]
                pm = [b for b in bars if b.timestamp.astimezone(ET).time() < time(9, 28)]
                if pm:
                    price, src = float(pm[-1].close), "premarket"
                else:
                    price, src = by_sym[s][prev]["c"], "prev_close"
                if not (ucfg["price_min"] <= price <= ucfg["price_max"]):
                    continue
                win = [b for b in bars if time(9, 25) <= b.timestamp.astimezone(ET).time() < time(10, 0)]
                for b in win:
                    bw.writerow([d.isoformat(), s, b.timestamp.astimezone(ET).strftime("%H:%M"),
                                 b.open, b.high, b.low, b.close, int(b.volume),
                                 round(b.vwap, 4) if b.vwap else "", b.trade_count or ""])
                reg = [b for b in win if b.timestamp.astimezone(ET).time() >= time(9, 30)]
                pc = by_sym[s][prev]["c"]
                if reg:
                    o, c = float(reg[0].open), float(reg[-1].close)
                    hi, lo = max(float(b.high) for b in reg), min(float(b.low) for b in reg)
                    # biggest low->later-high run inside 9:30-10:00
                    run, lo_so_far = 0.0, float("inf")
                    for b in reg:
                        lo_so_far = min(lo_so_far, float(b.low))
                        run = max(run, (float(b.high) - lo_so_far) / lo_so_far * 100)
                    vol = sum(int(b.volume) for b in reg)
                    uw.writerow([d.isoformat(), s, price, src, pc, int(by_sym[s][prev]["v"]),
                                 round((price / pc - 1) * 100, 2), o, c, hi, lo, round((c / o - 1) * 100, 2),
                                 round(run, 2), vol, len(win)])
                else:
                    uw.writerow([d.isoformat(), s, price, src, pc, int(by_sym[s][prev]["v"]),
                                 round((price / pc - 1) * 100, 2), "", "", "", "", "", "", 0, len(win)])
                n_ok += 1
            summary.append({"date": d.isoformat(), "checked": len(pre), "qualified": n_ok})
            print(f"{d}: {len(pre)} passed volume, {n_ok} qualified", flush=True)
            bf.flush(); uf.flush()

    (OUT / f"open_window_summary_{tag}.json").write_text(json.dumps(summary, indent=1))
    print("wrote", bars_path, uni_path, flush=True)


if __name__ == "__main__":
    main()
