相似K线形态匹配:从原理到Python量化选股实战
2026/9/16 6:16:27 网站建设 项目流程

做量化这些年,我最大的感受是:技术指标写起来容易,真正难的是给指标一个“可信的逻辑”,让它在历史数据里站得住脚。均线金叉、MACD背离这些经典套路,用的人太多,参数被反复优化,近几年的有效性衰减得很厉害。后来我开始把精力放到一个相对冷门的方向——相似K线。思路很简单:不去预测未来,而是去历史里翻答案。当前这只股票的K线形态,过去到底出现过多少次?出现之后一般都怎么走?这篇文章就以同花顺Supermind为环境,把相似K线的形态验证和选股从原理到代码完整过一遍,给想做技术面分析专题的朋友一个可以直接上手的参考。内容涉及Python量化交易策略代码、形态匹配算法、回测验证和选股落地,属于技术面分析的第一个专题,后续我会继续拆解其他因子方向。

1. 为什么相似K线能用于选股——先搞清楚逻辑再写代码

很多人一上来就问我代码怎么写,我的建议是反过来,先把逻辑想清楚。相似K线不是某个具体指标,而是一套“以史为鉴”的形态匹配方法。它背后的假设、适用场景和失效边界,直接决定了策略能不能长期跑下去。

1.1 相似K线到底在寻找什么

相似K线的核心思想,是从全市场的历史行情里,找到与当前股票最近一段走势“长得像”的历史片段,然后统计这些片段之后的涨跌表现。如果历史相似形态出现后上涨的概率显著大于下跌,那么当前这只股票就具备入选条件。

这里说的“长得像”,不是简单地看几根阳线几根阴线,而是把一段K线的走势轨迹当作一条曲线,用数学方法计算曲线之间的形状相似度。比如一只股票最近30个交易日的收盘价走势,是一条先横盘、再缩量回踩、然后放量突破的曲线。我们就去历史数据里找同样符合“横盘—回踩—突破”节奏的片段,看看它们之后5天、10天、20天分别怎么走。

为什么这种方法值得研究?行为金融学里有一个解释:市场上的参与者面对相似的图形,会产生相似的认知和操作反应。比如“放量突破平台”这个形态,很多人会认为它是启动信号,于是真金白银买入,这种集体行为反过来又强化了形态的有效性。换句话说,相似K线策略捕捉的是市场参与者的一致性行为惯性,而不是什么玄学。

另一个角度是筹码结构。两段走势相似,往往意味着背后的筹码分布、成本结构也有一定相似性。比如都经历了长期横盘换手,浮筹被清洗干净,一旦启动,上方抛压会比较小。这种筹码层面的逻辑,用传统的单点指标(比如某一天的RSI、某一天的布林带位置)根本表达不出来,但形态匹配可以。

1.2 这个逻辑什么时候会失效

我必须先泼一盆冷水:相似K线不是圣杯,它的逻辑成立有几个前提条件。

第一条,历史样本要足够多。A股市场有效的历史数据不过二三十年,如果模板取120天,全市场切片数量看似很多,但真正“极端相似”的样本可能只有十几个,统计意义很弱。所以模板长度不宜过长,一般20到40个交易日是一个比较均衡的区间。

第二条,市场环境会变。注册制之前和之后,市场生态完全不同;2015年之前小市值因子横行,2017年之后白马股抱团,2021年之后又是量化主导的存量博弈。同一段K线形态,在不同市场环境下出现后的走势可能有本质区别。所以做相似K线验证时,一定要按时间段切分来看,不能把十几年的数据混在一起一锅炖。

第三条,也是最重要的:相似K线解决的是“择时”和“选股”的一部分问题,它无法规避系统性风险。股灾、流动性危机这种极端行情下,再漂亮的形态也会被泥沙俱下地砸穿。所以实盘策略必须叠加市场状态过滤,比如大盘指数在20日线下方时,暂停选股或者降低仓位。

想清楚这三条,你再看接下来的代码和回测,就不会被表面的高胜率冲昏头脑。

2. K线相似度的数学定义——把“看着像”变成“算得出”

“像不像”是主观感受,量化交易必须把它变成可计算的数值。这一步是整个专题的核心,定义方式直接决定匹配效果。

2.1 数据标准化:不同价位的股票怎么放一起比

贵州茅台2000块和农业银行3块钱,收盘价曲线直接算距离没有任何意义,绝对价格差异会完全淹没形态差异。所以第一步是标准化。

我实践中推荐用“首日基准法”,而不是常见的min-max归一化。原因很简单:min-max归一化对极值敏感,两段完全不同的形态,只要最高点和最低点相同,归一化后可能长得很像,产生大量误匹配。首日基准法把每一天的收盘价除以第一天的收盘价再减1,得到的就是“相对于起点涨跌幅序列”。

比如模板第一天收盘10元,第二天10.5元,第三天9.8元,标准化后就是0%、5%、-2%。这样不同价位的股票就站在同一个尺度上了。同理可以处理开盘价、最高价、最低价,但我在实践里发现,收盘价序列的效果最稳定,因为收盘价是所有交易者博弈后的最终结果,噪声相对小。

2.2 三种相似度度量方法对比

标准化之后,需要一个数值来衡量两条曲线的接近程度。我实际用过三类方法,各有优劣:

方法计算思路优点缺点适用场景
欧氏距离逐日计算涨跌幅差值的平方和,再开方直观、计算快、对整体形状敏感对“相位偏移”敏感,早一天晚一天启动会被判为不相似形态稳定、节奏同步的匹配
皮尔逊相关系数计算两条序列的线性相关程度对整体趋势方向敏感,不关心绝对幅度只能衡量线性关系,且对波动幅度不敏感,上涨1%和上涨5%可能算出高相关性判断趋势方向是否一致
形状距离对序列做一阶差分,再计算差分序列的欧氏距离更关注“走势节奏”而非“绝对位置”对噪声更敏感,需要先平滑节奏型形态(如先横盘后突破)的匹配

我自己的经验是:单独用任何一种都容易出问题。欧氏距离把幅度和形状混在一起,相关系数忽略了幅度,形状距离容易被单日噪声干扰。最稳定的做法是用加权综合打分——形状距离占50%,欧氏距离占30%,相关系数占20%,三个分数分别排序后取加权排名。

2.3 规范化K线相似度计算代码

下面给出一套在Supermind上可以直接用的相似度计算函数,我用的是平台内置的Python环境,Pandas和NumPy都是自带的。这个函数直接传两个list进来就行,返回一个0到1之间的相似度,越小越像。

import numpy as np import pandas as pd def normalize_series(series): """ 首日基准标准化 把绝对价格序列转换为相对第一天涨跌幅的序列 """ arr = np.asarray(series, dtype=np.float64) if arr[0] == 0: return np.zeros_like(arr) return (arr - arr[0]) / arr[0] def compute_similarity(hist_series, target_series): """ 计算两段K线序列的综合相似度 返回0到1之间的值,越小越相似 """ # 统一长度 n = min(len(hist_series), len(target_series)) hist = normalize_series(hist_series[-n:]) target = normalize_series(target_series[-n:]) # 1. 欧氏距离 euclidean_dist = np.sqrt(np.sum((hist - target) ** 2)) # 2. 相关系数距离 corr = np.corrcoef(hist, target)[0, 1] if np.isnan(corr): corr = 0.0 corr_dist = 1 - abs(corr) # 3. 形状距离(对一阶差分做归一化再算欧氏距离) hist_diff = np.diff(hist) target_diff = np.diff(target) shape_dist = np.sqrt(np.sum((hist_diff - target_diff) ** 2)) # 综合打分:形状距离权重最高,因为它最贴近“走势节奏” # 将三个距离映射到0~1区间后再加权 # 这里的缩放系数是根据经验设置的,可以根据实际数据调整 score = 0.5 * (shape_dist / (n + 1e-9)) + \ 0.3 * (euclidean_dist / (n + 1e-9)) + \ 0.2 * corr_dist return min(score, 1.0)

这段代码有几个细节值得注意。

第一,np.corrcoef在两条序列完全一样时才能算出1.0,但实际行情序列几乎不可能完全一样,所以很少出现除零。但序列方差为0时相关系数会变成NaN,比如股票连续一字涨停,整个模板期内没有任何波动。这种极端情况需要用np.isnan兜底,否则会污染整个排序结果。

第二,形状距离为什么权重最高?因为技术分析的形态,本质上说的是“节奏”。先横盘三天再突破,和直接连涨三天,欧氏距离可能差不多,但意义完全不同。形状距离通过一阶差分把“先横盘”和“直接涨”区分开了。

第三,缩放系数里的n是模板长度。长度越长,累计距离越大,所以除以n做一个平均化处理,让不同模板长度下的得分可以互相比较。这个细节我在第一次写的时候漏掉了,导致30天模板和60天模板的分数完全不在一个量级上,还排查了半天。

3. 形态验证回测——怎么证明“相似形态之后真的会涨”

有了相似度计算函数,下一步就是验证:找到的相似形态,历史上出现之后到底涨不涨。这一步不能省,不验证就直接上选股,本质上就是在赌博。

3.1 回测框架怎么设计

形态验证回测和普通策略回测有一个重要区别:普通回测验证的是一个规则在时间序列上连续运行的效果,形态验证要做的则是“扫描历史、切片对比、统计后续表现”。

我设计的框架分四步:

  1. 确定模板片段。可以来自你关注的某只股票最近N天的实际走势,也可以人为设定一个通用形态(比如连续5天小阳线+放量突破)。
  2. 对全市场或某个股票池的历史数据做滑动窗口扫描。窗口长度与模板长度一致,每次滑1天,计算每个窗口与模板的相似度。
  3. 过滤掉相似度不够高的窗口,保留下来的就是“历史相似形态”。
  4. 统计这些相似形态出现后,未来5天/10天/20天的收益分布,计算胜率、平均收益、最大亏损等指标。

这里有一个统计陷阱必须提醒:滑动窗口扫描会产生大量重叠样本。比如第100天和第101天的窗口,有29天是重叠的,两者对应的是几乎相同的形态,后续收益也高度相关。如果把它们当成两个独立样本来统计,会严重高估样本量,导致胜率的置信度虚高。解决办法是:在筛选相似形态时做“非极大值抑制”,也就是两个相似形态出现的时间间隔小于模板长度的一半时,只保留相似度最高且后续收益最稳定那一个。

3.2 验证结果怎么解读

回测跑完之后,不要只看一个平均收益率就下结论,至少要拆解下面几个维度:

指标作用我的判断标准
样本数量确认统计意义少于30个样本的结果直接弃用
胜率形态出现后上涨的比例至少高于55%,且连续多年稳定
平均收益率期望收益跑赢同期基准3%以上才考虑实盘
最大单次亏损尾部风险超过8%需要警惕,说明形态有“踩雷”概率
年度分布逻辑是否逐年有效如果只有某一年特别赚钱,大概率是风格巧合

还要做一个关键对比:相似形态之后的收益,和“股票池全样本”之后的平均收益,差值才是这个形态真正贡献的超额收益。如果相似形态之后平均涨了5%,但全样本平均也涨了5%,那这个形态没有任何信息量,只是跟随大盘而已。

3.3 一套可直接运行的形态验证代码

下面这段代码,可以在Supermind上跑通。我用的是平台自带的HistoryData接口获取行情,如果你用别的数据源,替换成对应接口即可。

def validate_similar_pattern(stock_code, template_end_date, lookback=30, hold_days=10, threshold=0.15): """ 验证某只股票当前形态与历史相似形态的后续表现 stock_code: 股票代码 template_end_date: 模板结束日期,通常取最新交易日 lookback: 模板长度 hold_days: 持有天数 threshold: 相似度阈值,低于该值视为相似 """ hist = get_price(stock_code, start_date='2015-01-01', end_date=template_end_date, fields=['close'], frequency='1d') if hist is None or len(hist) < lookback + 50: return None close_arr = hist['close'].values template = close_arr[-lookback:] match_indices = [] # 滑动窗口扫描,注意留出持有期空间,避免用到未来数据 for i in range(0, len(close_arr) - lookback - hold_days): window = close_arr[i:i + lookback] sim = compute_similarity(window, template) if sim < threshold: match_indices.append(i) # 非极大值抑制:剔除间隔过近的重复样本 filtered = [] min_gap = int(lookback / 2) for idx in sorted(match_indices): if not filtered or idx - filtered[-1] >= min_gap: filtered.append(idx) if len(filtered) < 10: return { 'sample_count': len(filtered), 'warning': '样本数量不足10个,统计结果仅供参考' } # 统计未来收益 future_rets = [] for idx in filtered: entry_price = close_arr[idx + lookback - 1] exit_price = close_arr[idx + lookback - 1 + hold_days] future_rets.append(exit_price / entry_price - 1.0) future_rets = np.array(future_rets) win_rate = np.mean(future_rets > 0) return { 'sample_count': len(filtered), 'win_rate': round(win_rate, 4), 'avg_return': round(np.mean(future_rets), 4), 'max_loss': round(np.min(future_rets), 4), 'median_return': round(np.median(future_rets), 4) }

这个函数返回的字典,直接打印就能看到形态的历史表现。我在实际使用中会把template_end_date设为最新交易日,把stock_code替换成关注的股票池,批量跑一遍,就能知道当前市场环境下哪些形态还活着、哪些形态已经死了。

这里要特别强调一点:模板取的是包含最后一日的完整形态,扫描窗口的i循环结束条件是len(close_arr) - lookback - hold_days,目的就是确保每个匹配样本都有完整的持有观察期。否则你匹配到了最近几天的相似形态,还没来得及验证后续走势,等于用未来数据骗自己。

4. 从验证到实盘选股——把相似K线跑成一个完整的选股策略

验证通过只是第一步,真正落到选股上还需要一套工程化的流程。这里说的工程化,不是为了炫技,而是为了把人为干扰降到最低,让策略在实盘中稳定执行。

4.1 全市场选股的核心流程

我设计的实盘选股流程分为五个环节:

  1. 每日收盘后拉取全市场日线数据。在Supermind上就是用get_price批量获取,注意这里是全市场几千只股票,数据量比较大,建议只拉收盘价和成交量两个字段,把内存占用控制住。
  2. 统一形态模板。模板K线怎么定?两种思路。一种是用某只你自己关注的“明星股票”当前形态作为模板,寻找同形态的其他股票;另一种是先手动指定一个“上涨前兆”的通用形态模板,比如25天横盘+5天缩量回踩+放量启动。我更推荐第二种,因为它的逻辑更清晰,也更容易解释。
  3. 计算全市场每只股票最近N日K线与模板的相似度。直接复用compute_similarity函数。
  4. 对相似度排序,取前10到20只作为候选池
  5. 叠加硬过滤条件,剔除不能买、不敢买的股票。

这里的核心是第五步,很多新手容易忽略。候选池里很可能混着ST股、停牌股、次新股,它们的K线数据不完整或者交易规则特殊,直接买会出各种幺蛾子。

4.2 硬过滤条件清单

以下过滤条件我用的是“有一票否决权”的规则,命中任何一条就剔除,不管相似度多高:

  • 剔除ST、*ST,这类股票涨跌幅限制和流动性都和正常股票不同,形态参考意义有限。
  • 剔除上市不足120个交易日的次新股,K线样本太少,而且次新股走势受筹码解锁、炒作情绪影响极大。
  • 剔除停牌中的股票,停牌期间K线是平的,会被误判为“横盘企稳”形态。
  • 剔除当日涨停且封单量很大的股票,因为次日买入通常买不进,需要等回调,但回调后形态就变了。
  • 剔除日均成交额低于1亿的股票,流动性不足,你的买入冲击成本会直接吃掉形态带来的超额收益。

这些条件可以用一段简单的Pandas代码批量处理。在Supermind上,ST标记、上市日期、成交额这些字段都可以通过平台的数据接口拿到。我把这段代码写成函数,每天收盘后一键执行。

def filter_candidates(candidates_df, price_df, trade_days=120, min_amount=100000000): """ 对相似K线排序后的候选池做硬过滤 candidates_df: 包含股票代码、相似度分数的DataFrame price_df: 用于计算上市天数、停牌状态、成交额的数据 """ valid = candidates_df.copy() # 剔除上市不足120天 valid = valid[valid['list_days'] >= trade_days] # 剔除ST valid = valid[~valid['name'].str.contains('ST', na=False)] # 剔除停牌(最近5日成交量都为0视为停牌) valid = valid[valid['recent_5d_volume'].sum(axis=1) > 0] # 剔除日均成交额过低 valid = valid[valid['avg_amount_5d'] > min_amount] # 按相似度排序,取前N只 valid = valid.sort_values('similarity').head(10) return valid

这个函数是我实际策略里的简化版。真实场景里我还会加入一个“距离涨停板位置”的过滤——如果股票已经涨了7个点以上,再追进去的赔率已经不划算了,相似度再高也要放弃。这个参数因人而异,我做短线的话阈值设在5%。

4.3 参数调优和过拟合防范

每次讲到参数,我都得强调一遍过拟合这个问题。相似K线策略的参数包括模板长度、相似度阈值、持有天数、候选池大小。这四个参数一旦组合起来,光是全市场历史回测就能跑出几百种结果,你想找到一个“历史最优参数”太容易了——但那个参数大概率是过拟合的产物。

我的做法是“样本外验证”:把历史数据分成三段,2015到2018年做第一轮参数筛选,2019到2021年做验证,2022年之后只做最终确认,不再调整任何参数。如果一个参数组合在前两段都表现稳定,在第三段也没有明显失效,我才会考虑小仓位实盘。这个方法笨,但可靠,能筛掉大部分“回测明星、实盘拉胯”的假策略。

另外还要警惕参数敏感度。如果一个策略把模板长度从30改成31,年化收益就从30%变成5%,那这个策略基本不可用,纯粹是在拟合噪音。真正稳定的策略,参数小幅扰动时收益曲线应该是一条平滑的抛物线,而不是一座尖锐的山峰。

5. 在Supermind上实测踩坑记录——这些细节常规文档不会写

最后这部分是我个人在Supermind上做相似K线专题时踩过的一些坑,每一条都真实发生过,分享出来帮大家省时间。

5.1 复权方式不统一,K线直接断裂

第一个坑就是除权除息。股票分红送转之后,如果不复权,K线上会出现一个向下的跳空缺口,但这个缺口不是市场行为,而是财务操作造成的假象。如果你用不复权数据计算相似度,一只走势平稳的股票在除权日会被误判成一个巨大的下跌形态,既冤枉又危险。

我习惯统一用前复权数据。但需要注意,前复权数据在不同时间点拉取,最新复权因子会变化,导致历史价格被不断重新计算。如果你在2023年10月拉了一次历史数据,又在2024年5月拉了一次,同一段历史的收盘价可能对不上。所以策略里所有历史数据必须一次性拉取、落库保存,不要每次回测都重新从接口取数,否则回测结果根本不可复现。

5.2 涨跌停板导致“买不到”和“卖不出”

技术面形态匹配最容易匹配到刚启动的股票,这类股票往往已经涨停或者接近涨停。涨停板上有巨大的封单,普通小资金根本买不进。回测里算出来的收益再漂亮,实盘买不进去等于零。

我处理这个问题的方法:在候选池里加入“当日涨幅”过滤,涨幅超过5%的股票一律不追。同时,回测时设置一个可成交的约束——如果选股日股票涨停,直接用第二天的开盘价买入,用这个更保守的价格来测试策略的真实收益。这个方法会牺牲一部分收益,但换来了实盘可执行性,值得。

5.3 全市场扫描的性能瓶颈

如果说策略逻辑是大脑,那计算性能就是腿。相似K线全市场扫描,模板长度30天,全市场5000只股票,每只要做几千次滑动窗口匹配,计算量非常可观。我最开始在Supermind上跑全市场版本,一次完整扫描要十几分钟,完全没法做盘中决策。

优化思路有三条:一是向量化计算,把单股票的循环改成NumPy矩阵运算,速度能提升几十倍;二是预计算特征,把每段K线的一阶差分、归一化值提前算好存下来,相似度计算时直接复用,避免重复计算;三是缩小扫描范围,先用一个简单的波动率过滤把大量走势平淡、不可能匹配的股票排除掉,只对剩下的候选做精确匹配。三条优化做完,全市场扫描时间压到了1分钟以内,已经完全够用。

5.4 “看起来像”不等于“本质相同”——形态以外的信息不能丢

这是我在做这个专题时最深刻的体会。两段K线可能走势曲线高度重合,但背后的基本面、行业逻辑完全不同。一只股票的相似形态出现在业绩预增公告之后,另一只出现在大股东减持公告之后,后续走势能一样吗?

所以我现在会把相似K线当作“第一层筛选器”,而不指望它独立完成整个策略。选出来的候选股,我会再叠加一层逻辑过滤:中报或年报有没有商誉暴雷风险、近期有没有限售股解禁、所属行业当前是不是热点方向。形态给出的是“路径概率”,基本面排除的是“尾部风险”,两者结合才算一个完整的可交易信号。

5.5 幸存者偏差会比你想的更隐蔽

最后说一个数据层面的坑。做全市场回测时,如果用当前日期还在上市的股票池作为样本,那么那些已经退市的股票天然被排除在外。但历史上存在大量退市股票,它们在退市前往往走出一段瀑布式下跌,这种形态恰恰是对“相似之后会涨”结论最有力的反向证据。把这些股票漏掉,你的胜率会被系统性高估。

解决方法是:回测时必须使用“历史时点成分股”,确保每一段历史的股票池都是当时真实存在的股票,而不是今天的股票池。具体到Supermind,需要拉取历史上市状态表,把退市股也纳入相似度扫描样本。我第一次回测时没做这一步,胜率虚高了将近8个百分点,教训惨痛。

这个专题到这里算是一个相对完整的闭环了:从相似K线的定义,到验证方法,到实盘选股流程,再到数据层面的避坑。做相似K线给我最大的一个体会是,量化策略卖的不是高深的模型,而是纪律性和对细节的敬畏。把每一个数据坑填平、把每一个统计陷阱避开,策略的稳定性自然就出来了。下一期我打算接着写技术面分析专题里的另一个方向——成交量异动与资金行为识别,那个方向和我今天讲的相似K线组合起来用,效果会更有意思。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询