tdx2ckz 0.1.0__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.
tdx2ckz-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qiuxiaoying
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tdx2ckz-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.1
2
+ Name: tdx2ckz
3
+ Version: 0.1.0
4
+ Summary: 通达信 .day 日线与 gbbq 股本变迁读取器(Windows x64)
5
+ Author: qiuxiaoying
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/qiuxiaoying/tdx2ckz
8
+ Project-URL: Repository, https://github.com/qiuxiaoying/tdx2ckz
9
+ Keywords: 通达信,tdx,day,gbbq,stock-data,market-data
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+
25
+ # tdx2ckz
26
+
27
+ 通达信(TDX)本地数据读取器:把 `vipdoc` 下的 `.day` 日线文件与 `T0002/hq_cache/gbbq`
28
+ 股本变迁文件直接解析成 pandas `DataFrame`。
29
+
30
+ - 只有两个入口:`read_lday(path)`、`read_gbbq(path)`
31
+ - 单个 C 扩展 `tdx2ckz._core`,`Py_LIMITED_API=0x03080000`,wheel 标签 `cp38-abi3-win_amd64`
32
+ - 仅支持 **Windows x64**(C 源在非 Windows 直接 `#error`,`setup.py` 在非 `win32` 直接 `RuntimeError`)
33
+ - C 层不依赖 numpy C API:按列返回连续缓冲,Python 侧 `np.frombuffer` 零拷贝组表
34
+
35
+ ## 构建
36
+
37
+ ```powershell
38
+ # 开发:就地编译
39
+ python setup.py build_ext --inplace
40
+
41
+ # 发布 wheel(只产 wheel,不产 sdist)
42
+ python -m pip wheel . --no-deps --no-build-isolation -w dist
43
+
44
+ # 发布 sdist(只产 tar.gz,不产 wheel)
45
+ python setup.py sdist --dist-dir dist
46
+ ```
47
+
48
+ | 命令 | 产物 |
49
+ | --- | --- |
50
+ | `pip wheel ...` | `tdx2ckz-0.1.0-cp38-abi3-win_amd64.whl`(只产 wheel) |
51
+ | `setup.py sdist` | `tdx2ckz-0.1.0.tar.gz`(只产 sdist) |
52
+
53
+ `setup.py` 声明 `Extension`,并通过 `options={"bdist_wheel": {"py_limited_api": "cp38"}}`
54
+ 固定 wheel 标签;源码树中的 `setup.cfg` 已移除,构建不再依赖它。
55
+
56
+ `python -m build` 需要额外安装 `build` 包,离线环境请用上面两条命令。
57
+
58
+ `tar.gz` 解包后出现的 `tdx2ckz-0.1.0/setup.cfg` 是 `egg_info` 打包时**自动生成**的构建
59
+ 副产品(内容只有 `[egg_info]` 的 `tag_build` / `tag_date`),与源码树中已删除的那个
60
+ `setup.cfg` 不是同一个文件。
61
+
62
+ 清空 `.pyd` 与 `build/` 后重建,编译器(MSVC `/W3 /O2 /utf-8`)输出 **0 warning / 0 error**。
63
+
64
+ ## 用法
65
+
66
+ ```python
67
+ import tdx2ckz
68
+
69
+ lday = tdx2ckz.read_lday(r"C:\new_tdx_mock\vipdoc")
70
+ print(lday.dtypes)
71
+ print(lday.head())
72
+
73
+ gbbq = tdx2ckz.read_gbbq(r"C:\new_tdx_mock\T0002\hq_cache\gbbq")
74
+ print(gbbq.dtypes)
75
+ ```
76
+
77
+ ### read_lday
78
+
79
+ `path` 是 `vipdoc` 这类根目录:递归遍历所有子目录,只取 `.day`,其它扩展名一律跳过。
80
+
81
+ | 列 | dtype | 说明 |
82
+ | --- | --- | --- |
83
+ | code | category | 归一后的代码,如 `sh600000`、`ds47#IF00` |
84
+ | date | datetime64[ns] | 交易日 |
85
+ | open / high / low / close | float64 | 价格 |
86
+ | amount | float64 | 成交额 |
87
+ | volume | uint32 | 成交量 |
88
+
89
+ - `.day` 为 32 字节定长记录;`sh`/`sz`/`bj` 的 OHLC 为整数分,除以 100
90
+ - `ds`(扩展行情)的 OHLC 是 **float32 原值,不除以 100**;直接提升到 float64 会带二进制
91
+ 残尾(`209.97691345214844`),而真实价格是 3 位小数,因此 `open/high/low/close`
92
+ 统一四舍五入到 3 位小数(`209.977`、`181.462`)
93
+ - `ds` 的 `amount` 保持原值,不做小数处理:实测该字段是整数、量级 2.7×10⁹–1.0×10¹¹,
94
+ float32 在该量级的间隔(ULP)已达 8192,补 3 位小数没有意义
95
+ - 市场归属规则:相对路径中出现的市场目录名(`sh/sz/bj/ds`),或 `.day` 文件名自带的市场
96
+ 前缀;因此单独指向 `vipdoc\ds\lday` 时,其下所有文件仍能正确识别为 `ds`
97
+ - 递归时跳过符号链接 / 联接点(junction)目录,不跟随
98
+ - 行序只由文件扫描顺序(路径排序)决定,与线程调度无关
99
+ - 空目录返回 0 行、同 schema 的 DataFrame
100
+
101
+ 代码归一:
102
+
103
+ 1. 取文件名主干;前两位是 `sh/sz/bj/ds` 时识别为市场前缀并统一小写,其余字符原样保留
104
+ (`SH600000.day` → `sh600000`)
105
+ 2. 主干无前缀时补上一级市场目录名(`ds/lday/47#IF00.day` → `ds47#IF00`,`IF00` 保持大写,
106
+ `#` 前的交易所编号保留)
107
+ 3. 归一后代码重复(如 `sh600000` 与 `SH600000`)在扫描阶段直接抛 `ValueError`,异常文本列出
108
+ 全部冲突路径;跨市场同代码(`sh600000` 与 `sz600000`)不算冲突
109
+
110
+ 异常:目录不存在 → `FileNotFoundError`;路径不是目录、文件长度不是 32 的整数倍 →
111
+ `ValueError`,异常文本均带完整路径。长度校验默认用目录枚举得到的字节数;若实际读取到的
112
+ 大小与之不符,以实读结果为准(详见下文“为什么用目录枚举的大小”)。
113
+
114
+ ### read_gbbq
115
+
116
+ gbbq 文件:4 字节头记录数 + 每条 29 字节(前 24 字节为 16 轮 sbox 加密,后 5 字节明文)。
117
+
118
+ | 列 | dtype | 说明 |
119
+ | --- | --- | --- |
120
+ | market | uint8 | 市场编号 |
121
+ | code | category | 证券代码(文件内容,去掉尾部 NUL) |
122
+ | date | uint32 | `YYYYMMDD` 整数 |
123
+ | category | uint8 | 记录类别 |
124
+ | f1..f4 | float64 | 四个浮点字段 |
125
+
126
+ 头部记录数必须与文件长度严格一致,否则 `ValueError`(带路径、记录数与字节数);
127
+ 0 条记录返回 0 行同 schema DataFrame。
128
+
129
+ ## 性能
130
+
131
+ - 扫描用 `os.scandir` 递归,文件大小直接取 `DirEntry.stat().st_size`,随扫描结果一起传给 C 层
132
+ - 读取按文件多线程切分,默认 `min(4, CPU)`;`TDX2CKZ_THREADS=1` 回单线程
133
+ - 线程只写各自分片的预分配列缓冲,因此结果与调度顺序无关
134
+
135
+ ### 为什么用目录枚举的大小
136
+
137
+ 逐文件调用 `GetFileAttributesExW` 在本机会卷上有明显开销:约 **0.40 ms/次**,`sh` 的
138
+ 6,053 个文件合计约 **2.35 s**,占修复前 sh 耗时(3.004 s)的绝大部分。而 `os.scandir`
139
+ 返回的 `DirEntry.stat().st_size` 直接复用 `FindFirstFileW` 已给出的结果,同样 6,053 个
140
+ 文件只需 **0.029 s**,几乎免费。所以扫描阶段一次枚举就把大小带到 C 层,线程启动前不再
141
+ 逐文件 stat。
142
+
143
+ 代价是“扫描后、读取前文件被改动”不再是隐含假设,C 层对此有显式保护:
144
+
145
+ - 每轮开始前按当前行数重算偏移并重新分配列缓冲
146
+ - 某文件实际行数与枚举行数不符时 **不写任何字节**,改用实读大小重排该文件行数,最多重试
147
+ **4 轮**
148
+ - 实际长度不是 32 的整数倍 → `ValueError`(异常文本带实际字节数)
149
+ - 枚举大小不是 32 的整数倍 → `ValueError`(带枚举字节数);0 字节文件贡献 0 行
150
+ - 4 轮后仍不一致 → `ValueError("文件大小在扫描与读取之间持续变化: ...")`
151
+
152
+ ### 分市场(修复后,各 5 次取中位)
153
+
154
+ `C:\new_tdx_mock\vipdoc`,单进程、默认 4 线程:
155
+
156
+ | 市场 | 文件数 | 行数 | 中位耗时 | 峰值内存 |
157
+ | --- | --- | --- | --- | --- |
158
+ | sh | 6,053 | 15,328,535 | **0.746 s** | 938 MB |
159
+ | sz | 5,928 | 14,207,349 | **0.730 s** | 877 MB |
160
+ | bj | 373 | 283,177 | **0.038 s** | 94 MB |
161
+ | ds | 3 | 3,943 | **0.006 s** | 78 MB |
162
+
163
+ 修复前 sh 为 3.004 s;瓶颈是主线程串行的 6,053 次 `GetFileAttributesExW`。
164
+
165
+ ### C 核心层与分段耗时(sh)
166
+
167
+ | 项目 | 单线程 | 4 线程 |
168
+ | --- | --- | --- |
169
+ | C 核心(打开 + 读 + 解码 + 写列缓冲) | 1.868 s | 0.606 s(**3.08×**) |
170
+
171
+ 端到端 sh 单线程(`TDX2CKZ_THREADS=1`)**2.050 s**,默认 4 线程 0.746 s(2.75×)。
172
+
173
+ sh 分段(4 线程):scan **0.039 s**、core **0.606 s**、DataFrame 组装约 **0.20 s**
174
+ (其中 `np.repeat` 0.062 s、`Categorical` 0.022 s、`DataFrame` 构造 0.111 s)。
175
+
176
+ ### 整体(`vipdoc` 全量)
177
+
178
+ | 指标 | 数值 |
179
+ | --- | --- |
180
+ | 文件数 | 12,357(sh 6053 / sz 5928 / bj 373 / ds 3) |
181
+ | 总行数 | 29,823,004 |
182
+ | 出现代码 | 12,355(另有 2 个 0 字节 `.day` 无数据行) |
183
+ | 日期范围 | 1990-12-19 → 2026-11-06 |
184
+ | 热读耗时(4 次) | 1.540 / 1.462 / 1.794 / 1.561 s,中位 **1.561 s** |
185
+ | 峰值内存 | **1.75 GB**(DataFrame 本身 1.50 GB,列缓冲零拷贝) |
186
+
187
+ gbbq:194,352 条,热读中位 **0.087 s**。
188
+
189
+ 复现:
190
+
191
+ ```powershell
192
+ python bench/bench.py
193
+ ```
194
+
195
+ ## 测试
196
+
197
+ ```powershell
198
+ # 合成样本(schema/值/递归/归一/冲突/空目录/异常/gbbq 解密)
199
+ python -m pytest tests -q
200
+
201
+ # 追加真实数据逐值交叉验证(独立 Python 解析,逐值零差异)
202
+ TDX2CKZ_REAL=1 python -m pytest tests -q -k real
203
+
204
+ # PowerShell 下等价写法
205
+ $env:TDX2CKZ_REAL="1"; python -m pytest tests -q -k real
206
+ ```
207
+
208
+ 交叉验证用 numpy 结构化 dtype + pandas 日期解析 + 独立实现的 16 轮 sbox 解密,
209
+ 与 C 层逐文件、逐列比对(`assert_array_equal`,不做近似)。
210
+
211
+ ## 目录
212
+
213
+ ```
214
+ tdx2ckz.c # 唯一 C 源,导出 read_lday / read_gbbq
215
+ tdx2ckz/__init__.py # 两个入口,C 层缓冲 -> DataFrame
216
+ tdx2ckz/__init__.pyi # 类型与中文注释
217
+ tdx2ckz/py.typed
218
+ setup.py # Extension 声明 + wheel 标签(options.bdist_wheel.py_limited_api)
219
+ pyproject.toml # build-system + project 元数据
220
+ tests/test_readers.py
221
+ bench/bench.py
222
+ ```
@@ -0,0 +1,198 @@
1
+ # tdx2ckz
2
+
3
+ 通达信(TDX)本地数据读取器:把 `vipdoc` 下的 `.day` 日线文件与 `T0002/hq_cache/gbbq`
4
+ 股本变迁文件直接解析成 pandas `DataFrame`。
5
+
6
+ - 只有两个入口:`read_lday(path)`、`read_gbbq(path)`
7
+ - 单个 C 扩展 `tdx2ckz._core`,`Py_LIMITED_API=0x03080000`,wheel 标签 `cp38-abi3-win_amd64`
8
+ - 仅支持 **Windows x64**(C 源在非 Windows 直接 `#error`,`setup.py` 在非 `win32` 直接 `RuntimeError`)
9
+ - C 层不依赖 numpy C API:按列返回连续缓冲,Python 侧 `np.frombuffer` 零拷贝组表
10
+
11
+ ## 构建
12
+
13
+ ```powershell
14
+ # 开发:就地编译
15
+ python setup.py build_ext --inplace
16
+
17
+ # 发布 wheel(只产 wheel,不产 sdist)
18
+ python -m pip wheel . --no-deps --no-build-isolation -w dist
19
+
20
+ # 发布 sdist(只产 tar.gz,不产 wheel)
21
+ python setup.py sdist --dist-dir dist
22
+ ```
23
+
24
+ | 命令 | 产物 |
25
+ | --- | --- |
26
+ | `pip wheel ...` | `tdx2ckz-0.1.0-cp38-abi3-win_amd64.whl`(只产 wheel) |
27
+ | `setup.py sdist` | `tdx2ckz-0.1.0.tar.gz`(只产 sdist) |
28
+
29
+ `setup.py` 声明 `Extension`,并通过 `options={"bdist_wheel": {"py_limited_api": "cp38"}}`
30
+ 固定 wheel 标签;源码树中的 `setup.cfg` 已移除,构建不再依赖它。
31
+
32
+ `python -m build` 需要额外安装 `build` 包,离线环境请用上面两条命令。
33
+
34
+ `tar.gz` 解包后出现的 `tdx2ckz-0.1.0/setup.cfg` 是 `egg_info` 打包时**自动生成**的构建
35
+ 副产品(内容只有 `[egg_info]` 的 `tag_build` / `tag_date`),与源码树中已删除的那个
36
+ `setup.cfg` 不是同一个文件。
37
+
38
+ 清空 `.pyd` 与 `build/` 后重建,编译器(MSVC `/W3 /O2 /utf-8`)输出 **0 warning / 0 error**。
39
+
40
+ ## 用法
41
+
42
+ ```python
43
+ import tdx2ckz
44
+
45
+ lday = tdx2ckz.read_lday(r"C:\new_tdx_mock\vipdoc")
46
+ print(lday.dtypes)
47
+ print(lday.head())
48
+
49
+ gbbq = tdx2ckz.read_gbbq(r"C:\new_tdx_mock\T0002\hq_cache\gbbq")
50
+ print(gbbq.dtypes)
51
+ ```
52
+
53
+ ### read_lday
54
+
55
+ `path` 是 `vipdoc` 这类根目录:递归遍历所有子目录,只取 `.day`,其它扩展名一律跳过。
56
+
57
+ | 列 | dtype | 说明 |
58
+ | --- | --- | --- |
59
+ | code | category | 归一后的代码,如 `sh600000`、`ds47#IF00` |
60
+ | date | datetime64[ns] | 交易日 |
61
+ | open / high / low / close | float64 | 价格 |
62
+ | amount | float64 | 成交额 |
63
+ | volume | uint32 | 成交量 |
64
+
65
+ - `.day` 为 32 字节定长记录;`sh`/`sz`/`bj` 的 OHLC 为整数分,除以 100
66
+ - `ds`(扩展行情)的 OHLC 是 **float32 原值,不除以 100**;直接提升到 float64 会带二进制
67
+ 残尾(`209.97691345214844`),而真实价格是 3 位小数,因此 `open/high/low/close`
68
+ 统一四舍五入到 3 位小数(`209.977`、`181.462`)
69
+ - `ds` 的 `amount` 保持原值,不做小数处理:实测该字段是整数、量级 2.7×10⁹–1.0×10¹¹,
70
+ float32 在该量级的间隔(ULP)已达 8192,补 3 位小数没有意义
71
+ - 市场归属规则:相对路径中出现的市场目录名(`sh/sz/bj/ds`),或 `.day` 文件名自带的市场
72
+ 前缀;因此单独指向 `vipdoc\ds\lday` 时,其下所有文件仍能正确识别为 `ds`
73
+ - 递归时跳过符号链接 / 联接点(junction)目录,不跟随
74
+ - 行序只由文件扫描顺序(路径排序)决定,与线程调度无关
75
+ - 空目录返回 0 行、同 schema 的 DataFrame
76
+
77
+ 代码归一:
78
+
79
+ 1. 取文件名主干;前两位是 `sh/sz/bj/ds` 时识别为市场前缀并统一小写,其余字符原样保留
80
+ (`SH600000.day` → `sh600000`)
81
+ 2. 主干无前缀时补上一级市场目录名(`ds/lday/47#IF00.day` → `ds47#IF00`,`IF00` 保持大写,
82
+ `#` 前的交易所编号保留)
83
+ 3. 归一后代码重复(如 `sh600000` 与 `SH600000`)在扫描阶段直接抛 `ValueError`,异常文本列出
84
+ 全部冲突路径;跨市场同代码(`sh600000` 与 `sz600000`)不算冲突
85
+
86
+ 异常:目录不存在 → `FileNotFoundError`;路径不是目录、文件长度不是 32 的整数倍 →
87
+ `ValueError`,异常文本均带完整路径。长度校验默认用目录枚举得到的字节数;若实际读取到的
88
+ 大小与之不符,以实读结果为准(详见下文“为什么用目录枚举的大小”)。
89
+
90
+ ### read_gbbq
91
+
92
+ gbbq 文件:4 字节头记录数 + 每条 29 字节(前 24 字节为 16 轮 sbox 加密,后 5 字节明文)。
93
+
94
+ | 列 | dtype | 说明 |
95
+ | --- | --- | --- |
96
+ | market | uint8 | 市场编号 |
97
+ | code | category | 证券代码(文件内容,去掉尾部 NUL) |
98
+ | date | uint32 | `YYYYMMDD` 整数 |
99
+ | category | uint8 | 记录类别 |
100
+ | f1..f4 | float64 | 四个浮点字段 |
101
+
102
+ 头部记录数必须与文件长度严格一致,否则 `ValueError`(带路径、记录数与字节数);
103
+ 0 条记录返回 0 行同 schema DataFrame。
104
+
105
+ ## 性能
106
+
107
+ - 扫描用 `os.scandir` 递归,文件大小直接取 `DirEntry.stat().st_size`,随扫描结果一起传给 C 层
108
+ - 读取按文件多线程切分,默认 `min(4, CPU)`;`TDX2CKZ_THREADS=1` 回单线程
109
+ - 线程只写各自分片的预分配列缓冲,因此结果与调度顺序无关
110
+
111
+ ### 为什么用目录枚举的大小
112
+
113
+ 逐文件调用 `GetFileAttributesExW` 在本机会卷上有明显开销:约 **0.40 ms/次**,`sh` 的
114
+ 6,053 个文件合计约 **2.35 s**,占修复前 sh 耗时(3.004 s)的绝大部分。而 `os.scandir`
115
+ 返回的 `DirEntry.stat().st_size` 直接复用 `FindFirstFileW` 已给出的结果,同样 6,053 个
116
+ 文件只需 **0.029 s**,几乎免费。所以扫描阶段一次枚举就把大小带到 C 层,线程启动前不再
117
+ 逐文件 stat。
118
+
119
+ 代价是“扫描后、读取前文件被改动”不再是隐含假设,C 层对此有显式保护:
120
+
121
+ - 每轮开始前按当前行数重算偏移并重新分配列缓冲
122
+ - 某文件实际行数与枚举行数不符时 **不写任何字节**,改用实读大小重排该文件行数,最多重试
123
+ **4 轮**
124
+ - 实际长度不是 32 的整数倍 → `ValueError`(异常文本带实际字节数)
125
+ - 枚举大小不是 32 的整数倍 → `ValueError`(带枚举字节数);0 字节文件贡献 0 行
126
+ - 4 轮后仍不一致 → `ValueError("文件大小在扫描与读取之间持续变化: ...")`
127
+
128
+ ### 分市场(修复后,各 5 次取中位)
129
+
130
+ `C:\new_tdx_mock\vipdoc`,单进程、默认 4 线程:
131
+
132
+ | 市场 | 文件数 | 行数 | 中位耗时 | 峰值内存 |
133
+ | --- | --- | --- | --- | --- |
134
+ | sh | 6,053 | 15,328,535 | **0.746 s** | 938 MB |
135
+ | sz | 5,928 | 14,207,349 | **0.730 s** | 877 MB |
136
+ | bj | 373 | 283,177 | **0.038 s** | 94 MB |
137
+ | ds | 3 | 3,943 | **0.006 s** | 78 MB |
138
+
139
+ 修复前 sh 为 3.004 s;瓶颈是主线程串行的 6,053 次 `GetFileAttributesExW`。
140
+
141
+ ### C 核心层与分段耗时(sh)
142
+
143
+ | 项目 | 单线程 | 4 线程 |
144
+ | --- | --- | --- |
145
+ | C 核心(打开 + 读 + 解码 + 写列缓冲) | 1.868 s | 0.606 s(**3.08×**) |
146
+
147
+ 端到端 sh 单线程(`TDX2CKZ_THREADS=1`)**2.050 s**,默认 4 线程 0.746 s(2.75×)。
148
+
149
+ sh 分段(4 线程):scan **0.039 s**、core **0.606 s**、DataFrame 组装约 **0.20 s**
150
+ (其中 `np.repeat` 0.062 s、`Categorical` 0.022 s、`DataFrame` 构造 0.111 s)。
151
+
152
+ ### 整体(`vipdoc` 全量)
153
+
154
+ | 指标 | 数值 |
155
+ | --- | --- |
156
+ | 文件数 | 12,357(sh 6053 / sz 5928 / bj 373 / ds 3) |
157
+ | 总行数 | 29,823,004 |
158
+ | 出现代码 | 12,355(另有 2 个 0 字节 `.day` 无数据行) |
159
+ | 日期范围 | 1990-12-19 → 2026-11-06 |
160
+ | 热读耗时(4 次) | 1.540 / 1.462 / 1.794 / 1.561 s,中位 **1.561 s** |
161
+ | 峰值内存 | **1.75 GB**(DataFrame 本身 1.50 GB,列缓冲零拷贝) |
162
+
163
+ gbbq:194,352 条,热读中位 **0.087 s**。
164
+
165
+ 复现:
166
+
167
+ ```powershell
168
+ python bench/bench.py
169
+ ```
170
+
171
+ ## 测试
172
+
173
+ ```powershell
174
+ # 合成样本(schema/值/递归/归一/冲突/空目录/异常/gbbq 解密)
175
+ python -m pytest tests -q
176
+
177
+ # 追加真实数据逐值交叉验证(独立 Python 解析,逐值零差异)
178
+ TDX2CKZ_REAL=1 python -m pytest tests -q -k real
179
+
180
+ # PowerShell 下等价写法
181
+ $env:TDX2CKZ_REAL="1"; python -m pytest tests -q -k real
182
+ ```
183
+
184
+ 交叉验证用 numpy 结构化 dtype + pandas 日期解析 + 独立实现的 16 轮 sbox 解密,
185
+ 与 C 层逐文件、逐列比对(`assert_array_equal`,不做近似)。
186
+
187
+ ## 目录
188
+
189
+ ```
190
+ tdx2ckz.c # 唯一 C 源,导出 read_lday / read_gbbq
191
+ tdx2ckz/__init__.py # 两个入口,C 层缓冲 -> DataFrame
192
+ tdx2ckz/__init__.pyi # 类型与中文注释
193
+ tdx2ckz/py.typed
194
+ setup.py # Extension 声明 + wheel 标签(options.bdist_wheel.py_limited_api)
195
+ pyproject.toml # build-system + project 元数据
196
+ tests/test_readers.py
197
+ bench/bench.py
198
+ ```
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tdx2ckz"
7
+ version = "0.1.0"
8
+ description = "通达信 .day 日线与 gbbq 股本变迁读取器(Windows x64)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ authors = [{ name = "qiuxiaoying" }]
12
+ keywords = ["通达信", "tdx", "day", "gbbq", "stock-data", "market-data"]
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: Microsoft :: Windows",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "numpy>=1.20",
28
+ "pandas>=1.3",
29
+ ]
30
+ # setuptools 65.5 < 77,不支持 PEP 639,使用经典 license 表写法
31
+ license = { text = "MIT" }
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/qiuxiaoying/tdx2ckz"
35
+ Repository = "https://github.com/qiuxiaoying/tdx2ckz"
36
+
37
+ [tool.setuptools]
38
+ packages = ["tdx2ckz"]
39
+
40
+ [tool.setuptools.package-data]
41
+ tdx2ckz = ["py.typed", "__init__.pyi"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
tdx2ckz-0.1.0/setup.py ADDED
@@ -0,0 +1,19 @@
1
+ import sys
2
+
3
+ from setuptools import Extension, setup
4
+
5
+ if sys.platform != "win32":
6
+ raise RuntimeError("tdx2ckz 仅支持 Windows x64(当前平台: %s)" % sys.platform)
7
+
8
+ setup(
9
+ ext_modules=[
10
+ Extension(
11
+ "tdx2ckz._core",
12
+ sources=["tdx2ckz.c"],
13
+ define_macros=[("Py_LIMITED_API", "0x03080000")],
14
+ py_limited_api=True,
15
+ extra_compile_args=["/W3", "/O2", "/utf-8"],
16
+ )
17
+ ],
18
+ options={"bdist_wheel": {"py_limited_api": "cp38"}},
19
+ )