简介:一套基于TdxHqApi动态库封装的实时行情数据采集器工程,面向熟悉证券行情接口或个人量化学习的开发者,用于解决实时行情获取、数据解析与本地落盘等问题。项目融合C#、Java与C++组件,DLL负责底层API通信,CS文件承载业务逻辑与界面,Java文件提供跨平台调用,配合config、xml等配置便于快速部署。压缩包共299个文件,大小约110.7MB,涵盖源码、编译后的class与库文件、说明文档及多种数据格式参考,目录结构兼顾工程开发与学习查看。内容包含Web入口、交易接口封装与行情解析等核心模块,可帮助读者理解TdxHqApi的调用方式、实时数据采集流程及多语言工程组织方法。目前已有54人学习浏览。资源源自网络分享,仅供个人学习交流,请勿商业使用,若涉及版权请联系删除。
1. 实时数据采集器StockRealData是什么:一个dll调用流程的封装
一提到TdxHqApi这个dll,做过A股行情对接的人多半会心一笑——它就是通达信对外提供的一个C风格动态链接库,用来从行情服务器拉取实时报价和K线。StockRealData这个名字听起来像中台系统,实际就是把TdxHqApi.dll的调用流程封装成一个能持续运行的采集器:初始化、连接登录、循环查询、解析二进制结构体、把数据落盘或转发出去。做量化回测需要本地行情缓存、做盯盘工具又不想买付费数据源、或者想在自己的软件里复用通达信行情的人,都会用到这个方向。这篇笔记只讲我对这套接口的落地经验,从函数声明写到参数设置,再到踩过的坑。
2. 先看懂TdxHqApi dll的接口契约:数据结构与登录/查询流程
TdxHqApi这一类接口和常见HTTP接口最大的不同在于:它走的是TCP私有协议,请求和响应都是定长二进制结构体,没有JSON、没有XML,甚至连个正经的SDK头文件都要自己整理。你直接拿dumpbin /exports导出函数列表,能看到一串以TdxHq_开头的导出函数,剩下的就是靠抓包或者同行分享的声明把协议补齐。
2.1 接口能干什么:五档盘口、分时、K线与股票列表
这个dll对外提供的核心能力基本可以分成四类:实时快照、历史K线、分时成交、基础信息。实时快照对应的是TdxHq_GetSecurityQuotes,一次请求能带多个股票代码,返回最新价、昨收、今开、最高、最低、成交量、成交额、买卖五档价量等字段;历史K线对应TdxHq_GetSecurityBars,可以拉日线、周线、月线、一分钟、五分钟等周期数据;分时成交则通过专门的函数逐笔拉取。股票列表用TdxHq_GetSecurityList按市场分页获取,做全市场扫描时很常用。
这里要特别注意TdxHqApi接口的「一个连接只能绑定一个行情服务器」设计。深交所和上交所的行情服务器地址甚至端口都可能不同,所以大部分人的做法是同时维护两条连接,一条管上海(market=1),一条管深圳(market=0)。接口返回的结构体里通常不会有「市场」字段,而是靠你自己发起请求时的入参区分,字段里的股票代码往往也只是一个纯数字的代码,比如600000,需要配合外部逻辑拼成SH600000这种全量格式。
2.2 调用生命周期:初始化、登录、查询、断开
无论用什么语言包装,TdxHqApi dll的调用顺序都绕不开下面这个流程:
- 先调用
TdxHq_Init完成dll内部的资源初始化,这一步一般只需要一次。 - 注册回调函数,用于接收连接状态变化、行情推送等异步事件。这个函数指针在C#里要小心,不能让它被垃圾回收器回收,否则回调触发时程序会直接崩掉。
- 调用
TdxHq_Login,传入行情服务器IP、端口、用户名和密码,等待回调里返回登录结果。用户名密码通常是预留的占位参数,但程序不会校验空字符串。 - 登录成功后进入查询循环,调用
TdxHq_GetSecurityQuotes或TdxHq_GetSecurityBars拉数据,每次调用前设置超时时间,调用后检查返回值。 - 程序退出时调用
TdxHq_Disconnect和TdxHq_Release,把连接断开、释放资源。
伪代码大致是:
TdxHq_Init() TdxHq_SetCallback(OnConnected, OnData, OnError) TdxHq_Login("11.22.33.44", 7709, "user", "pwd") loop: TdxHq_GetSecurityQuotes(request) parse(response) sleep(interval) TdxHq_Disconnect() TdxHq_Release()这条流程里的坑集中在回调机制上。TdxHqApi的回调不是每个请求同步触发的,部分版本把行情数据分成了「主动推送」和「请求应答」两种模式。如果你只调了请求函数却没开推送标志,部分行情可能要等下一个心跳周期才返回来。另一个容易漏掉的是登录成功并不代表可以立刻发请求,服务器端需要几十毫秒建立会话,冒然发送数据会被当成非法包踢掉。
3. 用C# P/Invoke把StockRealData的最小实现跑起来
C#调用TdxHqApi dll是最常见的选择,因为写界面、存数据库、对接Excel都方便。难点在于P/Invoke声明和结构体布局。TdxHqApi是32位dll,如果你的项目编译成x64,运行时会直接报BadImageFormatException,所以第一步不是写代码,而是把项目的「平台目标」改成x86。
3.1 核心DllImport声明与结构体映射
先看写好的P/Invoke声明,这是整个StockRealData能跑起来的地基:
[DllImport("TdxHqApi.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] public static extern int TdxHq_Init(); [DllImport("TdxHqApi.dll", CallingConvention = CallingConvention.StdCall)] public static extern int TdxHq_Login(string serverIp, ushort serverPort, string userName, string password); [DllImport("TdxHqApi.dll", CallingConvention = CallingConvention.StdCall)] public static extern int TdxHq_GetSecurityQuotes(int market, string code, int count, IntPtr result); [DllImport("TdxHqApi.dll", CallingConvention = CallingConvention.StdCall)] public static extern int TdxHq_Disconnect();CallingConvention必须用StdCall,这是C/C++ DLL的默认调用约定。CharSet.Ansi是为了让字符串以GBK字节序列传给dll,如果手滑写成Unicode,行情代码传过去就变成一串乱码,服务器根本认不出来。
结构体映射要用LayoutKind.Sequential,字段顺序必须和dll内部C结构体完全一致。五档行情数据大致长这样:
[StructLayout(LayoutKind.Sequential, Pack = 1)] public struct TdxSecurityQuotes { public int market; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string code; public float price; public float lastClose; public float open; public float high; public float low; public int vol; public float amount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public float[] bidPrice; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public int[] bidVolume; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public float[] askPrice; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public int[] askVolume; }Pack = 1是字节对齐的关键。C#默认按8字节对齐,而C编译器在处理这种行情结构体时通常用1字节对齐,不写Pack = 1会导致后面的浮点数组字段错位,读出来的价格全是乱值。这个坑我见过不下三次,每次都有人在论坛上问「明明登录成功了,为什么价格是天文数字」,十有八九都是对齐没设对。
3.2 采集循环与回调处理:把IntPtr变成结构化数据
TdxHqApi的行情查询函数返回的是一个IntPtr指向的内存块,里面可能是单个结构体,也可能是连续多个结构体。连续多个时,需要自己按Marshal.SizeOf<TdxSecurityQuotes>()做步长遍历,把每一条数据还原成对象。写一个工具方法:
public static TdxSecurityQuotes[] ParseQuoteBuffer(IntPtr buffer, int count) { var result = new TdxSecurityQuotes[count]; int size = Marshal.SizeOf<TdxSecurityQuotes>(); for (int i = 0; i < count; i++) { IntPtr p = IntPtr.Add(buffer, i * size); result[i] = Marshal.PtrToStructure<TdxSecurityQuotes>(p); } return result; }实际操作中,如果TdxHqApi版本较老,返回的内存块可能在调用下一个请求后被复用,所以最好在拿到IntPtr后立即解析并拷贝字段,不要图省事只保存指针。采集循环本身不复杂,比较稳妥的节奏是:
while (!stopFlag) { IntPtr buf = IntPtr.Zero; int ret = TdxHq_GetSecurityQuotes(market, code, 1, out buf); if (ret == 0 && buf != IntPtr.Zero) { var quotes = ParseQuoteBuffer(buf, 1); OnQuoteArrived(quotes[0]); } Thread.Sleep(3000); }每只股票3秒查一次,一百只股票就是300秒一轮,如果要覆盖几千只股票,单线程肯定顶不住。常见做法是开多个采集线程,每个线程负责一组股票,但不要让超过三个连接同时打到同一台行情服务器,否则容易触发服务端的连接数限制,导致登录直接被拒。
参数说明:sleep(3000)不是随便拍的。对A股快照行情来说,3000毫秒能覆盖服务器端的刷新周期,又不会让请求频率高到被视为恶意访问。如果行情波动大,可以压到1000毫秒,但再用200毫秒以下就属于自找麻烦了,服务器照样按自己的节奏推数据,快不了多少还白白占用带宽和CPU。
4. StockRealData的参数怎么设:服务器、超时、心跳与编码陷阱
TdxHqApi的成败一半在代码,另一半在参数。这套接口不像HTTP服务有那么标准的RESTful风格,服务器地址、端口、超时、心跳间隔、编码这些都要自己摸索出一套合理的值。下面这个表是我经过多个版本验证后沉淀的参数组合,能覆盖绝大多数场景。
4.1 关键参数配置表与推荐值
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 平台目标 | x86 | TdxHqApi.dll通常为32位,x64进程直接报错 |
| 行情服务器 | 通达信官方行情服务的地址,端口7709为主 | 不同运营线路有不同IP,选ping值低的 |
| 登录超时 | 5000毫秒 | 小于3秒容易误判登录失败 |
| 查询超时 | 1000~3000毫秒 | 单次行情请求的等待上限 |
| 心跳/轮询间隔 | 3000毫秒 | 兼顾实时性与服务器压力 |
| 连接数 | 2(一个上海市场,一个深圳市场) | 不要超过3 |
| 字符串编码 | GBK等价UTF-8encoding=936 | 股票名称等中文信息必须转码 |
注意,行情服务器地址并不是一个固定IP,通达信的行情服务器有很多条线路,网络环境不同,可连的服务器差别很大。我一般会在程序里内置一个服务器列表,启动时逐个尝试登录,先到先得,这个思路比写死一个地址更抗网络波动。
4.2 中文编码处理:GBK转UTF-8,否则股票名称面目全非
dll返回的股票名称、板块名称等字段是GBK编码字节。C#里如果直接用Marshal.PtrToStringAnsi拿字符串,中文可能会变成「鎴伐」这种乱码。正确姿势是先把字节读成byte数组,再手动转码:
public static string DecodeGbk(IntPtr ptr, int maxLen) { byte[] raw = new byte[maxLen]; Marshal.Copy(ptr, raw, 0, maxLen); int realLen = Array.IndexOf(raw, (byte)0); if (realLen < 0) realLen = maxLen; return Encoding.GetEncoding(936).GetString(raw, 0, realLen); }Encoding.GetEncoding(936)在.NET Core/Linux上可能抛NotSupportedException,需要先注册代码页提供程序:
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);这个细节很多人都栽过。Windows自带的.NET Framework没这个问题,但一旦把采集器部署到Linux服务器上或者用.NET 5以上的版本,不注册Provider就直接抛异常,查起来很费劲。
4.3 日志与状态机:让采集器自己报告「我在哪一步」
StockRealData不能只是一个闷头跑的循环,最好在关键节点留下状态日志:Init成功、Login成功、Login失败、查询超时、断线重连。我的习惯是维护一个简单的状态枚举:
Disconnected -> Connecting -> Connected -> Querying -> Reconnecting每个状态的转换都记录时间戳和错误码。TdxHqApi的错误码往往是一串负数,不查表根本不知道什么意思。把这些错误码和自己总结的中文含义写进日志,后续排查速度能快一倍。
5. StockRealData常见问题排查:从dll加载失败到断线重连的五个坑
这一章是血泪经验汇总。几乎每一个新上手TdxHqApi的人都会在下面这几个坑里翻车,严重的一个接一个,熬通宵都是轻的。
5.1 提示找不到指定的模块或DllNotFoundException
现象:程序启动,DllNotFoundException直接抛出来,明明已经把TdxHqApi.dll放到了exe同目录。
原因:TdxHqApi.dll本身还依赖一批VC运行时库。有些精简版系统缺msvcr120.dll或api-ms-win-*系列dll,主dll就加载不进去。另外,你把dll放到了exe目录,不代表加载器能正确找到它,特别是当你的程序被某个安装包改过默认工作目录时。
解决:先用Dependencies工具看dll的导入表,把缺失的运行时库装上;然后把dll放在exe同目录之外,用绝对路径加载:
[DllImport(@"C:\StockRealData\bin\TdxHqApi.dll")]或者更稳妥一点,在进程启动时临时切换当前工作目录:
Environment.CurrentDirectory = @"C:\StockRealData\bin";5.2regsvr32注册失败或0x3错误
现象:有人习惯拿到dll先右键注册,结果弹窗「无法注册dll/ocx:regsvr32失败,错误码0x3」。
原因:TdxHqApi.dll不是COM组件,没有DllRegisterServer导出函数,regsvr32纯粹是白费功夫。错误码0x3通常意味着系统找不到指定路径,但根因还是注册方式不对。
解决:这个dll根本不需要注册,只要进程能加载即可。更需要注意的是,有些杀毒软件会把陌生dll直接拦截,注册表的AppInit_DLLs也可能干扰加载。我一般会先关掉实时防护,加载成功后加白名单。
5.3 行情回调触发但价格字段全是零或乱值
现象:登录成功,查询函数返回值也是0,但结构体里的价格全是0或者大得不正常。
原因:多半是结构体布局不对。Pack=1没设置,或者bidPrice之类的数组大小定义错了,比如服务器端是5档,你只定义了3档。还有一种可能是你只申请了单条结构体的内存,而dll返回的是多条连续内存,解析时越界读到垃圾数据。
解决:先打印Marshal.SizeOf<TdxSecurityQuotes>(),对比C结构体的大小。通达信快照结构体一般在150~250字节之间,如果差了几十字节,大概率是字段顺序或对齐错了。和服务器真实数据对拍时,可以先查一只停牌股票,它的价格应该是0,但lastClose不为0,这样能定位字段偏移是否整体错位。
5.4 用一段时间后连接被踢,进入死循环重连
现象:采集器运行半小时或两小时后,Login突然失败,程序不断重试,日志刷屏。
原因:高频查询触发了服务器端的限流,或者服务器端做了一次长连接回收,你的心跳包没有及时回。TdxHqApi对连接保活的判断有时模糊,断开是静默的,不主动发一单查询根本发现不了。
解决:轮询循环里加一个「连续失败计数」,超过3次就拉长重试间隔,从10秒起步,指数退避到60秒封顶,不要出现「断线、重连、秒断、再重连」的恶性循环。另外,登录成功后每隔10分钟主动发一次轻量查询,一是验证链路,二是让服务器认为连接还活跃。还要记得处理重连后的补偿逻辑——断线期间漏掉的行情,要在恢复后重新拉取一次快照,而不是继续傻等增量。
5.5 回调委托被垃圾回收,程序不定时崩溃
现象:程序能跑,但偶尔在毫无征兆的情况下崩溃,崩溃点总在回调函数里。
原因:C#里把方法传给dll作为回调委托后,如果这个委托对象没有其他引用,垃圾回收器会把委托回收,dll继续调用这块已回收的内存,直接访问违规。
解决:在类中用一个静态字段或者类级别字段保持委托引用,别用局部变量。比如:
private static readonly TdxHqCallBack _callback = OnServerCallback;把_callback注册给dll,保证整个进程生命周期内它都活着。这块机制有点玄学,但实打实是踩得最深的坑之一。
6. 进阶玩法:从「可以跑」到「值得信」
如果只是定时打印行情,这个采集器也就止步于玩具了。真正把它做成数据服务,我习惯再加三件事:数据落库、数据对拍、消费解耦。
落库我直接用SQLite,按交易日分表,表结构就是一个快照一个表,字段和结构体一一对应。落库时注意要把价格从float转成decimal再存,浮点误差在行情数据里很隐蔽,但对策略回测是致命的。转储频率不要按条写,攒50条或者间隔500毫秒批量提交一次,磁盘IO压力小很多。
数据对拍是我最建议新手做的事。拿StockRealData拉到的行情,和通达信客户端、或者同花顺/东方财富页面上的同一时点价格对一遍,误差应该在分拆价位上不超过1个tick。如果发现慢了几秒,大概率是轮询间隔太大,可以缩小到1秒;如果价格都对不上,就要回去检查结构体字段拿错了。
消费解耦的做法是在后台开一个BlockingCollection队列,采集线程只往队列里塞数据,消费线程负责入库或转发,两个环节互不拖累:
BlockingCollection<TdxSecurityQuotes> quoteQueue = new BlockingCollection<TdxSecurityQuotes>();采集线程满了就丢弃最老的数据,消费线程写库失败可以回头补。这个模式看着朴素,但能扛住多大并发都不怕,也方便以后改成WebSocket推给前端或者合并成分钟K线。
最后说一个我自己的习惯。做好重连带来的重复数据治理,是行情采集器能否长期稳定运行的分水岭。断线重连后服务器会从最新快照重新推数据,重复写入会导致K线被「凭空拉高」。我用(代码, 时间, 最新价)作为唯一键做插入,重复数据就当幂等跳过。这个习惯救了我很多次,数据仓库里的行情表干净得让人心安。希望这篇TdxHqApi的实战笔记帮到你,少踩几个我踩过的坑。
本文还有配套的精品资源,点击获取