"""TR-Donchian W7-TEST: vectorbtpro run on Dukascopy H1 Bid (EURUSD, EURJPY).

Run (canonical): py -3.11 test.py   (from this folder; vectorbtpro required)
Reads working copies in ../_data (gitignored), writes results/ CSVs.
Base rule: Donchian(20) alternating lower-new-low then upper-new-high event +
SMA200 filter, SL at opposite band, fixed 1.5R target, next-bar open entry,
one position per symbol, stop-first on ambiguous H1 bars.
Costs: fixed assumed spread 1.0 pip EURUSD / 1.2 pip EURJPY + $7/lot round
trip ($0.07 at 0.01 lot); EURJPY commission at fixed USDJPY=140.
See RULES.md for frozen assumptions; RESULTS.md for measurements.
"""

import os
import sys

import vectorbtpro  # noqa: F401  (hard requirement: real engine, no fallback)

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import w7_vbt as W

STRATEGY = "tr-donchian"


def compute_all(df, p):
    """Causal indicators: Donchian bands, SMA200, ATR14 (for alt stop)."""
    ind = {}
    ind["dc_up"] = df["high"].rolling(p["dc_len"], min_periods=p["dc_len"]).max()
    ind["dc_lo"] = df["low"].rolling(p["dc_len"], min_periods=p["dc_len"]).min()
    ind["sma200"] = df["close"].rolling(200, min_periods=200).mean()
    ind["atr14"] = W.atr_wilder(df, 14)
    return ind


def _scan(o, h, low, c, up, lo, sma, atr, p, start):
    """Alternating-state scan. Fires use pre-bar state so arm and trigger can
    never occur on the same bar (strict cross-bar alternation)."""
    sigs = []
    long_armed = False
    short_armed = False
    n = len(c)
    for i in range(start, n - 1):  # need bar i+1 to enter
        u, u0, d, d0, s = up[i], up[i - 1], lo[i], lo[i - 1], sma[i]
        if not (u == u and u0 == u0 and d == d and d0 == d0 and s == s):
            continue
        new_high = u > u0  # equality is not a new high (boundary)
        new_low = d < d0  # equality is not a new low (boundary)
        if new_high and long_armed and c[i] > s:
            a = atr[i]
            sl = d if p["stop"] == "band" else c[i] - p["atr_mult"] * a
            if p["stop"] == "band" or (a == a and a > 0):
                sigs.append((i, 1, float(sl)))
                long_armed = False
        if new_low and short_armed and c[i] < s:
            a = atr[i]
            sl = u if p["stop"] == "band" else c[i] + p["atr_mult"] * a
            if p["stop"] == "band" or (a == a and a > 0):
                sigs.append((i, -1, float(sl)))
                short_armed = False
        if new_low:
            long_armed = True
        if new_high:
            short_armed = True
    return sigs


def build_signals(strategy, df, ind, p):
    if strategy != STRATEGY:
        raise ValueError(strategy)
    o = df["open"].to_numpy()
    h = df["high"].to_numpy()
    low = df["low"].to_numpy()
    c = df["close"].to_numpy()
    return _scan(o, h, low, c, ind["dc_up"].to_numpy(), ind["dc_lo"].to_numpy(),
                 ind["sma200"].to_numpy(), ind["atr14"].to_numpy(),
                 p, max(W.WARMUP_BARS, 1))


def _unit_tests():
    """Positive/negative/boundary checks on the real _scan (hand-built bars)."""
    import numpy as np

    n = 30
    o = np.full(n, 1.10)
    h = np.full(n, 1.101)
    low = np.full(n, 1.099)
    c = np.full(n, 1.10)
    sma = np.full(n, 1.05)  # price above filter
    atr = np.full(n, 0.002)
    up = np.full(n, 1.101)
    lo = np.full(n, 1.099)
    lo[10] = 1.095  # lower band steps down -> arms long side
    up[15] = 1.105  # upper band steps up -> trigger bar
    h[15] = 1.105
    c[15] = 1.104
    p = {"stop": "band", "atr_mult": 2.0}
    sigs = _scan(o, h, low, c, up, lo, sma, atr, p, 1)
    assert [(i, d) for (i, d, _) in sigs] == [(15, 1)], sigs
    assert abs(sigs[0][2] - 1.099) < 1e-12, sigs  # SL = opposite (lower) band
    print("unit donchian positive long: PASS (signal bar 15, SL=lower band)")

    c2 = c.copy()
    c2[15] = 1.04  # below SMA200 -> filter blocks
    assert _scan(o, h, low, c2, up, lo, sma, atr, p, 1) == [], "filter leak"
    print("unit donchian negative (SMA200 filter): PASS (no signal)")

    up3 = up.copy()
    up3[15] = 1.101  # equality: not a new high -> no trigger (boundary)
    assert _scan(o, h, low, c, up3, lo, sma, atr, p, 1) == [], "boundary leak"
    print("unit donchian boundary (equal band, no new high): PASS (no signal)")

    # short mirror: upper steps up at 10 (arms short), lower steps down at 15
    sma4 = np.full(n, 1.15)  # price below filter
    up4 = np.full(n, 1.101)
    lo4 = np.full(n, 1.099)
    up4[10] = 1.108
    lo4[15] = 1.092
    low4 = low.copy()
    low4[15] = 1.092
    c4 = np.full(n, 1.10)
    c4[15] = 1.093
    sigs4 = _scan(o, h, low4, c4, up4, lo4, sma4, atr, p, 1)
    assert [(i, d) for (i, d, _) in sigs4] == [(15, -1)], sigs4
    assert abs(sigs4[0][2] - 1.101) < 1e-12, sigs4  # SL = opposite (upper) band
    print("unit donchian positive short mirror: PASS (signal bar 15)")


W.compute_all = compute_all
W.build_signals = build_signals

VARIANTS = [
    dict(id="base", dc_len=20, stop="band", atr_mult=2.0),
    dict(id="dc10", dc_len=10, stop="band", atr_mult=2.0),
    dict(id="atrstop", dc_len=20, stop="atr", atr_mult=2.0),
]

if __name__ == "__main__":
    _unit_tests()
    outdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "results")
    rows = W.run_strategy(STRATEGY, VARIANTS, outdir, use_vbt=True)
    for s in rows:
        print("%s %-8s n=%d TP=%d SL=%d END=%d wr=%.2f%% be=%.2f%% effR=%s ddR=%s IS=%d/%.0f%% OOS=%d/%.0f%% both=%d" % (
            s["scope"], s["variant"], s["n_trades"], s["n_tp"], s["n_sl"], s["n_end"],
            100 * s["win_rate"], 100 * s["breakeven_win_rate"], s["effective_R"], s["maxdd_R"],
            s["is_n"], 100 * s["is_winrate"], s["oos_n"], 100 * s["oos_winrate"], s["both_hit_trades"]))
    print("python", sys.version.split()[0], "vbt", rows[0]["vbt_version"],
          "reconciled", rows[0]["vbt_reconciled"])
