☰
不预测涨跌,只管理风险:A股量化动态仓位风控系统实战
2026/10/14 7:54:47 网站建设 项目流程

1. 为什么我放弃了预测涨跌这条路

做了三年多A股量化,最开始我也是个"指标狂魔"。MACD金叉死叉、KDJ超买超卖、布林带突破,甚至花了大价钱买过所谓"主力资金全景指标"的源码,试图找到那个能提前告诉我明天涨跌的圣杯。结果呢?回测曲线漂亮得像艺术品,实盘一跑就原形毕露。最惨的一次是2022年那波急跌,我的择时模型还在发出"满仓买入"信号,因为所有技术指标都在超卖区钝化,模型认为"跌够了该反弹了"。

那次回撤让我彻底想明白一件事:A股短期涨跌本质上是不可预测的。不是因为模型不够好,而是因为市场参与者的行为、突发消息、资金博弈这些因素,根本不在历史数据的分布里。你用一个基于历史统计的模型去预测一个非平稳的、受情绪驱动的系统,就像用昨天的天气预报去决定今天穿什么——偶尔对,但长期看期望值为负。

所以我把整个思路推倒重来。不再问"明天涨还是跌",而是问"在当前持仓和当前市场状态下,我应该承担多少风险"。这就是风险管理思路做量化的核心:不预测方向,只管理暴露。具体来说,我构建了一套动态仓位风控系统,它不产生买卖信号,而是根据市场波动率、账户回撤、持仓集中度等维度,动态调整总仓位水平。市场平稳时仓位可以高一些,波动加剧时自动降仓,账户回撤触及阈值时强制减仓。

这套东西适合谁?适合那些已经有一套选股或择时策略、但总是死在仓位管理上的朋友。也适合刚入门量化、还没被"预测思维"带偏的新手。它不需要你有多深的机器学习功底,核心就是几个波动率指标加一套仓位映射规则,用Python加backtrader就能跑起来。下面我把整套思路拆开讲,包括为什么这么设计、参数怎么定、实盘踩过哪些坑。

2. 整体设计思路:把仓位当成风险预算来分配

2.1 从"预测收益"转向"预算风险"的思维切换

传统量化策略的骨架是:信号生成 → 仓位分配 → 执行。大多数人把精力全花在信号生成上,仓位分配往往就是简单的"信号强就满仓,信号弱就半仓"。这种做法的隐含假设是:信号强度等于预期收益,而风险是恒定的。但现实是,同样的信号强度,在不同市场环境下对应的风险完全不同。

我举个例子。同样是"均线多头排列"这个信号,在2020年7月那种低波动普涨行情里,满仓进去风险其实不大;但在2022年3月那种高波动震荡市里,同样的信号满仓进去,可能两天就给你砸出10%的回撤。信号没变,变的是市场状态。所以我的设计思路是:信号只决定"买什么",风险预算决定"买多少"。

具体做法是引入一个"风险预算"的概念。假设我设定账户单日最大可接受回撤是2%,那么在当前市场波动率下,我反推应该持有多少仓位。波动率越高,同样仓位对应的潜在回撤越大,所以仓位就要压低。这个反推过程就是动态仓位风控的核心。

2.2 三层风控架构的搭建逻辑

我把整个系统分成三层,从外到内依次收紧:

第一层是市场波动率层,负责判断当前市场整体风险水平。我用的是沪深300指数的20日已实现波动率,年化后跟历史分位数比较。波动率处于历史80%分位以上时,基础仓位上限压到50%;40%到80%之间,上限70%;40%以下,上限可以到90%。这个分位数阈值不是拍脑袋定的,后面会讲怎么用滚动窗口算。

第二层是账户回撤层,负责监控账户自身的风险状态。这里我用的是"当前净值距近期高点的回撤幅度"。回撤在5%以内,不干预;5%到10%,仓位上限打八折;10%到15%,打六折;超过15%,强制降到三成以下,并且暂停新开仓。这一层的作用是防止在市场持续下跌中越亏越加仓,把回撤控制在可恢复范围内。

第三层是个股集中度层,负责控制单票和行业暴露。单票市值不超过总资产的15%,单一行业不超过30%。这一层看起来简单,但实盘中最容易出问题——因为强势股往往集中在少数行业,你不自觉就超配了。

三层是相乘关系,最终仓位上限 = 市场层上限 × 账户层系数 × 集中度约束。这样任何一层触发风控,整体仓位都会被动下降,形成多重保护。

2.3 为什么选择波动率而不是其他风险指标

有人会问,为什么用波动率而不是最大回撤、VaR或者Beta?我的考虑是三点:可计算性、前瞻性和稳定性。

最大回撤是事后指标,等它出现时你已经亏了;VaR对尾部风险的估计在A股这种跳空频繁的市场里经常失效;Beta需要选基准且不稳定。而波动率是日频可算的,用已实现波动率(Realized Volatility)能较快反映市场状态变化,而且它有很强的聚集效应——高波动往往持续一段时间,这正好给了我们调整仓位的窗口期。

我用的是Parkinson波动率估计,比简单的收盘价对数收益标准差更敏感,因为它用了日内最高最低价。公式是:

import numpy as np import pandas as pd def parkinson_vol(high, low, window=20): """ Parkinson波动率估计 high, low: 日最高价、最低价序列 window: 滚动窗口 返回年化波动率 """ hl_ratio = np.log(high / low) ** 2 factor = 1.0 / (4.0 * np.log(2.0)) daily_var = factor * hl_ratio.rolling(window).mean() # A股一年约244个交易日 annual_vol = np.sqrt(daily_var * 244) return annual_vol

实测下来,Parkinson估计在A股上比收盘价波动率提前1到2天反映波动放大,这个提前量在急跌行情里很值钱。

3. 核心细节解析:参数怎么定、阈值怎么算

3.1 波动率分位数的滚动计算与阈值选择

波动率分位数不能用一个固定的历史区间来算,因为市场结构在变。2015年的波动率水平和2023年完全不是一个量级。我的做法是用滚动三年窗口(约732个交易日)计算当前波动率在窗口内的分位数。这样既能反映近期市场特征,又不会因为窗口太短而频繁跳变。

def rolling_percentile(series, window=732): """ 滚动分位数:当前值在过去window期中的百分位 """ result = series.rolling(window).apply( lambda x: (x[-1] > x[:-1]).mean(), raw=True ) return result

阈值我定在40%和80%,这是经过参数敏感性测试的。我试过30/70和50/90,30/70太敏感,仓位频繁调整增加交易成本;50/90太迟钝,2022年那种持续高波动行情里降仓不够及时。40/80在回测中夏普比率和最大回撤的平衡最好。

注意:分位数计算需要至少一个完整窗口的数据,所以系统上线初期要么用历史数据预热,要么先用固定阈值过渡,等数据够了再切换。

3.2 账户回撤的度量口径与冷却机制

回撤度量有个细节容易踩坑:用净值高点还是用成本线?我一开始用成本线,结果发现账户刚创新高后小幅回撤就触发风控,过于保守。后来改成用滚动60日净值高点,这样既跟踪了近期高点,又不会因为很久以前的高点导致长期误触发。

冷却机制是另一个关键。回撤触发降仓后,不能一回撤修复就立刻加回来,否则会在震荡市里反复打脸。我设了一个5日冷却期:回撤修复到阈值以内后,观察5个交易日,确认不是假修复,再逐步恢复仓位。恢复也是分批的,每次恢复不超过总仓位的20%。

def drawdown_control(nav_series, lookback=60): """ 基于滚动高点的回撤控制系数 """ rolling_high = nav_series.rolling(lookback).max() drawdown = (nav_series - rolling_high) / rolling_high # 回撤分档系数 if drawdown.iloc[-1] > -0.05: return 1.0 elif drawdown.iloc[-1] > -0.10: return 0.8 elif drawdown.iloc[-1] > -0.15: return 0.6 else: return 0.3

3.3 仓位映射的非线性处理

波动率到仓位的映射,很多人用线性关系,比如"波动率每上升1%,仓位降2%"。但实测下来,非线性映射更符合实际。因为低波动区间里,波动率小幅变化对风险影响不大;但到了高波动区间,波动率再往上走,风险是加速上升的。

我用的是分段函数加平滑:

波动率分位基础仓位上限说明
0-20%95%极低波动,可接近满仓
20-40%85%低波动,正常偏高仓位
40-60%70%中等波动,中性仓位
60-80%50%偏高波动,防御仓位
80-100%30%高波动,最低仓位

段与段之间用线性插值平滑,避免仓位跳变导致交易成本上升。这个表是我根据A股历史波动率分布和对应区间的市场表现统计出来的,不是理论推导,是实盘磨出来的。

4. 实操过程:从数据到实盘的完整链路

4.1 数据准备与清洗的坑

A股数据清洗是个体力活。我用的是本地存储的日频数据,来源是公开数据接口。几个必须处理的点:

第一是停牌。停牌期间没有价格,波动率计算会出错。我的处理是停牌日不参与波动率计算,用前值填充,但标记出来,避免在停牌股上开仓。

第二是涨跌停。涨跌停日的价格不能反映真实供需,尤其是连续涨跌停。我在计算波动率时会剔除涨跌停日的极端值,用中位数替代。

第三是复权。必须用后复权价格计算波动率和回撤,否则除权除息会造成假信号。这个坑我踩过,有一次某票分红除权,系统误判为暴跌,直接触发降仓。

def clean_price_data(df): """ df: 包含 date, open, high, low, close, volume, suspend, limit_up, limit_down """ df = df.sort_values('date').copy() # 停牌日用前值填充但标记 df['is_suspend'] = df['suspend'].fillna(False) # 涨跌停日标记 df['is_extreme'] = df['limit_up'] | df['limit_down'] # 波动率计算时剔除极端日 df['high_clean'] = np.where(df['is_extreme'], df['close'], df['high']) df['low_clean'] = np.where(df['is_extreme'], df['close'], df['low']) return df

4.2 回测框架的选择与多股回测配置

回测我用的是backtrader,主要是因为它对多股回测和自定义仓位管理的支持比较灵活。但backtrader有个坑:默认的仓位管理是按信号来的,要做动态仓位风控,得重写sizer。

我的做法是继承bt.Sizer,在_getsizing方法里读取当前的市场波动率、账户回撤和集中度约束,动态计算可买数量。

class RiskBudgetSizer(bt.Sizer): def __init__(self, vol_series, nav_series): self.vol_series = vol_series self.nav_series = nav_series def _getsizing(self, comminfo, cash, data, isbuy): if not isbuy: return self.broker.getposition(data).size # 获取当前风控系数 market_coef = self._get_market_coef() account_coef = self._get_account_coef() concentration_coef = self._get_concentration_coef(data) total_coef = market_coef * account_coef * concentration_coef target_value = self.broker.getvalue() * total_coef * 0.15 # 单票上限15% size = int(target_value / data.close[0] / 100) * 100 # 整手 return max(size, 0)

多股回测时要注意资金分配。如果同时持有20只票,每只都按15%上限买,总仓位就300%了。所以实际执行时,单票上限要除以持仓数量,或者用组合层面的风险预算来分配。我的做法是先算组合总仓位上限,再在个股间按等风险贡献分配。

4.3 实盘执行的滑点与冲击成本控制

回测和实盘最大的差距在执行成本。动态仓位风控会导致调仓频率上升,如果每次调仓都市价单,滑点能吃掉不少收益。我的应对策略:

  • 调仓阈值:仓位目标变化小于5%不调仓,避免频繁微调。
  • 分批执行:单次调仓分2到3笔,间隔几分钟,减少冲击。
  • 限价单为主:用对手价挂限价单,成交不了再追,比市价单省成本。
  • 避开开盘和收盘:开盘前15分钟和收盘前15分钟流动性差、波动大,尽量避开。

实测下来,这些措施能把年化滑点从3%压到1%以内,对高频调仓的策略很关键。

5. 常见问题与排查技巧实录

5.1 波动率突然跳变导致仓位误调整

问题现象:某天波动率分位从50%直接跳到85%,系统强制降仓,结果第二天市场就反弹,白白踏空。

排查思路:先看是不是数据问题——有没有涨跌停、停牌、除权导致的异常值。如果数据正常,那就是波动率估计窗口太短,对单日极端值太敏感。

解决方法:给波动率加一个中位数滤波,用5日滚动中位数替代单日值,或者用EWMA平滑。我最后用的是5日中位数加20日EWMA的混合,既保留敏感性又过滤噪音。

5.2 回撤控制与仓位恢复的节奏冲突

问题现象:回撤修复后仓位恢复太慢,错过反弹;恢复太快,又遇到二次探底。

排查思路:这是冷却期和恢复速度的参数问题。冷却期太短,假修复识别不了;太长,错过机会。恢复速度也是,一次性恢复容易被打脸,太慢又跟不上。

解决方法:我把冷却期设为5日,恢复分3批,每批间隔3日,每批恢复目标仓位的1/3。这样既不会一次性暴露,也不会太慢。参数可以根据策略换手率调整,高换手策略可以缩短冷却期。

5.3 集中度约束与信号冲突

问题现象:信号强烈指向某个行业,但集中度约束不让加仓,导致跑输基准。

排查思路:这是风控和收益的天然矛盾。集中度约束的目的是防止黑天鹅,但确实会在行业beta行情里拖累收益。

解决方法:我的做法是分层约束。单票上限15%不动,但行业上限可以动态调整——当行业波动率低于市场整体时,行业上限放宽到35%;行业波动率高于市场时,收紧到25%。这样在低波动的行业趋势行情里能多拿一些,高波动时又能控制风险。

5.4 常见问题速查表

问题可能原因排查方法解决措施
仓位频繁跳变波动率窗口太短检查波动率序列加中位数滤波或EWMA
回撤触发后踏空冷却期太长回看触发时点市场状态缩短冷却期或分批恢复
单票超限集中度计算口径错误核对持仓市值计算用后复权价格重算
回测实盘差异大滑点未计入对比回测和实盘成交价加滑点模型和分批执行
波动率分位失真历史窗口含极端行情检查窗口区间用滚动三年窗口

实操心得:风控系统最怕的不是参数不准,而是参数频繁改。我一开始每周调一次参数,结果系统行为完全不可预期。后来定了个规矩:参数至少跑满一个季度再评估,除非出现逻辑性错误,否则不动。

6. 几个我踩过的坑和对应的经验

6.1 别用未来信息算波动率

这个坑很隐蔽。我一开始用当日收盘价算波动率,然后用于当日开盘的仓位决策——这就是典型的未来信息泄露。回测里看着很美,实盘根本没法执行。正确做法是用前一交易日收盘后的数据算波动率,用于下一交易日的仓位决策。所有滚动指标都要shift(1)。

6.2 风控不是越严越好

我有一段时间把回撤阈值设得很紧,5%就降仓,结果在正常波动里频繁触发,交易成本高企,收益反而更差。后来想明白:风控的目标是控制尾部风险,不是消除所有波动。阈值要设在"正常波动不会触发,但异常波动能及时响应"的位置。5%到10%这个区间对A股来说太窄,我最后用的是10%起步。

6.3 仓位恢复要参考市场状态

回撤修复后恢复仓位,不能只看账户自身,还要看市场波动率。如果市场还在高波动区间,即使账户回撤修复了,也不应该急着加仓。我的做法是:账户回撤修复 + 市场波动率分位低于60%,两个条件同时满足才启动恢复。

6.4 记录每一次风控触发的原因

这个习惯帮我省了很多事。每次风控触发,我都记录:触发时间、触发层级、当时市场波动率、账户回撤、后续5日市场表现。积累了几百条记录后,我能清楚看到哪些触发是有效的、哪些是误报,参数优化就有了依据,而不是凭感觉调。

这套动态仓位风控系统跑了一年多,最大的感受是:它不会让你赚得更多,但会让你亏得更少。在A股这种波动巨大的市场里,活下来比什么都重要。我现在的账户最大回撤控制在15%以内,虽然牛市里跑不赢满仓的,但熊市里回撤小很多,长期复利下来反而更稳。后续我打算把北向资金流向、融资余额变化这些宏观风险指标也纳入进来,做更细粒度的风险预算分配,这部分等跑出结果再分享。

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

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

立即咨询