如何避免名单剧烈波动、持仓快速膨胀
第177页 · 预计14页
第十四章的手工回测进行到第二周时,对方发现了一个之前完全没预料到的问题:每日买入名单几乎每天都在变,旧的持仓没怎么卖出,新的股票不停买进来,持仓数量像滚雪球一样越来越多。本以为是“精选10只股票”的策略,眼看就要变成持有几十只的宽基指数。
这个问题非常典型。很多人做动态调仓策略,都只设计了买入规则,没设计好卖出规则,或者卖出规则太松,最后持仓越积越多,策略完全失效。这一章我们就拆解动态调仓的两大核心痛点,给出三个可落地的解决方案,再用一个经典的外汇回测悖论,讲透回测方法本身的局限性。
对方的原话:“老师,我注意到9月1日到5日的买入清单几乎每天都在变。但您之前说买入后一直持有直到卖出信号,这不就意味着我的持仓里会有很多只股票,而且每天都在增加新股票,很少卖出?那账户不是越来越臃肿吗?”
这正是我之前预判的“持仓膨胀”问题。按照初始的建仓规则,每天买入当天选出的前10只,但卖出规则是“不再满足四维条件时才卖出”,还加了“连续两日不满足才卖出”的确认机制。在下跌市中,很多股票虽然不再满足条件,但亏损没到-15%,也没触发熔断,就会一直留在账户里,处于“浮亏但不满足卖出条件”的尴尬状态。
我检查了第二周结束时(9月12日)的模拟持仓:共持有22只股票。其中9月1日买入的南山铝业(虽然它不是未来产业赛道,但当时因基本面和技术面得分尚可被入选)已经连续8个交易日不满足四维条件,但亏损只有-8%,未触发止损,所以依然持有。这种就是典型的僵尸持仓——逻辑上已经被策略淘汰,但因为没到止损线,一直占着仓位,不贡献收益还占用资金。
我向对方解释:“老师,这个问题本质上是卖出规则的灵敏度不够。‘不再满足任何一维就卖出’听上去严格,但实际操作中我们加了‘确认机制’——连续两日不满足才卖。这导致股票在被剔除后还能在账户里躺两天甚至更久。更麻烦的是,有些股票可能永远不满足条件但也不触发止损,变成僵尸持仓,占用资金还不贡献收益。”
手工回测到第三周时,两个问题已经严重到无法忽视,必须调整规则,不然回测就失去意义了。
四维框架中的资金面条件(近5日主力净流入占流通市值>0.3%,且4/5交易日为正)对短期资金行为高度敏感。在下跌市中,主力资金流入流出切换频繁,经常是今天流入达标入选,明天小幅流出就剔除,后天又流入再入选,来回拉锯。
举个实际数据:9月1日到9月20日之间,生益电子入选3次、晶合集成2次、中际旭创2次、寒武纪2次。很多股票都是进进出出,每次停留一两天,导致模拟盘频繁买卖。
这种频繁进出有两个核心问题:一是产生大量交易成本,每次买卖千分之二点五的综合成本,一个月来回个四五次,1%的收益就被吃掉了;二是很多时候都是被动追涨杀跌,资金进的时候买在高位,资金出的时候卖在低位,反而做反了节奏。
截至9月20日,模拟盘共持有31只股票。其中,最早买入的南山铝业、海螺水泥等传统股(在早期筛选标准下买入的)已经不再满足任何维度,但因为亏损不大且未触发止损,一直没被卖出。这些股票占用了约18%的资金,却没有为组合贡献任何超额收益,反而拖累了净值。
如果继续这样运行下去,到10月底持仓可能超过50只,几乎变成一只宽基指数。四维框架的选股能力被彻底稀释,超额收益会被完全摊薄,最后和大盘收益差不多,还多了交易成本。
对方急切地问:“老师,有没有办法既保留每日动态筛选的灵活性,又能控制持仓数量和交易频率?”
针对上述两个痛点,我提出了三个补充规则,在回测中立即实施,效果立竿见影。
在原有的信号平仓和止损平仓之外,增加每月最后一个交易日的强制再平衡,专门清理长期游离在策略之外的僵尸股。
具体操作:
这条规则直接解决了僵尸持仓的问题。9月30日第一次执行再平衡时,共卖出了6只僵尸持仓,释放了约15%的资金。这些股票本来就已经被策略淘汰,只是因为没触发止损赖在账户里,强制清理之后,资金效率立刻提升了。
为了防止持仓数量无限膨胀,设置绝对上限,保证策略的聚焦度:
这条规则保证了策略永远不会持有超过25只股票,每一只新股票进入都必须有老股票退出。不会无限扩张,不会被动变成指数基金。
结合规则二,我定义了具体的卖出优先级(从高到低),确保风控优先:
这个优先级确保止损和清理僵尸优先于止盈,符合风控优先的原则。
对方听完后问:“这些规则会不会导致我卖在最低点?比如某只股票刚卖出就因为资金回流重新入选,然后我又得高价买回来?”
我承认这种可能性存在,但指出:“任何规则都有缺陷。我们的目标是防止持仓失控和过度交易,而不是追求完美择时。如果一只股票频繁进出,说明它自身的资金面不稳定,不值得长期持有。频繁买卖的成本也会侵蚀收益,不如放弃。”
在讨论每日动态调仓的挑战时,我突然想起一个多年前的极端案例。我决定借此机会向对方说明:回测方法的选择,可以制造神,也可以制造鬼。很多人拿出来的回测曲线好看,本质上是回测方法错了,不是策略真的好。
“老师,我想给您讲一个我亲身经历的故事,可能与A股不完全相关,但对您理解回测陷阱会有很大帮助。”
大约在2018年,我开发了一个完全基于技术指标的外汇MT4策略,没有用到任何基本面或资金面数据。策略逻辑很简单:在欧元/美元15分钟图上,根据RSI和均线交叉产生买卖信号,每笔交易设固定止损20点、止盈40点。
我用MT4自带的“控制点法”回测(Method: Every tick based on control points)。这个方法的原理是:在每根K线内,只采样几个关键时间点(开盘、收盘、最高、最低),并假设价格在这些点之间线性变化。绝大多数MT4用户默认使用这个方法,因为它速度快、资源占用少。
回测结果令人震惊:年化收益率超过100%,最大回撤仅8%,夏普比率3.5。曲线几乎呈45度角向上,完美得不像真的。
出于谨慎,我用另一个回测引擎(Tickstory导出的真实ticks数据,每个tick都是交易所的真实成交记录)重新测试了完全相同的策略。这次的结果是:净值曲线在两周内从10000美元暴跌至870美元,几乎爆仓。
我花了三天时间逐笔比对两个回测的差异,找到了三个致命原因:
我曾用这个案例作为考核策略师的门槛:“后来有一段时间,我要求所有向我请教的策略师,必须先拿出一条像样的、用即时价格回测得到的资金曲线——控制点法的曲线我一概不看。这个门槛过滤掉了至少80%的人,因为他们根本无法理解其中的差别。”
“您可能会说,A股不是外汇,没有那么多跳空和滑点。但原理是相通的。我们的模拟盘使用的是日线数据,每日以开盘价成交。这本质上也是一种‘控制点法’——我们忽略了盘中价格的波动。例如,某只股票开盘跌3%触及我们的止损线,但实际成交价可能因为跌停而无法成交,或者需要更差的价格才能卖出。日线模拟无法捕捉这种风险。”
“另外,A股也有‘跳空’——比如某公司突发利空,第二天直接跌停开盘。我们的规则是在开盘价卖出,但如果跌停封死,根本卖不出去。日线模拟会假设成交,而实盘则要面对连续跌停的流动性风险。”
我向对方建议:在正式运行模拟盘时,要意识到日线回测是一个乐观的近似,而不是真实世界的完美映射。实盘的回撤大概率会比回测大,要预留安全边际。
对方听完沉默了一会儿,然后说:“老师,这个案例我记住了。以后任何人给我看回测曲线,我都会问一句:您用的是控制点法还是逐tick回测?”
为了将上述所有规则(含定期再平衡、最大持仓限制、MT4警示下的成交假设调整)清晰化,我画出了一张完整的每日回测流程图,所有逻辑都固化在伪代码里,没有模糊地带。
def run_daily_backtest(date):
# 1. 加载前一交易日持仓 holdings_prev, cash_prev
# 2. 计算当日的买入清单 buy_list(基于前一交易日数据)
# 3. 确定卖出名单
sell_list = []
for each stock in holdings_prev:
# 3.1 止损平仓
if (current_price - cost_price) / cost_price <= -0.15:
sell_list.append(stock)
# 3.2 信号平仓(连续两日不满足四维条件且不在 buy_list 中)
elif stock not in buy_list for 2 consecutive days:
sell_list.append(stock)
# 3.3 再平衡(每月最后一个交易日且持仓>30天且近10日未入选TOP10)
elif is_month_end(date) and holding_days[stock] > 30 and stock not in recent_top10(stock, 10):
sell_list.append(stock)
# 4. 执行卖出(开盘价)
cash_new = cash_prev
for each stock in sell_list:
revenue = holdings_prev[stock].shares * open_price(stock) * (1 - commission - slippage)
cash_new += revenue
remove stock from holdings_prev
# 5. 处理新股买入(使用最大持仓限制)
holdings_new = holdings_prev (after sells)
cash_available = cash_new
# 如果当日不满足“至少3只”条件,则跳过买入
if len(buy_list) < 3:
skip buy
else:
# 限制买入数量:最大持仓25只,可用资金等分
max_new_positions = min(25 - len(holdings_new), len(buy_list))
buy_selected = buy_list[:max_new_positions]
cash_per_stock = cash_available / max_new_positions
for each stock in buy_selected:
shares = floor(cash_per_stock / open_price(stock) / 100) * 100
cost = shares * open_price(stock) * (1 + commission + slippage)
cash_available -= cost
add stock to holdings_new with shares, cost_price = open_price(stock)
# 6. 计算当日净值四要素
nav_open = calc_nav(holdings_prev, open_prices) + cash_prev
nav_close = calc_nav(holdings_new, close_prices) + cash_available
nav_high = max(nav_open, nav_close, calc_nav(holdings_new, high_prices) + cash_available)
nav_low = min(nav_open, nav_close, calc_nav(holdings_new, low_prices) + cash_available)
# 7. 熔断检查
if max_drawdown_20d >= 0.20:
clear_all_positions at next open, suspend trading for 5 days
return new holdings, cash, (nav_open, nav_high, nav_low, nav_close)
我向对方展示了这个流程图,并说:“老师,这就是我们手工回测后半程要严格遵循的算法。它会比前两周更复杂,但也能解决您看到的那些问题。”
“继续吧。我想看看9月下半月和10月的表现。”对方回复。
以2020年3月美股熔断为例,使用日线回测某均线策略的夏普比率为1.2,而使用分钟级回测仅为0.3,差异巨大。A股同样存在:某低换手率策略,日线回测年化收益25%,分钟级回测仅12%,核心影响因素就是开盘跳空和盘中涨跌停导致的成交偏差。
def check_liquidity(order_price, open_price, high, low):
if order_price == open_price:
if open_price == low and open_price == high:
return '一字板,无法成交'
elif open_price == low:
return '开盘即最低,可能瞬间跌停'
# 其他情况
return '可成交'
如果需要构建专业级的高频回测系统,或者验证策略在真实成交场景下的表现,可访问intoquant.com壹信量化官网,提供专业的回测引擎开发与策略验证服务。
本章内容节选自原创量化书籍《因子会说谎》,更多回测陷阱与避坑方法,可登录intoquant.com查阅完整内容。
本章完。下一章将进入下跌市的最核心考验:允许空仓规则的实际效果,以及强制买入vs空仓的对比回测结果。
⚠️ 风险提示:本书内容仅为量化研究与知识分享,不构成任何投资建议。套利交易存在基差、费率、流动性、平台与极端行情等风险,历史表现不代表未来收益。投资有风险,入市需谨慎,请自主决策、量力而行。
💡 键盘 ←/→ 翻章 · T 键切换目录