# 横截面策略 (/zh/features/strategies/cross-sectional-strategies)

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

```python title="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)]  # (1)
>>> momentum = (close.shift(21) / close.shift(252) - 1).loc[dates]  # (2)
>>> top = momentum.rank(axis=1, ascending=False) <= 5
>>> eligible = momentum.notna()  # (3)

>>> def simulate(weights):
...     return vbt.PF.from_orders(
...         close,
...         size=weights.loc["2008":].reindex(close.index),  # (4)
...         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
```

1.  每月最后一个交易日。
2.  从十二个月前到一个月前的收益。跳过最近一个月，避免其短期反转影响，且所有数值均在再平衡日之前已知。
3.  ETF 拥有一年历史后才具备入选资格。XLRE 于 2015 年成立，XLC 于 2018 年成立，因此资产池从 33 只基金增至 37 只。
4.  权重仅在再平衡日生效。其间，持仓权重随价格漂移。

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

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

## 排序与选择 \[#ranking-and-selection]

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

若整个选择在 K
线开始前已知，排序表就足够。若入场来自各品种自身信号，限制又依赖已有持仓，例如最多五个未平仓头寸，则决策需要投资组合状态，参见[事件驱动回测](/zh/features/backtesting/event-driven-backtesting/#stateful-trading-rules)。

## 因子研究 \[#factor-research]

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

```python title="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",  # (1)
...     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
```

1.  五个各含 37 列的投资组合，每个五分位组对应一个，并各自持有独立现金。

2008 至 2024 年按月度 12-1 动量五分位构建的五个 ETF 投资组合净值. [Figure data (JSON)](/assets/figures/features/strategies/momentum-quintiles.fc5d3c32b26b.json)

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

```python title="Measure the rank information coefficient of momentum"
>>> fwd = close.loc[dates].pct_change().shift(-1)  # (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
```

1.  每只 ETF 在各再平衡日之后一个月的收益。

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

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

## 动态资产池 \[#changing-universes]

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

### 只为合资格资产排序 \[#rank-only-eligible-assets]

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

```python title="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"`
使并列得分优先选择靠前的列。随后可将配置表与价格一起模拟，并计入手续费和滑点，参见[投资组合优化](/zh/features/optimization/portfolio-optimization/#backtest-your-own-weights)。

## 比较轮动规则 \[#compare-rotation-rules]

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

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

!!! info "教程"
    会员专享的[投资组合优化（英文）](https://members.vectorbt.pro/tutorials/portfolio-optimization/)教程使用自定义函数和优化库，在资产池中分配资金。
