Files
momentum-system/schwab/streaming/client.py
T
momentum devandClaude Opus 4.8 007cd95c79 Baseline before Postgres migration (WS0)
Snapshot before the DuckDB->PostgreSQL migration. data/, archive/, the 232MB
rebuild zip, the Windows .venv, and .env are excluded via .gitignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:39:29 -04:00

88 lines
3.0 KiB
Python

"""Schwab streaming client scaffold.
(Repaired: original had dataclass default-ordering errors and unbalanced
parentheses.) Real-time streaming requires the streamer-info handshake from
user preferences; this scaffold builds the login payload and exposes a
callback interface, while the live system polls REST minute bars instead.
"""
from __future__ import annotations
import json
import logging
from dataclasses import dataclass, field
from typing import Callable
logger = logging.getLogger(__name__)
@dataclass
class StreamerInfo:
streamer_socket_url: str = ""
schwab_client_customer_id: str = ""
schwab_client_correl_id: str = ""
schwab_client_channel: str = "N9"
schwab_client_function_id: str = "APIAPP"
@classmethod
def from_user_preferences(cls, payload: dict) -> "StreamerInfo":
info = (payload.get("streamerInfo") or [{}])[0]
return cls(
streamer_socket_url=info.get("streamerSocketUrl", ""),
schwab_client_customer_id=info.get("schwabClientCustomerId", ""),
schwab_client_correl_id=info.get("schwabClientCorrelId", ""),
schwab_client_channel=info.get("schwabClientChannel", "N9"),
schwab_client_function_id=info.get("schwabClientFunctionId", "APIAPP"),
)
@dataclass
class StreamingClient:
streamer_info: StreamerInfo
access_token: str
request_id: int = 0
on_message: Callable[[dict], None] | None = None
subscriptions: list[dict] = field(default_factory=list)
def _next_request_id(self) -> int:
self.request_id += 1
return self.request_id
def build_login_request(self) -> dict:
return {
"service": "ADMIN",
"command": "LOGIN",
"requestid": self._next_request_id(),
"SchwabClientCustomerId": self.streamer_info.schwab_client_customer_id,
"SchwabClientCorrelId": self.streamer_info.schwab_client_correl_id,
"parameters": {
"Authorization": self.access_token,
"SchwabClientChannel": self.streamer_info.schwab_client_channel,
"SchwabClientFunctionId": self.streamer_info.schwab_client_function_id,
},
}
def build_quote_subscription(self, symbols: list[str]) -> dict:
request = {
"service": "LEVELONE_EQUITIES",
"command": "SUBS",
"requestid": self._next_request_id(),
"SchwabClientCustomerId": self.streamer_info.schwab_client_customer_id,
"SchwabClientCorrelId": self.streamer_info.schwab_client_correl_id,
"parameters": {
"keys": ",".join(symbols),
"fields": "0,1,2,3,4,5,8",
},
}
self.subscriptions.append(request)
return request
def handle_raw_message(self, raw: str) -> None:
try:
payload = json.loads(raw)
except json.JSONDecodeError:
logger.warning("unparseable streaming message: %.120s", raw)
return
if self.on_message:
self.on_message(payload)