Trading-Plattform-Pruefung: Abendkontrolle prueft die Sicherung des vorigen Handelstags (Paket E9, Task 726)
abendkontrolle_wsl.py nimmt --sicherung (Stand der Ablage vom Host) und meldet per ntfy, wenn die Sicherung des vorigen Handelstags fehlt, kein Archiv hat, nicht bestaetigt ist oder die Bestaetigung nicht passt; vor erster-tag still, ohne erster-tag ein Befund. abendkontrolle_angriffe.py mit --nur-sicherung (60 Faelle), abendkontrolle_mutanten.py mit --nur und 30 Sicherungs-Mutanten. Gegengeprueft: byte-gleich mit der Plankopie, Lauf gegen echte Archive ohne Abweichung. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
This commit is contained in:
1 parent
c5db92c6de
commit
3e55bb915d
3 files changed
+514
-3
No files matched your search
@@ -81,8 +81,35 @@ Seit Paket IB2 prueft --teil ibkr statt des Alpaca-Runners die IBKR-Instanz in i
|
||||
Nicht gesehen im Teil ibkr: ein Tagesbericht (die Instanz hat keine Tagesauswertung),
|
||||
handelstag_auswertung.py und der Dienst ibkr-gateway.service.
|
||||
|
||||
Seit Paket E9 prueft --sicherung <datei> fuer beide Teile die Sicherung des vorigen Handelstags auf
|
||||
dem Docker-Host. Die Datei schreibt abendkontrolle-host.sh vor jedem Lauf (absoluter Pfad, im
|
||||
Container /abend/sicherung.json); die Ablage selbst sieht das Werkzeug nicht. Die Pruefung laeuft
|
||||
nach dem Teil in einem eigenen Schritt, auch an einem Tag ohne Handel und nach einem Abbruch.
|
||||
E1. Ist der Stand lesbar? Projektvorgabe (Plan E9, Task 726): Datei fehlt, kein JSON, kein
|
||||
Objekt, ohne erster_tag oder eintraege -> Befund. Ein Schluessel
|
||||
fehler (Ablage fehlt, nicht lesbar, erster-tag kaputt) -> Befund.
|
||||
E2. Ab wann wird geprueft? Projektvorgabe (S6, S8): erster_tag aus erster-tag der Ablage;
|
||||
null, kein Datum oder ein Tag nach --tag -> Befund, sonst
|
||||
schwiege die Pruefung auf Dauer; nur ein voriger Handelstag vor
|
||||
erster_tag -> nicht geprueft, still.
|
||||
E3. Welcher Tag? Quelle: derselbe Kalender wie Frage 1, GET /v2/calendar fuer die
|
||||
SICHERUNG_FENSTER_TAGE Kalendertage vor --tag, bis zum Vortag; der
|
||||
juengste Eintrag darin, unabhaengig von der Reihenfolge der Liste.
|
||||
Die Spanne setzt das Werkzeug selbst durch, fuer die Antwort wie
|
||||
fuer eine Kalenderdatei. Form wie Frage 1, dazu date als
|
||||
JJJJ-MM-TT, sonst Befund; keiner -> Befund.
|
||||
E4. Liegt die Sicherung? Projektvorgabe (Schnittstelle E9): eintraege["<tag>-<instanz>"],
|
||||
instanz nach --teil; fehlt der Eintrag -> Befund; summe nicht 64
|
||||
Hexzeichen in Kleinbuchstaben -> Befund; archiv nicht true (die
|
||||
Datei <name>.tar fehlt auf dem Host) -> Befund.
|
||||
E5. Ist sie nach D: geholt? Projektvorgabe (S4, S5): bestaetigt hat die Form von summe und ist
|
||||
ihr gleich, sonst Befund. Die Windows-Seite bestaetigt erst nach
|
||||
geprueftem Archiv, und abendkontrolle-lesen.sh nimmt nur die Summe
|
||||
des Hosts an.
|
||||
Nicht gesehen: ob die Kopie auf D: noch liegt und ob sie sich zuruecksichern laesst. Ein Werktag ohne
|
||||
Handel hat eine Sicherung, die niemand prueft.
|
||||
Ein Fangnetz, kein Nachweis ueber den Handelstag.
|
||||
Aufruf (WSL, Venv): python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung] [--teil ibkr]
|
||||
Aufruf (WSL, Venv): python abendkontrolle_wsl.py --tag JJJJ-MM-TT [--ohne-meldung] [--teil ibkr] [--sicherung <datei>]
|
||||
Nur eine Meldung schicken: python abendkontrolle_wsl.py --meldung "<titel>" "<text>"
|
||||
Fuer Angriffsfaelle ohne Netz und ohne systemd: --projekt, --kalender, --journal, --dienste,
|
||||
--ohne-broker, --ohne-kerzen; im Teil ibkr dazu --ibkr-ordner, --ibkr-konto (JSON mit exit und
|
||||
@@ -585,6 +612,142 @@ def kontrolliere_ibkr(argumente: argparse.Namespace, k: Kontrolle) -> None:
|
||||
pruefe_konto_ibkr(ergebnis[0], ergebnis[1], k)
|
||||
|
||||
|
||||
# --- Sicherung des vorigen Handelstags (seit Paket E9) ---------------------------------------------
|
||||
|
||||
# Wie weit der Kalender zurueck gefragt wird. Projektvorgabe (Plan E9, Task 726), nicht am Kalender
|
||||
# gemessen: Zehn Kalendertage decken ein langes Wochenende mit Feiertag und eine kurze
|
||||
# ausserplanmaessige Schliessung.
|
||||
SICHERUNG_FENSTER_TAGE = 10
|
||||
# Summe und Bestaetigung auf dem Host (Schnittstelle Paket E9) und ein Datum, nur ASCII-Ziffern.
|
||||
SUMME = re.compile(r"[0-9a-f]{64}")
|
||||
DATUM = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
|
||||
|
||||
|
||||
def in_spanne(liste, start: str, ende: str):
|
||||
"""Aus einer Liste des Kalenders nur die Eintraege, deren date von start bis ende reicht.
|
||||
|
||||
Es faellt nur ein Eintrag mit einem date JJJJ-MM-TT ausserhalb der Spanne weg; jeder andere
|
||||
bleibt, und eine Antwort, die keine Liste ist, kommt unveraendert zurueck. Die Form prueft
|
||||
juengster_handelstag.
|
||||
"""
|
||||
if not isinstance(liste, list):
|
||||
return liste
|
||||
return [
|
||||
eintrag
|
||||
for eintrag in liste
|
||||
if not isinstance(eintrag, dict)
|
||||
or not isinstance(eintrag.get("date"), str)
|
||||
or not DATUM.fullmatch(eintrag["date"])
|
||||
or start <= eintrag["date"] <= ende
|
||||
]
|
||||
|
||||
|
||||
def lies_kalender_spanne(start: str, ende: str, kalender_datei: str | None, projekt: Path):
|
||||
"""Die Liste aus /v2/calendar oder aus einer Datei (Angriffsfaelle), je nur von start bis ende.
|
||||
|
||||
Die Spanne setzt das Werkzeug fuer beide Quellen selbst durch (Plan E9, Plankritik Runde 1): So
|
||||
pinnen die Angriffsfaelle mit Kalenderdatei die Grenzen, mit denen es Alpaca fragt.
|
||||
"""
|
||||
if kalender_datei is not None:
|
||||
return in_spanne(json.loads(Path(kalender_datei).read_text(encoding="utf-8")), start, ende)
|
||||
import requests # erst hier: Die Angriffsfaelle laufen ohne Netz
|
||||
|
||||
sys.path.insert(0, str(projekt))
|
||||
sys.path.insert(0, str(AUSWERTUNG.parent))
|
||||
from handelstag_auswertung import kopfzeilen # liest die Schluessel, gibt sie nie aus
|
||||
|
||||
kopf = kopfzeilen()
|
||||
if kopf is None:
|
||||
raise RuntimeError("Schluessel fuer Alpaca nicht lesbar")
|
||||
antwort = requests.get(
|
||||
f"{HANDEL_URL}/v2/calendar", headers=kopf, params={"start": start, "end": ende}, timeout=30
|
||||
)
|
||||
antwort.raise_for_status()
|
||||
return in_spanne(antwort.json(), start, ende)
|
||||
|
||||
|
||||
def juengster_handelstag(liste) -> str | None:
|
||||
"""Der juengste Handelstag in der Liste, None, wenn keiner darin steht.
|
||||
|
||||
Die Liste reicht schon nur bis zum Tag vor dem geprueften (lies_kalender_spanne). Die Grenze steht
|
||||
allein dort, damit ein Angriffsfall sie pinnen kann. Die Reihenfolge der Liste zaehlt nicht. Hat die
|
||||
Liste nicht die gemessene Form (Frage 1) oder ein date nicht die Form JJJJ-MM-TT, ist das ein
|
||||
ValueError.
|
||||
"""
|
||||
if not isinstance(liste, list):
|
||||
raise ValueError("Kalender ist keine Liste")
|
||||
tage = []
|
||||
for eintrag in liste:
|
||||
datum = eintrag.get("date") if isinstance(eintrag, dict) else None
|
||||
if not isinstance(datum, str) or not DATUM.fullmatch(datum):
|
||||
raise ValueError("Kalender mit einem Eintrag ohne date JJJJ-MM-TT")
|
||||
tage.append(datum)
|
||||
return max(tage) if tage else None
|
||||
|
||||
|
||||
def pruefe_sicherung(argumente: argparse.Namespace, k: Kontrolle) -> None:
|
||||
"""Die Sicherung des vorigen Handelstags fuer den geprueften Teil (Paket E9, S6).
|
||||
|
||||
Liest nur die Datei aus --sicherung, nie die Ablage. Jede Abweichung ist ein Befund, auch ein
|
||||
fehlender erster Sicherungstag, einer nach dem geprueften Tag und ein fehlendes Archiv. Still
|
||||
bleibt nur ein voriger Handelstag vor dem ersten Sicherungstag.
|
||||
"""
|
||||
geprueft = date.fromisoformat(argumente.tag) # ein falsches Datum ist ein Bedienfehler
|
||||
tag = geprueft.isoformat()
|
||||
instanz = "ibkr" if argumente.teil == "ibkr" else "alpaca"
|
||||
stand = json.loads(Path(argumente.sicherung).read_text(encoding="utf-8"))
|
||||
if not isinstance(stand, dict):
|
||||
k.befund("Sicherung: Stand ist kein Objekt")
|
||||
return
|
||||
if "fehler" in stand:
|
||||
k.befund(f"Sicherung nicht geprueft: {str(stand['fehler'])[:160]}")
|
||||
return
|
||||
if "erster_tag" not in stand or not isinstance(stand.get("eintraege"), dict):
|
||||
k.befund("Sicherung: Stand in unerwarteter Form (erster_tag oder eintraege fehlt)")
|
||||
return
|
||||
erster = stand["erster_tag"]
|
||||
if erster is None:
|
||||
# Seit der Plankritik Runde 1 ein Befund: Ohne erster-tag schwiege die Pruefung auf Dauer.
|
||||
k.befund("Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung")
|
||||
return
|
||||
if not isinstance(erster, str) or not DATUM.fullmatch(erster):
|
||||
k.befund("Sicherung: erster_tag ist kein Datum JJJJ-MM-TT")
|
||||
return
|
||||
# Ein erster Sicherungstag nach dem geprueften Tag (etwa ein Tippfehler im Jahr) schaltete die
|
||||
# Pruefung sonst still ab, bis der Tag kommt. DATUM hat die Form gesichert, also vergleicht der
|
||||
# Text wie das Datum.
|
||||
if erster > tag:
|
||||
k.befund(f"Sicherung nicht geprueft: erster_tag {erster} liegt nach dem geprueften Tag {tag}")
|
||||
return
|
||||
start = (geprueft - timedelta(days=SICHERUNG_FENSTER_TAGE)).isoformat()
|
||||
ende = (geprueft - timedelta(days=1)).isoformat()
|
||||
vorher = juengster_handelstag(lies_kalender_spanne(start, ende, argumente.kalender, Path(argumente.projekt)))
|
||||
if vorher is None:
|
||||
k.befund(f"Sicherung: kein Handelstag in den {SICHERUNG_FENSTER_TAGE} Tagen vor {tag} im Kalender")
|
||||
return
|
||||
name = f"{vorher}-{instanz}"
|
||||
k.fakten["sicherung"] = {"name": name, "erster_tag": erster, "geprueft": False}
|
||||
# Still bleibt nur das: ein voriger Handelstag vor dem ersten Sicherungstag (die ersten Tage nach
|
||||
# dem Einspielen).
|
||||
if vorher < erster:
|
||||
return
|
||||
k.fakten["sicherung"]["geprueft"] = True
|
||||
eintrag = stand["eintraege"].get(name)
|
||||
if not isinstance(eintrag, dict):
|
||||
k.befund(f"Sicherung {name} fehlt auf dem Docker-Host")
|
||||
return
|
||||
summe = eintrag.get("summe")
|
||||
bestaetigt = eintrag.get("bestaetigt")
|
||||
if not isinstance(summe, str) or not SUMME.fullmatch(summe):
|
||||
k.befund(f"Sicherung {name}: keine gueltige Summe auf dem Docker-Host")
|
||||
elif eintrag.get("archiv") is not True:
|
||||
k.befund(f"Sicherung {name}: Archiv fehlt auf dem Docker-Host")
|
||||
elif not isinstance(bestaetigt, str) or not SUMME.fullmatch(bestaetigt):
|
||||
k.befund(f"Sicherung {name}: nicht bestaetigt, also nicht nach D: geholt")
|
||||
elif bestaetigt != summe:
|
||||
k.befund(f"Sicherung {name}: die Bestaetigung passt nicht zur Summe auf dem Docker-Host")
|
||||
|
||||
|
||||
# --- Meldung ---------------------------------------------------------------------------------------
|
||||
|
||||
def melde(titel: str, text: str, projekt: Path) -> str:
|
||||
@@ -678,6 +841,8 @@ def parse_args(argv=None) -> argparse.Namespace:
|
||||
# Nur fuer die Angriffsfaelle: Exit und Ausgabe von ibkr_pruefen.py als JSON-Datei, und die Uhr.
|
||||
zerleger.add_argument("--ibkr-konto")
|
||||
zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt mit Versatz statt der Uhr")
|
||||
# Seit Paket E9 (S6): der Stand der Sicherungsablage des Hosts als JSON-Datei (abendkontrolle-host.sh).
|
||||
zerleger.add_argument("--sicherung")
|
||||
argumente = zerleger.parse_args(argv)
|
||||
if bool(argumente.tag) == bool(argumente.meldung):
|
||||
zerleger.error("genau eines von --tag und --meldung")
|
||||
@@ -702,6 +867,9 @@ def main(argv=None) -> int:
|
||||
except Exception as ausnahme:
|
||||
k.befund(f"Abendkontrolle abgebrochen: {type(ausnahme).__name__}: {ausnahme}")
|
||||
code = 2
|
||||
# Seit Paket E9 (S6): die Sicherung des vorigen Handelstags, fuer sich und vor der Meldung.
|
||||
if argumente.sicherung:
|
||||
k.schritt("Sicherung", pruefe_sicherung, argumente, k)
|
||||
if k.befunde and not argumente.ohne_meldung:
|
||||
text = "\n".join(f"- {befund}" for befund in k.befunde[:12])
|
||||
titel = f"Abendkontrolle {argumente.tag}: {len(k.befunde)} Befund(e)"
|
||||
|
||||
Reference in new issue
Block a user