拓冰建站拓冰建站
首页 / 资讯中心 / 正文

布林指标与KD指标组合:Python实现股票池量化策略全解析

简介基于布林指标与随机摆动指标的股票池量化策略分析是一份面向具备一定量化基础读者的系统文档。内容覆盖从股票池构建到回测结果解读的完整研究链条先以动态市盈率、每股收益、净利润环比增长率、净资产收益率、流动资产与流动负债差额、经营现金流量六大指标筛选出四只股票并选取江山股份、天赐材料、盛新锂能、中矿资源等案例进行基本面分析随后利用布林通道与随机摆动指标设计买卖择时规则结合编程实现和回测参数设置完成收益、成交与风险分析。资源包共含一个Word文档压缩包大小约391KB便于直接查看和按需修改。目前已有326人学习文档结构清晰从选股逻辑、指标说明到代码实现、回测结果逐步递进对理解选股与择时指标的组合逻辑、搭建完整量化策略框架有实际参考价值也可用作课程设计或量化入门项目的写作范本。1. 为什么说“布林指标 KD指标”组合适合股票池量化策略很多量化爱好者在选指标时陷入一个误区觉得某个指标单独使用就足以走遍天下。事实上布林指标处理的是价格相对均线的波动幅度KD指标处理的是当前价格在近期高低点之间的相对位置两者一个管“波动边界”一个管“动量强弱”互补性很强。把它们组合进股票池量化策略核心是用布林带先框出哪些股票正处于“均值回归”的潜在区域再用KD指标去卡入场时间。这种思路对日线或周线级别的中低频交易尤其实用。适合有一定Python基础、想从单票交易转向批量筛选的技术从业者下文会给出从数据读取到回测报表的完整可改代码。2. 搭股票池用Python构建可复现的量化分析底座2.1 股票池量化策略需要先解决“数据面”问题股票池量化策略的第一个瓶颈不是指标公式而是数据怎么组织。常见做法是用接口或CSV文件把全市场日线数据拉下来统一成{code: DataFrame}字典结构每个DataFrame的索引是交易日列至少包含open/high/low/close/volume/amount。这里有两个容易踩的坑第一个是除权除息没有复权导致后复权与前复权混用信号和回测全变样第二个是交易日历不统一不同股票因停牌导致的缺行会直接破坏rolling窗口的连续性。我一般在数据落地时做一次“标准化”处理把价格列全部按前复权因子重算并剔除上市不满60个交易日、当日停牌、近20日平均成交额低于3000万的个股。这样进池子的标的至少保证有一定流动性和可交易性避免回测里出现“一支小盘股一天就能把曲线拉成直线”的失真场景。另外股票池不是每天重建一次而是按固定调仓周期重建。我常用的做法是周频调仓每周五收盘后重建股票池下周一执行买入。这样既控制了换手率又不会因为单日价格异动让池子剧烈抖动。2.2 用最小代码实现基础股票池筛选下面这段代码给定一个基准日date从全市场日线字典中筛出当期股票池。它不依赖任何专门的量化框架只用pandas方便你在现有工程里直接嵌入。import pandas as pd def build_stock_pool(daily_data, date, min_list_days60, min_avg_amount3e7, exclude_stTrue): 按基准日生成股票池。 daily_data: dictkey 为股票代码value 为 DataFrame 索引是交易日字段含 open/high/low/close/volume/amount/name date: 基准日字符串格式 YYYY-MM-DD min_list_days: 上市最少天数过滤次新股 min_avg_amount: 近20日平均成交额下限单位元 exclude_st: 是否剔除 ST / *ST pool [] for code, df in daily_data.items(): # 截取基准日之前的数据避免用到未来数据 hist df.loc[:date] if len(hist) min_list_days: continue if exclude_st and name in hist.columns: latest_name str(hist[name].iloc[-1]) if ST in latest_name.upper(): continue if amount in hist.columns: avg_amount hist[amount].tail(20).mean() else: avg_amount 0 if avg_amount min_avg_amount: continue pool.append({ code: code, last_close: hist[close].iloc[-1], avg_amount: avg_amount }) return pd.DataFrame(pool)从逻辑上看这个函数有几个关键判断点len(hist) min_list_days拦住上市时间过短的标的latest_name中带ST的标的被剔除avg_amount低于min_avg_amount的流动性过滤也被挡在外面。参数min_avg_amount我一般设 3000 万如果你做的是小市值实验可以降到 1000 万但注意滑点风险会显著上升反之做中大盘策略可以提高到 1 亿以上。需要特别提醒的是这个函数并没有对复权做处理。在实际应用中你应该在把原始数据存入daily_data之前就完成前复权操作。不同数据源的前复权接口差异较大但核心原则只有一个回测与实盘必须使用同一套复权基准否则信号的位置会整体偏移。2.3 回测需要的最小框架向量化与逐日循环的取舍按日线做股票池策略回测引擎不需要很复杂。常见做法是逐日循环每天先判断股票是否还在池中再对持仓标的打信号。这样做贴近实盘方便加手续费、滑点和涨跌停限制坏处是速度慢。单标的几千行数据无所谓但全市场几千只股票滚动跑速度就成问题。所以工程上通常采用“先算好信号再统一撮合”的思路把指标向量化计算buy/sell 两个布尔序列直接生成回测时只做状态切换。下面这段python量化交易策略代码既是回测骨架也是后续章节评估绩效的输入。import pandas as pd import numpy as np def run_backtest(close: pd.Series, buy_signal: pd.Series, sell_signal: pd.Series, initial_cash1_000_000, fee_rate0.0003): 向量化信号前提下的逐日回测。 close: 收盘价序列index 为交易日 buy_signal / sell_signal: 布尔序列True 表示当天触发 fee_rate: 单边手续费率默认万三 cash initial_cash position 0.0 equity [] for i in range(len(close)): price close.iloc[i] if buy_signal.iloc[i] and position 0: # 全仓买入手续费以持仓市值形式扣除 position cash / price cash 0.0 position * (1 - fee_rate) elif sell_signal.iloc[i] and position 0: cash position * price * (1 - fee_rate) position 0.0 equity.append(cash position * price) return pd.Series(equity, indexclose.index)这个回测函数假设当日信号当日按收盘价成交是最大简化版。其中fee_rate是单边费率一般 A 股默认万三到万五如果考虑滑点可以在函数外把成交价改成close * (1 slippage)效果类似。更细的撮合逻辑比如涨停买不进、跌停卖不出需要在信号过滤阶段先处理第五章会给出具体做法。3. 布林指标与KD指标信号怎么算、怎么合并3.1 布林指标计算公式与标准差口径布林指标由中轨、上轨、下轨三条线组成。中轨是N日均线上下轨是中轨加减K倍标准差。这里有一个统计口径差异pandas 的std()默认ddof1而布林指标的标准差一般取总体标准差ddof0。两者在小窗口下差异尤其明显20日窗口大约有百分之几的偏差直接影响上下轨宽度。下面给出一段标准实现直接指定ddof0避免不同版本回测口径不一致def bollinger_bands(close: pd.Series, window: int 20, num_std: float 2.0): 计算布林指标上中下轨。 close: 收盘价序列 window: 均线窗口默认20周线策略可改用 20*5100 num_std: 标准差倍数默认2 返回 (upper, middle, lower) middle close.rolling(windowwindow).mean() std close.rolling(windowwindow).std(ddof0) upper middle num_std * std lower middle - num_std * std return upper, middle, lower为什么num_std默认取 2因为在正态分布假设下价格落在均值±2倍标准差内的概率约为95%。实际金融数据不服从正态分布布林带给出的“统计带”更多是情绪参考当价格贴着下轨运行说明短期情绪偏弱均值回归概率在提升但不是说一定会反弹。这也就是为什么只用布林带一个指标容易在单边下跌行情里反复抄底亏损——价格可以在下轨之下运行很久。加入KD指标作为二次确认就是用来过滤掉这种“情绪弱但动能仍在向下”的场景。3.2 KD指标计算公式与递归初始化细节KD指标的核心是 RSV随后对 RSV 做指数平滑得到 K 和 D。标准公式如下RSV (收盘价 - 9日最低价) / (9日最高价 - 9日最低价) * 100 K 2/3 * K(前一日) 1/3 * RSV D 2/3 * D(前一日) 1/3 * K初始 K 和 D 一般取第一个 RSV 或直接取 50。用 pandas 的ewm可以直接实现递归平滑。注意alpha要设为1/3并且adjustFalse这样才能匹配标准KD的更新式。def kd_indicator(df: pd.DataFrame, n: int 9, alpha: float 1/3): 计算KD指标的K值和D值。 df: 必须含 high, low, close 三列 n: RSV 的回看窗口默认9 alpha: K 与 D 的平滑系数标准值为1/3 low_n df[low].rolling(windown).min() high_n df[high].rolling(windown).max() rsv (df[close] - low_n) / (high_n - low_n) * 100 rsv rsv.fillna(50.0) k rsv.ewm(alphaalpha, adjustFalse).mean() d k.ewm(alphaalpha, adjustFalse).mean() return k, d这里有个容易被忽略的细节ewm(adjustFalse)的递归公式是y_t (1 - alpha) * y_{t-1} alpha * x_t正好对应 K 的更新式。如果误用默认的adjustTrue结果会变成从起始点向当前值的全量加权平均前面几百行的K值会与标准KD差异巨大信号位置也会整体偏移。另外rsv.fillna(50.0)这一句是给前n-1行一个中性初值避免最前面的 K 和 D 出现 NaN。如果你准备逐日滚动计算而非一次性全量计算这个初值决定权在你自己但标准做法是取 50不会对后续稳态产生实质影响。3.3 组合信号设计布林定位置KD定时点单用布林带你得到的是一个价格通道单用 KD你得到的是一个摆动区间。把它们组合成“布林KD”的入场信号我常用一个非常朴素的逻辑布林带下轨与KD低位金叉同时发生才买入。“低位”指 K 值低于 20 或 30“金叉”指 K 从下向上穿越 D。def generate_signals(df, bb_window20, bb_std2.0, kd_n9, kd_low20): 生成布林KD组合买入/卖出信号。 df: 含 open/high/low/close 的日线 DataFrame 返回 df 副本新增 buy_signal / sell_signal 两列 df df.copy() upper, middle, lower bollinger_bands( df[close], bb_window, bb_std) k, d kd_indicator( df[[high, low, close]], nkd_n) # 买入前一日收盘仍在下轨之上 # 当日收盘跌破下轨且 K 在低位上穿 D buy_signal ( (df[close].shift(1) lower.shift(1)) (df[close] lower) (k d) (k.shift(1) d.shift(1)) (k kd_low) ) # 卖出价格向上穿越中轨或 K 值大于 80 后死叉 sell_signal ( (df[close] middle) | ((k d) (k.shift(1) d.shift(1)) (k 80)) ) df[buy_signal] buy_signal df[sell_signal] sell_signal return df这里是整个策略的核心。买入信号加了三层过滤跌破下轨、KD金叉、K值在低位。为什么不直接用“当日收盘跌破下轨就买”因为跌破下轨既可能是均值回归机会也可能是趋势加速的第一天单独看布林带无法区分。KD低位金叉作为附加条件是在用动量指标确认下跌动能正在衰减。卖出信号没有做太严格的限制。原因在于这个策略的盈利来源是中轨回归价格一旦回到布林带中轨以上意味着“从偏离到回归”的过程大概率已经走完K值80以上的高位死叉作为止盈保护防止在极端强势行情中过早下车。如果你在实盘里觉得卖出太频繁可以再加一条持仓时间不小于5日的最低持有规则但这属于交易成本与信号频率的权衡不是策略逻辑的必要条件。4. 回测结果评估与参数调优4.1 核心绩效指标用夏普而不是总收益评判回测跑完不能只看总收益还要看年化、最大回撤、夏普比率、胜率和盈亏比。用第三章的run_backtest函数拿到权益曲线后我通常立刻算下面几个指标def evaluate_equity(equity: pd.Series, periods_per_year252): 从权益曲线计算核心绩效指标。 equity: 逐日权益序列 periods_per_year: 年交易日数日线取252周线取52 ret equity.pct_change().dropna() total_return equity.iloc[-1] / equity.iloc[0] - 1 years len(equity) / periods_per_year annual_return (1 total_return) ** (1 / years) - 1 peak equity.cummax() drawdown (equity - peak) / peak max_drawdown drawdown.min() # 夏普比率无风险利率按0计 sharpe ret.mean() / ret.std() * (periods_per_year ** 0.5) return { total_return: total_return, annual_return: annual_return, max_drawdown: max_drawdown, sharpe: sharpe, volatility: ret.std() * (periods_per_year ** 0.5) }最大回撤是这个策略最需要盯的指标。布林KD这种均值回归型策略往往胜率不错但偶尔会遇到单边下跌的品种下轨不断下移、买进去就被套。如果最大回撤超过20%说明股票池里混进了不该有的趋势性下跌个股此时应优先调整入池条件而不是盲目修改指标参数。4.2 三个核心参数怎么选布林窗口、带宽倍数、KD周期布林和KD合在一起最关键的可调参数有三个布林窗口bb_window、带宽倍数bb_std、KD周期kd_n。下表是常见取值范围和它们对结果的影响方向参数常见取值参数变大时的表现参数偏小时的典型问题bb_window15~30通道更宽信号更少信号频繁手续费侵蚀收益bb_std1.5~3.0触发更难假信号减少假突破增多胜率下降kd_n5~14KD更平滑金叉更晚过早金叉下跌中继被套参数的选取没有全局最优。比较稳妥的做法是网格遍历后看参数平面的稳定性如果3×3×3的参数空间里只有少数几组参数赚钱绝大多数亏钱说明策略本身不稳健靠的是参数拟合如果大片区域都能盈利且回撤差距不大说明策略逻辑成立参数选择只是微调。4.3 用滚动窗口做参数调优防止前视偏差“前视偏差”是参数调优中最容易犯的错。正确做法是用历史某段时间内的数据选出参数再用没参与选参的后一段数据做样本外验证。下面给出两段式网格搜索的骨架from itertools import product def grid_search(daily_df, param_grid, in_sample_dates, out_sample_dates): 两段式网格搜索样本内选参样本外验证。 daily_df: 单只股票或股票池等权组合的行情 DataFrame param_grid: {bb_window: [15, 20, 25], bb_std: [1.5, 2, 2.5], kd_n: [9, 14]} 返回样本内外核心指标组成的 DataFrame results [] for bb_window, bb_std, kd_n in product( param_grid[bb_window], param_grid[bb_std], param_grid[kd_n]): df generate_signals(daily_df, bb_window, bb_std, kd_n) df_is df.loc[in_sample_dates] df_oos df.loc[out_sample_dates] eq_is run_backtest(df_is[close], df_is[buy_signal], df_is[sell_signal]) eq_oos run_backtest(df_oos[close], df_oos[buy_signal], df_oos[sell_signal]) results.append({ bb_window: bb_window, bb_std: bb_std, kd_n: kd_n, is_sharpe: evaluate_equity(eq_is)[sharpe], oos_sharpe: evaluate_equity(eq_oos)[sharpe], }) result_df pd.DataFrame(results) return result_df.sort_values(oos_sharpe, ascendingFalse)一个实际工程要点股票池场景下不一定对每只股票单独调参而是对“全池子等权组合”调同一组参数。原因是个股噪音大单独调参容易过拟合组合级别的参数普适性更高。我在项目管理几十只股票时只跑组合级网格个股级调参的人力成本完全不划算。5. 把回测报表当体检报告三个必须看的隐藏项很多人拿到一份漂亮的回测权益曲线就开始兴奋但回测报表里最关键的三个“隐藏项”往往不在首页。第一个是幸存者偏差。如果你用的股票池是今天存在的股票、而不是历史当时存在的股票那么退市股和暴跌股从一开始就不在回测里年化收益虚高10%以上都很常见。解决办法是使用带退市标记的全量历史数据或者在入池流程中保留“之后被退市”的标的直到退市日。第二个是手续费与滑点的真实占比。布林KD组合策略的交易频率不低按20日布林带、每周调仓来算年单边换手可能到20倍以上。回测里如果只用万三佣金且不算滑点夏普比率会比较好看一旦把滑点按0.1%计入很多参数组合会从盈利变亏损。验证手段很简单把fee_rate从0.0003改成0.001看年化收益缩水多少缩水幅度超过40%就要考虑降低调仓频率。第三个是出场信号是否真的可执行。这个策略的买入条件是“跌破下轨 KD低位金叉”成交价是按收盘价模拟的。但在跌停板上收盘买入往往无法成交涨停板上卖出也同理。更保守的做法是在信号生成后加一条规则当日不是一字涨停或跌停才允许成交。代码里加一行过滤即可def apply_limit_filter(df): 剔除一字涨跌停日的伪信号。 limit_up df[close] df[close].shift(1) * 1.095 limit_down df[close] df[close].shift(1) * 0.905 df.loc[limit_up | limit_down, [buy_signal, sell_signal]] False return df把generate_signals的输出接上这个过滤再进回测曲线会朴素很多但每一笔收益都是实盘里拿得到的。拿这个口径去对比优化前后的夏普比率你才能真正判断“布林KD”这一套框架在股票池量化策略中值不值得上线。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门