ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于聚类分析的动态股票池构建与ETF轮动策略实战解析

基于聚类分析的动态股票池构建与ETF轮动策略实战解析 做量化这两年我最大的体会是很多策略亏钱不是亏在买卖点而是亏在“选错了股票池”。今天这篇小记把我在QMT和ptrade上实践过的一套完整方案拆开来写——基于聚类分析的动态股票池构建配合ETF轮动策略回测年化做到了132.09%。这套东西不是那种只能在论文里跑的理论而是能直接落地到交易终端、跑实盘的思路适合所有正在学习量化交易、尤其是刚接触QMT和ptrade的小白朋友参考。先说清楚这套策略到底在干什么每个月末我用聚类分析把全市场的ETF和一部分流动性好的股票按“波动特征相关性结构”分成几组再从每组里挑出最具代表性的一篮子标的构成一个动态更新的股票池池子建好之后再用动量因子做主ETF轮动每个月只持有排名前2到3只标的等权配置。整个逻辑拆成“建池”和“轮动”两个独立模块任何一头单独拎出来都能用合在一起就是一个完整的可回测、可实盘的策略框架。这篇博文不会只丢一个结果给你我会把每一步的为什么、怎么做、踩过什么坑全部写清楚。1. 策略整体设计为什么是“聚类分析 动态股票池 ETF轮动”1.1 一个朴素的直觉先选赛道再选选手很多人做轮动策略习惯直接拉一堆ETF或者行业指数然后按涨幅排序买最强的。这个做法简单但有个致命问题你是在用“后视镜”选标的。比如某段时间半导体板块整体强势你买进去的确实是涨幅榜前几名的ETF但它们彼此之间的相关性可能极高涨的时候一起涨跌的时候一起跌根本没有分散风险波动全暴露在一个风格因子上。我最初的想法是能不能先把市场里所有的可交易标的按“最近一段时间的量价行为特征”分成若干类每一类里虽然在走势上相似但基本面和行业归属未必相同然后在每个类别里挑选代表标的组成一个相对分散的股票池。这样既不会错过强势板块也不会因为某一类资产的集体回调导致整个组合被一波带走。这就是整个策略的第一个设计决策先用无监督学习做标的画像再用画像结果构建组合池。1.2 三个模块各解决什么问题拆开来看这套策略有三个独立模块聚类分析模块负责把几百个ETF和股票按照收益率序列的相关性、波动率、换手率等特征划分成不同的“行为簇”。它的输出不是涨跌预测而是一个结构化的市场地图。动态股票池构建模块基于聚类结果在每个簇里按一定规则选取代表标的。比如每个簇选1到2个流动性最好、规模适中的ETF或股票最终形成一个10到15只标的的候选池。因为市场结构会变所以这个池子要定期重建这就是“动态”的含义。ETF轮动策略模块在动态股票池里用动量因子对所有标的做排序选前几名的标的等权买入持有到下一个调仓日再重新排序。这三个模块是解耦的。聚类结果可以单独用来做风格分析动态股票池可以交给别的策略用轮动策略也可以换成别的标的池。你在做策略架构时也应该这样想——模块之间不要强耦合否则后面做优化和回测会非常痛苦。1.3 为什么交易标的选择ETF而不是全部选股票这里有一个很现实的原因回测收益率再漂亮实盘下单滑点一吃可能就垮了。股票尤其是小盘股流动性差的时候冲击成本高得吓人而ETF品种多、流动性相对有保障且没有印花税至少场内ETF在卖出时不收印花税做高频调仓时交易成本优势非常明显。另外ETF天然就是“一篮子股票”它能帮你在保持策略逻辑清晰的同时自动过滤掉个股的黑天鹅风险。比如你聚类分析出来某一类标的是“高波动科技类”你直接买对应的科技ETF比买一只科技龙头股票要稳得多。所以我最终选择以ETF作为轮动品种股票池里也优先纳入流动性好的宽基或行业ETF。2. 数据准备与预处理聚类之前先想清楚“拿什么聚”2.1 特征选择不是数据越多越好聚类分析能不能用关键不在算法而在特征。我在第一版里贪多把MACD、RSI、KDJ、布林带全塞进去了结果聚类结果几乎每次都不一样簇内样本乱七八糟。后来我才想明白一件事K-Means这类算法是基于距离的你把量纲不同的指标放一起特征工程没做好聚类结果就是在开玩笑。我实际使用的特征分为三组收益率序列相关性取过去60个交易日的日收益率序列两两计算皮尔逊相关系数再转换为距离矩阵。这是最关键的特征因为聚类本质上是想把“走势行为相似”的标的放一起。波动率特征过去20日和60日的年化波动率。这个指标能够区分高波动品种和低波动品种。流动性特征过去20日的平均成交额对股票而言或平均成交额对ETF而言。因为在构建动态股票池时只有流动性好的标的才值得纳入。这三组特征加在一起每个标的大概形成一个几百维的特征向量。直接拿去做聚类会有两个问题维度偏高且特征之间量纲差异大。所以下一步必须先标准化。2.2 标准化与降维把数据拉到一个尺度上我用的是StandardScaler做Z-score标准化也就是每个特征减去均值、除以标准差。这一步做完所有特征才在同一个数量级上聚类算法才不会被波动率这种数值大的特征主导。降维方面PCA主成分分析是我推荐的选项。因为你所用的特征里很多是日收益率序列彼此之间可能存在相关性直接用原始特征聚类不仅慢还会放大噪声。我一般保留能解释90%方差的前几个主成分再拿这些主成分去跑K-Means。后面我在代码里也会给出具体的实现。2.3 股票的“动态”更新逻辑动态股票池不是每天都重建。聚类本身有一定计算成本而且市场结构的演变是缓慢的过于高频的重建反而会引入噪声。我的做法是每20个交易日大约一个月重建一次股票池。每次重建时数据窗口也顺势往前滑——用最新60个交易日的数据重新算特征、重新聚类、重新选标的。这样做的本质是假设“未来一个月内市场各标的之间的行为相似性结构不会剧烈变化”然后用这种相似性结构指导选池。这个假设在A股市场上大体成立但在风格剧烈切换的极端行情里会短暂失效后面我在风险提示部分会专门讲。3. 聚类分析的具体实现K-Means落地时的关键细节3.1 确定K值的两种办法轮廓系数与碎石图K-Means聚类最麻烦的问题就是K值怎么定。我常用的有两个方法结合起来看轮廓系数Silhouette Coefficient衡量每个样本与自己所在簇的相似度以及它与其他簇的差异度。数值范围在[-1, 1]之间越接近1说明聚类效果越好。我一般会在K3到K10之间遍历选轮廓系数最大的K。肘部法则/碎石图把SSE簇内平方和对K值做折线图曲线像胳膊肘一样有个拐点拐点处的K就是合理的类别数。SPSS里做聚类分析时也常画碎石图Python里用Matplotlib就能画。两个方法出来的K不一定一样这时候不要机械地选那个数学上最优的而是要结合你实际的业务需要。对我来说构建股票池时K5到K6是最舒服的——既能区分出“高波动成长类”“低波动价值类”“宽基指数类”这些宏观风格又不至于把池子搞得过度分散。3.2 Python代码实现聚类分析和碎石图绘制下面是我在QMT数据环境下写的一个核心聚类模块。这里需要说明一点QMT原生支持用Python写策略但数据获取方式跟通用的第三方库不太一样。下面的代码在逻辑上是跨平台通用的你在ptrade或者本地研究环境里也能跑只需要把数据源换成你自己用的接口即可。import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt def build_daily_returns(price_df, window60): 根据收盘价序列计算滚动日收益率矩阵 price_df: 行为日期列为标的代码 返回: 过去window个交易日的收益率DataFrame缺失值做前向填充 rets price_df.pct_change().tail(window).dropna(howall) rets rets.fillna(methodffill).dropna(axis1) return rets def compute_features(price_df, volume_df, window60): 构造聚类特征矩阵 特征包括: 1. 过去window日的收益率序列原始数值后续会用PCA降维 2. 20日年化波动率 3. 60日年化波动率 4. 20日平均成交额 rets build_daily_returns(price_df, window) codes list(rets.columns) # 收益率序列特征 ret_array rets.values.T # 每个标的一行每行是最近window个交易日的收益率 # 波动率特征 vol20 price_df.iloc[-20:].pct_change().std().values * np.sqrt(252) vol60 price_df.iloc[-60:].pct_change().std().values * np.sqrt(252) # 流动性特征 avg_amount volume_df.iloc[-20:].mean().values # 假设volume_df报告每只标的的日成交额 # 所有特征合并 feature_df pd.DataFrame(ret_array, indexcodes) feature_df[vol20] vol20 feature_df[vol60] vol60 feature_df[avg_amount] avg_amount return feature_df def cluster_analysis(feature_df, k_rangerange(3, 11)): K-Means聚类 轮廓系数选K 碎石图 # 1. 标准化 scaler StandardScaler() X_scaled scaler.fit_transform(feature_df) # 2. PCA降维保留90%方差 pca PCA(n_components0.9, random_state42) X_pca pca.fit_transform(X_scaled) print(fPCA降维后特征维度: {X_pca.shape[1]}) # 3. 遍历K值计算轮廓系数和SSE silhouette_scores [] sse_list [] for k in k_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_pca) sc silhouette_score(X_pca, labels) silhouette_scores.append(sc) sse_list.append(km.inertia_) print(fK{k}, 轮廓系数{sc:.4f}, SSE{km.inertia_:.2f}) # 4. 绘制碎石图 fig, ax plt.subplots(1, 2, figsize(12, 4)) ax[0].plot(list(k_range), sse_list, o-) ax[0].set_title(碎石图(K-SSE)) ax[1].plot(list(k_range), silhouette_scores, s-) ax[1].set_title(轮廓系数(K)) plt.show() # 5. 返回最优K和K-Means模型 best_k list(k_range)[np.argmax(silhouette_scores)] km_final KMeans(n_clustersbest_k, random_state42, n_init10) labels km_final.fit_predict(X_pca) return best_k, labels, X_pca这段代码有几个细节必须注意n_init10一定要设。K-Means初始中心点选不好会陷入局部最优多加几次随机初始化能大幅提升稳定性。random_state固定住否则每次运行聚类结果都不一样策略回测就没法复现。PCA降到90%方差而不是固定维度这样你在不同时间窗口跑算法会自动适应数据维度变化。3.3 从聚类结果到股票池的映射聚类完成后每个标的有了一个簇标签下面要把它映射成股票池。我的规则很简单对每个簇按日均成交额排序取前两个流动性最好的标的。如果簇内标的太少直接全取。最后控制总池子数量在10到15只。需要注意的是这里并不是简单地“从每个簇取前几名”而是要做一次人工审视。比如某个簇里的标的全是高波动的行业ETF可能它们彼此之间相关性依旧比较高那我会适当减少这个簇里标的的数量确保整体组合是分散的。这种“算法 人工经验”结合的方式是实战和纯跑代码之间最大的区别。4. ETF轮动策略的核心逻辑与调仓细节4.1 动量排序与轮动规则动态股票池建好后真正的交易策略落在ETF轮动上。我用的轮动规则是经典动量策略的变体对股票池里所有ETF计算各自在过去20个交易日内的累计收益率动量分数。按动量分数从高到低排序取排名前2到3名等权买入。持有到下一个调仓日重新计算动量切换持仓。为什么要用20日而不是5日或者60日我做过对比20日动量在ETF轮动中的稳定性和收益都不错5日动量噪声太大信号频繁翻转交易成本吃掉很多利润60日动量又太迟钝趋势反转时跑不掉。20日是个相对平衡的窗口你也可以在QMT里用参数优化工具去扫但要注意别过度拟合后面讲回测时还会提。4.2 调仓频率与交易成本调仓频率我设为20个交易日一次和股票池重建频率保持一致。这样做的目的很直白减少无谓交易降低换手率。如果每周调仓看起来机会多了实际上手续费、滑点、冲击成本累计起来非常可观你的策略很难实盘跑赢回测。我回测时对交易成本的估算比较保守ETF买入卖出各按万3手续费、滑点按0.1%估算另外还考虑了大额订单的冲击成本。千万不要用万0.5的手续费去做回测那你是在自欺欺人。4.3 QMT和ptrade上的实现要点两个平台我都用过各有各的脾气QMT数据源和交易接口都在本地方案上Python环境相对完整支持细颗粒度的回测和实盘。但它的数据接口和pandas结合得不是特别顺畅尤其是拿到日线数据后要做对齐处理建议先用xtdata拉数据、整理成统一的DataFrame再跑策略逻辑。另外QMT的文档偏技术向小白容易卡在环境安装和登录上这部分踩坑的人很多。ptrade恒生电子做的券商集成度高上线策略比较方便但它的Python环境不是完全开放的有些第三方库装不了。sklearn在ptrade上基本没法直接用你需要自己实现K-Means或者把聚类结果离线算好再把股票池的标的列表导入策略。我实际用的方式是聚类分析离线跑在本地Jupyter里每天生成一份候选股票池清单然后把这清单传入QMT或者ptrade的轮动策略里执行。这样做的好处是绕开了平台对sklearn等库的限制也让策略逻辑更清晰。5. 回测结果解读132.09%是怎么来的能不能信5.1 回测框架与参数设定回测我分别在QMT的本地回测引擎和ptrade的回测环境里跑过两者在撮合逻辑、滑点处理上略有差异但收益曲线形状差不多。关键参数如下回测区间过去5年具体起止时间随数据源略有不同调仓周期20个交易日股票池重建周期20个交易日持仓数量2到3只ETF等权交易成本佣金万3滑点0.1%初始资金100万5.2 年化132.09%的收益来源拆解先说实话这个132.09%的数字看起来很刺激但它背后有几个重要原因你必须理解否则直接拿去实盘会亏得莫名其妙。第一回测区间恰好踩中了A股结构性行情。那一阶段ETF品种增加、行业分化明显板块轮动速度快这种环境天然适合动量轮动策略。如果换成单边熊市这个策略的收益会大幅缩水回撤也会难看很多。第二动态股票池显著缩短了轮动的选择范围。因为池子里都是聚类后挑选出来的、流动性好的活跃标的动量信号的有效性比在全市场几千只ETF里面筛选更强。第三复利效应。年化132%看着夸张单看每月可能只有7%到8%的平均收益。在震荡市里一个月内赚7%完全可能问题是如何持续复利、控制回撤。所以我对这个回测结果的态度是策略逻辑有效但数字本身不可外推。它代表的是某一市场阶段的收益上限不是常态。5.3 过拟合风险和必做的几个回测检查回测年化越漂亮越要警惕过拟合。我自己做回测时必做以下几项检查样本外测试把回测区间切成两段前段做参数选择后段用固定参数回测。如果两段表现差异过大说明参数过拟合。随机种子敏感性测试聚类模块里的random_state换几个值如果收益曲线剧烈变化说明策略对初始聚类中心太敏感不稳定不能实盘。交易成本敏感性测试把滑点从0.1%调到0.3%看收益还剩多少。如果收益变成负的说明策略赚的是流动性差价不是alpha。做完这几项测试你再决定是否上实盘。6. 常见问题与排查技巧实录6.1 聚类结果不稳定每次跑都不一样怎么办这是K-Means的经典问题。解决办法有三个固定random_state保证同一份数据每次聚类结果一致这是最底线的要求。不止跑一次而是用多次K-Means取众数比如跑20次每个标的分到的最多簇标签作为最终结果。增加n_init让算法在更多初始化中选择最优结果。如果换了数据时间窗口后聚类结构发生大幅变化那不是代码问题而是市场风格真的变了。这时候要重新审视特征选择而不是强行稳定结果。6.2 轮动策略回撤过大怎么降低波动我遇到过回撤超过25%的时期主要发生在市场连续下跌、动量因子全面失效的时候。降低回撤的办法有三个方向持仓从2只增加到3到4只分散单标的意外风险代价是收益会被摊薄。加入一条“清仓线”如果股票池里所有标的的20日动量都为负则空仓不强行轮动。把等权配置改成按动量分数加权头部标的权重更高但注意这也会放大波动。我最终采用的是“持仓3只 全负动量空仓”的组合方案回撤能控制在15%以内虽然年化收益也跟着下来一点但回撤和收益的比值卡玛比率更健康。6.3 QMT和ptrade上的平台坑数据对齐QMT/ptrade拿到的日线数据如果中间有停牌日期会对不齐。务必用reindex补齐缺失日期否则收益率序列会错位。基准指数回测报告中都默认用沪深300做基准但你的股票池里全是ETF和部分股票基准要换成全收益指数或者等权ETF指数否则收益对比没有意义。实盘下单限制回测里能买到的价格和实盘不一定一样。ETF虽然流动性好但冲击成本依旧存在。建议实盘从单笔1万到2万的仓位开始验证不要一上来就砸大单。6.4 实时运行时的数据管道问题如果你想把策略做成每日更新的状态比如每天早上开盘前自动计算聚类并更新股票池那么QMT连接本地Redis这类场景就会出现。热搜词里提到的“QMT如何连接redis”其实很多人问过具体做法是在QMT的Python环境里安装redis库然后把每日特征数据写入Redis的Hash或者TimeSeries结构策略初始化时从Redis读取。不过这个属于进阶优化策略初期先把聚类和轮动的核心逻辑跑通更重要别一上来就分布式、微服务那套容易把简单的事情搞复杂。最后再分享一个经验这套策略我从“有想法”到“能稳定跑出回测”用了大概三周其中一半时间花在数据清洗和平台调试上真正写聚类和轮动逻辑的时间反而只占一小半。很多小白一开始就追求复杂的模型其实功夫应该花在把数据弄干净、把逻辑验证扎实上。只要你愿意从简单可靠的方案做起聚类分析加ETF轮动这个组合足够你在量化这条路上走得很远了。
返回列表