ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Python解析通达信日线.day二进制文件:格式、读取与批量入库

Python解析通达信日线.day二进制文件:格式、读取与批量入库 简介面向量化投资与金融数据分析的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文件夹里面按交易所分了sh、sz等子目录日线文件统一放在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.py、readFile_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(dropTrue)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 线做一次 unpackPython 函数调用开销叠加后读全市场五千只股票会明显发慢。换 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, dtypeTDX_DAY_DTYPE, countn) 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(dropTrue)np.dtype里的字段名和类型必须和 struct 版本严格一致u4表示小端无符号 4 字节整数f4表示小端单精度浮点。itemsize自动计算为 32st_size // itemsize算出完整记录条数count参数保证即使文件尾部有残缺numpy 也只会读取完整记录不会抛出字节数不匹配的错误。这个细节容易踩坑不传count时fromfile遇到不完整的结构化记录可能直接抛异常传了就等于把 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_indexTrue)glob(*/lday/*.day)两层通配能同时覆盖sh和sz目录fp.stem取出文件名去掉后缀的部分正好是sh600519这样的完整证券标识。pd.concat时设置ignore_indexTrue重建全局索引避免每只股票都从 0 开始导致后面分组操作混乱。这个函数的瓶颈不在解析而在 DataFrame 构造五千个文件逐个构造小 DataFrame 再合并总体耗时几十秒量级跑批可以接受。4.2 CSV、Parquet 还是 SQLite存储取舍全市场日线常年累计下来会有几百万行每次重读.day文件虽然快但重复解析同一份数据不值得。读取一遍后把它缓存成中间格式后续研究直接加载缓存。三种常见存储方案各有定位存储格式读取速度压缩率适用场景CSV慢低临时交换、肉眼检查Parquet快高分析主存储列式读取SQLite中等中等频繁按 code 点查与单条更新我用得最多的是 Parquet按列存储回测时只加载date、close两列比全量加载省很多 IO。写入时指定pyarrow引擎和zstd压缩压缩率通常能到原始 CSV 的五分之一以上df.to_parquet(daily_lib.parquet, enginepyarrow, compressionzstd, indexFalse)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(days5)] 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永远保留不复权版本复权结果单独生成这样因子表出错了还能一键重算。把复权因子表单独存档增量更新时用同一组规则重算一遍即可。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表