Files
AH-Oil-Trader/backtest_pbreak_rvalue.py
T
Axel HocksandClaude Opus 5 0944b1c13a P(break) neu trainiert auf live-spiegelnder Stichprobe (Weg b) - eingebaut
Behebt die am selben Tag gefundene Merkmals-Diskrepanz: das alte Modell war
auf "Anlauf zu einem beim Entry FIXIERTEN Level" trainiert, live waehlt
_draw_levels das Level jede Sekunde neu -> mom3 live 0,15 statt 1,45 ->
Ausgabe immer ~23 % -> Gate seit Inbetriebnahme faktisch nie aktiv.

backtest_pbreak_retrain.py: Stichprobe spiegelt den Live-Pfad (Level
dynamisch mit Hysterese, gesampelt an JEDEM Bar im 0,15xATR-Band, keine
Anlauf-Bedingung). Kontrolle: Merkmalsmittel der Stichprobe (mom6 0,203 /
mom3 0,202) treffen die live rekonstruierten Werte (0,181 / 0,149).

  AUC auf dieser Stichprobe (H2, out-of-sample):
      ALT-Modell  0,368   (schlechter als Zufall, 19,2 % vs real 39,4 %)
      NEU-Modell  0,654   kalibriert

backtest_pbreak_rvalue.py (R-Ertrag, Echtkosten, Baseline = nur Trailing):
NEU schlaegt ALT in BEIDEN Haelften bei JEDER Schwelle. Gegen die Baseline
gewinnt es bei 0,35/0,45/0,55 in beiden (bei 0,60 kippt H1).
Bestwert 0,35: H1 -149 vs -506 (+357), H2 +1308 vs -110 (+1418).
Nebenbefund: das ALTE Modell war in H1 schlechter als gar kein Auto-Close.

EINGEBAUT: neue _PB_MU/_SD/_W (final auf allen 80k gefittet, n=76.542,
Basisrate 37,8 %, nach H1->H2-Validierung; alte Werte als Kommentar) +
sr_close_pbreak 0,55 -> 0,35, Config-Waechter-Anker mitgezogen.
Verifiziert: Chart-Linien zeigen 43 %/36 % statt 8-15 %.

ACHTUNG - invertiert eine alte Projekt-Regel: mom3-Gewicht dreht das
Vorzeichen (+1,4330 -> -0,5420). Alt: "kriecht ans Level -> 26 % Bruch, mit
Schwung -> laufen lassen". Neu: Anlauf-Schub -> 26,5 % (Level absorbiert
ihn und haelt), Schwung weg vom Level -> 51 %. Beides gilt in seiner
Population (fixiertes vs. dynamisches Level); fuer den Live-Pfad gilt die
neue Lesart.

Offen: auch NEU schliesst bei 0,35 noch ~94-97 % der Positionen (waehlt vor
allem den besseren Moment). Touch-Zahl bringt im neuen Modell nichts mehr
(0,652 vs 0,654) - die dynamische Level-Wahl erfasst den Effekt bereits.

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

182 lines
7.6 KiB
Python
Raw 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
"""R-Ertrag: bringt das NEU trainierte P(break)-Modell live mehr als das alte?
Zweiter, entscheidender Teil zu `backtest_pbreak_retrain.py`. Dort wurde gezeigt,
dass das ALTE Modell auf live-typischen Situationen nur AUC ~0,34 erreicht (also
schlechter als Zufall) und das neue ~0,66. **AUC allein reicht aber nicht** — die
Projektregel verlangt einen besseren R-Ertrag in BEIDEN Hälften.
SIMULIERTE REGEL (exakt der Live-Pfad):
Position öffnet beim Wave-Signal. Danach jeden Bar prüfen:
ist der Kurs im 0,15×ATR-Band um das dynamisch gewählte Level (mit Hysterese)?
→ P(break) berechnen. Ist P < Schwelle X → JETZT schließen (R_close).
→ sonst weiterlaufen lassen.
Wird nie geschlossen → R_run (Live-Trailing: SL 2,0 · Trail 1,5 ab 0,3 · BE 1,3).
Alles mit Echtkosten (Bar-Spread/ATR).
VERGLICHEN WERDEN:
* Baseline — nie am Level schließen (nur Trailing) = der ehrliche Maßstab
* ALT-Modell — die aktuell live eingebetteten _PB_MU/_SD/_W
* NEU-Modell — auf der live-spiegelnden Stichprobe gefittet (Fit auf H1!)
⚠ Das NEUE Modell wird auf H1 gefittet und in H2 out-of-sample angewandt — sonst
wäre der Vergleich zirkulär.
"""
import sys
from collections import deque
import numpy as np
import MetaTrader5 as mt5
from core.analysis import calc_trend_angle
from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD,
_REVERSAL_STRETCH, _STRETCH_MAX)
from backtest_pbreak_retrain import (
_ema_series, _atr_series, build_levels, _cluster, pick_level,
fit_logreg, _OLD_MU, _OLD_SD, _OLD_W, _ZONE, _ATRMIN, _M30)
_MAXH = 200
_SL_ATR = 2.0; _TRAILON = 0.3; _MULT = 1.5; _BE = 1.3
_LOCK_START = 3.5; _LOCK_SCALE = 0.6; _LOCK_MIN = 1.2; _TP_INIT = 3.5
def sim_run(entry, d, atr, H, L, C, j0):
"""Live-Exit (identisch zu backtest_srclose_prob.py)."""
sl = entry - d * _SL_ATR * atr; tp = entry + d * _TP_INIT * 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
if (hi >= tp) if d > 0 else (lo <= tp): return (tp - entry) * d / atr
hw = max(hw, hi) if d > 0 else min(hw, lo)
profit = (hw - entry) * d
ph = 0 if profit < _TRAILON * atr else (1 if profit < _LOCK_START * atr else 2)
if ph < rank: ph = rank
rank = ph
if ph == 1:
cand = hw - d * _MULT * atr
if profit >= _BE * atr: cand = max(cand, entry) if d > 0 else min(cand, entry)
sl = max(sl, cand) if d > 0 else min(sl, cand)
elif ph == 2:
tm = max(_LOCK_MIN, _MULT * _LOCK_SCALE); cand = hw - d * tm * atr
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 positions(a, b, H, L, C, EF, ES, AT, SP, LV):
"""→ [{R_run, cost, events:[(feat, R_close)]}] — Events in zeitlicher Reihenfolge."""
TH = _REVERSAL_STRETCH
out = []
i = max(a, _N_BARS, 320)
while i < min(b, len(C) - _MAXH - 1):
atr = AT[i]
if not atr or atr <= 0:
i += 1; continue
atr = max(atr, _ATRMIN); es = ES[i]; ef = EF[i]
stretch = (C[i] - es) / atr
ang = calc_trend_angle(C[i - _ANGLE_LR - 2:i], _ANGLE_LR); ad = ang - 90.0
d = 0
if stretch <= -TH and ad >= _ANGLE_DEAD: d = 1
elif stretch >= TH and ad <= -_ANGLE_DEAD: d = -1
elif abs(stretch) < _STRETCH_MAX: d = 1 if ef > es else -1 if ef < es else 0
if not d:
i += 1; continue
entry = C[i]
cost = (SP[i] if SP[i] > 0 else 0.0225) / atr
rec = {"R_run": sim_run(entry, d, atr, H, L, C, i + 1) - cost, "events": []}
prev_lvl = None
for j in range(i + 1, min(i + _MAXH, len(C) - 12)):
aj = AT[j]
if not aj:
continue
aj = max(aj, _ATRMIN)
lv = LV[min(j // _M30, len(LV) - 1)]
level = pick_level(_cluster(lv, aj), C[j], d, aj, prev_lvl)
prev_lvl = level
if level is None or abs(C[j] - level) > _ZONE * aj:
continue
feat = [(C[j] - C[max(0, j - 6)]) * d / aj,
(C[j] - C[max(0, j - 3)]) * d / aj,
1.0 if (EF[j] - ES[j]) * d > 0 else 0.0,
abs(level - entry) / aj]
# Schließen am Level: der Kurs IST dort (±0,15×ATR) → C[j] als Fill
rec["events"].append((feat, (C[j] - entry) * d / aj - cost))
out.append(rec)
i += 12
return out
def p_old(feat):
z = _OLD_W[0]
for k in range(4):
z += _OLD_W[k + 1] * (feat[k] - _OLD_MU[k]) / _OLD_SD[k]
return 1 / (1 + np.exp(-max(-30, min(30, z))))
def total(pos, pfun, X):
"""ΣR der Regel: beim ERSTEN Event mit P<X schließen, sonst laufen lassen."""
s = 0.0; closed = 0
for r in pos:
done = False
for feat, rc in r["events"]:
if pfun(feat) < X:
s += rc; closed += 1; done = True; break
if not done:
s += r["R_run"]
return s, closed
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
mt5.initialize(); sym = None
for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
if mt5.symbol_info(c): sym = c; break
si = mt5.symbol_info(sym); point = si.point
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
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]
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW); AT = _atr_series(H, L, C)
LV = build_levels(H, L)
mid = len(C) // 2
P1 = positions(_N_BARS, mid, H, L, C, EF, ES, AT, SP, LV)
P2 = positions(mid, len(C), H, L, C, EF, ES, AT, SP, LV)
# NEUES Modell auf H1-Events fitten
X1 = np.array([f for r in P1 for f, _ in r["events"]])
# Ausgang je Event wird hier NICHT gebraucht — wir fitten auf demselben
# Kriterium wie im Retrain-Skript, deshalb dort erzeugte Gewichte verwenden.
from backtest_pbreak_retrain import collect as rt_collect
s1 = rt_collect(_N_BARS, mid, H, L, C, EF, ES, AT, LV)
Xf = np.array([e["feat"] for e in s1]); yf = np.array([e["y"] for e in s1], float)
mu = Xf.mean(0); sd = Xf.std(0) + 1e-9
w = fit_logreg((Xf - mu) / sd, yf)
def p_new(feat):
z = w[0] + float(np.dot(w[1:], (np.array(feat) - mu) / sd))
return 1 / (1 + np.exp(-max(-30, min(30, z))))
print("=" * 96)
print(f" R-ERTRAG der P(break)-Close-Regel — {sym} M5 ({len(C)} Bars), Echtkosten")
print(f" NEU-Modell auf H1 gefittet ({len(s1)} Events) → H2 ist out-of-sample")
print("=" * 96)
for lbl, P in (("H1 (in-sample)", P1), ("H2 (OUT-OF-SAMPLE)", P2)):
base = sum(r["R_run"] for r in P)
nev = sum(1 for r in P if r["events"])
print(f"\n {lbl}: {len(P)} Positionen, davon {nev} mit Level-Kontakt")
print(f" Baseline (nur Trailing, nie am Level schließen): ΣR = {base:+.0f}")
print(f" {'Schwelle':<10}{'ALT ΣR':>10}{'ALT %zu':>9}{'NEU ΣR':>10}{'NEU %zu':>9}"
f"{'NEU vs ALT':>12}{'NEU vs Base':>13}")
for X in (0.35, 0.45, 0.55, 0.60):
so, co = total(P, p_old, X)
sn, cn = total(P, p_new, X)
print(f" P<{X:.2f} {so:>+10.0f}{100*co/max(1,len(P)):>8.0f}%"
f"{sn:>+10.0f}{100*cn/max(1,len(P)):>8.0f}%"
f"{sn-so:>+12.0f}{sn-base:>+13.0f}")
if __name__ == "__main__":
main()