From 14c3a94c62996bc2a375a18fb4dec01d2b8423b1 Mon Sep 17 00:00:00 2001 From: "zihao.jiang" Date: Wed, 17 Dec 2025 11:38:31 +0800 Subject: [PATCH 1/8] websocket enhancement --- okx/websocket/WsPrivateAsync.py | 151 +++++++++++++++++++-- okx/websocket/WsPublicAsync.py | 87 ++++++++++-- test/WsPrivateAsyncTest.py | 228 +++++++++++++++++++++++++++++++- test/WsPublicAsyncTest.py | 48 ++++++- 4 files changed, 484 insertions(+), 30 deletions(-) diff --git a/okx/websocket/WsPrivateAsync.py b/okx/websocket/WsPrivateAsync.py index c5359aa..c085c19 100644 --- a/okx/websocket/WsPrivateAsync.py +++ b/okx/websocket/WsPrivateAsync.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import warnings from okx.websocket import WsUtils from okx.websocket.WebSocketFactory import WebSocketFactory @@ -9,7 +10,7 @@ class WsPrivateAsync: - def __init__(self, apiKey, passphrase, secretKey, url, useServerTime): + def __init__(self, apiKey, passphrase, secretKey, url, useServerTime=None, debug=False): self.url = url self.subscriptions = set() self.callback = None @@ -18,28 +19,43 @@ def __init__(self, apiKey, passphrase, secretKey, url, useServerTime): self.apiKey = apiKey self.passphrase = passphrase self.secretKey = secretKey - self.useServerTime = useServerTime + self.useServerTime = False self.websocket = None + self.debug = debug + + # 设置日志级别 + if debug: + logger.setLevel(logging.DEBUG) + + # 废弃 useServerTime 参数警告 + if useServerTime is not None: + warnings.warn("useServerTime parameter is deprecated. Please remove it.", DeprecationWarning) async def connect(self): self.websocket = await self.factory.connect() async def consume(self): async for message in self.websocket: - logger.debug("Received message: {%s}", message) + if self.debug: + logger.debug("Received message: {%s}", message) if self.callback: self.callback(message) - async def subscribe(self, params: list, callback): + async def subscribe(self, params: list, callback, id: str = None): self.callback = callback logRes = await self.login() await asyncio.sleep(5) if logRes: - payload = json.dumps({ + payload_dict = { "op": "subscribe", "args": params - }) + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"subscribe: {payload}") await self.websocket.send(payload) # await self.consume() @@ -50,26 +66,133 @@ async def login(self): passphrase=self.passphrase, secretKey=self.secretKey ) + if self.debug: + logger.debug(f"login: {loginPayload}") await self.websocket.send(loginPayload) return True - async def unsubscribe(self, params: list, callback): + async def unsubscribe(self, params: list, callback, id: str = None): self.callback = callback - payload = json.dumps({ + payload_dict = { "op": "unsubscribe", "args": params - }) - logger.info(f"unsubscribe: {payload}") + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"unsubscribe: {payload}") + else: + logger.info(f"unsubscribe: {payload}") + await self.websocket.send(payload) + + async def send(self, op: str, args: list, callback=None, id: str = None): + """ + 通用发送方法 + :param op: 操作类型 + :param args: 参数列表 + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + payload_dict = { + "op": op, + "args": args + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"send: {payload}") await self.websocket.send(payload) - # for param in params: - # self.subscriptions.discard(param) + + async def place_order(self, args: list, callback=None, id: str = None): + """ + 下单 + :param args: 下单参数列表 + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + await self.send("order", args, id=id) + + async def batch_orders(self, args: list, callback=None, id: str = None): + """ + 批量下单 + :param args: 批量下单参数列表 + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + await self.send("batch-orders", args, id=id) + + async def cancel_order(self, args: list, callback=None, id: str = None): + """ + 撤单 + :param args: 撤单参数列表 + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + await self.send("cancel-order", args, id=id) + + async def batch_cancel_orders(self, args: list, callback=None, id: str = None): + """ + 批量撤单 + :param args: 批量撤单参数列表 + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + await self.send("batch-cancel-orders", args, id=id) + + async def amend_order(self, args: list, callback=None, id: str = None): + """ + 改单 + :param args: 改单参数列表 + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + await self.send("amend-order", args, id=id) + + async def batch_amend_orders(self, args: list, callback=None, id: str = None): + """ + 批量改单 + :param args: 批量改单参数列表 + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + await self.send("batch-amend-orders", args, id=id) + + async def mass_cancel(self, args: list, callback=None, id: str = None): + """ + Mass cancel (批量撤销) + 注意:此方法用于 /ws/v5/business 频道,限速 1次/秒 + :param args: 撤销参数列表,包含 instType 和 instFamily + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + await self.send("mass-cancel", args, id=id) async def stop(self): await self.factory.close() - self.loop.stop() async def start(self): - logger.info("Connecting to WebSocket...") + if self.debug: + logger.debug("Connecting to WebSocket...") + else: + logger.info("Connecting to WebSocket...") await self.connect() self.loop.create_task(self.consume()) diff --git a/okx/websocket/WsPublicAsync.py b/okx/websocket/WsPublicAsync.py index e576d65..997b27c 100644 --- a/okx/websocket/WsPublicAsync.py +++ b/okx/websocket/WsPublicAsync.py @@ -2,53 +2,118 @@ import json import logging +from okx.websocket import WsUtils from okx.websocket.WebSocketFactory import WebSocketFactory logger = logging.getLogger(__name__) class WsPublicAsync: - def __init__(self, url): + def __init__(self, url, apiKey='', passphrase='', secretKey='', debug=False): self.url = url self.subscriptions = set() self.callback = None self.loop = asyncio.get_event_loop() self.factory = WebSocketFactory(url) self.websocket = None + self.debug = debug + # 用于 business 频道的登录凭证 + self.apiKey = apiKey + self.passphrase = passphrase + self.secretKey = secretKey + self.isLoggedIn = False + + # 设置日志级别 + if debug: + logger.setLevel(logging.DEBUG) async def connect(self): self.websocket = await self.factory.connect() async def consume(self): async for message in self.websocket: - logger.debug("Received message: {%s}", message) + if self.debug: + logger.debug("Received message: {%s}", message) if self.callback: self.callback(message) - async def subscribe(self, params: list, callback): + async def login(self): + """ + 登录方法,用于需要登录的 business 频道(如 /ws/v5/business) + """ + if not self.apiKey or not self.secretKey or not self.passphrase: + raise ValueError("apiKey, secretKey and passphrase are required for login") + + loginPayload = WsUtils.initLoginParams( + useServerTime=False, + apiKey=self.apiKey, + passphrase=self.passphrase, + secretKey=self.secretKey + ) + if self.debug: + logger.debug(f"login: {loginPayload}") + await self.websocket.send(loginPayload) + self.isLoggedIn = True + return True + + async def subscribe(self, params: list, callback, id: str = None): self.callback = callback - payload = json.dumps({ + payload_dict = { "op": "subscribe", "args": params - }) + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"subscribe: {payload}") await self.websocket.send(payload) # await self.consume() - async def unsubscribe(self, params: list, callback): + async def unsubscribe(self, params: list, callback, id: str = None): self.callback = callback - payload = json.dumps({ + payload_dict = { "op": "unsubscribe", "args": params - }) - logger.info(f"unsubscribe: {payload}") + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"unsubscribe: {payload}") + else: + logger.info(f"unsubscribe: {payload}") + await self.websocket.send(payload) + + async def send(self, op: str, args: list, callback=None, id: str = None): + """ + 通用发送方法 + :param op: 操作类型 + :param args: 参数列表 + :param callback: 回调函数 + :param id: 可选的请求ID + """ + if callback: + self.callback = callback + payload_dict = { + "op": op, + "args": args + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"send: {payload}") await self.websocket.send(payload) async def stop(self): await self.factory.close() - self.loop.stop() async def start(self): - logger.info("Connecting to WebSocket...") + if self.debug: + logger.debug("Connecting to WebSocket...") + else: + logger.info("Connecting to WebSocket...") await self.connect() self.loop.create_task(self.consume()) diff --git a/test/WsPrivateAsyncTest.py b/test/WsPrivateAsyncTest.py index ba7fcff..f478984 100644 --- a/test/WsPrivateAsyncTest.py +++ b/test/WsPrivateAsyncTest.py @@ -8,13 +8,14 @@ def privateCallback(message): async def main(): + """订阅测试""" url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( apiKey="your apiKey", passphrase="your passphrase", secretKey="your secretKey", url=url, - useServerTime=False + debug=True ) await ws.start() args = [] @@ -33,7 +34,230 @@ async def main(): print("-----------------------------------------unsubscribe all--------------------------------------------") args3 = [arg1, arg3] await ws.unsubscribe(args3, callback=privateCallback) + await asyncio.sleep(1) + await ws.stop() + + +async def test_place_order(): + """ + 测试下单功能 + URL: /ws/v5/private (限速: 60次/秒) + """ + url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" + ws = WsPrivateAsync( + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + url=url, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # 下单参数 + order_args = [{ + "instId": "BTC-USDT", + "tdMode": "cash", + "clOrdId": "client_order_001", + "side": "buy", + "ordType": "limit", + "sz": "0.001", + "px": "30000" + }] + await ws.place_order(order_args, callback=privateCallback, id="order001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_batch_orders(): + """ + 测试批量下单功能 + URL: /ws/v5/private (限速: 60次/秒, 最多20个订单) + """ + url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" + ws = WsPrivateAsync( + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + url=url, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # 批量下单参数 (最多20个) + order_args = [ + { + "instId": "BTC-USDT", + "tdMode": "cash", + "clOrdId": "batch_order_001", + "side": "buy", + "ordType": "limit", + "sz": "0.001", + "px": "30000" + }, + { + "instId": "ETH-USDT", + "tdMode": "cash", + "clOrdId": "batch_order_002", + "side": "buy", + "ordType": "limit", + "sz": "0.01", + "px": "2000" + } + ] + await ws.batch_orders(order_args, callback=privateCallback, id="batchOrder001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_cancel_order(): + """ + 测试撤单功能 + URL: /ws/v5/private (限速: 60次/秒) + """ + url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" + ws = WsPrivateAsync( + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + url=url, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # 撤单参数 (ordId 和 clOrdId 必须传一个) + cancel_args = [{ + "instId": "BTC-USDT", + "ordId": "your_order_id" + # 或者使用 "clOrdId": "client_order_001" + }] + await ws.cancel_order(cancel_args, callback=privateCallback, id="cancel001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_batch_cancel_orders(): + """ + 测试批量撤单功能 + URL: /ws/v5/private (限速: 60次/秒, 最多20个订单) + """ + url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" + ws = WsPrivateAsync( + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + url=url, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + cancel_args = [ + {"instId": "BTC-USDT", "ordId": "order_id_1"}, + {"instId": "ETH-USDT", "ordId": "order_id_2"} + ] + await ws.batch_cancel_orders(cancel_args, callback=privateCallback, id="batchCancel001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_amend_order(): + """ + 测试改单功能 + URL: /ws/v5/private (限速: 60次/秒) + """ + url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" + ws = WsPrivateAsync( + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + url=url, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # 改单参数 + amend_args = [{ + "instId": "BTC-USDT", + "ordId": "your_order_id", + "newSz": "0.002", + "newPx": "31000" + }] + await ws.amend_order(amend_args, callback=privateCallback, id="amend001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_mass_cancel(): + """ + 测试批量撤销功能 + URL: /ws/v5/business (限速: 1次/秒) + 注意: 此功能使用 business 频道 + """ + url = "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999" + ws = WsPrivateAsync( + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + url=url, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # 批量撤销参数 + mass_cancel_args = [{ + "instType": "SPOT", + "instFamily": "BTC-USDT" + }] + await ws.mass_cancel(mass_cancel_args, callback=privateCallback, id="massCancel001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_send_method(): + """测试通用send方法""" + url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" + ws = WsPrivateAsync( + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + url=url, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # 使用通用send方法下单 - 注意要传入callback才能收到响应 + order_args = [{ + "instId": "BTC-USDT", + "tdMode": "cash", + "side": "buy", + "ordType": "limit", + "sz": "0.001", + "px": "30000" + }] + await ws.send("order", order_args, callback=privateCallback, id="send001") + await asyncio.sleep(5) + await ws.stop() if __name__ == '__main__': - asyncio.run(main()) + # asyncio.run(main()) + asyncio.run(test_place_order()) + asyncio.run(test_batch_orders()) + asyncio.run(test_cancel_order()) + asyncio.run(test_batch_cancel_orders()) + asyncio.run(test_amend_order()) + asyncio.run(test_mass_cancel()) # 注意使用 business 频道 + asyncio.run(test_send_method()) diff --git a/test/WsPublicAsyncTest.py b/test/WsPublicAsyncTest.py index 14276a0..8fda306 100644 --- a/test/WsPublicAsyncTest.py +++ b/test/WsPublicAsyncTest.py @@ -8,10 +8,9 @@ def publicCallback(message): async def main(): - # url = "wss://wspap.okex.com:8443/ws/v5/public?brokerId=9999" url = "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999" - ws = WsPublicAsync(url=url) + ws = WsPublicAsync(url=url, debug=True) # 开启debug日志 await ws.start() args = [] arg1 = {"channel": "instruments", "instType": "FUTURES"} @@ -31,7 +30,50 @@ async def main(): print("-----------------------------------------unsubscribe all--------------------------------------------") args3 = [arg1, arg2, arg3] await ws.unsubscribe(args3, publicCallback) + await asyncio.sleep(1) + await ws.stop() + + +async def test_business_channel_with_login(): + """ + 测试 business 频道的登录功能 + business 频道需要登录后才能订阅某些私有数据 + """ + url = "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999" + ws = WsPublicAsync( + url=url, + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + debug=True + ) + await ws.start() + + # 登录 + await ws.login() + await asyncio.sleep(5) + + # 订阅需要登录的频道 + args = [{"channel": "candle1m", "instId": "BTC-USDT"}] + await ws.subscribe(args, publicCallback) + await asyncio.sleep(30) + await ws.stop() + + +async def test_send_method(): + """测试通用send方法""" + url = "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999" + ws = WsPublicAsync(url=url, debug=True) + await ws.start() + + # 使用通用send方法订阅 - 注意要传入callback才能收到响应 + args = [{"channel": "tickers", "instId": "BTC-USDT"}] + await ws.send("subscribe", args, callback=publicCallback, id="send001") + await asyncio.sleep(10) + await ws.stop() if __name__ == '__main__': - asyncio.run(main()) + # asyncio.run(main()) + # asyncio.run(test_business_channel_with_login()) + asyncio.run(test_send_method()) From 94c20702cfd3b9da0774ba73736827bf5512adcf Mon Sep 17 00:00:00 2001 From: "zihao.jiang" Date: Thu, 18 Dec 2025 17:53:20 +0800 Subject: [PATCH 2/8] websocket enhancement --- test/unit/__init__.py | 2 + test/unit/okx/__init__.py | 2 + test/unit/okx/websocket/__init__.py | 2 + .../okx/websocket/test_ws_private_async.py | 585 ++++++++++++++++++ .../okx/websocket/test_ws_public_async.py | 322 ++++++++++ 5 files changed, 913 insertions(+) create mode 100644 test/unit/__init__.py create mode 100644 test/unit/okx/__init__.py create mode 100644 test/unit/okx/websocket/__init__.py create mode 100644 test/unit/okx/websocket/test_ws_private_async.py create mode 100644 test/unit/okx/websocket/test_ws_public_async.py diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 0000000..75f7509 --- /dev/null +++ b/test/unit/__init__.py @@ -0,0 +1,2 @@ +# Unit tests for okx SDK + diff --git a/test/unit/okx/__init__.py b/test/unit/okx/__init__.py new file mode 100644 index 0000000..7e12b04 --- /dev/null +++ b/test/unit/okx/__init__.py @@ -0,0 +1,2 @@ +# Unit tests for okx module + diff --git a/test/unit/okx/websocket/__init__.py b/test/unit/okx/websocket/__init__.py new file mode 100644 index 0000000..b0061bd --- /dev/null +++ b/test/unit/okx/websocket/__init__.py @@ -0,0 +1,2 @@ +# Unit tests for okx.websocket module + diff --git a/test/unit/okx/websocket/test_ws_private_async.py b/test/unit/okx/websocket/test_ws_private_async.py new file mode 100644 index 0000000..e81de0d --- /dev/null +++ b/test/unit/okx/websocket/test_ws_private_async.py @@ -0,0 +1,585 @@ +""" +Unit tests for okx.websocket.WsPrivateAsync module + +Mirrors the structure: okx/websocket/WsPrivateAsync.py -> test/unit/okx/websocket/test_ws_private_async.py +""" +import json +import unittest +import asyncio +import warnings +from unittest.mock import patch, MagicMock, AsyncMock + + +class TestWsPrivateAsyncInit(unittest.TestCase): + """Unit tests for WsPrivateAsync initialization""" + + def test_init_with_required_params(self): + """Test initialization with required parameters only""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory') as mock_factory: + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + + self.assertEqual(ws.apiKey, "test_api_key") + self.assertEqual(ws.passphrase, "test_passphrase") + self.assertEqual(ws.secretKey, "test_secret_key") + self.assertEqual(ws.url, "wss://test.example.com") + self.assertFalse(ws.useServerTime) + self.assertFalse(ws.debug) + mock_factory.assert_called_once_with("wss://test.example.com") + + def test_init_with_debug_enabled(self): + """Test initialization with debug mode enabled""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com", + debug=True + ) + + self.assertTrue(ws.debug) + + def test_init_with_deprecated_useServerTime_shows_warning(self): + """Test that using deprecated useServerTime parameter shows warning""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com", + useServerTime=True + ) + + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[0].category, DeprecationWarning)) + self.assertIn("useServerTime parameter is deprecated", str(w[0].message)) + + def test_init_without_useServerTime_no_warning(self): + """Test that not using useServerTime parameter shows no warning""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + + # No deprecation warning expected + deprecation_warnings = [warning for warning in w if issubclass(warning.category, DeprecationWarning)] + self.assertEqual(len(deprecation_warnings), 0) + + +class TestWsPrivateAsyncSubscribe(unittest.TestCase): + """Unit tests for WsPrivateAsync subscribe method""" + + def test_subscribe_without_id(self): + """Test subscribe without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \ + patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login, \ + patch('okx.websocket.WsPrivateAsync.asyncio.sleep', new_callable=AsyncMock): + + mock_init_login.return_value = '{"op":"login"}' + + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "account", "ccy": "BTC"}] + + async def run_test(): + await ws.subscribe(params, callback) + self.assertEqual(ws.callback, callback) + # Second call should be the subscribe (first is login) + subscribe_call = mock_websocket.send.call_args_list[1] + payload = json.loads(subscribe_call[0][0]) + self.assertEqual(payload["op"], "subscribe") + self.assertEqual(payload["args"], params) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_subscribe_with_id(self): + """Test subscribe with id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \ + patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login, \ + patch('okx.websocket.WsPrivateAsync.asyncio.sleep', new_callable=AsyncMock): + + mock_init_login.return_value = '{"op":"login"}' + + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "account", "ccy": "BTC"}] + + async def run_test(): + await ws.subscribe(params, callback, id="sub001") + # Second call should be the subscribe (first is login) + subscribe_call = mock_websocket.send.call_args_list[1] + payload = json.loads(subscribe_call[0][0]) + self.assertEqual(payload["op"], "subscribe") + self.assertEqual(payload["id"], "sub001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncUnsubscribe(unittest.TestCase): + """Unit tests for WsPrivateAsync unsubscribe method""" + + def test_unsubscribe_without_id(self): + """Test unsubscribe without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "account", "ccy": "BTC"}] + + async def run_test(): + await ws.unsubscribe(params, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "unsubscribe") + self.assertEqual(payload["args"], params) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_unsubscribe_with_id(self): + """Test unsubscribe with id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "account", "ccy": "BTC"}] + + async def run_test(): + await ws.unsubscribe(params, callback, id="unsub001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "unsubscribe") + self.assertEqual(payload["id"], "unsub001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncSend(unittest.TestCase): + """Unit tests for WsPrivateAsync generic send method""" + + def test_send_without_id(self): + """Test generic send method without id""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, callback=callback) + self.assertEqual(ws.callback, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "custom_op") + self.assertEqual(payload["args"], args) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_send_with_id(self): + """Test generic send method with id""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, id="send001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "custom_op") + self.assertEqual(payload["id"], "send001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncOrderMethods(unittest.TestCase): + """Unit tests for WsPrivateAsync order-related methods""" + + def _create_ws_instance(self): + """Helper to create WsPrivateAsync instance with mocked websocket""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + return ws, mock_websocket + + def test_place_order_sends_correct_payload(self): + """Test place_order sends correct operation""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + order_args = [{ + "instId": "BTC-USDT", + "tdMode": "cash", + "side": "buy", + "ordType": "limit", + "sz": "0.001", + "px": "30000" + }] + + async def run_test(): + await ws.place_order(order_args, callback=callback, id="order001") + self.assertEqual(ws.callback, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "order") + self.assertEqual(payload["args"], order_args) + self.assertEqual(payload["id"], "order001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_place_order_without_id(self): + """Test place_order without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + order_args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.place_order(order_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "order") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_orders_sends_correct_payload(self): + """Test batch_orders sends correct operation""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + order_args = [ + {"instId": "BTC-USDT", "side": "buy", "sz": "0.001", "px": "30000"}, + {"instId": "ETH-USDT", "side": "buy", "sz": "0.01", "px": "2000"} + ] + + async def run_test(): + await ws.batch_orders(order_args, callback=callback, id="batch001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-orders") + self.assertEqual(payload["args"], order_args) + self.assertEqual(payload["id"], "batch001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_orders_without_id(self): + """Test batch_orders without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + order_args = [{"instId": "BTC-USDT"}, {"instId": "ETH-USDT"}] + + async def run_test(): + await ws.batch_orders(order_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-orders") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_cancel_order_sends_correct_payload(self): + """Test cancel_order sends correct operation""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + cancel_args = [{"instId": "BTC-USDT", "ordId": "12345"}] + + async def run_test(): + await ws.cancel_order(cancel_args, callback=callback, id="cancel001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "cancel-order") + self.assertEqual(payload["args"], cancel_args) + self.assertEqual(payload["id"], "cancel001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_cancel_order_without_id(self): + """Test cancel_order without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + cancel_args = [{"instId": "BTC-USDT", "ordId": "12345"}] + + async def run_test(): + await ws.cancel_order(cancel_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "cancel-order") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_cancel_orders_sends_correct_payload(self): + """Test batch_cancel_orders sends correct operation""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + cancel_args = [ + {"instId": "BTC-USDT", "ordId": "12345"}, + {"instId": "ETH-USDT", "ordId": "67890"} + ] + + async def run_test(): + await ws.batch_cancel_orders(cancel_args, callback=callback, id="batchCancel001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-cancel-orders") + self.assertEqual(payload["args"], cancel_args) + self.assertEqual(payload["id"], "batchCancel001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_cancel_orders_without_id(self): + """Test batch_cancel_orders without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + cancel_args = [{"instId": "BTC-USDT", "ordId": "12345"}] + + async def run_test(): + await ws.batch_cancel_orders(cancel_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-cancel-orders") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_amend_order_sends_correct_payload(self): + """Test amend_order sends correct operation""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + amend_args = [{ + "instId": "BTC-USDT", + "ordId": "12345", + "newSz": "0.002", + "newPx": "31000" + }] + + async def run_test(): + await ws.amend_order(amend_args, callback=callback, id="amend001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "amend-order") + self.assertEqual(payload["args"], amend_args) + self.assertEqual(payload["id"], "amend001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_amend_order_without_id(self): + """Test amend_order without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + amend_args = [{"instId": "BTC-USDT", "ordId": "12345", "newSz": "0.002"}] + + async def run_test(): + await ws.amend_order(amend_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "amend-order") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_amend_orders_sends_correct_payload(self): + """Test batch_amend_orders sends correct operation""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + amend_args = [ + {"instId": "BTC-USDT", "ordId": "12345", "newSz": "0.002"}, + {"instId": "ETH-USDT", "ordId": "67890", "newPx": "2100"} + ] + + async def run_test(): + await ws.batch_amend_orders(amend_args, callback=callback, id="batchAmend001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-amend-orders") + self.assertEqual(payload["args"], amend_args) + self.assertEqual(payload["id"], "batchAmend001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_amend_orders_without_id(self): + """Test batch_amend_orders without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + amend_args = [{"instId": "BTC-USDT", "ordId": "12345", "newSz": "0.002"}] + + async def run_test(): + await ws.batch_amend_orders(amend_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-amend-orders") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_mass_cancel_sends_correct_payload(self): + """Test mass_cancel sends correct operation""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + mass_cancel_args = [{ + "instType": "SPOT", + "instFamily": "BTC-USDT" + }] + + async def run_test(): + await ws.mass_cancel(mass_cancel_args, callback=callback, id="massCancel001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "mass-cancel") + self.assertEqual(payload["args"], mass_cancel_args) + self.assertEqual(payload["id"], "massCancel001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_mass_cancel_without_id(self): + """Test mass_cancel without id parameter""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): + ws, mock_websocket = self._create_ws_instance() + mass_cancel_args = [{"instType": "SPOT", "instFamily": "BTC-USDT"}] + + async def run_test(): + await ws.mass_cancel(mass_cancel_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "mass-cancel") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncLogin(unittest.TestCase): + """Unit tests for WsPrivateAsync login method""" + + def test_login_calls_init_login_params(self): + """Test login calls WsUtils.initLoginParams with correct parameters""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \ + patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login: + + mock_init_login.return_value = '{"op":"login","args":[...]}' + + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + + async def run_test(): + result = await ws.login() + self.assertTrue(result) + mock_init_login.assert_called_once_with( + useServerTime=False, + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key" + ) + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncStartStop(unittest.TestCase): + """Unit tests for WsPrivateAsync start and stop methods""" + + def test_stop(self): + """Test stop method closes the factory""" + with patch('okx.websocket.WsPrivateAsync.WebSocketFactory') as mock_factory_class: + mock_factory_instance = MagicMock() + mock_factory_instance.close = AsyncMock() + mock_factory_class.return_value = mock_factory_instance + + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url="wss://test.example.com" + ) + + async def run_test(): + await ws.stop() + mock_factory_instance.close.assert_called_once() + + asyncio.get_event_loop().run_until_complete(run_test()) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/okx/websocket/test_ws_public_async.py b/test/unit/okx/websocket/test_ws_public_async.py new file mode 100644 index 0000000..b2b2c8f --- /dev/null +++ b/test/unit/okx/websocket/test_ws_public_async.py @@ -0,0 +1,322 @@ +""" +Unit tests for okx.websocket.WsPublicAsync module + +Mirrors the structure: okx/websocket/WsPublicAsync.py -> test/unit/okx/websocket/test_ws_public_async.py +""" +import json +import unittest +import asyncio +from unittest.mock import patch, MagicMock, AsyncMock + + +class TestWsPublicAsyncInit(unittest.TestCase): + """Unit tests for WsPublicAsync initialization""" + + def test_init_with_url_only(self): + """Test initialization with only url parameter""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + + self.assertEqual(ws.url, "wss://test.example.com") + self.assertEqual(ws.apiKey, '') + self.assertEqual(ws.passphrase, '') + self.assertEqual(ws.secretKey, '') + self.assertFalse(ws.debug) + self.assertFalse(ws.isLoggedIn) + + def test_init_with_credentials(self): + """Test initialization with all credentials for business channel""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync( + url="wss://test.example.com", + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key" + ) + + self.assertEqual(ws.apiKey, "test_api_key") + self.assertEqual(ws.passphrase, "test_passphrase") + self.assertEqual(ws.secretKey, "test_secret_key") + + def test_init_with_debug_enabled(self): + """Test initialization with debug mode enabled""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com", debug=True) + + self.assertTrue(ws.debug) + + def test_init_with_debug_disabled(self): + """Test initialization with debug mode disabled (default)""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com", debug=False) + + self.assertFalse(ws.debug) + + +class TestWsPublicAsyncLogin(unittest.TestCase): + """Unit tests for WsPublicAsync login method""" + + def test_login_without_credentials_raises_error(self): + """Test that login raises ValueError when credentials are missing""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + + async def run_test(): + with self.assertRaises(ValueError) as context: + await ws.login() + self.assertIn("apiKey, secretKey and passphrase are required for login", str(context.exception)) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_login_with_credentials_success(self): + """Test successful login with valid credentials""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory') as mock_factory, \ + patch('okx.websocket.WsPublicAsync.WsUtils.initLoginParams') as mock_init_login: + + mock_init_login.return_value = '{"op":"login","args":[...]}' + + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync( + url="wss://test.example.com", + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + + async def run_test(): + result = await ws.login() + self.assertTrue(result) + self.assertTrue(ws.isLoggedIn) + mock_init_login.assert_called_once_with( + useServerTime=False, + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key" + ) + mock_websocket.send.assert_called_once() + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPublicAsyncSubscribe(unittest.TestCase): + """Unit tests for WsPublicAsync subscribe method""" + + def test_subscribe_without_id(self): + """Test subscribe without id parameter""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "tickers", "instId": "BTC-USDT"}] + + async def run_test(): + await ws.subscribe(params, callback) + self.assertEqual(ws.callback, callback) + mock_websocket.send.assert_called_once() + + # Verify the payload + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "subscribe") + self.assertEqual(payload["args"], params) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_subscribe_with_id(self): + """Test subscribe with id parameter""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "tickers", "instId": "BTC-USDT"}] + + async def run_test(): + await ws.subscribe(params, callback, id="sub001") + + # Verify the payload includes id + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "subscribe") + self.assertEqual(payload["args"], params) + self.assertEqual(payload["id"], "sub001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_subscribe_with_multiple_channels(self): + """Test subscribe with multiple channels""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [ + {"channel": "tickers", "instId": "BTC-USDT"}, + {"channel": "tickers", "instId": "ETH-USDT"} + ] + + async def run_test(): + await ws.subscribe(params, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(len(payload["args"]), 2) + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPublicAsyncUnsubscribe(unittest.TestCase): + """Unit tests for WsPublicAsync unsubscribe method""" + + def test_unsubscribe_without_id(self): + """Test unsubscribe without id parameter""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "tickers", "instId": "BTC-USDT"}] + + async def run_test(): + await ws.unsubscribe(params, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "unsubscribe") + self.assertEqual(payload["args"], params) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_unsubscribe_with_id(self): + """Test unsubscribe with id parameter""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "tickers", "instId": "BTC-USDT"}] + + async def run_test(): + await ws.unsubscribe(params, callback, id="unsub001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "unsubscribe") + self.assertEqual(payload["id"], "unsub001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPublicAsyncSend(unittest.TestCase): + """Unit tests for WsPublicAsync send method""" + + def test_send_without_id(self): + """Test generic send method without id""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, callback=callback) + self.assertEqual(ws.callback, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "custom_op") + self.assertEqual(payload["args"], args) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_send_with_id(self): + """Test generic send method with id""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, id="send001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "custom_op") + self.assertEqual(payload["id"], "send001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_send_without_callback(self): + """Test send method without callback (preserves existing callback)""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + existing_callback = MagicMock() + ws.callback = existing_callback + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args) + # Callback should remain unchanged + self.assertEqual(ws.callback, existing_callback) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_send_with_new_callback_replaces_existing(self): + """Test send method with new callback replaces existing callback""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + old_callback = MagicMock() + new_callback = MagicMock() + ws.callback = old_callback + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, callback=new_callback) + self.assertEqual(ws.callback, new_callback) + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPublicAsyncStartStop(unittest.TestCase): + """Unit tests for WsPublicAsync start and stop methods""" + + def test_stop(self): + """Test stop method closes the factory""" + with patch('okx.websocket.WsPublicAsync.WebSocketFactory') as mock_factory_class: + mock_factory_instance = MagicMock() + mock_factory_instance.close = AsyncMock() + mock_factory_class.return_value = mock_factory_instance + + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url="wss://test.example.com") + + async def run_test(): + await ws.stop() + mock_factory_instance.close.assert_called_once() + + asyncio.get_event_loop().run_until_complete(run_test()) + + +if __name__ == '__main__': + unittest.main() From 61562c4bfff8fdaf83b89b45a23ac5051e2e7e5c Mon Sep 17 00:00:00 2001 From: "zihao.jiang" Date: Fri, 19 Dec 2025 16:58:32 +0800 Subject: [PATCH 3/8] websocket enhancement --- test/WsPrivateAsyncTest.py | 46 +++++++++++++++++++------------------- test/WsPublicAsyncTest.py | 14 ++++++------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/test/WsPrivateAsyncTest.py b/test/WsPrivateAsyncTest.py index f478984..eb7e852 100644 --- a/test/WsPrivateAsyncTest.py +++ b/test/WsPrivateAsyncTest.py @@ -8,7 +8,7 @@ def privateCallback(message): async def main(): - """订阅测试""" + """Subscription test""" url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( apiKey="your apiKey", @@ -40,8 +40,8 @@ async def main(): async def test_place_order(): """ - 测试下单功能 - URL: /ws/v5/private (限速: 60次/秒) + Test place order functionality + URL: /ws/v5/private (Rate limit: 60 requests/second) """ url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( @@ -55,7 +55,7 @@ async def test_place_order(): await ws.login() await asyncio.sleep(5) - # 下单参数 + # Order parameters order_args = [{ "instId": "BTC-USDT", "tdMode": "cash", @@ -72,8 +72,8 @@ async def test_place_order(): async def test_batch_orders(): """ - 测试批量下单功能 - URL: /ws/v5/private (限速: 60次/秒, 最多20个订单) + Test batch orders functionality + URL: /ws/v5/private (Rate limit: 60 requests/second, max 20 orders) """ url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( @@ -87,7 +87,7 @@ async def test_batch_orders(): await ws.login() await asyncio.sleep(5) - # 批量下单参数 (最多20个) + # Batch order parameters (max 20) order_args = [ { "instId": "BTC-USDT", @@ -115,8 +115,8 @@ async def test_batch_orders(): async def test_cancel_order(): """ - 测试撤单功能 - URL: /ws/v5/private (限速: 60次/秒) + Test cancel order functionality + URL: /ws/v5/private (Rate limit: 60 requests/second) """ url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( @@ -130,11 +130,11 @@ async def test_cancel_order(): await ws.login() await asyncio.sleep(5) - # 撤单参数 (ordId 和 clOrdId 必须传一个) + # Cancel order parameters (either ordId or clOrdId must be provided) cancel_args = [{ "instId": "BTC-USDT", "ordId": "your_order_id" - # 或者使用 "clOrdId": "client_order_001" + # Or use "clOrdId": "client_order_001" }] await ws.cancel_order(cancel_args, callback=privateCallback, id="cancel001") await asyncio.sleep(5) @@ -143,8 +143,8 @@ async def test_cancel_order(): async def test_batch_cancel_orders(): """ - 测试批量撤单功能 - URL: /ws/v5/private (限速: 60次/秒, 最多20个订单) + Test batch cancel orders functionality + URL: /ws/v5/private (Rate limit: 60 requests/second, max 20 orders) """ url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( @@ -169,8 +169,8 @@ async def test_batch_cancel_orders(): async def test_amend_order(): """ - 测试改单功能 - URL: /ws/v5/private (限速: 60次/秒) + Test amend order functionality + URL: /ws/v5/private (Rate limit: 60 requests/second) """ url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( @@ -184,7 +184,7 @@ async def test_amend_order(): await ws.login() await asyncio.sleep(5) - # 改单参数 + # Amend order parameters amend_args = [{ "instId": "BTC-USDT", "ordId": "your_order_id", @@ -198,9 +198,9 @@ async def test_amend_order(): async def test_mass_cancel(): """ - 测试批量撤销功能 - URL: /ws/v5/business (限速: 1次/秒) - 注意: 此功能使用 business 频道 + Test mass cancel functionality + URL: /ws/v5/business (Rate limit: 1 request/second) + Note: This function uses the business channel """ url = "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999" ws = WsPrivateAsync( @@ -214,7 +214,7 @@ async def test_mass_cancel(): await ws.login() await asyncio.sleep(5) - # 批量撤销参数 + # Mass cancel parameters mass_cancel_args = [{ "instType": "SPOT", "instFamily": "BTC-USDT" @@ -225,7 +225,7 @@ async def test_mass_cancel(): async def test_send_method(): - """测试通用send方法""" + """Test generic send method""" url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( apiKey="your apiKey", @@ -238,7 +238,7 @@ async def test_send_method(): await ws.login() await asyncio.sleep(5) - # 使用通用send方法下单 - 注意要传入callback才能收到响应 + # Use generic send method to place order - callback must be provided to receive response order_args = [{ "instId": "BTC-USDT", "tdMode": "cash", @@ -259,5 +259,5 @@ async def test_send_method(): asyncio.run(test_cancel_order()) asyncio.run(test_batch_cancel_orders()) asyncio.run(test_amend_order()) - asyncio.run(test_mass_cancel()) # 注意使用 business 频道 + asyncio.run(test_mass_cancel()) # Note: uses business channel asyncio.run(test_send_method()) diff --git a/test/WsPublicAsyncTest.py b/test/WsPublicAsyncTest.py index 8fda306..c3f7be8 100644 --- a/test/WsPublicAsyncTest.py +++ b/test/WsPublicAsyncTest.py @@ -10,7 +10,7 @@ def publicCallback(message): async def main(): # url = "wss://wspap.okex.com:8443/ws/v5/public?brokerId=9999" url = "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999" - ws = WsPublicAsync(url=url, debug=True) # 开启debug日志 + ws = WsPublicAsync(url=url, debug=True) # Enable debug logging await ws.start() args = [] arg1 = {"channel": "instruments", "instType": "FUTURES"} @@ -36,8 +36,8 @@ async def main(): async def test_business_channel_with_login(): """ - 测试 business 频道的登录功能 - business 频道需要登录后才能订阅某些私有数据 + Test business channel login functionality + Business channel requires login to subscribe to certain private data """ url = "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999" ws = WsPublicAsync( @@ -49,11 +49,11 @@ async def test_business_channel_with_login(): ) await ws.start() - # 登录 + # Login await ws.login() await asyncio.sleep(5) - # 订阅需要登录的频道 + # Subscribe to channels that require login args = [{"channel": "candle1m", "instId": "BTC-USDT"}] await ws.subscribe(args, publicCallback) await asyncio.sleep(30) @@ -61,12 +61,12 @@ async def test_business_channel_with_login(): async def test_send_method(): - """测试通用send方法""" + """Test generic send method""" url = "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999" ws = WsPublicAsync(url=url, debug=True) await ws.start() - # 使用通用send方法订阅 - 注意要传入callback才能收到响应 + # Use generic send method to subscribe - callback must be provided to receive response args = [{"channel": "tickers", "instId": "BTC-USDT"}] await ws.send("subscribe", args, callback=publicCallback, id="send001") await asyncio.sleep(10) From b6c3b839fc5d4d51abd8bda3a40e518910e12015 Mon Sep 17 00:00:00 2001 From: "zihao.jiang" Date: Fri, 19 Dec 2025 17:04:59 +0800 Subject: [PATCH 4/8] websocket enhancement --- okx/websocket/WsPublicAsync.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/okx/websocket/WsPublicAsync.py b/okx/websocket/WsPublicAsync.py index d6091f6..1a7e28d 100644 --- a/okx/websocket/WsPublicAsync.py +++ b/okx/websocket/WsPublicAsync.py @@ -37,7 +37,6 @@ async def consume(self): if self.callback: self.callback(message) - async def subscribe(self, params: list, callback, id: str = None): async def login(self): """ 登录方法,用于需要登录的 business 频道(如 /ws/v5/business) @@ -66,10 +65,6 @@ async def subscribe(self, params: list, callback, id: str = None): if id is not None: payload_dict["id"] = id payload = json.dumps(payload_dict) - } - if id is not None: - payload_dict["id"] = id - payload = json.dumps(payload_dict) if self.debug: logger.debug(f"subscribe: {payload}") await self.websocket.send(payload) @@ -84,11 +79,6 @@ async def unsubscribe(self, params: list, callback, id: str = None): if id is not None: payload_dict["id"] = id payload = json.dumps(payload_dict) - logger.info(f"unsubscribe: {payload}") - } - if id is not None: - payload_dict["id"] = id - payload = json.dumps(payload_dict) if self.debug: logger.debug(f"unsubscribe: {payload}") else: From ee3c26378c3c6ae2d5f930e2103fa3516ab05eea Mon Sep 17 00:00:00 2001 From: "zihao.jiang" Date: Mon, 22 Dec 2025 10:16:50 +0800 Subject: [PATCH 5/8] websocket enhancement --- okx/websocket/WsPrivateAsync.py | 72 ++++++++++++++++----------------- okx/websocket/WsPublicAsync.py | 16 ++++---- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/okx/websocket/WsPrivateAsync.py b/okx/websocket/WsPrivateAsync.py index 8ca8edd..b7e328e 100644 --- a/okx/websocket/WsPrivateAsync.py +++ b/okx/websocket/WsPrivateAsync.py @@ -23,11 +23,11 @@ def __init__(self, apiKey, passphrase, secretKey, url, useServerTime=None, debug self.websocket = None self.debug = debug - # 设置日志级别 + # Set log level if debug: logger.setLevel(logging.DEBUG) - # 废弃 useServerTime 参数警告 + # Deprecation warning for useServerTime parameter if useServerTime is not None: warnings.warn("useServerTime parameter is deprecated. Please remove it.", DeprecationWarning) @@ -88,11 +88,11 @@ async def unsubscribe(self, params: list, callback, id: str = None): async def send(self, op: str, args: list, callback=None, id: str = None): """ - 通用发送方法 - :param op: 操作类型 - :param args: 参数列表 - :param callback: 回调函数 - :param id: 可选的请求ID + Generic send method + :param op: Operation type + :param args: Parameter list + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback @@ -109,10 +109,10 @@ async def send(self, op: str, args: list, callback=None, id: str = None): async def place_order(self, args: list, callback=None, id: str = None): """ - 下单 - :param args: 下单参数列表 - :param callback: 回调函数 - :param id: 可选的请求ID + Place order + :param args: Order parameter list + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback @@ -120,10 +120,10 @@ async def place_order(self, args: list, callback=None, id: str = None): async def batch_orders(self, args: list, callback=None, id: str = None): """ - 批量下单 - :param args: 批量下单参数列表 - :param callback: 回调函数 - :param id: 可选的请求ID + Batch place orders + :param args: Batch order parameter list + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback @@ -131,10 +131,10 @@ async def batch_orders(self, args: list, callback=None, id: str = None): async def cancel_order(self, args: list, callback=None, id: str = None): """ - 撤单 - :param args: 撤单参数列表 - :param callback: 回调函数 - :param id: 可选的请求ID + Cancel order + :param args: Cancel order parameter list + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback @@ -142,10 +142,10 @@ async def cancel_order(self, args: list, callback=None, id: str = None): async def batch_cancel_orders(self, args: list, callback=None, id: str = None): """ - 批量撤单 - :param args: 批量撤单参数列表 - :param callback: 回调函数 - :param id: 可选的请求ID + Batch cancel orders + :param args: Batch cancel order parameter list + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback @@ -153,10 +153,10 @@ async def batch_cancel_orders(self, args: list, callback=None, id: str = None): async def amend_order(self, args: list, callback=None, id: str = None): """ - 改单 - :param args: 改单参数列表 - :param callback: 回调函数 - :param id: 可选的请求ID + Amend order + :param args: Amend order parameter list + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback @@ -164,10 +164,10 @@ async def amend_order(self, args: list, callback=None, id: str = None): async def batch_amend_orders(self, args: list, callback=None, id: str = None): """ - 批量改单 - :param args: 批量改单参数列表 - :param callback: 回调函数 - :param id: 可选的请求ID + Batch amend orders + :param args: Batch amend order parameter list + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback @@ -175,11 +175,11 @@ async def batch_amend_orders(self, args: list, callback=None, id: str = None): async def mass_cancel(self, args: list, callback=None, id: str = None): """ - Mass cancel (批量撤销) - 注意:此方法用于 /ws/v5/business 频道,限速 1次/秒 - :param args: 撤销参数列表,包含 instType 和 instFamily - :param callback: 回调函数 - :param id: 可选的请求ID + Mass cancel orders + Note: This method is for /ws/v5/business channel, rate limit: 1 request/second + :param args: Cancel parameter list, contains instType and instFamily + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback diff --git a/okx/websocket/WsPublicAsync.py b/okx/websocket/WsPublicAsync.py index 1a7e28d..d7eefdc 100644 --- a/okx/websocket/WsPublicAsync.py +++ b/okx/websocket/WsPublicAsync.py @@ -17,13 +17,13 @@ def __init__(self, url, apiKey='', passphrase='', secretKey='', debug=False): self.factory = WebSocketFactory(url) self.websocket = None self.debug = debug - # 用于 business 频道的登录凭证 + # Credentials for business channel login self.apiKey = apiKey self.passphrase = passphrase self.secretKey = secretKey self.isLoggedIn = False - # 设置日志级别 + # Set log level if debug: logger.setLevel(logging.DEBUG) @@ -39,7 +39,7 @@ async def consume(self): async def login(self): """ - 登录方法,用于需要登录的 business 频道(如 /ws/v5/business) + Login method for business channel that requires authentication (e.g. /ws/v5/business) """ if not self.apiKey or not self.secretKey or not self.passphrase: raise ValueError("apiKey, secretKey and passphrase are required for login") @@ -87,11 +87,11 @@ async def unsubscribe(self, params: list, callback, id: str = None): async def send(self, op: str, args: list, callback=None, id: str = None): """ - 通用发送方法 - :param op: 操作类型 - :param args: 参数列表 - :param callback: 回调函数 - :param id: 可选的请求ID + Generic send method + :param op: Operation type + :param args: Parameter list + :param callback: Callback function + :param id: Optional request ID """ if callback: self.callback = callback From 4813e7e04163527b7f2252de27c7255881128c40 Mon Sep 17 00:00:00 2001 From: "zihao.jiang" Date: Mon, 22 Dec 2025 10:20:17 +0800 Subject: [PATCH 6/8] websocket enhancement --- test/unit/okx/websocket/test_ws_public_async.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/unit/okx/websocket/test_ws_public_async.py b/test/unit/okx/websocket/test_ws_public_async.py index 9ac561c..ca27b38 100644 --- a/test/unit/okx/websocket/test_ws_public_async.py +++ b/test/unit/okx/websocket/test_ws_public_async.py @@ -111,9 +111,6 @@ async def run_test(): class TestWsPublicAsyncSubscribe(unittest.TestCase): """Unit tests for WsPublicAsync subscribe method""" - def test_subscribe_sets_callback(self): - """Test subscribe sets callback correctly""" - with patch.object(ws_public_module, 'WebSocketFactory'): def test_subscribe_without_id(self): """Test subscribe without id parameter""" with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): From 9dfb5b64cbef3af1da5f7ed85ffd68d12e2dd5c0 Mon Sep 17 00:00:00 2001 From: "zihao.jiang" Date: Mon, 22 Dec 2025 10:45:07 +0800 Subject: [PATCH 7/8] websocket enhancement --- test/test_ws_private_async.py | 49 ++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/test/test_ws_private_async.py b/test/test_ws_private_async.py index fbee060..10f34bf 100644 --- a/test/test_ws_private_async.py +++ b/test/test_ws_private_async.py @@ -51,11 +51,12 @@ async def test_place_order(): Test place order functionality URL: /ws/v5/private (Rate limit: 60 requests/second) """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( - apiKey="your apiKey", - passphrase="your passphrase", - secretKey="your secretKey", + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, url=url, debug=True ) @@ -83,11 +84,12 @@ async def test_batch_orders(): Test batch orders functionality URL: /ws/v5/private (Rate limit: 60 requests/second, max 20 orders) """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( - apiKey="your apiKey", - passphrase="your passphrase", - secretKey="your secretKey", + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, url=url, debug=True ) @@ -126,11 +128,12 @@ async def test_cancel_order(): Test cancel order functionality URL: /ws/v5/private (Rate limit: 60 requests/second) """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( - apiKey="your apiKey", - passphrase="your passphrase", - secretKey="your secretKey", + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, url=url, debug=True ) @@ -154,11 +157,12 @@ async def test_batch_cancel_orders(): Test batch cancel orders functionality URL: /ws/v5/private (Rate limit: 60 requests/second, max 20 orders) """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( - apiKey="your apiKey", - passphrase="your passphrase", - secretKey="your secretKey", + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, url=url, debug=True ) @@ -180,11 +184,12 @@ async def test_amend_order(): Test amend order functionality URL: /ws/v5/private (Rate limit: 60 requests/second) """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( - apiKey="your apiKey", - passphrase="your passphrase", - secretKey="your secretKey", + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, url=url, debug=True ) @@ -210,11 +215,12 @@ async def test_mass_cancel(): URL: /ws/v5/business (Rate limit: 1 request/second) Note: This function uses the business channel """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() url = "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999" ws = WsPrivateAsync( - apiKey="your apiKey", - passphrase="your passphrase", - secretKey="your secretKey", + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, url=url, debug=True ) @@ -234,11 +240,12 @@ async def test_mass_cancel(): async def test_send_method(): """Test generic send method""" + api_key, api_secret_key, passphrase, _ = get_api_credentials() url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" ws = WsPrivateAsync( - apiKey="your apiKey", - passphrase="your passphrase", - secretKey="your secretKey", + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, url=url, debug=True ) From fb237f6a72b6bc448d8a495317c8322a26bc07f2 Mon Sep 17 00:00:00 2001 From: "zihao.jiang" Date: Mon, 22 Dec 2025 10:55:13 +0800 Subject: [PATCH 8/8] websocket enhancement --- test/unit/okx/websocket/test_ws_private_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/okx/websocket/test_ws_private_async.py b/test/unit/okx/websocket/test_ws_private_async.py index 35ea1cc..531f5a8 100644 --- a/test/unit/okx/websocket/test_ws_private_async.py +++ b/test/unit/okx/websocket/test_ws_private_async.py @@ -545,7 +545,7 @@ async def run_test(): result = await ws.login() self.assertTrue(result) mock_ws_utils.initLoginParams.assert_called_once_with( - useServerTime=True, + useServerTime=False, apiKey="test_api_key", passphrase="test_passphrase", secretKey="test_secret_key"