Automatischer Push: 8 Datei(en) geändert

- .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 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-09-18 04:00:01 +02:00
1 parent 3bb28e4b3c
commit 371a1f5d2a
8 files changed
+890 -3

No files matched your search

@@ -2,10 +2,10 @@
type: resource type: resource
status: active status: active
title: "Trading Plattform: Prüfwerkzeuge des Koordinators fürs Dashboard" 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] tags: [vault-tools, trading-plattform]
created: 2026-09-17 created: 2026-09-17
updated: 2026-09-17 updated: 2026-09-18
--- ---
# Prüfwerkzeuge fürs Dashboard der Trading Plattform # 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 | | `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` | | `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 <port> <ablage> [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 <basis-url> <ablage-mit-zugang.json>` |
| `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:<port> <ablage>/zugang.json <ausgabeordner> [--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 <jsonl>` |
| `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. 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/`. Die generischen Planwerkzeuge liegen unter `.vault-tools/plan-kopie/`.
@@ -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 <basis-url> <ablageordner des probe-backends> [--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)
@@ -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 <basis-url> <zugang.json> <ausgabeordner> [--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)
@@ -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())
@@ -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 <protokoll.jsonl>
"""
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))
@@ -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)
@@ -32,4 +32,4 @@ app = erzeuge_app(
dist=Path("dashboard/dist"), dist=Path("dashboard/dist"),
) )
print(f"Probe-Backend auf 127.0.0.1:{port}, Takt {config.sendeintervall_s} s, Register {temp}", flush=True) 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)
@@ -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 <port> <ausgabeordner> [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)