简介:这套源码是通达信平台上的缠论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、低点 9,B 高点 9.5、低点 8.5,这种不是包含,也不需要合并。A 高点高于 B,但 A 低点也高于 B,它们其实是两根正常向上排列的K线。只有高低点分别满足同向的大于等于、小于等于时才构成包含。这个点在写包含判断时极容易写反。我见过不少实现用“高点交叉且低点交叉”去判断,结果把正常上涨的K线也合并了,分型全错,而且错得毫无规律。
2.3 包含关系会传染:必须逐根递推,不能只合并相邻两根
包含处理绝不能只看相邻两根就完事,因为合并后的K线可能继续与下一根形成新的包含,而且这种传递可以连续发生多根。看一个简单序列:
| 序号 | 高点 | 低点 | 处理结果 |
|---|---|---|---|
| 1 | 10 | 8 | 基准K线 |
| 2 | 11 | 7 | 与1包含,方向向上,合并得高点11、低点8 |
| 3 | 12 | 8 | 与合并结果又包含,继续合并得高点12、低点8 |
| 4 | 11 | 9 | 与合并结果无包含,输出前一段并更新方向 |
这个例子里,如果只把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; vector<KBar> bars(count); for (int i = 0; i < count; i++) { bars[i].high = high[i]; bars[i].low = low[i]; bars[i].index = i; } vector<KBar> 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 翻车点4:DLL内存释放与多线程调用
现象:插件刚开始加载能用,第二次调用时返回数据错乱,或者通达信直接崩溃。
原因:函数内使用了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线数量变化是否合理。如果任何一组输出和手工推演对不上,优先检查方向更新分支,而不是通达信调用。
这个项目里最容易翻车的不是算法本身,而是方向状态和时间戳保留方式。把这些细节定死以后,插件才能真正稳定跑在盘中行情上,希望帮到你。
本文还有配套的精品资源,点击获取