Files
AH-Oil-Trader/backtest_candle_fade.py
Axel HocksandClaude Opus 5 03be3b8f5f D+A: Klimax-Fade verworfen, Kerzen-Anatomie-Kachel gebaut (reine Anzeige, v=123)
D - backtest_candle_fade.py: der Lead aus backtest_candles.py (Fade der
volumenstarken Klimax-Kerze) faellt im echten Trade-Sim durch. Live-Exit
(SL 2,0/Trail 1,5/BE 1,3/Time-Stop 120') + Echtkosten, mit KONTROLLGRUPPE
"Fade ohne Volumen-Filter" (die Lehre aus backtest_patterns.py).

  Kontrolle   -0,080 / -0,046
  Vol >= 2,0  -0,059 / +0,103
  Vol >= 2,5  +0,144 / -0,080
  Vol >= 3,0  +0,113 / -0,064

Benachbarte Schwellen exakt gegenlaeufig = Rauschmuster. Die einzige
beidseitig positive Zelle (Vol>=2,0 + Marubozu, +0,036/+0,043) hat n=91/118
bei 7 Varianten = Mehrfachvergleich-Artefakt. 18. verworfener Eingriff.

Wichtigste Lehre: Informations-Ueberschuss != handelbarer Edge. Dieselbe
2,0-Schwelle liefert im Forward-Fenster +0,12/+0,20 beim Fade, im echten
Trade -0,059/+0,103 - die Gegenbewegung ist diffus, der 2xATR-Stop wird
unterwegs getroffen (WR nur 35-40 %). Der Weg zum Ziel zaehlt.

A - core/candles.py + Kachel #card-candles (M5, ~20 s, unter mt5_lock):
Marubozu, Langer Koerper, Langer Docht oben/unten, Spinning Top, Doji,
Engulfing. Schwellen identisch zum Backtest.

Bewusste UI-Entscheidung: die Kachel ist NEUTRAL gefaerbt (keine gruen/rot-
Ampel) und nennt die GEMESSENE Lesart ("Klimax-Kerze - der Kurs laeuft
danach eher GEGEN die Kerze"), nicht die Lehrbuch-Lesart. Eine
Richtungsampel wuerde hier systematisch falsch stupsen - gleiche Korrektur
wie bei der Bounce-Anzeige. Kein Verdict-Gewicht, kein Trigger.

Beim Bau gefunden: mt5_lock ist ein CONTEXT-MANAGER, kein Lock-Objekt -
.acquire()/.release() wirft. Jetzt "with mt5_lock(3.0) as got".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:15:46 +02:00

177 lines
6.9 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
"""Fade der Klimax-Kerze: langer Körper + hohes Volumen GEGEN die Körperrichtung.
Folgetest zu `backtest_candles.py` (2026-07-31). Dort ergab die reine INFORMATIONS-
Messung (Forward-Return 60 min) den einzigen robusten Lead des Laufs:
Langer Körper Überschuss -0,059 / -0,076 (beide Hälften)
... + Volumen >= 2x Ø Überschuss -0,117 / -0,204 (monoton verstärkt)
d. h. nach einer volumenstarken großen Kerze läuft der Kurs GEGEN sie — das Gegenteil
der Lehrbuch-Lesart („2× Volumen = echt"). Brutto +0,117/+0,204 beim Fade, real
kosten aber ~0,265×ATR → unter der Schwelle. OFFEN blieb, ob ein **Live-Exit** mehr
holt als das starre 60-min-Fenster (bei den Chartmustern machte genau das den
Unterschied zwischen negativ und +0,43).
DESHALB HIER: echter Trade-Sim mit dem LIVE-Exit (Initial-SL 2,0×ATR · Trail 1,5 ab
0,3 · Breakeven 1,3 · Lock ab 3,5 · Time-Stop 120 min) und ECHTKOSTEN (Bar-Spread/ATR).
⚠ ENTSCHEIDEND — die KONTROLLGRUPPE: „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 + Trailing funktioniert (der Kontrollfehler, der in
`backtest_patterns.py` unentdeckt blieb und den Muster-„Edge" als Exit-Artefakt
entlarvte, als er nachgeholt wurde).
Urteil „trägt" nur, wenn (a) in BEIDEN Hälften ØR netto > 0, (b) die Volumen-Variante
die Kontrolle in BEIDEN Hälften schlägt, (c) benachbarte Volumen-Schwellen nicht das
Vorzeichen kippen.
"""
import sys
from statistics import median
import MetaTrader5 as mt5
_MAXH = 200 # max. Haltedauer in Bars
_ATRMIN = 0.06 # Live-Floor
_VOL_N = 20 # Fenster für Ø-Volumen / Median-Body
_COOL = 12 # Cooldown nach einem Trade (keine Überlappung)
# Live-Exit
_SL = 2.0; _TRAIL = 1.5; _TRAIL_START = 0.3; _BE = 1.3
_LOCK_START = 3.5; _LOCK_SCALE = 0.6; _LOCK_MIN = 1.2
_TIMESTOP = 24 # 120 min = 24 M5-Bars, greift nur in Phase „Init"
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])))
out = []
for i in range(len(C)):
if not i:
out.append(None); continue
w = t[max(1, i - p + 1):i + 1]
out.append(sum(w) / max(1, len(w)))
return out
def _ema(v, p):
k = 2.0 / (p + 1); o = []; e = v[0]
for i, x in enumerate(v):
e = x if i == 0 else x * k + e * (1 - k)
o.append(e)
return o
def sim(entry, d, atr, H, L, C, j0):
"""LIVE-Exit-Modell (identisch zu backtest_trailing_sr.py)."""
sl = entry - d * _SL * atr
hw = entry; rank = 0
end = min(j0 + _MAXH, len(C) - 1)
for j in range(j0, end + 1):
hi, lo = H[j], L[j]
if (lo <= sl) if d > 0 else (hi >= sl):
return (sl - entry) * d / atr
hw = max(hw, hi) if d > 0 else min(hw, lo)
profit = (hw - entry) * d
ph = 0 if profit < _TRAIL_START * atr else (1 if profit < _LOCK_START * atr else 2)
if ph < rank:
ph = rank
rank = ph
# Time-Stop: nach 120 min noch in Phase „Init" (nie 0,3×ATR im Plus)
if rank == 0 and (j - j0) >= _TIMESTOP:
return (C[j] - entry) * d / atr
if ph == 0:
continue
m = _TRAIL if ph == 1 else max(_LOCK_MIN, _TRAIL * _LOCK_SCALE)
cand = hw - d * m * atr
if profit >= _BE * atr or ph == 2:
cand = max(cand, entry) if d > 0 else min(cand, entry)
sl = max(sl, cand) if d > 0 else min(sl, cand)
return (C[end] - entry) * d / atr
def st(Rs):
if not Rs:
return "Trades= 0"
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
pf = up / dn if dn > 0 else 9.99
return (f"Trades={n:>5} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={pf:>4.2f} "
f"Worst={min(Rs):+.2f} ΣR={s:+.0f}")
def scan(O, H, L, C, V, A, SP, EF, ES, lo_i, hi_i, vol_x, body_x=1.5,
br_min=0.60, only_with_trend=False):
"""vol_x = 0 → KONTROLLE (kein Volumen-Filter)."""
Rs = []
i = max(lo_i, _VOL_N + 2)
while i < min(hi_i, len(C) - _MAXH - 2):
atr = A[i]
if not atr or atr < _ATRMIN:
i += 1; continue
rng = H[i] - L[i]
body = abs(C[i] - O[i])
if rng <= 0 or body / rng < br_min:
i += 1; continue
bref = median([abs(C[k] - O[k]) for k in range(i - _VOL_N, i)])
if bref <= 0 or body < body_x * bref:
i += 1; continue
if vol_x > 0:
vref = sum(V[i - _VOL_N:i]) / _VOL_N
if vref <= 0 or V[i] < vol_x * vref:
i += 1; continue
d_body = 1 if C[i] > O[i] else -1
if only_with_trend:
trend = 1 if EF[i] > ES[i] else -1
if d_body != trend: # nur Klimax IM Trend
i += 1; continue
d = -d_body # FADE: gegen die Körperrichtung
entry = C[i] # Einstieg am Close der Signal-Kerze
cost = (SP[i] if SP[i] > 0 else 0.0225) / atr
Rs.append(sim(entry, d, atr, H, L, C, i + 1) - cost)
i += _COOL
return Rs
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
mt5.initialize(); sym = None
for cand in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
if mt5.symbol_info(cand):
sym = cand; break
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
si = mt5.symbol_info(sym); point = si.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); EF = _ema(C, 12); ES = _ema(C, 50)
N = len(C); mid = N // 2
halves = [("H1", 100, mid), ("H2", mid, N)]
print("=" * 104)
print(f" KLIMAX-FADE — {sym} M5 ({N} Bars) · Live-Exit (SL 2,0 · Trail 1,5 ab 0,3 · "
f"BE 1,3 · Time-Stop 120') · Echtkosten")
print(" Einstieg GEGEN einen langen Körper (Body/Range>=0,60, Body>=1,5x Median)")
print("=" * 104)
variants = [
("KONTROLLE: Fade ohne Volumen-Filter", dict(vol_x=0)),
("Volumen >= 1,5x Ø", dict(vol_x=1.5)),
("Volumen >= 2,0x Ø", dict(vol_x=2.0)),
("Volumen >= 2,5x Ø", dict(vol_x=2.5)),
("Volumen >= 3,0x Ø", dict(vol_x=3.0)),
("Vol >= 2,0x + nur IM Trend", dict(vol_x=2.0, only_with_trend=True)),
("Vol >= 2,0x + Marubozu (BR>=0,90)", dict(vol_x=2.0, br_min=0.90)),
]
for name, kw in variants:
print(f"\n{name}")
for lbl, a, b in halves:
Rs = scan(O, H, L, C, V, A, SP, EF, ES, a, b, **kw)
print(f" {lbl} {st(Rs)}")
if __name__ == "__main__":
main()