阅读提示

这篇是系列的收尾实战——从一篇量化研报出发,把公式转化为因子脚本、配置 YAML、跑通 Dashboard、生成报告。学完这篇,你就掌握了”从想法到可交付成果”的完整闭环。

导言

假设你读到了一篇中信建投的研报《基于高频非流动性的 A 股选股因子》,想把里面的 ILLIQ 因子复现到 Betalens 里。

完整流程是:

1
研报公式 → Python 计算脚本 → YAML 配置 → Dashboard 发现 → 跑回测 → 绩效报告

这篇以 ILLIQ_v2 为例,走一遍全流程。

步骤 1:理解研报公式

研报中的 ILLIQ_v2 公式:

$$ILLIQ_v2 = \frac{1}{D} \sum_{d=1}^{D} \frac{|r_d|}{VOL_d}$$

其中:

  • $r_d$:第 $d$ 日收益率(%)
  • $VOL_d$:第 $d$ 日成交额(万元)
  • $D$:月度窗口(通常 20 个交易日)

实现思路:对每只股票,计算过去 20 个交易日的日收益率绝对值之和 / 日成交额之和。

步骤 2:写因子计算脚本

betalens-factor/ 下创建目录结构:

1
2
3
4
5
6
7
8
betalens-factor/
citic_hf_behavior/ # 因子家族目录
ILLIQ_v2/ # 因子目录
factor_ILLIQ_v2.py # 计算逻辑
factor_ILLIQ_v2.yaml # 配置参数
outputs/ # 运行产物
runs/
legacy/

编写 factor_ILLIQ_v2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"""
ILLIQ_v2 因子计算脚本
公式:ILLIQ_v2 = (1/D) * sum(|r_d| / VOL_d)
"""

from betalens.factor.config import load_yaml_config, factor_spec_options, run_parameters
from betalens.factor.factor import pre_query_characteristic_data, get_tradable_pool
import pandas as pd
import numpy as np


def compute(
ret_abs_wide: pd.DataFrame,
volume_wide: pd.DataFrame,
compute_kwargs: dict,
**kwargs,
) -> pd.DataFrame:
"""
计算 ILLIQ_v2 因子值。

参数:
ret_abs_wide: 日收益率绝对值(宽表,index=date, columns=code)
volume_wide: 日成交额(宽表,index=date, columns=code)
compute_kwargs: YAML 中的 compute_kwargs

返回:
factor_values: DataFrame, index=DatetimeIndex, columns=[code...]
"""
window_days = compute_kwargs.get("window_days", 20)

# ILLIQ = sum(|r| / VOL) / D
ratio = ret_abs_wide / volume_wide # 分子/分母(注意:成交额需转换单位)
illiq = ratio.rolling(window=window_days, min_periods=window_days).mean()

return illiq


# === 以下是 Betalens 因子管线约定 ===
def build_spec(config: dict, config_path: str):
"""从 YAML 配置构建 FactorSpec。"""
return factor_spec_options(config), run_parameters(config)


# 暴露全局变量供 FactorPipeline 使用
spec = None
FactorPipeline = None

编写 factor_ILLIQ_v2.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
meta:
class: citic_hf_behavior
name: ILLIQ_v2
source: 中信建投《基于高频非流动性的 A 股选股因子》
formula: "ILLIQ_v2 = (1/D) * sum(|r_d| / VOL_d)"
logic: ILLIQ = 高流动性 = 机构更容易买入 = 正超额收益

factor_spec:
inputs:
ret_abs_wide: 收益率(%) # 日收益率(百分比)
volume_wide: 成交额(万元) # 日成交额(万元)
compute_kwargs:
window_days: 20 # 月度窗口(交易日数)
direction: negative # ILLIQ 越低越好 → negative
table_name: daily_market
backtest_metric: 收盘价(元)
use_industry: true
use_mktcap: true
industry_scheme: 申万一级行业

weight:
mode: classic-long-short
long_groups: null # 自动取最低分位(低 ILLIQ)
short_groups: null # 自动取最高分位(高 ILLIQ)

run:
start_date: '2020-01-01'
end_date: '2024-12-31'
rebal_freq: M # 月频调仓
n_quantiles: 10
initial_amount: 100000000
benchmark_code: 000906.SH
include_profiling: true
dump_excel: true
output_dir: outputs/runs/manual

步骤 3:在 Dashboard 里发现并运行

重启 Dashboard(或刷新因子列表):

1
2
3
4
# 在 betalens 仓库根目录
.\dashboard\run.bat
# 或刷新 API:
# 浏览器访问 http://127.0.0.1:8000/api/factors?refresh=true

打开 http://127.0.0.1:5173,在”截面因子”页面找到 citic_hf_behavior/ILLIQ_v2

点击”运行”,等待回测完成。下载 Excel 报告,查看 ICIR、分组收益、净值曲线。

步骤 4:调试与迭代

如果报告结果不理想,从以下角度排查:

因子值全是 NaN

1
2
3
4
# 检查输入数据是否正确
print(ret_abs_wide.head())
print(volume_wide.head())
# 确保 index 是日期,columns 是股票代码

ICIR 太低

  • 检查 direction 是否设反了(positive vs negative
  • 检查是否做了行业中性化(use_industry: true
  • 尝试增大/减小 window_days

收益和 IC 不一致

  • IC 衡量的是”预测能力”,收益衡量的是”实际交易结果”。
  • 两者可能出现背离(IC 强但收益差 → 说明交易成本太高或整数手约束影响大)。

步骤 5:用 factor-forge skill 自动化(可选)

如果研报是 PDF/DOCX 格式,可以用 factor-forge skill 自动抽取公式并生成脚本:

1
2
# 在 Cursor 里输入:
用 factor-forge skill 复现研报 C:\研报\ILLIQ_v2.pdf

factor-forge 会根据研报内容生成因子 YAML 和 Python 脚本的初稿,你再根据 Betalens 的接口规范做微调。

开发者侧:因子脚本的约束

回顾 Betalens 因子脚本的关键约定:

  1. import 时不跑回测、不写文件:脚本只声明算子,compute 函数里不能直接调用 BacktestBase
  2. 暴露 specFactorPipelinebuild_spec:供 Dashboard 的 FactorPipeline.run() 动态调用。
  3. compute 参数名匹配 factor_spec.inputs 的 keyinputs 里的 key 必须和 compute 的参数名一致。
  4. CLI 只支持 --config PATH:不要加其他命令行参数。

常见错误

1. YAML 中 inputs key 和 Python 参数名不一致

1
2
3
4
5
6
# YAML
inputs:
ret_abs_wide: 收益率(%) # ← key 是 ret_abs_wide

# Python
def compute(ret_abs, volume_wide, ...): # ← 参数名是 ret_abs,不匹配!

2. direction 设错

1
2
# ILLIQ 是"越低越好",设成 positive 会导致高流动性股票被做空
direction: negative # ← 低 ILLIQ 做多,高 ILLIQ 做空

3. 输出没有对齐调仓日

compute 函数返回的 DataFrame 的 index 必须和 run.start_daterun.end_date 的调仓日对齐。FactorPipeline 会自动处理这个对齐,但如果你手动调用 compute,需要注意。

延伸阅读