Skip to content

feat: async WebSocket client for all data types (on the generated WS surface)#168

Merged
martinkersner merged 3 commits into
mainfrom
feat/ws-ticker-client
Jul 5, 2026
Merged

feat: async WebSocket client for all data types (on the generated WS surface)#168
martinkersner merged 3 commits into
mainfrom
feat/ws-ticker-client

Conversation

@martinkersner

@martinkersner martinkersner commented Jul 5, 2026

Copy link
Copy Markdown
Member

Summary

Async WebSocket client covering every DataMaxi+ WS data type, driven by the codegen-generated WS surface and validated against production.

  • Consumes the generated filesdatamaxi/_ws_endpoints.py (WS_CHANNELS / WS_BASE_PATH / WS_AUTH_HEADER) and datamaxi/_ws_models.py (per-channel TypedDicts), both emitted by datamaxi-codegen #35. Excluded from black/flake8 like _endpoints.py.
  • datamaxi/aio/ws.pyAsyncDatamaxiWS on the websockets lib (new optional [ws] extra), with generic channel types (Subscription / MarketSubscription / Feed) over one AsyncWSConnection transport. SUBSCRIBE/UNSUBSCRIBE/PING protocol, app-level keepalive (~30s vs the ~90s proxy idle), reconnect-with-resubscribe, ack filtering.

Accessors (all from the generated registry):

from datamaxi.aio.ws import AsyncDatamaxiWS

async with AsyncDatamaxiWS(api_key="...") as ws:
    async for t in ws.ticker.subscribe("BTC-USDT@binance", market="spot"): ...
    async for oi in ws.open_interest.subscribe("BTC-USDT@binance"): ...
    async for lq in ws.liquidation_feed.stream(): ...           # firehose, no params
    async for pr in ws.premium.subscribe("binance:upbit:bitcoin:USDT:KRW:spot:spot"): ...

ticker (market-keyed), forex, premium, funding_rate, open_interest, liquidation, liquidation_feed, announcement / announcement_internal (Pro+). Orderbook is intentionally excluded (unsupported product — backend removal tracked in datamaxi-backend#7927).

Routing: channel-streamsubscribe() yields every message on the channel; callers filter by msg["s"]/["e"] (the wire tags messages by payload fields, not a channel id).

Validated against production

Live-connected to wss://api.datamaxiplus.com and confirmed:

  • Generated models are byte-accurate to prod across both extractor paths — Go-struct channels (ticker/forex/funding/premium: exact key match) and proto channels (open-interest/liquidation: model is the correct total=False superset, since protojson omits zero fields).
  • The codegen's wire-type distinction holds live — protojson int64 → str (e.g. open_interest.d) vs Go-json int64 → int (e.g. ticker.d).
  • Param formats confirmed — incl. premium's colon-delimited key and funding-rate's SYMBOL@exchange (the backend doc comment was wrong → #7926).
  • Live testing also caught + fixed a client bug: empty-param subscribe acks arrive as {"id": N} (server omits result) and were leaking as data; now filtered (dict with keys ⊆ {"result","id"}), re-verified live.

Tests

tests/test_ws.py runs a real in-process websockets server: auth header, path from the generated registry, subscribe protocol, streamed data, ack filtering (incl. empty-result), multi-symbol multiplexing, keepalive PING, forex-subscribe, feed-stream, orderbook-excluded, and every channel maps to a generated model + client accessor. importorskip-guarded; websockets in requirements-test.

  • pytest -m "not integration": 211 passed, 11 skipped.
  • black + flake8 clean; import datamaxi does not load websockets (extra stays optional).

Scope / next

Depends on codegen #35 (the generated _ws_*.py). Remaining un-live-tested: plain /liquidation (sporadic), announcement data (rare), and reconnect. Rust stays parked.

Known failures

None.

Validates the codegen WS pipeline end-to-end for Python. Consumes the
generated datamaxi/_ws_endpoints.py (WS_CHANNELS/WS_BASE_PATH/WS_AUTH_HEADER)
and datamaxi/_ws_models.py (TickerMessage) — both emitted by datamaxi-codegen
from the backend WS surface (route table + protobuf + Go view structs).

datamaxi/aio/ws.py: AsyncDatamaxiWS + AsyncWSConnection + TickerChannel on the
websockets lib (optional [ws] extra). One connection per channel path; SUBSCRIBE
/UNSUBSCRIBE/PING protocol; app-level PING keepalive (~30s vs the ~90s proxy
idle); reconnect-with-resubscribe; ack filtering; channel-stream routing
(caller filters by msg['s']/['e'] — the wire has no channel tag).

  async with AsyncDatamaxiWS(api_key) as ws:
      async for msg in ws.ticker.subscribe('BTC-USDT@binance', market='spot'):
          ...

Tests (tests/test_ws.py) run a real in-process websockets server: auth header,
path from the generated registry, subscribe protocol, streamed data, ack
filtering, multi-symbol multiplexing, keepalive PING. importorskip-guarded;
websockets added to requirements-test. Generated _ws_*.py excluded from
black/flake8 like _endpoints.py. import datamaxi does not load websockets.

Suite: 205 passed.
Live testing surfaced that when the accepted param list is empty (e.g. the
announcement channel), the server omits `result` and sends just {"id": N}.
The ack filter only matched {"result":..., "id":...}, so {"id": N} leaked
as a data message. Detect an ack as any dict whose keys are a subset of
{"result", "id"} (data payloads always carry other fields — and may
themselves include an "id" token, so presence of "id" alone isn't enough).
Verified live against the announcement channel.
Expand the async WS client from ticker-only to every DataMaxi+ WS data type,
driven by the generated WS_CHANNELS registry. Accessors: ticker (market-keyed),
forex, premium, funding_rate, open_interest, liquidation (subscribe),
liquidation_feed (firehose), announcement / announcement_internal (Pro+).

Generic channel types (Subscription / MarketSubscription / Feed) over the same
AsyncWSConnection transport. Regenerate _ws_endpoints.py + _ws_models.py from
codegen with orderbook excluded (unsupported product, backend removal tracked
in Bisonai/datamaxi-backend#7927) -> 10 channels / 8 models.

Tests: orderbook-excluded, every channel maps to a generated model + a client
accessor, param_format from registry, plus forex-subscribe and feed-stream over
the mock server. Live-verified through the client against prod (ticker,
open_interest, liquidation_feed, premium). Suite: 211 passed.
@martinkersner martinkersner changed the title feat: async WebSocket ticker client (pilot) on the generated WS surface feat: async WebSocket client for all data types (on the generated WS surface) Jul 5, 2026
@martinkersner martinkersner merged commit ec7b859 into main Jul 5, 2026
5 checks passed
@martinkersner martinkersner deleted the feat/ws-ticker-client branch July 5, 2026 02:37
@martinkersner martinkersner self-assigned this Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant