analyze_scalping_costs.py: B3-Check fuer ein M1-Scalping-Modul
User-Frage "wie sinnvoll waere ein Scalping-Modul auf 1M-Basis". Vor dem Bau gemessen, weil es zuerst eine Kosten- und keine Signalfrage ist: der Spread ist absolut ~konstant und skaliert nicht mit der Zeitebene, die Bewegung schon. Spread/ATR im Median: M1 0,319 · M5 0,193 · M15 0,133. Eine mittlere M1-Kerze ist nur 3,4x so gross wie der Spread. Noetige Trefferquote bei 1:1 steigt von 50 auf 65,9 %; um 01:00 uebersteigt der Spread mit 1,051xATR_M1 die gesamte durchschnittliche Bar-Spanne. Dazu drei Gruende, die schwerer wiegen als die Kosten: - M1-Historie ist hart bei 80.000 Bars / 80 Tage gedeckelt -> kein 2-Stichproben-Test ueber verschiedene Regime moeglich - das Aufloesungs-Artefakt vom 05.08. trifft M1 am haertesten (Scalp lebt in 2-3 Bars, Bar-Sim ueberschaetzt enge Stops) - der Slippage-Schwanz ist absolut: Ø 0,234 $ uebersteigt einen 2xATR_M1-Stop (0,174 $) bereits im Mittel Empfehlung: kein eigenstaendiges Modul; messbar waere der Squeeze auf M1 (offene Variante B) beschraenkt auf 10-17 Uhr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bf838b4d03
commit
c888bcc3e8
@@ -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).
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user