diff --git a/CLAUDE.md b/CLAUDE.md index e5fc758..d1204dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8960,6 +8960,84 @@ negativ (−0,003 / −0,023), der bärische **kippt das Vorzeichen**. Ein Kanal nichts vorhersagt, bekommt keine Stimme — dieselbe Begründung wie bei TU (2026-07-06) und beim Orderbuch (06.08.). +## ⚠⚠ „TAGESTREND MEHR GEWICHT?" — VIER VARIANTEN GEMESSEN, KEINE BESTEHT (2026-08-20) + +User-Frage nach dem Stretch-Befund. Vier konkrete Vorschläge, **alle vorher +gemessen statt gebaut** (`backtest_tagestrend.py`, 60k M5 = 244 Handelstage, +voller Live-Gate-Stack — echtes `_build` + Breakout k=0,3 + `_room_gate`, +Reversal aus —, kanonischer Exit, Echtkosten, 2 Halbjahre, 95-%-Bootstrap, +Schwellen 0,15 / **0,30** / 0,60 % als Nachbar-Probe). +**Tagestrend = Kurs gegen die TAGESERÖFFNUNG** (Berlin-Tag, DST-korrekt) — +bewusst so und nicht als D1-EMA: das ist, was ein Mensch sieht, wenn er sagt +„der Tagestrend ist aufwärts", und es braucht keinen 16.000-Bar-Vorlauf. + +| Variante | H1 ØR / ΣR | H2 ØR / ΣR | n (H1/H2) | +|---|---|---|---| +| **BASIS (wie live)** | −0,062 / −34,9 | −0,107 / −88,9 | 563 / 831 | +| A) VETO gegen den Tagestrend | −0,058 / −31,5 | **−0,114** / −89,5 | 543 / 785 | +| B) NUR MIT dem Tagestrend | −0,010 / −4,8 | **−0,126** / −86,1 | 483 / 683 | +| C) Spät-Einstieg erlaubt (stretch → 5,0) | −0,031 / −20,3 | −0,089 / −84,8 | 656 / 953 | +| **D) Pullback im Tagestrend** | **−0,008 / −1,6** | **−0,012 / −3,5** | **196 / 291** | + +**A und B fallen sofort:** beide sind in H2 an **allen drei** Schwellen +schlechter als BASIS. Die von ihnen entfernte Gruppe kippt zwischen den Hälften +(A: −0,105 / **+0,032**; bei 0,15 % sogar **+0,208** in H2) — sie werfen also +teils die *besseren* Trades weg. Damit ist die Klasse zum **vierten Mal** +gemessen (Höher-TF-Winkel · H1-Stärke · Konfluenz-Strafe · jetzt Tagestrend). + +⚠⚠ **C sieht in der Tabelle gut aus und ist trotzdem kein Beleg.** Es ist in +**beiden** Hälften und an **allen drei** Schwellen besser als BASIS — aber die +Trades, die es zusätzlich nimmt, sind **H1 −0,134 / H2 +0,060**, also +gegenläufig. Der Widerspruch löst sich in der Mechanik auf: die Sim hat EINEN +Slot (`i = xj + _COOL`), ein zusätzlicher Trade **verschiebt die gesamte +Folge**. C ist damit keine Obermenge von BASIS, sondern eine andere +Trade-Folge — der scheinbare Gewinn ist ein **Selektionseffekt**, exakt die +Falle vom 14.08. +✅ Nebenbefund, der zum Stretch-Lauf desselben Tages passt: der Spät-Einstieg +zahlt auch dann nicht, wenn der Tagestrend ihn stützt. + +⚠⚠ **D ist die einzige Variante mit der RICHTIGEN Signatur — und trotzdem kein +Bau.** Sie ist beidhälftig besser als BASIS, an allen drei Schwellen, und die +Gruppe, die sie **entfernt**, ist an **allen sechs** Stellen negativ +(H1 −0,068/−0,060/−0,063 · H2 −0,072/−0,080/−0,053) — sie wirft also +konsistent Schlechtes weg, nicht Gutes. Das ist die Form, die A, B und C fehlt. +❌ **Aber Bedingung (2) ist verfehlt: JEDES Intervall enthält die Null** +(D selbst: [−0,266 … +0,257]). +❌ Und der ΣR-Sprung (−34,9 → **−1,6**) kommt fast vollständig daher, dass D nur +noch **35 %** der Trades nimmt. Das ist **Handelsvermeidung**, kein Edge — +dasselbe Muster, an dem `backtest_metalabel.py` gescheitert ist („ΣR steigt nur, +weil n fällt; ØR bleibt negativ"). D verwandelt eine verlierende Population in +eine **breakeven** Population, indem sie zwei Drittel davon nicht handelt. + +✅✅ **DIE INHALTLICHE ANTWORT AUF DIE FRAGE steckt im Vergleich C gegen D:** wenn +der Tagestrend etwas beiträgt, dann **nicht als Erlaubnis, spät einzusteigen** +(C: die Zusatz-Trades kippen), sondern als **Kontext für den Rücksetzer** +(D: die entfernte Gruppe ist an allen sechs Stellen negativ). Das deckt sich mit +dem einzigen im Projekt belegten Einstiegs-Effekt — dem **Pullback-Bonus** +(„⭐ tiefer Pullback" +15, `backtest_pullback.py`) — und mit dem invertierten +Winkel-Befund vom 04.08. („LONG bei fallendem Kurzfrist-Winkel trägt doppelt +so gut"). +⚠ Es heißt **nicht**, dass D gebaut werden sollte. Es heißt: **die Richtung, in +der man suchen müsste, ist der Pullback, nicht der späte Einstieg.** + +⚠ **Der ehrlichste Satz zum ganzen Lauf:** die BASIS-Population ist mit +−0,062/−0,107 selbst negativ. Kein Umgang mit dem Tagestrend macht daraus einen +Edge — das deckt sich mit dem Ausrichtungs-Split (MIT Signal 56,5 % gegen OHNE +Signal 56,6 % = identisch) und mit `analyze_signal_live.py` (Richtung 48–51 %). +**29. verworfener Signal-Eingriff** (A, B, C zusammen als eine Klasse gezählt). + +⚠⚠ **METHODEN-BEFUND, der über diesen Lauf hinausgeht: `paarweise` war hier das +FALSCHE Werkzeug — und es hat es selbst gezeigt.** Der erste Entwurf verglich +gepaart auf gemeinsamen Einstiegen und lieferte in **allen acht** Zellen +`Delta = +0,000, KI [0,000 … 0,000]`. Der Grund ist offensichtlich, sobald man +ihn sieht: diese Varianten ändern nicht den **Exit**, sondern **welche Einstiege +genommen werden** — auf gemeinsamen Fällen sind die R-Werte per Konstruktion +identisch. +➜ **Regel: `paarweise` für EXIT-Varianten (gleiche Einstiege, anderes Ergebnis), +die Auswertung der ENTFERNTEN bzw. HINZUGEFÜGTEN Gruppe für AUSWAHL-Varianten** +(Muster `backtest_htf_vola.py`). Ein Intervall von exakt [0,000 … 0,000] ist +kein Ergebnis, sondern die Fehlermeldung eines falsch gewählten Tests. + ## ⚠⚠⚠ P(break) IST LIVE **INVERTIERT** — und es steuert echtes Geld (2026-08-19) Die fällige Messung ist entscheidbar geworden: **n=530 entkoppelt** gegen die diff --git a/backtest_tagestrend.py b/backtest_tagestrend.py new file mode 100644 index 0000000..a8f03eb --- /dev/null +++ b/backtest_tagestrend.py @@ -0,0 +1,283 @@ +"""Bringt es etwas, dem TAGESTREND mehr Gewicht zu geben? (2026-08-20) + +ANLASS (User): "hast du Optimierungsvorschlaege, um den Tagestrend mehr Gewicht +zu verleihen?" - nach dem Befund, dass die Empfehlung heute um 09:13 LONG sagte +und seit 12:58 wegen Ueberdehnung schweigt, waehrend der Tag +2,89 % lief. + +⚠ HOHE BEWEISLAST. Signal-Eingriffe sind in diesem Projekt 28-mal gescheitert, +darunter dreimal genau diese Klasse (Hoeher-TF-Winkel-Filter, H1-Staerke-Filter, +Konfluenz-Strafe - alle gemessen SCHLECHTER). Deshalb wird hier NICHTS gebaut, +sondern zuerst gemessen. + +VIER VARIANTEN, alle auf DERSELBEN Population (voller Live-Gate-Stack: echtes +`_build` + Breakout-Bestaetigung k=0,3 + `_room_gate`, Reversal aus): + + BASIS wie live + A VETO Signal GEGEN den Tagestrend verwerfen - die einzige Form, die + im Projekt je robust war (Ausrichtungs-Split: gegen das Signal + -6,60 EUR/Lot, beidhaelftig) + B NUR-MIT nur Signale MIT dem Tagestrend (strenger als A: auch neutrale + Tage fallen weg) + C SPAET `_STRETCH_MAX` 3,5 -> 5,0, solange der Tagestrend in + Signalrichtung zeigt. Das ist EXAKT die heutige Lage. + D PULLBACK nur MIT dem Tagestrend UND nur bei kleiner Ueberdehnung + (|stretch| <= 1,5) - nicht spaet kaufen, sondern den + Ruecksetzer nehmen. Nutzt den im Projekt BELEGTEN + Pullback-Effekt ("tiefer Pullback" +15). + +TAGESTREND = Kurs gegen die TAGESEROEFFNUNG (Berlin-Kalendertag, DST-korrekt). +Bewusst so und nicht als D1-EMA: das ist, was ein Mensch sieht, wenn er sagt +"der Tagestrend ist aufwaerts" - und es braucht keinen 16.000-Bar-Vorlauf. +Schwelle 0,30 % vom Kurs; 0,15 / 0,60 % werden als Nachbarn mitgeprueft. +⚠ KAUSAL: die Eroeffnung des LAUFENDEN Tages ist jederzeit bekannt, es fliesst +nichts aus der Zukunft ein. + +REGEL VORAB FIXIERT: eine Variante wird nur dann zum Bauen empfohlen, wenn sie +(1) in BEIDEN Halbjahren einen hoeheren OeR liefert als BASIS, +(2) ihr GEPAARTES 95-%-Intervall (gemeinsame Einstiege - die Falle vom 14.08.) + die Null ausschliesst, und +(3) die Nachbar-Schwellen nicht kippen. + +Aufruf: python backtest_tagestrend.py [bars] +""" +import datetime as dt +import sys +from bisect import bisect_left, bisect_right +from zoneinfo import ZoneInfo + +import MetaTrader5 as mt5 + +import core.wave_rec as WR +from backtest_breakout_gated import (_ATRMIN, _ATR_FLOOR_PB, _COOL, _PIV_K, + _PIV_LOOK, _TIMEOUT_BARS, _TU, + _atr_series, _htf_sign) +from backtest_cost_gate import kennzahlen +from core.analysis import calc_trend_angle +from core.exit_model import LIVE, simulate +from core.wave_rec import (WaveRecommender, _ANGLE_LR, _EMA_FAST, _EMA_SLOW, + _ema_series) + +BERLIN = ZoneInfo("Europe/Berlin") +BROKER = dt.timezone(dt.timedelta(hours=3)) +K_BREAKOUT = 0.3 +PULLBACK_MAX = 1.5 +STRETCH_LOCKER = 5.0 +MODI = (("basis", "BASIS (wie live)"), + ("veto", "A) VETO gegen Tagestrend"), + ("nurmit", "B) NUR MIT Tagestrend"), + ("spaet", "C) Spaet-Einstieg bis 5,0"), + ("pullback", "D) Pullback im Tagestrend")) + + +def _tagesoeffnung(T, C): + """Index -> Eroeffnungskurs SEINES Berliner Kalendertags (kausal). + + ⚠ MT5 liefert in `time` die Broker-WALLCLOCK, keinen echten UTC-Epoch - + genau der Fehler, der `analyze_tageszeit.py` am 05.08. um 3 h verschoben hat. + """ + out = [None] * len(C) + tag = op = None + for i, ts in enumerate(T): + d = dt.datetime.fromtimestamp(ts, dt.timezone.utc) \ + .replace(tzinfo=BROKER).astimezone(BERLIN).date() + if d != tag: + tag, op = d, C[i] + out[i] = op + return out + + +def _tagestrend(C, OP, i, schwelle): + if not OP[i]: + return 0 + rel = (C[i] - OP[i]) / OP[i] + return 0 if abs(rel) < schwelle else (1 if rel > 0 else -1) + + +def run(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL, OP, + lo, hi, modus, schwelle): + Rs, keys = [], [] + pend = None + i = max(lo, _PIV_LOOK + _EMA_SLOW + 5) + while i < hi: + atr = A[i] + if not atr or atr < _ATRMIN: + i += 1 + continue + w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)} + a1 = bisect_left(PH, i - _PIV_LOOK) + b1 = bisect_right(PH, i - _PIV_K) + a2 = bisect_left(PL, i - _PIV_LOOK) + b2 = bisect_right(PL, i - _PIV_K) + w._pb_levels = {"ph": [C[j] for j in PH[a1:b1]], + "pl": [C[j] for j in PL[a2:b2]]} + + td = _tagestrend(C, OP, i, schwelle) + WR._STRETCH_MAX = 3.5 + if modus == "spaet" and td: + stretch = (C[i] - ES[i]) / atr + if (td > 0 and stretch > 0) or (td < 0 and stretch < 0): + WR._STRETCH_MAX = STRETCH_LOCKER + rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, + htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) + WR._STRETCH_MAX = 3.5 + sig = rec.get("signal") + + if K_BREAKOUT > 0 and sig in ("LONG", "SHORT"): + d = 1 if sig == "LONG" else -1 + if pend is None or pend["dir"] != d: + pend = {"dir": d, "level": C[i] + d * K_BREAKOUT * atr, + "invalid": C[i] - d * K_BREAKOUT * atr, + "confirmed": False, "bar": i} + if not pend["confirmed"]: + broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"]) + gegen = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"]) + if broke: + pend["confirmed"] = True + else: + if gegen or (i - pend["bar"]) > _TIMEOUT_BARS: + pend = {"dir": d, "level": C[i] + d * K_BREAKOUT * atr, + "invalid": C[i] - d * K_BREAKOUT * atr, + "confirmed": False, "bar": i} + i += 1 + continue + elif sig == "WARTEN": + pend = None + if sig not in ("LONG", "SHORT"): + i += 1 + continue + rec = w._room_gate(rec, C[i]) + if rec.get("signal") not in ("LONG", "SHORT"): + i += 1 + continue + + d = 1 if rec["signal"] == "LONG" else -1 + if modus == "veto" and td and td != d: + i += 1 + pend = None + continue + if modus in ("nurmit", "pullback") and td != d: + i += 1 + pend = None + continue + if modus == "pullback" and abs((C[i] - ES[i]) / atr) > PULLBACK_MAX: + i += 1 + pend = None + 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 - ((SP[i] if SP[i] > 0 else 0.0225) / a)) + keys.append(i) + pend = None + i = xj + _COOL + return Rs, keys + + +def zeig(lbl, k): + if not k: + print(" {:<28} --".format(lbl)) + return + ci = k.get("ci") + m = "" + if ci: + m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1]) + + ("" if ci[0] * ci[1] > 0 else " ~0")) + print(" {:<28} n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f}{}" + .format(lbl, k["n"], k["wr"], k["oer"], k["pf"], m)) + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 60000 + schwellen = [0.0030, 0.0015, 0.0060] + mt5.initialize() + sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") + if mt5.symbol_info(c)), None) + b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) + point = mt5.symbol_info(sym).point + mt5.shutdown() + T = [int(x["time"]) for x in b] + H = [float(x["high"]) for x in b] + L = [float(x["low"]) for x in b] + C = [float(x["close"]) for x in b] + SP = [float(x["spread"]) * point for x in b] + A = _atr_series(H, L, C) + EF = _ema_series(C, _EMA_FAST) + ES = _ema_series(C, _EMA_SLOW) + OP = _tagesoeffnung(T, C) + AD = [calc_trend_angle(C[max(0, i - _ANGLE_LR):i + 1]) if i >= _ANGLE_LR else 90.0 + for i in range(len(C))] + HTF = [_htf_sign(C, i, 6, A[i]) for i in range(len(C))] + H1 = [_htf_sign(C, i, 12, A[i]) for i in range(len(C))] + PH = [j for j in range(_PIV_K, len(C) - _PIV_K) + if C[j] == max(C[j - _PIV_K:j + _PIV_K + 1])] + PL = [j for j in range(_PIV_K, len(C) - _PIV_K) + if C[j] == min(C[j - _PIV_K:j + _PIV_K + 1])] + + w = WaveRecommender(_TU(), mt5.TIMEFRAME_M5) + w.set_reversal_enabled(False) + w.set_entry_room(0.6) + w.set_breakout_k(0.0) + + N = len(C) + mid = N // 2 + ende = N - LIVE.max_hold - 1 + tage = len({dt.datetime.fromtimestamp(t, dt.timezone.utc) + .replace(tzinfo=BROKER).astimezone(BERLIN).date() for t in T}) + print("=" * 104) + print(" TAGESTREND MEHR GEWICHT? - {} · {} M5-Bars · {} Handelstage" + .format(sym, N, tage)) + print(" voller Live-Gate-Stack (echtes _build · Breakout k=0,3 · _room_gate)" + " · kanonischer Exit · Echtkosten") + print("=" * 104) + + for schwelle in schwellen: + titel = "Tagestrend-Schwelle {:.2f} %".format(schwelle * 100) + if schwelle != schwellen[0]: + titel += " (Nachbar-Probe)" + print("\n " + titel) + print(" " + "-" * 100) + erg = {} + for modus, lbl in MODI: + r1, k1 = run(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL, OP, + 0, mid, modus, schwelle) + r2, k2 = run(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL, OP, + mid, ende, modus, schwelle) + erg[modus] = (dict(zip(k1, r1)), dict(zip(k2, r2))) + zeig("H1 " + lbl, kennzahlen(r1)) + zeig("H2 " + lbl, kennzahlen(r2)) + print() + # ⚠⚠ KEIN gepaarter Test hier - er waere DEGENERIERT. Die Varianten + # aendern nicht den EXIT, sondern WELCHE Einstiege genommen werden; + # auf den gemeinsamen Faellen sind die R-Werte per Konstruktion + # IDENTISCH (erster Lauf: Delta +0,000, KI [0,000 .. 0,000] in allen + # acht Zellen). `paarweise` ist das Werkzeug fuer Exit-Varianten + # (Falle vom 14.08.), nicht fuer Auswahl-Varianten. + # Entscheidend ist stattdessen, WAS der Eingriff wegnimmt bzw. + # hinzufuegt - Muster wie in `backtest_htf_vola.py`. + print(" WAS DER EINGRIFF WEGNIMMT / HINZUFUEGT (die eigentliche Frage):") + b1, b2 = erg["basis"] + for modus, lbl in MODI[1:]: + v1, v2 = erg[modus] + for hl, bb, vv in (("H1", b1, v1), ("H2", b2, v2)): + weg = [bb[k] for k in bb if k not in vv] + neu_ = [vv[k] for k in vv if k not in bb] + gruppe, txt = (neu_, "hinzugefuegt") if len(neu_) > len(weg) else (weg, "entfernt") + kk = kennzahlen(gruppe) + if not kk: + print(" {:<28} {} {} n={} - zu wenig" + .format(lbl, hl, txt, len(gruppe))) + continue + ci = kk.get("ci"); m = "" + if ci: + m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1]) + + ("" if ci[0] * ci[1] > 0 else " ~0")) + print(" {:<28} {} {:<12} n={:>4} WR={:>3.0f}% OeR={:+.3f}{}" + .format(lbl, hl, txt, kk["n"], kk["wr"], kk["oer"], m)) + print("\n " + "=" * 100) + print(" REGEL: bauen nur, wenn beidhaelftig besser UND das gepaarte") + print(" Intervall die Null ausschliesst UND die Nachbarn nicht kippen.") + + +if __name__ == "__main__": + main()