☰
通达信DLL公式实战:用C语言实现MA均线指标
2026/10/20 19:42:13 网站建设 项目流程

简介:通达信插件开发资料包,面向具有C语言与Windows编程基础、希望扩展通达信分析功能的开发者。资源围绕通达信C接口、MA移动平均线算法与DLL公式展开,展示了如何把均线计算逻辑封装为动态库函数,并通过通达信外部接口集成到行情软件中,适合学习插件调用机制或作为自定义指标二次开发模板。压缩包共12个文件,以cpp源码与h头文件为核心,配合dsp、dsw工程文件组织编译,另含plg、opt、ncb等辅助配置,整体仅10KB,结构紧凑,便于快速定位MA算法实现与接口声明。已有780人学习下载,开发者可直接对照源码理解通达信DLL公式调用方式和MA周期参数传递流程,也可在此基础上扩展其他技术指标。

1. 通达信DLL公式:把 C 算法挂进行情软件的一条近路

纯公式写 MA 均线只需一行,可一旦你想复用现成的 C 算法,或者要在通达信客户端里跑自定义的指标逻辑,公式脚本就会变成一个说不清的黑匣子:数据对齐、参数传递、输出缓冲区的生命周期全靠感觉。C 接口 DLL 公式就是为这条路准备的——把算法编译成标准 DLL,注册给通达信公式引擎,之后像调用内置函数一样在公式里使用。TestPluginTCale.zip 提供的正是这样一条最小可用的 C 接口实现路线,核心是 MA 算法,包内工程把接口注册、数据交换、均线计算拆得很干净。适合已经写过几个通达信公式、对 C 有基础但第一次接触 DLL 扩展的开发者。它解决的不是“画出均线”,而是“把自己的算法安全挂进 K 线计算流程”。

2. 通达信DLL公式接口拆解:RegisterTdxFunc 与 CalcFunc 的职责分工

2.1 你拿到的包里,哪些文件才是关键

TestPluginTCale.zip 解压后,内容按“接口声明—算法实现—注册入口”三个层次组织。我通常先翻 readme 而不是直接开工程,因为 readme 会写明编译器的版本要求和 DLL 放置目录,这两点在通达信 DLL 开发里最容易翻车。

文件作用通常需要改的位置
tdx_interface.h定义与公式引擎交换数据的结构体和导出宏只在换接口版本时动
plugin_register.c实现 RegisterTdxFunc 和 CalcFunc 两个导出入口增加新函数时在这里登记
ma_func.c核心 MA 算法,与接口层无关替换成你自己的算法时主要改这里
工程文件 / 构建脚本控制编译参数与 CRT 链接方式切换 32 位 / 64 位时必改
readme编译、注册、验证步骤说明按你的实测经验补充

这种拆分的好处是接口层和算法层解耦。后续想把 DLL 里的 MA 换成 EMA,只需要改 ma_func.c,不用动导出入口;反过来,如果客户端升级导致接口结构体字段发生变化,只动 tdx_interface.h 和 plugin_register.c 就能兜住。踩过一次 DLL 加载失败的坑之后,我对文件职责划分就特别敏感——把注册逻辑和算法逻辑揉在一个文件里,出了问题你根本分不清是协议写错还是算法算错。

2.2 数据从公式系统传进 DLL 的路径

通达信客户端在计算公式时,会先从行情里取到 K 线时间序列,把 CLOSE 这样的序列整理成 float 数组,然后调用 DLL 的导出函数。整个过程可以简化为三步:先把 CLOSE 序列、参数值、输出缓冲区的地址打包进一个结构体,再进入 CalcFunc,最后把输出数组写回给公式引擎。

这里的关键在于输出缓冲区。公式引擎为了减少内存分配开销,经常复用同一块缓冲,里面可能残留上一次计算的结果。你的函数不能依赖“初始值是 0”,必须在循环里对所有索引位逐根赋值,否则就会出现画面上残留旧均线、数据错位这类看起来像玄学的问题。

// tdx_interface.h(节选) typedef struct tagTDX_CALC_INFO { int nDataCount; // 参与计算的K线根数 int nParamCount; // 公式里传入的参数个数 float* pfParam; // 参数数组,pfParam[0]是第1个参数 const float* pfInput; // 输入序列,比如CLOSE的收盘价 float* pfOutput; // 输出序列,长度与nDataCount一致 } TDX_CALC_INFO;

注意pfInput声明为 const float*,这是工程里比较规范的做法:算法层只读行情序列,避免误改输入数据。不同客户端版本对结构体字段顺序略有差异,以你拿到的包内头文件为准,但主要字段就是这几个。

提示:先确认 nDataCount 的实际含义,它代表本次调用参与计算的 K 线根数,不等同于当前 K 线的绝对位置。

2.3 参数表和输出缓冲区的三个约定

实战中容易在参数表上出问题,我总结了三点:

第一,参数从pfParam[0]开始,不是pfParam[1]。公式里写MA(CLOSE,5),那么pfParam[0]就是 5.0f,第一次写 DLL 的人常在这里错读一位。

第二,nParamCount是公式里传参的总个数。如果公式里写了MA(CLOSE,5,1),第三个参数也会传进来,即使你的算法只用前一个。DLL 内部要做参数个数校验,别假定pfParam一定够长。

第三,对无结果的 K 线位置,必须显式写值。MA5 的前 4 根本来就没有有效值,你需要在 pOut 里写 0 或者 NAN,不能跳过不写。至于 0 和 NAN 哪个会被公式引擎当作“空值”,不同版本处理不一样,包内默认写 0,如果你的客户端不识别,需要改成 NAN 分支。

注册入口的代码也在这个包里,逻辑非常直接:

// plugin_register.c // 告诉公式引擎:这个DLL里有哪些函数,每个函数几个参数 BOOL WINAPI RegisterTdxFunc(TDX_REG_INFO* pRegInfo) { if (pRegInfo == NULL) return FALSE; pRegInfo->nFuncCount = 1; pRegInfo->pFuncs[0].nParamCount = 1; pRegInfo->pFuncs[0].strFuncName = "MA"; return TRUE; }

这段代码里最需要注意的是WINAPI调用约定,也就是__stdcall。如果编译器默认用__cdecl,导出表里的函数名和参数栈处理方式都会变,客户端加载时可能直接报“不是有效的 DLL”。这个坑我踩过一次,后来凡是写通达信 DLL 导出函数,我都会先确认编译宏里是不是WINAPI。

3. 用 C 实现 MA 算法的核心循环:滑动窗口与边界处理

3.1 双重循环求均值为什么不能直接搬进来

最顺手的 MA 写法是两层循环:外层遍历每个 K 线索引,内层从 i-N+1 累加到 i。这种写法在数据量小的时候没有明显问题,但放进通达信 DLL 环境就不合适了。

公式引擎刷新一次可能调用 CalcFunc 多次,每次传入的 K 线数量可能是几千根。如果你在公式里又嵌套调用了这个 DLL 函数,时间复杂度会逼近 O(N²),快速翻票时能感觉到明显的卡顿。更隐蔽的是浮点误差问题——每次重新累加窗口内的浮点数,末尾几根均线可能出现可观察的漂移,导致后面两线重合测试时差出零点几个点,看起来像接口没写对,其实是算法层精度不够。

这两层问题合在一起,决定了 MA 算法不能直接照搬教科书写法,要用滑动窗口。

3.2 滑动窗口求均值:避免累计误差的写法

// ma_func.c // 计算简单移动平均 MA(N) // pIn: 输入序列,长度 nCount // pOut: 输出序列,长度 nCount // nMA: 移动平均周期,调用前已经做合法性检查 int CalcSMA(const float* pIn, float* pOut, int nCount, int nMA) { if (pIn == NULL || pOut == NULL || nCount <= 0 || nMA <= 0) return 0; double sum = 0.0; for (int i = 0; i < nCount; i++) { sum += pIn[i]; // 窗口滑出旧值:i>=nMA 时,pIn[i-nMA] 已经不在窗口内 if (i >= nMA) sum -= pIn[i - nMA]; if (i >= nMA - 1) pOut[i] = (float)(sum / nMA); // 窗口已满,输出有效均值 else pOut[i] = 0.0f; // 前 nMA-1 根没有有效值 } return 1; }

这个写法维护一个增量累加变量 sum,新值进窗口就加,旧值出窗口就减,避免了每次从零开始重新累加,时间复杂度降到 O(N)。累加用 double 而不是 float,是为了避免连续几千次 float 累加造成的精度损失,尤其当 nMA 比较大的时候,float 的舍入误差会累积到结果尾部。

对前导无效区的处理,我用的是pOut[i] = 0.0f,这是包内默认行为。实际项目中,如果客户端支持 NAN 作为空值标记,我会在编译时加一个宏分支:

#ifdef TDX_NAN_EMPTY pOut[i] = (float)NAN; #else pOut[i] = 0.0f; #endif

具体用哪个,以你在目标客户端上做的空值实验为准。

3.3 把 MA 算法挂进 CalcFunc 的完整主干代码

算法本身写好了,还需要一个壳把它接到通达信公式引擎上,这就是 CalcFunc 的职责:

// plugin_register.c #include "tdx_interface.h" #include <windows.h> // 通达信公式引擎每次计算都会调用这个函数 BOOL WINAPI CalcFunc(TDX_CALC_INFO* pInfo) { int nMA; if (pInfo == NULL || pInfo->pfInput == NULL || pInfo->pfOutput == NULL) return FALSE; // pfParam[0] 是公式里传入的周期参数,四舍五入取整 nMA = (int)(pInfo->pfParam[0] + 0.5f); if (nMA < 1 || nMA > 500) return FALSE; // 调用算法层,真正的MA计算在ma_func.c里 if (CalcSMA(pInfo->pfInput, pInfo->pfOutput, pInfo->nDataCount, nMA) == 0) return FALSE; return TRUE; }

参数说明:(int)(pInfo->pfParam[0] + 0.5f)是把浮点参数四舍五入成整数。用户在公式里可能写MA(CLOSE, 5)也可能写MA(CLOSE, 5.0),不加处理直接强转 int 会把 5.0 之外的边缘值截断出错。周期上限限制在 500 是我的习惯,防止公式里误写一个超大周期导致计算量失控。返回 FALSE 时公式引擎会认为这次调用无效,不会使用输出缓冲区里的结果,这是比较安全的失败模式。

如果你在 CalcFunc 里需要调试,我一般会在开头加一个计数器自增,然后在公式里调用它,看数值是否变化,以此确认 DLL 确实被调入执行,而不是公式引擎只用了缓存。

4. 编译、安装与调用:让 DLL 在通达信客户端里跑起来

4.1 编译前必须确认的配置

打开工程文件之前,先确认三件事,否则后面全是白忙。

第一,目标平台。绝大多数通达信客户端仍是 32 位进程,即使运行在 64 位 Windows 上,DLL 也必须按 x86 编译。编出 x64 版本会导致导入时直接报“模块无法加载”。

第二,调用约定。导出函数必须使用WINAPI,也就是__stdcall。C 源文件里可以直接写BOOL WINAPI CalcFunc(...),如果是 C++ 文件,还需要加上extern "C"防止函数名被修饰。

第三,CRT 链接方式。建议使用静态运行时库/MT,而不是动态的/MD。客户端机器上可能没有对应版本的 VC 运行库,动态链接部署到别的电脑时容易找不到msvcr*.dll。

在开发命令行里编译的命令大致是:

cl /c /O2 /MT /D "WIN32" ma_func.c plugin_register.c link /DLL /OUT:TDXDLL1.dll ma_func.obj plugin_register.obj

参数说明:/O2是速度优化,/MT是静态链接运行库;/DLL告诉链接器生成动态库而不是 EXE。链接完成后,用dumpbin /exports TDXDLL1.dll检查导出表,确认能看到 RegisterTdxFunc 和 CalcFunc 两个符号,名称没有被编译器修饰过。

4.2 复制 DLL 与导入注册

编译完成后,把 TDXDLL1.dll 复制到通达信安装目录下与 DLL 公式相关的子目录。不同版本的客户端放置路径不完全一样,常见做法是放在安装目录的公式子目录,或者用户目录下的公式目录里。

然后打开通达信客户端,进入公式管理器,切换到 DLL 函数一栏,点导入,选中刚才的 DLL。导入成功后会列出函数名 MA,参数个数 1。导入完成建议重启一次客户端,让公式引擎彻底重新加载 DLL,避免旧实例占用文件句柄。

注意:如果你改了 DLL 代码重新编译,需要先在公式管理器里移除旧的 DLL 引用,再覆盖文件。否则 DLL 文件被客户端占用,复制会提示失败。

4.3 在公式里写第一行调用

注册完成以后,在 K 线图主图新建一个公式,写入:

A:TDXDLL1.MA(CLOSE,5),COLORWHITE; B:MA(CLOSE,5),COLORYELLOW;

A 是 DLL 算出来的 MA5,B 是通达信内置的 MA5。把两条线叠加在同一个主图上,如果接口和算法都正常,白色线和黄色线会完全重合。

第一次跑这个公式时,建议先把行情切换到日线级别、选择一根已经收盘的股票或指数,因为盘中最后一根 K 线的 CLOSE 还在变化,对比内置 MA 时容易出现“差一点对不上”的干扰。确认日线下重合之后,再切到分时周期看盘中表现。

4.4 判断接口有没有被真正执行

两线重合只能说明最终结果对,但有时候你改完代码重新编译,发现数值一点没变,这时候要先怀疑 DLL 根本没被重新加载。

我一般会在 CalcFunc 开头加一个全局计数:

// 调试用全局变量 volatile int g_nCallCount = 0; BOOL WINAPI CalcFunc(TDX_CALC_INFO* pInfo) { g_nCallCount++; // ... 原有逻辑 }

然后在公式里用一个自定义输出函数把这个计数显示出来,或者写到一个日志文件里。如果切换公式周期后计数不增长,说明 DLL 没有被重新调用,问题出在注册或缓存上,而不是算法。

5. 避坑与排查:装载失败、数值错位、白屏和缓存四类典型问题

5.1 现象:DLL 导入时报错或函数列表为空

导入 DLL 时提示不是有效的程序,或者导入成功但函数列表空白。

原因通常是两种:一是位数不匹配,编出来的 DLL 是 x64 而客户端是 32 位;二是导出符号被编译器修饰了,函数名变成_RegisterTdxFunc@4这样的形式,客户端找不到原始符号。还有一种比较隐蔽的是编译时用了/clr生成托管入口,通达信完全认不了。

解决方法是先把工程改成 32 位纯 C 编译,再确认导出函数声明了WINAPI。然后用 dumpbin 检查:

dumpbin /exports TDXDLL1.dll

输出里应该出现未修饰的RegisterTdxFunc和CalcFunc。如果看到@后缀或下划线前缀,去检查是不是没有加__stdcall或者没有用.def文件指定导出名。

5.2 现象:两线不重合,且所有点整体偏移一根K线

白色线和黄色线形状一样,但整体错开一根 K 线的位置,日线上看特别明显。

原因多半不是 DLL 算法算错,而是输入序列引用方式不同。内置 MA 直接引用当前 K 线 CLOSE,而公式里如果你写成TDXDLL1.MA(REF(CLOSE,1),5),那么输入序列整体滞后了一根。另一层干扰来自盘中最后一根未收盘 K 线,CLOSE 随时间变化,和内置 MA 对比时最后一根会出现动态偏差。

解决方法是先切到已收盘的日线再对比,把盘中变量排除掉;公式里只写CLOSE,不要加REF。确认这层没问题后,再检查 DLL 内部是不是对 pIn 的起始位置做了偏移,比如把pIn[i]误写成pIn[i+1]。

5.3 现象:切换周期后数值一直沿用上一周期的结果

先看日线数据正常,切到五分钟线后 DLL 输出的 MA 还是日线数值,完全不更新。

原因有两个层面。第一,公式引擎对计算结果做了缓存,周期性切换时没有触发 DLL 重新调用;第二,你自己的 DLL 代码里用了 static 变量保存中间结果,比如把上一周期的 sum 缓存下来,新周期数据进来时没有正确判断数据长度变化。

解决方法是先确认 DLL 是否被重新调用,可以用上一章的计数法。如果计数在增长但数值没变,那就是 DLL 内部缓存的问题。不要在 CalcFunc 里用 static 数组或 static 累加变量存放中间结果,每次调用都按pInfo->nDataCount重新计算。如果确实需要缓存,把数据长度和参数值拼接成缓存键,数据长度一变就整体失效。

5.4 现象:连续切换股票后软件白屏或闪退

快速翻票时软件偶尔白屏,严重时整个客户端退出。

这类问题大概率是内存越界。常见越界点有两个:一是 nMA 设置得比 nDataCount 还大,循环内读 pIn[i] 时已经越过有效区间;二是对 pfOutput 的写入超过了 nDataCount 范围。还有一种不太容易察觉的是在 CalcFunc 里修改 pfParam 数组内容,导致后续调用参数错乱,连锁引发崩溃。

解决方法是进入 CalcFunc 后先做防御性校验:

if (pInfo == NULL || pInfo->pfInput == NULL || pInfo->pfOutput == NULL) return FALSE; if (nMA <= 0 || nMA > pInfo->nDataCount) return FALSE;

这里把nMA > nDataCount也拦截掉,因为当 K 线根数少于均线周期时,滑动窗口根本不成立,直接返回 FALSE 比强行计算安全得多。

6. 验证与扩展:从 MA 到 EMA 的接口改造习惯

6.1 每次改完都跑一遍“两线重合”

我建议把验证公式存成模板文件,内容就是 DLL 输出和内置 MA 输出并排叠加。每次改完 DLL 代码,重新编译、导入,然后拉到已收盘的日线上跑一遍这个模板。凡是寄存器协议问题、参数位数问题、边界处理问题,都会在两线不重合时暴露出来。再抽查最后 10 根 K 线的具体数值,在图表上用光标逐根对比,能发现肉眼看不清的细微偏差。

6.2 把 MA 换成 EMA 只需动一个文件

MA 验证通过以后,算法层就可以做扩展了。EMA 和 MA 的差异只在递推公式上,接口层完全不用改:

// 在 ma_func.c 中追加 EMA 实现 int CalcEMA(const float* pIn, float* pOut, int nCount, int nParam) { if (nCount <= 0 || nParam <= 0) return 0; float k = 2.0f / (nParam + 1); double ema = pIn[0]; pOut[0] = (float)ema; for (int i = 1; i < nCount; i++) { ema = pIn[i] * k + ema * (1.0f - k); pOut[i] = (float)ema; } return 1; }

这段代码里,ema用 double 保持递推精度,输出转 float 时只在最后一刻截断,避免每一步都引入舍入误差。如果你想在通达信公式里同时调用 MA 和 EMA,需要在 RegisterTdxFunc 里把函数个数改成 2,并登记第二个函数名。这个扩展点正好验证了第 2 章接口层和算法层拆分的设计价值。

之前做 DLL 扩展时,有一次我在切换周期后盯着屏幕反复怀疑寄存器协议写错了,折腾很久才发现只是 DLL 内部一个 static 变量没清理。从那以后,我每次修改完 DLL,都会强制走一遍模板验证,并且重新编译后顺手看一眼 dumpbin 导出表,确认函数名没被编译器加料。这两个习惯让我少花了很多在客户端里反复试错的功夫,希望帮到你。

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

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

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

立即咨询