☰
用C++编写通达信MACD插件:DLL接口与EMA递推实现详解
2026/11/3 17:34:15 网站建设 项目流程

简介:这是一套面向通达信软件的MACD指标插件源码,采用C++开发,适合需要在通达信平台中自定义技术指标、研究插件编写方式的程序员或股票分析爱好者。压缩包采用RAR格式,体积非常小巧,共11个文件、约15KB,其中包含C++源文件与头文件,前者承载MACD指标的计算逻辑与接口实现,后者负责相关数据结构的声明;同时提供dsw、sln、vcproj、dsp等工程配置文件,可在Visual C++环境中直接打开编译,生成供通达信调用的插件程序。目前已有403人学习下载,资源虽小但结构典型,作者也说明可能存在不够严谨之处,恰便于读者自行验证和修正,适合初次接触通达信插件开发的人作为入门范例。通过源码可以理清插件对外接口、输出结构定义与MACD核心算法实现,掌握从指标计算到结果输出的完整流程,也便于在此基础上调试、改写或封装自己的指标策略。

1. macd.rar 背后的事:C++ 通达信 MACD 插件到底解决什么

很多人下载到 macd.rar 这类压缩包后,解压出来的 C++ 源码大概率编译不过,或者好不容易编出 DLL 放进通达信却没有任何反应。问题基本都出在「通达信 MACD 插件」这个组合上:通达信计算 MACD 用的是公开的 EMA 递推公式,但要把自己的 C++ 实现接进去,需要先搞懂 DLL 接口、导出函数名和 32 位程序这些外部约定。下面只讲一件事:怎么用 C++ 写一份能被通达信调用的 MACD 插件,并让它和通达信自带指标对得上账。适合能写基础 C++、受够了公式语言限制、想在通达信里跑自定义算法的从业者。

2. 通达信 DLL 插件机制与 MACD 递推公式:先想清楚再写代码

2.1 公式语言做不了什么:插件该出场的位置

通达信自带 MACD 指标的核心逻辑其实很短:DIF 是快慢 EMA 的差,DEA 是对 DIF 的 EMA,MACD 柱是 DIF 减 DEA 的两倍。两行公式就能写完,那为什么还需要 C++ 插件?

典型场景是公式语言的循环、状态和外部数据受限。比如「MACD 三背离公式」要在多段 K 线区间里找相邻峰谷并比较力度,这需要遍历整段数据,并且维护大量中间状态;或者要读入自己下载的复权因子文件做除权处理;又或者要在刷选时用到日线以外的多周期数据做聚合。这些在公式脚本里写起来要么慢,要么写不动。C++ 插件相当于把计算能力放回通用语言侧,公式只负责传数据进、拿结果画。

我不建议一开始就从网上抄大段源码。先想清楚「为什么要插件」:如果只是把 DIF/DEA 画出来,公式已经够用;只有当你有外部输入、复杂状态、性能瓶颈,才值得付出 DLL 编译与加载的折腾。顺便说一句,有读者会在外部用 Python 算好 MACD 双底高低点再想办法接回来,这在通达信里比 C++ DLL 更别扭,因为软件不会主动读你的 Python 结果,而 DLL 可以在一次调用里拿到全部 K 线数据、算完再返回数组——这是插件最顺的工作方式。

2.2 MACD 的 EMA 递推与精度:开头初值决定整条曲线

MACD 三根线的基础都是 EMA。通达信公式语言对 EMA 的定义是平滑系数 2/(N+1),没有像某些软件那样用「首 N 根做算术平均」的种子算法,而是从第一根 K 线就开始递推。用 C++ 表达这个递推只需要一行核心式:

double emaNext(double close, double prevEma, int n) { return (2.0 * close + (n - 1) * prevEma) / (n + 1.0); }

逻辑说明:close 是当前 K 线收盘价,prevEma 是上一根 K 线的 EMA 值,n 对 DIF 分别是 12 和 26,对 DEA 是 9。当 prevEma 取第一条 K 线的收盘价时,第一次输出会明显偏向 close,之后逐步收敛,所以整条序列的前几十个点对起始位置非常敏感。

这里有两个结果层面的细节。第一个是初值:通达信按数据起始点从左往右递推,而不是从当前屏幕右侧倒推,随便用一个历史序列喂进去,如果序列起点和软件不一致,前 N 根 DIF 一定对不上。第二个是精度:递推几千根 K 线后,float 的累计误差可能让 DIF 显示值差 0.01,别让这种误差出现在对账阶段。我一般会在插件内部用 double 保存 emaFast、emaSlow 和 deaVal,只在写回输出缓冲区时才转成 float,因为通达信公式侧的数据缓冲区是 float 数组。「内部 double、对外 float」这个习惯,比什么移植技巧都管用。

还有一个约定要提前定死:公式侧传给 DLL 的是一个 float 数组加一个表示长度的整数。DLL 侧不能假设数组有 1024 根或 4096 根,count 是唯一可信的边界。对账时直接跳过前 100 根 K 线,只比对稳定段,这不是偷懒,而是 EMA 递推本身就决定了头部数据对起点敏感,正规做法就是让插件从完整数据开头计算,再评估后半段。

3. 用 C++ 写一个最少可用的 MACD 插件:接口、源码到装入通达信

3.1 先定导出接口:extern "C" 与 __stdcall 为什么不能省

通达信插件本质是一个 Win32 DLL,公式侧按它对外约定的方式调用 DLL 里的导出函数。常见做法是:DLL 导出一个计算函数,参数里传进 K 线序列、条数和参数,传出一个或多个结果缓冲区。不同版本通达信对具体函数名和结构体格式有差异,网上那些 macd.rar 里附带的头文件不一定适用于你的版本,所以先别急着抄,把「导出层」和「算法层」分开写,就算换接口,算法部分也不用动。

导出函数必须用extern "C"加__stdcall。原因是 C++ 编译器会对函数名做修饰(name mangling),导出一串带问号的乱码名,通达信按约定函数名去找就会扑空。__stdcall是 Windows 下常见的调用约定,参数从右往左压栈、由被调用方清理,在 DLL 接口里属于硬约定。头文件骨架如下:

// macd_plugin.h #pragma once #if defined(_WIN32) #define MACD_API extern "C" __declspec(dllexport) #define MACD_CALL __stdcall #else #define MACD_API extern "C" #define MACD_CALL #endif MACD_API int MACD_CALL CalcMACD( const float* close, int count, int fast, int slow, int signal, float* dif, float* dea, float* macd);

代码逻辑说明:MACD_API 宏把函数声明成「C 语言链接 + 从 DLL 导出」,MACD_CALL 统一__stdcall。后面六个参数是数据通道:close 是输入序列,count 是序列长度,fast/slow/signal 对应通达信指标参数里的 12/26/9,三个输出缓冲区由调用方按 count 长度预分配。对外暴露整数和指针,比塞一个自定义结构体更容易适配不同版本的通达信;等你拿到手头 SDK 的实际请求格式,再改这一层的映射即可。

注意不要在函数里对 close、dif 这些指针做任何长度假设。旧代码里用魔数 1024、4096 去遍历的情况不少,一旦某只股票数据超过长度就随机崩。count 就是唯一可信的边界。

3.2 核心实现:一个直接能改的 CalcMACD

按前面的递推公式实现完整函数:

// macd_plugin.cpp #include "macd_plugin.h" MACD_API int MACD_CALL CalcMACD( const float* close, int count, int fast, int slow, int signal, float* dif, float* dea, float* macd) { if (!close || !dif || !dea || !macd || count < 2 || fast < 2 || slow < 2 || signal < 1 || fast >= slow) return 0; double emaFast = close[0]; double emaSlow = close[0]; double deaVal = 0.0; for (int i = 0; i < count; ++i) { if (i == 0) { dif[0] = 0.0f; dea[0] = 0.0f; macd[0] = 0.0f; continue; } emaFast = (2.0 * close[i] + (fast - 1) * emaFast) / (fast + 1.0); emaSlow = (2.0 * close[i] + (slow - 1) * emaSlow) / (slow + 1.0); double diff = emaFast - emaSlow; dif[i] = (float)diff; deaVal = (2.0 * diff + (signal - 1) * deaVal) / (signal + 1.0); dea[i] = (float)deaVal; macd[i] = (float)((diff - deaVal) * 2.0); } return 1; }

逐段说明:开头先拦掉空指针和非法参数,count < 2 是因为首根没有递推意义;fast >= slow 时快线比慢线还慢,公式语义已错,在入口返回 0,比在通达信里画一条混乱曲线更容易排查。i == 0 时把三个输出都写 0,这是插件内部的起点约定,与通达信数据窗口起点无关;从 i = 1 开始,先递推两条 EMA,再算 DIF,再用 DIF 递推 DEA。最后 MACD 柱乘以 2,这是通达信指标写法里的原始定义,很多人只减不乘,导致柱子幅度差一倍,对账时一眼就能看出问题。

参数对应关系上,fast 对应 12,slow 对应 26,signal 对应 9。跑通达信同款默认参数,调用时传 12、26、9。DEA 递推用的是 double 类型的 diff,而不是刚写回 dif[i] 的 float,这一步能省掉不少后期「差 0.01」的精力。

提示:如果通达信调用约定要求的是 float* 传参数而不是 int,把这四个参数挪到一个结构体里,改导出层即可,算法层完全不用动。

3.3 编译调试:VSCode + MSVC 命令行产出 32 位 DLL

写代码用 VSCode 配一下 C/C++ 环境就够了,不需要开大型 IDE。重点在编译命令:控制台要用 x86 的开发者命令行,保证 cl 默认目标是 32 位。编译命令:

cl /nologo /LD /MT /O2 macd_plugin.cpp /Fe:macd.dll

参数含义:/LD 表示生成 DLL,/MT 表示静态链接 C 运行库,/O2 是速度优化。/MT 这一步很关键,它把运行库打进 DLL,目标机器即使没装 Microsoft Visual C++ Redistributable 也能加载。建议在「x86 Native Tools Command Prompt」里手动跑这条命令,或者让 VSCode 终端先初始化 vcvars 环境再执行,否则直接 tasks 里跑 cl 会提示找不到命令。

VSCode 里可以建一个 tasks.json 固定这条命令,避免每次手动切命令行:

{ "version": "2.0.0", "tasks": [ { "label": "build macd dll", "type": "shell", "command": "cl /nologo /LD /MT /O2 macd_plugin.cpp /Fe:macd.dll", "options": { "cwd": "${workspaceFolder}" }, "group": "build" } ] }

保存后 Ctrl+Shift+B 就能编译。编译成功先用 dumpbin 看一眼导出名,确认不是被修饰过的乱码:

dumpbin /exports macd.dll

输出里应当有一行 CalcMACD 而不是?CalcMACD@@YGH...。如果看到带问号的导出名,回到 3.1 查 extern "C"。这一小步是后面所有排查的参照物。

DLL 的摆放位置,不同版本通达信不太一样,常见做法是放到通达信主程序所在目录,具体看安装目录里有没有现成 DLL 作参考。公式侧按通达信外部函数的语法调用,一般来说写起来类似DIFX:MYMACD(1,C,12,26,9);,把收盘价序列和三个参数传进去,输出的三个数组分别作为 DIF、DEA、MACD 柱画图。如果第一版画出来是空曲线,优先怀疑导出名和调用方式,而不是算法。

4. 通达信 MACD 插件常见问题:现象、原因、解法

4.1 找不到导出函数:导出名被 C++ 编译器改写

现象:DLL 放进通达信后,公式调用插件提示「函数不存在」或运行结果全是 0,DLL 明明就在目录里。

原因:最常见的是编译时没有 extern "C"。C++ 编译器按重载规则给函数改名,导出的符号带 ? 和编号,通达信按约定名找 CalcMACD 自然找不到。第二个常见原因是调用约定不匹配,导出函数写成了 __cdecl,而平台侧按 __stdcall 调用,栈清理方式不同,轻则拿不到结果,重则直接 access violation。

解法:源码里统一用上一章的 MACD_API/MACD_CALL 宏,编完用 dumpbin /exports 看导出名,确认是 CalcMACD 而不是带修饰符的名字。改了之后重编重放,基本当场解决。如果手头 SDK 有自己的导出约定,以 SDK 头文件为准,但「导出名要对得上」这句话永远适用。

4.2 64 位 DLL 在 32 位通达信里加载失败

现象:在 64 位 Windows 上用 VS 编译,本地一切正常;把 macd.dll 放进通达信后,公式计算不出来,加载时甚至可能弹错。

原因:通达信主程序是 32 位进程,只能加载同位数 DLL。64 位 DLL 和 32 位进程存在寄存器约定、指针大小层面的不兼容,不是改个路径就能救回来的。

解法:编译器目标平台选 x86 或 Win32;命令行编译确认工具链是 x86 的 cl,编完用 dumpbin /headers 看机器类型。同时用 32 位运行库配合,别让 DLL 依赖 64 位运行时文件。这套规则和用不用 C++ 无关,是 Windows DLL 的通用约束,也是网上那些压缩包「编译不过」的重灾区。

4.3 前几十根 K 线对不上:EMA 初值不一致

现象:插件画出的 MACD,后半段跟通达信自带指标几乎重合,开头却差出一大截,越靠前差得越厉害。

原因:EMA 是递推的,起点初值从哪根 K 线开始、用多少数据预热,直接决定头部输出。通达信内置 MACD 从自选数据范围的起始点开始递推;如果插件只拿最近几百根算,EMA 还没收敛,头部自然对不上;反之,数据范围比内置更长,结果又会偏向另一套初值。

解法:这是逻辑问题不是 bug。最稳妥的对账方式是让插件和内置指标读取同一条完整日线序列,并且约定忽略前 100 根。我在 3.2 里特意写「首根置 0」,不是为了让第一根和软件相等,而是给递推一个可复现的起点;验证越过预热段后,差值应该在 0.00 附近。真要让头部也对齐,就得用和通达信完全相同的起点,通常意味着把整个历史数据都喂进去,代价比收益大。

4.4 除权缺口污染 MACD:复权因子要在外部准备好

现象:某只股票分红送转后,MACD 柱突然剧烈下拐或走平,和行情软件里的显示对不上;切换复权方式后曲线又变成另一套。

原因:MACD 公式吃的是传入的收盘价序列,它不知道除权。不复权序列里,除权日价格跳空巨大,EMA 被假缺口猛拉,柱子直接翻车。通达信显示的内置 MACD 和当前界面选择的复权方式有关,你传给插件的序列用了另一种复权设置,整条曲线都会平移或变形。

解法:插件内部不做复权判断,数据准备在外部完成。最省心的是让插件读取与通达信界面一致的复权序列;如果做选股回测,我会先把下载好的复权因子文件读进来、按除权日修正价格,再交给 DLL,这样结果不受通达信界面切换影响。注意「前复权 vs 后复权」只能二选一,混着用会在对账时出现系统性偏差,而且千万别在数据中间切换复权方式,递推会立即被污染。

4.5 换台电脑就加载崩:运行库动态链接问题

现象:本机编译运行都正常,把 DLL 拷到另一台机器上,通达信要么加载失败,要么直接异常退出。

原因:DLL 默认 /MD 动态链接 MSVC 运行库,目标机器缺对应版本运行库,加载时解析依赖失败;另一种隐蔽情况是编译成 Debug 配置,依赖 debug 版运行库,在别的机器上根本不存在。

解法:发布时用 /MT 静态链接,编译配置选 Release。有条件的话,在干净虚拟机里做冒烟测试:装运行时、卸载运行时各跑一次,验证 DLL 是否仍能加载。给别人的压缩包里,把「Release 配置 + /MT 静态链接」写进说明第一行,比放一堆源代码实用得多。

5. 把插件做扎实:参数调优、一致性验证与一个查错习惯

5.1 参数不是只有 12/26/9

插件写好后不要锁死参数。通达信默认 12/26/9 适合日线趋势跟踪;分时或短线波段可以换更快的一组。给几组常用参考:

适用周期fastslowsignal说明
日线默认12269与通达信内置一致,适合对账
短线日内6199快 MACD 风格
中线趋势213413过滤高频抖动

参数由公式侧传入,DLL 保持通用。想自动寻参时,可以用 C++ 随机数做网格扰动多跑几轮,但要小心过拟合——某一组参数在单一股票上再漂亮,换到别的票上也可能失效。

5.2 和通达信自带 MACD 对账:如何确认实现是对的

新建一个公式,把插件输出和内置 MACD 同时画出来,选一只长期停牌少、分红简单的票,日线不复权,跳过前 100 根对比 DIF/DEA 数值差。差值为 0.00,说明插件与内置口径一致;再把复权、周期、参数扩展逐个加上,心里才有底。我最早也是拿到别人给的 C++ 通达信插件源码不对账就直接用,最后发现翻车根本不在算法,而在编译配置上。从那以后养成一个习惯:每次先 dumpbin 看导出名,再确认位数和 /MT,最后才谈指标逻辑——这个顺序能省掉一半玄学时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询