Some checks failed
CI Pipeline (nottags) / security (push) Successful in 17s
CI Pipeline (tags) / security (push) Successful in 17s
CI Pipeline (nottags) / test (push) Failing after 52s
CI Pipeline (tags) / test (push) Failing after 50s
CI Pipeline (tags) / release (push) Has been skipped
CI Pipeline (tags) / metadata (push) Has been skipped
325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
import * as plugins from './calculation.plugins.js';
|
|
import { Calculator } from './calculation.classes.calculator.js';
|
|
|
|
export interface ICashFlow {
|
|
amount: plugins.Decimal.Value;
|
|
period?: number;
|
|
}
|
|
|
|
/**
|
|
* Financial calculations class providing time value of money and investment analysis functions
|
|
*/
|
|
export class Financial extends Calculator {
|
|
constructor() {
|
|
super({ precision: 15 });
|
|
}
|
|
|
|
/**
|
|
* Calculate Present Value (PV)
|
|
* @param futureValue The future value
|
|
* @param rate The interest rate per period
|
|
* @param periods The number of periods
|
|
*/
|
|
public presentValue(
|
|
futureValue: plugins.Decimal.Value,
|
|
rate: plugins.Decimal.Value,
|
|
periods: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const r = this.decimal(rate);
|
|
const n = this.decimal(periods);
|
|
const fv = this.decimal(futureValue);
|
|
|
|
const denominator = this.power(this.add(1, r), n);
|
|
return this.divide(fv, denominator);
|
|
}
|
|
|
|
/**
|
|
* Calculate Future Value (FV)
|
|
* @param presentValue The present value
|
|
* @param rate The interest rate per period
|
|
* @param periods The number of periods
|
|
*/
|
|
public futureValue(
|
|
presentValue: plugins.Decimal.Value,
|
|
rate: plugins.Decimal.Value,
|
|
periods: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const r = this.decimal(rate);
|
|
const n = this.decimal(periods);
|
|
const pv = this.decimal(presentValue);
|
|
|
|
const multiplier = this.power(this.add(1, r), n);
|
|
return this.multiply(pv, multiplier);
|
|
}
|
|
|
|
/**
|
|
* Calculate Payment (PMT) for a loan
|
|
* @param principal The loan principal
|
|
* @param rate The interest rate per period
|
|
* @param periods The number of periods
|
|
*/
|
|
public payment(
|
|
principal: plugins.Decimal.Value,
|
|
rate: plugins.Decimal.Value,
|
|
periods: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const r = this.decimal(rate);
|
|
const n = this.decimal(periods);
|
|
const p = this.decimal(principal);
|
|
|
|
if (r.isZero()) {
|
|
return this.divide(p, n);
|
|
}
|
|
|
|
const numerator = this.multiply(p, r);
|
|
const denominator = this.subtract(1, this.power(this.add(1, r), this.multiply(-1, n)));
|
|
|
|
return this.divide(numerator, denominator);
|
|
}
|
|
|
|
/**
|
|
* Calculate Net Present Value (NPV)
|
|
* @param rate The discount rate
|
|
* @param cashFlows Array of cash flows (first element is initial investment, usually negative)
|
|
*/
|
|
public npv(rate: plugins.Decimal.Value, cashFlows: plugins.Decimal.Value[]): plugins.Decimal {
|
|
const r = this.decimal(rate);
|
|
|
|
return cashFlows.reduce<plugins.Decimal>((npv, cashFlow, period) => {
|
|
const cf = this.decimal(cashFlow);
|
|
const discountFactor = this.power(this.add(1, r), period);
|
|
const presentValue = this.divide(cf, discountFactor);
|
|
return this.add(npv, presentValue);
|
|
}, this.decimal(0));
|
|
}
|
|
|
|
/**
|
|
* Calculate Internal Rate of Return (IRR) using Newton-Raphson method
|
|
* @param cashFlows Array of cash flows
|
|
* @param guess Initial guess for IRR (default: 0.1)
|
|
* @param tolerance Convergence tolerance (default: 0.000001)
|
|
* @param maxIterations Maximum iterations (default: 1000)
|
|
*/
|
|
public irr(
|
|
cashFlows: plugins.Decimal.Value[],
|
|
guess: plugins.Decimal.Value = 0.1,
|
|
tolerance: plugins.Decimal.Value = 0.000001,
|
|
maxIterations: number = 1000
|
|
): plugins.Decimal {
|
|
let rate = this.decimal(guess);
|
|
const tol = this.decimal(tolerance);
|
|
|
|
for (let i = 0; i < maxIterations; i++) {
|
|
const npvValue = this.npv(rate, cashFlows);
|
|
const npvDerivative = this.npvDerivative(rate, cashFlows);
|
|
|
|
if (npvDerivative.isZero()) {
|
|
throw new Error('IRR calculation failed: derivative is zero');
|
|
}
|
|
|
|
const newRate = this.subtract(rate, this.divide(npvValue, npvDerivative));
|
|
|
|
if (this.abs(this.subtract(newRate, rate)).lessThan(tol)) {
|
|
return newRate;
|
|
}
|
|
|
|
rate = newRate;
|
|
}
|
|
|
|
throw new Error('IRR calculation failed: maximum iterations reached');
|
|
}
|
|
|
|
/**
|
|
* Calculate the derivative of NPV with respect to rate (for IRR calculation)
|
|
*/
|
|
private npvDerivative(rate: plugins.Decimal.Value, cashFlows: plugins.Decimal.Value[]): plugins.Decimal {
|
|
const r = this.decimal(rate);
|
|
|
|
return cashFlows.reduce<plugins.Decimal>((derivative, cashFlow, period) => {
|
|
if (period === 0) return derivative;
|
|
|
|
const cf = this.decimal(cashFlow);
|
|
const negPeriod = this.decimal(period).neg();
|
|
const onePlusR = this.add(1, r);
|
|
const power = this.add(negPeriod, -1);
|
|
const discountFactor = this.power(onePlusR, power);
|
|
const termDerivative = this.multiply(this.multiply(negPeriod, cf), discountFactor);
|
|
|
|
return this.add(derivative, termDerivative);
|
|
}, this.decimal(0));
|
|
}
|
|
|
|
/**
|
|
* Calculate Modified Internal Rate of Return (MIRR)
|
|
* @param cashFlows Array of cash flows
|
|
* @param financeRate Rate for negative cash flows
|
|
* @param reinvestRate Rate for positive cash flows
|
|
*/
|
|
public mirr(
|
|
cashFlows: plugins.Decimal.Value[],
|
|
financeRate: plugins.Decimal.Value,
|
|
reinvestRate: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const fRate = this.decimal(financeRate);
|
|
const rRate = this.decimal(reinvestRate);
|
|
const n = cashFlows.length - 1;
|
|
|
|
let pvNegative = this.decimal(0);
|
|
let fvPositive = this.decimal(0);
|
|
|
|
cashFlows.forEach((cashFlow, period) => {
|
|
const cf = this.decimal(cashFlow);
|
|
if (cf.lessThan(0)) {
|
|
pvNegative = this.add(pvNegative, this.presentValue(cf, fRate, period));
|
|
} else {
|
|
fvPositive = this.add(fvPositive, this.futureValue(cf, rRate, n - period));
|
|
}
|
|
});
|
|
|
|
if (pvNegative.isZero() || fvPositive.isZero()) {
|
|
throw new Error('MIRR calculation failed: no negative or positive cash flows');
|
|
}
|
|
|
|
const ratio = this.divide(fvPositive, this.abs(pvNegative));
|
|
const result = this.subtract(this.power(ratio, this.divide(1, n)), 1);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculate the number of periods for an investment to reach a target value
|
|
* @param presentValue The present value
|
|
* @param futureValue The future value
|
|
* @param rate The interest rate per period
|
|
*/
|
|
public periods(
|
|
presentValue: plugins.Decimal.Value,
|
|
futureValue: plugins.Decimal.Value,
|
|
rate: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const pv = this.decimal(presentValue);
|
|
const fv = this.decimal(futureValue);
|
|
const r = this.decimal(rate);
|
|
|
|
if (r.isZero()) {
|
|
throw new Error('Cannot calculate periods with zero interest rate');
|
|
}
|
|
|
|
const ratio = this.divide(fv, pv);
|
|
const numerator = this.ln(ratio);
|
|
const denominator = this.ln(this.add(1, r));
|
|
|
|
return this.divide(numerator, denominator);
|
|
}
|
|
|
|
/**
|
|
* Calculate the interest rate given PV, FV, and periods
|
|
* @param presentValue The present value
|
|
* @param futureValue The future value
|
|
* @param periods The number of periods
|
|
*/
|
|
public rate(
|
|
presentValue: plugins.Decimal.Value,
|
|
futureValue: plugins.Decimal.Value,
|
|
periods: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const pv = this.decimal(presentValue);
|
|
const fv = this.decimal(futureValue);
|
|
const n = this.decimal(periods);
|
|
|
|
if (n.isZero()) {
|
|
throw new Error('Cannot calculate rate with zero periods');
|
|
}
|
|
|
|
const ratio = this.divide(fv, pv);
|
|
const exponent = this.divide(1, n);
|
|
const result = this.subtract(this.power(ratio, exponent), 1);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculate Extended Internal Rate of Return (XIRR) for irregular cash flows
|
|
* @param cashFlows Array of cash flow objects with amounts and dates
|
|
* @param dates Array of dates corresponding to cash flows
|
|
* @param guess Initial guess for XIRR (default: 0.1)
|
|
*/
|
|
public xirr(
|
|
cashFlows: plugins.Decimal.Value[],
|
|
dates: Date[],
|
|
guess: plugins.Decimal.Value = 0.1
|
|
): plugins.Decimal {
|
|
if (cashFlows.length !== dates.length) {
|
|
throw new Error('Cash flows and dates arrays must have the same length');
|
|
}
|
|
|
|
const startDate = dates[0];
|
|
const yearFractions = dates.map(date => {
|
|
const daysDiff = (date.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
return this.divide(daysDiff, 365);
|
|
});
|
|
|
|
let rate = this.decimal(guess);
|
|
const tolerance = this.decimal(0.000001);
|
|
const maxIterations = 1000;
|
|
|
|
for (let i = 0; i < maxIterations; i++) {
|
|
const xnpv = this.xnpv(rate, cashFlows, yearFractions);
|
|
const xnpvDerivative = this.xnpvDerivative(rate, cashFlows, yearFractions);
|
|
|
|
if (xnpvDerivative.isZero()) {
|
|
throw new Error('XIRR calculation failed: derivative is zero');
|
|
}
|
|
|
|
const newRate = this.subtract(rate, this.divide(xnpv, xnpvDerivative));
|
|
|
|
if (this.abs(this.subtract(newRate, rate)).lessThan(tolerance)) {
|
|
return newRate;
|
|
}
|
|
|
|
rate = newRate;
|
|
}
|
|
|
|
throw new Error('XIRR calculation failed: maximum iterations reached');
|
|
}
|
|
|
|
/**
|
|
* Calculate XNPV for irregular cash flows
|
|
*/
|
|
private xnpv(
|
|
rate: plugins.Decimal.Value,
|
|
cashFlows: plugins.Decimal.Value[],
|
|
yearFractions: plugins.Decimal[]
|
|
): plugins.Decimal {
|
|
const r = this.decimal(rate);
|
|
|
|
return cashFlows.reduce<plugins.Decimal>((npv, cashFlow, index) => {
|
|
const cf = this.decimal(cashFlow);
|
|
const t = yearFractions[index];
|
|
const discountFactor = this.power(this.add(1, r), t);
|
|
const presentValue = this.divide(cf, discountFactor);
|
|
return this.add(npv, presentValue);
|
|
}, this.decimal(0));
|
|
}
|
|
|
|
/**
|
|
* Calculate the derivative of XNPV
|
|
*/
|
|
private xnpvDerivative(
|
|
rate: plugins.Decimal.Value,
|
|
cashFlows: plugins.Decimal.Value[],
|
|
yearFractions: plugins.Decimal[]
|
|
): plugins.Decimal {
|
|
const r = this.decimal(rate);
|
|
|
|
return cashFlows.reduce<plugins.Decimal>((derivative, cashFlow, index) => {
|
|
const cf = this.decimal(cashFlow);
|
|
const t = yearFractions[index];
|
|
const negT = this.multiply(-1, t);
|
|
const discountFactor = this.power(this.add(1, r), this.subtract(negT, 1));
|
|
const termDerivative = this.multiply(this.multiply(negT, cf), discountFactor);
|
|
|
|
return this.add(derivative, termDerivative);
|
|
}, this.decimal(0));
|
|
}
|
|
} |