#!/usr/bin/env python3
"""backtest_handbook.py — die NOCH NICHT gemessenen Konzepte aus dem WTI-Handbuch
(User-Vorgabe 2026-07-30 „lerne aus dem Handbuch Kontext für die Empfehlung").

Das Handbuch überschneidet sich zu großen Teilen mit bereits gemessenen (und
meist verworfenen) Ideen — s. Abgleich in CLAUDE.md. Hier werden nur die
**echten Lücken** getestet, alle mechanisch sauber definierbar:

  1) **PDH/PDL** (Vortages-Hoch/Tief) — „WTI reagiert extrem stark". Ein ANDERER
     Level-Typ als unsere Pivots: zeit- statt strukturbasiert. Getestet als
     Reaktions-Level (prallt der Kurs ab?) und als Sweep-Setup.
  2) **Asian Range H/L** (00–09 CET) — „wird oft in London/NY gesweept".
  3) **Discount/Premium** („kaufe nur unter 50 % der Range, verkaufe nur darüber")
     — als FILTER auf das bestehende Wave-Signal, nicht als eigener Entry.
  4) **Freitag-Nachmittag** („nach 16:00 CET keine neuen Positionen, höhere
     Loss-Rate") — ebenfalls als Filter, wie das gemessene Dead-Hours-Gate.

Methodik wie im ganzen Projekt: M5, 2 Halbjahre, Live-Exit (SL 2,0×ATR ·
Trail 1,5 ab 0,3 · BE 1,3 · Time-Stop 120 min), Kosten = echter Bar-Spread/ATR.
Filter (3/4) werden gegen die UNGEFILTERTE Basis gehalten — ein Filter taugt nur,
wenn er in BEIDEN Hälften verbessert (Lehre aus 9 verworfenen Filtern).

Aufruf:  python backtest_handbook.py [n_bars]
"""
from __future__ import annotations
import sys
from datetime import datetime, timezone

import MetaTrader5 as mt5

_SL_ATR, _TRAIL, _TRAIL_ON, _BE_ON, _TIMESTOP, _MAXH = 2.0, 1.5, 0.3, 1.3, 24, 288
_ATRMIN = 0.06
_COOL = 12
_TOUCH = 0.15      # „am Level" = innerhalb x×ATR
_SWEEP_MIN, _SWEEP_MAX = 0.05, 1.0
_EMA_F, _EMA_S = 12, 50


def _atr_series(H, L, C, p=14):
    tr = [0.0]
    for i in range(1, len(C)):
        tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
    out = [None] * len(C); run = 0.0
    for i in range(1, len(C)):
        run += tr[i]
        if i > p: run -= tr[i - p]
        out[i] = run / min(i, p)
    return out


def _ema(vals, p):
    k = 2.0 / (p + 1); o = [vals[0]]
    for v in vals[1:]: o.append(v * k + o[-1] * (1 - k))
    return o


def _sim(entry, d, atr, H, L, C, j0):
    eff = entry - d * _SL_ATR * atr; hw = entry; started = False
    end = min(j0 + _MAXH, len(C) - 1); px = C[end]
    for j in range(j0, end + 1):
        if (L[j] <= eff) if d > 0 else (H[j] >= eff): px = eff; break
        hw = max(hw, H[j]) if d > 0 else min(hw, L[j])
        prof = (C[j] - entry) * d
        if prof >= _TRAIL_ON * atr:
            started = True
            cand = hw - d * _TRAIL * atr
            if prof >= _BE_ON * atr:
                cand = max(cand, entry) if d > 0 else min(cand, entry)
            eff = max(eff, cand) if d > 0 else min(eff, cand)
        if not started and (j - j0) >= _TIMESTOP: px = C[j]; break
    return (px - entry) * d / atr


def _rep(name, Rs, ind="    "):
    if not Rs or len(Rs) < 10:
        print(f"{ind}{name:<34} n={len(Rs):>4}  (zu wenige)"); return None
    n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
    up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
    print(f"{ind}{name:<34} n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} "
          f"PF={up/dn if dn>0 else 9.99:.2f} ΣR={s:+.0f}")
    return s / n


def day_levels(T, H, L, hb):
    """Je Bar: (PDH, PDL, AsiaH, AsiaL) des jeweils VORHERGEHENDEN Tages bzw. der
    heutigen, ABGESCHLOSSENEN Asia-Session (00–09 CET). Kein Look-ahead."""
    n = len(T)
    day = [datetime.fromtimestamp(t, timezone.utc).date() for t in T]
    pdh = [None] * n; pdl = [None] * n; ah = [None] * n; al = [None] * n
    cur = day[0]; hi = H[0]; lo = L[0]
    prev_hi = prev_lo = None
    a_hi = a_lo = None; a_done_hi = a_done_lo = None
    for i in range(n):
        if day[i] != cur:                      # neuer Tag → Vortag einfrieren
            prev_hi, prev_lo = hi, lo
            cur = day[i]; hi, lo = H[i], L[i]
            a_hi = a_lo = None; a_done_hi = a_done_lo = None
        else:
            hi = max(hi, H[i]); lo = min(lo, L[i])
        if hb[i] < 9:                          # Asia-Session läuft (00–09 CET)
            a_hi = H[i] if a_hi is None else max(a_hi, H[i])
            a_lo = L[i] if a_lo is None else min(a_lo, L[i])
        elif a_hi is not None and a_done_hi is None:
            a_done_hi, a_done_lo = a_hi, a_lo   # Asia vorbei → einfrieren
        pdh[i], pdl[i] = prev_hi, prev_lo
        ah[i], al[i] = a_done_hi, a_done_lo
    return pdh, pdl, ah, al


def scan_level(H, L, C, SP, A, lvl_hi, lvl_lo, lo_i, hi_i, mode):
    """mode 'bounce': am Level in die Gegenrichtung (Level hält).
       mode 'sweep' : Level kurz durchstochen, Close zurück → Gegenrichtung."""
    Rs = []
    def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225) / atr
    i = max(lo_i, 5)
    hi_i = min(hi_i, len(C) - _MAXH - 2)
    while i < hi_i:
        atr = A[i]
        if not atr or atr < _ATRMIN: i += 1; continue
        hit = None
        for lv, d in ((lvl_hi[i], -1), (lvl_lo[i], 1)):   # oben→short, unten→long
            if lv is None: continue
            if mode == "bounce":
                if abs(C[i] - lv) <= _TOUCH * atr: hit = (lv, d); break
            else:
                over = (H[i] - lv) if d < 0 else (lv - L[i])
                back = (C[i] < lv) if d < 0 else (C[i] > lv)
                if _SWEEP_MIN * atr <= over <= _SWEEP_MAX * atr and back:
                    hit = (lv, d); break
        if hit is None: i += 1; continue
        _lv, d = hit
        Rs.append(_sim(C[i], d, atr, H, L, C, i + 1) - cost(i, atr))
        i += _COOL
    return Rs


def scan_signal(H, L, C, SP, A, EF, ES, hb, wd, lo_i, hi_i, mode):
    """Basis-Signal (EMA12/50-Trend, wie die Welle) mit optionalem Filter:
       'base' · 'discount' (nur im günstigen Range-Drittel) · 'nofriday'."""
    Rs = []
    def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225) / atr
    i = max(lo_i, 60)
    hi_i = min(hi_i, len(C) - _MAXH - 2)
    while i < hi_i:
        atr = A[i]
        if not atr or atr < _ATRMIN: i += 1; continue
        d = 1 if EF[i] > ES[i] + 0.15 * atr else -1 if EF[i] < ES[i] - 0.15 * atr else 0
        if d == 0: i += 1; continue
        if mode == "nofriday" and wd[i] == 4 and hb[i] >= 16:
            i += 1; continue                    # Freitag ab 16:00 CET
        if mode == "discount":
            # Range der letzten 60 Bars; LONG nur unter 50 %, SHORT nur darüber
            rh = max(H[i - 60:i]); rl = min(L[i - 60:i])
            if rh <= rl: i += 1; continue
            pos = (C[i] - rl) / (rh - rl)
            if (d > 0 and pos > 0.5) or (d < 0 and pos < 0.5):
                i += 1; continue
        Rs.append(_sim(C[i], d, atr, H, L, C, i + 1) - cost(i, atr))
        i += _COOL
    return Rs


def main():
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
    mt5.initialize(); sym = None
    for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
        if mt5.symbol_info(c): sym = c; break
    bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
    si = mt5.symbol_info(sym); point = si.point
    mt5.shutdown()
    H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
    C = [float(b["close"]) for b in bars]; SP = [float(b["spread"]) * point for b in bars]
    T = [int(b["time"]) for b in bars]
    A = _atr_series(H, L, C); EF = _ema(C, _EMA_F); ES = _ema(C, _EMA_S)
    # Broker UTC+3 → Berlin = Broker − 1 h
    hb = [(datetime.fromtimestamp(t, timezone.utc).hour - 1) % 24 for t in T]
    wd = [datetime.fromtimestamp(t - 3600, timezone.utc).weekday() for t in T]
    PDH, PDL, AH, AL = day_levels(T, H, L, hb)
    N = len(C); mid = N // 2

    print("=" * 92)
    print(f"  WTI-HANDBUCH — die noch NICHT gemessenen Konzepte ({sym} M5, {N} Bars)")
    print(f"  Live-Exit · Echtkosten · 2 Halbjahre · Maßstab Squeeze ØR +0,14…+0,23")
    print("=" * 92)

    verdict = {}
    print("\n### 1/2 — PDH/PDL und Asian Range als Reaktions-Level")
    for lbl, lh, ll in (("PDH/PDL", PDH, PDL), ("Asian Range H/L", AH, AL)):
        for mode in ("bounce", "sweep"):
            print(f"  {lbl} · {mode}")
            for hlbl, a, b in (("H1", 5, mid), ("H2", mid, N)):
                Rs = scan_level(H, L, C, SP, A, lh, ll, a, b, mode)
                verdict.setdefault((lbl, mode), []).append(_rep(f"[{hlbl}]", Rs))

    print("\n### 3/4 — Discount/Premium und Freitag-Nachmittag als FILTER")
    print("    (Basis = EMA-Trendsignal ohne Filter; ein Filter taugt nur, wenn er")
    print("     in BEIDEN Hälften verbessert)")
    for mode, lbl in (("base", "BASIS (ohne Filter)"),
                      ("discount", "nur Discount/Premium"),
                      ("nofriday", "ohne Fr ab 16:00 CET")):
        print(f"  {lbl}")
        for hlbl, a, b in (("H1", 60, mid), ("H2", mid, N)):
            Rs = scan_signal(H, L, C, SP, A, EF, ES, hb, wd, a, b, mode)
            verdict.setdefault(("filter", lbl), []).append(_rep(f"[{hlbl}]", Rs))

    print("\n" + "=" * 92)
    print("  URTEIL")
    base = verdict.get(("filter", "BASIS (ohne Filter)"))
    for key, vals in verdict.items():
        if len(vals) != 2 or any(v is None for v in vals): continue
        if key[0] == "filter":
            if key[1].startswith("BASIS"): continue
            ok = all(vals[i] > base[i] for i in (0, 1))
            print(f"  Filter {key[1]:<28} H1 {vals[0]:+.3f} (Basis {base[0]:+.3f}) · "
                  f"H2 {vals[1]:+.3f} (Basis {base[1]:+.3f})  "
                  f"{'✅ verbessert beide' if ok else '❌ nicht robust'}")
        else:
            ok = all(v > 0 for v in vals); band = all(v >= 0.14 for v in vals)
            mark = "✅ TRÄGT" if (ok and band) else ("⚠ positiv, unter Band" if ok
                                                    else "❌ fällt durch")
            print(f"  {key[0]} · {key[1]:<22} H1 {vals[0]:+.3f} · H2 {vals[1]:+.3f}  {mark}")


if __name__ == "__main__":
    main()
