Module 2 · Financial Arithmetic, Time Value, and Returns Module demo
Loan & Investment Calculator
The decision behind the calculator.
Transcript
34 sentences · select one to jump thereCode lab
Run it yourself
The demo source in one language. Edit it, run TypeScript and Python right here, and compare with the expected output.
/**
* 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