- abendkontrolle.py: --quelle und --quelle-ibkr (Vorgabe wsl wie bisher, dockerhost liest das Ergebnis des Hosts ueber SSH), fahre_dockerhost - abendkontrolle_wsl.py: --ibkr-python fuer ibkr_pruefen.py im Container - abendkontrolle_einrichten.ps1: -Quelle und -QuelleIbkr, 23:15 sobald ein Teil vom Host kommt - neue Angriffstreiber abendkontrolle_dockerhost_angriffe.py und abendkontrolle_ibkr_python_angriffe.py - Abdichtung gegen echte WSL- und SSH-Wege in den Treibern, auch in abendkontrolle_angriffe.py und rueckweg_aapl_angriffe.py (nach dem ntfy-Vorfall eines Mutanten am 2026-09-26) - LIESMICH: Abschnitt Paket DH - .claude/workflows/plankritik.js: LF statt CRLF (das CR war die Ursache von "script contains control characters"), meta gekuerzt, Einzelheiten im Kommentar Co-Authored-By: Claude Opus 5.5 <[email protected]>
823 lines
48 KiB
Python
823 lines
48 KiB
Python
"""Angriffsfaelle fuer die Abendkontrolle (abendkontrolle_wsl.py in WSL, abendkontrolle.py unter Windows).
|
|
|
|
Koordinator-Werkzeug, nicht fuers Repo. Jeder Fall laeuft einzeln auf einer Wegwerfkopie, zuerst die
|
|
Grundlinie; ohne Netz (--ohne-broker/--ohne-kerzen, Kalender und Journal aus Dateien, eine Ersatz-
|
|
Auswertung fuer die Brokerzeile) und ohne Meldung (--ohne-meldung). Faelle an zwei Listenpositionen,
|
|
wo die Reihenfolge zaehlen kann (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 3).
|
|
Aufruf in WSL (Venv) und unter Windows: python abendkontrolle_angriffe.py
|
|
Exit 0, wenn jeder Fall so ausgeht wie erwartet; sonst 1 mit der Liste der Abweichungen.
|
|
"""
|
|
|
|
import atexit
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
HIER = Path(__file__).resolve().parent
|
|
TAG = "2026-09-23"
|
|
ECHTES_PROJEKT = Path("/mnt/d/Claude/Code Projekte/active - Trading Plattform")
|
|
ECHTES_PROTOKOLL = ECHTES_PROJEKT / "artifacts" / "paper" / "2026-09-23T131023-paper.jsonl"
|
|
KALENDER_TAG = [{"date": TAG, "open": "09:30", "close": "16:00"}]
|
|
JOURNAL_GUT = (
|
|
"Ende: boersenschluss, Position 0, Orders 2, Killswitch aktiv, Exit 0\n"
|
|
"trading-paper.service: Deactivated successfully.\n"
|
|
)
|
|
DIENSTE_GUT = {
|
|
"trading-dashboard.service": "active",
|
|
"trading-paper.timer": "active",
|
|
"trading-auswertung.timer": "active",
|
|
}
|
|
abweichungen: list[str] = []
|
|
|
|
|
|
# --- WSL-Teil ----------------------------------------------------------------------------------
|
|
|
|
def baue_projekt(wurzel: Path, killswitch: str | None = "aktiv") -> Path:
|
|
projekt = wurzel / "projekt"
|
|
(projekt / "artifacts" / "paper").mkdir(parents=True)
|
|
(projekt / "data" / "paper" / "auswertungen").mkdir(parents=True)
|
|
(projekt / "config").mkdir()
|
|
# Die Auswertung importiert trading.geheimnisse aus dem Arbeitsverzeichnis.
|
|
(projekt / "trading").symlink_to(ECHTES_PROJEKT / "trading")
|
|
shutil.copyfile(ECHTES_PROTOKOLL, projekt / "artifacts" / "paper" / f"{TAG}T131023-paper.jsonl")
|
|
(projekt / "data" / "paper" / "auswertungen" / f"{TAG}.md").write_text("# Bericht\n", encoding="utf-8")
|
|
with sqlite3.connect(projekt / "data" / "registry.db") as db:
|
|
db.execute("CREATE TABLE killswitch_zustand (id INTEGER PRIMARY KEY, zustand TEXT, seit TEXT, ausloeser TEXT)")
|
|
if killswitch is not None:
|
|
db.execute("INSERT INTO killswitch_zustand VALUES (1, ?, '2026-09-23T13:31:00+00:00', 'pascal')", (killswitch,))
|
|
(wurzel / "kalender.json").write_text(json.dumps(KALENDER_TAG), encoding="utf-8")
|
|
(wurzel / "journal.txt").write_text(JOURNAL_GUT, encoding="utf-8")
|
|
(wurzel / "dienste.json").write_text(json.dumps(DIENSTE_GUT), encoding="utf-8")
|
|
return projekt
|
|
|
|
|
|
def fahre(wurzel: Path, projekt: Path, extra_env: dict | None = None, tag: str = TAG, meldung: bool = False) -> dict:
|
|
befehl = [
|
|
sys.executable, str(HIER / "abendkontrolle_wsl.py"), "--tag", tag, "--projekt", str(projekt),
|
|
"--kalender", str(wurzel / "kalender.json"), "--journal", str(wurzel / "journal.txt"),
|
|
"--dienste", str(wurzel / "dienste.json"), "--ohne-kerzen",
|
|
]
|
|
if not meldung:
|
|
befehl.append("--ohne-meldung")
|
|
# Mit der Ersatz-Auswertung darf die Brokerzeile geprueft werden: Der Ersatz geht nicht ans Netz.
|
|
if not (extra_env and "ABENDKONTROLLE_AUSWERTUNG" in extra_env):
|
|
befehl.append("--ohne-broker")
|
|
env = {k: v for k, v in os.environ.items() if k != "ABENDKONTROLLE_AUSWERTUNG"}
|
|
env.update(extra_env or {})
|
|
lauf = subprocess.run(befehl, capture_output=True, text=True, env=env, timeout=300)
|
|
try:
|
|
ergebnis = json.loads(lauf.stdout.strip().splitlines()[-1])
|
|
except (IndexError, json.JSONDecodeError):
|
|
return {"befunde": [f"KEINE JSON-AUSGABE exit {lauf.returncode}: {lauf.stderr[-300:]}"], "exit": lauf.returncode}
|
|
ergebnis["_exit"] = lauf.returncode
|
|
return ergebnis
|
|
|
|
|
|
def erwarte(name: str, ergebnis: dict, *, befund: str | None, anzahl: int | None = None, exit_code: int = 0,
|
|
auch: tuple = (), nicht: tuple = (), fakt=None) -> None:
|
|
befunde = ergebnis.get("befunde", [])
|
|
alles = " | ".join(befunde)
|
|
ok = True
|
|
if befund is None and befunde:
|
|
ok = False
|
|
if befund is not None and not any(befund in b for b in befunde):
|
|
ok = False
|
|
if any(teil not in alles for teil in auch) or any(teil in alles for teil in nicht):
|
|
ok = False
|
|
if anzahl is not None and len(befunde) != anzahl:
|
|
ok = False
|
|
if ergebnis.get("_exit", ergebnis.get("exit")) != exit_code:
|
|
ok = False
|
|
if fakt is not None:
|
|
try:
|
|
ok = ok and bool(fakt(ergebnis.get("fakten") or {}, ergebnis))
|
|
except Exception:
|
|
ok = False
|
|
zeile = f"{'OK ' if ok else 'FEHL'} {name}: {len(befunde)} Befund(e) {befunde[:3]} meldung={(ergebnis.get('fakten') or {}).get('meldung')}"
|
|
print(zeile)
|
|
if not ok:
|
|
abweichungen.append(zeile)
|
|
|
|
|
|
def wsl_faelle() -> None:
|
|
def fall(name, vorbereiten, *, befund, anzahl=None, exit_code=0, env=None, tag=TAG, meldung=False,
|
|
auch=(), nicht=(), fakt=None):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
wurzel = Path(tmp)
|
|
projekt = baue_projekt(wurzel)
|
|
extra = vorbereiten(wurzel, projekt)
|
|
umgebung = {**(env or {}), **(extra if isinstance(extra, dict) else {})}
|
|
erwarte(name, fahre(wurzel, projekt, umgebung, tag, meldung), befund=befund, anzahl=anzahl,
|
|
exit_code=exit_code, auch=auch, nicht=nicht, fakt=fakt)
|
|
|
|
nichts = lambda w, p: None # noqa: E731
|
|
prot = lambda p: p / "artifacts" / "paper" / f"{TAG}T131023-paper.jsonl" # noqa: E731
|
|
|
|
def schreibe(pfad: Path, text: str) -> None:
|
|
pfad.write_text(text, encoding="utf-8")
|
|
|
|
def zeilen_aendern(p: Path, aendern) -> None:
|
|
zeilen = prot(p).read_text(encoding="utf-8").splitlines()
|
|
schreibe(prot(p), "\n".join(aendern(zeilen)) + "\n")
|
|
|
|
fall("Grundlinie", nichts, befund=None, anzahl=0)
|
|
# 1 Kalender
|
|
# Ein Feiertag ohne Protokoll: kein Befund. Haelt die Kontrolle ihn faelschlich fuer einen Handelstag,
|
|
# fehlt das Protokoll und es gibt einen -- so trennt der Fall "kein Handelstag" von "immer Handelstag".
|
|
def feiertag(inhalt):
|
|
def vorbereiten(w, p):
|
|
schreibe(w / "kalender.json", inhalt)
|
|
prot(p).unlink()
|
|
return vorbereiten
|
|
fall("Kalender leer (Feiertag, Wochenende)", feiertag("[]"), befund=None, anzahl=0)
|
|
fall("Kalender nennt nur einen anderen Tag", feiertag(json.dumps([{"date": "2026-09-22"}])), befund=None, anzahl=0)
|
|
fall("Kalender als Objekt", lambda w, p: schreibe(w / "kalender.json", "{}"), befund="Kalender in unerwarteter Form")
|
|
fall("Kalender Eintrag ohne date, erste Position",
|
|
lambda w, p: schreibe(w / "kalender.json", json.dumps([{"open": "09:30"}] + KALENDER_TAG)),
|
|
befund="Kalender in unerwarteter Form")
|
|
fall("Kalender Eintrag ohne date, letzte Position",
|
|
lambda w, p: schreibe(w / "kalender.json", json.dumps(KALENDER_TAG + [{"date": 5}])),
|
|
befund="Kalender in unerwarteter Form")
|
|
fall("Kalender-Datei fehlt", lambda w, p: (w / "kalender.json").unlink(), befund="Kalender, und damit")
|
|
fall("Kalender kein JSON", lambda w, p: schreibe(w / "kalender.json", "<html>"), befund="Kalender, und damit")
|
|
# 2 Protokoll
|
|
fall("kein Protokoll", lambda w, p: prot(p).unlink(), befund="der Runner lief nicht")
|
|
fall("nur Probeprotokoll", lambda w, p: prot(p).rename(prot(p).with_name(f"{TAG}T131023-probe.jsonl")),
|
|
befund="der Runner lief nicht")
|
|
# 6 Handel
|
|
fall("nie aktiv",
|
|
lambda w, p: zeilen_aendern(p, lambda z: [s.replace('"killswitch": "aktiv"', '"killswitch": "angehalten"') for s in z]),
|
|
befund="Tag ohne Handel")
|
|
fall("kaputte Zeile vorne", lambda w, p: zeilen_aendern(p, lambda z: [z[0], "{kaputt", *z[1:]]), befund="nicht lesbar")
|
|
fall("kaputte Zeile hinten", lambda w, p: zeilen_aendern(p, lambda z: [*z, "[1, 2]"]), befund="nicht lesbar")
|
|
# 3 Journal
|
|
fall("Journal ohne Ende", lambda w, p: schreibe(w / "journal.txt", "gestartet\n"), befund="keine Zeile 'Ende")
|
|
fall("Journal leer", lambda w, p: schreibe(w / "journal.txt", ""), befund="keine Zeile 'Ende")
|
|
fall("Journal fehlt", lambda w, p: (w / "journal.txt").unlink(), befund="Journal nicht geprueft")
|
|
fall("Exit 4", lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT.replace("Exit 0", "Exit 4")), befund="Exit 4")
|
|
fall("Exit 4 im ersten von zwei Laeufen",
|
|
lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT.replace("Exit 0", "Exit 4") + JOURNAL_GUT), befund="Lauf 1 von 2")
|
|
fall("Exit 4 im letzten von zwei Laeufen",
|
|
lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT + JOURNAL_GUT.replace("Exit 0", "Exit 4")), befund="Lauf 2 von 2")
|
|
fall("Grund stoerung", lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT.replace("Ende: boersenschluss", "Ende: stoerung")),
|
|
befund="nicht mit boersenschluss")
|
|
fall("Position 10", lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT.replace("Position 0", "Position 10")),
|
|
befund="Position 10")
|
|
fall("Failed with result",
|
|
lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT + "trading-paper.service: Failed with result 'exit-code'.\n"),
|
|
befund="Failed with result")
|
|
# 4 Auswertung
|
|
fall("Orderzeile nicht gefuellt",
|
|
lambda w, p: zeilen_aendern(p, lambda z: [s.replace('"status": "filled"', '"status": "canceled"', 1) if '"art": "order"' in s else s for s in z]),
|
|
befund="nicht gefuellt")
|
|
fall("Schlussposition 10 im Protokoll",
|
|
lambda w, p: zeilen_aendern(p, lambda z: [s.replace('"position": 0', '"position": 10') if '"art": "ende"' in s else s for s in z]),
|
|
befund="Schlussposition ungleich null")
|
|
# 5 Broker (Ersatz-Auswertung, kein Netz)
|
|
ersatz = HIER / "abendkontrolle_ersatz_auswertung.py"
|
|
fall("Broker 2 offen", nichts, befund="2 offene Position",
|
|
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz), "ERSATZ_AUSGABE": "offene positionen beim broker jetzt: 2"},
|
|
)
|
|
fall("Broker 0 offen", nichts, befund=None, anzahl=0,
|
|
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz), "ERSATZ_AUSGABE": "offene positionen beim broker jetzt: 0"})
|
|
fall("Auswertung Exit 3", nichts, befund="Exit 3",
|
|
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz), "ERSATZ_AUSGABE": "VORAUSSETZUNG FEHLT: x", "ERSATZ_EXIT": "3"})
|
|
fall("Auswertung Kontowert in BEFUND wird maskiert", nichts, befund="kontowert [x]",
|
|
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz), "ERSATZ_AUSGABE": "BEFUND: kontowert 12345.67 zu tief"})
|
|
# 7 Bericht
|
|
fall("Bericht fehlt", lambda w, p: (p / "data" / "paper" / "auswertungen" / f"{TAG}.md").unlink(), befund="Tagesbericht")
|
|
# 8 Dienste
|
|
fall("Dashboard inaktiv (erste Position)",
|
|
lambda w, p: schreibe(w / "dienste.json", json.dumps({**DIENSTE_GUT, "trading-dashboard.service": "failed"})),
|
|
befund="trading-dashboard.service ist nicht aktiv")
|
|
fall("Auswertungs-Zeitgeber fehlt (letzte Position)",
|
|
lambda w, p: schreibe(w / "dienste.json", json.dumps({k: v for k, v in DIENSTE_GUT.items() if "auswertung" not in k})),
|
|
befund="trading-auswertung.timer ist nicht aktiv")
|
|
fall("Dienste als Liste", lambda w, p: schreibe(w / "dienste.json", "[]"), befund="Dienste: Zustand nicht lesbar")
|
|
# 9 Killswitch
|
|
def killswitch(zustand):
|
|
def vorbereiten(w, p):
|
|
with sqlite3.connect(p / "data" / "registry.db") as db:
|
|
if zustand is None:
|
|
db.execute("DELETE FROM killswitch_zustand")
|
|
else:
|
|
db.execute("UPDATE killswitch_zustand SET zustand = ?", (zustand,))
|
|
return vorbereiten
|
|
fall("Killswitch gesperrt", killswitch("gesperrt"), befund="gesperrt")
|
|
fall("Killswitch ohne Zeile", killswitch(None), befund="kein Killswitch-Zustand")
|
|
fall("Register fehlt", lambda w, p: (p / "data" / "registry.db").unlink(), befund="registry.db fehlt")
|
|
# 3 Ende je Lauf: mittags abgebrochen, der zweite Lauf bleibt angehalten (Befund des Pruefers W2)
|
|
def mittags_abgebrochen(w, p):
|
|
zeilen = prot(p).read_text(encoding="utf-8").splitlines()
|
|
takte = [z for z in zeilen if '"art": "takt"' in z]
|
|
ende = json.loads(next(z for z in zeilen if '"art": "ende"' in z))
|
|
lauf1 = [zeilen[0], *takte[:30], json.dumps({**ende, "grund": "abbruch"})]
|
|
lauf2 = [zeilen[0], *[z.replace('"killswitch": "aktiv"', '"killswitch": "angehalten"') for z in takte[30:]], json.dumps(ende)]
|
|
schreibe(prot(p), "\n".join(lauf1) + "\n")
|
|
schreibe(p / "artifacts" / "paper" / f"{TAG}T160000-paper.jsonl", "\n".join(lauf2) + "\n")
|
|
schreibe(w / "journal.txt", JOURNAL_GUT.replace("Ende: boersenschluss", "Ende: abbruch") + JOURNAL_GUT)
|
|
fall("mittags abgebrochen, danach angehalten", mittags_abgebrochen, befund="Lauf endete mit abbruch",
|
|
auch=("Lauf 1 von 2) endete nicht mit boersenschluss",))
|
|
|
|
# Lauf nach Boersenschluss (WSL-Neustart abends): kein Befund (Befund des Pruefers W3)
|
|
def nach_schluss(w, p):
|
|
kopf = prot(p).read_text(encoding="utf-8").splitlines()[0]
|
|
ende = {"art": "ende", "zeit": f"{TAG}T21:06:40+00:00", "grund": "wartezeit", "fehler": None, "position": 0,
|
|
"orders": 0, "killswitch": "angehalten", "schritte": 0}
|
|
schreibe(p / "artifacts" / "paper" / f"{TAG}T210500-paper.jsonl", kopf + "\n" + json.dumps(ende) + "\n")
|
|
schreibe(w / "journal.txt", JOURNAL_GUT + "Ende: wartezeit, Position 0, Orders 0, Killswitch angehalten, Exit 3\n")
|
|
fall("Lauf nach Boersenschluss (wartezeit, Exit 3)", nach_schluss, befund=None, anzahl=0)
|
|
fall("nur ein Lauf mit wartezeit an einem Handelstag", lambda w, p: (
|
|
nach_schluss(w, p), prot(p).unlink(),
|
|
schreibe(w / "journal.txt", "Ende: wartezeit, Position 0, Orders 0, Killswitch angehalten, Exit 3\n")),
|
|
befund="keine Zeile 'Ende", auch=("kein Lauf des scharfen Runners in der Handelszeit",))
|
|
# 9 Killswitch: unbekannter Zustand, und eine Frage, die scheitert, haelt die anderen nicht auf
|
|
fall("Killswitch unbekannter Zustand", killswitch("kaputt"), befund="unbekanntem Zustand")
|
|
|
|
def register_ohne_tabelle(w, p):
|
|
(p / "data" / "registry.db").unlink()
|
|
sqlite3.connect(p / "data" / "registry.db").close()
|
|
(p / "data" / "paper" / "auswertungen" / f"{TAG}.md").unlink()
|
|
fall("Register ohne Tabelle, Bericht fehlt", register_ohne_tabelle, befund="Killswitch nicht geprueft",
|
|
auch=("Tagesbericht",))
|
|
# Maske auch ohne Geldwort (Befund des Pruefers W4)
|
|
fall("Maske ohne Geldwort", nichts, befund="Wert",
|
|
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz),
|
|
"ERSATZ_AUSGABE": "BEFUND: Wert 100123.45 und 172.3456 und -4.56 USD\noffene positionen beim broker jetzt: 0"},
|
|
nicht=("100123", "172.3456", "4.56"))
|
|
|
|
# Meldeweg (Befund des Pruefers B1): ein Fehlschlag darf nicht als "gesendet" stehen
|
|
def mit_melder(port):
|
|
def vorbereiten(w, p):
|
|
text = (ECHTES_PROJEKT / "config" / "paper.toml").read_text(encoding="utf-8")
|
|
anker = 'ntfy_url = "https://ntfy.sh"'
|
|
if text.count(anker) != 1:
|
|
raise RuntimeError("Anker ntfy_url fehlt in config/paper.toml")
|
|
schreibe(p / "config" / "paper.toml", text.replace(anker, f'ntfy_url = "http://127.0.0.1:{port}"\nklartext_lokal = true'))
|
|
schreibe(w / "secrets.toml", '[ntfy]\naktiv = true\nthema = "abendkontrolle-angriff"\n')
|
|
(p / "data" / "paper" / "auswertungen" / f"{TAG}.md").unlink() # ein Befund, damit gemeldet wird
|
|
return {"TRADING_GEHEIMNISSE": str(w / "secrets.toml")}
|
|
return vorbereiten
|
|
fall("Meldung scheitert (niemand hoert)", mit_melder(9), befund="Tagesbericht", meldung=True,
|
|
fakt=lambda f, e: str(f.get("meldung", "")).startswith("nicht gesendet"))
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
mitschrift = Path(tmp) / "mitschrift.jsonl"
|
|
port = 18765
|
|
server = subprocess.Popen([sys.executable, str(HIER / "ntfy_probe_server.py"), str(port), str(mitschrift)],
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
try:
|
|
import socket
|
|
import time
|
|
for _ in range(50):
|
|
try:
|
|
socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
|
|
break
|
|
except OSError:
|
|
time.sleep(0.1)
|
|
fall("Meldung kommt an", mit_melder(port), befund="Tagesbericht", meldung=True,
|
|
fakt=lambda f, e: f.get("meldung") == "gesendet" and "Abendkontrolle" in mitschrift.read_text(encoding="utf-8"))
|
|
finally:
|
|
server.terminate()
|
|
server.wait(timeout=10)
|
|
|
|
# Bedienfehler
|
|
fall("Tag falsch", nichts, befund="abgebrochen", exit_code=2, tag="2026-13-40")
|
|
|
|
|
|
# --- Windows-Teil --------------------------------------------------------------------------------
|
|
|
|
def abdichten(ak) -> list:
|
|
"""Sperrt die echten Wege von abendkontrolle.py fuer die Windows-Faelle (Plankritik DH, 2026-09-26).
|
|
|
|
Die Faelle laufen mit --ohne-wsl; bricht ein Mutant diesen Schalter, liefe sonst der echte
|
|
WSL-Teil gegen den echten Projektordner, und seit Paket DH ssh zum Docker-Host. wsl.exe und
|
|
ssh.exe zeigen ins Leere, fahre_wsl und fahre_dockerhost schreiben jeden Aufruf mit und
|
|
lehnen ab; jeder Aufruf ist eine Abweichung.
|
|
"""
|
|
gerufen = []
|
|
abdichten.erwartet = False
|
|
|
|
def verboten(name):
|
|
def weg(*argumente, **benannte):
|
|
gerufen.append(name)
|
|
zeile = f"FEHL echter Weg {name} gerufen: {argumente}"
|
|
abweichungen.append(zeile)
|
|
if not abdichten.erwartet:
|
|
print(zeile)
|
|
raise RuntimeError(f"echter Weg {name} in den Angriffsfaellen")
|
|
|
|
weg.sperre = True
|
|
return weg
|
|
|
|
leer = Path(tempfile.mkdtemp(prefix="abendkontrolle-angriffe-kein-weg-"))
|
|
atexit.register(shutil.rmtree, leer, True) # der leere Ordner bleibt nicht im Temp liegen
|
|
ak.WSL = str(leer / "wsl.exe")
|
|
ak.SSH = str(leer / "ssh.exe")
|
|
ak.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
|
|
ak.fahre_wsl = verboten("fahre_wsl")
|
|
ak.fahre_dockerhost = verboten("fahre_dockerhost")
|
|
return gerufen
|
|
|
|
|
|
def windows_faelle() -> None:
|
|
sys.path.insert(0, str(HIER))
|
|
import abendkontrolle as ak
|
|
from datetime import datetime
|
|
|
|
gerufen = abdichten(ak)
|
|
# Die Abdichtung greift: Ohne --ohne-wsl nimmt main den WSL-Weg, und der muss als Abweichung
|
|
# ankommen, nicht als Lauf. Die erwartete Abweichung wird danach wieder entfernt.
|
|
vorher = len(abweichungen)
|
|
abdichten.erwartet = True
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
(Path(tmp) / "Inbox").mkdir()
|
|
ak.main(["--vault", tmp, "--tag", TAG, "--teil", "alpaca", "--jetzt", "2026-09-23T22:45:00"])
|
|
abdichten.erwartet = False
|
|
# Beleg fuer die uebrigen Zeilen von abdichten: beide Programme und das Skript ins Leere,
|
|
# fahre_dockerhost gesperrt.
|
|
gesperrt = (not Path(ak.WSL).exists() and not Path(ak.SSH).exists()
|
|
and ak.WSL_SKRIPT.startswith("/nicht-vorhanden/")
|
|
and getattr(ak.fahre_dockerhost, "sperre", False))
|
|
ok = gesperrt and gerufen == ["fahre_wsl"] and len(abweichungen) == vorher + 1
|
|
del abweichungen[vorher:]
|
|
del gerufen[:]
|
|
zeile = f"{'OK ' if ok else 'FEHL'} W Abdichtung: der echte WSL-Weg kommt als Abweichung an"
|
|
print(zeile)
|
|
if not ok:
|
|
abweichungen.append(zeile)
|
|
|
|
gut = {"tag": TAG, "befunde": [], "hinweise": ["auto_fortsetzen: fortgesetzt"], "exit": 0,
|
|
"fakten": {"handelstag": True, "ende": {"grund": "boersenschluss", "exit": "0", "orders": "79", "position": "0"},
|
|
"laeufe": [{"takte_aktiv": 375}], "broker_offen": 0, "bericht": True,
|
|
"killswitch": {"zustand": "aktiv", "ausloeser": "automatik"}}}
|
|
|
|
def lauf(name, notiz_vorher, ergebnis, *, jetzt="2026-09-23T22:45:00", pruefe):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
vault = Path(tmp)
|
|
(vault / "Inbox").mkdir()
|
|
notiz = vault / "Inbox" / f"{TAG}.md"
|
|
if notiz_vorher is not None:
|
|
notiz.write_bytes(notiz_vorher if isinstance(notiz_vorher, bytes) else notiz_vorher.encode("utf-8"))
|
|
eingabe = vault / "ergebnis.json"
|
|
eingabe.write_text(ergebnis if isinstance(ergebnis, str) else json.dumps(ergebnis), encoding="utf-8")
|
|
code = ak.main(["--vault", str(vault), "--tag", TAG, "--ohne-wsl", str(eingabe), "--jetzt", jetzt])
|
|
text = notiz.read_bytes().decode("utf-8", errors="replace") if notiz.exists() else ""
|
|
marke = (vault / ak.ABLAGE_TEIL / f"{TAG}.json").exists()
|
|
try:
|
|
ok, grund = pruefe(code, text, marke)
|
|
except Exception as ausnahme: # der Treiber selbst bricht nicht ab
|
|
ok, grund = False, f"Pruefung warf {ausnahme!r}; Notiz {text[-120:]!r}"
|
|
zeile = f"{'OK ' if ok else 'FEHL'} W {name}: {grund}"
|
|
print(zeile)
|
|
if not ok:
|
|
abweichungen.append(zeile)
|
|
|
|
kopf = "---\ntype: daily\n---\n\n# 2026-09-23\n\n"
|
|
lauf("neue Notiz, ohne Befund", None, gut, pruefe=lambda c, t, m: (
|
|
c == 0 and m and "## Agentenprotokoll" in t and "ohne Befund" in t and "type: daily" in t, f"code {c} marke {m}"))
|
|
lauf("Abschnitt vor einer weiteren Ueberschrift", kopf + "## Agentenprotokoll\n\n- alt\n\n## Danach\n\ntext\n", gut,
|
|
pruefe=lambda c, t, m: (t.index("ohne Befund") < t.index("## Danach") and "- alt\n- 22:45" in t
|
|
and "\n\n## Danach" in t and "\n\n\n" not in t, repr(t[-80:])))
|
|
lauf("Abschnitt am Ende", kopf + "## Agentenprotokoll\n\n- alt\n", gut,
|
|
pruefe=lambda c, t, m: (t.endswith(".\n - Hinweis: auto_fortsetzen: fortgesetzt\n") and "- alt\n- 22:45" in t, repr(t[-60:])))
|
|
lauf("ohne Abschnitt", kopf + "## Anderes\n\nx\n", gut,
|
|
pruefe=lambda c, t, m: (t.count("## Agentenprotokoll") == 1 and t.index("## Anderes") < t.index("## Agentenprotokoll"), repr(t[-60:])))
|
|
lauf("Abschnitt direkt vor naechster Ueberschrift", kopf + "## Agentenprotokoll\n## Danach\n", gut,
|
|
pruefe=lambda c, t, m: ("## Agentenprotokoll\n\n- 22:45" in t and "\n\n## Danach" in t, repr(t)))
|
|
lauf("CRLF-Notiz", (kopf + "## Agentenprotokoll\n\n- alt\n").replace("\n", "\r\n"), gut,
|
|
pruefe=lambda c, t, m: ("\n" not in t.replace("\r\n", ""), "gemischte Zeilenenden" if "\n" in t.replace("\r\n", "") else "ok"))
|
|
mit_befund = {**gut, "befunde": ["Runner endete mit Exit 4", "Beim Broker stehen 2 offene Position(en)"]}
|
|
lauf("mit Befund", None, mit_befund, pruefe=lambda c, t, m: (
|
|
"**2 Befund(e)** #needs-review" in t and " - Runner endete mit Exit 4" in t and m, repr(t[-160:])))
|
|
lauf("Ergebnis ohne befunde", None, {"tag": TAG}, pruefe=lambda c, t, m: (
|
|
c == 1 and not m and "**gescheitert**" in t and "#needs-review" in t, f"code {c} marke {m}"))
|
|
lauf("Ergebnis kein JSON", None, "nicht json", pruefe=lambda c, t, m: (c == 1 and not m and "**gescheitert**" in t, f"code {c}"))
|
|
lauf("kein Handelstag", None, {"tag": TAG, "befunde": [], "hinweise": [], "fakten": {"handelstag": False}},
|
|
pruefe=lambda c, t, m: ("kein Handelstag" in t and m, repr(t[-60:])))
|
|
# Befunde des Pruefers: Ueberschrift mit Zusatz, Notiz ohne UTF-8, Abbruch mit JSON, Nachbardatei
|
|
lauf("Ueberschrift mit Zusatz", kopf + "## Agentenprotokoll — Sitzung 9\n\n- alt\n\n## Offene Punkte\n\n- x\n", gut,
|
|
pruefe=lambda c, t, m: (t.count("## Agentenprotokoll") == 1 and t.index("ohne Befund") < t.index("## Offene Punkte"), repr(t[-120:])))
|
|
kaputt = "---\ntype: daily\n---\n\n# Tag äöü\n".encode("cp1252")
|
|
lauf("Notiz kein UTF-8", kaputt, mit_befund, pruefe=lambda c, t, m: (m and c == 1, f"code {c} marke {m}"))
|
|
lauf("Abbruch mit JSON (exit 2) wird wiederholt", None,
|
|
{"tag": TAG, "befunde": ["Abendkontrolle abgebrochen: X"], "hinweise": [], "fakten": {}, "exit": 2},
|
|
pruefe=lambda c, t, m: (not m and "**1 Befund(e)**" in t and c == 1, f"code {c} marke {m}"))
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
vault = Path(tmp)
|
|
(vault / "Inbox").mkdir()
|
|
eingabe = vault / "e.json"
|
|
eingabe.write_text(json.dumps(gut), encoding="utf-8")
|
|
ak.main(["--vault", str(vault), "--tag", TAG, "--ohne-wsl", str(eingabe), "--jetzt", "2026-09-23T22:45:00"])
|
|
rest = [p.name for p in (vault / "Inbox").iterdir() if not p.name.endswith(".md")]
|
|
ok = not rest
|
|
print(f"{'OK ' if ok else 'FEHL'} W keine Nachbardatei uebrig: {rest}")
|
|
if not ok:
|
|
abweichungen.append("Nachbardatei uebrig")
|
|
|
|
# faellige_tage
|
|
from datetime import date
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
ablage = Path(tmp)
|
|
def tage(jetzt):
|
|
return ak.faellige_tage(datetime.fromisoformat(jetzt), ablage)
|
|
faelle = [
|
|
("Do vor 22:30: heute nicht", "2026-09-24T21:00", ["2026-09-24"], False),
|
|
("Do nach 22:30: heute", "2026-09-24T22:45", ["2026-09-24"], True),
|
|
("Mo frueh: Fr nachholen, Wochenende nicht", "2026-09-28T08:00", ["2026-09-25"], True),
|
|
("Mo frueh: nichts vor ERSTER_TAG", "2026-09-28T08:00", ["2026-09-23"], False),
|
|
]
|
|
for name, jetzt, erwartet, drin in faelle:
|
|
ergebnis = tage(jetzt)
|
|
ok = all((tag in ergebnis) == drin for tag in erwartet) and not any(
|
|
date.fromisoformat(t).weekday() >= 5 for t in ergebnis)
|
|
zeile = f"{'OK ' if ok else 'FEHL'} T {name}: {ergebnis}"
|
|
print(zeile)
|
|
if not ok:
|
|
abweichungen.append(zeile)
|
|
(ablage / "2026-09-25.json").write_text("{}", encoding="utf-8")
|
|
ergebnis = tage("2026-09-28T08:00")
|
|
ok = "2026-09-25" not in ergebnis
|
|
print(f"{'OK ' if ok else 'FEHL'} T Ergebnisdatei da: nicht erneut: {ergebnis}")
|
|
if not ok:
|
|
abweichungen.append("Ergebnisdatei ignoriert")
|
|
ergebnis = tage("2026-10-12T08:00")
|
|
ok = min(ergebnis) >= "2026-10-05"
|
|
print(f"{'OK ' if ok else 'FEHL'} T hoechstens sieben Tage zurueck: {ergebnis}")
|
|
if not ok:
|
|
abweichungen.append("Nachholfenster")
|
|
|
|
|
|
# --- IBKR-Instanz (Paket IB2), WSL-Teil ------------------------------------------------------------
|
|
|
|
IBKR_JOURNAL_GUT = (
|
|
"Ende: boersenschluss, Position 0, Orders 2, Killswitch aktiv, Exit 0\n"
|
|
"trading-paper-ibkr.service: Deactivated successfully.\n"
|
|
)
|
|
# Erfundene Ausgabe von scripts/ibkr_pruefen.py lesen: ohne Kontonummer, ohne Betrag.
|
|
IBKR_KONTO_GUT = (
|
|
"verbunden: Gateway 127.0.0.1:4004, Client-ID 12, Handelsmodus paper, Konto DU... (9 Zeichen), "
|
|
"vom Gateway bestaetigt\n"
|
|
"Konto: Kontowert, Bargeld und Kaufkraft gelesen (Betraege werden nicht ausgegeben)\n"
|
|
"Waehrung des Kontowerts: EUR\n"
|
|
"Positionen: 0\n"
|
|
"Offene Orders: 0\n"
|
|
"ERGEBNIS: gelesen\n"
|
|
)
|
|
# Eine erfundene Kontonummer in der Form der Paper-Konten, nur fuer die Maske.
|
|
KONTO_ERFUNDEN = "DU0000000"
|
|
KONTO_ERFUNDEN_LANG = "DUQ000000"
|
|
# 22:45 Berlin ist 16:45 New York (nach Boersenschluss), 16:00 Berlin ist 10:00 New York.
|
|
NACH_SCHLUSS = f"{TAG}T22:45:00+02:00"
|
|
IM_HANDEL = f"{TAG}T16:00:00+02:00"
|
|
|
|
|
|
def baue_ibkr(wurzel: Path) -> Path:
|
|
ordner = wurzel / "betrieb"
|
|
(ordner / "artifacts" / "paper").mkdir(parents=True)
|
|
(ordner / "data" / "paper").mkdir(parents=True)
|
|
(ordner / "data" / "paper" / "broker.txt").write_text("ibkr\n", encoding="utf-8")
|
|
shutil.copyfile(ECHTES_PROTOKOLL, ordner / "artifacts" / "paper" / f"{TAG}T131023-paper.jsonl")
|
|
with sqlite3.connect(ordner / "data" / "registry.db") as db:
|
|
db.execute("CREATE TABLE killswitch_zustand (id INTEGER PRIMARY KEY, zustand TEXT, seit TEXT, ausloeser TEXT)")
|
|
db.execute("CREATE TABLE killswitch_ereignisse (id INTEGER PRIMARY KEY AUTOINCREMENT, zeit TEXT, stufe TEXT, "
|
|
"ausloeser TEXT, grund TEXT, ergebnis TEXT, erfolgreich INTEGER)")
|
|
db.execute("INSERT INTO killswitch_zustand VALUES (1, 'aktiv', '2026-09-23T13:31:00+00:00', 'automatik')")
|
|
db.execute("INSERT INTO killswitch_ereignisse (zeit, stufe, ausloeser, grund, ergebnis, erfolgreich) "
|
|
"VALUES ('2026-09-23T13:31:00+00:00', 'fortsetzen', 'automatik', 'Tagesstart', '{}', 1)")
|
|
(wurzel / "kalender.json").write_text(json.dumps(KALENDER_TAG), encoding="utf-8")
|
|
(wurzel / "journal.txt").write_text(IBKR_JOURNAL_GUT, encoding="utf-8")
|
|
(wurzel / "dienste.json").write_text(json.dumps({"trading-paper-ibkr.timer": "active"}), encoding="utf-8")
|
|
(wurzel / "konto.json").write_text(json.dumps({"exit": 0, "text": IBKR_KONTO_GUT}), encoding="utf-8")
|
|
return ordner
|
|
|
|
|
|
def fahre_ibkr(wurzel: Path, ordner: Path, jetzt: str = NACH_SCHLUSS, tag: str = TAG, *,
|
|
projekt: Path | None = None, meldung: bool = False, env: dict | None = None) -> dict:
|
|
befehl = [
|
|
sys.executable, str(HIER / "abendkontrolle_wsl.py"), "--teil", "ibkr", "--tag", tag,
|
|
"--ibkr-ordner", str(ordner), "--projekt", str(wurzel if projekt is None else projekt),
|
|
"--kalender", str(wurzel / "kalender.json"), "--journal", str(wurzel / "journal.txt"),
|
|
"--dienste", str(wurzel / "dienste.json"), "--ibkr-konto", str(wurzel / "konto.json"), "--jetzt", jetzt,
|
|
]
|
|
if not meldung:
|
|
befehl.append("--ohne-meldung")
|
|
umgebung = {k: v for k, v in os.environ.items() if k != "ABENDKONTROLLE_AUSWERTUNG"}
|
|
umgebung.update(env or {})
|
|
lauf = subprocess.run(befehl, capture_output=True, text=True, env=umgebung, timeout=300)
|
|
try:
|
|
ergebnis = json.loads(lauf.stdout.strip().splitlines()[-1])
|
|
except (IndexError, json.JSONDecodeError):
|
|
return {"befunde": [f"KEINE JSON-AUSGABE exit {lauf.returncode}: {lauf.stderr[-300:]}"], "exit": lauf.returncode}
|
|
ergebnis["_exit"] = lauf.returncode
|
|
return ergebnis
|
|
|
|
|
|
def ibkr_wsl_faelle() -> None:
|
|
def fall(name, vorbereiten, *, befund, anzahl=None, exit_code=0, jetzt=NACH_SCHLUSS, tag=TAG,
|
|
auch=(), nicht=(), fakt=None):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
wurzel = Path(tmp)
|
|
ordner = baue_ibkr(wurzel)
|
|
vorbereiten(wurzel, ordner)
|
|
erwarte("IBKR " + name, fahre_ibkr(wurzel, ordner, jetzt, tag), befund=befund, anzahl=anzahl,
|
|
exit_code=exit_code, auch=auch, nicht=nicht, fakt=fakt)
|
|
|
|
nichts = lambda w, o: None # noqa: E731
|
|
prot = lambda o: o / "artifacts" / "paper" / f"{TAG}T131023-paper.jsonl" # noqa: E731
|
|
|
|
def schreibe(pfad: Path, text: str) -> None:
|
|
pfad.write_text(text, encoding="utf-8")
|
|
|
|
def konto(text: str, code: int = 0):
|
|
return lambda w, o: schreibe(w / "konto.json", json.dumps({"exit": code, "text": text}))
|
|
|
|
def register(*befehle: str):
|
|
def vorbereiten(w, o):
|
|
with sqlite3.connect(o / "data" / "registry.db") as db:
|
|
for befehl in befehle:
|
|
db.execute(befehl)
|
|
return vorbereiten
|
|
|
|
def zeilen_aendern(o: Path, aendern) -> None:
|
|
zeilen = prot(o).read_text(encoding="utf-8").splitlines()
|
|
schreibe(prot(o), "\n".join(aendern(zeilen)) + "\n")
|
|
|
|
fall("Grundlinie", nichts, befund=None, anzahl=0,
|
|
fakt=lambda f, e: f.get("konto") == {"positionen": 0, "orders": 0, "exit": 0} and e.get("teil") == "ibkr")
|
|
# I2 No-Go-Tag: Zeitgeber aus und kein Protokoll ist leise; Zeitgeber an und kein Protokoll ist ein Befund.
|
|
def zeitgeber_aus(w, o):
|
|
prot(o).unlink()
|
|
schreibe(w / "dienste.json", json.dumps({"trading-paper-ibkr.timer": "inactive"}))
|
|
fall("Zeitgeber aus, kein Protokoll", zeitgeber_aus, befund=None, anzahl=0,
|
|
fakt=lambda f, e: f.get("nicht_gestartet") is True
|
|
and any("IBKR-Instanz heute nicht gestartet" in h for h in e.get("hinweise", [])))
|
|
fall("Zeitgeber an, kein Protokoll", lambda w, o: prot(o).unlink(), befund="die Instanz lief nicht")
|
|
fall("Zeitgeber aus, aber von Hand gelaufen",
|
|
lambda w, o: schreibe(w / "dienste.json", json.dumps({"trading-paper-ibkr.timer": "inactive"})),
|
|
befund=None, anzahl=0, fakt=lambda f, e: len(f.get("laeufe") or []) == 1 and not f.get("nicht_gestartet"))
|
|
fall("Kalender leer", lambda w, o: (schreibe(w / "kalender.json", "[]"), prot(o).unlink()), befund=None, anzahl=0)
|
|
# I3 Instanzmarke
|
|
fall("Marke fehlt", lambda w, o: (o / "data" / "paper" / "broker.txt").unlink(), befund="Instanzmarke")
|
|
fall("Marke alpaca", lambda w, o: schreibe(o / "data" / "paper" / "broker.txt", "alpaca\n"),
|
|
befund="nicht der Betriebsordner der IBKR-Instanz")
|
|
# I4 Register
|
|
fall("gesperrt nach Red Button",
|
|
register("UPDATE killswitch_zustand SET zustand = 'gesperrt', ausloeser = 'pascal'",
|
|
"INSERT INTO killswitch_ereignisse (zeit, stufe, ausloeser, grund, ergebnis, erfolgreich) "
|
|
"VALUES ('2026-09-23T15:00:00+00:00', 'glattstellen', 'pascal', 'Probe', '{}', 1)"),
|
|
befund="IBKR-Killswitch steht auf gesperrt", auch=("letztes Ereignis glattstellen von pascal",))
|
|
fall("gesperrt ohne Ereignis",
|
|
register("UPDATE killswitch_zustand SET zustand = 'gesperrt'", "DELETE FROM killswitch_ereignisse"),
|
|
befund="IBKR-Killswitch steht auf gesperrt", auch=("ohne Ereignis im Register",))
|
|
fall("Killswitch unbekannter Zustand", register("UPDATE killswitch_zustand SET zustand = 'kaputt'"),
|
|
befund="IBKR-Killswitch in unbekanntem Zustand")
|
|
fall("Register fehlt", lambda w, o: (o / "data" / "registry.db").unlink(), befund="IBKR-Register data/registry.db fehlt")
|
|
fall("Register ohne Ereignistabelle", register("DROP TABLE killswitch_ereignisse"),
|
|
befund="IBKR-Killswitch nicht geprueft")
|
|
# I5 Ende je Lauf, Handel, Schlussposition
|
|
fall("Lauf endete mit abbruch",
|
|
lambda w, o: zeilen_aendern(o, lambda z: [s.replace('"grund": "boersenschluss"', '"grund": "abbruch"')
|
|
if '"art": "ende"' in s else s for s in z]),
|
|
befund="Lauf endete mit abbruch")
|
|
fall("nie aktiv",
|
|
lambda w, o: zeilen_aendern(o, lambda z: [s.replace('"killswitch": "aktiv"', '"killswitch": "angehalten"') for s in z]),
|
|
befund="Tag ohne Handel")
|
|
# Das Journal meldet Position 0, das Protokoll eine Restposition: Die Schlussposition kommt aus dem
|
|
# Protokoll, unabhaengig vom Journal (Anforderung Nr. 5, "Position 0").
|
|
fall("Restposition im Protokoll bei sauberem Journal",
|
|
lambda w, o: zeilen_aendern(o, lambda z: [s.replace('"position": 0', '"position": 10')
|
|
if '"art": "ende"' in s else s for s in z]),
|
|
befund="endete laut Protokoll mit Position 10", anzahl=1)
|
|
# Ein Lauf ohne Zeile ende: pruefe_laeufe ueberspringt ihn, im Teil ibkr meldet ihn
|
|
# pruefe_schlusspositionen (Nachkritik IB2).
|
|
fall("Lauf ohne Zeile ende",
|
|
lambda w, o: zeilen_aendern(o, lambda z: [s for s in z if '"art": "ende"' not in s]),
|
|
befund="Lauf ohne Zeile ende im Protokoll")
|
|
# I6 Journal
|
|
fall("Journal Exit 4", lambda w, o: schreibe(w / "journal.txt", IBKR_JOURNAL_GUT.replace("Exit 0", "Exit 4")),
|
|
befund="Exit 4")
|
|
fall("Journal ohne Ende", lambda w, o: schreibe(w / "journal.txt", "gestartet\n"),
|
|
befund="von trading-paper-ibkr.service an diesem Tag")
|
|
fall("Journal Failed with result",
|
|
lambda w, o: schreibe(w / "journal.txt", IBKR_JOURNAL_GUT + "trading-paper-ibkr.service: Failed with result 'exit-code'.\n"),
|
|
befund="Journal: trading-paper-ibkr.service endete mit 'Failed with result'")
|
|
fall("Journal Traceback",
|
|
lambda w, o: schreibe(w / "journal.txt", IBKR_JOURNAL_GUT + "Traceback (most recent call last):\n File x\nValueError: y\n"),
|
|
befund="zeigt 'Traceback")
|
|
fall("Journal Marke passt nicht zur Konfiguration",
|
|
lambda w, o: schreibe(w / "journal.txt", IBKR_JOURNAL_GUT + "Dieser Ordner ist die IBKR-Instanz, die Konfiguration "
|
|
"waehlt aber [broker] wahl = 'alpaca'. Nichts wird gebaut, das Alpaca-Konto bleibt unberuehrt.\n"),
|
|
befund="zeigt 'Nichts wird gebaut'")
|
|
# I7 Konto am Gateway
|
|
fall("Konto 2 Positionen",
|
|
konto(IBKR_KONTO_GUT.replace("Positionen: 0\n", "Positionen: 2\n AAPL.XNAS 10\n KO.XNYS 10\n")),
|
|
befund="Beim IBKR-Gateway stehen 2 Position(en)", nicht=("AAPL.XNAS 10", "KO.XNYS 10"))
|
|
fall("Konto 1 offene Order", konto(IBKR_KONTO_GUT.replace("Offene Orders: 0", "Offene Orders: 1")),
|
|
befund="Beim IBKR-Gateway stehen 1 offene Order(s)")
|
|
fall("Konto ohne Zeilen", konto("verbunden: Gateway 127.0.0.1:4004\nERGEBNIS: gelesen\n"),
|
|
befund="Konto am Gateway nicht gelesen")
|
|
fall("Konto Voraussetzung fehlt", konto("Voraussetzung fehlt: config/paper-ibkr.toml fehlt\n", 4),
|
|
befund="endete mit Exit 4", auch=("Voraussetzung fehlt",))
|
|
fall("Konto BEFUND mit Kontonummer wird maskiert",
|
|
konto(IBKR_KONTO_GUT + f"BEFUND: Gateway meldet das Konto {KONTO_ERFUNDEN} als gesperrt\n", 1),
|
|
befund="[konto]", nicht=(KONTO_ERFUNDEN,))
|
|
# Die echte Form hat drei Buchstaben vorn und sechs Ziffern (Pruefung IB2); erfunden, wie oben.
|
|
fall("Konto BEFUND mit Kontonummer in der echten Form wird maskiert",
|
|
konto(IBKR_KONTO_GUT + f"BEFUND: Gateway meldet das Konto {KONTO_ERFUNDEN_LANG} als gesperrt\n", 1),
|
|
befund="[konto]", nicht=(KONTO_ERFUNDEN_LANG,))
|
|
fall("im Handelsfenster wird das Konto nicht gelesen", konto("kaputt", 9), befund=None, anzahl=0, jetzt=IM_HANDEL,
|
|
fakt=lambda f, e: "konto" not in f and any("nicht gelesen" in h for h in e.get("hinweise", [])))
|
|
# Bedienung
|
|
fall("Betriebsordner fehlt", lambda w, o: shutil.rmtree(o), befund="Betriebsordner der IBKR-Instanz fehlt", anzahl=1)
|
|
fall("Tag falsch", nichts, befund="abgebrochen", exit_code=2, tag="2026-13-40")
|
|
|
|
# Meldeweg des Teils ibkr ohne --ohne-meldung, gegen den lokalen Probeserver: Titel beginnt mit
|
|
# "IBKR Abendkontrolle", Prioritaet hoch (Anforderung Nr. 5). Eine Zeile kennung in der kopierten
|
|
# config/paper.toml faellt heraus, sonst stuende sie vor dem Titel.
|
|
with tempfile.TemporaryDirectory() as tmp_server:
|
|
mitschrift = Path(tmp_server) / "mitschrift.jsonl"
|
|
port = 18766
|
|
server = subprocess.Popen([sys.executable, str(HIER / "ntfy_probe_server.py"), str(port), str(mitschrift)],
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
try:
|
|
import socket
|
|
import time
|
|
for _ in range(50):
|
|
try:
|
|
socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
|
|
break
|
|
except OSError:
|
|
time.sleep(0.1)
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
wurzel = Path(tmp)
|
|
projekt = baue_projekt(wurzel)
|
|
ordner = baue_ibkr(wurzel)
|
|
(ordner / "data" / "paper" / "broker.txt").unlink() # ein Befund, damit gemeldet wird
|
|
text = (ECHTES_PROJEKT / "config" / "paper.toml").read_text(encoding="utf-8")
|
|
anker = 'ntfy_url = "https://ntfy.sh"'
|
|
if text.count(anker) != 1:
|
|
raise RuntimeError("Anker ntfy_url fehlt in config/paper.toml")
|
|
text = text.replace(anker, f'ntfy_url = "http://127.0.0.1:{port}"\nklartext_lokal = true')
|
|
text = "\n".join(z for z in text.split("\n") if not z.strip().startswith("kennung"))
|
|
schreibe(projekt / "config" / "paper.toml", text)
|
|
schreibe(wurzel / "secrets.toml", '[ntfy]\naktiv = true\nthema = "abendkontrolle-angriff"\n')
|
|
ergebnis = fahre_ibkr(wurzel, ordner, projekt=projekt, meldung=True,
|
|
env={"TRADING_GEHEIMNISSE": str(wurzel / "secrets.toml")})
|
|
zeilen = ([json.loads(z) for z in mitschrift.read_text(encoding="utf-8").split("\n") if z.strip()]
|
|
if mitschrift.exists() else [])
|
|
titel = [str(z.get("title") or "") for z in zeilen]
|
|
stufen = [z.get("priority") for z in zeilen]
|
|
gesendet = (ergebnis.get("fakten") or {}).get("meldung")
|
|
ok = (gesendet == "gesendet" and len(zeilen) == 1 and titel[0].startswith("IBKR Abendkontrolle")
|
|
and stufen == ["high"])
|
|
zeile = (f"{'OK ' if ok else 'FEHL'} IBKR Meldung mit Titel IBKR und Prioritaet hoch: "
|
|
f"titel={titel} prioritaet={stufen} meldung={gesendet}")
|
|
print(zeile)
|
|
if not ok:
|
|
abweichungen.append(zeile)
|
|
finally:
|
|
server.terminate()
|
|
server.wait(timeout=10)
|
|
|
|
|
|
# --- IBKR-Instanz (Paket IB2), Windows-Teil ----------------------------------------------------------
|
|
|
|
def ibkr_windows_faelle() -> None:
|
|
sys.path.insert(0, str(HIER))
|
|
import abendkontrolle as ak
|
|
from datetime import datetime
|
|
|
|
abdichten(ak)
|
|
|
|
gut = {"tag": TAG, "teil": "ibkr", "befunde": [], "hinweise": [], "exit": 0,
|
|
"fakten": {"handelstag": True,
|
|
"ende": {"grund": "boersenschluss", "exit": "0", "orders": "40", "position": "0"},
|
|
"laeufe": [{"takte_aktiv": 375}], "konto": {"positionen": 0, "orders": 0, "exit": 0},
|
|
"killswitch": {"zustand": "aktiv", "ausloeser": "automatik"}}}
|
|
alpaca = {"tag": TAG, "befunde": [], "hinweise": [], "exit": 0,
|
|
"fakten": {"handelstag": True,
|
|
"ende": {"grund": "boersenschluss", "exit": "0", "orders": "79", "position": "0"},
|
|
"laeufe": [{"takte_aktiv": 375}], "broker_offen": 0, "bericht": True,
|
|
"killswitch": {"zustand": "aktiv", "ausloeser": "automatik"}}}
|
|
|
|
def pruefe_fall(name, ok, grund):
|
|
zeile = f"{'OK ' if ok else 'FEHL'} W IBKR {name}: {grund}"
|
|
print(zeile)
|
|
if not ok:
|
|
abweichungen.append(zeile)
|
|
|
|
def lauf(name, ergebnis, *, teil="ibkr", pruefe):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
vault = Path(tmp)
|
|
(vault / "Inbox").mkdir()
|
|
eingabe = vault / "ergebnis.json"
|
|
eingabe.write_text(ergebnis if isinstance(ergebnis, str) else json.dumps(ergebnis), encoding="utf-8")
|
|
argumente = ["--vault", str(vault), "--tag", TAG, "--ohne-wsl", str(eingabe), "--jetzt", "2026-09-23T22:45:00"]
|
|
if teil is not None:
|
|
argumente += ["--teil", teil]
|
|
code = ak.main(argumente)
|
|
notiz = vault / "Inbox" / f"{TAG}.md"
|
|
text = notiz.read_text(encoding="utf-8") if notiz.exists() else ""
|
|
eigene = (vault / ak.ABLAGE_TEIL / f"{TAG}-ibkr.json").exists()
|
|
fremde = (vault / ak.ABLAGE_TEIL / f"{TAG}.json").exists()
|
|
try:
|
|
ok, grund = pruefe(code, text, eigene, fremde)
|
|
except Exception as ausnahme: # der Treiber selbst bricht nicht ab
|
|
ok, grund = False, f"Pruefung warf {ausnahme!r}"
|
|
pruefe_fall(name, ok, grund)
|
|
|
|
lauf("ohne Befund: eigene Zeile, eigene Datei", gut, pruefe=lambda c, t, e, f: (
|
|
c == 0 and e and not f and "— Abendkontrolle IBKR (automatisch): ohne Befund" in t
|
|
and "Konto beim Gateway: 0 Position(en), 0 offene Order(s)" in t
|
|
and "Abendkontrolle Trading Plattform" not in t, f"code {c} eigene {e} fremde {f} {t[-160:]!r}"))
|
|
lauf("mit Befund", {**gut, "befunde": ["Beim IBKR-Gateway stehen 2 Position(en)"]}, pruefe=lambda c, t, e, f: (
|
|
"Abendkontrolle IBKR (automatisch): **1 Befund(e)** #needs-review" in t
|
|
and " - Beim IBKR-Gateway stehen 2 Position(en)" in t and e, repr(t[-200:])))
|
|
lauf("nicht gestartet ist leise",
|
|
{"tag": TAG, "teil": "ibkr", "befunde": [], "exit": 0,
|
|
"hinweise": ["IBKR-Instanz heute nicht gestartet (trading-paper-ibkr.timer: inactive)"],
|
|
"fakten": {"handelstag": True, "nicht_gestartet": True, "zeitgeber": "inactive"}},
|
|
pruefe=lambda c, t, e, f: ("Abendkontrolle IBKR (automatisch): IBKR-Instanz heute nicht gestartet (Zeitgeber aus)." in t
|
|
and "#needs-review" not in t and e and c == 0, repr(t[-160:])))
|
|
lauf("kein Handelstag", {"tag": TAG, "teil": "ibkr", "befunde": [], "hinweise": [], "exit": 0,
|
|
"fakten": {"handelstag": False}},
|
|
pruefe=lambda c, t, e, f: ("Abendkontrolle IBKR (automatisch): kein Handelstag" in t and e, repr(t[-120:])))
|
|
lauf("Abbruch wird wiederholt", {"tag": TAG, "teil": "ibkr", "befunde": ["Abendkontrolle abgebrochen: X"],
|
|
"hinweise": [], "fakten": {}, "exit": 2},
|
|
pruefe=lambda c, t, e, f: (c == 1 and not e and "**1 Befund(e)**" in t, f"code {c} eigene {e}"))
|
|
lauf("gescheitert", "nicht json", pruefe=lambda c, t, e, f: (
|
|
c == 1 and not e and "Abendkontrolle IBKR (automatisch): **gescheitert**" in t, f"code {c} {t[-120:]!r}"))
|
|
# Ein Ergebnis des falschen Teils (etwa von einem WSL-Teil ohne IB2) landet nicht in der Datei der Instanz.
|
|
lauf("Ergebnis eines anderen Teils", {**gut, "teil": "alpaca"}, pruefe=lambda c, t, e, f: (
|
|
c == 1 and not e and not f and "Abendkontrolle IBKR (automatisch): **gescheitert**" in t,
|
|
f"code {c} eigene {e} fremde {f}"))
|
|
lauf("ohne --teil mit --ohne-wsl nur Alpaca", alpaca, teil=None, pruefe=lambda c, t, e, f: (
|
|
f and not e and "Abendkontrolle IBKR" not in t and "Abendkontrolle Trading Plattform" in t,
|
|
f"eigene {e} fremde {f}"))
|
|
|
|
# Beide Teile in derselben Notiz, Alpaca zuerst, je mit eigener Ergebnisdatei.
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
vault = Path(tmp)
|
|
(vault / "Inbox").mkdir()
|
|
(vault / "a.json").write_text(json.dumps(alpaca), encoding="utf-8")
|
|
(vault / "i.json").write_text(json.dumps(gut), encoding="utf-8")
|
|
gemeinsam = ["--vault", str(vault), "--tag", TAG, "--jetzt", "2026-09-23T22:45:00"]
|
|
ak.main(gemeinsam + ["--teil", "alpaca", "--ohne-wsl", str(vault / "a.json")])
|
|
ak.main(gemeinsam + ["--teil", "ibkr", "--ohne-wsl", str(vault / "i.json")])
|
|
text = (vault / "Inbox" / f"{TAG}.md").read_text(encoding="utf-8")
|
|
stelle_a = text.find("Abendkontrolle Trading Plattform")
|
|
stelle_i = text.find("Abendkontrolle IBKR")
|
|
ok = (0 <= stelle_a < stelle_i and text.count("## Agentenprotokoll") == 1
|
|
and (vault / ak.ABLAGE_TEIL / f"{TAG}.json").exists()
|
|
and (vault / ak.ABLAGE_TEIL / f"{TAG}-ibkr.json").exists())
|
|
pruefe_fall("beide Teile in einer Notiz", ok, f"alpaca {stelle_a} ibkr {stelle_i}")
|
|
|
|
# Relativ zu IBKR_ERSTER_TAG: Den Wert setzt der Koordinator vor Welle 1 (Planuebersicht IB2).
|
|
erster = ak.IBKR_ERSTER_TAG.isoformat()
|
|
pruefe_fall("IBKR_ERSTER_TAG ist ein Werktag nicht vor ERSTER_TAG",
|
|
ak.IBKR_ERSTER_TAG.weekday() < 5 and ak.IBKR_ERSTER_TAG >= ak.ERSTER_TAG, erster)
|
|
|
|
# Ohne --tag: am ersten Tag vor 22:30 ist fuer den Teil ibkr nichts faellig, auch kein Tag davor.
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
vault = Path(tmp)
|
|
(vault / "Inbox").mkdir()
|
|
(vault / "i.json").write_text(json.dumps(gut), encoding="utf-8")
|
|
ak.main(["--vault", str(vault), "--teil", "ibkr", "--ohne-wsl", str(vault / "i.json"), "--jetzt", f"{erster}T08:00:00"])
|
|
dateien = sorted(p.name for p in (vault / ak.ABLAGE_TEIL).glob("*-ibkr.json"))
|
|
pruefe_fall("ohne --tag nichts vor IBKR_ERSTER_TAG", dateien == [], str(dateien))
|
|
|
|
# faellige Tage je Teil, direkt
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
ablage = Path(tmp)
|
|
|
|
def ibkr_tage(jetzt):
|
|
return ak.faellige_tage(datetime.fromisoformat(jetzt), ablage, erster_tag=ak.IBKR_ERSTER_TAG,
|
|
endung=ak.IBKR_ENDUNG)
|
|
|
|
ergebnis = ibkr_tage(f"{erster}T08:00")
|
|
pruefe_fall("faellig: nichts vor IBKR_ERSTER_TAG", ergebnis == [], str(ergebnis))
|
|
ergebnis = ibkr_tage(f"{erster}T22:45")
|
|
pruefe_fall("faellig: der erste Tag nach 22:30", erster in ergebnis, str(ergebnis))
|
|
(ablage / "2026-09-29-ibkr.json").write_text("{}", encoding="utf-8")
|
|
ibkr = ibkr_tage("2026-09-30T08:00")
|
|
alpaca_tage = ak.faellige_tage(datetime.fromisoformat("2026-09-30T08:00"), ablage)
|
|
pruefe_fall("faellig: eigene Ergebnisdatei je Teil", "2026-09-29" not in ibkr and "2026-09-29" in alpaca_tage,
|
|
f"ibkr {ibkr} alpaca {alpaca_tage}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if sys.platform == "win32":
|
|
windows_faelle()
|
|
ibkr_windows_faelle()
|
|
else:
|
|
wsl_faelle()
|
|
ibkr_wsl_faelle()
|
|
print(f"ERGEBNIS: {len(abweichungen)} Abweichung(en)")
|
|
sys.exit(1 if abweichungen else 0)
|