Files
AH-Oil-Trader/backtest_fibonacci.py
Axel HocksandClaude Opus 5 a172353cee Fibonacci-Retracements gemessen — verworfen (21. Signal-Eingriff)
Anlass: USOIL-Kommentar "ein Fibonacci-Retracement sollte den Kurs auf 79 $
druecken". Fib-Level waren im Projekt nie gemessen worden. Aufbau wie beim
P(break)-Training: Schwuenge aus M15-Pivots, Beruehrung <=0,15xATR, Ausgang
+/-0,5xATR in 12 M5-Bars, 80k Bars, 2 Halbjahre, ZWEI Kontrollgruppen.

Haltequote:
                          H1       H2
  Fibonacci             54,3 %   54,5 %
  (a) Zufalls-Level     55,7 %   53,8 %
  (b) M30-Pivots        56,6 %   55,4 %

Handelbarkeit (Bounce, kanonischer Exit, Echtkosten):
  H1 OR -0,168 / PF 0,71     H2 OR -0,082 / PF 0,85

Der aufschlussreichste Wert ist die Zufalls-Kontrolle: ~55 % Haltequote. Ein
BELIEBIGES Level haelt in diesem Markt in gut der Haelfte der Faelle - das ist
die Basisrate kurzfristiger Mean-Reversion, keine Eigenschaft von Fibonacci.
Damit ist auch das Selbsterfuellungs-Argument entkraeftet: dann muesste Fib
ueber der Zufallslinie liegen; es liegt darunter bzw. gleichauf.

Bestaetigt umgekehrt die Struktur-These: M30-Pivots - Stellen, an denen real
gehandelt wurde - halten in BEIDEN Haelften besser als beide gerechneten Linien.
Dieselbe Trennlinie wie beim Regressionskanal und beim P(break)-Modell:
"Linie im Chart" ist nicht "Struktur im Markt".

Zur konkreten Aussage: 79,00 ist beim aktuellen M15-Schwung (78,808 -> 82,680)
gar kein Fib-Level, sondern ein Retracement von 95 %. Die naechsten echten
Marken sind 80,29 (61,8 %) und 79,64 (78,6 %). Der kalibrierte Kegel legt 79,00
in allen drei Horizonten ausserhalb des 80-%-Bandes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:10:57 +02:00

219 lines
9.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()