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
@@ -5,6 +5,7 @@ Grundlinie; ohne Netz (--ohne-broker/--ohne-kerzen, Kalender und Journal aus Dat
|
||||
Auswertung fuer die Brokerzeile) und ohne Meldung (--ohne-meldung). Faelle an zwei Listenpositionen,
|
||||
wo die Reihenfolge zaehlen kann (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 3).
|
||||
Aufruf in WSL (Venv) und unter Windows: python abendkontrolle_angriffe.py
|
||||
Nur die Faelle zur Sicherung (Paket E9, Task 726), in WSL: python abendkontrolle_angriffe.py --nur-sicherung
|
||||
Exit 0, wenn jeder Fall so ausgeht wie erwartet; sonst 1 mit der Liste der Abweichungen.
|
||||
"""
|
||||
|
||||
@@ -811,12 +812,269 @@ def ibkr_windows_faelle() -> None:
|
||||
f"ibkr {ibkr} alpaca {alpaca_tage}")
|
||||
|
||||
|
||||
# --- Sicherung des vorigen Handelstags (Paket E9, Task 726), WSL-Teil ------------------------------
|
||||
|
||||
# TAG ist ein Mittwoch, S_VORHER der Dienstag davor, S_DAVOR der Montag. Die Summen sind erfunden.
|
||||
S_VORHER = "2026-09-22"
|
||||
S_DAVOR = "2026-09-21"
|
||||
S_ERSTER = "2026-09-14"
|
||||
S_SUMME = "ab" * 32
|
||||
S_ANDERE = "cd" * 32
|
||||
S_KALENDER = [
|
||||
{"date": S_DAVOR, "open": "09:30", "close": "16:00"},
|
||||
{"date": S_VORHER, "open": "09:30", "close": "16:00"},
|
||||
] + KALENDER_TAG
|
||||
|
||||
|
||||
def s_eintraege(anders: dict | None = None, ohne: tuple = ()) -> dict:
|
||||
"""Beide Instanzen an S_DAVOR und S_VORHER mit Archiv und bestaetigt; anders ersetzt, ohne loescht."""
|
||||
eintraege = {
|
||||
f"{tag}-{instanz}": {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": True}
|
||||
for tag in (S_DAVOR, S_VORHER)
|
||||
for instanz in ("alpaca", "ibkr")
|
||||
}
|
||||
for name in ohne:
|
||||
del eintraege[name]
|
||||
eintraege.update(anders or {})
|
||||
return eintraege
|
||||
|
||||
|
||||
def s_stand(eintraege: dict | None = None, **oben) -> dict:
|
||||
"""Ein Stand wie von abendkontrolle-host.sh; oben ersetzt oder ergaenzt Schluessel der obersten Ebene."""
|
||||
stand = {"erster_tag": S_ERSTER, "eintraege": s_eintraege() if eintraege is None else eintraege}
|
||||
stand.update(oben)
|
||||
return stand
|
||||
|
||||
|
||||
def fahre_sicherung(wurzel: Path, ziel: Path, teil: str, *, schalter: bool = True, tag: str = TAG,
|
||||
meldung: bool = False, env: dict | None = None) -> dict:
|
||||
"""Ein Lauf eines Teils mit --sicherung; ziel ist der Projektordner (alpaca) oder der Betriebsordner (ibkr)."""
|
||||
befehl = [
|
||||
sys.executable, str(HIER / "abendkontrolle_wsl.py"), "--tag", tag,
|
||||
"--kalender", str(wurzel / "kalender.json"), "--journal", str(wurzel / "journal.txt"),
|
||||
"--dienste", str(wurzel / "dienste.json"),
|
||||
]
|
||||
if teil == "ibkr":
|
||||
befehl += ["--teil", "ibkr", "--ibkr-ordner", str(ziel), "--projekt", str(wurzel),
|
||||
"--ibkr-konto", str(wurzel / "konto.json"), "--jetzt", NACH_SCHLUSS]
|
||||
else:
|
||||
befehl += ["--projekt", str(ziel), "--ohne-kerzen", "--ohne-broker"]
|
||||
if schalter:
|
||||
befehl += ["--sicherung", str(wurzel / "sicherung.json")]
|
||||
if not meldung:
|
||||
befehl.append("--ohne-meldung")
|
||||
umgebung = {k: v for k, v in os.environ.items() if k != "ABENDKONTROLLE_AUSWERTUNG"}
|
||||
umgebung.update(env or {})
|
||||
lauf = subprocess.run(befehl, capture_output=True, text=True, env=umgebung, timeout=300)
|
||||
try:
|
||||
ergebnis = json.loads(lauf.stdout.strip().splitlines()[-1])
|
||||
except (IndexError, json.JSONDecodeError):
|
||||
return {"befunde": [f"KEINE JSON-AUSGABE exit {lauf.returncode}: {lauf.stderr[-300:]}"], "exit": lauf.returncode}
|
||||
ergebnis["_exit"] = lauf.returncode
|
||||
return ergebnis
|
||||
|
||||
|
||||
def sicherung_faelle() -> None:
|
||||
def fall(name, stand, *, befund, teil="alpaca", kalender=S_KALENDER, anzahl=None, exit_code=0,
|
||||
schalter=True, tag=TAG, auch=(), nicht=(), fakt=None):
|
||||
"""stand: None heisst, die Datei fehlt; eine Zeichenkette wird roh geschrieben, sonst als JSON."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
wurzel = Path(tmp)
|
||||
ziel = baue_ibkr(wurzel) if teil == "ibkr" else baue_projekt(wurzel)
|
||||
(wurzel / "kalender.json").write_text(json.dumps(kalender), encoding="utf-8")
|
||||
if isinstance(stand, str):
|
||||
(wurzel / "sicherung.json").write_text(stand, encoding="utf-8")
|
||||
elif stand is not None:
|
||||
(wurzel / "sicherung.json").write_text(json.dumps(stand), encoding="utf-8")
|
||||
erwarte("Sicherung " + name, fahre_sicherung(wurzel, ziel, teil, schalter=schalter, tag=tag),
|
||||
befund=befund, anzahl=anzahl, exit_code=exit_code, auch=auch, nicht=nicht, fakt=fakt)
|
||||
|
||||
a = f"{S_VORHER}-alpaca"
|
||||
i = f"{S_VORHER}-ibkr"
|
||||
offen = {"summe": S_SUMME, "bestaetigt": None, "archiv": True}
|
||||
|
||||
# Grundlinie in beiden Teilen; ohne Schalter bleibt das Werkzeug, wie es war.
|
||||
fall("Grundlinie alpaca", s_stand(), befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung") == {"name": a, "erster_tag": S_ERSTER, "geprueft": True})
|
||||
fall("Grundlinie ibkr", s_stand(), teil="ibkr", befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung") == {"name": i, "erster_tag": S_ERSTER, "geprueft": True})
|
||||
fall("ohne Schalter", s_stand(s_eintraege({a: offen})), schalter=False, befund=None, anzahl=0,
|
||||
fakt=lambda f, e: "sicherung" not in f)
|
||||
# Umgebung und Form der Datei
|
||||
fall("Datei fehlt", None, befund="Sicherung nicht geprueft (FileNotFoundError", anzahl=1)
|
||||
fall("Datei kein JSON", "<html>", befund="Sicherung nicht geprueft (JSONDecodeError", anzahl=1)
|
||||
fall("Datei leer", "", befund="Sicherung nicht geprueft (JSONDecodeError", anzahl=1)
|
||||
fall("Stand null", "null", befund="Sicherung: Stand ist kein Objekt", anzahl=1)
|
||||
fall("Stand als Liste", [], befund="Sicherung: Stand ist kein Objekt", anzahl=1)
|
||||
fall("eintraege als Liste", s_stand([]), befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("eintraege fehlt", {"erster_tag": S_ERSTER}, befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("erster_tag fehlt", {"eintraege": s_eintraege()}, befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("erster_tag als Zahl", s_stand(erster_tag=20260914), befund="Sicherung: erster_tag ist kein Datum", anzahl=1)
|
||||
fall("erster_tag kein Datum", s_stand(erster_tag="morgen"), befund="Sicherung: erster_tag ist kein Datum", anzahl=1)
|
||||
# Fehler, die abendkontrolle-host.sh in den Stand schreibt
|
||||
fall("Host meldet fehler", {"fehler": "Ablage der Sicherung fehlt"},
|
||||
befund="Sicherung nicht geprueft: Ablage der Sicherung fehlt", anzahl=1)
|
||||
fall("fehler neben vollem Stand", s_stand(fehler="Ablage der Sicherung nicht lesbar"),
|
||||
befund="Sicherung nicht geprueft: Ablage der Sicherung nicht lesbar", anzahl=1)
|
||||
# Erster Sicherungstag (seit der Plankritik Runde 1): null und ein Tag nach TAG sind Befunde, sonst
|
||||
# schwiege die Pruefung auf Dauer. Still bleibt nur ein voriger Handelstag vor erster_tag; TAG selbst
|
||||
# als erster_tag ist erlaubt, und der erste Sicherungstag selbst wird geprueft.
|
||||
fall("erster_tag null", s_stand({}, erster_tag=None),
|
||||
befund="Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung", anzahl=1,
|
||||
fakt=lambda f, e: "sicherung" not in f
|
||||
and not any("erster-tag" in h for h in e.get("hinweise", [])))
|
||||
fall("erster_tag nach dem geprueften Tag", s_stand({}, erster_tag="2026-09-24"),
|
||||
befund=f"Sicherung nicht geprueft: erster_tag 2026-09-24 liegt nach dem geprueften Tag {TAG}", anzahl=1,
|
||||
fakt=lambda f, e: "sicherung" not in f)
|
||||
fall("voriger Handelstag vor erster_tag", s_stand({}, erster_tag=TAG), befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("geprueft") is False)
|
||||
fall("erster_tag ist der vorige Handelstag", s_stand({}, erster_tag=S_VORHER),
|
||||
befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# Eintrag des vorigen Handelstags
|
||||
fall("Eintrag fehlt", s_stand(s_eintraege(ohne=(a,))), befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
fall("Eintrag null", s_stand(s_eintraege({a: None})), befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
fall("Eintrag als Liste", s_stand(s_eintraege({a: [S_SUMME, S_SUMME]})),
|
||||
befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# summe
|
||||
for titel, eintrag in (
|
||||
("summe null", {"summe": None, "bestaetigt": S_SUMME}),
|
||||
("summe fehlt", {"bestaetigt": S_SUMME}),
|
||||
("summe leer", {"summe": "", "bestaetigt": S_SUMME}),
|
||||
("summe in Grossbuchstaben", {"summe": S_SUMME.upper(), "bestaetigt": S_SUMME.upper()}),
|
||||
("summe mit 63 Zeichen", {"summe": S_SUMME[:-1], "bestaetigt": S_SUMME[:-1]}),
|
||||
("summe mit 65 Zeichen", {"summe": S_SUMME + "a", "bestaetigt": S_SUMME + "a"}),
|
||||
("summe als Zahl", {"summe": 5, "bestaetigt": S_SUMME}),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: eintrag})), befund=f"Sicherung {a}: keine gueltige Summe", anzahl=1)
|
||||
# bestaetigt
|
||||
for titel, eintrag, text in (
|
||||
("bestaetigt null", {"summe": S_SUMME, "bestaetigt": None}, "nicht bestaetigt"),
|
||||
("bestaetigt fehlt", {"summe": S_SUMME}, "nicht bestaetigt"),
|
||||
("bestaetigt leer", {"summe": S_SUMME, "bestaetigt": ""}, "nicht bestaetigt"),
|
||||
("bestaetigt in Grossbuchstaben", {"summe": S_SUMME, "bestaetigt": S_SUMME.upper()}, "nicht bestaetigt"),
|
||||
("bestaetigt erstes Zeichen anders", {"summe": S_SUMME, "bestaetigt": "0" + S_SUMME[1:]},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
("bestaetigt letztes Zeichen anders", {"summe": S_SUMME, "bestaetigt": S_SUMME[:-1] + "0"},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
("Bestaetigung einer anderen Sicherung", {"summe": S_SUMME, "bestaetigt": S_ANDERE},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: {**eintrag, "archiv": True}})), befund=f"Sicherung {a}: {text}", anzahl=1)
|
||||
# archiv (seit der Plankritik Runde 1): Bei gueltiger Summe zaehlt nur true, sonst fehlt das Archiv.
|
||||
for titel, eintrag in (
|
||||
("archiv false", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": False}),
|
||||
("archiv fehlt", {"summe": S_SUMME, "bestaetigt": S_SUMME}),
|
||||
("archiv null", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": None}),
|
||||
("archiv als Text", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": "true"}),
|
||||
("archiv false, nicht bestaetigt", {"summe": S_SUMME, "bestaetigt": None, "archiv": False}),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: eintrag})), befund=f"Sicherung {a}: Archiv fehlt auf dem Docker-Host",
|
||||
anzahl=1)
|
||||
# Instanz nach --teil
|
||||
fall("Teil ibkr, nur Alpaca bestaetigt", s_stand(s_eintraege({i: offen})), teil="ibkr",
|
||||
befund=f"Sicherung {i}: nicht bestaetigt", anzahl=1, nicht=("alpaca",))
|
||||
fall("Teil alpaca, nur IBKR bestaetigt", s_stand(s_eintraege({a: offen})),
|
||||
befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1, nicht=("ibkr",))
|
||||
fall("Teil ibkr, Eintrag fehlt", s_stand(s_eintraege(ohne=(i,))), teil="ibkr",
|
||||
befund=f"Sicherung {i} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# Reihenfolge: derselbe Schaden am ersten und am letzten Eintrag
|
||||
andere = {name: wert for name, wert in s_eintraege().items() if name != a}
|
||||
fall("Schaden am ersten Eintrag", s_stand({a: offen, **andere}), befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
fall("Schaden am letzten Eintrag", s_stand({**andere, a: offen}), befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
# Kalender: der juengste Handelstag vor TAG, nicht der Vortag und nicht der letzte Listeneintrag
|
||||
fall("Kalender unsortiert", s_stand(s_eintraege({a: offen})), kalender=list(reversed(S_KALENDER)),
|
||||
befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
feiertag = [S_KALENDER[0]] + KALENDER_TAG
|
||||
fall("Feiertag am Vortag, der Tag davor bestaetigt", s_stand(s_eintraege({a: offen})), kalender=feiertag,
|
||||
befund=None, anzahl=0, fakt=lambda f, e: f.get("sicherung", {}).get("name") == f"{S_DAVOR}-alpaca")
|
||||
fall("Feiertag am Vortag, der Tag davor offen", s_stand(s_eintraege({f"{S_DAVOR}-alpaca": offen})),
|
||||
kalender=feiertag, befund=f"Sicherung {S_DAVOR}-alpaca: nicht bestaetigt", anzahl=1)
|
||||
fall("Kalender ohne Tag davor", s_stand(), kalender=KALENDER_TAG, befund="Sicherung: kein Handelstag in den", anzahl=1)
|
||||
fall("Kalender als Objekt", s_stand(), kalender={}, befund="Sicherung nicht geprueft (ValueError", anzahl=2,
|
||||
auch=("Kalender in unerwarteter Form",))
|
||||
fall("Kalender Eintrag ohne date, erste Position", s_stand(), kalender=[{"open": "09:30"}] + S_KALENDER,
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=2)
|
||||
fall("Kalender Eintrag ohne date, letzte Position", s_stand(), kalender=S_KALENDER + [{"date": 5}],
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=2)
|
||||
fall("Kalender mit Datum in anderer Form", s_stand(), kalender=[{"date": "22.09.2026"}] + S_KALENDER,
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=1)
|
||||
# Fenster (seit der Plankritik Runde 1): SICHERUNG_FENSTER_TAGE Kalendertage vor TAG, der Rand gehoert
|
||||
# dazu, TAG selbst nie. Das Werkzeug prueft keinen Wochentag; Rand und Tag davor sind Wochenendtage.
|
||||
rand = "2026-09-13"
|
||||
fall("Kalender voriger Handelstag am Rand des Fensters",
|
||||
s_stand(s_eintraege({f"{rand}-alpaca": {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": True}}),
|
||||
erster_tag="2026-09-01"),
|
||||
kalender=[{"date": rand, "open": "09:30", "close": "16:00"}] + KALENDER_TAG, befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("name") == f"{rand}-alpaca")
|
||||
fall("Kalender voriger Handelstag ausserhalb des Fensters", s_stand(),
|
||||
kalender=[{"date": "2026-09-12", "open": "09:30", "close": "16:00"}] + KALENDER_TAG,
|
||||
befund=f"Sicherung: kein Handelstag in den 10 Tagen vor {TAG} im Kalender", anzahl=1)
|
||||
fall("Kalender Tag selbst nie der vorige", s_stand(s_eintraege({f"{TAG}-alpaca": offen})),
|
||||
kalender=KALENDER_TAG + [S_KALENDER[1]], befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("name") == a)
|
||||
# Zusaetzlich: unbekannte Schluessel und fremde Namen stoeren nicht
|
||||
fall("unbekannte Schluessel und fremde Namen",
|
||||
s_stand(s_eintraege({"notiz": {"summe": None}, f"{S_VORHER}-paper": None}), neu=1), befund=None, anzahl=0)
|
||||
# Bedienfehler: Ein falscher Tag bricht den Teil ab, und die Sicherung meldet sich trotzdem.
|
||||
fall("Tag falsch", s_stand(), tag="2026-13-40", befund="abgebrochen", exit_code=2, anzahl=2,
|
||||
auch=("Sicherung nicht geprueft (ValueError",))
|
||||
|
||||
# Meldeweg: Ein Befund der Sicherung allein loest die Meldung aus (lokaler Probeserver, kein Netz).
|
||||
with tempfile.TemporaryDirectory() as tmp_server:
|
||||
mitschrift = Path(tmp_server) / "mitschrift.jsonl"
|
||||
port = 18767
|
||||
server = subprocess.Popen([sys.executable, str(HIER / "ntfy_probe_server.py"), str(port), str(mitschrift)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
import socket
|
||||
import time
|
||||
for _ in range(50):
|
||||
try:
|
||||
socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
wurzel = Path(tmp)
|
||||
projekt = baue_projekt(wurzel)
|
||||
(wurzel / "kalender.json").write_text(json.dumps(S_KALENDER), encoding="utf-8")
|
||||
(wurzel / "sicherung.json").write_text(json.dumps(s_stand(s_eintraege({a: offen}))), encoding="utf-8")
|
||||
text = (ECHTES_PROJEKT / "config" / "paper.toml").read_text(encoding="utf-8")
|
||||
anker = 'ntfy_url = "https://ntfy.sh"'
|
||||
if text.count(anker) != 1:
|
||||
raise RuntimeError("Anker ntfy_url fehlt in config/paper.toml")
|
||||
(projekt / "config" / "paper.toml").write_text(
|
||||
text.replace(anker, f'ntfy_url = "http://127.0.0.1:{port}"\nklartext_lokal = true'), encoding="utf-8")
|
||||
(wurzel / "secrets.toml").write_text('[ntfy]\naktiv = true\nthema = "abendkontrolle-angriff"\n',
|
||||
encoding="utf-8")
|
||||
ergebnis = fahre_sicherung(wurzel, projekt, "alpaca", meldung=True,
|
||||
env={"TRADING_GEHEIMNISSE": str(wurzel / "secrets.toml")})
|
||||
zeilen = ([json.loads(z) for z in mitschrift.read_text(encoding="utf-8").split("\n") if z.strip()]
|
||||
if mitschrift.exists() else [])
|
||||
titel = [str(z.get("title") or "") for z in zeilen]
|
||||
befunde = ergebnis.get("befunde", [])
|
||||
gesendet = (ergebnis.get("fakten") or {}).get("meldung")
|
||||
ok = (gesendet == "gesendet" and len(zeilen) == 1 and f"Abendkontrolle {TAG}: 1 Befund(e)" in titel[0]
|
||||
and len(befunde) == 1 and befunde[0].startswith(f"Sicherung {a}: nicht bestaetigt"))
|
||||
zeile = (f"{'OK ' if ok else 'FEHL'} Sicherung Befund geht in die Meldung: "
|
||||
f"befunde={befunde[:2]} titel={titel} meldung={gesendet}")
|
||||
print(zeile)
|
||||
if not ok:
|
||||
abweichungen.append(zeile)
|
||||
finally:
|
||||
server.terminate()
|
||||
server.wait(timeout=10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.platform == "win32":
|
||||
windows_faelle()
|
||||
ibkr_windows_faelle()
|
||||
elif "--nur-sicherung" in sys.argv[1:]:
|
||||
sicherung_faelle()
|
||||
else:
|
||||
wsl_faelle()
|
||||
ibkr_wsl_faelle()
|
||||
sicherung_faelle()
|
||||
print(f"ERGEBNIS: {len(abweichungen)} Abweichung(en)")
|
||||
sys.exit(1 if abweichungen else 0)
|
||||
@@ -4,8 +4,11 @@ Koordinator-Werkzeug, nicht fuers Repo. Jeder Mutant laeuft auf einer Wegwerfkop
|
||||
In WSL (Venv) die Mutanten von abendkontrolle_wsl.py, unter Windows die von abendkontrolle.py und
|
||||
rueckweg_aapl.py. Aufruf: python abendkontrolle_mutanten.py Exit 0, wenn keiner ueberlebt.
|
||||
Ein Mutant, der das Verhalten nicht aendert, ist keine Luecke (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 8).
|
||||
Seit Paket E9: --nur TEXT faehrt nur die Mutanten mit TEXT im Namen (etwa --nur Sicherung in WSL); ein
|
||||
fuenftes Element im Tupel geht als Argument an den Treiber.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -64,6 +67,78 @@ WSL_MUTANTEN = [
|
||||
("IBKR Muster aus", "abendkontrolle_wsl.py", " if muster in text:", " if False:"),
|
||||
("IBKR Titel ohne Praefix", "abendkontrolle_wsl.py", 'IBKR_TITEL = "IBKR "', 'IBKR_TITEL = ""'),
|
||||
("IBKR Meldeweiche aus", "abendkontrolle_wsl.py", "melde(IBKR_TITEL + titel if ibkr else titel,", "melde(titel,"),
|
||||
# Seit Paket E9 (Task 726): --sicherung. Der Treiber faehrt dafuer nur seine Faelle zur Sicherung.
|
||||
("Sicherung letzter Listeneintrag statt juengster Tag", "abendkontrolle_wsl.py",
|
||||
" return max(tage) if tage else None", " return tage[-1] if tage else None", "--nur-sicherung"),
|
||||
("Sicherung Vortag statt Kalender", "abendkontrolle_wsl.py",
|
||||
" vorher = juengster_handelstag(lies_kalender_spanne(start, ende, argumente.kalender, Path(argumente.projekt)))",
|
||||
" vorher = ende", "--nur-sicherung"),
|
||||
("Sicherung Kalenderform egal", "abendkontrolle_wsl.py",
|
||||
' raise ValueError("Kalender mit einem Eintrag ohne date JJJJ-MM-TT")', " continue",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Fehler des Hosts ueberlesen", "abendkontrolle_wsl.py", ' if "fehler" in stand:', " if False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Stand ohne Typpruefung", "abendkontrolle_wsl.py", " if not isinstance(stand, dict):", " if False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung eintraege ohne Typpruefung", "abendkontrolle_wsl.py",
|
||||
' if "erster_tag" not in stand or not isinstance(stand.get("eintraege"), dict):',
|
||||
' if "erster_tag" not in stand:', "--nur-sicherung"),
|
||||
("Sicherung erster_tag ohne Formpruefung", "abendkontrolle_wsl.py",
|
||||
" if not isinstance(erster, str) or not DATUM.fullmatch(erster):", " if False:", "--nur-sicherung"),
|
||||
("Sicherung ohne erster_tag nur Hinweis", "abendkontrolle_wsl.py",
|
||||
' k.befund("Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung")',
|
||||
' k.hinweis("Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung")', "--nur-sicherung"),
|
||||
("Sicherung vor erster_tag geprueft", "abendkontrolle_wsl.py", " if vorher < erster:", " if False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung erster_tag selbst ausgenommen", "abendkontrolle_wsl.py", " if vorher < erster:",
|
||||
" if vorher <= erster:", "--nur-sicherung"),
|
||||
("Sicherung kein Handelstag still", "abendkontrolle_wsl.py",
|
||||
' k.befund(f"Sicherung: kein Handelstag in den {SICHERUNG_FENSTER_TAGE} Tagen vor {tag} im Kalender")',
|
||||
" pass", "--nur-sicherung"),
|
||||
("Sicherung immer alpaca", "abendkontrolle_wsl.py",
|
||||
' instanz = "ibkr" if argumente.teil == "ibkr" else "alpaca"', ' instanz = "alpaca"', "--nur-sicherung"),
|
||||
("Sicherung fehlender Eintrag still", "abendkontrolle_wsl.py",
|
||||
' k.befund(f"Sicherung {name} fehlt auf dem Docker-Host")', " pass", "--nur-sicherung"),
|
||||
("Sicherung Summe ungeprueft", "abendkontrolle_wsl.py",
|
||||
" if not isinstance(summe, str) or not SUMME.fullmatch(summe):", " if summe is None:", "--nur-sicherung"),
|
||||
("Sicherung Bestaetigung ungeprueft", "abendkontrolle_wsl.py",
|
||||
" elif not isinstance(bestaetigt, str) or not SUMME.fullmatch(bestaetigt):", " elif False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Vergleich aus", "abendkontrolle_wsl.py", " elif bestaetigt != summe:", " elif False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Schalter nicht verdrahtet", "abendkontrolle_wsl.py", " if argumente.sicherung:", " if False:",
|
||||
"--nur-sicherung"),
|
||||
# Seit der Plankritik Runde 1 (Task 726): Fenster, erster_tag, Archiv und die zwei Faelle ohne Mutanten.
|
||||
("Sicherung Fenster 1 Tag", "abendkontrolle_wsl.py", "SICHERUNG_FENSTER_TAGE = 10", "SICHERUNG_FENSTER_TAGE = 1",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Fenster einen Tag kuerzer", "abendkontrolle_wsl.py",
|
||||
" start = (geprueft - timedelta(days=SICHERUNG_FENSTER_TAGE)).isoformat()",
|
||||
" start = (geprueft - timedelta(days=SICHERUNG_FENSTER_TAGE - 1)).isoformat()", "--nur-sicherung"),
|
||||
("Sicherung Ende am Tag selbst", "abendkontrolle_wsl.py",
|
||||
" ende = (geprueft - timedelta(days=1)).isoformat()", " ende = geprueft.isoformat()", "--nur-sicherung"),
|
||||
("Sicherung Spanne der Datei aus", "abendkontrolle_wsl.py",
|
||||
' return in_spanne(json.loads(Path(kalender_datei).read_text(encoding="utf-8")), start, ende)',
|
||||
' return json.loads(Path(kalender_datei).read_text(encoding="utf-8"))', "--nur-sicherung"),
|
||||
("Sicherung Spanne ohne Anfang", "abendkontrolle_wsl.py", ' or start <= eintrag["date"] <= ende',
|
||||
' or eintrag["date"] <= ende', "--nur-sicherung"),
|
||||
("Sicherung Spanne ohne Typpruefung", "abendkontrolle_wsl.py",
|
||||
" if not isinstance(liste, list):\n return liste", " if False:\n return liste",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung Kalender ohne Typpruefung", "abendkontrolle_wsl.py",
|
||||
' if not isinstance(liste, list):\n raise ValueError("Kalender ist keine Liste")',
|
||||
' if False:\n raise ValueError("Kalender ist keine Liste")', "--nur-sicherung"),
|
||||
("Sicherung erster_tag nach dem Tag still", "abendkontrolle_wsl.py", " if erster > tag:", " if False:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung erster_tag am Tag selbst als Befund", "abendkontrolle_wsl.py", " if erster > tag:",
|
||||
" if erster >= tag:", "--nur-sicherung"),
|
||||
("Sicherung Archiv ungeprueft", "abendkontrolle_wsl.py", ' elif eintrag.get("archiv") is not True:',
|
||||
" elif False:", "--nur-sicherung"),
|
||||
("Sicherung Archiv nur false zaehlt", "abendkontrolle_wsl.py", ' elif eintrag.get("archiv") is not True:',
|
||||
' elif eintrag.get("archiv") is False:', "--nur-sicherung"),
|
||||
("Sicherung ohne Schalter geprueft", "abendkontrolle_wsl.py", " if argumente.sicherung:", " if True:",
|
||||
"--nur-sicherung"),
|
||||
("Sicherung unbekannter Schluessel als Fehler", "abendkontrolle_wsl.py", ' if "fehler" in stand:',
|
||||
' if "fehler" in stand or set(stand) - {"erster_tag", "eintraege"}:', "--nur-sicherung"),
|
||||
]
|
||||
WIN_MUTANTEN = [
|
||||
("keine Ergebnisdatei", "abendkontrolle.py", '(ablage / f"{tag}{endung}.json").write_text(',
|
||||
@@ -109,9 +184,19 @@ TREIBER = {
|
||||
"rueckweg_aapl.py": "rueckweg_aapl_angriffe.py",
|
||||
}
|
||||
|
||||
zerleger = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
# Seit Paket E9: nur die Mutanten, deren Name den Text enthaelt.
|
||||
zerleger.add_argument("--nur", help="nur Mutanten, deren Name diesen Text enthaelt")
|
||||
argumente = zerleger.parse_args()
|
||||
mutanten = WIN_MUTANTEN if sys.platform == "win32" else WSL_MUTANTEN
|
||||
if argumente.nur is not None:
|
||||
mutanten = [mutant for mutant in mutanten if argumente.nur in mutant[0]]
|
||||
if not mutanten:
|
||||
print(f"KEIN MUTANT mit {argumente.nur!r} im Namen")
|
||||
sys.exit(2)
|
||||
ueberlebt = []
|
||||
for name, datei, alt, neu in mutanten:
|
||||
# Ein fuenftes Element im Tupel geht als Argument an den Treiber (seit Paket E9).
|
||||
for name, datei, alt, neu, *treiber_argumente in mutanten:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ziel = Path(tmp) / "pruefung"
|
||||
shutil.copytree(HIER, ziel, ignore=shutil.ignore_patterns("__pycache__"))
|
||||
@@ -122,7 +207,7 @@ for name, datei, alt, neu in mutanten:
|
||||
ueberlebt.append(name)
|
||||
continue
|
||||
pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
|
||||
lauf = subprocess.run([sys.executable, str(ziel / TREIBER[datei])], capture_output=True, text=True,
|
||||
lauf = subprocess.run([sys.executable, str(ziel / TREIBER[datei]), *treiber_argumente], capture_output=True, text=True,
|
||||
timeout=1800, encoding="utf-8", errors="replace", cwd=tmp)
|
||||
faelle = [z for z in lauf.stdout.splitlines() if z.startswith("FEHL")]
|
||||
rot = lauf.returncode != 0
|
||||
|
||||
@@ -81,8 +81,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