Compare commits
9
Commits
127ee2041c
...
f73f84e3ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f73f84e3ac | ||
|
|
8b53bf7a74 | ||
|
|
ef8059903c | ||
|
|
88ec4fd40f | ||
|
|
02ff0e878d | ||
|
|
639c76bddc | ||
|
|
3e55bb915d | ||
|
|
c5db92c6de | ||
|
|
77de5059e4 |
No files matched your search
@@ -23,6 +23,7 @@ Du denkst, du tippst nicht. Dein Ergebnis ist ein Plan, den ein Executer ohne R
|
||||
- `rot_bei` nennt parametrisierte Tests mit voller Kennung, und zwar nur die Fälle, die der Mutant umkippt. Bei B1 (2026-09-27) wären unter `return False` drei von fünf Fällen grün geblieben.
|
||||
- `aus` und `wird` stehen in `'''…'''`, damit Backslashes wörtlich bleiben.
|
||||
- **Keine Platzhalter.** Kein „analog zu Task 2", kein „hier noch ergänzen", kein Pseudocode. Was im Plan steht, ist tippfertig.
|
||||
- **Blöcke in der Form, die `plan_materialisiere.py` anwendet.** Die Marke steht als letzte nicht leere Zeile vor dem Zaun, ohne Formatierung: `Aus:`, `wird:`, `Die ganze Datei ist neu:` oder `Häng diesen Block ans Ende:`. Die Überschrift (`##` bis `####`) über dem Block endet mit dem Pfad der Zieldatei in Backticks; ein Pfad ohne Schrägstrich und ohne Endung `.py`, `.md`, `.toml`, `.json`, `.ts` oder `.css` steht als `./name` (etwa `./versions.env`). Eine fette Marke oder eine Überschrift, deren letzter Pfad nicht das Ziel ist, übergeht das Werkzeug ohne Fehler. Am 2026-10-11 im Edge-Stack betraf das 8 von 11 Taskdateien, die Plankritik hätte ohne Kopie gearbeitet.
|
||||
- **Nachbesserung nach einer ausgeführten Welle schreibst du als neue Taskdateien, nicht als Neufassung.** Du hast kein Edit-Werkzeug und schreibst jede überarbeitete Datei komplett neu; am 2026-09-23 im Nexhorizons Hub waren dabei 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Deshalb: neue `task-NN-nachbesserung-<thema>.md` mit `Aus:`/`wird:`-Blöcken gegen den Baum, bestehende Taskdateien bleiben byte-gleich. Was ältere Tasks an den neuen Stand anpassen muss, schreibst du als Folgenliste `nachbesserung-<welle>-folgen.md` im selben `Aus:`/`wird:`-Format; eingesetzt wird sie vom Koordinator oder einem Executer. Für Korrekturen vor der ersten Ausführung gilt das nicht, außer der Auftrag verlangt es.
|
||||
- **Offene Entscheidungen benennen, nicht überspielen.** Wo der Auftrag mehrdeutig ist: Annahme treffen, im Abschnitt „Getroffene Annahmen" explizit nennen. Nur abbrechen, wenn jede Annahme den Plan unbrauchbar machen würde.
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ Nachbesserung vom 2026-10-09 (Paket RB3 der Trading Plattform, Plan `docs/plans/
|
||||
- Lauf mit echten Daten: die 73 Mutanten des Pakets RB3 (Kopie `rb3d-kopie`) durch das neue Skript, alle `OK`, T1 und T12 im Namensraum.
|
||||
- Ein `pruefer` fand keinen blockierenden Befund. Seine Lücken (kein Fall für den Fehlerzweig der Probe und den Zweig nach `||` im Hauptaufruf, Kopfkommentar zu weit gefasst, der Satz zur Reihenfolge oben) behebt Nachbesserung 1. Rohdaten: `AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/`.
|
||||
|
||||
Seit 2026-10-11 schreibt `plan_materialisiere.py` nur noch innerhalb der Kopie. Vorher schlug ein absoluter Pfad in einer Überschrift unter Windows die Kopie (`kopie / "D:/…"` ist der absolute Pfad): Das Skript ersetzte dann die echte Datei, mit Exit 0 und „0 Befund(e)“. Gemessen an einer Probe-Datei im Scratchpad. Entdeckt beim Planen von Paket E9 der Trading Plattform, dessen Tasks 725 und 726 Dateien unter `D:/Claude/.vault-tools/` ändern. Jetzt ist ein Ziel außerhalb der Kopie (absolut oder mit `..`) ein Befund mit Exit 2, und nichts wird geschrieben. Belegt am selben Tag mit drei Fällen (Aus/wird absolut, ganze Datei absolut, `../`): je Befund, die Dateien draußen unverändert; und mit einer Gegenprobe: Die Tasks 723 und 724 von E9 ergeben in einer frischen Kopie dieselben Zählungen und byte-gleiche Dateien. Pläne für Dateien außerhalb des Projekts nennen sie deshalb relativ zu einem Wurzelordner (etwa `.vault-tools/trading-plattform-pruefung/abendkontrolle.py` gegen eine Kopie, die diesen Ordner enthält).
|
||||
|
||||
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-24 hat `rot_nachweis.py` die Schalter `--venv`, `--frontend` und `--projektname`; vorher waren `~/venvs/trading`, `dashboard` und `trading-plattform/` fest verdrahtet. Ein Hub-Lauf im Trading-Venv wäre für die Tasks mit `argon2-cffi` und `pyotp` rot aus einem fehlenden Paket gewesen, nicht aus der fehlenden Umsetzung. Fehlt das Venv in WSL, steht beim Task jetzt `VORAUSSETZUNG FEHLT` statt `ROT`, und die Schlusszeile zählt diese Läufe getrennt. Belegt am selben Tag: Der Vorgabe-Befehl ist byte-gleich mit der alten Fassung, falsche Schalterwerte enden mit Exit 3, und der erste echte Lauf über 38 Tasks des Hub meldete keinen grün ohne Umsetzung. Im selben Lauf zeigte sich eine zweite Lücke: Beim Zurücksetzen blieben Ordner stehen, die erst der Task angelegt hatte. Ein leeres `hub/auth/` liest Python als Namespace-Paket, und die Fehlerzeile nannte `hub.auth.passwort` statt `hub.auth`. Jetzt gehen solche Ordner mit, sobald sie leer sind; derselbe Lauf für die Tasks 7, 9 und 11 zeigt danach genau die Zeile aus dem Plan. Die unabhängige Prüfung desselben Tages fand eine dritte Lücke: Ein vergessenes `--venv` fiel still auf das Trading-Venv zurück, und Task 7 stand als `ROT … No module named 'argon2'` da. Seitdem wird eine erste Fehlerzeile, die ein Modul außerhalb des Projekts und außerhalb der Zieldateien vermisst, als `VORAUSSETZUNG FEHLT` gemeldet. Das trifft auch den Ende-zu-Ende-Task des Hub, dessen Tests `playwright` brauchen. Mit den Vorgaben lieferte das Skript für die sieben Tasks des Trading-Pakets K vor und nach allen Änderungen dieselben Zeilen. Zwei weitere Lücken desselben Tages, beide ein ROT, das nichts misst: **Ein Arbeitsordner mit Windows-Kurznamen** (`C:/Users/PASCAL~1/…`) ließ vitest dort starten, die Dateien aber über den langen Namen auflösen, und jede Oberflächen-Testdatei endete als `Cannot find module '/src/…'`. Das Skript löst den Arbeitsordner jetzt auf den langen Namen auf; danach zeigte Trading-Task 395 wieder `9 failed | 52 passed` statt `no tests`, und die Hub-Tasks 24, 25, 29 und 37 die Zeile aus dem Plan (`Error: Failed to resolve import "./kontrast" …`). Die erste `Error:`-Zeile steht seitdem mit in der Ergebniszeile, weil die `FAIL`-Zeile allein den Grund nicht zeigt. **Ein Test, der über Vite eine Datei aus `node_modules` lädt** (`?raw`), scheitert hinter der Junction mit `Denied ID`, weil Vite nichts außerhalb seines Wurzelordners ausliefert; das Skript meldet das als `VORAUSSETZUNG FEHLT`. Für solche Tasks läuft der Nachweis in einer Kopie mit echtem `node_modules` (242 MB, gut 20 Sekunden).
|
||||
|
||||
@@ -62,6 +62,23 @@ def bloecke(taskdatei: Path):
|
||||
i += 1
|
||||
|
||||
|
||||
def in_kopie(kopie: Path, ziel: str) -> Path | None:
|
||||
"""Der Zielpfad in der Kopie, oder None, wenn er aus ihr hinausfuehrt.
|
||||
|
||||
Nachbesserung 2026-10-11 (Paket E9 der Trading Plattform): Ein absoluter Pfad in einer
|
||||
Ueberschrift schlug unter Windows die Kopie (kopie / "D:/..." ist der absolute Pfad), und das
|
||||
Skript schrieb mit Exit 0 in die echte Datei; gemessen im Scratchpad an einer Probe-Datei.
|
||||
Seitdem ist ein Ziel ausserhalb der Kopie (absolut oder mit "..") ein Befund, und nichts wird
|
||||
geschrieben.
|
||||
"""
|
||||
pfad = (kopie / ziel).resolve()
|
||||
try:
|
||||
pfad.relative_to(kopie.resolve())
|
||||
except ValueError:
|
||||
return None
|
||||
return pfad
|
||||
|
||||
|
||||
LOESCHZIEL = re.compile(r"^- `(.+?)`\s*\((.*)\)\s*$")
|
||||
|
||||
|
||||
@@ -110,7 +127,12 @@ def main() -> int:
|
||||
print(f"BEFUND {name}:{block['zeile']}: wird-Block ohne Aus-Block oder ohne Zieldatei")
|
||||
befunde += 1
|
||||
continue
|
||||
pfad = kopie / ziel
|
||||
pfad = in_kopie(kopie, ziel)
|
||||
if pfad is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben")
|
||||
befunde += 1
|
||||
alt = None
|
||||
continue
|
||||
if not pfad.exists():
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} fehlt in der Kopie")
|
||||
befunde += 1
|
||||
@@ -129,14 +151,22 @@ def main() -> int:
|
||||
continue
|
||||
alt = None
|
||||
if ("Die ganze Datei ist neu" in marke or "vollständig ersetzt" in marke) and ziel is not None:
|
||||
pfad = kopie / ziel
|
||||
pfad = in_kopie(kopie, ziel)
|
||||
if pfad is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben")
|
||||
befunde += 1
|
||||
continue
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(pfad, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(block["text"].rstrip("\n") + "\n")
|
||||
zaehler["neu"] += 1
|
||||
continue
|
||||
if marke.startswith("Häng diesen Block ans Ende") and ziel is not None:
|
||||
pfad = kopie / ziel
|
||||
pfad = in_kopie(kopie, ziel)
|
||||
if pfad is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben")
|
||||
befunde += 1
|
||||
continue
|
||||
text = normal(pfad.read_text(encoding="utf-8"))
|
||||
with open(pfad, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(text.rstrip("\n") + "\n" + block["text"].rstrip("\n") + "\n")
|
||||
|
||||
@@ -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-10-08
|
||||
updated: 2026-10-12
|
||||
---
|
||||
|
||||
# Prüfwerkzeuge fürs Dashboard der Trading Plattform
|
||||
@@ -180,6 +180,62 @@ Nach dem Umzug auf den Docker-Host (Hyper-V-VM) prüft der Host selbst: `deploy/
|
||||
|
||||
Was es nicht tut: Es prüft nicht, ob der Host die Kontrolle überhaupt fährt — bleibt sie dort aus, steht jeden Abend „noch kein Ergebnis“ in der Notiz, und das ist der Hinweis. Steht `-QuelleIbkr` falsch, fällt das nicht immer auf: Prüft die Aufgabe den Teil ibkr in WSL, obwohl die Instanz schon auf dem Host läuft, meldet sie ihn dort nur leise als „nicht gestartet“, weil der Zeitgeber in WSL aus ist. Ein Anmelde-Lauf zwischen 22:30 und dem Ende der Prüfung auf dem Host bekommt einmal „noch kein Ergebnis“, weil die Frist `FRUEHESTENS` für beide Quellen gilt (die Mutanten suchen genau diese Zeile).
|
||||
|
||||
## Paket E9 (Trading Plattform 0.39.0): Sicherung nach D:
|
||||
|
||||
Seit Paket E9 sichert der Docker-Host nach jedem Werktag um 16:45 New Yorker Zeit Register, `data/paper` und `artifacts/paper` beider Instanzen in seine Ablage (`deploy/docker-host/sicherung-host.sh` aus dem Repo). Die Werkzeuge hier holen die Sicherung nach `D:\Trading-Sicherung`, bestätigen sie beim Host und melden über die Abendkontrolle des Hosts, wenn sie fehlt oder unbestätigt ist. Plan im Projekt: `docs/plans/2026-10-11-paket-e9-sicherung/`; Codedoku dort in `docs/code/deployment.md`, Unterabschnitt „Sicherung (seit 0.39.0, Paket E9)“.
|
||||
|
||||
- **Gebaut und geprüft, nicht eingespielt.** `SICHERUNG_ERSTER_TAG` in `abendkontrolle.py` steht auf `None`, bis der Koordinator E9 mit Pascals Ja einspielt; bis dahin holt `abendkontrolle.py` nichts ab.
|
||||
- Beim Einspielen kommt `abendkontrolle_wsl.py` vor `host-einrichten.sh --anwenden` nach `/opt/trading-plattform/werkzeuge/`. Umgekehrt lehnt das alte Werkzeug `--sicherung` ab, und jede Abendkontrolle endet ohne Ergebnis.
|
||||
- Die Zahlen der Fälle in den Abschnitten oben gelten für den Stand vor E9; wie viele es jetzt sind, sagt die Zählzeile des Laufs.
|
||||
- Das Fenster von `abendkontrolle.py` (heute und zehn Kalendertage davor) ist kürzer als die Aufbewahrung des Hosts (zehn Sicherungstage). War der Rechner länger aus, fallen ältere Tage still heraus; die holt nur ein Aufruf mit `--tag`.
|
||||
- Bis 0.39.0 scheiterte bei einem Lauf ohne erreichbaren Host jeder Tag des Fensters mit einer eigenen Zeile `#needs-review`, auch ein Tag, der schon vollständig auf D: lag (Quorum 4 zur Trading Plattform, Befund G1). Seit 0.39.1 steht dann genau eine Zeile in der Notiz von heute (Abschnitt „Paket NT“ unten).
|
||||
|
||||
| Datei | Was sich ändert | Aufruf |
|
||||
|---|---|---|
|
||||
| `abendkontrolle.py` | holt für jeden Teil mit Quelle `dockerhost` nach den Ergebnissen die Sicherungen ab. Je Kalendertag der letzten `SICHERUNG_NACHHOLEN_TAGE` (10) bis heute ab `SICHERUNG_ERSTER_TAG` fragt es die Summe des Hosts (`sicherung-summe`); wo `<instanz>.tar.sha256` auf D: fehlt oder eine andere Summe trägt, holt es das Archiv in `<instanz>.tar.teil` (`sicherung`, 600 s), prüft die Summe, benennt nach `<tag>\<instanz>.tar` um, bestätigt beim Host (`sicherung-bestaetigt`) und schreibt zuletzt die Summendatei. Exit 3 des Hosts ist für heute und das Wochenende still, für einen vergangenen Werktag ein Fehlschlag. Zeitbudget 1500 s ab dem Start des Laufs. Je Tag mit Neuem eine Zeile „Sicherung Trading Plattform (automatisch)“ in der Notiz dieses Tags, ein Fehlschlag mit `#needs-review`. Mit `--tag` nur dieser Tag, auch einer vor `SICHERUNG_ERSTER_TAG`. `--sicherung-ziel` lenkt die Ablage um, nur für die Angriffsfälle | `python abendkontrolle.py --quelle dockerhost --quelle-ibkr dockerhost [--tag JJJJ-MM-TT]` |
|
||||
| `abendkontrolle_einrichten.ps1` | setzt den `StartBoundary` des Abend-Auslösers ohne UTC-Versatz (`JJJJ-MM-TTT23:15:00`), damit die Aufgabe nach Ortszeit startet (S10); wirkt erst, wenn die Aufgabe neu eingetragen ist | `powershell -NoProfile -File abendkontrolle_einrichten.ps1 -Quelle dockerhost -QuelleIbkr dockerhost` |
|
||||
| `abendkontrolle_wsl.py` | `--sicherung <datei>`: prüft für den Teil die Sicherung des vorigen Handelstags nach dem Kalender (zehn Kalendertage vor `--tag` bis zum Vortag; die Spanne setzt das Werkzeug selbst durch, auch für eine Kalenderdatei) gegen den Stand, den `abendkontrolle-host.sh` als JSON schreibt: Eintrag, Summe, Archiv, Bestätigung gleich der Summe, dazu ein fehlender `erster-tag` und einer nach dem geprüften Tag. Jede Abweichung ist ein Befund und geht per ntfy hinaus; still bleibt nur ein voriger Handelstag vor dem ersten Sicherungstag. Ohne den Schalter wie vorher | auf dem Host über `abendkontrolle-host.sh`; von Hand in WSL `python abendkontrolle_wsl.py --tag JJJJ-MM-TT --sicherung <datei> --ohne-meldung` (ohne `--ohne-meldung` geht jeder Befund per ntfy hinaus) |
|
||||
| `abendkontrolle_angriffe.py` | dazu die Fälle zur Sicherung in WSL: Stand unlesbar, `fehler`, `erster_tag` fehlt, kaputt oder nach dem geprüften Tag, jede Abweichung bei Eintrag, Summe, Archiv und Bestätigung, Grenzen des Kalenderfensters mit Kalenderdatei, Kalender in falscher Form, Bedienfehler und ein Meldefall gegen `ntfy_probe_server.py` | in WSL im Venv `python abendkontrolle_angriffe.py --nur-sicherung` (nur diese Fälle) oder ohne Schalter alle |
|
||||
| `abendkontrolle_mutanten.py` | `--nur TEXT` fährt nur die Mutanten mit TEXT im Namen; ein fünftes Element im Tupel geht als Argument an den Treiber. Neu sind die Mutanten „Sicherung …“ von `abendkontrolle_wsl.py`; sie fahren den Treiber mit `--nur-sicherung`. Ohne `--nur` fährt jeder bisherige Mutant den ganzen Treiber samt Sicherungsfällen und dauert länger | in WSL im Venv `timeout 3600 python abendkontrolle_mutanten.py --nur Sicherung`; Exit 0, wenn keiner überlebt |
|
||||
| `abendkontrolle_dockerhost_angriffe.py` | Die `ssh.exe`-Attrappe kennt die drei Sicherungsbefehle so streng wie `abendkontrolle-lesen.sh` (Formen, Exit 0 bis 4), gegen eine Host-Ablage im Wegwerf-Ordner. Neue Fälle zu Holen, Prüfen, Ablegen und Bestätigen, falscher Summe, abgebrochener Übertragung, Exit 3, schon vorhandener Datei, fremdem Inhalt im Zielordner, gescheiterter Bestätigung, Fenster, Zeitbudget und dem Auslöser der Aufgabe. Abgedichtet: `lauf()` setzt `SICHERUNG_ERSTER_TAG` auf `None` und `SICHERUNG_ZIEL` in den Wegwerf-Ordner, `sicherungslauf()` lenkt mit `--sicherung-ziel` um; unter `D:\Trading-Sicherung` landet nichts. `--mutanten` fährt erst die Grundlinie, dann `MUTANTEN` auf Kopien von `abendkontrolle.py` und `MUTANTEN_AUFGABE` auf Kopien der `.ps1` (als Text); jeder Mutant nennt die Fälle, die rot werden müssen | unter Windows `PYTHONIOENCODING=utf-8 python -X utf8 abendkontrolle_dockerhost_angriffe.py [--mutanten]`; Exit 0 ohne Abweichung, sonst 2 |
|
||||
|
||||
Was die Kette nicht prüft:
|
||||
|
||||
- Ob das echte `ssh.exe` die Bytes eines Archivs unverändert lässt. Jede Abweichung fiele als falsche Summe auf; den Nachweis bringt erst das erste echte Holen.
|
||||
- Dass `cmd.exe` stdout der `.cmd`-Attrappe unberührt durchreicht, ist abgeleitet; der Fall mit CR, LF und Strg-Z im Archiv zeigt es.
|
||||
- Die Units des Hosts, SELinux, das Packen auf dem Host, das Zurückspielen und ob die Kopie auf D: noch liegt; Veeam und die VHDX auch nicht.
|
||||
- Ein von Hand verändertes Archiv neben passender Summendatei auf D:: `abendkontrolle.py` hasht nicht nach. Das zeigt erst `sha256sum -c` im Tagesordner.
|
||||
- Eine Bestätigung belegt nur, dass der Absender die Summe des Hosts kennt, nicht, dass er geholt hat; derselbe Schlüssel liest die Summe.
|
||||
- `abendkontrolle_mutanten.py` zählt jeden Exit ungleich 0 des Treibers als gefangen und zeigt je Mutant nur die ersten zwei `FEHL`-Zeilen. Dass der gemeinte Fall rot wird, belegen dort nur diese Zeilen und der Rot-Nachweis im Plan.
|
||||
- Ohne eigenen Mutanten: `chmod 0644` der JSON-Datei auf dem Host, die Prüfung vor der Meldung (nur ein Fall belegt sie) und die Spanne auf der Antwort von Alpaca (kein Fall fährt den Netzweg).
|
||||
- Ein `--tag` in falscher Form: Bis 0.39.0 prüfte der Fall dazu nur, dass keine Sicherung geholt wird, und die Abendkontrolle davor schrieb eine Notiz mit „gescheitert“, bei `../…` außerhalb von `Inbox/`. Seit 0.39.1 endet der Lauf gleich mit Exit 2, und der Fall `ein_tag_ausser_der_form_endet_mit_zwei_ohne_notiz_und_ohne_ssh` prüft das (Abschnitt „Paket NT“ unten).
|
||||
|
||||
## Paket NT (Trading Plattform 0.39.1): Sicherung bei Hostausfall, `--tag` früh geprüft
|
||||
|
||||
Seit Paket NT bricht `abendkontrolle.py` die Sicherung ab, wenn der Docker-Host nicht erreichbar ist, und prüft `--tag`, bevor es etwas anfängt. Plan im Projekt: `docs/plans/2026-10-11-paket-nt-nachtraege/` (Task 729 und 739); Codedoku dort in `docs/code/deployment.md`, Unterabschnitt „Sicherung (seit 0.39.0, Paket E9)“, Absatz „Abholen unter Windows“.
|
||||
|
||||
- **Wirkt ab dem nächsten Start der Windows-Aufgabe,** nicht erst beim Einspielen: Sie führt `abendkontrolle.py` aus diesem Ordner aus, abends und fünf Minuten nach jeder Anmeldung. Solange `SICHERUNG_ERSTER_TAG` `None` ist, holt sie keine Sicherung; sofort wirkt nur die frühe Prüfung von `--tag`.
|
||||
- **Host nicht erreichbar:** Endet `sicherung-summe` oder `sicherung-bestaetigt` mit ssh-Exit 255 oder im Zeitlimit `SSH_TIMEOUT_S` (120 s), schreibt der Lauf genau eine Zeile „… **gescheitert** #needs-review. Docker-Host nicht erreichbar (…), Sicherung abgebrochen. … Der nächste Lauf versucht es erneut.“ in die Notiz von heute und endet mit Exit 1. Fertige Tage behalten ihre Zeilen; was ein angefangener Tag schon hat, steht hinter „Bis dahin am …“.
|
||||
- **Abbruch im Archiv:** Exit 255 oder Zeitlimit `SICHERUNG_TIMEOUT_S` (600 s) bei `sicherung` lässt nur diese Instanz scheitern, mit „Archiv nicht geholt (…)“; der Lauf macht weiter.
|
||||
- **`--tag`** außer der Form `JJJJ-MM-TT`, ohne gültiges Datum oder leer: eine Zeile auf stderr, Exit 2, keine Notiz, kein `lauf.log`, kein ssh, kein WSL.
|
||||
- `abendkontrolle_wsl.py` nennt im Kopf-Docstring die zwei neuen `fehler`-Gründe aus `abendkontrolle-host.sh` (Summendatei oder Bestätigung nicht lesbar); sein Verhalten ändert sich nicht.
|
||||
- Stand 0.39.1: Der Treiber hat 39 Fälle und 57 Mutanten. Weicht die Zählzeile eines Laufs davon ab, gilt sie.
|
||||
|
||||
| Datei | Was sich ändert | Aufruf |
|
||||
|---|---|---|
|
||||
| `abendkontrolle.py` | `ssh_lauf` erkennt den Ausfall (`HostNichtErreichbar`); `hole_sicherungen` bricht bei Summe und Bestätigung ab und schreibt die eine Zeile, beim Archiv scheitert nur die Instanz; `main` prüft `--tag` über `tag_gueltig` gleich nach dem Zerlegen, `sicherungstage` prüft den Tag nicht mehr selbst; der Kommentar über `SICHERUNG_NACHHOLEN_TAGE` beginnt nicht mehr mit „(S3).“; der Docstring von `ssh_lauf` sagt, dass Exit 255 bei einem Signal auf dem Host nur für OpenSSH-portable 9.5p1 nachgelesen ist, nicht für Win32-OpenSSH 9.5p2 | `python abendkontrolle.py --quelle dockerhost --quelle-ibkr dockerhost [--tag JJJJ-MM-TT]` |
|
||||
| `abendkontrolle_dockerhost_angriffe.py` | Die `ssh.exe`-Attrappe kennt drei Störungen mehr: `ANGRIFF_HOST_WEG=<n>` lässt jeden Aufruf nach den ersten n mit Exit 255 enden wie ssh ohne Host, `ANGRIFF_SICHERUNG=langsam` hält jeden Sicherungsbefehl 5 s auf, `ANGRIFF_SICHERUNG=archiv_unlesbar` endet beim Archiv wie `abendkontrolle-lesen.sh` ohne Leserecht (Exit 1). `lauf()` nimmt `ANGRIFF_HOST_WEG` aus der Umgebung, `sicherungslauf()` setzt es über den Schalter `host_weg`. Neue Fälle: `ein_unlesbares_archiv_auf_dem_host_scheitert_nur_fuer_seine_instanz`, `ein_unerreichbarer_host_gibt_eine_zeile_von_heute_und_keine_weitere_notiz`, `ein_host_der_mitten_im_lauf_wegfaellt_laesst_die_zeilen_davor_stehen`, `ein_zeitlimit_beim_host_zaehlt_wie_ein_unerreichbarer_host`, `ein_zeitlimit_beim_archiv_laesst_nur_diese_instanz_scheitern`, `eine_bestaetigung_ohne_host_bricht_die_sicherung_ab` (Task 739) und `kein_kommentar_beginnt_mit_s3`. `ein_tag_ausser_der_form_endet_mit_zwei_ohne_notiz_und_ohne_ssh` ersetzt `ein_tag_ausser_der_form_holt_keine_sicherung`; `eine_abgebrochene_uebertragung_hinterlaesst_nichts` erwartet einen Grund mit dem Befehl. In `MUTANTEN` ersetzen E729-01 bis E729-15 und E729-25 die Mutanten „Tag ungeprueft“ und „Datum ungeprueft“; dazu kommt E739-01, und „Exit des Archivs egal“ zielt jetzt auf den Fall mit dem unlesbaren Archiv | unter Windows `PYTHONIOENCODING=utf-8 python -X utf8 abendkontrolle_dockerhost_angriffe.py [--mutanten]`; Exit 0 ohne Abweichung, sonst 2 |
|
||||
| `abendkontrolle_wsl.py` | nur der Kopf-Docstring: die zwei neuen `fehler`-Gründe | wie im Abschnitt Paket E9 |
|
||||
|
||||
Was die Kette nicht prüft:
|
||||
|
||||
- Ob Exit 255 unter Win32-OpenSSH 9.5p2 auch kommt, wenn auf dem Host ein Signal `abendkontrolle-lesen.sh` beendet. Laut Quelltext von OpenSSH-portable 9.5p1 ja; dann hieße die Zeile bei Summe oder Bestätigung „Docker-Host nicht erreichbar“, obwohl er erreichbar war.
|
||||
- Lässt sich `lauf.log` nicht schreiben, fehlt auch die Zeile in der Notiz; übrig bleibt Exit 1 (kritische Prüfung NT, Befund 3, so seit E9). Ein Fall dazu fehlt, die Abhilfe steht im `todo.md` des Projekts.
|
||||
- Bricht das Archiv der letzten Instanz des letzten Tags ab, weil der Host gerade wegfällt, folgt keine Frage nach einer Summe mehr; die Zeile nennt dann nur „Archiv nicht geholt (… Exit 255 …)“, nicht „Docker-Host nicht erreichbar“.
|
||||
- Bricht das erste Archiv eines Tags ab, bleibt der leere Tagesordner auf D: zurück (so seit E9).
|
||||
- Ohne eigenen Fall: die ASCII-Darstellung des Tags in der Meldung zu `--tag` (ein Mutant `!a` nach `!r` überlebte), und „kein WSL“ bei falschem `--tag` misst der Fall nur mit `--quelle dockerhost`.
|
||||
- Ein Abend mit ausgeschaltetem Host über das echte `ssh.exe`.
|
||||
|
||||
## Paket AB (2026-10-07): Abbau des Restbestands nach dem Netzschalter-Abriss
|
||||
|
||||
Am 2026-10-07 um 20:13 beendete ein Druck auf den Netzschalter beide Paper-Runner mitten im Handel, ohne Glattstellen. Pascal am selben Abend: am 2026-10-08 soweit es geht alles verkaufen, Testläufe bis Dienstag gestoppt. Die Zeitgeber `trading-paper.timer` und `trading-paper-ibkr.timer` sind seitdem aus, die Abendkontrolle ebenfalls; die Meldung dieses Werkzeugs ist in der Pause das einzige Netz. Plan im Projekt: `docs/plans/2026-10-07-paket-ab-abbau-restbestand/`.
|
||||
|
||||
@@ -29,13 +29,25 @@ Augenblick schreibt, schuetzt das nicht (Zeitfenster Lesen bis Ersetzen, Millise
|
||||
|
||||
Annahme, benannt: Die Uhr dieses Rechners geht nach Berliner Zeit (tzutil: W. Europe Standard Time,
|
||||
gelesen am 2026-09-24); ohne das Paket tzdata kennt Python unter Windows keine Zeitzonen-Datenbank.
|
||||
Seit Paket E9 holt die Aufgabe fuer jeden Teil mit Quelle dockerhost auch die Sicherung des Hosts ab
|
||||
(hole_sicherungen): je Kalendertag der letzten SICHERUNG_NACHHOLEN_TAGE die Summe des Hosts fragen; wo
|
||||
die Summendatei auf D: fehlt oder abweicht, <tag>-<instanz>.tar ueber denselben Schluessel holen, Summe
|
||||
pruefen, nach SICHERUNG_ZIEL/<tag>/<instanz>.tar umbenennen, beim Host bestaetigen, zuletzt
|
||||
<instanz>.tar.sha256 schreiben; eine Zeile in der Notiz je Tag mit Neuem. Solange SICHERUNG_ERSTER_TAG
|
||||
None ist, holt sie nichts. Den Abend-Ausloeser legt abendkontrolle_einrichten.ps1 in Ortszeit an (S10).
|
||||
Seit Paket NT (Quorum 4): Endet die Frage nach einer Summe oder die Bestaetigung mit ssh-Exit 255 oder
|
||||
im Zeitlimit, ist der Docker-Host nicht erreichbar, und die Sicherung hoert sofort auf: eine Zeile in der
|
||||
Notiz von heute, Exit 1. Bricht nur das Archiv so ab, scheitert allein diese Instanz. --tag ausser der
|
||||
Form JJJJ-MM-TT oder ohne gueltiges Datum endet gleich nach dem Zerlegen mit Exit 2, ohne Notiz und ssh.
|
||||
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
|
||||
[--quelle wsl|dockerhost] [--quelle-ibkr wsl|dockerhost]
|
||||
[--quelle wsl|dockerhost] [--quelle-ibkr wsl|dockerhost] [--sicherung-ziel <ordner>]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
@@ -44,6 +56,7 @@ from datetime import datetime
|
||||
from datetime import time
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from time import monotonic
|
||||
|
||||
VAULT = Path(r"D:\Claude")
|
||||
PROJEKT_TEIL = Path("Code Projekte") / "active - Trading Plattform"
|
||||
@@ -82,6 +95,48 @@ KOEPFE = {
|
||||
}
|
||||
|
||||
|
||||
# Seit Paket E9 (Trading Plattform 0.39.0): Der Host sichert werktags 16:45 New Yorker Zeit je
|
||||
# Instanz Register, data/paper und artifacts/paper (sicherung-host.sh). Diese Aufgabe holt fuer
|
||||
# jeden Teil mit Quelle dockerhost die Sicherung ueber denselben Schluessel ab (abendkontrolle-lesen.sh
|
||||
# mit sicherung-summe, sicherung und sicherung-bestaetigt), prueft die Summe und legt sie unter
|
||||
# SICHERUNG_ZIEL/<tag>/<instanz>.tar samt <instanz>.tar.sha256 ab. Massstab ist die aktuelle Summe des
|
||||
# Hosts: Fehlt die Summendatei auf D: oder weicht sie ab, holt der Lauf neu und ersetzt die alte Fassung
|
||||
# (Plankritik Runde 1, Befunde 5 und 24). Sonst loescht er auf D: nichts.
|
||||
SICHERUNG_ZIEL = Path(r"D:\Trading-Sicherung")
|
||||
# Der erste Tag, den die Aufgabe abholt; None holt nichts. Gesetzt beim Einspielen von E9 (S8): Diese
|
||||
# Datei laeuft schon vorher jeden Abend, der Host kennt die Lesebefehle erst danach.
|
||||
SICHERUNG_ERSTER_TAG: date | None = None
|
||||
# So viele Kalendertage vor heute fragt jeder Lauf, dazu heute. Das Fenster liegt in der Aufbewahrung
|
||||
# des Hosts: Er haelt die neuesten zehn Sicherungstage, nur werktags gesichert, also gut zwei Kalenderwochen.
|
||||
# Die Sicherung entsteht 16:45 New Yorker Zeit, in Berlin meist 22:45, vom 25.10. bis 31.10.2026
|
||||
# und vom 14.3. bis 27.3.2027 schon 21:45. Die Aufgabe startet 23:15 Ortszeit (S10); fehlt die
|
||||
# Sicherung von heute trotzdem noch, ist das kein Fehlschlag.
|
||||
SICHERUNG_NACHHOLEN_TAGE = 10
|
||||
# Je Archiv; Summe und Bestaetigung nehmen SSH_TIMEOUT_S. Die Aufgabe selbst endet nach 45 Minuten.
|
||||
SICHERUNG_TIMEOUT_S = 600
|
||||
# Zeitbudget je Lauf fuer die Sicherung, geprueft vor jeder Instanz: Ist es um, faengt der Lauf nichts
|
||||
# mehr an, und der naechste holt den Rest (Plankritik Runde 1, Befund 12).
|
||||
SICHERUNG_BUDGET_S = 1500
|
||||
SICHERUNG_KOPF = "Sicherung Trading Plattform (automatisch)"
|
||||
# Eine Zeile im Format von sha256sum: 64 Hexzeichen in Kleinbuchstaben, zwei Leerzeichen, Name, LF.
|
||||
SUMMENZEILE = re.compile(r"([0-9a-f]{64}) (\S+)\n")
|
||||
TAGFORM = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
|
||||
|
||||
|
||||
def tag_gueltig(tag: str) -> bool:
|
||||
"""Ist tag ein Datum JJJJ-MM-TT, das es gibt, nur mit ASCII-Ziffern? Seit Paket NT fuer --tag in main.
|
||||
|
||||
TAGFORM allein liesse 2026-13-05 durch, date.fromisoformat allein seit Python 3.11 auch 20261005.
|
||||
"""
|
||||
if not TAGFORM.fullmatch(tag):
|
||||
return False
|
||||
try:
|
||||
date.fromisoformat(tag)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def faellige_tage(jetzt: datetime, ablage: Path, erster_tag: date = ERSTER_TAG, endung: str = "") -> list[str]:
|
||||
tage = []
|
||||
for zurueck in range(NACHHOLEN_TAGE, -1, -1):
|
||||
@@ -155,6 +210,271 @@ def fahre_dockerhost(tag: str, teil: str = "alpaca") -> dict:
|
||||
raise RuntimeError(f"keine JSON-Zeile vom Docker-Host (Exit {lauf.returncode}; {' | '.join(fehler)[:300]})")
|
||||
|
||||
|
||||
def ssh_aufruf(befehl: str) -> list[str]:
|
||||
"""Die Befehlszeile fuer einen Befehl der Sicherung, mit denselben Optionen wie fahre_dockerhost."""
|
||||
return [
|
||||
SSH,
|
||||
"-p", SSH_PORT,
|
||||
"-i", str(SSH_SCHLUESSEL),
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
"-o", f"UserKnownHostsFile={SSH_BEKANNTE}",
|
||||
"-o", "ConnectTimeout=20",
|
||||
SSH_ZIEL,
|
||||
befehl,
|
||||
]
|
||||
|
||||
|
||||
def fehlerzeile(lauf: subprocess.CompletedProcess) -> str:
|
||||
"""Exit und die letzten Zeilen von stderr, gekuerzt."""
|
||||
fehler = (lauf.stderr or b"").decode("utf-8", errors="replace").strip().splitlines()[-3:]
|
||||
return f"Exit {lauf.returncode}; {' | '.join(fehler)[:300]}"
|
||||
|
||||
|
||||
def summe_von(datei: Path) -> str:
|
||||
"""SHA-256 einer Datei in Kleinbuchstaben, stueckweise gelesen."""
|
||||
summe = hashlib.sha256()
|
||||
with datei.open("rb") as strom:
|
||||
for stueck in iter(lambda: strom.read(1 << 20), b""):
|
||||
summe.update(stueck)
|
||||
return summe.hexdigest()
|
||||
|
||||
|
||||
class HostNichtErreichbar(Exception):
|
||||
"""ssh selbst scheiterte (Exit 255) oder ein Befehl der Sicherung lief in sein Zeitlimit (seit Paket NT).
|
||||
|
||||
Beim Archiv macht hole_sicherung daraus den Fehlschlag dieser einen Instanz.
|
||||
"""
|
||||
|
||||
|
||||
def ssh_lauf(befehl: str, timeout, stdout=None) -> subprocess.CompletedProcess:
|
||||
"""Ein Befehl der Sicherung beim Host. Mit stdout (offene Datei) geht die Ausgabe dorthin.
|
||||
|
||||
Eigene Funktion, damit die Angriffsfaelle Zeitlimit und Dauer jedes Befehls sehen. Seit Paket NT
|
||||
(Quorum 4, G1): abendkontrolle-lesen.sh endet mit 0 bis 4. Exit 255 meldet ssh selbst: keine
|
||||
Verbindung oder Anmeldung, laut Quelltext von OpenSSH-portable 9.5p1 auch ein Befehl, den auf dem
|
||||
Host ein Signal beendet (fuer Win32-OpenSSH 9.5p2 auf diesem Rechner nicht nachgelesen). Er und ein
|
||||
abgelaufenes Zeitlimit heissen: Der Docker-Host ist nicht erreichbar (HostNichtErreichbar).
|
||||
"""
|
||||
try:
|
||||
if stdout is None:
|
||||
lauf = subprocess.run(
|
||||
ssh_aufruf(befehl), capture_output=True, timeout=timeout, creationflags=KEIN_FENSTER,
|
||||
)
|
||||
else:
|
||||
lauf = subprocess.run(
|
||||
ssh_aufruf(befehl), stdout=stdout, stderr=subprocess.PIPE, timeout=timeout,
|
||||
creationflags=KEIN_FENSTER,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HostNichtErreichbar(f"{befehl}: Zeitlimit {timeout} s abgelaufen") from None
|
||||
if lauf.returncode == 255:
|
||||
raise HostNichtErreichbar(f"{befehl}: {fehlerzeile(lauf)}")
|
||||
return lauf
|
||||
|
||||
|
||||
def summe_auf_d(ordner: Path, instanz: str) -> str | None:
|
||||
"""Die Summe aus <instanz>.tar.sha256 in ordner, wenn sie dort richtig steht und das Archiv daneben liegt.
|
||||
|
||||
Die Summendatei schreibt nur hole_sicherung, nach Pruefung und Bestaetigung: Sie steht fuer ein
|
||||
geprueftes und bestaetigtes Archiv. Das Archiv selbst hasht dieser Lauf nicht nach (Plankritik
|
||||
Runde 1, Befund 21); sonst laese jeder Lauf alle Archive des Fensters.
|
||||
"""
|
||||
archiv = ordner / f"{instanz}.tar"
|
||||
try:
|
||||
treffer = SUMMENZEILE.fullmatch((ordner / f"{instanz}.tar.sha256").read_bytes().decode("ascii"))
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return None
|
||||
if treffer is None or treffer.group(2) != archiv.name or not archiv.is_file():
|
||||
return None
|
||||
return treffer.group(1)
|
||||
|
||||
|
||||
def sicherungstage(jetzt: datetime, tag: str | None = None) -> list[str]:
|
||||
"""Die Tage, fuer die der Lauf den Host fragt, der aelteste zuerst.
|
||||
|
||||
Ohne SICHERUNG_ERSTER_TAG keiner. Mit tag (--tag) nur dieser. Sonst jeder Kalendertag der letzten
|
||||
SICHERUNG_NACHHOLEN_TAGE bis heute ab SICHERUNG_ERSTER_TAG, auch Samstag und Sonntag (ein
|
||||
nachgeholter Lauf des Hosts traegt das Datum des Nachholens) und auch heute (Plankritik Runde 1,
|
||||
Befunde 8, 16, 18 und 19). Ob das Ergebnis der Abendkontrolle fuer den Tag schon liegt, spielt
|
||||
keine Rolle.
|
||||
"""
|
||||
erster = SICHERUNG_ERSTER_TAG
|
||||
if erster is None:
|
||||
return []
|
||||
if tag is not None:
|
||||
# Der Tag wird Teil eines Pfads und eines Befehls auf dem Host. Seine Form (JJJJ-MM-TT, ein Datum,
|
||||
# das es gibt) prueft seit Paket NT main gleich nach dem Zerlegen der Argumente (tag_gueltig).
|
||||
return [tag]
|
||||
tage = []
|
||||
for zurueck in range(SICHERUNG_NACHHOLEN_TAGE, -1, -1):
|
||||
kandidat = jetzt.date() - timedelta(days=zurueck)
|
||||
if kandidat >= erster:
|
||||
tage.append(kandidat.isoformat())
|
||||
return tage
|
||||
|
||||
|
||||
def summe_vom_host(name: str) -> str | None:
|
||||
"""Die Summe, die der Host fuer <name> fuehrt; None bei Exit 3 (dort liegt keine Sicherung)."""
|
||||
lauf = ssh_lauf(f"sicherung-summe {name}", SSH_TIMEOUT_S)
|
||||
if lauf.returncode == 3:
|
||||
return None
|
||||
if lauf.returncode != 0:
|
||||
raise RuntimeError(f"Summe nicht gelesen ({fehlerzeile(lauf)})")
|
||||
treffer = SUMMENZEILE.fullmatch(lauf.stdout.decode("ascii", errors="replace"))
|
||||
if treffer is None or treffer.group(2) != f"{name}.tar":
|
||||
raise RuntimeError("Summendatei des Hosts in unerwarteter Form")
|
||||
return treffer.group(1)
|
||||
|
||||
|
||||
def ohne_sicherung_still(tag: str, jetzt: datetime) -> bool:
|
||||
"""Ist Exit 3 fuer diesen Tag kein Fehlschlag? Ja fuer heute (und spaeter) und fuer Samstag und Sonntag.
|
||||
|
||||
Heute kann die Sicherung des Hosts noch ausstehen; am Wochenende sichert er nur, wenn er einen
|
||||
verpassten Lauf nachholt. Fehlt sie an einem vergangenen Werktag, ist das ein Fehlschlag.
|
||||
"""
|
||||
kandidat = date.fromisoformat(tag)
|
||||
return kandidat >= jetzt.date() or kandidat.weekday() >= 5
|
||||
|
||||
|
||||
def sichere(tag: str, instanz: str, ziel: Path, jetzt: datetime) -> tuple[str, bool]:
|
||||
"""Eine Instanz eines Tags. Liefert ihren Teil der Zeile und ob geholt wurde; Fehlschlag: Ausnahme.
|
||||
|
||||
Massstab ist die aktuelle Summe des Hosts (Plankritik Runde 1, Befunde 5 und 24). Steht dieselbe in
|
||||
der Summendatei auf D:, ist der Tag geholt und bestaetigt. Sonst holt der Lauf neu und ersetzt die
|
||||
alte Fassung, etwa nachdem der Host den Tag ein zweites Mal gesichert hat.
|
||||
"""
|
||||
name = f"{tag}-{instanz}"
|
||||
summe = summe_vom_host(name)
|
||||
if summe is None:
|
||||
if ohne_sicherung_still(tag, jetzt):
|
||||
return f"{instanz} auf dem Docker-Host noch nicht gesichert", False
|
||||
raise RuntimeError(f"auf dem Docker-Host liegt fuer {name} noch keine Sicherung")
|
||||
if summe_auf_d(ziel / tag, instanz) == summe:
|
||||
return f"{instanz} lag schon vollständig", False
|
||||
return hole_sicherung(tag, instanz, ziel, summe), True
|
||||
|
||||
|
||||
def hole_sicherung(tag: str, instanz: str, ziel: Path, summe: str) -> str:
|
||||
"""Holt die Sicherung <tag>-<instanz> mit der Summe des Hosts, prueft, legt ab und bestaetigt.
|
||||
|
||||
Reihenfolge: Archiv in die Nachbardatei <instanz>.tar.teil holen, Summe pruefen, eine alte
|
||||
<instanz>.tar.sha256 entfernen, auf <instanz>.tar umbenennen (ersetzt eine alte Fassung), beim
|
||||
Host bestaetigen, zuletzt <instanz>.tar.sha256 schreiben. Erst die Summendatei macht die Ablage
|
||||
vollstaendig: Scheitert die Bestaetigung, fehlt sie, und der naechste Lauf holt und bestaetigt
|
||||
erneut. Bei falscher Summe wird nichts umbenannt und nichts bestaetigt. Jeder Fehlschlag ist
|
||||
eine Ausnahme mit dem Grund. Liefert den Teil der Zeile.
|
||||
"""
|
||||
name = f"{tag}-{instanz}"
|
||||
ordner = ziel / tag
|
||||
ordner.mkdir(parents=True, exist_ok=True)
|
||||
teil = ordner / f"{instanz}.tar.teil"
|
||||
archiv = ordner / f"{instanz}.tar"
|
||||
summendatei = ordner / f"{instanz}.tar.sha256"
|
||||
try:
|
||||
with teil.open("wb") as strom:
|
||||
try:
|
||||
lauf = ssh_lauf(f"sicherung {name}", SICHERUNG_TIMEOUT_S, strom)
|
||||
except HostNichtErreichbar as abbruch:
|
||||
# Seit Paket NT (Plankritik, Befund 1): Mitten im Archiv heissen Zeitlimit und Exit 255 nicht,
|
||||
# dass der Host fehlt; er hat die Summe gerade beantwortet. Nur diese Instanz scheitert, und
|
||||
# ein einzelnes zu langsames Archiv haelt nicht jeden Lauf an derselben Stelle an. Ist der
|
||||
# Host wirklich weg, merkt es die naechste Frage nach einer Summe.
|
||||
raise RuntimeError(f"Archiv nicht geholt ({abbruch})") from None
|
||||
if lauf.returncode != 0:
|
||||
raise RuntimeError(f"Archiv nicht geholt ({fehlerzeile(lauf)})")
|
||||
if summe_von(teil) != summe:
|
||||
raise RuntimeError("Summe des geholten Archivs ist nicht die des Hosts; nicht abgelegt, nicht bestaetigt")
|
||||
summendatei.unlink(missing_ok=True)
|
||||
os.replace(teil, archiv)
|
||||
finally:
|
||||
try:
|
||||
teil.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
lauf = ssh_lauf(f"sicherung-bestaetigt {name} {summe}", SSH_TIMEOUT_S)
|
||||
if lauf.returncode != 0:
|
||||
raise RuntimeError(f"abgelegt, aber nicht bestaetigt ({fehlerzeile(lauf)})")
|
||||
neu = ordner / f"{instanz}.tar.sha256.neu"
|
||||
neu.write_bytes(f"{summe} {archiv.name}\n".encode("ascii"))
|
||||
os.replace(neu, summendatei)
|
||||
return f"{instanz} abgelegt und bestätigt ({archiv.stat().st_size} Bytes)"
|
||||
|
||||
|
||||
def hole_sicherungen(jetzt: datetime, vault: Path, ablage: Path, ziel: Path, instanzen: list[str],
|
||||
frist: float, tag: str | None = None) -> int:
|
||||
"""Je Tag des Fensters und Instanz: Summe des Hosts fragen und holen, wo D: sie nicht hat.
|
||||
|
||||
Eine Zeile in der Notiz je Tag, an dem etwas geholt wurde, etwas scheiterte oder das Zeitbudget
|
||||
endete; ein Fehlschlag traegt #needs-review. Ein Tag ohne Neues bekommt keine Zeile. frist ist der
|
||||
Stand von monotonic(), an dem SICHERUNG_BUDGET_S ab dem Start des Laufs um ist (main); danach
|
||||
faengt der Lauf keine Instanz mehr an, und der naechste holt den Rest.
|
||||
"""
|
||||
if not instanzen:
|
||||
return 0
|
||||
code = 0
|
||||
erschoepft = False
|
||||
for kandidat in sicherungstage(jetzt, tag):
|
||||
if erschoepft:
|
||||
break
|
||||
uhrzeit = jetzt.strftime("%H:%M") if kandidat == jetzt.date().isoformat() else f"{jetzt:%Y-%m-%d %H:%M}"
|
||||
stuecke = []
|
||||
gescheitert = False
|
||||
getan = False
|
||||
for instanz in instanzen:
|
||||
if monotonic() >= frist:
|
||||
erschoepft = getan = True
|
||||
stuecke.append(
|
||||
f"{instanz} nicht angefangen: Zeitbudget des Laufs ({SICHERUNG_BUDGET_S} s) erschöpft, "
|
||||
"der nächste Lauf holt den Rest"
|
||||
)
|
||||
logge(ablage, f"sicherung {kandidat}-{instanz}: Zeitbudget erschoepft")
|
||||
continue
|
||||
try:
|
||||
stueck, geholt = sichere(kandidat, instanz, ziel, jetzt)
|
||||
stuecke.append(stueck)
|
||||
if geholt:
|
||||
getan = True
|
||||
logge(ablage, f"sicherung {kandidat}-{instanz}: abgelegt und bestaetigt")
|
||||
except HostNichtErreichbar as ausnahme:
|
||||
# Seit Paket NT (Quorum 4, G1): Ohne Host scheiterte sonst jeder Tag des Fensters mit eigener
|
||||
# Zeile, auch in neu angelegten Notizen von Wochenenden. Die Sicherung hoert hier auf: eine
|
||||
# Zeile in der Notiz von heute, Exit 1. Fertige Tage behalten ihre Zeilen; was der
|
||||
# angefangene Tag schon hat, nennt diese Zeile. Auf D: entsteht danach nichts mehr.
|
||||
logge(ablage, f"sicherung {kandidat}-{instanz}: Docker-Host nicht erreichbar ({ausnahme}), Sicherung abgebrochen")
|
||||
zeile = (
|
||||
f"- {jetzt:%H:%M} Uhr — {SICHERUNG_KOPF}: **gescheitert** #needs-review. Docker-Host nicht "
|
||||
f"erreichbar ({str(ausnahme)[:200]}), Sicherung abgebrochen."
|
||||
)
|
||||
if stuecke:
|
||||
zeile += f" Bis dahin am {kandidat}: {'; '.join(stuecke)}."
|
||||
zeile += " Der nächste Lauf versucht es erneut."
|
||||
try:
|
||||
schreibe_notiz(vault, jetzt.date().isoformat(), zeile)
|
||||
except Exception as zweite:
|
||||
logge(ablage, f"sicherung: Notiz nicht geschrieben {zweite!r}")
|
||||
return 1
|
||||
except Exception as ausnahme:
|
||||
gescheitert = True
|
||||
stuecke.append(f"{instanz} gescheitert ({type(ausnahme).__name__}: {str(ausnahme)[:200]})")
|
||||
logge(ablage, f"sicherung {kandidat}-{instanz}: gescheitert {ausnahme!r}")
|
||||
if not (gescheitert or getan):
|
||||
continue
|
||||
kopf = f"- {uhrzeit} Uhr — {SICHERUNG_KOPF}"
|
||||
if gescheitert:
|
||||
code = 1
|
||||
zeile = f"{kopf}: **gescheitert** #needs-review. {'; '.join(stuecke)}. Der nächste Lauf versucht es erneut."
|
||||
else:
|
||||
zeile = f"{kopf}: {'; '.join(stuecke)}."
|
||||
try:
|
||||
schreibe_notiz(vault, kandidat, zeile)
|
||||
except Exception as ausnahme:
|
||||
code = 1
|
||||
logge(ablage, f"sicherung {kandidat}: Notiz nicht geschrieben {ausnahme!r}")
|
||||
return code
|
||||
|
||||
|
||||
def zeile_fuer(ergebnis: dict, uhrzeit: str) -> str:
|
||||
kopf = f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch)"
|
||||
befunde = ergebnis.get("befunde") or []
|
||||
@@ -284,7 +604,21 @@ def main(argv=None) -> int:
|
||||
"--quelle-ibkr", choices=QUELLEN,
|
||||
help="die Quelle fuer den Teil ibkr; ohne Angabe dieselbe wie --quelle",
|
||||
)
|
||||
zerleger.add_argument(
|
||||
"--sicherung-ziel",
|
||||
help="Ordner fuer die Sicherungen statt SICHERUNG_ZIEL (Angriffsfaelle)",
|
||||
)
|
||||
argumente = zerleger.parse_args(argv)
|
||||
# Seit Paket NT (Quorum 4): --tag gleich hier pruefen. Der Tag wird Name der Notiz, Pfad unter
|
||||
# SICHERUNG_ZIEL und Teil eines Befehls auf dem Host; vorher legte ein Tag ausser der Form
|
||||
# Inbox/<tag>.md an, mit ../ auch ausserhalb von Inbox/. Leer zaehlt als ausser der Form. Die
|
||||
# Ausgabe ist ASCII (!a), damit kein Zeichen an der Kodierung von stderr scheitert.
|
||||
if argumente.tag is not None and not tag_gueltig(argumente.tag):
|
||||
print(f"abendkontrolle: --tag {argumente.tag!a} ist kein Datum JJJJ-MM-TT", file=sys.stderr)
|
||||
return 2
|
||||
# Seit Paket E9: Das Zeitbudget der Sicherung zaehlt ab hier, also mit der Zeit fuer die
|
||||
# Ergebnisse (bis zu zwoelf Abfragen mit je SSH_TIMEOUT_S; Plankritik Runde 2, Befund 1).
|
||||
frist = monotonic() + SICHERUNG_BUDGET_S
|
||||
vault = Path(argumente.vault)
|
||||
ablage = vault / ABLAGE_TEIL
|
||||
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
|
||||
@@ -313,6 +647,21 @@ def main(argv=None) -> int:
|
||||
logge(ablage, f"{vorsatz}abgebrochen: {type(ausnahme).__name__}")
|
||||
except Exception:
|
||||
pass
|
||||
# Seit Paket E9: die Sicherung jedes Teils mit Quelle dockerhost, nach den Ergebnissen beider
|
||||
# Teile und unabhaengig davon, ob ein Ergebnis gescheitert ist.
|
||||
instanzen = [
|
||||
teil for teil in teile
|
||||
if ((argumente.quelle_ibkr or argumente.quelle) if teil == "ibkr" else argumente.quelle) == "dockerhost"
|
||||
]
|
||||
ziel = Path(argumente.sicherung_ziel) if argumente.sicherung_ziel else SICHERUNG_ZIEL
|
||||
try:
|
||||
code = max(code, hole_sicherungen(jetzt, vault, ablage, ziel, instanzen, frist, argumente.tag))
|
||||
except Exception as ausnahme:
|
||||
code = 1
|
||||
try:
|
||||
logge(ablage, f"sicherung abgebrochen: {type(ausnahme).__name__}: {ausnahme}")
|
||||
except Exception:
|
||||
pass
|
||||
return code
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Grundlinie; ohne Netz (--ohne-broker/--ohne-kerzen, Kalender und Journal aus Dat
|
||||
Auswertung fuer die Brokerzeile) und ohne Meldung (--ohne-meldung). Faelle an zwei Listenpositionen,
|
||||
wo die Reihenfolge zaehlen kann (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 3).
|
||||
Aufruf in WSL (Venv) und unter Windows: python abendkontrolle_angriffe.py
|
||||
Nur die Faelle zur Sicherung (Paket E9, Task 726), in WSL: python abendkontrolle_angriffe.py --nur-sicherung
|
||||
Exit 0, wenn jeder Fall so ausgeht wie erwartet; sonst 1 mit der Liste der Abweichungen.
|
||||
"""
|
||||
|
||||
@@ -811,12 +812,269 @@ def ibkr_windows_faelle() -> None:
|
||||
f"ibkr {ibkr} alpaca {alpaca_tage}")
|
||||
|
||||
|
||||
# --- Sicherung des vorigen Handelstags (Paket E9, Task 726), WSL-Teil ------------------------------
|
||||
|
||||
# TAG ist ein Mittwoch, S_VORHER der Dienstag davor, S_DAVOR der Montag. Die Summen sind erfunden.
|
||||
S_VORHER = "2026-09-22"
|
||||
S_DAVOR = "2026-09-21"
|
||||
S_ERSTER = "2026-09-14"
|
||||
S_SUMME = "ab" * 32
|
||||
S_ANDERE = "cd" * 32
|
||||
S_KALENDER = [
|
||||
{"date": S_DAVOR, "open": "09:30", "close": "16:00"},
|
||||
{"date": S_VORHER, "open": "09:30", "close": "16:00"},
|
||||
] + KALENDER_TAG
|
||||
|
||||
|
||||
def s_eintraege(anders: dict | None = None, ohne: tuple = ()) -> dict:
|
||||
"""Beide Instanzen an S_DAVOR und S_VORHER mit Archiv und bestaetigt; anders ersetzt, ohne loescht."""
|
||||
eintraege = {
|
||||
f"{tag}-{instanz}": {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": True}
|
||||
for tag in (S_DAVOR, S_VORHER)
|
||||
for instanz in ("alpaca", "ibkr")
|
||||
}
|
||||
for name in ohne:
|
||||
del eintraege[name]
|
||||
eintraege.update(anders or {})
|
||||
return eintraege
|
||||
|
||||
|
||||
def s_stand(eintraege: dict | None = None, **oben) -> dict:
|
||||
"""Ein Stand wie von abendkontrolle-host.sh; oben ersetzt oder ergaenzt Schluessel der obersten Ebene."""
|
||||
stand = {"erster_tag": S_ERSTER, "eintraege": s_eintraege() if eintraege is None else eintraege}
|
||||
stand.update(oben)
|
||||
return stand
|
||||
|
||||
|
||||
def fahre_sicherung(wurzel: Path, ziel: Path, teil: str, *, schalter: bool = True, tag: str = TAG,
|
||||
meldung: bool = False, env: dict | None = None) -> dict:
|
||||
"""Ein Lauf eines Teils mit --sicherung; ziel ist der Projektordner (alpaca) oder der Betriebsordner (ibkr)."""
|
||||
befehl = [
|
||||
sys.executable, str(HIER / "abendkontrolle_wsl.py"), "--tag", tag,
|
||||
"--kalender", str(wurzel / "kalender.json"), "--journal", str(wurzel / "journal.txt"),
|
||||
"--dienste", str(wurzel / "dienste.json"),
|
||||
]
|
||||
if teil == "ibkr":
|
||||
befehl += ["--teil", "ibkr", "--ibkr-ordner", str(ziel), "--projekt", str(wurzel),
|
||||
"--ibkr-konto", str(wurzel / "konto.json"), "--jetzt", NACH_SCHLUSS]
|
||||
else:
|
||||
befehl += ["--projekt", str(ziel), "--ohne-kerzen", "--ohne-broker"]
|
||||
if schalter:
|
||||
befehl += ["--sicherung", str(wurzel / "sicherung.json")]
|
||||
if not meldung:
|
||||
befehl.append("--ohne-meldung")
|
||||
umgebung = {k: v for k, v in os.environ.items() if k != "ABENDKONTROLLE_AUSWERTUNG"}
|
||||
umgebung.update(env or {})
|
||||
lauf = subprocess.run(befehl, capture_output=True, text=True, env=umgebung, timeout=300)
|
||||
try:
|
||||
ergebnis = json.loads(lauf.stdout.strip().splitlines()[-1])
|
||||
except (IndexError, json.JSONDecodeError):
|
||||
return {"befunde": [f"KEINE JSON-AUSGABE exit {lauf.returncode}: {lauf.stderr[-300:]}"], "exit": lauf.returncode}
|
||||
ergebnis["_exit"] = lauf.returncode
|
||||
return ergebnis
|
||||
|
||||
|
||||
def sicherung_faelle() -> None:
|
||||
def fall(name, stand, *, befund, teil="alpaca", kalender=S_KALENDER, anzahl=None, exit_code=0,
|
||||
schalter=True, tag=TAG, auch=(), nicht=(), fakt=None):
|
||||
"""stand: None heisst, die Datei fehlt; eine Zeichenkette wird roh geschrieben, sonst als JSON."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
wurzel = Path(tmp)
|
||||
ziel = baue_ibkr(wurzel) if teil == "ibkr" else baue_projekt(wurzel)
|
||||
(wurzel / "kalender.json").write_text(json.dumps(kalender), encoding="utf-8")
|
||||
if isinstance(stand, str):
|
||||
(wurzel / "sicherung.json").write_text(stand, encoding="utf-8")
|
||||
elif stand is not None:
|
||||
(wurzel / "sicherung.json").write_text(json.dumps(stand), encoding="utf-8")
|
||||
erwarte("Sicherung " + name, fahre_sicherung(wurzel, ziel, teil, schalter=schalter, tag=tag),
|
||||
befund=befund, anzahl=anzahl, exit_code=exit_code, auch=auch, nicht=nicht, fakt=fakt)
|
||||
|
||||
a = f"{S_VORHER}-alpaca"
|
||||
i = f"{S_VORHER}-ibkr"
|
||||
offen = {"summe": S_SUMME, "bestaetigt": None, "archiv": True}
|
||||
|
||||
# Grundlinie in beiden Teilen; ohne Schalter bleibt das Werkzeug, wie es war.
|
||||
fall("Grundlinie alpaca", s_stand(), befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung") == {"name": a, "erster_tag": S_ERSTER, "geprueft": True})
|
||||
fall("Grundlinie ibkr", s_stand(), teil="ibkr", befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung") == {"name": i, "erster_tag": S_ERSTER, "geprueft": True})
|
||||
fall("ohne Schalter", s_stand(s_eintraege({a: offen})), schalter=False, befund=None, anzahl=0,
|
||||
fakt=lambda f, e: "sicherung" not in f)
|
||||
# Umgebung und Form der Datei
|
||||
fall("Datei fehlt", None, befund="Sicherung nicht geprueft (FileNotFoundError", anzahl=1)
|
||||
fall("Datei kein JSON", "<html>", befund="Sicherung nicht geprueft (JSONDecodeError", anzahl=1)
|
||||
fall("Datei leer", "", befund="Sicherung nicht geprueft (JSONDecodeError", anzahl=1)
|
||||
fall("Stand null", "null", befund="Sicherung: Stand ist kein Objekt", anzahl=1)
|
||||
fall("Stand als Liste", [], befund="Sicherung: Stand ist kein Objekt", anzahl=1)
|
||||
fall("eintraege als Liste", s_stand([]), befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("eintraege fehlt", {"erster_tag": S_ERSTER}, befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("erster_tag fehlt", {"eintraege": s_eintraege()}, befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("erster_tag als Zahl", s_stand(erster_tag=20260914), befund="Sicherung: erster_tag ist kein Datum", anzahl=1)
|
||||
fall("erster_tag kein Datum", s_stand(erster_tag="morgen"), befund="Sicherung: erster_tag ist kein Datum", anzahl=1)
|
||||
# Fehler, die abendkontrolle-host.sh in den Stand schreibt
|
||||
fall("Host meldet fehler", {"fehler": "Ablage der Sicherung fehlt"},
|
||||
befund="Sicherung nicht geprueft: Ablage der Sicherung fehlt", anzahl=1)
|
||||
fall("fehler neben vollem Stand", s_stand(fehler="Ablage der Sicherung nicht lesbar"),
|
||||
befund="Sicherung nicht geprueft: Ablage der Sicherung nicht lesbar", anzahl=1)
|
||||
# Erster Sicherungstag (seit der Plankritik Runde 1): null und ein Tag nach TAG sind Befunde, sonst
|
||||
# schwiege die Pruefung auf Dauer. Still bleibt nur ein voriger Handelstag vor erster_tag; TAG selbst
|
||||
# als erster_tag ist erlaubt, und der erste Sicherungstag selbst wird geprueft.
|
||||
fall("erster_tag null", s_stand({}, erster_tag=None),
|
||||
befund="Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung", anzahl=1,
|
||||
fakt=lambda f, e: "sicherung" not in f
|
||||
and not any("erster-tag" in h for h in e.get("hinweise", [])))
|
||||
fall("erster_tag nach dem geprueften Tag", s_stand({}, erster_tag="2026-09-24"),
|
||||
befund=f"Sicherung nicht geprueft: erster_tag 2026-09-24 liegt nach dem geprueften Tag {TAG}", anzahl=1,
|
||||
fakt=lambda f, e: "sicherung" not in f)
|
||||
fall("voriger Handelstag vor erster_tag", s_stand({}, erster_tag=TAG), befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("geprueft") is False)
|
||||
fall("erster_tag ist der vorige Handelstag", s_stand({}, erster_tag=S_VORHER),
|
||||
befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# Eintrag des vorigen Handelstags
|
||||
fall("Eintrag fehlt", s_stand(s_eintraege(ohne=(a,))), befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
fall("Eintrag null", s_stand(s_eintraege({a: None})), befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
fall("Eintrag als Liste", s_stand(s_eintraege({a: [S_SUMME, S_SUMME]})),
|
||||
befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# summe
|
||||
for titel, eintrag in (
|
||||
("summe null", {"summe": None, "bestaetigt": S_SUMME}),
|
||||
("summe fehlt", {"bestaetigt": S_SUMME}),
|
||||
("summe leer", {"summe": "", "bestaetigt": S_SUMME}),
|
||||
("summe in Grossbuchstaben", {"summe": S_SUMME.upper(), "bestaetigt": S_SUMME.upper()}),
|
||||
("summe mit 63 Zeichen", {"summe": S_SUMME[:-1], "bestaetigt": S_SUMME[:-1]}),
|
||||
("summe mit 65 Zeichen", {"summe": S_SUMME + "a", "bestaetigt": S_SUMME + "a"}),
|
||||
("summe als Zahl", {"summe": 5, "bestaetigt": S_SUMME}),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: eintrag})), befund=f"Sicherung {a}: keine gueltige Summe", anzahl=1)
|
||||
# bestaetigt
|
||||
for titel, eintrag, text in (
|
||||
("bestaetigt null", {"summe": S_SUMME, "bestaetigt": None}, "nicht bestaetigt"),
|
||||
("bestaetigt fehlt", {"summe": S_SUMME}, "nicht bestaetigt"),
|
||||
("bestaetigt leer", {"summe": S_SUMME, "bestaetigt": ""}, "nicht bestaetigt"),
|
||||
("bestaetigt in Grossbuchstaben", {"summe": S_SUMME, "bestaetigt": S_SUMME.upper()}, "nicht bestaetigt"),
|
||||
("bestaetigt erstes Zeichen anders", {"summe": S_SUMME, "bestaetigt": "0" + S_SUMME[1:]},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
("bestaetigt letztes Zeichen anders", {"summe": S_SUMME, "bestaetigt": S_SUMME[:-1] + "0"},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
("Bestaetigung einer anderen Sicherung", {"summe": S_SUMME, "bestaetigt": S_ANDERE},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: {**eintrag, "archiv": True}})), befund=f"Sicherung {a}: {text}", anzahl=1)
|
||||
# archiv (seit der Plankritik Runde 1): Bei gueltiger Summe zaehlt nur true, sonst fehlt das Archiv.
|
||||
for titel, eintrag in (
|
||||
("archiv false", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": False}),
|
||||
("archiv fehlt", {"summe": S_SUMME, "bestaetigt": S_SUMME}),
|
||||
("archiv null", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": None}),
|
||||
("archiv als Text", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": "true"}),
|
||||
("archiv false, nicht bestaetigt", {"summe": S_SUMME, "bestaetigt": None, "archiv": False}),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: eintrag})), befund=f"Sicherung {a}: Archiv fehlt auf dem Docker-Host",
|
||||
anzahl=1)
|
||||
# Instanz nach --teil
|
||||
fall("Teil ibkr, nur Alpaca bestaetigt", s_stand(s_eintraege({i: offen})), teil="ibkr",
|
||||
befund=f"Sicherung {i}: nicht bestaetigt", anzahl=1, nicht=("alpaca",))
|
||||
fall("Teil alpaca, nur IBKR bestaetigt", s_stand(s_eintraege({a: offen})),
|
||||
befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1, nicht=("ibkr",))
|
||||
fall("Teil ibkr, Eintrag fehlt", s_stand(s_eintraege(ohne=(i,))), teil="ibkr",
|
||||
befund=f"Sicherung {i} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# Reihenfolge: derselbe Schaden am ersten und am letzten Eintrag
|
||||
andere = {name: wert for name, wert in s_eintraege().items() if name != a}
|
||||
fall("Schaden am ersten Eintrag", s_stand({a: offen, **andere}), befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
fall("Schaden am letzten Eintrag", s_stand({**andere, a: offen}), befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
# Kalender: der juengste Handelstag vor TAG, nicht der Vortag und nicht der letzte Listeneintrag
|
||||
fall("Kalender unsortiert", s_stand(s_eintraege({a: offen})), kalender=list(reversed(S_KALENDER)),
|
||||
befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
feiertag = [S_KALENDER[0]] + KALENDER_TAG
|
||||
fall("Feiertag am Vortag, der Tag davor bestaetigt", s_stand(s_eintraege({a: offen})), kalender=feiertag,
|
||||
befund=None, anzahl=0, fakt=lambda f, e: f.get("sicherung", {}).get("name") == f"{S_DAVOR}-alpaca")
|
||||
fall("Feiertag am Vortag, der Tag davor offen", s_stand(s_eintraege({f"{S_DAVOR}-alpaca": offen})),
|
||||
kalender=feiertag, befund=f"Sicherung {S_DAVOR}-alpaca: nicht bestaetigt", anzahl=1)
|
||||
fall("Kalender ohne Tag davor", s_stand(), kalender=KALENDER_TAG, befund="Sicherung: kein Handelstag in den", anzahl=1)
|
||||
fall("Kalender als Objekt", s_stand(), kalender={}, befund="Sicherung nicht geprueft (ValueError", anzahl=2,
|
||||
auch=("Kalender in unerwarteter Form",))
|
||||
fall("Kalender Eintrag ohne date, erste Position", s_stand(), kalender=[{"open": "09:30"}] + S_KALENDER,
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=2)
|
||||
fall("Kalender Eintrag ohne date, letzte Position", s_stand(), kalender=S_KALENDER + [{"date": 5}],
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=2)
|
||||
fall("Kalender mit Datum in anderer Form", s_stand(), kalender=[{"date": "22.09.2026"}] + S_KALENDER,
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=1)
|
||||
# Fenster (seit der Plankritik Runde 1): SICHERUNG_FENSTER_TAGE Kalendertage vor TAG, der Rand gehoert
|
||||
# dazu, TAG selbst nie. Das Werkzeug prueft keinen Wochentag; Rand und Tag davor sind Wochenendtage.
|
||||
rand = "2026-09-13"
|
||||
fall("Kalender voriger Handelstag am Rand des Fensters",
|
||||
s_stand(s_eintraege({f"{rand}-alpaca": {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": True}}),
|
||||
erster_tag="2026-09-01"),
|
||||
kalender=[{"date": rand, "open": "09:30", "close": "16:00"}] + KALENDER_TAG, befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("name") == f"{rand}-alpaca")
|
||||
fall("Kalender voriger Handelstag ausserhalb des Fensters", s_stand(),
|
||||
kalender=[{"date": "2026-09-12", "open": "09:30", "close": "16:00"}] + KALENDER_TAG,
|
||||
befund=f"Sicherung: kein Handelstag in den 10 Tagen vor {TAG} im Kalender", anzahl=1)
|
||||
fall("Kalender Tag selbst nie der vorige", s_stand(s_eintraege({f"{TAG}-alpaca": offen})),
|
||||
kalender=KALENDER_TAG + [S_KALENDER[1]], befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("name") == a)
|
||||
# Zusaetzlich: unbekannte Schluessel und fremde Namen stoeren nicht
|
||||
fall("unbekannte Schluessel und fremde Namen",
|
||||
s_stand(s_eintraege({"notiz": {"summe": None}, f"{S_VORHER}-paper": None}), neu=1), befund=None, anzahl=0)
|
||||
# Bedienfehler: Ein falscher Tag bricht den Teil ab, und die Sicherung meldet sich trotzdem.
|
||||
fall("Tag falsch", s_stand(), tag="2026-13-40", befund="abgebrochen", exit_code=2, anzahl=2,
|
||||
auch=("Sicherung nicht geprueft (ValueError",))
|
||||
|
||||
# Meldeweg: Ein Befund der Sicherung allein loest die Meldung aus (lokaler Probeserver, kein Netz).
|
||||
with tempfile.TemporaryDirectory() as tmp_server:
|
||||
mitschrift = Path(tmp_server) / "mitschrift.jsonl"
|
||||
port = 18767
|
||||
server = subprocess.Popen([sys.executable, str(HIER / "ntfy_probe_server.py"), str(port), str(mitschrift)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
import socket
|
||||
import time
|
||||
for _ in range(50):
|
||||
try:
|
||||
socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
wurzel = Path(tmp)
|
||||
projekt = baue_projekt(wurzel)
|
||||
(wurzel / "kalender.json").write_text(json.dumps(S_KALENDER), encoding="utf-8")
|
||||
(wurzel / "sicherung.json").write_text(json.dumps(s_stand(s_eintraege({a: offen}))), encoding="utf-8")
|
||||
text = (ECHTES_PROJEKT / "config" / "paper.toml").read_text(encoding="utf-8")
|
||||
anker = 'ntfy_url = "https://ntfy.sh"'
|
||||
if text.count(anker) != 1:
|
||||
raise RuntimeError("Anker ntfy_url fehlt in config/paper.toml")
|
||||
(projekt / "config" / "paper.toml").write_text(
|
||||
text.replace(anker, f'ntfy_url = "http://127.0.0.1:{port}"\nklartext_lokal = true'), encoding="utf-8")
|
||||
(wurzel / "secrets.toml").write_text('[ntfy]\naktiv = true\nthema = "abendkontrolle-angriff"\n',
|
||||
encoding="utf-8")
|
||||
ergebnis = fahre_sicherung(wurzel, projekt, "alpaca", meldung=True,
|
||||
env={"TRADING_GEHEIMNISSE": str(wurzel / "secrets.toml")})
|
||||
zeilen = ([json.loads(z) for z in mitschrift.read_text(encoding="utf-8").split("\n") if z.strip()]
|
||||
if mitschrift.exists() else [])
|
||||
titel = [str(z.get("title") or "") for z in zeilen]
|
||||
befunde = ergebnis.get("befunde", [])
|
||||
gesendet = (ergebnis.get("fakten") or {}).get("meldung")
|
||||
ok = (gesendet == "gesendet" and len(zeilen) == 1 and f"Abendkontrolle {TAG}: 1 Befund(e)" in titel[0]
|
||||
and len(befunde) == 1 and befunde[0].startswith(f"Sicherung {a}: nicht bestaetigt"))
|
||||
zeile = (f"{'OK ' if ok else 'FEHL'} Sicherung Befund geht in die Meldung: "
|
||||
f"befunde={befunde[:2]} titel={titel} meldung={gesendet}")
|
||||
print(zeile)
|
||||
if not ok:
|
||||
abweichungen.append(zeile)
|
||||
finally:
|
||||
server.terminate()
|
||||
server.wait(timeout=10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.platform == "win32":
|
||||
windows_faelle()
|
||||
ibkr_windows_faelle()
|
||||
elif "--nur-sicherung" in sys.argv[1:]:
|
||||
sicherung_faelle()
|
||||
else:
|
||||
wsl_faelle()
|
||||
ibkr_wsl_faelle()
|
||||
sicherung_faelle()
|
||||
print(f"ERGEBNIS: {len(abweichungen)} Abweichung(en)")
|
||||
sys.exit(1 if abweichungen else 0)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -41,6 +41,11 @@ if (-not $QuelleIbkr) { $QuelleIbkr = $Quelle }
|
||||
$Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '" --quelle ' + $Quelle + ' --quelle-ibkr ' + $QuelleIbkr) -WorkingDirectory "D:\Claude"
|
||||
if ($Quelle -eq "dockerhost" -or $QuelleIbkr -eq "dockerhost") { $Uhrzeit = "23:15" } else { $Uhrzeit = "22:45" }
|
||||
$Abends = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday, Tuesday, Wednesday, Thursday, Friday -At $Uhrzeit
|
||||
# Seit Paket E9 (S10): -At legt den Startzeitpunkt mit UTC-Versatz an (gemessen 2026-10-11:
|
||||
# ...T21:15:00Z). Eine Aufgabe mit Versatz startet im Winter eine Stunde frueher nach Ortszeit, vor
|
||||
# der Abendkontrolle des Hosts. Ohne Versatz gesetzt, speichert die Aufgabenplanung den Wert so und
|
||||
# startet nach Ortszeit.
|
||||
$Abends.StartBoundary = (Get-Date -Format 'yyyy-MM-dd') + 'T' + $Uhrzeit + ':00'
|
||||
$Anmeldung = New-ScheduledTaskTrigger -AtLogOn -User $Benutzer
|
||||
$Anmeldung.Delay = "PT5M"
|
||||
$Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited
|
||||
|
||||
@@ -4,8 +4,11 @@ Koordinator-Werkzeug, nicht fuers Repo. Jeder Mutant laeuft auf einer Wegwerfkop
|
||||
In WSL (Venv) die Mutanten von abendkontrolle_wsl.py, unter Windows die von abendkontrolle.py und
|
||||
rueckweg_aapl.py. Aufruf: python abendkontrolle_mutanten.py Exit 0, wenn keiner ueberlebt.
|
||||
Ein Mutant, der das Verhalten nicht aendert, ist keine Luecke (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 8).
|
||||
Seit Paket E9: --nur TEXT faehrt nur die Mutanten mit TEXT im Namen (etwa --nur Sicherung in WSL); ein
|
||||
fuenftes Element im Tupel geht als Argument an den Treiber.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -64,6 +67,78 @@ WSL_MUTANTEN = [
|
||||
("IBKR Muster aus", "abendkontrolle_wsl.py", " if muster in text:", " if False:"),
|
||||
("IBKR Titel ohne Praefix", "abendkontrolle_wsl.py", 'IBKR_TITEL = "IBKR "', 'IBKR_TITEL = ""'),
|
||||
("IBKR Meldeweiche aus", "abendkontrolle_wsl.py", "melde(IBKR_TITEL + titel if ibkr else titel,", "melde(titel,"),
|
||||
# Seit Paket E9 (Task 726): --sicherung. Der Treiber faehrt dafuer nur seine Faelle zur Sicherung.
|
||||
("Sicherung letzter Listeneintrag statt juengster Tag", "abendkontrolle_wsl.py",
|
||||
" return max(tage) if tage else None", " return tage[-1] if tage else None", "--nur-sicherung"),
|
||||
("Sicherung Vortag statt Kalender", "abendkontrolle_wsl.py",
|
||||
" vorher = juengster_handelstag(lies_kalender_spanne(start, ende, argumente.kalender, Path(argumente.projekt)))",
|
||||
" vorher = ende", "--nur-sicherung"),
|
||||
("Sicherung Kalenderform egal", "abendkontrolle_wsl.py",
|
||||
' raise ValueError("Kalender mit einem Eintrag ohne date JJJJ-MM-TT")', " continue",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Fehler des Hosts ueberlesen", "abendkontrolle_wsl.py", ' if "fehler" in stand:', " if False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Stand ohne Typpruefung", "abendkontrolle_wsl.py", " if not isinstance(stand, dict):", " if False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung eintraege ohne Typpruefung", "abendkontrolle_wsl.py",
|
||||
' if "erster_tag" not in stand or not isinstance(stand.get("eintraege"), dict):',
|
||||
' if "erster_tag" not in stand:', "--nur-sicherung"),
|
||||
("Sicherung erster_tag ohne Formpruefung", "abendkontrolle_wsl.py",
|
||||
" if not isinstance(erster, str) or not DATUM.fullmatch(erster):", " if False:", "--nur-sicherung"),
|
||||
("Sicherung ohne erster_tag nur Hinweis", "abendkontrolle_wsl.py",
|
||||
' k.befund("Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung")',
|
||||
' k.hinweis("Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung")', "--nur-sicherung"),
|
||||
("Sicherung vor erster_tag geprueft", "abendkontrolle_wsl.py", " if vorher < erster:", " if False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung erster_tag selbst ausgenommen", "abendkontrolle_wsl.py", " if vorher < erster:",
|
||||
" if vorher <= erster:", "--nur-sicherung"),
|
||||
("Sicherung kein Handelstag still", "abendkontrolle_wsl.py",
|
||||
' k.befund(f"Sicherung: kein Handelstag in den {SICHERUNG_FENSTER_TAGE} Tagen vor {tag} im Kalender")',
|
||||
" pass", "--nur-sicherung"),
|
||||
("Sicherung immer alpaca", "abendkontrolle_wsl.py",
|
||||
' instanz = "ibkr" if argumente.teil == "ibkr" else "alpaca"', ' instanz = "alpaca"', "--nur-sicherung"),
|
||||
("Sicherung fehlender Eintrag still", "abendkontrolle_wsl.py",
|
||||
' k.befund(f"Sicherung {name} fehlt auf dem Docker-Host")', " pass", "--nur-sicherung"),
|
||||
("Sicherung Summe ungeprueft", "abendkontrolle_wsl.py",
|
||||
" if not isinstance(summe, str) or not SUMME.fullmatch(summe):", " if summe is None:", "--nur-sicherung"),
|
||||
("Sicherung Bestaetigung ungeprueft", "abendkontrolle_wsl.py",
|
||||
" elif not isinstance(bestaetigt, str) or not SUMME.fullmatch(bestaetigt):", " elif False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Vergleich aus", "abendkontrolle_wsl.py", " elif bestaetigt != summe:", " elif False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Schalter nicht verdrahtet", "abendkontrolle_wsl.py", " if argumente.sicherung:", " if False:",
|
||||
"--nur-sicherung"),
|
||||
# Seit der Plankritik Runde 1 (Task 726): Fenster, erster_tag, Archiv und die zwei Faelle ohne Mutanten.
|
||||
("Sicherung Fenster 1 Tag", "abendkontrolle_wsl.py", "SICHERUNG_FENSTER_TAGE = 10", "SICHERUNG_FENSTER_TAGE = 1",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Fenster einen Tag kuerzer", "abendkontrolle_wsl.py",
|
||||
" start = (geprueft - timedelta(days=SICHERUNG_FENSTER_TAGE)).isoformat()",
|
||||
" start = (geprueft - timedelta(days=SICHERUNG_FENSTER_TAGE - 1)).isoformat()", "--nur-sicherung"),
|
||||
("Sicherung Ende am Tag selbst", "abendkontrolle_wsl.py",
|
||||
" ende = (geprueft - timedelta(days=1)).isoformat()", " ende = geprueft.isoformat()", "--nur-sicherung"),
|
||||
("Sicherung Spanne der Datei aus", "abendkontrolle_wsl.py",
|
||||
' return in_spanne(json.loads(Path(kalender_datei).read_text(encoding="utf-8")), start, ende)',
|
||||
' return json.loads(Path(kalender_datei).read_text(encoding="utf-8"))', "--nur-sicherung"),
|
||||
("Sicherung Spanne ohne Anfang", "abendkontrolle_wsl.py", ' or start <= eintrag["date"] <= ende',
|
||||
' or eintrag["date"] <= ende', "--nur-sicherung"),
|
||||
("Sicherung Spanne ohne Typpruefung", "abendkontrolle_wsl.py",
|
||||
" if not isinstance(liste, list):\n return liste", " if False:\n return liste",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Kalender ohne Typpruefung", "abendkontrolle_wsl.py",
|
||||
' if not isinstance(liste, list):\n raise ValueError("Kalender ist keine Liste")',
|
||||
' if False:\n raise ValueError("Kalender ist keine Liste")', "--nur-sicherung"),
|
||||
("Sicherung erster_tag nach dem Tag still", "abendkontrolle_wsl.py", " if erster > tag:", " if False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung erster_tag am Tag selbst als Befund", "abendkontrolle_wsl.py", " if erster > tag:",
|
||||
" if erster >= tag:", "--nur-sicherung"),
|
||||
("Sicherung Archiv ungeprueft", "abendkontrolle_wsl.py", ' elif eintrag.get("archiv") is not True:',
|
||||
" elif False:", "--nur-sicherung"),
|
||||
("Sicherung Archiv nur false zaehlt", "abendkontrolle_wsl.py", ' elif eintrag.get("archiv") is not True:',
|
||||
' elif eintrag.get("archiv") is False:', "--nur-sicherung"),
|
||||
("Sicherung ohne Schalter geprueft", "abendkontrolle_wsl.py", " if argumente.sicherung:", " if True:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung unbekannter Schluessel als Fehler", "abendkontrolle_wsl.py", ' if "fehler" in stand:',
|
||||
' if "fehler" in stand or set(stand) - {"erster_tag", "eintraege"}:', "--nur-sicherung"),
|
||||
]
|
||||
WIN_MUTANTEN = [
|
||||
("keine Ergebnisdatei", "abendkontrolle.py", '(ablage / f"{tag}{endung}.json").write_text(',
|
||||
@@ -109,9 +184,19 @@ TREIBER = {
|
||||
"rueckweg_aapl.py": "rueckweg_aapl_angriffe.py",
|
||||
}
|
||||
|
||||
zerleger = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
# Seit Paket E9: nur die Mutanten, deren Name den Text enthaelt.
|
||||
zerleger.add_argument("--nur", help="nur Mutanten, deren Name diesen Text enthaelt")
|
||||
argumente = zerleger.parse_args()
|
||||
mutanten = WIN_MUTANTEN if sys.platform == "win32" else WSL_MUTANTEN
|
||||
if argumente.nur is not None:
|
||||
mutanten = [mutant for mutant in mutanten if argumente.nur in mutant[0]]
|
||||
if not mutanten:
|
||||
print(f"KEIN MUTANT mit {argumente.nur!r} im Namen")
|
||||
sys.exit(2)
|
||||
ueberlebt = []
|
||||
for name, datei, alt, neu in mutanten:
|
||||
# Ein fuenftes Element im Tupel geht als Argument an den Treiber (seit Paket E9).
|
||||
for name, datei, alt, neu, *treiber_argumente in mutanten:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ziel = Path(tmp) / "pruefung"
|
||||
shutil.copytree(HIER, ziel, ignore=shutil.ignore_patterns("__pycache__"))
|
||||
@@ -122,7 +207,7 @@ for name, datei, alt, neu in mutanten:
|
||||
ueberlebt.append(name)
|
||||
continue
|
||||
pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
|
||||
lauf = subprocess.run([sys.executable, str(ziel / TREIBER[datei])], capture_output=True, text=True,
|
||||
lauf = subprocess.run([sys.executable, str(ziel / TREIBER[datei]), *treiber_argumente], capture_output=True, text=True,
|
||||
timeout=1800, encoding="utf-8", errors="replace", cwd=tmp)
|
||||
faelle = [z for z in lauf.stdout.splitlines() if z.startswith("FEHL")]
|
||||
rot = lauf.returncode != 0
|
||||
|
||||
@@ -81,8 +81,36 @@ Seit Paket IB2 prueft --teil ibkr statt des Alpaca-Runners die IBKR-Instanz in i
|
||||
Nicht gesehen im Teil ibkr: ein Tagesbericht (die Instanz hat keine Tagesauswertung),
|
||||
handelstag_auswertung.py und der Dienst ibkr-gateway.service.
|
||||
|
||||
Seit Paket E9 prueft --sicherung <datei> fuer beide Teile die Sicherung des vorigen Handelstags auf
|
||||
dem Docker-Host. Die Datei schreibt abendkontrolle-host.sh vor jedem Lauf (absoluter Pfad, im
|
||||
Container /abend/sicherung.json); die Ablage selbst sieht das Werkzeug nicht. Die Pruefung laeuft
|
||||
nach dem Teil in einem eigenen Schritt, auch an einem Tag ohne Handel und nach einem Abbruch.
|
||||
E1. Ist der Stand lesbar? Projektvorgabe (Plan E9, Task 726): Datei fehlt, kein JSON, kein
|
||||
Objekt, ohne erster_tag oder eintraege -> Befund. Ein Schluessel
|
||||
fehler (Ablage fehlt, nicht lesbar, erster-tag kaputt, seit Paket NT
|
||||
auch eine unlesbare Summendatei oder Bestaetigung) -> Befund.
|
||||
E2. Ab wann wird geprueft? Projektvorgabe (S6, S8): erster_tag aus erster-tag der Ablage;
|
||||
null, kein Datum oder ein Tag nach --tag -> Befund, sonst
|
||||
schwiege die Pruefung auf Dauer; nur ein voriger Handelstag vor
|
||||
erster_tag -> nicht geprueft, still.
|
||||
E3. Welcher Tag? Quelle: derselbe Kalender wie Frage 1, GET /v2/calendar fuer die
|
||||
SICHERUNG_FENSTER_TAGE Kalendertage vor --tag, bis zum Vortag; der
|
||||
juengste Eintrag darin, unabhaengig von der Reihenfolge der Liste.
|
||||
Die Spanne setzt das Werkzeug selbst durch, fuer die Antwort wie
|
||||
fuer eine Kalenderdatei. Form wie Frage 1, dazu date als
|
||||
JJJJ-MM-TT, sonst Befund; keiner -> Befund.
|
||||
E4. Liegt die Sicherung? Projektvorgabe (Schnittstelle E9): eintraege["<tag>-<instanz>"],
|
||||
instanz nach --teil; fehlt der Eintrag -> Befund; summe nicht 64
|
||||
Hexzeichen in Kleinbuchstaben -> Befund; archiv nicht true (die
|
||||
Datei <name>.tar fehlt auf dem Host) -> Befund.
|
||||
E5. Ist sie nach D: geholt? Projektvorgabe (S4, S5): bestaetigt hat die Form von summe und ist
|
||||
ihr gleich, sonst Befund. Die Windows-Seite bestaetigt erst nach
|
||||
geprueftem Archiv, und abendkontrolle-lesen.sh nimmt nur die Summe
|
||||
des Hosts an.
|
||||
Nicht gesehen: ob die Kopie auf D: noch liegt und ob sie sich zuruecksichern laesst. Ein Werktag ohne
|
||||
Handel hat eine Sicherung, die niemand prueft.
|
||||
Ein Fangnetz, kein Nachweis ueber den Handelstag.
|
||||
Aufruf (WSL, Venv): python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung] [--teil ibkr]
|
||||
Aufruf (WSL, Venv): python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung] [--teil ibkr] [--sicherung <datei>]
|
||||
Nur eine Meldung schicken: python abendkontrolle_wsl.py --meldung "<titel>" "<text>"
|
||||
Fuer Angriffsfaelle ohne Netz und ohne systemd: --projekt, --kalender, --journal, --dienste,
|
||||
--ohne-broker, --ohne-kerzen; im Teil ibkr dazu --ibkr-ordner, --ibkr-konto (JSON mit exit und
|
||||
@@ -585,6 +613,142 @@ def kontrolliere_ibkr(argumente: argparse.Namespace, k: Kontrolle) -> None:
|
||||
pruefe_konto_ibkr(ergebnis[0], ergebnis[1], k)
|
||||
|
||||
|
||||
# --- Sicherung des vorigen Handelstags (seit Paket E9) ---------------------------------------------
|
||||
|
||||
# Wie weit der Kalender zurueck gefragt wird. Projektvorgabe (Plan E9, Task 726), nicht am Kalender
|
||||
# gemessen: Zehn Kalendertage decken ein langes Wochenende mit Feiertag und eine kurze
|
||||
# ausserplanmaessige Schliessung.
|
||||
SICHERUNG_FENSTER_TAGE = 10
|
||||
# Summe und Bestaetigung auf dem Host (Schnittstelle Paket E9) und ein Datum, nur ASCII-Ziffern.
|
||||
SUMME = re.compile(r"[0-9a-f]{64}")
|
||||
DATUM = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
|
||||
|
||||
|
||||
def in_spanne(liste, start: str, ende: str):
|
||||
"""Aus einer Liste des Kalenders nur die Eintraege, deren date von start bis ende reicht.
|
||||
|
||||
Es faellt nur ein Eintrag mit einem date JJJJ-MM-TT ausserhalb der Spanne weg; jeder andere
|
||||
bleibt, und eine Antwort, die keine Liste ist, kommt unveraendert zurueck. Die Form prueft
|
||||
juengster_handelstag.
|
||||
"""
|
||||
if not isinstance(liste, list):
|
||||
return liste
|
||||
return [
|
||||
eintrag
|
||||
for eintrag in liste
|
||||
if not isinstance(eintrag, dict)
|
||||
or not isinstance(eintrag.get("date"), str)
|
||||
or not DATUM.fullmatch(eintrag["date"])
|
||||
or start <= eintrag["date"] <= ende
|
||||
]
|
||||
|
||||
|
||||
def lies_kalender_spanne(start: str, ende: str, kalender_datei: str | None, projekt: Path):
|
||||
"""Die Liste aus /v2/calendar oder aus einer Datei (Angriffsfaelle), je nur von start bis ende.
|
||||
|
||||
Die Spanne setzt das Werkzeug fuer beide Quellen selbst durch (Plan E9, Plankritik Runde 1): So
|
||||
pinnen die Angriffsfaelle mit Kalenderdatei die Grenzen, mit denen es Alpaca fragt.
|
||||
"""
|
||||
if kalender_datei is not None:
|
||||
return in_spanne(json.loads(Path(kalender_datei).read_text(encoding="utf-8")), start, ende)
|
||||
import requests # erst hier: Die Angriffsfaelle laufen ohne Netz
|
||||
|
||||
sys.path.insert(0, str(projekt))
|
||||
sys.path.insert(0, str(AUSWERTUNG.parent))
|
||||
from handelstag_auswertung import kopfzeilen # liest die Schluessel, gibt sie nie aus
|
||||
|
||||
kopf = kopfzeilen()
|
||||
if kopf is None:
|
||||
raise RuntimeError("Schluessel fuer Alpaca nicht lesbar")
|
||||
antwort = requests.get(
|
||||
f"{HANDEL_URL}/v2/calendar", headers=kopf, params={"start": start, "end": ende}, timeout=30
|
||||
)
|
||||
antwort.raise_for_status()
|
||||
return in_spanne(antwort.json(), start, ende)
|
||||
|
||||
|
||||
def juengster_handelstag(liste) -> str | None:
|
||||
"""Der juengste Handelstag in der Liste, None, wenn keiner darin steht.
|
||||
|
||||
Die Liste reicht schon nur bis zum Tag vor dem geprueften (lies_kalender_spanne). Die Grenze steht
|
||||
allein dort, damit ein Angriffsfall sie pinnen kann. Die Reihenfolge der Liste zaehlt nicht. Hat die
|
||||
Liste nicht die gemessene Form (Frage 1) oder ein date nicht die Form JJJJ-MM-TT, ist das ein
|
||||
ValueError.
|
||||
"""
|
||||
if not isinstance(liste, list):
|
||||
raise ValueError("Kalender ist keine Liste")
|
||||
tage = []
|
||||
for eintrag in liste:
|
||||
datum = eintrag.get("date") if isinstance(eintrag, dict) else None
|
||||
if not isinstance(datum, str) or not DATUM.fullmatch(datum):
|
||||
raise ValueError("Kalender mit einem Eintrag ohne date JJJJ-MM-TT")
|
||||
tage.append(datum)
|
||||
return max(tage) if tage else None
|
||||
|
||||
|
||||
def pruefe_sicherung(argumente: argparse.Namespace, k: Kontrolle) -> None:
|
||||
"""Die Sicherung des vorigen Handelstags fuer den geprueften Teil (Paket E9, S6).
|
||||
|
||||
Liest nur die Datei aus --sicherung, nie die Ablage. Jede Abweichung ist ein Befund, auch ein
|
||||
fehlender erster Sicherungstag, einer nach dem geprueften Tag und ein fehlendes Archiv. Still
|
||||
bleibt nur ein voriger Handelstag vor dem ersten Sicherungstag.
|
||||
"""
|
||||
geprueft = date.fromisoformat(argumente.tag) # ein falsches Datum ist ein Bedienfehler
|
||||
tag = geprueft.isoformat()
|
||||
instanz = "ibkr" if argumente.teil == "ibkr" else "alpaca"
|
||||
stand = json.loads(Path(argumente.sicherung).read_text(encoding="utf-8"))
|
||||
if not isinstance(stand, dict):
|
||||
k.befund("Sicherung: Stand ist kein Objekt")
|
||||
return
|
||||
if "fehler" in stand:
|
||||
k.befund(f"Sicherung nicht geprueft: {str(stand['fehler'])[:160]}")
|
||||
return
|
||||
if "erster_tag" not in stand or not isinstance(stand.get("eintraege"), dict):
|
||||
k.befund("Sicherung: Stand in unerwarteter Form (erster_tag oder eintraege fehlt)")
|
||||
return
|
||||
erster = stand["erster_tag"]
|
||||
if erster is None:
|
||||
# Seit der Plankritik Runde 1 ein Befund: Ohne erster-tag schwiege die Pruefung auf Dauer.
|
||||
k.befund("Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung")
|
||||
return
|
||||
if not isinstance(erster, str) or not DATUM.fullmatch(erster):
|
||||
k.befund("Sicherung: erster_tag ist kein Datum JJJJ-MM-TT")
|
||||
return
|
||||
# Ein erster Sicherungstag nach dem geprueften Tag (etwa ein Tippfehler im Jahr) schaltete die
|
||||
# Pruefung sonst still ab, bis der Tag kommt. DATUM hat die Form gesichert, also vergleicht der
|
||||
# Text wie das Datum.
|
||||
if erster > tag:
|
||||
k.befund(f"Sicherung nicht geprueft: erster_tag {erster} liegt nach dem geprueften Tag {tag}")
|
||||
return
|
||||
start = (geprueft - timedelta(days=SICHERUNG_FENSTER_TAGE)).isoformat()
|
||||
ende = (geprueft - timedelta(days=1)).isoformat()
|
||||
vorher = juengster_handelstag(lies_kalender_spanne(start, ende, argumente.kalender, Path(argumente.projekt)))
|
||||
if vorher is None:
|
||||
k.befund(f"Sicherung: kein Handelstag in den {SICHERUNG_FENSTER_TAGE} Tagen vor {tag} im Kalender")
|
||||
return
|
||||
name = f"{vorher}-{instanz}"
|
||||
k.fakten["sicherung"] = {"name": name, "erster_tag": erster, "geprueft": False}
|
||||
# Still bleibt nur das: ein voriger Handelstag vor dem ersten Sicherungstag (die ersten Tage nach
|
||||
# dem Einspielen).
|
||||
if vorher < erster:
|
||||
return
|
||||
k.fakten["sicherung"]["geprueft"] = True
|
||||
eintrag = stand["eintraege"].get(name)
|
||||
if not isinstance(eintrag, dict):
|
||||
k.befund(f"Sicherung {name} fehlt auf dem Docker-Host")
|
||||
return
|
||||
summe = eintrag.get("summe")
|
||||
bestaetigt = eintrag.get("bestaetigt")
|
||||
if not isinstance(summe, str) or not SUMME.fullmatch(summe):
|
||||
k.befund(f"Sicherung {name}: keine gueltige Summe auf dem Docker-Host")
|
||||
elif eintrag.get("archiv") is not True:
|
||||
k.befund(f"Sicherung {name}: Archiv fehlt auf dem Docker-Host")
|
||||
elif not isinstance(bestaetigt, str) or not SUMME.fullmatch(bestaetigt):
|
||||
k.befund(f"Sicherung {name}: nicht bestaetigt, also nicht nach D: geholt")
|
||||
elif bestaetigt != summe:
|
||||
k.befund(f"Sicherung {name}: die Bestaetigung passt nicht zur Summe auf dem Docker-Host")
|
||||
|
||||
|
||||
# --- Meldung ---------------------------------------------------------------------------------------
|
||||
|
||||
def melde(titel: str, text: str, projekt: Path) -> str:
|
||||
@@ -678,6 +842,8 @@ def parse_args(argv=None) -> argparse.Namespace:
|
||||
# Nur fuer die Angriffsfaelle: Exit und Ausgabe von ibkr_pruefen.py als JSON-Datei, und die Uhr.
|
||||
zerleger.add_argument("--ibkr-konto")
|
||||
zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt mit Versatz statt der Uhr")
|
||||
# Seit Paket E9 (S6): der Stand der Sicherungsablage des Hosts als JSON-Datei (abendkontrolle-host.sh).
|
||||
zerleger.add_argument("--sicherung")
|
||||
argumente = zerleger.parse_args(argv)
|
||||
if bool(argumente.tag) == bool(argumente.meldung):
|
||||
zerleger.error("genau eines von --tag und --meldung")
|
||||
@@ -702,6 +868,9 @@ def main(argv=None) -> int:
|
||||
except Exception as ausnahme:
|
||||
k.befund(f"Abendkontrolle abgebrochen: {type(ausnahme).__name__}: {ausnahme}")
|
||||
code = 2
|
||||
# Seit Paket E9 (S6): die Sicherung des vorigen Handelstags, fuer sich und vor der Meldung.
|
||||
if argumente.sicherung:
|
||||
k.schritt("Sicherung", pruefe_sicherung, argumente, k)
|
||||
if k.befunde and not argumente.ohne_meldung:
|
||||
text = "\n".join(f"- {befund}" for befund in k.befunde[:12])
|
||||
titel = f"Abendkontrolle {argumente.tag}: {len(k.befunde)} Befund(e)"
|
||||
|
||||
Reference in new issue
Block a user