fundamental-js 1.0.0 → 1.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.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # fundamental-js
2
+
3
+ A comprehensive, zero-dependency TypeScript financial calculator library for investing, loans, and personal finance.
4
+
5
+ All functions are **pure**, **deterministic**, and work in both **Node.js** and **browser** environments. Rates are expressed as decimals (0.12 = 12%).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install fundamental-js
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```ts
16
+ import {
17
+ emi,
18
+ sipFutureValue,
19
+ stepUpSipFutureValue,
20
+ swpPlan,
21
+ xirr,
22
+ } from "fundamental-js";
23
+
24
+ // EMI for a ₹25L loan at 8.5% for 20 years
25
+ const loan = emi(2500000, 0.085, 240);
26
+ console.log(loan);
27
+ // { emi: 21698.69, totalPayment: 5207685.73, totalInterest: 2707685.73 }
28
+
29
+ // SIP: ₹25,000/month at 12% for 15 years
30
+ const sip = sipFutureValue({
31
+ monthlyInvestment: 25000,
32
+ annualRate: 0.12,
33
+ months: 180,
34
+ });
35
+ console.log(sip);
36
+ // { futureValue: 12649741.45, totalInvested: 4500000, estimatedGains: 8149741.45 }
37
+
38
+ // Step-up SIP: ₹25,000/month, 10% annual step-up
39
+ const stepUp = stepUpSipFutureValue({
40
+ monthlyInvestment: 25000,
41
+ annualRate: 0.12,
42
+ months: 180,
43
+ stepUpPercentAnnual: 10,
44
+ });
45
+ console.log(stepUp);
46
+
47
+ // SWP: ₹1Cr corpus, ₹50K/month withdrawal at 8%
48
+ const withdrawal = swpPlan({
49
+ initialCorpus: 10000000,
50
+ monthlyWithdrawal: 50000,
51
+ annualRate: 0.08,
52
+ months: 300,
53
+ });
54
+ console.log(withdrawal.endingCorpus, withdrawal.totalWithdrawn);
55
+
56
+ // XIRR: Irregular cashflows with dates
57
+ const result = xirr([
58
+ { date: new Date("2020-01-01"), amount: -100000 },
59
+ { date: new Date("2020-06-15"), amount: 5000 },
60
+ { date: new Date("2021-01-01"), amount: 115000 },
61
+ ]);
62
+ console.log(result); // ~0.1975 (19.75%)
63
+ ```
64
+
65
+ ## API Reference
66
+
67
+ ### Time Value of Money (Excel-compatible)
68
+
69
+ | Function | Description |
70
+ |----------|-------------|
71
+ | `pv(rate, nper, pmt, fv?, type?)` | Present Value |
72
+ | `fv(rate, nper, pmt, pv?, type?)` | Future Value |
73
+ | `pmt(rate, nper, pv, fv?, type?)` | Payment per period |
74
+ | `nper(rate, pmt, pv, fv?, type?)` | Number of periods |
75
+ | `rateCalc(nper, pmt, pv, fv?, type?, guess?)` | Solve for interest rate (iterative) |
76
+ | `npv(rate, cashflows)` | Net Present Value |
77
+ | `irr(cashflows, guess?)` | Internal Rate of Return (Newton-Raphson + bisection fallback) |
78
+ | `xnpv(rate, cashflows, dayCount?)` | NPV with irregular dates |
79
+ | `xirr(cashflows, guess?, dayCount?)` | IRR with irregular dates |
80
+
81
+ **Parameters:**
82
+ - `rate` — periodic interest rate as decimal
83
+ - `nper` — total number of periods
84
+ - `pmt` — payment per period
85
+ - `type` — 0 = end of period (default), 1 = beginning of period
86
+ - `cashflows` for xnpv/xirr: `{ date: Date, amount: number }[]`
87
+ - `dayCount` — `"ACT/365"` (default) or `"ACT/360"`
88
+
89
+ ### Returns Analysis
90
+
91
+ | Function | Description |
92
+ |----------|-------------|
93
+ | `absoluteReturn(beginValue, endValue)` | Simple return as a decimal |
94
+ | `cagr(beginValue, endValue, years)` | Compound Annual Growth Rate |
95
+ | `annualizedReturn(beginValue, endValue, days)` | Annualized return using ACT/365 |
96
+ | `trailingReturn(series, windowDays)` | Trailing return over a rolling window |
97
+
98
+ **Parameters:**
99
+ - `series` for trailingReturn: `{ date: Date, value: number }[]`
100
+ - `windowDays` — lookback window in days
101
+
102
+ ### SIP / Lumpsum / SWP
103
+
104
+ | Function | Description |
105
+ |----------|-------------|
106
+ | `sipFutureValue(params)` | SIP future value with optional step-up |
107
+ | `stepUpSipFutureValue(params)` | SIP with required annual step-up |
108
+ | `lumpsumFutureValue(principal, annualRate, years)` | One-time investment growth |
109
+ | `swpPlan(params)` | Systematic Withdrawal Plan with schedule |
110
+ | `inflationAdjustedValue(value, inflationRate, years)` | Purchasing power after inflation |
111
+ | `realReturn(nominalRate, inflationRate)` | Fisher equation real return |
112
+
113
+ **sipFutureValue params:**
114
+ ```ts
115
+ {
116
+ monthlyInvestment: number,
117
+ annualRate: number, // decimal (0.12 = 12%)
118
+ months: number,
119
+ stepUpPercentAnnual?: number, // e.g. 10 for 10%
120
+ investmentAt?: "begin" | "end"
121
+ }
122
+ ```
123
+ Returns: `{ futureValue, totalInvested, estimatedGains }`
124
+
125
+ **swpPlan params:**
126
+ ```ts
127
+ {
128
+ initialCorpus: number,
129
+ monthlyWithdrawal: number,
130
+ annualRate: number,
131
+ months: number,
132
+ inflationRateAnnual?: number, // e.g. 6 for 6%
133
+ withdrawalAt?: "begin" | "end"
134
+ }
135
+ ```
136
+ Returns: `{ endingCorpus, totalWithdrawn, schedule[] }`
137
+
138
+ ### Goal Planning
139
+
140
+ | Function | Description |
141
+ |----------|-------------|
142
+ | `requiredMonthlyInvestmentForGoal(params)` | Required monthly SIP for a target amount |
143
+ | `requiredLumpsumForGoal(goal, annualRate, years)` | Required lumpsum for a target amount |
144
+
145
+ **requiredMonthlyInvestmentForGoal params:**
146
+ ```ts
147
+ {
148
+ goalAmountFuture: number,
149
+ annualRate: number,
150
+ months: number,
151
+ stepUpPercentAnnual?: number,
152
+ investmentAt?: "begin" | "end"
153
+ }
154
+ ```
155
+ Returns: `{ requiredMonthlyInvestment, assumptions }`
156
+
157
+ ### Loans
158
+
159
+ | Function | Description |
160
+ |----------|-------------|
161
+ | `emi(principal, annualRate, months)` | Equated Monthly Installment |
162
+ | `amortizationSchedule(params)` | Full loan schedule with extra payments |
163
+ | `prepaymentImpact(params)` | Compare original vs prepaid loan savings |
164
+
165
+ **emi** returns: `{ emi, totalPayment, totalInterest }`
166
+
167
+ **amortizationSchedule params:**
168
+ ```ts
169
+ {
170
+ principal: number,
171
+ annualRate: number,
172
+ months: number,
173
+ extraPaymentMonthly?: number,
174
+ extraPayments?: { month: number, amount: number }[]
175
+ }
176
+ ```
177
+ Returns: `{ emi, schedule[], totalInterest, totalPaid, payoffMonth }`
178
+
179
+ **prepaymentImpact params:**
180
+ ```ts
181
+ {
182
+ principal: number,
183
+ annualRate: number,
184
+ months: number,
185
+ prepayments: { month: number, amount: number }[],
186
+ mode: "reduceTenure" | "reduceEmi"
187
+ }
188
+ ```
189
+ Returns: `{ original, new, savings: { interestSaved, monthsSaved } }`
190
+
191
+ ### Risk Metrics
192
+
193
+ | Function | Description |
194
+ |----------|-------------|
195
+ | `volatility(returns, periodsPerYear?)` | Annualized volatility (sample stdev * sqrt(periods)) |
196
+ | `sharpe(returns, riskFreeRate?, periodsPerYear?)` | Sharpe Ratio |
197
+ | `sortino(returns, riskFreeRate?, periodsPerYear?)` | Sortino Ratio (downside deviation only) |
198
+ | `maxDrawdown(values)` | Maximum drawdown with peak/trough indices |
199
+
200
+ **Parameters:**
201
+ - `returns` — array of periodic returns as decimals
202
+ - `values` — array of portfolio values
203
+ - `periodsPerYear` — defaults to 252 (daily trading)
204
+ - `riskFreeRate` — annual risk-free rate as decimal
205
+
206
+ ### Portfolio Helpers
207
+
208
+ | Function | Description |
209
+ |----------|-------------|
210
+ | `weightedReturn(returns, weights)` | Weighted average return |
211
+ | `rebalance(targetWeights, currentValues)` | Calculate trades needed to rebalance |
212
+
213
+ **rebalance** returns: `{ trades, newValues, total }` — positive trade = buy, negative = sell
214
+
215
+ ### Utilities
216
+
217
+ | Function | Description |
218
+ |----------|-------------|
219
+ | `safeNum(value, fallback?)` | Returns fallback if value is NaN/Infinity/null/undefined |
220
+
221
+ ## Conventions
222
+
223
+ - **Rates as decimals**: 12% = `0.12`, not `12`
224
+ - **Step-up as percentage**: 10% step-up = `10`, not `0.10` (matches common Indian finance convention)
225
+ - **Inflation as percentage** in SWP: `inflationRateAnnual: 6` means 6%
226
+ - **Day count**: XNPV/XIRR default to ACT/365; pass `"ACT/360"` to override
227
+ - **Period type**: `0` = end-of-period (ordinary annuity), `1` = beginning-of-period (annuity due)
228
+
229
+ ## Error Handling
230
+
231
+ Functions return safe defaults (0, empty arrays) for invalid inputs instead of throwing, making them safe for use in reactive UIs. The `safeNum` utility guards against NaN/Infinity.
232
+
233
+ ## License
234
+
235
+ MIT
package/dist/index.cjs CHANGED
@@ -30,9 +30,12 @@ __export(index_exports, {
30
30
  irr: () => irr,
31
31
  lumpsumFutureValue: () => lumpsumFutureValue,
32
32
  maxDrawdown: () => maxDrawdown,
33
+ nper: () => nper,
33
34
  npv: () => npv,
35
+ pmt: () => pmt,
34
36
  prepaymentImpact: () => prepaymentImpact,
35
37
  pv: () => pv,
38
+ rateCalc: () => rateCalc,
36
39
  realReturn: () => realReturn,
37
40
  rebalance: () => rebalance,
38
41
  requiredLumpsumForGoal: () => requiredLumpsumForGoal,
@@ -41,9 +44,13 @@ __export(index_exports, {
41
44
  sharpe: () => sharpe,
42
45
  sipFutureValue: () => sipFutureValue,
43
46
  sortino: () => sortino,
47
+ stepUpSipFutureValue: () => stepUpSipFutureValue,
44
48
  swpPlan: () => swpPlan,
49
+ trailingReturn: () => trailingReturn,
45
50
  volatility: () => volatility,
46
- weightedReturn: () => weightedReturn
51
+ weightedReturn: () => weightedReturn,
52
+ xirr: () => xirr,
53
+ xnpv: () => xnpv
47
54
  });
48
55
  module.exports = __toCommonJS(index_exports);
49
56
  function safeNum(v, fallback = 0) {
@@ -261,15 +268,126 @@ function annualizedReturn(beginValue, endValue, days) {
261
268
  if (beginValue <= 0 || days <= 0) return 0;
262
269
  return Math.pow(endValue / beginValue, 365 / days) - 1;
263
270
  }
264
- function pv(rate, nper, pmt, fv2 = 0, type = 0) {
265
- if (rate === 0) return -(pmt * nper + fv2);
266
- const factor = Math.pow(1 + rate, nper);
267
- return -(pmt * (1 + rate * type) * ((factor - 1) / rate) + fv2) / factor;
271
+ function pv(rate, nper2, pmt2, fv2 = 0, type = 0) {
272
+ if (rate === 0) return -(pmt2 * nper2 + fv2);
273
+ const factor = Math.pow(1 + rate, nper2);
274
+ return -(pmt2 * (1 + rate * type) * ((factor - 1) / rate) + fv2) / factor;
268
275
  }
269
- function fv(rate, nper, pmt, pvVal = 0, type = 0) {
270
- if (rate === 0) return -(pvVal + pmt * nper);
271
- const factor = Math.pow(1 + rate, nper);
272
- return -(pvVal * factor + pmt * (1 + rate * type) * ((factor - 1) / rate));
276
+ function fv(rate, nper2, pmt2, pvVal = 0, type = 0) {
277
+ if (rate === 0) return -(pvVal + pmt2 * nper2);
278
+ const factor = Math.pow(1 + rate, nper2);
279
+ return -(pvVal * factor + pmt2 * (1 + rate * type) * ((factor - 1) / rate));
280
+ }
281
+ function pmt(rate, nper2, pvVal, fvVal = 0, type = 0) {
282
+ if (nper2 <= 0) return 0;
283
+ if (rate === 0) return -(pvVal + fvVal) / nper2;
284
+ const factor = Math.pow(1 + rate, nper2);
285
+ return -(pvVal * factor + fvVal) * rate / ((1 + rate * type) * (factor - 1));
286
+ }
287
+ function nper(rate, pmtVal, pvVal, fvVal = 0, type = 0) {
288
+ if (rate === 0) {
289
+ if (pmtVal === 0) return 0;
290
+ return -(pvVal + fvVal) / pmtVal;
291
+ }
292
+ const pmtAdj = pmtVal * (1 + rate * type);
293
+ const num = Math.log((pmtAdj - fvVal * rate) / (pmtAdj + pvVal * rate));
294
+ const den = Math.log(1 + rate);
295
+ if (den === 0) return 0;
296
+ return num / den;
297
+ }
298
+ function rateCalc(nperVal, pmtVal, pvVal, fvVal = 0, type = 0, guess = 0.1) {
299
+ if (nperVal <= 0) return 0;
300
+ let r = guess;
301
+ const maxIter = 1e3;
302
+ const tol = 1e-10;
303
+ for (let i = 0; i < maxIter; i++) {
304
+ const factor = Math.pow(1 + r, nperVal);
305
+ const pmtAdj = pmtVal * (1 + r * type);
306
+ const f = pvVal * factor + pmtAdj * ((factor - 1) / r) + fvVal;
307
+ const dfactor = nperVal * Math.pow(1 + r, nperVal - 1);
308
+ const dpmtAdj = pmtVal * type;
309
+ const df = pvVal * dfactor + dpmtAdj * ((factor - 1) / r) + pmtAdj * ((dfactor * r - (factor - 1)) / (r * r));
310
+ if (Math.abs(df) < tol) break;
311
+ const newR = r - f / df;
312
+ if (Math.abs(newR - r) < tol) return newR;
313
+ r = newR;
314
+ }
315
+ return r;
316
+ }
317
+ function yearFrac(d1, d2, dayCount = "ACT/365") {
318
+ const ms = d2.getTime() - d1.getTime();
319
+ const days = ms / (1e3 * 60 * 60 * 24);
320
+ return dayCount === "ACT/360" ? days / 360 : days / 365;
321
+ }
322
+ function xnpv(rate, cashflows, dayCount = "ACT/365") {
323
+ if (cashflows.length === 0) return 0;
324
+ const sorted = [...cashflows].sort((a, b) => a.date.getTime() - b.date.getTime());
325
+ const d0 = sorted[0].date;
326
+ return sorted.reduce((sum, cf) => {
327
+ const yf = yearFrac(d0, cf.date, dayCount);
328
+ return sum + cf.amount / Math.pow(1 + rate, yf);
329
+ }, 0);
330
+ }
331
+ function xirr(cashflows, guess = 0.1, dayCount = "ACT/365") {
332
+ if (cashflows.length < 2) return 0;
333
+ const sorted = [...cashflows].sort((a, b) => a.date.getTime() - b.date.getTime());
334
+ const d0 = sorted[0].date;
335
+ const yfs = sorted.map((cf) => yearFrac(d0, cf.date, dayCount));
336
+ const amounts = sorted.map((cf) => cf.amount);
337
+ let rate = guess;
338
+ const maxIter = 1e3;
339
+ const tol = 1e-10;
340
+ for (let i = 0; i < maxIter; i++) {
341
+ let f = 0;
342
+ let df = 0;
343
+ for (let j = 0; j < amounts.length; j++) {
344
+ const disc = Math.pow(1 + rate, yfs[j]);
345
+ f += amounts[j] / disc;
346
+ if (yfs[j] !== 0) {
347
+ df -= yfs[j] * amounts[j] / Math.pow(1 + rate, yfs[j] + 1);
348
+ }
349
+ }
350
+ if (Math.abs(f) < tol) return rate;
351
+ if (Math.abs(df) < tol) break;
352
+ rate -= f / df;
353
+ }
354
+ let lo = -0.99;
355
+ let hi = 10;
356
+ for (let i = 0; i < 1e3; i++) {
357
+ const mid = (lo + hi) / 2;
358
+ let f = 0;
359
+ for (let j = 0; j < amounts.length; j++) {
360
+ f += amounts[j] / Math.pow(1 + mid, yfs[j]);
361
+ }
362
+ if (Math.abs(f) < tol) return mid;
363
+ let fLo = 0;
364
+ for (let j = 0; j < amounts.length; j++) {
365
+ fLo += amounts[j] / Math.pow(1 + lo, yfs[j]);
366
+ }
367
+ if (f * fLo > 0) lo = mid;
368
+ else hi = mid;
369
+ }
370
+ return (lo + hi) / 2;
371
+ }
372
+ function trailingReturn(series, windowDays) {
373
+ if (series.length < 2 || windowDays <= 0) return 0;
374
+ const sorted = [...series].sort((a, b) => a.date.getTime() - b.date.getTime());
375
+ const latest = sorted[sorted.length - 1];
376
+ const cutoff = latest.date.getTime() - windowDays * 24 * 60 * 60 * 1e3;
377
+ let closest = sorted[0];
378
+ let minDiff = Math.abs(sorted[0].date.getTime() - cutoff);
379
+ for (const pt of sorted) {
380
+ const diff = Math.abs(pt.date.getTime() - cutoff);
381
+ if (diff < minDiff) {
382
+ minDiff = diff;
383
+ closest = pt;
384
+ }
385
+ }
386
+ if (closest.value <= 0) return 0;
387
+ return (latest.value - closest.value) / closest.value;
388
+ }
389
+ function stepUpSipFutureValue(params) {
390
+ return sipFutureValue(params);
273
391
  }
274
392
  function volatility(returns, periodsPerYear = 252) {
275
393
  if (returns.length < 2) return 0;
package/dist/index.d.ts CHANGED
@@ -103,6 +103,32 @@ export declare function absoluteReturn(beginValue: number, endValue: number): nu
103
103
  export declare function annualizedReturn(beginValue: number, endValue: number, days: number): number;
104
104
  export declare function pv(rate: number, nper: number, pmt: number, fv?: number, type?: number): number;
105
105
  export declare function fv(rate: number, nper: number, pmt: number, pvVal?: number, type?: number): number;
106
+ export declare function pmt(rate: number, nper: number, pvVal: number, fvVal?: number, type?: number): number;
107
+ export declare function nper(rate: number, pmtVal: number, pvVal: number, fvVal?: number, type?: number): number;
108
+ export declare function rateCalc(nperVal: number, pmtVal: number, pvVal: number, fvVal?: number, type?: number, guess?: number): number;
109
+ export declare function xnpv(rate: number, cashflows: {
110
+ date: Date;
111
+ amount: number;
112
+ }[], dayCount?: "ACT/365" | "ACT/360"): number;
113
+ export declare function xirr(cashflows: {
114
+ date: Date;
115
+ amount: number;
116
+ }[], guess?: number, dayCount?: "ACT/365" | "ACT/360"): number;
117
+ export declare function trailingReturn(series: {
118
+ date: Date;
119
+ value: number;
120
+ }[], windowDays: number): number;
121
+ export declare function stepUpSipFutureValue(params: {
122
+ monthlyInvestment: number;
123
+ annualRate: number;
124
+ months: number;
125
+ stepUpPercentAnnual: number;
126
+ investmentAt?: "begin" | "end";
127
+ }): {
128
+ futureValue: number;
129
+ totalInvested: number;
130
+ estimatedGains: number;
131
+ };
106
132
  export declare function volatility(returns: number[], periodsPerYear?: number): number;
107
133
  export declare function sharpe(returns: number[], riskFreeRateAnnual?: number, periodsPerYear?: number): number;
108
134
  export declare function sortino(returns: number[], riskFreeRateAnnual?: number, periodsPerYear?: number): number;
package/dist/index.mjs CHANGED
@@ -214,15 +214,126 @@ function annualizedReturn(beginValue, endValue, days) {
214
214
  if (beginValue <= 0 || days <= 0) return 0;
215
215
  return Math.pow(endValue / beginValue, 365 / days) - 1;
216
216
  }
217
- function pv(rate, nper, pmt, fv2 = 0, type = 0) {
218
- if (rate === 0) return -(pmt * nper + fv2);
219
- const factor = Math.pow(1 + rate, nper);
220
- return -(pmt * (1 + rate * type) * ((factor - 1) / rate) + fv2) / factor;
217
+ function pv(rate, nper2, pmt2, fv2 = 0, type = 0) {
218
+ if (rate === 0) return -(pmt2 * nper2 + fv2);
219
+ const factor = Math.pow(1 + rate, nper2);
220
+ return -(pmt2 * (1 + rate * type) * ((factor - 1) / rate) + fv2) / factor;
221
221
  }
222
- function fv(rate, nper, pmt, pvVal = 0, type = 0) {
223
- if (rate === 0) return -(pvVal + pmt * nper);
224
- const factor = Math.pow(1 + rate, nper);
225
- return -(pvVal * factor + pmt * (1 + rate * type) * ((factor - 1) / rate));
222
+ function fv(rate, nper2, pmt2, pvVal = 0, type = 0) {
223
+ if (rate === 0) return -(pvVal + pmt2 * nper2);
224
+ const factor = Math.pow(1 + rate, nper2);
225
+ return -(pvVal * factor + pmt2 * (1 + rate * type) * ((factor - 1) / rate));
226
+ }
227
+ function pmt(rate, nper2, pvVal, fvVal = 0, type = 0) {
228
+ if (nper2 <= 0) return 0;
229
+ if (rate === 0) return -(pvVal + fvVal) / nper2;
230
+ const factor = Math.pow(1 + rate, nper2);
231
+ return -(pvVal * factor + fvVal) * rate / ((1 + rate * type) * (factor - 1));
232
+ }
233
+ function nper(rate, pmtVal, pvVal, fvVal = 0, type = 0) {
234
+ if (rate === 0) {
235
+ if (pmtVal === 0) return 0;
236
+ return -(pvVal + fvVal) / pmtVal;
237
+ }
238
+ const pmtAdj = pmtVal * (1 + rate * type);
239
+ const num = Math.log((pmtAdj - fvVal * rate) / (pmtAdj + pvVal * rate));
240
+ const den = Math.log(1 + rate);
241
+ if (den === 0) return 0;
242
+ return num / den;
243
+ }
244
+ function rateCalc(nperVal, pmtVal, pvVal, fvVal = 0, type = 0, guess = 0.1) {
245
+ if (nperVal <= 0) return 0;
246
+ let r = guess;
247
+ const maxIter = 1e3;
248
+ const tol = 1e-10;
249
+ for (let i = 0; i < maxIter; i++) {
250
+ const factor = Math.pow(1 + r, nperVal);
251
+ const pmtAdj = pmtVal * (1 + r * type);
252
+ const f = pvVal * factor + pmtAdj * ((factor - 1) / r) + fvVal;
253
+ const dfactor = nperVal * Math.pow(1 + r, nperVal - 1);
254
+ const dpmtAdj = pmtVal * type;
255
+ const df = pvVal * dfactor + dpmtAdj * ((factor - 1) / r) + pmtAdj * ((dfactor * r - (factor - 1)) / (r * r));
256
+ if (Math.abs(df) < tol) break;
257
+ const newR = r - f / df;
258
+ if (Math.abs(newR - r) < tol) return newR;
259
+ r = newR;
260
+ }
261
+ return r;
262
+ }
263
+ function yearFrac(d1, d2, dayCount = "ACT/365") {
264
+ const ms = d2.getTime() - d1.getTime();
265
+ const days = ms / (1e3 * 60 * 60 * 24);
266
+ return dayCount === "ACT/360" ? days / 360 : days / 365;
267
+ }
268
+ function xnpv(rate, cashflows, dayCount = "ACT/365") {
269
+ if (cashflows.length === 0) return 0;
270
+ const sorted = [...cashflows].sort((a, b) => a.date.getTime() - b.date.getTime());
271
+ const d0 = sorted[0].date;
272
+ return sorted.reduce((sum, cf) => {
273
+ const yf = yearFrac(d0, cf.date, dayCount);
274
+ return sum + cf.amount / Math.pow(1 + rate, yf);
275
+ }, 0);
276
+ }
277
+ function xirr(cashflows, guess = 0.1, dayCount = "ACT/365") {
278
+ if (cashflows.length < 2) return 0;
279
+ const sorted = [...cashflows].sort((a, b) => a.date.getTime() - b.date.getTime());
280
+ const d0 = sorted[0].date;
281
+ const yfs = sorted.map((cf) => yearFrac(d0, cf.date, dayCount));
282
+ const amounts = sorted.map((cf) => cf.amount);
283
+ let rate = guess;
284
+ const maxIter = 1e3;
285
+ const tol = 1e-10;
286
+ for (let i = 0; i < maxIter; i++) {
287
+ let f = 0;
288
+ let df = 0;
289
+ for (let j = 0; j < amounts.length; j++) {
290
+ const disc = Math.pow(1 + rate, yfs[j]);
291
+ f += amounts[j] / disc;
292
+ if (yfs[j] !== 0) {
293
+ df -= yfs[j] * amounts[j] / Math.pow(1 + rate, yfs[j] + 1);
294
+ }
295
+ }
296
+ if (Math.abs(f) < tol) return rate;
297
+ if (Math.abs(df) < tol) break;
298
+ rate -= f / df;
299
+ }
300
+ let lo = -0.99;
301
+ let hi = 10;
302
+ for (let i = 0; i < 1e3; i++) {
303
+ const mid = (lo + hi) / 2;
304
+ let f = 0;
305
+ for (let j = 0; j < amounts.length; j++) {
306
+ f += amounts[j] / Math.pow(1 + mid, yfs[j]);
307
+ }
308
+ if (Math.abs(f) < tol) return mid;
309
+ let fLo = 0;
310
+ for (let j = 0; j < amounts.length; j++) {
311
+ fLo += amounts[j] / Math.pow(1 + lo, yfs[j]);
312
+ }
313
+ if (f * fLo > 0) lo = mid;
314
+ else hi = mid;
315
+ }
316
+ return (lo + hi) / 2;
317
+ }
318
+ function trailingReturn(series, windowDays) {
319
+ if (series.length < 2 || windowDays <= 0) return 0;
320
+ const sorted = [...series].sort((a, b) => a.date.getTime() - b.date.getTime());
321
+ const latest = sorted[sorted.length - 1];
322
+ const cutoff = latest.date.getTime() - windowDays * 24 * 60 * 60 * 1e3;
323
+ let closest = sorted[0];
324
+ let minDiff = Math.abs(sorted[0].date.getTime() - cutoff);
325
+ for (const pt of sorted) {
326
+ const diff = Math.abs(pt.date.getTime() - cutoff);
327
+ if (diff < minDiff) {
328
+ minDiff = diff;
329
+ closest = pt;
330
+ }
331
+ }
332
+ if (closest.value <= 0) return 0;
333
+ return (latest.value - closest.value) / closest.value;
334
+ }
335
+ function stepUpSipFutureValue(params) {
336
+ return sipFutureValue(params);
226
337
  }
227
338
  function volatility(returns, periodsPerYear = 252) {
228
339
  if (returns.length < 2) return 0;
@@ -322,9 +433,12 @@ export {
322
433
  irr,
323
434
  lumpsumFutureValue,
324
435
  maxDrawdown,
436
+ nper,
325
437
  npv,
438
+ pmt,
326
439
  prepaymentImpact,
327
440
  pv,
441
+ rateCalc,
328
442
  realReturn,
329
443
  rebalance,
330
444
  requiredLumpsumForGoal,
@@ -333,7 +447,11 @@ export {
333
447
  sharpe,
334
448
  sipFutureValue,
335
449
  sortino,
450
+ stepUpSipFutureValue,
336
451
  swpPlan,
452
+ trailingReturn,
337
453
  volatility,
338
- weightedReturn
454
+ weightedReturn,
455
+ xirr,
456
+ xnpv
339
457
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fundamental-js",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Comprehensive financial calculator library for investing, loans, and personal finance. Includes EMI, SIP, SWP, amortization, CAGR, IRR, NPV, risk metrics, and more.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -13,7 +13,8 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "README.md"
17
18
  ],
18
19
  "keywords": [
19
20
  "finance",