阅读提示

这篇讲解 Betalens 的择时策略模式——它和截面因子有什么不同、YAML 配置怎么写、权重是怎么生成的。如果你对单标的择时感兴趣,这篇是入门的最佳起点。

导言

Betalens 的因子策略分为两大类:

  • 截面因子strategy_type: cross_sectional,默认值):在每个调仓日对全市场股票打分排名,买入高分组、卖出低分组。多空均可,适合选股型策略。
  • 择时策略strategy_type: timing):针对单个标的,根据技术指标/情绪指标生成仓位信号(0%~100%),满仓/空仓/半仓操作。多为纯多头,适合交易型策略。

两种模式的核心区别在于权重是怎么生成的

1
2
截面因子:因子值 → 分组标签 → 组内等权/加权 → 权重矩阵(多头合计1,空头合计-1)
择时策略:指标值 → 信号/仓位 → 目标权重(单标的 + cash)

截面 vs 择时:关键差异

维度 截面因子 择时策略
标的数量 多只股票(截面) 单标的
权重范围 -1(空头)~ +1(多头) 0(空仓)~ 1(满仓)
调仓逻辑 分组后等权配比 信号触发后直接给仓位
YAML strategy_type 缺省或 cross_sectional timing
YAML weight.mode classic-long-short freeplay(占位)
Dashboard 归属 “截面因子”页面 “择时策略”页面

XICHOU_timing 完整解析

betalens-factor/tdx/XICHOU/factor_XICHOU_timing.yaml 为例,逐段讲解:

1
2
3
4
meta:
class: tdx
name: XICHOU_timing
strategy_type: timing # ← 标记为择时策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
factor_spec:
inputs:
low_wide: 最低价(元) # XICHOU 的基础数据
high_wide: 最高价(元) # 用于计算"创新高等待天数"
compute_kwargs:
sma_n: 2 # TDX SMA 窗口
ema_n: 5 # TDX EMA 窗口
llv_n: 10 # 创 N 日新低判断
stock_code: 300750.SZ # 单标的:宁德时代
trigger_threshold: 0.1 # 触发阈值
trigger_operator: auto # 自动选择 > 还是 <
history_window: 5 # 最多参考 5 个历史触发事件
duration_quantile: 0.8 # 开仓:历史持续天数达到 80% 分位才开仓
exit_wait_quantile: 0.5 # 平仓:历史创新高等待天数达到 50% 分位时平仓
min_history_events: 3 # 至少 3 个历史事件后才启用(避免冷启动过拟合)
default_exit_wait_days: 5 # 默认持仓延续天数
max_weight: 1.0 # 最高满仓
1
2
3
4
weight:
mode: freeplay # 择时策略直接生成仓位,这里只是占位
long_groups: null # 不使用截面分组
short_groups: null # 不使用截面做空
1
2
3
4
5
6
run:
start_date: '2025-01-01'
end_date: '2025-12-31'
rebal_freq: D # 每日检查信号
n_quantiles: 10 # 保留字段(择时脚本会忽略)
initial_amount: 100000000

择时策略的计算逻辑

择时策略的 compute 函数不走分组,而是直接生成目标仓位:

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
# 伪代码(参考 factor_XICHOU_timing.py)
def compute_xichou_timing(low_wide, high_wide, **compute_kwargs):
# 1. 计算 XICHOU 吸筹能量指标
xichou_energy = compute_xichou_energy(low_wide, sma_n, ema_n, llv_n)

# 2. 判断是否触发开仓信号
if xichou_energy > trigger_threshold:
# 3. 统计历史触发事件持续天数
history_durations = get_historical_durations(history_window)
duration_threshold = np.percentile(history_durations, duration_quantile * 100)

if current_duration >= duration_threshold:
target_position = max_weight # 满仓
else:
target_position = current_duration / duration_threshold * max_weight # 按比例
else:
target_position = 0 # 不开仓

# 4. 判断是否需要平仓
# 基于历史"触发后创新高等待天数"估算平仓时机
...

# 5. 生成权重矩阵
weights = pd.DataFrame({
stock_code: [target_position], # 单标的仓位
"cash": [1 - target_position], # 现金 = 1 - 仓位
}, index=[rebalance_date])

return weights

核心特点

  1. 不分组:不把股票分成 Top/Bottom 组,直接生成一个 0~1 的仓位。
  2. 带历史信息:用历史触发事件的分布(分位数)来决定当前仓位,避免冷启动过拟合。
  3. 有最保守的默认持仓天数default_exit_wait_days):即使统计样本不足,也能有基本的行为基准。

Dashboard 中跑择时策略

  1. 打开 Dashboard:http://127.0.0.1:5173
  2. 进入”择时策略“页面(不是”截面因子”页面)
  3. 选择因子:tdx/XICHOU_timing
  4. 修改参数(最常改的是 stock_codetrigger_thresholdmax_weight
  5. 点击”运行”

Dashboard 会显示:

  • 信号记录:每次开仓/平仓的时间点
  • 仓位曲线:每日目标仓位(0~1)
  • 累计净值:策略净值 vs 基准净值
  • 胜率/赔率:开仓次数、盈利次数、亏损次数、平均盈利、平均亏损
  • 平均仓位:年化平均持仓比例(衡量择时信号的积极性)

开发者侧:择时 YAML 的约束

如果要给现有的截面因子新增一个择时版本(比如 factor_ROE_timing.py),需要注意以下约束:

  1. YAML 必须有 strategy_type: timing:否则 Dashboard 不会把它放到择时策略页面。
  2. compute 函数直接生成权重矩阵:不走 get_single_factor_weight,而是返回一个 DataFrame(index=DatetimeIndex,columns=[stock_code, “cash”])。
  3. 权重范围 0~1(纯多头),如果要做空,可以输出负值。
  4. weight.mode: freeplay 只是占位:Dashboard 配置结构要求有这个字段,但择时脚本不会使用它。
  5. 返回标准的 RunResultFactorPipeline.run() 仍然返回 RunResult(backtest=bt, analyst=analyst, factor_values=...),不要另建第二套返回结构。
1
2
3
4
# 择时因子脚本的 compute 函数签名
def compute_xxx_timing(**inputs, compute_kwargs, **kwargs) -> RunResult:
# ... 生成权重 ...
return RunResult(backtest=bt, analyst=analyst, factor_values=factor_values)

常见问题

1. 择时策略在 Dashboard 里找不到

检查 YAML:strategy_type: timing 是否拼写正确,大小写敏感。

1
2
3
4
5
# 错
strategy_type: Timing # 大写

# 对
strategy_type: timing # 全小写

2. 仓位一直是 0

原因可能是:

  • trigger_threshold 设太高,信号从未触发。
  • min_history_events 设太高,历史触发事件不足 3 个。
  • factor_values 输出里的”是否触发”列,确认触发逻辑。

3. 择时回测和手动计算不一致

择时策略的调仓执行时间会加 10 分钟(pd.Timedelta(minutes=10)),这是框架约定。如果你在 Jupyter 里手动算信号和仓位,注意对齐到交易日边界。

延伸阅读