xtquant-share 1.2.6__tar.gz → 1.2.8__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.2.6 → xtquant_share-1.2.8}/PKG-INFO +3 -1
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/pyproject.toml +5 -2
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/tests/test_server.py +1 -1
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xqshare/auth.py +3 -3
- xtquant_share-1.2.8/xqshare/clients.yaml.example +69 -0
- xtquant_share-1.2.8/xqshare/qmt_datadir/__init__.py +73 -0
- xtquant_share-1.2.8/xqshare/qmt_datadir/divid.py +129 -0
- xtquant_share-1.2.8/xqshare/qmt_datadir/kline.py +136 -0
- xtquant_share-1.2.8/xqshare/qmt_datadir/misc.py +104 -0
- xtquant_share-1.2.8/xqshare/qmt_datadir/reader.py +169 -0
- xtquant_share-1.2.8/xqshare/qmt_datadir/sector.py +111 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xqshare/server.py +33 -14
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xtquant_share.egg-info/PKG-INFO +3 -1
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xtquant_share.egg-info/SOURCES.txt +7 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xtquant_share.egg-info/requires.txt +3 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/LICENSE +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/README.md +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/setup.cfg +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/tests/test_auth.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/tests/test_client.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/tests/test_client_permission.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/tests/test_integration.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/tests/test_server_permission.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/tests/test_tools_common.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xqshare/__init__.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xqshare/client.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xqshare/tools/__init__.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xqshare/tools/common.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xqshare/tools/xtdata.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xqshare/tools/xttrader.py +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xtquant_share.egg-info/dependency_links.txt +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/xtquant_share.egg-info/entry_points.txt +0 -0
- {xtquant_share-1.2.6 → xtquant_share-1.2.8}/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.2.
|
|
3
|
+
Version: 1.2.8
|
|
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
|
|
@@ -32,6 +32,8 @@ Provides-Extra: dev
|
|
|
32
32
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
33
|
Requires-Dist: black>=23.0; extra == "dev"
|
|
34
34
|
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
35
|
+
Provides-Extra: datadir
|
|
36
|
+
Requires-Dist: pandas>=1.0; extra == "datadir"
|
|
35
37
|
Dynamic: license-file
|
|
36
38
|
|
|
37
39
|
# 极限量化 (xqshare)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "xtquant-share"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.8"
|
|
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"
|
|
@@ -50,6 +50,9 @@ dev = [
|
|
|
50
50
|
"black>=23.0",
|
|
51
51
|
"flake8>=6.0",
|
|
52
52
|
]
|
|
53
|
+
datadir = [
|
|
54
|
+
"pandas>=1.0",
|
|
55
|
+
]
|
|
53
56
|
|
|
54
57
|
[project.scripts]
|
|
55
58
|
xqshare-server = "xqshare.server:main"
|
|
@@ -67,4 +70,4 @@ where = ["."]
|
|
|
67
70
|
include = ["xqshare*"]
|
|
68
71
|
|
|
69
72
|
[tool.setuptools.package-data]
|
|
70
|
-
"*" = ["*.md", "*.txt"]
|
|
73
|
+
"*" = ["*.md", "*.txt", "*.yaml", "*.example"]
|
|
@@ -278,7 +278,7 @@ class TestExposedGetDatadir:
|
|
|
278
278
|
service = self._make_service()
|
|
279
279
|
|
|
280
280
|
mock_reader = Mock()
|
|
281
|
-
mock_reader.__class__.__module__ = '
|
|
281
|
+
mock_reader.__class__.__module__ = 'xqshare.qmt_datadir.reader'
|
|
282
282
|
|
|
283
283
|
# 注入 server 级单例
|
|
284
284
|
original = XtQuantService._datadir_reader
|
|
@@ -217,8 +217,7 @@ class PermissionChecker:
|
|
|
217
217
|
logging.info(f"[配置加载] 有效账号数量: {len(self._clients)}")
|
|
218
218
|
except Exception as e:
|
|
219
219
|
# 配置加载失败,使用默认客户端
|
|
220
|
-
|
|
221
|
-
logging.warning(f"加载客户端配置失败: {e},使用默认客户端")
|
|
220
|
+
logging.warning(f"[配置加载] clients.yaml 解析失败: {e},使用默认客户端。请检查 YAML 语法或参考 clients.yaml.example 修正配置")
|
|
222
221
|
self._create_default_client()
|
|
223
222
|
|
|
224
223
|
def _create_default_client(self) -> None:
|
|
@@ -229,7 +228,8 @@ class PermissionChecker:
|
|
|
229
228
|
level=AccountLevel.ENTERPRISE
|
|
230
229
|
)
|
|
231
230
|
import logging
|
|
232
|
-
logging.warning(f"[配置加载]
|
|
231
|
+
logging.warning(f"[配置加载] 未找到有效的 clients.yaml,已启用默认账号: {DEFAULT_CLIENT_ID} (level=enterprise)。")
|
|
232
|
+
logging.warning(f"[配置加载] 如需自定义客户端,请编辑当前目录下的 clients.yaml(可参考 clients.yaml.example 模板)")
|
|
233
233
|
|
|
234
234
|
def check_and_reload_if_changed(self) -> bool:
|
|
235
235
|
"""
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# xqshare 客户端配置示例
|
|
2
|
+
#
|
|
3
|
+
# 复制此文件为 clients.yaml 并修改配置
|
|
4
|
+
# cp clients.yaml.example clients.yaml
|
|
5
|
+
#
|
|
6
|
+
# 配置说明:
|
|
7
|
+
# - secret: 客户端认证密钥
|
|
8
|
+
# - level: 账号等级,决定可访问的 API 权限
|
|
9
|
+
#
|
|
10
|
+
# 权限等级说明:
|
|
11
|
+
# ┌─────────────┬───────────────────────────────────────────────────────────┐
|
|
12
|
+
# │ 等级 │ 权限说明 │
|
|
13
|
+
# ├─────────────┼───────────────────────────────────────────────────────────┤
|
|
14
|
+
# │ free │ 基础信息 + 日线数据 │
|
|
15
|
+
# │ plus │ 基础信息 + 日线 + 分钟线数据 │
|
|
16
|
+
# │ standard │ 基础信息 + 日线 + 分钟线 + 实时行情 + 回调功能 │
|
|
17
|
+
# │ premium │ standard + 交易查询(持仓、资产、委托等) │
|
|
18
|
+
# │ enterprise │ premium + 完整交易(下单、撤单) │
|
|
19
|
+
# └─────────────┴───────────────────────────────────────────────────────────┘
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# 安全建议:
|
|
23
|
+
# - 生产环境请使用强密码(建议 32 位以上随机字符串)
|
|
24
|
+
# - 定期更换密钥
|
|
25
|
+
# - 不同客户端使用不同密钥
|
|
26
|
+
|
|
27
|
+
clients:
|
|
28
|
+
# ==================== 免费用户 ====================
|
|
29
|
+
# 权限:基础信息 + 日线数据
|
|
30
|
+
free-user:
|
|
31
|
+
secret: "free-secret-change-me"
|
|
32
|
+
level: free
|
|
33
|
+
|
|
34
|
+
# ==================== 进阶用户 ====================
|
|
35
|
+
# 权限:基础信息 + 日线 + 分钟线数据
|
|
36
|
+
plus-user:
|
|
37
|
+
secret: "plus-secret-change-me"
|
|
38
|
+
level: plus
|
|
39
|
+
|
|
40
|
+
# ==================== 标准用户 ====================
|
|
41
|
+
# 权限:基础信息 + 日线 + 分钟线 + 实时行情 + 回调功能
|
|
42
|
+
standard-user:
|
|
43
|
+
secret: "standard-secret-change-me"
|
|
44
|
+
level: standard
|
|
45
|
+
|
|
46
|
+
# ==================== 高级用户 ====================
|
|
47
|
+
# 权限:standard + 交易查询(持仓、资产、委托等)
|
|
48
|
+
# 注意:不能下单、撤单
|
|
49
|
+
premium-user:
|
|
50
|
+
secret: "premium-secret-change-me"
|
|
51
|
+
level: premium
|
|
52
|
+
|
|
53
|
+
# ==================== 企业用户 ====================
|
|
54
|
+
# 权限:premium + 完整交易(下单、撤单)
|
|
55
|
+
# 拥有所有权限
|
|
56
|
+
enterprise-user:
|
|
57
|
+
secret: "enterprise-secret-change-me"
|
|
58
|
+
level: enterprise
|
|
59
|
+
|
|
60
|
+
# ==================== 自定义示例 ====================
|
|
61
|
+
# 示例:内部测试账号
|
|
62
|
+
# internal-dev:
|
|
63
|
+
# secret: "dev-only-secret"
|
|
64
|
+
# level: enterprise
|
|
65
|
+
|
|
66
|
+
# 示例:只读分析账号
|
|
67
|
+
# readonly-analyst:
|
|
68
|
+
# secret: "analyst-secret"
|
|
69
|
+
# level: standard
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qmt_datadir —— QMT 本地 datadir 解析包
|
|
3
|
+
|
|
4
|
+
支持的数据类型:
|
|
5
|
+
1. K线数据 SH/SZ/{period}/*.DAT
|
|
6
|
+
2. 板块成分股 Sector/Temple/{分类}/{板块名}
|
|
7
|
+
3. 指数权重 Weight/systemSectorWeightData.txt
|
|
8
|
+
4. 分红除权 DividData/*.ldb(LevelDB SSTable 直接扫描)
|
|
9
|
+
5. ETF成分股列表 TradeDateAndETFStockListCache
|
|
10
|
+
6. 交易所列表 marketlistinfo(Base64)
|
|
11
|
+
7. 涨跌幅快照元数据 increase/{market}(ZiPeDiT+zlib 压缩位图)
|
|
12
|
+
|
|
13
|
+
快速使用:
|
|
14
|
+
from xqshare.qmt_datadir import QmtDataReader
|
|
15
|
+
|
|
16
|
+
reader = QmtDataReader('~/Downloads/pazq_qmt/.../userdata_mini/datadir')
|
|
17
|
+
df = reader.kline('600000.SH', '1d')
|
|
18
|
+
|
|
19
|
+
底层函数直接使用:
|
|
20
|
+
from xqshare.qmt_datadir import read_kline_dir, read_divid, read_all_sectors
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# ── 统一入口类 ────────────────────────────────────────────────────────────────
|
|
24
|
+
from .reader import QmtDataReader
|
|
25
|
+
|
|
26
|
+
# ── K线 ──────────────────────────────────────────────────────────────────────
|
|
27
|
+
from .kline import (
|
|
28
|
+
read_kline,
|
|
29
|
+
read_kline_dir,
|
|
30
|
+
FILE_HEADER_SIZE,
|
|
31
|
+
RECORD_SIZE,
|
|
32
|
+
PERIOD_MAP,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# ── 板块 & 权重 ───────────────────────────────────────────────────────────────
|
|
36
|
+
from .sector import (
|
|
37
|
+
read_sector,
|
|
38
|
+
read_all_sectors,
|
|
39
|
+
list_sector_categories,
|
|
40
|
+
read_weight,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# ── 分红除权 ──────────────────────────────────────────────────────────────────
|
|
44
|
+
from .divid import read_divid
|
|
45
|
+
|
|
46
|
+
# ── 杂项 ──────────────────────────────────────────────────────────────────────
|
|
47
|
+
from .misc import (
|
|
48
|
+
read_etf_list,
|
|
49
|
+
read_market_list,
|
|
50
|
+
read_increase_meta,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
# 统一入口
|
|
55
|
+
'QmtDataReader',
|
|
56
|
+
# K线
|
|
57
|
+
'read_kline',
|
|
58
|
+
'read_kline_dir',
|
|
59
|
+
'FILE_HEADER_SIZE',
|
|
60
|
+
'RECORD_SIZE',
|
|
61
|
+
'PERIOD_MAP',
|
|
62
|
+
# 板块 & 权重
|
|
63
|
+
'read_sector',
|
|
64
|
+
'read_all_sectors',
|
|
65
|
+
'list_sector_categories',
|
|
66
|
+
'read_weight',
|
|
67
|
+
# 分红除权
|
|
68
|
+
'read_divid',
|
|
69
|
+
# 杂项
|
|
70
|
+
'read_etf_list',
|
|
71
|
+
'read_market_list',
|
|
72
|
+
'read_increase_meta',
|
|
73
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
分红除权数据解析模块
|
|
3
|
+
|
|
4
|
+
数据来源:datadir/DividData/*.ldb(LevelDB SSTable 格式)
|
|
5
|
+
|
|
6
|
+
解析方式:直接扫描 .ldb 文件中的可读字符串,无需安装 LevelDB 库。
|
|
7
|
+
|
|
8
|
+
key 格式:{exchange}|{code}|4000|{除权日时间戳ms}
|
|
9
|
+
value 结构:
|
|
10
|
+
offset 16: int64 除权日时间戳(ms)
|
|
11
|
+
offset 24: float64 分红比例(每股分红金额,元)
|
|
12
|
+
|
|
13
|
+
注意:
|
|
14
|
+
- ratio 极小值(< 0.01)是 LevelDB 扫描误命中的浮点数噪声,已过滤
|
|
15
|
+
- 可转债代码(114xxx/127xxx)的 ratio 可能是转股价(10~30),属正常值
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import struct
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
_KEY_PATTERN = re.compile(rb'[A-Z]{2}\|[0-9]{6}\|[0-9]+\|[0-9]+')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_divid(data_dir: str,
|
|
30
|
+
exchange: Optional[str] = None,
|
|
31
|
+
code: Optional[str] = None) -> pd.DataFrame:
|
|
32
|
+
"""
|
|
33
|
+
读取分红除权数据(DividData LevelDB)。
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
data_dir: datadir 根目录
|
|
37
|
+
exchange: 可选过滤,如 'SH'/'SZ'/'BJ'
|
|
38
|
+
code: 可选过滤,如 '600000'(纯6位代码,不含交易所后缀)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
DataFrame,columns:
|
|
42
|
+
exchange str 交易所代码
|
|
43
|
+
code str 股票代码(6位)
|
|
44
|
+
symbol str 完整代码,如 '600000.SH'
|
|
45
|
+
ex_date datetime 除权日
|
|
46
|
+
ratio float 每股分红金额(元)
|
|
47
|
+
raw_ts int key 中的原始时间戳(ms)
|
|
48
|
+
"""
|
|
49
|
+
div_dir = os.path.join(data_dir, 'DividData')
|
|
50
|
+
if not os.path.isdir(div_dir):
|
|
51
|
+
raise FileNotFoundError(f"DividData 目录不存在: {div_dir}")
|
|
52
|
+
|
|
53
|
+
# 构建前缀过滤(加速扫描)
|
|
54
|
+
prefix = b''
|
|
55
|
+
if exchange and code:
|
|
56
|
+
prefix = f'{exchange}|{code}|'.encode()
|
|
57
|
+
elif exchange:
|
|
58
|
+
prefix = f'{exchange}|'.encode()
|
|
59
|
+
elif code:
|
|
60
|
+
prefix = f'|{code}|'.encode()
|
|
61
|
+
|
|
62
|
+
records = []
|
|
63
|
+
for fname in sorted(os.listdir(div_dir)):
|
|
64
|
+
if not fname.endswith('.ldb'):
|
|
65
|
+
continue
|
|
66
|
+
fpath = os.path.join(div_dir, fname)
|
|
67
|
+
data = open(fpath, 'rb').read()
|
|
68
|
+
|
|
69
|
+
for m in _KEY_PATTERN.finditer(data):
|
|
70
|
+
k = m.group()
|
|
71
|
+
if prefix and prefix not in k:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
parts = k.decode().split('|')
|
|
75
|
+
if len(parts) != 4:
|
|
76
|
+
continue
|
|
77
|
+
ex, cd, _, ts_str = parts
|
|
78
|
+
|
|
79
|
+
if exchange and ex != exchange:
|
|
80
|
+
continue
|
|
81
|
+
if code and cd != code:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
raw_ts = int(ts_str)
|
|
85
|
+
val = data[m.end(): m.end() + 80]
|
|
86
|
+
if len(val) < 32:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# offset 16: int64 ms 时间戳(除权日)
|
|
90
|
+
ex_ts = struct.unpack_from('<q', val, 16)[0]
|
|
91
|
+
# offset 24: float64 分红比例
|
|
92
|
+
ratio = struct.unpack_from('<d', val, 24)[0]
|
|
93
|
+
|
|
94
|
+
# 时间戳合理性校验
|
|
95
|
+
if not (1e12 < ex_ts < 2e12):
|
|
96
|
+
if raw_ts > 1e12:
|
|
97
|
+
ex_ts = raw_ts
|
|
98
|
+
else:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# 过滤噪声:极小浮点数是 LevelDB 扫描误命中
|
|
102
|
+
if not (0.01 <= ratio < 100):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
ex_date = datetime.fromtimestamp(ex_ts / 1000)
|
|
107
|
+
if not (2000 <= ex_date.year <= 2030):
|
|
108
|
+
continue
|
|
109
|
+
except Exception:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
records.append({
|
|
113
|
+
'exchange': ex,
|
|
114
|
+
'code': cd,
|
|
115
|
+
'symbol': f'{cd}.{ex}',
|
|
116
|
+
'ex_date': ex_date,
|
|
117
|
+
'ratio': ratio,
|
|
118
|
+
'raw_ts': raw_ts,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if not records:
|
|
122
|
+
return pd.DataFrame(columns=['exchange', 'code', 'symbol',
|
|
123
|
+
'ex_date', 'ratio', 'raw_ts'])
|
|
124
|
+
|
|
125
|
+
df = pd.DataFrame(records)
|
|
126
|
+
df['ex_date'] = pd.to_datetime(df['ex_date'])
|
|
127
|
+
df = df.drop_duplicates(subset=['symbol', 'ex_date'])
|
|
128
|
+
df = df.sort_values(['symbol', 'ex_date']).reset_index(drop=True)
|
|
129
|
+
return df
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
K线数据解析模块
|
|
3
|
+
|
|
4
|
+
QMT .DAT 文件格式(逆向工程确认):
|
|
5
|
+
- 文件头:8 字节,魔数 0xFEFFFFFFFFFFFF7F
|
|
6
|
+
- 每条记录:64 字节,小端序
|
|
7
|
+
offset 0: uint32 Unix 秒时间戳(本地时间)
|
|
8
|
+
offset 4: uint32 open × 100
|
|
9
|
+
offset 8: uint32 high × 100
|
|
10
|
+
offset 12: uint32 low × 100
|
|
11
|
+
offset 16: uint32 close × 100
|
|
12
|
+
offset 20: uint32 (保留,始终0)
|
|
13
|
+
offset 24: uint32 volume(手)
|
|
14
|
+
offset 28: uint32 (保留,始终0)
|
|
15
|
+
offset 32: int64 amount(元)
|
|
16
|
+
offset 40: uint32 (保留)
|
|
17
|
+
offset 44: float32 当日复权因子(除权当天 ≠ 1.0)
|
|
18
|
+
offset 48: float32 累计复权因子(从上市至今)
|
|
19
|
+
offset 52: uint32 pre_close × 100
|
|
20
|
+
offset 56: uint32 (保留,始终0)
|
|
21
|
+
offset 60: uint32 (保留,固定 32764)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import struct
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
28
|
+
import pandas as pd
|
|
29
|
+
|
|
30
|
+
# ── 常量 ──────────────────────────────────────────────────────────────────────
|
|
31
|
+
FILE_HEADER_SIZE = 8
|
|
32
|
+
RECORD_SIZE = 64
|
|
33
|
+
FILE_MAGIC = 0x7FFFFFFFFFFFFFFE # int64 小端
|
|
34
|
+
|
|
35
|
+
PERIOD_MAP = {
|
|
36
|
+
'1d': '86400', 'day': '86400', 'd': '86400', '86400': '86400',
|
|
37
|
+
'5m': '300', '5min': '300', '300': '300',
|
|
38
|
+
'1m': '60', '1min': '60', '60': '60',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_record(rec: bytes) -> dict:
|
|
43
|
+
"""解析单条 64 字节 K 线记录,返回字段字典"""
|
|
44
|
+
ts = struct.unpack_from('<I', rec, 0)[0]
|
|
45
|
+
open_x100 = struct.unpack_from('<I', rec, 4)[0]
|
|
46
|
+
high_x100 = struct.unpack_from('<I', rec, 8)[0]
|
|
47
|
+
low_x100 = struct.unpack_from('<I', rec, 12)[0]
|
|
48
|
+
close_x100 = struct.unpack_from('<I', rec, 16)[0]
|
|
49
|
+
volume = struct.unpack_from('<I', rec, 24)[0]
|
|
50
|
+
amount = struct.unpack_from('<q', rec, 32)[0]
|
|
51
|
+
adj_factor = struct.unpack_from('<f', rec, 44)[0]
|
|
52
|
+
cum_adj_factor = struct.unpack_from('<f', rec, 48)[0]
|
|
53
|
+
pre_close_x100 = struct.unpack_from('<I', rec, 52)[0]
|
|
54
|
+
return {
|
|
55
|
+
'timestamp': ts,
|
|
56
|
+
'datetime': datetime.fromtimestamp(ts),
|
|
57
|
+
'open': open_x100 / 100.0,
|
|
58
|
+
'high': high_x100 / 100.0,
|
|
59
|
+
'low': low_x100 / 100.0,
|
|
60
|
+
'close': close_x100 / 100.0,
|
|
61
|
+
'volume': volume,
|
|
62
|
+
'amount': amount,
|
|
63
|
+
'pre_close': pre_close_x100 / 100.0,
|
|
64
|
+
'adj_factor': adj_factor, # 当日复权因子(除权当天 ≠ 1.0)
|
|
65
|
+
'cum_adj_factor': cum_adj_factor, # 累计复权因子(从上市至今)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def read_kline(filepath: str, include_raw_ts: bool = False) -> pd.DataFrame:
|
|
70
|
+
"""
|
|
71
|
+
读取 QMT .DAT K线文件,返回 DataFrame。
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
filepath: .DAT 文件绝对路径
|
|
75
|
+
include_raw_ts: 是否保留原始 Unix 时间戳列(timestamp)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
DataFrame,index 为 DatetimeIndex(本地时间),
|
|
79
|
+
columns: open, high, low, close, volume, amount,
|
|
80
|
+
pre_close, adj_factor, cum_adj_factor
|
|
81
|
+
"""
|
|
82
|
+
if not os.path.exists(filepath):
|
|
83
|
+
raise FileNotFoundError(f"文件不存在: {filepath}")
|
|
84
|
+
|
|
85
|
+
with open(filepath, 'rb') as f:
|
|
86
|
+
f.read(FILE_HEADER_SIZE)
|
|
87
|
+
data = f.read()
|
|
88
|
+
|
|
89
|
+
n = len(data) // RECORD_SIZE
|
|
90
|
+
if n == 0:
|
|
91
|
+
return pd.DataFrame()
|
|
92
|
+
|
|
93
|
+
records = []
|
|
94
|
+
for i in range(n):
|
|
95
|
+
rec = data[i * RECORD_SIZE: (i + 1) * RECORD_SIZE]
|
|
96
|
+
if len(rec) < RECORD_SIZE:
|
|
97
|
+
break
|
|
98
|
+
try:
|
|
99
|
+
records.append(_parse_record(rec))
|
|
100
|
+
except Exception:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if not records:
|
|
104
|
+
return pd.DataFrame()
|
|
105
|
+
|
|
106
|
+
df = pd.DataFrame(records)
|
|
107
|
+
df['datetime'] = pd.to_datetime(df['datetime'])
|
|
108
|
+
df = df.set_index('datetime')
|
|
109
|
+
df.index.name = 'datetime'
|
|
110
|
+
df = df[(df['close'] > 0) & (df.index.year >= 1990)]
|
|
111
|
+
|
|
112
|
+
if not include_raw_ts:
|
|
113
|
+
df = df.drop(columns=['timestamp'], errors='ignore')
|
|
114
|
+
|
|
115
|
+
return df
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def read_kline_dir(data_dir: str, symbol: str, period: str,
|
|
119
|
+
include_raw_ts: bool = False) -> pd.DataFrame:
|
|
120
|
+
"""
|
|
121
|
+
通过股票代码和周期读取 K线数据。
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
data_dir: datadir 根目录
|
|
125
|
+
symbol: 股票代码,如 '600000.SH' 或 '000001.SZ'
|
|
126
|
+
period: 周期,如 '1d'/'86400'/'5m'/'300'/'1m'/'60'
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
DataFrame(同 read_kline)
|
|
130
|
+
"""
|
|
131
|
+
code, exchange = (symbol.rsplit('.', 1) if '.' in symbol
|
|
132
|
+
else (symbol, 'SH'))
|
|
133
|
+
exchange = exchange.upper()
|
|
134
|
+
period_dir = PERIOD_MAP.get(period.lower(), period)
|
|
135
|
+
filepath = os.path.join(data_dir, exchange, period_dir, f'{code}.DAT')
|
|
136
|
+
return read_kline(filepath, include_raw_ts=include_raw_ts)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
杂项数据解析模块
|
|
3
|
+
|
|
4
|
+
包含:
|
|
5
|
+
- ETF 成分股列表(TradeDateAndETFStockListCache)
|
|
6
|
+
- 交易所列表(marketlistinfo,Base64 编码)
|
|
7
|
+
- 涨跌幅快照元数据(increase/,ZiPeDiT+zlib 压缩位图)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import struct
|
|
14
|
+
import zlib
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read_etf_list(data_dir: str) -> list:
|
|
18
|
+
"""
|
|
19
|
+
从 TradeDateAndETFStockListCache 读取 ETF 成分股代码列表。
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
list[str],纯6位代码(不含交易所后缀),如 ['510010', '600009', ...]
|
|
23
|
+
"""
|
|
24
|
+
cache_path = os.path.join(data_dir, 'TradeDateAndETFStockListCache')
|
|
25
|
+
if not os.path.exists(cache_path):
|
|
26
|
+
raise FileNotFoundError(f"缓存文件不存在: {cache_path}")
|
|
27
|
+
|
|
28
|
+
data = open(cache_path, 'rb').read()
|
|
29
|
+
idx = data.find(b'etflist')
|
|
30
|
+
if idx < 0:
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
after = data[idx + len(b'etflist'):]
|
|
34
|
+
codes_raw = re.findall(rb'(?<![0-9])[0-9]{6}(?![0-9])', after[:100000])
|
|
35
|
+
seen = set()
|
|
36
|
+
codes = []
|
|
37
|
+
for c in codes_raw:
|
|
38
|
+
s = c.decode()
|
|
39
|
+
if s not in seen:
|
|
40
|
+
seen.add(s)
|
|
41
|
+
codes.append(s)
|
|
42
|
+
return codes
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_market_list(data_dir: str) -> list:
|
|
46
|
+
"""
|
|
47
|
+
读取支持的交易所列表(marketlistinfo,Base64 编码)。
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
list[str],如 ['BJ', 'BKZS', 'DF', 'GF', 'HGT', 'IF',
|
|
51
|
+
'INE', 'SF', 'SGT', 'SH', 'SHO', 'SZ', 'SZO', 'ZF']
|
|
52
|
+
"""
|
|
53
|
+
fp = os.path.join(data_dir, 'marketlistinfo')
|
|
54
|
+
if not os.path.exists(fp):
|
|
55
|
+
raise FileNotFoundError(f"文件不存在: {fp}")
|
|
56
|
+
|
|
57
|
+
raw = open(fp, 'rb').read()
|
|
58
|
+
decoded = base64.b64decode(raw.strip()).decode('ascii')
|
|
59
|
+
return [m for m in decoded.strip(',').split(',') if m]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_increase_meta(data_dir: str, market: str = 'SH') -> dict:
|
|
63
|
+
"""
|
|
64
|
+
读取涨跌幅快照文件的元数据(文件头信息)。
|
|
65
|
+
|
|
66
|
+
注意:increase/ 文件内部是 ZiPeDiT+zlib 压缩位图,完整解析需要配合
|
|
67
|
+
股票列表,此函数仅返回文件头元数据,不解析位图内容。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data_dir: datadir 根目录
|
|
71
|
+
market: 市场代码,如 'SH'/'SZ'/'BJ'
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
dict,字段:
|
|
75
|
+
market str 市场代码
|
|
76
|
+
file_size int 原始文件大小(字节)
|
|
77
|
+
n_dates int 日期数量(文件头 offset 8,uint16)
|
|
78
|
+
n_stocks int 股票数量(文件头 offset 10,uint16)
|
|
79
|
+
decompressed_size int 解压后大小(字节),解压失败时为 0
|
|
80
|
+
"""
|
|
81
|
+
fp = os.path.join(data_dir, 'increase', market)
|
|
82
|
+
if not os.path.exists(fp):
|
|
83
|
+
raise FileNotFoundError(f"文件不存在: {fp}")
|
|
84
|
+
|
|
85
|
+
raw = open(fp, 'rb').read()
|
|
86
|
+
n_dates = struct.unpack_from('<H', raw, 8)[0]
|
|
87
|
+
n_stocks = struct.unpack_from('<H', raw, 10)[0]
|
|
88
|
+
|
|
89
|
+
decompressed_size = 0
|
|
90
|
+
idx = raw.find(b'ZiPeDiT')
|
|
91
|
+
if idx >= 0:
|
|
92
|
+
try:
|
|
93
|
+
dec = zlib.decompress(raw[idx + 7:])
|
|
94
|
+
decompressed_size = len(dec)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
'market': market,
|
|
100
|
+
'file_size': len(raw),
|
|
101
|
+
'n_dates': n_dates,
|
|
102
|
+
'n_stocks': n_stocks,
|
|
103
|
+
'decompressed_size': decompressed_size,
|
|
104
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QmtDataReader —— QMT 本地 datadir 统一读取入口
|
|
3
|
+
|
|
4
|
+
将所有子模块的功能聚合为一个易用的类接口。
|
|
5
|
+
|
|
6
|
+
用法示例:
|
|
7
|
+
from xqshare.qmt_datadir import QmtDataReader
|
|
8
|
+
|
|
9
|
+
reader = QmtDataReader('~/Downloads/pazq_qmt/.../userdata_mini/datadir')
|
|
10
|
+
|
|
11
|
+
# K线
|
|
12
|
+
df = reader.kline('600000.SH', '1d')
|
|
13
|
+
df5 = reader.kline('600000.SH', '5m')
|
|
14
|
+
|
|
15
|
+
# 板块
|
|
16
|
+
sw = reader.sectors('申万行业') # {板块名: [代码列表]}
|
|
17
|
+
bank = reader.sector('申万行业', 'SW1银行')
|
|
18
|
+
|
|
19
|
+
# 权重
|
|
20
|
+
wt = reader.weight() # {板块名: [{'symbol':..., 'weight':...}]}
|
|
21
|
+
|
|
22
|
+
# 分红除权
|
|
23
|
+
divid = reader.divid(exchange='SH', code='600000')
|
|
24
|
+
|
|
25
|
+
# ETF / 交易所 / 涨跌幅元数据
|
|
26
|
+
etfs = reader.etf_list()
|
|
27
|
+
markets = reader.market_list()
|
|
28
|
+
meta = reader.increase_meta('SH')
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
import pandas as pd
|
|
35
|
+
|
|
36
|
+
from .kline import read_kline, read_kline_dir
|
|
37
|
+
from .sector import (list_sector_categories, read_all_sectors,
|
|
38
|
+
read_sector, read_weight)
|
|
39
|
+
from .divid import read_divid
|
|
40
|
+
from .misc import read_etf_list, read_market_list, read_increase_meta
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class QmtDataReader:
|
|
44
|
+
"""
|
|
45
|
+
QMT 本地 datadir 统一读取器。
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data_dir: userdata_mini/datadir 的绝对路径或 ~ 开头的路径
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, data_dir: str):
|
|
52
|
+
self.data_dir = os.path.expanduser(data_dir)
|
|
53
|
+
if not os.path.isdir(self.data_dir):
|
|
54
|
+
raise FileNotFoundError(f"datadir 不存在: {self.data_dir}")
|
|
55
|
+
|
|
56
|
+
# ── K线 ──────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
def kline(self, symbol: str, period: str = '1d',
|
|
59
|
+
include_raw_ts: bool = False) -> pd.DataFrame:
|
|
60
|
+
"""
|
|
61
|
+
读取 K 线数据。
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
symbol: 股票代码,如 '600000.SH'
|
|
65
|
+
period: 周期,支持 '1d'/'86400'/'5m'/'300'/'1m'/'60'
|
|
66
|
+
include_raw_ts: 是否保留原始 Unix 时间戳列
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
DataFrame,index=DatetimeIndex,
|
|
70
|
+
columns: open, high, low, close, volume, amount,
|
|
71
|
+
pre_close, adj_factor, cum_adj_factor
|
|
72
|
+
"""
|
|
73
|
+
return read_kline_dir(self.data_dir, symbol, period, include_raw_ts)
|
|
74
|
+
|
|
75
|
+
# ── 板块 ─────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
def sector_categories(self) -> list:
|
|
78
|
+
"""列出所有板块分类,如 ['申万行业', '证监会行业']"""
|
|
79
|
+
return list_sector_categories(self.data_dir)
|
|
80
|
+
|
|
81
|
+
def sectors(self, category: str = '申万行业') -> dict:
|
|
82
|
+
"""
|
|
83
|
+
读取某分类下所有板块成分股。
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
dict[str, list[str]],{板块名: [代码列表]}
|
|
87
|
+
"""
|
|
88
|
+
return read_all_sectors(self.data_dir, category)
|
|
89
|
+
|
|
90
|
+
def sector(self, category: str, sector_name: str) -> list:
|
|
91
|
+
"""
|
|
92
|
+
读取单个板块成分股列表。
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
category: 板块分类,如 '申万行业'
|
|
96
|
+
sector_name: 板块名称,如 'SW1银行'
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
list[str],股票代码列表
|
|
100
|
+
"""
|
|
101
|
+
fp = os.path.join(self.data_dir, 'Sector', 'Temple',
|
|
102
|
+
category, sector_name)
|
|
103
|
+
return read_sector(fp)
|
|
104
|
+
|
|
105
|
+
# ── 权重 ─────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def weight(self) -> dict:
|
|
108
|
+
"""
|
|
109
|
+
读取指数/板块权重数据。
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
dict[str, list[dict]],{板块名: [{'symbol': str, 'weight': float}]}
|
|
113
|
+
"""
|
|
114
|
+
return read_weight(self.data_dir)
|
|
115
|
+
|
|
116
|
+
# ── 分红除权 ──────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def divid(self, exchange: Optional[str] = None,
|
|
119
|
+
code: Optional[str] = None) -> pd.DataFrame:
|
|
120
|
+
"""
|
|
121
|
+
读取分红除权数据(DividData LevelDB)。
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
exchange: 可选过滤,如 'SH'/'SZ'/'BJ'
|
|
125
|
+
code: 可选过滤,纯6位代码,如 '600000'
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
DataFrame,columns: exchange, code, symbol, ex_date, ratio, raw_ts
|
|
129
|
+
"""
|
|
130
|
+
return read_divid(self.data_dir, exchange=exchange, code=code)
|
|
131
|
+
|
|
132
|
+
# ── ETF 列表 ──────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
def etf_list(self) -> list:
|
|
135
|
+
"""
|
|
136
|
+
读取 ETF 成分股代码列表(纯6位代码)。
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
list[str],如 ['510010', '600009', ...]
|
|
140
|
+
"""
|
|
141
|
+
return read_etf_list(self.data_dir)
|
|
142
|
+
|
|
143
|
+
# ── 交易所列表 ────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
def market_list(self) -> list:
|
|
146
|
+
"""
|
|
147
|
+
读取支持的交易所列表。
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
list[str],如 ['BJ', 'SH', 'SZ', ...]
|
|
151
|
+
"""
|
|
152
|
+
return read_market_list(self.data_dir)
|
|
153
|
+
|
|
154
|
+
# ── 涨跌幅快照元数据 ──────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
def increase_meta(self, market: str = 'SH') -> dict:
|
|
157
|
+
"""
|
|
158
|
+
读取涨跌幅快照文件元数据(不解析位图内容)。
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
market: 市场代码,如 'SH'/'SZ'/'BJ'
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
dict: market, file_size, n_dates, n_stocks, decompressed_size
|
|
165
|
+
"""
|
|
166
|
+
return read_increase_meta(self.data_dir, market)
|
|
167
|
+
|
|
168
|
+
def __repr__(self) -> str:
|
|
169
|
+
return f"QmtDataReader(data_dir='{self.data_dir}')"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
板块成分股 & 指数权重解析模块
|
|
3
|
+
|
|
4
|
+
数据来源:
|
|
5
|
+
- 板块成分股:datadir/Sector/Temple/{分类}/{板块名}
|
|
6
|
+
格式:逗号分隔的股票代码,如 '000022.SZ,000088.SZ,...'
|
|
7
|
+
- 指数权重:datadir/Weight/systemSectorWeightData.txt
|
|
8
|
+
格式(每行):{板块名};{代码1},{权重1};{代码2},{权重2};...
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_sector(filepath: str) -> list:
|
|
15
|
+
"""
|
|
16
|
+
读取单个板块成分股文件,返回股票代码列表。
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
filepath: 板块文件绝对路径
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
list[str],如 ['000022.SZ', '000088.SZ', ...]
|
|
23
|
+
"""
|
|
24
|
+
content = open(filepath, 'r', encoding='utf-8', errors='replace').read()
|
|
25
|
+
return [c.strip() for c in content.split(',') if c.strip()]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_all_sectors(data_dir: str, category: str = '申万行业') -> dict:
|
|
29
|
+
"""
|
|
30
|
+
读取某分类下所有板块的成分股。
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
data_dir: datadir 根目录
|
|
34
|
+
category: 板块分类名,如 '申万行业' 或 '证监会行业'
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict[str, list[str]],板块名 → 成分股代码列表
|
|
38
|
+
如 {'SW1银行': ['600000.SH', '600015.SH', ...], ...}
|
|
39
|
+
"""
|
|
40
|
+
sector_dir = os.path.join(data_dir, 'Sector', 'Temple', category)
|
|
41
|
+
if not os.path.isdir(sector_dir):
|
|
42
|
+
raise FileNotFoundError(f"板块目录不存在: {sector_dir}")
|
|
43
|
+
|
|
44
|
+
result = {}
|
|
45
|
+
for name in sorted(os.listdir(sector_dir)):
|
|
46
|
+
fp = os.path.join(sector_dir, name)
|
|
47
|
+
if os.path.isfile(fp):
|
|
48
|
+
try:
|
|
49
|
+
result[name] = read_sector(fp)
|
|
50
|
+
except Exception:
|
|
51
|
+
result[name] = []
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def list_sector_categories(data_dir: str) -> list:
|
|
56
|
+
"""
|
|
57
|
+
列出所有可用的板块分类名称。
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
list[str],如 ['申万行业', '证监会行业']
|
|
61
|
+
"""
|
|
62
|
+
temple_dir = os.path.join(data_dir, 'Sector', 'Temple')
|
|
63
|
+
if not os.path.isdir(temple_dir):
|
|
64
|
+
return []
|
|
65
|
+
return sorted(os.listdir(temple_dir))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def read_weight(data_dir: str) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
读取指数/板块权重数据。
|
|
71
|
+
|
|
72
|
+
文件格式(每行):{板块名};{代码1},{权重1};{代码2},{权重2};...
|
|
73
|
+
如:上证A股;600051.SH,1;605090.SH,1;...
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
data_dir: datadir 根目录
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
dict[str, list[dict]],板块名 → [{'symbol': str, 'weight': float}, ...]
|
|
80
|
+
"""
|
|
81
|
+
fp = os.path.join(data_dir, 'Weight', 'systemSectorWeightData.txt')
|
|
82
|
+
if not os.path.exists(fp):
|
|
83
|
+
raise FileNotFoundError(f"权重文件不存在: {fp}")
|
|
84
|
+
|
|
85
|
+
result = {}
|
|
86
|
+
with open(fp, 'r', encoding='utf-8', errors='replace') as f:
|
|
87
|
+
for line in f:
|
|
88
|
+
line = line.strip()
|
|
89
|
+
if not line:
|
|
90
|
+
continue
|
|
91
|
+
parts = line.split(';')
|
|
92
|
+
if len(parts) < 2:
|
|
93
|
+
continue
|
|
94
|
+
sector_name = parts[0]
|
|
95
|
+
members = []
|
|
96
|
+
for item in parts[1:]:
|
|
97
|
+
item = item.strip()
|
|
98
|
+
if not item:
|
|
99
|
+
continue
|
|
100
|
+
kv = item.split(',')
|
|
101
|
+
if len(kv) == 2:
|
|
102
|
+
try:
|
|
103
|
+
members.append({
|
|
104
|
+
'symbol': kv[0].strip(),
|
|
105
|
+
'weight': float(kv[1].strip()),
|
|
106
|
+
})
|
|
107
|
+
except ValueError:
|
|
108
|
+
pass
|
|
109
|
+
if members:
|
|
110
|
+
result[sector_name] = members
|
|
111
|
+
return result
|
|
@@ -48,21 +48,11 @@ except ImportError:
|
|
|
48
48
|
|
|
49
49
|
# QmtDataReader 导入(用于 datadir 文件解析能力)
|
|
50
50
|
try:
|
|
51
|
-
from
|
|
51
|
+
from xqshare.qmt_datadir import QmtDataReader
|
|
52
52
|
QMTDATAREADER_AVAILABLE = True
|
|
53
53
|
except ImportError:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
import sys as _sys
|
|
57
|
-
import os as _os
|
|
58
|
-
_pkg_dir = _os.path.join(_os.path.dirname(__file__), '..', 'utils')
|
|
59
|
-
if _os.path.isdir(_pkg_dir):
|
|
60
|
-
_sys.path.insert(0, _os.path.dirname(_pkg_dir))
|
|
61
|
-
from utils.qmt_datadir import QmtDataReader
|
|
62
|
-
QMTDATAREADER_AVAILABLE = True
|
|
63
|
-
except ImportError:
|
|
64
|
-
QmtDataReader = None
|
|
65
|
-
QMTDATAREADER_AVAILABLE = False
|
|
54
|
+
QmtDataReader = None
|
|
55
|
+
QMTDATAREADER_AVAILABLE = False
|
|
66
56
|
|
|
67
57
|
|
|
68
58
|
# ==================== 日志配置 ====================
|
|
@@ -687,6 +677,32 @@ class XtQuantService(rpyc.Service):
|
|
|
687
677
|
return f"已启动异步回调,共 {count} 次,间隔 {delay} 秒"
|
|
688
678
|
|
|
689
679
|
|
|
680
|
+
# ==================== clients.yaml 初始化 ====================
|
|
681
|
+
|
|
682
|
+
def _init_clients_yaml():
|
|
683
|
+
"""
|
|
684
|
+
如果当前工作目录下不存在 clients.yaml,则从包目录复制示例文件。
|
|
685
|
+
|
|
686
|
+
这样用户无需手动创建配置文件,启动服务后即可得到一份带注释的模板。
|
|
687
|
+
"""
|
|
688
|
+
import shutil
|
|
689
|
+
target = os.path.join(os.getcwd(), "clients.yaml")
|
|
690
|
+
if os.path.exists(target):
|
|
691
|
+
return # 已存在,跳过
|
|
692
|
+
|
|
693
|
+
# 从包目录查找示例文件
|
|
694
|
+
example_path = os.path.join(os.path.dirname(__file__), "clients.yaml.example")
|
|
695
|
+
if not os.path.exists(example_path):
|
|
696
|
+
return # 示例文件也不存在(不应发生)
|
|
697
|
+
|
|
698
|
+
try:
|
|
699
|
+
shutil.copyfile(example_path, target)
|
|
700
|
+
print(f" \u2714 已生成 clients.yaml(从 {example_path})")
|
|
701
|
+
print(f" 请编辑此文件配置客户端账号和权限等级")
|
|
702
|
+
except Exception as e:
|
|
703
|
+
logger.warning(f"自动复制 clients.yaml.example 失败: {e}")
|
|
704
|
+
|
|
705
|
+
|
|
690
706
|
# ==================== datadir 初始化 ====================
|
|
691
707
|
|
|
692
708
|
def _init_datadir_reader():
|
|
@@ -699,7 +715,7 @@ def _init_datadir_reader():
|
|
|
699
715
|
3. 均不可用时记录 WARNING,_datadir_reader 保持 None
|
|
700
716
|
"""
|
|
701
717
|
if not QMTDATAREADER_AVAILABLE:
|
|
702
|
-
logger.warning("[datadir] QmtDataReader 不可用(
|
|
718
|
+
logger.warning("[datadir] QmtDataReader 不可用(qmt_datadir 未安装或 pandas 缺失),datadir 功能已禁用")
|
|
703
719
|
return
|
|
704
720
|
|
|
705
721
|
# 步骤1:从环境变量读取
|
|
@@ -790,6 +806,9 @@ def start_server(host="0.0.0.0", port=None, use_ssl=False, certfile=None, keyfil
|
|
|
790
806
|
print(f" 日志级别: {log_level}")
|
|
791
807
|
print("=" * 70)
|
|
792
808
|
|
|
809
|
+
# ── 自动生成 clients.yaml ──────────────────────────────────────
|
|
810
|
+
_init_clients_yaml()
|
|
811
|
+
|
|
793
812
|
# 预加载权限检查器(加载 clients.yaml 配置)
|
|
794
813
|
if XtQuantService._permission_checker is None:
|
|
795
814
|
XtQuantService._permission_checker = get_permission_checker()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xtquant-share
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.8
|
|
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
|
|
@@ -32,6 +32,8 @@ Provides-Extra: dev
|
|
|
32
32
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
33
|
Requires-Dist: black>=23.0; extra == "dev"
|
|
34
34
|
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
35
|
+
Provides-Extra: datadir
|
|
36
|
+
Requires-Dist: pandas>=1.0; extra == "datadir"
|
|
35
37
|
Dynamic: license-file
|
|
36
38
|
|
|
37
39
|
# 极限量化 (xqshare)
|
|
@@ -11,7 +11,14 @@ tests/test_tools_common.py
|
|
|
11
11
|
xqshare/__init__.py
|
|
12
12
|
xqshare/auth.py
|
|
13
13
|
xqshare/client.py
|
|
14
|
+
xqshare/clients.yaml.example
|
|
14
15
|
xqshare/server.py
|
|
16
|
+
xqshare/qmt_datadir/__init__.py
|
|
17
|
+
xqshare/qmt_datadir/divid.py
|
|
18
|
+
xqshare/qmt_datadir/kline.py
|
|
19
|
+
xqshare/qmt_datadir/misc.py
|
|
20
|
+
xqshare/qmt_datadir/reader.py
|
|
21
|
+
xqshare/qmt_datadir/sector.py
|
|
15
22
|
xqshare/tools/__init__.py
|
|
16
23
|
xqshare/tools/common.py
|
|
17
24
|
xqshare/tools/xtdata.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|