@actuarial-ts/core 0.3.0 → 0.4.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 +104 -0
- package/dist/berquist.d.ts.map +1 -1
- package/dist/berquist.js +4 -12
- package/dist/berquist.js.map +1 -1
- package/dist/caseOutstanding.d.ts.map +1 -1
- package/dist/caseOutstanding.js +2 -9
- package/dist/caseOutstanding.js.map +1 -1
- package/dist/casualtyDiagnostics.d.ts +53 -0
- package/dist/casualtyDiagnostics.d.ts.map +1 -0
- package/dist/casualtyDiagnostics.js +122 -0
- package/dist/casualtyDiagnostics.js.map +1 -0
- package/dist/fisherLange.d.ts.map +1 -1
- package/dist/fisherLange.js +2 -9
- package/dist/fisherLange.js.map +1 -1
- package/dist/freqSev.d.ts.map +1 -1
- package/dist/freqSev.js +3 -10
- package/dist/freqSev.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/metricDiagnostics.d.ts +212 -0
- package/dist/metricDiagnostics.d.ts.map +1 -0
- package/dist/metricDiagnostics.js +627 -0
- package/dist/metricDiagnostics.js.map +1 -0
- package/dist/munichChainLadder.d.ts.map +1 -1
- package/dist/munichChainLadder.js +2 -7
- package/dist/munichChainLadder.js.map +1 -1
- package/dist/periods.d.ts +46 -0
- package/dist/periods.d.ts.map +1 -0
- package/dist/periods.js +121 -0
- package/dist/periods.js.map +1 -0
- package/dist/util.d.ts +8 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +15 -0
- package/dist/util.js.map +1 -1
- package/package.json +1 -1
- package/src/berquist.ts +4 -15
- package/src/caseOutstanding.ts +12 -14
- package/src/casualtyDiagnostics.ts +202 -0
- package/src/fisherLange.ts +12 -14
- package/src/freqSev.ts +11 -15
- package/src/index.ts +3 -0
- package/src/metricDiagnostics.ts +876 -0
- package/src/munichChainLadder.ts +6 -12
- package/src/periods.ts +177 -0
- package/src/util.ts +20 -0
package/src/munichChainLadder.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mackEstimators, extrapolateSigma2 } from "./mack.js";
|
|
2
2
|
import type { Triangle } from "./types.js";
|
|
3
3
|
import { ReservingError } from "./types.js";
|
|
4
|
-
import { isNum, lastObservedIndex } from "./util.js";
|
|
4
|
+
import { assertSameShape, isNum, lastObservedIndex } from "./util.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Munich chain ladder, Quarg & Mack (2004): a paired paid/incurred projection
|
|
@@ -116,17 +116,11 @@ export function runMunichChainLadder(
|
|
|
116
116
|
const n = paid.origins.length;
|
|
117
117
|
const K = paid.ages.length;
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
incurred
|
|
122
|
-
paid
|
|
123
|
-
|
|
124
|
-
) {
|
|
125
|
-
throw new ReservingError(
|
|
126
|
-
"SHAPE",
|
|
127
|
-
"Munich chain ladder needs paid and incurred triangles with identical origins and ages",
|
|
128
|
-
);
|
|
129
|
-
}
|
|
119
|
+
assertSameShape(
|
|
120
|
+
paid,
|
|
121
|
+
incurred,
|
|
122
|
+
"Munich chain ladder needs paid and incurred triangles with identical origins and ages",
|
|
123
|
+
);
|
|
130
124
|
for (const tri of [paid, incurred]) {
|
|
131
125
|
if (tri.values.length !== n || tri.values.some((row) => row.length !== K)) {
|
|
132
126
|
throw new ReservingError(
|
package/src/periods.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ReservingError } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type QuarterNumber = 1 | 2 | 3 | 4;
|
|
4
|
+
|
|
5
|
+
export interface QuarterPeriod {
|
|
6
|
+
year: number;
|
|
7
|
+
quarter: QuarterNumber;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type QuarterFormat = "compact" | "hyphenated" | "quarter-first";
|
|
11
|
+
export type DevelopmentAgeConvention = "quarter-end-first-observation" | "elapsed";
|
|
12
|
+
|
|
13
|
+
const QUARTER_PATTERNS = [
|
|
14
|
+
/^(\d{4})Q([1-4])$/i,
|
|
15
|
+
/^(\d{4})-Q([1-4])$/i,
|
|
16
|
+
/^Q([1-4])\s+(\d{4})$/i,
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
/** Parses `2024Q3`, `2024-Q3`, or `Q3 2024`; no lexical date guessing. */
|
|
20
|
+
export function parseQuarterPeriod(value: string): QuarterPeriod {
|
|
21
|
+
const text = value.trim();
|
|
22
|
+
for (let i = 0; i < QUARTER_PATTERNS.length; i++) {
|
|
23
|
+
const match = QUARTER_PATTERNS[i]!.exec(text);
|
|
24
|
+
if (!match) continue;
|
|
25
|
+
const quarterFirst = i === 2;
|
|
26
|
+
const year = Number(match[quarterFirst ? 2 : 1]);
|
|
27
|
+
const quarter = Number(match[quarterFirst ? 1 : 2]) as QuarterNumber;
|
|
28
|
+
if (Number.isSafeInteger(year) && year >= 1 && year <= 9999) return { year, quarter };
|
|
29
|
+
}
|
|
30
|
+
throw new ReservingError(
|
|
31
|
+
"BAD_ORIGIN",
|
|
32
|
+
`Quarter period must be YYYYQn, YYYY-Qn, or Qn YYYY with n from 1 to 4; got ${JSON.stringify(value)}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatQuarterPeriod(
|
|
37
|
+
period: QuarterPeriod,
|
|
38
|
+
format: QuarterFormat = "compact",
|
|
39
|
+
): string {
|
|
40
|
+
assertQuarterPeriod(period);
|
|
41
|
+
const year = String(period.year).padStart(4, "0");
|
|
42
|
+
if (format === "hyphenated") return `${year}-Q${period.quarter}`;
|
|
43
|
+
if (format === "quarter-first") return `Q${period.quarter} ${year}`;
|
|
44
|
+
return `${year}Q${period.quarter}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function compareQuarterPeriods(a: QuarterPeriod | string, b: QuarterPeriod | string): number {
|
|
48
|
+
const left = typeof a === "string" ? parseQuarterPeriod(a) : assertQuarterPeriod(a);
|
|
49
|
+
const right = typeof b === "string" ? parseQuarterPeriod(b) : assertQuarterPeriod(b);
|
|
50
|
+
return quarterIndex(left) - quarterIndex(right);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function sortQuarterPeriods<T extends QuarterPeriod | string>(periods: readonly T[]): T[] {
|
|
54
|
+
return [...periods].sort(compareQuarterPeriods);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function quarterIndex(period: QuarterPeriod): number {
|
|
58
|
+
assertQuarterPeriod(period);
|
|
59
|
+
return period.year * 4 + period.quarter - 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function addQuarters(period: QuarterPeriod | string, count: number): QuarterPeriod {
|
|
63
|
+
if (!Number.isSafeInteger(count)) {
|
|
64
|
+
throw new ReservingError("BAD_ORIGIN", `Quarter offset must be an integer; got ${count}`);
|
|
65
|
+
}
|
|
66
|
+
const parsed = typeof period === "string" ? parseQuarterPeriod(period) : assertQuarterPeriod(period);
|
|
67
|
+
const index = quarterIndex(parsed) + count;
|
|
68
|
+
const year = Math.floor(index / 4);
|
|
69
|
+
const quarter = ((index % 4 + 4) % 4 + 1) as QuarterNumber;
|
|
70
|
+
return assertQuarterPeriod({ year, quarter });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Development age in months. The default reflects quarter-end snapshots:
|
|
75
|
+
* the first observation in an origin quarter is age 3. Select `elapsed` only
|
|
76
|
+
* when the source genuinely defines a same-quarter observation as age zero.
|
|
77
|
+
*/
|
|
78
|
+
export function developmentAgeMonths(
|
|
79
|
+
origin: QuarterPeriod | string,
|
|
80
|
+
valuation: QuarterPeriod | string,
|
|
81
|
+
convention: DevelopmentAgeConvention = "quarter-end-first-observation",
|
|
82
|
+
): number {
|
|
83
|
+
const o = typeof origin === "string" ? parseQuarterPeriod(origin) : assertQuarterPeriod(origin);
|
|
84
|
+
const v = typeof valuation === "string" ? parseQuarterPeriod(valuation) : assertQuarterPeriod(valuation);
|
|
85
|
+
const elapsed = quarterIndex(v) - quarterIndex(o);
|
|
86
|
+
if (elapsed < 0) {
|
|
87
|
+
throw new ReservingError(
|
|
88
|
+
"BAD_DATE",
|
|
89
|
+
`Valuation quarter ${formatQuarterPeriod(v)} precedes origin quarter ${formatQuarterPeriod(o)}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return (elapsed + (convention === "quarter-end-first-observation" ? 1 : 0)) * 3;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface PolicyPeriodOptions {
|
|
96
|
+
/** First calendar quarter in the policy/fiscal year. Defaults to Q1. */
|
|
97
|
+
startQuarter?: QuarterNumber;
|
|
98
|
+
/** Caller override for nonstandard labels or boundaries. */
|
|
99
|
+
mapper?: (period: QuarterPeriod) => string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Maps a calendar quarter to the starting year of its policy/fiscal period. */
|
|
103
|
+
export function policyPeriodLabel(
|
|
104
|
+
period: QuarterPeriod | string,
|
|
105
|
+
options: PolicyPeriodOptions = {},
|
|
106
|
+
): string {
|
|
107
|
+
const parsed = typeof period === "string" ? parseQuarterPeriod(period) : assertQuarterPeriod(period);
|
|
108
|
+
if (options.mapper) return options.mapper({ ...parsed });
|
|
109
|
+
const startQuarter = options.startQuarter ?? 1;
|
|
110
|
+
if (![1, 2, 3, 4].includes(startQuarter)) {
|
|
111
|
+
throw new ReservingError("BAD_ORIGIN", `Policy-year startQuarter must be 1 through 4; got ${startQuarter}`);
|
|
112
|
+
}
|
|
113
|
+
const startYear = startQuarter === 1 || parsed.quarter >= startQuarter ? parsed.year : parsed.year - 1;
|
|
114
|
+
return String(startYear);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface CompleteQuarterCutoffOptions {
|
|
118
|
+
/** Include the in-progress quarter. Default false: only completed quarters. */
|
|
119
|
+
includePartial?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Returns the latest included quarter for an ISO calendar date. */
|
|
123
|
+
export function completeQuarterCutoff(
|
|
124
|
+
asOfDate: string,
|
|
125
|
+
options: CompleteQuarterCutoffOptions = {},
|
|
126
|
+
): QuarterPeriod {
|
|
127
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(asOfDate);
|
|
128
|
+
if (!match) throw new ReservingError("BAD_DATE", `asOfDate must be YYYY-MM-DD; got ${JSON.stringify(asOfDate)}`);
|
|
129
|
+
const year = Number(match[1]);
|
|
130
|
+
const month = Number(match[2]);
|
|
131
|
+
const day = Number(match[3]);
|
|
132
|
+
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
133
|
+
const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
134
|
+
if (year < 1 || year > 9999 || month < 1 || month > 12 || day < 1 || day > daysInMonth[month - 1]!) {
|
|
135
|
+
throw new ReservingError("BAD_DATE", `asOfDate is not a valid calendar date: ${asOfDate}`);
|
|
136
|
+
}
|
|
137
|
+
const quarter = (Math.floor((month - 1) / 3) + 1) as QuarterNumber;
|
|
138
|
+
if (options.includePartial) return { year, quarter };
|
|
139
|
+
const endMonth = quarter * 3;
|
|
140
|
+
const endDay = daysInMonth[endMonth - 1]!;
|
|
141
|
+
if (month === endMonth && day === endDay) return { year, quarter };
|
|
142
|
+
return addQuarters({ year, quarter }, -1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface CompleteQuarterlyCutoffsOptions {
|
|
146
|
+
originAsOfDate?: string;
|
|
147
|
+
valuationAsOfDate?: string;
|
|
148
|
+
includePartialOrigin?: boolean;
|
|
149
|
+
includePartialValuation?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Explicit origin and valuation cutoffs, independently configurable. */
|
|
153
|
+
export function completeQuarterlyCutoffs(
|
|
154
|
+
asOfDate: string,
|
|
155
|
+
options: CompleteQuarterlyCutoffsOptions = {},
|
|
156
|
+
): { originThrough: QuarterPeriod; valuationThrough: QuarterPeriod } {
|
|
157
|
+
return {
|
|
158
|
+
originThrough: completeQuarterCutoff(options.originAsOfDate ?? asOfDate, {
|
|
159
|
+
includePartial: options.includePartialOrigin,
|
|
160
|
+
}),
|
|
161
|
+
valuationThrough: completeQuarterCutoff(options.valuationAsOfDate ?? asOfDate, {
|
|
162
|
+
includePartial: options.includePartialValuation,
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function assertQuarterPeriod(period: QuarterPeriod): QuarterPeriod {
|
|
168
|
+
if (
|
|
169
|
+
!Number.isSafeInteger(period.year) ||
|
|
170
|
+
period.year < 1 ||
|
|
171
|
+
period.year > 9999 ||
|
|
172
|
+
![1, 2, 3, 4].includes(period.quarter)
|
|
173
|
+
) {
|
|
174
|
+
throw new ReservingError("BAD_ORIGIN", `Invalid quarter period ${JSON.stringify(period)}`);
|
|
175
|
+
}
|
|
176
|
+
return period;
|
|
177
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
/** Shared numeric helpers. All null-safe by construction. */
|
|
2
2
|
|
|
3
|
+
import type { Triangle } from "./types.js";
|
|
4
|
+
import { ReservingError } from "./types.js";
|
|
5
|
+
|
|
3
6
|
/** True when v is a usable finite number. */
|
|
4
7
|
export function isNum(v: number | null | undefined): v is number {
|
|
5
8
|
return typeof v === "number" && Number.isFinite(v);
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Shared origins/ages shape guard: throws ReservingError("SHAPE", message)
|
|
13
|
+
* unless both triangles have the same number of origins and ages AND the
|
|
14
|
+
* same origin/age labels in the same order. `message` is thrown verbatim,
|
|
15
|
+
* so callers own their exact wording.
|
|
16
|
+
*/
|
|
17
|
+
export function assertSameShape(a: Triangle, b: Triangle, message: string): void {
|
|
18
|
+
if (
|
|
19
|
+
a.origins.length !== b.origins.length ||
|
|
20
|
+
a.ages.length !== b.ages.length ||
|
|
21
|
+
a.origins.some((o, i) => o !== b.origins[i]) ||
|
|
22
|
+
a.ages.some((g, j) => g !== b.ages[j])
|
|
23
|
+
) {
|
|
24
|
+
throw new ReservingError("SHAPE", message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
/**
|
|
9
29
|
* Safe ratio: returns null when either side is missing or the denominator
|
|
10
30
|
* is missing, zero, or negative ("no factor", never an exception).
|