4. 回测框架搭建:基于Python的回测引擎基础架构与数据流设计

一个健壮的回测引擎是量化策略验证的基石,尤其当我们需要在回测过程中嵌入坏账率哨兵PSI监控时,引擎的数据流设计必须支持时间切片、样本外滚动以及实时指标计算。本节将拆解回测引擎的核心模块与数据流动路径。

4.1 回测引擎核心模块架构

我们将回测引擎划分为五个独立且可组合的模块,每个模块负责单一职责,便于后续扩展PSI监控逻辑。

模块名称 职责描述 与PSI监控的关联
数据加载器 读取原始信贷样本、特征矩阵、时间戳及标签(好坏标识) 提供PSI计算所需的“参考分布”与“当前分布”数据源
策略执行器 按时间顺序逐笔或逐批次执行授信/拒绝决策 触发PSI哨兵在每次决策后检查特征偏移
风控计算器 计算坏账率、逾期率、通过率等核心指标 坏账率作为PSI监控的最终验证标签
PSI哨兵模块 计算特征分布偏移、触发预警、记录偏移日志 回测框架的核心新增组件
绩效归因器 汇总回测结果,生成收益/风险/稳定性报告 输出包含PSI预警次数的综合绩效表
设计原则: 每个模块通过标准化的 DataFrame 接口通信,PSI哨兵模块作为“中间件”插入策略执行器与风控计算器之间,不侵入原有业务逻辑。

4.2 数据流设计:时间轴滚动与分布快照

回测数据流遵循严格的时间顺序,PSI监控要求我们在每个时间窗口(如每月)结束时,对当前批次的评分分布与历史参考分布进行对比。

回测引擎数据流与PSI哨兵插入点 数据加载器 原始样本 + 时间戳 时间切片器 按月/按周滚动窗口 策略执行器 授信/拒绝决策 PSI哨兵模块 分布偏移计算 预警阈值判定 风控计算器 坏账率/通过率 绩效归集器 含PSI预警报告 参考分布快照库 初始训练集分布 数据流关键规则 • 时间切片器驱动整个回测时钟 • PSI哨兵在每个切片结束时触发 • 参考分布仅从训练集计算一次
数据流要点: 时间切片器是回测的“心跳”,它决定PSI监控的频率。建议在切片器输出中加入 window_id 字段,方便后续按窗口聚合坏账率与PSI值。

4.3 核心代码骨架:回测引擎主循环

以下代码展示了嵌入PSI监控的回测主循环结构。注意 psi_monitor 对象在每次时间窗口结束时被调用。

import pandas as pd
import numpy as np
from datetime import datetime

class BacktestEngine:
    def __init__(self, data: pd.DataFrame, strategy, psi_monitor, window='M'):
        self.data = data.sort_values('apply_date')
        self.strategy = strategy
        self.psi_monitor = psi_monitor
        self.window = window
        self.results = []

    def run(self):
        # 按时间窗口分组
        for window_id, group in self.data.groupby(pd.Grouper(key='apply_date', freq=self.window)):
            if len(group) == 0:
                continue

            # 1. 策略执行:生成授信决策
            decisions = self.strategy.decide(group)

            # 2. 计算当前窗口的评分分布
            current_scores = decisions['score'].values

            # 3. PSI哨兵监控:对比参考分布
            psi_value = self.psi_monitor.calculate(current_scores)
            is_alarm = psi_value > self.psi_monitor.threshold

            # 4. 风控指标计算
            bad_rate = decisions['is_bad'].mean()
            pass_rate = decisions['is_approved'].mean()

            # 5. 记录结果
            self.results.append({
                'window_id': window_id,
                'psi': psi_value,
                'psi_alarm': is_alarm,
                'bad_rate': bad_rate,
                'pass_rate': pass_rate,
                'sample_size': len(group)
            })

            # 6. 若触发预警,记录详细日志
            if is_alarm:
                self.psi_monitor.log_alarm(window_id, psi_value, current_scores)

        return pd.DataFrame(self.results)
常见陷阱: 在回测中,PSI的参考分布必须严格来自训练集或第一个时间窗口,不能使用全量数据的分布,否则会导致未来信息泄露,使PSI监控失去预警意义。

4.4 数据流中的关键字段设计

为了支持PSI监控与坏账率回溯,回测数据流中的每个样本必须包含以下核心字段:

字段名 类型 说明 PSI监控用途
apply_date datetime 申请时间戳 确定所属时间窗口
score float 模型输出的信用评分 PSI计算的核心变量
is_approved int (0/1) 策略是否通过 区分通过/拒绝子群体分布
is_bad int (0/1) 实际好坏标签(表现期后) 计算坏账率,验证PSI预警有效性
feature_vector array 入模特征向量 可选:多维PSI监控

4.5 回测引擎初始化与PSI哨兵注入

在实际使用中,我们通过依赖注入的方式将PSI哨兵模块挂载到回测引擎中,保持引擎核心逻辑的纯净性。

# 示例:初始化PSI哨兵
from psi_sentinel import PSISentinel

# 参考分布:使用训练集评分
reference_distribution = np.histogram(train_scores, bins=20, range=(0,1))[0]

psi_sentinel = PSISentinel(
    reference_dist=reference_distribution,
    threshold=0.25,          # PSI预警阈值
    bins=20,
    alarm_callback=send_alert  # 可选:预警回调函数
)

# 注入回测引擎
engine = BacktestEngine(
    data=full_data,
    strategy=my_strategy,
    psi_monitor=psi_sentinel,
    window='M'               # 按月监控
)

result_df = engine.run()
架构优势: 通过依赖注入,PSI哨兵可以轻松替换为其他稳定性监控指标(如CSI、群体稳定性指标),无需修改回测引擎核心代码。这种设计使得回测框架具备良好的可扩展性与可测试性。

4.6 回测输出结构:含PSI预警的绩效表

回测完成后,引擎返回的DataFrame应包含以下列,便于后续分析PSI预警与坏账率的关系:

window_id psi psi_alarm bad_rate pass_rate sample_size
2024-01 0.12 False 3.2% 68% 1250
2024-02 0.31 True 5.8% 65% 1180
2024-03 0.08 False 2.9% 70% 1320
分析建议: 重点关注 psi_alarm=True 的时间窗口,对比其前后窗口的 bad_rate 变化。通常PSI预警会领先坏账率恶化1-2个时间窗口,这是哨兵监控的核心价值所在。