Files
claude-regelwerk/.vault-tools/trading-plattform-pruefung/generalprobe_echtlauf.py
T
Pascal MichelandClaude Opus 5 593acdd122 Automatischer Push: 14 Datei(en) geändert
- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/bremse_echtlauf.py
- .vault-tools/trading-plattform-pruefung/browser_w.py
- .vault-tools/trading-plattform-pruefung/generalprobe_echtlauf.py
- .vault-tools/trading-plattform-pruefung/ha_aktiv_setzen.py
- .vault-tools/trading-plattform-pruefung/handelstag_auswertung.py
- .vault-tools/trading-plattform-pruefung/hausmelder_echtlauf.py
- .vault-tools/trading-plattform-pruefung/killswitch_simulation.py
- .vault-tools/trading-plattform-pruefung/killswitch_simulation_mutanten.py
- .vault-tools/trading-plattform-pruefung/latenz_auswertung.py
- .vault-tools/trading-plattform-pruefung/s1_alpaca_lesen.py
- .vault-tools/trading-plattform-pruefung/s5_echtlauf.py
- … und 2 weitere

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

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 04:00:01 +02:00

344 lines
15 KiB
Python

"""Eigener Nachtlauf der Generalprobe: _generalprobe aus scripts/paper.py mit echten Kerzen, ohne Netz.
Koordinator-Pruefcode, nicht Teil des Repos. Gerufen wird die echte Funktion _generalprobe -- dieselbe
Verdrahtung wie um 9:00 Uhr --, umgebogen sind nur die Stellen, die sonst ins Netz oder in den Betrieb
gingen:
- REGISTER zeigt auf eine Sicherungskopie des echten Registers in einem Wegwerf-Ordner (das echte wird
nur lesend geoeffnet), ARTEFAKTE auf den echten artifacts-Ordner (nur lesen: das Registermodell).
- Statusdatei und Protokollordner liegen im Wegwerf-Ordner.
- AlpacaKerzenquelle.from_secrets liefert die echten IEX-Kerzen des Tages aus einer JSON-Datei.
- AlpacaBroker.from_secrets liefert ein mitschreibendes Konto, das glatt ist und bei jedem schreibenden
Aufruf wirft -- damit steht im Bericht, was die Generalprobe am "echten" Broker ruft.
- requests.Session.request schreibt mit und wirft: Jeder Netzversuch des Laufs faellt auf.
- Der Melder ist ein echter Melder mit mitschreibendem Transport, in der Huelle GeneralprobeMelder.
"Pascal" klickt aus einem zweiten Faden -- wie das Dashboard mit einem frischen Killswitch-Objekt aus dem
Register, am mitschreibenden Konto: fortsetzen, spaeter anhalten und fortsetzen, glattstellen, freigeben.
Aufruf (WSL): python generalprobe_echtlauf.py <code-baum> <echter-projektordner> <kerzen.json> <wegwerf-ordner> [tempo]
"""
import importlib.util
import json
import os
import sqlite3
import sys
import threading
import time
from dataclasses import replace
from datetime import UTC
from datetime import datetime
from datetime import timedelta
from pathlib import Path
CODE = Path(sys.argv[1]).resolve()
ECHT = Path(sys.argv[2]).resolve()
KERZEN = Path(sys.argv[3]).resolve()
WEG = Path(sys.argv[4]).resolve()
TEMPO = float(sys.argv[5]) if len(sys.argv) > 5 else 1.0
os.chdir(CODE)
sys.path.insert(0, str(CODE))
import requests # noqa: E402
from trading.broker.schnittstelle import Marktuhr # noqa: E402
from trading.killswitch.killswitch import Killswitch # noqa: E402
from trading.killswitch.killswitch import lade_killswitch_config # noqa: E402
from trading.paper.config import lade_paper_config # noqa: E402
from trading.paper.kerzen import HANDELSZONE # noqa: E402
from trading.paper.kerzen import Kerze # noqa: E402
from trading.paper.melder import Melder # noqa: E402
from trading.paper.melder import MelderConfig # noqa: E402
from trading.paper.status import lies_status # noqa: E402
from trading.registry.registry import Registry # noqa: E402
NETZVERSUCHE: list[str] = []
def _kein_netz(self, method, url, *args, **kwargs):
NETZVERSUCHE.append(f"{method} {url.split('?')[0]}")
raise requests.ConnectionError("Nachtlauf: kein Netz")
requests.sessions.Session.request = _kein_netz
class Antwort:
status_code = 200
class Transport:
def __init__(self):
self.aufrufe = []
def __call__(self, url, *, data=None, headers=None, timeout=None, **rest):
rumpf = data.decode("utf-8") if isinstance(data, bytes) else str(data)
self.aufrufe.append(
{
"titel": headers.get("Title"),
"prioritaet": headers.get("Priority"),
"erste_zeile": rumpf.split("\n")[0],
"echt": datetime.now(UTC).isoformat(timespec="seconds"),
}
)
return Antwort()
class MitschreibendesKonto:
"""Steht fuer das echte Alpaca-Paper-Konto: glatt, keine Order, Boerse zu. Schreiben wirft."""
def __init__(self, name):
self.name = name
self.aufrufe = []
def positionen(self):
self.aufrufe.append("positionen")
return []
def offene_orders(self):
self.aufrufe.append("offene_orders")
return []
def marktuhr(self):
self.aufrufe.append("marktuhr")
jetzt = datetime.now(UTC)
return Marktuhr(is_open=False, next_open=jetzt + timedelta(hours=8),
next_close=jetzt + timedelta(hours=14))
def storniere_alle(self):
self.aufrufe.append("storniere_alle")
return []
def schliesse_alle(self):
self.aufrufe.append("schliesse_alle")
return []
def __getattr__(self, name):
def wirft(*a, **k):
self.aufrufe.append(f"VERBOTEN:{name}")
raise AssertionError(f"{self.name}: unerwarteter Aufruf {name}")
return wirft
def lade_kerzen(pfad):
roh = json.loads(Path(pfad).read_text(encoding="utf-8"))
return [
Kerze(zeit=datetime.fromisoformat(e["zeit"]), open=e["open"], high=e["high"],
low=e["low"], close=e["close"], volume=e["volume"])
for e in roh
]
def main():
kerzen = lade_kerzen(KERZEN)
tag = kerzen[0].zeit.astimezone(HANDELSZONE).date().isoformat()
WEG.mkdir(parents=True, exist_ok=False)
# Sicherungskopie des echten Registers; das echte wird nur lesend geoeffnet.
quelle = sqlite3.connect(f"file:{ECHT / 'data' / 'registry.db'}?mode=ro", uri=True)
ziel = sqlite3.connect(WEG / "registry.db")
quelle.backup(ziel)
quelle.close()
# Die Spalte artefakt_pfad ist relativ zum Projektordner. In der Kopie wird sie absolut, damit der
# Lauf aus dem Code-Baum heraus das Registermodell im echten artifacts-Ordner findet (nur lesen).
for zeile in ziel.execute("SELECT agent, version, artefakt_pfad FROM versionen").fetchall():
pfad = Path(zeile[2].replace("\\", "/"))
if not pfad.is_absolute():
ziel.execute("UPDATE versionen SET artefakt_pfad = ? WHERE agent = ? AND version = ?",
(str(ECHT / pfad), zeile[0], zeile[1]))
ziel.commit()
ziel.close()
spec = importlib.util.spec_from_file_location("paper_skript_nachtlauf", CODE / "scripts" / "paper.py")
paper = importlib.util.module_from_spec(spec)
spec.loader.exec_module(paper)
paper.REGISTER = str(WEG / "registry.db")
paper.ARTEFAKTE = str(ECHT / "artifacts")
konto_der_probe = MitschreibendesKonto("Konto der Generalprobe")
konto_des_dashboards = MitschreibendesKonto("Konto des Dashboards")
class Kerzenquelle:
@classmethod
def from_secrets(cls, *a, **k):
return cls()
def kerzen_des_tages(self, instrument_id, ende):
return list(kerzen)
class BrokerFabrik:
@classmethod
def from_secrets(cls, *a, **k):
return konto_der_probe
paper.AlpacaKerzenquelle = Kerzenquelle
paper.AlpacaBroker = BrokerFabrik
config = replace(
lade_paper_config("config/paper.toml"),
status_datei=str(WEG / "paper" / "status.json"),
protokoll_ordner=str(WEG / "protokolle"),
)
ks_config = lade_killswitch_config("config/killswitch.toml")
transport = Transport()
melder = Melder(
config=MelderConfig(aktiv=True, ntfy_url="https://ntfy.sh", thema="nachtlauf-geht-nie-ins-netz",
zeitlimit_s=3.0, stille_nach_s=180.0),
token="", transport=transport,
)
beobachtet = {"max_alter_s": 0.0, "modi": set(), "marktzeiten": [], "phasen": [], "klicks": [],
"rueckwaerts": 0, "position_nach_glatt": None, "fehler": []}
halt = threading.Event()
def dashboard_killswitch():
registry = Registry(db_pfad=WEG / "registry.db", artefakt_wurzel=ECHT / "artifacts")
gespeichert = registry.killswitch_zustand()
return Killswitch(broker=konto_des_dashboards, registry=registry, config=ks_config,
zustand=gespeichert.zustand if gespeichert is not None else "angehalten")
def klick(was, marktzeit, position):
ks = dashboard_killswitch()
vorher = ks.zustand
if was == "glattstellen":
ergebnis = ks.glattstellen("pascal", "Nachtlauf", ks_config.bestaetigungswort)
else:
ergebnis = getattr(ks, was)("pascal", "Nachtlauf")
beobachtet["klicks"].append(
f"{marktzeit.astimezone(HANDELSZONE):%H:%M} {was}: {vorher} -> {ergebnis.zustand}, "
f"erfolgreich={ergebnis.erfolgreich}, gespielte Position vorher {position}"
)
def pascal():
plan = [("fortsetzen", (9, 31)), ("anhalten", (10, 30)), ("fortsetzen", (10, 36)),
("glattstellen", (12, 0)), ("freigeben", (12, 10))]
letzte = None
glatt_um = None
while not halt.is_set():
status = lies_status(config.status_datei)
if isinstance(status, dict):
try:
zeit = datetime.fromisoformat(status["zeit"])
markt = datetime.fromisoformat(status["marktzeit"])
except (KeyError, ValueError, TypeError) as ausnahme:
beobachtet["fehler"].append(repr(ausnahme))
time.sleep(0.2)
continue
if status.get("phase") != "beendet":
alter = (datetime.now(UTC) - zeit).total_seconds()
beobachtet["max_alter_s"] = max(beobachtet["max_alter_s"], alter)
beobachtet["modi"].add(status.get("modus"))
if not beobachtet["phasen"] or beobachtet["phasen"][-1] != status.get("phase"):
beobachtet["phasen"].append(status.get("phase"))
if letzte is not None and markt < letzte:
beobachtet["rueckwaerts"] += 1
if letzte is None or markt != letzte:
beobachtet["marktzeiten"].append(markt)
letzte = markt
ort = markt.astimezone(HANDELSZONE)
if plan and (ort.hour, ort.minute) >= plan[0][1]:
was, _ = plan.pop(0)
try:
klick(was, markt, status.get("position"))
except Exception as ausnahme:
beobachtet["klicks"].append(f"{ort:%H:%M} {was}: AUSNAHME {ausnahme!r}")
if was == "glattstellen":
glatt_um = markt
if glatt_um is not None and beobachtet["position_nach_glatt"] is None \
and markt >= glatt_um + timedelta(minutes=2):
beobachtet["position_nach_glatt"] = status.get("position")
time.sleep(0.2)
faden = threading.Thread(target=pascal, name="pascal", daemon=True)
faden.start()
beginn = time.monotonic()
code = paper._generalprobe(config, tag, TEMPO, melder)
dauer = time.monotonic() - beginn
time.sleep(0.5)
halt.set()
faden.join(timeout=5)
print("=" * 70)
print(f"Tag {tag}, {len(kerzen)} Kerzen, Tempo {TEMPO:g} s je Marktminute, echte Dauer {dauer:.0f} s, Exit {code}")
schluss = lies_status(config.status_datei)
print(f"Statusdatei am Ende: modus={schluss.get('modus')}, phase={schluss.get('phase')}, "
f"position={schluss.get('position')}, schluessel={sorted(schluss)}")
print(f"Modi gesehen: {sorted(str(m) for m in beobachtet['modi'])}; Phasen: {beobachtet['phasen']}")
zeiten = beobachtet["marktzeiten"]
print(f"Marktzeit: {len(zeiten)} verschiedene Werte, von {zeiten[0].astimezone(HANDELSZONE):%Y-%m-%d %H:%M} "
f"bis {zeiten[-1].astimezone(HANDELSZONE):%Y-%m-%d %H:%M}, Ruecksprung {beobachtet['rueckwaerts']}")
print(f"Groesstes Alter des Herzschlags gegen die echte Uhr: {beobachtet['max_alter_s']:.1f} s")
print("Klicks:")
for zeile in beobachtet["klicks"]:
print(" " + zeile)
print(f"Gespielte Position zwei Marktminuten nach dem Glattstellen: {beobachtet['position_nach_glatt']}")
print(f"Lesefehler im Beobachter: {beobachtet['fehler'][:3]}")
protokolle = sorted((WEG / "protokolle").glob("*.jsonl"))
zeilen = []
for pfad in protokolle:
with pfad.open(encoding="utf-8") as datei:
zeilen += [json.loads(zeile) for zeile in datei if zeile.strip()]
takte = [z for z in zeilen if z.get("art") == "takt"]
arten = {}
for z in zeilen:
schluessel = z.get("art") or z.get("was") or "?"
arten[schluessel] = arten.get(schluessel, 0) + 1
print(f"Protokoll: {[p.name for p in protokolle]}, {len(zeilen)} Zeilen, Arten {arten}")
orders = [z for z in zeilen if z.get("art") == "order" or z.get("was") == "order"]
print(f"Takte {len(takte)}, Orderzeilen {len(orders)}")
if takte:
print(f"Erster Takt: {json.dumps(takte[0], ensure_ascii=False)[:400]}")
print(f"Letzter Takt: {json.dumps(takte[-1], ensure_ascii=False)[:400]}")
print(f"Meldungen ({len(transport.aufrufe)}):")
for m in transport.aufrufe:
print(f" {m['echt']} [{m['prioritaet']}] {m['titel']} | {m['erste_zeile']}")
print(f"Aufrufe am Konto der Generalprobe: {konto_der_probe.aufrufe}")
print(f"Aufrufe am Konto des Dashboards: {konto_des_dashboards.aufrufe}")
print(f"Netzversuche ueber requests: {NETZVERSUCHE}")
tagesstart = list(WEG.rglob("tagesstart.json"))
print(f"tagesstart.json im Wegwerf-Ordner: {tagesstart}")
registry = Registry(db_pfad=WEG / "registry.db", artefakt_wurzel=ECHT / "artifacts")
ereignisse = registry.killswitch_ereignisse()
frisch = [e for e in ereignisse if getattr(e, "grund", "") and "Nachtlauf" in e.grund
or getattr(e, "ausloeser", "") in ("neustart", "paper-runner")]
print(f"Killswitch-Ereignisse im Wegwerf-Register: {len(ereignisse)} gesamt; die letzten acht:")
for e in ereignisse[-8:] if len(ereignisse) >= 8 else ereignisse:
print(f" {getattr(e, 'zeit', '?')} {e.stufe} / {e.ausloeser} / {e.grund[:60]} / erfolgreich={e.erfolgreich}")
print(f"Killswitch am Ende: {registry.killswitch_zustand().zustand}")
befunde = []
if code != 0:
befunde.append(f"Exit {code} statt 0")
if beobachtet["modi"] != {"generalprobe"}:
befunde.append(f"Modi {beobachtet['modi']}")
if any(not str(m["titel"]).startswith("Generalprobe: ") for m in transport.aufrufe):
befunde.append("eine Meldung ohne Vorsatz")
if not transport.aufrufe:
befunde.append("keine Meldung")
if NETZVERSUCHE:
befunde.append("Netzversuch")
if sorted(konto_der_probe.aufrufe) != ["marktuhr", "offene_orders", "positionen"]:
befunde.append("Konto der Generalprobe: andere Aufrufe als die drei lesenden")
if any(a.startswith("VERBOTEN") for a in konto_des_dashboards.aufrufe):
befunde.append("Konto des Dashboards: verbotener Aufruf")
if beobachtet["position_nach_glatt"] != 0:
befunde.append(f"Position nach dem Glattstellen {beobachtet['position_nach_glatt']}")
if beobachtet["max_alter_s"] > 5.0:
befunde.append(f"Herzschlag bis {beobachtet['max_alter_s']:.1f} s alt")
if beobachtet["rueckwaerts"]:
befunde.append("Marktzeit lief rueckwaerts")
if tagesstart:
befunde.append("tagesstart.json entstanden")
if schluss.get("position") != 0:
befunde.append(f"Position am Ende {schluss.get('position')}")
print(f"{len(befunde)} Befund(e): {befunde}")
return 2 if befunde else 0
if __name__ == "__main__":
sys.exit(main())