
简介通达信插件开发资料包面向具有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-N1 累加到 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]; // 窗口滑出旧值inMA 时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 算出来的 MA5B 是通达信内置的 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 位二是导出符号被编译器修饰了函数名变成_RegisterTdxFunc4这样的形式客户端找不到原始符号。还有一种比较隐蔽的是编译时用了/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[i1]。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 导出表确认函数名没被编译器加料。这两个习惯让我少花了很多在客户端里反复试错的功夫希望帮到你。本文还有配套的精品资源点击获取