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
Ergebnis, Lots aus der jeweils simulierten Equity abgeleitet.
⚠ 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
**~405 €** (notiert 670 — bei 1.279 € Balance und ~3 Lots).
geschätzt**: EUR je $ und Lot = **86,90** (notiert 87,6).
⚠⚠ **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:**
| | |
@@ -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
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)
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
# waeren genau die Divergenz, die im Projekt dreimal zugeschlagen hat.
# 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 = (
# (Tabelle, Zeitspalte, erwartete Kadenz in Minuten, wozu)
("recommendations", "timestamp", 1, "Empfehlungs-Log"),
("candles_m1", "time", 1, "M1-Kerzen-Sammlung"),
("verdict_votes", "ts", 1, "Modul-Stimmen"),
("m15_states", "ts", 60, "M15-Karte Selbstmessung"),
("cone_checks", "ts", 60, "Kegel-Kalibrierung (ACI)"),
("pbreak_predictions", "ts", 60, "P(break)-Prognose-Tracking"),
("rec_outcomes", "ts", 180, "Selbst-Kalibrierung Empfehlung"),
# (Tabelle, Zeitspalte, Kadenz in Minuten, wozu, Vorbedingung)
("recommendations", "timestamp", 1, "Empfehlungs-Log", None),
("candles_m1", "time", 1, "M1-Kerzen-Sammlung", None),
("verdict_votes", "ts", 1, "Modul-Stimmen", None),
("m15_states", "ts", 60, "M15-Karte Selbstmessung", None),
("cone_checks", "ts", 60, "Kegel-Kalibrierung (ACI)", None),
("pbreak_predictions", "ts", 60, "P(break)-Prognose-Tracking",
"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]:
@@ -458,20 +470,29 @@ class HistoryLogger:
try:
jetzt = time.time()
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:
n = conn.execute(f"SELECT COUNT(*) FROM {tab}").fetchone()[0]
letzt = conn.execute(f"SELECT MAX({sp}) FROM {tab}").fetchone()[0]
except Exception:
continue # Tabelle gibt es (noch) nicht
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})")
continue
# ⚠ `candles_m1.time` ist ROHE Brokerzeit (UTC+3) — ohne den
# Abzug meldet der Puls dort dauerhaft 3 h Rueckstand.
ts = letzt - 10800 if tab == "candles_m1" else letzt
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 "
f"(erwartet ~alle {kadenz} min) — {wozu}")
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)
return (round(float(lvl or 0), 2), int(ts or 0) // 3600)
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 "
"einheitlich 1,0 statt 1,53,0 je TF. Deshalb NICHT sofort zurückgebaut, "
"sondern ein sauberes Fenster mit harter Latte.",
"have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'SQUEEZE%' "
"AND pnl IS NOT NULL AND entry_time >= "
"strftime('%s','2026-08-01')"),
"need": 20, "unit": "Squeeze-Trades seit 01.08.",
"cmd": "ABBRUCHREGEL: Verhältnis (ØGewinn/ØVerlust) < 1,0 ODER PF < 1 "
"→ `auto_squeeze=false` (B5-Rückbau, kein Ego). Sonst weiterlaufen "
"lassen. Zahlen: /api/squeeze_monitor bzw. Statistik-Tab.",
"have": lambda: 20, # ⚠ ENTSCHIEDEN — s. `cmd`
"need": 20, "unit": "Squeeze-Trades seit 01.08. (ENTSCHIEDEN bei 11)",
"cmd": "✅ ERLEDIGT 2026-08-17 — die Regel ist RECHNERISCH entschieden, "
"nicht vertagt. Stand war 11/20: ØGewinn 13,62 gegen ØVerlust 94,15 "
"= Verhältnis 0,14 (Latte 1,0), PF 0,39 (Latte 1,0), Σ 173,46 €. "
"Nachgerechnet, was die 9 Rest-Trades leisten müssten: selbst im "
"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",
+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())