通达信.day文件解析与CSV转换实战:构建本地金融数据仓库

📅 2026/8/13 8:44:40
通达信.day文件解析与CSV转换实战:构建本地金融数据仓库
1. 从通达信.day文件到CSV一个数据工程师的日常今天想聊聊一个在金融数据领域尤其是个人量化研究初期几乎人人都会遇到的一个“脏活累活”把通达信软件的.day日线数据文件转换成我们更熟悉的CSV格式。这听起来像是个简单的格式转换但如果你真的动手做过就知道里面藏着不少门道。为什么不用现成的数据源为什么非得是通达信这背后其实是一个关于数据自主性、成本控制以及历史数据完整性的故事。很多朋友刚开始做量化回测第一道坎往往不是策略本身而是找不到一份干净、连续、覆盖足够长时间的历史行情数据。网上的免费API要么有频率限制要么数据质量参差不齐而专业的金融数据服务对于个人研究者来说又价格不菲。通达信的.day文件作为一款在国内市场占有率极高的终端软件本地存储的格式反而成了一个稳定、免费且数据质量相对可靠的“宝藏”。把它用好相当于为自己搭建了一个本地的、可控的基础数据仓库。这个过程的核心就是理解.day这个二进制文件的“黑盒”结构并把它“翻译”成人类可读、程序可处理的表格形式。我最初接触这个需求是为了回测一个需要用到十几年A股全市场日线数据的策略。从网上零散地下载CSV总会遇到格式不统一、字段缺失、复权方式混乱的问题折腾得心力交瘁。最后下定决心直接从最源头的.day文件开始处理虽然前期解析需要花些功夫但一旦流程跑通后续的数据维护和更新就变得异常清晰和高效。这期内容我就把自己趟过的路、踩过的坑以及最终稳定运行的代码逻辑毫无保留地分享出来。你会发现这个转换过程不仅是一个技术实现更是一种数据治理思维的体现。2. 理解源头通达信.day文件的二进制结构解析在动手写代码之前我们必须先搞清楚我们要处理的“原材料”到底是什么。通达信的.day文件是一种紧凑的二进制格式设计初衷是为了软件自身快速读取和存储而不是为了开放交换。每个股票对应一个.day文件例如sh600000.day就代表了浦发银行的日线数据。它的结构是固定的每32个字节bytes记录一天的行情数据。这32个字节就像一条记录了当天所有关键信息的“数据记录”按照特定的顺序排列。我们可以把这32个字节拆解成若干个字段每个字段都有固定的数据类型和长度。以下是一个通用的结构定义具体字节顺序可能因通达信版本略有差异但主流版本如下字节偏移 (起始为0)字段长度 (字节)数据类型字段含义解析说明0 - 34unsigned int日期 (Date)存储格式为YYYYMMDD的整数如20231027。4 - 74unsigned int开盘价 (Open)实际价格乘以1000或100取决于版本以整数存储。常见是乘以1000即精确到0.001元。8 - 114unsigned int最高价 (High)同上实际价格乘以1000。12 - 154unsigned int最低价 (Low)同上实际价格乘以1000。16 - 194unsigned int收盘价 (Close)同上实际价格乘以1000。20 - 234float成交金额 (Amount)以“万元”为单位的浮点数。这是最容易混淆的地方单位是万不是元。24 - 274unsigned int成交量 (Volume)以“手”为单位的整数1手100股。28 - 314unsigned int保留字段通常为0不同版本可能用于存储换手率等其他信息但主流.day文件此字段忽略。注意价格相关的“缩放因子”是第一个关键坑点。通达信早期为节省存储空间和避免浮点数精度问题将价格以整数形式存储。你需要确认你的数据源使用的缩放因子是100还是1000。一个简单的判断方法是找一只你知道价格的股票用代码读出一个收盘价的整数值除以100和1000看哪个结果更接近真实交易价格。目前市面上大部分导出的数据缩放因子是1000。第二个关键点是字节序。通达信软件运行在Windows系统上而Windows通常采用小端序。这意味着在读取这4字节的整数或浮点数时我们需要按照小端序来解析。Python的struct模块可以轻松处理这个问题。例如读取日期字段我们需要用‘I’这个格式符代表小端I代表4字节无符号整数。理解了这个结构转换的逻辑就清晰了打开一个.day文件以二进制模式读取每次读取32个字节作为一个数据块然后用struct.unpack按照上述格式解析这个数据块得到原始的数值最后进行单位换算价格除以缩放因子成交金额乘以10000转换为“元”并格式化为CSV的一行。这个过程本质上是一个“数据反序列化”。3. 实战代码拆解从文件读取到CSV写入的完整流程理论清晰后我们来看具体的Python实现。我会用一个函数化的、结构清晰的代码示例并解释每一部分的意图和可能遇到的细节问题。首先我们需要准备环境。除了Python标准库核心只用到了struct模块无需额外安装。import struct import os import pandas as pd from pathlib import Path from typing import List, Optional # 定义常量根据你的数据源调整 RECORD_SIZE 32 # 每条记录固定32字节 PRICE_SCALE 1000.0 # 价格缩放因子可能是100或1000 AMOUNT_SCALE 10000.0 # 成交金额单位是“万元”乘以10000得到“元” def parse_tdx_day_file(file_path: str) - List[dict]: 解析单个通达信.day文件返回字典列表。 Args: file_path: .day文件的完整路径。 Returns: 按日期倒序排列的日线数据列表每个元素是一个字典。 data_list [] try: with open(file_path, rb) as f: while True: # 每次读取32字节 chunk f.read(RECORD_SIZE) if not chunk or len(chunk) RECORD_SIZE: break # 文件读取完毕或数据不完整 # 使用小端序解析32字节数据块 # 格式字符串: I I I I I f I I (具体含义见下表) # 注意最后一个I是保留字段我们通常忽略 date_raw, open_raw, high_raw, low_raw, close_raw, amount_raw, volume_raw, _ \ struct.unpack(IIIIIfII, chunk) # 1. 处理日期整数转字符串 ‘YYYY-MM-DD’ date_str str(date_raw) formatted_date f{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]} # 2. 处理价格除以缩放因子保留3位小数 open_price open_raw / PRICE_SCALE high_price high_raw / PRICE_SCALE low_price low_raw / PRICE_SCALE close_price close_raw / PRICE_SCALE # 3. 处理成交金额万元转元并保留2位小数通常足够 # 注意amount_raw本身就是浮点数万元例如12.5代表12.5万元 amount_yuan amount_raw * AMOUNT_SCALE # 4. 成交量单位已是“手”直接使用 volume volume_raw data_list.append({ date: formatted_date, open: round(open_price, 3), high: round(high_price, 3), low: round(low_price, 3), close: round(close_price, 3), amount: round(amount_yuan, 2), # 成交额单位元 volume: volume, # 成交量单位手 }) except FileNotFoundError: print(f文件未找到: {file_path}) return [] except struct.error as e: print(f解析文件 {file_path} 时发生结构错误文件可能已损坏或格式不符: {e}) return [] # 通达信.day文件默认按日期倒序存储最新数据在前 # 我们通常希望CSV按日期正序排列所以这里反转列表 return data_list[::-1]这段代码是核心解析器。struct.unpack(‘IIIIIfII’, chunk)这一行是关键它严格按照我们前面分析的8个字段顺序和类型进行解析。这里有一个非常重要的细节通达信的.day文件内部是按日期倒序排列的即文件开头是最近一天的行情文件末尾是最早的数据。所以我们在函数最后使用了data_list[::-1]将列表反转这样输出到CSV时数据就是按日期从远到近的正序排列符合大多数分析工具的习惯。接下来我们需要一个函数来处理批量转换并生成CSV。def convert_day_dir_to_csv(day_dir: str, output_dir: str, code_filter: Optional[str] None): 将一个目录下的所有.day文件批量转换为CSV。 Args: day_dir: 存放.day文件的目录路径。 output_dir: 输出CSV文件的目录路径。 code_filter: 可选股票代码过滤如sh600000只转换匹配的文件。 day_path Path(day_dir) output_path Path(output_dir) output_path.mkdir(parentsTrue, exist_okTrue) # 创建输出目录 # 遍历目录下所有.day文件 for day_file in day_path.glob(*.day): stock_code day_file.stem # 获取文件名不含后缀如sh600000 # 如果指定了过滤条件则进行匹配 if code_filter and code_filter not in stock_code: continue print(f正在处理: {stock_code}) data parse_tdx_day_file(str(day_file)) if not data: print(f - 警告: {stock_code} 未解析出数据可能文件为空或损坏。) continue # 使用pandas DataFrame进行整理和保存 df pd.DataFrame(data) # 设置日期为索引可选但非常方便 df[date] pd.to_datetime(df[date]) df.set_index(date, inplaceTrue) # 保存为CSV csv_file output_path / f{stock_code}.csv df.to_csv(csv_file, encodingutf-8-sig) # 使用utf-8-sig避免中文Excel乱码 print(f - 已保存: {csv_file}, 共 {len(df)} 条记录。)这个批量处理函数展示了如何将单个解析函数应用到整个目录。这里有几个实用的设计点使用pathlib.Path这是处理文件路径的现代、安全的方式比直接拼接字符串更优雅。创建输出目录output_path.mkdir(parentsTrue, exist_okTrue)这一行确保了输出目录存在避免运行时错误。编码问题保存CSV时指定encoding‘utf-8-sig’这个编码会在文件开头添加一个BOM字节顺序标记使得用微软Excel打开时能正确识别UTF-8编码避免中文乱码。这是处理金融数据时的一个小技巧。使用Pandas虽然我们完全可以用Python标准库写CSV但Pandas的DataFrame在数据清洗、查看和后续处理上提供了巨大的便利。将日期列转换为datetime类型并设为索引后续进行时间序列分析会非常方便。你可以这样调用这个函数if __name__ __main__: # 假设你的.day文件放在 D:/tdx_data/day 下 source_dir D:/tdx_data/day # 希望输出的CSV放在 D:/tdx_data/csv 下 target_dir D:/tdx_data/csv convert_day_dir_to_csv(source_dir, target_dir) # 如果只想转换某一只股票可以加上过滤 # convert_day_dir_to_csv(source_dir, target_dir, code_filtersh600000)4. 关键细节、常见陷阱与数据质量校验代码跑起来不难但要让转换后的数据真正可靠用于严肃的回测我们还得关注以下几个容易出错的细节和必须进行的校验步骤。4.1 复权问题转换的是“裸数据”这是最重要的一点。通达信本地存储的.day文件是未经过复权的原始行情数据即除权除息日的价格会出现跳空缺口。我们的转换程序只是忠实地将这些原始数字提取出来不会进行任何前复权或后复权处理。如果你需要连续的价格序列用于计算收益率必须在转换后另行进行复权处理。提示复权是一个独立的、复杂的步骤需要用到除权除息信息。通常的做法是获取一份完整的除权除息表然后对转换出来的原始CSV数据进行处理。也可以使用一些开源库如akshare的复权接口但需要注意数据源的一致性。在转换阶段心里一定要绷紧这根弦我拿到的是原始价。4.2 数据完整性校验转换完成后不要急着用先做快速校验时间范围检查用Pandas打开几个不同股票的CSV检查起始日期和结束日期是否合理。例如检查最早的数据是不是在股票上市日之后最新的数据是否更新到最近一个交易日。价格合理性检查随机抽查几天数据用(high low)和(high open)、(high close)、(low open)、(low close)这些基本逻辑规则进行验证。虽然原始数据出问题的概率低但校验一下能防止因解析程序bug导致的数据错乱。缺失值检查检查是否有整行数据为NaN或0尤其是成交额为0可能意味着停牌日但价格不应全为0。使用df.isnull().sum()快速查看。顺序检查确认数据是否按日期严格递增因为我们反转了列表。4.3 文件路径与编码的坑路径中的空格和中文字符如果源文件路径包含空格或中文确保在代码中正确处理。pathlib和open()函数通常能处理但在某些老旧系统或复杂环境下可能需要额外注意。文件权限通达信的.day文件目录有时会被软件独占或设置权限。确保在通达信软件完全退出后再运行转换脚本否则可能因文件被锁定而读取失败。版本差异虽然我们讨论了主流格式但通达信软件历史上可能存在细微的版本差异。如果发现解析出来的价格明显不对比如所有价格都异常大或小首先怀疑PRICE_SCALE常量设置错误其次是字节序。可以尝试将格式字符串中的小端改为大端测试。4.4 性能优化建议当你需要处理全市场几千只股票、长达二十年的数据时效率就很重要了。向量化操作上述代码是逐条解析的。对于单个文件速度尚可。如果追求极致性能可以考虑一次性读取整个文件字节流然后用numpy的frombuffer函数配合dtype定义结构化数组来一次性解析速度会有数量级的提升。但这需要对numpy有更深的理解。并行处理股票文件之间是独立的非常适合并行。可以使用Python的concurrent.futures.ProcessPoolExecutor进行多进程转换充分利用多核CPU。注意并行时文件读写最好分散到不同磁盘或目录避免IO成为瓶颈。内存考虑全市场数据全部读入内存可能会很大。上述代码是处理一个保存一个内存友好。如果使用向量化方法需要注意单个大数组的内存占用。5. 从CSV到数据应用构建本地数据管道的思路转换出CSV不是终点而是起点。接下来我们可以基于这些干净的CSV文件构建一个更自动化、更健壮的个人数据管道。5.1 设计一个统一的数据存储层不建议每次分析都直接读取成千上万个分散的CSV文件。一个更好的做法是将所有股票的CSV数据导入到一个统一的数据库中。SQLite是一个完美的选择它无需安装服务器单个文件即可管理并且支持完整的SQL查询。你可以创建一个包含date日期、code代码、open、high、low、close、volume、amount等字段的数据表。然后编写一个脚本遍历所有CSV文件将其数据插入INSERT或替换REPLACE到数据库的相应位置。这样你所有的日线数据都在一张表里查询起来非常方便例如“获取2023年所有沪深300成分股的日收盘价”一句SQL就能搞定。5.2 实现增量更新市场每天都有新数据。我们不可能每天全量转换所有文件。一个实用的方案是首次运行全量转换并导入数据库。此后每天只处理更新过的.day文件。如何判断可以比较.day文件的最后修改时间mtime或者更可靠的是在数据库中记录每只股票最新的数据日期然后去.day文件中解析出最新的几条记录只补充数据库中缺失的日期。这个过程可以脚本化结合Windows的任务计划程序或Linux的cron实现每日收盘后的自动数据更新。5.3 与量化框架集成有了结构化的数据库你就可以轻松地与各种量化分析框架如backtrader,zipline, 或国内的RQAlpha对接。通常这些框架都支持从Pandas DataFrame或自定义数据源加载数据。你可以写一个通用的数据加载函数根据策略需要的股票列表和时间范围从你的SQLite数据库中快速查询并组装成框架要求的格式。这样做的好处是数据主权完全掌握在自己手中。你清楚数据的每一个细节知道它有没有复权、如何更新的避免了因第三方数据源API变动或服务中断而导致的研究受阻。回过头看从通达信.day文件到CSV的转换看似是一个简单的格式解析问题实则贯穿了数据获取、解析、清洗、存储和管理的全流程。它强迫你去理解最原始的数据结构在这个过程中建立起来的数据敏感性和处理能力远比直接调用一个现成的get_price()API要宝贵得多。我的经验是初期多花些时间在数据基础建设上后期策略研究和迭代的效率会呈指数级提升。当你不再为数据问题分心才能更专注于策略逻辑本身。