功能

横截面策略

每期为资产池排序、轮动至领先资产,并通过五分位组合测试因子

如何为资产池排序,并交易其中最强和最弱的品种?本页用 Python 回测横截面动量轮动:每月按过去一年的收益为 37 只 ETF 排序,持有领先者,并在成分随时间变化的资产池中,通过五分位组合检验因子。

Rotate monthly into the five ETFs with the strongest 12-1 momentum
symbols = [
    "XLB", "XLE", "XLF", "XLI", "XLK", "XLP", "XLU", "XLV", "XLY", "XLRE", "XLC",
    "EWA", "EWC", "EWG", "EWH", "EWJ", "EWU", "EWZ", "EWW", "EWY", "EWT", "EWS",
    "EWL", "EWQ", "EWP", "EWI", "EWD", "EWN", "FXI", "INDA", "EIDO",
    "TLT", "IEF", "GLD", "SLV", "DBC", "VNQ",
]
close = vbt.YFData.pull(symbols, start="2007-01-01", end="2025-01-01").close

month = pd.Series(close.index.month, index=close.index)
dates = close.index[month != month.shift(-1)]  
momentum = (close.shift(21) / close.shift(252) - 1).loc[dates]  
top = momentum.rank(axis=1, ascending=False) <= 5
eligible = momentum.notna()  

def simulate(weights):
    return vbt.PF.from_orders(
        close,
        size=weights.loc["2008":].reindex(close.index),  
        size_type="targetpercent",
        group_by=True,
        cash_sharing=True,
        call_seq="auto",
        fees=0.001,
    )

pfs = {
    "Top 5 momentum": simulate(top.div(top.sum(axis=1), axis=0)),
    "Equal weight": simulate(eligible.div(eligible.sum(axis=1), axis=0)),
}
metrics = ["total_return", "sharpe_ratio", "max_dd"]
pd.DataFrame({k: pf.stats(metrics) for k, pf in pfs.items()}).astype(float).round(2)
                  Top 5 momentum  Equal weight
Total Return [%]           84.61        152.43
Sharpe Ratio                0.35          0.45
Max Drawdown [%]           37.41         51.90

2008 至 2024 年,每月持有最强五只 ETF 的收益为 85%,而等权持有全部合资格 ETF 的收益为 152%,前者回撤更小。在这里,动量降低了风险,但没有增加收益。

同一工作流也接受自己的得分:技术指标、基本面因子或模型预测。提供按日期和交易品种排列的得分表,选择持有数量,再比较得到的投资组合。动量只是其中一个例子。

排序与选择

排序是在每个交易品种占一列的表格上执行 pandas 操作,因此 rank(axis=1) 可一次为所有日期排序,rank <= 5 选择领先者。某日没有数据的品种得到 NaN,不参与排序。选中结果转化为权重后,可像上例一样交给 vbt.PF.from_orders,以共享现金按目标百分比交易;也可转化为 vbt.PF.from_signals 的信号。投资组合优化页用 vbt.PFO.from_allocate_func 按任意计划计算相同权重。负权重表示做空,因此多空组合的权重是领先组权重减去落后组权重。对于数千个品种,可按时间切分得分表,逐期排序,再通过 split_apply 合并。每期保留完整资产池,使股票仍与全部同类比较,同时减少中间排序表的内存占用。

若整个选择在 K 线开始前已知,排序表就足够。若入场来自各品种自身信号,限制又依赖已有持仓,例如最多五个未平仓头寸,则决策需要投资组合状态,参见事件驱动回测。

因子研究

有效因子的排名应能区分未来收益,因此可按排名将资产池分成五组并比较。每个五分位组都是同一投资组合内的一组列:

Backtest the five momentum quintiles in one simulation
quintile = np.ceil(momentum.rank(axis=1, pct=True) * 5)
members = {q: quintile == q for q in range(1, 6)}
q_weights = pd.concat(
    {q: m.div(m.sum(axis=1), axis=0) for q, m in members.items()},
    axis=1,
    names=["quintile"],
)
q_close = pd.concat({q: close for q in range(1, 6)}, axis=1, names=["quintile"])
q_pf = vbt.PF.from_orders(
    q_close,
    size=q_weights.loc["2008":].reindex(close.index),
    size_type="targetpercent",
    group_by="quintile",  
    cash_sharing=True,
    call_seq="auto",
    fees=0.001,
)
q_pf.stats(metrics, agg_func=None).astype(float).round(2)
          Total Return [%]  Sharpe Ratio  Max Drawdown [%]
quintile
1                   132.68          0.38             57.79
2                    67.94          0.29             55.03
3                   100.37          0.36             58.13
4                   136.40          0.42             51.06
5                   118.17          0.42             40.87
2008 至 2024 年按月度 12-1 动量五分位构建的五个 ETF 投资组合净值 图表数据(JSON)

如果因子有效,收益应从动量最弱的第 1 组到最强的第 5 组逐步上升。这里收益没有按顺序排列,只有回撤随动量增强而下降。排名信息系数,即每月排名与下月收益的相关性,可对此给出数值衡量:

Measure the rank information coefficient of momentum
fwd = close.loc[dates].pct_change().shift(-1)  
ic = momentum.corrwith(fwd, axis=1, method="spearman").loc["2008":].dropna()
t_stat = ic.mean() / ic.std() * np.sqrt(ic.count())
print(round(ic.mean(), 3), round(t_stat, 2), round((ic > 0).mean(), 2))
0.006 0.29 0.52

平均 IC 为 0.006,在 52% 的月份为正,与零没有明显区别。买入第 5 组、做空第 1 组且两边各分配一半资金的多空组合,同期损失约四分之一净值。VBT 没有 Alphalens 那样内置的因子分析报告,但排名、未来收益和五分位投资组合都是普通表格,因此各项统计量只需一行 pandas 代码。

若要单独检验某个因子,应先剔除它对其他特征的暴露:在每个日期将该因子对其他特征回归并保留残差,或减去其所在组(例如行业)的均值。指标表达式提供 cs_rank、cs_demean 和 cs_rescale,用于排序、组内去均值及重新缩放。cs_rank 输出百分位排名,在合资格品种数量变化时很有用。这些函数也用于技术指标页中的 WorldQuant 101 Alphas。

动态资产池

加入或退出资产池的品种在整个时期始终保留在表中,加入前和退出后填充 NaN,因此资产轴不会变化,每次模拟都保持向量化。这里的动量还要求一年历史,新的 ETF 在满足条件前不会入选。对于股票指数,时点成分表会在各股票非成分股期间屏蔽它们,避免只用今天仍存续的股票回测,具体见真实回测。一次只持有一个品种的策略,也可将所选品种的价格拼接为一条序列,并在每次切换时平仓。

只为合资格资产排序

资格条件可结合指数成员资格、足够的价格历史和自定义流动性筛选。应在排序前应用这些条件,避免不可交易资产占据选中名额。下面是三个资产在四个再平衡日的虚构得分:

Select up to two eligible assets and turn the rankings into allocations
dates = pd.to_datetime(["2025-01-31", "2025-02-28", "2025-03-31", "2025-04-30"])
scores = pd.DataFrame(
    {"A": [8, 9, 7, 8], "B": [6, 5, 8, 9], "C": [4, 10, 9, 7]},
    index=dates,
)
eligible = pd.DataFrame(
    {"A": [True, False, False, False], "B": [True, True, False, False], "C": [True, True, True, False]},
    index=dates,
)
ranks = scores.where(eligible).rank(axis=1, ascending=False, method="first")
selected = ranks <= 2
weights = selected.div(selected.sum(axis=1).replace(0, np.nan), axis=0).fillna(0.0)
pfo = vbt.PFO.from_allocations(scores.vbt.wrapper, weights)
pfo.allocations
              A    B    C
2025-01-31  0.5  0.5  0.0
2025-02-28  0.0  0.5  0.5
2025-03-31  0.0  0.0  1.0
2025-04-30  0.0  0.0  0.0

二月,尽管 A 的得分高于 B,它仍退出候选。三月,C 是唯一合资格资产,获得全部目标配置。四月没有合资格资产,因此目标为全现金。method="first" 使并列得分优先选择靠前的列。随后可将配置表与价格一起模拟,并计入手续费和滑点,参见投资组合优化。

比较轮动规则

结果是否依赖持有五个品种、使用一年回看期或每月再平衡?将得分、选择和模拟步骤封装为函数,再通过参数优化比较这些选项。像开篇示例一样,在排名投资组合旁保留等权投资组合,观察选择相较于持有整个资产池带来了什么。

将因子从排名 IC 推进到实际交易组合。比较扣除成本后的表现,再检查订单,了解持仓更换频率和轮动成本。五分位投资组合有助于区分两类因子:一类能在整个资产池中区分收益,另一类的结果主要依赖少数排名靠前的品种。

教程

会员专享的投资组合优化(英文)教程使用自定义函数和优化库,在资产池中分配资金。

版权所有 © 2021–2026 Oleg Polakow。保留所有权利。

本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。