资讯动态

缠论程序化实现:从K线包含到笔、中枢、背驰的量化回测

发布时间:2026/10/13 8:30:22 来源:尧图企业网站定制
简介缠论是A股市场颇具争议的技术分析体系这份资源以Python将缠论中的K线包含处理、顶底分型识别与画笔规则做成可运行脚本适合对量化交易和缠论自动化的Python开发者参考。项目采用清晰的分层布局源码、文档与配置相互分离已实现K线包含处理、找顶底、划笔三个核心模块同时标出线段划分、均线选股、均线轮动和板块强弱指标等后续扩展方向便于读者追踪缠论程序化的完整路径。压缩包共33个文件以21个Python脚本为主体辅以少量XML配置、TXT说明与Markdown文档整体约25KB轻量易读可直接对照缠论原文梳理代码逻辑。已有898人学习下载适合有一定Python基础、希望将手工画图流程转译为代码的股票研究者也可作为进一步实现每日走势分类30分钟8根K线等功能的起点。1. ChZhShCh 缠论程序化先把“笔”和“中枢”写成代码解决的是什么问题ChZhShCh 这套实践核心是给缠论画出一条可计算的边界把K线包含处理、分型、笔、中枢和背驰判定这些能明确写成规则的部分做成可回测的程序线段和走势类型这类解释空间大的环节先留给人工复核。手工画图和程序画图的差别在实操里很明显。同一段行情有人把某一笔拆成三段有人合并成一段最后对中枢位置的判断完全不同。程序化的价值不是让机器替你做决策而是把“哪一根K线被合并、哪一个分型先确认”这些过程固定下来让每次计算可复现、可回测、可复盘。这套实践适合做量化开发入门、技术分析信号验证以及想在CTA类策略里引入结构判定的从业者前提是接受“部分缠论”这个提法不追求完整还原一套理论只追求能复现的规则先落地。2. 缠论程序化从哪一步开始K线包含处理与分型判定的数据基础2.1 用高低点合并把包含K线归一化方向判定是第一道坑K线包含处理是缠论程序化里最容易写错、也最影响后续结果的一步。两根K线一根的高点更高、低点也更低或者一根的高点更低、低点更高这就是包含关系。包含关系的处理规则本身不复杂向上趋势里取两根K线中较高的高点和较高的低点向下趋势里取较低的低点和较低的高点。麻烦在于“向上还是向下”不是看当前这两根K线而是看最近一次非包含K线给出的方向。很多翻车案例都是因为直接用当前K线相对上一根K线的高低价差判方向在连续包含时方向会反复横跳。我一般会这样写合并逻辑def merge_bars(bars): bars: [(high, low), ...]按时间升序排列返回包含处理后的K线列表 if not bars: return [] merged [] direction 1 # 1 表示向上-1 表示向下初始值只占位 for i, (high, low) in enumerate(bars): if not merged: merged.append([high, low]) continue last merged[-1] # 判断当前K线是否与最后一根处理后的K线存在包含 if (high last[1] and low last[0]) or (high last[1] and low last[0]): if direction 1: merged[-1] [max(last[0], high), max(last[1], low)] else: merged[-1] [min(last[0], high), min(last[1], low)] else: merged.append([high, low]) # 方向只由最近一次非包含K线决定 if high last[1] and low last[0]: direction 1 elif high last[1] and low last[0]: direction -1 return [tuple(x) for x in merged]这里的核心不是合并动作本身而是direction的更新时机。只有当前K线与最后一根处理后的K线不包含时才更新方向只要发生包含方向就沿用上一次的值。这样连续包含时不会因为某根小实体K线而改变合并方向。这段代码里merged[-1]同时承担两个角色既是被比较的基准K线也是合并结果的容器。所以合并时直接原地修改merged[-1]不需要额外维护一个“上一根”变量。参数上没有额外入参但要注意输入顺序必须严格时间升序如果数据源返回的是降序先bars[::-1]再进函数。2.2 顶底分型与成笔判定把图形语言变成离散序列包含处理之后下一步是找分型。顶分型是中间K线的高点最高、低点也最高底分型是中间K线的低点最低、高点也最低。这里最关键的是严格不等式如果出现高点相等或低点相等这个分型就不成立。放宽成会让分型数量明显变多后续用这些分型构建的笔也会频繁跳动。def find_fractals(kline): kline: 包含处理后的 [(high, low), ...] fractals [] for i in range(1, len(kline) - 1): p_high, p_low kline[i - 1] c_high, c_low kline[i] n_high, n_low kline[i 1] # 顶分型中间高点最高且中间低点也最高 if c_high p_high and c_high n_high and c_low p_low and c_low n_low: fractals.append({type: top, index: i, price: c_high}) # 底分型中间低点最低且中间高点也最低 elif c_low p_low and c_low n_low and c_high p_high and c_high n_high: fractals.append({type: bottom, index: i, price: c_low}) return fractals得到分型序列后还不能直接把它们连成笔。缠论的笔要求顶底分型之间必须有足够多的独立K线而且顶底必须严格交替。如果两个分型同类型比如连续两个顶分型一般保留价格更高的那个作为候选顶再往下找底。def build_bi(fractals, min_bars4): 在分型序列上构建笔min_bars 是两个分型之间的最少独立K线数 bi [] candidate None for f in fractals: if candidate is None: candidate f continue # 间距不够不构成笔 if f[index] - candidate[index] min_bars: continue # 同类型分型保留更极端的一个 if f[type] candidate[type]: if (f[type] top and f[price] candidate[price]) or \ (f[type] bottom and f[price] candidate[price]): candidate f continue # 顶底交替且价格创新高/新低才形成一笔 if candidate[type] bottom and f[type] top and f[price] candidate[price]: bi.append((candidate, f)) candidate f elif candidate[type] top and f[type] bottom and f[price] candidate[price]: bi.append((candidate, f)) candidate f return bimin_bars4是我在日线上常用的取值意思是两个分型中间至少隔 4 根独立K线。这个值不是理论原文规定的原文只要求顶底分型之间至少有一根独立K线。取值越小笔越密信号越多但噪声越大取值越大笔越稀中枢和背驰信号都变少。换成小时线或分钟线min_bars要重新扫不能沿用日线的经验值。这段代码里还有一个容易被忽略的细节当顶底交替但价格没有创新高或新低时当前分型会被丢弃candidate保持不变。比如底分型之后出现一个更低的顶分型但它的价格没有超过前一个底分型的低点那这个顶不合法继续保留原有底分型等下一个顶。这个取舍直接影响后续中枢的起点位置。2.3 线段与走势类型先砍掉为什么“部分缠论”是一种工程选择完整缠论里笔之上还有线段线段之上还有走势类型。线段由至少三笔构成但线段终结需要处理特征序列特征序列又依赖缺口和分型的包含关系。这一层规则不是不能写而是不同人对特征序列的理解经常不一致。同一个行情一个人画三笔一段另一个人画五笔一段线段端点差出好几个K线位置。程序化最怕这种“同输入不同输出”的规则一旦写进去回测结果没法归因。所以在 ChZhShCh 的落地路径里我明确把线段和走势类型划到人工复核范围。程序只负责输出笔序列、中枢区间和背驰信号使用者可以拿着这些中间结果在图上复盘再决定要不要相信信号。这样做换来的是每一层计算都可追溯包含处理错了查包含分型错了查分型不会出现一个黑匣子式的“走势类型判定”把问题吞掉。3. 中枢与背驰的量化判定从连续三笔重叠到MACD面积衰减3.1 中枢重叠区间这样求才不会把进入段算进去有了笔序列之后中枢判定就变成一个几何问题连续三笔的重叠区间。具体说取三笔中最小的“高点”作为中枢上沿最大的“低点”作为中枢下沿只有上沿大于下沿时才形成中枢。这里最常见的错误是把进入段也算进重叠导致中枢区间被拉宽上下沿失真。def zhongshu_from_bi(bi_list, min_overlap_bars3): bi_list: [(start_price, end_price), ...]每笔用端点价格表示 zones [] for i in range(len(bi_list) - min_overlap_bars 1): window bi_list[i:i min_overlap_bars] # 进入段不参与重叠从窗口第二笔开始取区间 highs [max(pair) for pair in window[1:]] lows [min(pair) for pair in window[1:]] zg min(highs) # 中枢上沿 zd max(lows) # 中枢下沿 if zg zd: zones.append((zd, zg)) return zonesmin_overlap_bars3表示至少三笔参与重叠。缠论里中枢通常由至少三笔重叠构成所以这个参数就是中枢的最低成立条件。如果取 4中枢会更严格但很多行情根本凑不出四笔重叠信号数量会急剧下降。注意window[1:]这一步。三笔构成一个中枢时第一笔是进入段后两笔与中枢内部的笔重叠。如果直接对window全部取min(highs)和max(lows)进入段的极端价格会污染中枢边界。正确做法是把窗口第二笔到第三笔当作中枢重叠区间的主体进入段只负责连接上一个中枢或确认趋势方向。这里还有一个边界问题同一段行情可能连续出现多个重叠区间比如五笔里任意三笔都重叠会算出多个中枢。实际落地时我通常取最后一个被完整确认的中枢作为当前信号参照前面的中枢归入历史状态。这个“只取最后一个”的规则要写死在代码里否则同一时刻可能出现两个互相矛盾的中枢信号。3.2 背驰判定用价格速度与MACD面积的“衰减”代替人工比长度背驰是缠论里最依赖主观经验的部分。手工看背驰是看两段同向走势的长度和力度对比比如下跌的进入段和离开段离开段走得没进入段急或者MACD绿柱面积明显比进入段小就叫背驰。程序化以后我把“没进入段急”翻译成一个可比较的数值面积衰减或斜率衰减。def check_chi(enter_area, leave_area, modearea): 比较进入段与离开段的MACD力度返回是否有背驰 if mode area: # 面积模式离开段MACD柱面积绝对值小于进入段 return abs(leave_area) abs(enter_area) # 斜率模式除以区间长度比较单位时间内的力度 return (leave_area / leave_len) (enter_area / enter_len)enter_area是进入段覆盖的MACD柱面积之和leave_area是离开段覆盖的MACD柱面积之和。面积模式直接比较两者绝对值斜率模式需要把面积除以各自的K线数量消除“离开段更长所以面积更大”的干扰。我只推荐在同一个中枢的前后两段之间比较背驰不要跨中枢比较。很多人把背驰写成全局比较结果拿两段相隔很远的走势硬比面积程序跑出来信号一堆复盘时每一笔都对不上。缠论的背驰语义天然带位置约束必须和某个中枢绑定。MACD参数我一般用 (12, 26, 9)但这不是缠论自己规定的参数只是技术指标库的默认值。换一个品种、换一个周期(12, 26, 9) 算出来的面积衰减未必稳定。背驰判定里真正有价值的不是MACD本身而是“进入段和离开段的力度衰减”这个相对关系MACD只是承载这个关系的工具。3.3 从K线到信号输出一个可调用的迷你流程到这里所有组件都齐了。把它们串成一个函数输入原始K线输出方向信号或“无信号”。def signal_from_kline(bars): bars: [(high, low, close), ...]返回 BUY / SELL / NO_SIGNAL # 1. 包含处理只保留高低点 kline [(h, l) for h, l, c in bars] merged merge_bars(kline) # 2. 分型与笔 fractals find_fractals(merged) bi build_bi(fractals) if len(bi) 4: return NO_SIGNAL # 3. 找最后一个中枢 zones zhongshu_from_bi(bi) if not zones: return NO_SIGNAL # 4. 背驰比较倒数第三笔作为进入段最后一笔作为离开段 enter_price bi[-3] leave_price bi[-1] enter_area macd_area(enter_price) # 这里使用外部实现的MACD面积计算 leave_area macd_area(leave_price) if check_chi(enter_area, leave_area): if leave_price[1] leave_price[0]: return BUY return SELL return NO_SIGNAL这个流程的输入输出很清楚原始K线进信号标签出。中间的macd_area需要自己实现常见做法是先算DIF和DEA然后对柱状值按K线窗口求和。这里有几个约定必须统一bi[-3]是进入段bi[-1]是离开段中间bi[-2]参与中枢内部结构。信号只在收盘后生成不能把当根K线的盘中最高最低算进当根信号的判定里。这个迷你函数已经可以跑通一个简单的信号回测。接下来要解决的是参数设定和数据口径问题这两件事决定了信号在历史数据上是否可信。4. 信号回测与参数设定缠论判定的数据准备和统计口径4.1 回测数据的最低字段与复权处理缠论判定只需要价格结构但回测收益需要真实成交价。数据字段最低要求是时间、开盘价、最高价、最低价、收盘价成交量是可选项。最高价和最低价用于包含处理和分型判定开盘价用于模拟次日成交收盘价用于信号生成和收益计算。字段用途说明time时间索引按周期结束时间排序信号不能跨周期漂移open次日成交近似信号生成在当日收盘后第二天开盘价进场high包含处理分型判定依赖高低点low包含处理分型判定依赖高低点close信号生成与收益计算收盘价必须真实不能用盘中实时价volume可选过滤用于剔除流动性低的K线复权问题比很多人想的严重。股票数据如果不复权除权除息日会留下一个巨大的价格缺口这个缺口会直接变成一笔虚假的笔中枢位置被整体拉偏。前复权数据可以解决这个问题但前复权会随着时间推移不断更新历史价格同一个历史K线在不同日期拉取可能得到不同的数值。我一般会把回测用的复权数据固化成一份本地快照避免每次回测拉到的历史价格都不一样。4.2 信号生成与回测统计把信号写在收盘后用次日开盘价成交缠论信号天然适合收盘后生成。因为分型和笔的确认需要完整K线当根K线没收盘时最高价和最低价还在变化基于它们算出的分型随时可能被推翻。正确的做法是第 N 根K线收盘后计算信号第 N1 根K线开盘价成交。def run_backtest(df): df 必须含 signal 列signal 在当日收盘后生成成交在次日开盘 position 0 equity [] for i in range(1, len(df)): prev df.iloc[i - 1] row df.iloc[i] # 用前一日的信号在今日开盘价成交 if prev[signal] BUY and position 0: position 1 entry_price row[open] elif prev[signal] SELL and position 1: position 0 # 每日收益持仓时跟随涨跌空仓时为零 daily_ret row[close] / prev[close] - 1.0 if equity: equity.append(equity[-1] * (1.0 position * daily_ret)) else: equity.append(1.0 * (1.0 position * daily_ret)) return equity注意循环里取的是prev[signal]不是row[signal]。这行代码直接决定了回测有没有引入未来函数。很多新手在这里顺手写成row[signal]等于用当天收盘后算出的信号去成交当天的开盘价收益被系统性高估回测曲线漂亮得不像话实盘一跑就露馅。回测统计不能只看胜率。缠论信号频次低一个品种日线一年可能才十几次完整信号胜率偶然性太大。我一般会同时看三组数信号次数、平均盈亏比、最大回撤。胜率低于40%但盈亏比大于2的策略仍然可做相反胜率70%但单笔亏损吃掉前面六笔盈利的策略反而危险。4.3 三个必调参数成笔最少K线、中枢重叠段数、MACD参数窗口缠论程序化真正需要反复调的参数不多但每一个都直接改变信号结构。参数作用域调整方式min_bars成笔最少独立K线数日线默认 4改大信号变少变稳改小信号变多变噪min_overlap_bars中枢至少重叠笔数3 是最低要求4 更严格但信号稀缺macd_windowMACD快慢线与信号窗口常见 (12, 26, 9)换品种必须重新扫min_bars是最敏感的参数。它从 4 改成 5看起来只是多过滤一根K线实际效果可能让整段中枢消失因为笔的端点位置变了。调这个参数时不要盯着胜率找最优值而是找参数平台的中间值把同一段数据用 3、4、5、6 分别跑一遍如果只有某个值表现特别好、旁边两个值都很差这往往是过拟合不是真实规律。MACD参数窗口通常不碰 (12, 26, 9)但背驰比较的是面积相对大小参数变化会影响面积数值。我的习惯是先固定min_bars和min_overlap_bars单独扫 MACD 参数扫完再看 MACD 面积衰减在不同窗口下是否稳定。如果换一个窗口背驰结论就从有变无说明这个信号本身不扎实。5. 缠论程序化的避坑清单未来函数、K线合并顺序与中枢区间歧义5.1 同一天数据昨天和今天画出的笔不一样未来函数在作怪现象同一个品种、同一段K线昨天程序画出一笔从 A 到 B今天再跑同一段数据这笔突然变成了 A 到 CB 点消失。用手工画图的人也有类似体验但程序化里这个现象尤其隐蔽。原因分型和笔的确认依赖后续K线。顶分型在第三根K线出现时只能算是“候选”需要后面再走几根K线才能确认它没有被破坏。如果你的程序每次都用全量最新数据重算所有历史笔那么新K线的出现可能改变旧分型的有效性导致历史笔被重画。解决固定数据窗口做快照重算或者只重算最后一个未确认的分型。我现在的做法是信号只在K线收盘后计算一次比如 15 分钟周期只在每根K线收盘时跑一次signal_from_kline盘中不重算。回测时更严格统一用end_time截断数据保证每一根K线使用的信息量不超过它当时能看到的范围。5.2 连续包含之后高低点方向反了合并顺序错误现象出现连续三四根包含关系的K线时合并后的K线高低点方向明显不对连外行看图都觉得这笔画反了。比如明明是下跌趋势中的包含合并后高点反而抬高了。原因合并方向依赖最近一次非包含K线的涨跌方向。如果代码里用当前K线相对上一根K线的方向来决定合并在连续包含时会反复翻转方向最终按错误方向做了高低点合并。解决在merge_bars里把方向更新放在非包含分支中只有不包含时才用高低价差刷新方向包含分支只沿用已有方向。这个逻辑我在 2.1 的代码里已经体现但实现时很容易被“看起来更直观”的写法替代。验证方法很简单找一段明显的连续包含行情手工画一遍合并结果再和程序输出对比。5.3 中枢区间出现上下沿颠倒或跨越全图中枢语义没统一现象程序输出的中枢上下沿区间异常要么zg zd被当作有效中枢要么中枢区间宽到覆盖了整段行情回测里挂在图上一看就离谱。原因中枢的重叠区间语义不清。有人用窗口内所有笔的高低点求重叠把进入段也包含进去有人用窗口内全部K线求最高最低而不是第三笔到第N笔的重叠还有人把“三笔重叠”理解成“三根K线重叠”完全偏离了笔中枢的结构。解决统一用“进入段不参与重叠从窗口第二笔开始取min(highs)和max(lows)”的公式见 3.1 的zhongshu_from_bi。同时在中枢函数里加一道校验zg zd不成立就丢弃不作为有效中枢。我习惯在输出里保留zg和zd两个字段复盘时直接看数值是否合理而不是只给一个信号。5.4 历史回测胜率虚高实盘却失效信号太少与参数过拟合现象某开发者拿着同一套参数在历史数据上跑出漂亮的胜率换到最近一段实盘数据上连续止损最后怀疑整个缠论程序化方向不可行。原因缠论信号天生低频日线上一个品种一年可能只有十几笔完整信号。十几笔样本里挑出表现最好的参数组合等于在噪声里画靶子。回测出的最优参数换一个时间段往往完全失效。解决先看信号次数信号少于 30 次的结果一律不做参考。调参时不要找胜率峰值而是找参数“平台”把参数从 3 到 7 逐个测试如果连续多个值的结果差异不大说明这个方向有稳定性如果只有某一个值特别好旁边马上变差直接放弃。还可以把数据切成前后两段前段调参、后段验证避免用同一段数据既调参又评价。5.5 换一个周期信号全变了笔的过滤没有跨周期校验现象日线跑出 BUY 信号换到 30 分钟线却连一个中枢都没画出来或者 30 分钟线信号频繁日线上完全没动静。两个周期的结论互相矛盾不知道该信哪个。原因不同周期的K线噪声结构不同。日线一根K线包含的信息密度远大于分钟线同样的min_bars4在日线上过滤掉了大量小波动在分钟线上却仍然放进了很多短笔。笔的端点位置变了中枢区间随之改变背驰信号自然对不上。解决周期切换时不能只换K线数据、不换参数。要么对不同周期单独扫描min_bars要么引入多周期同构校验只有小周期信号与大周期最近一笔方向一致时才保留信号。这个校验在第 6 章会给出具体做法它是缠论程序化里最实用的过滤手段之一。6. 多周期同构过滤缠论信号验证里最值得先做的一个技巧多周期同构的意思是日线发出的 BUY 信号必须得到周线上最近一笔向上走势的确认如果周线最近一笔还在向下日线的买点先扣住不放。这个技巧能过滤掉大量“小周期结构到位、大周期方向相反”的翻车信号。def filter_by_sync(day_signal, week_signal): day_signal 是日线信号week_signal 是周线最近一笔的方向 if day_signal in (BUY, SELL) and day_signal week_signal: return day_signal return HOLD实现上有个关键点周线信号也必须用收盘后确认的笔不能用未完成笔。我一般把周线信号计算放在周五收盘后的数据快照上和日线信号分开跑之后再做比对。参数建议取值说明周期对日线 周线长期趋势过滤用周线短线交易可换成 30 分钟 日线同构规则大周期最近一笔方向与信号一致不一致时输出 HOLD不强行反向确认延迟大周期笔收盘后 1 根K线避免使用尚未走完的当周K线这个技巧还有个附加好处它强迫你把自己的中间结果暴露出来。每次信号被过滤掉你都能在图上看到是周线哪一笔挡住了它而不是面对一个只输出买卖点的黑匣子。我现在做缠论程序化无论信号在参数上多漂亮都会先把周线笔画出来看一眼日线的买点和周线一笔方向相反就直接过滤同向才进入候选池。这个习惯帮我挡掉了不少在回测里看起来很美、实盘里却很别扭的信号。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑