news 2026/10/13 22:59:08

同花顺日线文件解析:二进制K线提取与批量处理实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
同花顺日线文件解析:二进制K线提取与批量处理实战

简介:这份资源面向金融数据分析初学者与量化爱好者,聚焦同花顺日线二进制文件(.day)的解析问题。同花顺日线数据包含每日开盘价、收盘价、最高价、最低价、成交量与成交额等关键字段,但文件采用特殊二进制格式存储,直接读取并不直观。资源通过一个精简的Python脚本演示如何以二进制模式打开文件、读取头信息、按固定长度记录逐条解析数据记录,并将原始字节转换为可用的数值与日期对象,最终整理成便于后续分析的数据结构。压缩包共2个文件,包含1个day示例数据与1个py解析脚本,整体约4KB,体积轻巧,适合直接运行验证。目前已有2247人学习下载。读者可借此掌握二进制文件解析的基本思路与Python实现方法,理解字段偏移与数据转换技巧,为股票历史行情分析、回测数据准备等场景打下基础。

1. 同花顺日线文件解析:从一堆二进制里把K线抠出来

做量化回测的人迟早会撞上这件事:策略在模拟盘跑得挺顺,一换到本地历史数据就抓瞎。行情软件自带的导出功能要么字段残缺,要么只给最近几百根,想拿十年日线做长周期验证,只能去啃它本地的数据文件。同花顺日线文件解析就是干这个的——把安装目录下那批.day文件读成一张能直接喂给 pandas 的表。

它解决的不是"能不能看到K线"的问题,而是"能不能批量、可复现、带完整字段地拿到K线"的问题。适合两类人:一类是自己搭回测框架、需要干净历史数据的开发者;另一类是做数据校验,想拿本地文件和接口数据对账的从业者。难点在于这些文件没有公开格式文档,全是定长二进制记录,字段偏移错一位,整列价格就全乱。下面按我实际踩过的路子,从格式推断讲到批量落地。

2. 先搞懂 .day 文件的二进制布局:定长记录与字段偏移

2.1 为什么是定长二进制而不是文本

行情软件本地存日线,追求的是读写快、体积小。文本格式(CSV、JSON)每行都要解析分隔符,几万只股票乘以几千个交易日,开销很可观。定长二进制记录的好处是:第 N 条记录的起始位置可以直接算出来,offset = 32 * N,不需要顺序扫描。这也是为什么你打开.day文件用记事本看是一堆乱码——它本来就不是给人看的。

常见做法是每条记录固定 32 字节,字段按顺序紧密排列:日期、开、高、低、收、成交量、成交额,外加一个保留位。不同版本、不同市场(沪市/深市/北交所)在字段宽度上可能有细微差别,所以第一步永远是先确认记录长度,而不是直接套一个网上的偏移表。

判断记录长度有个笨但可靠的办法:拿文件总字节数除以你预估的交易日数量,看能不能整除。比如一个文件 320000 字节,你估计有 10000 个交易日,那 32 字节/条就对得上。如果除出来是 31.9 这种小数,说明字段宽度或者记录头尾还有额外字节。

2.2 用 Python 读出第一条记录并验证

先别急着批量,拿单文件、单记录把字段对齐了再说。下面这段代码读文件头 32 字节,按常见布局解包:

import struct # 常见 .day 记录布局:日期(int32) + 开高低收(int32*4) + 成交量(float32) + 成交额(float32) + 保留(int32) # 合计 4 + 16 + 4 + 4 + 4 = 32 字节 RECORD_SIZE = 32 FMT = "<i iiiif f i" # 小端:日期、开、高、低、收、量、额、保留 def read_first_record(path): with open(path, "rb") as f: raw = f.read(RECORD_SIZE) if len(raw) < RECORD_SIZE: raise ValueError("文件不足一条记录,可能不是日线文件") fields = struct.unpack(FMT, raw) date_int, o, h, l, c, vol, amt, _ = fields # 价格字段常见做法是放大 100 倍存整数,读出来要除回去 return { "date": date_int, "open": o / 100.0, "high": h / 100.0, "low": l / 100.0, "close": c / 100.0, "volume": vol, "amount": amt, } print(read_first_record("demo.day"))

逻辑说明:struct.unpack按格式串把字节流切成 Python 值,<表示小端序,这是 x86 平台本地文件的默认字节序。价格用整数存、读出来除以 100,是行情数据的通用压缩手法,能避免浮点存储的精度漂移。

参数说明:RECORD_SIZE必须和实际文件对齐,错了会整体错位;FMT里的字段顺序一旦和真实布局不符,解出来的开高低收会互相串位。验证方法很直接——把解出来的收盘价和软件界面上同一天的收盘价对一下,对不上就说明偏移或缩放系数有问题。

提示:日期字段常见是YYYYMMDD的整数形式,比如 20240115。如果解出来是 19700101 这种,多半是字节序反了,把<换成>再试。

2.3 日期字段的两种存法与换算

日期看着简单,其实是翻车高发区。常见两种存法:一种是直接存20240115这样的整数,肉眼可读;另一种是存"距某个基准日的天数",类似 Unix 时间戳但基准不同。前者直接转字符串就行,后者需要加一个基准日期。

判断方法:如果解出来的日期整数在 19000101 到 21001231 之间,基本就是YYYYMMDD;如果是个几万以内的小整数,那就是天数偏移。换算时用datetime.date(1990, 12, 19) + timedelta(days=n)这类方式,基准日要靠前几条记录和软件界面对照反推,不要凭记忆写。

from datetime import date, timedelta def to_date(date_int): if 19000101 <= date_int <= 21001231: s = str(date_int) return date(int(s[:4]), int(s[4:6]), int(s[6:8])) # 天数偏移存法,基准日需按实际文件反推 return date(1990, 12, 19) + timedelta(days=date_int)

这段的关键是那个基准日,它因文件来源而异,必须用已知交易日反推验证,不能照抄。

3. 批量解析与字段清洗:把单文件脚本变成能跑全市场的工具

3.1 遍历目录、按股票代码归档

单文件跑通后,下一步是批量。同花顺的日线文件一般按市场分目录,文件名就是股票代码。批量脚本要处理三件事:递归找文件、从路径或文件名提取代码、把结果汇总成一张带代码列的大表。

import os import pandas as pd def parse_one(path): rows = [] with open(path, "rb") as f: while True: raw = f.read(RECORD_SIZE) if len(raw) < RECORD_SIZE: break d, o, h, l, c, v, a, _ = struct.unpack(FMT, raw) rows.append((to_date(d), o/100, h/100, l/100, c/100, v, a)) df = pd.DataFrame(rows, columns=["date","open","high","low","close","volume","amount"]) df["code"] = os.path.splitext(os.path.basename(path))[0] return df def parse_dir(root): frames = [] for dirpath, _, files in os.walk(root): for name in files: if name.lower().endswith(".day"): frames.append(parse_one(os.path.join(dirpath, name))) return pd.concat(frames, ignore_index=True)

逻辑说明:while True配合read(RECORD_SIZE)逐条读,读到不足一条就停,这样能自然处理文件尾部可能的残缺字节。os.walk递归整个目录树,兼容按市场分层的结构。

参数说明:os.path.splitext取文件名主干作为代码,如果文件名带前缀(比如SH600000.day),需要额外切片处理。pd.concat汇总时ignore_index=True避免索引重复。

3.2 清洗:停牌、除权与异常值

原始数据不能直接用。停牌日可能没有记录,也可能有记录但成交量为 0;除权除息会让价格出现跳空,回测时如果不复权,收益率会算错。清洗要做的:剔除成交量为 0 的行、按日期排序、标记除权日。

def clean(df): df = df[df["volume"] > 0].copy() df = df.sort_values(["code", "date"]).reset_index(drop=True) # 相邻收盘价跳变超过阈值,标记为疑似除权 df["prev_close"] = df.groupby("code")["close"].shift(1) df["suspect_adj"] = (df["close"] / df["prev_close"] - 1).abs() > 0.11 return df

逻辑说明:volume > 0过滤停牌;groupby("code")保证每只股票内部排序,不会跨股票串行;跳变阈值 0.11 对应普通股 10% 涨跌停加一点余量,超过就怀疑是除权而非正常波动。

参数说明:阈值不能设太小,否则正常涨停会被误判;也不能太大,否则漏掉除权。ST 股涨跌幅限制不同,如果数据里混了 ST,阈值要单独调。

3.3 落盘格式选择:Parquet 还是 CSV

解析完的数据建议存 Parquet。CSV 体积大、读回来还要重新解析类型,Parquet 带 schema、压缩率高、pandas 直接读。几百万行的日线,Parquet 通常比 CSV 小一个数量级,读取快好几倍。

df.to_parquet("daily_all.parquet", index=False) # 读回 df2 = pd.read_parquet("daily_all.parquet")

如果后续要接数据库,也可以直接写 SQLite,但要注意日期字段的存储类型,别存成字符串导致范围查询失效。

4. 避坑与排查:解析日线文件最容易翻车的五个地方

4.1 现象:价格全是几百倍的整数,除完还是不对

原因:缩放系数不是固定的 100。有的文件价格放大 1000 倍,有的成交量是手数而非股数。解决:拿一条已知记录,用软件界面上的真实值反推系数,系数 = 原始整数 / 真实价格,算出来是多少就用多少,别默认 100。

4.2 现象:解出来的日期全是 1970 年

原因:字节序搞反了,或者把天数偏移当成了YYYYMMDD。解决:先打印原始整数值,看数量级。几万以内是偏移,八位数是YYYYMMDD。字节序用<和>各试一次,哪个解出来合理用哪个。

4.3 现象:批量跑完发现某些股票数据缺一大段

原因:文件本身不完整,或者遍历时漏了子目录。解决:解析前先统计每个文件的记录数,和该股票的上市天数粗略对比,差太多的单独查。遍历用os.walk而不是os.listdir,避免漏掉嵌套目录。

4.4 现象:回测收益率明显偏高,像是偷了未来

原因:没复权,除权日的价格跳空被当成了真实收益。解决:解析阶段只负责还原原始价格,复权在回测框架里做,或者单独拉一份复权因子表。别在解析脚本里硬编码复权逻辑,容易和后续处理冲突。

4.5 现象:内存爆掉,几百万行一次性 concat

原因:pd.concat把所有 DataFrame 堆在内存里。解决:分批写 Parquet,或者用pyarrow的流式写入。单机内存有限时,按市场或按年份分片处理,最后再合并。

5. 进阶:用内存映射加速大文件与增量更新

单文件解析慢,瓶颈往往在 Python 的逐条read。文件大、股票多的时候,可以用numpy.memmap把整个文件映射成数组,一次性 reshape 成(N, 8)的矩阵,再按列取字段,速度能快一个数量级。

import numpy as np def parse_fast(path): # 按 int32 视图读取,8 个字段对应 32 字节 data = np.fromfile(path, dtype=np.int32) n = len(data) // 8 data = data[:n * 8].reshape(n, 8) df = pd.DataFrame({ "date": data[:, 0], "open": data[:, 1] / 100.0, "high": data[:, 2] / 100.0, "low": data[:, 3] / 100.0, "close": data[:, 4] / 100.0, "volume": data[:, 5].view(np.float32), "amount": data[:, 6].view(np.float32), }) return df

逻辑说明:np.fromfile把整个文件读成一维 int32 数组,reshape(n, 8)还原成记录矩阵,按列切片比逐条 unpack 快得多。成交量和成交额如果是 float32 存的,需要用.view(np.float32)重新解释字节,不能直接当整数用。

参数说明:dtype=np.int32要求记录确实是 4 字节对齐;如果文件里有 float 字段,整体按 int32 读再 view 是可行的,但字段顺序必须和真实布局一致。n * 8截断保证 reshape 不报错,尾部残缺字节直接丢弃。

增量更新是另一个实用技巧:每天收盘后只解析当天新增的记录,而不是全量重跑。做法是记录上次解析到的文件偏移量,下次从该偏移继续读。这要求文件是追加写的,且记录定长——正好符合.day的特点。

def parse_incremental(path, last_offset): with open(path, "rb") as f: f.seek(last_offset) rows = [] while True: raw = f.read(RECORD_SIZE) if len(raw) < RECORD_SIZE: break rows.append(struct.unpack(FMT, raw)) new_offset = f.tell() return rows, new_offset

把last_offset存到一个小状态文件里,下次接着读。这样每天的处理量从几百万行降到几千行,回测数据维护从"每周全量重跑"变成"每天几秒钟"。

我自己维护这套解析脚本的习惯是:格式推断阶段一定留一个debug_first_record函数,任何时候字段对不上,先跑它打印原始字节和解析结果,比对着文档猜快得多。另外,解析和复权严格分开,解析层只保证"还原文件里存了什么",不掺任何业务逻辑,这样出问题时能快速定位是数据源的问题还是处理逻辑的问题。希望帮到你。

本文还有配套的精品资源,点击获取

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

多智能体强化学习电梯群控:目的楼层预约调度实战

简介&#xff1a;本资源是一套面向计算机、人工智能及自动化等相关专业学生的毕业设计与课程设计实践项目&#xff0c;聚焦于多智能体协同下的电梯群控调度优化问题&#xff0c;采用深度强化学习&#xff08;DQN、Sarsa等&#xff09;实现目的楼层预约式智能调度算法。资源包含…

作者头像 李华
网站建设 2026/10/13 22:55:55

MATLAB实现LU分解:从原理到列主元与PA=LU的完整指南

简介&#xff1a;这份资源聚焦数值计算中的经典算法——LU分解法&#xff0c;提供了一套基于MATLAB的完整实现&#xff0c;面向正在学习线性方程组求解、数值分析课程的学生&#xff0c;以及希望理解算法底层流程的C语言程序员。资源包共2个文件&#xff0c;包含1个m脚本与1个t…

作者头像 李华
网站建设 2026/10/13 22:55:16

最小绝对偏差(LAD):比最小二乘更稳健的回归方法

1. 什么是“最小绝对偏差”——一个被低估却异常实用的统计思想你可能在机器学习课上听过“最小二乘法”&#xff0c;知道它用平方误差来拟合直线&#xff1b;也可能在做回归分析时&#xff0c;发现模型对几个离群点特别敏感&#xff0c;预测结果一塌糊涂。这时候&#xff0c;有…

作者头像 李华
网站建设 2026/10/13 22:52:04

WebGL 3D 方向性光照:从点积、法线到逆转置矩阵的完整实战

图形学 【免费下载链接】webgl-fundamentals WebGL lessons that start with the basics 项目地址&#xff1a; https://gitcode.com/gh_mirrors/we/webgl-fundamentals 点击查看 免费下载 方向性光照是 3D 图形中最简单的灯光模型——假设光从某个固定方向平行射入&#xff0…

作者头像 李华
网站建设 2026/10/13 22:52:02

Pandas数据变形实战:宽表与长表的pivot/melt转换

1. 变形&#xff1a;Pandas里最被低估的一章我第一次认真对待"数据变形"这件事&#xff0c;是在处理一张几百列的业务流水表时。当时我需要的只是把其中十几个字段从行变成列、再从列拆回行&#xff0c;如果不会这些操作&#xff0c;就只能复制粘贴到Excel里手动折腾…

作者头像 李华