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
+89 -2
View File
@@ -5626,8 +5626,16 @@ Folge der S/R-Widerlegung: der Hebel zeigt auf die GRÖSSE. Pfad-Simulation übe
**1.278 geschlossene Trades (08.05.13.08.)**, `pnl/lots` als größenunabhängiges **1.278 geschlossene Trades (08.05.13.08.)**, `pnl/lots` als größenunabhängiges
Ergebnis, Lots aus der jeweils simulierten Equity abgeleitet. Ergebnis, Lots aus der jeweils simulierten Equity abgeleitet.
⚠ Zwei dokumentierte Zahlen stimmen nicht mehr und wurden **aus den Daten neu ⚠ Zwei dokumentierte Zahlen stimmen nicht mehr und wurden **aus den Daten neu
geschätzt**: EUR je $ und Lot = **86,90** (notiert 87,6) und Margin je Lot geschätzt**: EUR je $ und Lot = **86,90** (notiert 87,6).
**~405 €** (notiert 670 — bei 1.279 € Balance und ~3 Lots). ⚠⚠ **KORREKTUR MEINER EIGENEN KORREKTUR (2026-08-17, vom Konstanten-Wächter
gefunden):** hier stand „Margin je Lot **~405 €** (notiert 670)". **Die 405 waren
falsch.** Ich hatte sie aus `Balance × 95 % / Lots` abgeleitet — das unterstellt,
jede Position sei auf volle 95 % gesized. Der **exakte** Wert kommt vom Broker
(`position.margin / position.lots`) und beträgt **~713 €/Lot**; die ursprünglich
notierten 670 lagen also deutlich näher als meine „Korrektur".
**Lehre: eine abgeleitete Zahl ist keine gemessene.** Wo der Broker die Größe
selbst ausrechnet, gehört sie von dort geholt — `tools/check_konstanten.py`
macht das jetzt.
**(1) DER IST-PFAD IST DIE EIGENTLICHE BOTSCHAFT:** **(1) DER IST-PFAD IST DIE EIGENTLICHE BOTSCHAFT:**
| | | | | |
@@ -5975,6 +5983,85 @@ Funding trägt nicht); und der einzige je gemessene grosse Hebel war die
zusammen). Eine neue Datenquelle müsste diese Latte schlagen, nicht nur „auch zusammen). Eine neue Datenquelle müsste diese Latte schlagen, nicht nur „auch
Information enthalten". Information enthalten".
### ✅✅ SIEBEN VERBESSERUNGEN UMGESETZT (User-Auftrag 2026-08-17)
**1 · CIRCUIT-BREAKER SCHARF** — `[trading] daily_loss_limit_pct = 0 → 8`
(Backup `…bak-2026-08-17-breaker`). Live verifiziert: `enabled=True`,
`limit_eur` **89,44 €**. ⚠ Steht **nicht** in `runtime_state.json` — die ini
greift direkt (die Falle, durch die `auto_signal` am 31.07. unbemerkt lief).
Begründung: der gemessene IST-Pfad hat **MaxDD 82,8 %** und einen schlechtesten
Einzeltrade von **381 €**; der Breaker kostet an normalen Tagen nichts und
greift nur in der Serie. ⚠ Ehrlich: 70 % des gemessenen Nutzens stammten aus
EINEM Tag — das ist das Wesen einer Versicherung, aber die +739 € sind eine
Realisierung, keine Erwartung. **Zurück: `= 0`.**
**2 · B5-DEADLOCK RECHNERISCH AUFGELÖST — die Regel ist ENTSCHIEDEN.**
Stand 11/20: ØGewinn 13,62 gegen ØVerlust 94,15 = **Verhältnis 0,14** (Latte
1,0), **PF 0,39** (Latte 1,0). Nachgerechnet, was die 9 Rest-Trades leisten
müssten:
| Rest-Szenario | nötiger Ø-Gewinn | ר bisher |
|---|---|---|
| 9 Gewinner / 0 Verlierer | **165,72 €** | **12,2×** |
| 7 / 2 | 186,17 € | 13,7× |
| 5 / 4 | 222,99 € | 16,4× |
⚠ Der **grösste** Squeeze-Gewinn des Zeitraums war **77,34 €** — selbst der
beste Fall verlangt **2,1× den Rekord**, neunmal in Folge. **Die Latte ist nicht
mehr erreichbar.** Auf 20 zu warten hätte den Zustand nie entschieden, zumal der
Trader seit 11.08. aus ist und der Zähler damit einfror (der Deadlock).
`auto_squeeze` bleibt **FALSE**. ⚠ Eine Wiederaufnahme wäre eine **NEUE**
Entscheidung mit **NEUER, vorab fixierter** Latte — nicht die Fortsetzung dieser.
**3 · `core/kontrolle.py` — Kontroll-Fixture.** Referenzwerte (Squeeze
Touchfill +0,124/+0,292 · Close-Bruch +0,456/+0,611 · Markt 0,158/0,006) plus
`pruefe_squeeze()`. Anlass: zwei Messungen am 13.08. konnten die Referenz nicht
reproduzieren, wodurch ihre Absolutwerte unbrauchbar waren. ⚠ Es bricht **nicht
ab** — ein Backtest darf abweichen, er muss es nur WISSEN und sagen; ein harter
Abbruch würde legitime Varianten unmöglich machen, eine stille Abweichung
erzeugt genau die Fehlschlüsse.
**4 · `core/live_config.py` — EINE Quelle der Live-Parameter.** Liest ini +
`runtime_state.json` in derselben Vorrangfolge wie der Engine-Start. ⚠ Anlass
ist die **dritte** Instanz derselben Fehlerklasse: `ATRMIN_STD`=0,12 statt live
0,06 (13.08.), `set_entry_room(0.6)` vergessen (05.08.), nachgebauter
Level-Cluster (07.08.). **Ein aufgerufenes Gate ist nicht dasselbe wie ein
aktives.** Aufruf: `python -m core.live_config`.
**5 · `stichprobe.paarweise()` / `zeig_paarweise()`.** Anlass: beim
`trail_start`-Sweep (14.08.) sahen sequentiell vier Werte beidhälftig besser aus
— gepaart kippte in H2 das Vorzeichen, weil ein anderer Exit ANDERE Trades in
den EINEN Slot lässt (Selektions- statt Exit-Effekt). ⚠ Der gepaarte Test ist
nicht nur richtiger, sondern **trennschärfer**: er misst die Streuung des
UNTERSCHIEDS, nicht die der Trades. Selbsttest: identisch → KI [0,0000 … 0,0000],
+0,30 → KI ohne Null, Rauschen → KI mit Null.
**6 · `tools/check_konstanten.py` — Wächter für veraltete Zahlen.** Leitet vier
tragende Grössen aus den Daten ab und hält sie gegen CLAUDE.md.
⚠⚠ **Er hat beim ersten Lauf zwei Befunde geliefert — beide waren FEHLER IN
MEINER EIGENEN ABLEITUNG**, also genau die Fehlalarme, gegen die Punkt 7 gebaut
ist: „Margin je Lot" war aus `Balance × 95 % / Lots` geschätzt (unterstellt volle
Auslastung), „Elliott-Stimmanteil" zählte über ALLE Zeilen statt über das
dokumentierte Fenster vor der Entmachtung. Beide korrigiert.
**Danach fand er einen ECHTEN veralteten Wert — meinen:** die am 13.08. von
mir notierten „~405 €/Lot" sind falsch; der exakte Broker-Wert
(`position.margin/lots`) ist **712,84 €/Lot**, die ursprünglich notierten 670
lagen also näher als meine „Korrektur". **Lehre: eine abgeleitete Zahl ist keine
gemessene.** Stand jetzt: **0 Befunde** über alle vier Grössen.
**7 · Puls-Wächter bedingungs-bewusst.** Er meldete `pbreak_predictions` als
„63,5 h still" — die Tabelle schreibt aber nur bei **frischem Level-Touch auf
der Ziel-Seite einer OFFENEN Position**. `PULS` trägt jetzt je Zeile ihre
**Vorbedingung**; bedingte Tabellen erscheinen als **ⓘ Hinweis** statt als
Ausfall. ⚠ **0 Zeilen bleibt IMMER ein Befund** — „hat nie geschrieben" lässt
sich nicht wegerklären (das war der `rec_outcomes`-Fall). Grund: ein Wächter mit
Fehlalarmen wird ignoriert, und mit ihm die eine echte Meldung — genau das ist
dem Divergenz-Wächter passiert (6 Tage Dauerwarnung).
✅ Pipeline grün (85 Tests), Deployment über `tools/deploy.py --feld
circuit_breaker` verifiziert, genau eine Instanz je Port.
**Nicht angefasst:** das Sizing (95 % Margin) — Ihre mehrfach bestätigte
Entscheidung; und die 44 Backtests mit eigener Exit-Kopie (Migrations-Regel:
ihre publizierten Zahlen würden sich verschieben).
### ✅ S/R-AUTO-CLOSE WIEDER AN (User-Entscheidung 2026-08-11) ### ✅ S/R-AUTO-CLOSE WIEDER AN (User-Entscheidung 2026-08-11)
Frage war: „sollten wir Trades bei einem S/R-Bounce automatisch schliessen?" Frage war: „sollten wir Trades bei einem S/R-Bounce automatisch schliessen?"
+31 -10
View File
@@ -436,15 +436,27 @@ class HistoryLogger:
# BEIDE Nutzer (Reminder-Popup + Tagesreport) brauchen ihn. Zwei Kopien # BEIDE Nutzer (Reminder-Popup + Tagesreport) brauchen ihn. Zwei Kopien
# waeren genau die Divergenz, die im Projekt dreimal zugeschlagen hat. # waeren genau die Divergenz, die im Projekt dreimal zugeschlagen hat.
# Richtungsregel: Skripte duerfen `core` importieren, nicht umgekehrt. # Richtungsregel: Skripte duerfen `core` importieren, nicht umgekehrt.
# ⚠⚠ BEDINGUNGS-KENNTNIS (2026-08-17). Der Puls hat `pbreak_predictions` als
# „63,5 h still" gemeldet — dabei schreibt die Tabelle NUR bei frischem
# Level-Touch auf der ZIEL-Seite einer OFFENEN Position. Ohne offene
# Position kann sie legitim tagelang schweigen. Ein Waechter, der
# Fehlalarme produziert, wird ignoriert — und mit ihm die eine echte
# Meldung; genau das ist dem Divergenz-Waechter passiert (6 Tage
# Dauerwarnung, 02.08.). Deshalb traegt jede Zeile jetzt ihre VORBEDINGUNG.
# `bedingung=None` heisst „schreibt unbedingt, Stille ist immer ein Fehler".
PULS = ( PULS = (
# (Tabelle, Zeitspalte, erwartete Kadenz in Minuten, wozu) # (Tabelle, Zeitspalte, Kadenz in Minuten, wozu, Vorbedingung)
("recommendations", "timestamp", 1, "Empfehlungs-Log"), ("recommendations", "timestamp", 1, "Empfehlungs-Log", None),
("candles_m1", "time", 1, "M1-Kerzen-Sammlung"), ("candles_m1", "time", 1, "M1-Kerzen-Sammlung", None),
("verdict_votes", "ts", 1, "Modul-Stimmen"), ("verdict_votes", "ts", 1, "Modul-Stimmen", None),
("m15_states", "ts", 60, "M15-Karte Selbstmessung"), ("m15_states", "ts", 60, "M15-Karte Selbstmessung", None),
("cone_checks", "ts", 60, "Kegel-Kalibrierung (ACI)"), ("cone_checks", "ts", 60, "Kegel-Kalibrierung (ACI)", None),
("pbreak_predictions", "ts", 60, "P(break)-Prognose-Tracking"), ("pbreak_predictions", "ts", 60, "P(break)-Prognose-Tracking",
("rec_outcomes", "ts", 180, "Selbst-Kalibrierung Empfehlung"), "braucht eine OFFENE Position und einen frischen Level-Touch auf der "
"Ziel-Seite — ohne offene Position ist Stille normal"),
("rec_outcomes", "ts", 180, "Selbst-Kalibrierung Empfehlung",
"braucht einen Richtungs-WECHSEL der Empfehlung — bei ~93 % WARTEN "
"kann das lange dauern"),
) )
def telemetrie_puls(self) -> list[str]: def telemetrie_puls(self) -> list[str]:
@@ -458,20 +470,29 @@ class HistoryLogger:
try: try:
jetzt = time.time() jetzt = time.time()
with self._lock, self._connect() as conn: with self._lock, self._connect() as conn:
for tab, sp, kadenz, wozu in self.PULS: for tab, sp, kadenz, wozu, bedingung in self.PULS:
try: try:
n = conn.execute(f"SELECT COUNT(*) FROM {tab}").fetchone()[0] n = conn.execute(f"SELECT COUNT(*) FROM {tab}").fetchone()[0]
letzt = conn.execute(f"SELECT MAX({sp}) FROM {tab}").fetchone()[0] letzt = conn.execute(f"SELECT MAX({sp}) FROM {tab}").fetchone()[0]
except Exception: except Exception:
continue # Tabelle gibt es (noch) nicht continue # Tabelle gibt es (noch) nicht
if not n: if not n:
# ⚠ 0 Zeilen ist IMMER ein Befund — auch mit Vorbedingung:
# „hat nie geschrieben" laesst sich nicht wegerklaeren.
out.append(f"{tab}: 0 Zeilen — Logger hat NIE geschrieben ({wozu})") out.append(f"{tab}: 0 Zeilen — Logger hat NIE geschrieben ({wozu})")
continue continue
# ⚠ `candles_m1.time` ist ROHE Brokerzeit (UTC+3) — ohne den # ⚠ `candles_m1.time` ist ROHE Brokerzeit (UTC+3) — ohne den
# Abzug meldet der Puls dort dauerhaft 3 h Rueckstand. # Abzug meldet der Puls dort dauerhaft 3 h Rueckstand.
ts = letzt - 10800 if tab == "candles_m1" else letzt ts = letzt - 10800 if tab == "candles_m1" else letzt
alter_min = (jetzt - ts) / 60.0 alter_min = (jetzt - ts) / 60.0
if alter_min > kadenz * 20: if alter_min <= kadenz * 20:
continue
if bedingung:
# Bedingte Tabellen: als HINWEIS melden, nicht als Ausfall —
# sonst wird der Waechter zum Rauschen (s. Kommentar an PULS).
out.append(f"{tab}: still seit {alter_min/60:.1f} h — "
f"{bedingung} ({wozu})")
else:
out.append(f"{tab}: letzte Zeile vor {alter_min/60:.1f} h " out.append(f"{tab}: letzte Zeile vor {alter_min/60:.1f} h "
f"(erwartet ~alle {kadenz} min) — {wozu}") f"(erwartet ~alle {kadenz} min) — {wozu}")
except Exception: except Exception:
+95
View File
@@ -0,0 +1,95 @@
"""KONTROLL-FIXTURE: ein Backtest muss die Referenz reproduzieren, BEVOR er berichtet.
⚠⚠ WARUM ES DAS GIBT (2026-08-13). Zwei an einem Tag gebaute Messungen konnten
den dokumentierten Squeeze-Referenzwert (+0,124/+0,292, `backtest_squeeze_
touchfill.py`) NICHT reproduzieren — sie kamen auf 0,151/0,079 bei einem
Drittel der Trades. Beide Ergebnisse mussten deshalb auf den **relativen**
Vergleich beschränkt werden; die absolute Aussage war nicht führbar. Und es
blieb offen, ob der dokumentierte Wert oder die neue Pipeline danebenliegt.
`backtest_verworfene_v2.py` macht es vorbildlich von Hand („✅ Die Kontrolle
reproduziert … Pipeline in Ordnung"). Hier ist dasselbe als geteilte Funktion,
damit es niemand vergisst.
from core.kontrolle import pruefe_squeeze
ok = pruefe_squeeze(h1_oer, h2_oer, n_h1, n_h2) # meldet und gibt bool
⚠ Es wird NICHT abgebrochen. Ein Backtest darf von der Referenz abweichen — er
muss es nur WISSEN und sagen. Ein harter Abbruch würde legitime Varianten
(andere Fill-Annahme, anderer Exit) unmöglich machen; eine stille Abweichung
dagegen erzeugt genau die Fehlschlüsse, die hier vermieden werden sollen.
⚠ Die Referenzen sind MESSWERTE, keine Wunschzahlen. Wer sie ändert, muss die
Quelle nennen — sonst wandert die Latte mit dem Ergebnis.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Referenz:
name: str
quelle: str
h1: float
h2: float
n_h1: int
n_h2: int
toleranz: float = 0.06 # ØR-Abstand, ab dem gewarnt wird
# ── Die dokumentierten Referenzwerte des Projekts ────────────────────────────
SQUEEZE_TOUCHFILL = Referenz(
name="Squeeze · ruhende Order am Level, Fill bei Beruehrung",
quelle="backtest_squeeze_touchfill.py (2026-08-06)",
h1=+0.124, h2=+0.292, n_h1=1092, n_h2=1807)
SQUEEZE_CLOSEBRUCH = Referenz(
name="Squeeze · Fill bei Close-Bruch (die OPTIMISTISCHE Annahme)",
quelle="backtest_squeeze_entry.py (2026-08-05)",
h1=+0.456, h2=+0.611, n_h1=801, n_h2=1280)
SQUEEZE_MARKT = Referenz(
name="Squeeze · Markt-Einstieg am Bar-Close",
quelle="backtest_squeeze_entry.py (2026-08-05)",
h1=-0.158, h2=-0.006, n_h1=801, n_h2=1280)
def pruefe(ref: Referenz, h1: float, h2: float,
n_h1: int | None = None, n_h2: int | None = None,
still: bool = False) -> bool:
"""Reproduziert der Lauf die Referenz? Meldet den Befund, gibt bool zurueck."""
d1, d2 = abs(h1 - ref.h1), abs(h2 - ref.h2)
ok = d1 <= ref.toleranz and d2 <= ref.toleranz
if still:
return ok
print(f"\n ── KONTROLLE: {ref.name}")
print(f" Referenz {ref.quelle}")
print(f" erwartet H1 {ref.h1:+.3f} · H2 {ref.h2:+.3f}"
f" (n {ref.n_h1}/{ref.n_h2})")
n_txt = "" if n_h1 is None else f" (n {n_h1}/{n_h2})"
print(f" gemessen H1 {h1:+.3f} · H2 {h2:+.3f}{n_txt}")
print(f" Abstand H1 {d1:.3f} · H2 {d2:.3f} (Toleranz {ref.toleranz})")
if ok:
print(" ✅ reproduziert — die Pipeline ist in Ordnung, ABSOLUTWERTE "
"sind belastbar.")
else:
print(" ❌ WEICHT AB. Dann gilt:")
print(" · der RELATIVE Vergleich innerhalb dieses Laufs bleibt gueltig,")
print(" solange alle Varianten dieselbe Entry-Liste benutzen;")
print(" · die ABSOLUTWERTE sind es NICHT — sie duerfen nicht als")
print(" Ertragsaussage berichtet werden.")
if n_h1 is not None and (n_h1 < ref.n_h1 * 0.6 or n_h1 > ref.n_h1 * 1.6):
print(f" · ⚠ auch die Trade-ZAHL weicht stark ab ({n_h1} gegen "
f"{ref.n_h1}) — meist ein anderer Filter (ATR-Floor, Cooldown,")
print(" Slot-Behandlung), nicht der Markt.")
return ok
def pruefe_squeeze(h1: float, h2: float, n_h1=None, n_h2=None,
variante: str = "touchfill") -> bool:
"""Bequemer Aufruf fuer die haeufigste Kontrolle."""
ref = {"touchfill": SQUEEZE_TOUCHFILL,
"closebruch": SQUEEZE_CLOSEBRUCH,
"markt": SQUEEZE_MARKT}[variante]
return pruefe(ref, h1, h2, n_h1, n_h2)
+155
View File
@@ -0,0 +1,155 @@
"""DIE Live-Parameter — EINE Quelle für Backtests.
⚠⚠ WARUM ES DAS GIBT. Ein Backtest, der einen geteilten Kern aufruft, aber
dessen **Modul-Defaults** übernimmt, misst eine andere Welt als die laufende.
Das ist im Projekt DREIMAL passiert:
· 2026-08-05 `set_entry_room(0.6)` vergessen → `_room_gate` war ein No-op;
die Population schrumpfte von 967 auf 295, als es korrigiert war.
· 2026-08-07 der Level-Cluster war NACHGEBAUT und wich ab (4,5 % der
Zeitpunkte hatten einen anderen Widerstand).
· 2026-08-13 `squeeze_scan.ATRMIN_STD` = 0,12 genommen statt der **live**
gültigen 0,06 → H1 127 gegen H2 483 Einstiege, also eine völlig
andere Population. Aufgefallen nur an der Schieflage.
**Ein aufgerufenes Gate ist nicht dasselbe wie ein aktives Gate.**
Deshalb: wer einen Backtest schreibt, holt die Werte HIER — nicht aus dem
Gedächtnis, nicht aus dem Modul-Default, nicht aus CLAUDE.md.
from core.live_config import LIVE_CFG
scan(..., atr_min=LIVE_CFG.squeeze_atr_min, box_n=LIVE_CFG.squeeze_box_n)
⚠ Gelesen wird die ECHTE `oil_widget_config.ini` PLUS `runtime_state.json` — und
zwar in derselben Vorrangfolge wie beim Engine-Start (runtime gewinnt). Genau
diese Reihenfolge hat am 31.07. dafür gesorgt, dass `auto_signal` unbemerkt
lief, obwohl die ini `false` sagte.
⚠ Diese Datei trifft KEINE Entscheidungen und ändert nichts. Sie liest nur.
"""
from __future__ import annotations
import configparser
import json
import os
from dataclasses import dataclass, field
_HIER = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_INI = os.path.join(_HIER, "oil_widget_config.ini")
_RT = os.path.join(_HIER, "runtime_state.json")
def _lies() -> tuple[dict, dict]:
c = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
try:
c.read(_INI, encoding="utf-8")
t = dict(c["trading"]) if c.has_section("trading") else {}
except Exception:
t = {}
try:
with open(_RT, encoding="utf-8") as f:
rt = json.load(f)
except Exception:
rt = {}
return t, rt
def _f(d: dict, k: str, std: float) -> float:
try:
return float(d.get(k, std))
except (TypeError, ValueError):
return std
def _i(d: dict, k: str, std: int) -> int:
try:
return int(float(d.get(k, std)))
except (TypeError, ValueError):
return std
def _b(d: dict, rt: dict, k: str, std: bool) -> bool:
"""⚠ runtime_state gewinnt — genau wie beim Engine-Start."""
if k in rt:
return bool(rt[k])
v = str(d.get(k, str(std))).strip().lower()
return v in ("true", "1", "yes", "ja")
@dataclass(frozen=True)
class LiveConfig:
# ── Squeeze (der einzige validierte Auto-Entry) ───────────────────────
squeeze_box_n: int = 10 # `wave_rec._SQ_N` — NICHT squeeze_scan.BOX_N_STD (12)
squeeze_k: float = 0.1
squeeze_mult: float = 2.5
squeeze_atr_min: float = 0.06 # ⚠ Modul-Default ist 0,12 — live gilt 0,06
# ── Signal-Gates ─────────────────────────────────────────────────────
breakout_k: float = 0.3
entry_room_atr: float = 0.6
min_conf: int = 55
stretch_max: float = 3.5
reversal_enabled: bool = False
# ── Exit ─────────────────────────────────────────────────────────────
sr_close_pbreak: float = 0.35
adverse_15min_atr: float = 0.0
auto_flip_close: bool = False
# ── Ausfuehrung / Risiko ─────────────────────────────────────────────
squeeze_pending_entry: bool = True
squeeze_max_chase_atr: float = 0.20
margin_buffer_pct: float = 95.0
risk_pct: float = 0.0
daily_loss_limit_pct: float = 0.0
# ── Schalter (runtime_state hat Vorrang) ─────────────────────────────
auto_squeeze: bool = False
auto_sr_close: bool = True
squeeze_night_hours: tuple = field(default_factory=tuple)
# ── Abgeleitete Groessen, die Backtests brauchen ─────────────────────
# Der Live-SL kommt aus ATR_M15; ueber 121 echte Trades (13.08.) betraegt
# er im Median 3,4xATR_M5. Backtests rechnen in ATR_M5 — deshalb hier.
sl_atr_m5_aequivalent: float = 3.4
eur_pro_dollar_lot: float = 86.90 # aus 1.051 Trades geschaetzt (13.08.)
@classmethod
def laden(cls) -> "LiveConfig":
t, rt = _lies()
std = cls()
nh = str(t.get("auto_squeeze_night_hours", "")).strip()
stunden = tuple(int(x) for x in nh.split(",") if x.strip().isdigit()) if nh else ()
return cls(
squeeze_atr_min=std.squeeze_atr_min, # lebt im Code, nicht in der ini
squeeze_box_n=std.squeeze_box_n,
squeeze_k=std.squeeze_k,
squeeze_mult=std.squeeze_mult,
breakout_k=_f(t, "breakout_k", std.breakout_k),
entry_room_atr=_f(t, "entry_room_atr", std.entry_room_atr),
min_conf=std.min_conf,
stretch_max=std.stretch_max,
reversal_enabled=_b(t, {}, "reversal_enabled", std.reversal_enabled),
sr_close_pbreak=_f(t, "sr_close_pbreak", std.sr_close_pbreak),
adverse_15min_atr=_f(t, "adverse_15min_atr", std.adverse_15min_atr),
auto_flip_close=_b(t, {}, "auto_flip_close", std.auto_flip_close),
squeeze_pending_entry=_b(t, {}, "squeeze_pending_entry",
std.squeeze_pending_entry),
squeeze_max_chase_atr=_f(t, "squeeze_max_chase_atr",
std.squeeze_max_chase_atr),
margin_buffer_pct=_f(t, "margin_buffer_pct", std.margin_buffer_pct),
risk_pct=_f(t, "risk_pct", std.risk_pct),
daily_loss_limit_pct=_f(t, "daily_loss_limit_pct",
std.daily_loss_limit_pct),
auto_squeeze=_b(t, rt, "auto_squeeze", std.auto_squeeze),
auto_sr_close=_b(t, rt, "auto_sr_close", std.auto_sr_close),
squeeze_night_hours=stunden,
sl_atr_m5_aequivalent=std.sl_atr_m5_aequivalent,
eur_pro_dollar_lot=std.eur_pro_dollar_lot,
)
def zeig(self) -> str:
z = [" LIVE-KONFIGURATION (aus ini + runtime_state, Vorrang wie beim Start)"]
for k, v in sorted(self.__dict__.items()):
z.append(f" {k:<26} {v}")
return "\n".join(z)
LIVE_CFG = LiveConfig.laden()
if __name__ == "__main__":
print(LIVE_CFG.zeig())
+46
View File
@@ -123,3 +123,49 @@ def schluessel_level_stunde(level_feld="level", ts_feld="ts"):
ts = _feld(z, ts_feld) ts = _feld(z, ts_feld)
return (round(float(lvl or 0), 2), int(ts or 0) // 3600) return (round(float(lvl or 0), 2), int(ts or 0) // 3600)
return f return f
def paarweise(basis: dict, variante: dict, wiederholungen: int = 3000,
seed: int = 7):
"""GEPAARTER Vergleich zweier Exit-/Regel-Varianten auf DENSELBEN Faellen.
⚠⚠ WARUM DAS NOETIG IST (2026-08-14, real passiert). Beim `trail_start`-Sweep
sahen SEQUENTIELL vier Werte in beiden Haelften besser aus (Δ bis +0,101).
Gepaart kippte in H2 das Vorzeichen. Ursache: eine sequentielle Sim mit EINEM
Positions-Slot nimmt bei anderem Exit ANDERE Trades — der scheinbare Gewinn
war ein **Selektionseffekt**, kein Exit-Effekt. Dieselbe Falle steckte am
31.07. in `backtest_exit_combo.py` („verschiedene Trade-Folgen sind nicht
vergleichbar").
⚠ Und der gepaarte Test ist nicht nur richtiger, sondern **trennschaerfer**:
die Einzel-Intervalle messen die Streuung der TRADES, das gepaarte die des
UNTERSCHIEDS — und die ist um ein Vielfaches kleiner.
`basis` / `variante`: {schluessel: ergebnis}. Der Schluessel identifiziert den
Fall (z. B. der Einstiegs-Bar-Index). Verglichen wird nur die Schnittmenge.
Rueckgabe: (mittlere Differenz, lo, hi, n) — bei n < 20 (None, None, None, n).
"""
import random as _r
gemeinsam = [k for k in basis if k in variante]
dif = [variante[k] - basis[k] for k in gemeinsam]
n = len(dif)
if n < 20:
return None, None, None, n
m = sum(dif) / n
r = _r.Random(seed)
bs = sorted(sum(dif[r.randrange(n)] for _ in range(n)) / n
for _ in range(wiederholungen))
return m, bs[int(.025 * wiederholungen)], bs[int(.975 * wiederholungen)], n
def zeig_paarweise(name: str, basis: dict, variante: dict) -> bool:
"""Druckt den gepaarten Vergleich und sagt, ob er die Null ausschliesst."""
m, lo, hi, n = paarweise(basis, variante)
if m is None:
print(f" {name:<28} zu wenige gemeinsame Faelle (n={n})")
return False
ok = lo > 0 or hi < 0
print(f" {name:<28} Δ je Fall {m:>+.4f} KI [{lo:+.4f}{hi:+.4f}] "
f"n={n} {'✅ ohne 0' if ok else '⚠ enthaelt 0'}")
return ok
+15 -7
View File
@@ -213,13 +213,21 @@ CHECKS = [
"Berührungen = faktisch pauschaler S/R-Close), 15-Min-Regel aus, Trailing " "Berührungen = faktisch pauschaler S/R-Close), 15-Min-Regel aus, Trailing "
"einheitlich 1,0 statt 1,53,0 je TF. Deshalb NICHT sofort zurückgebaut, " "einheitlich 1,0 statt 1,53,0 je TF. Deshalb NICHT sofort zurückgebaut, "
"sondern ein sauberes Fenster mit harter Latte.", "sondern ein sauberes Fenster mit harter Latte.",
"have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'SQUEEZE%' " "have": lambda: 20, # ⚠ ENTSCHIEDEN — s. `cmd`
"AND pnl IS NOT NULL AND entry_time >= " "need": 20, "unit": "Squeeze-Trades seit 01.08. (ENTSCHIEDEN bei 11)",
"strftime('%s','2026-08-01')"), "cmd": "✅ ERLEDIGT 2026-08-17 — die Regel ist RECHNERISCH entschieden, "
"need": 20, "unit": "Squeeze-Trades seit 01.08.", "nicht vertagt. Stand war 11/20: ØGewinn 13,62 gegen ØVerlust 94,15 "
"cmd": "ABBRUCHREGEL: Verhältnis (ØGewinn/ØVerlust) < 1,0 ODER PF < 1 " "= Verhältnis 0,14 (Latte 1,0), PF 0,39 (Latte 1,0), Σ 173,46 €. "
"→ `auto_squeeze=false` (B5-Rückbau, kein Ego). Sonst weiterlaufen " "Nachgerechnet, was die 9 Rest-Trades leisten müssten: selbst im "
"lassen. Zahlen: /api/squeeze_monitor bzw. Statistik-Tab.", "BESTEN Fall (9 von 9 Gewinnern, kein weiterer Verlust) bräuchte es "
"Ø 165,72 € je Trade — das 12-fache des bisherigen Durchschnitts und "
"2,1× den bisherigen REKORD (77,34 €). Mit Verlusten darunter steigt "
"die Anforderung auf 175223 €. ⚠ Die Latte ist damit nicht mehr "
"erreichbar; auf 20 zu warten hätte den Zustand nie entschieden — "
"und der Trader stand seit 11.08. ohnehin AUS, wodurch der Zähler "
"eingefroren war (Deadlock). `auto_squeeze` bleibt FALSE. "
"Wiederaufnahme wäre eine NEUE Entscheidung mit NEUER, vorab "
"fixierter Latte — nicht die Fortsetzung dieser.",
}, },
{ {
"key": "squeeze_entry_gap", "key": "squeeze_entry_gap",
+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())