Pruefwerkzeuge Trading Plattform: Abendkontrolle vom Docker-Host (Paket DH), abgedichtete Angriffstreiber; plankritik.js auf LF

- 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]>
This commit is contained in:
Pascal MichelandClaude Opus 5.5 committed 2026-09-26 14:53:53 +02:00
1 parent c115a13190
commit 59087fb235
9 files changed
+588 -16

No files matched your search

@@ -1,8 +1,9 @@
"""Startet die Abendkontrolle des Paper-Betriebs und schreibt ihr Ergebnis in die Tagesnotiz.
Koordinator-Werkzeug, nicht fuers Repo. Laeuft unter Windows mit pythonw.exe als geplante Aufgabe
"Trading Plattform - Abendkontrolle" (abendkontrolle_einrichten.ps1): werktags 22:45 und fuenf Minuten
nach jeder Anmeldung. Auftrag Pascal 2026-09-24: dauerhaft, uebersteht Neustarts, blockiert keine
"Trading Plattform - Abendkontrolle" (abendkontrolle_einrichten.ps1): werktags 22:45 (kommt ein Teil
vom Docker-Host, 23:15; seit Paket DH) und fuenf Minuten nach jeder Anmeldung. Auftrag Pascal
2026-09-24: dauerhaft, uebersteht Neustarts, blockiert keine
Updates -- deshalb eine Windows-Aufgabe (bleibt ueber Neustarts, holt verpasste Laeufe nach) und kein
Claude-Prozess (haelt keine Datei von Claude Code oder Desktop offen).
@@ -29,6 +30,7 @@ Augenblick schreibt, schuetzt das nicht (Zeitfenster Lesen bis Ersetzen, Millise
Annahme, benannt: Die Uhr dieses Rechners geht nach Berliner Zeit (tzutil: W. Europe Standard Time,
gelesen am 2026-09-24); ohne das Paket tzdata kennt Python unter Windows keine Zeitzonen-Datenbank.
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
[--quelle wsl|dockerhost] [--quelle-ibkr wsl|dockerhost]
"""
import argparse
@@ -55,6 +57,20 @@ FRUEHESTENS = time(22, 30)
WSL_TIMEOUT_S = 1200
KEIN_FENSTER = 0x08000000 # CREATE_NO_WINDOW: pythonw hat keine Konsole, wsl.exe soll keine oeffnen
UEBERSCHRIFT = "## Agentenprotokoll"
# Seit Paket DH (Trading Plattform 0.34.0): Nach dem Umzug auf den Docker-Host prueft der Host
# selbst (abendkontrolle-host.sh, werktags 16:30 New Yorker Zeit, beide Teile); diese Aufgabe
# liest nur noch sein Ergebnis ueber SSH. Der Schluessel darf dort genau einen Befehl
# (abendkontrolle-lesen.sh mit JJJJ-MM-TT oder JJJJ-MM-TT-ibkr), der Host ist ueber seinen
# Fingerabdruck festgelegt. Umgeschaltet wird je Teil: --quelle fuer alpaca, --quelle-ibkr fuer
# ibkr (ohne Angabe wie --quelle), weil die IBKR-Instanz an einem eigenen Abend umzieht.
QUELLEN = ("wsl", "dockerhost")
SSH = r"C:\Windows\System32\OpenSSH\ssh.exe"
SSH_ZIEL = "[email protected]"
SSH_PORT = "2498"
SSH_SCHLUESSEL = Path.home() / ".ssh" / "trading-dh-kontrolle"
# Ohne Leerzeichen im Wert: ssh trennt UserKnownHostsFile an Leerraum; ~ setzt ssh selbst ein.
SSH_BEKANNTE = "~/.ssh/known_hosts_trading_dh"
SSH_TIMEOUT_S = 120
# Seit Paket IB2: die IBKR-Instanz als zweiter Teil mit eigener Ergebnisdatei und eigener Zeile.
TEILE = ("alpaca", "ibkr")
# Der erste Tag, den der Teil ibkr prueft; gesetzt beim Einspielen von IB2 (Planuebersicht IB2).
@@ -97,6 +113,48 @@ def fahre_wsl(tag: str, teil: str = "alpaca") -> dict:
raise RuntimeError(f"keine JSON-Zeile vom WSL-Teil (Exit {lauf.returncode}; {' | '.join(fehler)[:300]})")
def fahre_dockerhost(tag: str, teil: str = "alpaca") -> dict:
"""Liest das Ergebnis, das die Abendkontrolle auf dem Docker-Host fuer <tag> und <teil> abgelegt hat.
Der Host legt je Tag <tag>.json (alpaca) und <tag>-ibkr.json (ibkr) ab; angefragt wird mit
genau dem Namen ohne .json. Exit 3 von abendkontrolle-lesen.sh heisst: Dafuer liegt dort noch
nichts. Das ist ein Fehlschlag wie jeder andere -- die Notiz bekommt #needs-review, und der
naechste Lauf versucht es erneut. BatchMode: nie eine Rueckfrage. StrictHostKeyChecking=yes:
nur der bekannte Host.
"""
ziel = tag + (IBKR_ENDUNG if teil == "ibkr" else "")
lauf = subprocess.run(
[
SSH,
"-p", SSH_PORT,
"-i", str(SSH_SCHLUESSEL),
"-o", "BatchMode=yes",
"-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=yes",
"-o", f"UserKnownHostsFile={SSH_BEKANNTE}",
"-o", "ConnectTimeout=20",
SSH_ZIEL,
ziel,
],
capture_output=True, timeout=SSH_TIMEOUT_S, creationflags=KEIN_FENSTER,
)
if lauf.returncode == 3:
raise RuntimeError(
f"auf dem Docker-Host liegt fuer {ziel} noch kein Ergebnis "
"(dort laeuft die Abendkontrolle werktags 16:30 New Yorker Zeit)"
)
ausgabe = lauf.stdout.decode("utf-8", errors="replace")
for zeile in reversed(ausgabe.splitlines()):
zeile = zeile.strip()
if zeile.startswith("{"):
try:
return json.loads(zeile)
except json.JSONDecodeError:
break
fehler = lauf.stderr.decode("utf-8", errors="replace").strip().splitlines()[-3:]
raise RuntimeError(f"keine JSON-Zeile vom Docker-Host (Exit {lauf.returncode}; {' | '.join(fehler)[:300]})")
def zeile_fuer(ergebnis: dict, uhrzeit: str) -> str:
kopf = f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch)"
befunde = ergebnis.get("befunde") or []
@@ -218,6 +276,14 @@ def main(argv=None) -> int:
zerleger.add_argument("--ohne-wsl", help="Ergebnis aus einer JSON-Datei statt aus WSL (Angriffsfaelle)")
zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt statt der Uhr (Angriffsfaelle)")
zerleger.add_argument("--teil", choices=TEILE, help="nur diesen Teil; ohne Angabe beide, mit --ohne-wsl nur alpaca")
zerleger.add_argument(
"--quelle", choices=QUELLEN, default="wsl",
help="wsl (Vorgabe): pruefen in WSL; dockerhost: das Ergebnis des Docker-Hosts ueber SSH lesen",
)
zerleger.add_argument(
"--quelle-ibkr", choices=QUELLEN,
help="die Quelle fuer den Teil ibkr; ohne Angabe dieselbe wie --quelle",
)
argumente = zerleger.parse_args(argv)
vault = Path(argumente.vault)
ablage = vault / ABLAGE_TEIL
@@ -238,7 +304,9 @@ def main(argv=None) -> int:
# schreibbar, lauf.log gesperrt), laeuft der naechste trotzdem (Pruefung IB2).
try:
logge(ablage, f"{vorsatz}start, faellig: {tage}")
code = max(code, fahre_teil(teil, tage, jetzt, vault, ablage, argumente.ohne_wsl))
# Seit Paket DH: die Quelle je Teil; die IBKR-Instanz zieht an einem eigenen Abend um.
quelle = (argumente.quelle_ibkr or argumente.quelle) if teil == "ibkr" else argumente.quelle
code = max(code, fahre_teil(teil, tage, jetzt, vault, ablage, argumente.ohne_wsl, quelle))
except Exception as ausnahme:
code = 1
try:
@@ -248,8 +316,12 @@ def main(argv=None) -> int:
return code
def fahre_teil(teil: str, tage: list[str], jetzt: datetime, vault: Path, ablage: Path, ohne_wsl: str | None) -> int:
"""Ein Teil fuer alle faelligen Tage: Ergebnisdatei, Zeile in der Notiz, lauf.log."""
def fahre_teil(teil: str, tage: list[str], jetzt: datetime, vault: Path, ablage: Path, ohne_wsl: str | None,
quelle: str = "wsl") -> int:
"""Ein Teil fuer alle faelligen Tage: Ergebnisdatei, Zeile in der Notiz, lauf.log.
quelle (seit Paket DH): wsl prueft in WSL, dockerhost liest das Ergebnis des Docker-Hosts.
"""
endung = IBKR_ENDUNG if teil == "ibkr" else ""
zeile_von = zeile_fuer_ibkr if teil == "ibkr" else zeile_fuer
vorsatz = "" if teil == "alpaca" else f"{teil} "
@@ -259,6 +331,8 @@ def fahre_teil(teil: str, tage: list[str], jetzt: datetime, vault: Path, ablage:
try:
if ohne_wsl:
ergebnis = json.loads(Path(ohne_wsl).read_text(encoding="utf-8"))
elif quelle == "dockerhost":
ergebnis = fahre_dockerhost(tag, teil)
else:
ergebnis = fahre_wsl(tag, teil)
if not isinstance(ergebnis, dict) or not isinstance(ergebnis.get("befunde"), list):