
简介面向股票软件二次开发人员的一份大智慧/通达信DLL插件编程源码与讲解资源。通达信官方DLL接口规范原文晦涩作者基于实际测试为示例代码补写了详细注释并修正了原工程中的一处错误主要位于MyPlugin.cpp便于开发者快速理解插件加载、数据交互等关键流程对大智慧、通达信两类软件的DLL扩展机制均有参考价值。资源包内含23个文件以h/cpp源码、dll/lib链接产物、pdb调试文件及工程配置dsp/dsw为主整体约829KB既保留了完整可编译工程也附带中间编译产物适合对照学习或直接改造。目前已有6879人学习下载。对于想在大智慧、通达信环境中编写自定义指标或扩展功能的C开发者来说这份资料能省去反复踩坑的时间理解示例逻辑后可自行在此基础上扩展更多实际应用场景。1. 为什么要自己写DLL插件——指标公式满足不了的需求1.1 公式引擎的边界在哪里先说个现象。通达信和大智慧自带的公式语言处理常规技术指标确实方便MA(C,5)、CROSS(...)这些一行就能搞定。但它的本质是一种面向序列的解释型脚本加载到内存里的是按时间排列的K线数组公式引擎逐行扫描、逐根K线计算。这种设计在80%的场景下没有问题可一旦遇到以下需求你就知道公式有多拧巴了需要做递归迭代前一根K线的结果参与后一根的逻辑判断公式里虽然能写REF但复杂的递归状态维护非常痛苦需要全市场横向对比比如同时对几千只股票做排序、打分、取分位数公式语言里的循环上限和死循环检测会让这事变得几乎不可行需要读取外部数据比如龙虎榜文件、自定义数据表、资金流数据公式语言没有标准的文件IO接口需要处理Level-2的逐笔成交、十档行情这类二进制大数据包靠脚本逐条解析相当费劲。公式语言还有个隐形天花板它跑在行情软件自己的进程里受限于软件主线程的调度节奏。你要是写一个计算量特别大的公式软件会直接卡死或者进入公式计算超时的提示。我最早就是在写一个跨周期、多品种共振的选股策略时被卡死的当时盯着计算超时四个字心里只有一个念头——得换个玩法。1.2 什么时候该上DLL插件这时候就必须请出DLL插件了。所谓DLL插件本质上就是把复杂的计算逻辑用C/C写好编译成一个动态链接库文件比如MyStrategy.dll放在行情软件的插件目录下然后在公式里通过类似DLLCALL的方式调用。行情软件把K线数据通过内存指针传递给你的DLL函数你的DLL算完再把结果写回缓冲区软件拿到结果正常画线、选股、预警。这带来的好处是实打实的算力解放C原生编译执行的效率比解释型公式强一个数量级。同样是遍历5000只股票做打分排序公式可能跑到软件没响应DLL几百毫秒就出结果。复杂逻辑随便写递归、动态规划、STL容器、自定义结构体DLL里没有任何公式引擎的限制。外部数据随意读DLL是普通Windows程序你可以读文件、访问HTTP接口、连接数据库把外部数据源和行情数据捏合在一起。二进制数据包处理这就是热搜词里反复出现的L2数据二进制数据包的关键场景。Level-2行情每秒都有大量逐笔成交和十档委托数据解析这种高频二进制流必须靠C/C级别的内存操作。当然DLL插件不是什么时候都应该上。如果你只是想写个MACD金叉选股用公式语言一小时就能搞定没必要为了写DLL去配环境、调接口。我的判断标准是公式脚本超过150行、或者涉及全市场扫描、或者需要访问外部数据就值得上DLL。凡是满足其中一个就不要再硬用公式扛了。2. 插件DLL的本质一个必须导出的函数2.1 接口规范与数据结构行情软件的DLL插件说穿了就是约定好的一组导出函数。软件在加载DLL后会调用约定的初始化函数然后在每根K线、每个周期、每个品种的计算节点调用主计算函数把行情数据包的指针递进去再从输出缓冲区把结果拿回来。因为数据是通过指针传递的整个过程没有复制开销这就是它快的原因。不同版本的软件、不同券商定制的版本接口签名会有差异。但核心的数据结构大同小异通常包含以下字段字段含义说明m_dwDate日期一般是整数格式比如20250110m_dwTime时间整型比如145500表示14点55分00秒m_fOpen开盘价float类型单位通常是元m_fHigh最高价float类型m_fLow最低价float类型m_fClose收盘价float类型m_fVolume成交量float类型注意单位可能是股也可能是手m_fAmount成交额float类型单位是元m_fReserve预留字段一般是0写代码时要注意几点字段类型别搞错日期、时间是DWORD32位无符号整数价格、成交量是float这和公式语言里所有数据都是浮点数不同。如果你按double去读float字节数据会变成天文数字。单位和精度成交量到底按股还是按手不同接口版本有差异。成交额有的接口预除过100有的没有。我建议在正式写逻辑之前先写一个探针DLL把原始字段打印出来和软件界面上显示的数值对一下再确认单位和精度。结构体对齐行情软件是用C编译的结构体默认按4字节对齐。你在自己的DLL里定义结构体时不要随便加#pragma pack(push, 1)这类对齐指令否则字段内存布局对不上读出来的全是乱码。这也是新手最常见的坑之一。2.2 开发环境与工程配置开发环境我推荐Visual Studio 2019 或 2022用经典Win32 DLL工程模板。需要注意几个关键配置平台选x86不要选x64。目前绝大多数行情软件主程序还是32位进程你编译一个64位DLL软件加载的时候直接报不是有效的Win32应用程序。这是新手最容易踩的坑我见过好几个朋友折腾一晚上最后发现只是平台选错了。调用约定与软件约定的导出函数默认用__cdecl或__stdcall需要对齐。建议直接用__declspec(dllexport)导出并确保函数签名和头文件完全一致。字符集建议使用多字节字符集不要用Unicode。插件场景下涉及文件名路径传递时多字节字符集的问题少一些。运行时库如果打算把DLL分发给别人用建议在C/C - 代码生成 - 运行库里选择/MT静态链接。这样DLL不依赖你机器上的VC运行库拿过去就能跑。但要注意静态链接的CRT在跨模块分配/释放内存时会有坑后面排查章节再说。还有个建议务必写日志。DLL在别人的机器上跑出了bug你根本没办法远程调试。一开始就在DLL里内置一个写日志文件的函数把关键输入参数、中间结果、异常分支都记下来。我所有正式版本的插件都有日志开关默认关闭排查问题时打开这个习惯能救你无数次。3. 完整源码讲解一个能跑通的选股插件3.1 代码逐段拆解下面给一个通用的DLL插件模板功能是计算最新收盘价相对前N日收盘价的涨幅百分比并输出到公式端。代码结构可以直接抄业务逻辑按需替换。// PluginDemo.cpp #include windows.h #include stdio.h // 行情K线数据结构字段顺序以常见版本为例 typedef struct _tagKLineData { DWORD m_dwDate; // 日期 DWORD m_dwTime; // 时间 float m_fOpen; // 开盘 float m_fHigh; // 最高 float m_fLow; // 最低 float m_fClose; // 收盘 float m_fVolume; // 成交量 float m_fAmount; // 成交额 float m_fReserve; // 预留 } KLINE_DATA, *LPKLINE_DATA; // 数据包结构一次传入一组K线序列 typedef struct _tagDataPack { LPKLINE_DATA lpData; // K线数组指针 DWORD dwDataCount; // K线根数 DWORD dwIndex; // 当前序列索引 } DATA_PACK, *LPDATA_PACK; // 日志开关调试时置1 static int g_dwDebug 1; static void WriteLog(const char* szMsg) { if (!g_dwDebug) return; FILE* fp fopen(D:\\PluginDemo.log, a); if (fp) { fprintf(fp, %s\n, szMsg); fclose(fp); } } // 初始化函数软件加载DLL后调用 __declspec(dllexport) int PluginInit(int* pVersion) { if (pVersion) *pVersion 0x00010000; WriteLog(PluginInit called); return 0; } // 主计算函数软件在每轮计算时调用 __declspec(dllexport) void PluginData( int nDataLen, // 每根K线float数量 float* pfOUT, // 输出缓冲区 DWORD* pdwOUT, // 输出掩码 LPDATA_PACK pDataPack, // 行情数据包数组 int nNum, // 数据包数量 DWORD* pdwOUTNum) // 输出数量 { if (!pDataPack || nNum 0) { WriteLog(PluginData invalid params); return; } for (int i 0; i nNum; i) { LPKLINE_DATA pK pDataPack[i].lpData; DWORD dwCount pDataPack[i].dwDataCount; if (!pK || dwCount 2) { pfOUT[i] 0.0f; continue; } // 取最新收盘价和昨日收盘价计算涨跌幅百分比 float fTodayClose pK[dwCount - 1].m_fClose; float fPrevClose pK[dwCount - 2].m_fClose; if (fPrevClose 0.0f) { pfOUT[i] (fTodayClose - fPrevClose) / fPrevClose * 100.0f; } else { pfOUT[i] 0.0f; } char szLog[256]; sprintf(szLog, idx%d count%d close%f prev%f out%f, i, dwCount, fTodayClose, fPrevClose, pfOUT[i]); WriteLog(szLog); } if (pdwOUTNum) *pdwOUTNum nNum; }这段代码的核心逻辑很简单遍历每个数据包 → 拿到K线数组 → 取最后一个收盘价和前一个收盘价 → 计算涨跌幅 → 写入输出缓冲区。几个要点pfOUT[i]和pDataPack[i]是一一对应的输出数组的下标必须和数据包下标对齐输出缓冲区pfOUT的每一根K线长度由nDataLen决定但公式端读取时通常只认前几个float位置所以业务输出尽量放在pfOUT[i]的位置不要随意偏移WriteLog在调试阶段一定打开验证数据正确后再关掉日志不要写到C盘根目录权限问题会坑你。这个模板在通达信公式里的调用方式类似涨幅:DLLCALL(PluginDemo.dll, PluginData, 涨幅);不同的软件调用语法有差异大智慧的历史版本中还有通过扩展数据方式加载DLL的。实际用的时候以软件自带的DLL调用说明为准。3.2 编译、部署与调试编译出PluginDemo.dll后把它放到行情软件的插件目录通常是软件安装目录下的Plugin或DLL子目录。然后打开公式编辑器新建一个指标调用DLL函数把输出值画在副图或者用于选股。调试有两条路日志调试在DLL里埋日志看输出的数值是否正常。日志文件路径最好用绝对路径而且程序需要对当前目录有写权限。我习惯把日志写到D盘的固定目录避免软件安装目录权限不足。VS附加进程调试在Visual Studio里打开DLL工程设置断点然后在调试 - 附加到进程里选择行情软件的主进程软件触发公式计算时断点就会命中。这种方法能直接看内存、查变量效率最高。但要注意附加后如果DLL是Release版本变量可能被优化掉所以调试阶段用Debug编译、发布时再切Release。还有一点每次编译DLL后行情软件必须完全退出再启动才能重新加载新版本的DLL。很多人改了代码编完直接点运行发现还是旧逻辑就是吃了这个亏。4. L2数据的二进制值包解析——真正拉开差距的地方4.1 L2数据长什么样热搜词里频繁出现的L2数据的二进制数据包值得单独说。Level-2行情相比普通行情核心差异在于新增了逐笔成交、十档买卖盘、委托队列这些细粒度数据。券商通过行情接口推送这些数据时为了降低带宽和解析开销几乎全部使用二进制编码而不是文本格式。一个典型的逐笔成交记录在二进制包里的长度通常在20到40字节之间包含市场标识、成交时间、成交价、成交量、成交金额、买卖订单号等字段。这种二进制包的特点是字段密集、对齐紧凑、单位约定特殊。比如成交价往往不是直接以元为单位而是乘以10000后的整数即价格精度到0.0001元成交金额可能乘以100。如果不知道这个约定把一个很大的整数当价格读出来你会觉得行情数据全乱了。4.2 实战解析思路解析二进制数据包第一步是定义结构体用#pragma pack(push, 1)取消字节对齐优化保证结构体按字段原始长度紧密排列。下面是一个简化版的逐笔成交结构示例#pragma pack(push, 1) typedef struct _tagTickRecord { BYTE bMarket; // 市场标识0深圳 1上海 BYTE bType; // 记录类型 DWORD dwTime; // 成交时间格式 HHMMSSmmm DWORD dwPrice; // 成交价实际价格 dwPrice / 10000.0 DWORD dwVolume; // 成交量单位股 DWORD dwAmount; // 成交金额单位元dwAmount / 100.0 DWORD dwBuyNo; // 买方委托序号 DWORD dwSellNo; // 卖方委托序号 } TICK_RECORD; #pragma pack(pop)拿到一段二进制内存pBuf后可以按字节偏移直接读取TICK_RECORD* pTick (TICK_RECORD*)pBuf; // 读取第2条记录时指针按结构体大小步进 TICK_RECORD* pTick2 (TICK_RECORD*)(pBuf sizeof(TICK_RECORD));重点提醒几个实战经验大小端问题国内行情接口绝大多数是小端Little-Endian也就是和x86处理器一致直接强制类型转换就能读。如果你拿到的是网络字节序的包就得先做字节序转换DWORD nPrice (dwRaw 0xFF) 24 | (dwRaw 0xFF00) 8 | (dwRaw 0xFF0000) 8 | (dwRaw 24) 0xFF;字节对齐的坑结构体定义乱加align或者不统一会导致sizeof(TICK_RECORD)和实际推送长度不一致后面的数据全错位。解析前建议先用前几条已知数据验证一下结构体长度。价格精度还原dwPrice / 10000.0f得到的是元不是直接转float。反过来当你组包发送时价格要乘以10000再取整不要用float强转会有精度误差。字段冗余校验有些数据包在尾部带CRC或长度字段解析时先校验。别偷懒L2数据量大一条错位就会让后续几千条全废校验字段是最后一道防线。实际应用中我在DLL插件里维护一个环形缓冲区把L2逐笔数据按序列号塞进去然后在公式端通过自定义输出值把最近一分钟的主买主卖成交量差等指标实时算出来。这个思路可以扩展到很多盘中监控策略比单纯看K线灵敏得多。5. 常见问题与排查技巧实录5.1 加载失败、DLL冲突这类拦路虎把我这些年踩过的坑整理成一张速查表遇到问题直接对号入座现象可能原因解决办法加载DLL时报找不到指定的模块缺少VC运行库或依赖的第三方DLL用/MT静态编译或者把VC运行库一起分发报不是有效的Win32应用程序平台选的x64软件是32位重新用x86编译软件启动时弹出DLL冲突提示插件目录存在同名DLL检查是否有多个版本混放删掉旧版调用DLL后指标不刷新DLL没有正确导出函数名用dumpbin /exports PluginDemo.dll检查导出表数据全是0或NaN结构体字段类型或对齐不对写日志打印原始字段值和软件界面核对其他机器运行崩溃依赖的VC运行时缺失改/MT静态链接重新编译关于网上常见的DLL修复工具我的看法是绝大多数场景下DLL缺失或冲突最好手动解决。修复工具能治标但可能把系统里其他软件的DLL版本也改掉引出一堆连锁问题。插件DLL的问题建议先用dumpbin或Dependencies工具看依赖缺什么补什么少依赖第三方库才是根治。5.2 数据异常、崩溃与调试心得开发DLL插件时遇到最头疼的问题是数据对了但软件崩了或软件没崩但结果全是乱的。这类问题多半出在内存管理上缓冲区越界pfOUT的长度是固定的你写入的下标如果超过nNum就会踩坏软件的内存。判断标准很简单只在0 i nNum范围内写。野指针和生命周期行情软件传递的K线数据指针只在本次调用内有效。如果你把指针存到全局变量里等下一次事件触发再访问软件可能已经释放了这块内存。必须在使用周期内完成计算或者把需要的数据拷贝到自己分配的内存里。多线程访问冲突有些软件版本会并行调用DLL函数如果你的DLL里用了全局变量会出现并发写冲突。对策是加锁或用线程局部存储但更简单的方式是保证导出函数内不依赖全局状态。模块边界分配内存DLL里new出来的内存不要在DLL卸载后再由软件释放。如果接口要求返回字符串或数组优先使用由调用方传入的缓冲区。还有一个很值得说的经验找一台干净的测试机器。我遇到过DLL在公司电脑调试得好好的发给用户后一运行就崩最后排查发现是用户安装了某个拦截软件给进程注入了钩子影响了DLL的内存分配。这种环境问题没办法从代码层面彻底解决但尽量静态链接、减少依赖、不用全局钩子能显著降低踩中的概率。最后分享一个我自己的小习惯每个DLL插件内部都放一个隐蔽的自检模式通过约定一个特殊参数触发。自检模式下DLL会生成一组已知结果的模拟数据算完以后和预期比对输出一个自检报告到日志文件。每次发布新版本前先跑一遍自检再上实盘。这花不了几分钟但能挡住大部分低级错误。写插件这么多年最大的体会就是DLL插件本身并不难难的是把各种边界条件和环境差异都处理干净。只要数据结构和生命周期这两件事儿想清楚了你就已经超过80%的初学者了。本文还有配套的精品资源点击获取