通达信日线数据批量转CSV:量化回测数据准备完整实现 📅 发布时间:2026/9/2 11:25:37 👁 浏览次数: 简介这是一份通达信day格式文件转CSV工具面向需要批量处理日线数据的量化爱好者与本地数据维护者。工具以C语言编写包含源码与可直接运行的exe覆盖上证、深证、港股并对股票、基金及港股的不同字段结构做了适配。用法上只需将可执行文件复制到对应vipdoc目录下运行即可完成转换无需额外运行环境。资源包共2个文件1个C源码和1个exe大小仅8KB轻量易用源码可供学习解析流程、字段映射和跨市场差异处理。已有6251人学习适合有基础编程能力、希望二次定制输出格式的投资者和开发者使用。 做量化回测的第一件事往往是搞定数据。我这两年一直用通达信做行情复盘客户端里看盘很顺手可真到要把数据倒进 Python 里做回测就发现它默认保存的 .day 文件完全是私有二进制格式pandas 不认Excel 也不认。于是自己写了一个小转换工具把通达信本地日线数据批量转换成 CSV中间顺手解决了港股、基金这些特殊品种的若干坑。这篇文章就把思路和完整实现记录在这里给有类似需求的朋友参考。1. 为什么要把通达信 day 文件搬出来使用场景与数据定位先明确一个前提通达信本身有“数据导出”功能但只能导出当前正在看的股票而且一次一个标的批量操作非常痛苦。对于需要维护几十只、几百只甚至全市场数据的场景直接解析本地 .day 文件才是稳妥路径。.day 文件的好处是本地离线、结构紧凑、下载完整后不依赖网络坏处也很明显——私有格式别的软件不认。我的实际需求有三个一是把日线数据导入 pandas 做策略回测二是给自建的小型行情数据库灌数据三是作为一份脱离通达信客户端也能长期保存的 CSV 备份。如果你也有类似需求这个转换方法可以直接复用。1.1 通达信数据文件的位置通达信盘后数据下载完以后文件会落在安装目录下的vipdoc文件夹里。以我本机的通达信为例目录结构大致是这样的D:\new_tdx\vipdoc\ ├── sh\lday\sh600000.day ├── sz\lday\sz000001.day ├── bj\lday\bj430001.day ├── hk\lday\hk00700.day └── gbbq\其中sh表示上海市场sz表示深圳市场bj是北交所hk是港股。lday就是日线数据的存放目录文件名统一是“市场代码前缀 证券代码 .day”例如sh600000.day代表上海市场的浦发银行日线hk00700.day代表港股腾讯控股的日线。另外vipdoc下还有minline分钟线、fzline分笔数据、gbbq股本变迁也就是除权除息数据等目录本文只处理lday下的日线文件其他目录以后有时间单独说。1.2 转换后的数据形态我最终选择 CSV 作为通用中间格式而不是直接写进数据库。原因很直接CSV 可以被任何工具读取pandas、Excel、R、数据库导入脚本全都认识它。每个标的生成一个独立的 CSV 文件按市场分子目录存放converted_day_csv\ ├── sh\sh600000.csv ├── sz\sz000001.csv ├── bj\bj430001.csv └── hk\hk00700.csv这样做的目的是避免把所有标的全塞进一个大文件万一某个文件坏了也不影响其他数据。等到回测框架需要统一加载时再遍历所有 CSV 拼成 DataFrame 即可。2. 日线二进制文件的记录结构解析方案与缩放系数我在 Windows 上抓了几个不同市场的 .day 文件用十六进制编辑器逐字节看过之后确定下面这套结构在我接触到的通达信版本里是稳定的每条记录固定 32 字节按小端字节序排列。字段顺序如下表字段类型字节数说明日期uint324格式为 YYYYMMDD例如 20231215开盘价uint324整数存储实际价格 整数 / 缩放系数最高价uint324同上最低价uint324同上收盘价uint324同上成交量uint324原始量可能是股或手需自行确认成交额double8单位通常是元注意这里不是整数也就是说一条记录是4*5 4 8 32字节。解析的时候不需要逐字节读直接按这个定长结构切片就行。Python 里最方便的做法是用struct.Struct提前定义好格式import struct DAY_RECORD struct.Struct(IIIIIId) DAY_SIZE DAY_RECORD.size # 32 def read_day_file(path, price_scale): records [] with open(path, rb) as f: buf f.read() count len(buf) // DAY_SIZE for i in range(count): off i * DAY_SIZE date, open_, high, low, close, volume, amount DAY_RECORD.unpack_from(buf, off) records.append({ date: f{date // 10000:04d}-{date // 100 % 100:02d}-{date % 100:02d}, open: open_ / price_scale, high: high_ / price_scale, low: low_ / price_scale, close: close_ / price_scale, volume: volume, amount: amount, }) return records注意price_scale这个参数不能写死。不少网上资料默认写 100这在沪深 A 股普通股票上没问题但遇到基金、港股价格就会整体差一个数量级后面我会详细说。2.1 为什么价格字段要除 100 而不是 1000这需要理解通达信存储价格的逻辑。它不会把浮点数直接存成 double而是把最小价格变动单位放大成整数。对 A 股普通股票来说报价最小单位是 0.01 元所以 12.34 元保存为 1234读取时除以 100 就还原了。但港股和场内基金不一样港股很多股票最小变动是 0.001 甚至 0.005基金净值也有三位小数这时候如果还是除以 100结果就变成实际价格的 10 倍或 100 倍。我见过的最典型的错误是某只 ETF 价格显示 1.234 元转换出来的 CSV 里却变成 12.34这就是因为用了 100 倍缩放。解决办法是把该类标的的price_scale调整为 1000。有些资料里说统一用 1000这也不完全对普通 A 股要是用 100012.34 元会被还原成 123.40 元同样出错。所以必须按市场、按品种区分。2.2 日期、成交量和成交额的原始语义日期字段就是 YYYYMMDD 整数比如 20231215我在代码里直接把它格式化成YYYY-MM-DD字符串方便 CSV 读取时直接被 pandas 识别为日期类型。成交量字段要注意通达信客户端上显示的成交量经常是“手”但文件里存的可能是“股”或者“手”我建议转换时不要做任何单位换算原样输出只在列名里标注清楚。成交额字段是 double单位通常是元。为什么不要换算因为一旦换算错后续核对数据时会很难定位是解析问题还是单位问题。保留原始值再通过校验脚本去和客户端比对才是最稳妥的。3. 批量转换脚本实现从 vipdoc 到 CSV前面的read_day_file只是核心解析函数实际使用中还需要一个能批量扫描目录、自动判断每只标的所属市场、并输出 CSV 的入口。下面这个脚本是我在用的版本去掉了业务无关部分保留核心逻辑。import csv import struct from pathlib import Path DAY_RECORD struct.Struct(IIIIIId) DAY_SIZE DAY_RECORD.size # 按市场前缀配置默认缩放系数 PRICE_SCALE_BY_MARKET { sh: 100.0, sz: 100.0, bj: 100.0, hk: 1000.0, } def get_price_scale(market, code): # 沪深市场内部还要再区分 # 沪市 5 开头是场内基金深市 1 开头是场内基金 # 沪市 000 开头是指数深市 399 开头是指数 if market sh and (code.startswith(5) or code.startswith(000)): return 1000.0 if market sz and (code.startswith(1) or code.startswith(399)): return 1000.0 return PRICE_SCALE_BY_MARKET.get(market, 100.0) def read_day_file(path, price_scale): with open(path, rb) as f: buf f.read() records [] count len(buf) // DAY_SIZE for i in range(count): off i * DAY_SIZE date, open_, high, low, close, volume, amount DAY_RECORD.unpack_from(buf, off) records.append({ date: f{date // 10000:04d}-{date // 100 % 100:02d}-{date % 100:02d}, open: open_ / price_scale, high: high_ / price_scale, low: low_ / price_scale, close: close_ / price_scale, volume: volume, amount: amount, }) return records def convert_one_day_file(src_path, dst_dir, market, price_scale): code src_path.stem[len(market):] records read_day_file(src_path, price_scale) if not records: return 0 out_path Path(dst_dir) / market / f{market}{code}.csv out_path.parent.mkdir(parentsTrue, exist_okTrue) with open(out_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([date, open, high, low, close, volume, amount]) for r in records: writer.writerow([ r[date], r[open], r[high], r[low], r[close], r[volume], r[amount], ]) return len(records) def batch_convert(vipdoc_root, dst_dir, marketsNone): vipdoc_root Path(vipdoc_root) dst_dir Path(dst_dir) markets markets or [sh, sz, bj, hk] total_files 0 for market in markets: lday_dir vipdoc_root / market / lday if not lday_dir.exists(): print(f跳过不存在的目录: {lday_dir}) continue for day_path in sorted(lday_dir.glob(*.day)): code day_path.stem[len(market):] price_scale get_price_scale(market, code) try: n convert_one_day_file(day_path, dst_dir, market, price_scale) print(f{market} {code}: {n} rows) total_files 1 except Exception as e: print(f转换失败: {day_path}: {e}) print(f完成共处理 {total_files} 个文件) if __name__ __main__: batch_convert(rD:\new_tdx\vipdoc, rD:\converted_day_csv)直接运行后在终端里调用python convert_tdx_day.py脚本会遍历vipdoc下所有指定市场的lday目录把所有.day文件转换成同名 CSV。如果你只想转换某几个市场可以这样调用batch_convert(rD:\new_tdx\vipdoc, rD:\converted_day_csv, markets[sh, sz])3.1 文件名解析本文还有配套的精品资源点击获取