cfquant 0.2.0__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 (90) hide show
  1. LTtx/__init__.py +2 -0
  2. LTtx/tx/Config.txt +8 -0
  3. LTtx/tx/LTtx_server.py +1177 -0
  4. LTtx/tx/__init__.py +5 -0
  5. LTtx/tx/tx.py +1935 -0
  6. cfquant/__init__.py +26 -0
  7. cfquant/account_routing.py +119 -0
  8. cfquant/channels.py +76 -0
  9. cfquant/cli.py +266 -0
  10. cfquant/client.py +365 -0
  11. cfquant/config.py +86 -0
  12. cfquant/hub.py +304 -0
  13. cfquant/logging_i18n.py +348 -0
  14. cfquant/normal_bridge.py +676 -0
  15. cfquant/pipe_bridge.py +249 -0
  16. cfquant/pipe_client.py +223 -0
  17. cfquant/pipe_hub.py +602 -0
  18. cfquant/pipe_transport.py +493 -0
  19. cfquant/protocol.py +238 -0
  20. cfquant/qmt_bridge.py +1081 -0
  21. cfquant/runtime_report.py +334 -0
  22. cfquant/tx.py +1898 -0
  23. cfquant/tx_trade_bridge.py +1905 -0
  24. cfquant/version.py +5 -0
  25. cfquant/xtconstant.py +262 -0
  26. cfquant/xtdata.py +462 -0
  27. cfquant/xttrader.py +955 -0
  28. cfquant/xttype.py +586 -0
  29. cfquant-0.2.0.dist-info/LICENSE +21 -0
  30. cfquant-0.2.0.dist-info/METADATA +144 -0
  31. cfquant-0.2.0.dist-info/RECORD +90 -0
  32. cfquant-0.2.0.dist-info/WHEEL +5 -0
  33. cfquant-0.2.0.dist-info/entry_points.txt +4 -0
  34. cfquant-0.2.0.dist-info/top_level.txt +6 -0
  35. cfquant_pipe_hub.py +45 -0
  36. cfquant_web_server.py +12137 -0
  37. qmt_scripts/CFQUANT.py +507 -0
  38. qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
  39. qmt_scripts/CFQUANT_LITE.py +4928 -0
  40. qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
  41. qmt_scripts/__init__.py +1 -0
  42. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SH.py +866 -0
  43. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py +866 -0
  44. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SH.py +5020 -0
  45. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SZ.py +5020 -0
  46. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SH.py +436 -0
  47. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SZ.py +436 -0
  48. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/readme.md +95 -0
  49. web_dashboard/__init__.py +1 -0
  50. web_dashboard/app.js +9847 -0
  51. web_dashboard/avatars/amber-pulse.svg +15 -0
  52. web_dashboard/avatars/copper-grid.svg +15 -0
  53. web_dashboard/avatars/market-blue.svg +15 -0
  54. web_dashboard/avatars/rose-circuit.svg +15 -0
  55. web_dashboard/avatars/signal-green.svg +15 -0
  56. web_dashboard/avatars/slate-wave.svg +15 -0
  57. web_dashboard/avatars/teal-orbit.svg +16 -0
  58. web_dashboard/avatars/violet-node.svg +15 -0
  59. web_dashboard/favicon.svg +5 -0
  60. web_dashboard/image/qmt_deploy/image-10.png +0 -0
  61. web_dashboard/image/qmt_deploy/image-11.png +0 -0
  62. web_dashboard/image/qmt_deploy/image-12.png +0 -0
  63. web_dashboard/image/qmt_deploy/image-13.png +0 -0
  64. web_dashboard/image/qmt_deploy/image-14.png +0 -0
  65. web_dashboard/image/qmt_deploy/image-15.png +0 -0
  66. web_dashboard/image/qmt_deploy/image-16.png +0 -0
  67. web_dashboard/image/qmt_deploy/image-17.png +0 -0
  68. web_dashboard/image/qmt_deploy/image-18.png +0 -0
  69. web_dashboard/image/qmt_deploy/image-19.png +0 -0
  70. web_dashboard/image/qmt_deploy/image-20.png +0 -0
  71. web_dashboard/image/qmt_deploy/image-21.png +0 -0
  72. web_dashboard/image/qmt_deploy/image-4.png +0 -0
  73. web_dashboard/image/qmt_deploy/image-5.png +0 -0
  74. web_dashboard/image/qmt_deploy/image-6.png +0 -0
  75. web_dashboard/image/qmt_deploy/image-7.png +0 -0
  76. web_dashboard/image/qmt_deploy/image-8.png +0 -0
  77. web_dashboard/image/qmt_deploy/image-9.png +0 -0
  78. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-1.png +0 -0
  79. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-2.png +0 -0
  80. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-3.png +0 -0
  81. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image.png +0 -0
  82. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479325179.png +0 -0
  83. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479745262.png +0 -0
  84. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480178700.png +0 -0
  85. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480428759.png +0 -0
  86. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783489807217.png +0 -0
  87. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783490503491.png +0 -0
  88. web_dashboard/index.html +2317 -0
  89. web_dashboard/styles.css +7817 -0
  90. web_dashboard/vendor/mermaid.min.js +2029 -0
cfquant/qmt_bridge.py ADDED
@@ -0,0 +1,1081 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import queue
4
+ import sys
5
+ import threading
6
+ import time
7
+
8
+ from .config import get_config
9
+ from .logging_i18n import get_log_language, set_log_language, translate_log
10
+ from .protocol import loads_message, pack_event, pack_response
11
+ from .xttype import filter_cancelable_orders
12
+
13
+
14
+ class CfquantQmtBridge(object):
15
+ """
16
+ 大 QMT 内置端 LTtx/TX 桥接器。
17
+
18
+ - 大 QMT 固定订阅 request_channel。
19
+ - 外部 cfquant 请求里携带 client_id。
20
+ - 响应和回调事件统一推送到 client_id 对应的 LTtx 频道。
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ context,
26
+ ip=None,
27
+ port=None,
28
+ token=None,
29
+ request_channel=None,
30
+ show=True,
31
+ globals_dict=None,
32
+ ):
33
+ cfg = get_config()
34
+ self.context = context
35
+ self.ip = ip or cfg["host"]
36
+ self.port = int(port or cfg["port"])
37
+ self.token = token or cfg["token"]
38
+ self.request_channel = request_channel or cfg["request_channel"]
39
+ self.show = show
40
+ self.globals_dict = globals_dict or {}
41
+ self.running = False
42
+ self.tx = None
43
+ self.connect_thread = None
44
+ self.recv_thread = None
45
+ self.log_lock = threading.RLock()
46
+ self.log_file = self._default_log_file()
47
+ self.log_queue = queue.Queue(maxsize=10000)
48
+ self.log_thread = None
49
+ self.main_thread_queue = queue.Queue(maxsize=10000)
50
+ self.subscriptions = {}
51
+ self.client_subscriptions = {}
52
+ self.auto_trade_callback_enabled = False
53
+
54
+ def start(self):
55
+ if self.running:
56
+ return self
57
+ self.running = True
58
+ self._start_log_thread()
59
+ self._enable_auto_trade_callback()
60
+ self.connect_thread = threading.Thread(target=self._connect_loop)
61
+ self.connect_thread.daemon = True
62
+ self.connect_thread.start()
63
+ self._log(
64
+ "cfquant QMT TX桥接已启动,LTtx=%s:%s request_channel=%s"
65
+ % (self.ip, self.port, self.request_channel)
66
+ )
67
+ self._log("cfquant QMT桥接文件日志:%s" % self.log_file)
68
+ return self
69
+
70
+ def set_context(self, context):
71
+ self.context = context
72
+ self._enable_auto_trade_callback()
73
+ self._log("cfquant context poll loop started")
74
+ while self.running:
75
+ self.poll(max_messages=100)
76
+ time.sleep(0.001)
77
+ self._log("cfquant桥接ContextInfo已绑定")
78
+
79
+ def close(self):
80
+ self.running = False
81
+ tx = self.tx
82
+ self.tx = None
83
+ if tx is not None:
84
+ try:
85
+ tx.Q.put(None)
86
+ except Exception:
87
+ pass
88
+ try:
89
+ tx.close()
90
+ except Exception:
91
+ pass
92
+ self._log("cfquant QMT TX桥接已关闭")
93
+
94
+ def poll(self, max_messages=20):
95
+ count = 0
96
+ while count < max_messages:
97
+ try:
98
+ raw, received_at = self.main_thread_queue.get_nowait()
99
+ except queue.Empty:
100
+ break
101
+ self._process_and_reply(raw, received_at, qmt_thread=True)
102
+ count += 1
103
+ return count
104
+
105
+ def _connect_loop(self):
106
+ while self.running:
107
+ tx = None
108
+ try:
109
+ txl = self._load_txl()
110
+ tx = txl(self.ip, self.port, self.token)
111
+ tx.start_tx()
112
+ tx.start_txg(self.request_channel)
113
+ self.tx = tx
114
+ self._log("cfquant QMT桥接已订阅请求频道:%s" % self.request_channel)
115
+ self._recv_loop(tx)
116
+ except Exception as e:
117
+ if self.running:
118
+ self._log("cfquant QMT TX桥接连接异常:%s,1秒后重试" % e)
119
+ time.sleep(1)
120
+ finally:
121
+ if self.tx is tx:
122
+ self.tx = None
123
+ if tx is not None:
124
+ try:
125
+ tx.close()
126
+ except Exception:
127
+ pass
128
+
129
+ def _recv_loop(self, tx):
130
+ while self.running and self.tx is tx:
131
+ try:
132
+ raw = tx.Q.get()
133
+ if raw is None:
134
+ break
135
+ received_at = time.perf_counter()
136
+ self._log("stage=request_dequeued raw=%s" % self._brief(raw))
137
+ self._process_and_reply(raw, received_at)
138
+ except Exception as e:
139
+ if self.running:
140
+ self._log("cfquant接收请求异常:%s" % e)
141
+ time.sleep(0.05)
142
+
143
+ def _process_and_reply(self, raw, received_at, qmt_thread=False):
144
+ parse_start = time.perf_counter()
145
+ msg = loads_message(raw)
146
+ parse_ms = self._elapsed_ms(parse_start)
147
+ if not msg:
148
+ self._log("stage=parse_invalid parse_ms=%.2f raw=%s" % (parse_ms, self._brief(raw)))
149
+ return
150
+ if msg.get("type") != "request":
151
+ self._log("cfquant桥接忽略消息 type=%s" % msg.get("type"))
152
+ return
153
+ client_id = msg.get("client_id")
154
+ request_id = msg.get("id")
155
+ action = msg.get("action")
156
+ if not qmt_thread and self._requires_qmt_thread(action):
157
+ try:
158
+ self.main_thread_queue.put_nowait((raw, received_at))
159
+ self._log("stage=request_enqueued_qmt_thread action=%s id=%s" % (action, request_id))
160
+ except queue.Full as e:
161
+ if client_id:
162
+ response = pack_response(request_id, ok=False, error=e)
163
+ self._push("response", response, client_id)
164
+ return
165
+ response = self._handle_request(msg, received_at, parse_ms)
166
+ if not client_id:
167
+ self._log("cfquant请求缺少client_id,无法回包 action=%s id=%s" % (action, request_id))
168
+ return
169
+ self._push("response", response, client_id)
170
+ self._log(
171
+ "stage=response_sent action=%s id=%s client_id=%s total_ms=%.2f"
172
+ % (action, request_id, client_id, self._elapsed_ms(received_at))
173
+ )
174
+
175
+ def _requires_qmt_thread(self, action):
176
+ return action in {
177
+ "xttrader.query_stock_asset",
178
+ "xttrader.query_stock_orders",
179
+ "xttrader.query_stock_trades",
180
+ "xttrader.query_stock_positions",
181
+ "xttrader.order_stock",
182
+ "xttrader.order_stock_async",
183
+ "xttrader.cancel_order_stock",
184
+ "xttrader.cancel_order_stock_async",
185
+ }
186
+
187
+ def _handle_request(self, msg, received_at=None, parse_ms=0.0):
188
+ request_id = msg.get("id")
189
+ action = msg.get("action")
190
+ dispatch_ms = 0.0
191
+ try:
192
+ self._log(
193
+ "stage=request_received action=%s id=%s parse_ms=%.2f params=%s"
194
+ % (action, request_id, parse_ms, self._brief(msg.get("params") or {}))
195
+ )
196
+ dispatch_start = time.perf_counter()
197
+ result = self._dispatch(action, msg.get("params") or {}, msg)
198
+ dispatch_ms = self._elapsed_ms(dispatch_start)
199
+ response = pack_response(request_id, ok=True, result=result)
200
+ self._log(
201
+ "stage=response_ready action=%s id=%s dispatch_ms=%.2f result=%s"
202
+ % (action, request_id, dispatch_ms, self._brief(result))
203
+ )
204
+ return response
205
+ except Exception as e:
206
+ self._log("cfquant桥接命令处理失败 action=%s id=%s error=%s" % (action, request_id, e))
207
+ return pack_response(request_id, ok=False, error=e)
208
+
209
+ def _dispatch(self, action, params, msg):
210
+ if action == "cfquant.ping":
211
+ return {"pong": True, "ts": time.time(), "request_channel": self.request_channel}
212
+ if action == "cfquant.set_log_language":
213
+ lang = set_log_language(params.get("language") or params.get("lang"))
214
+ self._log("QMT日志语言已切换为:%s" % ("中文" if lang == "zh" else "English"))
215
+ return {"language": lang}
216
+ if action == "cfquant.get_log_language":
217
+ return {"language": get_log_language()}
218
+ if action == "cfquant.status":
219
+ return {
220
+ "bridge": type(self).__name__,
221
+ "running": self.running,
222
+ "request_channel": self.request_channel,
223
+ "log_language": get_log_language(),
224
+ "context_ready": self.context is not None,
225
+ "tx_ready": self.tx is not None,
226
+ "subscriptions": len(self.subscriptions),
227
+ "ts": time.time(),
228
+ }
229
+ if self.context is None:
230
+ raise RuntimeError("QMT ContextInfo尚未绑定")
231
+ if action == "xtdata.get_market_data":
232
+ return self._get_market_data(params)
233
+ if action == "xtdata.get_market_data_ex":
234
+ return self._get_market_data_ex(params)
235
+ if action == "xtdata.get_full_tick":
236
+ return self.context.get_full_tick(params.get("code_list", []))
237
+ if action == "xtdata.subscribe_quote":
238
+ return self._subscribe_quote(params, msg)
239
+ if action == "xtdata.subscribe_whole_quote":
240
+ return self._subscribe_whole_quote(params, msg)
241
+ if action == "xtdata.unsubscribe_quote":
242
+ return self._unsubscribe_quote(params)
243
+ if action == "xtdata.download_history_data":
244
+ return self._download_history_data(params, msg)
245
+ if action == "xtdata.download_history_data2":
246
+ return self._download_history_data2(params, msg)
247
+ if action == "xtdata.get_instrument_detail":
248
+ return self._get_instrument_detail(params)
249
+ if action == "xtdata.get_stock_list_in_sector":
250
+ return self.context.get_stock_list_in_sector(params.get("sector_name", ""))
251
+ if action == "xttrader.subscribe":
252
+ return 0
253
+ if action == "xttrader.unsubscribe":
254
+ return 0
255
+ if action == "xttrader.order_stock":
256
+ return self._order_stock(params)
257
+ if action == "xttrader.order_stock_async":
258
+ return self._order_stock_async(params, msg)
259
+ if action == "xttrader.cancel_order_stock":
260
+ return self._cancel_order_stock(params)
261
+ if action == "xttrader.cancel_order_stock_async":
262
+ return self._cancel_order_stock_async(params, msg)
263
+ if action == "xttrader.query_stock_asset":
264
+ return self._query_trade_detail(params, "ACCOUNT")
265
+ if action == "xttrader.query_stock_orders":
266
+ return self._query_trade_detail(params, "ORDER")
267
+ if action == "xttrader.query_stock_trades":
268
+ return self._query_trade_detail(params, "DEAL")
269
+ if action == "xttrader.query_stock_positions":
270
+ return self._query_trade_detail(params, "POSITION")
271
+ raise ValueError("暂不支持的cfquant动作:%s" % action)
272
+
273
+ def _get_market_data(self, params):
274
+ func = getattr(self.context, "get_market_data", None)
275
+ if not func:
276
+ return self._get_market_data_ex(params)
277
+ return func(
278
+ params.get("field_list", []),
279
+ params.get("stock_list", []),
280
+ params.get("start_time", ""),
281
+ params.get("end_time", ""),
282
+ params.get("skip_paused", params.get("fill_data", True)),
283
+ params.get("period", "1d"),
284
+ params.get("dividend_type", "none"),
285
+ params.get("count", -1),
286
+ )
287
+
288
+ def _get_market_data_ex(self, params):
289
+ return self.context.get_market_data_ex(
290
+ params.get("field_list", []),
291
+ params.get("stock_list", []),
292
+ params.get("period", "1d"),
293
+ params.get("start_time", ""),
294
+ params.get("end_time", ""),
295
+ params.get("count", -1),
296
+ params.get("dividend_type", "none"),
297
+ params.get("fill_data", True),
298
+ )
299
+
300
+ def _subscribe_quote(self, params, msg):
301
+ stock_code = params.get("stock_code", "")
302
+ period = params.get("period", "1d")
303
+ dividend_type = params.get("dividend_type") or "none"
304
+ client_id = msg.get("client_id")
305
+ holder = {"id": None}
306
+
307
+ def callback(data):
308
+ subscribe_id = holder.get("id")
309
+ event = pack_event(
310
+ "quote:%s" % subscribe_id,
311
+ data=data,
312
+ client_id=client_id,
313
+ subscription_id=subscribe_id,
314
+ )
315
+ self._push("event", event, client_id)
316
+
317
+ func = getattr(self.context, "subscribe_quote")
318
+ subscribe_id = self._call_variants(
319
+ func,
320
+ [
321
+ ((stock_code, period, dividend_type, "", callback), {}),
322
+ ((stock_code, period, params.get("start_time", ""), params.get("end_time", ""), params.get("count", 0), callback), {}),
323
+ ((stock_code, period, callback), {}),
324
+ ],
325
+ )
326
+ holder["id"] = subscribe_id
327
+ self._remember_subscription(subscribe_id, client_id, "quote", params)
328
+ return {"subscribe_id": subscribe_id}
329
+
330
+ def _subscribe_whole_quote(self, params, msg):
331
+ code_list = params.get("code_list", params.get("stock_list", []))
332
+ client_id = msg.get("client_id")
333
+ holder = {"id": None}
334
+
335
+ def callback(data):
336
+ subscribe_id = holder.get("id")
337
+ event = pack_event(
338
+ "quote:%s" % subscribe_id,
339
+ data=data,
340
+ client_id=client_id,
341
+ subscription_id=subscribe_id,
342
+ )
343
+ self._push("event", event, client_id)
344
+
345
+ func = getattr(self.context, "subscribe_whole_quote")
346
+ subscribe_id = self._call_variants(
347
+ func,
348
+ [
349
+ ((code_list,), {"callback": callback}),
350
+ ((code_list, callback), {}),
351
+ ],
352
+ )
353
+ holder["id"] = subscribe_id
354
+ self._remember_subscription(subscribe_id, client_id, "whole_quote", params)
355
+ return {"subscribe_id": subscribe_id}
356
+
357
+ def _remember_subscription(self, subscribe_id, client_id, kind, params):
358
+ self.subscriptions[subscribe_id] = {
359
+ "client_id": client_id,
360
+ "kind": kind,
361
+ "params": params,
362
+ }
363
+ self.client_subscriptions.setdefault(client_id, set()).add(subscribe_id)
364
+
365
+ def _unsubscribe_quote(self, params):
366
+ subscribe_id = params.get("subscribe_id")
367
+ try:
368
+ func = getattr(self.context, "unsubscribe_quote")
369
+ result = func(subscribe_id)
370
+ finally:
371
+ info = self.subscriptions.pop(subscribe_id, None)
372
+ if info:
373
+ client_id = info.get("client_id")
374
+ if client_id in self.client_subscriptions:
375
+ self.client_subscriptions[client_id].discard(subscribe_id)
376
+ return result
377
+
378
+ def _download_event_meta(self, params, kind, stage):
379
+ meta = {
380
+ "download": True,
381
+ "download_kind": kind,
382
+ "stage": stage,
383
+ "bridge_id": getattr(self, "bridge_id", "default"),
384
+ }
385
+ job_id = params.get("download_job_id") or params.get("job_id")
386
+ if job_id:
387
+ meta["job_id"] = str(job_id)
388
+ for name in ("stock_code", "period", "start_time", "end_time"):
389
+ value = params.get(name)
390
+ if value not in (None, ""):
391
+ meta[name] = value
392
+ for name in ("stock_list", "code_list", "table_list"):
393
+ value = params.get(name)
394
+ if value:
395
+ meta[name] = value
396
+ return meta
397
+
398
+ def _send_download_event(self, client_id, params, kind, stage, data=None):
399
+ callback_event = params.get("callback_event")
400
+ if not callback_event or not client_id:
401
+ return
402
+ self._send_event(
403
+ client_id,
404
+ callback_event,
405
+ data if data is not None else {},
406
+ meta=self._download_event_meta(params, kind, stage),
407
+ )
408
+
409
+ def _download_history_data(self, params, msg=None):
410
+ client_id = msg.get("client_id") if msg else None
411
+ emit_lifecycle = bool(params.get("download_emit_lifecycle"))
412
+ func = self._get_callable("download_history_data", "down_history_data")
413
+ if not func:
414
+ if emit_lifecycle:
415
+ self._send_download_event(client_id, params, "history", "error", {
416
+ "stage": "error",
417
+ "error": "当前QMT内置ContextInfo未提供download_history_data",
418
+ })
419
+ raise NotImplementedError("当前QMT内置ContextInfo未提供download_history_data")
420
+ variants = []
421
+ incrementally = params.get("incrementally")
422
+ if incrementally is not None:
423
+ variants.append((
424
+ (
425
+ params.get("stock_code", ""),
426
+ params.get("period", "1d"),
427
+ params.get("start_time", ""),
428
+ params.get("end_time", ""),
429
+ incrementally,
430
+ ),
431
+ {},
432
+ ))
433
+ variants.append((
434
+ (
435
+ params.get("stock_code", ""),
436
+ params.get("period", "1d"),
437
+ params.get("start_time", ""),
438
+ params.get("end_time", ""),
439
+ ),
440
+ {},
441
+ ))
442
+ if emit_lifecycle:
443
+ self._send_download_event(client_id, params, "history", "submitted", {
444
+ "stage": "submitted",
445
+ "message": "history download request submitted",
446
+ })
447
+ try:
448
+ result = self._call_variants(func, variants)
449
+ except Exception as e:
450
+ if emit_lifecycle:
451
+ self._send_download_event(client_id, params, "history", "error", {
452
+ "stage": "error",
453
+ "error": str(e),
454
+ })
455
+ raise
456
+ if emit_lifecycle:
457
+ self._send_download_event(client_id, params, "history", "request_done", {
458
+ "stage": "request_done",
459
+ "result": result,
460
+ })
461
+ return result
462
+
463
+ def _download_history_data2(self, params, msg):
464
+ client_id = msg.get("client_id")
465
+ callback_event = params.get("callback_event")
466
+ emit_lifecycle = bool(params.get("download_emit_lifecycle"))
467
+ func = self._get_callable("download_history_data2", "down_history_data2")
468
+ if not func:
469
+ if emit_lifecycle:
470
+ self._send_download_event(client_id, params, "history", "error", {
471
+ "stage": "error",
472
+ "error": "当前QMT环境未提供download_history_data2",
473
+ })
474
+ raise NotImplementedError("当前QMT环境未提供download_history_data2")
475
+
476
+ def callback(data):
477
+ if callback_event and client_id:
478
+ self._send_event(
479
+ client_id,
480
+ callback_event,
481
+ data,
482
+ meta=self._download_event_meta(params, "history", "progress"),
483
+ )
484
+
485
+ callback_func = callback if callback_event else None
486
+ variants = []
487
+ incrementally = params.get("incrementally")
488
+ if incrementally is not None:
489
+ variants.append((
490
+ (
491
+ params.get("stock_list", params.get("code_list", [])),
492
+ params.get("period", "1d"),
493
+ params.get("start_time", ""),
494
+ params.get("end_time", ""),
495
+ callback_func,
496
+ incrementally,
497
+ ),
498
+ {},
499
+ ))
500
+ variants.append((
501
+ (
502
+ params.get("stock_list", params.get("code_list", [])),
503
+ params.get("period", "1d"),
504
+ params.get("start_time", ""),
505
+ params.get("end_time", ""),
506
+ callback_func,
507
+ ),
508
+ {},
509
+ ))
510
+ if emit_lifecycle:
511
+ self._send_download_event(client_id, params, "history", "submitted", {
512
+ "stage": "submitted",
513
+ "message": "history download request submitted",
514
+ })
515
+ try:
516
+ result = self._call_variants(func, variants)
517
+ except Exception as e:
518
+ if emit_lifecycle:
519
+ self._send_download_event(client_id, params, "history", "error", {
520
+ "stage": "error",
521
+ "error": str(e),
522
+ })
523
+ raise
524
+ if emit_lifecycle:
525
+ self._send_download_event(client_id, params, "history", "request_done", {
526
+ "stage": "request_done",
527
+ "result": result,
528
+ })
529
+ return result
530
+
531
+ def _get_instrument_detail(self, params):
532
+ func = getattr(self.context, "get_instrument_detail")
533
+ return func(params.get("stock_code", ""))
534
+
535
+ def _order_stock(self, params):
536
+ account = params.get("account") or {}
537
+ account_id = account.get("account_id", "")
538
+ user_order_id = self._first_param(
539
+ params,
540
+ ("order_remark", "remark", "strategy_name"),
541
+ "cfquant_%s" % int(time.time() * 1000),
542
+ )
543
+ args = (
544
+ params.get("optype", params.get("order_type")),
545
+ params.get("qmt_order_type", 1101),
546
+ account_id,
547
+ params.get("stock_code", ""),
548
+ params.get("price_type"),
549
+ params.get("price"),
550
+ params.get("order_volume"),
551
+ params.get("strategy_name", ""),
552
+ params.get("quick_trade", 2),
553
+ user_order_id,
554
+ )
555
+ passorder = getattr(self.context, "passorder", None) or self._get_global_func("passorder")
556
+ if passorder is None:
557
+ raise NotImplementedError("当前QMT环境未找到passorder函数")
558
+ try:
559
+ result = passorder(*args, self.context)
560
+ except TypeError:
561
+ result = passorder(*args)
562
+ order_id = result if isinstance(result, (int, str)) else None
563
+ if order_id is None:
564
+ order_id = self._find_order_id(
565
+ account_id,
566
+ user_order_id,
567
+ params.get("strategy_name", ""),
568
+ params.get("find_order_wait", 0.3),
569
+ )
570
+ return {
571
+ "order_id": order_id if order_id is not None else -1,
572
+ "request_result": result,
573
+ "order_remark": user_order_id,
574
+ }
575
+
576
+ def _order_stock_async(self, params, msg):
577
+ seq = params.get("seq")
578
+ client_id = msg.get("client_id")
579
+ try:
580
+ result = self._order_stock(params)
581
+ data = dict(result)
582
+ data.update({
583
+ "seq": seq,
584
+ "account_id": (params.get("account") or {}).get("account_id", ""),
585
+ "strategy_name": params.get("strategy_name", ""),
586
+ "order_remark": result.get("order_remark", params.get("order_remark", "")),
587
+ })
588
+ self._send_trader_event(client_id, "on_order_stock_async_response", data)
589
+ return {"seq": seq, "request_result": result}
590
+ except Exception as e:
591
+ data = {
592
+ "seq": seq,
593
+ "account_id": (params.get("account") or {}).get("account_id", ""),
594
+ "order_id": -1,
595
+ "error_msg": str(e),
596
+ }
597
+ self._send_trader_event(client_id, "on_order_stock_async_response", data)
598
+ raise
599
+
600
+ def _cancel_order_stock(self, params):
601
+ cancel_func = self._get_global_func("cancel")
602
+ if cancel_func is None:
603
+ raise NotImplementedError("当前QMT环境未找到cancel函数,暂不能撤单")
604
+ account = params.get("account") or {}
605
+ result = cancel_func(str(params.get("order_id")), account.get("account_id", ""), "STOCK", self.context)
606
+ return {"cancel_result": 0 if result else -1, "request_result": result}
607
+
608
+ def _cancel_order_stock_async(self, params, msg):
609
+ seq = params.get("seq")
610
+ client_id = msg.get("client_id")
611
+ result = self._cancel_order_stock(params)
612
+ data = dict(result)
613
+ data.update({
614
+ "seq": seq,
615
+ "account_id": (params.get("account") or {}).get("account_id", ""),
616
+ "order_id": params.get("order_id"),
617
+ })
618
+ self._send_trader_event(client_id, "on_cancel_order_stock_async_response", data)
619
+ return {"seq": seq, "request_result": result}
620
+
621
+ def _query_trade_detail(self, params, datatype):
622
+ account = params.get("account") or {}
623
+ func = self._get_global_func("get_trade_detail_data")
624
+ if not func and hasattr(self.context, "get_trade_detail_data"):
625
+ func = self.context.get_trade_detail_data
626
+ if not func:
627
+ raise NotImplementedError("当前QMT环境未找到get_trade_detail_data函数")
628
+ result = self._call_trade_detail_data(
629
+ func,
630
+ account.get("account_id", ""),
631
+ self._account_type_name(account.get("account_type")).lower(),
632
+ str(datatype).lower(),
633
+ )
634
+ rows = self._format_trade_detail_rows(result, datatype)
635
+ if str(datatype).upper() == "ORDER" and self._truthy_param(params.get("cancelable_only")):
636
+ rows = filter_cancelable_orders(rows)
637
+ return rows
638
+
639
+ def _call_trade_detail_data(self, func, account_id, account_type, datatype):
640
+ # QMT's fourth argument is strategyname, not ContextInfo.
641
+ variants = [
642
+ ((account_id, account_type, datatype), {}),
643
+ ((account_id, account_type, datatype, ""), {}),
644
+ ]
645
+ last_error = None
646
+ for args, kwargs in variants:
647
+ try:
648
+ return func(*args, **kwargs)
649
+ except TypeError as e:
650
+ last_error = e
651
+ continue
652
+ if last_error is not None:
653
+ raise last_error
654
+ raise RuntimeError("no available trade detail call variant")
655
+
656
+ def _format_trade_detail_rows(self, rows, datatype):
657
+ datatype = str(datatype).upper()
658
+ rows = rows or []
659
+ return [self._format_trade_detail(row, datatype) for row in rows]
660
+
661
+ def _format_trade_detail(self, obj, datatype):
662
+ if datatype == "ORDER":
663
+ return {
664
+ "stock_code": self._stock_code_from_trade_obj(obj),
665
+ "market": self._get_value(obj, "m_strExchangeID"),
666
+ "instrument_name": self._get_value(obj, "m_strInstrumentName"),
667
+ "order_source": self._order_source(obj),
668
+ "order_time": self._first_value(obj, (
669
+ "order_time",
670
+ "entrust_time",
671
+ "insert_time",
672
+ "m_strOrderTime",
673
+ "m_strEntrustTime",
674
+ "m_strInsertTime",
675
+ "m_nOrderTime",
676
+ "m_nEntrustTime",
677
+ "m_nInsertTime",
678
+ )),
679
+ "order_date": self._first_value(obj, (
680
+ "order_date",
681
+ "entrust_date",
682
+ "m_strOrderDate",
683
+ "m_strEntrustDate",
684
+ "m_strTradingDay",
685
+ "m_nOrderDate",
686
+ "m_nEntrustDate",
687
+ )),
688
+ "offset_flag": self._get_value(obj, "m_nOffsetFlag"),
689
+ "order_volume": self._get_value(obj, "m_nVolumeTotalOriginal"),
690
+ "traded_price": self._get_value(obj, "m_dTradedPrice"),
691
+ "traded_volume": self._get_value(obj, "m_nVolumeTraded"),
692
+ "trade_amount": self._get_value(obj, "m_dTradeAmount"),
693
+ "order_status": self._get_value(obj, "m_nOrderStatus"),
694
+ "m_strInstrumentID": self._get_value(obj, "m_strInstrumentID"),
695
+ "m_strExchangeID": self._get_value(obj, "m_strExchangeID"),
696
+ "m_strInstrumentName": self._get_value(obj, "m_strInstrumentName"),
697
+ "m_nOffsetFlag": self._get_value(obj, "m_nOffsetFlag"),
698
+ "m_nVolumeTotalOriginal": self._get_value(obj, "m_nVolumeTotalOriginal"),
699
+ "m_dTradedPrice": self._get_value(obj, "m_dTradedPrice"),
700
+ "m_nVolumeTraded": self._get_value(obj, "m_nVolumeTraded"),
701
+ "m_dTradeAmount": self._get_value(obj, "m_dTradeAmount"),
702
+ "m_strRemark": self._get_value(obj, "m_strRemark"),
703
+ "m_strStrategyName": self._get_value(obj, "m_strStrategyName"),
704
+ "m_strOrderSysID": self._get_value(obj, "m_strOrderSysID"),
705
+ "m_nOrderID": self._get_value(obj, "m_nOrderID"),
706
+ "m_strOrderID": self._get_value(obj, "m_strOrderID"),
707
+ "m_nOrderStatus": self._get_value(obj, "m_nOrderStatus"),
708
+ "m_strOrderStatus": self._get_value(obj, "m_strOrderStatus"),
709
+ "m_nOrderState": self._get_value(obj, "m_nOrderState"),
710
+ "m_strStatus": self._get_value(obj, "m_strStatus"),
711
+ "m_strOrderTime": self._get_value(obj, "m_strOrderTime"),
712
+ "m_strEntrustTime": self._get_value(obj, "m_strEntrustTime"),
713
+ "m_strInsertTime": self._get_value(obj, "m_strInsertTime"),
714
+ "m_nOrderTime": self._get_value(obj, "m_nOrderTime"),
715
+ "m_nEntrustTime": self._get_value(obj, "m_nEntrustTime"),
716
+ "m_nInsertTime": self._get_value(obj, "m_nInsertTime"),
717
+ "m_strOrderDate": self._get_value(obj, "m_strOrderDate"),
718
+ "m_strEntrustDate": self._get_value(obj, "m_strEntrustDate"),
719
+ "m_strTradingDay": self._get_value(obj, "m_strTradingDay"),
720
+ }
721
+ if datatype == "DEAL":
722
+ return {
723
+ "stock_code": self._stock_code_from_trade_obj(obj),
724
+ "market": self._get_value(obj, "m_strExchangeID"),
725
+ "instrument_name": self._get_value(obj, "m_strInstrumentName"),
726
+ "offset_flag": self._get_value(obj, "m_nOffsetFlag"),
727
+ "price": self._get_value(obj, "m_dPrice"),
728
+ "volume": self._get_value(obj, "m_nVolume"),
729
+ "trade_amount": self._get_value(obj, "m_dTradeAmount"),
730
+ "m_strInstrumentID": self._get_value(obj, "m_strInstrumentID"),
731
+ "m_strExchangeID": self._get_value(obj, "m_strExchangeID"),
732
+ "m_strInstrumentName": self._get_value(obj, "m_strInstrumentName"),
733
+ "m_nOffsetFlag": self._get_value(obj, "m_nOffsetFlag"),
734
+ "m_dPrice": self._get_value(obj, "m_dPrice"),
735
+ "m_nVolume": self._get_value(obj, "m_nVolume"),
736
+ "m_dTradeAmount": self._get_value(obj, "m_dTradeAmount"),
737
+ }
738
+ if datatype == "POSITION":
739
+ return {
740
+ "stock_code": self._stock_code_from_trade_obj(obj),
741
+ "market": self._get_value(obj, "m_strExchangeID"),
742
+ "instrument_name": self._get_value(obj, "m_strInstrumentName"),
743
+ "volume": self._get_value(obj, "m_nVolume"),
744
+ "can_use_volume": self._get_value(obj, "m_nCanUseVolume"),
745
+ "open_price": self._get_value(obj, "m_dOpenPrice"),
746
+ "market_value": self._get_value(obj, "m_dInstrumentValue"),
747
+ "position_cost": self._get_value(obj, "m_dPositionCost"),
748
+ "position_profit": self._get_value(obj, "m_dPositionProfit"),
749
+ "m_strInstrumentID": self._get_value(obj, "m_strInstrumentID"),
750
+ "m_strExchangeID": self._get_value(obj, "m_strExchangeID"),
751
+ "m_strInstrumentName": self._get_value(obj, "m_strInstrumentName"),
752
+ "m_nVolume": self._get_value(obj, "m_nVolume"),
753
+ "m_nCanUseVolume": self._get_value(obj, "m_nCanUseVolume"),
754
+ "m_dOpenPrice": self._get_value(obj, "m_dOpenPrice"),
755
+ "m_dInstrumentValue": self._get_value(obj, "m_dInstrumentValue"),
756
+ "m_dPositionCost": self._get_value(obj, "m_dPositionCost"),
757
+ "m_dPositionProfit": self._get_value(obj, "m_dPositionProfit"),
758
+ }
759
+ if datatype == "ACCOUNT":
760
+ return {
761
+ "balance": self._get_value(obj, "m_dBalance"),
762
+ "assure_asset": self._get_value(obj, "m_dAssureAsset"),
763
+ "market_value": self._get_value(obj, "m_dInstrumentValue"),
764
+ "total_debit": self._get_value(obj, "m_dTotalDebit"),
765
+ "available": self._get_value(obj, "m_dAvailable"),
766
+ "position_profit": self._get_value(obj, "m_dPositionProfit"),
767
+ "m_dBalance": self._get_value(obj, "m_dBalance"),
768
+ "m_dAssureAsset": self._get_value(obj, "m_dAssureAsset"),
769
+ "m_dInstrumentValue": self._get_value(obj, "m_dInstrumentValue"),
770
+ "m_dTotalDebit": self._get_value(obj, "m_dTotalDebit"),
771
+ "m_dAvailable": self._get_value(obj, "m_dAvailable"),
772
+ "m_dPositionProfit": self._get_value(obj, "m_dPositionProfit"),
773
+ }
774
+ return self._object_to_dict(obj)
775
+
776
+ def _stock_code_from_trade_obj(self, obj):
777
+ instrument_id = self._get_value(obj, "m_strInstrumentID")
778
+ exchange_id = self._get_value(obj, "m_strExchangeID")
779
+ if instrument_id and exchange_id:
780
+ return "%s.%s" % (instrument_id, exchange_id)
781
+ return instrument_id
782
+
783
+ def _order_source(self, obj):
784
+ values = [
785
+ self._get_value(obj, name)
786
+ for name in (
787
+ "order_source",
788
+ "source",
789
+ "order_remark",
790
+ "strategy_name",
791
+ "m_strRemark",
792
+ "m_strOrderRemark",
793
+ "m_strStrategyName",
794
+ )
795
+ ]
796
+ text = " ".join(str(value or "") for value in values).strip().lower()
797
+ return "cfquant" if "cfquant" in text else "other"
798
+
799
+ def _first_value(self, obj, names):
800
+ for name in names:
801
+ value = self._get_value(obj, name)
802
+ if value is not None and value != "":
803
+ return value
804
+ return None
805
+
806
+ def _object_to_dict(self, obj):
807
+ if hasattr(obj, "items"):
808
+ return dict(obj)
809
+ if hasattr(obj, "__dict__"):
810
+ return dict(vars(obj))
811
+ return {"value": str(obj)}
812
+
813
+ def _find_order_id(self, account_id, user_order_id, strategy_name="", wait_seconds=0.3):
814
+ wait_seconds = float(wait_seconds or 0)
815
+ deadline = time.time() + wait_seconds
816
+ while time.time() <= deadline:
817
+ try:
818
+ orders = self._query_trade_detail(
819
+ {"account": {"account_id": account_id, "account_type": 2}, "strategy_name": strategy_name},
820
+ "ORDER",
821
+ )
822
+ for order in orders or []:
823
+ remark = self._get_value(order, "m_strRemark")
824
+ if remark != user_order_id:
825
+ continue
826
+ for attr in ("m_strOrderSysID", "m_nOrderID", "order_id", "m_strOrderID"):
827
+ value = self._get_value(order, attr)
828
+ if value is not None:
829
+ return value
830
+ except Exception:
831
+ pass
832
+ if wait_seconds <= 0:
833
+ break
834
+ time.sleep(0.05)
835
+ return None
836
+
837
+ def _send_trader_event(self, client_id, name, data):
838
+ self._send_event(client_id, "trader:%s" % name, data)
839
+
840
+ def _send_event(self, client_id, name, data, subscription_id=None, meta=None):
841
+ event = pack_event(
842
+ name,
843
+ data=data,
844
+ client_id=client_id,
845
+ subscription_id=subscription_id,
846
+ meta=meta,
847
+ )
848
+ self._push("event", event, client_id)
849
+
850
+ def _push(self, key, payload, channel):
851
+ if not channel:
852
+ self._log("cfquant推送缺少channel key=%s payload=%s" % (key, self._brief(payload)))
853
+ return
854
+ tx = self.tx
855
+ if tx is None:
856
+ self._log("cfquant TX未连接,无法推送 channel=%s key=%s" % (channel, key))
857
+ return
858
+ result = tx.push(key, payload, channel)
859
+ if isinstance(result, dict) and result.get("code", 0) != 0:
860
+ self._log("cfquant TX推送失败 channel=%s result=%s" % (channel, result))
861
+
862
+ def _call_variants(self, func, variants):
863
+ last_error = None
864
+ for args, kwargs in variants:
865
+ try:
866
+ return func(*args, **kwargs)
867
+ except TypeError as e:
868
+ last_error = e
869
+ if last_error is not None:
870
+ raise last_error
871
+ raise RuntimeError("没有可用调用参数")
872
+
873
+ def _get_callable(self, *names):
874
+ for owner in (self.context, getattr(self.context, "context", None)):
875
+ if owner is None:
876
+ continue
877
+ for name in names:
878
+ func = getattr(owner, name, None)
879
+ if callable(func):
880
+ return func
881
+ for name in names:
882
+ func = self._get_global_func(name)
883
+ if callable(func):
884
+ return func
885
+ return None
886
+
887
+ def _enable_auto_trade_callback(self):
888
+ if self.context is None or self.auto_trade_callback_enabled:
889
+ return
890
+ func = getattr(self.context, "set_auto_trade_callback", None)
891
+ if callable(func):
892
+ try:
893
+ result = func(True)
894
+ self.auto_trade_callback_enabled = True
895
+ self._log("auto trade callback enabled result=%s" % result)
896
+ return
897
+ except Exception as e:
898
+ self._log("auto trade callback enable failed:%s" % e)
899
+ return
900
+ func = self._get_callable("set_auto_trade_callback")
901
+ if not callable(func):
902
+ self._log("auto trade callback enable skipped: set_auto_trade_callback not found")
903
+ return
904
+ try:
905
+ result = func(self.context, True)
906
+ self.auto_trade_callback_enabled = True
907
+ self._log("auto trade callback enabled result=%s" % result)
908
+ except TypeError:
909
+ try:
910
+ result = func(True)
911
+ self.auto_trade_callback_enabled = True
912
+ self._log("auto trade callback enabled result=%s" % result)
913
+ except Exception as e:
914
+ self._log("auto trade callback enable failed:%s" % e)
915
+ except Exception as e:
916
+ self._log("auto trade callback enable failed:%s" % e)
917
+
918
+ def _get_global_func(self, name):
919
+ func = self.globals_dict.get(name)
920
+ if callable(func):
921
+ return func
922
+ return None
923
+
924
+ def _get_value(self, obj, name):
925
+ if hasattr(obj, name):
926
+ return getattr(obj, name)
927
+ if hasattr(obj, "get"):
928
+ return obj.get(name)
929
+ return None
930
+
931
+ def _first_param(self, params, names, default=None):
932
+ for name in names:
933
+ value = params.get(name)
934
+ if value is not None and value != "":
935
+ return value
936
+ return default
937
+
938
+ def _truthy_param(self, value):
939
+ if isinstance(value, str):
940
+ return value.strip().lower() in ("1", "true", "yes", "y", "on")
941
+ return bool(value)
942
+
943
+ def _account_type_name(self, account_type):
944
+ mapping = {
945
+ 1: "FUTURE",
946
+ 2: "STOCK",
947
+ 3: "CREDIT",
948
+ 5: "FUTURE_OPTION",
949
+ 6: "STOCK_OPTION",
950
+ 7: "HUGANGTONG",
951
+ 10: "NEW3BOARD",
952
+ 11: "SHENGANGTONG",
953
+ }
954
+ if isinstance(account_type, str):
955
+ return account_type
956
+ return mapping.get(account_type, "STOCK")
957
+
958
+ def _load_txl(self):
959
+ package_error = None
960
+ try:
961
+ from .tx import txl
962
+ return txl
963
+ except Exception as e:
964
+ package_error = e
965
+ try:
966
+ from tx import txl
967
+ return txl
968
+ except Exception as path_error:
969
+ raise RuntimeError("无法导入 LTtx txl,请确认 cfquant 包完整或 tx.py 在 QMT Python 路径中: %s; fallback: %s" % (package_error, path_error))
970
+
971
+ def _log(self, msg):
972
+ msg = translate_log(msg)
973
+ line = "%s %s" % (self._timestamp_ms(), msg)
974
+ try:
975
+ self.log_queue.put_nowait(line)
976
+ except Exception:
977
+ pass
978
+ if self.show and self._should_print(msg):
979
+ print(msg)
980
+
981
+ def _start_log_thread(self):
982
+ if self.log_thread is not None and self.log_thread.is_alive():
983
+ return
984
+ self.log_thread = threading.Thread(target=self._log_loop)
985
+ self.log_thread.daemon = True
986
+ self.log_thread.start()
987
+
988
+ def _log_loop(self):
989
+ buffer = []
990
+ while self.running or not self.log_queue.empty():
991
+ try:
992
+ line = self.log_queue.get(timeout=0.5)
993
+ buffer.append(line)
994
+ while len(buffer) < 100:
995
+ try:
996
+ buffer.append(self.log_queue.get_nowait())
997
+ except Exception:
998
+ break
999
+ except Exception:
1000
+ pass
1001
+ if not buffer:
1002
+ continue
1003
+ try:
1004
+ with self.log_lock:
1005
+ with open(self.log_file, "a", encoding="utf-8") as f:
1006
+ f.write("\n".join(buffer) + "\n")
1007
+ except Exception:
1008
+ pass
1009
+ buffer = []
1010
+
1011
+ def _should_print(self, msg):
1012
+ if msg.startswith("stage="):
1013
+ return False
1014
+ if " action=" in msg and " id=" in msg:
1015
+ return False
1016
+ return True
1017
+
1018
+ def _default_log_file(self):
1019
+ try:
1020
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1021
+ except Exception:
1022
+ base_dir = os.getcwd()
1023
+ log_dir = (
1024
+ os.environ.get("CFQUANT_QMT_LOG_DIR")
1025
+ or os.environ.get("CFQUANT_LOG_DIR")
1026
+ or os.path.join(base_dir, "log")
1027
+ )
1028
+ log_dir = os.path.abspath(log_dir)
1029
+ try:
1030
+ os.makedirs(log_dir, exist_ok=True)
1031
+ except Exception:
1032
+ log_dir = base_dir
1033
+ return os.path.join(log_dir, "cfquant_qmt_bridge.log")
1034
+
1035
+ def _timestamp_ms(self):
1036
+ now = time.time()
1037
+ local = time.localtime(now)
1038
+ return "%s.%03d" % (time.strftime("%Y-%m-%d %H:%M:%S", local), int((now - int(now)) * 1000))
1039
+
1040
+ def _elapsed_ms(self, start, end=None):
1041
+ if end is None:
1042
+ end = time.perf_counter()
1043
+ return (end - start) * 1000.0
1044
+
1045
+ def _brief(self, value, limit=500):
1046
+ try:
1047
+ text = repr(value)
1048
+ except Exception:
1049
+ text = "<unrepresentable>"
1050
+ if len(text) > limit:
1051
+ text = text[:limit] + "...(%s chars)" % len(text)
1052
+ return text
1053
+
1054
+ def _safe_qsize(self, q):
1055
+ try:
1056
+ return q.qsize()
1057
+ except Exception:
1058
+ return -1
1059
+
1060
+
1061
+ def start_cfquant_bridge(
1062
+ context,
1063
+ ip=None,
1064
+ port=None,
1065
+ token=None,
1066
+ request_channel=None,
1067
+ show=True,
1068
+ ):
1069
+ try:
1070
+ globals_dict = sys._getframe(1).f_globals
1071
+ except Exception:
1072
+ globals_dict = {}
1073
+ return CfquantQmtBridge(
1074
+ context,
1075
+ ip=ip,
1076
+ port=port,
1077
+ token=token,
1078
+ request_channel=request_channel,
1079
+ show=show,
1080
+ globals_dict=globals_dict,
1081
+ ).start()