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
@@ -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())
|
||||
Reference in new issue
Block a user