"""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 """ 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 <{'|'.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())