简介面向量化开发、程序化交易及通达信接口集成人员的接口资料包围绕 Trade.dll 与 TradeX.dll 展开既包含老版交易接口也提供行情交易二合一接口核心思路是通过 TdxTradeServer 将动态库调用封装为 HTTP 接口便于在自建服务中完成自动交易与行情获取适合具备一定原生开发基础、需要把客户端接口接入自有交易系统的开发者参考。压缩包共 78 个文件约 61.61MB以动态库、授权文件、源码、工程文件、演示程序和说明文档为主另含运行库、服务器列表及多语言示例覆盖多种语言接入与运行环境配置。已有 6623 人学习适合用作接口联调、动态库调用验证、服务端封装与交易流程自动化的参考资料也可据此梳理新旧接口的目录组织、授权文件放置、依赖加载顺序及示例工程结构减少从零搭建和排错成本。1. 开盘前 9:15 那三分钟Trade.dll 与 TradeX.dll 到底解决什么问题集合竞价前的三分钟行情在跳、委托队列在刷很多人的第一版程序就卡在这里界面还能动单子发不出去回调一条不来。这类问题八成不是策略写错了而是没有把底层接口的职责边界理清。Trade.dll 是一个纯交易的 TDX 交易接口封装负责登录、下单、撤单、查资金持仓和回报推送TradeX.dll 是行情交易二合一接口把行情订阅和交易委托放在同一个连接上下文里省掉两个进程之间的数据搬运。适合做自动化委托、盘中监控、批量调仓、算法下单的从业者也适合想把行情触发和委托动作收进一个进程的开发者——前提是你清楚它为什么这么设计以及哪里会翻车。2. 两个 DLL 的分工导出表、调用约定与结构体对齐在动手写第一行代码之前先把这两个文件当成对象看不要当成一个“能下单的黑盒”。它们抽象层次不同适用的场景也不同。搞错这一层后面写的所有代码都要返工。2.1 Trade.dll 只干交易这一件事纯交易接口的设计哲学是单一职责连接柜台、维持会话、发委托、收回报。它不订阅行情不做行情分发也不关心行情快照怎么合并。这种拆分不是洁癖是运维需求——行情端可以随时重启交易端的会话不能断行情端处理几万条推送时交易的委托链路不应该被拖累。从接口形态看Trade.dll 常见导出函数分四族初始化与登录Init、Login、Logout、查询类QueryBalance、QueryPosition、QueryOrder、委托类Order、Cancel、回调注册类RegisterCallback、SetNotify。这四族函数基本按“先建连、再认证、再订阅、再操作”的顺序使用跳跃调用一般直接返回未登录错误码。选型判断很简单如果你的策略是定时调仓或人工确认后下单交易和行情分开部署更稳如果是行情满足条件瞬发委托行情到交易之间的进程间跳转是明确的开销要么走共享内存要么就用二合一。提示纯交易接口的会话通常有保活机制长时间不调用查询类函数会被柜台判定空闲而断链生产上一般用 1530 秒一次的轻量心跳查询来维持。2.2 TradeX.dll 把行情和交易合到一层的原因二合一的好处集中在三点。第一上下文统一一个登录态同时能拿行情、发委托省掉账户凭证和会话 ID 的跨模块传递。第二时序确定行情回调里拿到的最新价和随后发出的委托落在同一条内部消息链上不需要靠时间戳猜。第三部署简单一个进程、一份配置、一套日志个人开发和小团队维护成本明显低。代价也要讲清楚。行情洪峰和委托回报共用一套回调线程池时如果回调处理慢委托回报会被行情推送压住出现“单子已经成交了但回报延迟几秒”的假象。另一个问题是崩溃域耦合行情解析模块一旦异常交易进程一起退出。所以二合一适合中等频率、单账户或少量账户的场景高频多账户一般还是拆开用独立进程加可靠的进程间通道。实际选型里我会看两个指标每秒行情推送条数以及日内委托笔数。推送在千条每秒以内、委托几百笔的二合一完全够用推送上万条还要求委托低延迟的老老实实拆进程。2.3 先把导出表拉出来再看代码不要相信任何文档上的函数名直接看二进制。Windows SDK 自带的 dumpbin 就够用重点看三样导出函数名、函数修饰方式决定调用约定、依赖库决定运行时环境。# 1) 导出表函数名、序号、修饰形式 dumpbin /EXPORTS Trade.dll exports_trade.txt dumpbin /EXPORTS TradeX.dll exports_tradex.txt # 2) 依赖库看是否缺 VC 运行时或三方库 dumpbin /DEPENDENTS Trade.dll # 3) 位数x86 还是 x64决定宿主进程必须同位数 dumpbin /HEADERS Trade.dll | findstr /C:machine三条命令的逻辑说明/EXPORTS输出的是动态库对外暴露的符号表这是唯一可信的调用清单/DEPENDENTS列出的是加载期依赖缺一个就会在LoadLibrary时直接失败错误码通常是 126/HEADERS里的 machine 一行决定位数x86 的 DLL 塞进 64 位进程里加载必然失败而且报错信息往往很含糊。判断调用约定有个土办法导出名形如_Init8是 stdcall32 位下带 加参数字节数形如Init且没有修饰的多为 cdecl extern C形如?InitYGXXZ的是 C 修饰名需要用 undecorated 名或序号取地址。ctypes 里 stdcall 用WinDLLcdecl 用CDLL选错的表现是进程直接崩没有异常堆栈这是典型的血泪经验。常见的导出函数命名族大致如下实际名字以导出表为准功能族典型命名参数特征备注初始化Init / Open配置路径或结构体指针每进程调用一次登录Login / Logon账号、密码、超时部分实现需要二次确认查询Query*输出缓冲区指针同步返回fill或异步回调委托Order / Insert委托结构体指针注意数量单位撤单Cancel委托引用号需要原委托号订阅Subscribe / Reg合约代码数组有数量上限回调注册Register / SetNotify函数指针 用户上下文生命周期要管住注意导出表里如果同时存在InitA和InitW说明是宽窄字符两套Python 侧调用InitW要传c_wchar_p混用会读到乱码路径然后初始化失败。3. 跑通第一个连接位数匹配、初始化时序与最小骨架这一章的目标很朴素让程序能登录成功并且能干净地退出。别小看“干净退出”后面第五章有一条坑就是退出顺序导致的崩溃。3.1 三件套位数、运行时、工作目录位数问题前面提过这里说具体。宿主进程和 DLL 必须同为 32 位或同为 64 位。Python 用户尤其容易中招机器上装的是 64 位 PythonDLL 是 32 位的WinDLL直接抛OSError: [WinError 193] %1 不是有效的 Win32 应用程序。解决办法不是改 DLL而是换一个同位数解释器或者用 32 位宿主做一个独立的小服务主程序通过本地通道调用它。运行时依赖看/DEPENDENTS的输出。如果看到MSVCP140.dll、VCRUNTIME140.dll而目标机没有就要装对应的运行库如果看到某个三方行情或加密库那必须把这个库放在 DLL 同目录或系统能找到的路径。DLL 搜索顺序里有“应用程序目录”这一项所以最省事的做法是把所有依赖和 exe 放同一目录。工作目录也要定死。很多接口的配置、证书、日志路径是相对路径从 IDE 里启动和从计划任务里启动工作目录不同表现就是“我这儿好好的服务器上一启动就登录失败”。我一般会在程序入口处显式SetCurrentDirectory到 exe 所在目录。3.2 C 最小骨架从 LoadLibrary 到 Login下面这段是 32 位控制台程序的骨架函数名用占位名实际要按你的导出表替换。编译时用/MT静态链接运行时能省掉目标机装 VC 运行库的麻烦。// trade_min.cpp —— 32 位控制台程序DLL 与 exe 同目录 #include windows.h #include cstdio // 函数指针类型参数与返回值必须和导出函数严格一致 using InitFn int(*)(const char* cfgPath); using LoginFn int(*)(const char* account, const char* pwd, int timeoutMs); using LogoutFn int(*)(void); // 回调事件类型、载荷、用户上下文 using CbFn void(*)(int evt, const char* payload, void* user); using RegCbFn int(*)(void* user, CbFn cb); static void OnEvent(int evt, const char* payload, void* user) { // 回调里只做拷贝绝不做耗时操作 printf([evt%d] %s\n, evt, payload ? payload : ); } int main() { // 1) 加载找不到依赖时返回 NULL用 GetLastError 区分 126/193 HMODULE h LoadLibraryA(Trade.dll); if (!h) { printf(LoadLibrary failed: %lu\n, GetLastError()); return 1; } // 2) 取地址名字必须与 dumpbin 输出完全一致 auto pInit (InitFn) GetProcAddress(h, Init); auto pLogin (LoginFn) GetProcAddress(h, Login); auto pLogout (LogoutFn)GetProcAddress(h, Logout); auto pReg (RegCbFn) GetProcAddress(h, RegisterCallback); if (!pInit || !pLogin || !pLogout || !pReg) { printf(GetProcAddress failed\n); FreeLibrary(h); return 2; } // 3) 时序注册回调 - 初始化 - 登录顺序反了部分实现会丢早期事件 pReg(nullptr, OnEvent); if (pInit(./trade.ini) ! 0) { printf(init failed\n); FreeLibrary(h); return 3; } if (pLogin(ACC001, ******, 5000) ! 0) { printf(login failed\n); pLogout(); FreeLibrary(h); return 4; } // 4) 业务逻辑占位生产环境这里是消息循环 Sleep(3000); // 5) 退出顺序先登出再反注册最后释放 pLogout(); pReg(nullptr, nullptr); FreeLibrary(h); return 0; }逻辑说明整段的核心是四步时序——注册回调、初始化、登录、登出。回调要先注册是因为部分实现在 Init 阶段就会抛出配置加载事件注册晚了会丢。退出顺序反过来先 Logout 再 FreeLibrary中间留一点时间让内部线程收尾。参数说明Init的参数是配置文件路径相对路径基于当前工作目录Login的超时单位是毫秒设太短在网络抖动时会误判失败我一般给 5000RegisterCallback的user指针会原样回传给回调用它挂接自己的上下文对象避免全局变量。3.3 Python 侧用 ctypes 十分钟验证不是所有验证都要写 C。用 ctypes 做连通性验证快得多前提是类型声明写对。import ctypes as C import os # 32 位 DLL 必须由 32 位解释器加载先自检 assert C.sizeof(C.c_void_p) 4, 请换用 32 位 Python 解释器 os.chdir(os.path.dirname(os.path.abspath(__file__))) api C.WinDLL(r.\Trade.dll) # stdcall若导出为 cdecl 改 CDLL # 逐个函数声明签名argtypes 不写会按 int 传参指针会被截断 api.Init.argtypes [C.c_char_p] api.Init.restype C.c_int api.Login.argtypes [C.c_char_p, C.c_char_p, C.c_int] api.Login.restype C.c_int api.Logout.argtypes [] api.Logout.restype C.c_int # 回调类型WINFUNCTYPE 对应 stdcall 回调 CB C.WINFUNCTYPE(None, C.c_int, C.c_char_p, C.c_void_p) CB def on_event(evt, payload, user): # 回调运行在 DLL 内部线程这里只入队不碰业务 print(event, evt, payload.decode(gbk, ignore) if payload else ) api.RegisterCallback.argtypes [C.c_void_p, CB] api.RegisterCallback.restype C.c_int api.RegisterCallback(None, on_event) # 保持 on_event 的全局引用防止被 GC rc api.Init(b./trade.ini) print(init rc , rc) if rc 0: rc api.Login(bACC001, b******, 5000) print(login rc , rc) api.Logout()逻辑说明先做位数自检再改工作目录然后逐函数声明签名。argtypes这一步不能省——ctypes 默认把 Python 的 bytes 当char*传没问题但结构体指针和回调指针不声明就会被当成 32 位 int 截断表现为访问违例。参数说明C.c_char_p对应const char*传 bytes 不传 strC.c_void_p传 None 表示没有用户上下文回调对象必须保持模块级引用局部变量被回收后 DLL 再回调就是野指针这是最常见的一类崩溃。字符串编码用 GBK 兼容老接口的默认代码页UTF-8 传进去中文路径会乱。4. 行情订阅与下单回调线程模型、参数与回报匹配能登录只是起点。真正决定这套接口能不能上生产的是回调线程模型和委托回报的匹配逻辑。这一块写歪了压测时问题会成倍放大。4.1 回调注册调用你的函数是谁的线程DLL 内部一般有一到两个后台线程一个收行情推送一个收交易回报。你的回调函数就在这些线程上被执行不是主线程。这意味着两件事第一回调里不能做阻塞操作写大文件、同步网络请求、Sleep否则会堵住整个推送通道第二回调里不能直接调用下单撤单接口部分实现内部有锁回调线程持有读锁时再申请写锁就是死锁表现是程序卡住、CPU 归零、没有任何日志。标准做法是在回调里只做一件事把数据拷贝进无锁队列或线程安全队列交给业务线程消费。行情快照这种高频数据拷贝时只取需要的字段别整结构体 memcpy。import queue, threading q queue.Queue(maxsize200000) # 有界队列满了就丢保交易不保行情 def on_market(evt, payload, user): try: q.put_nowait((evt, bytes(payload))) # 只拷贝不解析 except queue.Full: dropped[0] 1 # 计数用于事后判断是否要扩容 def worker(): while running: evt, raw q.get() # 在这里解析、算指标、触发下单 handle(evt, raw) q.task_done() threading.Thread(targetworker, daemonTrue).start()逻辑说明生产端DLL 线程只做入队消费端业务线程做解析和决策两边彻底解耦。队列必须有界无界队列在行情洪峰时会吃光内存然后被系统杀掉比丢包更糟。参数说明maxsize按峰值推送速率乘上可容忍延迟来估千条每秒、容忍 10 秒就是一万量级留十倍余量dropped计数器要暴露到监控里丢包率超过千分之一说明消费端处理不过来了要么加线程要么减订阅。4.2 下单参数价格、数量与方向的单位陷阱委托结构体看着简单翻车点全在单位上。数量字段有的是“股”有的是“手”差一百倍价格字段有的是浮点元有的是整数分差一百倍买卖方向有的用0/1有的用1/2填反了就是反向开仓这个错误在实盘里代价极高。字段常见含义常见取值典型坑证券代码合约唯一标识600000带市场后缀的写法各家不同市场交易所标识0/1 或枚举与代码前缀不一致会被拒买卖方向买/卖0买 1卖 或 1买 2卖两套约定混用会反向价格类型限价/市价/对手方最优枚举整型市价单在部分柜台不支持委托价格限价单价格浮点元 或 整数分精度截断导致废单委托数量股数或手数整型单位错误差 100 倍委托引用号本地唯一号自增整数重复会导致回报串号下单函数调用本身很短真正的复杂度在参数组装。我一般会写一层适配内部统一用“整数分 股数 枚举方向”只在调用 DLL 前做一次转换把单位换算集中到一个函数里出错时只改一处。def build_order(symbol, side, price_yuan, qty_shares, price_type0): return OrderReq( codesymbol.encode(gbk), market0 if symbol.startswith((6, 5)) else 1, # 简单前缀判断实盘要查表 side0 if side buy else 1, price_typeprice_type, priceint(round(price_yuan * 100)), # 元 - 分四舍五入避免截断 qtyint(qty_shares), # 统一按股接口要手时在这里除 100 refnext_ref(), # 本地自增号回报匹配靠它 )逻辑说明所有外部输入在进入适配层前都归一化为内部单位适配层负责转成接口要的格式。四舍五入用round而不是intint(12.34*100)在浮点误差下可能得到 1233。参数说明price_type要按你的接口枚举来填限价一般是最稳的ref必须全局唯一且单调进程重启后要从持久化里恢复否则当天的回报匹配会串。4.3 撤单与回报匹配别拿时间戳当索引委托回报是异步的成交回报更晚。用“发出时间 代码”去匹配本地委托在并发下单时几乎必然串号因为两个委托可能落在同一毫秒。正确做法是本地委托引用号发单时自己生成并记入字典回报里带回同一个号用它做唯一索引。pending: dict[int, dict] {} def send(order): pending[order.ref] {state: sent, filled: 0, order: order} rc api.Order(byref(order)) if rc ! 0: pending.pop(order.ref, None) # 发送失败立刻清理别留脏数据 raise RuntimeError(forder rejected rc{rc}) def on_trade_report(ref, filled_qty, status): rec pending.get(ref) if rec is None: log.warning(unknown ref %s, maybe restored session, ref) # 断线重连场景 return rec[filled] filled_qty if status in (FILLED, CANCELLED, REJECTED): pending.pop(ref, None) # 终态清理逻辑说明pending是内存里的委托状态机发单登记、回报更新、终态清除三步闭环。发单返回非零必须立刻回滚登记否则这条脏记录会一直占着内存并让后续对账出现“幽灵委托”。参数说明status的枚举值要对着接口文档或实测抓出来别猜断线重连后本地pending会丢所以启动时要先QueryOrder把当日委托拉一遍重建字典这一步不做重连后所有回报都会变成 unknown ref。5. 避坑与排查五个血泪翻车现场接口跑通容易跑稳难。下面五条都是现象明确、原因清晰、解法可落地的按发生阶段从连接到退出排列。5.1 连接与登录阶段返回成功不等于可用现象一Login 返回 0紧接着 QueryPosition 返回未登录错误。原因通常是会话尚未就绪。部分实现里 Login 只是把认证请求发出去真正的柜台确认是异步回调返回 0 表示“请求已受理”而不是“已登录”。此时立刻发查询会被判定为无会话。解决方式是按状态机走Login 返回后不立刻查询等登录成功回调或轮询一个GetStatus直到返回已登录再执行后续动作并且设一个总超时超时就重连而不是无限等。现象二本地跑得好好的部署到服务器上初始化失败错误码看不出原因。八成是工作目录和路径问题。配置里的证书、日志、缓存路径是相对路径从计划任务启动时当前目录是system32文件自然找不到。解决方式是程序入口处显式切换工作目录到 exe 所在路径配置里所有路径改成绝对路径或基于工作目录的相对路径并且在失败时把完整路径打进日志。5.2 回调与线程阶段卡死往往没有堆栈现象三行情推送几秒后程序无响应CPU 占用掉到 0。这是死锁的典型特征。回调线程里调用了下单接口而接口内部的下单路径需要等待行情回调线程释放的锁两边互等。也可能是在回调里做了同步网络请求把推送通道堵死进而让内部缓冲区满、写线程阻塞。解决方式只有一条铁律回调只入队业务在别的线程做回调里不调用任何 DLL 的其他导出函数。如果确实需要在行情触发时下单把触发信号投递给业务线程由业务线程调用Order。现象四回调偶发崩溃地址访问违例重启后又能跑一段。ctypes 场景下最常见的原因是回调对象被垃圾回收。局部定义的CB装饰函数如果没有赋值给模块级变量函数返回后引用计数归零DLL 再回调就是野指针。C 场景下则是用户上下文指针指向了已经析构的对象比如把this传给RegisterCallback对象先于函数释放。解决方式是回调对象挂模块级全局生命周期覆盖整个接口使用期用户上下文用裸指针时确保对象析构前先反注册退出时先RegisterCallback(nullptr, nullptr)再释放对象。5.3 数据与退出阶段精度和顺序现象五收盘后进程偶发崩溃或者第二天启动提示会话残留。退出顺序错了。直接FreeLibrary而不 LogoutDLL 内部线程还在跑代码段已经被卸载下一跳就是崩溃。更隐蔽的是崩溃发生在退出路径上日志来不及刷盘看起来像“正常退出后无日志”。解决方式是固定退出序列停止接收新业务、等待在途委托终态、Logout、反注册回调、短暂等待内部线程退出100500 毫秒、最后FreeLibrary。这一套封装成shutdown()函数在所有退出路径包括异常和信号里都走同一个函数别在main末尾手写一遍。注意进程被强杀时上述顺序无法执行所以第二天启动时的“会话残留”清理要单独做——登录前先调用一次登出或会话查询把服务端的旧会话顶掉。6. 跑满一个交易日日志字段、压测口径与收盘对账接口稳不稳跑一个完整交易日才知道。我一般不看“能不能下单”看的是收盘后三组数据对不对委托笔数、成交笔数、持仓变动。对不上的那天日志就是唯一的后悔药。日志要记的字段不多但不能缺。每条委托至少记本地引用号、发出时间毫秒、代码、方向、价格分、数量、接口返回码每条回报记引用号、回报时间、成交数量、成交价、状态枚举原文。关键是引用号贯穿全链路和时间用毫秒时间戳出问题时能按号追一条委托的完整生命周期。压测别用真实委托去打用模拟环境或者最小数量反复撤单。我通常跑两种负载一是行情满订阅加空转看内存和队列水位会不会持续上涨涨就是泄漏二是每秒固定笔数的委托撤单循环看回报延迟的 P99超过 500 毫秒就说明回调通道被堵。第二种跑两小时队列dropped计数还是 0才算过关。收盘对账用十行脚本就够把当日日志里的委托按引用号去重汇总数量和方向和QueryPosition拉回来的持仓做差值比对差值不为零就打印出问题的那几条引用号。# close_check.py日志与接口持仓对账 from collections import defaultdict delta defaultdict(int) for rec in read_log(orders.log): # 只取终态记录 if rec[status] in (FILLED, PART_FILLED): sign 1 if rec[side] buy else -1 delta[rec[code]] sign * rec[filled] # 净买入股数 pos {p.code: p.qty for p in api.QueryPositions()} for code, d in delta.items(): if pos.get(code, 0) - d ! expected[code]: # expected 为昨日持仓快照 print(MISMATCH, code, log_delta, d, broker_qty, pos.get(code, 0))这段的重点不在代码本身而在口径日志侧只统计终态避免部分成交被重复计算持仓侧以柜台返回为准expected用昨日收盘快照不要用当日开盘快照否则会把隔夜变动算进去。对不上的第一个反应不是怀疑接口而是先查自己有没有漏记撤单和废单。从那以后我每次接一个新的交易接口都会先用模拟环境跑满一整天的空转加撤单循环把日志字段和退出顺序固定下来再谈策略。这套习惯救过我很多次——真正让人安心的不是接口文档写得多好而是收盘后账能对上。希望帮到你。本文还有配套的精品资源点击获取