@actuarial-ts/core 0.1.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.
Files changed (120) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +133 -0
  4. package/dist/benktander.d.ts +45 -0
  5. package/dist/benktander.d.ts.map +1 -0
  6. package/dist/benktander.js +38 -0
  7. package/dist/benktander.js.map +1 -0
  8. package/dist/berquist.d.ts +14 -0
  9. package/dist/berquist.d.ts.map +1 -0
  10. package/dist/berquist.js +292 -0
  11. package/dist/berquist.js.map +1 -0
  12. package/dist/bf.d.ts +23 -0
  13. package/dist/bf.d.ts.map +1 -0
  14. package/dist/bf.js +83 -0
  15. package/dist/bf.js.map +1 -0
  16. package/dist/capping.d.ts +96 -0
  17. package/dist/capping.d.ts.map +1 -0
  18. package/dist/capping.js +207 -0
  19. package/dist/capping.js.map +1 -0
  20. package/dist/caseOutstanding.d.ts +80 -0
  21. package/dist/caseOutstanding.d.ts.map +1 -0
  22. package/dist/caseOutstanding.js +147 -0
  23. package/dist/caseOutstanding.js.map +1 -0
  24. package/dist/chainladder.d.ts +15 -0
  25. package/dist/chainladder.d.ts.map +1 -0
  26. package/dist/chainladder.js +81 -0
  27. package/dist/chainladder.js.map +1 -0
  28. package/dist/clark.d.ts +91 -0
  29. package/dist/clark.d.ts.map +1 -0
  30. package/dist/clark.js +490 -0
  31. package/dist/clark.js.map +1 -0
  32. package/dist/diagnostics.d.ts +80 -0
  33. package/dist/diagnostics.d.ts.map +1 -0
  34. package/dist/diagnostics.js +359 -0
  35. package/dist/diagnostics.js.map +1 -0
  36. package/dist/discounting.d.ts +166 -0
  37. package/dist/discounting.d.ts.map +1 -0
  38. package/dist/discounting.js +210 -0
  39. package/dist/discounting.js.map +1 -0
  40. package/dist/elrMethods.d.ts +104 -0
  41. package/dist/elrMethods.d.ts.map +1 -0
  42. package/dist/elrMethods.js +151 -0
  43. package/dist/elrMethods.js.map +1 -0
  44. package/dist/factors.d.ts +19 -0
  45. package/dist/factors.d.ts.map +1 -0
  46. package/dist/factors.js +136 -0
  47. package/dist/factors.js.map +1 -0
  48. package/dist/fisherLange.d.ts +92 -0
  49. package/dist/fisherLange.d.ts.map +1 -0
  50. package/dist/fisherLange.js +224 -0
  51. package/dist/fisherLange.js.map +1 -0
  52. package/dist/freqSev.d.ts +57 -0
  53. package/dist/freqSev.d.ts.map +1 -0
  54. package/dist/freqSev.js +82 -0
  55. package/dist/freqSev.js.map +1 -0
  56. package/dist/ilf.d.ts +111 -0
  57. package/dist/ilf.d.ts.map +1 -0
  58. package/dist/ilf.js +449 -0
  59. package/dist/ilf.js.map +1 -0
  60. package/dist/index.d.ts +29 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +29 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/mack.d.ts +55 -0
  65. package/dist/mack.d.ts.map +1 -0
  66. package/dist/mack.js +244 -0
  67. package/dist/mack.js.map +1 -0
  68. package/dist/merzWuthrich.d.ts +29 -0
  69. package/dist/merzWuthrich.d.ts.map +1 -0
  70. package/dist/merzWuthrich.js +132 -0
  71. package/dist/merzWuthrich.js.map +1 -0
  72. package/dist/munichChainLadder.d.ts +101 -0
  73. package/dist/munichChainLadder.d.ts.map +1 -0
  74. package/dist/munichChainLadder.js +254 -0
  75. package/dist/munichChainLadder.js.map +1 -0
  76. package/dist/odpBootstrap.d.ts +65 -0
  77. package/dist/odpBootstrap.d.ts.map +1 -0
  78. package/dist/odpBootstrap.js +243 -0
  79. package/dist/odpBootstrap.js.map +1 -0
  80. package/dist/onlevel.d.ts +39 -0
  81. package/dist/onlevel.d.ts.map +1 -0
  82. package/dist/onlevel.js +104 -0
  83. package/dist/onlevel.js.map +1 -0
  84. package/dist/salvageSubro.d.ts +82 -0
  85. package/dist/salvageSubro.d.ts.map +1 -0
  86. package/dist/salvageSubro.js +100 -0
  87. package/dist/salvageSubro.js.map +1 -0
  88. package/dist/stochastic.d.ts +56 -0
  89. package/dist/stochastic.d.ts.map +1 -0
  90. package/dist/stochastic.js +99 -0
  91. package/dist/stochastic.js.map +1 -0
  92. package/dist/tail.d.ts +18 -0
  93. package/dist/tail.d.ts.map +1 -0
  94. package/dist/tail.js +121 -0
  95. package/dist/tail.js.map +1 -0
  96. package/dist/trend.d.ts +46 -0
  97. package/dist/trend.d.ts.map +1 -0
  98. package/dist/trend.js +99 -0
  99. package/dist/trend.js.map +1 -0
  100. package/dist/triangle.d.ts +19 -0
  101. package/dist/triangle.d.ts.map +1 -0
  102. package/dist/triangle.js +163 -0
  103. package/dist/triangle.js.map +1 -0
  104. package/dist/triangleAlgebra.d.ts +10 -0
  105. package/dist/triangleAlgebra.d.ts.map +1 -0
  106. package/dist/triangleAlgebra.js +97 -0
  107. package/dist/triangleAlgebra.js.map +1 -0
  108. package/dist/types.d.ts +280 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +79 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/ulae.d.ts +172 -0
  113. package/dist/ulae.d.ts.map +1 -0
  114. package/dist/ulae.js +164 -0
  115. package/dist/ulae.js.map +1 -0
  116. package/dist/util.d.ts +24 -0
  117. package/dist/util.d.ts.map +1 -0
  118. package/dist/util.js +66 -0
  119. package/dist/util.js.map +1 -0
  120. package/package.json +66 -0
@@ -0,0 +1,210 @@
1
+ import { ReservingError } from "./types.js";
2
+ import { isNum, safeRatio } from "./util.js";
3
+ /**
4
+ * Expected future incremental payments per origin, derived from a chain
5
+ * ladder result: the increment for development interval (ages[j], ages[j+1]]
6
+ * is ultimate x (percentDeveloped[j+1] - percentDeveloped[j]), and the tail
7
+ * cash implied by a tail factor > 1 (ultimate x (1 - percentDeveloped at the
8
+ * last age)) is compressed into a single interval of the same width as the
9
+ * last observed development step, immediately after the last age (warned; a
10
+ * single-age triangle uses a 12-month tail interval). Each row's cashflow
11
+ * amounts sum to its unpaid by construction.
12
+ *
13
+ * `ages` must be the development ages the chain ladder ran on (the result
14
+ * does not carry them). Negative increments (percent developed not monotone,
15
+ * i.e. an LDF below 1) are kept and warned - never dropped or fabricated
16
+ * away.
17
+ */
18
+ export function payoutPatternFromChainLadder(cl, ages) {
19
+ const K = ages.length;
20
+ if (K === 0 || cl.cdfs.length !== K) {
21
+ throw new ReservingError("SHAPE", `Expected the development ages the chain ladder ran on (${cl.cdfs.length} CDFs), got ${K} ages`);
22
+ }
23
+ for (let j = 0; j < K; j++) {
24
+ const a = ages[j];
25
+ if (!isNum(a) || a <= 0 || (j > 0 && a <= ages[j - 1])) {
26
+ throw new ReservingError("SHAPE", "Development ages must be finite, positive, and ascending");
27
+ }
28
+ }
29
+ const warnings = [];
30
+ const pct = cl.percentDeveloped;
31
+ const lastStep = K >= 2 ? ages[K - 1] - ages[K - 2] : 12;
32
+ const negativeColumns = new Set();
33
+ let tailWarned = false;
34
+ const rows = [];
35
+ for (const row of cl.rows) {
36
+ const k = ages.indexOf(row.latestAge);
37
+ if (k < 0) {
38
+ throw new ReservingError("SHAPE", `Origin ${row.origin}: latest age ${row.latestAge} is not among the supplied ages; pass the same ages the chain ladder ran on`);
39
+ }
40
+ const cashflows = [];
41
+ for (let j = k; j < K - 1; j++) {
42
+ const amount = row.ultimate * (pct[j + 1] - pct[j]);
43
+ if (amount < 0 && !negativeColumns.has(j)) {
44
+ negativeColumns.add(j);
45
+ warnings.push(`Development interval ${ages[j]}-${ages[j + 1]} months has a negative expected payment (percent developed is not monotone there); negative cashflows are kept, never dropped`);
46
+ }
47
+ cashflows.push({
48
+ fromMonths: ages[j] - ages[k],
49
+ toMonths: ages[j + 1] - ages[k],
50
+ amount,
51
+ });
52
+ }
53
+ const tailAmount = row.ultimate * (1 - pct[K - 1]);
54
+ if (Math.abs(tailAmount) > 1e-9 * Math.max(1, Math.abs(row.ultimate))) {
55
+ if (!tailWarned) {
56
+ tailWarned = true;
57
+ warnings.push(`Tail development beyond ${ages[K - 1]} months is compressed into a single ${lastStep}-month payment interval immediately after the last age; supply explicit cashflows if the tail is long`);
58
+ }
59
+ cashflows.push({
60
+ fromMonths: ages[K - 1] - ages[k],
61
+ toMonths: ages[K - 1] - ages[k] + lastStep,
62
+ amount: tailAmount,
63
+ });
64
+ }
65
+ rows.push({ origin: row.origin, latestAge: row.latestAge, unpaid: row.unpaid, cashflows });
66
+ }
67
+ // Off-diagonal guard: cashflow timing measures every row from its OWN
68
+ // latest observed age, which is only calendar-correct when all rows sit on
69
+ // one valuation diagonal. A stale origin (data feed stopped early) has a
70
+ // latest age at or below a YOUNGER origin's — its calendar-past
71
+ // development would be discounted as future. Rows clamped at the final age
72
+ // column are legitimate (fully-developed old origins on truncated-wide
73
+ // grids), so equality at the last age never flags. Warn, never re-time.
74
+ const maxAge = ages[K - 1];
75
+ for (let i = 0; i < rows.length; i++) {
76
+ const r = rows[i];
77
+ if (r.latestAge >= maxAge)
78
+ continue;
79
+ if (!r.cashflows.some((c) => Math.abs(c.amount) > 1e-9))
80
+ continue;
81
+ const shadowedBy = rows.slice(i + 1).find((later) => later.latestAge >= r.latestAge);
82
+ if (shadowedBy) {
83
+ warnings.push(`Origin ${r.origin} appears stale (latest age ${r.latestAge} months, at or below younger origin ${shadowedBy.origin}'s ${shadowedBy.latestAge}); its cashflow timing assumes its latest cell sits AT the valuation date, so calendar-past development may be discounted as future`);
84
+ }
85
+ }
86
+ return { rows, warnings };
87
+ }
88
+ const ISO_DATE = /^\d{4}-(\d{2})-(\d{2})$/;
89
+ function validateProvenance(provenance) {
90
+ if (provenance === undefined ||
91
+ typeof provenance.source !== "string" ||
92
+ provenance.source.trim() === "") {
93
+ throw new ReservingError("NO_PROVENANCE", "Rate provenance is required: ASOP 20 requires disclosing the basis of the discount rates (source) and the date they were determined (asOfDate)");
94
+ }
95
+ const match = typeof provenance.asOfDate === "string" ? ISO_DATE.exec(provenance.asOfDate) : null;
96
+ const m = match ? Number(match[1]) : 0;
97
+ const d = match ? Number(match[2]) : 0;
98
+ if (!match || m < 1 || m > 12 || d < 1 || d > 31) {
99
+ throw new ReservingError("BAD_DATE", `Rate provenance asOfDate must be an ISO date (yyyy-mm-dd), got "${provenance.asOfDate}"`);
100
+ }
101
+ return provenance;
102
+ }
103
+ function validateRates(rates) {
104
+ if (rates.kind === "flat") {
105
+ if (!isNum(rates.annualRate) || rates.annualRate <= -1) {
106
+ throw new ReservingError("BAD_RATE", `Flat annual discount rate must be a finite number greater than -100%, got ${rates.annualRate}`);
107
+ }
108
+ return;
109
+ }
110
+ if (rates.spotByYear.length === 0) {
111
+ throw new ReservingError("BAD_RATE", "A spot curve needs at least one annual rate");
112
+ }
113
+ rates.spotByYear.forEach((r, k) => {
114
+ if (!isNum(r) || r <= -1) {
115
+ throw new ReservingError("BAD_RATE", `Spot rate for year ${k + 1} must be a finite number greater than -100%, got ${r}`);
116
+ }
117
+ });
118
+ }
119
+ /**
120
+ * Discounts expected future payments and reports nominal and discounted
121
+ * unpaid side by side, per origin and in total. See the module doc for the
122
+ * timing convention, curve lookup, and the ASOP 20 provenance/risk-margin
123
+ * rules this function enforces.
124
+ */
125
+ export function discountUnpaid(input) {
126
+ const hasPattern = input.pattern !== undefined;
127
+ const hasCashflows = input.cashflows !== undefined;
128
+ if (hasPattern === hasCashflows) {
129
+ throw new ReservingError("BAD_CASHFLOWS", "Provide exactly one cashflow source: a payout pattern OR explicit per-origin cashflows");
130
+ }
131
+ validateRates(input.rates);
132
+ const provenance = validateProvenance(input.provenance);
133
+ if (input.riskMargin !== undefined && (!isNum(input.riskMargin) || input.riskMargin < 0)) {
134
+ throw new ReservingError("BAD_MARGIN", `An explicit risk margin must be a finite, non-negative amount, got ${input.riskMargin}`);
135
+ }
136
+ const sources = hasPattern
137
+ ? input.pattern.rows.map((r) => ({ origin: r.origin, cashflows: r.cashflows }))
138
+ : input.cashflows;
139
+ if (sources.length === 0) {
140
+ throw new ReservingError("NO_DATA", "No origins to discount");
141
+ }
142
+ const warnings = [];
143
+ const spot = input.rates.kind === "curve" ? input.rates.spotByYear : null;
144
+ let horizonWarned = false;
145
+ const negativeOrigins = [];
146
+ const rows = sources.map((source) => {
147
+ let nominal = 0;
148
+ let discounted = 0;
149
+ let hasNegative = false;
150
+ const details = source.cashflows.map((cf) => {
151
+ if (!isNum(cf.fromMonths) ||
152
+ cf.fromMonths < 0 ||
153
+ !isNum(cf.toMonths) ||
154
+ cf.toMonths < cf.fromMonths ||
155
+ !isNum(cf.amount)) {
156
+ throw new ReservingError("BAD_CASHFLOWS", `Origin ${source.origin}: cashflows need finite amounts and 0 <= fromMonths <= toMonths`);
157
+ }
158
+ if (cf.amount < 0)
159
+ hasNegative = true;
160
+ const timeYears = input.convention === "end-period"
161
+ ? cf.toMonths / 12
162
+ : (cf.fromMonths + cf.toMonths) / 24;
163
+ let rate;
164
+ if (spot === null) {
165
+ rate = input.rates.annualRate;
166
+ }
167
+ else {
168
+ if (timeYears > spot.length && !horizonWarned) {
169
+ horizonWarned = true;
170
+ warnings.push(`Cashflows beyond the ${spot.length}-year spot curve horizon are discounted at the last spot rate (${spot[spot.length - 1]}); extend the curve to refine`);
171
+ }
172
+ rate = spot[Math.min(Math.max(Math.ceil(timeYears), 1), spot.length) - 1];
173
+ }
174
+ const discountFactor = (1 + rate) ** -timeYears;
175
+ const pv = cf.amount * discountFactor;
176
+ nominal += cf.amount;
177
+ discounted += pv;
178
+ return { timeYears, amount: cf.amount, rate, discountFactor, discounted: pv };
179
+ });
180
+ if (hasNegative)
181
+ negativeOrigins.push(source.origin);
182
+ return {
183
+ origin: source.origin,
184
+ nominal,
185
+ discounted,
186
+ discount: nominal - discounted,
187
+ effectiveDiscountFactor: safeRatio(discounted, nominal),
188
+ cashflows: details,
189
+ };
190
+ });
191
+ if (negativeOrigins.length > 0) {
192
+ warnings.push(`Negative expected cashflows for origin(s) ${negativeOrigins.join(", ")}; discounted as-is (negative flows reduce the discounted unpaid)`);
193
+ }
194
+ const totals = rows.reduce((acc, r) => ({
195
+ nominal: acc.nominal + r.nominal,
196
+ discounted: acc.discounted + r.discounted,
197
+ discount: acc.discount + r.discount,
198
+ }), { nominal: 0, discounted: 0, discount: 0 });
199
+ return {
200
+ method: "discountUnpaid",
201
+ convention: input.convention,
202
+ rates: input.rates,
203
+ provenance,
204
+ rows,
205
+ totals: { ...totals, effectiveDiscountFactor: safeRatio(totals.discounted, totals.nominal) },
206
+ riskMargin: input.riskMargin ?? null,
207
+ warnings,
208
+ };
209
+ }
210
+ //# sourceMappingURL=discounting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discounting.js","sourceRoot":"","sources":["../src/discounting.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAgE7C;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,4BAA4B,CAC1C,EAAqB,EACrB,IAAc;IAEd,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;IACtB,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,cAAc,CACtB,OAAO,EACP,0DAA0D,EAAE,CAAC,IAAI,CAAC,MAAM,eAAe,CAAC,OAAO,CAChG,CAAC;IACJ,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;QACnB,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,cAAc,CAAC,OAAO,EAAE,0DAA0D,CAAC,CAAC;QAChG,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,GAAG,GAAG,EAAE,CAAC,gBAAgB,CAAC;IAChC,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3D,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAC1C,IAAI,UAAU,GAAG,KAAK,CAAC;IAEvB,MAAM,IAAI,GAAuB,EAAE,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACV,MAAM,IAAI,cAAc,CACtB,OAAO,EACP,UAAU,GAAG,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,6EAA6E,CAC/H,CAAC;QACJ,CAAC;QACD,MAAM,SAAS,GAAqB,EAAE,CAAC;QACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC,CAAC;YACtD,IAAI,MAAM,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1C,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACvB,QAAQ,CAAC,IAAI,CACX,wBAAwB,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,+HAA+H,CAC9K,CAAC;YACJ,CAAC;YACD,SAAS,CAAC,IAAI,CAAC;gBACb,UAAU,EAAE,IAAI,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,CAAC,CAAE;gBAC/B,QAAQ,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,CAAC,CAAE;gBACjC,MAAM;aACP,CAAC,CAAC;QACL,CAAC;QACD,MAAM,UAAU,GAAG,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC;QACpD,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YACtE,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,UAAU,GAAG,IAAI,CAAC;gBAClB,QAAQ,CAAC,IAAI,CACX,2BAA2B,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,uCAAuC,QAAQ,uGAAuG,CAC7L,CAAC;YACJ,CAAC;YACD,SAAS,CAAC,IAAI,CAAC;gBACb,UAAU,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,CAAC,CAAE;gBACnC,QAAQ,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,CAAC,CAAE,GAAG,QAAQ;gBAC5C,MAAM,EAAE,UAAU;aACnB,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAC7F,CAAC;IAED,sEAAsE;IACtE,2EAA2E;IAC3E,yEAAyE;IACzE,gEAAgE;IAChE,2EAA2E;IAC3E,uEAAuE;IACvE,wEAAwE;IACxE,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;QACnB,IAAI,CAAC,CAAC,SAAS,IAAI,MAAM;YAAE,SAAS;QACpC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;YAAE,SAAS;QAClE,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC;QACrF,IAAI,UAAU,EAAE,CAAC;YACf,QAAQ,CAAC,IAAI,CACX,UAAU,CAAC,CAAC,MAAM,8BAA8B,CAAC,CAAC,SAAS,uCAAuC,UAAU,CAAC,MAAM,MAAM,UAAU,CAAC,SAAS,qIAAqI,CACnR,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC5B,CAAC;AAyFD,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAE3C,SAAS,kBAAkB,CAAC,UAAsC;IAChE,IACE,UAAU,KAAK,SAAS;QACxB,OAAO,UAAU,CAAC,MAAM,KAAK,QAAQ;QACrC,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAC/B,CAAC;QACD,MAAM,IAAI,cAAc,CACtB,eAAe,EACf,gJAAgJ,CACjJ,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GACT,OAAO,UAAU,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtF,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACjD,MAAM,IAAI,cAAc,CACtB,UAAU,EACV,mEAAmE,UAAU,CAAC,QAAQ,GAAG,CAC1F,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,aAAa,CAAC,KAAoB;IACzC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,CAAC;YACvD,MAAM,IAAI,cAAc,CACtB,UAAU,EACV,6EAA6E,KAAK,CAAC,UAAU,EAAE,CAChG,CAAC;QACJ,CAAC;QACD,OAAO;IACT,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,cAAc,CAAC,UAAU,EAAE,6CAA6C,CAAC,CAAC;IACtF,CAAC;IACD,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAChC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,cAAc,CACtB,UAAU,EACV,sBAAsB,CAAC,GAAG,CAAC,oDAAoD,CAAC,EAAE,CACnF,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAA0B;IACvD,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC;IAC/C,MAAM,YAAY,GAAG,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC;IACnD,IAAI,UAAU,KAAK,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,cAAc,CACtB,eAAe,EACf,wFAAwF,CACzF,CAAC;IACJ,CAAC;IACD,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACxD,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,EAAE,CAAC;QACzF,MAAM,IAAI,cAAc,CACtB,YAAY,EACZ,sEAAsE,KAAK,CAAC,UAAU,EAAE,CACzF,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAsB,UAAU;QAC3C,CAAC,CAAC,KAAK,CAAC,OAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;QAChF,CAAC,CAAC,KAAK,CAAC,SAAU,CAAC;IACrB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,wBAAwB,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1E,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,MAAM,eAAe,GAAa,EAAE,CAAC;IAErC,MAAM,IAAI,GAA0B,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QACzD,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,IAAI,WAAW,GAAG,KAAK,CAAC;QACxB,MAAM,OAAO,GAA+B,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;YACtE,IACE,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC;gBACrB,EAAE,CAAC,UAAU,GAAG,CAAC;gBACjB,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC;gBACnB,EAAE,CAAC,QAAQ,GAAG,EAAE,CAAC,UAAU;gBAC3B,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,EACjB,CAAC;gBACD,MAAM,IAAI,cAAc,CACtB,eAAe,EACf,UAAU,MAAM,CAAC,MAAM,iEAAiE,CACzF,CAAC;YACJ,CAAC;YACD,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;gBAAE,WAAW,GAAG,IAAI,CAAC;YACtC,MAAM,SAAS,GACb,KAAK,CAAC,UAAU,KAAK,YAAY;gBAC/B,CAAC,CAAC,EAAE,CAAC,QAAQ,GAAG,EAAE;gBAClB,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACzC,IAAI,IAAY,CAAC;YACjB,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,IAAI,GAAI,KAAK,CAAC,KAA8C,CAAC,UAAU,CAAC;YAC1E,CAAC;iBAAM,CAAC;gBACN,IAAI,SAAS,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;oBAC9C,aAAa,GAAG,IAAI,CAAC;oBACrB,QAAQ,CAAC,IAAI,CACX,wBAAwB,IAAI,CAAC,MAAM,kEAAkE,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,+BAA+B,CAC1J,CAAC;gBACJ,CAAC;gBACD,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAE,CAAC;YAC7E,CAAC;YACD,MAAM,cAAc,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;YAChD,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,GAAG,cAAc,CAAC;YACtC,OAAO,IAAI,EAAE,CAAC,MAAM,CAAC;YACrB,UAAU,IAAI,EAAE,CAAC;YACjB,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;QAChF,CAAC,CAAC,CAAC;QACH,IAAI,WAAW;YAAE,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACrD,OAAO;YACL,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,OAAO;YACP,UAAU;YACV,QAAQ,EAAE,OAAO,GAAG,UAAU;YAC9B,uBAAuB,EAAE,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC;YACvD,SAAS,EAAE,OAAO;SACnB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,QAAQ,CAAC,IAAI,CACX,6CAA6C,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,kEAAkE,CAC1I,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CACxB,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,OAAO,EAAE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO;QAChC,UAAU,EAAE,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU;QACzC,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ;KACpC,CAAC,EACF,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAC3C,CAAC;IAEF,OAAO;QACL,MAAM,EAAE,gBAAgB;QACxB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,UAAU;QACV,IAAI;QACJ,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,uBAAuB,EAAE,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE;QAC5F,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,IAAI;QACpC,QAAQ;KACT,CAAC;AACJ,CAAC"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Expected-loss-ratio methods (Friedland ch. 8 and 10).
3
+ *
4
+ * Cape Cod (Stanard-Buhlmann) derives its ELR mechanically from the data:
5
+ * ELR* = sum(adjusted reported) / sum(adjusted used-up premium), where the
6
+ * per-origin adjustment factors bring losses and premium to a common (target)
7
+ * cost and rate level. Expected unreported for each origin then comes from
8
+ * the ELR restated to THAT origin's own level.
9
+ *
10
+ * The Expected Claims method applies a selected ELR (at target level) the
11
+ * same way, with no reliance on the origin's own emerged losses.
12
+ */
13
+ export interface ElrMethodRow {
14
+ origin: string;
15
+ /** Reported (or paid) losses on the latest diagonal, the method's basis. */
16
+ reported: number;
17
+ /** Cumulative development factor to ultimate for this origin's age. */
18
+ cdf: number;
19
+ /** Earned premium for the origin (raw). */
20
+ premium: number;
21
+ /**
22
+ * Factor bringing this origin's LOSSES to the target cost level
23
+ * (e.g. (1+sevTrend)^(target-year) x uncap adjustment). Default 1.
24
+ */
25
+ lossAdj?: number;
26
+ /**
27
+ * Factor bringing this origin's PREMIUM to the target rate/cost level
28
+ * (on-level factor x premium trend). Default 1.
29
+ */
30
+ premiumAdj?: number;
31
+ }
32
+ export interface CapeCodRow {
33
+ origin: string;
34
+ reported: number;
35
+ cdf: number;
36
+ premium: number;
37
+ usedUpPremium: number;
38
+ /**
39
+ * The ELR governing THIS origin at the target level. With decay = 1 it is
40
+ * the single pooled Cape Cod ELR (same for every row); with decay < 1 it
41
+ * is the origin's own decay-weighted average (Gluck 1997).
42
+ */
43
+ elrAtTargetLevel: number;
44
+ /** elrAtTargetLevel restated to this origin's own level. */
45
+ elrAtOriginLevel: number;
46
+ expectedUltimate: number;
47
+ ultimate: number;
48
+ ibnrToReported: number;
49
+ }
50
+ export interface CapeCodResult {
51
+ method: "capeCod";
52
+ /** The mechanical ELR at the TARGET (adjusted) level. */
53
+ elrAtTargetLevel: number;
54
+ rows: CapeCodRow[];
55
+ totals: {
56
+ reported: number;
57
+ ultimate: number;
58
+ usedUpPremium: number;
59
+ };
60
+ warnings: string[];
61
+ }
62
+ /**
63
+ * Cape Cod: ELR* = sum(reported x lossAdj) / sum(premium x premiumAdj / cdf).
64
+ * Ultimate_i = reported_i + expectedUltimate_i x (1 - 1/cdf_i), with
65
+ * expectedUltimate_i = ELR* x premium_i x premiumAdj_i / lossAdj_i (the
66
+ * target-level ELR restated to origin i's own level, times its premium).
67
+ *
68
+ * Generalized Cape Cod (Gluck 1997, PCAS LXXXIV, eq. 6.1): with a decay
69
+ * factor D in [0, 1], each origin gets its OWN target-level ELR — the
70
+ * weighted average over all origins with weights usedUp_j x D^|i-j|
71
+ * (distance in origin-row steps). D = 1 is the standard Cape Cod (single
72
+ * pooled ELR); D = 0 makes every year stand alone, reproducing the pure
73
+ * development ultimate. Gluck's practical guidance: D between 0.50 and
74
+ * 1.00, with 0.75 as a customary default; lower D suits large stable books
75
+ * (trend error dominates), higher D suits small erratic ones (development
76
+ * error dominates).
77
+ */
78
+ export declare function runCapeCod(rows: ElrMethodRow[], opts?: {
79
+ baseIsPurePremium?: boolean;
80
+ decay?: number;
81
+ }): CapeCodResult;
82
+ export interface ExpectedClaimsRow {
83
+ origin: string;
84
+ premium: number;
85
+ elrAtOriginLevel: number;
86
+ ultimate: number;
87
+ }
88
+ export interface ExpectedClaimsResult {
89
+ method: "expectedClaims";
90
+ /** The selected ELR at the target level the caller supplied. */
91
+ selectedElrAtTargetLevel: number;
92
+ rows: ExpectedClaimsRow[];
93
+ totals: {
94
+ ultimate: number;
95
+ };
96
+ warnings: string[];
97
+ }
98
+ /**
99
+ * Expected Claims: ultimate_i = selectedELR (target level) restated to the
100
+ * origin's level (x premiumAdj_i / lossAdj_i) x premium_i. Pure a-priori -
101
+ * the origin's own emerged losses never enter.
102
+ */
103
+ export declare function runExpectedClaims(rows: ElrMethodRow[], selectedElrAtTargetLevel: number): ExpectedClaimsResult;
104
+ //# sourceMappingURL=elrMethods.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elrMethods.d.ts","sourceRoot":"","sources":["../src/elrMethods.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAoCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,YAAY,EAAE,EACpB,IAAI,GAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACzD,aAAa,CA2Ff;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,gBAAgB,CAAC;IACzB,gEAAgE;IAChE,wBAAwB,EAAE,MAAM,CAAC;IACjC,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC1B,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,YAAY,EAAE,EACpB,wBAAwB,EAAE,MAAM,GAC/B,oBAAoB,CAsBtB"}
@@ -0,0 +1,151 @@
1
+ import { ReservingError } from "./types.js";
2
+ import { isNum } from "./util.js";
3
+ function validateRows(rows) {
4
+ if (rows.length === 0) {
5
+ throw new ReservingError("NO_DATA", "ELR methods need at least one origin row");
6
+ }
7
+ for (const r of rows) {
8
+ // CDFs below 1 are legitimate on incurred bases (case run-off develops
9
+ // reported incurred DOWNWARD); the methods then post an expected
10
+ // take-down rather than a provision. Only non-positive CDFs are garbage.
11
+ if (!isNum(r.cdf) || r.cdf <= 0) {
12
+ throw new ReservingError("BAD_CDF", `Origin ${r.origin}: the CDF to ultimate must be positive (got ${r.cdf})`);
13
+ }
14
+ if (!isNum(r.premium) || r.premium <= 0) {
15
+ throw new ReservingError("BAD_PREMIUM", `Origin ${r.origin}: the exposure base (earned premium or exposure units) must be positive`);
16
+ }
17
+ if (!isNum(r.reported) || r.reported < 0) {
18
+ throw new ReservingError("BAD_LOSSES", `Origin ${r.origin}: reported must be >= 0`);
19
+ }
20
+ const lossAdj = r.lossAdj ?? 1;
21
+ const premiumAdj = r.premiumAdj ?? 1;
22
+ if (!isNum(lossAdj) || lossAdj <= 0 || !isNum(premiumAdj) || premiumAdj <= 0) {
23
+ throw new ReservingError("BAD_ADJ", `Origin ${r.origin}: adjustment factors must be positive`);
24
+ }
25
+ }
26
+ }
27
+ /**
28
+ * Cape Cod: ELR* = sum(reported x lossAdj) / sum(premium x premiumAdj / cdf).
29
+ * Ultimate_i = reported_i + expectedUltimate_i x (1 - 1/cdf_i), with
30
+ * expectedUltimate_i = ELR* x premium_i x premiumAdj_i / lossAdj_i (the
31
+ * target-level ELR restated to origin i's own level, times its premium).
32
+ *
33
+ * Generalized Cape Cod (Gluck 1997, PCAS LXXXIV, eq. 6.1): with a decay
34
+ * factor D in [0, 1], each origin gets its OWN target-level ELR — the
35
+ * weighted average over all origins with weights usedUp_j x D^|i-j|
36
+ * (distance in origin-row steps). D = 1 is the standard Cape Cod (single
37
+ * pooled ELR); D = 0 makes every year stand alone, reproducing the pure
38
+ * development ultimate. Gluck's practical guidance: D between 0.50 and
39
+ * 1.00, with 0.75 as a customary default; lower D suits large stable books
40
+ * (trend error dominates), higher D suits small erratic ones (development
41
+ * error dominates).
42
+ */
43
+ export function runCapeCod(rows, opts = {}) {
44
+ validateRows(rows);
45
+ const warnings = [];
46
+ const decay = opts.decay ?? 1;
47
+ if (!isNum(decay) || decay < 0 || decay > 1) {
48
+ throw new ReservingError("BAD_ADJ", `The decay factor must be between 0 and 1 (got ${decay})`);
49
+ }
50
+ const losses = rows.map((r) => r.reported * (r.lossAdj ?? 1));
51
+ const usedUps = rows.map((r) => (r.premium * (r.premiumAdj ?? 1)) / r.cdf);
52
+ let lossSum = 0;
53
+ let usedUpSum = 0;
54
+ for (let j = 0; j < rows.length; j++) {
55
+ lossSum += losses[j];
56
+ usedUpSum += usedUps[j];
57
+ }
58
+ if (!(usedUpSum > 0)) {
59
+ throw new ReservingError("BAD_PREMIUM", "Used-up premium is not positive");
60
+ }
61
+ const pooledElr = lossSum / usedUpSum;
62
+ // Per-origin target-level ELR: pooled when D = 1 (same float, same code
63
+ // path — the published Cape Cod behavior must stay byte-identical), the
64
+ // Gluck decay-weighted average otherwise. 0^0 = 1 in JS, so D = 0 cleanly
65
+ // reduces to "own year only".
66
+ const elrByRow = rows.map((_, i) => {
67
+ if (decay === 1)
68
+ return pooledElr;
69
+ let num = 0;
70
+ let den = 0;
71
+ for (let j = 0; j < rows.length; j++) {
72
+ const w = decay ** Math.abs(i - j);
73
+ num += losses[j] * w;
74
+ den += usedUps[j] * w;
75
+ }
76
+ if (!(den > 0)) {
77
+ throw new ReservingError("BAD_PREMIUM", `Origin ${rows[i].origin}: decayed used-up premium is not positive`);
78
+ }
79
+ return num / den;
80
+ });
81
+ if (rows.some((r) => r.cdf < 1)) {
82
+ warnings.push("Some origins have CDFs below 1 (expected downward development): their Cape Cod provision is an expected take-down, standard for incurred bases with case redundancy");
83
+ }
84
+ // A pure premium (losses per exposure unit) is a dollar amount, not a ratio,
85
+ // so the "ELR looks too high" sanity check only applies to the loss-ratio base.
86
+ if (!opts.baseIsPurePremium && pooledElr > 2) {
87
+ warnings.push(`Cape Cod mechanical ELR is ${(pooledElr * 100).toFixed(0)}% - check that premium and losses are on comparable levels`);
88
+ }
89
+ const out = rows.map((r, i) => {
90
+ const lossAdj = r.lossAdj ?? 1;
91
+ const premiumAdj = r.premiumAdj ?? 1;
92
+ const usedUp = (r.premium * premiumAdj) / r.cdf;
93
+ const elrAtTargetLevel = elrByRow[i];
94
+ const elrAtOriginLevel = (elrAtTargetLevel * premiumAdj) / lossAdj;
95
+ const expectedUltimate = elrAtOriginLevel * r.premium;
96
+ const ultimate = r.reported + expectedUltimate * (1 - 1 / r.cdf);
97
+ return {
98
+ origin: r.origin,
99
+ reported: r.reported,
100
+ cdf: r.cdf,
101
+ premium: r.premium,
102
+ usedUpPremium: usedUp,
103
+ elrAtTargetLevel,
104
+ elrAtOriginLevel,
105
+ expectedUltimate,
106
+ ultimate,
107
+ ibnrToReported: ultimate - r.reported,
108
+ };
109
+ });
110
+ return {
111
+ method: "capeCod",
112
+ // With decay < 1 there is no single ELR; the scalar is the pooled (D=1)
113
+ // reference value. Per-row elrAtTargetLevel drives the ultimates.
114
+ elrAtTargetLevel: pooledElr,
115
+ rows: out,
116
+ totals: {
117
+ reported: out.reduce((a, r) => a + r.reported, 0),
118
+ ultimate: out.reduce((a, r) => a + r.ultimate, 0),
119
+ usedUpPremium: out.reduce((a, r) => a + r.usedUpPremium, 0),
120
+ },
121
+ warnings,
122
+ };
123
+ }
124
+ /**
125
+ * Expected Claims: ultimate_i = selectedELR (target level) restated to the
126
+ * origin's level (x premiumAdj_i / lossAdj_i) x premium_i. Pure a-priori -
127
+ * the origin's own emerged losses never enter.
128
+ */
129
+ export function runExpectedClaims(rows, selectedElrAtTargetLevel) {
130
+ validateRows(rows);
131
+ if (!isNum(selectedElrAtTargetLevel) || selectedElrAtTargetLevel <= 0) {
132
+ throw new ReservingError("BAD_ELR", "The selected ELR must be a positive number");
133
+ }
134
+ const out = rows.map((r) => {
135
+ const elrAtOriginLevel = (selectedElrAtTargetLevel * (r.premiumAdj ?? 1)) / (r.lossAdj ?? 1);
136
+ return {
137
+ origin: r.origin,
138
+ premium: r.premium,
139
+ elrAtOriginLevel,
140
+ ultimate: elrAtOriginLevel * r.premium,
141
+ };
142
+ });
143
+ return {
144
+ method: "expectedClaims",
145
+ selectedElrAtTargetLevel,
146
+ rows: out,
147
+ totals: { ultimate: out.reduce((a, r) => a + r.ultimate, 0) },
148
+ warnings: [],
149
+ };
150
+ }
151
+ //# sourceMappingURL=elrMethods.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elrMethods.js","sourceRoot":"","sources":["../src/elrMethods.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { AverageSpec, DevelopmentFactors, Triangle } from "./types.js";
2
+ /**
3
+ * Age-to-age (link ratio) development factors and the averages menu.
4
+ *
5
+ * Ground truth:
6
+ * - f[i][j] = C[i][j+1] / C[i][j] on cumulative data.
7
+ * - The volume-weighted average for a column is sum(numerators) / sum(denominators)
8
+ * over exactly the rows where BOTH cells exist -- not the mean of ratios.
9
+ * - Division by a missing, zero, or negative denominator = no factor (null).
10
+ */
11
+ export declare const DEFAULT_AVERAGES: AverageSpec[];
12
+ export declare function computeDevelopmentFactors(tri: Triangle, averageSpecs?: AverageSpec[]): DevelopmentFactors;
13
+ /**
14
+ * Per-column coefficient of variation of the individual age-to-age factors:
15
+ * the stability metric the capped-vs-unlimited layer comparison is judged on.
16
+ * null where a column has fewer than two observed factors.
17
+ */
18
+ export declare function factorVolatility(dev: DevelopmentFactors): (number | null)[];
19
+ //# sourceMappingURL=factors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factors.d.ts","sourceRoot":"","sources":["../src/factors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG5E;;;;;;;;GAQG;AAEH,eAAO,MAAM,gBAAgB,EAAE,WAAW,EASzC,CAAC;AAyEF,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,QAAQ,EACb,YAAY,GAAE,WAAW,EAAqB,GAC7C,kBAAkB,CAwBpB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,kBAAkB,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAiB3E"}
@@ -0,0 +1,136 @@
1
+ import { isNum, safeRatio } from "./util.js";
2
+ /**
3
+ * Age-to-age (link ratio) development factors and the averages menu.
4
+ *
5
+ * Ground truth:
6
+ * - f[i][j] = C[i][j+1] / C[i][j] on cumulative data.
7
+ * - The volume-weighted average for a column is sum(numerators) / sum(denominators)
8
+ * over exactly the rows where BOTH cells exist -- not the mean of ratios.
9
+ * - Division by a missing, zero, or negative denominator = no factor (null).
10
+ */
11
+ export const DEFAULT_AVERAGES = [
12
+ { key: "all-wtd", label: "All-year volume-weighted", kind: "weighted" },
13
+ { key: "all-str", label: "All-year straight", kind: "straight" },
14
+ { key: "5-wtd", label: "5-year volume-weighted", kind: "weighted", years: 5 },
15
+ { key: "5-str", label: "5-year straight", kind: "straight", years: 5 },
16
+ { key: "3-wtd", label: "3-year volume-weighted", kind: "weighted", years: 3 },
17
+ { key: "3-str", label: "3-year straight", kind: "straight", years: 3 },
18
+ { key: "med-5x1", label: "Medial 5-year (excl. hi/lo)", kind: "medial", years: 5 },
19
+ { key: "geo-all", label: "All-year geometric", kind: "geometric" },
20
+ ];
21
+ /** Valid (both cells present, positive denominator) pairs for column j, in origin order. */
22
+ function columnPairs(tri, j) {
23
+ const pairs = [];
24
+ for (let i = 0; i < tri.origins.length; i++) {
25
+ const den = tri.values[i][j] ?? null;
26
+ const num = tri.values[i][j + 1] ?? null;
27
+ const factor = safeRatio(num, den);
28
+ if (factor !== null && isNum(num) && isNum(den)) {
29
+ pairs.push({ rowIndex: i, numerator: num, denominator: den, factor });
30
+ }
31
+ }
32
+ return pairs;
33
+ }
34
+ function averageForColumn(pairs, spec) {
35
+ // An "n-year" average covers the latest n ORIGIN PERIODS that can carry a
36
+ // factor in this column (ending at the last row with a valid factor) -- not
37
+ // the last n valid factors. With an interior missing factor the window must
38
+ // not silently reach further back in time.
39
+ let window = pairs;
40
+ if (spec.years !== undefined && pairs.length > 0) {
41
+ const lastRow = pairs[pairs.length - 1].rowIndex;
42
+ window = pairs.filter((p) => p.rowIndex > lastRow - spec.years);
43
+ }
44
+ if (window.length === 0)
45
+ return null;
46
+ switch (spec.kind) {
47
+ case "straight": {
48
+ let s = 0;
49
+ for (const p of window)
50
+ s += p.factor;
51
+ return s / window.length;
52
+ }
53
+ case "weighted": {
54
+ let num = 0;
55
+ let den = 0;
56
+ for (const p of window) {
57
+ num += p.numerator;
58
+ den += p.denominator;
59
+ }
60
+ return den > 0 ? num / den : null;
61
+ }
62
+ case "medial": {
63
+ if (window.length < 3) {
64
+ // Not enough points to exclude hi/lo; fall back to a straight average.
65
+ let s = 0;
66
+ for (const p of window)
67
+ s += p.factor;
68
+ return s / window.length;
69
+ }
70
+ const sorted = window.map((p) => p.factor).sort((a, b) => a - b);
71
+ const inner = sorted.slice(1, sorted.length - 1);
72
+ let s = 0;
73
+ for (const f of inner)
74
+ s += f;
75
+ return s / inner.length;
76
+ }
77
+ case "geometric": {
78
+ let logSum = 0;
79
+ for (const p of window) {
80
+ if (p.factor <= 0)
81
+ return null; // geometric mean undefined
82
+ logSum += Math.log(p.factor);
83
+ }
84
+ return Math.exp(logSum / window.length);
85
+ }
86
+ }
87
+ }
88
+ export function computeDevelopmentFactors(tri, averageSpecs = DEFAULT_AVERAGES) {
89
+ const nCols = Math.max(0, tri.ages.length - 1);
90
+ const individual = tri.origins.map((_, i) => {
91
+ const row = [];
92
+ for (let j = 0; j < nCols; j++) {
93
+ row.push(safeRatio(tri.values[i][j + 1] ?? null, tri.values[i][j] ?? null));
94
+ }
95
+ return row;
96
+ });
97
+ const pairsByColumn = [];
98
+ for (let j = 0; j < nCols; j++)
99
+ pairsByColumn.push(columnPairs(tri, j));
100
+ const averages = averageSpecs.map((spec) => ({
101
+ spec,
102
+ values: pairsByColumn.map((pairs) => averageForColumn(pairs, spec)),
103
+ }));
104
+ return {
105
+ fromAges: tri.ages.slice(0, nCols),
106
+ toAges: tri.ages.slice(1),
107
+ individual,
108
+ averages,
109
+ };
110
+ }
111
+ /**
112
+ * Per-column coefficient of variation of the individual age-to-age factors:
113
+ * the stability metric the capped-vs-unlimited layer comparison is judged on.
114
+ * null where a column has fewer than two observed factors.
115
+ */
116
+ export function factorVolatility(dev) {
117
+ const nCols = dev.fromAges.length;
118
+ const out = new Array(nCols).fill(null);
119
+ for (let j = 0; j < nCols; j++) {
120
+ const values = [];
121
+ for (const row of dev.individual) {
122
+ const v = row[j];
123
+ if (v !== null && v !== undefined && Number.isFinite(v))
124
+ values.push(v);
125
+ }
126
+ if (values.length < 2)
127
+ continue;
128
+ const mean = values.reduce((a, v) => a + v, 0) / values.length;
129
+ if (mean === 0)
130
+ continue;
131
+ const variance = values.reduce((a, v) => a + (v - mean) ** 2, 0) / (values.length - 1);
132
+ out[j] = Math.sqrt(variance) / Math.abs(mean);
133
+ }
134
+ return out;
135
+ }
136
+ //# sourceMappingURL=factors.js.map