#!/usr/bin/env python3 import argparse import asyncio import json import logging import uuid from dataclasses import dataclass from typing import Callable, Coroutine, Dict, List, Optional from numpy.strings import str_len import websockets from websockets.asyncio.client import ClientConnection SubscriptionIdT = str UrlT = str CallbackT = Callable[[Dict], Coroutine[None, str, None]] @dataclass class CvttPricesSubscription: id_: str exchange_config_name_: str instrument_id_: str interval_sec_: int history_depth_sec_: int is_subscribed_: bool is_historical_: bool callback_: CallbackT def __init__( self, exchange_config_name: str, instrument_id: str, interval_sec: int, history_depth_sec: int, callback: CallbackT, ): self.exchange_config_name_ = exchange_config_name self.instrument_id_ = instrument_id self.interval_sec_ = interval_sec self.history_depth_sec_ = history_depth_sec self.callback_ = callback self.id_ = str(uuid.uuid4()) self.is_subscribed_ = False self.is_historical_ = history_depth_sec > 0 class CvttPricerWebSockClient: # Class members with type hints ws_url_: UrlT websocket_: Optional[ClientConnection] subscriptions_: Dict[SubscriptionIdT, CvttPricesSubscription] is_connected_: bool logger_: logging.Logger def __init__(self, url: str): self.ws_url_ = url self.websocket_ = None self.is_connected_ = False self.subscriptions_ = {} self.logger_ = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) async def subscribe( self, subscription: CvttPricesSubscription ) -> str: # returns subscription id if not self.is_connected_: self.logger_.info(f"Connecting to {self.ws_url_}") self.websocket_ = await websockets.connect(self.ws_url_) self.is_connected_ = True else: raise Exception(f"Unable to connect to {self.ws_url_}") subscr_msg = { "type": "subscr", "id": subscription.id_, "subscr_type": "MD_AGGREGATE", "exchange_config_name": subscription.exchange_config_name_, "instrument_id": subscription.instrument_id_, "interval_sec": subscription.interval_sec_, } if subscription.is_historical_: subscr_msg["history_depth_sec"] = subscription.history_depth_sec_ await self.websocket_.send(json.dumps(subscr_msg)) response = await self.websocket_.recv() response_data = json.loads(response) if not await self.handle_subscription_response(subscription, response_data): await self.websocket_.close() self.is_connected_ = False raise Exception(f"Subscription failed: {str(response)}") self.subscriptions_[subscription.id_] = subscription return subscription.id_ async def handle_subscription_response( self, subscription: CvttPricesSubscription, response: dict ) -> bool: if response.get("type") != "subscr" or response.get("id") != subscription.id_: return False if response.get("status") == "success": self.logger_.info(f"Subscription successful: {json.dumps(response)}") return True elif response.get("status") == "error": self.logger_.error(f"Subscription failed: {response.get('reason')}") return False return False async def connect_and_subscribe(self) -> None: assert self.websocket_ try: while self.is_connected_: try: message = await self.websocket_.recv() message_str = ( message.decode("utf-8") if isinstance(message, bytes) else message ) await self.process_message(json.loads(message_str)) except websockets.ConnectionClosed: self.logger_.warning("Connection closed") self.is_connected_ = False break except Exception as e: self.logger_.error(f"Error occurred: {str(e)}") self.is_connected_ = False await asyncio.sleep(5) # Wait before reconnecting async def process_message(self, message: Dict) -> None: if message.get("type") in ["md_aggregate", "historical_md_aggregate"]: subscription_id = message.get("id") if subscription_id not in self.subscriptions_: self.logger_.warning(f"Unknown subscription id: {subscription_id}") return subscription = self.subscriptions_[subscription_id] await subscription.callback_(message) else: self.logger_.warning(f"Unknown message type: {message.get('type')}") async def main() -> None: pass # parser = argparse.ArgumentParser(description="WebSocket API Testing Tool") # parser.add_argument("--url", required=True, help="WebSocket API URL") # parser.add_argument( # "--exchange_config_name", required=True, help="Exchange config name" # ) # parser.add_argument( # "--instrument_ids", required=True, help="Comma separated Instrument IDs" # ) # parser.add_argument( # "--interval_sec", type=int, required=True, help="Interval in seconds" # ) # parser.add_argument( # "--history_depth_sec", # default=0, # type=int, # required=False, # help="History depth in seconds", # ) # args = parser.parse_args() # config = PricerClientConfig( # url_=args.url, # exchange_config_name_=args.exchange_config_name, # instrument_ids_=args.instrument_ids.split(","), # interval_sec_=args.interval_sec, # history_depth_sec_=args.history_depth_sec, # ) # client = CvttPricerWebSockClient(config) # await client.connect_and_subscribe() if __name__ == "__main__": asyncio.run(main())