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.
Files changed (71) hide show
  1. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/PKG-INFO +1 -1
  2. {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
  3. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/helper/order_helper.py +58 -38
  4. kaq_quant_common-0.2.15/kaq_quant_common/api/rest/instruction/models/loan.py +22 -0
  5. kaq_quant_common-0.2.15/kaq_quant_common/common/modules/limit_order_helper.py +308 -0
  6. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/pyproject.toml +1 -1
  7. kaq_quant_common-0.2.13/kaq_quant_common/common/modules/limit_order_helper.py +0 -160
  8. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/README.md +0 -0
  9. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/__init__.py +0 -0
  10. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/__init__.py +0 -0
  11. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/common/__init__.py +0 -0
  12. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/common/api_interface.py +0 -0
  13. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/common/auth.py +0 -0
  14. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/__init__.py +0 -0
  15. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/api_client_base.py +0 -0
  16. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/api_server_base.py +0 -0
  17. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/helper/commission_helper.py +0 -0
  18. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/instruction_client.py +0 -0
  19. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/instruction_server_base.py +0 -0
  20. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/__init__.py +0 -0
  21. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/account.py +0 -0
  22. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/order.py +0 -0
  23. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/position.py +0 -0
  24. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/rest/instruction/models/transfer.py +0 -0
  25. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/__init__.py +0 -0
  26. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/exchange/models.py +0 -0
  27. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/exchange/ws_exchange_client.py +0 -0
  28. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/exchange/ws_exchange_server.py +0 -0
  29. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/instruction/__init__.py +0 -0
  30. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/instruction/ws_instruction_client.py +0 -0
  31. {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
  32. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/models.py +0 -0
  33. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/ws_client_base.py +0 -0
  34. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/api/ws/ws_server_base.py +0 -0
  35. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/__init__.py +0 -0
  36. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/ddb_table_monitor.py +0 -0
  37. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/http_monitor.py +0 -0
  38. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/modules/funding_rate_helper.py +0 -0
  39. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/modules/limit_order_symbol_monitor.py +0 -0
  40. {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
  41. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/monitor_base.py +0 -0
  42. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/monitor_group.py +0 -0
  43. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/redis_table_monitor.py +0 -0
  44. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/statistics/funding_rate_history_statistics.py +0 -0
  45. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/statistics/kline_history_statistics.py +0 -0
  46. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/common/ws_wrapper.py +0 -0
  47. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/config/config.yaml +0 -0
  48. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/__init__.py +0 -0
  49. {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
  50. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_ddb_stream_init_resources.py +0 -0
  51. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_ddb_stream_read_resources.py +0 -0
  52. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_ddb_stream_write_resources.py +0 -0
  53. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_mysql_init_resources.py +0 -0
  54. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_mysql_resources.py +0 -0
  55. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_postgresql_resources.py +0 -0
  56. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_quant_hive_resources.py +0 -0
  57. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/resources/kaq_redis_resources.py +0 -0
  58. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/__init__.py +0 -0
  59. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/dagster_job_check_utils.py +0 -0
  60. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/dagster_utils.py +0 -0
  61. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/date_util.py +0 -0
  62. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/enums_utils.py +0 -0
  63. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/error_utils.py +0 -0
  64. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/hash_utils.py +0 -0
  65. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/log_time_utils.py +0 -0
  66. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/logger_utils.py +0 -0
  67. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/mytt_utils.py +0 -0
  68. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/signal_utils.py +0 -0
  69. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/sqlite_utils.py +0 -0
  70. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/uuid_utils.py +0 -0
  71. {kaq_quant_common-0.2.13 → kaq_quant_common-0.2.15}/kaq_quant_common/utils/yml_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaq_quant_common
3
- Version: 0.2.13
3
+ Version: 0.2.15
4
4
  Summary:
5
5
  Author: kevinfuture
6
6
  Author-email: liuenbofuture@foxmail.com
@@ -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
- executed_qty,
277
- executed_usdt,
278
- ins_id,
279
- avg_price,
280
- fee,
281
- fee_rate,
282
- current_time,
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}' AND symbol = '{symbol}' AND position_side = '{position_side.value}' AND status = '{PositionStatus.OPEN.value}' AND is_mock = 1
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
- float(row.coin_quantity),
327
- float(row.usdt_quantity),
328
- row.open_ins_id,
329
- float(row.open_price),
330
- float(row.open_fee),
331
- float(row.open_fee_rate),
332
- int(row.open_time),
333
- ins_id,
334
- avg_price,
335
- fee,
336
- fee_rate,
337
- current_time,
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
- executed_qty,
279
- executed_usdt,
280
- ins_id,
281
- avg_price,
282
- fee,
283
- fee_rate,
284
- current_time,
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}' AND symbol = '{symbol}' AND position_side = '{position_side.value}' AND status = '{PositionStatus.OPEN.value}'
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
- float(row.coin_quantity),
325
- float(row.usdt_quantity),
326
- row.open_ins_id,
327
- float(row.open_price),
328
- float(row.open_fee),
329
- float(row.open_fee_rate),
330
- int(row.open_time),
331
- ins_id,
332
- avg_price,
333
- fee,
334
- fee_rate,
335
- current_time,
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "kaq_quant_common"
3
- version = "0.2.13"
3
+ version = "0.2.15"
4
4
  description = ""
5
5
  authors = [
6
6
  {name = "kevinfuture",email = "liuenbofuture@foxmail.com"}
@@ -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)