Gesamtempfehlung: Selbst-Kalibrierung, voller Ausrichtungs-Split, Block-Gruende,

Elliott entmachtet (v=144)

Vier Verbesserungen, alle aus dem Gemessenen abgeleitet — kein neues Signal.
Ausgangspunkt: die Karte hat genau eine belegte Funktion, das Veto; als Prognose
ist sie ein Muenzwurf (48-51 % ueber 1.071 Episoden).

(1) SELBST-KALIBRIERUNG. Neue Tabelle rec_outcomes: eine Zeile je EPISODE
    (Richtungswechsel, nicht je Minute), Auswertung ~alle 5 min gegen candles_m1
    nach 30/60 min, Snapshot rec_track, Zeile #vd-selfcal. Gegen 50 % zu lesen.
    Warum der groesste Hebel: die Qualitaet war bis heute unsichtbar und musste
    auf Nachfrage rueckwirkend gemessen werden. Dasselbe Muster
    (pbreak_predictions) hat den Ausfall des P(break)-Modells LIVE aufgedeckt.
    Mit 4 Szenarien getestet (tests_rec_outcomes.py). Zwei Fehlschlaege dabei
    waren Testfehler, keine Code-Fehler — dokumentiert.

(2) alignment_stats auf die VOLLE Historie (1.216 statt 40 Trades) und
    zusaetzlich je Lot. Absolute Euro-Vergleiche sind bei wechselnder
    Positionsgroesse ungueltig (Lehre 04.08.). Damit steht die einzige
    beidhaelftig robuste Aussage der Karte auf ihrer vollen Stichprobe.

(3) Block-Gruende sichtbar (block_mix -> #vd-blocks): "heute X % ohne Block ·
    entry_room … · min_conf …". Bisher nur per DB-Abfrage zu beantworten.

(4) ELLIOTT: Verdict-Gewicht 1,0 -> 0. Gemessen ueber 15.544 Zeilen hatte es mit
    1.384 Episoden die groesste Stichprobe der Tabelle und ist darin flach
    (-0,075/+0,032, 50 % Treffer) — bei 25,9 % Einfluss auf die Nadel, weil es
    in nur 1 % der Zeilen schweigt. Chip bleibt, Stimme entfaellt.
    ⚠ Folge: die Nadel schlaegt staerker aus (real +0,33 -> +1,00).

⚠ Namenskollision beim Bau gefunden und behoben: die neue Klasse hiess zuerst
.vd-track — so heisst bereits die Schiene der Bias-Nadel; sie waere
ueberschrieben worden. Jetzt .vd-selfcal.

Live verifiziert: alignment je Lot, block_mix (93 von 1381 ohne Block),
rec_outcomes angelegt, Elliott weight=0 bei erhaltenem Chip, v=144 ausgeliefert,
eine Instanz auf Port 8000.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-06 23:21:52 +02:00
co-authored by Claude Opus 5
parent 4f85d2ce75
commit 874ee16e00
9 changed files with 705 additions and 48 deletions
+54 -1
View File
@@ -3608,7 +3608,60 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
Anzeige zu einer Erwartung ein, die die Messung nicht deckt. Anzeige zu einer Erwartung ein, die die Messung nicht deckt.
⚠ Bewusst **statisches Markup, kein JS** — es ist ein Dauerhinweis, kein Zustand; ⚠ Bewusst **statisches Markup, kein JS** — es ist ein Dauerhinweis, kein Zustand;
damit entfällt auch jedes Null-Risiko beim Render (s. Cache-Regel oben). 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). - **⚠⚠ VIER VERBESSERUNGEN AN DER GESAMTEMPFEHLUNG (User-Vorgabe 2026-08-06,
v=144) — alle aus dem Gemessenen abgeleitet, KEIN neues Signal.** Ausgangspunkt:
die Karte hat genau eine belegte Funktion, das **Veto** (s. Ausrichtungs-Split);
als Prognose ist sie ein Münzwurf. Verbessert wird also das Veto und die
Sichtbarkeit — nicht die Vorhersage.
**(1) SELBST-KALIBRIERUNG — die Karte zeigt ihre eigene Trefferquote.**
Neue Tabelle **`rec_outcomes`** (DB-Backup `…bak-2026-08-06-recoutcomes`):
`history.log_rec_outcome` schreibt **eine Zeile je EPISODE** (Richtungswechsel,
nicht je Minute — sonst zählt man dieselbe Marktlage dutzendfach);
`engine._evaluate_rec_outcomes` trägt ~alle 5 min im Trend-Loop den
Forward-Return nach 30/60 min aus `candles_m1` nach (⚠ Broker-Offset, wie bei
`_evaluate_pbreak_predictions`). `history.rec_accuracy(200)` → Snapshot
**`rec_track`** → Zeile **`#vd-selfcal`**.
**Warum das der grösste Hebel ist:** die Qualität der Empfehlung war bis heute
unsichtbar — sie musste auf Nachfrage rückwirkend gemessen werden. Dasselbe
Muster (`pbreak_predictions`) hat den Ausfall des P(break)-Modells LIVE
aufgedeckt, was kein Backtest konnte. Die Zeile ist **gegen 50 % zu lesen**,
nicht gegen 0. Fehlen Bars (Wochenende), bleibt die Zeile offen statt falsch
bewertet; nach 6 h wird sie geschlossen. **Mit 4 Szenarien getestet**
(`tests_rec_outcomes.py`): Episoden-Dedup · WARTEN re-armt · Auswertung inkl.
Offset in beide Richtungen · fehlende Bar → bleibt offen · Aggregation.
⚠ Zwei Fehler dabei waren MEINE Testfehler, nicht Code-Fehler: die Test-Kerzen
aus dem Vorabschnitt lagen im Fenster des nächsten, und die DB-Kopie enthält
**echte** `candles_m1` — der Fall „keine Bar" entsteht erst, wenn das Fenster
wirklich geleert wird.
**(2) AUSRICHTUNGS-SPLIT auf die volle Historie + je Lot.** `alignment_stats`
lief auf den **letzten 40** Trades und zeigte absolute Euro. Jetzt über **alle
1.216** (Default `last_n=0`) und zusätzlich **`pnl_lot`** — absolute
Euro-Vergleiche sind bei wechselnder Positionsgrösse ungültig (Lehre 04.08.).
Der Order-Dialog zeigt beides. Damit steht die einzige beidhälftig robuste
Aussage der Karte auf ihrer vollen Stichprobe: **gegen das Signal 6,60 €/Lot**.
**(3) BLOCK-GRUND SICHTBAR** (`engine._block_mix_cached` → Snapshot
`block_mix``#vd-blocks`): „heute X % ohne Block · entry_room … · min_conf …".
Auf die Frage „warum gab es so wenige Empfehlungen?" war bisher eine DB-Abfrage
nötig. ~120 s gecacht, reine Anzeige.
**(4) ELLIOTT ENTMACHTET — Verdict-Gewicht 1,0 → 0** (gemessen
`analyze_verdict_modules.py`, 15.544 Zeilen). Elliott hatte mit **1.384
Episoden die grösste Stichprobe der ganzen Tabelle** und ist darin **flach**:
Überschuss 0,075/+0,032 ×ATR, **50 % Treffer**. Gleichzeitig war es mit
**25,9 % die zweitlauteste Stimme** — weil es in nur 1 % der Zeilen schweigt.
Mit H1 (38,1 %) bildete es einen Dauerhintergrund von ~64 %. **Der Chip bleibt**
(das Ziel ist als Kontext nützlich), nur die Stimme entfällt.
**Sichtbare Folge: die Nadel schlägt stärker aus.** Real beim Umbau
+0,33 → **+1,00**, weil nur noch H1 (1,5) und Liq-Trend (0,5) sprachen. Mit
wenigen sprechenden Modulen kann ein einzelnes die Nadel auf ±1 ziehen — das ist
ehrlicher als eine künstlich gedämpfte Nadel, sieht aber dramatischer aus.
**Reine Anzeige-Änderung** — die Order steuert die Welle über die Headline,
nicht der Konsens. Zurück: `ew = 1.0 if ev != 0 else 0.0` in `_verdict`.
**Nicht angefasst, aber auffällig** (für die 25.000-Verdict-Messung):
**Orderbuch** ist über 2.688 Episoden beidhälftig NEGATIV (0,054/0,066,
49 % Treffer) bei Gewicht 0,5 — der einzige konsistent falsche Beitrag.
**Muster** (+1,27/+0,67, 58 %, n=88) und **Liq-Trend** (+0,20/+0,32, n=132)
sehen gut aus, sind aber zu dünn. Alle übrigen kippen zwischen den Hälften.
- Asset-Version aktuell **v=144** (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`
+19 -3
View File
@@ -96,8 +96,18 @@ def _htf_sign(C, i, n, atr):
def lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi, def lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi,
min_conf, nacht, am_level, slip=0.0): min_conf, nacht, am_level, slip=0.0, fill="close"):
"""Ein Durchlauf: sequentiell, EIN Positions-Slot, Live-Dedup.""" """Ein Durchlauf: sequentiell, EIN Positions-Slot, Live-Dedup.
`fill` (2026-08-06 ergaenzt, Default = bisheriges Verhalten, bitgenau):
"close" — die Bestaetigung gilt, wenn der Bar-CLOSE das Level bricht.
"touch" — die ruhende Stop-Order fuellt bei der ERSTEN BERUEHRUNG, auch
wenn der Bar wieder zurueckfaellt. Das ist, was live passiert.
⚠ Der Unterschied ist kein Detail: beim Momentum betrug er 0,45 R und drehte
+0,40 auf 0,06 (`backtest_momentum_v2.py`), beim Squeeze 0,32 R
(`backtest_squeeze_touchfill.py`). "close" konditioniert auf den Bar-Schluss,
den eine liegende Order nicht kennen kann.
"""
def kosten(i, atr): def kosten(i, atr):
return (SP[i] if SP[i] > 0 else 0.0225) / atr return (SP[i] if SP[i] > 0 else 0.0225) / atr
@@ -135,7 +145,13 @@ def lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi,
pend = {"dir": d, "level": C[i] + d * _K * atr, pend = {"dir": d, "level": C[i] + d * _K * atr,
"invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i} "invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i}
if not pend["confirmed"]: if not pend["confirmed"]:
broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"]) if fill == "touch":
# Beruehrung genuegt — die Order liegt dort und wird ausgeloest.
broke = ((H[i] >= pend["level"]) if d > 0
else (L[i] <= pend["level"]))
else:
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"]) against = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])
if broke: if broke:
pend["confirmed"] = True pend["confirmed"] = True
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Haelt der SIG-Pending-Pfad auch mit realistischem Fill? — die offene Zahl.
ANLASS (2026-08-06): `signal_pending_entry` laeuft seit dem 05.08. mit ECHTEM
Geld. Die Erwartung dahinter (ØR +0,39/+0,36) stammt aus
`backtest_auto_signal_v3.py` und unterstellt: Einstieg zum Level-Preis, aber
**nur dort, wo der Bar-CLOSE das Level gebrochen hat**. Eine liegende Stop-Order
weiss das nicht — sie fuellt bei der ERSTEN BERUEHRUNG, auch bei jeder, die
wieder zurueckfaellt.
Wie teuer dieser Unterschied ist, wurde heute zweimal gemessen:
Momentum +0,40 → 0,06 (0,45 R Aufschlag) — Level WANDERT
Squeeze +0,46 → +0,13 (0,32 R Aufschlag) — Level STEHT
Der Squeeze haelt, das Momentum nicht. **Und `_pend` gehoert zur wandernden
Sorte:** der Anker wird bei jeder k×ATR-Gegenbewegung, nach Timeout und bei
Richtungswechsel neu gesetzt. Es ist also mit dem schlechteren Fall zu rechnen.
Benutzt dieselbe `lauf()` wie v3, nur mit `fill="touch"` — nichts nachgebaut,
damit die Zahlen vergleichbar bleiben. Der Default `fill="close"` reproduziert
v3 bitgenau (verifiziert).
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
`signal_pending_entry` bleibt nur gerechtfertigt, wenn der Pfad mit
`fill="touch"` in BEIDEN Haelften ØR > 0 UND PF > 1 haelt UND besser ist als
der Markt-Einstieg. Sonst gehoert er abgeschaltet.
Aufruf: python backtest_signal_touchfill.py [bars]
"""
import sys
import MetaTrader5 as mt5
import backtest_auto_signal_v3 as V
from core.wave_rec import WaveRecommender, calc_trend_angle
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)
point = mt5.symbol_info(sym).point
mt5.shutdown()
# ⚠ Aufbau 1:1 wie in v3.main() — aber ueber die IMPORTIERTEN Helfer, nicht
# ueber Kopien. Waere hier ein eigener EMA/ATR drin, waere der Vergleich
# zwischen den beiden Fill-Annahmen nicht mehr sauber.
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
C = [float(b["close"]) for b in bars]; T = [int(b["time"]) for b in bars]
SP = [float(b["spread"]) * point for b in bars]
EF = V._ema_series(C, V._EMA_FAST); ES = V._ema_series(C, V._EMA_SLOW)
A = V._atr_series(H, L, C)
AD = [0.0] * len(C)
for i in range(V._N_BARS, len(C)):
AD[i] = calc_trend_angle(C[i - V._N_BARS:i + 1], V._ANGLE_LR) - 90.0
if abs(AD[i]) < V._ANGLE_DEAD:
AD[i] = 0.0
HTF = [V._htf_sign(C, i, 6, A[i] or 0.1) for i in range(len(C))]
H1S = [V._htf_sign(C, i, 12, A[i] or 0.1) for i in range(len(C))]
PH = [j for j in range(V._PIV_K, len(C) - V._PIV_K)
if H[j] == max(H[j - V._PIV_K:j + V._PIV_K + 1])]
PL = [j for j in range(V._PIV_K, len(C) - V._PIV_K)
if L[j] == min(L[j - V._PIV_K:j + V._PIV_K + 1])]
w = WaveRecommender(lambda: None)
w.set_reversal_enabled(False)
w.set_entry_room(0.6) # ⚠ ohne das ist das Raum-Gate ein No-op
w.set_dead_hours("")
mid = len(C) // 2
print("=" * 104)
print(f" SIG-PENDING — Fill bei BAR-CLOSE gegen Fill bei BERUEHRUNG "
f"({sym}, {len(C)} M5-Bars)")
print("=" * 104)
print(f" {V._berlin(T[0]):%d.%m.%Y} bis {V._berlin(T[-1]):%d.%m.%Y} · "
f"voller Live-Gate-Stack · kanonischer Exit · Echtkosten · EIN Slot")
print()
def paar(mc, am_level, fill):
r1, _ = V.lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1S, PH, PL,
0, mid, mc, V._NACHT_LIVE, am_level, 0.0, fill)
r2, _ = V.lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1S, PH, PL,
mid, len(C), mc, V._NACHT_LIVE, am_level, 0.0, fill)
return V.kenn(r1), V.kenn(r2)
print(f" {'Variante':<38}{'H1 n WR OeR PF SigmaR':>33}"
f" |{'H2 n WR OeR PF SigmaR':>33}")
print(" " + "-" * 102)
print(" A) MARKT am Bar-Close (der Ist-Zustand vor dem 05.08.)")
for mc in (55, 65, 75):
V.zeile(f" Konf>={mc}", *paar(mc, False, "close"))
print(" " + "-" * 102)
print(" B) am Level, Fill nur bei CLOSE-Bruch (die Zahl, auf der live steht)")
for mc in (55, 65, 75):
V.zeile(f" Konf>={mc}", *paar(mc, True, "close"))
print(" " + "-" * 102)
print(" C) am Level, Fill bei BERUEHRUNG (was live wirklich passiert)")
haelt = []
for mc in (55, 65, 75):
k1, k2 = paar(mc, True, "touch")
if V.zeile(f" Konf>={mc}", k1, k2):
haelt.append(mc)
print()
print("=" * 104)
if haelt:
print(f" → Variante C haelt bei Konf {haelt} — `signal_pending_entry` ist gedeckt.")
else:
print(" → Variante C haelt in KEINER Konfidenz-Stufe.")
print(" Damit steht `signal_pending_entry=true` auf einer Zahl, die die")
print(" realistische Fill-Annahme nicht ueberlebt → abschalten.")
print(" ⚠ B gegen C gelesen = der Look-ahead-Aufschlag (Squeeze 0,32 R,")
print(" Momentum 0,45 R).")
return 0
if __name__ == "__main__":
sys.exit(main())
+161 -10
View File
@@ -690,6 +690,8 @@ class TradingEngine:
except Exception as ce: log.warning(f"candles.log: {ce}") except Exception as ce: log.warning(f"candles.log: {ce}")
try: self._evaluate_pbreak_predictions() # ~alle 5 min, self-throttled try: self._evaluate_pbreak_predictions() # ~alle 5 min, self-throttled
except Exception as pe: log.warning(f"pbreak_predictions: {pe}") except Exception as pe: log.warning(f"pbreak_predictions: {pe}")
try: self._evaluate_rec_outcomes() # ~alle 5 min, self-throttled
except Exception as re_: log.warning(f"rec_outcomes: {re_}")
# Wirtschaftskalender (self-throttled ~1 h, außerhalb des mt5_lock # Wirtschaftskalender (self-throttled ~1 h, außerhalb des mt5_lock
# unkritisch — reiner HTTP-Fetch mit Timeout) # unkritisch — reiner HTTP-Fetch mit Timeout)
try: self.calendar.maybe_refresh() try: self.calendar.maybe_refresh()
@@ -1675,6 +1677,30 @@ class TradingEngine:
except Exception as e: except Exception as e:
log.warning(f"log_recommendation: {e}") log.warning(f"log_recommendation: {e}")
# ── Selbst-Kalibrierung: eine Zeile je EPISODE (2026-08-06) ──────────
# WARUM: die Qualitaet der Empfehlung war bis heute unsichtbar — sie musste
# auf Nachfrage rueckwirkend gemessen werden (`analyze_signal_live.py`:
# 4851 % Treffer ueber 1.071 Episoden). Dasselbe Muster wie
# `pbreak_predictions` hat den Ausfall des P(break)-Modells LIVE aufgedeckt,
# was kein Backtest konnte. Reine Messung, kein Eingriff in die Logik.
# ⚠ Nur bei RICHTUNGSWECHSEL, nicht je Minute — sonst zaehlt man dieselbe
# Marktlage dutzendfach (der Fehler, den die Episoden-Bildung behebt).
try:
_sig = rec.get("signal")
if _sig in ("LONG", "SHORT") and _sig != getattr(self, "_rec_epi_sig", None):
_atr = float((s.get("pb_feats") or {}).get("atr") or 0.0)
_px = float((self.data.snapshot() or {}).get("bid") or 0.0)
if _atr > 0 and _px > 0 and self.history:
self.history.log_rec_outcome(
signal=_sig, conf_pct=int(rec.get("conf_pct") or 0),
tf=str(rec.get("tf") or self.wave.timeframe_name or ""),
price=_px, atr=_atr)
if _sig in ("LONG", "SHORT", "WARTEN"):
# WARTEN re-armt: die naechste Richtung ist eine NEUE Episode.
self._rec_epi_sig = _sig if _sig != "WARTEN" else None
except Exception as e:
log.debug(f"log_rec_outcome: {e}")
# Verdict-Stimmen-Logging (2026-07-24, Idee 2b): Modul-Votes 1×/min in die # Verdict-Stimmen-Logging (2026-07-24, Idee 2b): Modul-Votes 1×/min in die
# DB (`verdict_votes`), damit die UNBELEGTEN Stimmen (KI-Copilot, Elliott) # DB (`verdict_votes`), damit die UNBELEGTEN Stimmen (KI-Copilot, Elliott)
# nach ein paar Wochen datenbasiert bewertet werden können (behalten / # nach ein paar Wochen datenbasiert bewertet werden können (behalten /
@@ -2856,11 +2882,20 @@ class TradingEngine:
if tgt is not None and bid: if tgt is not None and bid:
ev = -1 if tgt < bid else 1 if tgt > bid else 0 ev = -1 if tgt < bid else 1 if tgt > bid else 0
edet = f"{(elliott or {}).get('wave', '')}{tgt:.3f}" edet = f"{(elliott or {}).get('wave', '')}{tgt:.3f}"
# ⚠ Gewicht nur bei GERICHTETEM Ziel (2026-07-31, Modul-Audit): liegt das # ⚠ GEWICHT AUF 0 GESETZT (2026-08-06, gemessen
# Ziel exakt auf dem Kurs, ist ev=0 — das ist keine Aussage und darf die # `analyze_verdict_modules.py` über 15.544 Zeilen). Elliott hatte mit
# Nadel nicht dämpfen. Betraf 1,3 % der Zeilen (klein, aber dieselbe Regel # **1.384 Episoden die grösste Stichprobe** der ganzen Tabelle und ist
# wie bei Welle/Squeeze/M30/H1/KI — Konsistenz statt Sonderfall). # darin **flach**: Überschuss 0,075 / +0,032 ×ATR, **50 % Treffer**.
ew = 1.0 if ev != 0 else 0.0 # Gleichzeitig war es mit **25,9 % die zweitlauteste Stimme** der
# Bias-Nadel — weil es praktisch nie schweigt (stumm in 1 % der Zeilen).
# Zusammen mit H1 (38,1 %) bildete es einen Dauerhintergrund von ~64 %,
# gegen den die episodischen Module erst anschieben mussten.
# Ein Modul mit der grössten Stichprobe und ohne messbaren Inhalt darf
# den Konsens nicht dominieren. Der CHIP bleibt (reine Anzeige, das Ziel
# ist als Kontext nützlich) — nur die Stimme entfällt.
# ⚠ Reine ANZEIGE-Änderung: die Order steuert die Welle über die
# Headline, nicht der Konsens. Zurück: `ew = 1.0 if ev != 0 else 0.0`.
ew = 0.0
add("Elliott", ev, ew, edet) add("Elliott", ev, ew, edet)
# 7) Squeeze-Breakout — Event-Signal: stimmt NUR bei `active` mit (dann 2,0, # 7) Squeeze-Breakout — Event-Signal: stimmt NUR bei `active` mit (dann 2,0,
@@ -3037,20 +3072,72 @@ class TradingEngine:
return getattr(self, "_manual_cache", None) return getattr(self, "_manual_cache", None)
def _alignment_cached(self) -> dict | None: def _alignment_cached(self) -> dict | None:
"""Ausrichtungs-Split (mit/gegen/ohne Empfehlung, letzte 40 Trades) für den """Ausrichtungs-Split (mit/gegen/ohne Empfehlung) für den Order-Dialog —
Order-Bestätigungsdialog — ehrliche eigene Zahlen statt generischem Hinweis ehrliche eigene Zahlen statt generischem Hinweis (2026-07-24; Gegen-Signal-
(2026-07-24, Verhaltens-Hebel: Gegen-Signal-Trades sind die gemessene Trades sind die gemessene Kern-Leckage).
Kern-Leckage). DB-Query gecacht (~120 s), Snapshot bleibt billig.""" ⚠ Seit 2026-08-06 über die **volle Historie** (1.216 Trades) statt der
letzten 40 — der Befund ist dort beidhälftig robust, über 40 war er eine
Momentaufnahme. Zusätzlich je Lot normiert (`pnl_lot`), weil absolute
Euro-Vergleiche bei wechselnder Positionsgröße ungültig sind.
DB-Query gecacht (~120 s), Snapshot bleibt billig."""
now = time.time() now = time.time()
if now - getattr(self, "_align_ts", 0.0) > 120: if now - getattr(self, "_align_ts", 0.0) > 120:
try: try:
self._align_cache = self.history.alignment_stats(40) self._align_cache = self.history.alignment_stats(0)
except Exception as e: except Exception as e:
log.debug(f"alignment_stats: {e}") log.debug(f"alignment_stats: {e}")
self._align_cache = None self._align_cache = None
self._align_ts = now self._align_ts = now
return getattr(self, "_align_cache", None) return getattr(self, "_align_cache", None)
def _block_mix_cached(self) -> dict | None:
"""WELCHE Gates blocken heute, und wie oft? (2026-08-06)
WARUM SICHTBAR: auf die Frage „warum gab es heute so wenige Empfehlungen?"
liess sich das nur per DB-Abfrage beantworten. Real am 06.08.: von 646
Zeilen waren **6 ungeblockt** — entry_room 274 · min_conf 225 ·
breakout_pending 88 · htf_counter 50. Die Karte sagt das jetzt selbst.
Reine Anzeige, ~120 s gecacht."""
now = time.time()
if now - getattr(self, "_blockmix_ts", 0.0) > 120:
try:
import sqlite3
con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0)
rows = con.execute(
"SELECT COALESCE(block_reason,'') b, COUNT(*) n FROM recommendations "
"WHERE date(timestamp,'unixepoch','localtime') = date('now','localtime') "
"GROUP BY b ORDER BY n DESC").fetchall()
con.close()
ges = sum(r[1] for r in rows)
self._blockmix_cache = {
"total": ges,
"frei": next((r[1] for r in rows if not r[0]), 0),
"top": [{"grund": r[0], "n": r[1]} for r in rows if r[0]][:4],
} if ges else None
except Exception as e:
log.debug(f"block_mix: {e}")
self._blockmix_cache = None
self._blockmix_ts = now
return getattr(self, "_blockmix_cache", None)
def _rec_track_cached(self) -> dict | None:
"""Selbst-Kalibrierung: Trefferquote der letzten Empfehlungs-Episoden.
⚠ Gegen 50 % zu lesen, nicht gegen 0 — es ist eine Richtungsaussage.
Gemessener Ausgangsstand (`analyze_signal_live.py`, 1.071 Episoden):
4851 %. Diese Zeile macht laufend sichtbar, ob sich das aendert, statt
dass es erst auf Nachfrage rueckwirkend gemessen wird. ~120 s gecacht."""
now = time.time()
if now - getattr(self, "_rectrack_ts", 0.0) > 120:
try:
self._rectrack_cache = (self.history.rec_accuracy(200)
if self.history else None)
except Exception as e:
log.debug(f"rec_accuracy: {e}")
self._rectrack_cache = None
self._rectrack_ts = now
return getattr(self, "_rectrack_cache", None)
def _loss_streak_today(self) -> tuple[int, int]: def _loss_streak_today(self) -> tuple[int, int]:
"""(Verlust-Trades heute, Trades heute) — für den Mental-Check der Entry- """(Verlust-Trades heute, Trades heute) — für den Mental-Check der Entry-
Checkliste („nach 2 Losses Session beenden"). DB-Query ~120 s gecacht.""" Checkliste („nach 2 Losses Session beenden"). DB-Query ~120 s gecacht."""
@@ -3276,6 +3363,68 @@ class TradingEngine:
except Exception as e: except Exception as e:
log.debug(f"pbreak_predictions auswerten: {e}") log.debug(f"pbreak_predictions auswerten: {e}")
def _evaluate_rec_outcomes(self):
"""Traegt fuer offene Empfehlungs-Episoden den Forward-Return nach.
⚠ `candles_m1.time` ist ROHE Broker-Zeit (UTC+3), `rec_outcomes.ts` ist
lokale Epoch — der Offset muss drauf (dieselbe Falle wie bei
`_evaluate_pbreak_predictions`). Genommen wird der letzte M1-Close bis
zum Zielzeitpunkt; fehlen Bars (Wochenende, Broker-Ausfall), bleibt die
Zeile offen statt falsch bewertet zu werden.
`hit` = 1, wenn der Kurs in Signalrichtung lief. Die Quote ist gegen 50 %
zu lesen, nicht gegen 0."""
now = time.time()
if now - getattr(self, "_rec_eval_ts", 0.0) < 300:
return
self._rec_eval_ts = now
try:
import sqlite3
off = self.trader._broker_offset_s()
con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0)
con.row_factory = sqlite3.Row
offen = con.execute(
"SELECT id, ts, signal, price, atr, ret30, ret60 FROM rec_outcomes "
"WHERE done IS NULL OR done = 0").fetchall()
for r in offen:
d = 1 if r["signal"] == "LONG" else -1
if not r["atr"] or r["atr"] <= 0:
con.execute("UPDATE rec_outcomes SET done=1 WHERE id=?", (r["id"],))
continue
werte = {}
for feld, minuten in (("ret30", 30), ("ret60", 60)):
if r[feld] is not None:
werte[feld] = r[feld]
continue
ziel = r["ts"] + minuten * 60
if now < ziel:
continue # noch nicht faellig
row = con.execute(
"SELECT c FROM candles_m1 WHERE time <= ? AND time > ? "
"ORDER BY time DESC LIMIT 1",
(ziel + off, ziel + off - 900)).fetchone()
if row:
werte[feld] = (row["c"] - r["price"]) * d / r["atr"]
sets, args = [], []
for feld in ("ret30", "ret60"):
if feld in werte and r[feld] is None:
sets.append(f"{feld}=?"); args.append(round(werte[feld], 4))
sets.append(f"hit{feld[3:]}=?"); args.append(1 if werte[feld] > 0 else 0)
faellig60 = now >= r["ts"] + 60 * 60
if faellig60 and ("ret60" in werte or r["ret60"] is not None):
sets.append("done=?"); args.append(1)
elif now >= r["ts"] + 6 * 3600:
# Nach 6 h keine Bars mehr zu erwarten (Wochenende/Ausfall) →
# schliessen, damit die Zeile die Auswertung nicht ewig belegt.
sets.append("done=?"); args.append(1)
if sets:
args.append(r["id"])
con.execute(f"UPDATE rec_outcomes SET {', '.join(sets)} WHERE id=?",
args)
con.commit(); con.close()
except Exception as e:
log.debug(f"rec_outcomes auswerten: {e}")
def squeeze_monitor(self) -> dict: def squeeze_monitor(self) -> dict:
"""Auto-Squeeze-Monitor (B4) für das Dashboard: (1) LIVE-Trades (echt, DB — """Auto-Squeeze-Monitor (B4) für das Dashboard: (1) LIVE-Trades (echt, DB —
das Gesamtsystem inkl. realer Exits) und (2) die MECHANISCHE Neuberechnung das Gesamtsystem inkl. realer Exits) und (2) die MECHANISCHE Neuberechnung
@@ -3576,6 +3725,8 @@ class TradingEngine:
"news_sentiment": (lambda sc: round(float(sc), 2) if sc is not None else None)( "news_sentiment": (lambda sc: round(float(sc), 2) if sc is not None else None)(
(self.news.sentiment or {}).get("score")), (self.news.sentiment or {}).get("score")),
"alignment": self._alignment_cached(), "alignment": self._alignment_cached(),
"block_mix": self._block_mix_cached(),
"rec_track": self._rec_track_cached(),
# Kennzahlen der MANUELLEN Trades für die gleichnamige Karte # Kennzahlen der MANUELLEN Trades für die gleichnamige Karte
# (2026-08-04). Rein deskriptiv; ~120 s gecacht, damit der heiße # (2026-08-04). Rein deskriptiv; ~120 s gecacht, damit der heiße
# Snapshot-Pfad nicht je Abruf die DB aggregiert. # Snapshot-Pfad nicht je Abruf die DB aggregiert.
+112 -28
View File
@@ -145,6 +145,22 @@ SCHEMA = [
) )
""", """,
""" """
CREATE TABLE IF NOT EXISTS rec_outcomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER, -- lokale Epoch, Beginn der EPISODE
signal TEXT, -- 'LONG' | 'SHORT'
conf_pct INTEGER,
tf TEXT,
price REAL, -- Kurs bei Ausgabe der Empfehlung
atr REAL, -- ATR_M5 zur Normierung
ret30 REAL, -- (Kurs[+30min] - price) x Richtung / atr
ret60 REAL,
hit30 INTEGER, -- 1 = Kurs lief in Signalrichtung
hit60 INTEGER,
done INTEGER -- 1 = beide Horizonte ausgewertet
)
""",
"""
CREATE TABLE IF NOT EXISTS verdict_votes ( CREATE TABLE IF NOT EXISTS verdict_votes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER, -- lokale Epoch ts INTEGER, -- lokale Epoch
@@ -420,6 +436,52 @@ class HistoryLogger:
conn.commit() conn.commit()
return cur.lastrowid return cur.lastrowid
def log_rec_outcome(self, *, signal: str, conf_pct: int, tf: str,
price: float, atr: float) -> int:
"""Loggt EINE Empfehlungs-EPISODE zur spaeteren Selbst-Kalibrierung.
⚠ WARUM EPISODEN UND NICHT ZEILEN: die Empfehlung wird ~1x/Minute
geloggt; eine Marktlage erzeugt dutzende identische Zeilen. Wer die zaehlt,
haelt 20.000 Faelle fuer eine Stichprobe, obwohl es 1.000 sind
(`analyze_signal_live.py`). Hier wird genau EINE Zeile je Richtungswechsel
geschrieben.
Ausgewertet wird separat (`engine._evaluate_rec_outcomes`) gegen
`candles_m1` — dasselbe Muster wie `pbreak_predictions`, das den Ausfall
des P(break)-Modells live aufgedeckt hat. Rueckgabe = Zeilen-ID."""
with self._lock, self._connect() as conn:
cur = conn.execute("""
INSERT INTO rec_outcomes (ts, signal, conf_pct, tf, price, atr)
VALUES (?,?,?,?,?,?)
""", (int(time.time()), signal, int(conf_pct or 0), tf or "",
float(price or 0.0), float(atr or 0.0)))
conn.commit()
return cur.lastrowid
def rec_accuracy(self, last_n: int = 200) -> dict:
"""Trefferquote der letzten `last_n` ausgewerteten Empfehlungs-Episoden.
⚠ Die Trefferquote ist gegen 50 % zu lesen, nicht gegen 0 — und der
Ø-Return gegen die Drift des Zeitraums. Gemessen ueber 1.071 Episoden lag
die Richtungstreffer-Quote bei 4851 % (`analyze_signal_live.py`); diese
Zeile macht laufend sichtbar, ob sich daran etwas aendert."""
with self._connect() as conn:
rows = conn.execute("""
SELECT signal, ret30, ret60, hit30, hit60 FROM rec_outcomes
WHERE done = 1 ORDER BY ts DESC LIMIT ?
""", (int(last_n),)).fetchall()
if not rows:
return {"n": 0}
def q(feld, hit):
v = [r[feld] for r in rows if r[feld] is not None]
h = [r[hit] for r in rows if r[hit] is not None]
return ((round(100.0 * sum(h) / len(h), 1) if h else None),
(round(sum(v) / len(v), 3) if v else None), len(h))
wr30, avg30, n30 = q("ret30", "hit30")
wr60, avg60, n60 = q("ret60", "hit60")
return {"n": len(rows), "wr30": wr30, "avg30": avg30, "n30": n30,
"wr60": wr60, "avg60": avg60, "n60": n60}
def pbreak_accuracy(self, period: str = "all") -> dict: def pbreak_accuracy(self, period: str = "all") -> dict:
"""Trefferquote der Abprall/Durchbruch-Prognose (nur ausgewertete Zeilen).""" """Trefferquote der Abprall/Durchbruch-Prognose (nur ausgewertete Zeilen)."""
since, until = self._range_to_ts(period) since, until = self._range_to_ts(period)
@@ -521,38 +583,60 @@ class HistoryLogger:
"pct": round(100.0 * (r["c"] or 0) / (r["n"] or 1), 1)} "pct": round(100.0 * (r["c"] or 0) / (r["n"] or 1), 1)}
return out return out
def alignment_stats(self, last_n: int = 40) -> dict: def alignment_stats(self, last_n: int = 0) -> dict:
"""Ausrichtungs-Split der letzten N geschlossenen Trades: liefen sie MIT, """Ausrichtungs-Split: liefen die Trades MIT, GEGEN oder OHNE die Empfehlung?
GEGEN oder OHNE die Empfehlung (rec_signal beim Entry)? Für die ehrliche
Verhaltens-Anzeige im Order-Bestätigungsdialog (B0 als Software — die ⚠⚠ ZWEI AENDERUNGEN AM 2026-08-06, beide gemessen begruendet:
gemessene Kern-Leckage sind diskretionäre Gegen-Signal-Trades).""" (1) **Volle Historie statt der letzten 40 Trades** (`last_n=0`). Ueber
1.216 geschlossene Trades ist der Befund beidhaelftig robust; ueber 40
war er eine Momentaufnahme. Das ist die einzige Richtungsaussage ueber
die Gesamtempfehlung, die den 2-Stichproben-Test besteht — sie sollte
auf der ganzen Stichprobe stehen.
(2) **Zusaetzlich JE LOT normiert** (`pnl_lot`). Absolute Euro-Vergleiche
ueber Zeitraeume mit unterschiedlicher Positionsgroesse sind ungueltig
— am 04.08. sah ein reiner Groessenzuwachs wie ein Strategie-Effekt aus
(Ø +55 € gegen +19 €, je Lot aber identisch). Das Order-Dialog-Feld
zeigt deshalb beides.
Gemessener Stand (06.08., ganze Historie, je Lot):
MIT n=354 WR 56,5 % +3,22 €/Lot (H1 +0,37 / H2 +5,62)
GEGEN n= 87 WR 40,2 % 6,60 €/Lot (H1 7,01 / H2 5,68)
OHNE n=775 WR 56,6 % +0,36 €/Lot (kippt zwischen den Haelften)
MIT und OHNE sind praktisch gleich — der Empfehlung zu FOLGEN bringt
gemessen nichts, nur sie zu VERLETZEN kostet. Sie ist ein Veto, keine
Prognose.
"""
sql = ("SELECT direction, rec_signal, pnl, lots FROM trades "
"WHERE exit_time IS NOT NULL AND pnl IS NOT NULL AND lots > 0 "
"ORDER BY exit_time DESC")
args: tuple = ()
if last_n and last_n > 0:
sql += " LIMIT ?"
args = (last_n,)
with self._connect() as conn: with self._connect() as conn:
rows = conn.execute(""" rows = conn.execute(sql, args).fetchall()
SELECT direction, rec_signal, pnl FROM trades
WHERE exit_time IS NOT NULL AND pnl IS NOT NULL def fasse(grp: list) -> dict:
ORDER BY exit_time DESC LIMIT ? n = len(grp)
""", (last_n,)).fetchall() if not n:
out = {} return {"n": 0, "wr": None, "pnl": 0.0, "pnl_lot": None}
for key, match in (("with", True), ("against", False)): pnl = sum(r["pnl"] for r in grp)
grp = [] lots = sum(r["lots"] for r in grp) or 0.0
return {"n": n,
"wr": round(100 * sum(1 for r in grp if r["pnl"] > 0) / n),
"pnl": round(pnl, 2),
"pnl_lot": round(pnl / lots, 2) if lots > 0 else None}
mit, gegen, ohne = [], [], []
for r in rows: for r in rows:
rec = (r["rec_signal"] or "").upper() rec = (r["rec_signal"] or "").upper()
if rec not in ("LONG", "SHORT"): if rec not in ("LONG", "SHORT"):
continue ohne.append(r)
aligned = (rec == "LONG") == (r["direction"] == "BUY") elif (rec == "LONG") == (r["direction"] == "BUY"):
if aligned == match: mit.append(r)
grp.append(r["pnl"]) else:
n = len(grp) gegen.append(r)
out[key] = {"n": n, return {"with": fasse(mit), "against": fasse(gegen), "none": fasse(ohne)}
"wr": round(100 * sum(1 for p in grp if p > 0) / n) if n else None,
"pnl": round(sum(grp), 2)}
none_grp = [r["pnl"] for r in rows
if (r["rec_signal"] or "").upper() not in ("LONG", "SHORT")]
n = len(none_grp)
out["none"] = {"n": n,
"wr": round(100 * sum(1 for p in none_grp if p > 0) / n) if n else None,
"pnl": round(sum(none_grp), 2)}
return out
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# STATISTIK-QUERIES # STATISTIK-QUERIES
+163
View File
@@ -0,0 +1,163 @@
"""Szenario-Test der Selbst-Kalibrierung (rec_outcomes) — ohne MT5, auf DB-Kopie.
Geprueft werden die Faelle, die live schiefgehen koennen:
1) Episode wird EINMAL geloggt, nicht je Minute
2) WARTEN re-armt → die naechste Richtung ist eine neue Episode
3) Auswertung rechnet den Forward-Return korrekt (inkl. Broker-Offset)
4) fehlende M1-Bars → Zeile bleibt offen statt falsch bewertet
5) rec_accuracy aggregiert Trefferquote und Ø-Return richtig
"""
import os
import shutil
import sqlite3
import sys
import tempfile
import time
import types
sys.path.insert(0, r"c:\Users\ah\Downloads\tradingrobot")
from core.engine import TradingEngine
from core.history import HistoryLogger
QUELL = r"c:\Users\ah\Downloads\tradingrobot\oil_widget_history.db"
TMP = os.path.join(tempfile.mkdtemp(), "t.db")
shutil.copy(QUELL, TMP)
con = sqlite3.connect(TMP)
con.execute("DELETE FROM rec_outcomes")
con.commit()
h = HistoryLogger(TMP)
fehler = []
def pruef(lbl, ist, soll):
ok = ist == soll
if not ok:
fehler.append(lbl)
print(f" {'OK ' if ok else 'FEHLER'} {lbl:<50} ist={ist!r:<14} soll={soll!r}")
print("=" * 86)
print(" 1) Episoden-Logging: eine Zeile je Richtungswechsel, nicht je Minute")
print("=" * 86)
class E:
"""Nur der Episoden-Teil aus `_run_analysis`, isoliert nachgestellt."""
def __init__(self):
self._rec_epi_sig = None
self.history = h
def tick(self, sig):
if sig in ("LONG", "SHORT") and sig != self._rec_epi_sig:
self.history.log_rec_outcome(signal=sig, conf_pct=70, tf="M5",
price=75.0, atr=0.2)
if sig in ("LONG", "SHORT", "WARTEN"):
self._rec_epi_sig = sig if sig != "WARTEN" else None
e = E()
for s in ["LONG"] * 5: # dieselbe Episode, 5 Minuten lang
e.tick(s)
pruef("5x LONG hintereinander → 1 Zeile",
con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 1)
for s in ["SHORT"] * 3: # Flip = neue Episode
e.tick(s)
pruef("Flip auf SHORT → 2 Zeilen",
con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 2)
e.tick("WARTEN"); e.tick("WARTEN")
pruef("WARTEN erzeugt keine Zeile",
con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 2)
e.tick("SHORT")
pruef("nach WARTEN ist SHORT eine NEUE Episode",
con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 3)
print()
print("=" * 86)
print(" 2) Auswertung gegen candles_m1 (mit Broker-Offset)")
print("=" * 86)
con.execute("DELETE FROM rec_outcomes")
con.execute("DELETE FROM candles_m1 WHERE symbol='TESTSYM'")
OFF = 3 * 3600
t0 = int(time.time()) - 7200 # vor 2 h → beide Horizonte faellig
# LONG @ 75.00, ATR 0.20; nach 30 min steht 75.10 (+0,5 ATR), nach 60 min 74.90
con.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr) "
"VALUES (?,?,?,?,?,?)", (t0, "LONG", 70, "M5", 75.00, 0.20))
for minuten, kurs in ((30, 75.10), (60, 74.90)):
con.execute("INSERT OR REPLACE INTO candles_m1 (time,o,h,l,c,spread,tick_volume,symbol)"
" VALUES (?,?,?,?,?,?,?,?)",
(t0 + minuten * 60 + OFF, kurs, kurs, kurs, kurs, 20, 1, "TESTSYM"))
con.commit()
eng = TradingEngine.__new__(TradingEngine)
eng._rec_eval_ts = 0.0
eng.trader = types.SimpleNamespace(_broker_offset_s=lambda: OFF)
import core.engine as CE
_alt = CE.HISTORY_DB_FILE
CE.HISTORY_DB_FILE = TMP
eng._evaluate_rec_outcomes()
CE.HISTORY_DB_FILE = _alt
r = con.execute("SELECT ret30,hit30,ret60,hit60,done FROM rec_outcomes").fetchone()
pruef("ret30 = (75.10-75.00)/0.20 = +0.5", round(r[0], 3), 0.5)
pruef("hit30 = 1 (lief in Signalrichtung)", r[1], 1)
pruef("ret60 = (74.90-75.00)/0.20 = -0.5", round(r[2], 3), -0.5)
pruef("hit60 = 0", r[3], 0)
pruef("done = 1", r[4], 1)
print()
print("=" * 86)
print(" 3) SHORT-Richtung spiegelbildlich · fehlende Bars → bleibt offen")
print("=" * 86)
con.execute("DELETE FROM rec_outcomes")
# ⚠ Test-Kerzen aus Abschnitt 2 MUESSEN weg — sonst faellt die dortige
# 60-Minuten-Bar in das Fenster dieses Abschnitts (beide Anker liegen 2 h
# zurueck) und der Fall "keine Bar" waere gar nicht getestet.
con.execute("DELETE FROM candles_m1 WHERE symbol='TESTSYM'")
t1 = int(time.time()) - 7200
con.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr) "
"VALUES (?,?,?,?,?,?)", (t1, "SHORT", 70, "M5", 75.00, 0.20))
con.execute("INSERT OR REPLACE INTO candles_m1 (time,o,h,l,c,spread,tick_volume,symbol)"
" VALUES (?,?,?,?,?,?,?,?)",
(t1 + 1800 + OFF, 74.9, 74.9, 74.9, 74.9, 20, 1, "TESTSYM"))
# fuer 60 min BEWUSST keine Bar → Zeile darf nicht faelschlich bewertet werden.
# ⚠ Dafuer muss das 60-Minuten-Fenster WIRKLICH leer sein: die DB-Kopie enthaelt
# ECHTE candles_m1 aus dem Live-Betrieb, und die liegen genau hier. Ohne dieses
# DELETE testet man nicht "keine Bar", sondern "echte Bar" (erster Versuch lieferte
# ret60 = -14,985 = der reale Kurs ~78).
con.execute("DELETE FROM candles_m1 WHERE time > ? AND time <= ?",
(t1 + 3600 + OFF - 900, t1 + 3600 + OFF))
con.commit()
eng._rec_eval_ts = 0.0
CE.HISTORY_DB_FILE = TMP
eng._evaluate_rec_outcomes()
CE.HISTORY_DB_FILE = _alt
r = con.execute("SELECT ret30,hit30,ret60,done FROM rec_outcomes").fetchone()
pruef("SHORT: Kurs faellt → ret30 = +0.5", round(r[0], 3), 0.5)
pruef("SHORT: hit30 = 1", r[1], 1)
pruef("ret60 ohne Bar bleibt NULL", r[2], None)
pruef("done bleibt offen (0/None)", bool(r[3]), False)
print()
print("=" * 86)
print(" 4) rec_accuracy aggregiert")
print("=" * 86)
con.execute("DELETE FROM rec_outcomes")
for ret, hit in ((0.5, 1), (0.3, 1), (-0.4, 0), (-0.2, 0)):
con.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr,"
"ret30,hit30,ret60,hit60,done) VALUES (?,?,?,?,?,?,?,?,?,?,1)",
(int(time.time()), "LONG", 70, "M5", 75.0, 0.2, ret, hit, ret, hit))
con.commit()
a = h.rec_accuracy(200)
pruef("n = 4", a["n"], 4)
pruef("Trefferquote 30' = 50 %", a["wr30"], 50.0)
pruef("Ø-Return 30' = +0.05", a["avg30"], 0.05)
con.close()
print()
print("=" * 86)
print(" ERGEBNIS: " + ("alle Szenarien korrekt" if not fehler
else f"{len(fehler)} FEHLER: {fehler}"))
print("=" * 86)
sys.exit(1 if fehler else 0)
+56 -2
View File
@@ -172,6 +172,52 @@ function renderVerdict(v, snap) {
`<span class="m-vote">${a}</span></div>` + `<span class="m-vote">${a}</span></div>` +
`<div class="m-det">${mod.detail || ""}</div></div>`; `<div class="m-det">${mod.detail || ""}</div></div>`;
}).join(""); }).join("");
// ── Selbst-Kalibrierung: die Karte zeigt ihre eigene Trefferquote ─────
// ⚠ Gegen 50 % zu lesen, nicht gegen 0 — es ist eine Richtungsaussage.
// Ausgangsstand (analyze_signal_live.py, 1.071 Episoden): 4851 %.
// Bis genug Episoden ausgewertet sind, steht hier ehrlich „sammelt noch".
// Null-sicher (s. Cache-Regel): neue IDs treffen evtl. auf alte, gecachte HTML.
const trkEl = $("vd-selfcal");
if (trkEl) {
const tr = snap && snap.rec_track;
if (tr && tr.n >= 20 && tr.wr30 != null) {
const d = (x) => (x == null ? "—" : (x >= 0 ? "+" : "") + fmt(x, 3));
const cls = tr.wr30 >= 55 ? "up" : tr.wr30 <= 45 ? "down" : "";
trkEl.className = "vd-selfcal " + cls;
trkEl.textContent =
`🎯 eigene Trefferquote (letzte ${tr.n} Episoden): ${tr.wr30} % auf 30`
+ (tr.wr60 != null ? ` · ${tr.wr60} % auf 60` : "")
+ ` · Ø ${d(tr.avg30)}×ATR`;
trkEl.title = "Selbst-Kalibrierung: jede gerichtete Empfehlung wird nach "
+ "30 und 60 Minuten gegen den echten Kurs ausgewertet. Gegen 50 % lesen, "
+ "nicht gegen 0. Gemessener Ausgangsstand über 1.071 Episoden: 4851 %.";
} else {
trkEl.className = "vd-selfcal dim";
trkEl.textContent = `🎯 eigene Trefferquote: sammelt noch`
+ (tr && tr.n ? ` (${tr.n} von 20 Episoden ausgewertet)` : "");
trkEl.title = "Jede gerichtete Empfehlung wird nach 30/60 Minuten gegen "
+ "den echten Kurs ausgewertet. Ab 20 ausgewerteten Episoden erscheint "
+ "die Quote hier.";
}
}
// ── Warum WARTEN? Verteilung der blockierenden Gates (heute) ───────────
const blkEl = $("vd-blocks");
if (blkEl) {
const bm = snap && snap.block_mix;
if (bm && bm.total) {
const pc = (n) => Math.round(100 * n / bm.total);
const top = (bm.top || []).map(x => `${x.grund} ${pc(x.n)} %`).join(" · ");
blkEl.textContent = `⛔ heute ${pc(bm.frei)} % ohne Block`
+ (top ? ` · ${top}` : "");
blkEl.title = "Welches Gate hält die Empfehlung gerade zurück — über den "
+ "heutigen Tag gezählt. Ohne diese Zeile war „warum gab es so wenige "
+ "Empfehlungen?" + '"' + " nur per Datenbank-Abfrage zu beantworten.";
} else {
blkEl.textContent = "";
}
}
} }
@@ -1171,8 +1217,16 @@ document.querySelectorAll(".btn[data-side]").forEach(btn => {
const isWait0 = (sigNow === "WARTEN" || sigNow === "—"); const isWait0 = (sigNow === "WARTEN" || sigNow === "—");
if (al && cl.items.some(x => x.key === "signal" && x.state === "fail")) { if (al && cl.items.some(x => x.key === "signal" && x.state === "fail")) {
const grp = isWait0 ? al.none : al.against, ref = al.with; const grp = isWait0 ? al.none : al.against, ref = al.with;
const f = g => (g && g.n >= 5) // ⚠ JE LOT ist die belastbare Zahl: absolute Euro-Vergleiche sind bei
? `${g.wr}% Treffer · ${g.pnl >= 0 ? "+" : ""}${fmt(g.pnl, 0)} € (n${g.n})` : null; // wechselnder Positionsgroesse ungueltig (Lehre 04.08. — ein reiner
// Groessenzuwachs sah wie ein Strategie-Effekt aus). Seit 06.08. laeuft
// der Split ueber die VOLLE Historie (1.216 Trades) statt ueber 40.
const f = g => {
if (!g || g.n < 5) return null;
const lot = (g.pnl_lot === null || g.pnl_lot === undefined) ? ""
: ` · ${g.pnl_lot >= 0 ? "+" : ""}${fmt(g.pnl_lot, 2)} €/Lot`;
return `${g.wr}% Treffer${lot} · ${g.pnl >= 0 ? "+" : ""}${fmt(g.pnl, 0)} € (n${g.n})`;
};
const own = f(grp), rf = f(ref); const own = f(grp), rf = f(ref);
if (own) body += `\n\n📊 Deine Trades ${isWait0 ? "OHNE" : "GEGEN"} Signal: ${own}`; if (own) body += `\n\n📊 Deine Trades ${isWait0 ? "OHNE" : "GEGEN"} Signal: ${own}`;
if (rf) body += `\n📊 Mit Signal: ${rf}`; if (rf) body += `\n📊 Mit Signal: ${rf}`;
+11 -2
View File
@@ -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=143"> <link rel="stylesheet" href="style.css?v=144">
</head> </head>
<body> <body>
<!-- Modul-Menü (ganz oben) --> <!-- Modul-Menü (ganz oben) -->
@@ -106,6 +106,15 @@
Einstiegspreis und Exit, nicht aus der Trefferquote — lies das hier als Einstiegspreis und Exit, nicht aus der Trefferquote — lies das hier als
<b>Auslöser für eine Order am Level</b>, nicht als Kursprognose. <b>Auslöser für eine Order am Level</b>, nicht als Kursprognose.
</div> </div>
<!-- Warum WARTEN? Verteilung der blockierenden Gates über den heutigen Tag.
Ohne die Zeile war „warum gab es so wenige Empfehlungen?" nur per
DB-Abfrage zu beantworten (real 06.08.: 6 von 646 Zeilen ohne Block). -->
<div class="vd-blocks" id="vd-blocks"></div>
<!-- SELBST-KALIBRIERUNG (2026-08-06): die Karte zeigt ihre eigene
Trefferquote. Muster wie `pbreak_predictions` — das hat den Ausfall des
P(break)-Modells LIVE aufgedeckt, was kein Backtest konnte. ⚠ Gegen
50 % zu lesen, nicht gegen 0. -->
<div class="vd-selfcal dim" id="vd-selfcal"></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>
@@ -331,6 +340,6 @@
</div> </div>
<div id="toast" class="toast hidden"></div> <div id="toast" class="toast hidden"></div>
<script src="app.js?v=143"></script> <script src="app.js?v=144"></script>
</body> </body>
</html> </html>
+10
View File
@@ -221,6 +221,16 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
border:1px dashed var(--line);border-radius:var(--r-sm); border:1px dashed var(--line);border-radius:var(--r-sm);
padding:7px 10px;margin:0 0 11px} padding:7px 10px;margin:0 0 11px}
.vd-note b{color:var(--bright);font-weight:700} .vd-note b{color:var(--bright);font-weight:700}
/* Block-Verteilung + Selbst-Kalibrierung: beide bewusst leise (Kontext, keine
Handlungsaufforderung). Die Trefferquote faerbt nur bei klarer Abweichung von
den gemessenen ~50 %, damit Rauschen nicht wie ein Signal aussieht. */
.vd-blocks{font-size:12px;color:var(--dim);margin:0 0 4px;font-family:var(--mono)}
/* ⚠ NICHT `.vd-track` nennen — so heisst bereits die Schiene der Bias-Nadel
(`.vd-meter .vd-track`). Eine gleichnamige Klasse haette sie ueberschrieben
und die Nadel unsichtbar gemacht. */
.vd-selfcal{font-size:12px;color:var(--dim);margin:0 0 9px;font-family:var(--mono)}
.vd-selfcal.up{color:var(--up)} .vd-selfcal.down{color:var(--down)}
.vd-selfcal.dim{color:var(--dim);opacity:.75}
.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)}