简介:这份缠论DLL源码面向具备C++基础、希望将缠论分析能力集成到自有金融软件中的开发者与量化爱好者。资源以动态链接库形式封装了笔、段、中枢等核心概念的算法实现,可直接被其他程序调用,省去重复编写趋势判断与多空平衡计算逻辑的成本,并支持在Visual Studio环境下编译与二次开发。压缩包共36个文件,约4.11MB,以h头文件、cpp源文件、sbr与obj编译中间文件为主,另含sln、vcproj等VS工程文件及pdf使用说明,便于快速还原工程结构。目前已有6561人学习下载。借助完全开源的代码,读者可深入理解笔的划分、段的连接与中枢形成逻辑,并在此基础上定制自己的行情分析工具,提升技术分析效率。
1. 缠论dll源码里的笔段中枢:一份能直接编译落地的量化工具包
如果你做过缠论相关的量化策略,大概率经历过这个阶段:网上找了一堆Python版本的缠论实现,K线包含处理写得五花八门,笔的划分规则各说各话,到了线段和中枢更是直接翻车——同一个品种同一段行情,两个库跑出来的中枢区间能差出好几个价位。问题出在缠论本身的规则虽然写在书里,但落到代码层面有大量边界情况需要处理,而大部分开源实现只覆盖了标准场景。
这份缠论dll源码把笔、段、中枢三个核心模块封装成了动态链接库,调用方只需要传入K线序列,就能拿到结构化的笔段中枢数据。它解决的不是“缠论是什么”的问题,而是“怎么让缠论的结构识别在代码层面稳定复现”的问题。适合两类人:一是做量化策略需要缠论结构作为信号输入的开发者,二是想研究缠论算法实现细节的技术人员。源码包里包含完整的dll工程文件、调用示例和参数配置文件,拿到手就能编译验证。
2. 笔段中枢的算法逻辑:从K线包含到中枢区间的完整链路
2.1 K线包含处理与笔的划分规则
缠论的第一道门槛是K线包含处理。原始K线存在大量包含关系(一根K线的高低点完全被另一根覆盖),必须先做合并才能进行后续的笔划分。这份源码里包含处理的核心逻辑是:向上处理时取高高、低高;向下处理时取高低、低低。方向由前一根合并后K线与当前K线的相对位置决定。
# K线包含处理核心逻辑(示意,实际在dll内部实现) def merge_kline(klines): merged = [klines[0]] for i in range(1, len(klines)): cur = klines[i] last = merged[-1] # 判断是否存在包含关系 if (cur.high <= last.high and cur.low >= last.low) or \ (cur.high >= last.high and cur.low <= last.low): # 确定处理方向:看再前一根合并K线的位置 if len(merged) >= 2: prev = merged[-2] direction = 1 if last.high > prev.high else -1 else: direction = 1 # 默认向上 if direction == 1: # 向上处理:取高高 new_high = max(cur.high, last.high) new_low = max(cur.low, last.low) else: # 向下处理:取低低 new_high = min(cur.high, last.high) new_low = min(cur.low, last.low) merged[-1] = KLine(new_high, new_low) else: merged.append(cur) return merged这段逻辑的关键参数是方向判断。很多实现翻车就翻在这里——当只有一根合并K线时无法判断方向,源码里默认向上处理,这在趋势初期可能产生偏差。实际使用时建议传入至少3根K线预热,或者通过外部参数指定初始方向。
笔的划分建立在合并K线之上。源码采用的规则是:相邻两个顶底分型之间至少有一根独立K线,且顶分型的高点高于底分型的低点。这里有个容易忽略的细节——分型之间不能共用K线,否则会出现笔的嵌套。dll内部通过索引偏移来保证这一点,调用方不需要额外处理。
2.2 线段的特征序列与破坏判定
线段是笔的上一级结构,由至少三笔构成。源码里线段的划分用的是特征序列方法:把线段中所有与线段方向相反的笔提取出来,构成特征序列,然后判断特征序列是否出现顶分型或底分型来确认线段结束。
# 线段特征序列提取(示意) def extract_feature_seq(bi_list, direction): """ bi_list: 笔的列表,每笔包含(high, low, start_idx, end_idx) direction: 1表示向上线段,-1表示向下线段 """ feature_seq = [] for bi in bi_list: if direction == 1 and bi.is_down: # 向上线段中,向下笔构成特征序列 feature_seq.append(bi) elif direction == -1 and bi.is_up: # 向下线段中,向上笔构成特征序列 feature_seq.append(bi) return feature_seq def check_segment_break(feature_seq): """判断特征序列是否形成分型""" if len(feature_seq) < 3: return False # 检查连续三个特征元素是否构成顶分型 for i in range(1, len(feature_seq) - 1): if feature_seq[i].high > feature_seq[i-1].high and \ feature_seq[i].high > feature_seq[i+1].high: return True return False特征序列方法有两个坑:一是特征序列本身也需要做包含处理,二是第一种情况和第二种情况的破坏判定标准不同。源码里对这两种情况做了区分,通过一个break_type参数返回给调用方。如果你只关心线段是否结束,忽略这个参数也能用;但如果要做线段级别的递归划分,这个参数必须处理。
2.3 中枢的构建与延伸扩展
中枢的定义是至少三个连续次级别走势的重叠区间。在笔段体系下,中枢由至少三笔的重叠部分构成。源码里中枢的构建逻辑是:取连续三笔的高低点区间,计算重叠部分,如果重叠区间存在则构成中枢。
# 中枢构建核心逻辑(示意) def build_zhongshu(bi_list): zs_list = [] i = 0 while i < len(bi_list) - 2: bi1, bi2, bi3 = bi_list[i], bi_list[i+1], bi_list[i+2] # 计算三笔的重叠区间 high = min(bi1.high, bi2.high, bi3.high) low = max(bi1.low, bi2.low, bi3.low) if high > low: # 存在重叠,构成中枢 zs = ZhongShu(high=high, low=low, start=i, end=i+2) # 检查后续笔是否延伸中枢 j = i + 3 while j < len(bi_list): if bi_list[j].high >= low and bi_list[j].low <= high: zs.end = j zs.high = min(zs.high, bi_list[j].high) zs.low = max(zs.low, bi_list[j].low) j += 1 else: break zs_list.append(zs) i = zs.end + 1 else: i += 1 return zs_list中枢延伸的处理是这份源码比较扎实的地方。很多简易实现只取前三笔就结束,但实际行情中中枢经常会延伸出更多笔。源码里用了一个while循环持续判断后续笔是否与中枢区间有重叠,有则纳入延伸,无则结束。这个逻辑看起来简单,但边界条件很多——比如第九笔之后中枢升级的处理,源码里通过level字段标记了中枢级别,调用方可以根据级别做过滤。
3. dll调用实战:从编译到拿到笔段中枢数据
3.1 编译环境与工程配置
源码包里的dll工程支持两种编译方式:Visual Studio直接打开sln文件,或者用CMake生成工程。我一般用CMake,因为跨平台方便,而且能直接集成到现有的构建流程里。
# CMake编译步骤 mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . --config Release # 编译产物在 build/Release/ 目录下 # Windows下生成 chanlun.dll 和 chanlun.lib # Linux下生成 libchanlun.so编译时需要注意几个参数:CMAKE_BUILD_TYPE建议用Release,Debug版本在大量K线数据下性能差距明显;如果目标平台是32位系统,需要加-A Win32参数。源码里没有依赖第三方库,标准C++11即可编译,这点比较省心。
编译完成后得到两个文件:动态库和对应的导入库。Windows下是.dll和.lib,Linux下是.so。调用方只需要链接导入库,运行时把动态库放到可执行文件同级目录或系统库路径下。
3.2 接口调用与数据传递
dll暴露的接口设计得比较克制,核心就三个函数:初始化、传入K线、获取结果。数据传递用的是结构体数组,没有用JSON或字符串,这样在大量数据下性能更好。
// 接口头文件关键定义 typedef struct { double high; double low; long long timestamp; // 毫秒时间戳 } KLineData; typedef struct { int start_idx; // 起始K线索引 int end_idx; // 结束K线索引 double high; // 笔的高点 double low; // 笔的低点 int direction; // 1向上,-1向下 } BiData; typedef struct { int start_idx; int end_idx; double zg; // 中枢上沿 double zd; // 中枢下沿 int level; // 中枢级别 } ZhongShuData; // 核心接口 int chan_init(int max_kline_count); int chan_feed_kline(KLineData* klines, int count); int chan_get_bi(BiData* out_bi, int* bi_count); int chan_get_zhongshu(ZhongShuData* out_zs, int* zs_count); void chan_release();调用流程是:先chan_init初始化,指定最大K线数量;然后chan_feed_kline传入K线数组;接着调chan_get_bi和chan_get_zhongshu获取结果;最后chan_release释放内存。注意chan_feed_kline是增量式的,可以多次调用追加数据,内部会自动做包含处理和笔段更新。
// 调用示例 #include "chanlun.h" int main() { // 初始化,最多处理10000根K线 chan_init(10000); // 模拟传入K线数据 KLineData klines[100]; // ... 填充klines数据 ... chan_feed_kline(klines, 100); // 获取笔 BiData bi_list[200]; int bi_count = 0; chan_get_bi(bi_list, &bi_count); // 获取中枢 ZhongShuData zs_list[50]; int zs_count = 0; chan_get_zhongshu(zs_list, &zs_count); // 使用结果... chan_release(); return 0; }参数方面,max_kline_count建议设成实际数据量的1.5倍,因为包含处理会合并部分K线,但内部缓存需要预留空间。如果传入的K线超过这个值,接口会返回错误码-1。chan_feed_kline的返回值表示本次新增的笔数量,可以用来判断是否有新结构产生。
3.3 参数调优与输出解读
源码里有一个配置文件chan_config.ini,可以调整几个关键参数:
| 参数名 | 默认值 | 说明 | 调整建议 |
|---|---|---|---|
| min_bi_kline | 5 | 构成一笔的最少K线数 | 短线策略可降到4,但会引入更多噪音 |
| zs_min_bi | 3 | 构成中枢的最少笔数 | 一般不改,改成2会破坏中枢定义 |
| seg_break_mode | 1 | 线段破坏判定模式 | 1为严格模式,0为宽松模式 |
| merge_direction | 0 | 初始包含处理方向 | 0自动判断,1强制向上,-1强制向下 |
min_bi_kline这个参数影响最大。默认5根K线构成一笔,在日线级别比较合适;如果做30分钟或更小级别,可以降到4,但要注意笔的数量会明显增加,后续中枢的稳定性会下降。我一般会在回测时对比不同参数下的信号胜率,而不是凭感觉调。
输出解读方面,BiData里的start_idx和end_idx是合并后K线的索引,不是原始K线索引。如果需要映射回原始K线,源码里提供了chan_map_index函数做转换。ZhongShuData的level字段表示中枢级别,1为笔中枢,2为线段中枢,3以上为更高级别。做策略时通常只关注笔中枢和线段中枢,更高级别需要传入更多数据才能稳定识别。
4. 避坑指南:缠论dll调用中的五个典型翻车场景
4.1 包含处理后索引错位导致笔划分异常
现象:传入1000根K线,获取到的笔数量明显偏少,且笔的起止位置和图表上肉眼看到的不一致。
原因:包含处理会合并K线,合并后的索引和原始索引不是一一对应。如果调用方直接用合并后索引去原始K线数组里取数据,会取到错误的位置。
解决:用chan_map_index做索引转换,或者直接使用dll返回的high和low值,不要自己去原始数组里查。源码包里有一个index_mapping示例,展示了正确的映射方式。
4.2 增量传入数据时笔的重复计算
现象:分多次调用chan_feed_kline,每次获取的笔列表里前面几笔和上次一样,但最后几笔在反复变化。
原因:增量传入时,最后一笔可能还未确认(因为后续K线可能改变分型结构),dll内部会标记未确认的笔。如果调用方不区分已确认和未确认的笔,就会看到“笔在跳动”。
解决:BiData结构体里有一个confirmed字段,1表示已确认,0表示未确认。策略信号只应基于已确认的笔生成。另外,每次增量传入后,建议只取新增的笔,而不是全量重新处理。
4.3 中枢延伸导致区间不断变化
现象:同一个中枢,随着新K线传入,zg和zd在不停变化,导致基于中枢上下沿的突破信号反复触发。
原因:中枢延伸时,源码会动态调整中枢区间。这是符合缠论定义的,但如果策略逻辑没有考虑这一点,就会出现信号闪烁。
解决:两种方案。一是等中枢确认结束后再使用,ZhongShuData里有一个finished字段,1表示中枢已结束不再延伸;二是用中枢的初始区间(前三笔确定的范围)作为信号基准,忽略后续延伸。我一般用第一种,虽然信号会滞后,但稳定性好很多。
4.4 多品种并发调用时的内存冲突
现象:在同一个进程里对多个品种同时调用dll接口,结果出现数据串扰,A品种的笔出现在B品种的结果里。
原因:dll内部使用了全局状态,chan_init会重置全局数据。如果多个线程或多次调用共享同一个dll实例,就会互相覆盖。
解决:每个品种单独初始化一个实例。源码支持多实例,通过chan_create返回实例句柄,后续所有操作都传入这个句柄。不要用全局的chan_init,那个是单实例模式,只适合单品种场景。
4.5 极端行情下的线段划分死循环
现象:在某些极端行情下(连续一字板或闪崩),调用chan_get_bi或chan_get_zhongshu时程序卡死。
原因:线段特征序列在极端情况下可能出现无法收敛的包含关系,导致内部循环无法退出。这是算法层面的边界问题,不是dll的bug。
解决:源码里有一个max_iteration参数,默认1000,可以在配置文件里调小到100。另外,在传入数据前做一次异常检测,如果连续多根K线的高低点完全相同,先做去重或标记,避免进入死循环。这个坑比较隐蔽,平时跑历史数据不一定遇到,但实盘极端行情下会暴露。
5. 进阶用法:用回调机制做实时笔段更新与信号触发
dll除了提供轮询式的chan_get_bi和chan_get_zhongshu,还支持回调模式。这个用法在实时策略里比较实用——不需要每次主动查询,dll在笔或中枢发生变化时自动触发回调。
// 回调注册示例 void on_bi_update(BiData* new_bi, int count, void* user_data) { // new_bi是本次新增或变化的笔 // count是数量 // user_data是注册时传入的自定义指针 for (int i = 0; i < count; i++) { if (new_bi[i].confirmed == 1) { // 只处理已确认的笔 printf("新笔确认: 方向=%d, 高点=%.2f, 低点=%.2f\n", new_bi[i].direction, new_bi[i].high, new_bi[i].low); } } } void on_zs_update(ZhongShuData* new_zs, int count, void* user_data) { for (int i = 0; i < count; i++) { if (new_zs[i].finished == 1) { printf("中枢结束: 上沿=%.2f, 下沿=%.2f, 级别=%d\n", new_zs[i].zg, new_zs[i].zd, new_zs[i].level); } } } // 注册回调 chan_register_bi_callback(on_bi_update, NULL); chan_register_zs_callback(on_zs_update, NULL);回调模式的关键在于confirmed和finished两个标志位。笔的回调里,只有confirmed=1的笔才应该触发交易信号;中枢的回调里,只有finished=1的中枢才适合做突破判断。如果忽略这两个标志,就会在笔和中枢还在形成过程中就产生信号,实盘里这种信号大部分是噪音。
还有一个实用技巧:回调里不要做耗时操作。dll的回调是在内部计算线程里同步调用的,如果回调函数执行时间过长,会阻塞后续K线的处理。我一般是在回调里只做数据标记,把实际的下单逻辑放到另一个线程里异步处理。
验证回调是否正常工作,可以写一个简单的测试:传入一段已知的历史数据,看回调触发的次数和chan_get_bi返回的笔数量是否一致。如果不一致,大概率是回调注册的时机不对——必须在chan_feed_kline之前注册,否则第一次传入的数据不会触发回调。
从那以后我每次集成新的dll版本,都会先用一段包含趋势、盘整、极端行情的混合数据跑一遍回调验证,确认笔和中枢的触发时机和数量都对得上,再接入实盘逻辑。这个习惯帮我省掉了好几次因为回调时序问题导致的信号错乱。希望帮到你。
本文还有配套的精品资源,点击获取