tdxlocal 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.
- tdxlocal-0.1.0/LICENSE +21 -0
- tdxlocal-0.1.0/PKG-INFO +127 -0
- tdxlocal-0.1.0/README.md +107 -0
- tdxlocal-0.1.0/pyproject.toml +30 -0
- tdxlocal-0.1.0/setup.cfg +4 -0
- tdxlocal-0.1.0/src/tdxdata/__init__.py +27 -0
- tdxlocal-0.1.0/src/tdxdata/__main__.py +3 -0
- tdxlocal-0.1.0/src/tdxdata/cli.py +44 -0
- tdxlocal-0.1.0/src/tdxdata/day.py +60 -0
- tdxlocal-0.1.0/src/tdxdata/minute.py +83 -0
- tdxlocal-0.1.0/src/tdxdata/records.py +37 -0
- tdxlocal-0.1.0/src/tdxdata/vipdoc.py +57 -0
- tdxlocal-0.1.0/src/tdxlocal.egg-info/PKG-INFO +127 -0
- tdxlocal-0.1.0/src/tdxlocal.egg-info/SOURCES.txt +17 -0
- tdxlocal-0.1.0/src/tdxlocal.egg-info/dependency_links.txt +1 -0
- tdxlocal-0.1.0/src/tdxlocal.egg-info/entry_points.txt +2 -0
- tdxlocal-0.1.0/src/tdxlocal.egg-info/requires.txt +3 -0
- tdxlocal-0.1.0/src/tdxlocal.egg-info/top_level.txt +1 -0
- tdxlocal-0.1.0/tests/test_tdxdata.py +97 -0
tdxlocal-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mengyiheng
|
|
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.
|
tdxlocal-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tdxlocal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 通达信本地二进制行情数据(.day / .lc1 / .lc5)读写库,零依赖
|
|
5
|
+
Author: mengyiheng
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: tdx,tongdaxin,stock,kline,quant
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: pandas
|
|
18
|
+
Requires-Dist: pandas; extra == "pandas"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
<p align="center"><img src="assets/cover.png" alt="tdxdata" width="720"/></p>
|
|
22
|
+
|
|
23
|
+
# tdxdata — 通达信本地行情数据读写库
|
|
24
|
+
|
|
25
|
+
读取和生成通达信(TDX)本地二进制 K 线文件:**`.day` 日线**、**`.lc1` 1 分钟线**、**`.lc5` 5 分钟线**。
|
|
26
|
+
|
|
27
|
+
- 🪶 **零第三方依赖**(pandas 可选)
|
|
28
|
+
- 📖 **读取**:一行代码解析为 Python 对象列表
|
|
29
|
+
- ✍️ **生成 / 增量更新**:与通达信客户端落盘格式逐字节一致,可被客户端直接识别
|
|
30
|
+
- 🗂 **vipdoc 目录约定**:按证券代码自动定位文件、自动推断市场(sh/sz/bj)
|
|
31
|
+
- 🖥 **命令行**:`tdxdata day <vipdoc> 000001 out.csv` 一键导出 CSV
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install tdxlocal # 含 pandas 支持
|
|
37
|
+
pip install tdxlocal --no-deps # 纯标准库
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 文件格式
|
|
41
|
+
|
|
42
|
+
两类文件均为 **每条记录 32 字节、小端 `<IIIIIfII>`**,但日期字段编码不同:
|
|
43
|
+
|
|
44
|
+
| 字段 | 日线 `.day` | 分钟线 `.lc1/.lc5` |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| 0 | 日期 `YYYYMMDD` 明文 | 紧凑日期 `(year-2004)*2048 + month*100 + day` |
|
|
47
|
+
| 4 | 开*100 | 时间 `hour*60 + minute` |
|
|
48
|
+
| 8–20 | 高/低/收 *100 | 高/低/收/开 *100 |
|
|
49
|
+
| 24 | 成交额 float32(元) | 成交额 float32(元) |
|
|
50
|
+
| 28 | 成交量(手)+ 保留字段 | 成交量(手) |
|
|
51
|
+
|
|
52
|
+
## 快速开始
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from tdxdata import read_day, read_minute, write_day, DayBar
|
|
56
|
+
|
|
57
|
+
# 读取日线
|
|
58
|
+
bars = read_day(r"D:\new_tdx64\vipdoc\sh\lday\sh000001.day")
|
|
59
|
+
print(bars[-1]) # DayBar(date=20260116, open=..., high=..., ...)
|
|
60
|
+
|
|
61
|
+
# 读取 1 分钟线
|
|
62
|
+
mins = read_minute(r"D:\new_tdx\vipdoc\sh\minline\sh999999.lc1")
|
|
63
|
+
|
|
64
|
+
# 生成日线文件(通达信可直接打开)
|
|
65
|
+
write_day("my.day", [DayBar(20260116, 10.0, 10.5, 9.9, 10.2, 1.2e8, 123456)])
|
|
66
|
+
|
|
67
|
+
# 增量更新(追加模式,与通达信盘中落盘一致)
|
|
68
|
+
write_day(path, [DayBar(...)], append=True)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### vipdoc 便捷入口
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from tdxdata import VipdocStore
|
|
75
|
+
|
|
76
|
+
store = VipdocStore(r"D:\new_tdx64\vipdoc")
|
|
77
|
+
bars = store.read("600519", "day") # 自动定位 sh/lday/sh600519.day
|
|
78
|
+
store.update("600519", [DayBar(...)]) # 增量追加
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 可选:输出 pandas DataFrame
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import pandas as pd
|
|
85
|
+
from dataclasses import asdict
|
|
86
|
+
from tdxdata import read_day
|
|
87
|
+
|
|
88
|
+
df = pd.DataFrame([asdict(b) for b in read_day(path)])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 命令行
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
tdxdata day D:\new_tdx64\vipdoc 000001 sh000001.csv
|
|
95
|
+
tdxdata lc1 D:\new_tdx\vipdoc 999999 sh999999.csv
|
|
96
|
+
python -m tdxdata day D:\new_tdx64\vipdoc\sh\lday\sh000001.day out.csv
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 运行测试
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install -e . pytest
|
|
103
|
+
pytest
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
测试自带合成数据做逐字节往返校验;若本机存在 `D:\new_tdx64\vipdoc`,会自动附加真实数据冒烟测试(缺失则跳过)。
|
|
107
|
+
|
|
108
|
+
## 免责声明
|
|
109
|
+
|
|
110
|
+
本项目仅读取/生成**本地已有**的通达信数据文件,不提供任何行情数据,不构成任何投资建议。数据仅供学习与量化研究使用,不作为实盘交易依据,使用前请[阅读完整免责声明](DISCLAIMER.md)。**投资有风险,入市需谨慎。**
|
|
111
|
+
|
|
112
|
+
## 量化相关定制需求
|
|
113
|
+
|
|
114
|
+
作者承接基于本库及量化研究场景的**定制开发**,包括但不限于:
|
|
115
|
+
|
|
116
|
+
| 方向 | 示例 |
|
|
117
|
+
|---|---|
|
|
118
|
+
| 📈 数据扩展 | `.zst`/`.档位` 等其他 TDX 文件解析、盘中增量落盘监控、CSV/parquet/SQLite 批量导出 |
|
|
119
|
+
| 🐍 量化工具链 | pandas/Polars 适配层、多周期重采样(1m→5m→30m→日线)、复权因子处理 |
|
|
120
|
+
| 🤖 策略工程 | 回测框架对接、实盘信号监控脚本、通达信客户端联动(公式批量调用/行情订阅) |
|
|
121
|
+
| 🛠 私有部署 | 数据管线搭建、性能优化(批量解析全市场文件)、CI/自动发版 |
|
|
122
|
+
|
|
123
|
+
**联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxdata/issues) 提需求描述,或通过 [Gitee 私信](https://gitee.com/mengyiheng) 联系。复杂需求请附上:数据量级、时效要求(日终/盘中)、期望交付形式。
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
tdxlocal-0.1.0/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<p align="center"><img src="assets/cover.png" alt="tdxdata" width="720"/></p>
|
|
2
|
+
|
|
3
|
+
# tdxdata — 通达信本地行情数据读写库
|
|
4
|
+
|
|
5
|
+
读取和生成通达信(TDX)本地二进制 K 线文件:**`.day` 日线**、**`.lc1` 1 分钟线**、**`.lc5` 5 分钟线**。
|
|
6
|
+
|
|
7
|
+
- 🪶 **零第三方依赖**(pandas 可选)
|
|
8
|
+
- 📖 **读取**:一行代码解析为 Python 对象列表
|
|
9
|
+
- ✍️ **生成 / 增量更新**:与通达信客户端落盘格式逐字节一致,可被客户端直接识别
|
|
10
|
+
- 🗂 **vipdoc 目录约定**:按证券代码自动定位文件、自动推断市场(sh/sz/bj)
|
|
11
|
+
- 🖥 **命令行**:`tdxdata day <vipdoc> 000001 out.csv` 一键导出 CSV
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install tdxlocal # 含 pandas 支持
|
|
17
|
+
pip install tdxlocal --no-deps # 纯标准库
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 文件格式
|
|
21
|
+
|
|
22
|
+
两类文件均为 **每条记录 32 字节、小端 `<IIIIIfII>`**,但日期字段编码不同:
|
|
23
|
+
|
|
24
|
+
| 字段 | 日线 `.day` | 分钟线 `.lc1/.lc5` |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| 0 | 日期 `YYYYMMDD` 明文 | 紧凑日期 `(year-2004)*2048 + month*100 + day` |
|
|
27
|
+
| 4 | 开*100 | 时间 `hour*60 + minute` |
|
|
28
|
+
| 8–20 | 高/低/收 *100 | 高/低/收/开 *100 |
|
|
29
|
+
| 24 | 成交额 float32(元) | 成交额 float32(元) |
|
|
30
|
+
| 28 | 成交量(手)+ 保留字段 | 成交量(手) |
|
|
31
|
+
|
|
32
|
+
## 快速开始
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from tdxdata import read_day, read_minute, write_day, DayBar
|
|
36
|
+
|
|
37
|
+
# 读取日线
|
|
38
|
+
bars = read_day(r"D:\new_tdx64\vipdoc\sh\lday\sh000001.day")
|
|
39
|
+
print(bars[-1]) # DayBar(date=20260116, open=..., high=..., ...)
|
|
40
|
+
|
|
41
|
+
# 读取 1 分钟线
|
|
42
|
+
mins = read_minute(r"D:\new_tdx\vipdoc\sh\minline\sh999999.lc1")
|
|
43
|
+
|
|
44
|
+
# 生成日线文件(通达信可直接打开)
|
|
45
|
+
write_day("my.day", [DayBar(20260116, 10.0, 10.5, 9.9, 10.2, 1.2e8, 123456)])
|
|
46
|
+
|
|
47
|
+
# 增量更新(追加模式,与通达信盘中落盘一致)
|
|
48
|
+
write_day(path, [DayBar(...)], append=True)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### vipdoc 便捷入口
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from tdxdata import VipdocStore
|
|
55
|
+
|
|
56
|
+
store = VipdocStore(r"D:\new_tdx64\vipdoc")
|
|
57
|
+
bars = store.read("600519", "day") # 自动定位 sh/lday/sh600519.day
|
|
58
|
+
store.update("600519", [DayBar(...)]) # 增量追加
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 可选:输出 pandas DataFrame
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import pandas as pd
|
|
65
|
+
from dataclasses import asdict
|
|
66
|
+
from tdxdata import read_day
|
|
67
|
+
|
|
68
|
+
df = pd.DataFrame([asdict(b) for b in read_day(path)])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 命令行
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
tdxdata day D:\new_tdx64\vipdoc 000001 sh000001.csv
|
|
75
|
+
tdxdata lc1 D:\new_tdx\vipdoc 999999 sh999999.csv
|
|
76
|
+
python -m tdxdata day D:\new_tdx64\vipdoc\sh\lday\sh000001.day out.csv
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 运行测试
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install -e . pytest
|
|
83
|
+
pytest
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
测试自带合成数据做逐字节往返校验;若本机存在 `D:\new_tdx64\vipdoc`,会自动附加真实数据冒烟测试(缺失则跳过)。
|
|
87
|
+
|
|
88
|
+
## 免责声明
|
|
89
|
+
|
|
90
|
+
本项目仅读取/生成**本地已有**的通达信数据文件,不提供任何行情数据,不构成任何投资建议。数据仅供学习与量化研究使用,不作为实盘交易依据,使用前请[阅读完整免责声明](DISCLAIMER.md)。**投资有风险,入市需谨慎。**
|
|
91
|
+
|
|
92
|
+
## 量化相关定制需求
|
|
93
|
+
|
|
94
|
+
作者承接基于本库及量化研究场景的**定制开发**,包括但不限于:
|
|
95
|
+
|
|
96
|
+
| 方向 | 示例 |
|
|
97
|
+
|---|---|
|
|
98
|
+
| 📈 数据扩展 | `.zst`/`.档位` 等其他 TDX 文件解析、盘中增量落盘监控、CSV/parquet/SQLite 批量导出 |
|
|
99
|
+
| 🐍 量化工具链 | pandas/Polars 适配层、多周期重采样(1m→5m→30m→日线)、复权因子处理 |
|
|
100
|
+
| 🤖 策略工程 | 回测框架对接、实盘信号监控脚本、通达信客户端联动(公式批量调用/行情订阅) |
|
|
101
|
+
| 🛠 私有部署 | 数据管线搭建、性能优化(批量解析全市场文件)、CI/自动发版 |
|
|
102
|
+
|
|
103
|
+
**联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxdata/issues) 提需求描述,或通过 [Gitee 私信](https://gitee.com/mengyiheng) 联系。复杂需求请附上:数据量级、时效要求(日终/盘中)、期望交付形式。
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tdxlocal"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "通达信本地二进制行情数据(.day / .lc1 / .lc5)读写库,零依赖"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "mengyiheng" }]
|
|
13
|
+
keywords = ["tdx", "tongdaxin", "stock", "kline", "quant"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
pandas = ["pandas"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
tdxdata = "tdxdata.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
tdxlocal-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""tdxdata — 读写通达信本地二进制行情数据(.day / .lc1 / .lc5)。
|
|
2
|
+
|
|
3
|
+
零第三方依赖;提供 pandas 支持时可选输出 DataFrame。
|
|
4
|
+
|
|
5
|
+
免责声明:本库仅处理本地已有数据文件,不提供任何行情数据,不构成投资建议。
|
|
6
|
+
数据仅供学习与量化研究使用,详见项目根目录 DISCLAIMER.md。
|
|
7
|
+
|
|
8
|
+
快速开始::
|
|
9
|
+
|
|
10
|
+
from tdxdata import read_day, read_minute
|
|
11
|
+
|
|
12
|
+
bars = read_day(r"D:\\new_tdx64\\vipdoc\\sh\\lday\\sh000001.day")
|
|
13
|
+
mins = read_minute(r"D:\\new_tdx\\vipdoc\\sh\\minline\\sh999999.lc1")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .day import DayBar, read_day, write_day
|
|
17
|
+
from .minute import MinuteBar, read_minute, write_minute
|
|
18
|
+
from .vipdoc import VipdocStore, find_bar_file
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DayBar", "read_day", "write_day",
|
|
24
|
+
"MinuteBar", "read_minute", "write_minute",
|
|
25
|
+
"VipdocStore", "find_bar_file",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""命令行工具:python -m tdxdata <day|lc1|lc5> <文件或数据目录> <代码> [输出.csv]"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .day import read_day
|
|
10
|
+
from .minute import read_minute
|
|
11
|
+
from .vipdoc import find_bar_file
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _dump(rows, header, out_path: str) -> None:
|
|
15
|
+
with open(out_path, "w", newline="", encoding="utf-8-sig") as f:
|
|
16
|
+
w = csv.writer(f)
|
|
17
|
+
w.writerow(header)
|
|
18
|
+
w.writerows([getattr(r, k) for k in header] for r in rows)
|
|
19
|
+
print(f"[tdxdata] {len(rows)} 条 -> {out_path}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main(argv: list[str] | None = None) -> int:
|
|
23
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
24
|
+
if len(argv) < 2:
|
|
25
|
+
print(__doc__)
|
|
26
|
+
return 1
|
|
27
|
+
kind, target = argv[0].lower(), argv[1]
|
|
28
|
+
code = argv[2] if len(argv) > 2 else None
|
|
29
|
+
out = argv[3] if len(argv) > 3 else "tdx_export.csv"
|
|
30
|
+
|
|
31
|
+
if kind == "day":
|
|
32
|
+
path = Path(target) if Path(target).is_file() else find_bar_file(target, code or "", "day")
|
|
33
|
+
_dump(read_day(path), ["date", "open", "high", "low", "close", "amount", "vol"], out)
|
|
34
|
+
elif kind in ("lc1", "lc5"):
|
|
35
|
+
path = Path(target) if Path(target).is_file() else find_bar_file(target, code or "", kind)
|
|
36
|
+
_dump(read_minute(path), ["datetime", "open", "high", "low", "close", "amount", "vol"], out)
|
|
37
|
+
else:
|
|
38
|
+
print(f"未知类型: {kind}")
|
|
39
|
+
return 1
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""通达信日线文件(vipdoc/<market>/lday/*.day)读写。
|
|
2
|
+
|
|
3
|
+
文件格式:每条记录 32 字节,小端 ``<IIIIIfII>``:
|
|
4
|
+
|
|
5
|
+
====== ======== ============================
|
|
6
|
+
偏移 类型 含义
|
|
7
|
+
====== ======== ============================
|
|
8
|
+
0 uint32 日期 YYYYMMDD
|
|
9
|
+
4 uint32 开盘价 * 100
|
|
10
|
+
8 uint32 最高价 * 100
|
|
11
|
+
12 uint32 最低价 * 100
|
|
12
|
+
16 uint32 收盘价 * 100
|
|
13
|
+
20 float32 成交额(元)
|
|
14
|
+
24 uint32 成交量(手)
|
|
15
|
+
28 uint32 保留字段(通常为 0)
|
|
16
|
+
====== ======== ============================
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import struct
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Iterable, List
|
|
24
|
+
|
|
25
|
+
from .records import DayBar
|
|
26
|
+
|
|
27
|
+
_STRUCT = struct.Struct("<IIIIIfII")
|
|
28
|
+
_PRICE_SCALE = 100.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_day(path: str | Path) -> List[DayBar]:
|
|
32
|
+
"""读取 .day 文件,按文件顺序返回 K 线列表。"""
|
|
33
|
+
data = Path(path).read_bytes()
|
|
34
|
+
n = len(data) // _STRUCT.size
|
|
35
|
+
bars: List[DayBar] = []
|
|
36
|
+
for i in range(n):
|
|
37
|
+
date, o, h, l, c, amount, vol, _ = _STRUCT.unpack_from(data, i * _STRUCT.size)
|
|
38
|
+
bars.append(DayBar(date, o / _PRICE_SCALE, h / _PRICE_SCALE,
|
|
39
|
+
l / _PRICE_SCALE, c / _PRICE_SCALE,
|
|
40
|
+
round(amount, 2), float(vol)))
|
|
41
|
+
return bars
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_day(path: str | Path, bars: Iterable[DayBar], *, append: bool = False) -> int:
|
|
45
|
+
"""把 K 线序列写入 .day 文件,返回写入条数。
|
|
46
|
+
|
|
47
|
+
append=True 时追加到文件末尾(用于增量更新,与通达信自身行为一致)。
|
|
48
|
+
"""
|
|
49
|
+
payload = bytearray()
|
|
50
|
+
count = 0
|
|
51
|
+
for b in bars:
|
|
52
|
+
payload += _STRUCT.pack(
|
|
53
|
+
int(b.date), round(b.open * _PRICE_SCALE), round(b.high * _PRICE_SCALE),
|
|
54
|
+
round(b.low * _PRICE_SCALE), round(b.close * _PRICE_SCALE),
|
|
55
|
+
float(b.amount), int(b.vol), 0)
|
|
56
|
+
count += 1
|
|
57
|
+
mode = "ab" if append else "wb"
|
|
58
|
+
with open(path, mode) as f:
|
|
59
|
+
f.write(payload)
|
|
60
|
+
return count
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""通达信分钟线文件(vipdoc/<market>/minline/*.lc1 | *.lc5)读写。
|
|
2
|
+
|
|
3
|
+
文件格式:每条记录 32 字节,小端 ``<IIIIIfII>``,但日期字段是**紧凑编码**:
|
|
4
|
+
|
|
5
|
+
====== ======== ====================================================
|
|
6
|
+
偏移 类型 含义
|
|
7
|
+
====== ======== ====================================================
|
|
8
|
+
0 uint32 紧凑日期:``year = d // 2048 + 2004``;
|
|
9
|
+
``month = (d % 2048) // 100``;``day = (d % 2048) % 100``
|
|
10
|
+
4 uint32 时间:``hour = t // 60``;``minute = t % 60``
|
|
11
|
+
8 uint32 开盘价 * 100
|
|
12
|
+
12 uint32 最高价 * 100
|
|
13
|
+
16 uint32 最低价 * 100
|
|
14
|
+
20 uint32 收盘价 * 100
|
|
15
|
+
24 float32 成交额(元)
|
|
16
|
+
28 uint32 成交量(手)
|
|
17
|
+
====== ======== ====================================================
|
|
18
|
+
|
|
19
|
+
注意与日线不同:分钟线没有保留字段,最后一个 uint32 就是成交量。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import struct
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Iterable, List
|
|
27
|
+
|
|
28
|
+
from .records import MinuteBar
|
|
29
|
+
|
|
30
|
+
_STRUCT = struct.Struct("<IIIIIIfI")
|
|
31
|
+
_PRICE_SCALE = 100.0
|
|
32
|
+
_EPOCH_YEAR = 2004
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _unpack_date(raw: int) -> tuple[int, int, int]:
|
|
36
|
+
rest = raw % 2048
|
|
37
|
+
return raw // 2048 + _EPOCH_YEAR, rest // 100, rest % 100
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _pack_date(year: int, month: int, day: int) -> int:
|
|
41
|
+
return (year - _EPOCH_YEAR) * 2048 + month * 100 + day
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_minute(path: str | Path) -> List[MinuteBar]:
|
|
45
|
+
"""读取 .lc1 / .lc5 文件,按文件顺序返回分钟 K 线列表。"""
|
|
46
|
+
data = Path(path).read_bytes()
|
|
47
|
+
n = len(data) // _STRUCT.size
|
|
48
|
+
bars: List[MinuteBar] = []
|
|
49
|
+
for i in range(n):
|
|
50
|
+
raw_date, raw_time, o, h, l, c, amount, vol = _STRUCT.unpack_from(data, i * _STRUCT.size)
|
|
51
|
+
year, month, day = _unpack_date(raw_date)
|
|
52
|
+
hour, minute = divmod(raw_time, 60)
|
|
53
|
+
bars.append(MinuteBar(
|
|
54
|
+
f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}",
|
|
55
|
+
o / _PRICE_SCALE, h / _PRICE_SCALE, l / _PRICE_SCALE, c / _PRICE_SCALE,
|
|
56
|
+
round(amount, 2), float(vol)))
|
|
57
|
+
return bars
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def write_minute(path: str | Path, bars: Iterable[MinuteBar], *, append: bool = False) -> int:
|
|
61
|
+
"""把分钟 K 线序列写入 .lc1 / .lc5 文件,返回写入条数。
|
|
62
|
+
|
|
63
|
+
``datetime`` 接受 ``"YYYY-MM-DD HH:MM"`` 或 ``"YYYYMMDD HHMM"`` 两种形式。
|
|
64
|
+
"""
|
|
65
|
+
payload = bytearray()
|
|
66
|
+
count = 0
|
|
67
|
+
for b in bars:
|
|
68
|
+
parts = "".join(ch if ch.isdigit() else " " for ch in b.datetime).split()
|
|
69
|
+
if len(parts) >= 5: # "2026-01-15 09:31"
|
|
70
|
+
year, month, day, hour, minute = (int(x) for x in parts[:5])
|
|
71
|
+
else: # "20260116 1405"
|
|
72
|
+
year, month, day = int(parts[0][:4]), int(parts[0][4:6]), int(parts[0][6:8])
|
|
73
|
+
hour, minute = int(parts[1][:2]), int(parts[1][2:4])
|
|
74
|
+
payload += _STRUCT.pack(
|
|
75
|
+
_pack_date(year, month, day), hour * 60 + minute,
|
|
76
|
+
round(b.open * _PRICE_SCALE), round(b.high * _PRICE_SCALE),
|
|
77
|
+
round(b.low * _PRICE_SCALE), round(b.close * _PRICE_SCALE),
|
|
78
|
+
float(b.amount), int(b.vol))
|
|
79
|
+
count += 1
|
|
80
|
+
mode = "ab" if append else "wb"
|
|
81
|
+
with open(path, mode) as f:
|
|
82
|
+
f.write(payload)
|
|
83
|
+
return count
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""基础 K 线记录结构。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class DayBar:
|
|
10
|
+
"""日线一根 K 线。
|
|
11
|
+
|
|
12
|
+
amount 单位为元,vol 单位为手(与通达信原始文件一致)。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
date: int # YYYYMMDD
|
|
16
|
+
open: float
|
|
17
|
+
high: float
|
|
18
|
+
low: float
|
|
19
|
+
close: float
|
|
20
|
+
amount: float
|
|
21
|
+
vol: float
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class MinuteBar:
|
|
26
|
+
"""分钟线一根 K 线(lc1 = 1 分钟,lc5 = 5 分钟)。
|
|
27
|
+
|
|
28
|
+
amount 单位为元,vol 单位为手。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
datetime: str # "YYYY-MM-DD HH:MM"
|
|
32
|
+
open: float
|
|
33
|
+
high: float
|
|
34
|
+
low: float
|
|
35
|
+
close: float
|
|
36
|
+
amount: float
|
|
37
|
+
vol: float
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""按通达信 vipdoc 目录约定定位数据文件。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_MARKET_BY_PREFIX = {"sh": 1, "sz": 0, "bj": 2}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def market_of(code: str) -> str:
|
|
11
|
+
"""根据 6 位代码推断市场(sh/sz/bj)。"""
|
|
12
|
+
if code.startswith(("6", "5", "9")):
|
|
13
|
+
return "sh"
|
|
14
|
+
if code.startswith(("4", "8")):
|
|
15
|
+
return "bj"
|
|
16
|
+
return "sz"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_bar_file(vipdoc: str | Path, code: str, kind: str) -> Path:
|
|
20
|
+
"""在 vipdoc 目录中定位单个证券的数据文件。
|
|
21
|
+
|
|
22
|
+
kind: ``"day"`` -> lday/*.day;``"lc1"``/``"lc5"`` -> minline/*.lc1|*.lc5
|
|
23
|
+
"""
|
|
24
|
+
kind = kind.lower()
|
|
25
|
+
market = market_of(code)
|
|
26
|
+
if kind == "day":
|
|
27
|
+
p = Path(vipdoc) / market / "lday" / f"{market}{code}.day"
|
|
28
|
+
elif kind in ("lc1", "lc5", "1", "5", "min1", "min5"):
|
|
29
|
+
suffix = "lc1" if kind in ("lc1", "1", "min1") else "lc5"
|
|
30
|
+
p = Path(vipdoc) / market / "minline" / f"{market}{code}.{suffix}"
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError(f"未知类型: {kind!r}(支持 day / lc1 / lc5)")
|
|
33
|
+
if not p.exists():
|
|
34
|
+
raise FileNotFoundError(f"未找到数据文件: {p}")
|
|
35
|
+
return p
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VipdocStore:
|
|
39
|
+
"""vipdoc 目录的便捷入口,自动识别市场前缀并读写。"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, vipdoc: str | Path):
|
|
42
|
+
self.vipdoc = Path(vipdoc)
|
|
43
|
+
|
|
44
|
+
def read(self, code: str, kind: str = "day"):
|
|
45
|
+
"""按代码读取 K 线。kind: day / lc1 / lc5。"""
|
|
46
|
+
path = find_bar_file(self.vipdoc, code, kind)
|
|
47
|
+
from . import day as _day, minute as _minute
|
|
48
|
+
|
|
49
|
+
return _day.read_day(path) if kind.lower() == "day" else _minute.read_minute(path)
|
|
50
|
+
|
|
51
|
+
def update(self, code: str, bars, kind: str = "day") -> int:
|
|
52
|
+
"""增量追加 K 线(与通达信盘中落盘方式一致)。"""
|
|
53
|
+
path = find_bar_file(self.vipdoc, code, kind)
|
|
54
|
+
from . import day as _day, minute as _minute
|
|
55
|
+
|
|
56
|
+
writer = _day.write_day if kind.lower() == "day" else _minute.write_minute
|
|
57
|
+
return writer(path, bars, append=True)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tdxlocal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 通达信本地二进制行情数据(.day / .lc1 / .lc5)读写库,零依赖
|
|
5
|
+
Author: mengyiheng
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: tdx,tongdaxin,stock,kline,quant
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: pandas
|
|
18
|
+
Requires-Dist: pandas; extra == "pandas"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
<p align="center"><img src="assets/cover.png" alt="tdxdata" width="720"/></p>
|
|
22
|
+
|
|
23
|
+
# tdxdata — 通达信本地行情数据读写库
|
|
24
|
+
|
|
25
|
+
读取和生成通达信(TDX)本地二进制 K 线文件:**`.day` 日线**、**`.lc1` 1 分钟线**、**`.lc5` 5 分钟线**。
|
|
26
|
+
|
|
27
|
+
- 🪶 **零第三方依赖**(pandas 可选)
|
|
28
|
+
- 📖 **读取**:一行代码解析为 Python 对象列表
|
|
29
|
+
- ✍️ **生成 / 增量更新**:与通达信客户端落盘格式逐字节一致,可被客户端直接识别
|
|
30
|
+
- 🗂 **vipdoc 目录约定**:按证券代码自动定位文件、自动推断市场(sh/sz/bj)
|
|
31
|
+
- 🖥 **命令行**:`tdxdata day <vipdoc> 000001 out.csv` 一键导出 CSV
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install tdxlocal # 含 pandas 支持
|
|
37
|
+
pip install tdxlocal --no-deps # 纯标准库
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 文件格式
|
|
41
|
+
|
|
42
|
+
两类文件均为 **每条记录 32 字节、小端 `<IIIIIfII>`**,但日期字段编码不同:
|
|
43
|
+
|
|
44
|
+
| 字段 | 日线 `.day` | 分钟线 `.lc1/.lc5` |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| 0 | 日期 `YYYYMMDD` 明文 | 紧凑日期 `(year-2004)*2048 + month*100 + day` |
|
|
47
|
+
| 4 | 开*100 | 时间 `hour*60 + minute` |
|
|
48
|
+
| 8–20 | 高/低/收 *100 | 高/低/收/开 *100 |
|
|
49
|
+
| 24 | 成交额 float32(元) | 成交额 float32(元) |
|
|
50
|
+
| 28 | 成交量(手)+ 保留字段 | 成交量(手) |
|
|
51
|
+
|
|
52
|
+
## 快速开始
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from tdxdata import read_day, read_minute, write_day, DayBar
|
|
56
|
+
|
|
57
|
+
# 读取日线
|
|
58
|
+
bars = read_day(r"D:\new_tdx64\vipdoc\sh\lday\sh000001.day")
|
|
59
|
+
print(bars[-1]) # DayBar(date=20260116, open=..., high=..., ...)
|
|
60
|
+
|
|
61
|
+
# 读取 1 分钟线
|
|
62
|
+
mins = read_minute(r"D:\new_tdx\vipdoc\sh\minline\sh999999.lc1")
|
|
63
|
+
|
|
64
|
+
# 生成日线文件(通达信可直接打开)
|
|
65
|
+
write_day("my.day", [DayBar(20260116, 10.0, 10.5, 9.9, 10.2, 1.2e8, 123456)])
|
|
66
|
+
|
|
67
|
+
# 增量更新(追加模式,与通达信盘中落盘一致)
|
|
68
|
+
write_day(path, [DayBar(...)], append=True)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### vipdoc 便捷入口
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from tdxdata import VipdocStore
|
|
75
|
+
|
|
76
|
+
store = VipdocStore(r"D:\new_tdx64\vipdoc")
|
|
77
|
+
bars = store.read("600519", "day") # 自动定位 sh/lday/sh600519.day
|
|
78
|
+
store.update("600519", [DayBar(...)]) # 增量追加
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 可选:输出 pandas DataFrame
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import pandas as pd
|
|
85
|
+
from dataclasses import asdict
|
|
86
|
+
from tdxdata import read_day
|
|
87
|
+
|
|
88
|
+
df = pd.DataFrame([asdict(b) for b in read_day(path)])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 命令行
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
tdxdata day D:\new_tdx64\vipdoc 000001 sh000001.csv
|
|
95
|
+
tdxdata lc1 D:\new_tdx\vipdoc 999999 sh999999.csv
|
|
96
|
+
python -m tdxdata day D:\new_tdx64\vipdoc\sh\lday\sh000001.day out.csv
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 运行测试
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install -e . pytest
|
|
103
|
+
pytest
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
测试自带合成数据做逐字节往返校验;若本机存在 `D:\new_tdx64\vipdoc`,会自动附加真实数据冒烟测试(缺失则跳过)。
|
|
107
|
+
|
|
108
|
+
## 免责声明
|
|
109
|
+
|
|
110
|
+
本项目仅读取/生成**本地已有**的通达信数据文件,不提供任何行情数据,不构成任何投资建议。数据仅供学习与量化研究使用,不作为实盘交易依据,使用前请[阅读完整免责声明](DISCLAIMER.md)。**投资有风险,入市需谨慎。**
|
|
111
|
+
|
|
112
|
+
## 量化相关定制需求
|
|
113
|
+
|
|
114
|
+
作者承接基于本库及量化研究场景的**定制开发**,包括但不限于:
|
|
115
|
+
|
|
116
|
+
| 方向 | 示例 |
|
|
117
|
+
|---|---|
|
|
118
|
+
| 📈 数据扩展 | `.zst`/`.档位` 等其他 TDX 文件解析、盘中增量落盘监控、CSV/parquet/SQLite 批量导出 |
|
|
119
|
+
| 🐍 量化工具链 | pandas/Polars 适配层、多周期重采样(1m→5m→30m→日线)、复权因子处理 |
|
|
120
|
+
| 🤖 策略工程 | 回测框架对接、实盘信号监控脚本、通达信客户端联动(公式批量调用/行情订阅) |
|
|
121
|
+
| 🛠 私有部署 | 数据管线搭建、性能优化(批量解析全市场文件)、CI/自动发版 |
|
|
122
|
+
|
|
123
|
+
**联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxdata/issues) 提需求描述,或通过 [Gitee 私信](https://gitee.com/mengyiheng) 联系。复杂需求请附上:数据量级、时效要求(日终/盘中)、期望交付形式。
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/tdxdata/__init__.py
|
|
5
|
+
src/tdxdata/__main__.py
|
|
6
|
+
src/tdxdata/cli.py
|
|
7
|
+
src/tdxdata/day.py
|
|
8
|
+
src/tdxdata/minute.py
|
|
9
|
+
src/tdxdata/records.py
|
|
10
|
+
src/tdxdata/vipdoc.py
|
|
11
|
+
src/tdxlocal.egg-info/PKG-INFO
|
|
12
|
+
src/tdxlocal.egg-info/SOURCES.txt
|
|
13
|
+
src/tdxlocal.egg-info/dependency_links.txt
|
|
14
|
+
src/tdxlocal.egg-info/entry_points.txt
|
|
15
|
+
src/tdxlocal.egg-info/requires.txt
|
|
16
|
+
src/tdxlocal.egg-info/top_level.txt
|
|
17
|
+
tests/test_tdxdata.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tdxdata
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import struct
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from tdxdata import DayBar, MinuteBar, read_day, read_minute, write_day, write_minute
|
|
9
|
+
from tdxdata.vipdoc import market_of
|
|
10
|
+
|
|
11
|
+
# ---------- 合成数据:逐字节往返 ----------
|
|
12
|
+
|
|
13
|
+
def test_day_roundtrip(tmp_path):
|
|
14
|
+
bars = [
|
|
15
|
+
DayBar(20260115, 10.0, 10.5, 9.9, 10.2, 1.234e8, 123456),
|
|
16
|
+
DayBar(20260116, 10.2, 10.8, 10.1, 10.6, 9.876e7, 98765),
|
|
17
|
+
]
|
|
18
|
+
p = tmp_path / "test.day"
|
|
19
|
+
assert write_day(p, bars) == 2
|
|
20
|
+
assert p.stat().st_size == 64
|
|
21
|
+
|
|
22
|
+
# 手工核对原始字节:第一条
|
|
23
|
+
raw = struct.unpack("<IIIIIfII", p.read_bytes()[:32])
|
|
24
|
+
assert raw[0] == 20260115
|
|
25
|
+
assert raw[1] == 1000 and raw[2] == 1050 and raw[3] == 990 and raw[4] == 1020
|
|
26
|
+
assert raw[6] == 123456
|
|
27
|
+
|
|
28
|
+
back = read_day(p)
|
|
29
|
+
assert back[0].date == 20260115
|
|
30
|
+
assert back[0].open == pytest.approx(10.0)
|
|
31
|
+
assert back[1].close == pytest.approx(10.6)
|
|
32
|
+
assert [asdict(b) for b in back][1]["vol"] == 98765
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_day_append(tmp_path):
|
|
36
|
+
p = tmp_path / "a.day"
|
|
37
|
+
write_day(p, [DayBar(20260101, 1, 1, 1, 1, 0, 1)])
|
|
38
|
+
write_day(p, [DayBar(20260102, 1, 1, 1, 1, 0, 2)], append=True)
|
|
39
|
+
assert len(read_day(p)) == 2
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_minute_roundtrip(tmp_path):
|
|
43
|
+
bars = [
|
|
44
|
+
MinuteBar("2026-01-15 09:31", 3.45, 3.5, 3.4, 3.48, 5e6, 1500),
|
|
45
|
+
MinuteBar("2026-01-15 09:32", 3.48, 3.52, 3.47, 3.5, 6e6, 1800),
|
|
46
|
+
]
|
|
47
|
+
p = tmp_path / "test.lc5"
|
|
48
|
+
assert write_minute(p, bars) == 2
|
|
49
|
+
assert p.stat().st_size == 64
|
|
50
|
+
|
|
51
|
+
# 紧凑日期字段核对: (2026-2004)*2048 + 1*100 + 15
|
|
52
|
+
raw = struct.unpack("<IIIIIfII", p.read_bytes()[:32])
|
|
53
|
+
assert raw[0] == (2026 - 2004) * 2048 + 115
|
|
54
|
+
assert raw[1] == 9 * 60 + 31
|
|
55
|
+
|
|
56
|
+
back = read_minute(p)
|
|
57
|
+
assert back[0].datetime == "2026-01-15 09:31"
|
|
58
|
+
assert back[0].open == pytest.approx(3.45)
|
|
59
|
+
assert back[1].high == pytest.approx(3.52)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_minute_datetime_alt_format(tmp_path):
|
|
63
|
+
p = tmp_path / "b.lc1"
|
|
64
|
+
write_minute(p, [MinuteBar("20260116 1405", 1, 1, 1, 1, 0, 1)])
|
|
65
|
+
assert read_minute(p)[0].datetime == "2026-01-16 14:05"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_market_of():
|
|
69
|
+
assert market_of("600519") == "sh"
|
|
70
|
+
assert market_of("510300") == "sh"
|
|
71
|
+
assert market_of("000001") == "sz"
|
|
72
|
+
assert market_of("300750") == "sz"
|
|
73
|
+
assert market_of("830799") == "bj"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------- 真实数据冒烟(本机有通达信数据时) ----------
|
|
77
|
+
|
|
78
|
+
REAL = Path(r"D:\new_tdx64\vipdoc\sh\lday\sh000001.day")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.skipif(not REAL.exists(), reason="本机无通达信日线数据")
|
|
82
|
+
def test_real_day_file():
|
|
83
|
+
bars = read_day(REAL)
|
|
84
|
+
assert len(bars) == REAL.stat().st_size // 32
|
|
85
|
+
assert bars == sorted(bars, key=lambda b: b.date)
|
|
86
|
+
last = bars[-1]
|
|
87
|
+
assert 1990 <= last.date // 10000 <= 2100
|
|
88
|
+
assert last.low <= last.open <= last.high
|
|
89
|
+
assert last.low <= last.close <= last.high
|
|
90
|
+
# 写出再读回,逐条一致(金额 float32 有 ~7 位精度)
|
|
91
|
+
out = io.BytesIO()
|
|
92
|
+
p = Path("_)tmp_check.day")
|
|
93
|
+
try:
|
|
94
|
+
write_day(p, bars)
|
|
95
|
+
assert read_day(p) == bars
|
|
96
|
+
finally:
|
|
97
|
+
p.unlink(missing_ok=True)
|