通达信DLL加密指标:从公式保护到C++编译实战
1. 为什么要把指标逻辑藏进DLL做通达信指标公式这行有些年头了从最早的公式管理器里手写MA、MACD到后来帮人写各种主力追踪、三步点金、波段大师之类的自定义指标我见过太多人辛辛苦苦调出来的参数组合被人复制粘贴走改个名字就到处发。公式源码这东西一旦以文本形式存在就等于裸奔——公式管理器里能看导出的公式文件能看甚至截图发群里都能被人照着敲出来。所以当有人问我“怎么让通达信指标代码不被看到”的时候我一般只给一个方向把核心计算逻辑从公式语言里搬出去编译成DLL公式里只留一行调用。这就是通达信DLL加密指标的核心思路。它解决的不是“加密”这个动作本身而是把可读的公式逻辑变成不可读的二进制机器码同时保留通达信调用外部函数的能力。这篇文章适合两类人看一类是手里有自己打磨多年的指标公式、想保护劳动成果的实战派另一类是对通达信公式系统底层机制好奇、想搞清楚DLL调用到底怎么回事的技术型玩家。我会把整个流程从环境准备到编译输出讲透包括我踩过的坑和几个关键参数的设置逻辑。你不需要是C高手但得愿意动手配一次环境。2. 通达信DLL机制到底是怎么运转的2.1 公式系统与外部函数的接口逻辑通达信本身有一套公式解释器你写的每一行公式比如MA(CLOSE,5)最终都会被解释器翻译成对内部函数的调用。而DLL机制的本质是通达信在解释器里开了一个口子允许公式调用外部动态链接库中导出的函数。这个口子就是EXTERNAL函数。公式里写EXTERNAL(MyDll.dll,MyFunc,参数1,参数2)通达信在计算这根K线的时候就会去加载指定的DLL找到名为MyFunc的导出函数把参数传进去拿回一个数值结果。整个过程对公式使用者是透明的——他看到的就是一个普通的指标线但实际计算发生在DLL内部。这里有个关键点通达信对DLL的调用是逐K线进行的。也就是说如果你的指标要显示1000根K线的值通达信会调用你的函数1000次每次传入当前K线的数据。这意味着你的DLL函数必须是“无状态”或者自己管理状态的不能依赖上一次调用的残留数据来做本次计算除非你显式地用静态变量保存。2.2 为什么DLL能实现代码隐藏公式语言是解释执行的源码必须以文本形式存在才能被解释器读取。而DLL是编译后的二进制文件里面是机器码没有对应的源码根本无法还原出原始逻辑。你可以把公式语言想象成一张写着菜谱的纸谁拿到都能照着做DLL则像是一台封装好的料理机你按按钮它出结果但里面刀片怎么转的、转速多少外面看不出来。更进一步你甚至可以在DLL里做反调试、代码混淆、字符串加密让逆向工程的成本高到不值得。当然对于大多数指标保护场景单纯的编译成DLL已经足够——毕竟会逆向二进制的人大概率也不会去抄你的股票指标。2.3 通达信版本与DLL的兼容性坑这里必须先泼一盆冷水不是所有通达信版本都支持DLL调用也不是所有版本用的DLL接口都一样。我实测下来券商版通达信和官方免费版在DLL支持上差异很大。有些券商版直接阉割了EXTERNAL函数你公式里写了也报错。有些版本虽然支持但只允许加载特定目录下的DLL路径写错了就找不到。比较稳妥的做法是先在你的通达信里新建一个公式只写一行EXTERNAL(test.dll,test)保存看是否报错。如果提示函数不存在那这个版本大概率不支持后面就不用折腾了。如果提示找不到DLL文件说明支持只是文件没放对位置。通常DLL需要放在通达信安装目录的T0002\dlls文件夹下没有这个文件夹就自己建一个。3. 动手之前的准备工作3.1 编译环境的选择与配置编译DLL需要C编译器。Windows平台上最省事的是Visual Studio社区版免费功能完整。我目前用的是VS2019够用且稳定。安装的时候记得勾选“使用C的桌面开发”工作负载里面包含了MSVC编译器和Windows SDK这两个是必须的。如果你不想装几个G的VS也可以用MinGW-w64体积小很多。但MinGW编译出来的DLL在通达信里加载时偶尔会遇到运行时库缺失的问题需要额外拷贝libgcc和libstdc的DLL过去。所以我还是推荐VS一次配好省心。安装完VS后新建项目选择“动态链接库(DLL)”项目类型选C不要选空项目让VS帮你生成基本的导出模板。项目建好后把平台改成x64还是x86这取决于你的通达信是32位还是64位。目前大多数通达信版本还是32位的所以编译目标平台选x86Win32。如果你编译了64位DLL放到32位通达信里会直接加载失败而且报错信息很不明显可能只是指标线不显示排查起来很费时间。3.2 通达信DLL的函数签名要求通达信调用DLL函数时对函数的签名有固定要求。不是随便写个函数就能被调用的。标准的通达信DLL导出函数签名是这样的extern C __declspec(dllexport) void MyFunc( int nCount, // 数据个数 float* pfIN, // 输入数据数组 float* pfOUT, // 输出数据数组 int nParam1, // 参数1 int nParam2, // 参数2 int nParam3 // 参数3 );注意几个关键点extern C是必须的否则C的名称修饰会让通达信找不到函数__declspec(dllexport)声明导出返回类型是void结果通过pfOUT数组返回。nCount是本次计算的数据总量pfIN是输入数据pfOUT是你需要填充的输出数组。这个签名和网上很多老教程里写的可能不太一样有些老版本通达信用的是__stdcall调用约定。我实测下来目前主流版本用的是__cdecl也就是默认的C调用约定。如果你编译出来的DLL加载后指标不显示可以试试在函数声明前加__stdcall再编译一次。3.3 公式端的调用写法DLL编译好之后公式端的写法相对简单。假设你的DLL叫MyIndicator.dll导出函数叫CalcMain公式里这样写EXTERNAL(MyIndicator.dll,CalcMain,CLOSE,VOL)这行公式的意思是把收盘价和成交量作为输入传给DLL的CalcMain函数函数返回的结果作为指标值。注意EXTERNAL函数的第一个参数是DLL文件名第二个是函数名后面跟的是输入参数。输入参数的个数要和DLL函数里pfIN数组的预期长度对应。这里有个容易搞混的地方通达信传给DLL的pfIN是一个一维数组如果你传了多个输入参数它们会按顺序拼接在这个数组里。比如上面传了CLOSE和VOL那么pfIN[0]到pfIN[nCount-1]是收盘价pfIN[nCount]到pfIN[2*nCount-1]是成交量。你在DLL里取数据的时候要按这个偏移量来取。4. 从零写一个可用的加密指标DLL4.1 核心计算函数的编写我拿一个简单的均线交叉逻辑来演示实际你可以把任何复杂的指标逻辑放进去。假设我要实现一个“5日均线上穿20日均线时输出1否则输出0”的信号指标在DLL里这样写#include windows.h #include vector extern C __declspec(dllexport) void CrossSignal( int nCount, float* pfIN, float* pfOUT, int nParam1, int nParam2, int nParam3) { // nParam1和nParam2分别对应短期和长期均线周期 int shortPeriod nParam1 0 ? nParam1 : 5; int longPeriod nParam2 0 ? nParam2 : 20; // 计算短期均线 std::vectorfloat shortMA(nCount, 0.0f); std::vectorfloat longMA(nCount, 0.0f); for (int i 0; i nCount; i) { if (i shortPeriod - 1) { float sum 0.0f; for (int j 0; j shortPeriod; j) { sum pfIN[i - j]; } shortMA[i] sum / shortPeriod; } if (i longPeriod - 1) { float sum 0.0f; for (int j 0; j longPeriod; j) { sum pfIN[i - j]; } longMA[i] sum / longPeriod; } } // 判断交叉 for (int i 0; i nCount; i) { if (i 0 shortMA[i] longMA[i] shortMA[i-1] longMA[i-1]) { pfOUT[i] 1.0f; } else { pfOUT[i] 0.0f; } } }这段代码的逻辑很直白先算两条均线然后逐根K线判断是否发生金叉。pfOUT数组里存的就是最终输出给通达信画图的值。你可以把任何公式语言能实现的逻辑翻译成C放进来而且C的执行效率比公式解释器高得多对于计算量大的指标用DLL反而能提升通达信的响应速度。4.2 参数传递与动态周期处理上面代码里用到了nParam1和nParam2这两个参数是从公式端传进来的。公式端这样写EXTERNAL(MyIndicator.dll,CrossSignal,CLOSE,5,20)注意EXTERNAL函数的参数传递规则第一个是DLL名第二个是函数名后面可以跟多个参数。但通达信对EXTERNAL的参数个数有限制我实测最多支持到10个左右。前几个参数如果是序列数据比如CLOSE、VOL会进入pfIN数组后面的常数参数会依次进入nParam1、nParam2、nParam3。但这里有个坑通达信只给DLL函数预留了3个整数参数位如果你需要传更多参数得把多个参数打包成一个或者用其他方式传递。我一般的做法是把最关键的几个周期参数通过nParam1到nParam3传其他配置项直接硬编码在DLL里。反正DLL是编译好的别人也看不到你硬编码了什么。4.3 编译输出与文件放置代码写好后在VS里选择“生成解决方案”如果没报错会在项目目录的x86\Debug或x86\Release下生成DLL文件。一定要用Release模式编译Debug模式编译出来的DLL依赖调试运行时库放到没有VS环境的机器上会加载失败。Release模式体积小、依赖少兼容性最好。编译出来的DLL文件复制到通达信安装目录下的T0002\dlls文件夹。如果这个文件夹不存在手动创建一个。然后重启通达信让它在启动时扫描DLL目录。有些版本不需要重启但重启是最稳妥的。放好之后在公式管理器里新建一个技术指标公式写上EXTERNAL调用那行保存。如果一切正常指标线就会显示出来。如果没显示先检查公式有没有语法错误再检查DLL文件名和函数名是否完全匹配——大小写敏感MyIndicator.dll和myindicator.dll在通达信看来是两个不同的文件。5. 加密之外的那些实战细节5.1 输入数据的边界处理通达信传给DLL的nCount是当前图表上加载的K线总数。但你在计算均线的时候前几根K线是不够周期计算的。上面代码里我用if (i shortPeriod - 1)做了判断不够周期的位置输出0。但实际使用中输出0可能会导致指标线从0突然跳到均线值看起来很不美观。更好的做法是不够周期的位置输出一个无效值比如-1或者通达信约定的无效值。通达信对无效值的处理是跳过不画这样指标线会从有有效值的地方开始显示视觉上更干净。我一般用-1.0f作为无效标记然后在公式端配合IF函数过滤。另外要注意pfIN数组的读取越界问题。通达信保证pfIN至少有nCount个元素但如果你传了多个输入参数总长度是nCount * 参数个数。你在取第二个参数的时候偏移量要算对否则会读到错误的数据指标结果就完全不对了。5.2 多输出指标的实现方式一个DLL函数只能返回一个输出数组但很多指标需要同时画多条线比如主力追踪指标可能同时输出主力线、散户线、差值线。怎么用DLL实现多输出有两种方案。第一种是编译多个导出函数每个函数负责一条线公式里写多行EXTERNAL调用。这种方案简单直接但每个函数都要重新计算一遍公共部分效率低。第二种方案是在一个函数里计算所有输出然后通过全局变量或者输出数组的分段来传递。通达信的pfOUT数组长度是nCount你没法在一个数组里塞多条线。我常用的做法是把多条线的结果分别写入不同的全局数组然后每个导出函数只负责从全局数组里读取对应位置的数据返回。这样公共计算只做一次多个导出函数共享结果。但要注意全局数组的线程安全问题——通达信可能在多个线程里同时调用DLL如果多个指标同时运行全局数组会被覆盖。解决办法是用线程局部存储TLS或者加锁但加锁会影响性能。对于单指标场景全局数组够用了。5.3 调试DLL的笨办法与巧办法调试DLL比调试普通程序麻烦因为DLL是被通达信加载的你没法直接运行它。我常用的笨办法是在DLL里把中间结果写到文本文件里然后看文件内容来判断计算是否正确。比如在函数开头写FILE* fp fopen(C:\\debug_log.txt, a); fprintf(fp, nCount%d, nParam1%d\n, nCount, nParam1); fclose(fp);这样每次通达信调用DLL都会往日志文件里追加一条记录。你打开日志就能看到通达信实际传了什么参数进来跟你预期的是否一致。这个办法虽然土但极其有效我靠它排查过好几次参数传递错误的问题。巧办法是用VS的“附加到进程”功能。先把通达信启动起来然后在VS里点“调试”-“附加到进程”找到通达信的进程附加进去。然后在DLL代码里下断点当通达信调用DLL时就会命中断点你可以像调试普通程序一样单步执行、查看变量。但这个办法有个前提你的DLL必须是Debug模式编译的并且PDB符号文件要和DLL放在一起。Release模式编译的DLL优化过断点可能不准。6. 常见问题速查与避坑指南6.1 DLL加载失败排查表现象可能原因排查方法公式保存时报“函数未定义”通达信版本不支持EXTERNAL换官方免费版或支持DLL的券商版指标线完全不显示DLL文件没放对位置确认DLL在T0002\dlls目录下指标线显示但全是0函数名或DLL名大小写不匹配逐字符核对公式里的名称指标线显示但数值异常pfIN数据偏移量算错用日志输出pfIN前几个值验证通达信启动变慢或卡死DLL里有死循环或阻塞操作检查循环边界避免在DLL里做网络请求换台电脑就不工作缺少运行时库用Release模式编译或静态链接运行时库6.2 我踩过的三个印象最深的坑第一个坑是平台位数不匹配。早期我编译了一个64位DLL放到32位通达信里指标死活不显示也没有任何报错。我排查了两个小时最后用Dependency Walker打开DLL才发现是64位的。通达信加载DLL失败时基本是静默的不会弹窗告诉你原因所以位数问题一定要一开始就确认好。第二个坑是函数名修饰。C编译器会对函数名进行名称修饰name mangling比如CrossSignal可能被编译成?CrossSignalYAXHPEAM0HHHZ这样的形式。通达信按名字找函数找不到修饰后的名字就加载失败。解决办法就是加extern C强制编译器用C语言的命名规则不做修饰。第三个坑是输入数组的生命周期。通达信传给DLL的pfIN指针只在本次调用期间有效。如果你在DLL里把这个指针存到全局变量里下次调用时再访问就是访问已释放的内存轻则数据错误重则通达信崩溃。我早期写过一个指标就是把pfIN存了全局结果通达信运行几分钟就闪退排查了很久才定位到这个问题。永远不要保存通达信传进来的指针需要的数据当场拷贝走。6.3 关于“加密强度”的实话编译成DLL确实能挡住99%的普通用户但挡不住真正懂逆向的人。如果有人用IDA Pro之类的工具反汇编你的DLL还是能看出逻辑的。要提高逆向成本可以在DLL里加一些混淆手段把关键常量拆成多个变量运算得到、插入无意义的跳转指令、对字符串进行加密存储。但这些手段对于股票指标来说性价比不高——你的指标逻辑再复杂核心也就是几条均线和条件判断逆向出来的成本可能比重新写一个还低。所以我的建议是DLL加密的目标是“防止随手复制”而不是“绝对不可破解”。把精力放在指标逻辑本身的独特性和实战效果上比花大力气做反逆向更值得。毕竟指标的核心价值在于参数组合和逻辑思路这些东西即使DLL被逆向了别人也未必能理解你为什么这么设计。7. 公式端与DLL端的配合技巧7.1 用公式做数据预处理DLL虽然强大但有些操作在公式端做更方便。比如复权处理、数据平滑、条件过滤这些用公式语言写几行就搞定没必要都塞进DLL。我通常的分工是公式端负责数据准备和结果展示DLL负责核心计算逻辑。举个例子如果你要做一个基于复权价格的指标可以在公式端先用COND或者复权函数把价格处理好再把处理后的序列传给DLL。这样DLL里就不用关心复权逻辑代码更简洁。公式端还可以做信号过滤比如DLL输出原始信号后公式端用FILTER函数过滤掉连续信号只保留第一次触发。7.2 参数动态调整的实现通达信的公式参数可以在公式管理器里设置用户能自己调。如果你把参数传给DLL用户改参数时DLL会自动收到新值。但要注意通达信对DLL的调用是逐K线的参数在每次调用时都会传。所以你在DLL里不需要缓存参数每次用nParam1这些值就行。但有个细节如果用户把参数设成0或者负数你的DLL要做好容错。我一般会在DLL开头做参数校验非法值就用默认值替代。这样即使用户乱设参数指标也不会计算出错或者崩溃。7.3 输出结果的格式约定通达信对DLL输出的pfOUT数组有格式要求每个元素对应一根K线的指标值类型是float。如果你输出的值超出了通达信的显示范围比如特别大或者特别小指标线可能会显示异常。我一般会把输出值限制在合理范围内比如信号类指标输出0或1均线类指标输出实际价格。另外通达信画图时对无效值的处理是如果某个位置的输出是特定值不同版本可能不同常见的是0或者-1可能会跳过不画。你可以利用这个特性来控制指标线的起始位置。但不同版本行为不一致稳妥的做法还是在公式端用IF函数过滤。8. 从单指标到指标体系的扩展思路8.1 多个DLL的协同工作当你有一组相关指标时可以把它们编译到同一个DLL里导出多个函数。这样只需要维护一个DLL文件公式端分别调用不同函数即可。好处是公共的计算逻辑可以复用比如多个指标都需要计算均线可以在DLL内部写一个公共函数各个导出函数调用它。但要注意同一个DLL被多个公式同时调用时如果DLL里有全局状态可能会出现竞争。解决办法是尽量让每个导出函数无状态所有数据都通过参数传入和输出返回。如果确实需要共享状态用线程局部存储来隔离。8.2 指标逻辑的模块化设计写DLL的时候不要把所有的计算都堆在一个函数里。我习惯把指标逻辑拆成几个模块数据预处理模块、核心计算模块、信号生成模块、输出格式化模块。每个模块是一个独立的C函数导出函数只负责按顺序调用这些模块。这样代码好维护也方便单独测试每个模块。比如一个主力追踪指标数据预处理模块负责计算换手率、量比等基础数据核心计算模块负责计算主力资金流向信号生成模块负责判断买卖点输出格式化模块负责把结果整理成通达信需要的格式。每个模块都可以单独用测试数据验证出了问题也容易定位。8.3 版本更新与向后兼容DLL一旦发布出去用户可能已经用你的指标做了很多分析。如果你更新了DLL改变了输出逻辑或者参数含义用户的既有分析结果就会变化。所以更新DLL时要考虑向后兼容新增功能用新的导出函数不要修改已有函数的签名和行为如果必须修改就在DLL文件名里加版本号比如MyIndicator_v2.dll让用户自己选择用哪个版本。我一般会在DLL里保留一个版本号导出函数公式端可以调用它来显示当前DLL版本。这样用户遇到问题时你能快速确认他用的哪个版本排查效率高很多。9. 一些关于指标保护的实在话把指标逻辑编译成DLL技术上确实能实现代码隐藏但这件事的意义要分两面看。一方面如果你花了大量时间打磨出一套有效的指标DLL加密能防止它被轻易复制传播保护你的劳动成果。另一方面股票指标的核心价值从来不在于代码本身而在于使用它的人对市场的理解和执行力。同一套指标不同的人用结果可能天差地别。我见过太多人到处收集“国宝级指标源码”“顶底信号98%指标”拿到手之后发现根本用不好。指标只是工具工具背后的逻辑和纪律才是关键。所以如果你决定走DLL加密这条路我的建议是把精力放在指标逻辑的实战验证上确保它真的有效加密只是保护手段不要本末倒置。另外通达信版本更新频繁今天能用的DLL接口明天可能就变了。我一般会在DLL里做版本检测如果发现通达信版本不兼容就输出一个提示信号而不是直接崩溃。这样用户至少知道是兼容性问题而不是以为指标坏了。最后分享一个我常用的调试技巧在DLL里加一个“调试模式”开关通过nParam3传入。当nParam3等于特定值时DLL把中间计算过程输出到日志文件正常使用时nParam3为0不输出日志。这样既不影响正常使用又能在需要时快速获取调试信息。这个开关我用了好几年排查问题时特别管用。