diff --git a/CLAUDE.md b/CLAUDE.md index a9b819b..f7144f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -284,6 +284,51 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: ~15 s→5 s schneller erkannt, dann ~1 s Handeln (`_pos_loop`). Gleiche validierte M5-Regel, reine Infra (kein Backtest nötig). **Variante B (Squeeze auf M1) = OFFEN/UNBELEGT** — erst backtesten, wenn der M1-Logger (`candles_m1`, seit 2026-07-21) ab ~2027-01 genug History hat. +- **⚠⚠ SCALPING-MODUL AUF M1 = ABGERATEN, bevor überhaupt gebaut wird (gemessen + `analyze_scalping_costs.py`, 2026-08-06, User-Frage „wie sinnvoll wäre ein + zusätzliches Modul Scalping auf 1M-Basis, über das Dashboard steuerbar?").** + Es ist zuerst eine **Kosten-**, keine Signalfrage: der Spread ist bei WTI absolut + ~konstant und skaliert NICHT mit der Zeitebene, die Bewegung schon. + | TF | Ø ATR | Ø Spread | **Median Sp/ATR** | Ø Sp/ATR | + |---|---|---|---|---| + | **M1** | 0,0870 $ | 0,0254 $ | **0,319** | **0,416** | + | M5 | 0,1677 $ | 0,0226 $ | 0,193 | 0,256 | + | M15 | 0,2269 $ | 0,0250 $ | 0,133 | 0,171 | + Der Spread ist auf allen drei Ebenen praktisch derselbe Betrag — **eine mittlere + M1-Kerze ist nur 3,4× so groß wie der Spread.** Nötige Trefferquote bei 1:1 + (Ziel/Stop je 1×ATR_M1): **65,9 %** statt 50 %; bei 0,5:0,5 sogar **81,9 %**. + Zum Vergleich: nichts in diesem Projekt hat je über ~46 % geliefert. + ⚠ **Nachts ist es arithmetisch unmöglich:** um **01:00 beträgt der Spread + 1,051×ATR_M1** — er ist größer als die durchschnittliche Bar-Spanne (00:00 0,801 · + 06:00 0,750). Handelbar wäre allenfalls **10–17 Uhr** (0,210–0,265), also dort, wo + M1 ungefähr die heutigen M5-Kosten erreicht. + ⚠⚠ **DREI Gründe, die schwerer wiegen als die Kosten selbst:** + **(1) Ein Backtest nach Projektstandard ist NICHT möglich.** Die M1-Historie des + Brokers ist hart bei **80.000 Bars = 80 Tage** gedeckelt (`copy_rates_from_pos` + liefert darüber `None`, `copy_rates_range` ebenso). Zwei Stichproben à 40 Tage + liegen im **selben Regime** — genau die Konstellation, an der ORB und die + Dead-Hour-Stunde 13 gescheitert sind. Der eigene `candles_m1`-Logger steht bei + 19 Tagen und erreicht die 180 erst ~2027-01. + **(2) Das Auflösungs-Artefakt trifft M1 am härtesten.** `analyze_trail_resolution.py` + (05.08.) hat gezeigt: eine Bar-Simulation **überschätzt enge Stops systematisch**, + Kipp-Punkt bei ~0,3×ATR. Ein Scalp lebt in 2–3 Bars — eine M1-Bar-Sim hat dort + praktisch keine Auflösung mehr. Ein positives Ergebnis wäre nicht überprüfbar; es + bräuchte Tick-Daten. + **(3) Der Slippage-Schwanz ist ABSOLUT und frisst deshalb den kleinen Stop.** + Gemessen (`analyze_execution.py`): Median 0,017 $ (unkritisch), **Ø 0,234 $, Max + 4,192 $**. Ein 2×ATR_M1-Stop ist **0,174 $** breit — der MITTLERE Slippage-Wert + übersteigt ihn bereits. Der reale 0,751-$-Fall vom 30.06. entspricht **8,6×** einem + M1-Stop gegen 2,2× einem M5-Stop; er löscht rund **11** gewonnene Scalps statt 4. + ✅ **Was FÜR die Idee spricht (fairerweise):** der Bau wäre billig — Pending-Stop- + Orders, `_auto_guard`, `_pending_ziel` mit Vorrang, Dashboard-Toggle (Muster + 🚀 BRK / 🎯 SIG), `runtime_state.json`, B4-Monitor und die Setup-Kennzeichnung + (seit 06.08.) existieren alle. Und **höhere Frequenz = schnellere Validierung**: + der Squeeze braucht 20 Trades und hat nach 5 Tagen 8. + ✅ **Empfehlung: KEIN eigenständiges Scalping-Modul, aber eine engere Variante ist + messbar** — der **Squeeze auf M1** (= die schon offene Variante B), beschränkt auf + 10–17 Uhr, mit ruhender Stop-Order. Das ist kein neues Signal, sondern eine + validierte Regel auf einer feineren Ebene; die 80 Tage reichen für ein + **Ausschluss**-Urteil (nicht für eine Bestätigung). Snapshot **`wave.squeeze {state, dir, level, box_atr}`**: `active` = Ausbruch (echtes Signal LONG/SHORT) · `armed` = komprimiert, Ausbruch steht bevor · None = keine Kompression (transient — der Ausbruch weitet die Box, Signal klärt sich selbst). diff --git a/analyze_scalping_costs.py b/analyze_scalping_costs.py new file mode 100644 index 0000000..f883661 --- /dev/null +++ b/analyze_scalping_costs.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Traegt ein M1-Scalping-Modul die Kosten? — B3-Check VOR jedem Bau. + +WOZU: Die Frage "Scalping auf M1" ist zuerst eine KOSTEN-Frage, nicht eine +Signal-Frage. Der Spread ist bei WTI absolut ~konstant (0,0225 $, gemessen +`analyze_execution.py`) — er skaliert NICHT mit der Zeitebene. Die Bewegung, aus +der ein Trade seinen Ertrag zieht, skaliert sehr wohl. Je feiner die Zeitebene, +desto groesser also der Anteil, den der Spread vom verfuegbaren Weg frisst. + +Die Projekt-Huerde (B3): Ø-Edge **netto > ~0,1×ATR** der gehandelten Zeitebene. +Auf M5 liegen die realen Kosten bei Ø 0,265×ATR_M5 — schon dort ist es eng. + +Gemessen wird deshalb: + A) Spread/ATR je Zeitebene (M1 gegen M5/M15) — Median, Ø, je Berliner Stunde + B) Wieviel Weg gibt eine M1-Bewegung ueberhaupt her? (MFE/MAE in N Bars) + C) Welche Trefferquote braeuchte ein Scalp, um die Kosten zu bezahlen? + +⚠ Das ist KEIN Backtest eines Scalping-Signals — es ist der Test, ob so ein +Signal ueberhaupt existieren KANN. Faellt A/C durch, eruebrigt sich die +Signalsuche (dieselbe Logik wie beim Entry-Raum-Gate: reine Arithmetik, +deshalb regime-unabhaengig robust). + +Aufruf: python analyze_scalping_costs.py [n_bars] +""" +import sys +from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo + +import MetaTrader5 as mt5 + +_BERLIN = ZoneInfo("Europe/Berlin") +_BROKER = timezone(timedelta(hours=3)) +_UTC = timezone.utc +_HURDE = 0.10 # B3: Ø-Edge netto muss > 0,1×ATR sein + + +def _berlin(ts): + """MT5-Bar-Zeitstempel → echte Berliner Zeit (s. CLAUDE.md, Fix 2026-08-05). + + ⚠ NICHT `fromtimestamp(ts, UTC+3)` — MT5 liefert dort bereits die + Broker-Wallclock, das ergaebe 3 Stunden Versatz. + """ + return (datetime.fromtimestamp(int(ts), _UTC) + .replace(tzinfo=_BROKER).astimezone(_BERLIN)) + + +def _atr(H, L, C, n=14): + tr = [0.0] + for i in range(1, len(C)): + tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out, s = [0.0] * len(C), 0.0 + for i in range(1, len(C)): + s += tr[i] + if i > n: + s -= tr[i - n] + out[i] = s / min(i, n) + return out + + +def _q(v, p): + s = sorted(v) + return s[min(len(s) - 1, int(p * len(s)))] if s else 0.0 + + +def hole(sym, tf, n): + b = mt5.copy_rates_from_pos(sym, tf, 0, n) + if b is None or len(b) < 200: + return None + point = mt5.symbol_info(sym).point + return { + "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], + "T": [int(x["time"]) for x in b], + } + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + if not mt5.initialize(): + print("MT5 nicht erreichbar"); return 1 + sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") + if mt5.symbol_info(c)), None) + if not sym: + mt5.shutdown(); print("Symbol nicht gefunden"); return 1 + + tfs = [("M1", mt5.TIMEFRAME_M1), ("M5", mt5.TIMEFRAME_M5), + ("M15", mt5.TIMEFRAME_M15)] + daten = {} + for lbl, tf in tfs: + d = hole(sym, tf, n) + if d: + d["ATR"] = _atr(d["H"], d["L"], d["C"]) + daten[lbl] = d + + print("=" * 86) + print(f" SCALPING auf M1 — traegt es die Kosten? Symbol {sym}") + print("=" * 86) + for lbl in ("M1", "M5", "M15"): + if lbl in daten: + d = daten[lbl] + print(f" {lbl}: {len(d['C'])} Bars " + f"{_berlin(d['T'][0]):%d.%m.%Y} – {_berlin(d['T'][-1]):%d.%m.%Y}") + + # ── A) Spread/ATR je Zeitebene ─────────────────────────────────────── + print() + print("=" * 86) + print(" A) KOSTEN — Spread als Anteil der ATR der jeweiligen Zeitebene") + print("=" * 86) + print(f" {'TF':<5} {'Ø ATR ($)':>10} {'Ø Spread ($)':>13} " + f"{'Median Sp/ATR':>14} {'Ø Sp/ATR':>10} {'75 %':>8}") + kosten = {} + for lbl in ("M1", "M5", "M15"): + if lbl not in daten: + continue + d = daten[lbl] + r = [d["SP"][i] / d["ATR"][i] for i in range(20, len(d["C"])) + if d["ATR"][i] > 0 and d["SP"][i] > 0] + if not r: + continue + kosten[lbl] = (_q(r, 0.5), sum(r) / len(r)) + ga = [a for a in d["ATR"][20:] if a > 0] + gs = [s for s in d["SP"][20:] if s > 0] + print(f" {lbl:<5} {sum(ga)/len(ga):>10.4f} {sum(gs)/len(gs):>13.4f} " + f"{_q(r, 0.5):>14.3f} {sum(r)/len(r):>10.3f} {_q(r, 0.75):>8.3f}") + print() + print(f" Referenz: B3-Huerde = Ø-Edge netto > {_HURDE:.2f}×ATR ·") + print(" gemessen auf M5 (backtest_realcosts.py): Ø 0,265×ATR = schon dort eng.") + + # ── A2) je Berliner Stunde (M1) ────────────────────────────────────── + if "M1" in daten: + d = daten["M1"] + prostd = {} + for i in range(20, len(d["C"])): + if d["ATR"][i] <= 0 or d["SP"][i] <= 0: + continue + prostd.setdefault(_berlin(d["T"][i]).hour, []).append( + d["SP"][i] / d["ATR"][i]) + print() + print(" Spread/ATR_M1 je Berliner Stunde (die guenstigsten zuerst):") + rang = sorted(((sum(v) / len(v), h, len(v)) for h, v in prostd.items() + if len(v) > 50)) + for m, h, k in rang[:5]: + print(f" {h:02d}:00 {m:.3f}×ATR_M1 (n={k})") + print(" ...") + for m, h, k in rang[-3:]: + print(f" {h:02d}:00 {m:.3f}×ATR_M1 (n={k})") + + # ── B) Wieviel Weg gibt M1 her? ────────────────────────────────────── + print() + print("=" * 86) + print(" B) VERFUEGBARER WEG — beste Auslenkung (MFE) in N M1-Bars, ×ATR_M1") + print("=" * 86) + if "M1" in daten: + d = daten["M1"] + H, L, C, A = d["H"], d["L"], d["C"], d["ATR"] + print(f" {'N Bars':<8} {'Median MFE':>12} {'Ø MFE':>9} {'Ø MAE':>9} " + f"{'MFE > Kosten?':>16}") + k_med = kosten.get("M1", (0, 0))[0] + for N in (3, 5, 10, 15, 30): + mfe, mae = [], [] + # Richtungsfrei: die BESTE der beiden Seiten (Obergrenze — ein + # echtes Signal muesste die Seite vorher treffen) + for i in range(20, len(C) - N, 7): + if A[i] <= 0: + continue + hi = max(H[i + 1:i + N + 1]); lo = min(L[i + 1:i + N + 1]) + mfe.append(max(hi - C[i], C[i] - lo) / A[i]) + mae.append(min(hi - C[i], C[i] - lo) / A[i]) + if not mfe: + continue + anteil = 100.0 * sum(1 for x in mfe if x > k_med) / len(mfe) + print(f" {N:<8} {_q(mfe, 0.5):>12.2f} {sum(mfe)/len(mfe):>9.2f} " + f"{sum(mae)/len(mae):>9.2f} {anteil:>15.0f} %") + print() + print(" ⚠ Das ist eine OBERGRENZE: gemessen wird die beste der beiden") + print(" Seiten im Nachhinein. Ein echtes Signal muss die Richtung") + print(" VORHER treffen — und das ist im Projekt 23× gescheitert.") + + # ── C) noetige Trefferquote ────────────────────────────────────────── + print() + print("=" * 86) + print(" C) WELCHE TREFFERQUOTE braeuchte ein M1-Scalp?") + print("=" * 86) + if "M1" in kosten: + k = kosten["M1"][0] + print(f" Kosten je Trade (Median) = {k:.3f}×ATR_M1") + print(f" {'Ziel':<8} {'Stop':<8} {'CRV':>6} {'noetige WR':>12} " + f"{'noetige WR OHNE Kosten':>24}") + for ziel, stop in ((0.5, 0.5), (1.0, 1.0), (1.0, 0.5), + (1.5, 1.5), (2.0, 2.0)): + g, v = ziel - k, stop + k + wr = 100.0 * v / (g + v) if g > 0 else float("nan") + wr0 = 100.0 * stop / (ziel + stop) + gg = f"{wr:.1f} %" if g > 0 else "unmoeglich" + print(f" {ziel:<8.1f} {stop:<8.1f} {ziel/stop:>6.2f} " + f"{gg:>12} {wr0:>23.1f} %") + print() + print(" Die letzte Spalte ist der KOSTENFREIE Fall. Der Abstand") + print(" dazwischen ist der Preis, den das Signal zusaetzlich verdienen muss.") + + mt5.shutdown() + return 0 + + +if __name__ == "__main__": + sys.exit(main())