"""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/.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 ] """ 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())