"""Sucht die \\n-Falle in allen Projektdateien — in BEIDE Richtungen. Die Falle: beim Schreiben ueber ein Shell-Heredoc wird aus einem gemeinten `\\n` ein ECHTER Zeilenumbruch mitten im String-Literal. In Python bricht das den Compile (zweimal heute passiert), in JS ebenso — aber nicht ueberall faellt es sofort auf. Geprueft wird: A) Python: kompiliert jede Datei. Ein zerrissenes Literal faellt hier auf. B) JS: Zeilen mit ungerader Zahl UNMASKIERTER Anfuehrungszeichen ausserhalb von Kommentaren = angefangenes, nicht geschlossenes String-Literal. C) Umgekehrter Fall: literales `\\n` als TEXT dort, wo es niemand lesen will (HTML-Attribute, Markdown-Fliesstext) — das entsteht, wenn ein `\\\\n` zu viel escaped wurde. D) Verdaechtige Zeilenpaare: eine Zeile endet mit oeffnendem Quote, die naechste besteht nur aus einem Quote — die Signatur der beiden heutigen Faelle. """ import ast import pathlib import py_compile import re import subprocess import sys import tempfile WURZEL = pathlib.Path(r"c:\Users\ah\Downloads\tradingrobot") SKIP = {".git", "__pycache__", ".reminders", "node_modules", ".removed_backup"} befunde = [] def dateien(*endungen): for p in WURZEL.rglob("*"): if any(s in p.parts for s in SKIP): continue if p.is_file() and p.suffix in endungen: yield p # ── A) Python ──────────────────────────────────────────────────────────── print("=" * 88) print(" A) PYTHON — kompiliert jede Datei (ein zerrissenes Literal faellt hier auf)") print("=" * 88) npy = 0 tmp = tempfile.mkdtemp() for p in dateien(".py"): npy += 1 try: py_compile.compile(str(p), cfile=f"{tmp}/x.pyc", doraise=True) except py_compile.PyCompileError as e: msg = str(e).strip().splitlines()[-1][:110] befunde.append(f"PY {p.relative_to(WURZEL)}: {msg}") print(f" FEHLER {p.relative_to(WURZEL)}") print(f" {msg}") print(f" {npy} Dateien geprueft.") # ── A2) pyflakes: was py_compile NICHT sieht ───────────────────────────── # ⚠⚠ WARUM DIESE STUFE NOETIG IST. `py_compile` prueft nur die SYNTAX. Ein # Tippfehler in einem Namen ist syntaktisch einwandfrei und fliegt erst zur # Laufzeit — und dieses Projekt hat sehr viele breite `except Exception`-Bloecke # (bewusst, als Fail-safe im Trading-Betrieb). Ein `NameError` darin ist damit # UNSICHTBAR: keine Meldung, kein Logeintrag, nur ein Zweig, der nichts tut. # Real gefunden beim ERSTEN Lauf ueberhaupt (2026-08-07): # · `core/trader.py` nutzte zweimal `log.` — den Namen gibt es dort nicht # (nur `log_trade`/`log_hist`). Einer davon seit dem Initial-Commit. # · Folge 1 (`_broker_offset_s`): der NameError lief in ein `except: pass` # und nahm die Zuweisung `self._boff = int(off)` mit → ein echter # Broker-Zeitzonen-Wechsel waere nie uebernommen worden. # · Folge 2 (`modify_sltp`): kein try drumherum → der Fehler lief bis in # `/api/sltp`; der Broker hatte SL/TP schon geaendert, der User bekam # trotzdem einen Fehler, und `trail.deactivate()` wurde uebersprungen. # GETRENNT NACH SCHWEREGRAD: eine Pruefung, die dauerhaft 100 harmlose Zeilen # ausgibt, wird ignoriert — und dann auch die eine wichtige. # ⚠ `redefinition of unused` steht BEWUSST NICHT hier drin. pyflakes meldet damit # auch das voellig legitime Muster `hook = None` gefolgt von einem bedingten # `def hook(...)` — real in `backtest_flipclose2.py` und # `backtest_adverse15_squeeze.py`, beide geprueft und korrekt. Zwei unvermeidbare # Dauer-Treffer sind genau der Weg, auf dem eine Pruefung ignoriert wird, und mit # ihr dann auch die eine echte Meldung. Wer die Klasse doch sehen will, ruft # `python -m pyflakes ` direkt auf. SCHWER = ("undefined name", "undefined local", "syntax error") print() print("=" * 88) # ── A3) TYPPRUEFUNG: falsche Aufrufe ───────────────────────────────────── # ⚠⚠ WARUM ES DAS BRAUCHT (2026-08-19): `paarweise(d)` wurde mit 1 von 2 # Pflichtargumenten gerufen. py_compile sah nichts (Syntax ok), ruff F821 auch # nicht (der Name IST definiert), und kein Test deckte den Pfad ab. Der Fehler # schlug erst zur Laufzeit zu. Genau diese Luecke schliesst `ty`. # ⚠ BEWUSST ENGES REGELSET (`ty.toml`): der volle Lauf meldet 172 Befunde, davon # 97 `unresolved-attribute` aus fehlenden MetaTrader5-Stubs. Eine Stufe mit # Dauer-Treffern wird ignoriert, und mit ihr die eine echte Meldung. Aktiv # sind nur die Aritaets-Regeln; `call-non-callable`/`not-iterable` sind AUS, # weil sie hier nur Falsch-Positive aus heterogenen dicts liefern (geprueft). # ⚠ EXIT-CODES wie bei ruff: 0 sauber · 1 Befunde · alles andere = Werkzeugfehler # und darf NIEMALS als "sauber" durchgehen (die Lehre vom 07.08.). def _stufe_typen(): print("") print("=" * 88) print(" A3) TYPPRUEFUNG — falsche Aufrufe (Aritaet)") print("=" * 88) cfg = WURZEL / "ty.toml" if not cfg.exists(): print(" ty.toml fehlt — Stufe uebersprungen.") return try: r = subprocess.run([sys.executable, "-m", "ty", "check", "--config-file", str(cfg), str(WURZEL / "core"), str(WURZEL / "server.py")], capture_output=True, text=True, timeout=300) except FileNotFoundError: print(" `ty` nicht installiert — Stufe uebersprungen " "(pip install ty). Eine frische Maschine darf hier nicht haengen.") return except Exception as e: print(f" ⚠ ty nicht ausfuehrbar ({e}) — Stufe uebersprungen.") return if r.returncode not in (0, 1): print(f" ⚠ ty meldet einen WERKZEUGFEHLER (Exit {r.returncode}) — " f"NICHT als sauber gewertet:") for z in (r.stderr or r.stdout).strip().splitlines()[:3]: print(f" {z}") befunde.append("TY Werkzeugfehler") return treffer = [z for z in (r.stdout or "").splitlines() if z.startswith("error[")] for z in treffer: befunde.append(f"TY {z.strip()}") print(f" FEHLER {z.strip()}") if not treffer: print(" keine falschen Aufrufe.") # ── B2) RENDER-PROBE: JS-Laufzeitfehler im Dashboard ───────────────────── # ⚠⚠ `node --check` prueft nur SYNTAX. Ein ReferenceError (falsche Variable im # falschen Scope) ist syntaktisch einwandfrei und bricht im Browser den # GESAMTEN Render ab — alles ab der Fehlerstelle bleibt auf "—" stehen. # Real am 19.08.: die RSI-Zeile griff in `renderM15(s)` auf `d.market` zu; # sechs leere Zeilen und eine komplett leere Breakout-Karte. # ⚠ Laeuft gegen einen GESPEICHERTEN Snapshot (`tests/fixtures/snapshot.json`), # NICHT gegen den laufenden Server — der Hook darf nicht davon abhaengen, ob # gerade ein Server laeuft. def _stufe_render(): print("") print("=" * 88) print(" B2) RENDER-PROBE — JS-Laufzeitfehler (was `node --check` nicht sieht)") print("=" * 88) werkzeug = WURZEL / "tools" / "render_check.mjs" fixture = WURZEL / "tests" / "fixtures" / "snapshot.json" if not werkzeug.exists() or not fixture.exists(): print(" Werkzeug oder Fixture fehlt — Stufe uebersprungen.") return try: r = subprocess.run(["node", str(werkzeug), str(fixture)], capture_output=True, text=True, timeout=120) except FileNotFoundError: print(" node nicht im PATH — Stufe uebersprungen.") return except Exception as e: print(f" ⚠ Render-Probe nicht ausfuehrbar ({e}) — uebersprungen.") return aus = (r.stdout or "") + (r.stderr or "") if r.returncode != 0 or "EXCEPTION" in aus: for z in aus.strip().splitlines()[:6]: print(f" {z}") befunde.append("RENDER " + aus.strip().splitlines()[0][:80]) else: print(" alle Render-Funktionen laufen ohne Exception durch.") print(" A2) LINTER — undefinierte Namen (was py_compile nicht sieht)") print("=" * 88) # ⚠ RUFF statt pyflakes (2026-08-07, Web-Recherche zum 2026er-Standard): gleiche # Befunde, deutlich schneller, ein Werkzeug statt vieler. Bewusst NUR # `ruff check` mit ENGER Regelauswahl — **kein `ruff format`**: das wuerde 161 # Dateien umformatieren, darunter alle Backtests, und damit die Migrations-Regel # verletzen (deren Zahlen sind dokumentiert). # F821 = undefined name ← die Klasse, die hier real weh tut # E999 = syntax error # F811 (redefinition) bleibt DRAUSSEN, s. Begruendung bei SCHWER oben. # pyflakes bleibt als Rueckfall, damit die Pruefung auch ohne ruff laeuft. # ⚠⚠ NUR F821 — `E999` gibt es in ruff 0.16 NICHT MEHR. Wird es trotzdem # selektiert, bricht ruff KOMPLETT ab („Rule E999 was removed") mit Exit 2. # Genau das ist beim ersten Bau passiert, und schlimmer: der Rueckgabecode # wurde nicht geprueft, die Fehlermeldung ging nach stderr — die Stufe meldete # also „keine undefinierten Namen", waehrend sie GAR NICHTS geprueft hat. Ein # absichtlich eingebauter `F821`-Fehler kam glatt durch den Hook. # Syntaxfehler deckt ohnehin Stufe A (`py_compile`) ab. # ⚠ RUFF-EXIT-CODES: 0 = sauber · 1 = Befunde · 2 = Werkzeugfehler. # 2 darf NIEMALS als „sauber" durchgehen. _lint_ok = False try: r = subprocess.run([sys.executable, "-m", "ruff", "check", "--select", "F821", "--no-cache", "--output-format", "concise", str(WURZEL)], capture_output=True, text=True, timeout=180) if r.returncode not in (0, 1): print(f" ⚠ ruff meldet einen WERKZEUGFEHLER (Exit {r.returncode}) — " f"Rueckfall auf pyflakes:") for z in (r.stderr or r.stdout).strip().splitlines()[:3]: print(f" {z}") raise RuntimeError("ruff nicht nutzbar") _lint_ok = True treffer = [z for z in r.stdout.splitlines() if z.strip() and not z.startswith(("Found", "All checks", "warning:"))] for z in treffer: kurz = z.replace(str(WURZEL) + "\\", "").replace(str(WURZEL) + "/", "") befunde.append(f"RUFF {kurz}") print(f" FEHLER {kurz}") if not treffer: print(" keine undefinierten Namen (ruff, F821/E999).") except Exception: pass if not _lint_ok: try: import io from pyflakes.api import checkPath from pyflakes.reporter import Reporter aus, err = io.StringIO(), io.StringIO() rep = Reporter(aus, err) for p in dateien(".py"): checkPath(str(p), rep) zeilen = [z for z in (aus.getvalue() + err.getvalue()).splitlines() if z.strip()] schwer = [z for z in zeilen if any(s in z for s in SCHWER)] for z in schwer: kurz = z.replace(str(WURZEL) + "\\", "").replace(str(WURZEL) + "/", "") befunde.append(f"PF {kurz}") print(f" FEHLER {kurz}") if not schwer: print(" keine undefinierten Namen (pyflakes-Rueckfall).") except ImportError: print(" ⚠ weder ruff noch pyflakes installiert — Stufe uebersprungen.") print(" Installation: python -m pip install -r requirements-dev.txt") # ── B) JavaScript ──────────────────────────────────────────────────────── def js_offene_strings(text: str): """Zeilen mit angefangenem, nicht geschlossenem ' oder " (Template-Literale duerfen mehrzeilig sein und werden uebersprungen).""" treffer = [] in_tmpl = False for nr, z in enumerate(text.splitlines(), 1): # Backticks zaehlen -> Template-Literal-Zustand fortschreiben bt = len(re.findall(r"(? 127}) if na and not bom: befunde.append(f"PS1 {p.relative_to(WURZEL)}: Nicht-ASCII ohne BOM") print(f" FEHLER {p.relative_to(WURZEL)} " f"{len(na)} Nicht-ASCII-Zeichen, KEIN BOM → bricht unter " f"powershell.exe 5.1") else: print(f" OK {p.relative_to(WURZEL)}" + (" (BOM)" if bom else " (rein ASCII)")) print() print("=" * 88) print(' D) SIGNATUR — Zeile endet offen, naechste besteht nur aus einem Quote') print("=" * 88) sig = 0 for p in list(dateien(".py")) + list(dateien(".js")): zeilen = p.read_text(encoding="utf-8", errors="replace").splitlines() for i in range(len(zeilen) - 1): if zeilen[i + 1].strip() in ('"', "'", '",', "',"): sig += 1 befunde.append(f"SIG {p.relative_to(WURZEL)}:{i+2}") print(f" VERDAECHTIG {p.relative_to(WURZEL)}:{i+2}") print(" keine gefunden." if not sig else f" {sig} Stellen.") print() print("=" * 88) # ── E) SLOT-TRENNUNG + ATTRIBUT-REIHENFOLGE ────────────────────────────── # ⚠⚠ Beide Fehlerklassen haben binnen zwei Tagen je einmal zugeschlagen, und # KEINE andere Stufe hat sie gefangen (py_compile, ruff F821, ty, pytest: # alle gruen). Der Attribut-Reihenfolge-Fehler brach den Serverstart KOMPLETT # ab — der Nutzer konnte zwei Minuten lang nicht schliessen. Gefunden hat ihn # erst `deploy.py` Schritt 2, also NACH dem Kill des laufenden Servers. # Diese Stufe findet ihn VORHER. def _stufe_slots(): print("") print("=" * 88) print(" E) SLOTS — Attribut-Reihenfolge in __init__ + Slot-Trennung") print("=" * 88) try: sys.path.insert(0, str(WURZEL / "tools")) import check_slots import importlib importlib.reload(check_slots) for z in check_slots.pruefe(): befunde.append("SLOT " + z) print(" FEHLER " + z) if not check_slots.pruefe(): print(" keine Slot-/Reihenfolge-Fehler.") except Exception as e: print(f" ⚠ Slot-Pruefung nicht ausfuehrbar ({e}) — NICHT als sauber gewertet.") 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}) # ⚠⚠ ABSICHTLICH ausgeblendete Elemente. Der JS-Block bleibt stehen — # er ist null-sicher (`$streng` + Waechter) — damit ein Zurueckholen eine # reine HTML-Aenderung ist. Ohne diese Liste meldete die Stufe drei # Dauer-Treffer, und eine Pruefung mit Dauer-Treffern wird ignoriert (die # Lehre vom Linter). ⚠ Wer hier etwas eintraegt, MUSS den Zugriff im JS # auf `$streng` + Waechter umstellen — sonst bricht der GESAMTE Render. AUSGEBLENDET = {"hl-note", "pat-note", "trail-note"} # User-Wunsch 2026-08-20 fehlt = sorted(set(re.findall(r'\$(?:streng)?\("([^"]+)"\)', js)) - set(html_ids) - AUSGEBLENDET) 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) print(" ERGEBNIS: " + ("keine \\n-Falle gefunden" if not befunde else f"{len(befunde)} Befund(e)")) print("=" * 88) sys.exit(1 if befunde else 0)