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 +235 -0
- package/dist/index.cjs +127 -9
- package/dist/index.d.ts +26 -0
- package/dist/index.mjs +127 -9
- package/package.json +3 -2
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,
|
|
265
|
-
if (rate === 0) return -(
|
|
266
|
-
const factor = Math.pow(1 + rate,
|
|
267
|
-
return -(
|
|
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,
|
|
270
|
-
if (rate === 0) return -(pvVal +
|
|
271
|
-
const factor = Math.pow(1 + rate,
|
|
272
|
-
return -(pvVal * factor +
|
|
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,
|
|
218
|
-
if (rate === 0) return -(
|
|
219
|
-
const factor = Math.pow(1 + rate,
|
|
220
|
-
return -(
|
|
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,
|
|
223
|
-
if (rate === 0) return -(pvVal +
|
|
224
|
-
const factor = Math.pow(1 + rate,
|
|
225
|
-
return -(pvVal * factor +
|
|
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.
|
|
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",
|