Skip to content

小K线藏着大信号,6行代码就能搭一个“投降信号”筛选器

作 者:老余捞鱼

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

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

本文约 4100 字 | 预计阅读 10 分钟,后有完整代码,建议先收藏。

· · ·

6 行代码,跑了 6 年半,1619 个交易日,就筛出 4 天。

拿着这 4 天信号进场,胜率 25%。

随便挑一天进场,胜率 49.2%。

这形态,比瞎蒙还差。

可我手上先有五个完美案例:波音、特斯拉、CSL、微软,全在大跌末端,一根小 K 线压着巨量,扭头就拉起来,一个比一个漂亮。

直到我把第六个,咱们大 A 散户天天盯的 510300,放进去,图直接垮掉。

是不是到了咱们大 A 就水土不服?

今天我手把手把这个 K 线形态筛选器从头搭一遍,拿这六个例子给它验验成色。三个坑我全替你踩了,每坑都有数据,代码文末直接跑。

信不信,看完你自己定。

一、要筛的东西,叫投降日

先说清楚它到底在筛什么。

跌势走到某个时刻,会出现这么一天。K 线实体和影线都很短,最高价减最低价没几个点,成交量却是平时的好几倍。

价格几乎没动,筹码却换了一大波。

关店甩卖最后一天,货全倒出去,价没再降

你可以把它想成一场关店大甩卖。最后一天,老板把剩下的货全搬到门口,半价处理。把想清的货这天全倒出去了,门口接货的排起了队。最后人累瘫在地上,价格牌却没再往下掉。

这就是投降日(Capitulation)。最后一批不肯走的人在这一天认输离场,卖压一次性放完,接的人把单子全吃了。价格窄幅,说明卖再多也没砸出坑。量巨大,说明换手彻底。两个条件叠在一起,就是小 K 线藏着大信号。

先看看骗我上船的那五个。

波音 BA。2026 年 1 月的低点,一根短阴线压着大约 3 倍于周围的量柱。之后直接见底,回到 220 美元上方。

波音,最典型的那一根,短阴线压着约三倍量柱

特斯拉 TSLA。2026 年年中,同一个低点附近,两次投降特征叠在一起。几周内掉头向上。

特斯拉,两次投降特征叠在同一低点

CSL。年内跌势最低点那天,一根放量短阴线。之后一路涨回 170 美元上方。

CSL,量柱压在全年最低点

微软 MSFT。360 美元附近,量柱盖过图上其他任何一天。见底后回到 500 美元上方。

微软,量柱盖过图上其他任何一天

苹果 AAPL。2026 年 1 月的真实低点那天,量能平平,形态压根没出现。

苹果,1 月的真实低点那天什么特征都没有

以上四个成立,一个打脸。苹果这张图倒是提醒了我两件事。

第一,形态不是每个底都有。苹果全年量能最猛的一天出现在 7 月,离 1 月的低点隔了半年。

第二,这根 K 线是红是绿不重要。红的,是卖方压了一下价、买方尾盘接走;绿的,是买方盘中就占上风。关键都在巨量窄幅本身,方向无关。

好,形态讲完了。五个例子摆在一起,你大概跟我想的一样,这东西看着是真有点东西。

第六个例子,我留到我们大 A 天天必看的标的上。

二、起步版,6 行代码

把形态翻成筛选条件,其实就两步。

每天的振幅除以成交量,得到一个比值,它衡量单位成交量推动了多少价格。数值越小,说明越多的量只推动了越少的价格。

再叠一个成交量高于均值,把缩量阴跌的日子排除掉。

① 起步版筛选器

df['Price_Range'] = df['High'] - df['Low']
df['Range_to_Volume_Ratio'] = df['Price_Range'] / df['Volume']
avg_volume = df['Volume'].mean()

days_with_low_ratio = df[
    (df['Range_to_Volume_Ratio'] < 0.00000005) &
    (df['Volume'] > avg_volume)
]

代码短得让人放松警惕,可里面埋着两个坑。

第一个是那个 0.00000005,按苹果的价格和成交量级别手调出来的硬编码,量纲是美元每股。

第二个藏得深一点。分母里的均值是全样本算的,等于今天的信号要靠未来八年的平均量来判定,实盘上你根本拿不到这个数。

还有第三个,它不在代码里,在参数里,等会儿单说。

一个一个拆。

三、第一个坑,量纲

数据先交代。通达信前复权日线,2020 年 1 月 2 日到 2026 年 9 月 4 日,510300 共 1619 根。国内 ETF 用成交量容易被分红除权干扰,我改用成交额,逻辑不变,量纲同理。

把起步版那个 5e-8 原样套上去试试。

② 起步版口径,固定阈值

df['range'] = df['high'] - df['low']
df['ratio'] = df['range'] / df['turnover']      # 成交额替代成交量avg_turn = df['turnover'].mean()

hits = df[(df['ratio'] < 5e-8) & (df['turnover'] > avg_turn)]

结果六年里命中 527 天,占总交易日的 32.6%。

等于没筛。

原因一眼能看穿。510300 的价格在三四块钱,日振幅通常不到一分钱,成交额几十亿。拿振幅除以成交额,中位数只有 0.00000000002,比 5e-8 整整小了三个数量级。

美元股票的阈值,对人民币 ETF 就是一张不过筛的网。

记住这条。凡是带固定阈值的形态筛选器,阈值都是标的量纲的函数,不是规律的参数。换标的不重算,筛出来的就不是信号,是噪音。

四、第二个坑,前视

第二个坑藏在分母里。

起步版那行 avg_volume = df[‘Volume’].mean(),均值是六年半一起算的。意思是 2020 年 1 月 2 日这天有没有信号,要等 2026 年 9 月 4 日收盘之后才知道。

你把未来六年的量平均了一遍,再回头判今天。实盘上这个数根本拿不到。

这类写法在回测里特别讨巧,它悄悄把当时不可能知道的信息喂给了信号,收益曲线平白好看一截。修法不复杂,全样本均值换成滚动 20 日均值,再往后移一天,只用信号发生之前的数据。

量纲顺手一起收拾。振幅除以收盘价,变成振幅百分比。成交额除以自己前 20 日的均值,变成放量倍数。两个指标都无量纲,跨标的能直接比。

③ 修前视,顺手去量纲

df['range_pct'] = df['range'] / df['close']
df['turn_ratio'] = df['turnover'] / df['turnover'].rolling(20).mean().shift(1)

修完再看,同样条件下信号从 5 个变成 4 个。数量差别不大,可方向是脏的。多出来的那一个,是回测拿未来数据给你发的糖。

以后看见回测特别漂亮的东西,先问一句。这个数,当天收盘的时候我算得出来吗。算不出来,就是前视。

五、第三个坑,阈值漂移

两个坑补上了,指标干净了。表出来了,咱们拆数字。

主口径这样定,振幅取全样本最小的 20%,放量倍数取 2 倍。

④ 主口径,窄幅加放量

sig = (df['range_pct'] < df['range_pct'].quantile(0.20)) & (df['turn_ratio'] >= 2.0)

考验在放量倍数上。取 3 倍,六年出 1 个信号。放宽到 2.5 倍,2 个。2 倍,4 个。1.5 倍,13 个。

放量倍数阈值信号数叠加60日线下方
3.010
2.520
2.041
1.5134

筛子从”几乎抓不到”到”一堆信号”,只动了 1.5 倍。

哪个是对的。不知道。这个形态没有客观的边界,每个标的、每个参数取法,结论都不一样。筛子的松紧是你在替市场做决定,不是市场在告诉你答案。

六、第六个例子,510300

标尺修好了,该拿它去量了。我拿 2 倍这档做主口径,检验信号质量。6 年 4 个信号,信号次日视为进场点,往后看 10 个交易日。

510300 投降信号分布(窄幅20%分位 × 放量2倍 × 60日线下方)

4 个信号,后 10 日胜率 25%,平均收益负 1.5%。同期全样本随便挑一天往后看 10 日,胜率 49.2%,平均正 0.23%。

还是比瞎蒙差一截。

再按形态的原始语义收紧,要求信号日收盘价站在 60 日均线下方,也就是只认跌势里的投降。6 年只剩 1 个信号,2023 年 12 月 29 日,之后 5 个交易日跌了 4.7%。

样本量 4 个和 1 个,什么都证明不了,它们连”策略无效”都证明不了。它们能证明的只有一件事。

在宽基指数上,这个形态六年只个位数出现,而且出现的几次也都不好看。

七、为什么宽基长不出投降日

答案在形态的成因里。

投降日的本质,是单一标的的筹码在同一天集中换手。一只票的持有人结构相对集中,恐慌的时候大家抢着跑,量能能在一天里冲到平时的五倍十倍,价格因为有人接住而收窄。

个股的情绪是共振的。所以这个形态在波音、特斯拉身上反复出现。

指数是几百只股票的平均。某几十只票恐慌的时候,总有另外一批在涨,整体量能被摊平。六年里,沪深300ETF 成交额放大到前 20 日均值 3 倍以上的日子,一只手数得过来。

指数没有恐慌日,只有持续的低迷。

把个股上总结的形态搬到指数上,等于拿着渔民观察鲸鱼的经验去看鱼群,对象的行为模式根本不同。

标的的行为结构决定筛子的生死,比参数重要得多。所以这套筛子想发挥用处,回个股去,别赖在宽基上。

八、正确用法,观察清单不是自动筛机

到这里,筛子的定位就清楚了。它是入场过滤器,帮你把值得看第二眼的日子捞出来,仅此而已。

落到日常,就三个动作。

筛出来的日子,点开图看第二眼,确认量能是不是真恐慌、位置是不是真在大跌末端。

看过的信号,预设止损,放在信号 K 线的低点下方。

错了,亏一小笔确定的钱;对了,跟的是形态本身的胜率。

机器海选,亲自看图,止损先画好

要防的是反过来用。把手调阈值的观察工具当成自动筛选机,跑到自己没看过的标的上直接认信号。那形态里偶尔混进去的假信号,会一次一次出现在你的持仓里。

顺手给一套验伪三步,以后见到任何形态类筛选器都能用。

第一步,查量纲。阈值里出现 0.00000005 这种数,先问它是谁的量纲。换一个标的重算,信号数量级不变才算过关。

第二步,动阈值。把关键参数上下松 30%,信号数量翻几倍或者归零,说明结论是参数的,不是规律的。

第三步,比基准。信号的事后收益,必须跟全样本随机进场的基准比。不比基准的胜率没有意义,50% 可能是天赋,也可能是行情本身。

三步半小时,能挡掉大部分看起来很美的形态教程。

观点总结

这篇搭完,三层。

形态是真的,投降日在个股大跌末端反复出现,前五个案例没有骗人。

筛子是难的,量纲、前视、阈值漂移,三道坎每一道都能让没准备的人拿到一堆假信号。

标的是命门,宽基指数六年只给个位数样本,事后还跑输随机。这个形态属于个股的恐慌,不属于指数的阴跌。

最后留一个跟数据直接挂钩的问题。你用过的形态筛选器里,有没有换一个标的就完全失灵的?评论区报个名字,我挑几个复现。

完整可运行脚本在文末,数据换成你自己的标的,一分钟出结果。

觉得有用的话,把这篇转给同样在搭筛选器的朋友。


完整复现脚本(复制即用)

# -*- coding: utf-8 -*-

"""
巨量窄幅K线投降信号筛选器(510300,2020-2026)
依赖:pip install pandas numpy matplotlib
数据:把 510300_daily.csv 换成你自己的日线 CSV(列名 date, open, high, low, close + 成交额列)
"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei"]
plt.rcParams["axes.unicode_minus"] = False


# ---------- 1. 数据 ----------
df = pd.read_csv("510300_daily.csv", parse_dates=["date"]).set_index("date").sort_index()
df.columns = [c.strip().lower() for c in df.columns]

# 成交额列名各家不一样,amount / turnover / 成交额 都认,取第一个存在的
amt_col = next(c for c in ["turnover", "amount", "成交额", "turn", "amt"] if c in df.columns)
df = df.rename(columns={amt_col: "turnover"})
df = df[df.index >= "2020-01-01"]
df["ret"] = df["close"].pct_change().fillna(0.0)

print(f"共 {len(df)} 根,{df.index[0].date()} ~ {df.index[-1].date()},成交额列 = {amt_col}")


# ---------- 2. 起步版口径:固定阈值(演示量纲不可迁移) ----------
df["range"] = df["high"] - df["low"]
df["ratio"] = df["range"] / df["turnover"]
hits_orig = df[(df["ratio"] < 5e-8) & (df["turnover"] > df["turnover"].mean())]
print("起步版命中 %d 天 / %d 天 (%.1f%%)" %
      (len(hits_orig), len(df), len(hits_orig) / len(df) * 100))


# ---------- 3. 相对化口径(无量纲,可跨标的,无前视) ----------
df["range_pct"] = df["range"] / df["close"]
df["turn_ratio"] = df["turnover"] / df["turnover"].rolling(20).mean().shift(1)
df["below_ma60"] = df["close"] < df["close"].rolling(60).mean()

# 阈值敏感性:看信号数随参数漂移
narrow = df["range_pct"].quantile(0.20)
for k in [3.0, 2.5, 2.0, 1.5]:
    s = (df["range_pct"] < narrow) & (df["turn_ratio"] >= k)
    print(f"放量>{k}倍: 信号 {int(s.sum())} 个, 叠加60日线下方 {int((s & df['below_ma60']).sum())} 个")

sig = (df["range_pct"] < narrow) & (df["turn_ratio"] >= 2.0)   # 主口径:4 个信号
sig_ma60 = sig & df["below_ma60"]                              # 收紧版:只认跌势里的投降,6 年只剩 1 个
print(f"主口径信号 {int(sig.sum())} 个,叠加60日线下方后 {int(sig_ma60.sum())} 个")


# ---------- 4. 信号质量:次日进场,未来 N 日收益 vs 基准 ----------
def signal_stats(sig, horizon, tag=""):
    outs = []
    for t in df.index[sig.reindex(df.index, fill_value=False)]:
        loc = df.index.get_loc(t)
        if loc + horizon < len(df):
            outs.append(df["close"].iloc[loc + horizon] / df["close"].iloc[loc] - 1)
    base = (df["close"].shift(-horizon) / df["close"] - 1).dropna()
    s = pd.Series(outs)
    if s.empty:
        print(f"{tag} 后{horizon}日:无信号")
        return
    print(f"{tag} 信号数 {len(s)}, 后{horizon}日胜率 {(s>0).mean()*100:.0f}%, "
          f"平均 {s.mean()*100:.2f}%, 基准胜率 {(base>0).mean()*100:.1f}%, "
          f"基准平均 {base.mean()*100:.2f}%")

for h in [5, 10, 20]:
    signal_stats(sig, h, "主口径")
print("--- 收紧版:收盘价在60日均线下方 ---")
for h in [5, 10, 20]:
    signal_stats(sig_ma60, h, "收紧版")


# ---------- 5. 信号分布图 ----------
fig, ax = plt.subplots(figsize=(12.8, 7.2), dpi=150)
ax.plot(df.index, df["close"], color="#5a7fa8", lw=1.0, label="510300 收盘价")
pts = df[sig]
ax.scatter(pts.index, pts["close"], color="#e17055", s=64, marker="v",
           zorder=5, label=f"投降信号({len(pts)} 个)")
ax.legend()
ax.grid(alpha=0.3)
fig.tight_layout()
fig.savefig("candle_signal_check.png")
plt.show()

#量化回测 #K线形态 #成交量分析 #投降信号 #Python量化 #沪深300ETF #510300 #策略复现 #选股过滤器 #量纲陷阱 #金融工程 #散户防坑


免责声明:本文所有回测数据仅为技术研究用途,历史表现不代表未来。文中不构成任何操作建议,据此操作风险自负。

Published inAI&Invest专栏

Be First to Comment

    发表回复