fundamental-js
v1.4.1
Published
Comprehensive financial calculator library for investing, loans, and personal finance. Includes EMI, SIP, SWP, amortization, CAGR, IRR, NPV, risk metrics, and more.
Maintainers
Readme
fundamental-js
A comprehensive, zero-dependency TypeScript financial calculator library for investing, loans, and personal finance.
All functions are pure, deterministic, and work in both Node.js and browser environments.
Live Demo — Interactive web app showcasing all calculators with visualizations.
Install
npm install fundamental-jsQuick Start
import {
emi,
sipFutureValue,
stepUpSipFutureValue,
swpPlan,
xirr,
} from "fundamental-js";
// EMI for a ₹25L loan at 8.5% for 20 years
const loan = emi(2500000, 0.085, 240);
// { emi: 21698.69, totalPayment: 5207685.73, totalInterest: 2707685.73 }
// SIP: ₹25,000/month at 12% for 15 years
const sip = sipFutureValue({
monthlyInvestment: 25000,
annualRate: 0.12,
months: 180,
});
// { futureValue: 12649741.45, totalInvested: 4500000, estimatedGains: 8149741.45 }
// Step-up SIP: ₹25,000/month, 10% annual step-up
const stepUp = stepUpSipFutureValue({
monthlyInvestment: 25000,
annualRate: 0.12,
months: 180,
stepUpPercentAnnual: 0.10,
});
// SWP: ₹1Cr corpus, ₹50K/month withdrawal at 8%, 6% inflation
const withdrawal = swpPlan({
initialCorpus: 10000000,
monthlyWithdrawal: 50000,
annualRate: 0.08,
months: 300,
inflationRateAnnual: 0.06,
});
// XIRR: Irregular cashflows with dates
const result = xirr([
{ date: new Date("2020-01-01"), amount: -100000 },
{ date: new Date("2020-06-15"), amount: 5000 },
{ date: new Date("2021-01-01"), amount: 115000 },
]);
// ~0.1975 (19.75%)Conventions
All rates and percentages are expressed as decimals throughout the library:
| Meaning | Value | NOT |
|---------|-------|-----|
| 12% annual return | 0.12 | 12 |
| 8.5% interest rate | 0.085 | 8.5 |
| 10% annual step-up | 0.10 | 10 |
| 6% inflation | 0.06 | 6 |
Other conventions:
- Period type:
0= end-of-period (ordinary annuity),1= beginning-of-period (annuity due) - Day count: XNPV/XIRR default to ACT/365; pass
"ACT/360"to override - Cash-flow sign: Outflows are negative, inflows are positive (Excel convention for PV/FV/PMT/NPER/RATE)
API Reference
Time Value of Money
Excel-compatible sign convention: cash you pay out is negative, cash you receive is positive.
| Function | Formula | Description |
|----------|---------|-------------|
| pv(rate, nper, pmt, fv?, type?) | PV = −[PMT·(1+r·type)·((1+r)ⁿ−1)/r + FV] / (1+r)ⁿ | Present Value |
| fv(rate, nper, pmt, pv?, type?) | FV = −[PV·(1+r)ⁿ + PMT·(1+r·type)·((1+r)ⁿ−1)/r] | Future Value |
| pmt(rate, nper, pv, fv?, type?) | PMT = −[PV·(1+r)ⁿ + FV]·r / [(1+r·type)·((1+r)ⁿ−1)] | Payment per period |
| 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 |
| rate(nper, pmt, pv, fv?, type?, guess?) | Newton-Raphson iterative solver | Solve for periodic rate |
Parameters:
rate— periodic interest rate as decimal (e.g., monthly rate = annual / 12)nper— total number of compounding periodstype—0= end of period (default),1= beginning of period
Cash-Flow Analysis
| Function | Formula | Description |
|----------|---------|-------------|
| npv(rate, cashflows) | NPV = Σ CFᵢ / (1+r)ⁱ, i = 0…n | Net Present Value (textbook-style: CF₀ at face value) |
| irr(cashflows, guess?) | Solves NPV = 0 | Internal Rate of Return |
| xnpv(rate, cashflows, dayCount?) | XNPV = Σ CFᵢ / (1+r)^(dᵢ/365) | NPV with irregular dates |
| xirr(cashflows, guess?, dayCount?) | Solves XNPV = 0 | IRR with irregular dates |
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)).
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).
Parameters:
cashflowsfor NPV/IRR:number[]cashflowsfor XNPV/XIRR:{ date: Date, amount: number }[]dayCount—"ACT/365"(default) or"ACT/360"
Returns Analysis
| Function | Formula | Description |
|----------|---------|-------------|
| absoluteReturn(beginValue, endValue) | (end − begin) / begin | Simple return as decimal |
| cagr(beginValue, endValue, years) | (end / begin)^(1/years) − 1 | Compound Annual Growth Rate |
| annualizedReturn(beginValue, endValue, days) | (end / begin)^(365/days) − 1 | Annualized return using ACT/365 |
| trailingReturn(series, windowDays) | Simple return from nearest point to cutoff | Trailing return over a rolling window |
Parameters:
seriesfor trailingReturn:{ date: Date, value: number }[]windowDays— lookback window in calendar days
SIP / Lumpsum / SWP
| Function | Description |
|----------|-------------|
| sipFutureValue(params) | SIP future value with optional annual step-up |
| stepUpSipFutureValue(params) | SIP with required annual step-up (delegates to sipFutureValue) |
| lumpsumFutureValue(principal, annualRate, years) | FV = P × (1 + r)^t |
| swpPlan(params) | Systematic Withdrawal Plan with month-by-month schedule |
| inflationAdjustedValue(value, inflationRate, years) | value / (1 + inflation)^years |
| realReturn(nominalRate, inflationRate) | Fisher equation: (1 + nominal) / (1 + inflation) − 1 |
sipFutureValue params:
{
monthlyInvestment: number,
annualRate: number, // 0.12 = 12%
months: number,
stepUpPercentAnnual?: number, // 0.10 = 10% annual increase
investmentAt?: "begin" | "end"
}SIP formula (end-of-period, no step-up): FV = P × [(1+r)ⁿ − 1] / r, where r = annualRate / 12
SIP formula (beginning-of-period): FV = P × (1+r) × [(1+r)ⁿ − 1] / r
With step-up: monthly investment increases by stepUpPercentAnnual every 12 months, computed iteratively.
Returns: { futureValue, totalInvested, estimatedGains }
swpPlan params:
{
initialCorpus: number,
monthlyWithdrawal: number,
annualRate: number, // 0.08 = 8%
months: number,
inflationRateAnnual?: number, // 0.06 = 6% (withdrawal increases annually)
withdrawalAt?: "begin" | "end"
}Returns: { endingCorpus, totalWithdrawn, schedule[] }
Goal Planning
| Function | Formula | Description |
|----------|---------|-------------|
| requiredMonthlyInvestmentForGoal(params) | Binary search over sipFutureValue | Required monthly SIP for a target amount |
| requiredLumpsumForGoal(goal, annualRate, years) | goal / (1 + r)^years | Required lumpsum (present value of goal) |
requiredMonthlyInvestmentForGoal params:
{
goalAmountFuture: number,
annualRate: number, // 0.12 = 12%
months: number,
stepUpPercentAnnual?: number, // 0.10 = 10%
investmentAt?: "begin" | "end"
}Returns: { requiredMonthlyInvestment, assumptions }
Loans
| Function | Formula | Description |
|----------|---------|-------------|
| emi(principal, annualRate, months) | EMI = P × r × (1+r)ⁿ / [(1+r)ⁿ − 1] | Equated Monthly Installment |
| amortizationSchedule(params) | Month-by-month principal/interest split | Full loan schedule with extra payments |
| prepaymentImpact(params) | Compares original vs prepaid loan | Interest saved and tenure reduction |
EMI formula: r = annualRate / 12. When annualRate = 0, EMI = principal / months.
Returns: { emi, totalPayment, totalInterest }
amortizationSchedule params:
{
principal: number,
annualRate: number,
months: number,
extraPaymentMonthly?: number,
extraPayments?: { month: number, amount: number }[]
}Returns: { emi, schedule[], totalInterest, totalPaid, payoffMonth }
prepaymentImpact params:
{
principal: number,
annualRate: number,
months: number,
prepayments: { month: number, amount: number }[],
mode: "reduceTenure" | "reduceEmi"
}reduceTenure— keeps EMI constant, pays off loan earlyreduceEmi— keeps tenure constant, recalculates lower EMI after each prepayment
Returns: { original, new, savings: { interestSaved, monthsSaved } }
Risk Metrics
| Function | Formula | Description |
|----------|---------|-------------|
| volatility(returns, periodsPerYear?) | σ = s(returns) × √periodsPerYear | Annualized volatility (sample std dev) |
| sharpe(returns, riskFreeRate?, periodsPerYear?) | (R̄·T − Rf) / σ | Sharpe Ratio (Sharpe 1994) |
| sortino(returns, riskFreeRate?, periodsPerYear?) | (R̄·T − Rf) / DD | Sortino Ratio (Sortino & Price 1994) |
| maxDrawdown(values) | max((peak − trough) / peak) | Maximum peak-to-trough decline |
Volatility: Uses sample standard deviation (N−1 denominator) × √periodsPerYear for annualization.
Sharpe Ratio: Arithmetic annualization of mean return. SR = (meanReturn × periodsPerYear − riskFreeRateAnnual) / volatility
Sortino Ratio: Downside deviation uses the full sample size N as denominator (per Sortino & Price 1994), not just the count of negative returns:
DD = √(Σ min(rᵢ − MAR, 0)² / N) × √periodsPerYear
Returns Infinity when no downside returns exist (zero downside risk).
Parameters:
returns— array of periodic returns as decimals (e.g., daily: 0.01 = 1%)values— array of portfolio values (for maxDrawdown)periodsPerYear— defaults to 252 (daily trading days)riskFreeRate— annual risk-free rate as decimal
Portfolio Helpers
| Function | Description |
|----------|-------------|
| weightedReturn(returns, weights) | Weighted average return: Σ(rᵢ × wᵢ) |
| rebalance(targetWeights, currentValues) | Calculate trades to reach target allocation |
rebalance returns: { trades, newValues, total } — positive trade = buy, negative = sell
Utilities
| Function | Description |
|----------|-------------|
| safeNum(value, fallback?) | Returns fallback if value is NaN/Infinity/null/undefined |
Migration from v1.2.x
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%).
- sipFutureValue({ monthlyInvestment: 25000, annualRate: 0.12, months: 180, stepUpPercentAnnual: 10 })
+ sipFutureValue({ monthlyInvestment: 25000, annualRate: 0.12, months: 180, stepUpPercentAnnual: 0.10 })
- swpPlan({ initialCorpus: 10000000, monthlyWithdrawal: 50000, annualRate: 0.08, months: 300, inflationRateAnnual: 6 })
+ swpPlan({ initialCorpus: 10000000, monthlyWithdrawal: 50000, annualRate: 0.08, months: 300, inflationRateAnnual: 0.06 })Error Handling
Most functions return safe defaults (0, empty arrays) for invalid inputs, making them safe for reactive UIs. Exceptions:
irr()andxirr()throw anErrorif cash flows don't contain at least one positive and one negative valuesortino()returnsInfinitywhen there are no downside returns
License
MIT
