From 371a1f5d2a65d589c02ae9ba88a73b6f7386a9a0 Mon Sep 17 00:00:00 2001 From: Pascal Michel Date: Fri, 18 Sep 2026 04:00:01 +0200 Subject: [PATCH] =?UTF-8?q?Automatischer=20Push:=208=20Datei(en)=20ge?= =?UTF-8?q?=C3=A4ndert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/angriffe.py - .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py - .vault-tools/trading-plattform-pruefung/handabruf_kerzen.py - .vault-tools/trading-plattform-pruefung/lies_paper_protokoll.py - .vault-tools/trading-plattform-pruefung/pruef_beobachtung_echt.py - .vault-tools/trading-plattform-pruefung/start_probe_backend.py - .vault-tools/trading-plattform-pruefung/start_probe_zugang.py Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 --- .../trading-plattform-pruefung/LIESMICH.md | 17 +- .../trading-plattform-pruefung/angriffe.py | 297 ++++++++++++++++++ .../browserlauf_zugang.py | 186 +++++++++++ .../handabruf_kerzen.py | 74 +++++ .../lies_paper_protokoll.py | 41 +++ .../pruef_beobachtung_echt.py | 167 ++++++++++ .../start_probe_backend.py | 2 +- .../start_probe_zugang.py | 109 +++++++ 8 files changed, 890 insertions(+), 3 deletions(-) create mode 100644 .vault-tools/trading-plattform-pruefung/angriffe.py create mode 100644 .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py create mode 100644 .vault-tools/trading-plattform-pruefung/handabruf_kerzen.py create mode 100644 .vault-tools/trading-plattform-pruefung/lies_paper_protokoll.py create mode 100644 .vault-tools/trading-plattform-pruefung/pruef_beobachtung_echt.py create mode 100644 .vault-tools/trading-plattform-pruefung/start_probe_zugang.py diff --git a/.vault-tools/trading-plattform-pruefung/LIESMICH.md b/.vault-tools/trading-plattform-pruefung/LIESMICH.md index 87cdb78..6fb1df8 100644 --- a/.vault-tools/trading-plattform-pruefung/LIESMICH.md +++ b/.vault-tools/trading-plattform-pruefung/LIESMICH.md @@ -2,10 +2,10 @@ type: resource status: active title: "Trading Plattform: Prüfwerkzeuge des Koordinators fürs Dashboard" -summary: "Eigener Lauf mit echten Daten zum Dashboard der Trading Plattform — Vergleich der Endpunkte gegen Vollparse und pandas, zwei WebSocket-Verbindungen, Browser-Lauf und Wiederaufbau mit Playwright, gefahrlose Proben der zwei Skripte, Mutantenläufe. Entstanden 2026-09-17 (Teil 3b), bewusst nicht im Projekt-Repo." +summary: "Eigener Lauf mit echten Daten zum Dashboard der Trading Plattform — Vergleich der Endpunkte gegen Vollparse und pandas, zwei WebSocket-Verbindungen, Browser-Lauf und Wiederaufbau mit Playwright, gefahrlose Proben der zwei Skripte, Mutantenläufe. Entstanden 2026-09-17 (Teil 3b), erweitert 2026-09-18 (Teil 10: Anmeldung, Angriffsfälle, Beobachtung, Kerzenlatenz), bewusst nicht im Projekt-Repo." tags: [vault-tools, trading-plattform] created: 2026-09-17 -updated: 2026-09-17 +updated: 2026-09-18 --- # Prüfwerkzeuge fürs Dashboard der Trading Plattform @@ -22,6 +22,19 @@ Koordinator-Werkzeuge, nicht fürs Repo. Sie starten **nie** den echten Broker: | `mutanten_py.py`, `mutanten_ts.py` | Mutanten zu Teil 3b auf einer Kopie mit angewendetem Plan, je mit Fehlerzeile | Pfad der Kopie als erstes Argument | | `real_pytest.sh` | volle Suite im echten Baum, mit Zählung der „Task was destroyed"-Meldungen | `wsl -d Debian -- bash -s -- -q < real_pytest.sh` | +## Teil 10 (2026-09-18): Paper-Runner, Anmeldung, Härtung + +| Datei | Was sie tut | Aufruf | +|---|---|---| +| `start_probe_zugang.py` | die echte App mit Attrappe, Wegwerf-Register und Wegwerf-Zugang (Benutzer `probe`, bekanntes Passwort, TOTP-Beispielgeheimnis der RFC-Literatur; beides in `zugang.json` der Ablage), auf Wunsch mit einer Paper-Statusdatei | aus dem Projektordner in WSL: `PYTHONPATH=. python start_probe_zugang.py [mit-paperstatus]` | +| `angriffe.py` | 56 Fälle gegen Host-Positivliste, Origin, Sitzungszwang, Cookie-Attribute, Bremse, CSP und die anderen Header, WebSocket ohne Sitzung; Regressionsnachweis gegen das Backend vor Teil B: 45 rot | `python angriffe.py ` | +| `browserlauf_zugang.py` | Anmeldeformular ohne Sitzung, falsche und richtige Anmeldung (frischer TOTP-Code), Name und „Abmelden" in der Kopfzeile, Strom, Karte „Paper-Betrieb", alle Ansichten unter der CSP, Abmelden und Neuladen, 375 px; protokolliert jede Antwort ab 400 mit Phase und Pfad (`antworten-ab-400.json`); `--sabotage` muss rot enden; Screenshots — **ansehen** (der Lauf fand so das `GET /api/paper` vor dem Tor und die unbeschrifteten Felder) | `python -X utf8 browserlauf_zugang.py http://localhost: /zugang.json [--sabotage]` | +| `pruef_beobachtung_echt.py` | `baue_beobachtung` gegen `NautilusEnv._beobachtung` auf den echten IEX-Kerzen eines Tages, beide Seiten unabhängig gerechnet; Sabotage rot | `PYTHONPATH=. python pruef_beobachtung_echt.py JJJJ-MM-TT` aus dem Projektordner | +| `lies_paper_protokoll.py` | fasst ein Paper- oder Probe-Protokoll zusammen (Takte je Phase, Killswitch je Takt, frische Kerzen, erste Entscheidung, Orders, Schlussphase, Ende) | `python lies_paper_protokoll.py ` | +| `handabruf_kerzen.py` | Alter der jüngsten AAPL-Minutenkerze per REST (`feed` iex oder sip) mit Urteil FRISCH/ALT — die Messung vor dem ersten Paper-Tag; nachts nur ein Funktionsnachweis. Gemessen 2026-09-18 nachts: iex antwortet in 0,5 s, sip mit 403 „subscription does not permit querying recent SIP data" | aus dem Projektordner: `TZ=Europe/Berlin python handabruf_kerzen.py [iex|sip]` | + +Die Compose-Probe (Bauen aus dem echten Baum mit verankerten Ausschlüssen, Start, Healthcheck, Angriffsfälle, Runner-Probe im Container, Aufräumen) lag als `compose_probe_echt.sh` im Scratchpad der Sitzung und braucht `sudo -S` mit dem WSL-Passwort aus `$WSL_PW`; sie ist deshalb nicht hier abgelegt. Muster: `tar --exclude=./data` — ohne Anker fällt auch `trading/data` weg. + Vor dem Browser-Lauf `npm run build` im Projekt, sonst dient das Backend eine alte Oberfläche aus. Die Datensatz- und Dateinamen in `browserlauf.py` und `pruef_teil3b.py` sind die des Bestands vom 2026-09-17; nach einem neuen Katalog oder neuen Protokollen nachziehen. Die Statustexte in `wiederaufbau.py` sind aus `Kopfzeile.tsx` abgeschrieben. Die generischen Planwerkzeuge liegen unter `.vault-tools/plan-kopie/`. diff --git a/.vault-tools/trading-plattform-pruefung/angriffe.py b/.vault-tools/trading-plattform-pruefung/angriffe.py new file mode 100644 index 0000000..8d13e09 --- /dev/null +++ b/.vault-tools/trading-plattform-pruefung/angriffe.py @@ -0,0 +1,297 @@ +"""Angriffsfaelle gegen ein laufendes Dashboard-Backend mit Anmeldung (Teil 10, Teil B). + +Koordinator-Pruefskript, nicht fuers Repo. Es beurteilt, ob ein LAUFENDES Backend die Zusagen der +Spec zu Teil 10 haelt; es ist ein Fangnetz, kein Nachweis. + +Geltungsbereich (Herkunft jeder Erwartung: Projektvorgabe, Spec Teil 10, Abschnitt "Teil B"): +- freie Pfade (/api/gesund, GET /api/sitzung), geschuetzte Pfade ohne Cookie 401, fremder Host 400, + schreibende Anfrage ohne oder mit fremdem Origin 403 (auch mit gueltigem Cookie), /docs /redoc + /openapi.json 404, Sicherheitskopfzeilen, einheitliche Ablehnung der Anmeldung, Cookie-Merkmale, + Wiederholung eines TOTP-Codes, Abmeldung entwertet das Cookie, WebSocket ohne Cookie und mit + fremdem Origin abgelehnt, Bremse (429 mit Retry-After, auch fuer die richtige Anmeldung), + Anmeldeprotokoll ohne Passwort und Code, /api/paper mit Protokollpfad ausserhalb des Ordners. +Was es nicht sieht: TLS und das Secure-Merkmal (das Probe-Backend spricht HTTP), DNS-Rebinding im +echten Browser, Zeitverhalten der Passwortpruefung, alles hinter der Anmeldung ausser einem GET und +einem POST je Fall, und ob die CSP die gebaute Oberflaeche wirklich traegt (dafuer der Browser-Lauf). +Regressionsnachweis: gegen das alte Probe-Backend ohne Anmeldung (start_probe_backend.py) muss der +Lauf mit Exit 2 enden und die Schutzfaelle einzeln rot zeigen. + +Aufruf (in WSL, Venv des Projekts): + python angriffe.py [--ohne-bremse] +Exit 0 ohne Befund, 2 mit Befund, 3 wenn eine Voraussetzung fehlt (auch das ist ein Befund). +""" + +import base64 +import hashlib +import hmac +import json +import struct +import sys +import time +from pathlib import Path + +import requests +from websockets.exceptions import InvalidStatus +from websockets.sync.client import connect + +befunde: list[str] = [] +bestanden: list[str] = [] + + +def pruefe(name: str, bedingung: bool, einzelheit: str = "") -> None: + if bedingung: + bestanden.append(name) + print(f" ok {name}") + else: + befunde.append(f"{name}: {einzelheit}") + print(f" ROT {name}: {einzelheit}") + + +def totp(geheimnis: str, zeit_s: float) -> str: + """Eigener Nachbau nach RFC 6238 / RFC 4226, unabhaengig vom Projektcode.""" + schluessel = base64.b32decode(geheimnis.upper() + "=" * (-len(geheimnis) % 8)) + zaehler = int(zeit_s // 30) + mac = hmac.new(schluessel, struct.pack(">Q", zaehler), hashlib.sha1).digest() + versatz = mac[-1] & 0x0F + zahl = struct.unpack(">I", mac[versatz : versatz + 4])[0] & 0x7FFFFFFF + return f"{zahl % 1_000_000:06d}" + + +def main() -> int: + if len(sys.argv) < 3: + print(__doc__) + return 3 + basis = sys.argv[1].rstrip("/") + ablage = Path(sys.argv[2]) + mit_bremse = "--ohne-bremse" not in sys.argv[3:] + host = basis.split("://", 1)[1] + origin = basis + + zugang_json = ablage / "zugang.json" + if not zugang_json.is_file(): + print(f"VORAUSSETZUNG FEHLT: {zugang_json} (Ablage des Probe-Backends)") + return 3 + zugang = json.loads(zugang_json.read_text(encoding="utf-8")) + benutzer, passwort, geheimnis = zugang["benutzer"], zugang["passwort"], zugang["geheimnis"] + + # RFC-6238-Vektor gegen den eigenen Nachbau, bevor irgendetwas damit gemessen wird. + if totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 59) != "287082": + print("VORAUSSETZUNG FEHLT: der eigene TOTP-Nachbau trifft den RFC-Vektor nicht") + return 3 + + try: + grund = requests.get(f"{basis}/api/gesund", timeout=5) + except requests.RequestException as ausnahme: + print(f"VORAUSSETZUNG FEHLT: Backend unter {basis} nicht erreichbar: {ausnahme!r}") + return 3 + + print("== Grundlinie") + pruefe("gesund ohne Cookie 200", grund.status_code == 200, f"HTTP {grund.status_code}") + sitzung = requests.get(f"{basis}/api/sitzung", timeout=5) + try: + sitzung_json = sitzung.json() + except ValueError: + sitzung_json = {} + pruefe( + "sitzung ohne Cookie: pflicht, eingerichtet, nicht angemeldet", + sitzung.status_code == 200 + and sitzung_json.get("pflicht") is True + and sitzung_json.get("eingerichtet") is True + and sitzung_json.get("angemeldet") is False, + f"HTTP {sitzung.status_code} {sitzung.text[:120]}", + ) + + print("== Geschuetzte Pfade ohne Cookie") + for pfad in ( + "/api/uebersicht", + "/api/agenten", + "/api/bestand", + "/api/laeufe", + "/api/markt/datensaetze", + "/api/paper", + ): + antwort = requests.get(f"{basis}{pfad}", timeout=10) + pruefe(f"GET {pfad} ohne Cookie 401", antwort.status_code == 401, f"HTTP {antwort.status_code}") + + print("== Fremder Host") + for pfad in ("/", "/api/gesund", "/api/uebersicht"): + antwort = requests.get(f"{basis}{pfad}", headers={"Host": "boese.example"}, timeout=5) + pruefe(f"GET {pfad} mit fremdem Host 400", antwort.status_code == 400, f"HTTP {antwort.status_code}") + + print("== Schnittstellenbeschreibung") + for pfad in ("/docs", "/redoc", "/openapi.json"): + antwort = requests.get(f"{basis}{pfad}", timeout=5) + pruefe(f"GET {pfad} 404", antwort.status_code == 404, f"HTTP {antwort.status_code}") + + print("== Kopfzeilen") + for pfad in ("/", "/api/gesund"): + kopf = requests.get(f"{basis}{pfad}", timeout=5).headers + csp = kopf.get("content-security-policy", "") + pruefe(f"{pfad}: CSP mit script-src 'self'", "script-src 'self'" in csp, csp[:80]) + pruefe(f"{pfad}: CSP mit frame-ancestors 'none'", "frame-ancestors 'none'" in csp, csp[:80]) + pruefe(f"{pfad}: X-Frame-Options DENY", kopf.get("x-frame-options") == "DENY", str(kopf.get("x-frame-options"))) + pruefe(f"{pfad}: nosniff", kopf.get("x-content-type-options") == "nosniff", str(kopf.get("x-content-type-options"))) + pruefe(f"{pfad}: Referrer-Policy", kopf.get("referrer-policy") == "no-referrer", str(kopf.get("referrer-policy"))) + pruefe(f"{pfad}: kein Server-Kopf", "server" not in kopf, str(kopf.get("server"))) + api_kopf = requests.get(f"{basis}/api/gesund", timeout=5).headers + pruefe("/api/gesund: Cache-Control no-store", api_kopf.get("cache-control") == "no-store", str(api_kopf.get("cache-control"))) + + print("== Anmeldung: Origin und einheitliche Ablehnung") + rumpf = {"benutzer": benutzer, "passwort": passwort, "code": totp(geheimnis, time.time())} + ohne_origin = requests.post(f"{basis}/api/anmeldung", json=rumpf, timeout=10) + pruefe("Anmeldung ohne Origin 403", ohne_origin.status_code == 403, f"HTTP {ohne_origin.status_code}") + fremd = requests.post( + f"{basis}/api/anmeldung", json=rumpf, headers={"Origin": "http://boese.example"}, timeout=10 + ) + pruefe("Anmeldung mit fremdem Origin 403", fremd.status_code == 403, f"HTTP {fremd.status_code}") + + kopf = {"Origin": origin} + texte = set() + for name, daten in ( + ("falsches Passwort", {**rumpf, "passwort": "ganz-falsches-passwort"}), + ("falscher Code", {**rumpf, "code": "000000" if rumpf["code"] != "000000" else "111111"}), + ("unbekannter Benutzer", {**rumpf, "benutzer": "niemand"}), + ): + antwort = requests.post(f"{basis}/api/anmeldung", json=daten, headers=kopf, timeout=10) + pruefe(f"Anmeldung, {name}: 401", antwort.status_code == 401, f"HTTP {antwort.status_code}") + texte.add(antwort.text) + pruefe("alle drei Ablehnungen lauten gleich", len(texte) == 1, str(texte)[:200]) + + print("== Anmeldung: Erfolg, Cookie, Wiederholung") + jetzt = time.time() + code = totp(geheimnis, jetzt) + sitz = requests.Session() + gut = sitz.post(f"{basis}/api/anmeldung", json={**rumpf, "code": code}, headers=kopf, timeout=10) + pruefe("richtige Anmeldung 200", gut.status_code == 200, f"HTTP {gut.status_code} {gut.text[:120]}") + setzen = gut.headers.get("set-cookie", "") + klein = setzen.lower() + pruefe("Cookie tp_sitzung gesetzt", setzen.startswith("tp_sitzung="), setzen[:40]) + pruefe("Cookie HttpOnly", "httponly" in klein, setzen[-80:]) + pruefe("Cookie SameSite=Strict", "samesite=strict" in klein, setzen[-80:]) + pruefe("Cookie Path=/", "path=/" in klein, setzen[-80:]) + pruefe("Cookie ohne Secure (HTTP)", "secure" not in klein, setzen[-80:]) + + wieder = requests.post(f"{basis}/api/anmeldung", json={**rumpf, "code": code}, headers=kopf, timeout=10) + pruefe("derselbe Code ein zweites Mal 401", wieder.status_code == 401, f"HTTP {wieder.status_code}") + + print("== Mit Cookie") + ueber = sitz.get(f"{basis}/api/uebersicht", timeout=15) + pruefe("GET /api/uebersicht mit Cookie 200", ueber.status_code == 200, f"HTTP {ueber.status_code}") + halt = sitz.post(f"{basis}/api/killswitch/anhalten", json={"grund": "angriffe.py"}, timeout=15) + pruefe("POST mit Cookie, ohne Origin 403", halt.status_code == 403, f"HTTP {halt.status_code}") + halt = sitz.post( + f"{basis}/api/killswitch/anhalten", + json={"grund": "angriffe.py"}, + headers={"Origin": "http://boese.example"}, + timeout=15, + ) + pruefe("POST mit Cookie, fremder Origin 403", halt.status_code == 403, f"HTTP {halt.status_code}") + halt = sitz.post( + f"{basis}/api/killswitch/anhalten", json={"grund": "angriffe.py"}, headers=kopf, timeout=15 + ) + pruefe( + "POST mit Cookie und Origin kommt an (200 oder 409)", + halt.status_code in (200, 409), + f"HTTP {halt.status_code} {halt.text[:120]}", + ) + + print("== /api/paper") + paper = sitz.get(f"{basis}/api/paper", timeout=10) + pruefe("GET /api/paper mit Cookie 200", paper.status_code == 200, f"HTTP {paper.status_code}") + status_datei = ablage / "status.json" + if status_datei.is_file() and paper.status_code == 200: + daten = paper.json() + pruefe("paper: Status und Zeilen geliefert", bool(daten.get("status")) and len(daten.get("zeilen", [])) > 0, str(daten)[:160]) + original = status_datei.read_text(encoding="utf-8") + verbogen = json.loads(original) + verbogen["protokoll"] = str(ablage / "registry.db") + status_datei.write_text(json.dumps(verbogen), encoding="utf-8") + try: + aussen = sitz.get(f"{basis}/api/paper", timeout=10).json() + pruefe( + "paper: Protokollpfad ausserhalb des Ordners liefert keine Zeilen, aber einen Hinweis", + aussen.get("zeilen") == [] and bool(aussen.get("hinweis")), + str(aussen)[:200], + ) + finally: + status_datei.write_text(original, encoding="utf-8") + else: + print(" (kein Paper-Status in der Ablage; Fall uebersprungen — Backend mit 'mit-paperstatus' starten)") + + print("== WebSocket") + ws_url = basis.replace("http", "ws", 1) + "/ws/uebersicht" + cookie = "; ".join(f"{c.name}={c.value}" for c in sitz.cookies) + + def ws_versuch(kopfzeilen: dict[str, str], mit_origin: str | None) -> str: + try: + with connect(ws_url, additional_headers=kopfzeilen, origin=mit_origin, open_timeout=5) as ws: + return "offen:" + str(ws.recv(timeout=10))[:40] + except InvalidStatus as ausnahme: + return f"abgelehnt:{ausnahme.response.status_code}" + except Exception as ausnahme: # noqa: BLE001 - jede andere Ablehnung ist auch eine + return f"abgelehnt:{ausnahme!r}"[:80] + + pruefe("WebSocket ohne Cookie abgelehnt", ws_versuch({}, origin).startswith("abgelehnt"), ws_versuch({}, origin)) + ergebnis = ws_versuch({"Cookie": cookie}, "http://boese.example") + pruefe("WebSocket mit Cookie, fremder Origin abgelehnt", ergebnis.startswith("abgelehnt"), ergebnis) + ergebnis = ws_versuch({"Cookie": cookie}, origin) + pruefe("WebSocket mit Cookie und Origin liefert einen Snapshot", ergebnis.startswith("offen:"), ergebnis) + + print("== Abmeldung") + ab = sitz.post(f"{basis}/api/abmeldung", headers=kopf, timeout=10) + pruefe("Abmeldung 204", ab.status_code == 204, f"HTTP {ab.status_code}") + danach = requests.get(f"{basis}/api/uebersicht", headers={"Cookie": cookie}, timeout=10) + pruefe("altes Cookie nach der Abmeldung 401", danach.status_code == 401, f"HTTP {danach.status_code}") + + if mit_bremse: + print("== Bremse (sperrt diese Adresse; deshalb zuletzt)") + status = [] + for _ in range(12): + antwort = requests.post( + f"{basis}/api/anmeldung", + json={**rumpf, "passwort": "ganz-falsches-passwort"}, + headers=kopf, + timeout=10, + ) + status.append(antwort.status_code) + if antwort.status_code == 429: + pruefe("429 traegt Retry-After", antwort.headers.get("retry-after", "").isdigit(), str(antwort.headers.get("retry-after"))) + break + pruefe("Bremse greift innerhalb von zwoelf Fehlversuchen", 429 in status, str(status)) + # Ein frischer Zeitschritt, damit die Ablehnung nicht an der Wiederholung des Codes liegt. + warten = 30 - (time.time() % 30) + 1 + time.sleep(warten) + richtig = requests.post( + f"{basis}/api/anmeldung", + json={**rumpf, "code": totp(geheimnis, time.time())}, + headers=kopf, + timeout=10, + ) + pruefe("gesperrt gilt auch fuer die richtige Anmeldung (429)", richtig.status_code == 429, f"HTTP {richtig.status_code}") + + print("== Anmeldeprotokoll") + protokoll = ablage / "anmeldungen.jsonl" + if not protokoll.is_file(): + pruefe("Anmeldeprotokoll vorhanden", False, str(protokoll)) + else: + text = protokoll.read_text(encoding="utf-8") + pruefe("Protokoll traegt das Passwort nicht", passwort not in text and "ganz-falsches-passwort" not in text, "Passwort im Protokoll") + pruefe("Protokoll traegt den Code nicht", code not in text, "Code im Protokoll") + ergebnisse = {json.loads(z).get("ergebnis") for z in text.splitlines() if z.strip()} + pruefe("Protokoll kennt ok und falsch", {"ok", "falsch"} <= ergebnisse, str(ergebnisse)) + + print() + print(f"{len(bestanden)} Faelle ok, {len(befunde)} Befund(e).") + for befund in befunde: + print(f"BEFUND: {befund}") + return 2 if befunde else 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Pruefskripts ist selbst ein Befund + for befund in befunde: + print(f"BEFUND: {befund}") + print(f"BEFUND: das Pruefskript ist abgebrochen: {ausnahme!r}") + sys.exit(2) diff --git a/.vault-tools/trading-plattform-pruefung/browserlauf_zugang.py b/.vault-tools/trading-plattform-pruefung/browserlauf_zugang.py new file mode 100644 index 0000000..aa97cb7 --- /dev/null +++ b/.vault-tools/trading-plattform-pruefung/browserlauf_zugang.py @@ -0,0 +1,186 @@ +"""Browser-Lauf zur Anmeldung (Teil 10) mit Playwright (Windows-Python), gegen das Probe-Backend mit Zugang. + +Koordinator-Pruefskript, nicht fuers Repo; ein Fangnetz, kein Nachweis. + +Fragen (Herkunft: Projektvorgabe, Spec Teil 10, Teil B): +- Zeigt die Oberflaeche ohne Sitzung das Anmeldeformular und NICHT die App? +- Fuehrt eine falsche Anmeldung zu einer Fehlerzeile (api.ts: "Benutzer, Passwort oder Code stimmt nicht.") und nicht in die App? +- Fuehrt die richtige Anmeldung (Passwort plus frischer TOTP-Code) in die App, mit Benutzername und + Knopf "Abmelden" in der Kopfzeile, laufendem Strom ("Backend verbunden") und der Karte "Paper-Betrieb"? +- Traegt die CSP die gebaute Oberflaeche: keine Konsolenmeldung, die "Content Security Policy" nennt, + in keiner Ansicht (auch "Markt" mit dem Chart)? +- Fuehrt "Abmelden" zurueck zum Formular, und bleibt es nach dem Neuladen dort? +Was es nicht sieht: TLS und das Secure-Merkmal, Safari und Firefox (nur Chromium), ob es gut aussieht +(dafuer die Screenshots, die der Koordinator selbst ansieht). +Regressionsnachweis: --sabotage erwartet nach der FALSCHEN Anmeldung die App und muss rot enden. + +Aufruf: python -X utf8 browserlauf_zugang.py [--sabotage] +Exit 0 ohne Befund, 2 mit Befund, 3 wenn eine Voraussetzung fehlt. +""" + +import base64 +import hashlib +import hmac +import json +import struct +import sys +import time +from pathlib import Path + +from playwright.sync_api import TimeoutError as PlaywrightTimeout +from playwright.sync_api import sync_playwright + + +def totp(geheimnis: str, zeit_s: float) -> str: + schluessel = base64.b32decode(geheimnis.upper() + "=" * (-len(geheimnis) % 8)) + mac = hmac.new(schluessel, struct.pack(">Q", int(zeit_s // 30)), hashlib.sha1).digest() + versatz = mac[-1] & 0x0F + zahl = struct.unpack(">I", mac[versatz : versatz + 4])[0] & 0x7FFFFFFF + return f"{zahl % 1_000_000:06d}" + + +def main() -> int: + if len(sys.argv) < 4: + print(__doc__) + return 3 + basis = sys.argv[1].rstrip("/") + zugang_datei = Path(sys.argv[2]) + ziel = Path(sys.argv[3]) + sabotage = "--sabotage" in sys.argv[4:] + if not zugang_datei.is_file(): + print(f"VORAUSSETZUNG FEHLT: {zugang_datei}") + return 3 + zugang = json.loads(zugang_datei.read_text(encoding="utf-8")) + ziel.mkdir(parents=True, exist_ok=True) + + befunde: list[str] = [] + konsole: list[str] = [] + antworten: list[str] = [] # jede Antwort ab 400 mit Phase, Status und Pfad + phase = ["start"] + + def pruefe(name: str, bedingung: bool, einzelheit: str = "") -> None: + print((" ok " if bedingung else " ROT ") + name + ("" if bedingung else f": {einzelheit}")) + if not bedingung: + befunde.append(f"{name}: {einzelheit}") + + with sync_playwright() as spiel: + browser = spiel.chromium.launch() + seite = browser.new_page(viewport={"width": 1440, "height": 900}) + seite.on("console", lambda m: konsole.append(f"{m.type}: {m.text}") if m.type in ("error", "warning") else None) + seite.on("pageerror", lambda f: konsole.append(f"pageerror: {f}")) + seite.on( + "response", + lambda a: antworten.append(f"{phase[0]} {a.status} {a.request.method} {a.url.replace(basis, '')}") + if a.status >= 400 + else None, + ) + + def formular_da() -> bool: + try: + seite.get_by_label("Passwort").wait_for(timeout=8000) + return True + except PlaywrightTimeout: + return False + + def app_da() -> bool: + try: + seite.get_by_role("button", name="Abmelden").wait_for(timeout=8000) + return True + except PlaywrightTimeout: + return False + + print("== ohne Sitzung") + phase[0] = "ohne Sitzung" + seite.goto(basis + "/") + pruefe("Anmeldeformular sichtbar", formular_da(), "kein Feld 'Passwort'") + pruefe("App nicht sichtbar", seite.get_by_role("button", name="Abmelden").count() == 0, "Knopf 'Abmelden' ist da") + seite.screenshot(path=str(ziel / "01-anmeldung.png")) + + print("== falsche Anmeldung") + phase[0] = "falsche Anmeldung" + seite.get_by_label("Benutzer").fill(zugang["benutzer"]) + seite.get_by_label("Passwort").fill("ganz-falsches-passwort") + seite.get_by_label("Code").fill("000000") + seite.get_by_role("button", name="Anmelden").click() + seite.wait_for_timeout(1500) + if sabotage: + pruefe("SABOTAGE: nach der falschen Anmeldung ist die App sichtbar", app_da(), "App nicht sichtbar (so soll es sein)") + else: + pruefe("weiter das Formular", seite.get_by_label("Passwort").count() == 1, "Formular weg") + pruefe("Fehlerzeile 'stimmt nicht'", seite.get_by_text("stimmt nicht").count() >= 1, "kein Fehlertext") + seite.screenshot(path=str(ziel / "02-abgelehnt.png")) + + print("== richtige Anmeldung") + phase[0] = "richtige Anmeldung" + # Ein frischer Zeitschritt, falls der Code dieses Schritts gerade verbraucht wurde. + rest = 30 - (time.time() % 30) + if rest < 5: + time.sleep(rest + 1) + seite.get_by_label("Passwort").fill(zugang["passwort"]) + seite.get_by_label("Code").fill(totp(zugang["geheimnis"], time.time())) + seite.get_by_role("button", name="Anmelden").click() + pruefe("App sichtbar (Knopf 'Abmelden')", app_da(), "kein Knopf 'Abmelden'") + pruefe("Benutzername in der Kopfzeile", seite.get_by_text(zugang["benutzer"], exact=True).count() >= 1, "Name fehlt") + try: + seite.get_by_text("Backend verbunden").wait_for(timeout=15000) + verbunden = True + except PlaywrightTimeout: + verbunden = False + pruefe("Strom laeuft ('Backend verbunden')", verbunden, "Statuspunkt zeigt es nicht") + pruefe("Karte 'Paper-Betrieb' da", seite.get_by_text("Paper-Betrieb").count() >= 1, "Karte fehlt") + seite.wait_for_timeout(1500) + seite.screenshot(path=str(ziel / "03-uebersicht.png"), full_page=True) + + print("== Ansichten unter der CSP") + phase[0] = "Ansichten unter der CSP" + for name in ("Agenten", "Laeufe", "Markt", "Bestand"): + knopf = seite.get_by_role("button", name=name.replace("ae", "ä") if name == "Laeufe" else name) + if knopf.count() == 0: + knopf = seite.get_by_role("link", name=name.replace("ae", "ä") if name == "Laeufe" else name) + if knopf.count() == 0: + pruefe(f"Ansicht {name} erreichbar", False, "kein Knopf und kein Link mit diesem Namen") + continue + knopf.first.click() + seite.wait_for_timeout(2500) + seite.screenshot(path=str(ziel / f"04-{name.lower()}.png")) + csp_meldungen = [m for m in konsole if "Content Security Policy" in m or "content security policy" in m.lower()] + pruefe("keine CSP-Meldung in der Konsole", not csp_meldungen, " | ".join(csp_meldungen[:3])) + # Chromium schreibt jede Antwort ab 400 als Konsolenfehler. Die 401 der absichtlich falschen + # Anmeldung ist erwartet; jede andere Antwort ab 400 wird mit Phase und Pfad zum Befund. + unerwartet = [a for a in antworten if not (" 401 POST /api/anmeldung" in a and a.startswith("falsche Anmeldung"))] + pruefe("keine unerwartete Antwort ab 400", not unerwartet, " | ".join(unerwartet[:5])) + andere = [m for m in konsole if m not in csp_meldungen and "Failed to load resource" not in m] + pruefe("keine andere Fehlermeldung in der Konsole", not andere, " | ".join(andere[:3])) + + print("== Abmelden") + phase[0] = "Abmelden" + seite.get_by_role("button", name="Abmelden").click() + pruefe("zurueck zum Formular", formular_da(), "Formular erscheint nicht") + seite.reload() + pruefe("nach dem Neuladen weiter das Formular", formular_da(), "Formular erscheint nicht") + seite.screenshot(path=str(ziel / "05-abgemeldet.png")) + + print("== schmale Breite") + phase[0] = "schmale Breite" + seite.set_viewport_size({"width": 375, "height": 800}) + seite.reload() + pruefe("Formular bei 375 px", formular_da(), "Formular erscheint nicht") + masse = seite.evaluate("() => ({scroll: document.documentElement.scrollWidth, sicht: document.documentElement.clientWidth})") + pruefe("kein waagerechter Scroll bei 375 px", masse["scroll"] <= masse["sicht"], str(masse)) + seite.screenshot(path=str(ziel / "06-anmeldung-375.png")) + browser.close() + + (ziel / "konsole.json").write_text(json.dumps(konsole, ensure_ascii=False, indent=1), encoding="utf-8") + (ziel / "antworten-ab-400.json").write_text(json.dumps(antworten, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"\n{len(befunde)} Befund(e); Screenshots unter {ziel}") + for befund in befunde: + print("BEFUND:", befund) + return 2 if befunde else 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Pruefskripts ist selbst ein Befund + print(f"BEFUND: das Pruefskript ist abgebrochen: {ausnahme!r}") + sys.exit(2) diff --git a/.vault-tools/trading-plattform-pruefung/handabruf_kerzen.py b/.vault-tools/trading-plattform-pruefung/handabruf_kerzen.py new file mode 100644 index 0000000..878f31e --- /dev/null +++ b/.vault-tools/trading-plattform-pruefung/handabruf_kerzen.py @@ -0,0 +1,74 @@ +"""Handabruf: Wie alt ist die juengste Minutenkerze, die Alpaca per REST fuer AAPL liefert? + +Koordinator-Werkzeug zu Teil 10, nicht fuers Repo. Beantwortet vor dem ersten Paper-Lauf die +ungemessene Frage, ob der IEX-Feed eine gerade abgeschlossene Minute innerhalb von Sekunden +liefert (der Runner wartet `kerze_verzoegerung_s` und versucht es `kerze_versuche` mal) oder +ob die Kerzen verzoegert kommen. Sinnvoll, solange gehandelt wird -- Vorboerse ab 10:00 Uhr +Berliner Zeit (04:00 ET), regulaer ab 15:30 Uhr. Nachts zeigt es nur die letzte Kerze des +Vortags; dann belegt es nur, dass Schluessel und Abruf funktionieren. + +Liest config/secrets.toml aus dem Projektordner, gibt die Schluessel nie aus. + +Aufruf aus dem Projektordner (WSL, Venv): + ~/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/handabruf_kerzen.py [feed] +feed: iex (Standard) oder sip. +""" + +import sys +import tomllib +from datetime import UTC, datetime, timedelta +from pathlib import Path + +import requests + +DATEN_URL = "https://data.alpaca.markets" +FRISCH_S = 120 # eine abgeschlossene Minute plus die Wartezeit des Runners + + +def main() -> int: + feed = sys.argv[1] if len(sys.argv) > 1 else "iex" + secrets = Path("config/secrets.toml") + if not secrets.exists(): + print(f"VORAUSSETZUNG FEHLT: {secrets} (aus dem Projektordner starten)") + 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"]} + + jetzt = datetime.now(UTC).replace(microsecond=0) + parameter = { + "symbols": "AAPL", + "timeframe": "1Min", + "start": (jetzt - timedelta(hours=36)).strftime("%Y-%m-%dT%H:%M:%SZ"), + "end": jetzt.strftime("%Y-%m-%dT%H:%M:%SZ"), + "feed": feed, + "adjustment": "raw", + "limit": 10_000, + "sort": "desc", + } + gesendet = datetime.now(UTC) + antwort = requests.get(f"{DATEN_URL}/v2/stocks/bars", headers=kopf, params=parameter, timeout=30) + dauer_s = (datetime.now(UTC) - gesendet).total_seconds() + print(f"Feed {feed}, Anfrage bis {jetzt.isoformat()} (jetzt), Antwort {antwort.status_code} in {dauer_s:.2f} s") + if antwort.status_code >= 400: + print("Antworttext:", antwort.text[:300]) + return 2 + kerzen = (antwort.json().get("bars") or {}).get("AAPL") or [] + if not kerzen: + print("keine Kerze in den letzten 36 Stunden -- das ist bei einem laufenden Handelstag ein Befund") + return 2 + juengste = kerzen[0] + zeit = datetime.fromisoformat(juengste["t"].replace("Z", "+00:00")) + alter_s = (jetzt - zeit).total_seconds() + berlin = zeit.astimezone().strftime("%H:%M:%S %Z") + print(f"{len(kerzen)} Kerzen; juengste Kerze beginnt {zeit.isoformat()} ({berlin}), " + f"Alter {alter_s:.0f} s, Schluss {juengste['c']}, Volumen {juengste['v']}") + if alter_s <= FRISCH_S: + print(f"FRISCH: innerhalb von {FRISCH_S} s -- der Runner bekommt seine Kerze rechtzeitig") + else: + print(f"ALT: {alter_s / 60:.1f} min -- entweder wird gerade nicht gehandelt, oder der Feed kommt verzoegert; " + "waehrend der Handelszeit ist das ein Befund") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.vault-tools/trading-plattform-pruefung/lies_paper_protokoll.py b/.vault-tools/trading-plattform-pruefung/lies_paper_protokoll.py new file mode 100644 index 0000000..8e9db6d --- /dev/null +++ b/.vault-tools/trading-plattform-pruefung/lies_paper_protokoll.py @@ -0,0 +1,41 @@ +"""Liest ein Paper- oder Probe-Protokoll mit eigenem Code und fasst zusammen, was darin steht. + +Koordinator-Werkzeug zu Teil 10, nicht fuers Repo. Es tut etwas und beurteilt wenig: Die Zahlen liest +der Koordinator selbst. Binaer gelesen, nur an b"\\n" getrennt. + +Aufruf: python lies_paper_protokoll.py +""" + +import json +import sys +from collections import Counter +from pathlib import Path + +zeilen = [json.loads(z) for z in Path(sys.argv[1]).read_bytes().split(b"\n") if z.strip()] +arten = Counter(z.get("art") for z in zeilen) +print("zeilen:", len(zeilen), dict(arten)) + +kopf = next(z for z in zeilen if z.get("art") == "kopf") +print("kopf:", {k: kopf.get(k) for k in ("modus", "agent", "instrument", "fenster", "basis_menge", "feed", "glatt_vor_schluss_min")}) + +takte = [z for z in zeilen if z.get("art") == "takt"] +print("takte:", len(takte), "phasen:", dict(Counter(t.get("phase") for t in takte))) +print("killswitch in takten:", dict(Counter(t.get("killswitch") for t in takte))) +print("frische kerze:", dict(Counter(t.get("frische_kerze") for t in takte))) +erste_entscheidung = next((t for t in takte if t.get("aktion") is not None), None) +if erste_entscheidung: + print("erste entscheidung: schritt", erste_entscheidung["schritt"], "kerzen", erste_entscheidung["kerzen"], "letzte_kerze", erste_entscheidung["letzte_kerze"], "aktion", erste_entscheidung["aktion"], "ziel", erste_entscheidung["zielposition"]) +print("aktionen:", dict(Counter(t.get("aktion") for t in takte))) +print("positionen:", dict(Counter(t.get("position") for t in takte))) +schliesst = [t for t in takte if t.get("phase") == "schliesst"] +if schliesst: + print("schlussphase: von", schliesst[0]["letzte_kerze"], "bis", schliesst[-1]["letzte_kerze"], "positionen", [t["position"] for t in schliesst]) + +for order in (z for z in zeilen if z.get("art") == "order"): + print("order:", {k: order.get(k) for k in ("seite", "menge", "gefuellt", "status", "position_danach")}) +for ereignis in (z for z in zeilen if z.get("art") == "ereignis"): + print("ereignis:", ereignis.get("was"), "|", str(ereignis.get("text"))[:140]) +ende = [z for z in zeilen if z.get("art") == "ende"] +print("ende:", ende) +hinweise = Counter(t.get("hinweis") for t in takte if t.get("hinweis")) +print("hinweise in takten:", dict(hinweise)) diff --git a/.vault-tools/trading-plattform-pruefung/pruef_beobachtung_echt.py b/.vault-tools/trading-plattform-pruefung/pruef_beobachtung_echt.py new file mode 100644 index 0000000..dabffea --- /dev/null +++ b/.vault-tools/trading-plattform-pruefung/pruef_beobachtung_echt.py @@ -0,0 +1,167 @@ +"""Beobachtung des Paper-Runners gegen die der NautilusEnv — auf ECHTEN IEX-Kerzen eines Tages. + +Koordinator-Pruefskript zu Teil 10, nicht fuers Repo; ein Fangnetz, kein Nachweis. + +Frage (Herkunft: Projektvorgabe, Spec Teil 10, "Abweichungen vom Training"): Liefert der Weg des +Runners (REST-Antwort -> AlpacaKerzenquelle -> Kerze -> baue_beobachtung) Wert fuer Wert dieselbe +Beobachtung wie der Weg des Trainings (dieselbe REST-Antwort -> alpaca_json_zu_bars -> Bar -> +NautilusEnv._beobachtung)? Der Test im Repo baut seine Kerzen aus den Bars selbst und sieht deshalb +keine Abweichung zwischen den zwei Umwandlungen; dieser Lauf nimmt zwei getrennte Wege von derselben +Antwort. +Was es nicht sieht: Beobachtungen mit offener Position (Position und Kontowert kommen im Betrieb vom +Broker), SIP gegen IEX (gemessen in der Vorabmessung), andere Instrumente und Tage. +Regressionsnachweis: --sabotage verschiebt einen Schlusskurs um einen Cent und muss mit Exit 2 enden. + +Aufruf in WSL aus dem Projektordner (dort liegt config/secrets.toml; der Schluessel wird nie ausgegeben): + python pruef_beobachtung_echt.py 2026-09-17 [--sabotage] +""" + +import dataclasses +import sys +import tomllib +from datetime import datetime +from pathlib import Path + +import numpy as np +import requests + +from nautilus_trader.model.data import BarType +from nautilus_trader.test_kit.providers import TestInstrumentProvider + +from trading.data.loaders import alpaca_json_zu_bars +from trading.data.loaders import nur_regulaere_handelszeit +from trading.paper.beobachtung import baue_beobachtung +from trading.paper.kerzen import HANDELSZONE +from trading.paper.kerzen import AlpacaKerzenquelle +from trading.paper.kerzen import handelsbeginn +from trading.paper.kerzen import handelsende +from trading.rl.env import EnvConfig +from trading.rl.env import NautilusEnv +from trading.sim.venues import lade_venue_config + +FENSTER = 20 +EPISODE = 120 +PERIODEN = (5, 20) +BASIS = 100 +MAX_STUFE = 2 +BAR_TYPE = "AAPL.XNAS-1-MINUTE-LAST-EXTERNAL" + + +def main() -> int: + if len(sys.argv) < 2: + print(__doc__) + return 3 + tag = datetime.strptime(sys.argv[1], "%Y-%m-%d").replace(tzinfo=HANDELSZONE) + sabotage = "--sabotage" in sys.argv[2:] + ende = handelsende(tag) + + # Weg des Runners. + kerzen = AlpacaKerzenquelle.from_secrets("config/secrets.toml", feed="iex").kerzen_des_tages( + "AAPL.XNAS", ende + ) + print("Kerzen ueber die Kerzenquelle:", len(kerzen)) + if len(kerzen) < FENSTER + EPISODE: + print("VORAUSSETZUNG FEHLT: zu wenige Kerzen fuer eine Episode") + return 3 + + # Weg des Trainings: dieselbe Anfrage noch einmal roh, dann die Umwandlung des Katalogs. + geheim = tomllib.loads(Path("config/secrets.toml").read_text(encoding="utf-8"))["alpaca"] + kopf = {"APCA-API-KEY-ID": geheim["key_id"], "APCA-API-SECRET-KEY": geheim["secret_key"]} + parameter = { + "symbols": "AAPL", + "timeframe": "1Min", + "start": handelsbeginn(ende).isoformat().replace("+00:00", "Z"), + "end": ende.isoformat().replace("+00:00", "Z"), + "feed": "iex", + "adjustment": "raw", + "limit": 10000, + } + antwort = requests.get( + "https://data.alpaca.markets/v2/stocks/bars", headers=kopf, params=parameter, timeout=30 + ) + antwort.raise_for_status() + nutzlast = antwort.json() + if nutzlast.get("next_page_token"): + print("VORAUSSETZUNG FEHLT: die Antwort hat eine Folgeseite; der Vergleich waere unvollstaendig") + return 3 + instrument = TestInstrumentProvider.equity(symbol="AAPL", venue="XNAS") + bars = nur_regulaere_handelszeit( + alpaca_json_zu_bars( + nutzlast, + symbol="AAPL", + bar_type=BarType.from_str(BAR_TYPE), + price_precision=instrument.price_precision, + size_precision=instrument.size_precision, + ) + ) + print("Kerzen ueber den Katalogweg: ", len(bars)) + if len(bars) != len(kerzen): + print("BEFUND: die zwei Wege liefern verschieden viele Kerzen") + return 2 + + halbe_cent = sum( + 1 + for eintrag in nutzlast["bars"]["AAPL"] + for feld in ("o", "h", "l", "c") + if round(float(eintrag[feld]) * 100) != float(eintrag[feld]) * 100 + ) + print("Rohkurse mit mehr als zwei Nachkommastellen:", halbe_cent) + + if sabotage: + kerzen = list(kerzen) + kerzen[57] = dataclasses.replace(kerzen[57], close=round(kerzen[57].close + 0.01, 2)) + + venue = lade_venue_config("config/venues/us_equity.toml") + env = NautilusEnv( + venue_config=venue, + instrument=instrument, + bars=bars, + config=EnvConfig( + bar_type=BAR_TYPE, + fenster=FENSTER, + basis_menge=BASIS, + max_stufe=MAX_STUFE, + episoden_laenge=EPISODE, + indikator_perioden=PERIODEN, + ), + ) + startkapital = venue.starting_balance.as_double() + + verglichen = 0 + ungleich = 0 + groesste = 0.0 + for start in range(0, len(bars) - FENSTER - EPISODE + 1, 5): + soll, _ = env.reset(options={"start_index": start}) + ist = baue_beobachtung( + kerzen[start : start + FENSTER], + position=0, + hoechste_position=MAX_STUFE * BASIS, + kontowert=startkapital, + startkapital=startkapital, + schritte_seit_trade=0, + episoden_laenge=EPISODE, + indikator_perioden=PERIODEN, + ) + verglichen += 1 + if soll.shape != ist.shape or not np.array_equal(soll, ist): + ungleich += 1 + abstand = float(np.max(np.abs(soll.astype(np.float64) - ist.astype(np.float64)))) + groesste = max(groesste, abstand) + if ungleich <= 3: + stellen = np.flatnonzero(soll != ist)[:5] + print(f" ungleich ab Kerze {start}: Stellen {stellen.tolist()}, groesster Abstand {abstand:.3e}") + env.close() + + print(f"{verglichen} Beobachtungen verglichen, {ungleich} ungleich, groesster Abstand {groesste:.3e}") + if ungleich: + print("BEFUND: die Beobachtung des Runners weicht von der der NautilusEnv ab") + return 2 + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Pruefskripts ist selbst ein Befund + print(f"BEFUND: das Pruefskript ist abgebrochen: {ausnahme!r}") + sys.exit(2) diff --git a/.vault-tools/trading-plattform-pruefung/start_probe_backend.py b/.vault-tools/trading-plattform-pruefung/start_probe_backend.py index 21b7627..af242ae 100644 --- a/.vault-tools/trading-plattform-pruefung/start_probe_backend.py +++ b/.vault-tools/trading-plattform-pruefung/start_probe_backend.py @@ -32,4 +32,4 @@ app = erzeuge_app( dist=Path("dashboard/dist"), ) print(f"Probe-Backend auf 127.0.0.1:{port}, Takt {config.sendeintervall_s} s, Register {temp}", flush=True) -uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning") +uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning", proxy_headers=False) diff --git a/.vault-tools/trading-plattform-pruefung/start_probe_zugang.py b/.vault-tools/trading-plattform-pruefung/start_probe_zugang.py new file mode 100644 index 0000000..fe24d18 --- /dev/null +++ b/.vault-tools/trading-plattform-pruefung/start_probe_zugang.py @@ -0,0 +1,109 @@ +"""Probe-Backend mit Anmeldung: die echte App mit Attrappe, Wegwerf-Register und Wegwerf-Zugang. + +Koordinator-Werkzeug zu Teil 10, nicht fuers Repo. Kein Broker-Aufruf, kein Ereignis im echten +Register, keine echte Zugangsdatei. Der Wegwerf-Zugang traegt ein bekanntes Passwort und das +Beispielgeheimnis der TOTP-Literatur; beides steht in zugang.json im Ausgabeordner, damit +angriffe.py und der Browser-Lauf sich anmelden koennen. + +Aufruf aus dem Projektordner (oder einer Kopie davon): + python start_probe_zugang.py [mit-paperstatus] +""" + +import dataclasses +import json +import sys +from datetime import UTC +from datetime import datetime +from pathlib import Path + +import uvicorn + +from trading.broker.attrappe import BrokerAttrappe +from trading.dashboard.app import erzeuge_app +from trading.dashboard.config import lade_dashboard_config +from trading.dashboard.zugang import Benutzer +from trading.dashboard.zugang import schreibe_benutzer +from trading.dashboard.zugang import setze_passwort +from trading.killswitch.killswitch import lade_killswitch_config +from trading.registry.registry import Registry + +BENUTZER = "probe" +PASSWORT = "nur-fuer-die-probe-2026" +GEHEIMNIS = "JBSWY3DPEHPK3PXP" + +port = int(sys.argv[1]) +ordner = Path(sys.argv[2]) +ordner.mkdir(parents=True, exist_ok=True) +mit_status = len(sys.argv) > 3 and sys.argv[3] == "mit-paperstatus" + +zugang_datei = ordner / "zugang.toml" +schreibe_benutzer( + zugang_datei, + {BENUTZER: Benutzer(name=BENUTZER, passwort=setze_passwort(PASSWORT), totp_geheimnis=GEHEIMNIS)}, +) +(ordner / "zugang.json").write_text( + json.dumps({"benutzer": BENUTZER, "passwort": PASSWORT, "geheimnis": GEHEIMNIS}), + encoding="utf-8", +) + +paper_ordner = ordner / "paper" +paper_ordner.mkdir(exist_ok=True) +status_datei = ordner / "status.json" +if mit_status: + protokoll = paper_ordner / "2026-09-18T131502-paper.jsonl" + zeilen = [ + {"art": "kopf", "modus": "paper"}, + {"art": "takt", "schritt": 1, "phase": "handelt", "position": 10, "zielposition": 10}, + ] + protokoll.write_text("\n".join(json.dumps(z) for z in zeilen) + "\n", encoding="utf-8") + status_datei.write_text( + json.dumps( + { + "zeit": datetime.now(UTC).isoformat(), + "modus": "paper", + "phase": "handelt", + "agent": { + "quelle": "register", + "name": "aapl-minute-ppo", + "version": "v20260907-074410", + "status": "paper", + }, + "instrument": "AAPL.XNAS", + "killswitch": "aktiv", + "position": 10, + "zielposition": 10, + "letzte_aktion": {"zeit": datetime.now(UTC).isoformat(), "aktion": 3, "stufe": 1}, + "letzte_kerze": datetime.now(UTC).replace(second=0, microsecond=0).isoformat(), + "orders": 1, + "kontowert": 100012.5, + "startkapital": 100000.0, + "hinweis": None, + "protokoll": str(protokoll), + "pid": 1, + } + ), + encoding="utf-8", + ) + +config = dataclasses.replace( + lade_dashboard_config("config/dashboard.toml"), + port=port, + broker_wahl="attrappe", + sendeintervall_s=2.0, + erlaubte_hosts=(f"127.0.0.1:{port}", f"localhost:{port}"), + anmeldung_pflicht=True, + mfa_pflicht=True, + zugang_datei=str(zugang_datei), + zugang_protokoll=str(ordner / "anmeldungen.jsonl"), + paper_status_datei=str(status_datei), + paper_protokoll_ordner=str(paper_ordner), +) +app = erzeuge_app( + broker=BrokerAttrappe(), + registry=Registry(db_pfad=ordner / "registry.db", artefakt_wurzel=ordner / "artifacts"), + config=config, + killswitch_config=lade_killswitch_config("config/killswitch.toml"), + dist=Path("dashboard/dist"), +) +print(f"Probe-Backend mit Anmeldung auf 127.0.0.1:{port}, Ablage {ordner}", flush=True) +uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning", server_header=False, proxy_headers=False)