☰
Python解析通达信本地行情文件:day/lc5/blk格式与源码详解
2026/10/3 4:27:15 网站建设 项目流程

先说我为什么碰这个事。之前做本地回测,每天导通达信的历史数据都要手动操作一遍:打开软件、切到K线图、右键导出、再选路径。数据一多还容易漏一天。后来想通了一个事——通达信本来就是把行情数据存在电脑上的,直接读它自己的原始文件不是更省事吗?研究了一个下午,把日线、分钟线和板块文件的格式全部摸清楚了,用Python的struct模块按字节解析,几十万条记录几秒钟就能全部载入内存。这篇文章把文件格式、完整解析源码和我在实际使用中踩过的坑一次性讲清楚,适合想在Python里直接使用通达信本地数据的开发者和量化分析入门者。

1. 先弄清楚通达信本地数据放在哪里、长什么样

1.1 vipdoc目录:sh、sz、bj三个子目录的秘密

通达信安装目录下有个vipdoc文件夹,这是它存放行情数据的地方。打开后你通常能看到sh、sz、bj三个子目录,分别对应上海、深圳、北京三个市场。每个子目录里都是按股票代码命名的数据文件,比如sh600000.day就是浦发银行的日线数据,sz000001.day就是平安银行的日线数据。

这类文件虽然后缀有的是.day、.lc5、.lc1,但本质都是二进制文件,不是文本。文件名里的市场前缀加代码组合起来就能唯一定位一只股票,所以在写解析程序时,我一般直接把它当成一个完整的标识符用,不用再单独去判断市场和代码。

值得留意的是,.day文件是日线数据,.lc5是5分钟线数据,.lc1是1分钟线数据。通达信安装目录里还可能有其他后缀的dat文件,比如公式缓存、自选股缓存之类的,但行情文件核心就是这三种。标题里提到的"dat"通常是对这类二进制行情文件的统称,真正需要解析的重点就是它们。

1.2 day、lc5、lc1三种文件的二进制字段布局

通达信的日线文件结构很规整,每条记录固定32字节,依次存储日期、开盘价、最高价、最低价、收盘价、成交额、成交量和保留字段。价格部分为了省空间,存的是整数,实际价格要除以100。

字段类型字节数说明
日期无符号整数4格式为YYYYMMDD
开盘价无符号整数4实际价格=数值/100
最高价无符号整数4实际价格=数值/100
最低价无符号整数4实际价格=数值/100
收盘价无符号整数4实际价格=数值/100
成交额单精度浮点4单位通常是元
成交量无符号整数4单位通常是股
保留字段无符号整数4一般不用

分钟线的记录长度同样是32字节,但多了一个时间字段,布局略有不同:日期、时间、开盘价、最高价、最低价、收盘价、成交额、成交量。这个时间字段的格式是HHMM,比如930代表早上9点30分,1459代表下午14点59分。

很多网上资料会把字段类型写成long,但用Python的struct模块时要注意,通达信这些整数基本都是4字节,对应格式串里的I,不是8字节的Q,这一点搞错了解析出来的数据就是一团乱码。

1.3 blocknew目录:blk板块文件其实是文本

除了行情文件,通达信的自选板块、行业板块默认放在安装目录下的T0002\blocknew目录里,后缀是.blk。它和上面的二进制行情文件不同,本质是文本文件,每行记录一个股票,大概是这样的形式:

600000,1,浦发银行 000001,0,平安银行 830799,2,艾融软件

中间用逗号分隔,第一列是股票代码,第二列是市场标志,第三列是股票名称。市场标志里,0通常代表深圳,1代表上海,2代表北京。但也有部分版本会用竖线|或者制表符分隔,解析时最好做兼容处理。

我遇到过一些老版本通达信,blk文件路径是在T0002\block而不是blocknew,甚至有的自定义板块文件名会带特殊字符。所以写解析代码时,路径别写死,最好做成参数或者自动探测。

2. 用struct逐字节切开二进制行情文件:核心源码

2.1 为什么是struct而不是pandas的read_csv

这类行情文件是固定长度的二进制格式,不是普通文本。Python里处理二进制最顺手的标准库就是struct,它能把一段bytes按照指定格式拆成对应的Python类型。比如<IIIIIfII就表示按小端字节序、依次读取5个无符号整数、1个单精度浮点数、2个无符号整数,共32字节。

pandas.read_csv之类的函数读不了这种文件,因为里面根本没有换行符和分隔符。你当然可以手动读32字节然后切片再转换,但struct.unpack一次就能拆完,代码简洁,性能也好。解析几万个记录只需要几十毫秒。

2.2 日线.day文件解析源码

下面这段是我实际使用的日线解析代码,逻辑很直白:读取文件大小,除以32算出总记录数,然后循环读取并解包。

import struct from pathlib import Path def _unpack_day_record(data): """解包一条32字节的日线记录""" fields = struct.unpack('<IIIIIfII', data) return { 'date': fields[0], # YYYYMMDD 'open': fields[1] / 100.0, # 开盘价 'high': fields[2] / 100.0, # 最高价 'low': fields[3] / 100.0, # 最低价 'close': fields[4] / 100.0, # 收盘价 'amount': fields[5], # 成交额 'volume': fields[6], # 成交量 } def parse_day_file(file_path): """解析通达信日线文件,返回记录列表""" path = Path(file_path) total = path.stat().st_size // 32 records = [] with open(path, 'rb') as f: for _ in range(total): data = f.read(32) if len(data) < 32: break records.append(_unpack_day_record(data)) return records

如果你已经打开了通达信的日线文件,直接调用parse_day_file('C:/new_tdx/vipdoc/sh/sh600000.day')就能拿到浦发银行的历史日线列表。每条记录是一个字典,日期、开高低收、成交额、成交量一目了然。

2.3 分钟线.lc5/lc1解析源码

分钟线解析方式和日线几乎一样,唯一区别是多了个时间字段。这里我单独写一个解包函数,保持代码结构清晰。

def _unpack_minute_record(data): """解包一条32字节的分钟线记录""" fields = struct.unpack('<IIIIIIfI', data) return { 'date': fields[0], # YYYYMMDD 'time': fields[1], # HHMM,如930、1459 'open': fields[2] / 100.0, 'high': fields[3] / 100.0, 'low': fields[4] / 100.0, 'close': fields[5] / 100.0, 'amount': fields[6], 'volume': fields[7], } def parse_minute_file(file_path): """解析通达信分钟线文件,.lc5和.lc1通用""" path = Path(file_path) total = path.stat().st_size // 32 records = [] with open(path, 'rb') as f: for _ in range(total): data = f.read(32) if len(data) < 32: break records.append(_unpack_minute_record(data)) return records

这个函数的后缀是.lc5还是.lc1都不影响,底层记录长度和结构一致。如果你想做盘中监控或者短线分析,这个解析器可以直接用。顺便提醒一句,分钟线文件通常比日线大很多,5分钟线一年的记录量大概在5000条左右,1分钟线一年记录量能到25000条,但即使这样,解析速度依然很快。

2.4 只读最后N条:文件偏移定位技巧

实际分析中经常只需要最近一段数据,比如最近20个交易日。这时完全没必要把整个文件读一遍再截断,可以直接用seek跳到目标偏移量再读,速度更快,内存占用也更少。

def parse_day_tail(file_path, n=20): """只解析日线文件最后n条记录""" path = Path(file_path) total = path.stat().st_size // 32 start = max(total - n, 0) records = [] with open(path, 'rb') as f: f.seek(start * 32) for _ in range(total - start): data = f.read(32) if len(data) < 32: break records.append(_unpack_day_record(data)) return records

如果某个股票的数据文件特别大,这个函数的优势就体现出来了。比如读取上证指数从1990年至今的日线,全量解析可能需要几百万条记录,但只取最后20条,几乎瞬间完成。这种按偏移量读取的思路在实时行情轮询场景下尤其好用。

3. 把blk板块文件读成结构化列表:编码与分隔符兼容

3.1 一个小坑:blk文件是GBK编码

首先得接受一个事实:通达信是国产老牌软件,它的文本文件默认用GBK编码,不是UTF-8。如果你直接用open默认编码读取blk文件,大概率会遇到UnicodeDecodeError。正确做法是显式指定编码,并且加errors参数兜底,防止某个股票名称里有生僻字导致整个解析失败。

def parse_blk_file(file_path): """解析通达信blk板块文件""" path = Path(file_path) text = path.read_text(encoding='gbk', errors='ignore') ...

这里用errors='ignore'处理个别无法解码的字符,虽然会丢掉极少数字符,但比整个程序崩溃强。如果你需要100%准确地保留股票名称,可以换成errors='replace',坏字符显示成占位符,至少能知道这里有问题。

3.2 兼容逗号、竖线、空格三种分隔符

不同版本的通达信,甚至不同板块文件,分隔符并不完全统一。有的用逗号,有的用竖线,还有的用制表符。我见过最头疼的是一个自定义板块文件里混用了两种分隔符。所以在解析前做一个标准化:把竖线和制表符统一替换成逗号,然后再按逗号切分。

def parse_blk_file(file_path): path = Path(file_path) text = path.read_text(encoding='gbk', errors='ignore') stocks = [] for line in text.splitlines(): line = line.strip() if not line or line.startswith('#') or line.startswith('['): continue # 统一分隔符 line = line.replace('|', ',').replace('\t', ',') parts = [p.strip() for p in line.split(',')] if len(parts) < 2: continue code = parts[0] market = int(parts[1]) if parts[1].isdigit() else 0 name = parts[2] if len(parts) > 2 else '' stocks.append({'code': code, 'market': market, 'name': name}) return stocks

有些板块文件顶部会有[Info]之类的配置段,以中括号开头,解析时直接跳过。同时,有的blk文件一行里只有股票代码没有名称,所以name字段要做容错处理。

3.3 市场标志归一化与代码补零

从blk文件里读出来的代码有可能缺前导零,比如1或者000001两种情况都存在。为了后续查询方便,最好统一补足到6位。市场标志理论上0、1、2对应深、沪、京,但有些数据源会不写或者写别的东西,我一般会对数位做一次校验,如果异常就返回0。

def normalize_code(code, market): code = code.strip() if len(code) < 6: code = code.zfill(6) return code, int(market) if str(market).isdigit() else 0

这样拼查询路径的时候就很方便:市场标志为0时拼sz,为1时拼sh,为2时拼bj。如果用户给的股票代码本身就是6位数字,这个函数不会做任何多余操作,兼容性很好。

4. 解析结果喂给pandas:算指标、画K线、导出CSV

4.1 记录列表转DataFrame的推荐姿势

拿到记录列表后,很多人第一步就是转成pandas的DataFrame。这里有几个细节值得注意:日期列要转成真正的日期类型,分钟线数据要做时间索引合并,然后按索引排序避免乱序。

import pandas as pd def to_dataframe(records, kind='day'): df = pd.DataFrame(records) if kind == 'minute': # 日期和时间拼成一个datetime列 time_str = df['time'].astype(str).str.zfill(4) dt_str = df['date'].astype(str) + time_str df['datetime'] = pd.to_datetime(dt_str, format='%Y%m%d%H%M') df = df.set_index('datetime').drop(columns=['date', 'time']) else: df['date'] = pd.to_datetime(df['date'].astype(str), format='%Y%m%d') df = df.set_index('date') return df.sort_index()

to_datetime的format参数一定要写明确,不要偷懒不写,因为通达信的日期格式是YYYYMMDD这种纯数字串,不指定格式时pandas会猜错。分钟线的时间要补零,因为930会被解析成数字,直接转字符串会变成930而不是0930。

4.2 一行代码算MA5/MA20和涨跌幅

数据变成DataFrame后,算技术指标就非常简单了。均线、涨跌幅、成交量变化率全是pandas的内置操作。

def add_indicators(df): df = df.copy() df['ma5'] = df['close'].rolling(5).mean() df['ma20'] = df['close'].rolling(20).mean() df['pct_change'] = df['close'].pct_change() * 100 return df

我经常用这个函数验证某只股票在某个时间段的趋势变化。比如读取浦发银行最近120个交易日的日线,加上MA5和MA20,马上就能判断短期均线是金叉还是死叉。注意pct_change()计算的是相邻两天的涨跌幅比例,乘100是为了显示成百分数,方便阅读。

4.3 导出CSV与matplotlib展示注意中文字体

把结果导成CSV供Excel或者其他程序使用,是刚需。导出时我推荐用utf-8-sig编码,这样Excel直接打开不会出现中文乱码。

df_with_indicator = add_indicators(df) df_with_indicator.to_csv('sh600000_day.csv', encoding='utf-8-sig')

画图时有个老生常谈的坑:matplotlib默认字体不支持中文,图例和标题会显示成方块。解决办法是设置中文字体,在代码开头加两行配置:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False

然后直接调用plot方法就能画出收盘价和均线的走势图,标题也能正常显示中文。如果你需要更专业的K线图,可以装一个mplfinance库,把DataFrame传进去,几十行代码就能画出一张带均线、成交量的K线图。

5. 我在实际解析过程中踩过的坑(排查思路版)

5.1 行情文件不更新:先看文件大小和修改时间

用解析器拿到数据后,我第一件事是核对数据完整性。有次我发现某只股票的日线数据停在了一个月前,怎么解析都只有那几条数据。我的排查思路是:先用Path.stat()看一下文件大小和修改时间。

def file_brief(file_path): stat = Path(file_path).stat() return { 'size': stat.st_size, 'records': stat.st_size // 32, 'modified': stat.st_mtime, }

如果修改时间停在很久以前,而通达信软件里能看到最新行情,那说明软件没有把完整数据写入本地文件。一般情况下,通达信默认只下载最近一段时间的日线,老历史数据需要手动在软件里执行"盘后数据下载"。所以遇到数据缺失,先别怀疑解析器,优先确认文件本身是否完整。

5.2 数据都是未复权:做回测前必须想清楚

通达信本地day文件记录的是原始成交价,不包含复权因子。如果个股在历史上有过分红送股,那么除权日前后用原始价格计算的收益率会出现假摔或者假涨。比如一只股票10送10,除权日价格会从20元直接跳到10元,如果你用原始数据算收益,会得到-50%的错误结果。

要处理这个问题,最稳妥的方式是在通达信软件里用"数据维护工具"导出前复权数据,或者单独获取除权除息信息做复权计算。本地直接解析出的未复权数据适合做行情展示、价格归档,做策略回测前一定得先处理复权,这是我在实测中踩过最深的一个坑。

5.3 当日最后一条记录可能是"半成品"

在交易时间段内运行解析程序,读到的最后一条记录往往不是完整的日线,而是当天截至目前的盘中快照。收盘价会随着行情波动持续变化,直到15点后才定格。我做过一个实时展示程序,盘中每5分钟刷一次最近20条日线,结果最后一条的收盘价一直在变,导致均线也跟着抖动。

一个简单粗暴的规避方法是:在收盘后再做最终计算;如果必须在盘中运行,就把最后一条记录标记为"未完成",或者干脆丢弃。不要拿半成品数据做任何判断,否则容易得出错误结论。

5.4 通达信版本升级导致目录结构变化

通达信不同版本的目录结构并不完全一致,有时候是T0002\blocknew,有时候是T0002\block,还有的版本在T0002下面找不到blk文件,反而是在用户自定义目录里。我的处理方式是用Python启动时自动探测一下目录,而不是硬编码路径。

def find_blocknew_dir(tdx_root): candidates = [ Path(tdx_root) / 'T0002' / 'blocknew', Path(tdx_root) / 'T0002' / 'block', Path(tdx_root) / 'T0002' / 'block_self', ] for d in candidates: if d.exists(): return d return None

这样一来,通达信升级后最多改一个根目录路径,代码其他部分不用动。

5.5 板块文件出现UnicodeDecodeError的解决办法

读取blk文件报编码错误是我见过最多的问题。原因很直接:文件是GBK编码,但Python在Windows上默认的读取编码可能是本地代码页,在其他系统上可能是UTF-8。解决办法我已经写进解析函数里了,就是显式用encoding='gbk'并加errors='ignore'。

如果你遇到的是另一种乱码情况——文件能读出来,但中文名全部变成"锟斤拷"之类的怪异字符,那说明文件的真实编码不是GBK而是UTF-8。这时候可以把编码参数改成utf-8再试一次。我建议封装一个自动尝试的函数,先用GBK读,失败了再换UTF-8,这样不管碰到什么版本的通达信都能兼容。

我在实际项目里的习惯是,把日线解析和板块解析封装成两个独立的模块,parse_day_file负责把二进制文件变成Python对象,parse_blk_file负责把板块文件变成结构化的股票列表。这样无论是做全市场扫描还是单股票分析,代码都能直接复用。另外有一点始终要记住:通达信本地文件只是行情数据的载体,解析出来之后,数据的准确性和完整性仍然需要你自己负责核对,尤其是涉及历史回测和量化交易的时候,多验证几次永远值得。

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

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

立即咨询