我回测了A股 10 年的追涨停策略,结果可能和你想的不一样

用户头像sh_*2176oo
2026-06-18 发布

我回测了 A 股 10 年的"追涨停"策略,结果可能和你想的不一样

A 股散户圈里有一种玩法叫"打板"——今天某只股票涨停了,明天开盘就买进去,赌它继续涨。

打板选手有一套自己的逻辑:涨停说明有资金强势介入,次日惯性上冲的概率不低,如果能接力涨停就赚 10%,不能涨停就止损出来。听起来赔率不错。

但"听起来"和"跑数据"是两回事。

这篇文章用 AlphaFeed 拉 A 股历史数据,做一个严格的统计回测:涨停之后的第二天、第三天、第五天,平均涨跌是多少?胜率多高?如果严格执行"涨停次日买入"策略,长期下来是赚是亏?

先说结论:追涨停长期来看大概率亏钱,但细节比你以为的复杂。

1. 找出历史上所有涨停的日子

A 股涨停幅度一般是 10%(创业板/科创板 20%),但实际因为四舍五入,涨幅在 9.5%–10.5% 之间就可以算涨停。

我们先选一组活跃股,拉长期 K 线,找出所有涨停日:

import pandas as pd
from alphafeed import AlphaFeed

af = AlphaFeed()

symbols = [
    "000001.SZ", "600519.SH", "000858.SZ", "601318.SH",
    "002594.SZ", "300750.SZ", "600036.SH", "000333.SZ",
    "601012.SH", "600276.SH", "000568.SZ", "601888.SH",
    "002415.SZ", "300059.SZ", "600809.SH", "000651.SZ",
    "002304.SZ", "300124.SZ", "002475.SZ", "600887.SH",
]

klines = af.klines.batch(
    symbols, period="1d", count=5000,
    adjust="none",   # 用不复权数据,否则涨跌幅会被复权扭曲
    to_dataframe=True, show_progress=True,
)

all_limit_ups = []

for sym, df in klines.items():
    df = df.sort_values("trade_date").reset_index(drop=True)
    df["ret"] = df["close"].pct_change()

    for i in range(1, len(df)):
        if df["ret"].iloc[i] >= 0.095:  # 涨停(含误差)
            all_limit_ups.append({
                "symbol": sym,
                "date": df["trade_date"].iloc[i],
                "close": df["close"].iloc[i],
                "idx": i,
            })

print(f"共找到 {len(all_limit_ups)} 次涨停")
print(f"覆盖 {len(klines)} 只标的")

2. 涨停次日表现:平均能赚多少?

核心问题:今天涨停,明天买进去,一天能赚多少?

import pandas as pd
import numpy as np
from alphafeed import AlphaFeed

af = AlphaFeed()

symbols = [
    "000001.SZ", "600519.SH", "000858.SZ", "601318.SH",
    "002594.SZ", "300750.SZ", "600036.SH", "000333.SZ",
    "601012.SH", "600276.SH", "000568.SZ", "601888.SH",
    "002415.SZ", "300059.SZ", "600809.SH", "000651.SZ",
]

klines = af.klines.batch(
    symbols, period="1d", count=5000,
    adjust="none", to_dataframe=True, show_progress=True,
)

results = []

for sym, df in klines.items():
    df = df.sort_values("trade_date").reset_index(drop=True)
    df["ret"] = df["close"].pct_change()

    for i in range(1, len(df) - 5):
        if df["ret"].iloc[i] < 0.095:
            continue

        # 涨停次日买入(用次日开盘价)
        if i + 1 >= len(df):
            continue
        buy_price = df["open"].iloc[i + 1]
        if buy_price <= 0:
            continue

        # 次日收益(用次日收盘价)
        ret_1d = df["close"].iloc[i + 1] / buy_price - 1

        # 第3日收益
        ret_2d = df["close"].iloc[i + 2] / buy_price - 1 if i + 2 < len(df) else None

        # 第5日收益
        ret_5d = df["close"].iloc[i + 5] / buy_price - 1 if i + 5 < len(df) else None

        # 是否连板(次日也涨停)
        next_ret = df["ret"].iloc[i + 1]
        is_连板 = next_ret >= 0.095

        results.append({
            "symbol": sym,
            "date": df["trade_date"].iloc[i],
            "ret_1d": ret_1d,
            "ret_2d": ret_2d,
            "ret_5d": ret_5d,
            "is_连板": is_连板,
            "open_pct": (buy_price / df["close"].iloc[i] - 1),  # 次日开盘溢价
        })

rdf = pd.DataFrame(results)

print(f"=== 涨停次日表现统计({len(rdf)} 次涨停样本)===")
print()

for label, col in [("持有1天", "ret_1d"), ("持有2天", "ret_2d"), ("持有5天", "ret_5d")]:
    valid = rdf[col].dropna()
    print(f"  {label}:")
    print(f"    平均收益: {valid.mean():+.2%}")
    print(f"    中位数:   {valid.median():+.2%}")
    print(f"    胜率:     {(valid > 0).mean():.1%}")
    print(f"    最大赚:   {valid.max():+.2%}")
    print(f"    最大亏:   {valid.min():+.2%}")
    print()

你可能会看到的结果

在多数统计中,涨停次日的平均收益接近 0 或微负,但中位数通常为负。这意味着:

  • 少数大赚的案例拉高了平均值(连板翻倍的那些)
  • 多数情况是小亏:高开低走、开盘冲高后回落
  • 胜率大概在 40%–50% 之间——连抛硬币都不如

3. 次日开盘溢价:买入成本被抬高了

涨停次日最大的问题不是"涨不涨",而是你买不到好价格

因为昨天涨停了,今天大家都想买,开盘价往往直接高开 3%–5%。你以为自己在追涨停,其实你在高位接盘:

print("=== 涨停次日开盘溢价分布 ===")
print(f"  平均开盘溢价: {rdf['open_pct'].mean():+.2%}")
print(f"  中位数溢价:   {rdf['open_pct'].median():+.2%}")
print()

bins = [(-1, -0.02), (-0.02, 0), (0, 0.02), (0.02, 0.05), (0.05, 0.10), (0.10, 1)]
labels = ["低开>2%", "低开0-2%", "高开0-2%", "高开2-5%", "高开5-10%", "高开>10%"]

for (lo, hi), label in zip(bins, labels):
    pct = ((rdf["open_pct"] >= lo) & (rdf["open_pct"] < hi)).mean()
    print(f"  {label}: {pct:.1%}")

如果次日平均高开 3%,那你买入的一瞬间已经"亏了 3%"。即使当天收涨 2%,你的实际收益是 -1%。

这就是追涨停最大的隐性成本:你的买入价不是昨天的涨停价,而是今天被抬高后的开盘价。

4. 连板概率:买到第二个涨停有多难

打板选手追求的最大收益来源是"连板"——买进去之后第二天继续涨停。我们来看看概率:

连板率 = rdf["is_连板"].mean()
print(f"=== 连板概率 ===")
print(f"  涨停后次日再涨停的概率: {连板率:.1%}")
print(f"  也就是说,每 {1/连板率:.0f} 次涨停里,大约有 1 次连板")
print()

# 连板 vs 不连板的收益差异
连板组 = rdf[rdf["is_连板"]]
非连板组 = rdf[~rdf["is_连板"]]

print(f"  连板组 (n={len(连板组)}):")
print(f"    次日收益: {连板组['ret_1d'].mean():+.2%}")
print(f"  非连板组 (n={len(非连板组)}):")
print(f"    次日收益: {非连板组['ret_1d'].mean():+.2%}")

一般来说,连板概率在 10%–20% 之间。这意味着你打 10 次板,可能只有 1–2 次吃到连板的肉,其余 8 次都在亏开盘溢价和高开低走的差价。

5. 什么样的涨停更值得追

不是所有涨停都一样。我们可以按几个维度拆分,看看哪种涨停次日表现更好:

import pandas as pd
import numpy as np
from alphafeed import AlphaFeed

af = AlphaFeed()

# 假设已经有 rdf(上面的结果 DataFrame)

# 维度1: 成交额大小
# 需要在构建 results 时加入成交额字段,这里补充
symbols = list(set(rdf["symbol"].tolist()))
klines = af.klines.batch(
    symbols, period="1d", count=5000,
    adjust="none", to_dataframe=True,
)

enriched = []
for _, row in rdf.iterrows():
    sym = row["symbol"]
    if sym not in klines:
        continue
    kdf = klines[sym]
    kdf = kdf.sort_values("trade_date").reset_index(drop=True)
    match = kdf[kdf["trade_date"] == row["date"]]
    if match.empty:
        continue

    amount = match["amount"].values[0]
    volume = match["volume"].values[0]
    prev_amount = kdf[kdf["trade_date"] < row["date"]]["amount"].tail(20).mean()
    vol_ratio = amount / prev_amount if prev_amount > 0 else 1

    enriched.append({
        **row.to_dict(),
        "amount": amount,
        "vol_ratio": vol_ratio,
    })

edf = pd.DataFrame(enriched)

print("=== 按成交额分组的涨停次日表现 ===")
edf["amount_group"] = pd.qcut(edf["amount"], q=3, labels=["低成交额", "中成交额", "高成交额"])

for group in ["低成交额", "中成交额", "高成交额"]:
    g = edf[edf["amount_group"] == group]
    print(f"  {group} (n={len(g)}): 次日均值{g['ret_1d'].mean():+.2%}  胜率{(g['ret_1d']>0).mean():.0%}")

print()
print("=== 按量比分组的涨停次日表现 ===")
edf["vol_group"] = pd.cut(edf["vol_ratio"], bins=[0, 1.5, 3, 100], labels=["量比<1.5", "量比1.5-3", "量比>3"])

for group in ["量比<1.5", "量比1.5-3", "量比>3"]:
    g = edf[edf["vol_group"] == group]
    if len(g) < 5:
        continue
    print(f"  {group} (n={len(g)}): 次日均值{g['ret_1d'].mean():+.2%}  胜率{(g['ret_1d']>0).mean():.0%}")

通常的发现是:

涨停类型 次日表现 解读
缩量涨停(量比低) 相对较好 筹码锁定好,抛压小
放巨量涨停(量比>3) 相对较差 换手大,可能是拉高出货
高成交额涨停 不确定 大盘股涨停少见,统计上有参考价值
低成交额涨停 波动大 小盘股弹性高但风险也高

6. 止损纪律:不止损会怎样

打板选手最重要的规则之一是"错了就砍"。不止损的后果有多严重?我们来对比:

import pandas as pd
import numpy as np

# 模拟三种执行方式
strategies = {
    "无止损(持有5天)": rdf["ret_5d"].dropna(),
    "止损-3%": rdf["ret_1d"].apply(lambda x: x if x > -0.03 else -0.03),
    "止损-5%": rdf["ret_1d"].apply(lambda x: x if x > -0.05 else -0.05),
}

print("=== 止损 vs 不止损 ===")
for name, rets in strategies.items():
    equity = (1 + rets).cumprod()
    total_ret = equity.iloc[-1] - 1
    max_dd = (equity / equity.cummax() - 1).min()
    print(f"  {name}:")
    print(f"    累计收益: {total_ret:+.2%}")
    print(f"    最大回撤: {max_dd:.2%}")
    print(f"    平均单次: {rets.mean():+.2%}")
    print()

不止损的结果通常非常难看——因为那些大亏的 case(涨停次日直接跌 5%–8%)会把利润全部吃掉。严格止损会改善结果,但可能从"大亏"变成"小亏"。

7. 累计净值曲线:长期跑下来什么样

把每一次"涨停次日买入、收盘卖出"的收益串成净值曲线:

import pandas as pd
import numpy as np

rdf_sorted = rdf.sort_values("date").reset_index(drop=True)

equity = [1.0]
for _, row in rdf_sorted.iterrows():
    ret = row["ret_1d"]
    if pd.isna(ret):
        continue
    cost = 0.0015  # 单边手续费+印花税
    net_ret = ret - cost * 2
    equity.append(equity[-1] * (1 + net_ret))

equity_series = pd.Series(equity)
total_return = equity_series.iloc[-1] - 1
max_dd = (equity_series / equity_series.cummax() - 1).min()

print(f"=== 追涨停策略长期表现 ===")
print(f"  交易次数:  {len(rdf_sorted)}")
print(f"  累计收益:  {total_return:+.2%}")
print(f"  最大回撤:  {max_dd:.2%}")
print(f"  最终净值:  {equity_series.iloc[-1]:.4f}")
print()
if total_return < 0:
    print(f"  结论: 无差别追涨停,长期是亏钱的")
else:
    print(f"  结论: 有正收益,但需要看夏普和回撤是否可接受")

加上交易成本之后,结果会更加难看。每次交易都要付万 3 佣金 + 千 1 印花税,打板交易频率高,成本累积很快。

8. 这意味着什么

回测结果通常指向几个结论:

1. 无差别追涨停是负期望的。

平均次日收益在扣除成本后大概率为负。涨停不是免费的"涨 10%"——你的买入成本被次日高开抬高了,而高开之后回落的概率比继续涨的概率大。

2. 少数人能赚钱,因为他们不是无差别追。

真正的打板高手会筛选:首板 vs 二板、题材强度、板块联动、分时走势、封单量。他们追的是特定条件下的涨停,而不是看到涨停就冲。

3. 追涨停赚钱的本质不是"涨停好",而是"选股好"。

如果你能选出明天涨停的票,你当然赚钱。但这和"追涨停"是两回事。追涨停是在涨停已经发生之后买入,这时候信息已经被价格消化了。

4. 打板的收益分布是极端的。

少数连板收益很大,多数普通涨停次日是亏的。这意味着你必须交易很多次才能碰到几次大赚,但过程中的亏损会消耗你的本金和心态。

9. 如果你还是想研究打板

把上面的分析框架改成你自己的筛选条件,看看特定条件下的涨停是否表现更好:

# 示例:只看"首板"(前一天不是涨停的涨停)
首板 = rdf[rdf["open_pct"] < 0.095]  # 次日不是一字板,说明不是连板后的

print(f"首板次日表现 (n={len(首板)}):")
print(f"  平均收益: {首板['ret_1d'].mean():+.2%}")
print(f"  胜率:     {(首板['ret_1d'] > 0).mean():.0%}")

你也可以加入更多筛选维度:

  • 板块里同时涨停的个数(题材强度)
  • 涨停时间(早盘涨停 vs 尾盘涨停)
  • 涨停前的走势(底部涨停 vs 高位涨停)
  • 市值区间(小盘涨停 vs 大盘涨停)

每加一个条件,样本量就会减少,但如果剩下的样本显示出稳定的正期望,那可能是一个值得深入研究的方向。

10. 你可以用这个方法验证任何"民间策略"

这篇文章的价值不只是"打板赚不赚钱"这一个结论,而是给了你一套验证方法:

提出假设 → 定义规则 → 拉历史数据 → 统计收益/胜率/回撤 → 下结论

AlphaFeed 可以拉数千根 K 线,支持不复权模式(对涨停分析很重要),批量获取几十只票的数据。有了数据,任何策略假设都可以用同样的方法验证。

下次有人跟你说"这种票明天一定涨",你不用争论——打开终端,跑一下数据。

结语

追涨停是 A 股散户圈最有争议的话题之一。有人靠它赚到过钱(他们的故事你都听过),有人靠它亏到清仓(他们的故事你没听过)。

数据告诉我们的是:无差别追涨停的长期期望收益为负。 涨停次日高开低走的概率、交易成本的累积、连板极低的概率,这些因素加在一起,让"看到涨停就追"变成一个负期望的游戏。

但这不意味着所有涨停都不值得关注。如果你有能力在涨停中进一步筛选——通过成交量特征、板块联动、市场情绪、分时结构——那有可能找到正期望的子集。只是,这已经不是"追涨停",而是"选股"了。

参考文献:

评论