Automatischer Push: 14 Datei(en) geändert
- .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]>
This commit is contained in:
1 parent
246af19683
commit
593acdd122
14 files changed
+2090
-5
No files matched your search
@@ -0,0 +1,343 @@
|
||||
"""Eigener Nachtlauf der Generalprobe: _generalprobe aus scripts/paper.py mit echten Kerzen, ohne Netz.
|
||||
|
||||
Koordinator-Pruefcode, nicht Teil des Repos. Gerufen wird die echte Funktion _generalprobe -- dieselbe
|
||||
Verdrahtung wie um 9:00 Uhr --, umgebogen sind nur die Stellen, die sonst ins Netz oder in den Betrieb
|
||||
gingen:
|
||||
- REGISTER zeigt auf eine Sicherungskopie des echten Registers in einem Wegwerf-Ordner (das echte wird
|
||||
nur lesend geoeffnet), ARTEFAKTE auf den echten artifacts-Ordner (nur lesen: das Registermodell).
|
||||
- Statusdatei und Protokollordner liegen im Wegwerf-Ordner.
|
||||
- AlpacaKerzenquelle.from_secrets liefert die echten IEX-Kerzen des Tages aus einer JSON-Datei.
|
||||
- AlpacaBroker.from_secrets liefert ein mitschreibendes Konto, das glatt ist und bei jedem schreibenden
|
||||
Aufruf wirft -- damit steht im Bericht, was die Generalprobe am "echten" Broker ruft.
|
||||
- requests.Session.request schreibt mit und wirft: Jeder Netzversuch des Laufs faellt auf.
|
||||
- Der Melder ist ein echter Melder mit mitschreibendem Transport, in der Huelle GeneralprobeMelder.
|
||||
"Pascal" klickt aus einem zweiten Faden -- wie das Dashboard mit einem frischen Killswitch-Objekt aus dem
|
||||
Register, am mitschreibenden Konto: fortsetzen, spaeter anhalten und fortsetzen, glattstellen, freigeben.
|
||||
|
||||
Aufruf (WSL): python generalprobe_echtlauf.py <code-baum> <echter-projektordner> <kerzen.json> <wegwerf-ordner> [tempo]
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import replace
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
CODE = Path(sys.argv[1]).resolve()
|
||||
ECHT = Path(sys.argv[2]).resolve()
|
||||
KERZEN = Path(sys.argv[3]).resolve()
|
||||
WEG = Path(sys.argv[4]).resolve()
|
||||
TEMPO = float(sys.argv[5]) if len(sys.argv) > 5 else 1.0
|
||||
|
||||
os.chdir(CODE)
|
||||
sys.path.insert(0, str(CODE))
|
||||
|
||||
import requests # noqa: E402
|
||||
|
||||
from trading.broker.schnittstelle import Marktuhr # noqa: E402
|
||||
from trading.killswitch.killswitch import Killswitch # noqa: E402
|
||||
from trading.killswitch.killswitch import lade_killswitch_config # noqa: E402
|
||||
from trading.paper.config import lade_paper_config # noqa: E402
|
||||
from trading.paper.kerzen import HANDELSZONE # noqa: E402
|
||||
from trading.paper.kerzen import Kerze # noqa: E402
|
||||
from trading.paper.melder import Melder # noqa: E402
|
||||
from trading.paper.melder import MelderConfig # noqa: E402
|
||||
from trading.paper.status import lies_status # noqa: E402
|
||||
from trading.registry.registry import Registry # noqa: E402
|
||||
|
||||
NETZVERSUCHE: list[str] = []
|
||||
|
||||
|
||||
def _kein_netz(self, method, url, *args, **kwargs):
|
||||
NETZVERSUCHE.append(f"{method} {url.split('?')[0]}")
|
||||
raise requests.ConnectionError("Nachtlauf: kein Netz")
|
||||
|
||||
|
||||
requests.sessions.Session.request = _kein_netz
|
||||
|
||||
|
||||
class Antwort:
|
||||
status_code = 200
|
||||
|
||||
|
||||
class Transport:
|
||||
def __init__(self):
|
||||
self.aufrufe = []
|
||||
|
||||
def __call__(self, url, *, data=None, headers=None, timeout=None, **rest):
|
||||
rumpf = data.decode("utf-8") if isinstance(data, bytes) else str(data)
|
||||
self.aufrufe.append(
|
||||
{
|
||||
"titel": headers.get("Title"),
|
||||
"prioritaet": headers.get("Priority"),
|
||||
"erste_zeile": rumpf.split("\n")[0],
|
||||
"echt": datetime.now(UTC).isoformat(timespec="seconds"),
|
||||
}
|
||||
)
|
||||
return Antwort()
|
||||
|
||||
|
||||
class MitschreibendesKonto:
|
||||
"""Steht fuer das echte Alpaca-Paper-Konto: glatt, keine Order, Boerse zu. Schreiben wirft."""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.aufrufe = []
|
||||
|
||||
def positionen(self):
|
||||
self.aufrufe.append("positionen")
|
||||
return []
|
||||
|
||||
def offene_orders(self):
|
||||
self.aufrufe.append("offene_orders")
|
||||
return []
|
||||
|
||||
def marktuhr(self):
|
||||
self.aufrufe.append("marktuhr")
|
||||
jetzt = datetime.now(UTC)
|
||||
return Marktuhr(is_open=False, next_open=jetzt + timedelta(hours=8),
|
||||
next_close=jetzt + timedelta(hours=14))
|
||||
|
||||
def storniere_alle(self):
|
||||
self.aufrufe.append("storniere_alle")
|
||||
return []
|
||||
|
||||
def schliesse_alle(self):
|
||||
self.aufrufe.append("schliesse_alle")
|
||||
return []
|
||||
|
||||
def __getattr__(self, name):
|
||||
def wirft(*a, **k):
|
||||
self.aufrufe.append(f"VERBOTEN:{name}")
|
||||
raise AssertionError(f"{self.name}: unerwarteter Aufruf {name}")
|
||||
return wirft
|
||||
|
||||
|
||||
def lade_kerzen(pfad):
|
||||
roh = json.loads(Path(pfad).read_text(encoding="utf-8"))
|
||||
return [
|
||||
Kerze(zeit=datetime.fromisoformat(e["zeit"]), open=e["open"], high=e["high"],
|
||||
low=e["low"], close=e["close"], volume=e["volume"])
|
||||
for e in roh
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
kerzen = lade_kerzen(KERZEN)
|
||||
tag = kerzen[0].zeit.astimezone(HANDELSZONE).date().isoformat()
|
||||
WEG.mkdir(parents=True, exist_ok=False)
|
||||
|
||||
# Sicherungskopie des echten Registers; das echte wird nur lesend geoeffnet.
|
||||
quelle = sqlite3.connect(f"file:{ECHT / 'data' / 'registry.db'}?mode=ro", uri=True)
|
||||
ziel = sqlite3.connect(WEG / "registry.db")
|
||||
quelle.backup(ziel)
|
||||
quelle.close()
|
||||
# Die Spalte artefakt_pfad ist relativ zum Projektordner. In der Kopie wird sie absolut, damit der
|
||||
# Lauf aus dem Code-Baum heraus das Registermodell im echten artifacts-Ordner findet (nur lesen).
|
||||
for zeile in ziel.execute("SELECT agent, version, artefakt_pfad FROM versionen").fetchall():
|
||||
pfad = Path(zeile[2].replace("\\", "/"))
|
||||
if not pfad.is_absolute():
|
||||
ziel.execute("UPDATE versionen SET artefakt_pfad = ? WHERE agent = ? AND version = ?",
|
||||
(str(ECHT / pfad), zeile[0], zeile[1]))
|
||||
ziel.commit()
|
||||
ziel.close()
|
||||
|
||||
spec = importlib.util.spec_from_file_location("paper_skript_nachtlauf", CODE / "scripts" / "paper.py")
|
||||
paper = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(paper)
|
||||
|
||||
paper.REGISTER = str(WEG / "registry.db")
|
||||
paper.ARTEFAKTE = str(ECHT / "artifacts")
|
||||
|
||||
konto_der_probe = MitschreibendesKonto("Konto der Generalprobe")
|
||||
konto_des_dashboards = MitschreibendesKonto("Konto des Dashboards")
|
||||
|
||||
class Kerzenquelle:
|
||||
@classmethod
|
||||
def from_secrets(cls, *a, **k):
|
||||
return cls()
|
||||
|
||||
def kerzen_des_tages(self, instrument_id, ende):
|
||||
return list(kerzen)
|
||||
|
||||
class BrokerFabrik:
|
||||
@classmethod
|
||||
def from_secrets(cls, *a, **k):
|
||||
return konto_der_probe
|
||||
|
||||
paper.AlpacaKerzenquelle = Kerzenquelle
|
||||
paper.AlpacaBroker = BrokerFabrik
|
||||
|
||||
config = replace(
|
||||
lade_paper_config("config/paper.toml"),
|
||||
status_datei=str(WEG / "paper" / "status.json"),
|
||||
protokoll_ordner=str(WEG / "protokolle"),
|
||||
)
|
||||
ks_config = lade_killswitch_config("config/killswitch.toml")
|
||||
transport = Transport()
|
||||
melder = Melder(
|
||||
config=MelderConfig(aktiv=True, ntfy_url="https://ntfy.sh", thema="nachtlauf-geht-nie-ins-netz",
|
||||
zeitlimit_s=3.0, stille_nach_s=180.0),
|
||||
token="", transport=transport,
|
||||
)
|
||||
|
||||
beobachtet = {"max_alter_s": 0.0, "modi": set(), "marktzeiten": [], "phasen": [], "klicks": [],
|
||||
"rueckwaerts": 0, "position_nach_glatt": None, "fehler": []}
|
||||
halt = threading.Event()
|
||||
|
||||
def dashboard_killswitch():
|
||||
registry = Registry(db_pfad=WEG / "registry.db", artefakt_wurzel=ECHT / "artifacts")
|
||||
gespeichert = registry.killswitch_zustand()
|
||||
return Killswitch(broker=konto_des_dashboards, registry=registry, config=ks_config,
|
||||
zustand=gespeichert.zustand if gespeichert is not None else "angehalten")
|
||||
|
||||
def klick(was, marktzeit, position):
|
||||
ks = dashboard_killswitch()
|
||||
vorher = ks.zustand
|
||||
if was == "glattstellen":
|
||||
ergebnis = ks.glattstellen("pascal", "Nachtlauf", ks_config.bestaetigungswort)
|
||||
else:
|
||||
ergebnis = getattr(ks, was)("pascal", "Nachtlauf")
|
||||
beobachtet["klicks"].append(
|
||||
f"{marktzeit.astimezone(HANDELSZONE):%H:%M} {was}: {vorher} -> {ergebnis.zustand}, "
|
||||
f"erfolgreich={ergebnis.erfolgreich}, gespielte Position vorher {position}"
|
||||
)
|
||||
|
||||
def pascal():
|
||||
plan = [("fortsetzen", (9, 31)), ("anhalten", (10, 30)), ("fortsetzen", (10, 36)),
|
||||
("glattstellen", (12, 0)), ("freigeben", (12, 10))]
|
||||
letzte = None
|
||||
glatt_um = None
|
||||
while not halt.is_set():
|
||||
status = lies_status(config.status_datei)
|
||||
if isinstance(status, dict):
|
||||
try:
|
||||
zeit = datetime.fromisoformat(status["zeit"])
|
||||
markt = datetime.fromisoformat(status["marktzeit"])
|
||||
except (KeyError, ValueError, TypeError) as ausnahme:
|
||||
beobachtet["fehler"].append(repr(ausnahme))
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
if status.get("phase") != "beendet":
|
||||
alter = (datetime.now(UTC) - zeit).total_seconds()
|
||||
beobachtet["max_alter_s"] = max(beobachtet["max_alter_s"], alter)
|
||||
beobachtet["modi"].add(status.get("modus"))
|
||||
if not beobachtet["phasen"] or beobachtet["phasen"][-1] != status.get("phase"):
|
||||
beobachtet["phasen"].append(status.get("phase"))
|
||||
if letzte is not None and markt < letzte:
|
||||
beobachtet["rueckwaerts"] += 1
|
||||
if letzte is None or markt != letzte:
|
||||
beobachtet["marktzeiten"].append(markt)
|
||||
letzte = markt
|
||||
ort = markt.astimezone(HANDELSZONE)
|
||||
if plan and (ort.hour, ort.minute) >= plan[0][1]:
|
||||
was, _ = plan.pop(0)
|
||||
try:
|
||||
klick(was, markt, status.get("position"))
|
||||
except Exception as ausnahme:
|
||||
beobachtet["klicks"].append(f"{ort:%H:%M} {was}: AUSNAHME {ausnahme!r}")
|
||||
if was == "glattstellen":
|
||||
glatt_um = markt
|
||||
if glatt_um is not None and beobachtet["position_nach_glatt"] is None \
|
||||
and markt >= glatt_um + timedelta(minutes=2):
|
||||
beobachtet["position_nach_glatt"] = status.get("position")
|
||||
time.sleep(0.2)
|
||||
|
||||
faden = threading.Thread(target=pascal, name="pascal", daemon=True)
|
||||
faden.start()
|
||||
beginn = time.monotonic()
|
||||
code = paper._generalprobe(config, tag, TEMPO, melder)
|
||||
dauer = time.monotonic() - beginn
|
||||
time.sleep(0.5)
|
||||
halt.set()
|
||||
faden.join(timeout=5)
|
||||
|
||||
print("=" * 70)
|
||||
print(f"Tag {tag}, {len(kerzen)} Kerzen, Tempo {TEMPO:g} s je Marktminute, echte Dauer {dauer:.0f} s, Exit {code}")
|
||||
schluss = lies_status(config.status_datei)
|
||||
print(f"Statusdatei am Ende: modus={schluss.get('modus')}, phase={schluss.get('phase')}, "
|
||||
f"position={schluss.get('position')}, schluessel={sorted(schluss)}")
|
||||
print(f"Modi gesehen: {sorted(str(m) for m in beobachtet['modi'])}; Phasen: {beobachtet['phasen']}")
|
||||
zeiten = beobachtet["marktzeiten"]
|
||||
print(f"Marktzeit: {len(zeiten)} verschiedene Werte, von {zeiten[0].astimezone(HANDELSZONE):%Y-%m-%d %H:%M} "
|
||||
f"bis {zeiten[-1].astimezone(HANDELSZONE):%Y-%m-%d %H:%M}, Ruecksprung {beobachtet['rueckwaerts']}")
|
||||
print(f"Groesstes Alter des Herzschlags gegen die echte Uhr: {beobachtet['max_alter_s']:.1f} s")
|
||||
print("Klicks:")
|
||||
for zeile in beobachtet["klicks"]:
|
||||
print(" " + zeile)
|
||||
print(f"Gespielte Position zwei Marktminuten nach dem Glattstellen: {beobachtet['position_nach_glatt']}")
|
||||
print(f"Lesefehler im Beobachter: {beobachtet['fehler'][:3]}")
|
||||
|
||||
protokolle = sorted((WEG / "protokolle").glob("*.jsonl"))
|
||||
zeilen = []
|
||||
for pfad in protokolle:
|
||||
with pfad.open(encoding="utf-8") as datei:
|
||||
zeilen += [json.loads(zeile) for zeile in datei if zeile.strip()]
|
||||
takte = [z for z in zeilen if z.get("art") == "takt"]
|
||||
arten = {}
|
||||
for z in zeilen:
|
||||
schluessel = z.get("art") or z.get("was") or "?"
|
||||
arten[schluessel] = arten.get(schluessel, 0) + 1
|
||||
print(f"Protokoll: {[p.name for p in protokolle]}, {len(zeilen)} Zeilen, Arten {arten}")
|
||||
orders = [z for z in zeilen if z.get("art") == "order" or z.get("was") == "order"]
|
||||
print(f"Takte {len(takte)}, Orderzeilen {len(orders)}")
|
||||
if takte:
|
||||
print(f"Erster Takt: {json.dumps(takte[0], ensure_ascii=False)[:400]}")
|
||||
print(f"Letzter Takt: {json.dumps(takte[-1], ensure_ascii=False)[:400]}")
|
||||
|
||||
print(f"Meldungen ({len(transport.aufrufe)}):")
|
||||
for m in transport.aufrufe:
|
||||
print(f" {m['echt']} [{m['prioritaet']}] {m['titel']} | {m['erste_zeile']}")
|
||||
print(f"Aufrufe am Konto der Generalprobe: {konto_der_probe.aufrufe}")
|
||||
print(f"Aufrufe am Konto des Dashboards: {konto_des_dashboards.aufrufe}")
|
||||
print(f"Netzversuche ueber requests: {NETZVERSUCHE}")
|
||||
tagesstart = list(WEG.rglob("tagesstart.json"))
|
||||
print(f"tagesstart.json im Wegwerf-Ordner: {tagesstart}")
|
||||
|
||||
registry = Registry(db_pfad=WEG / "registry.db", artefakt_wurzel=ECHT / "artifacts")
|
||||
ereignisse = registry.killswitch_ereignisse()
|
||||
frisch = [e for e in ereignisse if getattr(e, "grund", "") and "Nachtlauf" in e.grund
|
||||
or getattr(e, "ausloeser", "") in ("neustart", "paper-runner")]
|
||||
print(f"Killswitch-Ereignisse im Wegwerf-Register: {len(ereignisse)} gesamt; die letzten acht:")
|
||||
for e in ereignisse[-8:] if len(ereignisse) >= 8 else ereignisse:
|
||||
print(f" {getattr(e, 'zeit', '?')} {e.stufe} / {e.ausloeser} / {e.grund[:60]} / erfolgreich={e.erfolgreich}")
|
||||
print(f"Killswitch am Ende: {registry.killswitch_zustand().zustand}")
|
||||
|
||||
befunde = []
|
||||
if code != 0:
|
||||
befunde.append(f"Exit {code} statt 0")
|
||||
if beobachtet["modi"] != {"generalprobe"}:
|
||||
befunde.append(f"Modi {beobachtet['modi']}")
|
||||
if any(not str(m["titel"]).startswith("Generalprobe: ") for m in transport.aufrufe):
|
||||
befunde.append("eine Meldung ohne Vorsatz")
|
||||
if not transport.aufrufe:
|
||||
befunde.append("keine Meldung")
|
||||
if NETZVERSUCHE:
|
||||
befunde.append("Netzversuch")
|
||||
if sorted(konto_der_probe.aufrufe) != ["marktuhr", "offene_orders", "positionen"]:
|
||||
befunde.append("Konto der Generalprobe: andere Aufrufe als die drei lesenden")
|
||||
if any(a.startswith("VERBOTEN") for a in konto_des_dashboards.aufrufe):
|
||||
befunde.append("Konto des Dashboards: verbotener Aufruf")
|
||||
if beobachtet["position_nach_glatt"] != 0:
|
||||
befunde.append(f"Position nach dem Glattstellen {beobachtet['position_nach_glatt']}")
|
||||
if beobachtet["max_alter_s"] > 5.0:
|
||||
befunde.append(f"Herzschlag bis {beobachtet['max_alter_s']:.1f} s alt")
|
||||
if beobachtet["rueckwaerts"]:
|
||||
befunde.append("Marktzeit lief rueckwaerts")
|
||||
if tagesstart:
|
||||
befunde.append("tagesstart.json entstanden")
|
||||
if schluss.get("position") != 0:
|
||||
befunde.append(f"Position am Ende {schluss.get('position')}")
|
||||
print(f"{len(befunde)} Befund(e): {befunde}")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user