|
1 | | -"""Keyed live/integration lane for sporadic WS channels. |
| 1 | +"""Keyed live/integration lane for the real DataMaxi+ WS API. |
2 | 2 |
|
3 | 3 | The offline `test_ws.py` covers the protocol against a local server. These |
4 | | -tests hit the real DataMaxi+ WS API for the two event-driven channels that |
5 | | -only got ack-level live checks — `/liquidation` (subscribe-only) and |
6 | | -`/announcement/listing` (Pro+). Both are sporadic, so a quiet window (no event |
7 | | -in the timeout) is a PASS: the value is that connect + auth + SUBSCRIBE succeed |
8 | | -without raising. Opt-in and deselected from the keyless CI lane via the |
9 | | -`integration` marker; needs DATAMAXI_API_KEY. Skipped when `websockets` absent. |
| 4 | +tests hit prod. Two kinds of channel: |
| 5 | +
|
| 6 | +- Sporadic (event-driven): `/liquidation`, `/announcement/listing`, |
| 7 | + `/funding-rate`, `/open-interest`, and `/forex` (weekends/off-hours). A quiet |
| 8 | + window (no message in the timeout) is a PASS — the value is that connect + |
| 9 | + auth + SUBSCRIBE succeed without raising; when a message does arrive its shape |
| 10 | + is still checked. |
| 11 | +- High-traffic (continuous): `/ticker` and `/premium`. These emit non-stop, so |
| 12 | + a quiet window is a FAIL — the test asserts a real, well-formed data payload |
| 13 | + actually arrives, proving the SDK decodes and yields end-to-end. |
| 14 | +
|
| 15 | +Opt-in and deselected from the keyless CI lane via the `integration` marker; |
| 16 | +needs DATAMAXI_API_KEY. Skipped when `websockets` absent. |
10 | 17 | """ |
11 | 18 |
|
12 | 19 | import asyncio |
|
28 | 35 |
|
29 | 36 | # Sporadic channels may be silent for long stretches; a quiet window is a pass. |
30 | 37 | _QUIET_TIMEOUT = 10.0 |
| 38 | +# High-traffic channels emit continuously, so a quiet window is a FAIL. ticker |
| 39 | +# ticks roughly every ~10s in practice; give headroom so one slow tick can't flake. |
| 40 | +_DATA_TIMEOUT = 30.0 |
| 41 | +# /forex is quiet-tolerant but its first tick can lag (~14s observed); wait a bit |
| 42 | +# longer than _QUIET_TIMEOUT so an arriving tick gets shape-checked, not skipped. |
| 43 | +_FOREX_TIMEOUT = 20.0 |
31 | 44 |
|
32 | 45 |
|
33 | 46 | def _run(coro): |
@@ -76,3 +89,111 @@ async def run(): |
76 | 89 | pytest.skip(f"/announcement/listing rejected (needs Pro+ tier): {exc!r}") |
77 | 90 | if msg is not None: |
78 | 91 | assert isinstance(msg, dict) |
| 92 | + |
| 93 | + |
| 94 | +def test_ws_ticker_yields_live_data(): |
| 95 | + # /ticker (spot) is high-traffic: it emits continuously, so a quiet window is |
| 96 | + # a FAIL, not a pass. Unlike the sporadic channels above, this proves the SDK |
| 97 | + # decodes and yields a real payload end-to-end — connect + auth + SUBSCRIBE + |
| 98 | + # an actual well-formed data message for BTC-USDT@binance within the timeout. |
| 99 | + async def run(): |
| 100 | + async with AsyncDatamaxiWS(api_key=API_KEY, base_url=BASE_URL) as ws: |
| 101 | + stream = await ws.ticker.subscribe("BTC-USDT@binance", market="spot") |
| 102 | + return await asyncio.wait_for(stream.__anext__(), _DATA_TIMEOUT) |
| 103 | + |
| 104 | + msg = _run(run()) |
| 105 | + assert isinstance(msg, dict) |
| 106 | + assert msg["s"] == "BTC-USDT" |
| 107 | + assert "p" in msg |
| 108 | + |
| 109 | + |
| 110 | +def test_ws_ticker_futures_yields_live_data(): |
| 111 | + # /ticker/futures is the futures-market sibling of the spot test above: also |
| 112 | + # high-traffic, so a quiet window is a FAIL. Assert `m == "futures"` too, to |
| 113 | + # prove the futures path (not spot) is what's exercised end-to-end. |
| 114 | + async def run(): |
| 115 | + async with AsyncDatamaxiWS(api_key=API_KEY, base_url=BASE_URL) as ws: |
| 116 | + stream = await ws.ticker.subscribe("BTC-USDT@binance", market="futures") |
| 117 | + return await asyncio.wait_for(stream.__anext__(), _DATA_TIMEOUT) |
| 118 | + |
| 119 | + msg = _run(run()) |
| 120 | + assert isinstance(msg, dict) |
| 121 | + assert msg["s"] == "BTC-USDT" |
| 122 | + assert "p" in msg |
| 123 | + assert msg["m"] == "futures" |
| 124 | + |
| 125 | + |
| 126 | +def test_ws_premium_yields_live_data(): |
| 127 | + # /premium (cross-exchange premium) is high-traffic: crypto premium trades |
| 128 | + # 24/7, so — like ticker — a quiet window is a FAIL. Proves the SDK decodes |
| 129 | + # and yields a real premium payload end-to-end. The `premium` value itself |
| 130 | + # can be null, so assert on stable identity fields, not the value. |
| 131 | + key = "binance:upbit:bitcoin:USDT:KRW:spot:spot" |
| 132 | + |
| 133 | + async def run(): |
| 134 | + async with AsyncDatamaxiWS(api_key=API_KEY, base_url=BASE_URL) as ws: |
| 135 | + stream = await ws.premium.subscribe(key) |
| 136 | + return await asyncio.wait_for(stream.__anext__(), _DATA_TIMEOUT) |
| 137 | + |
| 138 | + msg = _run(run()) |
| 139 | + assert isinstance(msg, dict) |
| 140 | + assert msg["key"] == key |
| 141 | + assert "premium" in msg |
| 142 | + |
| 143 | + |
| 144 | +def test_ws_forex_tolerates_quiet_window(): |
| 145 | + # /forex is continuous during FX market hours but the first tick can lag |
| 146 | + # (~14s observed) and FX spot does not trade weekends — so a quiet window is |
| 147 | + # a legitimate PASS (like the sporadic channels). When a tick does arrive, |
| 148 | + # validate its shape. |
| 149 | + async def run(): |
| 150 | + async with AsyncDatamaxiWS(api_key=API_KEY, base_url=BASE_URL) as ws: |
| 151 | + stream = await ws.forex.subscribe("USD-KRW") |
| 152 | + try: |
| 153 | + return await asyncio.wait_for(stream.__anext__(), _FOREX_TIMEOUT) |
| 154 | + except asyncio.TimeoutError: |
| 155 | + # quiet window (off-hours/weekend): subscribed fine, just no tick |
| 156 | + return None |
| 157 | + |
| 158 | + msg = _run(run()) |
| 159 | + if msg is not None: |
| 160 | + assert isinstance(msg, dict) |
| 161 | + assert msg["s"] == "USD-KRW" |
| 162 | + assert "r" in msg |
| 163 | + |
| 164 | + |
| 165 | +def test_ws_funding_rate_tolerates_quiet_window(): |
| 166 | + # /funding-rate pushes on-change/snapshot — sporadic (0 msgs in a 20s probe), |
| 167 | + # so a quiet window is a PASS. Shape wasn't observable live; when a message |
| 168 | + # does arrive, assert only that it's a non-empty data dict (acks are already |
| 169 | + # filtered by the client's reader). |
| 170 | + async def run(): |
| 171 | + async with AsyncDatamaxiWS(api_key=API_KEY, base_url=BASE_URL) as ws: |
| 172 | + stream = await ws.funding_rate.subscribe("BTC-USDT@binance") |
| 173 | + try: |
| 174 | + return await asyncio.wait_for(stream.__anext__(), _QUIET_TIMEOUT) |
| 175 | + except asyncio.TimeoutError: |
| 176 | + return None |
| 177 | + |
| 178 | + msg = _run(run()) |
| 179 | + if msg is not None: |
| 180 | + assert isinstance(msg, dict) |
| 181 | + assert msg |
| 182 | + |
| 183 | + |
| 184 | +def test_ws_open_interest_tolerates_quiet_window(): |
| 185 | + # /open-interest is sporadic (0 msgs in a 20s probe), so a quiet window is a |
| 186 | + # PASS. Shape wasn't observable live; when a message does arrive, assert only |
| 187 | + # that it's a non-empty data dict (acks are already filtered by the reader). |
| 188 | + async def run(): |
| 189 | + async with AsyncDatamaxiWS(api_key=API_KEY, base_url=BASE_URL) as ws: |
| 190 | + stream = await ws.open_interest.subscribe("BTC-USDT@binance") |
| 191 | + try: |
| 192 | + return await asyncio.wait_for(stream.__anext__(), _QUIET_TIMEOUT) |
| 193 | + except asyncio.TimeoutError: |
| 194 | + return None |
| 195 | + |
| 196 | + msg = _run(run()) |
| 197 | + if msg is not None: |
| 198 | + assert isinstance(msg, dict) |
| 199 | + assert msg |
0 commit comments