☰
通达信.day文件解析与pytdx本地数据管线搭建:量化回测的可靠数据源
2026/10/3 4:46:06 网站建设 项目流程

做量化的朋友应该都有这种感觉:策略写起来不难,难的是搞到干净、完整、可复现的历史数据。我早年习惯从各种数据平台倒数据,不是格式混乱就是限流严重,免费接口常常隔三差五封IP,付费接口又贵得离谱。后来我干脆回头研究自己电脑上装着的通达信——每天收盘后它都会在本地落盘一批.day文件,里面就是全市场股票的日线行情。更妙的是,Python生态里有个叫pytdx的库,既能直接解析这些本地文件,又能连接通达信行情服务器拉网络数据,几分钟就能搭出一条稳定的本地数据管线。下面我就把这个过程完整拆开讲,从二进制格式原理、pytdx代码实战,到批量读取和踩坑记录,全部摊开。适合刚入门量化、不想被数据API绑架的朋友直接抄作业,也适合已经在写回测、但被数据质量折腾得头大的老手参考。

1. 先把三件事搞清楚:.day文件、pytdx和本地数据管线

1.1 通达信本地数据到底存在哪里

如果你用的是PC版通达信,安装目录下一般会有一个vipdoc文件夹,里面的结构大概是这样的:

vipdoc/ ├── sh/ │ └── lday/ │ ├── sh600000.day │ ├── sh600036.day │ └── ... ├── sz/ │ └── lday/ │ ├── sz000001.day │ └── ...

sh是上海市场,sz是深圳市场;lday是日线目录;文件名是"市场前缀+6位股票代码+.day"。比如sh600000.day就是浦发银行的日线数据,sz000001.day就是平安银行的日线数据。除了日线,vipdoc下面还有存分钟线的目录,比如fzline、minline,但后缀各不相同。现在我们重点处理的,就是这些后缀为.day的文件。

为什么通达信要把数据落地成文件?因为它自己也要读。每次你打开软件看K线,它本质上就是去读这些本地文件,然后渲染到界面上。也就是说,这些文件里的数据,和你眼睛看到的历史K线完全一致。既然软件自己能读,那我们当然也能用Python去读。这里有个很实用的排查技巧:如果你在自己电脑上找不到vipdoc目录,用系统的全盘文件搜索功能搜一下.day后缀,基本都能定位到,因为通达信不管装在哪个盘,日线数据目录的名称和结构相对固定。

1.2 pytdx这个库,本事比大多数人以为的大

很多人听说过pytdx,是因为它经常被用来连接通达信行情服务器、拉实时数据。这确实是最常见的用法,但pytdx的能力其实分成两块。

第一块是hq模块,对应"行情查询",走的是通达信协议,连上服务器之后可以拉K线、五档、分时数据。第二块是reader模块,对应"本地读取",直接解析通达信落盘的数据文件,包括我们这篇文章要讲的.day文件,还有分钟线、财务数据文件。也就是说,pytdx一个库把"网络拉取"和"本地解析"都覆盖了。很多教程只讲了前者,搞得大家都以为它只能联网用。实际上,TdxDailyBarReader这个类两行代码就能把.day文件读出来,根本不需要自己折腾二进制解析。

不过话说回来,底层格式解析也要懂。只有明白了.day文件里每个字节的含义,你才知道pytdx读出来的数据靠不靠谱,遇到缓存文件损坏、字段顺序变化时才不会两眼一抹黑。所以我后面第2章会先讲清楚文件格式,第3章再给pytdx的现成用法,两条路都走一遍,你在实际项目里才能灵活切换。

1.3 有现成的数据API,为什么还要折腾本地.day

如果你只是需要"最近几百根K线做技术指标演示",那各大平台的免费API完全够用。但如果是认真做量化回测,本地.day文件有几个优点是云上API很难替代的。

第一,没有数量和频率限制。免费行情接口一般都限制单次拉取数量和请求频率,有的拉完800根K线就得歇一会儿,频繁请求还容易封IP。本地.day文件不存在这个问题,整个市场几千只股票的历史日线就在你的硬盘里躺着,想读多少读多少。

第二,数据是"原始价",可复现性最好。网络接口因为各家处理复权的方式不同,同一段历史K线在不同平台可能长得不一样。而.day文件里存的是不复权的原始成交价格,复权逻辑你可以自己在回测框架里统一处理,从根上保证了数据的可复现性。这对策略回测来说特别重要,数据源不一致,回测结果就没有可比性。

第三,离线可用、速度快。本地文件读的是磁盘,解析百万条记录也就秒级,完全不用等网络。很多实盘环境在隔离网段,连不了外网,这时候本地.day文件几乎是唯一靠谱的历史数据来源。我自己就遇到过线上服务器不能访问外部行情接口的情况,当时全靠定期同步过去的.day文件撑起了整个回测流程。

2. .day文件的二进制格式:32字节定长记录的秘密

2.1 单条记录的结构拆解

.day文件不是文本文件,而是一个纯二进制文件,没有表头,没有分隔符。它的组织形式可以理解为一个固定宽度的"表格":每一行数据固定占32字节,每个字段占4字节。通达信协议系列文件基本都是这种风格,好处是读取极其高效,坏处是——如果不对照着字段表去看,就是一堆乱码。

单条记录32字节的字段布局如下:

字节偏移长度类型字段说明
04int32date日期,整数格式YYYYMMDD
44float32open开盘价(元)
84float32high最高价(元)
124float32low最低价(元)
164float32close收盘价(元)
204float32amount成交额(元)
244int32volume成交量
284int32reserved保留字段

这里有两个容易搞错的地方。第一,所有多字节整数和浮点数都是小端序存储,也就是低字节在前;Windows上的通达信按这个字节序写入,所以解析时格式串里一定要加<。第二,成交量字段的单位在不同资料里说法不一,我实际对比过,默认是按"股"存的,界面上显示的"手"通常是用这个数除以100得到的。你把本地文件解析出来的volume和通达信软件上的成交量对一下,单位是啥立刻就清楚了,不同券商版本可能存在细微差异。

2.2 从文件大小反推记录数

因为每条记录严格32字节,所以一个.day文件里有多少条K线,可以直接从文件大小算出来:

记录数 = 文件大小 / 32

比如sh600000.day文件刚好是5440字节,那就说明里面有170条日线记录。这个特性在调试的时候特别好用。如果你手工解析出来的记录数和这个公式对不上,那基本可以断定解析逻辑有问题,或者文件本身被截断过。

利用这个特性,我写了一个很小的手工解析函数,只用Python标准库的struct模块就够了:

import struct def parse_day_file(filepath): records = [] with open(filepath, 'rb') as f: while True: chunk = f.read(32) if len(chunk) < 32: break date, open_, high, low, close, amount, volume, _ = struct.unpack('<IfffffII', chunk) records.append({ 'date': date, 'open': open_, 'high': high, 'low': low, 'close': close, 'amount': amount, 'volume': volume, }) return records

struct格式串<IfffffII展开来说就是:小端序(<),无符号整型(I)存日期,连续5个浮点型(f)存开盘、最高、最低、收盘和成交额,再跟两个无符号整型(I)存成交量和保留字段。这里日期用无符号整型是为了和通达信写入时的uint32对齐,反正日期不会为负,用I比用i更严谨。

2.3 边界情况与隐蔽的大坑

手工解析.day文件,有几个边界情况如果不提前处理,早晚会踩进去。

第一个是文件尾部半条记录。断网断电、软件强制退出,都可能让.day文件最后残留几个不完整的字节。所以循环读取时必须判断读出来的chunk长度是否等于32,不足就直接跳出,否则struct.unpack会直接抛异常。

第二个是除权日的数据。通达信在除权日当天记录的原始价,会包含除权跳空,直接拿来做指标计算没问题,但如果你要做回测撮合,就得自己再算复权因子。这个不算bug,但很多新手第一次看到K线上突然出现一个巨大缺口时会以为数据坏了,其实这是正常现象。

第三个是全市场文件名前缀的坑。上海市场的文件名以sh开头,深圳市场以sz开头,这一点看似简单,但如果你写批量扫描脚本时不加区分,单纯按代码去匹配,很容易把sh600000和sz600000这种压根不存在的文件搞混。批量读取时,建议直接把文件名的market前缀一起解析出来,存成一个字段,后面做数据筛选会方便很多。

3. pytdx实战:两行代码读取.day文件

3.1 安装与版本注意

pytdx是一个比较老牌的库,安装很简单:

pip install pytdx pandas

pytdx的依赖很少,基本装上就能用。我建议在Python 3.8以上的环境里跑,再老的环境虽然也能装,但有些第三方依赖链容易出问题。pandas不是pytdx的硬依赖,但解析完数据总得有个像样的数据结构去存,所以干脆一起装上。

安装完之后,验证一下导入路径是否正确:

from pytdx.reader import TdxDailyBarReader

如果这一步报ModuleNotFoundError,大概率是装到了别的环境里。用which python确认一下当前解释器,或者pip list | grep pytdx看看版本到底装没装上。还有一个比较隐蔽的问题:某些Python环境里同时存在多个通达信相关库,比如pytdx和easyquotation,它们的模块名可能有冲突,如果导入时报的不是ModuleNotFoundError而是奇怪的属性错误,先看看是不是和别的库重名了。

3.2 最小可用代码:直接读出所有日线

TdxDailyBarReader的用法非常直接,下面这段代码可以在几秒钟内读完整个文件:

from pytdx.reader import TdxDailyBarReader reader = TdxDailyBarReader(r"C:\new_tdx\vipdoc\sh\lday\sh600000.day") count = reader.get_bar_count() bars = reader.get_bars(count)

get_bar_count()返回的是这个文件里有几条K线,get_bars(count)则是把前count条数据全部取出来。返回的bars是一个二维结构,每一行对应一条K线,字段顺序和.day文件里的物理布局基本一致,也就是date、open、high、low、close、amount、volume这一串。

拿到bars之后,我习惯立刻转成pandas的DataFrame,后面做筛选、合并、落库都方便:

import pandas as pd df = pd.DataFrame(bars, columns=['date', 'open', 'high', 'low', 'close', 'amount', 'volume']) df['date'] = pd.to_datetime(df['date'], format='%Y%m%d') df = df.sort_values('date').reset_index(drop=True) print(df.tail())

这里有个小提示:如果你装的是比较新的pytdx版本,bars的列数或者列顺序可能和我的假设有出入,别慌,先print(bars[:2])看一眼原始结构,再调整columns列表就行。不同版本之间确实有过列顺序调整的情况,我自己就在升级之后遇到过列对不上的问题,排查方法很简单,打印出第一条记录,对照前面的字段表一个个核对就行。

3.3 批量扫描整个vipdoc目录

单只股票肯定满足不了回测需求。实际项目中,我更习惯写一个批量扫描函数,把一个市场目录下所有的.day文件都读出来,统一合并成一张大表:

import os import pandas as pd from pytdx.reader import TdxDailyBarReader def batch_read_day_files(day_dir): frames = [] for root, _, files in os.walk(day_dir): for name in files: if not name.lower().endswith('.day'): continue filepath = os.path.join(root, name) code = os.path.splitext(name)[0] try: reader = TdxDailyBarReader(filepath) count = reader.get_bar_count() if count <= 0: continue bars = reader.get_bars(count) df = pd.DataFrame( bars, columns=['date', 'open', 'high', 'low', 'close', 'amount', 'volume'] ) df['code'] = code frames.append(df) except Exception as exc: print(f"{filepath} 解析失败: {exc}") continue if not frames: return pd.DataFrame() return pd.concat(frames, ignore_index=True)

几点实操心得。第一,单只股票的.day文件一般不大,但几千个文件合在一起就有点规模了,建议第一次全量生成后直接存成Parquet或者SQLite,之后只做增量更新,不要每次回测都从.day文件从头解析。第二,except里的日志一定要打全路径,因为批量读取时某一个文件损坏,你不会想在全市场几千个文件名里猜是哪只股票出问题。第三,concat之前先确认每个df的列名完全一致,否则pandas会给你拼出一堆NaN列。

3.4 本地数据和网络数据交叉验证

本地文件读出来之后,怎么确认它没坏?最直接的办法是和通达信行情服务器上的数据做交叉对比。pytdx的hq模块可以做这件事:

from pytdx.hq import TdxHq_API api = TdxHq_API() servers = [ ('119.147.212.81', 7709), ('124.71.187.122', 7709), ] df_net = None for host, port in servers: try: api.connect(host, port) bars = api.get_security_bars(9, 0, '000001', 0, 100) df_net = api.to_df(bars) break except Exception as exc: print(f"{host}:{port} 连接失败: {exc}") api.disconnect() continue if df_net is not None: print(df_net.tail())

get_security_bars的第一个参数9代表日K线,第二个参数0代表深圳市场,第三个参数是股票代码。因为网络接口每次最多返回800根,所以这里只取最后100根做抽样对比。拿本地文件的最后100条记录和df_net按日期对齐,比较收盘价是否一致;如果完全一致,基本可以确认本地数据没问题。不一致的话,优先检查是不是复权设置导致的价格差异,再看看本地文件最后更新日期和服务器上最新交易日是否相同。

公开的通达信行情服务器地址网上有很多,上面这两个是我实测还算稳定的。连不上的时候不要死磕一个地址,把候选列表放在数组里轮询是更务实的做法。

4. 从零手写解析器:理解原理才能玩出花儿

4.1 一次性读入内存再切片:性能更好

上一节我们用了pytdx的两行代码直接读文件,方便是方便,但对"为什么能读出来"这件事还是有点黑盒。所以这一章我们自己写一个更完整的解析器,顺便把性能优化也做了。

前面手工解析用的是while循环反复read(32),这种方式逻辑简单,但文件大了以后性能一般,因为每次read都要发生一次系统调用。更聪明的做法是一口气把整个文件读进内存,然后按32字节的步长去切片,这样系统调用的次数从"记录数"降到了1次,整体速度快一个量级。

import struct from pathlib import Path class DayFileParser: RECORD_SIZE = 32 FORMAT = '<IfffffII' def __init__(self, filepath: str): self.filepath = Path(filepath) def parse(self): data = self.filepath.read_bytes() total = len(data) // self.RECORD_SIZE records = [] for i in range(total): chunk = data[i * self.RECORD_SIZE:(i + 1) * self.RECORD_SIZE] date, open_, high, low, close, amount, volume, _ = struct.unpack( self.FORMAT, chunk ) records.append({ 'date': date, 'open': open_, 'high': high, 'low': low, 'close': close, 'amount': amount, 'volume': volume, }) return records

如果你对性能有更高的要求,可以再进一步,用numpy的frombuffer直接把整个文件映射成一个结构化数组:

import numpy as np dt = np.dtype([ ('date', '<u4'), ('open', '<f4'), ('high', '<f4'), ('low', '<f4'), ('close', '<f4'), ('amount', '<f4'), ('volume', '<u4'), ('reserved', '<u4'), ]) def parse_day_file_numpy(filepath): data = Path(filepath).read_bytes() arr = np.frombuffer(data, dtype=dt) return arr

这个版本几乎没有任何Python层面的逐条循环,百万条记录的解析也只需要几百毫秒,非常夸张。arr返回之后,直接arr['date']、arr['close']这样按列访问,后面再转成DataFrame也很快。这个方法很值得放进自己的工具库里,以后解析各种固定宽度的二进制文件都能复用,不一定非要和通达信绑定。

4.2 数据完整性校验

自己写了解析器,就多了一个别人没有的环节:校验。我强烈建议在日线数据进入回测库之前,至少做两道校验。

第一道是OHLC逻辑校验。合法的K线必须满足:

  • high>= max(open,close)
  • low<= min(open,close)

如果某条记录high比open和close都低,或者low比它们都高,那这条数据一定有问题,不是文件损坏就是解析时字段对错了位。

第二道是文件大小校验。拿文件大小除以32,如果不能整除,说明文件头部或尾部有残留字节。对残留部分,我通常选择直接丢弃而不是报错,因为通达信自己读这些文件时遇到的也是同样的情况,截断到最后一个完整记录反而是最兼容的做法。

下面是一段简单的校验函数:

def validate_records(records): errors = [] for i, r in enumerate(records): if r['high'] < max(r['open'], r['close']) - 1e-6: errors.append((i, 'high小于open/close')) if r['low'] > min(r['open'], r['close']) + 1e-6: errors.append((i, 'low大于open/close')) return errors

校验可能发现的问题,很多时候不是文件坏了,而是你解析时取了错误的字节偏移。比如把成交量字段当成价格来读,出来的数据就会乱七八糟,OHLC校验一秒就能揪出来。

4.3 增量更新的思路

最后聊一下怎么把.day文件持续用起来。通达信每天收盘后会更新当天的K线到本地文件,所以你的本地数据仓库也应该跟着增量更新。

一个比较省事的做法是:记录每个.day文件的大小和修改时间(mtime),每天跑定时任务时,只重新解析那些"大小变化了或者mtime变了"的文件。因为.day文件是不断追加的,日期靠前的那部分历史数据不会变,每次全量重读几千个文件纯属浪费磁盘和CPU。

def needs_update(filepath, cached_size, cached_mtime): stat = filepath.stat() return stat.st_size != cached_size or int(stat.st_mtime) != cached_mtime

配合上一节的批量解析函数,先在本地存一份manifest(路径、大小、mtime、解析后的数据落库位置),每天只需要几十毫秒就能判断出哪些文件需要重新读取。这个思路对做日频、周频回测的朋友特别实用,既省时间又不容易出错。

5. 常见问题与排查技巧实录

5.1 struct.error: unpack requires a buffer of 32 bytes

这是手工解析时最常见的报错,原因基本只有一个:文件尾部残留了不足32字节的半个记录。解决办法有两种。第一种是在循环里判断chunk长度,小于32就break;第二种是像前面那样,先按文件大小除以32得到完整记录数,然后只解析前total条。我个人推荐第二种,因为先算条数还能顺手统计文件是否被截断过。

不过要注意,如果把整个文件一次性读进内存再切片,要小心data[i*32:(i+1)*32]的切片范围。Python切片在越界时不会报错,只会返回更短的字节串,然后struct.unpack就会炸。解决方案就是在循环前先算出total = len(data) // 32,严格按这个范围取。

5.2 读出来的K线条数比自己预期少

如果你发现某只股票明明上市十几年,解析出来的日K却只有几百条,先别怀疑解析代码,很可能是两个原因。

第一,通达信默认"仅下载最近N天数据",或者你从来没完整下载过。这种情况需要到通达信菜单里找到"盘后数据下载",把日线数据完整下载一遍,.day文件才会补齐到全量历史。第二,你读的文件根本不是主图的日线,而是某个指标计算后生成的临时文件,路径或者后缀看错了。建议打开文件属性看一眼文件大小,结合"文件大小/32=记录数"这个公式,马上就能判断数据量够不够。

还有一种情况比较隐蔽:有的券商定制版通达信会把数据目录放在自定义位置,比如安装在D盘,但数据目录可能在C盘用户目录下。如果你在安装目录下找不到.day文件,去别的盘搜一下vipdoc可能会豁然开朗。

5.3 网络接口拉到的数据不全,只有800根

pytdx连行情服务器拉K线时,get_security_bars单次最多返回800根,很多新手不知道这个限制,以为只能拿到最近800天的数据。实际上可以通过循环翻页的方式把整段历史分批拉完:

def fetch_all_daily(api, market, code): frames = [] start = 0 while True: bars = api.get_security_bars(9, market, code, start, 800) if not bars: break frames.append(api.to_df(bars)) if len(bars) < 800: break start += 800 return pd.concat(frames, ignore_index=True)

start参数表示从哪根K线开始取,每次取800根,直到返回不足800根说明已经到最早期数据。这样拼出来的数据虽然也能用,但速度慢、有频率限制,如果只是为了拿历史数据做回测,还是本地.day文件更靠谱。

5.4 数据直接存CSV好,还是存Parquet/SQLite好

这个问题很多朋友问过我。早期我图省事,全市场日线一股脑导出成一个大CSV,结果文件好几个GB,每次读进来都要卡半天,后来老老实实换成了Parquet。

我的建议是:CSV只适合导出小批量数据给人看,不适合做全量存储。Parquet压缩率高、读起来快,还天然保留数据类型,推荐作为主力存储格式。如果不想引入额外依赖,SQLite也是不错的选择,查询按股票代码或日期范围非常灵活,写起来也简单。但不管存什么格式,都不要把它放在和.day文件同一个盘上,备份策略上至少保留两份副本,否则通达信哪天抽风重装覆盖了vipdoc,你会非常被动。

最后再分享一点个人体会。这套"pytdx读本地.day + 网络接口抽检"的组合,我已经在项目里跑了大半年,白天收盘后定时任务自动跑增量更新,晚上回测框架直接读本地数据,全程稳定、免费、可控,再也没有被哪家数据API的限流策略折腾过。

如果你只是想快速验证一个策略想法,直接用TdxDailyBarReader读单只股票就够了;但如果你打算认真搭建自己的回测数据管线,我强烈建议从第二章的手写解析器开始,把底层的文件格式吃透。这样后续无论是排查数据异常,还是对接其他通达信衍生数据(比如分钟线、财务数据),都会顺手得多。

另外再多说一句,通达信升级或重装时,可能会覆盖甚至清空vipdoc目录。怕丢数据的话,提前把整个vipdoc文件夹做个备份,或者想办法让通达信把数据目录指到别的盘。这个问题我踩过一次,现在每次升级前都会习惯性看一眼本地数据还在不在。数据是量化的根基,数据没了,策略写得再漂亮也是白搭。

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

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

立即咨询