Sieben Verbesserungen umgesetzt: Breaker scharf, B5 entschieden, 5 Werkzeuge

1) CIRCUIT-BREAKER SCHARF -- daily_loss_limit_pct 0 -> 8. Live verifiziert:
   enabled=True, limit_eur 89,44. Steht nicht in runtime_state.json, die ini
   greift direkt. Begruendung: MaxDD 82,8 % auf dem gemessenen IST-Pfad,
   schlechtester Einzeltrade -381 EUR.

2) B5-DEADLOCK RECHNERISCH AUFGELOEST -- die Regel ist ENTSCHIEDEN, nicht
   vertagt. Stand 11/20: Verhaeltnis 0,14 (Latte 1,0), PF 0,39 (Latte 1,0).
   Was die 9 Rest-Trades leisten muessten:
     9/0 Gewinner -> 165,72 EUR je Trade = 12,2x der bisherige Schnitt
     7/2          -> 186,17 EUR
     5/4          -> 222,99 EUR
   Der groesste Squeeze-Gewinn des Zeitraums war 77,34 EUR -- selbst der beste
   Fall verlangt 2,1x den Rekord, neunmal in Folge. Latte nicht mehr erreichbar.
   auto_squeeze bleibt FALSE. Wiederaufnahme waere eine NEUE Entscheidung mit
   NEUER, vorab fixierter Latte.

3) core/kontrolle.py -- Kontroll-Fixture mit den Referenzwerten. Bricht bewusst
   NICHT ab: ein Backtest darf abweichen, er muss es nur wissen und sagen.

4) core/live_config.py -- EINE Quelle der Live-Parameter (ini + runtime_state in
   der Vorrangfolge des Engine-Starts). Dritte Instanz derselben Fehlerklasse:
   ATRMIN_STD 0,12 statt 0,06, fehlendes set_entry_room, nachgebauter Cluster.

5) stichprobe.paarweise()/zeig_paarweise() -- gepaarter Vergleich. Anlass:
   trail_start sah sequentiell besser aus, gepaart kippte das Vorzeichen
   (Selektions- statt Exit-Effekt). Selbsttest in drei Richtungen bestanden.

6) tools/check_konstanten.py -- Waechter fuer veraltete Zahlen.
   Erster Lauf: zwei Befunde, BEIDE Fehler in meiner eigenen Ableitung (Margin
   aus Balance geschaetzt statt vom Broker gelesen; Elliott ohne
   Gueltigkeitsfenster gezaehlt). Beide korrigiert.
   Danach ein ECHTER Fund: meine am 13.08. notierten "~405 EUR/Lot" sind falsch,
   der exakte Broker-Wert ist 712,84 -- die urspruenglich notierten 670 lagen
   naeher als meine Korrektur. Lehre: eine abgeleitete Zahl ist keine gemessene.
   Stand jetzt 0 Befunde.

7) Puls-Waechter bedingungs-bewusst: PULS traegt je Zeile die Vorbedingung,
   bedingte Tabellen erscheinen als Hinweis statt als Ausfall. 0 Zeilen bleibt
   IMMER ein Befund (der rec_outcomes-Fall). Grund: ein Waechter mit
   Fehlalarmen wird ignoriert -- so ist der Divergenz-Waechter verstummt.

Pipeline gruen (85 Tests), Deployment ueber deploy.py --feld circuit_breaker
verifiziert, genau eine Instanz je Port.
Nicht angefasst: Sizing (User-Entscheidung) und die 44 Backtests mit eigener
Exit-Kopie (Migrations-Regel).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-17 00:57:38 +02:00
co-authored by Claude Opus 5
parent 9dbc5d0452
commit 792355e8f9
7 changed files with 641 additions and 19 deletions
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Waechter fuer VERALTETE KONSTANTEN — leitet die tragenden Zahlen aus den
Daten neu ab und meldet Abweichungen von der Dokumentation.
⚠⚠ ANLASS (2026-08-17). An EINEM Tag sind vier dokumentierte Zahlen als veraltet
aufgefallen — jede war Entscheidungsgrundlage gewesen:
· „Margin ~670 EUR/Lot" → real ~405 (bei 1.279 EUR Konto und ~3 Lots)
· „87,6 EUR je $ und Lot" → real 86,90 (aus 1.051 Trades geschaetzt)
· „X-API Basic 200 $/Monat" → seit Februar 2026 pay-per-use, ~45 $/Monat
· „Elliott hat Gewicht nur in 1,3 % der Zeilen" → real 99,9 %
CLAUDE.md hat 7.149 Zeilen. Dass dort Zahlen altern, ist unvermeidlich — dass
Entscheidungen darauf gestuetzt werden, ist das Problem.
⚠ Der Waechter meldet NICHT jede kleine Abweichung. Eine Pruefung mit
Dauer-Treffern wird ignoriert, und mit ihr die eine echte Meldung (die Lehre aus
`pyflakes`: kosmetische Hinweise unterdruecken). Gemeldet wird erst ab einer
Toleranz, die je Zahl begruendet ist.
⚠ Er AENDERT nichts. Er sagt nur, wo Doku und Daten auseinanderlaufen.
Aufruf: python tools/check_konstanten.py
"""
from __future__ import annotations
import os
import re
import sqlite3
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
HIER = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DB = os.path.join(HIER, "oil_widget_history.db")
DOK = os.path.join(HIER, "CLAUDE.md")
def _db():
c = sqlite3.connect(DB); c.row_factory = sqlite3.Row
return c
# ── Die einzelnen Ableitungen ────────────────────────────────────────────────
def eur_pro_dollar_lot():
"""EUR je 1,00 $ Kursbewegung und Lot — aus den echten Trades."""
db = _db()
f = []
for r in db.execute("""SELECT pnl,lots,entry_price,exit_price,direction FROM trades
WHERE exit_time IS NOT NULL AND pnl IS NOT NULL AND lots>0
AND entry_price>0 AND exit_price>0"""):
d = 1 if r["direction"] == "BUY" else -1
mv = (r["exit_price"] - r["entry_price"]) * d
if abs(mv) > 0.05:
f.append(r["pnl"] / (mv * r["lots"]))
if len(f) < 50:
return None
f.sort()
return f[len(f) // 2], len(f)
def sl_breite_atr_m5():
"""Wie breit ist der Initial-SL WIRKLICH, in ATR_M5?"""
db = _db()
v = [abs(r["entry_price"] - r["sl_at_entry"]) / r["ctx_atr_m5"]
for r in db.execute("""SELECT entry_price,sl_at_entry,ctx_atr_m5 FROM trades
WHERE sl_at_entry IS NOT NULL AND entry_price>0
AND ctx_atr_m5 IS NOT NULL AND ctx_atr_m5>0
AND exit_time >= strftime('%s','now')-30*86400""")]
if len(v) < 30:
return None
v.sort()
return v[len(v) // 2], len(v)
def margin_pro_lot():
"""Gebundene Margin je Lot — EXAKT aus der offenen Position.
⚠⚠ EIGENER FEHLER, korrigiert (2026-08-17): der erste Entwurf rechnete
`Balance × margin_pct / Median-Lots`. Das unterstellt, jede Position sei auf
exakt 95 % der Balance gesized — und lieferte prompt 564,92 gegen die
dokumentierten 405, obwohl die Broker-Eigenschaft (Hebel) sich gar nicht
geaendert hat. Ein Waechter, der seine eigene Herleitung falsch macht,
erzeugt genau die Fehlalarme, gegen die er gebaut ist.
➜ Jetzt der exakte Wert: `position.margin / position.lots` aus dem Snapshot
(der Broker rechnet ihn selbst). Ohne offene Position: nicht pruefbar —
das ist ehrlicher als eine Schaetzung.
"""
try:
import json
import urllib.request
d = json.load(urllib.request.urlopen(
"http://127.0.0.1:8000/api/snapshot", timeout=5))
p = d.get("position") or {}
m, l = p.get("margin"), p.get("lots")
except Exception:
return None
if not m or not l or l <= 0:
return None
return m / l, 1
_ELLIOTT_ENTMACHTET = "2026-08-06" # seither ist das Gewicht per Definition 0
def elliott_stimmanteil():
"""Anteil der Verdict-Zeilen MIT Elliott-Gewicht — nur im dokumentierten
Fenster.
⚠⚠ EIGENER FEHLER, korrigiert: der erste Entwurf zaehlte ueber ALLE Zeilen
und meldete 53,07 % gegen die dokumentierten 99,9 %. Beide Zahlen stimmen —
nur fuer verschiedene Zeitraeume: das Modul wurde am 06.08. entmachtet
(Gewicht 1,0 → 0), seither ist `elliott_w > 0` per Konstruktion falsch.
Der Kennzahl fehlte schlicht ihr Gueltigkeitsfenster.
"""
db = _db()
try:
a, b = db.execute(
"SELECT COUNT(*), SUM(elliott_w > 0) FROM verdict_votes "
"WHERE ts < strftime('%s',?)", (_ELLIOTT_ENTMACHTET,)).fetchone()
except sqlite3.OperationalError:
return None
if not a:
return None
return 100.0 * (b or 0) / a, a
# ── Was in der Doku steht ────────────────────────────────────────────────────
def dok_zahl(muster: str):
try:
s = open(DOK, encoding="utf-8").read()
except OSError:
return None
m = re.search(muster, s)
if not m:
return None
return float(m.group(1).replace(",", "."))
PRUEFUNGEN = [
# (Name, Ableitung, Doku-Muster, Toleranz in %, Begruendung der Toleranz)
("EUR je $ und Lot", eur_pro_dollar_lot,
r"EUR-Faktor (\d+[,.]\d+) €/\$/Lot", 3.0,
"3 % — Swap und Waehrungskurs schwanken leicht"),
("Initial-SL in ATR_M5", sl_breite_atr_m5,
r"durchgehend (\d+[,.]\d+)×ATR_M5 \(= der Live-Stop", 10.0,
"10 % — der ATR selbst schwankt, der Median ist traege"),
("Margin je Lot (EUR)", margin_pro_lot,
r"beträgt \*\*~(\d+) €/Lot\*\*", 15.0,
"15 % — haengt an Hebel und Kurs"),
("Elliott-Stimmanteil (%)", elliott_stimmanteil,
r"`elliott_w > 0` in [\d.]+ von [\d.]+ Zeilen \((\d+[,.]\d+) %\)", 5.0,
"5 Pp — reine Auszaehlung, sollte stabil sein"),
]
def main() -> int:
print("\n ── KONSTANTEN-WAECHTER ────────────────────────────────────────")
print(" Leitet die tragenden Zahlen aus den DATEN ab und haelt sie")
print(" gegen CLAUDE.md. Aendert nichts.\n")
befunde = 0
for name, fn, muster, tol, warum in PRUEFUNGEN:
try:
erg = fn()
except Exception as e:
print(f" {name:<26} ⚠ nicht pruefbar ({type(e).__name__})")
continue
if erg is None:
print(f" {name:<26} — zu wenige Daten / Server aus")
continue
wert, n = erg
dok = dok_zahl(muster)
if dok is None:
print(f" {name:<26} Daten {wert:>9.2f} (n={n}) "
f"⚠ in CLAUDE.md nicht gefunden")
continue
abw = abs(wert - dok) / dok * 100 if dok else 0
flag = "" if abw <= tol else "❌ VERALTET"
if abw > tol:
befunde += 1
print(f" {name:<26} Daten {wert:>9.2f} · Doku {dok:>9.2f} · "
f"Abw {abw:>5.1f} % {flag}")
if abw > tol:
print(f" → Toleranz {tol} % ({warum})")
print(f"\n ── SCHALTER: ini gegen CLAUDE.md ──────────────────────────────")
try:
from core.live_config import LIVE_CFG
s = open(DOK, encoding="utf-8").read()
paare = [("auto_emergency_margin_pct", r"auto_emergency_margin_pct\s*=\s*(\d+)")]
import configparser
c = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
c.read(os.path.join(HIER, "oil_widget_config.ini"), encoding="utf-8")
for key, mus in paare:
ist = c["trading"].get(key)
treffer = re.findall(mus, s)
soll = treffer[-1] if treffer else None
gleich = soll is None or str(ist) == str(soll)
if not gleich:
befunde += 1
print(f" {key:<26} ini={ist} Doku={soll} "
f"{'' if gleich else '❌ WIDERSPRUCH'}")
print(f" (Live-Schalter siehe `python -m core.live_config`)")
except Exception as e:
print(f" ⚠ Schalter-Vergleich nicht moeglich: {e}")
print(f"\n ── ERGEBNIS: {befunde} Befund(e)")
return 1 if befunde else 0
if __name__ == "__main__":
raise SystemExit(main())