feat(core): initial release of financial calculation package with decimal precision
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
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
This commit is contained in:
325
ts/calculation.classes.financial.ts
Normal file
325
ts/calculation.classes.financial.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
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));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user