#!/usr/bin/env python3 """Fade der Klimax-Kerze: langer Körper + hohes Volumen GEGEN die Körperrichtung. Folgetest zu `backtest_candles.py` (2026-07-31). Dort ergab die reine INFORMATIONS- Messung (Forward-Return 60 min) den einzigen robusten Lead des Laufs: Langer Körper Überschuss -0,059 / -0,076 (beide Hälften) ... + Volumen >= 2x Ø Überschuss -0,117 / -0,204 (monoton verstärkt) d. h. nach einer volumenstarken großen Kerze läuft der Kurs GEGEN sie — das Gegenteil der Lehrbuch-Lesart („2× Volumen = echt"). Brutto +0,117/+0,204 beim Fade, real kosten aber ~0,265×ATR → unter der Schwelle. OFFEN blieb, ob ein **Live-Exit** mehr holt als das starre 60-min-Fenster (bei den Chartmustern machte genau das den Unterschied zwischen negativ und +0,43). DESHALB HIER: echter Trade-Sim mit dem LIVE-Exit (Initial-SL 2,0×ATR · Trail 1,5 ab 0,3 · Breakeven 1,3 · Lock ab 3,5 · Time-Stop 120 min) und ECHTKOSTEN (Bar-Spread/ATR). ⚠ ENTSCHEIDEND — die KONTROLLGRUPPE: „Fade OHNE Volumen-Bedingung". Ohne sie wäre nicht unterscheidbar, ob der Ertrag vom Volumen kommt oder ob schlicht JEDER Fade einer großen Kerze + Trailing funktioniert (der Kontrollfehler, der in `backtest_patterns.py` unentdeckt blieb und den Muster-„Edge" als Exit-Artefakt entlarvte, als er nachgeholt wurde). Urteil „trägt" nur, wenn (a) in BEIDEN Hälften ØR netto > 0, (b) die Volumen-Variante die Kontrolle in BEIDEN Hälften schlägt, (c) benachbarte Volumen-Schwellen nicht das Vorzeichen kippen. """ import sys from statistics import median import MetaTrader5 as mt5 _MAXH = 200 # max. Haltedauer in Bars _ATRMIN = 0.06 # Live-Floor _VOL_N = 20 # Fenster für Ø-Volumen / Median-Body _COOL = 12 # Cooldown nach einem Trade (keine Überlappung) # Live-Exit _SL = 2.0; _TRAIL = 1.5; _TRAIL_START = 0.3; _BE = 1.3 _LOCK_START = 3.5; _LOCK_SCALE = 0.6; _LOCK_MIN = 1.2 _TIMESTOP = 24 # 120 min = 24 M5-Bars, greift nur in Phase „Init" def _atr_series(H, L, C, p=14): t = [0.0] for i in range(1, len(C)): t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) out = [] for i in range(len(C)): if not i: out.append(None); continue w = t[max(1, i - p + 1):i + 1] out.append(sum(w) / max(1, len(w))) return out def _ema(v, p): k = 2.0 / (p + 1); o = []; e = v[0] for i, x in enumerate(v): e = x if i == 0 else x * k + e * (1 - k) o.append(e) return o def sim(entry, d, atr, H, L, C, j0): """LIVE-Exit-Modell (identisch zu backtest_trailing_sr.py).""" sl = entry - d * _SL * atr hw = entry; rank = 0 end = min(j0 + _MAXH, len(C) - 1) for j in range(j0, end + 1): hi, lo = H[j], L[j] if (lo <= sl) if d > 0 else (hi >= sl): return (sl - entry) * d / atr hw = max(hw, hi) if d > 0 else min(hw, lo) profit = (hw - entry) * d ph = 0 if profit < _TRAIL_START * atr else (1 if profit < _LOCK_START * atr else 2) if ph < rank: ph = rank rank = ph # Time-Stop: nach 120 min noch in Phase „Init" (nie 0,3×ATR im Plus) if rank == 0 and (j - j0) >= _TIMESTOP: return (C[j] - entry) * d / atr if ph == 0: continue m = _TRAIL if ph == 1 else max(_LOCK_MIN, _TRAIL * _LOCK_SCALE) cand = hw - d * m * atr if profit >= _BE * atr or ph == 2: cand = max(cand, entry) if d > 0 else min(cand, entry) sl = max(sl, cand) if d > 0 else min(sl, cand) return (C[end] - entry) * d / atr def st(Rs): if not Rs: return "Trades= 0" 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) pf = up / dn if dn > 0 else 9.99 return (f"Trades={n:>5} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={pf:>4.2f} " f"Worst={min(Rs):+.2f} ΣR={s:+.0f}") def scan(O, H, L, C, V, A, SP, EF, ES, lo_i, hi_i, vol_x, body_x=1.5, br_min=0.60, only_with_trend=False): """vol_x = 0 → KONTROLLE (kein Volumen-Filter).""" Rs = [] i = max(lo_i, _VOL_N + 2) while i < min(hi_i, len(C) - _MAXH - 2): atr = A[i] if not atr or atr < _ATRMIN: i += 1; continue rng = H[i] - L[i] body = abs(C[i] - O[i]) if rng <= 0 or body / rng < br_min: i += 1; continue bref = median([abs(C[k] - O[k]) for k in range(i - _VOL_N, i)]) if bref <= 0 or body < body_x * bref: i += 1; continue if vol_x > 0: vref = sum(V[i - _VOL_N:i]) / _VOL_N if vref <= 0 or V[i] < vol_x * vref: i += 1; continue d_body = 1 if C[i] > O[i] else -1 if only_with_trend: trend = 1 if EF[i] > ES[i] else -1 if d_body != trend: # nur Klimax IM Trend i += 1; continue d = -d_body # FADE: gegen die Körperrichtung entry = C[i] # Einstieg am Close der Signal-Kerze cost = (SP[i] if SP[i] > 0 else 0.0225) / atr Rs.append(sim(entry, d, atr, H, L, C, i + 1) - cost) i += _COOL return Rs def main(): n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 mt5.initialize(); sym = None for cand in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): if mt5.symbol_info(cand): sym = cand; break bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) si = mt5.symbol_info(sym); point = si.point mt5.shutdown() O = [float(b["open"]) for b in bars]; H = [float(b["high"]) for b in bars] L = [float(b["low"]) for b in bars]; C = [float(b["close"]) for b in bars] V = [float(b["tick_volume"]) for b in bars] SP = [float(b["spread"]) * point for b in bars] A = _atr_series(H, L, C); EF = _ema(C, 12); ES = _ema(C, 50) N = len(C); mid = N // 2 halves = [("H1", 100, mid), ("H2", mid, N)] print("=" * 104) print(f" KLIMAX-FADE — {sym} M5 ({N} Bars) · Live-Exit (SL 2,0 · Trail 1,5 ab 0,3 · " f"BE 1,3 · Time-Stop 120') · Echtkosten") print(" Einstieg GEGEN einen langen Körper (Body/Range>=0,60, Body>=1,5x Median)") print("=" * 104) variants = [ ("KONTROLLE: Fade ohne Volumen-Filter", dict(vol_x=0)), ("Volumen >= 1,5x Ø", dict(vol_x=1.5)), ("Volumen >= 2,0x Ø", dict(vol_x=2.0)), ("Volumen >= 2,5x Ø", dict(vol_x=2.5)), ("Volumen >= 3,0x Ø", dict(vol_x=3.0)), ("Vol >= 2,0x + nur IM Trend", dict(vol_x=2.0, only_with_trend=True)), ("Vol >= 2,0x + Marubozu (BR>=0,90)", dict(vol_x=2.0, br_min=0.90)), ] for name, kw in variants: print(f"\n{name}") for lbl, a, b in halves: Rs = scan(O, H, L, C, V, A, SP, EF, ES, a, b, **kw) print(f" {lbl} {st(Rs)}") if __name__ == "__main__": main()