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.
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/PKG-INFO +64 -16
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/README.md +63 -15
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/pyproject.toml +1 -1
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_client_permission.py +2 -2
- xtquant_share-1.4.2/tests/test_qmt_assetlog.py +226 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server.py +8 -8
- xtquant_share-1.4.2/tests/test_server_dataframe_serialization.py +60 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/.env.example +8 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/__init__.py +3 -1
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/auth.py +6 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/check_env.py +1 -1
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/client.py +32 -2
- xtquant_share-1.4.2/xqshare/qmt_assetlog/__init__.py +26 -0
- xtquant_share-1.4.2/xqshare/qmt_assetlog/reader.py +275 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/server.py +117 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/tools/xtdata.py +2 -2
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/PKG-INFO +64 -16
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/SOURCES.txt +4 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/LICENSE +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/setup.cfg +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_auth.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_channels_bigqmt.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_channels_health.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_channels_router.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_client.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_integration.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_qmt_datadir_factor.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_quote_events.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_remote_data.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server_channel_routing.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server_download_history_data2_compat.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server_download_timeout.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_server_permission.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_tools_common.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/tests/test_trader_events.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/channels/__init__.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/channels/bigqmt.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/channels/health.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/channels/router.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/clients.yaml.example +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/__init__.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/divid.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/factor.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/kline.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/misc.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/reader.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_datadir/sector.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/qmt_watchdog.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/tools/__init__.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/tools/common.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xqshare/tools/xttrader.py +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/dependency_links.txt +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/entry_points.txt +0 -0
- {xtquant_share-1.4.1 → xtquant_share-1.4.2}/xtquant_share.egg-info/requires.txt +0 -0
- {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.
|
|
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="
|
|
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="
|
|
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
|
|
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
|
|
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("
|
|
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="
|
|
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("
|
|
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="
|
|
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("
|
|
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("
|
|
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("
|
|
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="
|
|
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="
|
|
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
|
|
783
|
-
telnet
|
|
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="
|
|
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="
|
|
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
|
|
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
|
|
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("
|
|
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="
|
|
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("
|
|
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="
|
|
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("
|
|
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("
|
|
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("
|
|
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="
|
|
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="
|
|
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
|
|
741
|
-
telnet
|
|
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.
|
|
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": "
|
|
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 == "
|
|
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
|