简介:面向通达信插件选股开发的VC++ 6.0示例源码与教程包,适合具备C++基础、希望理解通达信DLL插件接口和指标接口标准的量化爱好者与程序化交易开发者。压缩包共45个文件、约4.74MB,包含cpp/h源码、dll动态库、dsp/dsw工程文件、doc规范与用户手册,以及多张jpg操作步骤图,可对照完成工程打开、编译和插件加载验证。作者对原示例代码补充了注释,并修正了核心实现中的一处错误,使原本晦涩的接口说明更易理解;配套的编程规范、使用方法和选股结果截图,能帮助读者梳理DLL插件从注册、参数传递到文本输出的基本流程。已有6420人学习下载。需要说明的是,示例功能较简单,重在演示编程方法,距实盘应用尚有距离,适合用作接口研究、二次开发起步和排错参考,据此投资风险自负。
1. 通达信DLL选股插件源码:为什么老手宁可写 C 也不堆公式
收盘后翻两百只票,眼睛在 K 线和财务字段之间来回跳,手上一堆条件:量比、连续放量天数、均线斜率、换手率背离——这些条件单拎出来公式都能写,一旦串起来就变成嵌套七八层的 IF,改一个阈值要翻三屏,第二年自己都看不懂。这份通达信 DLL 选股插件源码要解决的就是这个场景:把整套判断逻辑从公式编辑器里搬到 C 代码里,编译成 DLL 挂进通达信的公式引擎,公式侧只剩一行XS_PICK(C,V,5,1.8),本质是给公式引擎加了一个自己写的内置函数。适合已经会用条件选股、但被公式复杂度和复用性卡住的从业者;也适合想把自己那套选股经验固化成可交付模块的人。VC++ 6.0 那一代的写法看着老,但在 32 位进程里它是最省心的组合。
2. 调用链路拆解:引擎从哪个目录把 DLL 拉进公式引擎
2.1 引擎怎么找到你的 DLL
通达信对自定义函数 DLL 的加载方式很朴素:认目录、认导出名,不认注册表也不认安装包。把编译好的 32 位 DLL 放进客户端安装目录下的扩展目录(常见路径形如安装目录\T0002\dlls\),重启客户端,公式引擎在初始化时会把该目录下所有 DLL 的导出函数扫一遍,登记成可调用函数。之后在公式编辑器里敲函数名,参数提示会跟着出来,跟内置函数没区别。
这里有两个容易忽略的细节。第一,函数名匹配是大小写敏感的,你导出XS_PICK,公式里写成xs_pick就找不到;我一般统一全大写加下划线前缀,避免和内置函数撞名。第二,扫描是启动时做的,换了 DLL 必须重启客户端,热替换不生效——这一点在反复调试阶段特别耗时间,所以下面会讲怎么用日志把调试周期压短。
提示:扩展目录的具体层级在不同版本里略有差别,源码包里通常会带一份目录说明,照着放,别自己猜。
2.2 参数怎么进、值怎么出:调用约定与返回类型
通达信调用自定义函数的方式是可变参数:引擎把公式里的实参按顺序压栈,同时把实参个数作为第一个参数传进来,函数里用va_arg逐个取。返回值是一个浮点数,公式引擎把它当成一个数值型函数结果参与后续运算——这意味着你的选股函数最终要收敛成"返回 1 表示命中、返回 0 表示不命中",或者返回一个连续的打分值,让公式侧用>阈值去做最终判断。
我倾向于后者。返回连续值的好处是:同一个函数既能当选股条件用,也能拿去做排序和分层。比如量价背离度返回 0 到 3 之间的一个数,公式里写XS_DIVERGE(C,V,20) > 1.5是选股,写XS_DIVERGE(C,V,20)放进排序指标就是排序,一份代码两处用。
参数类型上要留个心眼。绝大多数版本按 double 压栈,取参写va_arg(ap, double);但也有版本对整型实参做过优化,直接按键值传。稳妥做法是把公式侧的实参全部写成带小数点的形式,5写成5.0,让引擎统一按浮点处理,省掉一类玄学问题。这条不是理论推导,是踩过之后总结出来的。
2.3 为什么用 VC++ 6.0 编:运行时依赖与 32 位约束
现在还在用 VC++ 6.0,不是因为怀旧。三个现实原因:一是通达信客户端是 32 位进程,64 位 DLL 根本加载不进去,而 VC++ 6.0 默认就产出 32 位目标,不用折腾平台工具集;二是 VC++ 6.0 可以非常干净地静态链接 C 运行库(/MT),编出来的 DLL 丢到任何一台装了客户端的机器上都能跑,不会出现"开发机好好的,换台机器提示找不到某个运行时库";三是这套源码本身就是按 VC++ 6.0 的工程结构组织的,.dsp工程文件直接双击能开,省掉迁移成本。
代价也要说清楚:VC++ 6.0 的 C++ 标准支持停留在上个世纪,std::vector之类的能不用就不用,字符串处理老老实实用char缓冲区加sprintf,注意sprintf的越界问题,用_snprintf并在末尾手动补\0。另外它没有 64 位整数类型的完整支持,别在 DLL 里做超大数运算。
2.4 一份最小可编译骨架
源码包里通常会有一个统一导出声明的头文件,我把它简化成下面这个样子,方便你先跑通再往里塞逻辑:
/* tdx_export.h —— 统一导出声明,避免 C++ 名字修饰 */ #pragma once #include <windows.h> /* 引擎按 (参数个数, 参数表) 的方式调用, 返回值作为公式里的数值结果 */ #define TDX_EXPORT(name) \ extern "C" __declspec(dllexport) double WINAPI name(int nParamCount, ...)/* xs_pick.c —— 最小可跑通的选股函数骨架 */ #include "tdx_export.h" #include <stdarg.h> #include <math.h> /* 从可变参数里取 need 个 double,取不满直接判失败 */ static int fetch_args(int nParamCount, va_list ap, double *buf, int need) { int i; for (i = 0; i < nParamCount; i++) { double v = va_arg(ap, double); /* 公式侧实参一律按 double 取 */ if (i < need) buf[i] = v; } return (nParamCount >= need) ? 1 : 0; } /* 公式写法:XS_PICK(C, V, 5.0, 1.8) 参数:收盘价、成交量、回看周期、量比阈值 返回:1 命中 / 0 不命中 */ TDX_EXPORT(XS_PICK) { double buf[4]; double sum = 0.0, ratio; int i; va_list ap; va_start(ap, nParamCount); if (!fetch_args(nParamCount, ap, buf, 4)) { va_end(ap); return 0.0; /* 参数不足,安全退出,别读脏栈 */ } va_end(ap); if (buf[2] < 1.0 || buf[1] <= 0.0) return 0.0; /* 除零与非法周期拦截 */ for (i = 1; i <= (int)buf[2]; i++) sum += buf[1] / (double)i; /* 占位计算 */ ratio = sum / (buf[1] * buf[2]); return (ratio > buf[3]) ? 1.0 : 0.0; }TDX_EXPORT宏把三件事一次做完:extern "C"关掉 C++ 名字修饰,__declspec(dllexport)把函数放进导出表,double WINAPI固定返回类型和调用约定。少了任何一项,导出名就会变成?XS_PICK@@YANHZZ这种带修饰的形式,引擎扫不到。
fetch_args里那个if (i < need)判断是关键:即使用户在公式里多传了两个参数,也不会写越界。反过来,参数少传时函数返回 0,客户端不会崩——DLL 崩溃会带崩整个客户端进程,未保存的自选股和画线全部丢失,这个代价比算错一个信号大得多。
buf[1] <= 0.0的拦截看似多余,实盘里遇到停牌、新股首日时成交量为 0 是常态,一句除零就能让客户端直接退出。
3. 把选股因子写进 DLL:参数入参设计与公式侧调用
3.1 公式侧怎么调用
DLL 放好、客户端重启之后,在公式管理器里新建一个条件选股公式,主体就一行:
XS_PICK(CLOSE, VOL, 5.0, 1.8)注意参数顺序必须和 C 代码里buf的下标严格对应,公式引擎不做类型检查和个数校验,传错位置它照样调用,只是结果荒谬。我见过把周期和阈值写反的,跑出来天天命中几十只票,查了两小时才发现是5.0和1.8换了位置。
调试阶段建议先建一个技术指标公式而不是条件选股公式,把返回值画成曲线:
XS_PICK(CLOSE, VOL, 5.0, 1.8); XS_RAW(CLOSE, VOL, 5.0)第一行画 0/1 的阶跃线,第二行画连续值,两条线叠在一起看,能立刻发现"信号恒为 1"或者"信号恒为 0"这类低级错误。这比在条件选股里反复跑全市场快得多,因为指标只算当前这只票。
3.2 面板数据与逐股计算的差别:DLL 该做多少事
一个必须想清楚的设计问题:DLL 是被逐股、逐 bar 调用的,引擎不会把整个市场的面板数据一次性喂给你。也就是说,你在 DLL 里拿到的只有公式传进来的那几个序列的当前值,做不了跨股票的比较(比如"全市场换手率排名前 10%")。
这决定了功能边界。DLL 适合做的是:单只股票内部、基于时间序列的复杂计算——形态识别、多周期共振、条件组合、自定义平滑。不适合做的是:横截面排序、行业相对强弱、板块内排名。后者要么放到公式侧用RANK之类的内置函数拼,要么在 DLL 之外用脚本把全市场数据先算好落成文件,再让 DLL 读。
我一般的分工是:DLL 负责"这只票现在处于什么状态",公式侧负责"在所有处于这个状态的票里挑前 N 只"。职责一分开,DLL 就变成了纯粹的函数,无状态、可重入、好测试。
3.3 参数设计:把阈值、周期做成入参而不是常量
新手常犯的错是把阈值写死在 C 代码里,改一次参数就要重编译一次,重编译还要重启客户端,一轮调参五分钟。把周期、阈值、平滑系数全部做成入参之后,调参就变成了在公式编辑器里改数字,秒级反馈。
但入参也不是越多越好。参数超过六个,公式里那一行就没法读了,而且每加一个参数都要在fetch_args里对应扩buf数组,容易漏。我的经验是控制在三到五个:一个周期、一个主阈值、一个辅助阈值,剩下的用内部常量。
参数合法性检查必须写全。下面这张表是我在每个函数入口都会过一遍的清单:
| 参数 | 合法范围 | 非法时的处理 | 不处理的后果 |
|---|---|---|---|
| 周期 N | 1 ~ 250 | 返回 0 | 负数导致循环不执行,结果恒为 0 |
| 阈值 | 0.01 ~ 100 | 钳位到边界 | 传 0 时全部命中,选股失去意义 |
| 价格序列 | > 0 | 返回 0 | 停牌价为 0,除零崩溃 |
| 成交量 | >= 0 | 返回 0 | 同上 |
这张表不是形式主义。第三行和第四行对应的崩溃,是这类插件最常见的翻车原因,而且现象很迷惑——平时跑得好好的,某天扫到一只停牌股,客户端直接没了。
3.4 用条件选股做批量扫描与结果落盘
逻辑调通之后,走正式的批量路径:公式管理器里把公式类型设为条件选股,进入"条件选股"面板,选好公式、设定日期范围(一般取最近一个交易日),点"执行选股",结果会落到一个临时板块里。
这一步有三个实操要点。
第一,选股范围要先限定。全市场五千多只票,每次调用都要跑一遍你的 C 函数,如果函数里做了稍微重一点的计算(比如嵌套循环求斜率),整个扫描可能要几分钟。先用"沪深A股"再逐步缩小到自选板块,能省一半时间。
第二,日期范围不要跨太长。条件选股对历史区间的处理是逐日回算,跨一年等于把整个计算量乘以 240。要验证历史表现,用第 6 章讲的离线回放,比在客户端里硬跑高效得多。
第三,结果要落盘。选股结果存在临时板块里,重启客户端不一定还在。我的习惯是选中结果后立刻"另存为板块",命名带上日期,比如XS_20240315,后续复盘直接调板块看,不用重跑。
4. VC++ 6.0 编译与部署:一次跑通的七个核对点
4.1 工程设置:三个必须改的开关
打开源码包里的.dsp工程,先别急着编译,把这三处确认一遍:
Project -> Settings -> C/C++ -> Code Generation Use run-time library: Multithreaded (/MT) Project -> Settings -> Link Category: General -> Output file name: 你的模块名.dll Object/library modules: kernel32.lib user32.lib Project -> Settings -> C/C++ Category: Preprocessor -> Preprocessor definitions: WIN32;_WINDOWS;NDEBUG/MT是静态链接 C 运行库,这一项决定了 DLL 能不能在别人的机器上跑。如果用了/MD,编出来的 DLL 依赖msvcr71.dll,开发机上因为装了 VC++ 6.0 所以有,客户机上大概率没有,加载直接失败,而且报错信息非常含糊,只提示"初始化失败"。
kernel32.lib和user32.lib按需加。如果 DLL 里只是想写个调试日志,用OutputDebugString就够了,不需要额外的库;但要注意这个函数在 Release 下会被编译器优化掉,调试阶段用 Debug 配置编。
预处理器定义里NDEBUG决定assert是否生效。发布版本一定要带上,否则某个assert失败会弹出对话框阻塞引擎线程,整个客户端卡死——这个现象比崩溃还难查,因为进程还在,就是不响应。
4.2 导出表核对:用 dumpbin 看一眼
编译成功不等于导出正确。VC++ 6.0 自带dumpbin,在命令行里跑:
dumpbin /exports 你的模块名.dll正常情况下应该看到:
ordinal hint RVA name 1 0 00001000 XS_PICK 2 1 00001120 XS_RAW如果看到的是?XS_PICK@@YANHZZ这种带问号的形式,说明extern "C"没生效,检查头文件里的宏有没有被正确包含,或者源文件后缀是不是.cpp而宏里漏了extern "C"。
如果导出表是空的,多半是.def文件在工程里但没被加进链接器,或者源码用了__declspec(dllexport)但函数被声明成了static。这两种情况我都遇到过,前者是工程迁移时漏配,后者是自己手滑。
4.3 部署与位宽核对
把 DLL 复制到扩展目录,重启客户端。第一次部署建议做一个三步核对:
第一步,确认目标目录对不对。用资源管理器打开客户端安装目录,找那个约定好的扩展文件夹,把 DLL 放进去,同时确认目录里没有同名的旧版本——同名文件会被占用,复制时提示"文件正在使用",如果强行覆盖失败,实际加载的还是旧的。
第二步,确认位宽。在任务管理器里看客户端进程,32 位进程后面会标(32 位)。如果你的 DLL 是 64 位的,加载会失败但不一定有明确提示。用dumpbin /headers 你的模块名.dll看machine字段,14C是 x86,8664是 x64。
第三步,确认函数可见。在公式编辑器里敲函数名,如果能自动补全,说明引擎扫到了;如果敲完提示"未定义的函数",回到上一步查导出表。
4.4 用日志和探针函数做黑匣子排查
DLL 在引擎进程里跑,出问题时没法打断点。我的做法是加一个独立的探针函数,专门用来输出内部状态:
/* 调试探针:公式里单独调用 XS_DEBUG(C,V,5.0) 看内部中间值 */ TDX_EXPORT(XS_DEBUG) { double buf[3]; va_list ap; char msg[128]; va_start(ap, nParamCount); fetch_args(nParamCount, ap, buf, 3); va_end(ap); _snprintf(msg, sizeof(msg) - 1, "[XS] c=%.3f v=%.3f n=%.0f\n", buf[0], buf[1], buf[2]); msg[sizeof(msg) - 1] = '\0'; OutputDebugStringA(msg); /* 用 DebugView 之类的工具接 */ return buf[0]; }这个函数返回值原样透传收盘价,所以把它加进指标公式不会破坏图形。日志一定要限流——逐 bar 打印会在几秒内写出几十兆文本,把调试工具拖死。实际使用中我一般只在关键分支打印,或者用一个静态计数器每 500 次打一条。
4.5 性能预算:全市场扫描的时间尺度
五千只票、每只一次调用,如果单次调用耗时 1 毫秒,总耗时就是 5 秒,可以接受;如果单次 20 毫秒,总耗时接近两分钟,用户会以为卡死了。
判断标准很简单:DLL 里不要有文件 IO,不要有new/delete密集操作,不要有对参数序列的二次遍历嵌套。所有能在公式侧用内置函数算完的东西,不要搬进 C 里重算一遍——引擎的内置函数是优化过的,你手写的循环大概率更慢。
5. 避坑排查:五类让客户端直接退出的现场
5.1 公式里能敲出来,选股时报"函数不存在"
现象:在指标公式里调用一切正常,切到条件选股就提示函数未定义。
原因:条件选股和指标公式走的是两条加载路径,部分版本对条件选股里可用的自定义函数做了额外校验,或者只认特定前缀。另一种可能是 DLL 放在了指标准确目录,但客户端启动时该目录被安全软件锁了一下,扫描跳过。
解决:先把 DLL 复制到扩展目录的上一级做一次对照测试;再用探针函数确认引擎到底扫到了哪些导出名。如果确实只认前缀,把函数名统一改成带固定前缀的形式重新编译,比反复猜规则快。
5.2 扫描到一半客户端直接消失
现象:平时正常,某天扫全市场时进度条走到一半,客户端进程没了,没有任何错误提示。
原因:某只票触发了除零或数组越界。最典型的是停牌股成交量为 0,或者新股上市首日历史数据不足,回看周期 N 大于实际 bar 数时读到了未初始化内存。
解决:在函数入口把所有入参做一遍合法性拦截,价格 <= 0、成交量 <= 0、周期大于可用 bar 数,一律返回 0。同时给回看循环加上边界判断,宁可少算一只票,也不能让进程崩。这个坑我吃过一次,丢了一整天的画线。
5.3 昨天选出来 12 只,今天同样的参数选出 40 只
现象:参数一个字没改,两天的选股结果差异巨大。
原因:函数里用了未来数据。最常见的是在计算里引用了收盘后才更新的字段,或者用了REF方向的偏移取到了当根之后的值。另一种可能是 DLL 里有静态全局变量,第一只票算完残留的状态影响了第二只票,导致结果和调用顺序相关。
解决:把 DLL 改成无状态函数,所有中间变量放在栈上,禁止static变量参与计算;公式侧检查有没有引用未来周期的函数。验证方法很简单:同一组参数连跑三次,结果必须完全一致,不一致就是有状态残留。
5.4 开发机能跑,换台机器加载失败
现象:自己的机器上一切正常,把 DLL 连同公式导出发给同事,对方导入公式后函数全部失效。
原因:编的是 Debug 版本,或者运行库设成了/MD,目标机器没装对应的运行时;也可能是同事装的是不同位宽的客户端。
解决:发布前一律用 Release 配置加/MT重新编译,用dumpbin /dependents检查依赖表,只应该看到KERNEL32.dll、USER32.dll这类系统库。看到msvcr71.dll就说明静态链接没生效,回去改工程设置。
5.5 扫描慢到以为程序假死
现象:点了执行选股,进度条几乎不动,等了十分钟还在跑。
原因:函数内部做了重活。常见的是在每次调用里重新打开文件读参数表,或者在循环里拼字符串做日志。五千次调用里做五千次文件打开,光系统调用就能吃掉几分钟。
解决:参数表在 DLL 加载时读一次,缓存在静态区(只读,不会有状态污染问题);日志改成计数器限流,每 N 次打一条。如果逻辑确实重,把它拆成两级——粗筛用公式侧的内置函数先过滤掉 80% 的票,剩下 20% 再进 DLL 精算。
6. 进阶验证:用离线回放把插件调成确定性函数
调参调到后期,最浪费时间的不是写代码,是"改一个数、重启客户端、跑一遍全市场、看结果"这个循环。我后来把这件事拆开了:把 DLL 当纯函数,在客户端外面回放。
具体做法是用导出的日线数据生成一份 CSV,每行一只票一个交易日,列就是公式会传进去的那几个序列值,然后写一个小驱动,用LoadLibrary把 DLL 加载起来,按行喂参数:
/* replay.c —— 用同一份 DLL 在客户端外回放,验证逻辑一致性 */ #include <windows.h> #include <stdio.h> typedef double (WINAPI *PFN)(int, ...); int main(void) { HMODULE h = LoadLibraryA("你的模块名.dll"); PFN fn; FILE *fp; double c, v, n, th; int hit = 0, total = 0; if (!h) { printf("load fail: %lu\n", GetLastError()); return 1; } fn = (PFN)GetProcAddress(h, "XS_PICK"); /* 名字必须与导出表一致 */ if (!fn) { printf("symbol not found\n"); return 1; } fp = fopen("sample.csv", "r"); if (!fp) return 1; while (fscanf(fp, "%lf,%lf,%lf,%lf", &c, &v, &n, &th) == 4) { total++; if (fn(4, c, v, n, th) > 0.5) hit++; /* 参数个数在前,与引擎一致 */ } fclose(fp); FreeLibrary(h); printf("total=%d hit=%d rate=%.4f\n", total, hit, total ? (double)hit / total : 0.0); return 0; }这个驱动的价值在于它复用了同一份 DLL、同一套参数顺序,所以它算出来的命中率可以直接和客户端里的选股结果对拍。两边对不上,就说明问题出在引擎调用环节(参数顺序、类型、目录),而不是逻辑本身——把排查范围一刀切成两半,效率差别很大。用 VC++ 6.0 编这个控制台程序,同样/MT,同样 32 位,免得加载 32 位 DLL 时踩位宽不匹配。
回放还有一个副产品:它能跑出历史每一期的命中明细,把命中率、平均持有天数、最大回撤这些指标算出来。到了这一步,选股插件就不再是"感觉挺好用"的玄学,而是一个有数据支撑的模块。
那张对拍表我一般会留一份,格式是这样:
| 验证项 | 客户端结果 | 离线回放 | 判定 |
|---|---|---|---|
| 命中只数 | 12 | 12 | 一致 |
| 命中率 | 0.0240 | 0.0240 | 一致 |
| 重复跑三次 | 12/12/12 | 12/12/12 | 无状态 |
第三行是我最看重的。只要出现 12/13/11 这种波动,立刻回去查静态变量和未来数据,别急着调参——参数再好,逻辑不确定就是白搭。
源码包里的教程部分建议按"先跑通最小骨架、再加自己的因子、最后做离线对拍"这个顺序看,别一上来就扎进完整工程,那份代码里的历史包袱不少。压缩包我放在下面了,需要的话自取。
从那以后我每次改完 DLL,都强制走一遍"导出表核对 + 三次一致性回放",再进客户端跑全市场。这个习惯让我少加了很多次班,也少丢了几次画线。希望帮到你。
本文还有配套的精品资源,点击获取