最早我跟通达信“较劲”是在一次量化复盘的需求里手里有几百只股票想拿历史行情做回测结果发现通达信自带的数据导出功能点起来太折磨人不是得一只只选就是导出后要么缺字段、要么格式还得二次清洗。后来我索性不再走软件的导出菜单直接去读通达信安装目录下的本地数据文件。绕开界面操作之后整个流程变成了一个可重复的执行脚本数据转换工具也就在这时候成型了。如果你也需要把通达信的本地数据转成自己熟悉的格式比如 CSV、Excel 或者 DataFrame这篇文章刚好可以帮你少走弯路。我会把目录结构、二进制文件格式、转换工具的实现思路、以及我在实际使用中踩过的几个大坑一次性说完适合做量化分析、数据清洗或者技术指标二次开发的读者参考。1. 本地数据不在你想的地方先搞清楚通达信的文件目录很多人在通达信里找数据第一反应是打开行情软件按 F1 或右键菜单“数据导出”。但真要批量、稳定地读数据最靠谱的来源反而是软件安装目录下面那些不起眼的数据文件。通达信把行情数据存在一个叫 vipdoc 的目录里旁边还有一个 T0002 目录主要放自选股、板块和用户配置。vipdoc 内部按市场再分目录sh 代表上海市场sz 代表深圳市场。我在转换工具里最常用的路径大致如下相对 vipdoc 的路径内容常见扩展名sh/lday沪市日线数据.daysz/lday深市日线数据.daysh/fzline沪市5分钟K线.lc5sz/fzline深市5分钟K线.lc5sh/minline沪市1分钟K线.lc1sz/minline深市1分钟K线.lc1文件命名规则也很有意思它不是简单的“股票代码 . 后缀”而是带市场前缀比如 sh600000.day、sz000001.day。这个看起来不起眼的细节在写批量转换工具时非常关键如果光拿六位数字做唯一标识可能会把沪深两市的同号股票混在一起。做数据工具的第一件事就是要把“市场前缀代码”作为一个完整的股票标识来对待。为什么我坚持直接读本地文件而不是调用行情接口或者依赖手工导出原因有三点。第一是稳定。本地数据是行情软件自己下载并落盘的二进制文件只要文件存在读取结果就很稳定不受网络波动影响。第二是完整。接口通常只能取到有限长度或者有频率限制而本地日线文件里往往存着这只股票从上市以来到最近交易日的每一根K线对回测来说非常重要。第三是快。本地读取本质上是顺序扫描二进制文件比逐条调接口快几个数量级。后面我会专门算一下这个速度量级。2. 二进制记录的一眼化解析32字节读出一根K线通达信的本地数据文件说穿了就是一组定长二进制记录的串联。没有表头没有索引每条记录固定 32 字节。找到这个规律后解析工作就变得非常简单。2.1 日线 .day 文件记录结构日线文件的核心段可以拆成这样的字段表字节偏移字段类型说明0-3日期uint32存储为 20240506 这样直接可读的数字4-7开盘价float32单精度浮点单位元8-11最高价float32单精度浮点单位元12-15最低价float32单精度浮点单位元16-19收盘价float32单精度浮点单位元20-23成交额float32单位元24-27成交量uint32单位股28-31保留字段uint32一般不用这里有个非常容易踩的坑价格和成交额是浮点数不是整数。我最早写解析时图省事把 32 字节全部按照 uint32 解读结果读出来的开盘价、收盘价完全不对。用 Python 的 struct 模块按小端序把这些字段拆开就能得到正确结果。import pathlib import struct import pandas as pd def read_tdx_day(path: pathlib.Path) - pd.DataFrame: rows [] with open(path, rb) as f: while True: chunk f.read(32) if len(chunk) 32: break date_, open_, high_, low_, close_, amount_, vol_, _ struct.unpack( IfffffII, chunk ) rows.append((date_, open_, high_, low_, close_, amount_, vol_)) 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这段代码的核心就一行struct.unpack(IfffffII, chunk)。表示小端序I是无符号整型f是单精度浮点整体长度正好 41 45 4*2 32 字节和记录长度完全对应。按这个解析出来的成交量单位是“股”不是“手”。如果通达信界面显示某天成交 10000 手二进制里读到的就是 1000000 股这个换算关系在后续对账里要格外注意。2.2 分钟线结构与文本导出的差异日线之外分钟线文件同样令人感兴趣。5分钟线文件扩展名是 .lc51分钟线是 .lc1它们同样是 32 字节定长记录。常见的字段顺序是日期、时间、开盘、最高、最低、收盘、成交量、成交额。时间字段一般存成类似 931 这样的整数表示 9 点 31 分有的是 900 表示开盘第一分钟。这里我必须多说一句不同版本的行情软件、不同数据供应商生成的分钟线文件字段顺序和单位可能会有一点点区别。网上有人把成交量放前面有人把成交额放前面相差一个字段就会导致整列数据错位。我的做法是先拿一只比较熟悉的股票做对照读取最近两个 5 分钟K线跟盘中看到的实时数据比对一下确认字段顺序后再批量转换。如果你更喜欢导出的文本格式也有两个本地化的坑要处理。通达信导出的CSV很多时候是 GBK 编码直接用 pandas 的默认 utf-8 读取会出现中文乱码而二进制文件本身没有编码问题这也是我最终选择直接解析二进制的重要原因。2.3 为什么能做到“秒开”几千个文件算一笔账就清楚了。一只股票 20 年日线按每年约 250 个交易日计算总共 5000 根K线每条记录 32 字节文件大小约为 160KB。全市场就算取 5000 只股票日线文件合计也就几百 MB 级别这对现代计算机来说完全是可接受的数据量。用 Python 顺序扫描加 struct 解包跑完全市场日线也就是几十秒的事比逐只调用网络接口快了太多。这也是整个数据转换工具敢于选择“直接读本地文件”这个思路的根本原因。3. 转换工具的核心实现扫描目录、规整字段、落盘解析单只股票只是验证思路真正实用的工具必须能扫描整个 vipdoc 目录批量处理所有股票并把结果统一落盘。这个环节我把它拆成三块命令行入口、批量处理逻辑、输出格式。3.1 命令行入口设计为了让自己在日常使用时少打很多重复命令我设计了一个简单的命令行入口大概长这样python tdx2csv.py --src D:\new_tdx\vipdoc --out ./data --markets sh sz --types day参数含义很直白--src指向通达信安装目录下的 vipdoc--out是转换结果输出目录--markets控制扫描沪深哪个市场--types控制转日线还是分钟线。命令行设计成显式传参是为了避免把安装路径硬编码在脚本里。不同人的通达信安装位置千差万别硬盘盘符、目录名都不同一个能反复使用的转换工具首先要做到路径可配置。3.2 批量扫描与解析逻辑批量扫描的核心逻辑不复杂但要注意几个小细节。第一步是用 pathlib 遍历指定目录下的所有 .day、.lc5、.lc1 文件第二步是按文件名提取市场前缀和股票代码第三步是逐文件解析成 DataFrame顺手做一次空数据过滤第四步是统一输出。我当时是这么写的from pathlib import Path from concurrent.futures import ThreadPoolExecutor def scan_and_convert(src: Path, out_dir: Path, markets: list[str], types: list[str]): out_dir.mkdir(parentsTrue, exist_okTrue) files [] for market in markets: for t in types: sub {day: lday, lc5: fzline, lc1: minline}[t] files.extend((src / market / sub).glob(f*.{t})) def work(path: Path): try: df read_tdx_day(path) if path.suffix .day else read_tdx_minute(path) if df is None or df.empty: return None code path.stem df.insert(0, code, code) out_path out_dir / f{code}.csv df.to_csv(out_path, indexFalse, encodingutf-8-sig) return (code, len(df)) except Exception as exc: return (str(path), exc) with ThreadPoolExecutor(max_workers8) as pool: results list(pool.map(work, files))这里用了 ThreadPoolExecutor 做并发因为文件读取是个 IO 密集操作多线程能明显缩短全市场转换时间。编码用了utf-8-sig这是为了让生成的 CSV 文件用 Excel 直接打开时中文不乱码也算是一个很小的经验细节。3.3 输出格式选择与增量转换输出格式上我最有感触的一点是不要无脑全都导出 Excel。Excel 单个工作表最多 1048576 行听上去很多但如果你把全市场日线合到一个文件里几万行倒是没问题可一旦把分钟线也合进去几百万行就会直接超出上限。更合理的方案是单只股票、少量数据用 Excel 方便人工查看全市场日线用 CSV 按代码拆分每个文件只装一只股票如果要继续做量化分析优先输出 Parquet 或者直接 pickle 成 DataFrame。另外建议给工具加一个“跳过已转换文件”的选项。判断依据很简单如果输出目录里已经存在同名的 CSV而且文件大小大于 0就默认跳过。这样每天收盘后只需要转换新增的文件不用反复处理全部历史数据执行时间会短很多。4. 转换时最容易忽视的三个细节复权、时间戳和最后一根K线工具能跑通之后我一度以为大功告成结果在实际使用中接二连三发现问题。这些问题不是解析代码写错了而是对行情数据本身的深层语义理解不到位。这里挑三个最有代表性的说。4.1 本地数据默认是不复权数据通达信本地 .day 文件里存的是交易所直接下发的原始价格也就是不复权价格。遇到除权除息日股价会出现明显的跳空。如果你直接拿这个数据去计算技术指标或者做回测结果和你在行情软件里看到的前复权K线可能差得很远。关键在于前复权价格会随复权基准日的改变而改变如果你直接在转换工具里写死一个前复权逻辑反而会把数据弄坏。我的做法是转换时保留原始价格不动同时用独立的权息表去计算复权因子。如果你暂时没有权息表至少要在输出的 CSV 文件名或字段注释里标明“未复权”避免后面自己都分不清数据口径。4.2 最后一根K线可能并不是完整K线盘中或者收盘后不久去读取本地数据最后一根K线往往是“未完成”的。比如日线文件里已经有“今天”这根K线但它的最高、最低和收盘价格在盘中时还在不断变化。如果你直接把这个残影当成收盘数据去做策略信号结果会和盘后静态数据差很多。这个问题的解决办法不是删数据而是让工具提供过滤选项。我更推荐的是做一个--drop-last参数由用户根据自己取数时点决定是否去掉最后一根K线。更稳妥的做法是结合系统当前日期和文件修改时间做判断如果文件最后更新时间距离当前时间很近说明可能正在盘中那就默认不输出最后一根。4.3 代码重复、文件损坏与完整性校验批量转换全市场时最恼人的是一小撮“坏文件”。有些文件是刚下载了一半记录长度不是 32 字节的整数倍有些文件是某天程序异常退出里面残留了空记录还有些股票已经退市但在目录里仍然保留着历史数据文件。如果脚本在遇到第一个坏文件时就抛异常整个批量任务就前功尽弃了。我的容错策略是单个文件解析失败时记录错误日志并继续处理下一个文件。等全部跑完后先看日志里有哪些文件失败再针对性处理。此外我还会生成一个 manifest 文件记录每只股票的输出行数。之后和行情软件核对时一眼就能看出哪只股票少了数据不用把所有文件都打开检查一遍。5. 转换结果到底准不准用一次真实“对账”来验证工具写完以后最要紧的不是功能花哨而是数据准不准。我在项目里专门花了半天做了一个对账实验方法和结论都值得分享。5.1 与通达信界面数据逐项比对我选了沪市和深市各一只股票比如 sh600000 和 sz000001先读取它们最近 5 个交易日的日线记录再打开通达信软件手动记录同样的日期、开盘、最高、最低、收盘和成交量逐项比对。比对时特别注意里两个点价格数值要精确到小数点后两位如果出现类似 12.399999 这种浮点尾差说明数据本身没问题是浮点存储的正常现象成交量要确认单位。通达信界面通常会把成交量显示成“手”而二进制文件里是“股”比对时要把软件里的数值乘以 100 再比。我第一次对账时就发现成交量总差一百倍后来才反应过来单位问题。这一步如果做错后面所有策略结果都会失真。5.2 用转换结果快速算一个均线策略对账通过后我拿转换出的 DataFrame 直接算了一个简单的双均线策略验证数据能不能无缝进入回测流程。df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() df[position] (df[ma5] df[ma20]).astype(int) df[ret] df[close].pct_change() df[strategy_ret] df[position].shift(1) * df[ret] df[cum] (1 df[strategy_ret]).cumprod()这里最重要的一个细节是position.shift(1)。如果不把信号往后移一天你相当于在同一天收盘后看到了当天的信号立刻用当天的收益结算这在回测里叫“未来函数”。很多新手明明数据没问题策略曲线却漂亮得不真实基本就是这里出了错。5.3 对账中发现的常见问题和规避方法对账下来除单位问题外最常见的还有两类问题。一类是成交额和成交量的数值差异。有的行情软件在导出文本时会把成交额换成“千元”或“万元”但二进制里通常保留原始元数。另一类是日期字段的解析差异。二进制里是整数 20240506有人直接当成字符串转日期没问题但如果不小心把它当数值减一日期就会错乱。我建议工具里统一用pd.to_datetime(df[date].astype(str), format%Y%m%d)来做转换既明确又不容易出错。对账这一步看似繁琐但它能帮你把字段语义彻底固化下来之后换数据源、换市场、换软件版本时都更有底气。6. 转出来的数据还能怎么用公式指标、板块数据与本地小模型数据转换本身不是终点最终目的是让这些数据在更多场景里发挥价值。从我的实践看转换后的本地数据至少能在三个方向上继续延伸。6.1 给通达信公式和自定义指标提供外部数据通达信公式系统自带了很多指标但它本质上是在软件内部取数运算。如果你在 Python 里加工出了一些特殊指标比如经过机器学习预测的评分、清洗后的财务数据想拿回通达信里看效果可以考虑走通达信的“自定义数据”功能把 CSV 文件导入后再在公式里用对应函数引用。这里有个小提示外部数据导入前一定要把股票代码格式和通达信内部代码格式对齐特别是深市代码要不要带 sz 前缀不同版本要求不一样。我吃过几次亏之后都会在导入前先做一次字段重命名映射。6.2 把转换结果作为本地训练和推理模型的底料最近“开源本地数据推理模型”的概念比较热很多人想用本地数据训练自己的小模型。这类工作通常需要一个干净、完整的训练集而转换工具恰好能解决数据源头问题。你可以基于转换后的 DataFrame 构造一批特征过去 5 日和 20 日的收益率近 20 日的波动率当日成交量与 5 日均量的比值最高价与收盘价的偏离程度最近一段时间均线排列状态然后把特征表保存成 CSV喂给常见的轻量模型库在本地完成训练和推理。需要特别强调的是时间序列样本必须按照时间顺序切分训练集和验证集不能随机打乱否则相当于用未来信息训练模型验证结果会严重失真。6.3 与其它数据源按代码和时间对齐合并转换出的行情数据还可以和基本面数据、板块数据、资金流数据做合并。合并时最稳妥的方式是用 (市场前缀股票代码, 交易日期) 作为联合主键先做一次字段类型统一再按左连接或内连接合并。我用 pandas 的 merge 做过很多次类似操作只要保证代码和日期两列格式一致基本不会出问题。如果你的最终目标是搭建一个可复用的本地数据管道我建议在转换工具里就固定输出的列名顺序并增加一个带版本号的输出目录。这样一来后续的数据清洗、特征工程、模型训练都能基于同一套标准数据展开不容易在迭代中搞乱。我做这个通达信数据转换工具最大的体会是很多看似复杂的数据问题本质上只是文件格式和字段语义的问题。只要花时间把目录结构、二进制布局、单位换算这些底层细节摸清楚后续的一切都会顺畅很多。如果你也在做类似的事情建议先别急着把所有功能做全而是先挑一只股票把单文件解析、对账、输出这一条链路跑通再慢慢扩展到全市场。这样既能控制风险也能让你在每一步都清楚自己究竟在转换什么。