☰
通达信缠论K线包含处理插件源码:从合并算法到分型笔的完整实现
2026/10/13 21:36:39 网站建设 项目流程

简介:一套通达信交易平台下的缠论K线包含处理插件源码,面向量化交易者、缠论研究者和通达信二次开发人员,用于自动完成K线包含关系判断与合并,为后续笔、线段、中枢的构建提供基础。源码基于Visual C++工程编写,核心逻辑集中在ChanLunTdx.cpp中,util模块则封装了包含判断、合并等通用工具函数,整体流程覆盖K线数据读取、包含关系识别、K线合并以及缠论结构生成等步骤。压缩包共24个文件,以C++头文件与源文件、Visual Studio工程文件为主,另含编译好的DLL插件、调试符号和使用说明,整体大小仅693KB。已有5739人学习下载。阅读这套源码,既能深入学习缠论中K线包含处理的算法思路,也能掌握通达信DLL插件的接口开发方法,对想要自主实现缠论指标或交易工具的人很有参考价值。无论是理解缠论结构生成过程,还是借鉴工程实现细节,都能从中获得实际收益。

1. 通达信缠论K线包含处理插件源码:一笔画错,后面全线崩

如果你正在找通达信缠论K线包含处理插件源码,大概率已经受过一笔画不对的苦。缠论里K线包含处理是分型、笔、线段之前的地基,地基歪一寸,后面的分型错位、笔段断裂,图上看就是信号乱跳。我最初用某量化小组配好的公式跑历史数据,结果同样一段行情,画出来的顶分型比别人少十多个,最后定位到问题全出在包含合并方向上。这份源码解决的是最底层那段合并逻辑:输入原始K线的高低价,输出按规则处理后的K线序列,方向判断和递归合并都放在同一个模块里。适合指标开发、策略前处理、K线数据清洗这三类需求,新人能照步骤改,熟手可以直接拿着做二次计算。

2. 包含处理的核心算法:方向判断、合并规则与递归边界

2.1 包含关系怎么判定

缠论里K线只看四个值:最高价、最低价、开盘价、收盘价。分型成立与否只取决于高低价,开盘收盘在这个阶段先不参与。两根相邻K线构成包含关系,需要满足「低点包低点、高点包高点」的完整覆盖条件。

判定包含关系时,我一般用下面的比较方式:

int is_contain(double high1, double low1, double high2, double low2) { // high1/low1为前一根K线,high2/low2为后一根,区间完全覆盖即包含 if (high1 >= high2 && low1 <= low2) return 1; if (high2 >= high1 && low2 <= low1) return 1; return 0; }

这个函数把四根折线收窄成两个区间的相互覆盖判断。第一行是前一根包含后一根,第二行是后一根包含前一根,两种情况都归为包含。直接比较高低价的最大值最小值会更快,但写成显式分支后面做方向性合并时更直观。

有一点容易看漏:开盘价和收盘价在包含判定里不参与,因为分型只需要最高价最低价,插进开收反而在幅度计算时引入噪声。如果你手上的行情接口返回的是复权数据,优先用后复权,前复权会把历史高点不断重算,直接影响包含判断的稳定性。

2.2 向上合并与向下合并的规则

包含处理的前提是先确定当前方向,方向取决于最近一组已经明确非包含的K线。也就是说,不能拿到两根包含 K 线就直接合并,合并方式必须由前面的趋势方向决定。

方向向上时,合并后的高点取两根K线高点中的较大者,低点取两根低点中的较大者;方向向下时,合并后的高点取两根高点中的较小者,低点取两根低点中的较小者。

合并方向高点取值低点取值适用前提
向上合并max(高点1, 高点2)max(低点1, 低点2)前一组非包含K线为向上
向下合并min(高点1, 高点2)min(低点1, 低点2)前一组非包含K线为向下

这个规则的几何含义是:向上合并时两K线按最高上沿取边,这样处理后的K线在形态上延续上升结构;向下合并时按最低下沿取边,保持下降结构的连续性。如果方向判断反了,包含处理后K线高低点偏小或偏大,后续分型判断会直接错位。

方向更新本身有一个细节:只有出现一组非包含K线时才更新方向,包含处理过程中方向保持不变。比如连续三根K线前两根包含、合并后与第三根又包含,方向依然是合并前确定的那个,不能因为合并结果重新判定方向。

2.3 合并后序列的递归推进

合并不是一次性的。两根K线合并后产生一根新K线,这根新K线的位置相当于原序列里的前一根,它还要继续和下一根原始K线做包含判定。因此处理的本质是递归折叠原序列,直到当前K线与下一根不再包含。

递归推进的终止条件是整个K线序列扫描完毕。每合并一次,K线数量减一,但原始序列本身不能直接被截断,因为合并后的K线在走势上有自己的时间权重。实际处理时,我习惯用双数组保存合并结果:一个数组存原始高低价,另一个数组存处理后的序列,全部结束后再把处理后的序列写回。

递归过程中的一个重要边界是首尾K线。序列第一根K线没有前驱,方向只能由后面第一组非包含K线决定,所以源码里对第一根统一做「等待方向」处理,不参与合并。最后一根K线如果有包含,合并后让它进入结果序列即可,不要继续向后探测,否则越界。

3. 通达信插件怎么落地:公式语言限制、DLL 接口与数据流

3.1 为什么必须用插件而不是纯公式

通达信公式语言擅长表达指标计算,但它天然缺乏循环和递归语义。包含处理需要逐根比较并反复折叠序列,在纯公式环境里写这个逻辑,要么把判断次数固定死写入大量硬编码行,要么用数组函数模拟状态机,最终代码可读性和可维护性都很差。源码里直接做成插件,本质就是因为这种逐根递归的算法不适合放在公式层。

插件的形式是在主程序里编译成 DLL 后挂到通达信指定的公式接口里。这里我补一句:不同版本的软件加载方式略有差异,常见做法是把 DLL 放在安装目录下的 Plugin 文件夹,再在公式管理器里注册函数名。加载完成后,公式里可以像调用内置函数一样调用插件导出的处理函数。

把算法放插件层的另一好处是数据处理速度快。公式语言每根K线都要解释执行,插件编译后直接跑原生代码,处理几千根K线的时间差距非常明显。如果你做的是分钟级周期遍历回测,这个速度差会直接变成回测时间的几倍差异。

3.2 输入输出数据结构设计

插件输入的是原始K线序列,输出的是合并后的K线序列。为了兼容通达信公式调用,输入输出都通过固定的浮点数组指针来传递。设计结构时先约定四个数组:输入高点数组、输入低点数组、处理后高点数组、处理后低点数组,同时附带一个长度参数,返回处理后的K线数量。

int __stdcall ProcessKLine( float* inHigh, // 原始K线最高价数组 float* inLow, // 原始K线最低价数组 int inSize, // 原始K线数量 float* outHigh, // 处理后的最高价数组 float* outLow // 处理后的最低价数组 );

函数返回值是处理后K线的数量。调用方拿到这个数量后,从 outHigh 和 outLow 数组里读取结果,再传给分型或笔计算模块。注意 inHigh 和 inLow 是按同一根K线索引对齐的,inHigh[0] 和 inLow[0] 属于同一根K线,插件内部不许打乱顺序,否则合并后的高低点对应关系错乱。

3.3 调用流程与数据装配

插件内部我按两个阶段组织:先加载原始序列,再执行合并主逻辑。加载阶段不做任何判断,只把 inHigh 和 inLow 拷贝到内部工作区,避免在合并过程中直接修改输入数组,这样源码可以被同一公式周期内多次调用而不污染数据。

int process_wrapper(float* inHigh, float* inLow, int inSize, float* outHigh, float* outLow) { // 内部工作区,深拷贝一份原始数据,保证输入不被改动 float* workHigh = (float*)malloc(sizeof(float) * inSize); float* workLow = (float*)malloc(sizeof(float) * inSize); memcpy(workHigh, inHigh, sizeof(float) * inSize); memcpy(workLow, inLow, sizeof(float) * inSize); int resultSize = merge_kline(workHigh, workLow, inSize); memcpy(outHigh, workHigh, sizeof(float) * resultSize); memcpy(outLow, workLow, sizeof(float) * resultSize); free(workHigh); free(workLow); return resultSize; }

这段装配代码里,malloc 分配的大小按输入长度来,如果输入序列特别长,比如超过十万根K线,建议改用调用方预分配大缓冲区的方式,插件内做inSize参数上限校验即可。合并后的结果写回 workHigh 开头的位置,而不是另起数组,这样 memory footprint 只有输入序列的两倍,批量跑多周期数据时压力小。

调用流程走完后,公式端得到的是合并后的序列。很多第一次用插件的人会犯一个错:拿合并后序列的长度除以原始长度,当相差太多就开始怀疑处理有误。其实只要行情里包含关系出现频繁,K线数量减少是正常的。判断正确处理与否,应该看结果序列中相邻K线是否全部满足非包含条件,而不是看数量差。

4. 核心源码实现:合并主流程、方向切换与参数控制

4.1 主合并函数的实现

主合并函数是整份源码的心脏,逻辑路径是:从头到尾扫描,遇到包含关系按当前方向合并,遇到非包含关系更新方向,合并后回退检查上一步的K线是否与再前一根形成包含。

int merge_kline(float* high, float* low, int n) { if (n < 2) return n; // outPos:输出序列的当前位置,同时也是待比较K线的位置 int outPos = 0; // 方向状态:1代表向上,0代表向下,-1表示未确定 int direction = -1; for (int i = 1; i < n; i++) { if (outPos == 0) { // 序列开头没有前驱,先把首根直接放入结果序列 high[outPos] = high[0]; low[outPos] = low[0]; outPos++; // 首根之后的处理要保证至少有两根才能判方向 if (outPos == 1 && i == 1) { // 用第一根和第二根的非包含关系确定方向 } // 这里做了简化:实际逻辑是让后续循环自然推进 } // 判断当前位置的K线与新扫描到的K线是否包含 if (is_contain(high[outPos - 1], low[outPos - 1], high[i], low[i])) { // 包含则按方向合并 if (direction == 1) { // 向上合并:高取较高,低取较高 high[outPos - 1] = (high[outPos - 1] > high[i]) ? high[outPos - 1] : high[i]; low[outPos - 1] = (low[outPos - 1] > low[i]) ? low[outPos - 1] : low[i]; } else if (direction == 0) { // 向下合并:高取较低,低取较低 high[outPos - 1] = (high[outPos - 1] < high[i]) ? high[outPos - 1] : high[i]; low[outPos - 1] = (low[outPos - 1] < low[i]) ? low[outPos - 1] : low[i]; } // 合并后 outPos 不增长,继续用当前位置与下一根比较 } else { // 非包含:当前K线可以进入结果序列 high[outPos] = high[i]; low[outPos] = low[i]; outPos++; // 根据刚进入的两根非包含K线更新方向 if (outPos >= 2) { if (high[outPos - 1] > high[outPos - 2] && low[outPos - 1] > low[outPos - 2]) { direction = 1; } else if (high[outPos - 1] < high[outPos - 2] && low[outPos - 1] < low[outPos - 2]) { direction = 0; } } } } return outPos; }

这块是源码里最需要耐心读的部分。注意当 outPos 为 1 时,只有一根K线在结果序列里,此时来了第二根K线且非包含,通过两个条件更新 direction;之后每遇到包含就沿用这个方向合并。如果你的数据在开头连续出现包含,需要先让前两根非包含K线进序列,方向确定后才能开始正确合并,这一点顺着代码走两遍就能清楚。

4.2 方向切换与边界控制

方向切换这个点,是我反复排查最多的地方。缠论规则里,方向不能由两根包含K线决定,只能由一组非包含K线决定。上面代码中,只有在非包含分支才更新 direction,就遵守了这个约束。

方向从向上切到向下有一个考虑:向上合并过的最后一根K线,在切换到向下方向后,它本身不参与向下的首次合并,只有它与下一根K线确实出现包含时才触发向下合并。很多实现为了省事,在方向切换的边界强行做一次合并,这样会让转折点K线的位置偏移,产生多余的顶分型或底分型。

边界控制还有一个要关注的点:K线数量缩减到只剩两根时,方向可能仍未确定。这种情况下,源码默认不对这两根做合并,原样保留。因为包含处理的意义是为分型判断铺路,数量过少时强行合并反而引入没有意义的高低点。

4.3 浮点精度与相等判断

行情数据里的高低价经常出现完全相等的整数,比如连续涨停的K线高低点可能完全重叠。C 语言里浮点相等判断直接比较会出问题,尤其当数据源经过精度转换后,0.999999 与 1.000001 这种误差会导致误判方向。源码里我在高低价比较前增加了一个 epsilon 层。

#define EPSILON 0.000001f int is_contain_precise(float high1, float low1, float high2, float low2) { // 先做浮点近似比较,避免精度抖动造成误判 if ((high1 - high2) > -EPSILON && (low1 - low2) < EPSILON) { // high1 不低于 high2,low1 不高于 low2,判定包含 return 1; } if ((high2 - high1) > -EPSILON && (low2 - low1) < EPSILON) { return 1; } return 0; }

EPSILON 的取值要和你实际行情数据的精度一致。普通股票行情用 0.000001 足够,但期货主力连续合约有的价格带小数点后四位甚至六位,这时可以把 EPSILON 调到 0.00001 或者采用相对精度:取两根K线中高点绝对值的百万分之一作为浮动阈值。不要用绝对相等,会出现涨停价格连续相等时包含关系被漏掉。

用盘口数据做盘中动态K线时,最后一根未收盘K线的高低会跳动,跳动幅度超过 EPSILON 就可能重复触发合并。源码里提供输入参数allowUnclosed,为 0 时自动跳过序列最后一根K线不参与包含判断,为 1 时正常参与,默认值设成 1,因为多数历史回测场景里最后一根早已收盘。

5. 避坑排查:K线包含处理最容易翻车的五个场景

5.1 方向未确认就合并,分型位置全体左移

有一回我拿一段单边下跌行情跑处理,结果整个序列的底分型位置比手工画线早了两根。追查后发现问题只在初始化方向:代码在遇到第一组包含K线时就默认方向向上,直接按向上合并取高低点,导致下跌结构里的K线合并后低点被低估,底分型被提前确定。

原因:源码开头 direction 初始化为 -1 时,没有强制要求前两根非包含K线确定方向,等包含K线出现时直接用了错误方向。

解决:在包含分支里先检查 direction 是否为 -1,如果是则跳过合并并把当前K线暂存,直到方向确立后再回头处理。合并代码里要保留这个判断分支,我的处理是把「等方向」状态单独拆出来,遇到 -1 不合并。

5.2 合并后回查遗漏,两根K线被重复折叠

源码在合并一根K线后,没有回查合并结果与更前一根的关系,导致本应连续折叠的三根K线只处理了后两根,序列里残留一组包含。比如三根K线中第二第三合并后,新K线与第一根又包含,但代码直接跳到第四根,返回了一个有包含关系的结果序列。

原因:合并分支里 outPos 不增长,循环本应再次比较当前位置与下一根,但我的实现里把比较目标固定成了 i 的下一根,没有重新取出 outPos 位置的K线与 i+1 比较。

解决:循环里每次迭代都从 outPos - 1 取当前位置K线,与 i 位置的K线比较,而不是保留之前的临时指针。同时 if (outPos == 0) 的分支只在第一根K线时执行一次,后面不再重复首根。

5.3 EPSILON 没生效,相等K线被漏判

我在某行情软件导出的分钟数据高低价里见过带四位小数的价格,比如 12.3400 和 12.3401 这类相邻值。直接用 is_contain 判定时,因为 float 精度问题,本应包含的K线被判为不包含。

原因:浮点存储误差导致两个逻辑上相同的高度值在比较时不等。

解决:统一换成 is_contain_precise,并把 EPSILON 的取值与数据精度绑定。配置文件里加一个判断,数据源价格保留到小数点后几位就缩小校验阈值,避免跨越精度等级的行情误用统一值。

5.4 数量为 0 或 1 时越界访问

有一段空序列数据进到插件里,函数直接崩溃。定位后发现 for 循环里访问 high[i] 前没有校验 n 的上下限,当 n=0 时循环访问了 high[-1]。

原因:插件接口没有做入参合法性校验,数据加载阶段也能传入空指针或长度 0。

解决:process_wrapper 入口处增加三段判断,n < 2时直接返回原始序列长度并对结果数组置 0,high==NULL || low==NULL时返回 -1,n > MAX_BARS(一般设 50000)时截断到最大值。这套检查不止是防御,公式调用者传参错误时能立刻在日志里看到。

5.5 复权数据切换导致合并结果出现断层

换数据源跑同样一段代码,合并后的序列比之前的少了很多根,而且跳空缺口区域的K线高低点出现明显不合理。后来发现前一个数据源用后复权,换后的源默认前复权,价格序列头部被整体重算,包含关系和合并结果自然都变化了。

原因:不同复权方式下历史价格高低不同,影响包含关系判断。

解决:源码在使用前强制要求调用方声明复权类型,并在内部根据复权标记决定是否带上比较容差。建议回测统一用后复权,避免每次下载新数据后历史区间价格变化导致结果漂移。

6. 把包含处理结果接进分型与笔:一个十分钟的验证流程

插件做完包含处理,结果不能只打印到文件里就算完事。我每次都会把处理后序列拉回通达信里,和公式端的副图指标做一次叠图校验。验证流程很短:把插件输出写成文本文件,由公式读进来画到主图上,再和手工K线图对照。

叠图时重点看三类位置:上涨转下跌的转折处是否有顶分型,下跌转上涨的转折处是否有底分型,以及连续包含后方向切换的那根K线是否被正确处理。手工对照选最近几十根日线就够,不需要全量回放,你可以局部放大到包含密集区去看。

实际验证里我发现一个值得提到的简化写法:处理后的K线数量小于原始数量,分型计算可以直接在结果序列上跑,不需要考虑原始K线的位置偏移。这就是把包含处理做成一层的收益,后面的笔和线段模块只依赖处理后的序列,和原始K线全部解耦。

for (int j = 2; j < mergeCount - 1; j++) { // 顶分型:中间K线高点最高 if (mergedHigh[j] >= mergedHigh[j - 1] && mergedHigh[j] >= mergedHigh[j + 1]) { push_top(mergedHigh, j); } }

这个循环是沿用分型定义的经典写法,在合并后的序列里找顶分型。注意用的是 >= 而不是 >,相等K线不会同时构成顶底分型,仔细体会这个判断条件。

从那以后我每次写完包含处理代码,都会强制走一遍上面的叠图流程,哪怕改动只动了一个 EPSILON。这个习惯帮我挡住了至少三次改错方向回归导致的分型漂移。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询