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.
Files changed (67) hide show
  1. kaq_quant_common/__init__.py +0 -0
  2. kaq_quant_common/api/__init__.py +0 -0
  3. kaq_quant_common/api/common/__init__.py +1 -0
  4. kaq_quant_common/api/common/api_interface.py +38 -0
  5. kaq_quant_common/api/common/auth.py +118 -0
  6. kaq_quant_common/api/rest/__init__.py +0 -0
  7. kaq_quant_common/api/rest/api_client_base.py +42 -0
  8. kaq_quant_common/api/rest/api_server_base.py +135 -0
  9. kaq_quant_common/api/rest/instruction/helper/order_helper.py +342 -0
  10. kaq_quant_common/api/rest/instruction/instruction_client.py +86 -0
  11. kaq_quant_common/api/rest/instruction/instruction_server_base.py +154 -0
  12. kaq_quant_common/api/rest/instruction/models/__init__.py +17 -0
  13. kaq_quant_common/api/rest/instruction/models/account.py +49 -0
  14. kaq_quant_common/api/rest/instruction/models/order.py +248 -0
  15. kaq_quant_common/api/rest/instruction/models/position.py +70 -0
  16. kaq_quant_common/api/rest/instruction/models/transfer.py +32 -0
  17. kaq_quant_common/api/ws/__init__.py +0 -0
  18. kaq_quant_common/api/ws/exchange/models.py +23 -0
  19. kaq_quant_common/api/ws/exchange/ws_exchange_client.py +31 -0
  20. kaq_quant_common/api/ws/exchange/ws_exchange_server.py +440 -0
  21. kaq_quant_common/api/ws/instruction/__init__.py +0 -0
  22. kaq_quant_common/api/ws/instruction/ws_instruction_client.py +82 -0
  23. kaq_quant_common/api/ws/instruction/ws_instruction_server_base.py +139 -0
  24. kaq_quant_common/api/ws/models.py +46 -0
  25. kaq_quant_common/api/ws/ws_client_base.py +235 -0
  26. kaq_quant_common/api/ws/ws_server_base.py +288 -0
  27. kaq_quant_common/common/__init__.py +0 -0
  28. kaq_quant_common/common/ddb_table_monitor.py +106 -0
  29. kaq_quant_common/common/http_monitor.py +69 -0
  30. kaq_quant_common/common/modules/funding_rate_helper.py +137 -0
  31. kaq_quant_common/common/modules/limit_order_helper.py +158 -0
  32. kaq_quant_common/common/modules/limit_order_symbol_monitor.py +76 -0
  33. kaq_quant_common/common/modules/limit_order_symbol_monitor_group.py +69 -0
  34. kaq_quant_common/common/monitor_base.py +84 -0
  35. kaq_quant_common/common/monitor_group.py +97 -0
  36. kaq_quant_common/common/redis_table_monitor.py +123 -0
  37. kaq_quant_common/common/statistics/funding_rate_history_statistics.py +208 -0
  38. kaq_quant_common/common/statistics/kline_history_statistics.py +211 -0
  39. kaq_quant_common/common/ws_wrapper.py +21 -0
  40. kaq_quant_common/config/config.yaml +5 -0
  41. kaq_quant_common/resources/__init__.py +0 -0
  42. kaq_quant_common/resources/kaq_ddb_pool_stream_read_resources.py +56 -0
  43. kaq_quant_common/resources/kaq_ddb_stream_init_resources.py +88 -0
  44. kaq_quant_common/resources/kaq_ddb_stream_read_resources.py +81 -0
  45. kaq_quant_common/resources/kaq_ddb_stream_write_resources.py +359 -0
  46. kaq_quant_common/resources/kaq_mysql_init_resources.py +23 -0
  47. kaq_quant_common/resources/kaq_mysql_resources.py +341 -0
  48. kaq_quant_common/resources/kaq_postgresql_resources.py +58 -0
  49. kaq_quant_common/resources/kaq_quant_hive_resources.py +107 -0
  50. kaq_quant_common/resources/kaq_redis_resources.py +117 -0
  51. kaq_quant_common/utils/__init__.py +0 -0
  52. kaq_quant_common/utils/dagster_job_check_utils.py +29 -0
  53. kaq_quant_common/utils/dagster_utils.py +19 -0
  54. kaq_quant_common/utils/date_util.py +204 -0
  55. kaq_quant_common/utils/enums_utils.py +79 -0
  56. kaq_quant_common/utils/error_utils.py +22 -0
  57. kaq_quant_common/utils/hash_utils.py +48 -0
  58. kaq_quant_common/utils/log_time_utils.py +32 -0
  59. kaq_quant_common/utils/logger_utils.py +97 -0
  60. kaq_quant_common/utils/mytt_utils.py +372 -0
  61. kaq_quant_common/utils/signal_utils.py +23 -0
  62. kaq_quant_common/utils/sqlite_utils.py +169 -0
  63. kaq_quant_common/utils/uuid_utils.py +5 -0
  64. kaq_quant_common/utils/yml_utils.py +148 -0
  65. kaq_quant_common-0.2.12.dist-info/METADATA +66 -0
  66. kaq_quant_common-0.2.12.dist-info/RECORD +67 -0
  67. 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()