# app/scripts/check_ohlc_websocket.py from __future__ import annotations import asyncio import json from typing import Any from uuid import uuid4 import websockets from websockets.typing import Subprotocol from src.core.config import load_settings SYMBOLS = ( "BTC/USD_LEVERAGE", ) INTERVALS = ( "1m", "5m", ) CANDLE_TYPE = "classic" MAX_MESSAGES = 30 TOTAL_TIMEOUT_SECONDS = 300.0 RECEIVE_TIMEOUT_SECONDS = 15.0 def build_ws_url() -> str: settings = load_settings() raw_url = settings.exchange_ws_url or settings.exchange_base_url if raw_url.startswith("https://"): raw_url = raw_url.replace("https://", "wss://", 1) elif raw_url.startswith("http://"): raw_url = raw_url.replace("http://", "ws://", 1) raw_url = raw_url.rstrip("/") if raw_url.endswith("/connect"): return raw_url return f"{raw_url}/connect" def build_headers() -> dict[str, str]: settings = load_settings() headers = { "Origin": settings.exchange_base_url.rstrip("/"), "Content-Type": "application/json", } if settings.exchange_api_key: headers["X-MBX-APIKEY"] = settings.exchange_api_key return headers def build_subscribe_request( symbols: tuple[str, ...], intervals: tuple[str, ...], candle_type: str, ) -> dict[str, Any]: return { "correlationId": str(uuid4()), "destination": "OHLCMarketData.subscribe", "payload": { "intervals": list(intervals), "symbols": list(symbols), "type": candle_type, }, } def message_to_text( raw_message: str | bytes | bytearray | memoryview, ) -> str: if isinstance(raw_message, str): return raw_message if isinstance(raw_message, memoryview): return raw_message.tobytes().decode( "utf-8", errors="replace", ) return bytes(raw_message).decode( "utf-8", errors="replace", ) def decode_document( raw_message: str | bytes | bytearray | memoryview, ) -> object | None: text = message_to_text(raw_message) try: return json.loads(text) except json.JSONDecodeError: return None def format_json( raw_message: str | bytes | bytearray | memoryview, ) -> str: text = message_to_text(raw_message) try: document = json.loads(text) except json.JSONDecodeError: return text return json.dumps( document, ensure_ascii=False, indent=2, sort_keys=True, ) def destination_from_document(document: object) -> str | None: if not isinstance(document, dict): return None direct_destination = document.get("destination") if isinstance(direct_destination, str): return direct_destination upper_destination = document.get("Destination") if isinstance(upper_destination, str): return upper_destination payload = document.get("payload") if isinstance(payload, dict): nested_destination = payload.get("destination") if isinstance(nested_destination, str): return nested_destination nested_upper_destination = payload.get("Destination") if isinstance(nested_upper_destination, str): return nested_upper_destination upper_payload = document.get("Payload") if isinstance(upper_payload, dict): nested_destination = upper_payload.get("destination") if isinstance(nested_destination, str): return nested_destination nested_upper_destination = upper_payload.get("Destination") if isinstance(nested_upper_destination, str): return nested_upper_destination return None def is_ohlc_event(document: object) -> bool: destination = destination_from_document(document) if destination is None: return False return destination.lower() == "ohlc.event" def elapsed_seconds( loop: asyncio.AbstractEventLoop, started_at: float, ) -> float: return max(0.0, loop.time() - started_at) async def receive_ohlc() -> None: settings = load_settings() ws_url = build_ws_url() headers = build_headers() request = build_subscribe_request( SYMBOLS, INTERVALS, CANDLE_TYPE, ) print(f"WebSocket URL: {ws_url}") print(f"Symbols: {', '.join(SYMBOLS)}") print(f"Intervals: {', '.join(INTERVALS)}") print(f"Candle type: {CANDLE_TYPE}") print(f"Maximum messages: {MAX_MESSAGES}") print(f"Total timeout: {TOTAL_TIMEOUT_SECONDS:.0f} seconds") print() print("Subscription request:") print( json.dumps( request, ensure_ascii=False, indent=2, ) ) print() async with websockets.connect( ws_url, extra_headers=headers, subprotocols=[Subprotocol("json")], ping_interval=20, ping_timeout=float(settings.exchange_timeout_sec), open_timeout=float(settings.exchange_timeout_sec), close_timeout=float(settings.exchange_timeout_sec), ) as websocket: await websocket.send( json.dumps( request, ensure_ascii=False, ) ) print("Subscription request sent.") print("Waiting for acknowledgement and OHLC events...") print() loop = asyncio.get_running_loop() started_at = loop.time() deadline = started_at + TOTAL_TIMEOUT_SECONDS received_count = 0 ohlc_event_count = 0 timeout_count = 0 while received_count < MAX_MESSAGES: remaining_seconds = deadline - loop.time() if remaining_seconds <= 0: print( "Total timeout reached after " f"{elapsed_seconds(loop, started_at):.1f} seconds." ) break receive_timeout = min( RECEIVE_TIMEOUT_SECONDS, remaining_seconds, ) try: raw_message = await asyncio.wait_for( websocket.recv(), timeout=receive_timeout, ) except asyncio.TimeoutError: timeout_count += 1 print( "No OHLC event yet; connection is alive. " f"Elapsed: {elapsed_seconds(loop, started_at):.1f}s, " f"remaining: {max(0.0, remaining_seconds):.1f}s." ) continue if not isinstance( raw_message, (str, bytes, bytearray, memoryview), ): print( "Skipped unsupported WebSocket message type: " f"{type(raw_message).__name__}" ) continue received_count += 1 document = decode_document(raw_message) if is_ohlc_event(document): ohlc_event_count += 1 message_kind = "OHLC EVENT" else: message_kind = "CONTROL / ACKNOWLEDGEMENT" print("=" * 80) print( f"Message #{received_count} — {message_kind} — " f"elapsed {elapsed_seconds(loop, started_at):.1f}s" ) print(format_json(raw_message)) print() print("=" * 80) print("Diagnostic summary") print(f"Received messages: {received_count}") print(f"OHLC events: {ohlc_event_count}") print(f"Receive timeouts: {timeout_count}") print( "Elapsed time: " f"{elapsed_seconds(loop, started_at):.1f} seconds" ) def main() -> None: try: asyncio.run(receive_ohlc()) except KeyboardInterrupt: print() print("Stopped by user.") except Exception as exc: print() print( "WebSocket diagnostic failed: " f"{type(exc).__name__}: {exc}" ) raise if __name__ == "__main__": main()