"""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 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) _stufe_typen() _stufe_render() 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)