
简介通达信缠论K线包含处理插件源码面向通达信插件开发者与缠论量化分析学习者核心解决相邻K线包含关系的自动判断与合并问题为后续笔、线段和中枢构建提供规范化K线序列。缠论包含处理是笔、线段与中枢识别的基础环节。压缩包共24个文件约693KB以头文件与cpp源码、dll动态库、plg插件、dsp/dsw工程文件及lib/obj/pdb等编译调试中间文件为主既可参考成品验证效果也能按模块研读实现。已有5767人学习说明其在通达信缠论插件方向具有一定参考价值。源码覆盖K线数据读取、包含判断、合并更新及结果回显等环节并展示插件接口与计算函数集的对接方式读者可借此理解VC环境下缠论包含处理的工程组织、MFC或ATL插件编写、调试思路和通达信插件开发流程适合作为缠论指标插件开发与源码剖析的实践样本。1. 一段被错误合并的K线打乱的一笔先说个具体的。某开发者把一套缠论画笔指标挂到通达信上日线看着还挺像回事切到 5 分钟就全乱了——笔连成一串分型一个接一个本该一笔的地方冒出三笔。他以为是递归逻辑写错折腾两天最后发现真正的问题在最前面K线包含关系根本没有合并所有分型和笔都建立在带噪声的原始K线上。这份缠论K线包含处理插件源码要解决的就是这件最底层、也最容易被跳过的事在通达信环境里按缠论规则把原始K线做包含合并输出合并后的高低点序列为分型、笔、线段打好基础。它适合两类人写过通达信指标公式、想在公式层做递归却做不下去的以及想用 C 写 DLL 插件扩展公式能力、缺一个完整可编译样例的。算法本身不复杂难的是状态机怎样处理前复权、停牌、新股这些不规整的数据时不出岔子。2. 架构选型为什么包含处理不能纯公式写2.1 公式引擎只处理序列不保留可变状态通达信公式语言的执行模型是“对整条序列逐根求值”REF(X,1)只能回看固定根数HHV、LLV这类函数也是整段滑动窗口。这种模型写均线、MACD 很舒服写包含处理就会遇到根本问题。包含合并的本质是合并后的第 n 根未必对应原始的第 n 根。两个原始K线合成一根之后后面的索引整体前移你没法在公式里维护一个“当前处理到哪根原始K线、输出了几根合并K线”的游标。更麻烦的是合并方向要依赖“上一段无包含区间的方向”这是个需要跨多根回溯的可变状态公式里的变量每根都重新求值存不住。所以常见做法是拆成两层C 那一层负责跑真正的顺序状态机公式层只负责取数据和画图。这不是为了炫技是执行模型决定的。我在别的指标上也踩过同样的坑——凡是“结果长度和输入长度不一致”的逻辑只要硬塞进公式层最后都会变成一堆REF套REF的玄学表达式改一次错一次。2.2 源码目录与各文件职责拿到包之后先看结构不用急着编译。这类插件工程的目录组织有很强的惯例性职责边界清楚照着认一遍基本就能判断改动该落在哪路径职责改动频率src/ChanKline.cpp包含合并状态机、分型识别高算法主体src/TdxExport.cpp导出函数、注册表、入参校验低接口固定include/ChanKline.h合并K线结构体、函数声明中加字段时改include/TdxFunc.hDLL 导出约定、注册结构定义极低跟随终端版本formula/chan_merge.txt指标公式调用 DLL 并绘图中调参时改build/build.bat编译脚本指定位数和输出目录低test/sample.csv固定样本做回归验证中加案例时改ChanKline.cpp和TdxExport.cpp分开是有意为之。算法部分不引入任何终端相关的头文件纯 C能单独抽出来单元测试导出层只做三件事——校验指针和长度、调算法、把结果按输入长度回填。这样你换一套接口比如换成别的行情软件的函数签名算法代码一行都不用动。2.3 一条数据从行情到屏幕的完整链路把链路走一遍后面看代码就有位置感了。终端加载指标公式公式里的TDXDLL1(1, HIGH, LOW)触发一次 DLL 调用终端把当前图表的 HIGH、LOW 序列以float数组传进来同时告诉你长度nDataLen和一个预分配的输出数组pfOUT。DLL 在内部完成合并然后必须往pfOUT里写满nDataLen个值——注意是写满不能少写少写的位置会保留上一帧的残留数据图表上就会出现一段莫名其妙的横线。输出对齐是这套架构里最关键的设计点。合并后的K线数量少于原始K线但终端的绘图缓冲区长度是固定的。解决办法是在合并结构体里存一个srcIndex记录这根合并K线是由原始第几根生成的输出时按srcIndex把每一根原始K线映射到它所属的合并K线原始K线序号 0 1 2 3 4 → 合并后 0 0 0 1 1这样第 3、4 根原始K线都输出合并K线 1 的高点画出来的图就是阶梯状的肉眼能直接看到哪几根被并掉了。这个映射表还能反过来用想画原始K线做对比时按同一套索引回查即可。由于一个导出函数只能输出一组float序列合并后的高点、低点、方向标记需要拆成多个函数注册用序号区分。这套做法在通达信 DLL 插件里很常见代价是公式层要写多行调用好处是每路输出都能单独拿去参与别的计算。3. 核心算法拆解包含判定、方向合并与分型3.1 包含关系的判定条件与两个边界包含关系的数学定义很干脆两根K线一根的高点不高于另一根、低点不低于另一根就是包含。写成代码是一行// 前包含后或后包含前两种情况都算包含关系 bool bContain (h prev.high l prev.low) || (h prev.high l prev.low);看着简单实际实现时有三个边界要处理清楚。第一是相等。高点相等但低点不同严格说不构成包含因为区间没有被完全覆盖。但如果用和高点相等就会被判成包含把一根正常的K线并掉。缠论里对相等的处理有争议多数实现选择“相等也算包含”理由是行情软件复权后经常出现小数位相同的K线不并会留下大量伪分型。这份源码采用的是算包含代码里那两行、就是为这个开的口子。要改成严格不等把等号去掉即可但后面的分型结果会明显变多。第二是第一根K线。第一根没有前一根可比不能判包含直接入栈方向标记置 0 表示未定。这个 0 很关键它会影响第一次遇到包含时怎么合并。第三是长度。nLen为 0、指针为空、nLen超过内部数组容量这三个都得在进入循环前挡掉。后面排查章节会讲为什么这个检查不能省。3.2 合并方向一个必须回看的变量合并的规则是“向上取高高向下取低低”。向上合并时新高取两根里更高的高点新低取更高的低点向下合并反过来新高取更低的、新低取更低的。判断“向上还是向下”看的是最近一段没有包含关系的走势方向而不是当前这两根谁高谁低。这个变量必须在循环外声明、循环内更新int nDir 0; // 1 向上-1 向下0 未定 for (int i 1; i nLen; i) { float h pHigh[i]; float l pLow[i]; MergedBar prev pOut[nOut - 1]; bool bContain (h prev.high l prev.low) || (h prev.high l prev.low); if (!bContain) { // 无包含正常入栈同时用高低点关系刷新方向 if (h prev.high l prev.low) nDir 1; else if (h prev.high l prev.low) nDir -1; pOut[nOut].high h; pOut[nOut].low l; pOut[nOut].srcIndex i; pOut[nOut].dir nDir; nOut; } else { // 有包含按已有方向合并nOut 不推进srcIndex 保持不变 if (nDir 0) // 向上或方向未定按向上处理 { if (h prev.high) prev.high h; if (l prev.low) prev.low l; } else // 向下 { if (h prev.high) prev.high h; if (l prev.low) prev.low l; } prev.dir nDir; } }nDir 0这里把“方向未定”也划到向上分支是个权衡。理论上方向未定时应该保留两种可能但那样会让状态机变成二叉树复杂度翻几倍。实践中第一根之后立刻遇到包含的概率很低即便遇到按向上合并也只是让开头的形态偏强一点不影响后续笔的成立。我一般会在这里加一行日志跑样本时统计一下方向未定就合并的次数如果占比超过 5%说明数据起点选得不好往前多取 100 根K线一般能解决。还有一点容易忽略合并之后不需要回头再检查一遍。因为合并是沿着一个固定方向收敛的新生成的区间一定包含于原区间不会和被再前一根重新构成包含。有人为了保险加了回退重判结果是在窄幅震荡区间里来回合并输出的合并K线数量比预期少一半。3.3 从合并K线到分型三条硬条件分型建立在合并K线之上判定要用合并后的高低点这是所有后续结果的前提。顶分型的条件是中间那根高点最高、低点也最高底分型反过来高点最低、低点也最低。只判高点不判低点是常见的错误写法会把一些区间重叠的K线组合误判成分型。// 顶分型中间K线的高点、低点同时高于左右两根 bool IsTopFractal(const MergedBar* b, int i) { return b[i].high b[i-1].high b[i].high b[i1].high b[i].low b[i-1].low b[i].low b[i1].low; } // 底分型中间K线的高点、低点同时低于左右两根 bool IsBottomFractal(const MergedBar* b, int i) { return b[i].high b[i-1].high b[i].high b[i1].high b[i].low b[i-1].low b[i].low b[i1].low; }注意这里的下标用的是合并后的序号不是原始序号。要在图上画线得用srcIndex换算回原始位置否则线条会整体偏移几根——偏移量随包含合并的数量累积越往后越明显看起来像是“指标算错了”。这个换算是把分型结果输出给公式层时最容易出问题的地方。分型出来之后笔的成立还要加两个约束两个相邻的顶底分型之间至少隔一根独立K线顶分型的高点必须高于底分型的低点否则这一笔不成立需要继续往后找。这两条在源码里是独立函数按顺序过滤不做嵌套判断出问题时容易定位到是哪条卡住的。4. 接口对接导出函数与公式层的约定4.1 导出函数的统一签名DLL 那一侧的接口形态是终端定死的改不了。常见的形式是四个输入序列加一个输出序列返回值表示成功与否#include TdxFunc.h // 统一签名长度 输出数组 输入序列 // 不同终端版本参数个数可能不同以随包的 TdxFunc.h 为准 int WINAPI CalcMergedHigh(int nDataLen, float* pfOUT, float* pfIN1, float* pfIN2, float* pfIN3) { if (nDataLen 0 || nDataLen MAX_BARS) return 0; if (pfOUT NULL || pfIN1 NULL || pfIN2 NULL) return 0; // 静态缓冲避免每帧 new图表切换时高频调用 static MergedBar s_bars[MAX_BARS]; int n MergeKline(pfIN1, pfIN2, nDataLen, s_bars); if (n 0) return 0; // 关键输出长度必须与输入一致按 srcIndex 回填 int k 0; for (int i 0; i nDataLen; i) { while (k 1 n s_bars[k 1].srcIndex i) k; pfOUT[i] s_bars[k].high; } return 1; }static缓冲这行值得单独说。图表刷新、周期切换、指标叠加都会触发重新计算如果每次进来都new一片几百 K 的数组长时间挂着终端会看到内存曲线一路往上。静态缓冲的代价是不可重入多线程同时调用会打架但终端调用是串行的这个约束可以接受。真要上多线程改成线程局部存储。回填那段的while循环是这套代码的核心技巧。s_bars[k1].srcIndex i判断当前原始K线是否已经跨过了下一根合并K线的起点跨了就前进。k1 n是防越界最后一根合并K线会被所有剩余的原始K线共享。4.2 注册表序号就是公式层的调用凭据注册函数的写法各家实现差异比较大但结构一致把函数名、参数个数、函数地址塞进一个数组序号从 1 开始与公式里的第一个参数一一对应。void RegisterTdxFunc(PDLLFUNCARRAY pArray) { // 序号 1 对应公式里的 TDXDLL1(1, HIGH, LOW) pArray-Add(CHAN_MH, 2, (PVOID)CalcMergedHigh); pArray-Add(CHAN_ML, 2, (PVOID)CalcMergedLow); pArray-Add(CHAN_DIR, 2, (PVOID)CalcMergeDir); pArray-Add(CHAN_FX, 2, (PVOID)CalcFractal); }参数量这个字段要跟实际签名对上写 2 表示公式层传两个序列进来。写多了终端会多传空指针写少了后面两个指针是野值两种都会崩而且崩在终端进程里看不到堆栈日志只能靠猜。这是 DLL 插件最难受的一点。四个函数分别输出合并高点、合并低点、方向标记、分型类型1 顶、-1 底、0 无。拆成四个是为了让公式层能自由组合比如只想看方向就不必算分型。4.3 公式层调用、绘图与参数公式层不干重活只做三件事调 DLL、画K线、把分型标出来。{缠论包含处理合并后高低点 分型标记} MH : TDXDLL1(1, HIGH, LOW); ML : TDXDLL1(2, HIGH, LOW); DIR : TDXDLL1(3, HIGH, LOW); FX : TDXDLL1(4, HIGH, LOW); {用合并后的高低点重画K线阶梯状代表被合并} STICKLINE(MH ML, MH, ML, 8, 0), COLORYELLOW; {顶分型画绿箭头底分型画红箭头} DRAWICON(FX 1, MH * 1.002, 2); DRAWICON(FX -1, ML * 0.998, 1);DRAWICON的第二个参数是位置乘个 1.002 是为了让箭头浮在K线上方不遮挡。这个系数按品种价格量级调几十块的股票用 1.002 合适上千的就得改小不然箭头飘太远。三行TDXDLL1调用的第一个参数是注册序号第二个和第三个是输入序列。千万别把HIGH、LOW的顺序写反反了之后包含判定会用高点和低点交叉比较结果是一堆看起来很像但完全不对的合并K线而且不会报错只能靠肉眼比对发现。公式文件保存成.txt后由终端导入或者直接放进指标目录。DLL 文件的放置路径要跟终端版本对上放错位置的表现是公式保存成功、加载后线全是 0不报错。这个坑后面单独讲。5. 避坑排查五条踩过的坑5.1 崩溃与数值异常现象指标加载后公式编辑器提示函数不存在或者整条线输出全 0终端没有任何报错。原因位数不匹配或路径不对。64 位终端加载 32 位 DLL 会静默失败DLL 放在安装目录根下而不是约定的插件目录同样如此。解决先确认终端进程位数编译时对齐DLL 放到插件目录后完全退出终端再启动不要只关窗口。验证办法是注册一个只返回常数的测试函数能画出直线说明加载成功再去查算法。现象当前屏幕正常往前翻历史K线翻到某一页终端直接退出。原因内部静态数组越界。历史数据往前翻时nDataLen会突然变大超过MAX_BARS而入口检查只判了 0。解决入口加上限判断并直接返回 0宁可这一帧不画也不能崩。MAX_BARS按终端能提供的最大K线数留 30% 余量我一般设 20000。现象合并后的价格出现 0.01 级别的偏差日线上看不出来分钟线上累积得很明显。原因全程用float运算合并时反复取大取小误差累积。价格本身的有效位数不多但复权因子带小数乘完之后float的尾数就不够用了。解决内部结构和计算全用double只在写pfOUT的最后一步转float。改动量不大但能消掉大部分精度类怪现象。5.2 图形与信号类问题现象一笔画出来横跨了停牌区间复牌后的第一根K线被并进了停牌前那根里。原因停牌期间终端返回的K线高低点重复连续几十根完全相同包含判定会把整段压成一根复牌跳空又被这根吞掉。解决在合并前做切段检测到连续 N 根高低点完全相同或者日期跳变超过阈值就把状态机重置方向标记归 0从下一根重新开始。N 取 3 到 5 都行太小会误切一字板。现象5 分钟周期上笔特别多日线上看着挺干净同一只票两个周期对不上。原因包含处理对噪声天然敏感周期越小无意义的包含越多合并后的形态和高级别本来就不是缩放关系。解决别在 1 分钟、5 分钟上直接画笔。常见做法是在日线或 30 分钟上定笔低周期只用来找买卖点位置。指标参数里可以加一个“最小合并K线数”开关小于这个数直接不输出分型能过滤掉一大半碎笔。6. 二次开发把包含处理接进自己的笔段体系改这套代码第一个该动的是方向初始化的策略不是算法主体。nDir 0那段把“未定”归到向上是个能用但不精确的取舍。如果你做的品种趋势性强可以改成用前 5 根K线的收盘价斜率定初始方向震荡品种则相反宁可让第一段多合并几次。改这里只影响开头几十根回测时对比一下分型数量的变化就能判断好坏。第二个值得加的是合并映射表输出。现在只输出合并后的高低点公式层拿不到“哪几根原始K线被并了”。加一个导出函数输出每根原始K线所属的合并序号公式层就能画出原始K线和合并K线的对照调参时特别直观// 输出合并序号映射pfOUT[i] 原始第 i 根所属的合并K线序号 int WINAPI CalcMergeMap(int nDataLen, float* pfOUT, float* pfIN1, float* pfIN2, float* pfIN3) { if (nDataLen 0 || nDataLen MAX_BARS) return 0; static MergedBar s_bars[MAX_BARS]; int n MergeKline(pfIN1, pfIN2, nDataLen, s_bars); if (n 0) return 0; int k 0; for (int i 0; i nDataLen; i) { while (k 1 n s_bars[k 1].srcIndex i) k; pfOUT[i] (float)k; // 序号直接用来做颜色分组 } return 1; }验证方法比改代码更重要。test/sample.csv里放几段手工算过的样本最有用一段单边上涨、一段窄幅震荡、一段停牌跳空、一段新股上市头 60 根。每次改完算法跑一遍把合并K线数量和分型位置跟预期对一遍。手工算不需要算全程每个样本掐头去尾取 30 根就够重点是确认边界行为没变。没有这套回归样本改一个变成表面上只是分型多了几个实际可能把整段笔的划分全推翻了而你完全不知道是哪次改动引入的。最后提一句口径问题这套东西输出的只是结构不是买卖信号。顶底分型画出来之后怎么用取决于你自己的过滤条件别把它当成现成的交易系统。我早几年改这类指标习惯直接上终端看图觉得对就行。后来有一次把一个float改成double图上完全看不出差别但笔的数量少了 7%而我是一周后翻旧截图才发现的。从那以后我每次动算法都强制先跑一遍样本回归看完数字再看图。希望帮到你。本文还有配套的精品资源点击获取