简介:这份通达信缠论K线包含处理插件源码,面向熟悉通达信平台且希望深入学习缠论量化分析的开发者。它提供了一套基于VC++的DLL插件实现,核心代码集中在ChanLunTdx.cpp与util.cpp中,围绕K线包含判断、合并以及笔、线段、中枢的构建逻辑展开,并包含与通达信交互的接口头文件。压缩包共24个文件,以h头文件、cpp源文件、obj目标文件及pdb调试文件为主,并附带DLL成品与使用说明,整体约693KB,结构清晰,方便对照学习。已有5739人下载学习。通过阅读源码,既能掌握缠论K线包含处理在真实项目中的落地方式,也能了解通达信插件从工程配置到编译调用的完整流程,适合用来提升交易分析工具的开发能力。
1. 缠论K线包含处理插件源码:先过合并这一关,分型和笔才有意义
在通达信里写缠论相关的指标或插件源码,绕不开的第一道坎就是K线包含处理。很多人画分型和笔之前,压根没把包含关系合并干净,结果顶底分型满天飞,笔怎么画都对不上,回过头去调参数,其实问题出在最基础的合并逻辑上。这一篇就把缠论K线包含处理插件源码这件事拆透:规则怎么落地成代码、方向怎么定、坑在哪里,以及怎么验证你写的合并结果是可信的。适合自己在通达信用公式或扩展DLL做缠论量化验证的从业者,也适合刚看完缠论原文、想复现一笔一画的新手。
2. 包含处理的规则拆解:方向怎么定、合并取哪边、为什么要递推
2.1 先定方向再合并:向上取高高、向下取低低
包含关系的定义很直白:相邻两根K线,如果一根的高低区间完全盖住了另一根,也就是高点更高、低点同时更低,反过来也一样,这两根K线就存在包含。处理包含关系的标准动作是合并,但合并不是简单取一个外包络,而是必须先确定方向:当前走势向上就“取高高”,向下就“取低低”。
“取高高”的意思是,合并后的高点取两根K线高点里较高的那个,合并后的低点也取两根K线低点里较高的那个。这一步很多人第一次看会犯迷糊,觉得向上取高点取最高的可以理解,低点为什么也取高的?其实是因为向上的合并是要把两根K线压缩成一根“更有力度”的K线,低点取较低的那个会把下行阴影保留下来,反而让后续判断高点压力时失真。“取低低”同理,高点取较低、低点也取较低,保留下行力度。
还是用数字说话。假设前一根K线高点10、低点8,后一根K线高点11、低点7,区间10-8完全盖住11-7,属于标准包含。方向向上时,合并结果是高点取11、低点取8,得到(11,8);方向向下时,合并结果是高点取10、低点取7,得到(10,7)。同一对K线,方向不同合并结果完全不同,这就是为什么“先定方向”是绕不开的规矩。
那么方向从哪里来?标准做法是找最近一对不含包含关系的K线,用它们的相对高低来定方向。后一根高点高于前一根,或低点高于前一根,就是向上;反之向下。这里有个容易忽略的细节:方向判断的基准是“合并后的K线”,不是原始K线。也就是说,前面几根经过合并压缩出来的K线,会被当作新的序列成员,继续参与方向和后续合并的判断。很多人在这里开始出错,把方向理解成“当前K线比前一原始K线高”,一遇到连续包含就乱套。
2.2 递推合并:合并完的K线还要继续和下一根K线比
合并不是一次性的。两根K线合并成一根之后,这一根“合并K线”要接着和后面的原始K线继续判断是否包含;如果还包含,就继续合并。这个动作会一直重复,直到遇到一根与当前合并K线不包含的新K线为止。整个过程是严格从左到右扫描的增量递推,很多人写的公式只做了“相邻两根K线单独合并”,比如第1、2根合并完,第3根跑去和第4根比,完全没让合并结果参与下一轮,这在多根连续包含的行情里会直接翻车。
我用一个四根K线的例子说明递推的必要性。四根K线分别是(10,8)、(11,7)、(12,6)、(9,5)。第一对(10,8)和(11,7)包含,向上合并得(11,8);这根合并K线(11,8)再和第三根(12,6)比较,(12,6)的高点更高、低点更低,仍然包含,继续向上合并得(12,8);再和第四根(9,5)比较,低点8比5高,不包含,归档。最终这段四根K线合并成两根:(12,8)和(9,5)。如果只做单重合并,会把(11,8)当作最终结果,和第三、第四根再做组合,整个序列就乱了。
所以在写代码时,一定要保持一个“当前合并K线”的状态变量。每进来一根新K线,先判断它和当前合并K线是否包含;包含就按已有方向合并,方向不变;不包含就把当前合并K线归档输出,然后让新K线成为新的当前合并K线,同时用这对无包含K线的相对位置更新方向。合并动作本身不改变方向,连续包含时方向一直沿用最后一次确定的结果,直到出现不包含的K线才重新定向。
这套状态机逻辑不复杂,但它是整个缠论K线包含处理插件源码的核心。后面无论用通达信公式写、用DLL扩展写,还是用Python对拍验证,脑子里都要先有这个状态图,不然写出来的东西一定是只处理了表面、经不起连续行情检验的半成品。
3. 从规则到源码:一段可移植的合并逻辑与通达信公式的边界
3.1 可移植的合并核心:不依赖平台的标准算法实现
我习惯先把合并算法写成一段不依赖任何平台的纯逻辑代码,比如用C++结构体表达K线,用枚举表达方向,验证通了再搬到具体环境。这样做的好处是,同一套算法可以同时在通达信DLL里跑、在Python对拍脚本里跑、在Excel手工核对表里跑,避免不同语言实现细节不一致导致的“各算各的”局面。
下面这段就是合并核心的C++实现,直接对应上一章的状态机:
#include <vector> #include <algorithm> struct KBar { double high; double low; }; enum Trend { UP, DOWN, UNKNOWN }; // 判断两根K线是否包含 bool isContain(const KBar& a, const KBar& b) { return (a.high >= b.high && a.low <= b.low) || (b.high >= a.high && b.low <= a.low); } // 单次合并:按已有方向取高高或取低低 KBar mergeOne(const KBar& a, const KBar& b, Trend trend) { KBar out; if (trend == UP) { out.high = std::max(a.high, b.high); out.low = std::max(a.low, b.low); // 向上:低点取较高的 } else { out.high = std::min(a.high, b.high); out.low = std::min(a.low, b.low); // 向下:高点取较低的 } return out; } bool isUp(const KBar& a, const KBar& b) { return b.high > a.high || b.low > a.low; } // 主处理函数:输入原始K线序列,输出合并后的K线序列 std::vector<KBar> mergeBars(const std::vector<KBar>& bars) { std::vector<KBar> result; if (bars.size() < 3) return result; // 找到第一对不包含的相邻K线,确定初始方向 int start = -1; Trend trend = UNKNOWN; for (size_t i = 1; i < bars.size(); ++i) { if (!isContain(bars[i - 1], bars[i])) { start = (int)i; trend = isUp(bars[i - 1], bars[i]) ? UP : DOWN; break; } } if (start < 0) return result; // 全区间一直包含,无有效起点 // 从该对的第二根K线开始递推合并 KBar cur = bars[start]; for (size_t i = start + 1; i < bars.size(); ++i) { if (isContain(cur, bars[i])) { cur = mergeOne(cur, bars[i], trend); // 包含:按方向合并,不换向 } else { result.push_back(cur); // 不包含:归档当前合并K线 cur = bars[i]; // 新K线成为新的合并K线 trend = isUp(result.back(), cur) ? UP : DOWN; // 用前一根合并K线更新方向 } } result.push_back(cur); return result; }这段代码的逻辑要点有三处。第一处是初始方向的确定:从头扫描,找到第一对不包含的相邻K线,用它们的相对高低定方向,并把起点设置在这对的第二根上,起点之前的K线不参与合并。第二处是主循环里的分支:包含就合并且方向不变,不包含就归档并更新方向,这正好对应前面说的状态机。第三处是方向更新时比较的基准,用的是result里最后那根合并K线,而不是原始K线,这是很多实现容易写错的地方。
参数方面的说明:bars必须按时间正序排列,否则isContain的判断会错乱;high和low用浮点数,注意通达信导出的数据里价格精度一般是两位小数,但复权价可能出现较长小数位,比较时不要做四舍五入。trend枚举里的UNKNOWN只用于“未找到起点”的状态,一旦进入主循环,trend只会在UP和DOWN之间切换。这个“起点策略”是一个可调参数,我在下一章会详细说不同策略对结果的影响。
3.2 通达信公式能写到哪一步:单重合并示例与局限
如果只想在通达信公式里快速验证包含处理,不折腾DLL,可以写出一个“单重合并”的版本。它只处理当前K线与前一根K线之间的包含关系,适合做可视化和理解规则,但不适合直接用于严格回测,因为结果没有回代到后续K线。
{单重包含合并示意:只处理当前K线与前一根K线的包含关系} CONTAIN:=(HIGH>=REF(HIGH,1) AND LOW<=REF(LOW,1)) OR (HIGH<=REF(HIGH,1) AND LOW>=REF(LOW,1)); DIRUP:=HIGH+LOW>REF(HIGH,1)+REF(LOW,1); HM:IF(CONTAIN,IF(DIRUP,MAX(HIGH,REF(HIGH,1)),MIN(HIGH,REF(HIGH,1))),HIGH),COLORRED; LM:IF(CONTAIN,IF(DIRUP,MAX(LOW,REF(LOW,1)),MIN(LOW,REF(LOW,1))),LOW),COLORCYAN;这段公式里,CONTAIN是包含条件判断,DIRUP用“高低和”粗判方向,HM和LM输出合并后的高点和低点。注意DIRUP这种判断方式只适合做示意,因为在多根包含时“高低和”比较会把连续包含的区间信息搅在一起,方向判断不稳定。
公式的局限很明显:它只比较了当前K线与前一根原始K线,没有把上一轮的合并结果拿过来继续比较。行情里出现三根以上连续包含时,比如(10,8)、(11,7)、(12,6)这种,公式会把第一对合并得到(11,8),第三根来了又和原始第二根(11,7)比较,而不是和(11,8)比较,结果自然不准。要让公式支持递推,就得把合并结果写进中间变量,再手动展开两层、三层判断,代码会膨胀得很厉害,而且对连续包含数量没有上限。
所以我的判断是,通达信公式适合做展示和快速验证规则,不适合作为严格的缠论K线包含处理插件源码的载体。真要落地成“插件”,还是要走扩展DLL这条路。
3.3 扩展插件方式:把合并结果以独立序列输出给分型模块
严格意义上的“插件源码”,在通达信里通常是指扩展DLL。做法是把3.1节那段纯C++逻辑封装成导出函数,把通达信传进来的HIGH和LOW序列接收下来,处理完再返回合并后的序列。通达信公式侧只需要一行调用,比如把合并高点接到一个自定义变量上。关于导出函数的具体签名约定,以通达信官方扩展接口文档为准,这里不展开贴接口样板,因为不同版本之间的约定有过调整,接口层不是重点。
调用插件的有几个好处。一是算法和公式层分离,合并逻辑在DLL里是黑匣子,不会因为公式误改而改变结果。二是可以做严格的递推合并,不受通达信公式展开层次的限制。三是合并结果可以直接输出成独立序列,后续分型、笔、中枢模块全部消费这一份合并数据,而不是各写各的包含处理。
我一般会把插件拆成两层:接口层只做数据搬运,把通达信的序列数组转成内部结构体,处理完再转回去;核心算法层就是3.1节的mergeBars函数,不包含任何平台相关的调用。这样同一份核心代码还能用在Python对拍脚本里,两边输出做diff,验证插件有没有写错。插件的输出建议设计成三个独立序列:合并高点、合并低点、合并K线计数。前两个给分型模块用,计数序列用来做数据完整性校验,比如合并后的K线数量不能出现跳跃。
4. 参数与边界场景:初始化方向、连续包含和分型衔接怎么设
4.1 初始方向与处理起点:用前两根无包含K线定调
包含处理对起点极其敏感,这是一个容易被低估的“玄学”点。同样一段行情,前两根K线刚好处于包含状态时,不同实现会给出完全不同的合并序列。因为合并是从左到右递推的,第一步的方向一旦定错,后面所有合并取值都会跟着错,分型位置偏移一两根K线都是轻的,严重时笔的方向直接反转。
我一般会把“起点策略”做成显式参数,而不是写死在代码里。常见的可选项有三种。第一种是“从第一对无包含K线开始”,也就是3.1节代码里写的方式,起点之前的K线直接舍去,优点是可解释性强、结果稳定,缺点是会丢掉行情开头的一小段数据。第二种是“前两根包含时按向上暂存”,也就是方向未定时先按向上合并,等遇到第一个无包含对再校正,这种做法的好处是保留的数据更完整,但起点之前的合并方向可能和最终方向不一致。第三种是“显式指定初始方向”,直接传入UP或DOWN,适合在回测时对比不同方向假设对结果的影响。
从实践角度,做日线级别分析时我推荐第一种,因为日线数据量大,丢掉起始几根K线对整体笔划分几乎没有影响,换来的是合并规则全程一致,后续对拍验证也容易对齐。做小级别、K线数量很少的行情分析时,可以用第二种保住数据量,但要意识到起点段的合并结果带有一定主观性。
还有一个相关参数是“最少K线数”。缠论最终要画分型和笔,如果合并后的K线数量太少,笔是成立不了的。我习惯在插件里加一个输出项:合并后的有效K线数量,并和原始K线数量一起打印出来,方便快速判断这段行情的“压缩率”是否正常。
4.2 连续包含的收敛条件与合并次数上限
连续包含是合并逻辑里最考验实现质量的场景。正常情况下,一根新K线参与合并后,最多再等几根就能遇到不包含的K线,合并序列会稳定输出。但在极端行情里可能出现连续七八根甚至十几根K线一路包含,合并K线数量骤减,区间被压缩得很大。
这里有两个参数值得关注。第一个是“单根K线最大参与合并次数”。虽然理论上合并到不包含为止就行,但如果数据源里有跳变或脏数据,可能导致合并逻辑陷入异常循环。给每根K线设一个参与合并次数的上限,比如30次,超过就强制归档并报警,能避免插件在异常数据上卡死。这个上限设定在30通常不会影响正常行情,因为正常数据里连续包含超过20根的行情已经极其罕见。
第二个是“全区间包含时的降级策略”。如果整个输入序列从头到尾都在包含,3.1节代码会直接返回空结果。这时插件不能静默失败,我建议返回一个错误码,或者至少把原始序列的高低位用一个外包络K线表达出来,让上层模块知道这段数据没有有效合并结果,而不是拿一个空序列去画分型。
连续包含场景还影响方向判断的一个细节:合并过程中方向不变,无论包含了多少根,都沿用最后一次确定的方向。很多人在代码里把“包含时更新方向”和“不包含时更新方向”混在一起,导致连续包含几次之后方向被偷偷改掉,这是所有bug里最难用肉眼发现的类型。方向更新只能发生“不包含”分支里,这是写插件时的一条铁律。
4.3 与分型、笔模块衔接时的参数口径
包含处理做完了,合并序列要交给分型和笔模块。这里最容易出的问题是“两套K线混用”:分型用合并K线,笔却用原始K线,或者反过来。一旦混用,顶底分型的位置和笔的起止点会对不上,整个缠论结构就乱了。正确口径是,后续所有模块统一消费合并后的序列,原始K线只作为数据源存在,不进任何判断逻辑。
分型判断的标准是三根合并K线:中间一根的高点是三者中最高,就是顶分型;低点是三者中最低,就是底分型。注意中间K线必须是合并K线,不能用原始K线替代。笔的判断标准更严格一些,常见定义要求相邻分型之间至少间隔一根合并K线,而且笔的顶底分型必须交替出现。所以合并序列特别短的时候,比如连续包含把一大段行情压成三根K线,这时候顶分型能成立,但笔大概率成立不了——这不是bug,是缠论结构本身的要求。
建议在插件输出里再提供一个“合并序列序号”字段,方便上层模块校验分型之间是否满足间隔条件。还可以加一个参数控制“是否允许合并K线参与分型中间位置”,默认是允许的,因为分型本来就在合并序列上计算。但如果你用的缠论定义更严格,要求分型的三根K线里至少包含一根原始K线,那这个参数就要关掉。这类口径差异在不同缠论流派里确实存在,做成参数比写死更实用。
5. 缠论K线包含处理避坑:四种常见的错误合并结果
5.1 合并后K线数量对不上:多半是没做递推回代
现象:某段行情的原始K线是40根,手算合并后应该是23根,但指标输出显示28根,而且两根合并K线之间偶尔会出现极高或极低的不合理值。
原因:代码只做了单重包含合并,也就是每对相邻K线独立判断,合并结果没有回代。遇到三根以上连续包含时,后续K线拿的是原始K线而不是上一轮的合并结果,自然对不上。这是所有公开公式里最常见的错误形态,网上能找到的不少“缠论K线包含处理”公式都有这个问题。
解决:把3.1节的状态机逻辑完整实现,确认主循环里判断包含用的是cur这个当前合并K线,而不是原始序列里的前一根。在插件里加一个输出项:合并后K线总数,和手算结果对不上就直接暴露问题。还有一个检验技巧,把原始K线按包含关系分组打印出来,连续包含超过三根的地方重点检查。
5.2 首根K线方向定错,整条笔方向全反
现象:同一个行情,两个缠论指标画出来的笔方向整体相反,或者第一笔起点差了好几根K线。合并后的高低点序列在中间段看起来都正常,但分型的位置始终对不上。
原因:起点策略不同。一个工具用“第一对无包含K线”定方向,另一个工具用“第一根K线比前一根高”来定方向,前两根K线一旦处于包含状态,处理结果就分叉了。更隐蔽的是,有些实现会在方向未定时把合并方向默认设为向上,这在小级别行情里极容易把初始方向带偏。
解决:把“起点策略”设为插件的一个公开参数,并在输出文档里明确标注选的是哪一种。对拍验证时,先固定双方的起点策略完全一致,再比较合并序列。我吃过这个亏后,现在写任何合并逻辑都会在头部加一段注释记录起点策略的选择,防止隔几个月回来忘了当初是按什么规则写的。
5.3 盘中信号与收盘回测不一致:未来函数
现象:盘中某一根K线刚刚走出来时,指标显示顶分型已经成立;收盘后重新计算,同一个位置的分型却消失了,或者位置偏移了一根K线。这个问题在回测和实盘信号之间会造成巨大差异。
原因:公式或插件里引用了未来数据。常见写法是在判断当前K线方向时,用了下一根K线的高低价,比如通达信公式里写了REF(HIGH,-1)这种负周期引用;还有人是借助了ZIG类函数来间接实现合并,而ZIG本身就是用未来数据回溯出来的。盘中最后一根K线还没有走完,下一根K线数据根本不存在,计算结果和收盘后自然不一致。
解决:合并方向只能用当前K线及之前已收盘的K线数据来判断。在通达信公式里搜索所有REF(X,-N)形式的引用,N为负数的一律删掉重写。DLL插件里则要检查取数逻辑,确保循环处理到第i根时,只用i及之前的数据。一个有效的验证方法:把行情截断到第N根,再截断到第N+1根,前N根位置上的合并结果必须完全相同;如果不一样,就说明有未来函数混进去了。
5.4 停牌与一字板污染包含判断
现象:某只股票停牌复牌后,合并序列里突然出现一根区间巨大且与前后都不相接的K线,或者一字板附近的分型频繁误报。用连续正常交易的指数行情测试时没有这个问题,一测个股就现原形。
原因:停牌期间的高低价格数据可能为0或沿用前收盘价,在合并逻辑里会被当作一根“极窄”或“全包”的K线参与包含判断,把正常合并序列切断。一字板因为开盘、最高、最低、收盘是同一个价格,高低区间退化成一个点,和相邻K线非常容易形成包含关系,处理不当会把整个走势方向搅乱。
解决:数据预处理阶段把停牌K线剔除或用相邻K线补齐,不能让高低价为零的K线进入合并逻辑。一字板不需要特殊合并逻辑,它在区间上是一个点,按高低比较的自然结果参与就行了,但要保证方向判断里“低点更高”这种比较在相等价格时不会产生错误转向。复权数据也必须统一口径,前复权和后复权会产生不同的历史价格,同一段行情用不同复权方式计算,合并序列会有细微差别,对拍验证时要保持两边用的是同一份价格序列。
6. 验证合并结果:手工核算20根K线,再用脚本对拍
6.1 手工核算模板与过程记录
插件或公式写完后,第一件事不是直接拿到全量数据上跑,而是手工核算一小段。我习惯取前20根日线K线,用笔和表格从头合并一遍,把处理过程记录下来。下面用6根K线演示这个模板,原始数据是(10,8)、(11,7)、(12,9)、(12.5,10)、(11,9.5)、(11.5,9)。
前两根包含,按起点策略找到第一对无包含K线是第2根和第3根,方向向上,起点从第3根开始。第4根(12.5,10)比当前合并K线(12,9)高点高、低点也高,不包含,归档(12,9),当前合并K线更新为(12.5,10)。第5根(11,9.5)高点低、低点也低,不包含但方向向下,归档(12.5,10)。第6根(11.5,9)高点比(11,9.5)高、低点比(11,9.5)低,形成包含,方向向下,取低低合并得到(11,9)。最终合并序列是三根:12-9、12.5-10、11-9。
做这个手工核算时,把每一根原始K线、方向、归档结果都写清楚。这个过程只要做一次,后面所有代码对拍都以它为基准。
6.2 用Python脚本对拍通达信合并结果
手工核算只能验证小样本,全量验证要靠脚本对拍。把通达信导出的高低价序列存成CSV,用Python实现一遍和C++插件完全一致的合并逻辑,再和插件输出的合并序列做对比。
def is_contain(a_high, a_low, b_high, b_low): return (a_high >= b_high and a_low <= b_low) or ( b_high >= a_high and b_low <= a_low ) def merge_bars(highs, lows): n = len(highs) if n < 3: return [], [] start = -1 trend = None # 'up' or 'down' for i in range(1, n): if not is_contain(highs[i-1], lows[i-1], highs[i], lows[i]): start = i trend = 'up' if (highs[i] > highs[i-1] or lows[i] > lows[i-1]) else 'down' break if start < 0: return [], [] cur_h, cur_l = highs[start], lows[start] out_h, out_l = [], [] for i in range(start + 1, n): if is_contain(cur_h, cur_l, highs[i], lows[i]): if trend == 'up': cur_h = max(cur_h, highs[i]) cur_l = max(cur_l, lows[i]) else: cur_h = min(cur_h, highs[i]) cur_l = min(cur_l, lows[i]) else: out_h.append(cur_h) out_l.append(cur_l) cur_h, cur_l = highs[i], lows[i] trend = 'up' if (cur_h > out_h[-1] or cur_l > out_l[-1]) else 'down' out_h.append(cur_h) out_l.append(cur_l) return out_h, out_l这段Python代码的语义和3.1节C++版本是对应的,唯一的差异是它直接用列表和元组,没有结构体。对拍时,分别从通达信插件和Python脚本导出合并后的高低点序列,做个逐位diff。只要有一根K线不一致,就回到手工核算表里看这一步到底应该怎么处理。
6.3 让合并结果先于分型固化:一个值得养成的习惯
验证通过之后,还有一个习惯建议养起来:在写分型和笔模块之前,先把合并序列单独输出成一列,固化下来,再让后续模块去消费它。不要在同一个公式里边做合并边画线,那样合并逻辑和分型逻辑混在一起,出问题根本分不清是哪一步错的。
我踩过的坑是,早期把合并和分型写在一个公式里,结果某段行情分型位置总是偏移,调了三天参数才发现是合并方向在连续包含时被意外更新了。后来改成合并序列先落盘,分型模块只读合并结果,同类问题几分钟就能定位。这个习惯本质上是给处理流程加了边界,让每一步都有独立的验证入口。
合并处理是缠论量化的地基,地基没打平,后面画再漂亮的笔和中枢都是空中楼阁。希望这篇能帮你把包含处理这一步走稳。
本文还有配套的精品资源,点击获取