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