cfquant 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. LTtx/__init__.py +2 -0
  2. LTtx/tx/Config.txt +8 -0
  3. LTtx/tx/LTtx_server.py +1177 -0
  4. LTtx/tx/__init__.py +5 -0
  5. LTtx/tx/tx.py +1935 -0
  6. cfquant/__init__.py +26 -0
  7. cfquant/account_routing.py +119 -0
  8. cfquant/channels.py +76 -0
  9. cfquant/cli.py +266 -0
  10. cfquant/client.py +365 -0
  11. cfquant/config.py +86 -0
  12. cfquant/hub.py +304 -0
  13. cfquant/logging_i18n.py +348 -0
  14. cfquant/normal_bridge.py +676 -0
  15. cfquant/pipe_bridge.py +249 -0
  16. cfquant/pipe_client.py +223 -0
  17. cfquant/pipe_hub.py +602 -0
  18. cfquant/pipe_transport.py +493 -0
  19. cfquant/protocol.py +238 -0
  20. cfquant/qmt_bridge.py +1081 -0
  21. cfquant/runtime_report.py +334 -0
  22. cfquant/tx.py +1898 -0
  23. cfquant/tx_trade_bridge.py +1905 -0
  24. cfquant/version.py +5 -0
  25. cfquant/xtconstant.py +262 -0
  26. cfquant/xtdata.py +462 -0
  27. cfquant/xttrader.py +955 -0
  28. cfquant/xttype.py +586 -0
  29. cfquant-0.2.0.dist-info/LICENSE +21 -0
  30. cfquant-0.2.0.dist-info/METADATA +144 -0
  31. cfquant-0.2.0.dist-info/RECORD +90 -0
  32. cfquant-0.2.0.dist-info/WHEEL +5 -0
  33. cfquant-0.2.0.dist-info/entry_points.txt +4 -0
  34. cfquant-0.2.0.dist-info/top_level.txt +6 -0
  35. cfquant_pipe_hub.py +45 -0
  36. cfquant_web_server.py +12137 -0
  37. qmt_scripts/CFQUANT.py +507 -0
  38. qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
  39. qmt_scripts/CFQUANT_LITE.py +4928 -0
  40. qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
  41. qmt_scripts/__init__.py +1 -0
  42. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SH.py +866 -0
  43. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py +866 -0
  44. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SH.py +5020 -0
  45. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SZ.py +5020 -0
  46. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SH.py +436 -0
  47. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SZ.py +436 -0
  48. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/readme.md +95 -0
  49. web_dashboard/__init__.py +1 -0
  50. web_dashboard/app.js +9847 -0
  51. web_dashboard/avatars/amber-pulse.svg +15 -0
  52. web_dashboard/avatars/copper-grid.svg +15 -0
  53. web_dashboard/avatars/market-blue.svg +15 -0
  54. web_dashboard/avatars/rose-circuit.svg +15 -0
  55. web_dashboard/avatars/signal-green.svg +15 -0
  56. web_dashboard/avatars/slate-wave.svg +15 -0
  57. web_dashboard/avatars/teal-orbit.svg +16 -0
  58. web_dashboard/avatars/violet-node.svg +15 -0
  59. web_dashboard/favicon.svg +5 -0
  60. web_dashboard/image/qmt_deploy/image-10.png +0 -0
  61. web_dashboard/image/qmt_deploy/image-11.png +0 -0
  62. web_dashboard/image/qmt_deploy/image-12.png +0 -0
  63. web_dashboard/image/qmt_deploy/image-13.png +0 -0
  64. web_dashboard/image/qmt_deploy/image-14.png +0 -0
  65. web_dashboard/image/qmt_deploy/image-15.png +0 -0
  66. web_dashboard/image/qmt_deploy/image-16.png +0 -0
  67. web_dashboard/image/qmt_deploy/image-17.png +0 -0
  68. web_dashboard/image/qmt_deploy/image-18.png +0 -0
  69. web_dashboard/image/qmt_deploy/image-19.png +0 -0
  70. web_dashboard/image/qmt_deploy/image-20.png +0 -0
  71. web_dashboard/image/qmt_deploy/image-21.png +0 -0
  72. web_dashboard/image/qmt_deploy/image-4.png +0 -0
  73. web_dashboard/image/qmt_deploy/image-5.png +0 -0
  74. web_dashboard/image/qmt_deploy/image-6.png +0 -0
  75. web_dashboard/image/qmt_deploy/image-7.png +0 -0
  76. web_dashboard/image/qmt_deploy/image-8.png +0 -0
  77. web_dashboard/image/qmt_deploy/image-9.png +0 -0
  78. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-1.png +0 -0
  79. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-2.png +0 -0
  80. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-3.png +0 -0
  81. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image.png +0 -0
  82. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479325179.png +0 -0
  83. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479745262.png +0 -0
  84. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480178700.png +0 -0
  85. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480428759.png +0 -0
  86. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783489807217.png +0 -0
  87. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783490503491.png +0 -0
  88. web_dashboard/index.html +2317 -0
  89. web_dashboard/styles.css +7817 -0
  90. web_dashboard/vendor/mermaid.min.js +2029 -0
@@ -0,0 +1,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
+ )