diff --git a/.vault-tools/trading-plattform-pruefung/LIESMICH.md b/.vault-tools/trading-plattform-pruefung/LIESMICH.md index baded44..7dcc42f 100644 --- a/.vault-tools/trading-plattform-pruefung/LIESMICH.md +++ b/.vault-tools/trading-plattform-pruefung/LIESMICH.md @@ -49,7 +49,7 @@ Koordinator-Werkzeuge, nicht fürs Repo. Sie starten **nie** den echten Broker: | `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 ` | | `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 ` | -| `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` | +| `ntfy_lesen.py` | liest über die Abfrage-Schnittstelle von ntfy zurück, was seit `--seit` unter dem Thema der Maschine ankam (Vorgabe `12h`; auch Berliner Ortszeit `JJJJ-MM-TTTHH:MM`) — Zeit in Berliner Zeit, Priorität, Titel und erste Zeile, darin jede Ziffernfolge als `#`. **Schickt nichts und gibt das Thema nie aus**, auch nicht in einer Fehlermeldung. Setzt `TRADING_GEHEIMNISSE` selbst, wenn die Variable fehlt. Exit 3 ohne Thema oder Geheimnisdatei, Exit 4 bei Weiterleitung oder Fehlerstatus (einer Weiterleitung folgt es nie, sonst ginge das Token mit). ntfy.sh hält Meldungen nur Stunden: Am 2026-10-08 um 14 Uhr war die Meldung von 22:36 am Vortag schon weg | aus dem Projektordner oder einem Arbeitsbaum in WSL: `python -B ntfy_lesen.py --seit 12h` | | `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 diff --git a/.vault-tools/trading-plattform-pruefung/ntfy_lesen.py b/.vault-tools/trading-plattform-pruefung/ntfy_lesen.py index 3e8242a..881bcdb 100644 --- a/.vault-tools/trading-plattform-pruefung/ntfy_lesen.py +++ b/.vault-tools/trading-plattform-pruefung/ntfy_lesen.py @@ -1,36 +1,90 @@ -"""Liest nur zurueck, was in der letzten Stunde bei ntfy unter dem Thema der Maschine ankam. +"""Liest zurueck, was bei ntfy unter dem Thema der Maschine ankam, mit maskierten Ziffern. 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 +hier noch in einer Fehlermeldung. Aufruf aus dem Projektordner (WSL): +python -B ntfy_lesen.py [--seit 12h | --seit 2026-10-07T20:00] +Exit 0 gelesen, 1 Netzfehler, 3 kein Thema, 4 Weiterleitung oder Fehlerstatus. """ +import argparse import json +import os +import re import sys from datetime import datetime from pathlib import Path +from zoneinfo import ZoneInfo sys.path.insert(0, str(Path.cwd())) +# Ohne Login-Shell fehlt die Variable (LIESMICH, Abschnitt zu den Geheimnissen); der Pfad selbst +# ist kein Geheimnis, sein Inhalt schon. +os.environ.setdefault("TRADING_GEHEIMNISSE", "/home/claude/.config/trading-plattform/secrets.toml") + import requests +from trading import geheimnisse from trading.paper.melder import lade_melder_config from trading.paper.melder import lies_ntfy_geheimnisse +BERLIN = ZoneInfo("Europe/Berlin") +# ntfy nimmt fuer since eine Dauer (12h, 30m) oder einen Unix-Zeitstempel (docs.ntfy.sh/subscribe/api). +DAUER = re.compile(r"^\d+[smhd]$") -def main() -> int: - config = lade_melder_config("config/paper.toml", secrets_pfad=None) - thema, token = lies_ntfy_geheimnisse() + +def seit_parameter(wert: str) -> str: + """Eine Dauer bleibt, wie sie ist; eine Ortszeit ohne Zone gilt als Berliner Zeit.""" + if DAUER.match(wert): + return wert + zeit = datetime.fromisoformat(wert) + if zeit.tzinfo is None: + zeit = zeit.replace(tzinfo=BERLIN) + return str(int(zeit.timestamp())) + + +def maskiert(text: object) -> str: + """Jede Ziffernfolge als #, weil eine Meldung einen Betrag tragen kann.""" + return re.sub(r"\d+", "#", str(text)) + + +def main(argv: list[str] | None = None) -> int: + teiler = argparse.ArgumentParser(description="ntfy zuruecklesen, ohne das Thema zu zeigen.") + teiler.add_argument( + "--seit", default="12h", + help="Dauer wie 12h oder Berliner Ortszeit JJJJ-MM-TTTHH:MM; ntfy.sh haelt Meldungen nur Stunden.", + ) + args = teiler.parse_args(argv) + try: + seit = seit_parameter(args.seit) + except ValueError: + print(f"--seit nicht lesbar: {args.seit}") + return 2 + try: + # Beide lesen die Geheimnisdatei; fehlt sie, wirft schon die Konfiguration. + config = lade_melder_config("config/paper.toml", secrets_pfad=None) + thema, token = lies_ntfy_geheimnisse() + except OSError as ausnahme: + print(f"Geheimnisdatei nicht lesbar ({type(ausnahme).__name__}): {geheimnisse.beschreibung()}") + return 3 + if not thema: + # Mit leerem Thema hiesse die Adresse .../json; genau das endete am 2026-10-07 mit HTTP 307. + print(f"Kein Thema in der Geheimnisdatei: {geheimnisse.beschreibung()}") + return 3 kopf = {"Authorization": f"Bearer {token}"} if token else {} try: antwort = requests.get( - f"{config.ntfy_url}/{thema}/json", params={"poll": "1", "since": "1h"}, + f"{config.ntfy_url}/{thema}/json", params={"poll": "1", "since": seit}, 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}") + print(f"Ruecklesen seit {args.seit}: HTTP {antwort.status_code}") + if antwort.status_code >= 300: + # Einer Weiterleitung folgen hiesse, das Token an ein anderes Ziel zu schicken. + print("Kein Ergebnis: Weiterleitung oder Fehlerstatus, nicht gefolgt.") + return 4 anzahl = 0 for zeile in antwort.text.splitlines(): try: @@ -40,11 +94,12 @@ def main() -> int: if eintrag.get("event") != "message": continue anzahl += 1 + zeit = datetime.fromtimestamp(eintrag.get("time", 0), BERLIN).strftime("%Y-%m-%d %H:%M:%S") + erste = (str(eintrag.get("message", "")).splitlines() or [""])[0] # 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.") + print(f" {zeit} Prioritaet {eintrag.get('priority', 3)} Titel '{maskiert(eintrag.get('title'))}' " + f"erste Zeile '{maskiert(erste)[:70]}'") + print(f"{anzahl} Meldung(en).") return 0