"""_luk_momentum_pullback_20261019.py - daily-bar approximation of Martin Luk's setup (hb_stocks X thread 2026-10-19). DISCOVERY ONLY (<= 2020-12-31).

Written by: Claude (2026-10-19) on Kaloyan's request. EMPIRICAL RESEARCH, NOT LAW / NOT DEPLOYED. Rules fixed before the run, from the thread:
  universe   Tiingo daily, the 503 CURRENT S&P 500 members (data/tiingo_daily): SURVIVORSHIP-BIASED (today's winners), so results are an upper bound.
             FTMO lists only ~40 US mega caps as CFDs; Luk trades small-cap leaders that FTMO does not carry.
  step 1     strong stock: 126-day return >= +30% AND in the top 20% of the universe that day; 20-day median dollar volume >= $20M
  resting    the last two pullback lows rising (low of the last 5 days > low of days 6-15); 10-day ATR% < 30-day ATR%;
             close above EMA9 and EMA21
  zone       the day's low within 1.5% of EMA21 or EMA9 (the retest), i.e. a pullback into support
  trigger    next day trades above the signal day's high (daily stand-in for the 1-15 minute turn): buy-stop at that high (gap = open)
  stop       signal day's low, risk capped at 3% of price (skip wider); R = entry - stop
  exits      sell half at +3R; the rest exits on the first close below EMA9 (at the next open); hard time stop 40 days; stop always live
  costs      0.10% round trip (CFD spread) + 2.1%/yr financing on the notional
Reported per trade in R (the half sold at 3R counts half). One position per stock at a time.
"""
from __future__ import annotations

import glob
import os

import numpy as np
import pandas as pd

HERE = os.path.dirname(os.path.abspath(__file__))
LAB = os.path.dirname(HERE)
END = pd.Timestamp("2020-12-31")
COST, FIN = 0.0010, 0.021 / 252


def load() -> dict[str, pd.DataFrame]:
    out = {}
    for f in glob.glob(os.path.join(LAB, "data", "tiingo_daily", "*.parquet")):
        d = pd.read_parquet(f)
        d["date"] = pd.to_datetime(d["date"]).dt.tz_localize(None)
        d = d.set_index("date").sort_index()
        d = d[d.index <= END][["adjOpen", "adjHigh", "adjLow", "adjClose", "adjVolume", "close", "volume"]].dropna()
        d.columns = ["o", "h", "l", "c", "v", "craw", "vraw"]
        if len(d) > 300:
            out[os.path.basename(f)[:-8]] = d
    return out


def main():
    data = load()
    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, o = d.c, d.h, d.l, d.o
        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 = h.to_numpy(), l.to_numpy(), o.to_numpy(), 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]:                                  # no trigger the next day
                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):
                if L[k] <= stop and not (k == j and O[j] > stop and False):
                    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, risk_pct=risk / entry, half=half_done))
            i = k + 1
    T = pd.DataFrame(trades)
    t = T.R.mean() / T.R.std(ddof=1) * np.sqrt(len(T))
    print(f"trades {len(T)}  mean {T.R.mean():+.3f}R  t {t:.2f}  win {(T.R > 0).mean():.1%}  3R partial {T.half.mean():.1%}  "
          f"median hold {T.days.median():.0f} d  median stop {T.risk_pct.median():.2%}")
    y = T.groupby(T.date.dt.year).R.agg(["size", "mean"]).round(3)
    print("by year:", y.to_dict("index"))
    print("exit mix:", T.why.value_counts().to_dict())
    d = T.groupby("date").R.sum()
    print(f"daily-portfolio: days {len(d)}, mean {d.mean():+.3f}R, t {d.mean() / d.std() * np.sqrt(len(d)):.2f}")
    T.to_csv(os.path.join(HERE, "_luk_momentum_pullback_20261019_trades.csv"), index=False)


if __name__ == "__main__":
    main()
