Files
AH-Oil-Trader/backtest_buch_luis.py
Axel HocksandClaude Opus 5 adda5b0ccc Trading-Buch ausgewertet: 17/20 schon gemessen, die 3 Luecken fallen durch
User reichte 'Praktisches Trading 2.0' (Risueno Gomez) ein. Triage statt alles
nachmessen: 17 der ~20 Konzepte sind im Projekt bereits gemessen und ueberwiegend
verworfen. Ungemessen waren genau drei -- Bollinger, RSI-Divergenz, Drei
Soldaten/Kraehen. Alle drei mit Kontrollgruppe gemessen, keines besteht.

1 Bollinger-Squeeze ist SCHLECHTER als die live laufende ATR-Box (H2 -0,133 gegen
  -0,031, bei halb so vielen Trades). Der wertvollste Test, weil es kein neues
  Signal ist, sondern eine Variante des einzigen validierten Setups.
2 RSI-Divergenz kippt; die Gegenrichtung schlaegt sie in H1 mit KI ohne Null.
3 Drei Soldaten/Kraehen beidhaelftig negativ, Gegenrichtung besser.

In ALLEN DREI ist die Kontrollgruppe besser als die Buch-Lesart -- fuenfte
Bestaetigung derselben Inversion (Kerzen 4x, Winkel, Reversal).

Die Erfolgsraten-Tabelle S. 203 (Kopf-Schulter 83 %) ist unbelegt und
widerspricht der eigenen Messung (Ziel-Trefferquote 13-38 %).

Was das Buch BESTAETIGT ist sein Risikomanagement-Kapitel: nicht die
Trefferquote entscheidet, sondern OeGewinn/OeVerlust und 1-2 % Risiko je Trade.
Genau das misst dieses Projekt unabhaengig (Squeeze 73 % Treffer, PF 0,39,
Verhaeltnis 0,14; MaxDD 82,8 % gegen 22,2 % bei 0,5 % Risiko) -- und es ist der
einzige Punkt, an dem noch nichts umgesetzt ist.

26., 27. und 28. verworfener Signal-Eingriff. 40k Bars = Ausschluss-Urteil.

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

293 lines
11 KiB
Python

"""Die DREI ungemessenen Konzepte aus Risueno Gomez, "Praktisches Trading 2.0"
(User-Auftrag 2026-08-19: "nutze das PDF fuer neue Ideen").
TRIAGE ZUERST. Von den ~20 Konzepten des Buches sind 17 in diesem Projekt
bereits gemessen und ueberwiegend verworfen (Doppeltop/-boden, Kopf-Schulter,
Dreieck, Rechteck, Kanal, Marktstruktur HH/HL, Kerzenmuster inkl. Hammer/
Engulfing/Stern, Fibonacci, Order Blocks, FVG, Wyckoff, Pullback, RSI-Richtung,
Liquidity Sweeps). Ungemessen sind genau drei:
(1) BOLLINGER-SQUEEZE -- das Wort "Bollinger" kommt in CLAUDE.md NICHT vor.
⚠⚠ Das ist die interessanteste Luecke, weil es KEIN neues Signal ist:
der live laufende Squeeze benutzt eine ATR-Box (Spanne der letzten 10
M5-Bars <= 2,5x ATR). Das Buch definiert Kompression ueber die
BANDBREITE (SMA20 +- 2 Sigma). Gleiche Idee, andere Messgroesse --
also eine Variante des EINZIGEN validierten Setups statt eines 26.
Signal-Eingriffs. Genau dort sagt die Projekt-Logik, man solle suchen.
(2) RSI-DIVERGENZ -- gemessen wurde bisher die RSI-RICHTUNG
(`backtest_confluence.py`: bestaetigender RSI kontraproduktiv, RSI GEGEN
die Richtung = der Pullback-Effekt). Die DIVERGENZ (Preis neues Extrem,
RSI nicht) ist eine dritte, andere Aussage und nie geprueft. Das Buch
nennt sie "eines der wenigen Tools, das den Preis vorwegnehmen kann".
(3) DREI WEISSE SOLDATEN / SCHWARZE KRAEHEN -- als eigene Klasse ungemessen.
⚠ Hohe Beweislast: die Kerzen-Klasse ist 4x gemessen und in der
Lehrbuch-Lesart INVERTIERT. Wird deshalb in BEIDEN Lesarten getestet.
⚠⚠ EINORDNUNG DER BUCH-ZAHLEN (S. 203). Die Tabelle behauptet Kopf-Schulter
83 %, Engulfing 70-75 %, Doppelboden 70-75 %. In diesem Projekt gemessen:
Muster-Ziel-Trefferquote 13-38 % (`backtest_patterns.py`), Doppeltop in allen
Toleranzen durchgefallen, Kerzen-Lesart beidhaelftig invertiert. Die Tabelle
nennt keine Quelle, kein n und keinen Zeitraum. Sie wird hier NICHT als
Erwartung verwendet -- gemessen wird gegen null.
AUFBAU: 80k M5-Bars, 2 Halbjahre, kanonischer Exit (`exit_model.LIVE`),
Echtkosten aus dem Bar-Spread, Fill bei BERUEHRUNG (die am 06.08. als
entscheidend gemessene Achse), 95-%-Bootstrap je Zelle.
REGEL VORAB FIXIERT -- ein Konzept gilt nur dann als tragfaehig, wenn:
(a) OeR > 0 UND PF > 1 in BEIDEN Haelften
(b) das 95-%-KI schliesst in BEIDEN Haelften die Null aus
(c) die Nachbar-Parameter kippen nicht
(d) n >= 100 je Haelfte
Fuer (1) zusaetzlich: die Bollinger-Variante muss die ATR-Box-KONTROLLE auf
DERSELBEN Population schlagen -- sonst ist sie nur eine andere Schreibweise.
Aufruf: python backtest_buch_luis.py [bars]
"""
import sys
import MetaTrader5 as mt5
from backtest_breakout_gated import _atr_series
from backtest_cost_gate import kennzahlen
from core.exit_model import LIVE, simulate
_ATRMIN = 0.06
_COOL = 3
def sma(C, n):
out = [None] * len(C)
s = 0.0
for i, c in enumerate(C):
s += c
if i >= n:
s -= C[i - n]
if i >= n - 1:
out[i] = s / n
return out
def bb_width(C, n=20, k=2.0):
"""Bandbreite (oben-unten)/Mitte -- die Buch-Definition von Kompression."""
m = sma(C, n)
out = [None] * len(C)
for i in range(n - 1, len(C)):
mu = m[i]
var = sum((C[j] - mu) ** 2 for j in range(i - n + 1, i + 1)) / n
sd = var ** 0.5
out[i] = (2 * k * sd / mu) if mu else None
return out, m
def rsi_series(C, p=14):
out = [None] * len(C)
if len(C) < p + 1:
return out
g = l = 0.0
for i in range(1, p + 1):
d = C[i] - C[i - 1]
g += max(d, 0.0); l += max(-d, 0.0)
g /= p; l /= p
out[p] = 100.0 - 100.0 / (1 + g / l) if l else 100.0
for i in range(p + 1, len(C)):
d = C[i] - C[i - 1]
g = (g * (p - 1) + max(d, 0.0)) / p
l = (l * (p - 1) + max(-d, 0.0)) / p
out[i] = 100.0 - 100.0 / (1 + g / l) if l else 100.0
return out
def pivots(V, k):
"""Indizes lokaler Hochs/Tiefs. Kausal nutzbar erst ab j+k."""
ph, pl = [], []
for j in range(k, len(V) - k):
if V[j] is None:
continue
w = [V[x] for x in range(j - k, j + k + 1) if V[x] is not None]
if len(w) < 2 * k + 1:
continue
if V[j] == max(w):
ph.append(j)
if V[j] == min(w):
pl.append(j)
return ph, pl
def trade(i, d, H, L, C, A, SP, ende, puffer):
"""Ruhende Stop-Order am Extrem der Signalbar, Fill bei BERUEHRUNG."""
atr = A[i]
if not atr or atr < _ATRMIN or i + 1 >= ende:
return None
entry = (H[i] if d > 0 else L[i]) + d * puffer * atr
j = None
for x in range(i + 1, min(i + 13, ende)):
if (H[x] >= entry) if d > 0 else (L[x] <= entry):
j = x
break
if j is None or j + 1 >= ende:
return None
r = simulate(entry, d, atr, H, L, C, j + 1, LIVE, timestop=True, use_tp=True)
return r - ((SP[j] if SP[j] > 0 else 0.0225) / atr)
def zeig(lbl, k1, k2):
def f(k):
if not k:
return " --"
ci = k.get("ci")
m = ""
if ci:
m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1])
+ ("" if ci[0] * ci[1] > 0 else " ~0"))
return ("n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f}{}"
.format(k["n"], k["wr"], k["oer"], k["pf"], m))
print(" {:<30} H1 {}".format(lbl, f(k1)))
print(" {:<30} H2 {}".format("", f(k2)))
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
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)
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]; O = [float(b["open"]) 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; ende = N - LIVE.max_hold - 1
print("=" * 96)
print(" BUCH-KONZEPTE (Risueno Gomez) -- {} M5 ({} Bars, 2 Halbjahre)".format(sym, N))
print(" Exit kanonisch - Echtkosten - Fill bei BERUEHRUNG - 95-%-KI")
print("=" * 96)
# ---------------------------------------------------------------- (1)
print("\n (1) BOLLINGER-SQUEEZE gegen die ATR-BOX des laufenden Systems")
print(" " + "-" * 92)
BW, M20 = bb_width(C)
for lo_, hi_, tag in ((0, mid, "H1"), (mid, ende, "H2")):
pass
for pct, gu in ((0.15, 50), (0.25, 50), (0.15, 100)):
res = {"bb": [[], []], "atr": [[], []]}
for h, (lo_, hi_) in enumerate(((0, mid), (mid, ende))):
i = max(lo_, 120)
while i < hi_:
atr = A[i]
if not atr or atr < _ATRMIN or BW[i] is None:
i += 1; continue
fenster = [x for x in BW[i - gu:i] if x is not None]
if len(fenster) < gu // 2:
i += 1; continue
schwelle = sorted(fenster)[int(pct * len(fenster))]
# Buch: Kontraktion = Bandbreite im unteren Perzentil
bb_komp = BW[i] <= schwelle
# Projekt: Box-Spanne der letzten 10 Bars <= 2,5x ATR
box_hi = max(H[i - 10:i]); box_lo = min(L[i - 10:i])
atr_komp = (box_hi - box_lo) <= 2.5 * atr
d = 0
if C[i] > box_hi:
d = 1
elif C[i] < box_lo:
d = -1
if d:
if bb_komp:
r = trade(i, d, H, L, C, A, SP, ende, 0.0)
if r is not None:
res["bb"][h].append(r)
if atr_komp:
r = trade(i, d, H, L, C, A, SP, ende, 0.0)
if r is not None:
res["atr"][h].append(r)
i += _COOL
else:
i += 1
zeig("BOLLINGER p{:.0f} f{}".format(pct * 100, gu),
kennzahlen(res["bb"][0]), kennzahlen(res["bb"][1]))
zeig(" KONTROLLE ATR-Box (live)",
kennzahlen(res["atr"][0]), kennzahlen(res["atr"][1]))
print("")
# ---------------------------------------------------------------- (2)
print(" (2) RSI-DIVERGENZ (Preis neues Extrem, RSI nicht)")
print(" " + "-" * 92)
R = rsi_series(C)
for k in (3, 5):
ph, pl = pivots(C, k)
res = [[], []]
gegen = [[], []]
for name, piv, d in (("bull", pl, 1), ("bear", ph, -1)):
for a, b in zip(piv, piv[1:]):
if b - a > 60 or b - a < 5 or R[a] is None or R[b] is None:
continue
# bullisch: tieferes Tief im Preis, hoeheres Tief im RSI
if d > 0:
ok = C[b] < C[a] and R[b] > R[a]
else:
ok = C[b] > C[a] and R[b] < R[a]
if not ok:
continue
i = b + k # kausal: Pivot erst k Bars spaeter bekannt
if i >= ende:
continue
h = 0 if i < mid else 1
r = trade(i, d, H, L, C, A, SP, ende, 0.0)
if r is not None:
res[h].append(r)
rg = trade(i, -d, H, L, C, A, SP, ende, 0.0)
if rg is not None:
gegen[h].append(rg)
zeig("Divergenz k={} (Buch-Lesart)".format(k),
kennzahlen(res[0]), kennzahlen(res[1]))
zeig(" KONTROLLE Gegenrichtung", kennzahlen(gegen[0]), kennzahlen(gegen[1]))
print("")
# ---------------------------------------------------------------- (3)
print(" (3) DREI WEISSE SOLDATEN / DREI SCHWARZE KRAEHEN")
print(" " + "-" * 92)
res = [[], []]; geg = [[], []]
i = 5
while i < ende:
atr = A[i]
if not atr or atr < _ATRMIN:
i += 1; continue
d = 0
for dd in (1, -1):
ok = True
for x in (i - 2, i - 1, i):
koerper = (C[x] - O[x]) * dd
spanne = H[x] - L[x]
if koerper <= 0 or spanne <= 0 or koerper < 0.5 * spanne:
ok = False; break
if x > i - 2:
# oeffnet im Koerper der Vorkerze, schliesst nahe Extrem
if not (min(O[x - 1], C[x - 1]) <= O[x] <= max(O[x - 1], C[x - 1])):
ok = False; break
if (C[x] - C[x - 1]) * dd <= 0:
ok = False; break
if ok:
d = dd; break
if d:
h = 0 if i < mid else 1
r = trade(i, d, H, L, C, A, SP, ende, 0.0)
if r is not None:
res[h].append(r)
rg = trade(i, -d, H, L, C, A, SP, ende, 0.0)
if rg is not None:
geg[h].append(rg)
i += _COOL
else:
i += 1
zeig("Fortsetzung (Buch-Lesart)", kennzahlen(res[0]), kennzahlen(res[1]))
zeig(" KONTROLLE Gegenrichtung", kennzahlen(geg[0]), kennzahlen(geg[1]))
print("=" * 96)
if __name__ == "__main__":
main()