本手册面向零基础同学,指导你“自己到 CSMAR 抓数 + 用 Excel/WPS 完成横截面多因子小案例”。你将独立完成:数据下载、口径对齐、因子工程(winsor→标准化→规模中性化→合成)、Rank‑IC 与分组评估。全流程不写代码。
为兼容工具差异,每一步均提供两套做法:
XLOOKUP
、FILTER
、动态数组)XLOOKUP
),用 INDEX/MATCH
、数据透视与辅助列实现注:学校没有 CSMAR 行业分类权限。本手册附“静态行业映射表”,请手工建表使用;或退化为“全市场标准化”。
MF-FF5-Ashare.xlsx
),含以下工作表:
Data
:月度行情与市值、行业、次期收益Financials_Annual
:年度权益、资产、净利润与投资率Factors_Raw
→ Standardize
→ Neutralize
→ Composite
→ Research_Eval
AG_pos = -AG
LogMktCap = LN(MktCap)
(仅用于中性化,不进合成)Score
vs 次期收益 Ret_t1
的秩相关Q5−Q1
长短差建议样本:20 只 A 股 × 24–36 个月月度数据(名单见第 1 节)。
请新建工作表 Industry_Lookup
,建立两列:Ticker
、Industry
,手工录入下表(或复制粘贴):
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
以下为常见的 CSMAR 客户端菜单路径。不同高校界面略有差异,请以本地为准。
TRD_Monthly.xlsx
说明:优先用 Mretwd(复权月收益);若没有,将用“前复权收盘价”自算。
BS_Annual.xlsx
IS_Annual.xlsx
小贴士:如果“归母权益/净利润”字段命名不同,请选择含义相同的口径;确保为“合并报表、年度”。
新建 MF-FF5-Ashare.xlsx
,创建以下工作表并导入数据。
Data
:导入 TRD_Monthly.xlsx
Ticker
(Stkcd)、Date
(Trdmnt)、Mret
(Mretwd)、MktCap
(Msmvttl)、可选 Close
Date
为日期格式(建议转为每月月末日期)Financials_Annual
:合并 BS_Annual.xlsx
与 IS_Annual.xlsx
Ticker
、FiscalYear
(由 Accper 提取年份)、BookEquity
(Parentequity)、TotalAssets
、NetIncome
Ticker + FiscalYear
的两张表按键合并成一行将三张表都转换为“表格”(Ctrl+T),命名:
TblData
、TblFin
、TblInd
目标:把两份“年度财务”合成一张标准表,列为:Ticker, FiscalYear, BookEquity, TotalAssets, NetIncome
,并转为表对象 TblFin
。
BS_Annual
,将 BS_Annual.xlsx 的数据粘贴/导入,Ctrl+T 转为表,命名 TblBS
。IS_Annual
,将 IS_Annual.xlsx 的数据粘贴/导入,Ctrl+T 转为表,命名 TblIS
。Stkcd → Ticker
,Accper → Accper
,Parentequity(或权益)→ BookEquity
,Totassets → TotalAssets
Stkcd → Ticker
,Accper → Accper
,Netprofit_parent(或净利润)→ NetIncome
=TEXT([@Ticker],"000000")
后覆盖原列。FiscalYear
:
=YEAR([@Accper])
=INT([@Accper]/10000)
=YEAR(DATEVALUE([@Accper]))
Financials_Annual
,把 TblBS 的四列复制过来(或引用):Ticker, FiscalYear, BookEquity, TotalAssets
,Ctrl+T 转为表,命名 TblFin
。TblFin
新增列 NetIncome
,用同键(Ticker+FiscalYear)从 TblIS 查回:
=XLOOKUP(1, (TblIS[Ticker]=[@Ticker])*(TblIS[FiscalYear]=[@FiscalYear]), TblIS[NetIncome], NA())
Key = [@Ticker]&[@FiscalYear]
Key = [@Ticker]&[@FiscalYear]
NetIncome
列填:=IFERROR(INDEX(TblIS[NetIncome], MATCH([@Key], TblIS[Key], 0)), NA())
TblFin
→ 数据 → 删除重复项 → 勾选 Ticker, FiscalYear
Ticker
、FiscalYear
升序排序BookEquity / TotalAssets / NetIncome
与原文件一致NetIncome=NA()
;这是正常的“缺报”,可保留 NA完成后,TblFin
即为后续计算 AG 的基础表。
=YEAR([@Accper])
=INT([@Accper]/10000)
=XLOOKUP(1, (TblIS[Ticker]=[@Ticker])*(TblIS[FiscalYear]=[@FiscalYear]), TblIS[NetIncome], NA())
' 在 TblIS:Key = [@Ticker]&[@FiscalYear]
' 在 TblFin:Key = [@Ticker]&[@FiscalYear]
=IFERROR(INDEX(TblIS[NetIncome], MATCH([@Key], TblIS[Key], 0)), NA())
Year_Fin = YEAR([@Date]) - 1
(对应上一财报年度)Industry
:用 Industry_Lookup
映射A 版(XLOOKUP):
=IFERROR(XLOOKUP([@Ticker], TblInd[Ticker], TblInd[Industry], "All"), "All")
B 版(INDEX/MATCH):
=IFERROR(INDEX(TblInd[Industry], MATCH([@Ticker], TblInd[Ticker], 0)), "All")
Ret_t1
(次期月收益)
Mret
:对同一股票向后位移一行(注意按 Ticker+Date 排序)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
。
LogMktCap = LN([@MktCap])
新增列 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 版(辅助键列):
TblFin
新增 Key = Ticker & FiscalYear
=IFERROR([@TotalAssets] / INDEX(TblFin[TotalAssets], MATCH([@Ticker]&([@FiscalYear]-1), TblFin[Key], 0)) - 1, NA())
在 TblData
中新增三列:BookEquity
、NetIncome
、AG
(均来自 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):
TblData
新增 KeyFin = Ticker & Year_Fin
TblFin
已有 Key = Ticker & FiscalYear
=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 月,避免前视。
将 TblData
的关键列复制/引用到 Factors_Raw
:Date, 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 <= 0
,ROE
可能异常,后续 winsor 会处理极端值。
在 Standardize
表拷入 Factors_Raw
的列,并对每个 Date × Industry
分组分别处理三列:BM
、ROE
、AG_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))
)
对 ROE
、AG_pos
同样处理,得到 BM_win, ROE_win, AGpos_win
。
如果你将 Industry 全设为 “All”,上述“组内”即为“全市场”。
将 winsor 后的数值转为近似标准正态分数 。以 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_win
、AGpos_win
同理,得到 z_BM
、z_ROE
、z_AGpos
。
对每个 Date
,将 z_*
对 LogMktCap
做单变量回归,取残差:,其中 。
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_ROE
、z_AGpos
同理,得到 z_BM_perp
、z_ROE_perp
、z_AGpos_perp
。
进阶:也可选择在“Date × Industry”维度做规模中性化(更严谨,但样本少时不稳)。
在 Composite
表计算:
Score = AVERAGE([@z_BM_perp], [@z_ROE_perp], [@z_AGpos_perp])
课堂默认等权;IR 加权、风险平价等进阶放到后续课程。
在 Composite
表按 Date
组内分别对 Score
与 Ret_t1
排秩,再求相关。
新增辅助列:
Rank_Score
: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)
Rank_Ret
同理,将 Score
换为 Ret_t1
。在 Research_Eval
建立“唯一月份”列表:
=UNIQUE(Composite[Date])
Date
拖到“行”生成唯一日期;或“数据”→“删除重复项”对每个 Month
计算 Rank‑IC:
A 版:
=CORREL(
FILTER(Composite[Rank_Score], Composite[Date]=MonthCell),
FILTER(Composite[Rank_Ret], Composite[Date]=MonthCell)
)
B 版(透视替代):
Composite
新增列 MonthKey = TEXT(Date,"yyyy-mm")
,再用 SUMIFS/COUNTIFS
与手动相关函数计算(较麻烦)Rank_Score
与 Rank_Ret
明细,再用“数据分析加载项 → 相关”逐月计算;或将每月数据复制到同一列后用 CORREL
。汇总:
=AVERAGE(RankIC_series)
=AVERAGE(OFFSET(first_cell, ROW()-ROW(first_cell)-11, 0, 12, 1))
在 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 累计收益。
BM
、ROE
、AG
缺失的行是否自动排除?极端值是否被 winsor 限定?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)