ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python解析通达信.day文件,结合pytdx搭建本地量化数据链

Python解析通达信.day文件,结合pytdx搭建本地量化数据链 做量化的人几乎都会遇到同一个坎数据从哪来。市面上的免费数据接口今天能用明天就挂付费终端一年好几千对一个还没跑通策略的新手来说实在肉疼。后来我发现电脑里装的通达信软件本来就在本地落地了一份完整的行情库——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比如 202401154openfloat32开盘价需除以100即 1234.0 表示 12.348highfloat32最高价需除以10012lowfloat32最低价需除以10016closefloat32收盘价需除以10020amountfloat32成交额单位元24volumeuint32成交量单位股28reservedint32保留字段一般填 02.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 pytdxPython 版本我用的是 3.10实测 3.8 到 3.11 都能正常安装。有个小提醒pytdx 的核心代码年头不短了原作者基本不维护在 Python 3.12 上会不会有隐藏问题不好说如果你用的是最新版 Python 且安装后运行报错建议降到 3.10 或 3.11 再试。3.2 连接行情服务器并拉取日线pytdx 的入口是TdxHq_API先连接一个行情服务器然后调用get_security_bars拉取K线。category9表示日线market参数中1代表上海、0代表深圳和前面说的文件目录前缀要对应上。from pytdx.hq import TdxHq_API api TdxHq_API() with api.connect(119.147.212.81, 7709, time_out10): 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_filesNone): 扫描 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 datasetsglob(*/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_size0): 从 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 的 dtypesdf.to_parquet(SH600000.parquet, indexFalse)一个.day文件解析后大约 150KB 左右全市场存下来几百 MB完全在可接受范围内。5. 用pytdx拉服务端行情交叉验证本地文件5.1 为什么要做校验.day文件的可靠性很大程度上取决于通达信客户端的数据下载是否完整。有时候盘中直接看数据文件还没有更新有时候网络中断导致盘后下载失败本地记录就一直停在前一天。如果不做任何校验回测时缺了最近一根K线结果总感觉差一口气。校验的逻辑很简单拿本地解析出的最后一条记录和 pytdx 从服务器拉回的最新日线做对比两边收盘价一致就说明本地数据是新的不一致或本地日期小于服务器日期就用服务器的数据补上。5.2 完整校验代码from pytdx.hq import TdxHq_API def fetch_latest_daily(market, code, count5): 通过 pytdx 拉取最近 count 根日线。market: 1sh, 0sz 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_out10) 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_indexTrue) return local_df, True这段代码的思路是先对比最后日期的收盘价如果对得上就认为本地是最新的如果对不上说明中间有缺失或本地还没更新就把服务器上比本地最新日期更新的记录追加进去。market参数要和代码前缀对应比如SH600000对应market1SZ000001对应market0。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/exceptpytdx 在连接异常时抛的异常类型不固定代码里做兜底保证单个股票校验失败不会影响整个批量任务。7.4 每天都做盘后下载最后这条看着像废话但真的最影响实际体验。pytdx校验能补最近一两天的数据但毕竟不是长远的解决办法。如果本地.day一直不更新每次都要靠网络拉那本地文件的意义就不大了。我的经验是把“通达信盘后下载”和“Python解析”这两件事用一个定时脚本串起来每天收盘后先让通达信自动下载然后 Python 定时任务去读文件、做增量解析、更新 parquet一条链路全部自动跑完。这样才能保证策略系统每天早上醒来数据就是最新的。这几年我见过太多人在数据获取上绕弯路了。花点时间把.day文件这个本地金矿挖开配合 pytdx 做校验补全一套免费又稳定的量化数据基础设施就搭建起来了剩下的事就是专注在策略本身。
返回列表