Review-Durchlauf: Telemetrie-Epochen, doppeltes Verdict, Doku-Korrektur
Erster vollstaendiger Durchlauf von docs/review-prompt.md. KEINE Strategie-Aenderung - alles Telemetrie, Doku und toter Code. 1) analyze_divergence.py kannte keine Epochen und war damit selbst driftanfaellig. D0 mischte Vorhersagen des alten und des am 31.07. nachtrainierten P(break)-Modells und meldete dessen Fehlkalibrierung als aktuellen Alarm (das neue Modell hat n=0, Markt seit Fr zu). B las die Prae-Migrations-NULLs von block_reason als blinden Fleck. C druckte bei 0 Zeilen ein "OK", obwohl es fehlende Daten waren. Neu: _EPOCHS + _clamp(). 2) Konsens-Pfeil AR;K: der MQL5-Export rechnete im ~5-s-Takt ein komplettes zweites _verdict(), obwohl der Indikator die Zeile seit v1.33 per Default verwirft. Neu [trading] export_consensus_arrow (Default false). Verifiziert ueber die exportierte CSV: AR;K weg, AR;L und AR;S bleiben. 3) /api/autosqueeze loggt jetzt die Herkunft ([WEB] ...) wie /api/autosignal. Vorher war ein Zustandswechsel nicht als Nutzeraktion belegbar. 4) CLAUDE.md: die Reversal-Kennzahl "OR +0,185 / PF 1,35 / 70 %" stand unkorrigiert an der Fundstelle, die Widerlegung 2000 Zeilen weiter im Legacy-Recheck. Korrektur an die Fundstelle geholt. 5) core/notify.py: zwei tote "import datetime" entfernt (beide Funktionen nutzen _time), funktional nachgetestet. Geprueft und sauber: 0 fehlende Frontend-IDs von 86, nur 2 Config-Schluessel ohne Leser (beide dokumentiert dormant), Snapshot-Median 13 ms und alle DB-Abfragen <13 ms -> keine Performance-Massnahme, 124 Datei- und 83 Funktionsreferenzen in CLAUDE.md stimmen. Zwischenverdacht zurueckgezogen: "block_reason erklaert nur 33 % der WARTEN" war ein Migrations-Artefakt; seit 01.08. 100 % Abdeckung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3124fe5b4f
commit
20cd04caa7
@@ -439,10 +439,18 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
||||
**Winkel-Richtung, gegen die EMA** (überverkauft + Winkel auf → LONG; überkauft +
|
||||
Winkel ab → SHORT). Setup `WAVE_REV_*`, Grund „🔄 Reversal"; hebt Anti-Überdehnung
|
||||
**und** M30-Filter bewusst auf. **Schwelle 3,5→3,0 gesenkt** (User wollte mehr
|
||||
Bounces; `backtest_bounce.py`): Ø-R **+0,185**, PF 1,35, Treffer 70 %, Worst −2×ATR
|
||||
(SL-gedeckelt), ~1,5× mehr Bounces als 3,5 — profitabel, aber **schwächer als
|
||||
Trend** & **regime-anfällig** (trägt in Seitwärts/Mean-Reversion, blutet in starken
|
||||
Trends). Nachrangiges, markiertes Setup. **Bounce-Status — Multi-TF** im Wave-
|
||||
Bounces; `backtest_bounce.py`): Ø-R +0,185, PF 1,35, Treffer 70 %, Worst −2×ATR
|
||||
(SL-gedeckelt), ~1,5× mehr Bounces als 3,5.
|
||||
⚠⚠ **DIESE ZAHLEN GELTEN NICHT MEHR** (Legacy-Recheck 2026-07-31, hier eingetragen
|
||||
am 2026-08-02): `backtest_bounce.py` nutzt den **vereinfachten Exit OHNE Kosten**
|
||||
(`LEGACY_SIMPLE`: kein Breakeven, kein Lock, kein Time-Stop). Mit dem **echten**
|
||||
Exit + Echtkosten liegt das Setup bei ~**Breakeven** (PF 0,93/1,06) — es ist
|
||||
**kein +0,185-Ertragsbringer**. Was HÄLT, ist die *relative* Aussage: Reversal
|
||||
schlägt Trend in beiden Hälften (+0,140/+0,102). Deshalb bleibt es ein sinnvolles
|
||||
**nachrangiges, markiertes Zweitsignal** (so ist es live auch eingebaut), aber
|
||||
nicht mehr als das. Details im Abschnitt „LEGACY-RECHECK".
|
||||
Weiterhin gültig: **schwächer als Trend** & **regime-anfällig** (trägt in
|
||||
Seitwärts/Mean-Reversion, blutet in starken Trends). Nachrangiges, markiertes Setup. **Bounce-Status — Multi-TF** im Wave-
|
||||
Snapshot (`wave.bounce = {state, dir, tf}`): scannt **M1/M5/M15/M30** (`_bounce_one`,
|
||||
Aggregat in `refresh_market`, ~15 s-Throttle via TF-Ampel-Fetch), nimmt den
|
||||
**stärksten** (aktiv>erwartet, dann größte Überdehnung). **`expected`** = überdehnt
|
||||
@@ -2531,6 +2539,54 @@ konstant; (d) es gab **keine Telemetrie über die Entscheidung selbst**.
|
||||
in ~20 min gefunden; die Trefferquote spricht für weitere. `analyze_divergence.py`
|
||||
regelmäßig laufen lassen (Kandidat für den Wochenreport).
|
||||
|
||||
## Gesamtprüfung 2026-08-02 (nach `docs/review-prompt.md`)
|
||||
|
||||
Erster vollständiger Durchlauf des Review-Prompts. **Keine Strategie-Änderung** — alle
|
||||
Fixes betreffen Telemetrie, Doku und toten Code.
|
||||
|
||||
**Behoben:**
|
||||
1. **`analyze_divergence.py` kannte keine Epochen** — und war damit selbst
|
||||
driftanfällig. D0 mischte Vorhersagen des ALTEN und des am 31.07. nachtrainierten
|
||||
P(break)-Modells und meldete dessen Fehlkalibrierung als aktuellen Alarm (das neue
|
||||
Modell hatte **n=0**, weil der Markt seit Fr 22:55 zu ist); B las die
|
||||
Prä-Migrations-`NULL`s von `block_reason` als blinden Fleck („None 66,8 %"); C
|
||||
druckte bei 0 Zeilen ein „OK", obwohl es fehlende Daten waren. Neu: `_EPOCHS` +
|
||||
`_clamp()` schneiden jede Auswertung auf den Beginn ihrer eigenen Datenlage.
|
||||
⚠ **Beim Nachtrainieren eines Modells das Datum in `_EPOCHS` mitziehen** — sonst
|
||||
schlägt der Wächter wochenlang Fehlalarm, und ein Wächter, der immer warnt, wird
|
||||
ignoriert.
|
||||
2. **Konsens-Pfeil: doppelte Arbeit für eine unsichtbare Linie.** Der MQL5-Export
|
||||
rechnete im ~5-s-Takt ein **komplettes zweites `_verdict()`** für `AR;K` — der
|
||||
Indikator verwirft die Zeile aber seit v1.33 per Default (`InpShowConsensus=false`).
|
||||
Neu `[trading] export_consensus_arrow` (Default **false**, spiegelt den Indikator).
|
||||
Nebeneffekt beseitigt: das zweite Verdict konnte vom Verdict im Snapshot abweichen.
|
||||
3. **`/api/autosqueeze` loggt jetzt die Herkunft** (`[WEB] …`) wie `/api/autosignal`.
|
||||
Vorher war bei einem Zustandswechsel nicht belegbar, ob Dashboard oder Code —
|
||||
bei einem Schalter für autonome Echtgeld-Einstiege zu wenig.
|
||||
4. **Reversal-Kennzahl an der Fundstelle korrigiert.** „ØR +0,185 · PF 1,35 · 70 %
|
||||
Treffer" stand unverändert in der Reversal-Sektion, die Widerlegung 2 000 Zeilen
|
||||
später im Legacy-Recheck. Wer die Sektion liest, sah die Korrektur nie.
|
||||
5. Zwei tote `import datetime` in `core/notify.py` entfernt (beide Funktionen nutzen
|
||||
`_time`), funktional nachgetestet.
|
||||
|
||||
**Geprüft und sauber:** 0 fehlende Frontend-IDs (von 86) · nur 2 Config-Schlüssel ohne
|
||||
Leser (beide dokumentiert dormant) · Snapshot-Median **13 ms**, alle DB-Abfragen <13 ms,
|
||||
Indizes vorhanden → **keine Performance-Maßnahme nötig** · 124 Datei- und 83
|
||||
Funktionsreferenzen in CLAUDE.md stimmen · Asset- und Indikator-Version konsistent.
|
||||
|
||||
**Zwischenverdacht zurückgezogen:** „`block_reason` erklärt nur 33 % der WARTEN" war
|
||||
ein reines Migrations-Artefakt (Spalte kam am 31.07.); seit 01.08. **100 % Abdeckung**.
|
||||
|
||||
**Offen (nicht behoben, bewusst):**
|
||||
- **`auto_flip_close=true` läuft ohne jede UI-Anzeige.** Ein Exit, der gemessen in
|
||||
BEIDEN Hälften Geld kostet (−92/−160 R), schließt echte Trades; `flip_close`,
|
||||
`flip_close_min_r` und `flip_close_count` haben keinen Frontend-Konsumenten. Nur
|
||||
Log + Telegram. Entweder anzeigen oder abschalten — Entscheidung des Users.
|
||||
- **`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`
|
||||
nicht. Kandidat für den nächsten Recheck.
|
||||
- Die 5 Zähler-Snapshot-Felder (`close_alert_count` usw.) werden von nichts abgefragt.
|
||||
|
||||
## Ausstehende Messungen — Erinnerung per Timer
|
||||
|
||||
`measurement_reminder.py` + Windows-Task **`OilMeasurementReminder`** (täglich 18:00)
|
||||
|
||||
+59
-3
@@ -38,6 +38,36 @@ LOG = "oil_widget.log"
|
||||
_EXP_WAIT = 43.0 # backtest_dist.py: ~57 % LONG/SHORT, ~43 % WARTEN
|
||||
_EXP_WAIT_TOL = 15.0 # ab dieser Abweichung wird geflaggt (Pp)
|
||||
|
||||
# ── EPOCHEN ───────────────────────────────────────────────────────────────────
|
||||
# ⚠ Ergänzt 2026-08-02 (Review-Durchgang 2). Der Wächter rechnete über ein starres
|
||||
# Zeitfenster und wusste NICHT, ab wann ein Modell bzw. eine Telemetrie gilt.
|
||||
# Dadurch meldete er zwei FEHLALARME, die beide nur Artefakte des Fensters waren:
|
||||
# · D0 mischte Vorhersagen des ALTEN und des am 31.07. nachtrainierten
|
||||
# P(break)-Modells (Alarm „Δ +15 Pp" stammte zu 100 % aus dem alten; das neue
|
||||
# hatte n=0, weil der Markt seit Fr 22:55 zu ist).
|
||||
# · B zeigte „block_reason: None 66,8 %" — die Spalte existiert aber erst seit
|
||||
# dem 31.07., davor ist NULL schlicht „nicht erhoben", kein blinder Fleck.
|
||||
# Ein Wächter, der nach jedem Nachtraining wochenlang Alarm schlägt, wird
|
||||
# ignoriert — und dann fängt er den echten Fall auch nicht mehr.
|
||||
# Beim Ändern eines Modells/einer Telemetrie hier das Datum mitziehen.
|
||||
_EPOCHS = {
|
||||
# P(break) auf live-spiegelnder Stichprobe neu gefittet (neue _PB_MU/_SD/_W)
|
||||
"pbreak_model": "2026-07-31",
|
||||
# `recommendations.block_reason` eingeführt (DB-Migration)
|
||||
"block_reason": "2026-07-31",
|
||||
}
|
||||
|
||||
|
||||
def _epoch_ts(name: str) -> float:
|
||||
return datetime.fromisoformat(_EPOCHS[name]).timestamp()
|
||||
|
||||
|
||||
def _clamp(t0: float, epoch: str) -> tuple[float, bool]:
|
||||
"""→ (effektiver Startzeitpunkt, wurde beschnitten?). Sorgt dafür, dass eine
|
||||
Auswertung nie über den Beginn ihrer eigenen Epoche hinausreicht."""
|
||||
e = _epoch_ts(epoch)
|
||||
return (max(t0, e), e > t0)
|
||||
|
||||
|
||||
def _flag(ok: bool) -> str:
|
||||
return "OK " if ok else "⚠ "
|
||||
@@ -65,8 +95,16 @@ def sec_a(c, t0):
|
||||
|
||||
def sec_b(c, t0):
|
||||
print("\n" + "=" * 84)
|
||||
print(" B) WELCHES GATE BLOCKT? (block_reason, seit 2026-07-31)")
|
||||
print(f" B) WELCHES GATE BLOCKT? (block_reason, seit {_EPOCHS['block_reason']})")
|
||||
print("=" * 84)
|
||||
# ⚠ Fenster auf die Epoche beschneiden: vor der Migration ist block_reason
|
||||
# NULL, weil die Spalte nicht existierte — nicht, weil kein Gate gegriffen
|
||||
# hätte. Ungeschnitten meldete der Report „None 66,8 %" und legte damit einen
|
||||
# blinden Fleck nahe, den es nicht gibt (nachgeprüft: seit 01.08. 100 % Abdeckung).
|
||||
t0, cut = _clamp(t0, "block_reason")
|
||||
if cut:
|
||||
print(f" (Fenster auf den Beginn der Telemetrie gekürzt — davor wurde der "
|
||||
f"Grund nicht erhoben)")
|
||||
try:
|
||||
c.execute("SELECT block_reason, COUNT(*) FROM recommendations "
|
||||
"WHERE timestamp>=? AND signal='WARTEN' GROUP BY block_reason "
|
||||
@@ -111,7 +149,13 @@ def sec_c(t0):
|
||||
except FileNotFoundError:
|
||||
print(" kein Log gefunden"); return
|
||||
if not sw:
|
||||
print(" keine TF-Wechsel im Zeitraum OK"); return
|
||||
# ⚠ Kein „OK" mehr (2026-08-02): 0 Wechsel heißt bei geschlossenem Markt
|
||||
# schlicht „keine Daten". Das als Entwarnung zu drucken, wäre genau die
|
||||
# Sorte stiller Fehlschluss, die dieser Report aufdecken soll.
|
||||
print(" 0 TF-Wechsel gefunden — das ist KEINE Entwarnung, sondern")
|
||||
print(" möglicherweise fehlende Evidenz (Markt zu / Log rotiert / Bot aus).")
|
||||
print(" Aussagekräftig ist dieser Abschnitt nur über Handelszeiten.")
|
||||
return
|
||||
days = max(1.0, (datetime.now().timestamp() - t0) / 86400)
|
||||
gaps = sorted((sw[i][0] - sw[i - 1][0]).total_seconds() for i in range(1, len(sw)))
|
||||
med = gaps[len(gaps) // 2] / 60 if gaps else 0
|
||||
@@ -137,12 +181,24 @@ def sec_d0(c, t0):
|
||||
print("\n" + "=" * 84)
|
||||
print(" D0) MODELL-KALIBRIERUNG — sagt es voraus, was eintritt?")
|
||||
print("=" * 84)
|
||||
# ⚠ NUR Vorhersagen des AKTUELLEN Modells auswerten (Epochen-Schnitt, 2026-08-02).
|
||||
# Ungeschnitten mischte diese Sektion die Vorhersagen des alten und des am
|
||||
# 31.07. nachtrainierten Modells und meldete dessen Fehlkalibrierung als
|
||||
# aktuellen Alarm — obwohl das neue Modell noch gar keine ausgewertete
|
||||
# Vorhersage hatte. Nach einem Nachtraining hätte der Wächter so wochenlang
|
||||
# falsch Alarm geschlagen, bis die Altdaten aus dem Fenster gerollt wären.
|
||||
t0, cut = _clamp(t0, "pbreak_model")
|
||||
if cut:
|
||||
print(f" (nur Vorhersagen seit dem Nachtraining am {_EPOCHS['pbreak_model']})")
|
||||
c.execute("""SELECT AVG(p_break), AVG(CASE WHEN outcome='break' THEN 100.0 ELSE 0 END),
|
||||
COUNT(*) FROM pbreak_predictions
|
||||
WHERE ts>=? AND outcome IS NOT NULL""", (t0,))
|
||||
pm, rm, n = c.fetchone()
|
||||
if not n or n < 30:
|
||||
print(f" nur {n or 0} ausgewertete Vorhersagen — zu wenig"); return
|
||||
print(f" nur {n or 0} ausgewertete Vorhersagen seit dem Nachtraining — zu wenig.")
|
||||
print(f" (Das ist KEIN Entwarnungssignal, sondern fehlende Evidenz. "
|
||||
f"Fälligkeit: Messung `pbreak_accuracy_v2`, ≥800 Vorhersagen.)")
|
||||
return
|
||||
gap = (rm or 0) - (pm or 0)
|
||||
print(f" n={n} Ø-Vorhersage {pm:.1f} % echte Rate {rm:.1f} % "
|
||||
f"Δ {gap:+.1f} Pp {_flag(abs(gap) <= 8)}")
|
||||
|
||||
@@ -385,6 +385,10 @@ class TradingEngine:
|
||||
self._export_mql5 = self.cfg["trading"].get("export_mql5_levels", "true").lower() == "true"
|
||||
# Kegel im MT5-Chart: per Default AUS, seit die Pfeile ihn ersetzen (2026-07-31).
|
||||
self._export_cone = self.cfg["trading"].get("export_cone", "false").lower() == "true"
|
||||
# Konsens-Pfeil (AR;K) — Default AUS, spiegelt `InpShowConsensus=false` im
|
||||
# Indikator. Bei `true` wird je Export ein zweites `_verdict()` gerechnet.
|
||||
self._export_consensus_arrow = self.cfg["trading"].get(
|
||||
"export_consensus_arrow", "false").lower() == "true"
|
||||
self._draw_last = {"r": None, "s": None} # letzte gezeichnete S/R (Hysterese)
|
||||
try: # WHT-Quellensteuer (Broker behält % je GEWINN ein) → Netto-Anzeige
|
||||
self._wht_pct = max(0.0, float(self.cfg["trading"].get("wht_pct", "0") or 0))
|
||||
@@ -2368,6 +2372,16 @@ class TradingEngine:
|
||||
f"{round(float(_sq['level']) + _sd * 2.0 * _atr5, 3)};"
|
||||
f"Squeeze {_sq['dir']}")
|
||||
# (K) Konsens — Länge proportional zum Bias-Betrag (max 2×ATR)
|
||||
# ⚠ NUR berechnen, wenn der Pfeil auch gezeichnet wird
|
||||
# (Review 2026-08-02): Der Indikator verwirft `AR;K` seit v1.33
|
||||
# per Default (`InpShowConsensus=false` → `continue`), der Bot
|
||||
# rechnete dafür aber im ~5-s-Export-Takt ein KOMPLETTES zweites
|
||||
# `_verdict()` — Arbeit für eine Linie, die niemand zeichnet, plus
|
||||
# das Risiko, dass dieses zweite Verdict vom Verdict im Snapshot
|
||||
# abweicht. Der Schalter spiegelt jetzt den Indikator-Default;
|
||||
# wer `InpShowConsensus=true` setzt, setzt hier `export_consensus
|
||||
# _arrow=true` dazu.
|
||||
if self._export_consensus_arrow:
|
||||
_vd = self._verdict(self.data.snapshot(), self.wave.signal(),
|
||||
self.wave.snapshot(), None,
|
||||
self.agent.snapshot(), self.elliott.snapshot(),
|
||||
|
||||
@@ -72,7 +72,6 @@ def build_trade_open_message(
|
||||
entry_ts: int | None = None,
|
||||
) -> str:
|
||||
"""Formatiert die Telegram-Nachricht für einen Trade-Einstieg."""
|
||||
import datetime
|
||||
dir_str = "LONG" if (direction or "").upper() in ("BUY", "LONG") else "SHORT"
|
||||
emoji = "\U0001f7e2" if dir_str == "LONG" else "\U0001f534"
|
||||
setup_str = _SETUP_LABELS.get(setup or "", setup or "—")
|
||||
@@ -101,7 +100,6 @@ def build_trade_close_message(
|
||||
symbol: str | None = None,
|
||||
) -> str:
|
||||
"""Formatiert die Telegram-Nachricht für einen Trade-Abschluss."""
|
||||
import datetime
|
||||
pnl_net = pnl + (commission or 0.0)
|
||||
win = pnl_net >= 0
|
||||
sign = "+" if win else ""
|
||||
|
||||
@@ -334,6 +334,13 @@ async def toggle_autosqueeze(x_auth_token: str | None = Header(default=None)):
|
||||
_require_auth(x_auth_token)
|
||||
eng = app.state.engine
|
||||
state = eng.set_auto_squeeze(not eng._auto_squeeze)
|
||||
# ⚠ Herkunft mitloggen (Fix 2026-08-02, Review-Durchgang 1): `set_auto_squeeze`
|
||||
# loggt nur „Auto-Squeeze-Entry AN/AUS" ohne Quelle — beim Nachvollziehen eines
|
||||
# Zustandswechsels war dadurch nicht feststellbar, ob der Dashboard-Button oder
|
||||
# Code dahinterstand (real: BRK stand auf AUS und es kostete mehrere Schritte,
|
||||
# das als Nutzeraktion zu belegen). `/api/autosignal` macht das längst richtig —
|
||||
# bei einem Schalter für autonome ECHTGELD-Einstiege gehört die Herkunft ins Log.
|
||||
get_logger("server").info(f"[WEB] Auto-Squeeze-Entry {'AN' if state else 'AUS'}")
|
||||
return {"ok": True, "enabled": state}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user