尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#调用通达信TdxHqApi.dll实现实时行情采集与量化数据落地

C#调用通达信TdxHqApi.dll实现实时行情采集与量化数据落地 简介基于TdxHqApi动态库实现的实时数据采集器StockRealData是一份面向个人开发者、金融行情爱好者以及通达信接口编程初学者的实战型资料主要解决实时行情数据获取、解析与多语言调用演示等问题。资源以zip压缩包形式发布共包含299个文件总大小约110.7MB其中84个C#源文件与21个Java源文件构成两套独立实现23个动态链接库提供底层接口支撑13个配置文件和23个文本说明帮助理解参数与流程此外还带有备份、文档、工程文件等多种辅助材料便于按需查阅。目前已有54人学习浏览虽属于小众分享但结构完整值得深入接触。通过对照C#与Java两套调用方式可以清晰掌握TdxHqApi的封装逻辑、事件回调机制以及跨语言互操作的关键步骤同时附带的项目配置、数据格式描述和备份内容能够帮助快速梳理实时数据采集的完整链路适合在本地环境中动手验证并二次开发从而提升个人对金融接口编程的综合实践能力。1. StockRealData 是什么用 TdxHqApi.dll 接行情要解决什么问题TdxHqApi.dll 的实时数据采集器 StockRealData一句话说就是用 C# 或 C 把这个通达信行情接口的 dll 调起来定时拉取沪深快照落成本地可回放的数据文件。网页行情接口延迟高、字段会被反爬干扰付费数据源又贵又封闭TdxHqApi.dll 这条路延迟低、链路完全可控适合自己维护盯盘面板、做盘口异动告警或者给量化回测补一份分钟级数据的工程师和量化小队。这个方案最大的门槛不在于业务逻辑而在于 dll 没有文档、调用约定不透明、加载失败时一脸懵。把这三关过了剩下的就是一个定时器加一套写盘代码。2. 看懂 TdxHqApi dll 的导出表函数、参数与第一次握手做这个采集器第一件事不是写代码而是先搞清楚手上的 TdxHqApi.dll 到底导出了什么函数。很多第三方维护的行情 dll 连说明书都不带函数名、调用约定、参数个数全靠猜。猜错一个调用约定C# 侧栈直接不平衡进程当场崩掉。所以先花十分钟把导出表拉出来后面能少走一整晚弯路。2.1 用 dumpbin 核对导出函数别靠猜dumpbin 是 Visual Studio 自带的工具打开“Developer PowerShell for VS 2022”就能直接用。假设 dll 放在 D:\stock 目录下执行dumpbin /exports D:\stock\TdxHqApi.dll输出结果里每一行对应一个导出函数重点看三列序号 Ordinal、地址 RVA、函数名 Name。很多常见版本能看到类似 TdxHq_Init、TdxHq_Connect、TdxHq_GetSnapshot、TdxHq_Disconnect 这样的名字但具体到你自己手上这份一定要以实际导出表为准不要照搬别人的代码然后抱怨跑不通。看导出表的时候顺便确认一件事函数到底是 __cdecl 还是 __stdcall。名字没被改成 _TdxHq_Initx 格式的一般是 __cdecl被编译器修饰过带 和参数字节数的就是 __stdcall。C# 的 DllImport 里 CallingConvention 配错轻则返回垃圾数据重则直接 AccessViolation。这个细节在 Lua 调用 dll 时也一样LuaJIT 的 FFI 里 cdecl 和 stdcall 写错回调一进去就崩没有任何中间态。2.2 初始化与连接句柄拿到后才能谈行情大部分 TdxHqApi 封装是状态机模型先初始化拿句柄再连接行情源连接成功后才能发请求。以 C# 侧调用为例调用顺序大概是IntPtr h TdxHq_Init(); if (h IntPtr.Zero) { // Init 失败先查 dll 依赖是否齐全 return; } int rc TdxHq_Connect(h, host, port); if (rc ! 0) { // host 是行情代理地址port 由你本地服务端配置决定 return; }Init 返回空句柄常见原因有三个dll 依赖的运行库缺失、机器上已经加载了另一份同名 dll、当前进程位数和 dll 位数不一致。Connect 返回非 0 时优先确认 host 和 port 配的是不是目标行情服务端而不是一上来就怀疑 dll 坏了。端口这个参数在网上的各种工程里写什么的都有务必以自己服务端实际监听端口为准。还有一点容易翻车这类 dll 多数不是线程安全的。你如果开四个线程同时发请求返回的包大概率互相串。采集器所有请求最好收敛到单一后台线程里做UI 线程只负责展示结果。判断你的 dll 是否线程安全最笨也最可靠的办法是导出表里找有没有 Lock、EnterCriticalSection 这类字样没有的话就按串行处理。3. 用 C# P/Invoke 调用 TdxHqApi dll最小可用的快照代码导出表看完了接下来写最小可用的调用代码。这里有一个很多新手不知道的细节DllImport 指定的路径是静态的它按 exe 目录、SetDllDirectory 目录、PATH 环境变量的顺序搜索 dll。想从指定外部目录加载 dll最省事的做法是先用 LoadLibrary 拿绝对路径预加载加载器会记住这个模块后续 DllImport 直接命中同一份实例。3.1 LoadLibrary 预加载与 DllImport 声明先看最小声明代码using System; using System.Runtime.InteropServices; internal static class Native { // kernel32 的 LoadLibrary 用来预加载绝对路径下的 dll [DllImport(kernel32.dll, CharSet CharSet.Auto, SetLastError true)] public static extern IntPtr LoadLibrary(string lpFileName); // 导出函数按实际导出表逐个声明这里以常见名字为例 [DllImport(TdxHqApi.dll, CallingConvention CallingConvention.Cdecl)] public static extern IntPtr TdxHq_Init(); [DllImport(TdxHqApi.dll, CallingConvention CallingConvention.Cdecl)] public static extern int TdxHq_Connect(IntPtr handle, string host, int port); [DllImport(TdxHqApi.dll, CallingConvention CallingConvention.Cdecl)] public static extern int TdxHq_GetSnapshot(IntPtr handle, string market, string code, ref SnapshotData data); } [StructLayout(LayoutKind.Sequential, Pack 1)] public struct SnapshotData { public int last; // 最新价按整数放大存储 public int volume; // 累计总手 public int bid1; // 买一价 public int bidVol1; // 买一量 public int ask1; // 卖一价 public int askVol1; // 卖一量 }LoadLibrary 预加载要放在第一次调用任何导出函数之前代码里可以写成静态构造函数static Native() { string dllPath D:\stock\TdxHqApi.dll; IntPtr h LoadLibrary(dllPath); if (h IntPtr.Zero) { throw new InvalidOperationException(LoadLibrary 失败); } }DllImport 的函数名和 EntryPoint 必须和 dumpbin 导出的名字完全一致。如果导出表里函数名带 后缀例如 TdxHq_Init0说明是 __stdcallCallingConvention 要改成 StdCall。这里最容易踩的坑是函数名写错了不会编译报错而是运行时抛 EntryPointNotFoundException日志里看着像 dll 修复的问题实际就是名字或调用约定不匹配。3.2 拉一条快照并解析价格缩放、市场代码与结构体布局请求单条快照的最小代码SnapshotData snap new SnapshotData(); int ret Native.TdxHq_GetSnapshot(handle, 0, 600000, ref snap); if (ret 0) { // 价格字段通常是整数放大除以缩放系数得到真实价格 double lastPrice snap.last / 1000.0; double bidPrice snap.bid1 / 1000.0; double askPrice snap.ask1 / 1000.0; Console.WriteLine($600000 last{lastPrice:F2} bid{bidPrice:F2} ask{askPrice:F2}); }市场代码和证券代码的约定不同 dll 差异很大。有的用 0/1 区分沪深有的直接传 sh600000/sz000001。结构体布局同样没标准字段顺序、int 还是 short、价格放大 100 还是 1000全看封装者怎么定义。我的做法是先用一只昨天收盘价已知的股票反推缩放系数比如 600000 昨天收 10.51接口读出来 10510那缩放就是 100 倍再拿盘中价格验证一次确认不是巧合。字段含义常见单位last最新价整数0.001 元或 0.01 元volume累计成交手数手bid1 / ask1买一价 / 卖一价与 last 同缩放bidVol1 / askVol1买一量 / 卖一量手如果快照里读出来的价格明显离谱先不要怀疑数据源检查两条结构体字段顺序对不对、缩放系数对不对。这两个问题在 C# P/Invoke 里表现都是“有数据但数据是错的”比 LoadLibrary 失败更难排查因为没有任何异常抛出来。4. 把快照请求变成实时采集器轮询节奏、心跳与 CSV 落地跑通单条快照只是第一步实时采集器要解决三个工程问题用什么节奏请求数据、连接断了怎么自动恢复、数据落到本地什么格式。这三个问题解决不好程序能跑但不持久第二天早上起来数据文件里全是空洞。4.1 Timer 轮询加批量请求避免被行情服务器限流采集器常见的做法是 System.Timers.Timer 做统一心跳每次触发时遍历自选股列表逐条请求。一个可用的循环骨架长这样private readonly List(string market, string code) _watchList new() { (0, 600000), (0, 600036), (1, 000001), }; private readonly System.Timers.Timer _timer new(1000); private int _busy; public void Start() { _timer.Elapsed (_, _) { // 上次采集还没结束就跳过防止重入 if (Interlocked.CompareExchange(ref _busy, 1, 0) ! 0) return; try { CollectOnce(); } finally { Interlocked.Exchange(ref _busy, 0); } }; _timer.Start(); } private void CollectOnce() { foreach (var (market, code) in _watchList) { SnapshotData snap new SnapshotData(); int ret Native.TdxHq_GetSnapshot(_handle, market, code, ref snap); if (ret 0) { SaveToBuffer(market, code, snap); } } }Timer 间隔的选取要结合行情源承受能力。1 秒一次、每次几十条请求一般不会触发限流如果代码池扩大到几百条单条请求模式就不合适了。常见做法是优先看 dll 有没有批量请求函数比如名字里带 List 或 Multi 的导出函数一次传 30 到 50 个代码返回一个数组。批量请求不仅节省时间更重要的是降低了被服务端判定为高频异常的概率。在请求日志里记录每条请求的耗时如果响应时延从几十毫秒涨到几百毫秒说明节奏太快把 Timer 间隔调到 2 秒或 3 秒。4.2 心跳保活与断线续传行情服务端不会一直容忍一条静默连接采集器必须有心跳机制。最简单的方案不是在固定时间发心跳包而是利用轮询响应本身作为活着的证据private int _lastResponseTick; private void CollectOnce() { // ... 请求代码 if (ret 0) { _lastResponseTick Environment.TickCount; } } private void CheckConnection() { int idle Environment.TickCount - _lastResponseTick; if (idle 5000) // 超过5秒没有任何成功响应认为连接已断 { Reconnect(); } }Reconnect 不能写得太激进每秒重试一次很容易把行情代理的连接表打满。我一般用指数退避第一次等 1 秒第二次等 2 秒最多等 30 秒成功后重置退避计数。重连之后还有一个隐蔽问题断线期间错过的那几秒数据如果直接忽略K 线会多出缺口。能接受的话就做本地时间戳去重接受不了就查 dll 导出表里有没有按序号续拉的请求参数有的话把最后一条数据的序号传进去。4.3 数据落地CSV 与按代码归档落盘格式我推荐 CSV按代码分文件文件按天滚动这样后续不管是 Pandas 还是 Excel 都能直接读。写盘不要逐条 File.AppendAllText高频采集下这会让磁盘 I/O 变成瓶颈。正确做法是每只股票维护一个 StreamWriter先写内存缓冲攒到一定行数再 Flushprivate readonly Dictionarystring, StreamWriter _writers new(); private int _bufferCount; private void SaveToBuffer(string market, string code, SnapshotData snap) { string line ${DateTime.Now:yyyy-MM-dd HH:mm:ss.fff},{code},{snap.last / 1000.0:F3},{snap.volume},{snap.bid1 / 1000.0:F3},{snap.ask1 / 1000.0:F3}; if (!_writers.TryGetValue(code, out var writer)) { string file $data/{DateTime.Now:yyyyMMdd}/{code}.csv; writer new StreamWriter(file, append: true); _writers[code] writer; } writer.WriteLine(line); _bufferCount; // 攒到 200 行刷一次盘避免进程崩溃丢太多数据 if (_bufferCount 200) { FlushAll(); _bufferCount 0; } }断线续传除了依赖行情协议支持本地也要留个后悔药。我在 data 目录里放一个 meta.json每次写盘后把每个代码的最后数据时间戳记进去启动时读取它跳过已经采集过的时间段。这个文件很小不用频繁写进程退出时统一保存即可。注意如果只有一个进程在写数据CSV 按天归档没问题如果你同时开了回放程序和采集程序记得加文件共享锁或改用 SQLite否则 Windows 下文件被占用的报错会查到你怀疑人生。5. StockRealData 常见问题排查dll 加载失败、dll 冲突与回调崩溃这类项目里 80% 的时间不是在写业务逻辑而是在处理 dll 相关的玄学问题。下面按我遇到过的出现频率排序每一条都是现象、原因、解决三段式可以当排查手册用。5.1 BadImageFormatException 与进程位数不一致现象程序一启动在第一次调用 TdxHq_Init 的地方直接抛 BadImageFormatException提示“试图加载格式不正确的程序”。原因手上这份 TdxHqApi.dll 是 32 位编译的而你的 .NET 项目在 64 位系统上默认以 x64 进程运行。64 位进程加载不了 32 位 dll这是硬性限制不是代码问题。解决把项目平台目标改成 x86。Visual Studio 里右键项目属性生成平台目标选 x86重新编译。也可以用 dumpbin /headers 看 dll 的文件头机器字段是 0x14c 代表 x860x8664 代表 x64确认一次后再改工程配置。5.2 找不到指定的模块现象LoadLibrary 返回 IntPtr.Zero用 Marshal.GetLastWin32Error() 查错误码是 126即 ERROR_MOD_NOT_FOUND或者 DllImport 直接抛 DllNotFoundException提示“找不到指定的模块”。原因这个提示误导性很强通常不是你的代码路径写错了而是 TdxHqApi.dll 自己依赖的运行库缺失。动态库加载是连锁反应主 dll 找到了但它依赖的 msvcp140.dll 找不到整个加载就失败。解决用 Dependencies 这个开源 PE 依赖分析工具打开 dll查看红色标出的缺失依赖项最常见的是 VC 运行库。到微软官网下载对应的 vc_redist.x86.exe 安装问题就消失了。别拿网上那些来路不明的 dll 修复工具往 System32 里丢文件那种修法修好一个往往又制造出新的 dll 冲突。5.3 DllImport 加载了同名的另一份 dll现象程序在自己的机器上运行正常部署到另一台机器后行为完全不一致比如快照数据全错、函数调用直接崩溃。原因DllImport 的搜索顺序是 exe 目录、SetDllDirectory 指定的目录、PATH 变量中的目录。目标机器的 PATH 里恰好有一个同名但版本不同的 TdxHqApi.dll进程启动时先加载到了那一份。解决这是 dll 冲突的典型场景用第 3 节提到的 LoadLibrary 绝对路径预加载可以彻底钉死模块来源。注意 LoadLibrary 必须发生在任何导出函数第一次调用之前最稳妥的位置是静态构造函数里这样加载器一定会复用已加载的实例。5.4 回调委托被 GC 回收导致 native 层崩溃现象使用推送回调模式的 dll 时程序能正常跑几十秒甚至几分钟然后毫无征兆地崩溃在 native 层抓 dump 一看堆栈在行情库内部完全看不出业务代码的影子。原因C# 把委托传给了 native 函数但没有变量长期持有这个委托。垃圾回收把委托回收了native 侧再回调时就调用了一个悬空函数指针崩溃位置随机的。解决把回调委托保存到一个 static 字段或至少存活于整个采集器生命周期的对象里。用局部变量或 lambda 直接传回调迟早翻车。如果是非托管函数指针C# 9 以上可以用 delegate* 语法持有方式更清晰但同样要保证委托实例不被回收。5.5 请求频率过高导致限流和断开现象刚开始轮询数据一切正常把代码池从 20 只加到 200 只之后响应时延越来越大最终连接断开之后怎么重连都是秒断。原因行情服务端对单连接请求频率有限制高频请求会被判定为异常轻则丢包重则封禁一段时间。解决拆到多条连接也不行常见做法是降低单连接频率优先用批量请求接口把 200 只股票打包成 4 到 5 个请求。如果服务端是自建的行情代理可以在代理里加一层缓存做拉取合并。重连时用指数退避每 1、2、4、8、16、30 秒重试一次不要做成每秒疯狂重连的“猴子重连”那个只会让封禁时间越来越长。6. 数据质量自检价格规则校验与 Tick 重放采集程序跑起来不代表数据能用接入策略之前先做两天自检。我的习惯是分两层单条数据合法性校验以及整体数据重放对齐。合法性校验挡掉脏数据Tick 重放挡掉时序空洞。6.1 盘口合法性校验每条快照保存前先过一道规则不合法直接丢弃并计数。买了某只股票为什么回测结果总是和行情软件对不上很多时候就是脏数据没有被挡在门外。private bool IsValidSnapshot(SnapshotData snap) { double bid snap.bid1 / 1000.0; double ask snap.ask1 / 1000.0; double last snap.last / 1000.0; if (bid 0 || ask 0) return false; if (bid ask) return false; // 买一高于卖一异常盘口 if (last 0) return false; return true; }价格跳动范围判断也值得加比如某股票上一笔 10.51 元下一笔拉到 99 元基本可以判定是解包错位。把这种单条异常记录到一个 suspicious.log方便事后复盘。6.2 Tick 重放对 K 线数据攒够一天后我通常写一个十分钟的校验脚本读当天每只股票的 CSV按分钟聚合 high、low、open、close然后和行情软件里同一只股票的分钟 K 线对比。重点看 high 和 low 是否被包含在行情软件的区间内出现任何一根超出 K 线范围的 bar顺着时间戳回去查原始 CSV检查是不是缺了一段数据导致聚合错误。这个验证逻辑也会暴露采集器的一个隐藏问题如果 Timer 线程偶尔卡顿几秒数据文件的时间戳会出现空洞分钟聚合时看起来像是一分钟内价格从 10.00 跳到 11.00实际是中间丢失了数据。解决的办法有两种要么把采集间隔缩短到 500 毫秒以下加大采样密度要么在校验脚本里统计每秒条数低于阈值的时段标红人工复查。现在的新项目我都默认把数据质量自检做成采集器的一部分而不是事后脚本。盘中每 5 分钟做一次合法性校验计数发现异常立即告警收盘后自动跑一次 K 线重放第二天早上看一眼报告再决定要不要把数据交给策略。这一步看着繁琐但经历过数据里埋着三天烂数据导致回测曲线虚高的事之后就再也不敢跳过了。希望帮到你。本文还有配套的精品资源点击获取
返回列表