@actuarial-ts/core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/canonical.d.ts +19 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +118 -0
- package/dist/canonical.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +58 -18
- package/dist/mack.js.map +1 -1
- package/dist/odpBootstrap.d.ts +8 -2
- package/dist/odpBootstrap.d.ts.map +1 -1
- package/dist/odpBootstrap.js +11 -2
- package/dist/odpBootstrap.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +4 -2
- package/src/benktander.ts +90 -0
- package/src/berquist.ts +349 -0
- package/src/bf.ts +129 -0
- package/src/canonical.ts +122 -0
- package/src/capping.ts +295 -0
- package/src/caseOutstanding.ts +270 -0
- package/src/chainladder.ts +101 -0
- package/src/clark.ts +719 -0
- package/src/diagnostics.ts +435 -0
- package/src/discounting.ts +417 -0
- package/src/elrMethods.ts +257 -0
- package/src/factors.ts +147 -0
- package/src/fisherLange.ts +374 -0
- package/src/freqSev.ts +152 -0
- package/src/ilf.ts +567 -0
- package/src/index.ts +29 -0
- package/src/mack.ts +329 -0
- package/src/merzWuthrich.ts +147 -0
- package/src/munichChainLadder.ts +398 -0
- package/src/odpBootstrap.ts +337 -0
- package/src/onlevel.ts +155 -0
- package/src/salvageSubro.ts +205 -0
- package/src/stochastic.ts +151 -0
- package/src/tail.ts +156 -0
- package/src/trend.ts +150 -0
- package/src/triangle.ts +235 -0
- package/src/triangleAlgebra.ts +111 -0
- package/src/types.ts +357 -0
- package/src/ulae.ts +326 -0
- package/src/util.ts +68 -0
package/src/mack.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type { MackResult, MackRow, Triangle } from "./types.js";
|
|
2
|
+
import { ReservingError } from "./types.js";
|
|
3
|
+
import { isNum, lastObservedIndex } from "./util.js";
|
|
4
|
+
|
|
5
|
+
export interface MackOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Per-column selected LDFs (null = unselected, treated as 1.000 exactly
|
|
8
|
+
* like the chain ladder does). When omitted the projection uses the
|
|
9
|
+
* volume-weighted factors, which is Mack (1993) as published.
|
|
10
|
+
*/
|
|
11
|
+
selected?: (number | null)[];
|
|
12
|
+
/** Multiplicative tail factor beyond the last observed age (1 = none). */
|
|
13
|
+
tailFactor?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mack (1993) distribution-free chain ladder standard errors, alpha = 1,
|
|
18
|
+
* extended per Mack (1999) to selected development factors and a tail:
|
|
19
|
+
*
|
|
20
|
+
* - f_k = sum(C_{i,k+1}) / sum(C_{i,k}) over rows with both cells observed
|
|
21
|
+
* - s^2_k = 1/(n_k - 1) * sum C_{i,k} (F_{ik} - f_k)^2, always estimated
|
|
22
|
+
* around the volume-weighted f_k (the data-driven estimator) even when
|
|
23
|
+
* the projection uses selected factors
|
|
24
|
+
* - s^2 for the final column is extrapolated per Mack:
|
|
25
|
+
* min(s^4_{K-2}/s^2_{K-3}, min(s^2_{K-3}, s^2_{K-2}))
|
|
26
|
+
* - se(R_i)^2 = C_ult^2 * sum_k (s^2_k / f*_k^2) (1/C_ik + 1/sum_j C_jk)
|
|
27
|
+
* with f* the projection factors and projected C below the diagonal
|
|
28
|
+
* - a tail step (tailFactor > 1) extends the sum by one column, with s^2
|
|
29
|
+
* extrapolated once more by the same rule and the final column's volume
|
|
30
|
+
* as its denominator - an approximation, flagged in warnings
|
|
31
|
+
* - the total includes Mack's cross-covariance term between accident years
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Mack's base estimators: volume-weighted development factors f_k, their
|
|
35
|
+
* column volumes (sum of C_{i,k} over the rows used), the per-column pair
|
|
36
|
+
* counts, and the DATA-ESTIMATED sigma^2_k (null where fewer than two
|
|
37
|
+
* factors exist — extrapolation is runMack's business, not the estimator's).
|
|
38
|
+
* Shared by runMack and the residual diagnostics so the two can never drift.
|
|
39
|
+
*/
|
|
40
|
+
export function mackEstimators(tri: Triangle): {
|
|
41
|
+
f: number[];
|
|
42
|
+
denomSums: number[];
|
|
43
|
+
counts: number[];
|
|
44
|
+
sigma2: (number | null)[];
|
|
45
|
+
} {
|
|
46
|
+
const n = tri.origins.length;
|
|
47
|
+
const K = tri.ages.length;
|
|
48
|
+
if (K < 2) {
|
|
49
|
+
throw new ReservingError("TOO_SMALL", "Mack requires at least two development ages");
|
|
50
|
+
}
|
|
51
|
+
const f: number[] = [];
|
|
52
|
+
const denomSums: number[] = [];
|
|
53
|
+
const counts: number[] = [];
|
|
54
|
+
for (let k = 0; k < K - 1; k++) {
|
|
55
|
+
let num = 0;
|
|
56
|
+
let den = 0;
|
|
57
|
+
let count = 0;
|
|
58
|
+
for (let i = 0; i < n; i++) {
|
|
59
|
+
const c0 = tri.values[i]![k] ?? null;
|
|
60
|
+
const c1 = tri.values[i]![k + 1] ?? null;
|
|
61
|
+
if (isNum(c0) && isNum(c1) && c0 > 0) {
|
|
62
|
+
num += c1;
|
|
63
|
+
den += c0;
|
|
64
|
+
count++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (den <= 0) {
|
|
68
|
+
throw new ReservingError(
|
|
69
|
+
"NO_FACTOR",
|
|
70
|
+
`Development column ${tri.ages[k]}-${tri.ages[k + 1]} has no usable factors for Mack`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
f.push(num / den);
|
|
74
|
+
denomSums.push(den);
|
|
75
|
+
counts.push(count);
|
|
76
|
+
}
|
|
77
|
+
const sigma2: (number | null)[] = new Array(K - 1).fill(null);
|
|
78
|
+
for (let k = 0; k < K - 1; k++) {
|
|
79
|
+
let sum = 0;
|
|
80
|
+
let count = 0;
|
|
81
|
+
for (let i = 0; i < n; i++) {
|
|
82
|
+
const c0 = tri.values[i]![k] ?? null;
|
|
83
|
+
const c1 = tri.values[i]![k + 1] ?? null;
|
|
84
|
+
if (isNum(c0) && isNum(c1) && c0 > 0) {
|
|
85
|
+
const F = c1 / c0;
|
|
86
|
+
sum += c0 * (F - f[k]!) ** 2;
|
|
87
|
+
count++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
sigma2[k] = count > 1 ? sum / (count - 1) : null;
|
|
91
|
+
}
|
|
92
|
+
return { f, denomSums, counts, sigma2 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Mack's sigma^2 extrapolation for columns the data cannot estimate (fewer
|
|
97
|
+
* than two observed factors - usually only the final column):
|
|
98
|
+
* sigma^2_k = min(sigma^4_{k-1} / sigma^2_{k-2}, sigma^2_{k-2}, sigma^2_{k-1}),
|
|
99
|
+
* Mack (1993), also eq. (4.1) of Merz-Wuthrich (2008). Shared by runMack and
|
|
100
|
+
* runMerzWuthrich so the two can never disagree on the final column.
|
|
101
|
+
*
|
|
102
|
+
* Takes the raw per-column estimates (null = not estimable) and returns the
|
|
103
|
+
* completed array; when the min-rule inputs are unavailable it falls back to
|
|
104
|
+
* the prior column's value, then 0, pushing a warning either way.
|
|
105
|
+
*/
|
|
106
|
+
export function extrapolateSigma2(
|
|
107
|
+
sigma2Raw: (number | null)[],
|
|
108
|
+
ages: number[],
|
|
109
|
+
warnings: string[],
|
|
110
|
+
): number[] {
|
|
111
|
+
const sigma2: number[] = sigma2Raw.map((s) => (s === null ? NaN : s));
|
|
112
|
+
for (let k = 0; k < sigma2.length; k++) {
|
|
113
|
+
if (!Number.isNaN(sigma2[k]!)) continue;
|
|
114
|
+
const s2a = k >= 2 ? sigma2[k - 2]! : NaN;
|
|
115
|
+
const s2b = k >= 1 ? sigma2[k - 1]! : NaN;
|
|
116
|
+
if (isNum(s2a) && isNum(s2b) && s2a > 0) {
|
|
117
|
+
sigma2[k] = Math.min((s2b * s2b) / s2a, Math.min(s2a, s2b));
|
|
118
|
+
} else if (isNum(s2b)) {
|
|
119
|
+
sigma2[k] = s2b;
|
|
120
|
+
warnings.push(
|
|
121
|
+
`sigma^2 for the ${ages[k]}-${ages[k + 1]} column could not use Mack's extrapolation; reused the prior column's value`,
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
sigma2[k] = 0;
|
|
125
|
+
warnings.push(
|
|
126
|
+
`sigma^2 for the ${ages[k]}-${ages[k + 1]} column is not estimable; set to 0 (standard errors understated)`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return sigma2;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
|
|
134
|
+
const n = tri.origins.length;
|
|
135
|
+
const K = tri.ages.length;
|
|
136
|
+
const warnings: string[] = [];
|
|
137
|
+
|
|
138
|
+
const estimators = mackEstimators(tri);
|
|
139
|
+
const f = estimators.f;
|
|
140
|
+
const denomSums = estimators.denomSums;
|
|
141
|
+
|
|
142
|
+
// Projection factors: the caller's selections when provided (nulls and
|
|
143
|
+
// non-positive values become 1.000, mirroring the chain ladder), else the
|
|
144
|
+
// volume-weighted estimates - which reproduces Mack (1993) exactly.
|
|
145
|
+
let fEff: number[] = f;
|
|
146
|
+
if (options.selected !== undefined) {
|
|
147
|
+
if (options.selected.length !== K - 1) {
|
|
148
|
+
throw new ReservingError(
|
|
149
|
+
"SELECTION_SHAPE",
|
|
150
|
+
`Expected ${K - 1} LDF selections (one per development interval), got ${options.selected.length}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
fEff = options.selected.map((s, k) => {
|
|
154
|
+
if (s === null || s === undefined) return 1;
|
|
155
|
+
if (!isNum(s) || s <= 0) {
|
|
156
|
+
warnings.push(
|
|
157
|
+
`Selected LDF for ${tri.ages[k]}-${tri.ages[k + 1]} months is not positive; treated as 1.000`,
|
|
158
|
+
);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
return s;
|
|
162
|
+
});
|
|
163
|
+
const differs = fEff.some((v, k) => Math.abs(v - f[k]!) > 1e-9);
|
|
164
|
+
if (differs) {
|
|
165
|
+
warnings.push(
|
|
166
|
+
"Standard errors pair the selected development factors with sigma^2 estimated around the volume-weighted factors (Mack 1999)",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const tail = options.tailFactor ?? 1;
|
|
171
|
+
if (!isNum(tail) || tail <= 0) {
|
|
172
|
+
throw new ReservingError("BAD_TAIL", "Tail factor must be a positive number");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// sigma^2_k estimates: data-estimated where possible; Mack's extrapolation
|
|
176
|
+
// fills columns with a single factor (usually the last).
|
|
177
|
+
const sigma2 = extrapolateSigma2(estimators.sigma2, tri.ages, warnings);
|
|
178
|
+
|
|
179
|
+
// Tail step variance: extrapolate sigma^2 one more column by Mack's rule
|
|
180
|
+
// and reuse the final column's volume as its denominator (approximation).
|
|
181
|
+
let sigma2Tail = 0;
|
|
182
|
+
let denomTail = 0;
|
|
183
|
+
if (tail !== 1) {
|
|
184
|
+
const s2a = K >= 3 ? sigma2[K - 3]! : NaN;
|
|
185
|
+
const s2b = sigma2[K - 2]!;
|
|
186
|
+
if (isNum(s2a) && isNum(s2b) && s2a > 0) {
|
|
187
|
+
sigma2Tail = Math.min((s2b * s2b) / s2a, Math.min(s2a, s2b));
|
|
188
|
+
} else if (isNum(s2b)) {
|
|
189
|
+
sigma2Tail = s2b;
|
|
190
|
+
}
|
|
191
|
+
denomTail = denomSums[K - 2]!;
|
|
192
|
+
warnings.push(
|
|
193
|
+
"The tail step's standard-error contribution extrapolates sigma^2 beyond the observed columns and reuses the final column's volume; treat it as approximate (Mack 1999)",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Project the full rectangle with the projection factors.
|
|
198
|
+
const projected: number[][] = tri.values.map((row) => {
|
|
199
|
+
const out: number[] = new Array(K).fill(NaN);
|
|
200
|
+
const last = lastObservedIndex(row);
|
|
201
|
+
for (let j = 0; j <= last; j++) out[j] = row[j] ?? NaN;
|
|
202
|
+
for (let j = last + 1; j < K; j++) out[j] = out[j - 1]! * fEff[j - 1]!;
|
|
203
|
+
return out;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const rows: MackRow[] = [];
|
|
207
|
+
const mseByRow: number[] = new Array(n).fill(0);
|
|
208
|
+
for (let i = 0; i < n; i++) {
|
|
209
|
+
const last = lastObservedIndex(tri.values[i]!);
|
|
210
|
+
if (last < 0) continue;
|
|
211
|
+
const latest = tri.values[i]![last]!;
|
|
212
|
+
const ultimate = projected[i]![K - 1]! * tail;
|
|
213
|
+
// mse(R_i) accumulated over the projected development range plus tail.
|
|
214
|
+
let mse = 0;
|
|
215
|
+
let droppedColumns = 0;
|
|
216
|
+
for (let k = last; k < K - 1; k++) {
|
|
217
|
+
const cik = projected[i]![k]!;
|
|
218
|
+
if (!(cik > 0)) {
|
|
219
|
+
// Mack's 1/C_ik is undefined for a non-positive cumulative. Skipping
|
|
220
|
+
// the column is the only defensible arithmetic, but it removes real
|
|
221
|
+
// variance from the estimate, so the result must not present what
|
|
222
|
+
// remains as if it were the whole story.
|
|
223
|
+
droppedColumns++;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
mse += (sigma2[k]! / fEff[k]! ** 2) * (1 / cik + 1 / denomSums[k]!);
|
|
227
|
+
}
|
|
228
|
+
if (droppedColumns > 0) {
|
|
229
|
+
warnings.push(
|
|
230
|
+
`Origin ${tri.origins[i]} has a non-positive projected cumulative at ` +
|
|
231
|
+
`${droppedColumns} development step(s); those columns are excluded from its ` +
|
|
232
|
+
`standard error, which is therefore understated`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (tail !== 1 && projected[i]![K - 1]! > 0) {
|
|
236
|
+
mse += (sigma2Tail / tail ** 2) * (1 / projected[i]![K - 1]! + 1 / denomTail);
|
|
237
|
+
}
|
|
238
|
+
mse *= ultimate ** 2;
|
|
239
|
+
mseByRow[i] = mse;
|
|
240
|
+
const reserve = ultimate - latest;
|
|
241
|
+
rows.push({
|
|
242
|
+
origin: tri.origins[i]!,
|
|
243
|
+
latest,
|
|
244
|
+
ultimate,
|
|
245
|
+
reserve,
|
|
246
|
+
standardError: Math.sqrt(mse),
|
|
247
|
+
cv: reserve !== 0 ? Math.sqrt(mse) / reserve : null,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Total mse: sum of row mse plus the cross-covariance between every PAIR of
|
|
252
|
+
// accident years (Mack 1993 corollary). The tail step participates like one
|
|
253
|
+
// more development column.
|
|
254
|
+
//
|
|
255
|
+
// Two accident years share estimation error only over the columns they have
|
|
256
|
+
// BOTH yet to traverse, so each pair's sum starts at the maturity of the more
|
|
257
|
+
// developed of the two. That floor is per-pair: aggregating the later rows'
|
|
258
|
+
// ultimates before choosing it would assume maturity falls with row index —
|
|
259
|
+
// true of a tidy run-off triangle, false of a ragged or unsorted one, and the
|
|
260
|
+
// resulting total moved by up to 65% purely with row order.
|
|
261
|
+
let totalMse = 0;
|
|
262
|
+
for (let i = 0; i < n; i++) totalMse += mseByRow[i]!;
|
|
263
|
+
|
|
264
|
+
const maturity = tri.values.map((row) => lastObservedIndex(row));
|
|
265
|
+
const ultimateOf = (i: number): number => projected[i]![K - 1]! * tail;
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < n; i++) {
|
|
268
|
+
if (maturity[i]! < 0) continue;
|
|
269
|
+
// Mack's 1/C terms are undefined for a non-positive ultimate; the row
|
|
270
|
+
// estimate skips such columns for the same reason (see above).
|
|
271
|
+
const ui = ultimateOf(i);
|
|
272
|
+
if (!(ui > 0)) continue;
|
|
273
|
+
|
|
274
|
+
// Partners are grouped by the floor their pair uses, and each group's
|
|
275
|
+
// ultimates are summed BEFORE multiplying. On a triangle whose maturity
|
|
276
|
+
// falls with row index every partner shares one floor, so this collapses
|
|
277
|
+
// to a single product in the original operand order — the arithmetic is
|
|
278
|
+
// then identical to the last bit, which is what keeps the frozen
|
|
279
|
+
// conformance corpus reproducible.
|
|
280
|
+
const partnerUltimatesByFloor = new Map<number, number>();
|
|
281
|
+
for (let j = i + 1; j < n; j++) {
|
|
282
|
+
if (maturity[j]! < 0) continue;
|
|
283
|
+
const uj = ultimateOf(j);
|
|
284
|
+
if (!(uj > 0)) continue;
|
|
285
|
+
const floor = Math.max(maturity[i]!, maturity[j]!);
|
|
286
|
+
partnerUltimatesByFloor.set(floor, (partnerUltimatesByFloor.get(floor) ?? 0) + uj);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const [floor, partnerUltimates] of partnerUltimatesByFloor) {
|
|
290
|
+
// Both years already run off: no shared development remains.
|
|
291
|
+
if (tail === 1 && floor >= K - 1) continue;
|
|
292
|
+
|
|
293
|
+
let shared = 0;
|
|
294
|
+
for (let k = floor; k < K - 1; k++) {
|
|
295
|
+
shared += (2 * sigma2[k]!) / fEff[k]! ** 2 / denomSums[k]!;
|
|
296
|
+
}
|
|
297
|
+
if (tail !== 1) {
|
|
298
|
+
shared += (2 * sigma2Tail) / tail ** 2 / denomTail;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
totalMse += ui * partnerUltimates * shared;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const totals = rows.reduce(
|
|
306
|
+
(acc, r) => ({
|
|
307
|
+
latest: acc.latest + r.latest,
|
|
308
|
+
ultimate: acc.ultimate + r.ultimate,
|
|
309
|
+
reserve: acc.reserve + r.reserve,
|
|
310
|
+
}),
|
|
311
|
+
{ latest: 0, ultimate: 0, reserve: 0 },
|
|
312
|
+
);
|
|
313
|
+
const totalSe = Math.sqrt(totalMse);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
method: "mack",
|
|
317
|
+
developmentFactors: fEff,
|
|
318
|
+
sigmaSquared: sigma2,
|
|
319
|
+
tailFactor: tail,
|
|
320
|
+
sigmaSquaredTail: tail !== 1 ? sigma2Tail : undefined,
|
|
321
|
+
rows,
|
|
322
|
+
totals: {
|
|
323
|
+
...totals,
|
|
324
|
+
standardError: totalSe,
|
|
325
|
+
cv: totals.reserve !== 0 ? totalSe / totals.reserve : null,
|
|
326
|
+
},
|
|
327
|
+
warnings,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { MerzWuthrichResult, MerzWuthrichRow, Triangle } from "./types.js";
|
|
2
|
+
import { ReservingError } from "./types.js";
|
|
3
|
+
import { extrapolateSigma2, mackEstimators, runMack } from "./mack.js";
|
|
4
|
+
import { isNum } from "./util.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merz-Wuthrich (2008) one-year claims development result (CDR) msep.
|
|
8
|
+
*
|
|
9
|
+
* Source: Merz, M. & Wuthrich, M.V. (2008), "Modelling the Claims Development
|
|
10
|
+
* Result for Solvency Purposes", CAS E-Forum Fall 2008, 542-568. Implements
|
|
11
|
+
* Result 3.5's closed forms: eq. (3.17) per accident year and eq. (3.18) for
|
|
12
|
+
* the aggregate - the msep of the observable one-year CDR around 0, i.e. the
|
|
13
|
+
* Solvency II / SST one-year reserve-risk quantity. Everything is computable
|
|
14
|
+
* at time I from the observed triangle D_I alone: S_j^{I+1} = S_j^I +
|
|
15
|
+
* C_{I-j,j} adds only the current diagonal element, which is in D_I.
|
|
16
|
+
*
|
|
17
|
+
* Estimators are shared with runMack - volume-weighted fhat_j and Mack's
|
|
18
|
+
* sigma^2_j with the min-rule extrapolation for the final column, which is
|
|
19
|
+
* exactly the paper's eq. (4.1) - and Mack's full-runoff msep is computed via
|
|
20
|
+
* runMack so every row carries the one-year vs ultimate-view comparison.
|
|
21
|
+
*
|
|
22
|
+
* Constraints (Sec. 2 of the paper): the closed forms assume a regular
|
|
23
|
+
* run-off triangle with I = J - as many origin periods as development ages,
|
|
24
|
+
* every cell on or left of the latest diagonal observed and positive, and
|
|
25
|
+
* nothing observed beyond it. Violations throw ReservingError("SHAPE").
|
|
26
|
+
*
|
|
27
|
+
* Caveat: (3.17)/(3.18) are the paper's linear approximations (Appendix A)
|
|
28
|
+
* of the exact product-form formulas. They are the published, industry-
|
|
29
|
+
* standard form (the paper's Table 4 is produced by them), accurate when
|
|
30
|
+
* sigma^2_j / (fhat_j^2 C_{i,j}) << 1, which holds for typical triangles.
|
|
31
|
+
*/
|
|
32
|
+
export function runMerzWuthrich(tri: Triangle): MerzWuthrichResult {
|
|
33
|
+
const n = tri.origins.length;
|
|
34
|
+
const K = tri.ages.length;
|
|
35
|
+
if (n !== K) {
|
|
36
|
+
throw new ReservingError(
|
|
37
|
+
"SHAPE",
|
|
38
|
+
`Merz-Wuthrich requires a square triangle (I = J): got ${n} origin periods by ${K} development ages`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
// Regularity: every cell on or left of the time-I diagonal must be observed
|
|
42
|
+
// and positive (the formulas divide by diagonal cells and the variance
|
|
43
|
+
// assumption needs C > 0); nothing may be observed beyond the diagonal.
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
const row = tri.values[i] ?? [];
|
|
46
|
+
const diag = K - 1 - i;
|
|
47
|
+
for (let j = 0; j <= diag; j++) {
|
|
48
|
+
const v = row[j] ?? null;
|
|
49
|
+
if (!isNum(v) || v <= 0) {
|
|
50
|
+
throw new ReservingError(
|
|
51
|
+
"SHAPE",
|
|
52
|
+
`Merz-Wuthrich requires every cell on or left of the latest diagonal to be observed and positive: origin ${tri.origins[i]} at age ${tri.ages[j]} months is ${isNum(v) ? "non-positive" : "missing"}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (let j = diag + 1; j < K; j++) {
|
|
57
|
+
if (isNum(row[j] ?? null)) {
|
|
58
|
+
throw new ReservingError(
|
|
59
|
+
"SHAPE",
|
|
60
|
+
`Merz-Wuthrich assumes a time-I snapshot: origin ${tri.origins[i]} has an observation beyond the latest diagonal at age ${tri.ages[j]} months`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const warnings: string[] = [];
|
|
67
|
+
const estimators = mackEstimators(tri);
|
|
68
|
+
const f = estimators.f;
|
|
69
|
+
// S_j^I (2.9): column-j volume EXCLUDING the diagonal element C_{I-j,j}.
|
|
70
|
+
const sI = estimators.denomSums;
|
|
71
|
+
const sigma2 = extrapolateSigma2(estimators.sigma2, tri.ages, warnings);
|
|
72
|
+
const I = K - 1; // = J; row i's latest observed cell is column I - i.
|
|
73
|
+
|
|
74
|
+
// sjr(j) = sigmahat_j^2 / fhat_j^2, the paper's recurring ratio.
|
|
75
|
+
const sjr = sigma2.map((s2, j) => s2 / f[j]! ** 2);
|
|
76
|
+
// S_j^{I+1} = S_j^I + C_{I-j,j} (2.10): INCLUDES the diagonal element.
|
|
77
|
+
const sIPlus1 = sI.map((s, j) => s + tri.values[I - j]![j]!);
|
|
78
|
+
|
|
79
|
+
// Chain ladder ultimates Chat_{i,J}^I (2.11).
|
|
80
|
+
const ultimates: number[] = tri.values.map((row, i) => {
|
|
81
|
+
let u = row[I - i]!;
|
|
82
|
+
for (let j = I - i; j < I; j++) u *= f[j]!;
|
|
83
|
+
return u;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Mack's ultimate-view msep on the identical estimators, for comparison.
|
|
87
|
+
const mack = runMack(tri);
|
|
88
|
+
|
|
89
|
+
const rows: MerzWuthrichRow[] = [];
|
|
90
|
+
const msepByRow: number[] = new Array(n).fill(0);
|
|
91
|
+
// Estimation-error piece shared by (3.17) and (3.18)'s cross terms:
|
|
92
|
+
// sjr(I-i)/S_{I-i}^I + sum_{j=I-i+1}^{J-1} (C_{I-j,j}/S_j^{I+1}) sjr(j)/S_j^I
|
|
93
|
+
// (FIRST power of C_{I-j,j}/S_j^{I+1} - the Delta and Phi tails merge).
|
|
94
|
+
const estimationTerm: number[] = new Array(n).fill(0);
|
|
95
|
+
let totalReserve = 0;
|
|
96
|
+
for (let i = 0; i < n; i++) {
|
|
97
|
+
const d = I - i;
|
|
98
|
+
if (i > 0) {
|
|
99
|
+
let laterDiagonals = 0;
|
|
100
|
+
for (let j = d + 1; j <= I - 1; j++) {
|
|
101
|
+
laterDiagonals += (tri.values[I - j]![j]! / sIPlus1[j]!) * (sjr[j]! / sI[j]!);
|
|
102
|
+
}
|
|
103
|
+
estimationTerm[i] = sjr[d]! / sI[d]! + laterDiagonals;
|
|
104
|
+
// (3.17): process term sjr(I-i)/C_{i,I-i} plus the estimation term.
|
|
105
|
+
msepByRow[i] = ultimates[i]! ** 2 * (sjr[d]! / tri.values[i]![d]! + estimationTerm[i]!);
|
|
106
|
+
}
|
|
107
|
+
// i = 0 is fully developed: its CDR is identically 0 (the paper prints
|
|
108
|
+
// this row with reserve 0 and blank volatility cells).
|
|
109
|
+
const cdrMsepRoot = Math.sqrt(msepByRow[i]!);
|
|
110
|
+
const mackMsepRoot = mack.rows[i]!.standardError;
|
|
111
|
+
const reserve = ultimates[i]! - tri.values[i]![d]!;
|
|
112
|
+
totalReserve += reserve;
|
|
113
|
+
rows.push({
|
|
114
|
+
origin: tri.origins[i]!,
|
|
115
|
+
reserve,
|
|
116
|
+
cdrMsepRoot,
|
|
117
|
+
mackMsepRoot,
|
|
118
|
+
oneYearRatio: mackMsepRoot > 0 ? cdrMsepRoot / mackMsepRoot : null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// (3.18): aggregate = sum of the single-year mseps plus cross terms
|
|
123
|
+
// 2 * Chat_i * Chat_k over 0 < i < k <= I, each scaled by the estimation
|
|
124
|
+
// term of the EARLIER accident year i.
|
|
125
|
+
let totalMsep = msepByRow.reduce((a, b) => a + b, 0);
|
|
126
|
+
for (let i = 1; i < n; i++) {
|
|
127
|
+
let laterUltimates = 0;
|
|
128
|
+
for (let k = i + 1; k < n; k++) laterUltimates += ultimates[k]!;
|
|
129
|
+
totalMsep += 2 * ultimates[i]! * laterUltimates * estimationTerm[i]!;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const totalCdrMsepRoot = Math.sqrt(totalMsep);
|
|
133
|
+
const totalMackMsepRoot = mack.totals.standardError;
|
|
134
|
+
return {
|
|
135
|
+
method: "merzWuthrich",
|
|
136
|
+
developmentFactors: f,
|
|
137
|
+
sigmaSquared: sigma2,
|
|
138
|
+
rows,
|
|
139
|
+
totals: {
|
|
140
|
+
reserve: totalReserve,
|
|
141
|
+
cdrMsepRoot: totalCdrMsepRoot,
|
|
142
|
+
mackMsepRoot: totalMackMsepRoot,
|
|
143
|
+
oneYearRatio: totalMackMsepRoot > 0 ? totalCdrMsepRoot / totalMackMsepRoot : null,
|
|
144
|
+
},
|
|
145
|
+
warnings,
|
|
146
|
+
};
|
|
147
|
+
}
|