Last active
April 11, 2025 17:28
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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