"""The test-after-breakout (LPS / LPSY), and book 2's central claim about WHERE it happens.

This is the trade every case study in both books ends on, and the last major untested one:

  book 1 ch.19-20   after the breakout the move is only POTENTIAL; the confirmation is the
                    test back into the edge - Last Point of Support / Supply
  book 2 5.8.3      "if price tries to enter a value area and fails, being rejected at the
                    VA extreme, it is likely to initiate an imbalance in that direction"
  book 2 5.7.1      the level to wait at is not the price high/low - it is the VOLUME
                    PROFILE level of the structure: VAH, VAL, then the VPOC

That last sentence is a testable A/B and it is the whole reason to own a volume profile.
Same breakout, same stop, same target - only the RETEST LOCATION differs:

    A. the price edge of the range   (what a chart-only trader waits at)
    B. the value-area edge           (what book 2 says is better)
    C. the range's VPOC              (book 2's second level)

If book 2 is right, B and C beat A. If all three are the same, the volume profile is
decoration on this trade and the earlier magnet result (+0.5pp, ~0.01 R) was already the
whole story.

FILL MODEL: every entry is a MARKET ORDER at the next bar's open after the retest bar
closes. No level is ever used as an entry price, so the fill-timing artifact that invalidated
the previous round cannot occur.

NULL: entry, stop and target fixed at entry, so a driftless market gives expR = 0 exactly.
"""
import numpy as np, sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from test_retail import load_bars, race_px
from test_cause_effect import atr_of, find_ranges
from vplevels import broker_day

SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500')


def range_profile(sym, tf):
    """Volume profile of the bars INSIDE each detected range, so VAH/VAL/VPOC describe the
    structure itself rather than a calendar session. Built from the same tick histograms."""
    try:
        from vplevels import load
        from profiles import value_area
        day, b, tk, dw, binsize = load(sym)
    except FileNotFoundError:
        return None
    #--- sort once by day so a date window is a SLICE. Re-masking 6M cells per event turns
    #--- this test from seconds into hours.
    o = np.argsort(day, kind='stable')
    day, b, tk = day[o], b[o], tk[o]
    return day, b, tk, binsize, value_area


def levels_for(day, b, tk, binsize, value_area, d0, d1):
    """VPOC/VAL/VAH over broker-days [d0, d1]. Prices, not bins."""
    s = np.searchsorted(day, d0, 'left'); e = np.searchsorted(day, d1, 'right')
    if e - s < 20:
        return None
    bb, ww = b[s:e], tk[s:e]
    k, inv = np.unique(bb, return_inverse=True)
    agg = np.bincount(inv, weights=ww, minlength=len(k))
    full = np.arange(k[0], k[-1] + 1)
    dense = np.zeros(len(full)); dense[k - k[0]] = agg
    poc, lo, hi = value_area(full, dense)
    return poc * binsize, lo * binsize, hi * binsize


def run(sym, tf='H1', theta=0.60, tol=0.35, wait=60, H=200, path_tf='M5', kR=2.0):
    a1, I1 = load_bars(sym, tf)
    g = lambda k: a1[:, I1[k]]
    o, h, l, c = g('open'), g('high'), g('low'), g('close')
    spm = g('spread_mean')
    t1 = a1[:, I1['time']].astype(np.int64)
    n = len(c)
    atr = atr_of(h, l, c, 14); atr = np.concatenate([[atr[0]], atr[:-1]])
    L, hi_, lo_ = find_ranges(h, l, c, atr, theta=theta)
    a2, I2 = load_bars(sym, path_tf)
    ph, pl, pc = a2[:, I2['high']], a2[:, I2['low']], a2[:, I2['close']]
    pmap = np.searchsorted(a2[:, I2['time']], t1)
    step = 12 if tf == 'H1' else (3 if tf == 'M15' else 1)
    HH = H * step
    bd = broker_day(t1)
    RP = range_profile(sym, tf)

    up = (L > 0) & (c > hi_); dn = (L > 0) & (c < lo_)
    fire = np.nonzero(up | dn)[0]
    fire = fire[(fire > 300) & (fire < n - wait - 5)]
    if not len(fire):
        return {}
    d = np.where(up[fire], 1, -1)

    res = {}
    for tag in ('A price edge', 'B value-area edge', 'C range VPOC'):
        ev = []
        busy = -1
        for q in range(len(fire)):
            i = fire[q]
            if i <= busy:
                continue
            dd = d[q]
            edge = hi_[i] if dd > 0 else lo_[i]
            lvl = edge
            if tag != 'A price edge':
                if RP is None:
                    continue
                day, b, tk, binsize, va = RP
                got = levels_for(day, b, tk, binsize, va,
                                 int(bd[max(i - L[i], 0)]), int(bd[i]))
                if got is None:
                    continue
                poc, val, vah = got
                lvl = poc if tag == 'C range VPOC' else (vah if dd > 0 else val)
                if not np.isfinite(lvl) or lvl <= 0:
                    continue
            #--- retest: price comes back to within tol*ATR of the level, then CLOSES back
            #--- in the breakout direction. Entry is the NEXT bar's open.
            j = -1
            for k in range(1, wait + 1):
                b_ = i + k
                if b_ >= n - 2:
                    break
                near = (l[b_] <= lvl + tol * atr[i]) if dd > 0 else \
                       (h[b_] >= lvl - tol * atr[i])
                if near and ((c[b_] > lvl) if dd > 0 else (c[b_] < lvl)):
                    j = b_
                    break
                #--- a decisive close through the level kills the setup
                if (c[b_] < lvl - tol * atr[i]) if dd > 0 else (c[b_] > lvl + tol * atr[i]):
                    break
            if j < 0:
                continue
            e = j + 1
            pi = int(pmap[e]) if e < n else -1
            if pi < 0 or pi + HH >= len(ph):
                continue
            ent = o[e]
            ext = l[j] if dd > 0 else h[j]
            stop = ext - dd * 0.10 * atr[i]
            risk = abs(ent - stop)
            if risk <= 2 * spm[e]:
                continue
            ev.append((e, pi, dd, ent, stop, risk, spm[e]))
            busy = e + L[i]
        if len(ev) < 60:
            res[tag] = None
            continue
        E = np.array([x[0] for x in ev]); P = np.array([x[1] for x in ev])
        D = np.array([x[2] for x in ev]); EN = np.array([x[3] for x in ev])
        ST = np.array([x[4] for x in ev]); RK = np.array([x[5] for x in ev])
        SP = np.array([x[6] for x in ev])
        r = race_px(ph, pl, P, D, ST, EN + D * kR * RK, HH)
        R = np.where(r > 0, kR, np.where(r < 0, -1.0, 0.0))
        un = r == 0
        if un.any():
            q2 = np.minimum(P[un] + HH, len(pc) - 1)
            R[un] = (pc[q2] - EN[un]) * D[un] / RK[un]
        R = R - SP / RK
        res[tag] = R
    return res


if __name__ == '__main__':
    syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS)
    print("=== TEST-AFTER-BREAKOUT (LPS/LPSY): does the VOLUME PROFILE level beat the "
          "price level? ===")
    print("    same breakout, same stop rule, same 2R target - only the retest location "
          "differs.\n")
    print(f"  {'symbol':>7}{'tf':>5} {'retest at':<20}{'n':>6}{'expR':>9}{'t':>8}"
          f"   folds")
    for tf in ('M15', 'H1'):
        for s in syms:
            out = run(s, tf)
            for tag in ('A price edge', 'B value-area edge', 'C range VPOC'):
                R = out.get(tag)
                if R is None or len(R) < 60:
                    print(f"  {s:>7}{tf:>5} {tag:<20}{'-':>6}")
                    continue
                se = R.std(ddof=1) / np.sqrt(len(R))
                f = [float(x.mean()) for x in np.array_split(R, 4)]
                print(f"  {s:>7}{tf:>5} {tag:<20}{len(R):>6}{R.mean():>+9.3f}"
                      f"{R.mean()/max(se,1e-12):>+8.2f}   "
                      + "".join(f"{x:+7.2f}" for x in f)
                      + f"  {sum(1 for x in f if x>0)}/4")
