Roast-Punkte 1-3: Erfolgskontrolle erweitert, flip_close sichtbar, WARTEN-Massstab
korrigiert - und ein eigener Messfehler gefunden
(3) backtest_dist_gated.py: der 43-%-Massstab war UNGUELTIG. backtest_dist.py
misst _build ALLEIN, ohne Breakout-Bestaetigung und ohne Entry-Raum-Gate - genau
die beiden sind live die groessten Blocker. Ueber 80k Bars: nur _build 77,9 %
gerichtet, voller Live-Stack 19,5 %, live 7,2 %. Der reale Abstand ist 12,3 Pp,
nicht 50. Damit ist der "93 statt 43 %"-Alarm entschaerft, der den TF-Churn-Fix
ausgeloest hat; die dortige Erfolgskontrolle ist hinfaellig. Ein Rest bleibt
(min_conf live 16,3 % gegen 0,9 % in der Sim) und ist als naechster Ansatzpunkt
notiert.
⚠ Dabei EIGENEN Fehler gefunden: set_entry_room(0.6) fehlte in DREI heute neu
gebauten Skripten - _entry_room_atr ist im Konstruktor 0.0, _room_gate ist dann
ein No-op. Aufgefallen, weil entry_room in der Verteilung mit 0 % auftauchte
statt mit 35 %. Alle drei korrigiert und die SIG-Messung WIEDERHOLT: Population
aendert sich stark (n 967->295 / 1327->524), die Schlussfolgerung nicht - Market
in 7 von 8 Feldern negativ, am Level bestehen alle vier Schwellen
(OeR +0,392/+0,356, PF 2,37/2,39), Slippage-Test haelt bis 0,20 xATR. Die
heutige Bau-Entscheidung ist gedeckt.
(2) auto_flip_close ist jetzt sichtbar (#flip-note, v=139) - mit Schwelle,
Zaehler UND der Messung im Text ("gemessen in beiden Halbjahren negativ"). Ein
blanker Zaehler haette wie ein Erfolg ausgesehen. Der Exit bleibt an.
(1) analyze_squeeze_entry_gap.py erfasst jetzt BEIDE Bot-Pfade (Squeeze +
Signal), Reminder entsprechend erweitert - sonst wartet er auf eine Population,
die vielleicht nie kommt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
90d77a2305
commit
516f1eaa7f
@@ -331,6 +331,29 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
(`backtest_conf.py`): Band **40–54 % = negativer Edge** (−0,025), ≥55 % = +0,058;
|
(`backtest_conf.py`): Band **40–54 % = negativer Edge** (−0,025), ≥55 % = +0,058;
|
||||||
Gate hebt Ø-Edge ~+50 % und den Gesamtertrag (entfernt die schwachen Setups, bei
|
Gate hebt Ø-Edge ~+50 % und den Gesamtertrag (entfernt die schwachen Setups, bei
|
||||||
denen zu viele Strafen gestapelt sind — typisch der Lagging-Short am Boden).
|
denen zu viele Strafen gestapelt sind — typisch der Lagging-Short am Boden).
|
||||||
|
- **⚠⚠ DER 43-%-MASSSTAB WAR UNGÜLTIG — der richtige liegt bei ~80 % WARTEN
|
||||||
|
(gemessen `backtest_dist_gated.py`, 2026-08-05).** `backtest_dist.py` misst
|
||||||
|
**`_build` ALLEIN**, ohne **Breakout-Bestätigung** und ohne **Entry-Raum-Gate**.
|
||||||
|
Genau die beiden sind live die größten Blocker. Über 80k M5-Bars:
|
||||||
|
| | gerichtet | WARTEN |
|
||||||
|
|---|---|---|
|
||||||
|
| nur `_build` (die alte Referenz) | **77,9 %** | 22,1 % |
|
||||||
|
| **voller Live-Stack** | **19,5 %** | **80,5 %** |
|
||||||
|
| LIVE (seit 01.08., ohne `stale`) | 7,2 % | 92,8 % |
|
||||||
|
Aufschlüsselung im vollen Stack: entry_room **41,4 %** · breakout_pending 17,0 % ·
|
||||||
|
stretch 13,8 % · deadband 7,4 % · min_conf 0,9 %.
|
||||||
|
✅ **Damit ist der „93 % statt 43 %"-Alarm entschärft**, der den TF-Churn-Fix
|
||||||
|
(2026-07-31) ausgelöst hat: verglichen wurden zwei verschiedene Systeme. Der reale
|
||||||
|
Abstand zum gültigen Maßstab ist **12,3 Pp**, nicht 50.
|
||||||
|
⚠ **Ein Rest bleibt** (19,5 → 7,2 %) und ist NICHT wegerklärt. Kandidaten, die eine
|
||||||
|
Bar-Simulation prinzipiell nicht abbildet: der **TF-Wechsel** (live wandert die
|
||||||
|
Basis-TF M5…M30, die Sim ist fix M5), Cooldowns, EIA-/News-Fenster, und die
|
||||||
|
**Abtastung** — live wird pro Sekunde gemessen, ein `breakout_pending`-Zustand
|
||||||
|
hält über eine ganze Bar an und ist deshalb live überrepräsentiert (26,5 % gegen
|
||||||
|
17,0 %). Der auffälligste Einzelposten ist **min_conf: live 16,3 %, Sim 0,9 %** —
|
||||||
|
das ist der nächste konkrete Ansatzpunkt, wenn jemand dem Rest nachgehen will.
|
||||||
|
⚠ **Die im TF-Churn-Fix dokumentierte Erfolgskontrolle („WARTEN-Anteil Richtung
|
||||||
|
43 % bewegen") ist damit hinfällig** und sollte nicht weiterverfolgt werden.
|
||||||
- **Signalhäufigkeit** (`backtest_dist.py`): ~57 % der Zeit LONG/SHORT, ~43 % WARTEN.
|
- **Signalhäufigkeit** (`backtest_dist.py`): ~57 % der Zeit LONG/SHORT, ~43 % WARTEN.
|
||||||
WARTEN-Treiber: M30-Gegen-Trend (~27 % der Bars, gewollt, Edge×2) + Überdehnung
|
WARTEN-Treiber: M30-Gegen-Trend (~27 % der Bars, gewollt, Edge×2) + Überdehnung
|
||||||
+ Totband. `_STRETCH_MAX` 2,5→**3,5** angehoben (`backtest_stretch.py`: +26 %
|
+ Totband. `_STRETCH_MAX` 2,5→**3,5** angehoben (`backtest_stretch.py`: +26 %
|
||||||
@@ -1436,6 +1459,21 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
⚠ **B4-Pflicht:** dieselbe Erfolgskontrolle wie beim Squeeze — der Abstand
|
⚠ **B4-Pflicht:** dieselbe Erfolgskontrolle wie beim Squeeze — der Abstand
|
||||||
Einstieg↔Level muss gegen 0 gehen. AUTOSIG-Trades sind über `setup=AUTOSIG_*`
|
Einstieg↔Level muss gegen 0 gehen. AUTOSIG-Trades sind über `setup=AUTOSIG_*`
|
||||||
separierbar.
|
separierbar.
|
||||||
|
⚠⚠ **ZWEITER Methodik-Fehler, am selben Tag gefunden und die Messung WIEDERHOLT:
|
||||||
|
`set_entry_room(0.6)` fehlte.** `WaveRecommender.__init__` setzt
|
||||||
|
`_entry_room_atr = 0.0`, und `_room_gate` gibt dann `rec` **unverändert** zurück —
|
||||||
|
der Aufruf sieht im Code aus wie ein Gate, ist aber ein No-op. Betroffen waren
|
||||||
|
DREI am 05.08. neu gebaute Skripte (`backtest_auto_signal_v3`, `_metalabel`,
|
||||||
|
`_dist_gated`); die Referenz `backtest_breakout_gated.py` (04.08.) macht es richtig.
|
||||||
|
Aufgefallen ist es erst, weil `entry_room` in der Verteilung mit **0 %** auftauchte,
|
||||||
|
live aber 35 % blockt. **Neu gerechnet ändert sich die Population stark (n 967→295
|
||||||
|
bzw. 1327→524), die Schlussfolgerung aber NICHT:** Market bleibt in 7 von 8 Feldern
|
||||||
|
negativ, am Level bestehen weiterhin **alle vier** Konfidenz-Schwellen
|
||||||
|
(ØR +0,392/+0,356 · PF 2,37/2,39), und der Slippage-Härtetest hält bis 0,20×ATR
|
||||||
|
(+0,177/+0,118). **Die heutige Bau-Entscheidung ist damit gedeckt.**
|
||||||
|
⚠ Lehre: ein aufgerufenes Gate ist nicht dasselbe wie ein aktives Gate. Beim
|
||||||
|
Kopieren einer Backtest-Struktur gehört die **Konfiguration** mitkopiert — sonst
|
||||||
|
misst man einen Stack, den es nicht gibt.
|
||||||
⚠ **Methodik-Fehler beim Bau, gefunden und behoben:** der erste Entwurf liess
|
⚠ **Methodik-Fehler beim Bau, gefunden und behoben:** der erste Entwurf liess
|
||||||
`pend["level"]` nach der Bestätigung über weitere Bars stehen — bei verzögertem
|
`pend["level"]` nach der Bestätigung über weitere Bars stehen — bei verzögertem
|
||||||
Einstieg (Slot belegt, Konfidenz zu niedrig) wäre er zu einem Preis eingestiegen,
|
Einstieg (Slot belegt, Konfidenz zu niedrig) wäre er zu einem Preis eingestiegen,
|
||||||
@@ -2933,7 +2971,7 @@ 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=138** (in `web/index.html` hochzählen, siehe Workflows).
|
- Asset-Version aktuell **v=139** (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` —
|
||||||
@@ -3649,10 +3687,13 @@ Funktionsreferenzen in CLAUDE.md stimmen · Asset- und Indikator-Version konsist
|
|||||||
ein reines Migrations-Artefakt (Spalte kam am 31.07.); seit 01.08. **100 % Abdeckung**.
|
ein reines Migrations-Artefakt (Spalte kam am 31.07.); seit 01.08. **100 % Abdeckung**.
|
||||||
|
|
||||||
**Offen (nicht behoben, bewusst):**
|
**Offen (nicht behoben, bewusst):**
|
||||||
- **`auto_flip_close=true` läuft ohne jede UI-Anzeige.** Ein Exit, der gemessen in
|
- ✅ **ERLEDIGT 2026-08-05: `auto_flip_close` ist jetzt SICHTBAR** (`#flip-note` in
|
||||||
BEIDEN Hälften Geld kostet (−92/−160 R), schließt echte Trades; `flip_close`,
|
der Meldungen-Karte, v=139). Zeigt Schwelle und Auslöse-Zähler und **nennt
|
||||||
`flip_close_min_r` und `flip_close_count` haben keinen Frontend-Konsumenten. Nur
|
ausdrücklich die Messung** („⚠ gemessen in beiden Halbjahren negativ", −92/−160 R)
|
||||||
Log + Telegram. Entweder anzeigen oder abschalten — Entscheidung des Users.
|
— ein blanker Zähler hätte wie ein Erfolg ausgesehen. Der Exit bleibt AN
|
||||||
|
(User-Wunsch), aber die Entscheidung ist jetzt informiert. Live-Stand: 4
|
||||||
|
Auslösungen, +284,22 € (n=4 = ohne Aussagekraft). Abschalten:
|
||||||
|
`[trading] auto_flip_close=false`.
|
||||||
- **`breakout_k=0,3` ruht weiter auf dem `LEGACY_BE10`-Exit** (Breakeven 1,0 statt
|
- **`breakout_k=0,3` ruht weiter auf dem `LEGACY_BE10`-Exit** (Breakeven 1,0 statt
|
||||||
live 1,3). Der Legacy-Recheck hat EIA und Dead-Hours nachgerechnet, `breakout_k`
|
live 1,3). Der Legacy-Recheck hat EIA und Dead-Hours nachgerechnet, `breakout_k`
|
||||||
nicht. Kandidat für den nächsten Recheck.
|
nicht. Kandidat für den nächsten Recheck.
|
||||||
|
|||||||
@@ -33,8 +33,17 @@ _BERLIN = ZoneInfo("Europe/Berlin")
|
|||||||
_BROKER = timezone(timedelta(hours=3))
|
_BROKER = timezone(timedelta(hours=3))
|
||||||
_UTC = timezone.utc
|
_UTC = timezone.utc
|
||||||
_UMBAU = datetime(2026, 8, 5, 8, 55, tzinfo=_BERLIN) # Pending-Entry ging live
|
_UMBAU = datetime(2026, 8, 5, 8, 55, tzinfo=_BERLIN) # Pending-Entry ging live
|
||||||
_RX = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d).*AUTO-SQUEEZE-ENTRY: (\w+) "
|
# ⚠ BEIDE Market-Pfade erfassen (2026-08-05): seit dem Pending-Umbau laeuft auch
|
||||||
r"@ Ausbruch ([\d.]+)")
|
# der Signal-Entry ueber eine ruhende Stop-Order. Nur der FALLBACK schreibt noch
|
||||||
|
# eine Log-Zeile mit Level — die faengt dieses Muster ebenfalls ein.
|
||||||
|
_RX = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d).*"
|
||||||
|
r"(?:AUTO-SQUEEZE-ENTRY: (\w+) @ Ausbruch ([\d.]+)"
|
||||||
|
r"|AUTO-SIGNAL-ENTRY: (\w+) )")
|
||||||
|
# Der Signal-Log nennt kein Level (es steht nur im Snapshot) — dort wird der
|
||||||
|
# Abstand deshalb nicht berechnet, der Trade zaehlt aber als Market-Fallback.
|
||||||
|
_RX_SIG = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d).*AUTO-SIGNAL-ENTRY: (\w+) ")
|
||||||
|
_RX_SQ = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d).*AUTO-SQUEEZE-ENTRY: (\w+) "
|
||||||
|
r"@ Ausbruch ([\d.]+)")
|
||||||
|
|
||||||
|
|
||||||
def _berlin(ts):
|
def _berlin(ts):
|
||||||
@@ -57,20 +66,27 @@ def main():
|
|||||||
seit = datetime.now(_BERLIN) - timedelta(days=tage)
|
seit = datetime.now(_BERLIN) - timedelta(days=tage)
|
||||||
|
|
||||||
# ── 1) Log: Ausbruchs-Level je Market-Entry ──────────────────────────
|
# ── 1) Log: Ausbruchs-Level je Market-Entry ──────────────────────────
|
||||||
log_eintraege = []
|
log_eintraege, sig_fallback = [], []
|
||||||
for f in sorted(glob.glob(os.path.join(_HIER, "oil_widget.log*"))):
|
for f in sorted(glob.glob(os.path.join(_HIER, "oil_widget.log*"))):
|
||||||
try:
|
try:
|
||||||
with open(f, encoding="utf-8", errors="replace") as fh:
|
with open(f, encoding="utf-8", errors="replace") as fh:
|
||||||
for z in fh:
|
for z in fh:
|
||||||
m = _RX.match(z)
|
m = _RX_SQ.match(z)
|
||||||
if m:
|
if m:
|
||||||
ts = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") \
|
ts = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") \
|
||||||
.replace(tzinfo=_BERLIN)
|
.replace(tzinfo=_BERLIN)
|
||||||
if ts >= seit:
|
if ts >= seit:
|
||||||
log_eintraege.append((ts, m.group(2), float(m.group(3))))
|
log_eintraege.append((ts, m.group(2), float(m.group(3))))
|
||||||
|
continue
|
||||||
|
m = _RX_SIG.match(z)
|
||||||
|
if m:
|
||||||
|
ts = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") \
|
||||||
|
.replace(tzinfo=_BERLIN)
|
||||||
|
if ts >= seit:
|
||||||
|
sig_fallback.append((ts, m.group(2)))
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
log_eintraege.sort()
|
log_eintraege.sort(); sig_fallback.sort()
|
||||||
|
|
||||||
# ── 2) ATR-Reihe für die Normierung ──────────────────────────────────
|
# ── 2) ATR-Reihe für die Normierung ──────────────────────────────────
|
||||||
mt5.initialize()
|
mt5.initialize()
|
||||||
@@ -127,17 +143,23 @@ def main():
|
|||||||
print(" die Market-Order ist nur noch Fallback, und die Nachjagd-Bremse")
|
print(" die Market-Order ist nur noch Fallback, und die Nachjagd-Bremse")
|
||||||
print(" (squeeze_max_chase_atr) deckelt sie bei 0,20×ATR.")
|
print(" (squeeze_max_chase_atr) deckelt sie bei 0,20×ATR.")
|
||||||
print()
|
print()
|
||||||
print(" ⚠ Die eigentliche Erfolgsmeldung: Squeeze-Trades OHNE Log-Zeile —")
|
print(" ⚠ Die eigentliche Erfolgsmeldung: Bot-Trades OHNE Market-Log-Zeile —")
|
||||||
print(" das sind die per Pending-Order gefüllten.")
|
print(" das sind die per Pending-Order gefüllten (Squeeze UND Signal).")
|
||||||
n_sq = 0
|
sig_ts = {t.timestamp() for t, _ in sig_fallback}
|
||||||
|
n_bot = 0
|
||||||
for t in trades:
|
for t in trades:
|
||||||
if t["entry_time"] < _UMBAU.timestamp():
|
if t["entry_time"] < _UMBAU.timestamp():
|
||||||
continue
|
continue
|
||||||
n_sq += 1
|
n_bot += 1
|
||||||
if t["ticket"] not in getroffen:
|
# Market-Fallback erkannt, wenn eine der beiden Log-Zeilen zeitlich passt
|
||||||
|
market = (t["ticket"] in getroffen
|
||||||
|
or any(abs(t["entry_time"] - s) <= 90 for s in sig_ts))
|
||||||
|
if not market:
|
||||||
ohne_log.append(t)
|
ohne_log.append(t)
|
||||||
print(f" Trades seit dem Umbau: {n_sq} · davon ohne Market-Log-Zeile: "
|
print(f" Trades seit dem Umbau: {n_bot} · davon ohne Market-Log-Zeile: "
|
||||||
f"{len(ohne_log)}")
|
f"{len(ohne_log)}")
|
||||||
|
print(f" (Market-Fallback-Zeilen im Log: Squeeze {len(log_eintraege)} · "
|
||||||
|
f"Signal {len(sig_fallback)})")
|
||||||
for t in ohne_log[-10:]:
|
for t in ohne_log[-10:]:
|
||||||
print(f" {datetime.fromtimestamp(t['entry_time'], _BERLIN):%d.%m %H:%M} "
|
print(f" {datetime.fromtimestamp(t['entry_time'], _BERLIN):%d.%m %H:%M} "
|
||||||
f"{t['direction']:<5} @ {t['entry_price']:.3f} "
|
f"{t['direction']:<5} @ {t['entry_price']:.3f} "
|
||||||
|
|||||||
@@ -234,8 +234,14 @@ def main():
|
|||||||
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
|
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
|
||||||
|
|
||||||
w = WaveRecommender(lambda: None)
|
w = WaveRecommender(lambda: None)
|
||||||
w.set_reversal_enabled(False)
|
w.set_reversal_enabled(False) # Live-Stand seit 04.08.
|
||||||
w.set_dead_hours("")
|
# ⚠⚠ OHNE DIESE ZEILE IST DAS ENTRY-RAUM-GATE AUS: `_entry_room_atr`
|
||||||
|
# ist im Konstruktor 0.0, und `_room_gate` gibt dann `rec` unveraendert
|
||||||
|
# zurueck — der Aufruf sieht aus wie ein Gate, ist aber ein No-op.
|
||||||
|
# Genau das ist mir am 05.08. in drei Skripten passiert (live blockt das
|
||||||
|
# Gate 35 % aller Empfehlungen, im Backtest 0 %).
|
||||||
|
w.set_entry_room(0.6) # Live-Wert
|
||||||
|
w.set_dead_hours("") # live leer
|
||||||
mid = len(C) // 2
|
mid = len(C) // 2
|
||||||
|
|
||||||
print("=" * 106)
|
print("=" * 106)
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Signal-Verteilung mit dem VOLLEN Live-Gate-Stack (2026-08-05).
|
||||||
|
|
||||||
|
WARUM ES DAS BRAUCHT — der 43-%-Maßstab ist ungültig:
|
||||||
|
`backtest_dist.py` liefert „~57 % gerichtet / ~43 % WARTEN". Diese Zahl kommt aber
|
||||||
|
aus **`_build` ALLEIN** — ohne **Entry-Raum-Gate** und ohne **Breakout-Bestätigung**.
|
||||||
|
Genau die beiden erzeugen live **62 % aller Blocks** (seit 01.08., echte Marktzeit:
|
||||||
|
entry_room 35,3 % · breakout_pending 26,5 %). Der Vergleich „live 93 % WARTEN gegen
|
||||||
|
erwartete 43 %" stellt also zwei verschiedene Systeme gegenüber — und das
|
||||||
|
dokumentierte Erfolgskriterium des TF-Churn-Fixes ist damit per Konstruktion
|
||||||
|
unerreichbar.
|
||||||
|
|
||||||
|
Dieses Skript stellt den fehlenden Maßstab her: dieselbe Verteilung, aber über den
|
||||||
|
Stack, der live wirklich läuft (`_build` → `_confirm_breakout` → `_room_gate`).
|
||||||
|
|
||||||
|
⚠ ABGRENZUNG: gezählt wird **je BAR**, live je Snapshot (~1×/s). Für den ANTEIL ist
|
||||||
|
das vergleichbar; für absolute Häufigkeiten nicht.
|
||||||
|
⚠ NICHT modelliert (live-only, ohne Bar-Entsprechung): EIA-Fenster, News-Blackout,
|
||||||
|
Cooldowns, Startup-Schonfrist, `stale`. Deren Anteil ist live klein bzw. wird beim
|
||||||
|
Vergleich herausgerechnet.
|
||||||
|
|
||||||
|
Aufruf: python backtest_dist_gated.py [bars]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
from core.analysis import calc_trend_angle
|
||||||
|
from core.wave_rec import (WaveRecommender, _EMA_FAST, _EMA_SLOW, _N_BARS,
|
||||||
|
_ANGLE_LR, _ANGLE_DEAD, _HTF_DEADBAND)
|
||||||
|
|
||||||
|
_ATRMIN = 0.06
|
||||||
|
_TIMEOUT_BARS = 12
|
||||||
|
_PIV_K, _PIV_LOOK = 3, 300
|
||||||
|
_ATR_FLOOR_PB = 0.12
|
||||||
|
_K = 0.3
|
||||||
|
|
||||||
|
# Live-Referenz (seit 01.08., echte Marktzeit, `stale` herausgerechnet) — aus der
|
||||||
|
# `recommendations`-Tabelle; s. Roast-Analyse 2026-08-05.
|
||||||
|
_LIVE = {"(gerichtet)": 7.2, "entry_room": 35.3, "breakout_pending": 26.5,
|
||||||
|
"min_conf": 16.3, "htf_counter": 6.7, "stretch": 6.4, "deadband": 1.6}
|
||||||
|
|
||||||
|
|
||||||
|
def _ema_series(v, p):
|
||||||
|
k = 2 / (p + 1); out = [v[0]]
|
||||||
|
for x in v[1:]:
|
||||||
|
out.append(out[-1] + k * (x - out[-1]))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
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 _htf_sign(C, i, n, atr):
|
||||||
|
if i < n * _EMA_SLOW:
|
||||||
|
return 0
|
||||||
|
seg = C[i - n * _EMA_SLOW:i + 1:n]
|
||||||
|
if len(seg) < _EMA_SLOW + 2:
|
||||||
|
return 0
|
||||||
|
ef = _ema_series(seg, _EMA_FAST)[-1]; es = _ema_series(seg, _EMA_SLOW)[-1]
|
||||||
|
dead = _HTF_DEADBAND * atr * (n ** 0.5)
|
||||||
|
return 1 if ef - es > dead else (-1 if es - ef > dead else 0)
|
||||||
|
|
||||||
|
|
||||||
|
def verteilung(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, lo, hi, mit_gates):
|
||||||
|
"""→ Counter über die Block-Gründe. `mit_gates=False` = nur `_build` (die alte,
|
||||||
|
ungültige Referenz), True = voller Live-Stack."""
|
||||||
|
z = Counter()
|
||||||
|
pend = None
|
||||||
|
for i in range(max(lo, _PIV_LOOK + _EMA_SLOW + 5), hi):
|
||||||
|
atr = A[i]
|
||||||
|
if not atr or atr < _ATRMIN:
|
||||||
|
continue
|
||||||
|
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
|
||||||
|
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K],
|
||||||
|
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]}
|
||||||
|
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
|
||||||
|
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
|
||||||
|
sig = rec.get("signal")
|
||||||
|
if not mit_gates:
|
||||||
|
z["(gerichtet)" if sig in ("LONG", "SHORT")
|
||||||
|
else (rec.get("block") or "(unmarkiert)")] += 1
|
||||||
|
continue
|
||||||
|
# ── Breakout-Bestätigung (Mechanik 1:1 aus `_confirm_breakout`) ──────
|
||||||
|
if sig in ("LONG", "SHORT"):
|
||||||
|
d = 1 if sig == "LONG" else -1
|
||||||
|
if pend is None or pend["dir"] != d:
|
||||||
|
pend = {"dir": d, "level": C[i] + d * _K * atr,
|
||||||
|
"invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i}
|
||||||
|
if not pend["confirmed"]:
|
||||||
|
broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"])
|
||||||
|
against = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])
|
||||||
|
if broke:
|
||||||
|
pend["confirmed"] = True
|
||||||
|
else:
|
||||||
|
if against or (i - pend["bar"]) > _TIMEOUT_BARS:
|
||||||
|
pend = {"dir": d, "level": C[i] + d * _K * atr,
|
||||||
|
"invalid": C[i] - d * _K * atr,
|
||||||
|
"confirmed": False, "bar": i}
|
||||||
|
z["breakout_pending"] += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
pend = None
|
||||||
|
z[rec.get("block") or "(unmarkiert)"] += 1
|
||||||
|
continue
|
||||||
|
rec2 = w._room_gate(rec, C[i])
|
||||||
|
if rec2.get("signal") not in ("LONG", "SHORT"):
|
||||||
|
z["entry_room"] += 1
|
||||||
|
continue
|
||||||
|
z["(gerichtet)"] += 1
|
||||||
|
return z
|
||||||
|
|
||||||
|
|
||||||
|
def zeig(titel, z):
|
||||||
|
ges = sum(z.values())
|
||||||
|
print(f"\n {titel} (n={ges})")
|
||||||
|
print(f" {'Grund':<20}{'Anteil':>9}{'live':>9}{'Delta':>9}")
|
||||||
|
for g, n in z.most_common():
|
||||||
|
p = 100.0 * n / max(1, ges)
|
||||||
|
lv = _LIVE.get(g)
|
||||||
|
print(f" {g:<20}{p:>8.1f} %"
|
||||||
|
+ (f"{lv:>8.1f} %{p-lv:>+8.1f}" if lv is not None else f"{'—':>9}{'':>9}"))
|
||||||
|
return 100.0 * z["(gerichtet)"] / max(1, ges)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
n_bars = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
|
||||||
|
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_M5, 0, n_bars)
|
||||||
|
mt5.shutdown()
|
||||||
|
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
|
||||||
|
C = [float(b["close"]) for b in bars]
|
||||||
|
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
|
||||||
|
A = _atr_series(H, L, C)
|
||||||
|
AD = [0.0] * len(C)
|
||||||
|
for i in range(_N_BARS, len(C)):
|
||||||
|
AD[i] = calc_trend_angle(C[i - _N_BARS:i + 1], _ANGLE_LR) - 90.0
|
||||||
|
if abs(AD[i]) < _ANGLE_DEAD:
|
||||||
|
AD[i] = 0.0
|
||||||
|
HTF = [_htf_sign(C, i, 6, A[i] or 0.1) for i in range(len(C))]
|
||||||
|
H1S = [_htf_sign(C, i, 12, A[i] or 0.1) for i in range(len(C))]
|
||||||
|
PH = [j for j in range(_PIV_K, len(C) - _PIV_K)
|
||||||
|
if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])]
|
||||||
|
PL = [j for j in range(_PIV_K, len(C) - _PIV_K)
|
||||||
|
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
|
||||||
|
|
||||||
|
w = WaveRecommender(lambda: None)
|
||||||
|
w.set_reversal_enabled(False) # Live-Stand seit 04.08.
|
||||||
|
# ⚠⚠ OHNE DIESE ZEILE IST DAS ENTRY-RAUM-GATE AUS: `_entry_room_atr`
|
||||||
|
# ist im Konstruktor 0.0, und `_room_gate` gibt dann `rec` unveraendert
|
||||||
|
# zurueck — der Aufruf sieht aus wie ein Gate, ist aber ein No-op.
|
||||||
|
# Genau das ist mir am 05.08. in drei Skripten passiert (live blockt das
|
||||||
|
# Gate 35 % aller Empfehlungen, im Backtest 0 %).
|
||||||
|
w.set_entry_room(0.6) # Live-Wert
|
||||||
|
w.set_dead_hours("") # live leer
|
||||||
|
|
||||||
|
print("=" * 74)
|
||||||
|
print(f" SIGNAL-VERTEILUNG — {sym} M5, {len(C)} Bars")
|
||||||
|
print(" Der gueltige Massstab fuer den WARTEN-Anteil (s. Modulkopf)")
|
||||||
|
print("=" * 74)
|
||||||
|
g_ohne = zeig("A) NUR `_build` — die ALTE, ungueltige Referenz (~57 % erwartet)",
|
||||||
|
verteilung(w, H, L, C, A, EF, ES, AD, HTF, H1S, PH, PL,
|
||||||
|
0, len(C), False))
|
||||||
|
g_mit = zeig("B) VOLLER Live-Stack (+ Breakout-Bestaetigung + Entry-Raum)",
|
||||||
|
verteilung(w, H, L, C, A, EF, ES, AD, HTF, H1S, PH, PL,
|
||||||
|
0, len(C), True))
|
||||||
|
print("\n" + "=" * 74)
|
||||||
|
print(" ERGEBNIS")
|
||||||
|
print("=" * 74)
|
||||||
|
print(f" gerichtet nur `_build` {g_ohne:>6.1f} % (daher die 43-%-Erwartung)")
|
||||||
|
print(f" gerichtet voller Stack {g_mit:>6.1f} % <- der GUELTIGE Massstab")
|
||||||
|
print(f" gerichtet LIVE (seit 01.08.) {_LIVE['(gerichtet)']:>5.1f} %")
|
||||||
|
d = _LIVE["(gerichtet)"] - g_mit
|
||||||
|
print()
|
||||||
|
if abs(d) <= 3:
|
||||||
|
print(f" => Live liegt {d:+.1f} Pp am Massstab. Der WARTEN-Anteil ist NORMAL,")
|
||||||
|
print(" nicht defekt. Der 43-%-Alarm war ein Vergleichsfehler.")
|
||||||
|
elif d < 0:
|
||||||
|
print(f" => Live liegt {d:+.1f} Pp DARUNTER — es gibt also einen echten Rest,")
|
||||||
|
print(" den die Bar-Simulation nicht erklaert (Cooldowns, EIA, News, TF-Wechsel).")
|
||||||
|
else:
|
||||||
|
print(f" => Live liegt {d:+.1f} Pp DARUEBER — mehr Signale als der Stack hergibt.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -260,8 +260,14 @@ def main():
|
|||||||
vol_med = sorted(x for x in A if x)[len(A) // 2]
|
vol_med = sorted(x for x in A if x)[len(A) // 2]
|
||||||
|
|
||||||
w = WaveRecommender(lambda: None)
|
w = WaveRecommender(lambda: None)
|
||||||
w.set_reversal_enabled(False) # live-Stand seit 2026-08-04
|
w.set_reversal_enabled(False) # Live-Stand seit 04.08.
|
||||||
w.set_dead_hours("")
|
# ⚠⚠ OHNE DIESE ZEILE IST DAS ENTRY-RAUM-GATE AUS: `_entry_room_atr`
|
||||||
|
# ist im Konstruktor 0.0, und `_room_gate` gibt dann `rec` unveraendert
|
||||||
|
# zurueck — der Aufruf sieht aus wie ein Gate, ist aber ein No-op.
|
||||||
|
# Genau das ist mir am 05.08. in drei Skripten passiert (live blockt das
|
||||||
|
# Gate 35 % aller Empfehlungen, im Backtest 0 %).
|
||||||
|
w.set_entry_room(0.6) # Live-Wert
|
||||||
|
w.set_dead_hours("") # live leer
|
||||||
mid = len(C) // 2
|
mid = len(C) // 2
|
||||||
|
|
||||||
print("=" * 92)
|
print("=" * 92)
|
||||||
|
|||||||
@@ -160,9 +160,13 @@ CHECKS = [
|
|||||||
# Mitternacht: der Squeeze-Trade von 08:33 lief noch über die Market-Order und
|
# Mitternacht: der Squeeze-Trade von 08:33 lief noch über die Market-Order und
|
||||||
# würde die Zählung verfälschen. `strftime('%s', …)` liest UTC, `entry_time`
|
# würde die Zählung verfälschen. `strftime('%s', …)` liest UTC, `entry_time`
|
||||||
# ist lokale Epoch — deshalb 07:00 UTC als sicherer Schnitt.
|
# ist lokale Epoch — deshalb 07:00 UTC als sicherer Schnitt.
|
||||||
"have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'SQUEEZE%' "
|
# ⚠ Seit 05.08. laeuft AUCH der Signal-Pfad ueber Pending-Orders — beide
|
||||||
|
# zaehlen, sonst wartet der Reminder auf eine Population, die vielleicht
|
||||||
|
# nie kommt (der Squeeze feuert nur ~2-3x/Woche).
|
||||||
|
"have": lambda: _q("SELECT COUNT(*) FROM trades WHERE "
|
||||||
|
"(setup LIKE 'SQUEEZE%' OR setup LIKE 'AUTOSIG%') "
|
||||||
"AND entry_time >= strftime('%s','2026-08-05 07:00:00')"),
|
"AND entry_time >= strftime('%s','2026-08-05 07:00:00')"),
|
||||||
"need": 12, "unit": "Squeeze-Trades seit dem Umbau (05.08. 08:55)",
|
"need": 12, "unit": "Bot-Trades (Squeeze+Signal) seit dem Umbau (05.08. 08:55)",
|
||||||
"cmd": "python analyze_squeeze_entry_gap.py 60 → Der Median-Abstand muss "
|
"cmd": "python analyze_squeeze_entry_gap.py 60 → Der Median-Abstand muss "
|
||||||
"gegen 0 gehen und die Market-Log-Zeilen ('AUTO-SQUEEZE-ENTRY') "
|
"gegen 0 gehen und die Market-Log-Zeilen ('AUTO-SQUEEZE-ENTRY') "
|
||||||
"sollten weitgehend verschwinden. Bleibt der Abstand bei ~0,3×ATR, "
|
"sollten weitgehend verschwinden. Bleibt der Abstand bei ~0,3×ATR, "
|
||||||
|
|||||||
+19
@@ -888,6 +888,25 @@ function render(d) {
|
|||||||
if (t) { calEl.className = "news-conflict warn"; calEl.textContent = t; }
|
if (t) { calEl.className = "news-conflict warn"; calEl.textContent = t; }
|
||||||
else { calEl.className = "news-conflict hidden"; calEl.textContent = ""; }
|
else { calEl.className = "news-conflict hidden"; calEl.textContent = ""; }
|
||||||
}
|
}
|
||||||
|
// Auto-Flip-Close sichtbar machen (2026-08-05). Dieser Exit schliesst echte
|
||||||
|
// Positionen, ist aber in BEIDEN Halbjahren gemessen NEGATIV (−92/−160 R,
|
||||||
|
// backtest_flipclose2.py) und lief bis heute voellig ohne Anzeige. Die Zeile
|
||||||
|
// nennt bewusst die Messung mit — sonst sieht ein Zaehler nach Erfolg aus.
|
||||||
|
const flipEl = $("flip-note");
|
||||||
|
if (flipEl) {
|
||||||
|
if (d.flip_close) {
|
||||||
|
const n = d.flip_close_count || 0;
|
||||||
|
const r = d.flip_close_min_r;
|
||||||
|
flipEl.className = "news-conflict warn";
|
||||||
|
flipEl.textContent = `🔄 Auto-Flip-Close AKTIV (ab ${r != null ? r : "?"}×ATR im Plus)`
|
||||||
|
+ ` — bisher ${n}× ausgelöst · ⚠ gemessen in beiden Halbjahren negativ`;
|
||||||
|
flipEl.title = "Schliesst die Position, wenn die Empfehlung dreht. "
|
||||||
|
+ "backtest_flipclose2.py: −92 R (H1) / −160 R (H2) gegenueber Nichtstun. "
|
||||||
|
+ "Laeuft auf ausdruecklichen Wunsch; abschalten via [trading] auto_flip_close=false.";
|
||||||
|
} else {
|
||||||
|
flipEl.className = "news-conflict hidden"; flipEl.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
// Schalter-Zustand: Auto-Close am S/R an/aus + Mindestgewinn
|
// Schalter-Zustand: Auto-Close am S/R an/aus + Mindestgewinn
|
||||||
const srBtn = $("srclose-btn");
|
const srBtn = $("srclose-btn");
|
||||||
if (srBtn) {
|
if (srBtn) {
|
||||||
|
|||||||
+8
-2
@@ -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=138">
|
<link rel="stylesheet" href="style.css?v=139">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -102,6 +102,12 @@
|
|||||||
<div class="pos-stophint hidden" id="pos-stophint"></div>
|
<div class="pos-stophint hidden" id="pos-stophint"></div>
|
||||||
<div class="news-conflict hidden" id="news-conflict"></div>
|
<div class="news-conflict hidden" id="news-conflict"></div>
|
||||||
<div class="news-conflict hidden" id="cal-note"></div>
|
<div class="news-conflict hidden" id="cal-note"></div>
|
||||||
|
<!-- Auto-Flip-Close: schließt eine Position, wenn die Empfehlung dreht.
|
||||||
|
⚠ GEMESSEN in BEIDEN Halbjahren negativ (−92/−160 R,
|
||||||
|
backtest_flipclose2.py), läuft auf ausdrücklichen User-Wunsch. Hatte
|
||||||
|
bis 2026-08-05 KEINE Anzeige — ein Exit, der echtes Geld bewegt und den
|
||||||
|
niemand sieht. Jetzt sichtbar, damit die Entscheidung informiert ist. -->
|
||||||
|
<div class="news-conflict hidden" id="flip-note"></div>
|
||||||
<div class="reasons" id="wave-reasons"></div>
|
<div class="reasons" id="wave-reasons"></div>
|
||||||
<div class="kv"><span>Setup</span><b id="wave-setup">—</b></div>
|
<div class="kv"><span>Setup</span><b id="wave-setup">—</b></div>
|
||||||
<div class="kv"><span>Lauf</span><b id="wave-move">—</b></div>
|
<div class="kv"><span>Lauf</span><b id="wave-move">—</b></div>
|
||||||
@@ -281,6 +287,6 @@
|
|||||||
<div id="toast" class="toast hidden"></div>
|
<div id="toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script src="lightweight-charts.standalone.production.js?v=114"></script>
|
<script src="lightweight-charts.standalone.production.js?v=114"></script>
|
||||||
<script src="app.js?v=138"></script>
|
<script src="app.js?v=139"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user