@holoscript/plugin-insurance 2.0.1 → 2.0.2
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/package.json +4 -4
- package/src/__tests__/actuarial.test.ts +189 -54
- package/src/__tests__/fairness-underwriting.test.ts +6 -4
- package/src/actuarial.ts +135 -117
- package/src/fairness-underwriting.ts +16 -20
- package/src/index.ts +27 -5
- package/src/traits/ClaimTrait.ts +66 -12
- package/src/traits/PolicyTrait.ts +52 -9
- package/src/traits/RiskAssessmentTrait.ts +34 -8
- package/src/traits/UnderwritingTrait.ts +44 -9
- package/src/traits/types.ts +22 -4
- package/tsconfig.json +5 -1
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holoscript/plugin-insurance",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"peerDependencies": {
|
|
6
|
-
"@holoscript/core": "8.0.
|
|
7
|
-
"@holoscript/engine": "6.1.
|
|
6
|
+
"@holoscript/core": ">=8.0.0",
|
|
7
|
+
"@holoscript/engine": ">=6.1.0"
|
|
8
8
|
},
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "vitest run --passWithNoTests",
|
|
12
12
|
"test:coverage": "vitest run --coverage --passWithNoTests"
|
|
13
13
|
}
|
|
14
|
-
}
|
|
14
|
+
}
|
|
@@ -22,27 +22,107 @@ import {
|
|
|
22
22
|
// ─── Illustrative life table (ILT) — truncated 1958 CSO qx values ────────────
|
|
23
23
|
// Ages 0–100 (101 values). Terminal qx[100] = 1.0 (everyone dies by 100).
|
|
24
24
|
const CSO_QX: number[] = [
|
|
25
|
-
0.
|
|
26
|
-
0.
|
|
27
|
-
0.
|
|
28
|
-
0.
|
|
29
|
-
0.
|
|
30
|
-
0.
|
|
31
|
-
0.
|
|
32
|
-
0.
|
|
33
|
-
0.
|
|
34
|
-
0.
|
|
35
|
-
0.
|
|
36
|
-
0.
|
|
37
|
-
0.
|
|
38
|
-
0.
|
|
39
|
-
0.
|
|
40
|
-
0.
|
|
41
|
-
0.
|
|
42
|
-
0.
|
|
43
|
-
0.
|
|
44
|
-
0.
|
|
45
|
-
|
|
25
|
+
0.00453,
|
|
26
|
+
0.00108,
|
|
27
|
+
0.00091,
|
|
28
|
+
0.00083,
|
|
29
|
+
0.00079, // 0-4
|
|
30
|
+
0.00076,
|
|
31
|
+
0.00073,
|
|
32
|
+
0.00072,
|
|
33
|
+
0.00072,
|
|
34
|
+
0.00073, // 5-9
|
|
35
|
+
0.00073,
|
|
36
|
+
0.00077,
|
|
37
|
+
0.00085,
|
|
38
|
+
0.00099,
|
|
39
|
+
0.00115, // 10-14
|
|
40
|
+
0.00133,
|
|
41
|
+
0.00151,
|
|
42
|
+
0.00167,
|
|
43
|
+
0.00178,
|
|
44
|
+
0.00187, // 15-19
|
|
45
|
+
0.00196,
|
|
46
|
+
0.00201,
|
|
47
|
+
0.00202,
|
|
48
|
+
0.00201,
|
|
49
|
+
0.00198, // 20-24
|
|
50
|
+
0.00196,
|
|
51
|
+
0.00196,
|
|
52
|
+
0.00197,
|
|
53
|
+
0.00198,
|
|
54
|
+
0.002, // 25-29
|
|
55
|
+
0.00203,
|
|
56
|
+
0.00206,
|
|
57
|
+
0.00211,
|
|
58
|
+
0.00217,
|
|
59
|
+
0.00225, // 30-34
|
|
60
|
+
0.00236,
|
|
61
|
+
0.0025,
|
|
62
|
+
0.00268,
|
|
63
|
+
0.00289,
|
|
64
|
+
0.00314, // 35-39
|
|
65
|
+
0.00344,
|
|
66
|
+
0.00379,
|
|
67
|
+
0.00419,
|
|
68
|
+
0.00466,
|
|
69
|
+
0.00519, // 40-44
|
|
70
|
+
0.00581,
|
|
71
|
+
0.0065,
|
|
72
|
+
0.00728,
|
|
73
|
+
0.00817,
|
|
74
|
+
0.00918, // 45-49
|
|
75
|
+
0.01038,
|
|
76
|
+
0.01168,
|
|
77
|
+
0.01311,
|
|
78
|
+
0.0147,
|
|
79
|
+
0.01653, // 50-54
|
|
80
|
+
0.01864,
|
|
81
|
+
0.02101,
|
|
82
|
+
0.02358,
|
|
83
|
+
0.02644,
|
|
84
|
+
0.02956, // 55-59
|
|
85
|
+
0.03299,
|
|
86
|
+
0.0367,
|
|
87
|
+
0.04068,
|
|
88
|
+
0.04499,
|
|
89
|
+
0.04965, // 60-64
|
|
90
|
+
0.05477,
|
|
91
|
+
0.06033,
|
|
92
|
+
0.06644,
|
|
93
|
+
0.07304,
|
|
94
|
+
0.08024, // 65-69
|
|
95
|
+
0.08805,
|
|
96
|
+
0.09662,
|
|
97
|
+
0.1058,
|
|
98
|
+
0.1158,
|
|
99
|
+
0.1266, // 70-74
|
|
100
|
+
0.1382,
|
|
101
|
+
0.1506,
|
|
102
|
+
0.1639,
|
|
103
|
+
0.1782,
|
|
104
|
+
0.1936, // 75-79
|
|
105
|
+
0.2103,
|
|
106
|
+
0.2282,
|
|
107
|
+
0.2475,
|
|
108
|
+
0.268,
|
|
109
|
+
0.29, // 80-84
|
|
110
|
+
0.313,
|
|
111
|
+
0.3374,
|
|
112
|
+
0.363,
|
|
113
|
+
0.39,
|
|
114
|
+
0.4182, // 85-89
|
|
115
|
+
0.4476,
|
|
116
|
+
0.4779,
|
|
117
|
+
0.5091,
|
|
118
|
+
0.5411,
|
|
119
|
+
0.5738, // 90-94
|
|
120
|
+
0.607,
|
|
121
|
+
0.6404,
|
|
122
|
+
0.6738,
|
|
123
|
+
0.7069,
|
|
124
|
+
0.7394, // 95-99
|
|
125
|
+
1.0, // 100
|
|
46
126
|
];
|
|
47
127
|
|
|
48
128
|
const ILT = buildLifeTable('1958-CSO', CSO_QX, 0.05);
|
|
@@ -102,7 +182,7 @@ describe('computeActuarialValues — whole_life', () => {
|
|
|
102
182
|
// so NSP is higher (≈ $23,000 for $100k benefit). Verify structural bounds only.
|
|
103
183
|
const r = computeActuarialValues(
|
|
104
184
|
{ type: 'whole_life', issueAge: 35, benefitAmount: 100_000 },
|
|
105
|
-
ILT
|
|
185
|
+
ILT
|
|
106
186
|
);
|
|
107
187
|
expect(r.nsp).toBeGreaterThan(1_000);
|
|
108
188
|
expect(r.nsp).toBeLessThan(30_000);
|
|
@@ -111,26 +191,44 @@ describe('computeActuarialValues — whole_life', () => {
|
|
|
111
191
|
it('annual premium < NSP (premium is paid over lifetime)', () => {
|
|
112
192
|
const r = computeActuarialValues(
|
|
113
193
|
{ type: 'whole_life', issueAge: 35, benefitAmount: 100_000 },
|
|
114
|
-
ILT
|
|
194
|
+
ILT
|
|
115
195
|
);
|
|
116
196
|
expect(r.annualPremium).toBeLessThan(r.nsp);
|
|
117
197
|
});
|
|
118
198
|
|
|
119
199
|
it('older issue age produces higher NSP (higher mortality)', () => {
|
|
120
|
-
const r35 = computeActuarialValues(
|
|
121
|
-
|
|
200
|
+
const r35 = computeActuarialValues(
|
|
201
|
+
{ type: 'whole_life', issueAge: 35, benefitAmount: 100_000 },
|
|
202
|
+
ILT
|
|
203
|
+
);
|
|
204
|
+
const r55 = computeActuarialValues(
|
|
205
|
+
{ type: 'whole_life', issueAge: 55, benefitAmount: 100_000 },
|
|
206
|
+
ILT
|
|
207
|
+
);
|
|
122
208
|
expect(r55.nsp).toBeGreaterThan(r35.nsp);
|
|
123
209
|
});
|
|
124
210
|
|
|
125
211
|
it('higher interest rate produces lower NSP (discounting effect)', () => {
|
|
126
|
-
const r5pct
|
|
127
|
-
|
|
212
|
+
const r5pct = computeActuarialValues(
|
|
213
|
+
{ type: 'whole_life', issueAge: 40, benefitAmount: 100_000, interestRate: 0.05 },
|
|
214
|
+
ILT
|
|
215
|
+
);
|
|
216
|
+
const r10pct = computeActuarialValues(
|
|
217
|
+
{ type: 'whole_life', issueAge: 40, benefitAmount: 100_000, interestRate: 0.1 },
|
|
218
|
+
ILT
|
|
219
|
+
);
|
|
128
220
|
expect(r10pct.nsp).toBeLessThan(r5pct.nsp);
|
|
129
221
|
});
|
|
130
222
|
|
|
131
223
|
it('NSP scales linearly with benefit amount', () => {
|
|
132
|
-
const r100k = computeActuarialValues(
|
|
133
|
-
|
|
224
|
+
const r100k = computeActuarialValues(
|
|
225
|
+
{ type: 'whole_life', issueAge: 40, benefitAmount: 100_000 },
|
|
226
|
+
ILT
|
|
227
|
+
);
|
|
228
|
+
const r200k = computeActuarialValues(
|
|
229
|
+
{ type: 'whole_life', issueAge: 40, benefitAmount: 200_000 },
|
|
230
|
+
ILT
|
|
231
|
+
);
|
|
134
232
|
expect(r200k.nsp).toBeCloseTo(r100k.nsp * 2, 4);
|
|
135
233
|
});
|
|
136
234
|
|
|
@@ -145,7 +243,7 @@ describe('computeActuarialValues — whole_life', () => {
|
|
|
145
243
|
|
|
146
244
|
it('throws for issue age beyond table', () => {
|
|
147
245
|
expect(() =>
|
|
148
|
-
computeActuarialValues({ type: 'whole_life', issueAge: 200, benefitAmount: 1 }, ILT)
|
|
246
|
+
computeActuarialValues({ type: 'whole_life', issueAge: 200, benefitAmount: 1 }, ILT)
|
|
149
247
|
).toThrow();
|
|
150
248
|
});
|
|
151
249
|
});
|
|
@@ -154,19 +252,34 @@ describe('computeActuarialValues — whole_life', () => {
|
|
|
154
252
|
|
|
155
253
|
describe('computeActuarialValues — term_life', () => {
|
|
156
254
|
it('term NSP < whole_life NSP (subset of coverage period)', () => {
|
|
157
|
-
const wl
|
|
158
|
-
|
|
255
|
+
const wl = computeActuarialValues(
|
|
256
|
+
{ type: 'whole_life', issueAge: 35, benefitAmount: 100_000 },
|
|
257
|
+
ILT
|
|
258
|
+
);
|
|
259
|
+
const term = computeActuarialValues(
|
|
260
|
+
{ type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 20 },
|
|
261
|
+
ILT
|
|
262
|
+
);
|
|
159
263
|
expect(term.nsp).toBeLessThan(wl.nsp);
|
|
160
264
|
});
|
|
161
265
|
|
|
162
266
|
it('longer term produces higher NSP', () => {
|
|
163
|
-
const t10 = computeActuarialValues(
|
|
164
|
-
|
|
267
|
+
const t10 = computeActuarialValues(
|
|
268
|
+
{ type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 10 },
|
|
269
|
+
ILT
|
|
270
|
+
);
|
|
271
|
+
const t30 = computeActuarialValues(
|
|
272
|
+
{ type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 30 },
|
|
273
|
+
ILT
|
|
274
|
+
);
|
|
165
275
|
expect(t30.nsp).toBeGreaterThan(t10.nsp);
|
|
166
276
|
});
|
|
167
277
|
|
|
168
278
|
it('policyType is term_life', () => {
|
|
169
|
-
const r = computeActuarialValues(
|
|
279
|
+
const r = computeActuarialValues(
|
|
280
|
+
{ type: 'term_life', issueAge: 35, benefitAmount: 1, termYears: 20 },
|
|
281
|
+
ILT
|
|
282
|
+
);
|
|
170
283
|
expect(r.policyType).toBe('term_life');
|
|
171
284
|
expect(r.termYears).toBe(20);
|
|
172
285
|
});
|
|
@@ -176,13 +289,22 @@ describe('computeActuarialValues — term_life', () => {
|
|
|
176
289
|
|
|
177
290
|
describe('computeActuarialValues — endowment', () => {
|
|
178
291
|
it('endowment NSP ≥ equivalent term_life NSP (adds pure endowment)', () => {
|
|
179
|
-
const term = computeActuarialValues(
|
|
180
|
-
|
|
292
|
+
const term = computeActuarialValues(
|
|
293
|
+
{ type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 20 },
|
|
294
|
+
ILT
|
|
295
|
+
);
|
|
296
|
+
const endt = computeActuarialValues(
|
|
297
|
+
{ type: 'endowment', issueAge: 35, benefitAmount: 100_000, termYears: 20 },
|
|
298
|
+
ILT
|
|
299
|
+
);
|
|
181
300
|
expect(endt.nsp).toBeGreaterThan(term.nsp);
|
|
182
301
|
});
|
|
183
302
|
|
|
184
303
|
it('endowment NSP ≤ benefit amount (PV cannot exceed undiscounted payout)', () => {
|
|
185
|
-
const r = computeActuarialValues(
|
|
304
|
+
const r = computeActuarialValues(
|
|
305
|
+
{ type: 'endowment', issueAge: 35, benefitAmount: 100_000, termYears: 30 },
|
|
306
|
+
ILT
|
|
307
|
+
);
|
|
186
308
|
expect(r.nsp).toBeLessThan(100_000);
|
|
187
309
|
expect(r.nsp).toBeGreaterThan(0);
|
|
188
310
|
});
|
|
@@ -192,18 +314,27 @@ describe('computeActuarialValues — endowment', () => {
|
|
|
192
314
|
|
|
193
315
|
describe('computeActuarialValues — annuity_due', () => {
|
|
194
316
|
it('whole-life annuity NSP = benefit × ä_x (annuityDue)', () => {
|
|
195
|
-
const r = computeActuarialValues(
|
|
317
|
+
const r = computeActuarialValues(
|
|
318
|
+
{ type: 'annuity_due', issueAge: 65, benefitAmount: 10_000 },
|
|
319
|
+
ILT
|
|
320
|
+
);
|
|
196
321
|
expect(r.nsp).toBeCloseTo(r.annuityDue * 10_000, 4);
|
|
197
322
|
});
|
|
198
323
|
|
|
199
324
|
it('temporary annuity NSP < whole-life annuity NSP', () => {
|
|
200
|
-
const wl
|
|
201
|
-
const tmp = computeActuarialValues(
|
|
325
|
+
const wl = computeActuarialValues({ type: 'annuity_due', issueAge: 65, benefitAmount: 1 }, ILT);
|
|
326
|
+
const tmp = computeActuarialValues(
|
|
327
|
+
{ type: 'annuity_due', issueAge: 65, benefitAmount: 1, termYears: 10 },
|
|
328
|
+
ILT
|
|
329
|
+
);
|
|
202
330
|
expect(tmp.nsp).toBeLessThan(wl.nsp);
|
|
203
331
|
});
|
|
204
332
|
|
|
205
333
|
it('annualPremium is 0 for annuity products', () => {
|
|
206
|
-
const r = computeActuarialValues(
|
|
334
|
+
const r = computeActuarialValues(
|
|
335
|
+
{ type: 'annuity_due', issueAge: 60, benefitAmount: 1_000 },
|
|
336
|
+
ILT
|
|
337
|
+
);
|
|
207
338
|
expect(r.annualPremium).toBe(0);
|
|
208
339
|
});
|
|
209
340
|
});
|
|
@@ -218,16 +349,16 @@ describe('computeNPV', () => {
|
|
|
218
349
|
*/
|
|
219
350
|
const cashFlows = [
|
|
220
351
|
{ period: 0, amount: -1000 },
|
|
221
|
-
{ period: 1, amount:
|
|
222
|
-
{ period: 2, amount:
|
|
223
|
-
{ period: 3, amount:
|
|
352
|
+
{ period: 1, amount: 400 },
|
|
353
|
+
{ period: 2, amount: 400 },
|
|
354
|
+
{ period: 3, amount: 400 },
|
|
224
355
|
];
|
|
225
356
|
|
|
226
357
|
it('NPV at 10% is near zero for the ~9.7%-IRR project', () => {
|
|
227
358
|
// IRR ≈ 9.7%; at 10% discount the NPV is small but not exactly zero.
|
|
228
359
|
// Exact NPV = -1000 + 400/1.1 + 400/1.21 + 400/1.331 ≈ -$0.95... but
|
|
229
360
|
// actually: 363.64 + 330.58 + 300.53 = 994.75 → NPV = -5.25.
|
|
230
|
-
const r = computeNPV(cashFlows, 0.
|
|
361
|
+
const r = computeNPV(cashFlows, 0.1);
|
|
231
362
|
expect(Math.abs(r.npv)).toBeLessThan(10); // within $10
|
|
232
363
|
});
|
|
233
364
|
|
|
@@ -237,18 +368,24 @@ describe('computeNPV', () => {
|
|
|
237
368
|
});
|
|
238
369
|
|
|
239
370
|
it('IRR is approximately 9.7%', () => {
|
|
240
|
-
const r = computeNPV(cashFlows, 0.
|
|
371
|
+
const r = computeNPV(cashFlows, 0.1);
|
|
241
372
|
expect(r.irr).not.toBeNull();
|
|
242
373
|
expect(r.irr!).toBeCloseTo(0.097, 2);
|
|
243
374
|
});
|
|
244
375
|
|
|
245
376
|
it('payback period is period 3 (cumulative turns positive at t=3)', () => {
|
|
246
|
-
const r = computeNPV(cashFlows, 0.
|
|
377
|
+
const r = computeNPV(cashFlows, 0.1);
|
|
247
378
|
expect(r.paybackPeriod).toBe(3);
|
|
248
379
|
});
|
|
249
380
|
|
|
250
381
|
it('project with all positive flows has no IRR in (0,2) range', () => {
|
|
251
|
-
const r = computeNPV(
|
|
382
|
+
const r = computeNPV(
|
|
383
|
+
[
|
|
384
|
+
{ period: 0, amount: 100 },
|
|
385
|
+
{ period: 1, amount: 100 },
|
|
386
|
+
],
|
|
387
|
+
0.05
|
|
388
|
+
);
|
|
252
389
|
// NPV is always positive for all-positive flows; IRR is null (no zero crossing)
|
|
253
390
|
expect(r.irr).toBeNull();
|
|
254
391
|
});
|
|
@@ -262,9 +399,7 @@ describe('computeNPV', () => {
|
|
|
262
399
|
|
|
263
400
|
describe('computeVaR', () => {
|
|
264
401
|
// Simulate 1000 loss observations from an exponential-like distribution
|
|
265
|
-
const losses = Array.from({ length: 1000 }, (_, i) =>
|
|
266
|
-
-500 + i * 1.5 + Math.sin(i * 0.13) * 50,
|
|
267
|
-
);
|
|
402
|
+
const losses = Array.from({ length: 1000 }, (_, i) => -500 + i * 1.5 + Math.sin(i * 0.13) * 50);
|
|
268
403
|
|
|
269
404
|
it('var99 ≥ var95 (higher confidence = larger loss threshold)', () => {
|
|
270
405
|
const r = computeVaR(losses);
|
|
@@ -328,7 +463,7 @@ describe('gompertzMakehamQx', () => {
|
|
|
328
463
|
});
|
|
329
464
|
|
|
330
465
|
it('produces a valid life table when passed to buildLifeTable', () => {
|
|
331
|
-
const qx
|
|
466
|
+
const qx = gompertzMakehamQx(params, 99);
|
|
332
467
|
const table = buildLifeTable('gompertz-test', qx, 0.05);
|
|
333
468
|
expect(table.rows[0].lx).toBe(100_000);
|
|
334
469
|
expect(table.rows[0].ex).toBeGreaterThan(50);
|
|
@@ -348,7 +483,7 @@ describe('gompertzMakehamQx', () => {
|
|
|
348
483
|
describe('buildActuarialReceipt', () => {
|
|
349
484
|
const result = computeActuarialValues(
|
|
350
485
|
{ type: 'whole_life', issueAge: 40, benefitAmount: 100_000 },
|
|
351
|
-
ILT
|
|
486
|
+
ILT
|
|
352
487
|
);
|
|
353
488
|
|
|
354
489
|
it('produces receipt with plugin=insurance and CAEL event', () => {
|
|
@@ -198,7 +198,7 @@ describe('insurance-underwriting — replay determinism', () => {
|
|
|
198
198
|
// Independent re-run by a validator
|
|
199
199
|
const rerunDigest = computeDecisionDigest(
|
|
200
200
|
cohort.map((r) => model.decide(r.features)),
|
|
201
|
-
'sha256'
|
|
201
|
+
'sha256'
|
|
202
202
|
);
|
|
203
203
|
|
|
204
204
|
const reExec = verifyReplayExecution(receipt, {
|
|
@@ -220,7 +220,7 @@ describe('insurance-underwriting — replay determinism', () => {
|
|
|
220
220
|
const tampered = cohort.map((r, i) =>
|
|
221
221
|
i === 0
|
|
222
222
|
? { group: r.group, features: { ...r.features, zip_risk: r.features.zip_risk + 1e-4 } }
|
|
223
|
-
: r
|
|
223
|
+
: r
|
|
224
224
|
);
|
|
225
225
|
const t = await runFairnessSweep(model, tampered, {
|
|
226
226
|
seed: SEED,
|
|
@@ -260,14 +260,16 @@ describe('insurance-underwriting — regulatory crosswalk keys', () => {
|
|
|
260
260
|
});
|
|
261
261
|
|
|
262
262
|
it('NAIC crosswalk entry references race_proxy and homeowners-underwriting-scorer', () => {
|
|
263
|
-
const naicEntry =
|
|
263
|
+
const naicEntry =
|
|
264
|
+
NAIC_INSURANCE_CROSSWALK['NAIC AI Systems Evaluation Tool (sample case file + bias audit)'];
|
|
264
265
|
expect(naicEntry).toBeTruthy();
|
|
265
266
|
expect(naicEntry).toContain('homeowners-underwriting-scorer');
|
|
266
267
|
expect(naicEntry).toContain('race_proxy');
|
|
267
268
|
});
|
|
268
269
|
|
|
269
270
|
it('CO SB21-169 crosswalk entry references zip_risk proxy', () => {
|
|
270
|
-
const coEntry =
|
|
271
|
+
const coEntry =
|
|
272
|
+
NAIC_INSURANCE_CROSSWALK['Colorado SB21-169 / Reg 10-1-1 (unfair-discrimination testing)'];
|
|
271
273
|
expect(coEntry).toBeTruthy();
|
|
272
274
|
expect(coEntry).toContain('zip_risk');
|
|
273
275
|
});
|