"""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)