☰
布林带与KD指标共振量化策略:从股票池构建到参数调优的Python实战
2026/9/27 3:12:28 网站建设 项目流程

简介:这份文档资料面向具备一定量化基础的投资者与学习者,围绕布林通道指标与KD指标,完整演示股票池量化策略的构建与验证流程。内容从基本面选股入手,综合动态市盈率、每股收益、净利润环比增长率、净资产收益率、流动资产与流动负债差额、经营活动现金净流量六个维度筛选出四只股票组建股票池,再结合布林指标与KD指标制定买卖择时规则,最后通过回测参数设置与收益、成交、风险三方面分析检验策略有效性,并附有逐行注释的编程实现思路。资源包为1个docx文档,约391KB,结构紧凑,便于按研究思路逐步研读。目前已有326人学习参考,适合希望将基本面分析、技术择时与回测评估串联起来的中级量化学习者,可据此掌握选股逻辑、指标择时与风险指标解读的完整方法。

1. 布林带与 KD 共振:把股票池变成可复现的量化策略

很多人做量化卡在第一步:手里有几百只自选股,每天靠肉眼翻 K 线,看到布林带收口就兴奋,看到 KD 金叉就下单,结果一年下来手续费比盈利还高。问题不在指标本身,而在于没有把「布林指标 + KD 指标 + 股票池」这三件事串成一条可回测、可复现、可迭代的流水线。布林带负责定义价格波动的相对位置和通道宽度,KD 负责捕捉短期动量的拐点,股票池负责把全市场几千只标的压缩到几十只候选,三者叠加才是完整的量化策略分析框架。这套东西适合两类人:一是已经会写 Python 但不知道怎么把指标组合成策略的开发者,二是手工交易多年、想把盘感翻译成规则的老手。下面从数据准备讲到参数调优,每一步都给出可抄的代码和参数说明。

2. 布林带与 KD 的计算逻辑:为什么不能直接调库了事

2.1 布林带三条线的数学定义与窗口选择

布林带的核心就三行公式:中轨是 N 周期收盘价的简单移动平均,上轨是中轨加 K 倍标准差,下轨是中轨减 K 倍标准差。常见默认值是 N=20、K=2,但这两个参数直接决定了通道的敏感度和容错率。N 越小,通道越贴合价格,信号越频繁但假信号也越多;N 越大,通道越平滑,适合捕捉中期趋势但会错过短线拐点。K 值决定通道宽度,K=2 覆盖约 95% 的价格波动,K=1.5 会让通道收窄、突破信号更早出现但也更容易被噪音触发。

我一般会在股票池层面做一次参数扫描,而不是全市场统一用 20/2。大盘蓝筹波动率低,N=20、K=2 足够;中小盘波动剧烈,N=14、K=2.5 反而能过滤掉大量无效突破。标准差的计算必须用总体标准差还是样本标准差?pandas 的rolling().std()默认是样本标准差(ddof=1),而多数交易软件用的是总体标准差(ddof=0)。这个差异在 N=20 时大约影响 2.5% 的通道宽度,回测时如果和实盘软件对不上,先查这里。

import pandas as pd import numpy as np def bollinger_bands(close: pd.Series, n: int = 20, k: float = 2.0, ddof: int = 0): """ 计算布林带 :param close: 收盘价序列 :param n: 移动平均窗口 :param k: 标准差倍数 :param ddof: 标准差自由度,0=总体标准差,1=样本标准差 :return: DataFrame,包含中轨、上轨、下轨、带宽 """ mid = close.rolling(window=n, min_periods=n).mean() std = close.rolling(window=n, min_periods=n).std(ddof=ddof) upper = mid + k * std lower = mid - k * std # 带宽:衡量通道收口程度,用于过滤横盘 width = (upper - lower) / mid return pd.DataFrame({ "boll_mid": mid, "boll_upper": upper, "boll_lower": lower, "boll_width": width })

这段代码里min_periods=n保证前 N-1 根 K 线不产生信号,避免用不完整窗口计算出的假通道。boll_width是我额外加的,后面用来判断布林带是否收口——收口意味着波动率压缩,往往是变盘前兆,这个特征比单纯看价格触碰上下轨更有价值。

2.2 KD 指标的递推计算与平滑陷阱

KD 指标全称随机指标,先算 RSV(未成熟随机值),再对 RSV 做平滑得到 K 值,对 K 值再平滑得到 D 值。标准参数是 9、3、3:9 周期内最高价和最低价确定 RSV 的分母,K 值是 RSV 的 3 日平滑,D 值是 K 的 3 日平滑。这里最大的坑是平滑方式:国内多数软件用K = 2/3 * 前K + 1/3 * RSV,本质是 α=1/3 的指数移动平均;而有些平台用简单移动平均。两种算法在震荡市里信号差异能到 10% 以上,回测和实盘对不上时这是第二个要查的地方。

def kd_indicator(high: pd.Series, low: pd.Series, close: pd.Series, n: int = 9, m1: int = 3, m2: int = 3): """ 计算 KD 指标,采用国内常见的递推平滑方式 :param n: RSV 周期 :param m1: K 值平滑周期 :param m2: D 值平滑周期 """ low_n = low.rolling(window=n, min_periods=n).min() high_n = high.rolling(window=n, min_periods=n).max() # RSV:分母为0时(一字板)用50填充,避免除零 rsv = (close - low_n) / (high_n - low_n) * 100 rsv = rsv.fillna(50) # 递推平滑,等价于 alpha=1/m1 的 EMA k = rsv.ewm(alpha=1/m1, adjust=False).mean() d = k.ewm(alpha=1/m2, adjust=False).mean() return pd.DataFrame({"kd_k": k, "kd_d": d, "kd_rsv": rsv})

adjust=False是必须的,它保证递推公式和手工计算一致。fillna(50)处理一字涨停或跌停导致high_n == low_n的情况,否则 RSV 会变成无穷大或 NaN,后面所有信号都会断掉。KD 的经典用法是 K 上穿 D 金叉买入、下穿死叉卖出,但单独用 KD 在趋势市里会反复打脸——这就是为什么要和布林带叠加。

2.3 股票池的构建:从全市场到候选集的过滤链

股票池不是随便选几十只股票,而是一条有顺序的过滤链。我的做法分四层:第一层剔除 ST、退市整理、上市不足 60 个交易日的次新股;第二层按日均成交额过滤,低于 5000 万的直接扔掉,流动性不够的策略回测再好也执行不了;第三层按布林带宽排序,取带宽处于历史 30% 分位以下的标的,这些是波动率压缩、可能变盘的;第四层在候选集里等 KD 信号触发。这样每天从几千只压缩到 30 到 50 只,既不会漏掉机会,也不会让计算量爆炸。

def build_stock_pool(df: pd.DataFrame, min_amount: float = 5e7, width_pct: float = 0.3) -> list: """ 构建候选股票池 :param df: 包含 code, amount, boll_width 的截面数据 :param min_amount: 日均成交额下限 :param width_pct: 布林带宽分位数阈值 """ # 剔除流动性不足 df = df[df["amount"] >= min_amount].copy() # 按带宽分位数过滤,保留收口标的 threshold = df["boll_width"].quantile(width_pct) df = df[df["boll_width"] <= threshold] return df["code"].tolist()

min_amount设 5000 万是经验值,小资金可以降到 2000 万,大资金要提到 1 亿以上。width_pct=0.3表示只保留带宽最窄的 30%,这个参数在牛市可以放宽到 0.5,因为普涨行情里收口标的少。股票池每天收盘后重建一次,不要盘中动态调整,否则会引入未来函数。

3. 策略信号合成:布林带定方向,KD 定扳机

3.1 双指标共振的入场条件设计

单独用布林带突破,会在震荡市里被上下轨反复扫损;单独用 KD 金叉,会在单边下跌里不断抄底被埋。两者结合的逻辑是:布林带判断价格处于通道的什么位置,KD 判断动量是否配合。我用的入场条件是三个同时满足:收盘价从下轨下方回到下轨上方(或触及下轨后反弹),K 值在 20 以下上穿 D 值,且布林带宽处于收口状态。第一个条件确保价格没有继续破位,第二个条件确认短期动量转向,第三个条件保证是在变盘窗口而不是趋势中继。

def generate_signals(df: pd.DataFrame) -> pd.DataFrame: """ 合成布林带与 KD 的共振信号 要求 df 已包含 boll_lower, boll_upper, boll_width, kd_k, kd_d """ df = df.copy() # 条件1:价格从下轨下方收回 cond_price = (df["close"] > df["boll_lower"]) & \ (df["close"].shift(1) <= df["boll_lower"].shift(1)) # 条件2:KD 低位金叉 cond_kd = (df["kd_k"] > df["kd_d"]) & \ (df["kd_k"].shift(1) <= df["kd_d"].shift(1)) & \ (df["kd_d"] < 30) # 条件3:布林带收口 cond_width = df["boll_width"] < df["boll_width"].rolling(60).quantile(0.3) df["signal"] = (cond_price & cond_kd & cond_width).astype(int) return df

cond_price用 shift 比较前一根 K 线,确保是「收回」而不是「一直在上方」。cond_kd里kd_d < 30是低位约束,避免在高位追金叉。cond_width用 60 日滚动分位数而不是全历史分位数,这样能适应不同市场阶段的波动率变化。三个条件缺一不可,少一个都会显著增加假信号。

3.2 仓位管理与止损规则

信号只告诉你什么时候买,仓位和止损才决定你能不能活下来。我的规则是:单只股票仓位不超过总资金的 5%,同时持仓不超过 10 只,这样单票最大回撤对总资金的影响控制在 0.5% 以内。止损用布林带下轨作为动态止损线,入场后如果收盘价跌破下轨且 KD 死叉,次日开盘无条件离场。止盈分两档:第一档在价格触及中轨时减半仓,第二档在触及上轨或 KD 进入 80 以上死叉时清仓。

def position_sizing(capital: float, price: float, max_pct: float = 0.05) -> int: """ 计算单只股票买入股数,按手取整 """ max_value = capital * max_pct shares = int(max_value / price / 100) * 100 return max(shares, 0)

max_pct=0.05是单票上限,资金量小于 50 万时可以放宽到 0.1,但持仓数量要相应减少。按手取整是 A 股规则,科创板 200 股起买需要单独处理。止损规则里「收盘价跌破下轨」比「盘中跌破」更可靠,因为盘中假突破太多,收盘价确认能过滤掉大量噪音。

3.3 用向量化回测验证信号质量

回测不是看总收益,而是看信号在不同市场环境下的表现。我一般会把回测区间切成三段:趋势上涨、震荡、趋势下跌,分别统计胜率、盈亏比和最大回撤。如果策略只在上涨段赚钱,那它本质上是个 beta 策略,不值得投入。向量化回测的核心是把信号 shift 一根 K 线作为实际成交价,避免用当日收盘价成交的未来函数。

def vector_backtest(df: pd.DataFrame, fee: float = 0.0015) -> pd.Series: """ 向量化回测,信号次日开盘成交 :param fee: 单边手续费加滑点 """ df = df.copy() df["trade"] = df["signal"].shift(1).fillna(0) # 持仓状态:1 持有,0 空仓 df["position"] = df["trade"].replace(0, np.nan).ffill().fillna(0) # 日收益率 df["ret"] = df["close"].pct_change() df["strategy_ret"] = df["position"].shift(1) * df["ret"] # 扣除买卖手续费 df["cost"] = df["trade"].diff().abs().fillna(0) * fee df["strategy_ret"] = df["strategy_ret"] - df["cost"] return df["strategy_ret"].cumsum()

fee=0.0015包含万三佣金加千一滑点,实际做小盘股要提到 0.002 以上。position.shift(1)确保用昨天的持仓吃今天的收益,这是回测里最容易搞错的地方。累计收益曲线只是第一步,还要算夏普、卡玛比率和月度胜率分布,这些指标比总收益更能反映策略的稳健性。

4. 避坑与排查:回测赚钱实盘亏钱的五个血泪教训

4.1 现象:回测年化 40%,实盘三个月亏 15%

原因通常有三个:一是用了复权价回测但实盘看的是不复权价,除权除息日信号完全错位;二是股票池里包含了回测期间已经退市的股票,存活偏差让收益虚高;三是手续费和滑点设得太低,小盘股实际冲击成本远高于万三。解决方法是回测统一用后复权价,股票池按每日实际可交易标的动态生成,手续费按成交额的 0.2% 到 0.3% 计提。

4.2 现象:KD 金叉信号在盘中触发,收盘又消失

这是典型的盘中数据不稳定问题。KD 的 RSV 依赖当日最高价和最低价,盘中高低点会不断变化,导致 K、D 值在收盘前反复穿越。解决方法是所有信号一律用收盘后确认的数据计算,次日开盘执行。如果非要做盘中,至少等到下午两点半之后,此时高低点基本定型,信号稳定性大幅提升。

4.3 现象:布林带收口后突破,买入就回落

收口只代表波动率压缩,不代表突破方向。如果收口期间价格在中轨下方运行,向下突破的概率反而更大。我后来加了一个方向过滤:收口期间收盘价必须站上中轨至少 3 个交易日,才认定多头蓄势。这个条件过滤掉了大约四成的假突破,代价是错过一些 V 型反转,但盈亏比明显改善。

4.4 现象:股票池每天变化太大,持仓频繁换血

原因是带宽分位数阈值设得太敏感,今天 30% 分位是 0.05,明天可能变成 0.08,导致候选集剧烈波动。解决办法是对带宽分位数做平滑,用 5 日均值代替单日值,或者直接固定一个绝对带宽阈值(比如 0.06),只在极端行情时调整。股票池稳定性比精确性更重要,换手率降下来手续费才能降下来。

4.5 现象:同一套参数在沪深 300 上有效,在中证 1000 上失效

这不是策略问题,是标的特性问题。沪深 300 成分股波动率低、趋势性强,适合 N=20、K=2 的布林带;中证 1000 波动率高、震荡频繁,需要 N=14、K=2.5 并放宽 KD 的低位阈值到 25。我的做法是按指数成分分别维护参数组,而不是一套参数打天下。参数组每季度重新校准一次,用滚动窗口优化而不是全历史优化,避免过拟合。

5. 参数调优与滚动验证:让策略活过下一个季度

参数调优最容易犯的错是在全历史上找最优解,结果样本内夏普 2.0,样本外直接负收益。我的做法是滚动窗口验证:用前 250 个交易日做参数寻优,后 60 个交易日做样本外测试,然后窗口整体后移 60 天,重复这个过程。最终选参数时不看单次最优,而看参数在多个窗口里的稳定性——如果 N=18 和 N=22 的表现差不多,就选中间值 N=20,而不是选那个恰好最高的 N=19。

def rolling_optimize(df: pd.DataFrame, param_grid: dict, train: int = 250, test: int = 60): """ 滚动窗口参数寻优 :param param_grid: 参数网格,如 {"n": [14, 20, 26], "k": [1.5, 2.0, 2.5]} """ results = [] for start in range(0, len(df) - train - test, test): train_df = df.iloc[start:start + train] test_df = df.iloc[start + train:start + train + test] best_sharpe, best_param = -np.inf, None for n in param_grid["n"]: for k in param_grid["k"]: # 在训练集上计算布林带和信号 bb = bollinger_bands(train_df["close"], n=n, k=k) # ... 信号合成与回测,计算夏普 sharpe = 0 # 此处省略具体回测调用 if sharpe > best_sharpe: best_sharpe, best_param = sharpe, {"n": n, "k": k} # 用最优参数在测试集上验证 results.append({"start": start, "param": best_param, "train_sharpe": best_sharpe}) return pd.DataFrame(results)

train=250约一年交易日,test=60约一个季度,这个比例保证训练集足够覆盖一轮完整波动,测试集又能及时反映参数衰减。参数网格不要设太密,N 取 14、20、26 三档,K 取 1.5、2.0、2.5 三档,九种组合足够。如果最优参数在连续三个窗口里都是同一个,说明它比较稳健;如果每个窗口最优都不同,说明策略本身不稳定,应该放弃而不是继续调参。

最后说一个我自己的习惯:每次参数调整后,我会把新旧参数在最近 20 个交易日的信号差异打印出来,逐条看哪些信号被过滤了、哪些被新增了。如果新增的信号里有明显的基本面利空股,说明参数在拟合噪音而不是规律。这个人工复核步骤看起来笨,但帮我躲过了至少三次大的回撤。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询