简介:这是一套面向量化开发者与股票数据分析人员的本地数据解析工具,针对大智慧新一代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 Dictionary<string, int> PriceScale = new() { { "SH_STOCK", 100 }, { "SZ_STOCK", 100 }, { "SH_FUND", 1000 }, { "SZ_FUND", 1000 }, };参数说明:Pack=1保证结构体无填充,和文件布局一致。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 List<Bar> AggregateToMinute(List<TradeRecord> trades, int priceScale) { var bars = new List<Bar>(); 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才想起建索引。我早期图省事,先落库后建索引,结果一次查询要等几分钟。后来改成边解析边建索引,虽然写入慢一点,但查询体验完全不一样。如果你也要做长期数据积累,索引和分区要一开始就规划好。希望帮到你。
本文还有配套的精品资源,点击获取