fundamental-js 1.2.0 → 1.3.1
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 +128 -72
- package/dist/index.cjs +11 -8
- package/dist/index.mjs +11 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A comprehensive, zero-dependency TypeScript financial calculator library for investing, loans, and personal finance.
|
|
4
4
|
|
|
5
|
-
All functions are **pure**, **deterministic**, and work in both **Node.js** and **browser** environments.
|
|
5
|
+
All functions are **pure**, **deterministic**, and work in both **Node.js** and **browser** environments.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
|
|
24
24
|
// EMI for a ₹25L loan at 8.5% for 20 years
|
|
25
25
|
const loan = emi(2500000, 0.085, 240);
|
|
26
|
-
console.log(loan);
|
|
27
26
|
// { emi: 21698.69, totalPayment: 5207685.73, totalInterest: 2707685.73 }
|
|
28
27
|
|
|
29
28
|
// SIP: ₹25,000/month at 12% for 15 years
|
|
@@ -32,7 +31,6 @@ const sip = sipFutureValue({
|
|
|
32
31
|
annualRate: 0.12,
|
|
33
32
|
months: 180,
|
|
34
33
|
});
|
|
35
|
-
console.log(sip);
|
|
36
34
|
// { futureValue: 12649741.45, totalInvested: 4500000, estimatedGains: 8149741.45 }
|
|
37
35
|
|
|
38
36
|
// Step-up SIP: ₹25,000/month, 10% annual step-up
|
|
@@ -40,18 +38,17 @@ const stepUp = stepUpSipFutureValue({
|
|
|
40
38
|
monthlyInvestment: 25000,
|
|
41
39
|
annualRate: 0.12,
|
|
42
40
|
months: 180,
|
|
43
|
-
stepUpPercentAnnual: 10,
|
|
41
|
+
stepUpPercentAnnual: 0.10,
|
|
44
42
|
});
|
|
45
|
-
console.log(stepUp);
|
|
46
43
|
|
|
47
|
-
// SWP: ₹1Cr corpus, ₹50K/month withdrawal at 8%
|
|
44
|
+
// SWP: ₹1Cr corpus, ₹50K/month withdrawal at 8%, 6% inflation
|
|
48
45
|
const withdrawal = swpPlan({
|
|
49
46
|
initialCorpus: 10000000,
|
|
50
47
|
monthlyWithdrawal: 50000,
|
|
51
48
|
annualRate: 0.08,
|
|
52
49
|
months: 300,
|
|
50
|
+
inflationRateAnnual: 0.06,
|
|
53
51
|
});
|
|
54
|
-
console.log(withdrawal.endingCorpus, withdrawal.totalWithdrawn);
|
|
55
52
|
|
|
56
53
|
// XIRR: Irregular cashflows with dates
|
|
57
54
|
const result = xirr([
|
|
@@ -59,67 +56,105 @@ const result = xirr([
|
|
|
59
56
|
{ date: new Date("2020-06-15"), amount: 5000 },
|
|
60
57
|
{ date: new Date("2021-01-01"), amount: 115000 },
|
|
61
58
|
]);
|
|
62
|
-
|
|
59
|
+
// ~0.1975 (19.75%)
|
|
63
60
|
```
|
|
64
61
|
|
|
62
|
+
## Conventions
|
|
63
|
+
|
|
64
|
+
All rates and percentages are expressed as **decimals** throughout the library:
|
|
65
|
+
|
|
66
|
+
| Meaning | Value | NOT |
|
|
67
|
+
|---------|-------|-----|
|
|
68
|
+
| 12% annual return | `0.12` | `12` |
|
|
69
|
+
| 8.5% interest rate | `0.085` | `8.5` |
|
|
70
|
+
| 10% annual step-up | `0.10` | `10` |
|
|
71
|
+
| 6% inflation | `0.06` | `6` |
|
|
72
|
+
|
|
73
|
+
Other conventions:
|
|
74
|
+
- **Period type**: `0` = end-of-period (ordinary annuity), `1` = beginning-of-period (annuity due)
|
|
75
|
+
- **Day count**: XNPV/XIRR default to ACT/365; pass `"ACT/360"` to override
|
|
76
|
+
- **Cash-flow sign**: Outflows are negative, inflows are positive (Excel convention for PV/FV/PMT/NPER/RATE)
|
|
77
|
+
|
|
65
78
|
## API Reference
|
|
66
79
|
|
|
67
|
-
### Time Value of Money
|
|
80
|
+
### Time Value of Money
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
|
72
|
-
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `rate
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
Excel-compatible sign convention: cash you pay out is negative, cash you receive is positive.
|
|
83
|
+
|
|
84
|
+
| Function | Formula | Description |
|
|
85
|
+
|----------|---------|-------------|
|
|
86
|
+
| `pv(rate, nper, pmt, fv?, type?)` | PV = −[PMT·(1+r·type)·((1+r)ⁿ−1)/r + FV] / (1+r)ⁿ | Present Value |
|
|
87
|
+
| `fv(rate, nper, pmt, pv?, type?)` | FV = −[PV·(1+r)ⁿ + PMT·(1+r·type)·((1+r)ⁿ−1)/r] | Future Value |
|
|
88
|
+
| `pmt(rate, nper, pv, fv?, type?)` | PMT = −[PV·(1+r)ⁿ + FV]·r / [(1+r·type)·((1+r)ⁿ−1)] | Payment per period |
|
|
89
|
+
| `nper(rate, pmt, pv, fv?, type?)` | NPER = ln[(PMT·(1+r·type) − FV·r) / (PMT·(1+r·type) + PV·r)] / ln(1+r) | Number of periods |
|
|
90
|
+
| `rate(nper, pmt, pv, fv?, type?, guess?)` | Newton-Raphson iterative solver | Solve for periodic rate |
|
|
91
|
+
|
|
92
|
+
**Parameters:**
|
|
93
|
+
- `rate` — periodic interest rate as decimal (e.g., monthly rate = annual / 12)
|
|
94
|
+
- `nper` — total number of compounding periods
|
|
95
|
+
- `type` — `0` = end of period (default), `1` = beginning of period
|
|
96
|
+
|
|
97
|
+
### Cash-Flow Analysis
|
|
98
|
+
|
|
99
|
+
| Function | Formula | Description |
|
|
100
|
+
|----------|---------|-------------|
|
|
101
|
+
| `npv(rate, cashflows)` | NPV = Σ CFᵢ / (1+r)ⁱ, i = 0…n | Net Present Value (textbook-style: CF₀ at face value) |
|
|
102
|
+
| `irr(cashflows, guess?)` | Solves NPV = 0 | Internal Rate of Return |
|
|
103
|
+
| `xnpv(rate, cashflows, dayCount?)` | XNPV = Σ CFᵢ / (1+r)^(dᵢ/365) | NPV with irregular dates |
|
|
104
|
+
| `xirr(cashflows, guess?, dayCount?)` | Solves XNPV = 0 | IRR with irregular dates |
|
|
105
|
+
|
|
106
|
+
**Note on NPV:** This is the textbook NPV where `cashflows[0]` is at time 0 (not discounted). Excel's `NPV()` discounts from period 1; to replicate Excel in this library, do: `npv(rate, [0, ...cashflows])` or `cashflows[0] + npv(rate, cashflows.slice(1))`.
|
|
107
|
+
|
|
108
|
+
**IRR/XIRR validation:** Both functions throw an `Error` if cash flows do not contain at least one positive and one negative value (no sign change means no valid rate exists).
|
|
80
109
|
|
|
81
110
|
**Parameters:**
|
|
82
|
-
- `
|
|
83
|
-
- `
|
|
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 }[]`
|
|
111
|
+
- `cashflows` for NPV/IRR: `number[]`
|
|
112
|
+
- `cashflows` for XNPV/XIRR: `{ date: Date, amount: number }[]`
|
|
87
113
|
- `dayCount` — `"ACT/365"` (default) or `"ACT/360"`
|
|
88
114
|
|
|
89
115
|
### Returns Analysis
|
|
90
116
|
|
|
91
|
-
| Function | Description |
|
|
92
|
-
|
|
93
|
-
| `absoluteReturn(beginValue, endValue)` | Simple return as
|
|
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 |
|
|
117
|
+
| Function | Formula | Description |
|
|
118
|
+
|----------|---------|-------------|
|
|
119
|
+
| `absoluteReturn(beginValue, endValue)` | (end − begin) / begin | Simple return as decimal |
|
|
120
|
+
| `cagr(beginValue, endValue, years)` | (end / begin)^(1/years) − 1 | Compound Annual Growth Rate |
|
|
121
|
+
| `annualizedReturn(beginValue, endValue, days)` | (end / begin)^(365/days) − 1 | Annualized return using ACT/365 |
|
|
122
|
+
| `trailingReturn(series, windowDays)` | Simple return from nearest point to cutoff | Trailing return over a rolling window |
|
|
97
123
|
|
|
98
124
|
**Parameters:**
|
|
99
125
|
- `series` for trailingReturn: `{ date: Date, value: number }[]`
|
|
100
|
-
- `windowDays` — lookback window in days
|
|
126
|
+
- `windowDays` — lookback window in calendar days
|
|
101
127
|
|
|
102
128
|
### SIP / Lumpsum / SWP
|
|
103
129
|
|
|
104
130
|
| Function | Description |
|
|
105
131
|
|----------|-------------|
|
|
106
|
-
| `sipFutureValue(params)` | SIP future value with optional step-up |
|
|
107
|
-
| `stepUpSipFutureValue(params)` | SIP with required annual step-up |
|
|
108
|
-
| `lumpsumFutureValue(principal, annualRate, years)` |
|
|
109
|
-
| `swpPlan(params)` | Systematic Withdrawal Plan with schedule |
|
|
110
|
-
| `inflationAdjustedValue(value, inflationRate, years)` |
|
|
111
|
-
| `realReturn(nominalRate, inflationRate)` | Fisher equation
|
|
132
|
+
| `sipFutureValue(params)` | SIP future value with optional annual step-up |
|
|
133
|
+
| `stepUpSipFutureValue(params)` | SIP with required annual step-up (delegates to sipFutureValue) |
|
|
134
|
+
| `lumpsumFutureValue(principal, annualRate, years)` | FV = P × (1 + r)^t |
|
|
135
|
+
| `swpPlan(params)` | Systematic Withdrawal Plan with month-by-month schedule |
|
|
136
|
+
| `inflationAdjustedValue(value, inflationRate, years)` | value / (1 + inflation)^years |
|
|
137
|
+
| `realReturn(nominalRate, inflationRate)` | Fisher equation: (1 + nominal) / (1 + inflation) − 1 |
|
|
112
138
|
|
|
113
139
|
**sipFutureValue params:**
|
|
114
140
|
```ts
|
|
115
141
|
{
|
|
116
142
|
monthlyInvestment: number,
|
|
117
|
-
annualRate: number,
|
|
143
|
+
annualRate: number, // 0.12 = 12%
|
|
118
144
|
months: number,
|
|
119
|
-
stepUpPercentAnnual?: number, //
|
|
145
|
+
stepUpPercentAnnual?: number, // 0.10 = 10% annual increase
|
|
120
146
|
investmentAt?: "begin" | "end"
|
|
121
147
|
}
|
|
122
148
|
```
|
|
149
|
+
|
|
150
|
+
**SIP formula (end-of-period, no step-up):**
|
|
151
|
+
FV = P × [(1+r)ⁿ − 1] / r, where r = annualRate / 12
|
|
152
|
+
|
|
153
|
+
**SIP formula (beginning-of-period):**
|
|
154
|
+
FV = P × (1+r) × [(1+r)ⁿ − 1] / r
|
|
155
|
+
|
|
156
|
+
With step-up: monthly investment increases by `stepUpPercentAnnual` every 12 months, computed iteratively.
|
|
157
|
+
|
|
123
158
|
Returns: `{ futureValue, totalInvested, estimatedGains }`
|
|
124
159
|
|
|
125
160
|
**swpPlan params:**
|
|
@@ -127,9 +162,9 @@ Returns: `{ futureValue, totalInvested, estimatedGains }`
|
|
|
127
162
|
{
|
|
128
163
|
initialCorpus: number,
|
|
129
164
|
monthlyWithdrawal: number,
|
|
130
|
-
annualRate: number,
|
|
165
|
+
annualRate: number, // 0.08 = 8%
|
|
131
166
|
months: number,
|
|
132
|
-
inflationRateAnnual?: number, //
|
|
167
|
+
inflationRateAnnual?: number, // 0.06 = 6% (withdrawal increases annually)
|
|
133
168
|
withdrawalAt?: "begin" | "end"
|
|
134
169
|
}
|
|
135
170
|
```
|
|
@@ -137,18 +172,18 @@ Returns: `{ endingCorpus, totalWithdrawn, schedule[] }`
|
|
|
137
172
|
|
|
138
173
|
### Goal Planning
|
|
139
174
|
|
|
140
|
-
| Function | Description |
|
|
141
|
-
|
|
142
|
-
| `requiredMonthlyInvestmentForGoal(params)` | Required monthly SIP for a target amount |
|
|
143
|
-
| `requiredLumpsumForGoal(goal, annualRate, years)` | Required lumpsum
|
|
175
|
+
| Function | Formula | Description |
|
|
176
|
+
|----------|---------|-------------|
|
|
177
|
+
| `requiredMonthlyInvestmentForGoal(params)` | Binary search over sipFutureValue | Required monthly SIP for a target amount |
|
|
178
|
+
| `requiredLumpsumForGoal(goal, annualRate, years)` | goal / (1 + r)^years | Required lumpsum (present value of goal) |
|
|
144
179
|
|
|
145
180
|
**requiredMonthlyInvestmentForGoal params:**
|
|
146
181
|
```ts
|
|
147
182
|
{
|
|
148
183
|
goalAmountFuture: number,
|
|
149
|
-
annualRate: number,
|
|
184
|
+
annualRate: number, // 0.12 = 12%
|
|
150
185
|
months: number,
|
|
151
|
-
stepUpPercentAnnual?: number,
|
|
186
|
+
stepUpPercentAnnual?: number, // 0.10 = 10%
|
|
152
187
|
investmentAt?: "begin" | "end"
|
|
153
188
|
}
|
|
154
189
|
```
|
|
@@ -156,13 +191,15 @@ Returns: `{ requiredMonthlyInvestment, assumptions }`
|
|
|
156
191
|
|
|
157
192
|
### Loans
|
|
158
193
|
|
|
159
|
-
| Function | Description |
|
|
160
|
-
|
|
161
|
-
| `emi(principal, annualRate, months)` | Equated Monthly Installment |
|
|
162
|
-
| `amortizationSchedule(params)` | Full loan schedule with extra payments |
|
|
163
|
-
| `prepaymentImpact(params)` |
|
|
194
|
+
| Function | Formula | Description |
|
|
195
|
+
|----------|---------|-------------|
|
|
196
|
+
| `emi(principal, annualRate, months)` | EMI = P × r × (1+r)ⁿ / [(1+r)ⁿ − 1] | Equated Monthly Installment |
|
|
197
|
+
| `amortizationSchedule(params)` | Month-by-month principal/interest split | Full loan schedule with extra payments |
|
|
198
|
+
| `prepaymentImpact(params)` | Compares original vs prepaid loan | Interest saved and tenure reduction |
|
|
164
199
|
|
|
165
|
-
**
|
|
200
|
+
**EMI formula:** r = annualRate / 12. When annualRate = 0, EMI = principal / months.
|
|
201
|
+
|
|
202
|
+
Returns: `{ emi, totalPayment, totalInterest }`
|
|
166
203
|
|
|
167
204
|
**amortizationSchedule params:**
|
|
168
205
|
```ts
|
|
@@ -186,29 +223,41 @@ Returns: `{ emi, schedule[], totalInterest, totalPaid, payoffMonth }`
|
|
|
186
223
|
mode: "reduceTenure" | "reduceEmi"
|
|
187
224
|
}
|
|
188
225
|
```
|
|
226
|
+
- `reduceTenure` — keeps EMI constant, pays off loan early
|
|
227
|
+
- `reduceEmi` — keeps tenure constant, recalculates lower EMI after each prepayment
|
|
228
|
+
|
|
189
229
|
Returns: `{ original, new, savings: { interestSaved, monthsSaved } }`
|
|
190
230
|
|
|
191
231
|
### Risk Metrics
|
|
192
232
|
|
|
193
|
-
| Function | Description |
|
|
194
|
-
|
|
195
|
-
| `volatility(returns, periodsPerYear?)` | Annualized volatility (sample
|
|
196
|
-
| `sharpe(returns, riskFreeRate?, periodsPerYear?)` | Sharpe Ratio |
|
|
197
|
-
| `sortino(returns, riskFreeRate?, periodsPerYear?)` | Sortino Ratio (
|
|
198
|
-
| `maxDrawdown(values)` |
|
|
233
|
+
| Function | Formula | Description |
|
|
234
|
+
|----------|---------|-------------|
|
|
235
|
+
| `volatility(returns, periodsPerYear?)` | σ = s(returns) × √periodsPerYear | Annualized volatility (sample std dev) |
|
|
236
|
+
| `sharpe(returns, riskFreeRate?, periodsPerYear?)` | (R̄·T − Rf) / σ | Sharpe Ratio (Sharpe 1994) |
|
|
237
|
+
| `sortino(returns, riskFreeRate?, periodsPerYear?)` | (R̄·T − Rf) / DD | Sortino Ratio (Sortino & Price 1994) |
|
|
238
|
+
| `maxDrawdown(values)` | max((peak − trough) / peak) | Maximum peak-to-trough decline |
|
|
239
|
+
|
|
240
|
+
**Volatility:** Uses sample standard deviation (N−1 denominator) × √periodsPerYear for annualization.
|
|
241
|
+
|
|
242
|
+
**Sharpe Ratio:** Arithmetic annualization of mean return. `SR = (meanReturn × periodsPerYear − riskFreeRateAnnual) / volatility`
|
|
243
|
+
|
|
244
|
+
**Sortino Ratio:** Downside deviation uses the full sample size N as denominator (per Sortino & Price 1994), not just the count of negative returns:
|
|
245
|
+
`DD = √(Σ min(rᵢ − MAR, 0)² / N) × √periodsPerYear`
|
|
246
|
+
|
|
247
|
+
Returns `Infinity` when no downside returns exist (zero downside risk).
|
|
199
248
|
|
|
200
249
|
**Parameters:**
|
|
201
|
-
- `returns` — array of periodic returns as decimals
|
|
202
|
-
- `values` — array of portfolio values
|
|
203
|
-
- `periodsPerYear` — defaults to 252 (daily trading)
|
|
250
|
+
- `returns` — array of periodic returns as decimals (e.g., daily: 0.01 = 1%)
|
|
251
|
+
- `values` — array of portfolio values (for maxDrawdown)
|
|
252
|
+
- `periodsPerYear` — defaults to 252 (daily trading days)
|
|
204
253
|
- `riskFreeRate` — annual risk-free rate as decimal
|
|
205
254
|
|
|
206
255
|
### Portfolio Helpers
|
|
207
256
|
|
|
208
257
|
| Function | Description |
|
|
209
258
|
|----------|-------------|
|
|
210
|
-
| `weightedReturn(returns, weights)` | Weighted average return |
|
|
211
|
-
| `rebalance(targetWeights, currentValues)` | Calculate trades
|
|
259
|
+
| `weightedReturn(returns, weights)` | Weighted average return: Σ(rᵢ × wᵢ) |
|
|
260
|
+
| `rebalance(targetWeights, currentValues)` | Calculate trades to reach target allocation |
|
|
212
261
|
|
|
213
262
|
**rebalance** returns: `{ trades, newValues, total }` — positive trade = buy, negative = sell
|
|
214
263
|
|
|
@@ -218,17 +267,24 @@ Returns: `{ original, new, savings: { interestSaved, monthsSaved } }`
|
|
|
218
267
|
|----------|-------------|
|
|
219
268
|
| `safeNum(value, fallback?)` | Returns fallback if value is NaN/Infinity/null/undefined |
|
|
220
269
|
|
|
221
|
-
##
|
|
270
|
+
## Migration from v1.2.x
|
|
222
271
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
-
|
|
227
|
-
|
|
272
|
+
**Breaking change in v1.3.0:** `stepUpPercentAnnual` and `inflationRateAnnual` (in SWP) now follow the same decimal convention as all other rates. Previously they expected whole-number percentages (e.g., `10` for 10%); now they expect decimals (e.g., `0.10` for 10%).
|
|
273
|
+
|
|
274
|
+
```diff
|
|
275
|
+
- sipFutureValue({ monthlyInvestment: 25000, annualRate: 0.12, months: 180, stepUpPercentAnnual: 10 })
|
|
276
|
+
+ sipFutureValue({ monthlyInvestment: 25000, annualRate: 0.12, months: 180, stepUpPercentAnnual: 0.10 })
|
|
277
|
+
|
|
278
|
+
- swpPlan({ initialCorpus: 10000000, monthlyWithdrawal: 50000, annualRate: 0.08, months: 300, inflationRateAnnual: 6 })
|
|
279
|
+
+ swpPlan({ initialCorpus: 10000000, monthlyWithdrawal: 50000, annualRate: 0.08, months: 300, inflationRateAnnual: 0.06 })
|
|
280
|
+
```
|
|
228
281
|
|
|
229
282
|
## Error Handling
|
|
230
283
|
|
|
231
|
-
|
|
284
|
+
Most functions return safe defaults (0, empty arrays) for invalid inputs, making them safe for reactive UIs. Exceptions:
|
|
285
|
+
|
|
286
|
+
- `irr()` and `xirr()` throw an `Error` if cash flows don't contain at least one positive and one negative value
|
|
287
|
+
- `sortino()` returns `Infinity` when there are no downside returns
|
|
232
288
|
|
|
233
289
|
## License
|
|
234
290
|
|
package/dist/index.cjs
CHANGED
|
@@ -208,7 +208,7 @@ function sipFutureValue(params) {
|
|
|
208
208
|
let currentMonthly = monthlyInvestment;
|
|
209
209
|
for (let m = 1; m <= months; m++) {
|
|
210
210
|
if (stepUpPercentAnnual > 0 && m > 1 && (m - 1) % 12 === 0) {
|
|
211
|
-
currentMonthly *= 1 + stepUpPercentAnnual
|
|
211
|
+
currentMonthly *= 1 + stepUpPercentAnnual;
|
|
212
212
|
}
|
|
213
213
|
totalInvested += currentMonthly;
|
|
214
214
|
if (investmentAt === "begin") {
|
|
@@ -236,7 +236,7 @@ function swpPlan(params) {
|
|
|
236
236
|
const schedule = [];
|
|
237
237
|
for (let m = 1; m <= months; m++) {
|
|
238
238
|
if (inflationRateAnnual > 0 && m > 1 && (m - 1) % 12 === 0) {
|
|
239
|
-
currentWithdrawal *= 1 + inflationRateAnnual
|
|
239
|
+
currentWithdrawal *= 1 + inflationRateAnnual;
|
|
240
240
|
}
|
|
241
241
|
let interest;
|
|
242
242
|
let withdrawal = currentWithdrawal;
|
|
@@ -293,7 +293,7 @@ function requiredMonthlyInvestmentForGoal(params) {
|
|
|
293
293
|
}
|
|
294
294
|
return {
|
|
295
295
|
requiredMonthlyInvestment: (lo + hi) / 2,
|
|
296
|
-
assumptions: `Rate: ${(annualRate * 100).toFixed(1)}% p.a., Duration: ${months} months${stepUpPercentAnnual ? `, Step-up: ${stepUpPercentAnnual}% annually` : ""}`
|
|
296
|
+
assumptions: `Rate: ${(annualRate * 100).toFixed(1)}% p.a., Duration: ${months} months${stepUpPercentAnnual ? `, Step-up: ${(stepUpPercentAnnual * 100).toFixed(1)}% annually` : ""}`
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
function requiredLumpsumForGoal(goalAmountFuture, annualRate, years) {
|
|
@@ -459,14 +459,17 @@ function sharpe(returns, riskFreeRateAnnual = 0, periodsPerYear = 252) {
|
|
|
459
459
|
return (annualReturn - riskFreeRateAnnual) / vol;
|
|
460
460
|
}
|
|
461
461
|
function sortino(returns, riskFreeRateAnnual = 0, periodsPerYear = 252) {
|
|
462
|
+
if (returns.length < 2) return 0;
|
|
462
463
|
const mean = returns.reduce((s, r) => s + r, 0) / returns.length;
|
|
463
464
|
const rfPerPeriod = riskFreeRateAnnual / periodsPerYear;
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
465
|
+
const downsideSquaredSum = returns.reduce((s, r) => {
|
|
466
|
+
const diff = r - rfPerPeriod;
|
|
467
|
+
return diff < 0 ? s + diff * diff : s;
|
|
468
|
+
}, 0);
|
|
469
|
+
if (downsideSquaredSum === 0) return Infinity;
|
|
470
|
+
const downsideDev = Math.sqrt(downsideSquaredSum / returns.length) * Math.sqrt(periodsPerYear);
|
|
468
471
|
const annualReturn = mean * periodsPerYear;
|
|
469
|
-
if (downsideDev === 0) return
|
|
472
|
+
if (downsideDev === 0) return Infinity;
|
|
470
473
|
return (annualReturn - riskFreeRateAnnual) / downsideDev;
|
|
471
474
|
}
|
|
472
475
|
function maxDrawdown(values) {
|
package/dist/index.mjs
CHANGED
|
@@ -154,7 +154,7 @@ function sipFutureValue(params) {
|
|
|
154
154
|
let currentMonthly = monthlyInvestment;
|
|
155
155
|
for (let m = 1; m <= months; m++) {
|
|
156
156
|
if (stepUpPercentAnnual > 0 && m > 1 && (m - 1) % 12 === 0) {
|
|
157
|
-
currentMonthly *= 1 + stepUpPercentAnnual
|
|
157
|
+
currentMonthly *= 1 + stepUpPercentAnnual;
|
|
158
158
|
}
|
|
159
159
|
totalInvested += currentMonthly;
|
|
160
160
|
if (investmentAt === "begin") {
|
|
@@ -182,7 +182,7 @@ function swpPlan(params) {
|
|
|
182
182
|
const schedule = [];
|
|
183
183
|
for (let m = 1; m <= months; m++) {
|
|
184
184
|
if (inflationRateAnnual > 0 && m > 1 && (m - 1) % 12 === 0) {
|
|
185
|
-
currentWithdrawal *= 1 + inflationRateAnnual
|
|
185
|
+
currentWithdrawal *= 1 + inflationRateAnnual;
|
|
186
186
|
}
|
|
187
187
|
let interest;
|
|
188
188
|
let withdrawal = currentWithdrawal;
|
|
@@ -239,7 +239,7 @@ function requiredMonthlyInvestmentForGoal(params) {
|
|
|
239
239
|
}
|
|
240
240
|
return {
|
|
241
241
|
requiredMonthlyInvestment: (lo + hi) / 2,
|
|
242
|
-
assumptions: `Rate: ${(annualRate * 100).toFixed(1)}% p.a., Duration: ${months} months${stepUpPercentAnnual ? `, Step-up: ${stepUpPercentAnnual}% annually` : ""}`
|
|
242
|
+
assumptions: `Rate: ${(annualRate * 100).toFixed(1)}% p.a., Duration: ${months} months${stepUpPercentAnnual ? `, Step-up: ${(stepUpPercentAnnual * 100).toFixed(1)}% annually` : ""}`
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
245
|
function requiredLumpsumForGoal(goalAmountFuture, annualRate, years) {
|
|
@@ -405,14 +405,17 @@ function sharpe(returns, riskFreeRateAnnual = 0, periodsPerYear = 252) {
|
|
|
405
405
|
return (annualReturn - riskFreeRateAnnual) / vol;
|
|
406
406
|
}
|
|
407
407
|
function sortino(returns, riskFreeRateAnnual = 0, periodsPerYear = 252) {
|
|
408
|
+
if (returns.length < 2) return 0;
|
|
408
409
|
const mean = returns.reduce((s, r) => s + r, 0) / returns.length;
|
|
409
410
|
const rfPerPeriod = riskFreeRateAnnual / periodsPerYear;
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
const downsideSquaredSum = returns.reduce((s, r) => {
|
|
412
|
+
const diff = r - rfPerPeriod;
|
|
413
|
+
return diff < 0 ? s + diff * diff : s;
|
|
414
|
+
}, 0);
|
|
415
|
+
if (downsideSquaredSum === 0) return Infinity;
|
|
416
|
+
const downsideDev = Math.sqrt(downsideSquaredSum / returns.length) * Math.sqrt(periodsPerYear);
|
|
414
417
|
const annualReturn = mean * periodsPerYear;
|
|
415
|
-
if (downsideDev === 0) return
|
|
418
|
+
if (downsideDev === 0) return Infinity;
|
|
416
419
|
return (annualReturn - riskFreeRateAnnual) / downsideDev;
|
|
417
420
|
}
|
|
418
421
|
function maxDrawdown(values) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fundamental-js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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",
|