Automatischer Push: 8 Datei(en) geändert
- .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/abbau_alpaca_lesen.py - .vault-tools/trading-plattform-pruefung/abbau_glattstellen.sh - .vault-tools/trading-plattform-pruefung/abbau_ibkr_lesen.sh - .vault-tools/trading-plattform-pruefung/abbau_restbestand.py - .vault-tools/trading-plattform-pruefung/abbau_restbestand_angriffe.py - .vault-tools/trading-plattform-pruefung/abbau_restbestand_einrichten.ps1 - AI/Regelwerk/windows-shell.md 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
30ed04a643
commit
3784322a76
8 files changed
+1687
-3
No files matched your search
@@ -0,0 +1,575 @@
|
||||
"""Abbau Restbestand: stellt am 2026-10-08 beide Paper-Konten ueber den Notweg glatt, Ersatztag 2026-10-09.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-10-07
|
||||
Doku: LIESMICH.md in diesem Ordner, Abschnitt "Paket AB"
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Am 2026-10-07 um 20:13 beendete ein Druck auf den
|
||||
Netzschalter beide Paper-Runner mitten im Handel, ohne Glattstellen. Pascal am selben Abend:
|
||||
morgen soweit es geht alles verkaufen, Testlaeufe bis Dienstag gestoppt. Plan im Projekt:
|
||||
docs/plans/2026-10-07-paket-ab-abbau-restbestand/. Gestartet von der Windows-Aufgabe
|
||||
"Trading Plattform - Abbau Restbestand" (abbau_restbestand_einrichten.ps1) mit --modus scharf.
|
||||
|
||||
Ablauf im Modus scharf, jeder Schritt mit seinem Grund:
|
||||
1. Nur zwischen FRUEHESTENS und SPAETESTENS (Uhr dieses Rechners); sonst nichts, auch keine Datei.
|
||||
Ergebnisdatei mit "flach": true -> nichts mehr, nur eine Zeile im Log.
|
||||
2. Runner-Units (systemctl show, LoadState und ActiveState): Laeuft eine, ist sie nicht geladen
|
||||
oder ihr Zustand nicht lesbar, wird diese Instanz nicht angefasst (Befund). systemctl is-active
|
||||
meldete auch fuer eine Unit, die es nicht gibt, inactive. Die Zeitgeber sind seit 2026-10-07 aus.
|
||||
3. Marktuhr von Alpaca (abbau_alpaca_lesen.py): zu oder weniger als MIN_BIS_SCHLUSS_S bis zum
|
||||
Schluss -> nichts, nur eine Zeile im Log. Nicht lesbar -> Befund, nichts angefasst.
|
||||
4. Bestaende beider Instanzen; nicht lesbar -> Befund, diese Instanz nicht angefasst.
|
||||
5. Je Instanz mit Position oder offener Order genau ein Aufruf von abbau_glattstellen.sh
|
||||
(scripts/killswitch.py glattstellen; das Bestaetigungswort bleibt in WSL). Keine Schleife:
|
||||
Der naechste Termin der Aufgabe ist die Wiederholung. Wirft der Aufruf, gilt Exit None.
|
||||
6. Nach einem Eingriff WARTEN_S warten und beide Bestaende neu lesen.
|
||||
7. Ergebnisdatei abbau.json; "flach": true nur, wenn beide Instanzen nachher ohne Position und
|
||||
ohne offene Order gelesen sind und keine Instanz gesperrt ist. Sonst beobachtet der naechste
|
||||
Termin weiter.
|
||||
8. Bei Eingriff, Befund oder dem ersten "flach" eine Zeile in Inbox/<tag>.md und eine Meldung mit
|
||||
hoher Prioritaet (abendkontrolle_wsl.py --meldung). Die Abendkontrolle ist in der Pause aus;
|
||||
diese Meldung ist das einzige Netz.
|
||||
Modus lesen: nur die lesenden Aufrufe, ohne Fenster und ohne die Sperre durch "flach"; kein
|
||||
Glattstellen, keine Meldung, keine Notiz, keine Datei. Das Ergebnis steht auf stdout.
|
||||
Jeder Aufruf nach draussen geht ueber wsl(); die Angriffsfaelle ersetzen sie. wsl.exe schreibt eigene
|
||||
Fehler in UTF-16LE: wsl() setzt WSL_UTF8=1 und entfernt NUL, sonst scheiterte die Meldung an einem
|
||||
NUL im Argument. Ausgabe und Befunde sind einzeilig: Ein fremder Fehlertext mit Zeilenumbruch
|
||||
setzte sonst eigene Zeilen, bis hin zu einer Ueberschrift, in die Tagesnotiz. Befunde aus Ausnahmen
|
||||
nennen nur den Typ; der Traceback steht nur in abbau.log.
|
||||
Annahme, benannt wie in abendkontrolle.py: Die Uhr dieses Rechners geht nach Berliner Zeit.
|
||||
Aufruf: python abbau_restbestand.py --modus scharf|lesen [--jetzt ISO] [--projekt PFAD] [--vault PFAD]
|
||||
Exit: 0 ohne Befund, 1 mit Befund, 2 Bedienfehler.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from abendkontrolle import KEIN_FENSTER # noqa: E402
|
||||
from abendkontrolle import WSL # noqa: E402
|
||||
from abendkontrolle import WSL_PY # noqa: E402
|
||||
from abendkontrolle import WSL_SKRIPT # noqa: E402
|
||||
from abendkontrolle import schreibe_notiz # noqa: E402
|
||||
|
||||
VAULT = Path(r"D:\Claude")
|
||||
PROJEKT = VAULT / "Code Projekte" / "active - Trading Plattform"
|
||||
ABLAGE_TEIL = Path("data") / "paper" / "abbau"
|
||||
ERGEBNIS_NAME = "abbau.json"
|
||||
LOG_NAME = "abbau.log"
|
||||
# Entscheidung 4 der Anforderung, Berliner Ortszeit, beide Grenzen eingeschlossen.
|
||||
FRUEHESTENS = datetime(2026, 10, 8, 15, 40)
|
||||
SPAETESTENS = datetime(2026, 10, 9, 21, 45)
|
||||
MIN_BIS_SCHLUSS_S = 600
|
||||
WARTEN_S = 30
|
||||
SCHARF = "scharf"
|
||||
LESEN = "lesen"
|
||||
MODI = (SCHARF, LESEN)
|
||||
NICHTS = "nichts"
|
||||
STILL = "still"
|
||||
LESEN_ENDE = "lesen_ende"
|
||||
BERICHT = "bericht"
|
||||
INSTANZEN = ("alpaca", "ibkr")
|
||||
UNITS = {"alpaca": "trading-paper.service", "ibkr": "trading-paper-ibkr.service"}
|
||||
# Die Instanzordner in WSL. Nur dieses Skript nennt sie; die WSL-Teile bekommen sie als Argument.
|
||||
ORDNER_WSL = {
|
||||
"alpaca": "/mnt/d/Claude/Code Projekte/active - Trading Plattform",
|
||||
"ibkr": "/mnt/d/Claude/.arbeitsbaeume/trading-ibkr-betrieb",
|
||||
}
|
||||
HIER_WSL = "/mnt/d/Claude/.vault-tools/trading-plattform-pruefung"
|
||||
ALPACA_PY = "/home/claude/venvs/trading/bin/python"
|
||||
ALPACA_LESEN = HIER_WSL + "/abbau_alpaca_lesen.py"
|
||||
IBKR_LESEN = HIER_WSL + "/abbau_ibkr_lesen.sh"
|
||||
GLATTSTELLEN = HIER_WSL + "/abbau_glattstellen.sh"
|
||||
RUNNER_LAEUFT = ("active", "activating", "deactivating", "reloading", "refreshing")
|
||||
RUNNER_STEHT = ("inactive", "failed")
|
||||
RUNNER_TIMEOUT_S = 60
|
||||
LESEN_TIMEOUT_S = 180
|
||||
# killswitch.py: Gateway-Zeitlimit 10 s je Anfrage, bis zu drei Sammelversuche, 20 s Positionsfrist.
|
||||
GLATT_TIMEOUT_S = 600
|
||||
MELDUNG_TIMEOUT_S = 120
|
||||
ZEILEN_MAX = 12
|
||||
ZEILE_MAX = 200
|
||||
# CREATE_NO_WINDOW gibt es nur unter Windows; die Angriffsfaelle laufen auch in WSL.
|
||||
FENSTERLOS = KEIN_FENSTER if sys.platform == "win32" else 0
|
||||
NUL = chr(0)
|
||||
# Die Ausgabe von scripts/ibkr_pruefen.py lesen, gemessen am 2026-10-07 gegen 22:56 (Anforderung).
|
||||
IBKR_POSITIONEN = re.compile(r"^Positionen: (\d+)\s*$")
|
||||
IBKR_ORDERS = re.compile(r"^Offene Orders: (\d+)\s*$")
|
||||
IBKR_ZEILE = re.compile(r"^ (\S+) (-?\d+)(?:\.0+)?\s*$")
|
||||
IBKR_GELESEN = "ERGEBNIS: gelesen"
|
||||
TITEL = "Abbau Restbestand"
|
||||
KOPF = "Abbau Restbestand (automatisch, Auftrag Pascal 2026-10-07)"
|
||||
GESPERRT_HINWEIS = "Der Killswitch jeder angefassten Instanz steht danach auf gesperrt; Freigeben bleibt Pascal."
|
||||
|
||||
# Die Angriffsfaelle ersetzen schlafe und starte, damit kein Fall wartet und keiner einen Prozess startet.
|
||||
schlafe = time.sleep
|
||||
starte = subprocess.run
|
||||
|
||||
|
||||
def sauber(text: str) -> str:
|
||||
"""Text ohne NUL: wsl.exe schreibt eigene Fehler in UTF-16LE, und ein NUL im Argument laesst subprocess scheitern."""
|
||||
return text.replace(NUL, "")
|
||||
|
||||
|
||||
def einzeilig(text: str) -> str:
|
||||
"""Ohne NUL und ohne Zeilenumbruch: Ein fremder Text bleibt eine Zeile in Ausgabe, Log, Notiz und Meldung."""
|
||||
return sauber(text).replace("\r", " ").replace("\n", " ")
|
||||
|
||||
|
||||
def wsl(argumente: list[str], timeout: int) -> tuple[int, str, str]:
|
||||
"""Ein Aufruf in WSL ohne Shell (--exec): Exit, stdout, stderr. Der einzige Weg nach draussen."""
|
||||
lauf = starte(
|
||||
[WSL, "-d", "Debian", "-u", "claude", "--exec", *argumente],
|
||||
capture_output=True, timeout=timeout, creationflags=FENSTERLOS,
|
||||
env={**os.environ, "WSL_UTF8": "1"},
|
||||
)
|
||||
return (
|
||||
lauf.returncode,
|
||||
sauber(lauf.stdout.decode("utf-8", errors="replace")),
|
||||
sauber(lauf.stderr.decode("utf-8", errors="replace")),
|
||||
)
|
||||
|
||||
|
||||
def lies_runner(instanz: str) -> str:
|
||||
"""LoadState und ActiveState der Runner-Unit (systemctl show); liest nur."""
|
||||
_, ausgabe, _ = wsl(["systemctl", "show", "--property=LoadState,ActiveState", UNITS[instanz]], RUNNER_TIMEOUT_S)
|
||||
return ausgabe
|
||||
|
||||
|
||||
def runner_felder(ausgabe: str) -> tuple[str, str]:
|
||||
"""LoadState und ActiveState aus der Ausgabe von systemctl show; was fehlt, ist leer."""
|
||||
felder = {}
|
||||
for zeile in ausgabe.splitlines():
|
||||
name, _, wert = zeile.partition("=")
|
||||
felder[name.strip()] = wert.strip()
|
||||
return felder.get("LoadState", ""), felder.get("ActiveState", "")
|
||||
|
||||
|
||||
def runner_urteil(laden: str, aktiv: str) -> str | None:
|
||||
"""laeuft, steht, oder None (fail-closed). steht nur bei einer geladenen Unit, die inactive oder failed ist."""
|
||||
if aktiv in RUNNER_LAEUFT:
|
||||
return "laeuft"
|
||||
if laden == "loaded" and aktiv in RUNNER_STEHT:
|
||||
return "steht"
|
||||
return None
|
||||
|
||||
|
||||
def lies_alpaca() -> dict:
|
||||
"""Die JSON-Zeile von abbau_alpaca_lesen.py; ohne sie steht der Grund in beiden Fehlerfeldern."""
|
||||
try:
|
||||
code, ausgabe, _ = wsl([ALPACA_PY, ALPACA_LESEN, "--projekt", ORDNER_WSL["alpaca"]], LESEN_TIMEOUT_S)
|
||||
except Exception as ausnahme:
|
||||
code, ausgabe = f"nicht gestartet, {type(ausnahme).__name__}", ""
|
||||
for zeile in reversed(ausgabe.splitlines()):
|
||||
zeile = zeile.strip()
|
||||
if not zeile.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
daten = json.loads(zeile)
|
||||
except ValueError:
|
||||
break
|
||||
if isinstance(daten, dict):
|
||||
return daten
|
||||
break
|
||||
grund = f"abbau_alpaca_lesen.py ohne JSON-Zeile (Exit {code})"
|
||||
return {"uhr_fehler": grund, "bestand_fehler": grund}
|
||||
|
||||
|
||||
def uhr_lesbar(uhr) -> bool:
|
||||
"""Die Marktuhr in der Form von abbau_alpaca_lesen.py: offen als bool, Sekunden als ganze Zahl."""
|
||||
return (
|
||||
isinstance(uhr, dict)
|
||||
and type(uhr.get("offen")) is bool
|
||||
and type(uhr.get("sekunden_bis_schluss")) is int
|
||||
)
|
||||
|
||||
|
||||
def alpaca_bestand(daten: dict) -> dict | None:
|
||||
"""Symbol -> Stueck und die Zahl offener Orders aus der JSON-Zeile, oder None, wenn nicht gelesen."""
|
||||
positionen, orders = daten.get("positionen"), daten.get("orders")
|
||||
if not isinstance(positionen, list) or type(orders) is not int:
|
||||
return None
|
||||
stueck: dict[str, int] = {}
|
||||
for eintrag in positionen:
|
||||
if not isinstance(eintrag, dict) or not isinstance(eintrag.get("symbol"), str):
|
||||
return None
|
||||
if type(eintrag.get("menge")) is not int:
|
||||
return None
|
||||
stueck[eintrag["symbol"]] = stueck.get(eintrag["symbol"], 0) + eintrag["menge"]
|
||||
return {"positionen": stueck, "orders": orders}
|
||||
|
||||
|
||||
def ibkr_bestand(code: int, ausgabe: str) -> dict | None:
|
||||
"""Positionen und offene Orders aus stdout von scripts/ibkr_pruefen.py lesen, oder None.
|
||||
|
||||
Gelesen ist der Bestand nur mit Exit 0 oder 1 (1: Befund zur Kontogroesse, die Liste steht
|
||||
trotzdem da), mit der Zeile "ERGEBNIS: gelesen", beiden Zahlenzeilen und genau so vielen
|
||||
Positionszeilen, wie "Positionen: n" sagt.
|
||||
"""
|
||||
zeilen = ausgabe.splitlines()
|
||||
if not any(zeile.startswith(IBKR_GELESEN) for zeile in zeilen):
|
||||
return None
|
||||
if code not in (0, 1):
|
||||
return None
|
||||
anzahl = orders = None
|
||||
gezaehlt = 0
|
||||
stueck: dict[str, int] = {}
|
||||
im_block = False
|
||||
for zeile in zeilen:
|
||||
treffer = IBKR_POSITIONEN.match(zeile)
|
||||
if treffer:
|
||||
anzahl, im_block = int(treffer.group(1)), True
|
||||
continue
|
||||
treffer = IBKR_ORDERS.match(zeile)
|
||||
if treffer:
|
||||
orders, im_block = int(treffer.group(1)), False
|
||||
continue
|
||||
if im_block:
|
||||
treffer = IBKR_ZEILE.match(zeile)
|
||||
if treffer is None:
|
||||
return None
|
||||
symbol = treffer.group(1).split(".")[0]
|
||||
stueck[symbol] = stueck.get(symbol, 0) + int(treffer.group(2))
|
||||
gezaehlt += 1
|
||||
if anzahl is None or orders is None or gezaehlt != anzahl:
|
||||
return None
|
||||
return {"positionen": stueck, "orders": orders}
|
||||
|
||||
|
||||
def lies_ibkr() -> tuple[dict | None, int | None]:
|
||||
"""Bestand der IBKR-Instanz ueber abbau_ibkr_lesen.sh (ibkr_pruefen.py lesen) und dessen Exit."""
|
||||
try:
|
||||
code, ausgabe, _ = wsl(["bash", IBKR_LESEN, ORDNER_WSL["ibkr"]], LESEN_TIMEOUT_S)
|
||||
except Exception:
|
||||
return None, None
|
||||
return ibkr_bestand(code, ausgabe), code
|
||||
|
||||
|
||||
def hat_bestand(bestand: dict | None) -> bool:
|
||||
"""Position ungleich null oder offene Order. Ein nicht gelesener Bestand (None) hat keinen: fail-closed."""
|
||||
return bestand is not None and (any(menge != 0 for menge in bestand["positionen"].values()) or bestand["orders"] > 0)
|
||||
|
||||
|
||||
def ist_flach(bestand: dict | None) -> bool:
|
||||
"""Gelesen, keine Position ungleich null und keine offene Order."""
|
||||
return bestand is not None and not any(menge != 0 for menge in bestand["positionen"].values()) and bestand["orders"] == 0
|
||||
|
||||
|
||||
def flach_markiert(datei: Path) -> bool:
|
||||
"""Ob eine fruehere Ergebnisdatei "flach": true traegt; unlesbar oder anders heisst nein."""
|
||||
try:
|
||||
daten = json.loads(datei.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return isinstance(daten, dict) and daten.get("flach") is True
|
||||
|
||||
|
||||
def beschreibe(bestand: dict | None) -> str:
|
||||
"""Ein Bestand fuer Ausgabe, Notiz und Meldung: Symbol, Richtung, Stueck, offene Orders. Nie ein Betrag."""
|
||||
if bestand is None:
|
||||
return "nicht gelesen"
|
||||
teile = [
|
||||
f"{symbol} {'long' if menge > 0 else 'short'} {abs(menge)}"
|
||||
for symbol, menge in sorted(bestand["positionen"].items())
|
||||
if menge != 0
|
||||
]
|
||||
liste = ": " + ", ".join(teile) if teile else ""
|
||||
return f"{len(teile)} Position(en){liste}; offene Orders {bestand['orders']}"
|
||||
|
||||
|
||||
def glattstelle(instanz: str, modus: str) -> tuple[int, list[str]]:
|
||||
"""Genau ein Aufruf von abbau_glattstellen.sh fuer eine Instanz; nur im Modus scharf.
|
||||
|
||||
Die Zeilen kommen aus WSL schon gefiltert (Wort, Kontonummern, Betraege); hier werden sie nur gekuerzt.
|
||||
"""
|
||||
if modus != SCHARF:
|
||||
raise RuntimeError("glattstellen nur im Modus scharf")
|
||||
code, ausgabe, fehler = wsl(["bash", GLATTSTELLEN, instanz, ORDNER_WSL[instanz]], GLATT_TIMEOUT_S)
|
||||
zeilen = [zeile.strip()[:ZEILE_MAX] for zeile in (ausgabe + "\n" + fehler).splitlines() if zeile.strip()]
|
||||
return code, zeilen[-ZEILEN_MAX:]
|
||||
|
||||
|
||||
def melde(titel: str, text: str, modus: str) -> str:
|
||||
"""Eine Meldung mit hoher Prioritaet ueber abendkontrolle_wsl.py --meldung; nur im Modus scharf."""
|
||||
if modus != SCHARF:
|
||||
return "nicht gesendet: Modus lesen"
|
||||
titel, text = sauber(titel), sauber(text)
|
||||
try:
|
||||
code, ausgabe, _ = wsl([WSL_PY, WSL_SKRIPT, "--meldung", titel, text], MELDUNG_TIMEOUT_S)
|
||||
except Exception as ausnahme:
|
||||
return f"nicht gesendet: {type(ausnahme).__name__}"
|
||||
for zeile in reversed(ausgabe.splitlines()):
|
||||
zeile = zeile.strip()
|
||||
if zeile.startswith("{"):
|
||||
try:
|
||||
return str(json.loads(zeile).get("meldung"))[:200]
|
||||
except (ValueError, AttributeError):
|
||||
break
|
||||
return f"nicht gesendet: keine Antwort (Exit {code})"
|
||||
|
||||
|
||||
def logge(ablage: Path, text: str) -> None:
|
||||
"""Eine Zeile in abbau.log; ein Fehler dabei haelt den Lauf nicht auf."""
|
||||
try:
|
||||
ablage.mkdir(parents=True, exist_ok=True)
|
||||
with (ablage / LOG_NAME).open("a", encoding="utf-8", newline="\n") as datei:
|
||||
datei.write(f"{datetime.now().isoformat(timespec='seconds')} {text}\n")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def schreibe_ergebnis(ablage: Path, daten: dict) -> None:
|
||||
"""abbau.json ueber eine Nachbardatei und os.replace: nie eine halbe Datei."""
|
||||
ablage.mkdir(parents=True, exist_ok=True)
|
||||
neu = ablage / (ERGEBNIS_NAME + ".tmp")
|
||||
neu.write_text(json.dumps(daten, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n")
|
||||
os.replace(neu, ablage / ERGEBNIS_NAME)
|
||||
|
||||
|
||||
class Lauf:
|
||||
"""Was ein Lauf gesehen und getan hat; daraus entstehen Ausgabe, Ergebnisdatei, Notiz und Meldung."""
|
||||
|
||||
def __init__(self, jetzt: datetime, modus: str, ablage: Path) -> None:
|
||||
self.jetzt = jetzt
|
||||
self.modus = modus
|
||||
self.ablage = ablage
|
||||
self.ausgabe: list[str] = []
|
||||
self.befunde: list[str] = []
|
||||
self.gesperrt: set[str] = set()
|
||||
self.markt = ""
|
||||
self.vorher: dict = {}
|
||||
self.nachher: dict = {}
|
||||
self.eingriffe: dict = {}
|
||||
self.spur = ""
|
||||
|
||||
def sage(self, text: str) -> None:
|
||||
self.ausgabe.append(einzeilig(text))
|
||||
|
||||
def befund(self, text: str) -> None:
|
||||
self.befunde.append(einzeilig(text)[:300])
|
||||
|
||||
def zusammenfassung(self) -> list[str]:
|
||||
"""Je Instanz eine Zeile: vorher, was geschah, nachher."""
|
||||
zeilen = []
|
||||
for instanz in INSTANZEN:
|
||||
eingriff = self.eingriffe.get(instanz)
|
||||
tat = "nicht angefasst" if eingriff is None else f"glattstellen Exit {eingriff['exit']}"
|
||||
zeilen.append(
|
||||
f"{instanz}: vorher {beschreibe(self.vorher.get(instanz))}; {tat}; "
|
||||
f"nachher {beschreibe(self.nachher.get(instanz))}"
|
||||
)
|
||||
return zeilen
|
||||
|
||||
|
||||
def fahre(lauf: Lauf) -> str:
|
||||
"""Schritte 1 bis 6; gibt NICHTS, STILL, LESEN_ENDE oder BERICHT zurueck."""
|
||||
scharf = lauf.modus == SCHARF
|
||||
if not scharf:
|
||||
lauf.sage("Modus lesen: kein Glattstellen, keine Meldung, keine Notiz, keine Datei")
|
||||
# 1 Fenster und Ergebnisdatei
|
||||
im_fenster = FRUEHESTENS <= lauf.jetzt <= SPAETESTENS
|
||||
lage = "innerhalb" if im_fenster else "ausserhalb"
|
||||
lauf.sage(f"Fenster {FRUEHESTENS:%Y-%m-%d %H:%M} bis {SPAETESTENS:%Y-%m-%d %H:%M}: {lage}")
|
||||
if scharf and not im_fenster:
|
||||
return NICHTS
|
||||
if flach_markiert(lauf.ablage / ERGEBNIS_NAME):
|
||||
lauf.sage("Ergebnisdatei sagt flach")
|
||||
if scharf:
|
||||
return STILL
|
||||
|
||||
# 2 Runner-Units: ein laufender, nicht geladener oder unlesbarer Runner sperrt seine Instanz
|
||||
for instanz in INSTANZEN:
|
||||
try:
|
||||
laden, aktiv = runner_felder(lies_runner(instanz))
|
||||
except Exception as ausnahme:
|
||||
laden, aktiv = "", f"nicht lesbar, {type(ausnahme).__name__}"
|
||||
zustand = f"LoadState={laden or 'leer'}, ActiveState={aktiv or 'leer'}"
|
||||
lauf.sage(f"Runner {instanz} ({UNITS[instanz]}): {zustand}")
|
||||
urteil = runner_urteil(laden, aktiv)
|
||||
if urteil != "steht":
|
||||
lauf.gesperrt.add(instanz)
|
||||
grund = "laeuft" if urteil == "laeuft" else "Zustand nicht lesbar"
|
||||
lauf.befund(f"{instanz}: Runner-Unit {UNITS[instanz]} {grund} ({zustand[:80]}); nicht angefasst")
|
||||
|
||||
# 3 Marktuhr: zu oder bald zu heisst im Modus scharf still zu Ende
|
||||
alpaca = lies_alpaca()
|
||||
uhr = alpaca.get("uhr")
|
||||
if not uhr_lesbar(uhr):
|
||||
lauf.gesperrt.update(INSTANZEN)
|
||||
lauf.markt = "Marktuhr nicht lesbar"
|
||||
lauf.befund(f"Marktuhr nicht lesbar ({alpaca.get('uhr_fehler') or 'ohne Angabe'}); nichts angefasst")
|
||||
else:
|
||||
if uhr["offen"]:
|
||||
lauf.sage(f"Markt offen, {uhr['sekunden_bis_schluss'] // 60} min bis Schluss")
|
||||
else:
|
||||
lauf.sage("Markt zu")
|
||||
handelbar = uhr["offen"] and uhr["sekunden_bis_schluss"] >= MIN_BIS_SCHLUSS_S
|
||||
if not handelbar:
|
||||
lauf.markt = "Markt zu oder weniger als 10 min bis Schluss"
|
||||
if scharf:
|
||||
return STILL
|
||||
|
||||
# 4 Bestaende: nicht lesbar heisst Befund; hat_bestand(None) ist falsch, die Instanz bleibt unberuehrt
|
||||
lauf.vorher["alpaca"] = alpaca_bestand(alpaca)
|
||||
if lauf.vorher["alpaca"] is None:
|
||||
lauf.befund(f"alpaca: Bestand nicht lesbar ({alpaca.get('bestand_fehler') or 'ohne Angabe'}); nicht angefasst")
|
||||
lauf.vorher["ibkr"], ibkr_exit = lies_ibkr()
|
||||
if lauf.vorher["ibkr"] is None:
|
||||
lauf.befund(f"ibkr: Bestand nicht lesbar (ibkr_pruefen.py lesen, Exit {ibkr_exit}); nicht angefasst")
|
||||
for instanz in INSTANZEN:
|
||||
lauf.sage(f"Bestand {instanz}: {beschreibe(lauf.vorher[instanz])}")
|
||||
|
||||
# 5 Je Instanz hoechstens ein Eingriff, nur im Modus scharf; wirft er, gilt Exit None
|
||||
zu_glatt = [instanz for instanz in INSTANZEN if instanz not in lauf.gesperrt and hat_bestand(lauf.vorher[instanz])]
|
||||
if lauf.modus == LESEN:
|
||||
entscheidung = f"keine ({lauf.markt})" if lauf.markt else (", ".join(zu_glatt) or "keine")
|
||||
lauf.sage(f"Glattstellen im Modus scharf: {entscheidung}")
|
||||
return LESEN_ENDE
|
||||
for instanz in zu_glatt:
|
||||
try:
|
||||
ergebnis_code, zeilen = glattstelle(instanz, lauf.modus)
|
||||
except Exception as ausnahme:
|
||||
ergebnis_code, zeilen = None, [f"abgebrochen: {type(ausnahme).__name__}"]
|
||||
lauf.eingriffe[instanz] = {"exit": ergebnis_code, "zeilen": zeilen}
|
||||
if ergebnis_code != 0:
|
||||
lauf.befund(f"{instanz}: glattstellen endete mit Exit {ergebnis_code}")
|
||||
|
||||
# 6 Nach einem Eingriff warten und beide Bestaende neu lesen
|
||||
if lauf.eingriffe:
|
||||
schlafe(WARTEN_S)
|
||||
lauf.nachher["alpaca"] = alpaca_bestand(lies_alpaca())
|
||||
lauf.nachher["ibkr"], _ = lies_ibkr()
|
||||
for instanz in INSTANZEN:
|
||||
if lauf.nachher[instanz] is None:
|
||||
lauf.befund(f"{instanz}: Bestand nachher nicht lesbar; Ergebnis offen")
|
||||
else:
|
||||
lauf.nachher = dict(lauf.vorher)
|
||||
return BERICHT
|
||||
|
||||
|
||||
def ergebnis_daten(lauf: Lauf, flach: bool) -> dict:
|
||||
"""Inhalt von abbau.json: je Instanz vorher, nachher und das Glattstellen; dazu flach und die Befunde."""
|
||||
return {
|
||||
"zeit": lauf.jetzt.isoformat(timespec="seconds"),
|
||||
"flach": flach,
|
||||
"befunde": lauf.befunde,
|
||||
"instanzen": {
|
||||
instanz: {
|
||||
"vorher": lauf.vorher.get(instanz),
|
||||
"nachher": lauf.nachher.get(instanz),
|
||||
"glattstellen": lauf.eingriffe.get(instanz),
|
||||
}
|
||||
for instanz in INSTANZEN
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def titel_fuer(lauf: Lauf, flach: bool) -> str:
|
||||
"""Rangfolge: flach mit Befund, Befund, flach, Rest offen."""
|
||||
if flach and lauf.befunde:
|
||||
return f"{TITEL}: flach, mit Befund"
|
||||
if lauf.befunde:
|
||||
return f"{TITEL}: Befund"
|
||||
if flach:
|
||||
return f"{TITEL}: flach"
|
||||
return f"{TITEL}: Rest offen"
|
||||
|
||||
|
||||
def notizzeile(lauf: Lauf, flach: bool, gemeldet: str) -> str:
|
||||
"""Die Zeile unter "## Agentenprotokoll", mit Unterpunkten. Nur Symbol, Richtung, Stueck, Zahlen."""
|
||||
if flach and lauf.befunde:
|
||||
urteil = "flach, mit Befund #needs-review"
|
||||
elif lauf.befunde:
|
||||
urteil = "**Befund** #needs-review"
|
||||
elif flach:
|
||||
urteil = "flach"
|
||||
else:
|
||||
urteil = "Rest offen"
|
||||
zeilen = [f"- {lauf.jetzt:%H:%M} Uhr - {KOPF}: {urteil}."]
|
||||
zeilen += [f" - {teil}" for teil in lauf.zusammenfassung()]
|
||||
for instanz, eingriff in lauf.eingriffe.items():
|
||||
if eingriff["exit"] != 0:
|
||||
zeilen += [f" - {instanz}, killswitch.py: {text}" for text in eingriff["zeilen"][-5:]]
|
||||
zeilen += [f" - Befund: {befund}" for befund in lauf.befunde]
|
||||
if lauf.eingriffe:
|
||||
zeilen.append(f" - {GESPERRT_HINWEIS}")
|
||||
zeilen.append(f" - Meldung ans Telefon: {gemeldet}")
|
||||
return "\n".join(zeilen)
|
||||
|
||||
|
||||
def berichte(lauf: Lauf, vault: Path) -> int:
|
||||
"""Schritte 7 und 8: Ergebnisdatei und Log; Meldung und Notizzeile bei Eingriff, Befund oder flach."""
|
||||
# flach nur ohne gesperrte Instanz: Ein laufender oder unlesbarer Runner oder eine unlesbare
|
||||
# Marktuhr kann den Bestand gleich wieder aendern; der naechste Termin beobachtet weiter.
|
||||
flach = not lauf.gesperrt and all(ist_flach(lauf.nachher.get(instanz)) for instanz in INSTANZEN)
|
||||
try:
|
||||
schreibe_ergebnis(lauf.ablage, ergebnis_daten(lauf, flach))
|
||||
except OSError as ausnahme:
|
||||
lauf.befund(f"Ergebnisdatei nicht geschrieben ({type(ausnahme).__name__})")
|
||||
logge(lauf.ablage, f"scharf: {' | '.join(lauf.ausgabe + lauf.zusammenfassung() + lauf.befunde)} | flach={flach}")
|
||||
if lauf.spur:
|
||||
logge(lauf.ablage, lauf.spur)
|
||||
code = 1 if lauf.befunde else 0
|
||||
# Still nur ohne Eingriff, ohne Befund und ohne flach. Das erste flach ist die Entwarnung;
|
||||
# Laeufe nach der Marke enden schon in fahre().
|
||||
if not lauf.eingriffe and not lauf.befunde and not flach:
|
||||
return code
|
||||
titel = titel_fuer(lauf, flach)
|
||||
text = "; ".join(lauf.zusammenfassung() + [f"Befund: {befund}" for befund in lauf.befunde])
|
||||
gemeldet = melde(titel, text[:1500].replace('"', "'"), lauf.modus)
|
||||
try:
|
||||
schreibe_notiz(vault, lauf.jetzt.date().isoformat(), notizzeile(lauf, flach, gemeldet))
|
||||
except Exception as ausnahme:
|
||||
code = 1
|
||||
logge(lauf.ablage, f"Notiz nicht geschrieben: {type(ausnahme).__name__}")
|
||||
return code
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
zerleger = argparse.ArgumentParser(description=__doc__.splitlines()[0], allow_abbrev=False)
|
||||
zerleger.add_argument(
|
||||
"--modus", choices=MODI, required=True,
|
||||
help="lesen: nur lesende Aufrufe, schreibt nichts; scharf: alles, nur ueber die Windows-Aufgabe",
|
||||
)
|
||||
zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt statt der Uhr (Angriffsfaelle)")
|
||||
zerleger.add_argument("--projekt", default=str(PROJEKT), help="Projektordner unter Windows, fuer die Ablage")
|
||||
zerleger.add_argument("--vault", default=str(VAULT))
|
||||
argumente = zerleger.parse_args(argv)
|
||||
try:
|
||||
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
|
||||
except ValueError:
|
||||
zerleger.error("--jetzt ist kein ISO-Zeitpunkt")
|
||||
lauf = Lauf(jetzt, argumente.modus, Path(argumente.projekt) / ABLAGE_TEIL)
|
||||
try:
|
||||
ende = fahre(lauf)
|
||||
except Exception as ausnahme:
|
||||
# Nur der Typ: Der Text einer fremden Ausnahme kann Kontonummer oder Betrag tragen.
|
||||
# Der Traceback geht nur nach abbau.log (data/, nicht veroeffentlicht).
|
||||
lauf.befund(f"abgebrochen: {type(ausnahme).__name__}")
|
||||
lauf.spur = traceback.format_exc()
|
||||
ende = BERICHT if lauf.modus == SCHARF else LESEN_ENDE
|
||||
if ende == NICHTS:
|
||||
return 0
|
||||
if ende == STILL:
|
||||
logge(lauf.ablage, f"scharf, nichts angefasst: {' | '.join(lauf.ausgabe)}")
|
||||
return 0
|
||||
if ende == LESEN_ENDE:
|
||||
for zeile in lauf.ausgabe:
|
||||
print(zeile)
|
||||
for befund in lauf.befunde:
|
||||
print(f"BEFUND: {befund}")
|
||||
print(f"ERGEBNIS: gelesen, {'mit' if lauf.befunde else 'ohne'} Befund")
|
||||
return 1 if lauf.befunde else 0
|
||||
return berichte(lauf, Path(argumente.vault))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user