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