简介:这套缠论源码包提供基于笔、段、中枢等核心概念的完整算法实现,面向熟悉C语言与微软编译环境的量化开发者及缠论技术分析爱好者。整份压缩包共三十六个文件,大小四点一一兆字节,主体为五个头文件和五个源代码文件,另含编译过程生成的中间文件、项目工程文件以及使用说明文档,可直接在微软开发环境中打开工程查看算法细节并重新编译。已有六千五百六十二人学习浏览。源码完全开源,不仅展示K线笔划分、线段构建与中枢识别的具体逻辑,还附带缠论插件使用说明,便于二次开发时理解参数与接口。开发者可在此基础上扩展行情数据接入、指标展示或交易信号模块,打造个性化的金融分析工具。
1. 缠论dll源码能干什么:把"画笔线段"从看图软件搬进策略引擎
做量化的人碰缠论,早晚会卡在同一道坎上:思路看得懂,笔和线段画得出来,但一说到"让程序自动划分走势",市面上能找到的开源实现不是精度不够就是跑起来像黑匣子。某开发者为了做选股策略,前后试过好几套所谓"缠论源码",最后都因为笔的端点识别经常飘、线段被包含关系反复折腾而放弃,直到拆开这套缠论dll源码,才搞明白一个反直觉的事实——缠论自动画线真正的难度不在中枢,而在最底层的分型确认和包含处理。这套源码不是给你一个画完图的界面,而是一组动态库,把笔、线段、中枢的构造过程封装成可调用的C接口,喂K线数据进去,输出的是规整的笔端点、线段端点和中枢区间。适合两类人:一类是想把缠论信号写进自动交易策略的开发者,另一类是已经在用通达信或同花顺、想把自选股的画笔流程程序化验证的股民兼程序员。接下来直接从代码结构讲起,中间会把最容易画错线的几个环节单独拿出来拆开讲,最后给出二次开发的切入点。
2. 整体架构与数据结构:从K线序列到中枢输出的一层C接口
2.1 代码分层:为什么用dll而不是纯Python
这套源码选择用dll封装,核心原因在于性能。缠论划分笔和线段需要对每根K线反复做包含处理、分型确认和特征序列判断,纯Python在几千只股票全量扫描时,速度会慢到让人怀疑人生。dll方案把计算密集的部分下沉到编译型代码里,外部用Python或公式脚本只负责数据录入和结果展示。常见的项目组织方式是:最外层是行情数据接入与清洗模块,中间层是dll提供的计算内核,最底层是K线和分型的内存结构体。
使用dll还有一层好处:通达信、同花顺这类行情软件允许通过公式接口调用外部动态库,这意味着同一套笔段计算逻辑可以被Python策略和行情软件指标共用,不需要维护两套算法。某开发者的实际项目里,dll被放在一个独立的"algorithm"目录下,对外只暴露了不到十个导出函数,内部却维护了分型识别、包含处理、线段划分和中枢聚合四个相对独立的模块。
2.2 数据结构与对外接口设计
dll的核心数据结构围绕缠论的三个基础概念展开:K线、分型、笔。下面是这套源码里最关键的头文件定义,接手的开发者第一件事应该先读懂这段。
// 缠论基础数据结构定义 typedef struct { float open; float high; float low; float close; float volume; unsigned int time; // K线时间戳,用于内部状态判断 } Bar; typedef struct { int index; // 分型所在K线的索引位置 int type; // 1=顶分型,-1=底分型,0=无效 float value; // 分型极值点价格 int confirmed; // 分型是否已确认,防止右侧K线破坏 } Fractal; typedef struct { int start; // 笔起点K线索引 int end; // 笔终点K线索引 float start_price; float end_price; int direction; // 1=向上笔,-1=向下笔 int valid; // 笔是否有效,包含K线数量是否达标 } Stroke;导出接口方面,常见设计是四步调用链:先初始化上下文,再逐根喂入K线数据,然后调用计算函数产出笔段,最后从缓冲区取结果。实际源码里导出的函数大致是InitContext、FeedBar、BuildStrokes、GetStrokes这几个,参数上需要注意的坑是:喂K线必须按时间顺序逐根处理,一次性灌入数组虽然也有接口支持,但内部会丢失逐根确认的中间状态,导致最后一根K线附近的分型永远无法确认。很多使用者在二次开发时忽略了这个状态设计,直接批量调用,得到的笔段末尾总是差一截。
3. 核心算法拆解:笔、线段、中枢的状态机实现
3.1 包含关系处理:先从K线里去掉"噪音"
缠论画线的第一道工序是处理K线之间的包含关系。所谓包含,就是某一根K线的高点和低点都被相邻K线完全包含,这种情况下两根K线必须合并成一根,否则后续分型识别会被大量假信号干扰。源码在处理这一步时用的是迭代合并法:从第一根K线开始,向后逐个检查相邻K线是否构成包含关系,如果包含则按趋势方向合并。上升趋势中取高高点和高低点,下降趋势中取低低点和低高点。
// K线包含处理,按当前趋势方向合并相邻K线 void MergeBars(Bar* bars, int* count) { int out_idx = 0; int trend = 0; // 0未知,1上升,-1下降 for (int i = 1; i < *count; i++) { Bar cur = bars[i]; Bar prev = bars[out_idx]; // 判断是否包含:前一根完全覆盖当前,或当前完全覆盖前一根 if ((prev.high >= cur.high && prev.low <= cur.low) || (cur.high >= prev.high && cur.low <= prev.low)) { // 趋势向上时合并取高高;趋势向下时取低低 if (trend > 0) { bars[out_idx].high = MAX(prev.high, cur.high); bars[out_idx].low = MAX(prev.low, cur.low); } else if (trend < 0) { bars[out_idx].high = MIN(prev.high, cur.high); bars[out_idx].low = MIN(prev.low, cur.low); } else { // 趋势未知时默认向上处理 bars[out_idx].high = MAX(prev.high, cur.high); bars[out_idx].low = MAX(prev.low, cur.low); } // 合并后需要重新判断当前趋势方向 trend = (bars[out_idx].close >= bars[out_idx].open) ? 1 : -1; } else { out_idx++; bars[out_idx] = cur; } } *count = out_idx + 1; }这段逻辑里最容易被忽略的是趋势方向的维护。合并操作会改变当前K线的最高价和最低价,进而影响趋势判断基准,如果趋势方向没有根据合并结果及时更新,斜向合并时会出现方向粘连错误。实际调试中发现,处理连续多根包含K线时,合并顺序决定了最终得到的K线形态,所以源码中采用的是从前往后的单向扫描,而不是有些人实现的递归合并,后者在极端行情下计算量会指数级增长。
3.2 分型识别:顶底分型的确认机制
分型是笔的端点基础,顶分型由三根K线组成,中间K线的高点最高;底分型则要求中间K线的低点最低。源码里分型的识别并不难,难的是确认——新K线出现后,前一根K线构成的分型完全有可能被突破破坏,所以dll内部维护了一个延迟确认队列:只有在下一根K线的极值不再超越分型顶点时,分型才被标记为有效。这个机制直接影响笔的稳定性,如果不做延迟确认,画出来的笔经常随着新K线一跳再跳。
源码里对这个场景的处理方式是:分型候选先进入一个待确认列表,后续每来一根新K线就检查一次待确认分型是否仍然满足条件,连续三根K线没有破坏该分型,才把状态置为confirmed。需要注意的细节是,确认不是单纯比较后两根K线的最高价,还要看分型本身的类型:顶分型看的是后续K线的高点是否创出新高,底分型则看低点是否创新低,两者判断方向相反。
3.3 笔的构造:从分型到成笔的条件约束
有了确认后的分型,接下来是连接分型构造笔。缠论原文对笔有明确的约束:相邻顶底分型之间必须至少间隔一根独立的K线,而且连接顶底分型的那根K线不能与被连接的分型产生包含关系。源码里笔的构造是一个典型的有限状态机:状态在"等待顶分型"与"等待底分型"之间切换,每次切到新状态时检查上一笔的有效性。
// 笔构造核心逻辑:状态机驱动分型连成笔 void BuildStroke(Fractal* fractals, int fractal_count, Stroke* strokes, int* stroke_count) { int state = 0; // 0=等待底分型,1=等待顶分型 int last_index = -1; float last_price = 0.0f; for (int i = 0; i < fractal_count; i++) { if (fractals[i].confirmed == 0) continue; // 跳过未确认分型 if (state == 0 && fractals[i].type == -1) { // 记录当前底分型为候选笔起点 last_index = fractals[i].index; last_price = fractals[i].value; state = 1; } else if (state == 1 && fractals[i].type == 1) { // 遇到顶分型,检查K线数量是否满足成笔条件 if (fractals[i].index - last_index >= 5) { strokes[*stroke_count].start = last_index; strokes[*stroke_count].end = fractals[i].index; strokes[*stroke_count].start_price = last_price; strokes[*stroke_count].end_price = fractals[i].value; strokes[*stroke_count].direction = 1; strokes[*stroke_count].valid = 1; (*stroke_count)++; state = 0; // 一笔完成,回到等待底分型状态 } // 如果K线间隔不足,则忽略当前顶分型,继续等待 } // 同类型分型出现时,取更优者更新候选点 if (state == 1 && fractals[i].type == -1 && last_index != -1) { if (fractals[i].value < last_price) { last_index = fractals[i].index; last_price = fractals[i].value; } } if (state == 0 && fractals[i].type == 1 && last_index != -1) { if (fractals[i].value > last_price) { last_index = fractals[i].index; last_price = fractals[i].value; } } } }这个状态机设计里有个细节值得细看:当已经记录了一个底分型作为笔起点,后续又出现新的底分型且价格更低时,源码会更新起点到更优位置,而不是机械地使用第一个底分型。这个"取优"逻辑保证了笔的端点总是当前趋势中的极值点。但如果K线间隔不满足5根就反向切换状态,则说明当前笔不成立,需要把状态回退重新寻找起点,实际代码里用连续变量的方式实现了这个回退,比显式状态回退更简洁但更难读。
3.4 线段划分与中枢聚合:确认后的重头戏
笔构造完成后的线段划分是另一个复杂度高地。线段由至少三笔构成,且必须满足特征序列的条件——向上线段中,向下笔的低点不能跌破前一个向下笔的高点,即特征序列没有缺口;一旦出现缺口,线段就宣告结束。源码里线段划分的实现在笔序列上做二次扫描,每一笔作为特征序列的一个元素,用栈结构来判断特征序列的包含与分型。
中枢的构造则比较直观:在一个线段或走势中,至少需要连续三段次级别走势重叠,重叠区间的最高低价区间就是中枢区间。源码计算中枢时从笔序列里取连续三笔,计算它们在价格上的重叠区域。这里的典型bug是把三笔的高点取最大值、低点取最小值,这实际上是整个区间的包络,不是重叠区。正确做法是取三笔中最低的顶和最高的底构成重叠区间,区间上下沿分别是三段走势高低点的交集。
4. 常见问题与避坑:笔端飘移、除权跳空、状态残留
4.1 喂入数据没除权,线段的起点直接飞了
现象:跑某只股票的历史数据时,线段起点在除权日附近出现巨大跳空,笔的端点直接被拉到错误价位,后续所有中枢区间全部错位。 原因:dll内部处理K线只认高开低收的价格数值,不感知除权事件。除权日开盘价瞬间下跌或跳空高开,从数值上看就像一根巨大振幅的K线,包含处理和分型确认都会被这根"假K线"干扰。 解决:喂数据前先做前复权处理。某开发者的做法是在Python端用复权因子统一校正所有历史K线,确保同一只股票的价格序列连续。如果策略必须做实时判断,也应单独维护一个复权逻辑在dll外处理,不要把复权塞进dll内部,否则会让计算层依赖外部数据源的复权规则。
4.2 行情软件里画出假笔:公式脚本调用dll的状态残留
现象:在通达信里通过公式调用dll时,每根新K线触发一次调用,结果发现笔的端点每隔几根K线就跳一次,甚至在同一根K线上画出互相矛盾的笔。 原因:dll被设计为"逐根喂入"的流式处理接口,每次调用FeedBar后内部状态前进一次。但行情软件公式的调用方式不是这样——它会在每一根新K线上重新执行整个公式,如果每次都重新初始化dll上下文,那所有历史状态全部丢失;如果不重新初始化,dll内部状态又会被重复计算,产生叠加的脏数据。 解决:给dll加一个"批量计算模式",在公式脚本里一次性把所有历史K线传入,只取最后一次计算结果。另外还可以在dll导出接口里增加一个快照函数,允许外部把内部状态序列化保存,公式每次调用时先尝试加载快照。对比来看,直接走Python调用dll的批处理模式最省心,行情软件公式集成适合经验丰富的开发者尝试。
4.3 连续涨停板行情下分型大量失效
现象:某只股票连续涨停时,K线几乎都是小实体大振幅,分型识别居然连续跳过了多个明显的顶分型,笔的数量远少于手工画图的结果。 原因:连续涨停时每根K线的最高价都在刷新,新K线会不断破坏之前的分型,使得分型迟迟无法确认。源码里的确认机制要求后续K线不超越分型极值,而单边行情里这个条件被持续违反,分型确认失败是正确行为,但肉眼看上去却像漏画了笔。 解决:检查dll中分型确认的窗口长度参数。窗口是固定的连续K线数量,阈值太小会导致单边行情里分型无法确认,调大窗口又会引入滞后。实测下来5根窗口对A股日线行情比较合适,如果是15分钟或1分钟级别,需要按波动速度重新调参。
4.4 中框区间算出来是空的:边界条件未处理
现象:部分股票的三笔重叠区间计算结果显示high < low,中枢区间为空,程序直接崩溃。 原因:三段笔的价格区间可能完全不重叠,例如连续三段都是向下的笔,重叠区间本身就为空,这在中枢尚未形成的走势里是正常情况。 解决:源码里需要加边界检查——如果在聚合区间没有出现任何重叠,中枢状态置为"未形成",让上层策略跳过而不是报错。某开发者在这个位置吃过亏,最初直接把空区间传给排序函数,结果数组越界导致dll崩溃,排查了整整一下午才发现是边界分支没走。
5. 编译与接入:dll怎么调、数据怎么喂、结果怎么校验
5.1 环境准备与编译参数
拿到源码后第一步是编译dll。以Windows + Visual Studio为例,需要新建一个动态链接库项目,把源码目录下的chanlun.c、stroke.c、segment.c和merge.c几个模块加入编译。这里有一个常见的坑:因为源码内部用了多线程并行处理多只股票,编译时必须开启/MT静态链接运行时库,否则在目标机器上会出现缺少VCRUNTIME140.dll的报错。如果目标平台是Python调用,推荐编译成64位Release版本,配合Python 3.8以上的64位解释器使用,避免常见的位数不匹配问题。
编译完成后,需要一个最小的C调用示例来验证dll导出接口是否正常。建议在集成进策略之前,先写一个命令行工具,读入CSV格式的K线数据,输出笔和线段的关键点,人工对照行情软件确认结果正确后再做上层对接。
// 最小测试程序:读取CSV并调用dll接口 #include <stdio.h> #include "chanlun.h" int main() { ChanlunCtx* ctx = InitContext(5, 1); // 参数1:最小笔K线数,参数2:分型确认窗口 FILE* fp = fopen("stock_daily.csv", "r"); if (!fp) return -1; Bar bar; while (fscanf(fp, "%f,%f,%f,%f,%f,%u", &bar.open, &bar.high, &bar.low, &bar.close, &bar.volume, &bar.time) == 6) { FeedBar(ctx, &bar); } fclose(fp); int stroke_count = 0; Stroke* strokes = GetStrokes(ctx, &stroke_count); for (int i = 0; i < stroke_count; i++) { printf("Stroke[%d]: start=%d end=%d price=%.2f -> %.2f\n", i, strokes[i].start, strokes[i].end, strokes[i].start_price, strokes[i].end_price); } FreeContext(ctx); return 0; }这段测试代码里的两个初始化参数值得注意。min_stroke_bars控制一笔最少需要的K线数量,缠论原文标准是5,实际操作中有人用3到4来适配更短的走势周期,但这会让笔的数量显著增加、干扰信号变多。confirm_window控制分型确认窗口大小,即分型出现后连续多少根K线不破坏才确认有效。这两个参数是整个dll里最重要的调参对象,改任何一处都会直接影响所有下游的输出。
5.2 Python调用与结果校验
Python中调用这套dll最直接的方式是ctypes。需要先声明与C结构体对应的Python类,因为Bar结构体包含float和unsigned int混合字段,字段顺序和字节对齐必须与C头文件完全一致,大多数对齐问题都出在这里。某开发者的踩坑记录里有一条:Python端声明time字段时用了signed int,结果dll读入的时间戳出现负数,整个线段划分全乱套,排查了很久才发现是类型不匹配。
import ctypes class Bar(ctypes.Structure): _fields_ = [ ("open", ctypes.c_float), ("high", ctypes.c_float), ("low", ctypes.c_float), ("close", ctypes.c_float), ("volume", ctypes.c_float), ("time", ctypes.c_uint32) ] class Stroke(ctypes.Structure): _fields_ = [ ("start", ctypes.c_int), ("end", ctypes.c_int), ("start_price", ctypes.c_float), ("end_price", ctypes.c_float), ("direction", ctypes.c_int), ("valid", ctypes.c_int) ] chanlun = ctypes.CDLL("./chanlun.dll") chanlun.InitContext.restype = ctypes.c_void_p chanlun.GetStrokes.restype = ctypes.POINTER(Stroke)校验阶段强烈建议用人工标注好的样本数据做回归测试,而不是直接用真实行情。某开发者的做法是:手工在通达信上画出50只股票近半年的笔和线段,把端点索引导出为JSON,再用Python调dll跑同样的数据,比对两端点是否一致。这种做法能快速暴露dll内部状态机的边界问题,例如在横盘震荡行情里,笔的数量和手工画图结果经常有出入——有时候是源代码的取优逻辑和手工判断标准不一致,有时候是K线合并后索引偏移导致端点差了一两根。对比结果不匹配时先检查索引偏移,再检查参数差异,这比直接改算法高效得多。
6. 二次开发思路:背驰计算、参数自适应与策略接入
拿到一套能跑通的缠论dll,二次开发的第一个价值点是把"笔段中枢"升级为"背驰信号"。背驰判断需要依赖MACD面积或者均线斜率,而这个计算完全不涉及缠论本身,只需要在dll输出的线段或笔端点基础上,额外读取对应K线区间的副图指标数据。常见的做法是增加一个CalculateDivergence导出函数,接收笔序列和对应的MACD数据数组,输出每笔的动能面积,再对比相邻两笔的动能衰减情况。某开发者在这个基础上做了跨级别联立,用日线确认趋势方向,用30分钟线找背驰点,最终策略的胜率比单级别判断明显改善。
第二个推荐改造点是参数自适应。源码里的min_stroke_bars和confirm_window是静态参数,实际跑不同股票时最优值差异很大——波动剧烈的次新股需要更大窗口过滤噪音,而银行股这类低波动的票用标准参数会漏掉很多短线机会。可以写一个参数寻优模块,用历史数据的分笔结果和人工标注做对比,自动搜索每只股票的最优参数组合,逻辑是:先用默认参数跑一遍,然后在小范围内调整两个参数,计算分时段的线段重合度,选出重合度最高的参数作为该股票的专属配置。虽然这引入了过拟合风险,但作为辅助工具的价值远大于风险。
# 背驰判断的Python示例:对比两段相邻笔的动能 def check_divergence(strokes, macd_sums): # 计算每一笔的MACD动能总和 energies = [] for s in strokes: seg_sum = sum(macd_sums[s.start:s.end+1]) energies.append(seg_sum) # 动能衰减且价格新高 -> 顶背驰 if len(energies) >= 2 and energies[-1] < energies[-2]: return True return False最后说一个从这次拆解里沉淀下来的习惯。某开发者过去集成第三方算法库时,总喜欢直接在策略代码里调用,出了问题再回头查,结果经常分不清是策略逻辑写错了还是底层算法有bug。从那以后他每次都强制走一遍"黑盒校验"流程——先把dll对历史数据的输出固化下来,人工抽查三个不同行情的样本,再进策略回测。这套流程虽然花时间,但省掉的排查成本远大于此,尤其是缠论这种状态机密集、对输入顺序敏感的算法——数据喂错顺序、参数没校准、状态残留,任何一个坑都比想象中更深。希望这套思路对你也有用,落地的过程中少走些弯路。
本文还有配套的精品资源,点击获取