Automatischer Push: 46 Datei(en) geändert

- .claude/agents/planer.md
- .claude/skills/plan-executer-pruefung/SKILL.md
- .claude/workflows/plankritik.js
- .claude/workflows/planung.js
- .claude/workflows/pruefung.js
- .claude/workflows/verbrauch.js
- .claude/workflows/welle-mit-pruefung.js
- .vault-tools/plan-kopie/LIESMICH.md
- .vault-tools/plan-kopie/plan_zeichen.py
- .vault-tools/plan-kopie/rot_nachweis.py
- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/abendkontrolle.py
- … und 34 weitere

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-25 04:00:02 +02:00
1 parent 67d674cd0b
commit ea278dc24f
46 files changed
+8403 -80

No files matched your search

@@ -5,7 +5,7 @@ title: "Trading Plattform: Prüfwerkzeuge des Koordinators fürs Dashboard"
summary: "Eigener Lauf mit echten Daten zum Dashboard der Trading Plattform — Vergleich der Endpunkte gegen Vollparse und pandas, zwei WebSocket-Verbindungen, Browser-Lauf und Wiederaufbau mit Playwright, gefahrlose Proben der zwei Skripte, Mutantenläufe. Entstanden 2026-09-17 (Teil 3b), erweitert 2026-09-18 (Teil 10: Anmeldung, Angriffsfälle, Beobachtung, Kerzenlatenz), bewusst nicht im Projekt-Repo."
tags: [vault-tools, trading-plattform]
created: 2026-09-17
updated: 2026-09-21
updated: 2026-09-24
---
# Prüfwerkzeuge fürs Dashboard der Trading Plattform
@@ -103,3 +103,43 @@ Die Werkzeuge nennen `config/secrets.toml` nicht mehr fest. Wer eine Projektklas
|---|---|---|
| `handelstag_auswertung.py` | seit Paket M mit mehreren Instrumenten: Instrumente aus dem Protokollkopf; je Instrument Entscheidungen, Orders und die aus der Folge erwartete Zahl (Stufen, Versatz, Entscheidungen, Schlussbein, ohne geteilte Beine), erreichte Positionen und Takte mit frischer Kerze; groesste Zahl von Orders in einem Takt, Ereignisse je Art, Positionen je Instrument am Ende, BEFUND fuer jede nicht gefuellte Orderzeile; das Taktfeld orders gilt als Orders dieses Takts; `--broker` und `--kerzen` fragen alle Symbole des Kopfs, jede Order gegen die Kerze ihres Symbols | wie bisher: `python handelstag_auswertung.py <protokoll> [--broker] [--kerzen]` aus dem Projektordner |
| `m_probeaufruf.py` | lesender Probeaufruf vor dem Einspielen: `GET /v2/assets` roh (Schluessel und Typen) und ueber `AlpacaBroker.anlage`, `trades/latest` mit allen Symbolen roh und ueber `letzte_kurse`, Kerzen gebuendelt gegen einzeln, Seitenverhalten mit kleiner Seite; Exit 0 nur, wenn alles wie angenommen ist und keine Aktie die Startpruefung einer Folge mit Short verfehlt | aus einem Arbeitsbaum mit Paket M in WSL: `python m_probeaufruf.py <handelstag> <kennung> ...` |
## Betrieb ohne täglichen Klick (2026-09-24): Abendkontrolle und Rückweg
Pascal am 2026-09-24: eine dauerhafte Abendkontrolle, die Neustarts übersteht und keine Updates von Claude Code oder Desktop blockiert, und zehn Aktien bis Mittwoch 2026-09-30. Beides läuft als Windows-Aufgabe mit `pythonw.exe`, ohne Claude-Prozess.
| Datei | Was sie tut | Aufruf |
|---|---|---|
| `abendkontrolle_wsl.py` | liest einen Handelstag und urteilt im Code: Handelstag laut `GET /v2/calendar`, Protokoll des scharfen Runners, letzte Zeile „Ende: …, Exit n" im Journal (jeder Exit ungleich 0 ist ein Befund), BEFUND-Zeilen von `handelstag_auswertung.py --broker --kerzen`, offene Positionen beim Broker, mindestens ein Takt `aktiv`, Tagesbericht, Dashboard und Zeitgeber, Killswitch nicht `gesperrt`; bei Befund eine Meldung mit hoher Priorität über den Melder des Projekts. Gibt eine JSON-Zeile aus, nie Beträge | in WSL: `python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung]`; nur eine Meldung: `--meldung TITEL TEXT` |
| `abendkontrolle.py` | Windows-Seite: prüft jeden Werktag seit 2026-09-24 ohne Ergebnisdatei (höchstens sieben Tage zurück, heute erst ab 22:30), legt das Ergebnis unter `data/paper/abendkontrollen/<tag>.json` im Projekt ab und schreibt eine Zeile in `Inbox/<tag>.md` unter „Agentenprotokoll", bei Befund mit `#needs-review` | `python abendkontrolle.py [--tag JJJJ-MM-TT]` |
| `abendkontrolle_einrichten.ps1` | Aufgabe „Trading Plattform - Abendkontrolle": werktags 22:45 und fünf Minuten nach jeder Anmeldung, nur angemeldet, verpasste Läufe beim nächsten Start | `powershell -NoProfile -File abendkontrolle_einrichten.ps1 [-Aktion entfernen]` |
| `abendkontrolle_angriffe.py`, `abendkontrolle_ersatz_auswertung.py` | 44 Fälle in WSL (Kalender, Protokoll, Ende je Lauf samt mittags abgebrochenem und abendlichem Lauf, Journal, Auswertung, Broker, Maske, Bericht, Dienste, Killswitch, getrennte Fragen, Meldeweg gegen `ntfy_probe_server.py` und gegen einen toten Port, Bedienfehler; Reihenfolge an zwei Positionen) und 20 unter Windows (Notiz, Überschrift mit Zusatz, CRLF, kein UTF-8, Ergebnisform, Abbruch wird wiederholt, keine Nachbardatei, fällige Tage), ohne Netz. Am 2026-09-24 nach der unabhängigen Prüfung: 0 Abweichungen | `python abendkontrolle_angriffe.py` einmal in WSL (Venv), einmal unter Windows |
| `rueckweg_aapl.py`, `rueckweg_aapl_einrichten.ps1` | einmal: `config/paper-aapl-register.toml` wird `config/paper.toml`, lokal committet (nicht gepusht) — nur nach 2026-09-30 22:05, bei stehendem Runner, sauberem Projektordner mit `master`, wenn dort noch die Konfiguration mit `quelle = "folge"` liegt und beide Dateien ausser `[agent]`, den Instrumenten und dem Tagesverlust dieselben Schluessel und Werte tragen, in beide Richtungen verglichen (sonst schaltete der Tausch etwa ein abgeschaltetes `[auto_fortsetzen]` still wieder ein); ein halb erledigter Tausch wird zu Ende committet. Sonst Befund in der Notiz und eine Meldung. Aufgabe „Trading Plattform - Rueckweg AAPL" am 2026-09-30 22:50, zweiter Versuch 2026-10-01 22:10 | `python rueckweg_aapl.py`; Einrichten wie oben |
| `rueckweg_aapl_angriffe.py` | 22 Fälle auf einem Wegwerf-Git-Repo (dazu halb getauscht, Abschnitt nur in `paper.toml`, Automatik nur dort abgeschaltet, Abschnitt nur dort gelöscht, anderer Wert, anderer Zweig); am 2026-09-24 0 Abweichungen | `python rueckweg_aapl_angriffe.py` unter Windows |
| `abendkontrolle_mutanten.py` | baut je einen Fehler in eine Wegwerfkopie und verlangt, dass der Angriffstreiber rot wird: 16 Mutanten von `abendkontrolle_wsl.py` (in WSL), 9 von `abendkontrolle.py` und 12 von `rueckweg_aapl.py` (unter Windows). Am 2026-09-24: 37 von 37 gefangen | `python abendkontrolle_mutanten.py` einmal in WSL, einmal unter Windows |
## Einspielen (2026-09-24): Werkzeuge aus dem Scratchpad hierher
| Datei | Was sie tut | Aufruf |
|---|---|---|
| `dashboard_neustart.sh` | `sudo systemctl restart trading-dashboard.service` nur bei stehendem Runner, danach Tor 6b (Journal der letzten Minute ohne `PermissionError`/`Traceback`, sonst Exit 27), `/api/gesund` und die Oberfläche. Pascals dauerhafte Freigabe gilt für genau diesen Neustart außerhalb der Handelszeit | mit `WSL_PW` per WSLENV: `wsl -d Debian -u claude -- bash …/dashboard_neustart.sh` |
| `geheimnis_scan.py` | vor jedem Push: Werte ab 12 Zeichen aus Geheimnis- und Zugangsdatei gegen `git log -p <von>..<bis>`, nur Schlüsselnamen und Trefferzahlen; dazu, ob eine der drei Geheimnisdateien verfolgt wird. Regressionsnachweis mit `SCAN_PROBE=<bekannter Text>` (am 2026-09-24: 66 Treffer für einen bekannten Text, 0 für die echten Werte) | in WSL: `python geheimnis_scan.py <repo> <von> <bis> [<bis> …]` |
## IBKR-Zugang (2026-09-24): Paper-Konto über das Gateway im Container
Pascal am 2026-09-24: Zugang zum IBKR-Paper-Konto vorbereiten, Gateway als Container in WSL, erster Test nach 22:00. Nichts davon handelt.
| Datei | Was sie tut | Aufruf |
|---|---|---|
| `ibkr_zugang_eintragen.sh` | Pascal trägt Paper-Benutzername, Passwort (ohne Anzeige, zweimal) und Kontonummer ein; Ablage `/home/claude/.config/trading-plattform/ibkr/` (0700/0600); ausgegeben werden nur Längen. Mit erfundenen Werten in einem Wegwerf-Ordner geprüft (`IBKR_ZIEL`) | `wsl -d Debian -u claude -- bash …/ibkr_zugang_eintragen.sh` |
| `ibkr_gateway_test.sh` | Gateway-Container `ghcr.io/gnzsnz/ib-gateway` (auf eine Prüfsumme festgelegt), Paper, `READ_ONLY_API=yes`, Port nur auf `127.0.0.1:4004`; Schritte `laden`, `pruefen`, `starten`, `log` (Benutzername maskiert), `probe`, `stoppen`; verweigert zwischen 15 und 22 Uhr Berlin | mit `WSL_PW` per WSLENV: `wsl -d Debian -u claude -- bash …/ibkr_gateway_test.sh <schritt>` |
| `ibkr_probe.py` | lesende Probe über `nautilus-ibapi` im eigenen Venv `~/venvs/ibkr-probe`: Verbindung, Konto (nur Anfang und Länge), Kontoart, Währung, Positionen (Zahl), verzögerter AAPL-Kurs (nur die Kursart), Fehlercodes; nie ein Betrag. Ohne Gateway geprüft: „keine Verbindung", Code 502, Exit 3 | `~/venvs/ibkr-probe/bin/python …/ibkr_probe.py [--port 4004]` |
## Paket AF (2026-09-24): Fortsetzen zum Tagesstart ohne Klick
| Datei | Was sie tut | Aufruf |
|---|---|---|
| `af_gegenfall.py` | faehrt die echte PaperSitzung eines Arbeitsbaums mit AF und der ausgelieferten `config/paper.toml` gegen einen ProbeBroker, Kerzen aus einer Datei von `hole_kerzen.py` fuer jede Aktie, Wegwerf-Register mit der Neustartregel, ohne Netz; der Fall `restposition` (10 Stueck in der letzten Aktie) und der Fall `mitten` (Start 60 Marktminuten nach der Oeffnung) muessen `nicht_fortgesetzt` mit dem Grund in Protokoll und Meldung zeigen, der Fall `flach` ist die Gegenprobe mit `fortgesetzt`, Ausloeser `automatik` und Orders; Exit 0 nur ohne BEFUND | aus dem Arbeitsbaum in WSL: `python af_gegenfall.py <kerzen.json> <fall>`, Fall `restposition`, `mitten` oder `flach`; die Kerzen vorher einmal lesend mit `hole_kerzen.py <tag> <kerzen.json>` |
| `af_registerprobe.py` | fragt die echte Funktion `pruefe_register` (Paket AF) gegen `data/registry.db`, nur lesend (`mode=ro`; die Klasse `Registry` fuehrte beim Oeffnen das Schema aus und schriebe); druckt Zustand, die vier juengsten Killswitch-Ereignisse (Stufe, Ausloeser, Zeit) und den Befund; Exit 0 erfuellt, 1 nicht erfuellt, 3 Register fehlt. Belegt am 2026-09-24 an drei gespielten Registern (bereit / Pascals Anhalten ohne Fortsetzen / aktiv) und an einer Kopie des echten; die mtime des echten blieb gleich. Braucht den Code mit AF im genannten Ordner | in WSL im Projektordner nach dem Dashboard-Neustart: `~/venvs/trading/bin/python af_registerprobe.py .` |
Was die uebrigen Werkzeuge mit AF sehen: `handelstag_auswertung.py` braucht keine Aenderung, es zaehlt das Ereignis `auto_fortsetzen` unter `ereignisse je art` und druckt seinen Text. `abendkontrolle_wsl.py` liest Zustand, Seit und Ausloeser des Killswitch und berichtet jede Zeile `auto_fortsetzen` als Hinweis (die ersten 200 Zeichen des Feldes text, das mit dem Ergebnis beginnt). `neustart_echtlauf.py` setzt `angehalten`/`neustart` ohne Ereignis, `pause_echtlauf.py` einen Ausloeser `echtlauf`: Die Sitzung setzt dort nicht fort, schreibt aber je Lauf eine Zeile `auto_fortsetzen` und in "Paper-Runner wartet" eine Zeile mehr. `generalprobe_echtlauf.py` und `killswitch_simulation.py` laden `config/paper.toml` und klicken nach Plan; gegen einen Baum mit AF setzt die Sitzung dort mit dem Schalter selbst fort, bevor der geplante Klick kommt. Wer diese vier fuer einen Vergleich mit Paket M faehrt, setzt vorher `auto_fortsetzen=False` in ihr `replace(lade_paper_config(...), ...)`; `neustart_echtlauf.py` und `pause_echtlauf.py` rufen `lade_paper_config` ohne `replace` auf — dort in der Konfiguration ihrer Kopie `[auto_fortsetzen] aktiv = false` setzen.
@@ -0,0 +1,215 @@
"""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
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).
Ablauf: Jeder Werktag seit ERSTER_TAG, hoechstens NACHHOLEN_TAGE zurueck, ohne Ergebnisdatei in
ABLAGE wird geprueft; der heutige erst ab FRUEHESTENS (der Bericht der Tagesauswertung kommt 22:20).
Je Tag: abendkontrolle_wsl.py in WSL (liest nur, meldet bei Befund selbst ueber ntfy), dann zuerst das
Ergebnis als JSON nach ABLAGE und danach eine Zeile in Inbox/<tag>.md unter der ersten Ueberschrift,
die mit "## Agentenprotokoll" beginnt.
- Liefert der WSL-Teil kein Ergebnis oder endet er mit Exit 2 (abgebrochen), bleibt die Ergebnisdatei
aus, die Notiz bekommt einen Befund mit #needs-review, und der naechste Lauf versucht den Tag erneut.
- Scheitert nur die Notiz (etwa eine Datei, die kein UTF-8 ist), bleibt es bei der Ergebnisdatei und
einer Zeile in lauf.log: Die Kontrolle ist gelaufen und hat ihre Meldung schon geschickt; ein zweiter
Lauf schickte sie nur noch einmal.
- Ein Tag, der laenger als NACHHOLEN_TAGE scheitert, faellt aus dem Fenster (lauf.log behaelt ihn).
Die Notiz wird ueber eine Nachbardatei und os.replace geschrieben: Ein Abbruch mitten im Schreiben
hinterlaesst die alte Datei, nicht eine halbe. Gegen eine Claude-Sitzung, die dieselbe Notiz im selben
Augenblick schreibt, schuetzt das nicht (Zeitfenster Lesen bis Ersetzen, Millisekunden) -- benannt.
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>]
"""
import argparse
import json
import os
import subprocess
import sys
import traceback
from datetime import date
from datetime import datetime
from datetime import time
from datetime import timedelta
from pathlib import Path
VAULT = Path(r"D:\Claude")
PROJEKT_TEIL = Path("Code Projekte") / "active - Trading Plattform"
ABLAGE_TEIL = PROJEKT_TEIL / "data" / "paper" / "abendkontrollen"
WSL = r"C:\Windows\System32\wsl.exe"
WSL_PY = "/home/claude/venvs/trading/bin/python"
WSL_SKRIPT = "/mnt/d/Claude/.vault-tools/trading-plattform-pruefung/abendkontrolle_wsl.py"
ERSTER_TAG = date(2026, 9, 24)
NACHHOLEN_TAGE = 7
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"
def faellige_tage(jetzt: datetime, ablage: Path) -> list[str]:
tage = []
for zurueck in range(NACHHOLEN_TAGE, -1, -1):
tag = jetzt.date() - timedelta(days=zurueck)
if tag < ERSTER_TAG or tag.weekday() >= 5:
continue
if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:
continue
if (ablage / f"{tag.isoformat()}.json").exists():
continue
tage.append(tag.isoformat())
return tage
def fahre_wsl(tag: str) -> dict:
lauf = subprocess.run(
[WSL, "-d", "Debian", "-u", "claude", "--", WSL_PY, WSL_SKRIPT, "--tag", tag],
capture_output=True, timeout=WSL_TIMEOUT_S, creationflags=KEIN_FENSTER,
)
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 WSL-Teil (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 []
fakten = ergebnis.get("fakten") or {}
hinweise = ergebnis.get("hinweise") or []
if fakten.get("handelstag") is False and not befunde:
return f"{kopf}: kein Handelstag laut Alpaca-Kalender."
teile = []
ende = fakten.get("ende") or {}
if ende:
teile.append(
f"Ende {ende.get('grund')}, Exit {ende.get('exit')}, Orders {ende.get('orders')}, "
f"Position {ende.get('position')}"
)
laeufe = fakten.get("laeufe") or []
if laeufe:
teile.append(f"{len(laeufe)} Lauf/Läufe, {sum(l.get('takte_aktiv', 0) for l in laeufe)} Takte aktiv")
if "broker_offen" in fakten:
teile.append(f"offene Positionen beim Broker {fakten['broker_offen']}")
if "bericht" in fakten:
teile.append("Tagesbericht da" if fakten["bericht"] else "Tagesbericht fehlt")
killswitch = fakten.get("killswitch") or {}
if killswitch:
teile.append(f"Killswitch {killswitch.get('zustand')} ({killswitch.get('ausloeser')})")
zusammen = "; ".join(teile)
if not befunde:
text = f"{kopf}: ohne Befund. {zusammen}."
else:
text = f"{kopf}: **{len(befunde)} Befund(e)** #needs-review. {zusammen}."
text += "".join(f"\n - {befund}" for befund in befunde)
if fakten.get("meldung"):
text += f"\n - Meldung ans Telefon: {fakten['meldung']}"
text += "".join(f"\n - Hinweis: {hinweis}" for hinweis in hinweise[:5])
return text
def schreibe_notiz(vault: Path, tag: str, zeile: str) -> None:
notiz = vault / "Inbox" / f"{tag}.md"
if notiz.exists():
text = notiz.read_bytes().decode("utf-8") # kein UTF-8: Ausnahme, die Notiz bleibt unberuehrt
else:
text = (
f"---\ntype: daily\ntitle: \"{tag}\"\nstatus: open\nsummary: \"Tagesnotiz vom {tag}.\"\n"
f"tags: [daily]\ncreated: {tag}\nupdated: {tag}\n---\n\n# {tag}\n"
)
# Zeilenenden der Datei bleiben, wie sie sind; gesucht wird ohne \r.
zeilen = text.split("\n")
bereinigt = [z.rstrip("\r").rstrip() for z in zeilen]
cr = "\r" if any(z.endswith("\r") for z in zeilen) else ""
# Die erste Ueberschrift, die mit "## Agentenprotokoll" beginnt ("## Agentenprotokoll — …" kommt vor).
start = next((i for i, z in enumerate(bereinigt) if z == UEBERSCHRIFT or z.startswith(UEBERSCHRIFT + " ")), None)
if start is None:
nl = cr + "\n"
text = text.rstrip("\r\n") + nl + nl + UEBERSCHRIFT + nl + nl + nl.join(zeile.split("\n")) + nl
else:
ende = next((i for i in range(start + 1, len(zeilen)) if bereinigt[i].startswith("## ")), len(zeilen))
# Hinter den letzten nicht leeren Eintrag des Abschnitts; die Leerzeilen danach ersetzt eine.
einfuegen = ende
while einfuegen > start + 1 and not bereinigt[einfuegen - 1]:
einfuegen -= 1
block = [z + cr for z in zeile.split("\n")]
if einfuegen == start + 1:
block = [cr] + block
zeilen[einfuegen:ende] = block + ([cr] if ende < len(zeilen) else [""])
text = "\n".join(zeilen)
if not text.endswith("\n"):
text += "\n"
neu = notiz.with_name(notiz.name + ".abendkontrolle.tmp")
neu.write_bytes(text.encode("utf-8"))
os.replace(neu, notiz)
def logge(ablage: Path, text: str) -> None:
ablage.mkdir(parents=True, exist_ok=True)
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as datei:
datei.write(f"{datetime.now().isoformat(timespec='seconds')} {text}\n")
def main(argv=None) -> int:
zerleger = argparse.ArgumentParser(allow_abbrev=False)
zerleger.add_argument("--tag")
zerleger.add_argument("--vault", default=str(VAULT))
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)")
argumente = zerleger.parse_args(argv)
vault = Path(argumente.vault)
ablage = vault / ABLAGE_TEIL
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
tage = [argumente.tag] if argumente.tag else faellige_tage(jetzt, ablage)
logge(ablage, f"start, faellig: {tage}")
code = 0
for tag in tage:
uhrzeit = jetzt.strftime("%H:%M") if tag == jetzt.date().isoformat() else f"{jetzt:%Y-%m-%d %H:%M}"
try:
if argumente.ohne_wsl:
ergebnis = json.loads(Path(argumente.ohne_wsl).read_text(encoding="utf-8"))
else:
ergebnis = fahre_wsl(tag)
if not isinstance(ergebnis, dict) or not isinstance(ergebnis.get("befunde"), list):
raise RuntimeError("Ergebnis des WSL-Teils in unerwarteter Form")
except Exception as ausnahme:
code = 1
logge(ablage, f"{tag}: gescheitert {ausnahme!r}\n{traceback.format_exc()}")
try:
schreibe_notiz(
vault, tag,
f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch): **gescheitert** "
f"({type(ausnahme).__name__}: {str(ausnahme)[:200]}) #needs-review. Der nächste Lauf versucht es erneut.",
)
except Exception as zweite:
logge(ablage, f"{tag}: auch die Notiz scheiterte {zweite!r}")
continue
# Ein Abbruch im WSL-Teil (Exit 2) wird wiederholt; alles andere gilt als gelaufen.
if ergebnis.get("exit") != 2:
(ablage / f"{tag}.json").write_text(
json.dumps(ergebnis, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n"
)
else:
code = 1
logge(ablage, f"{tag}: {len(ergebnis['befunde'])} Befund(e), exit {ergebnis.get('exit')}")
try:
schreibe_notiz(vault, tag, zeile_fuer(ergebnis, uhrzeit))
except Exception as ausnahme:
code = 1
logge(ablage, f"{tag}: Notiz nicht geschrieben {ausnahme!r}")
return code
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,406 @@
"""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 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 windows_faelle() -> None:
sys.path.insert(0, str(HIER))
import abendkontrolle as ak
from datetime import datetime
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")
if __name__ == "__main__":
if sys.platform == "win32":
windows_faelle()
else:
wsl_faelle()
print(f"ERGEBNIS: {len(abweichungen)} Abweichung(en)")
sys.exit(1 if abweichungen else 0)
@@ -0,0 +1,43 @@
# Richtet die Windows-Aufgabe "Trading Plattform - Abendkontrolle" ein oder entfernt sie.
# Koordinator-Werkzeug, nicht fuers Repo. Auftrag Pascal 2026-09-24: taegliche Abendkontrolle,
# 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
# 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
# ohnehin (Aufgabe "Trading Plattform - WSL wachhalten"). pythonw.exe startet ohne Fenster.
# Voller Interpreterpfad: -Execute "python" endet in der Aufgabenplanung mit 0x80070002.
#
# Aufruf: powershell -NoProfile -File abendkontrolle_einrichten.ps1 [-Aktion einrichten|entfernen]
param(
[ValidateSet("einrichten", "entfernen")]
[string]$Aktion = "einrichten"
)
$Name = "Trading Plattform - Abendkontrolle"
if ($Aktion -eq "entfernen") {
Unregister-ScheduledTask -TaskName $Name -Confirm:$false
return
}
$Py = "C:\Users\Pascal Michel\AppData\Local\Programs\Python\Python313\pythonw.exe"
$Skript = "D:\Claude\.vault-tools\trading-plattform-pruefung\abendkontrolle.py"
if (-not (Test-Path $Py)) { throw "Interpreter fehlt: $Py" }
if (-not (Test-Path $Skript)) { throw "Skript fehlt: $Skript" }
$Benutzer = "$env:USERDOMAIN\$env:USERNAME"
$Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '"') -WorkingDirectory "D:\Claude"
$Abends = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday, Tuesday, Wednesday, Thursday, Friday -At "22:45"
$Anmeldung = New-ScheduledTaskTrigger -AtLogOn -User $Benutzer
$Anmeldung.Delay = "PT5M"
$Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited
$Regeln = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Minutes 45) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName $Name -Action $Aufruf -Trigger @($Abends, $Anmeldung) -Principal $Wer `
-Settings $Regeln -Force `
-Description "Abendkontrolle des Paper-Betriebs: liest nur, schreibt in die Tagesnotiz, meldet Befunde per ntfy. Koordinator-Werkzeug, siehe abendkontrolle.py." | Out-Null
$Aufgabe = Get-ScheduledTask -TaskName $Name
"{0}: {1}" -f $Aufgabe.TaskName, $Aufgabe.State
$Aufgabe.Triggers | ForEach-Object { "Ausloeser: {0} {1} {2}" -f $_.CimClass.CimClassName, $_.StartBoundary, $_.Delay }
"Naechster Lauf: {0}" -f (Get-ScheduledTaskInfo -TaskName $Name).NextRunTime
@@ -0,0 +1,7 @@
"""Ersatz fuer handelstag_auswertung.py in den Angriffsfaellen der Abendkontrolle: druckt ERSATZ_AUSGABE,
endet mit ERSATZ_EXIT. Geht nicht ans Netz. Koordinator-Werkzeug, nicht fuers Repo."""
import os
import sys
print(os.environ.get("ERSATZ_AUSGABE", ""))
sys.exit(int(os.environ.get("ERSATZ_EXIT", "0")))
@@ -0,0 +1,98 @@
"""Mutanten der Abendkontrolle und des Rueckwegs: jeder muss seinen Angriffstreiber rot machen.
Koordinator-Werkzeug, nicht fuers Repo. Jeder Mutant laeuft auf einer Wegwerfkopie dieses Verzeichnisses.
In WSL (Venv) die Mutanten von abendkontrolle_wsl.py, unter Windows die von abendkontrolle.py und
rueckweg_aapl.py. Aufruf: python abendkontrolle_mutanten.py Exit 0, wenn keiner ueberlebt.
Ein Mutant, der das Verhalten nicht aendert, ist keine Luecke (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 8).
"""
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
HIER = Path(__file__).resolve().parent
WSL_MUTANTEN = [
("Exit-Vergleich verkehrt", "abendkontrolle_wsl.py", 'if satz["exit"] != "0":', 'if satz["exit"] == "9":'),
("Handelstag immer wahr", "abendkontrolle_wsl.py", 'return any(eintrag["date"] == tag for eintrag in liste)', "return True"),
("gesperrt nicht geprueft", "abendkontrolle_wsl.py", 'if zeile[0] == "gesperrt":', 'if zeile[0] == "nie":'),
("unbekannter Zustand still", "abendkontrolle_wsl.py", "elif zeile[0] not in KILLSWITCH_ZUSTAENDE:", "elif False:"),
("Broker-Schwelle", "abendkontrolle_wsl.py", "if offen[-1] != 0:", "if offen[-1] > 5:"),
("Probe zaehlt mit", "abendkontrolle_wsl.py", 'glob(f"{tag}T*-paper.jsonl")', 'glob(f"{tag}T*.jsonl")'),
("Maske aus", "abendkontrolle_wsl.py", " text = muster.sub(ersatz, text)", " pass"),
("nie-aktiv aus", "abendkontrolle_wsl.py", 'if satz.get("killswitch") == "aktiv":', "if True:"),
("Bericht nicht geprueft", "abendkontrolle_wsl.py", "if not bericht.is_file():\n k.befund", "if False:\n k.befund"),
("Failed-Zeile aus", "abendkontrolle_wsl.py", 'if "Failed with result" in text:', 'if "nie da" in text:'),
("nur letzter Lauf im Journal", "abendkontrolle_wsl.py", "for nummer, satz in enumerate(enden, start=1):",
"for nummer, satz in list(enumerate(enden, start=1))[-1:]:"),
("Melder-Rueckgabe verworfen", "abendkontrolle_wsl.py",
'return "gesendet" if fehler is None else f"nicht gesendet: {fehler}"', 'return "gesendet"'),
("Ende je Protokoll nicht bewertet", "abendkontrolle_wsl.py",
" if ende.get(\"grund\") != \"boersenschluss\":\n k.befund(f\"{lauf['datei']}",
" if False:\n k.befund(f\"{lauf['datei']}"),
("wartezeit im Journal mitgezaehlt", "abendkontrolle_wsl.py",
'enden = [satz for satz in alle if satz["grund"] != "wartezeit"]', "enden = alle"),
("Lauf nach Schluss als Handelslauf", "abendkontrolle_wsl.py",
'return bool(lauf["ende"]) and lauf["ende"].get("grund") == "wartezeit" and lauf["takte"] == 0', "return False"),
("Fragen nicht voneinander getrennt", "abendkontrolle_wsl.py",
" except Exception as ausnahme:\n self.befund(f\"{name} nicht geprueft",
" except ZeroDivisionError as ausnahme:\n self.befund(f\"{name} nicht geprueft"),
]
WIN_MUTANTEN = [
("keine Ergebnisdatei", "abendkontrolle.py", '(ablage / f"{tag}.json").write_text(', '(ablage / f"{tag}.nix").write_text('),
("Wochenende mit", "abendkontrolle.py", "if tag < ERSTER_TAG or tag.weekday() >= 5:", "if tag < ERSTER_TAG:"),
("heute vor 22:30", "abendkontrolle.py", "if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:", "if False:"),
("Befund ohne Markierung", "abendkontrolle.py", "**{len(befunde)} Befund(e)** #needs-review", "{len(befunde)} Befund(e)"),
("CRLF vergessen", "abendkontrolle.py", 'cr = "\\r" if any(z.endswith("\\r") for z in zeilen) else ""', 'cr = ""'),
("Formpruefung aus", "abendkontrolle.py",
'if not isinstance(ergebnis, dict) or not isinstance(ergebnis.get("befunde"), list):', "if False:"),
("Ueberschrift nur exakt", "abendkontrolle.py",
"if z == UEBERSCHRIFT or z.startswith(UEBERSCHRIFT + \" \")), None)", "if z == UEBERSCHRIFT), None)"),
("Abbruch gilt als erledigt", "abendkontrolle.py", 'if ergebnis.get("exit") != 2:', "if True:"),
("nicht atomar", "abendkontrolle.py", " os.replace(neu, notiz)", " notiz.write_bytes(text.encode(\"utf-8\"))"),
("Zeit egal", "rueckweg_aapl.py", "if jetzt < FRUEHESTENS:", "if False:"),
("Runner egal", "rueckweg_aapl.py", " if runner_aktiv(runner_vorgabe):", " if False and runner_aktiv(runner_vorgabe):"),
("unlesbar gilt als steht", "rueckweg_aapl.py",
'raise RuntimeError(f"Zustand des Runners nicht lesbar: {zustand!r}")', "return False"),
("schmutziger Ordner egal", "rueckweg_aapl.py", " else:\n if geaendert:", " else:\n if False:"),
("folge egal", "rueckweg_aapl.py", 'if MEHRERE not in ziel.read_text(encoding="utf-8"):', "if False:"),
("Ergebnisdatei egal", "rueckweg_aapl.py", 'if (ablage / "rueckweg.json").exists():', "if False:"),
("kein Commit", "rueckweg_aapl.py", '("add", ZIEL.as_posix()), ("commit", "-m", NACHRICHT)', '("add", ZIEL.as_posix()),'),
("halb getauscht gilt als fertig", "rueckweg_aapl.py",
" if not geaendert:\n return \"config/paper.toml ist schon",
" if True:\n return \"config/paper.toml ist schon"),
("Schluesselvergleich aus", "rueckweg_aapl.py", " if fehlend:", " if False:"),
("Zweig egal", "rueckweg_aapl.py", " if zweig != ZWEIG:", " if False:"),
("Wertevergleich aus", "rueckweg_aapl.py", " elif neu[name] != alt[name]:", " elif False:"),
("Gegenrichtung aus", "rueckweg_aapl.py", " elif name not in alt:", " elif False:"),
]
TREIBER = {
"abendkontrolle_wsl.py": "abendkontrolle_angriffe.py",
"abendkontrolle.py": "abendkontrolle_angriffe.py",
"rueckweg_aapl.py": "rueckweg_aapl_angriffe.py",
}
mutanten = WIN_MUTANTEN if sys.platform == "win32" else WSL_MUTANTEN
ueberlebt = []
for name, datei, alt, neu in mutanten:
with tempfile.TemporaryDirectory() as tmp:
ziel = Path(tmp) / "pruefung"
shutil.copytree(HIER, ziel, ignore=shutil.ignore_patterns("__pycache__"))
pfad = ziel / datei
text = pfad.read_text(encoding="utf-8")
if text.count(alt) != 1:
print(f"ANKER {text.count(alt)}-mal: {name}")
ueberlebt.append(name)
continue
pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
lauf = subprocess.run([sys.executable, str(ziel / TREIBER[datei])], capture_output=True, text=True,
timeout=1800, encoding="utf-8", errors="replace", cwd=tmp)
faelle = [z for z in lauf.stdout.splitlines() if z.startswith("FEHL")]
rot = lauf.returncode != 0
print(f"{'ROT (gefangen)' if rot else 'GRUEN (UEBERLEBT)'} {name}: {[f[:110] for f in faelle[:2]]}")
if not rot:
ueberlebt.append(name)
print(f"ERGEBNIS: {len(ueberlebt)} ueberlebt von {len(mutanten)} {ueberlebt}")
sys.exit(1 if ueberlebt else 0)
@@ -0,0 +1,475 @@
"""Abendkontrolle eines Paper-Handelstags: liest nur, urteilt im Code, meldet nur bei Befund.
Koordinator-Werkzeug, nicht fuers Repo. Laeuft in WSL im Venv des Projekts; gestartet von
abendkontrolle.py (Windows-Aufgabe "Trading Plattform - Abendkontrolle", werktags 22:45 Berlin und
bei jeder Anmeldung). Auftrag Pascal 2026-09-24: taegliche Abendkontrolle, dauerhaft, uebersteht
Neustarts und blockiert keine Updates von Claude Code oder Desktop -- deshalb ohne Claude-Prozess.
Entscheidungsliste (Frage -> Herkunft der Antwort):
1. Ist <tag> ein Handelstag? Quelle: GET /v2/calendar?start=<tag>&end=<tag>, gemessen am
2026-09-24: eine Liste von Objekten mit 'date' (JJJJ-MM-TT) und
'close' (HH:MM New York); Feiertage fehlen, das Wochenende ist
leer, verkuerzte Tage tragen ein frueheres 'close'. Antwort
nicht lesbar -> Befund, und die Fragen 2 bis 7 bleiben offen.
2. Lief ein scharfer Runner? Quelle: scripts/paper.py:554, Protokoll
artifacts/paper/<UTC-Stempel>-paper.jsonl. Der Start um 09:10
New York traegt das Datum des Handelstags (gilt, solange
max_wartezeit_h unter etwa 13 h bleibt; heute 8). Keines -> Befund.
3. Wie endete jeder Lauf? Quelle: scripts/paper.py:517, Zeile "Ende: <grund>, Position <n>,
Orders <n>, Killswitch <z>, Exit <n>" im Journal, und die Zeile
'ende' jedes Protokolls. Projektvorgabe: jeder Lauf endet mit
boersenschluss und Exit 0, der letzte mit Position 0. Ausgenommen
ist ein Lauf ausserhalb der Handelszeit (Ende wartezeit, im
Protokoll ohne Takt; gemessen am 2026-09-20 nach WSL-Neustarts):
nur ein Hinweis. Ein Lauf, der mittags mit abbruch endet, ist ein
Befund, auch wenn der naechste sauber schliesst. Keine Endezeile
im Journal -> Befund; "Failed with result" -> Befund.
4. Was sagt das Protokoll? Quelle: handelstag_auswertung.py (dieses Verzeichnis); jede
Zeile, die mit "BEFUND" beginnt, ist ein Befund; Exit ungleich 0
ebenfalls.
5. Ist das Konto glatt? Quelle: dieselbe Auswertung mit --broker, Zeile "offene
positionen beim broker jetzt: <n>". n != 0 -> Befund; fehlt die
Zeile -> Befund (die Frage blieb unbeantwortet).
6. Hat der Runner gehandelt? Projektvorgabe (Pascal 2026-09-24, Paket AF): an einem Handelstag
steht der Killswitch in mindestens einem Takt auf aktiv. Kein
solcher Takt -> Befund "Tag ohne Handel". Takte, die nach dem
ersten aktiven nicht mehr aktiv sind (Watchdog, Pascals Anhalten),
sind ein Hinweis: Der Runner hat dafuer schon selbst gemeldet.
Ereignisse auto_fortsetzen werden berichtet, nicht bewertet.
7. Liegt der Tagesbericht? Quelle: scripts/tagesauswertung.py:58 und :192,
data/paper/auswertungen/<tag>.md. Fehlt -> Befund.
8. Laufen Dashboard und Zeitgeber? Projektvorgabe (Pakete F und Z): trading-dashboard.service,
trading-paper.timer, trading-auswertung.timer "active"; ein
anderer Zustand wird nach DIENSTE_NOCHMAL_S einmal neu gelesen
(eine eben geweckte WSL startet ihre Dienste noch).
9. Wie steht der Killswitch? Quelle: Tabelle killswitch_zustand in data/registry.db
(trading/registry/registry.py:281). gesperrt -> Befund: ohne
"Freigeben" handelt der naechste Tag nicht. Ein Zustand
ausserhalb aktiv/angehalten/gesperrt -> Befund.
Jede Frage laeuft fuer sich: Scheitert eine (Sperre im Register, Journal nicht lesbar), ist das ein
Befund, und die anderen werden trotzdem beantwortet.
Meldung: bei mindestens einem Befund eine Meldung mit hoher Prioritaet ueber den Melder des Projekts.
Ob sie ankam, sagt der Rueckgabewert von Melder.melde (None oder der Grund, trading/paper/melder.py);
er steht in fakten.meldung. Kein Alarm entsteht, wenn WSL selbst nicht laeuft -- dann steht nur die
Zeile in der Tagesnotiz (benannte Luecke; der Totmann in Home Assistant, Paket Z2-7, deckt sie ab).
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().
Ein Fangnetz, kein Nachweis ueber den Handelstag.
Aufruf (WSL, Venv): python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung]
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.
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).
"""
import argparse
import json
import os
import re
import sqlite3
import subprocess
import sys
import time
from datetime import date
from datetime import timedelta
from pathlib import Path
PROJEKT = "/mnt/d/Claude/Code Projekte/active - Trading Plattform"
GEHEIMNISSE = "/home/claude/.config/trading-plattform/secrets.toml"
AUSWERTUNG = Path(__file__).resolve().parent / "handelstag_auswertung.py"
# Nur fuer die Angriffsfaelle: ein Ersatz, der vorgegebene Zeilen druckt, statt ans Netz zu gehen.
AUSWERTUNG_ERSATZ = os.environ.get("ABENDKONTROLLE_AUSWERTUNG")
HANDEL_URL = "https://paper-api.alpaca.markets"
DIENSTE = ("trading-dashboard.service", "trading-paper.timer", "trading-auswertung.timer")
DIENSTE_NOCHMAL_S = 30
KILLSWITCH_ZUSTAENDE = ("aktiv", "angehalten", "gesperrt")
AUSWERTUNG_TIMEOUT_S = 900
ENDE = re.compile(
r"Ende: (?P<grund>[^,]+), Position (?P<position>[^,]+), Orders (?P<orders>[^,]+), "
r"Killswitch (?P<killswitch>[^,]+), Exit (?P<exit>-?\d+)"
)
BROKER_OFFEN = re.compile(r"offene positionen beim broker jetzt: (\d+)")
# Maske fuer alles, was aus fremder Ausgabe in Notiz und Meldung geht, in dieser Reihenfolge:
# - jede Zahl hinter einem Geldwort,
# - jede Zahl vor einer Waehrung,
# - jede Zahl mit mehr als zwei Nachkommastellen,
# - jede Zahl mit mindestens vier Stellen vor dem Komma und Nachkommastellen.
MASKEN = (
(re.compile(r"(?i)\b(kontowert|konto|startkapital|equity|cash|preis|kurs|usd|pnl)([^0-9-]{0,3})-?[0-9][0-9.,]*"),
lambda m: f"{m.group(1)}{m.group(2)}[x]"),
(re.compile(r"-?\d[\d.,]*\s?(?:USD|EUR|\$|€)"), lambda m: "[x]"),
(re.compile(r"-?\d[\d,]*\.\d{3,}"), lambda m: "[x]"),
(re.compile(r"-?\d{4,}[.,]\d{1,2}\b"), lambda m: "[x]"),
)
def maskiere(text: str) -> str:
for muster, ersatz in MASKEN:
text = muster.sub(ersatz, text)
return text
class Kontrolle:
def __init__(self) -> None:
self.befunde: list[str] = []
self.hinweise: list[str] = []
self.fakten: dict = {}
def befund(self, text: str) -> None:
self.befunde.append(maskiere(text))
def hinweis(self, text: str) -> None:
self.hinweise.append(maskiere(text))
def schritt(self, name: str, aufruf, *argumente):
"""Eine Frage fuer sich: Scheitert sie, ist das ein Befund, und der Rest laeuft weiter."""
try:
return aufruf(*argumente)
except Exception as ausnahme:
self.befund(f"{name} nicht geprueft ({type(ausnahme).__name__}: {str(ausnahme)[:160]})")
return None
# --- 1 Handelstag ---------------------------------------------------------------------------------
def lies_kalender(tag: str, kalender_datei: str | None, projekt: Path):
"""Die Liste aus /v2/calendar oder aus einer Datei (Angriffsfaelle)."""
if kalender_datei is not None:
return json.loads(Path(kalender_datei).read_text(encoding="utf-8"))
import requests # erst hier: Die Angriffsfaelle laufen ohne Netz
sys.path.insert(0, str(projekt))
sys.path.insert(0, str(AUSWERTUNG.parent))
from handelstag_auswertung import kopfzeilen # liest die Schluessel, gibt sie nie aus
kopf = kopfzeilen()
if kopf is None:
raise RuntimeError("Schluessel fuer Alpaca nicht lesbar")
antwort = requests.get(
f"{HANDEL_URL}/v2/calendar", headers=kopf, params={"start": tag, "end": tag}, timeout=30
)
antwort.raise_for_status()
return antwort.json()
def ist_handelstag(tag: str, liste) -> bool | None:
"""True, False, oder None, wenn die Antwort nicht die gemessene Form hat."""
if not isinstance(liste, list):
return None
for eintrag in liste:
if not isinstance(eintrag, dict) or not isinstance(eintrag.get("date"), str):
return None
return any(eintrag["date"] == tag for eintrag in liste)
# --- 2 Protokolle, 3 Ende je Lauf, 6 Handel --------------------------------------------------------
def protokolle(projekt: Path, tag: str) -> list[Path]:
return sorted((projekt / "artifacts" / "paper").glob(f"{tag}T*-paper.jsonl"))
def lies_protokoll(pfad: Path, k: Kontrolle) -> dict:
takte = aktiv = pausiert_danach = unlesbar = 0
auto = []
ende = None
# Dateiiteration statt splitlines: U+2028 in einem Text zerlegte sonst eine JSON-Zeile.
with pfad.open(encoding="utf-8", errors="replace") as datei:
for zeile in datei:
if not zeile.strip():
continue
try:
satz = json.loads(zeile)
except json.JSONDecodeError:
unlesbar += 1
continue
if not isinstance(satz, dict):
unlesbar += 1
continue
art = satz.get("art")
if art == "takt":
takte += 1
if satz.get("killswitch") == "aktiv":
aktiv += 1
elif aktiv:
pausiert_danach += 1
elif art == "ereignis" and satz.get("was") == "auto_fortsetzen":
auto.append(str(satz.get("text"))[:200])
elif art == "ende":
ende = {
schluessel: satz.get(schluessel)
for schluessel in ("grund", "fehler", "position", "positionen", "orders", "killswitch")
}
if unlesbar:
k.befund(f"{pfad.name}: {unlesbar} Zeile(n) nicht lesbar")
return {"datei": pfad.name, "takte": takte, "takte_aktiv": aktiv, "takte_pausiert_danach": pausiert_danach,
"auto_fortsetzen": auto, "ende": ende}
def ausserhalb_der_handelszeit(lauf: dict) -> bool:
"""Ein Lauf, der ohne einen einzigen Takt mit wartezeit endete (Start nach Boersenschluss)."""
return bool(lauf["ende"]) and lauf["ende"].get("grund") == "wartezeit" and lauf["takte"] == 0
def pruefe_laeufe(laeufe: list[dict], k: Kontrolle) -> None:
for lauf in laeufe:
ende = lauf["ende"]
if ende is None:
continue # die Auswertung meldet "keine Zeile 'ende'"
if ausserhalb_der_handelszeit(lauf):
k.hinweis(f"{lauf['datei']}: Lauf ausserhalb der Handelszeit (Ende wartezeit), nicht bewertet")
continue
if ende.get("grund") != "boersenschluss":
k.befund(f"{lauf['datei']}: Lauf endete mit {ende.get('grund')} nach {lauf['takte']} Takten")
if ende.get("fehler"):
k.befund(f"{lauf['datei']}: Lauf endete mit Fehler {str(ende.get('fehler'))[:120]}")
if lauf["takte_pausiert_danach"]:
k.hinweis(f"{lauf['datei']}: {lauf['takte_pausiert_danach']} Takte nach dem Fortsetzen nicht aktiv")
for text in lauf["auto_fortsetzen"]:
k.hinweis(f"auto_fortsetzen: {text}")
bewertet = [lauf for lauf in laeufe if not ausserhalb_der_handelszeit(lauf)]
if laeufe and not bewertet:
k.befund("kein Lauf des scharfen Runners in der Handelszeit, nur Laeufe nach Boersenschluss")
elif bewertet and sum(lauf["takte_aktiv"] for lauf in bewertet) == 0:
k.befund("Tag ohne Handel: in keinem Takt stand der Killswitch auf aktiv")
# --- 3 Journal -------------------------------------------------------------------------------------
def lies_journal(tag: str, journal_datei: str | None) -> 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",
"-o", "cat", "--no-pager"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60,
)
if lauf.returncode != 0:
raise RuntimeError(f"journalctl Exit {lauf.returncode}")
return lauf.stdout
def pruefe_journal(text: str, k: Kontrolle) -> 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"]
k.fakten["enden_im_journal"] = len(enden)
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'")
if not enden:
k.befund("Journal: keine Zeile 'Ende: …, Exit n' eines Handelslaufs von trading-paper.service 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.
for nummer, satz in enumerate(enden, start=1):
if satz["exit"] != "0":
k.befund(f"Runner (Lauf {nummer} von {len(enden)}) endete mit Exit {satz['exit']} (Grund {satz['grund']})")
if satz["grund"] != "boersenschluss":
k.befund(f"Runner (Lauf {nummer} von {len(enden)}) endete nicht mit boersenschluss, sondern mit {satz['grund']}")
letzte = enden[-1]
if letzte["position"] != "0":
k.befund(f"Runner endete mit Position {letzte['position']}")
return letzte
# --- 4 und 5 Auswertung ----------------------------------------------------------------------------
def fahre_auswertung(pfad: Path, projekt: Path, broker: bool, kerzen: bool, k: Kontrolle) -> None:
befehl = [sys.executable, AUSWERTUNG_ERSATZ or str(AUSWERTUNG), str(pfad)]
if broker:
befehl.append("--broker")
if kerzen:
befehl.append("--kerzen")
lauf = subprocess.run(
befehl, cwd=projekt, capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=AUSWERTUNG_TIMEOUT_S, env={**os.environ, "PYTHONIOENCODING": "utf-8"},
)
zeilen = lauf.stdout.splitlines() + lauf.stderr.splitlines()
for zeile in zeilen:
if zeile.strip().startswith("BEFUND") or zeile.startswith("VORAUSSETZUNG FEHLT"):
k.befund(f"{pfad.name}: {zeile.strip()}")
if lauf.returncode != 0:
k.befund(f"{pfad.name}: handelstag_auswertung.py endete mit Exit {lauf.returncode}")
if broker:
offen = [int(m.group(1)) for m in map(BROKER_OFFEN.search, zeilen) if m]
if not offen:
k.befund(f"{pfad.name}: keine Zeile zu offenen Positionen beim Broker -- Konto nicht geprueft")
else:
k.fakten["broker_offen"] = offen[-1]
if offen[-1] != 0:
k.befund(f"Beim Broker stehen {offen[-1]} offene Position(en)")
# --- 8 Dienste und 9 Killswitch -------------------------------------------------------------------
def lies_dienste(dienste_datei: str | None) -> dict[str, str]:
if dienste_datei is not None:
return json.loads(Path(dienste_datei).read_text(encoding="utf-8"))
def lesen():
ergebnis = {}
for dienst in DIENSTE:
lauf = subprocess.run(["systemctl", "is-active", dienst], capture_output=True, text=True, timeout=30)
ergebnis[dienst] = lauf.stdout.strip() or f"exit {lauf.returncode}"
return ergebnis
zustaende = lesen()
if any(wert != "active" for wert in zustaende.values()):
time.sleep(DIENSTE_NOCHMAL_S)
zustaende = lesen()
return zustaende
def pruefe_dienste(zustaende, k: Kontrolle) -> None:
if not isinstance(zustaende, dict):
k.befund("Dienste: Zustand nicht lesbar")
return
k.fakten["dienste"] = zustaende
for dienst in DIENSTE:
if zustaende.get(dienst) != "active":
k.befund(f"{dienst} ist nicht aktiv ({zustaende.get(dienst)})")
def pruefe_killswitch(projekt: Path, k: Kontrolle) -> None:
register = projekt / "data" / "registry.db"
if not register.is_file():
k.befund("Register data/registry.db fehlt")
return
with sqlite3.connect(f"file:{register}?mode=ro", uri=True, timeout=10) as verbindung:
zeile = verbindung.execute(
"SELECT zustand, seit, ausloeser FROM killswitch_zustand WHERE id = 1"
).fetchone()
if zeile is None:
k.befund("Register: kein Killswitch-Zustand")
return
k.fakten["killswitch"] = {"zustand": zeile[0], "seit": zeile[1], "ausloeser": zeile[2]}
if zeile[0] == "gesperrt":
k.befund("Killswitch steht auf gesperrt: Ohne 'Freigeben' handelt der naechste Tag nicht")
elif zeile[0] not in KILLSWITCH_ZUSTAENDE:
k.befund(f"Killswitch in unbekanntem Zustand {zeile[0]!r}")
# --- Meldung ---------------------------------------------------------------------------------------
def melde(titel: str, text: str, projekt: Path) -> str:
"""Eine Meldung mit hoher Prioritaet ueber den Melder des Projekts. Scheitert nie nach aussen.
Melder.melde wirft nicht: Es gibt None bei Erfolg und sonst den Grund zurueck (Typ der Ausnahme
oder "HTTP <status>"). Genau das steht im Ergebnis.
"""
try:
sys.path.insert(0, str(projekt))
os.chdir(projekt)
from trading.paper.melder import baue_melder
from trading.paper.meldungen import HOCH
from trading.paper.meldungen import Meldung
melder = baue_melder(config_pfad="config/paper.toml")
if not melder.aktiv:
return "nicht gesendet: Melder nicht aktiv"
fehler = melder.melde(Meldung(titel=titel, text=maskiere(text), prioritaet=HOCH, tags="warning"))
return "gesendet" if fehler is None else f"nicht gesendet: {fehler}"
except Exception as ausnahme: # die Kontrolle haengt nicht an der Meldung
return f"nicht gesendet: {type(ausnahme).__name__}"
# --- Ablauf ----------------------------------------------------------------------------------------
def kontrolliere(argumente: argparse.Namespace, k: Kontrolle) -> None:
projekt = Path(argumente.projekt)
tag = argumente.tag
date.fromisoformat(tag) # ein falsches Datum ist ein Bedienfehler und bricht ab
# handelstag_auswertung.py und der Melder lesen aus dem Arbeitsverzeichnis (Projektordner).
os.chdir(projekt)
zustaende = k.schritt("Dienste", lies_dienste, argumente.dienste)
if zustaende is not None:
pruefe_dienste(zustaende, k)
k.schritt("Killswitch", pruefe_killswitch, projekt, k)
liste = k.schritt("Kalender, und damit Lauf, Ende und Bericht,", lies_kalender, tag, argumente.kalender, 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 Bericht ungeprueft")
return
dateien = protokolle(projekt, tag)
if not handelstag:
if dateien:
k.hinweis(f"kein Handelstag, aber {len(dateien)} Protokoll(e) des scharfen Runners")
return
if not dateien:
k.befund("kein Protokoll des scharfen Runners an einem Handelstag -- der Runner 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)
text = k.schritt("Journal", lies_journal, tag, argumente.journal)
if text is not None:
k.fakten["ende"] = pruefe_journal(text, k)
ausserhalb = {lauf["datei"] for lauf in laeufe if ausserhalb_der_handelszeit(lauf)}
for pfad in dateien:
if pfad.name in ausserhalb:
continue
k.schritt(f"Auswertung {pfad.name}", fahre_auswertung, pfad, projekt,
not argumente.ohne_broker, not argumente.ohne_kerzen, k)
bericht = projekt / "data" / "paper" / "auswertungen" / f"{tag}.md"
k.fakten["bericht"] = bericht.is_file()
if not bericht.is_file():
k.befund(f"Tagesbericht data/paper/auswertungen/{tag}.md fehlt")
def parse_args(argv=None) -> argparse.Namespace:
zerleger = argparse.ArgumentParser(description=__doc__.splitlines()[0], allow_abbrev=False)
zerleger.add_argument("--tag")
zerleger.add_argument("--meldung", nargs=2, metavar=("TITEL", "TEXT"))
zerleger.add_argument("--projekt", default=PROJEKT)
zerleger.add_argument("--kalender")
zerleger.add_argument("--journal")
zerleger.add_argument("--dienste")
zerleger.add_argument("--ohne-broker", action="store_true")
zerleger.add_argument("--ohne-kerzen", action="store_true")
zerleger.add_argument("--ohne-meldung", action="store_true")
argumente = zerleger.parse_args(argv)
if bool(argumente.tag) == bool(argumente.meldung):
zerleger.error("genau eines von --tag und --meldung")
return argumente
def main(argv=None) -> int:
os.environ.setdefault("TRADING_GEHEIMNISSE", GEHEIMNISSE)
argumente = parse_args(argv)
if argumente.meldung:
print(json.dumps({"meldung": melde(*argumente.meldung, Path(argumente.projekt))}, ensure_ascii=False))
return 0
k = Kontrolle()
code = 0
try:
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)
)
print(json.dumps(
{"tag": argumente.tag, "befunde": k.befunde, "hinweise": k.hinweise, "fakten": k.fakten, "exit": code},
ensure_ascii=False, default=str,
))
return code
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,204 @@
"""Paket AF ohne Netz: Gegenfall und Gegenprobe der Fortsetzung zum Tagesstart.
Koordinator-Werkzeug, nicht Teil des Repos (Paket AF, 2026-09-24). Faehrt die echte
PaperSitzung des Arbeitsbaums mit der ausgelieferten config/paper.toml und ihrem Schalter
[auto_fortsetzen] gegen einen ProbeBroker, mit echten IEX-Kerzen aus einer JSON-Datei von
hole_kerzen.py (fuer jedes Instrument dieselben), einer ProbeUhr, einem Wegwerf-Register
mit der Neustartregel und einem Melder, der nur sammelt. Kein Netz, keine Order an Alpaca,
das echte Register bleibt unberuehrt.
Drei Faelle:
restposition vor der Oeffnung gestartet, Restposition beim Start -> nicht fortgesetzt
mitten 60 Marktminuten nach der Oeffnung gestartet, flach -> nicht fortgesetzt
flach vor der Oeffnung gestartet, flach -> fortgesetzt (die Gegenprobe)
Exit 0 heisst: Der Fall lief wie erwartet. Jede Abweichung ist eine Zeile BEFUND und Exit 2,
eine fehlende Voraussetzung Exit 3.
Aufruf (WSL, aus dem Arbeitsbaum): python af_gegenfall.py <kerzen.json> <fall>
"""
import json
import sys
import tempfile
from datetime import datetime
from datetime import timedelta
from pathlib import Path
sys.path.insert(0, str(Path.cwd()))
from trading.killswitch.killswitch import Killswitch # noqa: E402
from trading.killswitch.killswitch import lade_killswitch_config # noqa: E402
from trading.paper.agenten import agenten_je_instrument # noqa: E402
from trading.paper.agenten import beschreibung_je_instrument # noqa: E402
from trading.paper.agenten import lade_agent # noqa: E402
from trading.paper.config import enge_limits # noqa: E402
from trading.paper.config import lade_paper_config # noqa: E402
from trading.paper.kerzen import FesteKerzenquelle # noqa: E402
from trading.paper.kerzen import Kerze # noqa: E402
from trading.paper.probe import ProbeBroker # noqa: E402
from trading.paper.sitzung import PaperSitzung # noqa: E402
from trading.paper.uhr import ProbeUhr # noqa: E402
from trading.registry.registry import Registry # noqa: E402
from trading.risk.limits import lade_limits # noqa: E402
FAELLE = ("restposition", "mitten", "flach")
RESTPOSITION = 10
class SammelMelder:
"""Nimmt jede Meldung an und schickt nichts."""
aktiv = True
def __init__(self) -> None:
self.gesehen = []
def melde(self, meldung):
self.gesehen.append(meldung)
return None
class Protokoll:
"""Haelt die Zeilen im Speicher, durch json.dumps wie das echte Protokoll."""
def __init__(self) -> None:
self.zeilen = []
def schreibe(self, eintrag) -> None:
self.zeilen.append(json.loads(json.dumps(eintrag, ensure_ascii=False, default=str)))
def lies_kerzen(pfad: Path) -> list:
roh = json.loads(pfad.read_text(encoding="utf-8"))
return [
Kerze(
zeit=datetime.fromisoformat(k["zeit"]),
open=float(k["open"]),
high=float(k["high"]),
low=float(k["low"]),
close=float(k["close"]),
volume=float(k["volume"]),
)
for k in roh
]
def main() -> int:
if len(sys.argv) != 3 or sys.argv[2] not in FAELLE:
print(f"Aufruf: python af_gegenfall.py <kerzen.json> <{'|'.join(FAELLE)}>")
return 3
pfad, fall = Path(sys.argv[1]), sys.argv[2]
if not pfad.is_file():
print(f"VORAUSSETZUNG FEHLT: {pfad}")
return 3
kerzen = sorted(lies_kerzen(pfad), key=lambda kerze: kerze.zeit)
if not kerzen:
print(f"VORAUSSETZUNG FEHLT: keine Kerze in {pfad}")
return 3
config = lade_paper_config("config/paper.toml")
if not getattr(config, "auto_fortsetzen", False):
print("VORAUSSETZUNG FEHLT: config/paper.toml traegt [auto_fortsetzen] aktiv = true nicht")
return 3
limits = enge_limits(lade_limits("config/risk.toml"), config)
ks_config = lade_killswitch_config("config/killswitch.toml")
quelle = FesteKerzenquelle(kerzen)
if fall == "mitten":
beginn = kerzen[0].zeit + timedelta(minutes=60)
else:
beginn = kerzen[0].zeit - timedelta(minutes=30)
uhr = ProbeUhr(beginn)
rest = config.alle_instrumente[-1]
broker = ProbeBroker(
kerzenquelle=quelle,
uhr=uhr,
instrument_id=config.instrument_id,
instrumente=config.alle_instrumente,
positionen={rest: RESTPOSITION} if fall == "restposition" else None,
)
protokoll = Protokoll()
melder = SammelMelder()
with tempfile.TemporaryDirectory(prefix="af-gegenfall-") as wurzel:
ordner = Path(wurzel)
registry = Registry(db_pfad=ordner / "registry.db", artefakt_wurzel=ordner / "artifacts")
Killswitch.laden(broker=broker, registry=registry, config=ks_config)
agent, beschreibung = lade_agent(config, registry)
sitzung = PaperSitzung(
broker=broker,
kerzenquelle=quelle,
agent=agent,
agent_beschreibung=beschreibung_je_instrument(config, beschreibung),
agenten=agenten_je_instrument(config, agent),
registry=registry,
limits=limits,
killswitch_config=ks_config,
config=config,
uhr=uhr,
protokoll=protokoll,
protokoll_pfad="af-gegenfall.jsonl",
status_pfad=ordner / "status.json",
modus="probe",
soll_abbrechen=lambda: False,
melder=melder,
)
code = sitzung.laufe()
gespeichert = registry.killswitch_zustand()
ereignisse = registry.killswitch_ereignisse()
auto = [z for z in protokoll.zeilen if z.get("was") == "auto_fortsetzen"]
orders = [z for z in protokoll.zeilen if z.get("art") == "order"]
takte = [z for z in protokoll.zeilen if z.get("art") == "takt"]
print(f"Fall {fall}, {len(kerzen)} Kerzen ab {kerzen[0].zeit.isoformat()}, Beginn {beginn.isoformat()}")
print(f"Instrumente: {', '.join(config.alle_instrumente)}")
for zeile in auto:
print(f"auto_fortsetzen {zeile.get('ergebnis')} | {zeile.get('text')} | phase {zeile.get('phase')}")
for meldung in melder.gesehen:
print(f"Meldung [{meldung.prioritaet}] {meldung.titel}")
for zeile in meldung.text.split("\n"):
if zeile.startswith(("Killswitch", "Nicht selbst fortgesetzt", "Ausloeser", "Position")):
print(f" {zeile}")
print(f"Killswitch am Ende: {gespeichert.zustand}, Ausloeser {gespeichert.ausloeser}")
print(f"Killswitch-Ereignisse: {[(e.stufe, e.ausloeser) for e in ereignisse]}")
print(f"Takte {len(takte)}, Orders {len(orders)}, Exit {code}")
befunde = []
titel = [m.titel for m in melder.gesehen]
automatik = [e for e in ereignisse if e.ausloeser == "automatik"]
if fall == "flach":
if [z.get("ergebnis") for z in auto] != ["fortgesetzt"]:
befunde.append(f"auto_fortsetzen {[z.get('ergebnis') for z in auto]} statt ['fortgesetzt']")
if "Paper-Runner laeuft" not in titel or "Paper-Runner wartet" in titel:
befunde.append(f"Meldungen {titel}")
if len(automatik) != 1 or automatik[0].stufe != "fortsetzen":
befunde.append(f"Ereignisse mit Ausloeser automatik: {[(e.stufe, e.grund[:60]) for e in automatik]}")
if not orders:
befunde.append("keine Order -- fortgesetzt, aber nicht gehandelt")
if code != 0:
befunde.append(f"Exit {code} statt 0")
else:
if fall == "restposition":
erwartet = f"Konto nicht flach: {rest} {RESTPOSITION}"
else:
erwartet = "dieser Prozess ist nicht vor der Oeffnung gestartet"
if [z.get("text") for z in auto] != [f"nicht_fortgesetzt: {erwartet}"]:
befunde.append(f"auto_fortsetzen {[z.get('text') for z in auto]}")
wartet = [m for m in melder.gesehen if m.titel == "Paper-Runner wartet"]
if len(wartet) != 1 or f"Nicht selbst fortgesetzt: {erwartet}" not in wartet[0].text.split("\n"):
befunde.append(f"Meldungen {titel}")
if automatik:
befunde.append(f"{len(automatik)} Ereignis(se) mit Ausloeser automatik")
if orders:
befunde.append(f"{len(orders)} Order(s) ohne Fortsetzung")
if any(t.get("killswitch") != "angehalten" for t in takte):
befunde.append("ein Takt nicht angehalten")
erwarteter_code = 4 if fall == "restposition" else 0
if code != erwarteter_code:
befunde.append(f"Exit {code} statt {erwarteter_code}")
for befund in befunde:
print(f"BEFUND: {befund}")
if befunde:
return 2
print("ERGEBNIS: Fall wie erwartet")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,54 @@
"""Fragt pruefe_register (Paket AF) gegen ein Killswitch-Register, nur lesend.
Koordinator-Werkzeug, nicht fuers Repo (2026-09-24, Befund der AF-Pruefung: das Drehbuch hatte fuer
die Registerpruefung nur eine Erwartung in Prosa). Oeffnet data/registry.db mit mode=ro -- die Klasse
Registry fuehrt beim Oeffnen das Schema aus und schriebe damit in die Datei --, baut Zustand und
Ereignisse als einfache Objekte und ruft die echte Funktion trading.paper.auto_fortsetzen.pruefe_register
des Projekts. Druckt den Befund und die juengsten Ereignisse (Stufe, Ausloeser, Zeit), nie einen Betrag.
Was es nicht sieht: Konto, Spielgeld, Uhr und Schalter -- das pruefen Sitzung und vorstart_pruefung.py.
Vor der Oeffnung am Handelstag steht der Killswitch nach dem Dashboard-Neustart auf angehalten/neustart;
davor (etwa nach Pascals Klick am Vortag) ist "steht auf aktiv" der erwartete Befund, kein Fehler.
Aufruf (WSL, im Projektordner): ~/venvs/trading/bin/python af_registerprobe.py <projektordner>
Exit 0: erfuellt, 1: nicht erfuellt, 3: Register fehlt.
"""
import sqlite3
import sys
from pathlib import Path
from types import SimpleNamespace
def main(argv=None) -> int:
argumente = sys.argv[1:] if argv is None else argv
projekt = Path(argumente[0] if argumente else ".").resolve()
db = projekt / "data" / "registry.db"
if not db.is_file():
print(f"ABBRUCH: {db} fehlt")
return 3
sys.path.insert(0, str(projekt))
from trading.paper.auto_fortsetzen import pruefe_register
verbindung = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
try:
zeile = verbindung.execute("select zustand, ausloeser from killswitch_zustand").fetchone()
ereignisse = [
SimpleNamespace(id=nummer, stufe=stufe, ausloeser=ausloeser, zeit=zeit)
for nummer, stufe, ausloeser, zeit in verbindung.execute(
"select id, stufe, ausloeser, zeit from killswitch_ereignisse order by id"
)
]
finally:
verbindung.close()
gespeichert = None if zeile is None else SimpleNamespace(zustand=zeile[0], ausloeser=zeile[1])
befund = pruefe_register(gespeichert, ereignisse)
print(f"Zustand: {zeile}")
for ereignis in ereignisse[-4:]:
print(f" Nr. {ereignis.id}: {ereignis.stufe} von {ereignis.ausloeser}, {ereignis.zeit}")
print(f"pruefe_register: {'erfuellt' if befund.erfuellt else 'NICHT erfuellt'} -- {befund.grund}")
return 0 if befund.erfuellt else 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,23 @@
#!/bin/bash
# Dashboard-Neustart nach einem Einspielen, mit Tor 6b (liest das Dashboard die Geheimnisdatei unter PrivateUsers=yes?).
# Koordinator-Werkzeug, nicht fuers Repo. Pascals dauerhafte Freigabe gilt fuer genau diesen Neustart, nur ausserhalb der
# Handelszeit; das Skript verweigert, solange der Runner laeuft. Aufruf aus der PowerShell mit WSL_PW per WSLENV:
# wsl -d Debian -u claude -- bash /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/dashboard_neustart.sh
set -u
[ -n "${WSL_PW:-}" ] || { echo "WSL_PW fehlt"; exit 20; }
S() { printf '%s\n' "$WSL_PW" | sudo -S -p '' "$@"; }
if systemctl is-active --quiet trading-paper.service; then echo "Runner laeuft - kein Neustart"; exit 21; fi
date
S systemctl restart trading-dashboard.service || exit 22
sleep 8
systemctl is-active trading-dashboard.service
systemctl show trading-dashboard.service --property=NRestarts,ActiveEnterTimestamp
journalctl -u trading-dashboard.service --since "-1min" --no-pager | tail -10 | sed -E 's/[0-9]{6,}/######/g'
if journalctl -u trading-dashboard.service --since "-1min" --no-pager | grep -E "kann sie aber nicht lesen|PermissionError|secrets.toml|Traceback"; then
echo "TOR 6b ZU"; exit 27
fi
echo "Tor 6b offen"
code=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: 127.0.0.1:8144" http://127.0.0.1:8144/api/gesund)
echo "api/gesund: $code"
code=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: 127.0.0.1:8144" http://127.0.0.1:8144/)
echo "Oberflaeche /: $code"
@@ -0,0 +1,86 @@
"""Vor dem Push: kommt ein Wert aus der Geheimnis- oder Zugangsdatei in den neuen Commits vor?
Koordinator-Werkzeug, nicht fuers Repo (Pascal 2026-09-24: Push jederzeit, vorher dieser Scan).
Regressionsnachweis vor jedem Scan: SCAN_PROBE=<bekannter Text aus dem Verlauf> muss einen Treffer melden.
Liest die Werte, gibt sie nie aus, nur Schluesselname und Trefferzahl. Laeuft in WSL als claude.
Aufruf: python geheimnis_scan.py <repo> <von> <bis> [<bis> ...]
"""
import subprocess
import sys
import tomllib
from pathlib import Path
GEHEIM = Path("/home/claude/.config/trading-plattform/secrets.toml")
repo = Path(sys.argv[1])
von = sys.argv[2]
ziele = sys.argv[3:]
def blaetter(tabelle, pfad=""):
for schluessel, wert in tabelle.items():
name = f"{pfad}.{schluessel}" if pfad else schluessel
if isinstance(wert, dict):
yield from blaetter(wert, name)
elif isinstance(wert, str):
yield name, wert
werte = []
for datei in (GEHEIM, repo / "config" / "zugang.toml"):
if not datei.exists():
print(f"BEFUND: {datei.name} fehlt, Scan ohne diese Werte")
continue
with datei.open("rb") as f:
daten = tomllib.load(f)
for name, wert in blaetter(daten):
# Kurze Werte (true/false, Hostnamen wie localhost, Modellnamen) treffen Prosa.
if len(wert) >= 12:
werte.append((f"{datei.name}:{name}", wert))
schluessel_json = repo / "config" / "zugang-schluessel.json"
if schluessel_json.exists():
import json
for i, eintrag in enumerate(json.loads(schluessel_json.read_text(encoding="utf-8")) or []):
if isinstance(eintrag, dict):
for k, v in eintrag.items():
if isinstance(v, str) and len(v) >= 12:
werte.append((f"zugang-schluessel.json:{i}.{k}", v))
# Seit 2026-09-24 (Paket IB): die IBKR-Zugangsdateien. Die Kontonummer hat 9 Zeichen, deshalb
# hier ab 6 statt ab 12; diese drei Werte stehen in keiner Prosa.
IBKR = Path("/home/claude/.config/trading-plattform/ibkr")
for name in ("paper_benutzer", "paper_passwort", "paper_konto"):
datei = IBKR / name
if datei.exists():
wert = datei.read_text(encoding="utf-8").strip()
if len(wert) >= 6:
werte.append((f"ibkr:{name}", wert))
else:
print(f"BEFUND: ibkr/{name} fehlt, Scan ohne diesen Wert")
import os
if os.environ.get("SCAN_PROBE"):
# Regressionsnachweis: ein bekannter Text aus dem Verlauf muss als Treffer erscheinen.
werte.append(("probe", os.environ["SCAN_PROBE"]))
print(f"geprueft: {len(werte)} Werte ab 12 Zeichen")
befunde = 0
for ziel in ziele:
text = subprocess.run(
["git", "-c", "safe.directory=*", "-C", str(repo), "log", "-p", "--all-match", f"{von}..{ziel}"],
capture_output=True, text=True, encoding="utf-8", errors="replace",
).stdout
print(f"{von}..{ziel}: {len(text)} Zeichen Verlauf")
for name, wert in werte:
n = text.count(wert)
if n:
befunde += 1
print(f" TREFFER {name}: {n}")
verfolgt = subprocess.run(
["git", "-c", "safe.directory=*", "-C", str(repo), "ls-tree", "-r", "--name-only", ziel],
capture_output=True, text=True,
).stdout.splitlines()
for gesperrt in ("config/secrets.toml", "config/zugang.toml", "config/zugang-schluessel.json"):
if gesperrt in verfolgt:
befunde += 1
print(f" TREFFER verfolgt: {gesperrt}")
print(f"ERGEBNIS: {befunde} Treffer")
sys.exit(1 if befunde else 0)
@@ -0,0 +1,97 @@
#!/bin/bash
# Erster Anmeldetest am IBKR-Paper-Konto: IB Gateway als Container in WSL, nur lesend.
# Koordinator-Werkzeug, nicht fuers Repo. Pascal am 2026-09-24: Container in WSL, beide Downloads frei
# (ghcr.io/gnzsnz/ib-gateway, festgelegt auf die Pruefsumme unten; nautilus-ibapi im eigenen Venv
# ~/venvs/ibkr-probe), Test nach 22:00, zweiter Faktor bei ihm nur ein Passkey.
# Das Abbild ist dasselbe, das NautilusTrader 1.231.0 mit DockerizedIBGatewayConfig startet
# (config.py: container_image "ghcr.io/gnzsnz/ib-gateway:stable"; gateway.py: Paper aussen 4004).
# Die Zugangsdaten liest der Container aus /home/claude/.config/trading-plattform/ibkr/ (Pascal traegt sie
# mit ibkr_zugang_eintragen.sh ein); dieses Skript gibt sie nie aus und maskiert den Benutzernamen im Log.
# Aufruf aus der PowerShell (sudo per WSLENV, wie bei den anderen Betriebsskripten):
# $env:WSL_PW=[Environment]::GetEnvironmentVariable('DEBIAN_WSL_PASSWORD','User'); $env:WSLENV='WSL_PW'
# wsl -d Debian -u claude -- bash /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/ibkr_gateway_test.sh <schritt>
# Schritte: laden | pruefen | starten | log | probe | stoppen
set -u
BILD="ghcr.io/gnzsnz/ib-gateway@sha256:5fb04cb93cd705eff8a5a58b015503003864ffd9ea890be4d07273389149aa9f"
ZUGANG=/home/claude/.config/trading-plattform/ibkr
NAME=ibkr-gateway-probe
PROBE_PY=/home/claude/venvs/ibkr-probe/bin/python
PROBE=/mnt/d/Claude/.vault-tools/trading-plattform-pruefung/ibkr_probe.py
[ -n "${WSL_PW:-}" ] || { echo "WSL_PW fehlt (sudo per WSLENV)"; exit 20; }
S() { printf '%s\n' "$WSL_PW" | sudo -S -p '' "$@"; }
stunde=$(TZ=Europe/Berlin date +%-H)
# RUHE_AUSNAHME: nur auf Pascals ausdrueckliche Anweisung fuer einen einzelnen Lauf (so am 2026-09-24,
# 17:0x: "Bitte schon einmal testen ohne andere Workflows zu gefaehrden"); der Wert nennt Datum und Anlass.
if [ "$stunde" -ge 15 ] && [ "$stunde" -lt 22 ] && [ -z "${RUHE_AUSNAHME:-}" ]; then
echo "Ruhe-Regel: zwischen 15 und 22 Uhr Berlin laeuft hier nichts (jetzt $(TZ=Europe/Berlin date +%H:%M))."
exit 21
fi
maske() {
# Den Benutzernamen und lange Ziffernfolgen aus fremder Ausgabe nehmen, ohne ihn selbst zu zeigen.
python3 -c '
import re, sys
from pathlib import Path
benutzer = ""
p = Path(sys.argv[1])
if p.is_file():
benutzer = p.read_text(encoding="utf-8").strip()
for zeile in sys.stdin:
if benutzer:
zeile = zeile.replace(benutzer, "[benutzer]")
# Kontonummern: ein bis vier Grossbuchstaben und mindestens fuenf Ziffern. Die erste Fassung kannte
# nur DU plus Ziffern; das Paper-Konto hat einen Buchstaben mehr und stand am 2026-09-24 unmaskiert im Log.
zeile = re.sub(r"\b[A-Z]{1,4}\d{5,}\b", "[konto]", zeile)
sys.stdout.write(zeile)
' "$ZUGANG/paper_benutzer"
}
case "${1:-}" in
laden)
S docker pull "$BILD" 2>&1 | tail -n 3
S docker image inspect "$BILD" --format 'Id {{.Id}} Groesse {{.Size}} Erstellt {{.Created}}'
;;
pruefen)
# Welche Variablen liest das Abbild aus Dateien, und unter welcher UID laeuft es?
S docker run --rm --entrypoint sh "$BILD" -c '
echo "== id"; id
echo "== _FILE-Variablen in den Startskripten"; grep -rhoE "[A-Z_]+_FILE" /home /usr/local/bin /opt 2>/dev/null | sort -u | head -20
echo "== Ports"; grep -rhoE "4003|4004|4001|4002|5900" /home /usr/local/bin 2>/dev/null | sort | uniq -c | head
' 2>&1 | head -40
;;
starten)
for datei in paper_benutzer paper_passwort; do
[ -s "$ZUGANG/$datei" ] || { echo "fehlt: $ZUGANG/$datei (erst ibkr_zugang_eintragen.sh)"; exit 22; }
done
S docker rm -f "$NAME" >/dev/null 2>&1
# Benutzername und Passwort aus Dateien im Container; TWS_USERID_FILE nur, wenn "pruefen" es fand,
# sonst setzt BENUTZER_ALS_VARIABLE=1 den Namen als Variable (er ist kein Geheimnis wie das Passwort).
if [ "${BENUTZER_ALS_VARIABLE:-0}" = "1" ]; then
BENUTZER_ARG=(-e "TWS_USERID=$(cat "$ZUGANG/paper_benutzer")")
else
BENUTZER_ARG=(-e "TWS_USERID_FILE=/run/secrets/ibkr/paper_benutzer")
fi
S docker run -d --name "$NAME" \
-e TRADING_MODE=paper -e READ_ONLY_API=yes -e TWOFA_TIMEOUT_ACTION=exit \
-e TWS_PASSWORD_FILE=/run/secrets/ibkr/paper_passwort "${BENUTZER_ARG[@]}" \
-e TIME_ZONE=America/New_York \
-v "$ZUGANG:/run/secrets/ibkr:ro" \
-p 127.0.0.1:4004:4004 \
"$BILD" >/dev/null && echo "gestartet: $NAME (Paper, nur lesend, Port 127.0.0.1:4004)"
;;
log)
S docker logs --tail "${2:-60}" "$NAME" 2>&1 | maske
;;
probe)
cd /tmp && "$PROBE_PY" "$PROBE" --port 4004
;;
stoppen)
S docker rm -f "$NAME" 2>&1 | tail -n 1
;;
*)
echo "Schritt fehlt: laden | pruefen | starten | log | probe | stoppen"
exit 2
;;
esac
@@ -0,0 +1,157 @@
"""Lesende Probe gegen das IBKR-Paper-Konto ueber ein laufendes IB Gateway: verbindet, liest, trennt.
Koordinator-Werkzeug, nicht fuers Repo. Pascal am 2026-09-24: Zugang zum IBKR-Paper-Konto vorbereiten,
Gateway als Container in WSL (ghcr.io/gnzsnz/ib-gateway, wie DockerizedIBGatewayConfig in NautilusTrader
1.231.0), erster Test heute nach 22:00. Diese Probe handelt nie: Das Gateway laeuft mit READ_ONLY_API=yes,
und sie ruft keine Order-Methode.
Entscheidungsliste (Frage -> Herkunft der Antwort):
1. Erreicht die Probe das Gateway? Quelle: EClient.connect/isConnected (nautilus-ibapi 10.45.1),
Port 4004 = Paper aussen (gateway.py:43-45 in Nautilus 1.231.0).
2. Welches Konto ist angemeldet? Quelle: managedAccounts-Rueckruf. Ausgegeben: die ersten zwei
Zeichen und die Laenge (Paper-Konten beginnen im Doku-Beispiel
mit "DU"; das ist ein Beispiel, keine Zusage).
3. Welche Kontoart, welche Waehrung? Quelle: reqAccountSummary mit den Tags AccountType, Currency;
Betraege (NetLiquidation, TotalCashValue) nur als "vorhanden".
4. Wie viele Positionen? Quelle: reqPositions -> positionEnd; nur die Zahl.
5. Kommt ein verzoegerter Kurs? Quelle: reqMarketDataType(4, DELAYED_FROZEN; ohne Abo laut
Nautilus-Doku) und reqMktData(AAPL, SMART, USD, Snapshot).
Ausgegeben: welche Kursart kam (Tick-Typ), nie der Kurs.
6. Welche Fehler meldet das Gateway? Quelle: error-Rueckruf; Code und gekuerzter Text (ohne Zahlen
ueber vier Stellen).
Eine fehlende Antwort nach ZEITLIMIT_S ist ein Befund, kein Schweigen. Exit 0 alles beantwortet,
1 mindestens ein Befund, 3 keine Verbindung, 2 abgebrochen.
Aufruf (WSL, eigenes Venv): ~/venvs/ibkr-probe/bin/python ibkr_probe.py [--port 4004] [--client-id 991]
"""
import argparse
import re
import sys
import threading
import time
ZEITLIMIT_S = 30
NUR_LESEN_TAGS = "AccountType,Currency,NetLiquidation,TotalCashValue"
BETRAGS_TAGS = {"NetLiquidation", "TotalCashValue"}
def maskiere(text: str) -> str:
return re.sub(r"\d{5,}|\d+\.\d+", "[x]", str(text))
def main(argv=None) -> int:
zerleger = argparse.ArgumentParser(allow_abbrev=False)
zerleger.add_argument("--host", default="127.0.0.1")
zerleger.add_argument("--port", type=int, default=4004)
zerleger.add_argument("--client-id", type=int, default=991)
argumente = zerleger.parse_args(argv)
from ibapi.client import EClient
from ibapi.contract import Contract
from ibapi.wrapper import EWrapper
class Probe(EWrapper, EClient):
def __init__(self):
EWrapper.__init__(self)
EClient.__init__(self, self)
self.ereignisse = {name: threading.Event() for name in ("konto", "zusammenfassung", "positionen", "kurs", "zeit")}
self.konten = ""
self.zusammenfassung = {}
self.positionen = 0
self.kurs_arten = set()
self.fehler = []
self.zeit = None
def managedAccounts(self, accountsList):
self.konten = accountsList
self.ereignisse["konto"].set()
def currentTime(self, zeit):
self.zeit = zeit
self.ereignisse["zeit"].set()
def accountSummary(self, reqId, account, tag, value, currency):
self.zusammenfassung[tag] = "vorhanden" if tag in BETRAGS_TAGS else value
def accountSummaryEnd(self, reqId):
self.ereignisse["zusammenfassung"].set()
def position(self, account, contract, position, avgCost):
self.positionen += 1
def positionEnd(self):
self.ereignisse["positionen"].set()
def tickPrice(self, reqId, tickType, price, attrib):
if price and price > 0:
self.kurs_arten.add(tickType)
def tickSnapshotEnd(self, reqId):
self.ereignisse["kurs"].set()
def error(self, reqId, *rest):
# Die Signatur wechselt zwischen ibapi-Versionen: nautilus-ibapi 10.45.1 schickt vor dem
# Code einen Zeitstempel in Millisekunden (gemessen am 2026-09-24: 1790261211141 vor 502).
# Fehlercodes sind klein; der Zeitstempel ist es nicht.
werte = [r for r in rest if isinstance(r, (int, str))]
code = next((w for w in werte if isinstance(w, int) and 0 < w < 100000), None)
text = next((w for w in werte if isinstance(w, str) and len(w) > 3), "")
self.fehler.append((reqId, code, maskiere(text)[:160]))
probe = Probe()
befunde = []
try:
probe.connect(argumente.host, argumente.port, argumente.client_id)
faden = threading.Thread(target=probe.run, daemon=True)
faden.start()
beginn = time.time()
while not probe.isConnected() and time.time() - beginn < 10:
time.sleep(0.2)
if not probe.isConnected():
print(f"BEFUND: keine Verbindung zu {argumente.host}:{argumente.port}")
for f in probe.fehler[:5]:
print(f" fehler {f}")
return 3
print(f"verbunden mit {argumente.host}:{argumente.port}, Serverversion {probe.serverVersion()}")
probe.reqCurrentTime()
probe.reqAccountSummary(9001, "All", NUR_LESEN_TAGS)
probe.reqPositions()
probe.reqMarketDataType(4)
aapl = Contract()
aapl.symbol, aapl.secType, aapl.exchange, aapl.currency = "AAPL", "STK", "SMART", "USD"
probe.reqMktData(9002, aapl, "", True, False, [])
for name, ereignis in probe.ereignisse.items():
if not ereignis.wait(ZEITLIMIT_S):
befunde.append(f"keine Antwort auf {name} in {ZEITLIMIT_S} s")
konten = [k for k in probe.konten.split(",") if k]
print(f"konten: {len(konten)}; " + ", ".join(f"{k[:2]}… ({len(k)} Zeichen)" for k in konten))
print(f"serverzeit: {'da' if probe.zeit else 'fehlt'}")
print(f"kontoart: {probe.zusammenfassung.get('AccountType')}, waehrung: {probe.zusammenfassung.get('Currency')}, "
f"nettoliquidation: {probe.zusammenfassung.get('NetLiquidation', 'fehlt')}, bargeld: {probe.zusammenfassung.get('TotalCashValue', 'fehlt')}")
print(f"positionen: {probe.positionen}")
print(f"kursarten AAPL (Tick-Typen mit Preis): {sorted(probe.kurs_arten) or 'keine'}")
for f in probe.fehler:
print(f"gateway meldet: reqId {f[0]}, code {f[1]}: {f[2]}")
if not konten:
befunde.append("kein Konto gemeldet")
if not probe.kurs_arten:
befunde.append("kein verzoegerter Kurs fuer AAPL")
probe.cancelAccountSummary(9001)
probe.cancelPositions()
except Exception as ausnahme:
print(f"BEFUND: Probe abgebrochen: {type(ausnahme).__name__}: {maskiere(ausnahme)[:200]}")
return 2
finally:
if probe.isConnected():
probe.disconnect()
for befund in befunde:
print(f"BEFUND: {befund}")
print(f"ERGEBNIS: {len(befunde)} Befund(e)")
return 1 if befunde else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,44 @@
#!/bin/bash
# Traegt die Zugangsdaten des IBKR-Paper-Kontos ein. Pascal fuehrt es selbst in seinem Terminal aus;
# kein Agent sieht die Werte. Passwort ohne Anzeige, zweimal zur Kontrolle.
# Ablage: /home/claude/.config/trading-plattform/ibkr/ (Ordner 0700, Dateien 0600, Besitzer claude),
# neben der Geheimnisdatei aus Paket E2. Ausgegeben werden nur Dateinamen und Laengen.
# Koordinator-Werkzeug, nicht fuers Repo. Aufruf aus der PowerShell:
# wsl -d Debian -u claude -- bash /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/ibkr_zugang_eintragen.sh
set -eu
umask 077
# IBKR_ZIEL nur fuer die Probe des Koordinators mit erfundenen Werten in einem Wegwerf-Ordner.
ZIEL=${IBKR_ZIEL:-/home/claude/.config/trading-plattform/ibkr}
if [ "$(id -un)" != "claude" ]; then
echo "Bitte als Benutzer claude ausfuehren (wsl -d Debian -u claude -- bash ...)."
exit 2
fi
mkdir -p "$ZIEL"
chmod 700 "$ZIEL"
echo "IBKR-Paper-Konto: Zugangsdaten eintragen (nichts davon verlaesst diesen Rechner)."
read -r -p "Paper-Benutzername: " BENUTZER
read -r -s -p "Paper-Passwort (wird nicht angezeigt): " PASSWORT
echo
read -r -s -p "Passwort noch einmal: " PASSWORT2
echo
if [ -z "$BENUTZER" ] || [ -z "$PASSWORT" ]; then
echo "Benutzername oder Passwort leer, nichts geschrieben."
exit 2
fi
if [ "$PASSWORT" != "$PASSWORT2" ]; then
echo "Die beiden Passwoerter stimmen nicht ueberein, nichts geschrieben."
exit 2
fi
read -r -p "Paper-Kontonummer (beginnt meist mit DU; leer lassen, wenn du sie nicht zur Hand hast): " KONTO
printf '%s' "$BENUTZER" > "$ZIEL/paper_benutzer"
printf '%s' "$PASSWORT" > "$ZIEL/paper_passwort"
if [ -n "$KONTO" ]; then
printf '%s' "$KONTO" > "$ZIEL/paper_konto"
fi
unset PASSWORT PASSWORT2
chmod 600 "$ZIEL"/*
echo "Gespeichert unter $ZIEL, nur fuer den Benutzer claude lesbar:"
for datei in "$ZIEL"/*; do
printf ' %s: %s Zeichen\n' "$(basename "$datei")" "$(wc -c < "$datei" | tr -d ' ')"
done
echo "Fertig. Aendern: dieses Skript noch einmal ausfuehren. Loeschen: rm -r $ZIEL"
@@ -0,0 +1,236 @@
"""Rueckweg des Paper-Runners von zehn Aktien auf AAPL, einmal, nach Pascals Vorgabe.
Koordinator-Werkzeug, nicht fuers Repo. Pascal am 2026-09-24: zehn Aktien "bis Mittwoch" -- gelesen als
bis einschliesslich Mittwoch 2026-09-30; ab Donnerstag 2026-10-01 handelt der Runner wieder AAPL mit der
Registerversion. Der Rueckweg ist der aus Paket M: config/paper-aapl-register.toml wird config/paper.toml.
Geplante Aufgabe "Trading Plattform - Rueckweg AAPL" (rueckweg_aapl_einrichten.ps1): 2026-09-30 22:50,
zweiter Versuch 2026-10-01 22:10, verpasste Termine beim naechsten Start (StartWhenAvailable).
Schritte, jeder mit seinem Grund:
1. Nicht vor FRUEHESTENS (nach Boersenschluss des letzten Tages mit zehn Aktien).
2. Ergebnisdatei ABLAGE/rueckweg.json da -> schon erledigt, nichts tun.
3. Laeuft der Runner (systemctl is-active trading-paper.service in WSL), wird nichts getauscht: Er hat
seine Konfiguration beim Start gelesen, und ein Tausch mitten am Tag wirkte erst beim naechsten Start.
Befund, der zweite Termin versucht es erneut.
Stand er, wird nach WARTEN_S noch einmal gefragt: Hat erst dieser Aufruf WSL geweckt, startet der
Zeitgeber den Runner 60 s danach (OnStartupSec=60) und liest dabei die Konfiguration.
4. Im Projektordner muss ZWEIG ausgecheckt sein, sonst Befund.
5. config/paper.toml gleich der Rueckweg-Datei: sauberer Ordner -> schon getauscht (Ergebnisdatei,
Ende); nur config/paper.toml geaendert -> halb erledigt (ein gescheiterter Commit), weiter mit 8;
sonst Befund.
6. Sonst muss der Ordner sauber sein (git status --porcelain leer), sonst Befund: Ein Commit ueber
fremde Aenderungen waere schlimmer als ein Tag mehr mit zehn Aktien. Und config/paper.toml muss die
Konfiguration mit mehreren Aktien sein (Zeile 'quelle = "folge"'), sonst Befund.
7. Jeder Schluessel aus config/paper.toml ausser den erwarteten Unterschieden (ERWARTET_ANDERS) muss
in der Rueckweg-Datei mit demselben Wert stehen, sonst Befund: Der Tausch verloere ihn still oder
schaltete ihn still um (etwa [auto_fortsetzen] aktiv = false, von Pascal gesetzt).
8. Bytes kopieren, git add, git commit (lokal; gepusht wird von der naechsten Sitzung -- ein Push aus
einer Aufgabe ohne Fenster koennte an einer Anmeldeabfrage haengen).
9. Eine Zeile in Inbox/<heute>.md unter "## Agentenprotokoll"; bei einem Befund zusaetzlich eine Meldung
mit hoher Prioritaet ueber abendkontrolle_wsl.py --meldung.
Aufruf von Hand: python rueckweg_aapl.py [--jetzt ISO] [--projekt PFAD] [--vault PFAD] [--ohne-wsl]
"""
import argparse
import json
import subprocess
import sys
import time
import tomllib
import traceback
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from abendkontrolle import KEIN_FENSTER # noqa: E402
from abendkontrolle import WSL # noqa: E402
from abendkontrolle import WSL_PY # noqa: E402
from abendkontrolle import WSL_SKRIPT # noqa: E402
from abendkontrolle import schreibe_notiz # noqa: E402
VAULT = Path(r"D:\Claude")
PROJEKT = VAULT / "Code Projekte" / "active - Trading Plattform"
GIT = r"C:\Program Files\Git\cmd\git.exe"
FRUEHESTENS = datetime(2026, 9, 30, 22, 5)
ZIEL = Path("config") / "paper.toml"
RUECKWEG = Path("config") / "paper-aapl-register.toml"
MEHRERE = 'quelle = "folge"'
ZWEIG = "master"
ERWARTET_ANDERS = ("agent.", "umgebung.instrumente", "umgebung.instrument", "grenzen.max_tagesverlust_anteil")
WARTEN_S = 90
NOCHMAL = "Steht noch ein Termin der Aufgabe aus, versucht er es erneut; sonst entscheidet die naechste Sitzung."
NACHRICHT = (
"Rueckweg auf AAPL: config/paper.toml aus config/paper-aapl-register.toml\n\n"
"Pascal am 2026-09-24: zehn Aktien bis Mittwoch 2026-09-30. Ab 2026-10-01 handelt der Runner wieder\n"
"AAPL mit der Registerversion (Rueckweg aus Paket M). Automatisch durch die geplante Aufgabe\n"
"'Trading Plattform - Rueckweg AAPL' (.vault-tools/trading-plattform-pruefung/rueckweg_aapl.py).\n\n"
"Co-Authored-By: Claude Opus 5.5 <[email protected]>\n"
)
def git(projekt: Path, *argumente: str) -> subprocess.CompletedProcess:
return subprocess.run(
[GIT, "-C", str(projekt), *argumente], capture_output=True, text=True, encoding="utf-8",
errors="replace", timeout=120, creationflags=KEIN_FENSTER,
env={**__import__("os").environ, "GIT_TERMINAL_PROMPT": "0", "GCM_INTERACTIVE": "never"},
)
def runner_aktiv(vorgabe: str | None) -> bool:
if vorgabe is not None:
zustand = vorgabe # Angriffsfaelle: die Ausgabe von systemctl is-active, ohne WSL
else:
lauf = subprocess.run(
[WSL, "-d", "Debian", "-u", "claude", "--", "systemctl", "is-active", "trading-paper.service"],
capture_output=True, timeout=60, creationflags=KEIN_FENSTER,
)
zustand = lauf.stdout.decode("utf-8", errors="replace").strip()
if zustand in ("active", "activating", "deactivating", "reloading"):
return True
if zustand in ("inactive", "failed"):
return False
raise RuntimeError(f"Zustand des Runners nicht lesbar: {zustand!r}")
def melde(titel: str, text: str, ohne_wsl: bool) -> str:
if ohne_wsl:
return "ohne WSL, nicht gesendet"
lauf = subprocess.run(
[WSL, "-d", "Debian", "-u", "claude", "--", WSL_PY, WSL_SKRIPT, "--meldung", titel, text],
capture_output=True, timeout=120, creationflags=KEIN_FENSTER,
)
return lauf.stdout.decode("utf-8", errors="replace").strip()[-200:]
def blaetter(datei: Path) -> dict:
"""Alle Blattschluessel einer TOML-Datei als Punktpfad mit ihrem Wert."""
def ablaufen(tabelle: dict, pfad: str):
for name, wert in tabelle.items():
voll = f"{pfad}.{name}" if pfad else name
if isinstance(wert, dict):
yield from ablaufen(wert, voll)
else:
yield voll, wert
with datei.open("rb") as f:
return dict(ablaufen(tomllib.load(f), ""))
def fehlende_schluessel(ziel: Path, quelle: Path) -> list[str]:
"""Was der Tausch ausser den erwarteten Unterschieden aendern wuerde.
Projektvorgabe (Paket M, Rueckweg): Die beiden Dateien unterscheiden sich nur in [agent],
umgebung.instrumente (zehn Aktien) gegen umgebung.instrument (AAPL) und im Wert von
grenzen.max_tagesverlust_anteil (1 % gegen 0,2 %); verglichen am 2026-09-24 im Arbeitsbaum M.
Jeder andere Schluessel, der nur in config/paper.toml steht, ginge beim Tausch still verloren
(etwa [auto_fortsetzen] aus Paket AF oder etwas aus Z2), und jeder andere Wert, der sich
unterscheidet, sprang still um -- etwa auto_fortsetzen.aktiv, das Pascal in config/paper.toml
abgeschaltet hat (Befund der Plankritik AF, 2026-09-24).
"""
alt, neu = blaetter(ziel), blaetter(quelle)
befunde = []
for name in sorted(alt.keys() | neu.keys()):
if name.startswith(ERWARTET_ANDERS):
continue
if name not in neu:
befunde.append(f"{name} fehlt in der Rueckweg-Datei")
elif name not in alt:
# Die Gegenrichtung (Nachkritik AF, 2026-09-24): Hat Pascal [auto_fortsetzen] in
# config/paper.toml geloescht -- laut Task 421 ein gueltiges Abschalten --, brachte der
# Tausch aktiv = true aus der Rueckweg-Datei still zurueck.
befunde.append(f"{name} steht nur in der Rueckweg-Datei")
elif neu[name] != alt[name]:
befunde.append(f"{name} anderer Wert")
return befunde
def rueckweg(jetzt: datetime, projekt: Path, ablage: Path, runner_vorgabe: str | None) -> tuple[str, str | None]:
"""(Ergebnis fuer die Notiz, Befund oder None)."""
if jetzt < FRUEHESTENS:
return f"zu frueh ({jetzt:%Y-%m-%d %H:%M}), nichts getan", None
if (ablage / "rueckweg.json").exists():
return "schon erledigt", None
if runner_aktiv(runner_vorgabe):
return "", f"Der Runner laeuft; config/paper.toml nicht getauscht. {NOCHMAL}"
if runner_vorgabe is None:
# Hat erst dieser Aufruf WSL geweckt, startet der Zeitgeber den Runner 60 s danach
# (OnStartupSec=60) mit der Konfiguration, die er dann liest. Also warten und noch einmal fragen.
time.sleep(WARTEN_S)
if runner_aktiv(None):
return "", f"Der Runner ist eben gestartet; config/paper.toml nicht getauscht. {NOCHMAL}"
zweig = git(projekt, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
if zweig != ZWEIG:
return "", f"Im Projektordner ist {zweig!r} ausgecheckt, nicht {ZWEIG}; nicht getauscht"
ziel, quelle = projekt / ZIEL, projekt / RUECKWEG
if not quelle.is_file():
return "", f"{RUECKWEG.as_posix()} fehlt; nicht getauscht"
status = git(projekt, "status", "--porcelain")
if status.returncode != 0:
return "", f"git status scheiterte ({status.returncode}); nicht getauscht"
geaendert = [z for z in status.stdout.splitlines() if z.strip()]
if ziel.is_file() and ziel.read_bytes() == quelle.read_bytes():
if not geaendert:
return "config/paper.toml ist schon die AAPL-Konfiguration", None
if geaendert != [f" M {ZIEL.as_posix()}"] and geaendert != [f"M {ZIEL.as_posix()}"]:
return "", f"Projektordner nicht sauber (git status: {' | '.join(geaendert)[:200]}); nicht getauscht"
# Halb erledigt: getauscht, aber nicht committet (etwa ein gescheiterter Commit beim ersten Termin).
else:
if geaendert:
return "", f"Projektordner nicht sauber (git status: {' | '.join(geaendert)[:200]}); nicht getauscht"
if MEHRERE not in ziel.read_text(encoding="utf-8"):
return "", "config/paper.toml ist nicht die Konfiguration mit mehreren Aktien; nicht getauscht, Pascal entscheidet"
fehlend = fehlende_schluessel(ziel, quelle)
if fehlend:
return "", (
f"config/paper-aapl-register.toml weicht ausserhalb der erwarteten Stellen von config/paper.toml ab "
f"({', '.join(fehlend)[:200]}); nicht getauscht, Pascal entscheidet"
)
ziel.write_bytes(quelle.read_bytes())
for schritt in (("add", ZIEL.as_posix()), ("commit", "-m", NACHRICHT)):
lauf = git(projekt, *schritt)
if lauf.returncode != 0:
return "", (
f"git {schritt[0]} scheiterte ({lauf.returncode}: {(lauf.stderr or lauf.stdout).strip()[:200]}); "
f"Datei ist getauscht, aber nicht committet. {NOCHMAL}"
)
kopf = git(projekt, "rev-parse", "--short", "HEAD").stdout.strip()
return f"config/paper.toml auf AAPL (Register) zurueckgestellt, Commit {kopf} (nicht gepusht)", None
def main(argv=None) -> int:
zerleger = argparse.ArgumentParser(allow_abbrev=False)
zerleger.add_argument("--jetzt")
zerleger.add_argument("--projekt", default=str(PROJEKT))
zerleger.add_argument("--vault", default=str(VAULT))
zerleger.add_argument("--ohne-wsl", action="store_true", help="Angriffsfaelle: keine Meldung, Runner laut --runner-zustand")
zerleger.add_argument("--runner-zustand", help="Angriffsfaelle: Ausgabe von systemctl is-active statt WSL")
argumente = zerleger.parse_args(argv)
if argumente.ohne_wsl and argumente.runner_zustand is None:
argumente.runner_zustand = "inactive"
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
projekt, vault = Path(argumente.projekt), Path(argumente.vault)
ablage = projekt / "data" / "paper" / "abendkontrollen"
ablage.mkdir(parents=True, exist_ok=True)
try:
ergebnis, befund = rueckweg(jetzt, projekt, ablage, argumente.runner_zustand)
except Exception as ausnahme:
ergebnis, befund = "", f"abgebrochen: {type(ausnahme).__name__}: {str(ausnahme)[:200]}"
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as log:
log.write(traceback.format_exc())
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as log:
log.write(f"{datetime.now().isoformat(timespec='seconds')} rueckweg: {ergebnis or befund}\n")
if ergebnis in ("schon erledigt",) or ergebnis.startswith("zu frueh"):
return 0
kopf = f"- {jetzt:%H:%M} Uhr — Rückweg auf AAPL (automatisch, Pascals Vorgabe: zehn Aktien bis 30.09.)"
if befund is None:
(ablage / "rueckweg.json").write_text(
json.dumps({"zeit": jetzt.isoformat(), "ergebnis": ergebnis}, ensure_ascii=False), encoding="utf-8"
)
schreibe_notiz(vault, jetzt.date().isoformat(), f"{kopf}: {ergebnis}.")
return 0
gemeldet = melde("Rueckweg AAPL nicht ausgefuehrt", befund, argumente.ohne_wsl)
schreibe_notiz(vault, jetzt.date().isoformat(), f"{kopf}: **nicht ausgeführt** — {befund} #needs-review (Meldung: {gemeldet})")
return 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,113 @@
"""Angriffsfaelle fuer rueckweg_aapl.py auf einem Wegwerf-Git-Repo. Koordinator-Werkzeug, nicht fuers Repo.
Jeder Fall einzeln, zuerst die Grundlinie; ohne WSL (--ohne-wsl, Runnerzustand vorgegeben), ohne Meldung.
Aufruf unter Windows: python rueckweg_aapl_angriffe.py Exit 0, wenn jeder Fall wie erwartet ausgeht.
"""
import subprocess
import sys
import tempfile
from pathlib import Path
HIER = Path(__file__).resolve().parent
sys.path.insert(0, str(HIER))
import rueckweg_aapl as rw # noqa: E402
ZEHN = '[agent]\nquelle = "folge"\ninstrumente = ["AAPL.XNAS", "MSFT.XNAS"]\n'
AAPL = '[agent]\nquelle = "register"\ninstrument = "AAPL.XNAS"\n'
abweichungen: list[str] = []
def repo(tmp: Path, paper: str | None = ZEHN, aapl: str | None = AAPL) -> Path:
projekt = tmp / "projekt"
(projekt / "config").mkdir(parents=True)
if paper is not None:
(projekt / "config" / "paper.toml").write_bytes(paper.encode("utf-8"))
if aapl is not None:
(projekt / "config" / "paper-aapl-register.toml").write_bytes(aapl.encode("utf-8"))
(projekt / ".gitignore").write_text("data/\n", encoding="utf-8")
for befehl in (["init", "-q", "-b", "master"], ["config", "user.email", "[email protected]"], ["config", "user.name", "Test"],
["config", "core.autocrlf", "false"], ["add", "-A"], ["commit", "-q", "-m", "start"]):
subprocess.run([rw.GIT, "-C", str(projekt), *befehl], check=True, capture_output=True)
(tmp / "Inbox").mkdir()
return projekt
def fall(name, *, jetzt="2026-09-30T22:50:00", vorbereiten=None, runner="inactive", paper=ZEHN, aapl=AAPL,
erwartet_code, getauscht, notiz, marke, commits, grund=None):
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
projekt = repo(tmp, paper, aapl)
if vorbereiten:
vorbereiten(projekt)
vorher = subprocess.run([rw.GIT, "-C", str(projekt), "rev-list", "--count", "HEAD"], capture_output=True, text=True).stdout.strip()
code = rw.main(["--jetzt", jetzt, "--projekt", str(projekt), "--vault", str(tmp), "--ohne-wsl", "--runner-zustand", runner])
paper_jetzt = (projekt / "config" / "paper.toml").read_text(encoding="utf-8") if (projekt / "config" / "paper.toml").exists() else None
notizen = list((tmp / "Inbox").glob("*.md"))
text = notizen[0].read_text(encoding="utf-8") if notizen else ""
hat_marke = (projekt / "data" / "paper" / "abendkontrollen" / "rueckweg.json").exists()
nachher = subprocess.run([rw.GIT, "-C", str(projekt), "rev-list", "--count", "HEAD"], capture_output=True, text=True).stdout.strip()
ist = {
"code": code,
"getauscht": paper_jetzt == AAPL,
"notiz": ("nicht ausgeführt" if "nicht ausgeführt" in text else ("ja" if "Rückweg auf AAPL" in text else "nein")),
"marke": hat_marke,
"commits": int(nachher) - int(vorher),
}
soll = {"code": erwartet_code, "getauscht": getauscht, "notiz": notiz, "marke": marke, "commits": commits}
ok = ist == soll and (grund is None or grund in text)
zeile = f"{'OK ' if ok else 'FEHL'} {name}: {ist}" + ("" if ok else f" SOLL {soll}")
print(zeile)
if not ok:
abweichungen.append(zeile)
fall("Grundlinie", erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=1)
fall("zu frueh", jetzt="2026-09-30T21:59:00", erwartet_code=0, getauscht=False, notiz="nein", marke=False, commits=0)
fall("Ergebnisdatei schon da",
vorbereiten=lambda p: ((p / "data" / "paper" / "abendkontrollen").mkdir(parents=True), (p / "data" / "paper" / "abendkontrollen" / "rueckweg.json").write_text("{}")),
erwartet_code=0, getauscht=False, notiz="nein", marke=True, commits=0)
fall("Runner aktiv", runner="active", erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("Runner deactivating", runner="deactivating", erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("Runnerzustand unlesbar", runner="", erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("Runner failed (steht)", runner="failed", erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=1)
fall("schon AAPL", paper=AAPL, erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=0)
fall("Ordner nicht sauber (fremde Aenderung)",
vorbereiten=lambda p: (p / "config" / "risk.toml").write_text("x\n"),
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("paper.toml geaendert, nicht committet",
vorbereiten=lambda p: (p / "config" / "paper.toml").write_text(ZEHN + "# neu\n"),
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("paper.toml ohne folge", paper='[agent]\nquelle = "halte"\n',
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("Rueckweg-Datei fehlt", aapl=None, erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("zweiter Termin Donnerstag", jetzt="2026-10-01T22:10:00", erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=1)
# Befunde des Pruefers: halb erledigter Tausch, verlorener Abschnitt, falscher Zweig
fall("halb getauscht (Datei getauscht, nicht committet)",
vorbereiten=lambda p: (p / "config" / "paper.toml").write_bytes(AAPL.encode("utf-8")),
erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=1)
AUTO = "\n[auto_fortsetzen]\naktiv = true\n"
fall("halb getauscht und fremde Aenderung",
vorbereiten=lambda p: ((p / "config" / "paper.toml").write_bytes(AAPL.encode("utf-8")), (p / "config" / "risk.toml").write_text("x\n")),
erwartet_code=1, getauscht=True, notiz="nicht ausgeführt", marke=False, commits=0)
fall("Abschnitt nur in paper.toml", paper=ZEHN + AUTO,
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("Abschnitt in beiden", paper=ZEHN + AUTO, aapl=AAPL + AUTO,
erwartet_code=0, getauscht=False, notiz="ja", marke=True, commits=1)
AUS = "\n[auto_fortsetzen]\naktiv = false\n"
fall("Automatik nur in paper.toml abgeschaltet", paper=ZEHN + AUS, aapl=AAPL + AUTO,
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("Abschnitt in paper.toml geloescht, in der Rueckweg-Datei noch da", paper=ZEHN, aapl=AAPL + AUTO,
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0,
grund="auto_fortsetzen.aktiv steht nur in der Rueckweg-Datei")
fall("anderer Wert in [grenzen]", paper=ZEHN + "\n[grenzen]\nmax_orders = 5\n", aapl=AAPL + "\n[grenzen]\nmax_orders = 10\n",
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
fall("Tagesverlust darf sich unterscheiden",
paper=ZEHN + "\n[grenzen]\nmax_tagesverlust_anteil = 0.01\n", aapl=AAPL + "\n[grenzen]\nmax_tagesverlust_anteil = 0.002\n",
erwartet_code=0, getauscht=False, notiz="ja", marke=True, commits=1)
fall("anderer Zweig ausgecheckt",
vorbereiten=lambda p: subprocess.run([rw.GIT, "-C", str(p), "checkout", "-q", "-b", "z2"], check=True),
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
print(f"ERGEBNIS: {len(abweichungen)} Abweichung(en)")
sys.exit(1 if abweichungen else 0)
@@ -0,0 +1,37 @@
# Richtet die einmalige Windows-Aufgabe "Trading Plattform - Rueckweg AAPL" ein oder entfernt sie.
# Koordinator-Werkzeug, nicht fuers Repo. Pascal 2026-09-24: zehn Aktien bis Mittwoch 2026-09-30.
# Zwei Termine: Mi 2026-09-30 22:50 und als zweiter Versuch Do 2026-10-01 22:10; StartWhenAvailable holt
# einen verpassten Termin beim naechsten Start nach. rueckweg_aapl.py tut nach dem ersten Erfolg nichts mehr.
#
# Aufruf: powershell -NoProfile -File rueckweg_aapl_einrichten.ps1 [-Aktion einrichten|entfernen]
param(
[ValidateSet("einrichten", "entfernen")]
[string]$Aktion = "einrichten"
)
$Name = "Trading Plattform - Rueckweg AAPL"
if ($Aktion -eq "entfernen") {
Unregister-ScheduledTask -TaskName $Name -Confirm:$false
return
}
$Py = "C:\Users\Pascal Michel\AppData\Local\Programs\Python\Python313\pythonw.exe"
$Skript = "D:\Claude\.vault-tools\trading-plattform-pruefung\rueckweg_aapl.py"
if (-not (Test-Path $Py)) { throw "Interpreter fehlt: $Py" }
if (-not (Test-Path $Skript)) { throw "Skript fehlt: $Skript" }
$Benutzer = "$env:USERDOMAIN\$env:USERNAME"
$Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '"') -WorkingDirectory "D:\Claude"
$Erster = New-ScheduledTaskTrigger -Once -At ([datetime]"2026-09-30T22:50:00")
$Zweiter = New-ScheduledTaskTrigger -Once -At ([datetime]"2026-10-01T22:10:00")
$Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited
$Regeln = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Minutes 15) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName $Name -Action $Aufruf -Trigger @($Erster, $Zweiter) -Principal $Wer `
-Settings $Regeln -Force `
-Description "Rueckweg des Paper-Runners auf AAPL nach Pascals Vorgabe (zehn Aktien bis 2026-09-30). Siehe rueckweg_aapl.py." | Out-Null
$Aufgabe = Get-ScheduledTask -TaskName $Name
"{0}: {1}" -f $Aufgabe.TaskName, $Aufgabe.State
$Aufgabe.Triggers | ForEach-Object { "Ausloeser: {0} {1}" -f $_.CimClass.CimClassName, $_.StartBoundary }
"Naechster Lauf: {0}" -f (Get-ScheduledTaskInfo -TaskName $Name).NextRunTime