大智慧Level-2本地数据解析工具:C#源码实现与字段还原
简介这是一套面向量化开发者与股票数据分析人员的本地数据解析工具针对大智慧新一代Level-2 V3.03.08.0801版本设计可提取沪市、深市、板块及港股多市场数据完整解析DAY.DAT、MIN.DAT、REPORT.DAT、STKINFO60.DAT等核心文件覆盖代码表、日线行情、分钟K线、逐笔成交、除权分红、股本结构、财务报表、十大股东、基金净值与持仓组合等17类数据表结构。资源包共55个文件约655KB以C#源码21个cs、资源文件5个resx、工程文件3个csproj为主另含说明文档、注册表脚本、安装部署模块与图标资源结构完整。内置FxjData2FinData转换程序可自动识别目标逻辑库、增量写入缺失数据并在文件被占用时复制备份再读取字段映射、类型、偏移量、字节序规则均有明确标注。配套FinData与Fxj2txt工程含FxjReader读取器、FxjConverter转换器、MainForm主界面及安装模块开箱即用。目前已有101人学习适合量化开发、本地数据库构建与离线分析场景。1. 拆开一份大智慧 Level-2 本地数据解析工具C# 源码能跑出什么行情软件收盘后会在本地落一堆二进制文件很多人第一反应是拿 Excel 或 Python 硬啃结果卡在结构对齐和字节序上。这份大智慧新一代 Level-2 本地数据解析工具核心就是把这些落地的逐笔、快照、委托队列文件按字段还原成可读结构配套 C# 源码和全字段说明。它解决的是「数据在硬盘上但读不出来」的问题适合做量化回测、盘口行为分析、以及需要把 Level-2 明细接进自有系统的开发者。C# 源码意味着你能直接嵌进上位机或桌面工具不用再为跨语言调用折腾一层壳。下面按「文件长什么样 → 怎么解析 → 坑在哪 → 怎么验」的顺序拆。2. 先搞清本地文件结构Level-2 落盘格式与字段对齐2.1 逐笔成交、快照、委托队列三类文件的差异大智慧本地 Level-2 数据不是单一文件常见的是按交易日切分的多个二进制块。逐笔成交记录的是每一笔撮合的价、量、买卖方向快照是每隔固定间隔对盘口的切片包含十档甚至更多档位的价量委托队列则记录单只股票在某一时刻的挂单明细。三类文件的结构体长度不同字段偏移也不同。如果拿逐笔成交的解析器去读快照最常见的现象是价格字段读出来是天文数字因为偏移错位把成交量当成了价格。我一般会先确认文件头。多数本地行情文件会在开头放一个固定长度的头包含记录数、记录长度、数据日期等元信息。头部的魔数或版本号能帮你判断这是哪一代格式。新一代格式相比老版本字段做了扩展比如增加了委托笔数、撤单标识等。解析前先读头再按记录长度循环是最稳的做法。2.2 字段偏移与字节序为什么直接按结构体读会翻车C# 里用BinaryReader顺序读是最直观的但前提是你知道每个字段的确切类型和顺序。问题在于行情文件里经常出现「保留字段」和「对齐填充」。比如一个记录声明长度是 64 字节但实际有效字段只占 52 字节剩下 12 字节是预留。如果你按有效字段连续读下一条记录就会错位。另一个高频翻车点是字节序。本地文件多数是小端但个别字段可能是网络字节序。判断方法很简单读一个已知范围的值比如价格正常应该在几元到几百元之间。如果读出来是几十亿先检查字节序再检查偏移。// 读取单条逐笔成交记录的核心逻辑 public struct TickRecord { public int Time; // 时间格式 HHMMSSmmm 压缩成 int public int Price; // 价格单位厘需除以 1000 public int Volume; // 成交量单位股 public byte Side; // 买卖方向0 买 1 卖 public byte Flag; // 撤单等标识 public short Reserved; // 保留字段必须跳过 } public TickRecord ReadTick(BinaryReader reader) { var tick new TickRecord(); tick.Time reader.ReadInt32(); // 偏移 04 字节 tick.Price reader.ReadInt32(); // 偏移 44 字节 tick.Volume reader.ReadInt32(); // 偏移 84 字节 tick.Side reader.ReadByte(); // 偏移 121 字节 tick.Flag reader.ReadByte(); // 偏移 131 字节 tick.Reserved reader.ReadInt16(); // 偏移 142 字节补齐到 16 return tick; }这段代码的关键在最后两行。Reserved字段如果不读下一条记录的起始位置就会偏 2 字节时间字段立刻变成乱码。参数上Price除以 1000 是常见做法但不同数据源单位可能不同务必用已知股票某天的收盘价去反推单位。Time字段的压缩方式也要验证有的格式是HHMMSS加毫秒单独存不能想当然。2.3 用全字段说明表做交叉验证拿到一份全字段说明不要急着写代码。先挑一个交易日、一只你熟悉的股票把说明表里的字段和实际字节做对照。下面这张表是我整理字段时的常用模板你可以照着填。字段名偏移类型单位/说明验证方法Time0int32HHMMSSmmm与分时图时间轴比对Price4int32厘除以 1000与当日成交均价比对Volume8int32股与逐笔成交量累加比对Side12byte0 买 1 卖与盘口买卖方向比对Flag13byte撤单标识与撤单记录比对Reserved14int16保留跳过不参与计算验证顺序建议从价格开始因为价格范围最容易判断对错。价格对了偏移基本就对了价格不对先查字节序再查偏移。成交量用来验证记录是否连续如果某条记录的成交量突然变成负数或极大值说明上一条记录长度读错了。3. 用 C# 把二进制流还原成可用结构解析器实现与参数调优3.1 文件读取策略一次性加载还是流式读取本地 Level-2 文件动辄几百 MB 到几个 GB一次性File.ReadAllBytes在 32 位进程里直接爆内存。我一般用FileStream配合BinaryReader流式读每次只保留当前记录。如果要做批量回测可以把解析结果写成内存映射文件或直接落成 CSV/Parquet避免重复解析。流式读取的另一个好处是能处理损坏文件。二进制文件偶尔会在中间出现截断流式读可以在异常处记录偏移跳过坏块继续读后面的记录。一次性加载则整个文件都读不进来。public IEnumerableTickRecord ParseTickFile(string path) { using var fs new FileStream(path, FileMode.Open, FileAccess.Read); using var reader new BinaryReader(fs); // 先读文件头确认记录数和记录长度 var magic reader.ReadInt32(); var recordCount reader.ReadInt32(); var recordSize reader.ReadInt16(); if (recordSize ! 16) throw new InvalidDataException($记录长度异常: {recordSize}); for (int i 0; i recordCount; i) { if (fs.Position recordSize fs.Length) yield break; // 文件截断提前结束 yield return ReadTick(reader); } }这里recordSize的校验很关键。如果文件头声明的记录长度和实际结构体不一致说明你拿到的字段说明和文件版本不匹配。yield return让调用方可以边解析边处理不用等整个文件读完。参数上recordCount有时是 0 或负数表示文件头损坏这时候可以按文件长度除以记录长度估算记录数。3.2 价格、时间、方向的还原逻辑价格字段读出来是整数还原成实际价格要除以单位。常见单位是 1000也就是「厘」。但有的数据源用 10000还原前先用一只股票的收盘价验证。时间字段如果是HHMMSSmmm压缩成 int拆解时要注意毫秒位。public static DateTime ParseTime(int rawTime, DateTime tradeDate) { int hh rawTime / 10000000; int mm (rawTime / 100000) % 100; int ss (rawTime / 1000) % 100; int ms rawTime % 1000; return new DateTime(tradeDate.Year, tradeDate.Month, tradeDate.Day, hh, mm, ss, ms); }方向字段通常 0 买 1 卖但有的格式用 1 买 2 卖甚至用 ASCII 字符。拿到数据后先统计一下方向字段的取值分布如果出现 0、1 之外的值说明字段定义和实际不符。撤单标识同理先看分布再决定怎么用。3.3 批量解析的性能参数缓冲区与并行度单文件解析用默认缓冲区通常够但批量处理几千个文件时FileStream的缓冲区大小会影响吞吐。我一般把缓冲区设成 64KB 到 1MB太小会增加系统调用次数太大对顺序读没明显收益。并行度方面解析是 IO 和 CPU 混合型任务并行度设成 CPU 核数的一半到核数之间比较稳。设太高会导致磁盘随机读增多反而变慢。var options new ParallelOptions { MaxDegreeOfParallelism Environment.ProcessorCount / 2 }; Parallel.ForEach(files, options, file { var ticks ParseTickFile(file).ToList(); // 后续处理 });如果解析结果要写数据库批量插入比逐条插入快一个数量级。但要注意并行解析加并行写库容易把磁盘打满我一般解析并行、写库串行中间用队列缓冲。4. 避坑与排查解析 Level-2 本地数据最容易翻车的五个点4.1 现象价格读出来是几十亿原因偏移错位或字节序反了这是最高频的问题。先确认文件头声明的记录长度和你的结构体是否一致。如果一致检查字节序。C# 的BinaryReader默认小端如果文件是大端所有多字节字段都要反转。反转方法可以用BinaryPrimitives.ReadInt32BigEndian。如果字节序没问题那就是偏移错了拿全字段说明逐字段核对重点看保留字段有没有跳过。4.2 现象解析到一半记录全乱原因文件中间有截断或坏块本地文件在写入过程中可能因为程序退出、磁盘满等原因截断。流式读取时如果某条记录读出来的字段明显超出合理范围记录当前偏移跳过一条记录长度继续读。如果连续多条都异常说明坏块较大可以按固定步长搜索下一个有效记录头。更稳妥的做法是解析前先校验文件长度是否能被记录长度整除不能整除就说明有截断。4.3 现象同一只股票不同文件解析结果对不上原因复权或单位不一致不同数据源、不同时间段的文件价格单位可能不同。有的用厘有的用分。另外如果文件里存的是不复权价格而你的参考数据是复权价对不上是正常的。排查时先确认单位再确认复权方式。我一般会在解析后先算当日均价和行情软件上的均价对比差一个数量级就是单位问题。4.4 现象委托队列解析出来全是零原因字段类型或长度理解错委托队列的字段比逐笔成交复杂常见的是每个价位存一个价格加多个委托量。如果按固定长度读但实际是变长结构就会读出大量零。这时候要看文件头有没有声明每个价位的委托笔数或者用全字段说明里的变长标记。变长结构一般会先读一个长度字段再按长度读内容。4.5 现象解析速度慢到无法接受原因逐条查数据库或频繁 GC解析本身很快慢通常慢在后续处理。如果在循环里逐条查数据库或逐条写文件速度会掉两个数量级。解决办法是批量处理解析结果先放内存列表攒够一批再写。另外频繁创建小对象会触发 GC结构体用struct而不是class能减少堆分配。如果必须用类考虑对象池。5. 验证解析结果用已知行情做交叉比对与增量校验解析完不是终点验证才是。我一般用三步验证法。第一步拿一只当天有龙虎榜或大宗交易的股票把解析出的逐笔成交按时间排序看大单出现的时间点和公开信息是否吻合。第二步把逐笔成交按价格加权算均价和行情软件的分时均价对比误差应该在千分之一以内。第三步把快照的十档盘口和逐笔成交做勾稽比如快照里的买一价应该在逐笔成交里能找到对应价位的成交。增量校验是另一个实用技巧。如果你每天解析一次可以把前一天解析结果的最后一条记录的时间和价格存下来第二天解析时先检查新文件的第一条记录是否衔接。如果时间跳跃过大或价格断层说明文件缺失或解析偏移错了。这个习惯帮我提前发现过好几次文件损坏。// 增量校验检查相邻两个文件的衔接 public bool ValidateContinuity(TickRecord lastPrev, TickRecord firstCurr) { var prevTime ParseTime(lastPrev.Time, DateTime.Today); var currTime ParseTime(firstCurr.Time, DateTime.Today); var gap (currTime - prevTime).TotalSeconds; // 正常间隔不应超过 5 秒超过说明中间有缺失 return gap 0 gap 5; }参数上5 秒是个经验值集合竞价阶段间隔可能更长可以按时间段调整。如果校验不通过先检查文件是否完整再检查解析偏移。从那以后我每次解析新文件前都强制走一遍增量校验确认衔接无误再入库。希望帮到你。本文还有配套的精品资源点击获取