简介:一份基于通达信TdxHqApi.dll实现的实时数据采集器源码包,面向金融量化开发者、股票行情分析人员,用于从通达信行情接口高效获取实时数据,解决手动抓取效率低、接口对接复杂等问题。压缩包共248个文件,约105.88MB,以84个C#源码文件和21个Java源码文件为主,辅以23个DLL动态库、配置文件、说明文档及工程样例,便于多语言环境调用与配置过渡。已有831人学习下载,适合具备一定编程基础、希望掌握通达信接口封装与数据采集逻辑的开发者。资源内提供C#与Java两套调用实现,涵盖行情库加载、错误事件处理、TradeX核心封装及App示例,并附数据格式说明与工程文件,可帮助理解接口交互流程、减少环境搭建障碍,为实时行情采集、量化策略回测或交易工具开发提供可复用的代码基础。
1. 借 TdxHqApi.dll 实现的实时数据采集器:它到底解决什么问题
“借TdxHqApi.dll实现的实时数据采集器(StockRealData)”,这标题描述的事,简单说就是:把一个通达信行情DLL变成你自己可控的盘中数据管道。最典型的场景是开盘后一边盯行情软件,一边手工往表格里填价格,等填完价已经变了;或者做策略回测需要分钟级甚至 tick 级数据,却找不到稳定又便宜的行情源。这个方案让程序按你设定的频率去拉快照、逐笔、分时数据,再落成 CSV、SQLite 或数据库表,供策略、看板和复盘使用。
适合人群也相对明确:个人量化、小团队、要做盘中监控面板或攒行情历史库的开发者。这里要先泼一盆冷水:这类DLL的接入成本从来不是“调通一个函数”,而是后面的进程稳定性、时间戳对齐、字段解析边界。你从网上拿到 StockRealData.zip 这类资源时,里面那颗 DLL 的版本和接口形态五花八门,处理不好连第一行调用都跑不通。下文就从接口形态确认开始,一步步把它变成能扛住整天的采集器。
2. 把 TdxHqApi.dll 用起来:先确认接口形态,再写第一行调用
拿到 StockRealData.zip 后,第一件事不是急着编译工程,而是检查里面的 TdxHqApi.dll 到底以什么方式暴露接口。这个 DLL 在市面上流通的版本并不统一,同样一个文件名,可能是纯 C 导出函数,也可能是 COM 组件,甚至某些版本同时支持两种。用错调用姿势,轻则拿不到数据,重则进程直接崩。所以本章先把选型理由讲清楚,再给出确认形态和最小调用的完整路径。
2.1 为什么选通达信行情 DLL 做实时数据源
做实时行情采集,常见备选有网页爬取、窗口自动化、行情DLL三种。网页爬取看上去最通用,但行情网站延迟高,字段埋在 HTML 结构里要额外解析,改版一次就要重写一次解析器;窗口自动化控制行情软件界面,能拿到图但拿不到结构化数据,而且刷新频率稍高界面就会卡死。TdxHqApi.dll 这类行情接口直接绕过界面,提供的是内存结构体,延迟和字段质量都高一个量级。很多版本的接口还自带历史数据查询,对回测场景特别友好。
选型边界也要说清楚:它是行情通道,不是交易通道,不能用来下单;接口授权范围以你拿到DLL的渠道说明为准;并发连接数和请求频率有限制,不适合商业级行情分发。如果你是自用或小团队采集,频率控制在秒级,一天跑下来数据量在百万行以内,这套方案是性价比很高的选择。如果你的目标是全市场、全 tick、永久保存,建议直接考虑商业行情源,自建采集器的维护成本会超过数据本身的价值。
2.2 先查导出表:DLL 是 COM 还是 C 接口
拿到 DLL 文件后,不要凭文件名猜,直接看导出表。在命令行里执行:
dumpbin /exports TdxHqApi.dll如果没有安装 Visual Studio,也可以用 MinGW 套件里的 objdump:
objdump -x TdxHqApi.dll | grep -iE 'export'输出结果里如果能看到DllGetClassObject、DllRegisterServer、DllCanUnloadNow这些名字,说明它是 COM 组件,调用方式是注册后创建实例;如果看到Init、Login、Logout、GetQuote这类函数名,说明是 C 风格接口,直接在 C# 里用DllImport声明。还有一种情况是两种都有,优先走 COM 路线,因为 COM 封装一般把登录、连接、缓冲区管理都包好了,用起来比裸 C 接口省心。
确认是 COM 组件后,在管理员命令行里注册:
regsvr32 TdxHqApi.dll注册成功后,打开注册表编辑器展开HKEY_CLASSES_ROOT,搜索 DLL 文件名关键词比如TdxHq或TdxApi,能找到对应的 ProgID。这个 ProgID 是后面Type.GetTypeFromProgID的唯一线索,不同版本命名差异很大,务必以注册表里查到的为准。
2.3 最小调用骨架:登录一次,取回一只股票的快照
如果确定是 C 接口形态,C# 里可以这样声明。注意函数名以你 dump 出来的导出表为准,下面是一份常见命名示例:
[DllImport("TdxHqApi.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private static extern int TdxHq_Init(string configPath); [DllImport("TdxHqApi.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private static extern int TdxHq_Login(string server, int port, string user, string password); [DllImport("TdxHqApi.dll", CallingConvention = CallingConvention.Cdecl)] private static extern int TdxHq_GetSecurityQuotes(string[] codes, int count, IntPtr outBuffer, ref int outCount); [DllImport("TdxHqApi.dll", CallingConvention = CallingConvention.Cdecl)] private static extern int TdxHq_Logout();调用骨架如下:
TdxHq_Init("tdx_config.ini"); int ret = TdxHq_Login("你的行情服务器地址", 7709, "", ""); if (ret != 0) { Console.WriteLine($"登录失败,错误码: {ret}"); return; } string[] codes = new string[] { "600000" }; // 证券代码 IntPtr buffer = Marshal.AllocHGlobal(4096); int count = 0; TdxHq_GetSecurityQuotes(codes, 1, buffer, ref count); // 此处再把 buffer 里的结构体解析成可读字段,见第 4 章 TdxHq_Logout();如果你拿到的是 COM 形态,调用方式换成创建实例加反射调用:
Type tdxType = Type.GetTypeFromProgID("注册表里查到的ProgID"); object api = Activator.CreateInstance(tdxType); var result = tdxType.InvokeMember("GetQuote", System.Reflection.BindingFlags.InvokeMethod, null, api, new object[] { "600000" });这段代码有两个参数细节要留意。第一,CallingConvention不要凭感觉选,C 接口老版本 DLL 常见的是Cdecl,但也不排除StdCall,对不上会直接报EntryPointNotFoundException或者调用栈损坏。第二,CharSet用Ansi,老行情接口返回的多是 GBK 编码字符串,改成Unicode后代码端会收到乱码,反序列化时疯涨。第一次调通后先别写循环,打印一条快照和行情软件人工对一下字段值,确认没有偏移错位再往下走。
3. 把采集器从“取一次”改成“一直跑”:实时管线的架构与参数
跑通一次取快照很简单,难的是让它从开盘前到收盘后稳定运行,期间不掉线、不重连风暴、不产生数据空洞。直接把第 2 章的取数代码丢进while(true)是新手最常见翻车姿势,开盘半小时连接就可能被服务端掐断,或者跑一上午内存涨几百 MB。这套采集器我一般会拆成三层,每一层各管一件事。
3.1 采集器分层:连接层、调度层、存储层
连接层持有 DLL 实例,负责Init / Login / Logout和连接状态检测。连接层不关心业务股票池,只维护“当前登录是否有效”这一个状态。调度层维护股票池和轮询间隔,按批次发起取数请求,把返回数据交给存储层。存储层负责把结构体转成行记录,写入文件或数据库,也可以推到内存队列供策略端消费。
为什么要强制分层?DLL 实例的状态非常脆弱,登录态一旦失效,重连时调度层需要无感衔接;存储层如果在取数线程里同步写文件,磁盘抖动会直接拖垮取数节奏。层与层之间用队列解耦,生产者和消费者各跑各的。调度线程只管问“数据来了没”,存储线程只管“拿到数据怎么写”,任何一层崩溃重启都不影响另一层。
3.2 快照轮询还是逐笔拉取:频率与数据量的权衡
TdxHqApi.dll 大多是请求响应模型,不是 WebSocket 那种服务端主动推送,所以实时性是靠轮询频率撑起来的。不同行情类型的推荐策略差别很大,我常用的参数如下:
| 行情类型 | 获取方式 | 推荐间隔 | 单日数据量参考 | 典型用途 |
|---|---|---|---|---|
| 快照 | 批量拉取 | 3 秒 | 单股票约 4800 条 | 盘中监控、分钟因子、异动提醒 |
| 逐笔 | 按需拉取 | 30 秒或事件触发 | 单活跃股数千笔 | tick 级回测、盘口行为研究 |
| 分时 | 批量拉取 | 60 秒 | 单股票 240 条 | 日内走势复现 |
3 秒间隔不是拍脑袋定的。A 股连续竞价 4 小时共 14400 秒,3 秒一轮对单只股票能攒约 4800 个快照点,足够画平滑分时曲线;压到 1 秒数据量翻三倍,还更容易触发服务端频控。逐笔数据量要单独估算:一只活跃股一天几千笔,自选池几十只就是几十万行,想全市场存逐笔一天就是千万级以上,没有数仓规划不要轻易上。
3.3 采集主循环:一个能扛住整天的 C# 骨架
下面这个骨架是采集器的核心,把连接层、调度层、存储层的交互压缩到了一个循环里:
private void Run() { _api = new TdxApiWrapper(); // 内部封装 DLL 调用 if (!_api.Connect(_server, _port, _user, _pass)) return; var pool = LoadStockPool("stock_pool.txt"); // 一行一个证券代码 int batchSize = 80; // 单批请求股票数 int intervalMs = 3000; // 轮询间隔 int failCount = 0; while (!_cancelled) { var sw = Stopwatch.StartNew(); try { foreach (var batch in Chunk(pool, batchSize)) { var rows = _api.GetQuotes(batch); // 一次取一批 _storage.Write(rows); // 只入队,不阻塞 Thread.Sleep(50); // 批间小停顿 } failCount = 0; } catch (Exception ex) { _logger.Error(ex, "采集异常"); failCount++; if (failCount >= 3) Reconnect(); // 连续失败才重连 } var elapsed = sw.ElapsedMilliseconds; _logger.Info($"本轮耗时 {elapsed} ms"); if (elapsed < intervalMs) Thread.Sleep(intervalMs - (int)elapsed); // 若 elapsed 长期接近 intervalMs,说明需要降频或优化存储 } }三个必调参数说明。batchSize受 DLL 响应包长度限制,常见控制在 50 到 100,太小请求次数多效率低,太大返回可能被截断或超时。intervalMs要以本轮实际耗时为基准,目标让耗时占比低于 70%,留出余量给 CPU 抖动和 GC 停顿。批间Thread.Sleep(50)是为了防止请求排队,不要设成 0,否则连续请求会在对端被判定为扫描行为。
为什么要连续失败 3 次才重连?偶发一次超时很常见,立刻重连反而容易引发重连风暴。Reconnect()里要先释放旧连接再重新Init,顺序反了会导致句柄泄漏。日志里一定要记每轮耗时,这条数据是后面排查数据空洞的唯一线索。
4. 解析返回结构:快照、逐笔、分时的字段映射与时间对齐
取数只是把结构体从 DLL 拿到内存,解析才是把内存变成能信的字段。很多采集器跑出来的数据没法用于策略,问题不在采集,在解析层把字段理解错了。这一章按快照、逐笔、分时三种类型讲字段映射,再重点说两个高频翻车点。
4.1 三种行情数据分别回答什么问题
| 数据类型 | 内容 | 最小时间粒度 | 适合做什么 |
|---|---|---|---|
| 快照 | 最新价、涨跌幅、五档、成交额 | 轮询间隔 | 异动监控、因子计算、看板 |
| 逐笔 | 每笔成交价、量、主动性 | 每笔成交 | 高频回测、盘口行为研究 |
| 分时 | 每分钟均价、累计成交量 | 1 分钟 | 日内策略、复盘 |
快照能覆盖 80% 的场景,先把它做扎实再说逐笔。逐笔字段里“主动买卖方向”在部分 DLL 版本里没有直接字段,要用成交价与最近一次快照价的对比推断,不要假设一定存在。分时数据和快照高度相关,如果已经有 3 秒快照,60 秒分时其实是冗余数据,可以后面按需生成,不必单独采集。
4.2 从内存块到结构化记录:解析代码与字段映射
DLL 返回的通常是连续内存块,里面按固定长度排列结构体。C# 端定义一个与 C++ 头文件对齐的结构体,再用PtrToStructure批量转换。下面是一份常见结构体定义:
[StructLayout(LayoutKind.Sequential, Pack = 1)] public unsafe struct QuoteData { public int Market; // 市场标识,0=深,1=沪 public fixed byte Code[8]; // 证券代码字节流 public int Time; // 时间字段,可能是 HHMMSS 或当日累计秒 public float Price; // 最新价 public float PreClose; // 昨收 public float Open; public float High; public float Low; public int Volume; // 累计成交量,单位通常为股 public long Turnover; // 累计成交额,单位通常为元 public float Bid1; public int BidVol1; // 五档买卖以此类推 }解析循环:
for (int i = 0; i < outCount; i++) { QuoteData q = Marshal.PtrToStructure<QuoteData>( buffer + i * sizeof(QuoteData)); // 转成业务记录时,手工补上 TradeDate 和 ServerTime Storage.Write(new StockTick { Code = Encoding.GetEncoding("GBK").GetString(q.Code).TrimEnd('\0'), Time = NormalizeTime(q.Time, _tradeDate), Price = q.Price, Volume = q.Volume, Turnover = q.Turnover }); }字段映射要注意三条。第一,Pack = 1必须和 C++ 头文件的#pragma pack(push, 1)对应,否则结构体内部会按 4 字节或 8 字节对齐,字段全部错位。第二,fixed byte Code[8]转字符串用 GBK 编码,默认的 UTF8 会把汉字代码和数字代码解乱。第三,Volume 和 Turnover 的累加基数从当日 0 点开始,不是从你订阅时刻开始,核对数据时拿收盘后的累计成交额和行情软件对比,差很多说明结构体偏移错了。
4.3 时间戳与复权:两个高频翻车点
时间戳是最容易埋雷的字段。很多 DLL 版本返回的 Time 不带日期,只有时分秒,比如143015表示 14:30:15,跨日存储时必须自己在业务层补一个 TradeDate 字段。另一些版本返回当日累计秒,比如52215表示当日第 52215 秒。解析前务必先打印一条样本肉眼确认格式,再写死解析逻辑,不要想当然。
复权是第二个大坑。实时行情全部是未复权价,但回测系统里常用前复权数据,直接把实时价拿去做回测,遇到除权除息日曲线会硬生生砍一截。正确做法是采集时只存原始价,每天单独存一份复权因子,算指标时再决定用哪种价格。不要在采集器里做复权,因为除权日后复权因子会变化,历史数据跟着变,不如存原值留到计算层处理。
5. 装上到跑通,最容易翻车的 4 个避坑记录
以下四个坑,是调试同类采集器时反复遇到的,按现象、原因、解决写清楚。每一条都值得在测试阶段就验证一遍,别等跑了一周再回头查。
5.1 登录掉线后重连成风暴
现象:采集器跑几分钟后日志出现连接失败,重连成功后几十秒又失败,反复循环;严重时行情账号被服务端临时限制。原因:多数行情服务端对短连接敏感,失败后立刻重连会被判定为异常行为;也可能是本机 TCP 连接没释放干净,TIME_WAIT 堆积占满端口。解决:重连用指数退避,1 秒、5 秒、30 秒递推;重连前先释放 DLL 旧实例再重新初始化,顺序不能反;单机并发连接控制在个位数以内,不要一个股票池开一个连接。
5.2 32 位 DLL 撞上 64 位进程
现象:工程用 AnyCPU 编译后程序能跑,但调用取数函数返回 0 或结构体全是 0,有的直接 AccessViolation。原因:老版本 TdxHqApi.dll 是 32 位本机组件,无法在 64 位进程里加载。解决:项目平台目标改成x86再编译;.NET 工程勾选“首选 32 位”;用任务管理器确认进程位数,x86 进程会标注“32 位”。这个坑在安装版 DLL 上尤其隐蔽,64 位系统下 32 位组件注册在 WOW6432Node 节点,64 位程序根本找不到。
5.3 轮询间隔调小,数据反而出现空洞
现象:把快照轮询从 3 秒调到 500 毫秒后,日志时间戳出现不规则跳跃,有的记录间隔变成 5 秒甚至 8 秒。原因:DLL 请求是同步阻塞的,频率提高后请求排队,单轮实际耗时超过设定间隔,下一轮被推迟。解决:先记录单轮实际耗时,目标间隔不要小于耗时两倍;调参后看一个小时的日志,统计间隔分布,出现长尾就降频。记住,实时采集器的核心参数是“峰值耗时 + 余量”,不是拍脑袋定的轮询间隔。
5.4 多线程取同一个 DLL 实例,跑几小时崩一次
现象:稳定性测试时偶发内存访问错误,重启后恢复正常,崩溃时间没有规律,很像玄学。原因:DLL 内部状态不是线程安全的,多个线程同时调用同一实例会破坏内部缓冲区。解决:所有 DLL 调用放进同一线程,用锁串行化;需要并行时按股票池拆分到多个进程,每个进程一个 DLL 实例,进程间物理隔离;不要在回调里直接更新界面控件。这个规则要在设计初期就定下来,后面加线程只会让问题更隐蔽。
6. 给采集器加数据自检:连续竞价对账与缺失率统计
数据采了不等于数据可信。我的习惯是每天收盘后跑一次对账,确认当天落盘的数据没有漏段、错位,连续一周零差异才敢让采集器无人值守。这里分享三个自检方法。
6.1 理论条数对账
A 股连续竞价时段为 9:30 到 11:30、13:00 到 15:00,合计 14400 秒。按 3 秒轮询间隔,单只股票理论快照条数约 4800 条。对账脚本长这样:
import pandas as pd def check_snapshot_count(df, interval_s=3): # df 需要包含 trade_date, code, time 三列 expect = 14400 / interval_s actual = df.groupby(["trade_date", "code"]).size() bad = actual[actual < expect * 0.9] if not bad.empty: print("缺失较重的记录:", bad.index.tolist()) return bad90% 阈值是经验值,因为程序启动时刻、行情服务端重启都会造成少量缺失,低于 90% 才值得排查。注意这里按“交易日 + 证券代码”分组,如果某天你只从 10 点开始采集,要把理论值按实际启动时间折算。
6.2 抽样复核对账
每天从股票池随机抽 20 只,把落盘最后一笔快照的最新价与行情软件的收盘价比对,误差超过 0.01 元就检查是不是价格字段解析错了。这一步也顺带验证结构体偏移没有因为 DLL 更新而变化。DLL 一旦换版本,第一时间跑这个对账,比看任何文档都直观。
6.3 逐笔数据完整性检查
逐笔没有统一理论条数,实践中用两个指标:当天落盘的逐笔成交额总和与收盘快照里的当日累计成交额对比,偏差应小于 0.5%;每笔时间戳要单调递增且都在连续竞价时段内。出现异常时,优先查本地磁盘空间和存储层队列堆积,这两个原因占逐笔缺失的八成。
我自己的教训是:不要迷信 DLL 没报错就是没错。曾经采集器日志全绿,复盘时发现某天下午数据全是一个小时前的旧值,原因是行情服务端连接半开,本地一直读到旧缓存。从那以后每天收盘固定跑 15 分钟对账,确认条数、抽样价、逐笔成交额三个维度全部通过,才敢把实时采集器放手。这套自检流程不复杂,但能帮你免掉大量复盘时的数据清洗时间,希望帮到你。
本文还有配套的精品资源,点击获取