Financial Calculations in Java

Overview

Financial calculations require precision, accuracy, and proper handling of numerical operations. Java provides several libraries and techniques for performing financial computations correctly.

Core Principles

  • Use BigDecimal for monetary values - avoid double/float for money
  • Proper rounding - follow financial rounding rules
  • Date accuracy - handle time periods and compounding correctly
  • Performance - optimize calculations for high-frequency trading

Basic Financial Calculations

1. BigDecimal for Monetary Values

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
public class MonetaryCalculations {
public static void basicBigDecimalOperations() {
// Always use String constructor for precise initialization
BigDecimal price = new BigDecimal("99.99");
BigDecimal quantity = new BigDecimal("2.5");
BigDecimal taxRate = new BigDecimal("0.0825"); // 8.25%
// Multiplication
BigDecimal subtotal = price.multiply(quantity);
// Division with rounding
BigDecimal tax = subtotal.multiply(taxRate)
.setScale(2, RoundingMode.HALF_UP);
// Addition
BigDecimal total = subtotal.add(tax);
System.out.println("Subtotal: $" + subtotal);
System.out.println("Tax: $" + tax);
System.out.println("Total: $" + total);
}
public static BigDecimal calculateCompoundInterest(BigDecimal principal,
BigDecimal annualRate,
int years,
int compoundingPeriodsPerYear) {
// A = P(1 + r/n)^(nt)
BigDecimal ratePerPeriod = annualRate.divide(
new BigDecimal(compoundingPeriodsPerYear), 10, RoundingMode.HALF_UP);
BigDecimal base = BigDecimal.ONE.add(ratePerPeriod);
int exponent = compoundingPeriodsPerYear * years;
// Using BigDecimal's pow method for integer exponents
BigDecimal multiplier = base.pow(exponent);
return principal.multiply(multiplier)
.setScale(2, RoundingMode.HALF_UP);
}
}

2. Time Value of Money (TVM) Calculations

import java.math.BigDecimal;
import java.math.MathContext;
public class TimeValueOfMoney {
// Present Value of a single future amount
public static BigDecimal presentValue(BigDecimal futureValue,
BigDecimal annualRate,
int periods) {
// PV = FV / (1 + r)^n
BigDecimal denominator = BigDecimal.ONE.add(annualRate).pow(periods);
return futureValue.divide(denominator, MathContext.DECIMAL128)
.setScale(4, RoundingMode.HALF_UP);
}
// Future Value of a single present amount
public static BigDecimal futureValue(BigDecimal presentValue,
BigDecimal annualRate,
int periods) {
// FV = PV * (1 + r)^n
BigDecimal multiplier = BigDecimal.ONE.add(annualRate).pow(periods);
return presentValue.multiply(multiplier)
.setScale(4, RoundingMode.HALF_UP);
}
// Present Value of Annuity
public static BigDecimal presentValueAnnuity(BigDecimal payment,
BigDecimal ratePerPeriod,
int numberOfPeriods) {
// PVA = PMT * [1 - (1 + r)^-n] / r
if (ratePerPeriod.compareTo(BigDecimal.ZERO) == 0) {
return payment.multiply(new BigDecimal(numberOfPeriods));
}
BigDecimal onePlusR = BigDecimal.ONE.add(ratePerPeriod);
BigDecimal discountFactor = onePlusR.pow(-numberOfPeriods, MathContext.DECIMAL128);
BigDecimal numerator = BigDecimal.ONE.subtract(discountFactor);
return payment.multiply(numerator)
.divide(ratePerPeriod, MathContext.DECIMAL128)
.setScale(4, RoundingMode.HALF_UP);
}
// Future Value of Annuity
public static BigDecimal futureValueAnnuity(BigDecimal payment,
BigDecimal ratePerPeriod,
int numberOfPeriods) {
// FVA = PMT * [(1 + r)^n - 1] / r
if (ratePerPeriod.compareTo(BigDecimal.ZERO) == 0) {
return payment.multiply(new BigDecimal(numberOfPeriods));
}
BigDecimal onePlusR = BigDecimal.ONE.add(ratePerPeriod);
BigDecimal growthFactor = onePlusR.pow(numberOfPeriods);
BigDecimal numerator = growthFactor.subtract(BigDecimal.ONE);
return payment.multiply(numerator)
.divide(ratePerPeriod, MathContext.DECIMAL128)
.setScale(4, RoundingMode.HALF_UP);
}
}

Advanced Financial Calculations

1. Loan Amortization Schedule

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class LoanAmortization {
public static class AmortizationEntry {
private final int paymentNumber;
private final LocalDate paymentDate;
private final BigDecimal beginningBalance;
private final BigDecimal payment;
private final BigDecimal principal;
private final BigDecimal interest;
private final BigDecimal endingBalance;
public AmortizationEntry(int paymentNumber, LocalDate paymentDate,
BigDecimal beginningBalance, BigDecimal payment,
BigDecimal principal, BigDecimal interest,
BigDecimal endingBalance) {
this.paymentNumber = paymentNumber;
this.paymentDate = paymentDate;
this.beginningBalance = beginningBalance;
this.payment = payment;
this.principal = principal;
this.interest = interest;
this.endingBalance = endingBalance;
}
// Getters
public int getPaymentNumber() { return paymentNumber; }
public LocalDate getPaymentDate() { return paymentDate; }
public BigDecimal getBeginningBalance() { return beginningBalance; }
public BigDecimal getPayment() { return payment; }
public BigDecimal getPrincipal() { return principal; }
public BigDecimal getInterest() { return interest; }
public BigDecimal getEndingBalance() { return endingBalance; }
}
public static List<AmortizationEntry> calculateAmortizationSchedule(
BigDecimal loanAmount, BigDecimal annualInterestRate,
int loanTermYears, LocalDate startDate) {
List<AmortizationEntry> schedule = new ArrayList<>();
BigDecimal monthlyRate = annualInterestRate.divide(
new BigDecimal("12"), 10, RoundingMode.HALF_UP);
int totalPayments = loanTermYears * 12;
// Calculate monthly payment using annuity formula
BigDecimal monthlyPayment = calculateMonthlyPayment(
loanAmount, monthlyRate, totalPayments);
BigDecimal currentBalance = loanAmount;
LocalDate currentDate = startDate;
for (int paymentNum = 1; paymentNum <= totalPayments; paymentNum++) {
BigDecimal interestPayment = currentBalance.multiply(monthlyRate)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal principalPayment = monthlyPayment.subtract(interestPayment);
// For last payment, adjust principal to match remaining balance
if (paymentNum == totalPayments) {
principalPayment = currentBalance;
monthlyPayment = principalPayment.add(interestPayment);
}
BigDecimal endingBalance = currentBalance.subtract(principalPayment);
AmortizationEntry entry = new AmortizationEntry(
paymentNum, currentDate, currentBalance, monthlyPayment,
principalPayment, interestPayment, endingBalance);
schedule.add(entry);
currentBalance = endingBalance;
currentDate = currentDate.plusMonths(1);
}
return schedule;
}
private static BigDecimal calculateMonthlyPayment(BigDecimal loanAmount,
BigDecimal monthlyRate,
int totalPayments) {
if (monthlyRate.compareTo(BigDecimal.ZERO) == 0) {
return loanAmount.divide(new BigDecimal(totalPayments), 
RoundingMode.HALF_UP);
}
BigDecimal onePlusR = BigDecimal.ONE.add(monthlyRate);
BigDecimal discountFactor = onePlusR.pow(-totalPayments, MathContext.DECIMAL128);
BigDecimal numerator = monthlyRate.multiply(loanAmount);
BigDecimal denominator = BigDecimal.ONE.subtract(discountFactor);
return numerator.divide(denominator, MathContext.DECIMAL128)
.setScale(2, RoundingMode.HALF_UP);
}
public static void printAmortizationSchedule(List<AmortizationEntry> schedule) {
System.out.printf("%-4s %-12s %-12s %-10s %-10s %-10s %-12s%n",
"Pmt", "Date", "Beg Balance", "Payment", "Principal", "Interest", "End Balance");
for (AmortizationEntry entry : schedule) {
System.out.printf("%-4d %-12s %-12.2f %-10.2f %-10.2f %-10.2f %-12.2f%n",
entry.getPaymentNumber(),
entry.getPaymentDate().toString(),
entry.getBeginningBalance(),
entry.getPayment(),
entry.getPrincipal(),
entry.getInterest(),
entry.getEndingBalance());
}
}
}

2. Bond Valuation

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class BondValuation {
public static class Bond {
private final BigDecimal faceValue;
private final BigDecimal couponRate;
private final int paymentsPerYear;
private final LocalDate maturityDate;
private final LocalDate issueDate;
public Bond(BigDecimal faceValue, BigDecimal couponRate,
int paymentsPerYear, LocalDate maturityDate, LocalDate issueDate) {
this.faceValue = faceValue;
this.couponRate = couponRate;
this.paymentsPerYear = paymentsPerYear;
this.maturityDate = maturityDate;
this.issueDate = issueDate;
}
// Getters
public BigDecimal getFaceValue() { return faceValue; }
public BigDecimal getCouponRate() { return couponRate; }
public int getPaymentsPerYear() { return paymentsPerYear; }
public LocalDate getMaturityDate() { return maturityDate; }
public LocalDate getIssueDate() { return issueDate; }
}
// Calculate bond price using present value of cash flows
public static BigDecimal calculateBondPrice(Bond bond, BigDecimal marketRate,
LocalDate valuationDate) {
BigDecimal couponPayment = bond.getFaceValue()
.multiply(bond.getCouponRate())
.divide(new BigDecimal(bond.getPaymentsPerYear()), 
MathContext.DECIMAL128);
long totalPeriods = ChronoUnit.MONTHS.between(
bond.getIssueDate(), bond.getMaturityDate()) / (12 / bond.getPaymentsPerYear());
long periodsRemaining = ChronoUnit.MONTHS.between(
valuationDate, bond.getMaturityDate()) / (12 / bond.getPaymentsPerYear());
BigDecimal ratePerPeriod = marketRate.divide(
new BigDecimal(bond.getPaymentsPerYear()), MathContext.DECIMAL128);
// Present value of coupon payments (annuity)
BigDecimal pvCoupons = TimeValueOfMoney.presentValueAnnuity(
couponPayment, ratePerPeriod, (int) periodsRemaining);
// Present value of face value
BigDecimal pvFaceValue = bond.getFaceValue().divide(
BigDecimal.ONE.add(ratePerPeriod).pow((int) periodsRemaining),
MathContext.DECIMAL128);
return pvCoupons.add(pvFaceValue).setScale(4, RoundingMode.HALF_UP);
}
// Calculate Yield to Maturity using Newton-Raphson method
public static BigDecimal calculateYTM(Bond bond, BigDecimal currentPrice,
LocalDate valuationDate) {
final BigDecimal PRECISION = new BigDecimal("0.000001");
final int MAX_ITERATIONS = 100;
BigDecimal ytm = bond.getCouponRate(); // Initial guess
BigDecimal priceDiff;
int iterations = 0;
do {
BigDecimal calculatedPrice = calculateBondPrice(bond, ytm, valuationDate);
priceDiff = calculatedPrice.subtract(currentPrice);
// Calculate derivative (price sensitivity to yield)
BigDecimal derivative = calculateBondPriceDerivative(bond, ytm, valuationDate);
// Newton-Raphson: y_{n+1} = y_n - f(y_n)/f'(y_n)
ytm = ytm.subtract(priceDiff.divide(derivative, MathContext.DECIMAL128));
iterations++;
} while (priceDiff.abs().compareTo(PRECISION) > 0 && iterations < MAX_ITERATIONS);
return ytm.setScale(6, RoundingMode.HALF_UP);
}
private static BigDecimal calculateBondPriceDerivative(Bond bond, BigDecimal yield,
LocalDate valuationDate) {
// Numerical derivative calculation
BigDecimal h = new BigDecimal("0.0001");
BigDecimal price1 = calculateBondPrice(bond, yield.add(h), valuationDate);
BigDecimal price2 = calculateBondPrice(bond, yield.subtract(h), valuationDate);
return price1.subtract(price2).divide(h.multiply(new BigDecimal("2")), 
MathContext.DECIMAL128);
}
}

3. Portfolio Analysis

import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Arrays;
import java.util.List;
public class PortfolioAnalysis {
public static class AssetReturn {
private final String asset;
private final BigDecimal[] returns;
public AssetReturn(String asset, BigDecimal[] returns) {
this.asset = asset;
this.returns = returns;
}
public String getAsset() { return asset; }
public BigDecimal[] getReturns() { return returns; }
}
// Calculate portfolio expected return
public static BigDecimal portfolioExpectedReturn(BigDecimal[] weights,
BigDecimal[] expectedReturns) {
if (weights.length != expectedReturns.length) {
throw new IllegalArgumentException("Weights and returns arrays must have same length");
}
BigDecimal portfolioReturn = BigDecimal.ZERO;
for (int i = 0; i < weights.length; i++) {
portfolioReturn = portfolioReturn.add(
weights[i].multiply(expectedReturns[i]));
}
return portfolioReturn.setScale(6, RoundingMode.HALF_UP);
}
// Calculate portfolio variance
public static BigDecimal portfolioVariance(BigDecimal[] weights,
BigDecimal[][] covarianceMatrix) {
int n = weights.length;
BigDecimal variance = BigDecimal.ZERO;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
BigDecimal term = weights[i].multiply(weights[j])
.multiply(covarianceMatrix[i][j]);
variance = variance.add(term);
}
}
return variance.setScale(6, RoundingMode.HALF_UP);
}
// Calculate covariance matrix
public static BigDecimal[][] covarianceMatrix(List<AssetReturn> assetReturns) {
int n = assetReturns.size();
BigDecimal[][] covMatrix = new BigDecimal[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
covMatrix[i][j] = covariance(
assetReturns.get(i).getReturns(),
assetReturns.get(j).getReturns());
}
}
return covMatrix;
}
// Calculate covariance between two return series
public static BigDecimal covariance(BigDecimal[] returnsX, BigDecimal[] returnsY) {
if (returnsX.length != returnsY.length) {
throw new IllegalArgumentException("Return arrays must have same length");
}
int n = returnsX.length;
BigDecimal meanX = mean(returnsX);
BigDecimal meanY = mean(returnsY);
BigDecimal sum = BigDecimal.ZERO;
for (int i = 0; i < n; i++) {
BigDecimal diffX = returnsX[i].subtract(meanX);
BigDecimal diffY = returnsY[i].subtract(meanY);
sum = sum.add(diffX.multiply(diffY));
}
return sum.divide(new BigDecimal(n - 1), MathContext.DECIMAL128)
.setScale(6, RoundingMode.HALF_UP);
}
// Calculate mean of returns
public static BigDecimal mean(BigDecimal[] returns) {
BigDecimal sum = Arrays.stream(returns)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(returns.length), MathContext.DECIMAL128)
.setScale(6, RoundingMode.HALF_UP);
}
// Calculate standard deviation
public static BigDecimal standardDeviation(BigDecimal[] returns) {
BigDecimal variance = variance(returns);
return sqrt(variance).setScale(6, RoundingMode.HALF_UP);
}
// Calculate variance
public static BigDecimal variance(BigDecimal[] returns) {
BigDecimal mean = mean(returns);
BigDecimal sumSq = Arrays.stream(returns)
.map(r -> r.subtract(mean).pow(2))
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sumSq.divide(new BigDecimal(returns.length - 1), MathContext.DECIMAL128)
.setScale(6, RoundingMode.HALF_UP);
}
// Sharpe Ratio
public static BigDecimal sharpeRatio(BigDecimal portfolioReturn,
BigDecimal riskFreeRate,
BigDecimal portfolioStdDev) {
BigDecimal excessReturn = portfolioReturn.subtract(riskFreeRate);
return excessReturn.divide(portfolioStdDev, MathContext.DECIMAL128)
.setScale(4, RoundingMode.HALF_UP);
}
// Square root implementation for BigDecimal
private static BigDecimal sqrt(BigDecimal value) {
if (value.compareTo(BigDecimal.ZERO) < 0) {
throw new ArithmeticException("Square root of negative number");
}
BigDecimal x = new BigDecimal(Math.sqrt(value.doubleValue()));
return x.add(value.divide(x, MathContext.DECIMAL128))
.divide(new BigDecimal("2"), MathContext.DECIMAL128);
}
}

Financial Derivatives

1. Black-Scholes Option Pricing

import java.math.BigDecimal;
import java.math.MathContext;
public class BlackScholes {
// Black-Scholes option pricing model
public static BigDecimal callOptionPrice(BigDecimal spotPrice,
BigDecimal strikePrice,
BigDecimal timeToExpiration,
BigDecimal riskFreeRate,
BigDecimal volatility) {
BigDecimal d1 = calculateD1(spotPrice, strikePrice, timeToExpiration, 
riskFreeRate, volatility);
BigDecimal d2 = d1.subtract(volatility.multiply(sqrt(timeToExpiration)));
BigDecimal nd1 = cumulativeDistribution(d1);
BigDecimal nd2 = cumulativeDistribution(d2);
BigDecimal firstTerm = spotPrice.multiply(nd1);
BigDecimal discountFactor = exp(riskFreeRate.negate().multiply(timeToExpiration));
BigDecimal secondTerm = strikePrice.multiply(discountFactor).multiply(nd2);
return firstTerm.subtract(secondTerm).setScale(4, RoundingMode.HALF_UP);
}
public static BigDecimal putOptionPrice(BigDecimal spotPrice,
BigDecimal strikePrice,
BigDecimal timeToExpiration,
BigDecimal riskFreeRate,
BigDecimal volatility) {
// Using put-call parity: P = C - S + K * e^(-rT)
BigDecimal callPrice = callOptionPrice(spotPrice, strikePrice, 
timeToExpiration, riskFreeRate, volatility);
BigDecimal discountFactor = exp(riskFreeRate.negate().multiply(timeToExpiration));
return callPrice.subtract(spotPrice)
.add(strikePrice.multiply(discountFactor))
.setScale(4, RoundingMode.HALF_UP);
}
private static BigDecimal calculateD1(BigDecimal spotPrice, BigDecimal strikePrice,
BigDecimal timeToExpiration,
BigDecimal riskFreeRate, BigDecimal volatility) {
BigDecimal volSqrtT = volatility.multiply(sqrt(timeToExpiration));
BigDecimal numerator = ln(spotPrice.divide(strikePrice, MathContext.DECIMAL128))
.add(riskFreeRate.add(volatility.pow(2).divide(new BigDecimal("2")))
.multiply(timeToExpiration));
return numerator.divide(volSqrtT, MathContext.DECIMAL128);
}
// Greeks
public static BigDecimal deltaCall(BigDecimal spotPrice, BigDecimal strikePrice,
BigDecimal timeToExpiration,
BigDecimal riskFreeRate, BigDecimal volatility) {
BigDecimal d1 = calculateD1(spotPrice, strikePrice, timeToExpiration,
riskFreeRate, volatility);
return cumulativeDistribution(d1).setScale(4, RoundingMode.HALF_UP);
}
public static BigDecimal gamma(BigDecimal spotPrice, BigDecimal strikePrice,
BigDecimal timeToExpiration,
BigDecimal riskFreeRate, BigDecimal volatility) {
BigDecimal d1 = calculateD1(spotPrice, strikePrice, timeToExpiration,
riskFreeRate, volatility);
BigDecimal pdfD1 = probabilityDensity(d1);
BigDecimal volSqrtT = volatility.multiply(sqrt(timeToExpiration));
return pdfD1.divide(spotPrice.multiply(volSqrtT), MathContext.DECIMAL128)
.setScale(4, RoundingMode.HALF_UP);
}
// Helper mathematical functions
private static BigDecimal sqrt(BigDecimal value) {
// Use more precise sqrt implementation
return new BigDecimal(Math.sqrt(value.doubleValue()));
}
private static BigDecimal exp(BigDecimal value) {
return new BigDecimal(Math.exp(value.doubleValue()));
}
private static BigDecimal ln(BigDecimal value) {
return new BigDecimal(Math.log(value.doubleValue()));
}
// Cumulative distribution function for standard normal
private static BigDecimal cumulativeDistribution(BigDecimal x) {
return new BigDecimal(0.5 * (1 + erf(x.doubleValue() / Math.sqrt(2))));
}
// Probability density function for standard normal
private static BigDecimal probabilityDensity(BigDecimal x) {
double xVal = x.doubleValue();
double pdf = Math.exp(-0.5 * xVal * xVal) / Math.sqrt(2 * Math.PI);
return new BigDecimal(pdf);
}
// Error function approximation
private static double erf(double x) {
// Abramowitz and Stegun approximation
double t = 1.0 / (1.0 + 0.47047 * Math.abs(x));
double poly = t * (0.3480242 + t * (-0.0958798 + t * 0.7478556));
double ans = 1.0 - poly * Math.exp(-x * x);
return x >= 0 ? ans : -ans;
}
}

Financial Libraries and Best Practices

1. Using Joda-Money for Monetary Values

// Add to pom.xml:
// <dependency>
//     <groupId>org.joda</groupId>
//     <artifactId>joda-money</artifactId>
//     <version>1.0.1</version>
// </dependency>
import org.joda.money.BigMoney;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
public class JodaMoneyExamples {
public static void jodaMoneyUsage() {
// Creating monetary values
Money price = Money.of(CurrencyUnit.USD, 99.99);
Money quantity = Money.of(CurrencyUnit.USD, 2.5);
// Operations
Money subtotal = price.multipliedBy(quantity.getAmount());
Money tax = subtotal.multipliedBy(0.0825); // 8.25%
Money total = subtotal.plus(tax);
System.out.println("Total: " + total);
// Using BigMoney for high precision
BigMoney preciseAmount = BigMoney.of(CurrencyUnit.USD, new BigDecimal("123456.789012"));
BigMoney interest = preciseAmount.multipliedBy(new BigDecimal("0.05"));
}
}

2. Financial Calculation Best Practices

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
public class FinancialBestPractices {
// 1. Use proper precision and rounding
public static final MathContext FINANCIAL_CONTEXT = new MathContext(10, RoundingMode.HALF_UP);
public static final int FINANCIAL_SCALE = 4;
// 2. Currency handling
public static class CurrencyUtils {
public static BigDecimal normalizeCurrency(BigDecimal amount) {
return amount.setScale(2, RoundingMode.HALF_UP);
}
public static BigDecimal parseMoney(String amountStr) {
return new BigDecimal(amountStr.replaceAll("[$,]", ""));
}
public static String formatMoney(BigDecimal amount) {
return String.format("$%,.2f", amount);
}
}
// 3. Input validation
public static void validateFinancialInput(BigDecimal amount, String fieldName) {
if (amount == null) {
throw new IllegalArgumentException(fieldName + " cannot be null");
}
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException(fieldName + " cannot be negative");
}
}
// 4. Interest rate validation
public static void validateInterestRate(BigDecimal rate) {
if (rate.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Interest rate cannot be negative");
}
if (rate.compareTo(new BigDecimal("1.0")) > 0) {
throw new IllegalArgumentException("Interest rate cannot exceed 100%");
}
}
// 5. Safe division
public static BigDecimal safeDivide(BigDecimal numerator, BigDecimal denominator) {
if (denominator.compareTo(BigDecimal.ZERO) == 0) {
throw new ArithmeticException("Division by zero");
}
return numerator.divide(denominator, FINANCIAL_CONTEXT);
}
// 6. Compound annual growth rate (CAGR)
public static BigDecimal calculateCAGR(BigDecimal beginningValue,
BigDecimal endingValue,
int years) {
if (beginningValue.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Beginning value must be positive");
}
BigDecimal ratio = endingValue.divide(beginningValue, FINANCIAL_CONTEXT);
BigDecimal power = BigDecimal.ONE.divide(new BigDecimal(years), FINANCIAL_CONTEXT);
// CAGR = (EndingValue/BeginningValue)^(1/years) - 1
return pow(ratio, power).subtract(BigDecimal.ONE)
.setScale(FINANCIAL_SCALE, RoundingMode.HALF_UP);
}
// Power function for BigDecimal
private static BigDecimal pow(BigDecimal base, BigDecimal exponent) {
return new BigDecimal(Math.pow(base.doubleValue(), exponent.doubleValue()));
}
}

3. Performance Optimization

import java.math.BigDecimal;
import java.util.concurrent.ConcurrentHashMap;
public class FinancialCalculatorCache {
private final ConcurrentHashMap<String, BigDecimal> cache = 
new ConcurrentHashMap<>();
// Cache for expensive calculations
public BigDecimal cachedPresentValue(BigDecimal futureValue,
BigDecimal rate,
int periods) {
String key = String.format("PV_%s_%s_%d", futureValue, rate, periods);
return cache.computeIfAbsent(key, k -> 
TimeValueOfMoney.presentValue(futureValue, rate, periods));
}
// Precompute common factors
public static class PrecomputedFactors {
private static final BigDecimal[] DISCOUNT_FACTORS = new BigDecimal[1000];
static {
// Precompute discount factors for common rates and periods
for (int i = 0; i < DISCOUNT_FACTORS.length; i++) {
DISCOUNT_FACTORS[i] = BigDecimal.ONE.divide(
BigDecimal.ONE.add(new BigDecimal("0.01").multiply(new BigDecimal(i))),
MathContext.DECIMAL128);
}
}
public static BigDecimal getDiscountFactor(int basisPoints) {
if (basisPoints >= 0 && basisPoints < DISCOUNT_FACTORS.length) {
return DISCOUNT_FACTORS[basisPoints];
}
// Calculate dynamically for uncommon values
return BigDecimal.ONE.divide(
BigDecimal.ONE.add(new BigDecimal(basisPoints).divide(new BigDecimal("10000"))),
MathContext.DECIMAL128);
}
}
}

Testing Financial Calculations

import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
public class FinancialCalculationsTest {
@Test
void testPresentValue() {
BigDecimal futureValue = new BigDecimal("1000");
BigDecimal rate = new BigDecimal("0.05");
int periods = 5;
BigDecimal pv = TimeValueOfMoney.presentValue(futureValue, rate, periods);
// PV of $1000 at 5% for 5 years should be approximately $783.53
assertEquals(0, new BigDecimal("783.53").compareTo(pv.setScale(2, RoundingMode.HALF_UP)));
}
@Test
void testLoanAmortization() {
BigDecimal loanAmount = new BigDecimal("100000");
BigDecimal annualRate = new BigDecimal("0.06");
int years = 30;
var schedule = LoanAmortization.calculateAmortizationSchedule(
loanAmount, annualRate, years, java.time.LocalDate.now());
assertEquals(360, schedule.size()); // 30 years * 12 months
// First payment should have more interest than principal
var firstPayment = schedule.get(0);
assertTrue(firstPayment.getInterest().compareTo(firstPayment.getPrincipal()) > 0);
// Last payment should have more principal than interest
var lastPayment = schedule.get(schedule.size() - 1);
assertTrue(lastPayment.getPrincipal().compareTo(lastPayment.getInterest()) > 0);
}
@Test
void testBlackScholes() {
BigDecimal spotPrice = new BigDecimal("100");
BigDecimal strikePrice = new BigDecimal("100");
BigDecimal timeToExpiration = new BigDecimal("1.0"); // 1 year
BigDecimal riskFreeRate = new BigDecimal("0.05");
BigDecimal volatility = new BigDecimal("0.2");
BigDecimal callPrice = BlackScholes.callOptionPrice(
spotPrice, strikePrice, timeToExpiration, riskFreeRate, volatility);
// For at-the-money options, call price should be positive
assertTrue(callPrice.compareTo(BigDecimal.ZERO) > 0);
// Put-call parity should hold
BigDecimal putPrice = BlackScholes.putOptionPrice(
spotPrice, strikePrice, timeToExpiration, riskFreeRate, volatility);
BigDecimal putCallDifference = putPrice.subtract(callPrice)
.add(spotPrice)
.subtract(strikePrice.multiply(
BigDecimal.ONE.divide(BigDecimal.ONE.add(riskFreeRate), 
MathContext.DECIMAL128)));
// Difference should be very close to zero
assertTrue(putCallDifference.abs().compareTo(new BigDecimal("0.01")) < 0);
}
}

Financial calculations in Java require careful attention to precision, performance, and numerical stability. By using BigDecimal for monetary values, implementing proper rounding, and following financial mathematics principles, you can build robust financial applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper