Skip to content

Instantly share code, notes, and snippets.

@RP-3
Last active April 11, 2025 17:28
Show Gist options
  • Save RP-3/8e08f502e63d05e94c847feeaf656d2a to your computer and use it in GitHub Desktop.
Save RP-3/8e08f502e63d05e94c847feeaf656d2a to your computer and use it in GitHub Desktop.
Given a few test cases from bank calculators, find the best params to simulate borrowing power
/**
* Calculates the maximum borrowing power based on income and various parameters
* @param incomeAfterTax - Annual income after tax
* @param annualExpenses - Annual living expenses (default: 24000)
* @param baseRate - Base interest rate for the loan (default: 0.0619)
* @param bufferRate - Additional buffer rate added to base rate for stress testing (default: 0.038)
* @param loanPeriodYears - Duration of the loan in years (default: 30)
* @param marginPercentage - Percentage of income used as margin (default: 0.000)
* @param marginFloor - Minimum margin amount regardless of income (default: 7000)
* @param marginCeiling - Maximum margin amount regardless of income (default: 0)
* @returns The maximum borrowing power as a number
*/
export function calculateBorrowingPower(
incomeAfterTax: number,
annualExpenses: number = 24000,
baseRate: number = 0.0619,
bufferRate: number = 0.038,
loanPeriodYears: number = 30,
marginPercentage: number = 0.000,
marginFloor: number = 7000,
marginCeiling: number = 0
): number {
const effectiveAnnualRate = baseRate + bufferRate;
const monthlyRate = effectiveAnnualRate / 12;
const numberOfMonths = loanPeriodYears * 12;
let margin = incomeAfterTax * marginPercentage;
margin = Math.max(marginFloor, Math.min(marginCeiling, margin));
const availableIncome = incomeAfterTax - annualExpenses - margin;
if (availableIncome <= 0) return 0;
// Convert available income to monthly
const monthlyAvailableIncome = availableIncome / 12;
// Monthly present value factor using monthly compounding
const presentValueFactor = (1 - Math.pow(1 + monthlyRate, -numberOfMonths)) / monthlyRate;
const borrowingPower = monthlyAvailableIncome * presentValueFactor;
return borrowingPower;
}
// RMSE Calculation Using Percentage Errors
function calculateRMSEPercentAnnualFixedBase(
testCases: { income: number; expectedBorrowingPower: number }[],
baseRate: number,
bufferRate: number,
marginPercentage: number,
marginFloor: number,
marginCeiling: number
): number {
let sumOfSquaredPercentErrors = 0;
for (const testCase of testCases) {
const calculatedPower = calculateBorrowingPower(
testCase.income,
24000,
baseRate, // Use the fixed base rate
bufferRate,
30,
marginPercentage,
marginFloor,
marginCeiling
);
const percentError =
testCase.expectedBorrowingPower === 0
? 0
: (calculatedPower - testCase.expectedBorrowingPower) / testCase.expectedBorrowingPower;
sumOfSquaredPercentErrors += percentError * percentError;
}
const meanSquaredPercentError = sumOfSquaredPercentErrors / testCases.length;
const rootMeanSquaredPercentError = Math.sqrt(meanSquaredPercentError);
return rootMeanSquaredPercentError;
}
// To recalculate, grab these values (after tax income) from NAB and run findOptimalParameters() again.
const testCases = [
{ income: 1200000, expectedBorrowingPower: 11634472 },
{ income: 1000000, expectedBorrowingPower: 9597806 },
{ income: 900000, expectedBorrowingPower: 8579474 },
{ income: 700000, expectedBorrowingPower: 6542808 },
{ income: 500000, expectedBorrowingPower: 4506143 },
{ income: 450000, expectedBorrowingPower: 3996976 },
{ income: 400000, expectedBorrowingPower: 3487810 },
{ income: 350000, expectedBorrowingPower: 2978644 },
{ income: 300000, expectedBorrowingPower: 2469477 },
{ income: 250000, expectedBorrowingPower: 1960310 },
{ income: 200000, expectedBorrowingPower: 1547927 },
{ income: 150000, expectedBorrowingPower: 1103771 },
{ income: 100000, expectedBorrowingPower: 693586 },
];
export function findOptimalParameters(){
const baseRate = 0.0619;
// Combined Optimization (Fixed Base Rate)
let bestRMSE = Infinity;
let bestBufferRate = 0;
let bestMarginPercentage = 0;
let bestMarginFloor = 0;
let bestMarginCeiling = 0;
// Define the search space.
const bufferRateSteps = 0.001;
const percentageSteps = 0.001;
const floorSteps = 1000;
const ceilingSteps = 1000;
for (let bufferRate = 0.00; bufferRate <= 0.5; bufferRate += bufferRateSteps) {
for (let marginPercentage = 0.000; marginPercentage <= 0.025; marginPercentage += percentageSteps) {
for (let marginFloor = 2000; marginFloor <= 20000; marginFloor += floorSteps) {
for (let marginCeiling = 0; marginCeiling <= 50000; marginCeiling += ceilingSteps) {
const currentRMSE = calculateRMSEPercentAnnualFixedBase(
testCases,
baseRate,
bufferRate,
marginPercentage,
marginFloor,
marginCeiling
);
if (currentRMSE < bestRMSE) {
bestRMSE = currentRMSE;
bestBufferRate = bufferRate;
bestMarginPercentage = marginPercentage;
bestMarginFloor = marginFloor;
bestMarginCeiling = marginCeiling;
}
}
}
}
}
console.log(`Best RMSE (Percentage): ${(bestRMSE * 100).toFixed(2)}%`);
console.log(`Best Buffer Rate: ${bestBufferRate.toFixed(3)}`);
console.log(`Best Margin Percentage: ${bestMarginPercentage.toFixed(3)}`);
console.log(`Best Margin Floor: ${bestMarginFloor}`);
console.log(`Best Margin Ceiling: ${bestMarginCeiling}`);
// Display Results with Optimized Parameters
console.log("\nFinal Results Table (Using Best Parameters):");
console.log("┌─────────────────┬─────────────┬─────────────┬──────────┬──────────┐");
console.log("│ Income After Tax│ Original BP │ Calculated │ Error │ % Error │");
console.log("├─────────────────┼─────────────┼─────────────┼──────────┼──────────┤");
for (const testCase of testCases) {
const calculatedPower = calculateBorrowingPower(
testCase.income,
24000,
baseRate,
bestBufferRate,
30,
bestMarginPercentage,
bestMarginFloor,
bestMarginCeiling
);
const error = calculatedPower - testCase.expectedBorrowingPower;
const percentError = (error / testCase.expectedBorrowingPower) * 100;
// Format numbers with fixed width, accounting for negative signs
const incomeStr = testCase.income.toLocaleString().padStart(15);
const originalStr = testCase.expectedBorrowingPower.toLocaleString().padStart(11);
const calculatedStr = calculatedPower.toLocaleString(undefined, { maximumFractionDigits: 0 }).padStart(11);
const errorStr = error.toLocaleString(undefined, { maximumFractionDigits: 0 }).padStart(8);
const percentStr = `${percentError.toFixed(2)}%`.padStart(8);
console.log(`│ ${incomeStr} │ ${originalStr} │ ${calculatedStr} │ ${errorStr} │ ${percentStr} │`);
}
console.log("└─────────────────┴─────────────┴─────────────┴──────────┴──────────┘");
}
// Uncomment to run
// findOptimalParameters();
/*
Best RMSE (Percentage): 3.55%
Best Buffer Rate: 0.038
Best Margin Percentage: 0.000
Best Margin Floor: 7000
Best Margin Ceiling: 0
Final Results Table (Using Best Parameters):
┌─────────────────┬─────────────┬─────────────┬──────────┬──────────┐
│ Income After Tax│ Original BP │ Calculated │ Error │ % Error │
├─────────────────┼─────────────┼─────────────┼──────────┼──────────┤
│ 1,200,000 │ 11,634,472 │ 11,110,063 │ -524,409 │ -4.51% │
│ 1,000,000 │ 9,597,806 │ 9,209,283 │ -388,523 │ -4.05% │
│ 900,000 │ 8,579,474 │ 8,258,892 │ -320,582 │ -3.74% │
│ 700,000 │ 6,542,808 │ 6,358,111 │ -184,697 │ -2.82% │
│ 500,000 │ 4,506,143 │ 4,457,331 │ -48,812 │ -1.08% │
│ 450,000 │ 3,996,976 │ 3,982,136 │ -14,840 │ -0.37% │
│ 400,000 │ 3,487,810 │ 3,506,940 │ 19,130 │ 0.55% │
│ 350,000 │ 2,978,644 │ 3,031,745 │ 53,101 │ 1.78% │
│ 300,000 │ 2,469,477 │ 2,556,550 │ 87,073 │ 3.53% │
│ 250,000 │ 1,960,310 │ 2,081,355 │ 121,045 │ 6.17% │
│ 200,000 │ 1,547,927 │ 1,606,160 │ 58,233 │ 3.76% │
│ 150,000 │ 1,103,771 │ 1,130,965 │ 27,194 │ 2.46% │
│ 100,000 │ 693,586 │ 655,769 │ -37,817 │ -5.45% │
└─────────────────┴─────────────┴─────────────┴──────────┴──────────┘
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment