- .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]>
231 lines
10 KiB
Python
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)
|