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,215 @@
|
||||
"""Startet die Abendkontrolle des Paper-Betriebs und schreibt ihr Ergebnis in die Tagesnotiz.
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Laeuft unter Windows mit pythonw.exe als geplante Aufgabe
|
||||
"Trading Plattform - Abendkontrolle" (abendkontrolle_einrichten.ps1): werktags 22:45 und fuenf Minuten
|
||||
nach jeder Anmeldung. Auftrag Pascal 2026-09-24: dauerhaft, uebersteht Neustarts, blockiert keine
|
||||
Updates -- deshalb eine Windows-Aufgabe (bleibt ueber Neustarts, holt verpasste Laeufe nach) und kein
|
||||
Claude-Prozess (haelt keine Datei von Claude Code oder Desktop offen).
|
||||
|
||||
Ablauf: Jeder Werktag seit ERSTER_TAG, hoechstens NACHHOLEN_TAGE zurueck, ohne Ergebnisdatei in
|
||||
ABLAGE wird geprueft; der heutige erst ab FRUEHESTENS (der Bericht der Tagesauswertung kommt 22:20).
|
||||
Je Tag: abendkontrolle_wsl.py in WSL (liest nur, meldet bei Befund selbst ueber ntfy), dann zuerst das
|
||||
Ergebnis als JSON nach ABLAGE und danach eine Zeile in Inbox/<tag>.md unter der ersten Ueberschrift,
|
||||
die mit "## Agentenprotokoll" beginnt.
|
||||
- Liefert der WSL-Teil kein Ergebnis oder endet er mit Exit 2 (abgebrochen), bleibt die Ergebnisdatei
|
||||
aus, die Notiz bekommt einen Befund mit #needs-review, und der naechste Lauf versucht den Tag erneut.
|
||||
- Scheitert nur die Notiz (etwa eine Datei, die kein UTF-8 ist), bleibt es bei der Ergebnisdatei und
|
||||
einer Zeile in lauf.log: Die Kontrolle ist gelaufen und hat ihre Meldung schon geschickt; ein zweiter
|
||||
Lauf schickte sie nur noch einmal.
|
||||
- Ein Tag, der laenger als NACHHOLEN_TAGE scheitert, faellt aus dem Fenster (lauf.log behaelt ihn).
|
||||
Die Notiz wird ueber eine Nachbardatei und os.replace geschrieben: Ein Abbruch mitten im Schreiben
|
||||
hinterlaesst die alte Datei, nicht eine halbe. Gegen eine Claude-Sitzung, die dieselbe Notiz im selben
|
||||
Augenblick schreibt, schuetzt das nicht (Zeitfenster Lesen bis Ersetzen, Millisekunden) -- benannt.
|
||||
|
||||
Annahme, benannt: Die Uhr dieses Rechners geht nach Berliner Zeit (tzutil: W. Europe Standard Time,
|
||||
gelesen am 2026-09-24); ohne das Paket tzdata kennt Python unter Windows keine Zeitzonen-Datenbank.
|
||||
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
from datetime import time
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
VAULT = Path(r"D:\Claude")
|
||||
PROJEKT_TEIL = Path("Code Projekte") / "active - Trading Plattform"
|
||||
ABLAGE_TEIL = PROJEKT_TEIL / "data" / "paper" / "abendkontrollen"
|
||||
WSL = r"C:\Windows\System32\wsl.exe"
|
||||
WSL_PY = "/home/claude/venvs/trading/bin/python"
|
||||
WSL_SKRIPT = "/mnt/d/Claude/.vault-tools/trading-plattform-pruefung/abendkontrolle_wsl.py"
|
||||
ERSTER_TAG = date(2026, 9, 24)
|
||||
NACHHOLEN_TAGE = 7
|
||||
FRUEHESTENS = time(22, 30)
|
||||
WSL_TIMEOUT_S = 1200
|
||||
KEIN_FENSTER = 0x08000000 # CREATE_NO_WINDOW: pythonw hat keine Konsole, wsl.exe soll keine oeffnen
|
||||
UEBERSCHRIFT = "## Agentenprotokoll"
|
||||
|
||||
|
||||
def faellige_tage(jetzt: datetime, ablage: Path) -> list[str]:
|
||||
tage = []
|
||||
for zurueck in range(NACHHOLEN_TAGE, -1, -1):
|
||||
tag = jetzt.date() - timedelta(days=zurueck)
|
||||
if tag < ERSTER_TAG or tag.weekday() >= 5:
|
||||
continue
|
||||
if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:
|
||||
continue
|
||||
if (ablage / f"{tag.isoformat()}.json").exists():
|
||||
continue
|
||||
tage.append(tag.isoformat())
|
||||
return tage
|
||||
|
||||
|
||||
def fahre_wsl(tag: str) -> dict:
|
||||
lauf = subprocess.run(
|
||||
[WSL, "-d", "Debian", "-u", "claude", "--", WSL_PY, WSL_SKRIPT, "--tag", tag],
|
||||
capture_output=True, timeout=WSL_TIMEOUT_S, creationflags=KEIN_FENSTER,
|
||||
)
|
||||
ausgabe = lauf.stdout.decode("utf-8", errors="replace")
|
||||
for zeile in reversed(ausgabe.splitlines()):
|
||||
zeile = zeile.strip()
|
||||
if zeile.startswith("{"):
|
||||
try:
|
||||
return json.loads(zeile)
|
||||
except json.JSONDecodeError:
|
||||
break
|
||||
fehler = lauf.stderr.decode("utf-8", errors="replace").strip().splitlines()[-3:]
|
||||
raise RuntimeError(f"keine JSON-Zeile vom WSL-Teil (Exit {lauf.returncode}; {' | '.join(fehler)[:300]})")
|
||||
|
||||
|
||||
def zeile_fuer(ergebnis: dict, uhrzeit: str) -> str:
|
||||
kopf = f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch)"
|
||||
befunde = ergebnis.get("befunde") or []
|
||||
fakten = ergebnis.get("fakten") or {}
|
||||
hinweise = ergebnis.get("hinweise") or []
|
||||
if fakten.get("handelstag") is False and not befunde:
|
||||
return f"{kopf}: kein Handelstag laut Alpaca-Kalender."
|
||||
teile = []
|
||||
ende = fakten.get("ende") or {}
|
||||
if ende:
|
||||
teile.append(
|
||||
f"Ende {ende.get('grund')}, Exit {ende.get('exit')}, Orders {ende.get('orders')}, "
|
||||
f"Position {ende.get('position')}"
|
||||
)
|
||||
laeufe = fakten.get("laeufe") or []
|
||||
if laeufe:
|
||||
teile.append(f"{len(laeufe)} Lauf/Läufe, {sum(l.get('takte_aktiv', 0) for l in laeufe)} Takte aktiv")
|
||||
if "broker_offen" in fakten:
|
||||
teile.append(f"offene Positionen beim Broker {fakten['broker_offen']}")
|
||||
if "bericht" in fakten:
|
||||
teile.append("Tagesbericht da" if fakten["bericht"] else "Tagesbericht fehlt")
|
||||
killswitch = fakten.get("killswitch") or {}
|
||||
if killswitch:
|
||||
teile.append(f"Killswitch {killswitch.get('zustand')} ({killswitch.get('ausloeser')})")
|
||||
zusammen = "; ".join(teile)
|
||||
if not befunde:
|
||||
text = f"{kopf}: ohne Befund. {zusammen}."
|
||||
else:
|
||||
text = f"{kopf}: **{len(befunde)} Befund(e)** #needs-review. {zusammen}."
|
||||
text += "".join(f"\n - {befund}" for befund in befunde)
|
||||
if fakten.get("meldung"):
|
||||
text += f"\n - Meldung ans Telefon: {fakten['meldung']}"
|
||||
text += "".join(f"\n - Hinweis: {hinweis}" for hinweis in hinweise[:5])
|
||||
return text
|
||||
|
||||
|
||||
def schreibe_notiz(vault: Path, tag: str, zeile: str) -> None:
|
||||
notiz = vault / "Inbox" / f"{tag}.md"
|
||||
if notiz.exists():
|
||||
text = notiz.read_bytes().decode("utf-8") # kein UTF-8: Ausnahme, die Notiz bleibt unberuehrt
|
||||
else:
|
||||
text = (
|
||||
f"---\ntype: daily\ntitle: \"{tag}\"\nstatus: open\nsummary: \"Tagesnotiz vom {tag}.\"\n"
|
||||
f"tags: [daily]\ncreated: {tag}\nupdated: {tag}\n---\n\n# {tag}\n"
|
||||
)
|
||||
# Zeilenenden der Datei bleiben, wie sie sind; gesucht wird ohne \r.
|
||||
zeilen = text.split("\n")
|
||||
bereinigt = [z.rstrip("\r").rstrip() for z in zeilen]
|
||||
cr = "\r" if any(z.endswith("\r") for z in zeilen) else ""
|
||||
# Die erste Ueberschrift, die mit "## Agentenprotokoll" beginnt ("## Agentenprotokoll — …" kommt vor).
|
||||
start = next((i for i, z in enumerate(bereinigt) if z == UEBERSCHRIFT or z.startswith(UEBERSCHRIFT + " ")), None)
|
||||
if start is None:
|
||||
nl = cr + "\n"
|
||||
text = text.rstrip("\r\n") + nl + nl + UEBERSCHRIFT + nl + nl + nl.join(zeile.split("\n")) + nl
|
||||
else:
|
||||
ende = next((i for i in range(start + 1, len(zeilen)) if bereinigt[i].startswith("## ")), len(zeilen))
|
||||
# Hinter den letzten nicht leeren Eintrag des Abschnitts; die Leerzeilen danach ersetzt eine.
|
||||
einfuegen = ende
|
||||
while einfuegen > start + 1 and not bereinigt[einfuegen - 1]:
|
||||
einfuegen -= 1
|
||||
block = [z + cr for z in zeile.split("\n")]
|
||||
if einfuegen == start + 1:
|
||||
block = [cr] + block
|
||||
zeilen[einfuegen:ende] = block + ([cr] if ende < len(zeilen) else [""])
|
||||
text = "\n".join(zeilen)
|
||||
if not text.endswith("\n"):
|
||||
text += "\n"
|
||||
neu = notiz.with_name(notiz.name + ".abendkontrolle.tmp")
|
||||
neu.write_bytes(text.encode("utf-8"))
|
||||
os.replace(neu, notiz)
|
||||
|
||||
|
||||
def logge(ablage: Path, text: str) -> None:
|
||||
ablage.mkdir(parents=True, exist_ok=True)
|
||||
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as datei:
|
||||
datei.write(f"{datetime.now().isoformat(timespec='seconds')} {text}\n")
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
zerleger = argparse.ArgumentParser(allow_abbrev=False)
|
||||
zerleger.add_argument("--tag")
|
||||
zerleger.add_argument("--vault", default=str(VAULT))
|
||||
zerleger.add_argument("--ohne-wsl", help="Ergebnis aus einer JSON-Datei statt aus WSL (Angriffsfaelle)")
|
||||
zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt statt der Uhr (Angriffsfaelle)")
|
||||
argumente = zerleger.parse_args(argv)
|
||||
vault = Path(argumente.vault)
|
||||
ablage = vault / ABLAGE_TEIL
|
||||
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
|
||||
tage = [argumente.tag] if argumente.tag else faellige_tage(jetzt, ablage)
|
||||
logge(ablage, f"start, faellig: {tage}")
|
||||
code = 0
|
||||
for tag in tage:
|
||||
uhrzeit = jetzt.strftime("%H:%M") if tag == jetzt.date().isoformat() else f"{jetzt:%Y-%m-%d %H:%M}"
|
||||
try:
|
||||
if argumente.ohne_wsl:
|
||||
ergebnis = json.loads(Path(argumente.ohne_wsl).read_text(encoding="utf-8"))
|
||||
else:
|
||||
ergebnis = fahre_wsl(tag)
|
||||
if not isinstance(ergebnis, dict) or not isinstance(ergebnis.get("befunde"), list):
|
||||
raise RuntimeError("Ergebnis des WSL-Teils in unerwarteter Form")
|
||||
except Exception as ausnahme:
|
||||
code = 1
|
||||
logge(ablage, f"{tag}: gescheitert {ausnahme!r}\n{traceback.format_exc()}")
|
||||
try:
|
||||
schreibe_notiz(
|
||||
vault, tag,
|
||||
f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch): **gescheitert** "
|
||||
f"({type(ausnahme).__name__}: {str(ausnahme)[:200]}) #needs-review. Der nächste Lauf versucht es erneut.",
|
||||
)
|
||||
except Exception as zweite:
|
||||
logge(ablage, f"{tag}: auch die Notiz scheiterte {zweite!r}")
|
||||
continue
|
||||
# Ein Abbruch im WSL-Teil (Exit 2) wird wiederholt; alles andere gilt als gelaufen.
|
||||
if ergebnis.get("exit") != 2:
|
||||
(ablage / f"{tag}.json").write_text(
|
||||
json.dumps(ergebnis, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n"
|
||||
)
|
||||
else:
|
||||
code = 1
|
||||
logge(ablage, f"{tag}: {len(ergebnis['befunde'])} Befund(e), exit {ergebnis.get('exit')}")
|
||||
try:
|
||||
schreibe_notiz(vault, tag, zeile_fuer(ergebnis, uhrzeit))
|
||||
except Exception as ausnahme:
|
||||
code = 1
|
||||
logge(ablage, f"{tag}: Notiz nicht geschrieben {ausnahme!r}")
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user