xtquant-share 1.2.5__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.
Files changed (33) hide show
  1. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/PKG-INFO +3 -1
  2. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/pyproject.toml +5 -2
  3. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/tests/test_server.py +1 -1
  4. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xqshare/auth.py +3 -3
  5. xtquant_share-1.2.8/xqshare/clients.yaml.example +69 -0
  6. xtquant_share-1.2.8/xqshare/qmt_datadir/__init__.py +73 -0
  7. xtquant_share-1.2.8/xqshare/qmt_datadir/divid.py +129 -0
  8. xtquant_share-1.2.8/xqshare/qmt_datadir/kline.py +136 -0
  9. xtquant_share-1.2.8/xqshare/qmt_datadir/misc.py +104 -0
  10. xtquant_share-1.2.8/xqshare/qmt_datadir/reader.py +169 -0
  11. xtquant_share-1.2.8/xqshare/qmt_datadir/sector.py +111 -0
  12. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xqshare/server.py +44 -14
  13. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xtquant_share.egg-info/PKG-INFO +3 -1
  14. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xtquant_share.egg-info/SOURCES.txt +7 -0
  15. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xtquant_share.egg-info/requires.txt +3 -0
  16. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/LICENSE +0 -0
  17. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/README.md +0 -0
  18. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/setup.cfg +0 -0
  19. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/tests/test_auth.py +0 -0
  20. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/tests/test_client.py +0 -0
  21. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/tests/test_client_permission.py +0 -0
  22. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/tests/test_integration.py +0 -0
  23. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/tests/test_server_permission.py +0 -0
  24. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/tests/test_tools_common.py +0 -0
  25. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xqshare/__init__.py +0 -0
  26. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xqshare/client.py +0 -0
  27. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xqshare/tools/__init__.py +0 -0
  28. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xqshare/tools/common.py +0 -0
  29. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xqshare/tools/xtdata.py +0 -0
  30. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xqshare/tools/xttrader.py +0 -0
  31. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xtquant_share.egg-info/dependency_links.txt +0 -0
  32. {xtquant_share-1.2.5 → xtquant_share-1.2.8}/xtquant_share.egg-info/entry_points.txt +0 -0
  33. {xtquant_share-1.2.5 → 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.5
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.5"
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__ = 'utils.qmt_datadir.reader'
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
- import logging
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"[配置加载] 未找到 clients.yaml,启用默认账号: {DEFAULT_CLIENT_ID} (level=enterprise)")
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 utils.qmt_datadir import QmtDataReader
51
+ from xqshare.qmt_datadir import QmtDataReader
52
52
  QMTDATAREADER_AVAILABLE = True
53
53
  except ImportError:
54
- try:
55
- # 兼容:直接从 qmt_datadir 包导入
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 不可用(utils.qmt_datadir 未安装),datadir 功能已禁用")
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()
@@ -1080,8 +1099,19 @@ def _default_env_file():
1080
1099
 
1081
1100
  def main():
1082
1101
  """命令行入口函数"""
1102
+ import sys
1083
1103
  import argparse
1084
1104
 
1105
+ # Windows 终端默认 GBK,统一切换 stdout/stderr 为 UTF-8,避免中文乱码
1106
+ if sys.platform == "win32":
1107
+ try:
1108
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
1109
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
1110
+ except AttributeError:
1111
+ import io
1112
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
1113
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
1114
+
1085
1115
  parser = argparse.ArgumentParser(
1086
1116
  description="XtQuant Share (xqshare) 服务",
1087
1117
  formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xtquant-share
3
- Version: 1.2.5
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
@@ -2,6 +2,9 @@ rpyc>=6.0
2
2
  python-dotenv>=1.0
3
3
  pyyaml>=6.0
4
4
 
5
+ [datadir]
6
+ pandas>=1.0
7
+
5
8
  [dev]
6
9
  pytest>=7.0
7
10
  black>=23.0
File without changes
File without changes
File without changes