"""Vorpruefung vor dem Start des Paper-Runners: liest nur, startet nichts, aendert nichts. Koordinator-Werkzeug zu Teil 10, nicht fuers Repo. Fragen und Herkunft der Erwartung: 1. Laeuft schon ein Runner? Projektvorgabe (Doppelstart endet mit Exit 2; hier vorher sichtbar). 2. Wie steht der Killswitch? Nur Auskunft: Der Runner-Start stellt nach der Neustartregel immer auf angehalten. Ein Befund ist allein 'gesperrt'. Gelesen aus data/registry.db, nur lesend. 3. Ist das Paper-Konto glatt? Projektvorgabe (der Tag beginnt ohne Position und ohne offene Order). 4. Was sagt die Marktuhr des Brokers? Quelle: GET /v2/clock (is_open, next_open, next_close). 5. Wie weit liegt die eigene Uhr daneben? Quelle: Date-Kopf der Broker-Antwort gegen die eigene Uhr; mehr als 30 Sekunden waeren fuer TOTP und Minutentakt ein Problem. Was es nicht sieht: ob das Dashboard laeuft (das fragt der Koordinator unter Windows ab), ob der IEX-Feed frisch liefert (dafuer handabruf_kerzen.py), und den Code-Stand (dafuer git im Projektordner). Eine fehlende Voraussetzung ist ein Befund (Exit 3), jede Abweichung ein Befund (Exit 2). Liest config/secrets.toml aus dem Projektordner und gibt die Schluessel nie aus. Aufruf aus dem Projektordner (WSL, Venv): python vorstart_pruefung.py """ import sqlite3 import subprocess import sys import tomllib from datetime import UTC, datetime from email.utils import parsedate_to_datetime from pathlib import Path import requests HANDEL_URL = "https://paper-api.alpaca.markets" def main() -> int: befunde: list[str] = [] laeufer = subprocess.run(["pgrep", "-af", "scripts/paper.py"], capture_output=True, text=True).stdout.strip() eigene = [z for z in laeufer.splitlines() if "pgrep" not in z] print(f"1. laufende runner: {len(eigene)}") for zeile in eigene: print(" ", zeile[:140]) if eigene: befunde.append("es laeuft schon ein Runner") db = Path("data/registry.db") if not db.is_file(): print("VORAUSSETZUNG FEHLT: data/registry.db (aus dem Projektordner starten)") return 3 verbindung = sqlite3.connect(f"file:{db.as_posix()}?mode=ro", uri=True) try: zeile = verbindung.execute("select zustand, seit, ausloeser from killswitch_zustand").fetchone() finally: verbindung.close() # Kein Befund, gleich wie er steht: Der Runner-Start stellt nach der Neustartregel immer auf # angehalten (Registerereignis 'neustart'). Ein 'aktiv' VOR dem Start heisst nur, dass ein # frueheres Fortsetzen ueberschrieben wird -- der Klick muss NACH dem Start kommen. print(f"2. killswitch im register: {zeile}") if zeile and zeile[0] == "aktiv": print(" HINWEIS: steht auf aktiv; der Runner-Start ueberschreibt das mit angehalten, Fortsetzen erst danach") if zeile and zeile[0] == "gesperrt": befunde.append("Killswitch ist gesperrt; Fortsetzen allein genuegt dann nicht (Freigeben im Dashboard)") secrets = Path("config/secrets.toml") if not secrets.is_file(): print("VORAUSSETZUNG FEHLT: config/secrets.toml") return 3 werte = tomllib.loads(secrets.read_text(encoding="utf-8"))["alpaca"] kopf = {"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]} positionen = requests.get(f"{HANDEL_URL}/v2/positions", headers=kopf, timeout=20) offene = requests.get(f"{HANDEL_URL}/v2/orders", headers=kopf, params={"status": "open", "limit": 100}, timeout=20) if positionen.status_code != 200 or offene.status_code != 200: print(f"VORAUSSETZUNG FEHLT: Broker antwortet {positionen.status_code}/{offene.status_code}") return 3 print(f"3. paper-konto: {len(positionen.json())} position(en), {len(offene.json())} offene order(s)") for p in positionen.json(): print(f" position {p.get('symbol')} qty {p.get('qty')} side {p.get('side')}") for o in offene.json(): print(f" order {o.get('side')} {o.get('qty')} {o.get('symbol')} status {o.get('status')}") if positionen.json() or offene.json(): befunde.append("das Paper-Konto ist nicht glatt") vorher = datetime.now(UTC) uhr = requests.get(f"{HANDEL_URL}/v2/clock", headers=kopf, timeout=20) nachher = datetime.now(UTC) if uhr.status_code != 200: print(f"VORAUSSETZUNG FEHLT: /v2/clock antwortet {uhr.status_code}") return 3 daten = uhr.json() print(f"4. marktuhr: is_open {daten.get('is_open')}, next_open {daten.get('next_open')}, next_close {daten.get('next_close')}") datum = uhr.headers.get("Date") if datum: server = parsedate_to_datetime(datum) mitte = vorher + (nachher - vorher) / 2 abstand = (mitte - server).total_seconds() # Der Date-Kopf hat Sekundenaufloesung; unter zwei Sekunden ist deshalb nichts zu sagen. print(f"5. eigene uhr gegen den Date-Kopf des Brokers: {abstand:+.1f} s (Aufloesung eine Sekunde)") if abs(abstand) > 30: befunde.append(f"die eigene Uhr liegt {abstand:+.0f} s neben der des Brokers") else: print("5. kein Date-Kopf in der Antwort; Uhrvergleich entfaellt") print() for befund in befunde: print("BEFUND:", befund) print(f"{len(befunde)} Befund(e).") return 2 if befunde else 0 if __name__ == "__main__": try: sys.exit(main()) except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Werkzeugs ist selbst ein Befund print(f"BEFUND: das Werkzeug ist abgebrochen: {ausnahme!r}") sys.exit(2)