通达信缠论K线包含处理插件:从公式到DLL的完整实现 简介这套源码是通达信平台上的缠论K线包含处理DLL插件实现面向需要自动完成K线包含关系判断与合并、进而构造笔和中枢的投资者、缠论研究者及通达信插件开发者。K线包含处理是缠论技术分析的基础环节插件对此给出了完整的自动化流程。源码基于VC编写核心逻辑覆盖从通达信API读取K线、遍历相邻K线并判断包含、按缠论规则合并K线再到构建笔、线段、中枢并将结果绘制到行情图表的全过程且主逻辑、辅助函数与插件接口分层清晰便于理解通达信扩展函数的挂载方式。压缩包共24个文件以.h头文件、.cpp源文件、.obj编译中间文件、.pdb调试文件为主同时附带编译好的.dll插件、Visual Studio工程文件及使用说明文档整体693KB文件分工清晰便于按需查阅。目前已有5739人学习适合有一定C/C基础、想深入理解缠论算法落地细节或快速上手通达信DLL插件开发的读者通过阅读这套源码可以提取K线合并、中枢构造等关键算法思路并迁移到自己的量化分析工具中。1. 通达信缠论K线包含处理插件先想清楚它是防坑的不是画线的打开通达信随便挑一只活跃票切到日线你很快会遇到一个尴尬局面两根K线一根把另一根整个包住缠论里最基础的分型和笔突然就不知道从哪里下手。通达信缠论K线包含处理插件源码要解决的正是这个最容易被忽视、却决定后续所有判断能不能成立的问题——把这种包含关系按缠论规则合并成一根干净的K线让顶分型、底分型、笔、中枢的自动识别有一个可靠的数据底座。这个项目值得做根源在于大量现成缠论指标把包含处理硬塞进通达信公式里遇到连续包含时要么重绘、要么滞后、要么错位。插件不一样它用编译代码逐根递推一次算对还方便你自己调控参数。适合正在做通达信缠论自动标记、又想弄明白每一步在算什么的人。2. 包含处理到底在算什么从两根K线的三种关系到合并规则2.1 先区分高低点与实体缠论只认区间不认红绿通达信里每根K线有四个核心数据开盘、收盘、最高、最低。包含处理第一步是把开盘和收盘忘掉只保留最高价 HIGH 和最低价 LOW。原因很简单缠论的分型、笔、中枢全部以K线区间为基准。一根K线能否覆盖另一根指的是最高最低区间的覆盖不是实体覆盖。比如一根大阳线实体很长但最高点低于前一根最低点也高于前一根它在缠论视角下就是“被包含”而不是向上突破。我用指标公式写实现时一律直接取HIGH和LOW两个序列不碰OPEN和CLOSE这样处理逻辑才不会混入K线形态本身的噪声。判断两根K线是否有包含关系核心是比对高低点的大小。当前K线 A 与上一根合并后的K线 B如果 A 的高点大于等于 B 的高点且 A 的低点小于等于 B 的低点视为 A 包含 B反过来A 的高点小于等于 B 的高点且 A 的低点大于等于 B 的低点视为 B 包含 A。注意等号也要算进去。缠论里完全相等就是包含关系只是合并后结果不变。实际行情中等号不多见但除权、停牌、涨跌停板会制造出这类边界值判断条件里漏掉等号后面的分型会在边界处反复横跳。还有一个容易被忽略的点包含关系是“谁包含谁”的两种方向。写程序时我习惯把判断函数做成只返回布尔量不返回谁包含谁。原因在后面的递推逻辑里会看得更清楚动态处理时你只需要知道“当前K线与合并缓存K线到底有没有包含”不需要知道方向方向由趋势状态决定。2.2 向上处理取高高、向下处理取低低方向错了全盘崩一旦确认两根K线存在包含就要按方向合并。方向不是由这两根K线自己决定的而是由它们之前的那段趋势方向决定的。合并规则本身非常简单方向新K线高点新K线低点向上取两者较高取两者较高向下取两者较低取两者较低向上时为什么低点也要取较高因为上涨趋势要求低点逐步抬高。包含区域本质是多空交叠的K线按“高高”合并后低点被抬上去序列重新恢复出单边向上的特征。向下反过来取“低低”高点压低低点也压低。很多人初写实现时只处理高点低点仍然沿用旧值画出来的笔就会在窄幅震荡里乱跳根源就在这里。方向判定比合并本身更微妙。缠论原文的意思是要看包含K线之前最近的一根非包含K线。实际操作中我维护一个合并后的基准K线cur顺序读取后续K线。如果cur与下一根K线没有包含关系就根据两者位置更新方向新K线高点高于cur且低点也高于cur方向记为向上反之记为向下。等到后面再出现包含时就沿用最近一次记下来的方向去合并。所以方向不是一个每根K线都重新计算的量而是一个随非包含K线更新的状态值。这里有个反直觉结论如果两根K线的高点一高一低、低点也一高一低但方向不一致比如 A 高点 10、低点 9B 高点 9.5、低点 8.5这种不是包含也不需要合并。A 高点高于 B但 A 低点也高于 B它们其实是两根正常向上排列的K线。只有高低点分别满足同向的大于等于、小于等于时才构成包含。这个点在写包含判断时极容易写反。我见过不少实现用“高点交叉且低点交叉”去判断结果把正常上涨的K线也合并了分型全错而且错得毫无规律。2.3 包含关系会传染必须逐根递推不能只合并相邻两根包含处理绝不能只看相邻两根就完事因为合并后的K线可能继续与下一根形成新的包含而且这种传递可以连续发生多根。看一个简单序列序号高点低点处理结果1108基准K线2117与1包含方向向上合并得高点11、低点83128与合并结果又包含继续合并得高点12、低点84119与合并结果无包含输出前一段并更新方向这个例子里如果只把1和2合并完就停下来3就会被留在外面最终顶分型位置会少算一根K线导致笔的启动点错位。所以核心算法必须是一个循环不能是相邻两两比较。常见做法是维护两个状态当前合并K线cur和方向dir。从头开始遍历每来一根新K线先与cur判断包含。包含就按当前方向合并不包含就先把cur作为已处理结果输出再用cur与这根新K线的位置关系刷新方向然后把新K线设为新的cur。整个序列处理完输出的合并K线数量一定小于等于原始K线数量。要注意一种边界情况如果序列一开始就是连续包含此时方向状态还是未知。我一般先按向上处理因为一旦后面出现第一对非包含K线方向就会被纠正如果整个序列从头到尾全是包含最终合并成一根K线方向其实不影响结果。进阶实现中也可以加入“方向待定”状态待定期内只记录不合并等到方向确定后再回补。但回补逻辑复杂实战价值不大我最终没采用。还有一个设计细节合并结果的输出必须保留原始时间轴。因为合并后K线数量减少如果直接把合并结果紧凑排列后续分型和笔的判断就无法对齐原始K线。常见做法是准备一个与原始K线等长的输出数组只在合并K线所在的原始下标位置写入结果其他位置用无效值填充。这样通达信画图时不会错位分型判断也可以直接跳过无效位置。3. 用公式实现包含处理两条死路与一个判断点3.1 通达信公式写包含判断的最小代码通达信公式语言能做很多事情但它的执行模型是“逐K线计算每根K线只能访问当前及之前K线”。在这个模型下写一个包含判断并不难。下面这段代码可以快速标出哪些K线与前一根存在包含关系{当前与前一K线} H : HIGH; L : LOW; PH : REF(H, 1); PL : REF(L, 1); {包含判断} INCL : (H PH AND L PL) OR (H PH AND L PL); {向上合并高低点} UPH : IF(H PH, H, PH); UPL : IF(L PL, L, PL); {向下合并高低点} DNH : IF(H PH, H, PH); DNL : IF(L PL, L, PL);这段公式的作用是INCL标记包含关系UPH/UPL给出向上方向合并后K线的高低点DNH/DNL给出向下方向合并后的高低点。它只处理了相邻两根K线的一层关系参数PH/PL用的是前一根原始K线不是前一根合并后的K线。所以严格说它只是“包含判断的最小代码”不是真正的包含处理。真正要处理连续包含公式必须能引用前一根合并后的结果这就撞上了公式语言的限制。如果想强行用公式逼近可以尝试把合并结果存成变量然后用REF在下一根K线引用。例如XH : IF(INCL, UPH, H); XL : IF(INCL, UPL, L);但XH的定义中用到INCL而INCL又依赖原始的PH/PL不会引用上一根XH。要让XH形成真正的递推需要写成类似XH : IF(INCL, MAX(H, REF(XH,1)), H)的形式。这种自引用在部分通达信版本里会被判定为循环定义即便能算出来遇到连续包含时也容易因为顺序问题产生错位。所以我一向不建议在这条路上死磕。3.2 公式方案真正的坑递推延迟与未来函数公式方案真正的坑不是写不出来而是写出来之后有几类严重副作用。第一类是递推延迟。通达信公式虽然可以通过REF(X,1)引用上一个合并结果但这种引用是建立在“上一根K线已经算完”的基础上。在包含连续发生时合并结果是逐层依赖的公式版本可能只递推了有限层后面继续出现的包含就只能按原始K线处理。结果就是越复杂的位置公式算得越不准。第二类是未来函数。为了让公式提前知道一段包含的终点或方向很多人会使用BACKSET类函数把后来的信号提前标记到前面的K线上。这样做出来的指标在盘中看是准的但行情更新后历史信号会被改写这就是俗称的“重绘”。盘中你看到一个顶分型等收盘再回头看顶分型可能消失了。缠论本身是事后完全确定的几何结构公式版本却普遍存在重绘这是很多用户骂缠论指标“骗人”的根因。第三类是性能问题。公式语言的本质是逐根计算如果你把几十行递推逻辑放进去全市场几千只股票逐根跑一遍通达信会明显卡顿。尤其在做选股和预警时公式方案经常拖垮整个软件。我至今没见过纯公式写的缠论包含处理能在全市场选股时跑出稳定速度的版本。3.3 插件DLL为什么是更可靠的载体绕过这些限制的正路是把核心算法移出公式语言放到一个外部 DLL 插件里。用 C/C 写循环、数组、状态机没有任何公式语言的逐根限制一次拿到整段K线数组处理完再返回结果。通达信只是负责调用和显示算法本身是确定性的不依赖未来函数。对比项通达信公式外部DLL插件循环与递推受限甚至禁止原生支持连续包含处理很难处理多层一个循环解决是否重绘容易被迫使用未来函数不依赖未来函数运行性能逐K线解释执行编译代码批量执行调试只能看图猜问题可以打印日志、单元测试这也是为什么标题里写的是“插件源码”而非“指标公式源码”真正能扛住实盘检查的版本本质上是一个带导出函数的 DLL配合一个极薄的通达信公式外壳。当然如果你只是复盘用也可以把处理逻辑用 Python 写好结果导入通达信看静态图但那种方式无法盘中实时刷新。DLL 插件是少数能在通达信里做到“盘中计算、结果稳定”的方式。4. 写一个C DLL来做包含处理接口、方向判定与合并循环4.1 DLL导出函数骨架通达信外部 DLL 插件的具体接口形式在不同版本里有差异。我习惯把核心算法做成与通达信无关的纯函数外面套一层导出函数这样换版本时只需要改接口壳。下面是一个最小骨架#include windows.h #include vector using namespace std; struct KBar { double high; double low; int index; // 原始K线序号 }; extern C __declspec(dllexport) int ProcessInclude( const double* high, const double* low, int count, double* outHigh, double* outLow, int* outFlag) { if (!high || !low || count 0) return -1; // 核心处理逻辑见 4.2 return 0; }函数参数含义high和low是通达信传入的原始高低价数组count是K线根数outHigh和outLow是处理后的输出数组outFlag用于标记哪些原始位置是合并后的有效K线。这里把输出长度保持为与输入一致而不是压缩后的长度就是为了避免画图错位。在核心算法里我们只操作KBar这个内部结构体跟通达信数据格式解耦。4.2 核心C代码包含判断、方向更新、合并输出这一段是可以直接抄走的算法核心。先写包含判断和合并函数再写主循环static inline bool Contains(const KBar a, const KBar b) { // a 包含 b或 b 包含 a都算包含 return (a.high b.high a.low b.low) || (a.high b.high a.low b.low); } static inline KBar Merge(const KBar a, const KBar b, int dir) { KBar r; if (dir 0) { // 向上高点取较大低点取较大 r.high (a.high b.high) ? a.high : b.high; r.low (a.low b.low) ? a.low : b.low; } else { // 向下高点取较小低点取较小 r.high (a.high b.high) ? a.high : b.high; r.low (a.low b.low) ? a.low : b.low; } r.index a.index; // 时间戳保留前面那根 return r; } extern C __declspec(dllexport) int ProcessInclude( const double* high, const double* low, int count, double* outHigh, double* outLow, int* outFlag) { if (!high || !low || count 0) return -1; vectorKBar bars(count); for (int i 0; i count; i) { bars[i].high high[i]; bars[i].low low[i]; bars[i].index i; } vectorKBar merged; KBar cur bars[0]; int dir 0; // 0 表示方向未定1 向上-1 向下 for (int i 1; i count; i) { if (Contains(cur, bars[i])) { // 连续包含先定一个默认方向后面会被纠正 if (dir 0) dir 1; cur Merge(cur, bars[i], dir); } else { // 非包含输出当前合并K线更新方向换基准 merged.push_back(cur); if (bars[i].high cur.high bars[i].low cur.low) { dir 1; } else if (bars[i].high cur.high bars[i].low cur.low) { dir -1; } cur bars[i]; } } merged.push_back(cur); // 写回原始位置保持输出数组与输入等长 for (int i 0; i count; i) outFlag[i] 0; for (size_t k 0; k merged.size(); k) { int idx merged[k].index; outHigh[idx] merged[k].high; outLow[idx] merged[k].low; outFlag[idx] 1; } return (int)merged.size(); }逻辑说明cur是当前正在累积的合并K线dir是最近一次非包含关系确定的方向。每来一根新K线先判断它和cur是否包含。包含则按dir合并非包含则表明一段处理结束把cur塞进结果同时利用它与新K线的相对位置刷新方向。最后把合并结果按原始index放回输出数组outFlag为 1 的位置才是处理后的有效K线分型判断只读这些位置。参数说明dir初始为 0遇到第一对包含时我先强制按向上处理。这会让开头的连续包含段按上涨合并但后续方向一旦确定就会被纠正。如果你希望更贴近缠论原文可以把if (dir 0) dir 1;改成先不合并、只记录原始K线等方向确定后再回头处理。我实际测试下来后者会让简单序列的处理结果更好但会让代码多一个回填状态复杂度高不少。大多数人建议先按默认向上跑通再考虑回填。4.3 编译与接入通达信的步骤编译这一步不复杂但有几个细节容易卡住。用 Visual Studio 新建一个 C 空项目把上面的代码粘贴进.cpp文件然后在项目属性里把配置类型设为“动态库(.dll)”。注意extern C一定要加否则 C 编译器会对导出函数做名称修饰通达信在加载时找不到函数名。我早期吃过这个亏导出的函数名变成了一长串乱码怎么调用都失败。编译成功后会在输出目录得到一个 DLL 文件。接入通达信的具体方式因软件版本而异常见流程是在通达信的公式管理器里找到外部接口或者说 DLL 函数注册入口把 DLL 复制到指定目录然后在公式里声明并调用。导出函数的名字应当与 DLL 中的__declspec(dllexport)名称一致。有的版本要求 DLL 导出固定的入口点比如Plugin或RegisterPlugin这时候需要在壳函数里再包一层把通达信传入的行情结构体拆解成high和low数组再调用ProcessInclude。接入后先用最简单的公式验证一下{示例公式调用外部DLL} A:PLUGIN_INCLUDE(HIGH, LOW, 1, 0);这行公式是我用来占位的每个版本调用方式都不一样重要的是你手写的时候传入的参数必须和 DLL 导出函数签名对应。如果返回的数组长度、输出位置对不上先不要怀疑算法先检查接口壳。5. 缠论K线包含处理插件的4个常见翻车点与排查方法5.1 翻车点1方向判定被合并后的K线带偏现象用同一个K线数据集人工判断的结果和插件输出差很多笔的拐点总是多画几个。原因方向更新用了合并后的K线去比较。假设一段上涨后出现一根包含合并后的低点可能仍然抬升但如果拿合并后的K线和下一根比较高低点关系可能被误判成向下方向立刻反转。正确做法是方向只由最近一对非包含K线决定包含合并过程中方向保持不变。解决看第4章的代码方向更新只出现在else分支也就是非包含那一侧。在Contains分支里dir只被使用不被修改。如果还是出现误判可以在每次更新方向时打印日志把cur和新K线的高低点输出来对一下肉眼判断。5.2 翻车点2包含处理后K线数量减少时间轴对不上现象插件输出的数组很短或者输出长度和输入一样但后续分型计算串位越到后面越离谱。原因很多实现把合并后的K线按顺序紧凑排列用 vector 压完直接返回丢失了原始时间索引。原始K线第100根可能对应合并后的第60根后面画笔、画中枢全部错位。解决输出数组长度始终等于输入长度用outFlag标记有效位置。在outHigh和outLow里只有outFlag[i] 1的位置是合并后的K线。分型识别函数在遍历时看到outFlag[i] 0就跳过但不要压缩数组。这样时间轴永远对齐。5.3 翻车点3除权、停牌、涨停板造成的假包含现象某只票在除权日或连续停牌后插件把一段其实不相关的K线合并成一整根分型全部消失。原因除权让价格水平整体跳变停牌期间高低点可能为 0或者出现涨跌停板上的high low单点K线。这些数据在程序眼里都满足“高点相等、低点相等”于是被当成包含关系。解决在处理前过滤无效K线。我一般先剔除high 0 || low 0的K线然后对除权数据统一使用后复权价格。完全相等的情况建议做成一个参数equalAsInclude默认打开但在除权环境下关闭。更稳的做法是在 DLL 外先把复权数据算好再传入插件不要在插件内部处理复权细节。5.4 翻车点4DLL内存释放与多线程调用现象插件刚开始加载能用第二次调用时返回数据错乱或者通达信直接崩溃。原因函数内使用了static变量缓存中间结果或者全局变量在多线程环境下被多个窗口同时调用数据竞争把状态搞坏。另外输出缓冲区如果写入越界也会导致崩溃。解决把插件函数写成无状态函数所有中间数据放在函数栈上或局部vector中不要用static。如果通达信是多窗口同时刷新建议在函数入口加锁或者干脆每次调用都重新分配内存。重点是输出写入前用count限制边界。我后来的插件全部改成无状态设计后再没出现过这类问题。6. 不画分型也能验货用三组人工K线测插件对不对插件写好后不要急着进通达信看效果先在一个独立的测试程序里跑三组人工K线每组输入和预期输出都写死。这样做的好处是直接把算法错误和通达信接口错误隔离开来。我用的是 Python 的ctypes加载 DLL测试代码很短import ctypes dll ctypes.CDLL(./ChanInclusion.dll) dll.ProcessInclude.argtypes [ ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.c_int, ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_int) ] def run(h, l): n len(h) ha (ctypes.c_double * n)(*h) la (ctypes.c_double * n)(*l) oh (ctypes.c_double * n)() ol (ctypes.c_double * n)() flag (ctypes.c_int * n)() cnt dll.ProcessInclude(ha, la, n, oh, ol, flag) return cnt, [(i, flag[i], oh[i], ol[i]) for i in range(n)] print(run([10, 11, 12], [8, 7, 8])) print(run([10, 11, 9], [8, 7, 9])) print(run([10, 9, 8], [8, 7, 6]))第一组是上涨连续包含预期结果是第一根K线被合并掉最后只有一根有效输出位置应该落在第三根第二组是先涨后包含再跌方向切换后输出两根K线第三组是单边下跌不需要合并全部输出。如果第一组跑出的 K 线位置不对十有八九是Merge里的index取值出了问题。我习惯在Merge中取前一根的index这样输出位置代表这段区间开始的时间便于后续画分型。这个方法基本是十分钟就能跑完的验证流程但能省掉大量在通达信主图上反复切换股票的精力。我自己的习惯是先把这三组跑通再加上全市场随机抽样跑一遍只统计合并后K线数量变化是否合理。如果任何一组输出和手工推演对不上优先检查方向更新分支而不是通达信调用。这个项目里最容易翻车的不是算法本身而是方向状态和时间戳保留方式。把这些细节定死以后插件才能真正稳定跑在盘中行情上希望帮到你。本文还有配套的精品资源点击获取