Module 2 · Financial Arithmetic, Time Value, and Returns Module demo

Loan & Investment Calculator

The decision behind the calculator.

4:28 clipUses lessons 11–20Watch on YouTube

Transcript

34 sentences · select one to jump there

Code lab

Run it yourself

The demo source in one language. Edit it, run TypeScript and Python right here, and compare with the expected output.

demo-loan-and-investment-calculator.ts
Start from GitHub
/**
 * Fintech Math Bootcamp · Module 02 demo · Loan & Investment Calculator
 * Sam is buying a 20,000 car. The bank offers 6% APR over 3 years; the dealer offers 0% financing
 * or 1,500 off for paying cash. Which is cheaper, and what happens to the money Sam invests instead?
 * Lessons 011–020: simple and compound interest, present value, discount factors, NPV,
 * simple, log and cumulative returns, arithmetic vs geometric averages, annualization.
 * Synthetic example; not financial advice.
 */

export type Row = {month: number; payment: number; interest: number; principal: number; balance: number};

// 011 · simple interest: interest on the original principal only
export const simpleInterest = (principal: number, annualRate: number, years: number) => principal * annualRate * years;

// 012 · compound growth: interest earns interest every period
export const compound = (principal: number, periodRate: number, periods: number) => principal * (1 + periodRate) ** periods;

// 014 · discount factor for a payment t periods away
export const discountFactor = (periodRate: number, t: number) => (1 + periodRate) ** -t;

// 013 · present value of a level payment stream, solved for the payment that repays the loan
export function levelPayment(principal: number, periodRate: number, periods: number): number {
  if (periods <= 0) throw new Error("Term must be positive");
  if (periodRate === 0) return principal / periods;
  return principal * periodRate / (1 - (1 + periodRate) ** -periods);
}

export function amortize(principal: number, periodRate: number, periods: number): Row[] {
  const payment = levelPayment(principal, periodRate, periods);
  const rows: Row[] = [];
  let balance = principal;
  for (let month = 1; month <= periods; month++) {
    const interest = balance * periodRate;
    const principalPart = payment - interest;
    balance -= principalPart;
    rows.push({month, payment, interest, principal: principalPart, balance: Math.abs(balance) < 1e-8 ? 0 : balance});
  }
  return rows;
}

// 015 · net present value of dated cash flows (flows[0] happens today)
export const npv = (periodRate: number, flows: number[]) => flows.reduce((s, cf, t) => s + cf * discountFactor(periodRate, t), 0);

// 016 · simple total return including a cash dividend
export const simpleReturn = (start: number, end: number, dividend = 0) => (end + dividend - start) / start;

// 017 · log return; log returns add across periods
export const logReturn = (start: number, end: number) => Math.log(end / start);

// 020 · annualize a periodic rate by compounding
export const annualize = (periodRate: number, periodsPerYear: number) => (1 + periodRate) ** periodsPerYear - 1;

export function runDemo() {
  const price = 20000, apr = 0.06, years = 3, months = years * 12, i = apr / 12;

  // the bank loan
  const schedule = amortize(price, i, months);
  const payment = schedule[0].payment;
  const totalInterest = payment * months - price;
  const balanceByYear = [0, 12, 24, 36].map(m => (m === 0 ? price : schedule[m - 1].balance));

  // the dealer: 0% financing or 1,500 off in cash, judged at Sam's 6% cost of money
  const cashDiscount = 1500;
  const zeroPercentPayment = price / months;
  const financingFlows = [0, ...Array.from({length: months}, () => zeroPercentPayment)];
  const pvFinancing = npv(i, financingFlows);
  const financingValue = price - pvFinancing; // what 0% financing is worth today
  const bestChoice = financingValue > cashDiscount ? "0% financing" : "cash discount";

  // investing: a fund that rises 50% then falls 50%, and one year with a dividend
  const prices = [100, 150, 75];
  const simpleReturns = prices.slice(1).map((p, k) => simpleReturn(prices[k], p));
  const logReturns = prices.slice(1).map((p, k) => logReturn(prices[k], p));
  const totalLog = logReturns.reduce((s, r) => s + r, 0);
  const cumulative = simpleReturns.reduce((g, r) => g * (1 + r), 1) - 1; // 018
  const arithmeticMean = simpleReturns.reduce((s, r) => s + r, 0) / simpleReturns.length; // 019
  const geometricMean = (1 + cumulative) ** (1 / simpleReturns.length) - 1; // 019

  return {
    loan: {
      price, apr, years, months, monthlyRate: i, payment, totalPaid: payment * months, totalInterest,
      simpleInterest: simpleInterest(price, apr, years),
      compoundedDebt: compound(price, i, months),
      effectiveAnnualRate: annualize(i, 12),
      firstMonth: schedule[0], lastMonth: schedule[months - 1], balanceByYear,
      interestByYear: [0, 1, 2].map(y => schedule.slice(y * 12, y * 12 + 12).reduce((s, r) => s + r.interest, 0)),
      discountFactors: [1, 12, 24, 36].map(t => discountFactor(i, t)),
    },
    offer: {cashDiscount, zeroPercentPayment, pvFinancing, financingValue, bestChoice},
    fund: {
      prices, simpleReturns, logReturns, totalLog, cumulative, arithmeticMean, geometricMean,
      dividendYear: {start: 100, end: 104, dividend: 2, priceReturn: simpleReturn(100, 104), totalReturn: simpleReturn(100, 104, 2)},
    },
  };
}

export const checkedResult = {"loan":{"price":20000,"apr":0.06,"years":3,"months":36,"monthlyRate":0.005,"payment":608.4387490311142,"totalPaid":21903.794965120112,"totalInterest":1903.7949651201125,"simpleInterest":3600,"compoundedDebt":23933.610496468293,"effectiveAnnualRate":0.06167781186449828,"firstMonth":{"month":1,"payment":608.4387490311142,"interest":100,"principal":508.4387490311142,"balance":19491.561250968887},"lastMonth":{"month":36,"payment":608.4387490311142,"interest":3.027058452888821,"principal":605.4116905782254,"balance":0},"balanceByYear":[20000,13728.122098527487,7069.408491810839,0],"interestByYear":[1029.3870869008556,642.5513816567217,231.8564965620688],"discountFactors":[0.9950248756218907,0.9419053396659192,0.8871856688911706,0.8356449188036736]},"offer":{"cashDiscount":1500,"zeroPercentPayment":555.5555555555555,"pvFinancing":18261.675688481093,"financingValue":1738.324311518907,"bestChoice":"0% financing"},"fund":{"prices":[100,150,75],"simpleReturns":[0.5,-0.5],"logReturns":[0.4054651081081644,-0.6931471805599453],"totalLog":-0.2876820724517809,"cumulative":-0.25,"arithmeticMean":0,"geometricMean":-0.1339745962155614,"dividendYear":{"start":100,"end":104,"dividend":2,"priceReturn":0.04,"totalReturn":0.06}}};

// Run this file directly: npx tsx lessons/02-financial-arithmetic-time-value-and-returns/demo-loan-and-investment-calculator.ts
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/").split("/").pop()!)) {
  console.log(JSON.stringify(runDemo(), null, 2));
}

Your output

Press Run to execute the code in your browser.

Expected output

{
  "loan": {
    "price": 20000,
    "apr": 0.06,
    "years": 3,
    "months": 36,
    "monthlyRate": 0.005,
    "payment": 608.4387490311142,
    "totalPaid": 21903.794965120112,
    "totalInterest": 1903.7949651201125,
    "simpleInterest": 3600,
    "compoundedDebt": 23933.610496468293,
    "effectiveAnnualRate": 0.06167781186449828,
    "firstMonth": {
      "month": 1,
      "payment": 608.4387490311142,
      "interest": 100,
      "principal": 508.4387490311142,
      "balance": 19491.561250968887
    },
    "lastMonth": {
      "month": 36,
      "payment": 608.4387490311142,
      "interest": 3.027058452888821,
      "principal": 605.4116905782254,
      "balance": 0
    },
    "balanceByYear": [
      20000,
      13728.122098527487,
      7069.408491810839,
      0
    ],
    "interestByYear": [
      1029.3870869008556,
      642.5513816567217,
      231.8564965620688
    ],
    "discountFactors": [
      0.9950248756218907,
      0.9419053396659192,
      0.8871856688911706,
      0.8356449188036736
    ]
  },
  "offer": {
    "cashDiscount": 1500,
    "zeroPercentPayment": 555.5555555555555,
    "pvFinancing": 18261.675688481093,
    "financingValue": 1738.324311518907,
    "bestChoice": "0% financing"
  },
  "fund": {
    "prices": [
      100,
      150,
      75
    ],
    "simpleReturns": [
      0.5,
      -0.5
    ],
    "logReturns": [
      0.4054651081081644,
      -0.6931471805599453
    ],
    "totalLog": -0.2876820724517809,
    "cumulative": -0.25,
    "arithmeticMean": 0,
    "geometricMean": -0.1339745962155614,
    "dividendYear": {
      "start": 100,
      "end": 104,
      "dividend": 2,
      "priceReturn": 0.04,
      "totalReturn": 0.06
    }
  }
}

Prefer your own machine? Every file is in the course repository · open it in Codespaces.

What the demo does

Sam buys a 20,000 car: a 6% bank loan, 0% dealer financing or a 1,500 cash discount, then investing what is saved. Every module 02 lesson becomes one calculator feature.