4. 基础回测框架:搭建简单的量化策略回测引擎,计算每日/每笔坏账率

坏账率哨兵告警阈值优化的前提,是拥有一个能够准确、稳定地模拟策略历史表现的回测引擎。本节将搭建一个轻量级但功能完整的回测框架,重点实现每日坏账率与每笔坏账率的精确计算,为后续告警阈值分析提供可靠的数据基础。

4.1 回测引擎核心架构

一个基础的量化策略回测引擎,通常包含以下四个核心模块。它们按顺序协作,完成从原始数据到坏账率指标的完整计算链路。

核心模块清单:
  • 数据加载器(Data Loader): 读取历史交易、还款、逾期状态等时序数据,进行清洗与对齐。
  • 策略执行器(Strategy Executor): 根据预设的授信、放款、催收规则,模拟每日的决策动作。
  • 组合管理器(Portfolio Manager): 追踪每笔资产的状态迁移(正常→逾期→坏账),维护动态资产池。
  • 指标计算器(Metric Calculator): 基于资产池状态,按日或按笔计算坏账率、回收率等核心指标。
回测引擎核心架构与数据流 回测引擎核心架构与数据流 数据加载器 清洗·对齐·插值 策略执行器 授信·放款·催收 组合管理器 状态迁移·资产池 指标计算器 坏账率·回收率 数据存储层 历史交易表 | 还款计划表 | 逾期状态表 | 资产配置参数表

4.2 坏账率计算口径定义

在回测引擎中,坏账率并非单一指标。根据计算粒度和业务含义,我们至少需要区分以下两种口径:

指标名称 计算粒度 公式 适用场景
每日坏账率 时间序列(日频) 当日新增坏账本金 / 当日期初在贷余额 监控坏账的时序波动,触发哨兵告警
每笔坏账率 资产维度(单笔) 单笔最终损失金额 / 单笔放款本金 评估策略对单笔资产的区分能力
实践要点: 每日坏账率对时间窗口敏感,建议使用滚动窗口(如7日/30日移动平均)平滑短期波动;每笔坏账率则需关注账龄(Vintage)对齐,避免截断偏差。

4.3 回测引擎代码实现

以下是一个基于 Python 的轻量级回测引擎核心代码。它实现了每日坏账率和每笔坏账率的计算逻辑,并保留了扩展接口。

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

class BacktestEngine:
    """量化策略回测引擎——坏账率计算核心"""

    def __init__(self, loan_book: pd.DataFrame, repayment_schedule: pd.DataFrame):
        """
        初始化回测引擎
        :param loan_book: 放款记录表,字段包括 [loan_id, principal, issue_date, term]
        :param repayment_schedule: 还款计划表,字段包括 [loan_id, due_date, due_amount, status]
        """
        self.loan_book = loan_book.copy()
        self.repayment_schedule = repayment_schedule.copy()
        self.daily_metrics = None
        self.loan_metrics = None

    def _compute_daily_bad_rate(self) -> pd.DataFrame:
        """计算每日坏账率"""
        # 1. 生成每日资产池状态
        date_range = pd.date_range(
            start=self.loan_book['issue_date'].min(),
            end=self.repayment_schedule['due_date'].max(),
            freq='D'
        )
        daily_records = []
        for current_date in date_range:
            # 期初在贷余额:所有已放款且未结清的本金
            active_loans = self.loan_book[
                (self.loan_book['issue_date'] <= current_date)
            ]
            # 简化处理:假设到期未还即进入坏账
            bad_loans = self.repayment_schedule[
                (self.repayment_schedule['due_date'] == current_date) &
                (self.repayment_schedule['status'] == 'overdue')
            ]
            beginning_balance = active_loans['principal'].sum()
            new_bad_amount = bad_loans['due_amount'].sum()
            daily_bad_rate = new_bad_amount / beginning_balance if beginning_balance > 0 else 0.0
            daily_records.append({
                'date': current_date,
                'beginning_balance': beginning_balance,
                'new_bad_amount': new_bad_amount,
                'daily_bad_rate': daily_bad_rate
            })
        return pd.DataFrame(daily_records)

    def _compute_loan_bad_rate(self) -> pd.DataFrame:
        """计算每笔坏账率"""
        # 按 loan_id 聚合还款状态
        loan_performance = self.repayment_schedule.groupby('loan_id').agg(
            total_due=('due_amount', 'sum'),
            total_overdue=('due_amount', lambda x: x[self.repayment_schedule.loc[x.index, 'status'] == 'overdue'].sum())
        ).reset_index()
        loan_performance = loan_performance.merge(
            self.loan_book[['loan_id', 'principal']], on='loan_id', how='left'
        )
        loan_performance['bad_rate_per_loan'] = loan_performance['total_overdue'] / loan_performance['principal']
        return loan_performance

    def run(self):
        """执行完整回测"""
        self.daily_metrics = self._compute_daily_bad_rate()
        self.loan_metrics = self._compute_loan_bad_rate()
        print(f"回测完成:共 {len(self.daily_metrics)} 个交易日,{len(self.loan_metrics)} 笔贷款。")
        return self.daily_metrics, self.loan_metrics

4.4 回测输出与可视化

运行上述引擎后,我们将得到两个核心数据集。下表展示了每日坏账率输出的前5行示例:

date beginning_balance new_bad_amount daily_bad_rate
2024-01-01 5,000,000 12,500 0.0025
2024-01-02 5,100,000 15,300 0.0030
2024-01-03 5,050,000 10,100 0.0020
2024-01-04 5,200,000 20,800 0.0040
2024-01-05 5,150,000 7,725 0.0015

每笔坏账率输出则包含每个 loan_id 的最终损失比例,可用于后续的策略分组对比和坏账分布分析。

每日坏账率 vs 每笔坏账率计算流程 每日坏账率 vs 每笔坏账率计算流程 每日坏账率(时序视角) 按日聚合资产池 识别当日新增坏账 坏账率 = 当日新增坏账 / 期初在贷余额 每笔坏账率(资产视角) 按 loan_id 分组 汇总逾期总金额 坏账率 = 逾期总金额 / 放款本金 两种口径互为补充:每日坏账率用于时序监控,每笔坏账率用于策略评估

4.5 回测引擎验证要点

在将回测引擎投入哨兵告警阈值优化之前,必须完成以下验证,确保计算结果的可靠性:

  • 数据完整性验证: 检查 loan_id 是否在放款表和还款计划表中一一对应,无缺失、无重复。
  • 坏账定义一致性: 确认逾期天数阈值(如 M3+)与业务定义一致,避免因口径差异导致坏账率失真。
  • 边界条件处理: 验证期初在贷余额为 0 或分母极小时的每日坏账率计算是否返回 0 而非 NaN 或无穷大。
  • 滚动窗口平滑: 对每日坏账率序列计算 7 日移动平均,观察是否存在异常尖峰,排查数据或逻辑错误。
常见陷阱: 回测中容易忽略未来信息泄露。例如,在计算每日坏账率时,使用了当日的还款状态来判定坏账,但实际业务中还款状态通常在次日才能确认。建议在回测中引入延迟确认机制,将状态更新滞后 1 个交易日,以更贴近真实环境。