简介这份资源面向金融数据分析初学者与量化爱好者聚焦同花顺日线二进制文件.day的解析问题。压缩包仅4KB内含2个文件一个以600000为代码的日线样本文件以及一份约20行的Python解析脚本用最小示例演示如何读取并还原每日开盘、收盘、最高、最低、成交量与成交额等字段。脚本以二进制模式打开文件按固定记录长度逐条提取字段并完成数值与日期转换最终整理为便于后续分析的数据结构可作为理解二进制文件格式与Python字节操作的入门范例。已有2247人学习下载适合希望快速掌握日线数据读取思路、并在此基础上扩展批量处理与错误处理的开发者参考。1. 同花顺日线文件解析从二进制乱码到可回测的 OHLCV 表如果你从某终端导出一份日线数据用文本编辑器打开看到的是一堆乱码或者用 pandas 直接读出来全是错位的列那你碰到的就是典型的同花顺日线文件解析问题。这类文件通常不是 CSV而是按固定字节长度排列的二进制或类二进制的私有格式字段顺序、日期编码、复权标记都藏在文件头或固定偏移里。它解决的核心诉求很具体把本地历史行情变成能直接喂给回测框架、能画 K 线、能做因子计算的干净表格。适合谁做量化回测的个人开发者、需要离线校验策略的研究者以及想把多年日线归档成自有数据库的工程师。难点不在读取而在字段对齐和日期还原一旦偏移错一位后面所有价格都是错的。2. 先搞清文件结构同花顺日线文件的字段布局与日期编码2.1 常见日线文件的两种组织方式我接触过的同花顺日线导出文件大体分两类。第一类是纯二进制定长记录每条记录固定 32 字节或 40 字节按顺序存放日期、开盘、最高、最低、收盘、成交量、成交额。第二类是带文本头的混合格式前若干字节是股票代码、市场标识、数据条数之后才是紧凑的二进制块。判断方法很简单用十六进制查看器打开看前 64 字节里有没有可读的 ASCII 字符串。如果有类似SH600000或20240101的片段说明是混合格式如果全是不可打印字节那就是纯二进制。常见做法是先用 Python 读前 256 字节打印repr()观察可打印字符的位置。这一步能省掉大量猜测时间。with open(daily.dat, rb) as f: head f.read(256) print(repr(head))逻辑说明repr()会把不可打印字节显示为\xNN可打印字符原样显示。参数上读 256 字节足够覆盖大多数文件头如果文件头更长可以加到 512。看到连续的可读字符串就记下它的起始偏移和长度后面解析时直接跳过。2.2 日期字段的三种编码与还原日期是最容易翻车的地方。同花顺日线文件里日期常见三种存法第一种是YYYYMMDD的整数比如20240115存为 4 字节小端整数第二种是自 1970-01-01 起的天数偏移类似 Unix 天数但基准可能不同第三种是压缩成年-月-日各占不同位宽的位域。第一种最好处理第二种需要确认基准日第三种要用位运算拆。我一般会先假设是YYYYMMDD整数解析几条后看数值是否落在合理区间19900101 到 20301231。如果数值很小比如 19000 左右那基本就是天数偏移基准日通常是 1970-01-01 或 1899-12-30。用 pandas 验证import pandas as pd raw 19737 # 假设读到的日期字段 print(pd.Timestamp(1970-01-01) pd.Timedelta(daysraw))逻辑说明Timedelta(daysraw)把天数偏移还原成日期。参数上如果结果明显偏早或偏晚换基准日再试。注意有些文件日期字段是 4 字节大端读的时候要用int.from_bytes(b, big)别默认小端。2.3 价格与成交量的存储单位价格字段通常不是浮点数而是整数单位可能是分或厘。比如收盘价 12.34 元存为 1234表示分。成交量可能是股数或手数成交额可能是元或千元。解析时如果不除以正确倍数回测出来的收益率会差两个数量级。判断方法取一条记录用收盘价乘以成交量和成交额字段对比看比例关系。如果成交额约等于收盘价乘成交量说明价格单位是元如果差 100 倍说明价格单位是分。close_raw 1234 volume_raw 567800 amount_raw 700000000 close close_raw / 100.0 volume volume_raw amount amount_raw print(close * volume, amount)逻辑说明先按分还原价格再算close * volume和amount比。如果两者接近说明单位假设正确如果差 100 倍调整价格或成交额的单位。参数上除数是 100 还是 1000取决于文件里价格保留几位小数常见是两位所以除 100。3. 用 Python 把日线文件读成 DataFrame完整解析脚本与参数调优3.1 定长记录解析的骨架代码假设我们已经确认文件是纯二进制每条记录 32 字节字段依次为日期 4 字节小端整数、开盘 4 字节小端整数、最高 4 字节、最低 4 字节、收盘 4 字节、成交量 4 字节、成交额 4 字节、保留 4 字节。下面是一个可复现的解析脚本。import struct import pandas as pd RECORD_SIZE 32 FMT IIIIIII4s # 小端7个无符号整数4字节保留 records [] with open(daily.dat, rb) as f: while True: chunk f.read(RECORD_SIZE) if len(chunk) RECORD_SIZE: break fields struct.unpack(FMT, chunk) date_int, open_raw, high_raw, low_raw, close_raw, vol, amount, _ fields records.append({ date: date_int, open: open_raw / 100.0, high: high_raw / 100.0, low: low_raw / 100.0, close: close_raw / 100.0, volume: vol, amount: amount }) df pd.DataFrame(records) df[date] pd.to_datetime(df[date], format%Y%m%d) df df.sort_values(date).reset_index(dropTrue) print(df.head())逻辑说明struct.unpack按格式字符串一次性拆出所有字段比逐字节读快很多。FMT里的表示小端I是 4 字节无符号整数4s是 4 字节保留字段。参数上如果日期不是YYYYMMDD整数pd.to_datetime那行要换成天数偏移的还原方式。RECORD_SIZE必须和实际文件一致否则会错位。3.2 处理文件头与数据条数校验混合格式的文件前 64 字节可能是文件头里面包含数据条数。解析前先读文件头拿到条数后用文件总大小减去头长度再除以记录长度看是否吻合。如果不吻合说明记录长度或头长度猜错了。import os file_size os.path.getsize(daily.dat) header_size 64 record_size 32 data_bytes file_size - header_size count data_bytes // record_size print(推算条数:, count, 余数:, data_bytes % record_size)逻辑说明余数不为 0 说明记录长度不对或者文件尾部有额外信息。参数上header_size和record_size需要根据十六进制观察调整。常见做法是先用几个候选值试直到余数为 0。3.3 复权标记与除权处理同花顺日线文件里复权信息可能单独存放也可能在文件头里有一个标志位。如果文件里只有不复权价格做回测前必须自己算复权。常见做法是读取除权除息数据用前复权或后复权公式调整。如果文件里已经包含复权价通常会有一个字段区分比如价格字段的高位或单独一列。解析时先检查是否有额外字段没有就按不复权处理后续用外部除权数据修正。# 假设 df 是不复权数据adj_factor 是复权因子序列 df[close_adj] df[close] * adj_factor逻辑说明复权因子通常来自外部数据源按日期对齐后相乘。参数上前复权用最新因子归一后复权用历史因子累乘。注意如果文件里价格已经是复权价再乘因子就重复了先确认文件说明或对比已知行情。4. 解析过程中最容易翻车的五个坑排查与修复4.1 日期字段读出来是 1970 年附近现象解析后日期集中在 1970 年或者全是 1900 年左右。原因把天数偏移当成了YYYYMMDD整数或者基准日选错。解决先打印原始日期整数值看数量级。如果小于 100000基本是天数偏移用pd.Timestamp(1970-01-01) pd.Timedelta(daysraw)还原如果结果偏早几十年换1899-12-30基准再试。4.2 价格出现 0 或异常大数现象开盘价或收盘价为 0或者出现 12345678 这种明显不合理的值。原因字段偏移错位把成交量当成了价格或者单位倍数搞错。解决用十六进制查看器定位每条记录的起始字节逐字段核对。常见做法是取一条已知日期的记录手动算一遍和行情软件对比。如果价格是 0检查是否读到了保留字段。4.3 记录条数和文件大小对不上现象解析出的条数比预期少很多或者最后一条不完整。原因文件头长度猜错或者记录长度不是整数。解决用os.path.getsize算总大小减去头长度后除以记录长度看余数。余数不为 0 就调整头长度或记录长度。注意有些文件尾部有校验码占几个字节要减掉。4.4 成交量单位混淆导致回测失真现象回测时换手率异常高或低成交额和成交量比例不对。原因成交量是手还是股成交额是元还是千元没统一。解决取一条记录用收盘价 * 成交量和成交额字段比。如果差 100 倍调整成交量单位手转股乘 100或成交额单位。参数上A 股常见成交量单位是手成交额单位是元。4.5 大端小端搞反导致所有字段错乱现象日期变成几亿价格变成负数或极大值。原因文件是小端存储读的时候用了大端或者反过来。解决用struct.unpack时格式字符串开头用表示小端表示大端。先试小端如果数值离谱换大端。常见做法是读一个已知日期字段看哪种端序能得到合理年份。5. 从解析到可用校验、增量更新与性能优化5.1 用已知行情做交叉校验解析完不能直接信得校验。我一般会取最近 5 个交易日和行情软件里的开高低收对比误差超过 0.01 元就说明单位或复权有问题。另外检查high max(open, close)和low min(open, close)不满足的记录标记出来。成交量不能为负日期不能重复。这些校验用 pandas 几行就能做完。assert (df[high] df[[open, close]].max(axis1)).all() assert (df[low] df[[open, close]].min(axis1)).all() assert df[date].is_unique逻辑说明三个断言分别检查最高价不低于开盘收盘的较大者、最低价不高于开盘收盘的较小者、日期唯一。参数上如果断言失败先看失败记录的数量和位置再回头查解析偏移。5.2 增量更新只解析新增记录日线文件每天追加全量解析浪费时间。常见做法是记录上次解析的最后日期然后从文件末尾往前找或者根据文件头里的条数只读新增部分。如果文件是定长记录可以直接用seek跳到偏移位置。last_date 20240115 record_size 32 header_size 64 with open(daily.dat, rb) as f: f.seek(header_size) while True: chunk f.read(record_size) if len(chunk) record_size: break date_int struct.unpack(I, chunk[:4])[0] if date_int last_date: # 解析这条记录 pass逻辑说明seek跳到数据区起始逐条读日期只处理大于上次日期的记录。参数上last_date存成整数和文件里的日期格式一致。注意如果文件不是按日期排序这个方法会漏数据先确认排序。5.3 用 numpy 内存映射加速大文件解析文件超过几百 MB 时逐条read会慢。可以用numpy.memmap把文件映射成数组然后按列切片。前提是记录长度固定且字段类型一致。import numpy as np data np.memmap(daily.dat, dtypenp.uint32, moder, offset64) data data.reshape(-1, 8) # 每条记录 8 个 uint32 dates data[:, 0] closes data[:, 4] / 100.0逻辑说明memmap把文件当数组读offset64跳过文件头reshape(-1, 8)按 8 列整理。参数上dtype要和实际字段匹配如果价格是 4 字节整数用uint32如果有浮点字段换float32。注意内存映射不复制数据修改会写回文件只读模式用moder。5.4 解析结果的存储选型Parquet 还是 SQLite解析完的 DataFrame 存哪里取决于后续用途。做回测Parquet 列式存储读取快压缩比高做频繁按日期查询SQLite 更方便。我一般先存 Parquet需要 SQL 查询时再导入 SQLite。Parquet 用df.to_parquet(daily.parquet)SQLite 用df.to_sql(daily, conn)。参数上Parquet 的compressionsnappy兼顾速度和体积SQLite 建索引在date列上。6. 一个老手习惯先写校验再写解析我刚开始做日线解析时习惯先写解析代码跑通了再补校验。结果有一次字段偏移错了一位价格全错回测跑了一周才发现。后来改成先写校验函数再写解析解析完立刻跑校验不通过就停。这个习惯救了我很多次。具体做法是解析脚本里先定义validate(df)检查日期范围、价格关系、成交量非负、日期唯一然后才写parse()。校验不通过时打印前 10 条原始字节和解析结果对照十六进制找偏移。另一个习惯是保留原始文件的一份只读副本解析出错时随时回查。希望帮到你。本文还有配套的精品资源点击获取