Files
claude-regelwerk/.vault-tools/trading-plattform-pruefung/latenz_auswertung.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

132 lines
6.0 KiB
Python

"""Rechnet Reaktionszeiten aus dem, was ein Lauf des Paper-Runners ohnehin hinterlaesst -- nur lesend.
Koordinator-Werkzeug fuer Tage, an denen der Runner seine Latenzen noch nicht selbst misst (vor Paket L).
Quellen: das JSONL-Protokoll des Laufs (jede Zeile traegt echte Zeit, auch in der Generalprobe) und die
Tabelle killswitch_ereignisse im Register (Zeitpunkt jedes Klicks). Das Register wird mit mode=ro geoeffnet.
Gerechnet wird:
Klick -> Runner reagiert Registerereignis (Ausloeser pascal, erfolgreich) bis zur ersten Taktzeile
danach, die den neuen Zustand traegt. Die Taktzeile entsteht am ENDE des
Takts -- der Wert ist eine obere Schranke fuer "gelesen" und sagt, ab wann
der neue Zustand im Protokoll wirkt.
Kerze -> Taktende nur im Modus paper: Taktzeile mit frischer Kerze und Aktion, Zeit der Zeile
minus (juengste Kerze + 60 s). Obere Schranke fuer Kerze -> Entscheidung,
weil die Zeile erst nach den Orders geschrieben wird. In der Generalprobe
sinnlos (Kerzen von einem anderen Tag) und deshalb ausgelassen.
Taktabstand Abstand aufeinanderfolgender Taktzeilen -- der Rahmen fuer alles andere.
Nicht gerechnet: Fuellzeit (dafuer handelstag_auswertung.py --broker) und Meldungen (ntfy_lesen.py).
Gibt nie einen Kontowert aus.
Aufruf (WSL, aus dem Projektordner):
python latenz_auswertung.py <protokoll.jsonl> [<register.db>]
"""
import json
import sqlite3
import statistics
import sys
from datetime import datetime
from datetime import timedelta
from pathlib import Path
NEUER_ZUSTAND = {"anhalten": "angehalten", "glattstellen": "gesperrt", "fortsetzen": "aktiv", "freigeben": "aktiv"}
KLICKER = "pascal"
def zeit(text: str) -> datetime:
return datetime.fromisoformat(text)
def kennzahlen(name: str, werte: list[float], einheit: str = "s") -> None:
if not werte:
print(f" {name}: keine Messung")
return
sortiert = sorted(werte)
p90 = sortiert[min(len(sortiert) - 1, int(round(0.9 * (len(sortiert) - 1))))]
print(
f" {name}: n={len(werte)} Median {statistics.median(werte):.1f} {einheit} "
f"Mittel {statistics.fmean(werte):.1f} p90 {p90:.1f} Min {sortiert[0]:.1f} Max {sortiert[-1]:.1f}"
)
def main() -> int:
protokoll = Path(sys.argv[1])
register = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("data/registry.db")
zeilen = []
with open(protokoll, encoding="utf-8") as strom: # Dateiiteration, nie splitlines (U+2028)
for roh in strom:
if roh.strip():
zeilen.append(json.loads(roh))
kopf = next((z for z in zeilen if z.get("art") == "kopf"), {})
takte = [z for z in zeilen if z.get("art") == "takt"]
modus = kopf.get("modus", "unbekannt")
if not takte:
print("Keine Taktzeile im Protokoll.")
return 1
start, ende = zeit(zeilen[0]["zeit"]), zeit(zeilen[-1]["zeit"])
print(f"Protokoll {protokoll.name}: Modus {modus}, {len(takte)} Takte, {start:%H:%M:%S} bis {ende:%H:%M:%S} UTC")
abstaende = [
(zeit(b["zeit"]) - zeit(a["zeit"])).total_seconds() for a, b in zip(takte, takte[1:])
]
print("Rahmen:")
kennzahlen("Taktabstand", abstaende)
print("Klick -> Runner reagiert (Register gegen erste Taktzeile mit dem neuen Zustand):")
if not register.exists():
print(f" Register {register} fehlt -- ausgelassen")
else:
verbindung = sqlite3.connect(f"file:{register.as_posix()}?mode=ro", uri=True)
try:
ereignisse = verbindung.execute(
"SELECT zeit, stufe, ausloeser, erfolgreich FROM killswitch_ereignisse ORDER BY id"
).fetchall()
finally:
verbindung.close()
reaktionen = []
for ereignis_zeit, stufe, ausloeser, erfolgreich in ereignisse:
wann = zeit(ereignis_zeit)
if not (start <= wann <= ende):
continue
if ausloeser != KLICKER:
print(f" {wann:%H:%M:%S} {stufe} von {ausloeser}: kein Klick, ausgelassen")
continue
if not erfolgreich:
print(f" {wann:%H:%M:%S} {stufe}: nicht erfolgreich, ausgelassen")
continue
neu = NEUER_ZUSTAND.get(stufe)
treffer = next(
(t for t in takte if zeit(t["zeit"]) > wann and t.get("killswitch") == neu), None
)
if treffer is None:
print(f" {wann:%H:%M:%S} {stufe}: keine Taktzeile mit Zustand {neu} danach")
continue
sekunden = (zeit(treffer["zeit"]) - wann).total_seconds()
reaktionen.append(sekunden)
print(f" {wann:%H:%M:%S} {stufe:<13} -> Takt {treffer.get('schritt')} ({treffer.get('phase')}) nach {sekunden:5.1f} s")
kennzahlen("Klick -> Reaktion", reaktionen)
if modus == "paper":
print("Kerze -> Taktende (obere Schranke fuer Kerze -> Entscheidung):")
werte = []
for takt in takte:
if takt.get("frische_kerze") and takt.get("aktion") is not None and takt.get("letzte_kerze"):
schluss = zeit(takt["letzte_kerze"]) + timedelta(seconds=60)
werte.append((zeit(takt["zeit"]) - schluss).total_seconds())
kennzahlen("Kerze -> Taktende", werte)
ohne = sum(1 for takt in takte if takt.get("markt_offen") and not takt.get("frische_kerze"))
print(f" Takte bei offenem Markt ohne frische Kerze: {ohne} von {len(takte)}")
else:
print("Kerze -> Entscheidung: im Modus", modus, "nicht gerechnet (Kerzen eines anderen Tages).")
orders = [z for z in zeilen if z.get("art") == "order"]
meldefehler = [z for z in zeilen if z.get("art") == "ereignis" and z.get("was") == "benachrichtigung_fehlgeschlagen"]
print(f"Dazu: {len(orders)} Orderzeile(n), {len(meldefehler)} fehlgeschlagene Meldung(en).")
return 0
if __name__ == "__main__":
sys.exit(main())