kaq-quant-common 0.2.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kaq_quant_common/__init__.py +0 -0
- kaq_quant_common/api/__init__.py +0 -0
- kaq_quant_common/api/common/__init__.py +1 -0
- kaq_quant_common/api/common/api_interface.py +38 -0
- kaq_quant_common/api/common/auth.py +118 -0
- kaq_quant_common/api/rest/__init__.py +0 -0
- kaq_quant_common/api/rest/api_client_base.py +42 -0
- kaq_quant_common/api/rest/api_server_base.py +135 -0
- kaq_quant_common/api/rest/instruction/helper/order_helper.py +342 -0
- kaq_quant_common/api/rest/instruction/instruction_client.py +86 -0
- kaq_quant_common/api/rest/instruction/instruction_server_base.py +154 -0
- kaq_quant_common/api/rest/instruction/models/__init__.py +17 -0
- kaq_quant_common/api/rest/instruction/models/account.py +49 -0
- kaq_quant_common/api/rest/instruction/models/order.py +248 -0
- kaq_quant_common/api/rest/instruction/models/position.py +70 -0
- kaq_quant_common/api/rest/instruction/models/transfer.py +32 -0
- kaq_quant_common/api/ws/__init__.py +0 -0
- kaq_quant_common/api/ws/exchange/models.py +23 -0
- kaq_quant_common/api/ws/exchange/ws_exchange_client.py +31 -0
- kaq_quant_common/api/ws/exchange/ws_exchange_server.py +440 -0
- kaq_quant_common/api/ws/instruction/__init__.py +0 -0
- kaq_quant_common/api/ws/instruction/ws_instruction_client.py +82 -0
- kaq_quant_common/api/ws/instruction/ws_instruction_server_base.py +139 -0
- kaq_quant_common/api/ws/models.py +46 -0
- kaq_quant_common/api/ws/ws_client_base.py +235 -0
- kaq_quant_common/api/ws/ws_server_base.py +288 -0
- kaq_quant_common/common/__init__.py +0 -0
- kaq_quant_common/common/ddb_table_monitor.py +106 -0
- kaq_quant_common/common/http_monitor.py +69 -0
- kaq_quant_common/common/modules/funding_rate_helper.py +137 -0
- kaq_quant_common/common/modules/limit_order_helper.py +158 -0
- kaq_quant_common/common/modules/limit_order_symbol_monitor.py +76 -0
- kaq_quant_common/common/modules/limit_order_symbol_monitor_group.py +69 -0
- kaq_quant_common/common/monitor_base.py +84 -0
- kaq_quant_common/common/monitor_group.py +97 -0
- kaq_quant_common/common/redis_table_monitor.py +123 -0
- kaq_quant_common/common/statistics/funding_rate_history_statistics.py +208 -0
- kaq_quant_common/common/statistics/kline_history_statistics.py +211 -0
- kaq_quant_common/common/ws_wrapper.py +21 -0
- kaq_quant_common/config/config.yaml +5 -0
- kaq_quant_common/resources/__init__.py +0 -0
- kaq_quant_common/resources/kaq_ddb_pool_stream_read_resources.py +56 -0
- kaq_quant_common/resources/kaq_ddb_stream_init_resources.py +88 -0
- kaq_quant_common/resources/kaq_ddb_stream_read_resources.py +81 -0
- kaq_quant_common/resources/kaq_ddb_stream_write_resources.py +359 -0
- kaq_quant_common/resources/kaq_mysql_init_resources.py +23 -0
- kaq_quant_common/resources/kaq_mysql_resources.py +341 -0
- kaq_quant_common/resources/kaq_postgresql_resources.py +58 -0
- kaq_quant_common/resources/kaq_quant_hive_resources.py +107 -0
- kaq_quant_common/resources/kaq_redis_resources.py +117 -0
- kaq_quant_common/utils/__init__.py +0 -0
- kaq_quant_common/utils/dagster_job_check_utils.py +29 -0
- kaq_quant_common/utils/dagster_utils.py +19 -0
- kaq_quant_common/utils/date_util.py +204 -0
- kaq_quant_common/utils/enums_utils.py +79 -0
- kaq_quant_common/utils/error_utils.py +22 -0
- kaq_quant_common/utils/hash_utils.py +48 -0
- kaq_quant_common/utils/log_time_utils.py +32 -0
- kaq_quant_common/utils/logger_utils.py +97 -0
- kaq_quant_common/utils/mytt_utils.py +372 -0
- kaq_quant_common/utils/signal_utils.py +23 -0
- kaq_quant_common/utils/sqlite_utils.py +169 -0
- kaq_quant_common/utils/uuid_utils.py +5 -0
- kaq_quant_common/utils/yml_utils.py +148 -0
- kaq_quant_common-0.2.12.dist-info/METADATA +66 -0
- kaq_quant_common-0.2.12.dist-info/RECORD +67 -0
- kaq_quant_common-0.2.12.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from kaq_quant_common.api.common.api_interface import ApiInterface, api_method
|
|
5
|
+
from kaq_quant_common.api.rest.instruction.models import InstructionResponseBase
|
|
6
|
+
from kaq_quant_common.api.rest.instruction.models.account import (
|
|
7
|
+
ContractBalanceRequest,
|
|
8
|
+
ContractBalanceResponse,
|
|
9
|
+
)
|
|
10
|
+
from kaq_quant_common.api.rest.instruction.models.order import (
|
|
11
|
+
AllOpenOrdersRequest,
|
|
12
|
+
AllOpenOrdersResponse,
|
|
13
|
+
CancelOrderRequest,
|
|
14
|
+
CancelOrderResponse,
|
|
15
|
+
ChangeLeverageRequest,
|
|
16
|
+
ChangeLeverageResponse,
|
|
17
|
+
ChangeMarginModeRequest,
|
|
18
|
+
ChangeMarginModeResponse,
|
|
19
|
+
ModifyOrderRequest,
|
|
20
|
+
ModifyOrderResponse,
|
|
21
|
+
OrderRequest,
|
|
22
|
+
OrderResponse,
|
|
23
|
+
QueryMarginModeRequest,
|
|
24
|
+
QueryMarginModeResponse,
|
|
25
|
+
)
|
|
26
|
+
from kaq_quant_common.api.rest.instruction.models.position import (
|
|
27
|
+
QueryPositionRequest,
|
|
28
|
+
QueryPositionResponse,
|
|
29
|
+
)
|
|
30
|
+
from kaq_quant_common.api.rest.instruction.models.transfer import (
|
|
31
|
+
TransferRequest,
|
|
32
|
+
TransferResponse,
|
|
33
|
+
)
|
|
34
|
+
from kaq_quant_common.api.ws.ws_server_base import WsServerBase
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WsInstructionServerBase(WsServerBase, ApiInterface, ABC):
|
|
38
|
+
|
|
39
|
+
def __init__(self, host: str = "0.0.0.0", port: int = 8765):
|
|
40
|
+
super().__init__(self, host, port)
|
|
41
|
+
|
|
42
|
+
# 统一处理返回数据
|
|
43
|
+
def _wrap_response(self, rsp: InstructionResponseBase):
|
|
44
|
+
if rsp is not None:
|
|
45
|
+
if rsp.event_time is None:
|
|
46
|
+
rsp.event_time = int(time.time() * 1000)
|
|
47
|
+
return rsp
|
|
48
|
+
|
|
49
|
+
# 下单
|
|
50
|
+
@api_method(OrderRequest, OrderResponse)
|
|
51
|
+
def order(self, request: OrderRequest) -> OrderResponse:
|
|
52
|
+
return self._on_order(request)
|
|
53
|
+
|
|
54
|
+
# 修改订单
|
|
55
|
+
@api_method(ModifyOrderRequest, ModifyOrderResponse)
|
|
56
|
+
def modify_order(self, request: ModifyOrderRequest) -> ModifyOrderResponse:
|
|
57
|
+
return self._on_modify_order(request)
|
|
58
|
+
|
|
59
|
+
# 撤销订单
|
|
60
|
+
@api_method(CancelOrderRequest, CancelOrderResponse)
|
|
61
|
+
def cancel_order(self, request: CancelOrderRequest) -> CancelOrderResponse:
|
|
62
|
+
return self._on_cancel_order(request)
|
|
63
|
+
|
|
64
|
+
# 查询当前全部挂单
|
|
65
|
+
@api_method(AllOpenOrdersRequest, AllOpenOrdersResponse)
|
|
66
|
+
def all_open_orders(self, request: AllOpenOrdersRequest) -> AllOpenOrdersResponse:
|
|
67
|
+
return self._on_all_open_orders(request)
|
|
68
|
+
|
|
69
|
+
# 调整杠杆
|
|
70
|
+
@api_method(ChangeLeverageRequest, ChangeLeverageResponse)
|
|
71
|
+
def change_leverage(self, request: ChangeLeverageRequest) -> ChangeLeverageResponse:
|
|
72
|
+
return self._on_change_leverage(request)
|
|
73
|
+
|
|
74
|
+
# 查询联合保证金模式
|
|
75
|
+
@api_method(QueryMarginModeRequest, QueryMarginModeResponse)
|
|
76
|
+
def query_margin_mode(self, request: QueryMarginModeRequest) -> QueryMarginModeResponse:
|
|
77
|
+
return self._on_query_margin_mode(request)
|
|
78
|
+
|
|
79
|
+
# 修改联合保证金模式
|
|
80
|
+
@api_method(ChangeMarginModeRequest, ChangeMarginModeResponse)
|
|
81
|
+
def change_margin_mode(self, request: ChangeMarginModeRequest) -> ChangeMarginModeResponse:
|
|
82
|
+
return self._on_change_margin_mode(request)
|
|
83
|
+
|
|
84
|
+
# 查询持仓
|
|
85
|
+
@api_method(QueryPositionRequest, QueryPositionResponse)
|
|
86
|
+
def query_position(self, request: QueryPositionRequest) -> QueryPositionResponse:
|
|
87
|
+
return self._on_query_position(request)
|
|
88
|
+
|
|
89
|
+
# 划转
|
|
90
|
+
@api_method(TransferRequest, TransferResponse)
|
|
91
|
+
def transfer(self, request: TransferRequest) -> TransferResponse:
|
|
92
|
+
return self._on_transfer(request)
|
|
93
|
+
|
|
94
|
+
# 查询合约账户余额
|
|
95
|
+
@api_method(ContractBalanceRequest, ContractBalanceResponse)
|
|
96
|
+
def contract_balance(self, request: ContractBalanceRequest) -> ContractBalanceResponse:
|
|
97
|
+
return self._on_contract_balance(request)
|
|
98
|
+
|
|
99
|
+
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ abstract methods
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def _on_order(self, request: OrderRequest) -> OrderResponse:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def _on_modify_order(self, request: ModifyOrderRequest) -> ModifyOrderResponse:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def _on_cancel_order(self, request: CancelOrderRequest) -> CancelOrderResponse:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def _on_all_open_orders(self, request: AllOpenOrdersRequest) -> AllOpenOrdersResponse:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def _on_change_leverage(self, request: ChangeLeverageRequest) -> ChangeLeverageResponse:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
def _on_query_margin_mode(self, request: QueryMarginModeRequest) -> QueryMarginModeResponse:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def _on_change_margin_mode(self, request: ChangeMarginModeRequest) -> ChangeMarginModeResponse:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def _on_query_position(self, request: QueryPositionRequest) -> QueryPositionResponse:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
def _on_transfer(self, request: TransferRequest) -> TransferResponse:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def _on_contract_balance(self, request: ContractBalanceRequest) -> ContractBalanceResponse:
|
|
139
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ws 消息类型
|
|
8
|
+
class WsMessageType(str, Enum):
|
|
9
|
+
# 请求,对应RESPONSE
|
|
10
|
+
REQUEST = "request"
|
|
11
|
+
# 响应,对应REQUEST
|
|
12
|
+
RESPONSE = "response"
|
|
13
|
+
# 推送
|
|
14
|
+
PUSH = "push"
|
|
15
|
+
# 心跳
|
|
16
|
+
PING = "ping"
|
|
17
|
+
PONG = "pong"
|
|
18
|
+
# 订阅
|
|
19
|
+
SUBSCRIBE = "subscribe"
|
|
20
|
+
UNSUBSCRIBE = "unsubscribe"
|
|
21
|
+
#
|
|
22
|
+
ACK = "ack"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WsError(BaseModel):
|
|
26
|
+
code: int
|
|
27
|
+
message: str
|
|
28
|
+
details: Optional[Dict[str, Any]] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WsEnvelope(BaseModel):
|
|
32
|
+
# 请求的类型
|
|
33
|
+
type: WsMessageType
|
|
34
|
+
# 请求/响应相关
|
|
35
|
+
req_id: Optional[str] = None
|
|
36
|
+
method: Optional[str] = None
|
|
37
|
+
# 推送相关
|
|
38
|
+
topic: Optional[str] = None
|
|
39
|
+
# 负载
|
|
40
|
+
payload: Optional[Dict[str, Any]] = None
|
|
41
|
+
# 错误
|
|
42
|
+
error: Optional[WsError] = None
|
|
43
|
+
|
|
44
|
+
def model_dump_json(self) -> str:
|
|
45
|
+
# 简单包装,确保枚举序列化为字符串
|
|
46
|
+
return super().model_dump_json()
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Callable, Dict, Optional, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
import websockets
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from kaq_quant_common.api.ws.models import WsEnvelope, WsMessageType
|
|
9
|
+
from kaq_quant_common.utils import logger_utils, uuid_utils
|
|
10
|
+
from kaq_quant_common.api.common.auth import get_auth_token
|
|
11
|
+
|
|
12
|
+
R = TypeVar("R", bound=BaseModel)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WsClientBase:
|
|
16
|
+
"""
|
|
17
|
+
WebSocket 客户端基类:
|
|
18
|
+
- 管理长连接、自动重连和心跳(基于自定义 ping/pong)
|
|
19
|
+
- 请求/响应关联(req_id -> Future)
|
|
20
|
+
- 订阅主题并接收服务器推送
|
|
21
|
+
提供同步方法封装,便于调用。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, url: str, auto_reconnect: bool = True, token: Optional[str] = None):
|
|
25
|
+
self._url = url
|
|
26
|
+
self._auto_reconnect = auto_reconnect
|
|
27
|
+
self._token = token if token is not None else get_auth_token()
|
|
28
|
+
self._logger = logger_utils.get_logger(self)
|
|
29
|
+
|
|
30
|
+
# 事件循环线程
|
|
31
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
32
|
+
self._loop_thread: Optional[threading.Thread] = None
|
|
33
|
+
|
|
34
|
+
# 连接与控制
|
|
35
|
+
self._ws: Optional[websockets.WebSocketClientProtocol] = None
|
|
36
|
+
self._connected_event = threading.Event()
|
|
37
|
+
self._stop_flag = threading.Event()
|
|
38
|
+
|
|
39
|
+
# 请求映射
|
|
40
|
+
self._pending_requests: Dict[str, asyncio.Future] = {}
|
|
41
|
+
|
|
42
|
+
# 订阅映射:topic -> handler
|
|
43
|
+
self._subscriptions: Dict[str, Callable[[dict], None]] = {}
|
|
44
|
+
|
|
45
|
+
# 重连参数
|
|
46
|
+
self._reconnect_initial = 1.0
|
|
47
|
+
self._reconnect_max = 30.0
|
|
48
|
+
|
|
49
|
+
# ============================== 外部API ==============================
|
|
50
|
+
def connect(self):
|
|
51
|
+
"""启动事件循环与连接任务"""
|
|
52
|
+
if self._loop_thread and self._loop_thread.is_alive():
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# 开一条线程跑事件循环
|
|
56
|
+
def _run_loop():
|
|
57
|
+
self._loop = asyncio.new_event_loop()
|
|
58
|
+
asyncio.set_event_loop(self._loop)
|
|
59
|
+
try:
|
|
60
|
+
self._loop.run_until_complete(self._connect_forever())
|
|
61
|
+
finally:
|
|
62
|
+
try:
|
|
63
|
+
self._loop.close()
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
self._logger.warning("WsClient 事件循环结束")
|
|
67
|
+
|
|
68
|
+
# 循环接收线程,设置为守护进程,主线程关闭就自动关闭
|
|
69
|
+
self._loop_thread = threading.Thread(target=_run_loop, name="WsClientLoop", daemon=True)
|
|
70
|
+
self._loop_thread.start()
|
|
71
|
+
|
|
72
|
+
# 断开连接
|
|
73
|
+
def disconnect(self):
|
|
74
|
+
self._stop_flag.set()
|
|
75
|
+
if self._loop and self._ws:
|
|
76
|
+
|
|
77
|
+
async def _close():
|
|
78
|
+
# 关闭ws
|
|
79
|
+
try:
|
|
80
|
+
await self._ws.close()
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
fut = asyncio.run_coroutine_threadsafe(_close(), self._loop)
|
|
86
|
+
# 尝试快速关闭,若事件循环已结束则忽略超时
|
|
87
|
+
fut.result(timeout=1)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
# 等待线程结束
|
|
91
|
+
if self._loop_thread and self._loop_thread.is_alive():
|
|
92
|
+
self._loop_thread.join(timeout=5)
|
|
93
|
+
|
|
94
|
+
# 订阅,向服务器注册订阅
|
|
95
|
+
def subscribe(self, topic: str, handler: Callable[[dict], None]):
|
|
96
|
+
"""注册订阅,并在连接后自动发送SUBSCRIBE"""
|
|
97
|
+
self._subscriptions[topic] = handler
|
|
98
|
+
if self._loop and self._ws:
|
|
99
|
+
|
|
100
|
+
async def _send_sub():
|
|
101
|
+
# 构造订阅消息
|
|
102
|
+
env = WsEnvelope(type=WsMessageType.SUBSCRIBE, topic=topic)
|
|
103
|
+
await self._ws.send(env.model_dump_json())
|
|
104
|
+
|
|
105
|
+
asyncio.run_coroutine_threadsafe(_send_sub(), self._loop)
|
|
106
|
+
|
|
107
|
+
# 取消订阅
|
|
108
|
+
def unsubscribe(self, topic: str):
|
|
109
|
+
self._subscriptions.pop(topic, None)
|
|
110
|
+
if self._loop and self._ws:
|
|
111
|
+
|
|
112
|
+
async def _send_unsub():
|
|
113
|
+
# 构造取消订阅消息
|
|
114
|
+
env = WsEnvelope(type=WsMessageType.UNSUBSCRIBE, topic=topic)
|
|
115
|
+
# 发送取消订阅消息
|
|
116
|
+
await self._ws.send(env.model_dump_json())
|
|
117
|
+
|
|
118
|
+
asyncio.run_coroutine_threadsafe(_send_unsub(), self._loop)
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
def send_request(self, method: str, request_data: BaseModel, response_model: Type[R], timeout: float = 10.0) -> R:
|
|
122
|
+
"""同步请求封装,等待响应返回"""
|
|
123
|
+
if not self._connected_event.wait(timeout=5.0):
|
|
124
|
+
raise RuntimeError("WS not connected")
|
|
125
|
+
|
|
126
|
+
async def _send_and_wait() -> R:
|
|
127
|
+
# 生成请求id,用来处理响应
|
|
128
|
+
req_id = f"r_{uuid_utils.generate_uuid()}"
|
|
129
|
+
# 在事件循环中创建Future
|
|
130
|
+
fut = self._loop.create_future()
|
|
131
|
+
# 记录正在处理的请求
|
|
132
|
+
self._pending_requests[req_id] = fut
|
|
133
|
+
# 构造请求消息
|
|
134
|
+
env = WsEnvelope(type=WsMessageType.REQUEST, req_id=req_id, method=method, payload=request_data.model_dump())
|
|
135
|
+
# 发送
|
|
136
|
+
await self._ws.send(env.model_dump_json())
|
|
137
|
+
try:
|
|
138
|
+
# 等待执行完毕
|
|
139
|
+
payload: dict = await asyncio.wait_for(fut, timeout=timeout)
|
|
140
|
+
# 构造返回结构
|
|
141
|
+
return response_model(**payload)
|
|
142
|
+
finally:
|
|
143
|
+
# 移除已处理请求
|
|
144
|
+
self._pending_requests.pop(req_id, None)
|
|
145
|
+
|
|
146
|
+
# 提交到事件循环执行
|
|
147
|
+
cfut = asyncio.run_coroutine_threadsafe(_send_and_wait(), self._loop)
|
|
148
|
+
return cfut.result()
|
|
149
|
+
|
|
150
|
+
# ============================== 内部协程 ==============================
|
|
151
|
+
async def _connect_forever(self):
|
|
152
|
+
backoff = self._reconnect_initial
|
|
153
|
+
while not self._stop_flag.is_set():
|
|
154
|
+
try:
|
|
155
|
+
self._logger.info(f"WS connecting to {self._url}")
|
|
156
|
+
headers = {}
|
|
157
|
+
if self._token:
|
|
158
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
159
|
+
self._ws = await websockets.connect(self._url, ping_interval=None, additional_headers=headers or None)
|
|
160
|
+
self._connected_event.set()
|
|
161
|
+
self._logger.info("WS connected")
|
|
162
|
+
# 连接恢复后自动订阅
|
|
163
|
+
await self._resubscribe_all()
|
|
164
|
+
# 启动接收循环
|
|
165
|
+
await self._recv_loop()
|
|
166
|
+
# 正常退出接收循环(例如主动断开)
|
|
167
|
+
if self._stop_flag.is_set():
|
|
168
|
+
break
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self._connected_event.clear()
|
|
171
|
+
self._logger.warning(f"WS 连接失败或中断: {e}")
|
|
172
|
+
if not self._auto_reconnect or self._stop_flag.is_set():
|
|
173
|
+
break
|
|
174
|
+
await asyncio.sleep(backoff)
|
|
175
|
+
backoff = min(backoff * 2, self._reconnect_max)
|
|
176
|
+
|
|
177
|
+
# 退出时确保关闭连接
|
|
178
|
+
try:
|
|
179
|
+
if self._ws:
|
|
180
|
+
await self._ws.close()
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# 重新自动订阅
|
|
185
|
+
async def _resubscribe_all(self):
|
|
186
|
+
for topic in list(self._subscriptions.keys()):
|
|
187
|
+
try:
|
|
188
|
+
env = WsEnvelope(type=WsMessageType.SUBSCRIBE, topic=topic)
|
|
189
|
+
await self._ws.send(env.model_dump_json())
|
|
190
|
+
except Exception as e:
|
|
191
|
+
self._logger.warning(f"自动订阅 {topic} 失败: {e}")
|
|
192
|
+
|
|
193
|
+
# 循环接收
|
|
194
|
+
async def _recv_loop(self):
|
|
195
|
+
assert self._ws is not None
|
|
196
|
+
try:
|
|
197
|
+
async for message in self._ws:
|
|
198
|
+
# 收到消息会进入到这里
|
|
199
|
+
env = None
|
|
200
|
+
try:
|
|
201
|
+
# 校验消息格式是否正确
|
|
202
|
+
env = WsEnvelope.model_validate_json(message)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
self._logger.error(f"解析消息失败: {e}")
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if env.type == WsMessageType.RESPONSE:
|
|
208
|
+
# 对应REQUEST的响应
|
|
209
|
+
req_id = env.req_id or ""
|
|
210
|
+
# 找到对应的请求Future
|
|
211
|
+
fut = self._pending_requests.get(req_id)
|
|
212
|
+
# 如果Future存在且未完成
|
|
213
|
+
if fut and not fut.done():
|
|
214
|
+
if env.error:
|
|
215
|
+
# 异常响应,设置Future异常
|
|
216
|
+
fut.set_exception(RuntimeError(env.error.get("message") if isinstance(env.error, dict) else str(env.error)))
|
|
217
|
+
else:
|
|
218
|
+
# 成功响应,设置Future结果
|
|
219
|
+
fut.set_result(env.payload or {})
|
|
220
|
+
elif env.type == WsMessageType.PUSH:
|
|
221
|
+
# 推送过来的订阅的消息
|
|
222
|
+
topic = env.topic or ""
|
|
223
|
+
# 找到对应订阅处理的函数
|
|
224
|
+
handler = self._subscriptions.get(topic)
|
|
225
|
+
if handler and env.payload is not None:
|
|
226
|
+
# 在后台执行handler,避免阻塞事件循环
|
|
227
|
+
asyncio.create_task(asyncio.to_thread(handler, env.payload))
|
|
228
|
+
elif env.type == WsMessageType.PING:
|
|
229
|
+
# 心跳
|
|
230
|
+
pong = WsEnvelope(type=WsMessageType.PONG)
|
|
231
|
+
await self._ws.send(pong.model_dump_json())
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self._logger.warning(f"WS 接收循环异常: {e}")
|
|
234
|
+
finally:
|
|
235
|
+
self._connected_event.clear()
|