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:
1 parent
3bb28e4b3c
commit
371a1f5d2a
8 files changed
+890
-3
No files matched your search
@@ -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 <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.
|
||||
|
||||
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"),
|
||||
)
|
||||
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)
|
||||
Reference in new issue
Block a user