阅读提示

这篇是 BacktestBase 的参数手册,详细讲解每个参数的作用、默认值和调优思路。如果你想深入理解回测引擎的行为,这篇是必读的。

导言

BacktestBase 是 Betalens 回测引擎的核心类。它把一个权重矩阵(行=调仓日,列=股票+cash)转换成一组时间序列:净值、实际持仓、每日盈亏、调仓日志。理解它的参数,是调出”真实”回测结果的第一步。

完整函数签名

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
from betalens.backtest import BacktestBase

engine = BacktestBase(
weight, # 目标权重矩阵(DataFrame, DatetimeIndex × codes+cash)
symbol, # 策略标识符(字符串)
amount, # 初始资金(元)

# === 交易成本 ===
ftc=0.0, # 固定手续费(元/笔)
ptc=0.0, # 比例手续费(%,印花税+佣金等)

# === 数据来源 ===
metric="收盘价(元)", # 成交价格字段名
time_tolerance=24, # 价格查询时间容差(小时)
table_name="daily_market", # 数据表名

# === 交易规则 ===
check_trade_status=True, # 是否检查停牌状态
trade_status_mode="to_cash", # 停牌持仓处理模式
trade_status_table="trade_status",
lot_size=100, # 每手股数(A 股默认 100)

# === 输出控制 ===
verbose=True, # 是否打印调仓日志
)

参数逐个拆解

weight:目标权重矩阵

1
2
3
4
# weight 必须是 DataFrame
# 行索引:DatetimeIndex(调仓日)
# 列:股票代码 + "cash"
weight.tail(3)
要求 说明
行索引 必须是 DatetimeIndex,每个元素是一个调仓日
必须包含所有涉及的股票代码,以及 cash
多头合计 通常为 1(或 <1,差额是现金)
空头合计 通常为 -1(或 >-1,差额是现金)

最常见的错误:忘记补 cash 列:

1
2
3
4
5
6
# KeyError: 'cash'
engine = BacktestBase(weight=weights, ...) # weights 没有 cash 列

# 正确:
weights["cash"] = 0 # 多空策略:现金=0
weights["cash"] = 1 - weights.iloc[:, :-1].sum(axis=1) # 纯多头策略

amount:初始资金

初始资金决定了你能否买够整数手。如果金额太小,会出现”有钱但买不了任何股票”的困境。

1
2
3
4
5
6
# 100 万初始资金
engine = BacktestBase(weight=weights, amount=1_000_000, ...)

# 检查:实际有多少比例的资金参与了交易?
print(engine.actual_weight.iloc[:, :-1].abs().sum(axis=1).mean())
# 如果接近 0 → 资金太小,买不了股票

经验法则:初始资金 ≥ 组内股票数量 × 单手最低金额(≈ 股价 × 100)。如果每个调仓日有 20 只股票,平均股价 20 元,最小资金 ≈ 20 × 100 × 20 = 40,000 元。实际建议用 100 万以上。

ftc / ptc:交易成本

1
2
3
4
5
6
7
8
9
10
11
12
13
# 默认:无交易成本
engine = BacktestBase(weight=weights, ftc=0.0, ptc=0.0, ...)

# 真实 A 股成本:
# - 印花税:卖出时 0.1%(仅卖出收取)
# - 佣金:买卖均收,通常 0.03%(最低 5 元/笔)
# - 过户费:沪深有所不同,通常 0.001%
engine = BacktestBase(
weight=weights,
ftc=5.0, # 最低佣金 5 元/笔
ptc=0.0013, # 印花税 0.1% + 佣金 0.03% ≈ 0.13% = 0.0013
...
)

注意ptc 是买卖双向收取的。如果你想模拟”只收印花税”(卖出时才收),目前的 ptc 参数是双向的,需要在分析阶段单独处理印花税。框架未来可能支持单向 ptc_sell 参数。

metric:成交价格字段

1
2
3
4
5
6
7
8
# 默认:用收盘价成交
engine = BacktestBase(weight=weights, metric="收盘价(元)", ...)

# 用复权收盘价(更准确反映真实收益)
engine = BacktestBase(weight=weights, metric="复权收盘价(元)", ...)

# 用开盘价(更保守,假设以开盘价成交)
engine = BacktestBase(weight=weights, metric="开盘价(元)", ...)

time_tolerance:价格容差

1
engine = BacktestBase(weight=weights, time_tolerance=24, ...)

含义:如果某天某股票的价格数据缺失(如数据源断了一天),最多往前找多少小时的数据来填充。默认 24 小时,即最多往前找一天。

check_trade_status:停牌检查

1
2
3
4
5
# 默认:检查停牌状态
engine = BacktestBase(weight=weights, check_trade_status=True, ...)

# 关闭检查(用于模拟对照实验)
engine = BacktestBase(weight=weights, check_trade_status=False, ...)

关闭后,停牌股票会按正常价格成交——这在研究中有时有用(如估算”如果不考虑停牌,收益会高多少”),但不适合作为实际策略的参考。

trade_status_mode:停牌处理策略

模式 含义 适用场景
to_cash(默认) 停牌股票转现金,按当日现金比例分配 保守模拟,贴近实际交易
hold 继续持有,停牌日不操作 长期持有、减少换手
redistribute 停牌股票权重重新分配给其他正常交易的股票 希望满仓运行
as_normal 忽略停牌,按正常价格成交(不推荐) 对照实验
1
2
# 满仓策略:停牌股票的权重重新分配
engine = BacktestBase(weight=weights, trade_status_mode="redistribute", ...)

lot_size:每手股数

1
2
3
4
5
# A 股默认 100 股/手
engine = BacktestBase(weight=weights, lot_size=100, ...)

# 港股、美股:不同市场的每手股数可能不同
# 如果要做跨市场回测,需要分别跑不同市场的 engine 再合并

回测结果:所有产出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
engine = BacktestBase(...)

# 1. 净值曲线(最重要)
nav = engine.nav # Series, DatetimeIndex
print(nav.tail()) # 最后一天的累计净值

# 2. 实际权重矩阵(真实参与净值计算)
actual_weight = engine.actual_weight # DataFrame

# 3. 每日持仓股数(整数手)
position = engine.position # DataFrame

# 4. 每日盈亏
daily_pnl = engine.daily_pnl # 每日盈亏
daily_pnl_total = engine.daily_pnl_total # 含交易成本的每日盈亏

# 5. 调仓日志(审计用)
rebalance_log = engine.rebalance_log # DataFrame

# 6. 交易状态矩阵
trade_status_matrix = engine.trade_status_matrix # DataFrame

# 7. 持仓市值序列
daily_position_value = engine.daily_position_value # Series

开发者侧:整数手约束的实现

1
2
3
4
5
6
7
8
9
# BacktestBase 整数手撮合伪代码
for each rebalance_date:
for each stock in target_weights:
target_shares = (target_weight * portfolio_value) / price
actual_shares = trunc(target_shares / lot_size) * lot_size # 向零取整
actual_value = actual_shares * price
cash += target_value - actual_value # 未成交资金回到现金

actual_weights = actual_values / portfolio_value

注意 trunc(向零取整)的含义:-3.7 股 → -3 股,3.7 股 → 3 股。这和 floor(向下取整)有区别——空头会多买 1 手(因为向零取整比向下取整更接近 0)。这是 A 股整数手撮合的一个细微但重要的特性。

常见错误

1. 权重矩阵的索引不是 DatetimeIndex

1
2
3
4
5
6
# 报错:Index must be DatetimeIndex
engine = BacktestBase(weight=weights, ...)

# 修复:
weights.index = pd.to_datetime(weights.index)
engine = BacktestBase(weight=weights, ...)

2. ptc 双向收费导致成本高估

当前 ptc 是买卖双向的。如果只想模拟印花税(卖出时才收),需要手动在结果上调整,或者把 ptc 设为 0,用 ftc 来单独处理卖出印花税。

3. 资金太小导致 actual_weight 全 0

这是整数手约束导致的必然结果,不是 bug。增加 amount 或减少持仓数量(减少分组数)。

延伸阅读