xtquant-share 1.1.2__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.
- xqshare/__init__.py +38 -0
- xqshare/auth.py +466 -0
- xqshare/client.py +682 -0
- xqshare/server.py +868 -0
- xqshare/tools/__init__.py +7 -0
- xqshare/tools/common.py +457 -0
- xqshare/tools/xtdata.py +98 -0
- xqshare/tools/xttrader.py +122 -0
- xtquant_share-1.1.2.dist-info/METADATA +756 -0
- xtquant_share-1.1.2.dist-info/RECORD +14 -0
- xtquant_share-1.1.2.dist-info/WHEEL +5 -0
- xtquant_share-1.1.2.dist-info/entry_points.txt +4 -0
- xtquant_share-1.1.2.dist-info/licenses/LICENSE +21 -0
- xtquant_share-1.1.2.dist-info/top_level.txt +1 -0
xqshare/client.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XtQuant Share (xqshare) Client - Transparent remote proxy for xtquant
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import rpyc
|
|
7
|
+
import time
|
|
8
|
+
import threading
|
|
9
|
+
import ssl
|
|
10
|
+
import logging
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any, Callable, Dict, List
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
# 默认客户端配置(与服务端保持一致)
|
|
16
|
+
DEFAULT_CLIENT_ID = "client-standard"
|
|
17
|
+
DEFAULT_CLIENT_SECRET = "xqshare-default-secret"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ==================== 日志配置 ====================
|
|
21
|
+
|
|
22
|
+
def setup_logging(log_level: str = "INFO", quiet: bool = False):
|
|
23
|
+
"""配置客户端日志
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
log_level: 日志级别
|
|
27
|
+
quiet: 是否静默模式(不输出控制台日志)
|
|
28
|
+
"""
|
|
29
|
+
# 日志目录:优先使用环境变量,默认为工作目录的 logs
|
|
30
|
+
log_dir = os.environ.get("XQSHARE_LOG_DIR", "logs")
|
|
31
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
formatter = logging.Formatter(
|
|
34
|
+
fmt='%(asctime)s.%(msecs)03d | %(levelname)-8s | %(message)s',
|
|
35
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
root_logger = logging.getLogger('xtquant_client')
|
|
39
|
+
root_logger.setLevel(getattr(logging, log_level.upper()))
|
|
40
|
+
|
|
41
|
+
# 静默模式下不添加控制台 handler
|
|
42
|
+
if not quiet:
|
|
43
|
+
console_handler = logging.StreamHandler()
|
|
44
|
+
console_handler.setFormatter(formatter)
|
|
45
|
+
console_handler.setLevel(logging.INFO)
|
|
46
|
+
root_logger.addHandler(console_handler)
|
|
47
|
+
|
|
48
|
+
file_handler = logging.FileHandler(
|
|
49
|
+
os.path.join(log_dir, f"client_{datetime.now().strftime('%Y%m%d')}.log"),
|
|
50
|
+
encoding='utf-8'
|
|
51
|
+
)
|
|
52
|
+
file_handler.setFormatter(formatter)
|
|
53
|
+
file_handler.setLevel(logging.DEBUG)
|
|
54
|
+
root_logger.addHandler(file_handler)
|
|
55
|
+
|
|
56
|
+
return root_logger
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_logger = None
|
|
60
|
+
_quiet_mode = False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def set_quiet_mode(quiet: bool = True):
|
|
64
|
+
"""设置静默模式"""
|
|
65
|
+
global _quiet_mode
|
|
66
|
+
_quiet_mode = quiet
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_logger():
|
|
70
|
+
global _logger
|
|
71
|
+
if _logger is None:
|
|
72
|
+
_logger = setup_logging(quiet=_quiet_mode)
|
|
73
|
+
return _logger
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ==================== 反序列化传输数据 ====================
|
|
77
|
+
|
|
78
|
+
SERIALIZED_MARKER = "__xqshare_serialized__"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _deserialize_from_transfer(result):
|
|
82
|
+
"""反序列化服务端优化传输的数据
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
result: 服务端返回的数据
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
反序列化后的 Python 对象
|
|
89
|
+
"""
|
|
90
|
+
# 检查是否为序列化数据
|
|
91
|
+
if not isinstance(result, dict) or SERIALIZED_MARKER not in result:
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
serialized_type = result[SERIALIZED_MARKER]
|
|
95
|
+
data = result["data"]
|
|
96
|
+
|
|
97
|
+
if serialized_type == "none":
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
if serialized_type == "json":
|
|
101
|
+
return json.loads(data)
|
|
102
|
+
|
|
103
|
+
if serialized_type == "dataframe_csv":
|
|
104
|
+
import io
|
|
105
|
+
try:
|
|
106
|
+
import pandas as pd
|
|
107
|
+
return pd.read_csv(io.StringIO(data), index_col=0)
|
|
108
|
+
except ImportError:
|
|
109
|
+
# 无 pandas 时返回原始 CSV 字符串
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
if serialized_type == "dict_with_dataframe":
|
|
113
|
+
import io
|
|
114
|
+
try:
|
|
115
|
+
import pandas as pd
|
|
116
|
+
|
|
117
|
+
def deserialize_dataframes(obj):
|
|
118
|
+
"""递归反序列化 DataFrame"""
|
|
119
|
+
if isinstance(obj, dict):
|
|
120
|
+
if obj.get("__df__"):
|
|
121
|
+
return pd.read_csv(io.StringIO(obj["csv"]), index_col=0)
|
|
122
|
+
return {k: deserialize_dataframes(v) for k, v in obj.items()}
|
|
123
|
+
if isinstance(obj, list):
|
|
124
|
+
return [deserialize_dataframes(item) for item in obj]
|
|
125
|
+
return obj
|
|
126
|
+
|
|
127
|
+
deserialized = json.loads(data)
|
|
128
|
+
return deserialize_dataframes(deserialized)
|
|
129
|
+
except ImportError:
|
|
130
|
+
# 无 pandas 时返回原始 JSON
|
|
131
|
+
return json.loads(data)
|
|
132
|
+
|
|
133
|
+
# 未知类型,返回原始数据
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ==================== 异常定义 ====================
|
|
138
|
+
|
|
139
|
+
class ConnectionError(Exception):
|
|
140
|
+
"""连接错误"""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class AuthenticationError(Exception):
|
|
145
|
+
"""认证错误"""
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class CallbackError(Exception):
|
|
150
|
+
"""回调错误"""
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ==================== 重连策略 ====================
|
|
155
|
+
|
|
156
|
+
class ReconnectPolicy:
|
|
157
|
+
"""重连策略"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, max_retries=5, base_delay=1, max_delay=30, backoff_factor=2):
|
|
160
|
+
self.max_retries = max_retries
|
|
161
|
+
self.base_delay = base_delay
|
|
162
|
+
self.max_delay = max_delay
|
|
163
|
+
self.backoff_factor = backoff_factor
|
|
164
|
+
|
|
165
|
+
def get_delay(self, retry_count):
|
|
166
|
+
delay = self.base_delay * (self.backoff_factor ** retry_count)
|
|
167
|
+
return min(delay, self.max_delay)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ==================== 后台服务线程 ====================
|
|
171
|
+
|
|
172
|
+
from rpyc.utils.helpers import BgServingThread
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ==================== 远程模块代理 ====================
|
|
176
|
+
|
|
177
|
+
class RemoteModule:
|
|
178
|
+
"""远程模块代理 - 完全透明的动态代理"""
|
|
179
|
+
|
|
180
|
+
def __init__(self, client, module_name, module=None):
|
|
181
|
+
self._client = client
|
|
182
|
+
self._module_name = module_name
|
|
183
|
+
self._module = module # 支持直接传入对象
|
|
184
|
+
self._logger = get_logger()
|
|
185
|
+
|
|
186
|
+
def _ensure_module(self):
|
|
187
|
+
if self._module is None:
|
|
188
|
+
self._client._ensure_connected()
|
|
189
|
+
try:
|
|
190
|
+
method = getattr(self._client._conn.root, f'get_{self._module_name}')
|
|
191
|
+
self._module = method()
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self._module = None
|
|
194
|
+
raise
|
|
195
|
+
return self._module
|
|
196
|
+
|
|
197
|
+
def __getattr__(self, name):
|
|
198
|
+
module = self._ensure_module()
|
|
199
|
+
try:
|
|
200
|
+
attr = getattr(module, name)
|
|
201
|
+
if callable(attr):
|
|
202
|
+
return self._wrap_call(attr, name)
|
|
203
|
+
return attr
|
|
204
|
+
except Exception as e:
|
|
205
|
+
if self._client._should_reconnect(e):
|
|
206
|
+
self._module = None
|
|
207
|
+
module = self._ensure_module()
|
|
208
|
+
attr = getattr(module, name)
|
|
209
|
+
if callable(attr):
|
|
210
|
+
return self._wrap_call(attr, name)
|
|
211
|
+
return attr
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
def _wrap_call(self, func, func_name: str):
|
|
215
|
+
def wrapper(*args, **kwargs):
|
|
216
|
+
start_time = time.perf_counter()
|
|
217
|
+
args_str = self._summarize_args(args, kwargs)
|
|
218
|
+
self._logger.info(f"[CALL] {self._module_name}.{func_name}({args_str})")
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
result = func(*args, **kwargs)
|
|
222
|
+
# 反序列化服务端优化传输的数据
|
|
223
|
+
result = _deserialize_from_transfer(result)
|
|
224
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
225
|
+
result_summary = self._summarize_result(result)
|
|
226
|
+
self._logger.info(f"[OK] {self._module_name}.{func_name} | {elapsed_ms:.2f}ms | {result_summary}")
|
|
227
|
+
return result
|
|
228
|
+
except Exception as e:
|
|
229
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
230
|
+
self._logger.error(f"[ERROR] {self._module_name}.{func_name} | {elapsed_ms:.2f}ms | {type(e).__name__}: {e}")
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
# 手动设置属性,避免 Python 3.13 functools.wraps 的 __annotations__ 兼容性问题
|
|
234
|
+
wrapper.__name__ = func_name
|
|
235
|
+
wrapper.__qualname__ = f"{self._module_name}.{func_name}"
|
|
236
|
+
return wrapper
|
|
237
|
+
|
|
238
|
+
def _summarize_args(self, args, kwargs, max_len: int = 100) -> str:
|
|
239
|
+
parts = []
|
|
240
|
+
if args:
|
|
241
|
+
for arg in args[:3]:
|
|
242
|
+
try:
|
|
243
|
+
s = str(arg)[:30]
|
|
244
|
+
parts.append(s)
|
|
245
|
+
except:
|
|
246
|
+
parts.append("?")
|
|
247
|
+
if len(args) > 3:
|
|
248
|
+
parts.append(f"...+{len(args)-3}")
|
|
249
|
+
if kwargs:
|
|
250
|
+
for k, v in list(kwargs.items())[:2]:
|
|
251
|
+
try:
|
|
252
|
+
s = f"{k}={str(v)[:20]}"
|
|
253
|
+
parts.append(s)
|
|
254
|
+
except:
|
|
255
|
+
parts.append(f"{k}=?")
|
|
256
|
+
if len(kwargs) > 2:
|
|
257
|
+
parts.append(f"...+{len(kwargs)-2}")
|
|
258
|
+
return ", ".join(parts)[:max_len]
|
|
259
|
+
|
|
260
|
+
def _summarize_result(self, result, max_len: int = 100) -> str:
|
|
261
|
+
try:
|
|
262
|
+
if result is None:
|
|
263
|
+
return "None"
|
|
264
|
+
elif isinstance(result, (int, float, bool)):
|
|
265
|
+
return str(result)
|
|
266
|
+
elif isinstance(result, str):
|
|
267
|
+
return result[:max_len] if len(result) > max_len else result
|
|
268
|
+
elif isinstance(result, (list, tuple)):
|
|
269
|
+
return f"{type(result).__name__}[len={len(result)}]"
|
|
270
|
+
elif isinstance(result, dict):
|
|
271
|
+
return f"dict[{len(result)} keys]"
|
|
272
|
+
else:
|
|
273
|
+
return f"<{type(result).__name__}>"
|
|
274
|
+
except:
|
|
275
|
+
return "?"
|
|
276
|
+
|
|
277
|
+
def __dir__(self):
|
|
278
|
+
module = self._ensure_module()
|
|
279
|
+
return dir(module)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ==================== 主客户端类 ====================
|
|
283
|
+
|
|
284
|
+
class XtQuantRemote:
|
|
285
|
+
"""
|
|
286
|
+
远程 xtquant 完全透明代理
|
|
287
|
+
|
|
288
|
+
功能:
|
|
289
|
+
- 自动认证
|
|
290
|
+
- 断线自动重连
|
|
291
|
+
- SSL 加密(可选)
|
|
292
|
+
- 异步回调支持
|
|
293
|
+
- API调用日志
|
|
294
|
+
|
|
295
|
+
使用示例:
|
|
296
|
+
xt = XtQuantRemote("192.168.1.100")
|
|
297
|
+
stocks = xt.xtdata.get_stock_list_in_sector("沪深A股")
|
|
298
|
+
xt.close()
|
|
299
|
+
|
|
300
|
+
with XtQuantRemote("192.168.1.100") as xt:
|
|
301
|
+
df = xt.xtdata.get_market_data(["000001.SZ"])
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
host=None,
|
|
307
|
+
port=None,
|
|
308
|
+
client_id=None,
|
|
309
|
+
client_secret=None,
|
|
310
|
+
use_ssl=False,
|
|
311
|
+
ssl_verify=True,
|
|
312
|
+
auto_reconnect=True,
|
|
313
|
+
max_retries=5,
|
|
314
|
+
heartbeat_interval=30,
|
|
315
|
+
log_level="INFO",
|
|
316
|
+
env_file=None,
|
|
317
|
+
):
|
|
318
|
+
# 加载环境变量文件(None 时自动查找 .env)
|
|
319
|
+
try:
|
|
320
|
+
from dotenv import load_dotenv
|
|
321
|
+
load_dotenv(env_file)
|
|
322
|
+
except ImportError:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
# 支持环境变量:显式参数 > 环境变量 > 默认值
|
|
326
|
+
if host is None:
|
|
327
|
+
host = os.environ.get("XQSHARE_REMOTE_HOST", "localhost")
|
|
328
|
+
if port is None:
|
|
329
|
+
port = int(os.environ.get("XQSHARE_REMOTE_PORT", "18812"))
|
|
330
|
+
if client_id is None:
|
|
331
|
+
client_id = os.environ.get("XQSHARE_CLIENT_ID", DEFAULT_CLIENT_ID)
|
|
332
|
+
if client_secret is None:
|
|
333
|
+
client_secret = os.environ.get("XQSHARE_CLIENT_SECRET", DEFAULT_CLIENT_SECRET)
|
|
334
|
+
|
|
335
|
+
self._host = host
|
|
336
|
+
self._port = port
|
|
337
|
+
self._client_id = client_id
|
|
338
|
+
self._client_secret = client_secret
|
|
339
|
+
self._use_ssl = use_ssl
|
|
340
|
+
self._ssl_verify = ssl_verify
|
|
341
|
+
self._auto_reconnect = auto_reconnect
|
|
342
|
+
self._reconnect_policy = ReconnectPolicy(max_retries=max_retries)
|
|
343
|
+
self._heartbeat_interval = heartbeat_interval
|
|
344
|
+
self._log_level = log_level
|
|
345
|
+
|
|
346
|
+
self._conn = None
|
|
347
|
+
self._authenticated = False
|
|
348
|
+
self._connected = False
|
|
349
|
+
self._reconnecting = False
|
|
350
|
+
self._heartbeat_thread = None
|
|
351
|
+
self._stop_heartbeat = threading.Event()
|
|
352
|
+
self._bg_thread = None # BgServingThread for async callbacks
|
|
353
|
+
self._account_level = None # 账号等级
|
|
354
|
+
self._subscriptions = [] # 订阅列表,重连时恢复用
|
|
355
|
+
|
|
356
|
+
self._xtdata = RemoteModule(self, 'xtdata')
|
|
357
|
+
self._xttype = RemoteModule(self, 'xttype')
|
|
358
|
+
self._xtconstant = RemoteModule(self, 'xtconstant')
|
|
359
|
+
self._xtview = RemoteModule(self, 'xtview')
|
|
360
|
+
self._datadir = RemoteModule(self, 'datadir')
|
|
361
|
+
self._logger = get_logger()
|
|
362
|
+
|
|
363
|
+
self._connect()
|
|
364
|
+
|
|
365
|
+
def _should_reconnect(self, error):
|
|
366
|
+
if not self._auto_reconnect:
|
|
367
|
+
return False
|
|
368
|
+
error_str = str(error).lower()
|
|
369
|
+
hints = ['connection', 'closed', 'reset', 'broken', 'timeout', 'refused', 'eof', 'socket']
|
|
370
|
+
return any(h in error_str for h in hints)
|
|
371
|
+
|
|
372
|
+
def _create_ssl_context(self):
|
|
373
|
+
if not self._use_ssl:
|
|
374
|
+
return None
|
|
375
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
376
|
+
if self._ssl_verify:
|
|
377
|
+
ctx.verify_mode = ssl.CERT_REQUIRED
|
|
378
|
+
ctx.check_hostname = True
|
|
379
|
+
else:
|
|
380
|
+
ctx.check_hostname = False
|
|
381
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
382
|
+
return ctx
|
|
383
|
+
|
|
384
|
+
def _connect(self):
|
|
385
|
+
config = {
|
|
386
|
+
'allow_public_attrs': True,
|
|
387
|
+
'allow_pickle': True,
|
|
388
|
+
'allow_getattr': True,
|
|
389
|
+
'allow_setattr': True,
|
|
390
|
+
'allow_delattr': True,
|
|
391
|
+
'allow_all_attrs': True,
|
|
392
|
+
'sync_request_timeout': 300,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
ssl_context = self._create_ssl_context()
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
# 尝试新版本 rpyc API
|
|
399
|
+
try:
|
|
400
|
+
self._conn = rpyc.connect(self._host, self._port, config=config, ssl_context=ssl_context)
|
|
401
|
+
except TypeError:
|
|
402
|
+
# 旧版本 rpyc 不支持 ssl_context 参数
|
|
403
|
+
if ssl_context and self._use_ssl:
|
|
404
|
+
# 使用 SSL 包装 socket
|
|
405
|
+
import socket
|
|
406
|
+
sock = socket.create_connection((self._host, self._port))
|
|
407
|
+
sock = ssl_context.wrap_socket(sock, server_hostname=self._host)
|
|
408
|
+
self._conn = rpyc.connect_stream(sock, config=config)
|
|
409
|
+
else:
|
|
410
|
+
self._conn = rpyc.connect(self._host, self._port, config=config)
|
|
411
|
+
|
|
412
|
+
self._connected = True
|
|
413
|
+
|
|
414
|
+
# 启动后台服务线程处理异步回调
|
|
415
|
+
self._bg_thread = BgServingThread(self._conn)
|
|
416
|
+
self._logger.debug("后台服务线程已启动")
|
|
417
|
+
|
|
418
|
+
if self._client_secret:
|
|
419
|
+
result = self._conn.root.authenticate(self._client_id, self._client_secret)
|
|
420
|
+
# 处理认证响应(支持新格式)
|
|
421
|
+
if isinstance(result, dict):
|
|
422
|
+
self._account_level = result.get("level", "free")
|
|
423
|
+
self._logger.info(f"认证成功: client_id={self._client_id} | level={self._account_level}")
|
|
424
|
+
else:
|
|
425
|
+
self._logger.info(f"认证成功: client_id={self._client_id}")
|
|
426
|
+
|
|
427
|
+
if self._heartbeat_interval > 0:
|
|
428
|
+
self._start_heartbeat()
|
|
429
|
+
|
|
430
|
+
self._logger.info(f"连接成功: {self._host}:{self._port}")
|
|
431
|
+
except Exception as e:
|
|
432
|
+
self._connected = False
|
|
433
|
+
raise ConnectionError(f"连接失败: {e}")
|
|
434
|
+
|
|
435
|
+
def _ensure_connected(self):
|
|
436
|
+
if self._connected and self._conn:
|
|
437
|
+
return
|
|
438
|
+
if not self._auto_reconnect:
|
|
439
|
+
raise ConnectionError("连接已断开,自动重连已禁用")
|
|
440
|
+
self._reconnect()
|
|
441
|
+
|
|
442
|
+
def _reconnect(self):
|
|
443
|
+
if self._reconnecting:
|
|
444
|
+
for _ in range(10):
|
|
445
|
+
time.sleep(0.5)
|
|
446
|
+
if self._connected:
|
|
447
|
+
return
|
|
448
|
+
raise ConnectionError("重连超时")
|
|
449
|
+
|
|
450
|
+
self._reconnecting = True
|
|
451
|
+
retry_count = 0
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
while retry_count < self._reconnect_policy.max_retries:
|
|
455
|
+
try:
|
|
456
|
+
self._logger.info(f"重连中... 第 {retry_count + 1} 次尝试")
|
|
457
|
+
|
|
458
|
+
if self._conn:
|
|
459
|
+
try:
|
|
460
|
+
self._conn.close()
|
|
461
|
+
except:
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
self._conn = None
|
|
465
|
+
self._connected = False
|
|
466
|
+
self._token = None
|
|
467
|
+
self._connect()
|
|
468
|
+
|
|
469
|
+
# 重连后重置所有 RemoteModule 的内部缓存,下次调用时自动重新获取远程对象
|
|
470
|
+
self._xtdata._module = None
|
|
471
|
+
self._xttype._module = None
|
|
472
|
+
self._xtconstant._module = None
|
|
473
|
+
self._xtview._module = None
|
|
474
|
+
self._datadir._module = None
|
|
475
|
+
|
|
476
|
+
for sub in self._subscriptions:
|
|
477
|
+
if sub._active:
|
|
478
|
+
try:
|
|
479
|
+
sub.start()
|
|
480
|
+
except:
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
self._logger.info("重连成功")
|
|
484
|
+
return
|
|
485
|
+
except Exception as e:
|
|
486
|
+
retry_count += 1
|
|
487
|
+
delay = self._reconnect_policy.get_delay(retry_count - 1)
|
|
488
|
+
self._logger.warning(f"重连失败: {e},{delay}秒后重试...")
|
|
489
|
+
time.sleep(delay)
|
|
490
|
+
|
|
491
|
+
raise ConnectionError(f"重连失败,已尝试 {retry_count} 次")
|
|
492
|
+
finally:
|
|
493
|
+
self._reconnecting = False
|
|
494
|
+
|
|
495
|
+
def _start_heartbeat(self):
|
|
496
|
+
if self._heartbeat_thread and self._heartbeat_thread.is_alive():
|
|
497
|
+
return
|
|
498
|
+
self._stop_heartbeat.clear()
|
|
499
|
+
self._heartbeat_thread = threading.Thread(target=self._heartbeat_loop, daemon=True)
|
|
500
|
+
self._heartbeat_thread.start()
|
|
501
|
+
|
|
502
|
+
def _heartbeat_loop(self):
|
|
503
|
+
while not self._stop_heartbeat.is_set():
|
|
504
|
+
try:
|
|
505
|
+
if self._connected and self._conn:
|
|
506
|
+
try:
|
|
507
|
+
self._conn.root.heartbeat()
|
|
508
|
+
except Exception as e:
|
|
509
|
+
if self._auto_reconnect:
|
|
510
|
+
self._logger.warning(f"心跳失败: {e},尝试重连...")
|
|
511
|
+
try:
|
|
512
|
+
self._reconnect()
|
|
513
|
+
except:
|
|
514
|
+
pass
|
|
515
|
+
except Exception:
|
|
516
|
+
pass
|
|
517
|
+
self._stop_heartbeat.wait(self._heartbeat_interval)
|
|
518
|
+
|
|
519
|
+
def _stop_heartbeat_thread(self):
|
|
520
|
+
self._stop_heartbeat.set()
|
|
521
|
+
if self._heartbeat_thread:
|
|
522
|
+
self._heartbeat_thread.join(timeout=2)
|
|
523
|
+
|
|
524
|
+
# ==================== 公共接口 ====================
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def xtdata(self):
|
|
528
|
+
return self._xtdata
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def xttype(self):
|
|
532
|
+
return self._xttype
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def xtconstant(self):
|
|
536
|
+
return self._xtconstant
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def xtview(self):
|
|
540
|
+
return self._xtview
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def datadir(self):
|
|
544
|
+
"""QMT datadir 文件解析能力代理。
|
|
545
|
+
|
|
546
|
+
通过 RPyC 远程调用 Windows 端的 QmtDataReader,
|
|
547
|
+
接口风格与 xtdata 完全一致。
|
|
548
|
+
|
|
549
|
+
Raises:
|
|
550
|
+
RuntimeError: 当 server 端未配置 QMT_DATADIR_PATH 时抛出
|
|
551
|
+
"""
|
|
552
|
+
return self._datadir
|
|
553
|
+
|
|
554
|
+
def create_trader(self, userdata_path: str = None, session_id: int = None):
|
|
555
|
+
"""
|
|
556
|
+
创建交易实例
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
userdata_path: QMT 客户端 userdata_mini 目录路径
|
|
560
|
+
(可选,默认从环境变量 QMT_USERDATA_PATH 读取)
|
|
561
|
+
session_id: 会话ID(可选,默认自动生成时间戳)
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
XtQuantTrader 实例
|
|
565
|
+
|
|
566
|
+
Example:
|
|
567
|
+
# 方式1:使用环境变量
|
|
568
|
+
export QMT_USERDATA_PATH="C:\\QMT\\userdata_mini"
|
|
569
|
+
trader = xt.create_trader()
|
|
570
|
+
|
|
571
|
+
# 方式2:直接传参
|
|
572
|
+
trader = xt.create_trader("C:\\QMT\\userdata_mini")
|
|
573
|
+
"""
|
|
574
|
+
self._ensure_connected()
|
|
575
|
+
trader = self._conn.root.create_trader(userdata_path, session_id)
|
|
576
|
+
# 用 RemoteModule 包装,添加日志和反序列化支持
|
|
577
|
+
return RemoteModule(self, 'xttrader', trader)
|
|
578
|
+
|
|
579
|
+
def get_all_stocks(self):
|
|
580
|
+
self._ensure_connected()
|
|
581
|
+
return self._conn.root.get_all_stocks()
|
|
582
|
+
|
|
583
|
+
def get_index_list(self):
|
|
584
|
+
self._ensure_connected()
|
|
585
|
+
return self._conn.root.get_index_list()
|
|
586
|
+
|
|
587
|
+
def download_history_data2(self, stock_list: list, period: str = "1d",
|
|
588
|
+
start_time: str = "", end_time: str = "", incrementally: bool = None):
|
|
589
|
+
"""
|
|
590
|
+
下载历史数据(服务端封装版本,返回完整状态)
|
|
591
|
+
|
|
592
|
+
返回: {'finished': n, 'total': n, 'done': bool, 'message': str, 'result': {}}
|
|
593
|
+
"""
|
|
594
|
+
self._ensure_connected()
|
|
595
|
+
return self._conn.root.download_history_data2(stock_list, period, start_time, end_time, incrementally)
|
|
596
|
+
|
|
597
|
+
def is_connected(self):
|
|
598
|
+
return self._connected
|
|
599
|
+
|
|
600
|
+
def get_service_status(self):
|
|
601
|
+
self._ensure_connected()
|
|
602
|
+
return self._conn.root.get_service_status()
|
|
603
|
+
|
|
604
|
+
def reconnect(self):
|
|
605
|
+
self._reconnect()
|
|
606
|
+
|
|
607
|
+
def close(self):
|
|
608
|
+
self._stop_heartbeat_thread()
|
|
609
|
+
if self._bg_thread:
|
|
610
|
+
self._bg_thread.stop()
|
|
611
|
+
self._bg_thread = None
|
|
612
|
+
self._connected = False
|
|
613
|
+
if self._conn:
|
|
614
|
+
try:
|
|
615
|
+
self._conn.close()
|
|
616
|
+
except:
|
|
617
|
+
pass
|
|
618
|
+
self._conn = None
|
|
619
|
+
self._logger.info("连接已关闭")
|
|
620
|
+
|
|
621
|
+
def __enter__(self):
|
|
622
|
+
return self
|
|
623
|
+
|
|
624
|
+
def __exit__(self, *args):
|
|
625
|
+
self.close()
|
|
626
|
+
|
|
627
|
+
def __repr__(self):
|
|
628
|
+
status = "已连接" if self._connected else "已断开"
|
|
629
|
+
ssl_status = "SSL" if self._use_ssl else "明文"
|
|
630
|
+
return f"<XtQuantRemote {self._host}:{self._port} [{status}] [{ssl_status}]>"
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
# ==================== 全局便捷函数 ====================
|
|
634
|
+
|
|
635
|
+
_global_client = None
|
|
636
|
+
|
|
637
|
+
def connect(host=None, port=None, **kwargs):
|
|
638
|
+
"""创建全局连接
|
|
639
|
+
|
|
640
|
+
支持环境变量配置:
|
|
641
|
+
- XQSHARE_REMOTE_HOST: 服务端地址
|
|
642
|
+
- XQSHARE_REMOTE_PORT: 服务端端口
|
|
643
|
+
- XQSHARE_CLIENT_ID: 客户端标识
|
|
644
|
+
- XQSHARE_CLIENT_SECRET: 客户端密钥
|
|
645
|
+
|
|
646
|
+
优先级:显式参数 > 环境变量 > 默认值
|
|
647
|
+
"""
|
|
648
|
+
global _global_client
|
|
649
|
+
if host is None:
|
|
650
|
+
host = os.environ.get("XQSHARE_REMOTE_HOST", "localhost")
|
|
651
|
+
if port is None:
|
|
652
|
+
port = int(os.environ.get("XQSHARE_REMOTE_PORT", "18812"))
|
|
653
|
+
_global_client = XtQuantRemote(host, port, **kwargs)
|
|
654
|
+
return _global_client
|
|
655
|
+
|
|
656
|
+
def disconnect():
|
|
657
|
+
"""断开全局连接"""
|
|
658
|
+
global _global_client
|
|
659
|
+
if _global_client:
|
|
660
|
+
_global_client.close()
|
|
661
|
+
_global_client = None
|
|
662
|
+
|
|
663
|
+
def get_client():
|
|
664
|
+
"""获取全局客户端"""
|
|
665
|
+
return _global_client
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class _ModuleProxy:
|
|
669
|
+
def __init__(self, name):
|
|
670
|
+
self._name = name
|
|
671
|
+
|
|
672
|
+
def __getattr__(self, attr):
|
|
673
|
+
if _global_client is None:
|
|
674
|
+
raise RuntimeError("请先调用 connect() 建立连接")
|
|
675
|
+
return getattr(getattr(_global_client, self._name), attr)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
xtdata = _ModuleProxy('xtdata')
|
|
679
|
+
xttrader = _ModuleProxy('xttrader')
|
|
680
|
+
xttype = _ModuleProxy('xttype')
|
|
681
|
+
xtview = _ModuleProxy('xtview')
|
|
682
|
+
datadir = _ModuleProxy('datadir')
|