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/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()
|