news 2026/10/2 8:29:57

通达信日线.day文件二进制解析与SQLite入库实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
通达信日线.day文件二进制解析与SQLite入库实战

先把结论放前面:这篇文章要解决的问题,是很多做量化、做复盘、或者单纯想给自己留一份干净行情数据的朋友都会遇到的。通达信系的软件,包括申万宏源金融终端,会把日线行情以二进制文件存在本地,你可以在打开软件的情况下直接去读,但如果想用 Python、Pandas、SQLite 做进一步分析,就必须先把这些.day文件解析成结构化数据。这篇文章就以申万宏源金融终端默认数据目录C:/zd_swhy_gm/vipdoc/sh/lday下的sh600000.day文件为例,把从二进制解析到存入 SQLite3 的完整过程讲清楚,代码可以直接跑,路径改一改就能用于其他股票。

这篇文章适合两类人:一类是刚接触量化、想用本地行情数据做回测的初学者,另一类是已经在用行情软件但受不了每次手工导出 Excel 的操作型选手。读完你不仅能搞懂.day文件的二进制结构,还能拿到一套可复用的解析入库脚本,以后每天收盘后自动更新数据也不是难事。

1. 先搞懂申万宏源金融终端的本地数据目录

1.1 整条路径逐个拆开看

先看这个路径:C:/zd_swhy_gm/vipdoc/sh/lday/sh600000.day

我第一次看到这个目录的时候也懵了一下,zd_swhy_gm这串字符怎么看都不像券商名字。后来才明白,zd大概率是通达信内核早期版本留下的命名痕迹,swhy_gm才是申万宏源的拼音缩写。不同券商的通达信定制版,安装根目录名字长得都不一样,比如有的叫zd_cjzq,有的叫zd_zszq,但核心结构基本一致,vipdoc这个名字是通用的。

再说vipdoc,它是通达信系软件的数据仓库目录,日线、分钟线、分笔、财务数据都放在这下面。vipdoc里一般还能看到cw(财务数据)、fzline(分笔成交)、minline(分钟线)等子目录,这篇文章只关注lday,也就是日线数据。

sh表示上海市场,sz表示深圳市场,bj表示北交所。如果你把sh目录和sz目录分别打开对比,会发现一个规律:sh目录下是sh600000.day、sh601398.day这种命名,sz目录下则是sz000001.day、sz300750.day这种命名。文件名的前两位正是交易所缩写,中间是股票代码,后缀.day明确告诉你这是日线文件。

1.2 lday目录里到底有什么文件

打开lday目录后,你大概率会看到几百上千个文件,每个文件对应一只股票或者一个指数。比如sh600000.day是浦发银行的日线,sh000001.day是上证指数,sz399001.day是深证成指。这里有一个容易混淆的点:sh000001是上证指数,不是股票,但它同样以.day文件存在,而且二进制结构完全一样。

这些文件的大小通常不是整块的,因为每只股票上市时间不同、停牌天数不同,记录条数也就不一样。每条记录固定占 32 字节,所以文件大小等于记录条数乘以 32。如果文件大小除以 32 有余数,那就要小心了,要么文件没下载完整,要么这是某个定制版在文件头额外写了信息,后面我会专门说这个问题。

还有一个非常常见的误区:直接用记事本打开.day文件,看到的是乱码。别慌,这不是文件坏了,而是二进制存储本身就不可读。通达信为了追求读取效率和磁盘占用,把日期、价格、成交量全部编码成紧凑的二进制结构,人类肉眼直接读当然看不出所以然。下一节就来讲这个二进制结构到底长什么样。

2..day文件的32字节二进制结构解析

2.1 每条记录32字节的字段结构表

通达信日线.day文件的格式在业内其实已经是被反复验证过的公开结构:每一条记录固定 32 字节,按照小端序排列。各字段含义如下表所示:

偏移量长度类型字段名说明
04int日期格式如 20240102
44int开盘价实际价格乘以 100
84int最高价实际价格乘以 100
124int最低价实际价格乘以 100
164int收盘价实际价格乘以 100
204float成交额单位:元
244int成交量单位:股
284int保留字段通常为 0

用struct视角来看,这一条记录的格式可以写成<IIIIIfII。其中<表示小端序,I是 4 字节无符号整数,f是 4 字节单精度浮点数。16 字节的价格字段加上 4 字节成交额、4 字节成交量、4 字节保留字段,再加上 4 字节日期,正好 32 字节。

说句题外话,很多人在第一步就卡住,是因为不知道价格要除以 100。文件里存的不是10.50而是1050,这是通达信为了用整数运算提高效率故意的设计。如果你直接把1050当价格用,后面所有计算都会差两个数量级。

2.2 字节序、价格缩放与单位换算

为什么强调小端序?因为如果按大端序去解包,读出来的日期会变成完全不一样的天文数字。x86 和 ARM 处理器基本都是小端序,open('rb')读出来的字节序就是文件里的原始字节序,因此 Python 的struct解包时必须显式加上<,否则在某些平台上默认使用本机字节序,又可能出幺蛾子。

价格缩放很简单:整数除以 100 就是正常价格。但成交量和成交额的单位问题值得专门拿出来说。文件里的成交量字段单位是“股”,而通达信行情软件界面默认显示的是“手”,1 手等于 100 股。所以你在软件里看到123456手,文件里存的其实是12345600股。成交额字段单位是“元”,软件里经常显示成“万元”,换算就是除以 10000。

我做数据入库时一般建议保留原始单位:成交量存“股”,成交额存“元”。这样数据库里是最原始的数据,将来要 1 分钟线、分笔数据关联计算时不容易出错。如果你直接存成“手”,后面遇到指数、ETF 这种特殊品种时,单位口径不一致会让你算得很痛苦。

2.3 一个小实验:看字节怎么变成数字

为了加深理解,我们手工走一遍。假设文件中某条记录的起始 4 字节是E6 D6 34 01,小端序下它表达的数字是0x0134D6E6,也就是十进制20240102,对应日期 2024 年 1 月 2 日。再往下 4 字节如果是A0 8C 0E 00,小端序值是0x000E8CA0,十进制953504,除以 100 得到 9535.04,这就是某天的开盘价。

这种手工验算方法非常推荐大家试一次。它可以帮你确认两件事:一是你的字节序理解是否正确,二是文件里到底有没有额外的文件头。通常一个正常的.day文件,第一笔记录应该是该股票上市首日或者历史最早数据的日期,如果你手工解包第一笔记录发现日期是奇怪的数字,就要怀疑文件头偏移了。

3. 用Python解析sh600000.day实战

3.1 环境准备:最少依赖方案

解析这种固定长度二进制,Python 标准库的struct就够了,不需要装任何第三方库。但如果后面想把数据塞进 Pandas 或者做批量文件处理,建议还是装pandas和numpy,处理效率和代码量都会改善不少。

我的建议是分两层:第一层用纯标准库写一个最基础的解析函数,保证在任何机器上都能跑;第二层用 NumPy 的复合 dtype 读取,适合批量处理几千个文件时提升性能。两层都用同一套字段结构,维护成本也不高。

安装依赖就一条命令:

pip install numpy pandas

SQLite3 是 Python 内置模块,不需要额外安装。也就是说,你只要有一个 Python 3.6 以上的环境,就能完整跑通这篇文章里的全部代码。

3.2 基于struct的解析代码

下面这段代码是核心。它读取任意.day文件,返回一个包含全部记录的列表,每个元素是一个字典。为了让你看清结构,我刻意没有做 Pandas 转换:

import struct from pathlib import Path def parse_day_file(file_path): """ 解析通达信/申万宏源日线.day文件 返回: list[dict],每个dict包含date/open/high/low/close/amount/volume """ file_path = Path(file_path) record_size = 32 # 固定32字节一条记录 # 小端序: '<', 依次: 日期I, 开I, 高I, 低I, 收I, 成交额f, 成交量I, 保留I fmt = '<IIIIIfII' records = [] with open(file_path, 'rb') as f: data = f.read() # 校验文件大小是否整除32 if len(data) % record_size != 0: # 有些定制终端会在文件头加信息,这里先做个提示,具体处理见后面章节 print(f"警告: 文件大小 {len(data)} 不是32的整数倍,可能存在文件头") for i in range(len(data) // record_size): chunk = data[i * record_size:(i + 1) * record_size] date, open_price, high, low, close, amount, volume, reserved = struct.unpack(fmt, chunk) records.append({ 'date': date, 'open': open_price / 100, 'high': high / 100, 'low': low / 100, 'close': close / 100, 'amount': amount, 'volume': volume, 'reserved': reserved, }) return records if __name__ == '__main__': df_list = parse_day_file(r'C:/zd_swhy_gm/vipdoc/sh/lday/sh600000.day') print(f"共解析出 {len(df_list)} 条记录") for rec in df_list[:3]: print(rec)

运行之后你会看到类似这样的输出:

共解析出 4867 条记录 {'date': 20000104, 'open': 27.0, 'high': 27.6, 'low': 26.5, 'close': 26.9, 'amount': 64598688.0, 'volume': 2389144, 'reserved': 0} {'date': 20000105, 'open': 27.5, 'high': 27.86, 'low': 26.91, 'close': 27.4, 'amount': 32891672.0, 'volume': 1264500, 'reserved': 0} {'date': 20000106, 'open': 27.4, 'high': 27.75, 'low': 27.05, 'close': 27.75, 'amount': 36789520.0, 'volume': 1352600, 'reserved': 0}

看到20000104这样的日期格式,说明解析完全正确。这个输出只是示意,实际数据以你自己文件为准。

3.3 千万条数据的性能解法:NumPy复合dtype

如果你只是解析一只股票,上面struct循环完全够用。但如果你打算把整个lday目录下几千个文件全量导入数据库,纯 Python 循环加struct.unpack会慢得让你怀疑人生。这时候用 NumPy 的复合 dtype 一次性读取,性能会提升一两个数量级。

import numpy as np def parse_day_file_fast(file_path): """ 使用numpy复合dtype解析.day文件 返回: numpy.ndarray, 字段名 date/open/high/low/close/amount/volume """ dtype = np.dtype([ ('date', '<u4'), ('open', '<u4'), ('high', '<u4'), ('low', '<u4'), ('close', '<u4'), ('amount', '<f4'), ('volume', '<u4'), ('reserved', '<u4'), ]) arr = np.fromfile(file_path, dtype=dtype) # 价格除以100,转成正常价格 arr['open'] = arr['open'] / 100.0 arr['high'] = arr['high'] / 100.0 arr['low'] = arr['low'] / 100.0 arr['close'] = arr['close'] / 100.0 return arr

这段代码看起来简短,但每一行都值得解释。np.dtype里定义了每个字段的字节顺序、类型和名称,<u4表示小端无符号 4 字节整数,<f4表示小端 4 字节浮点数。np.fromfile会一次性把整个文件映射成结构化数组,速度极快。

需要注意一点:arr['open'] = arr['open'] / 100.0这一步在赋值时会把整数数组变成浮点数组,之后你再访问 open 字段就是浮点价格。这个逻辑在生成DataFrame之前做比较合适,因为 NumPy 在数组内部自动处理了 dtype 转换。

用parse_day_file_fast解析同一份sh600000.day,耗时通常只有struct版本的十分之一甚至更低。对于只是想认真管理本地数据的人,这个方案非常友好。

3.4 怎么验证解析结果是对的

数据解析出来之后,别急着入库,先做三件事确认正确性。

第一,看首条和末条记录的日期。启动申万宏源金融终端,按 F5 切到日 K 线,看它显示的最早日期和最晚日期,与脚本输出的第一行和最后一行对比。如果对得上,说明文件的边界没有读偏,日期字节序也正确。

第二,抽查某一天的价格和成交量。选一个最近的交易日,用软件里的十字光标对比,或者直接看软件右侧的盘口数据。这里要注意复权问题:软件默认可能显示的是前复权价格,而lday文件里是不复权的原始价格,两者在除权除息后会不一致,这是正常现象,不是解析错误。

第三,看成交量单位。我前面强调过文件里是“股”,软件里显示的是“手”。你在数据库里看到sh600000某天的 volume 是52000000,软件里显示成交量为520000手,两者是一致的。如果你发现差了 100 倍,别怀疑文件,检查一下是不是看串了单位。

4. 把解析结果写入SQLite3

4.1 表结构设计:别在第一天埋坑

SQLite3 是一个单文件数据库,特别适合这种“自己本地分析用”的场景。表结构的设计看似简单,但字段类型、主键、索引的选择会直接决定后续查询和增量更新的体验。

我的推荐建表语句如下:

CREATE TABLE IF NOT EXISTS day_data ( code TEXT NOT NULL, trade_date TEXT NOT NULL, open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL, amount REAL NOT NULL, volume INTEGER NOT NULL, PRIMARY KEY (code, trade_date) ) WITHOUT ROWID;

这里几个设计点说一下。

code字段我建议存储完整文件名前缀,比如sh600000,而不是只存600000。因为以后你可能把深市、沪市、北交所的数据都导入同一个表,如果只存代码,000001到底是平安银行还是上证指数会变得很难区分。

trade_date用TEXT类型,存储20240102这种 8 位字符串。长度为 8 的定长字符串在 SQLite 里比较和排序都非常快,比存成整数再格式化更直观。如果你需要按日期范围查询,BETWEEN '20240101' AND '20240131'这种写法也能充分利用 B 树索引。

主键用的是(code, trade_date)联合主键,两个作用。一是保证同一只股票同一天只能有一条记录,天然防重;二是配合INSERT OR REPLACE实现增量更新时,可以准确找到需要被覆盖的行。

WITHOUT ROWID是 SQLite 的优化选项,适用于主键明确的场景,可以减少存储开销并让主键查询更快。如果对 SQLite 不熟悉,可以先不写这一句,后续数据量大了再加也可以。

4.2 批量写入:executemany加手动事务

写入数据库时最忌讳的就是一条条INSERT,每条都自动提交事务,几千条数据可能卡到没脾气。正确做法是用executemany批量插入,并且手动控制事务提交时机。

下面是一个完整的入库代码,直接接在解析函数后面即可运行:

import sqlite3 def import_day_file_to_sqlite(db_path, code, file_path): """ 将单个.day文件解析并写入SQLite """ arr = parse_day_file_fast(file_path) conn = sqlite3.connect(db_path) # 建表 conn.execute(""" CREATE TABLE IF NOT EXISTS day_data ( code TEXT NOT NULL, trade_date TEXT NOT NULL, open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL, amount REAL NOT NULL, volume INTEGER NOT NULL, PRIMARY KEY (code, trade_date) ) WITHOUT ROWID """) # 组装参数列表 rows = [] for item in arr: # item['date']是np.uint32,需要转成int再格式化成字符串 trade_date = str(int(item['date'])) rows.append(( code, trade_date, float(item['open']), float(item['high']), float(item['low']), float(item['close']), float(item['amount']), int(item['volume']), )) # 手动事务,批量写入 conn.execute("BEGIN") conn.executemany(""" INSERT OR REPLACE INTO day_data (code, trade_date, open, high, low, close, amount, volume) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, rows) conn.commit() conn.close() print(f"{code} 导入完成,共 {len(rows)} 条") if __name__ == '__main__': import_day_file_to_sqlite( db_path=r'C:/stock_data/market.db', code='sh600000', file_path=r'C:/zd_swhy_gm/vipdoc/sh/lday/sh600000.day' )

关于INSERT OR REPLACE,我再多解释一句。它的语义是:如果联合主键(code, trade_date)已经存在,就把旧记录整行替换掉。这个特性非常适合每天收盘后增量更新:你每天跑一次脚本,如果当天数据已经在库里,就直接覆盖为新数据,不会产生重复记录,也不需要先DELETE再INSERT。

4.3 入库后如何查询和二次使用

数据入库只是开始,后面的查询才是真正发挥价值的地方。举几个我实际最常用的 SQL 例子。

查某只股票最近 10 个交易日:

SELECT code, trade_date, open, high, low, close, volume, amount FROM day_data WHERE code = 'sh600000' ORDER BY trade_date DESC LIMIT 10;

计算每日涨跌幅:

SELECT trade_date, close, close / LAG(close) OVER (ORDER BY trade_date) - 1 AS pct_chg FROM day_data WHERE code = 'sh600000' ORDER BY trade_date;

SQLite 从 3.25 版本开始支持窗口函数,LAG、SUM OVER这些都能用,做简单的技术指标计算足够用。如果后面要做更复杂的回测,直接把这张表读出到 Pandas,用pandas.to_sql也能无缝衔接。

5. 实战中容易踩的坑与增量更新思路

5.1 常见问题速查表

这部分是我自己反复踩过之后整理的,放成表格方便你排查。

症状可能原因解决办法
解析出来日期像乱码,数字巨大字节序错误确认struct格式串使用<开头
所有价格都差 100 倍忘记价格除以 100解析后统一除以 100
成交量比软件显示大 100 倍文件单位是“股”,软件显示“手”按需求除以 100
第一笔记录日期奇怪文件可能存在额外文件头检查文件大小是否整除 32,尝试跳过前 N 字节
价格和软件对不上软件显示的是前复权价确认lday是原始不复权数据
某只股票记录条数为 0停牌期间不产生记录属正常现象,交易日才有数据
文件名找不到股票代码前缀或目录选错确认代码所属交易所,sh/sz目录不同

5.2 复权数据是怎么回事

这是最容易让人误解的一个点。通达信系软件在行情界面默认显示的是前复权价格,也就是把历史分红、送股、配股都折算到当前股价口径下。而你从lday目录解析出来的.day文件,是交易所原始成交数据,不包含任何复权处理。

举例来说,某股票以前价格 20 元,后来 10 送 10,除权后股价变成 10 元。在软件前复权视角里,历史那天的价格会显示成 10 元附近;但在lday文件里,历史那天的价格依然记录的是 20 元。如果你拿数据库里的价格和软件界面对比,自然对不上。

这并不意味着解析错了。做量化回测时,到底用不复权还是复权数据,取决于你的策略逻辑。算收益率,一般用后复权或前复权更合理;做价格位置分析,也有人偏好原始价格。通达信的复权因子数据藏在另外的目录里,不在这次讨论范围内,如果你确实需要,可以先用行情软件自带的“数据导出”功能把复权日线导出来,或者等后续文章专门聊复权因子的解析。

5.3 增量更新:每天收盘后怎么只更新新增数据

全量导入一次之后,最自然的需求就是以后每天只更新当天新增的一条记录。原理很简单:每次入库前查一下该股票在数据库里的最大日期,然后只处理比它新的记录。

我给你一个精简版思路,核心就是一句 SQL:

def get_max_date_in_db(conn, code): cur = conn.execute( "SELECT MAX(trade_date) FROM day_data WHERE code = ?", (code,) ) row = cur.fetchone() if row and row[0]: return int(row[0]) return 0

拿到max_date之后,在解析出来的数组里用条件过滤,只保留date > max_date的记录,再走INSERT OR REPLACE入库即可。这一步可以在 Python 里做,也可以在 SQLite 里配合临时表做,但最简单的还是解析完在内存里过滤。

我建议每天收盘后定时跑一次这个脚本。至于定时任务怎么触发,Windows 上用“任务计划程序”,Linux 上用crontab,都不是今天重点,但思路是通用的。

5.4 批量导入多只股票

解析函数和入库函数都支持传入任意.day文件路径,所以批量导入就是遍历目录的事。

from pathlib import Path lday_dir = Path(r'C:/zd_swhy_gm/vipdoc/sh/lday') for f in lday_dir.glob('*.day'): code = f.stem # 文件名如 sh600000 import_day_file_to_sqlite( db_path=r'C:/stock_data/market.db', code=code, file_path=str(f) )

这里有一点要注意:lday目录里不仅有股票,还有指数,比如sh000001.day。如果你不希望指数混入股票表,可以在遍历时加个过滤规则,比如跳过sh000001到sh000999之间的代码,或者建一张instrument表专门维护“哪些代码是股票、哪些是指数”。这些业务规则没有统一标准,取决于你自己的使用场景。

还有一个小细节:不同券商的通达信定制版,lday目录下偶尔会出现重复的副本文件,比如带(1)后缀的备份文件。用glob('*.day')不会匹配到sh600000(1).day,但如果你用glob('*')再手工判断后缀,就要小心这种坑。

最后说点我自己的体会

这套解析入库的方案我从最早的struct循环一路演进到numpy复合 dtype,最直观的感受是:二进制解析本身并不难,难点在于你愿不愿意把边界情况处理清楚。比如文件头偏移、成交量单位、复权口径,每一个坑都会让你的数据在某个角落悄悄出错。所以我特别建议你在入库后加一道校验程序,每天随机抽取几只股票的收盘价和软件界面比对,真正稳定跑上一两周,这套数据管道才算靠谱。

另外,sh600000.day只是起点。一旦有了 SQLite 里的日线表,你后面可以做均线策略回测、做全市场选股扫描、甚至可以自己写一个小型行情分析网站。这些扩展都不需要再依赖任何收费接口,数据完全来自本地文件,干净、可控、免费。这也是我坚持折腾本地数据而不是直接买现成数据服务的原因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 8:29:47

面试官:说一说多线程常见锁的策略

一、为什么面试官总爱问“锁策略”多线程并发编程一直是 Java 后端面试的高频考点&#xff0c;而在并发编程中&#xff0c;“锁”又是绕不开的核心主题。很多同学能背出 synchronized、ReentrantLock、CAS、乐观锁、悲观锁这些名词&#xff0c;但一旦面试官追问“你为什么选择公…

作者头像 李华
网站建设 2026/10/2 8:27:53

Agent 开发实战:知识图谱、向量库与 Wiki 库的加载与结合

1. 引言 在 Agent 开发中&#xff0c;知识库的构建与加载是决定智能体回答质量的关键环节。单一知识源往往难以覆盖复杂场景&#xff1a;知识图谱擅长表达结构化关系&#xff0c;向量库擅长语义相似度检索&#xff0c;Wiki 库则擅长提供规范化的文档知识。这三类知识库本质上构…

作者头像 李华
网站建设 2026/10/2 8:27:48

自动管控档期|场地预约小程序怎么做2026搭建指南

中国信通院2026中小企业数字化调研显示&#xff0c;共享会议室、自习室、运动场馆这类空间商家&#xff0c;用上线上预约系统后&#xff0c;档期冲突问题可大幅减少。人工登记档期容易出现重复预定、信息遗漏&#xff0c;场地预约小程序核心价值就是系统自动管控档期。下面简单…

作者头像 李华
网站建设 2026/10/2 8:27:37

学业预警系统实战:特征工程、不平衡分类与随机森林预警模型

简介&#xff1a;一套完整的学业预警系统项目实践资料包&#xff0c;面向希望将人工智能与Python用于教育管理场景的学习者&#xff0c;解决如何从数据采集、数据处理、模型训练到预警推送构建可用系统的问题。压缩包共530个文件&#xff0c;约3.95MB&#xff0c;主要包含238个…

作者头像 李华
网站建设 2026/10/2 8:27:17

写给每一位想讲音疗的作者:请别把《内经》的诊断表当成处方表

随便打开一篇讲“五音养生”的文章&#xff0c;多半能看到这样一份配方&#xff1a;肝不好听角音&#xff0c;心不好听徵音&#xff0c;脾虚听宫音&#xff0c;肺燥听商音&#xff0c;肾虚听羽音。底下再配一张五行、五色、五味、五音的对照表&#xff0c;横平竖直&#xff0c;…

作者头像 李华