简介:面向需要借助通达信DLL接口实现自定义选股策略的投资者与VC++开发者,这份资料以可编译运行的示例工程为核心,将通达信官方插件编程规范中晦涩的说明逐行注释,并修正了原有示例中MyPlugin.cpp的错误,降低了入门门槛。压缩包共45个文件、4.74MB,主要包含VC++ 6.0工程文件、插件源码、编译生成的dll与lib,以及doc格式的编程规范和自动交易伴侣用户手册;10张步骤截图直观展示配置与选股操作流程。目前已有6333人学习下载。读者既能对照说明文档快速定位关键代码,也可直接打开工程验证DLL插件效果,适合想理解通达信插件机制并在此基础上做二次开发的初学者。值得注意的是,示例仅为说明编程方法,距实盘应用尚有较大距离,据此投资需自担风险。 我最早接触通达信的DLL选股插件,是因为公式语言实在扛不住我的策略需求。平时用通达信公式写写均线、MACD这类标准指标很顺手,可一旦涉及多周期共振、自定义统计量、甚至想把核心算法藏起来不被别人抄走,公式编辑器里的那套语法就变得非常拧巴。后来花了一个周末把DLL插件这条路跑通,才发现所谓“源码级选股”并没有想象中神秘——核心就是按通达信的接口约定导出一个函数,然后用C/C++把选股逻辑写进这个动态库里。这篇教程不打算讲虚的,直接给你一份可以照着敲的通达信DLL选股插件源码,再配合注册、调用、调试的完整流程。适合已经会写基本通达信公式、又想往底层多走一步的朋友。
1. 公式语言扛不住的需求,DLL插件为什么能接住
1.1 公式语言的四道坎
通达信公式语言的上手门槛确实低,输入MA(CLOSE,5)就能得到一条均线,但低门槛也意味着低天花板。我长期用下来,总结出四个绕不开的痛点。
第一是计算性能。公式语言的循环能力很弱,嵌套循环写不了几层,遇到需要逐根K线回看几十根甚至百根做统计计算的场景,公式会非常吃力,选股时能明显感觉到卡顿。
第二是数据结构匮乏。公式里的变量本质都是一条序列,没有结构体、没有自定义函数封装、没有文件I/O。策略一旦需要“先算一个因子A,再用另一个因子B过滤A,最后还要把结果跟历史分布做对比”,公式代码会膨胀到几乎没法维护。
第三是外部数据访问困难。公式语言基本不提供“读本地文件”的能力,你拿不到自建的股票池、行业权重、财报因子这类数据。而DLL里你爱怎么读文件就怎么读。
第四是源码保护。公式源码在通达信里要么是明文,要么是弱加密,别人拿到你的指标文件就能把核心策略抄走。核心逻辑一旦编译进DLL,别人拿到的只是一个二进制文件,想要逆向的成本就完全不一样了。
1.2 DLL插件在选股链路里的真实定位
很多人以为DLL插件是来替代通达信的,这是个误解。通达信负责行情展示、选股调度、结果列表,DLL只负责一件事:把复杂的信号计算做成一个“函数”给公式系统调用。
打个比方,通达信公式系统像一个按规则运作的流水线,普通公式是流水线上的标准模块,而DLL插件是你自己单独建的一个黑盒加工车间。行情数据从流水线送进黑盒,黑盒算完后返回一条结果序列,流水线再根据这条结果判定是否选入股票。选股逻辑和界面完全解耦,这就是DLL方案最舒服的地方。
对比一下普通公式和DLL插件的差异:
| 对比项 | 普通公式 | DLL插件 |
|---|---|---|
| 开发语言 | 通达信公式语法 | C/C++ |
| 循环与算法能力 | 弱,嵌套受限 | 完整,无限制 |
| 外部文件读取 | 不支持 | 支持 |
| 核心源码保护 | 弱 | 编译进二进制 |
| 调试体验 | 弱,只能靠裸数据查看 | 打日志、用调试工具 |
| 上手难度 | 低 | 稍高 |
2. 通达信DLL接口约定:一个函数签名背后的规则
2.1 接口长什么样
通达信官方并没有公开一份正式的DLL开发SDK文档,但DLL公式接口的约定在开发者圈子里流传很广,协议本身非常固定。所谓“插件”就是一个普通Windows DLL,它必须导出一个指定签名的函数,通达信才会认得它。
统一约定如下:
extern "C" __declspec(dllexport) int WINAPI TdxFunc( int DataLen, float* pfOUT, float* pfINa, float* pfINb, float* pfINc);逐参数解释一下:
DataLen:传入的K线序列长度,也就是当前选股周期里有多少根K线。pfOUT:输出缓冲区,长度至少是DataLen,你的计算结果往这里面写就行。pfINa、pfINb、pfINc:三个输入序列。通达信在公式里调用DLL时传入的实参会依次对应到这里。比如选股公式里写TdxFunc(CLOSE, VOL, AMOUNT),那pfINa就是CLOSE序列,pfINb是VOL序列,pfINc是AMOUNT序列。- 返回值:返回1表示计算成功,返回0表示失败。失败时通达信会认为这次计算无效。
- 调用约定是
WINAPI,在32位环境下就是__stdcall。这一点不能写错,下面踩坑部分会重点讲。
2.2 只有三个输入序列,不够用怎么办
这个接口的输入通道固定只有三个,刚接触时我也觉得太受限。实际用下来发现,三个序列足够覆盖绝大多数选股需求,因为通达信公式语言本身可以帮你做“预加工”。
比如你想同时看“量比”和“价格突破”,完全可以在调用DLL之前,先把原始行情加工成想要的序列再传进去。VOL / MA(VOL, 20)算出一个量能倍数序列,CLOSE / MA(CLOSE, 20)算出一个价格位置序列,这两个序列各自传进DLL,DLL只需要专注做信号判定,不用关心底层行情怎么算。
如果确实需要策略参数,比如均线周期、突破阈值这种东西,那就不该每次改代码重新编译——直接在DLL里读取同目录下的配置文件就行,后面我会专门讲这套参数化配置的做法。
3. 手写一个均线多头排列选股DLL:完整源码拆解
3.1 开发环境准备
开发环境方面,我推荐直接用 Visual Studio 2022 社区版,免费,装C++桌面开发组件即可。也可以使用MinGW-w64,但VS对工程配置和调试更省心。
工程创建步骤:新建一个C++空项目,在配置管理器里把平台改成x86,也就是Win32,项目属性里把配置类型改成“动态库(.dll)”。这里有个硬性约束:通达信主程序目前是32位,所以它加载的DLL也必须是32位。直接编译成x64在选股时会提示DLL加载失败,这是新手最常踩的坑。
3.2 均线多头排列选股的完整源码
下面是一份可以直接编译的源码,逻辑是经典的均线多头排列:MA5大于MA10、MA10大于MA20,同时当日收盘价站上MA5。满足条件时输出1,否则输出0。
#include <windows.h> extern "C" __declspec(dllexport) int WINAPI MultiMA5_10_20( int DataLen, float* pfOUT, float* pfINa, float* pfINb, float* pfINc) { if (DataLen <= 0 || !pfOUT || !pfINa) return 0; const int SHORT = 5; const int MID = 10; const int LONG = 20; double sumShort = 0.0, sumMid = 0.0, sumLong = 0.0; for (int i = 0; i < DataLen; i++) { sumShort += pfINa[i]; sumMid += pfINa[i]; sumLong += pfINa[i]; if (i >= SHORT) sumShort -= pfINa[i - SHORT]; if (i >= MID) sumMid -= pfINa[i - MID]; if (i >= LONG) sumLong -= pfINa[i - LONG]; if (i < LONG - 1) { pfOUT[i] = 0.0f; continue; } double maS = sumShort / SHORT; double maM = sumMid / MID; double maL = sumLong / LONG; int ok = (maS > maM && maM > maL && pfINa[i] > maS) ? 1 : 0; pfOUT[i] = (float)ok; } return 1; }3.3 为什么这么写:几个关键设计点
extern "C"的作用是避免C++对函数名做名字修饰,让导出函数名保持板正。WINAPI就是__stdcall的宏定义,对应通达信要求的调用约定。这两个少了任何一个,通达信都可能找不到这个函数。
窗口均值用了滚动求和而不是每次重新累加。如果每次对20根K线求和,两层循环复杂度是O(n*m),而滚动窗口是O(n)。当前这个示例数据量不大,效率差距体感不明显,但选股是全市场扫票,函数会被调用成千上万次,能省一点是一点。
前19根K线输出0是因为均线还没满20根,此时信号无意义。这种“预热期”的处理逻辑要在代码里显式写好,否则会用不完整的均值算出错误的信号。
输出用0和1的开关值而不是连续浮点数,是为了让条件选股公式直接拿结果做判定。你当然也可以改成输出连续值,比如多头排列的强度得分,然后由选股公式设定一个阈值来过滤,这是一个扩展方向。
3.4 用.def文件固定导出名
这一步很容易忽略,但非常重要。32位环境下,即使写了extern "C",__stdcall导出函数名仍然可能被修饰成类似_MultiMA5_10_20@20的形态。为了防止通达信按原始名字查找时扑空,最好在工程里新增一个 .def 文件,内容如下:
LIBRARY MultiMA EXPORTS MultiMA5_10_20然后在项目属性 → 链接器 → 输入 → 模块定义文件里填上这个.def。编译后用Dependency Walker或者Visual Studio自带的dumpbin /exports查看导出表,能看到函数名干干净净地暴露在那里。
4. 把DLL装进通达信并跑通第一次选股
4.1 DLL文件要放对位置
把编译出来的MultiMA.dll复制到通达信安装目录下的T0002\dlls目录,没有这个目录就自己新建。部分券商定制版可能放在安装根目录下的dlls目录或者主程序同目录,不同版本略有差异。如果你用的是券商定制版,建议先翻一下它的目录结构,确认dll文件夹的位置。
如果通达信提示DLL加载失败,先检查文件是否被杀毒软件拦了,或者DLL文件被另一个进程锁住。后者在反复编译调试时很常见——通达信一直开着,旧DLL被加载到进程里,编译时就会报文件占用错误。
4.2 在公式管理器里注册DLL函数
打开通达信,进入“功能 → 公式系统 → 公式管理器”,新建公式,公式类型选择“DLL函数”。不同版本叫法可能不一样,有的叫“扩展函数”,有的直接叫“DLL”,但本质都是同一个东西。
关键填写项:
| 填写项 | 值 |
|---|---|
| 公式名称 | MULTIMA |
| 公式类型 | DLL函数 |
| DLL文件名 | MultiMA.dll |
| 调用函数名 | MultiMA5_10_20 |
保存之后,MULTIMA这个名字就相当于通达信内置函数了,可以在任何公式里被调用。
4.3 写条件选股公式并执行第一次选股
注意,DLL函数公式本身不是一个可以直接跑选股的公式,它只是一个函数。真正执行选股需要再包一层条件选股公式。
新建一个条件选股公式,代码如下:
TMP := MULTIMA(CLOSE, HIGH, LOW); XG := TMP = 1;第一行把三条价格序列传入DLL,DLL返回的0/1序列存到TMP。第二行定义选股条件:只有最新一根K线输出为1才选中。然后进“功能 → 选股器 → 条件选股”,选这个公式,执行选股。
4.4 用日志验证DLL真的在干活
第一次跑通后别急着高兴,先用一个最笨也最靠谱的方法验证结果:肉眼核对。选一只明显走出均线多头形态的股票,把TMP := MULTIMA(CLOSE, HIGH, LOW);单独放进一个副图指标,挂在K线图上看看信号位置是否合理。
开发阶段最强的调试手段是写日志。DLL跑在通达信进程里,没法直接断点,但可以在代码里加一行:
FILE* f = fopen("D:\\tdx_log.txt", "a"); if (f) { fprintf(f, "i=%d out=%f close=%f\n", i, pfOUT[i], pfINa[i]); fclose(f); }跑完选股,打开这个txt看看每个索引的输出与收盘价是不是对得上。注意日志有IO消耗,调试完记得移除,否则全市场选股会被拖慢。
5. 参数化配置与多策略改造:别让DLL变成一块铁板
5.1 用INI文件把均线周期变成可调参数
第一版DLL把周期常量写死在代码里,改一次参数就要重新编译一次,用起来太僵硬。实战中我更建议把参数放进INI配置文件,DLL每次被调用时读取设置。
读取配置有个细节需要注意:用GetModuleFileNameA获取路径时,如果传入NULL,拿到的是通达信主程序的路径,不是DLL路径。要拿到DLL自身路径,得先通过GetModuleHandleA拿到当前模块句柄再取路径,然后截到目录位置,拼接配置文件。否则配置文件放DLL旁边也没用,它会去通达信目录里找。
char szIni[MAX_PATH] = {0}; HMODULE hMod = GetModuleHandleA("MultiMA.dll"); GetModuleFileNameA(hMod, szIni, MAX_PATH); char* pSlash = strrchr(szIni, '\\'); if (pSlash) *(pSlash + 1) = 0; strcat_s(szIni, "MultiMA.ini"); int nShort = GetPrivateProfileIntA("MultiMA", "MA_SHORT", 5, szIni); int nMid = GetPrivateProfileIntA("MultiMA", "MA_MID", 10, szIni); int nLong = GetPrivateProfileIntA("MultiMA", "MA_LONG", 20, szIni);配置文件MultiMA.ini放在DLL同目录:
[MultiMA] MA_SHORT=5 MA_MID=10 MA_LONG=20以后想改成8、13、34日这样的斐波那契组合,直接改INI文件重启选股就行,不用重新编译DLL。这个技巧能让你在策略调参时省下大量时间。
5.2 扩展示例:布林带上轨放量突破选股
参数化之后,DLL就从一个单策略代码变成可复用的策略容器了。下面再给一个完全不同的策略思路,展示怎么在这个框架里换逻辑:布林带20日上轨突破,同时当日成交量是前一日成交量1.5倍以上。
for (int i = 19; i < DataLen; i++) { double sum = 0.0; for (int j = i - 19; j <= i; j++) sum += pfINa[j]; double ma = sum / 20.0; double variance = 0.0; for (int j = i - 19; j <= i; j++) variance += (pfINa[j] - ma) * (pfINa[j] - ma); double sd = sqrt(variance / 20.0); double upper = ma + 2.0 * sd; int volumeUp = (i >= 1 && pfINb[i] > pfINb[i - 1] * 1.5) ? 1 : 0; pfOUT[i] = (pfINa[i] > upper && volumeUp) ? 1.0f : 0.0f; }这里把CLOSE和VOL分别作为pfINa和pfINb传入,pfINc暂时闲置。整个计算逻辑完全是C++自由发挥的空间,这正是DLL方案相比公式语言的降维打击之处。
6. 实测踩坑记录:加载、崩溃与调试的那些事
6.1 高频问题速查表
把这段时间我和身边朋友踩过的坑汇总成一张表,建议收藏:
| 现象 | 根因 | 对策 |
|---|---|---|
| 通达信提示DLL加载失败 | 编译成了x64架构 | 平台改成x86重新编译 |
| 提示找不到指定函数 | __stdcall导出名被修饰 | 用.def文件固定导出名 |
| 选股结果全部为0 | 输出序列前段留0或阈值设置不当 | 加日志验证单根K线输出 |
| 行情软件直接崩溃 | DLL数组越界或空指针访问 | 所有传入指针先判空,写入前检查下标 |
| 编译时报文件被占用 | 通达信正加载旧DLL | 关掉通达信再重新编译 |
| 杀毒软件拦截DLL | 未知动态库被误判 | 添加白名单或减少可疑API调用 |
6.2 调用约定不一致这个坑
函数签名里的WINAPI不是随便写的,它决定了参数怎么压栈、函数结束后谁来清理栈。如果声明成_cdecl或者漏写了extern "C",轻则通达信找不到函数,重则栈被破坏导致行情软件闪退。
我建议从第一天开始就保持统一的模板:
extern "C" __declspec(dllexport) int WINAPI 函数名(...)编译完成后用dumpbin查一下导出表,确认函数名没有奇怪的@后缀,再交给通达信去加载,能省掉大量排查时间。
6.3 选股器里找不到公式
DLL函数公式注册成功后,它并不会出现在“条件选股”的公式列表里,因为它只是一个函数。必须在条件选股公式里通过TMP := MULTIMA(CLOSE, HIGH, LOW); XG := TMP = 1;这种包装方式去引用。很多朋友卡在这一步,以为是DLL没注册成功,其实是理解错了公式类型的分工。
另外,某些券商精简版客户端会禁用DLL公式功能,现象是注册时提示类型不存在或无法选择。遇到这种版本,换一个完整版客户端即可,源码本身不用动。
6.4 调试DLL的两种有效手段
断点没法直接打到通达信进程里,但调试手段还是有的。第一种是日志文件法,前面讲过,适合看逐K线的数值输出,直观但有IO开销。第二种是OutputDebugStringA配合DebugView工具,适合看流程走到哪一步,开销小,不需要打开文件。
OutputDebugStringA("MultiMA calc start");两种手段配合使用,定位问题非常顺。真实经历是,我碰到过一次选股结果偏少的问题,加了一天日志后发现是均线预热期处理有误,前19根K线里有一段输出了1,导致选股器取了错误的信号。这类问题如果不靠日志,靠猜基本猜不出来。
最后再分享一个习惯:每次改完DLL源码,先写一个小测试程序直接调用这个函数,传一组构造好的K线数据验证输出符合预期,再放进通达信跑全市场选股。这套开发链路我用了很久,最值钱的其实不是某一个DLL,而是你掌握了一种把任意复杂逻辑接入行情软件的能力。希望这篇教程能让你少走我走过的弯路。
本文还有配套的精品资源,点击获取