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

+15 -2
View File
@@ -1,7 +1,7 @@
export const meta = { export const meta = {
name: 'plankritik', name: 'plankritik',
description: 'Liest einen Planordner mit getrennten pruefer-Linsen, gleichzeitig nach Verbrauch bis vier, fasst doppelte Befunde zusammen und lässt jeden wichtigen oder blockierenden Befund von einem zweiten pruefer zu widerlegen versuchen, höchstens zwei gleichzeitig', description: 'Plankritik: pruefer-Linsen nach Verbrauch, doppelte Befunde zusammengefasst, wichtige und blockierende von einem zweiten pruefer widerlegt',
whenToUse: 'Schritt 2 der Standardschleife vor dem ersten Executer, Pflicht ab etwa drei Tasks (AI/Regelwerk/codeworkflow.md, "Plankritik vor dem ersten Executer"). Args: {projekt, planordner, spec?, kopie?, materialisieren?: {ohne: [relpfade], tasks?: [taskdateien]}, suite?, linsen?, plaetze?: 1..4, verifizieren?: true, verifizieren_ab?: "wichtig"|"blockierend", effort?, werkzeuge?: Ordner der Planwerkzeuge (Pfad, keine Ausgabe; Vorgabe D:/Claude/.vault-tools/plan-kopie), zusatz?}. Ohne kopie baut materialisieren sie selbst (kopie_bauen.py plus plan_materialisiere.py); ohne() muss Geheimnisdateien und große Datenordner nennen. plaetze begrenzt die gleichzeitigen Linsen; verbrauch.js senkt sie nach dem 5-Stunden-Fenster und der Woche. Rückgabe mit status fertig oder gebremst; ein gebremster Lauf wird neu gestartet, nicht fortgesetzt.', whenToUse: 'Schritt 2 der Standardschleife vor dem ersten Executer, Pflicht ab etwa drei Tasks (AI/Regelwerk/codeworkflow.md). Args: {projekt, planordner, spec?, kopie?, materialisieren?: {ohne: [relpfade], tasks?: [taskdateien]}, suite?, linsen?, plaetze?: 1..4, verifizieren?: true, verifizieren_ab?: "wichtig"|"blockierend", effort?, werkzeuge?: Ordner der Planwerkzeuge, zusatz?}. Einzelheiten im Kommentar unter meta.',
phases: [ phases: [
{ title: 'Vorbereitung', detail: 'Hilfsagent: Kopie bauen, Plan anwenden, Planwerkzeuge fahren' }, { title: 'Vorbereitung', detail: 'Hilfsagent: Kopie bauen, Plan anwenden, Planwerkzeuge fahren' },
{ title: 'Plankritik', detail: 'je Linse ein pruefer, gleichzeitig nach Verbrauch' }, { title: 'Plankritik', detail: 'je Linse ein pruefer, gleichzeitig nach Verbrauch' },
@@ -9,6 +9,19 @@ export const meta = {
], ],
} }
// Diese Datei traegt LF. Mit CRLF im Arbeitsbaum (seit einer Nachbesserung am 2026-09-24) lehnte
// die Rechtepruefung sie per Name wie per scriptPath mit "script contains control characters" ab:
// Das CR ist das Steuerzeichen. Belegt am 2026-09-26 (Erinnerung
// gespeicherter-workflow-per-name-scheitert). Die Einzelheiten zu den Args stehen hier, damit meta
// kurz bleibt:
// - werkzeuge ist ein Pfad, keine Ausgabe; Vorgabe D:/Claude/.vault-tools/plan-kopie.
// - Ohne kopie baut materialisieren sie selbst (kopie_bauen.py plus plan_materialisiere.py);
// ohne() muss Geheimnisdateien und große Datenordner nennen.
// - plaetze begrenzt die gleichzeitigen Linsen; verbrauch.js senkt sie nach dem 5-Stunden-Fenster
// und der Woche.
// - Rückgabe mit status fertig oder gebremst; ein gebremster Lauf wird neu gestartet, nicht
// fortgesetzt.
//
// Rollenparameter: model wird immer gesetzt (Stamm). Der Effort der pruefer kommt aus dem // Rollenparameter: model wird immer gesetzt (Stamm). Der Effort der pruefer kommt aus dem
// Frontmatter von .claude/agents/pruefer.md; args.effort überschreibt ihn nur bewusst. // Frontmatter von .claude/agents/pruefer.md; args.effort überschreibt ihn nur bewusst.
// Der Hilfsagent hat keinen agentType und bekommt deshalb seinen Effort mit (low: gemessen // Der Hilfsagent hat keinen agentType und bekommt deshalb seinen Effort mit (low: gemessen
@@ -166,3 +166,16 @@ Seit Paket IB2 prüft die Abendkontrolle zwei Teile je Tag, nacheinander: den Al
| `abendkontrolle_mutanten.py` | dazu Mutanten der IBKR-Teile beider Dateien, auch für Schlussposition, Titelpräfix, Meldeweiche und die Prüfung des Teils; die Anker „keine Ergebnisdatei“ und „Wochenende mit“ folgen dem Code aus IB2 | wie oben | | `abendkontrolle_mutanten.py` | dazu Mutanten der IBKR-Teile beider Dateien, auch für Schlussposition, Titelpräfix, Meldeweiche und die Prüfung des Teils; die Anker „keine Ergebnisdatei“ und „Wochenende mit“ folgen dem Code aus IB2 | wie oben |
Die Zahlen der Fälle und Mutanten im Abschnitt vom 2026-09-24 gelten für den Stand vor IB2; wie viele es jetzt sind, sagt die Zeile `ERGEBNIS` des Laufs. Was der IBKR-Teil nicht sieht: einen Tagesbericht (die Instanz hat keine Tagesauswertung), `handelstag_auswertung.py` und den Dienst `ibkr-gateway.service`. Holt die Kontrolle einen Tag während der New Yorker Handelszeit nach (Anmeldung am Nachmittag), liest sie das Konto nicht; der nächste Abendlauf liest es für seinen eigenen Tag. Den Zeitgeber liest sie zum Zeitpunkt der Kontrolle, nicht für den geprüften Tag: Beim Nachholen nach einem Umschalten kann sie einen Ausfall als „nicht gestartet“ werten oder einen No-Go-Tag als Befund. Die Zahlen der Fälle und Mutanten im Abschnitt vom 2026-09-24 gelten für den Stand vor IB2; wie viele es jetzt sind, sagt die Zeile `ERGEBNIS` des Laufs. Was der IBKR-Teil nicht sieht: einen Tagesbericht (die Instanz hat keine Tagesauswertung), `handelstag_auswertung.py` und den Dienst `ibkr-gateway.service`. Holt die Kontrolle einen Tag während der New Yorker Handelszeit nach (Anmeldung am Nachmittag), liest sie das Konto nicht; der nächste Abendlauf liest es für seinen eigenen Tag. Den Zeitgeber liest sie zum Zeitpunkt der Kontrolle, nicht für den geprüften Tag: Beim Nachholen nach einem Umschalten kann sie einen Ausfall als „nicht gestartet“ werten oder einen No-Go-Tag als Befund.
## Paket DH (Trading Plattform 0.34.0): Abendkontrolle vom Docker-Host
Nach dem Umzug auf den Docker-Host (Hyper-V-VM) prüft der Host selbst: `deploy/docker-host/abendkontrolle-host.sh` aus dem Repo startet werktags 16:30 New Yorker Zeit `abendkontrolle_wsl.py` im Abbild der Plattform, mit `--journal` und `--dienste` statt `journalctl` und `systemctl` im Container — erst den Teil alpaca im Projekt `trading-alpaca`, dann den Teil ibkr im Projekt `trading-ibkr`, dort im Netz des Gateways und mit `--ibkr-python python`, weil es das Venv aus WSL im Abbild nicht gibt. Dafür liegen `abendkontrolle_wsl.py` und `handelstag_auswertung.py` als Kopie unter `/opt/trading-plattform/werkzeuge/` auf dem Host (Umzugs-Drehbuch, 4.4); wer eines davon hier ändert, kopiert es dort neu. Die Ergebnisse liegen je Tag unter `/var/lib/trading-abendkontrolle/<tag>.json` und `<tag>-ibkr.json`; scharf ist jeder Teil erst ab dem Datum in `erster-tag` beziehungsweise `erster-tag-ibkr` dort.
| Datei | Was sie tut | Aufruf |
|---|---|---|
| `abendkontrolle.py` | seit Paket DH mit `--quelle wsl` (Vorgabe, wie bisher) oder `--quelle dockerhost` für den Teil alpaca und `--quelle-ibkr` für den Teil ibkr (ohne Angabe wie `--quelle`). `dockerhost` liest das Ergebnis des Hosts über `ssh.exe` als Benutzer `kontrolle` mit dem Schlüssel `~/.ssh/trading-dh-kontrolle`, `BatchMode` und `StrictHostKeyChecking=yes` gegen `~/.ssh/known_hosts_trading_dh`, angefragt mit `JJJJ-MM-TT` oder `JJJJ-MM-TT-ibkr`. Auf dem Host darf der Schlüssel nur `abendkontrolle-lesen.sh`. Exit 3 dort heißt „noch kein Ergebnis“: Notiz mit `#needs-review`, der nächste Lauf versucht es erneut | `python abendkontrolle.py --quelle dockerhost [--quelle-ibkr wsl] [--tag JJJJ-MM-TT]` |
| `abendkontrolle_einrichten.ps1` | seit Paket DH mit `-Quelle` und `-QuelleIbkr` (je `wsl` oder `dockerhost`); kommt ein Teil vom Host, liegt der Abendlauf auf 23:15. Die letzte Zeile nennt beide Quellen | `powershell -NoProfile -File abendkontrolle_einrichten.ps1 -Quelle dockerhost -QuelleIbkr wsl` |
| `abendkontrolle_dockerhost_angriffe.py` | zwölf Fälle ohne Netz, abgedichtet: `wsl.exe` zeigt ins Leere, und `fahre_wsl` lehnt ohne eigene Attrappe des Falls ab, ein Aufruf des echten Wegs ist eine Abweichung (so auch der Windows-Teil von `abendkontrolle_angriffe.py`). Die Fälle: beide Ergebnisse landen in Ablage und Notiz, Argumente von ssh je Teil, Exit 3 als Befund, Müll als Befund, fällige Tage einzeln je Teil, ohne Schalter bleibt es WSL, `--quelle-ibkr wsl` hält die Instanz in WSL, ein Ergebnis des falschen Teils vom Host, unbekannte Quelle, Frist und Aufgabe passen zueinander. Fängt je Fall auch `SystemExit` | `python abendkontrolle_dockerhost_angriffe.py` unter Windows |
| `abendkontrolle_ibkr_python_angriffe.py` | seit der Prüfung vom 2026-09-26 mit zweiter Sperre: `IBKR_ORDNER` zeigt ins Leere; dazu dichtet `rueckweg_aapl_angriffe.py` seither `WSL` und `WSL_SKRIPT` im Modul `rueckweg_aapl` ab, und `abendkontrolle_angriffe.py` räumt seinen leeren Ordner im Temp wieder weg. Sonst: vier Fälle ohne Netz für `--ibkr-python` von `abendkontrolle_wsl.py`: Schalter, Vorgabe `IBKR_PY`, `ibkr_pruefen.py lesen` läuft mit dem genannten Interpreter (Ersatzskript im Wegwerf-Ordner), der Ablauf reicht den Schalter weiter | `python abendkontrolle_ibkr_python_angriffe.py` in WSL im Venv |
Was es nicht tut: Es prüft nicht, ob der Host die Kontrolle überhaupt fährt — bleibt sie dort aus, steht jeden Abend „noch kein Ergebnis“ in der Notiz, und das ist der Hinweis. Steht `-QuelleIbkr` falsch, fällt das nicht immer auf: Prüft die Aufgabe den Teil ibkr in WSL, obwohl die Instanz schon auf dem Host läuft, meldet sie ihn dort nur leise als „nicht gestartet“, weil der Zeitgeber in WSL aus ist. Ein Anmelde-Lauf zwischen 22:30 und dem Ende der Prüfung auf dem Host bekommt einmal „noch kein Ergebnis“, weil die Frist `FRUEHESTENS` für beide Quellen gilt (die Mutanten suchen genau diese Zeile).
@@ -1,8 +1,9 @@
"""Startet die Abendkontrolle des Paper-Betriebs und schreibt ihr Ergebnis in die Tagesnotiz. """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 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 "Trading Plattform - Abendkontrolle" (abendkontrolle_einrichten.ps1): werktags 22:45 (kommt ein Teil
nach jeder Anmeldung. Auftrag Pascal 2026-09-24: dauerhaft, uebersteht Neustarts, blockiert keine 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 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). 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, 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. 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>] 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 import argparse
@@ -55,6 +57,20 @@ FRUEHESTENS = time(22, 30)
WSL_TIMEOUT_S = 1200 WSL_TIMEOUT_S = 1200
KEIN_FENSTER = 0x08000000 # CREATE_NO_WINDOW: pythonw hat keine Konsole, wsl.exe soll keine oeffnen KEIN_FENSTER = 0x08000000 # CREATE_NO_WINDOW: pythonw hat keine Konsole, wsl.exe soll keine oeffnen
UEBERSCHRIFT = "## Agentenprotokoll" 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. # Seit Paket IB2: die IBKR-Instanz als zweiter Teil mit eigener Ergebnisdatei und eigener Zeile.
TEILE = ("alpaca", "ibkr") TEILE = ("alpaca", "ibkr")
# Der erste Tag, den der Teil ibkr prueft; gesetzt beim Einspielen von IB2 (Planuebersicht IB2). # 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]})") 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: def zeile_fuer(ergebnis: dict, uhrzeit: str) -> str:
kopf = f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch)" kopf = f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch)"
befunde = ergebnis.get("befunde") or [] 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("--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("--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("--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) argumente = zerleger.parse_args(argv)
vault = Path(argumente.vault) vault = Path(argumente.vault)
ablage = vault / ABLAGE_TEIL ablage = vault / ABLAGE_TEIL
@@ -238,7 +304,9 @@ def main(argv=None) -> int:
# schreibbar, lauf.log gesperrt), laeuft der naechste trotzdem (Pruefung IB2). # schreibbar, lauf.log gesperrt), laeuft der naechste trotzdem (Pruefung IB2).
try: try:
logge(ablage, f"{vorsatz}start, faellig: {tage}") 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: except Exception as ausnahme:
code = 1 code = 1
try: try:
@@ -248,8 +316,12 @@ def main(argv=None) -> int:
return code return code
def fahre_teil(teil: str, tage: list[str], jetzt: datetime, vault: Path, ablage: Path, ohne_wsl: str | None) -> int: def fahre_teil(teil: str, tage: list[str], jetzt: datetime, vault: Path, ablage: Path, ohne_wsl: str | None,
"""Ein Teil fuer alle faelligen Tage: Ergebnisdatei, Zeile in der Notiz, lauf.log.""" 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 "" endung = IBKR_ENDUNG if teil == "ibkr" else ""
zeile_von = zeile_fuer_ibkr if teil == "ibkr" else zeile_fuer zeile_von = zeile_fuer_ibkr if teil == "ibkr" else zeile_fuer
vorsatz = "" if teil == "alpaca" else f"{teil} " 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: try:
if ohne_wsl: if ohne_wsl:
ergebnis = json.loads(Path(ohne_wsl).read_text(encoding="utf-8")) ergebnis = json.loads(Path(ohne_wsl).read_text(encoding="utf-8"))
elif quelle == "dockerhost":
ergebnis = fahre_dockerhost(tag, teil)
else: else:
ergebnis = fahre_wsl(tag, teil) ergebnis = fahre_wsl(tag, teil)
if not isinstance(ergebnis, dict) or not isinstance(ergebnis.get("befunde"), list): if not isinstance(ergebnis, dict) or not isinstance(ergebnis.get("befunde"), list):
@@ -8,6 +8,7 @@ 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. Exit 0, wenn jeder Fall so ausgeht wie erwartet; sonst 1 mit der Liste der Abweichungen.
""" """
import atexit
import json import json
import os import os
import shutil import shutil
@@ -291,11 +292,66 @@ def wsl_faelle() -> None:
# --- Windows-Teil -------------------------------------------------------------------------------- # --- 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: def windows_faelle() -> None:
sys.path.insert(0, str(HIER)) sys.path.insert(0, str(HIER))
import abendkontrolle as ak import abendkontrolle as ak
from datetime import datetime 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, gut = {"tag": TAG, "befunde": [], "hinweise": ["auto_fortsetzen: fortgesetzt"], "exit": 0,
"fakten": {"handelstag": True, "ende": {"grund": "boersenschluss", "exit": "0", "orders": "79", "position": "0"}, "fakten": {"handelstag": True, "ende": {"grund": "boersenschluss", "exit": "0", "orders": "79", "position": "0"},
"laeufe": [{"takte_aktiv": 375}], "broker_offen": 0, "bericht": True, "laeufe": [{"takte_aktiv": 375}], "broker_offen": 0, "bericht": True,
@@ -637,6 +693,8 @@ def ibkr_windows_faelle() -> None:
import abendkontrolle as ak import abendkontrolle as ak
from datetime import datetime from datetime import datetime
abdichten(ak)
gut = {"tag": TAG, "teil": "ibkr", "befunde": [], "hinweise": [], "exit": 0, gut = {"tag": TAG, "teil": "ibkr", "befunde": [], "hinweise": [], "exit": 0,
"fakten": {"handelstag": True, "fakten": {"handelstag": True,
"ende": {"grund": "boersenschluss", "exit": "0", "orders": "40", "position": "0"}, "ende": {"grund": "boersenschluss", "exit": "0", "orders": "40", "position": "0"},
@@ -0,0 +1,291 @@
"""Angriffsfaelle fuer die Quellen wsl und dockerhost von abendkontrolle.py (Paket DH), unter Windows, ohne Netz.
Koordinator-Werkzeug, nicht fuers Repo. Ersetzt ssh.exe durch eine .cmd-Attrappe, die ein kleines
Python-Skript startet; der Vault ist ein Wegwerf-Ordner. Die Attrappe schreibt jeden Aufruf mit
und antwortet je nach ANGRIFF_ART mit einem Ergebnis des angefragten Teils (JJJJ-MM-TT oder
JJJJ-MM-TT-ibkr), mit Exit 3 (noch nichts da), mit Muell oder mit einem Ergebnis des falschen
Teils. Der Treiber faengt je Fall auch SystemExit: Gegen ein abendkontrolle.py ohne --quelle
bricht argparse damit ab, und das soll ein Fall sein, nicht das Ende des Laufs.
Abgedichtet (Plankritik DH, 2026-09-26): WSL zeigt in jedem Lauf ins Leere, und fahre_wsl ist
ohne eigene Attrappe des Falls eine Attrappe, die ablehnt; ein Aufruf des echten Wegs ist eine
Abweichung, kein Lauf. Zwei Faelle belegen beide Sperren.
Aufruf: python abendkontrolle_dockerhost_angriffe.py
Exit 0 ohne Abweichung, 2 mit Abweichung.
"""
import contextlib
import importlib.util
import io
import json
import os
import sys
import tempfile
from datetime import datetime
from pathlib import Path
HIER = Path(__file__).resolve().parent
ATTRAPPE_PY = r'''
import json
import os
import sys
with open(os.environ["ANGRIFF_PROTOKOLL"], "a", encoding="utf-8") as datei:
datei.write(json.dumps(sys.argv[1:]) + "\n")
art = os.environ.get("ANGRIFF_ART", "gut")
if art == "fehlt":
sys.stderr.write("abendkontrolle-lesen: fuer diesen Tag liegt noch kein Ergebnis\n")
sys.exit(3)
if art == "muell":
print("kein json")
sys.exit(0)
ziel = sys.argv[-1]
ibkr = ziel.endswith("-ibkr")
tag = ziel[:-5] if ibkr else ziel
teil = "ibkr" if ibkr and art != "falscher_teil" else "alpaca"
print(json.dumps({"tag": tag, "teil": teil, "befunde": [], "hinweise": [], "fakten": {"handelstag": True}, "exit": 0}))
'''
FAELLE = []
def fall(funktion):
FAELLE.append(funktion)
return funktion
def lade():
spec = importlib.util.spec_from_file_location("abendkontrolle_unter_angriff", HIER / "abendkontrolle.py")
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
return modul
def lauf(art, *argumente, modul=None):
"""Ein Lauf von main() in einem Wegwerf-Vault; liefert Exit, Notiz, Ergebnisdateien und ssh-Aufrufe."""
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
vault = wurzel / "vault"
(vault / "Inbox").mkdir(parents=True)
protokoll = wurzel / "ssh.jsonl"
attrappe = wurzel / "ssh_attrappe.py"
attrappe.write_text(ATTRAPPE_PY, encoding="utf-8")
cmd = wurzel / "ssh_attrappe.cmd"
cmd.write_text(f'@"{sys.executable}" "{attrappe}" %*\r\n', encoding="utf-8")
modul = modul or lade()
modul.SSH = str(cmd)
# Abdichtung (Plankritik DH, 2026-09-26): Der echte WSL-Weg lief sonst gegen den echten
# Projektordner, fragte das Gateway und schickte echte ntfy-Meldungen. wsl.exe und das
# Skript zeigen ins Leere, und fahre_wsl ist eine Attrappe, die jeden Aufruf samt beiden
# Pfaden mitschreibt und ablehnt -- ausser ein Fall hat seine eigene gesetzt (wsl_attrappe).
modul.WSL = str(wurzel / "kein-wsl" / "wsl.exe")
modul.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
unerwartet = []
if not getattr(modul.fahre_wsl, "attrappe", False):
def wsl_verboten(tag, teil="alpaca"):
unerwartet.append((tag, teil, modul.WSL, modul.WSL_SKRIPT))
raise RuntimeError("echter WSL-Weg ohne Attrappe")
modul.fahre_wsl = wsl_verboten
os.environ["ANGRIFF_PROTOKOLL"] = str(protokoll)
os.environ["ANGRIFF_ART"] = art
code = modul.main(["--vault", str(vault), *argumente])
if unerwartet:
raise AssertionError(f"WSL-Weg ohne Attrappe gerufen: {unerwartet}")
notizen = sorted((vault / "Inbox").glob("*.md"))
notiz = "\n".join(n.read_text(encoding="utf-8") for n in notizen)
ablage = vault / modul.ABLAGE_TEIL
ergebnisse = sorted(p.name for p in ablage.glob("*.json")) if ablage.exists() else []
aufrufe = []
if protokoll.exists():
aufrufe = [json.loads(z) for z in protokoll.read_text(encoding="utf-8").splitlines() if z.strip()]
return code, notiz, ergebnisse, aufrufe
def wsl_attrappe(modul, gerufen):
"""Ersetzt fahre_wsl im Modul; schreibt (tag, teil) mit und liefert ein Ergebnis des Teils."""
def fahre_wsl(tag, teil="alpaca"):
gerufen.append((tag, teil))
return {"tag": tag, "teil": teil, "befunde": [], "hinweise": [], "fakten": {}, "exit": 0}
fahre_wsl.attrappe = True
modul.fahre_wsl = fahre_wsl
@fall
def ergebnis_vom_host_landet_in_ablage_und_notiz():
code, notiz, ergebnisse, aufrufe = lauf(
"gut", "--quelle", "dockerhost", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30"
)
assert code == 0, code
assert ergebnisse == ["2026-10-05-ibkr.json", "2026-10-05.json"], ergebnisse
assert "Abendkontrolle Trading Plattform (automatisch): ohne Befund" in notiz, notiz
assert "Abendkontrolle IBKR (automatisch): ohne Befund" in notiz, notiz
assert len(aufrufe) == 2, aufrufe
@fall
def ssh_mit_festem_schluessel_ohne_rueckfrage_nur_zum_bekannten_host():
_, _, _, aufrufe = lauf("gut", "--quelle", "dockerhost", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30")
for argumente, ziel in zip(aufrufe, ("2026-10-05", "2026-10-05-ibkr"), strict=True):
assert argumente[:3] == ["-p", "2498", "-i"], argumente
assert argumente[3].endswith("trading-dh-kontrolle"), argumente
assert argumente[4:14] == [
"-o", "BatchMode=yes",
"-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=yes",
"-o", "UserKnownHostsFile=~/.ssh/known_hosts_trading_dh",
"-o", "ConnectTimeout=20",
], argumente
assert argumente[14:] == ["[email protected]", ziel], argumente
@fall
def noch_kein_ergebnis_ist_ein_befund_in_der_notiz_und_wird_wiederholt():
code, notiz, ergebnisse, _ = lauf(
"fehlt", "--quelle", "dockerhost", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30"
)
assert code == 1, code
assert ergebnisse == [], ergebnisse
assert "**gescheitert**" in notiz and "noch kein Ergebnis" in notiz, notiz
assert "#needs-review" in notiz, notiz
assert "fuer 2026-10-05-ibkr noch kein Ergebnis" in notiz, notiz
@fall
def muell_vom_host_ist_ein_befund():
code, notiz, ergebnisse, _ = lauf(
"muell", "--quelle", "dockerhost", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30"
)
assert code == 1, code
assert ergebnisse == [], ergebnisse
assert "keine JSON-Zeile vom Docker-Host" in notiz, notiz
@fall
def faellige_tage_kommen_einzeln_ueber_ssh():
modul = lade()
_, _, ergebnisse, aufrufe = lauf("gut", "--quelle", "dockerhost", "--jetzt", "2026-10-05T22:10", modul=modul)
tage = ["2026-09-28", "2026-09-29", "2026-09-30", "2026-10-01", "2026-10-02"]
ibkr = [f"{t}-ibkr" for t in tage if t >= modul.IBKR_ERSTER_TAG.isoformat()]
ziele = [a[-1] for a in aufrufe]
assert ziele == tage + ibkr, ziele
assert ergebnisse == sorted(f"{z}.json" for z in ziele), ergebnisse
@fall
def ohne_schalter_bleibt_es_bei_wsl():
modul = lade()
gerufen = []
wsl_attrappe(modul, gerufen)
def verboten(*_argumente):
raise AssertionError("ohne --quelle darf nichts ueber SSH laufen")
modul.fahre_dockerhost = verboten
code, _, ergebnisse, aufrufe = lauf("gut", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30", modul=modul)
assert code == 0, code
assert gerufen == [("2026-10-05", "alpaca"), ("2026-10-05", "ibkr")], gerufen
assert aufrufe == [], aufrufe
assert ergebnisse == ["2026-10-05-ibkr.json", "2026-10-05.json"], ergebnisse
@fall
def die_ibkr_instanz_bleibt_mit_quelle_ibkr_wsl_in_wsl():
modul = lade()
gerufen = []
wsl_attrappe(modul, gerufen)
code, _, ergebnisse, aufrufe = lauf(
"gut", "--quelle", "dockerhost", "--quelle-ibkr", "wsl", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30",
modul=modul,
)
assert code == 0, code
assert [a[-1] for a in aufrufe] == ["2026-10-05"], aufrufe
assert gerufen == [("2026-10-05", "ibkr")], gerufen
assert ergebnisse == ["2026-10-05-ibkr.json", "2026-10-05.json"], ergebnisse
@fall
def ein_ergebnis_des_falschen_teils_vom_host_gilt_als_gescheitert():
code, notiz, ergebnisse, aufrufe = lauf(
"falscher_teil", "--quelle", "dockerhost", "--teil", "ibkr", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30"
)
assert code == 1, code
assert [a[-1] for a in aufrufe] == ["2026-10-05-ibkr"], aufrufe
assert ergebnisse == [], ergebnisse
assert "gehoert zu 'alpaca'" in notiz and "**gescheitert**" in notiz, notiz
@fall
def eine_unbekannte_quelle_ist_ein_bedienfehler():
fehler = io.StringIO()
try:
with contextlib.redirect_stderr(fehler):
lauf("gut", "--quelle", "irgendwo", "--tag", "2026-10-05")
except SystemExit as ende:
assert ende.code == 2, ende.code
assert "--quelle: invalid choice" in fehler.getvalue(), fehler.getvalue()
return
raise AssertionError("argparse hat die Quelle irgendwo angenommen")
@fall
def die_heutige_frist_und_die_aufgabe_passen_zueinander():
quelle = (HIER / "abendkontrolle.py").read_text(encoding="utf-8")
aufgabe = (HIER / "abendkontrolle_einrichten.ps1").read_text(encoding="utf-8")
assert "StrictHostKeyChecking=no" not in quelle
assert "if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:" in quelle
assert aufgabe.count('[ValidateSet("wsl", "dockerhost")]') == 2
assert "'\" --quelle ' + $Quelle + ' --quelle-ibkr ' + $QuelleIbkr" in aufgabe
assert 'if ($Quelle -eq "dockerhost" -or $QuelleIbkr -eq "dockerhost") { $Uhrzeit = "23:15" }' in aufgabe
assert "if (-not $QuelleIbkr) { $QuelleIbkr = $Quelle }" in aufgabe
@fall
def der_echte_wsl_weg_ist_versperrt():
# Ohne --quelle und ohne eigene Attrappe nimmt main den WSL-Weg. Er muss als Abweichung
# enden, nicht als Lauf.
try:
lauf("gut", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30")
except AssertionError as ausnahme:
text = str(ausnahme)
assert "WSL-Weg ohne Attrappe" in text, text
# Beleg fuer die Zeilen in lauf(): Beim Aufruf zeigten wsl.exe und das Skript ins Leere.
assert "kein-wsl" in text and "/nicht-vorhanden/" in text, text
return
raise AssertionError("ein Lauf ohne WSL-Attrappe kam durch")
@fall
def der_echte_wsl_aufruf_findet_kein_wsl():
# Die zweite Sperre: Selbst die echte fahre_wsl findet in der Abdichtung kein wsl.exe, und das
# Skript, das sie dort starten wuerde, gibt es nicht.
modul = lade()
with tempfile.TemporaryDirectory() as ordner:
modul.WSL = str(Path(ordner) / "kein-wsl" / "wsl.exe")
modul.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
try:
modul.fahre_wsl("2026-10-05", "alpaca")
except FileNotFoundError:
return
raise AssertionError("fahre_wsl lief ohne wsl.exe weiter")
def main() -> int:
abweichungen = 0
for funktion in FAELLE:
try:
funktion()
except (Exception, SystemExit) as ausnahme:
abweichungen += 1
print(f"ABWEICHUNG {funktion.__name__}: {type(ausnahme).__name__}: {ausnahme}")
else:
print(f"OK {funktion.__name__}")
print(f"{len(FAELLE)} Faelle, {abweichungen} Abweichung(en) ({datetime.now():%Y-%m-%d %H:%M})")
return 2 if abweichungen else 0
if __name__ == "__main__":
sys.exit(main())
@@ -2,16 +2,27 @@
# Koordinator-Werkzeug, nicht fuers Repo. Auftrag Pascal 2026-09-24: taegliche Abendkontrolle, # Koordinator-Werkzeug, nicht fuers Repo. Auftrag Pascal 2026-09-24: taegliche Abendkontrolle,
# dauerhaft, uebersteht Neustarts, blockiert keine Updates von Claude Code oder Desktop. # dauerhaft, uebersteht Neustarts, blockiert keine Updates von Claude Code oder Desktop.
# #
# Zwei Ausloeser: werktags 22:45 (nach der Tagesauswertung um 22:20) und fuenf Minuten nach jeder # Zwei Ausloeser: werktags 22:45 (nach der Tagesauswertung um 22:20) oder statt dessen 23:15, sobald ein Teil vom
# Anmeldung (holt einen verpassten Abend nach; abendkontrolle.py prueft nur Tage ohne Ergebnisdatei). # Docker-Host kommt (-Quelle oder -QuelleIbkr dockerhost, seit Paket DH), und fuenf Minuten nach
# jeder Anmeldung (holt einen verpassten Abend nach; abendkontrolle.py prueft nur Tage ohne
# Ergebnisdatei).
# Nur bei angemeldetem Benutzer (Interactive): WSL und der ganze Paper-Betrieb brauchen die Anmeldung # Nur bei angemeldetem Benutzer (Interactive): WSL und der ganze Paper-Betrieb brauchen die Anmeldung
# ohnehin (Aufgabe "Trading Plattform - WSL wachhalten"). pythonw.exe startet ohne Fenster. # ohnehin (Aufgabe "Trading Plattform - WSL wachhalten"). pythonw.exe startet ohne Fenster.
# Voller Interpreterpfad: -Execute "python" endet in der Aufgabenplanung mit 0x80070002. # Voller Interpreterpfad: -Execute "python" endet in der Aufgabenplanung mit 0x80070002.
# #
# Aufruf: powershell -NoProfile -File abendkontrolle_einrichten.ps1 [-Aktion einrichten|entfernen] # Aufruf: powershell -NoProfile -File abendkontrolle_einrichten.ps1 [-Aktion einrichten|entfernen]
# [-Quelle wsl|dockerhost] [-QuelleIbkr wsl|dockerhost]
param( param(
[ValidateSet("einrichten", "entfernen")] [ValidateSet("einrichten", "entfernen")]
[string]$Aktion = "einrichten" [string]$Aktion = "einrichten",
# Seit Paket DH: wsl (Vorgabe) prueft in WSL wie bisher; dockerhost liest das Ergebnis des
# Docker-Hosts ueber SSH, werktags erst um 23:15 (der Host prueft um 16:30 New Yorker Zeit).
[ValidateSet("wsl", "dockerhost")]
[string]$Quelle = "wsl",
# Die Quelle fuer den Teil ibkr; ohne Angabe dieselbe wie -Quelle. Die IBKR-Instanz zieht an
# einem eigenen Abend um (Umzugs-Drehbuch DH, Phase 7).
[ValidateSet("wsl", "dockerhost")]
[string]$QuelleIbkr
) )
$Name = "Trading Plattform - Abendkontrolle" $Name = "Trading Plattform - Abendkontrolle"
@@ -26,8 +37,10 @@ if (-not (Test-Path $Py)) { throw "Interpreter fehlt: $Py" }
if (-not (Test-Path $Skript)) { throw "Skript fehlt: $Skript" } if (-not (Test-Path $Skript)) { throw "Skript fehlt: $Skript" }
$Benutzer = "$env:USERDOMAIN\$env:USERNAME" $Benutzer = "$env:USERDOMAIN\$env:USERNAME"
$Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '"') -WorkingDirectory "D:\Claude" if (-not $QuelleIbkr) { $QuelleIbkr = $Quelle }
$Abends = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday, Tuesday, Wednesday, Thursday, Friday -At "22:45" $Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '" --quelle ' + $Quelle + ' --quelle-ibkr ' + $QuelleIbkr) -WorkingDirectory "D:\Claude"
if ($Quelle -eq "dockerhost" -or $QuelleIbkr -eq "dockerhost") { $Uhrzeit = "23:15" } else { $Uhrzeit = "22:45" }
$Abends = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday, Tuesday, Wednesday, Thursday, Friday -At $Uhrzeit
$Anmeldung = New-ScheduledTaskTrigger -AtLogOn -User $Benutzer $Anmeldung = New-ScheduledTaskTrigger -AtLogOn -User $Benutzer
$Anmeldung.Delay = "PT5M" $Anmeldung.Delay = "PT5M"
$Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited $Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited
@@ -41,3 +54,4 @@ $Aufgabe = Get-ScheduledTask -TaskName $Name
"{0}: {1}" -f $Aufgabe.TaskName, $Aufgabe.State "{0}: {1}" -f $Aufgabe.TaskName, $Aufgabe.State
$Aufgabe.Triggers | ForEach-Object { "Ausloeser: {0} {1} {2}" -f $_.CimClass.CimClassName, $_.StartBoundary, $_.Delay } $Aufgabe.Triggers | ForEach-Object { "Ausloeser: {0} {1} {2}" -f $_.CimClass.CimClassName, $_.StartBoundary, $_.Delay }
"Naechster Lauf: {0}" -f (Get-ScheduledTaskInfo -TaskName $Name).NextRunTime "Naechster Lauf: {0}" -f (Get-ScheduledTaskInfo -TaskName $Name).NextRunTime
"Quellen: {0} (alpaca), {1} (ibkr)" -f $Quelle, $QuelleIbkr
@@ -0,0 +1,95 @@
"""Angriffsfaelle fuer --ibkr-python von abendkontrolle_wsl.py (Nachtrag zu Paket DH, IB2), in WSL, ohne Netz.
Koordinator-Werkzeug, nicht fuers Repo. Auf dem Docker-Host laeuft der Teil ibkr im Abbild; dort
gibt es das Venv aus WSL nicht, und ibkr_pruefen.py lesen laeuft mit dem python des Containers
(abendkontrolle-host.sh: --ibkr-python python). Die Faelle pruefen den Schalter, die Vorgabe, den
Aufruf mit dem genannten Interpreter gegen ein Ersatzskript in einem Wegwerf-Ordner und die
Weitergabe im Ablauf. Das Ersatzskript druckt den Interpreter, der es startet; so faellt ein
Aufruf mit dem festen Venv-Pfad auf, auch wenn es den Pfad gibt.
Aufruf (WSL, Venv): python abendkontrolle_ibkr_python_angriffe.py
Exit 0 ohne Abweichung, 2 mit Abweichung.
"""
import importlib.util
import sys
import tempfile
from datetime import datetime
from pathlib import Path
HIER = Path(__file__).resolve().parent
ERSATZ = (
"import sys\n"
"print('Interpreter: ' + sys.executable)\n"
"print('Positionen: 0')\n"
"print('Offene Orders: 0')\n"
)
FAELLE = []
def fall(funktion):
FAELLE.append(funktion)
return funktion
def lade():
spec = importlib.util.spec_from_file_location("abendkontrolle_wsl_unter_angriff", HIER / "abendkontrolle_wsl.py")
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
# Zweite Sperre (Pruefung DH-Nachtrag, 2026-09-26): Die Faelle geben den Ordner mit dem
# Ersatzskript ausdruecklich mit. Nimmt ein Mutant stattdessen die Vorgabe, zeigt sie ins Leere
# und nicht auf den Betriebsordner mit dem echten ibkr_pruefen.py.
modul.IBKR_ORDNER = "/nicht-vorhanden/trading-ibkr-betrieb"
return modul
@fall
def der_schalter_nennt_den_interpreter():
argumente = lade().parse_args(["--tag", "2026-10-05", "--teil", "ibkr", "--ibkr-python", "/usr/local/bin/python"])
assert argumente.ibkr_python == "/usr/local/bin/python", argumente
@fall
def ohne_schalter_bleibt_das_venv_aus_wsl():
modul = lade()
argumente = modul.parse_args(["--tag", "2026-10-05", "--teil", "ibkr"])
assert argumente.ibkr_python == modul.IBKR_PY, argumente
assert modul.IBKR_PY == "/home/claude/venvs/trading-ibkr/bin/python", modul.IBKR_PY
@fall
def lesen_laeuft_mit_dem_genannten_interpreter():
modul = lade()
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
(wurzel / "scripts").mkdir()
(wurzel / "scripts" / "ibkr_pruefen.py").write_text(ERSATZ, encoding="utf-8")
code, text = modul.lies_konto_ibkr(wurzel, None, sys.executable)
assert code == 0, (code, text)
assert f"Interpreter: {sys.executable}" in text, text
assert "Positionen: 0" in text and "Offene Orders: 0" in text, text
@fall
def der_ablauf_gibt_den_schalter_weiter():
quelle = (HIER / "abendkontrolle_wsl.py").read_text(encoding="utf-8")
assert "lies_konto_ibkr, ordner, argumente.ibkr_konto, argumente.ibkr_python)" in quelle
assert '[python, "scripts/ibkr_pruefen.py", "lesen"], cwd=ordner,' in quelle
def main() -> int:
abweichungen = 0
for funktion in FAELLE:
try:
funktion()
except (Exception, SystemExit) as ausnahme:
abweichungen += 1
print(f"ABWEICHUNG {funktion.__name__}: {type(ausnahme).__name__}: {ausnahme}")
else:
print(f"OK {funktion.__name__}")
print(f"{len(FAELLE)} Faelle, {abweichungen} Abweichung(en) ({datetime.now():%Y-%m-%d %H:%M})")
return 2 if abweichungen else 0
if __name__ == "__main__":
sys.exit(main())
@@ -87,6 +87,8 @@ Nur eine Meldung schicken: python abendkontrolle_wsl.py --meldung "<titel>" "<te
Fuer Angriffsfaelle ohne Netz und ohne systemd: --projekt, --kalender, --journal, --dienste, Fuer Angriffsfaelle ohne Netz und ohne systemd: --projekt, --kalender, --journal, --dienste,
--ohne-broker, --ohne-kerzen; im Teil ibkr dazu --ibkr-ordner, --ibkr-konto (JSON mit exit und --ohne-broker, --ohne-kerzen; im Teil ibkr dazu --ibkr-ordner, --ibkr-konto (JSON mit exit und
text statt des Aufrufs von ibkr_pruefen.py) und --jetzt (ISO-Zeitpunkt mit Versatz). text statt des Aufrufs von ibkr_pruefen.py) und --jetzt (ISO-Zeitpunkt mit Versatz).
Seit dem Nachtrag zu Paket DH (IB2): --ibkr-python nennt den Interpreter fuer ibkr_pruefen.py lesen
(Vorgabe IBKR_PY, das Venv in WSL; auf dem Docker-Host python im Abbild, abendkontrolle-host.sh).
Ausgabe: genau eine JSON-Zeile auf stdout. Exit 0 gelaufen (Befunde stehen in der Ausgabe), Ausgabe: genau eine JSON-Zeile auf stdout. Exit 0 gelaufen (Befunde stehen in der Ausgabe),
2 abgebrochen (die bis dahin gesammelten Befunde stehen trotzdem in der Ausgabe). 2 abgebrochen (die bis dahin gesammelten Befunde stehen trotzdem in der Ausgabe).
""" """
@@ -486,13 +488,17 @@ def lesen_erlaubt(jetzt: datetime) -> bool:
return not IBKR_SPERRE_VON <= (ny.hour, ny.minute) < IBKR_SPERRE_BIS return not IBKR_SPERRE_VON <= (ny.hour, ny.minute) < IBKR_SPERRE_BIS
def lies_konto_ibkr(ordner: Path, konto_datei: str | None) -> tuple[int, str]: def lies_konto_ibkr(ordner: Path, konto_datei: str | None, python: str = IBKR_PY) -> tuple[int, str]:
"""Exit und Ausgabe (stdout und stderr) von scripts/ibkr_pruefen.py lesen, oder aus einer Datei.""" """Exit und Ausgabe (stdout und stderr) von scripts/ibkr_pruefen.py lesen, oder aus einer Datei.
python ist der Interpreter dafuer: in WSL das Venv der Instanz (IBKR_PY), auf dem Docker-Host
das python des Abbilds (--ibkr-python, seit dem Nachtrag zu Paket DH).
"""
if konto_datei is not None: if konto_datei is not None:
daten = json.loads(Path(konto_datei).read_text(encoding="utf-8")) daten = json.loads(Path(konto_datei).read_text(encoding="utf-8"))
return int(daten["exit"]), str(daten["text"]) return int(daten["exit"]), str(daten["text"])
lauf = subprocess.run( lauf = subprocess.run(
[IBKR_PY, "scripts/ibkr_pruefen.py", "lesen"], cwd=ordner, capture_output=True, text=True, [python, "scripts/ibkr_pruefen.py", "lesen"], cwd=ordner, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=IBKR_LESEN_TIMEOUT_S, encoding="utf-8", errors="replace", timeout=IBKR_LESEN_TIMEOUT_S,
env={**os.environ, "PYTHONIOENCODING": "utf-8"}, env={**os.environ, "PYTHONIOENCODING": "utf-8"},
) )
@@ -574,7 +580,7 @@ def kontrolliere_ibkr(argumente: argparse.Namespace, k: Kontrolle) -> None:
k.hinweis(f"Konto am IBKR-Gateway nicht gelesen: {jetzt.astimezone(NEW_YORK):%H:%M} New Yorker Zeit " k.hinweis(f"Konto am IBKR-Gateway nicht gelesen: {jetzt.astimezone(NEW_YORK):%H:%M} New Yorker Zeit "
"liegt im Handelsfenster") "liegt im Handelsfenster")
return return
ergebnis = k.schritt("Konto am IBKR-Gateway", lies_konto_ibkr, ordner, argumente.ibkr_konto) ergebnis = k.schritt("Konto am IBKR-Gateway", lies_konto_ibkr, ordner, argumente.ibkr_konto, argumente.ibkr_python)
if ergebnis is not None: if ergebnis is not None:
pruefe_konto_ibkr(ergebnis[0], ergebnis[1], k) pruefe_konto_ibkr(ergebnis[0], ergebnis[1], k)
@@ -667,6 +673,8 @@ def parse_args(argv=None) -> argparse.Namespace:
# Seit Paket IB2: der Teil ibkr prueft die IBKR-Instanz statt des Alpaca-Runners. # Seit Paket IB2: der Teil ibkr prueft die IBKR-Instanz statt des Alpaca-Runners.
zerleger.add_argument("--teil", choices=("alpaca", "ibkr"), default="alpaca") zerleger.add_argument("--teil", choices=("alpaca", "ibkr"), default="alpaca")
zerleger.add_argument("--ibkr-ordner", default=IBKR_ORDNER) zerleger.add_argument("--ibkr-ordner", default=IBKR_ORDNER)
# Seit dem Nachtrag zu Paket DH (IB2): der Interpreter fuer ibkr_pruefen.py lesen.
zerleger.add_argument("--ibkr-python", default=IBKR_PY)
# Nur fuer die Angriffsfaelle: Exit und Ausgabe von ibkr_pruefen.py als JSON-Datei, und die Uhr. # Nur fuer die Angriffsfaelle: Exit und Ausgabe von ibkr_pruefen.py als JSON-Datei, und die Uhr.
zerleger.add_argument("--ibkr-konto") zerleger.add_argument("--ibkr-konto")
zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt mit Versatz statt der Uhr") zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt mit Versatz statt der Uhr")
@@ -13,6 +13,12 @@ HIER = Path(__file__).resolve().parent
sys.path.insert(0, str(HIER)) sys.path.insert(0, str(HIER))
import rueckweg_aapl as rw # noqa: E402 import rueckweg_aapl as rw # noqa: E402
# Abdichtung (Pruefung DH-Nachtrag, 2026-09-26): Die Faelle laufen mit --ohne-wsl. Bricht ein Mutant
# diesen Schalter, riefe melde() sonst den echten WSL-Teil mit --meldung, also ntfy. rueckweg_aapl
# holt WSL und WSL_SKRIPT als Wert aus abendkontrolle; deshalb werden sie hier im Modul ersetzt.
rw.WSL = str(Path(tempfile.gettempdir()) / "rueckweg-angriffe-kein-wsl" / "wsl.exe")
rw.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
ZEHN = '[agent]\nquelle = "folge"\ninstrumente = ["AAPL.XNAS", "MSFT.XNAS"]\n' ZEHN = '[agent]\nquelle = "folge"\ninstrumente = ["AAPL.XNAS", "MSFT.XNAS"]\n'
AAPL = '[agent]\nquelle = "register"\ninstrument = "AAPL.XNAS"\n' AAPL = '[agent]\nquelle = "register"\ninstrument = "AAPL.XNAS"\n'
abweichungen: list[str] = [] abweichungen: list[str] = []