简介:本资源为通达信平台缠论技术分析功能的C++ DLL模块开源实现,面向量化交易开发者、证券软件二次开发工程师及缠论算法研究者,提供可嵌入通达信客户端的底层指标计算逻辑。源码基于VC++6.0开发,完整呈现缠论笔、线段、中枢等核心结构的C++建模与递归判定逻辑,适合作为学习经典技术分析算法工程化落地的实践范例。压缩包为RAR格式,共含若干C++源文件(.cpp/.h)、项目工程文件(.dsp/.dsw)及编译说明,总大小891KB,结构紧凑,便于快速编译调试与逻辑验证。已有1201人学习下载,读者可直接获取可运行的DLL接口定义、缠论各级别走势分解的完整状态机实现、以及适配通达信数据结构的封装层代码,对理解技术指标DLL开发规范与缠论自动化识别具有较强参考价值。
1. 缠论DLL不是“插件包”,而是通达信指标生态里可编译、可调试、可嵌入的C++计算内核
很多刚接触通达信量化开发的人,看到“缠论DLL”第一反应是下载一个现成的.dll文件双击安装——结果发现根本无法加载,或者加载后指标不显示、K线不识别、分笔出错。这不是你电脑缺VC运行库,也不是通达信版本太低,而是从根本上混淆了“DLL”的角色:它不是封装好的黑盒工具,而是用C++实现缠论核心逻辑(如分笔、画线、中枢识别、走势类型判定)的动态链接库,必须与通达信的指标调用协议严格对齐,且需在Visual Studio中编译为x86架构、/MT静态链接、无CRT依赖的纯Win32 DLL。这类源码通常由资深交易者或C++工程师维护,比如标题中提到的southern977和perhapsryf,他们公开的代码不是为了“一键导入”,而是提供可审计、可定制、可对接实盘信号的底层计算能力。适合三类人:想把缠论规则真正落地为自动识别逻辑的量化开发者;需要在通达信主图/副图中嵌入高精度缠论结构标记的策略研究员;以及正在从公式语言(TDX公式)转向C++高性能计算的进阶用户。它解决的不是“怎么画线”,而是“为什么这样画线才符合缠论原文定义”。
2. 用Visual Studio 2019编译缠论C++源码生成x86通达信兼容DLL的最小可行流程
通达信所有版本(含最新V7.85)仅支持x86架构DLL,且明确要求导出函数使用__stdcall调用约定、无C++异常传播、不依赖外部DLL(如MSVCP140.dll)。这意味着你不能直接用CMakeLists.txt生成默认配置的DLL,也不能用MinGW或Clang编译。必须用Visual Studio原生工具链,并手动关闭所有可能引入运行时依赖的选项。
2.1 创建空DLL项目并配置关键编译参数
打开Visual Studio 2019 → 新建项目 → “动态链接库(DLL)” → 项目名设为ChanLunCore→ 目标平台选“x86”(不是x64!)→ 点击“创建”。
进入项目属性页(右键项目 → 属性),按顺序修改以下五项:
- 配置属性 → 常规 → 字符集:设为“使用多字节字符集”(不是Unicode)
- 配置属性 → C/C++ → 代码生成 → 运行时库:设为
/MT(多线程,静态链接) - 配置属性 → 链接器 → 高级 → 导入库:清空该字段(避免生成.lib)
- 配置属性 → 链接器 → 高级 → 入口点:填入
DllMain(确保Windows加载时能初始化) - 配置属性 → 链接器 → 输入 → 忽略所有默认库:设为“是”(/NODEFAULTLIB)
提示:
/MT是硬性要求。若用/MD(动态链接CRT),通达信加载时会报OSERROR: [WinError 1114] 动态链接库(DLL)初始化例程失败——这不是DLL损坏,而是CRT初始化冲突。/NODEFAULTLIB则防止链接器偷偷拉入kernel32.lib等非必要依赖。
2.2 实现通达信DLL标准导出接口:GetIndicatorData
通达信通过固定函数名调用DLL,最核心的是GetIndicatorData。它接收K线数据指针、周期参数、输出缓冲区地址,返回计算结果。以下是精简但完整的实现骨架(需替换为你实际的缠论逻辑):
// ChanLunCore.cpp #include "stdafx.h" #include <vector> #include <algorithm> // 通达信要求的导出函数签名(必须__stdcall,参数顺序不可变) extern "C" __declspec(dllexport) int __stdcall GetIndicatorData( double* pOpen, // 开盘价数组,按时间倒序(最新在前) double* pHigh, // 最高价 double* pLow, // 最低价 double* pClose, // 收盘价 double* pVolume, // 成交量 int nCount, // K线总数(通常≥500) double* pOutBuffer, // 输出缓冲区,长度由调用方分配 int nOutLen, // 输出缓冲区总长度(单位:double) int nParam1, // 用户自定义参数1(如分笔阈值) int nParam2, // 用户自定义参数2(如中枢震荡次数) int nParam3, // 用户自定义参数3(如走势类型过滤开关) char* pStrParam // 字符串参数(如“笔起点标记”) ) { if (nCount < 3 || !pOutBuffer || nOutLen < 100) return -1; // 步骤1:将倒序K线转为正序(缠论计算需从左到右遍历) std::vector<double> open(pOpen, pOpen + nCount); std::reverse(open.begin(), open.end()); // 同理处理high/low/close/volume... // 步骤2:执行缠论分笔(示例:简单顶底分型+包含处理) std::vector<int> biStarts; // 存储笔起点索引(正序索引) for (int i = 2; i < nCount - 2; ++i) { bool isTop = (open[i-2] < open[i-1]) && (open[i-1] < open[i]) && (open[i] > open[i+1]) && (open[i+1] > open[i+2]); bool isBottom = (open[i-2] > open[i-1]) && (open[i-1] > open[i]) && (open[i] < open[i+1]) && (open[i+1] < open[i+2]); if (isTop || isBottom) biStarts.push_back(i); } // 步骤3:写入输出缓冲区(通达信要求:每组数据占3个double,[x,y,value]) int writePos = 0; for (size_t j = 0; j < biStarts.size() && writePos + 3 <= nOutLen; ++j) { int idx = biStarts[j]; // 正序索引 pOutBuffer[writePos++] = static_cast<double>(idx); // X坐标(K线索引) pOutBuffer[writePos++] = open[idx]; // Y坐标(价格) pOutBuffer[writePos++] = 1.0; // value(标记类型,1=笔起点) } return writePos / 3; // 返回有效点数 }参数说明与通达信调用逻辑:
pOpen等数组是倒序排列(最新K线在索引0),这是通达信为兼容历史数据设计的约定,必须反转后再做缠论分析;nOutLen是调用方预分配的double数量,不是字节数,每个绘图点需3个double(X,Y,Value),因此最大可写点数为nOutLen/3;nParam1~3对应通达信公式中的PARAM#1、PARAM#2等,在TDX公式里写DRAWICON(REF(C,1)>REF(C,2) AND ... , L, 10)时,10即传给nParam1;pStrParam用于传递字符串指令,如"DRAWLINE"表示启用连线,"NO_LABEL"表示禁用文字标注。
2.3 添加DllMain确保线程安全与资源隔离
通达信可能在多个窗口同时加载同一DLL,必须保证全局状态不冲突。DllMain是唯一安全的初始化入口:
// DllMain.cpp #include "stdafx.h" // 每个线程独立的缠论计算上下文(避免static变量跨线程污染) __declspec(thread) struct ChanContext { std::vector<int> lastBiPoints; int lastCalcTime; } g_threadContext; BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // 进程加载时,可初始化全局只读资源(如小数点精度表) break; case DLL_THREAD_ATTACH: // 线程加载时,清空本线程上下文 g_threadContext.lastBiPoints.clear(); g_threadContext.lastCalcTime = 0; break; case DLL_THREAD_DETACH: // 线程卸载时,释放本线程独占资源 g_threadContext.lastBiPoints.clear(); break; case DLL_PROCESS_DETACH: // 进程卸载时,释放全局资源(如有) break; } return TRUE; }注意:通达信指标计算是单线程调用,但
DllMain仍需按规范实现DLL_THREAD_ATTACH/DETACH,否则在多窗口场景下可能出现内存越界。__declspec(thread)是Windows特有语法,确保每个线程有独立副本,比static更安全。
3. 在通达信中注册并调用缠论DLL:TDX公式语法与调试定位技巧
编译生成的ChanLunCore.dll不能直接拖入通达信目录,必须通过TDX公式显式声明调用路径,并严格匹配参数类型与返回值。通达信的DLL调用机制是“弱类型”设计,错误不会报详细异常,只会静默失败或返回0,因此调试必须前置。
3.1 TDX公式中声明DLL函数并绑定参数
在通达信公式编辑器中新建公式,输入以下内容(注意路径、函数名、参数个数必须与C++源码完全一致):
// 缠论笔识别主图指标 {参数} N1:=10; // 分笔波动阈值(%) N2:=3; // 中枢最小段数 N3:=1; // 1=启用笔标记,0=仅计算不绘图 // 声明DLL函数(路径必须是绝对路径或相对通达信根目录) DLLName:='ChanLunCore.dll'; FuncName:='GetIndicatorData'; // 调用DLL(参数顺序:OPEN,HIGH,LOW,CLOSE,VOLUME, COUNT, OUTPUT_BUFFER, OUTPUT_LEN, PARAM1, PARAM2, PARAM3, STR_PARAM) Result := CALLDLL(DLLName, FuncName, OPEN, HIGH, LOW, CLOSE, VOL, DATACOUNT, REF(BI_BUFFER,1), 1000, N1, N2, N3, 'DRAWICON'); // 解析返回结果(每个点占3个元素:X,Y,Value) IF Result > 0 THEN BEGIN FOR I := 0 TO Result-1 DO BEGIN X := REF(BI_BUFFER,1)[I*3+0]; // K线索引(正序) Y := REF(BI_BUFFER,1)[I*3+1]; // 价格 V := REF(BI_BUFFER,1)[I*3+2]; // 类型 IF V = 1 THEN DRAWICON(X, Y, 10); // 画笔起点图标(图标ID 10) END; END;关键细节解析:
REF(BI_BUFFER,1):BI_BUFFER是公式中预定义的ARRAY型变量(长度≥1000),REF(...,1)取其上一根K线的值,确保DLL写入时缓冲区已分配;DATACOUNT:通达信内置函数,返回当前周期K线总数,不是BARPOS(BARPOS是当前K线索引,从0开始);'DRAWICON'字符串参数:C++代码中用strcmp(pStrParam, "DRAWICON")==0判断是否启用图标绘制;- 图标ID
10:对应通达信内置图标库,DRAWICON(X,Y,10)在坐标(X,Y)处画一个小三角形,常用于标记笔起点。
3.2 定位DLL加载失败的三大日志入口
当指标不显示、CALLDLL返回-1或0时,不要盲目重装VC运行库,按以下顺序排查:
| 排查层级 | 操作方法 | 典型现象与原因 |
|---|---|---|
| 文件层 | 将ChanLunCore.dll复制到TdxW.exe同目录(如C:\tdx\Trade\),用Dependency Walker(x86版)打开DLL,检查右侧列表是否只有KERNEL32.dll、USER32.dll等系统DLL | 若出现MSVCP140.dll、VCRUNTIME140.dll,说明未设/MT,需回VS重新编译 |
| 注册层 | 在通达信公式编辑器中,点击“测试”按钮,观察右下角状态栏是否提示“DLL加载成功” | 若提示“找不到指定模块”,检查DLL路径是否含中文、空格或长路径(建议放根目录) |
| 函数层 | 在C++代码GetIndicatorData开头插入OutputDebugStringA("DLL called");,用DebugView(Sysinternals工具)捕获输出 | 若无任何输出,说明通达信根本未调用该函数——检查FuncName拼写、大小写、是否多空格 |
提示:通达信的
CALLDLL函数在x86模式下对函数名大小写敏感,getindicatordata和GetIndicatorData被视为不同函数。务必与__declspec(dllexport)声明的名称完全一致。
4. 缠论DLL性能优化:减少内存拷贝、规避浮点精度陷阱、适配通达信实时行情流
通达信实时行情推送频率可达毫秒级,而缠论计算(尤其中枢递归)复杂度为O(n²),若每次Tick都全量重算500根K线,CPU占用会飙升至30%以上。必须采用增量计算与缓存策略,且警惕通达信数据精度导致的分型误判。
4.1 实现K线增量更新机制:只重算最后N根
通达信在实时行情中,DATACOUNT每新来一根K线就+1,但旧K线数据不变。因此无需每次调用都处理全部K线,只需关注新增部分:
// 在GetIndicatorData中添加增量逻辑 static int s_lastCount = 0; static std::vector<int> s_cachedBiPoints; if (nCount > s_lastCount) { // 新增K线数 int newBars = nCount - s_lastCount; // 只对最后200根K线做分笔(覆盖至少3个笔的长度) int startIdx = std::max(0, nCount - 200); // 从startIdx开始重算,复用s_cachedBiPoints中旧结果 std::vector<int> newBiPoints = CalculateBiFrom(startIdx, nCount, ...); // 合并:保留旧缓存中未被新计算覆盖的部分 + 新增点 s_cachedBiPoints.clear(); s_cachedBiPoints.insert(s_cachedBiPoints.end(), s_cachedBiPoints.begin(), s_cachedBiPoints.begin() + (startIdx > 0 ? startIdx : 0)); s_cachedBiPoints.insert(s_cachedBiPoints.end(), newBiPoints.begin(), newBiPoints.end()); s_lastCount = nCount; } // 输出s_cachedBiPoints...参数选择依据:
200是经验值:缠论一笔至少5根K线,一个标准中枢需至少9根(3段),200根可覆盖约40笔,足够应对日线/30分钟线;startIdx动态计算,避免固定偏移导致漏判(如跳空缺口后分型位置偏移);- 缓存
std::vector而非全局数组,利用RAII自动管理内存,防止DllMain中DLL_PROCESS_DETACH时野指针。
4.2 处理通达信价格精度陷阱:用整数比替代浮点比较
通达信内部价格以double存储,但实际精度仅到小数点后2~3位(取决于品种)。直接用>比较pHigh[i] > pHigh[i-1]在极小波动时可能因浮点误差失效:
// 错误写法(受浮点舍入影响) if (pHigh[i] > pHigh[i-1] && pLow[i] > pLow[i-1]) { /* 上移中枢 */ } // 正确写法:转换为整数比较(假设价格精度为0.01) long long high1 = static_cast<long long>(pHigh[i-1] * 100 + 0.5); long long high2 = static_cast<long long>(pHigh[i] * 100 + 0.5); if (high2 > high1) { /* 确认真实上涨 */ }精度映射表(通达信常见品种):
| 品种类型 | 价格单位 | 推荐放大倍数 | 示例:10.234→10234 |
|---|---|---|---|
| A股股票 | 0.01元 | ×100 | 10.234 * 100 = 1023(截断)→ 应+0.5四舍五入 |
| 期货主力 | 0.001元 | ×1000 | 2345.678 * 1000 = 2345678 |
| 港股通 | 0.001港币 | ×1000 | 同上 |
| 加密货币 | 0.00000001 BTC | ×100000000 | 需long long容纳 |
注意:
+0.5是关键。static_cast<long long>(x * 100)直接截断,x=10.235会变成1023而非1024,加0.5后10.235*100+0.5=1024.0,再转long long得正确值。
4.3 适配通达信通讯协议:接收Level2逐笔委托数据
部分高级缠论实现需结合逐笔成交(Level2)判断买卖力衰竭,此时DLL需主动连接通达信行情服务器。通达信提供TDXTcpClient类(需引用TdxApi.h),但必须在DLL中单独开线程接收,不能阻塞GetIndicatorData主线程:
// 在DllMain DLL_PROCESS_ATTACH中启动接收线程 HANDLE hRecvThread = CreateThread(NULL, 0, [](LPVOID) -> DWORD { TDXTcpClient client; client.Connect("127.0.0.1", 7709); // 通达信Level2端口 while (g_running) { TdxPacket pkt; if (client.RecvPacket(&pkt)) { // 解析逐笔数据,更新买卖盘强度缓存 UpdateBuySellStrength(pkt); } } return 0; }, NULL, 0, NULL);安全约束:
g_running为全局volatile bool,DLL_PROCESS_DETACH时置false并WaitForSingleObject(hRecvThread, 1000);TDXTcpClient必须静态链接(#pragma comment(lib, "TdxApi.lib")),避免运行时DLL缺失;- 逐笔数据不参与
GetIndicatorData主计算流,仅作为pStrParam中"USE_LEVEL2"开关的辅助信号源。
5. 验证缠论DLL输出正确性的三步交叉校验法:用Python解析二进制缓冲区
通达信不提供DLL内部变量查看功能,CALLDLL返回的BI_BUFFER是二进制double数组,肉眼无法验证计算是否符合缠论原文定义(如“连续三段重叠构成中枢”)。必须脱离通达信环境,用Python独立解析输出缓冲区,与手工标注对比。
5.1 提取通达信输出缓冲区到文件
在TDX公式中添加调试语句,将BI_BUFFER内容导出为二进制文件:
// 在CALLDLL后添加 IF Result > 0 THEN BEGIN // 将BI_BUFFER前Result*3个double写入文件 FileName := 'C:\tdx\debug\bi_output.bin'; FileWrite(FileName, REF(BI_BUFFER,1), Result*3*8); // 8字节/double END;5.2 Python脚本解析并可视化笔结构
import numpy as np import matplotlib.pyplot as plt # 读取二进制文件(double数组) with open(r'C:\tdx\debug\bi_output.bin', 'rb') as f: data = np.frombuffer(f.read(), dtype=np.float64) # 每3个元素为一组:[X索引, Y价格, Value类型] points = data.reshape(-1, 3) print(f"共解析 {len(points)} 个笔点") # 过滤Value==1的笔起点 bi_starts = points[points[:, 2] == 1.0] print(f"笔起点:{bi_starts.shape[0]} 个,索引范围 {bi_starts[:,0].min():.0f} ~ {bi_starts[:,0].max():.0f}") # 绘制验证图(需配合原始K线数据) plt.figure(figsize=(12,6)) plt.plot(range(int(bi_starts[:,0].max())+10), np.random.rand(int(bi_starts[:,0].max())+10)*10+10, 'k-', alpha=0.3) # 模拟K线 plt.scatter(bi_starts[:,0], bi_starts[:,1], c='r', s=50, zorder=5, label='笔起点') plt.xlabel('K线索引(正序)') plt.ylabel('价格') plt.legend() plt.grid(True) plt.title('缠论DLL输出笔起点验证') plt.show()校验逻辑:
bi_starts[:,0]应为递增整数序列(如[5, 12, 23, 35,...]),若出现重复索引或乱序,说明C++中biStarts向量未按时间顺序push;bi_starts[:,1]价格值应与通达信主图对应K线的LOW或HIGH吻合(笔起点必为顶或底分型);- 数量级应合理:日线图上500根K线通常产生30~80个笔,若仅2~3个,大概率是分型识别阈值
nParam1设得过大。
5.3 对照缠论原文关键条款反向验证
最后一步,拿输出结果对照《缠论》原文第三课“走势分解”中的硬性条件:
| 缠论原文条款 | DLL应满足的代码逻辑 | 验证失败时的修复方向 |
|---|---|---|
| “笔必须有顶分型和底分型构成” | GetIndicatorData中biStarts必须交替出现顶/底标识(可用pStrParam传入"MARK_TYPE"让DLL返回类型) | 在C++中增加isTop/isBottom标志位,输出时第4个double存类型 |
| “中枢至少由三个连续次级别走势重叠” | 若DLL支持中枢计算,pOutBuffer中应有Value==2的中枢区间点,且X坐标跨度≥9 | 检查中枢识别循环是否用了for (int i=0; i<nCount-8; i++)确保覆盖9根K线 |
| “走势类型完成需离开中枢” | 笔终点价格与中枢区间无重叠,可用std::set_intersection验证 | 在C++中增加中枢区间缓存,计算笔终点时做区间判断 |
提示:真正的缠论DLL不是“画线工具”,而是可验证的数学结构提取器。每一次
CALLDLL调用,都应能回答:“这个笔,是否满足原文第X章第Y条定义?”——这才是southern977和perhapsryf开源代码的核心价值。
本文还有配套的精品资源,点击获取