Skip to content

盘整找如何突破?我实测了中证500和宁德时代(附源码)

作 者:老余捞鱼

原创不易,转载请标明出处及原作者。

文中示例仅用于技术讨论,不构成任何操作建议。
量化策略开发应以学习和技术交流为目的。
本号不荐股、不卖课、不承诺收益。
市场有风险,请合法合规投资。

本文约 5000 字 | 预计阅读 8-12 分钟,后有实例代码,建议先收藏。

· · ·

我复盘时有个习惯:先把当天的涨停梯队扫一遍,再去翻自选股的分钟图。翻得多了,我发现一个挺有意思的规律:那些真正让我赚到钱的票,在进场前,往往都有一段不太起眼的横盘。

不是V型反转,也不是火箭发射。就是十几个交易日里,价格在一条很窄的走廊里来回晃,量能一点点缩下去,新闻没了,群里也没人聊它。

直到某一天,一根放量的大阳线或者大阴线把走廊的门撞开,行情就来了。

老手把这段叫盘整,或者说是行情在憋一口气。

麻烦的是,这段平静光靠眼睛看,很吃亏。你看它横了三天,它可能还有二十天要横;你看它像是要突破,结果那根阳线第二天又缩回去了。这种差一点点的假突破,市场里一抓一大把。

今天我分享一套现成的方法,能把盘整变成数字,把突破变成规则。接下来我会用Python把它在A股整个复现了一遍,两个标的换成中证500和宁德时代。

先说清楚它是什么、不是什么。

图1:一段越走越窄的波浪走廊,量的是宽度,等的是推门那一刻

它只管两件事:第一,把“盘整”变成一个可以量出来的数字;第二,盯住价格冲出走廊的那一刻,替你把信号记下来。

它不管未来。冲出去之后能走多远,它不知道。后面我会用五个信号的真实表现给你看,比空口说更有说服力。

一、我测出来的结果,先给答案

先上数字。

中证500,往前取 9 个月。 脚本自动选中的参数是窗口 10 天、阈值 6.0%,一次命中。9 个月里量出 4 个盘整区,每一个都走出了信号。

盘整区天数信号信号价后5日后20日
02-13 ~ 02-2411向上突破8527.56-3.3%-8.9%
04-03 ~ 04-096向上突破8088.57+2.2%+5.5%
04-27 ~ 05-069向上突破8696.80-0.3%-3.9%
09-02 ~ 09-108向下突破7571.20样本不足两天后收回
图2:中证500 九个月,4 个盘整区 4 个信号,全部实跑输出

宁德时代,往前取 3 个月。 这次网格里没有参数组合命中目标,脚本走了兜底路径,用窗口 10 天、阈值 5.0% 找到 1 个区,随后等来 1 个向下突破信号。

盘整区天数信号信号价后5日后10日
08-20 ~ 08-244向下突破376.73-4.9%-10.9%
图3:宁德时代 三个月,1 个盘整区 1 个信号,跌破之后一路走了下来

刚跑完的时候,我还是有小鸡冻的。宁德那个向下突破信号,8 月 25 日 376 块离场,之后十天跌了 10.9%,一路跌到 305 附近。这个信号事后看,准得让人想立刻把它挂到实盘上去。

但我把五个信号全部对了一遍账,心气就降下来了。

五个信号对完账,站住的是两个。中证500 那三个向上突破的信号,两个在 20 个交易日后是亏的,02-25 那个买完 20 天亏 8.9%。还有一个 09-14 的向下突破信号,就在前两天刚触发,结果两个交易日后价格又收回区间里了,现在看更像一次假跌破。

四个宝箱,只有两个半装了宝。

账要一笔一笔算。这两个半是怎么装了宝的,才是这篇真正想讲的东西。

二、怎么装,怎么跑

整套东西装起来只要三步,全程不花钱,数据源是免费的。

第一步装两个库

① 装依赖,一行搞定

pip install akshare pandas matplotlib

akshare 负责数据,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 天,就成了一个盘整区。区的最高价就是天花板,最低价就是地板。

图4:中证500 每天的窗口振幅,红线是 6% 阈值,绿影里就是盘整日

这张图你看后是不是感觉:最近走势收敛了?其实就是曲线往下扎进红线的下方,图变成了可以核查的东西,这是整套方法里我觉得最值钱的一步。

核心逻辑就这几行。

④ 振幅怎么算,就这四行

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%,等于要求价格拿出一点真金白银的姿态,而不是蹭一下就跑。

图5:天花板上方多出一道 0.5% 的门,差一点的都被拦在外面

这次实跑还撞上了一个教科书级的案例。中证500 那个区,地板是 7610.58,乘上 0.995 之后触发线是 7572.53。9 月 14 日那天收盘 7571.20,比触发线只低了 1.33 个点。就差这一点点,向下突破信号响了。

更有意思的在后面。两个交易日后,9 月 16 日,指数收盘 7683,不但收回了地板,还站回了地板上方 1%。我在屏幕前看着这个信号,心情很复杂。缓冲挡得住“差一点点”的假动作,挡不住“过两天再骗你”的假动作。

七、五个信号的事后账

现在回到第二节欠的那笔账。这些信号,事后到底走成什么样。

我把每个信号触发日的收盘价记下来,往后数 5 个、10 个、20 个交易日,算涨跌。

对账的口径也交代清楚。信号靠收盘价触发,收盘之前它出不来。真要照着做,你得等收盘确认信号,第二天再动手,中间还隔着隔夜跳空和手续费。下面的账全部从信号当天收盘价起算,没占便宜,但也没扣跳空和手续费,你看的时候心里得有这杆秤。

图6:五个信号的对账,颜色越深代表回看越远,灰色代表样本还不够

一笔一笔过。

站住的两个。中证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股复盘 #量化策略

本文为技术方案拆解与历史数据实测,不构成任何操作建议。文中所有信号均为历史数据回放,不代表未来表现,也不针对任何具体标的做出买卖建议。量化工具开发应以学习和技术交流为目的。市场有风险,请合法合规投资。

Published inAI&Invest专栏

Be First to Comment

    发表回复