作 者:老余捞鱼
原创不易,转载请标明出处及原作者。

文中示例仅用于技术讨论,不构成任何操作建议。
量化策略开发应以学习和技术交流为目的。
本号不荐股、不卖课、不承诺收益。
市场有风险,请合法合规投资。
本文约 5000 字 | 预计阅读 8-12 分钟,后有实例代码,建议先收藏。
· · ·
我复盘时有个习惯:先把当天的涨停梯队扫一遍,再去翻自选股的分钟图。翻得多了,我发现一个挺有意思的规律:那些真正让我赚到钱的票,在进场前,往往都有一段不太起眼的横盘。
不是V型反转,也不是火箭发射。就是十几个交易日里,价格在一条很窄的走廊里来回晃,量能一点点缩下去,新闻没了,群里也没人聊它。
直到某一天,一根放量的大阳线或者大阴线把走廊的门撞开,行情就来了。
老手把这段叫盘整,或者说是行情在憋一口气。
麻烦的是,这段平静光靠眼睛看,很吃亏。你看它横了三天,它可能还有二十天要横;你看它像是要突破,结果那根阳线第二天又缩回去了。这种差一点点的假突破,市场里一抓一大把。
今天我分享一套现成的方法,能把盘整变成数字,把突破变成规则。接下来我会用Python把它在A股整个复现了一遍,两个标的换成中证500和宁德时代。
先说清楚它是什么、不是什么。

它只管两件事:第一,把“盘整”变成一个可以量出来的数字;第二,盯住价格冲出走廊的那一刻,替你把信号记下来。
它不管未来。冲出去之后能走多远,它不知道。后面我会用五个信号的真实表现给你看,比空口说更有说服力。
一、我测出来的结果,先给答案
先上数字。
中证500,往前取 9 个月。 脚本自动选中的参数是窗口 10 天、阈值 6.0%,一次命中。9 个月里量出 4 个盘整区,每一个都走出了信号。
| 盘整区 | 天数 | 信号 | 信号价 | 后5日 | 后20日 |
|---|---|---|---|---|---|
| 02-13 ~ 02-24 | 11 | 向上突破 | 8527.56 | -3.3% | -8.9% |
| 04-03 ~ 04-09 | 6 | 向上突破 | 8088.57 | +2.2% | +5.5% |
| 04-27 ~ 05-06 | 9 | 向上突破 | 8696.80 | -0.3% | -3.9% |
| 09-02 ~ 09-10 | 8 | 向下突破 | 7571.20 | 样本不足 | 两天后收回 |

宁德时代,往前取 3 个月。 这次网格里没有参数组合命中目标,脚本走了兜底路径,用窗口 10 天、阈值 5.0% 找到 1 个区,随后等来 1 个向下突破信号。
| 盘整区 | 天数 | 信号 | 信号价 | 后5日 | 后10日 |
|---|---|---|---|---|---|
| 08-20 ~ 08-24 | 4 | 向下突破 | 376.73 | -4.9% | -10.9% |

刚跑完的时候,我还是有小鸡冻的。宁德那个向下突破信号,8 月 25 日 376 块离场,之后十天跌了 10.9%,一路跌到 305 附近。这个信号事后看,准得让人想立刻把它挂到实盘上去。
但我把五个信号全部对了一遍账,心气就降下来了。
五个信号对完账,站住的是两个。中证500 那三个向上突破的信号,两个在 20 个交易日后是亏的,02-25 那个买完 20 天亏 8.9%。还有一个 09-14 的向下突破信号,就在前两天刚触发,结果两个交易日后价格又收回区间里了,现在看更像一次假跌破。
四个宝箱,只有两个半装了宝。
账要一笔一笔算。这两个半是怎么装了宝的,才是这篇真正想讲的东西。
二、怎么装,怎么跑
整套东西装起来只要三步,全程不花钱,数据源是免费的。
第一步装两个库
① 装依赖,一行搞定
pip install akshare pandas matplotlibakshare 负责数据,pandas 负责计算,matplotlib 负责画图。装完就齐了。
第二步把标的两行换掉
脚本里标的和区间集中放在一个配置里,你只要改这两行。
② 标的和区间,集中改这两行
# 指数用 sh 加代码,个股用 sz 加代码,个股建议加前复权
CASES = [
{"key": "csi500", "label": "中证500", "kind": "index", "symbol": "sh000905", "start": "2025-12-17"},
{"key": "catl", "label": "宁德时代", "kind": "stock", "symbol": "sz300750", "start": "2026-06-17"},
]第三步跑,然后看图
③ 跑起来就一条命令
python repro_consolidation.py跑完控制台会打出找到几个盘整区、每个信号的日期和价格,同时在目录里生成 K 线图。红涨绿跌,盘子用浅蓝底色框出来,信号用三角标在当天的价格上。
这一步的关键是数据。个股一定要用前复权,不然除权除息那天会凭空出现一个大缺口,脚本会把那个缺口当成天大的波动,盘整区全乱。
三、装好之后怎么用
把它当成一个盘前扫描器用。
每天早上花一分钟,把你自选股里的票挨个换进配置里跑一遍。有新形成的盘整区,它会告诉你上下沿在哪。已经有信号触发的,它会告诉你哪天触发、当时价格多少。参数不用你管,脚本自己找。
唯一的纪律是别拿它当买卖指令。它标记的是「价格离开了一段压缩区间」这个事实,不是「接下来会涨」这个预言。这句话你先记住,第七节我会拿真实数据给你看它错起来的样子。
以上是使用部分,照着做就能跑。往下是动手部分,讲这套规则里的三个关键设计,还有五个信号一笔一笔的对账。不关心代码的话,看第一节的表格和第七节的对账,也能把结论带走。
四、盘整两个字,是怎么变成一个数字的
先说这套方法最核心的一步。
每天收盘后,脚本回看最近N个交易日,找出这段时间里的最高点和最低点。然后算一个比值,最高点减最低点,除以最低点。这个比值就是这段行情的振幅。
举个具体的例子。最近 15 天里,价格最高摸到过 11 块,最低到过 10 块,那振幅就是 1 除以 10,等于 10%。反过来,如果这 15 天价格一直在 10 块到 10 块 2 之间晃,振幅就只有 2%。
数字一出来,事情就好办了。预先设一个阈值,比如 6%,振幅低于它的日子,就记一笔「今天在盘整」。连续的盘整日串起来,只要跨度超过 3 天,就成了一个盘整区。区的最高价就是天花板,最低价就是地板。

这张图你看后是不是感觉:最近走势收敛了?其实就是曲线往下扎进红线的下方,图变成了可以核查的东西,这是整套方法里我觉得最值钱的一步。
核心逻辑就这几行。
④ 振幅怎么算,就这四行
df['rolling_high'] = df['High'].rolling(window=window).max()
df['rolling_low'] = df['Low'].rolling(window=window).min()
df['volatility'] = (df['rolling_high'] - df['rolling_low']) / df['rolling_low']
df['in_consolidation'] = df['volatility'] < threshold四行代码,第一行找窗口内最高,第二行找最低,第三行算振幅,第四行跟阈值比大小。之后把连续的 True 串成区,跨度超过 3 天的留下来。
不过这里藏着一个我跑的时候才注意到的细节。代码判跨度用的是日历天,不是交易日。宁德那个 08-20 到 08-24 的区,日历上 4 天,擦线过了「大于 3 天」的门槛,摊开一看其实只有 3 个交易日。这种迷你区算不算盘整,我的看法是算,但你要知道它是这么算的,心里有数。
中证500 那边还有一个更夸张的。第一个区标着 02-13 到 02-24,日历上 11 天,中间隔着整个春节假期,真正开盘的只有 2 天。这种假期里诞生的区,说是盘整有点勉强,而四个信号里 20 日亏得最狠的那笔,恰好就是它发出来的。日历天这个口径,用之前得自己掂量。
五、参数是脚本自己试出来的
这里有个绕不开的麻烦。窗口取多少天,阈值定多少,没有一个对所有票都好使的答案。
同样的 6% 阈值,放在大盘指数上可能找出十几个区,放到一只波动大的票上可能一个都找不出来。手动调参调到吐,换个票又得从头再来。
这套做法的答案是让脚本自己试。窗口在 10、15、20、25 天里挑,阈值在 2.5% 到 6% 里挑,一共 20 种组合,挨个跑。目标是找出 2 到 7 个盘整区,太少说明工具没起作用,太多说明把噪音都框进来了。
⑤ 20 种组合,挨个试
window_options = [10, 15, 20, 25]
threshold_options = [0.025, 0.03, 0.04, 0.05, 0.06]
target_zone_count = (2, 7)
for window in window_options:
for threshold in threshold_options:
zones = identify_sideways_movement(data, window, threshold)
if target_zone_count[0] <= len(zones) <= target_zone_count[1]:
return window, threshold, zones我这次跑的两个标的,正好把两条路径都演示了一遍。
中证500 是顺的。窗口 10 天加阈值 6.0% 这组参数,一次就找出 4 个区,落在 2 到 7 的目标区间里,直接返回。
宁德时代是怂的。3 个月里把 20 种组合全部试了一遍,没有一组落在目标区间里。这时候脚本不会硬凑,它会退一步,用「至少能找出一个区」的最宽标准兜底,最后停在窗口 10 天、阈值 5.0%,找到 1 个区。
我当时跑完这个输出,对这套设计的评价就一句话。它知道自己在什么时候该认怂,而不是硬给你编一个好看的结果出来。
但有层窗户纸我得捅破。这 20 种组合是拿整段历史试出来的,试的时候,每个区在哪它都已经看在眼里,参数本质上是事后挑的。这不算作弊,工具本来就是帮你定规则的,可你心里要有数,过一个月再跑同一只票,选中的参数可能就换了一组。我的用法是把它当定期重跑的扫描器,跟着最新行情重新挑参数,而不是设定一次就扔在一边不管。
六、突破怎么才算数
找到盘整区只是前半件事。真正值钱的在后半段,价格什么时候离开这个区,往哪边离开。
规则写得很死。收盘价冲过区间的天花板,再多走 0.5%,才算一次向上突破。收盘价跌破地板,再多走 0.5%,才算一次向下突破。每个区只记第一个信号,后面的不算,免得同一个区反复打扰你。
⑥ 突破确认,多走 0.5% 再算数
if close > zone_high * (1 + confirm_buffer) and not signal_triggered:
signals.append({"date": idx, "signal": "BUY", "price": close})
elif close < zone_low * (1 - confirm_buffer) and not signal_triggered:
signals.append({"date": idx, "signal": "SELL", "price": close})那 0.5% 是干嘛的?它其实是给假突破设的一道门。
没有这道门,收盘价高出天花板一分钱,信号就响了。而市场里最不缺的,就是那种探出头一天、第二天就缩回去的假动作。多走 0.5%,等于要求价格拿出一点真金白银的姿态,而不是蹭一下就跑。

这次实跑还撞上了一个教科书级的案例。中证500 那个区,地板是 7610.58,乘上 0.995 之后触发线是 7572.53。9 月 14 日那天收盘 7571.20,比触发线只低了 1.33 个点。就差这一点点,向下突破信号响了。
更有意思的在后面。两个交易日后,9 月 16 日,指数收盘 7683,不但收回了地板,还站回了地板上方 1%。我在屏幕前看着这个信号,心情很复杂。缓冲挡得住“差一点点”的假动作,挡不住“过两天再骗你”的假动作。
七、五个信号的事后账
现在回到第二节欠的那笔账。这些信号,事后到底走成什么样。
我把每个信号触发日的收盘价记下来,往后数 5 个、10 个、20 个交易日,算涨跌。
对账的口径也交代清楚。信号靠收盘价触发,收盘之前它出不来。真要照着做,你得等收盘确认信号,第二天再动手,中间还隔着隔夜跳空和手续费。下面的账全部从信号当天收盘价起算,没占便宜,但也没扣跳空和手续费,你看的时候心里得有这杆秤。

一笔一笔过。
站住的两个。中证500 04-14 的向上突破信号,触发价 8088.57,20 个交易日后 +5.5%,中途还摸过更高。宁德 08-25 的向下突破更漂亮,触发价 376.73,10 个交易日后 -10.9%,方向一路没回头。
亏的两个。02-25 的向上突破,追在 8527,20 天后 -8.9%,买完就跌。05-07 的向上突破,追在 8696,20 天后 -3.9%。这两个都是标准的「突破追进去,然后区间见顶」。
失效的一个。09-14 的向下突破,刚触发两天,价格就收回区间,按规则它已经不会再发第二个信号,这个信号等于白响。
数一下,2 个站住,2 个亏,1 个白响。
所以我才说四个箱子打开两个半。这个成绩单放在任何策略评审会上都不算难看,但它清楚地告诉你,突破信号的价值不在每一次都对,而在它把「追涨杀跌」这个情绪动作,换成了一个可核查、可复盘、可统计的规则动作。单次对错是运气,样本攒多了才轮到规则说话。
还有一个必须老实交代的事。5 个信号,样本就是这么点,再乘以任何年化收益都是耍流氓。你想验证它,就得像我这样换标的、换区间多跑几轮,让样本自己爬出来。
八、它不猜未来
写到这里,把话收拢一下。
这套东西猜不了行情。它做的是三件更老实的事。把盘整从感觉变成一个每天可以核查的数字。把突破从一个瞬间的冲动,变成一条要连续满足两个条件才触发的规则。把每一次触发都记下日期、价格和当时区间的上下沿,让你事后能一笔一笔对账。
第三件事最不显眼,也最值钱。大部分散户的复盘,停在当时我感觉要突破了。它给你的复盘材料是这样的,9 月 14 日 7571 触发向下突破,区间 7610 到 7850,两个交易日后失效。感觉没法改进,数字才能。
最后留两个问题给你。
你自选股里那几只横了很久的票,振幅是多少,离 6% 还差多远,这个今天收盘就能算。还有,上一次你追一个突破被套的时候,那个价格离区间上沿有多远,够不够 0.5%。
完整脚本在下面,装好库就能跑。想要哪个标的,改配置里的两行就行。
⑦ 完整脚本,装好库直接跑
# -*- coding: utf-8 -*-
"""
盘整区检测与突破信号(A 股版)
策略逻辑复现自 The Quiet Before the Move
数据源 akshare(新浪),指数 sh 前缀,个股 sz 前缀 + 前复权
运行:python repro_consolidation.py
"""
import os
import json
import akshare as ak
import pandas as pd
from datetime import datetime
CASES = [
{"key": "csi500", "label": "中证500", "kind": "index", "symbol": "sh000905", "start": "2025-12-17"},
{"key": "catl", "label": "宁德时代", "kind": "stock", "symbol": "sz300750", "start": "2026-06-17"},
]
END = datetime.now().strftime("%Y-%m-%d")
CONFIRM_BUFFER = 0.005 # 突破确认缓冲 0.5%
def get_clean_financial_data(kind, symbol, start_date, end_date):
"""指数用 stock_zh_index_daily,个股用 stock_zh_a_daily 前复权"""
if kind == "index":
raw = ak.stock_zh_index_daily(symbol=symbol)
else:
raw = ak.stock_zh_a_daily(symbol=symbol, adjust="qfq")
raw["date"] = pd.to_datetime(raw["date"])
df = raw[["date", "open", "high", "low", "close"]].copy()
df.columns = ["Date", "Open", "High", "Low", "Close"]
df = df.set_index("Date").ffill()
return df.loc[start_date:end_date]
def identify_sideways_movement(data, window, threshold):
"""滚动窗口算振幅,低于阈值的日子连成盘整区(跨度需超过 3 个日历天)"""
zones = []
if len(data) < window:
return zones
df = data.copy()
df['rolling_high'] = df['High'].rolling(window=window).max()
df['rolling_low'] = df['Low'].rolling(window=window).min()
df['volatility'] = (df['rolling_high'] - df['rolling_low']) / df['rolling_low']
df['in_consolidation'] = df['volatility'] < threshold
in_zone, zone_start = False, None
for i in range(len(df)):
if df['in_consolidation'].iloc[i] and not in_zone:
in_zone, zone_start = True, df.index[i]
elif not df['in_consolidation'].iloc[i] and in_zone:
in_zone = False
zone_end = df.index[i - 1]
if (zone_end - zone_start).days > 3:
zones.append({"start": zone_start, "end": zone_end})
if in_zone and (df.index[-1] - zone_start).days > 3:
zones.append({"start": zone_start, "end": df.index[-1]})
return zones
def detect_breakout_signals(data, zones, confirm_buffer=CONFIRM_BUFFER):
"""收盘突破区高 +0.5% 记向上突破,跌破区低 -0.5% 记向下突破,每区只记第一个"""
signals = []
for period in zones:
if period["start"] == period["end"]:
continue
zone_data = data.loc[period["start"]:period["end"]]
zone_high = zone_data['High'].max()
zone_low = zone_data['Low'].min()
after_zone = data.iloc[data.index.get_loc(period["end"]):]
triggered = False
for idx, row in after_zone.iterrows():
close = row['Close']
if close > zone_high * (1 + confirm_buffer) and not triggered:
signals.append({
"date": str(idx.date()),
"signal": "BUY",
"price": round(close, 2),
"zone_high": round(zone_high, 2),
"zone_low": round(zone_low, 2),
})
triggered = True
elif close < zone_low * (1 - confirm_buffer) and not triggered:
signals.append({
"date": str(idx.date()),
"signal": "SELL",
"price": round(close, 2),
"zone_high": round(zone_high, 2),
"zone_low": round(zone_low, 2),
})
triggered = True
return pd.DataFrame(signals)
def find_optimal_parameters(data):
"""网格搜索窗口与阈值,目标 2~7 个区;找不到就退回「至少有一个区」的兜底"""
window_options = [10, 15, 20, 25]
threshold_options = [0.025, 0.03, 0.04, 0.05, 0.06]
target = (2, 7)
for window in window_options:
for threshold in threshold_options:
zones = identify_sideways_movement(data, window, threshold)
if target[0] <= len(zones) <= target[1]:
print(f"参数命中 -> Window={window}, Threshold={threshold:.1%}")
return window, threshold, zones, "hit"
for window in window_options:
for threshold in threshold_options:
zones = identify_sideways_movement(data, window, threshold)
if zones:
print(f"目标未命中,兜底 -> Window={window}, Threshold={threshold:.1%}")
return window, threshold, zones, "fallback"
return None, None, [], "none"
for c in CASES:
print("=" * 50)
print(f"{c['label']} {c['start']} ~ {END}")
df = get_clean_financial_data(c["kind"], c["symbol"], c["start"], END)
w, t, zones, mode = find_optimal_parameters(df)
sigs = detect_breakout_signals(df, zones)
print(f"盘整区 {len(zones)} 个,信号 {len(sigs)} 个")
for _, s in sigs.iterrows():
unit = "点" if c["kind"] == "index" else "元"
print(f" {s['date']} | {s['signal']} @ {s['price']}{unit} "
f"(区 {s['zone_low']} ~ {s['zone_high']})")跑完之后,把控制台的输出和图对照着看。想扫自选股,把 CASES 里的两行换掉就行。数据这块多说一句,指数接口走新浪,个股接口同样走新浪、记得带前复权。要是新浪接口哪天限流,换成券商或者通达信的本地数据也一样,只要喂进来的四列名字对上,后面一行代码都不用动。
#盘整突破 #区间突破 #中证500 #宁德时代 #量化工具 #Python #akshare #波动率 #假突破 #A股复盘 #量化策略
本文为技术方案拆解与历史数据实测,不构成任何操作建议。文中所有信号均为历史数据回放,不代表未来表现,也不针对任何具体标的做出买卖建议。量化工具开发应以学习和技术交流为目的。市场有风险,请合法合规投资。
Be First to Comment