Automatischer Push: 14 Datei(en) geändert

- .vault-tools/plan-kopie/LIESMICH.md
- .vault-tools/plan-kopie/rot_nachweis.py
- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/dienste_stand.sh
- .vault-tools/trading-plattform-pruefung/ha_einrichten.py
- .vault-tools/trading-plattform-pruefung/hole_kerzen.py
- .vault-tools/trading-plattform-pruefung/neustart_echtlauf.py
- .vault-tools/trading-plattform-pruefung/ntfy_lesen.py
- .vault-tools/trading-plattform-pruefung/pause_echtlauf.py
- .vault-tools/trading-plattform-pruefung/wiederanlauf_probe.ps1
- AI/Regelwerk/codeworkflow.md
- AI/Regelwerk/projekte.md
- … und 2 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-09-21 04:00:01 +02:00
1 parent 395642c480
commit 246af19683
14 files changed
+964 -7

No files matched your search

+3 -1
View File
@@ -5,7 +5,7 @@ title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergl
summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)."
tags: [vault-tools, codeworkflow]
created: 2026-09-17
updated: 2026-09-17
updated: 2026-09-20
---
# Planwerkzeuge für die Kopie mit angewendetem Plan
@@ -27,6 +27,8 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
Seit 2026-09-20 quotet `rot_nachweis.py` den Arbeitsordner für den WSL-Aufruf (`shlex.quote`). Ein Temp-Pfad mit Leerzeichen — der Benutzername steckt im Scratchpad-Pfad — ließ vorher das `cd` scheitern: Alle sieben Tasks eines Plans der Trading Plattform standen als „ROT pytest: keine Summenzeile" da, ohne dass ein einziger Test lief. Nach der Korrektur derselbe Lauf mit echten Summenzeilen (`3 failed, 17 passed`, `1 error`, …). Wer „keine Summenzeile" bei **jedem** Task sieht, hat kein Rot gemessen, sondern ein kaputtes Werkzeug.
`welle_vergleich_treiber.py` fährt 14 Angriffsfälle gegen `welle_vergleich.py` (Grundlinie zuerst; fremde Datei, unveränderte Zieldatei, Abweichung in erster und letzter Zeile und in der letzten Zieldatei, CRLF, Datei fehlt in der Kopie, Task ohne Zieldateien, Kopie fehlt, Löschung nicht ausgeführt, Prosa-Datei unverändert, Zieldatei außerhalb des Projekts, Datei länger als der Plan). `plan_zeichen.py` hat am 2026-09-17 an einem echten Fund rot gezeigt (U+2028 im Plan). `plan_materialisiere.py` reproduziert Task 108 des Teil-3b-Plans aus dem Stand vor den Wellen byte-genau. `kopie_bauen.py` ist am 2026-09-17 an drei kaputten Eingaben rot gesehen: Ziel existiert schon (Exit 2, nichts kopiert), Ausschluss `node_modules` ohne Gegenstück im Wurzelordner (Befund), und ein aus der Kopie entferntes Paket `trading/data` (die Paketprüfung nennt es; nach dem Zurücklegen leer). Einen Treiber dazu gibt es noch nicht — die drei Fälle sind von Hand gefahren.
## Was die Skripte nicht sehen
+5 -1
View File
@@ -28,6 +28,7 @@ spec.json: [{"task": "task-141-....md", "vor": ["task-...md", ...]}, ...]
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
@@ -148,7 +149,10 @@ def main() -> int:
ober_tests = [t for t in tests if not t.endswith(".py")]
zeilen = []
if python_tests:
befehl = f"cd {wsl_pfad(rot)} && ~/venvs/trading/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60"
# shlex.quote: Ein Arbeitsordner mit Leerzeichen (Benutzername im Temp-Pfad) liess das cd
# scheitern; jeder Task stand dann als "ROT pytest: keine Summenzeile" da, ohne dass ein Test
# lief (gefunden 2026-09-20).
befehl = f"cd {shlex.quote(wsl_pfad(rot))} && ~/venvs/trading/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60"
lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", befehl],
capture_output=True, text=True, encoding="utf-8", errors="replace")
ausgabe = lauf.stdout
@@ -5,7 +5,7 @@ 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), erweitert 2026-09-18 (Teil 10: Anmeldung, Angriffsfälle, Beobachtung, Kerzenlatenz), bewusst nicht im Projekt-Repo."
tags: [vault-tools, trading-plattform]
created: 2026-09-17
updated: 2026-09-18
updated: 2026-09-21
---
# Prüfwerkzeuge fürs Dashboard der Trading Plattform
@@ -41,6 +41,25 @@ Koordinator-Werkzeuge, nicht fürs Repo. Sie starten **nie** den echten Broker:
**Seit 2026-09-18 kennen `angriffe.py` und `browserlauf_zugang.py` https.** `angriffe.py` erwartet je nach Schema der Basis-URL `Secure` am Cookie und `Strict-Transport-Security` (https) oder beides nicht (http), dazu unter https keine Antwort 200 auf Klartext; `--ca <pem>` nennt das Zertifikat, dem vertraut wird (geprüft, nicht abgeschaltet). `browserlauf_zugang.py --selbstsigniert` schaltet die Zertifikatsprüfung des Browsers ab. Regressionsnachweis vom selben Tag: gegen ein TLS-Backend mit einem Mutanten von `sicherheit.py` (ohne `Secure`, ohne HSTS) fielen genau die vier https-Fälle rot, Exit 2; Grundlinie über http 59 Fälle, über https 60. Läuft das Backend auf einem anderen Rechner, fährt man `angriffe.py` dort in einem Wegwerf-Container mit `--network host` und legt die drei Dateien der Ablage (`zugang.json`, `status.json`, `anmeldungen.jsonl`) per Verknüpfung in einen Ordner — das Werkzeug braucht Dateizugriff auf die Ablage des Backends.
## Paket F (2026-09-20): Produktivbetrieb als Dienst
| Datei | Was sie tut | Aufruf |
|---|---|---|
| `pause_echtlauf.py` | Tagesverlust-Schutz in der Pause auf den echten IEX-Kerzen eines Tages: rechnet aus den Rohkerzen vor, in welchem Takt die Grenze reißen muss (long und short, Grenze bei der Hälfte des größten Tagesverlusts), und hält das gegen das Protokoll der echten `PaperSitzung` im Zustand `angehalten`; dazu die Gegenprobe ohne Position. Einziger Netzzugriff: die Kerzen, lesend; Meldungen gehen an einen mitschreibenden Transport. Gemessen am 2026-09-17: long Takt 6, short Takt 173, je erwartet gleich gemessen | aus dem Projektordner in WSL: `python pause_echtlauf.py JJJJ-MM-TT` |
| `dienste_stand.sh` | nur lesend: Unit-Dateien, `is-enabled`, Zustand, Startzeiten und letzter Exit-Code von Dashboard-Dienst und Runner, Zeitgeber, `/api/gesund`, Start von systemd, die letzten Journalzeilen beider Dienste, `status.json` und `tagesstart.json` ohne Kontowerte. Das Journal geht ohne `sudo`, wenn der Benutzer in `adm` oder `systemd-journal` ist (hier: ja); sonst nimmt das Skript `sudo -S` mit `WSL_PW` aus der Umgebung | aus PowerShell: `wsl -d Debian --exec bash /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/dienste_stand.sh` |
| `hole_kerzen.py` | holt die echten IEX-Minutenkerzen eines Handelstags lesend und legt sie als JSON ab — die Eingabe für `neustart_echtlauf.py`. Gemessen am 2026-09-20 für den 2026-09-17: 390 Kerzen, byte-gleich mit dem Abruf des Abschlussprüfers | aus dem Projektordner in WSL: `python hole_kerzen.py JJJJ-MM-TT <ziel.json>` |
| `neustart_echtlauf.py` | der Tagesverlust nach einem Neustart mitten am Tag: rechnet aus den Rohkerzen vor, bei welcher Kerze die echte Grenze (0,2 Prozent) gegen den Tagesstart reißt und bei welcher gegen den Neustartkurs, und fährt dann zwei echte `PaperSitzung`en nacheinander mit gemeinsamer `tagesstart.json`, dazu die Gegenprobe ohne Datei. Kein Netz. Gemessen am 2026-09-20 auf den Kerzen des 2026-09-17 und 2026-09-18: mit Datei Riss bei der vorgerechneten Kerze (5, 150, 19), ohne erst bei 18, 173, 42 | aus dem Projektordner in WSL: `python neustart_echtlauf.py <kerzen.json>` |
| `ntfy_lesen.py` | liest über die Abfrage-Schnittstelle von ntfy zurück, was in der letzten Stunde unter dem Thema der Maschine ankam — Titel, Priorität, Zeit, erste Zeile. **Schickt nichts und gibt das Thema nie aus**, auch nicht in einer Fehlermeldung | aus dem Projektordner in WSL: `python ntfy_lesen.py` |
| `wiederanlauf_probe.ps1` | beendet die Distribution hart (`wsl --terminate`) und misst, wann geplante Aufgabe, Distribution und Dashboard von selbst zurück sind. **Ein laufender Runner stirbt mit — nur außerhalb der Handelszeit, nur mit `-Ausfuehren`** (ohne den Schalter Exit 2, nichts getan). Während der Messung kein anderer `wsl.exe`-Aufruf, jeder Klient weckt die Distribution selbst. Gemessen am 2026-09-20: Rückkehr am nächsten Fünf-Minuten-Rasterpunkt, Dashboard 13 s danach | `powershell -File wiederanlauf_probe.ps1 -Ausfuehren` |
## Paket H (2026-09-21): Kennzahlen nach Home Assistant
| Datei | Was sie tut | Aufruf |
|---|---|---|
| `ha_einrichten.py` | richtet in Home Assistant die Gegenstelle für den Hausmelder ein: acht Helfer mit dem Präfix `trading_` und eine Automation mit Webhook-Auslöser (`local_only`, nur POST). Die Webhook-ID entsteht im Skript (64 Hex-Zeichen), wird nie ausgegeben und landet nur in `[homeassistant]` der `config/secrets.toml` des Trading-Projekts, mit `aktiv = false`. `pruefen` und `lesen` lesen nur (`lesen` zeigt zusätzlich die Zustände der Helfer), `probe` schickt eine Beispiel-Nutzlast und liest zurück, `entfernen` baut Helfer und Automation wieder ab. **Läuft unter Windows mit dem Python des Home-Assistant-Projekts** — dort liegen der Klient `src/ha_verbindung.py`, die Adresse der Instanz und der Name der Token-Variable; das Token bleibt auf der Windows-Seite und kommt nie auf den Trading-Rechner. Gemessen am 2026-09-20/21: eingerichtet, `probe` rückgelesen wie gesendet, und ein POST des echten `Hausmelder` aus WSL kam in den Helfern an | `<ha-projekt>\.venv\Scripts\python.exe -X utf8 ha_einrichten.py <pruefen|lesen|einrichten|probe|entfernen> "<ha-projekt>" "<trading-projekt>"` |
Das Einrichten selbst (`deploy/dienste-einrichten.sh` unter `sudo -S`, `deploy/windows/wsl-wachhalten.ps1 -Aktion einrichten`) und das Eintragen des ntfy-Themas liefen aus dem Scratchpad der Sitzung; die Befehle stehen in der Administrator-Anleitung des Projekts, Abschnitt „Produktivbetrieb als Dienst".
Die Compose-Probe (Bauen aus dem echten Baum mit verankerten Ausschlüssen, Start, Healthcheck, Angriffsfälle, Runner-Probe im Container, Aufräumen) lag als `compose_probe_echt.sh` im Scratchpad der Sitzung und braucht `sudo -S` mit dem WSL-Passwort aus `$WSL_PW`; sie ist deshalb nicht hier abgelegt. Muster: `tar --exclude=./data` — ohne Anker fällt auch `trading/data` weg.
Vor dem Browser-Lauf `npm run build` im Projekt, sonst dient das Backend eine alte Oberfläche aus. Die Datensatz- und Dateinamen in `browserlauf.py` und `pruef_teil3b.py` sind die des Bestands vom 2026-09-17; nach einem neuen Katalog oder neuen Protokollen nachziehen. Die Statustexte in `wiederaufbau.py` sind aus `Kopfzeile.tsx` abgeschrieben.
@@ -0,0 +1,52 @@
#!/bin/bash
# Nur lesend: Stand der eingerichteten Dienste der Trading Plattform in WSL-Debian.
# Das Journal liest, wer in der Gruppe adm oder systemd-journal ist (hier: claude in adm);
# sonst mit sudo -- dann das Passwort ueber WSLENV als WSL_PW durchreichen, nie ausgeben.
PROJEKT="/mnt/d/Claude/Code Projekte/active - Trading Plattform"
journal() {
if journalctl -u "$1" --no-pager -n 1 >/dev/null 2>&1; then
journalctl -u "$1" --no-pager -n "$2" -o short-iso | cut -c1-200
elif [ -n "$WSL_PW" ]; then
printf '%s\n' "$WSL_PW" | sudo -S -p '' journalctl -u "$1" --no-pager -n "$2" -o short-iso | cut -c1-200
else
echo "(Journal nicht lesbar: weder Gruppe adm/systemd-journal noch WSL_PW)"
fi
}
echo "=== $(date -Is) ==="
echo "--- dateien ---"
ls -la /etc/systemd/system/ | grep -i "trading" | awk '{print $1, $5, $9}'
echo "--- enabled ---"
for u in trading-dashboard.service trading-paper.timer trading-paper.service; do
printf '%s: ' "$u"; systemctl is-enabled "$u" 2>&1
done
echo "--- zustand ---"
systemctl show -p ActiveState -p SubState -p Result -p ExecMainStartTimestamp -p NRestarts trading-dashboard.service | tr '\n' ' '; echo
systemctl show -p ActiveState -p SubState -p Result -p ExecMainStartTimestamp -p ExecMainExitTimestamp -p ExecMainStatus trading-paper.service | tr '\n' ' '; echo
echo "--- zeitgeber ---"
systemctl list-timers trading-paper.timer --all --no-pager | head -4
echo "--- gesund ---"
curl -s -o /dev/null -w 'dashboard in WSL: HTTP %{http_code}\n' --max-time 4 http://127.0.0.1:8144/api/gesund
echo "--- systemd laeuft seit ---"
systemctl show -p UserspaceTimestamp --value
echo "--- journal runner (letzte 16) ---"
journal trading-paper.service 16
echo "--- journal dashboard (letzte 8) ---"
journal trading-dashboard.service 8
echo "--- status.json und tagesstart.json (ohne Kontowerte) ---"
python3 - "$PROJEKT" <<'EOF'
import json
import sys
from pathlib import Path
ordner = Path(sys.argv[1]) / "data" / "paper"
for name, felder in (
("status.json", ("zeit", "phase", "killswitch", "position", "letzter_grund", "pid", "boot")),
("tagesstart.json", ("handelstag",)),
):
try:
daten = json.loads((ordner / name).read_text(encoding="utf-8"))
except (OSError, ValueError) as ausnahme:
print(f"{name}: nicht lesbar ({type(ausnahme).__name__})")
continue
print(name, {k: daten.get(k) for k in felder})
EOF
@@ -0,0 +1,231 @@
"""Richtet in Home Assistant die Gegenstelle fuer die Kennzahlen der Trading Plattform ein.
Koordinator-Werkzeug, nicht Teil des Repos. Laeuft unter WINDOWS mit dem Python des HA-Projekts
(dort liegen `websockets` und `src/ha_verbindung.py`; Adresse der Instanz und der Name der
Token-Variable stehen in jenem Modul, nicht hier). Das HA-Token bleibt auf der Windows-Seite und
wird nie ausgegeben; auf den Trading-Rechner kommt nur die Webhook-Adresse.
Aktionen:
pruefen nur lesend: Version, vorhandene Helfer mit dem Praefix, vorhandene Automation
lesen nur lesend: wie pruefen, dazu die Zustaende der Helfer, wie sie gerade in Home Assistant stehen
einrichten legt acht Helfer und eine Automation mit Webhook-Ausloeser an (local_only, nur POST),
erzeugt dafuer eine Webhook-ID (64 Hex-Zeichen) und traegt die Adresse in die
config/secrets.toml des Trading-Projekts ein ([homeassistant], aktiv = false).
Gibt die ID nie aus. Bricht ab, wenn es den Abschnitt oder die Automation schon gibt.
probe schickt eine Beispiel-Nutzlast an den Webhook (Adresse aus der Geheimnisdatei) und
liest die Helfer ueber die HA-API zurueck
entfernen loescht Automation und Helfer wieder; der Abschnitt in der Geheimnisdatei bleibt stehen
und wird genannt
Aufruf: <ha-projekt>\\.venv\\Scripts\\python.exe ha_einrichten.py <aktion> <ha-projektordner> <trading-projektordner>
"""
import json
import os
import secrets
import sys
import time
import tomllib
import urllib.request
from pathlib import Path
AUTOMATION_ID = "trading_plattform_kennzahlen"
ALIAS = "Trading Plattform: Kennzahlen uebernehmen"
PRAEFIX = "trading_"
TEXTE = ("Trading Phase", "Trading Modus", "Trading Killswitch")
ZAHLEN = (
("Trading Position", -100000, 100000, 1, "Stk"),
("Trading Tagesergebnis USD", -1000000, 1000000, 0.01, "USD"),
("Trading Herzschlag Alter s", -1, 100000000, 1, "s"),
)
SCHALTER = ("Trading Veraltet",)
ZEITEN = ("Trading Gesendet",)
def aktionen() -> list[dict]:
def text(ziel: str, schluessel: str, vorgabe: str) -> dict:
return {
"service": "input_text.set_value",
"target": {"entity_id": f"input_text.{ziel}"},
"data": {"value": "{{ (trigger.json." + schluessel + " | default('" + vorgabe + "') | string)[:100] }}"},
"continue_on_error": True,
}
def zahl(ziel: str, schluessel: str, filter_: str) -> dict:
return {
"service": "input_number.set_value",
"target": {"entity_id": f"input_number.{ziel}"},
"data": {"value": "{{ trigger.json." + schluessel + " | default(0) | " + filter_ + " }}"},
"continue_on_error": True,
}
return [
text("trading_phase", "phase", "unbekannt"),
text("trading_modus", "modus", ""),
text("trading_killswitch", "killswitch", "unbekannt"),
zahl("trading_position", "position", "int(0)"),
zahl("trading_tagesergebnis_usd", "tagesergebnis_usd", "float(0) | round(2)"),
zahl("trading_herzschlag_alter_s", "herzschlag_alter_s", "int(-1)"),
{
"service": "input_boolean.turn_{{ 'on' if (trigger.json.veraltet | default(false)) else 'off' }}",
"target": {"entity_id": "input_boolean.trading_veraltet"},
"continue_on_error": True,
},
{
"service": "input_datetime.set_datetime",
"target": {"entity_id": "input_datetime.trading_gesendet"},
"data": {"timestamp": "{{ now().timestamp() }}"},
"continue_on_error": True,
},
]
def lies_abschnitt(datei: Path) -> dict | None:
return tomllib.loads(datei.read_text(encoding="utf-8")).get("homeassistant")
def trage_ein(datei: Path, adresse: str) -> None:
roh = datei.read_text(encoding="utf-8")
vorher = tomllib.loads(roh)
block = (
"\n[homeassistant]\n"
"# Webhook in Home Assistant fuer die Kennzahlen des Paper-Runners. Die ID in der Adresse ist das\n"
"# Geheimnis: Wer sie kennt, kann die Anzeige in HA faelschen. Eingeschaltet wird mit aktiv = true.\n"
f'webhook_url = "{adresse}"\n'
"aktiv = false\n"
)
neu = roh.rstrip("\n") + "\n" + block
nachher = tomllib.loads(neu)
if {k: v for k, v in nachher.items() if k != "homeassistant"} != vorher:
raise SystemExit("ABBRUCH: bestehende Abschnitte haetten sich geaendert")
nachbar = datei.with_name(datei.name + ".neu")
with open(nachbar, "w", encoding="utf-8", newline="\n") as strom:
strom.write(neu)
os.replace(nachbar, datei)
def helfer_liste(ha) -> dict[str, list[dict]]:
ergebnis = {}
for art in ("input_text", "input_number", "input_boolean", "input_datetime"):
eintraege = ha.befehl_ok(f"{art}/list") or []
ergebnis[art] = [e for e in eintraege if str(e.get("id", "")).startswith(PRAEFIX)]
return ergebnis
def main() -> int:
aktion, ha_projekt, trading = sys.argv[1], Path(sys.argv[2]), Path(sys.argv[3])
sys.path.insert(0, str(ha_projekt / "src"))
import ha_verbindung as hv
geheimnisse = trading / "config" / "secrets.toml"
token = hv.lies_token()
print(f"Token gelesen (Laenge {len(token)}), Instanz aus dem HA-Projekt, Aktion {aktion}")
# nur_lesen=False auch fuers Pruefen: Die Leseliste jenes Moduls kennt die Listenbefehle der
# Helfer und get_states nicht. 'pruefen' und 'probe' senden trotzdem nur lesende Befehle.
with hv.HaVerbindung(hv.BASIS_URL, token, nur_lesen=False) as ha:
vorhanden = helfer_liste(ha)
zahl_helfer = sum(len(v) for v in vorhanden.values())
automationen = [
s for s in (ha.befehl_ok("get_states") or [])
if s.get("entity_id", "").startswith("automation.") and s.get("attributes", {}).get("id") == AUTOMATION_ID
]
abschnitt = lies_abschnitt(geheimnisse)
print(f"Helfer mit Praefix {PRAEFIX}: {zahl_helfer} -- " + ", ".join(f"{a}: {[e['id'] for e in v]}" for a, v in vorhanden.items() if v))
print(f"Automation {AUTOMATION_ID}: {'vorhanden' if automationen else 'fehlt'}")
print("[homeassistant] in der Geheimnisdatei: " + ("fehlt" if abschnitt is None else str({k: (type(v).__name__, len(v) if isinstance(v, str) else v) for k, v in abschnitt.items()})))
if aktion == "pruefen":
return 0
if aktion == "lesen":
# Nur lesen: die Zustaende der Helfer, wie sie gerade in Home Assistant stehen.
zustaende = {s["entity_id"]: s["state"] for s in (ha.befehl_ok("get_states") or []) if f".{PRAEFIX}" in s.get("entity_id", "")}
for name in sorted(zustaende):
print(f" {name} = {zustaende[name]}")
return 0
if aktion == "einrichten":
if abschnitt is not None or automationen or zahl_helfer:
print("ABBRUCH: Es gibt schon Teile der Einrichtung; erst 'entfernen' oder von Hand klaeren.")
return 3
for name in TEXTE:
ha.befehl_ok("input_text/create", name=name, min=0, max=100, mode="text", icon="mdi:chart-line")
for name, kleinst, groesst, schritt, einheit in ZAHLEN:
ha.befehl_ok("input_number/create", name=name, min=kleinst, max=groesst, step=schritt, mode="box", unit_of_measurement=einheit, icon="mdi:chart-line")
for name in SCHALTER:
ha.befehl_ok("input_boolean/create", name=name, icon="mdi:alert-outline")
for name in ZEITEN:
ha.befehl_ok("input_datetime/create", name=name, has_date=True, has_time=True, icon="mdi:clock-outline")
webhook_id = secrets.token_hex(32)
status, antwort = ha.http_post(
f"/api/config/automation/config/{AUTOMATION_ID}",
{
"alias": ALIAS,
"description": "Uebernimmt Phase, Killswitch, Position und Tagesergebnis des Paper-Runners aus dem Webhook der Trading Plattform. Angelegt am 2026-09-20 ueber ha_einrichten.py; Rueckbau mit der Aktion entfernen.",
"trigger": [{"platform": "webhook", "webhook_id": webhook_id, "allowed_methods": ["POST"], "local_only": True}],
"condition": [],
"action": aktionen(),
"mode": "queued",
"max": 5,
},
)
print(f"Automation angelegt: HTTP {status}")
if status != 200:
print("ABBRUCH: Die Automation wurde nicht angenommen:", str(antwort)[:200].replace(webhook_id, "***"))
return 2
trage_ein(geheimnisse, f"{hv.BASIS_URL}/api/webhook/{webhook_id}")
print("Adresse eingetragen ([homeassistant], aktiv = false); die ID wurde nicht ausgegeben.")
return 0
if aktion == "probe":
if abschnitt is None:
print("ABBRUCH: kein [homeassistant] in der Geheimnisdatei")
return 3
nutzlast = {
"phase": "probe_der_einrichtung", "modus": "probe", "killswitch": "angehalten", "position": 7,
"tagesergebnis_usd": -12.34, "herzschlag_alter_s": 5, "veraltet": False,
"gesendet": time.strftime("%Y-%m-%dT%H:%M:%S"),
}
anfrage = urllib.request.Request(
abschnitt["webhook_url"], data=json.dumps(nutzlast).encode("utf-8"),
headers={"Content-Type": "application/json"}, method="POST",
)
try:
with urllib.request.urlopen(anfrage, timeout=5) as antwort:
print(f"Webhook: HTTP {antwort.status}")
except Exception as ausnahme:
# Nur der Typ: Die Ausnahme traegt die Adresse und damit die ID.
print(f"Webhook GESCHEITERT: {type(ausnahme).__name__}")
return 2
time.sleep(2.0)
zustaende = {s["entity_id"]: s["state"] for s in (ha.befehl_ok("get_states") or []) if f".{PRAEFIX}" in s.get("entity_id", "")}
for name in sorted(zustaende):
print(f" {name} = {zustaende[name]}")
erwartet = {
"input_text.trading_phase": "probe_der_einrichtung", "input_text.trading_killswitch": "angehalten",
"input_number.trading_position": "7.0", "input_number.trading_tagesergebnis_usd": "-12.34",
"input_boolean.trading_veraltet": "off",
}
falsch = {k: (v, zustaende.get(k)) for k, v in erwartet.items() if zustaende.get(k) != v}
print("Rueckgelesen wie gesendet." if not falsch else f"BEFUND: {falsch}")
return 0 if not falsch else 1
if aktion == "entfernen":
status = ha.http_delete(f"/api/config/automation/config/{AUTOMATION_ID}")
print(f"Automation geloescht: HTTP {status}")
for art, eintraege in vorhanden.items():
for eintrag in eintraege:
ha.befehl_ok(f"{art}/delete", **{f"{art}_id": eintrag["id"]})
print(f" geloescht: {art}.{eintrag['id']}")
if abschnitt is not None:
print("Hinweis: [homeassistant] steht weiter in der Geheimnisdatei des Trading-Projekts; von Hand entfernen.")
return 0
print("unbekannte Aktion")
return 2
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,48 @@
"""Holt die echten IEX-Minutenkerzen eines Handelstags und legt sie als JSON ab. Nur lesend.
Koordinator-Werkzeug, nicht Teil des Repos. Der einzige Netzzugriff ist der lesende Kerzenabruf
bei Alpaca; ausgegeben werden Anzahl und Eckkurse, nie ein Schluessel. Die Datei ist die Eingabe
fuer neustart_echtlauf.py. Entstanden in der Abschlusspruefung zu Paket F (2026-09-20).
Aufruf (WSL, aus dem Projektordner): python hole_kerzen.py JJJJ-MM-TT <ziel.json>
"""
import json
import sys
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path.cwd()))
from trading.paper.config import lade_paper_config
from trading.paper.kerzen import HANDELSZONE
from trading.paper.kerzen import AlpacaKerzenquelle
from trading.paper.kerzen import handelsende
def main() -> int:
datum, ziel = sys.argv[1], Path(sys.argv[2])
config = lade_paper_config("config/paper.toml")
mitternacht = datetime.strptime(datum, "%Y-%m-%d").replace(tzinfo=HANDELSZONE)
quelle = AlpacaKerzenquelle.from_secrets("config/secrets.toml", feed=config.feed, timeout=20.0)
kerzen = quelle.kerzen_des_tages(config.instrument_id, handelsende(mitternacht))
ziel.write_text(
json.dumps(
[
{"zeit": k.zeit.isoformat(), "open": k.open, "high": k.high,
"low": k.low, "close": k.close, "volume": k.volume}
for k in kerzen
]
),
encoding="utf-8",
)
print(f"{datum}: {len(kerzen)} Kerzen in {config.instrument_id} nach {ziel}")
if kerzen:
kurse = [k.close for k in kerzen]
print(f" erste {kerzen[0].zeit.isoformat()} Schluss {kurse[0]}, "
f"letzte {kerzen[-1].zeit.isoformat()} Schluss {kurse[-1]}, "
f"Tief {min(kurse)}, Hoch {max(kurse)}")
return 0 if kerzen else 2
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,197 @@
"""Eigener Lauf mit echten Daten: Der Tagesverlust zaehlt nach einem Neustart weiter ab dem Tagesstart.
Koordinator-Pruefcode, nicht Teil des Repos. Echte IEX-Kerzen eines Tages (JSON aus dem lesenden
Abruf der Abschlusspruefung). Aus den Rohkerzen wird vorgerechnet, bei welcher Kerze die Grenze von
0,2 Prozent gegen den Tagesstart reisst und bei welcher gegen den Neustartkurs; dann laufen zwei
echte PaperSitzungen nacheinander (vor und nach dem Neustart) mit gemeinsamer Tagesstart-Datei,
dazu die Gegenprobe ohne Datei. Kein Netz: Meldungen gehen an einen mitschreibenden Transport.
Aufruf (WSL, aus dem Projektordner): python neustart_echtlauf.py <kerzen.json>
"""
import json
import sys
import tempfile
from datetime import datetime
from datetime import timedelta
from pathlib import Path
sys.path.insert(0, str(Path.cwd()))
from trading.killswitch.killswitch import lade_killswitch_config
from trading.paper.config import enge_limits
from trading.paper.config import lade_paper_config
from trading.paper.kerzen import FesteKerzenquelle
from trading.paper.kerzen import Kerze
from trading.paper.melder import Melder
from trading.paper.melder import MelderConfig
from trading.paper.probe import ProbeBroker
from trading.paper.sitzung import PaperSitzung
from trading.paper.status import lies_tagesstart
from trading.paper.uhr import ProbeUhr
from trading.registry.registry import Registry
from trading.risk.limits import lade_limits
STUECK = 20
ZIEL_KAPITAL = 12_676.20 # so klein, dass 20 Stueck die echte Grenze von 0,2 Prozent erreichen
class Protokoll:
def __init__(self):
self.zeilen = []
def schreibe(self, eintrag):
self.zeilen.append(dict(eintrag))
class Antwort:
status_code = 200
class Transport:
def __init__(self):
self.aufrufe = []
def __call__(self, url, *, data=None, headers=None, timeout=None):
self.aufrufe.append(headers["Title"])
return Antwort()
class Flach:
def predict(self, beobachtung, deterministic=False):
return 2, None
def lade(pfad):
roh = json.loads(Path(pfad).read_text(encoding="utf-8"))
return [
Kerze(zeit=datetime.fromisoformat(e["zeit"]), open=e["open"], high=e["high"],
low=e["low"], close=e["close"], volume=e["volume"])
for e in roh
]
def lauf(ordner, kerzen, *, beginn, position, cash, tagesstart, config, limits, ks_config):
ordner.mkdir(parents=True, exist_ok=True)
quelle = FesteKerzenquelle(kerzen)
uhr = ProbeUhr(beginn)
broker = ProbeBroker(kerzenquelle=quelle, uhr=uhr, instrument_id=config.instrument_id,
startkapital=cash, position=position)
registry = Registry(db_pfad=ordner / "registry.db", artefakt_wurzel=ordner / "artifacts")
registry.setze_killswitch_zustand("angehalten", ausloeser="neustart")
protokoll = Protokoll()
transport = Transport()
sitzung = PaperSitzung(
broker=broker, kerzenquelle=quelle, agent=Flach(), agent_beschreibung={"quelle": "echtlauf"},
registry=registry, limits=limits, killswitch_config=ks_config, config=config, uhr=uhr,
protokoll=protokoll, protokoll_pfad="echtlauf.jsonl", status_pfad=ordner / "status.json",
modus="probe", soll_abbrechen=lambda: False,
melder=Melder(config=MelderConfig(aktiv=True, ntfy_url="https://ntfy.sh",
thema="echtlauf-geht-nie-ins-netz", zeitlimit_s=3.0,
stille_nach_s=180.0), token="", transport=transport),
tagesstart_pfad=tagesstart,
)
code = sitzung.laufe()
return code, protokoll.zeilen, transport.aufrufe, broker, registry
def kerze_beim_grenzereignis(zeilen, index_nach_zeit):
"""Index der juengsten Kerze in dem Takt, in dem die Zeile grenzwert steht, oder None."""
for stelle, zeile in enumerate(zeilen):
if zeile.get("was") == "grenzwert":
takt = next(z for z in zeilen[stelle:] if z.get("art") == "takt")
return index_nach_zeit[takt["letzte_kerze"]], takt["schritt"]
return None, None
def erster_riss(schluesse, *, ab, cash, position, bezug, anteil):
for c in range(ab, len(schluesse)):
if (cash + position * schluesse[c] - bezug) / bezug < -anteil:
return c
return None
def main():
kerzen = lade(sys.argv[1])
schluesse = [k.close for k in kerzen]
index_nach_zeit = {k.zeit.isoformat(): i for i, k in enumerate(kerzen)}
config = lade_paper_config("config/paper.toml")
limits = enge_limits(lade_limits("config/risk.toml"), config)
ks_config = lade_killswitch_config("config/killswitch.toml")
anteil = limits.max_tagesverlust_anteil
tag = kerzen[0].zeit.date().isoformat()
print(f"{tag}: {len(kerzen)} Kerzen, Schluss Kerze 0 {schluesse[0]:.2f}, "
f"Tief {min(schluesse):.2f}, Hoch {max(schluesse):.2f}, Grenze {anteil}")
befunde = 0
for seite, position in (("long", STUECK), ("short", -STUECK)):
cash = ZIEL_KAPITAL - position * schluesse[0]
tagesstart_wert = cash + position * schluesse[0]
riss_tag = erster_riss(schluesse, ab=0, cash=cash, position=position,
bezug=tagesstart_wert, anteil=anteil)
if riss_tag is None or riss_tag < 3:
print(f"[{seite}] der Tag reisst die Grenze gegen den Tagesstart nicht (oder sofort): uebersprungen")
continue
# Neustart so, dass die Sitzung davor die Kerze des Risses nicht mehr sieht: Sie laeuft
# ueber kerzen[:r] und sieht die Kerzen 0 bis r-2. Die Sitzung danach beginnt in der
# Minute der Kerze r und sieht zuerst die Kerze r-1.
r = riss_tag
bezug_neu = cash + position * schluesse[r - 1]
riss_neu = erster_riss(schluesse, ab=r - 1, cash=cash, position=position,
bezug=bezug_neu, anteil=anteil)
print(f"[{seite}] Tagesstart {tagesstart_wert:.2f}; vorgerechnet: Riss gegen den Tagesstart "
f"bei Kerze {riss_tag}, gegen den Neustartkurs ({bezug_neu:.2f}) bei Kerze {riss_neu}")
with tempfile.TemporaryDirectory(prefix="neustart-echtlauf-") as wurzel:
wurzel = Path(wurzel)
datei = wurzel / "paper" / "tagesstart.json"
gemeinsam = dict(position=position, cash=cash, config=config, limits=limits,
ks_config=ks_config)
code1, zeilen1, _, _, reg1 = lauf(
wurzel / "vorher", kerzen[:r], beginn=kerzen[0].zeit - timedelta(minutes=1),
tagesstart=datei, **gemeinsam)
riss1, _ = kerze_beim_grenzereignis(zeilen1, index_nach_zeit)
gemerkt = lies_tagesstart(datei, handelstag=tag)
print(f"[{seite}] vor dem Neustart: Exit {code1}, Killswitch "
f"{reg1.killswitch_zustand().zustand}, Riss {riss1}, gemerkt {gemerkt}")
code2, zeilen2, titel2, broker2, reg2 = lauf(
wurzel / "nachher", kerzen, beginn=kerzen[r].zeit - timedelta(minutes=1),
tagesstart=datei, **gemeinsam)
riss2, schritt2 = kerze_beim_grenzereignis(zeilen2, index_nach_zeit)
takte2 = [z for z in zeilen2 if z.get("art") == "takt"]
# Vor dem Ende des with-Blocks lesen: Danach ist das Wegwerf-Register weg.
zustand2 = reg2.killswitch_zustand().zustand
position2 = sum(p.menge for p in broker2.positionen())
print(f"[{seite}] nach dem Neustart mit Datei: Startkapital {takte2[0]['startkapital']}, "
f"Riss bei Kerze {riss2} (Schritt {schritt2}), Exit {code2}, Killswitch "
f"{zustand2}, Position {position2}, Meldungen {titel2}")
code3, zeilen3, _, broker3, reg3 = lauf(
wurzel / "ohne", kerzen, beginn=kerzen[r].zeit - timedelta(minutes=1),
tagesstart=None, **gemeinsam)
riss3, _ = kerze_beim_grenzereignis(zeilen3, index_nach_zeit)
takte3 = [z for z in zeilen3 if z.get("art") == "takt"]
print(f"[{seite}] nach dem Neustart ohne Datei: Startkapital {takte3[0]['startkapital']}, "
f"Riss bei Kerze {riss3}, Exit {code3}, Killswitch {reg3.killswitch_zustand().zustand}")
ok = (
riss1 is None and code1 == 4
and gemerkt is not None and abs(gemerkt - tagesstart_wert) < 1e-9
and takte2[0]["startkapital"] == gemerkt
and riss2 == riss_tag
and zustand2 == "gesperrt"
and position2 == 0
and titel2 == ["Paper-Runner wartet", "Grenze verletzt", "Paper-Runner beendet"]
and abs(takte3[0]["startkapital"] - bezug_neu) < 1e-9
and riss3 == riss_neu
)
print(f"[{seite}] {'OK' if ok else 'BEFUND'}")
befunde += 0 if ok else 1
print(f"{befunde} Befund(e).")
return 2 if befunde else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,52 @@
"""Liest nur zurueck, was in der letzten Stunde bei ntfy unter dem Thema der Maschine ankam.
Koordinator-Skript, nicht Teil des Repos. Schickt nichts. Das Thema wird nie ausgegeben -- weder
hier noch in einer Fehlermeldung. Aufruf aus dem Projektordner (WSL): python ntfy_lesen.py
"""
import json
import sys
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path.cwd()))
import requests
from trading.paper.melder import lade_melder_config
from trading.paper.melder import lies_ntfy_geheimnisse
def main() -> int:
config = lade_melder_config("config/paper.toml", secrets_pfad="config/secrets.toml")
thema, token = lies_ntfy_geheimnisse("config/secrets.toml")
kopf = {"Authorization": f"Bearer {token}"} if token else {}
try:
antwort = requests.get(
f"{config.ntfy_url}/{thema}/json", params={"poll": "1", "since": "1h"},
headers=kopf, timeout=10, allow_redirects=False,
)
except Exception as ausnahme:
# Nur der Typ: Die Ausnahme von requests traegt die Adresse und damit das Thema.
print(f"Ruecklesen gescheitert: {type(ausnahme).__name__}")
return 1
print(f"Ruecklesen: HTTP {antwort.status_code}")
anzahl = 0
for zeile in antwort.text.splitlines():
try:
eintrag = json.loads(zeile)
except ValueError:
continue
if eintrag.get("event") != "message":
continue
anzahl += 1
# Ohne Angabe fuehrt ntfy die Prioritaet 3 (default) und laesst das Feld weg.
print(f" bei ntfy: Titel '{eintrag.get('title')}', Prioritaet {eintrag.get('priority', 3)}, "
f"Zeit {datetime.fromtimestamp(eintrag.get('time', 0)).isoformat()}, "
f"erste Zeile '{str(eintrag.get('message', '')).splitlines()[0][:70]}'")
print(f"{anzahl} Meldung(en) in der letzten Stunde.")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,184 @@
"""Eigener Lauf mit echten Daten: Tagesverlust-Schutz in der Pause auf den IEX-Kerzen eines echten Tages.
Koordinator-Pruefcode, nicht Teil des Repos. Er rechnet aus den Rohkerzen vor, in welchem Takt die
Grenze reissen muss, und haelt das gegen das Protokoll der echten PaperSitzung. Der einzige
Netzzugriff holt lesend die Kerzen; Meldungen gehen an einen mitschreibenden Transport, nie ins Netz.
Aufruf (aus dem Projektordner, in WSL): python pause_echtlauf.py JJJJ-MM-TT
"""
import sys
import tempfile
from datetime import datetime
from datetime import timedelta
from pathlib import Path
sys.path.insert(0, str(Path.cwd()))
from trading.killswitch.killswitch import lade_killswitch_config
from trading.paper.config import enge_limits
from trading.paper.config import lade_paper_config
from trading.paper.kerzen import HANDELSZONE
from trading.paper.kerzen import AlpacaKerzenquelle
from trading.paper.kerzen import FesteKerzenquelle
from trading.paper.kerzen import handelsende
from trading.paper.melder import Melder
from trading.paper.melder import MelderConfig
from trading.paper.probe import ProbeBroker
from trading.paper.sitzung import PaperSitzung
from trading.paper.uhr import ProbeUhr
from trading.registry.registry import Registry
from trading.risk.limits import lade_limits
from dataclasses import replace
class Protokoll:
def __init__(self):
self.zeilen = []
def schreibe(self, eintrag):
self.zeilen.append(dict(eintrag))
class Antwort:
status_code = 200
class Transport:
def __init__(self):
self.aufrufe = []
def __call__(self, url, *, data=None, headers=None, timeout=None):
self.aufrufe.append((headers["Title"], headers["Priority"], data.decode("utf-8")))
return Antwort()
class Flach:
def predict(self, beobachtung, deterministic=False):
return 2, None
def lauf(kerzen, *, config, limits, killswitch_config, zustand, position):
ordner = Path(tempfile.mkdtemp(prefix="pause-echtlauf-"))
quelle = FesteKerzenquelle(kerzen)
uhr = ProbeUhr(kerzen[0].zeit - timedelta(minutes=1))
broker = ProbeBroker(
kerzenquelle=quelle, uhr=uhr, instrument_id=config.instrument_id, position=position
)
registry = Registry(db_pfad=ordner / "registry.db", artefakt_wurzel=ordner / "artifacts")
registry.setze_killswitch_zustand(zustand, ausloeser="echtlauf")
protokoll = Protokoll()
transport = Transport()
melder = Melder(
config=MelderConfig(
aktiv=True,
ntfy_url="https://ntfy.sh",
thema="echtlauf-geht-nie-ins-netz",
zeitlimit_s=3.0,
stille_nach_s=180.0,
),
token="",
transport=transport,
)
sitzung = PaperSitzung(
broker=broker,
kerzenquelle=quelle,
agent=Flach(),
agent_beschreibung={"quelle": "echtlauf"},
registry=registry,
limits=limits,
killswitch_config=killswitch_config,
config=config,
uhr=uhr,
protokoll=protokoll,
protokoll_pfad="echtlauf.jsonl",
status_pfad=ordner / "status.json",
modus="probe",
soll_abbrechen=lambda: False,
melder=melder,
)
code = sitzung.laufe()
return code, protokoll.zeilen, transport.aufrufe, broker, registry
def main():
datum = sys.argv[1]
mitternacht = datetime.strptime(datum, "%Y-%m-%d").replace(tzinfo=HANDELSZONE)
config = lade_paper_config("config/paper.toml")
killswitch_config = lade_killswitch_config("config/killswitch.toml")
quelle = AlpacaKerzenquelle.from_secrets("config/secrets.toml", feed=config.feed, timeout=10.0)
kerzen = list(quelle.kerzen_des_tages(config.instrument_id, handelsende(mitternacht)))
print(f"{datum}: {len(kerzen)} IEX-Kerzen in {config.instrument_id}")
schluesse = [k.close for k in kerzen]
print(f"Schluss erste Kerze {schluesse[0]:.2f}, Tief {min(schluesse):.2f}, Hoch {max(schluesse):.2f}")
stueck = 20
cash = 100_000.0
befunde = 0
for seite, position in (("long", stueck), ("short", -stueck)):
# Eigene Rechnung aus den Rohkerzen: Takt k (schritt k) sieht die Kerze k-1 als juengste
# abgeschlossene. Startkapital ist der Kontowert im ersten Takt.
start = cash + position * schluesse[0]
schlechtester = min(cash + position * s for s in schluesse) - start
if schlechtester >= 0:
print(f"[{seite}] der Tag bringt dieser Seite keinen Verlust gegen den ersten Takt; uebersprungen")
continue
# Die Grenze liegt bei der Haelfte des groessten Verlusts dieses Tages: Der Tag reisst sie
# wirklich, und nicht im ersten Takt.
anteil = abs(schlechtester) / start / 2.0
erwartet = None
for k, s in enumerate(schluesse, start=1):
if (cash + position * s - start) / start < -anteil:
erwartet = k
break
limits = replace(enge_limits(lade_limits("config/risk.toml"), config), max_tagesverlust_anteil=anteil)
code, zeilen, meldungen, broker, registry = lauf(
kerzen, config=config, limits=limits, killswitch_config=killswitch_config,
zustand="angehalten", position=position,
)
takte = [z for z in zeilen if z.get("art") == "takt"]
grenz = [z for z in zeilen if z.get("was") == "grenzwert"]
# Der Takt, in dem die Zeile grenzwert steht: die Taktzeile danach traegt seinen Schritt.
stelle = zeilen.index(grenz[0]) if grenz else None
schritt = next((z["schritt"] for z in zeilen[stelle:] if z.get("art") == "takt"), None) if grenz else None
titel = [m[0] for m in meldungen]
print(f"[{seite}] Startkapital {start:.2f}, Grenze {anteil:.6f} (= {anteil * start:.2f} USD), "
f"erwarteter Takt {erwartet}, gemessen {schritt}")
print(f"[{seite}] Zustand {registry.killswitch_zustand().zustand}, Position danach "
f"{sum(p.menge for p in broker.positionen())}, Exit {code}, Meldungen {titel}")
print(f"[{seite}] schliesse_alle: {broker.aufrufe.count('schliesse_alle')}, Takte {len(takte)}, "
f"grenzwert-Zeilen {len(grenz)}")
ok = (
schritt == erwartet
and registry.killswitch_zustand().zustand == "gesperrt"
and broker.aufrufe.count("schliesse_alle") == 1
and len(grenz) == 1
and titel == ["Paper-Runner wartet", "Grenze verletzt", "Paper-Runner beendet"]
and code == 0
)
print(f"[{seite}] {'OK' if ok else 'BEFUND'}")
befunde += 0 if ok else 1
# Gegenprobe: dieselben Kerzen, angehalten, ohne Position -- genau eine Kontoabfrage, nichts geschlossen.
limits = enge_limits(lade_limits("config/risk.toml"), config)
code, zeilen, meldungen, broker, registry = lauf(
kerzen, config=config, limits=limits, killswitch_config=killswitch_config,
zustand="angehalten", position=0,
)
titel = [m[0] for m in meldungen]
ok = (
broker.aufrufe.count("konto") == 1
and "schliesse_alle" not in broker.aufrufe
and titel == ["Paper-Runner wartet", "Paper-Runner beendet"]
and code == 0
)
print(f"[ohne Position] konto {broker.aufrufe.count('konto')}, Meldungen {titel}, Exit {code}: "
f"{'OK' if ok else 'BEFUND'}")
befunde += 0 if ok else 1
# Gegenprobe 2: aktiv mit dem echten Haltemuster aendert sich nicht -- das prueft die Probe des Skripts.
print(f"{befunde} Befund(e).")
return 2 if befunde else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,53 @@
# Wiederanlauf-Probe: beendet die Distribution HART (wsl --terminate) und misst, wann Wachhalter,
# Distribution und Dashboard von selbst zurueck sind. Ein laufender Paper-Runner stirbt dabei mit --
# deshalb nur ausserhalb der Handelszeit (15:30 bis 22:00 Uhr Berliner Zeit) und nur mit -Ausfuehren.
# Waehrend der Messung keinen anderen wsl.exe-Aufruf absetzen: Jeder Klient weckt die Distribution
# selbst und verfaelscht das Ergebnis. Die Datei ist reines ASCII (PowerShell 5.1 liest ohne BOM ANSI).
# Gemessen am 2026-09-20: terminate 19:30:17, Aufgabe und Distribution Running um 19:35:06, HTTP 200 um 19:35:13.
param(
[string]$Name = "Trading Plattform - WSL wachhalten",
[int]$MaxSekunden = 480,
[switch]$Ausfuehren
)
if (-not $Ausfuehren) {
"Nichts getan. Diese Probe beendet die WSL-Distribution hart; ein laufender Runner stirbt mit."
"Aufruf ausserhalb der Handelszeit: powershell -File wiederanlauf_probe.ps1 -Ausfuehren"
exit 2
}
$wsl = "$env:SystemRoot\System32\wsl.exe"
function Distro {
$roh = ((& $wsl -l -v) | Out-String).Replace([string][char]0, '')
foreach ($zeile in ($roh -split "\r?\n")) {
if ($zeile -match 'Debian\s+(\w+)') { return $Matches[1] }
}
return "unbekannt"
}
function Probe {
try {
$r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 3 http://127.0.0.1:8144/api/gesund
return "HTTP " + $r.StatusCode
} catch {
return "tot"
}
}
"Vorher: Aufgabe " + (Get-ScheduledTask -TaskName $Name).State + ", Distro " + (Distro) + ", Dashboard " + (Probe)
& $wsl --terminate Debian | Out-Null
$start = Get-Date
"terminate um " + $start.ToString("HH:mm:ss")
$letzter = ""
while (((Get-Date) - $start).TotalSeconds -lt $MaxSekunden) {
$a = (Get-ScheduledTask -TaskName $Name).State
$d = Distro
$p = Probe
$s = [int]((Get-Date) - $start).TotalSeconds
$jetzt = "Aufgabe $a / Distro $d / Dashboard $p"
if ($jetzt -ne $letzter) { "+${s}s (" + (Get-Date).ToString("HH:mm:ss") + "): $jetzt"; $letzter = $jetzt }
if ($s -gt 15 -and $a -eq "Running" -and $d -eq "Running" -and $p -eq "HTTP 200") { break }
Start-Sleep -Seconds 5
}
"Ende der Messung um " + (Get-Date).ToString("HH:mm:ss")