Squeeze auf 9 Instrumenten · Momentum neu gemessen · Fill-Annahme korrigiert · UI
Drei Messungen und eine UI-Aenderung, ausgeloest von der Frage, ob das Konzept zu ueberdenken ist. 1) SQUEEZE AUF ANDEREN INSTRUMENTEN (backtest_squeeze_multi.py): 7 von 9 halten alle vier Bedingungen, alle nachbar-robust. Die zwei Durchfaller (Gasoline, USDX) sind exakt die mit Spread >= ATR — der Test scheitert, wo er muss. ⚠ 7 sind nicht 7 unabhaengige Tests (Crude/Brent, Gold/Silber, NAS/GER40 korrelieren), und die Kompression traegt weniger als der Ausbruch selbst (Kontrolle "beliebige Box" ist ueberall positiv). 2) MOMENTUM (backtest_momentum_v2.py): bleibt verworfen. Mit Level-Order sah es nach +0,4 aus — das war Look-ahead. Eine liegende Order fuellt bei der ersten BERUEHRUNG, nicht erst wenn der Bar jenseits des Levels SCHLIESST. Ehrlich gerechnet: -0,04..-0,09 in H1. Struktureller Grund: das Momentum-Level wandert jede Bar mit C[i-N] und ist damit kein Ort, an dem eine Order liegen bleibt. 3) FILL-ANNAHME (backtest_squeeze_touchfill.py): dieselbe Frage fuer den Squeeze. Er haelt (+0,124/+0,292, PF 1,24/1,64) und die Umstellung auf ruhende Orders bleibt richtig (Market -0,158/-0,006). ABER die heute zitierten +0,456/+0,611 sind als Live-Erwartung ~0,32 R zu hoch. Die Beruehrungs-Variante trifft fast genau das original validierte Band +0,14..+0,23. ⚠ OFFEN: derselbe Test fehlt fuer den SIG-Pending-Pfad, der seit 05.08. live ist — und dort ist ein schlechteres Ergebnis zu erwarten, weil das _pend-Level wandert statt stillzustehen. 4) UI (v=143): fester Vorbehalt unter der Headline und "Score" statt nackter Prozentzahl am Ring. Statisches Markup, kein JS. Eigener Fehler dokumentiert: der erste Momentum-Entwurf pruefte nicht, ob das Level als Stop-Order auf der richtigen Marktseite liegt, und lieferte ØR +2,2 — unmoeglich, und genau daran erkennbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
64eef14871
commit
45bfc87153
@@ -319,6 +319,111 @@ 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,
|
~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
|
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.
|
backtesten, wenn der M1-Logger (`candles_m1`, seit 2026-07-21) ab ~2027-01 genug History hat.
|
||||||
|
- **⚠⚠ DER SQUEEZE TRÄGT AUCH AUF ANDEREN INSTRUMENTEN — 7 von 9 (gemessen
|
||||||
|
`backtest_squeeze_multi.py`, 2026-08-06).** Anlass: `analyze_signal_live.py` hat
|
||||||
|
gezeigt, dass die Richtung ein Münzwurf ist; das Grundproblem ist also nicht die
|
||||||
|
Qualität des Squeeze, sondern seine **Frequenz** (2–3×/Woche auf einem
|
||||||
|
Instrument). Auf dieses Problem hat das Projekt bisher zweimal reagiert — mehr
|
||||||
|
Signale auf demselben Instrument (23 Fehlschläge) oder feinere Zeitebene (Kosten
|
||||||
|
erledigen es). Die dritte Achse war unberührt: **dasselbe Signal, mehr Märkte.**
|
||||||
|
| Symbol | Spr/ATR | H1 ØR / PF | H2 ØR / PF | |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **GER40** | 0,086 | +0,668 / 4,23 | +0,595 / 3,82 | ✅ |
|
||||||
|
| **SpotBrent** | 0,184 | +0,503 / 2,90 | +0,653 / 3,85 | ✅ |
|
||||||
|
| **XAUUSD** | 0,028 | +0,559 / 3,74 | +0,556 / 3,95 | ✅ |
|
||||||
|
| **NAS100** | 0,072 | +0,552 / 3,32 | +0,481 / 2,94 | ✅ |
|
||||||
|
| SpotCrude *(Kontrolle)* | 0,181 | +0,460 / 2,67 | +0,605 / 3,51 | ✅ |
|
||||||
|
| XAGUSD | 0,261 | +0,395 / 2,19 | +0,379 / 2,30 | ✅ |
|
||||||
|
| Copper | 0,327 | +0,300 / 1,83 | +0,375 / 2,11 | ✅ |
|
||||||
|
| NatGas | 0,651 | +0,229 / 1,52 | +0,240 / 1,56 | ✅ |
|
||||||
|
| **Gasoline** | **0,972** | −0,610 / 0,35 | −0,148 / 0,76 | ❌ |
|
||||||
|
| **USDX** | **1,972** | −1,269 / 0,13 | −1,317 / 0,14 | ❌ |
|
||||||
|
✅ **Die Kontrolle reproduziert** (SpotCrude +0,460/+0,605 gegen bekannt
|
||||||
|
+0,456/+0,611) — die Pipeline ist in Ordnung. ✅ **Alle 7 sind nachbar-robust**
|
||||||
|
(Box 10/15 × k 0,1/0,2, 28 Zellen, keine kippt) und halten Einstiegs-Slippage bis
|
||||||
|
+0,20×ATR.
|
||||||
|
✅✅ **Die beiden Durchfaller sind der beste Beleg, dass es kein Zufall ist:** es
|
||||||
|
sind exakt die zwei mit **Spread ≥ ATR** — dieselbe Kosten-Arithmetik, die auch
|
||||||
|
M1 erledigt hat. Der Test scheitert dort, wo er scheitern MUSS.
|
||||||
|
⚠ **ATR-Floor relativ gesetzt** (0,542 × Median-ATR statt der WTI-Dollar-Zahl
|
||||||
|
0,06) — ein absoluter Floor wäre auf Gold wirkungslos und auf NatGas ein
|
||||||
|
Selektions-Artefakt (Lehre aus `analyze_tageszeit.py`).
|
||||||
|
⚠⚠ **DREI Vorbehalte, die vor einem Bau geklärt gehören:**
|
||||||
|
(1) **„7 von 9" sind NICHT 7 unabhängige Tests.** Crude/Brent korrelieren ~0,9,
|
||||||
|
Gold/Silber und NAS100/GER40 ebenso — effektiv sind es eher 3–4 unabhängige
|
||||||
|
Fälle, und alle im **selben Makro-Zeitraum**. Mehrere Slots gleichzeitig wären
|
||||||
|
dann dasselbe Risiko doppelt, keine Diversifikation.
|
||||||
|
(2) **Die Kompression trägt weniger als der Ausbruch selbst.** Die Kontrolle
|
||||||
|
„beliebige Box" ist überall ebenfalls positiv (+0,144…+0,533); der Beitrag der
|
||||||
|
Kompression liegt bei **+0,009…+0,155** und ist bei XAGUSD (+0,009/+0,040) und
|
||||||
|
NAS100 (+0,034/+0,013) kaum vorhanden. Gemessen wird hier also zu einem großen
|
||||||
|
Teil **„Ausbruch + unser Exit-Stack"**, nicht der Squeeze im engeren Sinn.
|
||||||
|
(3) **Swap/Kommission je Symbol, Handelszeiten und Mindest-Lots sind NICHT
|
||||||
|
modelliert** — nur der Bar-Spread.
|
||||||
|
⚠ Und der Fill-Befund direkt darunter gilt hier genauso: die Zahlen stehen in
|
||||||
|
Variante (a), real ist (b) — also rund **0,3 R niedriger** anzusetzen.
|
||||||
|
- **⚠⚠⚠ FILL-ANNAHME: „am Level einsteigen" hat ZWEI Bedeutungen — und die
|
||||||
|
teurere ist die richtige (gemessen `backtest_squeeze_touchfill.py` +
|
||||||
|
`backtest_momentum_v2.py`, 2026-08-06).** Aufgefallen beim Momentum-Nachtest.
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **(a) Projekt-Standard** | einsteigen, WO das Signal ausgelöst hat — also dort, wo der **Bar-CLOSE** das Level gebrochen hat, zum Level-Preis |
|
||||||
|
| **(b) Realität** | einsteigen, WANN die liegende Order gefüllt hätte — bei der **ersten Berührung**, egal wo der Bar schließt |
|
||||||
|
**Live gilt (b).** (a) konditioniert auf den Bar-Schluss, den eine ruhende Order
|
||||||
|
nicht kennen kann — das ist **Look-ahead**, kein Modellierungsdetail.
|
||||||
|
**Gemessen auf dem Squeeze (80k M5, 2 Halbjahre, kanonischer Exit, Echtkosten):**
|
||||||
|
| Fill-Annahme | H1 ØR / PF / n | H2 ØR / PF / n |
|
||||||
|
|---|---|---|
|
||||||
|
| (a) Close bricht Level | +0,461 / 2,68 / 801 | +0,605 / 3,51 / 1280 |
|
||||||
|
| **(b) Berührung (LIVE)** | **+0,124 / 1,24 / 1092** | **+0,292 / 1,64 / 1807** |
|
||||||
|
**Aufschlag +0,337 (H1) / +0,312 (H2) R je Trade** — und **291 bzw. 527
|
||||||
|
zusätzliche Fills**, im Schnitt Verlierer (Dochte, die wieder in die Box
|
||||||
|
zurückfallen; passt zu „54 % aller Box-Ausbrüche kehren binnen 3 Bars zurück",
|
||||||
|
`backtest_consolidation.py`).
|
||||||
|
✅ **Der Squeeze hält trotzdem** — beidhälftig positiv, PF > 1. Und die
|
||||||
|
Umstellung auf ruhende Orders bleibt richtig: (b) +0,124/+0,292 gegen
|
||||||
|
**Market −0,158/−0,006**. Bemerkenswert: (b) trifft fast genau das ORIGINAL
|
||||||
|
validierte Band **+0,14…+0,23** aus `backtest_breakout_squeeze.py` — der
|
||||||
|
B4-Maßstab war also die ganze Zeit der realistischere.
|
||||||
|
⚠⚠ **KONSEQUENZ: die heute mehrfach zitierten +0,456/+0,611 sind als
|
||||||
|
Live-Erwartung ~0,32 R zu hoch.** Sie bleiben als Vergleich zwischen Market und
|
||||||
|
Level gültig (dafür waren sie gerechnet), taugen aber nicht als Ertragsprognose.
|
||||||
|
⚠⚠ **OFFEN und wichtig: derselbe Test fehlt für den SIG-Pending-Pfad**
|
||||||
|
(`backtest_auto_signal_v3.py`, +0,35/+0,32 — ebenfalls Variante (a), und seit
|
||||||
|
05.08. live). Es gibt Grund, dort ein **schlechteres** Ergebnis zu erwarten als
|
||||||
|
beim Squeeze: die Squeeze-Box **steht still**, das `_pend`-Level dagegen
|
||||||
|
**wandert** bei jeder k×ATR-Gegenbewegung — genau die Eigenschaft, an der das
|
||||||
|
Momentum-Level gescheitert ist (s. u.), und die Ursache des Pending-Churns
|
||||||
|
(Drift-Fall 9). **Vor weiteren Schlüssen aus den SIG-Zahlen: nachmessen.**
|
||||||
|
- **⚠⚠ MOMENTUM-CONTINUATION MIT LEVEL-ORDER NEU GEMESSEN = BLEIBT VERWORFEN
|
||||||
|
(`backtest_momentum_v2.py`, 2026-08-06, User-Wunsch).** Kein bloßes Wiederholen:
|
||||||
|
der Lauf von 2026-07-17 stieg am **Bar-Close** ein, und genau diese Annahme hat
|
||||||
|
am 05.08. Squeeze und Auto-Signal von negativ auf positiv gedreht. Momentum hat
|
||||||
|
ein sauber definiertes Level (`mom_N` kreuzt X×ATR ⇔ Preis erreicht
|
||||||
|
**C[i−N] ± X×ATR**), die Frage war also offen.
|
||||||
|
| Variante | H1 | H2 |
|
||||||
|
|---|---|---|
|
||||||
|
| A) Bar-Close (der alte Lauf) | −0,10…−0,19 | −0,08…+0,00 |
|
||||||
|
| B) am Level, Fill bei **Kreuzung** | +0,35…+0,47 | +0,47…+0,56 |
|
||||||
|
| **C) am Level, Fill bei BERÜHRUNG (echt)** | **−0,04…−0,09** | **−0,02…+0,07** |
|
||||||
|
**Alle neun N/X-Kombinationen** sehen in B großartig aus und sind in C
|
||||||
|
beidhälftig ohne Edge. **Der Aufschlag von ~0,45 R ist reines Look-ahead.**
|
||||||
|
⚠⚠ **Der strukturelle Grund — und er ist die eigentliche Lehre:** das
|
||||||
|
Momentum-Level **wandert jede Bar** (`C[i−N]` rutscht mit). Es ist damit kein Ort,
|
||||||
|
an dem eine Order liegen bleiben könnte; sie müsste im 5-Minuten-Takt umgesetzt
|
||||||
|
werden und fängt dabei jede Zuckung ein. Die Squeeze-Box **steht** — deshalb hält
|
||||||
|
sie (+0,124/+0,292) und Momentum nicht. **Ein Level ist nur dann ein
|
||||||
|
Ausführungs-Vorteil, wenn es STILLSTEHT.**
|
||||||
|
⚠ **Zwei eigene Fehler auf dem Weg, beide dokumentiert:** (1) der erste Entwurf
|
||||||
|
prüfte nicht, ob das Level als Stop-Order überhaupt auf der **richtigen Seite des
|
||||||
|
Marktes** liegt — dadurch „füllte" die Sim zu längst passierten Preisen und
|
||||||
|
lieferte **ØR +2,2**, was schlicht unmöglich ist und den Fehler verriet.
|
||||||
|
(2) Variante B war überhaupt erst nötig, weil ich den Unterschied
|
||||||
|
Kreuzung/Berührung zunächst nicht gesehen hatte. **Merksatz: eine Zahl, die zu
|
||||||
|
gut ist, ist ein Befund über den Code, nicht über den Markt.**
|
||||||
|
✅ Der Befund bestätigt zugleich die heutige Regel aus dem Kerzen-Fade — „ruhende
|
||||||
|
Order hilft bei Momentum-Entries" — in **präzisierter** Form: sie hilft bei
|
||||||
|
**Ausbrüchen an stehenden Strukturen**, nicht bei Momentum allgemein.
|
||||||
- **⚠⚠ SCALPING-MODUL AUF M1 = ABGERATEN, bevor überhaupt gebaut wird (gemessen
|
- **⚠⚠ SCALPING-MODUL AUF M1 = ABGERATEN, bevor überhaupt gebaut wird (gemessen
|
||||||
`analyze_scalping_costs.py`, 2026-08-06, User-Frage „wie sinnvoll wäre ein
|
`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?").**
|
zusätzliches Modul Scalping auf 1M-Basis, über das Dashboard steuerbar?").**
|
||||||
@@ -3416,7 +3521,17 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
|
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
|
||||||
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
||||||
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
|
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
|
||||||
- Asset-Version aktuell **v=142** (in `web/index.html` hochzählen, siehe Workflows).
|
- **UI-Vorbehalt an der Gesamtempfehlung (2026-08-06, v=143):** Unter der Headline
|
||||||
|
steht jetzt eine feste Zeile (`#vd-note`) — „Richtung ≈ Münzwurf (gemessen 48–51 %
|
||||||
|
Treffer) … lies das als **Auslöser für eine Order am Level**, nicht als
|
||||||
|
Kursprognose". Der Konfidenz-Ring ist mit **„Score"** beschriftet
|
||||||
|
(`.vd-ring-lbl`) statt nackt in Prozent. Grund: `analyze_signal_live.py` (48–51 %
|
||||||
|
Treffer über 1.071 Live-Episoden) und `analyze_verdict_calibration.py` (conf_pct
|
||||||
|
nicht kalibriert, in H2 invertiert). Solange dort „▲ LONG · 89 %" stand, lud die
|
||||||
|
Anzeige zu einer Erwartung ein, die die Messung nicht deckt.
|
||||||
|
⚠ Bewusst **statisches Markup, kein JS** — es ist ein Dauerhinweis, kein Zustand;
|
||||||
|
damit entfällt auch jedes Null-Risiko beim Render (s. Cache-Regel oben).
|
||||||
|
- Asset-Version aktuell **v=143** (in `web/index.html` hochzählen, siehe Workflows).
|
||||||
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
|
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
|
||||||
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
|
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
|
||||||
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Momentum-Continuation — neu gemessen mit ruhender Order am Trigger-Level.
|
||||||
|
|
||||||
|
ANLASS (2026-08-06, User: „backteste auch das Momentum-Signal").
|
||||||
|
|
||||||
|
⚠ WARUM DAS KEINE WIEDERHOLUNG IST. `backtest_momentum.py` (2026-07-17) hat die
|
||||||
|
Idee verworfen: fast alle N/X in H1 negativ, in H2 positiv → regime-abhaengig.
|
||||||
|
Dieser Lauf steigt **am Bar-Close** ein. Genau diese Annahme ist heute zweimal
|
||||||
|
gekippt — beim Squeeze (−0,158 → +0,456) und beim Auto-Signal (−0,09 → +0,35),
|
||||||
|
allein durch den Einstieg AM LEVEL statt am Markt. Momentum hat ein sauber
|
||||||
|
definiertes Level, also ist die Frage offen.
|
||||||
|
|
||||||
|
DAS LEVEL: der Trigger ist `mom_N = C[i] − C[i−N]` kreuzt ±X×ATR. Aufgeloest nach
|
||||||
|
dem Preis ist das exakt
|
||||||
|
Level = C[i−N] ± X × ATR
|
||||||
|
und beide Groessen stehen am Ende von Bar i−1 fest. Eine BUY_STOP/SELL_STOP-Order
|
||||||
|
dort ist also **kausal platzierbar** — kein Look-ahead. Gefuellt wird sie, wenn die
|
||||||
|
Bar-Spanne das Level beruehrt; der Exit startet erst auf der Folgebar.
|
||||||
|
⚠ Bewusst `A[i-1]`, nicht `A[i]`: der ATR von Bar i ist am Ende von i−1 noch nicht
|
||||||
|
bekannt. Der alte Lauf durfte das benutzen, die ruhende Order darf es nicht.
|
||||||
|
|
||||||
|
⚠⚠ GEGENHYPOTHESE, die dieser Lauf ebenfalls prueft. Aus dem Kerzen-Fade von
|
||||||
|
heute stammt die Regel: **ruhende Order hilft bei MOMENTUM-Einstiegen, schadet bei
|
||||||
|
REVERSION-Einstiegen** (dort fuellt sie nur, wenn man falsch liegt). Momentum-
|
||||||
|
Continuation ist ein Momentum-Einstieg — die Regel sagt also „hilft". Faellt es
|
||||||
|
trotzdem durch, ist die Regel selbst angezaehlt. Deshalb laufen A und B parallel.
|
||||||
|
|
||||||
|
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
|
||||||
|
(1) ØR > 0 UND PF > 1 in BEIDEN Haelften
|
||||||
|
(2) mind. 40 Trades je Haelfte
|
||||||
|
(3) die Nachbar-Parameter (N und X je eine Stufe) kippen das Vorzeichen nicht
|
||||||
|
Sonst: bleibt verworfen. Kein nachtraegliches Lockern.
|
||||||
|
|
||||||
|
Aufruf: python backtest_momentum_v2.py [bars]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
from backtest_cost_gate import _atr_series, kennzahlen
|
||||||
|
from core.exit_model import LIVE, simulate
|
||||||
|
|
||||||
|
_N = (12, 24, 48) # Momentum-Fenster in M5-Bars (1 h / 2 h / 4 h)
|
||||||
|
_X = (1.0, 1.5, 2.0) # Schwelle in ×ATR
|
||||||
|
_ATRMIN = 0.06
|
||||||
|
_MIN_N = 40
|
||||||
|
|
||||||
|
|
||||||
|
def signale(H, L, C, SP, A, N, X):
|
||||||
|
"""Trigger-Kandidaten. `lvl` ist am Ende von Bar i−1 bekannt (C[i−N], A[i−1])."""
|
||||||
|
out, letzte = [], None
|
||||||
|
for i in range(max(N, 20) + 1, len(C) - 2):
|
||||||
|
atr = A[i - 1]
|
||||||
|
if not atr or atr < _ATRMIN:
|
||||||
|
continue
|
||||||
|
mom_v, mom = C[i - 1] - C[i - 1 - N], C[i] - C[i - N]
|
||||||
|
d = 0
|
||||||
|
if mom_v <= X * atr < mom:
|
||||||
|
d = 1
|
||||||
|
elif mom_v >= -X * atr > mom:
|
||||||
|
d = -1
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
lvl = C[i - N] + d * X * atr
|
||||||
|
if letzte == (d, round(lvl, 3)):
|
||||||
|
continue
|
||||||
|
letzte = (d, round(lvl, 3))
|
||||||
|
out.append({"i": i, "d": d, "lvl": lvl, "atr": atr,
|
||||||
|
"kosten": (SP[i] if SP[i] > 0 else 0.0225) / atr})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def lauf(K, H, L, C, lo, hi, am_level):
|
||||||
|
"""Sequentiell, EIN Slot. `am_level`=True → ruhende Stop-Order, sonst Bar-Close."""
|
||||||
|
rs, frei_ab = [], lo
|
||||||
|
for k in K:
|
||||||
|
i = k["i"]
|
||||||
|
if not (lo <= i < hi) or i < frei_ab:
|
||||||
|
continue
|
||||||
|
d, atr, lvl = k["d"], k["atr"], k["lvl"]
|
||||||
|
if am_level:
|
||||||
|
# ⚠⚠ PLATZIERBARKEIT: ein BUY_STOP muss UEBER dem Markt liegen, ein
|
||||||
|
# SELL_STOP darunter. Fehlt diese Pruefung, "fuellt" die Sim zu einem
|
||||||
|
# Preis, den der Markt laengst hinter sich hat — das ist keine Order,
|
||||||
|
# das ist geschenktes Geld. Genau daran ist der erste Entwurf
|
||||||
|
# gescheitert (ØR +2,2 in Variante C).
|
||||||
|
if (lvl <= C[i - 1]) if d > 0 else (lvl >= C[i - 1]):
|
||||||
|
continue
|
||||||
|
# Gefuellt nur, wenn die Bar-Spanne das Level erreicht.
|
||||||
|
if (H[i] < lvl) if d > 0 else (L[i] > lvl):
|
||||||
|
continue
|
||||||
|
entry = lvl
|
||||||
|
else:
|
||||||
|
entry = C[i]
|
||||||
|
r, xb = simulate(entry, d, atr, H, L, C, i + 1, LIVE,
|
||||||
|
timestop=True, use_tp=True, ret_bar=True)
|
||||||
|
rs.append(r - k["kosten"])
|
||||||
|
frei_ab = xb + 1
|
||||||
|
return rs
|
||||||
|
|
||||||
|
|
||||||
|
def lauf_echt(H, L, C, SP, A, N, X, lo, hi):
|
||||||
|
"""⚠⚠ DIE EHRLICHE VARIANTE — und sie widerlegt Variante B.
|
||||||
|
|
||||||
|
In B wird nur dort eingestiegen, wo das Momentum am Bar-CLOSE gekreuzt hat.
|
||||||
|
Eine real liegende Stop-Order weiss davon nichts: sie fuellt bei der ERSTEN
|
||||||
|
Beruehrung — auch bei allen Beruehrungen, die wieder zurueckfallen und nie ein
|
||||||
|
Signal werden. B konditioniert also auf den Bar-Schluss und ist damit
|
||||||
|
Look-ahead.
|
||||||
|
|
||||||
|
Verschaerfend: das Momentum-Level `C[i−N] ± X×ATR` WANDERT jede Bar (C[i−N]
|
||||||
|
rutscht mit). Anders als die Squeeze-Box, die ueber viele Bars stillsteht, ist
|
||||||
|
das kein Ort, an dem eine Order liegen bleiben koennte — sie muesste im
|
||||||
|
5-Minuten-Takt umgesetzt werden und faengt dabei jede Zuckung ein.
|
||||||
|
|
||||||
|
Hier deshalb: an JEDER Bar beide Stop-Orders am jeweils gueltigen Level,
|
||||||
|
Fill bei Beruehrung, Exit ab der Folgebar. Das ist das, was live passieren
|
||||||
|
wuerde."""
|
||||||
|
rs, frei_ab = [], lo
|
||||||
|
for j in range(max(N, 20) + 1, min(hi, len(C) - 2)):
|
||||||
|
if j < frei_ab or j < lo:
|
||||||
|
continue
|
||||||
|
atr = A[j - 1]
|
||||||
|
if not atr or atr < _ATRMIN:
|
||||||
|
continue
|
||||||
|
basis = C[j - N]
|
||||||
|
for d in (1, -1):
|
||||||
|
lvl = basis + d * X * atr
|
||||||
|
# ⚠ nur platzierbare Stop-Level (s. Kommentar in `lauf`)
|
||||||
|
if (lvl <= C[j - 1]) if d > 0 else (lvl >= C[j - 1]):
|
||||||
|
continue
|
||||||
|
if (H[j] < lvl) if d > 0 else (L[j] > lvl):
|
||||||
|
continue
|
||||||
|
r, xb = simulate(lvl, d, atr, H, L, C, j + 1, LIVE,
|
||||||
|
timestop=True, use_tp=True, ret_bar=True)
|
||||||
|
rs.append(r - (SP[j] if SP[j] > 0 else 0.0225) / atr)
|
||||||
|
frei_ab = xb + 1
|
||||||
|
break
|
||||||
|
return rs
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
|
||||||
|
point = mt5.symbol_info(sym).point
|
||||||
|
mt5.shutdown()
|
||||||
|
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)
|
||||||
|
m = len(C) // 2
|
||||||
|
|
||||||
|
print("=" * 98)
|
||||||
|
print(f" MOMENTUM-CONTINUATION — Bar-Close gegen ruhende Order am Trigger-Level")
|
||||||
|
print("=" * 98)
|
||||||
|
print(f" {len(C)} M5-Bars · kanonischer Exit · Echtkosten · sequentiell EIN Slot")
|
||||||
|
print(" Level = C[i−N] ± X×ATR[i−1] (am Ende der Vorbar bekannt = platzierbar)")
|
||||||
|
print()
|
||||||
|
print(f" {'N':>3} {'X':>4} | {'A) Bar-Close H1 / H2':^30} | "
|
||||||
|
f"{'B) am Level H1 / H2':^30} | Urteil")
|
||||||
|
print(" " + "-" * 94)
|
||||||
|
|
||||||
|
best = []
|
||||||
|
for N in _N:
|
||||||
|
for X in _X:
|
||||||
|
K = signale(H, L, C, SP, A, N, X)
|
||||||
|
a1, a2 = (kennzahlen(lauf(K, H, L, C, 0, m, False)),
|
||||||
|
kennzahlen(lauf(K, H, L, C, m, len(C), False)))
|
||||||
|
b1, b2 = (kennzahlen(lauf(K, H, L, C, 0, m, True)),
|
||||||
|
kennzahlen(lauf(K, H, L, C, m, len(C), True)))
|
||||||
|
if not all((a1, a2, b1, b2)):
|
||||||
|
print(f" {N:>3} {X:>4} | zu wenige Signale")
|
||||||
|
continue
|
||||||
|
ok = (b1["oer"] > 0 and b2["oer"] > 0 and b1["pf"] > 1 and b2["pf"] > 1
|
||||||
|
and b1["n"] >= _MIN_N and b2["n"] >= _MIN_N)
|
||||||
|
print(f" {N:>3} {X:>4} | {a1['oer']:>+7.3f} n{a1['n']:<4} / "
|
||||||
|
f"{a2['oer']:>+7.3f} n{a2['n']:<4} | "
|
||||||
|
f"{b1['oer']:>+7.3f} n{b1['n']:<4} / "
|
||||||
|
f"{b2['oer']:>+7.3f} n{b2['n']:<4} | {'✅' if ok else '❌'}")
|
||||||
|
if ok:
|
||||||
|
best.append((min(b1["oer"], b2["oer"]), N, X))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 98)
|
||||||
|
if not best:
|
||||||
|
print(" ERGEBNIS: keine Kombination besteht — Momentum bleibt VERWORFEN.")
|
||||||
|
print(" ⚠ Das ist zugleich ein Test der heutigen Regel „ruhende Order hilft")
|
||||||
|
print(" bei Momentum-Einstiegen — haelt sie hier nicht, ist sie enger zu")
|
||||||
|
print(" fassen als gedacht (dann gilt sie fuer AUSBRUECHE, nicht fuer")
|
||||||
|
print(" Momentum allgemein).")
|
||||||
|
return 0
|
||||||
|
print()
|
||||||
|
print("=" * 98)
|
||||||
|
print(" ⚠⚠ GEGENPROBE — ist so eine Order ueberhaupt platzierbar?")
|
||||||
|
print("=" * 98)
|
||||||
|
print(" B steigt nur ein, wo das Momentum am Bar-CLOSE gekreuzt hat. Eine real")
|
||||||
|
print(" liegende Order weiss das nicht: sie fuellt bei der ERSTEN Beruehrung,")
|
||||||
|
print(" auch bei denen, die zurueckfallen. Und das Level wandert jede Bar mit")
|
||||||
|
print(" C[i−N] — anders als die Squeeze-Box, die stillsteht.")
|
||||||
|
print()
|
||||||
|
print(f" {'N':>3} {'X':>4} | {'B) nur bei Kreuzung':^26} | "
|
||||||
|
f"{'C) jede Beruehrung (echt)':^28}")
|
||||||
|
print(" " + "-" * 70)
|
||||||
|
for N in _N:
|
||||||
|
for X in _X:
|
||||||
|
K = signale(H, L, C, SP, A, N, X)
|
||||||
|
b1, b2 = (kennzahlen(lauf(K, H, L, C, 0, m, True)),
|
||||||
|
kennzahlen(lauf(K, H, L, C, m, len(C), True)))
|
||||||
|
c1, c2 = (kennzahlen(lauf_echt(H, L, C, SP, A, N, X, 0, m)),
|
||||||
|
kennzahlen(lauf_echt(H, L, C, SP, A, N, X, m, len(C))))
|
||||||
|
if not all((b1, b2, c1, c2)):
|
||||||
|
continue
|
||||||
|
gut = c1["oer"] > 0 and c2["oer"] > 0
|
||||||
|
print(f" {N:>3} {X:>4} | {b1['oer']:>+7.3f} / {b2['oer']:>+7.3f} n{b1['n']:<5} | "
|
||||||
|
f"{c1['oer']:>+7.3f} / {c2['oer']:>+7.3f} n{c1['n']:<5} "
|
||||||
|
f"{'✅' if gut else '❌'}")
|
||||||
|
|
||||||
|
best.sort(reverse=True)
|
||||||
|
_, N, X = best[0]
|
||||||
|
print(f" BESTE KOMBINATION: N={N} · X={X} → Nachbar-Pruefung")
|
||||||
|
print("=" * 98)
|
||||||
|
alle = True
|
||||||
|
for nn in sorted({max(6, N - 12), N, N + 12}):
|
||||||
|
for xx in sorted({round(X - 0.5, 1), X, round(X + 0.5, 1)}):
|
||||||
|
if xx <= 0:
|
||||||
|
continue
|
||||||
|
K = signale(H, L, C, SP, A, nn, xx)
|
||||||
|
q1, q2 = (kennzahlen(lauf(K, H, L, C, 0, m, True)),
|
||||||
|
kennzahlen(lauf(K, H, L, C, m, len(C), True)))
|
||||||
|
if not q1 or not q2:
|
||||||
|
continue
|
||||||
|
gut = q1["oer"] > 0 and q2["oer"] > 0
|
||||||
|
alle &= gut
|
||||||
|
mark = " ← Optimum" if (nn == N and xx == X) else ""
|
||||||
|
print(f" N={nn:>3} X={xx:<4} H1 {q1['oer']:>+7.3f} n{q1['n']:<4} | "
|
||||||
|
f"H2 {q2['oer']:>+7.3f} n{q2['n']:<4} {'✅' if gut else '❌'}{mark}")
|
||||||
|
print()
|
||||||
|
print(f" → {'ROBUST — Kandidat' if alle else 'KIPPT bei den Nachbarn — verworfen'}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Traegt der validierte Squeeze auch auf ANDEREN Instrumenten?
|
||||||
|
|
||||||
|
ANLASS (2026-08-06): `analyze_signal_live.py` hat gezeigt, dass die Richtungs-
|
||||||
|
aussage der Empfehlung ein Muenzwurf ist (48–51 % ueber 1.071 Episoden). Der
|
||||||
|
Ertrag kommt aus Einstiegspreis und Exit-Asymmetrie. Damit ist das Grundproblem
|
||||||
|
nicht die QUALITAET des Squeeze, sondern seine **Frequenz** — 2–3×/Woche auf
|
||||||
|
einem Instrument.
|
||||||
|
|
||||||
|
Darauf hat das Projekt bisher zweimal reagiert: mehr Signale auf demselben
|
||||||
|
Instrument suchen (**23 verworfene Eingriffe**) oder auf eine feinere Zeitebene
|
||||||
|
gehen (`backtest_squeeze_m1.py` — die Kosten erledigen es). Die dritte Achse ist
|
||||||
|
unberuehrt: **dasselbe Signal, mehr Instrumente.** Praezedenzfall: `breakout_k=0,3`
|
||||||
|
wurde 2026-07-09 auf Brent nachvalidiert (ØR +0,305) — die Mechanik
|
||||||
|
Kompression→Ausbruch ist nicht oelspezifisch.
|
||||||
|
|
||||||
|
⚠⚠ ZWEI FALLEN, DIE HIER EINGEBAUT SIND — beide sind im Projekt schon einmal
|
||||||
|
teuer gewesen:
|
||||||
|
1) **Der ATR-Floor 0,06 ist ein WTI-DOLLAR-Wert.** Auf Gold (ATR ~3) waere er
|
||||||
|
wirkungslos, auf NatGas (~0,05) wuerde er fast alles wegwerfen — und ein
|
||||||
|
Filter, der nach einer Eigenschaft aussortiert, die mit der untersuchten
|
||||||
|
Achse korreliert, ERZEUGT den Befund (Lehre aus `analyze_tageszeit.py` und
|
||||||
|
dem ORB-Selektions-Artefakt). Der Floor wird deshalb **relativ** gesetzt:
|
||||||
|
derselbe Anteil am Median-ATR wie bei WTI.
|
||||||
|
2) **MEHRFACHVERGLEICH.** Acht Instrumente = acht Chancen auf einen Zufalls-
|
||||||
|
treffer. Bei 5 % Fehlalarmquote ist ~0,4 davon zu erwarten. Deshalb gilt die
|
||||||
|
Regel unten UND es wird ausgewiesen, wie viele Kandidaten bestanden haben —
|
||||||
|
bestehen 1 von 8, ist das etwas anderes als 6 von 8.
|
||||||
|
|
||||||
|
VORAB FIXIERTE ENTSCHEIDUNGSREGEL (vor dem ersten Lauf festgelegt):
|
||||||
|
Ein Instrument gilt nur dann als Kandidat, wenn
|
||||||
|
(1) ØR > 0 UND PF > 1 in BEIDEN Haelften,
|
||||||
|
(2) mindestens 40 Ausbrueche je Haelfte (sonst Rauschen),
|
||||||
|
(3) die Nachbar-Parameter (Box 10/15 × k 0,1/0,2) das Vorzeichen nicht kippen,
|
||||||
|
(4) SpotCrude als KONTROLLE seine bekannten Werte reproduziert — sonst ist die
|
||||||
|
Pipeline defekt und nichts davon zaehlt.
|
||||||
|
|
||||||
|
Aufruf: python backtest_squeeze_multi.py [bars]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
import core.wave_rec as wr
|
||||||
|
from backtest_cost_gate import _atr_series, kennzahlen
|
||||||
|
from core.exit_model import LIVE, simulate
|
||||||
|
from core.wave_rec import WaveRecommender
|
||||||
|
|
||||||
|
_SYMBOLE = ["SpotCrude", # Kontrolle — muss reproduzieren
|
||||||
|
"SpotBrent", "NatGas", "Gasoline", "Copper",
|
||||||
|
"XAUUSD", "XAGUSD", "NAS100", "GER40", "USDX"]
|
||||||
|
_MIN_N = 40
|
||||||
|
|
||||||
|
|
||||||
|
def hole(sym, n):
|
||||||
|
if not mt5.symbol_select(sym, True):
|
||||||
|
return None
|
||||||
|
si = mt5.symbol_info(sym)
|
||||||
|
b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
|
||||||
|
if b is None or len(b) < 5000 or si is None:
|
||||||
|
return None
|
||||||
|
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"]) * si.point for x in b], "digits": si.digits}
|
||||||
|
|
||||||
|
|
||||||
|
def kandidaten(d, A, floor, box=None, k=None, mult=None):
|
||||||
|
"""`mult` = Kompressions-Schwelle. Sehr hoch gesetzt (99) entfaellt die
|
||||||
|
Kompressions-Bedingung → Ausbruch aus einer BELIEBIGEN Box = die Kontrolle,
|
||||||
|
die den Squeeze auf WTI ueberhaupt erst validiert hat (dort schlug er die
|
||||||
|
beliebige Box in BEIDEN Haelften: +0,227/+0,141 gegen −0,045/+0,161)."""
|
||||||
|
alt_n, alt_k, alt_m = wr._SQ_N, wr._SQ_K, wr._SQ_MULT
|
||||||
|
if box is not None:
|
||||||
|
wr._SQ_N = box
|
||||||
|
if k is not None:
|
||||||
|
wr._SQ_K = k
|
||||||
|
if mult is not None:
|
||||||
|
wr._SQ_MULT = mult
|
||||||
|
H, L, C, SP = d["H"], d["L"], d["C"], d["SP"]
|
||||||
|
out, last = [], None
|
||||||
|
try:
|
||||||
|
for i in range(wr._SQ_N + 2, len(C) - 2):
|
||||||
|
atr = A[i]
|
||||||
|
if not atr or atr < floor:
|
||||||
|
continue
|
||||||
|
w0 = i - wr._SQ_N - 2
|
||||||
|
st = WaveRecommender._squeeze_one(H[w0:i + 1], L[w0:i + 1],
|
||||||
|
C[w0:i + 1], atr)
|
||||||
|
if st.get("state") != "active":
|
||||||
|
last = None
|
||||||
|
continue
|
||||||
|
sig = (st["dir"], st["level"])
|
||||||
|
if sig == last:
|
||||||
|
continue
|
||||||
|
last = sig
|
||||||
|
# ⚠ Kosten: kein WTI-Ersatzwert (0,0225) fuer fremde Instrumente —
|
||||||
|
# fehlt der Bar-Spread, wird der Median dieses Symbols genommen.
|
||||||
|
out.append({"i": i, "d": 1 if st["dir"] == "LONG" else -1,
|
||||||
|
"lvl": float(st["level"]), "atr": atr, "sp": SP[i]})
|
||||||
|
finally:
|
||||||
|
wr._SQ_N, wr._SQ_K, wr._SQ_MULT = alt_n, alt_k, alt_m
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def lauf(K, d, lo, hi, med_sp, slip=0.0):
|
||||||
|
H, L, C = d["H"], d["L"], d["C"]
|
||||||
|
rs, frei_ab = [], lo
|
||||||
|
for kd in K:
|
||||||
|
i = kd["i"]
|
||||||
|
if not (lo <= i < hi) or i < frei_ab:
|
||||||
|
continue
|
||||||
|
sp = kd["sp"] if kd["sp"] > 0 else med_sp
|
||||||
|
entry = kd["lvl"] + kd["d"] * slip * kd["atr"]
|
||||||
|
r, xb = simulate(entry, kd["d"], kd["atr"], H, L, C, i + 1, LIVE,
|
||||||
|
timestop=True, use_tp=True, ret_bar=True)
|
||||||
|
rs.append(r - sp / kd["atr"])
|
||||||
|
frei_ab = xb + 1
|
||||||
|
return rs
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
|
||||||
|
if not mt5.initialize():
|
||||||
|
print("MT5 nicht erreichbar"); return 1
|
||||||
|
|
||||||
|
daten, atr_med = {}, {}
|
||||||
|
for s in _SYMBOLE:
|
||||||
|
d = hole(s, n)
|
||||||
|
if not d:
|
||||||
|
print(f" ⚠ {s}: keine Daten / nicht handelbar — uebersprungen")
|
||||||
|
continue
|
||||||
|
d["A"] = _atr_series(d["H"], d["L"], d["C"])
|
||||||
|
gute = sorted(a for a in d["A"][20:] if a > 0)
|
||||||
|
if not gute:
|
||||||
|
continue
|
||||||
|
atr_med[s] = gute[len(gute) // 2]
|
||||||
|
sps = sorted(x for x in d["SP"][20:] if x > 0)
|
||||||
|
d["med_sp"] = sps[len(sps) // 2] if sps else 0.0
|
||||||
|
daten[s] = d
|
||||||
|
mt5.shutdown()
|
||||||
|
|
||||||
|
if "SpotCrude" not in daten:
|
||||||
|
print("SpotCrude fehlt — ohne Kontrolle keine Aussage"); return 1
|
||||||
|
# Relativer Floor: derselbe Anteil am Median-ATR wie live bei WTI
|
||||||
|
rel = 0.06 / atr_med["SpotCrude"]
|
||||||
|
|
||||||
|
print("=" * 100)
|
||||||
|
print(" SQUEEZE AUF MEHREREN INSTRUMENTEN — dasselbe Signal, mehr Maerkte")
|
||||||
|
print("=" * 100)
|
||||||
|
print(f" Bars je Symbol: {n} (M5) · Einstieg AM Level (ruhende Stop-Order) ·")
|
||||||
|
print(f" kanonischer Exit · Echtkosten aus dem Bar-Spread · sequentiell EIN Slot")
|
||||||
|
print(f" ATR-Floor relativ: {rel:.3f} × Median-ATR "
|
||||||
|
f"(= 0,06 $ bei WTI, massstabsgerecht uebertragen)")
|
||||||
|
print()
|
||||||
|
print(f" {'Symbol':<12} {'Med ATR':>9} {'Med Spr':>9} {'Spr/ATR':>8} "
|
||||||
|
f"{'H1 ØR':>8} {'PF':>5} {'n':>5} | {'H2 ØR':>8} {'PF':>5} {'n':>5} Urteil")
|
||||||
|
print(" " + "-" * 96)
|
||||||
|
|
||||||
|
besteht = []
|
||||||
|
for s, d in daten.items():
|
||||||
|
floor = rel * atr_med[s]
|
||||||
|
K = kandidaten(d, d["A"], floor)
|
||||||
|
m = len(d["C"]) // 2
|
||||||
|
r1 = lauf(K, d, 0, m, d["med_sp"])
|
||||||
|
r2 = lauf(K, d, m, len(d["C"]), d["med_sp"])
|
||||||
|
k1, k2 = kennzahlen(r1), kennzahlen(r2)
|
||||||
|
if not k1 or not k2:
|
||||||
|
print(f" {s:<12} — keine Ausbrueche")
|
||||||
|
continue
|
||||||
|
ok = (k1["oer"] > 0 and k2["oer"] > 0 and k1["pf"] > 1 and k2["pf"] > 1
|
||||||
|
and k1["n"] >= _MIN_N and k2["n"] >= _MIN_N)
|
||||||
|
duenn = "" if (k1["n"] >= _MIN_N and k2["n"] >= _MIN_N) else " (zu duenn)"
|
||||||
|
print(f" {s:<12} {atr_med[s]:>9.4f} {d['med_sp']:>9.4f} "
|
||||||
|
f"{d['med_sp']/atr_med[s]:>8.3f} "
|
||||||
|
f"{k1['oer']:>+8.3f} {k1['pf']:>5.2f} {k1['n']:>5} | "
|
||||||
|
f"{k2['oer']:>+8.3f} {k2['pf']:>5.2f} {k2['n']:>5} "
|
||||||
|
f"{'✅' if ok else '❌'}{duenn}")
|
||||||
|
if ok and s != "SpotCrude":
|
||||||
|
besteht.append(s)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 100)
|
||||||
|
print(" NACHBAR-PARAMETER der bestandenen Kandidaten (Box × k)")
|
||||||
|
print("=" * 100)
|
||||||
|
if not besteht:
|
||||||
|
print(" keiner bestanden — nichts zu pruefen")
|
||||||
|
fest = []
|
||||||
|
for s in besteht:
|
||||||
|
d = daten[s]
|
||||||
|
floor = rel * atr_med[s]
|
||||||
|
m = len(d["C"]) // 2
|
||||||
|
alle_ok, zeilen = True, []
|
||||||
|
for box, kk in ((10, 0.1), (15, 0.1), (10, 0.2), (15, 0.2)):
|
||||||
|
K = kandidaten(d, d["A"], floor, box=box, k=kk)
|
||||||
|
a, b = (kennzahlen(lauf(K, d, 0, m, d["med_sp"])),
|
||||||
|
kennzahlen(lauf(K, d, m, len(d["C"]), d["med_sp"])))
|
||||||
|
if not a or not b:
|
||||||
|
alle_ok = False
|
||||||
|
zeilen.append(f" Box {box} k {kk}: keine Ausbrueche")
|
||||||
|
continue
|
||||||
|
gut = a["oer"] > 0 and b["oer"] > 0
|
||||||
|
alle_ok &= gut
|
||||||
|
zeilen.append(f" Box {box} k {kk}: H1 {a['oer']:+.3f} (n{a['n']}) | "
|
||||||
|
f"H2 {b['oer']:+.3f} (n{b['n']}) {'✅' if gut else '❌'}")
|
||||||
|
print(f" {s}:")
|
||||||
|
for z in zeilen:
|
||||||
|
print(z)
|
||||||
|
print(f" → {'ROBUST' if alle_ok else 'KIPPT — verworfen'}")
|
||||||
|
if alle_ok:
|
||||||
|
fest.append(s)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 100)
|
||||||
|
print(" KONTROLLE — schlaegt die KOMPRESSION den Ausbruch aus BELIEBIGER Box?")
|
||||||
|
print("=" * 100)
|
||||||
|
print(" ⚠ Ohne diese Zeile waere der Befund nicht interpretierbar: 'Breakout +")
|
||||||
|
print(" Trailing' hat generisch Edge. Traegt die Kompression nichts bei, misst")
|
||||||
|
print(" man nur den Exit — und dann braucht es kein Squeeze-Modul.")
|
||||||
|
print(f" {'Symbol':<12} {'Squeeze H1/H2':>22} {'beliebige Box H1/H2':>24} Beitrag")
|
||||||
|
for s in fest:
|
||||||
|
d = daten[s]; floor = rel * atr_med[s]; m = len(d["C"]) // 2
|
||||||
|
Ks = kandidaten(d, d["A"], floor)
|
||||||
|
Kb = kandidaten(d, d["A"], floor, mult=99.0)
|
||||||
|
a1, a2 = (kennzahlen(lauf(Ks, d, 0, m, d["med_sp"])),
|
||||||
|
kennzahlen(lauf(Ks, d, m, len(d["C"]), d["med_sp"])))
|
||||||
|
b1, b2 = (kennzahlen(lauf(Kb, d, 0, m, d["med_sp"])),
|
||||||
|
kennzahlen(lauf(Kb, d, m, len(d["C"]), d["med_sp"])))
|
||||||
|
if not all((a1, a2, b1, b2)):
|
||||||
|
continue
|
||||||
|
gut = a1["oer"] > b1["oer"] and a2["oer"] > b2["oer"]
|
||||||
|
print(f" {s:<12} {a1['oer']:>+10.3f}/{a2['oer']:>+10.3f} "
|
||||||
|
f"{b1['oer']:>+12.3f}/{b2['oer']:>+10.3f} "
|
||||||
|
f"{'✅ Kompression traegt' if gut else '❌ kein Beitrag'}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 100)
|
||||||
|
print(" SLIPPAGE-HAERTETEST — die Stop-Order loest aus, fuellt aber zum Markt")
|
||||||
|
print("=" * 100)
|
||||||
|
for s in fest:
|
||||||
|
d = daten[s]; floor = rel * atr_med[s]; m = len(d["C"]) // 2
|
||||||
|
K = kandidaten(d, d["A"], floor)
|
||||||
|
zeile = f" {s:<12}"
|
||||||
|
for sl in (0.0, 0.10, 0.20):
|
||||||
|
x1 = kennzahlen(lauf(K, d, 0, m, d["med_sp"], slip=sl))
|
||||||
|
x2 = kennzahlen(lauf(K, d, m, len(d["C"]), d["med_sp"], slip=sl))
|
||||||
|
if x1 and x2:
|
||||||
|
zeile += f" +{sl:.2f}: {x1['oer']:+.3f}/{x2['oer']:+.3f}"
|
||||||
|
print(zeile)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 100)
|
||||||
|
print(f" ERGEBNIS: {len(fest)} von {len(daten)-1} geprueften Fremd-Instrumenten "
|
||||||
|
f"halten alle vier Bedingungen")
|
||||||
|
if fest:
|
||||||
|
print(f" Kandidaten: {', '.join(fest)}")
|
||||||
|
print()
|
||||||
|
print(" ⚠ MEHRFACHVERGLEICH mitlesen: bei 5 % Fehlalarmquote und "
|
||||||
|
f"{len(daten)-1} Tests")
|
||||||
|
print(" waere ~0,4 Zufallstreffer zu erwarten. 1 von 9 ist deshalb schwach,")
|
||||||
|
print(" mehrere sind ein Muster. Die Nachbar-Pruefung senkt die Rate zusaetzlich.")
|
||||||
|
print(" ⚠ NICHT gemessen: Korrelation der Instrumente untereinander (mehrere")
|
||||||
|
print(" Slots gleichzeitig = evtl. dasselbe Risiko doppelt), Swap/Kommission")
|
||||||
|
print(" je Symbol, Handelszeiten und Mindest-Lots. Das gehoert VOR einen Bau.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fuellt die ruhende Squeeze-Order wirklich so, wie der Backtest unterstellt?
|
||||||
|
|
||||||
|
ANLASS (2026-08-06): Bei `backtest_momentum_v2.py` ist aufgefallen, dass ein
|
||||||
|
"Einstieg am Level" zwei sehr verschiedene Dinge bedeuten kann:
|
||||||
|
(a) einsteigen, WO das Signal ausgeloest hat — also dort, wo der Bar-CLOSE das
|
||||||
|
Level ueberschritten hat, und zwar zum Level-Preis;
|
||||||
|
(b) einsteigen, WANN die liegende Order gefuellt haette — bei der ERSTEN
|
||||||
|
Beruehrung des Levels, unabhaengig davon, wo der Bar schliesst.
|
||||||
|
Live gilt (b). Der Unterschied ist kein Detail: beim Momentum lag er bei **0,45 R**
|
||||||
|
und hat aus +0,40 ein −0,06 gemacht — die Variante (a) konditioniert auf den
|
||||||
|
Bar-Schluss, den eine liegende Order nicht kennen kann. Look-ahead.
|
||||||
|
|
||||||
|
⚠ DIESELBE FRAGE MUSS FUER DEN SQUEEZE BEANTWORTET WERDEN, denn `squeeze_pending_
|
||||||
|
entry` laeuft seit 05.08. mit ECHTEM Geld, und die Erwartung dahinter (+0,456/
|
||||||
|
+0,611 aus `backtest_squeeze_entry.py`) stammt aus Variante (a).
|
||||||
|
|
||||||
|
GRUND ZUR HOFFNUNG — aber eben nur Hoffnung, bis gemessen: die Squeeze-Box steht
|
||||||
|
ueber viele Bars still, das Level ist also ein echter, stabiler Ort fuer eine
|
||||||
|
Order. Das Momentum-Level wandert dagegen mit `C[i−N]` jede Bar mit. Wenn der
|
||||||
|
Unterschied klein ist, war die Hoffnung berechtigt; ist er gross, ist die
|
||||||
|
Live-Erwartung zu hoch angesetzt und gehoert korrigiert.
|
||||||
|
|
||||||
|
Aufruf: python backtest_squeeze_touchfill.py [bars]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
import core.wave_rec as wr
|
||||||
|
from backtest_cost_gate import _atr_series, kennzahlen
|
||||||
|
from core.exit_model import LIVE, simulate
|
||||||
|
from core.wave_rec import WaveRecommender
|
||||||
|
|
||||||
|
_ATRMIN = 0.06
|
||||||
|
|
||||||
|
|
||||||
|
def boxen(H, L, C, A):
|
||||||
|
"""Je Bar die AKTUELLE Box-Grenze + Ausbruchs-Level — auch ohne Ausbruch.
|
||||||
|
|
||||||
|
Das ist der Zustand, in dem die ruhende Order liegt (`armed`): die Box steht,
|
||||||
|
die beiden Stop-Level stehen. Ob der Kurs sie beruehrt, entscheidet der Markt.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for i in range(wr._SQ_N + 2, len(C) - 2):
|
||||||
|
atr = A[i - 1] # am Ende der Vorbar bekannt
|
||||||
|
if not atr or atr < _ATRMIN:
|
||||||
|
out.append(None); continue
|
||||||
|
hb = H[i - wr._SQ_N - 1:i - 1]; lb = L[i - wr._SQ_N - 1:i - 1]
|
||||||
|
if not hb or not lb:
|
||||||
|
out.append(None); continue
|
||||||
|
hi, lo = max(hb), min(lb)
|
||||||
|
if (hi - lo) / atr > wr._SQ_MULT: # keine Kompression → keine Order
|
||||||
|
out.append(None); continue
|
||||||
|
out.append((i, hi + wr._SQ_K * atr, lo - wr._SQ_K * atr, atr))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def lauf_close(H, L, C, SP, A, lo, hi):
|
||||||
|
"""(a) Projekt-Standard: Einstieg dort, wo `_squeeze_one` `active` meldet
|
||||||
|
(Bar-CLOSE hat das Level gebrochen), zum Level-Preis."""
|
||||||
|
rs, frei_ab, last = [], lo, None
|
||||||
|
for i in range(wr._SQ_N + 2, len(C) - 2):
|
||||||
|
if not (lo <= i < hi) or i < frei_ab:
|
||||||
|
continue
|
||||||
|
atr = A[i]
|
||||||
|
if not atr or atr < _ATRMIN:
|
||||||
|
continue
|
||||||
|
w0 = i - wr._SQ_N - 2
|
||||||
|
st = WaveRecommender._squeeze_one(H[w0:i + 1], L[w0:i + 1], C[w0:i + 1], atr)
|
||||||
|
if st.get("state") != "active":
|
||||||
|
last = None; continue
|
||||||
|
sig = (st["dir"], st["level"])
|
||||||
|
if sig == last:
|
||||||
|
continue
|
||||||
|
last = sig
|
||||||
|
d = 1 if st["dir"] == "LONG" else -1
|
||||||
|
r, xb = simulate(float(st["level"]), d, atr, 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) / atr)
|
||||||
|
frei_ab = xb + 1
|
||||||
|
return rs
|
||||||
|
|
||||||
|
|
||||||
|
def lauf_touch(H, L, C, SP, A, B, lo, hi):
|
||||||
|
"""(b) Realitaet: die Order liegt an beiden Box-Grenzen und fuellt bei der
|
||||||
|
ERSTEN Beruehrung — auch wenn der Bar wieder in die Box zurueckfaellt."""
|
||||||
|
rs, frei_ab = [], lo
|
||||||
|
for eintrag in B:
|
||||||
|
if eintrag is None:
|
||||||
|
continue
|
||||||
|
i, lvl_hi, lvl_lo, atr = eintrag
|
||||||
|
if not (lo <= i < hi) or i < frei_ab:
|
||||||
|
continue
|
||||||
|
d = 0
|
||||||
|
if H[i] >= lvl_hi:
|
||||||
|
d, lvl = 1, lvl_hi
|
||||||
|
elif L[i] <= lvl_lo:
|
||||||
|
d, lvl = -1, lvl_lo
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
r, xb = simulate(lvl, d, atr, 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) / atr)
|
||||||
|
frei_ab = xb + 1
|
||||||
|
return rs
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
|
||||||
|
point = mt5.symbol_info(sym).point
|
||||||
|
mt5.shutdown()
|
||||||
|
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)
|
||||||
|
B = boxen(H, L, C, A)
|
||||||
|
m = len(C) // 2
|
||||||
|
|
||||||
|
print("=" * 92)
|
||||||
|
print(f" SQUEEZE — Fill bei BAR-CLOSE-Ausbruch gegen Fill bei BERUEHRUNG ({sym})")
|
||||||
|
print("=" * 92)
|
||||||
|
print(f" {len(C)} M5-Bars · kanonischer Exit · Echtkosten · sequentiell EIN Slot")
|
||||||
|
print()
|
||||||
|
a1 = kennzahlen(lauf_close(H, L, C, SP, A, 0, m))
|
||||||
|
a2 = kennzahlen(lauf_close(H, L, C, SP, A, m, len(C)))
|
||||||
|
b1 = kennzahlen(lauf_touch(H, L, C, SP, A, B, 0, m))
|
||||||
|
b2 = kennzahlen(lauf_touch(H, L, C, SP, A, B, m, len(C)))
|
||||||
|
for lbl, x1, x2 in (("(a) Close bricht Level (Backtest-Standard)", a1, a2),
|
||||||
|
("(b) Order fuellt bei Beruehrung (LIVE)", b1, b2)):
|
||||||
|
if not x1 or not x2:
|
||||||
|
print(f" {lbl:<44} — keine Trades"); continue
|
||||||
|
print(f" {lbl:<44} H1 ØR {x1['oer']:+.3f} PF {x1['pf']:.2f} n{x1['n']:<5} | "
|
||||||
|
f"H2 ØR {x2['oer']:+.3f} PF {x2['pf']:.2f} n{x2['n']:<5}")
|
||||||
|
if all((a1, a2, b1, b2)):
|
||||||
|
print()
|
||||||
|
print(f" Look-ahead-Aufschlag: H1 {a1['oer']-b1['oer']:+.3f} · "
|
||||||
|
f"H2 {a2['oer']-b2['oer']:+.3f} R je Trade")
|
||||||
|
print(f" Zusaetzliche Fills durch Beruehrung: H1 {b1['n']-a1['n']:+d} · "
|
||||||
|
f"H2 {b2['n']-a2['n']:+d}")
|
||||||
|
gut = b1["oer"] > 0 and b2["oer"] > 0 and b1["pf"] > 1 and b2["pf"] > 1
|
||||||
|
print()
|
||||||
|
print(f" → Der Squeeze haelt der realistischen Fill-Annahme "
|
||||||
|
f"{'STAND ✅' if gut else 'NICHT stand ❌'}")
|
||||||
|
print(" ⚠ Zum Vergleich Momentum (`backtest_momentum_v2.py`): dort betrug")
|
||||||
|
print(" der Aufschlag ~0,45 R und drehte +0,40 auf −0,06.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+22
-3
@@ -6,7 +6,7 @@
|
|||||||
<meta name="theme-color" content="#0d1117">
|
<meta name="theme-color" content="#0d1117">
|
||||||
<title>Oil · MT5</title>
|
<title>Oil · MT5</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="stylesheet" href="style.css?v=142">
|
<link rel="stylesheet" href="style.css?v=143">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -81,12 +81,31 @@
|
|||||||
<section class="card wide" id="card-verdict">
|
<section class="card wide" id="card-verdict">
|
||||||
<h2>Gesamtempfehlung</h2>
|
<h2>Gesamtempfehlung</h2>
|
||||||
<div class="vd-head">
|
<div class="vd-head">
|
||||||
<div class="vd-ring" id="vd-ring"><span id="vd-conf">—</span></div>
|
<!-- ⚠ Der Ring ist ein SCORE, keine Trefferwahrscheinlichkeit. Gemessen
|
||||||
|
(`analyze_verdict_calibration.py`) ist conf_pct nicht kalibriert und
|
||||||
|
in H2 sogar invertiert — die Beschriftung sagt das jetzt auch. -->
|
||||||
|
<div class="vd-ring" id="vd-ring"
|
||||||
|
title="Aggregierter Score aus den Modulen — KEINE Trefferwahrscheinlichkeit. Gemessen ist conf_pct nicht kalibriert (in der zweiten Stichprobe sogar invertiert): hohe Werte bedeuten starken Trend + Konfluenz, und das sind im Chop-Regime gerade die Überdehnungen vor der Wende.">
|
||||||
|
<span class="vd-ring-in"><b id="vd-conf">—</b><i class="vd-ring-lbl">Score</i></span>
|
||||||
|
</div>
|
||||||
<div class="vd-main">
|
<div class="vd-main">
|
||||||
<div class="vd-signal" id="vd-signal">◌ —</div>
|
<div class="vd-signal" id="vd-signal">◌ —</div>
|
||||||
<div class="vd-consensus" id="vd-consensus">—</div>
|
<div class="vd-consensus" id="vd-consensus">—</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- ⚠⚠ STEHENDER VORBEHALT (2026-08-06, gemessen `analyze_signal_live.py`
|
||||||
|
an 1.071 Live-Episoden): die Richtungsaussage trifft in 48–51 % der
|
||||||
|
Fälle — ein Münzwurf. Der Ertrag entsteht nachweislich aus dem
|
||||||
|
EINSTIEGSPREIS (ruhende Order am Level: ØR +0,35/+0,32 gegen −0,09/−0,11
|
||||||
|
am Markt) und der Exit-Asymmetrie, nicht aus der Trefferquote. Die Zeile
|
||||||
|
steht bewusst fest im Markup und nicht in JS: sie ist ein Dauerhinweis,
|
||||||
|
kein Zustand. -->
|
||||||
|
<div class="vd-note" id="vd-note"
|
||||||
|
title="Gemessen an 1.071 Live-Episoden (08.05.–06.08.2026), gegen die Drift der jeweiligen Hälfte gelesen: Treffer 48,3–51,1 %, Überschuss max. +0,085×ATR — unter den realen Handelskosten von 0,265×ATR.">
|
||||||
|
ⓘ Richtung ≈ Münzwurf (gemessen 48–51 % Treffer). Der Ertrag kommt aus
|
||||||
|
Einstiegspreis und Exit, nicht aus der Trefferquote — lies das hier als
|
||||||
|
<b>Auslöser für eine Order am Level</b>, nicht als Kursprognose.
|
||||||
|
</div>
|
||||||
<div class="vd-meter">
|
<div class="vd-meter">
|
||||||
<span class="vd-end short">SHORT</span>
|
<span class="vd-end short">SHORT</span>
|
||||||
<div class="vd-track"><div class="vd-needle" id="vd-needle"></div></div>
|
<div class="vd-track"><div class="vd-needle" id="vd-needle"></div></div>
|
||||||
@@ -312,6 +331,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="toast" class="toast hidden"></div>
|
<div id="toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script src="app.js?v=142"></script>
|
<script src="app.js?v=143"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+13
-2
@@ -208,8 +208,19 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
|
|||||||
display:flex;align-items:center;justify-content:center;
|
display:flex;align-items:center;justify-content:center;
|
||||||
background:conic-gradient(var(--dim) 0deg, var(--line) 0deg)}
|
background:conic-gradient(var(--dim) 0deg, var(--line) 0deg)}
|
||||||
.vd-ring span{width:46px;height:46px;border-radius:50%;background:var(--card);
|
.vd-ring span{width:46px;height:46px;border-radius:50%;background:var(--card);
|
||||||
display:flex;align-items:center;justify-content:center;
|
display:flex;flex-direction:column;align-items:center;
|
||||||
font-size:18px;font-weight:700}
|
justify-content:center;font-size:18px;font-weight:700}
|
||||||
|
/* ⚠ Der Ring zeigt einen SCORE, keine Trefferwahrscheinlichkeit — die Beschriftung
|
||||||
|
sagt das jetzt mit (gemessen: conf_pct ist nicht kalibriert, in H2 invertiert). */
|
||||||
|
#vd-conf{line-height:1}
|
||||||
|
.vd-ring-lbl{font-size:10px;font-weight:600;font-style:normal;color:var(--dim);
|
||||||
|
letter-spacing:.5px;line-height:1;margin-top:1px}
|
||||||
|
/* Stehender Vorbehalt unter der Headline: die Richtung ist gemessen ein Muenzwurf.
|
||||||
|
Bewusst gedaempft und gestrichelt — Kontext, keine Warnung. */
|
||||||
|
.vd-note{font-size:13px;line-height:1.45;color:var(--dim);background:var(--surface-2);
|
||||||
|
border:1px dashed var(--line);border-radius:var(--r-sm);
|
||||||
|
padding:7px 10px;margin:0 0 11px}
|
||||||
|
.vd-note b{color:var(--bright);font-weight:700}
|
||||||
.vd-main{flex:1;min-width:0}
|
.vd-main{flex:1;min-width:0}
|
||||||
.vd-signal{font-size:30px;font-weight:800;color:var(--dim);line-height:1.1}
|
.vd-signal{font-size:30px;font-weight:800;color:var(--dim);line-height:1.1}
|
||||||
.vd-signal.long{color:var(--up)} .vd-signal.short{color:var(--down)}
|
.vd-signal.long{color:var(--up)} .vd-signal.short{color:var(--down)}
|
||||||
|
|||||||
Reference in New Issue
Block a user