moonrunoff

    非寿险赔款发展三角、确定性准备金估计与历史回测工具

    actuarial
    chainladder
    reserving
    triangle
    Download zip
    Version
    0.1.0
    License
    MIT
    Last updated
    7 hours ago
    Downloads
    3

    No dependencies

    #MoonRunoff:非寿险赔款发展与准备金

    MoonRunoff 用 MoonBit 实现累计已支付赔款三角、确定性准备金模型、未来支付分解和历史留出回测。准备金分析人员、暴露量数据工程师和模型复核人员可以复用同一组校验与数值 API,也可以通过 JSON 请求引擎集成到服务或命令行任务。代码采用 MIT 许可证。

    #已实现的完整范围

    输入和任务处理输出
    准备金分析员的累计或增量赔款校验三角、选择 volume/simple/regression 因子、Chain Ladder 投影每事故期已付/最终赔款/未付准备金,发展期和支付日历
    数据工程师的先验最终赔款或暴露量BF、迭代 Benktander、Cape Cod统一估计对象、可对账现金流、JSON/CSV
    模型复核者的历史数据和因子选择最近窗口、配对排除、手工因子、历史截面重新拟合、单因子压力原始残差、留出 MAE/RMSE/WAPE、准备金变化

    此外提供组合相加、按事故期重述、完整累计投影和按远期利率贴现。贴现仅是确定性现金流运算;原始准备金不自动贴现。

    #环境和安装

    需要 MoonBit moonc 0.10.18 或更新版本;验证基线为 v0.10.18+6c83a7f62,满足比赛的最低 0.10.16 要求。CI 固定下载官方同版本编译器及 core,避免 latest 漂移。运行 JavaScript 后端需要 Node.js 22;原生后端需要可用的 C 编译工具链。库只依赖随工具链提供的 moonbitlang/core。

    在你的 MoonBit 项目目录运行:

    moon update moon add TWISTY2Ynb/moonrunoff@0.1.0

    在消费者包的 moon.pkg 中声明:

    import { "TWISTY2Ynb/moonrunoff" @runoff }
    options("is-main": true)

    在 main.mbt 中写入以下最小可运行样例:

    fn main raise {
    let t = @runoff.Triangle::new([
    [100.0, 150.0, 180.0],
    [120.0, 180.0],
    [140.0],
    ])
    let p = @runoff.Pattern::fit(t)
    let estimate = @runoff.chain_ladder(t, p)
    assert_true((estimate.total_reserve() - 148.0).abs() < 1.0e-9)
    assert_true(estimate.reconcile().balanced)
    println(estimate.to_json())
    }

    执行 moon run .。按事故期的最终赔款约为 [180,216,252],未付准备金约为 [0,36,112],总计约为 148。输出是 moonrunoff/v1 JSON。Double 有舍入误差,不要使用十进制字符串完全相等作为验收条件。

    #从源码运行三条工作流

    git clone https://github.com/TWISTY2Ynb/moonrunoff.git cd moonrunoff moon run examples/claims moon run examples/exposure moon run examples/review

    claims 从坐标 CSV 导入并估计 CL、对账、输出 JSON;exposure 在同一三角上分别运行 BF、Benktander 和 Cape Cod;review 使用因子窗口/手工覆盖和尾因子,输出残差、历史留出与压力报告。每个例子含结果断言,失败时进程返回非零。

    #CLI 与服务接入

    以下命令适用于 Bash 和 PowerShell;JSON 用单引号括起:

    moon run cmd/moonrunoff -- --request '{"triangle":[[100,150,180],[120,180],[140]]}' moon run cmd/moonrunoff -- --request '{"triangle":[[100,150,180],[120,180],[140]],"estimator":"bornhuetter-ferguson","prior":[200,220,240]}' moon run cmd/moonrunoff -- --request '{"triangle":[[100,150,180],[120,190],[140]],"action":"backtest"}' moon run cmd/moonrunoff -- --request '{"triangle":[[100,150,180],[120,180],[140]],"action":"stress","shock":0.02}'

    CLI 非法请求返回非零状态,由 MoonBit 运行时报告错误。嵌入服务时直接调用 execute_request(text) 接收 RunoffError,或调用 execute_request_result(text) 得到版本化错误 JSON;后者只返回字符串,不替宿主设置退出码。CLI 不读文件、不联网,不把请求写入磁盘;大批量或敏感数据推荐直接调用库,不通过命令行参数传递。

    #因子选择和先验模型

    selected_factors(t, average=Volume, recent=2, excluded=[(1,0)], overrides=[(2,1.1)]) 返回逐发展期的 factor、样本数、零基数计数和分子/分母;坐标均从 0 开始。recent=0 使用全部配对;最近窗口按原始事故期选择,随后应用排除,不回填更早配对。手工覆盖对应统计样本数为 0,明确表示没有拟合。

    Pattern::new(factors, tail=1.05) 构造固定模式。CDF 包含尾因子一次。bornhuetter_ferguson(t,p,prior) 的 prior 是预期最终赔款,不是保费;benktander(...,iterations=2) 从 prior 出发执行两次 BF,次数为 1–1000;cape_cod(t,p,exposure) 要求每个暴露量严格为正,返回拟合赔付率和估计。

    estimate.payments() 给出 origin/age/calendar/amount/is_tail;尾支付位于 age=n 单独标记,不挤入最后一个观测发展期。BF 等模型的准备金按同一模式的剩余发展权重分配,并非把 CL 现金流直接照搬。calendar_payments() 数组索引为从 0 开始的日历期;历史期为 0。discounted_reserves(rates) 接受 n 个远期利率,第一项作用于第一个未来日历期,负利率必须大于 -1。

    #数据合同与失败行为

    • 阶数 2–128;第 i 行恰有 n−i 个单元格;只接受有限、非负、同行不下降的累计已付赔款。增量模式也要求每个增量非负。不接受内部缺失、负追偿和不规则三角。
    • Triangle 和 Pattern 复制构造输入及导出数组。单位由调用者统一,Double 不用于精确货币记账。
    • x=0 的发展配对跳过并披露;没有正基数配对时拒绝估计,需要显式因子覆盖。溢出、零分母和非法选择会返回分类错误,不输出看似有效的 Infinity/NaN。
    • JSON/CSV 最多 1 Mi 个 UTF-16 代码单元;JSON 深度最多 16、拒绝重复键;CSV 最多 8256 个单元格,要求 origin,age,value 表头,不支持引号、注释和额外列。
    • 错误分类为 Shape、Value、Selection、Numerical、Parse。错误文本用于诊断,不作为稳定机器协议。
    • 回测仅预测下一条已知对角线中已存在事故期的单元格,最早事故期缺乏下一期因子、新事故期没有历史基数,均不纳入。每个截面重新拟合;未来观测不会参与拟合。
    • 残差是描述统计,不是 Mack 标准误;不提供 bootstrap、风险资本分位数、自动尾拟合、寿险生命表、IFRS/监管合规或财务建议。历史准确不证明模式适用于未来。

    #开发和复现

    python tools/check_toolchain.py moon info moon fmt --check moon check --deny-warn moon test --deny-warn moon build --deny-warn python tools/differential.py moon test --deny-warn --enable-coverage moon coverage report -- -f summary

    四个目标使用 --target wasm-gc|wasm|js|native。CI 在每个目标执行 check/build/test、三个例子和 CLI;在 wasm-gc 上额外执行标准库 Fraction 独立差分和覆盖率报告。差分生成固定种子的 12 个 2–9 阶正整数三角,3 类平均 × 4 类模型,共 144 组请求,不依赖复制自上游的数据夹具。更多结果、平台边界和审计口径见验证说明。

    #许可证、参考与 AI 使用

    原创实现采用 MIT。数学方法及功能范围参考 chainladder-python(MPL-2.0)和 R ChainLadder(GPL);没有复制它们的实现、注释或数据。core 为 MoonBit 官方运行依赖。权利边界见 NOTICE。AI 参与选题检索、设计、代码、测试和文档,验证记录以实际命令和 CI 结果为准。

    RunoffError

    pub(all) suberror RunoffError {
    Shape(String)
    Value(String)
    Selection(String)
    Numerical(String)
    Parse(String)
    } derive(
    Debug
    )

    Error categories are stable; messages add context, never change behavior.

    Average

    pub(all) enum Average {
    Volume
    Simple
    Regression
    } derive(Eq,
    Debug
    )

    Three classical paired-observation estimators, without automatic trimming.

    Average::equal

    fn Average::equal(Average, Average) -> Bool

    Average::not_equal

    fn Average::not_equal(x : Average, y : Average) -> Bool

    Average::to_repr

    BacktestScore

    pub(all) struct BacktestScore {
    count : Int
    bias : Double
    mean_absolute_error : Double
    root_mean_square_error : Double
    weighted_absolute_percentage_error : Double?
    } derive(
    Debug
    )

    CapeCodResult

    pub struct CapeCodResult {
    // private fields
    }

    CapeCodResult::adjusted_exposure

    fn CapeCodResult::adjusted_exposure(self : CapeCodResult) -> Double

    CapeCodResult::estimate

    fn CapeCodResult::estimate(self : CapeCodResult) -> Estimate

    CapeCodResult::rate

    fn CapeCodResult::rate(self : CapeCodResult) -> Double

    Estimate

    pub struct Estimate {
    // private fields
    }

    Full estimate carries the selected pattern for reproducible cash flows.

    Estimate::calendar_payments

    fn Estimate::calendar_payments(self : Estimate) -> Array[Double] raise RunoffError

    Estimate::discounted_reserves

    fn Estimate::discounted_reserves(self : Estimate, forward_rates : Array[Double]) -> Array[Double] raise RunoffError

    Origin-level deterministic discounting. Rates are per calendar period, beginning at the first future diagonal (calendar=triangle order).

    Estimate::estimator

    fn Estimate::estimator(self : Estimate) -> String

    Estimate::origins

    fn Estimate::origins(self : Estimate) -> Array[OriginEstimate]

    Estimate::pattern

    fn Estimate::pattern(self : Estimate) -> Pattern

    Estimate::payments

    fn Estimate::payments(self : Estimate) -> Array[Payment] raise RunoffError

    Allocates each method's reserve by remaining pattern weights. Tail is a separate final period (age=order), not observed development.

    Estimate::projected_cumulative

    fn Estimate::projected_cumulative(self : Estimate, triangle : Triangle) -> Array[Array[Double]] raise RunoffError

    Complete cumulative development table, preserving every observed value. Includes an additional tail column only when tail>1.

    Estimate::reconcile

    fn Estimate::reconcile(self : Estimate, relative_tolerance? : Double) -> Reconciliation raise RunoffError

    Independently sums all three result views with a scale-aware tolerance. Overflow remains an error, even when relative tolerances are large.

    Estimate::to_csv

    fn Estimate::to_csv(self : Estimate) -> String

    Coordinate CSV result, numeric-only cells safe for downstream CSV readers.

    Estimate::to_json

    fn Estimate::to_json(self : Estimate) -> String raise RunoffError

    Versioned result schema uses zero-based origins, ages and calendars.

    Estimate::total_latest

    fn Estimate::total_latest(self : Estimate) -> Double raise RunoffError

    Estimate::total_reserve

    fn Estimate::total_reserve(self : Estimate) -> Double raise RunoffError

    Estimate::total_ultimate

    fn Estimate::total_ultimate(self : Estimate) -> Double raise RunoffError

    FactorStat

    pub(all) struct FactorStat {
    age : Int
    factor : Double
    pairs : Int
    skipped_zero : Int
    numerator : Double
    denominator : Double
    } derive(
    Debug
    )

    One development period's transparent fitted statistics.

    FitSummary

    pub(all) struct FitSummary {
    pairs : Int
    zero_bases : Int
    mean_absolute_residual : Double
    root_mean_square_residual : Double
    maximum_absolute_residual : Double
    } derive(
    Debug
    )

    HoldoutCell

    pub(all) struct HoldoutCell {
    cutoff : Int
    origin : Int
    age : Int
    predicted : Double
    observed : Double
    error : Double
    } derive(
    Debug
    )

    OriginEstimate

    pub(all) struct OriginEstimate {
    origin : Int
    age : Int
    latest : Double
    ultimate : Double
    reserve : Double
    } derive(
    Debug
    )

    PairDiagnostic

    pub(all) struct PairDiagnostic {
    origin : Int
    age : Int
    base : Double
    observed : Double
    expected : Double
    residual : Double
    ratio : Double?
    } derive(
    Debug
    )

    Pattern

    pub struct Pattern {
    // private fields
    }

    Immutable selected age-to-age factors and one final tail multiplier.

    Pattern::cumulative_factors

    fn Pattern::cumulative_factors(self : Pattern) -> Array[Double]

    Pattern::factors

    fn Pattern::factors(self : Pattern) -> Array[Double]

    Pattern::fit

    fn Pattern::fit(triangle : Triangle, average? : Average, tail? : Double) -> Pattern raise RunoffError

    Pattern::from_json

    fn Pattern::from_json(text : String) -> Pattern raise RunoffError

    Pattern::new

    fn Pattern::new(factors : Array[Double], tail? : Double) -> Pattern raise RunoffError

    Pattern::order

    fn Pattern::order(self : Pattern) -> Int

    Pattern::reported_fraction

    fn Pattern::reported_fraction(self : Pattern, age : Int) -> Double raise RunoffError

    Pattern::tail

    fn Pattern::tail(self : Pattern) -> Double

    Pattern::to_json

    fn Pattern::to_json(self : Pattern) -> String

    Payment

    pub(all) struct Payment {
    origin : Int
    age : Int
    calendar : Int
    amount : Double
    is_tail : Bool
    } derive(
    Debug
    )

    Payment::to_repr

    Reconciliation

    pub(all) struct Reconciliation {
    latest : Double
    ultimate : Double
    reserve : Double
    payment_total : Double
    calendar_total : Double
    maximum_origin_error : Double
    tolerance : Double
    balanced : Bool
    } derive(
    Debug
    )

    StressResult

    pub(all) struct StressResult {
    name : String
    base_reserve : Double
    stressed_reserve : Double
    change : Double
    relative_change : Double?
    } derive(
    Debug
    )

    Triangle

    pub struct Triangle {
    // private fields
    }

    An immutable-by-contract square cumulative paid-loss upper triangle.

    Triangle::at

    fn Triangle::at(self : Triangle, origin : Int, age : Int) -> Double raise RunoffError

    Triangle::at_calendar

    fn Triangle::at_calendar(self : Triangle, calendar : Int) -> Triangle raise RunoffError

    Historical valuation: retain only origin+age<=calendar. No future leakage.

    Triangle::calendar_totals

    fn Triangle::calendar_totals(self : Triangle) -> Array[Double] raise RunoffError

    Calendar-diagonal observed incremental sums, including early diagonals.

    Triangle::combine

    fn Triangle::combine(self : Triangle, other : Triangle) -> Triangle raise RunoffError

    Add portfolios cell-wise before fitting. This does not average fitted factors.

    Triangle::cumulative

    fn Triangle::cumulative(self : Triangle) -> Array[Array[Double]]

    A defensive copy; modifying it cannot mutate this triangle.

    Triangle::development_totals

    fn Triangle::development_totals(self : Triangle) -> Array[Double] raise RunoffError

    Development-period observed incremental sums; each period has fewer origins.

    Triangle::from_csv

    fn Triangle::from_csv(text : String, incremental? : Bool) -> Triangle raise RunoffError

    Strict numeric CSV: origin,age,value; no quoting, comments or missing cells. Cell order is irrelevant, but duplicate coordinates are errors.

    Triangle::from_incremental

    fn Triangle::from_incremental(rows : Array[Array[Double]]) -> Triangle raise RunoffError

    Construct from nonnegative incremental observations, checking accumulation.

    Triangle::from_json

    fn Triangle::from_json(text : String, incremental? : Bool) -> Triangle raise RunoffError

    Bare JSON array-of-arrays, without hidden origin labels or coercion.

    Triangle::incremental

    fn Triangle::incremental(self : Triangle) -> Array[Array[Double]]

    Convert cumulative data to independent incremental rows.

    Triangle::latest

    fn Triangle::latest(self : Triangle) -> Array[Double]

    Triangle::new

    fn Triangle::new(rows : Array[Array[Double]]) -> Triangle raise RunoffError

    Construct from cumulative observations. Copies all nested arrays.

    Triangle::observed_count

    fn Triangle::observed_count(self : Triangle) -> Int

    Triangle::order

    fn Triangle::order(self : Triangle) -> Int

    Triangle::restate

    fn Triangle::restate(self : Triangle, weights : Array[Double]) -> Triangle raise RunoffError

    Restate all cumulative claims of an origin by a positive exposure/trend weight. Factors must be refitted explicitly after restatement.

    Triangle::scale

    fn Triangle::scale(self : Triangle, factor : Double) -> Triangle raise RunoffError

    Scale all observations. Zero is allowed; factor fitting may then fail.

    Triangle::to_csv

    fn Triangle::to_csv(self : Triangle, incremental? : Bool) -> String

    Triangle::to_json

    fn Triangle::to_json(self : Triangle, incremental? : Bool) -> String

    backtest_json

    fn backtest_json(cells : Array[HoldoutCell]) -> String raise RunoffError

    backtest_score

    fn backtest_score(cells : Array[HoldoutCell]) -> BacktestScore raise RunoffError

    benktander

    fn benktander(triangle : Triangle, pattern : Pattern, prior : Array[Double], iterations? : Int) -> Estimate raise RunoffError

    Conceptual iteration zero is the prior; the public API starts at one. Each subsequent iteration feeds the previous ultimate into BF.

    bornhuetter_ferguson

    fn bornhuetter_ferguson(triangle : Triangle, pattern : Pattern, prior : Array[Double]) -> Estimate raise RunoffError

    Prior is expected ultimate, not premium. No fitted exposure assumptions.

    cape_cod

    fn cape_cod(triangle : Triangle, pattern : Pattern, exposure : Array[Double]) -> CapeCodResult raise RunoffError

    Exposures share one unit and must be positive. Rate is fitted to paid claims.

    chain_ladder

    fn chain_ladder(triangle : Triangle, pattern : Pattern) -> Estimate raise RunoffError

    compare_stress

    fn compare_stress(triangle : Triangle, base : Pattern, stressed : Pattern, name? : String) -> StressResult raise RunoffError

    development_factors

    fn development_factors(triangle : Triangle, average? : Average) -> Array[FactorStat] raise RunoffError

    Fit all observed development periods using all available pairs.

    diagnostics_json

    fn diagnostics_json(triangle : Triangle, pattern : Pattern) -> String raise RunoffError

    execute_request

    fn execute_request(text : String) -> String raise RunoffError

    Pure request engine shared by the CLI, WASM hosts and services. Unknown fields and irrelevant model controls are errors, not silent fallbacks.

    execute_request_result

    fn execute_request_result(text : String) -> String

    Error output remains valid JSON, even for quotes or non-ASCII messages.

    factor_sensitivity

    fn factor_sensitivity(triangle : Triangle, pattern : Pattern, shock? : Double) -> Array[StressResult] raise RunoffError

    One-at-a-time upward factor shocks, preserving the base pattern.

    fit_summary

    fn fit_summary(triangle : Triangle, pattern : Pattern) -> FitSummary raise RunoffError

    holdout

    fn holdout(triangle : Triangle, cutoff : Int, average? : Average) -> Array[HoldoutCell] raise RunoffError

    Fit solely to calendar<=cutoff; predict the next observed diagonal. New origins with no observation at cutoff are excluded.

    pair_diagnostics

    fn pair_diagnostics(triangle : Triangle, pattern : Pattern) -> Array[PairDiagnostic] raise RunoffError

    Raw residuals are descriptive, not Mack standard errors.

    rolling_holdout

    fn rolling_holdout(triangle : Triangle, average? : Average) -> Array[HoldoutCell] raise RunoffError

    selected_factors

    fn selected_factors(triangle : Triangle, average? : Average, recent? : Int, excluded? : Array[(Int, Int)], overrides? : Array[(Int, Double)]) -> Array[FactorStat] raise RunoffError

    Explicit exclusions are (origin, development-age) pairs. Recent=0 uses all.

    stress_json

    fn stress_json(results : Array[StressResult]) -> String raise RunoffError

    stress_pattern

    fn stress_pattern(pattern : Pattern, multipliers : Array[Double], tail_multiplier? : Double) -> Pattern raise RunoffError

    Multipliers apply to age-to-age factors (not reserves). Tail stays explicit.