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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b137967a68
commit
0944b1c13a
@@ -0,0 +1,181 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user