Reversal-Winkelbedingung gemessen: alle drei Varianten fallen durch
Anlass: User-Frage "warum SHORT im starken M5-Aufwaertstrend?". Antwort war ein
WAVE_REV_SHORT (3,25xATR ueberdehnt). Beim Nachrechnen kam heraus, dass die
Winkelbedingung gegen ihre eigene Dokumentation laeuft:
calc_trend_angle liefert 0 Grad = starker AUFWAERTStrend, 180 = ABWAERTS.
ad = angle-90 heisst also: ad<0 = STEIGEND. Der Code triggert
stretch>=+3.0 and ad<=-2 -> SHORT, kommentiert als "Winkel ab" - feuert also bei
ueberkauft UND WEITER STEIGENDEM Winkel, waehrend CLAUDE.md "Einstieg IN
Winkel-Richtung" beschreibt. Dasselbe Vorzeichen im Konfidenz-Block: ein SHORT
bekommt "Winkel bestaetigt" (+5), waehrend der Kurs steigt. Mit synthetischen
Reihen direkt nachgestellt.
Erklaert zwei dokumentierte Befunde: ~50 % Richtungstreffer der Bounce-Anzeige
und "blutet in starken Trends".
Statt das Vorzeichen blind zu drehen (alle Messungen beruhen auf dem
IST-Verhalten): backtest_reversal_angle.py, 80k M5, kanonischer Exit,
Echtkosten, 2 Halbjahre, drei Varianten.
IST -0,122 / -0,031 PF 0,79/0,94 n=1100/1615
KORRIGIERT -0,173 / -0,061 PF 0,68/0,89 n= 106/ 163
KONTROLLE -0,099 / -0,023 PF 0,82/0,96 n=1153/1693
(a) Die Vorzeichenkorrektur ist SCHLECHTER - gut, dass sie nicht blind eingebaut
wurde. Sie feuert zudem kaum, weil bei 3xATR Ueberdehnung die 14-Bar-
Regression fast immer noch steigt.
(b) Die Kontrolle OHNE Winkel ist die beste der drei - die Winkelbedingung
entfernt Trades, die im Schnitt besser waren als die behaltenen.
(c) Der Trigger ist in jeder Variante beidhaelftig negativ.
Gemessen wurde der ROHE Trigger ohne Konfidenz-/Raum-Gate; der Legacy-Recheck
der gegateten Variante kam auf ~Breakeven (PF 0,93/1,06), dieselbe Richtung.
Keine Messung stuetzt die dokumentierten "+0,185 / PF 1,35" mehr.
Noch KEINE Strategieaenderung - das Abschalten ist eine User-Entscheidung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
94fbde1dba
commit
d052905319
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reversal-Trigger: IST-Verhalten gegen vorzeichenkorrigierte Variante (2026-08-04).
|
||||
|
||||
Anlass: Beim Nachgehen der User-Frage „warum SHORT im starken M5-Aufwärtstrend?"
|
||||
kam heraus, dass die Reversal-Bedingung in `wave_rec._build` GEGEN ihre eigene
|
||||
Dokumentation läuft.
|
||||
|
||||
`calc_trend_angle` liefert laut Docstring 0° = starker AUFWÄRTStrend
|
||||
90° = seitwärts
|
||||
180° = starker ABWÄRTStrend
|
||||
→ `ad = angle − 90`: ad < 0 = STEIGEND, ad > 0 = FALLEND.
|
||||
|
||||
Der Code macht:
|
||||
stretch >= +3.0 und ad <= -2 → SHORT (Kommentar: „Winkel ab")
|
||||
stretch <= -3.0 und ad >= +2 → LONG (Kommentar: „Winkel auf")
|
||||
Mit obiger Konvention heißt das: SHORT bei überkauft UND WEITER STEIGENDEM
|
||||
Winkel; LONG bei überverkauft UND WEITER FALLENDEM. CLAUDE.md beschreibt aber
|
||||
„Einstieg IN Winkel-Richtung" — also genau umgekehrt.
|
||||
|
||||
⚠ Warum das nicht einfach umgedreht wurde: sämtliche Messungen (`backtest_bounce.py`,
|
||||
der Legacy-Recheck, `backtest_bounce_reliability.py`) rufen dasselbe `_build` — sie
|
||||
beschreiben also das IST-Verhalten. Ein Vorzeichenwechsel machte aus einem gemessenen
|
||||
Setup ein ungemessenes. Deshalb hier beide Varianten gegeneinander, plus eine
|
||||
Kontrolle ohne Winkelbedingung (trägt der Winkel überhaupt etwas bei?).
|
||||
|
||||
Aufbau: M5 (Basis-TF), kanonischer Live-Exit (`core/exit_model.LIVE`), Echtkosten
|
||||
(Bar-Spread/ATR), sequentielle 1-Positions-Sim, 2 Halbjahre.
|
||||
Nur der TRIGGER wird verglichen — ohne Konfidenz-Gate/Raum-Gate, sonst mischen sich
|
||||
Effekte. Maßstab: beidhälftig ØR>0 und PF>1.
|
||||
|
||||
Aufruf: python backtest_reversal_angle.py [bars]
|
||||
"""
|
||||
import sys
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
from core.analysis import calc_trend_angle
|
||||
from core.exit_model import LIVE, simulate
|
||||
from core.wave_rec import _ANGLE_DEAD, _ANGLE_LR, _EMA_SLOW, _REVERSAL_STRETCH
|
||||
|
||||
_ATRMIN = 0.06
|
||||
_COOL = 3
|
||||
|
||||
|
||||
def _ema(vals, p):
|
||||
k = 2.0 / (p + 1)
|
||||
out = [vals[0]]
|
||||
for v in vals[1:]:
|
||||
out.append(out[-1] + k * (v - out[-1]))
|
||||
return out
|
||||
|
||||
|
||||
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])))
|
||||
return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1])))
|
||||
if i else None for i in range(len(C))]
|
||||
|
||||
|
||||
def st(Rs):
|
||||
if not Rs:
|
||||
return None
|
||||
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)
|
||||
return dict(n=n, wr=100 * w / n, oR=s / n, pf=(up / dn if dn > 0 else 9.99), sum=s)
|
||||
|
||||
|
||||
def line(lbl, s):
|
||||
if not s:
|
||||
return f" {lbl:<22} — (keine Trades)"
|
||||
return (f" {lbl:<22} n={s['n']:>5} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} "
|
||||
f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+7.0f}")
|
||||
|
||||
|
||||
def run(H, L, C, A, SP, E50, AD, lo, hi, variant):
|
||||
def cost(i, atr):
|
||||
return (SP[i] if SP[i] > 0 else 0.0225) / atr
|
||||
|
||||
Rs = []
|
||||
i = max(lo, _EMA_SLOW + _ANGLE_LR + 5)
|
||||
while i < hi:
|
||||
atr = A[i]
|
||||
if not atr or atr < _ATRMIN:
|
||||
i += 1; continue
|
||||
stretch = (C[i] - E50[i]) / atr
|
||||
ad = AD[i]
|
||||
d = 0
|
||||
if variant == "IST": # wie der Live-Code heute
|
||||
if stretch <= -_REVERSAL_STRETCH and ad >= _ANGLE_DEAD:
|
||||
d = 1
|
||||
elif stretch >= _REVERSAL_STRETCH and ad <= -_ANGLE_DEAD:
|
||||
d = -1
|
||||
elif variant == "KORR": # vorzeichenkorrigiert = dokumentierte Absicht
|
||||
if stretch <= -_REVERSAL_STRETCH and ad <= -_ANGLE_DEAD:
|
||||
d = 1 # überverkauft + Winkel STEIGT = gedreht
|
||||
elif stretch >= _REVERSAL_STRETCH and ad >= _ANGLE_DEAD:
|
||||
d = -1 # überkauft + Winkel FÄLLT = gedreht
|
||||
else: # KONTROLLE: nur Überdehnung, kein Winkel
|
||||
if stretch <= -_REVERSAL_STRETCH:
|
||||
d = 1
|
||||
elif stretch >= _REVERSAL_STRETCH:
|
||||
d = -1
|
||||
if d == 0:
|
||||
i += 1; continue
|
||||
a = max(atr, _ATRMIN)
|
||||
r, xj = simulate(C[i], d, a, H, L, C, i + 1, LIVE,
|
||||
timestop=True, use_tp=True, ret_bar=True)
|
||||
Rs.append(r - cost(i, a))
|
||||
i = xj + _COOL
|
||||
return Rs
|
||||
|
||||
|
||||
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]; SP = [float(b["spread"]) * point for b in bars]
|
||||
A = _atr_series(H, L, C)
|
||||
E50 = _ema(C, _EMA_SLOW)
|
||||
# Winkel EINMAL je Bar (variantenunabhängig), exakt wie im Live-Code aufgerufen
|
||||
AD = [0.0] * len(C)
|
||||
for i in range(_ANGLE_LR + 2, len(C)):
|
||||
AD[i] = calc_trend_angle(C[i - (_ANGLE_LR + 1):i + 1], _ANGLE_LR) - 90.0
|
||||
N = len(C); mid = N // 2
|
||||
|
||||
print("=" * 88)
|
||||
print(f" Reversal-Trigger: IST vs. vorzeichenkorrigiert — {sym} M5 ({N} Bars)")
|
||||
print(f" Kanonischer Exit (SL {LIVE.sl_atr}× · Trail {LIVE.mult}× · BE {LIVE.be}×"
|
||||
f" · Time-Stop), Echtkosten, 2 Halbjahre")
|
||||
print(f" Trigger: |stretch| ≥ {_REVERSAL_STRETCH}×ATR, Winkel-Totband ±{_ANGLE_DEAD}°")
|
||||
print("=" * 88)
|
||||
beschreibung = {
|
||||
"IST": "IST (Live heute): überkauft + Winkel STEIGT → SHORT",
|
||||
"KORR": "KORRIGIERT (Doku): überkauft + Winkel FÄLLT → SHORT",
|
||||
"KTRL": "KONTROLLE: nur Überdehnung, Winkel ignoriert",
|
||||
}
|
||||
for v in ("IST", "KORR", "KTRL"):
|
||||
s1 = st(run(H, L, C, A, SP, E50, AD, 0, mid, v))
|
||||
s2 = st(run(H, L, C, A, SP, E50, AD, mid, N - LIVE.max_hold - 1, v))
|
||||
print(f"\n {beschreibung[v]}")
|
||||
print(line("H1 (alt)", s1))
|
||||
print(line("H2 (neu)", s2))
|
||||
ok = (s1 and s2 and s1["oR"] > 0 and s2["oR"] > 0
|
||||
and s1["pf"] > 1 and s2["pf"] > 1)
|
||||
print(f" → {'ROBUST (beide Hälften positiv)' if ok else 'fällt durch'}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user