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
|
@@ -0,0 +1,1905 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from .protocol import loads_message, pack_event, pack_response
|
|
9
|
+
from .version import __version__ as CORE_VERSION
|
|
10
|
+
from . import account_routing
|
|
11
|
+
from .logging_i18n import get_log_enabled, get_log_language, set_log_enabled, set_log_language, translate_log
|
|
12
|
+
from .runtime_report import build_qmt_runtime_report, write_qmt_runtime_marker
|
|
13
|
+
from .xttype import filter_cancelable_orders
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
XTTRADER_COMPAT_CANDIDATES = {
|
|
17
|
+
"query_account_info": ("query_account_info", "get_account_info"),
|
|
18
|
+
"query_account_infos": ("query_account_infos", "get_account_infos", "query_account_info", "get_account_info"),
|
|
19
|
+
"query_account_status": ("query_account_status", "get_account_status"),
|
|
20
|
+
"query_position_statistics": ("query_position_statistics", "get_position_statistics"),
|
|
21
|
+
"query_secu_account": ("query_secu_account", "get_secu_account"),
|
|
22
|
+
"query_credit_detail": ("query_credit_detail", "get_credit_detail"),
|
|
23
|
+
"query_credit_subjects": ("query_credit_subjects", "get_credit_subjects"),
|
|
24
|
+
"query_credit_slo_code": ("query_credit_slo_code", "get_credit_slo_code"),
|
|
25
|
+
"query_credit_assure": ("query_credit_assure", "get_credit_assure"),
|
|
26
|
+
"query_stk_compacts": ("query_stk_compacts", "get_stk_compacts"),
|
|
27
|
+
"query_ipo_data": ("query_ipo_data", "get_ipo_data"),
|
|
28
|
+
"query_new_purchase_limit": ("query_new_purchase_limit", "get_new_purchase_limit"),
|
|
29
|
+
"query_bank_info": ("query_bank_info", "get_bank_info"),
|
|
30
|
+
"query_bank_amount": ("query_bank_amount", "get_bank_amount"),
|
|
31
|
+
"query_bank_transfer_stream": ("query_bank_transfer_stream", "get_bank_transfer_stream"),
|
|
32
|
+
"bank_transfer_in": ("bank_transfer_in", "transfer_bank_to_security"),
|
|
33
|
+
"bank_transfer_out": ("bank_transfer_out", "transfer_security_to_bank"),
|
|
34
|
+
"fund_transfer": ("fund_transfer",),
|
|
35
|
+
"secu_transfer": ("secu_transfer",),
|
|
36
|
+
"ctp_transfer_future_to_option": ("ctp_transfer_future_to_option",),
|
|
37
|
+
"ctp_transfer_option_to_future": ("ctp_transfer_option_to_future",),
|
|
38
|
+
"query_data": ("query_data",),
|
|
39
|
+
"export_data": ("export_data",),
|
|
40
|
+
"sync_transaction_from_external": ("sync_transaction_from_external",),
|
|
41
|
+
"smt_query_compact": ("smt_query_compact",),
|
|
42
|
+
"smt_query_order": ("smt_query_order",),
|
|
43
|
+
"smt_query_quoter": ("smt_query_quoter",),
|
|
44
|
+
"smt_appointment_order": ("smt_appointment_order",),
|
|
45
|
+
"smt_appointment_cancel": ("smt_appointment_cancel",),
|
|
46
|
+
"smt_negotiate_order": ("smt_negotiate_order",),
|
|
47
|
+
"smt_compact_return": ("smt_compact_return",),
|
|
48
|
+
"smt_compact_renewal": ("smt_compact_renewal",),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
XTDATA_COMPAT_CANDIDATES = {
|
|
53
|
+
"get_trading_calendar": ("get_trading_calendar",),
|
|
54
|
+
"get_trading_period": ("get_trading_period",),
|
|
55
|
+
"get_kline_trading_period": ("get_kline_trading_period",),
|
|
56
|
+
"get_all_trading_periods": ("get_all_trading_periods",),
|
|
57
|
+
"get_period_list": ("get_period_list",),
|
|
58
|
+
"create_sector": ("create_sector",),
|
|
59
|
+
"add_sector": ("add_sector",),
|
|
60
|
+
"remove_sector": ("remove_sector",),
|
|
61
|
+
"reset_sector": ("reset_sector",),
|
|
62
|
+
"remove_stock_from_sector": ("remove_stock_from_sector",),
|
|
63
|
+
"create_formula": ("create_formula",),
|
|
64
|
+
"call_formula": ("call_formula",),
|
|
65
|
+
"subscribe_formula": ("subscribe_formula",),
|
|
66
|
+
"unsubscribe_formula": ("unsubscribe_formula",),
|
|
67
|
+
"get_formula_result": ("get_formula_result",),
|
|
68
|
+
"get_l2_quote": ("get_l2_quote",),
|
|
69
|
+
"get_l2_order": ("get_l2_order",),
|
|
70
|
+
"get_l2_transaction": ("get_l2_transaction",),
|
|
71
|
+
"subscribe_l2thousand": ("subscribe_l2thousand",),
|
|
72
|
+
"get_l2thousand_queue": ("get_l2thousand_queue",),
|
|
73
|
+
"get_tabular_data": ("get_tabular_data",),
|
|
74
|
+
"download_tabular_data": ("download_tabular_data", "down_tabular_data"),
|
|
75
|
+
"push_custom_data": ("push_custom_data",),
|
|
76
|
+
"download_sector_data": ("download_sector_data", "down_sector_data"),
|
|
77
|
+
"download_index_weight": ("download_index_weight", "down_index_weight"),
|
|
78
|
+
"download_history_contracts": ("download_history_contracts", "down_history_contracts"),
|
|
79
|
+
"download_holiday_data": ("download_holiday_data", "down_holiday_data"),
|
|
80
|
+
"download_etf_info": ("download_etf_info", "down_etf_info"),
|
|
81
|
+
"download_cb_data": ("download_cb_data", "down_cb_data"),
|
|
82
|
+
"download_his_st_data": ("download_his_st_data", "down_his_st_data"),
|
|
83
|
+
"download_metatable_data": ("download_metatable_data", "down_metatable_data"),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
XTDATA_MAINCHAIN_UNSUPPORTED = {
|
|
87
|
+
"connect",
|
|
88
|
+
"disconnect",
|
|
89
|
+
"reconnect",
|
|
90
|
+
"get_quote_server_status",
|
|
91
|
+
"watch_quote_server_status",
|
|
92
|
+
"get_quote_server_config",
|
|
93
|
+
"get_data_dir",
|
|
94
|
+
"set_data_dir",
|
|
95
|
+
"read_feather",
|
|
96
|
+
"write_feather",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TxTradeBridge(object):
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
context,
|
|
104
|
+
ip="127.0.0.1",
|
|
105
|
+
port=2049,
|
|
106
|
+
token="LTtx",
|
|
107
|
+
request_channel="cfquant.request",
|
|
108
|
+
bridge_id="default",
|
|
109
|
+
account_id="",
|
|
110
|
+
show=True,
|
|
111
|
+
globals_dict=None,
|
|
112
|
+
):
|
|
113
|
+
self.context = context
|
|
114
|
+
self.ip = ip
|
|
115
|
+
self.port = int(port)
|
|
116
|
+
self.token = token
|
|
117
|
+
self.request_channel = request_channel
|
|
118
|
+
self.bridge_id = bridge_id or "default"
|
|
119
|
+
self.account_id = account_id
|
|
120
|
+
self.show = show
|
|
121
|
+
self.globals_dict = globals_dict or {}
|
|
122
|
+
self.running = False
|
|
123
|
+
self.tx = None
|
|
124
|
+
self.log_file = self._default_log_file()
|
|
125
|
+
self.account_subscribers = {}
|
|
126
|
+
self.client_accounts = {}
|
|
127
|
+
self.subscriber_lock = threading.RLock()
|
|
128
|
+
self.started_at = 0.0
|
|
129
|
+
self.account_type = ""
|
|
130
|
+
self.auto_trade_callback_enabled = False
|
|
131
|
+
|
|
132
|
+
def set_context(self, context):
|
|
133
|
+
self.context = context
|
|
134
|
+
if self.account_id:
|
|
135
|
+
self._set_context_account(self.account_id, self.account_type)
|
|
136
|
+
self._enable_auto_trade_callback()
|
|
137
|
+
self._log("tx trade bridge context ready")
|
|
138
|
+
self._publish_runtime_report("context_ready")
|
|
139
|
+
|
|
140
|
+
def start(self):
|
|
141
|
+
if self.running:
|
|
142
|
+
return self
|
|
143
|
+
self.running = True
|
|
144
|
+
if not self.started_at:
|
|
145
|
+
self.started_at = time.time()
|
|
146
|
+
txl = self._load_txl()
|
|
147
|
+
self.tx = txl(self.ip, self.port, self.token)
|
|
148
|
+
self.tx.start_tx()
|
|
149
|
+
self.tx.start_txg(self.request_channel)
|
|
150
|
+
self._log(
|
|
151
|
+
"tx trade bridge started LTtx=%s:%s request_channel=%s"
|
|
152
|
+
% (self.ip, self.port, self.request_channel)
|
|
153
|
+
)
|
|
154
|
+
self._publish_runtime_report("start")
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def close(self):
|
|
158
|
+
self.running = False
|
|
159
|
+
tx = self.tx
|
|
160
|
+
self.tx = None
|
|
161
|
+
if tx is not None:
|
|
162
|
+
try:
|
|
163
|
+
tx.close()
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
self._log("tx trade bridge stopped")
|
|
167
|
+
|
|
168
|
+
def run_forever(self, sleep_seconds=0.05):
|
|
169
|
+
self.start()
|
|
170
|
+
while self.running:
|
|
171
|
+
self.poll(max_messages=100, timeout=sleep_seconds)
|
|
172
|
+
|
|
173
|
+
def poll(self, max_messages=100, timeout=0):
|
|
174
|
+
self.start()
|
|
175
|
+
count = 0
|
|
176
|
+
while self.running and count < max_messages:
|
|
177
|
+
try:
|
|
178
|
+
raw = self.tx.Q.get(timeout=timeout if count == 0 else 0)
|
|
179
|
+
except Exception:
|
|
180
|
+
break
|
|
181
|
+
if raw is None:
|
|
182
|
+
break
|
|
183
|
+
self._handle_raw(raw)
|
|
184
|
+
count += 1
|
|
185
|
+
return count
|
|
186
|
+
|
|
187
|
+
def _handle_raw(self, raw):
|
|
188
|
+
received_at = time.time()
|
|
189
|
+
msg = loads_message(raw)
|
|
190
|
+
if not msg or msg.get("type") != "request":
|
|
191
|
+
return
|
|
192
|
+
request_id = msg.get("id")
|
|
193
|
+
action = msg.get("action")
|
|
194
|
+
client_id = msg.get("client_id") or msg.get("reply_channel")
|
|
195
|
+
try:
|
|
196
|
+
result = self._dispatch(action, msg.get("params") or {}, msg)
|
|
197
|
+
response = pack_response(request_id, ok=True, result=result)
|
|
198
|
+
self._log("tx trade response_ready action=%s id=%s" % (action, request_id))
|
|
199
|
+
except Exception as e:
|
|
200
|
+
response = pack_response(request_id, ok=False, error=e)
|
|
201
|
+
self._log("tx trade request_error action=%s id=%s error=%s" % (action, request_id, e))
|
|
202
|
+
if client_id:
|
|
203
|
+
self.tx.push("response", response, client_id)
|
|
204
|
+
self._log(
|
|
205
|
+
"tx trade response_sent action=%s id=%s client_id=%s total_ms=%.2f"
|
|
206
|
+
% (action, request_id, client_id, (time.time() - received_at) * 1000)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def _dispatch(self, action, params, msg):
|
|
210
|
+
if action == "cfquant.ping":
|
|
211
|
+
return {
|
|
212
|
+
"pong": True,
|
|
213
|
+
"ts": time.time(),
|
|
214
|
+
"request_channel": self.request_channel,
|
|
215
|
+
"bridge_id": self.bridge_id,
|
|
216
|
+
}
|
|
217
|
+
if action == "cfquant.status":
|
|
218
|
+
return self._status()
|
|
219
|
+
if action == "cfquant.set_log_language":
|
|
220
|
+
return self._set_log_language(params)
|
|
221
|
+
if action == "cfquant.get_log_language":
|
|
222
|
+
return {"language": get_log_language()}
|
|
223
|
+
if action == "cfquant.set_log_enabled":
|
|
224
|
+
return self._set_log_enabled(params)
|
|
225
|
+
if action == "cfquant.get_log_enabled":
|
|
226
|
+
return {"enabled": get_log_enabled()}
|
|
227
|
+
if action == "cfquant.cleanup_qmt_logs":
|
|
228
|
+
return self._cleanup_qmt_userdata_logs(params)
|
|
229
|
+
if action == "cfquant.query_info":
|
|
230
|
+
return self._query_info(params)
|
|
231
|
+
if action == "xttrader.subscribe":
|
|
232
|
+
return self._subscribe_account(params, msg)
|
|
233
|
+
if action == "xttrader.unsubscribe":
|
|
234
|
+
return self._unsubscribe_account(params, msg)
|
|
235
|
+
if action == "xttrader.query_stock_positions":
|
|
236
|
+
return self._query_trade_detail(params, "position")
|
|
237
|
+
if action == "xttrader.query_stock_orders":
|
|
238
|
+
return self._query_trade_detail(params, "order")
|
|
239
|
+
if action == "xttrader.query_stock_trades":
|
|
240
|
+
return self._query_trade_detail(params, "deal")
|
|
241
|
+
if action == "xttrader.query_stock_asset":
|
|
242
|
+
return self._query_trade_detail(params, "account")
|
|
243
|
+
if action == "xttrader.order_stock":
|
|
244
|
+
return self._order_stock(params, msg)
|
|
245
|
+
if action == "xttrader.order_stock_batch":
|
|
246
|
+
return self._order_stock_batch(params, msg)
|
|
247
|
+
if action == "xttrader.order_stock_async":
|
|
248
|
+
return self._order_stock_async(params, msg)
|
|
249
|
+
if action == "xttrader.cancel_order_stock":
|
|
250
|
+
return self._cancel_order_stock(params)
|
|
251
|
+
if action == "xttrader.cancel_order_stock_async":
|
|
252
|
+
return self._cancel_order_stock_async(params, msg)
|
|
253
|
+
if action == "xttrader.cancel_order_stock_sysid":
|
|
254
|
+
return self._cancel_order_stock_sysid(params)
|
|
255
|
+
if action == "xttrader.cancel_order_stock_sysid_async":
|
|
256
|
+
return self._cancel_order_stock_sysid_async(params, msg)
|
|
257
|
+
if action == "xtdata.get_market_data":
|
|
258
|
+
return self._get_market_data(params)
|
|
259
|
+
if action == "xtdata.get_market_data_ex":
|
|
260
|
+
return self._get_market_data_ex(params)
|
|
261
|
+
if action == "xtdata.get_full_tick":
|
|
262
|
+
return self.context.get_full_tick(params.get("code_list", []))
|
|
263
|
+
if action == "xtdata.get_local_data":
|
|
264
|
+
return self._get_local_data(params)
|
|
265
|
+
if action == "xtdata.download_history_data":
|
|
266
|
+
return self._download_history_data(params, msg)
|
|
267
|
+
if action == "xtdata.download_history_data2":
|
|
268
|
+
return self._download_history_data2(params, msg)
|
|
269
|
+
if action == "xtdata.get_financial_data":
|
|
270
|
+
return self._get_financial_data(params)
|
|
271
|
+
if action == "xtdata.get_raw_financial_data":
|
|
272
|
+
return self._get_raw_financial_data(params)
|
|
273
|
+
if action == "xtdata.download_financial_data":
|
|
274
|
+
return self._download_financial_data(params, msg)
|
|
275
|
+
if action == "xtdata.download_financial_data2":
|
|
276
|
+
return self._download_financial_data(params, msg)
|
|
277
|
+
if action == "xtdata.get_instrument_detail":
|
|
278
|
+
return self._get_instrument_detail(params)
|
|
279
|
+
if action == "xtdata.get_stock_list_in_sector":
|
|
280
|
+
return self.context.get_stock_list_in_sector(params.get("sector_name", ""))
|
|
281
|
+
if action.startswith("xtdata."):
|
|
282
|
+
return self._dispatch_xtdata_compat(action, params, msg)
|
|
283
|
+
if action.startswith("xttrader."):
|
|
284
|
+
return self._dispatch_xttrader_compat(action, params, msg)
|
|
285
|
+
raise ValueError("unsupported action: %s" % action)
|
|
286
|
+
|
|
287
|
+
def _status(self):
|
|
288
|
+
runtime = self._runtime_info()
|
|
289
|
+
status = {
|
|
290
|
+
"bridge": type(self).__name__,
|
|
291
|
+
"bridge_id": self.bridge_id,
|
|
292
|
+
"running": self.running,
|
|
293
|
+
"request_channel": self.request_channel,
|
|
294
|
+
"account_id": self.account_id,
|
|
295
|
+
"version": CORE_VERSION,
|
|
296
|
+
"core_version": CORE_VERSION,
|
|
297
|
+
"runtime_core_version": CORE_VERSION,
|
|
298
|
+
"qmt_runtime_core_version": CORE_VERSION,
|
|
299
|
+
"runtime": runtime,
|
|
300
|
+
"account_subscribers": self._account_subscriber_status(),
|
|
301
|
+
"log_language": get_log_language(),
|
|
302
|
+
"log_enabled": get_log_enabled(),
|
|
303
|
+
"context_ready": self.context is not None,
|
|
304
|
+
"tx_ready": self.tx is not None,
|
|
305
|
+
"ts": time.time(),
|
|
306
|
+
}
|
|
307
|
+
try:
|
|
308
|
+
extra = self._status_extra()
|
|
309
|
+
if extra:
|
|
310
|
+
status.update(extra)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
status["status_extra_error"] = str(e)
|
|
313
|
+
return status
|
|
314
|
+
|
|
315
|
+
def _runtime_info(self):
|
|
316
|
+
globals_dict = self.globals_dict or {}
|
|
317
|
+
config = globals_dict.get("RUNTIME_CONFIG") if isinstance(globals_dict.get("RUNTIME_CONFIG"), dict) else {}
|
|
318
|
+
channels = globals_dict.get("BRIDGE_CHANNELS") if isinstance(globals_dict.get("BRIDGE_CHANNELS"), dict) else {}
|
|
319
|
+
if not channels and isinstance(config.get("channels"), dict):
|
|
320
|
+
channels = config.get("channels")
|
|
321
|
+
channel_key = "normal" if "normal" in str(self.request_channel or "").lower() else "trade"
|
|
322
|
+
transport = "pipe" if getattr(self, "pipe_name", "") else "lttx"
|
|
323
|
+
entry_file = ""
|
|
324
|
+
try:
|
|
325
|
+
entry_file = str(globals_dict.get("__file__") or "")
|
|
326
|
+
except Exception:
|
|
327
|
+
entry_file = ""
|
|
328
|
+
return build_qmt_runtime_report(
|
|
329
|
+
reason="status",
|
|
330
|
+
version=CORE_VERSION,
|
|
331
|
+
core_version=CORE_VERSION,
|
|
332
|
+
bridge=type(self).__name__,
|
|
333
|
+
bridge_id=self.bridge_id,
|
|
334
|
+
account_id=self.account_id,
|
|
335
|
+
account_type=self.account_type or config.get("account_type") or globals_dict.get("DEFAULT_ACCOUNT_TYPE"),
|
|
336
|
+
account_key=config.get("account_key"),
|
|
337
|
+
mode=config.get("mode") or transport,
|
|
338
|
+
transport=transport,
|
|
339
|
+
runtime_mode=type(self).__name__,
|
|
340
|
+
channel_key=channel_key,
|
|
341
|
+
request_channel=self.request_channel,
|
|
342
|
+
channels=channels,
|
|
343
|
+
pipe_name=getattr(self, "pipe_name", ""),
|
|
344
|
+
market=config.get("market") or globals_dict.get("QMT_MARKET"),
|
|
345
|
+
market_role=config.get("market_role"),
|
|
346
|
+
market_route_parent_bridge_id=config.get("market_route_parent_bridge_id"),
|
|
347
|
+
config=config,
|
|
348
|
+
globals_dict=globals_dict,
|
|
349
|
+
entry_file=entry_file,
|
|
350
|
+
module_file=__file__,
|
|
351
|
+
started_at=self.started_at,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def _publish_runtime_report(self, reason):
|
|
355
|
+
try:
|
|
356
|
+
channel_key = "normal" if "normal" in str(self.request_channel or "").lower() else "trade"
|
|
357
|
+
if not self.started_at:
|
|
358
|
+
self.started_at = time.time()
|
|
359
|
+
data = self._runtime_info()
|
|
360
|
+
data.update({
|
|
361
|
+
"reason": reason,
|
|
362
|
+
"transport": "pipe" if getattr(self, "pipe_name", "") else "lttx",
|
|
363
|
+
"channel_key": channel_key,
|
|
364
|
+
})
|
|
365
|
+
try:
|
|
366
|
+
config = self.globals_dict.get("RUNTIME_CONFIG") if isinstance(self.globals_dict.get("RUNTIME_CONFIG"), dict) else {}
|
|
367
|
+
entry_file = str((self.globals_dict or {}).get("__file__") or "")
|
|
368
|
+
entry_base_dir = os.path.dirname(os.path.abspath(entry_file)) if entry_file and not entry_file.startswith("<") else ""
|
|
369
|
+
marker = write_qmt_runtime_marker(data, config=config, entry_base_dir=entry_base_dir)
|
|
370
|
+
if marker.get("ok"):
|
|
371
|
+
self._log(
|
|
372
|
+
"qmt runtime marker written version=%s reason=%s file=%s"
|
|
373
|
+
% (data.get("core_version") or "-", reason, marker.get("primary_file") or "")
|
|
374
|
+
)
|
|
375
|
+
elif marker.get("errors"):
|
|
376
|
+
self._log("qmt runtime marker write failed reason=%s error=%s" % (reason, "; ".join(marker.get("errors") or [])))
|
|
377
|
+
except Exception as e:
|
|
378
|
+
self._log("qmt runtime marker write failed reason=%s error=%s" % (reason, e))
|
|
379
|
+
|
|
380
|
+
tx = self.tx
|
|
381
|
+
if tx is None or not hasattr(tx, "put"):
|
|
382
|
+
return
|
|
383
|
+
payload = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
|
384
|
+
key = "cfquant.qmt.runtime.%s" % self.bridge_id
|
|
385
|
+
tx.put(key, payload)
|
|
386
|
+
tx.put("%s.%s" % (key, channel_key), payload)
|
|
387
|
+
tx.put("%s.version" % key, CORE_VERSION)
|
|
388
|
+
self._log("tx trade runtime report published version=%s reason=%s" % (CORE_VERSION, reason))
|
|
389
|
+
except Exception as e:
|
|
390
|
+
self._log("tx trade runtime report failed:%s" % e)
|
|
391
|
+
|
|
392
|
+
def _status_extra(self):
|
|
393
|
+
return {}
|
|
394
|
+
|
|
395
|
+
def _set_log_language(self, params):
|
|
396
|
+
params = params or {}
|
|
397
|
+
lang = set_log_language(params.get("language") or params.get("lang"))
|
|
398
|
+
self._log("QMT日志语言已切换为:%s" % ("中文" if lang == "zh" else "English"))
|
|
399
|
+
return {"language": lang}
|
|
400
|
+
|
|
401
|
+
def _set_log_enabled(self, params):
|
|
402
|
+
params = params or {}
|
|
403
|
+
if "enabled" in params:
|
|
404
|
+
value = params.get("enabled")
|
|
405
|
+
else:
|
|
406
|
+
value = params.get("show")
|
|
407
|
+
enabled = set_log_enabled(value)
|
|
408
|
+
self.show = True
|
|
409
|
+
self._log("QMT log output enabled=%s" % ("1" if enabled else "0"), force=True)
|
|
410
|
+
return {"enabled": enabled}
|
|
411
|
+
|
|
412
|
+
def _cleanup_qmt_userdata_logs(self, params):
|
|
413
|
+
params = params or {}
|
|
414
|
+
retention_days = self._retention_days(params.get("retention_days"), default=5)
|
|
415
|
+
dry_run = str(params.get("dry_run") or "").strip().lower() in ("1", "true", "yes", "on")
|
|
416
|
+
log_dir, candidate_dirs, python_dir, entry_file = self._qmt_userdata_log_dir()
|
|
417
|
+
result = {
|
|
418
|
+
"bridge_id": self.bridge_id,
|
|
419
|
+
"request_channel": self.request_channel,
|
|
420
|
+
"retention_days": retention_days,
|
|
421
|
+
"dry_run": dry_run,
|
|
422
|
+
"entry_file": entry_file,
|
|
423
|
+
"python_dir": python_dir,
|
|
424
|
+
"log_dir": log_dir,
|
|
425
|
+
"candidate_dirs": candidate_dirs,
|
|
426
|
+
"exists": bool(log_dir and os.path.isdir(log_dir)),
|
|
427
|
+
"scanned_files": 0,
|
|
428
|
+
"kept_files": 0,
|
|
429
|
+
"deleted_files": 0,
|
|
430
|
+
"would_delete_files": 0,
|
|
431
|
+
"failed_files": 0,
|
|
432
|
+
"deleted_bytes": 0,
|
|
433
|
+
"errors": [],
|
|
434
|
+
"ts": time.time(),
|
|
435
|
+
}
|
|
436
|
+
if not result["exists"]:
|
|
437
|
+
return result
|
|
438
|
+
|
|
439
|
+
cutoff = time.time() - retention_days * 86400
|
|
440
|
+
for current_root, dirs, files in os.walk(log_dir):
|
|
441
|
+
for name in files:
|
|
442
|
+
path = os.path.join(current_root, name)
|
|
443
|
+
result["scanned_files"] += 1
|
|
444
|
+
try:
|
|
445
|
+
stat_result = os.stat(path)
|
|
446
|
+
if stat_result.st_mtime >= cutoff:
|
|
447
|
+
result["kept_files"] += 1
|
|
448
|
+
continue
|
|
449
|
+
if dry_run:
|
|
450
|
+
result["would_delete_files"] += 1
|
|
451
|
+
result["deleted_bytes"] += stat_result.st_size
|
|
452
|
+
else:
|
|
453
|
+
os.remove(path)
|
|
454
|
+
result["deleted_files"] += 1
|
|
455
|
+
result["deleted_bytes"] += stat_result.st_size
|
|
456
|
+
except Exception as e:
|
|
457
|
+
result["failed_files"] += 1
|
|
458
|
+
result["errors"].append("%s: %s" % (path, e))
|
|
459
|
+
self._log(
|
|
460
|
+
"qmt userdata log cleanup log_dir=%s retention_days=%s deleted=%s failed=%s dry_run=%s"
|
|
461
|
+
% (log_dir, retention_days, result["deleted_files"], result["failed_files"], dry_run)
|
|
462
|
+
)
|
|
463
|
+
return result
|
|
464
|
+
|
|
465
|
+
def _qmt_userdata_log_dir(self):
|
|
466
|
+
entry_file = self.globals_dict.get("__file__") or ""
|
|
467
|
+
if entry_file:
|
|
468
|
+
entry_file = os.path.abspath(entry_file)
|
|
469
|
+
python_dir = os.path.dirname(entry_file)
|
|
470
|
+
else:
|
|
471
|
+
python_dir = os.path.abspath(os.getcwd())
|
|
472
|
+
candidate_dirs = []
|
|
473
|
+
if os.path.basename(python_dir).lower() == "python":
|
|
474
|
+
candidate_dirs.append(os.path.join(os.path.dirname(python_dir), "userdata", "log"))
|
|
475
|
+
candidate_dirs.append(os.path.join(python_dir, "userdata", "log"))
|
|
476
|
+
|
|
477
|
+
normalized = []
|
|
478
|
+
seen = set()
|
|
479
|
+
for path in candidate_dirs:
|
|
480
|
+
path = os.path.abspath(path)
|
|
481
|
+
key = path.lower()
|
|
482
|
+
if key in seen:
|
|
483
|
+
continue
|
|
484
|
+
seen.add(key)
|
|
485
|
+
normalized.append(path)
|
|
486
|
+
for path in normalized:
|
|
487
|
+
if os.path.isdir(path):
|
|
488
|
+
return path, normalized, python_dir, entry_file
|
|
489
|
+
return normalized[0] if normalized else "", normalized, python_dir, entry_file
|
|
490
|
+
|
|
491
|
+
def _retention_days(self, value, default=5):
|
|
492
|
+
try:
|
|
493
|
+
days = int(value)
|
|
494
|
+
except Exception:
|
|
495
|
+
days = int(default)
|
|
496
|
+
if days < 1:
|
|
497
|
+
days = 1
|
|
498
|
+
if days > 3650:
|
|
499
|
+
days = 3650
|
|
500
|
+
return days
|
|
501
|
+
|
|
502
|
+
def _query_info(self, params):
|
|
503
|
+
return {
|
|
504
|
+
"orders": self._query_trade_detail(params, "order"),
|
|
505
|
+
"deals": self._query_trade_detail(params, "deal"),
|
|
506
|
+
"positions": self._query_trade_detail(params, "position"),
|
|
507
|
+
"accounts": self._query_trade_detail(params, "account"),
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
def _query_trade_detail(self, params, detail_type):
|
|
511
|
+
func = self._get_callable("get_trade_detail_data")
|
|
512
|
+
if not func:
|
|
513
|
+
raise NotImplementedError("get_trade_detail_data not found")
|
|
514
|
+
account = params.get("account") or {}
|
|
515
|
+
account_id = account.get("account_id") or params.get("account_id") or self.account_id
|
|
516
|
+
if not account_id:
|
|
517
|
+
raise ValueError("account_id is required")
|
|
518
|
+
account_type = self._account_type_name(account.get("account_type") or params.get("account_type"))
|
|
519
|
+
self._log(
|
|
520
|
+
"query_trade_detail start account=%s account_type=%s detail_type=%s"
|
|
521
|
+
% (account_id, account_type.lower(), detail_type.lower())
|
|
522
|
+
)
|
|
523
|
+
try:
|
|
524
|
+
rows = self._call_trade_detail_data(
|
|
525
|
+
func,
|
|
526
|
+
account_id,
|
|
527
|
+
account_type.lower(),
|
|
528
|
+
detail_type.lower(),
|
|
529
|
+
) or []
|
|
530
|
+
except Exception as e:
|
|
531
|
+
self._log(
|
|
532
|
+
"query_trade_detail call failed account=%s detail_type=%s error=%s"
|
|
533
|
+
% (account_id, detail_type, e)
|
|
534
|
+
)
|
|
535
|
+
raise
|
|
536
|
+
|
|
537
|
+
result = []
|
|
538
|
+
for index, row in enumerate(rows):
|
|
539
|
+
try:
|
|
540
|
+
result.append(self._format_trade_detail(row, detail_type))
|
|
541
|
+
except Exception as e:
|
|
542
|
+
self._log(
|
|
543
|
+
"query_trade_detail format failed detail_type=%s index=%s type=%s error=%s"
|
|
544
|
+
% (detail_type, index, type(row).__name__, e)
|
|
545
|
+
)
|
|
546
|
+
result.append({
|
|
547
|
+
"format_error": str(e),
|
|
548
|
+
"raw_type": type(row).__name__,
|
|
549
|
+
})
|
|
550
|
+
if detail_type.lower() == "order" and self._truthy_param(params.get("cancelable_only")):
|
|
551
|
+
result = filter_cancelable_orders(result)
|
|
552
|
+
self._log(
|
|
553
|
+
"query_trade_detail done detail_type=%s count=%s"
|
|
554
|
+
% (detail_type, len(result))
|
|
555
|
+
)
|
|
556
|
+
return result
|
|
557
|
+
|
|
558
|
+
def _call_trade_detail_data(self, func, account_id, account_type, detail_type):
|
|
559
|
+
# QMT's fourth argument is strategyname, not ContextInfo.
|
|
560
|
+
variants = [
|
|
561
|
+
((account_id, account_type, detail_type), {}),
|
|
562
|
+
((account_id, account_type, detail_type, ""), {}),
|
|
563
|
+
]
|
|
564
|
+
last_error = None
|
|
565
|
+
for args, kwargs in variants:
|
|
566
|
+
try:
|
|
567
|
+
return func(*args, **kwargs)
|
|
568
|
+
except TypeError as e:
|
|
569
|
+
last_error = e
|
|
570
|
+
continue
|
|
571
|
+
if last_error is not None:
|
|
572
|
+
raise last_error
|
|
573
|
+
raise RuntimeError("no available trade detail call variant")
|
|
574
|
+
|
|
575
|
+
def _order_stock(self, params, msg):
|
|
576
|
+
passorder = self._get_callable("passorder")
|
|
577
|
+
if not passorder:
|
|
578
|
+
raise NotImplementedError("passorder not found")
|
|
579
|
+
account = params.get("account") or {}
|
|
580
|
+
account_id = account.get("account_id") or params.get("account_id") or self.account_id
|
|
581
|
+
order_type = params.get("optype", params.get("order_type"))
|
|
582
|
+
if not account_id:
|
|
583
|
+
raise ValueError("account_id is required")
|
|
584
|
+
if isinstance(order_type, str):
|
|
585
|
+
order_type = 23 if order_type.lower() == "buy" else 24
|
|
586
|
+
price_type = params.get("price_type", 11)
|
|
587
|
+
order_remark = self._first_param(
|
|
588
|
+
params,
|
|
589
|
+
("order_remark", "remark", "strategy_name"),
|
|
590
|
+
msg.get("id", "tx_order"),
|
|
591
|
+
)
|
|
592
|
+
result = passorder(
|
|
593
|
+
order_type,
|
|
594
|
+
params.get("qmt_order_type", 1101),
|
|
595
|
+
account_id,
|
|
596
|
+
params.get("stock_code", params.get("code", "")),
|
|
597
|
+
price_type,
|
|
598
|
+
params.get("price", 0),
|
|
599
|
+
params.get("order_volume", params.get("num", 0)),
|
|
600
|
+
params.get("strategy_name", "1"),
|
|
601
|
+
params.get("quick_trade", 2),
|
|
602
|
+
order_remark,
|
|
603
|
+
self.context,
|
|
604
|
+
)
|
|
605
|
+
return {"request_result": result, "order_id": result, "order_remark": order_remark}
|
|
606
|
+
|
|
607
|
+
def _order_stock_async(self, params, msg):
|
|
608
|
+
result = self._order_stock(params, msg)
|
|
609
|
+
data = {
|
|
610
|
+
"seq": params.get("seq"),
|
|
611
|
+
"account_id": (params.get("account") or {}).get("account_id", params.get("account_id", "")),
|
|
612
|
+
"account_type": self._account_type_name((params.get("account") or {}).get("account_type") or params.get("account_type")).upper(),
|
|
613
|
+
"order_id": result.get("order_id", -1) if isinstance(result, dict) else result,
|
|
614
|
+
"order_remark": result.get("order_remark", params.get("order_remark", "")) if isinstance(result, dict) else params.get("order_remark", ""),
|
|
615
|
+
}
|
|
616
|
+
self._send_trader_event(msg.get("client_id"), "on_order_stock_async_response", data)
|
|
617
|
+
return result
|
|
618
|
+
|
|
619
|
+
def _order_stock_batch(self, params, msg):
|
|
620
|
+
orders = params.get("orders") or []
|
|
621
|
+
if not isinstance(orders, list) or not orders:
|
|
622
|
+
raise ValueError("orders must be a non-empty list")
|
|
623
|
+
common_account = params.get("account") or {}
|
|
624
|
+
stop_on_error = bool(params.get("stop_on_error"))
|
|
625
|
+
results = []
|
|
626
|
+
for index, order in enumerate(orders):
|
|
627
|
+
row = dict(params)
|
|
628
|
+
row.pop("orders", None)
|
|
629
|
+
row.update(order or {})
|
|
630
|
+
if common_account and not row.get("account"):
|
|
631
|
+
row["account"] = common_account
|
|
632
|
+
if self._first_param(row, ("order_remark", "remark", "strategy_name")) is None:
|
|
633
|
+
row["order_remark"] = "%s_%s" % (params.get("order_remark") or msg.get("id", "batch_order"), index + 1)
|
|
634
|
+
try:
|
|
635
|
+
result = self._order_stock(row, msg)
|
|
636
|
+
results.append({
|
|
637
|
+
"index": index,
|
|
638
|
+
"ok": True,
|
|
639
|
+
"stock_code": row.get("stock_code", row.get("code", "")),
|
|
640
|
+
"result": result,
|
|
641
|
+
})
|
|
642
|
+
except Exception as e:
|
|
643
|
+
results.append({
|
|
644
|
+
"index": index,
|
|
645
|
+
"ok": False,
|
|
646
|
+
"stock_code": row.get("stock_code", row.get("code", "")),
|
|
647
|
+
"error": str(e),
|
|
648
|
+
})
|
|
649
|
+
if stop_on_error:
|
|
650
|
+
break
|
|
651
|
+
return {
|
|
652
|
+
"total": len(orders),
|
|
653
|
+
"submitted": len([item for item in results if item.get("ok")]),
|
|
654
|
+
"failed": len([item for item in results if not item.get("ok")]),
|
|
655
|
+
"results": results,
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
def _cancel_order_stock(self, params):
|
|
659
|
+
cancel_func = self._get_callable("cancel")
|
|
660
|
+
if not cancel_func:
|
|
661
|
+
raise NotImplementedError("cancel not found")
|
|
662
|
+
account = params.get("account") or {}
|
|
663
|
+
account_id = account.get("account_id") or params.get("account_id") or self.account_id
|
|
664
|
+
order_id = str(params.get("order_id", ""))
|
|
665
|
+
if not account_id:
|
|
666
|
+
raise ValueError("account_id is required")
|
|
667
|
+
if not order_id:
|
|
668
|
+
raise ValueError("order_id is required")
|
|
669
|
+
account_type = self._account_type_name(account.get("account_type") or params.get("account_type"))
|
|
670
|
+
result = cancel_func(order_id, account_id, account_type, self.context)
|
|
671
|
+
return {"cancel_result": 0 if result else -1, "request_result": result, "order_id": order_id}
|
|
672
|
+
|
|
673
|
+
def _cancel_order_stock_async(self, params, msg):
|
|
674
|
+
result = self._cancel_order_stock(params)
|
|
675
|
+
data = {
|
|
676
|
+
"seq": params.get("seq"),
|
|
677
|
+
"account_id": (params.get("account") or {}).get("account_id", params.get("account_id", "")),
|
|
678
|
+
"account_type": self._account_type_name((params.get("account") or {}).get("account_type") or params.get("account_type")).upper(),
|
|
679
|
+
"order_id": params.get("order_id"),
|
|
680
|
+
"cancel_result": result.get("cancel_result", -1) if isinstance(result, dict) else result,
|
|
681
|
+
}
|
|
682
|
+
self._send_trader_event(msg.get("client_id"), "on_cancel_order_stock_async_response", data)
|
|
683
|
+
return result
|
|
684
|
+
|
|
685
|
+
def _cancel_order_stock_sysid(self, params):
|
|
686
|
+
row = dict(params)
|
|
687
|
+
row["order_id"] = params.get("sysid", params.get("order_id", ""))
|
|
688
|
+
result = self._cancel_order_stock(row)
|
|
689
|
+
result["market"] = params.get("market")
|
|
690
|
+
result["sysid"] = params.get("sysid")
|
|
691
|
+
return result
|
|
692
|
+
|
|
693
|
+
def _cancel_order_stock_sysid_async(self, params, msg):
|
|
694
|
+
result = self._cancel_order_stock_sysid(params)
|
|
695
|
+
data = {
|
|
696
|
+
"seq": params.get("seq"),
|
|
697
|
+
"account_id": (params.get("account") or {}).get("account_id", params.get("account_id", "")),
|
|
698
|
+
"order_id": params.get("sysid", params.get("order_id")),
|
|
699
|
+
"cancel_result": result.get("cancel_result", -1) if isinstance(result, dict) else result,
|
|
700
|
+
}
|
|
701
|
+
self._send_trader_event(msg.get("client_id"), "on_cancel_order_stock_async_response", data)
|
|
702
|
+
return result
|
|
703
|
+
|
|
704
|
+
def _dispatch_xttrader_compat(self, action, params, msg):
|
|
705
|
+
method = action.split(".", 1)[1]
|
|
706
|
+
if method == "query_com_fund":
|
|
707
|
+
rows = self._query_trade_detail(params, "account")
|
|
708
|
+
return rows[0] if rows else {}
|
|
709
|
+
if method == "query_com_position":
|
|
710
|
+
return self._query_trade_detail(params, "position")
|
|
711
|
+
if method == "query_stock_asset_async":
|
|
712
|
+
return self._query_trade_detail(params, "account")
|
|
713
|
+
if method == "query_stock_orders_async":
|
|
714
|
+
return self._query_trade_detail(params, "order")
|
|
715
|
+
if method == "query_stock_trades_async":
|
|
716
|
+
return self._query_trade_detail(params, "deal")
|
|
717
|
+
if method == "query_stock_positions_async":
|
|
718
|
+
return self._query_trade_detail(params, "position")
|
|
719
|
+
return self._generic_xttrader_call(method, params)
|
|
720
|
+
|
|
721
|
+
def _dispatch_xtdata_compat(self, action, params, msg):
|
|
722
|
+
method = action.split(".", 1)[1]
|
|
723
|
+
if method == "get_trading_dates":
|
|
724
|
+
return self._get_trading_dates(params)
|
|
725
|
+
if method in (
|
|
726
|
+
"is_stock",
|
|
727
|
+
"is_fund",
|
|
728
|
+
"is_future",
|
|
729
|
+
"get_stock_type",
|
|
730
|
+
"get_stock_name",
|
|
731
|
+
"get_open_date",
|
|
732
|
+
"get_contract_expire_date",
|
|
733
|
+
"get_contract_multiplier",
|
|
734
|
+
):
|
|
735
|
+
return self._call_stock_callable(method, params)
|
|
736
|
+
if method == "get_weight_in_index":
|
|
737
|
+
return self._get_weight_in_index(params)
|
|
738
|
+
if method == "get_turnover_rate":
|
|
739
|
+
return self._get_turnover_rate(params)
|
|
740
|
+
if method in ("get_ETF_list", "get_etf_list"):
|
|
741
|
+
return self._get_etf_list(params)
|
|
742
|
+
if method == "get_option_detail_data":
|
|
743
|
+
return self._get_option_detail_data(params)
|
|
744
|
+
if method == "get_option_list":
|
|
745
|
+
return self._get_option_list(params)
|
|
746
|
+
if method == "get_option_undl":
|
|
747
|
+
return self._get_option_undl(params)
|
|
748
|
+
if method == "get_option_undl_data":
|
|
749
|
+
return self._get_option_undl_data(params)
|
|
750
|
+
if method == "get_his_st_data":
|
|
751
|
+
return self._get_his_st_data(params)
|
|
752
|
+
if method == "get_his_index_data":
|
|
753
|
+
return self._get_his_index_data(params)
|
|
754
|
+
if method == "get_factor_data":
|
|
755
|
+
return self._get_factor_data(params)
|
|
756
|
+
if method in ("get_financial_data_ori", "get_financial_data_raw"):
|
|
757
|
+
return self._get_raw_financial_data(params)
|
|
758
|
+
if method in XTDATA_MAINCHAIN_UNSUPPORTED:
|
|
759
|
+
raise NotImplementedError(
|
|
760
|
+
"xtdata.%s belongs to MiniQMT client/local data-dir management and is not implemented in cfquant QMT bridge"
|
|
761
|
+
% method
|
|
762
|
+
)
|
|
763
|
+
if method in XTDATA_COMPAT_CANDIDATES:
|
|
764
|
+
return self._generic_xtdata_call(method, params, msg)
|
|
765
|
+
raise NotImplementedError("xtdata.%s is not implemented by cfquant QMT bridge" % method)
|
|
766
|
+
|
|
767
|
+
def _generic_xtdata_call(self, method, params, msg=None):
|
|
768
|
+
candidates = XTDATA_COMPAT_CANDIDATES.get(method, (method,))
|
|
769
|
+
func = self._get_callable(*candidates)
|
|
770
|
+
if not func:
|
|
771
|
+
raise NotImplementedError(
|
|
772
|
+
"xtdata.%s requires QMT callable: %s"
|
|
773
|
+
% (method, ", ".join(candidates))
|
|
774
|
+
)
|
|
775
|
+
args = list(params.get("args") or [])
|
|
776
|
+
kwargs = dict(params.get("kwargs") or {})
|
|
777
|
+
callback_event = params.get("callback_event")
|
|
778
|
+
callback_positions = []
|
|
779
|
+
for item in params.get("callback_positions") or []:
|
|
780
|
+
try:
|
|
781
|
+
callback_positions.append(int(item))
|
|
782
|
+
except Exception:
|
|
783
|
+
pass
|
|
784
|
+
client_id = msg.get("client_id") if msg else None
|
|
785
|
+
|
|
786
|
+
variants = []
|
|
787
|
+
if callback_event and client_id:
|
|
788
|
+
def callback(data):
|
|
789
|
+
self._send_event(
|
|
790
|
+
client_id,
|
|
791
|
+
callback_event,
|
|
792
|
+
data,
|
|
793
|
+
meta=self._generic_xtdata_event_meta(params, method, "callback"),
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
callback_args = list(args)
|
|
797
|
+
for index in callback_positions:
|
|
798
|
+
if 0 <= index < len(callback_args):
|
|
799
|
+
callback_args[index] = callback
|
|
800
|
+
if callback_positions:
|
|
801
|
+
variants.append((tuple(callback_args), dict(kwargs)))
|
|
802
|
+
callback_kwargs = dict(kwargs)
|
|
803
|
+
callback_kwargs.setdefault(params.get("callback_name") or "callback", callback)
|
|
804
|
+
variants.append((tuple(args), callback_kwargs))
|
|
805
|
+
variants.append((tuple(args) + (callback,), dict(kwargs)))
|
|
806
|
+
variants.extend([
|
|
807
|
+
(tuple(args), dict(kwargs)),
|
|
808
|
+
((params,), {}),
|
|
809
|
+
((), {}),
|
|
810
|
+
])
|
|
811
|
+
return self._call_variants(func, variants)
|
|
812
|
+
|
|
813
|
+
def _generic_xtdata_event_meta(self, params, method, stage):
|
|
814
|
+
meta = {
|
|
815
|
+
"xtdata_generic": True,
|
|
816
|
+
"method": method,
|
|
817
|
+
"stage": stage,
|
|
818
|
+
"bridge_id": self.bridge_id,
|
|
819
|
+
}
|
|
820
|
+
for key in ("job_id", "download_job_id", "stock_code", "stockcode", "period", "start_time", "end_time"):
|
|
821
|
+
value = params.get(key)
|
|
822
|
+
if value not in (None, ""):
|
|
823
|
+
meta[key] = value
|
|
824
|
+
return meta
|
|
825
|
+
|
|
826
|
+
def _generic_xttrader_call(self, method, params):
|
|
827
|
+
candidates = XTTRADER_COMPAT_CANDIDATES.get(method, (method,))
|
|
828
|
+
func = self._get_callable(*candidates)
|
|
829
|
+
if not func:
|
|
830
|
+
raise NotImplementedError(
|
|
831
|
+
"xttrader.%s requires QMT callable: %s"
|
|
832
|
+
% (method, ", ".join(candidates))
|
|
833
|
+
)
|
|
834
|
+
args = list(params.get("args") or [])
|
|
835
|
+
kwargs = dict(params.get("kwargs") or {})
|
|
836
|
+
account = params.get("account") or {}
|
|
837
|
+
account_id = account.get("account_id") or params.get("account_id") or self.account_id
|
|
838
|
+
account_type_value = account.get("account_type") or params.get("account_type")
|
|
839
|
+
account_type = self._account_type_name(account_type_value)
|
|
840
|
+
variants = []
|
|
841
|
+
if account:
|
|
842
|
+
variants.extend([
|
|
843
|
+
((account,) + tuple(args), kwargs),
|
|
844
|
+
((account_id,) + tuple(args), kwargs),
|
|
845
|
+
((account_id, account_type.lower()) + tuple(args), kwargs),
|
|
846
|
+
((account_id, account_type) + tuple(args), kwargs),
|
|
847
|
+
((account_id, account_type_value) + tuple(args), kwargs),
|
|
848
|
+
])
|
|
849
|
+
variants.extend([
|
|
850
|
+
(tuple(args), kwargs),
|
|
851
|
+
((params,), {}),
|
|
852
|
+
])
|
|
853
|
+
return self._call_variants(func, variants)
|
|
854
|
+
|
|
855
|
+
def _get_market_data(self, params):
|
|
856
|
+
func = self._get_callable("get_market_data")
|
|
857
|
+
if not func:
|
|
858
|
+
return self._get_market_data_ex(params)
|
|
859
|
+
return func(
|
|
860
|
+
params.get("field_list", []),
|
|
861
|
+
params.get("stock_list", []),
|
|
862
|
+
params.get("start_time", ""),
|
|
863
|
+
params.get("end_time", ""),
|
|
864
|
+
params.get("skip_paused", params.get("fill_data", True)),
|
|
865
|
+
params.get("period", "1d"),
|
|
866
|
+
params.get("dividend_type", "none"),
|
|
867
|
+
params.get("count", -1),
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
def _get_market_data_ex(self, params):
|
|
871
|
+
func = self._get_callable("get_market_data_ex")
|
|
872
|
+
if not func:
|
|
873
|
+
raise NotImplementedError("get_market_data_ex not found")
|
|
874
|
+
return func(
|
|
875
|
+
params.get("field_list", []),
|
|
876
|
+
params.get("stock_list", []),
|
|
877
|
+
params.get("period", "1d"),
|
|
878
|
+
params.get("start_time", ""),
|
|
879
|
+
params.get("end_time", ""),
|
|
880
|
+
params.get("count", -1),
|
|
881
|
+
params.get("dividend_type", "none"),
|
|
882
|
+
params.get("fill_data", True),
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def _get_local_data(self, params):
|
|
886
|
+
func = self._get_callable("get_local_data")
|
|
887
|
+
if not func:
|
|
888
|
+
return self._get_market_data_ex(params)
|
|
889
|
+
stock_code = self._first_param(params, ("stock_code", "stockcode", "stock", "code"), "")
|
|
890
|
+
stock_list = self._list_param(params.get("stock_list", params.get("code_list", [])))
|
|
891
|
+
if not stock_code and stock_list:
|
|
892
|
+
return dict((code, self._call_local_data(func, code, params)) for code in stock_list)
|
|
893
|
+
return self._call_local_data(func, stock_code, params)
|
|
894
|
+
|
|
895
|
+
def _call_local_data(self, func, stock_code, params):
|
|
896
|
+
return self._call_variants(func, [
|
|
897
|
+
((
|
|
898
|
+
stock_code,
|
|
899
|
+
params.get("start_time", params.get("start_date", "19700101")),
|
|
900
|
+
params.get("end_time", params.get("end_date", "22010101")),
|
|
901
|
+
params.get("period", "follow"),
|
|
902
|
+
params.get("divid_type", params.get("dividend_type", "none")),
|
|
903
|
+
params.get("count", -1),
|
|
904
|
+
), {}),
|
|
905
|
+
((
|
|
906
|
+
stock_code,
|
|
907
|
+
params.get("start_time", params.get("start_date", "19700101")),
|
|
908
|
+
params.get("end_time", params.get("end_date", "22010101")),
|
|
909
|
+
params.get("period", "follow"),
|
|
910
|
+
params.get("divid_type", params.get("dividend_type", "none")),
|
|
911
|
+
), {}),
|
|
912
|
+
((
|
|
913
|
+
stock_code,
|
|
914
|
+
params.get("start_time", params.get("start_date", "19700101")),
|
|
915
|
+
params.get("end_time", params.get("end_date", "22010101")),
|
|
916
|
+
), {}),
|
|
917
|
+
((stock_code,), {}),
|
|
918
|
+
])
|
|
919
|
+
|
|
920
|
+
def _download_event_meta(self, params, kind, stage):
|
|
921
|
+
meta = {
|
|
922
|
+
"download": True,
|
|
923
|
+
"download_kind": kind,
|
|
924
|
+
"stage": stage,
|
|
925
|
+
"bridge_id": self.bridge_id,
|
|
926
|
+
}
|
|
927
|
+
job_id = params.get("download_job_id") or params.get("job_id")
|
|
928
|
+
if job_id:
|
|
929
|
+
meta["job_id"] = str(job_id)
|
|
930
|
+
for name in ("stock_code", "period", "start_time", "end_time"):
|
|
931
|
+
value = params.get(name)
|
|
932
|
+
if value not in (None, ""):
|
|
933
|
+
meta[name] = value
|
|
934
|
+
for name in ("stock_list", "code_list", "table_list"):
|
|
935
|
+
value = params.get(name)
|
|
936
|
+
if value:
|
|
937
|
+
meta[name] = value
|
|
938
|
+
return meta
|
|
939
|
+
|
|
940
|
+
def _send_download_event(self, client_id, params, kind, stage, data=None):
|
|
941
|
+
callback_event = params.get("callback_event")
|
|
942
|
+
if not callback_event or not client_id:
|
|
943
|
+
return
|
|
944
|
+
self._send_event(
|
|
945
|
+
client_id,
|
|
946
|
+
callback_event,
|
|
947
|
+
data if data is not None else {},
|
|
948
|
+
meta=self._download_event_meta(params, kind, stage),
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
def _download_history_data(self, params, msg=None):
|
|
952
|
+
client_id = msg.get("client_id") if msg else None
|
|
953
|
+
emit_lifecycle = bool(params.get("download_emit_lifecycle"))
|
|
954
|
+
func = self._get_callable("download_history_data", "down_history_data")
|
|
955
|
+
if not func:
|
|
956
|
+
if emit_lifecycle:
|
|
957
|
+
self._send_download_event(client_id, params, "history", "error", {
|
|
958
|
+
"stage": "error",
|
|
959
|
+
"error": "download_history_data not found",
|
|
960
|
+
})
|
|
961
|
+
raise NotImplementedError("download_history_data not found")
|
|
962
|
+
variants = []
|
|
963
|
+
incrementally = params.get("incrementally")
|
|
964
|
+
if incrementally is not None:
|
|
965
|
+
variants.append((
|
|
966
|
+
(
|
|
967
|
+
params.get("stock_code", ""),
|
|
968
|
+
params.get("period", "1d"),
|
|
969
|
+
params.get("start_time", ""),
|
|
970
|
+
params.get("end_time", ""),
|
|
971
|
+
incrementally,
|
|
972
|
+
),
|
|
973
|
+
{},
|
|
974
|
+
))
|
|
975
|
+
variants.append((
|
|
976
|
+
(
|
|
977
|
+
params.get("stock_code", ""),
|
|
978
|
+
params.get("period", "1d"),
|
|
979
|
+
params.get("start_time", ""),
|
|
980
|
+
params.get("end_time", ""),
|
|
981
|
+
),
|
|
982
|
+
{},
|
|
983
|
+
))
|
|
984
|
+
if emit_lifecycle:
|
|
985
|
+
self._send_download_event(client_id, params, "history", "submitted", {
|
|
986
|
+
"stage": "submitted",
|
|
987
|
+
"message": "history download request submitted",
|
|
988
|
+
})
|
|
989
|
+
try:
|
|
990
|
+
result = self._call_variants(func, variants)
|
|
991
|
+
except Exception as e:
|
|
992
|
+
if emit_lifecycle:
|
|
993
|
+
self._send_download_event(client_id, params, "history", "error", {
|
|
994
|
+
"stage": "error",
|
|
995
|
+
"error": str(e),
|
|
996
|
+
})
|
|
997
|
+
raise
|
|
998
|
+
if emit_lifecycle:
|
|
999
|
+
self._send_download_event(client_id, params, "history", "request_done", {
|
|
1000
|
+
"stage": "request_done",
|
|
1001
|
+
"result": result,
|
|
1002
|
+
})
|
|
1003
|
+
return result
|
|
1004
|
+
|
|
1005
|
+
def _download_history_data2(self, params, msg):
|
|
1006
|
+
client_id = msg.get("client_id")
|
|
1007
|
+
callback_event = params.get("callback_event")
|
|
1008
|
+
emit_lifecycle = bool(params.get("download_emit_lifecycle"))
|
|
1009
|
+
func = self._get_callable("download_history_data2", "down_history_data2")
|
|
1010
|
+
if not func:
|
|
1011
|
+
if emit_lifecycle:
|
|
1012
|
+
self._send_download_event(client_id, params, "history", "error", {
|
|
1013
|
+
"stage": "error",
|
|
1014
|
+
"error": "download_history_data2 not found",
|
|
1015
|
+
})
|
|
1016
|
+
raise NotImplementedError("download_history_data2 not found")
|
|
1017
|
+
|
|
1018
|
+
def callback(data):
|
|
1019
|
+
if callback_event and client_id:
|
|
1020
|
+
self._send_event(
|
|
1021
|
+
client_id,
|
|
1022
|
+
callback_event,
|
|
1023
|
+
data,
|
|
1024
|
+
meta=self._download_event_meta(params, "history", "progress"),
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
callback_func = callback if callback_event else None
|
|
1028
|
+
variants = []
|
|
1029
|
+
incrementally = params.get("incrementally")
|
|
1030
|
+
if incrementally is not None:
|
|
1031
|
+
variants.append((
|
|
1032
|
+
(
|
|
1033
|
+
params.get("stock_list", params.get("code_list", [])),
|
|
1034
|
+
params.get("period", "1d"),
|
|
1035
|
+
params.get("start_time", ""),
|
|
1036
|
+
params.get("end_time", ""),
|
|
1037
|
+
callback_func,
|
|
1038
|
+
incrementally,
|
|
1039
|
+
),
|
|
1040
|
+
{},
|
|
1041
|
+
))
|
|
1042
|
+
variants.append((
|
|
1043
|
+
(
|
|
1044
|
+
params.get("stock_list", params.get("code_list", [])),
|
|
1045
|
+
params.get("period", "1d"),
|
|
1046
|
+
params.get("start_time", ""),
|
|
1047
|
+
params.get("end_time", ""),
|
|
1048
|
+
callback_func,
|
|
1049
|
+
),
|
|
1050
|
+
{},
|
|
1051
|
+
))
|
|
1052
|
+
if emit_lifecycle:
|
|
1053
|
+
self._send_download_event(client_id, params, "history", "submitted", {
|
|
1054
|
+
"stage": "submitted",
|
|
1055
|
+
"message": "history download request submitted",
|
|
1056
|
+
})
|
|
1057
|
+
try:
|
|
1058
|
+
result = self._call_variants(func, variants)
|
|
1059
|
+
except Exception as e:
|
|
1060
|
+
if emit_lifecycle:
|
|
1061
|
+
self._send_download_event(client_id, params, "history", "error", {
|
|
1062
|
+
"stage": "error",
|
|
1063
|
+
"error": str(e),
|
|
1064
|
+
})
|
|
1065
|
+
raise
|
|
1066
|
+
if emit_lifecycle:
|
|
1067
|
+
self._send_download_event(client_id, params, "history", "request_done", {
|
|
1068
|
+
"stage": "request_done",
|
|
1069
|
+
"result": result,
|
|
1070
|
+
})
|
|
1071
|
+
return result
|
|
1072
|
+
|
|
1073
|
+
def _get_instrument_detail(self, params):
|
|
1074
|
+
func = self._get_callable("get_instrument_detail")
|
|
1075
|
+
if not func:
|
|
1076
|
+
raise NotImplementedError("get_instrument_detail not found")
|
|
1077
|
+
return func(params.get("stock_code", ""))
|
|
1078
|
+
|
|
1079
|
+
def _get_financial_data(self, params):
|
|
1080
|
+
func = self._get_callable("get_financial_data")
|
|
1081
|
+
if not func:
|
|
1082
|
+
raise NotImplementedError("get_financial_data not found")
|
|
1083
|
+
fields = params.get("field_list") or []
|
|
1084
|
+
stock_list = params.get("stock_list", params.get("code_list", []))
|
|
1085
|
+
table_list = params.get("table_list") or []
|
|
1086
|
+
start_time = params.get("start_time", params.get("start_date", ""))
|
|
1087
|
+
end_time = params.get("end_time", params.get("end_date", ""))
|
|
1088
|
+
report_type = params.get("report_type") or ("announce_time" if fields else "report_time")
|
|
1089
|
+
variants = []
|
|
1090
|
+
if fields:
|
|
1091
|
+
variants.append(((fields, stock_list, start_time, end_time, report_type), {}))
|
|
1092
|
+
variants.append(((fields, stock_list, start_time, end_time), {}))
|
|
1093
|
+
if not variants:
|
|
1094
|
+
raise ValueError("field_list is required")
|
|
1095
|
+
return self._call_variants(func, variants)
|
|
1096
|
+
|
|
1097
|
+
def _get_raw_financial_data(self, params):
|
|
1098
|
+
func = self._get_callable("get_raw_financial_data")
|
|
1099
|
+
if not func:
|
|
1100
|
+
raise NotImplementedError("get_raw_financial_data not found")
|
|
1101
|
+
fields = params.get("field_list") or []
|
|
1102
|
+
stock_list = params.get("stock_list", params.get("code_list", []))
|
|
1103
|
+
if not fields:
|
|
1104
|
+
raise ValueError("field_list is required for get_raw_financial_data")
|
|
1105
|
+
return self._call_variants(func, [
|
|
1106
|
+
((
|
|
1107
|
+
fields,
|
|
1108
|
+
stock_list,
|
|
1109
|
+
params.get("start_time", params.get("start_date", "")),
|
|
1110
|
+
params.get("end_time", params.get("end_date", "")),
|
|
1111
|
+
params.get("report_type") or "announce_time",
|
|
1112
|
+
), {}),
|
|
1113
|
+
((
|
|
1114
|
+
fields,
|
|
1115
|
+
stock_list,
|
|
1116
|
+
params.get("start_time", params.get("start_date", "")),
|
|
1117
|
+
params.get("end_time", params.get("end_date", "")),
|
|
1118
|
+
), {}),
|
|
1119
|
+
])
|
|
1120
|
+
|
|
1121
|
+
def _default_financial_field(self, table):
|
|
1122
|
+
table = str(table or "").strip().upper()
|
|
1123
|
+
defaults = {
|
|
1124
|
+
"ASHAREBALANCESHEET": "fix_assets",
|
|
1125
|
+
"ASHAREINCOME": "net_profit_excl_min_int_inc",
|
|
1126
|
+
"ASHARECASHFLOW": "net_cash_flows_oper_act",
|
|
1127
|
+
"CAPITALSTRUCTURE": "capital",
|
|
1128
|
+
"PERSHAREINDEX": "eps",
|
|
1129
|
+
}
|
|
1130
|
+
return defaults.get(table, "fix_assets")
|
|
1131
|
+
|
|
1132
|
+
def _financial_probe_fields(self, params):
|
|
1133
|
+
fields = self._list_param(params.get("field_list") or params.get("fields"))
|
|
1134
|
+
tables = self._list_param(params.get("table_list") or params.get("tables") or params.get("table"))
|
|
1135
|
+
if not tables:
|
|
1136
|
+
tables = ["ASHAREBALANCESHEET"]
|
|
1137
|
+
if not fields:
|
|
1138
|
+
fields = [self._default_financial_field(tables[0])]
|
|
1139
|
+
if len(tables) == 1:
|
|
1140
|
+
table = tables[0]
|
|
1141
|
+
fields = [
|
|
1142
|
+
field if "." in str(field) or "。" in str(field) else "%s.%s" % (table, field)
|
|
1143
|
+
for field in fields
|
|
1144
|
+
]
|
|
1145
|
+
return fields
|
|
1146
|
+
|
|
1147
|
+
def _summarize_data_result(self, value):
|
|
1148
|
+
if value is None:
|
|
1149
|
+
return {"type": "None", "empty": True}
|
|
1150
|
+
type_name = value.__class__.__name__
|
|
1151
|
+
if type_name == "DataFrame":
|
|
1152
|
+
shape = list(getattr(value, "shape", []) or [])
|
|
1153
|
+
columns = [str(item) for item in list(getattr(value, "columns", []) or [])[:20]]
|
|
1154
|
+
return {
|
|
1155
|
+
"type": "DataFrame",
|
|
1156
|
+
"shape": shape,
|
|
1157
|
+
"columns": columns,
|
|
1158
|
+
"empty": bool(getattr(value, "empty", False)),
|
|
1159
|
+
}
|
|
1160
|
+
if type_name == "Series":
|
|
1161
|
+
size = int(getattr(value, "size", 0) or 0)
|
|
1162
|
+
return {"type": "Series", "count": size, "empty": size <= 0}
|
|
1163
|
+
if isinstance(value, dict):
|
|
1164
|
+
return {
|
|
1165
|
+
"type": "dict",
|
|
1166
|
+
"count": len(value),
|
|
1167
|
+
"keys": [str(item) for item in list(value.keys())[:20]],
|
|
1168
|
+
"empty": len(value) <= 0,
|
|
1169
|
+
}
|
|
1170
|
+
if isinstance(value, (list, tuple, set)):
|
|
1171
|
+
return {"type": type_name, "count": len(value), "empty": len(value) <= 0}
|
|
1172
|
+
return {"type": type_name, "preview": str(value)[:200], "empty": False}
|
|
1173
|
+
|
|
1174
|
+
def _check_local_financial_data(self, params):
|
|
1175
|
+
fields = self._financial_probe_fields(params)
|
|
1176
|
+
stock_list = params.get("stock_list", params.get("code_list", []))
|
|
1177
|
+
start_time = params.get("start_time", params.get("start_date", ""))
|
|
1178
|
+
end_time = params.get("end_time", params.get("end_date", ""))
|
|
1179
|
+
report_type = params.get("report_type") or "report_time"
|
|
1180
|
+
func = self._get_callable("get_raw_financial_data")
|
|
1181
|
+
action = "get_raw_financial_data"
|
|
1182
|
+
if not func:
|
|
1183
|
+
func = self._get_callable("get_financial_data")
|
|
1184
|
+
action = "get_financial_data"
|
|
1185
|
+
if not func:
|
|
1186
|
+
raise NotImplementedError("get_raw_financial_data/get_financial_data not found")
|
|
1187
|
+
result = self._call_variants(func, [
|
|
1188
|
+
((fields, stock_list, start_time, end_time, report_type), {}),
|
|
1189
|
+
((fields, stock_list, start_time, end_time), {}),
|
|
1190
|
+
])
|
|
1191
|
+
return {
|
|
1192
|
+
"download_supported": False,
|
|
1193
|
+
"manual_download_required": True,
|
|
1194
|
+
"manual_download_hint": "QMT官方脚本侧未提供财务数据下载函数;请先在QMT客户端 数据管理 - 财务数据下载 中下载,再读取本地财务数据。",
|
|
1195
|
+
"query_action": action,
|
|
1196
|
+
"field_list": fields,
|
|
1197
|
+
"stock_list": stock_list,
|
|
1198
|
+
"query_summary": self._summarize_data_result(result),
|
|
1199
|
+
"result": True,
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
def _download_financial_data(self, params, msg=None):
|
|
1203
|
+
stock_list = params.get("stock_list", params.get("code_list", []))
|
|
1204
|
+
table_list = params.get("table_list") or []
|
|
1205
|
+
start_time = params.get("start_time", params.get("start_date", ""))
|
|
1206
|
+
end_time = params.get("end_time", params.get("end_date", ""))
|
|
1207
|
+
callback_event = params.get("callback_event")
|
|
1208
|
+
client_id = msg.get("client_id") if msg else None
|
|
1209
|
+
emit_lifecycle = bool(params.get("download_emit_lifecycle"))
|
|
1210
|
+
func = self._get_callable("download_financial_data2", "down_financial_data2")
|
|
1211
|
+
if func:
|
|
1212
|
+
def callback(data):
|
|
1213
|
+
if callback_event and client_id:
|
|
1214
|
+
self._send_event(
|
|
1215
|
+
client_id,
|
|
1216
|
+
callback_event,
|
|
1217
|
+
data,
|
|
1218
|
+
meta=self._download_event_meta(params, "financial", "progress"),
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
callback_func = callback if callback_event else None
|
|
1222
|
+
variants = [
|
|
1223
|
+
((stock_list, table_list, start_time, end_time, callback_func), {}),
|
|
1224
|
+
((stock_list, table_list, start_time, end_time), {}),
|
|
1225
|
+
]
|
|
1226
|
+
if emit_lifecycle:
|
|
1227
|
+
self._send_download_event(client_id, params, "financial", "submitted", {
|
|
1228
|
+
"stage": "submitted",
|
|
1229
|
+
"message": "financial download request submitted",
|
|
1230
|
+
})
|
|
1231
|
+
try:
|
|
1232
|
+
result = self._call_variants(func, variants)
|
|
1233
|
+
except Exception as e:
|
|
1234
|
+
if emit_lifecycle:
|
|
1235
|
+
self._send_download_event(client_id, params, "financial", "error", {
|
|
1236
|
+
"stage": "error",
|
|
1237
|
+
"error": str(e),
|
|
1238
|
+
})
|
|
1239
|
+
raise
|
|
1240
|
+
if emit_lifecycle:
|
|
1241
|
+
self._send_download_event(client_id, params, "financial", "request_done", {
|
|
1242
|
+
"stage": "request_done",
|
|
1243
|
+
"result": result,
|
|
1244
|
+
})
|
|
1245
|
+
return result
|
|
1246
|
+
func = self._get_callable("download_financial_data", "down_financial_data")
|
|
1247
|
+
if not func:
|
|
1248
|
+
if emit_lifecycle:
|
|
1249
|
+
self._send_download_event(client_id, params, "financial_check", "submitted", {
|
|
1250
|
+
"stage": "submitted",
|
|
1251
|
+
"message": "开始校验本地财务数据,QMT官方脚本侧未提供财务下载函数。",
|
|
1252
|
+
})
|
|
1253
|
+
try:
|
|
1254
|
+
result = self._check_local_financial_data(params)
|
|
1255
|
+
except Exception as e:
|
|
1256
|
+
if emit_lifecycle:
|
|
1257
|
+
self._send_download_event(client_id, params, "financial_check", "error", {
|
|
1258
|
+
"stage": "error",
|
|
1259
|
+
"error": str(e),
|
|
1260
|
+
})
|
|
1261
|
+
raise
|
|
1262
|
+
if emit_lifecycle:
|
|
1263
|
+
self._send_download_event(client_id, params, "financial_check", "request_done", {
|
|
1264
|
+
"stage": "request_done",
|
|
1265
|
+
"message": "本地财务数据校验已返回。",
|
|
1266
|
+
"summary": result.get("query_summary"),
|
|
1267
|
+
"available": not bool((result.get("query_summary") or {}).get("empty")),
|
|
1268
|
+
})
|
|
1269
|
+
return result
|
|
1270
|
+
if emit_lifecycle:
|
|
1271
|
+
self._send_download_event(client_id, params, "financial", "submitted", {
|
|
1272
|
+
"stage": "submitted",
|
|
1273
|
+
"message": "financial download request submitted",
|
|
1274
|
+
})
|
|
1275
|
+
try:
|
|
1276
|
+
result = self._call_variants(func, [
|
|
1277
|
+
((stock_list, table_list), {}),
|
|
1278
|
+
((stock_list,), {}),
|
|
1279
|
+
])
|
|
1280
|
+
except Exception as e:
|
|
1281
|
+
if emit_lifecycle:
|
|
1282
|
+
self._send_download_event(client_id, params, "financial", "error", {
|
|
1283
|
+
"stage": "error",
|
|
1284
|
+
"error": str(e),
|
|
1285
|
+
})
|
|
1286
|
+
raise
|
|
1287
|
+
if emit_lifecycle:
|
|
1288
|
+
self._send_download_event(client_id, params, "financial", "request_done", {
|
|
1289
|
+
"stage": "request_done",
|
|
1290
|
+
"result": result,
|
|
1291
|
+
})
|
|
1292
|
+
return result
|
|
1293
|
+
|
|
1294
|
+
def _get_trading_dates(self, params):
|
|
1295
|
+
func = self._get_callable("get_trading_dates")
|
|
1296
|
+
if not func:
|
|
1297
|
+
raise NotImplementedError("get_trading_dates not found")
|
|
1298
|
+
return func(
|
|
1299
|
+
self._first_param(params, ("stockcode", "stock_code", "stock", "code"), ""),
|
|
1300
|
+
params.get("start_date", params.get("start_time", "")),
|
|
1301
|
+
params.get("end_date", params.get("end_time", "")),
|
|
1302
|
+
params.get("count", -1),
|
|
1303
|
+
params.get("period", "1d"),
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
def _call_stock_callable(self, method, params):
|
|
1307
|
+
func = self._get_callable(method)
|
|
1308
|
+
if not func:
|
|
1309
|
+
raise NotImplementedError("%s not found" % method)
|
|
1310
|
+
return func(self._first_param(params, ("stock_code", "stockcode", "stock", "code"), ""))
|
|
1311
|
+
|
|
1312
|
+
def _get_weight_in_index(self, params):
|
|
1313
|
+
func = self._get_callable("get_weight_in_index")
|
|
1314
|
+
if not func:
|
|
1315
|
+
raise NotImplementedError("get_weight_in_index not found")
|
|
1316
|
+
return func(
|
|
1317
|
+
self._first_param(params, ("mtkindexcode", "index_code", "index", "index_code_ref"), ""),
|
|
1318
|
+
self._first_param(params, ("stockcode", "stock_code", "stock", "code"), ""),
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
def _get_turnover_rate(self, params):
|
|
1322
|
+
func = self._get_callable("get_turnover_rate")
|
|
1323
|
+
if not func:
|
|
1324
|
+
raise NotImplementedError("get_turnover_rate not found")
|
|
1325
|
+
return func(
|
|
1326
|
+
self._first_param(params, ("stock_code", "stockcode", "stock", "code"), ""),
|
|
1327
|
+
params.get("start_time", params.get("start_date", "")),
|
|
1328
|
+
params.get("end_time", params.get("end_date", "")),
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
def _get_etf_list(self, params):
|
|
1332
|
+
func = self._get_callable("get_ETF_list", "get_etf_list")
|
|
1333
|
+
if not func:
|
|
1334
|
+
raise NotImplementedError("get_ETF_list not found")
|
|
1335
|
+
return func(
|
|
1336
|
+
params.get("market", ""),
|
|
1337
|
+
self._first_param(params, ("stockcode", "stock_code", "stock", "code"), ""),
|
|
1338
|
+
self._list_param(params.get("typeList", params.get("type_list", []))),
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1341
|
+
def _get_option_detail_data(self, params):
|
|
1342
|
+
func = self._get_callable("get_option_detail_data")
|
|
1343
|
+
if not func:
|
|
1344
|
+
raise NotImplementedError("get_option_detail_data not found")
|
|
1345
|
+
return func(self._first_param(params, ("stockcode", "stock_code", "opt_code", "code"), ""))
|
|
1346
|
+
|
|
1347
|
+
def _get_option_list(self, params):
|
|
1348
|
+
func = self._get_callable("get_option_list")
|
|
1349
|
+
if not func:
|
|
1350
|
+
raise NotImplementedError("get_option_list not found")
|
|
1351
|
+
return func(
|
|
1352
|
+
self._first_param(params, ("object", "underlying_code", "undl_code", "stock_code", "code"), ""),
|
|
1353
|
+
params.get("dedate", params.get("expire_date", "")),
|
|
1354
|
+
params.get("opttype", params.get("option_type", "")),
|
|
1355
|
+
params.get("isavailavle", params.get("is_available", params.get("available", False))),
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
def _get_option_undl(self, params):
|
|
1359
|
+
func = self._get_callable("get_option_undl")
|
|
1360
|
+
if not func:
|
|
1361
|
+
raise NotImplementedError("get_option_undl not found")
|
|
1362
|
+
return func(self._first_param(params, ("opt_code", "stock_code", "stockcode", "code"), ""))
|
|
1363
|
+
|
|
1364
|
+
def _get_option_undl_data(self, params):
|
|
1365
|
+
func = self._get_callable("get_option_undl_data")
|
|
1366
|
+
if not func:
|
|
1367
|
+
raise NotImplementedError("get_option_undl_data not found")
|
|
1368
|
+
return self._call_variants(func, [
|
|
1369
|
+
((self._first_param(params, ("undl_code_ref", "undl_code", "underlying_code", "stock_code", "code"), ""),), {}),
|
|
1370
|
+
((), {}),
|
|
1371
|
+
])
|
|
1372
|
+
|
|
1373
|
+
def _get_his_st_data(self, params):
|
|
1374
|
+
func = self._get_callable("get_his_st_data")
|
|
1375
|
+
if not func:
|
|
1376
|
+
raise NotImplementedError("get_his_st_data not found")
|
|
1377
|
+
return func(self._first_param(params, ("stockCode", "stock_code", "stockcode", "code"), ""))
|
|
1378
|
+
|
|
1379
|
+
def _get_his_index_data(self, params):
|
|
1380
|
+
func = self._get_callable("get_his_index_data")
|
|
1381
|
+
if not func:
|
|
1382
|
+
raise NotImplementedError("get_his_index_data not found")
|
|
1383
|
+
return func(self._first_param(params, ("stockCode", "stock_code", "stockcode", "code"), ""))
|
|
1384
|
+
|
|
1385
|
+
def _get_factor_data(self, params):
|
|
1386
|
+
func = self._get_callable("get_factor_data")
|
|
1387
|
+
if not func:
|
|
1388
|
+
raise NotImplementedError("get_factor_data not found")
|
|
1389
|
+
return func(
|
|
1390
|
+
params.get("field_list", params.get("fields", [])),
|
|
1391
|
+
params.get("stock_list", params.get("code_list", [])),
|
|
1392
|
+
params.get("start_date", params.get("start_time", "")),
|
|
1393
|
+
params.get("end_date", params.get("end_time", "")),
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
def _subscribe_account(self, params, msg=None):
|
|
1397
|
+
account = params.get("account") or {}
|
|
1398
|
+
account_id = account.get("account_id") or params.get("account_id") or self.account_id
|
|
1399
|
+
if not account_id:
|
|
1400
|
+
raise ValueError("account_id is required")
|
|
1401
|
+
account_id = str(account_id).strip()
|
|
1402
|
+
account_type = self._account_type_name(account.get("account_type") or params.get("account_type"))
|
|
1403
|
+
self.account_id = account_id
|
|
1404
|
+
self.account_type = account_type
|
|
1405
|
+
subscriber_key = (account_type.upper(), account_id)
|
|
1406
|
+
client_id = ""
|
|
1407
|
+
if msg:
|
|
1408
|
+
client_id = msg.get("client_id") or msg.get("reply_channel") or ""
|
|
1409
|
+
if client_id:
|
|
1410
|
+
with self.subscriber_lock:
|
|
1411
|
+
self.account_subscribers.setdefault(subscriber_key, set()).add(client_id)
|
|
1412
|
+
self.client_accounts.setdefault(client_id, set()).add(subscriber_key)
|
|
1413
|
+
account_routing.subscribe(self.bridge_id, account_id, client_id, account_type=account_type)
|
|
1414
|
+
self._set_context_account(account_id, account_type)
|
|
1415
|
+
self._enable_auto_trade_callback()
|
|
1416
|
+
self._log("account subscribed account=%s client_id=%s" % (account_id, client_id or "-"))
|
|
1417
|
+
return 0
|
|
1418
|
+
|
|
1419
|
+
def _unsubscribe_account(self, params, msg=None):
|
|
1420
|
+
account = params.get("account") or {}
|
|
1421
|
+
account_id = account.get("account_id") or params.get("account_id")
|
|
1422
|
+
account_type = self._account_type_name(account.get("account_type") or params.get("account_type"))
|
|
1423
|
+
subscriber_key = None
|
|
1424
|
+
client_id = ""
|
|
1425
|
+
if msg:
|
|
1426
|
+
client_id = msg.get("client_id") or msg.get("reply_channel") or ""
|
|
1427
|
+
if account_id:
|
|
1428
|
+
account_id = str(account_id).strip()
|
|
1429
|
+
subscriber_key = (account_type.upper(), account_id)
|
|
1430
|
+
with self.subscriber_lock:
|
|
1431
|
+
if account_id and client_id:
|
|
1432
|
+
subscribers = self.account_subscribers.get(subscriber_key)
|
|
1433
|
+
if subscribers:
|
|
1434
|
+
subscribers.discard(client_id)
|
|
1435
|
+
if not subscribers:
|
|
1436
|
+
self.account_subscribers.pop(subscriber_key, None)
|
|
1437
|
+
accounts = self.client_accounts.get(client_id)
|
|
1438
|
+
if accounts:
|
|
1439
|
+
accounts.discard(subscriber_key)
|
|
1440
|
+
accounts.discard(account_id)
|
|
1441
|
+
if not accounts:
|
|
1442
|
+
self.client_accounts.pop(client_id, None)
|
|
1443
|
+
elif client_id:
|
|
1444
|
+
accounts = self.client_accounts.pop(client_id, set())
|
|
1445
|
+
for item in accounts:
|
|
1446
|
+
subscribers = self.account_subscribers.get(item)
|
|
1447
|
+
if subscribers:
|
|
1448
|
+
subscribers.discard(client_id)
|
|
1449
|
+
if not subscribers:
|
|
1450
|
+
self.account_subscribers.pop(item, None)
|
|
1451
|
+
account_routing.unsubscribe(self.bridge_id, account_id=account_id, client_id=client_id, account_type=account_type if account_id else None)
|
|
1452
|
+
if account_id and account_id == self.account_id:
|
|
1453
|
+
self.account_id = ""
|
|
1454
|
+
self.account_type = ""
|
|
1455
|
+
self._log("account unsubscribed account=%s client_id=%s" % (account_id or "-", client_id or "-"))
|
|
1456
|
+
return 0
|
|
1457
|
+
|
|
1458
|
+
def _format_trade_detail(self, obj, detail_type):
|
|
1459
|
+
detail_type = str(detail_type).lower()
|
|
1460
|
+
if detail_type == "order":
|
|
1461
|
+
return {
|
|
1462
|
+
"stock_code": self._stock_code(obj),
|
|
1463
|
+
"market": self._get_value(obj, "m_strExchangeID"),
|
|
1464
|
+
"instrument_name": self._get_value(obj, "m_strInstrumentName"),
|
|
1465
|
+
"order_source": self._order_source(obj),
|
|
1466
|
+
"order_time": self._first_value(obj, (
|
|
1467
|
+
"order_time",
|
|
1468
|
+
"entrust_time",
|
|
1469
|
+
"insert_time",
|
|
1470
|
+
"m_strOrderTime",
|
|
1471
|
+
"m_strEntrustTime",
|
|
1472
|
+
"m_strInsertTime",
|
|
1473
|
+
"m_nOrderTime",
|
|
1474
|
+
"m_nEntrustTime",
|
|
1475
|
+
"m_nInsertTime",
|
|
1476
|
+
)),
|
|
1477
|
+
"order_date": self._first_value(obj, (
|
|
1478
|
+
"order_date",
|
|
1479
|
+
"entrust_date",
|
|
1480
|
+
"m_strOrderDate",
|
|
1481
|
+
"m_strEntrustDate",
|
|
1482
|
+
"m_strTradingDay",
|
|
1483
|
+
"m_nOrderDate",
|
|
1484
|
+
"m_nEntrustDate",
|
|
1485
|
+
)),
|
|
1486
|
+
"offset_flag": self._get_value(obj, "m_nOffsetFlag"),
|
|
1487
|
+
"order_volume": self._get_value(obj, "m_nVolumeTotalOriginal"),
|
|
1488
|
+
"traded_price": self._get_value(obj, "m_dTradedPrice"),
|
|
1489
|
+
"traded_volume": self._get_value(obj, "m_nVolumeTraded"),
|
|
1490
|
+
"trade_amount": self._get_value(obj, "m_dTradeAmount"),
|
|
1491
|
+
"order_status": self._get_value(obj, "m_nOrderStatus"),
|
|
1492
|
+
"m_strInstrumentID": self._get_value(obj, "m_strInstrumentID"),
|
|
1493
|
+
"m_strExchangeID": self._get_value(obj, "m_strExchangeID"),
|
|
1494
|
+
"m_strInstrumentName": self._get_value(obj, "m_strInstrumentName"),
|
|
1495
|
+
"m_nOffsetFlag": self._get_value(obj, "m_nOffsetFlag"),
|
|
1496
|
+
"m_nVolumeTotalOriginal": self._get_value(obj, "m_nVolumeTotalOriginal"),
|
|
1497
|
+
"m_dTradedPrice": self._get_value(obj, "m_dTradedPrice"),
|
|
1498
|
+
"m_nVolumeTraded": self._get_value(obj, "m_nVolumeTraded"),
|
|
1499
|
+
"m_dTradeAmount": self._get_value(obj, "m_dTradeAmount"),
|
|
1500
|
+
"m_strRemark": self._get_value(obj, "m_strRemark"),
|
|
1501
|
+
"m_strStrategyName": self._get_value(obj, "m_strStrategyName"),
|
|
1502
|
+
"m_strOrderSysID": self._get_value(obj, "m_strOrderSysID"),
|
|
1503
|
+
"m_nOrderID": self._get_value(obj, "m_nOrderID"),
|
|
1504
|
+
"m_strOrderID": self._get_value(obj, "m_strOrderID"),
|
|
1505
|
+
"m_nOrderStatus": self._get_value(obj, "m_nOrderStatus"),
|
|
1506
|
+
"m_strOrderStatus": self._get_value(obj, "m_strOrderStatus"),
|
|
1507
|
+
"m_nOrderState": self._get_value(obj, "m_nOrderState"),
|
|
1508
|
+
"m_strStatus": self._get_value(obj, "m_strStatus"),
|
|
1509
|
+
"m_strOrderTime": self._get_value(obj, "m_strOrderTime"),
|
|
1510
|
+
"m_strEntrustTime": self._get_value(obj, "m_strEntrustTime"),
|
|
1511
|
+
"m_strInsertTime": self._get_value(obj, "m_strInsertTime"),
|
|
1512
|
+
"m_nOrderTime": self._get_value(obj, "m_nOrderTime"),
|
|
1513
|
+
"m_nEntrustTime": self._get_value(obj, "m_nEntrustTime"),
|
|
1514
|
+
"m_nInsertTime": self._get_value(obj, "m_nInsertTime"),
|
|
1515
|
+
"m_strOrderDate": self._get_value(obj, "m_strOrderDate"),
|
|
1516
|
+
"m_strEntrustDate": self._get_value(obj, "m_strEntrustDate"),
|
|
1517
|
+
"m_strTradingDay": self._get_value(obj, "m_strTradingDay"),
|
|
1518
|
+
}
|
|
1519
|
+
if detail_type == "deal":
|
|
1520
|
+
return {
|
|
1521
|
+
"stock_code": self._stock_code(obj),
|
|
1522
|
+
"market": self._get_value(obj, "m_strExchangeID"),
|
|
1523
|
+
"instrument_name": self._get_value(obj, "m_strInstrumentName"),
|
|
1524
|
+
"trade_time": self._first_value(obj, (
|
|
1525
|
+
"trade_time",
|
|
1526
|
+
"deal_time",
|
|
1527
|
+
"m_strTradeTime",
|
|
1528
|
+
"m_strDealTime",
|
|
1529
|
+
"m_nTradeTime",
|
|
1530
|
+
"m_nDealTime",
|
|
1531
|
+
)),
|
|
1532
|
+
"trade_date": self._first_value(obj, (
|
|
1533
|
+
"trade_date",
|
|
1534
|
+
"deal_date",
|
|
1535
|
+
"m_strTradeDate",
|
|
1536
|
+
"m_strDealDate",
|
|
1537
|
+
"m_strTradingDay",
|
|
1538
|
+
"m_nTradeDate",
|
|
1539
|
+
"m_nDealDate",
|
|
1540
|
+
)),
|
|
1541
|
+
"offset_flag": self._get_value(obj, "m_nOffsetFlag"),
|
|
1542
|
+
"price": self._get_value(obj, "m_dPrice"),
|
|
1543
|
+
"volume": self._get_value(obj, "m_nVolume"),
|
|
1544
|
+
"trade_amount": self._get_value(obj, "m_dTradeAmount"),
|
|
1545
|
+
"m_strInstrumentID": self._get_value(obj, "m_strInstrumentID"),
|
|
1546
|
+
"m_strExchangeID": self._get_value(obj, "m_strExchangeID"),
|
|
1547
|
+
"m_strInstrumentName": self._get_value(obj, "m_strInstrumentName"),
|
|
1548
|
+
"m_nOffsetFlag": self._get_value(obj, "m_nOffsetFlag"),
|
|
1549
|
+
"m_dPrice": self._get_value(obj, "m_dPrice"),
|
|
1550
|
+
"m_nVolume": self._get_value(obj, "m_nVolume"),
|
|
1551
|
+
"m_dTradeAmount": self._get_value(obj, "m_dTradeAmount"),
|
|
1552
|
+
"m_strTradeTime": self._get_value(obj, "m_strTradeTime"),
|
|
1553
|
+
"m_strDealTime": self._get_value(obj, "m_strDealTime"),
|
|
1554
|
+
"m_nTradeTime": self._get_value(obj, "m_nTradeTime"),
|
|
1555
|
+
"m_nDealTime": self._get_value(obj, "m_nDealTime"),
|
|
1556
|
+
"m_strTradeDate": self._get_value(obj, "m_strTradeDate"),
|
|
1557
|
+
"m_strDealDate": self._get_value(obj, "m_strDealDate"),
|
|
1558
|
+
"m_strTradingDay": self._get_value(obj, "m_strTradingDay"),
|
|
1559
|
+
}
|
|
1560
|
+
if detail_type == "position":
|
|
1561
|
+
return {
|
|
1562
|
+
"stock_code": self._stock_code(obj),
|
|
1563
|
+
"market": self._get_value(obj, "m_strExchangeID"),
|
|
1564
|
+
"instrument_name": self._get_value(obj, "m_strInstrumentName"),
|
|
1565
|
+
"volume": self._get_value(obj, "m_nVolume"),
|
|
1566
|
+
"can_use_volume": self._get_value(obj, "m_nCanUseVolume"),
|
|
1567
|
+
"open_price": self._get_value(obj, "m_dOpenPrice"),
|
|
1568
|
+
"market_value": self._get_value(obj, "m_dInstrumentValue"),
|
|
1569
|
+
"position_cost": self._get_value(obj, "m_dPositionCost"),
|
|
1570
|
+
"position_profit": self._get_value(obj, "m_dPositionProfit"),
|
|
1571
|
+
"m_strInstrumentID": self._get_value(obj, "m_strInstrumentID"),
|
|
1572
|
+
"m_strExchangeID": self._get_value(obj, "m_strExchangeID"),
|
|
1573
|
+
"m_strInstrumentName": self._get_value(obj, "m_strInstrumentName"),
|
|
1574
|
+
"m_nVolume": self._get_value(obj, "m_nVolume"),
|
|
1575
|
+
"m_nCanUseVolume": self._get_value(obj, "m_nCanUseVolume"),
|
|
1576
|
+
"m_dOpenPrice": self._get_value(obj, "m_dOpenPrice"),
|
|
1577
|
+
"m_dInstrumentValue": self._get_value(obj, "m_dInstrumentValue"),
|
|
1578
|
+
"m_dPositionCost": self._get_value(obj, "m_dPositionCost"),
|
|
1579
|
+
"m_dPositionProfit": self._get_value(obj, "m_dPositionProfit"),
|
|
1580
|
+
}
|
|
1581
|
+
if detail_type == "account":
|
|
1582
|
+
return {
|
|
1583
|
+
"balance": self._get_value(obj, "m_dBalance"),
|
|
1584
|
+
"assure_asset": self._get_value(obj, "m_dAssureAsset"),
|
|
1585
|
+
"market_value": self._get_value(obj, "m_dInstrumentValue"),
|
|
1586
|
+
"total_debit": self._get_value(obj, "m_dTotalDebit"),
|
|
1587
|
+
"available": self._get_value(obj, "m_dAvailable"),
|
|
1588
|
+
"position_profit": self._get_value(obj, "m_dPositionProfit"),
|
|
1589
|
+
"m_dBalance": self._get_value(obj, "m_dBalance"),
|
|
1590
|
+
"m_dAssureAsset": self._get_value(obj, "m_dAssureAsset"),
|
|
1591
|
+
"m_dInstrumentValue": self._get_value(obj, "m_dInstrumentValue"),
|
|
1592
|
+
"m_dTotalDebit": self._get_value(obj, "m_dTotalDebit"),
|
|
1593
|
+
"m_dAvailable": self._get_value(obj, "m_dAvailable"),
|
|
1594
|
+
"m_dPositionProfit": self._get_value(obj, "m_dPositionProfit"),
|
|
1595
|
+
}
|
|
1596
|
+
return {"value": str(obj)}
|
|
1597
|
+
|
|
1598
|
+
def _first_value(self, obj, names):
|
|
1599
|
+
for name in names:
|
|
1600
|
+
value = self._get_value(obj, name)
|
|
1601
|
+
if value is not None and value != "":
|
|
1602
|
+
return value
|
|
1603
|
+
return None
|
|
1604
|
+
|
|
1605
|
+
def _stock_code(self, obj):
|
|
1606
|
+
instrument_id = self._get_value(obj, "m_strInstrumentID")
|
|
1607
|
+
exchange_id = self._get_value(obj, "m_strExchangeID")
|
|
1608
|
+
if instrument_id and exchange_id:
|
|
1609
|
+
return "%s.%s" % (instrument_id, exchange_id)
|
|
1610
|
+
return instrument_id
|
|
1611
|
+
|
|
1612
|
+
def _order_source(self, obj):
|
|
1613
|
+
values = [
|
|
1614
|
+
self._get_value(obj, name)
|
|
1615
|
+
for name in (
|
|
1616
|
+
"order_source",
|
|
1617
|
+
"source",
|
|
1618
|
+
"order_remark",
|
|
1619
|
+
"strategy_name",
|
|
1620
|
+
"m_strRemark",
|
|
1621
|
+
"m_strOrderRemark",
|
|
1622
|
+
"m_strStrategyName",
|
|
1623
|
+
)
|
|
1624
|
+
]
|
|
1625
|
+
text = " ".join(str(value or "") for value in values).strip().lower()
|
|
1626
|
+
return "cfquant" if "cfquant" in text else "other"
|
|
1627
|
+
|
|
1628
|
+
def _get_value(self, obj, name):
|
|
1629
|
+
if obj is None:
|
|
1630
|
+
return None
|
|
1631
|
+
try:
|
|
1632
|
+
return self._plain_value(getattr(obj, name))
|
|
1633
|
+
except AttributeError:
|
|
1634
|
+
pass
|
|
1635
|
+
except Exception as e:
|
|
1636
|
+
self._log(
|
|
1637
|
+
"trade detail getattr failed type=%s field=%s error=%s"
|
|
1638
|
+
% (type(obj).__name__, name, e)
|
|
1639
|
+
)
|
|
1640
|
+
try:
|
|
1641
|
+
getter = getattr(obj, "get", None)
|
|
1642
|
+
if callable(getter):
|
|
1643
|
+
return self._plain_value(getter(name))
|
|
1644
|
+
except AttributeError:
|
|
1645
|
+
pass
|
|
1646
|
+
except Exception as e:
|
|
1647
|
+
self._log(
|
|
1648
|
+
"trade detail get failed type=%s field=%s error=%s"
|
|
1649
|
+
% (type(obj).__name__, name, e)
|
|
1650
|
+
)
|
|
1651
|
+
return None
|
|
1652
|
+
|
|
1653
|
+
def _plain_value(self, value):
|
|
1654
|
+
if value is None or isinstance(value, (str, bool, int, float)):
|
|
1655
|
+
return value
|
|
1656
|
+
if isinstance(value, bytes):
|
|
1657
|
+
try:
|
|
1658
|
+
return value.decode("utf-8")
|
|
1659
|
+
except Exception:
|
|
1660
|
+
return str(value)
|
|
1661
|
+
try:
|
|
1662
|
+
item = getattr(value, "item", None)
|
|
1663
|
+
if callable(item):
|
|
1664
|
+
return item()
|
|
1665
|
+
except Exception:
|
|
1666
|
+
pass
|
|
1667
|
+
if isinstance(value, (list, tuple)):
|
|
1668
|
+
return [self._plain_value(item) for item in value]
|
|
1669
|
+
if isinstance(value, dict):
|
|
1670
|
+
return dict((str(k), self._plain_value(v)) for k, v in value.items())
|
|
1671
|
+
return str(value)
|
|
1672
|
+
|
|
1673
|
+
def _first_param(self, params, names, default=None):
|
|
1674
|
+
for name in names:
|
|
1675
|
+
value = params.get(name)
|
|
1676
|
+
if value is not None and value != "":
|
|
1677
|
+
return value
|
|
1678
|
+
return default
|
|
1679
|
+
|
|
1680
|
+
def _truthy_param(self, value):
|
|
1681
|
+
if isinstance(value, str):
|
|
1682
|
+
return value.strip().lower() in ("1", "true", "yes", "y", "on")
|
|
1683
|
+
return bool(value)
|
|
1684
|
+
|
|
1685
|
+
def _list_param(self, value):
|
|
1686
|
+
if value is None:
|
|
1687
|
+
return []
|
|
1688
|
+
if isinstance(value, (list, tuple)):
|
|
1689
|
+
return list(value)
|
|
1690
|
+
if isinstance(value, str):
|
|
1691
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
1692
|
+
return [value]
|
|
1693
|
+
|
|
1694
|
+
def _account_type_name(self, account_type):
|
|
1695
|
+
mapping = {
|
|
1696
|
+
1: "future",
|
|
1697
|
+
2: "stock",
|
|
1698
|
+
3: "credit",
|
|
1699
|
+
5: "future_option",
|
|
1700
|
+
6: "stock_option",
|
|
1701
|
+
7: "hugangtong",
|
|
1702
|
+
10: "new3board",
|
|
1703
|
+
11: "shengangtong",
|
|
1704
|
+
}
|
|
1705
|
+
if isinstance(account_type, str):
|
|
1706
|
+
return account_type
|
|
1707
|
+
return mapping.get(account_type, "stock")
|
|
1708
|
+
|
|
1709
|
+
def _set_context_account(self, account_id, account_type=None):
|
|
1710
|
+
if self.context is None or not account_id:
|
|
1711
|
+
return
|
|
1712
|
+
account_id = str(account_id).strip()
|
|
1713
|
+
account_type_text = str(account_type or "").strip().upper()
|
|
1714
|
+
try:
|
|
1715
|
+
if account_type_text:
|
|
1716
|
+
self.context.set_account(account_id, account_type_text)
|
|
1717
|
+
self._log("context account set account=%s account_type=%s mode=with_type" % (account_id, account_type_text))
|
|
1718
|
+
else:
|
|
1719
|
+
self.context.set_account(account_id)
|
|
1720
|
+
self._log("context account set account=%s account_type=%s mode=account_only" % (account_id, account_type_text or "-"))
|
|
1721
|
+
except Exception as e:
|
|
1722
|
+
try:
|
|
1723
|
+
self.context.set_account(account_id)
|
|
1724
|
+
self._log("context account set account=%s account_type=%s mode=fallback error=%s" % (account_id, account_type_text or "-", e))
|
|
1725
|
+
except Exception as fallback_error:
|
|
1726
|
+
self._log("context account set failed account=%s account_type=%s error=%s fallback_error=%s" % (account_id, account_type_text or "-", e, fallback_error))
|
|
1727
|
+
raise
|
|
1728
|
+
|
|
1729
|
+
def _enable_auto_trade_callback(self):
|
|
1730
|
+
if self.context is None or self.auto_trade_callback_enabled:
|
|
1731
|
+
return
|
|
1732
|
+
func = getattr(self.context, "set_auto_trade_callback", None)
|
|
1733
|
+
if callable(func):
|
|
1734
|
+
try:
|
|
1735
|
+
result = func(True)
|
|
1736
|
+
self.auto_trade_callback_enabled = True
|
|
1737
|
+
self._log("auto trade callback enabled result=%s" % result)
|
|
1738
|
+
return
|
|
1739
|
+
except Exception as e:
|
|
1740
|
+
self._log("auto trade callback enable failed:%s" % e)
|
|
1741
|
+
return
|
|
1742
|
+
func = self._get_callable("set_auto_trade_callback")
|
|
1743
|
+
if not callable(func):
|
|
1744
|
+
self._log("auto trade callback enable skipped: set_auto_trade_callback not found")
|
|
1745
|
+
return
|
|
1746
|
+
try:
|
|
1747
|
+
result = func(self.context, True)
|
|
1748
|
+
self.auto_trade_callback_enabled = True
|
|
1749
|
+
self._log("auto trade callback enabled result=%s" % result)
|
|
1750
|
+
except TypeError:
|
|
1751
|
+
try:
|
|
1752
|
+
result = func(True)
|
|
1753
|
+
self.auto_trade_callback_enabled = True
|
|
1754
|
+
self._log("auto trade callback enabled result=%s" % result)
|
|
1755
|
+
except Exception as e:
|
|
1756
|
+
self._log("auto trade callback enable failed:%s" % e)
|
|
1757
|
+
except Exception as e:
|
|
1758
|
+
self._log("auto trade callback enable failed:%s" % e)
|
|
1759
|
+
|
|
1760
|
+
def _send_trader_event(self, client_id, name, data):
|
|
1761
|
+
if client_id:
|
|
1762
|
+
self._send_event(client_id, "trader:%s" % name, data)
|
|
1763
|
+
|
|
1764
|
+
def _client_ids_for_account(self, account_id, account_type=None):
|
|
1765
|
+
account_id = str(account_id or "").strip()
|
|
1766
|
+
if not account_id:
|
|
1767
|
+
return []
|
|
1768
|
+
account_type = self._account_type_name(account_type).upper() if account_type not in (None, "") else ""
|
|
1769
|
+
with self.subscriber_lock:
|
|
1770
|
+
if account_type:
|
|
1771
|
+
client_ids = set(self.account_subscribers.get((account_type, account_id), set()))
|
|
1772
|
+
else:
|
|
1773
|
+
client_ids = set()
|
|
1774
|
+
for key, ids in self.account_subscribers.items():
|
|
1775
|
+
if isinstance(key, tuple) and len(key) == 2 and key[1] == account_id:
|
|
1776
|
+
client_ids.update(ids)
|
|
1777
|
+
elif key == account_id:
|
|
1778
|
+
client_ids.update(ids)
|
|
1779
|
+
if account_type:
|
|
1780
|
+
client_ids.update(account_routing.client_ids(self.bridge_id, account_id, account_type=account_type))
|
|
1781
|
+
return sorted(client_ids)
|
|
1782
|
+
|
|
1783
|
+
def _send_trader_event_to_account(self, account_id, name, data, account_type=None):
|
|
1784
|
+
if account_type and isinstance(data, dict):
|
|
1785
|
+
data.setdefault("account_type", self._account_type_name(account_type).upper())
|
|
1786
|
+
for client_id in self._client_ids_for_account(account_id, account_type=account_type):
|
|
1787
|
+
self._send_trader_event(client_id, name, data)
|
|
1788
|
+
|
|
1789
|
+
def _account_subscriber_status(self):
|
|
1790
|
+
with self.subscriber_lock:
|
|
1791
|
+
status = {}
|
|
1792
|
+
for key, client_ids in self.account_subscribers.items():
|
|
1793
|
+
if isinstance(key, tuple) and len(key) == 2:
|
|
1794
|
+
label = "%s:%s" % (key[0], key[1])
|
|
1795
|
+
else:
|
|
1796
|
+
label = "STOCK:%s" % key
|
|
1797
|
+
status[label] = len(client_ids)
|
|
1798
|
+
for account_id, count in account_routing.status(self.bridge_id).items():
|
|
1799
|
+
status[account_id] = max(status.get(account_id, 0), count)
|
|
1800
|
+
return status
|
|
1801
|
+
|
|
1802
|
+
def _send_event(self, client_id, name, data, subscription_id=None, meta=None):
|
|
1803
|
+
if not client_id or self.tx is None:
|
|
1804
|
+
return
|
|
1805
|
+
event = pack_event(name, data=data, client_id=client_id, subscription_id=subscription_id, meta=meta)
|
|
1806
|
+
self.tx.push("event", event, client_id)
|
|
1807
|
+
|
|
1808
|
+
def _call_variants(self, func, variants):
|
|
1809
|
+
last_error = None
|
|
1810
|
+
for args, kwargs in variants:
|
|
1811
|
+
try:
|
|
1812
|
+
return func(*args, **kwargs)
|
|
1813
|
+
except TypeError as e:
|
|
1814
|
+
last_error = e
|
|
1815
|
+
continue
|
|
1816
|
+
if last_error:
|
|
1817
|
+
raise last_error
|
|
1818
|
+
return func()
|
|
1819
|
+
|
|
1820
|
+
def _get_callable(self, *names):
|
|
1821
|
+
owners = [self.globals_dict]
|
|
1822
|
+
if self.context is not None:
|
|
1823
|
+
owners.append(self.context)
|
|
1824
|
+
inner_context = getattr(self.context, "context", None)
|
|
1825
|
+
if inner_context is not None and inner_context is not self.context:
|
|
1826
|
+
owners.append(inner_context)
|
|
1827
|
+
for owner in owners:
|
|
1828
|
+
for name in names:
|
|
1829
|
+
if isinstance(owner, dict):
|
|
1830
|
+
func = owner.get(name)
|
|
1831
|
+
else:
|
|
1832
|
+
func = getattr(owner, name, None)
|
|
1833
|
+
if callable(func):
|
|
1834
|
+
return func
|
|
1835
|
+
return None
|
|
1836
|
+
|
|
1837
|
+
def _load_txl(self):
|
|
1838
|
+
package_error = None
|
|
1839
|
+
try:
|
|
1840
|
+
from .tx import txl
|
|
1841
|
+
return txl
|
|
1842
|
+
except Exception as e:
|
|
1843
|
+
package_error = e
|
|
1844
|
+
try:
|
|
1845
|
+
from tx import txl
|
|
1846
|
+
return txl
|
|
1847
|
+
except Exception as path_error:
|
|
1848
|
+
raise RuntimeError(
|
|
1849
|
+
"failed to import txl from cfquant.tx or tx.py: %s; fallback: %s"
|
|
1850
|
+
% (package_error, path_error)
|
|
1851
|
+
)
|
|
1852
|
+
|
|
1853
|
+
def _default_log_file(self):
|
|
1854
|
+
base_dir = os.getcwd()
|
|
1855
|
+
log_dir = (
|
|
1856
|
+
os.environ.get("CFQUANT_QMT_LOG_DIR")
|
|
1857
|
+
or os.environ.get("CFQUANT_LOG_DIR")
|
|
1858
|
+
or os.path.join(base_dir, "log")
|
|
1859
|
+
)
|
|
1860
|
+
log_dir = os.path.abspath(log_dir)
|
|
1861
|
+
try:
|
|
1862
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
1863
|
+
except Exception:
|
|
1864
|
+
log_dir = base_dir
|
|
1865
|
+
return os.path.join(log_dir, "cfquant_qmt_bridge.log")
|
|
1866
|
+
|
|
1867
|
+
def _log(self, msg, force=False):
|
|
1868
|
+
if not force and not get_log_enabled():
|
|
1869
|
+
return
|
|
1870
|
+
msg = translate_log(msg)
|
|
1871
|
+
line = "%s %s" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg)
|
|
1872
|
+
try:
|
|
1873
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
|
1874
|
+
f.write(line + "\n")
|
|
1875
|
+
except Exception:
|
|
1876
|
+
pass
|
|
1877
|
+
if self.show:
|
|
1878
|
+
print(msg)
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def start_tx_trade_bridge(
|
|
1882
|
+
context,
|
|
1883
|
+
ip="127.0.0.1",
|
|
1884
|
+
port=2049,
|
|
1885
|
+
token="LTtx",
|
|
1886
|
+
request_channel="cfquant.request",
|
|
1887
|
+
bridge_id="default",
|
|
1888
|
+
account_id="",
|
|
1889
|
+
show=True,
|
|
1890
|
+
):
|
|
1891
|
+
try:
|
|
1892
|
+
globals_dict = sys._getframe(1).f_globals
|
|
1893
|
+
except Exception:
|
|
1894
|
+
globals_dict = {}
|
|
1895
|
+
return TxTradeBridge(
|
|
1896
|
+
context,
|
|
1897
|
+
ip=ip,
|
|
1898
|
+
port=port,
|
|
1899
|
+
token=token,
|
|
1900
|
+
request_channel=request_channel,
|
|
1901
|
+
bridge_id=bridge_id,
|
|
1902
|
+
account_id=account_id,
|
|
1903
|
+
show=show,
|
|
1904
|
+
globals_dict=globals_dict,
|
|
1905
|
+
)
|