"""_luk_bounds_20261019.py - entry-day ordering bounds for _luk_momentum_pullback_20261019.py. DISCOVERY ONLY (<= 2020-12-31).

Written by: Claude (2026-10-19). EMPIRICAL RESEARCH, NOT LAW / NOT DEPLOYED. Same rules, data and costs as the original script.

Why: on daily bars the original books a stop whenever the entry day's low reaches the stop, i.e. it assumes the low came AFTER the buy-stop
filled. That is the pessimistic bound. When the day opened at or below the trigger, the low may have come first, before the entry existed.
  pessimistic  the original rule (entry-day low <= stop -> stopped)
  optimistic   on the entry day a stop counts only when it is certain: the day gapped above the trigger (the open is the entry, so every later
               low is after it) or the close is at or below the stop (price went through the stop after the entry). Otherwise the trade
               carries on from the next day.
The same treatment is applied to the +3R partial on the entry day in both bounds (a high after the trigger is always after the entry).
The truth lies between the two bounds.
"""
from __future__ import annotations

import os
import sys

import numpy as np
import pandas as pd

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from _luk_momentum_pullback_20261019 import COST, FIN, load  # noqa: E402


def run(data: dict[str, pd.DataFrame], optimistic: bool) -> pd.DataFrame:
    ret126 = pd.DataFrame({k: v.c.pct_change(126) for k, v in data.items()})
    rank = ret126.rank(axis=1, pct=True)
    trades = []
    for sym, d in data.items():
        c, h, l = d.c, d.h, d.l
        e9, e21 = c.ewm(span=9, adjust=False).mean(), c.ewm(span=21, adjust=False).mean()
        tr = pd.concat([h - l, (h - c.shift()).abs(), (l - c.shift()).abs()], axis=1).max(axis=1) / c
        dv = (d.craw * d.vraw).rolling(20).median()
        r126, rk = ret126[sym].reindex(d.index), rank[sym].reindex(d.index)
        strong = (r126 >= 0.30) & (rk >= 0.80) & (dv >= 20e6)
        resting = (l.rolling(5).min() > l.shift(5).rolling(10).min()) & (tr.rolling(10).mean() < tr.rolling(30).mean()) & (c > e9) & (c > e21)
        zone = ((l - e21).abs() / e21 <= 0.015) | ((l - e9).abs() / e9 <= 0.015)
        sig = (strong & resting & zone).to_numpy()
        H, L, O, C, E9 = d.h.to_numpy(), d.l.to_numpy(), d.o.to_numpy(), d.c.to_numpy(), e9.to_numpy()
        idx = d.index
        i, n = 0, len(d)
        while i < n - 2:
            if not sig[i]:
                i += 1; continue
            j = i + 1
            if H[j] <= H[i]:
                i += 1; continue
            entry = max(O[j], H[i]); stop = L[i]
            risk = entry - stop
            if risk <= 0 or risk / entry > 0.03:
                i += 1; continue
            half_done, r_sum, k, why = False, 0.0, j, None
            while k < min(n, j + 40):
                hit = L[k] <= stop
                if k == j and hit and optimistic:
                    hit = O[j] > H[i] or C[j] <= stop
                if hit:
                    px = min(O[k], stop) if k > j else stop
                    r_sum += (0.5 if half_done else 1.0) * (px - entry) / risk; why = "stop"; break
                if not half_done and H[k] >= entry + 3 * risk:
                    r_sum += 0.5 * 3.0; half_done = True
                if k > j and C[k] < E9[k] and k + 1 < n:
                    r_sum += (0.5 if half_done else 1.0) * (O[k + 1] - entry) / risk; why = "ema9"; k += 1; break
                k += 1
            else:
                r_sum += (0.5 if half_done else 1.0) * (C[min(k, n - 1)] - entry) / risk; why = "time"
            days = k - j
            r_sum -= (COST * entry + FIN * days * entry) / risk
            trades.append(dict(sym=sym, date=idx[j], R=r_sum, why=why, days=days, cost=(COST * entry + FIN * days * entry) / risk))
            i = k + 1
    return pd.DataFrame(trades)


def main():
    data = load()
    for name, opt in (("pessimistic", False), ("optimistic", True)):
        T = run(data, opt)
        t = T.R.mean() / T.R.std(ddof=1) * np.sqrt(len(T))
        dd = T.groupby("date").R.sum()
        td = dd.mean() / dd.std() * np.sqrt(len(dd))
        y = T.groupby(T.date.dt.year).R.mean()
        print(f"{name:12s} trades {len(T)}  mean {T.R.mean():+.3f}R  t {t:.2f}  daily-portfolio t {td:.2f}  win {(T.R > 0).mean():.1%}  "
              f"entry-day stops {((T.why == 'stop') & (T.days == 0)).sum()}  years positive {(y > 0).sum()}/{len(y)}")
        g = T.R + T.cost
        print(f"{'':12s} before costs {g.mean():+.3f}R  t {g.mean() / g.std(ddof=1) * np.sqrt(len(g)):.2f}  mean cost {T.cost.mean():.3f}R")
        T.to_csv(os.path.join(HERE, f"_luk_bounds_20261019_{name}.csv"), index=False)


if __name__ == "__main__":
    main()
