xtquant-share 1.3.0__tar.gz → 1.3.1__tar.gz

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 (51) hide show
  1. {xtquant_share-1.3.0/xtquant_share.egg-info → xtquant_share-1.3.1}/PKG-INFO +1 -1
  2. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/pyproject.toml +1 -1
  3. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_quote_events.py +82 -3
  4. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_server.py +43 -0
  5. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/__init__.py +1 -1
  6. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/server.py +146 -65
  7. {xtquant_share-1.3.0 → xtquant_share-1.3.1/xtquant_share.egg-info}/PKG-INFO +1 -1
  8. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/LICENSE +0 -0
  9. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/README.md +0 -0
  10. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/setup.cfg +0 -0
  11. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_auth.py +0 -0
  12. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_channels_bigqmt.py +0 -0
  13. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_channels_health.py +0 -0
  14. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_channels_router.py +0 -0
  15. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_client.py +0 -0
  16. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_client_permission.py +0 -0
  17. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_integration.py +0 -0
  18. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_qmt_datadir_factor.py +0 -0
  19. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_remote_data.py +0 -0
  20. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_server_channel_routing.py +0 -0
  21. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_server_download_history_data2_compat.py +0 -0
  22. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_server_download_timeout.py +0 -0
  23. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_server_permission.py +0 -0
  24. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_tools_common.py +0 -0
  25. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/tests/test_trader_events.py +0 -0
  26. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/.env.example +0 -0
  27. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/auth.py +0 -0
  28. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/channels/__init__.py +0 -0
  29. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/channels/bigqmt.py +0 -0
  30. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/channels/health.py +0 -0
  31. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/channels/router.py +0 -0
  32. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/check_env.py +0 -0
  33. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/client.py +0 -0
  34. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/clients.yaml.example +0 -0
  35. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/qmt_datadir/__init__.py +0 -0
  36. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/qmt_datadir/divid.py +0 -0
  37. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/qmt_datadir/factor.py +0 -0
  38. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/qmt_datadir/kline.py +0 -0
  39. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/qmt_datadir/misc.py +0 -0
  40. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/qmt_datadir/reader.py +0 -0
  41. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/qmt_datadir/sector.py +0 -0
  42. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/qmt_watchdog.py +0 -0
  43. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/tools/__init__.py +0 -0
  44. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/tools/common.py +0 -0
  45. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/tools/xtdata.py +0 -0
  46. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xqshare/tools/xttrader.py +0 -0
  47. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xtquant_share.egg-info/SOURCES.txt +0 -0
  48. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xtquant_share.egg-info/dependency_links.txt +0 -0
  49. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xtquant_share.egg-info/entry_points.txt +0 -0
  50. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xtquant_share.egg-info/requires.txt +0 -0
  51. {xtquant_share-1.3.0 → xtquant_share-1.3.1}/xtquant_share.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xtquant-share
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: Xtreme Quant Share - eXtreme transparent sharing proxy for XtQuant on macOS/Linux
5
5
  Author-email: Jason Hu <63170682@qq.com>
6
6
  License-Expression: GPL-3.0-only
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xtquant-share"
7
- version = "1.3.0"
7
+ version = "1.3.1"
8
8
  description = "Xtreme Quant Share - eXtreme transparent sharing proxy for XtQuant on macOS/Linux"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-only"
@@ -30,7 +30,7 @@ sys.modules['xtquant.xttype'] = mock_xttype
30
30
  from xqshare.server import _init_logging
31
31
  _init_logging("WARNING")
32
32
 
33
- from xqshare.server import LoggingProxy, _QuoteEventCallback
33
+ from xqshare.server import LoggingProxy, XtQuantService, _QuoteEventCallback
34
34
  from xqshare.client import RemoteQuoteCallback
35
35
  from xqshare.auth import AccountLevel
36
36
 
@@ -118,6 +118,83 @@ class TestQuoteEventCallback:
118
118
  finally:
119
119
  qec.stop()
120
120
 
121
+ def test_register_idempotent_per_client(self):
122
+ """同一 client_info 重复 register(断线重连后新 netref)应覆盖旧 entry 而非累积"""
123
+ qec = _QuoteEventCallback()
124
+ try:
125
+ old_cb = MagicMock()
126
+ new_cb = MagicMock()
127
+ qec.register(old_cb, client_info="c1@127.0.0.1:1")
128
+ qec.register(new_cb, client_info="c1@127.0.0.1:1")
129
+
130
+ assert len(qec._subscribers) == 1, "重连后旧 netref 应被覆盖,不累积"
131
+ entry = next(iter(qec._subscribers.values()))
132
+ assert entry['callback'] is new_cb
133
+ finally:
134
+ qec.stop()
135
+
136
+ def test_register_without_client_info_no_dedup(self):
137
+ """client_info 为 None 时不做幂等去重(保持原语义,按 id 累积)"""
138
+ qec = _QuoteEventCallback()
139
+ try:
140
+ qec.register(MagicMock())
141
+ qec.register(MagicMock())
142
+ assert len(qec._subscribers) == 2
143
+ finally:
144
+ qec.stop()
145
+
146
+ def test_clear_client_callbacks(self):
147
+ """clear_client_callbacks 只清理指定 client 的回调,不影响其他 client"""
148
+ qec = _QuoteEventCallback()
149
+ try:
150
+ cb1, cb2, cb3 = MagicMock(), MagicMock(), MagicMock()
151
+ qec.register(cb1, client_info="c1@127.0.0.1:1")
152
+ qec.register(cb2, client_info="c1@127.0.0.1:1")
153
+ qec.register(cb3, client_info="c2@127.0.0.1:2")
154
+
155
+ qec.clear_client_callbacks("c1@127.0.0.1:1")
156
+
157
+ remaining = [e['callback'] for e in qec._subscribers.values()]
158
+ assert remaining == [cb3], "只应保留其他 client 的回调"
159
+ finally:
160
+ qec.stop()
161
+
162
+ def test_on_disconnect_clears_quote_subscribers(self):
163
+ """on_disconnect 应清理该连接注册的 quote 回调,避免 netref 累积泄漏"""
164
+ qec = _QuoteEventCallback()
165
+ original = XtQuantService._quote_event_callback
166
+ XtQuantService._quote_event_callback = qec
167
+ try:
168
+ service = XtQuantService()
169
+ service._client_info = "c1@127.0.0.1:1"
170
+ service._traders = []
171
+ qec.register(MagicMock(), client_info="c1@127.0.0.1:1")
172
+ qec.register(MagicMock(), client_info="c2@127.0.0.1:2")
173
+
174
+ service.on_disconnect(MagicMock())
175
+
176
+ remaining = [e['client_info'] for e in qec._subscribers.values()]
177
+ assert remaining == ["c2@127.0.0.1:2"], "断开连接的客户端回调应被清理"
178
+ finally:
179
+ qec.stop()
180
+ XtQuantService._quote_event_callback = original
181
+
182
+ def test_on_disconnect_without_quote_callback(self):
183
+ """quote 单例未初始化时 on_disconnect 不应报错,也不应创建单例"""
184
+ original = XtQuantService._quote_event_callback
185
+ XtQuantService._quote_event_callback = None
186
+ try:
187
+ service = XtQuantService()
188
+ service._client_info = "c1@127.0.0.1:1"
189
+ service._traders = []
190
+
191
+ service.on_disconnect(MagicMock())
192
+
193
+ assert XtQuantService._quote_event_callback is None, \
194
+ "不应在断开时懒创建 quote 单例"
195
+ finally:
196
+ XtQuantService._quote_event_callback = original
197
+
121
198
 
122
199
  class TestLoggingProxyQuoteIntercept:
123
200
  """LoggingProxy 拦截 xtdata 订阅并接管 callback"""
@@ -143,8 +220,10 @@ class TestLoggingProxyQuoteIntercept:
143
220
 
144
221
  kwargs = mock.subscribe_whole_quote.call_args.kwargs
145
222
  assert kwargs["callback"] is qec, "原生 callback 应由服务端路由器接管"
146
- assert len(qec._subscribers) == 1 and next(iter(qec._subscribers.values())) is client_cb, \
147
- "客户端回调应注册到路由器"
223
+ assert len(qec._subscribers) == 1, "客户端回调应注册到路由器"
224
+ entry = next(iter(qec._subscribers.values()))
225
+ assert entry['callback'] is client_cb
226
+ assert entry['client_info'] == "test-client", "应记录 client_info 供断线清理"
148
227
  finally:
149
228
  qec.stop()
150
229
 
@@ -195,6 +195,49 @@ class TestXtQuantService:
195
195
  assert "test_token" not in service._tokens
196
196
 
197
197
 
198
+ class TestTraderCallbackCleanup:
199
+ """测试 trader 事件路由器线程的清理(防泄漏)"""
200
+
201
+ def test_on_disconnect_stops_trader_event_callback(self):
202
+ """on_disconnect 应停止该连接创建的事件路由器 dispatch 线程"""
203
+ from xqshare.server import _TraderEventCallback
204
+ service = XtQuantService()
205
+ service._conn = Mock()
206
+ service._conn.peer = "127.0.0.1:12345"
207
+ service._client_info = "test_client@127.0.0.1:12345"
208
+ trader = Mock()
209
+ cb = _TraderEventCallback()
210
+ service._traders = [(trader, cb)]
211
+ try:
212
+ service.on_disconnect(service._conn)
213
+
214
+ assert cb._running is False, "on_disconnect 应停止事件路由器线程"
215
+ trader.stop.assert_called_once()
216
+ finally:
217
+ cb.stop()
218
+
219
+ def test_trader_event_callback_stop_terminates_thread(self):
220
+ """_TraderEventCallback.stop() 应让 dispatch daemon 线程退出"""
221
+ from xqshare.server import _TraderEventCallback
222
+ cb = _TraderEventCallback()
223
+ thread = cb._event_thread
224
+ assert thread.is_alive(), "dispatch 线程应在创建后运行"
225
+
226
+ cb.stop()
227
+ thread.join(timeout=3.0)
228
+ assert not thread.is_alive(), "stop() 后 dispatch 线程应退出"
229
+
230
+ def test_trader_event_callback_stop_idempotent(self):
231
+ """stop() 多次调用应无副作用"""
232
+ from xqshare.server import _TraderEventCallback
233
+ cb = _TraderEventCallback()
234
+ thread = cb._event_thread
235
+ cb.stop()
236
+ cb.stop()
237
+ thread.join(timeout=3.0)
238
+ assert not thread.is_alive()
239
+
240
+
198
241
  class TestSSLContext:
199
242
  """测试 SSL 上下文创建"""
200
243
 
@@ -4,7 +4,7 @@ XtQuant Share (xqshare) - Transparent remote proxy for xtquant library
4
4
  Allows using xtquant on macOS/Linux by proxying calls to a Windows server.
5
5
  """
6
6
 
7
- __version__ = "1.3.0"
7
+ __version__ = "1.3.1"
8
8
  __author__ = "Jason Hu"
9
9
 
10
10
  from .client import (
@@ -563,12 +563,16 @@ class _QuoteEventCallback:
563
563
  # key 用 id(netref)(本地内存地址)而非 netref 本身:
564
564
  # set 的 add/discard 会对 netref 求 hash(触发 RPC),客户端断开后
565
565
  # 该 RPC 会抛 EOFError,导致 dispatch 线程崩溃。id() 是纯本地操作。
566
- self._subscribers = {} # id(client_callback_netref) -> client_callback_netref
566
+ # value 记录 client_info,供 on_disconnect 按连接清理(修复 netref 累积泄漏)。
567
+ self._subscribers = {} # id(client_callback_netref) -> {callback, client_info}
567
568
  self._running = True
568
569
  self._queue = []
569
570
  self._queue_lock = threading.Lock()
570
571
  self._event_thread = threading.Thread(target=self._dispatch_loop, daemon=True)
571
572
  self._event_thread.start()
573
+ # 订阅者数量告警水位:超过阈值打 warning,用于观察是否仍存在泄漏
574
+ self._max_subscribers_warning = _get_max_quote_subscribers()
575
+ self._warned = False
572
576
 
573
577
  def __call__(self, data):
574
578
  """使路由器可被 xtquant 作为函数式 callback 调用。
@@ -604,7 +608,7 @@ class _QuoteEventCallback:
604
608
  continue
605
609
  for payload in batch:
606
610
  with self._lock:
607
- callbacks = list(self._subscribers.values())
611
+ callbacks = [entry['callback'] for entry in self._subscribers.values()]
608
612
  for cb in callbacks:
609
613
  try:
610
614
  cb.exposed_on_quote_event(payload)
@@ -613,15 +617,58 @@ class _QuoteEventCallback:
613
617
  logger.warning(f"[QuoteEvent] 推送到客户端失败,自动移除: {e}")
614
618
  self.unregister(cb)
615
619
 
616
- def register(self, client_callback):
620
+ def register(self, client_callback, client_info: Optional[str] = None):
617
621
  with self._lock:
618
- self._subscribers[id(client_callback)] = client_callback
622
+ if client_info is not None:
623
+ # 幂等化:同一 client_info 重复 register(客户端断线重连后新 netref
624
+ # 的 id 不同)时,先移除该 client 的旧 entry,避免按 id(netref) 累积。
625
+ stale = [
626
+ cid for cid, entry in self._subscribers.items()
627
+ if entry['client_info'] == client_info
628
+ ]
629
+ for cid in stale:
630
+ self._subscribers.pop(cid, None)
631
+ self._subscribers[id(client_callback)] = {
632
+ 'callback': client_callback,
633
+ 'client_info': client_info,
634
+ }
635
+ self._maybe_warn_subscriber_count()
619
636
  logger.info(f"[QuoteEvent] 注册订阅者 total={len(self._subscribers)}")
620
637
 
621
638
  def unregister(self, client_callback):
622
639
  with self._lock:
623
640
  self._subscribers.pop(id(client_callback), None)
624
641
 
642
+ def clear_client_callbacks(self, client_info: Optional[str]):
643
+ """按连接清理该客户端注册的所有 quote 回调(on_disconnect 调用)。"""
644
+ if client_info is None:
645
+ return
646
+ with self._lock:
647
+ stale = [
648
+ cid for cid, entry in self._subscribers.items()
649
+ if entry['client_info'] == client_info
650
+ ]
651
+ for cid in stale:
652
+ self._subscribers.pop(cid, None)
653
+ if stale:
654
+ logger.info(
655
+ f"[QuoteEvent] 清理断开客户端的订阅者: client={client_info} "
656
+ f"removed={len(stale)} total={len(self._subscribers)}"
657
+ )
658
+
659
+ def _maybe_warn_subscriber_count(self):
660
+ """订阅者数量超过水位时打 warning(回落时复位,便于反复告警)。"""
661
+ n = len(self._subscribers)
662
+ if n > self._max_subscribers_warning:
663
+ if not self._warned:
664
+ self._warned = True
665
+ logger.warning(
666
+ f"[QuoteEvent] 订阅者数量异常: total={n} "
667
+ f"(阈值 {self._max_subscribers_warning}),可能存在回调泄漏"
668
+ )
669
+ elif self._warned:
670
+ self._warned = False
671
+
625
672
  def stop(self):
626
673
  self._running = False
627
674
 
@@ -655,6 +702,15 @@ def _get_download_timeout() -> int:
655
702
  DOWNLOAD_TIMEOUT_SECONDS = _get_download_timeout()
656
703
 
657
704
 
705
+ def _get_max_quote_subscribers() -> int:
706
+ """读取 quote 订阅者告警水位,默认 500,可用 env XQSHARE_MAX_QUOTE_SUBSCRIBERS 覆盖。"""
707
+ raw = os.environ.get("XQSHARE_MAX_QUOTE_SUBSCRIBERS", "500")
708
+ try:
709
+ return int(raw)
710
+ except (TypeError, ValueError):
711
+ return 500
712
+
713
+
658
714
  def _call_download_with_timeout(fn, *args, timeout=None, name=None, **kwargs):
659
715
  """在独立 daemon 线程执行下载,主线程带超时等待。
660
716
 
@@ -790,13 +846,13 @@ class LoggingProxy:
790
846
  pos = _QUOTE_SUBSCRIBE_CALLBACK_POS[name]
791
847
  client_callback = kwargs.pop('callback', None)
792
848
  if client_callback is not None:
793
- # 客户端传了接收器(netref),注册到路由器
794
- quote_event_callback.register(client_callback)
849
+ # 客户端传了接收器(netref),注册到路由器(带 client_info 供断线清理)
850
+ quote_event_callback.register(client_callback, get_client_info())
795
851
  kwargs['callback'] = quote_event_callback
796
852
  elif len(args) > pos:
797
853
  args = list(args)
798
854
  if args[pos] is not None:
799
- quote_event_callback.register(args[pos])
855
+ quote_event_callback.register(args[pos], get_client_info())
800
856
  args[pos] = quote_event_callback
801
857
  args = tuple(args)
802
858
  else:
@@ -915,9 +971,15 @@ class XtQuantService(rpyc.Service):
915
971
  def on_disconnect(self, conn):
916
972
  client_info = getattr(self, '_client_info', 'unknown')
917
973
  logger.info(f"[断开] 客户端离开: {client_info}")
918
- # 自动清理本次连接创建的所有 trader 实例,防止 session 资源泄漏
974
+ # 清理本连接注册的 quote 行情回调(根因:netref 随重连累积,只进不出)
975
+ qec = XtQuantService._quote_event_callback
976
+ if qec is not None:
977
+ qec.clear_client_callbacks(client_info)
978
+ # 自动清理本次连接创建的所有 trader 实例及其事件路由器,
979
+ # 含停止 _TraderEventCallback 的 dispatch daemon 线程(否则线程持
980
+ # 有回调对象与 netref,随断线累积泄漏)
919
981
  traders = getattr(self, '_traders', [])
920
- for trader in traders:
982
+ for trader, event_callback in traders:
921
983
  # disconnect() 在部分 xtquant 版本中不存在,仅作可选清理
922
984
  if hasattr(trader, 'disconnect'):
923
985
  try:
@@ -930,6 +992,12 @@ class XtQuantService(rpyc.Service):
930
992
  logger.info(f"[清理Trader] 已自动 stop trader | client={client_info}")
931
993
  except Exception as e:
932
994
  logger.warning(f"[清理Trader] stop trader 失败: {e} | client={client_info}")
995
+ if event_callback is not None and hasattr(event_callback, 'stop'):
996
+ try:
997
+ event_callback.stop()
998
+ logger.info(f"[清理Trader] 已停止事件路由器线程 | client={client_info}")
999
+ except Exception as e:
1000
+ logger.warning(f"[清理Trader] 停止事件路由器线程失败: {e} | client={client_info}")
933
1001
 
934
1002
  def _delayed_disconnect(self, delay: float = 0.5):
935
1003
  """延迟断开连接,确保异常能传输到客户端"""
@@ -1098,13 +1166,13 @@ class XtQuantService(rpyc.Service):
1098
1166
 
1099
1167
  logger.info(f"[创建Trader] userdata_path={userdata_path} | session_id={session_id}")
1100
1168
 
1101
- # 记录 trader 实例,供 on_disconnect 时自动清理
1102
- self._traders.append(trader)
1103
-
1104
1169
  # 每个 trader 独立一个事件路由器,用于把 on_stock_* 推送给远端客户端
1105
1170
  event_callback = _TraderEventCallback()
1106
1171
  trader.register_callback(event_callback)
1107
1172
 
1173
+ # 记录 trader + 事件路由器,供 on_disconnect 时自动清理(含停止线程)
1174
+ self._traders.append((trader, event_callback))
1175
+
1108
1176
  # 用 LoggingProxy 包装 trader,支持日志记录、序列化和事件回调透传
1109
1177
  return LoggingProxy(
1110
1178
  trader, 'xttrader',
@@ -1143,8 +1211,8 @@ class XtQuantService(rpyc.Service):
1143
1211
  )
1144
1212
  trader.register_callback(adapter)
1145
1213
 
1146
- # 记录 trader 实例,供 on_disconnect 时自动清理
1147
- self._traders.append(trader)
1214
+ # 记录 trader + 事件路由器,供 on_disconnect 时自动清理(含停止线程)
1215
+ self._traders.append((trader, event_callback))
1148
1216
 
1149
1217
  return LoggingProxy(
1150
1218
  trader, 'xttrader',
@@ -1176,18 +1244,23 @@ class XtQuantService(rpyc.Service):
1176
1244
 
1177
1245
  logger.info(f"[创建并连接大QMT Trader] account_id={account_id} | session_id={session_id}")
1178
1246
 
1179
- start_result = trader.start()
1180
- logger.info(f"[大QMT Trader] start() 返回: {start_result}")
1181
- if start_result not in (0, None):
1182
- raise RuntimeError(f"启动大QMT 交易线程失败,错误码: {start_result}")
1183
-
1184
- # bigqmt connect() 同步:内部 RPC ping + 合成 on_account_status;失败抛异常
1185
- connect_result = trader.connect()
1186
- logger.info(f"[大QMT Trader] connect() 返回: {connect_result}")
1187
- if connect_result not in (0, None):
1188
- raise ConnectionError(f"大QMT 连接失败: 错误码 {connect_result}")
1247
+ try:
1248
+ start_result = trader.start()
1249
+ logger.info(f"[大QMT Trader] start() 返回: {start_result}")
1250
+ if start_result not in (0, None):
1251
+ raise RuntimeError(f"启动大QMT 交易线程失败,错误码: {start_result}")
1252
+
1253
+ # bigqmt connect() 同步:内部 RPC ping + 合成 on_account_status;失败抛异常
1254
+ connect_result = trader.connect()
1255
+ logger.info(f"[大QMT Trader] connect() 返回: {connect_result}")
1256
+ if connect_result not in (0, None):
1257
+ raise ConnectionError(f"大QMT 连接失败: 错误码 {connect_result}")
1258
+ except Exception:
1259
+ # 失败路径也要停止事件路由器线程,避免泄漏
1260
+ event_callback.stop()
1261
+ raise
1189
1262
 
1190
- self._traders.append(trader)
1263
+ self._traders.append((trader, event_callback))
1191
1264
 
1192
1265
  return LoggingProxy(
1193
1266
  trader, 'xttrader',
@@ -1247,48 +1320,56 @@ class XtQuantService(rpyc.Service):
1247
1320
 
1248
1321
  logger.info(f"[创建并连接Trader] userdata_path={userdata_path} | session_id={session_id}")
1249
1322
 
1250
- start_result = trader.start()
1251
- logger.info(f"[Trader] start() 返回: {start_result}")
1252
- if start_result not in (0, None):
1253
- raise RuntimeError(f"启动交易线程失败,错误码: {start_result}")
1254
-
1255
- connect_result = trader.connect()
1256
- logger.info(f"[Trader] connect() 返回: {connect_result}")
1257
- if connect_result not in (0, None):
1258
- error_codes = {
1259
- -1: "交易服务器未连接",
1260
- -2: "账号未登录",
1261
- -3: "请求超时",
1262
- -4: "资金账号不存在",
1263
- }
1264
- raise ConnectionError(f"连接失败: {error_codes.get(connect_result, f'错误码 {connect_result}')}")
1265
-
1266
- # 等待真实的交易连接建立
1267
- connected = callback.wait_for_connected(connect_timeout)
1268
- logger.info(f"[Trader] on_connected 回调状态: {connected}")
1269
- if not connected:
1270
- # 某些版本的 xtquant 可能不会触发 on_connected 回调,
1271
- # 尝试用 query_stock_asset 验证连接是否真正可用
1272
- logger.info("[Trader] on_connected 未触发,尝试 query_stock_asset 验证连接...")
1273
- try:
1274
- from xtquant.xttype import StockAccount
1275
- account_id = os.environ.get("QMT_ACCOUNT_ID")
1276
- if account_id:
1277
- test_account = StockAccount(account_id)
1278
- asset = trader.query_stock_asset(test_account)
1279
- logger.info(f"[Trader] query_stock_asset 验证成功: {asset}")
1280
- connected = True
1281
- except Exception as e:
1282
- logger.warning(f"[Trader] query_stock_asset 验证失败: {e}")
1323
+ try:
1324
+ start_result = trader.start()
1325
+ logger.info(f"[Trader] start() 返回: {start_result}")
1326
+ if start_result not in (0, None):
1327
+ raise RuntimeError(f"启动交易线程失败,错误码: {start_result}")
1328
+
1329
+ connect_result = trader.connect()
1330
+ logger.info(f"[Trader] connect() 返回: {connect_result}")
1331
+ if connect_result not in (0, None):
1332
+ error_codes = {
1333
+ -1: "交易服务器未连接",
1334
+ -2: "账号未登录",
1335
+ -3: "请求超时",
1336
+ -4: "资金账号不存在",
1337
+ }
1338
+ raise ConnectionError(f"连接失败: {error_codes.get(connect_result, f'错误码 {connect_result}')}")
1339
+
1340
+ # 等待真实的交易连接建立
1341
+ connected = callback.wait_for_connected(connect_timeout)
1342
+ logger.info(f"[Trader] on_connected 回调状态: {connected}")
1343
+ if not connected:
1344
+ # 某些版本的 xtquant 可能不会触发 on_connected 回调,
1345
+ # 尝试用 query_stock_asset 验证连接是否真正可用
1346
+ logger.info("[Trader] on_connected 未触发,尝试 query_stock_asset 验证连接...")
1347
+ try:
1348
+ from xtquant.xttype import StockAccount
1349
+ account_id = os.environ.get("QMT_ACCOUNT_ID")
1350
+ if account_id:
1351
+ test_account = StockAccount(account_id)
1352
+ asset = trader.query_stock_asset(test_account)
1353
+ logger.info(f"[Trader] query_stock_asset 验证成功: {asset}")
1354
+ connected = True
1355
+ except Exception as e:
1356
+ logger.warning(f"[Trader] query_stock_asset 验证失败: {e}")
1283
1357
 
1284
- if not connected:
1285
- trader.stop()
1286
- raise TimeoutError(
1287
- f"等待交易连接建立超时({connect_timeout}秒),"
1288
- "请检查 MiniQMT 是否已登录交易账号,或设置更长的 --connect-timeout"
1289
- )
1358
+ if not connected:
1359
+ raise TimeoutError(
1360
+ f"等待交易连接建立超时({connect_timeout}秒),"
1361
+ "请检查 MiniQMT 是否已登录交易账号,或设置更长的 --connect-timeout"
1362
+ )
1363
+ except Exception:
1364
+ # 失败路径:停止 trader 与事件路由器线程,避免泄漏
1365
+ try:
1366
+ trader.stop()
1367
+ except Exception:
1368
+ pass
1369
+ callback.stop()
1370
+ raise
1290
1371
 
1291
- self._traders.append(trader)
1372
+ self._traders.append((trader, callback))
1292
1373
 
1293
1374
  return LoggingProxy(
1294
1375
  trader, 'xttrader',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xtquant-share
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: Xtreme Quant Share - eXtreme transparent sharing proxy for XtQuant on macOS/Linux
5
5
  Author-email: Jason Hu <63170682@qq.com>
6
6
  License-Expression: GPL-3.0-only
File without changes
File without changes
File without changes