xtquant-share 1.4.1__tar.gz → 1.4.2__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 (55) hide show
  1. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/PKG-INFO +64 -16
  2. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/README.md +63 -15
  3. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/pyproject.toml +1 -1
  4. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_client_permission.py +2 -2
  5. xtquant_share-1.4.2/tests/test_qmt_assetlog.py +226 -0
  6. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server.py +8 -8
  7. xtquant_share-1.4.2/tests/test_server_dataframe_serialization.py +60 -0
  8. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/.env.example +8 -0
  9. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/__init__.py +3 -1
  10. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/auth.py +6 -0
  11. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/check_env.py +1 -1
  12. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/client.py +32 -2
  13. xtquant_share-1.4.2/xqshare/qmt_assetlog/__init__.py +26 -0
  14. xtquant_share-1.4.2/xqshare/qmt_assetlog/reader.py +275 -0
  15. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/server.py +117 -0
  16. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/tools/xtdata.py +2 -2
  17. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/PKG-INFO +64 -16
  18. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/SOURCES.txt +4 -0
  19. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/LICENSE +0 -0
  20. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/setup.cfg +0 -0
  21. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_auth.py +0 -0
  22. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_channels_bigqmt.py +0 -0
  23. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_channels_health.py +0 -0
  24. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_channels_router.py +0 -0
  25. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_client.py +0 -0
  26. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_integration.py +0 -0
  27. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_qmt_datadir_factor.py +0 -0
  28. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_quote_events.py +0 -0
  29. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_remote_data.py +0 -0
  30. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server_channel_routing.py +0 -0
  31. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server_download_history_data2_compat.py +0 -0
  32. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server_download_timeout.py +0 -0
  33. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server_permission.py +0 -0
  34. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_tools_common.py +0 -0
  35. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_trader_events.py +0 -0
  36. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/channels/__init__.py +0 -0
  37. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/channels/bigqmt.py +0 -0
  38. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/channels/health.py +0 -0
  39. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/channels/router.py +0 -0
  40. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/clients.yaml.example +0 -0
  41. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/__init__.py +0 -0
  42. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/divid.py +0 -0
  43. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/factor.py +0 -0
  44. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/kline.py +0 -0
  45. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/misc.py +0 -0
  46. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/reader.py +0 -0
  47. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/sector.py +0 -0
  48. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_watchdog.py +0 -0
  49. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/tools/__init__.py +0 -0
  50. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/tools/common.py +0 -0
  51. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/tools/xttrader.py +0 -0
  52. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/dependency_links.txt +0 -0
  53. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/entry_points.txt +0 -0
  54. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/requires.txt +0 -0
  55. {xtquant_share-1.4.1 → xtquant_share-1.4.2}/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.4.1
3
+ Version: 1.4.2
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
@@ -53,6 +53,7 @@ Dynamic: license-file
53
53
  - ✅ **服务实例标识** - `get_instance_id()` 轻量感知 server 重启;行情订阅幂等自动去重,阻断底层 miniquote 订阅累积
54
54
  - ✅ **异步回调** - 支持交易事件(on_stock_*)与行情 tick 订阅回调(`xtdata.register_quote_callback` + `subscribe_whole_quote`)
55
55
  - ✅ **无感切换** - 大QMT ↔ miniQMT 无感切换(行情侧),自动探测 miniQMT 可用性,不可用自动切 bigqmt 通道(复用 xtquant_big_convert)
56
+ - ✅ **资产兜底** - 大QMT 日志资金心跳 → 总资产/可用/市值/负债(miniQMT 独立交易不可用时仍能读资产)
56
57
  - ✅ **完整日志** - API调用日志,记录函数名、参数、耗时
57
58
  - ✅ **零学习成本** - 无需记忆新 API
58
59
 
@@ -111,7 +112,7 @@ python -m xqshare.server
111
112
  ### 客户端快速测试
112
113
 
113
114
  ```bash
114
- export XQSHARE_REMOTE_HOST="21.214.136.216"
115
+ export XQSHARE_REMOTE_HOST="203.0.113.10"
115
116
  xtdata get_stock_list_in_sector --sector-name "沪深A股" --limit 10
116
117
  ```
117
118
 
@@ -201,7 +202,7 @@ xttrader order_cancel --order-id 1082130745
201
202
  **推荐:使用环境变量配置(避免敏感信息泄露)**
202
203
  ```bash
203
204
  # 设置环境变量
204
- export XQSHARE_REMOTE_HOST="21.214.136.216"
205
+ export XQSHARE_REMOTE_HOST="203.0.113.10"
205
206
  export XQSHARE_CLIENT_SECRET="your-secret"
206
207
 
207
208
  # 获取股票列表
@@ -233,10 +234,10 @@ python examples/query_positions.py
233
234
  **备选:命令行参数(覆盖环境变量):**
234
235
  ```bash
235
236
  # 显式指定服务端地址
236
- python examples/get_stock_list.py --host 21.214.136.216 --sector "沪深300"
237
+ python examples/get_stock_list.py --host 203.0.113.10 --sector "沪深300"
237
238
 
238
239
  # 显式指定认证密钥
239
- python examples/get_tick_data.py --host 21.214.136.216 --secret "your-secret" --codes "000001.SZ"
240
+ python examples/get_tick_data.py --host 203.0.113.10 --secret "your-secret" --codes "000001.SZ"
240
241
 
241
242
  # 查看帮助
242
243
  python examples/get_stock_list.py --help
@@ -250,11 +251,11 @@ python examples/get_stock_list.py --help
250
251
  from xqshare import XtQuantRemote, connect, disconnect, xtdata, xttrader, xttype
251
252
 
252
253
  # 方式1:类实例(推荐)
253
- with XtQuantRemote("21.214.136.216", client_secret="xxx") as xt:
254
+ with XtQuantRemote("203.0.113.10", client_secret="xxx") as xt:
254
255
  stocks = xt.xtdata.get_stock_list_in_sector("沪深A股")
255
256
 
256
257
  # 方式2:全局便捷函数
257
- connect(host="21.214.136.216", client_secret="xxx")
258
+ connect(host="203.0.113.10", client_secret="xxx")
258
259
  stocks = xtdata.get_stock_list_in_sector("沪深A股")
259
260
  disconnect()
260
261
  ```
@@ -263,6 +264,7 @@ disconnect()
263
264
  - `xt.xtdata` - 行情数据模块
264
265
  - `xt.xttype` - 类型定义模块(StockAccount 等)
265
266
  - `xt.datadir` - QMT datadir 文件解析模块(无需 miniQMT 进程)
267
+ - `xt.assetlog` / `xt.get_asset()` - 大QMT 日志资金心跳 → 资产快照(trader 不可用时的兜底)
266
268
  - `xt.create_trader()` - 创建交易实例(需自行 connect)
267
269
  - `xt.create_trader_and_connect()` - 创建交易实例并等待真实连接建立(推荐用于下单)
268
270
 
@@ -302,7 +304,7 @@ QMT_DATADIR_PATH=D:\国金证券QMT交易端\datadir
302
304
  ```python
303
305
  from xqshare import XtQuantRemote
304
306
 
305
- with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
307
+ with XtQuantRemote("203.0.113.10", client_secret="my-secret") as xt:
306
308
  # 读取 K 线数据(直接解析 .DAT 文件,无需 miniQMT 进程)
307
309
  df = xt.datadir.kline("600000.SH", "1d")
308
310
  print(df.tail())
@@ -338,7 +340,7 @@ with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
338
340
  ```python
339
341
  import xqshare
340
342
 
341
- xqshare.connect(host="21.214.136.216", client_secret="my-secret")
343
+ xqshare.connect(host="203.0.113.10", client_secret="my-secret")
342
344
 
343
345
  # 与 xqshare.xtdata 用法完全对称
344
346
  df = xqshare.datadir.kline("600000.SH", "1d")
@@ -368,12 +370,58 @@ RuntimeError: datadir 不可用:未配置 QMT_DATADIR_PATH 且无法自动推
368
370
 
369
371
  ---
370
372
 
373
+ ## assetlog:日志资金心跳 → 资产
374
+
375
+ miniQMT / 独立交易模式被券商关闭后,`xttrader.query_stock_asset` 会报「交易服务端未启动」。但大QMT 客户端登录后,每 3~12 秒会往 `<QMT>/userdata/log/XtClient_YYYYMMDD.log` 写一条资金心跳:
376
+
377
+ ```
378
+ 2026-11-09 17:12:11,309 [INFO] ... [calc] update fund [2_10039_1000_<ACCOUNT>] bal 138879.92 avai 55757.92 debt 0.00 mv 83122.00
379
+ ```
380
+
381
+ `xqshare.assetlog` 增量 tail 这个文件,给出总资产 / 可用资金 / 市值 / 负债。
382
+
383
+ ### Server 端配置(Windows)
384
+
385
+ ```ini
386
+ # 日志目录(可选):不配则按 QMT_USERDATA_PATH 推断为 <QMT安装>/userdata/log
387
+ QMT_LOG_DIR=C:\Install\pazq_qmt\pazq_qmt_2.0.8.0\userdata\log
388
+
389
+ # 判为过期的阈值(秒),默认 60
390
+ QMT_ASSETLOG_STALE_S=60
391
+ ```
392
+
393
+ ### Client 端使用
394
+
395
+ ```python
396
+ from xqshare import XtQuantRemote
397
+
398
+ with XtQuantRemote("203.0.113.10", client_secret="my-secret") as xt:
399
+ snap = xt.get_asset("<ACCOUNT_ID>") # 或 xt.assetlog.get_asset(...)
400
+ print(snap["total_asset"], snap["cash"], snap["market_value"], snap["stale"])
401
+ # 138879.92 55757.92 83122.0 False
402
+ ```
403
+
404
+ > ⚠️ 这是**推送快照**,不是查询接口:断线就停,返回值带 `as_of` / `age_s` / `stale`,用之前先看新鲜度。
405
+
406
+ ### 能力边界
407
+
408
+ | 项 | 是否支持 | 说明 |
409
+ |----|---------|------|
410
+ | 总资产 / 可用 / 市值 / 负债 | ✅ | 心跳里直接有 |
411
+ | 持仓明细 | ❌ | 日志没有逐票数量与成本 |
412
+ | 冻结资金 | ❌ | `accountdetail` 行的 marketvalue 是 DBL_MAX,整行不可信 |
413
+ | 即时查询 | ❌ | 只有 3~12 秒的推送节拍 |
414
+
415
+ 详细字段与异常说明见 [doc/xqshare-api.md](doc/xqshare-api.md#8-assetlog-模块-api)。
416
+
417
+ ---
418
+
371
419
  ## 使用示例
372
420
 
373
421
  ```python
374
422
  from xqshare import XtQuantRemote
375
423
 
376
- with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
424
+ with XtQuantRemote("203.0.113.10", client_secret="my-secret") as xt:
377
425
  # 获取股票列表
378
426
  stocks = xt.xtdata.get_stock_list_in_sector("沪深A股")
379
427
  print(f"股票数量: {len(stocks)}")
@@ -396,7 +444,7 @@ with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
396
444
  ```python
397
445
  from xqshare import XtQuantRemote
398
446
 
399
- with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
447
+ with XtQuantRemote("203.0.113.10", client_secret="my-secret") as xt:
400
448
  # 创建交易实例并等待真实连接建立(推荐)
401
449
  # userdata_path 可通过环境变量 QMT_USERDATA_PATH 配置
402
450
  trader = xt.create_trader_and_connect("C:\\QMT\\userdata_mini")
@@ -478,7 +526,7 @@ logs/client_20260228.log
478
526
  **客户端也会记录调用:**
479
527
  ```python
480
528
  # 设置日志级别
481
- xt = XtQuantRemote("21.214.136.216", log_level="DEBUG")
529
+ xt = XtQuantRemote("203.0.113.10", log_level="DEBUG")
482
530
  ```
483
531
 
484
532
  客户端日志示例:
@@ -590,7 +638,7 @@ python -m xqshare.server --ssl --cert server.crt --key server.key
590
638
 
591
639
  ```python
592
640
  xt = XtQuantRemote(
593
- host="21.214.136.216",
641
+ host="203.0.113.10",
594
642
  use_ssl=True,
595
643
  ssl_verify=False # 自签名证书需禁用验证
596
644
  )
@@ -608,7 +656,7 @@ xt = XtQuantRemote(
608
656
 
609
657
  ```python
610
658
  xt = XtQuantRemote(
611
- host="21.214.136.216",
659
+ host="203.0.113.10",
612
660
  auto_reconnect=True,
613
661
  max_retries=10,
614
662
  heartbeat_interval=15,
@@ -779,8 +827,8 @@ tail -f logs/client_*.log
779
827
 
780
828
  ```bash
781
829
  # 检查网络
782
- ping 21.214.136.216
783
- telnet 21.214.136.216 18812
830
+ ping 203.0.113.10
831
+ telnet 203.0.113.10 18812
784
832
  ```
785
833
 
786
834
  ### 查看服务状态
@@ -11,6 +11,7 @@
11
11
  - ✅ **服务实例标识** - `get_instance_id()` 轻量感知 server 重启;行情订阅幂等自动去重,阻断底层 miniquote 订阅累积
12
12
  - ✅ **异步回调** - 支持交易事件(on_stock_*)与行情 tick 订阅回调(`xtdata.register_quote_callback` + `subscribe_whole_quote`)
13
13
  - ✅ **无感切换** - 大QMT ↔ miniQMT 无感切换(行情侧),自动探测 miniQMT 可用性,不可用自动切 bigqmt 通道(复用 xtquant_big_convert)
14
+ - ✅ **资产兜底** - 大QMT 日志资金心跳 → 总资产/可用/市值/负债(miniQMT 独立交易不可用时仍能读资产)
14
15
  - ✅ **完整日志** - API调用日志,记录函数名、参数、耗时
15
16
  - ✅ **零学习成本** - 无需记忆新 API
16
17
 
@@ -69,7 +70,7 @@ python -m xqshare.server
69
70
  ### 客户端快速测试
70
71
 
71
72
  ```bash
72
- export XQSHARE_REMOTE_HOST="21.214.136.216"
73
+ export XQSHARE_REMOTE_HOST="203.0.113.10"
73
74
  xtdata get_stock_list_in_sector --sector-name "沪深A股" --limit 10
74
75
  ```
75
76
 
@@ -159,7 +160,7 @@ xttrader order_cancel --order-id 1082130745
159
160
  **推荐:使用环境变量配置(避免敏感信息泄露)**
160
161
  ```bash
161
162
  # 设置环境变量
162
- export XQSHARE_REMOTE_HOST="21.214.136.216"
163
+ export XQSHARE_REMOTE_HOST="203.0.113.10"
163
164
  export XQSHARE_CLIENT_SECRET="your-secret"
164
165
 
165
166
  # 获取股票列表
@@ -191,10 +192,10 @@ python examples/query_positions.py
191
192
  **备选:命令行参数(覆盖环境变量):**
192
193
  ```bash
193
194
  # 显式指定服务端地址
194
- python examples/get_stock_list.py --host 21.214.136.216 --sector "沪深300"
195
+ python examples/get_stock_list.py --host 203.0.113.10 --sector "沪深300"
195
196
 
196
197
  # 显式指定认证密钥
197
- python examples/get_tick_data.py --host 21.214.136.216 --secret "your-secret" --codes "000001.SZ"
198
+ python examples/get_tick_data.py --host 203.0.113.10 --secret "your-secret" --codes "000001.SZ"
198
199
 
199
200
  # 查看帮助
200
201
  python examples/get_stock_list.py --help
@@ -208,11 +209,11 @@ python examples/get_stock_list.py --help
208
209
  from xqshare import XtQuantRemote, connect, disconnect, xtdata, xttrader, xttype
209
210
 
210
211
  # 方式1:类实例(推荐)
211
- with XtQuantRemote("21.214.136.216", client_secret="xxx") as xt:
212
+ with XtQuantRemote("203.0.113.10", client_secret="xxx") as xt:
212
213
  stocks = xt.xtdata.get_stock_list_in_sector("沪深A股")
213
214
 
214
215
  # 方式2:全局便捷函数
215
- connect(host="21.214.136.216", client_secret="xxx")
216
+ connect(host="203.0.113.10", client_secret="xxx")
216
217
  stocks = xtdata.get_stock_list_in_sector("沪深A股")
217
218
  disconnect()
218
219
  ```
@@ -221,6 +222,7 @@ disconnect()
221
222
  - `xt.xtdata` - 行情数据模块
222
223
  - `xt.xttype` - 类型定义模块(StockAccount 等)
223
224
  - `xt.datadir` - QMT datadir 文件解析模块(无需 miniQMT 进程)
225
+ - `xt.assetlog` / `xt.get_asset()` - 大QMT 日志资金心跳 → 资产快照(trader 不可用时的兜底)
224
226
  - `xt.create_trader()` - 创建交易实例(需自行 connect)
225
227
  - `xt.create_trader_and_connect()` - 创建交易实例并等待真实连接建立(推荐用于下单)
226
228
 
@@ -260,7 +262,7 @@ QMT_DATADIR_PATH=D:\国金证券QMT交易端\datadir
260
262
  ```python
261
263
  from xqshare import XtQuantRemote
262
264
 
263
- with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
265
+ with XtQuantRemote("203.0.113.10", client_secret="my-secret") as xt:
264
266
  # 读取 K 线数据(直接解析 .DAT 文件,无需 miniQMT 进程)
265
267
  df = xt.datadir.kline("600000.SH", "1d")
266
268
  print(df.tail())
@@ -296,7 +298,7 @@ with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
296
298
  ```python
297
299
  import xqshare
298
300
 
299
- xqshare.connect(host="21.214.136.216", client_secret="my-secret")
301
+ xqshare.connect(host="203.0.113.10", client_secret="my-secret")
300
302
 
301
303
  # 与 xqshare.xtdata 用法完全对称
302
304
  df = xqshare.datadir.kline("600000.SH", "1d")
@@ -326,12 +328,58 @@ RuntimeError: datadir 不可用:未配置 QMT_DATADIR_PATH 且无法自动推
326
328
 
327
329
  ---
328
330
 
331
+ ## assetlog:日志资金心跳 → 资产
332
+
333
+ miniQMT / 独立交易模式被券商关闭后,`xttrader.query_stock_asset` 会报「交易服务端未启动」。但大QMT 客户端登录后,每 3~12 秒会往 `<QMT>/userdata/log/XtClient_YYYYMMDD.log` 写一条资金心跳:
334
+
335
+ ```
336
+ 2026-11-09 17:12:11,309 [INFO] ... [calc] update fund [2_10039_1000_<ACCOUNT>] bal 138879.92 avai 55757.92 debt 0.00 mv 83122.00
337
+ ```
338
+
339
+ `xqshare.assetlog` 增量 tail 这个文件,给出总资产 / 可用资金 / 市值 / 负债。
340
+
341
+ ### Server 端配置(Windows)
342
+
343
+ ```ini
344
+ # 日志目录(可选):不配则按 QMT_USERDATA_PATH 推断为 <QMT安装>/userdata/log
345
+ QMT_LOG_DIR=C:\Install\pazq_qmt\pazq_qmt_2.0.8.0\userdata\log
346
+
347
+ # 判为过期的阈值(秒),默认 60
348
+ QMT_ASSETLOG_STALE_S=60
349
+ ```
350
+
351
+ ### Client 端使用
352
+
353
+ ```python
354
+ from xqshare import XtQuantRemote
355
+
356
+ with XtQuantRemote("203.0.113.10", client_secret="my-secret") as xt:
357
+ snap = xt.get_asset("<ACCOUNT_ID>") # 或 xt.assetlog.get_asset(...)
358
+ print(snap["total_asset"], snap["cash"], snap["market_value"], snap["stale"])
359
+ # 138879.92 55757.92 83122.0 False
360
+ ```
361
+
362
+ > ⚠️ 这是**推送快照**,不是查询接口:断线就停,返回值带 `as_of` / `age_s` / `stale`,用之前先看新鲜度。
363
+
364
+ ### 能力边界
365
+
366
+ | 项 | 是否支持 | 说明 |
367
+ |----|---------|------|
368
+ | 总资产 / 可用 / 市值 / 负债 | ✅ | 心跳里直接有 |
369
+ | 持仓明细 | ❌ | 日志没有逐票数量与成本 |
370
+ | 冻结资金 | ❌ | `accountdetail` 行的 marketvalue 是 DBL_MAX,整行不可信 |
371
+ | 即时查询 | ❌ | 只有 3~12 秒的推送节拍 |
372
+
373
+ 详细字段与异常说明见 [doc/xqshare-api.md](doc/xqshare-api.md#8-assetlog-模块-api)。
374
+
375
+ ---
376
+
329
377
  ## 使用示例
330
378
 
331
379
  ```python
332
380
  from xqshare import XtQuantRemote
333
381
 
334
- with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
382
+ with XtQuantRemote("203.0.113.10", client_secret="my-secret") as xt:
335
383
  # 获取股票列表
336
384
  stocks = xt.xtdata.get_stock_list_in_sector("沪深A股")
337
385
  print(f"股票数量: {len(stocks)}")
@@ -354,7 +402,7 @@ with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
354
402
  ```python
355
403
  from xqshare import XtQuantRemote
356
404
 
357
- with XtQuantRemote("21.214.136.216", client_secret="my-secret") as xt:
405
+ with XtQuantRemote("203.0.113.10", client_secret="my-secret") as xt:
358
406
  # 创建交易实例并等待真实连接建立(推荐)
359
407
  # userdata_path 可通过环境变量 QMT_USERDATA_PATH 配置
360
408
  trader = xt.create_trader_and_connect("C:\\QMT\\userdata_mini")
@@ -436,7 +484,7 @@ logs/client_20260228.log
436
484
  **客户端也会记录调用:**
437
485
  ```python
438
486
  # 设置日志级别
439
- xt = XtQuantRemote("21.214.136.216", log_level="DEBUG")
487
+ xt = XtQuantRemote("203.0.113.10", log_level="DEBUG")
440
488
  ```
441
489
 
442
490
  客户端日志示例:
@@ -548,7 +596,7 @@ python -m xqshare.server --ssl --cert server.crt --key server.key
548
596
 
549
597
  ```python
550
598
  xt = XtQuantRemote(
551
- host="21.214.136.216",
599
+ host="203.0.113.10",
552
600
  use_ssl=True,
553
601
  ssl_verify=False # 自签名证书需禁用验证
554
602
  )
@@ -566,7 +614,7 @@ xt = XtQuantRemote(
566
614
 
567
615
  ```python
568
616
  xt = XtQuantRemote(
569
- host="21.214.136.216",
617
+ host="203.0.113.10",
570
618
  auto_reconnect=True,
571
619
  max_retries=10,
572
620
  heartbeat_interval=15,
@@ -737,8 +785,8 @@ tail -f logs/client_*.log
737
785
 
738
786
  ```bash
739
787
  # 检查网络
740
- ping 21.214.136.216
741
- telnet 21.214.136.216 18812
788
+ ping 203.0.113.10
789
+ telnet 203.0.113.10 18812
742
790
  ```
743
791
 
744
792
  ### 查看服务状态
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xtquant-share"
7
- version = "1.4.1"
7
+ version = "1.4.2"
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"
@@ -184,14 +184,14 @@ class TestClientEnvConfig:
184
184
  with patch('xqshare.client.rpyc.connect', return_value=mock_conn):
185
185
  with patch('xqshare.client.BgServingThread'):
186
186
  with patch.dict(os.environ, {
187
- "XQSHARE_REMOTE_HOST": "21.214.136.216",
187
+ "XQSHARE_REMOTE_HOST": "203.0.113.10",
188
188
  "XQSHARE_REMOTE_PORT": "18888",
189
189
  "XQSHARE_CLIENT_ID": "env-client",
190
190
  "XQSHARE_CLIENT_SECRET": "env-secret"
191
191
  }):
192
192
  client = XtQuantRemote(heartbeat_interval=0)
193
193
 
194
- assert client._host == "21.214.136.216"
194
+ assert client._host == "203.0.113.10"
195
195
  assert client._port == 18888
196
196
  assert client._client_id == "env-client"
197
197
  assert client._client_secret == "env-secret"
@@ -0,0 +1,226 @@
1
+ # -*- coding: utf-8 -*-
2
+ """qmt_assetlog(大QMT 日志资金心跳 → 资产快照)单元测试。"""
3
+ import os
4
+ import time
5
+
6
+ import pytest
7
+
8
+ from xqshare.qmt_assetlog import QmtAssetLogReader
9
+
10
+
11
+ ACC = "307100903096"
12
+ ACC2 = "307199913362"
13
+ KEY = f"2_10039_1000_{ACC}"
14
+ KEY2 = f"3_10039_1000_{ACC2}"
15
+
16
+
17
+ def _line(ts, key, bal, avai, mv, debt=0.0, sv=None):
18
+ sv = mv if sv is None else sv
19
+ return (
20
+ f"{ts},309 [INFO] [0x00003b88] [0x00000754] [17:12:11,191] [calc] "
21
+ f"update fund [{key}] bal {bal} avai {avai} debt {debt} "
22
+ f"mv {mv} sv {sv} em 0.00 um 0.00\n"
23
+ )
24
+
25
+
26
+ def _write_log(log_dir, date_str, lines):
27
+ path = os.path.join(log_dir, f"XtClient_{date_str}.log")
28
+ with open(path, "w", encoding="utf-8") as f:
29
+ f.writelines(lines)
30
+ return path
31
+
32
+
33
+ class _Clock:
34
+ def __init__(self, ts):
35
+ self.ts = ts
36
+
37
+ def __call__(self):
38
+ return self.ts
39
+
40
+ def advance(self, seconds):
41
+ self.ts += seconds
42
+
43
+
44
+ def _epoch(date_str, hhmmss="17:12:11"):
45
+ return time.mktime(time.strptime(f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:]} {hhmmss}",
46
+ "%Y-%m-%d %H:%M:%S"))
47
+
48
+
49
+ def test_get_asset_parses_fields(tmp_path):
50
+ date = "20261109"
51
+ _write_log(tmp_path, date, [
52
+ "2026-11-09 17:12:00,001 [INFO] unrelated line\n",
53
+ _line("2026-11-09 17:12:11", KEY, "138879.92", "55757.92", "83122.00"),
54
+ ])
55
+ reader = QmtAssetLogReader(str(tmp_path), clock=_Clock(_epoch(date, "17:12:12")))
56
+
57
+ snap = reader.get_asset(ACC)
58
+
59
+ assert snap["account"] == ACC
60
+ assert snap["account_key"] == KEY
61
+ assert snap["total_asset"] == pytest.approx(138879.92)
62
+ assert snap["cash"] == pytest.approx(55757.92)
63
+ assert snap["market_value"] == pytest.approx(83122.00)
64
+ assert snap["debt"] == pytest.approx(0.0)
65
+ assert snap["as_of"] == "2026-11-09 17:12:11"
66
+ assert snap["age_s"] == pytest.approx(1.0)
67
+ assert snap["stale"] is False
68
+ assert snap["raw"]["sv"] == pytest.approx(83122.00)
69
+ assert snap["raw"]["um"] == pytest.approx(0.0)
70
+
71
+
72
+ def test_incremental_read_only_new_bytes(tmp_path):
73
+ date = "20261109"
74
+ path = _write_log(tmp_path, date, [
75
+ _line("2026-11-09 17:12:11", KEY, "100.0", "50.0", "50.0"),
76
+ ])
77
+ reader = QmtAssetLogReader(str(tmp_path), clock=_Clock(_epoch(date, "17:12:12")))
78
+ assert reader.get_asset(ACC)["total_asset"] == pytest.approx(100.0)
79
+
80
+ with open(path, "a", encoding="utf-8") as f:
81
+ f.write(_line("2026-11-09 17:12:14", KEY, "200.0", "60.0", "140.0"))
82
+
83
+ snap = reader.get_asset(ACC)
84
+ assert snap["total_asset"] == pytest.approx(200.0)
85
+ assert snap["as_of"] == "2026-11-09 17:12:14"
86
+
87
+
88
+ def test_partial_line_is_buffered_until_newline(tmp_path):
89
+ date = "20261109"
90
+ path = os.path.join(tmp_path, f"XtClient_{date}.log")
91
+ full = _line("2026-11-09 17:12:11", KEY, "300.0", "100.0", "200.0")
92
+ with open(path, "w", encoding="utf-8") as f:
93
+ f.write(full[:-1]) # 去掉换行,模拟写到一半
94
+
95
+ reader = QmtAssetLogReader(str(tmp_path), clock=_Clock(_epoch(date, "17:12:12")))
96
+ with pytest.raises(LookupError):
97
+ reader.get_asset(ACC)
98
+
99
+ with open(path, "a", encoding="utf-8") as f:
100
+ f.write("\n")
101
+
102
+ assert reader.get_asset(ACC)["total_asset"] == pytest.approx(300.0)
103
+
104
+
105
+ def test_multiple_accounts_require_explicit_account(tmp_path, monkeypatch):
106
+ monkeypatch.delenv("QMT_ACCOUNT_ID", raising=False)
107
+ date = "20261109"
108
+ _write_log(tmp_path, date, [
109
+ _line("2026-11-09 17:12:11", KEY, "100.0", "50.0", "50.0"),
110
+ _line("2026-11-09 17:12:13", KEY2, "0.11", "0.11", "0.0"),
111
+ ])
112
+ reader = QmtAssetLogReader(str(tmp_path), clock=_Clock(_epoch(date, "17:12:20")))
113
+
114
+ with pytest.raises(ValueError):
115
+ reader.get_asset()
116
+
117
+ assert reader.get_asset(ACC2)["total_asset"] == pytest.approx(0.11)
118
+ assert set(reader.list_accounts()) == {KEY, KEY2}
119
+
120
+
121
+ def test_unknown_account_raises(tmp_path):
122
+ date = "20261109"
123
+ _write_log(tmp_path, date, [
124
+ _line("2026-11-09 17:12:11", KEY, "100.0", "50.0", "50.0"),
125
+ ])
126
+ reader = QmtAssetLogReader(str(tmp_path), clock=_Clock(_epoch(date, "17:12:12")))
127
+
128
+ with pytest.raises(LookupError):
129
+ reader.get_asset("999999")
130
+
131
+
132
+ def test_no_heartbeat_raises(tmp_path):
133
+ date = "20261109"
134
+ _write_log(tmp_path, date, ["2026-11-09 17:12:00,001 [INFO] nothing here\n"])
135
+ reader = QmtAssetLogReader(str(tmp_path), clock=_Clock(_epoch(date, "17:12:12")))
136
+
137
+ with pytest.raises(LookupError):
138
+ reader.get_asset(ACC)
139
+
140
+
141
+ def test_stale_after_threshold(tmp_path):
142
+ date = "20261109"
143
+ _write_log(tmp_path, date, [
144
+ _line("2026-11-09 17:12:11", KEY, "100.0", "50.0", "50.0"),
145
+ ])
146
+ clock = _Clock(_epoch(date, "17:12:12"))
147
+ reader = QmtAssetLogReader(str(tmp_path), stale_after=30, clock=clock)
148
+
149
+ assert reader.get_asset(ACC)["stale"] is False
150
+
151
+ clock.advance(31)
152
+ snap = reader.get_asset(ACC)
153
+ assert snap["stale"] is True
154
+ assert snap["age_s"] == pytest.approx(32.0)
155
+ assert snap["stale_after_s"] == pytest.approx(30.0)
156
+
157
+
158
+ def test_falls_back_to_latest_previous_log(tmp_path):
159
+ """当天文件还没生成(刚过 0 点/客户端未启动)时读最近一份。"""
160
+ _write_log(tmp_path, "20261108", [
161
+ _line("2026-11-08 15:00:00", KEY, "88.0", "8.0", "80.0"),
162
+ ])
163
+ reader = QmtAssetLogReader(
164
+ str(tmp_path), clock=_Clock(_epoch("20261109", "00:00:10"))
165
+ )
166
+
167
+ snap = reader.get_asset(ACC)
168
+ assert snap["total_asset"] == pytest.approx(88.0)
169
+ assert snap["stale"] is True
170
+
171
+
172
+ def test_missing_directory_raises(tmp_path):
173
+ with pytest.raises(FileNotFoundError):
174
+ QmtAssetLogReader(str(tmp_path / "nope"))
175
+
176
+
177
+ def test_truncated_file_is_reread(tmp_path):
178
+ date = "20261109"
179
+ path = _write_log(tmp_path, date, [
180
+ _line("2026-11-09 17:12:11", KEY, "100.0", "50.0", "50.0"),
181
+ ])
182
+ reader = QmtAssetLogReader(str(tmp_path), clock=_Clock(_epoch(date, "17:12:12")))
183
+ assert reader.get_asset(ACC)["total_asset"] == pytest.approx(100.0)
184
+
185
+ with open(path, "w", encoding="utf-8") as f: # 模拟日志被重写/清空
186
+ f.write(_line("2026-11-09 17:12:20", KEY, "500.0", "200.0", "300.0"))
187
+
188
+ assert reader.get_asset(ACC)["total_asset"] == pytest.approx(500.0)
189
+
190
+ # ── server 端初始化 ──────────────────────────────────────────────────────────
191
+
192
+ def test_server_infers_log_dir_from_userdata_path(tmp_path, monkeypatch):
193
+ import logging
194
+ from xqshare import server
195
+
196
+ monkeypatch.setattr(server, "logger", logging.getLogger("test.assetlog"))
197
+ (tmp_path / "userdata" / "log").mkdir(parents=True)
198
+ monkeypatch.setenv("QMT_USERDATA_PATH", str(tmp_path / "userdata_mini"))
199
+ monkeypatch.delenv("QMT_LOG_DIR", raising=False)
200
+ server.XtQuantService._assetlog_reader = None
201
+ try:
202
+ server._init_assetlog_reader()
203
+ assert isinstance(server.XtQuantService._assetlog_reader, QmtAssetLogReader)
204
+ assert server.XtQuantService._assetlog_dir == str(tmp_path / "userdata" / "log")
205
+ finally:
206
+ server.XtQuantService._assetlog_reader = None
207
+ server.XtQuantService._assetlog_dir = None
208
+ server.XtQuantService._assetlog_error = None
209
+
210
+
211
+ def test_server_reports_missing_log_dir(tmp_path, monkeypatch):
212
+ import logging
213
+ from xqshare import server
214
+
215
+ monkeypatch.setattr(server, "logger", logging.getLogger("test.assetlog"))
216
+ monkeypatch.delenv("QMT_LOG_DIR", raising=False)
217
+ monkeypatch.setenv("QMT_LOG_DIR", str(tmp_path / "nope"))
218
+ server.XtQuantService._assetlog_reader = None
219
+ try:
220
+ server._init_assetlog_reader()
221
+ assert server.XtQuantService._assetlog_reader is None
222
+ assert "不存在" in server.XtQuantService._assetlog_error
223
+ finally:
224
+ server.XtQuantService._assetlog_reader = None
225
+ server.XtQuantService._assetlog_dir = None
226
+ server.XtQuantService._assetlog_error = None