@robotixai/calculator-engine 0.1.0 → 0.2.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/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +18 -1
- package/dist/backtest.d.ts.map +1 -1
- package/dist/backtest.js +4 -0
- package/dist/defaults.d.ts +7 -1
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +18 -0
- package/dist/heatmap.d.ts.map +1 -1
- package/dist/heatmap.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +56 -0
- package/dist/monte-carlo.d.ts.map +1 -1
- package/dist/monte-carlo.js +10 -0
- package/dist/optimizer.d.ts.map +1 -1
- package/dist/optimizer.js +13 -0
- package/dist/portfolio.d.ts.map +1 -1
- package/dist/portfolio.js +7 -0
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +18 -0
- package/dist/sensitivity.d.ts.map +1 -1
- package/dist/sensitivity.js +13 -1
- package/dist/tax.d.ts.map +1 -1
- package/dist/tax.js +13 -4
- package/dist/withdrawal.d.ts.map +1 -1
- package/dist/withdrawal.js +5 -2
- package/package.json +1 -1
package/dist/advanced.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,QAAQ,EAER,WAAW,EACX,OAAO,EACR,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,QAAQ,EAER,WAAW,EACX,OAAO,EACR,MAAM,SAAS,CAAC;AAyIjB,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAusB/C"}
|
package/dist/advanced.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { CadenceMultiplier } from './defaults';
|
|
20
20
|
import { calculateTax } from './tax';
|
|
21
21
|
import { calculateWithdrawal, } from './withdrawal';
|
|
22
|
+
import { getLogger } from './logger';
|
|
22
23
|
// =============================================================================
|
|
23
24
|
// Helper Functions
|
|
24
25
|
// =============================================================================
|
|
@@ -140,6 +141,12 @@ const INCOME_CATEGORIES = new Set([
|
|
|
140
141
|
export function runAdvancedProjection(scenario, overrideReturns) {
|
|
141
142
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
|
|
142
143
|
const { current_age, retirement_age, end_age, inflation_pct, inflation_enabled, financial_items, liquidity_events, enable_taxes, effective_tax_rate_pct, tax_jurisdiction, tax_config, black_swan_enabled, black_swan_age, black_swan_loss_pct, desired_estate, } = scenario;
|
|
144
|
+
const log = getLogger();
|
|
145
|
+
log.info('Starting advanced projection', {
|
|
146
|
+
currentAge: current_age,
|
|
147
|
+
retirementAge: retirement_age,
|
|
148
|
+
itemCount: financial_items.length,
|
|
149
|
+
});
|
|
143
150
|
const items = financial_items.filter((item) => item.enabled);
|
|
144
151
|
// -------------------------------------------------------------------------
|
|
145
152
|
// Initialize state
|
|
@@ -489,7 +496,9 @@ export function runAdvancedProjection(scenario, overrideReturns) {
|
|
|
489
496
|
continue;
|
|
490
497
|
// Cap at available cash
|
|
491
498
|
if (contrib > Math.max(0, cashBalance)) {
|
|
492
|
-
|
|
499
|
+
const shortfall = contrib - Math.max(0, cashBalance);
|
|
500
|
+
log.warn('Contribution shortfall', { age, shortfall, requested: contrib, available: Math.max(0, cashBalance) });
|
|
501
|
+
shortfallContributions += shortfall;
|
|
493
502
|
contrib = Math.max(0, cashBalance);
|
|
494
503
|
}
|
|
495
504
|
cashBalance -= contrib;
|
|
@@ -552,6 +561,9 @@ export function runAdvancedProjection(scenario, overrideReturns) {
|
|
|
552
561
|
// 8. INSOLVENCY CHECK
|
|
553
562
|
// =====================================================================
|
|
554
563
|
const insolvency = cashBalance < 0;
|
|
564
|
+
if (insolvency) {
|
|
565
|
+
log.warn('Insolvency detected', { age, cashBalance });
|
|
566
|
+
}
|
|
555
567
|
if (insolvency && firstShortfallAge === null) {
|
|
556
568
|
firstShortfallAge = age;
|
|
557
569
|
}
|
|
@@ -722,6 +734,11 @@ export function runAdvancedProjection(scenario, overrideReturns) {
|
|
|
722
734
|
? Math.min(200, (totalActual / totalDesired) * 100)
|
|
723
735
|
: 100;
|
|
724
736
|
}
|
|
737
|
+
const insolvencyCount = timeline.filter((r) => r.insolvency).length;
|
|
738
|
+
log.info('Advanced projection complete', {
|
|
739
|
+
terminalReal,
|
|
740
|
+
insolvencyCount,
|
|
741
|
+
});
|
|
725
742
|
const metrics = {
|
|
726
743
|
terminal_nominal: terminalNominal,
|
|
727
744
|
terminal_real: terminalReal,
|
package/dist/backtest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backtest.d.ts","sourceRoot":"","sources":["../src/backtest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"backtest.d.ts","sourceRoot":"","sources":["../src/backtest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AA8KjD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACrE,cAAc,CAyEhB"}
|
package/dist/backtest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from './logger';
|
|
1
2
|
// ---------------------------------------------------------------------------
|
|
2
3
|
// Historical Backtest — Shiller Data (real total stock returns, 1871-2024)
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -177,6 +178,7 @@ const SHILLER_DATA = [
|
|
|
177
178
|
* @returns Array of BacktestPeriod results and the overall success rate (0-100).
|
|
178
179
|
*/
|
|
179
180
|
export function runHistoricalBacktest(scenario, projectionFn) {
|
|
181
|
+
const log = getLogger();
|
|
180
182
|
const span = scenario.end_age - scenario.current_age;
|
|
181
183
|
// Guard: span must be at least 1
|
|
182
184
|
if (span < 1) {
|
|
@@ -199,6 +201,7 @@ export function runHistoricalBacktest(scenario, projectionFn) {
|
|
|
199
201
|
for (const entry of SHILLER_DATA) {
|
|
200
202
|
returnsByYear.set(entry.year, entry.realStockReturn);
|
|
201
203
|
}
|
|
204
|
+
log.info('Starting backtest', { spanYears: span, windowCount });
|
|
202
205
|
let survivedCount = 0;
|
|
203
206
|
for (let i = 0; i < windowCount; i++) {
|
|
204
207
|
const startYear = firstYear + i;
|
|
@@ -231,5 +234,6 @@ export function runHistoricalBacktest(scenario, projectionFn) {
|
|
|
231
234
|
const successRate = periods.length > 0
|
|
232
235
|
? (survivedCount / periods.length) * 100
|
|
233
236
|
: 0;
|
|
237
|
+
log.info('Backtest complete', { successRate, periodsAnalyzed: periods.length });
|
|
234
238
|
return { periods, successRate };
|
|
235
239
|
}
|
package/dist/defaults.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type { Cadence, Scenario } from './types';
|
|
1
|
+
import type { Cadence, CurrencyCode, Scenario } from './types';
|
|
2
2
|
export declare const CadenceMultiplier: Record<Cadence, number>;
|
|
3
|
+
export interface CurrencyInfo {
|
|
4
|
+
code: CurrencyCode;
|
|
5
|
+
symbol: string;
|
|
6
|
+
decimals: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const CURRENCY_MAP: Record<CurrencyCode, CurrencyInfo>;
|
|
3
9
|
export declare const DEFAULT_SCENARIO: Scenario;
|
|
4
10
|
//# sourceMappingURL=defaults.d.ts.map
|
package/dist/defaults.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM/D,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAKrD,CAAC;AAMF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,YAAY,CAiB3D,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,QAqF9B,CAAC"}
|
package/dist/defaults.js
CHANGED
|
@@ -10,6 +10,24 @@ export const CadenceMultiplier = {
|
|
|
10
10
|
'Bi-Weekly': 26,
|
|
11
11
|
Weekly: 52,
|
|
12
12
|
};
|
|
13
|
+
export const CURRENCY_MAP = {
|
|
14
|
+
USD: { code: 'USD', symbol: '$', decimals: 2 },
|
|
15
|
+
EUR: { code: 'EUR', symbol: '€', decimals: 2 },
|
|
16
|
+
GBP: { code: 'GBP', symbol: '£', decimals: 2 },
|
|
17
|
+
CHF: { code: 'CHF', symbol: 'CHF', decimals: 2 },
|
|
18
|
+
HKD: { code: 'HKD', symbol: 'HK$', decimals: 2 },
|
|
19
|
+
SGD: { code: 'SGD', symbol: 'S$', decimals: 2 },
|
|
20
|
+
AED: { code: 'AED', symbol: 'د.إ', decimals: 2 },
|
|
21
|
+
JPY: { code: 'JPY', symbol: '¥', decimals: 0 },
|
|
22
|
+
CAD: { code: 'CAD', symbol: 'C$', decimals: 2 },
|
|
23
|
+
AUD: { code: 'AUD', symbol: 'A$', decimals: 2 },
|
|
24
|
+
NZD: { code: 'NZD', symbol: 'NZ$', decimals: 2 },
|
|
25
|
+
ZAR: { code: 'ZAR', symbol: 'R', decimals: 2 },
|
|
26
|
+
INR: { code: 'INR', symbol: '₹', decimals: 2 },
|
|
27
|
+
BRL: { code: 'BRL', symbol: 'R$', decimals: 2 },
|
|
28
|
+
MXN: { code: 'MXN', symbol: 'Mex$', decimals: 2 },
|
|
29
|
+
KYD: { code: 'KYD', symbol: 'CI$', decimals: 2 },
|
|
30
|
+
};
|
|
13
31
|
// ---------------------------------------------------------------------------
|
|
14
32
|
// Default Scenario
|
|
15
33
|
// ---------------------------------------------------------------------------
|
package/dist/heatmap.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heatmap.d.ts","sourceRoot":"","sources":["../src/heatmap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"heatmap.d.ts","sourceRoot":"","sources":["../src/heatmap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAOjD,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EACnD,OAAO,CAAC,EAAE,cAAc,GACvB,WAAW,EAAE,CA0Df"}
|
package/dist/heatmap.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from './logger';
|
|
1
2
|
/**
|
|
2
3
|
* Deep-clone a scenario.
|
|
3
4
|
*/
|
|
@@ -34,6 +35,8 @@ export function generateHeatmap(scenario, projectionFn, options) {
|
|
|
34
35
|
const effectiveSteps = Math.max(steps, 2);
|
|
35
36
|
const ageStep = (ageMax - ageMin) / (effectiveSteps - 1);
|
|
36
37
|
const spendStep = (spendMax - spendMin) / (effectiveSteps - 1);
|
|
38
|
+
const log = getLogger();
|
|
39
|
+
log.info('Generating heatmap', { rows: effectiveSteps, cols: effectiveSteps });
|
|
37
40
|
const cells = [];
|
|
38
41
|
const desiredEstate = (_k = scenario.desired_estate) !== null && _k !== void 0 ? _k : 0;
|
|
39
42
|
for (let ai = 0; ai < effectiveSteps; ai++) {
|
|
@@ -59,5 +62,7 @@ export function generateHeatmap(scenario, projectionFn, options) {
|
|
|
59
62
|
});
|
|
60
63
|
}
|
|
61
64
|
}
|
|
65
|
+
const viableCells = cells.filter((c) => c.viable).length;
|
|
66
|
+
log.info('Heatmap complete', { viableCells, totalCells: cells.length });
|
|
62
67
|
return cells;
|
|
63
68
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { Cadence, CurrencyCode, ContribStep, ProfitStep, RaiseStep, YieldStep, IncomeStep, LoanDraw, LumpRepayment, SpendingPhase, TaxConfig, IncomeSource, Asset, LiquidityEvent, FinancialItemCategory, FinancialItem, Scenario, TimelineRow, FanChartRow, Metrics, } from './types';
|
|
2
|
-
export { CadenceMultiplier, DEFAULT_SCENARIO } from './defaults';
|
|
2
|
+
export { CadenceMultiplier, CURRENCY_MAP, DEFAULT_SCENARIO, type CurrencyInfo } from './defaults';
|
|
3
3
|
export { runProjection } from './projection';
|
|
4
4
|
export { runAdvancedProjection } from './advanced';
|
|
5
5
|
export { runMonteCarloSimulation, type MCOptions, type MCResult, type ProjectionFn, } from './monte-carlo';
|
|
@@ -8,4 +8,5 @@ export { runHistoricalBacktest, type BacktestPeriod, type BacktestResult, } from
|
|
|
8
8
|
export { findEarliestRetirementAge, type OptimizerResult, type OptimizerOutput, type OptimizerOptions, } from './optimizer';
|
|
9
9
|
export { generateHeatmap, type HeatmapCell, type HeatmapOptions, } from './heatmap';
|
|
10
10
|
export { blendPortfolio, calculateEstateValue, type BlendedPortfolio, } from './portfolio';
|
|
11
|
+
export { getLogger, setLogLevel, setLogger, type Logger, type LogLevel } from './logger';
|
|
11
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,YAAY,EACV,OAAO,EACP,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,QAAQ,EACR,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,KAAK,EACL,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,OAAO,GACR,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,YAAY,EACV,OAAO,EACP,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,QAAQ,EACR,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,KAAK,EACL,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,OAAO,GACR,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAGlG,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGnD,OAAO,EACL,uBAAuB,EACvB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,sBAAsB,EACtB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,yBAAyB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Engine — barrel export
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
export { CadenceMultiplier, DEFAULT_SCENARIO } from './defaults';
|
|
4
|
+
export { CadenceMultiplier, CURRENCY_MAP, DEFAULT_SCENARIO } from './defaults';
|
|
5
5
|
// Deterministic projection (basic & advanced)
|
|
6
6
|
export { runProjection } from './projection';
|
|
7
7
|
export { runAdvancedProjection } from './advanced';
|
|
@@ -17,3 +17,5 @@ export { findEarliestRetirementAge, } from './optimizer';
|
|
|
17
17
|
export { generateHeatmap, } from './heatmap';
|
|
18
18
|
// Portfolio blending & estate value
|
|
19
19
|
export { blendPortfolio, calculateEstateValue, } from './portfolio';
|
|
20
|
+
// Logger utilities
|
|
21
|
+
export { getLogger, setLogLevel, setLogger } from './logger';
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
|
|
2
|
+
export interface Logger {
|
|
3
|
+
error(message: string, data?: Record<string, unknown>): void;
|
|
4
|
+
warn(message: string, data?: Record<string, unknown>): void;
|
|
5
|
+
info(message: string, data?: Record<string, unknown>): void;
|
|
6
|
+
debug(message: string, data?: Record<string, unknown>): void;
|
|
7
|
+
}
|
|
8
|
+
/** Get the active logger instance */
|
|
9
|
+
export declare function getLogger(): Logger;
|
|
10
|
+
/** Set the minimum log level (default: 'warn') */
|
|
11
|
+
export declare function setLogLevel(level: LogLevel): void;
|
|
12
|
+
/** Provide a custom logger implementation (e.g., for server-side or testing) */
|
|
13
|
+
export declare function setLogger(logger: Logger | null): void;
|
|
14
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtE,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9D;AAyCD,qCAAqC;AACrC,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAEjD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAErD"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Logger — structured logging with configurable levels
|
|
3
|
+
//
|
|
4
|
+
// Consumers can configure via setLogLevel() or provide a custom logger.
|
|
5
|
+
// Defaults to silent in production, 'warn' otherwise.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const LOG_PRIORITY = {
|
|
8
|
+
silent: 0,
|
|
9
|
+
error: 1,
|
|
10
|
+
warn: 2,
|
|
11
|
+
info: 3,
|
|
12
|
+
debug: 4,
|
|
13
|
+
};
|
|
14
|
+
let currentLevel = 'warn';
|
|
15
|
+
let customLogger = null;
|
|
16
|
+
function shouldLog(level) {
|
|
17
|
+
return LOG_PRIORITY[level] <= LOG_PRIORITY[currentLevel];
|
|
18
|
+
}
|
|
19
|
+
function formatMessage(level, message, data) {
|
|
20
|
+
const timestamp = new Date().toISOString();
|
|
21
|
+
const prefix = `[calculator-engine] ${timestamp} ${level.toUpperCase()}:`;
|
|
22
|
+
if (data && Object.keys(data).length > 0) {
|
|
23
|
+
return `${prefix} ${message} ${JSON.stringify(data)}`;
|
|
24
|
+
}
|
|
25
|
+
return `${prefix} ${message}`;
|
|
26
|
+
}
|
|
27
|
+
const defaultLogger = {
|
|
28
|
+
error(message, data) {
|
|
29
|
+
if (shouldLog('error'))
|
|
30
|
+
console.error(formatMessage('error', message, data));
|
|
31
|
+
},
|
|
32
|
+
warn(message, data) {
|
|
33
|
+
if (shouldLog('warn'))
|
|
34
|
+
console.warn(formatMessage('warn', message, data));
|
|
35
|
+
},
|
|
36
|
+
info(message, data) {
|
|
37
|
+
if (shouldLog('info'))
|
|
38
|
+
console.info(formatMessage('info', message, data));
|
|
39
|
+
},
|
|
40
|
+
debug(message, data) {
|
|
41
|
+
if (shouldLog('debug'))
|
|
42
|
+
console.debug(formatMessage('debug', message, data));
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
/** Get the active logger instance */
|
|
46
|
+
export function getLogger() {
|
|
47
|
+
return customLogger !== null && customLogger !== void 0 ? customLogger : defaultLogger;
|
|
48
|
+
}
|
|
49
|
+
/** Set the minimum log level (default: 'warn') */
|
|
50
|
+
export function setLogLevel(level) {
|
|
51
|
+
currentLevel = level;
|
|
52
|
+
}
|
|
53
|
+
/** Provide a custom logger implementation (e.g., for server-side or testing) */
|
|
54
|
+
export function setLogger(logger) {
|
|
55
|
+
customLogger = logger;
|
|
56
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monte-carlo.d.ts","sourceRoot":"","sources":["../src/monte-carlo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"monte-carlo.d.ts","sourceRoot":"","sources":["../src/monte-carlo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAO3E,MAAM,MAAM,YAAY,GAAG,CACzB,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,KACvB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,wBAAwB,EAAE,MAAM,CAAC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAMD,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAS;gBAEV,IAAI,GAAE,MAAW;IAI7B,uEAAuE;IACvE,IAAI,IAAI,MAAM;IAQd,yEAAyE;IACzE,QAAQ,IAAI,MAAM;CAUnB;AAMD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,GAAG,QAAQ,GACpC,MAAM,CAuBR;AAMD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E;AAMD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,GAAE,SAAc,GACtB,QAAQ,CA+IV"}
|
package/dist/monte-carlo.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* a caller-provided projection function, keeping MC fully decoupled from the
|
|
7
7
|
* projection engine.
|
|
8
8
|
*/
|
|
9
|
+
import { getLogger } from './logger';
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// SeededRNG — Deterministic PRNG (mulberry32)
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -88,6 +89,8 @@ export function runMonteCarloSimulation(scenario, projectionFn, options = {}) {
|
|
|
88
89
|
if (runs < 100 || runs > 10000) {
|
|
89
90
|
throw new Error(`mc_runs must be 0 (disabled) or between 100 and 10000. Got: ${runs}`);
|
|
90
91
|
}
|
|
92
|
+
const log = getLogger();
|
|
93
|
+
log.info('Starting Monte Carlo', { runs, seed, distribution: scenario.return_distribution });
|
|
91
94
|
const rng = new SeededRNG(seed);
|
|
92
95
|
const startTime = Date.now();
|
|
93
96
|
const numYears = scenario.end_age - scenario.current_age;
|
|
@@ -108,6 +111,7 @@ export function runMonteCarloSimulation(scenario, projectionFn, options = {}) {
|
|
|
108
111
|
const elapsed = Date.now() - startTime;
|
|
109
112
|
if (elapsed > budgetMs) {
|
|
110
113
|
truncated = true;
|
|
114
|
+
log.warn('Monte Carlo truncated due to budget', { runsCompleted: run, elapsed, budgetMs });
|
|
111
115
|
break;
|
|
112
116
|
}
|
|
113
117
|
}
|
|
@@ -165,6 +169,12 @@ export function runMonteCarloSimulation(scenario, projectionFn, options = {}) {
|
|
|
165
169
|
// Compute probability of no shortfall
|
|
166
170
|
// -----------------------------------------------------------------------
|
|
167
171
|
const probabilityNoShortfall = runsCompleted > 0 ? (noShortfallCount / runsCompleted) * 100 : 0;
|
|
172
|
+
log.info('Monte Carlo complete', {
|
|
173
|
+
runsCompleted,
|
|
174
|
+
successProbability: probabilityNoShortfall,
|
|
175
|
+
medianTerminal: p50Terminal,
|
|
176
|
+
truncated,
|
|
177
|
+
});
|
|
168
178
|
return {
|
|
169
179
|
probability_no_shortfall: probabilityNoShortfall,
|
|
170
180
|
median_terminal: p50Terminal,
|
package/dist/optimizer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimizer.d.ts","sourceRoot":"","sources":["../src/optimizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"optimizer.d.ts","sourceRoot":"","sources":["../src/optimizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAOjD,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA4GD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EACnD,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,wBAAwB,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe,CAiEjB"}
|
package/dist/optimizer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from './logger';
|
|
1
2
|
/**
|
|
2
3
|
* Deep-clone a scenario.
|
|
3
4
|
*/
|
|
@@ -102,11 +103,16 @@ function findMinContribution(scenario, retirementAge, projectionFn, mcFn, mcThre
|
|
|
102
103
|
*/
|
|
103
104
|
export function findEarliestRetirementAge(scenario, projectionFn, mcFn, options) {
|
|
104
105
|
var _a;
|
|
106
|
+
const log = getLogger();
|
|
105
107
|
const mcThreshold = (_a = options === null || options === void 0 ? void 0 : options.mcThreshold) !== null && _a !== void 0 ? _a : 90;
|
|
106
108
|
const results = [];
|
|
107
109
|
let earliestViableAge = null;
|
|
108
110
|
const startAge = scenario.current_age + 1;
|
|
109
111
|
const endAge = scenario.end_age - 1;
|
|
112
|
+
log.info('Starting optimizer', {
|
|
113
|
+
searchRange: [startAge, endAge],
|
|
114
|
+
mcEnabled: mcFn != null,
|
|
115
|
+
});
|
|
110
116
|
// Budget guard: track wall-clock time (50s limit per CONTRACT-005)
|
|
111
117
|
const startTime = Date.now();
|
|
112
118
|
const BUDGET_MS = 50000;
|
|
@@ -116,6 +122,12 @@ export function findEarliestRetirementAge(scenario, projectionFn, mcFn, options)
|
|
|
116
122
|
break;
|
|
117
123
|
}
|
|
118
124
|
const { result, viable } = isViable(scenario, age, projectionFn, mcFn, mcThreshold);
|
|
125
|
+
log.debug('Optimizer candidate', {
|
|
126
|
+
age,
|
|
127
|
+
terminalReal: result.terminalReal,
|
|
128
|
+
survived: result.survived,
|
|
129
|
+
viable,
|
|
130
|
+
});
|
|
119
131
|
results.push(result);
|
|
120
132
|
if (viable && earliestViableAge === null) {
|
|
121
133
|
earliestViableAge = age;
|
|
@@ -126,6 +138,7 @@ export function findEarliestRetirementAge(scenario, projectionFn, mcFn, options)
|
|
|
126
138
|
if (earliestViableAge !== null) {
|
|
127
139
|
minContribution = findMinContribution(scenario, earliestViableAge, projectionFn, mcFn, mcThreshold);
|
|
128
140
|
}
|
|
141
|
+
log.info('Optimizer complete', { earliestViableAge, minContribution });
|
|
129
142
|
return {
|
|
130
143
|
results,
|
|
131
144
|
earliestViableAge,
|
package/dist/portfolio.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portfolio.d.ts","sourceRoot":"","sources":["../src/portfolio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"portfolio.d.ts","sourceRoot":"","sources":["../src/portfolio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAOpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,gBAAgB,CA6DhE;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,iBAAiB,EAAE,MAAM,EACzB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,aAAa,EAAE,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAcR"}
|
package/dist/portfolio.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from './logger';
|
|
1
2
|
/**
|
|
2
3
|
* Computes weighted-average return, fee, perf-fee, and liquid percentage
|
|
3
4
|
* across a set of basic-mode assets.
|
|
@@ -44,6 +45,12 @@ export function blendPortfolio(assets) {
|
|
|
44
45
|
.filter((a) => a.is_liquid)
|
|
45
46
|
.reduce((sum, a) => sum + a.current_value, 0);
|
|
46
47
|
const liquidPct = (liquidTotal / totalValue) * 100;
|
|
48
|
+
const log = getLogger();
|
|
49
|
+
log.debug('Portfolio blended', {
|
|
50
|
+
assetCount: enabled.length,
|
|
51
|
+
blendedReturn,
|
|
52
|
+
liquidPct,
|
|
53
|
+
});
|
|
47
54
|
return {
|
|
48
55
|
totalValue,
|
|
49
56
|
blendedReturn,
|
package/dist/projection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAgB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAgB,MAAM,SAAS,CAAC;AAqE5E,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CA+b/C"}
|
package/dist/projection.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { CadenceMultiplier } from './defaults';
|
|
12
12
|
import { calculateTax, getRMDAmount, calculateRothConversion } from './tax';
|
|
13
13
|
import { calculateWithdrawal, NEAR_ZERO_THRESHOLD, } from './withdrawal';
|
|
14
|
+
import { getLogger } from './logger';
|
|
14
15
|
// =============================================================================
|
|
15
16
|
// Helpers
|
|
16
17
|
// =============================================================================
|
|
@@ -57,6 +58,13 @@ export function runProjection(scenario, overrideReturns) {
|
|
|
57
58
|
const { current_age, retirement_age, end_age, current_balance, contrib_amount, contrib_cadence, contrib_increase_pct, nominal_return_pct, inflation_pct, inflation_enabled, fee_pct, perf_fee_pct, enable_taxes, effective_tax_rate_pct, tax_jurisdiction, tax_config, tax_deferred_pct, planning_mode, partner_current_age, partner_income_sources, income_sources, liquidity_events,
|
|
58
59
|
// assets — not used in basic-mode projection (estate_pct is advanced-mode only)
|
|
59
60
|
black_swan_enabled, black_swan_age, black_swan_loss_pct, spending_phases, withdrawal_strategy, } = scenario;
|
|
61
|
+
const log = getLogger();
|
|
62
|
+
log.info('Starting projection', {
|
|
63
|
+
currentAge: current_age,
|
|
64
|
+
retirementAge: retirement_age,
|
|
65
|
+
endAge: end_age,
|
|
66
|
+
detailMode: scenario.detail_mode,
|
|
67
|
+
});
|
|
60
68
|
const timeline = [];
|
|
61
69
|
// Running state
|
|
62
70
|
let prevEndBalance = current_balance;
|
|
@@ -271,6 +279,7 @@ export function runProjection(scenario, overrideReturns) {
|
|
|
271
279
|
if (black_swan_enabled && age === black_swan_age) {
|
|
272
280
|
// Override growth with the loss
|
|
273
281
|
growth = -(startBalance * (black_swan_loss_pct / 100));
|
|
282
|
+
log.warn('Black swan event triggered', { age, lossPct: black_swan_loss_pct });
|
|
274
283
|
}
|
|
275
284
|
else {
|
|
276
285
|
// Mid-year cash flow assumption:
|
|
@@ -286,6 +295,7 @@ export function runProjection(scenario, overrideReturns) {
|
|
|
286
295
|
// Track shortfall before flooring
|
|
287
296
|
if (endBalance < 0 && age >= retirement_age && firstShortfallAge === null) {
|
|
288
297
|
firstShortfallAge = age;
|
|
298
|
+
log.warn('First shortfall detected', { age, endBalance });
|
|
289
299
|
}
|
|
290
300
|
// Near-zero depletion threshold (edge case: asymptotic drain)
|
|
291
301
|
if (endBalance >= 0 &&
|
|
@@ -293,6 +303,7 @@ export function runProjection(scenario, overrideReturns) {
|
|
|
293
303
|
age >= retirement_age &&
|
|
294
304
|
firstShortfallAge === null) {
|
|
295
305
|
firstShortfallAge = age;
|
|
306
|
+
log.warn('Near-zero depletion shortfall', { age, endBalance });
|
|
296
307
|
}
|
|
297
308
|
// Floor at 0 in basic mode
|
|
298
309
|
endBalance = Math.max(endBalance, 0);
|
|
@@ -330,6 +341,7 @@ export function runProjection(scenario, overrideReturns) {
|
|
|
330
341
|
shortfall_contributions: 0,
|
|
331
342
|
shortfall_withdrawals: shortfallWithdrawals,
|
|
332
343
|
};
|
|
344
|
+
log.debug('Year end', { age, end_balance: endBalance });
|
|
333
345
|
timeline.push(row);
|
|
334
346
|
// Update running state for next year
|
|
335
347
|
prevEndBalance = endBalance;
|
|
@@ -378,5 +390,11 @@ export function runProjection(scenario, overrideReturns) {
|
|
|
378
390
|
total_taxes: totalTaxes,
|
|
379
391
|
estate_value: estateValue,
|
|
380
392
|
};
|
|
393
|
+
log.info('Projection complete', {
|
|
394
|
+
terminalReal,
|
|
395
|
+
terminalNominal,
|
|
396
|
+
shortfallAge: firstShortfallAge,
|
|
397
|
+
years: timeline.length,
|
|
398
|
+
});
|
|
381
399
|
return { timeline, metrics };
|
|
382
400
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sensitivity.d.ts","sourceRoot":"","sources":["../src/sensitivity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"sensitivity.d.ts","sourceRoot":"","sources":["../src/sensitivity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAOjD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAkCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAClD,iBAAiB,EAAE,CA6ErB"}
|
package/dist/sensitivity.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from './logger';
|
|
1
2
|
const PARAMETERS = [
|
|
2
3
|
{ name: 'nominal_return_pct', label: 'Return +/-1%', delta: 1, deltaIsPct: false },
|
|
3
4
|
{ name: 'inflation_pct', label: 'Inflation +/-0.5%', delta: 0.5, deltaIsPct: false },
|
|
@@ -36,6 +37,9 @@ function clamp(value, min, max) {
|
|
|
36
37
|
* - withdrawal_pct delta is skipped when withdrawal_strategy is Age-Banded
|
|
37
38
|
*/
|
|
38
39
|
export function runSensitivityAnalysis(scenario, projectionFn) {
|
|
40
|
+
var _a;
|
|
41
|
+
const log = getLogger();
|
|
42
|
+
log.info('Starting sensitivity analysis', { parameterCount: PARAMETERS.length });
|
|
39
43
|
const factors = [];
|
|
40
44
|
for (const param of PARAMETERS) {
|
|
41
45
|
// Skip withdrawal_pct when strategy is Age-Banded (not applicable)
|
|
@@ -76,6 +80,13 @@ export function runSensitivityAnalysis(scenario, projectionFn) {
|
|
|
76
80
|
const highScenario = cloneScenario(scenario);
|
|
77
81
|
highScenario[param.name] = highValue;
|
|
78
82
|
const highResult = projectionFn(highScenario);
|
|
83
|
+
const spread = Math.abs(highResult.metrics.terminal_real - lowResult.metrics.terminal_real);
|
|
84
|
+
log.debug('Sensitivity parameter result', {
|
|
85
|
+
name: param.name,
|
|
86
|
+
lowTerminal: lowResult.metrics.terminal_real,
|
|
87
|
+
highTerminal: highResult.metrics.terminal_real,
|
|
88
|
+
spread,
|
|
89
|
+
});
|
|
79
90
|
factors.push({
|
|
80
91
|
name: param.name,
|
|
81
92
|
label: param.label,
|
|
@@ -83,10 +94,11 @@ export function runSensitivityAnalysis(scenario, projectionFn) {
|
|
|
83
94
|
highValue,
|
|
84
95
|
lowTerminal: lowResult.metrics.terminal_real,
|
|
85
96
|
highTerminal: highResult.metrics.terminal_real,
|
|
86
|
-
spread
|
|
97
|
+
spread,
|
|
87
98
|
});
|
|
88
99
|
}
|
|
89
100
|
// Sort by spread descending (largest impact first)
|
|
90
101
|
factors.sort((a, b) => b.spread - a.spread);
|
|
102
|
+
log.info('Sensitivity complete', { topFactor: (_a = factors[0]) === null || _a === void 0 ? void 0 : _a.name });
|
|
91
103
|
return factors;
|
|
92
104
|
}
|
package/dist/tax.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tax.d.ts","sourceRoot":"","sources":["../src/tax.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"tax.d.ts","sourceRoot":"","sources":["../src/tax.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AA6JzC;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,GACnB,MAAM,CA4BR;AAMD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,SAAS,EAAE,MAAM,GAChB,MAAM,CAUR;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GAChB,MAAM,CAMR"}
|
package/dist/tax.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Also includes RMD (Required Minimum Distribution) and Roth conversion logic.
|
|
8
8
|
*/
|
|
9
|
+
import { getLogger } from './logger';
|
|
9
10
|
const US_STANDARD_DEDUCTION_SINGLE = 15000;
|
|
10
11
|
const US_STANDARD_DEDUCTION_MFJ = 30000;
|
|
11
12
|
const US_BRACKETS_2025_SINGLE = [
|
|
@@ -136,17 +137,25 @@ function calculateUKTax(grossIncome) {
|
|
|
136
137
|
export function calculateTax(taxableIncome, config, jurisdiction) {
|
|
137
138
|
if (taxableIncome <= 0)
|
|
138
139
|
return 0;
|
|
140
|
+
let taxAmount;
|
|
139
141
|
switch (jurisdiction) {
|
|
140
142
|
case 'Cayman Islands':
|
|
141
|
-
|
|
143
|
+
taxAmount = 0;
|
|
144
|
+
break;
|
|
142
145
|
case 'US':
|
|
143
|
-
|
|
146
|
+
taxAmount = calculateUSTax(taxableIncome, config.filing_status);
|
|
147
|
+
break;
|
|
144
148
|
case 'UK':
|
|
145
|
-
|
|
149
|
+
taxAmount = calculateUKTax(taxableIncome);
|
|
150
|
+
break;
|
|
146
151
|
case 'Custom':
|
|
147
152
|
default:
|
|
148
|
-
|
|
153
|
+
taxAmount = taxableIncome * (config.flat_rate_pct / 100);
|
|
154
|
+
break;
|
|
149
155
|
}
|
|
156
|
+
const log = getLogger();
|
|
157
|
+
log.debug('Tax calculated', { jurisdiction, taxableIncome, taxAmount });
|
|
158
|
+
return taxAmount;
|
|
150
159
|
}
|
|
151
160
|
// =============================================================================
|
|
152
161
|
// RMD (Required Minimum Distributions)
|
package/dist/withdrawal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withdrawal.d.ts","sourceRoot":"","sources":["../src/withdrawal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"withdrawal.d.ts","sourceRoot":"","sources":["../src/withdrawal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAOvD,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAMvC,MAAM,WAAW,OAAO;IACtB,sEAAsE;IACtE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,eAAe,EAAE,MAAM,CAAC;CACzB;AAMD,MAAM,WAAW,wBAAwB;IACvC,gBAAgB,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IAChD,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IACtD,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IAEjB,8EAA8E;IAC9E,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAExB,4EAA4E;IAC5E,cAAc,EAAE,wBAAwB,CAAC;IAEzC,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,8BAA8B,EAAE,MAAM,CAAC;CACxC;AAMD,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,wBAAwB,GAC/B,MAAM,CA2BR;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,6BAA6B,GACpC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAkF1C;AAMD;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,yBAAyB,GAChC,MAAM,CA2BR;AAMD,0FAA0F;AAC1F,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,QAAQ,EAAE,QAAQ,CAAC;IAEnB,gCAAgC;IAChC,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;QACxB,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;KACzB,CAAC;CACH;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAC/B,gBAAgB,CA6FlB"}
|
package/dist/withdrawal.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - GK oscillation: bounded by floor/ceiling — no special handling needed
|
|
12
12
|
* - RMD override: caller responsibility (if RMD > withdrawal, caller uses RMD)
|
|
13
13
|
*/
|
|
14
|
+
import { getLogger } from './logger';
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Constants
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -150,8 +151,8 @@ export function calculateAgeBandedWithdrawal(params) {
|
|
|
150
151
|
const phase = spendingPhases.find((p) => age >= p.start_age && age <= p.end_age);
|
|
151
152
|
if (!phase) {
|
|
152
153
|
// Gap in spending phases — no withdrawal for this year
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
const log = getLogger();
|
|
155
|
+
log.warn('Age-Banded gap: no spending phase covers age', { age });
|
|
155
156
|
return 0;
|
|
156
157
|
}
|
|
157
158
|
let withdrawal;
|
|
@@ -230,6 +231,8 @@ export function calculateWithdrawal(scenario, state) {
|
|
|
230
231
|
throw new Error(`Unknown withdrawal strategy: ${_exhaustive}`);
|
|
231
232
|
}
|
|
232
233
|
}
|
|
234
|
+
const log = getLogger();
|
|
235
|
+
log.debug('Withdrawal calculated', { strategy: withdrawal_strategy, amount: withdrawal });
|
|
233
236
|
// Near-zero threshold: if balance after withdrawal would be below $100, treat as depleted
|
|
234
237
|
const balanceAfterWithdrawal = availableBalance - withdrawal;
|
|
235
238
|
const effectivelyDepleted = balanceAfterWithdrawal >= 0 && balanceAfterWithdrawal < NEAR_ZERO_THRESHOLD;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robotixai/calculator-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Financial retirement projection engine with Monte Carlo simulation, multi-jurisdiction tax, and withdrawal strategies",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "./dist/index.js",
|