@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/src/actuarial.ts
CHANGED
|
@@ -15,28 +15,25 @@
|
|
|
15
15
|
* London, D. "Survival Models and Their Estimation" (3rd ed., 1997).
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
DOMAIN_SIMULATION_RECEIPT_SCHEMA,
|
|
20
|
-
buildDomainSimulationReceipt,
|
|
21
|
-
} from '@holoscript/core';
|
|
18
|
+
import { DOMAIN_SIMULATION_RECEIPT_SCHEMA, buildDomainSimulationReceipt } from '@holoscript/core';
|
|
22
19
|
|
|
23
20
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
24
21
|
|
|
25
22
|
/** One row of a standard life table (age x). */
|
|
26
23
|
export interface LifeTableRow {
|
|
27
|
-
age:
|
|
28
|
-
qx:
|
|
29
|
-
lx:
|
|
30
|
-
dx:
|
|
31
|
-
Lx:
|
|
32
|
-
Tx:
|
|
33
|
-
ex:
|
|
24
|
+
age: number; // x — integer age
|
|
25
|
+
qx: number; // probability of death between age x and x+1
|
|
26
|
+
lx: number; // number of survivors at age x (radix l₀ = 100 000)
|
|
27
|
+
dx: number; // deaths between x and x+1
|
|
28
|
+
Lx: number; // person-years lived between x and x+1
|
|
29
|
+
Tx: number; // total person-years lived above age x
|
|
30
|
+
ex: number; // curtate life expectancy at age x
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
/** Abridged model life table — minimum required: age 0 through at least age 100. */
|
|
37
34
|
export interface LifeTable {
|
|
38
|
-
id:
|
|
39
|
-
rows:
|
|
35
|
+
id: string;
|
|
36
|
+
rows: LifeTableRow[];
|
|
40
37
|
/** Annual effective interest rate (e.g. 0.05 = 5%) */
|
|
41
38
|
interestRate: number;
|
|
42
39
|
}
|
|
@@ -44,63 +41,63 @@ export interface LifeTable {
|
|
|
44
41
|
export type PolicyType = 'whole_life' | 'term_life' | 'endowment' | 'annuity_due';
|
|
45
42
|
|
|
46
43
|
export interface PolicySpec {
|
|
47
|
-
type:
|
|
48
|
-
issueAge:
|
|
49
|
-
termYears?:
|
|
50
|
-
benefitAmount: number;
|
|
44
|
+
type: PolicyType;
|
|
45
|
+
issueAge: number; // age at policy issue
|
|
46
|
+
termYears?: number; // required for term_life and endowment
|
|
47
|
+
benefitAmount: number; // face amount (currency units)
|
|
51
48
|
/** Annual effective interest rate override; falls back to table default */
|
|
52
49
|
interestRate?: number;
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
export interface ActuarialResult {
|
|
56
53
|
/** Net Single Premium — expected present value of benefits */
|
|
57
|
-
nsp:
|
|
54
|
+
nsp: number;
|
|
58
55
|
/** Level annual premium (equivalence principle) */
|
|
59
56
|
annualPremium: number;
|
|
60
57
|
/** Curtate life expectancy at issue age */
|
|
61
58
|
lifeExpectancy: number;
|
|
62
59
|
/** Present value of an annuity-due of 1 per year at issue age */
|
|
63
|
-
annuityDue:
|
|
60
|
+
annuityDue: number;
|
|
64
61
|
/** Policy type */
|
|
65
|
-
policyType:
|
|
66
|
-
issueAge:
|
|
67
|
-
termYears:
|
|
62
|
+
policyType: PolicyType;
|
|
63
|
+
issueAge: number;
|
|
64
|
+
termYears: number | null;
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
export interface CashFlow {
|
|
71
68
|
/** Period index (0-based; typically year number) */
|
|
72
|
-
period:
|
|
69
|
+
period: number;
|
|
73
70
|
/** Cash inflow (+) or outflow (−) */
|
|
74
|
-
amount:
|
|
71
|
+
amount: number;
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
export interface NPVResult {
|
|
78
|
-
npv:
|
|
79
|
-
irr:
|
|
75
|
+
npv: number;
|
|
76
|
+
irr: number | null; // null if no real IRR found in 0–200% range
|
|
80
77
|
paybackPeriod: number | null; // period where cumulative CF first ≥ 0
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
export interface VaRResult {
|
|
84
|
-
confidenceLevel: number;
|
|
85
|
-
var95:
|
|
86
|
-
var99:
|
|
87
|
-
cvar95:
|
|
88
|
-
cvar99:
|
|
89
|
-
expectedLoss:
|
|
90
|
-
maxLoss:
|
|
91
|
-
sampleSize:
|
|
81
|
+
confidenceLevel: number; // e.g. 0.95
|
|
82
|
+
var95: number; // VaR at 95%
|
|
83
|
+
var99: number; // VaR at 99%
|
|
84
|
+
cvar95: number; // CVaR (expected shortfall) at 95%
|
|
85
|
+
cvar99: number; // CVaR at 99%
|
|
86
|
+
expectedLoss: number;
|
|
87
|
+
maxLoss: number;
|
|
88
|
+
sampleSize: number;
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
export interface ActuarialReceipt {
|
|
95
|
-
plugin:
|
|
96
|
-
runId:
|
|
97
|
-
payloadHash:
|
|
92
|
+
plugin: string;
|
|
93
|
+
runId: string;
|
|
94
|
+
payloadHash: string;
|
|
98
95
|
hashAlgorithm: string;
|
|
99
|
-
cael:
|
|
100
|
-
acceptance:
|
|
96
|
+
cael: { event: string; schemaVersion: string; ts: string };
|
|
97
|
+
acceptance: { accepted: boolean; violations: string[] };
|
|
101
98
|
resultSummary: {
|
|
102
|
-
nsp:
|
|
103
|
-
annualPremium:
|
|
99
|
+
nsp: number;
|
|
100
|
+
annualPremium: number;
|
|
104
101
|
lifeExpectancy: number;
|
|
105
102
|
};
|
|
106
103
|
}
|
|
@@ -115,7 +112,8 @@ const RADIX = 100_000;
|
|
|
115
112
|
*/
|
|
116
113
|
export function buildLifeTable(id: string, qxByAge: number[], interestRate: number): LifeTable {
|
|
117
114
|
if (qxByAge.length < 2) throw new Error('[actuarial] qxByAge must have at least 2 entries');
|
|
118
|
-
if (qxByAge.some((q) => q < 0 || q > 1))
|
|
115
|
+
if (qxByAge.some((q) => q < 0 || q > 1))
|
|
116
|
+
throw new Error('[actuarial] all qx values must be in [0,1]');
|
|
119
117
|
if (interestRate < 0) throw new Error('[actuarial] interestRate must be ≥ 0');
|
|
120
118
|
|
|
121
119
|
const rows: LifeTableRow[] = [];
|
|
@@ -154,12 +152,12 @@ export function buildLifeTable(id: string, qxByAge: number[], interestRate: numb
|
|
|
154
152
|
for (let x = 0; x < n; x++) {
|
|
155
153
|
rows.push({
|
|
156
154
|
age: x,
|
|
157
|
-
qx:
|
|
158
|
-
lx:
|
|
159
|
-
dx:
|
|
160
|
-
Lx:
|
|
161
|
-
Tx:
|
|
162
|
-
ex:
|
|
155
|
+
qx: qxByAge[x],
|
|
156
|
+
lx: lx_arr[x],
|
|
157
|
+
dx: dx_arr[x],
|
|
158
|
+
Lx: Lx_arr[x],
|
|
159
|
+
Tx: Tx_arr[x],
|
|
160
|
+
ex: ex_arr[x],
|
|
163
161
|
});
|
|
164
162
|
}
|
|
165
163
|
|
|
@@ -177,13 +175,19 @@ export function buildLifeTable(id: string, qxByAge: number[], interestRate: numb
|
|
|
177
175
|
* Cx = v^(x+1) · dx (discounted deaths)
|
|
178
176
|
* Mx = Σ(x to ω) Cx (accumulated Cx)
|
|
179
177
|
*/
|
|
180
|
-
function commutationColumns(
|
|
181
|
-
|
|
178
|
+
function commutationColumns(
|
|
179
|
+
table: LifeTable,
|
|
180
|
+
iRate: number
|
|
181
|
+
): {
|
|
182
|
+
Dx: number[];
|
|
183
|
+
Nx: number[];
|
|
184
|
+
Cx: number[];
|
|
185
|
+
Mx: number[];
|
|
182
186
|
} {
|
|
183
|
-
const v
|
|
184
|
-
const n
|
|
185
|
-
const Dx
|
|
186
|
-
const Cx
|
|
187
|
+
const v = 1 / (1 + iRate);
|
|
188
|
+
const n = table.rows.length;
|
|
189
|
+
const Dx = table.rows.map((r) => Math.pow(v, r.age) * r.lx);
|
|
190
|
+
const Cx = table.rows.map((r) => Math.pow(v, r.age + 1) * r.dx);
|
|
187
191
|
|
|
188
192
|
const Nx: number[] = new Array(n).fill(0);
|
|
189
193
|
const Mx: number[] = new Array(n).fill(0);
|
|
@@ -209,90 +213,91 @@ function commutationColumns(table: LifeTable, iRate: number): {
|
|
|
209
213
|
* annuity_due — benefit B paid at START of each year while alive (no death benefit)
|
|
210
214
|
*/
|
|
211
215
|
export function computeActuarialValues(spec: PolicySpec, table: LifeTable): ActuarialResult {
|
|
212
|
-
const n
|
|
213
|
-
const iRate
|
|
214
|
-
const x
|
|
215
|
-
const B
|
|
216
|
+
const n = table.rows.length;
|
|
217
|
+
const iRate = spec.interestRate ?? table.interestRate;
|
|
218
|
+
const x = spec.issueAge;
|
|
219
|
+
const B = spec.benefitAmount;
|
|
216
220
|
|
|
217
|
-
if (x < 0 || x >= n)
|
|
218
|
-
|
|
221
|
+
if (x < 0 || x >= n)
|
|
222
|
+
throw new Error(`[actuarial] issueAge ${x} out of table range [0, ${n - 1}]`);
|
|
223
|
+
if (iRate < 0) throw new Error('[actuarial] interestRate must be ≥ 0');
|
|
219
224
|
|
|
220
225
|
const { Dx, Nx, Mx } = commutationColumns(table, iRate);
|
|
221
226
|
|
|
222
|
-
const row
|
|
223
|
-
const lifeExp
|
|
227
|
+
const row = table.rows[x];
|
|
228
|
+
const lifeExp = row.ex;
|
|
224
229
|
|
|
225
230
|
// ── Whole life ──────────────────────────────────────────────────────────────
|
|
226
231
|
if (spec.type === 'whole_life') {
|
|
227
|
-
const Ax
|
|
228
|
-
const annuity
|
|
229
|
-
const nsp
|
|
230
|
-
const Px
|
|
232
|
+
const Ax = Mx[x] / Dx[x]; // net single premium per unit benefit
|
|
233
|
+
const annuity = Nx[x] / Dx[x]; // ä_x (annuity-due of 1)
|
|
234
|
+
const nsp = B * Ax;
|
|
235
|
+
const Px = Ax / annuity; // level premium per unit benefit per year
|
|
231
236
|
return {
|
|
232
237
|
nsp,
|
|
233
238
|
annualPremium: B * Px,
|
|
234
239
|
lifeExpectancy: lifeExp,
|
|
235
|
-
annuityDue:
|
|
236
|
-
policyType:
|
|
237
|
-
issueAge:
|
|
238
|
-
termYears:
|
|
240
|
+
annuityDue: annuity,
|
|
241
|
+
policyType: 'whole_life',
|
|
242
|
+
issueAge: x,
|
|
243
|
+
termYears: null,
|
|
239
244
|
};
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
// ── Term life ───────────────────────────────────────────────────────────────
|
|
243
248
|
if (spec.type === 'term_life') {
|
|
244
249
|
const term = spec.termYears ?? 20;
|
|
245
|
-
const xn
|
|
246
|
-
const A1xn = (Mx[x] - Mx[xn]) / Dx[x];
|
|
247
|
-
const axn
|
|
248
|
-
const nsp
|
|
250
|
+
const xn = Math.min(x + term, n - 1);
|
|
251
|
+
const A1xn = (Mx[x] - Mx[xn]) / Dx[x]; // A^1_{x:n} term insurance per unit
|
|
252
|
+
const axn = (Nx[x] - Nx[xn]) / Dx[x]; // ä_{x:n} term annuity-due per unit
|
|
253
|
+
const nsp = B * A1xn;
|
|
249
254
|
return {
|
|
250
255
|
nsp,
|
|
251
256
|
annualPremium: axn > 0 ? nsp / axn : 0,
|
|
252
257
|
lifeExpectancy: lifeExp,
|
|
253
|
-
annuityDue:
|
|
254
|
-
policyType:
|
|
255
|
-
issueAge:
|
|
256
|
-
termYears:
|
|
258
|
+
annuityDue: axn,
|
|
259
|
+
policyType: 'term_life',
|
|
260
|
+
issueAge: x,
|
|
261
|
+
termYears: term,
|
|
257
262
|
};
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
// ── Endowment ───────────────────────────────────────────────────────────────
|
|
261
266
|
if (spec.type === 'endowment') {
|
|
262
267
|
const term = spec.termYears ?? 20;
|
|
263
|
-
const xn
|
|
268
|
+
const xn = Math.min(x + term, n - 1);
|
|
264
269
|
// Endowment = term life + pure endowment (survival to n)
|
|
265
|
-
const A1xn = (Mx[x] - Mx[xn]) / Dx[x];
|
|
266
|
-
const Exn
|
|
267
|
-
const Axn
|
|
268
|
-
const axn
|
|
269
|
-
const nsp
|
|
270
|
+
const A1xn = (Mx[x] - Mx[xn]) / Dx[x]; // insurance component
|
|
271
|
+
const Exn = Dx[xn] / Dx[x]; // pure endowment (n Ex)
|
|
272
|
+
const Axn = A1xn + Exn; // endowment NSP per unit
|
|
273
|
+
const axn = (Nx[x] - Nx[xn]) / Dx[x];
|
|
274
|
+
const nsp = B * Axn;
|
|
270
275
|
return {
|
|
271
276
|
nsp,
|
|
272
277
|
annualPremium: axn > 0 ? nsp / axn : 0,
|
|
273
278
|
lifeExpectancy: lifeExp,
|
|
274
|
-
annuityDue:
|
|
275
|
-
policyType:
|
|
276
|
-
issueAge:
|
|
277
|
-
termYears:
|
|
279
|
+
annuityDue: axn,
|
|
280
|
+
policyType: 'endowment',
|
|
281
|
+
issueAge: x,
|
|
282
|
+
termYears: term,
|
|
278
283
|
};
|
|
279
284
|
}
|
|
280
285
|
|
|
281
286
|
// ── Life annuity-due ────────────────────────────────────────────────────────
|
|
282
287
|
if (spec.type === 'annuity_due') {
|
|
283
|
-
const term
|
|
288
|
+
const term = spec.termYears;
|
|
284
289
|
let annuityDue: number;
|
|
285
290
|
let termYears: number | null;
|
|
286
291
|
|
|
287
292
|
if (term != null) {
|
|
288
293
|
// Temporary life annuity-due ä_{x:n}
|
|
289
|
-
const xn
|
|
294
|
+
const xn = Math.min(x + term, n - 1);
|
|
290
295
|
annuityDue = (Nx[x] - Nx[xn]) / Dx[x];
|
|
291
|
-
termYears
|
|
296
|
+
termYears = term;
|
|
292
297
|
} else {
|
|
293
298
|
// Whole life annuity-due ä_x
|
|
294
299
|
annuityDue = Nx[x] / Dx[x];
|
|
295
|
-
termYears
|
|
300
|
+
termYears = null;
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
const nsp = B * annuityDue;
|
|
@@ -301,8 +306,8 @@ export function computeActuarialValues(spec: PolicySpec, table: LifeTable): Actu
|
|
|
301
306
|
annualPremium: 0, // annuities don't have a separately stated premium
|
|
302
307
|
lifeExpectancy: lifeExp,
|
|
303
308
|
annuityDue,
|
|
304
|
-
policyType:
|
|
305
|
-
issueAge:
|
|
309
|
+
policyType: 'annuity_due',
|
|
310
|
+
issueAge: x,
|
|
306
311
|
termYears,
|
|
307
312
|
};
|
|
308
313
|
}
|
|
@@ -318,7 +323,7 @@ export function computeActuarialValues(spec: PolicySpec, table: LifeTable): Actu
|
|
|
318
323
|
*/
|
|
319
324
|
export function computeNPV(cashFlows: CashFlow[], discountRate: number): NPVResult {
|
|
320
325
|
if (cashFlows.length === 0) throw new Error('[actuarial] cashFlows must not be empty');
|
|
321
|
-
if (discountRate < 0)
|
|
326
|
+
if (discountRate < 0) throw new Error('[actuarial] discountRate must be ≥ 0');
|
|
322
327
|
|
|
323
328
|
const sorted = [...cashFlows].sort((a, b) => a.period - b.period);
|
|
324
329
|
|
|
@@ -329,18 +334,25 @@ export function computeNPV(cashFlows: CashFlow[], discountRate: number): NPVResu
|
|
|
329
334
|
let paybackPeriod: number | null = null;
|
|
330
335
|
for (const cf of sorted) {
|
|
331
336
|
cum += cf.amount;
|
|
332
|
-
if (cum >= 0 && paybackPeriod === null) {
|
|
337
|
+
if (cum >= 0 && paybackPeriod === null) {
|
|
338
|
+
paybackPeriod = cf.period;
|
|
339
|
+
}
|
|
333
340
|
}
|
|
334
341
|
|
|
335
342
|
// IRR via bisection (finds rate where NPV = 0)
|
|
336
343
|
let irr: number | null = null;
|
|
337
344
|
const npvAt = (r: number) => sorted.reduce((s, cf) => s + cf.amount / (1 + r) ** cf.period, 0);
|
|
338
|
-
const lo = 0,
|
|
345
|
+
const lo = 0,
|
|
346
|
+
hi = 2.0; // 0% to 200%
|
|
339
347
|
if (Math.sign(npvAt(lo)) !== Math.sign(npvAt(hi))) {
|
|
340
|
-
let a = lo,
|
|
348
|
+
let a = lo,
|
|
349
|
+
b = hi;
|
|
341
350
|
for (let i = 0; i < 100; i++) {
|
|
342
351
|
const mid = (a + b) / 2;
|
|
343
|
-
if (Math.abs(b - a) < 1e-10) {
|
|
352
|
+
if (Math.abs(b - a) < 1e-10) {
|
|
353
|
+
irr = mid;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
344
356
|
Math.sign(npvAt(mid)) === Math.sign(npvAt(a)) ? (a = mid) : (b = mid);
|
|
345
357
|
irr = (a + b) / 2;
|
|
346
358
|
}
|
|
@@ -359,7 +371,7 @@ export function computeVaR(losses: number[]): VaRResult {
|
|
|
359
371
|
if (losses.length < 2) throw new Error('[actuarial] VaR requires at least 2 loss observations');
|
|
360
372
|
|
|
361
373
|
const sorted = [...losses].sort((a, b) => a - b);
|
|
362
|
-
const n
|
|
374
|
+
const n = sorted.length;
|
|
363
375
|
|
|
364
376
|
const idx95 = Math.ceil(0.95 * n) - 1;
|
|
365
377
|
const idx99 = Math.ceil(0.99 * n) - 1;
|
|
@@ -374,7 +386,7 @@ export function computeVaR(losses: number[]): VaRResult {
|
|
|
374
386
|
const cvar99 = tail99.reduce((s, v) => s + v, 0) / tail99.length;
|
|
375
387
|
|
|
376
388
|
const expectedLoss = sorted.reduce((s, v) => s + v, 0) / n;
|
|
377
|
-
const maxLoss
|
|
389
|
+
const maxLoss = sorted[n - 1];
|
|
378
390
|
|
|
379
391
|
return {
|
|
380
392
|
confidenceLevel: 0.95,
|
|
@@ -400,13 +412,16 @@ export function computeVaR(losses: number[]): VaRResult {
|
|
|
400
412
|
*
|
|
401
413
|
* Returns qx values for ages 0 .. maxAge.
|
|
402
414
|
*/
|
|
403
|
-
export function gompertzMakehamQx(
|
|
415
|
+
export function gompertzMakehamQx(
|
|
416
|
+
params: { A: number; B: number; c: number },
|
|
417
|
+
maxAge: number
|
|
418
|
+
): number[] {
|
|
404
419
|
const { A, B, c } = params;
|
|
405
420
|
if (B <= 0 || c <= 1) throw new Error('[actuarial] Gompertz params require B>0 and c>1');
|
|
406
421
|
const lnC = Math.log(c);
|
|
407
422
|
return Array.from({ length: maxAge + 1 }, (_, x) => {
|
|
408
423
|
// qx ≈ 1 − exp(−∫ₓˣ⁺¹ μ(t) dt) = 1 − exp(−A − B(cˣ)(c−1)/ln(c))
|
|
409
|
-
const integral = A + B * Math.pow(c, x) * (c - 1) / lnC;
|
|
424
|
+
const integral = A + (B * Math.pow(c, x) * (c - 1)) / lnC;
|
|
410
425
|
return Math.min(1, 1 - Math.exp(-integral));
|
|
411
426
|
});
|
|
412
427
|
}
|
|
@@ -414,34 +429,37 @@ export function gompertzMakehamQx(params: { A: number; B: number; c: number }, m
|
|
|
414
429
|
// ─── Receipt ─────────────────────────────────────────────────────────────────
|
|
415
430
|
|
|
416
431
|
export function buildActuarialReceipt(
|
|
417
|
-
result:
|
|
418
|
-
options?: { runId?: string }
|
|
432
|
+
result: ActuarialResult,
|
|
433
|
+
options?: { runId?: string }
|
|
419
434
|
): ActuarialReceipt {
|
|
420
435
|
const violations: Array<{ criterion: string; message: string }> = [];
|
|
421
436
|
if (result.nsp < 0)
|
|
422
437
|
violations.push({ criterion: 'nsp', message: `negative NSP: ${result.nsp.toFixed(4)}` });
|
|
423
438
|
if (result.annualPremium < 0)
|
|
424
|
-
violations.push({
|
|
439
|
+
violations.push({
|
|
440
|
+
criterion: 'premium',
|
|
441
|
+
message: `negative annual premium: ${result.annualPremium.toFixed(4)}`,
|
|
442
|
+
});
|
|
425
443
|
|
|
426
444
|
const raw = buildDomainSimulationReceipt({
|
|
427
|
-
plugin:
|
|
445
|
+
plugin: 'insurance',
|
|
428
446
|
pluginVersion: '1.0.0',
|
|
429
|
-
runId:
|
|
430
|
-
modelId:
|
|
447
|
+
runId: options?.runId ?? `act-${Date.now().toString(36)}`,
|
|
448
|
+
modelId: `${result.policyType}-age${result.issueAge}`,
|
|
431
449
|
solverConfig: {
|
|
432
|
-
solverType:
|
|
433
|
-
scale:
|
|
434
|
-
policyType:
|
|
435
|
-
issueAge:
|
|
450
|
+
solverType: 'commutation-columns',
|
|
451
|
+
scale: 'individual-policy',
|
|
452
|
+
policyType: result.policyType,
|
|
453
|
+
issueAge: result.issueAge,
|
|
436
454
|
},
|
|
437
455
|
resultSummary: {
|
|
438
|
-
nsp:
|
|
439
|
-
annualPremium:
|
|
456
|
+
nsp: +result.nsp.toFixed(4),
|
|
457
|
+
annualPremium: +result.annualPremium.toFixed(4),
|
|
440
458
|
lifeExpectancy: +result.lifeExpectancy.toFixed(2),
|
|
441
459
|
},
|
|
442
460
|
cael: {
|
|
443
|
-
version:
|
|
444
|
-
event:
|
|
461
|
+
version: 'cael.v1',
|
|
462
|
+
event: 'insurance.actuarial',
|
|
445
463
|
solverType: 'insurance.commutation-columns',
|
|
446
464
|
},
|
|
447
465
|
acceptance: { accepted: violations.length === 0, violations },
|
|
@@ -100,7 +100,7 @@ export interface HomeownersModelWeights {
|
|
|
100
100
|
*/
|
|
101
101
|
export function createHomeownersModel(
|
|
102
102
|
weights: HomeownersModelWeights,
|
|
103
|
-
modelId = 'homeowners-underwriting-scorer'
|
|
103
|
+
modelId = 'homeowners-underwriting-scorer'
|
|
104
104
|
): FairnessModel {
|
|
105
105
|
return {
|
|
106
106
|
id: modelId,
|
|
@@ -129,10 +129,10 @@ export function createHomeownersModel(
|
|
|
129
129
|
export const BIASED_WEIGHTS: HomeownersModelWeights = {
|
|
130
130
|
prior_claims: 0.45,
|
|
131
131
|
credit_tier: 0.55,
|
|
132
|
-
zip_risk: 0.
|
|
133
|
-
age_band: 0.
|
|
134
|
-
bias: 0.
|
|
135
|
-
threshold: 0.
|
|
132
|
+
zip_risk: 0.6, // over-penalises high-decile (the bias lever)
|
|
133
|
+
age_band: 0.1,
|
|
134
|
+
bias: 0.7,
|
|
135
|
+
threshold: 0.5,
|
|
136
136
|
};
|
|
137
137
|
|
|
138
138
|
/**
|
|
@@ -147,12 +147,12 @@ export const BIASED_WEIGHTS: HomeownersModelWeights = {
|
|
|
147
147
|
* constraint; the higher bias provides the margin).
|
|
148
148
|
*/
|
|
149
149
|
export const REMEDIATED_WEIGHTS: HomeownersModelWeights = {
|
|
150
|
-
prior_claims: 0.
|
|
150
|
+
prior_claims: 0.4,
|
|
151
151
|
credit_tier: 0.55,
|
|
152
|
-
zip_risk: 0.
|
|
152
|
+
zip_risk: 0.0, // zeroed out — D.057 remediation
|
|
153
153
|
age_band: 0.08,
|
|
154
154
|
bias: 0.55,
|
|
155
|
-
threshold: 0.
|
|
155
|
+
threshold: 0.5,
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
// ── Synthetic cohort generator ───────────────────────────────────────────────
|
|
@@ -185,18 +185,18 @@ export function makeInsuranceCohort(seed: number, n = 400): FairnessRecord[] {
|
|
|
185
185
|
// ZIP-risk proxy: the bias trap — high-decile draws from higher range
|
|
186
186
|
const zip_risk =
|
|
187
187
|
group === 'high-decile'
|
|
188
|
-
? clamp01(0.55 + 0.
|
|
188
|
+
? clamp01(0.55 + 0.4 * rng()) // [0.55, 0.95]
|
|
189
189
|
: clamp01(0.05 + 0.45 * rng()); // [0.05, 0.50]
|
|
190
190
|
|
|
191
191
|
// Credit tier: slightly lower for high-decile (systemic disparity)
|
|
192
192
|
const credit_tier =
|
|
193
193
|
group === 'high-decile'
|
|
194
|
-
? clamp01(0.
|
|
195
|
-
: clamp01(0.
|
|
194
|
+
? clamp01(0.3 + 0.55 * rng()) // mean ~0.575
|
|
195
|
+
: clamp01(0.4 + 0.55 * rng()); // mean ~0.675
|
|
196
196
|
|
|
197
197
|
// Prior claims and age band: comparable across groups
|
|
198
198
|
const prior_claims = clamp01(0.05 + 0.45 * rng()); // [0.05, 0.50]
|
|
199
|
-
const age_band = clamp01(0.
|
|
199
|
+
const age_band = clamp01(0.2 + 0.6 * rng()); // [0.20, 0.80]
|
|
200
200
|
|
|
201
201
|
rows.push({
|
|
202
202
|
group,
|
|
@@ -227,7 +227,7 @@ export function makeInsuranceCohort(seed: number, n = 400): FairnessRecord[] {
|
|
|
227
227
|
*/
|
|
228
228
|
export const insuranceUnderwritingPerturber: CohortPerturber = (
|
|
229
229
|
cohort: readonly FairnessRecord[],
|
|
230
|
-
{ driftShift, noiseScale, rng }: FairnessPerturbation
|
|
230
|
+
{ driftShift, noiseScale, rng }: FairnessPerturbation
|
|
231
231
|
): FairnessRecord[] => {
|
|
232
232
|
const n = cohort.length;
|
|
233
233
|
const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
|
|
@@ -242,16 +242,12 @@ export const insuranceUnderwritingPerturber: CohortPerturber = (
|
|
|
242
242
|
zip_risk: clamp01(
|
|
243
243
|
src.features.zip_risk +
|
|
244
244
|
(isHighDecile ? driftShift : 0) + // directed drift for high-decile
|
|
245
|
-
(rng() - 0.5) * 0.5 * noiseScale
|
|
245
|
+
(rng() - 0.5) * 0.5 * noiseScale // symmetric noise for both
|
|
246
246
|
),
|
|
247
247
|
// Credit bureau measurement noise (both groups)
|
|
248
|
-
credit_tier: clamp01(
|
|
249
|
-
src.features.credit_tier + (rng() - 0.5) * 2 * noiseScale,
|
|
250
|
-
),
|
|
248
|
+
credit_tier: clamp01(src.features.credit_tier + (rng() - 0.5) * 2 * noiseScale),
|
|
251
249
|
// Prior claims: low noise (claim history is more stable)
|
|
252
|
-
prior_claims: clamp01(
|
|
253
|
-
src.features.prior_claims + (rng() - 0.5) * noiseScale,
|
|
254
|
-
),
|
|
250
|
+
prior_claims: clamp01(src.features.prior_claims + (rng() - 0.5) * noiseScale),
|
|
255
251
|
// Age band: stable (no drift)
|
|
256
252
|
age_band: src.features.age_band,
|
|
257
253
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
createPolicyHandler,
|
|
3
|
+
type PolicyConfig,
|
|
4
|
+
type PolicyType,
|
|
5
|
+
type PolicyStatus,
|
|
6
|
+
} from './traits/PolicyTrait';
|
|
2
7
|
export { createClaimHandler, type ClaimConfig, type ClaimStatus } from './traits/ClaimTrait';
|
|
3
|
-
export {
|
|
4
|
-
|
|
8
|
+
export {
|
|
9
|
+
createRiskAssessmentHandler,
|
|
10
|
+
type RiskAssessmentConfig,
|
|
11
|
+
type RiskFactor,
|
|
12
|
+
} from './traits/RiskAssessmentTrait';
|
|
13
|
+
export {
|
|
14
|
+
createUnderwritingHandler,
|
|
15
|
+
type UnderwritingConfig,
|
|
16
|
+
type UnderwritingDecision,
|
|
17
|
+
} from './traits/UnderwritingTrait';
|
|
5
18
|
export * from './traits/types';
|
|
6
19
|
|
|
7
20
|
import { createPolicyHandler } from './traits/PolicyTrait';
|
|
@@ -12,5 +25,14 @@ import { createUnderwritingHandler } from './traits/UnderwritingTrait';
|
|
|
12
25
|
export * from './actuarial';
|
|
13
26
|
export * from './fairness-underwriting';
|
|
14
27
|
|
|
15
|
-
export const pluginMeta = {
|
|
16
|
-
|
|
28
|
+
export const pluginMeta = {
|
|
29
|
+
name: '@holoscript/plugin-insurance',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
traits: ['policy', 'claim', 'risk_assessment', 'underwriting', 'actuarial_math'],
|
|
32
|
+
};
|
|
33
|
+
export const traitHandlers = [
|
|
34
|
+
createPolicyHandler(),
|
|
35
|
+
createClaimHandler(),
|
|
36
|
+
createRiskAssessmentHandler(),
|
|
37
|
+
createUnderwritingHandler(),
|
|
38
|
+
];
|