- .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/bremse_echtlauf.py - .vault-tools/trading-plattform-pruefung/browser_w.py - .vault-tools/trading-plattform-pruefung/generalprobe_echtlauf.py - .vault-tools/trading-plattform-pruefung/ha_aktiv_setzen.py - .vault-tools/trading-plattform-pruefung/handelstag_auswertung.py - .vault-tools/trading-plattform-pruefung/hausmelder_echtlauf.py - .vault-tools/trading-plattform-pruefung/killswitch_simulation.py - .vault-tools/trading-plattform-pruefung/killswitch_simulation_mutanten.py - .vault-tools/trading-plattform-pruefung/latenz_auswertung.py - .vault-tools/trading-plattform-pruefung/s1_alpaca_lesen.py - .vault-tools/trading-plattform-pruefung/s5_echtlauf.py - … und 2 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
225 lines
11 KiB
Python
225 lines
11 KiB
Python
"""Eigener Lauf des Hausmelders gegen einen mitschreibenden Server auf 127.0.0.1 -- und wahlweise ein POST ans echte HA.
|
|
|
|
Koordinator-Pruefcode, nicht Teil des Repos. Teil 1 (immer): echter Hausmelder-Faden, echter requests-POST,
|
|
lokaler HTTP-Server, gestellte Statusdatei, Wegwerf-Register. Teil 2 (nur mit --echt <secrets.toml>): ein
|
|
einziger pruefe()-Durchlauf mit unterscheidbaren Werten an die Adresse aus der Geheimnisdatei; die Adresse
|
|
wird nie ausgegeben. Rueckgelesen wird danach unter Windows mit ha_einrichten.py.
|
|
|
|
Aufruf (WSL): python hausmelder_echtlauf.py <code-baum> <wegwerf-ordner> [--echt <secrets.toml>]
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from datetime import UTC
|
|
from datetime import datetime
|
|
from http.server import BaseHTTPRequestHandler
|
|
from http.server import ThreadingHTTPServer
|
|
from pathlib import Path
|
|
|
|
CODE = Path(sys.argv[1]).resolve()
|
|
WEG = Path(sys.argv[2]).resolve()
|
|
ECHT_SECRETS = Path(sys.argv[4]).resolve() if len(sys.argv) > 4 and sys.argv[3] == "--echt" else None
|
|
|
|
os.chdir(CODE)
|
|
sys.path.insert(0, str(CODE))
|
|
|
|
from trading.dashboard import hausmelder as modul # noqa: E402
|
|
from trading.dashboard.hausmelder import Hausmelder # noqa: E402
|
|
from trading.dashboard.hausmelder import killswitch_leser # noqa: E402
|
|
from trading.dashboard.hausmelder import lies_hausmelder_config # noqa: E402
|
|
from trading.dashboard.hausmelder import starte_hausmelder # noqa: E402
|
|
from trading.paper.status import schreibe_status # noqa: E402
|
|
from trading.registry.registry import Registry # noqa: E402
|
|
|
|
EMPFANGEN = []
|
|
ANTWORT = {"code": 200, "location": None}
|
|
|
|
|
|
class Mitschreiber(BaseHTTPRequestHandler):
|
|
def do_POST(self):
|
|
laenge = int(self.headers.get("Content-Length", "0"))
|
|
rumpf = self.rfile.read(laenge)
|
|
EMPFANGEN.append({"pfad": self.path, "typ": self.headers.get("Content-Type"),
|
|
"daten": json.loads(rumpf.decode("utf-8")), "antwort": ANTWORT["code"]})
|
|
self.send_response(ANTWORT["code"])
|
|
if ANTWORT["location"]:
|
|
self.send_header("Location", ANTWORT["location"])
|
|
self.send_header("Content-Length", "0")
|
|
self.end_headers()
|
|
|
|
def do_GET(self):
|
|
EMPFANGEN.append({"pfad": self.path, "typ": "GET", "daten": None, "antwort": 200})
|
|
self.send_response(200)
|
|
self.send_header("Content-Length", "0")
|
|
self.end_headers()
|
|
|
|
def log_message(self, *a):
|
|
pass
|
|
|
|
|
|
def status(pfad, **felder):
|
|
grund = {"zeit": datetime.now(UTC).isoformat(), "modus": "paper", "phase": "handelt", "position": 0,
|
|
"kontowert": 100000.0, "startkapital": 100000.0}
|
|
grund.update(felder)
|
|
schreibe_status(pfad, grund)
|
|
|
|
|
|
def warte_bis(bedingung, sekunden=5.0):
|
|
ende = time.monotonic() + sekunden
|
|
while time.monotonic() < ende:
|
|
if bedingung():
|
|
return True
|
|
time.sleep(0.05)
|
|
return False
|
|
|
|
|
|
def main():
|
|
WEG.mkdir(parents=True, exist_ok=False)
|
|
befunde = []
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Mitschreiber)
|
|
port = server.server_address[1]
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
kennung = "nachtlauf-" + "a" * 40
|
|
url = f"http://127.0.0.1:{port}/api/webhook/{kennung}"
|
|
|
|
registry = Registry(db_pfad=WEG / "registry.db", artefakt_wurzel=WEG / "artifacts")
|
|
registry.setze_killswitch_zustand("angehalten", ausloeser="neustart")
|
|
statusdatei = WEG / "paper" / "status.json"
|
|
statusdatei.parent.mkdir(parents=True)
|
|
|
|
# Teil 1a: der Faden selbst, mit kurzem Takt.
|
|
melder = Hausmelder(webhook_url=url, status_datei=statusdatei, killswitch=killswitch_leser(registry),
|
|
takt_s=0.1, lebenszeichen_s=1.5)
|
|
melder.starte()
|
|
if not warte_bis(lambda: len(EMPFANGEN) == 1):
|
|
befunde.append("kein erster POST ohne Statusdatei")
|
|
erster = EMPFANGEN[0]["daten"] if EMPFANGEN else {}
|
|
print("1. ohne Statusdatei:", json.dumps(erster, ensure_ascii=False))
|
|
if erster.get("phase") != "kein_status" or erster.get("killswitch") != "angehalten" or erster.get("herzschlag_alter_s") != -1:
|
|
befunde.append("Rueckfall ohne Statusdatei stimmt nicht")
|
|
if EMPFANGEN and (EMPFANGEN[0]["pfad"] != f"/api/webhook/{kennung}" or EMPFANGEN[0]["typ"] != "application/json"):
|
|
befunde.append("Pfad oder Content-Type falsch")
|
|
|
|
status(statusdatei, phase="handelt", position=20, kontowert=100012.345)
|
|
warte_bis(lambda: len(EMPFANGEN) == 2)
|
|
zweiter = EMPFANGEN[-1]["daten"]
|
|
print("2. nach Statusaenderung:", json.dumps(zweiter, ensure_ascii=False))
|
|
if zweiter.get("position") != 20 or zweiter.get("tagesergebnis_usd") != 12.35 or zweiter.get("phase") != "handelt":
|
|
befunde.append("Statuswerte kommen nicht an")
|
|
|
|
# Ohne Aenderung: In 0,8 s (acht Takte) darf nichts kommen; der Herzschlag wird dabei aelter.
|
|
stand = len(EMPFANGEN)
|
|
time.sleep(0.8)
|
|
if len(EMPFANGEN) != stand:
|
|
befunde.append(f"ohne Aenderung kamen {len(EMPFANGEN) - stand} POST(s)")
|
|
# Lebenszeichen nach 1,5 s.
|
|
if not warte_bis(lambda: len(EMPFANGEN) == stand + 1, 3.0):
|
|
befunde.append("kein Lebenszeichen")
|
|
print(f"3. ohne Aenderung still, Lebenszeichen nach der Frist: {len(EMPFANGEN)} POSTs bisher")
|
|
|
|
# Killswitch aus dem Register, ohne dass die Statusdatei sich aendert.
|
|
registry.setze_killswitch_zustand("gesperrt", ausloeser="pascal")
|
|
stand = len(EMPFANGEN)
|
|
warte_bis(lambda: len(EMPFANGEN) == stand + 1)
|
|
print("4. Killswitch:", EMPFANGEN[-1]["daten"].get("killswitch"))
|
|
if EMPFANGEN[-1]["daten"].get("killswitch") != "gesperrt":
|
|
befunde.append("Killswitch-Wechsel kommt nicht an")
|
|
|
|
# Fehlschlag: 500 kippt den Merker nicht, es wird wiederholt; danach 200.
|
|
ANTWORT["code"] = 500
|
|
status(statusdatei, phase="pausiert", position=0, kontowert=99990.0)
|
|
stand = len(EMPFANGEN)
|
|
warte_bis(lambda: len(EMPFANGEN) >= stand + 3)
|
|
wiederholt = len(EMPFANGEN) - stand
|
|
ANTWORT["code"] = 200
|
|
warte_bis(lambda: EMPFANGEN[-1]["antwort"] == 200)
|
|
stand = len(EMPFANGEN)
|
|
time.sleep(0.6)
|
|
print(f"5. bei HTTP 500 wiederholt: {wiederholt} Versuche; nach 200 still: {len(EMPFANGEN) - stand} weitere; "
|
|
f"Fehlschlaege: {sorted(set(melder.fehlschlaege))}")
|
|
if wiederholt < 3 or len(EMPFANGEN) != stand:
|
|
befunde.append("Wiederholung nach Fehlschlag stimmt nicht")
|
|
if EMPFANGEN[-1]["daten"].get("tagesergebnis_usd") != -10.0:
|
|
befunde.append("negatives Tagesergebnis stimmt nicht")
|
|
|
|
# Weiterleitung: wird nicht gefolgt (kein GET am Server), gilt als Fehlschlag.
|
|
ANTWORT["code"] = 302
|
|
ANTWORT["location"] = f"http://127.0.0.1:{port}/woanders"
|
|
status(statusdatei, phase="handelt", position=10)
|
|
stand = len(EMPFANGEN)
|
|
warte_bis(lambda: len(EMPFANGEN) >= stand + 2)
|
|
gets = [e for e in EMPFANGEN if e["typ"] == "GET" or e["pfad"] == "/woanders"]
|
|
ANTWORT["code"] = 200
|
|
ANTWORT["location"] = None
|
|
warte_bis(lambda: EMPFANGEN[-1]["antwort"] == 200)
|
|
print(f"6. Weiterleitung: gefolgt {len(gets)}-mal; Fehlschlaege: {sorted(set(melder.fehlschlaege))}")
|
|
if gets:
|
|
befunde.append("einer Weiterleitung wurde gefolgt")
|
|
|
|
# Veraltet: Herzschlag 200 s alt, Phase nicht beendet.
|
|
status(statusdatei, phase="handelt", position=10,
|
|
zeit=datetime.fromtimestamp(time.time() - 200, UTC).isoformat())
|
|
stand = len(EMPFANGEN)
|
|
warte_bis(lambda: len(EMPFANGEN) >= stand + 1)
|
|
print("7. veraltet:", EMPFANGEN[-1]["daten"].get("veraltet"), EMPFANGEN[-1]["daten"].get("herzschlag_alter_s"))
|
|
if EMPFANGEN[-1]["daten"].get("veraltet") is not True:
|
|
befunde.append("veraltet wird nicht gemeldet")
|
|
|
|
melder.stoppe()
|
|
if melder.laeuft:
|
|
befunde.append("Faden laeuft nach stoppe() weiter")
|
|
geheim = [text for text in melder.fehlschlaege if kennung in text or "127.0.0.1" in text]
|
|
if geheim or kennung in repr(melder):
|
|
befunde.append("Adresse in Fehlschlag oder repr")
|
|
schluessel = sorted(EMPFANGEN[-1]["daten"])
|
|
print("8. Schluessel der Nutzlast:", schluessel)
|
|
if schluessel != sorted(["phase", "modus", "killswitch", "position", "tagesergebnis_usd",
|
|
"herzschlag_alter_s", "veraltet", "gesendet"]):
|
|
befunde.append("Nutzlast traegt andere Schluessel")
|
|
|
|
# Teil 1b: Verdrahtung ueber die Geheimnisdatei.
|
|
secrets = WEG / "secrets.toml"
|
|
secrets.write_text(f'[homeassistant]\nwebhook_url = "{url}"\naktiv = true\n', encoding="utf-8")
|
|
stand = len(EMPFANGEN)
|
|
zweiter_melder = starte_hausmelder(status_datei=statusdatei, killswitch=killswitch_leser(registry),
|
|
secrets_pfad=secrets)
|
|
ok = zweiter_melder is not None and warte_bis(lambda: len(EMPFANGEN) > stand)
|
|
if zweiter_melder is not None:
|
|
zweiter_melder.stoppe()
|
|
print(f"9. starte_hausmelder mit aktiv = true: Faden {'lief und hat gesendet' if ok else 'FEHLT'}")
|
|
if not ok:
|
|
befunde.append("starte_hausmelder sendet nicht")
|
|
secrets.write_text(f'[homeassistant]\nwebhook_url = "{url}"\naktiv = false\n', encoding="utf-8")
|
|
if starte_hausmelder(status_datei=statusdatei, killswitch=killswitch_leser(registry), secrets_pfad=secrets) is not None:
|
|
befunde.append("aktiv = false startet einen Faden")
|
|
secrets.write_text('[homeassistant]\nwebhook_url = "ftp://falsch"\naktiv = true\n', encoding="utf-8")
|
|
if starte_hausmelder(status_datei=statusdatei, killswitch=killswitch_leser(registry), secrets_pfad=secrets) is not None:
|
|
befunde.append("eine falsche Adresse startet einen Faden")
|
|
server.shutdown()
|
|
|
|
# Teil 2: ein POST ans echte HA, mit Werten, die sich von der neutralen Lage unterscheiden.
|
|
if ECHT_SECRETS is not None:
|
|
config = lies_hausmelder_config(ECHT_SECRETS)
|
|
if config is None or not config.webhook_url:
|
|
befunde.append("echte Geheimnisdatei ohne [homeassistant]")
|
|
else:
|
|
status(statusdatei, modus="generalprobe", phase="handelt", position=7, kontowert=100004.56)
|
|
registry.setze_killswitch_zustand("aktiv", ausloeser="pascal")
|
|
echter = Hausmelder(webhook_url=config.webhook_url, status_datei=statusdatei,
|
|
killswitch=killswitch_leser(registry))
|
|
echter.pruefe()
|
|
print(f"10. echtes HA: zugestellt={echter._zuletzt is not None}, Fehlschlaege={echter.fehlschlaege}, "
|
|
f"Laenge der Adresse {len(config.webhook_url)}")
|
|
if echter._zuletzt is None:
|
|
befunde.append("echtes HA hat nicht mit 200 geantwortet")
|
|
|
|
print(f"{len(befunde)} Befund(e): {befunde}")
|
|
return 2 if befunde else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|