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
@@ -0,0 +1,676 @@
1
+ # -*- coding: utf-8 -*-
2
+ import datetime as dt
3
+ import json
4
+ import queue
5
+ import threading
6
+ import time
7
+
8
+ from .protocol import loads_message, pack_event, pack_response
9
+ from .tx_trade_bridge import TxTradeBridge
10
+
11
+
12
+ COALESCED_QUERY_ACTIONS = set([
13
+ "xttrader.query_stock_asset",
14
+ "xttrader.query_stock_positions",
15
+ "xttrader.query_stock_orders",
16
+ "xttrader.query_stock_trades",
17
+ "xttrader.query_credit_detail",
18
+ "xttrader.query_credit_subjects",
19
+ "xttrader.query_credit_slo_code",
20
+ "xttrader.query_credit_assure",
21
+ "xttrader.query_stk_compacts",
22
+ ])
23
+
24
+
25
+ class NormalQmtBridge(TxTradeBridge):
26
+ def __init__(
27
+ self,
28
+ context,
29
+ ip="127.0.0.1",
30
+ port=2049,
31
+ token="LTtx",
32
+ request_channel="cfquant.request",
33
+ callback_event_channel="cfquant.callback.event",
34
+ bridge_id="default",
35
+ account_id="",
36
+ show=True,
37
+ globals_dict=None,
38
+ schedule_timer=True,
39
+ pump_max_count=20,
40
+ pump_max_ms=0,
41
+ dispatch_on_qmt_thread=False,
42
+ ):
43
+ super(NormalQmtBridge, self).__init__(
44
+ context,
45
+ ip=ip,
46
+ port=port,
47
+ token=token,
48
+ request_channel=request_channel,
49
+ bridge_id=bridge_id,
50
+ account_id=account_id,
51
+ show=show,
52
+ globals_dict=globals_dict,
53
+ )
54
+ self.request_queue = queue.Queue(maxsize=10000)
55
+ self.recv_thread = None
56
+ self.worker_thread = None
57
+ self.worker_event = threading.Event()
58
+ self.worker_source = ""
59
+ self.worker_source_lock = threading.Lock()
60
+ self.pump_max_count = int(pump_max_count)
61
+ self.pump_max_ms = float(pump_max_ms)
62
+ self.dispatch_on_qmt_thread = bool(dispatch_on_qmt_thread)
63
+ self.dispatch_lock = threading.RLock()
64
+ self.coalesce_lock = threading.RLock()
65
+ self.coalesced_requests = {}
66
+ self.coalesce_join_count = 0
67
+ self.coalesce_dispatch_count = 0
68
+ self.subscription_seq = 0
69
+ self.quote_subscriptions = {}
70
+ self.whole_quote_publish_sub_id = None
71
+ self.whole_quote_publish_enabled = False
72
+ self.whole_quote_sub_id = None
73
+ self.schedule_key = None
74
+ self.callback_event_channel = callback_event_channel
75
+ self.bridge_id = bridge_id or "default"
76
+ self.schedule_timer = bool(schedule_timer)
77
+
78
+ def start(self):
79
+ if self.running:
80
+ return self
81
+ self.running = True
82
+ if not self.started_at:
83
+ self.started_at = time.time()
84
+ txl = self._load_txl()
85
+ self.tx = txl(self.ip, self.port, self.token)
86
+ self.tx.start_tx()
87
+ self.tx.start_txg(self.request_channel)
88
+ self.recv_thread = threading.Thread(target=self._recv_loop)
89
+ self.recv_thread.daemon = True
90
+ self.recv_thread.start()
91
+ self._log(
92
+ "normal bridge started LTtx=%s:%s request_channel=%s"
93
+ % (self.ip, self.port, self.request_channel)
94
+ )
95
+ self._publish_runtime_report("start")
96
+ return self
97
+
98
+ def set_context(self, context):
99
+ self.context = context
100
+ if self.account_id:
101
+ self._set_context_account(self.account_id, self.account_type)
102
+ self._enable_auto_trade_callback()
103
+ self._subscribe_internal_whole_quote()
104
+ if self.dispatch_on_qmt_thread:
105
+ self._log("normal bridge QMT-thread dispatch enabled")
106
+ else:
107
+ self._start_worker_thread(context)
108
+ if self.schedule_timer:
109
+ self._schedule_timer()
110
+ if self.dispatch_on_qmt_thread:
111
+ dispatch_source = "QMT timer/handlebar callbacks" if self.schedule_timer else "QMT caller thread callbacks"
112
+ self._log("normal bridge requests are consumed by %s" % dispatch_source)
113
+ else:
114
+ self._log("normal bridge worker is released by quote/timer/handlebar callbacks")
115
+ self._log("normal bridge context ready")
116
+ self._publish_runtime_report("context_ready")
117
+
118
+ def close(self):
119
+ self.running = False
120
+ self.worker_event.set()
121
+ if self.context is not None and self.schedule_key:
122
+ try:
123
+ self.context.cancel_schedule_run(self.schedule_key)
124
+ except Exception:
125
+ pass
126
+ super(NormalQmtBridge, self).close()
127
+
128
+ def _recv_loop(self):
129
+ while self.running:
130
+ try:
131
+ raw = self.tx.Q.get()
132
+ if raw is None:
133
+ break
134
+ self._handle_raw_from_thread(raw)
135
+ except Exception as e:
136
+ if self.running:
137
+ self._log("normal bridge recv error: %s" % e)
138
+ time.sleep(0.05)
139
+
140
+ def _handle_raw_from_thread(self, raw):
141
+ msg = loads_message(raw)
142
+ if not msg or msg.get("type") != "request":
143
+ return
144
+ action = msg.get("action")
145
+ if action == "cfquant.ping":
146
+ self._send_response(msg, {"pong": True, "ts": time.time(), "request_channel": self.request_channel})
147
+ return
148
+ if action == "cfquant.status":
149
+ self._send_response(msg, self._status())
150
+ return
151
+ if action == "xtdata.subscribe_whole_quote":
152
+ self._handle_whole_quote_publish_subscribe(msg)
153
+ return
154
+ if action == "xtdata.subscribe_quote":
155
+ self._handle_quote_subscribe(msg, kind="quote")
156
+ return
157
+ if action == "xtdata.unsubscribe_quote":
158
+ self._handle_quote_unsubscribe(msg)
159
+ return
160
+ if self._try_enqueue_coalesced_request(msg):
161
+ return
162
+ try:
163
+ self.request_queue.put_nowait((msg, time.time(), None))
164
+ self._release_worker("enqueue")
165
+ self._log(
166
+ "normal bridge request queued action=%s id=%s queue_size=%s"
167
+ % (msg.get("action"), msg.get("id"), self.request_queue.qsize())
168
+ )
169
+ except queue.Full as e:
170
+ self._send_error(msg, e)
171
+
172
+ def _publish_runtime_report(self, reason):
173
+ super(NormalQmtBridge, self)._publish_runtime_report(reason)
174
+ if self.tx is None or not self.callback_event_channel:
175
+ return
176
+ try:
177
+ data = self._runtime_info()
178
+ data.update({
179
+ "reason": reason,
180
+ "transport": "lttx" if self.port else "pipe",
181
+ "channel_key": "normal",
182
+ "callback_event_channel": self.callback_event_channel,
183
+ })
184
+ payload = pack_event(
185
+ "cfquant.runtime",
186
+ data=data,
187
+ client_id=self.callback_event_channel,
188
+ meta={
189
+ "bridge_id": self.bridge_id,
190
+ "account_id": self.account_id,
191
+ "source": "qmt_runtime_report",
192
+ },
193
+ )
194
+ self.tx.push("event", payload, self.callback_event_channel)
195
+ self._log("normal bridge runtime report sent version=%s reason=%s" % (data.get("core_version") or "-", reason))
196
+ except Exception as e:
197
+ self._log("normal bridge runtime report failed:%s" % e)
198
+
199
+ def _start_worker_thread(self, context):
200
+ if self.worker_thread is not None and self.worker_thread.is_alive():
201
+ return
202
+ self.context = context
203
+ self.worker_thread = threading.Thread(target=self._worker_loop, args=(context,))
204
+ self.worker_thread.daemon = True
205
+ self.worker_thread.start()
206
+ self._log("normal bridge worker thread started in init context")
207
+
208
+ def _handle_quote_subscribe(self, msg, kind):
209
+ self.subscription_seq += 1
210
+ sub_id = self.subscription_seq
211
+ params = msg.get("params") or {}
212
+ self.quote_subscriptions[sub_id] = {
213
+ "kind": kind,
214
+ "client_id": msg.get("client_id"),
215
+ "stock_code": params.get("stock_code", ""),
216
+ "code_list": params.get("code_list", params.get("stock_list", [])),
217
+ }
218
+ self._send_response(msg, {"subscribe_id": sub_id})
219
+ self._log("normal bridge quote subscribed id=%s kind=%s" % (sub_id, kind))
220
+
221
+ def _handle_whole_quote_publish_subscribe(self, msg):
222
+ if self.whole_quote_publish_sub_id is None:
223
+ self.subscription_seq += 1
224
+ self.whole_quote_publish_sub_id = self.subscription_seq
225
+ sub_id = self.whole_quote_publish_sub_id
226
+ params = msg.get("params") or {}
227
+ self.quote_subscriptions[sub_id] = {
228
+ "kind": "whole_quote",
229
+ "client_id": msg.get("client_id"),
230
+ "code_list": params.get("code_list", params.get("stock_list", ["SH", "SZ"])),
231
+ "internal_subscribe_id": self.whole_quote_sub_id,
232
+ "publish_existing": True,
233
+ }
234
+ self.whole_quote_publish_enabled = True
235
+ self._send_response(msg, {
236
+ "subscribe_id": sub_id,
237
+ "internal_subscribe_id": self.whole_quote_sub_id,
238
+ "publish_existing": True,
239
+ })
240
+ self._log(
241
+ "normal bridge whole quote publish enabled id=%s internal_id=%s"
242
+ % (sub_id, self.whole_quote_sub_id)
243
+ )
244
+
245
+ def _handle_quote_unsubscribe(self, msg):
246
+ params = msg.get("params") or {}
247
+ sub_id = params.get("subscribe_id")
248
+ removed = self.quote_subscriptions.pop(sub_id, None)
249
+ if removed is None:
250
+ try:
251
+ removed = self.quote_subscriptions.pop(int(sub_id), None)
252
+ except Exception:
253
+ removed = None
254
+ if str(sub_id) == str(self.whole_quote_publish_sub_id):
255
+ self.whole_quote_publish_enabled = False
256
+ self._send_response(msg, True)
257
+ self._log("normal bridge quote unsubscribed id=%s" % sub_id)
258
+
259
+ def _try_enqueue_coalesced_request(self, msg):
260
+ coalesce_key = self._coalesce_key(msg)
261
+ if not coalesce_key:
262
+ return False
263
+ received_at = time.time()
264
+ action = msg.get("action")
265
+ with self.coalesce_lock:
266
+ current = self.coalesced_requests.get(coalesce_key)
267
+ if current is not None:
268
+ current["waiters"].append((msg, received_at))
269
+ self.coalesce_join_count += 1
270
+ self._log(
271
+ "normal bridge request coalesced action=%s id=%s waiters=%s"
272
+ % (action, msg.get("id"), len(current["waiters"]))
273
+ )
274
+ return True
275
+ entry = {
276
+ "key": coalesce_key,
277
+ "action": action,
278
+ "primary_id": msg.get("id"),
279
+ "waiters": [(msg, received_at)],
280
+ }
281
+ self.coalesced_requests[coalesce_key] = entry
282
+ try:
283
+ self.request_queue.put_nowait((msg, received_at, coalesce_key))
284
+ self._release_worker("enqueue")
285
+ self._log(
286
+ "normal bridge request queued action=%s id=%s queue_size=%s coalesced=1"
287
+ % (action, msg.get("id"), self.request_queue.qsize())
288
+ )
289
+ return True
290
+ except queue.Full as e:
291
+ with self.coalesce_lock:
292
+ if self.coalesced_requests.get(coalesce_key) is entry:
293
+ self.coalesced_requests.pop(coalesce_key, None)
294
+ self._send_error(msg, e)
295
+ return True
296
+
297
+ def _coalesce_key(self, msg):
298
+ action = msg.get("action")
299
+ if action not in COALESCED_QUERY_ACTIONS:
300
+ return None
301
+ params = msg.get("params") or {}
302
+ try:
303
+ params_key = json.dumps(params, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
304
+ except Exception:
305
+ params_key = repr(params)
306
+ return "%s|%s" % (action, params_key)
307
+
308
+ def pump(self):
309
+ if self.dispatch_on_qmt_thread:
310
+ return self._drain_requests("pump")
311
+ self._release_worker("pump")
312
+ return self.request_queue.qsize()
313
+
314
+ def on_timer(self, *args, **kwargs):
315
+ if self.dispatch_on_qmt_thread:
316
+ self._drain_requests("timer")
317
+ return
318
+ self._release_worker("timer")
319
+
320
+ def _release_worker(self, source):
321
+ with self.worker_source_lock:
322
+ self.worker_source = source
323
+ self.worker_event.set()
324
+
325
+ def _worker_loop(self, context=None):
326
+ if context is not None:
327
+ self.context = context
328
+ while self.running:
329
+ self.worker_event.wait(0.5)
330
+ if not self.running:
331
+ break
332
+ if not self.worker_event.is_set():
333
+ continue
334
+ self.worker_event.clear()
335
+ with self.worker_source_lock:
336
+ source = self.worker_source or "unknown"
337
+ try:
338
+ self._drain_requests(source)
339
+ except Exception as e:
340
+ self._log("normal bridge worker error source=%s error=%s" % (source, e))
341
+
342
+ def _drain_requests(self, source):
343
+ with self.dispatch_lock:
344
+ start = time.perf_counter()
345
+ count = 0
346
+ while self.running and count < self.pump_max_count:
347
+ if self.pump_max_ms > 0 and (time.perf_counter() - start) * 1000 >= self.pump_max_ms:
348
+ break
349
+ try:
350
+ item = self.request_queue.get_nowait()
351
+ except queue.Empty:
352
+ break
353
+ msg, received_at, coalesce_key = self._queue_item_parts(item)
354
+ if coalesce_key:
355
+ self._drain_coalesced_request(source, msg, received_at, coalesce_key)
356
+ else:
357
+ self._drain_single_request(source, msg, received_at)
358
+ count += 1
359
+ return count
360
+
361
+ def _queue_item_parts(self, item):
362
+ try:
363
+ if len(item) == 3:
364
+ return item
365
+ except Exception:
366
+ pass
367
+ msg, received_at = item
368
+ return msg, received_at, None
369
+
370
+ def _drain_single_request(self, source, msg, received_at):
371
+ try:
372
+ result = self._dispatch(msg.get("action"), msg.get("params") or {}, msg)
373
+ self._send_response(msg, result)
374
+ self._log(
375
+ "normal bridge worker response source=%s action=%s id=%s total_ms=%.2f"
376
+ % (source, msg.get("action"), msg.get("id"), (time.time() - received_at) * 1000)
377
+ )
378
+ except Exception as e:
379
+ self._log(
380
+ "normal bridge worker request_error source=%s action=%s id=%s error=%s"
381
+ % (source, msg.get("action"), msg.get("id"), e)
382
+ )
383
+ self._send_error(msg, e)
384
+
385
+ def _drain_coalesced_request(self, source, msg, received_at, coalesce_key):
386
+ try:
387
+ result = self._dispatch(msg.get("action"), msg.get("params") or {}, msg)
388
+ with self.coalesce_lock:
389
+ entry = self.coalesced_requests.pop(coalesce_key, None)
390
+ self.coalesce_dispatch_count += 1
391
+ waiters = entry.get("waiters", []) if entry else [(msg, received_at)]
392
+ for waiter_msg, _ in waiters:
393
+ self._send_response(waiter_msg, result)
394
+ self._log(
395
+ "normal bridge worker coalesced_response source=%s action=%s id=%s waiters=%s total_ms=%.2f"
396
+ % (source, msg.get("action"), msg.get("id"), len(waiters), (time.time() - received_at) * 1000)
397
+ )
398
+ except Exception as e:
399
+ with self.coalesce_lock:
400
+ entry = self.coalesced_requests.pop(coalesce_key, None)
401
+ self.coalesce_dispatch_count += 1
402
+ waiters = entry.get("waiters", []) if entry else [(msg, received_at)]
403
+ self._log(
404
+ "normal bridge worker coalesced_error source=%s action=%s id=%s waiters=%s error=%s"
405
+ % (source, msg.get("action"), msg.get("id"), len(waiters), e)
406
+ )
407
+ for waiter_msg, _ in waiters:
408
+ self._send_error(waiter_msg, e)
409
+
410
+ def _on_whole_quote(self, data):
411
+ self._release_worker("whole_quote")
412
+ if not self.quote_subscriptions:
413
+ return
414
+ for sub_id, sub in list(self.quote_subscriptions.items()):
415
+ if sub.get("kind") == "whole_quote" and not self.whole_quote_publish_enabled:
416
+ continue
417
+ client_id = sub.get("client_id")
418
+ if not client_id:
419
+ continue
420
+ event_data = data
421
+ if sub.get("kind") == "quote":
422
+ stock_code = sub.get("stock_code")
423
+ if stock_code and isinstance(data, dict):
424
+ value = data.get(stock_code)
425
+ if value is None:
426
+ continue
427
+ event_data = {stock_code: value}
428
+ event = pack_event(
429
+ "quote:%s" % sub_id,
430
+ data=event_data,
431
+ client_id=client_id,
432
+ subscription_id=sub_id,
433
+ )
434
+ self.tx.push("event", event, client_id)
435
+
436
+ def _on_timer(self, *args, **kwargs):
437
+ self.on_timer(*args, **kwargs)
438
+
439
+ def _subscribe_internal_whole_quote(self):
440
+ if self.context is None or self.whole_quote_sub_id:
441
+ return
442
+ try:
443
+ self.whole_quote_sub_id = self.context.subscribe_whole_quote(["SH", "SZ"], callback=self._on_whole_quote)
444
+ self._log("normal bridge internal whole quote subscribed id=%s" % self.whole_quote_sub_id)
445
+ except Exception as e:
446
+ self._log("normal bridge internal whole quote subscribe failed: %s" % e)
447
+
448
+ def _schedule_timer(self):
449
+ if self.context is None or self.schedule_key:
450
+ return
451
+ try:
452
+ first_time = dt.datetime.now() + dt.timedelta(seconds=1)
453
+ self.schedule_key = self.context.schedule_run(
454
+ self._on_timer,
455
+ first_time,
456
+ repeat_times=-1,
457
+ interval=dt.timedelta(milliseconds=500),
458
+ name="cfquant_normal_bridge_pump",
459
+ )
460
+ self._log("normal bridge timer scheduled key=%s" % self.schedule_key)
461
+ except Exception as e:
462
+ self._log("normal bridge timer schedule failed: %s" % e)
463
+
464
+ def _send_response(self, msg, result):
465
+ client_id = msg.get("client_id") or msg.get("reply_channel")
466
+ if not client_id:
467
+ return
468
+ response = pack_response(msg.get("id"), ok=True, result=result)
469
+ self.tx.push("response", response, client_id)
470
+
471
+ def _send_error(self, msg, error):
472
+ client_id = msg.get("client_id") or msg.get("reply_channel")
473
+ if not client_id:
474
+ return
475
+ self._log(
476
+ "normal bridge send_error action=%s id=%s client_id=%s error=%s"
477
+ % (msg.get("action"), msg.get("id"), client_id, error)
478
+ )
479
+ response = pack_response(msg.get("id"), ok=False, error=error)
480
+ self.tx.push("response", response, client_id)
481
+
482
+ def publish_callback_event(self, event_name, obj):
483
+ if self.tx is None:
484
+ return
485
+ data = self._callback_object_to_dict(obj)
486
+ account_id = self._callback_account_id(obj, data)
487
+ account_type = self._callback_account_type(obj, data)
488
+ payload = {
489
+ "type": "event",
490
+ "event": event_name,
491
+ "account_id": account_id,
492
+ "account_type": account_type,
493
+ "bridge_id": self.bridge_id,
494
+ "source": "CFQUANT",
495
+ "ts": int(time.time() * 1000),
496
+ "data": data,
497
+ }
498
+ self.tx.push("event", json.dumps(payload, ensure_ascii=False), self.callback_event_channel)
499
+ if account_id:
500
+ self._send_trader_event_to_account(account_id, event_name.replace("trader:", "", 1), data, account_type=account_type or None)
501
+ self._log("normal bridge callback event sent event=%s account=%s" % (event_name, account_id or "-"))
502
+
503
+ def _callback_object_to_dict(self, obj):
504
+ fields = [
505
+ "account_id",
506
+ "account_type",
507
+ "m_strAccountID",
508
+ "m_strAccountId",
509
+ "m_strAccount",
510
+ "m_accountID",
511
+ "m_nAccountType",
512
+ "m_strAccountType",
513
+ "order_source",
514
+ "source",
515
+ "order_remark",
516
+ "strategy_name",
517
+ "m_strStatus",
518
+ "m_strInstrumentID",
519
+ "m_strExchangeID",
520
+ "m_strInstrumentName",
521
+ "m_nOffsetFlag",
522
+ "m_nVolumeTotalOriginal",
523
+ "m_nVolumeTraded",
524
+ "m_nVolume",
525
+ "m_nCanUseVolume",
526
+ "m_dPrice",
527
+ "m_dTradedPrice",
528
+ "m_dTradeAmount",
529
+ "m_dBalance",
530
+ "m_dAssureAsset",
531
+ "m_dInstrumentValue",
532
+ "m_dTotalDebit",
533
+ "m_dAvailable",
534
+ "m_dPositionProfit",
535
+ "m_dOpenPrice",
536
+ "m_dPositionCost",
537
+ "m_strRemark",
538
+ "m_strOrderRemark",
539
+ "m_strStrategyName",
540
+ "m_strOrderSysID",
541
+ "m_strOrderID",
542
+ "m_nOrderID",
543
+ "m_nOrderStatus",
544
+ "m_strOrderStatus",
545
+ "m_nOrderState",
546
+ "m_strStatusMsg",
547
+ "m_strOrderTime",
548
+ "m_strEntrustTime",
549
+ "m_strInsertTime",
550
+ "m_nOrderTime",
551
+ "m_nEntrustTime",
552
+ "m_nInsertTime",
553
+ "m_strOrderDate",
554
+ "m_strEntrustDate",
555
+ "m_strTradingDay",
556
+ ]
557
+ data = {}
558
+ for field in fields:
559
+ value = self._get_value(obj, field)
560
+ if value is not None:
561
+ data[field] = value
562
+ code = data.get("m_strInstrumentID")
563
+ market = data.get("m_strExchangeID")
564
+ if code and market:
565
+ data["stock_code"] = "%s.%s" % (code, market)
566
+ source_text = " ".join(str(item or "") for item in (
567
+ data.get("order_source"),
568
+ data.get("source"),
569
+ data.get("order_remark"),
570
+ data.get("strategy_name"),
571
+ data.get("m_strRemark"),
572
+ data.get("m_strOrderRemark"),
573
+ data.get("m_strStrategyName"),
574
+ )).strip().lower()
575
+ data["order_source"] = "cfquant" if "cfquant" in source_text else "other"
576
+ return data
577
+
578
+ def _callback_account_id(self, obj, data):
579
+ for key in ("account_id", "m_strAccountID", "m_strAccountId", "m_strAccount", "m_accountID"):
580
+ value = data.get(key)
581
+ if value:
582
+ return str(value).strip()
583
+ for name in ("account_id", "m_strAccountID", "m_strAccountId", "m_strAccount", "m_accountID"):
584
+ value = self._get_value(obj, name)
585
+ if value:
586
+ return str(value).strip()
587
+ return str(self.account_id or "").strip()
588
+
589
+ def _callback_account_type(self, obj, data):
590
+ candidates = [
591
+ data.get("account_type") if isinstance(data, dict) else None,
592
+ data.get("m_nAccountType") if isinstance(data, dict) else None,
593
+ data.get("m_strAccountType") if isinstance(data, dict) else None,
594
+ self._get_value(obj, "account_type"),
595
+ self._get_value(obj, "m_nAccountType"),
596
+ self._get_value(obj, "m_strAccountType"),
597
+ ]
598
+ for value in candidates:
599
+ if value in (None, ""):
600
+ continue
601
+ text = str(value).strip().upper()
602
+ if text in ("2", "SECURITY", "SECURITY_ACCOUNT", "STOCK_ACCOUNT"):
603
+ return "STOCK"
604
+ if text in ("3", "CREDIT_ACCOUNT", "MARGIN"):
605
+ return "CREDIT"
606
+ return text
607
+ return ""
608
+
609
+ def _status_extra(self):
610
+ with self.coalesce_lock:
611
+ coalesced_waiters = sum(len(item.get("waiters", [])) for item in self.coalesced_requests.values())
612
+ coalesced_group_count = len(self.coalesced_requests)
613
+ return {
614
+ "request_queue_size": self.request_queue.qsize(),
615
+ "recv_thread_alive": self.recv_thread.is_alive() if self.recv_thread else False,
616
+ "worker_thread_alive": self.worker_thread.is_alive() if self.worker_thread else False,
617
+ "whole_quote_sub_id": self.whole_quote_sub_id,
618
+ "schedule_key": self.schedule_key,
619
+ "quote_subscription_count": len(self.quote_subscriptions),
620
+ "whole_quote_publish_enabled": self.whole_quote_publish_enabled,
621
+ "whole_quote_publish_sub_id": self.whole_quote_publish_sub_id,
622
+ "schedule_timer": self.schedule_timer,
623
+ "pump_max_count": self.pump_max_count,
624
+ "pump_max_ms": self.pump_max_ms,
625
+ "dispatch_on_qmt_thread": self.dispatch_on_qmt_thread,
626
+ "dispatch_thread": (
627
+ "qmt_timer_or_handlebar"
628
+ if self.dispatch_on_qmt_thread and self.schedule_timer
629
+ else "qmt_caller_thread"
630
+ if self.dispatch_on_qmt_thread
631
+ else "worker"
632
+ ),
633
+ "coalesced_group_count": coalesced_group_count,
634
+ "coalesced_waiter_count": coalesced_waiters,
635
+ "coalesce_join_count": self.coalesce_join_count,
636
+ "coalesce_dispatch_count": self.coalesce_dispatch_count,
637
+ }
638
+
639
+
640
+ def start_normal_bridge(
641
+ context,
642
+ ip="127.0.0.1",
643
+ port=2049,
644
+ token="LTtx",
645
+ request_channel="cfquant.request",
646
+ callback_event_channel="cfquant.callback.event",
647
+ bridge_id="default",
648
+ account_id="",
649
+ show=True,
650
+ schedule_timer=True,
651
+ pump_max_count=20,
652
+ pump_max_ms=0,
653
+ dispatch_on_qmt_thread=False,
654
+ ):
655
+ import sys
656
+
657
+ try:
658
+ globals_dict = sys._getframe(1).f_globals
659
+ except Exception:
660
+ globals_dict = {}
661
+ return NormalQmtBridge(
662
+ context,
663
+ ip=ip,
664
+ port=port,
665
+ token=token,
666
+ request_channel=request_channel,
667
+ callback_event_channel=callback_event_channel,
668
+ bridge_id=bridge_id,
669
+ account_id=account_id,
670
+ show=show,
671
+ globals_dict=globals_dict,
672
+ schedule_timer=schedule_timer,
673
+ pump_max_count=pump_max_count,
674
+ pump_max_ms=pump_max_ms,
675
+ dispatch_on_qmt_thread=dispatch_on_qmt_thread,
676
+ ).start()