ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于TdxHqApi.dll的股票实时数据采集系统:本地行情接入最短路径

2026/10/2 4:41:05 拓冰建站 浏览量
基于TdxHqApi.dll的股票实时数据采集系统:本地行情接入最短路径 简介面向金融软件开发与量化交易学习者基于TdxHqApi.dll的证券行情实时数据采集系统实现覆盖从行情接入、协议解析到业务计算的完整链路适合用作研究通达信行情协议、搭建实时行情处理程序的参考工程。系统分为数据接入层、协议解析层和业务逻辑层接入层维持与行情服务器的持久连接解析层按标准拆解二进制流并结构化重组业务层完成指标计算与异常过滤整体采用多线程异步处理、数据校验、断线自动重连及缓存补偿机制可应对千级数据流并发与毫秒级延迟场景。压缩包共299个文件、约105.88MB主要文件类型包括cs源码、java源码、dll动态库以及config、txt、pdf等配置与说明文档目录结构便于按模块检索。已有46人学习可从中获得完整工程骨架、并发数据流处理思路和通达信接口封装示例也可直接参考其中价格预警触发、技术指标计算、行情图表生成等业务逻辑便于二次开发与验证。1. 基于TdxHqApi.dll的股票实时数据采集系统为什么它是本地行情接入的最短路径做量化或者盯盘工具的人迟早会遇到一个问题想要实时行情但不想付昂贵的L2费用也不想用爬虫去抓网页版行情——延迟高、字段乱、还容易被反爬。基于TdxHqApi.dll的股票实时数据采集系统就是在这条路上被验证过无数次的本地方案。它直接加载通达信行情动态库走TCP长连接从行情服务器拿数据拿到的是结构化报文不是HTML延迟在几十毫秒级别。这套系统解决的核心问题有三个实时分笔订阅、历史K线补齐、盘口数据落地。适合想自己搭行情源的量化散户、做桌面行情工具的个人开发者以及需要内部行情数据库的小型团队。2. 初始化TdxHqApi.dll连接从加载动态库到建立TCP会话的完整流程2.1 先搞清dll在系统里的角色和依赖TdxHqApi.dll在通达信体系里承担的是“客户端与行情服务器之间的协议层”。你调用它它帮你把请求打包成通达信私有协议发到行情端口再把服务器的二进制响应解包成容易读取的结构体。所以你不是在写协议解析你是在写“怎么用好这个黑匣子”。先说你机器上需要什么一个能跑的通达信客户端或者至少一个包含TdxHqApi.dll的目录。常见的做法是直接从通达信安装目录里找到这个dll复制到你自己项目的bin目录下。它依赖一些VC运行时库如果目标机器是精简系统记得把vcruntime140.dll一起带上不然LoadLibrary失败的时候你会一头雾水。另外注意一个问题这个dll是32位还是64位取决于你拿到的版本。如果你的采集程序是64位的而dll是32位的直接加载会报“模块映像格式无效”。这种情况最常见的解法是让采集进程以32位模式编译或者用32位辅助进程做数据中转。别在这个问题上硬扛浪费半天不如直接统一位数。2.2 加载与初始化最小可用代码加载dll的方式分两种隐式链接和显式加载。隐式链接要在编译期指定lib适合自己写的C工程显式加载用LoadLibrary加GetProcAddress适合做插件化或者语言绑定。下面这段C代码是显式加载的最小骨架也是我平时起项目的第一步typedef int (*InitFunc)(const char* ip, int port, const char* user, const char* pass); typedef int (*ConnectFunc)(); typedef int (*DisconnectFunc)(); // 1. 加载dll路径按你自己工程的相对目录来 HMODULE hDll LoadLibraryA(TdxHqApi.dll); if (!hDll) { printf(LoadLibrary failed, error%d\n, GetLastError()); return -1; } // 2. 取出关键入口函数 InitFunc init (InitFunc)GetProcAddress(hDll, Init); ConnectFunc connect (ConnectFunc)GetProcAddress(hDll, Connect); DisconnectFunc disconnect (DisconnectFunc)GetProcAddress(hDll, Disconnect); if (!init || !connect || !disconnect) { printf(GetProcAddress failed\n); FreeLibrary(hDll); return -1; } // 3. 初始化并建立连接 int ret init(119.147.212.81, 7709, , ); if (ret ! 0) { printf(Init failed, ret%d\n, ret); return -1; } ret connect(); if (ret ! 0) { printf(Connect failed, ret%d\n, ret); return -1; }这里的逻辑说明Init负责把dll内部的协议栈状态复位传入的是行情服务器IP和端口。Connect才真正建立TCP长连接。Init里的用户名密码留空是常见做法因为通达信行情服务器一般不做用户校验真正的鉴权在登录交易柜台那套体系里跟行情接口是分开的。参数说明行情服务器IP建议用主站列表里的地址不要用客户端里随机分配的那个短连接地址短连接地址随时可能失效。端口7709是通达信行情主端口如果连不上可以试试7710或者别的备用端口。Init返回0表示成功非0值时先查一下是不是IP被限流再查dll依赖库是否缺失。2.3 登录与连接状态管理connect成功之后你以为就能直接收数据了还差一步得确认连接状态是“已就绪”而不是“已建立”。通达信的TCP连接建立不等于行情可用服务器可能还在做会话校验。常见做法是轮询一个状态接口或者等待一个就绪回调。我一般会做一个2秒超时的等待循环循环里检查状态值直到变成就绪态再开始订阅。如果超时就调用disconnect然后重来。这里还有一个容易被忽略的细节dll内部的连接是有心跳的一般30秒左右发一次但如果你长时间不订阅任何数据某些服务器会在60秒左右主动断开。所以建议在系统里加一个定时器每隔20秒主动调用一次GetSecurityCount或者类似的数据请求保持连接活跃别让服务器把你当死链接清掉。连接状态管理这块强烈建议做成独立线程不要和UI线程混在一起。dll的回调线程和你自己的业务线程是两条线回调线程负责收数据业务线程负责处理中间用队列解耦不然你会在“回调里写文件”这件事上翻车。3. 订阅实时行情并解析把分笔、盘口与K线变成结构化数据3.1 订阅股票实时数据的协议流程连接就绪以后采集系统就进入核心阶段订阅。通达信的订阅模型不是“你给我推所有股票”而是“我告诉你我要哪些你持续给我推”。所以第一步是设置股票列表第二步是发起订阅。常见的封装接口大致是这个形式SubscribeMarketData(market, code, count)其中market取0或者10代表深圳1代表上海。这里有个很容易搞反的点如果你在通达信软件里看到股票代码是“600000”它在接口里对应的不是600000这个整数而是市场1加代码600000的复合形式。所以传参之前先把代码拆成“市场6位代码”两段。收到订阅确认以后数据就开始往回调里灌了。但要注意通达信行情服务器不是每笔成交都推的。它一般按“快照”模式推送快照间隔在3秒左右。也就是说你看到的“实时”其实是3秒级别的快照不是逐笔成交。如果你要做的是高频盘口分析这个精度不够但做日内的分钟级策略和盘口统计完全够用。3.2 分笔行情的回调解析时间、价格与买卖标识分笔数据在回调里的结构大致包含时间戳、价格、成交量、买卖标识。买卖标识是关键0代表主动买1代表主动卖这个字段决定你算出来的主动买量和主动卖量是否准确。看一段解析代码struct TickData { int market; // 0深 1沪 int code; // 6位代码 int time_int; // HHMMSS格式 double price; // 当前价 int volume; // 当前笔成交量 int buy_sell_flag; // 0买 1卖 }; void OnTickCallback(TickData* tick, int count) { for (int i 0; i count; i) { // 注意这里的时间是int不能直接当字符串用 int hh tick[i].time_int / 10000; int mm (tick[i].time_int / 100) % 100; int ss tick[i].time_int % 100; // 主动买量累计 if (tick[i].buy_sell_flag 0) { active_buy_volume[tick[i].market][tick[i].code] tick[i].volume; } } }逻辑说明time_int是整型的时间编码比如93500代表09:35:00直接格式化输出可以但不要拿去做时间比较因为跨日的时候会失效。buy_sell_flag这字段在分笔里才有意义快照接口里不一定有。参数说明某些接口里volume的单位是“手”而不是“股”1手等于100股。如果你拿到的值和软件界面显示的数量差100倍不是bug是单位没换算。这个坑在最后做存储和回测的时候会放大建议在采集入口统一转成“股”后面所有模块都用股为单位避免到处乘除。3.3 五档盘口与K线数据的获取路径盘口数据一般走独立的接口比如GetQuote(market, code)返回的是一个包含买一至买五、卖一至卖五的结构。这组数据不是推送的是你主动拉取的所以如果要做盘口的连续记录就得在自己这边做定时轮询。轮询频率建议控制在1到3秒一次别低于1秒。通达信服务器对于单IP的请求频率是有限制的过于频繁的主动拉取大概率被断开连接。我自己的经验是2秒轮询一次是性价比最高的点既不会漏掉盘口的明显变化也不会触发限流。K线数据就更直接了GetKLine(market, code, period, start, count)period取0到4分别代表5分钟、15分钟、30分钟、60分钟和日线。K线建议在开盘期间按周期结束时批量拉取不要在盘中频繁拉最后一根未走完的K线因为那个值会一直变你存了也是错的。还有一个点如果要做分钟K线的实时累计不要直接存服务器推的最后一根K线而是自己用分笔数据累加。服务器推的最后一根K线是快照式的可能在周期结束前一秒出现也可能不出现可靠性不如自己算。4. 数据落地快照表、增量表与本地库的取舍4.1 库存放SQLite还是时序库采集到的数据最终要落到本地。选型上小规模个人使用我推荐SQLite不需要额外起服务一个文件搞定备份也方便。但如果你的数据量到了每天几百万行以上或者你要做长时间的历史回测查询直接用TimescaleDB或者DuckDB这类列式方案会更舒服。SQLite的瓶颈不在写入量而在并发写。你只有一个采集进程写所以用WAL模式就能绕开锁的问题。时序库的优点是压缩率高、按时序聚合查询快代价是部署多一个服务。我的建议是先上SQLite数据量真上来了再换时序库不要一开始就把架构搞重。4.2 表结构与写入策略分笔表和快照表要分开。分笔表存每一次回调里的成交明细快照表存每2秒拉一次的五档盘口。这两类数据的查询模式完全不同混在一张表里会让索引失效。DDL大致长这样CREATE TABLE tick_data ( ts INTEGER NOT NULL, -- 时间戳Unix秒 date_tag TEXT NOT NULL, -- 交易日如2025-06-10 market INTEGER NOT NULL, -- 0深 1沪 code TEXT NOT NULL, -- 6位代码 price REAL NOT NULL, -- 成交价 volume INTEGER NOT NULL, -- 成交量股 buy_sell_flag INTEGER NOT NULL, PRIMARY KEY (date_tag, market, code, ts) ); CREATE TABLE snapshot_data ( ts INTEGER NOT NULL, date_tag TEXT NOT NULL, market INTEGER NOT NULL, code TEXT NOT NULL, bid1_price REAL, bid1_vol INTEGER, ask1_price REAL, ask1_vol INTEGER, bid2_price REAL, bid2_vol INTEGER, ask2_price REAL, ask2_vol INTEGER, PRIMARY KEY (date_tag, market, code, ts) );逻辑说明主键里带上date_tag是为了让每天的收盘清洗可以直接按分区删掉重来不用精确到秒去对比。ts用Unix秒可以让跨语言处理更友好不要存成字符串时间查询排序都比字符串高效。写入策略上不要来一条写一条。dll回调频率高的时候每秒可能上百条分笔每条都写盘会把磁盘IO打满。我一般做批量写入内存里攒够500条或者攒够2秒再一次性事务提交。4.3 收盘后的补齐任务盘中采集有个天然缺陷你不可能100%保证连接不中断一旦断线中间就缺了一段数据。所以收盘后必须做一次补齐把当天缺失的分钟K线和分笔数据补回来。补齐任务我放在交易时段结束30分钟后跑那时候当天数据已经完全固定。补齐逻辑是对比本地已有的最大时间戳和服务器拉到的当天数据只补缺失的时间段。注意补齐的时候用K线接口拉分钟线而不是用分笔接口补分笔接口补起来太慢而且容易触发限流。补齐完还要做一次校验拿本地数据的总笔数和通达信软件当日显示的总笔数对比差值超过1%就说明断线缺口没补干净需要告警。5. 实时采集系统的6个典型坑从断线、乱码到数据缺口的排查记录5.1 断线重连后数据缺口现象程序跑了一个小时中间有一次网络抖动重连成功后发现数据少了一截而且日志里没有明显报错。原因重连后dll会重新发快照但快照只包含当前时刻的盘口和最新价不会把断线期间的每一笔分笔重新推给你。你看到连接恢复了就以为数据是完整的实际上缺口已经产生。解决维护一个本地最大时间戳重连成功后立刻用K线接口拉缺失区间的数据补齐。如果缺口超过5分钟且你订阅的是分笔就直接放弃补齐收盘后再走批量补齐任务。不要试图用实时流去追历史缺口追不上的。5.2 中文乱码现象拿到的股票名称字段是乱码比如“贵州茅台”变成“璐″彔” 。原因TdxHqApi.dll返回的字符串是GBK编码而你的程序内部用的是UTF-8直接转换就会乱。这个坑在股票名称、板块名、公告标题上都会出现。解决在解析字符串字段后做一个统一的GBK转UTF-8。C里可以用MultiByteToWideChar转成宽字符再转UTF-8。还有一点如果你用的数据库连接库默认字符集是UTF-8写入之前必须先转码不然后面查出来还是花的。5.3 订阅过快被服务器断开现象程序启动后批量订阅了5000只股票的实时行情运行不到10秒连接被断开重连后再次断开。原因通达信行情服务器对单连接的单次订阅数量有限制一次性订阅全部股票会触发服务端的保护机制。解决分批订阅每次最多订阅500只中间间隔500毫秒到1秒。如果你确实需要全市场数据用“订阅指数成分股按板块分批订阅”的组合方式别一股脑全量灌。5.4 除权除息导致K线跳变现象某股票今天10派5盘中分钟K线和昨天比突然出现一个巨大的向下跳空程序没报错但回测结果严重失真。原因通达信行情服务器返回的K线是未复权数据除权除息当天价格天然跳空。如果你拿这套数据直接做回测等于把分红当成暴跌。解决采集端不做复权但在存储端要增加一个adj_factor字段每天收盘后从服务器拉取复权因子存入。回测时用前复权公式计算前复权价 未复权价 × 当前因子 / 历史因子。有了这个字段回测数据才能跨日比较。5.5 高频写入导致磁盘IO吃紧现象采集程序运行一段时间后整个机器变卡磁盘IO一直在95%以上SQLite数据库文件越来越大。原因写入过于频繁而且SQLite没有开启WAL模式每次写都触发fsync。另外旧数据没有清理机制表无限膨胀。解决开启WAL模式把写入合并成批量事务同时做表分区管理比如按月份分表超过3个月的旧表直接归档删除或用外部列式存储保存。记住一点实时采集系统落库的是最近N天的热数据历史数据不应该是数据库无限膨胀的理由。5.6 市场代码颠倒现象订阅沪市股票但收到的数据全是乱码或者价格明显不对甚至收不到任何分笔。原因市场代码和股票代码的对应关系搞反了。深市是0沪市是1但有些接口的描述里把“深圳”写在第一位看文档的人想当然认为深市是1。解决在订阅前做一次自检用已知的平安银行深市000001和浦发银行沪市600000各订阅一只对比收到的价格和软件界面显示的价格不一致就交换market参数。这类问题不是看文档能解决的文档写得不一致的时候以实际线上数据为准。6. 进阶给采集器加增量缓存与质量校验让数据能看图也能回测增量缓存是我后期加上去的组件解决的是“断线重连后数据补齐太慢”的问题。思路很简单在内存里维护每个订阅标的的最后一条分笔时间重连后不直接拉全量而是按缺失时间窗增量拉取。拉取的时候用GetDataByTimestamp这类接口不同版本的dll接口名略有差异把断线期间的数据从服务器回补回补完再继续实时订阅。配合SQLite主键里的date_tag ts重复数据会被主键约束挡掉不会污染库。质量校验方面我在实践中沉淀了三套规则。第一交易日内的分笔累计成交额和收盘后服务器推送的日线成交额对比偏差超过0.5%就说明有丢数据。第二每只股票的日内最高价和最低价必须满足“最高价≥最新价≥最低价”不满足说明快照顺序乱了或者字段错位。第三相邻两条分笔的时间戳差值大于60秒但价格没有变化时标记为“疑似断线静默期”这个东西不是错误但你要知道它存在不然做连续性分析的时候会以为市场停盘了。这套系统做到最后你会发现TdxHqApi.dll本身并不难难的是把“实时”“完整”“可回测”这三个词同时落地。我的习惯是每天收盘后花10分钟看一下校验报表确认当天数据质量达标才离开。数据采集是后面一切策略的根基脏数据进去策略再漂亮也是白搭希望帮到你。本文还有配套的精品资源点击获取