Regelwerk, Agenten, Skills und Werkzeuge veröffentlichen

Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore
arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur,
was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen,
das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor.

Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale
Bezeichnung ersetzt; die Regeln selbst sind unverändert.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-09-17 22:38:15 +02:00
commit 2c9a4475fc
97 files changed
+14627

No files matched your search

@@ -0,0 +1,27 @@
---
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."
tags: [vault-tools, trading-plattform]
created: 2026-09-17
updated: 2026-09-17
---
# Prüfwerkzeuge fürs Dashboard der Trading Plattform
Koordinator-Werkzeuge, nicht fürs Repo. Sie starten **nie** den echten Broker: Das Probe-Backend läuft mit `BrokerAttrappe` und einem Wegwerf-Register unter `/tmp`, liest aber die echten Protokolle und den echten Katalog. Python-Teile laufen in WSL aus dem Projektordner, die Browser-Teile mit dem Windows-Python und Playwright (Chromium).
| Datei | Was sie tut | Aufruf |
|---|---|---|
| `pruef_teil3b.py` | Liste und Einzelansicht der Läufe gegen einen eigenen Vollparse, Kerzen gegen eine eigene pandas-Rechnung; `--sabotage` verfälscht je einen Wert und muss mit Exit 2 enden (sieben Fälle) | `wsl -d Debian -- bash -s -- pruef_teil3b.py alles [--sabotage zahl] < lauf_pruef.sh` |
| `pruef_ws.py`, `lauf_ws_probe.sh`, `start_probe_backend.py`, `start_probe.sh` | zwei WebSocket-Verbindungen am Probe-Backend müssen dieselben `zeit`-Werte sehen | `wsl -d Debian -- bash -s < lauf_ws_probe.sh` |
| `browserlauf.py` | bei 375, 768, 1024 und 1440 px: Zeilen in „Läufe", zwei Einzelansichten, Minuten- und Tageschart gezeichnet, kein waagerechter Scroll, keine Konsolenmeldung; schreibt Screenshots — **ansehen** | Probe-Backend starten (`wsl -d Debian -- bash -s 8145 2.0 < start_probe.sh`, im Hintergrund), dann `python -X utf8 browserlauf.py http://localhost:8145 <ausgabeordner>` |
| `wiederaufbau.py` | Backend beenden und neu starten, die Kopfzeile muss „Backend getrennt" und wieder „Backend verbunden" zeigen, ohne Neuladen; `--ohne-neustart` muss rot enden | `python -X utf8 wiederaufbau.py` |
| `skripte_probe.sh`, `abkuerzung_probe.sh` | `--help`, Lauf ohne Schalter, unbekanntes Argument und Abkürzungen von `--ausfuehren` — **ausschließlich aus einem leeren Temp-Verzeichnis**; die Übung selbst wird nie gefahren | `wsl -d Debian -- bash -s < skripte_probe.sh` |
| `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` |
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,19 @@
#!/bin/bash
# Nimmt argparse eine Abkuerzung von --ausfuehren an? Ausschliesslich aus einem leeren Temp-Verzeichnis:
# Dort endet ein angenommener Schalter an der fehlenden config/risk.toml, bevor irgendetwas laeuft.
# Aufruf: wsl -d Debian -- bash -s < abkuerzung_probe.sh
set -u
P="/mnt/d/Claude/Code Projekte/active - Trading Plattform"
PY="$HOME/venvs/trading/bin/python"
T=$(mktemp -d)
cd "$T" || exit 9
if [ "$(ls -A "$T" | wc -l)" != "0" ]; then echo "ABBRUCH: Verzeichnis nicht leer"; exit 9; fi
for arg in --a --ausf --ausfuehre; do
"$PY" "$P/scripts/killswitch_uebung.py" "$arg" > "$T/aus.txt" 2> "$T/err.txt" < /dev/null
code=$?
echo "=== $arg: exit=$code"
grep -v -E "Pandas4Warning|utcnow|warnings.warn" "$T/err.txt" | tail -2
rm -f "$T/aus.txt" "$T/err.txt"
done
echo "Rest im Temp-Verzeichnis: $(ls -A "$T" | wc -l) Eintraege"
rmdir "$T" 2>/dev/null
@@ -0,0 +1,128 @@
"""Browser-Lauf zu Teil 3b mit Playwright (Windows-Python), gegen ein laufendes Backend.
Koordinator-Pruefskript, nicht fuers Repo. Aufruf:
python -X utf8 browserlauf.py <basis-url> <ausgabeordner>
Prueft je Breite: kein waagerechter Scroll, keine Konsolenfehler, Ansicht "Laeufe" zeigt Zeilen,
Ansicht "Markt" zeichnet (Canvas nicht leer). Schreibt Screenshots und eine ergebnis.json.
Was es nicht sieht: ob die Zahlen stimmen (dafuer pruef_teil3b.py) und wie es aussieht (dafuer
die Screenshots, die der Koordinator selbst ansieht).
"""
import json
import sys
from pathlib import Path
from playwright.sync_api import TimeoutError as PlaywrightTimeout
from playwright.sync_api import sync_playwright
BREITEN = [375, 768, 1024, 1440]
# Je ein Minuten- und ein Tagesdatensatz; die Namen stehen so im Katalog (gemessen 2026-09-17).
MARKT_DATENSAETZE = [
("minuten", "AAPL.XNAS-1-MINUTE-LAST-EXTERNAL"),
("tage", "MSFT.XNAS-1-DAY-LAST-EXTERNAL"),
]
# aria-label der Knoepfe aus Laeufe.tsx: `Lauf ${quelle}/${datei}`.
LAEUFE_EINZEL = [
("tage", "Lauf signal/tage-teil9-20260917-030800.jsonl"),
("kampagne", "Lauf laeufe/aapl-minute-ppo-v20260914-132555.jsonl"),
]
CANVAS_PROBE = """
() => {
const flaechen = Array.from(document.querySelectorAll('canvas'));
let groesste = null;
for (const c of flaechen) {
if (!groesste || c.width * c.height > groesste.width * groesste.height) groesste = c;
}
if (!groesste || groesste.width === 0 || groesste.height === 0) return {anzahl: flaechen.length, farben: 0};
const kontext = groesste.getContext('2d');
const daten = kontext.getImageData(0, 0, groesste.width, groesste.height).data;
const farben = new Set();
const schritt = Math.max(4, Math.floor(daten.length / 4 / 20000) * 4);
for (let i = 0; i < daten.length; i += schritt) {
farben.add(`${daten[i]},${daten[i+1]},${daten[i+2]},${daten[i+3]}`);
}
return {anzahl: flaechen.length, breite: groesste.width, hoehe: groesste.height, farben: farben.size};
}
"""
SCROLL_PROBE = "() => ({scroll: document.documentElement.scrollWidth, sicht: document.documentElement.clientWidth})"
def main() -> int:
basis = sys.argv[1].rstrip("/")
ziel = Path(sys.argv[2])
ziel.mkdir(parents=True, exist_ok=True)
ergebnis: dict = {"basis": basis, "breiten": {}, "befunde": []}
with sync_playwright() as spiel:
browser = spiel.chromium.launch()
for breite in BREITEN:
seite = browser.new_page(viewport={"width": breite, "height": 900})
meldungen: list[str] = []
seite.on("console", lambda m, liste=meldungen: liste.append(f"{m.type}: {m.text}") if m.type in ("error", "warning") else None)
seite.on("pageerror", lambda f, liste=meldungen: liste.append(f"pageerror: {f}"))
seite.goto(basis + "/", wait_until="networkidle")
eintrag: dict = {}
def messe(name: str) -> None:
masse = seite.evaluate(SCROLL_PROBE)
eintrag[f"scroll_{name}"] = masse
if masse["scroll"] > masse["sicht"] + 1:
ergebnis["befunde"].append(f"{breite}px {name}: waagerechter Scroll {masse}")
alarme = seite.get_by_role("alert").count()
if alarme:
ergebnis["befunde"].append(f"{breite}px {name}: {alarme} Alarm(e): {seite.get_by_role('alert').first.inner_text()[:160]}")
seite.screenshot(path=str(ziel / f"{name}-{breite}.png"), full_page=True)
for ansicht in ("Läufe", "Markt"):
seite.get_by_role("button", name=ansicht, exact=True).click()
seite.wait_for_timeout(1500)
if ansicht == "Markt":
# Die Ansicht laedt nach Plan nichts von selbst: erst die Auswahl holt Kerzen.
eintrag["canvas"] = {}
for kurz, bar_type in MARKT_DATENSAETZE:
try:
seite.get_by_label("Datensatz").select_option(bar_type)
seite.wait_for_selector("canvas", timeout=20000)
seite.wait_for_timeout(2000)
except PlaywrightTimeout:
pass
probe = seite.evaluate(CANVAS_PROBE)
eintrag["canvas"][kurz] = probe
if probe["farben"] < 5:
ergebnis["befunde"].append(f"{breite}px Markt/{kurz}: kein gezeichnetes Canvas ({probe})")
messe(f"markt-{kurz}")
else:
eintrag["laeufe_zeilen"] = seite.locator("table tbody tr").count()
if eintrag["laeufe_zeilen"] == 0:
ergebnis["befunde"].append(f"{breite}px Laeufe: keine Tabellenzeile")
messe("laeufe")
for kurz, marke in LAEUFE_EINZEL:
try:
seite.get_by_role("button", name=marke, exact=True).click(timeout=8000)
seite.wait_for_load_state("networkidle", timeout=20000)
seite.wait_for_timeout(1200)
except PlaywrightTimeout:
ergebnis["befunde"].append(f"{breite}px Laeufe/{kurz}: Knopf '{marke}' nicht bedienbar")
continue
eintrag[f"einzel_{kurz}_tabellen"] = seite.locator("table").count()
if eintrag[f"einzel_{kurz}_tabellen"] < 2:
ergebnis["befunde"].append(f"{breite}px Laeufe/{kurz}: die Einzelansicht zeigt keine eigene Tabelle")
messe(f"laeufe-{kurz}")
eintrag["konsole"] = meldungen
if any(m.startswith(("error", "pageerror")) for m in meldungen):
ergebnis["befunde"].append(f"{breite}px: Konsolenfehler {meldungen[:3]}")
ergebnis["breiten"][str(breite)] = eintrag
seite.close()
browser.close()
(ziel / "ergebnis.json").write_text(json.dumps(ergebnis, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps(ergebnis, indent=2, ensure_ascii=False))
return 2 if ergebnis["befunde"] else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,11 @@
#!/bin/bash
# Startet ein Python-Skript aus dem Scratchpad im echten Projektordner, mit Argumenten.
# Aufruf: wsl -d Debian -- bash -s -- <skript.py> [argumente...] < lauf_pruef.sh
set -u
SCRATCH="/mnt/d/Claude/.vault-tools/trading-plattform-pruefung"
cd "/mnt/d/Claude/Code Projekte/active - Trading Plattform" || exit 9
SKRIPT="$1"
shift
PYTHONPATH=. timeout 560 "$HOME/venvs/trading/bin/python" "$SCRATCH/$SKRIPT" "$@" < /dev/null 2> /tmp/lauf_pruef_stderr.txt
echo "EXIT=$?"
grep -v -E "Pandas4Warning|utcnow|warnings.warn" /tmp/lauf_pruef_stderr.txt | tail -8
@@ -0,0 +1,18 @@
#!/bin/bash
# Startet das Probe-Backend auf Port 8145 (Takt 1 s), faehrt pruef_ws.py dagegen, beendet das Backend.
set -u
SCRATCH="/mnt/d/Claude/.vault-tools/trading-plattform-pruefung"
cd "/mnt/d/Claude/Code Projekte/active - Trading Plattform" || exit 9
PY="$HOME/venvs/trading/bin/python"
PYTHONPATH=. "$PY" "$SCRATCH/start_probe_backend.py" 8145 1.0 < /dev/null > /tmp/probe_backend.log 2>&1 &
BACKEND=$!
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
if "$PY" -c "import socket; s=socket.create_connection(('127.0.0.1', 8145), 0.5); s.close()" 2>/dev/null; then break; fi
sleep 1
done
PYTHONPATH=. timeout 60 "$PY" "$SCRATCH/pruef_ws.py" 8145 < /dev/null
echo "EXIT_PRUEF=$?"
kill "$BACKEND" 2>/dev/null
wait "$BACKEND" 2>/dev/null
echo "--- Backend-Protokoll ---"
grep -v -E "Pandas4Warning|utcnow" /tmp/probe_backend.log | tail -5
@@ -0,0 +1,165 @@
"""Mutantenlauf auf der materialisierten Kopie: Schlaegt jeder neue Test an, wenn sein Gegenstand kaputt ist?
Laeuft in WSL; Aufruf: python mutanten_py.py <wsl-pfad-der-kopie>. Je Mutant: Datei als Bytes sichern, genau eine Stelle ersetzen, die genannten Tests
fahren, Datei aus der Sicherung zurueckspielen. Erwartet ist ROT; ein gruener Mutant ist ein Befund.
Zuerst die Grundlinie: Ohne Mutant muessen dieselben Tests gruen sein.
"""
import subprocess
import sys
from pathlib import Path
MAT = Path(sys.argv[1]) # WSL-Pfad der Kopie mit dem angewendeten Plan
PY = str(Path.home() / "venvs/trading/bin/python")
MUTANTEN = [
(
"lies_lauf trennt mit der Zeilentrennung von str",
"trading/dashboard/laeufe.py",
' with pfad.open("rb") as strom:\n for roh in strom:\n if not roh.strip():',
' with pfad.open("rb") as strom:\n for roh in [z.encode("utf-8") for z in strom.read().decode("utf-8").splitlines()]:\n if not roh.strip():',
["tests/test_dashboard_laeufe.py::test_die_einzelansicht_trennt_nicht_an_u2028"],
),
(
"_ist_protokolldatei laesst alles durch",
"trading/dashboard/laeufe.py",
" return pfad.is_file() and not pfad.is_symlink()",
" return True",
[
"tests/test_dashboard_laeufe.py::test_ein_ordner_mit_der_endung_jsonl_ist_kein_lauf",
"tests/test_dashboard_laeufe.py::test_eine_symbolische_verknuepfung_ist_kein_lauf",
],
),
(
"_ist_protokolldatei folgt Verknuepfungen",
"trading/dashboard/laeufe.py",
" return pfad.is_file() and not pfad.is_symlink()",
" return pfad.is_file()",
["tests/test_dashboard_laeufe.py::test_eine_symbolische_verknuepfung_ist_kein_lauf"],
),
(
"letzte_zeilen gibt ein Bruchstueck ohne Trenner zurueck",
"trading/dashboard/laeufe.py",
" if not trenner:\n return []",
" if not trenner:\n return [_bruchstueck]",
["tests/test_dashboard_laeufe.py::test_eine_letzte_zeile_ueber_dem_block_ohne_zeilenende_liefert_nichts"],
),
(
"Eimeranfang mit festem Stundenschritt",
"trading/dashboard/markt.py",
" anfang_min = HANDELSSTART_MIN + nummer * aufloesung_min",
" anfang_min = HANDELSSTART_MIN + nummer * 60",
["tests/test_dashboard_markt.py::test_15_und_5_minuten_teilen_den_handelstag_ohne_rest"],
),
(
"Tagesachse in der Achsenzone statt in der Handelszone",
"trading/dashboard/markt.py",
" return zeitpunkt.tz_convert(HANDELSZONE).date().isoformat()",
" return zeitpunkt.tz_convert(zone).date().isoformat()",
["tests/test_dashboard_markt.py::test_eine_tageskerze_bekommt_den_new_yorker_kalendertag_als_string"],
),
(
"Tagesachse in UTC",
"trading/dashboard/markt.py",
" return zeitpunkt.tz_convert(HANDELSZONE).date().isoformat()",
" return zeitpunkt.date().isoformat()",
["tests/test_dashboard_markt.py::test_eine_tageskerze_bekommt_den_new_yorker_kalendertag_als_string"],
),
(
"Handelstag mit 391 Kerzen",
"trading/dashboard/markt.py",
"KERZEN_JE_HANDELSTAG = 390",
"KERZEN_JE_HANDELSTAG = 391",
["tests/test_dashboard_markt.py::test_der_handelstag_hat_390_minuten_und_die_lesespanne_folgt_daraus"],
),
(
"Kalenderfaktor 1,5 statt 1,6",
"trading/dashboard/markt.py",
"KALENDERFAKTOR_MINUTEN = 1.6",
"KALENDERFAKTOR_MINUTEN = 1.5",
["tests/test_dashboard_markt.py::test_der_handelstag_hat_390_minuten_und_die_lesespanne_folgt_daraus"],
),
(
"lies_kerzen liest die Metadaten ungeschuetzt",
"trading/dashboard/markt.py",
" meta = _meta(katalog, bar_type)\n bis = _bis(ende, meta)",
" meta = katalog.meta(bar_type)\n bis = _bis(ende, meta)",
["tests/test_dashboard_markt.py::test_lies_kerzen_uebersteht_eine_unlesbare_metadatei"],
),
(
"katalog() prueft nur die Wurzel",
"trading/dashboard/app.py",
' if not (wurzel / "_meta").is_dir():',
" if not wurzel.is_dir():",
["tests/test_dashboard_endpunkte.py::test_ein_ordner_ohne_meta_ist_kein_katalog_und_bekommt_keines"],
),
(
"_ersetze laesst den alten Snapshot liegen",
"trading/dashboard/verteiler.py",
" if warteschlange.full():\n try:",
" if warteschlange.full():\n return\n if warteschlange.full():\n try:",
["tests/test_dashboard_verteiler.py::test_eine_volle_warteschlange_bekommt_den_neuen_snapshot"],
),
(
"killswitch_uebung prueft den Schalter erst nach der Konfiguration",
"scripts/killswitch_uebung.py",
" if not parse_args().ausfuehren:\n print(OHNE_SCHALTER, file=sys.stderr)\n return 2\n\n limits = lade_limits(\"config/risk.toml\")",
" limits = lade_limits(\"config/risk.toml\")\n if not parse_args().ausfuehren:\n print(OHNE_SCHALTER, file=sys.stderr)\n return 2\n",
[
"tests/test_skripte_starten.py::test_die_killswitch_uebung_ohne_schalter_endet_mit_exit_2",
],
),
]
GRUND = ""
def fahre(tests: list[str]) -> bool:
"""Exitcode allein ist kein Beleg: Ein Mutant mit Syntaxfehler waere genauso rot.
Deshalb wird die erste Fehlerzeile mitgeschrieben und in der Ausgabe gezeigt."""
global GRUND
ergebnis = subprocess.run(
[PY, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-x", "--tb=short", *tests],
cwd=MAT,
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
)
zeilen = [z.strip() for z in ergebnis.stdout.splitlines() if z.startswith("E ")]
GRUND = " | ".join(zeilen[:2])[:260]
return ergebnis.returncode == 0
def main() -> int:
befunde = 0
alle = sorted({t for m in MUTANTEN for t in m[4]})
if not fahre(alle):
print("BEFUND Grundlinie: ohne Mutant sind nicht alle Tests gruen -- der Lauf misst nichts.")
return 2
print(f"Grundlinie gruen ({len(alle)} Tests).")
for name, datei, alt, neu, tests in MUTANTEN:
pfad = MAT / datei
sicherung = pfad.read_bytes()
text = sicherung.decode("utf-8")
anzahl = text.count(alt)
if anzahl != 1:
print(f"BEFUND Mutant '{name}': Stelle steht {anzahl}-mal in {datei}")
befunde += 1
continue
try:
pfad.write_bytes(text.replace(alt, neu).encode("utf-8"))
gruen = fahre(tests)
finally:
pfad.write_bytes(sicherung)
if gruen:
print(f"BEFUND Mutant '{name}' bleibt GRUEN: {tests}")
befunde += 1
else:
print(f"rot {name}\n {GRUND}")
print(f"{befunde} Befund(e).")
return 2 if befunde else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,116 @@
"""Mutantenlauf Frontend auf der materialisierten Kopie (Windows-Python, vitest je Datei).
Je Mutant: Datei als Bytes sichern, genau eine Stelle ersetzen, die Testdatei fahren, zurueckspielen.
Erwartet ist ROT; ein gruener Mutant ist ein Befund. Zuerst die Grundlinie.
"""
import subprocess
import sys
from pathlib import Path
DASH = Path(sys.argv[1]) / "dashboard" # Windows-Pfad der Kopie mit dem angewendeten Plan
MUTANTEN = [
(
"stufe() gibt den rohen Index zurueck",
"src/components/LaufKampagne.tsx",
" const wert = aktion - (verteilung.length - 1) / 2;",
" const wert = aktion;",
"src/components/LaufKampagne.test.tsx",
),
(
"stufe() ohne Pruefung der Verteilung",
"src/components/LaufKampagne.tsx",
" if (!Array.isArray(verteilung) || verteilung.length % 2 === 0) return STRICH;",
" if (!Array.isArray(verteilung)) return STRICH;",
"src/components/LaufKampagne.test.tsx",
),
(
"stufe() ohne Pruefung des Bereichs",
"src/components/LaufKampagne.tsx",
" if (aktion < 0 || aktion >= verteilung.length) return STRICH;\n",
"",
"src/components/LaufKampagne.test.tsx",
),
(
"Stille Fenster als Ertrag formatiert",
"src/components/LaufKampagne.tsx",
" wert={anteil(abschluss.anteil_fenster_ohne_trade)}",
" wert={prozent(abschluss.anteil_fenster_ohne_trade)}",
"src/components/LaufKampagne.test.tsx",
),
(
"close() merkt sich das Ende nicht",
"src/lib/api.ts",
" beendet = true;\n",
"",
"src/lib/api.test.ts",
),
(
"close() raeumt den Zeitgeber nicht",
"src/lib/api.ts",
" if (zeitgeber !== null) clearTimeout(zeitgeber);\n",
"",
"src/lib/api.test.ts",
),
(
# Der Backslash kommt aus chr(92): Eine Escape-Folge im eigenen Quelltext wertet sonst
# irgendeine Schicht aus -- genau das ist bei der Umsetzung passiert.
"Lizenztext mit doppeltem Backslash (Escape-Folge als Klartext auf der Seite)",
"src/components/Markt.tsx",
"Charts" + chr(92) + "u2122 Copyright (" + chr(92) + "u0441) 2025",
"Charts" + chr(92) * 2 + "u2122 Copyright (" + chr(92) * 2 + "u0441) 2025",
"src/components/Markt.test.tsx",
),
]
def fahre(testdatei: str) -> tuple[bool, str]:
ergebnis = subprocess.run(
f"npx vitest run {testdatei}",
cwd=DASH,
shell=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
stdin=subprocess.DEVNULL,
)
ausgabe = ergebnis.stdout + ergebnis.stderr
rote = [z.strip() for z in ausgabe.splitlines() if z.strip().startswith(("×", "FAIL", "✗")) or " > " in z and "FAIL" in z]
return ergebnis.returncode == 0, "; ".join(rote[:4])
def main() -> int:
befunde = 0
for testdatei in sorted({m[4] for m in MUTANTEN}):
gruen, _ = fahre(testdatei)
if not gruen:
print(f"BEFUND Grundlinie: {testdatei} ist ohne Mutant nicht gruen")
return 2
print("Grundlinie gruen.")
for name, datei, alt, neu, testdatei in MUTANTEN:
pfad = DASH / datei
sicherung = pfad.read_bytes()
text = sicherung.decode("utf-8")
anzahl = text.count(alt)
if anzahl != 1:
print(f"BEFUND Mutant '{name}': Stelle steht {anzahl}-mal in {datei}")
befunde += 1
continue
try:
pfad.write_bytes(text.replace(alt, neu).encode("utf-8"))
gruen, rote = fahre(testdatei)
finally:
pfad.write_bytes(sicherung)
if gruen:
print(f"BEFUND Mutant '{name}' bleibt GRUEN ({testdatei})")
befunde += 1
else:
print(f"rot {name} [{rote}]")
print(f"{befunde} Befund(e).")
return 2 if befunde else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,394 @@
"""Eigener Lauf mit echten Daten zu Teil 3b: Liste und Einzelansicht der Laeufe, Kerzen.
Koordinator-Pruefskript, nicht fuers Repo. Laeuft in WSL aus dem Projektordner (PYTHONPATH=.).
Geltungsbereich und Herkunft jeder Antwort:
F1 Die Liste nennt jede *.jsonl der konfigurierten Ordner genau einmal. Quelle: eigenes glob
F2 art und zeit je Eintrag gleich der ersten Zeile der Datei. Quelle: eigener Vollparse
F3 vollstaendig genau dann, wenn die letzte Zeile ergebnis/zusammenfassung. Projektvorgabe (Spec)
F4 Jede Zahl der Abschlusszeilen gleich dem eigenen Vollparse, ausser den
ausdruecklich weggelassenen Schluesseln beste_zelle, beste_zelle_je_gruppe. Projektvorgabe (Spec)
F5 Einzelansicht: Auslassungen (kurven, zeitstempel, startpunkte, training),
Zellen je Symbol und Zaehlungen je Symbol gleich eigener Zaehlung. Projektvorgabe (Spec)
F6 Kerzen: Zusammenfassung ab 09:30 New York gleich eigener pandas-Rechnung
aus demselben Rohabruf; Achsenwert = Berliner Wanduhr; Tagesstrings. Projektvorgabe (Spec)
Was es nicht sieht: die Oberflaeche, den WebSocket, die Skripte. Es ist ein Fangnetz, kein Nachweis.
Regressionsnachweis: --sabotage verfaelscht je einen Wert der Antwort vor dem Vergleich; der Lauf
muss dann mit Exit 2 und der passenden Meldung enden.
"""
import argparse
import json
import math
import sys
import tempfile
import time
import traceback
from pathlib import Path
BEFUNDE: list[str] = []
def befund(text: str) -> None:
BEFUNDE.append(text)
print("BEFUND:", text)
def gleich(a, b) -> bool:
# FastAPI 0.141.1 serialisiert ueber pydantic und macht aus NaN und Infinity ein null
# (gemessen 2026-09-17); der eigene Vollparse liefert dafuer float('nan') oder inf.
if a is None and isinstance(b, float) and not math.isfinite(b):
return True
if b is None and isinstance(a, float) and not math.isfinite(a):
return True
if isinstance(a, float) and isinstance(b, float):
if math.isnan(a) and math.isnan(b):
return True
return a == b
if isinstance(a, dict) and isinstance(b, dict):
return a.keys() == b.keys() and all(gleich(a[k], b[k]) for k in a)
if isinstance(a, list) and isinstance(b, list):
return len(a) == len(b) and all(gleich(x, y) for x, y in zip(a, b))
return a == b
def vollparse(pfad: Path) -> list[dict]:
zeilen = []
with pfad.open("rb") as strom:
for roh in strom:
roh = roh.rstrip(b"\n")
if roh.strip():
zeilen.append(json.loads(roh.decode("utf-8")))
return zeilen
def baue_client():
from fastapi.testclient import TestClient
from trading.broker.attrappe import BrokerAttrappe
from trading.dashboard.app import erzeuge_app
from trading.dashboard.config import lade_dashboard_config
from trading.killswitch.killswitch import lade_killswitch_config
from trading.registry.registry import Registry
config = lade_dashboard_config("config/dashboard.toml")
temp = Path(tempfile.mkdtemp(prefix="pruef3b-"))
registry = Registry(db_pfad=temp / "registry.db", artefakt_wurzel=temp / "artifacts")
app = erzeuge_app(
broker=BrokerAttrappe(),
registry=registry,
config=config,
killswitch_config=lade_killswitch_config("config/killswitch.toml"),
dist=Path("dashboard/dist"),
)
return TestClient(app), config
ABSCHLUSS = {
"kampagne": ("walk_forward", "ergebnis"),
"signal": ("zusammenfassung",),
"signal_tage": ("zusammenfassung",),
}
LETZTE = {"kampagne": "ergebnis", "signal": "zusammenfassung", "signal_tage": "zusammenfassung"}
WEGGELASSEN = ("beste_zelle", "beste_zelle_je_gruppe")
def pruefe_laeufe(client, config, sabotage: str | None) -> None:
t0 = time.perf_counter()
antwort = client.get("/api/laeufe")
dauer = time.perf_counter() - t0
if antwort.status_code != 200:
befund(f"/api/laeufe antwortet {antwort.status_code}")
return
daten = antwort.json()
liste = daten["laeufe"]
print(f"/api/laeufe: {len(liste)} Eintraege in {dauer:.3f} s, hinweise={daten.get('hinweise')}")
if sabotage == "zahl":
for eintrag in liste:
if eintrag["art"] == "kampagne" and eintrag["ergebnis"]:
eintrag["ergebnis"]["walk_forward"]["sharpe"] += 1e-9
break
if sabotage == "fehlt":
liste.pop()
if sabotage == "art":
liste[0]["art"] = "signal" if liste[0]["art"] != "signal" else "kampagne"
dateien = {}
for ordner in config.laeufe_ordner:
for pfad in sorted(Path(ordner).glob("*.jsonl")):
dateien[(Path(ordner).name, pfad.name)] = pfad
gelistet = [(e["quelle"], e["datei"]) for e in liste]
if sorted(gelistet) != sorted(dateien):
befund(f"F1: Liste und Ordner weichen ab: nur Liste {set(gelistet) - set(dateien)}, nur Ordner {set(dateien) - set(gelistet)}")
if len(gelistet) != len(set(gelistet)):
befund("F1: doppelte Eintraege in der Liste")
zeiten = [e["zeit"] for e in liste if e["zeit"] is not None]
if zeiten != sorted(zeiten, reverse=True):
befund("Sortierung: zeit ist nicht absteigend")
arten: dict[str, int] = {}
for eintrag in liste:
schluessel = (eintrag["quelle"], eintrag["datei"])
pfad = dateien.get(schluessel)
if pfad is None:
continue
zeilen = vollparse(pfad)
kopf = zeilen[0]
arten[eintrag["art"]] = arten.get(eintrag["art"], 0) + 1
if eintrag["art"] != kopf.get("art"):
befund(f"F2 {pfad.name}: art {eintrag['art']!r} gegen {kopf.get('art')!r}")
continue
if eintrag["zeit"] != kopf.get("zeit"):
befund(f"F2 {pfad.name}: zeit {eintrag['zeit']!r} gegen {kopf.get('zeit')!r}")
if eintrag["groesse_bytes"] != pfad.stat().st_size:
befund(f"{pfad.name}: groesse_bytes {eintrag['groesse_bytes']} gegen {pfad.stat().st_size}")
art = kopf.get("art")
erwartet_voll = zeilen[-1].get("art") == LETZTE.get(art)
if eintrag["vollstaendig"] != erwartet_voll:
befund(f"F3 {pfad.name}: vollstaendig {eintrag['vollstaendig']} gegen {erwartet_voll}")
for name in ABSCHLUSS.get(art, ()):
eigene = [z for z in zeilen if z.get("art") == name]
if not eigene:
continue
soll = {k: v for k, v in eigene[-1].items() if k not in WEGGELASSEN}
ist = (eintrag["ergebnis"] or {}).get(name)
if ist is None:
befund(f"F4 {pfad.name}: Abschlusszeile {name} fehlt in der Liste")
elif not gleich(ist, soll):
abweichend = [k for k in soll if k not in ist or not gleich(ist[k], soll[k])]
zusaetzlich = [k for k in ist if k not in soll]
befund(f"F4 {pfad.name}: {name} weicht ab in {abweichend}, zusaetzlich {zusaetzlich}")
kopf_ist = eintrag["kopf"]
kopf_soll = json.loads(json.dumps(kopf))
if art == "signal_tage":
kopf_soll.get("daten", {}).pop("instrumente", None)
if not gleich(kopf_ist, kopf_soll):
befund(f"{pfad.name}: kopf weicht ab in {[k for k in kopf_soll if not gleich(kopf_ist.get(k), kopf_soll[k])]}")
print("Arten:", arten)
def pruefe_einzelansicht(client, config, sabotage: str | None) -> None:
kandidaten = [
("kampagne", "laeufe", "aapl-minute-ppo-v20260914-132555.jsonl", None),
# Traegt 30 Zeilen mit "gewinnfaktor": NaN (Protokollschreiber vor dem Fix vom 2026-09-14).
("kampagne", "laeufe", "aapl-minute-ppo-v20260914-085048.jsonl", None),
("signal", "signal", "signal-20260914-213950.jsonl", None),
("signal_tage", "signal", "tage-teil9-20260917-030800.jsonl", "MSFT"),
]
ordner = {Path(o).name: Path(o) for o in config.laeufe_ordner}
for art, quelle, datei, symbol in kandidaten:
pfad = ordner[quelle] / datei
if not pfad.exists():
befund(f"Voraussetzung fehlt: {pfad}")
continue
adresse = f"/api/laeufe/{quelle}/{datei}" + (f"?symbol={symbol}" if symbol else "")
t0 = time.perf_counter()
antwort = client.get(adresse)
dauer = time.perf_counter() - t0
if antwort.status_code != 200:
befund(f"{adresse} antwortet {antwort.status_code}")
continue
daten = antwort.json()
print(f"{adresse}: {len(antwort.content):,} Bytes in {dauer:.3f} s, Zeilenarten { {k: len(v) for k, v in daten['zeilen'].items()} }, unlesbar {daten.get('unlesbare_zeilen')}")
zeilen = vollparse(pfad)
if daten["art"] != art:
befund(f"F5 {datei}: art {daten['art']!r}")
if not gleich(daten["kopf"], zeilen[0]):
befund(f"F5 {datei}: kopf der Einzelansicht ist nicht die ganze Kopfzeile")
if sabotage == "zelle" and art == "signal_tage":
daten["zeilen"]["zelle_instrument"].pop()
if sabotage == "zaehlung" and art == "signal_tage":
daten["symbole"][0]["k1"] += 1
if art == "kampagne":
if "training" in daten["zeilen"] and daten["zeilen"]["training"]:
befund("F5: training-Zeilen sind in der Einzelansicht")
eigene = [z for z in zeilen if z.get("art") == "fenster"]
ist = daten["zeilen"].get("fenster", [])
if len(ist) != len(eigene):
befund(f"F5: {len(ist)} fenster gegen {len(eigene)}")
for a, b in zip(ist, eigene):
soll = {k: v for k, v in b.items() if k not in ("kurven", "zeitstempel", "startpunkte")}
if not gleich(a, soll):
befund(f"F5: fenster {b.get('nummer')} weicht ab oder traegt Masse: {sorted(set(a) ^ set(soll))}")
break
for name in ("walk_forward", "ergebnis"):
if not gleich(daten["zeilen"].get(name, []), [z for z in zeilen if z.get("art") == name]):
befund(f"F5: {name} weicht ab")
if art == "signal":
for name in ("zelle", "permutation", "zusammenfassung"):
if not gleich(daten["zeilen"].get(name, []), [z for z in zeilen if z.get("art") == name]):
befund(f"F5 signal: {name} weicht ab")
if art == "signal_tage":
for name in ("diagnose", "zelle_pool", "jahr", "permutation", "zusammenfassung"):
if not gleich(daten["zeilen"].get(name, []), [z for z in zeilen if z.get("art") == name]):
befund(f"F5 signal_tage: {name} weicht ab")
eigene = [z for z in zeilen if z.get("art") == "zelle_instrument" and z.get("symbol") == symbol]
if not gleich(daten["zeilen"].get("zelle_instrument", []), eigene):
befund(f"F5 signal_tage: Zellen fuer {symbol}: {len(daten['zeilen'].get('zelle_instrument', []))} gegen {len(eigene)}")
print(f" Zellen {symbol}: {len(eigene)}")
zaehlung: dict[str, dict] = {}
for z in zeilen:
if z.get("art") != "zelle_instrument":
continue
s = zaehlung.setdefault(z["symbol"], {"symbol": z["symbol"], "gruppe": z.get("gruppe"), "zellen": 0, "t_ab_2": 0, "k1": 0, "k2": 0, "signale": 0})
s["zellen"] += 1
t = z.get("t_ic_massgeblich")
if isinstance(t, (int, float)) and not math.isnan(t) and abs(t) >= 2:
s["t_ab_2"] += 1
s["k1"] += 1 if z.get("K1") is True else 0
s["k2"] += 1 if z.get("K2") is True else 0
s["signale"] += 1 if z.get("signal") is True else 0
ist = {s["symbol"]: s for s in daten.get("symbole", [])}
if ist.keys() != zaehlung.keys():
befund(f"F5 symbole: Menge weicht ab: {set(ist) ^ set(zaehlung)}")
for name, soll in zaehlung.items():
if name in ist and not gleich({k: ist[name].get(k) for k in soll}, soll):
befund(f"F5 symbole {name}: {ist[name]} gegen {soll}")
print(f" Symbole: {len(zaehlung)}, Summe Signale {sum(s['signale'] for s in zaehlung.values())}, Summe K1 {sum(s['k1'] for s in zaehlung.values())}")
ohne = client.get(f"/api/laeufe/{quelle}/{datei}").json()
if ohne["zeilen"].get("zelle_instrument"):
befund("F5: ohne symbol kommen Instrumentenzellen")
for adresse in ("/api/laeufe/signal/gibt-es-nicht.jsonl", "/api/laeufe/fremd/signal-20260914-213950.jsonl", "/api/laeufe/signal/..%2F..%2Fconfig%2Fsecrets.toml"):
status = client.get(adresse).status_code
print(f"{adresse}: {status}")
if status != 404:
befund(f"Pfadsicherheit: {adresse} antwortet {status} statt 404")
def pruefe_kerzen(client, config, sabotage: str | None) -> None:
import numpy as np
import pandas as pd
from trading.data.catalog import Catalog
antwort = client.get("/api/markt/datensaetze")
if antwort.status_code != 200:
befund(f"/api/markt/datensaetze antwortet {antwort.status_code}")
return
saetze = antwort.json()["datensaetze"]
print(f"/api/markt/datensaetze: {len(saetze)} Datensaetze")
katalog = Catalog(config.markt_katalog)
if sorted(s["bar_type"] for s in saetze) != katalog.bar_types():
befund("F6: Datensaetze weichen von Catalog.bar_types() ab")
minute = [s for s in saetze if s["bar_type"] == "AAPL.XNAS-1-MINUTE-LAST-EXTERNAL"]
if not minute or minute[0].get("aufloesungen_min") != [1, 5, 15, 60]:
befund(f"F6: aufloesungen_min des Minutendatensatzes: {minute[0].get('aufloesungen_min') if minute else 'fehlt'}")
tag = "MSFT.XNAS-1-DAY-LAST-EXTERNAL"
t0 = time.perf_counter()
antwort = client.get("/api/markt/kerzen", params={"bar_type": tag, "aufloesung_min": 1, "anzahl": 3000})
dauer = time.perf_counter() - t0
if antwort.status_code != 200:
befund(f"Tageskerzen antworten {antwort.status_code}: {antwort.text[:300]}")
else:
kerzen = antwort.json()["kerzen"]
roh = katalog.read(tag)
print(f"Tageskerzen {tag}: {len(kerzen)} in {dauer:.3f} s (Katalog {len(roh)})")
if len(kerzen) != len(roh):
befund(f"F6 Tage: {len(kerzen)} gegen {len(roh)}")
for k, b in list(zip(kerzen, roh))[:: max(1, len(roh) // 200)]:
soll_tag = pd.Timestamp(b.ts_event, unit="ns", tz="UTC").tz_convert("America/New_York").strftime("%Y-%m-%d")
if k["achse"] != soll_tag or not gleich([k["open"], k["high"], k["low"], k["close"], k["volumen"]], [float(b.open), float(b.high), float(b.low), float(b.close), float(b.volume)]):
befund(f"F6 Tage: Kerze {k} gegen {b}")
break
minute_typ = "AAPL.XNAS-1-MINUTE-LAST-EXTERNAL"
for aufloesung, ende, anzahl in [(1, "2026-09-10", 500), (5, "2026-09-10", 500), (15, "2026-03-13", 400), (60, "2025-11-05", 300), (60, "2026-09-10", 3000)]:
t0 = time.perf_counter()
antwort = client.get("/api/markt/kerzen", params={"bar_type": minute_typ, "aufloesung_min": aufloesung, "ende": ende, "anzahl": anzahl})
dauer = time.perf_counter() - t0
if antwort.status_code != 200:
befund(f"Minuten {aufloesung}/{ende}: {antwort.status_code} {antwort.text[:300]}")
continue
kerzen = antwort.json()["kerzen"]
if sabotage == "kerze" and aufloesung == 5:
kerzen[3]["high"] += 0.01
if sabotage == "achse" and aufloesung == 15:
kerzen[5]["achse"] += 3600
bis = pd.Timestamp(ende, tz="UTC") + pd.Timedelta(days=1)
von = bis - pd.Timedelta(days=int(anzahl * aufloesung / 390 * 1.6) + 30)
roh = katalog.read(minute_typ, von, bis)
rahmen = pd.DataFrame(
{
"ts": [b.ts_event for b in roh],
"open": [float(b.open) for b in roh],
"high": [float(b.high) for b in roh],
"low": [float(b.low) for b in roh],
"close": [float(b.close) for b in roh],
"volumen": [float(b.volume) for b in roh],
}
)
ny = pd.to_datetime(rahmen["ts"], unit="ns", utc=True).dt.tz_convert("America/New_York")
rahmen = rahmen[ny.dt.strftime("%Y-%m-%d") <= ende].copy()
ny = ny[rahmen.index]
minuten = ny.dt.hour * 60 + ny.dt.minute - (9 * 60 + 30)
rahmen["tag"] = ny.dt.strftime("%Y-%m-%d")
rahmen["eimer"] = (minuten // aufloesung).astype(int)
gruppen = rahmen.groupby(["tag", "eimer"], sort=True)
eigen = gruppen.agg(open=("open", "first"), high=("high", "max"), low=("low", "min"), close=("close", "last"), volumen=("volumen", "sum")).reset_index()
anfang = pd.to_datetime(eigen["tag"]).dt.tz_localize("America/New_York") + pd.to_timedelta(9 * 60 + 30 + eigen["eimer"] * aufloesung, unit="m")
eigen["zeit_utc"] = anfang.dt.tz_convert("UTC")
berlin = anfang.dt.tz_convert("Europe/Berlin").dt.tz_localize(None)
eigen["achse"] = (berlin - pd.Timestamp("1970-01-01")) // pd.Timedelta(seconds=1)
eigen = eigen.tail(anzahl).reset_index(drop=True)
print(f"Minuten {aufloesung:>2} min bis {ende}: {len(kerzen)} Kerzen in {dauer:.3f} s (eigene Rechnung {len(eigen)}, Rohkerzen {len(roh)})")
if len(kerzen) != len(eigen):
befund(f"F6 {aufloesung} min bis {ende}: {len(kerzen)} Kerzen gegen {len(eigen)}")
continue
for i, k in enumerate(kerzen):
e = eigen.iloc[i]
zeit_ist = pd.Timestamp(k["zeit"])
werte_ist = np.array([k["open"], k["high"], k["low"], k["close"], k["volumen"]], dtype=float)
werte_soll = np.array([e["open"], e["high"], e["low"], e["close"], e["volumen"]], dtype=float)
if zeit_ist != e["zeit_utc"] or int(k["achse"]) != int(e["achse"]) or not np.allclose(werte_ist, werte_soll, rtol=0, atol=1e-9):
befund(f"F6 {aufloesung} min bis {ende}, Kerze {i}: ist {k} soll zeit {e['zeit_utc']} achse {int(e['achse'])} werte {werte_soll.tolist()}")
break
achsen = [k["achse"] for k in kerzen]
if achsen != sorted(set(achsen)):
befund(f"F6 {aufloesung} min bis {ende}: Achsenwerte nicht streng steigend")
for params, erwartet in [
({"bar_type": "GIBT.ES-NICHT", "aufloesung_min": 1, "anzahl": 10}, 404),
({"bar_type": minute_typ, "aufloesung_min": 7, "anzahl": 10}, 422),
({"bar_type": minute_typ, "aufloesung_min": 5, "anzahl": 0}, 422),
({"bar_type": minute_typ, "aufloesung_min": 5, "anzahl": 999999}, 422),
({"bar_type": minute_typ, "aufloesung_min": 5, "anzahl": 10, "ende": "kein-datum"}, 422),
({"bar_type": tag, "aufloesung_min": 5, "anzahl": 10}, 422),
]:
status = client.get("/api/markt/kerzen", params=params).status_code
print(f"{params}: {status}")
if status != erwartet:
befund(f"F6 Fehlerfall {params}: {status} statt {erwartet}")
def main() -> int:
zerleger = argparse.ArgumentParser()
zerleger.add_argument("teil", choices=["laeufe", "einzel", "kerzen", "alles"])
zerleger.add_argument("--sabotage", choices=["zahl", "fehlt", "art", "zelle", "zaehlung", "kerze", "achse"])
argumente = zerleger.parse_args()
try:
client, config = baue_client()
with client:
if argumente.teil in ("laeufe", "alles"):
pruefe_laeufe(client, config, argumente.sabotage)
if argumente.teil in ("einzel", "alles"):
pruefe_einzelansicht(client, config, argumente.sabotage)
if argumente.teil in ("kerzen", "alles"):
pruefe_kerzen(client, config, argumente.sabotage)
except Exception:
befund("unerwarteter Abbruch:\n" + traceback.format_exc())
print()
if BEFUNDE:
print(f"{len(BEFUNDE)} Befund(e).")
return 2
print("Kein Befund.")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,58 @@
"""Eigener Lauf zu Teil 3b: Bekommen zwei Verbindungen am laufenden Backend denselben Snapshot?
Koordinator-Pruefskript, nicht fuers Repo. Laeuft in WSL gegen ws://127.0.0.1:8144/ws/uebersicht.
Frage: Tragen die Snapshots beider Verbindungen dieselben zeit-Werte (ein Bau je Takt), und kommt
der erste Snapshot einer spaeteren Verbindung sofort? Herkunft: Projektvorgabe (Spec, C4a).
Was es nicht sieht: die Zahl der Broker-Anfragen selbst (die zaehlt der Test mit der Attrappe).
"""
import asyncio
import json
import sys
import time
import websockets
PORT = sys.argv[1] if len(sys.argv) > 1 else "8144"
ADRESSE = f"ws://127.0.0.1:{PORT}/ws/uebersicht"
async def lies(name: str, anzahl: int, ergebnis: dict) -> None:
t0 = time.monotonic()
async with websockets.connect(ADRESSE) as strom:
zeiten = []
for i in range(anzahl):
daten = json.loads(await strom.recv())
if i == 0:
ergebnis[name + "_erste_nach_s"] = round(time.monotonic() - t0, 3)
zeiten.append(daten["zeit"])
ergebnis[name] = zeiten
async def main() -> int:
ergebnis: dict = {}
erste = asyncio.create_task(lies("a", 4, ergebnis))
await asyncio.sleep(2.0)
zweite = asyncio.create_task(lies("b", 3, ergebnis))
await asyncio.gather(erste, zweite)
print(json.dumps(ergebnis, indent=2))
gemeinsam = set(ergebnis["a"]) & set(ergebnis["b"])
print("gemeinsame zeit-Werte:", len(gemeinsam))
befunde = []
if len(gemeinsam) < 2:
befunde.append("die zwei Verbindungen teilen weniger als zwei Snapshots")
if ergebnis["b"][0] not in ergebnis["a"]:
befunde.append("der erste Snapshot der spaeteren Verbindung ist kein Snapshot der ersten")
if ergebnis["b_erste_nach_s"] > 1.0:
befunde.append(f"die spaetere Verbindung wartete {ergebnis['b_erste_nach_s']} s auf ihren ersten Snapshot")
for text in befunde:
print("BEFUND:", text)
return 2 if befunde else 0
if __name__ == "__main__":
try:
sys.exit(asyncio.run(main()))
except Exception as ausnahme:
print("BEFUND: unerwarteter Abbruch:", repr(ausnahme))
sys.exit(2)
@@ -0,0 +1,8 @@
#!/bin/bash
# Faehrt pytest im echten Projektordner. Aufruf: wsl -d Debian -- bash -s -- <pytest-argumente...> < real_pytest.sh
set -u
cd "/mnt/d/Claude/Code Projekte/active - Trading Plattform" || exit 9
"$HOME/venvs/trading/bin/python" -m pytest -p no:cacheprovider "$@" < /dev/null 2> /tmp/real_pytest_stderr.txt | grep -v -E "Pandas4Warning|utcnow|^$" | tail -12
echo "--- stderr (ohne Warnungen) ---"
grep -v -E "Pandas4Warning|utcnow" /tmp/real_pytest_stderr.txt | tail -10
echo "Task-destroyed-Meldungen: $(grep -c 'Task was destroyed' /tmp/real_pytest_stderr.txt)"
@@ -0,0 +1,36 @@
#!/bin/bash
# Probe der zwei Skripte mit argparse -- ausschliesslich aus einem leeren Temp-Verzeichnis.
# Die Uebung selbst (--ausfuehren) wird NICHT gefahren. Aufruf: wsl -d Debian -- bash -s < skripte_probe.sh
set -u
P="/mnt/d/Claude/Code Projekte/active - Trading Plattform"
PY="$HOME/venvs/trading/bin/python"
# Vorbedingung: Es muessen die neuen Fassungen sein. Die alte Uebung ignoriert jedes Argument und handelt.
if ! grep -q "if not parse_args().ausfuehren" "$P/scripts/killswitch_uebung.py"; then echo "ABBRUCH: killswitch_uebung.py prueft den Schalter nicht"; exit 9; fi
if ! grep -q "argparse" "$P/scripts/dashboard.py"; then echo "ABBRUCH: dashboard.py ohne argparse"; exit 9; fi
T=$(mktemp -d)
cd "$T" || exit 9
echo "Arbeitsverzeichnis: $T ($(ls -A "$T" | wc -l) Eintraege)"
VORHER=$(ls -la --time-style=full-iso "$P/data/registry.db" 2>/dev/null | md5sum)
lauf() {
name="$1"; shift
"$PY" "$@" > "$T/aus.txt" 2> "$T/err.txt" < /dev/null
code=$?
echo "=== $name: exit=$code"
echo "--- stdout (erste Zeilen):"; head -4 "$T/aus.txt"
echo "--- stderr (ohne Warnungen, letzte Zeilen):"; grep -v -E "Pandas4Warning|utcnow|warnings.warn" "$T/err.txt" | tail -3
rm -f "$T/aus.txt" "$T/err.txt"
}
lauf "dashboard.py --help" "$P/scripts/dashboard.py" --help
lauf "killswitch_uebung.py --help" "$P/scripts/killswitch_uebung.py" --help
lauf "killswitch_uebung.py ohne Schalter" "$P/scripts/killswitch_uebung.py"
lauf "killswitch_uebung.py --unbekannt" "$P/scripts/killswitch_uebung.py" --unbekannt
lauf "dashboard.py ohne Konfiguration im Verzeichnis" "$P/scripts/dashboard.py"
NACHHER=$(ls -la --time-style=full-iso "$P/data/registry.db" 2>/dev/null | md5sum)
echo "Registerordner unveraendert: $([ "$VORHER" = "$NACHHER" ] && echo ja || echo NEIN)"
echo "Rest im Temp-Verzeichnis: $(ls -A "$T" | wc -l) Eintraege"
rmdir "$T" 2>/dev/null
@@ -0,0 +1,6 @@
#!/bin/bash
# Probe-Backend im Vordergrund (fuer run_in_background des Werkzeugs). Aufruf: bash -s <port> <takt> < start_probe.sh
set -u
SCRATCH="/mnt/d/Claude/.vault-tools/trading-plattform-pruefung"
cd "/mnt/d/Claude/Code Projekte/active - Trading Plattform" || exit 9
PYTHONPATH=. exec "$HOME/venvs/trading/bin/python" "$SCRATCH/start_probe_backend.py" "$1" "$2" < /dev/null
@@ -0,0 +1,35 @@
"""Probe-Backend des Koordinators: die echte App mit Attrappe und Wegwerf-Register auf eigenem Port.
Kein Broker-Aufruf, kein Ereignis im echten Register. Aufruf aus dem Projektordner:
python start_probe_backend.py <port> [sendeintervall_s]
"""
import dataclasses
import sys
import tempfile
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.killswitch.killswitch import lade_killswitch_config
from trading.registry.registry import Registry
port = int(sys.argv[1])
config = lade_dashboard_config("config/dashboard.toml")
aenderungen = {"port": port, "broker_wahl": "attrappe"}
if len(sys.argv) > 2:
aenderungen["sendeintervall_s"] = float(sys.argv[2])
config = dataclasses.replace(config, **aenderungen)
temp = Path(tempfile.mkdtemp(prefix="probe-backend-"))
app = erzeuge_app(
broker=BrokerAttrappe(),
registry=Registry(db_pfad=temp / "registry.db", artefakt_wurzel=temp / "artifacts"),
config=config,
killswitch_config=lade_killswitch_config("config/killswitch.toml"),
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")
@@ -0,0 +1,115 @@
"""Wiederaufbau-Test im Browser: Backend weg, Backend wieder da -- ohne Neuladen der Seite.
Koordinator-Pruefskript (Windows-Python mit Playwright), nicht fuers Repo.
Frage: Zeigt die Kopfzeile "Backend getrennt", wenn das Backend stirbt, und von selbst wieder
"Backend verbunden", wenn es zurueck ist? Herkunft: Projektvorgabe (Spec, Abschnitt Frontend).
Statustexte aus dashboard/src/components/Kopfzeile.tsx (abgeschrieben am 2026-09-17).
Was es nicht sieht: die Wartezeiten 1/2/5/10 s im Einzelnen (dafuer api.test.ts), andere Browser als Chromium.
Regressionsnachweis: --ohne-neustart laesst das Backend unten; der Lauf muss dann mit Exit 2 am Schritt
"wieder verbunden" enden.
Aufruf: python -X utf8 wiederaufbau.py [--ohne-neustart]
"""
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
from playwright.sync_api import TimeoutError as PlaywrightTimeout
from playwright.sync_api import sync_playwright
PORT = 8146
BASIS = f"http://localhost:{PORT}"
HIER = Path(__file__).parent
def starte_backend() -> subprocess.Popen:
skript = open(HIER / "start_probe.sh", "rb")
return subprocess.Popen(
["wsl", "-d", "Debian", "--", "bash", "-s", str(PORT), "1.0"],
stdin=skript, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
def beende_backend() -> None:
subprocess.run(["wsl", "-d", "Debian", "--", "pkill", "-f", f"start_probe_backend.py {PORT}"],
stdin=subprocess.DEVNULL, capture_output=True)
def warte_auf_backend(sekunden: float) -> bool:
ende = time.time() + sekunden
while time.time() < ende:
try:
with urllib.request.urlopen(BASIS + "/api/uebersicht", timeout=1) as antwort:
if antwort.status == 200:
return True
except OSError:
time.sleep(0.5)
return False
def main() -> int:
ohne_neustart = "--ohne-neustart" in sys.argv
beende_backend()
prozess = starte_backend()
schritte: list[str] = []
try:
if not warte_auf_backend(40):
print("BEFUND: Probe-Backend kommt nicht hoch -- der Lauf misst nichts")
return 2
with sync_playwright() as p:
browser = p.chromium.launch()
seite = browser.new_page(viewport={"width": 1280, "height": 800})
fehler: list[str] = []
seite.on("pageerror", lambda e: fehler.append(str(e)))
seite.goto(BASIS + "/", wait_until="domcontentloaded")
seite.evaluate("() => { window.__marke = 'nicht neu geladen'; }")
def erwarte(text: str, sekunden: float, name: str) -> bool:
start = time.time()
try:
seite.get_by_text(text, exact=True).first.wait_for(timeout=sekunden * 1000)
except PlaywrightTimeout:
print(f"BEFUND: Schritt '{name}': '{text}' erscheint nicht binnen {sekunden} s")
return False
schritte.append(f"{name}: '{text}' nach {time.time() - start:.1f} s")
return True
if not erwarte("Backend verbunden", 20, "erste Verbindung"):
return 2
beende_backend()
prozess.wait(timeout=20)
if not erwarte("Backend getrennt", 20, "nach dem Ende des Backends"):
return 2
if not ohne_neustart:
prozess = starte_backend()
if not warte_auf_backend(40):
print("BEFUND: Probe-Backend kommt nach dem Neustart nicht hoch")
return 2
if not erwarte("Backend verbunden", 30, "wieder verbunden"):
return 2
marke = seite.evaluate("() => window.__marke")
if marke != "nicht neu geladen":
print(f"BEFUND: Die Seite wurde neu geladen (Marke {marke!r})")
return 2
if fehler:
print(f"BEFUND: Seitenfehler: {fehler[:3]}")
return 2
browser.close()
for schritt in schritte:
print("ok " + schritt)
print("Kein Befund: Wiederaufbau ohne Neuladen.")
return 0
finally:
for schritt in schritte:
pass
beende_backend()
try:
prozess.wait(timeout=10)
except subprocess.TimeoutExpired:
prozess.kill()
if __name__ == "__main__":
sys.exit(main())