Files
claude-regelwerk/.vault-tools/trading-plattform-pruefung/pruef_beobachtung_echt.py
T
Pascal MichelandClaude Opus 5 67d674cd0b Automatischer Push: 32 Datei(en) geändert
- .claude/agents/executer.md
- .claude/agents/planer.md
- .claude/agents/pruefer.md
- .claude/agents/recherche.md
- .claude/skills/plan-executer-pruefung/SKILL.md
- .claude/workflows/executer-welle.js
- .claude/workflows/plankritik.js
- .claude/workflows/pruefung.js
- .claude/workflows/welle-mit-pruefung.js
- .gitignore
- .vault-tools/regelwerk-check/ENTWURF.md
- .vault-tools/regelwerk-check/regelwerk_check.py
- … und 20 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-24 04:00:02 +02:00

231 lines
10 KiB
Python

"""Beobachtung des Paper-Runners gegen die der NautilusEnv — auf ECHTEN IEX-Kerzen eines Tages.
Koordinator-Pruefskript zu Teil 10, nicht fuers Repo; ein Fangnetz, kein Nachweis.
Frage (Herkunft: Projektvorgabe, Spec Teil 10, "Abweichungen vom Training"): Liefert der Weg des
Runners (REST-Antwort -> AlpacaKerzenquelle -> Kerze -> baue_beobachtung) Wert fuer Wert dieselbe
Beobachtung wie der Weg des Trainings (dieselbe REST-Antwort -> alpaca_json_zu_bars -> Bar ->
NautilusEnv._beobachtung)? Der Test im Repo baut seine Kerzen aus den Bars selbst und sieht deshalb
keine Abweichung zwischen den zwei Umwandlungen; dieser Lauf nimmt zwei getrennte Wege von derselben
Antwort.
Zweite Frage (seit 2026-09-18, Prueferbefund zu Teil 10: der erste Teil vergleicht nur position = 0,
kontowert = startkapital, schritte_seit_trade = 0 -- drei der vier Schlusswerte sind dort auf beiden
Seiten Konstanten): Stimmen die vier Schlusswerte auch bei offener Long- und Short-Position, bei einem
Kontowert ungleich dem Startkapital und bei schritte_seit_trade groesser null? Dafuer faehrt die
Umgebung eine feste Aktionsfolge ueber alle fuenf Stufen, und nach jedem Schritt baut der Weg des
Runners seine Beobachtung aus DEMSELBEN Zustand (Position, Kontowert und Zaehler aus der Umgebung
gelesen). Geprueft wird damit die Rechnung des Runners, nicht die Herkunft seines Zustands.
Was es nicht sieht: ob Position und Kontowert im Betrieb richtig vom Broker kommen (Vorzeichen bei
Shorts: erst am Paper-Konto messbar), den Zaehler schritte_seit_trade des Runners selbst (er zaehlt nur
in Entscheidungstakten, todo Teil 10), SIP gegen IEX (gemessen in der Vorabmessung), andere
Instrumente und Tage.
Regressionsnachweis: --sabotage verschiebt einen Schlusskurs um einen Cent und muss mit Exit 2 enden.
--mutant-vorzeichen tauscht die Funktion des Runners gegen eine, die den Betrag der Position nimmt
(ein Vorzeichenfehler im Ziel); der erste Teil bleibt dabei gruen, der zweite muss rot sein.
Aufruf in WSL aus dem Projektordner (dort liegt config/secrets.toml; der Schluessel wird nie ausgegeben):
python pruef_beobachtung_echt.py 2026-09-17 [--sabotage] [--mutant-vorzeichen]
"""
import dataclasses
import sys
import tomllib
from datetime import datetime
from pathlib import Path
import numpy as np
import requests
from nautilus_trader.model.data import BarType
from nautilus_trader.test_kit.providers import TestInstrumentProvider
from trading import geheimnisse
from trading.data.loaders import alpaca_json_zu_bars
from trading.data.loaders import nur_regulaere_handelszeit
from trading.paper.beobachtung import baue_beobachtung
from trading.paper.kerzen import HANDELSZONE
from trading.paper.kerzen import AlpacaKerzenquelle
from trading.paper.kerzen import handelsbeginn
from trading.paper.kerzen import handelsende
from trading.rl.env import EnvConfig
from trading.rl.env import NautilusEnv
from trading.sim.venues import lade_venue_config
FENSTER = 20
EPISODE = 120
PERIODEN = (5, 20)
BASIS = 100
MAX_STUFE = 2
BAR_TYPE = "AAPL.XNAS-1-MINUTE-LAST-EXTERNAL"
def main() -> int:
if len(sys.argv) < 2:
print(__doc__)
return 3
tag = datetime.strptime(sys.argv[1], "%Y-%m-%d").replace(tzinfo=HANDELSZONE)
sabotage = "--sabotage" in sys.argv[2:]
mutant = "--mutant-vorzeichen" in sys.argv[2:]
ende = handelsende(tag)
# Weg des Runners.
kerzen = AlpacaKerzenquelle.from_secrets(None, feed="iex").kerzen_des_tages(
"AAPL.XNAS", ende
)
print("Kerzen ueber die Kerzenquelle:", len(kerzen))
if len(kerzen) < FENSTER + EPISODE:
print("VORAUSSETZUNG FEHLT: zu wenige Kerzen fuer eine Episode")
return 3
# Weg des Trainings: dieselbe Anfrage noch einmal roh, dann die Umwandlung des Katalogs.
geheim = tomllib.loads(geheimnisse.pfad().read_text(encoding="utf-8"))["alpaca"]
kopf = {"APCA-API-KEY-ID": geheim["key_id"], "APCA-API-SECRET-KEY": geheim["secret_key"]}
parameter = {
"symbols": "AAPL",
"timeframe": "1Min",
"start": handelsbeginn(ende).isoformat().replace("+00:00", "Z"),
"end": ende.isoformat().replace("+00:00", "Z"),
"feed": "iex",
"adjustment": "raw",
"limit": 10000,
}
antwort = requests.get(
"https://data.alpaca.markets/v2/stocks/bars", headers=kopf, params=parameter, timeout=30
)
antwort.raise_for_status()
nutzlast = antwort.json()
if nutzlast.get("next_page_token"):
print("VORAUSSETZUNG FEHLT: die Antwort hat eine Folgeseite; der Vergleich waere unvollstaendig")
return 3
instrument = TestInstrumentProvider.equity(symbol="AAPL", venue="XNAS")
bars = nur_regulaere_handelszeit(
alpaca_json_zu_bars(
nutzlast,
symbol="AAPL",
bar_type=BarType.from_str(BAR_TYPE),
price_precision=instrument.price_precision,
size_precision=instrument.size_precision,
)
)
print("Kerzen ueber den Katalogweg: ", len(bars))
if len(bars) != len(kerzen):
print("BEFUND: die zwei Wege liefern verschieden viele Kerzen")
return 2
halbe_cent = sum(
1
for eintrag in nutzlast["bars"]["AAPL"]
for feld in ("o", "h", "l", "c")
if round(float(eintrag[feld]) * 100) != float(eintrag[feld]) * 100
)
print("Rohkurse mit mehr als zwei Nachkommastellen:", halbe_cent)
if sabotage:
kerzen = list(kerzen)
kerzen[57] = dataclasses.replace(kerzen[57], close=round(kerzen[57].close + 0.01, 2))
venue = lade_venue_config("config/venues/us_equity.toml")
env = NautilusEnv(
venue_config=venue,
instrument=instrument,
bars=bars,
config=EnvConfig(
bar_type=BAR_TYPE,
fenster=FENSTER,
basis_menge=BASIS,
max_stufe=MAX_STUFE,
episoden_laenge=EPISODE,
indikator_perioden=PERIODEN,
),
)
startkapital = venue.starting_balance.as_double()
verglichen = 0
ungleich = 0
groesste = 0.0
for start in range(0, len(bars) - FENSTER - EPISODE + 1, 5):
soll, _ = env.reset(options={"start_index": start})
ist = baue_beobachtung(
kerzen[start : start + FENSTER],
position=0,
hoechste_position=MAX_STUFE * BASIS,
kontowert=startkapital,
startkapital=startkapital,
schritte_seit_trade=0,
episoden_laenge=EPISODE,
indikator_perioden=PERIODEN,
)
verglichen += 1
if soll.shape != ist.shape or not np.array_equal(soll, ist):
ungleich += 1
abstand = float(np.max(np.abs(soll.astype(np.float64) - ist.astype(np.float64))))
groesste = max(groesste, abstand)
if ungleich <= 3:
stellen = np.flatnonzero(soll != ist)[:5]
print(f" ungleich ab Kerze {start}: Stellen {stellen.tolist()}, groesster Abstand {abstand:.3e}")
print(f"Teil 1: {verglichen} Beobachtungen verglichen, {ungleich} ungleich, groesster Abstand {groesste:.3e}")
# Teil 2: Zustand ungleich null. Aktion a heisst Stufe a - MAX_STUFE; die Folge geht ueber
# +2, +1, flach, -2, -1 und haelt dazwischen, damit schritte_seit_trade waechst.
folge = [4, 4, 4, 3, 3, 2, 0, 0, 0, 1, 1, 2]
runner = baue_beobachtung
if mutant:
def runner(fenster_kerzen, *, position, **rest): # noqa: E306 - Mutant: Betrag statt Vorzeichen
return baue_beobachtung(fenster_kerzen, position=abs(position), **rest)
zustand = {"long": 0, "short": 0, "flach": 0, "kontowert_ungleich": 0, "zaehler_groesser_null": 0}
verglichen2 = 0
ungleich2 = 0
groesste2 = 0.0
letzter_start = len(bars) - FENSTER - EPISODE
for start in range(0, letzter_start + 1, max(1, letzter_start // 9)):
env.reset(options={"start_index": start})
for k, aktion in enumerate(folge, 1):
soll, _, beendet, abgeschnitten, _ = env.step(aktion)
position = env._strategie.netto_position()
ist = runner(
kerzen[start + k : start + k + FENSTER],
position=position,
hoechste_position=MAX_STUFE * BASIS,
kontowert=env._kontowert,
startkapital=startkapital,
schritte_seit_trade=env._schritte_seit_trade,
episoden_laenge=EPISODE,
indikator_perioden=PERIODEN,
)
zustand["long" if position > 0 else "short" if position < 0 else "flach"] += 1
zustand["kontowert_ungleich"] += env._kontowert != startkapital
zustand["zaehler_groesser_null"] += env._schritte_seit_trade > 0
verglichen2 += 1
if soll.shape != ist.shape or not np.array_equal(soll, ist):
ungleich2 += 1
abstand = float(np.max(np.abs(soll.astype(np.float64) - ist.astype(np.float64))))
groesste2 = max(groesste2, abstand)
if ungleich2 <= 3:
stellen = np.flatnonzero(soll != ist)[:5]
print(
f" Teil 2 ungleich: Start {start}, Schritt {k}, Position {position}, "
f"Stellen {stellen.tolist()}, groesster Abstand {abstand:.3e}"
)
if beendet or abgeschnitten:
break
env.close()
print(f"Teil 2: {verglichen2} Beobachtungen verglichen, {ungleich2} ungleich, groesster Abstand {groesste2:.3e}")
print(f" gesehene Zustaende: {zustand}")
if not (zustand["long"] and zustand["short"] and zustand["kontowert_ungleich"] and zustand["zaehler_groesser_null"]):
print("BEFUND: Teil 2 hat nicht jeden Zustand gesehen (long, short, Kontowert ungleich, Zaehler groesser null)")
return 2
if ungleich or ungleich2:
print("BEFUND: die Beobachtung des Runners weicht von der der NautilusEnv ab")
return 2
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Pruefskripts ist selbst ein Befund
print(f"BEFUND: das Pruefskript ist abgebrochen: {ausnahme!r}")
sys.exit(2)