"""_algomatic24_lhll_20261019.py - AlgomaticTrade "Strategy #24 Short-term LH/LL breakout" (X post 2026-10-18), re-built from the free text. DISCOVERY ONLY.

Written by: Claude (2026-10-19) on Kaloyan's request. EMPIRICAL RESEARCH, NOT LAW / NOT DEPLOYED. The author's exits are paywalled, so the exit set below
is fixed by me BEFORE the run (3 exits x 2 markets = 6 cells, all printed):
  session day   18:00 ET -> 17:00 ET (CFD day); daily high / low from H1 mid bars
  setup         day D-1: high < high(D-2) AND low < low(D-2)  (a weak day)
  trigger       during day D, the first H1 mid close above high(D-1); entry = ask open of the next H1 bar (if it is still day D or the first bar of D+1)
  stop          low(D-1) (the weak day's low), R = entry - stop; skip if R <= 0
  exits         X1 first daily close above the entry (bid), max 5 sessions     X2 close of the 3rd session after entry     X3 target +1R, max 5 sessions
                all with the stop; stop/target on H1 bid bars, stop first on a tie, gap through the stop fills at the bar open
  costs         spread in the fills (file bid/ask; US100 1.70 pt round trip floor not added), financing 2.1%/yr of the notional per night held
Markets: nas100usd (2005-2020), spx500usd (2015-2020), true clock via data_loader. Statistics per trade in R; t over trades.
"""
from __future__ import annotations

import os
import sys

import numpy as np
import pandas as pd

HERE = os.path.dirname(os.path.abspath(__file__))
LAB = os.path.dirname(HERE)
sys.path.insert(0, os.path.join(LAB, "prop_gpu_miner"))
sys.path.insert(0, LAB)
import data_loader as DL  # noqa: E402

NY = "America/New_York"
FIN_NIGHT = 0.021 / 360


def h1(symbol: str) -> pd.DataFrame:
    q = DL.load_true_clock(symbol, "discovery").dropna()
    q.index = q.index.tz_convert(NY)
    agg = {"bid_open": "first", "ask_open": "first", "bid_high": "max", "ask_high": "max", "bid_low": "min", "ask_low": "min", "bid_close": "last", "ask_close": "last"}
    b = q.resample("1h", label="left", closed="left").agg(agg).dropna()
    b["mid_h"] = (b.bid_high + b.ask_high) / 2
    b["mid_l"] = (b.bid_low + b.ask_low) / 2
    b["mid_c"] = (b.bid_close + b.ask_close) / 2
    b["sess"] = (b.index + pd.Timedelta(hours=6)).normalize().tz_localize(None)       # 18:00 ET opens the next session day
    b = b[b.sess.dt.dayofweek < 5]
    return b


def run(symbol: str) -> pd.DataFrame:
    b = h1(symbol)
    day = b.groupby("sess").agg(H=("mid_h", "max"), L=("mid_l", "min"))
    day = day[b.groupby("sess").size() >= 18]                                         # full sessions only
    sess = list(day.index)
    pos = {d: i for i, d in enumerate(sess)}
    rows_by_sess = {d: g for d, g in b.groupby("sess")}
    out = []
    for i in range(2, len(sess)):
        d2, d1, d0 = sess[i - 2], sess[i - 1], sess[i]
        if not (day.H[d1] < day.H[d2] and day.L[d1] < day.L[d2]):
            continue
        g = rows_by_sess[d0]
        hit = np.flatnonzero(g.mid_c.to_numpy() > day.H[d1])
        if not hit.size:
            continue
        # entry bar = next H1 bar (same session, or the first bar of the next one)
        allb = b.iloc[b.index.get_loc(g.index[hit[0]]) + 1:]
        if allb.empty or allb.sess.iloc[0] not in (d0, sess[i + 1] if i + 1 < len(sess) else None):
            continue
        e_px = allb.ask_open.iloc[0]
        stop = day.L[d1]
        R = e_px - stop
        if R <= 0:
            continue
        e_sess = allb.sess.iloc[0]
        for xname in ("X1", "X2", "X3"):
            tgt = e_px + R if xname == "X3" else None
            exit_px, why, nights = None, None, 0
            last_sess = e_sess
            for k in range(len(allb)):
                bar = allb.iloc[k]
                if bar.sess != last_sess:
                    nights += 1; last_sess = bar.sess
                held = pos.get(bar.sess, 10**9) - pos.get(e_sess, 0)
                if bar.bid_low <= stop:
                    exit_px, why = min(bar.bid_open, stop) if k > 0 else stop, "stop"; break
                if tgt is not None and bar.bid_high >= tgt:
                    exit_px, why = max(bar.bid_open, tgt) if k > 0 else tgt, "target"; break
                is_last = k + 1 == len(allb) or allb.sess.iloc[k + 1] != bar.sess
                if is_last:
                    if xname == "X1" and (bar.bid_close > e_px or held >= 4):
                        exit_px, why = bar.bid_close, "close"; break
                    if xname == "X2" and held >= 3:
                        exit_px, why = bar.bid_close, "time"; break
                    if xname == "X3" and held >= 4:
                        exit_px, why = bar.bid_close, "time"; break
            if exit_px is None:
                continue
            r = (exit_px - e_px) / R - nights * FIN_NIGHT * e_px / R
            out.append(dict(sym=symbol, exit=xname, date=d0, R=r, why=why, nights=nights, risk_pct=R / e_px))
    return pd.DataFrame(out)


def main():
    pd.set_option("display.width", 200)
    T = pd.concat([run(s) for s in ("nas100usd", "spx500usd")])
    t = lambda x: x.mean() / x.std(ddof=1) * np.sqrt(len(x))
    s = T.groupby(["sym", "exit"]).agg(n=("R", "size"), mean_R=("R", "mean"), t=("R", t), win=("R", lambda x: (x > 0).mean()),
                                      nights=("nights", "mean"), stop_pct=("risk_pct", "median"))
    print(s.round(3).to_string())
    T["half"] = np.where(T.date < pd.Timestamp("2013-01-01"), "2005-12", "2013-20")
    print(T.groupby(["sym", "exit", "half"]).R.agg(["size", "mean"]).round(3).unstack().to_string())
    T.to_csv(os.path.join(HERE, "_algomatic24_lhll_20261019_trades.csv"), index=False)


if __name__ == "__main__":
    main()
