阅读提示

这篇的目标是让你在 5 分钟内跑出一个完整的因子回测——从查数据到分组、从分组到权重、从权重到净值报告。跑完之后,你会对”Datafeed → Factor → Backtest → Analyst”这条流水线有一个感性认识。

导言

假设你已经装好了 Betalens(如果没装,先去看 03 篇),现在想知道一件事:股息率高的股票,是不是长期来看真的比股息率低的股票更赚钱?

这个问题用 Betalens 来回答,只需要 30 行代码。

完整代码

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
47
48
49
50
51
from betalens.datafeed import get_absolute_trade_days
from betalens.factor.factor import (
get_tradable_pool,
pre_query_characteristic_data,
single_characteristic,
get_single_factor_weight,
)
from betalens.backtest import BacktestBase
from betalens.analyst import Analyst

# ===== 1. 调仓日 + 可交易池 =====
# 获取每年4月30日的交易日列表(对齐年报披露截止日)
days = get_absolute_trade_days("2020-04-30", "2024-04-30", "Y")
# date_ranges / code_ranges 是分组和查询的必填参数
date_ranges, code_ranges = get_tradable_pool(days)

# ===== 2. 查询因子并分组 =====
# 从 fundamentals 表取股息率,用 date_ranges/code_ranges 过滤
data = pre_query_characteristic_data(
days,
"股息率(报告期)",
table_name="fundamentals",
date_ranges=date_ranges,
code_ranges=code_ranges,
)
# 按股息率从高到低分组,第10组(最后组)是股息率最低的10%股票
labeled = single_characteristic(data, "股息率(报告期)", {"股息率(报告期)": 10})

# ===== 3. 生成多空权重 =====
# 买入第10组(高股息),做空第1组(低股息),classic-long-short 模式
weights = get_single_factor_weight(labeled, {
"factor_key": "股息率(报告期)",
"mode": "classic-long-short",
})
# 权重矩阵必须包含 cash 列,这里空头对冲所以现金为0
weights["cash"] = 0

# ===== 4. 回测 =====
# 默认从 daily_market 取"收盘价(元)",time_tolerance=24小时
engine = BacktestBase(
weight=weights,
symbol="Dividend",
amount=1_000_000, # 初始资金100万
table_name="daily_market",
)

# ===== 5. 绩效评价 =====
Analyst.from_backtest(engine, name="Dividend").report(
to_excel="report.xlsx",
to_html="report.html",
)

运行完毕,当前目录下会多出 report.xlsxreport.html。用浏览器打开 HTML 就能看到净值曲线、收益统计、持仓明细等图表。

代码解读:流水线上的五个步骤

步骤 1:调仓日 + 可交易池

1
2
days = get_absolute_trade_days("2020-04-30", "2024-04-30", "Y")
date_ranges, code_ranges = get_tradable_pool(days)
  • get_absolute_trade_days:返回交易日列表。"Y" 表示每年,落在 4 月 30 日(或其最近交易日)。
  • get_tradable_pool:根据交易日列表,返回哪些股票在哪些日期是”可交易的”(剔除停牌、未上市)。返回两个对象——date_ranges(日期×可交易证券)和 code_ranges(证券代码范围),这两个对象会在后续查询中作为过滤条件。

步骤 2:查询因子并分组

1
2
3
4
5
6
7
data = pre_query_characteristic_data(
days, "股息率(报告期)",
table_name="fundamentals",
date_ranges=date_ranges,
code_ranges=code_ranges,
)
labeled = single_characteristic(data, "股息率(报告期)", {"股息率(报告期)": 10})
  • pre_query_characteristic_data:从数据库预取原始因子值。默认从 fundamentals 表取,time_tolerance 默认 24*2*365(两年)。
  • single_characteristic:按分位数把股票分成 10 组(分位点 10),返回带分组标签的 DataFrame。注意函数名是 characteristic 不是 factor,这是最容易记错的命名坑。
  • {"股息率(报告期)": 10} 表示按股息率从低到高分成 10 组。

步骤 3:生成多空权重

1
2
3
4
5
weights = get_single_factor_weight(labeled, {
"factor_key": "股息率(报告期)",
"mode": "classic-long-short",
})
weights["cash"] = 0
  • classic-long-short:做多最高股息率组(第 10 组),做空最低组(第 1 组),多头合计 1、空头合计 -1。
  • 权重矩阵的列是证券代码,行是调仓日DatetimeIndex)。最后必须补一列 cash(现金比例)。
  • 这里的 cash=0 表示资金全部用上了(多头用了 100%,空头对冲)。

步骤 4:回测

1
2
3
4
5
6
engine = BacktestBase(
weight=weights,
symbol="Dividend",
amount=1_000_000,
table_name="daily_market",
)
  • BacktestBase 接收目标权重矩阵,按整数手(每手 100 股)成交,模拟日频净值。
  • engine.actual_weight 才是真正参与净值计算的权重(输入的 weight 可能会因停牌、整数手等原因被调整)。
  • check_trade_status=True 时,停牌股票会自动转现金(trade_status_mode="to_cash")。

步骤 5:绩效评价

1
2
3
4
Analyst.from_backtest(engine, name="Dividend").report(
to_excel="report.xlsx",
to_html="report.html",
)
  • Analyst.from_backtest 从回测实例生成完整报告。
  • report() 同时输出 Excel 和 HTML,HTML 用 plotly 渲染成交互图表。
  • 也可用 Analyst.from_excel(path) 从已有的 Excel 报告文件重新分析。

开发者侧:流水线的数据流

从开发者的角度理解,上面的 quickstart 其实走的是这样一条路径:

1
2
3
4
5
6
PostgreSQL (fundamentals) ──→ pre_query_characteristic_data
PostgreSQL (trade_status) ──→ get_tradable_pool
两者合并 ──→ single_characteristic ──→ labeled DataFrame
labeled ──→ get_single_factor_weight ──→ weight DataFrame
weight ──→ BacktestBase ──→ engine (nav / actual_weight / position ...)
engine ──→ Analyst.from_backtest ──→ report.xlsx / report.html

流水线中每一个函数都有明确的输入输出约定,不要在中间插入自定义的 pandas 操作——如果需要自定义,在 betalens-factor/ 下写因子脚本,通过 FactorPipeline 接入。

常见错误

1. 忘记补 cash

1
2
# 报错:KeyError: 'cash' 不在 columns 中
engine = BacktestBase(weight=weights, ...) # weights 没有 cash 列

解决:无论哪种权重模式,都要在权重矩阵末尾加一列 cash = 1 - sum(stock_weights)(多空模式 cash=0,纯多头模式 cash 大于 0)。

2. single_characteristic 函数名记错

1
2
3
4
5
# 错:没有 single_factor 这个函数
labeled = single_factor(data, {"股息率(报告期)": 10})

# 对:真实函数名是 single_characteristic
labeled = single_characteristic(data, "股息率(报告期)", {"股息率(报告期)": 10})

3. 回测表名写错

BacktestBase 默认 table_name="daily_market"metric="收盘价(元)"。如果你的数据库用的是其他表名或字段名,需要显式传入参数。

4. 初始资金太小,买不了任何股票

amount=1_000_000(100 万)可以买大多数 A 股;如果设成 10_000(1 万),整数手约束下可能凑不出任何持仓,导致 actual_weight 全是 0。

延伸阅读