Automatischer Push: 46 Datei(en) geändert
- .claude/agents/planer.md - .claude/skills/plan-executer-pruefung/SKILL.md - .claude/workflows/plankritik.js - .claude/workflows/planung.js - .claude/workflows/pruefung.js - .claude/workflows/verbrauch.js - .claude/workflows/welle-mit-pruefung.js - .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/plan_zeichen.py - .vault-tools/plan-kopie/rot_nachweis.py - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/abendkontrolle.py - … und 34 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
67d674cd0b
commit
ea278dc24f
46 files changed
+8403
-80
No files matched your search
@@ -5,7 +5,7 @@ title: "Trading Plattform: Prüfwerkzeuge des Koordinators fürs Dashboard"
|
||||
summary: "Eigener Lauf mit echten Daten zum Dashboard der Trading Plattform — Vergleich der Endpunkte gegen Vollparse und pandas, zwei WebSocket-Verbindungen, Browser-Lauf und Wiederaufbau mit Playwright, gefahrlose Proben der zwei Skripte, Mutantenläufe. Entstanden 2026-09-17 (Teil 3b), erweitert 2026-09-18 (Teil 10: Anmeldung, Angriffsfälle, Beobachtung, Kerzenlatenz), bewusst nicht im Projekt-Repo."
|
||||
tags: [vault-tools, trading-plattform]
|
||||
created: 2026-09-17
|
||||
updated: 2026-09-21
|
||||
updated: 2026-09-24
|
||||
---
|
||||
|
||||
# Prüfwerkzeuge fürs Dashboard der Trading Plattform
|
||||
@@ -103,3 +103,43 @@ Die Werkzeuge nennen `config/secrets.toml` nicht mehr fest. Wer eine Projektklas
|
||||
|---|---|---|
|
||||
| `handelstag_auswertung.py` | seit Paket M mit mehreren Instrumenten: Instrumente aus dem Protokollkopf; je Instrument Entscheidungen, Orders und die aus der Folge erwartete Zahl (Stufen, Versatz, Entscheidungen, Schlussbein, ohne geteilte Beine), erreichte Positionen und Takte mit frischer Kerze; groesste Zahl von Orders in einem Takt, Ereignisse je Art, Positionen je Instrument am Ende, BEFUND fuer jede nicht gefuellte Orderzeile; das Taktfeld orders gilt als Orders dieses Takts; `--broker` und `--kerzen` fragen alle Symbole des Kopfs, jede Order gegen die Kerze ihres Symbols | wie bisher: `python handelstag_auswertung.py <protokoll> [--broker] [--kerzen]` aus dem Projektordner |
|
||||
| `m_probeaufruf.py` | lesender Probeaufruf vor dem Einspielen: `GET /v2/assets` roh (Schluessel und Typen) und ueber `AlpacaBroker.anlage`, `trades/latest` mit allen Symbolen roh und ueber `letzte_kurse`, Kerzen gebuendelt gegen einzeln, Seitenverhalten mit kleiner Seite; Exit 0 nur, wenn alles wie angenommen ist und keine Aktie die Startpruefung einer Folge mit Short verfehlt | aus einem Arbeitsbaum mit Paket M in WSL: `python m_probeaufruf.py <handelstag> <kennung> ...` |
|
||||
|
||||
## Betrieb ohne täglichen Klick (2026-09-24): Abendkontrolle und Rückweg
|
||||
|
||||
Pascal am 2026-09-24: eine dauerhafte Abendkontrolle, die Neustarts übersteht und keine Updates von Claude Code oder Desktop blockiert, und zehn Aktien bis Mittwoch 2026-09-30. Beides läuft als Windows-Aufgabe mit `pythonw.exe`, ohne Claude-Prozess.
|
||||
|
||||
| Datei | Was sie tut | Aufruf |
|
||||
|---|---|---|
|
||||
| `abendkontrolle_wsl.py` | liest einen Handelstag und urteilt im Code: Handelstag laut `GET /v2/calendar`, Protokoll des scharfen Runners, letzte Zeile „Ende: …, Exit n" im Journal (jeder Exit ungleich 0 ist ein Befund), BEFUND-Zeilen von `handelstag_auswertung.py --broker --kerzen`, offene Positionen beim Broker, mindestens ein Takt `aktiv`, Tagesbericht, Dashboard und Zeitgeber, Killswitch nicht `gesperrt`; bei Befund eine Meldung mit hoher Priorität über den Melder des Projekts. Gibt eine JSON-Zeile aus, nie Beträge | in WSL: `python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung]`; nur eine Meldung: `--meldung TITEL TEXT` |
|
||||
| `abendkontrolle.py` | Windows-Seite: prüft jeden Werktag seit 2026-09-24 ohne Ergebnisdatei (höchstens sieben Tage zurück, heute erst ab 22:30), legt das Ergebnis unter `data/paper/abendkontrollen/<tag>.json` im Projekt ab und schreibt eine Zeile in `Inbox/<tag>.md` unter „Agentenprotokoll", bei Befund mit `#needs-review` | `python abendkontrolle.py [--tag JJJJ-MM-TT]` |
|
||||
| `abendkontrolle_einrichten.ps1` | Aufgabe „Trading Plattform - Abendkontrolle": werktags 22:45 und fünf Minuten nach jeder Anmeldung, nur angemeldet, verpasste Läufe beim nächsten Start | `powershell -NoProfile -File abendkontrolle_einrichten.ps1 [-Aktion entfernen]` |
|
||||
| `abendkontrolle_angriffe.py`, `abendkontrolle_ersatz_auswertung.py` | 44 Fälle in WSL (Kalender, Protokoll, Ende je Lauf samt mittags abgebrochenem und abendlichem Lauf, Journal, Auswertung, Broker, Maske, Bericht, Dienste, Killswitch, getrennte Fragen, Meldeweg gegen `ntfy_probe_server.py` und gegen einen toten Port, Bedienfehler; Reihenfolge an zwei Positionen) und 20 unter Windows (Notiz, Überschrift mit Zusatz, CRLF, kein UTF-8, Ergebnisform, Abbruch wird wiederholt, keine Nachbardatei, fällige Tage), ohne Netz. Am 2026-09-24 nach der unabhängigen Prüfung: 0 Abweichungen | `python abendkontrolle_angriffe.py` einmal in WSL (Venv), einmal unter Windows |
|
||||
| `rueckweg_aapl.py`, `rueckweg_aapl_einrichten.ps1` | einmal: `config/paper-aapl-register.toml` wird `config/paper.toml`, lokal committet (nicht gepusht) — nur nach 2026-09-30 22:05, bei stehendem Runner, sauberem Projektordner mit `master`, wenn dort noch die Konfiguration mit `quelle = "folge"` liegt und beide Dateien ausser `[agent]`, den Instrumenten und dem Tagesverlust dieselben Schluessel und Werte tragen, in beide Richtungen verglichen (sonst schaltete der Tausch etwa ein abgeschaltetes `[auto_fortsetzen]` still wieder ein); ein halb erledigter Tausch wird zu Ende committet. Sonst Befund in der Notiz und eine Meldung. Aufgabe „Trading Plattform - Rueckweg AAPL" am 2026-09-30 22:50, zweiter Versuch 2026-10-01 22:10 | `python rueckweg_aapl.py`; Einrichten wie oben |
|
||||
| `rueckweg_aapl_angriffe.py` | 22 Fälle auf einem Wegwerf-Git-Repo (dazu halb getauscht, Abschnitt nur in `paper.toml`, Automatik nur dort abgeschaltet, Abschnitt nur dort gelöscht, anderer Wert, anderer Zweig); am 2026-09-24 0 Abweichungen | `python rueckweg_aapl_angriffe.py` unter Windows |
|
||||
| `abendkontrolle_mutanten.py` | baut je einen Fehler in eine Wegwerfkopie und verlangt, dass der Angriffstreiber rot wird: 16 Mutanten von `abendkontrolle_wsl.py` (in WSL), 9 von `abendkontrolle.py` und 12 von `rueckweg_aapl.py` (unter Windows). Am 2026-09-24: 37 von 37 gefangen | `python abendkontrolle_mutanten.py` einmal in WSL, einmal unter Windows |
|
||||
|
||||
## Einspielen (2026-09-24): Werkzeuge aus dem Scratchpad hierher
|
||||
|
||||
| Datei | Was sie tut | Aufruf |
|
||||
|---|---|---|
|
||||
| `dashboard_neustart.sh` | `sudo systemctl restart trading-dashboard.service` nur bei stehendem Runner, danach Tor 6b (Journal der letzten Minute ohne `PermissionError`/`Traceback`, sonst Exit 27), `/api/gesund` und die Oberfläche. Pascals dauerhafte Freigabe gilt für genau diesen Neustart außerhalb der Handelszeit | mit `WSL_PW` per WSLENV: `wsl -d Debian -u claude -- bash …/dashboard_neustart.sh` |
|
||||
| `geheimnis_scan.py` | vor jedem Push: Werte ab 12 Zeichen aus Geheimnis- und Zugangsdatei gegen `git log -p <von>..<bis>`, nur Schlüsselnamen und Trefferzahlen; dazu, ob eine der drei Geheimnisdateien verfolgt wird. Regressionsnachweis mit `SCAN_PROBE=<bekannter Text>` (am 2026-09-24: 66 Treffer für einen bekannten Text, 0 für die echten Werte) | in WSL: `python geheimnis_scan.py <repo> <von> <bis> [<bis> …]` |
|
||||
|
||||
## IBKR-Zugang (2026-09-24): Paper-Konto über das Gateway im Container
|
||||
|
||||
Pascal am 2026-09-24: Zugang zum IBKR-Paper-Konto vorbereiten, Gateway als Container in WSL, erster Test nach 22:00. Nichts davon handelt.
|
||||
|
||||
| Datei | Was sie tut | Aufruf |
|
||||
|---|---|---|
|
||||
| `ibkr_zugang_eintragen.sh` | Pascal trägt Paper-Benutzername, Passwort (ohne Anzeige, zweimal) und Kontonummer ein; Ablage `/home/claude/.config/trading-plattform/ibkr/` (0700/0600); ausgegeben werden nur Längen. Mit erfundenen Werten in einem Wegwerf-Ordner geprüft (`IBKR_ZIEL`) | `wsl -d Debian -u claude -- bash …/ibkr_zugang_eintragen.sh` |
|
||||
| `ibkr_gateway_test.sh` | Gateway-Container `ghcr.io/gnzsnz/ib-gateway` (auf eine Prüfsumme festgelegt), Paper, `READ_ONLY_API=yes`, Port nur auf `127.0.0.1:4004`; Schritte `laden`, `pruefen`, `starten`, `log` (Benutzername maskiert), `probe`, `stoppen`; verweigert zwischen 15 und 22 Uhr Berlin | mit `WSL_PW` per WSLENV: `wsl -d Debian -u claude -- bash …/ibkr_gateway_test.sh <schritt>` |
|
||||
| `ibkr_probe.py` | lesende Probe über `nautilus-ibapi` im eigenen Venv `~/venvs/ibkr-probe`: Verbindung, Konto (nur Anfang und Länge), Kontoart, Währung, Positionen (Zahl), verzögerter AAPL-Kurs (nur die Kursart), Fehlercodes; nie ein Betrag. Ohne Gateway geprüft: „keine Verbindung", Code 502, Exit 3 | `~/venvs/ibkr-probe/bin/python …/ibkr_probe.py [--port 4004]` |
|
||||
|
||||
## Paket AF (2026-09-24): Fortsetzen zum Tagesstart ohne Klick
|
||||
|
||||
| Datei | Was sie tut | Aufruf |
|
||||
|---|---|---|
|
||||
| `af_gegenfall.py` | faehrt die echte PaperSitzung eines Arbeitsbaums mit AF und der ausgelieferten `config/paper.toml` gegen einen ProbeBroker, Kerzen aus einer Datei von `hole_kerzen.py` fuer jede Aktie, Wegwerf-Register mit der Neustartregel, ohne Netz; der Fall `restposition` (10 Stueck in der letzten Aktie) und der Fall `mitten` (Start 60 Marktminuten nach der Oeffnung) muessen `nicht_fortgesetzt` mit dem Grund in Protokoll und Meldung zeigen, der Fall `flach` ist die Gegenprobe mit `fortgesetzt`, Ausloeser `automatik` und Orders; Exit 0 nur ohne BEFUND | aus dem Arbeitsbaum in WSL: `python af_gegenfall.py <kerzen.json> <fall>`, Fall `restposition`, `mitten` oder `flach`; die Kerzen vorher einmal lesend mit `hole_kerzen.py <tag> <kerzen.json>` |
|
||||
| `af_registerprobe.py` | fragt die echte Funktion `pruefe_register` (Paket AF) gegen `data/registry.db`, nur lesend (`mode=ro`; die Klasse `Registry` fuehrte beim Oeffnen das Schema aus und schriebe); druckt Zustand, die vier juengsten Killswitch-Ereignisse (Stufe, Ausloeser, Zeit) und den Befund; Exit 0 erfuellt, 1 nicht erfuellt, 3 Register fehlt. Belegt am 2026-09-24 an drei gespielten Registern (bereit / Pascals Anhalten ohne Fortsetzen / aktiv) und an einer Kopie des echten; die mtime des echten blieb gleich. Braucht den Code mit AF im genannten Ordner | in WSL im Projektordner nach dem Dashboard-Neustart: `~/venvs/trading/bin/python af_registerprobe.py .` |
|
||||
|
||||
Was die uebrigen Werkzeuge mit AF sehen: `handelstag_auswertung.py` braucht keine Aenderung, es zaehlt das Ereignis `auto_fortsetzen` unter `ereignisse je art` und druckt seinen Text. `abendkontrolle_wsl.py` liest Zustand, Seit und Ausloeser des Killswitch und berichtet jede Zeile `auto_fortsetzen` als Hinweis (die ersten 200 Zeichen des Feldes text, das mit dem Ergebnis beginnt). `neustart_echtlauf.py` setzt `angehalten`/`neustart` ohne Ereignis, `pause_echtlauf.py` einen Ausloeser `echtlauf`: Die Sitzung setzt dort nicht fort, schreibt aber je Lauf eine Zeile `auto_fortsetzen` und in "Paper-Runner wartet" eine Zeile mehr. `generalprobe_echtlauf.py` und `killswitch_simulation.py` laden `config/paper.toml` und klicken nach Plan; gegen einen Baum mit AF setzt die Sitzung dort mit dem Schalter selbst fort, bevor der geplante Klick kommt. Wer diese vier fuer einen Vergleich mit Paket M faehrt, setzt vorher `auto_fortsetzen=False` in ihr `replace(lade_paper_config(...), ...)`; `neustart_echtlauf.py` und `pause_echtlauf.py` rufen `lade_paper_config` ohne `replace` auf — dort in der Konfiguration ihrer Kopie `[auto_fortsetzen] aktiv = false` setzen.
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Startet die Abendkontrolle des Paper-Betriebs und schreibt ihr Ergebnis in die Tagesnotiz.
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Laeuft unter Windows mit pythonw.exe als geplante Aufgabe
|
||||
"Trading Plattform - Abendkontrolle" (abendkontrolle_einrichten.ps1): werktags 22:45 und fuenf Minuten
|
||||
nach jeder Anmeldung. Auftrag Pascal 2026-09-24: dauerhaft, uebersteht Neustarts, blockiert keine
|
||||
Updates -- deshalb eine Windows-Aufgabe (bleibt ueber Neustarts, holt verpasste Laeufe nach) und kein
|
||||
Claude-Prozess (haelt keine Datei von Claude Code oder Desktop offen).
|
||||
|
||||
Ablauf: Jeder Werktag seit ERSTER_TAG, hoechstens NACHHOLEN_TAGE zurueck, ohne Ergebnisdatei in
|
||||
ABLAGE wird geprueft; der heutige erst ab FRUEHESTENS (der Bericht der Tagesauswertung kommt 22:20).
|
||||
Je Tag: abendkontrolle_wsl.py in WSL (liest nur, meldet bei Befund selbst ueber ntfy), dann zuerst das
|
||||
Ergebnis als JSON nach ABLAGE und danach eine Zeile in Inbox/<tag>.md unter der ersten Ueberschrift,
|
||||
die mit "## Agentenprotokoll" beginnt.
|
||||
- Liefert der WSL-Teil kein Ergebnis oder endet er mit Exit 2 (abgebrochen), bleibt die Ergebnisdatei
|
||||
aus, die Notiz bekommt einen Befund mit #needs-review, und der naechste Lauf versucht den Tag erneut.
|
||||
- Scheitert nur die Notiz (etwa eine Datei, die kein UTF-8 ist), bleibt es bei der Ergebnisdatei und
|
||||
einer Zeile in lauf.log: Die Kontrolle ist gelaufen und hat ihre Meldung schon geschickt; ein zweiter
|
||||
Lauf schickte sie nur noch einmal.
|
||||
- Ein Tag, der laenger als NACHHOLEN_TAGE scheitert, faellt aus dem Fenster (lauf.log behaelt ihn).
|
||||
Die Notiz wird ueber eine Nachbardatei und os.replace geschrieben: Ein Abbruch mitten im Schreiben
|
||||
hinterlaesst die alte Datei, nicht eine halbe. Gegen eine Claude-Sitzung, die dieselbe Notiz im selben
|
||||
Augenblick schreibt, schuetzt das nicht (Zeitfenster Lesen bis Ersetzen, Millisekunden) -- benannt.
|
||||
|
||||
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.
|
||||
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
from datetime import time
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
VAULT = Path(r"D:\Claude")
|
||||
PROJEKT_TEIL = Path("Code Projekte") / "active - Trading Plattform"
|
||||
ABLAGE_TEIL = PROJEKT_TEIL / "data" / "paper" / "abendkontrollen"
|
||||
WSL = r"C:\Windows\System32\wsl.exe"
|
||||
WSL_PY = "/home/claude/venvs/trading/bin/python"
|
||||
WSL_SKRIPT = "/mnt/d/Claude/.vault-tools/trading-plattform-pruefung/abendkontrolle_wsl.py"
|
||||
ERSTER_TAG = date(2026, 9, 24)
|
||||
NACHHOLEN_TAGE = 7
|
||||
FRUEHESTENS = time(22, 30)
|
||||
WSL_TIMEOUT_S = 1200
|
||||
KEIN_FENSTER = 0x08000000 # CREATE_NO_WINDOW: pythonw hat keine Konsole, wsl.exe soll keine oeffnen
|
||||
UEBERSCHRIFT = "## Agentenprotokoll"
|
||||
|
||||
|
||||
def faellige_tage(jetzt: datetime, ablage: Path) -> list[str]:
|
||||
tage = []
|
||||
for zurueck in range(NACHHOLEN_TAGE, -1, -1):
|
||||
tag = jetzt.date() - timedelta(days=zurueck)
|
||||
if tag < ERSTER_TAG or tag.weekday() >= 5:
|
||||
continue
|
||||
if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:
|
||||
continue
|
||||
if (ablage / f"{tag.isoformat()}.json").exists():
|
||||
continue
|
||||
tage.append(tag.isoformat())
|
||||
return tage
|
||||
|
||||
|
||||
def fahre_wsl(tag: str) -> dict:
|
||||
lauf = subprocess.run(
|
||||
[WSL, "-d", "Debian", "-u", "claude", "--", WSL_PY, WSL_SKRIPT, "--tag", tag],
|
||||
capture_output=True, timeout=WSL_TIMEOUT_S, creationflags=KEIN_FENSTER,
|
||||
)
|
||||
ausgabe = lauf.stdout.decode("utf-8", errors="replace")
|
||||
for zeile in reversed(ausgabe.splitlines()):
|
||||
zeile = zeile.strip()
|
||||
if zeile.startswith("{"):
|
||||
try:
|
||||
return json.loads(zeile)
|
||||
except json.JSONDecodeError:
|
||||
break
|
||||
fehler = lauf.stderr.decode("utf-8", errors="replace").strip().splitlines()[-3:]
|
||||
raise RuntimeError(f"keine JSON-Zeile vom WSL-Teil (Exit {lauf.returncode}; {' | '.join(fehler)[:300]})")
|
||||
|
||||
|
||||
def zeile_fuer(ergebnis: dict, uhrzeit: str) -> str:
|
||||
kopf = f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch)"
|
||||
befunde = ergebnis.get("befunde") or []
|
||||
fakten = ergebnis.get("fakten") or {}
|
||||
hinweise = ergebnis.get("hinweise") or []
|
||||
if fakten.get("handelstag") is False and not befunde:
|
||||
return f"{kopf}: kein Handelstag laut Alpaca-Kalender."
|
||||
teile = []
|
||||
ende = fakten.get("ende") or {}
|
||||
if ende:
|
||||
teile.append(
|
||||
f"Ende {ende.get('grund')}, Exit {ende.get('exit')}, Orders {ende.get('orders')}, "
|
||||
f"Position {ende.get('position')}"
|
||||
)
|
||||
laeufe = fakten.get("laeufe") or []
|
||||
if laeufe:
|
||||
teile.append(f"{len(laeufe)} Lauf/Läufe, {sum(l.get('takte_aktiv', 0) for l in laeufe)} Takte aktiv")
|
||||
if "broker_offen" in fakten:
|
||||
teile.append(f"offene Positionen beim Broker {fakten['broker_offen']}")
|
||||
if "bericht" in fakten:
|
||||
teile.append("Tagesbericht da" if fakten["bericht"] else "Tagesbericht fehlt")
|
||||
killswitch = fakten.get("killswitch") or {}
|
||||
if killswitch:
|
||||
teile.append(f"Killswitch {killswitch.get('zustand')} ({killswitch.get('ausloeser')})")
|
||||
zusammen = "; ".join(teile)
|
||||
if not befunde:
|
||||
text = f"{kopf}: ohne Befund. {zusammen}."
|
||||
else:
|
||||
text = f"{kopf}: **{len(befunde)} Befund(e)** #needs-review. {zusammen}."
|
||||
text += "".join(f"\n - {befund}" for befund in befunde)
|
||||
if fakten.get("meldung"):
|
||||
text += f"\n - Meldung ans Telefon: {fakten['meldung']}"
|
||||
text += "".join(f"\n - Hinweis: {hinweis}" for hinweis in hinweise[:5])
|
||||
return text
|
||||
|
||||
|
||||
def schreibe_notiz(vault: Path, tag: str, zeile: str) -> None:
|
||||
notiz = vault / "Inbox" / f"{tag}.md"
|
||||
if notiz.exists():
|
||||
text = notiz.read_bytes().decode("utf-8") # kein UTF-8: Ausnahme, die Notiz bleibt unberuehrt
|
||||
else:
|
||||
text = (
|
||||
f"---\ntype: daily\ntitle: \"{tag}\"\nstatus: open\nsummary: \"Tagesnotiz vom {tag}.\"\n"
|
||||
f"tags: [daily]\ncreated: {tag}\nupdated: {tag}\n---\n\n# {tag}\n"
|
||||
)
|
||||
# Zeilenenden der Datei bleiben, wie sie sind; gesucht wird ohne \r.
|
||||
zeilen = text.split("\n")
|
||||
bereinigt = [z.rstrip("\r").rstrip() for z in zeilen]
|
||||
cr = "\r" if any(z.endswith("\r") for z in zeilen) else ""
|
||||
# Die erste Ueberschrift, die mit "## Agentenprotokoll" beginnt ("## Agentenprotokoll — …" kommt vor).
|
||||
start = next((i for i, z in enumerate(bereinigt) if z == UEBERSCHRIFT or z.startswith(UEBERSCHRIFT + " ")), None)
|
||||
if start is None:
|
||||
nl = cr + "\n"
|
||||
text = text.rstrip("\r\n") + nl + nl + UEBERSCHRIFT + nl + nl + nl.join(zeile.split("\n")) + nl
|
||||
else:
|
||||
ende = next((i for i in range(start + 1, len(zeilen)) if bereinigt[i].startswith("## ")), len(zeilen))
|
||||
# Hinter den letzten nicht leeren Eintrag des Abschnitts; die Leerzeilen danach ersetzt eine.
|
||||
einfuegen = ende
|
||||
while einfuegen > start + 1 and not bereinigt[einfuegen - 1]:
|
||||
einfuegen -= 1
|
||||
block = [z + cr for z in zeile.split("\n")]
|
||||
if einfuegen == start + 1:
|
||||
block = [cr] + block
|
||||
zeilen[einfuegen:ende] = block + ([cr] if ende < len(zeilen) else [""])
|
||||
text = "\n".join(zeilen)
|
||||
if not text.endswith("\n"):
|
||||
text += "\n"
|
||||
neu = notiz.with_name(notiz.name + ".abendkontrolle.tmp")
|
||||
neu.write_bytes(text.encode("utf-8"))
|
||||
os.replace(neu, notiz)
|
||||
|
||||
|
||||
def logge(ablage: Path, text: str) -> None:
|
||||
ablage.mkdir(parents=True, exist_ok=True)
|
||||
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as datei:
|
||||
datei.write(f"{datetime.now().isoformat(timespec='seconds')} {text}\n")
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
zerleger = argparse.ArgumentParser(allow_abbrev=False)
|
||||
zerleger.add_argument("--tag")
|
||||
zerleger.add_argument("--vault", default=str(VAULT))
|
||||
zerleger.add_argument("--ohne-wsl", help="Ergebnis aus einer JSON-Datei statt aus WSL (Angriffsfaelle)")
|
||||
zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt statt der Uhr (Angriffsfaelle)")
|
||||
argumente = zerleger.parse_args(argv)
|
||||
vault = Path(argumente.vault)
|
||||
ablage = vault / ABLAGE_TEIL
|
||||
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
|
||||
tage = [argumente.tag] if argumente.tag else faellige_tage(jetzt, ablage)
|
||||
logge(ablage, f"start, faellig: {tage}")
|
||||
code = 0
|
||||
for tag in tage:
|
||||
uhrzeit = jetzt.strftime("%H:%M") if tag == jetzt.date().isoformat() else f"{jetzt:%Y-%m-%d %H:%M}"
|
||||
try:
|
||||
if argumente.ohne_wsl:
|
||||
ergebnis = json.loads(Path(argumente.ohne_wsl).read_text(encoding="utf-8"))
|
||||
else:
|
||||
ergebnis = fahre_wsl(tag)
|
||||
if not isinstance(ergebnis, dict) or not isinstance(ergebnis.get("befunde"), list):
|
||||
raise RuntimeError("Ergebnis des WSL-Teils in unerwarteter Form")
|
||||
except Exception as ausnahme:
|
||||
code = 1
|
||||
logge(ablage, f"{tag}: gescheitert {ausnahme!r}\n{traceback.format_exc()}")
|
||||
try:
|
||||
schreibe_notiz(
|
||||
vault, tag,
|
||||
f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch): **gescheitert** "
|
||||
f"({type(ausnahme).__name__}: {str(ausnahme)[:200]}) #needs-review. Der nächste Lauf versucht es erneut.",
|
||||
)
|
||||
except Exception as zweite:
|
||||
logge(ablage, f"{tag}: auch die Notiz scheiterte {zweite!r}")
|
||||
continue
|
||||
# Ein Abbruch im WSL-Teil (Exit 2) wird wiederholt; alles andere gilt als gelaufen.
|
||||
if ergebnis.get("exit") != 2:
|
||||
(ablage / f"{tag}.json").write_text(
|
||||
json.dumps(ergebnis, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n"
|
||||
)
|
||||
else:
|
||||
code = 1
|
||||
logge(ablage, f"{tag}: {len(ergebnis['befunde'])} Befund(e), exit {ergebnis.get('exit')}")
|
||||
try:
|
||||
schreibe_notiz(vault, tag, zeile_fuer(ergebnis, uhrzeit))
|
||||
except Exception as ausnahme:
|
||||
code = 1
|
||||
logge(ablage, f"{tag}: Notiz nicht geschrieben {ausnahme!r}")
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,406 @@
|
||||
"""Angriffsfaelle fuer die Abendkontrolle (abendkontrolle_wsl.py in WSL, abendkontrolle.py unter Windows).
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Jeder Fall laeuft einzeln auf einer Wegwerfkopie, zuerst die
|
||||
Grundlinie; ohne Netz (--ohne-broker/--ohne-kerzen, Kalender und Journal aus Dateien, eine Ersatz-
|
||||
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
|
||||
Exit 0, wenn jeder Fall so ausgeht wie erwartet; sonst 1 mit der Liste der Abweichungen.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
TAG = "2026-09-23"
|
||||
ECHTES_PROJEKT = Path("/mnt/d/Claude/Code Projekte/active - Trading Plattform")
|
||||
ECHTES_PROTOKOLL = ECHTES_PROJEKT / "artifacts" / "paper" / "2026-09-23T131023-paper.jsonl"
|
||||
KALENDER_TAG = [{"date": TAG, "open": "09:30", "close": "16:00"}]
|
||||
JOURNAL_GUT = (
|
||||
"Ende: boersenschluss, Position 0, Orders 2, Killswitch aktiv, Exit 0\n"
|
||||
"trading-paper.service: Deactivated successfully.\n"
|
||||
)
|
||||
DIENSTE_GUT = {
|
||||
"trading-dashboard.service": "active",
|
||||
"trading-paper.timer": "active",
|
||||
"trading-auswertung.timer": "active",
|
||||
}
|
||||
abweichungen: list[str] = []
|
||||
|
||||
|
||||
# --- WSL-Teil ----------------------------------------------------------------------------------
|
||||
|
||||
def baue_projekt(wurzel: Path, killswitch: str | None = "aktiv") -> Path:
|
||||
projekt = wurzel / "projekt"
|
||||
(projekt / "artifacts" / "paper").mkdir(parents=True)
|
||||
(projekt / "data" / "paper" / "auswertungen").mkdir(parents=True)
|
||||
(projekt / "config").mkdir()
|
||||
# Die Auswertung importiert trading.geheimnisse aus dem Arbeitsverzeichnis.
|
||||
(projekt / "trading").symlink_to(ECHTES_PROJEKT / "trading")
|
||||
shutil.copyfile(ECHTES_PROTOKOLL, projekt / "artifacts" / "paper" / f"{TAG}T131023-paper.jsonl")
|
||||
(projekt / "data" / "paper" / "auswertungen" / f"{TAG}.md").write_text("# Bericht\n", encoding="utf-8")
|
||||
with sqlite3.connect(projekt / "data" / "registry.db") as db:
|
||||
db.execute("CREATE TABLE killswitch_zustand (id INTEGER PRIMARY KEY, zustand TEXT, seit TEXT, ausloeser TEXT)")
|
||||
if killswitch is not None:
|
||||
db.execute("INSERT INTO killswitch_zustand VALUES (1, ?, '2026-09-23T13:31:00+00:00', 'pascal')", (killswitch,))
|
||||
(wurzel / "kalender.json").write_text(json.dumps(KALENDER_TAG), encoding="utf-8")
|
||||
(wurzel / "journal.txt").write_text(JOURNAL_GUT, encoding="utf-8")
|
||||
(wurzel / "dienste.json").write_text(json.dumps(DIENSTE_GUT), encoding="utf-8")
|
||||
return projekt
|
||||
|
||||
|
||||
def fahre(wurzel: Path, projekt: Path, extra_env: dict | None = None, tag: str = TAG, meldung: bool = False) -> dict:
|
||||
befehl = [
|
||||
sys.executable, str(HIER / "abendkontrolle_wsl.py"), "--tag", tag, "--projekt", str(projekt),
|
||||
"--kalender", str(wurzel / "kalender.json"), "--journal", str(wurzel / "journal.txt"),
|
||||
"--dienste", str(wurzel / "dienste.json"), "--ohne-kerzen",
|
||||
]
|
||||
if not meldung:
|
||||
befehl.append("--ohne-meldung")
|
||||
# Mit der Ersatz-Auswertung darf die Brokerzeile geprueft werden: Der Ersatz geht nicht ans Netz.
|
||||
if not (extra_env and "ABENDKONTROLLE_AUSWERTUNG" in extra_env):
|
||||
befehl.append("--ohne-broker")
|
||||
env = {k: v for k, v in os.environ.items() if k != "ABENDKONTROLLE_AUSWERTUNG"}
|
||||
env.update(extra_env or {})
|
||||
lauf = subprocess.run(befehl, capture_output=True, text=True, env=env, 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 erwarte(name: str, ergebnis: dict, *, befund: str | None, anzahl: int | None = None, exit_code: int = 0,
|
||||
auch: tuple = (), nicht: tuple = (), fakt=None) -> None:
|
||||
befunde = ergebnis.get("befunde", [])
|
||||
alles = " | ".join(befunde)
|
||||
ok = True
|
||||
if befund is None and befunde:
|
||||
ok = False
|
||||
if befund is not None and not any(befund in b for b in befunde):
|
||||
ok = False
|
||||
if any(teil not in alles for teil in auch) or any(teil in alles for teil in nicht):
|
||||
ok = False
|
||||
if anzahl is not None and len(befunde) != anzahl:
|
||||
ok = False
|
||||
if ergebnis.get("_exit", ergebnis.get("exit")) != exit_code:
|
||||
ok = False
|
||||
if fakt is not None:
|
||||
try:
|
||||
ok = ok and bool(fakt(ergebnis.get("fakten") or {}, ergebnis))
|
||||
except Exception:
|
||||
ok = False
|
||||
zeile = f"{'OK ' if ok else 'FEHL'} {name}: {len(befunde)} Befund(e) {befunde[:3]} meldung={(ergebnis.get('fakten') or {}).get('meldung')}"
|
||||
print(zeile)
|
||||
if not ok:
|
||||
abweichungen.append(zeile)
|
||||
|
||||
|
||||
def wsl_faelle() -> None:
|
||||
def fall(name, vorbereiten, *, befund, anzahl=None, exit_code=0, env=None, tag=TAG, meldung=False,
|
||||
auch=(), nicht=(), fakt=None):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
wurzel = Path(tmp)
|
||||
projekt = baue_projekt(wurzel)
|
||||
extra = vorbereiten(wurzel, projekt)
|
||||
umgebung = {**(env or {}), **(extra if isinstance(extra, dict) else {})}
|
||||
erwarte(name, fahre(wurzel, projekt, umgebung, tag, meldung), befund=befund, anzahl=anzahl,
|
||||
exit_code=exit_code, auch=auch, nicht=nicht, fakt=fakt)
|
||||
|
||||
nichts = lambda w, p: None # noqa: E731
|
||||
prot = lambda p: p / "artifacts" / "paper" / f"{TAG}T131023-paper.jsonl" # noqa: E731
|
||||
|
||||
def schreibe(pfad: Path, text: str) -> None:
|
||||
pfad.write_text(text, encoding="utf-8")
|
||||
|
||||
def zeilen_aendern(p: Path, aendern) -> None:
|
||||
zeilen = prot(p).read_text(encoding="utf-8").splitlines()
|
||||
schreibe(prot(p), "\n".join(aendern(zeilen)) + "\n")
|
||||
|
||||
fall("Grundlinie", nichts, befund=None, anzahl=0)
|
||||
# 1 Kalender
|
||||
# Ein Feiertag ohne Protokoll: kein Befund. Haelt die Kontrolle ihn faelschlich fuer einen Handelstag,
|
||||
# fehlt das Protokoll und es gibt einen -- so trennt der Fall "kein Handelstag" von "immer Handelstag".
|
||||
def feiertag(inhalt):
|
||||
def vorbereiten(w, p):
|
||||
schreibe(w / "kalender.json", inhalt)
|
||||
prot(p).unlink()
|
||||
return vorbereiten
|
||||
fall("Kalender leer (Feiertag, Wochenende)", feiertag("[]"), befund=None, anzahl=0)
|
||||
fall("Kalender nennt nur einen anderen Tag", feiertag(json.dumps([{"date": "2026-09-22"}])), befund=None, anzahl=0)
|
||||
fall("Kalender als Objekt", lambda w, p: schreibe(w / "kalender.json", "{}"), befund="Kalender in unerwarteter Form")
|
||||
fall("Kalender Eintrag ohne date, erste Position",
|
||||
lambda w, p: schreibe(w / "kalender.json", json.dumps([{"open": "09:30"}] + KALENDER_TAG)),
|
||||
befund="Kalender in unerwarteter Form")
|
||||
fall("Kalender Eintrag ohne date, letzte Position",
|
||||
lambda w, p: schreibe(w / "kalender.json", json.dumps(KALENDER_TAG + [{"date": 5}])),
|
||||
befund="Kalender in unerwarteter Form")
|
||||
fall("Kalender-Datei fehlt", lambda w, p: (w / "kalender.json").unlink(), befund="Kalender, und damit")
|
||||
fall("Kalender kein JSON", lambda w, p: schreibe(w / "kalender.json", "<html>"), befund="Kalender, und damit")
|
||||
# 2 Protokoll
|
||||
fall("kein Protokoll", lambda w, p: prot(p).unlink(), befund="der Runner lief nicht")
|
||||
fall("nur Probeprotokoll", lambda w, p: prot(p).rename(prot(p).with_name(f"{TAG}T131023-probe.jsonl")),
|
||||
befund="der Runner lief nicht")
|
||||
# 6 Handel
|
||||
fall("nie aktiv",
|
||||
lambda w, p: zeilen_aendern(p, lambda z: [s.replace('"killswitch": "aktiv"', '"killswitch": "angehalten"') for s in z]),
|
||||
befund="Tag ohne Handel")
|
||||
fall("kaputte Zeile vorne", lambda w, p: zeilen_aendern(p, lambda z: [z[0], "{kaputt", *z[1:]]), befund="nicht lesbar")
|
||||
fall("kaputte Zeile hinten", lambda w, p: zeilen_aendern(p, lambda z: [*z, "[1, 2]"]), befund="nicht lesbar")
|
||||
# 3 Journal
|
||||
fall("Journal ohne Ende", lambda w, p: schreibe(w / "journal.txt", "gestartet\n"), befund="keine Zeile 'Ende")
|
||||
fall("Journal leer", lambda w, p: schreibe(w / "journal.txt", ""), befund="keine Zeile 'Ende")
|
||||
fall("Journal fehlt", lambda w, p: (w / "journal.txt").unlink(), befund="Journal nicht geprueft")
|
||||
fall("Exit 4", lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT.replace("Exit 0", "Exit 4")), befund="Exit 4")
|
||||
fall("Exit 4 im ersten von zwei Laeufen",
|
||||
lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT.replace("Exit 0", "Exit 4") + JOURNAL_GUT), befund="Lauf 1 von 2")
|
||||
fall("Exit 4 im letzten von zwei Laeufen",
|
||||
lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT + JOURNAL_GUT.replace("Exit 0", "Exit 4")), befund="Lauf 2 von 2")
|
||||
fall("Grund stoerung", lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT.replace("Ende: boersenschluss", "Ende: stoerung")),
|
||||
befund="nicht mit boersenschluss")
|
||||
fall("Position 10", lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT.replace("Position 0", "Position 10")),
|
||||
befund="Position 10")
|
||||
fall("Failed with result",
|
||||
lambda w, p: schreibe(w / "journal.txt", JOURNAL_GUT + "trading-paper.service: Failed with result 'exit-code'.\n"),
|
||||
befund="Failed with result")
|
||||
# 4 Auswertung
|
||||
fall("Orderzeile nicht gefuellt",
|
||||
lambda w, p: zeilen_aendern(p, lambda z: [s.replace('"status": "filled"', '"status": "canceled"', 1) if '"art": "order"' in s else s for s in z]),
|
||||
befund="nicht gefuellt")
|
||||
fall("Schlussposition 10 im Protokoll",
|
||||
lambda w, p: zeilen_aendern(p, lambda z: [s.replace('"position": 0', '"position": 10') if '"art": "ende"' in s else s for s in z]),
|
||||
befund="Schlussposition ungleich null")
|
||||
# 5 Broker (Ersatz-Auswertung, kein Netz)
|
||||
ersatz = HIER / "abendkontrolle_ersatz_auswertung.py"
|
||||
fall("Broker 2 offen", nichts, befund="2 offene Position",
|
||||
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz), "ERSATZ_AUSGABE": "offene positionen beim broker jetzt: 2"},
|
||||
)
|
||||
fall("Broker 0 offen", nichts, befund=None, anzahl=0,
|
||||
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz), "ERSATZ_AUSGABE": "offene positionen beim broker jetzt: 0"})
|
||||
fall("Auswertung Exit 3", nichts, befund="Exit 3",
|
||||
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz), "ERSATZ_AUSGABE": "VORAUSSETZUNG FEHLT: x", "ERSATZ_EXIT": "3"})
|
||||
fall("Auswertung Kontowert in BEFUND wird maskiert", nichts, befund="kontowert [x]",
|
||||
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz), "ERSATZ_AUSGABE": "BEFUND: kontowert 12345.67 zu tief"})
|
||||
# 7 Bericht
|
||||
fall("Bericht fehlt", lambda w, p: (p / "data" / "paper" / "auswertungen" / f"{TAG}.md").unlink(), befund="Tagesbericht")
|
||||
# 8 Dienste
|
||||
fall("Dashboard inaktiv (erste Position)",
|
||||
lambda w, p: schreibe(w / "dienste.json", json.dumps({**DIENSTE_GUT, "trading-dashboard.service": "failed"})),
|
||||
befund="trading-dashboard.service ist nicht aktiv")
|
||||
fall("Auswertungs-Zeitgeber fehlt (letzte Position)",
|
||||
lambda w, p: schreibe(w / "dienste.json", json.dumps({k: v for k, v in DIENSTE_GUT.items() if "auswertung" not in k})),
|
||||
befund="trading-auswertung.timer ist nicht aktiv")
|
||||
fall("Dienste als Liste", lambda w, p: schreibe(w / "dienste.json", "[]"), befund="Dienste: Zustand nicht lesbar")
|
||||
# 9 Killswitch
|
||||
def killswitch(zustand):
|
||||
def vorbereiten(w, p):
|
||||
with sqlite3.connect(p / "data" / "registry.db") as db:
|
||||
if zustand is None:
|
||||
db.execute("DELETE FROM killswitch_zustand")
|
||||
else:
|
||||
db.execute("UPDATE killswitch_zustand SET zustand = ?", (zustand,))
|
||||
return vorbereiten
|
||||
fall("Killswitch gesperrt", killswitch("gesperrt"), befund="gesperrt")
|
||||
fall("Killswitch ohne Zeile", killswitch(None), befund="kein Killswitch-Zustand")
|
||||
fall("Register fehlt", lambda w, p: (p / "data" / "registry.db").unlink(), befund="registry.db fehlt")
|
||||
# 3 Ende je Lauf: mittags abgebrochen, der zweite Lauf bleibt angehalten (Befund des Pruefers W2)
|
||||
def mittags_abgebrochen(w, p):
|
||||
zeilen = prot(p).read_text(encoding="utf-8").splitlines()
|
||||
takte = [z for z in zeilen if '"art": "takt"' in z]
|
||||
ende = json.loads(next(z for z in zeilen if '"art": "ende"' in z))
|
||||
lauf1 = [zeilen[0], *takte[:30], json.dumps({**ende, "grund": "abbruch"})]
|
||||
lauf2 = [zeilen[0], *[z.replace('"killswitch": "aktiv"', '"killswitch": "angehalten"') for z in takte[30:]], json.dumps(ende)]
|
||||
schreibe(prot(p), "\n".join(lauf1) + "\n")
|
||||
schreibe(p / "artifacts" / "paper" / f"{TAG}T160000-paper.jsonl", "\n".join(lauf2) + "\n")
|
||||
schreibe(w / "journal.txt", JOURNAL_GUT.replace("Ende: boersenschluss", "Ende: abbruch") + JOURNAL_GUT)
|
||||
fall("mittags abgebrochen, danach angehalten", mittags_abgebrochen, befund="Lauf endete mit abbruch",
|
||||
auch=("Lauf 1 von 2) endete nicht mit boersenschluss",))
|
||||
|
||||
# Lauf nach Boersenschluss (WSL-Neustart abends): kein Befund (Befund des Pruefers W3)
|
||||
def nach_schluss(w, p):
|
||||
kopf = prot(p).read_text(encoding="utf-8").splitlines()[0]
|
||||
ende = {"art": "ende", "zeit": f"{TAG}T21:06:40+00:00", "grund": "wartezeit", "fehler": None, "position": 0,
|
||||
"orders": 0, "killswitch": "angehalten", "schritte": 0}
|
||||
schreibe(p / "artifacts" / "paper" / f"{TAG}T210500-paper.jsonl", kopf + "\n" + json.dumps(ende) + "\n")
|
||||
schreibe(w / "journal.txt", JOURNAL_GUT + "Ende: wartezeit, Position 0, Orders 0, Killswitch angehalten, Exit 3\n")
|
||||
fall("Lauf nach Boersenschluss (wartezeit, Exit 3)", nach_schluss, befund=None, anzahl=0)
|
||||
fall("nur ein Lauf mit wartezeit an einem Handelstag", lambda w, p: (
|
||||
nach_schluss(w, p), prot(p).unlink(),
|
||||
schreibe(w / "journal.txt", "Ende: wartezeit, Position 0, Orders 0, Killswitch angehalten, Exit 3\n")),
|
||||
befund="keine Zeile 'Ende", auch=("kein Lauf des scharfen Runners in der Handelszeit",))
|
||||
# 9 Killswitch: unbekannter Zustand, und eine Frage, die scheitert, haelt die anderen nicht auf
|
||||
fall("Killswitch unbekannter Zustand", killswitch("kaputt"), befund="unbekanntem Zustand")
|
||||
|
||||
def register_ohne_tabelle(w, p):
|
||||
(p / "data" / "registry.db").unlink()
|
||||
sqlite3.connect(p / "data" / "registry.db").close()
|
||||
(p / "data" / "paper" / "auswertungen" / f"{TAG}.md").unlink()
|
||||
fall("Register ohne Tabelle, Bericht fehlt", register_ohne_tabelle, befund="Killswitch nicht geprueft",
|
||||
auch=("Tagesbericht",))
|
||||
# Maske auch ohne Geldwort (Befund des Pruefers W4)
|
||||
fall("Maske ohne Geldwort", nichts, befund="Wert",
|
||||
env={"ABENDKONTROLLE_AUSWERTUNG": str(ersatz),
|
||||
"ERSATZ_AUSGABE": "BEFUND: Wert 100123.45 und 172.3456 und -4.56 USD\noffene positionen beim broker jetzt: 0"},
|
||||
nicht=("100123", "172.3456", "4.56"))
|
||||
|
||||
# Meldeweg (Befund des Pruefers B1): ein Fehlschlag darf nicht als "gesendet" stehen
|
||||
def mit_melder(port):
|
||||
def vorbereiten(w, p):
|
||||
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")
|
||||
schreibe(p / "config" / "paper.toml", text.replace(anker, f'ntfy_url = "http://127.0.0.1:{port}"\nklartext_lokal = true'))
|
||||
schreibe(w / "secrets.toml", '[ntfy]\naktiv = true\nthema = "abendkontrolle-angriff"\n')
|
||||
(p / "data" / "paper" / "auswertungen" / f"{TAG}.md").unlink() # ein Befund, damit gemeldet wird
|
||||
return {"TRADING_GEHEIMNISSE": str(w / "secrets.toml")}
|
||||
return vorbereiten
|
||||
fall("Meldung scheitert (niemand hoert)", mit_melder(9), befund="Tagesbericht", meldung=True,
|
||||
fakt=lambda f, e: str(f.get("meldung", "")).startswith("nicht gesendet"))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mitschrift = Path(tmp) / "mitschrift.jsonl"
|
||||
port = 18765
|
||||
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)
|
||||
fall("Meldung kommt an", mit_melder(port), befund="Tagesbericht", meldung=True,
|
||||
fakt=lambda f, e: f.get("meldung") == "gesendet" and "Abendkontrolle" in mitschrift.read_text(encoding="utf-8"))
|
||||
finally:
|
||||
server.terminate()
|
||||
server.wait(timeout=10)
|
||||
|
||||
# Bedienfehler
|
||||
fall("Tag falsch", nichts, befund="abgebrochen", exit_code=2, tag="2026-13-40")
|
||||
|
||||
|
||||
# --- Windows-Teil --------------------------------------------------------------------------------
|
||||
|
||||
def windows_faelle() -> None:
|
||||
sys.path.insert(0, str(HIER))
|
||||
import abendkontrolle as ak
|
||||
from datetime import datetime
|
||||
|
||||
gut = {"tag": TAG, "befunde": [], "hinweise": ["auto_fortsetzen: fortgesetzt"], "exit": 0,
|
||||
"fakten": {"handelstag": True, "ende": {"grund": "boersenschluss", "exit": "0", "orders": "79", "position": "0"},
|
||||
"laeufe": [{"takte_aktiv": 375}], "broker_offen": 0, "bericht": True,
|
||||
"killswitch": {"zustand": "aktiv", "ausloeser": "automatik"}}}
|
||||
|
||||
def lauf(name, notiz_vorher, ergebnis, *, jetzt="2026-09-23T22:45:00", pruefe):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
vault = Path(tmp)
|
||||
(vault / "Inbox").mkdir()
|
||||
notiz = vault / "Inbox" / f"{TAG}.md"
|
||||
if notiz_vorher is not None:
|
||||
notiz.write_bytes(notiz_vorher if isinstance(notiz_vorher, bytes) else notiz_vorher.encode("utf-8"))
|
||||
eingabe = vault / "ergebnis.json"
|
||||
eingabe.write_text(ergebnis if isinstance(ergebnis, str) else json.dumps(ergebnis), encoding="utf-8")
|
||||
code = ak.main(["--vault", str(vault), "--tag", TAG, "--ohne-wsl", str(eingabe), "--jetzt", jetzt])
|
||||
text = notiz.read_bytes().decode("utf-8", errors="replace") if notiz.exists() else ""
|
||||
marke = (vault / ak.ABLAGE_TEIL / f"{TAG}.json").exists()
|
||||
try:
|
||||
ok, grund = pruefe(code, text, marke)
|
||||
except Exception as ausnahme: # der Treiber selbst bricht nicht ab
|
||||
ok, grund = False, f"Pruefung warf {ausnahme!r}; Notiz {text[-120:]!r}"
|
||||
zeile = f"{'OK ' if ok else 'FEHL'} W {name}: {grund}"
|
||||
print(zeile)
|
||||
if not ok:
|
||||
abweichungen.append(zeile)
|
||||
|
||||
kopf = "---\ntype: daily\n---\n\n# 2026-09-23\n\n"
|
||||
lauf("neue Notiz, ohne Befund", None, gut, pruefe=lambda c, t, m: (
|
||||
c == 0 and m and "## Agentenprotokoll" in t and "ohne Befund" in t and "type: daily" in t, f"code {c} marke {m}"))
|
||||
lauf("Abschnitt vor einer weiteren Ueberschrift", kopf + "## Agentenprotokoll\n\n- alt\n\n## Danach\n\ntext\n", gut,
|
||||
pruefe=lambda c, t, m: (t.index("ohne Befund") < t.index("## Danach") and "- alt\n- 22:45" in t
|
||||
and "\n\n## Danach" in t and "\n\n\n" not in t, repr(t[-80:])))
|
||||
lauf("Abschnitt am Ende", kopf + "## Agentenprotokoll\n\n- alt\n", gut,
|
||||
pruefe=lambda c, t, m: (t.endswith(".\n - Hinweis: auto_fortsetzen: fortgesetzt\n") and "- alt\n- 22:45" in t, repr(t[-60:])))
|
||||
lauf("ohne Abschnitt", kopf + "## Anderes\n\nx\n", gut,
|
||||
pruefe=lambda c, t, m: (t.count("## Agentenprotokoll") == 1 and t.index("## Anderes") < t.index("## Agentenprotokoll"), repr(t[-60:])))
|
||||
lauf("Abschnitt direkt vor naechster Ueberschrift", kopf + "## Agentenprotokoll\n## Danach\n", gut,
|
||||
pruefe=lambda c, t, m: ("## Agentenprotokoll\n\n- 22:45" in t and "\n\n## Danach" in t, repr(t)))
|
||||
lauf("CRLF-Notiz", (kopf + "## Agentenprotokoll\n\n- alt\n").replace("\n", "\r\n"), gut,
|
||||
pruefe=lambda c, t, m: ("\n" not in t.replace("\r\n", ""), "gemischte Zeilenenden" if "\n" in t.replace("\r\n", "") else "ok"))
|
||||
mit_befund = {**gut, "befunde": ["Runner endete mit Exit 4", "Beim Broker stehen 2 offene Position(en)"]}
|
||||
lauf("mit Befund", None, mit_befund, pruefe=lambda c, t, m: (
|
||||
"**2 Befund(e)** #needs-review" in t and " - Runner endete mit Exit 4" in t and m, repr(t[-160:])))
|
||||
lauf("Ergebnis ohne befunde", None, {"tag": TAG}, pruefe=lambda c, t, m: (
|
||||
c == 1 and not m and "**gescheitert**" in t and "#needs-review" in t, f"code {c} marke {m}"))
|
||||
lauf("Ergebnis kein JSON", None, "nicht json", pruefe=lambda c, t, m: (c == 1 and not m and "**gescheitert**" in t, f"code {c}"))
|
||||
lauf("kein Handelstag", None, {"tag": TAG, "befunde": [], "hinweise": [], "fakten": {"handelstag": False}},
|
||||
pruefe=lambda c, t, m: ("kein Handelstag" in t and m, repr(t[-60:])))
|
||||
# Befunde des Pruefers: Ueberschrift mit Zusatz, Notiz ohne UTF-8, Abbruch mit JSON, Nachbardatei
|
||||
lauf("Ueberschrift mit Zusatz", kopf + "## Agentenprotokoll — Sitzung 9\n\n- alt\n\n## Offene Punkte\n\n- x\n", gut,
|
||||
pruefe=lambda c, t, m: (t.count("## Agentenprotokoll") == 1 and t.index("ohne Befund") < t.index("## Offene Punkte"), repr(t[-120:])))
|
||||
kaputt = "---\ntype: daily\n---\n\n# Tag äöü\n".encode("cp1252")
|
||||
lauf("Notiz kein UTF-8", kaputt, mit_befund, pruefe=lambda c, t, m: (m and c == 1, f"code {c} marke {m}"))
|
||||
lauf("Abbruch mit JSON (exit 2) wird wiederholt", None,
|
||||
{"tag": TAG, "befunde": ["Abendkontrolle abgebrochen: X"], "hinweise": [], "fakten": {}, "exit": 2},
|
||||
pruefe=lambda c, t, m: (not m and "**1 Befund(e)**" in t and c == 1, f"code {c} marke {m}"))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
vault = Path(tmp)
|
||||
(vault / "Inbox").mkdir()
|
||||
eingabe = vault / "e.json"
|
||||
eingabe.write_text(json.dumps(gut), encoding="utf-8")
|
||||
ak.main(["--vault", str(vault), "--tag", TAG, "--ohne-wsl", str(eingabe), "--jetzt", "2026-09-23T22:45:00"])
|
||||
rest = [p.name for p in (vault / "Inbox").iterdir() if not p.name.endswith(".md")]
|
||||
ok = not rest
|
||||
print(f"{'OK ' if ok else 'FEHL'} W keine Nachbardatei uebrig: {rest}")
|
||||
if not ok:
|
||||
abweichungen.append("Nachbardatei uebrig")
|
||||
|
||||
# faellige_tage
|
||||
from datetime import date
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ablage = Path(tmp)
|
||||
def tage(jetzt):
|
||||
return ak.faellige_tage(datetime.fromisoformat(jetzt), ablage)
|
||||
faelle = [
|
||||
("Do vor 22:30: heute nicht", "2026-09-24T21:00", ["2026-09-24"], False),
|
||||
("Do nach 22:30: heute", "2026-09-24T22:45", ["2026-09-24"], True),
|
||||
("Mo frueh: Fr nachholen, Wochenende nicht", "2026-09-28T08:00", ["2026-09-25"], True),
|
||||
("Mo frueh: nichts vor ERSTER_TAG", "2026-09-28T08:00", ["2026-09-23"], False),
|
||||
]
|
||||
for name, jetzt, erwartet, drin in faelle:
|
||||
ergebnis = tage(jetzt)
|
||||
ok = all((tag in ergebnis) == drin for tag in erwartet) and not any(
|
||||
date.fromisoformat(t).weekday() >= 5 for t in ergebnis)
|
||||
zeile = f"{'OK ' if ok else 'FEHL'} T {name}: {ergebnis}"
|
||||
print(zeile)
|
||||
if not ok:
|
||||
abweichungen.append(zeile)
|
||||
(ablage / "2026-09-25.json").write_text("{}", encoding="utf-8")
|
||||
ergebnis = tage("2026-09-28T08:00")
|
||||
ok = "2026-09-25" not in ergebnis
|
||||
print(f"{'OK ' if ok else 'FEHL'} T Ergebnisdatei da: nicht erneut: {ergebnis}")
|
||||
if not ok:
|
||||
abweichungen.append("Ergebnisdatei ignoriert")
|
||||
ergebnis = tage("2026-10-12T08:00")
|
||||
ok = min(ergebnis) >= "2026-10-05"
|
||||
print(f"{'OK ' if ok else 'FEHL'} T hoechstens sieben Tage zurueck: {ergebnis}")
|
||||
if not ok:
|
||||
abweichungen.append("Nachholfenster")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.platform == "win32":
|
||||
windows_faelle()
|
||||
else:
|
||||
wsl_faelle()
|
||||
print(f"ERGEBNIS: {len(abweichungen)} Abweichung(en)")
|
||||
sys.exit(1 if abweichungen else 0)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Richtet die Windows-Aufgabe "Trading Plattform - Abendkontrolle" ein oder entfernt sie.
|
||||
# Koordinator-Werkzeug, nicht fuers Repo. Auftrag Pascal 2026-09-24: taegliche Abendkontrolle,
|
||||
# dauerhaft, uebersteht Neustarts, blockiert keine Updates von Claude Code oder Desktop.
|
||||
#
|
||||
# Zwei Ausloeser: werktags 22:45 (nach der Tagesauswertung um 22:20) und fuenf Minuten nach jeder
|
||||
# Anmeldung (holt einen verpassten Abend nach; abendkontrolle.py prueft nur Tage ohne Ergebnisdatei).
|
||||
# Nur bei angemeldetem Benutzer (Interactive): WSL und der ganze Paper-Betrieb brauchen die Anmeldung
|
||||
# ohnehin (Aufgabe "Trading Plattform - WSL wachhalten"). pythonw.exe startet ohne Fenster.
|
||||
# Voller Interpreterpfad: -Execute "python" endet in der Aufgabenplanung mit 0x80070002.
|
||||
#
|
||||
# Aufruf: powershell -NoProfile -File abendkontrolle_einrichten.ps1 [-Aktion einrichten|entfernen]
|
||||
param(
|
||||
[ValidateSet("einrichten", "entfernen")]
|
||||
[string]$Aktion = "einrichten"
|
||||
)
|
||||
|
||||
$Name = "Trading Plattform - Abendkontrolle"
|
||||
if ($Aktion -eq "entfernen") {
|
||||
Unregister-ScheduledTask -TaskName $Name -Confirm:$false
|
||||
return
|
||||
}
|
||||
|
||||
$Py = "C:\Users\Pascal Michel\AppData\Local\Programs\Python\Python313\pythonw.exe"
|
||||
$Skript = "D:\Claude\.vault-tools\trading-plattform-pruefung\abendkontrolle.py"
|
||||
if (-not (Test-Path $Py)) { throw "Interpreter fehlt: $Py" }
|
||||
if (-not (Test-Path $Skript)) { throw "Skript fehlt: $Skript" }
|
||||
|
||||
$Benutzer = "$env:USERDOMAIN\$env:USERNAME"
|
||||
$Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '"') -WorkingDirectory "D:\Claude"
|
||||
$Abends = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday, Tuesday, Wednesday, Thursday, Friday -At "22:45"
|
||||
$Anmeldung = New-ScheduledTaskTrigger -AtLogOn -User $Benutzer
|
||||
$Anmeldung.Delay = "PT5M"
|
||||
$Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited
|
||||
$Regeln = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 45) -MultipleInstances IgnoreNew
|
||||
Register-ScheduledTask -TaskName $Name -Action $Aufruf -Trigger @($Abends, $Anmeldung) -Principal $Wer `
|
||||
-Settings $Regeln -Force `
|
||||
-Description "Abendkontrolle des Paper-Betriebs: liest nur, schreibt in die Tagesnotiz, meldet Befunde per ntfy. Koordinator-Werkzeug, siehe abendkontrolle.py." | Out-Null
|
||||
|
||||
$Aufgabe = Get-ScheduledTask -TaskName $Name
|
||||
"{0}: {1}" -f $Aufgabe.TaskName, $Aufgabe.State
|
||||
$Aufgabe.Triggers | ForEach-Object { "Ausloeser: {0} {1} {2}" -f $_.CimClass.CimClassName, $_.StartBoundary, $_.Delay }
|
||||
"Naechster Lauf: {0}" -f (Get-ScheduledTaskInfo -TaskName $Name).NextRunTime
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Ersatz fuer handelstag_auswertung.py in den Angriffsfaellen der Abendkontrolle: druckt ERSATZ_AUSGABE,
|
||||
endet mit ERSATZ_EXIT. Geht nicht ans Netz. Koordinator-Werkzeug, nicht fuers Repo."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
print(os.environ.get("ERSATZ_AUSGABE", ""))
|
||||
sys.exit(int(os.environ.get("ERSATZ_EXIT", "0")))
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Mutanten der Abendkontrolle und des Rueckwegs: jeder muss seinen Angriffstreiber rot machen.
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Jeder Mutant laeuft auf einer Wegwerfkopie dieses Verzeichnisses.
|
||||
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).
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
|
||||
WSL_MUTANTEN = [
|
||||
("Exit-Vergleich verkehrt", "abendkontrolle_wsl.py", 'if satz["exit"] != "0":', 'if satz["exit"] == "9":'),
|
||||
("Handelstag immer wahr", "abendkontrolle_wsl.py", 'return any(eintrag["date"] == tag for eintrag in liste)', "return True"),
|
||||
("gesperrt nicht geprueft", "abendkontrolle_wsl.py", 'if zeile[0] == "gesperrt":', 'if zeile[0] == "nie":'),
|
||||
("unbekannter Zustand still", "abendkontrolle_wsl.py", "elif zeile[0] not in KILLSWITCH_ZUSTAENDE:", "elif False:"),
|
||||
("Broker-Schwelle", "abendkontrolle_wsl.py", "if offen[-1] != 0:", "if offen[-1] > 5:"),
|
||||
("Probe zaehlt mit", "abendkontrolle_wsl.py", 'glob(f"{tag}T*-paper.jsonl")', 'glob(f"{tag}T*.jsonl")'),
|
||||
("Maske aus", "abendkontrolle_wsl.py", " text = muster.sub(ersatz, text)", " pass"),
|
||||
("nie-aktiv aus", "abendkontrolle_wsl.py", 'if satz.get("killswitch") == "aktiv":', "if True:"),
|
||||
("Bericht nicht geprueft", "abendkontrolle_wsl.py", "if not bericht.is_file():\n k.befund", "if False:\n k.befund"),
|
||||
("Failed-Zeile aus", "abendkontrolle_wsl.py", 'if "Failed with result" in text:', 'if "nie da" in text:'),
|
||||
("nur letzter Lauf im Journal", "abendkontrolle_wsl.py", "for nummer, satz in enumerate(enden, start=1):",
|
||||
"for nummer, satz in list(enumerate(enden, start=1))[-1:]:"),
|
||||
("Melder-Rueckgabe verworfen", "abendkontrolle_wsl.py",
|
||||
'return "gesendet" if fehler is None else f"nicht gesendet: {fehler}"', 'return "gesendet"'),
|
||||
("Ende je Protokoll nicht bewertet", "abendkontrolle_wsl.py",
|
||||
" if ende.get(\"grund\") != \"boersenschluss\":\n k.befund(f\"{lauf['datei']}",
|
||||
" if False:\n k.befund(f\"{lauf['datei']}"),
|
||||
("wartezeit im Journal mitgezaehlt", "abendkontrolle_wsl.py",
|
||||
'enden = [satz for satz in alle if satz["grund"] != "wartezeit"]', "enden = alle"),
|
||||
("Lauf nach Schluss als Handelslauf", "abendkontrolle_wsl.py",
|
||||
'return bool(lauf["ende"]) and lauf["ende"].get("grund") == "wartezeit" and lauf["takte"] == 0', "return False"),
|
||||
("Fragen nicht voneinander getrennt", "abendkontrolle_wsl.py",
|
||||
" except Exception as ausnahme:\n self.befund(f\"{name} nicht geprueft",
|
||||
" except ZeroDivisionError as ausnahme:\n self.befund(f\"{name} nicht geprueft"),
|
||||
]
|
||||
WIN_MUTANTEN = [
|
||||
("keine Ergebnisdatei", "abendkontrolle.py", '(ablage / f"{tag}.json").write_text(', '(ablage / f"{tag}.nix").write_text('),
|
||||
("Wochenende mit", "abendkontrolle.py", "if tag < ERSTER_TAG or tag.weekday() >= 5:", "if tag < ERSTER_TAG:"),
|
||||
("heute vor 22:30", "abendkontrolle.py", "if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:", "if False:"),
|
||||
("Befund ohne Markierung", "abendkontrolle.py", "**{len(befunde)} Befund(e)** #needs-review", "{len(befunde)} Befund(e)"),
|
||||
("CRLF vergessen", "abendkontrolle.py", 'cr = "\\r" if any(z.endswith("\\r") for z in zeilen) else ""', 'cr = ""'),
|
||||
("Formpruefung aus", "abendkontrolle.py",
|
||||
'if not isinstance(ergebnis, dict) or not isinstance(ergebnis.get("befunde"), list):', "if False:"),
|
||||
("Ueberschrift nur exakt", "abendkontrolle.py",
|
||||
"if z == UEBERSCHRIFT or z.startswith(UEBERSCHRIFT + \" \")), None)", "if z == UEBERSCHRIFT), None)"),
|
||||
("Abbruch gilt als erledigt", "abendkontrolle.py", 'if ergebnis.get("exit") != 2:', "if True:"),
|
||||
("nicht atomar", "abendkontrolle.py", " os.replace(neu, notiz)", " notiz.write_bytes(text.encode(\"utf-8\"))"),
|
||||
("Zeit egal", "rueckweg_aapl.py", "if jetzt < FRUEHESTENS:", "if False:"),
|
||||
("Runner egal", "rueckweg_aapl.py", " if runner_aktiv(runner_vorgabe):", " if False and runner_aktiv(runner_vorgabe):"),
|
||||
("unlesbar gilt als steht", "rueckweg_aapl.py",
|
||||
'raise RuntimeError(f"Zustand des Runners nicht lesbar: {zustand!r}")', "return False"),
|
||||
("schmutziger Ordner egal", "rueckweg_aapl.py", " else:\n if geaendert:", " else:\n if False:"),
|
||||
("folge egal", "rueckweg_aapl.py", 'if MEHRERE not in ziel.read_text(encoding="utf-8"):', "if False:"),
|
||||
("Ergebnisdatei egal", "rueckweg_aapl.py", 'if (ablage / "rueckweg.json").exists():', "if False:"),
|
||||
("kein Commit", "rueckweg_aapl.py", '("add", ZIEL.as_posix()), ("commit", "-m", NACHRICHT)', '("add", ZIEL.as_posix()),'),
|
||||
("halb getauscht gilt als fertig", "rueckweg_aapl.py",
|
||||
" if not geaendert:\n return \"config/paper.toml ist schon",
|
||||
" if True:\n return \"config/paper.toml ist schon"),
|
||||
("Schluesselvergleich aus", "rueckweg_aapl.py", " if fehlend:", " if False:"),
|
||||
("Zweig egal", "rueckweg_aapl.py", " if zweig != ZWEIG:", " if False:"),
|
||||
("Wertevergleich aus", "rueckweg_aapl.py", " elif neu[name] != alt[name]:", " elif False:"),
|
||||
("Gegenrichtung aus", "rueckweg_aapl.py", " elif name not in alt:", " elif False:"),
|
||||
]
|
||||
TREIBER = {
|
||||
"abendkontrolle_wsl.py": "abendkontrolle_angriffe.py",
|
||||
"abendkontrolle.py": "abendkontrolle_angriffe.py",
|
||||
"rueckweg_aapl.py": "rueckweg_aapl_angriffe.py",
|
||||
}
|
||||
|
||||
mutanten = WIN_MUTANTEN if sys.platform == "win32" else WSL_MUTANTEN
|
||||
ueberlebt = []
|
||||
for name, datei, alt, neu in mutanten:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ziel = Path(tmp) / "pruefung"
|
||||
shutil.copytree(HIER, ziel, ignore=shutil.ignore_patterns("__pycache__"))
|
||||
pfad = ziel / datei
|
||||
text = pfad.read_text(encoding="utf-8")
|
||||
if text.count(alt) != 1:
|
||||
print(f"ANKER {text.count(alt)}-mal: {name}")
|
||||
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,
|
||||
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
|
||||
print(f"{'ROT (gefangen)' if rot else 'GRUEN (UEBERLEBT)'} {name}: {[f[:110] for f in faelle[:2]]}")
|
||||
if not rot:
|
||||
ueberlebt.append(name)
|
||||
print(f"ERGEBNIS: {len(ueberlebt)} ueberlebt von {len(mutanten)} {ueberlebt}")
|
||||
sys.exit(1 if ueberlebt else 0)
|
||||
@@ -0,0 +1,475 @@
|
||||
"""Abendkontrolle eines Paper-Handelstags: liest nur, urteilt im Code, meldet nur bei Befund.
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Laeuft in WSL im Venv des Projekts; gestartet von
|
||||
abendkontrolle.py (Windows-Aufgabe "Trading Plattform - Abendkontrolle", werktags 22:45 Berlin und
|
||||
bei jeder Anmeldung). Auftrag Pascal 2026-09-24: taegliche Abendkontrolle, dauerhaft, uebersteht
|
||||
Neustarts und blockiert keine Updates von Claude Code oder Desktop -- deshalb ohne Claude-Prozess.
|
||||
|
||||
Entscheidungsliste (Frage -> Herkunft der Antwort):
|
||||
1. Ist <tag> ein Handelstag? Quelle: GET /v2/calendar?start=<tag>&end=<tag>, gemessen am
|
||||
2026-09-24: eine Liste von Objekten mit 'date' (JJJJ-MM-TT) und
|
||||
'close' (HH:MM New York); Feiertage fehlen, das Wochenende ist
|
||||
leer, verkuerzte Tage tragen ein frueheres 'close'. Antwort
|
||||
nicht lesbar -> Befund, und die Fragen 2 bis 7 bleiben offen.
|
||||
2. Lief ein scharfer Runner? Quelle: scripts/paper.py:554, Protokoll
|
||||
artifacts/paper/<UTC-Stempel>-paper.jsonl. Der Start um 09:10
|
||||
New York traegt das Datum des Handelstags (gilt, solange
|
||||
max_wartezeit_h unter etwa 13 h bleibt; heute 8). Keines -> Befund.
|
||||
3. Wie endete jeder Lauf? Quelle: scripts/paper.py:517, Zeile "Ende: <grund>, Position <n>,
|
||||
Orders <n>, Killswitch <z>, Exit <n>" im Journal, und die Zeile
|
||||
'ende' jedes Protokolls. Projektvorgabe: jeder Lauf endet mit
|
||||
boersenschluss und Exit 0, der letzte mit Position 0. Ausgenommen
|
||||
ist ein Lauf ausserhalb der Handelszeit (Ende wartezeit, im
|
||||
Protokoll ohne Takt; gemessen am 2026-09-20 nach WSL-Neustarts):
|
||||
nur ein Hinweis. Ein Lauf, der mittags mit abbruch endet, ist ein
|
||||
Befund, auch wenn der naechste sauber schliesst. Keine Endezeile
|
||||
im Journal -> Befund; "Failed with result" -> Befund.
|
||||
4. Was sagt das Protokoll? Quelle: handelstag_auswertung.py (dieses Verzeichnis); jede
|
||||
Zeile, die mit "BEFUND" beginnt, ist ein Befund; Exit ungleich 0
|
||||
ebenfalls.
|
||||
5. Ist das Konto glatt? Quelle: dieselbe Auswertung mit --broker, Zeile "offene
|
||||
positionen beim broker jetzt: <n>". n != 0 -> Befund; fehlt die
|
||||
Zeile -> Befund (die Frage blieb unbeantwortet).
|
||||
6. Hat der Runner gehandelt? Projektvorgabe (Pascal 2026-09-24, Paket AF): an einem Handelstag
|
||||
steht der Killswitch in mindestens einem Takt auf aktiv. Kein
|
||||
solcher Takt -> Befund "Tag ohne Handel". Takte, die nach dem
|
||||
ersten aktiven nicht mehr aktiv sind (Watchdog, Pascals Anhalten),
|
||||
sind ein Hinweis: Der Runner hat dafuer schon selbst gemeldet.
|
||||
Ereignisse auto_fortsetzen werden berichtet, nicht bewertet.
|
||||
7. Liegt der Tagesbericht? Quelle: scripts/tagesauswertung.py:58 und :192,
|
||||
data/paper/auswertungen/<tag>.md. Fehlt -> Befund.
|
||||
8. Laufen Dashboard und Zeitgeber? Projektvorgabe (Pakete F und Z): trading-dashboard.service,
|
||||
trading-paper.timer, trading-auswertung.timer "active"; ein
|
||||
anderer Zustand wird nach DIENSTE_NOCHMAL_S einmal neu gelesen
|
||||
(eine eben geweckte WSL startet ihre Dienste noch).
|
||||
9. Wie steht der Killswitch? Quelle: Tabelle killswitch_zustand in data/registry.db
|
||||
(trading/registry/registry.py:281). gesperrt -> Befund: ohne
|
||||
"Freigeben" handelt der naechste Tag nicht. Ein Zustand
|
||||
ausserhalb aktiv/angehalten/gesperrt -> Befund.
|
||||
Jede Frage laeuft fuer sich: Scheitert eine (Sperre im Register, Journal nicht lesbar), ist das ein
|
||||
Befund, und die anderen werden trotzdem beantwortet.
|
||||
Meldung: bei mindestens einem Befund eine Meldung mit hoher Prioritaet ueber den Melder des Projekts.
|
||||
Ob sie ankam, sagt der Rueckgabewert von Melder.melde (None oder der Grund, trading/paper/melder.py);
|
||||
er steht in fakten.meldung. Kein Alarm entsteht, wenn WSL selbst nicht laeuft -- dann steht nur die
|
||||
Zeile in der Tagesnotiz (benannte Luecke; der Totmann in Home Assistant, Paket Z2-7, deckt sie ab).
|
||||
Nicht gesehen: die Guete der Kurse (steht im Bericht), Teilausfuehrungen einzeln, alles vor dem
|
||||
Handelstag. Betraege gibt das Werkzeug nie aus; fremde Zeilen laufen durch maskiere().
|
||||
|
||||
Ein Fangnetz, kein Nachweis ueber den Handelstag.
|
||||
Aufruf (WSL, Venv): python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung]
|
||||
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.
|
||||
Ausgabe: genau eine JSON-Zeile auf stdout. Exit 0 gelaufen (Befunde stehen in der Ausgabe),
|
||||
2 abgebrochen (die bis dahin gesammelten Befunde stehen trotzdem in der Ausgabe).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import date
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
PROJEKT = "/mnt/d/Claude/Code Projekte/active - Trading Plattform"
|
||||
GEHEIMNISSE = "/home/claude/.config/trading-plattform/secrets.toml"
|
||||
AUSWERTUNG = Path(__file__).resolve().parent / "handelstag_auswertung.py"
|
||||
# Nur fuer die Angriffsfaelle: ein Ersatz, der vorgegebene Zeilen druckt, statt ans Netz zu gehen.
|
||||
AUSWERTUNG_ERSATZ = os.environ.get("ABENDKONTROLLE_AUSWERTUNG")
|
||||
HANDEL_URL = "https://paper-api.alpaca.markets"
|
||||
DIENSTE = ("trading-dashboard.service", "trading-paper.timer", "trading-auswertung.timer")
|
||||
DIENSTE_NOCHMAL_S = 30
|
||||
KILLSWITCH_ZUSTAENDE = ("aktiv", "angehalten", "gesperrt")
|
||||
AUSWERTUNG_TIMEOUT_S = 900
|
||||
|
||||
ENDE = re.compile(
|
||||
r"Ende: (?P<grund>[^,]+), Position (?P<position>[^,]+), Orders (?P<orders>[^,]+), "
|
||||
r"Killswitch (?P<killswitch>[^,]+), Exit (?P<exit>-?\d+)"
|
||||
)
|
||||
BROKER_OFFEN = re.compile(r"offene positionen beim broker jetzt: (\d+)")
|
||||
# Maske fuer alles, was aus fremder Ausgabe in Notiz und Meldung geht, in dieser Reihenfolge:
|
||||
# - jede Zahl hinter einem Geldwort,
|
||||
# - jede Zahl vor einer Waehrung,
|
||||
# - jede Zahl mit mehr als zwei Nachkommastellen,
|
||||
# - jede Zahl mit mindestens vier Stellen vor dem Komma und Nachkommastellen.
|
||||
MASKEN = (
|
||||
(re.compile(r"(?i)\b(kontowert|konto|startkapital|equity|cash|preis|kurs|usd|pnl)([^0-9-]{0,3})-?[0-9][0-9.,]*"),
|
||||
lambda m: f"{m.group(1)}{m.group(2)}[x]"),
|
||||
(re.compile(r"-?\d[\d.,]*\s?(?:USD|EUR|\$|€)"), lambda m: "[x]"),
|
||||
(re.compile(r"-?\d[\d,]*\.\d{3,}"), lambda m: "[x]"),
|
||||
(re.compile(r"-?\d{4,}[.,]\d{1,2}\b"), lambda m: "[x]"),
|
||||
)
|
||||
|
||||
|
||||
def maskiere(text: str) -> str:
|
||||
for muster, ersatz in MASKEN:
|
||||
text = muster.sub(ersatz, text)
|
||||
return text
|
||||
|
||||
|
||||
class Kontrolle:
|
||||
def __init__(self) -> None:
|
||||
self.befunde: list[str] = []
|
||||
self.hinweise: list[str] = []
|
||||
self.fakten: dict = {}
|
||||
|
||||
def befund(self, text: str) -> None:
|
||||
self.befunde.append(maskiere(text))
|
||||
|
||||
def hinweis(self, text: str) -> None:
|
||||
self.hinweise.append(maskiere(text))
|
||||
|
||||
def schritt(self, name: str, aufruf, *argumente):
|
||||
"""Eine Frage fuer sich: Scheitert sie, ist das ein Befund, und der Rest laeuft weiter."""
|
||||
try:
|
||||
return aufruf(*argumente)
|
||||
except Exception as ausnahme:
|
||||
self.befund(f"{name} nicht geprueft ({type(ausnahme).__name__}: {str(ausnahme)[:160]})")
|
||||
return None
|
||||
|
||||
|
||||
# --- 1 Handelstag ---------------------------------------------------------------------------------
|
||||
|
||||
def lies_kalender(tag: str, kalender_datei: str | None, projekt: Path):
|
||||
"""Die Liste aus /v2/calendar oder aus einer Datei (Angriffsfaelle)."""
|
||||
if kalender_datei is not None:
|
||||
return json.loads(Path(kalender_datei).read_text(encoding="utf-8"))
|
||||
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": tag, "end": tag}, timeout=30
|
||||
)
|
||||
antwort.raise_for_status()
|
||||
return antwort.json()
|
||||
|
||||
|
||||
def ist_handelstag(tag: str, liste) -> bool | None:
|
||||
"""True, False, oder None, wenn die Antwort nicht die gemessene Form hat."""
|
||||
if not isinstance(liste, list):
|
||||
return None
|
||||
for eintrag in liste:
|
||||
if not isinstance(eintrag, dict) or not isinstance(eintrag.get("date"), str):
|
||||
return None
|
||||
return any(eintrag["date"] == tag for eintrag in liste)
|
||||
|
||||
|
||||
# --- 2 Protokolle, 3 Ende je Lauf, 6 Handel --------------------------------------------------------
|
||||
|
||||
def protokolle(projekt: Path, tag: str) -> list[Path]:
|
||||
return sorted((projekt / "artifacts" / "paper").glob(f"{tag}T*-paper.jsonl"))
|
||||
|
||||
|
||||
def lies_protokoll(pfad: Path, k: Kontrolle) -> dict:
|
||||
takte = aktiv = pausiert_danach = unlesbar = 0
|
||||
auto = []
|
||||
ende = None
|
||||
# Dateiiteration statt splitlines: U+2028 in einem Text zerlegte sonst eine JSON-Zeile.
|
||||
with pfad.open(encoding="utf-8", errors="replace") as datei:
|
||||
for zeile in datei:
|
||||
if not zeile.strip():
|
||||
continue
|
||||
try:
|
||||
satz = json.loads(zeile)
|
||||
except json.JSONDecodeError:
|
||||
unlesbar += 1
|
||||
continue
|
||||
if not isinstance(satz, dict):
|
||||
unlesbar += 1
|
||||
continue
|
||||
art = satz.get("art")
|
||||
if art == "takt":
|
||||
takte += 1
|
||||
if satz.get("killswitch") == "aktiv":
|
||||
aktiv += 1
|
||||
elif aktiv:
|
||||
pausiert_danach += 1
|
||||
elif art == "ereignis" and satz.get("was") == "auto_fortsetzen":
|
||||
auto.append(str(satz.get("text"))[:200])
|
||||
elif art == "ende":
|
||||
ende = {
|
||||
schluessel: satz.get(schluessel)
|
||||
for schluessel in ("grund", "fehler", "position", "positionen", "orders", "killswitch")
|
||||
}
|
||||
if unlesbar:
|
||||
k.befund(f"{pfad.name}: {unlesbar} Zeile(n) nicht lesbar")
|
||||
return {"datei": pfad.name, "takte": takte, "takte_aktiv": aktiv, "takte_pausiert_danach": pausiert_danach,
|
||||
"auto_fortsetzen": auto, "ende": ende}
|
||||
|
||||
|
||||
def ausserhalb_der_handelszeit(lauf: dict) -> bool:
|
||||
"""Ein Lauf, der ohne einen einzigen Takt mit wartezeit endete (Start nach Boersenschluss)."""
|
||||
return bool(lauf["ende"]) and lauf["ende"].get("grund") == "wartezeit" and lauf["takte"] == 0
|
||||
|
||||
|
||||
def pruefe_laeufe(laeufe: list[dict], k: Kontrolle) -> None:
|
||||
for lauf in laeufe:
|
||||
ende = lauf["ende"]
|
||||
if ende is None:
|
||||
continue # die Auswertung meldet "keine Zeile 'ende'"
|
||||
if ausserhalb_der_handelszeit(lauf):
|
||||
k.hinweis(f"{lauf['datei']}: Lauf ausserhalb der Handelszeit (Ende wartezeit), nicht bewertet")
|
||||
continue
|
||||
if ende.get("grund") != "boersenschluss":
|
||||
k.befund(f"{lauf['datei']}: Lauf endete mit {ende.get('grund')} nach {lauf['takte']} Takten")
|
||||
if ende.get("fehler"):
|
||||
k.befund(f"{lauf['datei']}: Lauf endete mit Fehler {str(ende.get('fehler'))[:120]}")
|
||||
if lauf["takte_pausiert_danach"]:
|
||||
k.hinweis(f"{lauf['datei']}: {lauf['takte_pausiert_danach']} Takte nach dem Fortsetzen nicht aktiv")
|
||||
for text in lauf["auto_fortsetzen"]:
|
||||
k.hinweis(f"auto_fortsetzen: {text}")
|
||||
bewertet = [lauf for lauf in laeufe if not ausserhalb_der_handelszeit(lauf)]
|
||||
if laeufe and not bewertet:
|
||||
k.befund("kein Lauf des scharfen Runners in der Handelszeit, nur Laeufe nach Boersenschluss")
|
||||
elif bewertet and sum(lauf["takte_aktiv"] for lauf in bewertet) == 0:
|
||||
k.befund("Tag ohne Handel: in keinem Takt stand der Killswitch auf aktiv")
|
||||
|
||||
|
||||
# --- 3 Journal -------------------------------------------------------------------------------------
|
||||
|
||||
def lies_journal(tag: str, journal_datei: str | None) -> str:
|
||||
if journal_datei is not None:
|
||||
return Path(journal_datei).read_text(encoding="utf-8", errors="replace")
|
||||
morgen = (date.fromisoformat(tag) + timedelta(days=1)).isoformat()
|
||||
lauf = subprocess.run(
|
||||
["journalctl", "-u", "trading-paper.service", "--since", f"{tag} 00:00", "--until", f"{morgen} 06:00",
|
||||
"-o", "cat", "--no-pager"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60,
|
||||
)
|
||||
if lauf.returncode != 0:
|
||||
raise RuntimeError(f"journalctl Exit {lauf.returncode}")
|
||||
return lauf.stdout
|
||||
|
||||
|
||||
def pruefe_journal(text: str, k: Kontrolle) -> dict | None:
|
||||
alle = [{schluessel: wert.strip() for schluessel, wert in m.groupdict().items()} for m in ENDE.finditer(text)]
|
||||
# Ein Lauf ausserhalb der Handelszeit (Neustart nach Boersenschluss) endet mit wartezeit und Exit 3.
|
||||
enden = [satz for satz in alle if satz["grund"] != "wartezeit"]
|
||||
k.fakten["enden_im_journal"] = len(enden)
|
||||
if len(alle) > len(enden):
|
||||
k.hinweis(f"{len(alle) - len(enden)} Lauf/Laeufe ausserhalb der Handelszeit im Journal (Ende wartezeit)")
|
||||
if "Failed with result" in text:
|
||||
k.befund("Journal: trading-paper.service endete mit 'Failed with result'")
|
||||
if not enden:
|
||||
k.befund("Journal: keine Zeile 'Ende: …, Exit n' eines Handelslaufs von trading-paper.service an diesem Tag")
|
||||
return None
|
||||
# Jeder Lauf des Tages zaehlt: Ein Runner, der mittags mit einer Stoerung endete und neu gestartet
|
||||
# wurde, ist auch dann ein Befund, wenn der zweite sauber schliesst.
|
||||
for nummer, satz in enumerate(enden, start=1):
|
||||
if satz["exit"] != "0":
|
||||
k.befund(f"Runner (Lauf {nummer} von {len(enden)}) endete mit Exit {satz['exit']} (Grund {satz['grund']})")
|
||||
if satz["grund"] != "boersenschluss":
|
||||
k.befund(f"Runner (Lauf {nummer} von {len(enden)}) endete nicht mit boersenschluss, sondern mit {satz['grund']}")
|
||||
letzte = enden[-1]
|
||||
if letzte["position"] != "0":
|
||||
k.befund(f"Runner endete mit Position {letzte['position']}")
|
||||
return letzte
|
||||
|
||||
|
||||
# --- 4 und 5 Auswertung ----------------------------------------------------------------------------
|
||||
|
||||
def fahre_auswertung(pfad: Path, projekt: Path, broker: bool, kerzen: bool, k: Kontrolle) -> None:
|
||||
befehl = [sys.executable, AUSWERTUNG_ERSATZ or str(AUSWERTUNG), str(pfad)]
|
||||
if broker:
|
||||
befehl.append("--broker")
|
||||
if kerzen:
|
||||
befehl.append("--kerzen")
|
||||
lauf = subprocess.run(
|
||||
befehl, cwd=projekt, capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
timeout=AUSWERTUNG_TIMEOUT_S, env={**os.environ, "PYTHONIOENCODING": "utf-8"},
|
||||
)
|
||||
zeilen = lauf.stdout.splitlines() + lauf.stderr.splitlines()
|
||||
for zeile in zeilen:
|
||||
if zeile.strip().startswith("BEFUND") or zeile.startswith("VORAUSSETZUNG FEHLT"):
|
||||
k.befund(f"{pfad.name}: {zeile.strip()}")
|
||||
if lauf.returncode != 0:
|
||||
k.befund(f"{pfad.name}: handelstag_auswertung.py endete mit Exit {lauf.returncode}")
|
||||
if broker:
|
||||
offen = [int(m.group(1)) for m in map(BROKER_OFFEN.search, zeilen) if m]
|
||||
if not offen:
|
||||
k.befund(f"{pfad.name}: keine Zeile zu offenen Positionen beim Broker -- Konto nicht geprueft")
|
||||
else:
|
||||
k.fakten["broker_offen"] = offen[-1]
|
||||
if offen[-1] != 0:
|
||||
k.befund(f"Beim Broker stehen {offen[-1]} offene Position(en)")
|
||||
|
||||
|
||||
# --- 8 Dienste und 9 Killswitch -------------------------------------------------------------------
|
||||
|
||||
def lies_dienste(dienste_datei: str | None) -> dict[str, str]:
|
||||
if dienste_datei is not None:
|
||||
return json.loads(Path(dienste_datei).read_text(encoding="utf-8"))
|
||||
|
||||
def lesen():
|
||||
ergebnis = {}
|
||||
for dienst in DIENSTE:
|
||||
lauf = subprocess.run(["systemctl", "is-active", dienst], capture_output=True, text=True, timeout=30)
|
||||
ergebnis[dienst] = lauf.stdout.strip() or f"exit {lauf.returncode}"
|
||||
return ergebnis
|
||||
|
||||
zustaende = lesen()
|
||||
if any(wert != "active" for wert in zustaende.values()):
|
||||
time.sleep(DIENSTE_NOCHMAL_S)
|
||||
zustaende = lesen()
|
||||
return zustaende
|
||||
|
||||
|
||||
def pruefe_dienste(zustaende, k: Kontrolle) -> None:
|
||||
if not isinstance(zustaende, dict):
|
||||
k.befund("Dienste: Zustand nicht lesbar")
|
||||
return
|
||||
k.fakten["dienste"] = zustaende
|
||||
for dienst in DIENSTE:
|
||||
if zustaende.get(dienst) != "active":
|
||||
k.befund(f"{dienst} ist nicht aktiv ({zustaende.get(dienst)})")
|
||||
|
||||
|
||||
def pruefe_killswitch(projekt: Path, k: Kontrolle) -> None:
|
||||
register = projekt / "data" / "registry.db"
|
||||
if not register.is_file():
|
||||
k.befund("Register data/registry.db fehlt")
|
||||
return
|
||||
with sqlite3.connect(f"file:{register}?mode=ro", uri=True, timeout=10) as verbindung:
|
||||
zeile = verbindung.execute(
|
||||
"SELECT zustand, seit, ausloeser FROM killswitch_zustand WHERE id = 1"
|
||||
).fetchone()
|
||||
if zeile is None:
|
||||
k.befund("Register: kein Killswitch-Zustand")
|
||||
return
|
||||
k.fakten["killswitch"] = {"zustand": zeile[0], "seit": zeile[1], "ausloeser": zeile[2]}
|
||||
if zeile[0] == "gesperrt":
|
||||
k.befund("Killswitch steht auf gesperrt: Ohne 'Freigeben' handelt der naechste Tag nicht")
|
||||
elif zeile[0] not in KILLSWITCH_ZUSTAENDE:
|
||||
k.befund(f"Killswitch in unbekanntem Zustand {zeile[0]!r}")
|
||||
|
||||
|
||||
# --- Meldung ---------------------------------------------------------------------------------------
|
||||
|
||||
def melde(titel: str, text: str, projekt: Path) -> str:
|
||||
"""Eine Meldung mit hoher Prioritaet ueber den Melder des Projekts. Scheitert nie nach aussen.
|
||||
|
||||
Melder.melde wirft nicht: Es gibt None bei Erfolg und sonst den Grund zurueck (Typ der Ausnahme
|
||||
oder "HTTP <status>"). Genau das steht im Ergebnis.
|
||||
"""
|
||||
try:
|
||||
sys.path.insert(0, str(projekt))
|
||||
os.chdir(projekt)
|
||||
from trading.paper.melder import baue_melder
|
||||
from trading.paper.meldungen import HOCH
|
||||
from trading.paper.meldungen import Meldung
|
||||
|
||||
melder = baue_melder(config_pfad="config/paper.toml")
|
||||
if not melder.aktiv:
|
||||
return "nicht gesendet: Melder nicht aktiv"
|
||||
fehler = melder.melde(Meldung(titel=titel, text=maskiere(text), prioritaet=HOCH, tags="warning"))
|
||||
return "gesendet" if fehler is None else f"nicht gesendet: {fehler}"
|
||||
except Exception as ausnahme: # die Kontrolle haengt nicht an der Meldung
|
||||
return f"nicht gesendet: {type(ausnahme).__name__}"
|
||||
|
||||
|
||||
# --- Ablauf ----------------------------------------------------------------------------------------
|
||||
|
||||
def kontrolliere(argumente: argparse.Namespace, k: Kontrolle) -> None:
|
||||
projekt = Path(argumente.projekt)
|
||||
tag = argumente.tag
|
||||
date.fromisoformat(tag) # ein falsches Datum ist ein Bedienfehler und bricht ab
|
||||
# handelstag_auswertung.py und der Melder lesen aus dem Arbeitsverzeichnis (Projektordner).
|
||||
os.chdir(projekt)
|
||||
|
||||
zustaende = k.schritt("Dienste", lies_dienste, argumente.dienste)
|
||||
if zustaende is not None:
|
||||
pruefe_dienste(zustaende, k)
|
||||
k.schritt("Killswitch", pruefe_killswitch, projekt, k)
|
||||
|
||||
liste = k.schritt("Kalender, und damit Lauf, Ende und Bericht,", lies_kalender, tag, argumente.kalender, projekt)
|
||||
handelstag = None if liste is None else ist_handelstag(tag, liste)
|
||||
k.fakten["handelstag"] = handelstag
|
||||
if handelstag is None:
|
||||
if liste is not None:
|
||||
k.befund("Kalender in unerwarteter Form; Lauf, Ende und Bericht ungeprueft")
|
||||
return
|
||||
dateien = protokolle(projekt, tag)
|
||||
if not handelstag:
|
||||
if dateien:
|
||||
k.hinweis(f"kein Handelstag, aber {len(dateien)} Protokoll(e) des scharfen Runners")
|
||||
return
|
||||
|
||||
if not dateien:
|
||||
k.befund("kein Protokoll des scharfen Runners an einem Handelstag -- der Runner lief nicht")
|
||||
laeufe = [lauf for lauf in (k.schritt(f"Protokoll {pfad.name}", lies_protokoll, pfad, k) for pfad in dateien) if lauf]
|
||||
k.fakten["laeufe"] = laeufe
|
||||
pruefe_laeufe(laeufe, k)
|
||||
|
||||
text = k.schritt("Journal", lies_journal, tag, argumente.journal)
|
||||
if text is not None:
|
||||
k.fakten["ende"] = pruefe_journal(text, k)
|
||||
|
||||
ausserhalb = {lauf["datei"] for lauf in laeufe if ausserhalb_der_handelszeit(lauf)}
|
||||
for pfad in dateien:
|
||||
if pfad.name in ausserhalb:
|
||||
continue
|
||||
k.schritt(f"Auswertung {pfad.name}", fahre_auswertung, pfad, projekt,
|
||||
not argumente.ohne_broker, not argumente.ohne_kerzen, k)
|
||||
|
||||
bericht = projekt / "data" / "paper" / "auswertungen" / f"{tag}.md"
|
||||
k.fakten["bericht"] = bericht.is_file()
|
||||
if not bericht.is_file():
|
||||
k.befund(f"Tagesbericht data/paper/auswertungen/{tag}.md fehlt")
|
||||
|
||||
|
||||
def parse_args(argv=None) -> argparse.Namespace:
|
||||
zerleger = argparse.ArgumentParser(description=__doc__.splitlines()[0], allow_abbrev=False)
|
||||
zerleger.add_argument("--tag")
|
||||
zerleger.add_argument("--meldung", nargs=2, metavar=("TITEL", "TEXT"))
|
||||
zerleger.add_argument("--projekt", default=PROJEKT)
|
||||
zerleger.add_argument("--kalender")
|
||||
zerleger.add_argument("--journal")
|
||||
zerleger.add_argument("--dienste")
|
||||
zerleger.add_argument("--ohne-broker", action="store_true")
|
||||
zerleger.add_argument("--ohne-kerzen", action="store_true")
|
||||
zerleger.add_argument("--ohne-meldung", action="store_true")
|
||||
argumente = zerleger.parse_args(argv)
|
||||
if bool(argumente.tag) == bool(argumente.meldung):
|
||||
zerleger.error("genau eines von --tag und --meldung")
|
||||
return argumente
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
os.environ.setdefault("TRADING_GEHEIMNISSE", GEHEIMNISSE)
|
||||
argumente = parse_args(argv)
|
||||
if argumente.meldung:
|
||||
print(json.dumps({"meldung": melde(*argumente.meldung, Path(argumente.projekt))}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
k = Kontrolle()
|
||||
code = 0
|
||||
try:
|
||||
kontrolliere(argumente, k)
|
||||
except Exception as ausnahme:
|
||||
k.befund(f"Abendkontrolle abgebrochen: {type(ausnahme).__name__}: {ausnahme}")
|
||||
code = 2
|
||||
if k.befunde and not argumente.ohne_meldung:
|
||||
text = "\n".join(f"- {befund}" for befund in k.befunde[:12])
|
||||
k.fakten["meldung"] = melde(
|
||||
f"Abendkontrolle {argumente.tag}: {len(k.befunde)} Befund(e)", text, Path(argumente.projekt)
|
||||
)
|
||||
print(json.dumps(
|
||||
{"tag": argumente.tag, "befunde": k.befunde, "hinweise": k.hinweise, "fakten": k.fakten, "exit": code},
|
||||
ensure_ascii=False, default=str,
|
||||
))
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,204 @@
|
||||
"""Paket AF ohne Netz: Gegenfall und Gegenprobe der Fortsetzung zum Tagesstart.
|
||||
|
||||
Koordinator-Werkzeug, nicht Teil des Repos (Paket AF, 2026-09-24). Faehrt die echte
|
||||
PaperSitzung des Arbeitsbaums mit der ausgelieferten config/paper.toml und ihrem Schalter
|
||||
[auto_fortsetzen] gegen einen ProbeBroker, mit echten IEX-Kerzen aus einer JSON-Datei von
|
||||
hole_kerzen.py (fuer jedes Instrument dieselben), einer ProbeUhr, einem Wegwerf-Register
|
||||
mit der Neustartregel und einem Melder, der nur sammelt. Kein Netz, keine Order an Alpaca,
|
||||
das echte Register bleibt unberuehrt.
|
||||
|
||||
Drei Faelle:
|
||||
restposition vor der Oeffnung gestartet, Restposition beim Start -> nicht fortgesetzt
|
||||
mitten 60 Marktminuten nach der Oeffnung gestartet, flach -> nicht fortgesetzt
|
||||
flach vor der Oeffnung gestartet, flach -> fortgesetzt (die Gegenprobe)
|
||||
Exit 0 heisst: Der Fall lief wie erwartet. Jede Abweichung ist eine Zeile BEFUND und Exit 2,
|
||||
eine fehlende Voraussetzung Exit 3.
|
||||
Aufruf (WSL, aus dem Arbeitsbaum): python af_gegenfall.py <kerzen.json> <fall>
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
from trading.killswitch.killswitch import Killswitch # noqa: E402
|
||||
from trading.killswitch.killswitch import lade_killswitch_config # noqa: E402
|
||||
from trading.paper.agenten import agenten_je_instrument # noqa: E402
|
||||
from trading.paper.agenten import beschreibung_je_instrument # noqa: E402
|
||||
from trading.paper.agenten import lade_agent # noqa: E402
|
||||
from trading.paper.config import enge_limits # noqa: E402
|
||||
from trading.paper.config import lade_paper_config # noqa: E402
|
||||
from trading.paper.kerzen import FesteKerzenquelle # noqa: E402
|
||||
from trading.paper.kerzen import Kerze # noqa: E402
|
||||
from trading.paper.probe import ProbeBroker # noqa: E402
|
||||
from trading.paper.sitzung import PaperSitzung # noqa: E402
|
||||
from trading.paper.uhr import ProbeUhr # noqa: E402
|
||||
from trading.registry.registry import Registry # noqa: E402
|
||||
from trading.risk.limits import lade_limits # noqa: E402
|
||||
|
||||
FAELLE = ("restposition", "mitten", "flach")
|
||||
RESTPOSITION = 10
|
||||
|
||||
|
||||
class SammelMelder:
|
||||
"""Nimmt jede Meldung an und schickt nichts."""
|
||||
|
||||
aktiv = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.gesehen = []
|
||||
|
||||
def melde(self, meldung):
|
||||
self.gesehen.append(meldung)
|
||||
return None
|
||||
|
||||
|
||||
class Protokoll:
|
||||
"""Haelt die Zeilen im Speicher, durch json.dumps wie das echte Protokoll."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.zeilen = []
|
||||
|
||||
def schreibe(self, eintrag) -> None:
|
||||
self.zeilen.append(json.loads(json.dumps(eintrag, ensure_ascii=False, default=str)))
|
||||
|
||||
|
||||
def lies_kerzen(pfad: Path) -> list:
|
||||
roh = json.loads(pfad.read_text(encoding="utf-8"))
|
||||
return [
|
||||
Kerze(
|
||||
zeit=datetime.fromisoformat(k["zeit"]),
|
||||
open=float(k["open"]),
|
||||
high=float(k["high"]),
|
||||
low=float(k["low"]),
|
||||
close=float(k["close"]),
|
||||
volume=float(k["volume"]),
|
||||
)
|
||||
for k in roh
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3 or sys.argv[2] not in FAELLE:
|
||||
print(f"Aufruf: python af_gegenfall.py <kerzen.json> <{'|'.join(FAELLE)}>")
|
||||
return 3
|
||||
pfad, fall = Path(sys.argv[1]), sys.argv[2]
|
||||
if not pfad.is_file():
|
||||
print(f"VORAUSSETZUNG FEHLT: {pfad}")
|
||||
return 3
|
||||
kerzen = sorted(lies_kerzen(pfad), key=lambda kerze: kerze.zeit)
|
||||
if not kerzen:
|
||||
print(f"VORAUSSETZUNG FEHLT: keine Kerze in {pfad}")
|
||||
return 3
|
||||
config = lade_paper_config("config/paper.toml")
|
||||
if not getattr(config, "auto_fortsetzen", False):
|
||||
print("VORAUSSETZUNG FEHLT: config/paper.toml traegt [auto_fortsetzen] aktiv = true nicht")
|
||||
return 3
|
||||
limits = enge_limits(lade_limits("config/risk.toml"), config)
|
||||
ks_config = lade_killswitch_config("config/killswitch.toml")
|
||||
quelle = FesteKerzenquelle(kerzen)
|
||||
if fall == "mitten":
|
||||
beginn = kerzen[0].zeit + timedelta(minutes=60)
|
||||
else:
|
||||
beginn = kerzen[0].zeit - timedelta(minutes=30)
|
||||
uhr = ProbeUhr(beginn)
|
||||
rest = config.alle_instrumente[-1]
|
||||
broker = ProbeBroker(
|
||||
kerzenquelle=quelle,
|
||||
uhr=uhr,
|
||||
instrument_id=config.instrument_id,
|
||||
instrumente=config.alle_instrumente,
|
||||
positionen={rest: RESTPOSITION} if fall == "restposition" else None,
|
||||
)
|
||||
protokoll = Protokoll()
|
||||
melder = SammelMelder()
|
||||
with tempfile.TemporaryDirectory(prefix="af-gegenfall-") as wurzel:
|
||||
ordner = Path(wurzel)
|
||||
registry = Registry(db_pfad=ordner / "registry.db", artefakt_wurzel=ordner / "artifacts")
|
||||
Killswitch.laden(broker=broker, registry=registry, config=ks_config)
|
||||
agent, beschreibung = lade_agent(config, registry)
|
||||
sitzung = PaperSitzung(
|
||||
broker=broker,
|
||||
kerzenquelle=quelle,
|
||||
agent=agent,
|
||||
agent_beschreibung=beschreibung_je_instrument(config, beschreibung),
|
||||
agenten=agenten_je_instrument(config, agent),
|
||||
registry=registry,
|
||||
limits=limits,
|
||||
killswitch_config=ks_config,
|
||||
config=config,
|
||||
uhr=uhr,
|
||||
protokoll=protokoll,
|
||||
protokoll_pfad="af-gegenfall.jsonl",
|
||||
status_pfad=ordner / "status.json",
|
||||
modus="probe",
|
||||
soll_abbrechen=lambda: False,
|
||||
melder=melder,
|
||||
)
|
||||
code = sitzung.laufe()
|
||||
gespeichert = registry.killswitch_zustand()
|
||||
ereignisse = registry.killswitch_ereignisse()
|
||||
|
||||
auto = [z for z in protokoll.zeilen if z.get("was") == "auto_fortsetzen"]
|
||||
orders = [z for z in protokoll.zeilen if z.get("art") == "order"]
|
||||
takte = [z for z in protokoll.zeilen if z.get("art") == "takt"]
|
||||
print(f"Fall {fall}, {len(kerzen)} Kerzen ab {kerzen[0].zeit.isoformat()}, Beginn {beginn.isoformat()}")
|
||||
print(f"Instrumente: {', '.join(config.alle_instrumente)}")
|
||||
for zeile in auto:
|
||||
print(f"auto_fortsetzen {zeile.get('ergebnis')} | {zeile.get('text')} | phase {zeile.get('phase')}")
|
||||
for meldung in melder.gesehen:
|
||||
print(f"Meldung [{meldung.prioritaet}] {meldung.titel}")
|
||||
for zeile in meldung.text.split("\n"):
|
||||
if zeile.startswith(("Killswitch", "Nicht selbst fortgesetzt", "Ausloeser", "Position")):
|
||||
print(f" {zeile}")
|
||||
print(f"Killswitch am Ende: {gespeichert.zustand}, Ausloeser {gespeichert.ausloeser}")
|
||||
print(f"Killswitch-Ereignisse: {[(e.stufe, e.ausloeser) for e in ereignisse]}")
|
||||
print(f"Takte {len(takte)}, Orders {len(orders)}, Exit {code}")
|
||||
|
||||
befunde = []
|
||||
titel = [m.titel for m in melder.gesehen]
|
||||
automatik = [e for e in ereignisse if e.ausloeser == "automatik"]
|
||||
if fall == "flach":
|
||||
if [z.get("ergebnis") for z in auto] != ["fortgesetzt"]:
|
||||
befunde.append(f"auto_fortsetzen {[z.get('ergebnis') for z in auto]} statt ['fortgesetzt']")
|
||||
if "Paper-Runner laeuft" not in titel or "Paper-Runner wartet" in titel:
|
||||
befunde.append(f"Meldungen {titel}")
|
||||
if len(automatik) != 1 or automatik[0].stufe != "fortsetzen":
|
||||
befunde.append(f"Ereignisse mit Ausloeser automatik: {[(e.stufe, e.grund[:60]) for e in automatik]}")
|
||||
if not orders:
|
||||
befunde.append("keine Order -- fortgesetzt, aber nicht gehandelt")
|
||||
if code != 0:
|
||||
befunde.append(f"Exit {code} statt 0")
|
||||
else:
|
||||
if fall == "restposition":
|
||||
erwartet = f"Konto nicht flach: {rest} {RESTPOSITION}"
|
||||
else:
|
||||
erwartet = "dieser Prozess ist nicht vor der Oeffnung gestartet"
|
||||
if [z.get("text") for z in auto] != [f"nicht_fortgesetzt: {erwartet}"]:
|
||||
befunde.append(f"auto_fortsetzen {[z.get('text') for z in auto]}")
|
||||
wartet = [m for m in melder.gesehen if m.titel == "Paper-Runner wartet"]
|
||||
if len(wartet) != 1 or f"Nicht selbst fortgesetzt: {erwartet}" not in wartet[0].text.split("\n"):
|
||||
befunde.append(f"Meldungen {titel}")
|
||||
if automatik:
|
||||
befunde.append(f"{len(automatik)} Ereignis(se) mit Ausloeser automatik")
|
||||
if orders:
|
||||
befunde.append(f"{len(orders)} Order(s) ohne Fortsetzung")
|
||||
if any(t.get("killswitch") != "angehalten" for t in takte):
|
||||
befunde.append("ein Takt nicht angehalten")
|
||||
erwarteter_code = 4 if fall == "restposition" else 0
|
||||
if code != erwarteter_code:
|
||||
befunde.append(f"Exit {code} statt {erwarteter_code}")
|
||||
for befund in befunde:
|
||||
print(f"BEFUND: {befund}")
|
||||
if befunde:
|
||||
return 2
|
||||
print("ERGEBNIS: Fall wie erwartet")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Fragt pruefe_register (Paket AF) gegen ein Killswitch-Register, nur lesend.
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo (2026-09-24, Befund der AF-Pruefung: das Drehbuch hatte fuer
|
||||
die Registerpruefung nur eine Erwartung in Prosa). Oeffnet data/registry.db mit mode=ro -- die Klasse
|
||||
Registry fuehrt beim Oeffnen das Schema aus und schriebe damit in die Datei --, baut Zustand und
|
||||
Ereignisse als einfache Objekte und ruft die echte Funktion trading.paper.auto_fortsetzen.pruefe_register
|
||||
des Projekts. Druckt den Befund und die juengsten Ereignisse (Stufe, Ausloeser, Zeit), nie einen Betrag.
|
||||
|
||||
Was es nicht sieht: Konto, Spielgeld, Uhr und Schalter -- das pruefen Sitzung und vorstart_pruefung.py.
|
||||
Vor der Oeffnung am Handelstag steht der Killswitch nach dem Dashboard-Neustart auf angehalten/neustart;
|
||||
davor (etwa nach Pascals Klick am Vortag) ist "steht auf aktiv" der erwartete Befund, kein Fehler.
|
||||
|
||||
Aufruf (WSL, im Projektordner): ~/venvs/trading/bin/python af_registerprobe.py <projektordner>
|
||||
Exit 0: erfuellt, 1: nicht erfuellt, 3: Register fehlt.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
argumente = sys.argv[1:] if argv is None else argv
|
||||
projekt = Path(argumente[0] if argumente else ".").resolve()
|
||||
db = projekt / "data" / "registry.db"
|
||||
if not db.is_file():
|
||||
print(f"ABBRUCH: {db} fehlt")
|
||||
return 3
|
||||
sys.path.insert(0, str(projekt))
|
||||
from trading.paper.auto_fortsetzen import pruefe_register
|
||||
|
||||
verbindung = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
|
||||
try:
|
||||
zeile = verbindung.execute("select zustand, ausloeser from killswitch_zustand").fetchone()
|
||||
ereignisse = [
|
||||
SimpleNamespace(id=nummer, stufe=stufe, ausloeser=ausloeser, zeit=zeit)
|
||||
for nummer, stufe, ausloeser, zeit in verbindung.execute(
|
||||
"select id, stufe, ausloeser, zeit from killswitch_ereignisse order by id"
|
||||
)
|
||||
]
|
||||
finally:
|
||||
verbindung.close()
|
||||
gespeichert = None if zeile is None else SimpleNamespace(zustand=zeile[0], ausloeser=zeile[1])
|
||||
befund = pruefe_register(gespeichert, ereignisse)
|
||||
print(f"Zustand: {zeile}")
|
||||
for ereignis in ereignisse[-4:]:
|
||||
print(f" Nr. {ereignis.id}: {ereignis.stufe} von {ereignis.ausloeser}, {ereignis.zeit}")
|
||||
print(f"pruefe_register: {'erfuellt' if befund.erfuellt else 'NICHT erfuellt'} -- {befund.grund}")
|
||||
return 0 if befund.erfuellt else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Dashboard-Neustart nach einem Einspielen, mit Tor 6b (liest das Dashboard die Geheimnisdatei unter PrivateUsers=yes?).
|
||||
# Koordinator-Werkzeug, nicht fuers Repo. Pascals dauerhafte Freigabe gilt fuer genau diesen Neustart, nur ausserhalb der
|
||||
# Handelszeit; das Skript verweigert, solange der Runner laeuft. Aufruf aus der PowerShell mit WSL_PW per WSLENV:
|
||||
# wsl -d Debian -u claude -- bash /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/dashboard_neustart.sh
|
||||
set -u
|
||||
[ -n "${WSL_PW:-}" ] || { echo "WSL_PW fehlt"; exit 20; }
|
||||
S() { printf '%s\n' "$WSL_PW" | sudo -S -p '' "$@"; }
|
||||
if systemctl is-active --quiet trading-paper.service; then echo "Runner laeuft - kein Neustart"; exit 21; fi
|
||||
date
|
||||
S systemctl restart trading-dashboard.service || exit 22
|
||||
sleep 8
|
||||
systemctl is-active trading-dashboard.service
|
||||
systemctl show trading-dashboard.service --property=NRestarts,ActiveEnterTimestamp
|
||||
journalctl -u trading-dashboard.service --since "-1min" --no-pager | tail -10 | sed -E 's/[0-9]{6,}/######/g'
|
||||
if journalctl -u trading-dashboard.service --since "-1min" --no-pager | grep -E "kann sie aber nicht lesen|PermissionError|secrets.toml|Traceback"; then
|
||||
echo "TOR 6b ZU"; exit 27
|
||||
fi
|
||||
echo "Tor 6b offen"
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: 127.0.0.1:8144" http://127.0.0.1:8144/api/gesund)
|
||||
echo "api/gesund: $code"
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: 127.0.0.1:8144" http://127.0.0.1:8144/)
|
||||
echo "Oberflaeche /: $code"
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Vor dem Push: kommt ein Wert aus der Geheimnis- oder Zugangsdatei in den neuen Commits vor?
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo (Pascal 2026-09-24: Push jederzeit, vorher dieser Scan).
|
||||
Regressionsnachweis vor jedem Scan: SCAN_PROBE=<bekannter Text aus dem Verlauf> muss einen Treffer melden.
|
||||
|
||||
Liest die Werte, gibt sie nie aus, nur Schluesselname und Trefferzahl. Laeuft in WSL als claude.
|
||||
Aufruf: python geheimnis_scan.py <repo> <von> <bis> [<bis> ...]
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
GEHEIM = Path("/home/claude/.config/trading-plattform/secrets.toml")
|
||||
repo = Path(sys.argv[1])
|
||||
von = sys.argv[2]
|
||||
ziele = sys.argv[3:]
|
||||
|
||||
|
||||
def blaetter(tabelle, pfad=""):
|
||||
for schluessel, wert in tabelle.items():
|
||||
name = f"{pfad}.{schluessel}" if pfad else schluessel
|
||||
if isinstance(wert, dict):
|
||||
yield from blaetter(wert, name)
|
||||
elif isinstance(wert, str):
|
||||
yield name, wert
|
||||
|
||||
|
||||
werte = []
|
||||
for datei in (GEHEIM, repo / "config" / "zugang.toml"):
|
||||
if not datei.exists():
|
||||
print(f"BEFUND: {datei.name} fehlt, Scan ohne diese Werte")
|
||||
continue
|
||||
with datei.open("rb") as f:
|
||||
daten = tomllib.load(f)
|
||||
for name, wert in blaetter(daten):
|
||||
# Kurze Werte (true/false, Hostnamen wie localhost, Modellnamen) treffen Prosa.
|
||||
if len(wert) >= 12:
|
||||
werte.append((f"{datei.name}:{name}", wert))
|
||||
schluessel_json = repo / "config" / "zugang-schluessel.json"
|
||||
if schluessel_json.exists():
|
||||
import json
|
||||
for i, eintrag in enumerate(json.loads(schluessel_json.read_text(encoding="utf-8")) or []):
|
||||
if isinstance(eintrag, dict):
|
||||
for k, v in eintrag.items():
|
||||
if isinstance(v, str) and len(v) >= 12:
|
||||
werte.append((f"zugang-schluessel.json:{i}.{k}", v))
|
||||
# Seit 2026-09-24 (Paket IB): die IBKR-Zugangsdateien. Die Kontonummer hat 9 Zeichen, deshalb
|
||||
# hier ab 6 statt ab 12; diese drei Werte stehen in keiner Prosa.
|
||||
IBKR = Path("/home/claude/.config/trading-plattform/ibkr")
|
||||
for name in ("paper_benutzer", "paper_passwort", "paper_konto"):
|
||||
datei = IBKR / name
|
||||
if datei.exists():
|
||||
wert = datei.read_text(encoding="utf-8").strip()
|
||||
if len(wert) >= 6:
|
||||
werte.append((f"ibkr:{name}", wert))
|
||||
else:
|
||||
print(f"BEFUND: ibkr/{name} fehlt, Scan ohne diesen Wert")
|
||||
import os
|
||||
if os.environ.get("SCAN_PROBE"):
|
||||
# Regressionsnachweis: ein bekannter Text aus dem Verlauf muss als Treffer erscheinen.
|
||||
werte.append(("probe", os.environ["SCAN_PROBE"]))
|
||||
print(f"geprueft: {len(werte)} Werte ab 12 Zeichen")
|
||||
|
||||
befunde = 0
|
||||
for ziel in ziele:
|
||||
text = subprocess.run(
|
||||
["git", "-c", "safe.directory=*", "-C", str(repo), "log", "-p", "--all-match", f"{von}..{ziel}"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
).stdout
|
||||
print(f"{von}..{ziel}: {len(text)} Zeichen Verlauf")
|
||||
for name, wert in werte:
|
||||
n = text.count(wert)
|
||||
if n:
|
||||
befunde += 1
|
||||
print(f" TREFFER {name}: {n}")
|
||||
verfolgt = subprocess.run(
|
||||
["git", "-c", "safe.directory=*", "-C", str(repo), "ls-tree", "-r", "--name-only", ziel],
|
||||
capture_output=True, text=True,
|
||||
).stdout.splitlines()
|
||||
for gesperrt in ("config/secrets.toml", "config/zugang.toml", "config/zugang-schluessel.json"):
|
||||
if gesperrt in verfolgt:
|
||||
befunde += 1
|
||||
print(f" TREFFER verfolgt: {gesperrt}")
|
||||
print(f"ERGEBNIS: {befunde} Treffer")
|
||||
sys.exit(1 if befunde else 0)
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# Erster Anmeldetest am IBKR-Paper-Konto: IB Gateway als Container in WSL, nur lesend.
|
||||
# Koordinator-Werkzeug, nicht fuers Repo. Pascal am 2026-09-24: Container in WSL, beide Downloads frei
|
||||
# (ghcr.io/gnzsnz/ib-gateway, festgelegt auf die Pruefsumme unten; nautilus-ibapi im eigenen Venv
|
||||
# ~/venvs/ibkr-probe), Test nach 22:00, zweiter Faktor bei ihm nur ein Passkey.
|
||||
# Das Abbild ist dasselbe, das NautilusTrader 1.231.0 mit DockerizedIBGatewayConfig startet
|
||||
# (config.py: container_image "ghcr.io/gnzsnz/ib-gateway:stable"; gateway.py: Paper aussen 4004).
|
||||
# Die Zugangsdaten liest der Container aus /home/claude/.config/trading-plattform/ibkr/ (Pascal traegt sie
|
||||
# mit ibkr_zugang_eintragen.sh ein); dieses Skript gibt sie nie aus und maskiert den Benutzernamen im Log.
|
||||
# Aufruf aus der PowerShell (sudo per WSLENV, wie bei den anderen Betriebsskripten):
|
||||
# $env:WSL_PW=[Environment]::GetEnvironmentVariable('DEBIAN_WSL_PASSWORD','User'); $env:WSLENV='WSL_PW'
|
||||
# wsl -d Debian -u claude -- bash /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/ibkr_gateway_test.sh <schritt>
|
||||
# Schritte: laden | pruefen | starten | log | probe | stoppen
|
||||
set -u
|
||||
BILD="ghcr.io/gnzsnz/ib-gateway@sha256:5fb04cb93cd705eff8a5a58b015503003864ffd9ea890be4d07273389149aa9f"
|
||||
ZUGANG=/home/claude/.config/trading-plattform/ibkr
|
||||
NAME=ibkr-gateway-probe
|
||||
PROBE_PY=/home/claude/venvs/ibkr-probe/bin/python
|
||||
PROBE=/mnt/d/Claude/.vault-tools/trading-plattform-pruefung/ibkr_probe.py
|
||||
|
||||
[ -n "${WSL_PW:-}" ] || { echo "WSL_PW fehlt (sudo per WSLENV)"; exit 20; }
|
||||
S() { printf '%s\n' "$WSL_PW" | sudo -S -p '' "$@"; }
|
||||
|
||||
stunde=$(TZ=Europe/Berlin date +%-H)
|
||||
# RUHE_AUSNAHME: nur auf Pascals ausdrueckliche Anweisung fuer einen einzelnen Lauf (so am 2026-09-24,
|
||||
# 17:0x: "Bitte schon einmal testen ohne andere Workflows zu gefaehrden"); der Wert nennt Datum und Anlass.
|
||||
if [ "$stunde" -ge 15 ] && [ "$stunde" -lt 22 ] && [ -z "${RUHE_AUSNAHME:-}" ]; then
|
||||
echo "Ruhe-Regel: zwischen 15 und 22 Uhr Berlin laeuft hier nichts (jetzt $(TZ=Europe/Berlin date +%H:%M))."
|
||||
exit 21
|
||||
fi
|
||||
|
||||
maske() {
|
||||
# Den Benutzernamen und lange Ziffernfolgen aus fremder Ausgabe nehmen, ohne ihn selbst zu zeigen.
|
||||
python3 -c '
|
||||
import re, sys
|
||||
from pathlib import Path
|
||||
benutzer = ""
|
||||
p = Path(sys.argv[1])
|
||||
if p.is_file():
|
||||
benutzer = p.read_text(encoding="utf-8").strip()
|
||||
for zeile in sys.stdin:
|
||||
if benutzer:
|
||||
zeile = zeile.replace(benutzer, "[benutzer]")
|
||||
# Kontonummern: ein bis vier Grossbuchstaben und mindestens fuenf Ziffern. Die erste Fassung kannte
|
||||
# nur DU plus Ziffern; das Paper-Konto hat einen Buchstaben mehr und stand am 2026-09-24 unmaskiert im Log.
|
||||
zeile = re.sub(r"\b[A-Z]{1,4}\d{5,}\b", "[konto]", zeile)
|
||||
sys.stdout.write(zeile)
|
||||
' "$ZUGANG/paper_benutzer"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
laden)
|
||||
S docker pull "$BILD" 2>&1 | tail -n 3
|
||||
S docker image inspect "$BILD" --format 'Id {{.Id}} Groesse {{.Size}} Erstellt {{.Created}}'
|
||||
;;
|
||||
pruefen)
|
||||
# Welche Variablen liest das Abbild aus Dateien, und unter welcher UID laeuft es?
|
||||
S docker run --rm --entrypoint sh "$BILD" -c '
|
||||
echo "== id"; id
|
||||
echo "== _FILE-Variablen in den Startskripten"; grep -rhoE "[A-Z_]+_FILE" /home /usr/local/bin /opt 2>/dev/null | sort -u | head -20
|
||||
echo "== Ports"; grep -rhoE "4003|4004|4001|4002|5900" /home /usr/local/bin 2>/dev/null | sort | uniq -c | head
|
||||
' 2>&1 | head -40
|
||||
;;
|
||||
starten)
|
||||
for datei in paper_benutzer paper_passwort; do
|
||||
[ -s "$ZUGANG/$datei" ] || { echo "fehlt: $ZUGANG/$datei (erst ibkr_zugang_eintragen.sh)"; exit 22; }
|
||||
done
|
||||
S docker rm -f "$NAME" >/dev/null 2>&1
|
||||
# Benutzername und Passwort aus Dateien im Container; TWS_USERID_FILE nur, wenn "pruefen" es fand,
|
||||
# sonst setzt BENUTZER_ALS_VARIABLE=1 den Namen als Variable (er ist kein Geheimnis wie das Passwort).
|
||||
if [ "${BENUTZER_ALS_VARIABLE:-0}" = "1" ]; then
|
||||
BENUTZER_ARG=(-e "TWS_USERID=$(cat "$ZUGANG/paper_benutzer")")
|
||||
else
|
||||
BENUTZER_ARG=(-e "TWS_USERID_FILE=/run/secrets/ibkr/paper_benutzer")
|
||||
fi
|
||||
S docker run -d --name "$NAME" \
|
||||
-e TRADING_MODE=paper -e READ_ONLY_API=yes -e TWOFA_TIMEOUT_ACTION=exit \
|
||||
-e TWS_PASSWORD_FILE=/run/secrets/ibkr/paper_passwort "${BENUTZER_ARG[@]}" \
|
||||
-e TIME_ZONE=America/New_York \
|
||||
-v "$ZUGANG:/run/secrets/ibkr:ro" \
|
||||
-p 127.0.0.1:4004:4004 \
|
||||
"$BILD" >/dev/null && echo "gestartet: $NAME (Paper, nur lesend, Port 127.0.0.1:4004)"
|
||||
;;
|
||||
log)
|
||||
S docker logs --tail "${2:-60}" "$NAME" 2>&1 | maske
|
||||
;;
|
||||
probe)
|
||||
cd /tmp && "$PROBE_PY" "$PROBE" --port 4004
|
||||
;;
|
||||
stoppen)
|
||||
S docker rm -f "$NAME" 2>&1 | tail -n 1
|
||||
;;
|
||||
*)
|
||||
echo "Schritt fehlt: laden | pruefen | starten | log | probe | stoppen"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Lesende Probe gegen das IBKR-Paper-Konto ueber ein laufendes IB Gateway: verbindet, liest, trennt.
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Pascal am 2026-09-24: Zugang zum IBKR-Paper-Konto vorbereiten,
|
||||
Gateway als Container in WSL (ghcr.io/gnzsnz/ib-gateway, wie DockerizedIBGatewayConfig in NautilusTrader
|
||||
1.231.0), erster Test heute nach 22:00. Diese Probe handelt nie: Das Gateway laeuft mit READ_ONLY_API=yes,
|
||||
und sie ruft keine Order-Methode.
|
||||
|
||||
Entscheidungsliste (Frage -> Herkunft der Antwort):
|
||||
1. Erreicht die Probe das Gateway? Quelle: EClient.connect/isConnected (nautilus-ibapi 10.45.1),
|
||||
Port 4004 = Paper aussen (gateway.py:43-45 in Nautilus 1.231.0).
|
||||
2. Welches Konto ist angemeldet? Quelle: managedAccounts-Rueckruf. Ausgegeben: die ersten zwei
|
||||
Zeichen und die Laenge (Paper-Konten beginnen im Doku-Beispiel
|
||||
mit "DU"; das ist ein Beispiel, keine Zusage).
|
||||
3. Welche Kontoart, welche Waehrung? Quelle: reqAccountSummary mit den Tags AccountType, Currency;
|
||||
Betraege (NetLiquidation, TotalCashValue) nur als "vorhanden".
|
||||
4. Wie viele Positionen? Quelle: reqPositions -> positionEnd; nur die Zahl.
|
||||
5. Kommt ein verzoegerter Kurs? Quelle: reqMarketDataType(4, DELAYED_FROZEN; ohne Abo laut
|
||||
Nautilus-Doku) und reqMktData(AAPL, SMART, USD, Snapshot).
|
||||
Ausgegeben: welche Kursart kam (Tick-Typ), nie der Kurs.
|
||||
6. Welche Fehler meldet das Gateway? Quelle: error-Rueckruf; Code und gekuerzter Text (ohne Zahlen
|
||||
ueber vier Stellen).
|
||||
Eine fehlende Antwort nach ZEITLIMIT_S ist ein Befund, kein Schweigen. Exit 0 alles beantwortet,
|
||||
1 mindestens ein Befund, 3 keine Verbindung, 2 abgebrochen.
|
||||
Aufruf (WSL, eigenes Venv): ~/venvs/ibkr-probe/bin/python ibkr_probe.py [--port 4004] [--client-id 991]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
ZEITLIMIT_S = 30
|
||||
NUR_LESEN_TAGS = "AccountType,Currency,NetLiquidation,TotalCashValue"
|
||||
BETRAGS_TAGS = {"NetLiquidation", "TotalCashValue"}
|
||||
|
||||
|
||||
def maskiere(text: str) -> str:
|
||||
return re.sub(r"\d{5,}|\d+\.\d+", "[x]", str(text))
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
zerleger = argparse.ArgumentParser(allow_abbrev=False)
|
||||
zerleger.add_argument("--host", default="127.0.0.1")
|
||||
zerleger.add_argument("--port", type=int, default=4004)
|
||||
zerleger.add_argument("--client-id", type=int, default=991)
|
||||
argumente = zerleger.parse_args(argv)
|
||||
|
||||
from ibapi.client import EClient
|
||||
from ibapi.contract import Contract
|
||||
from ibapi.wrapper import EWrapper
|
||||
|
||||
class Probe(EWrapper, EClient):
|
||||
def __init__(self):
|
||||
EWrapper.__init__(self)
|
||||
EClient.__init__(self, self)
|
||||
self.ereignisse = {name: threading.Event() for name in ("konto", "zusammenfassung", "positionen", "kurs", "zeit")}
|
||||
self.konten = ""
|
||||
self.zusammenfassung = {}
|
||||
self.positionen = 0
|
||||
self.kurs_arten = set()
|
||||
self.fehler = []
|
||||
self.zeit = None
|
||||
|
||||
def managedAccounts(self, accountsList):
|
||||
self.konten = accountsList
|
||||
self.ereignisse["konto"].set()
|
||||
|
||||
def currentTime(self, zeit):
|
||||
self.zeit = zeit
|
||||
self.ereignisse["zeit"].set()
|
||||
|
||||
def accountSummary(self, reqId, account, tag, value, currency):
|
||||
self.zusammenfassung[tag] = "vorhanden" if tag in BETRAGS_TAGS else value
|
||||
|
||||
def accountSummaryEnd(self, reqId):
|
||||
self.ereignisse["zusammenfassung"].set()
|
||||
|
||||
def position(self, account, contract, position, avgCost):
|
||||
self.positionen += 1
|
||||
|
||||
def positionEnd(self):
|
||||
self.ereignisse["positionen"].set()
|
||||
|
||||
def tickPrice(self, reqId, tickType, price, attrib):
|
||||
if price and price > 0:
|
||||
self.kurs_arten.add(tickType)
|
||||
|
||||
def tickSnapshotEnd(self, reqId):
|
||||
self.ereignisse["kurs"].set()
|
||||
|
||||
def error(self, reqId, *rest):
|
||||
# Die Signatur wechselt zwischen ibapi-Versionen: nautilus-ibapi 10.45.1 schickt vor dem
|
||||
# Code einen Zeitstempel in Millisekunden (gemessen am 2026-09-24: 1790261211141 vor 502).
|
||||
# Fehlercodes sind klein; der Zeitstempel ist es nicht.
|
||||
werte = [r for r in rest if isinstance(r, (int, str))]
|
||||
code = next((w for w in werte if isinstance(w, int) and 0 < w < 100000), None)
|
||||
text = next((w for w in werte if isinstance(w, str) and len(w) > 3), "")
|
||||
self.fehler.append((reqId, code, maskiere(text)[:160]))
|
||||
|
||||
probe = Probe()
|
||||
befunde = []
|
||||
try:
|
||||
probe.connect(argumente.host, argumente.port, argumente.client_id)
|
||||
faden = threading.Thread(target=probe.run, daemon=True)
|
||||
faden.start()
|
||||
beginn = time.time()
|
||||
while not probe.isConnected() and time.time() - beginn < 10:
|
||||
time.sleep(0.2)
|
||||
if not probe.isConnected():
|
||||
print(f"BEFUND: keine Verbindung zu {argumente.host}:{argumente.port}")
|
||||
for f in probe.fehler[:5]:
|
||||
print(f" fehler {f}")
|
||||
return 3
|
||||
print(f"verbunden mit {argumente.host}:{argumente.port}, Serverversion {probe.serverVersion()}")
|
||||
|
||||
probe.reqCurrentTime()
|
||||
probe.reqAccountSummary(9001, "All", NUR_LESEN_TAGS)
|
||||
probe.reqPositions()
|
||||
probe.reqMarketDataType(4)
|
||||
aapl = Contract()
|
||||
aapl.symbol, aapl.secType, aapl.exchange, aapl.currency = "AAPL", "STK", "SMART", "USD"
|
||||
probe.reqMktData(9002, aapl, "", True, False, [])
|
||||
|
||||
for name, ereignis in probe.ereignisse.items():
|
||||
if not ereignis.wait(ZEITLIMIT_S):
|
||||
befunde.append(f"keine Antwort auf {name} in {ZEITLIMIT_S} s")
|
||||
|
||||
konten = [k for k in probe.konten.split(",") if k]
|
||||
print(f"konten: {len(konten)}; " + ", ".join(f"{k[:2]}… ({len(k)} Zeichen)" for k in konten))
|
||||
print(f"serverzeit: {'da' if probe.zeit else 'fehlt'}")
|
||||
print(f"kontoart: {probe.zusammenfassung.get('AccountType')}, waehrung: {probe.zusammenfassung.get('Currency')}, "
|
||||
f"nettoliquidation: {probe.zusammenfassung.get('NetLiquidation', 'fehlt')}, bargeld: {probe.zusammenfassung.get('TotalCashValue', 'fehlt')}")
|
||||
print(f"positionen: {probe.positionen}")
|
||||
print(f"kursarten AAPL (Tick-Typen mit Preis): {sorted(probe.kurs_arten) or 'keine'}")
|
||||
for f in probe.fehler:
|
||||
print(f"gateway meldet: reqId {f[0]}, code {f[1]}: {f[2]}")
|
||||
if not konten:
|
||||
befunde.append("kein Konto gemeldet")
|
||||
if not probe.kurs_arten:
|
||||
befunde.append("kein verzoegerter Kurs fuer AAPL")
|
||||
probe.cancelAccountSummary(9001)
|
||||
probe.cancelPositions()
|
||||
except Exception as ausnahme:
|
||||
print(f"BEFUND: Probe abgebrochen: {type(ausnahme).__name__}: {maskiere(ausnahme)[:200]}")
|
||||
return 2
|
||||
finally:
|
||||
if probe.isConnected():
|
||||
probe.disconnect()
|
||||
for befund in befunde:
|
||||
print(f"BEFUND: {befund}")
|
||||
print(f"ERGEBNIS: {len(befunde)} Befund(e)")
|
||||
return 1 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Traegt die Zugangsdaten des IBKR-Paper-Kontos ein. Pascal fuehrt es selbst in seinem Terminal aus;
|
||||
# kein Agent sieht die Werte. Passwort ohne Anzeige, zweimal zur Kontrolle.
|
||||
# Ablage: /home/claude/.config/trading-plattform/ibkr/ (Ordner 0700, Dateien 0600, Besitzer claude),
|
||||
# neben der Geheimnisdatei aus Paket E2. Ausgegeben werden nur Dateinamen und Laengen.
|
||||
# Koordinator-Werkzeug, nicht fuers Repo. Aufruf aus der PowerShell:
|
||||
# wsl -d Debian -u claude -- bash /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/ibkr_zugang_eintragen.sh
|
||||
set -eu
|
||||
umask 077
|
||||
# IBKR_ZIEL nur fuer die Probe des Koordinators mit erfundenen Werten in einem Wegwerf-Ordner.
|
||||
ZIEL=${IBKR_ZIEL:-/home/claude/.config/trading-plattform/ibkr}
|
||||
if [ "$(id -un)" != "claude" ]; then
|
||||
echo "Bitte als Benutzer claude ausfuehren (wsl -d Debian -u claude -- bash ...)."
|
||||
exit 2
|
||||
fi
|
||||
mkdir -p "$ZIEL"
|
||||
chmod 700 "$ZIEL"
|
||||
echo "IBKR-Paper-Konto: Zugangsdaten eintragen (nichts davon verlaesst diesen Rechner)."
|
||||
read -r -p "Paper-Benutzername: " BENUTZER
|
||||
read -r -s -p "Paper-Passwort (wird nicht angezeigt): " PASSWORT
|
||||
echo
|
||||
read -r -s -p "Passwort noch einmal: " PASSWORT2
|
||||
echo
|
||||
if [ -z "$BENUTZER" ] || [ -z "$PASSWORT" ]; then
|
||||
echo "Benutzername oder Passwort leer, nichts geschrieben."
|
||||
exit 2
|
||||
fi
|
||||
if [ "$PASSWORT" != "$PASSWORT2" ]; then
|
||||
echo "Die beiden Passwoerter stimmen nicht ueberein, nichts geschrieben."
|
||||
exit 2
|
||||
fi
|
||||
read -r -p "Paper-Kontonummer (beginnt meist mit DU; leer lassen, wenn du sie nicht zur Hand hast): " KONTO
|
||||
printf '%s' "$BENUTZER" > "$ZIEL/paper_benutzer"
|
||||
printf '%s' "$PASSWORT" > "$ZIEL/paper_passwort"
|
||||
if [ -n "$KONTO" ]; then
|
||||
printf '%s' "$KONTO" > "$ZIEL/paper_konto"
|
||||
fi
|
||||
unset PASSWORT PASSWORT2
|
||||
chmod 600 "$ZIEL"/*
|
||||
echo "Gespeichert unter $ZIEL, nur fuer den Benutzer claude lesbar:"
|
||||
for datei in "$ZIEL"/*; do
|
||||
printf ' %s: %s Zeichen\n' "$(basename "$datei")" "$(wc -c < "$datei" | tr -d ' ')"
|
||||
done
|
||||
echo "Fertig. Aendern: dieses Skript noch einmal ausfuehren. Loeschen: rm -r $ZIEL"
|
||||
@@ -0,0 +1,236 @@
|
||||
"""Rueckweg des Paper-Runners von zehn Aktien auf AAPL, einmal, nach Pascals Vorgabe.
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Pascal am 2026-09-24: zehn Aktien "bis Mittwoch" -- gelesen als
|
||||
bis einschliesslich Mittwoch 2026-09-30; ab Donnerstag 2026-10-01 handelt der Runner wieder AAPL mit der
|
||||
Registerversion. Der Rueckweg ist der aus Paket M: config/paper-aapl-register.toml wird config/paper.toml.
|
||||
Geplante Aufgabe "Trading Plattform - Rueckweg AAPL" (rueckweg_aapl_einrichten.ps1): 2026-09-30 22:50,
|
||||
zweiter Versuch 2026-10-01 22:10, verpasste Termine beim naechsten Start (StartWhenAvailable).
|
||||
|
||||
Schritte, jeder mit seinem Grund:
|
||||
1. Nicht vor FRUEHESTENS (nach Boersenschluss des letzten Tages mit zehn Aktien).
|
||||
2. Ergebnisdatei ABLAGE/rueckweg.json da -> schon erledigt, nichts tun.
|
||||
3. Laeuft der Runner (systemctl is-active trading-paper.service in WSL), wird nichts getauscht: Er hat
|
||||
seine Konfiguration beim Start gelesen, und ein Tausch mitten am Tag wirkte erst beim naechsten Start.
|
||||
Befund, der zweite Termin versucht es erneut.
|
||||
Stand er, wird nach WARTEN_S noch einmal gefragt: Hat erst dieser Aufruf WSL geweckt, startet der
|
||||
Zeitgeber den Runner 60 s danach (OnStartupSec=60) und liest dabei die Konfiguration.
|
||||
4. Im Projektordner muss ZWEIG ausgecheckt sein, sonst Befund.
|
||||
5. config/paper.toml gleich der Rueckweg-Datei: sauberer Ordner -> schon getauscht (Ergebnisdatei,
|
||||
Ende); nur config/paper.toml geaendert -> halb erledigt (ein gescheiterter Commit), weiter mit 8;
|
||||
sonst Befund.
|
||||
6. Sonst muss der Ordner sauber sein (git status --porcelain leer), sonst Befund: Ein Commit ueber
|
||||
fremde Aenderungen waere schlimmer als ein Tag mehr mit zehn Aktien. Und config/paper.toml muss die
|
||||
Konfiguration mit mehreren Aktien sein (Zeile 'quelle = "folge"'), sonst Befund.
|
||||
7. Jeder Schluessel aus config/paper.toml ausser den erwarteten Unterschieden (ERWARTET_ANDERS) muss
|
||||
in der Rueckweg-Datei mit demselben Wert stehen, sonst Befund: Der Tausch verloere ihn still oder
|
||||
schaltete ihn still um (etwa [auto_fortsetzen] aktiv = false, von Pascal gesetzt).
|
||||
8. Bytes kopieren, git add, git commit (lokal; gepusht wird von der naechsten Sitzung -- ein Push aus
|
||||
einer Aufgabe ohne Fenster koennte an einer Anmeldeabfrage haengen).
|
||||
9. Eine Zeile in Inbox/<heute>.md unter "## Agentenprotokoll"; bei einem Befund zusaetzlich eine Meldung
|
||||
mit hoher Prioritaet ueber abendkontrolle_wsl.py --meldung.
|
||||
Aufruf von Hand: python rueckweg_aapl.py [--jetzt ISO] [--projekt PFAD] [--vault PFAD] [--ohne-wsl]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tomllib
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from abendkontrolle import KEIN_FENSTER # noqa: E402
|
||||
from abendkontrolle import WSL # noqa: E402
|
||||
from abendkontrolle import WSL_PY # noqa: E402
|
||||
from abendkontrolle import WSL_SKRIPT # noqa: E402
|
||||
from abendkontrolle import schreibe_notiz # noqa: E402
|
||||
|
||||
VAULT = Path(r"D:\Claude")
|
||||
PROJEKT = VAULT / "Code Projekte" / "active - Trading Plattform"
|
||||
GIT = r"C:\Program Files\Git\cmd\git.exe"
|
||||
FRUEHESTENS = datetime(2026, 9, 30, 22, 5)
|
||||
ZIEL = Path("config") / "paper.toml"
|
||||
RUECKWEG = Path("config") / "paper-aapl-register.toml"
|
||||
MEHRERE = 'quelle = "folge"'
|
||||
ZWEIG = "master"
|
||||
ERWARTET_ANDERS = ("agent.", "umgebung.instrumente", "umgebung.instrument", "grenzen.max_tagesverlust_anteil")
|
||||
WARTEN_S = 90
|
||||
NOCHMAL = "Steht noch ein Termin der Aufgabe aus, versucht er es erneut; sonst entscheidet die naechste Sitzung."
|
||||
NACHRICHT = (
|
||||
"Rueckweg auf AAPL: config/paper.toml aus config/paper-aapl-register.toml\n\n"
|
||||
"Pascal am 2026-09-24: zehn Aktien bis Mittwoch 2026-09-30. Ab 2026-10-01 handelt der Runner wieder\n"
|
||||
"AAPL mit der Registerversion (Rueckweg aus Paket M). Automatisch durch die geplante Aufgabe\n"
|
||||
"'Trading Plattform - Rueckweg AAPL' (.vault-tools/trading-plattform-pruefung/rueckweg_aapl.py).\n\n"
|
||||
"Co-Authored-By: Claude Opus 5.5 <[email protected]>\n"
|
||||
)
|
||||
|
||||
|
||||
def git(projekt: Path, *argumente: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[GIT, "-C", str(projekt), *argumente], capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace", timeout=120, creationflags=KEIN_FENSTER,
|
||||
env={**__import__("os").environ, "GIT_TERMINAL_PROMPT": "0", "GCM_INTERACTIVE": "never"},
|
||||
)
|
||||
|
||||
|
||||
def runner_aktiv(vorgabe: str | None) -> bool:
|
||||
if vorgabe is not None:
|
||||
zustand = vorgabe # Angriffsfaelle: die Ausgabe von systemctl is-active, ohne WSL
|
||||
else:
|
||||
lauf = subprocess.run(
|
||||
[WSL, "-d", "Debian", "-u", "claude", "--", "systemctl", "is-active", "trading-paper.service"],
|
||||
capture_output=True, timeout=60, creationflags=KEIN_FENSTER,
|
||||
)
|
||||
zustand = lauf.stdout.decode("utf-8", errors="replace").strip()
|
||||
if zustand in ("active", "activating", "deactivating", "reloading"):
|
||||
return True
|
||||
if zustand in ("inactive", "failed"):
|
||||
return False
|
||||
raise RuntimeError(f"Zustand des Runners nicht lesbar: {zustand!r}")
|
||||
|
||||
|
||||
def melde(titel: str, text: str, ohne_wsl: bool) -> str:
|
||||
if ohne_wsl:
|
||||
return "ohne WSL, nicht gesendet"
|
||||
lauf = subprocess.run(
|
||||
[WSL, "-d", "Debian", "-u", "claude", "--", WSL_PY, WSL_SKRIPT, "--meldung", titel, text],
|
||||
capture_output=True, timeout=120, creationflags=KEIN_FENSTER,
|
||||
)
|
||||
return lauf.stdout.decode("utf-8", errors="replace").strip()[-200:]
|
||||
|
||||
|
||||
def blaetter(datei: Path) -> dict:
|
||||
"""Alle Blattschluessel einer TOML-Datei als Punktpfad mit ihrem Wert."""
|
||||
def ablaufen(tabelle: dict, pfad: str):
|
||||
for name, wert in tabelle.items():
|
||||
voll = f"{pfad}.{name}" if pfad else name
|
||||
if isinstance(wert, dict):
|
||||
yield from ablaufen(wert, voll)
|
||||
else:
|
||||
yield voll, wert
|
||||
with datei.open("rb") as f:
|
||||
return dict(ablaufen(tomllib.load(f), ""))
|
||||
|
||||
|
||||
def fehlende_schluessel(ziel: Path, quelle: Path) -> list[str]:
|
||||
"""Was der Tausch ausser den erwarteten Unterschieden aendern wuerde.
|
||||
|
||||
Projektvorgabe (Paket M, Rueckweg): Die beiden Dateien unterscheiden sich nur in [agent],
|
||||
umgebung.instrumente (zehn Aktien) gegen umgebung.instrument (AAPL) und im Wert von
|
||||
grenzen.max_tagesverlust_anteil (1 % gegen 0,2 %); verglichen am 2026-09-24 im Arbeitsbaum M.
|
||||
Jeder andere Schluessel, der nur in config/paper.toml steht, ginge beim Tausch still verloren
|
||||
(etwa [auto_fortsetzen] aus Paket AF oder etwas aus Z2), und jeder andere Wert, der sich
|
||||
unterscheidet, sprang still um -- etwa auto_fortsetzen.aktiv, das Pascal in config/paper.toml
|
||||
abgeschaltet hat (Befund der Plankritik AF, 2026-09-24).
|
||||
"""
|
||||
alt, neu = blaetter(ziel), blaetter(quelle)
|
||||
befunde = []
|
||||
for name in sorted(alt.keys() | neu.keys()):
|
||||
if name.startswith(ERWARTET_ANDERS):
|
||||
continue
|
||||
if name not in neu:
|
||||
befunde.append(f"{name} fehlt in der Rueckweg-Datei")
|
||||
elif name not in alt:
|
||||
# Die Gegenrichtung (Nachkritik AF, 2026-09-24): Hat Pascal [auto_fortsetzen] in
|
||||
# config/paper.toml geloescht -- laut Task 421 ein gueltiges Abschalten --, brachte der
|
||||
# Tausch aktiv = true aus der Rueckweg-Datei still zurueck.
|
||||
befunde.append(f"{name} steht nur in der Rueckweg-Datei")
|
||||
elif neu[name] != alt[name]:
|
||||
befunde.append(f"{name} anderer Wert")
|
||||
return befunde
|
||||
|
||||
|
||||
def rueckweg(jetzt: datetime, projekt: Path, ablage: Path, runner_vorgabe: str | None) -> tuple[str, str | None]:
|
||||
"""(Ergebnis fuer die Notiz, Befund oder None)."""
|
||||
if jetzt < FRUEHESTENS:
|
||||
return f"zu frueh ({jetzt:%Y-%m-%d %H:%M}), nichts getan", None
|
||||
if (ablage / "rueckweg.json").exists():
|
||||
return "schon erledigt", None
|
||||
if runner_aktiv(runner_vorgabe):
|
||||
return "", f"Der Runner laeuft; config/paper.toml nicht getauscht. {NOCHMAL}"
|
||||
if runner_vorgabe is None:
|
||||
# Hat erst dieser Aufruf WSL geweckt, startet der Zeitgeber den Runner 60 s danach
|
||||
# (OnStartupSec=60) mit der Konfiguration, die er dann liest. Also warten und noch einmal fragen.
|
||||
time.sleep(WARTEN_S)
|
||||
if runner_aktiv(None):
|
||||
return "", f"Der Runner ist eben gestartet; config/paper.toml nicht getauscht. {NOCHMAL}"
|
||||
zweig = git(projekt, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
|
||||
if zweig != ZWEIG:
|
||||
return "", f"Im Projektordner ist {zweig!r} ausgecheckt, nicht {ZWEIG}; nicht getauscht"
|
||||
ziel, quelle = projekt / ZIEL, projekt / RUECKWEG
|
||||
if not quelle.is_file():
|
||||
return "", f"{RUECKWEG.as_posix()} fehlt; nicht getauscht"
|
||||
status = git(projekt, "status", "--porcelain")
|
||||
if status.returncode != 0:
|
||||
return "", f"git status scheiterte ({status.returncode}); nicht getauscht"
|
||||
geaendert = [z for z in status.stdout.splitlines() if z.strip()]
|
||||
if ziel.is_file() and ziel.read_bytes() == quelle.read_bytes():
|
||||
if not geaendert:
|
||||
return "config/paper.toml ist schon die AAPL-Konfiguration", None
|
||||
if geaendert != [f" M {ZIEL.as_posix()}"] and geaendert != [f"M {ZIEL.as_posix()}"]:
|
||||
return "", f"Projektordner nicht sauber (git status: {' | '.join(geaendert)[:200]}); nicht getauscht"
|
||||
# Halb erledigt: getauscht, aber nicht committet (etwa ein gescheiterter Commit beim ersten Termin).
|
||||
else:
|
||||
if geaendert:
|
||||
return "", f"Projektordner nicht sauber (git status: {' | '.join(geaendert)[:200]}); nicht getauscht"
|
||||
if MEHRERE not in ziel.read_text(encoding="utf-8"):
|
||||
return "", "config/paper.toml ist nicht die Konfiguration mit mehreren Aktien; nicht getauscht, Pascal entscheidet"
|
||||
fehlend = fehlende_schluessel(ziel, quelle)
|
||||
if fehlend:
|
||||
return "", (
|
||||
f"config/paper-aapl-register.toml weicht ausserhalb der erwarteten Stellen von config/paper.toml ab "
|
||||
f"({', '.join(fehlend)[:200]}); nicht getauscht, Pascal entscheidet"
|
||||
)
|
||||
ziel.write_bytes(quelle.read_bytes())
|
||||
for schritt in (("add", ZIEL.as_posix()), ("commit", "-m", NACHRICHT)):
|
||||
lauf = git(projekt, *schritt)
|
||||
if lauf.returncode != 0:
|
||||
return "", (
|
||||
f"git {schritt[0]} scheiterte ({lauf.returncode}: {(lauf.stderr or lauf.stdout).strip()[:200]}); "
|
||||
f"Datei ist getauscht, aber nicht committet. {NOCHMAL}"
|
||||
)
|
||||
kopf = git(projekt, "rev-parse", "--short", "HEAD").stdout.strip()
|
||||
return f"config/paper.toml auf AAPL (Register) zurueckgestellt, Commit {kopf} (nicht gepusht)", None
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
zerleger = argparse.ArgumentParser(allow_abbrev=False)
|
||||
zerleger.add_argument("--jetzt")
|
||||
zerleger.add_argument("--projekt", default=str(PROJEKT))
|
||||
zerleger.add_argument("--vault", default=str(VAULT))
|
||||
zerleger.add_argument("--ohne-wsl", action="store_true", help="Angriffsfaelle: keine Meldung, Runner laut --runner-zustand")
|
||||
zerleger.add_argument("--runner-zustand", help="Angriffsfaelle: Ausgabe von systemctl is-active statt WSL")
|
||||
argumente = zerleger.parse_args(argv)
|
||||
if argumente.ohne_wsl and argumente.runner_zustand is None:
|
||||
argumente.runner_zustand = "inactive"
|
||||
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
|
||||
projekt, vault = Path(argumente.projekt), Path(argumente.vault)
|
||||
ablage = projekt / "data" / "paper" / "abendkontrollen"
|
||||
ablage.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
ergebnis, befund = rueckweg(jetzt, projekt, ablage, argumente.runner_zustand)
|
||||
except Exception as ausnahme:
|
||||
ergebnis, befund = "", f"abgebrochen: {type(ausnahme).__name__}: {str(ausnahme)[:200]}"
|
||||
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as log:
|
||||
log.write(traceback.format_exc())
|
||||
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as log:
|
||||
log.write(f"{datetime.now().isoformat(timespec='seconds')} rueckweg: {ergebnis or befund}\n")
|
||||
if ergebnis in ("schon erledigt",) or ergebnis.startswith("zu frueh"):
|
||||
return 0
|
||||
kopf = f"- {jetzt:%H:%M} Uhr — Rückweg auf AAPL (automatisch, Pascals Vorgabe: zehn Aktien bis 30.09.)"
|
||||
if befund is None:
|
||||
(ablage / "rueckweg.json").write_text(
|
||||
json.dumps({"zeit": jetzt.isoformat(), "ergebnis": ergebnis}, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
schreibe_notiz(vault, jetzt.date().isoformat(), f"{kopf}: {ergebnis}.")
|
||||
return 0
|
||||
gemeldet = melde("Rueckweg AAPL nicht ausgefuehrt", befund, argumente.ohne_wsl)
|
||||
schreibe_notiz(vault, jetzt.date().isoformat(), f"{kopf}: **nicht ausgeführt** — {befund} #needs-review (Meldung: {gemeldet})")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Angriffsfaelle fuer rueckweg_aapl.py auf einem Wegwerf-Git-Repo. Koordinator-Werkzeug, nicht fuers Repo.
|
||||
|
||||
Jeder Fall einzeln, zuerst die Grundlinie; ohne WSL (--ohne-wsl, Runnerzustand vorgegeben), ohne Meldung.
|
||||
Aufruf unter Windows: python rueckweg_aapl_angriffe.py Exit 0, wenn jeder Fall wie erwartet ausgeht.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HIER))
|
||||
import rueckweg_aapl as rw # noqa: E402
|
||||
|
||||
ZEHN = '[agent]\nquelle = "folge"\ninstrumente = ["AAPL.XNAS", "MSFT.XNAS"]\n'
|
||||
AAPL = '[agent]\nquelle = "register"\ninstrument = "AAPL.XNAS"\n'
|
||||
abweichungen: list[str] = []
|
||||
|
||||
|
||||
def repo(tmp: Path, paper: str | None = ZEHN, aapl: str | None = AAPL) -> Path:
|
||||
projekt = tmp / "projekt"
|
||||
(projekt / "config").mkdir(parents=True)
|
||||
if paper is not None:
|
||||
(projekt / "config" / "paper.toml").write_bytes(paper.encode("utf-8"))
|
||||
if aapl is not None:
|
||||
(projekt / "config" / "paper-aapl-register.toml").write_bytes(aapl.encode("utf-8"))
|
||||
(projekt / ".gitignore").write_text("data/\n", encoding="utf-8")
|
||||
for befehl in (["init", "-q", "-b", "master"], ["config", "user.email", "[email protected]"], ["config", "user.name", "Test"],
|
||||
["config", "core.autocrlf", "false"], ["add", "-A"], ["commit", "-q", "-m", "start"]):
|
||||
subprocess.run([rw.GIT, "-C", str(projekt), *befehl], check=True, capture_output=True)
|
||||
(tmp / "Inbox").mkdir()
|
||||
return projekt
|
||||
|
||||
|
||||
def fall(name, *, jetzt="2026-09-30T22:50:00", vorbereiten=None, runner="inactive", paper=ZEHN, aapl=AAPL,
|
||||
erwartet_code, getauscht, notiz, marke, commits, grund=None):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
projekt = repo(tmp, paper, aapl)
|
||||
if vorbereiten:
|
||||
vorbereiten(projekt)
|
||||
vorher = subprocess.run([rw.GIT, "-C", str(projekt), "rev-list", "--count", "HEAD"], capture_output=True, text=True).stdout.strip()
|
||||
code = rw.main(["--jetzt", jetzt, "--projekt", str(projekt), "--vault", str(tmp), "--ohne-wsl", "--runner-zustand", runner])
|
||||
paper_jetzt = (projekt / "config" / "paper.toml").read_text(encoding="utf-8") if (projekt / "config" / "paper.toml").exists() else None
|
||||
notizen = list((tmp / "Inbox").glob("*.md"))
|
||||
text = notizen[0].read_text(encoding="utf-8") if notizen else ""
|
||||
hat_marke = (projekt / "data" / "paper" / "abendkontrollen" / "rueckweg.json").exists()
|
||||
nachher = subprocess.run([rw.GIT, "-C", str(projekt), "rev-list", "--count", "HEAD"], capture_output=True, text=True).stdout.strip()
|
||||
ist = {
|
||||
"code": code,
|
||||
"getauscht": paper_jetzt == AAPL,
|
||||
"notiz": ("nicht ausgeführt" if "nicht ausgeführt" in text else ("ja" if "Rückweg auf AAPL" in text else "nein")),
|
||||
"marke": hat_marke,
|
||||
"commits": int(nachher) - int(vorher),
|
||||
}
|
||||
soll = {"code": erwartet_code, "getauscht": getauscht, "notiz": notiz, "marke": marke, "commits": commits}
|
||||
ok = ist == soll and (grund is None or grund in text)
|
||||
zeile = f"{'OK ' if ok else 'FEHL'} {name}: {ist}" + ("" if ok else f" SOLL {soll}")
|
||||
print(zeile)
|
||||
if not ok:
|
||||
abweichungen.append(zeile)
|
||||
|
||||
|
||||
fall("Grundlinie", erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=1)
|
||||
fall("zu frueh", jetzt="2026-09-30T21:59:00", erwartet_code=0, getauscht=False, notiz="nein", marke=False, commits=0)
|
||||
fall("Ergebnisdatei schon da",
|
||||
vorbereiten=lambda p: ((p / "data" / "paper" / "abendkontrollen").mkdir(parents=True), (p / "data" / "paper" / "abendkontrollen" / "rueckweg.json").write_text("{}")),
|
||||
erwartet_code=0, getauscht=False, notiz="nein", marke=True, commits=0)
|
||||
fall("Runner aktiv", runner="active", erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("Runner deactivating", runner="deactivating", erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("Runnerzustand unlesbar", runner="", erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("Runner failed (steht)", runner="failed", erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=1)
|
||||
fall("schon AAPL", paper=AAPL, erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=0)
|
||||
fall("Ordner nicht sauber (fremde Aenderung)",
|
||||
vorbereiten=lambda p: (p / "config" / "risk.toml").write_text("x\n"),
|
||||
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("paper.toml geaendert, nicht committet",
|
||||
vorbereiten=lambda p: (p / "config" / "paper.toml").write_text(ZEHN + "# neu\n"),
|
||||
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("paper.toml ohne folge", paper='[agent]\nquelle = "halte"\n',
|
||||
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("Rueckweg-Datei fehlt", aapl=None, erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("zweiter Termin Donnerstag", jetzt="2026-10-01T22:10:00", erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=1)
|
||||
# Befunde des Pruefers: halb erledigter Tausch, verlorener Abschnitt, falscher Zweig
|
||||
fall("halb getauscht (Datei getauscht, nicht committet)",
|
||||
vorbereiten=lambda p: (p / "config" / "paper.toml").write_bytes(AAPL.encode("utf-8")),
|
||||
erwartet_code=0, getauscht=True, notiz="ja", marke=True, commits=1)
|
||||
AUTO = "\n[auto_fortsetzen]\naktiv = true\n"
|
||||
fall("halb getauscht und fremde Aenderung",
|
||||
vorbereiten=lambda p: ((p / "config" / "paper.toml").write_bytes(AAPL.encode("utf-8")), (p / "config" / "risk.toml").write_text("x\n")),
|
||||
erwartet_code=1, getauscht=True, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("Abschnitt nur in paper.toml", paper=ZEHN + AUTO,
|
||||
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("Abschnitt in beiden", paper=ZEHN + AUTO, aapl=AAPL + AUTO,
|
||||
erwartet_code=0, getauscht=False, notiz="ja", marke=True, commits=1)
|
||||
AUS = "\n[auto_fortsetzen]\naktiv = false\n"
|
||||
fall("Automatik nur in paper.toml abgeschaltet", paper=ZEHN + AUS, aapl=AAPL + AUTO,
|
||||
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("Abschnitt in paper.toml geloescht, in der Rueckweg-Datei noch da", paper=ZEHN, aapl=AAPL + AUTO,
|
||||
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0,
|
||||
grund="auto_fortsetzen.aktiv steht nur in der Rueckweg-Datei")
|
||||
fall("anderer Wert in [grenzen]", paper=ZEHN + "\n[grenzen]\nmax_orders = 5\n", aapl=AAPL + "\n[grenzen]\nmax_orders = 10\n",
|
||||
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
fall("Tagesverlust darf sich unterscheiden",
|
||||
paper=ZEHN + "\n[grenzen]\nmax_tagesverlust_anteil = 0.01\n", aapl=AAPL + "\n[grenzen]\nmax_tagesverlust_anteil = 0.002\n",
|
||||
erwartet_code=0, getauscht=False, notiz="ja", marke=True, commits=1)
|
||||
fall("anderer Zweig ausgecheckt",
|
||||
vorbereiten=lambda p: subprocess.run([rw.GIT, "-C", str(p), "checkout", "-q", "-b", "z2"], check=True),
|
||||
erwartet_code=1, getauscht=False, notiz="nicht ausgeführt", marke=False, commits=0)
|
||||
|
||||
print(f"ERGEBNIS: {len(abweichungen)} Abweichung(en)")
|
||||
sys.exit(1 if abweichungen else 0)
|
||||
@@ -0,0 +1,37 @@
|
||||
# Richtet die einmalige Windows-Aufgabe "Trading Plattform - Rueckweg AAPL" ein oder entfernt sie.
|
||||
# Koordinator-Werkzeug, nicht fuers Repo. Pascal 2026-09-24: zehn Aktien bis Mittwoch 2026-09-30.
|
||||
# Zwei Termine: Mi 2026-09-30 22:50 und als zweiter Versuch Do 2026-10-01 22:10; StartWhenAvailable holt
|
||||
# einen verpassten Termin beim naechsten Start nach. rueckweg_aapl.py tut nach dem ersten Erfolg nichts mehr.
|
||||
#
|
||||
# Aufruf: powershell -NoProfile -File rueckweg_aapl_einrichten.ps1 [-Aktion einrichten|entfernen]
|
||||
param(
|
||||
[ValidateSet("einrichten", "entfernen")]
|
||||
[string]$Aktion = "einrichten"
|
||||
)
|
||||
|
||||
$Name = "Trading Plattform - Rueckweg AAPL"
|
||||
if ($Aktion -eq "entfernen") {
|
||||
Unregister-ScheduledTask -TaskName $Name -Confirm:$false
|
||||
return
|
||||
}
|
||||
|
||||
$Py = "C:\Users\Pascal Michel\AppData\Local\Programs\Python\Python313\pythonw.exe"
|
||||
$Skript = "D:\Claude\.vault-tools\trading-plattform-pruefung\rueckweg_aapl.py"
|
||||
if (-not (Test-Path $Py)) { throw "Interpreter fehlt: $Py" }
|
||||
if (-not (Test-Path $Skript)) { throw "Skript fehlt: $Skript" }
|
||||
|
||||
$Benutzer = "$env:USERDOMAIN\$env:USERNAME"
|
||||
$Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '"') -WorkingDirectory "D:\Claude"
|
||||
$Erster = New-ScheduledTaskTrigger -Once -At ([datetime]"2026-09-30T22:50:00")
|
||||
$Zweiter = New-ScheduledTaskTrigger -Once -At ([datetime]"2026-10-01T22:10:00")
|
||||
$Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited
|
||||
$Regeln = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 15) -MultipleInstances IgnoreNew
|
||||
Register-ScheduledTask -TaskName $Name -Action $Aufruf -Trigger @($Erster, $Zweiter) -Principal $Wer `
|
||||
-Settings $Regeln -Force `
|
||||
-Description "Rueckweg des Paper-Runners auf AAPL nach Pascals Vorgabe (zehn Aktien bis 2026-09-30). Siehe rueckweg_aapl.py." | Out-Null
|
||||
|
||||
$Aufgabe = Get-ScheduledTask -TaskName $Name
|
||||
"{0}: {1}" -f $Aufgabe.TaskName, $Aufgabe.State
|
||||
$Aufgabe.Triggers | ForEach-Object { "Ausloeser: {0} {1}" -f $_.CimClass.CimClassName, $_.StartBoundary }
|
||||
"Naechster Lauf: {0}" -f (Get-ScheduledTaskInfo -TaskName $Name).NextRunTime
|
||||
Reference in new issue
Block a user