Files
calculation/ts/calculation.classes.financial.ts
Juergen Kunz d63339cb71
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
feat(core): initial release of financial calculation package with decimal precision
2025-07-29 09:20:06 +00:00

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));
}
}