kaq-quant-common 0.2.13__tar.gz → 0.2.15__tar.gz
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-0.2.13 → kaq_quant_common-0.2.15}/PKG-INFO +1 -1
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/helper/mock_order_helper.py +62 -60
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/helper/order_helper.py +58 -38
- kaq_quant_common-0.2.15/kaq_quant_common/api/rest/instruction/models/loan.py +22 -0
- kaq_quant_common-0.2.15/kaq_quant_common/common/modules/limit_order_helper.py +308 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/pyproject.toml +1 -1
- kaq_quant_common-0.2.13/kaq_quant_common/common/modules/limit_order_helper.py +0 -160
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/README.md +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/common/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/common/api_interface.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/common/auth.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/api_client_base.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/api_server_base.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/helper/commission_helper.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/instruction_client.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/instruction_server_base.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/account.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/order.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/position.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/transfer.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/exchange/models.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/exchange/ws_exchange_client.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/exchange/ws_exchange_server.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/instruction/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/instruction/ws_instruction_client.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/instruction/ws_instruction_server_base.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/models.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/ws_client_base.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/ws_server_base.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/ddb_table_monitor.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/http_monitor.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/modules/funding_rate_helper.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/modules/limit_order_symbol_monitor.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/modules/limit_order_symbol_monitor_group.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/monitor_base.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/monitor_group.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/redis_table_monitor.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/statistics/funding_rate_history_statistics.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/statistics/kline_history_statistics.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/ws_wrapper.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/config/config.yaml +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_ddb_pool_stream_read_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_ddb_stream_init_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_ddb_stream_read_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_ddb_stream_write_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_mysql_init_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_mysql_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_postgresql_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_quant_hive_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_redis_resources.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/__init__.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/dagster_job_check_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/dagster_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/date_util.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/enums_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/error_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/hash_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/log_time_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/logger_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/mytt_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/signal_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/sqlite_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/uuid_utils.py +0 -0
- {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/yml_utils.py +0 -0
|
@@ -37,6 +37,7 @@ class MockOrderHelper:
|
|
|
37
37
|
exchange: str,
|
|
38
38
|
symbol: str,
|
|
39
39
|
position_side,
|
|
40
|
+
lever: int,
|
|
40
41
|
coin_quantity: float,
|
|
41
42
|
usdt_quantity: float,
|
|
42
43
|
open_ins_id: str,
|
|
@@ -44,6 +45,7 @@ class MockOrderHelper:
|
|
|
44
45
|
open_fee: float,
|
|
45
46
|
open_fee_rate: float,
|
|
46
47
|
open_time: int,
|
|
48
|
+
is_spot: bool,
|
|
47
49
|
):
|
|
48
50
|
redis = self._server._redis
|
|
49
51
|
if redis is None:
|
|
@@ -53,6 +55,7 @@ class MockOrderHelper:
|
|
|
53
55
|
"exchange": exchange,
|
|
54
56
|
"symbol": symbol,
|
|
55
57
|
"position_side": position_side.value,
|
|
58
|
+
"lever": lever,
|
|
56
59
|
"coin_quantity": coin_quantity,
|
|
57
60
|
"usdt_quantity": usdt_quantity,
|
|
58
61
|
"open_ins_id": open_ins_id,
|
|
@@ -64,6 +67,7 @@ class MockOrderHelper:
|
|
|
64
67
|
"close_price": 0,
|
|
65
68
|
"close_time": 0,
|
|
66
69
|
"status": PositionStatus.OPEN.value,
|
|
70
|
+
"is_spot": is_spot,
|
|
67
71
|
# 标识模拟
|
|
68
72
|
"is_mock": True,
|
|
69
73
|
}
|
|
@@ -75,6 +79,7 @@ class MockOrderHelper:
|
|
|
75
79
|
exchange: str,
|
|
76
80
|
symbol: str,
|
|
77
81
|
position_side,
|
|
82
|
+
lever: int,
|
|
78
83
|
coin_quantity: float,
|
|
79
84
|
usdt_quantity: float,
|
|
80
85
|
open_ins_id: str,
|
|
@@ -87,6 +92,7 @@ class MockOrderHelper:
|
|
|
87
92
|
close_fee: float,
|
|
88
93
|
close_fee_rate: float,
|
|
89
94
|
close_time: int,
|
|
95
|
+
is_spot: bool,
|
|
90
96
|
):
|
|
91
97
|
redis = self._server._redis
|
|
92
98
|
if redis is None:
|
|
@@ -95,26 +101,21 @@ class MockOrderHelper:
|
|
|
95
101
|
# 先从 redis 读取现有的 position 数据,获取 funding_rate_records 字段
|
|
96
102
|
funding_rate_records = None
|
|
97
103
|
try:
|
|
98
|
-
existing_position_json = redis.client.hget(
|
|
99
|
-
self._redis_key_position, position_id
|
|
100
|
-
)
|
|
104
|
+
existing_position_json = redis.client.hget(self._redis_key_position, position_id)
|
|
101
105
|
if existing_position_json:
|
|
102
106
|
existing_position = json.loads(existing_position_json)
|
|
103
107
|
if existing_position and "funding_rate_records" in existing_position:
|
|
104
|
-
funding_rate_records = existing_position.get(
|
|
105
|
-
"funding_rate_records"
|
|
106
|
-
)
|
|
108
|
+
funding_rate_records = existing_position.get("funding_rate_records")
|
|
107
109
|
except Exception as e:
|
|
108
110
|
# 读取失败不影响后续流程,记录日志
|
|
109
|
-
self._logger.warning(
|
|
110
|
-
f"Failed to get funding_rate_records for position {position_id}: {e}"
|
|
111
|
-
)
|
|
111
|
+
self._logger.warning(f"Failed to get funding_rate_records for position {position_id}: {e}")
|
|
112
112
|
|
|
113
113
|
data = {
|
|
114
114
|
"id": position_id,
|
|
115
115
|
"exchange": exchange,
|
|
116
116
|
"symbol": symbol,
|
|
117
117
|
"position_side": position_side.value,
|
|
118
|
+
"lever": lever,
|
|
118
119
|
"coin_quantity": coin_quantity,
|
|
119
120
|
"usdt_quantity": usdt_quantity,
|
|
120
121
|
"open_ins_id": open_ins_id,
|
|
@@ -128,6 +129,7 @@ class MockOrderHelper:
|
|
|
128
129
|
"close_fee_rate": close_fee_rate,
|
|
129
130
|
"close_time": close_time,
|
|
130
131
|
"status": PositionStatus.CLOSE.value,
|
|
132
|
+
"is_spot": is_spot,
|
|
131
133
|
# 标识模拟
|
|
132
134
|
"is_mock": True,
|
|
133
135
|
}
|
|
@@ -178,6 +180,9 @@ class MockOrderHelper:
|
|
|
178
180
|
symbol = order.symbol
|
|
179
181
|
side = order.side
|
|
180
182
|
position_side = order.position_side
|
|
183
|
+
lever = order.level
|
|
184
|
+
# TODO
|
|
185
|
+
is_spot = False
|
|
181
186
|
|
|
182
187
|
is_open = True
|
|
183
188
|
side_str = "开仓"
|
|
@@ -198,9 +203,7 @@ class MockOrderHelper:
|
|
|
198
203
|
side_str = "平仓"
|
|
199
204
|
is_open = False
|
|
200
205
|
|
|
201
|
-
self._logger.info(
|
|
202
|
-
f"[MOCK] {ins_id}_{exchange}_{symbol} step 1. {side_str}模拟挂单 {order_id}"
|
|
203
|
-
)
|
|
206
|
+
self._logger.info(f"[MOCK] {ins_id}_{exchange}_{symbol} step 1. {"现货" if is_spot else "合约"}{side_str}模拟挂单 {order_id}")
|
|
204
207
|
|
|
205
208
|
# 步骤1.挂单成功 插入到订单记录
|
|
206
209
|
current_time = int(time.time() * 1000)
|
|
@@ -208,8 +211,8 @@ class MockOrderHelper:
|
|
|
208
211
|
if mysql is not None:
|
|
209
212
|
status = OrderStatus.CREATE
|
|
210
213
|
sql = f"""
|
|
211
|
-
INSERT INTO {self._mysql_table_name_order} (ins_id, exchange, symbol, side, position_side, orig_price, orig_coin_quantity, order_id, status, create_time, last_update_time, is_mock)
|
|
212
|
-
VALUES ( '{ins_id}', '{exchange}', '{symbol}', '{side.value}', '{order.position_side.value}', {order.current_price or order.target_price}, {order.quantity}, '{order_id}', '{status.value}', {current_time}, {current_time}, 1 );
|
|
214
|
+
INSERT INTO {self._mysql_table_name_order} (ins_id, exchange, symbol, side, position_side, lever, orig_price, orig_coin_quantity, order_id, status, create_time, last_update_time, is_spot, is_mock)
|
|
215
|
+
VALUES ( '{ins_id}', '{exchange}', '{symbol}', '{side.value}', '{order.position_side.value}', '{lever}', {order.current_price or order.target_price}, {order.quantity}, '{order_id}', '{status.value}', {current_time}, {current_time}, '{1 if is_spot else 0}', 1 );
|
|
213
216
|
"""
|
|
214
217
|
execute_ret = mysql.execute_sql(sql, True)
|
|
215
218
|
|
|
@@ -229,17 +232,13 @@ class MockOrderHelper:
|
|
|
229
232
|
end_time = time.time()
|
|
230
233
|
cost_time = end_time - start_time
|
|
231
234
|
|
|
232
|
-
self._logger.info(
|
|
233
|
-
f"[MOCK] {ins_id}_{exchange}_{symbol} step 2. {side_str}模拟订单 {order_id} 成交 耗时 {int(cost_time * 1000)}ms"
|
|
234
|
-
)
|
|
235
|
+
self._logger.info(f"[MOCK] {ins_id}_{exchange}_{symbol} step 2. {side_str}模拟订单 {order_id} 成交 耗时 {int(cost_time * 1000)}ms")
|
|
235
236
|
|
|
236
237
|
# 步骤3.把最终持仓写进去
|
|
237
238
|
current_time = int(time.time() * 1000)
|
|
238
239
|
|
|
239
240
|
if mysql is None:
|
|
240
|
-
self._logger.warning(
|
|
241
|
-
f"[MOCK] {ins_id}_{exchange}_{symbol} 仅操作,没有入库,请设置 mysql!!"
|
|
242
|
-
)
|
|
241
|
+
self._logger.warning(f"[MOCK] {ins_id}_{exchange}_{symbol} 仅操作,没有入库,请设置 mysql!!")
|
|
243
242
|
return
|
|
244
243
|
|
|
245
244
|
status = OrderStatus.FINISH
|
|
@@ -252,34 +251,34 @@ class MockOrderHelper:
|
|
|
252
251
|
execute_ret = mysql.execute_sql(sql, True)
|
|
253
252
|
|
|
254
253
|
self._logger.info(
|
|
255
|
-
f"[MOCK] {ins_id}_{exchange}_{symbol} step 2. 模拟订单成交 {order_id}, {side_str}价格 {avg_price}, {side_str}数量 {executed_qty}, {side_str}usdt {executed_usdt}"
|
|
254
|
+
f"[MOCK] {ins_id}_{exchange}_{symbol} step 2. 模拟订单成交 {order_id}, {side_str}价格 {avg_price}, {side_str}数量 {executed_qty}, {side_str}usdt {executed_usdt} 杠杆 {lever}"
|
|
256
255
|
)
|
|
257
256
|
|
|
258
257
|
if is_open:
|
|
259
258
|
# 同时插入持仓表
|
|
260
259
|
position_id = uuid_utils.generate_uuid()
|
|
261
260
|
sql = f"""
|
|
262
|
-
INSERT INTO {self._mysql_table_name_position} (id, exchange, symbol, position_side, coin_quantity, usdt_quantity, open_ins_id, open_price, open_fee, open_fee_rate, open_time, status, is_mock)
|
|
263
|
-
VALUES ( '{position_id}', '{exchange}', '{symbol}', '{position_side.value}', '{executed_qty}', '{executed_usdt}', '{ins_id}', '{avg_price}', '{fee}', '{fee_rate}', {current_time}, '{PositionStatus.OPEN.value}', 1 );
|
|
261
|
+
INSERT INTO {self._mysql_table_name_position} (id, exchange, symbol, position_side, lever, coin_quantity, usdt_quantity, open_ins_id, open_price, open_fee, open_fee_rate, open_time, status, is_spot, is_mock)
|
|
262
|
+
VALUES ( '{position_id}', '{exchange}', '{symbol}', '{position_side.value}', '{lever}', '{executed_qty}', '{executed_usdt}', '{ins_id}', '{avg_price}', '{fee}', '{fee_rate}', {current_time}, '{PositionStatus.OPEN.value}', '{1 if is_spot else 0}', 1 );
|
|
264
263
|
"""
|
|
265
264
|
execute_ret = mysql.execute_sql(sql, True)
|
|
266
265
|
|
|
267
|
-
self._logger.info(
|
|
268
|
-
f"[MOCK] {ins_id}_{exchange}_{symbol} step 3. 创建持仓记录 {position_id}"
|
|
269
|
-
)
|
|
266
|
+
self._logger.info(f"[MOCK] {ins_id}_{exchange}_{symbol} step 3. 创建持仓记录 {position_id}")
|
|
270
267
|
try:
|
|
271
268
|
self._write_position_open_to_redis(
|
|
272
|
-
position_id,
|
|
273
|
-
exchange,
|
|
274
|
-
symbol,
|
|
275
|
-
position_side,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
269
|
+
position_id=position_id,
|
|
270
|
+
exchange=exchange,
|
|
271
|
+
symbol=symbol,
|
|
272
|
+
position_side=position_side,
|
|
273
|
+
lever=lever,
|
|
274
|
+
coin_quantity=executed_qty,
|
|
275
|
+
usdt_quantity=executed_usdt,
|
|
276
|
+
open_ins_id=ins_id,
|
|
277
|
+
open_price=avg_price,
|
|
278
|
+
open_fee=fee,
|
|
279
|
+
open_fee_rate=fee_rate,
|
|
280
|
+
open_time=current_time,
|
|
281
|
+
is_spot=is_spot,
|
|
283
282
|
)
|
|
284
283
|
except:
|
|
285
284
|
pass
|
|
@@ -287,7 +286,12 @@ class MockOrderHelper:
|
|
|
287
286
|
# 需要找到对应的持仓记录
|
|
288
287
|
sql = f"""
|
|
289
288
|
SELECT * FROM {self._mysql_table_name_position}
|
|
290
|
-
WHERE exchange = '{exchange}'
|
|
289
|
+
WHERE exchange = '{exchange}'
|
|
290
|
+
AND symbol = '{symbol}'
|
|
291
|
+
AND position_side = '{position_side.value}'
|
|
292
|
+
AND status = '{PositionStatus.OPEN.value}'
|
|
293
|
+
AND is_spot = '{1 if is_spot else 0}'
|
|
294
|
+
AND is_mock = 1
|
|
291
295
|
ORDER BY open_time ASC;
|
|
292
296
|
"""
|
|
293
297
|
|
|
@@ -297,9 +301,7 @@ class MockOrderHelper:
|
|
|
297
301
|
SELECT * FROM {self._mysql_table_name_position}
|
|
298
302
|
WHERE id = '{order.position_id}' AND status = '{PositionStatus.OPEN.value}' AND is_mock = 1
|
|
299
303
|
"""
|
|
300
|
-
self._logger.info(
|
|
301
|
-
f"[MOCK] {ins_id}_{exchange}_{symbol} get position by id {order.position_id}"
|
|
302
|
-
)
|
|
304
|
+
self._logger.info(f"[MOCK] {ins_id}_{exchange}_{symbol} get position by id {order.position_id}")
|
|
303
305
|
|
|
304
306
|
execute_ret = mysql.execute_sql(sql)
|
|
305
307
|
try:
|
|
@@ -314,27 +316,27 @@ class MockOrderHelper:
|
|
|
314
316
|
"""
|
|
315
317
|
execute_ret = mysql.execute_sql(sql, True)
|
|
316
318
|
|
|
317
|
-
self._logger.info(
|
|
318
|
-
f"[MOCK] {ins_id}_{exchange}_{symbol} step 3. 更新持仓记录 {position_id}"
|
|
319
|
-
)
|
|
319
|
+
self._logger.info(f"[MOCK] {ins_id}_{exchange}_{symbol} step 3. 更新持仓记录 {position_id}")
|
|
320
320
|
try:
|
|
321
321
|
self._write_position_close_to_redis(
|
|
322
|
-
position_id,
|
|
323
|
-
exchange,
|
|
324
|
-
symbol,
|
|
325
|
-
position_side,
|
|
326
|
-
|
|
327
|
-
float(row.
|
|
328
|
-
row.
|
|
329
|
-
|
|
330
|
-
float(row.
|
|
331
|
-
float(row.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
322
|
+
position_id=position_id,
|
|
323
|
+
exchange=exchange,
|
|
324
|
+
symbol=symbol,
|
|
325
|
+
position_side=position_side,
|
|
326
|
+
lever=row.lever,
|
|
327
|
+
coin_quantity=float(row.coin_quantity),
|
|
328
|
+
usdt_quantity=float(row.usdt_quantity),
|
|
329
|
+
open_ins_id=row.open_ins_id,
|
|
330
|
+
open_price=float(row.open_price),
|
|
331
|
+
open_fee=float(row.open_fee),
|
|
332
|
+
open_fee_rate=float(row.open_fee_rate),
|
|
333
|
+
open_time=int(row.open_time),
|
|
334
|
+
close_ins_id=ins_id,
|
|
335
|
+
close_price=avg_price,
|
|
336
|
+
close_fee=fee,
|
|
337
|
+
close_fee_rate=fee_rate,
|
|
338
|
+
close_time=current_time,
|
|
339
|
+
is_spot=row.is_spot,
|
|
338
340
|
)
|
|
339
341
|
except:
|
|
340
342
|
pass
|
|
@@ -36,6 +36,7 @@ class OrderHelper:
|
|
|
36
36
|
exchange: str,
|
|
37
37
|
symbol: str,
|
|
38
38
|
position_side,
|
|
39
|
+
lever: int,
|
|
39
40
|
coin_quantity: float,
|
|
40
41
|
usdt_quantity: float,
|
|
41
42
|
open_ins_id: str,
|
|
@@ -43,6 +44,7 @@ class OrderHelper:
|
|
|
43
44
|
open_fee: float,
|
|
44
45
|
open_fee_rate: float,
|
|
45
46
|
open_time: int,
|
|
47
|
+
is_spot: bool,
|
|
46
48
|
):
|
|
47
49
|
redis = self._server._redis
|
|
48
50
|
if redis is None:
|
|
@@ -52,6 +54,7 @@ class OrderHelper:
|
|
|
52
54
|
"exchange": exchange,
|
|
53
55
|
"symbol": symbol,
|
|
54
56
|
"position_side": position_side.value,
|
|
57
|
+
"lever": lever,
|
|
55
58
|
"coin_quantity": coin_quantity,
|
|
56
59
|
"usdt_quantity": usdt_quantity,
|
|
57
60
|
"open_ins_id": open_ins_id,
|
|
@@ -63,6 +66,7 @@ class OrderHelper:
|
|
|
63
66
|
"close_price": 0,
|
|
64
67
|
"close_time": 0,
|
|
65
68
|
"status": PositionStatus.OPEN.value,
|
|
69
|
+
"is_spot": is_spot,
|
|
66
70
|
}
|
|
67
71
|
redis.client.hset(self._redis_key_position, position_id, json.dumps(data))
|
|
68
72
|
|
|
@@ -72,6 +76,7 @@ class OrderHelper:
|
|
|
72
76
|
exchange: str,
|
|
73
77
|
symbol: str,
|
|
74
78
|
position_side,
|
|
79
|
+
lever: int,
|
|
75
80
|
coin_quantity: float,
|
|
76
81
|
usdt_quantity: float,
|
|
77
82
|
open_ins_id: str,
|
|
@@ -84,11 +89,12 @@ class OrderHelper:
|
|
|
84
89
|
close_fee: float,
|
|
85
90
|
close_fee_rate: float,
|
|
86
91
|
close_time: int,
|
|
92
|
+
is_spot: bool,
|
|
87
93
|
):
|
|
88
94
|
redis = self._server._redis
|
|
89
95
|
if redis is None:
|
|
90
96
|
return
|
|
91
|
-
|
|
97
|
+
|
|
92
98
|
# 先从 redis 读取现有的 position 数据,获取 funding_rate_records 字段
|
|
93
99
|
funding_rate_records = None
|
|
94
100
|
try:
|
|
@@ -100,12 +106,13 @@ class OrderHelper:
|
|
|
100
106
|
except Exception as e:
|
|
101
107
|
# 读取失败不影响后续流程,记录日志
|
|
102
108
|
self._logger.warning(f"Failed to get funding_rate_records for position {position_id}: {e}")
|
|
103
|
-
|
|
109
|
+
|
|
104
110
|
data = {
|
|
105
111
|
"id": position_id,
|
|
106
112
|
"exchange": exchange,
|
|
107
113
|
"symbol": symbol,
|
|
108
114
|
"position_side": position_side.value,
|
|
115
|
+
"lever": lever,
|
|
109
116
|
"coin_quantity": coin_quantity,
|
|
110
117
|
"usdt_quantity": usdt_quantity,
|
|
111
118
|
"open_ins_id": open_ins_id,
|
|
@@ -119,12 +126,13 @@ class OrderHelper:
|
|
|
119
126
|
"close_fee_rate": close_fee_rate,
|
|
120
127
|
"close_time": close_time,
|
|
121
128
|
"status": PositionStatus.CLOSE.value,
|
|
129
|
+
"is_spot": is_spot,
|
|
122
130
|
}
|
|
123
|
-
|
|
131
|
+
|
|
124
132
|
# 如果存在 funding_rate_records,添加到 data 中
|
|
125
133
|
if funding_rate_records is not None:
|
|
126
134
|
data["funding_rate_records"] = funding_rate_records
|
|
127
|
-
|
|
135
|
+
|
|
128
136
|
redis.client.hdel(self._redis_key_position, position_id)
|
|
129
137
|
redis.client.rpush(self._redis_key_position_history, json.dumps(data))
|
|
130
138
|
|
|
@@ -163,6 +171,9 @@ class OrderHelper:
|
|
|
163
171
|
symbol = order.symbol
|
|
164
172
|
side = order.side
|
|
165
173
|
position_side = order.position_side
|
|
174
|
+
lever = order.level
|
|
175
|
+
# TODO
|
|
176
|
+
is_spot = False
|
|
166
177
|
|
|
167
178
|
is_open = True
|
|
168
179
|
side_str = "开仓"
|
|
@@ -184,7 +195,7 @@ class OrderHelper:
|
|
|
184
195
|
is_open = False
|
|
185
196
|
|
|
186
197
|
if first:
|
|
187
|
-
self._logger.info(f"{ins_id}_{exchange}_{symbol} step 1. {side_str}挂单成功 {order_id}")
|
|
198
|
+
self._logger.info(f"{ins_id}_{exchange}_{symbol} step 1. {"现货" if is_spot else "合约"}{side_str}挂单成功 {order_id}")
|
|
188
199
|
|
|
189
200
|
# 步骤1.挂单成功 插入到订单记录
|
|
190
201
|
# 获取当前时间-ms
|
|
@@ -193,8 +204,8 @@ class OrderHelper:
|
|
|
193
204
|
if mysql is not None:
|
|
194
205
|
status = OrderStatus.CREATE
|
|
195
206
|
sql = f"""
|
|
196
|
-
INSERT INTO {self._mysql_table_name_order} (ins_id, exchange, symbol, side, position_side, orig_price, orig_coin_quantity, order_id, status, create_time, last_update_time)
|
|
197
|
-
VALUES ( '{ins_id}', '{exchange}', '{symbol}', '{side.value}', '{order.position_side.value}', {order.current_price or order.target_price}, {order.quantity}, '{order_id}', '{status.value}', {current_time}, {current_time} );
|
|
207
|
+
INSERT INTO {self._mysql_table_name_order} (ins_id, exchange, symbol, side, position_side, lever, orig_price, orig_coin_quantity, order_id, status, create_time, last_update_time, is_spot)
|
|
208
|
+
VALUES ( '{ins_id}', '{exchange}', '{symbol}', '{side.value}', '{order.position_side.value}', '{lever}', {order.current_price or order.target_price}, {order.quantity}, '{order_id}', '{status.value}', {current_time}, {current_time}, '{1 if is_spot else 0}' );
|
|
198
209
|
"""
|
|
199
210
|
execute_ret = mysql.execute_sql(sql, True)
|
|
200
211
|
|
|
@@ -257,31 +268,33 @@ class OrderHelper:
|
|
|
257
268
|
execute_ret = mysql.execute_sql(sql, True)
|
|
258
269
|
|
|
259
270
|
self._logger.info(
|
|
260
|
-
f"{ins_id}_{exchange}_{symbol} step 2. 订单成交 {order_id}, {side_str}价格 {avg_price}, {side_str}数量 {executed_qty}, {side_str}usdt {executed_usdt}"
|
|
271
|
+
f"{ins_id}_{exchange}_{symbol} step 2. 订单成交 {order_id}, {side_str}价格 {avg_price}, {side_str}数量 {executed_qty}, {side_str}usdt {executed_usdt} 杠杆 {lever}"
|
|
261
272
|
)
|
|
262
273
|
if is_open:
|
|
263
274
|
# 同时插入持仓表
|
|
264
275
|
position_id = uuid_utils.generate_uuid()
|
|
265
276
|
sql = f"""
|
|
266
|
-
INSERT INTO {self._mysql_table_name_position} (id, exchange, symbol, position_side, coin_quantity, usdt_quantity, open_ins_id, open_price, open_fee, open_fee_rate, open_time, status)
|
|
267
|
-
VALUES ( '{position_id}', '{exchange}', '{symbol}', '{position_side.value}', '{executed_qty}', '{executed_usdt}', '{ins_id}', '{avg_price}', '{fee}', '{fee_rate}', {current_time}, '{PositionStatus.OPEN.value}' );
|
|
277
|
+
INSERT INTO {self._mysql_table_name_position} (id, exchange, symbol, position_side, lever, coin_quantity, usdt_quantity, open_ins_id, open_price, open_fee, open_fee_rate, open_time, status, is_spot)
|
|
278
|
+
VALUES ( '{position_id}', '{exchange}', '{symbol}', '{position_side.value}', '{lever}', '{executed_qty}', '{executed_usdt}', '{ins_id}', '{avg_price}', '{fee}', '{fee_rate}', {current_time}, '{PositionStatus.OPEN.value}', '{1 if is_spot else 0}' );
|
|
268
279
|
"""
|
|
269
280
|
execute_ret = mysql.execute_sql(sql, True)
|
|
270
281
|
|
|
271
282
|
self._logger.info(f"{ins_id}_{exchange}_{symbol} step 3. 创建持仓记录 {position_id}")
|
|
272
283
|
try:
|
|
273
284
|
self._write_position_open_to_redis(
|
|
274
|
-
position_id,
|
|
275
|
-
exchange,
|
|
276
|
-
symbol,
|
|
277
|
-
position_side,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
+
position_id=position_id,
|
|
286
|
+
exchange=exchange,
|
|
287
|
+
symbol=symbol,
|
|
288
|
+
position_side=position_side,
|
|
289
|
+
lever=lever,
|
|
290
|
+
coin_quantity=executed_qty,
|
|
291
|
+
usdt_quantity=executed_usdt,
|
|
292
|
+
open_ins_id=ins_id,
|
|
293
|
+
open_price=avg_price,
|
|
294
|
+
open_fee=fee,
|
|
295
|
+
open_fee_rate=fee_rate,
|
|
296
|
+
open_time=current_time,
|
|
297
|
+
is_spot=is_spot,
|
|
285
298
|
)
|
|
286
299
|
except:
|
|
287
300
|
pass
|
|
@@ -289,7 +302,12 @@ class OrderHelper:
|
|
|
289
302
|
# 需要找到对应的持仓记录
|
|
290
303
|
sql = f"""
|
|
291
304
|
SELECT * FROM {self._mysql_table_name_position}
|
|
292
|
-
WHERE exchange = '{exchange}'
|
|
305
|
+
WHERE exchange = '{exchange}'
|
|
306
|
+
AND symbol = '{symbol}'
|
|
307
|
+
AND position_side = '{position_side.value}'
|
|
308
|
+
AND status = '{PositionStatus.OPEN.value}'
|
|
309
|
+
AND is_spot = '{1 if is_spot else 0}'
|
|
310
|
+
AND is_mock = 0
|
|
293
311
|
ORDER BY open_time ASC;
|
|
294
312
|
"""
|
|
295
313
|
|
|
@@ -317,22 +335,24 @@ class OrderHelper:
|
|
|
317
335
|
self._logger.info(f"{ins_id}_{exchange}_{symbol} step 3. 更新持仓记录 {position_id}")
|
|
318
336
|
try:
|
|
319
337
|
self._write_position_close_to_redis(
|
|
320
|
-
position_id,
|
|
321
|
-
exchange,
|
|
322
|
-
symbol,
|
|
323
|
-
position_side,
|
|
324
|
-
|
|
325
|
-
float(row.
|
|
326
|
-
row.
|
|
327
|
-
|
|
328
|
-
float(row.
|
|
329
|
-
float(row.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
338
|
+
position_id=position_id,
|
|
339
|
+
exchange=exchange,
|
|
340
|
+
symbol=symbol,
|
|
341
|
+
position_side=position_side,
|
|
342
|
+
lever=row.lever,
|
|
343
|
+
coin_quantity=float(row.coin_quantity),
|
|
344
|
+
usdt_quantity=float(row.usdt_quantity),
|
|
345
|
+
open_ins_id=row.open_ins_id,
|
|
346
|
+
open_price=float(row.open_price),
|
|
347
|
+
open_fee=float(row.open_fee),
|
|
348
|
+
open_fee_rate=float(row.open_fee_rate),
|
|
349
|
+
open_time=int(row.open_time),
|
|
350
|
+
close_ins_id=ins_id,
|
|
351
|
+
close_price=avg_price,
|
|
352
|
+
close_fee=fee,
|
|
353
|
+
close_fee_rate=fee_rate,
|
|
354
|
+
close_time=current_time,
|
|
355
|
+
is_spot=row.is_spot,
|
|
336
356
|
)
|
|
337
357
|
except:
|
|
338
358
|
pass
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from kaq_quant_common.api.rest.instruction.models import (
|
|
4
|
+
InstructionRequestBase, InstructionResponseBase)
|
|
5
|
+
|
|
6
|
+
# -----------------------------借贷-------------------
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 活期借币
|
|
10
|
+
class QueryFlexibleLoanAssetRequest(InstructionRequestBase):
|
|
11
|
+
# 币种代码
|
|
12
|
+
coin: str
|
|
13
|
+
|
|
14
|
+
class QueryFlexibleLoanAssetResponse(InstructionResponseBase):
|
|
15
|
+
# 币种代码
|
|
16
|
+
coin: str
|
|
17
|
+
# 活期借币利率
|
|
18
|
+
interestRate: Optional[float] = None
|
|
19
|
+
# 最低可借数量
|
|
20
|
+
minLimit: Optional[float] = None
|
|
21
|
+
# 最高可借数量
|
|
22
|
+
maxLimit: Optional[float] = None
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LimitOrderHelper - 高性能数据批量写入助手
|
|
3
|
+
|
|
4
|
+
使用独立事件循环的 asyncio 实现,消除 GIL 影响,提升性能。
|
|
5
|
+
|
|
6
|
+
特性:
|
|
7
|
+
- 外部同步接口,内部 asyncio 实现
|
|
8
|
+
- 独立线程运行事件循环,无 GIL 竞争
|
|
9
|
+
- 适配所有交易所 SDK(同步/异步)
|
|
10
|
+
- 线程安全的跨线程调用
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import datetime
|
|
15
|
+
import threading
|
|
16
|
+
from typing import Callable, Optional
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
from kaq_quant_common.resources.kaq_ddb_stream_write_resources import (
|
|
20
|
+
KaqQuantDdbStreamMTWWriteRepository,
|
|
21
|
+
KaqQuantDdbStreamWriteRepository,
|
|
22
|
+
)
|
|
23
|
+
from kaq_quant_common.utils import logger_utils
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LimitOrderHelper:
|
|
27
|
+
"""
|
|
28
|
+
使用独立事件循环的高性能数据批量写入助手
|
|
29
|
+
|
|
30
|
+
架构说明:
|
|
31
|
+
1. 外部通过同步方法 push_data() 推送数据(适配任意 SDK)
|
|
32
|
+
2. 内部使用独立线程运行 asyncio 事件循环
|
|
33
|
+
3. 数据通过 asyncio.Queue 缓冲,定时批量写入数据库
|
|
34
|
+
4. 消除 GIL 锁竞争,提升高频场景性能
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
ddb: KaqQuantDdbStreamWriteRepository | KaqQuantDdbStreamMTWWriteRepository,
|
|
40
|
+
ddb_table_name: str,
|
|
41
|
+
_flush_interval_ms: int = 100,
|
|
42
|
+
max_queue_size: int = 10000,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
初始化 LimitOrderHelper
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
ddb: DDB 数据库连接(支持普通或 MTW 版本)
|
|
49
|
+
ddb_table_name: 数据库表名
|
|
50
|
+
_flush_interval_ms: 刷新间隔(毫秒),默认 100ms
|
|
51
|
+
max_queue_size: 队列最大容量,默认 10000
|
|
52
|
+
"""
|
|
53
|
+
# DDB 相关
|
|
54
|
+
self._ddb = ddb
|
|
55
|
+
self._isMtwDdb = isinstance(self._ddb, KaqQuantDdbStreamMTWWriteRepository)
|
|
56
|
+
self._ddb_table_name = ddb_table_name
|
|
57
|
+
|
|
58
|
+
# 配置参数
|
|
59
|
+
self._flush_interval_ms = _flush_interval_ms
|
|
60
|
+
self._max_queue_size = max_queue_size
|
|
61
|
+
|
|
62
|
+
# 异步组件(将在后台线程中创建)
|
|
63
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
64
|
+
self._data_queue: Optional[asyncio.Queue] = None
|
|
65
|
+
self._flusher_task: Optional[asyncio.Task] = None
|
|
66
|
+
|
|
67
|
+
# 线程控制
|
|
68
|
+
self._event_loop_thread: Optional[threading.Thread] = None
|
|
69
|
+
self._stop_event = threading.Event()
|
|
70
|
+
self._started = threading.Event()
|
|
71
|
+
|
|
72
|
+
# 日志和回调
|
|
73
|
+
self._logger = logger_utils.get_logger(self)
|
|
74
|
+
self._build_data: Optional[Callable] = None
|
|
75
|
+
|
|
76
|
+
def set_build_data(self, build_data: Callable):
|
|
77
|
+
"""
|
|
78
|
+
设置数据构建函数
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
build_data: 回调函数,签名为 (symbol, data, arg) -> list | DataFrame
|
|
82
|
+
"""
|
|
83
|
+
self._build_data = build_data
|
|
84
|
+
|
|
85
|
+
def push_data(self, symbol: str, data: dict, arg: dict = None):
|
|
86
|
+
"""
|
|
87
|
+
同步接口:推送数据到队列
|
|
88
|
+
|
|
89
|
+
该方法是线程安全的,可从任何线程调用。
|
|
90
|
+
内部使用 call_soon_threadsafe 将数据投递到异步队列。
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
symbol: 交易对符号
|
|
94
|
+
data: 数据字典
|
|
95
|
+
arg: 可选参数
|
|
96
|
+
"""
|
|
97
|
+
if not self._started.is_set():
|
|
98
|
+
self._logger.warning("Helper 未启动,数据被丢弃")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# 使用 call_soon_threadsafe 将数据投递到异步队列
|
|
102
|
+
self._loop.call_soon_threadsafe(self._async_push_data, symbol, data, arg)
|
|
103
|
+
|
|
104
|
+
def _async_push_data(self, symbol: str, data: dict, arg: dict):
|
|
105
|
+
"""
|
|
106
|
+
内部方法:异步推送数据(在事件循环线程中执行)
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
symbol: 交易对符号
|
|
110
|
+
data: 数据字典
|
|
111
|
+
arg: 可选参数
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
# 非阻塞推送
|
|
115
|
+
self._data_queue.put_nowait((symbol, data, arg))
|
|
116
|
+
except asyncio.QueueFull:
|
|
117
|
+
self._logger.warning(f"队列已满 ({self._max_queue_size}),丢弃 {symbol} 数据")
|
|
118
|
+
|
|
119
|
+
def start(self):
|
|
120
|
+
"""启动后台线程和事件循环"""
|
|
121
|
+
if self._event_loop_thread is not None:
|
|
122
|
+
self._logger.warning("Helper 已经启动")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
self._stop_event.clear()
|
|
126
|
+
|
|
127
|
+
# 启动后台线程
|
|
128
|
+
self._event_loop_thread = threading.Thread(target=self._run_event_loop, daemon=True, name="LimitOrderHelperAsyncThread")
|
|
129
|
+
self._event_loop_thread.start()
|
|
130
|
+
|
|
131
|
+
# 等待事件循环就绪
|
|
132
|
+
self._started.wait(timeout=5.0)
|
|
133
|
+
|
|
134
|
+
if not self._started.is_set():
|
|
135
|
+
raise RuntimeError("事件循环启动超时")
|
|
136
|
+
|
|
137
|
+
self._logger.info("LimitOrderHelper 启动成功(asyncio 模式)")
|
|
138
|
+
|
|
139
|
+
def stop(self):
|
|
140
|
+
"""优雅停止"""
|
|
141
|
+
if not self._started.is_set():
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
self._logger.info("正在停止 LimitOrderHelper...")
|
|
145
|
+
|
|
146
|
+
# 设置停止标志
|
|
147
|
+
self._stop_event.set()
|
|
148
|
+
|
|
149
|
+
# 如果事件循环正在运行,停止它
|
|
150
|
+
if self._loop and self._loop.is_running():
|
|
151
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
152
|
+
|
|
153
|
+
# 等待线程结束
|
|
154
|
+
if self._event_loop_thread:
|
|
155
|
+
self._event_loop_thread.join(timeout=10.0)
|
|
156
|
+
if self._event_loop_thread.is_alive():
|
|
157
|
+
self._logger.error("事件循环线程未能正常退出")
|
|
158
|
+
|
|
159
|
+
self._logger.info("LimitOrderHelper 已停止")
|
|
160
|
+
|
|
161
|
+
def _run_event_loop(self):
|
|
162
|
+
"""
|
|
163
|
+
在独立线程中运行事件循环
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
# 创建新的事件循环
|
|
167
|
+
self._loop = asyncio.new_event_loop()
|
|
168
|
+
asyncio.set_event_loop(self._loop)
|
|
169
|
+
|
|
170
|
+
# 创建异步队列
|
|
171
|
+
self._data_queue = asyncio.Queue(maxsize=self._max_queue_size)
|
|
172
|
+
|
|
173
|
+
# 创建后台刷新任务
|
|
174
|
+
self._flusher_task = self._loop.create_task(self._flush_loop())
|
|
175
|
+
|
|
176
|
+
# 标记已启动
|
|
177
|
+
self._started.set()
|
|
178
|
+
|
|
179
|
+
# 运行事件循环
|
|
180
|
+
self._loop.run_forever()
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
self._logger.error(f"事件循环异常: {e}", exc_info=True)
|
|
184
|
+
finally:
|
|
185
|
+
# 清理
|
|
186
|
+
try:
|
|
187
|
+
# 取消所有任务
|
|
188
|
+
pending = asyncio.all_tasks(self._loop)
|
|
189
|
+
for task in pending:
|
|
190
|
+
task.cancel()
|
|
191
|
+
|
|
192
|
+
# 等待任务完成
|
|
193
|
+
if pending:
|
|
194
|
+
self._loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
195
|
+
|
|
196
|
+
# 关闭事件循环
|
|
197
|
+
self._loop.close()
|
|
198
|
+
except Exception as e:
|
|
199
|
+
self._logger.error(f"清理事件循环时出错: {e}")
|
|
200
|
+
|
|
201
|
+
async def _flush_loop(self):
|
|
202
|
+
"""
|
|
203
|
+
异步刷新循环(在事件循环线程中执行)
|
|
204
|
+
|
|
205
|
+
定时从队列批量获取数据并写入数据库。
|
|
206
|
+
"""
|
|
207
|
+
cum_count = 0
|
|
208
|
+
cum_total_use_time = 0
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
while not self._stop_event.is_set():
|
|
212
|
+
# 批量收集数据
|
|
213
|
+
batch = {}
|
|
214
|
+
deadline = asyncio.get_event_loop().time() + self._flush_interval_ms / 1000.0
|
|
215
|
+
|
|
216
|
+
# 在时间窗口内尽可能多地收集数据
|
|
217
|
+
while asyncio.get_event_loop().time() < deadline:
|
|
218
|
+
try:
|
|
219
|
+
remaining_time = deadline - asyncio.get_event_loop().time()
|
|
220
|
+
if remaining_time <= 0:
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
# 从队列获取数据(带超时)
|
|
224
|
+
symbol, data, arg = await asyncio.wait_for(self._data_queue.get(), timeout=remaining_time)
|
|
225
|
+
|
|
226
|
+
# 只保留每个 symbol 的最新数据(去重)
|
|
227
|
+
batch[symbol] = (data, arg)
|
|
228
|
+
|
|
229
|
+
except asyncio.TimeoutError:
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
# 如果有数据,批量写入
|
|
233
|
+
if batch:
|
|
234
|
+
start_time = datetime.datetime.now()
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
await self._write_to_db(batch)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self._logger.error(f"批量写入失败: {e}", exc_info=True)
|
|
240
|
+
|
|
241
|
+
# 统计
|
|
242
|
+
end_time = datetime.datetime.now()
|
|
243
|
+
total_use_time = (end_time - start_time).total_seconds() * 1000
|
|
244
|
+
|
|
245
|
+
cum_count += len(batch)
|
|
246
|
+
cum_total_use_time += total_use_time
|
|
247
|
+
|
|
248
|
+
# if total_use_time > 500:
|
|
249
|
+
# self._logger.debug(
|
|
250
|
+
# f"批量写入 {len(batch)} 条数据耗时 {total_use_time:.2f}ms "
|
|
251
|
+
# f"(avg {cum_total_use_time / cum_count:.2f}ms)"
|
|
252
|
+
# )
|
|
253
|
+
else:
|
|
254
|
+
# 没有数据时短暂休眠
|
|
255
|
+
await asyncio.sleep(0.01)
|
|
256
|
+
|
|
257
|
+
except asyncio.CancelledError:
|
|
258
|
+
self._logger.info("刷新循环被取消")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
self._logger.error(f"刷新循环异常: {e}", exc_info=True)
|
|
261
|
+
|
|
262
|
+
async def _write_to_db(self, batch: dict):
|
|
263
|
+
"""
|
|
264
|
+
写入数据库(异步)
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
batch: 批量数据字典,格式为 {symbol: (data, arg)}
|
|
268
|
+
"""
|
|
269
|
+
# 转换数据
|
|
270
|
+
df: Optional[pd.DataFrame] = None
|
|
271
|
+
list_data: list = []
|
|
272
|
+
|
|
273
|
+
for symbol, (data, arg) in batch.items():
|
|
274
|
+
sub_data = self._build_data(symbol, data, arg)
|
|
275
|
+
|
|
276
|
+
if sub_data is None or len(sub_data) == 0:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
if not self._isMtwDdb:
|
|
280
|
+
# DataFrame 方式
|
|
281
|
+
if isinstance(sub_data, pd.DataFrame):
|
|
282
|
+
df = sub_data if df is None else pd.concat([df, sub_data], ignore_index=True)
|
|
283
|
+
# List 方式
|
|
284
|
+
else:
|
|
285
|
+
if isinstance(sub_data[0], list):
|
|
286
|
+
list_data.extend(sub_data)
|
|
287
|
+
else:
|
|
288
|
+
list_data.append(sub_data)
|
|
289
|
+
else:
|
|
290
|
+
# MTW 直接写入(使用 to_thread 包装同步调用)
|
|
291
|
+
try:
|
|
292
|
+
await asyncio.to_thread(self._ddb.save2stream_list, sub_data)
|
|
293
|
+
except Exception as e:
|
|
294
|
+
self._logger.error(f"MTW 写入失败: {e}")
|
|
295
|
+
|
|
296
|
+
# 批量写入(使用 to_thread 包装同步 DDB 调用)
|
|
297
|
+
if not self._isMtwDdb:
|
|
298
|
+
if df is not None and not df.empty:
|
|
299
|
+
try:
|
|
300
|
+
await asyncio.to_thread(self._ddb.save2stream_batch, self._ddb_table_name, df=df)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
self._logger.error(f"批量写入 DataFrame 失败: {e}")
|
|
303
|
+
|
|
304
|
+
if list_data:
|
|
305
|
+
try:
|
|
306
|
+
await asyncio.to_thread(self._ddb.save2stream_batch_list, self._ddb_table_name, data=list_data)
|
|
307
|
+
except Exception as e:
|
|
308
|
+
self._logger.error(f"批量写入 List 失败: {e}")
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
# 避免写入导致阻塞
|
|
2
|
-
import datetime
|
|
3
|
-
import threading
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
import pandas as pd
|
|
7
|
-
from kaq_quant_common.resources.kaq_ddb_stream_write_resources import (
|
|
8
|
-
KaqQuantDdbStreamMTWWriteRepository,
|
|
9
|
-
KaqQuantDdbStreamWriteRepository,
|
|
10
|
-
)
|
|
11
|
-
from kaq_quant_common.utils import logger_utils
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class LimitOrderHelper:
|
|
15
|
-
|
|
16
|
-
def __init__(
|
|
17
|
-
self, ddb: KaqQuantDdbStreamWriteRepository | KaqQuantDdbStreamMTWWriteRepository, ddb_table_name: str, _flush_interval_ms: int = 100
|
|
18
|
-
):
|
|
19
|
-
# 最新快照缓存与刷库线程控制
|
|
20
|
-
self._latest_snapshots: dict[str, tuple] = {}
|
|
21
|
-
self._latest_lock = threading.Lock()
|
|
22
|
-
# 写入到ddb的频率,默认100ms
|
|
23
|
-
self._flush_interval_ms = _flush_interval_ms
|
|
24
|
-
self._stop_event = threading.Event()
|
|
25
|
-
self._flusher_thread = threading.Thread(target=self._flush_loop, daemon=True)
|
|
26
|
-
self._flusher_thread.name = "LimitOrderHelperFlusherThread"
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
self._ddb = ddb
|
|
30
|
-
self._isMtwDdb = isinstance(self._ddb, KaqQuantDdbStreamMTWWriteRepository)
|
|
31
|
-
self._ddb_table_name = ddb_table_name
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
self._logger = logger_utils.get_logger(self)
|
|
35
|
-
|
|
36
|
-
self._build_data: callable = None
|
|
37
|
-
|
|
38
|
-
def set_build_data(self, build_data: callable):
|
|
39
|
-
self._build_data = build_data
|
|
40
|
-
|
|
41
|
-
def push_data(self, symbol: str, data: dict, arg: dict = None):
|
|
42
|
-
with self._latest_lock:
|
|
43
|
-
self._latest_snapshots[symbol] = (data, arg)
|
|
44
|
-
|
|
45
|
-
def start(self):
|
|
46
|
-
self._flusher_thread.start()
|
|
47
|
-
|
|
48
|
-
def stop(self):
|
|
49
|
-
self._stop_event.set()
|
|
50
|
-
self._flusher_thread.join()
|
|
51
|
-
|
|
52
|
-
def _flush_loop(self):
|
|
53
|
-
cum_count = 0
|
|
54
|
-
cum_convert_time = 0
|
|
55
|
-
cum_write_ddb_time = 0
|
|
56
|
-
cum_total_use_time = 0
|
|
57
|
-
# 周期性地将每个symbol的最新快照批量入库
|
|
58
|
-
while not self._stop_event.is_set():
|
|
59
|
-
to_process = None
|
|
60
|
-
with self._latest_lock:
|
|
61
|
-
if self._latest_snapshots:
|
|
62
|
-
to_process = list(self._latest_snapshots.items())
|
|
63
|
-
self._latest_snapshots.clear()
|
|
64
|
-
|
|
65
|
-
if to_process:
|
|
66
|
-
df: pd.DataFrame = None
|
|
67
|
-
list_data: list = []
|
|
68
|
-
now = int(datetime.datetime.now().timestamp() * 1000)
|
|
69
|
-
|
|
70
|
-
for symbol, (data, arg) in to_process:
|
|
71
|
-
sub_data = self._build_data(symbol, data, arg)
|
|
72
|
-
|
|
73
|
-
if sub_data is None:
|
|
74
|
-
continue
|
|
75
|
-
|
|
76
|
-
if len(sub_data) == 0:
|
|
77
|
-
continue
|
|
78
|
-
|
|
79
|
-
if not self._isMtwDdb:
|
|
80
|
-
# 可以是数组,可以是dataFrame
|
|
81
|
-
is_df = type(sub_data) is pd.DataFrame
|
|
82
|
-
|
|
83
|
-
if is_df:
|
|
84
|
-
# df就用df的方式写入
|
|
85
|
-
data_first_now = int(sub_data["create_time"].iloc[0])
|
|
86
|
-
# if now - data_first_now > 2000:
|
|
87
|
-
# self._logger.debug(f"数据时间{data_first_now} 与当前时间{now} 差值{now - data_first_now} 超过2000ms")
|
|
88
|
-
# pass
|
|
89
|
-
|
|
90
|
-
if df is None:
|
|
91
|
-
df = sub_data
|
|
92
|
-
else:
|
|
93
|
-
df = pd.concat([df, sub_data], ignore_index=True)
|
|
94
|
-
else:
|
|
95
|
-
# 数组就用数组的方式写入
|
|
96
|
-
# 子元素是否数组
|
|
97
|
-
is_sub_list = type(sub_data[0]) is list
|
|
98
|
-
if is_sub_list:
|
|
99
|
-
# 多条数据
|
|
100
|
-
data_first_now = int(sub_data[0][0])
|
|
101
|
-
# if now - data_first_now > 2000:
|
|
102
|
-
# self._logger.debug(f"数据时间{data_first_now} 与当前时间{now} 差值{now - data_first_now} 超过2000ms")
|
|
103
|
-
# pass
|
|
104
|
-
list_data.extend(sub_data)
|
|
105
|
-
else:
|
|
106
|
-
# 单条数据
|
|
107
|
-
data_first_now = int(sub_data[0])
|
|
108
|
-
# if now - data_first_now > 2000:
|
|
109
|
-
# self._logger.debug(f"数据时间{data_first_now} 与当前时间{now} 差值{now - data_first_now} 超过2000ms")
|
|
110
|
-
# pass
|
|
111
|
-
list_data.append(sub_data)
|
|
112
|
-
|
|
113
|
-
else:
|
|
114
|
-
# 直接调用 save2stream_list 写入
|
|
115
|
-
try:
|
|
116
|
-
self._ddb.save2stream_list(sub_data)
|
|
117
|
-
except Exception as e:
|
|
118
|
-
# 避免刷库异常导致线程退出
|
|
119
|
-
self._logger.error(f"批量写入数组失败: {e}")
|
|
120
|
-
|
|
121
|
-
convert_time = int(datetime.datetime.now().timestamp() * 1000)
|
|
122
|
-
|
|
123
|
-
# 入库
|
|
124
|
-
if not self._isMtwDdb:
|
|
125
|
-
# 兼容df和数组
|
|
126
|
-
if df is not None and not df.empty:
|
|
127
|
-
try:
|
|
128
|
-
self._ddb.save2stream_batch(self._ddb_table_name, df=df)
|
|
129
|
-
except Exception as e:
|
|
130
|
-
# 避免刷库异常导致线程退出
|
|
131
|
-
self._logger.error(f"批量写入df失败: {e}")
|
|
132
|
-
if len(list_data) > 0:
|
|
133
|
-
try:
|
|
134
|
-
self._ddb.save2stream_batch_list(self._ddb_table_name, data=list_data)
|
|
135
|
-
except Exception as e:
|
|
136
|
-
# 避免刷库异常导致线程退出
|
|
137
|
-
self._logger.error(f"批量写入list失败: {e}")
|
|
138
|
-
|
|
139
|
-
# 统计一下
|
|
140
|
-
end = int(datetime.datetime.now().timestamp() * 1000)
|
|
141
|
-
total_use_time = end - now
|
|
142
|
-
convert_use = convert_time - now
|
|
143
|
-
write_ddb_use = total_use_time - convert_use
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
cum_count += len(to_process)
|
|
147
|
-
cum_convert_time += convert_use
|
|
148
|
-
cum_write_ddb_time += write_ddb_use
|
|
149
|
-
cum_total_use_time += total_use_time
|
|
150
|
-
|
|
151
|
-
# if total_use_time > 500 and cum_count > 0:
|
|
152
|
-
# self._logger.debug(
|
|
153
|
-
# f"批量写入{len(to_process)}条数据耗时{total_use_time}ms(avg {cum_total_use_time / cum_count:.2f}ms) 转换耗时{convert_use}ms(avg {cum_convert_time / cum_count:.2f}ms) 写入ddb耗时{write_ddb_use}ms(avg {cum_write_ddb_time / cum_count:.2f}ms)"
|
|
154
|
-
# )
|
|
155
|
-
|
|
156
|
-
if not self._isMtwDdb:
|
|
157
|
-
time.sleep(self._flush_interval_ms / 1000.0)
|
|
158
|
-
else:
|
|
159
|
-
# mtw交由ddb自己控制节奏
|
|
160
|
-
time.sleep(50 / 1000.0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/common/api_interface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/api_client_base.py
RENAMED
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/api_server_base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/exchange/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/instruction/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/ws_client_base.py
RENAMED
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/ws_server_base.py
RENAMED
|
File without changes
|
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/ddb_table_monitor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/monitor_group.py
RENAMED
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/redis_table_monitor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/log_time_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|