Automatischer Push: 14 Datei(en) geändert
- .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/rot_nachweis.py - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/dienste_stand.sh - .vault-tools/trading-plattform-pruefung/ha_einrichten.py - .vault-tools/trading-plattform-pruefung/hole_kerzen.py - .vault-tools/trading-plattform-pruefung/neustart_echtlauf.py - .vault-tools/trading-plattform-pruefung/ntfy_lesen.py - .vault-tools/trading-plattform-pruefung/pause_echtlauf.py - .vault-tools/trading-plattform-pruefung/wiederanlauf_probe.ps1 - AI/Regelwerk/codeworkflow.md - AI/Regelwerk/projekte.md - … 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
395642c480
commit
246af19683
14 files changed
+964
-7
No files matched your search
@@ -5,7 +5,7 @@ title: "Trading Plattform: Prüfwerkzeuge des Koordinators fürs Dashboard"
|
||||
summary: "Eigener Lauf mit echten Daten zum Dashboard der Trading Plattform — Vergleich der Endpunkte gegen Vollparse und pandas, zwei WebSocket-Verbindungen, Browser-Lauf und Wiederaufbau mit Playwright, gefahrlose Proben der zwei Skripte, Mutantenläufe. Entstanden 2026-09-17 (Teil 3b), erweitert 2026-09-18 (Teil 10: Anmeldung, Angriffsfälle, Beobachtung, Kerzenlatenz), bewusst nicht im Projekt-Repo."
|
||||
tags: [vault-tools, trading-plattform]
|
||||
created: 2026-09-17
|
||||
updated: 2026-09-18
|
||||
updated: 2026-09-21
|
||||
---
|
||||
|
||||
# Prüfwerkzeuge fürs Dashboard der Trading Plattform
|
||||
@@ -41,6 +41,25 @@ Koordinator-Werkzeuge, nicht fürs Repo. Sie starten **nie** den echten Broker:
|
||||
|
||||
**Seit 2026-09-18 kennen `angriffe.py` und `browserlauf_zugang.py` https.** `angriffe.py` erwartet je nach Schema der Basis-URL `Secure` am Cookie und `Strict-Transport-Security` (https) oder beides nicht (http), dazu unter https keine Antwort 200 auf Klartext; `--ca <pem>` nennt das Zertifikat, dem vertraut wird (geprüft, nicht abgeschaltet). `browserlauf_zugang.py --selbstsigniert` schaltet die Zertifikatsprüfung des Browsers ab. Regressionsnachweis vom selben Tag: gegen ein TLS-Backend mit einem Mutanten von `sicherheit.py` (ohne `Secure`, ohne HSTS) fielen genau die vier https-Fälle rot, Exit 2; Grundlinie über http 59 Fälle, über https 60. Läuft das Backend auf einem anderen Rechner, fährt man `angriffe.py` dort in einem Wegwerf-Container mit `--network host` und legt die drei Dateien der Ablage (`zugang.json`, `status.json`, `anmeldungen.jsonl`) per Verknüpfung in einen Ordner — das Werkzeug braucht Dateizugriff auf die Ablage des Backends.
|
||||
|
||||
## Paket F (2026-09-20): Produktivbetrieb als Dienst
|
||||
|
||||
| Datei | Was sie tut | Aufruf |
|
||||
|---|---|---|
|
||||
| `pause_echtlauf.py` | Tagesverlust-Schutz in der Pause auf den echten IEX-Kerzen eines Tages: rechnet aus den Rohkerzen vor, in welchem Takt die Grenze reißen muss (long und short, Grenze bei der Hälfte des größten Tagesverlusts), und hält das gegen das Protokoll der echten `PaperSitzung` im Zustand `angehalten`; dazu die Gegenprobe ohne Position. Einziger Netzzugriff: die Kerzen, lesend; Meldungen gehen an einen mitschreibenden Transport. Gemessen am 2026-09-17: long Takt 6, short Takt 173, je erwartet gleich gemessen | aus dem Projektordner in WSL: `python pause_echtlauf.py JJJJ-MM-TT` |
|
||||
| `dienste_stand.sh` | nur lesend: Unit-Dateien, `is-enabled`, Zustand, Startzeiten und letzter Exit-Code von Dashboard-Dienst und Runner, Zeitgeber, `/api/gesund`, Start von systemd, die letzten Journalzeilen beider Dienste, `status.json` und `tagesstart.json` ohne Kontowerte. Das Journal geht ohne `sudo`, wenn der Benutzer in `adm` oder `systemd-journal` ist (hier: ja); sonst nimmt das Skript `sudo -S` mit `WSL_PW` aus der Umgebung | aus PowerShell: `wsl -d Debian --exec bash /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/dienste_stand.sh` |
|
||||
| `hole_kerzen.py` | holt die echten IEX-Minutenkerzen eines Handelstags lesend und legt sie als JSON ab — die Eingabe für `neustart_echtlauf.py`. Gemessen am 2026-09-20 für den 2026-09-17: 390 Kerzen, byte-gleich mit dem Abruf des Abschlussprüfers | aus dem Projektordner in WSL: `python hole_kerzen.py JJJJ-MM-TT <ziel.json>` |
|
||||
| `neustart_echtlauf.py` | der Tagesverlust nach einem Neustart mitten am Tag: rechnet aus den Rohkerzen vor, bei welcher Kerze die echte Grenze (0,2 Prozent) gegen den Tagesstart reißt und bei welcher gegen den Neustartkurs, und fährt dann zwei echte `PaperSitzung`en nacheinander mit gemeinsamer `tagesstart.json`, dazu die Gegenprobe ohne Datei. Kein Netz. Gemessen am 2026-09-20 auf den Kerzen des 2026-09-17 und 2026-09-18: mit Datei Riss bei der vorgerechneten Kerze (5, 150, 19), ohne erst bei 18, 173, 42 | aus dem Projektordner in WSL: `python neustart_echtlauf.py <kerzen.json>` |
|
||||
| `ntfy_lesen.py` | liest über die Abfrage-Schnittstelle von ntfy zurück, was in der letzten Stunde unter dem Thema der Maschine ankam — Titel, Priorität, Zeit, erste Zeile. **Schickt nichts und gibt das Thema nie aus**, auch nicht in einer Fehlermeldung | aus dem Projektordner in WSL: `python ntfy_lesen.py` |
|
||||
| `wiederanlauf_probe.ps1` | beendet die Distribution hart (`wsl --terminate`) und misst, wann geplante Aufgabe, Distribution und Dashboard von selbst zurück sind. **Ein laufender Runner stirbt mit — nur außerhalb der Handelszeit, nur mit `-Ausfuehren`** (ohne den Schalter Exit 2, nichts getan). Während der Messung kein anderer `wsl.exe`-Aufruf, jeder Klient weckt die Distribution selbst. Gemessen am 2026-09-20: Rückkehr am nächsten Fünf-Minuten-Rasterpunkt, Dashboard 13 s danach | `powershell -File wiederanlauf_probe.ps1 -Ausfuehren` |
|
||||
|
||||
## Paket H (2026-09-21): Kennzahlen nach Home Assistant
|
||||
|
||||
| Datei | Was sie tut | Aufruf |
|
||||
|---|---|---|
|
||||
| `ha_einrichten.py` | richtet in Home Assistant die Gegenstelle für den Hausmelder ein: acht Helfer mit dem Präfix `trading_` und eine Automation mit Webhook-Auslöser (`local_only`, nur POST). Die Webhook-ID entsteht im Skript (64 Hex-Zeichen), wird nie ausgegeben und landet nur in `[homeassistant]` der `config/secrets.toml` des Trading-Projekts, mit `aktiv = false`. `pruefen` und `lesen` lesen nur (`lesen` zeigt zusätzlich die Zustände der Helfer), `probe` schickt eine Beispiel-Nutzlast und liest zurück, `entfernen` baut Helfer und Automation wieder ab. **Läuft unter Windows mit dem Python des Home-Assistant-Projekts** — dort liegen der Klient `src/ha_verbindung.py`, die Adresse der Instanz und der Name der Token-Variable; das Token bleibt auf der Windows-Seite und kommt nie auf den Trading-Rechner. Gemessen am 2026-09-20/21: eingerichtet, `probe` rückgelesen wie gesendet, und ein POST des echten `Hausmelder` aus WSL kam in den Helfern an | `<ha-projekt>\.venv\Scripts\python.exe -X utf8 ha_einrichten.py <pruefen|lesen|einrichten|probe|entfernen> "<ha-projekt>" "<trading-projekt>"` |
|
||||
|
||||
Das Einrichten selbst (`deploy/dienste-einrichten.sh` unter `sudo -S`, `deploy/windows/wsl-wachhalten.ps1 -Aktion einrichten`) und das Eintragen des ntfy-Themas liefen aus dem Scratchpad der Sitzung; die Befehle stehen in der Administrator-Anleitung des Projekts, Abschnitt „Produktivbetrieb als Dienst".
|
||||
|
||||
Die Compose-Probe (Bauen aus dem echten Baum mit verankerten Ausschlüssen, Start, Healthcheck, Angriffsfälle, Runner-Probe im Container, Aufräumen) lag als `compose_probe_echt.sh` im Scratchpad der Sitzung und braucht `sudo -S` mit dem WSL-Passwort aus `$WSL_PW`; sie ist deshalb nicht hier abgelegt. Muster: `tar --exclude=./data` — ohne Anker fällt auch `trading/data` weg.
|
||||
|
||||
Vor dem Browser-Lauf `npm run build` im Projekt, sonst dient das Backend eine alte Oberfläche aus. Die Datensatz- und Dateinamen in `browserlauf.py` und `pruef_teil3b.py` sind die des Bestands vom 2026-09-17; nach einem neuen Katalog oder neuen Protokollen nachziehen. Die Statustexte in `wiederaufbau.py` sind aus `Kopfzeile.tsx` abgeschrieben.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
# Nur lesend: Stand der eingerichteten Dienste der Trading Plattform in WSL-Debian.
|
||||
# Das Journal liest, wer in der Gruppe adm oder systemd-journal ist (hier: claude in adm);
|
||||
# sonst mit sudo -- dann das Passwort ueber WSLENV als WSL_PW durchreichen, nie ausgeben.
|
||||
PROJEKT="/mnt/d/Claude/Code Projekte/active - Trading Plattform"
|
||||
journal() {
|
||||
if journalctl -u "$1" --no-pager -n 1 >/dev/null 2>&1; then
|
||||
journalctl -u "$1" --no-pager -n "$2" -o short-iso | cut -c1-200
|
||||
elif [ -n "$WSL_PW" ]; then
|
||||
printf '%s\n' "$WSL_PW" | sudo -S -p '' journalctl -u "$1" --no-pager -n "$2" -o short-iso | cut -c1-200
|
||||
else
|
||||
echo "(Journal nicht lesbar: weder Gruppe adm/systemd-journal noch WSL_PW)"
|
||||
fi
|
||||
}
|
||||
echo "=== $(date -Is) ==="
|
||||
echo "--- dateien ---"
|
||||
ls -la /etc/systemd/system/ | grep -i "trading" | awk '{print $1, $5, $9}'
|
||||
echo "--- enabled ---"
|
||||
for u in trading-dashboard.service trading-paper.timer trading-paper.service; do
|
||||
printf '%s: ' "$u"; systemctl is-enabled "$u" 2>&1
|
||||
done
|
||||
echo "--- zustand ---"
|
||||
systemctl show -p ActiveState -p SubState -p Result -p ExecMainStartTimestamp -p NRestarts trading-dashboard.service | tr '\n' ' '; echo
|
||||
systemctl show -p ActiveState -p SubState -p Result -p ExecMainStartTimestamp -p ExecMainExitTimestamp -p ExecMainStatus trading-paper.service | tr '\n' ' '; echo
|
||||
echo "--- zeitgeber ---"
|
||||
systemctl list-timers trading-paper.timer --all --no-pager | head -4
|
||||
echo "--- gesund ---"
|
||||
curl -s -o /dev/null -w 'dashboard in WSL: HTTP %{http_code}\n' --max-time 4 http://127.0.0.1:8144/api/gesund
|
||||
echo "--- systemd laeuft seit ---"
|
||||
systemctl show -p UserspaceTimestamp --value
|
||||
echo "--- journal runner (letzte 16) ---"
|
||||
journal trading-paper.service 16
|
||||
echo "--- journal dashboard (letzte 8) ---"
|
||||
journal trading-dashboard.service 8
|
||||
echo "--- status.json und tagesstart.json (ohne Kontowerte) ---"
|
||||
python3 - "$PROJEKT" <<'EOF'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ordner = Path(sys.argv[1]) / "data" / "paper"
|
||||
for name, felder in (
|
||||
("status.json", ("zeit", "phase", "killswitch", "position", "letzter_grund", "pid", "boot")),
|
||||
("tagesstart.json", ("handelstag",)),
|
||||
):
|
||||
try:
|
||||
daten = json.loads((ordner / name).read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as ausnahme:
|
||||
print(f"{name}: nicht lesbar ({type(ausnahme).__name__})")
|
||||
continue
|
||||
print(name, {k: daten.get(k) for k in felder})
|
||||
EOF
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Richtet in Home Assistant die Gegenstelle fuer die Kennzahlen der Trading Plattform ein.
|
||||
|
||||
Koordinator-Werkzeug, nicht Teil des Repos. Laeuft unter WINDOWS mit dem Python des HA-Projekts
|
||||
(dort liegen `websockets` und `src/ha_verbindung.py`; Adresse der Instanz und der Name der
|
||||
Token-Variable stehen in jenem Modul, nicht hier). Das HA-Token bleibt auf der Windows-Seite und
|
||||
wird nie ausgegeben; auf den Trading-Rechner kommt nur die Webhook-Adresse.
|
||||
|
||||
Aktionen:
|
||||
pruefen nur lesend: Version, vorhandene Helfer mit dem Praefix, vorhandene Automation
|
||||
lesen nur lesend: wie pruefen, dazu die Zustaende der Helfer, wie sie gerade in Home Assistant stehen
|
||||
einrichten legt acht Helfer und eine Automation mit Webhook-Ausloeser an (local_only, nur POST),
|
||||
erzeugt dafuer eine Webhook-ID (64 Hex-Zeichen) und traegt die Adresse in die
|
||||
config/secrets.toml des Trading-Projekts ein ([homeassistant], aktiv = false).
|
||||
Gibt die ID nie aus. Bricht ab, wenn es den Abschnitt oder die Automation schon gibt.
|
||||
probe schickt eine Beispiel-Nutzlast an den Webhook (Adresse aus der Geheimnisdatei) und
|
||||
liest die Helfer ueber die HA-API zurueck
|
||||
entfernen loescht Automation und Helfer wieder; der Abschnitt in der Geheimnisdatei bleibt stehen
|
||||
und wird genannt
|
||||
|
||||
Aufruf: <ha-projekt>\\.venv\\Scripts\\python.exe ha_einrichten.py <aktion> <ha-projektordner> <trading-projektordner>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import time
|
||||
import tomllib
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
AUTOMATION_ID = "trading_plattform_kennzahlen"
|
||||
ALIAS = "Trading Plattform: Kennzahlen uebernehmen"
|
||||
PRAEFIX = "trading_"
|
||||
|
||||
TEXTE = ("Trading Phase", "Trading Modus", "Trading Killswitch")
|
||||
ZAHLEN = (
|
||||
("Trading Position", -100000, 100000, 1, "Stk"),
|
||||
("Trading Tagesergebnis USD", -1000000, 1000000, 0.01, "USD"),
|
||||
("Trading Herzschlag Alter s", -1, 100000000, 1, "s"),
|
||||
)
|
||||
SCHALTER = ("Trading Veraltet",)
|
||||
ZEITEN = ("Trading Gesendet",)
|
||||
|
||||
|
||||
def aktionen() -> list[dict]:
|
||||
def text(ziel: str, schluessel: str, vorgabe: str) -> dict:
|
||||
return {
|
||||
"service": "input_text.set_value",
|
||||
"target": {"entity_id": f"input_text.{ziel}"},
|
||||
"data": {"value": "{{ (trigger.json." + schluessel + " | default('" + vorgabe + "') | string)[:100] }}"},
|
||||
"continue_on_error": True,
|
||||
}
|
||||
|
||||
def zahl(ziel: str, schluessel: str, filter_: str) -> dict:
|
||||
return {
|
||||
"service": "input_number.set_value",
|
||||
"target": {"entity_id": f"input_number.{ziel}"},
|
||||
"data": {"value": "{{ trigger.json." + schluessel + " | default(0) | " + filter_ + " }}"},
|
||||
"continue_on_error": True,
|
||||
}
|
||||
|
||||
return [
|
||||
text("trading_phase", "phase", "unbekannt"),
|
||||
text("trading_modus", "modus", ""),
|
||||
text("trading_killswitch", "killswitch", "unbekannt"),
|
||||
zahl("trading_position", "position", "int(0)"),
|
||||
zahl("trading_tagesergebnis_usd", "tagesergebnis_usd", "float(0) | round(2)"),
|
||||
zahl("trading_herzschlag_alter_s", "herzschlag_alter_s", "int(-1)"),
|
||||
{
|
||||
"service": "input_boolean.turn_{{ 'on' if (trigger.json.veraltet | default(false)) else 'off' }}",
|
||||
"target": {"entity_id": "input_boolean.trading_veraltet"},
|
||||
"continue_on_error": True,
|
||||
},
|
||||
{
|
||||
"service": "input_datetime.set_datetime",
|
||||
"target": {"entity_id": "input_datetime.trading_gesendet"},
|
||||
"data": {"timestamp": "{{ now().timestamp() }}"},
|
||||
"continue_on_error": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def lies_abschnitt(datei: Path) -> dict | None:
|
||||
return tomllib.loads(datei.read_text(encoding="utf-8")).get("homeassistant")
|
||||
|
||||
|
||||
def trage_ein(datei: Path, adresse: str) -> None:
|
||||
roh = datei.read_text(encoding="utf-8")
|
||||
vorher = tomllib.loads(roh)
|
||||
block = (
|
||||
"\n[homeassistant]\n"
|
||||
"# Webhook in Home Assistant fuer die Kennzahlen des Paper-Runners. Die ID in der Adresse ist das\n"
|
||||
"# Geheimnis: Wer sie kennt, kann die Anzeige in HA faelschen. Eingeschaltet wird mit aktiv = true.\n"
|
||||
f'webhook_url = "{adresse}"\n'
|
||||
"aktiv = false\n"
|
||||
)
|
||||
neu = roh.rstrip("\n") + "\n" + block
|
||||
nachher = tomllib.loads(neu)
|
||||
if {k: v for k, v in nachher.items() if k != "homeassistant"} != vorher:
|
||||
raise SystemExit("ABBRUCH: bestehende Abschnitte haetten sich geaendert")
|
||||
nachbar = datei.with_name(datei.name + ".neu")
|
||||
with open(nachbar, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(neu)
|
||||
os.replace(nachbar, datei)
|
||||
|
||||
|
||||
def helfer_liste(ha) -> dict[str, list[dict]]:
|
||||
ergebnis = {}
|
||||
for art in ("input_text", "input_number", "input_boolean", "input_datetime"):
|
||||
eintraege = ha.befehl_ok(f"{art}/list") or []
|
||||
ergebnis[art] = [e for e in eintraege if str(e.get("id", "")).startswith(PRAEFIX)]
|
||||
return ergebnis
|
||||
|
||||
|
||||
def main() -> int:
|
||||
aktion, ha_projekt, trading = sys.argv[1], Path(sys.argv[2]), Path(sys.argv[3])
|
||||
sys.path.insert(0, str(ha_projekt / "src"))
|
||||
import ha_verbindung as hv
|
||||
|
||||
geheimnisse = trading / "config" / "secrets.toml"
|
||||
token = hv.lies_token()
|
||||
print(f"Token gelesen (Laenge {len(token)}), Instanz aus dem HA-Projekt, Aktion {aktion}")
|
||||
|
||||
# nur_lesen=False auch fuers Pruefen: Die Leseliste jenes Moduls kennt die Listenbefehle der
|
||||
# Helfer und get_states nicht. 'pruefen' und 'probe' senden trotzdem nur lesende Befehle.
|
||||
with hv.HaVerbindung(hv.BASIS_URL, token, nur_lesen=False) as ha:
|
||||
vorhanden = helfer_liste(ha)
|
||||
zahl_helfer = sum(len(v) for v in vorhanden.values())
|
||||
automationen = [
|
||||
s for s in (ha.befehl_ok("get_states") or [])
|
||||
if s.get("entity_id", "").startswith("automation.") and s.get("attributes", {}).get("id") == AUTOMATION_ID
|
||||
]
|
||||
abschnitt = lies_abschnitt(geheimnisse)
|
||||
print(f"Helfer mit Praefix {PRAEFIX}: {zahl_helfer} -- " + ", ".join(f"{a}: {[e['id'] for e in v]}" for a, v in vorhanden.items() if v))
|
||||
print(f"Automation {AUTOMATION_ID}: {'vorhanden' if automationen else 'fehlt'}")
|
||||
print("[homeassistant] in der Geheimnisdatei: " + ("fehlt" if abschnitt is None else str({k: (type(v).__name__, len(v) if isinstance(v, str) else v) for k, v in abschnitt.items()})))
|
||||
|
||||
if aktion == "pruefen":
|
||||
return 0
|
||||
|
||||
if aktion == "lesen":
|
||||
# Nur lesen: die Zustaende der Helfer, wie sie gerade in Home Assistant stehen.
|
||||
zustaende = {s["entity_id"]: s["state"] for s in (ha.befehl_ok("get_states") or []) if f".{PRAEFIX}" in s.get("entity_id", "")}
|
||||
for name in sorted(zustaende):
|
||||
print(f" {name} = {zustaende[name]}")
|
||||
return 0
|
||||
|
||||
if aktion == "einrichten":
|
||||
if abschnitt is not None or automationen or zahl_helfer:
|
||||
print("ABBRUCH: Es gibt schon Teile der Einrichtung; erst 'entfernen' oder von Hand klaeren.")
|
||||
return 3
|
||||
for name in TEXTE:
|
||||
ha.befehl_ok("input_text/create", name=name, min=0, max=100, mode="text", icon="mdi:chart-line")
|
||||
for name, kleinst, groesst, schritt, einheit in ZAHLEN:
|
||||
ha.befehl_ok("input_number/create", name=name, min=kleinst, max=groesst, step=schritt, mode="box", unit_of_measurement=einheit, icon="mdi:chart-line")
|
||||
for name in SCHALTER:
|
||||
ha.befehl_ok("input_boolean/create", name=name, icon="mdi:alert-outline")
|
||||
for name in ZEITEN:
|
||||
ha.befehl_ok("input_datetime/create", name=name, has_date=True, has_time=True, icon="mdi:clock-outline")
|
||||
webhook_id = secrets.token_hex(32)
|
||||
status, antwort = ha.http_post(
|
||||
f"/api/config/automation/config/{AUTOMATION_ID}",
|
||||
{
|
||||
"alias": ALIAS,
|
||||
"description": "Uebernimmt Phase, Killswitch, Position und Tagesergebnis des Paper-Runners aus dem Webhook der Trading Plattform. Angelegt am 2026-09-20 ueber ha_einrichten.py; Rueckbau mit der Aktion entfernen.",
|
||||
"trigger": [{"platform": "webhook", "webhook_id": webhook_id, "allowed_methods": ["POST"], "local_only": True}],
|
||||
"condition": [],
|
||||
"action": aktionen(),
|
||||
"mode": "queued",
|
||||
"max": 5,
|
||||
},
|
||||
)
|
||||
print(f"Automation angelegt: HTTP {status}")
|
||||
if status != 200:
|
||||
print("ABBRUCH: Die Automation wurde nicht angenommen:", str(antwort)[:200].replace(webhook_id, "***"))
|
||||
return 2
|
||||
trage_ein(geheimnisse, f"{hv.BASIS_URL}/api/webhook/{webhook_id}")
|
||||
print("Adresse eingetragen ([homeassistant], aktiv = false); die ID wurde nicht ausgegeben.")
|
||||
return 0
|
||||
|
||||
if aktion == "probe":
|
||||
if abschnitt is None:
|
||||
print("ABBRUCH: kein [homeassistant] in der Geheimnisdatei")
|
||||
return 3
|
||||
nutzlast = {
|
||||
"phase": "probe_der_einrichtung", "modus": "probe", "killswitch": "angehalten", "position": 7,
|
||||
"tagesergebnis_usd": -12.34, "herzschlag_alter_s": 5, "veraltet": False,
|
||||
"gesendet": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
}
|
||||
anfrage = urllib.request.Request(
|
||||
abschnitt["webhook_url"], data=json.dumps(nutzlast).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"}, method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(anfrage, timeout=5) as antwort:
|
||||
print(f"Webhook: HTTP {antwort.status}")
|
||||
except Exception as ausnahme:
|
||||
# Nur der Typ: Die Ausnahme traegt die Adresse und damit die ID.
|
||||
print(f"Webhook GESCHEITERT: {type(ausnahme).__name__}")
|
||||
return 2
|
||||
time.sleep(2.0)
|
||||
zustaende = {s["entity_id"]: s["state"] for s in (ha.befehl_ok("get_states") or []) if f".{PRAEFIX}" in s.get("entity_id", "")}
|
||||
for name in sorted(zustaende):
|
||||
print(f" {name} = {zustaende[name]}")
|
||||
erwartet = {
|
||||
"input_text.trading_phase": "probe_der_einrichtung", "input_text.trading_killswitch": "angehalten",
|
||||
"input_number.trading_position": "7.0", "input_number.trading_tagesergebnis_usd": "-12.34",
|
||||
"input_boolean.trading_veraltet": "off",
|
||||
}
|
||||
falsch = {k: (v, zustaende.get(k)) for k, v in erwartet.items() if zustaende.get(k) != v}
|
||||
print("Rueckgelesen wie gesendet." if not falsch else f"BEFUND: {falsch}")
|
||||
return 0 if not falsch else 1
|
||||
|
||||
if aktion == "entfernen":
|
||||
status = ha.http_delete(f"/api/config/automation/config/{AUTOMATION_ID}")
|
||||
print(f"Automation geloescht: HTTP {status}")
|
||||
for art, eintraege in vorhanden.items():
|
||||
for eintrag in eintraege:
|
||||
ha.befehl_ok(f"{art}/delete", **{f"{art}_id": eintrag["id"]})
|
||||
print(f" geloescht: {art}.{eintrag['id']}")
|
||||
if abschnitt is not None:
|
||||
print("Hinweis: [homeassistant] steht weiter in der Geheimnisdatei des Trading-Projekts; von Hand entfernen.")
|
||||
return 0
|
||||
|
||||
print("unbekannte Aktion")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Holt die echten IEX-Minutenkerzen eines Handelstags und legt sie als JSON ab. Nur lesend.
|
||||
|
||||
Koordinator-Werkzeug, nicht Teil des Repos. Der einzige Netzzugriff ist der lesende Kerzenabruf
|
||||
bei Alpaca; ausgegeben werden Anzahl und Eckkurse, nie ein Schluessel. Die Datei ist die Eingabe
|
||||
fuer neustart_echtlauf.py. Entstanden in der Abschlusspruefung zu Paket F (2026-09-20).
|
||||
Aufruf (WSL, aus dem Projektordner): python hole_kerzen.py JJJJ-MM-TT <ziel.json>
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
from trading.paper.config import lade_paper_config
|
||||
from trading.paper.kerzen import HANDELSZONE
|
||||
from trading.paper.kerzen import AlpacaKerzenquelle
|
||||
from trading.paper.kerzen import handelsende
|
||||
|
||||
|
||||
def main() -> int:
|
||||
datum, ziel = sys.argv[1], Path(sys.argv[2])
|
||||
config = lade_paper_config("config/paper.toml")
|
||||
mitternacht = datetime.strptime(datum, "%Y-%m-%d").replace(tzinfo=HANDELSZONE)
|
||||
quelle = AlpacaKerzenquelle.from_secrets("config/secrets.toml", feed=config.feed, timeout=20.0)
|
||||
kerzen = quelle.kerzen_des_tages(config.instrument_id, handelsende(mitternacht))
|
||||
ziel.write_text(
|
||||
json.dumps(
|
||||
[
|
||||
{"zeit": k.zeit.isoformat(), "open": k.open, "high": k.high,
|
||||
"low": k.low, "close": k.close, "volume": k.volume}
|
||||
for k in kerzen
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"{datum}: {len(kerzen)} Kerzen in {config.instrument_id} nach {ziel}")
|
||||
if kerzen:
|
||||
kurse = [k.close for k in kerzen]
|
||||
print(f" erste {kerzen[0].zeit.isoformat()} Schluss {kurse[0]}, "
|
||||
f"letzte {kerzen[-1].zeit.isoformat()} Schluss {kurse[-1]}, "
|
||||
f"Tief {min(kurse)}, Hoch {max(kurse)}")
|
||||
return 0 if kerzen else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Eigener Lauf mit echten Daten: Der Tagesverlust zaehlt nach einem Neustart weiter ab dem Tagesstart.
|
||||
|
||||
Koordinator-Pruefcode, nicht Teil des Repos. Echte IEX-Kerzen eines Tages (JSON aus dem lesenden
|
||||
Abruf der Abschlusspruefung). Aus den Rohkerzen wird vorgerechnet, bei welcher Kerze die Grenze von
|
||||
0,2 Prozent gegen den Tagesstart reisst und bei welcher gegen den Neustartkurs; dann laufen zwei
|
||||
echte PaperSitzungen nacheinander (vor und nach dem Neustart) mit gemeinsamer Tagesstart-Datei,
|
||||
dazu die Gegenprobe ohne Datei. Kein Netz: Meldungen gehen an einen mitschreibenden Transport.
|
||||
Aufruf (WSL, aus dem Projektordner): python neustart_echtlauf.py <kerzen.json>
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
from trading.killswitch.killswitch import lade_killswitch_config
|
||||
from trading.paper.config import enge_limits
|
||||
from trading.paper.config import lade_paper_config
|
||||
from trading.paper.kerzen import FesteKerzenquelle
|
||||
from trading.paper.kerzen import Kerze
|
||||
from trading.paper.melder import Melder
|
||||
from trading.paper.melder import MelderConfig
|
||||
from trading.paper.probe import ProbeBroker
|
||||
from trading.paper.sitzung import PaperSitzung
|
||||
from trading.paper.status import lies_tagesstart
|
||||
from trading.paper.uhr import ProbeUhr
|
||||
from trading.registry.registry import Registry
|
||||
from trading.risk.limits import lade_limits
|
||||
|
||||
STUECK = 20
|
||||
ZIEL_KAPITAL = 12_676.20 # so klein, dass 20 Stueck die echte Grenze von 0,2 Prozent erreichen
|
||||
|
||||
|
||||
class Protokoll:
|
||||
def __init__(self):
|
||||
self.zeilen = []
|
||||
|
||||
def schreibe(self, eintrag):
|
||||
self.zeilen.append(dict(eintrag))
|
||||
|
||||
|
||||
class Antwort:
|
||||
status_code = 200
|
||||
|
||||
|
||||
class Transport:
|
||||
def __init__(self):
|
||||
self.aufrufe = []
|
||||
|
||||
def __call__(self, url, *, data=None, headers=None, timeout=None):
|
||||
self.aufrufe.append(headers["Title"])
|
||||
return Antwort()
|
||||
|
||||
|
||||
class Flach:
|
||||
def predict(self, beobachtung, deterministic=False):
|
||||
return 2, None
|
||||
|
||||
|
||||
def lade(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 lauf(ordner, kerzen, *, beginn, position, cash, tagesstart, config, limits, ks_config):
|
||||
ordner.mkdir(parents=True, exist_ok=True)
|
||||
quelle = FesteKerzenquelle(kerzen)
|
||||
uhr = ProbeUhr(beginn)
|
||||
broker = ProbeBroker(kerzenquelle=quelle, uhr=uhr, instrument_id=config.instrument_id,
|
||||
startkapital=cash, position=position)
|
||||
registry = Registry(db_pfad=ordner / "registry.db", artefakt_wurzel=ordner / "artifacts")
|
||||
registry.setze_killswitch_zustand("angehalten", ausloeser="neustart")
|
||||
protokoll = Protokoll()
|
||||
transport = Transport()
|
||||
sitzung = PaperSitzung(
|
||||
broker=broker, kerzenquelle=quelle, agent=Flach(), agent_beschreibung={"quelle": "echtlauf"},
|
||||
registry=registry, limits=limits, killswitch_config=ks_config, config=config, uhr=uhr,
|
||||
protokoll=protokoll, protokoll_pfad="echtlauf.jsonl", status_pfad=ordner / "status.json",
|
||||
modus="probe", soll_abbrechen=lambda: False,
|
||||
melder=Melder(config=MelderConfig(aktiv=True, ntfy_url="https://ntfy.sh",
|
||||
thema="echtlauf-geht-nie-ins-netz", zeitlimit_s=3.0,
|
||||
stille_nach_s=180.0), token="", transport=transport),
|
||||
tagesstart_pfad=tagesstart,
|
||||
)
|
||||
code = sitzung.laufe()
|
||||
return code, protokoll.zeilen, transport.aufrufe, broker, registry
|
||||
|
||||
|
||||
def kerze_beim_grenzereignis(zeilen, index_nach_zeit):
|
||||
"""Index der juengsten Kerze in dem Takt, in dem die Zeile grenzwert steht, oder None."""
|
||||
for stelle, zeile in enumerate(zeilen):
|
||||
if zeile.get("was") == "grenzwert":
|
||||
takt = next(z for z in zeilen[stelle:] if z.get("art") == "takt")
|
||||
return index_nach_zeit[takt["letzte_kerze"]], takt["schritt"]
|
||||
return None, None
|
||||
|
||||
|
||||
def erster_riss(schluesse, *, ab, cash, position, bezug, anteil):
|
||||
for c in range(ab, len(schluesse)):
|
||||
if (cash + position * schluesse[c] - bezug) / bezug < -anteil:
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
kerzen = lade(sys.argv[1])
|
||||
schluesse = [k.close for k in kerzen]
|
||||
index_nach_zeit = {k.zeit.isoformat(): i for i, k in enumerate(kerzen)}
|
||||
config = lade_paper_config("config/paper.toml")
|
||||
limits = enge_limits(lade_limits("config/risk.toml"), config)
|
||||
ks_config = lade_killswitch_config("config/killswitch.toml")
|
||||
anteil = limits.max_tagesverlust_anteil
|
||||
tag = kerzen[0].zeit.date().isoformat()
|
||||
print(f"{tag}: {len(kerzen)} Kerzen, Schluss Kerze 0 {schluesse[0]:.2f}, "
|
||||
f"Tief {min(schluesse):.2f}, Hoch {max(schluesse):.2f}, Grenze {anteil}")
|
||||
|
||||
befunde = 0
|
||||
for seite, position in (("long", STUECK), ("short", -STUECK)):
|
||||
cash = ZIEL_KAPITAL - position * schluesse[0]
|
||||
tagesstart_wert = cash + position * schluesse[0]
|
||||
riss_tag = erster_riss(schluesse, ab=0, cash=cash, position=position,
|
||||
bezug=tagesstart_wert, anteil=anteil)
|
||||
if riss_tag is None or riss_tag < 3:
|
||||
print(f"[{seite}] der Tag reisst die Grenze gegen den Tagesstart nicht (oder sofort): uebersprungen")
|
||||
continue
|
||||
# Neustart so, dass die Sitzung davor die Kerze des Risses nicht mehr sieht: Sie laeuft
|
||||
# ueber kerzen[:r] und sieht die Kerzen 0 bis r-2. Die Sitzung danach beginnt in der
|
||||
# Minute der Kerze r und sieht zuerst die Kerze r-1.
|
||||
r = riss_tag
|
||||
bezug_neu = cash + position * schluesse[r - 1]
|
||||
riss_neu = erster_riss(schluesse, ab=r - 1, cash=cash, position=position,
|
||||
bezug=bezug_neu, anteil=anteil)
|
||||
print(f"[{seite}] Tagesstart {tagesstart_wert:.2f}; vorgerechnet: Riss gegen den Tagesstart "
|
||||
f"bei Kerze {riss_tag}, gegen den Neustartkurs ({bezug_neu:.2f}) bei Kerze {riss_neu}")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="neustart-echtlauf-") as wurzel:
|
||||
wurzel = Path(wurzel)
|
||||
datei = wurzel / "paper" / "tagesstart.json"
|
||||
gemeinsam = dict(position=position, cash=cash, config=config, limits=limits,
|
||||
ks_config=ks_config)
|
||||
|
||||
code1, zeilen1, _, _, reg1 = lauf(
|
||||
wurzel / "vorher", kerzen[:r], beginn=kerzen[0].zeit - timedelta(minutes=1),
|
||||
tagesstart=datei, **gemeinsam)
|
||||
riss1, _ = kerze_beim_grenzereignis(zeilen1, index_nach_zeit)
|
||||
gemerkt = lies_tagesstart(datei, handelstag=tag)
|
||||
print(f"[{seite}] vor dem Neustart: Exit {code1}, Killswitch "
|
||||
f"{reg1.killswitch_zustand().zustand}, Riss {riss1}, gemerkt {gemerkt}")
|
||||
|
||||
code2, zeilen2, titel2, broker2, reg2 = lauf(
|
||||
wurzel / "nachher", kerzen, beginn=kerzen[r].zeit - timedelta(minutes=1),
|
||||
tagesstart=datei, **gemeinsam)
|
||||
riss2, schritt2 = kerze_beim_grenzereignis(zeilen2, index_nach_zeit)
|
||||
takte2 = [z for z in zeilen2 if z.get("art") == "takt"]
|
||||
# Vor dem Ende des with-Blocks lesen: Danach ist das Wegwerf-Register weg.
|
||||
zustand2 = reg2.killswitch_zustand().zustand
|
||||
position2 = sum(p.menge for p in broker2.positionen())
|
||||
print(f"[{seite}] nach dem Neustart mit Datei: Startkapital {takte2[0]['startkapital']}, "
|
||||
f"Riss bei Kerze {riss2} (Schritt {schritt2}), Exit {code2}, Killswitch "
|
||||
f"{zustand2}, Position {position2}, Meldungen {titel2}")
|
||||
|
||||
code3, zeilen3, _, broker3, reg3 = lauf(
|
||||
wurzel / "ohne", kerzen, beginn=kerzen[r].zeit - timedelta(minutes=1),
|
||||
tagesstart=None, **gemeinsam)
|
||||
riss3, _ = kerze_beim_grenzereignis(zeilen3, index_nach_zeit)
|
||||
takte3 = [z for z in zeilen3 if z.get("art") == "takt"]
|
||||
print(f"[{seite}] nach dem Neustart ohne Datei: Startkapital {takte3[0]['startkapital']}, "
|
||||
f"Riss bei Kerze {riss3}, Exit {code3}, Killswitch {reg3.killswitch_zustand().zustand}")
|
||||
|
||||
ok = (
|
||||
riss1 is None and code1 == 4
|
||||
and gemerkt is not None and abs(gemerkt - tagesstart_wert) < 1e-9
|
||||
and takte2[0]["startkapital"] == gemerkt
|
||||
and riss2 == riss_tag
|
||||
and zustand2 == "gesperrt"
|
||||
and position2 == 0
|
||||
and titel2 == ["Paper-Runner wartet", "Grenze verletzt", "Paper-Runner beendet"]
|
||||
and abs(takte3[0]["startkapital"] - bezug_neu) < 1e-9
|
||||
and riss3 == riss_neu
|
||||
)
|
||||
print(f"[{seite}] {'OK' if ok else 'BEFUND'}")
|
||||
befunde += 0 if ok else 1
|
||||
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Liest nur zurueck, was in der letzten Stunde bei ntfy unter dem Thema der Maschine ankam.
|
||||
|
||||
Koordinator-Skript, nicht Teil des Repos. Schickt nichts. Das Thema wird nie ausgegeben -- weder
|
||||
hier noch in einer Fehlermeldung. Aufruf aus dem Projektordner (WSL): python ntfy_lesen.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
import requests
|
||||
|
||||
from trading.paper.melder import lade_melder_config
|
||||
from trading.paper.melder import lies_ntfy_geheimnisse
|
||||
|
||||
|
||||
def main() -> int:
|
||||
config = lade_melder_config("config/paper.toml", secrets_pfad="config/secrets.toml")
|
||||
thema, token = lies_ntfy_geheimnisse("config/secrets.toml")
|
||||
kopf = {"Authorization": f"Bearer {token}"} if token else {}
|
||||
try:
|
||||
antwort = requests.get(
|
||||
f"{config.ntfy_url}/{thema}/json", params={"poll": "1", "since": "1h"},
|
||||
headers=kopf, timeout=10, allow_redirects=False,
|
||||
)
|
||||
except Exception as ausnahme:
|
||||
# Nur der Typ: Die Ausnahme von requests traegt die Adresse und damit das Thema.
|
||||
print(f"Ruecklesen gescheitert: {type(ausnahme).__name__}")
|
||||
return 1
|
||||
print(f"Ruecklesen: HTTP {antwort.status_code}")
|
||||
anzahl = 0
|
||||
for zeile in antwort.text.splitlines():
|
||||
try:
|
||||
eintrag = json.loads(zeile)
|
||||
except ValueError:
|
||||
continue
|
||||
if eintrag.get("event") != "message":
|
||||
continue
|
||||
anzahl += 1
|
||||
# Ohne Angabe fuehrt ntfy die Prioritaet 3 (default) und laesst das Feld weg.
|
||||
print(f" bei ntfy: Titel '{eintrag.get('title')}', Prioritaet {eintrag.get('priority', 3)}, "
|
||||
f"Zeit {datetime.fromtimestamp(eintrag.get('time', 0)).isoformat()}, "
|
||||
f"erste Zeile '{str(eintrag.get('message', '')).splitlines()[0][:70]}'")
|
||||
print(f"{anzahl} Meldung(en) in der letzten Stunde.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Eigener Lauf mit echten Daten: Tagesverlust-Schutz in der Pause auf den IEX-Kerzen eines echten Tages.
|
||||
|
||||
Koordinator-Pruefcode, nicht Teil des Repos. Er rechnet aus den Rohkerzen vor, in welchem Takt die
|
||||
Grenze reissen muss, und haelt das gegen das Protokoll der echten PaperSitzung. Der einzige
|
||||
Netzzugriff holt lesend die Kerzen; Meldungen gehen an einen mitschreibenden Transport, nie ins Netz.
|
||||
Aufruf (aus dem Projektordner, in WSL): python pause_echtlauf.py JJJJ-MM-TT
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
from trading.killswitch.killswitch import lade_killswitch_config
|
||||
from trading.paper.config import enge_limits
|
||||
from trading.paper.config import lade_paper_config
|
||||
from trading.paper.kerzen import HANDELSZONE
|
||||
from trading.paper.kerzen import AlpacaKerzenquelle
|
||||
from trading.paper.kerzen import FesteKerzenquelle
|
||||
from trading.paper.kerzen import handelsende
|
||||
from trading.paper.melder import Melder
|
||||
from trading.paper.melder import MelderConfig
|
||||
from trading.paper.probe import ProbeBroker
|
||||
from trading.paper.sitzung import PaperSitzung
|
||||
from trading.paper.uhr import ProbeUhr
|
||||
from trading.registry.registry import Registry
|
||||
from trading.risk.limits import lade_limits
|
||||
from dataclasses import replace
|
||||
|
||||
|
||||
class Protokoll:
|
||||
def __init__(self):
|
||||
self.zeilen = []
|
||||
|
||||
def schreibe(self, eintrag):
|
||||
self.zeilen.append(dict(eintrag))
|
||||
|
||||
|
||||
class Antwort:
|
||||
status_code = 200
|
||||
|
||||
|
||||
class Transport:
|
||||
def __init__(self):
|
||||
self.aufrufe = []
|
||||
|
||||
def __call__(self, url, *, data=None, headers=None, timeout=None):
|
||||
self.aufrufe.append((headers["Title"], headers["Priority"], data.decode("utf-8")))
|
||||
return Antwort()
|
||||
|
||||
|
||||
class Flach:
|
||||
def predict(self, beobachtung, deterministic=False):
|
||||
return 2, None
|
||||
|
||||
|
||||
def lauf(kerzen, *, config, limits, killswitch_config, zustand, position):
|
||||
ordner = Path(tempfile.mkdtemp(prefix="pause-echtlauf-"))
|
||||
quelle = FesteKerzenquelle(kerzen)
|
||||
uhr = ProbeUhr(kerzen[0].zeit - timedelta(minutes=1))
|
||||
broker = ProbeBroker(
|
||||
kerzenquelle=quelle, uhr=uhr, instrument_id=config.instrument_id, position=position
|
||||
)
|
||||
registry = Registry(db_pfad=ordner / "registry.db", artefakt_wurzel=ordner / "artifacts")
|
||||
registry.setze_killswitch_zustand(zustand, ausloeser="echtlauf")
|
||||
protokoll = Protokoll()
|
||||
transport = Transport()
|
||||
melder = Melder(
|
||||
config=MelderConfig(
|
||||
aktiv=True,
|
||||
ntfy_url="https://ntfy.sh",
|
||||
thema="echtlauf-geht-nie-ins-netz",
|
||||
zeitlimit_s=3.0,
|
||||
stille_nach_s=180.0,
|
||||
),
|
||||
token="",
|
||||
transport=transport,
|
||||
)
|
||||
sitzung = PaperSitzung(
|
||||
broker=broker,
|
||||
kerzenquelle=quelle,
|
||||
agent=Flach(),
|
||||
agent_beschreibung={"quelle": "echtlauf"},
|
||||
registry=registry,
|
||||
limits=limits,
|
||||
killswitch_config=killswitch_config,
|
||||
config=config,
|
||||
uhr=uhr,
|
||||
protokoll=protokoll,
|
||||
protokoll_pfad="echtlauf.jsonl",
|
||||
status_pfad=ordner / "status.json",
|
||||
modus="probe",
|
||||
soll_abbrechen=lambda: False,
|
||||
melder=melder,
|
||||
)
|
||||
code = sitzung.laufe()
|
||||
return code, protokoll.zeilen, transport.aufrufe, broker, registry
|
||||
|
||||
|
||||
def main():
|
||||
datum = sys.argv[1]
|
||||
mitternacht = datetime.strptime(datum, "%Y-%m-%d").replace(tzinfo=HANDELSZONE)
|
||||
config = lade_paper_config("config/paper.toml")
|
||||
killswitch_config = lade_killswitch_config("config/killswitch.toml")
|
||||
quelle = AlpacaKerzenquelle.from_secrets("config/secrets.toml", feed=config.feed, timeout=10.0)
|
||||
kerzen = list(quelle.kerzen_des_tages(config.instrument_id, handelsende(mitternacht)))
|
||||
print(f"{datum}: {len(kerzen)} IEX-Kerzen in {config.instrument_id}")
|
||||
schluesse = [k.close for k in kerzen]
|
||||
print(f"Schluss erste Kerze {schluesse[0]:.2f}, Tief {min(schluesse):.2f}, Hoch {max(schluesse):.2f}")
|
||||
|
||||
stueck = 20
|
||||
cash = 100_000.0
|
||||
befunde = 0
|
||||
for seite, position in (("long", stueck), ("short", -stueck)):
|
||||
# Eigene Rechnung aus den Rohkerzen: Takt k (schritt k) sieht die Kerze k-1 als juengste
|
||||
# abgeschlossene. Startkapital ist der Kontowert im ersten Takt.
|
||||
start = cash + position * schluesse[0]
|
||||
schlechtester = min(cash + position * s for s in schluesse) - start
|
||||
if schlechtester >= 0:
|
||||
print(f"[{seite}] der Tag bringt dieser Seite keinen Verlust gegen den ersten Takt; uebersprungen")
|
||||
continue
|
||||
# Die Grenze liegt bei der Haelfte des groessten Verlusts dieses Tages: Der Tag reisst sie
|
||||
# wirklich, und nicht im ersten Takt.
|
||||
anteil = abs(schlechtester) / start / 2.0
|
||||
erwartet = None
|
||||
for k, s in enumerate(schluesse, start=1):
|
||||
if (cash + position * s - start) / start < -anteil:
|
||||
erwartet = k
|
||||
break
|
||||
limits = replace(enge_limits(lade_limits("config/risk.toml"), config), max_tagesverlust_anteil=anteil)
|
||||
code, zeilen, meldungen, broker, registry = lauf(
|
||||
kerzen, config=config, limits=limits, killswitch_config=killswitch_config,
|
||||
zustand="angehalten", position=position,
|
||||
)
|
||||
takte = [z for z in zeilen if z.get("art") == "takt"]
|
||||
grenz = [z for z in zeilen if z.get("was") == "grenzwert"]
|
||||
# Der Takt, in dem die Zeile grenzwert steht: die Taktzeile danach traegt seinen Schritt.
|
||||
stelle = zeilen.index(grenz[0]) if grenz else None
|
||||
schritt = next((z["schritt"] for z in zeilen[stelle:] if z.get("art") == "takt"), None) if grenz else None
|
||||
titel = [m[0] for m in meldungen]
|
||||
print(f"[{seite}] Startkapital {start:.2f}, Grenze {anteil:.6f} (= {anteil * start:.2f} USD), "
|
||||
f"erwarteter Takt {erwartet}, gemessen {schritt}")
|
||||
print(f"[{seite}] Zustand {registry.killswitch_zustand().zustand}, Position danach "
|
||||
f"{sum(p.menge for p in broker.positionen())}, Exit {code}, Meldungen {titel}")
|
||||
print(f"[{seite}] schliesse_alle: {broker.aufrufe.count('schliesse_alle')}, Takte {len(takte)}, "
|
||||
f"grenzwert-Zeilen {len(grenz)}")
|
||||
ok = (
|
||||
schritt == erwartet
|
||||
and registry.killswitch_zustand().zustand == "gesperrt"
|
||||
and broker.aufrufe.count("schliesse_alle") == 1
|
||||
and len(grenz) == 1
|
||||
and titel == ["Paper-Runner wartet", "Grenze verletzt", "Paper-Runner beendet"]
|
||||
and code == 0
|
||||
)
|
||||
print(f"[{seite}] {'OK' if ok else 'BEFUND'}")
|
||||
befunde += 0 if ok else 1
|
||||
|
||||
# Gegenprobe: dieselben Kerzen, angehalten, ohne Position -- genau eine Kontoabfrage, nichts geschlossen.
|
||||
limits = enge_limits(lade_limits("config/risk.toml"), config)
|
||||
code, zeilen, meldungen, broker, registry = lauf(
|
||||
kerzen, config=config, limits=limits, killswitch_config=killswitch_config,
|
||||
zustand="angehalten", position=0,
|
||||
)
|
||||
titel = [m[0] for m in meldungen]
|
||||
ok = (
|
||||
broker.aufrufe.count("konto") == 1
|
||||
and "schliesse_alle" not in broker.aufrufe
|
||||
and titel == ["Paper-Runner wartet", "Paper-Runner beendet"]
|
||||
and code == 0
|
||||
)
|
||||
print(f"[ohne Position] konto {broker.aufrufe.count('konto')}, Meldungen {titel}, Exit {code}: "
|
||||
f"{'OK' if ok else 'BEFUND'}")
|
||||
befunde += 0 if ok else 1
|
||||
|
||||
# Gegenprobe 2: aktiv mit dem echten Haltemuster aendert sich nicht -- das prueft die Probe des Skripts.
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,53 @@
|
||||
# Wiederanlauf-Probe: beendet die Distribution HART (wsl --terminate) und misst, wann Wachhalter,
|
||||
# Distribution und Dashboard von selbst zurueck sind. Ein laufender Paper-Runner stirbt dabei mit --
|
||||
# deshalb nur ausserhalb der Handelszeit (15:30 bis 22:00 Uhr Berliner Zeit) und nur mit -Ausfuehren.
|
||||
# Waehrend der Messung keinen anderen wsl.exe-Aufruf absetzen: Jeder Klient weckt die Distribution
|
||||
# selbst und verfaelscht das Ergebnis. Die Datei ist reines ASCII (PowerShell 5.1 liest ohne BOM ANSI).
|
||||
# Gemessen am 2026-09-20: terminate 19:30:17, Aufgabe und Distribution Running um 19:35:06, HTTP 200 um 19:35:13.
|
||||
param(
|
||||
[string]$Name = "Trading Plattform - WSL wachhalten",
|
||||
[int]$MaxSekunden = 480,
|
||||
[switch]$Ausfuehren
|
||||
)
|
||||
|
||||
if (-not $Ausfuehren) {
|
||||
"Nichts getan. Diese Probe beendet die WSL-Distribution hart; ein laufender Runner stirbt mit."
|
||||
"Aufruf ausserhalb der Handelszeit: powershell -File wiederanlauf_probe.ps1 -Ausfuehren"
|
||||
exit 2
|
||||
}
|
||||
|
||||
$wsl = "$env:SystemRoot\System32\wsl.exe"
|
||||
|
||||
function Distro {
|
||||
$roh = ((& $wsl -l -v) | Out-String).Replace([string][char]0, '')
|
||||
foreach ($zeile in ($roh -split "\r?\n")) {
|
||||
if ($zeile -match 'Debian\s+(\w+)') { return $Matches[1] }
|
||||
}
|
||||
return "unbekannt"
|
||||
}
|
||||
|
||||
function Probe {
|
||||
try {
|
||||
$r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 3 http://127.0.0.1:8144/api/gesund
|
||||
return "HTTP " + $r.StatusCode
|
||||
} catch {
|
||||
return "tot"
|
||||
}
|
||||
}
|
||||
|
||||
"Vorher: Aufgabe " + (Get-ScheduledTask -TaskName $Name).State + ", Distro " + (Distro) + ", Dashboard " + (Probe)
|
||||
& $wsl --terminate Debian | Out-Null
|
||||
$start = Get-Date
|
||||
"terminate um " + $start.ToString("HH:mm:ss")
|
||||
$letzter = ""
|
||||
while (((Get-Date) - $start).TotalSeconds -lt $MaxSekunden) {
|
||||
$a = (Get-ScheduledTask -TaskName $Name).State
|
||||
$d = Distro
|
||||
$p = Probe
|
||||
$s = [int]((Get-Date) - $start).TotalSeconds
|
||||
$jetzt = "Aufgabe $a / Distro $d / Dashboard $p"
|
||||
if ($jetzt -ne $letzter) { "+${s}s (" + (Get-Date).ToString("HH:mm:ss") + "): $jetzt"; $letzter = $jetzt }
|
||||
if ($s -gt 15 -and $a -eq "Running" -and $d -eq "Running" -and $p -eq "HTTP 200") { break }
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
"Ende der Messung um " + (Get-Date).ToString("HH:mm:ss")
|
||||
Reference in new issue
Block a user