☰
Python解析通达信.day文件,结合pytdx搭建本地量化数据链
2026/10/2 18:41:49 网站建设 项目流程

做量化的人,几乎都会遇到同一个坎:数据从哪来。市面上的免费数据接口今天能用明天就挂,付费终端一年好几千,对一个还没跑通策略的新手来说实在肉疼。后来我发现,电脑里装的通达信软件本来就在本地落地了一份完整的行情库——vipdoc目录下全是.day后缀的文件,每只股票一个文件,一百多KB,却装着十几年每一天的开高低收、成交量和成交额。

问题只有一个:这种东西要怎么用 Python 读出来?我翻了不少资料,试了不少错,最后用不到五十行代码写了一个解析器,再配合pytdx库做数据校验和补全,把“本地.day文件 → DataFrame → 策略回测”这条链路彻底跑通了。这篇文章就把二进制格式、完整解析代码、pytdx 的正确用法以及我踩过的坑全部摊开讲一次,适合刚开始学量化、或者想摆脱网络接口做回测的 Python 开发者。

1. 先理清:.day文件在量化数据链路里的位置

1.1 本地日线数据的价值

做量化回测,数据是第一道坎。网络上能拿到的免费数据五花八门,但普遍有几个痛点:一是接口限频,拉全市场几千只股票的日线经常被断开;二是字段不规范,有的用前复权、有的用后复权,一不留神就对不上;三是历史数据深度不够,很多免费接口只能拿最近两三年的日线,拿来做长周期策略根本不够用。

本地.day文件恰好把这些问题补上了。通达信每天盘后只要执行一次“盘后数据下载”,本地就会落一份完整的日线数据。它不依赖网络,不担心接口限频,历史可以回溯到十几年前,字段又非常稳定——开高低收、成交额、成交量,全是标准K线原子数据。对于个人量化研究来说,这是一份最朴素、最可靠的数据源。

1.2 pytdx的真实定位:不是用来解析.day的

这里要先澄清一个很多新手会搞混的点:pytdx这个库本身不负责解析.day文件,它是个纯 Python 的通达信行情协议客户端,用来连接通达信的行情服务器,拉取实时行情、历史K线、财务数据等等。

那为什么标题里要把 pytdx 和.day解析放在一起?因为在实际工作中,两者是天然互补的:

  • .day文件解析负责读取本地历史数据,速度快、量大、不受网络限制;
  • pytdx负责校验数据一致性、补全最近一个交易日的行情、获取除权除息信息等。

我最早只写了解析器,结果某天发现本地.day文件最后一条数据还停留在昨天——因为没手动做盘后下载。从那以后,我就在解析流程里加了一步 pytdx 校验,如果本地最新日期落后于服务器最新交易日,就自动用 pytdx 拉最近K线补位。这条链路才是完整可用的。

1.3 .day文件的存放位置和命名规则

通达信安装目录下有一个vipdoc文件夹,结构大概是这样的:

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

命名规则很好记:市场前缀加股票代码,上海是sh,深圳是sz,文件后缀统一是.day。lday表示"line day",即日线数据目录。指数也是同样的存储方式,比如上证指数就是sh000001.day。

2. .day文件的32字节记录结构:逐字段拆给你看

2.1 一条K线记录是怎么排列的

.day文件不是文本文件,是纯二进制文件,里面每条K线记录固定占用32 字节,一个挨着一个,顺序按日期递增排列。这意味着文件总大小除以 32,就是这只股票一共多少个交易日的数据,非常规整。

32 字节的具体布局如下:

偏移量字段类型说明
0dateuint32日期,格式为 YYYYMMDD,比如 20240115
4openfloat32开盘价,需除以100,即 1234.0 表示 12.34
8highfloat32最高价,需除以100
12lowfloat32最低价,需除以100
16closefloat32收盘价,需除以100
20amountfloat32成交额,单位元
24volumeuint32成交量,单位股
28reservedint32保留字段,一般填 0

2.2 价格为什么要除以100

这个点我必须单独拎出来说,因为太多网上流传的解析代码在这里出错。

通达信存储价格时,并不是把12.34这个浮点数直接写进文件,而是先乘以 100,变成1234.0,再用 4 字节浮点存进去。解析出来之后要除以 100 才是真实价格。为什么这么干?两个原因:一是避免二进制浮点误差,二是方便统一精度——把价格限定到“分”这个量级,做本地存储更干净。

用 Python 的struct模块,这个格式对应的就是:

import struct record = struct.unpack('<IffffIIi', data) # date = record[0] # open = record[1] / 100.0 # high = record[2] / 100.0 # low = record[3] / 100.0 # close = record[4] / 100.0 # amount= record[5] # volume= record[6]

<表示小端序。通达信的数据统一按小端序存储,这在 Intel/AMD 平台上不需要额外处理,但如果你在别的架构上解析,一定不能漏掉这个符号。

2.3 网上流传的“八个无符号整数解析”为什么不靠谱

我最早在技术博客和开源项目里看到很多版本是这样写的:

# 这是错误示范,不要直接抄 date, open, high, low, close, amount, volume, reserved = struct.unpack('<IIIIIIII', data)

猛一看,文件确实是 8 个字段 × 4 字节 = 32 字节,于是用 8 个无符号整数一次性解包,再把价格除以 100 完事。这个写法乍看很工整,实际上它把浮点数的二进制位当成了整数来读。

价格用float32存储,比如12.34经100.0倍换算后是1234.0,它在内存里的 4 字节和整数1234的 4 字节完全不同。用I去解float的结果是一串莫名其妙的大数字,哪怕除以 100 也不可能是合理的价格。

我当年在这里卡了一天半:文件能读,字段数也对,解析出来的日期、成交量都是正常的,但价格怎么都对不上。最后把十六进制字节一个个打印出来对比,才意识到是结构化类型的问题。所以大家看到类似代码时,多留个心眼——以IffffIIi这种带浮点标记的格式为准。

3. 环境与pytdx连接:装好库只是第一步

3.1 安装和Python版本选择

pytdx是纯 Python 写的,安装非常简单:

pip install pytdx

Python 版本我用的是 3.10,实测 3.8 到 3.11 都能正常安装。有个小提醒:pytdx 的核心代码年头不短了,原作者基本不维护,在 Python 3.12+ 上会不会有隐藏问题不好说,如果你用的是最新版 Python 且安装后运行报错,建议降到 3.10 或 3.11 再试。

3.2 连接行情服务器并拉取日线

pytdx 的入口是TdxHq_API,先连接一个行情服务器,然后调用get_security_bars拉取K线。category=9表示日线,market参数中1代表上海、0代表深圳,和前面说的文件目录前缀要对应上。

from pytdx.hq import TdxHq_API api = TdxHq_API() with api.connect('119.147.212.81', 7709, time_out=10): bars = api.get_security_bars(9, 1, '600000', 0, 10) # 上证指数的样例写法,实际是浦发银行 if bars: df = api.to_df(bars) print(df.head())

这段代码连接的是119.147.212.81这个公开行情服务器。注意行情服务器不是 100% 稳定,我列几个常用的备用地址:

服务器地址端口备注
119.147.212.817709深圳电信
221.231.141.607709南京电信
202.108.253.1307709北京移动

实际使用中我会写一个轮询函数,依次尝试连接,连上为止;如果全部失败就返回空结果并告警。

3.3 一个非常容易踩的坑:get_security_bars的条数限制

get_security_bars单次调用最多返回 800 条记录,并不是它懒,而是协议端就限制了单包长度。如果你需要很久以前的历史数据,只能分段拉取,比如从最后往前翻:

start = 0 all_bars = [] while True: bars = api.get_security_bars(9, 1, '600000', start, 800) if not bars: break all_bars.extend(bars) if len(bars) < 800: break start += 800

好在日线数据本身量级不大,一只股票 20 年也就 4800 根K线,循环 6 次就能拿全。对本地校验来说,通常只需要拉最近几条做对比,完全没有这个压力。

4. 解析器完整代码:单文件、批量目录、增量更新一次搞定

4.1 单文件解析:用struct.iter_unpack提速

前面讲过,单条记录 32 字节。如果从头到尾用for i in range(...)切片再解包,也能工作,但全市场几千个文件跑起来就有点慢了。Python 的struct.iter_unpack是专门为这种"等长结构体数组"设计的,直接扔一个完整的二进制串进去,它会自动按 32 字节切分并解包,不用手动算下标。

import struct import pandas as pd DAY_RECORD_FMT = '<IffffIIi' DAY_RECORD_LEN = struct.calcsize(DAY_RECORD_FMT) def parse_day_file(file_path): """解析单个通达信 .day 文件,返回 DataFrame。""" with open(file_path, 'rb') as f: raw = f.read() # 文件末尾可能存在不足一条记录的多余字节,做一次对齐截断 usable_len = len(raw) - len(raw) % DAY_RECORD_LEN raw = raw[:usable_len] rows = [] for rec in struct.iter_unpack(DAY_RECORD_FMT, raw): date, open_, high, low, close, amount, volume, _ = rec rows.append(( date, open_ / 100.0, high / 100.0, low / 100.0, close / 100.0, amount, volume, )) df = pd.DataFrame(rows, columns=[ 'date', 'open', 'high', 'low', 'close', 'amount', 'volume' ]) df['date'] = pd.to_datetime(df['date'], format='%Y%m%d') return df

这个函数核心就做五件事:读文件、对齐长度、迭代解包、浮点换算、转 DataFrame。代码量不多,但已经把二进制格式的所有细节都消化掉了。

4.2 批量扫描vipdoc目录

单只股票的数据没什么意思,真正有用的是扫描整个vipdoc目录,一次性把所有股票都加载进来。

from pathlib import Path def scan_vipdoc(vipdoc_root, max_files=None): """ 扫描 vipdoc 目录下的所有 .day 文件。 vipdoc_root 是通达信安装目录下的 vipdoc 文件夹路径。 """ root = Path(vipdoc_root) day_files = sorted(root.glob('*/lday/*.day')) if max_files: day_files = day_files[:max_files] datasets = {} for f in day_files: code = f.stem.upper() # 例如 SH600000 try: datasets[code] = parse_day_file(f) except Exception as exc: print(f'解析失败: {f} -> {exc}') return datasets

glob('*/lday/*.day')能同时匹配sh/lday和sz/lday,目录结构变动时也不用改代码。max_files参数是调试用的,数据量大时先加载一部分验证逻辑,再全量跑。

全市场大概 5000 多只股票,struct.iter_unpack+ DataFrame 构建,实测在普通电脑上跑完大约 10 秒左右;如果用之前那种逐条切片的方式,可能要到 30 秒以上。数据量大时这几十秒的差距还是很明显的。

4.3 增量更新:只解析新增部分

每天收盘后,通达信只会在.day文件末尾追加一个 32 字节的记录。如果每次都把整个文件从头读一遍,虽然也不是不能忍,但日积月累确实浪费。

更聪明的办法是记住每只股票已经解析到的日期,下次只读文件的新增部分。通达信没有提供增量写入的标记,但增量部分恰好就是"从上次文件大小到当前文件大小"的那一段字节,我们可以直接按字节偏移读:

def parse_day_incremental(file_path, last_size=0): """从 last_size 字节处开始解析 .day 文件的新增记录。""" file_size = os.path.getsize(file_path) if file_size <= last_size: return None, last_size with open(file_path, 'rb') as f: f.seek(last_size) raw = f.read() # 这里直接复用 4.1 节的核心逻辑,把 raw 解析成 DataFrame df = parse_day_raw(raw) # 保存新的文件大小,供下次调用传入 return df, file_size

有个细节要注意:.day文件不是按自然日追加的,而是按交易日追加。如果某天停牌,通达信不会在文件里写任何内容。所以增量解析判断的唯一依据就是文件字节大小,而不是日期差。

4.4 落地保存:避免每次回测都重新解析

解析出的 DataFrame 直接存成 parquet 或 CSV,后面做策略就不需要再碰二进制了。我一般用 parquet,因为压缩率高、读入速度快,而且保留了 DataFrame 的 dtypes:

df.to_parquet('SH600000.parquet', index=False)

一个.day文件解析后大约 150KB 左右,全市场存下来几百 MB,完全在可接受范围内。

5. 用pytdx拉服务端行情交叉验证本地文件

5.1 为什么要做校验

.day文件的可靠性很大程度上取决于通达信客户端的数据下载是否完整。有时候盘中直接看数据,文件还没有更新;有时候网络中断导致盘后下载失败,本地记录就一直停在前一天。如果不做任何校验,回测时缺了最近一根K线,结果总感觉差一口气。

校验的逻辑很简单:拿本地解析出的最后一条记录,和 pytdx 从服务器拉回的最新日线做对比,两边收盘价一致就说明本地数据是新的;不一致或本地日期小于服务器日期,就用服务器的数据补上。

5.2 完整校验代码

from pytdx.hq import TdxHq_API def fetch_latest_daily(market, code, count=5): """通过 pytdx 拉取最近 count 根日线。market: 1=sh, 0=sz""" servers = [ ('119.147.212.81', 7709), ('221.231.141.60', 7709), ('202.108.253.130', 7709), ] api = TdxHq_API() for host, port in servers: try: api.connect(host, port, time_out=10) bars = api.get_security_bars(9, market, code, 0, count) if bars: return api.to_df(bars) except Exception as exc: print(f'服务器 {host} 连接失败: {exc}') finally: api.disconnect() return None def check_and_patch(local_df, market, code): remote_df = fetch_latest_daily(market, code) if remote_df is None: return local_df, False remote_last = remote_df.iloc[-1] local_last = local_df.iloc[-1] # 对比最后一条记录 if local_last['date'] == remote_last['date'] and \ abs(local_last['close'] - remote_last['close']) < 0.001: return local_df, True # 数据一致 # 本地落后,把服务器数据追加进来 new_records = remote_df[remote_df['date'] > local_last['date']] if not new_records.empty: local_df = pd.concat([local_df, new_records], ignore_index=True) return local_df, True

这段代码的思路是:先对比最后日期的收盘价,如果对得上就认为本地是最新的;如果对不上,说明中间有缺失或本地还没更新,就把服务器上比本地最新日期更新的记录追加进去。market参数要和代码前缀对应,比如SH600000对应market=1,SZ000001对应market=0。

5.3 校验结果异常时的排查方向

如果发现本地数据和服务端数据对不上,通常优先怀疑这三件事:

第一,通达信没有执行盘后下载。本地数据本来就是客户端落地的,客户端不更新,文件就不会变。解决办法是在通达信里手动做一次“盘后数据下载”。

第二,你拿到的.day文件根本就不是你想的那只股票。我犯过这种低级错误——把sh600000的路径当成sh000001来读,指数和个股的数据当然对不上。

第三,成交量的单位差异。.day文件里的volume单位是股,而通达信界面默认显示的是手,1 手等于 100 股。如果用界面数值和解析结果直接对比,需要先换算。

6. 小小实战:用解析出的日线跑一个20/60日均线策略

6.1 策略逻辑

数据链路搭好之后,不跑个策略总觉得少了点什么。我用最常见的双均线策略做演示:短期均线(20日)上穿长期均线(60日)时买入,下穿时卖出。

先加载数据,生成信号:

df = parse_day_file('vipdoc/sh/lday/SH600000.day') df['ma20'] = df['close'].rolling(20).mean() df['ma60'] = df['close'].rolling(60).mean() # signal = 1 表示持仓,0 表示空仓 df['signal'] = (df['ma20'] > df['ma60']).astype(int) # 策略每日收益率:信号需要 shift(1),避免用当天K线信号做当天交易 df['pct_change'] = df['close'].pct_change() df['strategy_ret'] = df['signal'].shift(1) * df['pct_change'] # 累计收益曲线 df['strategy_cum'] = (1 + df['strategy_ret']).cumprod() df['buy_hold_cum'] = (1 + df['pct_change']).cumprod()

策略收益和买入持有收益放一起对比,很直观。

6.2 一个新手必踩的坑:未来函数

上面代码里那个shift(1)特别重要,不是装模作样。如果没有shift,当天的ma20和ma60是在收盘后才知道的,但你却用同一个收盘价算出来的信号去决定当天能不能买卖,这在回测里就叫“未来函数”,收益会被严重高估。

我第一次写策略脚本时把shift(1)漏了,回测结果翻倍还多,当时还挺高兴,后来细想才意识到是未来函数在起作用。这个坑在量化里太常见了,宁可信号晚一天触发,也不能用未来数据。

6.3 复权问题:为什么长周期回测不能直接用原始价

.day文件里的价格是不复权的原始成交价。如果一只股票发生过送股或大比例分红,除权除息当天价格会出现一个向下的“跳空缺口”,从 K 线图上看像暴跌,但实际上是分红导致的除权。

短期回测一般影响不大,但拉长到三五年以上,这些缺口会让均线信号失真。处理办法是获取除权除息信息,把历史价格做后复权。pytdx提供了get_xdxr_info这个接口,能拿到股票历次除权除息信息,配合这些信息就能算后复权因子。这块内容展开讲会非常长,我之后单独写一篇。这里先提醒一句:跑长周期策略之前,务必先把复权处理做了,否则策略信号的基本逻辑都是错的。

7. 解析和使用.day时最容易踩的坑清单

7.1 文件末尾的多余字节

我在 4.1 节的代码里做了usable_len的对齐处理,因为实际接触到的.day文件偶尔会在末尾多出几个字节,原因可能是通达信升级版本后写入逻辑微调,也可能是文件被非正常中断。如果不做对齐,struct.iter_unpack会直接抛错,导致整个文件解析失败。

这段防御性代码虽然只有两行,但能省掉很多线上排查的时间,建议保留。

7.2 成交量的单位换算

volume字段单位是股,不是手。很多初学者会把解析结果和通达信界面的成交量对比,发现数值大了 100 倍,以为自己解析错了。其实界面默认显示的是手,文件里存的是股。我自己刚上手时也在这上面浪费过半小时。

写策略时涉及成交量单位要全链路统一,暂停用股就用股,暂停用手就用手,不要来回切换。

7.3 pytdx断连和服务器维护

公开行情服务器不是商业SLA级别,经常出现连不上、连上后无响应、响应超时等情况。我的处理办法有三个:

第一个是轮询多个服务器,一个不行马上换下一个,不要在单点上死磕。第二个是设置time_out,默认值如果太长,遇到维护中的服务器会白白等很久;我习惯设为 10 秒。第三个是每次调用都放入try/except,pytdx 在连接异常时抛的异常类型不固定,代码里做兜底,保证单个股票校验失败不会影响整个批量任务。

7.4 每天都做盘后下载

最后这条看着像废话,但真的最影响实际体验。pytdx校验能补最近一两天的数据,但毕竟不是长远的解决办法。如果本地.day一直不更新,每次都要靠网络拉,那本地文件的意义就不大了。

我的经验是把“通达信盘后下载”和“Python解析”这两件事用一个定时脚本串起来:每天收盘后先让通达信自动下载,然后 Python 定时任务去读文件、做增量解析、更新 parquet,一条链路全部自动跑完。这样才能保证策略系统每天早上醒来数据就是最新的。

这几年我见过太多人在数据获取上绕弯路了。花点时间把.day文件这个本地金矿挖开,配合 pytdx 做校验补全,一套免费又稳定的量化数据基础设施就搭建起来了;剩下的事,就是专注在策略本身。

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

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

立即咨询