拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TdxHqApi实战指南:构建实时行情采集管道StockRealData

TdxHqApi实战指南:构建实时行情采集管道StockRealData

简介:基于TdxHqApi.dll实现的实时数据采集器StockRealData,定位于个人学习场景,面向金融数据接口学习者与量化入门者。资源通过DLL封装调用通达信行情接口,可用于体验实时数据采集的完整过程,适合结合源码研究接口协议与并发调度。压缩包共299个文件,约110.7MB,主体为C#工程源码(84个.cs文件)与Java调用示例(涵盖行情与交易API封装),另有23个DLL组件、配置文件、TXT说明、PDF文档及通达信数据格式样例,覆盖多语言混编与配置部署要点。当前已有54人学习浏览,样本虽小但便于交流。通过查看源码可掌握TdxHqApi的加载方式、实时数据解析与回调处理逻辑,也能参考其中的目录结构、项目配置和Web工程骨架,为二次开发或自研采集器提供起点。资源来自网络分享,仅限学习交流,请勿商用。

1. 借TdxHqApi抓实时行情:StockRealData解决的真正问题

做行情数据的人多半都经历过这个阶段:先用网页接口拉行情,拉几次就被限流;换第三方数据源,又要注册要鉴权,延迟还不稳定。我当时需要的是一个能稳定拿实时快照的本地数据源,最后落地的方案就是借用TdxHqApi这个dll——它是通达信行情客户端同源的协议接口,直接以dll形式暴露出来,整个采集器命名为StockRealData,相当于把行情协议封装成了一个自己可控的数据管道。它能解决的核心问题有两个:一是把行情数据从“客户端黑匣子”里解放出来,二是提供可编程的拉取和订阅方式,方便落库、回放和策略验证。适合的人群很明确:想研究行情协议的人、需要本地行情快照的量化爱好者、以及想搞懂Windows dll调用链路的工程师。

2. 把dll先跑起来:加载方式、初始化流程与连接状态机

2.1 为什么选TdxHqApi而不是网页接口

网页接口看起来简单,实际维护成本很高。请求头里有签名参数,服务端会校验User-Agent和Referer,抓得稍微频繁一点就弹验证码。就算你绕过了这些前置条件,拿到的数据也是延迟快照,做不了Tick级别的采集。换一个角度说,网页接口是给别人“看”的,而TdxHqApi是给程序“调”的,二者定位完全不同。

TdxHqApi走的是客户端协议,直接和行情服务器对话。你不需要理解TCP粘包、不需要自己拼协议包头,dll内部把这些全部消化掉了。调用的边界非常清晰:初始化、登录、查询、订阅、断开。这个“边界清晰”恰恰是它适合个人学习的原因——你可以把精力集中在数据采集和落库上,而不是陷进协议逆向里去。

需要说明的是,TdxHqApi有多个流传的版本,函数导出名不完全一致。拿到dll后第一步不是写代码,而是先看一眼导出表,确认你手上这个版本的函数命名。用Visual Studio自带的dumpbin就能看:

dumpbin /exports TdxHqApi.dll

常见做法是导出 Init、Logon、QueryData、Disconnect 这一组函数,但版本之间命名有差异,比如有的版本把登录写成 Logon,有的写成 ConnectServer。这一步花两分钟,能省掉后面半天排查时间。我一般会把导出的函数名截图存下来,和dll文件放一起,防止以后换版本时对不上。

2.2 静态导入与LoadLibrary:选择动态加载的三个理由

调用Windows dll有两条路:静态导入和动态加载。静态导入是在编译期把lib链接进exe,程序启动时系统自动加载dll,找不到就直接报“无法启动,缺少DLL”。动态加载则是在代码里用LoadLibrary手动加载,用GetProcAddress拿函数指针。对TdxHqApi这种场景,我强烈建议动态加载,原因有三个。

第一个理由是启动容错。动态加载可以捕获失败并给出友好提示,甚至可以在dll缺失时降级运行其他数据源,静态导入做不到。第二个理由是版本切换。不同版本的TdxHqApi函数名可能有差异,动态加载可以把你对函数名的依赖收敛到一个映射表里,换dll只改映射,不改业务代码。第三个理由是调试友好。你可以单步跟踪LoadLibrary的返回值,确认dll是否真的加载成功,而不是被系统启动器直接拦掉。

下面是动态加载的基本框架:

typedef int(__stdcall* TdxInitFn)(const char* logDir); typedef int(__stdcall* TdxLogonFn)(const char* host, int port, const char* user); HMODULE hDll = LoadLibraryA("TdxHqApi.dll"); if (!hDll) { DWORD err = GetLastError(); // err == 0x7E 表示找不到模块,0x7F 表示找不到入口点 return -1; } TdxInitFn tdxInit = (TdxInitFn)GetProcAddress(hDll, "Init"); TdxLogonFn tdxLogon = (TdxLogonFn)GetProcAddress(hDll, "Logon"); if (!tdxInit || !tdxLogon) { FreeLibrary(hDll); return -2; }

这段代码把dll的生命周期完全控制在自己手里。GetProcAddress返回值是空的,说明这个版本的导出表里没有对应名字,可以打印GetLastError确认。参数里host是行情服务器地址,port默认用7709,user传空串即可。LoadLibraryA用的是ANSI版本,路径里不要出现中文,否则在旧版Windows上可能加载失败。

2.3 初始化与登录:连接状态机的四个阶段

初始化不是一步到位的。TdxHqApi内部维护了一条连接状态链路,建议把状态机显式做出来,否则断线重连时你会迷失在回调地狱里。四个阶段分别是:Init(日志与全局环境)、Logon(建立连接并鉴权)、Query(数据查询或订阅)、Disconnect(主动断开)。

// 阶段一:Init 只需要传日志目录 if (tdxInit("C:/logs/tdx") != 0) { // 返回非0说明日志目录不可写,或者dll版本不支持该参数 return -3; } // 阶段二:Logon 同步阻塞,超时要单独处理 int loginRet = tdxLogon("119.147.212.113", 7709, ""); if (loginRet != 0) { // 常见返回码:1=超时,2=账户被踢,3=网络不可达 return -4; }

这里ToDesk有一点比较玄学:部分版本的Logon是同步阻塞的,网络不通时可能要等几十秒才返回,不能依赖它自身的超时。我一般会在调用前自己起一个超时线程,或者把Logon放到独立线程里,主线程最多等10秒。

登录成功后,建议立刻把连接状态标记成online,然后注册一个断线回调。常见做法是dll会在连接断开时回调你注册的函数,这个回调里只做一件事:把状态切回offline,然后触发重连。千万不要在回调里直接调用Logon,回调线程的上下文和dll内部锁可能冲突,容易出现死锁。正确做法是回调里抛一个事件给主线程,由主线程做重连。

3. 实时行情采集的核心:主动拉取与推送订阅的取舍

3.1 行情接口边界:能拿到什么字段

TdxHqApi暴露的行情字段比较完整,但不等于你随便拉都能拿到。以我手上的版本为例,单次查询能返回最新价、昨收、今开、成交量、成交额、买卖五档、时间戳这些字段。字段的粒度是快照级别,不是逐笔成交,这一点先要明确。

它适合做的是“准实时快照采集”,频率做到1秒一次问题不大,但做不了逐笔还原。接口边界还体现在股票范围上:沪深A股、基金、债券基本都能覆盖,但科创板部分字段在新旧版本dll里有差异,有的版本返回的买卖五档只有一档有效。建议拿到dll后先用一只股票试拉一次,把返回的字段和通达信客户端逐项对齐,确认字段顺序再写解析逻辑。

字段顺序这件事是最大的坑。TdxHqApi返回的不是带Key的JSON,而是一个顺序排布的结构化二进制块。不同版本之间字段顺序可能有调整,我踩过最痛的一次是v1版本把“昨收”放在第3位,v2版本把“买一价”插到了第3位,导致整条数据解析错位。所以每一版dll都要做一次字段对齐验证。

3.2 按需拉取:GetSecurityQuotes的参数与频率控制

主动拉取适合股票数量少、采集频率低的场景。函数名一般是GetSecurityQuotes或QueryData,传入市场代码和股票代码,返回的快照填到传入的结构体里。关键参数有三个:市场代码、股票代码、超时时间。

struct Snapshot { float price; // 最新价 float lastClose; // 昨收 float open; // 今开 int volume; // 成交量(手) int amount; // 成交额(元) }; // 市场代码:0=深圳,1=上海 int market = 0; char code[8] = "000001"; Snapshot snap = { 0 }; int ret = tdxQuery(market, code, &snap, 3000); if (ret == 0) { // 拉取成功,snap里就是最新快照 printf("price=%.2f vol=%d\n", snap.price, snap.volume); }

第四个参数是超时毫秒数。服务端响应正常情况下30毫秒以内,但行情波动剧烈时可能变慢,超时设3000毫秒是一个比较稳妥的值。需要注意频率控制:拉得太快会被服务端视为异常,轻则断开连接,重则封IP一段时间。我实践下来,单连接每秒拉取不超过20次是安全的,如果你要采集500只股票,应该用订阅推送而不是轮询。

关于代码的标准化,股票代码要补零到6位。深圳和上海的市场代码不要混,深市是0,沪市是1,B股和基金也是这两个市场代码之一。传错了市场代码,接口不会报错,但返回的数值全是0,这类错误排查起来非常容易让人怀疑人生。

3.3 订阅推送:回调里做差分更新

股票数量一多,轮询就撑不住了。订阅推送是另一个路子:你把关注的股票列表注册给dll,服务端有行情变化时主动推给你。这时的核心是一个回调函数,dll每收到一条快照就调用一次。

// 回调函数:注意必须保持导出约定,且不能做阻塞操作 void __stdcall OnQuote(int market, const char* code, const Snapshot* snap) { // 只做记录,丢给工作线程处理 PostMessage(hWnd, WM_QUOTE_UPDATE, market, (LPARAM)(new Snapshot(*snap))); } // 注册回调 + 订阅股票 tdxSetCallback(OnQuote); const char* codes[] = { "000001", "600000", "600519" }; for (int i = 0; i < 3; i++) { tdxSubscribe(0, codes[i]); // 订阅直接传市场代码和股票代码 }

回调函数里切忌做I/O操作。打印日志、写数据库、甚至加锁都要尽量避免,因为dll内部很可能持有关键锁,你在这里阻塞会导致整个连接线程卡死。我处理的办法是回调里只PostMessage给UI线程或放入无锁队列,由独立工作线程负责落库。

选轮询还是订阅,核心看股票数量和实时性要求。数量低于50只、频率低于1秒一次,轮询更简单;数量大、要求秒级推送,订阅是唯一选择。两个方案还可以组合:订阅负责实时推送,轮询只在启动时做一次全量补拉,把漏掉的快照补齐。这个组合也是我目前在生产环境里用的结构。

4. 数据落地:StockRealData的本地结构、调度与持久化

4.1 网络包到结构体:字节序与内存对齐

TdxHqApi返回的数据本质上是内存块,不同的语言拿到的视角不一样。C++可以直接用结构体指针强转,但有两个隐患:字节序和结构体对齐。行情服务器返回的整数和浮点数都是主机字节序,这点Intel平台没问题;结构体对齐不同编译器处理不同,必须用#pragma pack强制按1字节对齐。

#pragma pack(push, 1) struct RawSnapshot { float price; float lastClose; float open; int volume; int amount; int timestamp; // Unix时间戳 }; #pragma pack(pop) // 假设buf是从dll拷贝出来的原始数据 RawSnapshot* raw = (RawSnapshot*)buf; double ts = raw->timestamp;

#pragma pack(push, 1)的意思是让编译器不再自作主张地填充空隙。如果不强制对齐,结构体里float和int之间会被塞进填充字节,你解析出来的字段全是乱的,而且这个问题在Debug和Release版本下表现还不一样。

time_t类型在32位和64位下宽度不同,建议在结构体里用int存时间戳,解析后再转成time_t。Java或C#调用时,不需要关注内存对齐,但要关注字段顺序和类型宽度。

4.2 多股票轮询的调度与超时处理

单线程轮询多只股票最大的问题是阻塞:某只股票响应慢了,后面的全部被堵住。我一般用“一个线程按顺序拉数据 + 一个超时看门狗”的结构。看门狗线程每隔固定时间检查一轮是否完成,没完成就跳过本次,下一轮再补。

std::queue<std::pair<int, std::string>> tasks; std::atomic<bool> running{true}; void workerLoop() { while (running) { while (!tasks.empty()) { auto [market, code] = tasks.front(); tasks.pop(); Snapshot snap; if (tdxQuery(market, code.c_str(), &snap, 2000) == 0) { // 数据正常,交给存储层 storeSnapshot(code, snap); } // 无论如何都留出50ms间隔,避免触发限流 Sleep(50); } Sleep(100); // 队列空时等待 } } void watchdog() { auto last = steady_clock::now(); while (running) { if (steady_clock::now() - last > 5s) { // 5秒没有数据,强制重连 reconnect(); last = steady_clock::now(); } Sleep(1000); } }

这段逻辑的关键是Sleep(50)这行。查询本身很快,但频繁调用会让dll内部缓冲区溢出。加50毫秒间隔,一轮20只股票大约1秒跑完,刚好满足每秒快照的需求。还有一个细节:失败的任务不要立即重试,要放进一个retry队列,下一轮再处理,否则网络抖动会导致某个股票反复卡住当前轮次。

超时看门狗重连时,要把tasks队列清空,因为里面的任务是在旧连接上排队的,重连后再跑大概率失败。清空后再重新按顺序填充,配合订阅推送可以做到无感恢复。

4.3 持久化:SQLite还是CSV

数据落地我分两层:一层是原始快照的实时存储,另一层是清洗后的行情库。实时存储我用SQLite,因为它单文件、不需要单独服务,事务性能足够应对每秒几十条写入。清洗后的数据,如果只是给自己看的,直接导出CSV也行,配合Python做回放最方便。

SQLite写入前要开事务,否则每条Insert都是一次磁盘同步,写入速度会掉到1/10。

CREATE TABLE IF NOT EXISTS quote_snap ( ts INTEGER NOT NULL, market INTEGER NOT NULL, code TEXT NOT NULL, price REAL NOT NULL, volume INTEGER NOT NULL, amount INTEGER NOT NULL ); BEGIN TRANSACTION; INSERT INTO quote_snap VALUES (1700000000, 0, '000001', 10.25, 123456, 1269000); COMMIT;

写入时有个小技巧:时间戳统一用Unix秒,不要用本地时间的字符串,因为回放时跨时区、跨夏令时都不会乱。股票代码和价格之间不要拼接成字符串,拆成独立字段,查询时按ts和code建联合索引,回放性能会好很多。

CSV导出适合做数据交换,SQLite适合做查询。如果你只是自己研究,SQLite就够了;如果你打算把数据喂给其他程序,CSV反而更方便。两种格式的转换我写了一个小工具,每秒能转5万条记录。

5. 避坑记录:dll初始化失败、位数冲突与断线重连

5.1 加载时直接报错“找不到模块”或“初始化例程失败”

现象:LoadLibrary返回空,GetLastError报0x7E;或者报“error loading DLL,初始化例程失败”的弹窗。Oserror 1114就是这个问题的Windows版本:动态链接库(DLL)初始化例程失败。

原因:绝大多数问题不在TdxHqApi本身,而在依赖链。很多版本的TdxHqApi链接的是老版VC运行库(比如msvcr100.dll),目标机器没装对应的运行库就加载失败。另外api-ms-win-*.dll这类系统运行库缺失,也是同一类问题。

解决:先用Dependencies打开TdxHqApi.dll,看它的依赖列表,缺哪个补哪个。补的时候注意位数要匹配,x64系统不意味着能加载32位运行库到64位进程里。这个坑我踩过三次,每次都是Dependencies扫一眼就定位了,比瞎猜快得多。

5.2 32位和64位进程混用导致函数调用崩溃

现象:LoadLibrary成功了,GetProcAddress也返回了非空地址,但一调用就崩溃,或者返回全0数据。崩溃位置不固定,在Release模式下尤其难排查。

原因:dll本身是32位的,而你的调用程序编译成了64位。进程位数和dll位数不匹配,函数入口点会被错误解析,参数传递的栈布局完全错乱。更隐蔽的情况是调用了64位动态库中的32位入口。

解决:确认dll位数后,把工程平台改成对应的x86或x64。Visual Studio里项目属性→链接器→高级→“目标计算机”,或者直接在配置管理器里切换平台。32位dll就老老实实编译x86程序,不要因为系统是64位的就编译x64。这个问题有时候预编译头缓存会掩盖,清理解决方案后重新编译再验证。

5.3 杀毒软件把dll当木马隔离

现象:第一次运行正常,重启电脑后提示找不到TdxHqApi.dll,检查后发现文件被移到隔离区。

原因:行情dll的行为和木马有相似之处——注入进程、连接固定IP、大量网络发包。部分杀毒软件对未签名的dll敏感,TdxHqApi多数版本没有正规数字签名,很容易被误判。

解决:把dll所在目录加入杀毒软件白名单,不要放到系统目录里,放到自己工程目录下。如果公司电脑强制安全策略,建议换有签名的数据源,或者做好dll校验和恢复脚本。个人机器上,白名单加一次就够,但换dll版本后记得重新加一次。

5.4 连接正常但收不到推送数据

现象:登录成功,订阅函数调用返回0,但回调一直不触发,等几分钟还是没有数据。主动Query倒是正常返回。

原因:订阅接口对股票代码前缀有要求,有的版本要求代码带市场前缀(例如“0.000001”而不是“000001”),或者需要先注册回调再订阅,顺序反了就不推送。还有一种可能是服务端认为连接空闲,自动断开了长连接,但dll没有上报断线事件。

解决:订阅前确认回调已经注册,且订阅列表非空。代码前缀问题看dll自带的说明文档或示例代码,不同版本格式差异很大。连接空闲断线的问题,用一个30秒的轻量Query保住心跳,例如每30秒拉一次上证指数,连接就一直是活跃的。

5.5 不要对该dll执行regsvr32注册

现象:网上教程让执行regsvr32 TdxHqApi.dll,结果报“已加载,但DllRegisterServer入口点未找到”,或者注册成功但没有任何效果。

原因:regsvr32是给COM组件注册用的,要求dll导出DllRegisterServer。TdxHqApi是普通C接口dll,根本没有这个导出函数,注册没有任何意义。

解决:这类dll不需要安装、不需要注册,直接把dll放在exe同目录或者LoadLibrary指定路径就行。看到任何“需要注册才能使用”的说法,直接忽略。以后遇到任何非COM的第三方dll都用同一条判断标准:看导出表里有没有DllRegisterServer,没有就是不需要注册。

6. 进阶验证:把采集器接到一个回放校验流程里

TdxHqApi拉下来的数据直接入库,最大的风险是“看着正常,实际上字段错位”。体验一下:连续跑一个小时,收盘价和成交量都像那么回事,但策略一跑就亏,回查数据才发现字段早就错位了。所以我把一个回放校验流程做成了采集器的固定出口。

校验器不复杂,核心是对三个约束做检查:价格必须在合理区间、时间戳必须单调递增、同一只股票的快照序列号必须连续。价格区间用昨收的上下20%判断,超出就标红;时间戳单调性和序列号连续性用SQL查相邻记录。

import sqlite3 conn = sqlite3.connect("quotes.db") cur = conn.cursor() # 检查时间戳是否单调 cur.execute(""" SELECT code, ts FROM quote_snap WHERE ts < (SELECT MAX(ts) FROM quote_snap) AND ts >= (SELECT ts FROM quote_snap ORDER BY ts LIMIT 1 OFFSET 1) ORDER BY code, ts """) prev = None bad_rows = [] for row in cur.fetchall(): if prev and row[1] < prev: bad_rows.append(row) prev = row[1] # 检查同一股票的快照间隔平均值 cur.execute(""" SELECT code, COUNT(*), AVG(delta) FROM ( SELECT code, ts - LAG(ts) OVER (PARTITION BY code ORDER BY ts) AS delta FROM quote_snap ) GROUP BY code """)

这个校验脚本用Python 3跑就行,数据库文件换成你的实际文件。LAG窗口函数需要SQLite 3.25以上版本,老版本会报语法错误,可以用自连接替代。平均间隔超过2秒说明轮询频率不够,超过5秒说明连接有断点。

从那以后我每次改完采集逻辑都会强制走一遍这个回放校验流程,数据对不上就回头查协议版本而不是查代码。这个习惯救过我很多次。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表