#!/usr/bin/env python3 """Halten Fibonacci-Retracements? (2026-08-04) Anlass: Kommentar zu USOIL — „Ein Fibonacci-Retracement sollte den Kurs vor dem nächsten Aufwärtsimpuls auf 79 $ drücken." Fibonacci-Level sind in diesem Projekt **nie gemessen worden**, weder als S/R noch als Einstiegszone. Diese Lücke wird hier geschlossen. ⚠ Ausgangslage ist zwiespältig, und das ist der Grund für die Kontrollgruppen: `backtest_structure_tf.py` hat gezeigt, dass **gerechnete Linien** (Regressions- kanal) NICHT halten — 24/24 Zellen negativ, weil hinter einer gerechneten Linie keine Orders liegen. Ein Fib-Level ist ebenfalls gerechnet. **ABER** Fibonacci wird von sehr vielen Marktteilnehmern beobachtet — das Selbsterfüllungs-Argument, das beim Regressionskanal fehlt. Ob es trägt, entscheidet der Vergleich gegen: (a) ZUFALLS-Level im selben Abstand → misst „ist Fib besser als irgendein Level?" (b) M30-PIVOTS (was der Bot heute nutzt) → misst „ist Fib besser als Struktur?" Zwei Teile: TEIL A — HALTEQUOTE: prallt der Kurs an einem Fib-Level häufiger ab als an den Kontroll-Leveln? Barriere-Rennen wie beim P(break)-Training (±0,5×ATR innerhalb von 12 M5-Bars, Timeout zählt als Abprall). TEIL B — HANDELBARKEIT: Bounce-Einstieg am Level, kanonischer Live-Exit, Echtkosten, 2 Halbjahre. Schwünge stammen aus **M15**-Pivots (die im Kommentar genannte Zeitebene), Berührungen und Trades laufen auf **M5** (dort ist der Live-Exit kalibriert). ⚠ Regel VORAB fixiert: Fib gilt nur als tragfähig, wenn die Haltequote in BEIDEN Hälften über BEIDEN Kontrollen liegt UND Teil B in beiden Hälften ØR>0 mit PF>1 liefert. Ein einzelner besserer Wert zählt nicht. Aufruf: python backtest_fibonacci.py [bars] """ import random import sys import MetaTrader5 as mt5 from core.analysis import FIB_RATIOS from core.exit_model import LIVE, simulate _ATRMIN = 0.06 _TOUCH = 0.15 # Berührung: |Kurs − Level| ≤ x×ATR (wie P(break)-Training) _BARRIER = 0.5 # Ausgang: ±x×ATR (wie P(break)-Training) _WIN = 12 # Fenster in M5-Bars (= 60 min, wie P(break)-Training) _PIVK = 3 # Pivot-Fenster _MIN_SWING_ATR = 2.0 # Schwung muss mind. so groß sein, sonst kein Retracement _COOL = 6 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]))) return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1]))) if i else None for i in range(len(C))] def _m15_swings(H, L, n): """M15-Pivots (k=3) → Liste (bekannt_ab_m5_index, tief, hoch, richtung). Kausal: ein M15-Pivot bei Bar j ist erst bei j+_PIVK bestätigt; der entsprechende M5-Index ist (j+_PIVK)*3. """ HH = [max(H[i:i + 3]) for i in range(0, n - 2, 3)] LL = [min(L[i:i + 3]) for i in range(0, n - 2, 3)] piv = [] for j in range(_PIVK, len(HH) - _PIVK): if HH[j] == max(HH[j - _PIVK:j + _PIVK + 1]): piv.append((j, HH[j], "H")) elif LL[j] == min(LL[j - _PIVK:j + _PIVK + 1]): piv.append((j, LL[j], "L")) swings = [] for a, b in zip(piv, piv[1:]): if a[2] == b[2]: continue bekannt = (b[0] + _PIVK) * 3 # erst dann im M5-Index nutzbar if a[2] == "L": swings.append((bekannt, a[1], b[1], +1)) # Aufwärts-Schwung else: swings.append((bekannt, b[1], a[1], -1)) # Abwärts-Schwung return swings def _aktiver_swing(swings, idx, i): """Letzter Schwung, der bei M5-Bar i bereits bekannt ist.""" while idx + 1 < len(swings) and swings[idx + 1][0] <= i: idx += 1 return idx def _ausgang(H, L, i, level, d_app, atr): """Barriere-Rennen. d_app = Richtung, aus der der Kurs kam (+1 = von oben). → 'hold' (prallt zurück) oder 'break' (läuft durch). Timeout = hold.""" zurueck = level + d_app * _BARRIER * atr durch = level - d_app * _BARRIER * atr for j in range(i + 1, min(i + 1 + _WIN, len(H))): if d_app > 0: if H[j] >= zurueck: return "hold" if L[j] <= durch: return "break" else: if L[j] <= zurueck: return "hold" if H[j] >= durch: return "break" return "hold" def _quote(res): n = len(res) return (n, 100.0 * sum(1 for x in res if x == "hold") / n) if n else (0, 0.0) def main(): n_req = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 random.seed(20260804) mt5.initialize() sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") if mt5.symbol_info(c)), None) bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n_req) point = mt5.symbol_info(sym).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] A = _atr_series(H, L, C) N = len(C); mid = N // 2 swings = _m15_swings(H, L, N) # M30-Pivots als Kontrolle (b) — dieselbe Quelle, die der Bot live nutzt HH30 = [max(H[i:i + 6]) for i in range(0, N - 5, 6)] LL30 = [min(L[i:i + 6]) for i in range(0, N - 5, 6)] p30 = [] for j in range(_PIVK, len(HH30) - _PIVK): if HH30[j] == max(HH30[j - _PIVK:j + _PIVK + 1]): p30.append(((j + _PIVK) * 6, HH30[j])) elif LL30[j] == min(LL30[j - _PIVK:j + _PIVK + 1]): p30.append(((j + _PIVK) * 6, LL30[j])) print("=" * 94) print(f" FIBONACCI-RETRACEMENTS — {sym} · Schwünge aus M15, Berührung/Trade auf M5") print(f" {N} Bars · 2 Halbjahre · Berührung ≤{_TOUCH}×ATR · Ausgang ±{_BARRIER}×ATR " f"in {_WIN} Bars") print(f" Kontrollen: (a) Zufalls-Level gleicher Distanz (b) M30-Pivots (Live-Quelle)") print("=" * 94) for lbl, lo_i, hi_i in (("H1 (alt)", 0, mid), ("H2 (neu)", mid, N - _WIN - 2)): res_fib, res_rnd, res_piv = [], [], [] trades = [] letzte = {} si = 0; pi = 0 i = max(lo_i, 200) while i < hi_i: atr = A[i] if not atr or atr < _ATRMIN: i += 1; continue si = _aktiver_swing(swings, si, i) if si >= len(swings) or swings[si][0] > i: i += 1; continue _, slo, shi, sdir = swings[si] if (shi - slo) < _MIN_SWING_ATR * atr: i += 1; continue while pi + 1 < len(p30) and p30[pi + 1][0] <= i: pi += 1 # --- Fib-Level des aktiven Schwungs for r in FIB_RATIOS: lvl = shi - (shi - slo) * r if sdir > 0 else slo + (shi - slo) * r if abs(C[i] - lvl) > _TOUCH * atr: continue key = ("fib", round(lvl, 2)) if i - letzte.get(key, -999) < _COOL: continue letzte[key] = i d_app = 1 if C[i - 1] > lvl else -1 res_fib.append(_ausgang(H, L, i, lvl, d_app, atr)) # TEIL B: Bounce-Trade zurück in die Anlaufrichtung d = d_app cost = (SP[i] if SP[i] > 0 else 0.0225) / atr r_, _xj = simulate(C[i], d, atr, H, L, C, i + 1, LIVE, timestop=True, use_tp=True, ret_bar=True) trades.append(r_ - cost) # --- Kontrolle (a): Zufalls-Level im selben Abstandsbereich lvl_r = C[i] + random.uniform(-_TOUCH, _TOUCH) * atr key = ("rnd", i // _COOL) if key not in letzte: letzte[key] = i d_app = 1 if C[i - 1] > lvl_r else -1 res_rnd.append(_ausgang(H, L, i, lvl_r, d_app, atr)) # --- Kontrolle (b): M30-Pivot if pi < len(p30): lvl_p = p30[pi][1] if abs(C[i] - lvl_p) <= _TOUCH * atr: key = ("piv", round(lvl_p, 2)) if i - letzte.get(key, -999) >= _COOL: letzte[key] = i d_app = 1 if C[i - 1] > lvl_p else -1 res_piv.append(_ausgang(H, L, i, lvl_p, d_app, atr)) i += 1 print(f"\n {lbl}") for name, res in (("Fibonacci-Level", res_fib), ("(a) Zufalls-Level", res_rnd), ("(b) M30-Pivots", res_piv)): c, q = _quote(res) print(f" {name:<20} n={c:>6} Haltequote {q:5.1f} %") if trades: nt = len(trades); s = sum(trades) up = sum(x for x in trades if x > 0); dn = -sum(x for x in trades if x < 0) print(f" TEIL B Bounce-Trade n={nt:>6} WR " f"{100*sum(1 for x in trades if x>0)/nt:4.1f} % ØR {s/nt:+.3f} " f"PF {(up/dn if dn else 9.99):4.2f} ΣR {s:+.0f}") print("\n Regel (vorab): tragfähig nur, wenn die Haltequote in BEIDEN Hälften über") print(" BEIDEN Kontrollen liegt UND Teil B beidhälftig ØR>0 mit PF>1 liefert.") print() if __name__ == "__main__": main()