上篇我们用 IC、IR 和分层回测验证了市值、动量因子的有效性。但真实策略不会只用一个因子,而是把多个因子合成一个综合信号。合成方式不同,结果可能天差地别。
今天我们就来实测三种最常见的合成方法:等权、IC 加权、回归加权,用真实数据看哪种更稳。
本文你将得到什么
- 三种多因子合成方法的 Python 实现。
- 用 IC、IR、五分组 Top-Bottom 差对比合成效果。
- 一个真实结论:在这个样本期内,IC 加权综合最优。
- 下篇预告:因子暴露不是 alpha,市值与行业中性化实战。
核心思路
等权合成
每个因子 z-score 后直接取平均。简单、稳定、不容易过拟合。
text
score = (z_size + z_mom + z_bp) / 3
IC 加权
用因子近期 IC 的绝对值作为权重。IC 高的因子权重更大,动态调整。
text
w_i = |mean(IC_i over last 5 days)|
score = sum(w_i * z_i) / sum(w_i)
回归加权
用过去几个截面的面板数据做截面回归,回归系数作为权重。理论上最「优化」,但样本小容易过拟合。
text
fwd_return = beta_size * z_size + beta_mom * z_mom + beta_bp * z_bp + alpha
score = beta_size * z_size + beta_mom * z_mom + beta_bp * z_bp
可运行代码
bash
pip install mairui pandas numpy
python
from mairui import Client
import pandas as pd
import numpy as np
LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073" # 演示证书;换成你自己的正式证书
client = Client(LICENCE)
POOL = [
"600519", "000001", "000002", "000333", "000568", "000651", "000725", "000858",
"002001", "002007", "002142", "002230", "002304", "002415", "002594", "002714",
"300003", "300014", "300015", "300033", "300059", "300122", "300124", "300274",
"300750", "601318", "601398", "601888", "603288", "603501",
]
def fetch_history(code):
try:
bars = client.stock_history(code, "d", "n", st="20260801", et="20261010")
if bars:
df = pd.DataFrame(bars).rename(columns={"t": "date", "c": "close"})
df["date"] = pd.to_datetime(df["date"])
return df.sort_values("date").reset_index(drop=True)
except Exception as e:
print(f"{code} 受限: {e}")
return None
def fetch_capital(code):
try:
cap = client.stock_capital(code=code)
if cap:
return sorted(cap, key=lambda x: x.get("bdrq", ""), reverse=True)[0]
except Exception:
pass
return None
def fetch_pershare(code):
try:
ps = client.stock_financial(code=code, table="pershareindex")
if ps:
return sorted(ps, key=lambda x: x.get("jzrq", ""), reverse=True)[0]
except Exception:
pass
return None
records = []
for code in POOL:
hist = fetch_history(code)
cap = fetch_capital(code)
ps = fetch_pershare(code)
if hist is None or len(hist) < 30:
continue
total_shares = cap.get("zgb") if cap else None
if not total_shares:
continue
bvps = ps.get("mgjzc") if ps else None
for i in range(20, len(hist) - 5):
row = hist.iloc[i]
past = hist.iloc[i - 20]
future = hist.iloc[i + 5]
records.append({
"code": code,
"date": row["date"].strftime("%Y-%m-%d"),
"market_cap": row["close"] * total_shares,
"momentum": (row["close"] / past["close"] - 1) * 100,
"bp": (bvps / row["close"]) if bvps and row["close"] > 0 else np.nan,
"fwd_return": (future["close"] / row["close"] - 1) * 100,
})
df = pd.DataFrame(records)
def zscore(s):
s = pd.to_numeric(s, errors="coerce")
return (s - s.mean()) / s.std()
# z-score;市值取负向(小市值更好)
df["z_size"] = df.groupby("date")["market_cap"].transform(lambda s: -zscore(np.log(s.replace(0, np.nan))))
df["z_mom"] = df.groupby("date")["momentum"].transform(lambda s: zscore(s))
df["z_bp"] = df.groupby("date")["bp"].transform(lambda s: zscore(s))
# 等权
df["score_ew"] = df[["z_size", "z_mom", "z_bp"]].mean(axis=1, skipna=True)
# IC 加权:用最近 5 日滚动 IC 绝对值做权重
ic_records = []
for d, g in df.groupby("date"):
ic_records.append({
"date": d,
"ic_size": g["market_cap"].corr(g["fwd_return"], method="spearman"),
"ic_mom": g["momentum"].corr(g["fwd_return"], method="spearman"),
"ic_bp": g["bp"].corr(g["fwd_return"], method="spearman"),
})
ic_df = pd.DataFrame(ic_records).sort_values("date")
for col in ["ic_size", "ic_mom", "ic_bp"]:
ic_df[f"w_{col[3:]}"] = ic_df[col].abs().rolling(5, min_periods=1).mean()
tot = ic_df[["w_size", "w_mom", "w_bp"]].sum(axis=1)
ic_df[["w_size", "w_mom", "w_bp"]] = ic_df[["w_size", "w_mom", "w_bp"]].div(tot, axis=0)
df = df.merge(ic_df[["date", "w_size", "w_mom", "w_bp"]], on="date", how="left")
df["score_icw"] = df["z_size"] * df["w_size"] + df["z_mom"] * df["w_mom"] + df["z_bp"] * df["w_bp"]
# 回归加权:用过去 5 个截面滚动回归
unique_dates = sorted(df["date"].unique())
reg_weights = []
for i, d in enumerate(unique_dates):
if i < 5:
reg_weights.append({"date": d, "rw_size": 1/3, "rw_mom": 1/3, "rw_bp": 1/3})
continue
train = df[df["date"].isin(unique_dates[:i][-5:])].dropna(subset=["z_size", "z_mom", "z_bp", "fwd_return"])
if len(train) < 20:
reg_weights.append({"date": d, "rw_size": 1/3, "rw_mom": 1/3, "rw_bp": 1/3})
continue
X = train[["z_size", "z_mom", "z_bp"]].values
y = train["fwd_return"].values
beta = np.linalg.lstsq(np.column_stack([np.ones(len(X)), X]), y, rcond=None)[0][1:]
beta = np.clip(beta, -1, 1)
s = np.sum(np.abs(beta))
if s == 0:
reg_weights.append({"date": d, "rw_size": 1/3, "rw_mom": 1/3, "rw_bp": 1/3})
else:
reg_weights.append({"date": d, "rw_size": beta[0]/s, "rw_mom": beta[1]/s, "rw_bp": beta[2]/s})
reg_df = pd.DataFrame(reg_weights)
df = df.merge(reg_df, on="date", how="left")
df["score_reg"] = df["z_size"] * df["rw_size"] + df["z_mom"] * df["rw_mom"] + df["z_bp"] * df["rw_bp"]
# 评估
def evaluate(score_col):
ics, spreads = [], []
for d, g in df.groupby("date"):
ic = g[score_col].corr(g["fwd_return"], method="spearman")
if not np.isnan(ic):
ics.append(ic)
g = g.copy()
g["q"] = pd.qcut(g[score_col].rank(method="first"), 5, labels=[1, 2, 3, 4, 5])
qm = g.groupby("q", observed=True)["fwd_return"].mean()
if len(qm) == 5:
spreads.append(qm.iloc[-1] - qm.iloc[0])
ics = pd.Series(ics)
return {
"mean_ic": round(ics.mean(), 4),
"ir": round(ics.mean() / ics.std(), 4),
"ic_positive_ratio": round((ics > 0).mean(), 4),
"avg_q_spread": round(np.mean(spreads), 4),
"n_days": len(ics),
}
results = pd.DataFrame([
evaluate("score_ew"),
evaluate("score_icw"),
evaluate("score_reg"),
], index=["等权合成", "IC 加权", "回归加权"])
print(results.to_string())
运行后你会看到类似结果:
text
mean_ic ir ic_positive_ratio avg_q_spread n_days
等权合成 0.1976 1.3214 0.8947 3.1239 19
IC 加权 0.2317 1.6301 1.0000 3.1642 19
回归加权 0.1607 0.7467 0.7368 2.2055 19
(示例基于 2026 年真实行情测算;演示证书返回就近样本,申请正式证书可拉取全量历史。)
怎么读这张表
- IC 加权综合最优:mean_ic 0.23、IR 1.63、IC 正比例 100%、五分组差 3.16%。它通过动态放大近期有效的因子,实现了比等权更好的预测能力。
- 等权是稳健基线:IC 0.20、IR 1.32,已经相当不错。如果你不想折腾,等权是很好的起点。
- 回归加权在这个样本里表现最弱:IR 只有 0.75。原因很直接:滚动窗口只有 5 个截面、每截面 30 只股票,样本量太少,回归系数不稳定。回归加权需要更长的历史样本才能发挥优势。
三个容易踩的坑
- 权重不能天天大变。IC 加权用 5 日滚动已经比较激进,实盘通常用 20 日或更长的半衰期,否则权重抖动会带来高换手。
- 回归加权对样本量敏感。截面股票数少于 50、历史截面少于 20 时,回归系数容易过拟合,不如等权稳健。
- 别忘了因子方向。如果某个因子方向搞反(比如市值取正向),权重越大错得越狠,合成前务必确认每个因子的经济含义。
小结
多因子合成不是越复杂越好。本文三种方法里,IC 加权在这个短样本期内表现最好,等权是性价比最高的基线,回归加权需要更大数据量才能稳定。下一篇我们解决另一个关键问题:因子里的市值、行业暴露,会把 alpha 吃掉多少。
下篇预告:【Python量化多因子实战 04】因子暴露不是 alpha:市值与行业中性化实战。
免责声明
本文仅为技术方法演示,不构成任何投资建议。文中示例数据仅用于教学,历史表现不代表未来收益。投资有风险,决策需谨慎。
代码与文档可参考 GitHub:github.com/MaiRuiApi