截面动量 · 横截面排序选股
直觉
前面两条均线是时间序列动量(和自己过去比)。截面动量换了个角度:不预测绝对涨跌,而是在同一时刻,把所有股票按过去涨幅排序,做多最强的、做空最弱的。它不赌大盘涨跌,只赌「强者恒强、弱者恒弱」的相对排序。
策略规则
每个交易日 :
- 算每只股票过去 期的动量(如 6 个月收益);
- 按动量横截面排序(
rank(axis=1)); - 做多排名最高的 只(等权)、做空排名最低的 只(等权);
- 多空对冲,组合收益 = 多头收益 − 空头收益。
「人话」解释:为什么「多空对冲」而不是只做多?
只做多「最强的」依然暴露在大盘涨跌上——大盘暴跌时最强的也难幸免。 多空对冲(long-short)把市场敞口尽量抵消,剩下的纯粹是「强者减弱者」的相对超额,这正是 模块 0 说的 Alpha。 这也是为什么截面动量是因子投资(模块 5)的经典原型。
:::caution 关于时序对齐(防前视)
动量用「过去 期」的收益排序,决策发生在 收盘;实际持仓要用 收盘的信息去吃 的收益。代码里我们对动量做 shift(1),与引擎 shift(1) 防前视的约定一致。
:::
可运行案例:50 只合成股票的截面动量
读取 sp500_universe_daily.csv(50 只合成股票的宽表),构造 6 月动量、取前后各 10 只做市中性多空,看资金曲线与指标。
import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
prices = pd.read_csv('/data/sp500_universe_daily.csv', parse_dates=['date']).set_index('date')
n_stocks = prices.shape[1]
nlong = 10
# 6 月动量(126 日收益),shift(1) 防前视
mom = prices.pct_change(126).shift(1)
ranks = mom.rank(axis=1, method='first')
long_mask = ranks > (n_stocks - nlong) # 前 nlong 名
short_mask = ranks <= nlong # 后 nlong 名
ret1 = prices.pct_change()
long_ret = (ret1 * long_mask).sum(axis=1) / nlong
short_ret = (ret1 * short_mask).sum(axis=1) / nlong
ls_ret = (long_ret - short_ret).fillna(0.0)
equity = (1 + ls_ret).cumprod()
mkt = (1 + ret1.mean(axis=1).fillna(0.0)).cumprod()
print(quant.performance_summary(equity, ls_ret, freq=252).round(3))
plt.figure(figsize=(9, 3.8))
plt.plot(equity, color='crimson', label='多空截面动量(LS)')
plt.plot(mkt, color='#94a3b8', label='等权全市场(对比)')
plt.title('截面动量多空组合 vs 市场'); plt.ylabel('净值'); plt.legend()
plt.tight_layout(); plt.show()
动手改一改:拖动参数即时看回测
拖动动量窗口与多空持仓数——窗口越短越可能从「动量」翻转为「反转」;持仓数越少波动越大。
# ParamSlider(SSR 预览)
参数: LOOKBACK, NLONG
import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
prices = pd.read_csv('/data/sp500_universe_daily.csv', parse_dates=['date']).set_index('date')
n_stocks = prices.shape[1]
mom = prices.pct_change(LOOKBACK).shift(1) # 防前视
ranks = mom.rank(axis=1, method='first')
long_mask = ranks > (n_stocks - NLONG)
short_mask = ranks <= NLONG
ret1 = prices.pct_change()
long_ret = (ret1 * long_mask).sum(axis=1) / NLONG
short_ret = (ret1 * short_mask).sum(axis=1) / NLONG
ls = (long_ret - short_ret).fillna(0.0)
eq = (1 + ls).cumprod()
print(quant.performance_summary(eq, ls, freq=252).round(3))
plt.figure(figsize=(9, 3.6))
plt.plot(eq, color='crimson', label=f'LS 动量({LOOKBACK}日, 前{NLONG})')
plt.ylabel('净值'); plt.legend(fontsize=9); plt.tight_layout(); plt.show()
小结
- 截面动量 = 横截面排序,做多最强、做空最弱,市场中性;
- 多空对冲剥离大盘 Beta,追求纯 Alpha;
- 防前视:排序用
shift(1)后的动量;这是模块 5 因子投资的骨架。