简介:缠论dll源码是一套基于“笔、段、中枢”核心理论的C++算法实现,面向量化交易开发者与缠论爱好者。包内含头文件、源码文件、Visual Studio工程文件及PDF使用说明等共36个文件,压缩包大小约4.11MB,可在VS环境中直接编译生成动态链接库,便于集成到行情软件或交易系统中。代码完全开源,开发者可自由查看、修改和二次分发,根据自身策略定制缠论分析模块,节省重复造轮子的成本。文件结构清晰,涵盖ChanlunCore、ChanlunTools等核心模块,并附有编译后的中间文件,方便调试与排错。已有6496人学习下载,适合具备C++基础、希望将缠论理论落地为自动化指标的开发者参考使用。 做缠论量化的人应该都有过这种经历:不管你是用通达信写公式,还是拿Python跑脚本,一旦数据量上来、或者要接到自己的交易系统里,就绕不开一个坎——算法性能不够,或者语言之间互相调不动。我自己折腾缠论DLL源码就是因为这个原因:笔、段、中枢这套逻辑,用脚本语言写着方便,但真要做全市场扫描或者盘中实时计算,还得靠C/C++级别的代码把核心算法固化成DLL,给其他模块调用。
这篇文章就围绕“用DLL封装缠论笔段中枢算法”这件事展开,从规则拆解、数据结构设计、接口约定到边界条件处理,把我在实际开发中踩过的坑和验证过的方案完整梳理一遍。适合正在做缠论量化、想把算法工程化,或者纯粹对技术分析算法落地感兴趣的朋友参考。
1. 为什么偏偏要用DLL来装缠论算法
先说结论:缠论算法本质上是一套“状态机式”的序列处理逻辑,K线一根根进来,分型、笔、段、中枢都是增量构建的。这种逻辑在C++里写,内存布局和数据结构都可控,处理海量数据时性能优势非常明显。而DLL这个形态,恰好解决了跨语言调用的问题。
1.1 脚本语言的瓶颈在哪里
初版我用Python验证过缠论算法。行情数据拉全A股5000多只股票,每只算笔段中枢,内存里的对象一多,速度就明显下来。更麻烦的是Python的GIL锁,多线程扫描时只能串行。后来想用C#做界面,Python做算法,两个进程之间靠文件或者socket通信,磕磕绊绊能用,但工程上非常难受。
把核心算法抽出来编译成DLL之后,情况完全不一样了。C++代码直接操作内存里的数组,不需要解释执行,也不存在GC停顿。外部程序只需要通过几个导出函数,把行情数据传进去,把计算结果拿回来,剩下的优化全部在DLL内部进行。
1.2 DLL的边界优势
DLL(动态链接库)之所以适合封装缠论算法,核心在于接口稳定、实现隐藏。你只需要公开一个头文件,里面写清楚结构体和函数签名,调用方不需要关心你内部是用什么数据结构组织笔、怎么递归判断线段被破坏。后续优化算法时,只要接口不变,DLL替换掉就行,调用方一行代码都不用改。
这个特性对量化系统的意义很大。策略端和算法端往往是不同人维护的,把算法收敛到DLL里,相当于把复杂度关在一个黑盒里,外部只看到“传入K线数组,传出笔段中枢数组”。不同模块之间互相不污染,定位问题也快。
2. 缠论规则拆解:从文字描述到可计算逻辑
缠论的原始定义是用自然语言写的,比如“顶分型”“底分型”“笔”“线段”“中枢”这些概念,人脑判断很直观,但程序没法直接处理。要把它们变成代码,第一步就是把每条规则转成明确的输入、条件和输出。
2.1 我整理的规则化表格
我在编码前做了一张规则拆解表,把缠论概念逐一量化。这张表是后面所有代码的“需求文档”,非常重要,因为缠论的模糊地带非常多,不先定清楚,代码写一半一定返工。
| 概念 | 输入数据 | 判定条件 | 输出 |
|---|---|---|---|
| 包含关系处理 | 相邻K线高低点 | 前一根同时包含后一根的高低价,则合并 | 处理后的标准K线序列 |
| 分型 | 3根标准K线 | 中间K线高点最高且低点最高为顶分型,反之底分型 | 分型数组 |
| 笔 | 分型序列 | 相邻顶底分型间隔至少4根标准K线,且方向交替 | 笔的端点(顶/底)集合 |
| 线段 | 笔序列 | 特征序列的顶底分型是否被破坏 | 线段端点集合 |
| 中枢 | 线段或笔的走势 | 至少3个连续次级别走势类型价格区间重叠 | ZG(中枢高点)、ZD(中枢低点)、GG(最高点)、DD(最低点) |
这里有个容易踩的坑:缠论原文里对“次级别”的界定在不同语境下是可以变的。比如日线级别的笔,在30分钟级别就是走势类型。所以在DLL里做中枢计算,我直接设计一个参数level_shift,指定中枢由几个笔组成(一般是3个),而不是把级别嵌套写死。这样灵活度高,不会因为级别定义分歧导致程序逻辑僵死。
2.2 包含关系:最容易被忽略的预处理
很多人写缠论程序,先做分型再做笔,最后发现笔的端点对不上,回头查半天才发现是K线包含关系没处理干净。包含关系的处理规则其实很机械:如果相邻两根K线存在包含,就把它们合并成一根,方向由合并前的趋势方向决定。
趋势方向怎么判断?看合并前两根K线的高低点关系。如果前一根是阳线且高点更高,那么合并时取高点中较高的、低点中较高的,相当于向右上方合并;反之取两者中较低的高点和较低的低点。这个过程是递归的——合并完的新K线可能继续和下一根发生包含,所以要用循环处理到不再出现包含为止。
这个步骤我单独写了函数merge_inclusive_bars,放在DLL的最底层。原因很简单:笔段中枢的所有计算都建立在纯净的K线序列之上,前面错一个点,后面全错。
3. DLL接口设计:数据结构与内存管理约定
接口设计决定了DLL好用到什么程度。我第一版直接把所有K线数据一次性传入,返回一个复杂嵌套的链表结构,结果调用方解析起来极其痛苦。后来参考了金融行情接口的常见做法,改成“扁平的数组 + 索引区间”模式。
3.1 核心数据结构的C++定义
// 标准K线结构,也是传入DLL的行情数据结构 typedef struct { double high; double low; double open; double close; long long timestamp; // 时间戳,用于回放模式比较 } BarData; // 笔结构:记录一笔的起止位置和方向 typedef struct { int start_index; // 笔起始点在标准K线数组中的下标 int end_index; // 笔结束点下标 int direction; // 1为向上笔,-1为向下笔 double start_price; double end_price; } BiStruct; // 中枢结构:记录重叠区间的范围和组成 typedef struct { double zg; // 中枢上沿 double zd; // 中枢下沿 double gg; // 区间内的最高点 double dd; // 区间内的最低点 int start_index; // 中枢起始位置 int end_index; // 中枢结束位置(进入第三段的位置) int segment_count; // 构成中枢的走势段数量 } ZhongshuStruct;选择double存价格,而不是float,是因为缠论计算里有很多区间比较和重叠判断。float的精度在价格很大时(比如指数几万点)可能出现两个本应相等的临界值误判。用double,再配合一个极小阈值做容差比较,可靠性高很多。
3.2 导出函数的命名与约定
DLL的导出函数我用extern "C"包装,避免C++名字修饰导致其他语言无法识别。核心导出接口最终精简为四个:
// 初始化DLL内部上下文,传入需要计算的最大K线数量 __declspec(dllexport) void* ZL_Init(int max_bars); // 释放上下文内存 __declspec(dllexport) void ZL_Release(void* ctx); // 执行笔段中枢计算,输入原始K线数组和数量,输出各结果数组下标区间 __declspec(dllexport) int ZL_Calculate(void* ctx, BarData* bars, int bar_count, int* bi_ranges, int* seg_ranges, ZhongshuStruct* zs_list, int max_output); // 获取最后一次计算的错误码 __declspec(dllexport) int ZL_GetLastError(void* ctx);内存约定上,我坚持“DLL内部分配的内存由DLL内部管理”。调用方传入的是自己分配好的输出缓冲区,DLL只负责往缓冲区里填数据,不额外分配需要外部释放的内存。这样做是为了避免跨DLL边界的free/delete不匹配问题——在Windows上用过不同编译器版本的人应该都有过这种血泪教训。
4. 笔段中枢核心算法:增量构建与关键判定
数据结构定好了,算法是重头戏。缠论笔段中枢的实现在工程上可以总结为“逐K线扫描 + 分型识别 + 破坏判定”。
4.1 笔的划分:分型确认与最小间隔约束
笔的划分核心是找出有效的顶分型和底分型,并且相邻两个分型必须满足最小间隔。缠论原文里说得很清楚:顶分型与底分型之间至少要有1根独立的标准K线,加上分型本身占用的3根,换算成标准K线就是至少5根。
代码里我是这样处理的:先扫描整个标准K线数组,找出所有顶底分型,记录它们的位置。然后对分型序列做一轮筛选——从第一个分型开始,如果相邻两个分型的间隔不满足最小K线数要求,就保留“更高”的顶分型或者“更低”的底分型,舍弃另一个。这一步其实就是典型的贪心选择。
// 分型筛选核心逻辑(简化版) void filter_fractals(int* fractal_index, int* fractal_type, int fractal_count, int* out_index, int* out_type, int* out_count, int min_gap) { int last_bottom = -1, last_top = -1; for (int i = 0; i < fractal_count; i++) { int idx = fractal_index[i]; if (fractal_type[i] == FRACTAL_BOTTOM) { if (last_bottom != -1 && idx - last_bottom < min_gap) { // 保留更低点:比较标准K线中的low if (bars[idx].low >= bars[last_bottom].low) continue; } last_bottom = idx; // 记录有效底分型... } // 同理处理顶分型,注意顶底交替约束 } }这里有一个“方向交替”的约束很容易写错:两个连续的顶分型之间必须夹着一个底分型,否则不能成笔。如果中间某个底分型因为间隔不足被剔除了,那相邻两个顶分型之间就不存在笔。我第一版代码就因为这个bug,在连续上涨行情中画出了“无底之顶”的错误笔。
4.2 线段划分:特征序列是理解的关键
线段在缠论里的定义比笔复杂得多。一个线段至少由3笔组成,线段会被新的反向线段“破坏”。判断破坏的核心是特征序列的顶底分型在笔的方向上是否确立。
简化实践中,我采用的是更工程化的做法:基于已确定笔序列,按方向分组。比如向上线段,观察向下笔的高点之间能否形成特征序列的顶分型;一旦形成,且新出现的反向笔向下延伸超过该分型位置,则判定线段结束。
// 线段判定主循环(示意逻辑) for (int i = 1; i < bi_count; i++) { BiStruct* b = &bi_list[i]; if (cur_seg_direction == UP) { if (b->direction == DOWN) { // 收集向下笔的高点,判断是否形成特征序列顶分型 check_feature_sequence(b, &seg_state); if (seg_state.broken) { // 当前向上线段结束,新线段起点为... } } } // 对称处理向下线段 }诚实说,线段是最容易产生分歧的模块。缠论原文对“破坏”的定义在特殊边界情况下有回旋余地,不同人画出来的线段不完全一样。我的解决办法是把线段判定设计成“可配置严格度”:SEGMENT_MODE_STRICT和SEGMENT_MODE_LOOSE两个模式,前者要求特征序列分型确认后才算破坏,后者只要出现反向笔创新低/新高就算破坏。实盘中严格模式更接近缠论原意,但漏判多一些;宽松模式对趋势反转更敏感。以自己策略风格为准。
4.3 中枢的区间计算
中枢的定义相对机械:连续三个次级别走势类型的价格区间重叠。重叠区间的上沿ZG等于三个区间ZD的最大值,下沿ZD等于三个区间ZG的最小值。GG和DD则是所有构成中枢的笔的最高点和最低点。
用代码实现时,我按“笔区间”做滑动窗口:每形成一个新笔,就查看它与之前若干个同级别笔的区间是否构成三笔重叠。判断三笔重叠有一个快速做法——如果当前笔的上沿小于前三笔任何一个下沿,肯定无重叠;反之,重叠区间的上沿取三笔上沿的最小值、下沿取三笔下沿的最大值。
// 三笔重叠区间判断 bool overlap_3bi(const BiStruct* bi1, const BiStruct* bi2, const BiStruct* bi3, double* zg, double* zd) { // 每一笔的区间取高低点 double up1 = max(bi1->start_price, bi1->end_price); double down1 = min(bi1->start_price, bi1->end_price); // ... 同理获取 up2 down2 up3 down3 *zg = min(up1, min(up2, up3)); // 重叠区间上沿 = 三者的最小值 *zd = max(down1, max(down2, down3)); // 重叠区间下沿 = 三者的最大值 return *zg > *zd; // 上沿大于下沿才说明真有重叠 }这个算法的时间复杂度取决于笔数量。全量扫描时,每来一个新笔,最多回溯前面所有笔去找成中枢的最早起点,最坏是O(n^2)。实盘几万根K线也能接受,但做全市场扫描时我会把历史中枢只计算一次,实时部分换成增量逻辑,速度就上来了。
5. 边界条件与浮点陷阱:最容易翻车的三处
写缠论DLL最磨人的不是算法结构,而是各种边界情况。分享三个我实际踩过的坑,这些都是看源码学不来的经验。
5.1 浮点比较必须用容差
价格比较直接用==会导致大量误判。原因很简单:从不同数据源拿到的K线价格,可能一个是1.2300000000000001,一个是1.2299999999999998。在判断两笔是否重叠时,这种微小误差直接让本该成立的区间重叠变成不重叠。
我统一的解决方案是定义常量PRICE_EPSILON = 1e-8,所有价格比较都改成“差值绝对值是否小于EPSILON”。宁可让判断稍微宽松一点,也不要在临界点反复横跳。
5.2 未完成K线和未来函数问题
这是个致命的坑。如果你做盘中实时计算,最后一根K线是未走完的,它的高点低点随时会变。如果在DLL计算中枢时把未完成K线当成完整K线处理,出来的结果会来回跳变,这在量化交易里就是未来函数了——你看到的结果包含了未来信息。
我的做法是DLL里加一个is_closed数组标记,或者简单粗暴地在ZL_Calculate里增加一个参数complete_count,表示前多少个K线是已收盘的。实时计算只把未完成K线参与“当前笔尚未结束”的状态维护,不允许用它开启新笔或者新线段。回测时complete_count等于bar_count,行为完全一致。
5.3 数据量不足时的优雅降级
如果传入的数据只有几十根K线,那大概率连一笔都成不了。这时候DLL绝不能返回奇怪的负下标或者野指针。我在ZL_Calculate开头会检查bar_count < min_required,直接返回0,表示“本次没有计算任何输出”。调用方看到结果数量为0就能自行处理。这个处理逻辑想起来简单,但真出问题的时候往往是数组越界带来的崩溃,比逻辑错误难查一百倍。
6. 验证方法:怎样确认DLL里的笔段中枢画对了
算法写完了,怎么验证它算的是对的,是另一个大问题。我的验证方法分两层:跑历史数据和可视化对比。
6.1 用历史大牛股/大熊股做回归样本
我专门收集了一批典型行情数据:一波流畅上涨的、一段宽幅震荡的、一个爆拉后V型反转的、还有长期阴跌的。这些行情里的笔段中枢结构相对清晰,手工画和程序算应该能对上。DLL每次改完,我第一件事就是跑这组回归样本,对比前后输出的笔段中枢数量是否变化。之所以强调“数量对比”,是因为这个指标最敏感——任何细节的改动都会体现在中枢数量和位置的增减上。
6.2 导出中间结果到图表工具
算法内部的每一步都有中间产物:原始K线、合并后的标准K线、分型位置、笔端点、线段端点、中枢区间。我写代码时给DLL加了一个调试模式,可以把这些中间结构导出成独立的CSV文件。然后在看图软件里加载原始K线,把笔端点连成线、把中枢框成矩形,人眼对照缠论规则逐段审核。
这一步看似笨重,但效率极高。许多逻辑错误——比如笔跨越了不该超越的顶分型、中枢区间画得太宽——直接看图就能发现,定位到具体下标再去查代码逻辑,几分钟就能找到根因。
6.3 和已有缠论指标对比
网上有很多现成的缠论指标源码,图片公式、通达信公式、Python库都有。我拿DLL的输出结果和其中口碑较好的几个指标做交叉验证。异同都要分析:一致当然好,不一致时优先检查DLL的中间日志,看是笔的划分差异还是线段的破坏判定差异。坦率说,没有两个缠论完全一致的程序,“细节分歧”在缠论社区里太正常了,关键是确认你的逻辑自洽,而不是盲目对齐别人的输出。
7. DLL性能调优:从全量扫描到增量更新
最后聊性能。DLL封装缠论算法,性能目标通常是“盘中实时刷新不卡顿”和“全市场后台扫描可接受”。两种场景的优化策略不一样,我的实践结论如下:
7.1 实时计算:维护增量状态机
盘中实时计算时,每来一根新K线,不需要从头跑整个算法。正确做法是保持当前已确认的笔段中枢状态,让新K线参与到“最后一笔/最后一个中枢”的更新中。状态机里记录正在构建中的笔的起点、方向、当前极值,新K线到来时先尝试推进当前笔,再判断是否满足新分型条件。
我用这个思路改造后,单只股票的实时刷新耗时从全量重算的几十毫秒下降到微秒级。整个改动核心就在于把“全量扫描”拆成“状态增量”,每个K线只做常数量的运算。
7.2 全市场扫描:用缓存换重复计算
全市场扫描的性能瓶颈主要在读取大量K线数据和重复计算重叠区间。我的优化是两层缓存:第一层,已计算过的股票如果最后K线数量没有增加,直接复用上一次的结果;第二层,同一只股票做多周期分析时(比如日线和60分钟都算中枢),把日线级别的笔序列缓存起来供30分钟级别做参照,避免重复从原始K线开始解析。
整套DLL在本地测试机上扫全市场约5000只股票,从最初的一分多钟优化到十几秒,已经能满足日常复盘的节奏。
7.3 多线程时DLL的注意事项
DLL被多个线程同时调用时,内部如果用到了静态/全局变量,就会产生线程安全问题。我设计的ZL_Init会返回一个独立的上下文指针ctx,所有后续调用都必须把这个指针传回来。每个线程持有自己的ctx,互不干扰。这样虽然增加了调用方的使用复杂度,但绝对避免了数据竞争。如果你想把DLL接口改成“无状态”纯函数,也可以,但那样每次调用都要重新分配内部临时内存,性能会有损失。两个方案我取舍后选了上下文方式,稳字当头。
提示:多线程下务必检查DLL编译时是否启用了
/MT或/MD运行库的一致配置。调用方和DLL的运行库不一致,在多线程环境下可能引发内存崩溃,这个坑非常隐蔽。
写在最后
从缠论文本到一套可用的DLL源码,中间隔着大量工程细节。把“笔段中枢”这四个字变成真正能在回测和实盘中稳定运行的代码,需要的不只是缠论理解,更是对数据结构、边界条件和跨语言接口的驾驭能力。我个人最深的体会是:算法实现中反复出现的“模糊地带”,正是缠论本身留给使用者的自由裁量空间,在DLL里把这些裁量显式化成配置参数,比暗中拍脑袋定死逻辑要可靠得多。
后续我计划在现有DLL基础上增加背驰判断和买卖点标记的导出接口,把整个体系从“结构划分”推进到“信号输出”。这个方向涉及的规则化难度更高,等有可复现的结果再来分享。
本文还有配套的精品资源,点击获取