Pipeline Stufe E: Slot-Trennung und Attribut-Reihenfolge (tools/check_slots.py)
User: "Uebernehme die Logikpruefung auf allen Slots in die Deployment pipeline" -
nach dem Ausfall, bei dem der Serverstart komplett abbrach und zwei Minuten lang
kein Close moeglich war.
ZWEI FEHLERKLASSEN, beide binnen zwei Tagen real aufgetreten, beide von KEINER
bestehenden Stufe gefangen (py_compile, ruff F821, ty, pytest: alle gruen):
(A) ATTRIBUT VOR SEINER ZUWEISUNG. `self.trader_brk.history = self.history`
stand in `__init__` VOR `self.trader_brk = TradeManager(...)`.
AttributeError im Konstruktor -> Serverstart brach ab -> "Netzwerkfehler" am
Close-Knopf. Syntaktisch korrekt, und kein Test instanziiert die Engine (sie
braucht MT5). Gefunden hat es erst deploy.py Schritt 2 - also NACH dem Kill
des laufenden Servers. Derselbe Fehler am Vortag mit `_margin_brk` vor
`_load_runtime_state()`, dort sogar still.
Die Pruefung sammelt je `self.X` die ZEILE der ersten Zuweisung und meldet
jede Benutzung davor.
(B) SLOT-VERWECHSLUNG. BRK-Pfade (_check_auto_squeeze, _manage_squeeze_pending,
_check_pending_fill) duerfen den SLOT-ZUSTAND von `self.trader` nicht
anfassen (close/snapshot/open_*/set_open_context/refresh). ⚠ Bewusst eine
WEISSE Liste der Pfade - eine schwarze wuerde jede neue Funktion still
durchlassen. `pending_orders`/`cancel_pending`/`place_stop` sind ausdruecklich
ERLAUBT: magic-basierte Broker-Abfragen ohne Slot-Zustand (geprueft).
Dazu die Invariante, dass `TradingEngine.close` beide Slots erwaehnen muss -
sonst waere eine BRK-Position per Dashboard nicht schliessbar.
DREI RICHTUNGEN VERIFIZIERT: sauber -> gruen; Reihenfolge-Fehler zurueckgebaut ->
"self.trader_brk benutzt, aber erst in Zeile 270 zugewiesen"; Slot-Verwechslung
zurueckgebaut -> "self.trader.close im BRK-Pfad". Danach wieder gruen.
Statisch (AST), kein MT5, Millisekunden. Werkzeugfehler gilt NICHT als sauber.
Laeuft ueber check_nfalle.py im pre-commit-Hook mit.
⚠ Was diese Stufe NICHT kann: ein Startfehler AUSSERHALB von __init__. Dafuer
bliebe ein echter Rauchtest mit gestubbtem MT5 - der ist nicht gebaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a652223a05
commit
2b498fc20e
@@ -0,0 +1,142 @@
|
||||
"""Statische Prüfung: Attribut-Reihenfolge in `__init__` + Slot-Trennung.
|
||||
|
||||
⚠⚠ WOZU — beide Fehlerklassen haben in ZWEI TAGEN je einmal zugeschlagen, und
|
||||
KEINE bestehende Stufe hat sie gefangen:
|
||||
|
||||
(A) ATTRIBUT VOR SEINER ZUWEISUNG BENUTZT.
|
||||
`self.trader_brk.history = self.history` stand in `__init__` VOR
|
||||
`self.trader_brk = TradeManager(...)`. Ergebnis: AttributeError im
|
||||
Konstruktor → der Serverstart brach KOMPLETT ab, und der Nutzer konnte
|
||||
zwei Minuten lang keine Position schliessen ("Netzwerkfehler" am
|
||||
Close-Knopf). py_compile, ruff F821, ty und pytest liefen alle GRÜN —
|
||||
syntaktisch ist es korrekt, und kein Test instanziiert die Engine (sie
|
||||
braucht MT5). Gefunden hat es erst `deploy.py` Schritt 2 — also NACH dem
|
||||
Kill des laufenden Servers.
|
||||
Derselbe Fehler am Vortag mit `_margin_brk` vor `_load_runtime_state()`:
|
||||
dort blieb er sogar still (der Wert wurde nur ueberschrieben).
|
||||
|
||||
(B) SLOT-VERWECHSLUNG. Seit dem Zwei-Slot-Umbau (19.08.) gibt es
|
||||
`self.trader` (manuell + M15) und `self.trader_brk` (Breakout). Mehrere
|
||||
Pfade griffen nach dem falschen: `engine.close()` schloss nur Slot 1 (eine
|
||||
BRK-Position war NICHT schliessbar), `squeeze_reverse` schloss die
|
||||
manuelle statt der BRK-Position, `open_long` routete den Market-Fallback
|
||||
auf Slot 1 ("Position bereits offen!").
|
||||
|
||||
Beide Prüfungen sind STATISCH (AST) — kein MT5, keine Laufzeit, Millisekunden.
|
||||
|
||||
Rückgabe: Liste von Befunden (leer = sauber). Wird von `tools/check_nfalle.py`
|
||||
als Stufe D eingebunden und läuft damit im pre-commit-Hook mit.
|
||||
|
||||
Aufruf einzeln: python tools/check_slots.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
WURZEL = pathlib.Path(__file__).resolve().parent.parent
|
||||
|
||||
# Funktionen, die zum BRK-Pfad gehören und deshalb NICHT auf `self.trader`
|
||||
# zugreifen dürfen. ⚠ Bewusst eine WEISSE Liste: eine schwarze Liste („alles
|
||||
# ausser…") würde bei jeder neuen Funktion stillschweigend durchlassen.
|
||||
BRK_PFADE = {
|
||||
"_check_auto_squeeze",
|
||||
"_manage_squeeze_pending",
|
||||
"_check_pending_fill",
|
||||
}
|
||||
# Zugriffe, die den SLOT-ZUSTAND betreffen. `pending_orders`, `cancel_pending`
|
||||
# und `place_stop` sind magic-basierte BROKER-Abfragen ohne Slot-Zustand und
|
||||
# deshalb ausdruecklich erlaubt (geprüft 2026-08-20).
|
||||
SLOT_METHODEN = {"close", "snapshot", "open_long", "open_short",
|
||||
"set_open_context", "refresh"}
|
||||
|
||||
|
||||
def _init_reihenfolge(baum: ast.AST, quelle: str) -> list:
|
||||
"""(A) `self.X.…` benutzt, bevor `self.X = …` zugewiesen wurde."""
|
||||
befunde = []
|
||||
for kls in [n for n in ast.walk(baum) if isinstance(n, ast.ClassDef)]:
|
||||
init = next((f for f in kls.body
|
||||
if isinstance(f, ast.FunctionDef) and f.name == "__init__"), None)
|
||||
if not init:
|
||||
continue
|
||||
gesetzt: set[str] = set()
|
||||
for knoten in ast.walk(init):
|
||||
# Zuweisungen in Reihenfolge einsammeln … aber ast.walk ist NICHT
|
||||
# zeilentreu. Deshalb unten über die Zeilennummer entschieden.
|
||||
pass
|
||||
# Zeile → Name, an der `self.X` zugewiesen wird (erste Zuweisung zählt)
|
||||
erste: dict[str, int] = {}
|
||||
for knoten in ast.walk(init):
|
||||
if isinstance(knoten, ast.Assign):
|
||||
for ziel in knoten.targets:
|
||||
for t in ([ziel] if not isinstance(ziel, ast.Tuple) else ziel.elts):
|
||||
if (isinstance(t, ast.Attribute)
|
||||
and isinstance(t.value, ast.Name)
|
||||
and t.value.id == "self"):
|
||||
erste.setdefault(t.attr, knoten.lineno)
|
||||
# Benutzungen: `self.X.irgendwas` — X muss vorher zugewiesen sein
|
||||
for knoten in ast.walk(init):
|
||||
if (isinstance(knoten, ast.Attribute)
|
||||
and isinstance(knoten.value, ast.Attribute)
|
||||
and isinstance(knoten.value.value, ast.Name)
|
||||
and knoten.value.value.id == "self"):
|
||||
name = knoten.value.attr
|
||||
if name not in erste:
|
||||
continue # kommt von ausserhalb → nicht pruefbar
|
||||
if knoten.lineno < erste[name]:
|
||||
befunde.append(
|
||||
f"{kls.name}.__init__:{knoten.lineno}: `self.{name}` "
|
||||
f"benutzt, aber erst in Zeile {erste[name]} zugewiesen "
|
||||
f"→ AttributeError beim Start")
|
||||
return sorted(set(befunde))
|
||||
|
||||
|
||||
def _slot_trennung(baum: ast.AST) -> list:
|
||||
"""(B) BRK-Pfade duerfen den Slot-Zustand von `self.trader` nicht anfassen."""
|
||||
befunde = []
|
||||
for fn in [n for n in ast.walk(baum) if isinstance(n, ast.FunctionDef)]:
|
||||
if fn.name not in BRK_PFADE:
|
||||
continue
|
||||
for knoten in ast.walk(fn):
|
||||
if not isinstance(knoten, ast.Attribute):
|
||||
continue
|
||||
v = knoten.value
|
||||
if (isinstance(v, ast.Attribute) and v.attr == "trader"
|
||||
and isinstance(v.value, ast.Name) and v.value.id == "self"
|
||||
and knoten.attr in SLOT_METHODEN):
|
||||
befunde.append(
|
||||
f"{fn.name}:{knoten.lineno}: `self.trader.{knoten.attr}` im "
|
||||
f"BRK-Pfad — muss `self.trader_brk` sein")
|
||||
return sorted(set(befunde))
|
||||
|
||||
|
||||
def _close_beide(baum: ast.AST) -> list:
|
||||
"""`engine.close()` muss BEIDE Slots schliessen (sonst ist eine BRK-Position
|
||||
nicht schliessbar — die gefaehrlichste Variante der Luecke)."""
|
||||
for kls in [n for n in ast.walk(baum) if isinstance(n, ast.ClassDef)]:
|
||||
if kls.name != "TradingEngine":
|
||||
continue
|
||||
fn = next((f for f in kls.body
|
||||
if isinstance(f, ast.FunctionDef) and f.name == "close"), None)
|
||||
if fn and "trader_brk" not in ast.unparse(fn):
|
||||
return ["TradingEngine.close: erwaehnt `trader_brk` NICHT — eine "
|
||||
"BRK-Position waere per Dashboard nicht schliessbar"]
|
||||
return []
|
||||
|
||||
|
||||
def pruefe() -> list:
|
||||
p = WURZEL / "core" / "engine.py"
|
||||
quelle = p.read_text(encoding="utf-8")
|
||||
baum = ast.parse(quelle)
|
||||
return (_init_reihenfolge(baum, quelle) + _slot_trennung(baum)
|
||||
+ _close_beide(baum))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
b = pruefe()
|
||||
for z in b:
|
||||
print(" FEHLER " + z)
|
||||
print(" keine Slot-/Reihenfolge-Fehler." if not b
|
||||
else f" {len(b)} Befund(e)")
|
||||
sys.exit(1 if b else 0)
|
||||
Reference in New Issue
Block a user