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.
- LTtx/__init__.py +2 -0
- LTtx/tx/Config.txt +8 -0
- LTtx/tx/LTtx_server.py +1177 -0
- LTtx/tx/__init__.py +5 -0
- LTtx/tx/tx.py +1935 -0
- cfquant/__init__.py +26 -0
- cfquant/account_routing.py +119 -0
- cfquant/channels.py +76 -0
- cfquant/cli.py +266 -0
- cfquant/client.py +365 -0
- cfquant/config.py +86 -0
- cfquant/hub.py +304 -0
- cfquant/logging_i18n.py +348 -0
- cfquant/normal_bridge.py +676 -0
- cfquant/pipe_bridge.py +249 -0
- cfquant/pipe_client.py +223 -0
- cfquant/pipe_hub.py +602 -0
- cfquant/pipe_transport.py +493 -0
- cfquant/protocol.py +238 -0
- cfquant/qmt_bridge.py +1081 -0
- cfquant/runtime_report.py +334 -0
- cfquant/tx.py +1898 -0
- cfquant/tx_trade_bridge.py +1905 -0
- cfquant/version.py +5 -0
- cfquant/xtconstant.py +262 -0
- cfquant/xtdata.py +462 -0
- cfquant/xttrader.py +955 -0
- cfquant/xttype.py +586 -0
- cfquant-0.2.0.dist-info/LICENSE +21 -0
- cfquant-0.2.0.dist-info/METADATA +144 -0
- cfquant-0.2.0.dist-info/RECORD +90 -0
- cfquant-0.2.0.dist-info/WHEEL +5 -0
- cfquant-0.2.0.dist-info/entry_points.txt +4 -0
- cfquant-0.2.0.dist-info/top_level.txt +6 -0
- cfquant_pipe_hub.py +45 -0
- cfquant_web_server.py +12137 -0
- qmt_scripts/CFQUANT.py +507 -0
- qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
- qmt_scripts/CFQUANT_LITE.py +4928 -0
- qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
- qmt_scripts/__init__.py +1 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- web_dashboard/__init__.py +1 -0
- web_dashboard/app.js +9847 -0
- web_dashboard/avatars/amber-pulse.svg +15 -0
- web_dashboard/avatars/copper-grid.svg +15 -0
- web_dashboard/avatars/market-blue.svg +15 -0
- web_dashboard/avatars/rose-circuit.svg +15 -0
- web_dashboard/avatars/signal-green.svg +15 -0
- web_dashboard/avatars/slate-wave.svg +15 -0
- web_dashboard/avatars/teal-orbit.svg +16 -0
- web_dashboard/avatars/violet-node.svg +15 -0
- web_dashboard/favicon.svg +5 -0
- web_dashboard/image/qmt_deploy/image-10.png +0 -0
- web_dashboard/image/qmt_deploy/image-11.png +0 -0
- web_dashboard/image/qmt_deploy/image-12.png +0 -0
- web_dashboard/image/qmt_deploy/image-13.png +0 -0
- web_dashboard/image/qmt_deploy/image-14.png +0 -0
- web_dashboard/image/qmt_deploy/image-15.png +0 -0
- web_dashboard/image/qmt_deploy/image-16.png +0 -0
- web_dashboard/image/qmt_deploy/image-17.png +0 -0
- web_dashboard/image/qmt_deploy/image-18.png +0 -0
- web_dashboard/image/qmt_deploy/image-19.png +0 -0
- web_dashboard/image/qmt_deploy/image-20.png +0 -0
- web_dashboard/image/qmt_deploy/image-21.png +0 -0
- web_dashboard/image/qmt_deploy/image-4.png +0 -0
- web_dashboard/image/qmt_deploy/image-5.png +0 -0
- web_dashboard/image/qmt_deploy/image-6.png +0 -0
- web_dashboard/image/qmt_deploy/image-7.png +0 -0
- web_dashboard/image/qmt_deploy/image-8.png +0 -0
- web_dashboard/image/qmt_deploy/image-9.png +0 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- web_dashboard/index.html +2317 -0
- web_dashboard/styles.css +7817 -0
- web_dashboard/vendor/mermaid.min.js +2029 -0
cfquant/normal_bridge.py
ADDED
|
@@ -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()
|