Python解析通达信日线.day二进制文件:格式、读取与批量入库
2026/9/12 11:56:36 网站建设 项目流程

简介:面向量化投资与金融数据分析的Python实用脚本,解决从通达信(TDX)本地数据文件中批量提取股票日线数据的难题,适合需要对接通达信行情数据、开展数据分析和策略回测的Python开发者。压缩包内共1个文件,为ReadFile.py脚本,整体大小仅2KB,脚本虽短小,但完整涵盖安装路径定位、数据文件解析、字段清洗以及转为pandas DataFrame等关键流程,可适配用户自定义的通达信安装目录。脚本使用os、pandas、struct等模块,演示解析日期、开盘价、收盘价、最高价、最低价和成交量等核心字段,并输出结构化的数据供后续分析。目前已有2192人学习浏览。通过研读此脚本,读者能掌握读取非标准金融数据文件的常用思路,直接获得可运行的日线数据提取工具,为后续行情分析、量化建模和投资决策提供高效、可复用的数据基础。

1. 通达信日线数据其实是个二进制文件,readFile_tdx 干的就是解析它

写回测脚本最卡壳的一步不是策略,而是行情从哪来。拉公开接口要鉴权、要限频,盘中可能被断流,下班回家网络一波动,整个研究环境就瘫了。通达信客户端只要日常打开过,收盘后就会把全市场日线写进本地vipdoc/**/lday/*.day文件里,这些文件没有索引、没有表头,每条 K 线固定占用 32 字节。标题里的ReadFile.rar是这类解析工具在论坛里最常见的打包名,里面的 readFile_tdx 本质就是把 32 字节二进制块掰成日期、开高低收、成交额、成交量。这套姿势和 tdx 服务端接口无关,纯本地、极快,适合离线回测和数据归档。本文直接用 Python 标准库加 pandas 把它写出来,顺带把批量入库、增量更新和文件校验一起收尾。

2. 通达信 .day 日线格式:32 字节一条记录,字段顺序别搞反

2.1 vipdoc 目录下的文件名怎么对应市场与代码

通达信安装目录下能找到vipdoc文件夹,里面按交易所分了shsz等子目录,日线文件统一放在lday子目录下,文件名格式是“市场前缀 + 6 位证券代码 + .day”。例如沪市贵州茅台的日线文件是sh600519.day,深市平安银行是sz000001.day。想读取哪只股票,先在本地把这个路径拼出来,再交给解析函数。

这个文件名规则同时也是数据完整性的第一道防线:程序里习惯把fp.stem直接当作证券代码字段写入 DataFrame,这样后面按股票分组、做增量更新都不需要额外维护映射表。部分通达신版本还支持北交所等新市场,目录前缀可能不同,但结构一致:交易所目录/lday/前缀代码.day。我一般扫描时直接glob('*/lday/*.day'),不写死市场列表,新增市场也能自动覆盖。

2.2 32 字节记录的字节级字段表

.day文件是典型的二进制定长记录文件,每条 K 线固定 32 字节,字节序为小端(x86 架构默认)。字段顺序按照通达信约定固定不变,错一个字节,后面的数据全部错位。官方没有公开过格式文档,这是多年逆向和经验积累出的通用布局:

偏移量字节数类型字段名存储说明
04uint32日期形如 20240105,无分隔符
44uint32开盘价实际价格放大 100 倍后取整存放
84uint32最高价同上
124uint32最低价同上
164uint32收盘价同上
204float32成交额单位:元
244uint32成交量单位:股
284uint32保留字段通常为 0,不参与计算

价格字段用整数存放大值,这一设计很关键:A 股价格精确到分,把 7.91 存成 791,比存浮点更稳定,也避免了 float32 在反复计算时累积误差。解析后必须手动除以 100 还原。<IIIIIfII是标准的 struct 解包格式串,其中<表示小端,I是无符号 32 位整数,f是单精度浮点。成交额列用的是 float32,单精度只有约 7 位有效数字,遇到单日成交额几百亿的极端情况会有几十元误差,做资金流分析时要留意,普通量价回测不受影响。

2.3 打开文件先做 32 字节对齐校验

复制过的 .day 文件偶尔会出现尾部残缺,原因可能是硬盘坏道、U 盘未安全弹出或通达信写盘时被强制关闭。读取之前先做一次字节数对齐校验,能避免解析到一半莫名报错。

from pathlib import Path for fp in Path('vipdoc/sh/lday').glob('*.day'): size = fp.stat().st_size if size % 32 != 0: print(f'{fp.name}: {size} 字节,不是 32 的倍数,尾部有残缺') elif size == 0: print(f'{fp.name}: 空文件')

这段脚本只做两件事:先通过st_size取文件字节数,再判断能否被 32 整除。文件大小正好是 32 的整数倍,说明每条 K 线记录都完整;有余数意味着最后一条记录被截断。glob('*.day')只匹配当前目录,不递归;如果要扫描沪深两市,把路径写成Path('vipdoc').glob('*/lday/*.day')即可。校验通过后,才能放心交给后续的解析函数。

3. 用 Python 写 readFile_tdx:一个函数读完全部日线

论坛里流转的ReadFile.rar解压出来,脚本命名出现过readFile_tdx.pyreadFile_tentmev.py几个版本,名字有出入,做的事情完全一样。我一般不会直接用压缩包里的代码,因为里面经常混着过时的 print 调试和硬编码路径,而是按下面这个最小实现自己维护。

3.1 struct.unpack 逐个解析 32 字节,所有字段一次解出

import struct from pathlib import Path import pandas as pd TDX_DAY_STRUCT = '<IIIIIfII' def readFile_tdx(file_path: str | Path) -> pd.DataFrame: """读取通达信日线 .day 文件,返回 DataFrame""" fp = Path(file_path) if not fp.exists() or fp.stat().st_size == 0: return pd.DataFrame(columns=['date', 'open', 'high', 'low', 'close', 'amount', 'volume']) rows = [] with fp.open('rb') as f: while True: block = f.read(32) if len(block) < 32: break date_int, open_px, high_px, low_px, close_px, amount, volume, reserved = \ struct.unpack(TDX_DAY_STRUCT, block) rows.append((date_int, open_px / 100.0, high_px / 100.0, low_px / 100.0, close_px / 100.0, amount, volume)) df = pd.DataFrame(rows, columns=['date', 'open', 'high', 'low', 'close', 'amount', 'volume']) df['date'] = pd.to_datetime(df['date'].astype(str), format='%Y%m%d') return df.sort_values('date').reset_index(drop=True)

f.read(32)每次读一条固定长度记录;len(block) < 32时直接 break,等于把 2.3 里的尾部残缺校验内聚到了读取函数里,碰到不完整块就静默丢弃,不抛异常中断整个批次。struct.unpack返回 8 个值,一一对应字段表。价格字段全部除以 100.0,这里用浮点除法而不是整除,避免价差被截断。日期列先用astype(str)把整数20240105变成字符串再交给pd.to_datetimeformat='%Y%m%d'是为了明确告诉 pandas 日期的排列方式,不加会走默认推断,慢且容易出警告。

3.2 numpy.fromfile 向量化版本,读取速度更快

struct 循环对每根 K 线做一次 unpack,Python 函数调用开销叠加后,读全市场五千只股票会明显发慢。换 numpy 的fromfile可以直接把整个文件映射成结构化数组,C 层面完成解析,速度提升一个数量级。

import numpy as np TDX_DAY_DTYPE = np.dtype([ ('date', '<u4'), ('open', '<u4'), ('high', '<u4'), ('low', '<u4'), ('close', '<u4'), ('amount', '<f4'), ('volume', '<u4'), ('reserved', '<u4'), ]) def readFile_tdx_np(file_path: str | Path) -> pd.DataFrame: fp = Path(file_path) if not fp.exists() or fp.stat().st_size == 0: return pd.DataFrame(columns=['date', 'open', 'high', 'low', 'close', 'amount', 'volume']) n = fp.stat().st_size // TDX_DAY_DTYPE.itemsize arr = np.fromfile(fp, dtype=TDX_DAY_DTYPE, count=n) df = pd.DataFrame({ 'date': pd.to_datetime(arr['date'].astype(str), format='%Y%m%d'), 'open': arr['open'] / 100.0, 'high': arr['high'] / 100.0, 'low': arr['low'] / 100.0, 'close': arr['close'] / 100.0, 'amount': arr['amount'], 'volume': arr['volume'], }) return df.sort_values('date').reset_index(drop=True)

np.dtype里的字段名和类型必须和 struct 版本严格一致,'<u4'表示小端无符号 4 字节整数,'<f4'表示小端单精度浮点。itemsize自动计算为 32,st_size // itemsize算出完整记录条数,count参数保证即使文件尾部有残缺,numpy 也只会读取完整记录,不会抛出字节数不匹配的错误。这个细节容易踩坑:不传countfromfile遇到不完整的结构化记录可能直接抛异常,传了就等于把 2.3 的校验下沉到了读取层。

3.3 价格还原、日期处理和异常 K 线过滤

结构化数组直接取列再除 100,比 struct 版本代码更短,但有个隐含差异:numpy 的u4除法会产生浮点数组,amount字段读出来是 float32,直接丢进 DataFrame 后精度保持不了那么多位。若对成交额精度敏感,可以在 DataFrame 构造后round(2),否则只影响展示,不影响回测计算。

数据进 DataFrame 后还缺两道清洗。第一,通达信部分版本写盘顺序并非严格升序,尤其是手工复制过的文件,因此最后统一sort_values('date')并把索引重置。第二,市场偶尔出现停牌日,连续几天没有成交,成交量字段为 0 是合法的,但收盘价为 0 的脏数据需要剔除:

df = df[df['close'] > 0] df = df.drop_duplicates(subset=['date'])

close > 0过滤掉未初始化的空记录;drop_duplicates以日期为键去重,防止通达信两次写盘导致同一根 K 线出现两条。过滤逻辑要放在日期转换之后,因为null日期参与排序会干扰结果。

4. 批量读取全部 A 股日线,落成 Parquet 并做增量更新

4.1 用 pathlib 遍历 vipdoc,一次读出全市场日线

单只股票的读取函数能跑通后,下一步是把整个vipdoc目录变成一个大 DataFrame。常见做法是按“市场目录/lday”的层级做一次递归遍历,把所有.day文件路径收集起来,逐个调用读取函数,再给每个 DataFrame 打上证券代码标签。

def build_daily_library(vipdoc_root: str | Path) -> pd.DataFrame: root = Path(vipdoc_root) files = sorted(root.glob('*/lday/*.day')) print(f'发现 {len(files)} 个日线文件') frames = [] for fp in files: df = readFile_tdx_np(fp) if df.empty: continue df['code'] = fp.stem frames.append(df) return pd.concat(frames, ignore_index=True)

glob('*/lday/*.day')两层通配能同时覆盖shsz目录,fp.stem取出文件名去掉后缀的部分,正好是sh600519这样的完整证券标识。pd.concat时设置ignore_index=True重建全局索引,避免每只股票都从 0 开始导致后面分组操作混乱。这个函数的瓶颈不在解析,而在 DataFrame 构造,五千个文件逐个构造小 DataFrame 再合并,总体耗时几十秒量级,跑批可以接受。

4.2 CSV、Parquet 还是 SQLite:存储取舍

全市场日线常年累计下来会有几百万行,每次重读.day文件虽然快,但重复解析同一份数据不值得。读取一遍后把它缓存成中间格式,后续研究直接加载缓存。三种常见存储方案各有定位:

存储格式读取速度压缩率适用场景
CSV临时交换、肉眼检查
Parquet分析主存储,列式读取
SQLite中等中等频繁按 code 点查与单条更新

我用得最多的是 Parquet,按列存储,回测时只加载dateclose两列比全量加载省很多 IO。写入时指定pyarrow引擎和zstd压缩,压缩率通常能到原始 CSV 的五分之一以上:

df.to_parquet('daily_lib.parquet', engine='pyarrow', compression='zstd', index=False)

Parquet 的劣势是不支持原地更新,增量合并时要把新老数据读出来再整体覆写。如果不需要列存加速,SQLite 按code + date建唯一索引后做 upsert 更方便,适合小体量的个人行情库。CSV 除非要发给别人看,否则不建议作为主存储,几百万行 CSV 光解析就要花几十秒。

4.3 增量更新:最后 K 线日期到今天的间隔决定要不要重读

每次收盘后全量重建行情库有点浪费,更合理的做法是只追加缺失日期。判断标准很简单:拿现有库中每只股票的最后 K 线日期,和当天日期算间隔,超过阈值就触发重读。

from datetime import timedelta last_dates = df.groupby('code')['date'].max() today = pd.Timestamp.today().normalize() stale = last_dates[last_dates < today - pd.Timedelta(days=5)] print(f'有 {len(stale)} 只股票超过 5 个自然日未更新')

这里面有个容易误解的点:文件日期和刷新时间不是一回事。通达信收盘后不一定立即把所有股票写盘,某些冷门股可能延迟到次日才补数据,所以 5 个自然日的阈值比“今天必须更新”更稳妥。groupby('code')['date'].max()是按证券代码取每个分组最大日期,normalize()把当前时间归零到当天零点,避免小时分钟干扰日期差计算。拿到过期名单后再去对应.day文件增量解析,最后合并写回 cache。这个思路等同于搜索里常问的“通达信指定日期到今天的天数”,只不过日期不是手动输入,而是从最后一条 K 线里取。

5. 文件完整性与除权缺口:两个验证技巧

5.1 用文件大小与 K 线数量做一致性校验

数据入库后要先证明没问题,再拿去做回测。最简单的一致性校验是交叉验证:文件大小除以 32 等于理论 K 线条数,解析结果的行数必须和它一致。任何对不上都说明解析有遗漏或文件已经损坏。

for fp in sorted(Path('vipdoc/sh/lday').glob('*.day')): size = fp.stat().st_size expected = size // 32 df = readFile_tdx_np(fp) if size % 32 != 0: print(f'{fp.name}: 文件字节数异常') elif len(df) != expected: print(f'{fp.name}: 解析 {len(df)} 条,期望 {expected} 条')

expected是理论完整记录条数,len(df)是实际解析条数。正常情况下两者相等;不等时优先怀疑 3.3 的过滤条件误伤了合法记录,比如close为 0 的历史原始数据。运行完整个目录后,批处理日志里出现过的文件名应该为零,这就是全库健康的静态证据。这个脚本耗时很短,适合每次跑增量前垫一道。

5.2 用相邻 K 线跳空定位除权日,维护复权因子表

.day文件本身是不复权数据,除权除息日当天价格会垂直跳空,前收盘价和后开盘价之间的缺口会被回测误认为巨大跌幅。识别这类缺口有一个廉价方法:计算每根 K 线开盘价相对前一根收盘价的变化率,超过正常涨跌停幅度(如 9.8%)的位置就标记为疑似除权日。

prev_close = df['close'].shift(1) gap = df['open'] / prev_close - 1 suspects = df.index[(gap.abs() > 0.098) & (prev_close > 0)]

shift(1)把收盘价整体下移一行,形成“前一根收盘价”序列;开盘价除以它减 1 得到跳空幅度。prev_close > 0排除停牌后复牌导致的空值。涨停、跌停也会触发大于 9.8% 的阈值,所以这张表只是嫌疑名单,要和通达信页面的除权信息复核。确认是除权日后,把因子记入独立的复权因子表,后续计算前复权价时用累计因子乘回去。记住一个原则:原始.day永远保留不复权版本,复权结果单独生成,这样因子表出错了还能一键重算。把复权因子表单独存档,增量更新时用同一组规则重算一遍即可。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询