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:
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())
|
||||
Reference in new issue
Block a user