因子分析框架详解(AlphaMiner)
由bqd002m创建,最终由bqd002m 被浏览 4 用户
本文档基于 因子分析框架 中的
AlphaMiner类,逐步骤、逐细节地介绍整个因子分析流程。
目录
- 因子配置参数说明
- 框架总览与初始化流程
- 步骤一:因子数据获取与预处理
- 步骤二:因子分组与个股收益率对齐
- 步骤三:分组收益率与累计收益率计算
- 步骤四:综合收益与 IC 指标计算
- 步骤五:图表展示
- 关键设计细节汇总
一、因子配置参数说明
每次运行分析前,需要先定义一个 dict 类型的参数对象,例如:
alpha_test = {
"alpha_class": "test",
"alpha_name": "turn_60",
"alpha_name_chinese": "60日换手率均值",
"alpha_sql": """
SELECT date, instrument, -1 * m_avg(turn,60) AS factor
FROM cn_stock_bar1d
ORDER BY date, instrument
""",
"alpha_desc": " ",
"group_num": 10,
"instruments": "全市场",
"benchmark": "中证500",
"data_process": True,
"is_bigvip": False,
"is_featured": False,
}
各参数详细说明
| 参数名 | 类型 | 说明 |
|---|---|---|
alpha_class |
str | 因子分类,用于生成 alpha_id,标识因子所属类别 |
alpha_name |
str | 因子英文名,与 alpha_class 拼接构成唯一 ID:alpha_{class}_{name} |
alpha_name_chinese |
str | 因子中文名,用于展示和平台上传 |
alpha_sql |
str | 核心:用 SQL 表达因子逻辑,必须输出 date、instrument、factor 三列 |
alpha_desc |
str | 因子描述,可留空 |
group_num |
int | 分组数量,默认 10 组,决定分层测试的精细程度 |
instruments |
str | 股票池范围,可选:全市场、中证500、中证1000、沪深300 |
benchmark |
str | 业绩基准指数,可选:中证500(000905.SH)、中证1000(000852.SH)、沪深300(000300.SH) |
data_process |
bool | 是否执行因子预处理(去极值 → 标准化 → 中性化),建议开启 |
is_bigvip / is_featured |
bool | 平台相关标记,本地分析可忽略 |
关于 alpha_sql 的规范
- 必须输出
date、instrument、factor三列 instrument格式为XXXXXX.SH或XXXXXX.SZfactor列为因子值,可通过 SQL 内置函数(如m_avg、m_lag)计算- 示例中的
-1 * m_avg(turn,60)表示对 60 日换手率均值取反(做空高换手、做多低换手)
二、框架总览与初始化流程
类结构
AlphaMiner
│
├── __init__(params) # 主流程,按顺序调用以下方法
│
├── get_factor_data() # 步骤一:获取并预处理因子数据
├── get_group_data() # 步骤二:分组 + 对齐个股收益率
├── get_group_cumret() # 步骤三:分组收益率 & 累计收益率
├── get_whole_perf() # 步骤四a:全周期综合绩效
├── get_yearly_perf() # 步骤四b:年度分年绩效
├── get_all_ic() # 步骤四c:IC 时序计算
│
└── render() # 步骤五:图表渲染与展示
初始化流程
def __init__(self, params, version="sql"):
t0 = time.time()
self.params = params
self.params['alpha_id'] = 'alpha_' + params['alpha_class'] + '_' + params['alpha_name']
self.sd = '2018-04-25' # 固定起始日期
self.ed = datetime.now().date().strftime("%Y-%m-%d") # 今天作为结束日期
关键细节:
alpha_id由alpha_class+alpha_name自动拼接,作为因子的全局唯一标识- 回测区间固定从
2018-04-25开始(A 股新规实施后,数据稳定期),到运行当日截止 - 整个流程按顺序串行执行,每步完成后打印耗时,便于性能调优
三、步骤一:因子数据获取与预处理
方法:get_factor_data(data_process, pool_class, alpha_sql)
这是整个框架最核心的数据清洗步骤,通过一个嵌套 CTE 的 SQL 完成以下四个子步骤:
子步骤 1:执行因子 SQL(data_alpha)
用户传入的 alpha_sql 被嵌套为 CTE data_alpha,直接执行,得到 (date, instrument, factor) 三列原始因子值。
子步骤 2:过滤异常值(data_alpha_origin)
data_alpha_origin AS (
SELECT *
FROM data_alpha
QUALIFY COLUMNS(*) IS NOT NULL
AND factor != 'Infinity'
AND factor != '-Infinity'
)
- 过滤掉任意列含
NULL的记录 - 过滤掉因子值为正无穷或负无穷的记录(常见于除法类因子)
子步骤 3:因子预处理(data_alpha_process)
data_alpha_process AS (
SELECT
date, instrument, factor,
clip(factor, c_avg(factor) - 3*c_std(factor), c_avg(factor) + 3*c_std(factor)) AS clipped_factor,
c_normalize(clipped_factor) AS normalized_factor,
c_neutralize(normalized_factor, sw2021_level1, LOG(total_market_cap)) AS neutralized_factor,
FROM data_alpha_origin
JOIN cn_stock_factors_base USING (date, instrument)
WHERE {pool_filter}
AND amount > 0 -- 排除停牌(成交额为零)
AND st_status = 0 -- 排除 ST 股
AND trading_days > 252 -- 排除上市不足一年的次新股
AND (instrument LIKE '%SH' OR instrument LIKE '%SZ') -- 只保留沪深两市
QUALIFY COLUMNS(*) IS NOT NULL
ORDER BY date, instrument
)
三步预处理的逻辑如下:
① 去极值(Winsorize / Clip)
clipped_factor = clip(factor, mean - 3σ, mean + 3σ)
- 每个截面日(同一
date下)计算均值c_avg和标准差c_std - 将超出
[均值 - 3σ, 均值 + 3σ]范围的因子值截断到边界 - 目的:消除极端离群值对后续分析的干扰
② 截面标准化(Z-score Normalize)
normalized_factor = c_normalize(clipped_factor)
- 对每个截面日的去极值后因子做 Z-score 标准化:
(x - mean) / std - 使不同时期、不同量级的因子值具有可比性
③ 行业 + 市值中性化(Neutralize)
neutralized_factor = c_neutralize(normalized_factor, sw2021_level1, LOG(total_market_cap))
- 以申万2021一级行业分类(
sw2021_level1)和对数市值(LOG(total_market_cap))为控制变量 - 通过截面回归,剥离因子中与行业、市值相关的部分,保留纯因子暴露
- 这是量化因子分析的标准流程,确保因子不是在暗中做行业轮动或大小盘轮动
子步骤 4:股票池过滤
pool_dict = {
"中证500": "is_zz500 = 1",
"中证1000": "is_zz1000 = 1",
"沪深300": "is_hs300 = 1",
"全市场": "1=1",
}
- 通过
cn_stock_factors_base表中的指数成分股字段来过滤 全市场使用1=1不做任何过滤
是否预处理的分支
data_process_sql = (
"SELECT date, instrument, neutralized_factor AS factor FROM data_alpha_process ..."
if data_process == True
else "SELECT * FROM data_alpha"
)
data_process=True(默认):最终取中性化后的neutralized_factordata_process=False:直接使用原始factor,不做任何处理
数据查询
df = dai.query(sql, filters={'date': [self.sd, self.ed]}).df()
- 使用
dai框架执行 SQL,并通过filters参数在数据库层面过滤日期范围,提升效率 - 返回
pd.DataFrame,列为(date, instrument, factor)
四、步骤二:因子分组与个股收益率对齐
方法:get_group_data()
将因子数据与未来收益率对齐,并按因子值大小划分分组。
子步骤 1:获取每日收益率
SELECT
date, instrument,
(m_lead(open, 2) / m_lead(open, 1) - 1) AS daily_ret
FROM cn_stock_bar1d
WHERE date BETWEEN DATE '{sd}' - INTERVAL 10 DAY AND '{ed}'
ORDER BY date, instrument
关键细节:T+2 开盘价收益率
m_lead(open, 1):下下个交易日的开盘价(T+1 开盘价)m_lead(open, 2):再下一个交易日的开盘价(T+2 开盘价)daily_ret = m_lead(open,2) / m_lead(open,1) - 1:即在 T+1 日开盘买入,T+2 日开盘卖出 的收益率- 这样设计是为了模拟实盘中:今日(T 日)收盘后得到因子信号,T+1 日开盘建仓,持有一天后 T+2 日开盘平仓
- 日期范围向前额外延伸 10 个自然日(
- INTERVAL 10 DAY),确保有足够的m_lead数据不会产生空值
子步骤 2:因子分组函数
def cut(df, group_num=10):
df = df.drop_duplicates('factor') # 去掉相同因子值的重复记录
df['group'] = pd.qcut(df['factor'], q=group_num, labels=False, duplicates='drop')
df = df.dropna(subset=['group'], how='any')
df['group'] = df['group'].apply(int).apply(str) # 转为字符串 "0"~"9"
return df
pd.qcut:等分位数分组,将因子值从小到大排序后均匀切分为group_num组- 第 0 组(
"0"):因子值最小的股票(空头组合) - 第 9 组(
"9"):因子值最大的股票(多头组合) drop_duplicates('factor'):避免完全相同的因子值导致分位数边界冲突duplicates='drop':进一步处理边界重叠问题
子步骤 3:合并并逐日分组
merge_data = pd.merge(factor_data, daily_ret_data, on=['date','instrument'], how='left')
group_data = merge_data.groupby('date', group_keys=False).apply(cut, group_num=...)
- 以
(date, instrument)为键,将因子值与 T+2 收益率左连接合并 - 按日期分组,对每个截面日独立执行
cut分组,确保每天都是等量分组 - 最终
group_data包含列:date, instrument, factor, daily_ret, group
五、步骤三:分组收益率与累计收益率计算
方法:get_group_cumret()
子步骤 1:获取基准收益率
SELECT date, instrument,
(close - m_Lag(close,1)) / m_LAG(close, 1) as benchmark_ret
FROM cn_stock_index_bar1d
WHERE instrument = '{benchmark_code}'
基准指数代码映射:
| 基准名称 | 指数代码 |
|---|---|
| 中证500 | 000905.SH |
| 中证1000 | 000852.SH |
| 沪深300 | 000300.SH |
- 使用指数的日度收盘价涨跌幅作为基准收益率
子步骤 2:计算各分组日均收益率
groupret_data = group_data[['date','group','daily_ret']] \
.groupby(['date','group'], group_keys=False) \
.apply(lambda x: np.nanmean(x)) \
.reset_index()
- 对每个
(date, group)组合,取组内所有股票daily_ret的等权平均(忽略 NaN) - 即每个分组的日收益率 = 该组所有成分股的等权平均日收益率
子步骤 3:Pivot 宽表并添加多空组合与基准
groupret_pivotdata = groupret_data.pivot(index='date', values='g_ret', columns='group')
groupret_pivotdata['ls'] = groupret_pivotdata[str(group_num-1)] - groupret_pivotdata['0']
groupret_pivotdata['bm'] = bm_ret['benchmark_ret']
groupret_pivotdata = groupret_pivotdata.shift(1) # 关键:shift(1) 时间对齐
- 将长表转为宽表,每列对应一个分组(
"0"到"9"),行为日期 ls(Long-Short)= 多头组(第 9 组)- 空头组(第 0 组),即多空对冲组合bm:基准收益率列shift(1)的作用:收益率发生在 T+1 ~ T+2 之间,而因子信号在 T 日生成,shift 操作将收益率向后移一天,与因子日期对齐,避免未来函数
子步骤 4:计算累计收益率
groupcumret_pivotdata = groupret_pivotdata.cumsum()
- 对各分组的日收益率做累加求和(
cumsum),得到累计收益率曲线 - 注意:这里用的是算术累加(非复利),适合短期/因子分层对比,不是精确的复利净值
六、步骤四:综合收益与 IC 指标计算
(一)全周期综合绩效:get_whole_perf()
分别对多头组(Long)、空头组(Short)、**多空对冲(Long-Short)**三个组合计算以下指标:
1. 基础绩效指标(get_basic_perf)
| 指标名 | 计算方式 | 说明 |
|---|---|---|
return_ratio |
series.sum() |
全周期累计收益率(算术) |
annual_return_ratio |
sum * 242 / trading_days |
年化收益率(以 242 个交易日/年) |
ex_return_ratio |
(series - bm).sum() |
相对基准的超额累计收益 |
ex_annual_return_ratio |
(series - bm).sum() * 242 / days |
年化超额收益 |
sharp_ratio |
empyrical.sharpe_ratio(series, 0.035/242) |
夏普比率,无风险利率取 3.5%/年 |
return_volatility |
empyrical.annual_volatility(series) |
年化波动率 |
max_drawdown |
empyrical.max_drawdown(series) |
最大回撤(负数) |
information_ratio |
mean / std |
日度信息比率 |
win_percent |
count(ret > 0) / total_days |
胜率(日度) |
trading_days |
len(series) |
回测总交易日数 |
ret_3/10/21/63/126/252 |
series.tail(N).sum() |
近 3/10/21/63/126/252 交易日收益 |
关键细节: 夏普比率的无风险利率使用 0.035/242,即 3.5% 年化利率折算到日度,这是 A 股常用的基准利率设定。
2. IC 绩效指标(get_ic)
def cal_ic(df):
return df['daily_ret'].corr(df['factor'], method='spearman')
- 使用 Spearman 秩相关系数计算 IC(Information Coefficient)
- Spearman IC 对排名的相关性,比 Pearson IC 更稳健,不受极端值影响
| 指标名 | 说明 |
|---|---|
ic |
全周期平均 IC |
ir |
IC / IC_std,即 IC 的信噪比(Information Ratio) |
ic_3/10/21/63/126/252 |
近 N 交易日的平均 IC |
针对多头/空头组合,只用对应分组的数据计算 IC;多空组合则合并两组数据。
3. 换手率(get_turnover)
def count_repeat(dfs):
if dfs.name > 0:
return len(set(dfs['instrument']) & set(dfs['instrument_lag']))
else:
return 0
df_ins['turnover'] = 1 - df_ins['repeat_count'] / df_ins['instrument_count']
- 每天计算当日成分股与前一日成分股的交集比例
换手率 = 1 - 重合数量 / 当日成分股数量- 换手率越高,说明因子信号每天调仓幅度越大,交易成本越高
- 多空组合换手率 = 多头组换手率 + 空头组换手率
汇总
三类指标(IC 绩效、基础绩效、换手率)被合并为一张宽表 whole_perf,每行对应一种组合类型(long/short/long_short)。
(二)年度分年绩效:get_yearly_perf()
对多头组合按年度切分,分别计算每年的绩效指标。
year_df['year'] = year_df['date'].apply(lambda x: x.year)
yearly_perf = year_df.groupby(['year']).apply(cal_Performance)
年度绩效指标与全周期基本一致,包含:return_ratio、annual_return_ratio、ex_return_ratio、ex_annual_return_ratio、sharp_ratio、return_volatility、max_drawdown、win_percent、trading_days
额外添加年度 IC:
ic_data['year'] = ic_data['date'].apply(lambda x: x.year)
yearly_ic = ic_data.groupby('year').apply(lambda x: np.nanmean(x['g_ic']))
yearly_perf['ic'] = yearly_ic
(三)IC 时序:get_all_ic()
def cal_ic(df):
return df['daily_ret'].corr(df['factor'], method='spearman')
group_ic_data = group_data[['date','daily_ret','factor']] \
.groupby('date', group_keys=False) \
.apply(lambda x: cal_ic(x)) \
.reset_index()
group_ic_data = group_ic_data.shift(1) # 时间对齐
group_ic_data['ic_cumsum'] = group_ic_data['g_ic'].cumsum() # 累计 IC
group_ic_data['ic_roll_ma'] = group_ic_data['g_ic'].rolling(22).mean() # 近22日均值
- 对全部股票(不区分分组)逐日计算当日截面 Spearman IC
shift(1):与收益率时间对齐ic_cumsum:IC 累积曲线,可直观反映因子的持续有效性,斜率越稳定越好ic_roll_ma:22 日滚动平均 IC,约等于一个月,平滑噪声,观察因子近期有效性趋势
七、步骤五:图表展示
方法:render(local_plot=False)
调用 bigcharts 库生成 7 张图表,统一渲染为一个 page 页面:
| 图表编号 | 类型 | 内容 |
|---|---|---|
c1 |
表格 | 整体绩效指标:多头/空头/多空三组合的 IC、IR、换手率、收益、夏普、回撤等 |
c2 |
表格 | 年度绩效指标(仅多头组合):逐年的收益、夏普、IC 等 |
c3 |
折线图 | 分组累计收益曲线:10 个分组 + 多空组合 + 基准的累计收益,可直观看分层效果 |
c4 |
表格 | IC 分析摘要:全周期平均 IC、` |
c5 |
叠加图 | IC 时序图(柱状 + 22 日均线) + IC 累积曲线(双 Y 轴叠加) |
c6 |
表格 | 因子值最大的 5 只股票(最新日期,潜在多头标的) |
c7 |
表格 | 因子值最小的 5 只股票(最新日期,潜在空头标的) |
IC 显著性判断
abs_ic = self.ic['g_ic'].abs()
significant_ic_ratio = abs_ic[abs_ic >= 0.02].shape[0] / abs_ic.shape[0]
|IC| >= 0.02被视为具有统计显著性的 IC 值significant_ic_ratio表示所有交易日中,IC 绝对值超过 0.02 的比例- 该比例越高,说明因子的预测效果越稳定
local_plot 参数
local_plot=True:在 Jupyter Notebook 中直接display渲染,用于本地调试local_plot=False:不本地展示,通常用于平台提交流程
八、关键设计细节汇总
时间对齐逻辑
整个框架的时间对齐关系如下:
T 日收盘 → 生成因子信号
T+1 日开盘 → 按信号建仓
T+2 日开盘 → 平仓,计算收益
daily_ret = m_lead(open,2) / m_lead(open,1) - 1:T+1 开 → T+2 开的收益groupret_pivotdata.shift(1):将收益率向后移一天,与 T 日的因子对齐group_ic_data.shift(1):同上,IC 计算也做相同对齐
因子方向约定
- 因子值越大 → 第
group_num-1组(最高组)→ 多头 - 因子值越小 → 第
0组(最低组)→ 空头 - 若因子与收益负相关(如高换手率对应低未来收益),应在 SQL 中乘以
-1进行方向调整(示例中已对换手率取反)
股票过滤规则
以下类型股票被强制排除,确保分析的可操作性:
| 过滤条件 | 说明 |
|---|---|
amount = 0 |
成交额为零,当日停牌不可交易 |
st_status != 0 |
ST/*ST 股票,风险特殊 |
trading_days <= 252 |
上市未满一年的次新股,历史数据不足 |
instrument NOT LIKE '%SH/%SZ' |
排除北交所等非标市场标的 |
年化收益率计算约定
annual_return_ratio = series.sum() * 242 / trading_days
- 使用 242 个交易日/年 作为年化基准(A 股惯例)
- 采用算术年化(非几何复利),适合分析因子多空组合的超额收益
IC 计算范围差异
| 计算位置 | 使用的股票范围 | 说明 |
|---|---|---|
get_ic(long) |
仅第 group_num-1 组(多头组) |
衡量多头组内因子有效性 |
get_ic(short) |
仅第 0 组(空头组) |
衡量空头组内因子有效性 |
get_ic(long_short) |
多头 + 空头组合 | 两端合并计算 |
get_all_ic() |
全部分组 | 全市场截面 IC,最具代表性 |
快速使用示例
# 1. 定义因子参数
alpha_test = {
"alpha_class": "test",
"alpha_name": "turn_60",
"alpha_name_chinese": "60日换手率均值",
"alpha_sql": """
SELECT date, instrument, -1 * m_avg(turn,60) AS factor
FROM cn_stock_bar1d
ORDER BY date, instrument
""",
"alpha_desc": " ",
"group_num": 10,
"instruments": "全市场",
"benchmark": "中证500",
"data_process": True,
"is_bigvip": False,
"is_featured": False,
}
# 2. 运行因子分析
alpha_miner = AlphaMiner(params=alpha_test)
# 3. 展示图表(本地 Jupyter)
alpha_miner.render(local_plot=True)
运行完成后,可通过以下属性直接访问中间结果:
| 属性 | 内容 |
|---|---|
alpha_miner.factor_data |
预处理后的因子数据(date, instrument, factor) |
alpha_miner.group_data |
带分组标签和收益率的个股数据 |
alpha_miner.group_ret |
分组日收益率宽表(含 ls、bm 列) |
alpha_miner.group_cumret |
分组累计收益率宽表 |
alpha_miner.whole_perf |
全周期绩效汇总表 |
alpha_miner.yearly_perf |
年度绩效汇总表 |
alpha_miner.ic |
IC 时序数据(含累计 IC、22 日均值) |
\