学生用案例操作手册|量化多因子策略(FF5 框架、A 股、零编程)

本手册面向零基础同学,指导你“自己到 CSMAR 抓数 + 用 Excel/WPS 完成横截面多因子小案例”。你将独立完成:数据下载、口径对齐、因子工程(winsor→标准化→规模中性化→合成)、Rank‑IC 与分组评估。全流程不写代码。

为兼容工具差异,每一步均提供两套做法:

注:学校没有 CSMAR 行业分类权限。本手册附“静态行业映射表”,请手工建表使用;或退化为“全市场标准化”。


0. 目标产出与所需数据

建议样本:20 只 A 股 × 24–36 个月月度数据(名单见第 1 节)。


1. 样本股票与行业映射(手工建表,替代 CSMAR 行业)

请新建工作表 Industry_Lookup,建立两列:TickerIndustry,手工录入下表(或复制粘贴):

Ticker NameCN Industry
600519.SH 贵州茅台 食品饮料
000858.SZ 五粮液 食品饮料
603288.SH 海天味业 食品饮料
600036.SH 招商银行 银行
601318.SH 中国平安 保险
000333.SZ 美的集团 家电
300750.SZ 宁德时代 电力设备
002594.SZ 比亚迪 汽车
601012.SH 隆基绿能 电力设备
600309.SH 万华化学 化工
600276.SH 恒瑞医药 医药生物
300760.SZ 迈瑞医疗 医疗器械
002415.SZ 海康威视 电子/安防
600031.SH 三一重工 机械
601888.SH 中国中免 商贸零售
601899.SH 紫金矿业 有色金属
601668.SH 中国建筑 建筑
002714.SZ 牧原股份 农林牧渔
000002.SZ 万科A 房地产
600660.SH 福耀玻璃 汽车零部件

若你不想使用行业:把 Industry 一列全部填为 All,后续就做“全市场标准化”。

CSMAR 文本筛选用股票代码列表(直接粘贴)

600519.SH
000858.SZ
603288.SH
600036.SH
601318.SH
000333.SZ
300750.SZ
002594.SZ
601012.SH
600309.SH
600276.SH
300760.SZ
002415.SZ
600031.SH
601888.SH
601899.SH
601668.SH
002714.SZ
000002.SZ
600660.SH
600519
000858
603288
600036
601318
000333
300750
002594
601012
600309
600276
300760
002415
600031
601888
601899
601668
002714
000002
600660

2. 从 CSMAR 抓取数据(你自己操作)

以下为常见的 CSMAR 客户端菜单路径。不同高校界面略有差异,请以本地为准。

2.1 月度交易与市值(TRD_Mnth)

说明:优先用 Mretwd(复权月收益);若没有,将用“前复权收盘价”自算。

2.2 年度财务(资产负债表 + 利润表,合并报表口径)

小贴士:如果“归母权益/净利润”字段命名不同,请选择含义相同的口径;确保为“合并报表、年度”。


3. 新建工作簿与导入原始表

新建 MF-FF5-Ashare.xlsx,创建以下工作表并导入数据。

将三张表都转换为“表格”(Ctrl+T),命名:

合并 BS_Annual.xlsx 与 IS_Annual.xlsx(生成 Financials_Annual / TblFin)

目标:把两份“年度财务”合成一张标准表,列为:Ticker, FiscalYear, BookEquity, TotalAssets, NetIncome,并转为表对象 TblFin

1) 导入两张源表并规范列

2) 在两张表中提取 FiscalYear

3) 以资产负债表为“底表”,添加净利润

4) 去重与排序

5) 质检要点

完成后,TblFin 即为后续计算 AG 的基础表。

可直接复制的关键公式


4. 预处理与口径对齐(避免前视)

4.1 在 Data 表创建辅助列

A 版(XLOOKUP):

=IFERROR(XLOOKUP([@Ticker], TblInd[Ticker], TblInd[Industry], "All"), "All")

B 版(INDEX/MATCH):

=IFERROR(INDEX(TblInd[Industry], MATCH([@Ticker], TblInd[Ticker], 0)), "All")

A 版(动态数组,无需辅助列):

=LET(
  tk, [@Ticker],
  dt, [@Date],
  rnext, XLOOKUP(dt, FILTER(TblData[Date], TblData[Ticker]=tk),
                     FILTER(TblData[Mret], TblData[Ticker]=tk), , 1),
  rnext
)

B 版(OFFSET + 分组判断):

=IF(AND([@Ticker]=OFFSET([@Ticker],-1,0), [@Date]>OFFSET([@Date],-1,0)),
     OFFSET([@Mret],-1,0),
     NA())

如果没有 Mret 字段:用“前复权收盘价”计算 Ret = AdjClose/上期AdjClose - 1,再右移一月得到 Ret_t1

4.2 在 Financials_Annual 计算年度投资率 AG

新增列 AG(按同一股票相邻年份):

A 版(XLOOKUP+FILTER):

=LET(
 tk, [@Ticker],
 yr, [@FiscalYear],
 lastA, XLOOKUP(yr-1, FILTER(TblFin[FiscalYear], TblFin[Ticker]=tk),
                     FILTER(TblFin[TotalAssets], TblFin[Ticker]=tk)),
 IFERROR([@TotalAssets]/lastA - 1, NA())
)

B 版(辅助键列):

=IFERROR([@TotalAssets] / INDEX(TblFin[TotalAssets], MATCH([@Ticker]&([@FiscalYear]-1), TblFin[Key], 0)) - 1, NA())

4.3 将上一年度财务映射到月度 Data

TblData 中新增三列:BookEquityNetIncomeAG(均来自 Year_Fin 对应年度)

A 版:

= XLOOKUP(1, (TblFin[Ticker]=[@Ticker])*(TblFin[FiscalYear]=[@Year_Fin]), TblFin[BookEquity])
= XLOOKUP(1, (TblFin[Ticker]=[@Ticker])*(TblFin[FiscalYear]=[@Year_Fin]), TblFin[NetIncome])
= XLOOKUP(1, (TblFin[Ticker]=[@Ticker])*(TblFin[FiscalYear]=[@Year_Fin]), TblFin[AG])

B 版(用 Key):

=INDEX(TblFin[BookEquity], MATCH([@KeyFin], TblFin[Key], 0))
=INDEX(TblFin[NetIncome], MATCH([@KeyFin], TblFin[Key], 0))
=INDEX(TblFin[AG], MATCH([@KeyFin], TblFin[Key], 0))

检查点:随机抽 1 只股票,确认 2023 年财报值被用于 2024 年 1–12 月,避免前视。


5. 构造原始描述子(Factors_Raw)

TblData 的关键列复制/引用到 Factors_RawDate, Ticker, Industry, MktCap, LogMktCap, Ret_t1, BookEquity, NetIncome, AG

新增三个描述子(“越大越好”):

BM     = IFERROR([@BookEquity]/[@MktCap], NA())
ROE    = IFERROR([@NetIncome]/[@BookEquity], NA())
AG_pos = IFERROR(-1*[@AG], NA())

注意:若 BookEquity <= 0ROE 可能异常,后续 winsor 会处理极端值。


6. 行业内去极值(1%–99% Winsor)

Standardize 表拷入 Factors_Raw 的列,并对每个 Date × Industry 分组分别处理三列:BMROEAG_pos

A 版(FILTER):

=LET(
 d, [@Date], ind, [@Industry],
 x, FILTER(Standardize[BM], (Standardize[Date]=d)*(Standardize[Industry]=ind)),
 P1, PERCENTILE.INC(x, 0.01),
 P99, PERCENTILE.INC(x, 0.99),
 MIN(MAX([@BM], P1), P99)
)

B 版(数组公式,输入后按 Ctrl+Shift+Enter;WPS 经典版同理):

=MIN( MAX([@BM],
  PERCENTILE.INC(IF((Standardize[Date]=[@Date])*(Standardize[Industry]=[@Industry]), Standardize[BM]), 0.01)),
  PERCENTILE.INC(IF((Standardize[Date]=[@Date])*(Standardize[Industry]=[@Industry]), Standardize[BM]), 0.99))
)

ROEAG_pos 同样处理,得到 BM_win, ROE_win, AGpos_win

如果你将 Industry 全设为 “All”,上述“组内”即为“全市场”。


7. 行业内秩→正态分位标准化

将 winsor 后的数值转为近似标准正态分数 zz。以 BM_win 为例:

A 版:

=LET(
 d, [@Date], ind, [@Industry],
 x, FILTER(Standardize[BM_win], (Standardize[Date]=d)*(Standardize[Industry]=ind)),
 r, RANK.AVG([@BM_win], x, 0),
 n, ROWS(x),
 NORM.S.INV((r-0.5)/n)
)

B 版(CSE):

=LET(
 r, RANK.AVG([@BM_win],
      IF((Standardize[Date]=[@Date])*(Standardize[Industry]=[@Industry]), Standardize[BM_win]), 0),
 n, COUNTIFS(Standardize[Date],[@Date], Standardize[Industry],[@Industry]),
 NORM.S.INV((r-0.5)/n)
)

ROE_winAGpos_win 同理,得到 z_BMz_ROEz_AGpos


8. 规模中性化(对数市值残差)

对每个 Date,将 z_*LogMktCap 做单变量回归,取残差:z=zβlog(MktCap)z^{\perp} = z - \beta \cdot \log(\text{MktCap}),其中 β=Cov(z,logM)Var(logM)\beta = \frac{\mathrm{Cov}(z, \log M)}{\mathrm{Var}(\log M)}

A 版(全市场维度):

=LET(
 d, [@Date],
 z, FILTER(Standardize[z_BM], Standardize[Date]=d),
 s, FILTER(Standardize[LogMktCap], Standardize[Date]=d),
 beta, COVARIANCE.S(z, s)/VAR.S(s),
 [@z_BM] - beta*[@LogMktCap]
)

B 版(CSE):

=[@z_BM] - (
   COVARIANCE.S(
     IF(Standardize[Date]=[@Date], Standardize[z_BM]),
     IF(Standardize[Date]=[@Date], Standardize[LogMktCap])
   )
   / VAR.S(IF(Standardize[Date]=[@Date], Standardize[LogMktCap]))
) * [@LogMktCap]

z_ROEz_AGpos 同理,得到 z_BM_perpz_ROE_perpz_AGpos_perp

进阶:也可选择在“Date × Industry”维度做规模中性化(更严谨,但样本少时不稳)。


9. 合成综合分数(等权)

Composite 表计算:

Score = AVERAGE([@z_BM_perp], [@z_ROE_perp], [@z_AGpos_perp])

课堂默认等权;IR 加权、风险平价等进阶放到后续课程。


10. 研究级评估(一):每月 Rank‑IC

Composite 表按 Date 组内分别对 ScoreRet_t1 排秩,再求相关。

新增辅助列:

A 版:

=LET(d, [@Date],
 x, FILTER(Composite[Score], Composite[Date]=d),
 RANK.AVG([@Score], x, 0))

B 版(CSE):

=RANK.AVG([@Score], IF(Composite[Date]=[@Date], Composite[Score]), 0)

Research_Eval 建立“唯一月份”列表:

对每个 Month 计算 Rank‑IC:

A 版:

=CORREL(
  FILTER(Composite[Rank_Score], Composite[Date]=MonthCell),
  FILTER(Composite[Rank_Ret],   Composite[Date]=MonthCell)
)

B 版(透视替代):

汇总:


11. 研究级评估(二):五分位分组与多空组合回报

Composite 表打分组标签(每个 Date 组内按 Score 分 5 组):

A 版:

Perc_Score = LET(d,[@Date], x, FILTER(Composite[Score], Composite[Date]=d),
                 PERCENTRANK.INC(x, [@Score]))
Bucket     = 1 + INT(5 * [@Perc_Score])

B 版(CSE):

Perc_Score = PERCENTRANK.INC(IF(Composite[Date]=[@Date], Composite[Score]), [@Score])
Bucket     = 1 + INT(5 * [@Perc_Score])

Research_Eval 汇总每月各组次期收益:

Ret_Qk_t   = AVERAGEIFS(Composite[Ret_t1], Composite[Date], MonthCell, Composite[Bucket], k)
LongShort  = Ret_Q5_t - Ret_Q1_t

累计曲线:

Cum_Q5 = PRODUCT(1 + range_Q5) - 1
Cum_Q1 = PRODUCT(1 + range_Q1) - 1
Cum_LS = PRODUCT(1 + range_LS) - 1

或逐期累计列:

=IF(ROW()=ROW(first), 1+Ret_Q5_t, previous_cell*(1+Ret_Q5_t))

绘图:插入折线图展示 Rank‑IC、Q1..Q5、Q5−Q1 累计收益。


12. 质量控制清单(交作业前逐条自检)


15. 关键公式速查(复制即用)

A版:=LET(tk,[@Ticker],yr,[@FiscalYear],lastA,XLOOKUP(yr-1,FILTER(TblFin[FiscalYear],TblFin[Ticker]=tk),FILTER(TblFin[TotalAssets],TblFin[Ticker]=tk)), IFERROR([@TotalAssets]/lastA-1,NA()))
B版:=IFERROR([@TotalAssets]/INDEX(TblFin[TotalAssets], MATCH([@Ticker]&([@FiscalYear]-1), TblFin[Key], 0))-1, NA())
A版:=XLOOKUP(1,(TblFin[Ticker]=[@Ticker])*(TblFin[FiscalYear]=[@Year_Fin]), TblFin[BookEquity])
B版:=INDEX(TblFin[BookEquity], MATCH([@KeyFin], TblFin[Key], 0))
A版:=LET(tk,[@Ticker],dt,[@Date], XLOOKUP(dt, FILTER(TblData[Date],TblData[Ticker]=tk), FILTER(TblData[Mret],TblData[Ticker]=tk), , 1))
B版:=IF(AND([@Ticker]=OFFSET([@Ticker],-1,0), [@Date]>OFFSET([@Date],-1,0)), OFFSET([@Mret],-1,0), NA())
A版:=LET(d,[@Date],ind,[@Industry],x,FILTER(Standardize[BM],(Standardize[Date]=d)*(Standardize[Industry]=ind)), MIN(MAX([@BM],PERCENTILE.INC(x,0.01)), PERCENTILE.INC(x,0.99)))
B版(CSE):=MIN(MAX([@BM], PERCENTILE.INC(IF((Standardize[Date]=[@Date])*(Standardize[Industry]=[@Industry]),Standardize[BM]),0.01)), PERCENTILE.INC(IF((Standardize[Date]=[@Date])*(Standardize[Industry]=[@Industry]),Standardize[BM]),0.99))
A版:=LET(d,[@Date],ind,[@Industry],x,FILTER(Standardize[BM_win],(Standardize[Date]=d)*(Standardize[Industry]=ind)), r,RANK.AVG([@BM_win],x,0), n,ROWS(x), NORM.S.INV((r-0.5)/n))
B版(CSE):=LET(r,RANK.AVG([@BM_win], IF((Standardize[Date]=[@Date])*(Standardize[Industry]=[@Industry]),Standardize[BM_win]),0), n,COUNTIFS(Standardize[Date],[@Date],Standardize[Industry],[@Industry]), NORM.S.INV((r-0.5)/n))
A版:=LET(d,[@Date], z,FILTER(Standardize[z_BM],Standardize[Date]=d), s,FILTER(Standardize[LogMktCap],Standardize[Date]=d), beta,COVARIANCE.S(z,s)/VAR.S(s), [@z_BM]-beta*[@LogMktCap])
B版(CSE):=[@z_BM]-( COVARIANCE.S(IF(Standardize[Date]=[@Date],Standardize[z_BM]), IF(Standardize[Date]=[@Date],Standardize[LogMktCap])) / VAR.S(IF(Standardize[Date]=[@Date],Standardize[LogMktCap])) )*[@LogMktCap]
A版:=CORREL(FILTER(Composite[Rank_Score], Composite[Date]=MonthCell), FILTER(Composite[Rank_Ret], Composite[Date]=MonthCell))
A版:Perc=LET(d,[@Date],x,FILTER(Composite[Score],Composite[Date]=d), PERCENTRANK.INC(x,[@Score])) ; Bucket=1+INT(5*Perc)
B版(CSE):Perc=PERCENTRANK.INC(IF(Composite[Date]=[@Date],Composite[Score]), [@Score]) ; Bucket=1+INT(5*Perc)