Auf die Frage nach einem Lint-Modul: pyflakes installiert und als Stufe A2 in tools/check_nfalle.py eingebunden. py_compile prueft nur die SYNTAX; ein Tippfehler in einem NAMEN ist syntaktisch einwandfrei — und dieses Projekt hat sehr viele breite except-Exception-Bloecke, in denen ein NameError unsichtbar bleibt. Beim ERSTEN Lauf zwei echte Fehler, beide in core/trader.py, beide nutzten log. (den Namen gibt es dort nicht, nur log_trade/log_hist): · _broker_offset_s(): der NameError lief in das except-pass UND nahm die darunter stehende Zuweisung self._boff = int(off) mit -> ein echter Broker-Zeitzonen-Wechsel waere nie uebernommen worden, dauerhaft und lautlos. Ausgerechnet der Zweig, der als Deployment-Drift Fall 4 gebaut wurde (falscher Offset -> Time-Stop-Alter negativ). · modify_sltp(): steht NICHT in einem try -> der Fehler lief bis in /api/sltp. Der Broker hatte SL/TP bereits geaendert, der User bekam trotzdem eine Fehlermeldung, und set_sltp kam nie bis trail.deactivate() — das Trailing blieb an und haette die Handeingabe zurueckgezogen. Seit dem Initial-Commit drin. Beide behoben (log_trade). Zusaetzlich die Annotation 'HistoryLogger | None' ueber if TYPE_CHECKING sauber importiert (String-Annotation, kein Laufzeit-Import, kein Zirkel). Projektweit jetzt 0 undefinierte Namen. Schweregrad-Trennung ist Absicht: nur undefined name / syntax error werden gemeldet, die ~105 kosmetischen Hinweise unterdrueckt. Auch 'redefinition of unused' bleibt draussen — es trifft das legitime Muster hook = None + bedingtes def hook (in zwei Backtests geprueft, beide korrekt). Eine Pruefung mit Dauer-Treffern wird ignoriert, und mit ihr die eine echte. Backtest-Dateien nicht angefasst (Zahlen). Neustart verifiziert, Log sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
208 lines
8.8 KiB
Python
208 lines
8.8 KiB
Python
"""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 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 <datei>` direkt auf.
|
|
SCHWER = ("undefined name", "undefined local", "syntax error")
|
|
print()
|
|
print("=" * 88)
|
|
print(" A2) PYFLAKES — undefinierte Namen (was py_compile nicht sieht)")
|
|
print("=" * 88)
|
|
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.")
|
|
print(f" ({len(zeilen) - len(schwer)} kosmetische Hinweise unterdrueckt — "
|
|
f"ungenutzte Importe, f-Strings ohne Platzhalter)")
|
|
except ImportError:
|
|
print(" ⚠ pyflakes nicht installiert — Stufe uebersprungen.")
|
|
print(" Installation: python -m pip install pyflakes")
|
|
|
|
|
|
# ── 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"(?<!\\)`", z))
|
|
if in_tmpl:
|
|
in_tmpl ^= bool(bt % 2)
|
|
continue
|
|
if bt % 2:
|
|
in_tmpl = True
|
|
continue
|
|
s = re.sub(r"//.*$", "", z) # Zeilenkommentar weg
|
|
# ⚠ Quotes NICHT einfach zaehlen — das erzeugt Fehlalarme: '"' ist ein
|
|
# gueltiges Literal, das EIN doppeltes Anfuehrungszeichen enthaelt (real
|
|
# in app.js aufgetreten). Stattdessen zeichenweise laufen und den jeweils
|
|
# AKTIVEN Quote-Typ verfolgen; Quotes der anderen Art zaehlen dann nicht.
|
|
aktiv, esc = None, False
|
|
for ch in s:
|
|
if esc:
|
|
esc = False
|
|
continue
|
|
if ch == "\\":
|
|
esc = True
|
|
continue
|
|
if aktiv:
|
|
if ch == aktiv:
|
|
aktiv = None
|
|
elif ch in ('"', "'"):
|
|
aktiv = ch
|
|
if aktiv:
|
|
treffer.append((nr, z.strip()[:100]))
|
|
return treffer
|
|
|
|
|
|
print()
|
|
print("=" * 88)
|
|
print(" B) JAVASCRIPT — unabgeschlossene String-Literale")
|
|
print("=" * 88)
|
|
for p in dateien(".js"):
|
|
if "lightweight-charts" in p.name:
|
|
continue
|
|
tr = js_offene_strings(p.read_text(encoding="utf-8", errors="replace"))
|
|
if tr:
|
|
for nr, z in tr:
|
|
befunde.append(f"JS {p.relative_to(WURZEL)}:{nr}")
|
|
print(f" FEHLER {p.relative_to(WURZEL)}:{nr} {z}")
|
|
else:
|
|
print(f" OK {p.relative_to(WURZEL)}")
|
|
|
|
|
|
# ── C) literales \n als TEXT ─────────────────────────────────────────────
|
|
print()
|
|
print("=" * 88)
|
|
print(" C) UMGEKEHRT — literales \\n im sichtbaren Text (HTML-Attribute, Markdown)")
|
|
print("=" * 88)
|
|
for p in list(dateien(".html")) + list(dateien(".md")):
|
|
t = p.read_text(encoding="utf-8", errors="replace")
|
|
roh = []
|
|
for m in re.finditer(r'title="([^"]*)"', t):
|
|
if "\\n" in m.group(1):
|
|
roh.append(f"title=...{m.group(1)[:60]}")
|
|
if p.suffix == ".md":
|
|
for nr, z in enumerate(t.splitlines(), 1):
|
|
if "\\n" in z and "`" not in z and "\\\\n" not in z:
|
|
roh.append(f"Zeile {nr}: {z.strip()[:80]}")
|
|
if roh:
|
|
for r in roh[:5]:
|
|
befunde.append(f"TXT {p.relative_to(WURZEL)}: {r}")
|
|
print(f" FEHLER {p.relative_to(WURZEL)} {r}")
|
|
else:
|
|
print(f" OK {p.relative_to(WURZEL)}")
|
|
|
|
|
|
# ── D) Signatur der heutigen Faelle ──────────────────────────────────────
|
|
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)
|
|
print(" ERGEBNIS: " + ("keine \\n-Falle gefunden" if not befunde
|
|
else f"{len(befunde)} Befund(e)"))
|
|
print("=" * 88)
|
|
sys.exit(1 if befunde else 0)
|