#!/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())