C#解析大智慧Level-2本地二进制数据:从文件到可查询行情表

发布时间:2026/10/17 17:44:42
C#解析大智慧Level-2本地二进制数据:从文件到可查询行情表 简介这是一套面向量化开发者与股票数据分析人员的本地数据解析工具针对大智慧新一代Level-2 V3.03.08.0801版本设计可提取沪市、深市、板块及港股多市场数据解决本地DAT与FDT文件难以直接读取、字段含义不明的痛点。资源包共55个文件约655KB以C#源码21个cs、WinForms界面资源resx、工程文件csproj/sln为主另含doc说明文档、reg注册脚本、msi安装包及xls字段表等覆盖从读取、转换到部署的完整链路。工具可解析DAY.DAT、MIN.DAT、MIN1.DAT、REPORT.DAT、STKINFO60.DAT、EXPROF.FDT、STKCAPITAL.FDT、STKFINANCE.FDT等核心文件涵盖代码表、日线、5分钟与1分钟K线、逐笔成交、除权分红、股本结构、财务报表、十大股东、基金净值及持仓组合等17类数据表。内置FxjData2FinData转换程序自动识别目标逻辑库、增量写入并在文件被占用时复制备份再读取。字段映射如dm/代码、rq/日期、kp/开盘、sl/成交量等均标注类型、偏移量与字节序。已有101人学习适合量化开发、本地数据库构建与离线分析。1. 大智慧新一代Level-2本地数据解析从二进制文件到可查询行情表行情软件每天收盘后会在本地落一堆文件很多人只会在界面里看K线却不知道这些文件里躺着逐笔成交、委托队列和千档盘口。大智慧新一代Level-2的本地数据就是典型它不是标准CSV也不是随手能打开的文本而是带压缩、带变长记录、带字段位标志的二进制块。我第一次拿到这批文件时用十六进制编辑器翻了半小时才找到记录头那种感觉像在拆一个没有说明书的黑匣子。这篇要讲的就是怎么用C#把这批本地数据解析出来落成能查询、能统计、能对接自己策略的表结构。适合两类人一类是做量化回测需要把Level-2逐笔和快照还原成事件流另一类是做上位机或数据中台要把行情文件接进自己的数据库。核心不是调某个接口而是理解文件布局、写对解析循环、把字段说明固化成结构体。下面按“先看清格式、再写解析器、最后避坑”的顺序推。2. 先搞清Level-2本地文件到底存了什么记录类型与字段布局2.1 逐笔成交、逐笔委托、快照三类记录的区分方式大智慧新一代Level-2本地数据通常按交易日分目录目录下再按市场和时间切片。每个切片文件不是单一记录流而是混合了多种记录类型。常见做法是文件头之后每条记录带一个固定长度的记录头记录头里有一个类型码类型码决定后面跟的是逐笔成交、逐笔委托还是快照。我一般先写一个只读文件头的探测程序把前若干字节的类型码分布打出来。如果类型码集中在两三个值上说明这个文件是混合流如果只有一个值说明是单类型文件。这一步不做后面解析循环很容易把委托记录当成成交记录读字段全错位。记录头里除了类型码通常还有记录长度和序号。记录长度这个字段非常关键因为Level-2记录是变长的逐笔成交可能带多个价位快照带千档盘口时长度会膨胀。解析循环必须用记录长度来推进文件指针而不是用固定步长。用固定步长是新手最常见的翻车点读几十条后指针就偏了。字段布局上逐笔成交一般包含证券代码、成交时间、成交价格、成交数量、买卖方向、成交编号。逐笔委托包含证券代码、委托时间、委托价格、委托数量、委托方向、委托编号。快照包含证券代码、时间、最新价、买卖各档价格与数量、总成交量和总成交额。不同版本字段顺序可能有差异所以字段说明不能靠猜要靠样本比对。2.2 用样本比对法确认字段偏移和字节序没有官方字段手册时最可靠的办法是样本比对。找一只当天有明显行情的股票比如开盘涨停或尾盘跳水把它的逐笔成交导出成文本和界面里看到的成交明细对照。价格字段通常是整数乘以一个价格精度因子比如实际价格12.34在文件里存成1234精度因子是100。数量字段有的是股数有的是手数也要用样本核对。字节序方面这类本地文件大多是小端序。C#在x86/x64平台上默认就是小端用BitConverter读取时一般不用翻转。但如果你把文件拿到其他架构上处理或者用某些流式读取库就要显式确认。我习惯在解析器里加一个字节序探测读一个已知为小整数的字段如果值大得离谱就说明字节序反了。下面这段代码演示怎么读记录头并判断类型不依赖任何第三方库// 读取记录头类型码(2字节) 记录长度(4字节) 序号(4字节) public static RecordHeader ReadHeader(BinaryReader reader) { var header new RecordHeader(); header.TypeCode reader.ReadUInt16(); // 记录类型 header.Length reader.ReadInt32(); // 整条记录长度含头 header.Sequence reader.ReadInt32(); // 序号用于校验丢包 return header; } // 解析主循环用记录长度推进而不是固定步长 public static void ParseFile(string path) { using var fs File.OpenRead(path); using var reader new BinaryReader(fs); // 跳过文件头具体偏移用样本确认 fs.Seek(FileHeaderSize, SeekOrigin.Begin); while (fs.Position 10 fs.Length) { var header ReadHeader(reader); if (header.Length 10 || fs.Position header.Length - 10 fs.Length) break; // 长度异常防止越界 var payload reader.ReadBytes(header.Length - 10); switch (header.TypeCode) { case 0x01: ParseTrade(payload); break; // 逐笔成交 case 0x02: ParseOrder(payload); break; // 逐笔委托 case 0x03: ParseSnapshot(payload); break;// 快照 default: break; // 未知类型直接跳过保证循环不崩 } } }逻辑说明ReadHeader按固定10字节读头ParseFile用header.Length推进。参数上FileHeaderSize需要你用样本确认常见是几十到几百字节。TypeCode的具体值不同版本可能不同所以switch里要留default分支。这段代码的关键是“长度驱动”只要长度字段可信解析就不会错位。2.3 把字段说明固化成C#结构体和精度因子表字段说明如果只写在文档里过两周就忘了。我的做法是直接固化成结构体和一张精度因子表。结构体用显式布局字段顺序和文件一致方便用Marshal直接映射。精度因子表按市场和品种区分比如股票价格精度100基金可能1000债券可能10000。[StructLayout(LayoutKind.Sequential, Pack 1)] public struct TradeRecord { public uint SecurityId; // 证券代码内部编码 public uint Time; // 时间HHmmssfff压缩 public int Price; // 价格需除以精度因子 public int Volume; // 成交量单位股 public byte Side; // 买卖方向 public uint TradeNo; // 成交编号 } // 精度因子表按市场品种查 public static readonly Dictionarystring, int PriceScale new() { { SH_STOCK, 100 }, { SZ_STOCK, 100 }, { SH_FUND, 1000 }, { SZ_FUND, 1000 }, };参数说明Pack1保证结构体无填充和文件布局一致。SecurityId是内部编码需要另一张映射表转成常见代码。Time字段是压缩时间解析时要拆成时、分、秒、毫秒。Price除以PriceScale后才是真实价格。这张表要按你实际拿到的数据版本调整不能照搬。3. 用C#写一个可复现的解析器从文件读取到落库3.1 工程结构与依赖选择只留必要库我一般建一个控制台工程加一个类库类库放解析逻辑控制台做批处理入口。依赖上尽量少文件读取用System.IO二进制用BinaryReader落库用Microsoft.Data.Sqlite或直接写CSV。不引入重型ORM因为Level-2数据量大逐条插入会被ORM拖死。工程结构建议Level2Parser.Core放RecordHeader、TradeRecord、OrderRecord、SnapshotRecord和ParseFile。Level2Parser.Cli命令行入口接收目录和输出路径。Level2Parser.Tests放样本文件的小型回归测试确保改代码后解析结果不变。如果要做上位机展示可以再加一个WPF或WinForms工程但解析核心不要和界面耦合。我见过把解析逻辑写在按钮事件里的代码后来想批处理就得复制一遍维护成本很高。3.2 逐笔成交解析时间、价格、方向的还原逐笔成交解析的难点在时间和方向。时间字段常见压缩格式是HHmmssfff比如93000123表示9点30分00秒123毫秒。方向字段有的用0/1表示买/卖有的用B/S字符还有的用主动买/主动卖编码。必须用样本核对不能凭感觉。public static TradeRecord ParseTrade(byte[] payload) { using var ms new MemoryStream(payload); using var reader new BinaryReader(ms); var record new TradeRecord(); record.SecurityId reader.ReadUInt32(); record.Time reader.ReadUInt32(); record.Price reader.ReadInt32(); record.Volume reader.ReadInt32(); record.Side reader.ReadByte(); record.TradeNo reader.ReadUInt32(); return record; } // 时间还原93000123 - 09:30:00.123 public static DateTime DecodeTime(uint compressed, DateTime tradeDate) { int ms (int)(compressed % 1000); int sec (int)(compressed / 1000 % 100); int min (int)(compressed / 100000 % 100); int hour (int)(compressed / 10000000 % 100); return new DateTime(tradeDate.Year, tradeDate.Month, tradeDate.Day, hour, min, sec, ms); }逻辑说明ParseTrade按字段顺序读DecodeTime做数学拆解。参数上tradeDate从文件名或目录名取。注意compressed的位数如果实际是HHmmssfff除以10000000取小时是对的如果版本不同要先打印几个样本确认。方向字段的还原要结合交易所规则。常见做法是主动买对应外盘主动卖对应内盘。但有的文件里方向是委托方向而不是成交方向这时候要结合逐笔委托一起判断。我一般先把方向原样存下来在落库时再加一列“推断方向”避免解析阶段就引入错误。3.3 快照解析千档盘口的变长数组处理快照是Level-2里最占空间的部分因为带千档盘口。变长数组的处理方式是先读档位数量再循环读每档的价格和数量。档位数量字段可能是2字节或4字节要用样本确认。public static SnapshotRecord ParseSnapshot(byte[] payload) { using var ms new MemoryStream(payload); using var reader new BinaryReader(ms); var snap new SnapshotRecord(); snap.SecurityId reader.ReadUInt32(); snap.Time reader.ReadUInt32(); snap.LastPrice reader.ReadInt32(); snap.TotalVolume reader.ReadInt64(); snap.TotalAmount reader.ReadInt64(); int bidCount reader.ReadUInt16(); // 买档数量 snap.Bids new PriceLevel[bidCount]; for (int i 0; i bidCount; i) { snap.Bids[i] new PriceLevel { Price reader.ReadInt32(), Volume reader.ReadInt32() }; } int askCount reader.ReadUInt16(); // 卖档数量 snap.Asks new PriceLevel[askCount]; for (int i 0; i askCount; i) { snap.Asks[i] new PriceLevel { Price reader.ReadInt32(), Volume reader.ReadInt32() }; } return snap; }逻辑说明先读固定字段再读档位数量最后按数量循环。参数上bidCount和askCount的类型要确认有的版本用4字节。如果读出来的数量特别大比如几万说明类型读错了要立刻停止并检查样本。千档盘口落库时不要一行一档那样表会爆炸。我的做法是快照主表存固定字段档位存成JSON或二进制列查询时再展开。如果要做盘口统计可以另建一张只存前10档的汇总表兼顾查询性能。3.4 批量落库SQLite表结构与索引设计落库用SQLite足够单机几千万条也能扛。表结构按记录类型分表不要混在一张表里。逐笔成交表按证券代码和时间建联合索引快照表按证券代码和时间建索引。CREATE TABLE trade ( id INTEGER PRIMARY KEY AUTOINCREMENT, security_id INTEGER NOT NULL, trade_time INTEGER NOT NULL, price INTEGER NOT NULL, volume INTEGER NOT NULL, side INTEGER NOT NULL, trade_no INTEGER NOT NULL ); CREATE INDEX idx_trade_sec_time ON trade(security_id, trade_time); CREATE TABLE snapshot ( id INTEGER PRIMARY KEY AUTOINCREMENT, security_id INTEGER NOT NULL, snap_time INTEGER NOT NULL, last_price INTEGER NOT NULL, total_volume INTEGER NOT NULL, total_amount INTEGER NOT NULL, bids_json TEXT, asks_json TEXT ); CREATE INDEX idx_snap_sec_time ON snapshot(security_id, snap_time);参数说明security_id用内部编码查询前先映射。trade_time存压缩时间或还原后的整数建议存还原后的HHmmssfff方便排序。bids_json和asks_json存档位数组如果数据量特别大可以改成BLOB。插入时用事务包一批比如每5000条提交一次比逐条插入快一个数量级。4. 解析Level-2本地数据最容易踩的五个坑4.1 坑一用固定步长读变长记录读几十条就错位现象解析到第几十条时价格突然变成天文数字或者证券代码对不上。原因Level-2记录是变长的快照带千档盘口时长度远大于逐笔成交用固定步长推进文件指针必然错位。解决严格用记录头里的长度字段推进并在每次读取前校验长度是否越界。如果长度字段本身不可信先用样本文件统计长度分布确认长度字段的位置和字节序。4.2 坑二价格和数量没除精度因子回测结果差几百倍现象回测时买入价格是1234而不是12.34导致盈亏计算完全失真。原因文件里价格是整数需要除以精度因子。解决建一张精度因子表按市场和品种查。落库前统一转换或者在查询时用视图转换。我习惯在解析层就转成真实价格存成REAL避免后续每个查询都要记得除。4.3 坑三时间字段直接当Unix时间戳用日期全错现象解析出来的时间是1970年或某个奇怪日期。原因Level-2时间字段是压缩的HHmmssfff不是Unix时间戳。解决用DecodeTime拆解日期从文件名或目录名取。如果文件跨日要在解析入口传入交易日。落库时存还原后的时间不要存压缩值否则查询时要写一堆数学。4.4 坑四忽略记录序号丢包了不知道现象回测时发现某只股票在某个时间段没有成交但实际行情里是有的。原因文件传输或落盘时可能丢记录记录头里的序号就是用来校验的。解决解析时检查序号是否连续发现跳号就记录日志并标记该时间段数据不完整。不要静默跳过否则回测结果不可信。4.5 坑五把逐笔委托当逐笔成交方向判断全反现象统计主动买卖时发现买卖比例和界面完全相反。原因逐笔委托和逐笔成交的记录类型码不同但字段布局有相似之处类型判断错了就会把委托当成交。解决在解析循环里严格按类型码分发未知类型码不要猜直接跳过并计数。用样本文件验证每种类型码的解析结果和界面明细对照。5. 进阶把解析结果接进回测与实时监控的实用技巧解析落库只是第一步真正有价值的是把数据用起来。我一般会做三件事一是把逐笔成交聚合成分钟K线和界面K线对照验证解析正确性二是把快照的前10档盘口做成买卖压力指标用于盘中监控三是把逐笔委托和逐笔成交按时间对齐推断订单流。聚合分钟K线时注意时间边界。Level-2时间是交易所时间聚合时用左闭右开区间避免同一笔成交被算进两个分钟。下面这段代码演示按分钟聚合// 按分钟聚合逐笔成交生成OHLC和成交量 public static ListBar AggregateToMinute(ListTradeRecord trades, int priceScale) { var bars new ListBar(); var groups trades.GroupBy(t t.Time / 100000); // 取HHmm foreach (var g in groups.OrderBy(x x.Key)) { var prices g.Select(t (double)t.Price / priceScale).ToList(); bars.Add(new Bar { Time g.Key, Open prices.First(), High prices.Max(), Low prices.Min(), Close prices.Last(), Volume g.Sum(t t.Volume) }); } return bars; }参数说明t.Time / 100000取到分钟如果时间格式不同要调整除数。priceScale用于还原价格。聚合后和界面K线对照如果OHLC有差异优先检查时间边界和精度因子。实时监控场景下不要每次查询都扫全表。我的习惯是解析时额外写一张“最新快照表”只保留每只股票的最新一条快照盘中监控直接查这张表。逐笔成交则写一张“当日成交表”按时间倒序查最近N条。这样查询延迟稳定不会随数据量增长而变慢。还有一个实用技巧把证券代码映射表单独维护。Level-2文件里是内部编码界面和策略里用常见代码。映射表可以解析时生成也可以从其他数据源导入。映射错了所有统计都会张冠李戴。我一般会在解析完成后跑一个校验随机抽若干只股票把解析出的成交明细和界面明细逐条比对确认代码、时间、价格、数量、方向全部一致。这个校验跑通后面用起来才放心。最后说个血泪经验不要等到数据攒了几百GB才想起建索引。我早期图省事先落库后建索引结果一次查询要等几分钟。后来改成边解析边建索引虽然写入慢一点但查询体验完全不一样。如果你也要做长期数据积累索引和分区要一开始就规划好。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询