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>
219 lines
9.1 KiB
Python
219 lines
9.1 KiB
Python
#!/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()
|