diff --git a/CLAUDE.md b/CLAUDE.md index 3942b24..ba8a743 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9257,6 +9257,91 @@ liegen — meine Invariante war zu streng; (2) zwei inhaltsgleiche dicts sind mich selbst: auf einer frischen Maschine soll der Bot mit möglichst kleiner Oberfläche wieder laufen. Ergänzt wurde ausschliesslich `requirements-dev.txt`. +## ⚠⚠ SECHSTER DURCHGANG: SLOTS · JAVASCRIPT · BIBLIOTHEKEN (2026-08-20) + +User-Auftrag. Diesmal wurde aus **jeder** der drei Achsen eine **Pipeline-Stufe** +gemacht — die vorigen Durchgänge waren Handarbeit und liefen nur, wenn jemand +daran dachte. + +### JavaScript — die Render-Probe deckte 2 von 15 Funktionen ab + +⚠⚠ **`tools/render_check.mjs` schnitt per Regex genau ZWEI Funktionen heraus** +(`renderM15`, `renderBrk`). **`renderKontext` und 13 weitere waren nie +abgedeckt** — und genau in `renderKontext` ist mir am selben Tag der +`d`-statt-`snap`-Beinahe-Fehler unterlaufen. Die Probe hätte ihn **durchgelassen**. +✅ **Neu geschrieben**: die **komplette** `app.js` läuft in einem VM-Kontext gegen +ein Minimal-DOM, und **jede** gefundene `render*`-Funktion wird aufgerufen — +zweimal: mit dem Fixture **und mit einem leeren Snapshot** (der Kaltstart, in dem +jedes Feld fehlt; dort schlagen fehlende Null-Prüfungen zu, nicht bei vollen +Daten). **13 Funktionen laufen durch.** +✅ **Mutationsprobe mit genau dem heutigen Fehler:** +``` +❌ renderKontext(Fixture): ReferenceError: d is not defined +❌ render(Fixture): ReferenceError: d is not defined +❌ renderKontext(leerer Snapshot): … +``` +⚠ **`node --check` meldet dabei NICHTS** — die Datei ist syntaktisch einwandfrei. +Das ist der blinde Fleck, den diese Stufe schliesst. +⚠ Kein npm, kein jsdom: das DOM ist eine Attrappe von 20 Zeilen. Prüf- und +Laufumgebung bleiben derselbe Parser (V8). + +### Zwei neue Pipeline-Stufen + +**F) ELEMENT-IDs** — greift die JS auf IDs zu, die es in der HTML nicht gibt, und +gibt es doppelte? Stand: **133 IDs, keine verwaist, keine doppelt.** +⚠ Kommentare werden vorher entfernt: `$("neu")` und `$("x")` stehen als +**Beispiele** im Kopfkommentar von `app.js` und wären sonst zwei Dauer-Fehlalarme +— und eine Prüfung mit Dauer-Treffern wird ignoriert (die Lehre vom Linter). +**G) ABHÄNGIGKEITEN** — ist jeder Import aus `server.py`/`core/` in +`requirements.txt`? Stand: **39 Dateien gelesen, alle 9 externen Importe +aufgeführt**, keine Versions-Drift zwischen gepinnt und installiert. +✅ Beide **mutationsgeprüft**: ID umbenannt → Befund; `feedparser` aus der Liste +gestrichen → Befund. + +⚠⚠ **UND DIE PIPELINE HAT SICH SELBST ERWISCHT.** Mein erster Entwurf von Stufe G +importierte `ast` nicht. Der stille `except Exception: continue` liess dadurch +**jede** Datei durchfallen — und die Stufe meldete *„alle 0 externen +Laufzeit-Importe sind aufgeführt."* Ein grüner Haken, hinter dem **nichts** +geprüft wurde: die **fünfte** Wiederholung dieser Fehlerklasse im Projekt. +Gefangen hat es **`ruff F821` in derselben Pipeline**. +✅ Behoben doppelt: `ast` importiert **und** eine Plausibilitätsprüfung +(„weniger Dateien gelesen als vorhanden, oder 0 externe Importe → Befund"). Eine +Stufe, die nichts gelesen hat, darf nicht „sauber" melden. + +### Slots — keine neuen Defekte, drei Punkte präzisiert + +✅ **Kein geteilter Modul-Zustand**: die modulweiten Container in `trailing.py` +und `trader.py` (`_TF_LABELS`, `_PHASE_RANK`, `_RETCODE_MSG`) sind reine +Nachschlagetabellen, kein `global`-Statement. Die beiden Instanzen sind +tatsächlich unabhängig — das war die Voraussetzung des ganzen Umbaus. +⚠ **`set_sltp` bleibt Slot-1-only, verhält sich aber SICHER**: liegt nur eine +BRK-Position, gibt `modify_sltp` einen Fehlertext zurück (`if not self.ticket`). +Es fasst also **nicht** die falsche Position an — die SL/TP-Felder der +Trade-Leiste sind lediglich auf Slot 1 beschränkt. Bekannte Grenze, kein Fehler. +⚠ **`trail_brk.enabled` wird NICHT persistiert** (nur `brk_ticket`). +`_rebind_brk` schaltet es nach einem Neustart bedingungslos ein. Das ist die +sichere Richtung — aber wer es bewusst abgeschaltet hat, bekommt es nach einem +Neustart zurück. Bewusst so gelassen: eine BRK-Position ohne Trailing ist +gemessen die schlechtere Variante. + +### Bibliotheken + +✅ **Nichts fehlt, nichts driftet.** `pandas` und `scikit-learn` stehen in +`requirements-dev.txt` und werden in **0** Dateien importiert (so dokumentiert); +`numba`, `joblib`, `tqdm`, `pandas_ta`, `rich`, `httpx` sind als Beifang anderer +Pakete installiert und ebenfalls ungenutzt. Kein Handlungsbedarf — sie stehen +nicht im Laufzeit-Pfad. +⚠ **Struktureller Rest, jetzt benannt**: `core/engine.py` importiert +`weekly_review` und `measurement_reminder` — zwei Skripte der obersten Ebene. +Dieselbe Klasse wie der frühere `backtest_breakout_squeeze`-Import, der am 07.08. +nach `core/squeeze_scan.py` gezogen wurde. **Hier entschärft**, weil beide +**lazy** und in `try` importiert werden (1×/Tag bzw. 1×/Woche) und ein +Fehlschlag sichtbar gemeldet wird. In Stufe G als projekteigen ausgenommen — +mit Begründung im Code, damit es nicht als Versehen gelesen wird. + +**Pipeline jetzt: A · A2 · A3 · B · B2 · C · C2 · D · E · F · G** — alle grün, +97 Tests, deployt und verifiziert. + ## ⚠⚠⚠ P(break) IST LIVE **INVERTIERT** — und es steuert echtes Geld (2026-08-19) Die fällige Messung ist entscheidbar geworden: **n=530 entkoppelt** gegen die diff --git a/tools/check_nfalle.py b/tools/check_nfalle.py index 5639295..bec3f91 100644 --- a/tools/check_nfalle.py +++ b/tools/check_nfalle.py @@ -16,6 +16,7 @@ Geprueft wird: naechste besteht nur aus einem Quote — die Signatur der beiden heutigen Faelle. """ +import ast import pathlib import py_compile import re @@ -448,9 +449,120 @@ def _stufe_slots(): befunde.append("SLOT Werkzeugfehler") +# ⚠⚠ Zwei Pruefungen, die die Gesamtdurchsicht vom 2026-08-20 als Luecke +# gezeigt hat — beide waren bis dahin HANDARBEIT und liefen nur, wenn jemand +# daran dachte. +def _stufe_ids(): + """F) Greift die JS auf Element-IDs zu, die es in der HTML NICHT gibt? + + ⚠ Ein fehlendes Element wird seit v=150 vom Auffang-`$` abgefangen (die + Regel war bei 0 von 95 Zugriffen befolgt) — aber `$streng` gibt weiter + `None`, und genau dort haengt der ORDER-Pfad. Ausserdem ist eine verwaiste + ID fast immer der Rest eines halb entfernten Features. + ⚠ Kommentare werden ENTFERNT, bevor gesucht wird: `$("neu")` und `$("x")` + stehen als BEISPIELE im Kopfkommentar von `app.js` und waeren sonst zwei + Dauer-Fehlalarme — und eine Pruefung mit Dauer-Treffern wird ignoriert. + """ + print("") + print("=" * 88) + print(" F) ELEMENT-IDs — JS gegen HTML") + print("=" * 88) + h = WURZEL / "web" / "index.html" + j = WURZEL / "web" / "app.js" + if not (h.exists() and j.exists()): + print(" web/ fehlt — Stufe uebersprungen.") + return + html = h.read_text(encoding="utf-8") + js = j.read_text(encoding="utf-8") + js = re.sub(r"/\*.*?\*/", "", js, flags=re.S) + js = re.sub(r"(?m)^\s*//.*$", "", js) + html_ids = re.findall(r'id="([^"]+)"', html) + doppelt = sorted({i for i in html_ids if html_ids.count(i) > 1}) + fehlt = sorted(set(re.findall(r'\$(?:streng)?\("([^"]+)"\)', js)) - set(html_ids)) + for i in fehlt: + befunde.append(f"ID verwaist: $(\"{i}\") ohne Element in index.html") + print(f" FEHLER verwaiste ID: {i}") + for i in doppelt: + befunde.append(f"ID doppelt in index.html: {i}") + print(f" FEHLER doppelte ID: {i}") + if not fehlt and not doppelt: + print(f" {len(set(html_ids))} IDs, keine verwaist, keine doppelt.") + + +def _stufe_requirements(): + """G) Ist jeder LAUFZEIT-Import in `requirements.txt` aufgefuehrt? + + ⚠ Bis 2026-08-07 gab es gar keine Liste; nach einem Maschinenausfall waere + die Rekonstruktion Raten gewesen. Die Liste zu HABEN reicht aber nicht — sie + veraltet beim ersten neuen Import still. Geprueft wird nur `server.py` + + `core/` (der Live-Pfad); Backtests duerfen dev-Pakete nutzen. + """ + print("") + print("=" * 88) + print(" G) ABHAENGIGKEITEN — Laufzeit-Importe gegen requirements.txt") + print("=" * 88) + req_p = WURZEL / "requirements.txt" + if not req_p.exists(): + print(" requirements.txt fehlt — Stufe uebersprungen.") + return + req = req_p.read_text(encoding="utf-8").lower() + alias = {"bs4": "beautifulsoup4", "deep_translator": "deep-translator", + "yaml": "pyyaml", "dotenv": "python-dotenv"} + stdlib = set(sys.stdlib_module_names) + # ⚠ Projekt-EIGENE Module, keine pip-Pakete. `weekly_review` und + # `measurement_reminder` sind Skripte auf oberster Ebene — `engine.py` + # importiert sie beide LAZY (im Funktionskoerper, in `try`), also nur + # 1x/Tag bzw. 1x/Woche. Das ist dieselbe Klasse wie der frueher direkte + # `backtest_breakout_squeeze`-Import, der am 07.08. nach + # `core/squeeze_scan.py` gezogen wurde — hier aber entschaerft, weil der + # Live-Pfad sie nicht beim Start braucht und ein Fehlschlag gemeldet wird. + eigen = {"core", "tools", "server", "weekly_review", "measurement_reminder"} + dateien_ = [WURZEL / "server.py"] + sorted((WURZEL / "core").rglob("*.py")) + mods = set() + gelesen = 0 + for p_ in dateien_: + try: + b = ast.parse(p_.read_text(encoding="utf-8")) + gelesen += 1 + except Exception as e: + # ⚠⚠ NICHT still ueberspringen. Der erste Entwurf tat genau das — + # `ast` war nicht importiert, JEDE Datei fiel in diesen Zweig, und + # die Stufe meldete "alle 0 externen Laufzeit-Importe sind + # aufgefuehrt". Ein gruener Haken, hinter dem NICHTS geprueft + # wurde: die vierte Wiederholung dieser Fehlerklasse im Projekt. + # Gefangen hat es `ruff F821` in derselben Pipeline. + befunde.append(f"REQ {p_.name} nicht lesbar: {e}") + print(f" FEHLER {p_.name} nicht analysierbar: {e}") + continue + for n in ast.walk(b): + if isinstance(n, ast.Import): + mods |= {a.name.split(".")[0] for a in n.names} + elif isinstance(n, ast.ImportFrom) and n.level == 0 and n.module: + mods.add(n.module.split(".")[0]) + fehlend = [m for m in sorted(mods) + if m and m not in stdlib and m not in eigen + and alias.get(m, m).lower() not in req] + for m in fehlend: + befunde.append(f"REQ Laufzeit-Import `{m}` fehlt in requirements.txt") + print(f" FEHLER `{m}` wird im Live-Pfad importiert, steht aber nicht in " + f"requirements.txt") + extern = [m for m in mods if m and m not in stdlib and m not in eigen] + if gelesen < len(dateien_) or not extern: + # Eine Stufe, die nichts gelesen hat, darf nicht "sauber" melden. + befunde.append(f"REQ nur {gelesen}/{len(dateien_)} Dateien gelesen, " + f"{len(extern)} externe Importe gefunden — unplausibel") + print(f" FEHLER nur {gelesen}/{len(dateien_)} Dateien gelesen, " + f"{len(extern)} externe Importe — die Stufe hat nichts geprueft") + elif not fehlend: + print(f" {gelesen} Dateien gelesen, alle {len(extern)} externen " + f"Laufzeit-Importe sind aufgefuehrt.") + + _stufe_typen() _stufe_render() _stufe_slots() +_stufe_ids() +_stufe_requirements() print("") print("=" * 88) diff --git a/tools/render_check.mjs b/tools/render_check.mjs index 197f1b8..2655d52 100644 --- a/tools/render_check.mjs +++ b/tools/render_check.mjs @@ -1,34 +1,118 @@ -// Render-Probe: laesst renderM15 + renderBrk gegen ein Minimal-DOM laufen. +// Render-Probe: laesst ALLE render*-Funktionen gegen ein Minimal-DOM laufen. // -// ⚠⚠ WARUM ES DAS GIBT (2026-08-19): `node --check` prueft nur die SYNTAX. Ein -// ReferenceError (falsche Variable im falschen Scope) ist syntaktisch -// einwandfrei und faellt erst im Browser auf - dort bricht er aber den -// GESAMTEN Render ab: alles ab der Fehlerstelle bleibt auf "—", und die -// danach gerufenen Renderer laufen gar nicht mehr. Real passiert: die -// RSI-Zeile griff in `renderM15(s)` auf `d.market` zu; das Dashboard zeigte -// sechs leere Zeilen und eine komplett leere Breakout-Karte. -// Python hat dafuer `ruff F821` - fuer JS gab es nichts Vergleichbares. +// ⚠⚠ WOZU. Ein `ReferenceError` mitten im Render bricht den GESAMTEN Durchlauf +// ab: das Dashboard friert auf Altwerten ein, Server und Snapshot sind dabei +// korrekt. `node --check` sieht das NICHT — die Datei ist syntaktisch einwandfrei. +// Real passiert: +// · 01.08. — ein direkter Zugriff auf ein noch nicht ausgeliefertes Element +// · 20.08. — die RSI-Zeile griff in `renderM15(s)` auf `d.market` zu +// · 20.08. — der Trailing-Block landete in `renderKontext(snap)` und las `d.…` +// Der dritte Fall waere durch die ALTE Fassung dieser Probe geschluepft: sie +// schnitt per Regex genau ZWEI Funktionen heraus (`renderM15`, `renderBrk`). +// `renderKontext` und 13 weitere waren nie abgedeckt. +// +// ⚠ Jetzt wird die KOMPLETTE `app.js` in einem VM-Kontext geladen und jede +// gefundene `render*`-Funktion aufgerufen — mit dem Fixture-Snapshot UND mit +// einem leeren Objekt (der Kaltstart-Fall, in dem jedes Feld fehlt). +// +// ⚠ Kein npm, kein jsdom: das DOM ist eine Attrappe von 20 Zeilen. Damit bleibt +// die Pruefumgebung dieselbe Laufzeit wie die Zielumgebung (V8) — genau die +// Divergenz, die beim `.ps1`-BOM und beim pre-commit-Framework zugeschlagen hat. // // Aufruf: node tools/render_check.mjs -// Snapshot holen: curl -s http://127.0.0.1:8000/api/snapshot > snap.json -import fs from "fs"; -const snap = JSON.parse(fs.readFileSync(process.argv[2], "utf-8")); +import fs from "node:fs"; +import vm from "node:vm"; + +const pfad = process.argv[2] || "tests/fixtures/snapshot.json"; +const snap = JSON.parse(fs.readFileSync(pfad, "utf-8")); const src = fs.readFileSync("web/app.js", "utf-8"); -const store = {}; -const mk = (id) => ({ id, set textContent(v){ store[id]=v; }, get textContent(){return store[id]||"";}, - set className(v){}, get className(){return "";}, classList:{add(){},remove(){},toggle(){}}, - style:{}, value:"", title:"", addEventListener(){}, blur(){}, focus(){} }); -const cache = {}; -global.document = { getElementById: (id) => (cache[id] ||= mk(id)), - querySelectorAll: () => [], querySelector: () => null, addEventListener(){}, - get activeElement(){return null;}, createElement: () => mk("_") }; -const m = src.match(/function renderM15\([\s\S]*?\n\}/); -const b = src.match(/function renderBrk\([\s\S]*?\n\}/); -const helfer = 'const _F=document.createElement("span");' + - 'const $=(id)=>document.getElementById(id)||_F; const $streng=(id)=>document.getElementById(id);'; -const fn = new Function("snapshot", helfer + "\n" + m[0] + "\n" + (b?b[0]:"function renderBrk(){}") + - "\nrenderM15(snapshot.m15_setup, snapshot); renderBrk(snapshot);"); -try { fn(snap); console.log(" ✅ renderM15 + renderBrk laufen OHNE Exception durch"); } -catch (e) { console.log(" ❌ EXCEPTION: " + e.message); process.exit(1); } -for (const k of ["m15-rsi","m15-m5","m15-motor","brk-pos","brk-state"]) - console.log(" " + k.padEnd(12) + " = " + (store[k] || "(leer)")); + +function el() { + const o = { + textContent: "", innerHTML: "", className: "", title: "", value: "", + disabled: false, checked: false, style: {}, dataset: {}, + classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } }, + appendChild() {}, removeChild() {}, setAttribute() {}, removeAttribute() {}, + addEventListener() {}, focus() {}, blur() {}, remove() {}, + getBoundingClientRect() { return { top: 0, left: 0, width: 0, height: 0 }; }, + querySelector() { return el(); }, querySelectorAll() { return []; }, + children: [], parentNode: null, firstChild: null, + }; + return o; +} + +const ctx = { + console, + document: { + getElementById: () => el(), querySelector: () => el(), + querySelectorAll: () => [], createElement: () => el(), + addEventListener() {}, body: el(), documentElement: el(), + activeElement: null, hidden: false, visibilityState: "visible", + }, + window: { + addEventListener() {}, setTimeout() {}, clearTimeout() {}, + location: { href: "", protocol: "http:", host: "x" }, + localStorage: { getItem: () => null, setItem() {}, removeItem() {} }, + matchMedia: () => ({ matches: false, addEventListener() {} }), + }, + navigator: { userAgent: "node" }, + location: { href: "", protocol: "http:", host: "x" }, + localStorage: { getItem: () => null, setItem() {}, removeItem() {} }, + setInterval: () => 0, clearInterval() {}, setTimeout: () => 0, clearTimeout() {}, + requestAnimationFrame: () => 0, + fetch: () => new Promise(() => {}), + WebSocket: function () { return { close() {}, send() {} }; }, + AudioContext: function () { + return { createOscillator: () => ({ connect() {}, start() {}, stop() {}, + frequency: { setValueAtTime() {} }, type: "" }), + createGain: () => ({ connect() {}, gain: { setValueAtTime() {}, + exponentialRampToValueAtTime() {} } }), + currentTime: 0, destination: {}, resume: () => Promise.resolve(), + state: "running" }; + }, + Notification: function () {}, + alert() {}, confirm: () => true, prompt: () => null, +}; +ctx.window.document = ctx.document; +ctx.globalThis = ctx; +ctx.self = ctx; +vm.createContext(ctx); + +try { + vm.runInContext(src, ctx, { filename: "app.js" }); +} catch (e) { + console.log(` ❌ app.js liess sich nicht laden: ${e.constructor.name}: ${e.message}`); + process.exit(1); +} + +// Jede render*-Funktion einsammeln (die im Kontext wirklich existiert). +const namen = Object.keys(ctx).filter( + (k) => /^render/.test(k) && typeof ctx[k] === "function"); +if (namen.length < 5) { + console.log(` ❌ nur ${namen.length} render*-Funktionen gefunden — Probe greift nicht`); + process.exit(1); +} + +// ⚠ Zwei Durchlaeufe. Der LEERE Snapshot ist der wichtigere: er stellt den +// Kaltstart nach, in dem noch kein Feld befuellt ist — dort schlagen fehlende +// Null-Pruefungen zu, nicht bei vollen Daten. +const faelle = [["Fixture", snap], ["leerer Snapshot", {}]]; +let fehler = 0; +for (const [lbl, daten] of faelle) { + for (const n of namen) { + try { + // `renderM15` nimmt (setup, snapshot), alle uebrigen nur (snapshot). + if (n === "renderM15") ctx[n](daten.m15_setup || {}, daten); + else ctx[n](daten); + } catch (e) { + fehler++; + console.log(` ❌ ${n}(${lbl}): ${e.constructor.name}: ${e.message}`); + } + } +} +if (fehler) { + console.log(` ${fehler} Fehler in ${namen.length} render*-Funktionen`); + process.exit(1); +} +console.log(` ✅ ${namen.length} render*-Funktionen laufen durch ` + + `(Fixture UND leerer Snapshot)`);