Add CoinEx live websocket stream
Этот коммит содержится в:
@@ -1,5 +1,11 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
|
||||||
class CoinExClient:
|
class CoinExClient:
|
||||||
@@ -23,3 +29,106 @@ class CoinExClient:
|
|||||||
response = await client.get(f'{self.api_base}/spot/ticker', params=params)
|
response = await client.get(f'{self.api_base}/spot/ticker', params=params)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
class CoinExLiveStream:
|
||||||
|
def __init__(self, ws_url: str, rest_client: CoinExClient) -> None:
|
||||||
|
self.ws_url = ws_url
|
||||||
|
self.rest_client = rest_client
|
||||||
|
|
||||||
|
async def trades(self, market: str) -> AsyncIterator[dict[str, Any]]:
|
||||||
|
market = market.upper()
|
||||||
|
try:
|
||||||
|
async for event in self._trades_from_ws(market):
|
||||||
|
yield event
|
||||||
|
except Exception as exc: # noqa: BLE001 - dashboard must keep breathing
|
||||||
|
yield {
|
||||||
|
'type': 'stream_warning',
|
||||||
|
'market': market,
|
||||||
|
'message': f'CoinEx WebSocket unavailable, using HTTP fallback: {exc}',
|
||||||
|
}
|
||||||
|
async for event in self._trades_from_http_fallback(market):
|
||||||
|
yield event
|
||||||
|
|
||||||
|
async def _trades_from_ws(self, market: str) -> AsyncIterator[dict[str, Any]]:
|
||||||
|
subscribe_message = {
|
||||||
|
'id': int(time.time()),
|
||||||
|
'method': 'deals.subscribe',
|
||||||
|
'params': {
|
||||||
|
'market_list': [market],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
async with websockets.connect(self.ws_url, ping_interval=20, ping_timeout=20) as websocket:
|
||||||
|
await websocket.send(json.dumps(subscribe_message))
|
||||||
|
async for raw_message in websocket:
|
||||||
|
message = json.loads(raw_message)
|
||||||
|
if message.get('method') == 'server.ping':
|
||||||
|
await websocket.send(json.dumps({'id': message.get('id'), 'method': 'server.pong', 'params': {}}))
|
||||||
|
continue
|
||||||
|
normalized = self._normalize_deals_message(market, message)
|
||||||
|
if normalized is not None:
|
||||||
|
yield normalized
|
||||||
|
|
||||||
|
async def _trades_from_http_fallback(self, market: str) -> AsyncIterator[dict[str, Any]]:
|
||||||
|
while True:
|
||||||
|
ticker = await self.rest_client.get_ticker(market)
|
||||||
|
price = self._extract_price_from_ticker(ticker)
|
||||||
|
if price is not None:
|
||||||
|
yield {
|
||||||
|
'type': 'live_price',
|
||||||
|
'source': 'coinex_http_fallback',
|
||||||
|
'market': market,
|
||||||
|
'price': price,
|
||||||
|
'ts': int(time.time() * 1000),
|
||||||
|
'raw': ticker,
|
||||||
|
}
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
def _normalize_deals_message(self, market: str, message: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
params = message.get('params')
|
||||||
|
if not params:
|
||||||
|
return None
|
||||||
|
|
||||||
|
deals: list[Any] = []
|
||||||
|
if isinstance(params, dict):
|
||||||
|
if params.get('market') and params.get('market') != market:
|
||||||
|
return None
|
||||||
|
deals = params.get('deal_list') or params.get('deals') or []
|
||||||
|
elif isinstance(params, list):
|
||||||
|
for item in params:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
deals.extend(item.get('deal_list') or item.get('deals') or [item])
|
||||||
|
|
||||||
|
prices: list[float] = []
|
||||||
|
for deal in deals:
|
||||||
|
if not isinstance(deal, dict):
|
||||||
|
continue
|
||||||
|
price_raw = deal.get('price') or deal.get('deal_price')
|
||||||
|
try:
|
||||||
|
prices.append(float(price_raw))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not prices:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'live_price',
|
||||||
|
'source': 'coinex_ws',
|
||||||
|
'market': market,
|
||||||
|
'price': prices[-1],
|
||||||
|
'ts': int(time.time() * 1000),
|
||||||
|
'raw': message,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_price_from_ticker(self, ticker: dict[str, Any]) -> float | None:
|
||||||
|
data = ticker.get('data')
|
||||||
|
if isinstance(data, list) and data:
|
||||||
|
data = data[0]
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
price_raw = data.get('last') or data.get('close') or data.get('last_price')
|
||||||
|
try:
|
||||||
|
return float(price_raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user