简介:面向金融软件开发与量化交易学习者,基于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或者1,0代表深圳,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分钟看一下校验报表,确认当天数据质量达标才离开。数据采集是后面一切策略的根基,脏数据进去,策略再漂亮也是白搭,希望帮到你。
本文还有配套的精品资源,点击获取