- .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]>
476 lines
23 KiB
Python
476 lines
23 KiB
Python
"""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())
|