尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python解析通达信本地行情文件:day/lc5/blk格式与源码详解

Python解析通达信本地行情文件:day/lc5/blk格式与源码详解 先说我为什么碰这个事。之前做本地回测每天导通达信的历史数据都要手动操作一遍打开软件、切到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, n20): 只解析日线文件最后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(encodinggbk, errorsignore) ...这里用errorsignore处理个别无法解码的字符虽然会丢掉极少数字符但比整个程序崩溃强。如果你需要100%准确地保留股票名称可以换成errorsreplace坏字符显示成占位符至少能知道这里有问题。3.2 兼容逗号、竖线、空格三种分隔符不同版本的通达信甚至不同板块文件分隔符并不完全统一。有的用逗号有的用竖线还有的用制表符。我见过最头疼的是一个自定义板块文件里混用了两种分隔符。所以在解析前做一个标准化把竖线和制表符统一替换成逗号然后再按逗号切分。def parse_blk_file(file_path): path Path(file_path) text path.read_text(encodinggbk, errorsignore) 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线、导出CSV4.1 记录列表转DataFrame的推荐姿势拿到记录列表后很多人第一步就是转成pandas的DataFrame。这里有几个细节值得注意日期列要转成真正的日期类型分钟线数据要做时间索引合并然后按索引排序避免乱序。import pandas as pd def to_dataframe(records, kindday): 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, encodingutf-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。解决办法我已经写进解析函数里了就是显式用encodinggbk并加errorsignore。如果你遇到的是另一种乱码情况——文件能读出来但中文名全部变成锟斤拷之类的怪异字符那说明文件的真实编码不是GBK而是UTF-8。这时候可以把编码参数改成utf-8再试一次。我建议封装一个自动尝试的函数先用GBK读失败了再换UTF-8这样不管碰到什么版本的通达信都能兼容。我在实际项目里的习惯是把日线解析和板块解析封装成两个独立的模块parse_day_file负责把二进制文件变成Python对象parse_blk_file负责把板块文件变成结构化的股票列表。这样无论是做全市场扫描还是单股票分析代码都能直接复用。另外有一点始终要记住通达信本地文件只是行情数据的载体解析出来之后数据的准确性和完整性仍然需要你自己负责核对尤其是涉及历史回测和量化交易的时候多验证几次永远值得。
返回列表