C#解析大智慧Level-2本地数据:从二进制到结构化行情

发布时间:2026/10/17 20:43:56
C#解析大智慧Level-2本地数据:从二进制到结构化行情
简介这是一套面向量化开发者与股票数据分析爱好者的本地数据解析工具针对大智慧新一代Level-2 V3.03.08.0801版本设计可提取沪市、深市、板块及港股多市场数据解决本地DAT文件难以直接读取与转换的问题。资源包共55个文件约655KB以C#源码21个cs文件为核心辅以csproj工程、resx资源、doc说明文档、reg注册脚本及msi安装包等涵盖FxjReader数据读取器、FxjConverter转换器、MainForm主界面与安装部署模块开箱即用。工具可完整解析DAY.DAT、MIN.DAT、REPORT.DAT、STKINFO60.DAT等核心文件覆盖代码表、日线、分钟K线、逐笔成交、除权分红、股本结构、财务报表、十大股东、基金净值及持仓组合等17类数据表并内置FxjData2FinData转换程序支持增量写入与文件占用时自动备份读取。字段映射说明明确标注dm、rq、kp、sl等字段的类型、偏移量与字节序规则。目前已有101人学习适合量化开发、本地数据库构建与离线分析场景。1. 大智慧新一代Level-2本地数据解析从二进制文件到可用行情表如果你手里有一份大智慧新一代Level-2的本地数据目录却只能靠软件界面看盘那这些数据基本等于锁在黑匣子里。Level-2 相比传统快照行情多了逐笔委托、逐笔成交、委托队列和千档盘口数据量和字段复杂度都上了一个台阶。很多做量化回测、盘口行为分析、甚至只是想把历史逐笔落成数据库的开发者第一步就卡在“文件读不出来”上。这篇笔记讲的就是用 C# 把大智慧新一代 Level-2 的本地数据解析成结构化记录包含源码思路和全字段说明。适合有 C# 基础、想自己掌控行情数据链路的从业者也适合做上位机或数据中台的同行参考。核心不是调某个接口而是把文件格式、字段偏移和解析流程讲透让你能自己复现。2. 先搞清楚Level-2本地数据长什么样文件类型与字段布局2.1 大智慧新一代Level-2落地的几种文件形态大智慧新一代的 Level-2 数据在本地通常不是单一文件而是按日期和品种分目录存放。常见形态包括按市场代码分的日目录、按证券代码分的子目录以及目录下的逐笔成交文件、逐笔委托文件、快照文件。不同版本在命名上会有差异但结构逻辑一致时间、市场、代码、类型四要素决定文件归属。我一般先做一件事把数据目录完整列一遍统计文件扩展名和大小分布。这一步能快速判断你拿到的是压缩包解压后的原始文件还是已经被软件整理过的缓存。原始文件通常是定长记录或带长度前缀的变长记录缓存文件可能混有索引。下面这段代码用来扫描目录并输出文件类型统计帮你建立第一手认知。using System; using System.IO; using System.Linq; using System.Collections.Generic; class Level2FileScanner { static void Main(string[] args) { string root D:\DZH_Level2\Data; // 替换为你的数据根目录 if (!Directory.Exists(root)) { Console.WriteLine(目录不存在请检查路径); return; } // 按扩展名分组统计文件数量和总大小 var stats Directory.EnumerateFiles(root, *.*, SearchOption.AllDirectories) .GroupBy(f Path.GetExtension(f).ToLower()) .Select(g new { Ext g.Key, Count g.Count(), TotalMB g.Sum(f new FileInfo(f).Length) / 1024.0 / 1024.0 }) .OrderByDescending(x x.TotalMB); foreach (var s in stats) { Console.WriteLine($扩展名: {s.Ext,-10} 文件数: {s.Count,-8} 总大小: {s.TotalMB:F2} MB); } } }这段代码的逻辑很直接递归枚举所有文件按扩展名聚合。参数上root指向你的数据根目录SearchOption.AllDirectories保证子目录也被扫描。输出结果里如果看到大量.dat或.tmp说明是原始落盘文件如果看到.idx或.db说明有索引或数据库缓存。注意扫描大目录时可能耗时较长建议先在小范围目录上跑通再全量执行。2.2 逐笔成交与逐笔委托的字段偏移规律Level-2 逐笔数据的核心字段包括证券代码、交易所、成交时间精确到毫秒或微秒、成交价格、成交数量、买卖方向、买方委托序号、卖方委托序号。逐笔委托则多了委托类型、委托价格、委托数量、原始订单号。这些字段在二进制文件里通常按固定偏移排列但不同数据版本会有微调。常见做法是先用十六进制查看器打开一个文件找到时间戳和价格的可读模式反推字段宽度。比如时间字段可能是 4 字节的 Unix 时间戳价格可能是 4 字节整数除以 10000。我一般会写一个小的探测脚本把文件前几百字节按不同偏移打印出来人工比对。using System; using System.IO; using System.Text; class HexProbe { static void Main() { string file D:\DZH_Level2\Data\SH\600000\20240102\trade.dat; byte[] buffer new byte[256]; using (var fs new FileStream(file, FileMode.Open, FileAccess.Read)) { int read fs.Read(buffer, 0, buffer.Length); Console.WriteLine($读取 {read} 字节); for (int i 0; i read; i 16) { // 打印偏移和十六进制 Console.Write(${i:X4}: ); for (int j 0; j 16 i j read; j) Console.Write(${buffer[i j]:X2} ); // 打印可读字符 Console.Write( ); for (int j 0; j 16 i j read; j) { byte b buffer[i j]; Console.Write(b 32 b 127 ? (char)b : .); } Console.WriteLine(); } } } }这段代码把文件前 256 字节按 16 字节一行打印左侧是偏移中间是十六进制右侧是可读字符。参数file指向一个具体的逐笔成交文件。通过观察可读字符你能快速定位到证券代码的 ASCII 片段然后围绕它推断前后字段。注意不要一上来就全文件读取先用小缓冲区探测避免内存浪费。2.3 用C#定义强类型结构体映射二进制记录一旦字段偏移确定下一步就是用 C# 结构体或类来映射每条记录。推荐用StructLayout特性配合Marshal.PtrToStructure或者直接用BinaryReader按字段顺序读取。前者适合定长记录后者适合变长或需要灵活跳过的场景。下面是一个逐笔成交记录的强类型定义和读取示例。假设每条记录固定 32 字节字段依次为时间戳4字节、价格4字节整数实际价格需除以10000、成交量4字节、买卖方向1字节、买方委托号4字节、卖方委托号4字节、保留11字节。using System; using System.IO; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential, Pack 1)] public struct TickTrade { public uint TimeStamp; // Unix 时间戳秒或毫秒 public int PriceRaw; // 原始价格除以 10000 得实际价格 public int Volume; // 成交量 public byte Side; // 1买2卖 public uint BuyOrderId; // 买方委托序号 public uint SellOrderId; // 卖方委托序号 [MarshalAs(UnmanagedType.ByValArray, SizeConst 11)] public byte[] Reserved; // 保留字段 } class TickReader { static void Main() { string file D:\DZH_Level2\Data\SH\600000\20240102\trade.dat; int recordSize Marshal.SizeOfTickTrade(); byte[] buffer new byte[recordSize]; using (var fs new FileStream(file, FileMode.Open, FileAccess.Read)) { while (fs.Read(buffer, 0, recordSize) recordSize) { GCHandle handle GCHandle.Alloc(buffer, GCHandleType.Pinned); TickTrade tick Marshal.PtrToStructureTickTrade(handle.AddrOfPinnedObject()); handle.Free(); double price tick.PriceRaw / 10000.0; Console.WriteLine($时间:{tick.TimeStamp} 价格:{price:F4} 量:{tick.Volume} 方向:{tick.Side}); } } } }这里的关键点Pack 1保证结构体按字节对齐避免编译器插入填充字节。Marshal.SizeOf得到记录大小用于循环读取。GCHandle固定托管数组以便PtrToStructure转换。参数上PriceRaw除以 10000 是常见做法但不同数据源可能用 1000 或 100需要根据实际价格范围验证。注意如果文件有文件头或记录间有分隔符需要先跳过或按实际偏移读取。3. 解析流程落地从文件读取到结构化入库3.1 按市场与日期组织解析任务实际数据目录往往包含多个市场、多个日期、多个品种。直接全量遍历容易乱我一般先构建一个任务列表每个任务对应一个文件包含市场、代码、日期、文件类型。然后按任务并行或串行解析。下面代码展示如何生成任务列表并过滤出逐笔成交文件。using System; using System.Collections.Generic; using System.IO; using System.Linq; class TaskBuilder { public class ParseTask { public string Market { get; set; } public string Code { get; set; } public string Date { get; set; } public string FileType { get; set; } public string FullPath { get; set; } } static void Main() { string root D:\DZH_Level2\Data; var tasks new ListParseTask(); // 假设目录结构root/市场/代码/日期/文件名 foreach (var marketDir in Directory.GetDirectories(root)) { string market Path.GetFileName(marketDir); foreach (var codeDir in Directory.GetDirectories(marketDir)) { string code Path.GetFileName(codeDir); foreach (var dateDir in Directory.GetDirectories(codeDir)) { string date Path.GetFileName(dateDir); foreach (var file in Directory.GetFiles(dateDir)) { string fileName Path.GetFileNameWithoutExtension(file).ToLower(); string fileType fileName.Contains(trade) ? trade : fileName.Contains(order) ? order : snapshot; tasks.Add(new ParseTask { Market market, Code code, Date date, FileType fileType, FullPath file }); } } } } Console.WriteLine($共生成 {tasks.Count} 个解析任务); foreach (var t in tasks.Take(5)) Console.WriteLine(${t.Market}/{t.Code}/{t.Date} - {t.FileType}); } }这段代码按四级目录结构生成任务market、code、date从路径中提取fileType根据文件名关键字判断。参数上root是数据根目录Take(5)只预览前五个任务。注意如果目录层级不同需要调整循环嵌套。生成任务列表后可以按文件类型分别处理逐笔成交和逐笔委托的解析逻辑不同。3.2 逐笔成交解析的完整实现与字段校验有了任务列表逐笔成交解析可以独立成一个方法。核心步骤打开文件、循环读取固定长度记录、转换为结构体、校验字段合理性、输出或入库。校验包括价格是否在合理范围、成交量是否为正、时间戳是否在交易时段内。using System; using System.IO; using System.Runtime.InteropServices; public class TradeParser { [StructLayout(LayoutKind.Sequential, Pack 1)] public struct TickTrade { public uint TimeStamp; public int PriceRaw; public int Volume; public byte Side; public uint BuyOrderId; public uint SellOrderId; [MarshalAs(UnmanagedType.ByValArray, SizeConst 11)] public byte[] Reserved; } public static void Parse(string filePath) { int recordSize Marshal.SizeOfTickTrade(); byte[] buffer new byte[recordSize]; int lineCount 0; using (var fs new FileStream(filePath, FileMode.Open, FileAccess.Read)) { while (fs.Read(buffer, 0, recordSize) recordSize) { GCHandle handle GCHandle.Alloc(buffer, GCHandleType.Pinned); TickTrade tick Marshal.PtrToStructureTickTrade(handle.AddrOfPinnedObject()); handle.Free(); double price tick.PriceRaw / 10000.0; // 简单校验价格大于0成交量大于0 if (price 0 || tick.Volume 0) continue; // 这里可以写入数据库或输出到控制台 if (lineCount 10) // 只打印前10条作为示例 { Console.WriteLine($时间:{tick.TimeStamp} 价格:{price:F4} 量:{tick.Volume} 方向:{tick.Side} 买序:{tick.BuyOrderId} 卖序:{tick.SellOrderId}); } lineCount; } } Console.WriteLine($共解析 {lineCount} 条有效记录); } }逻辑说明循环读取固定大小缓冲区用GCHandle固定后转换为结构体。校验部分过滤掉价格或成交量为非正数的异常记录。参数上PriceRaw除以 10000 是假设实际需要根据品种调整。注意如果文件末尾有不足一条记录的数据Read返回值会小于recordSize循环自然结束。输出部分只打印前 10 条避免刷屏。3.3 逐笔委托与快照的差异化处理逐笔委托比逐笔成交多了委托类型和原始订单号字段长度可能不同。快照数据则是按时间间隔切片包含买卖各十档或千档的价位和量。处理思路类似但结构体定义和校验规则不同。下面给出逐笔委托的结构体示例假设记录 40 字节。[StructLayout(LayoutKind.Sequential, Pack 1)] public struct TickOrder { public uint TimeStamp; public int PriceRaw; public int Volume; public byte Side; // 1买2卖 public byte OrderType; // 1市价2限价3本方最优 public uint OrderId; // 原始订单号 [MarshalAs(UnmanagedType.ByValArray, SizeConst 22)] public byte[] Reserved; }对于快照通常包含多个档位每个档位有价格和量。可以定义嵌套结构体或数组。解析时注意档位数量是固定的循环读取即可。参数上快照的时间间隔可能是 3 秒或 1 秒取决于数据版本。校验时关注买卖档位价格是否交叉、量是否非负。4. 避坑与排查Level-2解析中容易翻车的几个点4.1 字节序与对齐问题导致价格变成天文数字现象解析出来的价格是 123456789 这种明显不合理的值。原因二进制文件可能使用大端序而 C# 默认按小端序读取或者结构体对齐方式与文件不一致。解决用BitConverter手动转换字节序或者调整StructLayout的Pack值。我一般先用十六进制查看器确认价格字段的字节顺序再决定是否反转。4.2 文件头或分隔符未跳过导致记录错位现象解析几条后数据开始混乱时间戳和价格完全对不上。原因文件开头有文件头如版本号、记录数或者记录之间有分隔符而代码直接从 0 偏移开始读。解决先用探测脚本找到第一条有效记录的起始偏移然后在FileStream上调用Seek跳过。常见文件头长度是 16、32 或 64 字节需要实际验证。4.3 时间戳精度混淆导致回测时间轴错乱现象逐笔成交的时间戳看起来是秒级但实际数据是毫秒级导致回测时同一秒内大量成交被错误排序。原因时间戳字段可能是毫秒或微秒而代码按秒处理。解决观察时间戳数值范围如果超过 1e12 可能是微秒超过 1e9 可能是毫秒。根据交易所实际精度调整。我一般会取几条记录用在线时间戳转换工具核对。4.4 不同市场或品种的记录长度不一致现象解析上海市场的文件正常解析深圳市场的文件就报错或数据错位。原因不同市场的 Level-2 数据格式可能有差异记录长度或字段顺序不同。解决为每个市场单独定义结构体或者在解析前先读取文件头中的记录长度字段。不要假设所有市场共用一套格式。4.5 大文件一次性读取导致内存溢出现象解析几个 GB 的逐笔文件时程序崩溃或卡死。原因使用了File.ReadAllBytes一次性加载整个文件。解决改用FileStream配合固定大小缓冲区循环读取每次只处理一条记录。内存占用与记录大小成正比而不是文件大小。这是最基本但最容易忽略的一点。5. 进阶技巧把解析结果落到数据库并做快速验证5.1 用SQLite批量插入提升入库速度解析出来的记录如果逐条插入数据库速度会非常慢。我一般用事务加批量插入或者先写入 CSV 再用数据库工具导入。下面是一个用 SQLite 批量插入的示例每 1000 条提交一次事务。using System; using System.Data.SQLite; using System.IO; using System.Runtime.InteropServices; public class DbInserter { [StructLayout(LayoutKind.Sequential, Pack 1)] public struct TickTrade { public uint TimeStamp; public int PriceRaw; public int Volume; public byte Side; public uint BuyOrderId; public uint SellOrderId; [MarshalAs(UnmanagedType.ByValArray, SizeConst 11)] public byte[] Reserved; } public static void ParseAndInsert(string filePath, string dbPath) { using (var conn new SQLiteConnection($Data Source{dbPath};Version3;)) { conn.Open(); using (var transaction conn.BeginTransaction()) { using (var cmd new SQLiteCommand(conn)) { cmd.CommandText INSERT INTO trades (ts, price, volume, side, buy_id, sell_id) VALUES (ts, price, vol, side, bid, sid); cmd.Parameters.Add(new SQLiteParameter(ts)); cmd.Parameters.Add(new SQLiteParameter(price)); cmd.Parameters.Add(new SQLiteParameter(vol)); cmd.Parameters.Add(new SQLiteParameter(side)); cmd.Parameters.Add(new SQLiteParameter(bid)); cmd.Parameters.Add(new SQLiteParameter(sid)); int recordSize Marshal.SizeOfTickTrade(); byte[] buffer new byte[recordSize]; int count 0; using (var fs new FileStream(filePath, FileMode.Open, FileAccess.Read)) { while (fs.Read(buffer, 0, recordSize) recordSize) { GCHandle handle GCHandle.Alloc(buffer, GCHandleType.Pinned); TickTrade tick Marshal.PtrToStructureTickTrade(handle.AddrOfPinnedObject()); handle.Free(); cmd.Parameters[ts].Value tick.TimeStamp; cmd.Parameters[price].Value tick.PriceRaw / 10000.0; cmd.Parameters[vol].Value tick.Volume; cmd.Parameters[side].Value tick.Side; cmd.Parameters[bid].Value tick.BuyOrderId; cmd.Parameters[sid].Value tick.SellOrderId; cmd.ExecuteNonQuery(); count; if (count % 1000 0) { transaction.Commit(); transaction conn.BeginTransaction(); } } } transaction.Commit(); } } } } }这段代码在循环内执行插入每 1000 条提交一次事务。参数上dbPath是 SQLite 数据库路径表结构需要提前建好。注意transaction在提交后需要重新开始否则后续插入会报错。批量提交能显著减少磁盘 I/O实测比逐条提交快一个数量级。5.2 用聚合查询验证解析完整性入库后用几条 SQL 快速验证数据是否完整。比如统计每个交易日的记录数、检查价格范围、查看买卖方向分布。下面是一个验证查询示例。-- 统计每日记录数和价格范围 SELECT date(ts, unixepoch) AS trade_date, COUNT(*) AS total, MIN(price) AS min_price, MAX(price) AS max_price FROM trades GROUP BY trade_date ORDER BY trade_date; -- 检查买卖方向分布 SELECT side, COUNT(*) AS cnt FROM trades GROUP BY side;第一条查询按日期分组统计记录数和价格极值。如果某天记录数明显偏少可能是文件缺失或解析中断。第二条查询检查买卖方向正常情况买卖数量应该接近。如果严重失衡可能是方向字段解析错误。这些查询能快速定位问题不用逐条翻数据。5.3 一个我常用的习惯先跑通单文件再全量每次拿到新数据我不会直接全量解析。先挑一个品种、一个日期的单文件跑通解析、入库、查询全流程确认字段和校验逻辑无误再扩展到全量。这个习惯帮我省了很多后悔药。Level-2 数据格式在不同版本间有差异单文件验证是最快的试错方式。希望帮到你。本文还有配套的精品资源点击获取