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 +21 -0
- tdx2ckz-0.1.0/PKG-INFO +222 -0
- tdx2ckz-0.1.0/README.md +198 -0
- tdx2ckz-0.1.0/pyproject.toml +41 -0
- tdx2ckz-0.1.0/setup.cfg +4 -0
- tdx2ckz-0.1.0/setup.py +19 -0
- tdx2ckz-0.1.0/tdx2ckz/__init__.py +234 -0
- tdx2ckz-0.1.0/tdx2ckz/__init__.pyi +71 -0
- tdx2ckz-0.1.0/tdx2ckz/py.typed +0 -0
- tdx2ckz-0.1.0/tdx2ckz.c +998 -0
- tdx2ckz-0.1.0/tdx2ckz.egg-info/PKG-INFO +222 -0
- tdx2ckz-0.1.0/tdx2ckz.egg-info/SOURCES.txt +13 -0
- tdx2ckz-0.1.0/tdx2ckz.egg-info/dependency_links.txt +1 -0
- tdx2ckz-0.1.0/tdx2ckz.egg-info/requires.txt +2 -0
- tdx2ckz-0.1.0/tdx2ckz.egg-info/top_level.txt +1 -0
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
|
+
```
|
tdx2ckz-0.1.0/README.md
ADDED
|
@@ -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"]
|
tdx2ckz-0.1.0/setup.cfg
ADDED
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
|
+
)
|