VWAP-Baender auf M15 gemessen = verworfen (24. Signal-Eingriff)

User-Wunsch nach einem M15-Daytrading-Modul auf Profi-Methodik. Abgleich: fast
alles aus dem Werkzeugkasten ist hier schon durchgefallen (Volume Profile,
Liquidity Sweeps, Market Structure, ORB, RSI/MACD, Inter-Market);
Footprint/CVD/DOM existieren nicht (Market-Maker-Broker). VWAP war die EINZIGE
nie gemessene Zutat.

Gemessen: 26.000 M15-Bars, session-verankert auf den Berliner Kalendertag,
2 Haelften, kanonischer Exit, Echtkosten, Fill bei Beruehrung.

ZUERST DER EIGENE FEHLER: der erste Lauf zeigte Reversion mit OeR +0,13 bis
+0,27 und PF bis 1,71 in beiden Haelften - verdaechtig sauber. Das Band an Bar i
wird MIT Bar i berechnet, und getestet wurde, ob genau diese Bar es beruehrt.
Ein Ausschlag zieht das Band zu sich hin und erzeugt seine eigene Beruehrung.
Mit Band aus i-1 bleibt:
  k=2,0   +0,130/+0,130  ->  +0,041/-0,009
  k=2,5   +0,189/+0,235  ->  +0,046/+0,087
  k=3,0   +0,162/+0,272  ->  +0,024/+0,010
Der Look-ahead war 0,10-0,25 R wert.

Alle drei vorab fixierten Kriterien verfehlt:
  (b) Eine WILLKUERLICHE Linie gleicher Distanz haelt in 9 von 10 Zellen
      genauso gut oder besser (VWAP 70,3-75,1 %, Zufall 73,9-76,6 %) - der
      Fibonacci-Befund reproduziert.
  (c) k=2,0 kippt in H2, also der Nachbar der besten Zelle. Und alle sechs
      95-%-Bootstrap-Intervalle enthalten die Null (beste Zelle k=2,5 H2:
      +0,087 [-0,012 ... +0,185]).
  (a) Die Regel war von mir unterspezifiziert ("<= Squeeze-Box-Niveau" = 0 ist
      fuer ein gleitendes Band unerfuellbar). Gemessen: Median 0,03-0,05xATR je
      Bar, 90-Perzentil aber bis 0,34xATR - mehr als die Einstiegstoleranz.

Kohaerenz-Kontrolle: der Ausbruch an den Baendern ist spiegelbildlich negativ.
Reversion leicht positiv, Ausbruch klar negativ - ein stimmiges Bild.

Nichts gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-08 14:19:17 +02:00
co-authored by Claude Opus 5
parent 30c8616280
commit 56a3fe31d8
2 changed files with 325 additions and 0 deletions
+52
View File
@@ -4942,6 +4942,58 @@ Modell schliesst keine Trades.
„die Rangfolge ist kaputt" ist **nicht** belegt; belegt ist nur die „die Rangfolge ist kaputt" ist **nicht** belegt; belegt ist nur die
Basisraten-/Kalibrierungslücke. Basisraten-/Kalibrierungslücke.
## ⚠⚠ VWAP-BÄNDER AUF M15 = VERWORFEN (2026-08-08) — die letzte ungetestete Zutat
Anlass: User-Wunsch nach einem **M15-Daytrading-Modul** auf Profi-Methodik
(Order-Flow, Volume Profile, VWAP, Session-Struktur, Liquidity Sweeps, MSS,
Mean-Reversion). Abgleich mit dem Gemessenen ergab: **fast alles davon ist in
diesem Projekt bereits durchgefallen** — Volume Profile (POC-Reversion
beidhälftig negativ), Liquidity Sweeps (alle 9 Kombis), Market Structure (24/24
Zellen), ORB (mit realer Fill-Annahme alle 4), RSI/MACD, Inter-Market.
Footprint/CVD/DOM existieren hier nicht (Market-Maker-Broker ohne Orderbuch; das
HL-Orderbuch ist gemessen 0,21,0 bp gegen ~3 bp Kostenhürde und läuft dem CFD
**hinterher**). Kpler/Bloomberg scheitern am Preis, CoT und Cushing an der
wöchentlichen Frequenz.
**VWAP war die EINZIGE nie gemessene Zutat** (`calc_vwap` existiert in
`core/analysis/indicators.py`, taucht in CLAUDE.md nirgends als Signal auf).
Jetzt gemessen: `backtest_vwap_m15.py`, 26.000 M15-Bars (02.07.2507.08.26),
session-verankert auf den Berliner Kalendertag, 2 Hälften, kanonischer Exit,
Echtkosten, **Fill bei Berührung**.
⚠⚠ **ZUERST DER EIGENE FEHLER — er ist die halbe Lehre.** Der erste Lauf zeigte
Reversion bei k=2,03,0 mit **ØR +0,13 bis +0,27 und PF bis 1,71 in BEIDEN
Hälften** — verdächtig sauber. Ursache: das Band an Bar `i` wird **mit** Bar `i`
berechnet (der VWAP kumuliert sie), und getestet wurde, ob genau diese Bar es
berührt. **Ein Ausschlag zieht das Band zu sich hin und erzeugt seine eigene
Berührung.** Mit Band aus `i1` (live zwangsläufig der Fall) bleibt:
| k | vorher H1/H2 | **nach dem Fix** |
|---|---|---|
| 2,0 | +0,130 / +0,130 | **+0,041 / 0,009** |
| 2,5 | +0,189 / +0,235 | **+0,046 / +0,087** |
| 3,0 | +0,162 / +0,272 | **+0,024 / +0,010** |
**Der Look-ahead war 0,100,25 R wert** — dieselbe Größenordnung wie beim
Squeeze (0,32), Momentum (0,45) und SIG (0,770,85).
**ALLE DREI vorab fixierten Kriterien verfehlt:**
· **(b) Haltequote gegen Zufallskontrolle** — eine **willkürliche Linie gleicher
Distanz** hält in **9 von 10 Zellen genauso gut oder besser** (VWAP 70,375,1 %,
Zufall 73,976,6 %). Exakt der Fibonacci-Befund reproduziert: die Frage ist nie
„hält das Level?", sondern „hält es BESSER als eine beliebige Linie?".
· **(c) Ertrag** — k=2,0 kippt in H2 (0,009), also kippt der **Nachbar** der
besten Zelle. Und **alle sechs 95-%-Bootstrap-Intervalle enthalten die Null**
(k=2,5 H2, die beste Zelle: +0,087 [0,012 … +0,185]).
· **(a) Beweglichkeit** — ⚠ **die Regel war von mir unterspezifiziert**
(„≤ Squeeze-Box-Niveau" = 0 ist für ein gleitendes Band unerfüllbar). Gemessen
rückt das Band im Median nur 0,030,05×ATR je Bar, im **90-Perzentil aber bis
0,34×ATR** — mehr als die Einstiegstoleranz von 0,15. Eine ruhende Order müsste
also laufend nachgezogen werden: dasselbe operative Problem wie bei `_pend`.
**Kohärenz-Kontrolle:** der **Ausbruch** an den Bändern ist spiegelbildlich
negativ (H1 0,107…−0,230 über alle k). Reversion leicht positiv, Ausbruch klar
negativ — ein stimmiges Bild, kein Zufallsrauschen.
**24. verworfener Signal-Eingriff.** Damit ist aus dem eingereichten
Profi-Werkzeugkasten **keine ungetestete Zutat mehr offen.**
## ⚠⚠ VERLUSTBEGRENZUNG BEI „FALSCHEN" EINSTIEGEN — BEIDE IDEEN GEMESSEN, BEIDE DURCHGEFALLEN (2026-08-07) ## ⚠⚠ VERLUSTBEGRENZUNG BEI „FALSCHEN" EINSTIEGEN — BEIDE IDEEN GEMESSEN, BEIDE DURCHGEFALLEN (2026-08-07)
User: „um meine Verluste bei falschen Einstiegen zu begrenzen würde ich gerne den User: „um meine Verluste bei falschen Einstiegen zu begrenzen würde ich gerne den
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""VWAP-Bänder auf M15 — taugen sie als LEVEL für ein Daytrading-Modul?
ANLASS (2026-08-08, User-Wunsch nach einem M15-Modul auf Profi-Methodik). Vom
eingereichten Werkzeugkasten ist fast alles in diesem Projekt schon gemessen und
durchgefallen — Volume Profile (POC-Reversion beidhälftig negativ), Liquidity
Sweeps (alle 9 Kombis), Market Structure (24/24 Zellen), ORB (alle 4 Varianten
mit realer Fill-Annahme), RSI/MACD, Inter-Market. Footprint/CVD/DOM gibt es hier
gar nicht (Market-Maker-Broker ohne Orderbuch).
**VWAP ist die einzige Zutat aus der Liste, die nie gemessen wurde** —
`core/analysis/indicators.calc_vwap` existiert, taucht in CLAUDE.md aber nirgends
als Signal auf.
⚠⚠ DIE VORFRAGE ENTSCHEIDET ALLES — und sie steht deshalb ganz vorne:
**STEHT das Band still?** Der einzige im Projekt belegte Hebel ist die
Ausführung: eine ruhende Order an einem STEHENDEN Level (Squeeze +0,32 R
gegenüber Market; SIG +0,35/+0,32 am Level gegen 0,09/0,11 am Markt). Und der
Gegenbeleg ist genauso klar: das Momentum-Level WANDERT jede Bar, deshalb fällt
es durch (+0,45 R Aufschlag war reines Look-ahead). Ein VWAP-Band bewegt sich
per Konstruktion mit jedem Tick. Wandert es so stark wie das Momentum-Level, ist
das Modul erledigt, bevor irgendein Ertrag gerechnet wird.
GEMESSEN WIRD, in dieser Reihenfolge:
0) BEWEGLICHKEIT — wie weit rückt das Band je Bar (in ×ATR)? Gegen zwei
Referenzen: die Squeeze-Box (steht, trägt) und `C[iN]` (wandert, trägt nicht).
1) HALTEQUOTE — prallt der Kurs am Band ab? ⚠ MIT KONTROLLGRUPPE: eine
Zufallslinie gleicher Distanz. Die Fibonacci-Messung hat gezeigt, dass ein
BELIEBIGES Level hier zu ~55 % hält — ohne Kontrolle misst man die Basisrate.
2) ERTRAG — Reversion (Limit am Band) und Ausbruch (Stop hinter dem
Band), beide mit FILL BEI BERÜHRUNG (die ehrliche Annahme, s. 06.08.),
kanonischer Exit, Echtkosten, 2 Halbjahre.
⚠⚠ ENTSCHEIDUNGSREGEL, VORAB FESTGELEGT:
(a) Beweglichkeit ≤ Squeeze-Box-Niveau, sonst Abbruch — ein wanderndes Level
ist als ruhende Order nicht handelbar.
(b) Haltequote muss die Zufallskontrolle in BEIDEN Hälften schlagen.
(c) ØR > 0 und PF > 1 in BEIDEN Hälften, und die Nachbar-k dürfen nicht kippen.
Sonst: kein Einbau. Bei 23 gescheiterten Signal-Eingriffen gilt hohe Beweislast.
Aufruf: python backtest_vwap_m15.py [n_M15_bars] (Default 26000 ≈ 9 Monate)
"""
from __future__ import annotations
import sys
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
import MetaTrader5 as mt5
from core.exit_model import LIVE, simulate
_BERLIN = ZoneInfo("Europe/Berlin")
_BROKER = timezone(timedelta(hours=3)) # Brokerzeit = UTC+3
_ATRMIN = 0.12
_BAENDER = (1.0, 1.5, 2.0, 2.5, 3.0)
_ZONE = 0.15 # „am Band" in ×ATR (wie `at_level` live)
def _berlin(ts: int) -> datetime:
"""⚠ MT5 liefert die BROKER-Wallclock, keinen echten UTC-Epoch. Genau hier
ist `analyze_tageszeit.py` am 05.08. um drei Stunden verrutscht."""
return datetime.fromtimestamp(ts, timezone.utc).replace(
tzinfo=_BROKER).astimezone(_BERLIN)
def atr_series(H, L, C, p=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 = [None]
for i in range(1, len(C)):
seg = tr[max(1, i - p + 1):i + 1]
out.append(sum(seg) / len(seg))
return out
def vwap_baender(T, H, L, C, V):
"""Session-verankerter VWAP + kumulierte Standardabweichung je M15-Bar.
Anker = Berliner Kalendertag (Reset 00:00). ⚠ KAUSAL: an Bar i fliessen nur
Bars bis EINSCHLIESSLICH i ein — das Band an i ist also aus i bekannt, und
eine Order kann fruehestens ab i+1 liegen (so wird unten auch gehandelt).
"""
vw = [None] * len(C)
sd = [None] * len(C)
cum_pv = cum_v = cum_p2v = 0.0
tag = None
for i in range(len(C)):
d = _berlin(T[i]).date()
if d != tag:
tag = d
cum_pv = cum_v = cum_p2v = 0.0
tp = (H[i] + L[i] + C[i]) / 3.0
v = float(V[i] or 1)
cum_pv += tp * v
cum_v += v
cum_p2v += tp * tp * v
if cum_v <= 0:
continue
m = cum_pv / cum_v
var = max(0.0, cum_p2v / cum_v - m * m)
vw[i] = m
sd[i] = var ** 0.5
return vw, sd
def beweglichkeit(vw, sd, AT, k):
"""Wie weit rueckt das obere Band je Bar, in ×ATR? Median + 90-Perzentil."""
d = []
for i in range(1, len(vw)):
if not (vw[i] and vw[i - 1] and sd[i] is not None and sd[i - 1] is not None):
continue
a = AT[i]
if not a or a <= 0:
continue
o1 = vw[i - 1] + k * sd[i - 1]
o2 = vw[i] + k * sd[i]
d.append(abs(o2 - o1) / max(a, _ATRMIN))
if not d:
return None
d.sort()
return d[len(d) // 2], d[int(0.9 * len(d))]
def halte_quote(H, L, C, AT, vw, sd, k, lo, hi, fenster=8, zufall=False):
"""Prallt der Kurs am Band ab? Beruehrung = Bar-Spanne trifft die Zone.
Ausgang: erreicht er zuerst Band∓0,5×ATR (haelt) oder Band±0,5×ATR (bricht)?
`zufall=True` = Kontrolllinie in derselben Distanz, aber ohne VWAP-Bezug."""
import random
rnd = random.Random(7)
haelt = bricht = 0
war = False
for i in range(max(lo, 1), min(hi, len(C) - fenster - 1)):
# ⚠⚠ BAND AUS i-1, NICHT AUS i. Der VWAP an i enthaelt Bar i selbst —
# gegen die Spanne DERSELBEN Bar zu testen ist Look-ahead: ein Ausschlag
# zieht das Band zu sich hin und erzeugt so seine eigene „Beruehrung".
# Live liegt die Order zwangslaeufig auf dem Stand des VORBARS.
if not vw[i - 1] or sd[i - 1] is None:
war = False
continue
a = AT[i]
if not a or a <= 0:
war = False
continue
a = max(a, _ATRMIN)
lvl = vw[i - 1] + k * sd[i - 1]
if zufall:
# gleiche Distanz zum Kurs, aber willkuerliche Lage
dist = abs(lvl - C[i])
lvl = C[i] + dist * rnd.choice((1, -1))
oben = lvl > C[i]
beruehrt = (L[i] <= lvl + _ZONE * a) and (H[i] >= lvl - _ZONE * a)
if beruehrt and not war:
durch = lvl + (0.5 * a if oben else -0.5 * a)
zurueck = lvl - (0.5 * a if oben else -0.5 * a)
aus = None
for j in range(i + 1, i + 1 + fenster):
if (H[j] >= durch) if oben else (L[j] <= durch):
aus = "break"; break
if (L[j] <= zurueck) if oben else (H[j] >= zurueck):
aus = "halt"; break
if aus == "break":
bricht += 1
else:
haelt += 1 # Timeout zaehlt als Halt (wie im P(break)-Training)
war = beruehrt
n = haelt + bricht
return (n, haelt / n if n else None)
def ertrag(H, L, C, AT, SP, vw, sd, k, lo, hi, art):
"""`art`: 'reversion' = Limit AM Band, Richtung zurueck zum VWAP ·
'ausbruch' = Stop HINTER dem Band, Richtung weiter.
⚠ FILL BEI BERUEHRUNG — die einzige live erreichbare Annahme (s. 06.08.:
der Close-Bruch-Fill ist Look-ahead und schenkt 0,30,85 R)."""
Rs = []
i = max(lo, 1)
war = False
while i < min(hi, len(C) - LIVE.max_hold - 2):
# ⚠⚠ Band aus i-1 (s. `halte_quote`): das an i berechnete Band enthaelt
# Bar i selbst und waere Look-ahead.
if not vw[i - 1] or sd[i - 1] is None:
war = False; i += 1; continue
a = AT[i]
if not a or a <= 0:
war = False; i += 1; continue
a = max(a, _ATRMIN)
for seite in (1, -1): # oberes und unteres Band
lvl = vw[i - 1] + seite * k * sd[i - 1]
beruehrt = (L[i] <= lvl + _ZONE * a) and (H[i] >= lvl - _ZONE * a)
if not beruehrt or war:
continue
d = -seite if art == "reversion" else seite
kosten = (SP[i] if SP[i] > 0 else 0.0225) / a
R, xb = simulate(lvl, d, a, H, L, C, i + 1, LIVE, ret_bar=True)
Rs.append(R - kosten)
i = xb
break
if vw[i - 1] and sd[i - 1] is not None:
_o = vw[i - 1] + k * sd[i - 1]
war = (L[i] <= _o + _ZONE * a) and (H[i] >= _o - _ZONE * a)
else:
war = False
i += 1
return Rs
def kz(Rs):
if not Rs:
return "n=0"
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0)
return (f"n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} "
f"PF={(g/v if v > 0 else 9.99):>4.2f} ΣR={s:>+6.0f}")
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 26000
mt5.initialize()
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
if mt5.symbol_info(c)), None)
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M15, 0, n)
point = mt5.symbol_info(sym).point
mt5.shutdown()
if bars is None or len(bars) < 5000:
print(f"zu wenige M15-Bars ({0 if bars is None else len(bars)})"); return
T = [int(b["time"]) for b in bars]
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
C = [float(b["close"]) for b in bars]
V = [float(b["tick_volume"]) for b in bars]
SP = [float(b["spread"]) * point for b in bars]
AT = atr_series(H, L, C)
vw, sd = vwap_baender(T, H, L, C, V)
mid = len(C) // 2
print("=" * 92)
print(f" VWAP-BÄNDER auf M15 — {sym}, {len(C)} Bars "
f"({_berlin(T[0]):%d.%m.%Y} bis {_berlin(T[-1]):%d.%m.%Y})")
print("=" * 92)
# ── 0) DIE VORFRAGE ───────────────────────────────────────────────────
print("\n 0) BEWEGLICHKEIT — steht das Band still? (Verschiebung je Bar in ×ATR)")
print(f" {'k':>5}{'Median':>10}{'90-Perzentil':>15}")
for k in _BAENDER:
b = beweglichkeit(vw, sd, AT, k)
if b:
print(f" {k:>5.1f}{b[0]:>10.3f}{b[1]:>15.3f}")
print(" Referenz: Squeeze-Box steht (0,000 zwischen den Ausbrüchen) und TRÄGT;")
print(" das Momentum-Level `C[iN]` wandert und TRÄGT NICHT.")
# ── 1) HALTEQUOTE mit Kontrolle ───────────────────────────────────────
print("\n 1) HALTEQUOTE am oberen Band — gegen eine Zufallslinie gleicher Distanz")
print(f" {'k':>5}{'H1 VWAP':>12}{'H1 Zufall':>12}{'H2 VWAP':>12}{'H2 Zufall':>12}")
for k in _BAENDER:
z = []
for lo, hi in ((0, mid), (mid, len(C))):
for zuf in (False, True):
nn, q = halte_quote(H, L, C, AT, vw, sd, k, lo, hi, zufall=zuf)
z.append(f"{100*q:.1f}% ({nn})" if q is not None else "")
print(f" {k:>5.1f}" + "".join(f"{x:>12}" for x in z))
# ── 2) ERTRAG ─────────────────────────────────────────────────────────
for art in ("reversion", "ausbruch"):
print(f"\n 2) ERTRAG — {art.upper()} (Fill bei Berührung, kanonischer Exit, Echtkosten)")
for k in _BAENDER:
r1 = ertrag(H, L, C, AT, SP, vw, sd, k, 0, mid, art)
r2 = ertrag(H, L, C, AT, SP, vw, sd, k, mid, len(C), art)
print(f" k={k:<4} H1 {kz(r1)}")
print(f" {'':<7}H2 {kz(r2)}")
if __name__ == "__main__":
main()