基于TdxHqApi.dll的A股实时行情采集器:从TCP协议到落库实践

📅 发布时间:2026/10/7 9:28:04
基于TdxHqApi.dll的A股实时行情采集器:从TCP协议到落库实践
简介这是一套借助TdxHqApi.dll实现的实时股票数据采集器面向量化交易、行情分析及通达信接口二次开发者。压缩包内含C#与Java两套示例源码以及对应DLL、配置文件、数据格式说明和文档资料可帮助读者快速掌握通达信行情接口的调用方式、实时数据解析与采集逻辑。资源共248个文件以cs、java源码为主辅以dll动态库、txt说明、config配置、class编译文件以及少量pdf、xlsx、csv等文档表格整体打包约105.88MB目录结构清晰便于按语言和模块查阅。目前已有832人学习下载适合需要对接通达信行情、构建实时数据管道的初中级开发者参考既可直接借鉴C#或Java调用示例也可深入研究TdxHqApi.dll的封装与数据格式细节。1. 借用TdxHqApi.dll做实时数据采集器为什么比HTTP轮询和云行情都划算先立一个场景做A股实时行情采集的人多半都经历过新浪、腾讯的HTTP行情接口延迟不稳、请求频繁被限流的憋屈。拉个分时数据要轮询轮询一勤快就被封IP数据一断就是几分钟空洞。想上Level-2或者云厂商的实时行情服务报价又让人肉疼。这时候通达信客户端自带的TdxHqApi.dll就成了一个很务实的选择——它走的是通达信自有的TCP行情协议跟行情主站建立长连接后数据是主站主动推送过来的延迟低了一个量级而且没有HTTP那种请求次数限制接口又是免费开放的。标题里的StockRealData.zip就是一个把TdxHqApi.dll包进采集进程的典型工程负责连接主站、订阅股票、解析行情、落库最终产出一份随时保鲜的本地实时行情库。适合做量化回测、自建盯盘工具、维护数据仓库的开发者不适合只想拿现成数据、不想碰底层协议的纯业务方。2. 先读懂TdxHqApi.dll的行情通道自定义TCP协议、导出函数和数据流向2.1 通达信行情DLL的底层协议为什么大家都绕着它做通达信行情接口不是开放的RESTful API。它走的是自己定义的一套二进制TCP协议封包格式、字段偏移、加解密规则都是内部实现。早期想做实时采集的人得靠抓包去逆这套协议工作量非常大且协议一旦改版就要跟着改。它的价值在于这层协议已经被DLL封装好了。你不需要理解封包里第几个字节是价格只需要把DLL加载进进程、传入服务器地址和端口、注册回调函数它就能把解好密、拆好包的数据从回调里吐给你。这个DLL的本质是C接口的动态库不是.NET组件所以C#要P/Invoke导入。它的数据流向有两条线一条是主动请求比如客户端发起查询某只股票当前快照DLL收包后在回调里返回结果另一条是订阅推送你注册了某只股票的关注主站一有行情变动就推给你对应的是回调里的某个消息类型。实时采集器能叫实时靠的就是第二条线——不是你去轮询是服务端主动找上门。2.2 把TdxHqApi.dll接进C#工程最小P/Invoke调用代码与导出表确认拿到DLL的第一件事不是写代码而是看清导出函数表。这DLL的导出函数命名和序号在不同版本里可能不同有些为了减小体积甚至只用序号导出函数名都不给。我通常先用Visual Studio自带的dumpbin把导出表打出来dumpbin /exports TdxHqApi.dll输出大致长这样不同版本差异很大仅示意格式ordinal hint RVA name 1 0 00001000 TdxHq_Init 2 1 00001200 TdxHq_Connect 3 2 00001400 TdxHq_RegisterNotify看到真实函数名或序号之后再在C#里声明。如果只有序号没有名字就通过EntryPoint#序号来绑定。一个最小可用的P/Invoke声明大概是这样的// 注意函数名与序号仅示意实际以dumpbin导出的为准 [DllImport(TdxHqApi.dll, EntryPoint #1, CallingConvention CallingConvention.StdCall)] private static extern int TdxHq_Init(string configDir); [DllImport(TdxHqApi.dll, EntryPoint #2, CallingConvention CallingConvention.StdCall)] private static extern int TdxHq_Connect(string host, int port); [DllImport(TdxHqApi.dll, EntryPoint #3, CallingConvention CallingConvention.StdCall)] private static extern int TdxHq_RegisterNotify(NotifyCallback cb, IntPtr userData);参数里最关键的是CallingConvention。TdxHqApi.dll这类老牌Windows动态库清栈方式基本是StdCall也就是调用方在被调函数返回后不需要自己调整栈。写错成Cdecl轻则参数错乱重则进程在调试器里看起来毫无征兆地崩溃属于典型的玄学故障。另一个关键参数是configDir它指向一个配置文件目录DLL初始化时要读这里面的服务器地址列表、通讯超时等参数路径给错的话后续连接会返回一个让人摸不着头脑的错误码。2.3 主动查询与推送回调采集器的实时性从哪来接下来要把调用逻辑按两条路径设计。主动查询是拉适合启动时拉快照、补漏掉的历史Tick推送回调是推行情变动时DLL在内部接收线程上触发你注册的委托。实时数据采集器的实时两字完全靠推送回调撑着。// 行情推送回调的典型签名具体消息类型和参数以导出表为准 private delegate void NotifyCallback(int msgType, int market, byte[] rawData, int length); private static void OnNotify(int msgType, int market, byte[] rawData, int length) { // msgType 区分是快照、成交明细还是指数行情这边只关心快照 if (msgType ! 0x0101) return; // market 区分沪市和深市注意不同版本的约定可能相反 // 先把数据塞进队列绝不在回调里做解析落库 _incomingQueue.Enqueue(new RawPacket(market, rawData, length)); }这段代码揭示了一个核心原则回调是在DLL的接收线程上执行的你在回调里做的任何耗时操作都会阻塞它的接收循环。如果回调阻塞超过一定时间TCP接收缓冲堆积主站会判定这个客户端处理不过来直接断开连接。所以回调函数里只做浅拷贝和入队解析、落库、打日志全部丢给其它线程。3. 组装StockRealData采集器连接、登录、心跳与断线重连3.1 建立连接和登录主站服务器列表、端口和配置解析生产环境的采集器不能只连一台行情主站。主站是分地域、分运营商部署的单台能接纳的连接数有限而且偶尔有维护。常见做法是准备一个服务器列表配置文件里面维护多台主站。通达信行情主站的默认端口一般是7709这个数字在大量开源项目里都能对上。// ServerList.txt 示例 // 格式ip,端口,市场 // 119.147.212.13,7709,1 // 60.28.23.33,7709,0 private static void ConnectAllServers() { var lines File.ReadAllLines(ServerList.txt); foreach (var line in lines) { var parts line.Split(,); if (parts.Length ! 3) continue; int ret TdxHq_Connect(parts[0], int.Parse(parts[1])); if (ret 0) { // 连接成功后需要登录登录接口一般在connect之后调用 // 很多版本不校验账户密码只要求传客户端标识 int loginRet TdxHq_Login(stockDataCollector); if (loginRet 0) break; // 第一台连上就够其他备用 } } }我在这里做了连上就停的取舍。为什么不多连几台做负载均衡因为一台采集器实例维护多条主连接意味着多套心跳、多套消息序号管理复杂度成倍上升而且主站的异常检测会更敏感。单主连接单实例备用服务器只参与重连这是最稳的形态。3.2 心跳包和断线重连让采集器常年挂在服务器上的两个前提实时采集器的生命周期不是几分钟是几个月。它的网络连接必须过心跳保活和断线重连两道关。主站对空闲连接是有超时保护的我观察到的阈值一般在60到90秒超过这个时间没有数据往来连接会被主动断开。所以采集器必须自己维持心跳。// 心跳保活线程每30秒发一次最轻量的查询保持连接不空闲 private static async Task HeartbeatLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) { try { // 发送一个无副作用的查询比如查询服务器时间或行情状态 TdxHq_SendHeartbeat(); await Task.Delay(TimeSpan.FromSeconds(30), token); } catch (Exception ex) { // 心跳一旦失败立即触发重连流程而不是等下一次心跳 Log($Heartbeat failed: {ex.Message}); await ReconnectAsync(token); await Task.Delay(TimeSpan.FromSeconds(5), token); } } }心跳间隔为什么是30秒因为要在主站60秒的空闲阈值内至少产生两个数据包即便网络抖动丢掉一个也不会触发踢线。重连的姿势比心跳更重要。我建议把重连做成状态机连接失败按指数退避1秒、2秒、4秒、8秒……上限30秒。收到断开通知就立刻重连但连带失败的次数多了就要拉长间隔避免高频重连把自己的IP送进对端风控名单。3.3 采集范围与启动流程自选股列表、全量快照加增量推送连接稳定后要决定采什么。实时采集器不会去订阅沪深两市全部股票主站对单连接的订阅数量是有限制的。所以工程上要有股票池清单然后启动时先拉一次全量快照再进入推送订阅。private static async Task InitSnapshotAsync(string[] stockCodes, CancellationToken token) { // 1. 对股票池分批查询当前快照写入内存库 var snapshot await RequestSnapshotAsync(stockCodes, token); _store.BulkUpsert(snapshot); // 2. 快照拉完后再向DLL注册订阅进入实时推送模式 int subRet TdxHq_Subscribe(stockCodes); if (subRet ! 0) { Log($Subscribe failed, code{subRet}); } // 3. 订阅后3秒内如果没有推送判定订阅失败触发告警 await HealthCheckAsync(TimeSpan.FromSeconds(3), token); }顺序不能反先订阅再拉快照推送的最新价会覆盖快照里的开盘价、昨收、成交量等基础字段最终数据表里只会残留被更新的那几列其它字段全是空。先快照打底再增量推送才能保证任何时刻同一只股票的数据都是一行完整的记录。4. 解析与落库字段对齐、字节序、快照和成交明细分开存4.1 原始包怎么拆字段字节序和定点数的坑回调里拿到的rawData是DLL解过密的但还不是结构体它按照协议排列成字节流。拆包时最大的坑是字节序和数值表示。通达信协议里不同消息类型的字节序可能不一致有些版本索性全用大端。价格和成交量很少是浮点数而是整数定点数比如价格乘以1000存储收到后要除回去。// 从字节流里读取一个32位整数按定点数比例转换为价格 private static decimal ToPrice(byte[] buf, int offset, bool bigEndian) { if (bigEndian) { int val (buf[offset] 24) | (buf[offset 1] 16) | (buf[offset 2] 8) | buf[offset 3]; return val / 1000m; // 实际比例系数按品种类型配不一定都是1000 } int little BitConverter.ToInt32(buf, offset); return little / 1000m; }这个函数只处理了一个价格字段实际包里还有昨收、今开、最高、最低、买一卖一价等十几个字段每个都要走一遍类似的转换。经验是不要相信这个DLL一定是小端或者所有价格都乘1000这种经验之谈。我一般会在解析器里做一个校准模式先拿一只价格明确且带小数的股票做对比价格字段能对上再批量信任对不上说明定点系数配错了趁早查配置。4.2 表结构设计快照表、成交明细表和逐笔委托表三分落库结构上见过最稳的是拆成三张表别想着把推送的所有字段塞进一张宽表。快照表存的是某一刻的状态快照更新频率极高成交明细表是逐笔成交插入频率更高而且只增不改逐笔委托表讲究更细一般采集器都放在后面考虑。三张表的主键设计也不一样。表名主键典型字段snapshot_stock(ts, code)时间、代码、开盘、最高、最低、现价、成交量、买一卖一五档trade_detail(ts, code, seq)时间、代码、成交价、成交量、买卖方向order_detail(ts, code, seq)时间、代码、委托价、委托量、委托类型快照表主键必须加时间维度。同一毫秒内行情可能多次变动每股每秒都可能推送多帧你要是只拿代码当主键做覆盖写历史数据就被抹掉了后面想回放等于没数据。成交明细表要用序列号做辅助去重防止主站重复推送或者TCP重传导致同一笔成交写两次。4.3 写库的压力控制回调只入队写库线程批量提交数据库写入是实时采集器最容易翻车的地方。每次推送回调都直接执行Insert行情一活跃数据库连接池被打满回调阻塞很快就被主站断开。解决思路是生产者-消费者模式回调入队后台线程定时批量提交。// 批量落库线程每500毫秒或攒够512条触发一次写入 private static async Task FlushToDatabaseAsync(CancellationToken token) { var batch new ListSnapshotStock(512); while (!token.IsCancellationRequested) { while (_queue.TryDequeue(out var item) batch.Count 512) { batch.Add(item); } if (batch.Count 0) { try { // 用Dapper的Execute或EF的批量扩展不要单条Insert await BulkInsertSnapshotsAsync(batch, token); } catch (Exception ex) { Log($Bulk insert failed: {ex.Message}); // 写入失败要保留数据不能直接Clear重新放回队列或写本地缓冲 } batch.Clear(); } await Task.Delay(TimeSpan.FromMilliseconds(500), token); } }这里有两个容易被忽略的细节。一是批量写失败时不能直接把batch丢了否则数据莫名消失且没有任何痕迹。我会把失败的batch写进本地文件缓冲等连接恢复后再重放。二是积压有上限不能无限制堆积否则进程内存会被撑爆。队列长度超过1万条时选择丢弃最旧的数据并计数告警宁可少一小段行情也不能让进程OOM。5. 实时数据采集器常见故障排查5个让我翻车的地方5.1 现象一加载TdxHqApi.dll就抛BadImageFormatException原因是DLL是32位的而C#工程编译成了AnyCPU或x64。在64位系统上加载32位DLLCLR直接抛格式不正确异常。这个报错内容很长很吓人新手很容易往缺少VC运行库、依赖缺失方向排查。解决是进入项目属性把平台目标设为x86且确保宿主进程也是32位。我把采集器注册成Windows服务后还踩过一次服务管理器里启动的进程不一定继承主机的平台配置所以入口处要主动打印Environment.Is64BitProcess日志里能一眼确认当前进程位数。5.2 现象连上主站后几十秒就断或者订阅后完全没有推送这个现象要分两类排查。连上就断大概率是心跳保活没做或间隔太长主站识别的空闲连接超时主动断开。完全没推送几乎可以锁定在订阅代码格式上比如把600000传成了没有交易所后缀的裸代码主站匹配不到股票就不推行情而DLL在订阅环节通常不返回错误看起来就像连接正常但数据为空。解决方法是抓包看TCP层有包且被断开去修心跳完全没数据包把代码格式统一成600000.SH、000001.SZ这类带后缀的写法并在订阅后立刻打印返回码。我第一次做这个方向时就在裸代码上浪费了一晚上属于最容易忽略的静默故障。5.3 现象解析出的价格和行情软件里对不上差一个数量级或固定小数位原因基本是定点数比例系数不对。通达信协议里价格字段的隐式小数位因消息类型甚至品种类型而异股票、指数、债券用的系数可能不一样。照抄网上某份解析代码按统一系数除必然对一部分品种翻车。解决方式是把系数做成配置化表按品种类型分别配。落地时写一个对拍校验工具解析结果里随机抽20只股票和行情软件里显示的现价、均价逐行对比系数错一眼就能看出规律。5.4 现象更新脚本时发现TdxHqApi.dll被占用无法覆盖原因是采集器进程没退出或者退出后还有线程没完全释放DLL模块句柄。Windows对正在被进程加载的DLL文件会加锁直到所有引用它的进程退出。很多运维习惯直接杀掉进程就复制文件但进程刚死那几秒句柄可能还没释放。解决方法是不要原地覆盖先停服务再复制再启动分三步走。更省心的是给DLL文件名带版本号配置里指定加载哪个版本更新时把配置指向新版本旧文件等下次维护窗口再清理。这样还能随时回滚相当于给采集器留了后悔药。5.5 现象多线程环境下行情数据错乱、内存持续上涨数据错乱是因为DLL回调线程和主线程共享了非线程安全的集合或者虽然是线程安全集合但用了错误的使用方式。内存上涨是因为行情数据被某种方式持有迟迟没有释放。这类问题通常要在采集器运行几小时甚至几天后才会暴露属于慢性病。解决方法是所有共享中转集合一律用ConcurrentQueue、ConcurrentDictionary并且明确这些集合只做中转不做存储落库后立即移除引用。同时给队列设置硬上限超过就丢弃最旧数据并计数告警宁可让监控发现丢了几个Tick也不能让进程被OOM杀掉。6. 把StockRealData做成常驻服务进程守护、数据保鲜自检和升级技巧如果采集器只是在自己电脑上偶尔跑一跑上面那些坑最多浪费点时间。但当它要部署到无人值守的服务器上连续运行几个月时还需要一套服务化的保障层。第一个技巧是进程守护。不要把EXE裸奔在桌面上要注册成Windows服务。SCM能帮你做崩溃重启。用sc create StockRealData binPath ...创建服务在服务属性里把失败后重启设为重新启动服务进程崩溃后最多断几分钟就能拉起来。服务账户如果用LocalSystem网络权限没问题但工作目录容易变到系统目录DLL的相对路径就会找不到配置文件。最笨但最有效的做法是在代码里用AppDomain.CurrentDomain.BaseDirectory拼接所有文件路径强制与EXE所在目录绑定。第二个技巧是数据保鲜自检。实时行情最怕的不是数据错而是看起来一切正常实际上已经停更半天。写一个独立的监控逻辑定期查询数据表里最新行情时间戳和当前时间的差值超过90秒就判定采集器失联触发告警和自动重连。// 每60秒检查一次最新行情新鲜度超阈值自杀重启让系统接管 private static async Task FreshnessWatchdogAsync(CancellationToken token) { while (!token.IsCancellationRequested) { var latestTs await GetLatestQuoteTimestampAsync(token); if (DateTime.Now - latestTs TimeSpan.FromSeconds(90)) { await ReconnectAsync(token); // 重连两次无效就退出进程让Windows服务管理器拉起 } await Task.Delay(TimeSpan.FromSeconds(60), token); } }第三个技巧是日志轮转和升级回滚。采集器长时间运行日志增长很快我按天分文件只保留最近7天老日志自动清理。DLL升级时采用带版本号的文件名加配置切换避免文件占用导致的更新失败。这套组合做下来采集器基本达到只坏网络不坏程序的稳定状态。做了几年行情采集我最大的教训是实时采集器的崩溃往往不在技术难点而在对静默故障的容忍度太低。你可以优化心跳、优化批量落库、优化线程模型但想让它长期稳定运行一定要有自检机制和进程守护时刻知道自己采集的数据到底还新鲜不新鲜。希望这些经验能帮你少走几条弯路把StockRealData稳稳当当地跑起来做成一条可靠的实时数据管道。本文还有配套的精品资源点击获取