Files
AH-Oil-Trader/backtest_candle_fade2.py
Axel HocksandClaude Opus 5 65e12ab264 Kerzen-Anatomie zum dritten Mal gemessen: alle 40 Zellen negativ
backtest_candle_fade2.py. Bewusst nur EINE neue Achse geprueft - der
Einstiegspreis, der am selben Tag Squeeze und Signal von negativ auf positiv
gedreht hat: Market am Bar-Close gegen ruhende Limit-Order am Kerzen-Extrem,
dazu der kanonische Exit (Trail 1,0 statt der alten Kopie mit 1,5).
Sequentiell EIN Slot, Echtkosten, 80k M5, 2 Halbjahre, Kontrollgruppe ohne
Volumen, Regel vorab fixiert.

Ergebnis: keine einzige Variante ist irgendwo positiv - beide Einstiegsarten,
alle Volumen-Schwellen, alle Gueltigkeitsdauern der Limit-Order.

Der lehrreiche Teil: der Ausfuehrungs-Hebel ist NICHT universell. Beim Squeeze
und beim Signal dreht die ruhende Order das Vorzeichen, hier macht sie es
schlechter. Strukturell: eine Stop-Order fuellt einen BREAKOUT im Moment der
Bestaetigung, eine Limit-Order fuellt einen FADE nur dann, wenn die Klimax sich
weiter ausdehnt - also genau wenn man falsch liegt. Sichtbar an der Schere:
Trefferquote steigt (38,0 -> 40,1 %, bei Vol>=2,5 auf 48,5 %), OeR faellt
trotzdem. Merksatz: ruhende Order hilft bei Momentum-Entries, schadet bei
Reversion-Entries.

Nebenbefund: im 12k-Vorlauf gab es positive Zellen (bis +0,291), ueber 80k
verschwinden sie restlos. Ein kleiner Lauf haette zum dritten Mal zum
Fehl-Einbau gefuehrt.

Kachel bleibt reine Anzeige, kein neuer Zaehl-Eintrag (dieselbe Klasse wie #18).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:22:03 +02:00

228 lines
9.6 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
"""Klimax-Kerzen-Fade, NEUMESSUNG mit dem Stand vom 2026-08-05.
⚠⚠ HOHE BEWEISLAST — diese Klasse ist bereits ZWEIMAL durchgefallen:
* `backtest_candles.py` (31.07.): die Lehrbuch-Lesart ist in beiden Hälften
INVERTIERT — nach einer volumenstarken großen Kerze läuft der Kurs GEGEN sie
(Überschuss 0,117/0,204 bei Volumen ≥2×). Das ist der Informations-Befund.
* `backtest_candle_fade.py` (31.07.): als TRADE fällt der Fade durch — benachbarte
Volumen-Schwellen exakt gegenläufig (Vol≥2,0: 0,059/**+0,103**; Vol≥2,5:
**+0,144**/0,080) = Rauschmuster. 18. verworfener Signal-Eingriff.
Es wird hier NICHT neu gefischt. Gemessen wird EINE Achse, die im alten Lauf fehlte
und die am 05.08. Squeeze UND Signal von negativ auf positiv gedreht hat:
**DER EINSTIEGSPREIS.**
Der alte Lauf steigt am **Bar-Close der Klimax-Kerze** ein (Market). Für einen Fade
ist das der denkbar schlechteste Moment — man verkauft, NACHDEM die Kerze schon
gelaufen ist. Die natürliche ruhende Order liegt am **Extrem der Kerze**: eine
Sell-Limit am Hoch (bzw. Buy-Limit am Tief) füllt nur, wenn die Klimax sich noch
etwas ausdehnt — besserer Preis UND zusätzlicher Filter in einem.
ZWEITE Änderung: **kanonischer Exit** (`exit_model.LIVE`, Trail **1,0**). Der alte
Lauf nutzte eine Exit-KOPIE mit Trail 1,5 — seit dem 31.07. nicht mehr der Live-Stand.
⚠ DIE KONTROLLGRUPPE BLEIBT PFLICHT („Fade OHNE Volumen-Bedingung"). Ohne sie wäre
nicht unterscheidbar, ob der Ertrag vom Volumen kommt oder ob schlicht JEDER Fade
einer großen Kerze funktioniert — genau der Kontrollfehler, der den Chartmuster-
„Edge" als Exit-Artefakt entlarvte, als er nachgeholt wurde.
VORAB FIXIERTE ENTSCHEIDUNGSREGEL (vor dem ersten Lauf festgelegt):
Der Fade gilt nur als tragfähig, wenn ALLE vier Bedingungen halten:
(a) ØR netto > 0 UND PF > 1 in BEIDEN Halbjahren,
(b) die Volumen-Variante schlägt die KONTROLLE in BEIDEN Halbjahren,
(c) die benachbarten Volumen-Schwellen kippen das Vorzeichen NICHT,
(d) n ≥ 100 je Hälfte (sonst ist es keine Stichprobe, sondern eine Anekdote).
Sonst: bleibt verworfen, und die Kachel bleibt reine Anzeige.
Aufruf: python backtest_candle_fade2.py [bars]
"""
import sys
import MetaTrader5 as mt5
from core.exit_model import LIVE, simulate
_ATRMIN = 0.06
_REF_N = 20 # Fenster für Median-Body und Ø-Volumen (wie core/candles.py)
_LONG_BR = 0.60 # Body/Range für „langer Körper"
_LONG_X = 1.5 # Body >= X × Median-Body
_WARTE = (3, 6, 12) # Gültigkeit der Limit-Order in Bars
_VOL = (0.0, 1.5, 2.0, 2.5, 3.0) # 0.0 = KONTROLLE (kein Volumen-Filter)
def _atr_series(H, L, C, p=14):
tr = [0.0]
for i in range(1, len(C)):
tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
out = [None]
for i in range(1, len(C)):
seg = tr[max(1, i - p + 1):i + 1]
out.append(sum(seg) / len(seg))
return out
def klimax(O, H, L, C, V, i):
"""→ Fade-Richtung (+1 LONG / 1 SHORT) und Volumen-Vielfaches, sonst None."""
rng = H[i] - L[i]
body = abs(C[i] - O[i])
if rng <= 0 or body <= 0:
return None
if body / rng < _LONG_BR:
return None
ref = sorted(abs(C[j] - O[j]) for j in range(i - _REF_N, i))
med = ref[len(ref) // 2] if ref else 0.0
if med <= 0 or body < _LONG_X * med:
return None
vs = [V[j] for j in range(i - _REF_N, i) if V[j] > 0]
vx = (V[i] / (sum(vs) / len(vs))) if vs else 0.0
# FADE = gegen die Körperrichtung
return (-1 if C[i] > O[i] else 1), vx
def lauf(O, H, L, C, V, A, SP, lo, hi, vol_x, am_extrem, warte=6):
"""Sequentiell, EIN Positions-Slot (wie live). `am_extrem=False` → Market am
Bar-Close (alte Annahme); True → ruhende Limit-Order am Kerzen-Extrem."""
Rs = []
frei_ab = lo
verpasst = 0
for i in range(max(lo, _REF_N + 2), hi - 2):
if i < frei_ab:
continue
atr = A[i]
if not atr or atr < _ATRMIN:
continue
k = klimax(O, H, L, C, V, i)
if not k:
continue
d, vx = k
if vol_x > 0 and vx < vol_x:
continue
kosten = (SP[i] if SP[i] > 0 else 0.0225) / atr
if not am_extrem:
entry, j0 = C[i], i + 1
else:
# SHORT-Fade → Sell-Limit am HOCH; LONG-Fade → Buy-Limit am TIEF.
# Fill nur, wenn der Kurs das Extrem in `warte` Bars nochmal erreicht.
limit = H[i] if d < 0 else L[i]
j0 = None
for j in range(i + 1, min(i + 1 + warte, len(C) - 1)):
if (H[j] >= limit) if d < 0 else (L[j] <= limit):
j0 = j + 1
break
if j0 is None:
verpasst += 1
continue
entry = limit
r, xb = simulate(entry, d, atr, H, L, C, j0, LIVE,
timestop=True, use_tp=True, ret_bar=True)
Rs.append(r - kosten)
frei_ab = xb + 1
return Rs, verpasst
def kenn(rs):
if not rs:
return None
n = len(rs); s = sum(rs)
g = sum(x for x in rs if x > 0); v = -sum(x for x in rs if x < 0)
return {"n": n, "wr": 100 * sum(1 for x in rs if x > 0) / n,
"oer": s / n, "sr": s, "pf": (g / v) if v > 0 else float("inf"),
"worst": min(rs)}
def zeile(lbl, k1, k2, extra=""):
def f(k):
if not k:
return f"{'—':>36}"
return (f"{k['n']:>5} {k['wr']:>5.1f}% {k['oer']:>+7.3f} "
f"{k['pf']:>5.2f} {k['sr']:>+8.1f}")
gut = bool(k1 and k2 and k1["oer"] > 0 and k2["oer"] > 0
and k1["pf"] > 1 and k2["pf"] > 1 and k1["n"] >= 100 and k2["n"] >= 100)
print(f" {lbl:<30}{f(k1)} |{f(k2)} {extra}"
+ (" <<< (a)+(d) ok" if gut else ""))
return gut
def main():
n_bars = 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_bars)
point = mt5.symbol_info(sym).point
mt5.shutdown()
O = [float(b["open"]) for b in bars]; H = [float(b["high"]) for b in bars]
L = [float(b["low"]) for b in bars]; C = [float(b["close"]) for b in bars]
V = [float(b["tick_volume"]) 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
print("=" * 104)
print(f" KLIMAX-KERZEN-FADE — Neumessung · {sym} M5, {N} Bars · 2 Halbjahre")
print(f" sequentiell (EIN Slot) · kanonischer Exit (Trail {LIVE.mult}) · Echtkosten")
print(" ⚠ Klasse bereits 2× verworfen — hier wird NUR der Einstiegspreis neu geprüft.")
print("=" * 104)
print(f" {'Variante':<30}{'H1 n WR OeR PF SigmaR':>36}"
f" |{'H2 n WR OeR PF SigmaR':>36}")
erg = {}
for am_extrem, titel in ((False, "A) MARKET am Bar-Close (alte Annahme)"),
(True, "B) LIMIT am Kerzen-Extrem (6 Bars gueltig)")):
print(" " + "-" * 102)
print(f" {titel}")
for vx in _VOL:
lbl = (" KONTROLLE (ohne Volumen)" if vx == 0
else f" Volumen >= {vx:.1f}x")
r1, v1 = lauf(O, H, L, C, V, A, SP, 0, mid, vx, am_extrem)
r2, v2 = lauf(O, H, L, C, V, A, SP, mid, N, vx, am_extrem)
erg[(am_extrem, vx)] = (kenn(r1), kenn(r2))
zeile(lbl, kenn(r1), kenn(r2),
extra=(f"verpasst {v1}/{v2}" if am_extrem else ""))
# Wartefenster der Limit-Order variieren (Robustheit)
print(" " + "-" * 102)
print(" C) LIMIT — Gueltigkeitsdauer variiert (Volumen >= 2,0x)")
for wt in _WARTE:
r1, v1 = lauf(O, H, L, C, V, A, SP, 0, mid, 2.0, True, wt)
r2, v2 = lauf(O, H, L, C, V, A, SP, mid, N, 2.0, True, wt)
zeile(f" {wt} Bars gueltig", kenn(r1), kenn(r2),
extra=f"verpasst {v1}/{v2}")
# ── Urteil nach der VORAB fixierten Regel ─────────────────────────────
print()
print("=" * 104)
print(" URTEIL (Regel VOR dem Lauf fixiert)")
print("=" * 104)
for am_extrem, titel in ((False, "A) Market"), (True, "B) Limit am Extrem")):
ko1, ko2 = erg[(am_extrem, 0.0)]
treffer = []
for idx, vx in enumerate([v for v in _VOL if v > 0]):
k1, k2 = erg[(am_extrem, vx)]
a = bool(k1 and k2 and k1["oer"] > 0 and k2["oer"] > 0
and k1["pf"] > 1 and k2["pf"] > 1)
d_ = bool(k1 and k2 and k1["n"] >= 100 and k2["n"] >= 100)
b = bool(k1 and k2 and ko1 and ko2
and k1["oer"] > ko1["oer"] and k2["oer"] > ko2["oer"])
nb = [v for v in _VOL if v > 0 and abs(v - vx) <= 0.51 and v != vx]
c = all((erg[(am_extrem, m)][0] or {}).get("oer", -1) > 0
and (erg[(am_extrem, m)][1] or {}).get("oer", -1) > 0 for m in nb)
if a and b and c and d_:
treffer.append(vx)
print(f" {titel:<22} Vol>={vx:.1f}x "
f"(a){'ok ' if a else 'nein'} (b){'ok ' if b else 'nein'} "
f"(c){'ok ' if c else 'nein'} (d){'ok ' if d_ else 'nein'}")
if treffer:
print(f" => {titel}: BESTEHT bei {treffer}")
else:
print(f" => {titel}: faellt durch")
print()
print(" ⚠ Auch ein Bestehen waere KEIN Einbau-Auftrag: die Klasse ist 2× verworfen,")
print(" ein dritter Lauf ist ein Mehrfachvergleich. Erst live-klein beobachten.")
print()
if __name__ == "__main__":
main()