Automatischer Push: 11 Datei(en) geändert

- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/abendkontrolle.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_angriffe.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_mutanten.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_wsl.py
- .vault-tools/trading-plattform-pruefung/browserlauf.py
- .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py
- .vault-tools/trading-plattform-pruefung/ha_totmann.py
- .vault-tools/trading-plattform-pruefung/ha_totmann_wsl.py
- .vault-tools/trading-plattform-pruefung/scan_push.sh
- AI/Regelwerk/codeworkflow.md

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-09-26 04:00:02 +02:00
1 parent ea278dc24f
commit c115a13190
11 files changed
+1848 -37

No files matched your search

@@ -55,11 +55,38 @@ Zeile in der Tagesnotiz (benannte Luecke; der Totmann in Home Assistant, Paket Z
Nicht gesehen: die Guete der Kurse (steht im Bericht), Teilausfuehrungen einzeln, alles vor dem
Handelstag. Betraege gibt das Werkzeug nie aus; fremde Zeilen laufen durch maskiere().
Seit Paket IB2 prueft --teil ibkr statt des Alpaca-Runners die IBKR-Instanz in ihrem Betriebsordner
(--ibkr-ordner, Vorgabe IBKR_ORDNER), mit eigener Ausgabe und eigener Meldung (Titel "IBKR ..."):
I1. Handelstag? wie Frage 1, derselbe Kalender von Alpaca.
I2. Lief die Instanz? Protokolle <tag>T*-paper.jsonl unter artifacts/paper des Betriebsordners.
Keines und trading-paper-ibkr.timer nicht active -> nur der Hinweis
"IBKR-Instanz heute nicht gestartet", kein Befund, der Rest entfaellt
(No-Go-Tag). Keines bei aktivem oder unlesbarem Zeitgeber -> Befund.
Der Zeitgeber wird zum Zeitpunkt der Kontrolle gelesen, nicht fuer
den geprueften Tag -- beim Nachholen eine benannte Luecke.
I3. Ist der Ordner die Instanz? data/paper/broker.txt sagt ibkr, sonst Befund.
I4. Killswitch der Instanz? killswitch_zustand in data/registry.db des Betriebsordners (mode=ro);
gesperrt -> Befund mit Stufe und Ausloeser des juengsten Ereignisses.
I5. Ende je Lauf, Handel? wie Fragen 3 und 6 (pruefe_laeufe); dazu die Schlussposition jedes
Laufs aus seiner Zeile ende (pruefe_schlusspositionen), unabhaengig
vom Journal: Position != 0 -> Befund.
I6. Journal? wie Frage 3 fuer trading-paper-ibkr.service; jede Zeile mit einem
Muster aus IBKR_MUSTER -> Befund.
I7. Konto am Gateway glatt? scripts/ibkr_pruefen.py lesen im Betriebsordner mit IBKR_PY (Client-ID
12, lesend), nur werktags ausserhalb 09:00 bis 16:15 New Yorker Zeit
und am Wochenende, sonst ein Hinweis. Weiter gehen nur die Zahlen aus
"Positionen: n" und "Offene Orders: n" und die BEFUND-Zeilen, maskiert,
Kontonummern als [konto]. n != 0, eine fehlende Zeile oder Exit != 0 ->
Befund.
Nicht gesehen im Teil ibkr: ein Tagesbericht (die Instanz hat keine Tagesauswertung),
handelstag_auswertung.py und der Dienst ibkr-gateway.service.
Ein Fangnetz, kein Nachweis ueber den Handelstag.
Aufruf (WSL, Venv): python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung]
Aufruf (WSL, Venv): python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung] [--teil ibkr]
Nur eine Meldung schicken: python abendkontrolle_wsl.py --meldung "<titel>" "<text>"
Fuer Angriffsfaelle ohne Netz und ohne systemd: --projekt, --kalender, --journal, --dienste,
--ohne-broker, --ohne-kerzen.
--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).
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).
"""
@@ -73,8 +100,10 @@ import subprocess
import sys
import time
from datetime import date
from datetime import datetime
from datetime import timedelta
from pathlib import Path
from zoneinfo import ZoneInfo
PROJEKT = "/mnt/d/Claude/Code Projekte/active - Trading Plattform"
GEHEIMNISSE = "/home/claude/.config/trading-plattform/secrets.toml"
@@ -87,6 +116,30 @@ DIENSTE_NOCHMAL_S = 30
KILLSWITCH_ZUSTAENDE = ("aktiv", "angehalten", "gesperrt")
AUSWERTUNG_TIMEOUT_S = 900
# Seit Paket IB2: die IBKR-Instanz im eigenen Betriebsordner (Unit trading-paper-ibkr.service).
IBKR_ORDNER = "/mnt/d/Claude/.arbeitsbaeume/trading-ibkr-betrieb"
IBKR_PY = "/home/claude/venvs/trading-ibkr/bin/python"
IBKR_DIENST = "trading-paper-ibkr.service"
IBKR_ZEITGEBER = "trading-paper-ibkr.timer"
IBKR_MARKE = Path("data") / "paper" / "broker.txt"
IBKR_TITEL = "IBKR "
IBKR_LESEN_TIMEOUT_S = 180
# Journalzeilen, die ohne Ausnahme ein Befund sind: ein Traceback, und der Satz, mit dem
# scripts/paper.py abbricht, wenn Instanzmarke und Konfiguration nicht zusammenpassen.
IBKR_MUSTER = ("Traceback (most recent call last)", "Nichts wird gebaut")
# Das Konto wird am Gateway nur ausserhalb dieses Fensters gelesen (New Yorker Zeit, werktags):
# Waehrend der Handelszeit gehoerten Positionen und Orders schon zum naechsten Handelstag.
NEW_YORK = ZoneInfo("America/New_York")
IBKR_SPERRE_VON = (9, 0)
IBKR_SPERRE_BIS = (16, 15)
IBKR_POSITIONEN = re.compile(r"^Positionen: (\d+)[ \t\r]*$", re.MULTILINE)
IBKR_ORDERS = re.compile(r"^Offene Orders: (\d+)[ \t\r]*$", re.MULTILINE)
# IBKR-Kontonummern: Buchstaben vorn, dann Ziffern. Das Paper-Konto hat mehr als "DU" vorn;
# die erste Fassung (nur DU/U) traf die echte Nummer nicht (Pruefung IB2, 2026-09-25). Dieselbe
# Form wie im Projekt (trading/broker/ibkr.py). Nie in Notiz oder Meldung.
KONTONUMMER = re.compile(r"\b[A-Z]{1,4}[0-9]{5,}\b")
NICHT_GESTARTET = "IBKR-Instanz heute nicht gestartet"
ENDE = re.compile(
r"Ende: (?P<grund>[^,]+), Position (?P<position>[^,]+), Orders (?P<orders>[^,]+), "
r"Killswitch (?P<killswitch>[^,]+), Exit (?P<exit>-?\d+)"
@@ -238,12 +291,12 @@ def pruefe_laeufe(laeufe: list[dict], k: Kontrolle) -> None:
# --- 3 Journal -------------------------------------------------------------------------------------
def lies_journal(tag: str, journal_datei: str | None) -> str:
def lies_journal(tag: str, journal_datei: str | None, dienst: str = "trading-paper.service") -> str:
if journal_datei is not None:
return Path(journal_datei).read_text(encoding="utf-8", errors="replace")
morgen = (date.fromisoformat(tag) + timedelta(days=1)).isoformat()
lauf = subprocess.run(
["journalctl", "-u", "trading-paper.service", "--since", f"{tag} 00:00", "--until", f"{morgen} 06:00",
["journalctl", "-u", dienst, "--since", f"{tag} 00:00", "--until", f"{morgen} 06:00",
"-o", "cat", "--no-pager"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60,
)
@@ -252,7 +305,7 @@ def lies_journal(tag: str, journal_datei: str | None) -> str:
return lauf.stdout
def pruefe_journal(text: str, k: Kontrolle) -> dict | None:
def pruefe_journal(text: str, k: Kontrolle, dienst: str = "trading-paper.service") -> dict | None:
alle = [{schluessel: wert.strip() for schluessel, wert in m.groupdict().items()} for m in ENDE.finditer(text)]
# Ein Lauf ausserhalb der Handelszeit (Neustart nach Boersenschluss) endet mit wartezeit und Exit 3.
enden = [satz for satz in alle if satz["grund"] != "wartezeit"]
@@ -260,9 +313,9 @@ def pruefe_journal(text: str, k: Kontrolle) -> dict | None:
if len(alle) > len(enden):
k.hinweis(f"{len(alle) - len(enden)} Lauf/Laeufe ausserhalb der Handelszeit im Journal (Ende wartezeit)")
if "Failed with result" in text:
k.befund("Journal: trading-paper.service endete mit 'Failed with result'")
k.befund(f"Journal: {dienst} endete mit 'Failed with result'")
if not enden:
k.befund("Journal: keine Zeile 'Ende: …, Exit n' eines Handelslaufs von trading-paper.service an diesem Tag")
k.befund(f"Journal: keine Zeile 'Ende: …, Exit n' eines Handelslaufs von {dienst} an diesem Tag")
return None
# Jeder Lauf des Tages zaehlt: Ein Runner, der mittags mit einer Stoerung endete und neu gestartet
# wurde, ist auch dann ein Befund, wenn der zweite sauber schliesst.
@@ -354,6 +407,178 @@ def pruefe_killswitch(projekt: Path, k: Kontrolle) -> None:
k.befund(f"Killswitch in unbekanntem Zustand {zeile[0]!r}")
# --- IBKR-Instanz (seit Paket IB2) -----------------------------------------------------------------
def pruefe_marke_ibkr(ordner: Path, k: Kontrolle) -> None:
datei = ordner / IBKR_MARKE
marke = datei.read_text(encoding="utf-8").strip() if datei.is_file() else None
k.fakten["marke"] = marke
if marke != "ibkr":
k.befund(
f"Instanzmarke {IBKR_MARKE.as_posix()} sagt {marke!r} statt 'ibkr': "
"Das ist nicht der Betriebsordner der IBKR-Instanz"
)
def pruefe_register_ibkr(ordner: Path, k: Kontrolle) -> None:
"""Zustand und juengstes Ereignis aus dem Register der Instanz, nur lesend (mode=ro)."""
register = ordner / "data" / "registry.db"
if not register.is_file():
k.befund("IBKR-Register data/registry.db fehlt im Betriebsordner")
return
with sqlite3.connect(f"file:{register}?mode=ro", uri=True, timeout=10) as verbindung:
stand = verbindung.execute(
"SELECT zustand, seit, ausloeser FROM killswitch_zustand WHERE id = 1"
).fetchone()
letztes = verbindung.execute(
"SELECT stufe, ausloeser FROM killswitch_ereignisse ORDER BY id DESC LIMIT 1"
).fetchone()
if stand is None:
k.befund("IBKR-Register: kein Killswitch-Zustand")
return
zustand = stand[0]
k.fakten["killswitch"] = {"zustand": zustand, "seit": stand[1], "ausloeser": stand[2]}
if zustand == "gesperrt":
grund = "ohne Ereignis im Register" if letztes is None else f"letztes Ereignis {letztes[0]} von {letztes[1]}"
k.befund(f"IBKR-Killswitch steht auf gesperrt ({grund}): Ohne Freigeben an der Konsole handelt die Instanz nicht")
elif zustand not in KILLSWITCH_ZUSTAENDE:
k.befund(f"IBKR-Killswitch in unbekanntem Zustand {zustand!r}")
def pruefe_schlusspositionen(laeufe: list[dict], k: Kontrolle) -> None:
"""Die Schlussposition jedes Laufs aus seiner Zeile ende, unabhaengig vom Journal.
pruefe_laeufe bewertet die Position nicht, und pruefe_journal nur die des letzten Laufs;
der Alpaca-Teil faengt sie ueber handelstag_auswertung.py, das der Teil ibkr nicht faehrt.
Ebenso einen Lauf ohne Zeile ende: pruefe_laeufe ueberspringt ihn, weil im Alpaca-Teil die
Auswertung ihn meldet (Nachkritik IB2).
"""
for lauf in laeufe:
ende = lauf["ende"]
if ende is None:
# Im Alpaca-Teil meldet das handelstag_auswertung.py; hier sonst niemand.
k.befund(f"{lauf['datei']}: Lauf ohne Zeile ende im Protokoll")
elif ende.get("position") != 0:
k.befund(f"{lauf['datei']}: Lauf endete laut Protokoll mit Position {ende.get('position')}")
def lies_zeitgeber_ibkr(dienste_datei: str | None) -> str:
"""Der Zustand von trading-paper-ibkr.timer (active, inactive, ...); aus einer Datei in den Angriffsfaellen.
Gelesen wird der Zustand jetzt, nicht der am geprueften Tag: Beim Nachholen kann er sich
inzwischen geaendert haben (benannte Luecke).
"""
if dienste_datei is not None:
zustaende = json.loads(Path(dienste_datei).read_text(encoding="utf-8"))
if not isinstance(zustaende, dict):
raise ValueError("Dienste-Datei ist kein Objekt")
return str(zustaende.get(IBKR_ZEITGEBER, "fehlt"))
lauf = subprocess.run(["systemctl", "is-active", IBKR_ZEITGEBER], capture_output=True, text=True, timeout=30)
return lauf.stdout.strip() or f"exit {lauf.returncode}"
def lesen_erlaubt(jetzt: datetime) -> bool:
"""Ob der Stand am Gateway jetzt fuer den Tagesschluss spricht: Wochenende, oder werktags
ausserhalb IBKR_SPERRE_VON bis IBKR_SPERRE_BIS New Yorker Zeit."""
ny = jetzt.astimezone(NEW_YORK)
if ny.weekday() >= 5:
return True
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]:
"""Exit und Ausgabe (stdout und stderr) von scripts/ibkr_pruefen.py lesen, oder aus einer Datei."""
if konto_datei is not None:
daten = json.loads(Path(konto_datei).read_text(encoding="utf-8"))
return int(daten["exit"]), str(daten["text"])
lauf = subprocess.run(
[IBKR_PY, "scripts/ibkr_pruefen.py", "lesen"], cwd=ordner, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=IBKR_LESEN_TIMEOUT_S,
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
)
return lauf.returncode, lauf.stdout + "\n" + lauf.stderr
def ohne_kontonummer(text: str) -> str:
# Erst die Kontonummer, dann die Maske: Sonst machte die Maske aus "Konto DU..." ein
# "Konto DU[x]", und die Kontonummer-Regel faende nichts mehr.
return maskiere(KONTONUMMER.sub("[konto]", text))
def pruefe_konto_ibkr(code: int, text: str, k: Kontrolle) -> None:
"""Positionen und offene Orders am Gateway; aus der Ausgabe gehen nur Zahlen und Befunde weiter."""
treffer_positionen = IBKR_POSITIONEN.search(text)
treffer_orders = IBKR_ORDERS.search(text)
positionen = int(treffer_positionen.group(1)) if treffer_positionen else None
orders = int(treffer_orders.group(1)) if treffer_orders else None
k.fakten["konto"] = {"positionen": positionen, "orders": orders, "exit": code}
for zeile in text.splitlines():
if zeile.strip().startswith(("BEFUND", "Voraussetzung fehlt")):
k.befund(f"ibkr_pruefen.py lesen: {ohne_kontonummer(zeile.strip())[:200]}")
if code != 0:
k.befund(f"ibkr_pruefen.py lesen endete mit Exit {code}")
if positionen is None or orders is None:
k.befund("Konto am Gateway nicht gelesen: keine Zeile 'Positionen: n' oder 'Offene Orders: n'")
return
if positionen != 0:
k.befund(f"Beim IBKR-Gateway stehen {positionen} Position(en)")
if orders != 0:
k.befund(f"Beim IBKR-Gateway stehen {orders} offene Order(s)")
def kontrolliere_ibkr(argumente: argparse.Namespace, k: Kontrolle) -> None:
ordner = Path(argumente.ibkr_ordner)
tag = argumente.tag
date.fromisoformat(tag) # ein falsches Datum ist ein Bedienfehler und bricht ab
k.fakten["ordner"] = str(ordner)
if not ordner.is_dir():
k.befund(f"Betriebsordner der IBKR-Instanz fehlt: {ordner}")
return
liste = k.schritt("Kalender, und damit Lauf, Ende und Konto,", lies_kalender, tag, argumente.kalender,
Path(argumente.projekt))
handelstag = None if liste is None else ist_handelstag(tag, liste)
k.fakten["handelstag"] = handelstag
if handelstag is None:
if liste is not None:
k.befund("Kalender in unerwarteter Form; Lauf, Ende und Konto der IBKR-Instanz ungeprueft")
return
dateien = protokolle(ordner, tag)
if not handelstag:
if dateien:
k.hinweis(f"kein Handelstag, aber {len(dateien)} Protokoll(e) der IBKR-Instanz")
return
zeitgeber = k.schritt("Zeitgeber der IBKR-Instanz", lies_zeitgeber_ibkr, argumente.dienste)
k.fakten["zeitgeber"] = zeitgeber
# No-Go-Tag: Pascal hat den Zeitgeber abgeschaltet, und die Instanz lief nicht. Leise.
if not dateien and zeitgeber is not None and zeitgeber != "active":
k.fakten["nicht_gestartet"] = True
k.hinweis(f"{NICHT_GESTARTET} ({IBKR_ZEITGEBER}: {zeitgeber})")
return
k.schritt("Instanzmarke", pruefe_marke_ibkr, ordner, k)
k.schritt("IBKR-Killswitch", pruefe_register_ibkr, ordner, k)
if not dateien:
k.befund(f"kein Protokoll der IBKR-Instanz an einem Handelstag, obwohl {IBKR_ZEITGEBER} "
f"{zeitgeber} ist -- die Instanz lief nicht")
laeufe = [lauf for lauf in (k.schritt(f"Protokoll {pfad.name}", lies_protokoll, pfad, k) for pfad in dateien) if lauf]
k.fakten["laeufe"] = laeufe
pruefe_laeufe(laeufe, k)
pruefe_schlusspositionen(laeufe, k)
text = k.schritt("Journal", lies_journal, tag, argumente.journal, IBKR_DIENST)
if text is not None:
k.fakten["ende"] = pruefe_journal(text, k, IBKR_DIENST)
for muster in IBKR_MUSTER:
if muster in text:
k.befund(f"Journal: {IBKR_DIENST} zeigt '{muster}'")
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now(NEW_YORK)
if not lesen_erlaubt(jetzt):
k.hinweis(f"Konto am IBKR-Gateway nicht gelesen: {jetzt.astimezone(NEW_YORK):%H:%M} New Yorker Zeit "
"liegt im Handelsfenster")
return
ergebnis = k.schritt("Konto am IBKR-Gateway", lies_konto_ibkr, ordner, argumente.ibkr_konto)
if ergebnis is not None:
pruefe_konto_ibkr(ergebnis[0], ergebnis[1], k)
# --- Meldung ---------------------------------------------------------------------------------------
def melde(titel: str, text: str, projekt: Path) -> str:
@@ -439,6 +664,12 @@ def parse_args(argv=None) -> argparse.Namespace:
zerleger.add_argument("--ohne-broker", action="store_true")
zerleger.add_argument("--ohne-kerzen", action="store_true")
zerleger.add_argument("--ohne-meldung", action="store_true")
# 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("--ibkr-ordner", default=IBKR_ORDNER)
# 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("--jetzt", help="ISO-Zeitpunkt mit Versatz statt der Uhr")
argumente = zerleger.parse_args(argv)
if bool(argumente.tag) == bool(argumente.meldung):
zerleger.error("genau eines von --tag und --meldung")
@@ -454,18 +685,22 @@ def main(argv=None) -> int:
k = Kontrolle()
code = 0
ibkr = argumente.teil == "ibkr"
try:
kontrolliere(argumente, k)
if ibkr:
kontrolliere_ibkr(argumente, k)
else:
kontrolliere(argumente, k)
except Exception as ausnahme:
k.befund(f"Abendkontrolle abgebrochen: {type(ausnahme).__name__}: {ausnahme}")
code = 2
if k.befunde and not argumente.ohne_meldung:
text = "\n".join(f"- {befund}" for befund in k.befunde[:12])
k.fakten["meldung"] = melde(
f"Abendkontrolle {argumente.tag}: {len(k.befunde)} Befund(e)", text, Path(argumente.projekt)
)
titel = f"Abendkontrolle {argumente.tag}: {len(k.befunde)} Befund(e)"
k.fakten["meldung"] = melde(IBKR_TITEL + titel if ibkr else titel, text, Path(argumente.projekt))
print(json.dumps(
{"tag": argumente.tag, "befunde": k.befunde, "hinweise": k.hinweise, "fakten": k.fakten, "exit": code},
{"tag": argumente.tag, "teil": argumente.teil, "befunde": k.befunde, "hinweise": k.hinweise,
"fakten": k.fakten, "exit": code},
ensure_ascii=False, default=str,
))
return code