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>
This commit is contained in:
Axel Hocks
2026-07-31 08:15:46 +02:00
co-authored by Claude Opus 5
parent 7873bf2fb7
commit 03be3b8f5f
7 changed files with 467 additions and 3 deletions
+176
View File
@@ -0,0 +1,176 @@
#!/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()