diff --git a/CLAUDE.md b/CLAUDE.md index b0ce24d..8795860 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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?" diff --git a/core/history.py b/core/history.py index d1b49a3..24c5f29 100644 --- a/core/history.py +++ b/core/history.py @@ -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: diff --git a/core/kontrolle.py b/core/kontrolle.py new file mode 100644 index 0000000..e89dc3b --- /dev/null +++ b/core/kontrolle.py @@ -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) diff --git a/core/live_config.py b/core/live_config.py new file mode 100644 index 0000000..cd139bf --- /dev/null +++ b/core/live_config.py @@ -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()) diff --git a/core/stichprobe.py b/core/stichprobe.py index d51468f..a7a17b1 100644 --- a/core/stichprobe.py +++ b/core/stichprobe.py @@ -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 diff --git a/measurement_reminder.py b/measurement_reminder.py index d05dcac..33b5814 100644 --- a/measurement_reminder.py +++ b/measurement_reminder.py @@ -213,13 +213,21 @@ CHECKS = [ "Berührungen = faktisch pauschaler S/R-Close), 15-Min-Regel aus, Trailing " "einheitlich 1,0 statt 1,5–3,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 175–223 €. ⚠ 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", diff --git a/tools/check_konstanten.py b/tools/check_konstanten.py new file mode 100644 index 0000000..44755a2 --- /dev/null +++ b/tools/check_konstanten.py @@ -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())