Pipeline-Stufe 1 automatisiert: requirements, pre-commit-Hook, ruff

Nach Web-Recherche zum 2026er-Python-Standard umgesetzt, aber gefiltert: von
uv/Ruff/pyproject/CI passt hier nur, was zu einer Einzelmaschine mit
GUI-gebundenem MT5 und Live-Geld passt.

(1) requirements.txt (Laufzeit) + requirements-dev.txt (Werkzeuge/Messung),
    Versionen gepinnt. Bis dahin gab es GAR KEINE Liste — nach einem
    Maschinenausfall waere die Rekonstruktion Raten gewesen. Per AST ueber
    core/, server.py, tools/ ermittelt statt geraten.

(2) tools/pre_commit.py faehrt vor jedem Commit check_nfalle + pytest (~5 s).
    Einrichten: python tools/pre_commit.py --install. Notausgang
    'git commit --no-verify' ist Absicht.
    Bewusst OHNE das pre-commit-Framework: dessen Nutzen ist
    Werkzeug-Versionierung ueber mehrere Maschinen. Hier gibt es eine, und es
    wuerde eine ZWEITE ruff-Version in einer isolierten Umgebung installieren —
    genau die Divergenz 'Pruefumgebung != Laufumgebung', die heute schon einmal
    zugeschlagen hat.

(3) Ruff statt pyflakes (--select F821), ganzer Scan in 2,1 s. KEIN
    ruff format — das wuerde 161 Dateien umformatieren, darunter alle
    Backtests, und die Migrations-Regel verletzen.

⚠ VIERTER Fall der Klasse 'Erfolg melden, wo nichts geprueft wurde', und nur
durch den Test aufgefallen: --select F821,E999 laesst ruff 0.16 komplett
abbrechen (E999 wurde entfernt, Exit 2); die Meldung geht nach stderr, und weil
ich den Rueckgabecode nicht pruefte, meldete die Stufe 'keine undefinierten
Namen' — ohne irgendetwas geprueft zu haben. Ein absichtlich eingebauter
F821-Fehler kam glatt durch den Hook. Behoben: nur F821, und Exit 2 loest jetzt
den pyflakes-Rueckfall aus.

Beide Richtungen verifiziert: mit Fehler stoppt der Hook den Commit, ohne ist
er gruen. Der Test-Commit wurde zurueckgenommen (war nur lokal, ahead 1).

Nicht umgesetzt: Gitea Actions (Runner auf derselben Maschine wie der Live-Bot,
kein Mehrwert bei 5 s Pruefzeit), Container/Blue-Green (MT5 braucht eine
Desktop-Sitzung, EINE Broker-Verbindung), mypy/ty (kaum Annotationen).

Struktureller Fund: core/engine.py importiert backtest_breakout_squeeze — ein
Backtest ist Teil des LIVE-Abhaengigkeitsgraphen.
This commit is contained in:
Axel Hocks
2026-08-07 07:08:02 +02:00
parent 41e527c492
commit a35a0e7006
5 changed files with 254 additions and 21 deletions
+64 -21
View File
@@ -19,6 +19,7 @@ Geprueft wird:
import pathlib
import py_compile
import re
import subprocess
import sys
import tempfile
@@ -80,31 +81,73 @@ print(f" {npy} Dateien geprueft.")
SCHWER = ("undefined name", "undefined local", "syntax error")
print()
print("=" * 88)
print(" A2) PYFLAKES — undefinierte Namen (was py_compile nicht sieht)")
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:
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:
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"PF {kurz}")
befunde.append(f"RUFF {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")
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 ────────────────────────────────────────────────────────
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Pre-Commit-Pruefung: faehrt die Stufe-1-Checks automatisch vor jedem Commit.
⚠ WOZU. Am 2026-08-07 habe ich `py_compile`, `pytest` und `check_nfalle` rund
fuenfzehnmal von Hand gestartet — und trotzdem gingen zwei Fehler durch, weil
ich sie im falschen Moment aufrief: die `\\n`-Falle fiel erst beim naechsten
Aufruf auf, die beiden `log.`-Fehler in `core/trader.py` lagen MONATE drin
(einer seit dem Initial-Commit). Ein Hook vergisst das nicht.
WAS GEPRUEFT WIRD (~34 s):
1. `tools/check_nfalle.py` — kompiliert alle .py, ruff (F821/E999),
JS-String-Literale, die \\n-Falle in beide Richtungen, .ps1-BOM
2. `pytest` — die Szenario-Suite
⚠ NOTAUSGANG: `git commit --no-verify` ueberspringt den Hook. Das ist Absicht —
eine Pruefung, die einen dringenden Fix blockiert, wird sonst dauerhaft
deinstalliert. Wer ihn nutzt, sollte die Pruefung danach nachholen.
⚠ BEWUSST OHNE das `pre-commit`-Framework: dessen Nutzen ist die Verwaltung von
Werkzeug-Versionen ueber mehrere Entwickler und Maschinen hinweg. Hier gibt es
eine Maschine und einen Entwickler; ein eigenes, versioniertes Skript ist
transparenter und braucht keine isolierten Zusatz-Umgebungen (die dann eine
ANDERE ruff-Version haetten als die Kommandozeile — genau die Art Divergenz,
die dieses Projekt schon oft genug hatte).
Aufruf:
python tools/pre_commit.py # Pruefung von Hand
python tools/pre_commit.py --install # als Git-Hook einrichten
"""
import subprocess
import sys
import time
from pathlib import Path
WURZEL = Path(__file__).resolve().parents[1]
PY = sys.executable
def lauf(titel: str, argv: list[str]) -> bool:
t0 = time.time()
r = subprocess.run(argv, cwd=WURZEL, capture_output=True, text=True)
dauer = time.time() - t0
if r.returncode == 0:
print(f"{titel:<28} {dauer:.1f}s")
return True
print(f"{titel:<28} {dauer:.1f}s")
# Nur die letzten Zeilen — der Hook soll die Ursache zeigen, nicht fluten.
for z in (r.stdout + r.stderr).strip().splitlines()[-25:]:
print(f" {z}")
return False
def installieren() -> int:
hooks = WURZEL / ".git" / "hooks"
if not hooks.is_dir():
print(" Kein .git/hooks-Verzeichnis gefunden."); return 1
ziel = hooks / "pre-commit"
# ⚠ Git fuer Windows fuehrt Hooks ueber `sh` aus, nicht ueber cmd — deshalb
# ein sh-Shim mit Vorwaertsschraegstrichen.
ziel.write_text(
"#!/bin/sh\n"
"# Erzeugt von tools/pre_commit.py --install\n"
f'exec "{Path(PY).as_posix()}" -X utf8 '
f'"{(WURZEL / "tools" / "pre_commit.py").as_posix()}"\n',
encoding="utf-8", newline="\n")
print(f" Hook installiert: {ziel}")
print(" Ueberspringen im Notfall: git commit --no-verify")
return 0
def main() -> int:
if "--install" in sys.argv:
return installieren()
print("── Pre-Commit ─────────────────────────────────────")
ok = True
ok &= lauf("Syntax · Linter · Scanner",
[PY, "-X", "utf8", str(WURZEL / "tools" / "check_nfalle.py")])
ok &= lauf("Szenario-Tests (pytest)", [PY, "-X", "utf8", "-m", "pytest", "-q"])
if ok:
print("── alles gruen ────────────────────────────────────")
return 0
print("── COMMIT GESTOPPT ────────────────────────────────")
print(" Beheben — oder bewusst uebergehen: git commit --no-verify")
return 1
if __name__ == "__main__":
sys.exit(main())