"""Lesender Probeaufruf fuer Paket M: GET /v2/assets, trades/latest und bars mit mehreren Symbolen. Koordinator-Werkzeug, nicht Teil des Repos. Anlass: Paket M (zehn Aktien, 2026-09-25) haengt an drei Antwortformen, die im Projekt nie gemessen wurden -- GET /v2/assets/{symbol}, GET /v2/stocks/trades/latest mit mehreren Symbolen und das Seitenverhalten von GET /v2/stocks/bars mit mehreren Symbolen. Das Werkzeug belegt sie an genau dem Code, der sie liest (AlpacaBroker.anlage, AlpacaBroker.letzte_kurse, AlpacaKerzenquelle.kerzen_mehrerer), und zusaetzlich roh: Schluessel und Typen, nie Werte ausser den Wahrheitswerten der Startpruefung, Zaehlungen und dem Alter des letzten Trades. Nur GET, kein schreibender Aufruf. Liest die Geheimnisdatei ueber den Aufloeser des Projekts (TRADING_GEHEIMNISSE, sonst config/secrets.toml) und gibt die Schluessel nie aus. Aufruf (WSL, aus einem Arbeitsbaum mit Paket M, Venv): python m_probeaufruf.py [ ...] Exit 0: alle Formen wie angenommen. 2: eine Abweichung oder ein Starthindernis. 1: Ausnahme. """ import sys import tomllib from datetime import UTC from datetime import datetime from pathlib import Path import requests sys.path.insert(0, str(Path.cwd())) from trading import geheimnisse # noqa: E402 from trading.broker.alpaca import DATEN_URL # noqa: E402 from trading.broker.alpaca import TRADING_URL # noqa: E402 from trading.broker.alpaca import AlpacaBroker # noqa: E402 from trading.broker.schnittstelle import zu_symbol # noqa: E402 from trading.paper.kerzen import HANDELSZONE # noqa: E402 from trading.paper.kerzen import AlpacaKerzenquelle # noqa: E402 from trading.paper.kerzen import handelsbeginn # noqa: E402 from trading.paper.kerzen import handelsende # noqa: E402 FEED = "iex" # Klein genug, dass zehn Symbole mal 390 Minuten mehrere Seiten brauchen. KLEINE_SEITE = 1000 MAX_SEITEN = 50 def typ(wert) -> str: return type(wert).__name__ def iso_z(zeitpunkt: datetime) -> str: return zeitpunkt.astimezone(UTC).isoformat().replace("+00:00", "Z") def zaehle_seiten(sitzung, kopf: dict, parameter: dict) -> tuple[dict[str, int], int, int]: """Kerzen je Symbol ueber alle Seiten, Zahl der Seiten, Seiten mit mehr als einem Symbol.""" parameter = dict(parameter) je_symbol: dict[str, int] = {} seiten = 0 gemischt = 0 while seiten < MAX_SEITEN: antwort = sitzung.get(f"{DATEN_URL}/v2/stocks/bars", headers=kopf, params=parameter, timeout=30) seiten += 1 if antwort.status_code != 200: raise RuntimeError(f"GET /v2/stocks/bars antwortete {antwort.status_code}") nutzlast = antwort.json() bars = nutzlast.get("bars") or {} if len(bars) > 1: gemischt += 1 for symbol, liste in bars.items(): je_symbol[symbol] = je_symbol.get(symbol, 0) + len(liste or []) token = nutzlast.get("next_page_token") if not token: break parameter["page_token"] = token return je_symbol, seiten, gemischt def main() -> int: if len(sys.argv) < 3: print(__doc__) return 1 datum = sys.argv[1] kennungen = sys.argv[2:] symbole = [zu_symbol(kennung) for kennung in kennungen] werte = tomllib.loads(geheimnisse.pfad().read_text(encoding="utf-8"))["alpaca"] kopf = {"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]} sitzung = requests.Session() sitzung.trust_env = False abweichungen = 0 # 1. GET /v2/assets roh fuer das erste Symbol: nur Schluessel und Typen. antwort = sitzung.get(f"{TRADING_URL}/v2/assets/{symbole[0]}", headers=kopf, timeout=20) print(f"GET /v2/assets/{symbole[0]}: HTTP {antwort.status_code}") roh = antwort.json() if antwort.status_code == 200 else {} if isinstance(roh, dict): print(f" schluessel und typen: {dict((k, typ(v)) for k, v in sorted(roh.items()))}") for feld, erwartet in (("symbol", "str"), ("tradable", "bool"), ("shortable", "bool"), ("easy_to_borrow", "bool")): gefunden = typ(roh.get(feld)) if isinstance(roh, dict) and feld in roh else "fehlt" if gefunden != erwartet: print(f"ABWEICHUNG: Feld {feld} ist {gefunden}, erwartet {erwartet}") abweichungen += 1 # 2. anlage() je Instrument ueber den Code des Projekts. broker = AlpacaBroker( werte["key_id"], werte["secret_key"], universum=tuple(kennungen), feed=FEED, timeout=20.0 ) for kennung in kennungen: anlage = broker.anlage(kennung) print( f"anlage {kennung}: tradable {anlage.handelbar}, shortable {anlage.leerverkaufbar}, " f"easy_to_borrow {anlage.leicht_leihbar}" ) if not (anlage.handelbar and anlage.leerverkaufbar and anlage.leicht_leihbar): print(f"STARTHINDERNIS: {kennung} haelt die Startpruefung einer Folge mit Short nicht") abweichungen += 1 # 3. trades/latest roh und ueber den Code. antwort = sitzung.get( f"{DATEN_URL}/v2/stocks/trades/latest", headers=kopf, params={"symbols": ",".join(symbole), "feed": FEED}, timeout=20, ) print(f"GET /v2/stocks/trades/latest mit {len(symbole)} Symbolen: HTTP {antwort.status_code}") roh = antwort.json() if antwort.status_code == 200 else {} trades = roh.get("trades") if isinstance(roh, dict) else None print(f" schluessel oben: {sorted(roh) if isinstance(roh, dict) else typ(roh)}, trades ist {typ(trades)}") if not isinstance(trades, dict) or sorted(trades) != sorted(symbole): gesehen = sorted(trades) if isinstance(trades, dict) else typ(trades) print(f"ABWEICHUNG: trades traegt {gesehen} statt {sorted(symbole)}") abweichungen += 1 kurse = broker.letzte_kurse(kennungen) jetzt = datetime.now(UTC) for kennung in kennungen: kurs = kurse.get(kennung) alter = "keiner" if kurs is None else f"vor {(jetzt - kurs.zeit).total_seconds():.0f} s" print(f"letzter trade {kennung}: {alter}") # 4. Kerzen des Handelstags: gebuendelt ueber den Code gegen einzeln ueber den Code. mitternacht = datetime.strptime(datum, "%Y-%m-%d").replace(tzinfo=HANDELSZONE) bis = handelsende(mitternacht) quelle = AlpacaKerzenquelle(werte["key_id"], werte["secret_key"], feed=FEED, timeout=30.0) gebuendelt = quelle.kerzen_mehrerer(kennungen, bis) for kennung in kennungen: einzeln = quelle.kerzen_des_tages(kennung, bis) gleich = [k.zeit for k in einzeln] == [k.zeit for k in gebuendelt[kennung]] print( f"kerzen {kennung}: gebuendelt {len(gebuendelt[kennung])}, einzeln {len(einzeln)}" + ("" if gleich else " ABWEICHUNG") ) if not gleich: abweichungen += 1 # 5. Seitenverhalten roh: eine grosse Seite gegen viele kleine, je Symbol dieselbe Summe. grundparameter = { "symbols": ",".join(symbole), "timeframe": "1Min", "start": iso_z(handelsbeginn(bis)), "end": iso_z(bis), "feed": FEED, "adjustment": "raw", } gross, seiten_gross, _ = zaehle_seiten(sitzung, kopf, grundparameter | {"limit": 10_000}) klein, seiten_klein, gemischt = zaehle_seiten(sitzung, kopf, grundparameter | {"limit": KLEINE_SEITE}) summe = sum(gross.values()) print(f"seiten mit limit 10000: {seiten_gross}, kerzen zusammen {summe}") print(f"seiten mit limit {KLEINE_SEITE}: {seiten_klein}, davon mit mehr als einem symbol: {gemischt}") if gross != klein: print(f"ABWEICHUNG: je Symbol ueber die kleinen Seiten {klein}, ueber die grosse {gross}") abweichungen += 1 if summe > KLEINE_SEITE and seiten_klein == 1: print("HINWEIS: limit gilt offenbar je Symbol und nicht ueber alle; der Code sammelt in beiden Faellen richtig") if abweichungen: print(f"ERGEBNIS: {abweichungen} Abweichung(en) oder Starthindernis(se)") return 2 print("ERGEBNIS: alle Formen wie angenommen") return 0 if __name__ == "__main__": try: sys.exit(main()) except Exception as ausnahme: # noqa: BLE001 - ein Absturz ist selbst ein Befund print(f"BEFUND: das Werkzeug ist abgebrochen: {ausnahme!r}") sys.exit(1)