简介:一套基于TdxHqApi.dll的实时股票数据采集器完整项目源码,面向量化交易开发者、行情数据研究人员及C#/Java混合技术栈学习者,解决从通达信接口获取实时行情与交易数据时的封装、解析和工程集成难题。压缩包共248个文件、约105.88MB,涵盖84个C#源文件、21个Java源文件、23个DLL、13个配置文件,以及文档、表格、数据样例和工程文件;其中hqApiJava、TradeXApiJava等核心类负责行情与交易接口封装,还带有通达信数据格式、分析家DAD格式相关处理模块。目前已有831人学习/浏览,适合具备一定编程基础、希望快速接入通达信实时数据的开发者。通过这套源码,读者可完整看到TdxHqApi.dll的调用方法、多语言互操作实现、项目构建与配置文件组织方式,以及不同市场数据格式的转换示例,可直接用作自研行情采集系统的骨架,也能为二次开发提供排错参考。
1. 实时数据采集器:TdxHqApi.dll 能把行情管道做到多短
把通达信的实时行情搬到自己的程序里,通常需要先处理繁琐的协议封装。TdxHqApi.dll 是通达信行情终端对外开放的接口库,里面把交易所连接、登录鉴权、订阅推送这些底层逻辑都封装好了。我这里要拆的这个实时数据采集器,就是借助这个 DLL 实现的一个轻量行情采集工具,代码量不大,却能完成连接、订阅、解析、落库一整条流程。适合做量化回测数据准备、盘中盯盘提醒、以及自建行情服务的开发者。它帮你省掉自己从零实现 TCP 协议连交易所的时间,但协议细节、回调线程、服务器参数这些坑,仍然需要你亲手踩一遍。
2. 调用链拆解:TdxHqApi.dll 的初始化、连接与数据推送
TdxHqApi.dll 本质上是一个 C/C++ 风格的 Windows 动态库,里面封装了通达信 P2P 行情协议。用熟之后你会发现,它的核心调用链其实非常短:初始化全局上下文,连接行情服务器,用账号登录,然后订阅你关心的股票代码,最后在回调里拿数据。真正让你翻车的,往往是链路上每一环的参数取舍和线程边界。
2.1 用 dumpbin /exports 先摸清 DLL 的家底
拿到 DLL 的第一件事,不是查文档,而是直接把它的导出表打出来看。我一般会在 Visual Studio 的“开发者命令提示符”里执行:
dumpbin /exports TdxHqApi.dll输出的 Export Table 里有一长串函数名。不同版本的 DLL 导出名会有差异,但结构基本逃不出下面几类:
| 功能分组 | 常见导出名 | 作用 |
|---|---|---|
| 初始化/释放 | TdxHq_Init, TdxHq_Exit | 创建和销毁全局上下文 |
| 连接管理 | TdxHq_Connect, TdxHq_Disconnect | 建立和断开与行情服务器的 TCP 连接 |
| 登录鉴权 | TdxHq_Login | 向服务器提交账号、密码、机构号 |
| 行情订阅 | TdxHq_Subscribe, TdxHq_GetSecurityQuotes | 订阅实时行情或主动拉取最新快照 |
| 历史数据 | TdxHq_GetHistoryData, TdxHq_GetMinuteData | 补充获取日线、分钟线 |
看到导出表之后,你可以在 Python 里用 ctypes 把这 DLL 包一层。注意一点:在 64 位 Windows 上调用约定没有 x86 那种 stdcall/cdecl 的区分,直接用 ctypes.WinDLL 加载即可;如果跑在 32 位进程里,遇到传入结构体的接口就要用 WINFUNCTYPE 显式声明回调函数类型。
import ctypes api = ctypes.WinDLL("TdxHqApi.dll") api.TdxHq_Init.restype = ctypes.c_int这样把导入层的蛋壳剥开之后,剩下的工作就是把每个函数的功能、参数顺序和返回码记录到你自己的调用文档里。老项目的常见做法是维护一个薄封装类,把 DLL 函数映射为 Python 方法,避免后面业务代码直接散落着 ctypes 调用。
2.2 行情是“推”过来的,不是“拉”出来的
很多人第一次用 TdxHqApi.dll 时有个误解:以为调用一次 GetSecurityQuotes 就能拿到持续变化的行情。实际上,DLL 内部维护着一条持续的 TCP 连接,行情服务器会主动把订阅过的代码的成交数据推送到本地。那一条 GetSecurityQuotes,更像是在读取 DLL 内部缓存里的最新快照,而不是发起一次网络请求。
这就引出了线程问题的核心:DLL 的内部接收线程会在行情到达时更新缓存,并触发回调。如果你在回调函数里做重活,比如写数据库、打印日志、拼接字符串,接收线程就会被卡住,轻则行情延迟,重则 DLL 内部缓冲区溢出导致连接断开。我一般会在回调里只做一件事:把原始字节拷贝到一个线程安全的队列,然后立刻返回。
import ctypes import queue from ctypes import WINFUNCTYPE, c_int, c_void_p raw_queue = queue.Queue() CALLBACK = WINFUNCTYPE(c_int, c_void_p, c_int, c_void_p) @CALLBACK def on_quote(raw_ptr, size, ctx): # 拷贝原始字节,不能直接保存 raw_ptr,等下次回调可能被覆盖 data = ctypes.string_at(raw_ptr, size) raw_queue.put(data) return 0这段代码里有一个重要细节:回调参数中的 raw_ptr 指针只在本枚回调期间有效。DLL 的下一次数据推送可能复用同一块内存,所以必须用 ctypes.string_at 马上把字节拷贝出来,再放进队列。把这事做对了,你的采集器在高频推送下就不会丢数据,也不会因为指针生命周期问题读到脏数据。
从调用链的角度看,整个数据流可以概括为:确认导出表,用 ctypes 加载,调用 Init 建立全局上下文,Connect 连服务器,Login 做鉴权,Subscribe 订阅代码,回调线程把原始包丢进队列,业务线程解析入库。这条链路理解透了,后面配置任何参数都不会心虚。
3. 手写 StockRealData 采集器:核心代码和参数说明
采集器的名字里带着 StockRealData,定位就是拿实时行情为主。这个环节我不讲抽象的设计,直接把可运行的骨架写出来:初始化、连接、登录、订阅、解析,每一步都配上该有的参数和注释。
3.1 初始化与登录:把连接参数说清楚
初始化是第一步,也是出错率最高的一步。TdxHq_Init 通常需要接收一个配置文件路径或空指针,里面会决定 DLL 把日志写到哪、缓存目录在哪、超时时间是多少。我把配置路径固定成当前目录下的 tdx.ini,这样排错时能直接看到日志。
import ctypes import sys dll = ctypes.WinDLL("TdxHqApi.dll") cfg_path = b"./tdx.ini" ret = dll.TdxHq_Init(cfg_path, None) if ret != 0: sys.exit(f"[FATAL] TdxHq_Init failed, ret={ret}")接着是 Connect。这个函数负责建立 TCP 连接,参数里最重要的就是行情服务器地址和端口。通达信分公开发布的行情服务器端口一般是 7709,但这个端口只负责行情推送,登录鉴权用的端口有时候是另外一组。我踩过的坑是:把数据库服务器端口和行情端口搞混,结果 Connect 返回超时。所以这个端口号必须从服务器列表配置里确认,不能只凭惯例。
host = b"119.147.212.181" # 这只是示例 IP,实际按你的服务器列表替换 port = 7709 # 行情端口 ret = dll.TdxHq_Connect(host, port, 5) if ret != 0: sys.exit(f"[FATAL] Connect failed, ret={ret}")Connect 的第三个参数我习惯传 5,表示 5 秒超时。这个值不能太小,否则服务器在弱网环境下稍有波动就会误判失败;也不能太大,否则一次网络抖动会让采集器卡很久。服务器 IP 列表建议放在外部配置里,不要写死在代码中,因为通达信的服务器 IP 可能因为网络调整而失效,留着配置项,换 IP 时只改配置文件就可以。
登录这一步比较特殊。很多公开行情服务器用的是固定游客账号,用户名和密码都是 guest 或者 123456,但机构号是必须传入的。机构号填错会直接返回鉴权失败,且这个错误在日志里常常不够明显,只显示一个非零返回码。
ret = dll.TdxHq_Login(0, b"guest", b"guest") if ret != 0: sys.exit(f"[FATAL] Login failed, ret={ret}")Login 的第一个参数对应机构或服务器编号,0 通常表示默认公共服务器。如果你是从自己的行情网关登录,这里要换成网关分配给你的编号。把这些基础参数搞清楚之后,连接链路就不会再给你添乱。
3.2 订阅行情并解析:把原始字节变成可计算的数字
连接与登录成功,接下来是订阅。订阅接口要做的事是把市场代码和证券代码编码成一个协议包,提交给 DLL。市场代码里常见的约定是:0 表示深圳,1 表示上海,2 表示北京。写代码时最值得注意的就是代码字符串的编码方式:老协议里用的是 GBK,不是 UTF-8。
def subscribe(dll, market, code): # market: 0=深, 1=沪, 2=北 # code: "600000" / "000001" req = f"{market}|{code}".encode("gbk") ret = dll.TdxHq_Subscribe(req, len(req), None) if ret != 0: print(f"subscribe failed: {market} {code}, ret={ret}") return ret这段代码的关键是编码。Python 的字符串默认 UTF-8,而 DLL 内部按 GBK 解析协议里的代码字段,如果你直接用code.encode()去发请求,遇到中文名称的股票会直接乱码,订阅返回的行情包解析出来全是错位数据。我一般会在代码里统一显式声明 GBK 编码,避免不同环境下默认编码不一致带来的坑。
解析行情包时,原始数据一般是结构体数组。常见布局里,价格字段用整数存储,需要除以 100 换算成元;成交量字段的单位是手,不是股;时间字段可能是东八区时间。下面是一个简化的解析套路,可以用 struct 快速把字节拆开:
import struct def parse_quote(raw: bytes): # 典型字段布局,实际以你抓包得到的协议为准 parts = struct.unpack(">H III f", raw[:14]) market = parts[0] price = parts[1] / 100.0 # 价格换算 volume = parts[2] # 成交量(手) amount = parts[3] # 成交金额 ts = parts[4] return { "market": market, "price": round(price, 2), "volume": volume, "amount": amount, "timestamp": ts, }这段代码里>H III f是大端字节序,表示 1 个无符号短整型加 3 个无符号整型加 1 个浮点型。为什么是大端?通达信的协议包在传输过程中普遍按大端序组织,你在小端序的 x86 机器上直接解会得到完全离谱的值,比如价格变成十几万。用 struct 显式声明>前缀就是为了消除这种字节序歧义。价格字段除以 100 的换算比例,不同版本的协议可能不同,有除以 100 的,也有除以 1000 的,拿到实际行情数据后先和行情软件做一次交叉验证,再定这个系数。
解析完的字典可以直接送往下一步处理:打印、入队、落库都可以。整个采集器到这里就已经跑通了一条最小链路:登录后订阅,订阅后回调,回调入队,业务线程解析。后面的章节里,我把这条链路放大到长时间运行场景,讲讲那些真正会让人头疼的故障怎么排查、怎么加固。
4. 避坑与排查:四个高频故障现场与修复方法
这一章全部来自实际跑程序的教训。每一条都给出现象、原因和解决方法,顺序基本按我踩坑的频次来排。
4.1 初始化返回非零,DLL 根本没加载起来
现象:程序启动后 TdxHq_Init 返回 -1 或 0xC0000135 这类错误,日志里什么都没有。
原因:这种情况十有八九是 DLL 的依赖项没装上。TdxHqApi.dll 并不是一个纯单文件库,它依赖 Visual C++ 运行库,老版本 DLL 还必须依赖 Microsoft.VC90.CRT 或 VC100.CRT。如果你在干净的 Windows Server 上跑,很容易因为缺运行库直接静默失败。
解决:先看 Windows 事件查看器里的应用程序日志,找到对应进程的错误模块,确认是否缺少 VCRUNTIME140.dll 或 MSVCP90.dll。然后安装对应版本的 Visual C++ Redistributable。装完之后再用 dumpbin /dependents TdxHqApi.dll 查看它依赖了哪些 DLL,逐一确认路径能被找到。
这个坑最迷惑人的地方在于:代码层面完全没做错,编译也能通过,但运行环境不完整。从那以后我每到一个新服务器,第一件事就是把 VC 运行库和常用依赖装齐,再开始跑采集器。
4.2 连接超时:服务器 IP 和端口都对,但就是连不上
现象:TdxHq_Connect 一直卡到超时,返回码是超时类错误,偶尔第一次能连上,重连时又失败。
原因:行情服务器的 7709 端口在企业网络里经常被防火墙拦掉,这是第一个原因。第二个原因更隐蔽:你爬到的服务器 IP 列表里有些是内网段 IP,只能从特定网络访问,放在普通宽带环境里根本不通。
解决:在命令行里先用 telnet 或 Test-NetConnection 验证端口通不通:
telnet 119.147.212.181 7709如果不通,用一张外网可用的服务器列表逐个替换测试。我一般会在配置里维护一个 IP 池,每次启动时按顺序尝试连接,把第一个成功连接的 IP 缓存到本地文件,下次启动直接用它。这样即使部分 IP 失效,采集器也能自愈。端口不通的排查顺序是:先本地防火墙,再运营商网络限制,最后才是服务器本身状态。
4.3 登录成功但行情一直没推送:订阅环节静默丢包
现象:TdxHq_Login 返回 0,连接也没有断开,但回调函数一个数据都没收到,程序像死了一样安静。
原因:订阅请求没有真正被服务器受理。常见原因有三类:一是订阅时传的代码格式不对,比如混入了小数点或者其他符号;二是市场代码和交易所不匹配,把上海股票写成了 0(深圳);三是交易时段之外订阅,服务器响应策略不同,非交易时段即使订阅成功也不会有行情推送。
解决:先缩小问题范围。订阅单只代码,例如沪市 600000,然后在交易时间验证。如果还是没有数据,用抓包工具看 DLL 有没有真的发出 Subscribe 协议包,包里的市场代码和代码字符串是否和预期一致。我的一个习惯是:订阅后主动调用一次 GetSecurityQuotes 拉快照,如果快照能拿到,说明连接和鉴权都没问题,问题就在推送路径或回调注册上。用这个思路逐步收窄,比盲目改参数快得多。
4.4 解析出的价格离谱或字段错位:字节序和编码两个老冤家
现象:收到的价格变成了几千块,或者股票代码字符串变成乱码,有时同一个 code 时而正确时而错乱。
原因:字节序错误。x86 是小端,协议包是大端,没做转换就会出现数据完全对不上的情况。另一个原因是编码,代码里的中文名、板块名这些字符串字段走 GBK,你按 UTF-8 解码就乱了。
解决:解析时统一用 struct 的>前缀声明大端序,字符串字段显式 decode("gbk")。在抓包调试阶段,把每条原始数据包打印成 hex 字符串,先人工核对前几个字节的字段含义。字段错位的结果往往非常有规律,比如价格总是成交量相邻位置的数据,这种规律性说明差了一个字段偏移,逐一对齐即可。
我自己的调试流程是:把原始包先存成 hex 文件,再写一个小脚本对照协议文档逐字节拆。这个办法虽然慢,但最可靠,因为协议文档和实际服务器版本往往有出入,只有实际字节不会骗人。
最终,采集器能不能稳定跑,取决于重连、增量和落库这三个要素,我把它们放在下一章展开。
5. 稳定运行与效率优化:重连、增量更新和落库设计
长时间运行的采集器最大的敌人是网络抖动和服务器重启。行情通道一旦断开,如果不做重连,整个数据全断;如果只做重连而不做增量补回,断开期间缺失的数据也会留下隐患。
5.1 断线重连:用状态机管住连接生命周期
单独一个重连代码块其实很容易写出 bug,因为重连期间的状态太多:连接中、已连接、已登录、订阅中、断开。我一般用一个小状态机来管理,避免在重连逻辑里堆 if/else。
class TdxReconnector: DISCONNECTED = 0 CONNECTING = 1 CONNECTED = 2 LOGGED_IN = 3 def __init__(self, hosts, market, code): self.state = self.DISCONNECTED self.hosts = hosts self.market = market self.code = code def poll(self): if self.state == self.DISCONNECTED: self._connect() elif self.state == self.CONNECTED: self._login() elif self.state == self.LOGGED_IN: self._ensure_subscribe() def _connect(self): for host, port in self.hosts: if dll.TdxHq_Connect(host.encode("gbk"), port, 3) == 0: self.state = self.CONNECTED return time.sleep(5) # 全部失败,等待下次轮询 def _login(self): if dll.TdxHq_Login(0, b"guest", b"guest") == 0: self.state = self.LOGGED_IN def _ensure_subscribe(self): subscribe(dll, self.market, self.code)状态机的价值在于:每个状态只做一件事,不会有多个线程同时进入 Connect 的竞争条件。poll() 建议放在一个 1 秒定时器里循环调用,不要在断线后用一个 while True 猛重连,那样服务器会直接封你的 IP。
这里还有一个检测断线的细节:很多 DLL 不会主动通知你连接断了,你需要定期调一个心跳接口或者 GetSecurityQuotes,如果返回值持续错误或连续 N 次超时,就强制把状态拉回 DISCONNECTED,触发重连。我在实际项目里是每 10 秒做一次心跳检查,连续 3 次失败才判定断线,避免单次抖动误触发。
5.2 增量更新:别把全天数据全量重拉一遍
行情数据有两种更新策略:全量快照和增量推送。TdxHqApi.dll 的订阅本身就是增量推送的基础,所以本地缓存层只需要维护“最新快照+增量流水”就够了。
我的做法是每次收到 tick 时,只在内存里更新这只股票的最新价、累计成交量、累计金额。每天开盘前先拉一次日线级快照,把前一天的收盘价、涨跌停价初始化好,盘中用增量 tick 不断刷新。这样即使跑了一整天,内存里的状态也只有很小的体积,重启后重新拉一次快照就能恢复全部状态。
snapshot = {} # (market, code) -> latest quote def on_parse(quote): key = (quote["market"], quote["code"]) snapshot[key] = quote # 直接覆盖,只保存最新状态这段代码虽然简单,却是增量更新的核心:覆盖而不是累加。如果你不小心把每个 tick 都往内存里塞,跑一个交易日常驻内存会多出上百万条对象,GC 压力直接拉满。最新状态永远只有一份,历史记录则交给数据库去存。
5.3 数据落库:SQLite 批量写入比逐条插入快一个数量级
行情数据落地最常见的选择是 SQLite,单机场景完全够用。真正的性能瓶颈在写入方式:逐条 INSERT 每秒只能处理几十条,把 100 条 tick 攒成一笔事务批量提交,吞吐可以提升到上千条。
import sqlite3 conn = sqlite3.connect("tick.db", check_same_thread=False) conn.execute(""" CREATE TABLE IF NOT EXISTS tick ( ts TEXT, market INTEGER, code TEXT, price REAL, volume INTEGER ) """) batch = sample_buffer # 队列中攒够的 tick 列表 conn.executemany( "INSERT INTO tick(ts, market, code, price, volume) VALUES (?,?,?,?,?)", [(q["timestamp"], q["market"], q["code"], q["price"], q["volume"]) for q in batch], ) conn.commit()check_same_thread=False 是这里的关键设置,因为行情回调线程和业务线程并不是同一个线程,不加这个参数,SQLite 会拒绝执行。批量提交的频率我一般控制在每秒一次或每 500 条一次,既保证了数据不积压,又不至于频繁 commit 拖慢线程。另外一个容易忽略的点是建表时给 (market, code, ts) 加上索引,否则跑一天下来,按代码和时间查询的复盘脚本会慢得让你怀疑人生。
落库以后,采集器的闭环才算完整:网络层有重连状态机,内存层有增量快照,硬盘层有批量写库。最后一步,也是很多从网上下的源码包里不会给你写的,是数据验证。
6. 采集完成后的验证清单:从单笔 tick 到全天记录都对得上
验证这块没有太多玄学,全看细心。我按时间维度整理了一份自检清单,每次上线新采集器或者更换服务器 IP 后,都强制自己走一遍。
开盘前必须确认:DLL 能完成 Init、Connect、Login 三步,返回码全为 0;系统时间与行情服务器误差在 5 秒以内,因为有些 tick 打的是本地时间,对时不准会影响后续回测;本地网络能通 7709 端口,telnet 不会卡住。
9:25 集合竞价结束后,第一件对比的事是 000001 和 600000 的开盘价,和行情客户端显示一致,说明主协议解析没错。开盘后随机抽 3 到 5 只股票,分别记录某个时刻的价格、成交量、成交金额,与实际行情软件手工对比一次。价格误差必须在 0.01 元以内,成交量误差在 1 手以内。如果对不上,优先检查价格换算比例和成交量单位,这两个最常翻车。
收盘后做日级校验:用采集到的全天最后一笔 tick 对比当日收盘价,再对比日线接口拉到的收盘数据,三者如果一致,说明全链路正确。另外检查一下数据库里当天 tick 条数是否合理,比如上证指数一天的复权样本大概是几千笔,如果只有几十笔,那大概率中间断线且重连逻辑没恢复正常。
我自己养成的习惯是把这些检查写成脚本,每天 9:30、11:35、15:05 自动跑一遍,输出一份简短的验证报告。现在每换一次服务器配置,我都会把这个脚本先跑通再上生产,再也不像刚接触 TdxHqApi.dll 那会儿,对着异常数据猜半天。希望这套验证清单也能帮你把采集器的数据质量管住。
本文还有配套的精品资源,点击获取