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>
293 lines
11 KiB
Python
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()
|