跟vectorbt打交道这两年,我最大的感受是:它真正擅长的不是“跑一个回测”,而是“批量跑几百个、几万个回测”。投资组合优化听起来像是PyPortfolioOpt、cvxpy那些库的专属领域,但如果你把优化问题拆开看——本质上是在一堆候选权重里找一个评价指标最好的——那就完全落进了vectorbt最擅长的那片区域。这篇文章我会用一个5只ETF构成的投资池,从数据准备、目标函数、权重搜索到样本外验证,把整个流程串起来,每一步都给出可以抄的代码和关键解释。
适合谁看?手里已经有一套会跑的选股或择时信号、想让组合层面的权重配置更有依据的人,以及想搞清楚“优化出来的权重到底能不能信”的人。对vectorbt版本差异不熟的读者也不用担心,我会在踩坑的部分专门说版本迁移的注意点。
1. 为什么我会在vectorbt里做投资组合优化
1.1 vectorbt被低估的地方:向量化批量回测
多数人第一次接触vectorbt,是被它的速度和交互式图表吸引。它底层把所有标的、所有参数组合的收益序列摊平成二维或三维NumPy数组,一次算完。这意味着你不需要写for循环去逐个测试,而是把“测试空间”直接当成一个矩阵维度。
这个特性放到投资组合优化里,价值就非常明显了。传统做法是:先用优化器解出一个最优权重,再拿这个权重去跑一次回测,看看效果。如果回测结果不满意,就要回过去改目标函数、改约束、改参数,然后再跑。这个过程在非向量化的工具里非常痛苦,因为一次只能评估一个组合。
而在vectorbt的语境下,你可以一次性生成上万个候选权重,然后每个权重对应的组合收益序列都被算好,绩效指标也被列成一张长表。你做的不是“优化一次,验证一次”,而是“优化和验证同时进行”。这个思路上的转变,是我觉得vectorbt做组合优化最有魅力的地方。
1.2 传统组合优化库和vectorbt的分工
市面上做马科维茨这类优化的库已经很成熟了,最典型的是PyPortfolioOpt。它提供了从协方差估计、有效前沿计算到回测后分析的完整流程。
我的实际分工方式是:
- PyPortfolioOpt做初筛:快速算出一组合理的候选权重
- scipy.optimize做自定义目标函数:因为有些约束和偏好,现成库不一定方便表达
- vectorbt做最终裁决:把候选权重全部丢进回测,看样本外表现、回撤、换手,用结果说话
很多初学者容易陷入一个误区,觉得“用优化器算出来的权重就是结论”。实际上,组合优化只是给你一版“假设”,真正重要的是这个假设放进回测系统里跑出来的“结果”。vectorbt在这件事上比任何纯优化库都顺手,因为它本来就是做回测的。
1.3 用5只ETF搭一个能复现的案例
为了让这篇文章不悬空,我固定用一个投资池往下走,你可以换成任何你自己关注的标的。
我选了5只A股ETF,分别代表不同资产属性:
| 代码 | 名称 | 资产属性 |
|---|---|---|
| 510300 | 沪深300ETF | 大盘蓝筹 |
| 510500 | 中证500ETF | 中小盘成长 |
| 159915 | 创业板ETF | 高弹性成长 |
| 510880 | 红利ETF | 高股息防御 |
| 511260 | 十年国债ETF | 债券类资产 |
这5个标的风格差异挺大,相关性不算高。做组合优化的时候,资产之间的相关性逻辑比单个资产的收益高低更重要,所以这个池子用来演示是够用的。如果你要换标的,注意尽量选资产类别差异大的,否则优化结果会显得很“偏科”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据准备:优化与回测都依赖同一份干净收益表
2.1 资产选择与数据对齐
我先说数据源。A股ETF的历史行情,用akshare可以直接拉,接口简单,不用注册。我习惯把复权方式设成前复权,这样后续算收益率的时候不会被除权除息跳空干扰。
python复制import akshare as ak
import pandas as pd
import numpy as np
symbols = ["510300", "510500", "159915", "510880", "511260"]
price_dict = {}
for s in symbols:
df = ak.fund_etf_hist_em(
symbol=s,
period="daily",
start_date="20200101",
end_date="20241231",
adjust="qfq"
)
price_dict[s] = df.set_index("日期")["收盘"].rename(s)
price = pd.DataFrame(price_dict).dropna()
print(price.head())
这里有几个容易踩的细节:
- 停牌或上市时间不一致会导致某些行只有NaN。我直接用了
dropna(),这意味着所有ETF必须从同一时间开始都有交易数据,才会被保留。这个方法简单但粗暴,如果某只标的上市晚,就会把前面一大段数据全删掉。更好一点的做法是:每个标的使用自己可用的起始日期,回测时按公共区间截取。 - 指数型ETF的收盘价是交易所公布的收盘价,已经包含了一定的尾盘定价机制,对回测来说可以接受。
- 有的ETF会有份额折算,前复权数据已经处理掉了,这个不用担心。
2.2 收益率计算与协方差矩阵
组合优化里的所有目标函数,几乎都要用到收益率序列和协方差矩阵。先算日收益率:
python复制returns = price.pct_change().dropna()
然后进入优化的核心,年化收益均值和年化协方差:
python复制mu = returns.mean().values * 252
cov = returns.cov().values * 252
为什么乘以252?因为日频数据需要年化才能用在年化目标函数里,年化收益率直接把日均收益乘上交易日数量,年化协方差则直接乘252,这在大多数量化工具里都成立。
一个容易被忽略的问题:协方差矩阵的估计质量。样本协方差对异常值非常敏感,特别是标的不多、样本量又有限的时候,矩阵的条件数可能很大,导致后面优化器求解不稳定。业界的补救手段是收缩估计(比如Ledoit-Wolf),PyPortfolioOpt里也内置了相关方法。
我在实际使用中会做一点小处理:如果发现优化结果权重在样本内和样本外差异巨大,我第一反应不是改目标函数,而是回去看协方差矩阵是不是太极端了。
2.3 切割样本内与样本外
投资组合优化最怕的不是算错权重,而是过拟合到样本内。为了避免“自己考自己”,我在案例里会把数据切成两块:
- 样本内(训练集):2021-01-01到2023-12-31,用来算均值和协方差、做权重优化
- 样本外(测试集):2024-01-01到2024-12-31,用来验证权重真实表现
这个切法是最简单的一次性分割。更严谨的做法是滚动窗口,后面我会在第5章单独讲。
python复制train = returns.loc["2021-01-01":"2023-12-31"]
test = returns.loc["2024-01-01":"2024-12-31"]
这里再提醒一句:所有在样本内做的操作,包括数据清洗、协方差估计方式的选择、优化目标的选择,都不能再回头根据样本外结果去调整。这是量化里最基本的纪律,但真的执行起来很容易手滑。
3. 三种经典目标函数:风险怎么定义,权重就怎么变
3.1 最小方差组合:只看风险的边界
组合优化并不只等于“最大化收益”。实际上,在绝大多数目标函数里,你都必须为收益和风险之间做一个权衡。最小方差组合是里面最保守的一种,它完全不管预期收益,只求整仓波动率最低。
数学上就是解这样一个问题:
code复制min w^T Σ w
s.t. sum(w) = 1
0 <= w_i <= 1
Σ是协方差矩阵,w是权重向量。注意这里我用s实际上指的是Σ,公式里用Σ更清楚。
python复制from scipy.optimize import minimize
def min_variance(w):
return w @ cov @ w
cons = {"type": "eq", "fun": lambda w: np.sum(w) - 1}
bounds = [(0, 1)] * len(symbols)
x0 = np.ones(len(symbols)) / len(symbols)
res_minvar = minimize(
min_variance,
x0,
method="SLSQP",
bounds=bounds,
constraints=cons
)
minvar_weights = res_minvar.x
为什么这个组合值得看?因为在一个相关性偏低的资产池里,最小方差组合往往会有大量的债券和红利资产权重,它输出的是一种“防御姿势”。如果你的目的不是打败市场,而是让组合少受伤,这个目标函数比最大夏普更稳健。
3.2 最大夏普比率:收益和风险的天平
最大夏普是我在实践中最常用的目标函数,它不只是看收益,也不只是看风险,而是看“每一单位风险换来多少超额收益”。
目标函数是:
code复制max (w^T μ - rf) / sqrt(w^T Σ w)
rf是无风险利率,我用的是1.5%(对应货币基金的近似收益水平)。
因为scipy的minimize默认是求最小化,所以我把目标函数写成负夏普:
python复制rf = 0.015
def neg_sharpe(w):
port_ret = w @ mu
port_vol = np.sqrt(w @ cov @ w)
return -(port_ret - rf) / port_vol
res_sharpe = minimize(
neg_sharpe,
x0,
method="SLSQP",
bounds=bounds,
constraints=cons
)
sharpe_weights = res_sharpe.x
这里有个非常经典的问题:最大夏普对输入参数(尤其是预期收益μ)的估计误差极其敏感。你用过去三年的平均收益估μ,和用过去一年的平均收益估μ,解出来的权重可能天差地别。
所以,我的实际习惯是:最大夏普的结果一定要配合敏感度分析一起看。如果权重在μ的合理变动范围内波动剧烈,那这个“最优解”本身就没什么可执行的价值。
3.3 风险平价:让每个资产“贡献”相当
风险平价(Risk Parity)在国内被很多人叫“等风险贡献”,它是一种不依赖预期收益的组合方法,只基于协方差矩阵。核心思想:每个资产对组合总风险的边际贡献相等。
简化目标函数可以写成:
code复制min sum( (RC_i - RC_j)^2 )
其中RC是每个资产的风险贡献,等于权重乘以边际风险:
python复制def risk_parity(w):
port_var = w @ cov @ w
marginal = cov @ w
contrib = w * marginal
return np.sum((contrib - np.mean(contrib)) ** 2)
res_rp = minimize(
risk_parity,
x0,
method="SLSQP",
bounds=bounds,
constraints=cons
)
rp_weights = res_rp.x
风险平价有个很好的特性:它对协方差矩阵的依赖、对预期收益的依赖都相对稳,组合通常不会特别极端。对于没有太强方向判断的配置型需求,我会优先看这个。
3.4 scipy优化器里的约束与初始值细节
用SLSQP做这类带约束优化,有几个细节值得注意:
- 权重上下界一定要给。如果不给bonds,优化器可能会给出负权重(做空)或者超过100%的杠杆权重,后者在你的回测系统里可能完全没法执行。
- 初始值不要全设为0。如果初始权重在约束边界上,SLSQP容易卡在局部极值。我习惯用等权重做初始值。
- 等式约束一定要写成
sum(w) - 1 = 0的形式。如果你写成sum(w) - 0.99 = 0,最终优化值会和预期差一点,而且很难发现。 - 优化器返回的结果可能存在很小的数值误差,比如权重加起来是0.99999。我会做一次归一化处理。
python复制def normalize_weights(w):
return w / np.sum(w)
三种目标函数的结果放在一起对比很有意思:
| 方法 | 特征 | 适用场景 |
|---|---|---|
| 最小方差 | 防守型,债券资产权重大 | 熊市防守、低风险偏好 |
| 最大夏普 | 进攻型,波动大的资产权重可能偏高 | 趋势明确、追求高效比 |
| 风险平价 | 均衡型,不依赖收益预期 | 长期配置、稳健策略 |
4. 向量化网格搜索:vectorbt最擅长的事
4.1 为什么在低维组合里,网格搜索比解析解更可靠
当资产数量在3到6只之间,我觉得完全没必要执着于求解器的精确解。原因是:优化目标里只要带点非线性或约束,解析解就不存在,而scipy求解器对初始值、边界条件都敏感,解出来的“最优解”到底是不是全局最优,谁也不知道。
但换一个思路就简单了:直接生成一大批候选权重,把所有组合的收益率序列算出来,再一次性算绩效指标。这就是网格搜索或随机抽样。在vectorbt的语境下,这一步天然是向量化的,速度极快。
当然,网格搜索有它自己的边界。当资产数量超过8到10只时,单纯网格的维度会爆炸,必须用随机采样或更高级的优化算法去替代。我在本章会给出两种方案,你按自己的资产数量选择。
4.2 从均匀网格到Dirichlet随机抽样
先说低维场景。如果你只有3只资产,你可以用numpy的meshgrid生成一个密集网格:
python复制grid = np.arange(0, 1.0001, 0.05)
w1_all, w2_all, w3_all = np.meshgrid(grid, grid, grid)
w_all = np.column_stack([
w1_all.ravel(),
w2_all.ravel(),
w3_all.ravel()
])
# 剔除权重和不等于1的行
w_all = w_all[np.isclose(w_all.sum(axis=1), 1.0)]
这个做法很直观,但注意两个坑:
- 步长0.05意味着每个维度有21个候选,三维就有9261个候选,剔除后大概还剩231个左右。三维还行,五维如果用同样的网格就完全不可接受。
- 用meshgrid生成后必须做一次规范化,因为浮点数求和不精确。
np.isclose的容差要设置好。
对于5只以上或者不希望网格过密的情况,我推荐用Dirichlet分布直接抽样。Dirichlet分布天生保证抽出来的向量非负且和为1,非常适合生成候选权重:
python复制n = len(symbols)
n_samples = 20000
w_samples = np.random.dirichlet(np.ones(n), size=n_samples)
np.ones(n)是Dirichlet分布的浓度参数。全部设为1时,生成的点在单纯形上近似均匀分布;如果你想让某些资产倾向于权重更高,可以调大对应位置的浓度参数。
4.3 用NumPy矩阵运算一次算出全部组合收益
生成了两万个候选权重后,接下来的计算才是真正体现向量化的地方。
python复制# returns 是 (T, n) 的日收益率矩阵
ret_mat = returns.values # shape (T, n)
T = ret_mat.shape[0]
# w_samples @ ret_mat.T 得到 (n_samples, T)
port_rets = w_samples @ ret_mat.T
这一步操作的本质是:一次矩阵乘法,算出了两万个组合各自的时间序列收益。如果你的机器内存充足,两万乘几百的矩阵完全没问题。
接下来直接算所有组合的绩效指标:
python复制annual_ret = port_rets.mean(axis=1) * 252
annual_vol = port_rets.std(axis=1) * np.sqrt(252)
sharpe_all = (annual_ret - rf) / annual_vol
# 最大回撤
def max_drawdown(series):
cum = np.cumprod(1 + series)
peak = np.maximum.accumulate(cum)
dd = (cum - peak) / peak
return dd.min()
mdd_all = np.array([max_drawdown(row) for row in port_rets])
这里有一个取舍。上面最大回撤我用了Python循环,因为np.maximum.accumulate本质上需要逐行处理。如果你的候选组合量级在百万级别,这一步会变成瓶颈。一个优化办法是把所有组合的净值矩阵一次性算出来,然后做三次方累积最大值操作,但那样内存占用会高很多。两万量级用循环其实完全可以接受。
实际测下来,我机器上生成两万个组合、算完年化收益/波动/夏普/最大回撤全部指标大概只需要一两秒。这速度在之前用循环回测的时代是不可想象的。
4.4 用vectorbt把最优组合跑成完整回测
权重扫描完成之后,我们得到了一个“最优权重”best_w。接下来做一个更有说服力的事:把这个组合的收益序列丢进vectorbt,生成一个完整回测,算出真正的绩效指标、画资金曲线。
不同版本的vectorbt,API略有差异。我使用的是0.26.x的新语法,Portfolio.from_returns可以直接从收益序列构造组合:
python复制import vectorbt as vbt
best_w = w_samples[np.argmax(sharpe_all)]
best_return_series = returns @ best_w
pf = vbt.Portfolio.from_returns(
best_return_series,
price=price.loc[returns.index[0]:].iloc[:, 0],
init_cash=100000,
fees=0.0003
)
print(pf.stats())
pf.plot().show()
如果读者使用的是旧版本,from_returns不可用,可以改用from_orders配上目标权重来模拟调仓。我在第6章会专门讲版本差异。
price参数在这里不是必须的,但传入后,vectorbt可以更准确地将收益率转换为资金曲线,也能模拟出场价格波动。注意这里我传的是一个价格序列,如果你的资产池是多标的,这里需要换成组合的净值曲线,而不是单只ETF的价格。
4.5 从热力图里看权重与夏普的关系
网格搜索最大的好处是能可视化。你不仅知道哪个权重好,还能看到整个“权重-绩效”地图的形状,这对理解模型的稳健性非常有帮助。
以3资产为例,把样本内夏普画成三角热力图,颜色越亮代表夏普越高。这时候你会很容易发现:最优区域到底是尖尖的一个点,还是一整片高原地带。如果是一片高原,说明很多不同权重组合都能达到相近效果,这代表系统对权重选择不敏感,更适合实盘;如果只是一个点,那就说明模型对参数极其敏感,实盘中稍微偏离就会掉出高绩效区,这种优化结果要谨慎。
我每次做组合优化的固定动作:先画出权重分布和绩效热力图,再去看数值最优解。数值最优解往往只是信息量最小的一部分。
5. 样本外验证:别让优化结果停留在历史数据里
5.1 固定切分和滚动窗口的区别
前文用2021到2023做样本内、2024做样本外,这个做法直观,但有一个问题:它只验证了“一个历史时点优化的权重”在“另一个时点的表现”。而实际投资是一个持续过程,你可能每个季度都要重新调仓。
滚动窗口优化更贴近实战。做法是:
- 每次用过去252个交易日(约一年)的收益数据,重新算权重
- 按调仓周期(比如每周、每月、每季度)持有这个权重
- 下个周期回来后,用最新252天重新优化
用代码表达就是:
python复制window = 252
rebalance_days = returns.index[::30] # 每30个交易日调仓一次
weights_series = []
for i in range(window, len(returns)):
if returns.index[i] not in rebalance_days:
continue
train_returns = returns.iloc[i-window:i]
mu_roll = train_returns.mean() * 252
cov_roll = train_returns.cov() * 252
# 用同样的优化函数求解
# ...
滚动窗口的成本更高,因为你每次都要重新做一次优化,但它对策略的“持续有效性”有一个更真实的检验。如果滚动结果和固定切分结果差异很大,说明市场结构在你研究的区间内发生了不小的变化,固定切分的结论要打问号。
5.2 再平衡和交易成本
组合优化的权重不会自己维持不变。标的涨跌会导致权重漂移,比如创业板涨得多了,权重就从30%漂移到35%。如果不管它,你实际持有的组合风险特征很快就会偏离当初的优化目标。
所以,调仓频率是一个需要权衡的参数:
- 调得越勤,权重越贴近目标,但交易成本和摩擦越大
- 调得越疏,成本越低,但风险暴露偏移越大
我在vectorbt里做调仓模拟时,会把每次调仓的换手率算出来,并计入成本:
python复制def apply_rebalance(weight_history):
turnover = 0
for i in range(1, len(weight_history)):
turnover += np.abs(weight_history[i] - weight_history[i-1]).sum()
return turnover * fee_rate
vectorbt的from_orders可以更精细地模拟换手成本。核心思路是:调仓时卖出旧权重、买入新权重,每一笔都按成交金额收佣金,如果还有滑点设置,一并加进去。
这部分如果想认真做,我会建议多测几个调仓频率:每周、每月、每季度各跑一遍,看净收益差多少。很多策略的“优化优势”其实全被频繁调仓的交易成本吃掉了,这是最容易被忽略的坑。
5.3 与等权重基准对比
组合优化做了半天,至少要和最简单的基准比一比才有意义。我的基准通常选两个:
- 等权重组合(每个标的平均分配)
- 单只ETF中表现最好的那个
等权重组合尤其值得对比。它在没有任何优化的情况下,天然拥有分散化优势,经常能跑赢复杂的优化结果。如果你优化出来的组合在样本外打不过等权重,那说明你优化出来的那点收益优势可能只是过拟合噪音。
python复制equal_weight_series = returns.mean(axis=1) # 等权重收益
optimized_series = returns @ best_w
# 对比累计净值
(1 + equal_weight_series).cumprod().plot(label="Equal Weight")
(1 + optimized_series).cumprod().plot(label="Optimized")
我遇到过不少例子,样本内最大夏普组合跑得飞起,样本外却被等权重按在地上摩擦。原因很简单:最大夏普会把权重集中在近期涨得好的资产上,而近期的强势资产在样本外往往发生反转。
这一点,无论是固定切分还是滚动窗口,结论都一样:优化框架带来的边际价值,必须经过基准检验才算数。
6. 实操中让人抓狂的几类细节问题
6.1 协方差矩阵求逆的数值陷阱
很多目标函数里会用到协方差矩阵的逆矩阵,比如我们要算边际风险的时候。但协方差矩阵在标的数量接近样本数量时,会变成奇异矩阵或近似奇异。
典型报错是LinAlgError: Singular matrix,或者优化器输出权重震荡得厉害。
解决办法有几种,按我推荐的顺序排列:
- 增加样本量:用更长周期的日收益率
- 用收缩估计:把样本协方差朝单位矩阵方向收缩,降低极端值影响
- 加一个小正则项:
cov + np.eye(n) * epsilon,其中epsilon一般取对角线均值的万分之一
我实际测试中,加正则项是最省事的。注意正则项不要加太大,否则等于把协方差矩阵变成了单位矩阵,相关性信息就全丢了。
另外,Min-Variance组合对协方差矩阵的偏导非常敏感。如果在样本内优化出的权重在样本外波动剧烈,我会直接怀疑协方差矩阵条件数太大,而不是怀疑市场本身。
6.2 优化目标对输入参数极度敏感
这是最磨人、也最容易被忽视的问题。最大夏普目标的输入是预期收益 μ 和协方差矩阵 Σ。μ 是历史均值估计出来的,误差非常大。你稍微改变一下样本区间,μ就可能从正的变成负的,权重自然就会大变。
我建议做三件事:
- 把样本内区间换成不同的长度各跑一遍,比如60天、120天、252天,看看权重稳不稳
- 把协方差矩阵的收缩强度调一下,再看权重
- 观察权重变化的幅度,如果一只资产权重从40%跳到80%,这个优化结果在实操中就没法用
这种情况下,我会更倾向于风险平价或者等权重。它们对输入参数的敏感性低得多,虽然理论收益上限不一定比得上最大夏普,但不会让人在实盘里整天提心吊胆。
6.3 高维资产池下的网格爆炸与替代方案
前面说过,网格搜索在低维组合里非常好用,但资产数量一旦多起来,组合空间会指数增长。5只资产的Dirichlet抽样还能覆盖,10只资产的时候,两万个样本在10维单纯形里已经是稀疏到像在大海里撒盐。
高维场景下有几个替代方案:
- 先用聚类或相关性分组,把资产压缩成几个大类(比如股票、债券、商品),再在大类内部做二次配置
- 用遗传算法、模拟退火这类全局优化方法,同时保留向量化回测的验证框架
- 用factor model降维,把协方差矩阵的结构用少数几个主成分解释
在我个人的项目里,最常用的还是“先分组、再配置”的层级式做法。它的好处是你能看到每一层的逻辑,出了问题也知道去哪里排查。
6.4 权重漂移与停牌、涨跌停的现实约束
回测是理想化的,但实盘有涨跌停、停牌、大额申赎折溢价这些限制。尤其是停牌,你在调仓日想卖掉一只停牌的ETF,根本卖不掉。这个现实约束在回测里不处理,策略实盘就容易出现偏差。
vectorbt里应对停牌的方法,无非是在信号侧做过滤:如果一个标的在调仓日成交量为0或停牌,就用前一天的权重补上,同时把这部分调仓需求顺延到下一个交易日。处理方式不复杂,但如果没有意识到这层,回测结果就会偏乐观。
关于权重漂移,我的建议是:把再平衡阈值设成一个区间,比如权重偏离目标超过5个百分点才调仓,这样既控制成本,又不会让组合过度暴露。
6.5 版本迁移:从Portfolio.from_signals到from_returns
vectorbt的API在0.26版本之后有比较大的变化,很多老项目迁移时会卡住。我给读者一个简单的对照:
| 场景 | 旧版本写法 | 0.26.x写法 |
|---|---|---|
| 从信号构造组合 | vbt.Portfolio.from_signals(price, entries, exits) |
vbt.Portfolio.from_signals(price, entries, exits)(基本保留) |
| 从收益序列构造组合 | 无直接API,只能用from_orders | vbt.Portfolio.from_returns(returns) |
如果你在用很老的0.9.x或1.0.x,建议先升级到0.26.x,官方API文档按版本归档,升级后尽量统一用新写法。如果你就是那类不想折腾升级的人,那from_orders会是更通用的选择,因为它在老版本里也存在,只是参数更啰嗦。
我在做组合优化案例的时候,最终固定下来的流程是:
- 用
from_orders构建一次准确的调仓回测,模拟真实下单过程,把手续费和滑点都算进去 - 用
from_returns做快速绩效比较,在大量候选权重中筛出潜在可行解 - 等候选权重数量收敛到个位数时,再回头用
from_orders跑精细回测
这个流程兼顾了速度和准确性,也是我在实际项目中反复验证后觉得最顺手的打法。
最后说一点我对“组合优化”这个事的个人看法。做了这么多次优化、看了这么多权重分布图之后,我现在越来越不迷信“最优”这两个字。所谓最优权重,永远是针对某个历史样本、某个目标函数、某个约束集合的最优,换一个市场环境就不作数。反而是那些“在一大片参数区域里都不错”的稳健解,才是真正值得在实盘里长期持有的组合。vectorbt给你的是一个快速验证所有假设的工具,但最终要控制的是你自己的贪婪和过度自信。如果你能把“把搜索范围从单个最优解扩展到一个稳健区域”这个思路想清楚,这篇文章的核心价值你就已经拿到了。
