rspace-online/modules/rflows/lib/mortgage-engine.ts

749 lines
26 KiB
TypeScript

/**
* rMortgage Amortization Engine
*
* Pure math functions — no React dependencies.
* Handles amortization, tranche splitting, overpayment distribution,
* and comparison with traditional mortgages.
*/
import type {
MortgageTranche,
MortgageState,
MortgageSimulatorConfig,
AmortizationEntry,
Lender,
LendingTier,
} from './mortgage-types'
import { DEFAULT_CONFIG } from './mortgage-types'
// ─── Core Amortization Math ──────────────────────────────
/** Monthly payment for a fixed-rate fully-amortizing loan */
export function monthlyPayment(principal: number, annualRate: number, termMonths: number): number {
if (annualRate === 0) return principal / termMonths
const r = annualRate / 12
return principal * (r * Math.pow(1 + r, termMonths)) / (Math.pow(1 + r, termMonths) - 1)
}
/** Interest portion of a payment given current balance */
export function interestPortion(balance: number, annualRate: number): number {
return balance * (annualRate / 12)
}
/** Generate full amortization schedule for a single loan */
export function amortizationSchedule(
principal: number,
annualRate: number,
termMonths: number,
monthlyOverpayment: number = 0,
): AmortizationEntry[] {
const schedule: AmortizationEntry[] = []
let balance = principal
const basePayment = monthlyPayment(principal, annualRate, termMonths)
let cumulativeInterest = 0
let cumulativePrincipal = 0
for (let month = 1; month <= termMonths && balance > 0.01; month++) {
const interest = interestPortion(balance, annualRate)
const totalPayment = Math.min(balance + interest, basePayment + monthlyOverpayment)
const principalPaid = totalPayment - interest
balance = Math.max(0, balance - principalPaid)
cumulativeInterest += interest
cumulativePrincipal += principalPaid
schedule.push({
month,
payment: totalPayment,
principal: principalPaid,
interest,
balance,
cumulativeInterest,
cumulativePrincipal,
})
}
return schedule
}
// ─── Lender Name Generator ───────────────────────────────
const FIRST_NAMES = [
'Alice', 'Bob', 'Carmen', 'David', 'Elena', 'Frank', 'Grace', 'Hassan',
'Iris', 'James', 'Kira', 'Leo', 'Maya', 'Noah', 'Olga', 'Pat',
'Quinn', 'Rosa', 'Sam', 'Tara', 'Uma', 'Victor', 'Wren', 'Xavier',
'Yara', 'Zane', 'Amara', 'Basil', 'Clara', 'Diego', 'Ella', 'Felix',
'Gia', 'Hugo', 'Ivy', 'Jules', 'Kaia', 'Liam', 'Mira', 'Nico',
'Opal', 'Priya', 'Ravi', 'Sage', 'Teo', 'Una', 'Val', 'Willow',
'Xena', 'Yuri', 'Zara', 'Arlo', 'Bea', 'Cruz', 'Dahlia', 'Elio',
'Flora', 'Gael', 'Hana', 'Idris', 'June', 'Kai', 'Luz', 'Marcel',
'Noor', 'Owen', 'Pearl', 'Rio', 'Soren', 'Thea', 'Umar', 'Vera',
]
function generateLender(index: number): Lender {
const name = FIRST_NAMES[index % FIRST_NAMES.length]
const suffix = index >= FIRST_NAMES.length ? ` ${Math.floor(index / FIRST_NAMES.length) + 1}` : ''
return {
id: `lender-${index}`,
name: `${name}${suffix}`,
walletAddress: `0x${index.toString(16).padStart(40, '0')}`,
}
}
// ─── Initialize Mortgage State ───────────────────────────
export function initializeMortgage(config: MortgageSimulatorConfig = DEFAULT_CONFIG): MortgageState {
const totalPrincipal = config.propertyValue * (1 - config.downPaymentPercent / 100)
const numTranches = Math.ceil(totalPrincipal / config.trancheSize)
const defaultTermMonths = config.termYears * 12
// Create tranches with optional rate variation
const tranches: MortgageTranche[] = []
let remainingPrincipal = totalPrincipal
// Determine which tranches are funded vs open
const fundedCount = Math.round(numTranches * (config.fundingPercent / 100))
// Determine which lenders are reinvestors (among funded tranches only)
const reinvestorCount = Math.round(fundedCount * (config.reinvestorPercent / 100))
const reinvestRates = config.reinvestmentRates.length > 0 ? config.reinvestmentRates : [0.05]
// Build tier assignments: distribute tranches across tiers by allocation
const tiers = config.useVariableTerms ? config.lendingTiers : []
const tierAssignments: (LendingTier | null)[] = []
if (tiers.length > 0) {
// Normalize allocations
const totalAlloc = tiers.reduce((s, t) => s + t.allocation, 0)
let assigned = 0
for (const tier of tiers) {
const count = Math.round((tier.allocation / totalAlloc) * numTranches)
for (let j = 0; j < count && assigned < numTranches; j++) {
tierAssignments.push(tier)
assigned++
}
}
// Fill any remainder with the last tier
while (tierAssignments.length < numTranches) {
tierAssignments.push(tiers[tiers.length - 1])
}
}
for (let i = 0; i < numTranches; i++) {
const principal = Math.min(config.trancheSize, remainingPrincipal)
remainingPrincipal -= principal
// Determine rate and term from tier or base config
const tier = tierAssignments[i] ?? null
const termMonths = tier ? tier.termYears * 12 : defaultTermMonths
const tierLabel = tier ? tier.label : `${config.termYears}yr`
// Apply rate variation: spread linearly across tranches
const baseRate = tier ? tier.rate : config.interestRate
const variationOffset = config.rateVariation > 0
? config.rateVariation * (2 * (i / Math.max(numTranches - 1, 1)) - 1)
: 0
const rate = Math.max(0.01, baseRate + variationOffset)
const isFunded = i < fundedCount
const mp = monthlyPayment(principal, rate, termMonths)
// Assign reinvestment strategy: spread reinvestors evenly among funded
const isReinvestor = isFunded && i < reinvestorCount
const reinvestmentRate = isReinvestor
? reinvestRates[i % reinvestRates.length]
: undefined
tranches.push({
id: `tranche-${i}`,
lender: generateLender(i),
principal,
principalRemaining: principal,
interestRate: rate,
termMonths,
monthsElapsed: 0,
monthlyPayment: isFunded ? mp : 0,
monthlyPrincipal: isFunded ? mp - interestPortion(principal, rate) : 0,
monthlyInterest: isFunded ? interestPortion(principal, rate) : 0,
totalInterestPaid: 0,
totalPrincipalPaid: 0,
transferable: true,
listedForSale: false,
transferHistory: [],
status: 'active',
funded: isFunded,
tierLabel,
reinvestmentRate,
isReinvested: false,
reinvestmentPool: 0,
})
}
const totalMonthly = tranches.reduce((s, t) => s + t.monthlyPayment, 0)
const tradMonthly = monthlyPayment(totalPrincipal, config.interestRate, defaultTermMonths)
const tradSchedule = amortizationSchedule(totalPrincipal, config.interestRate, defaultTermMonths)
const tradTotalInterest = tradSchedule.length > 0
? tradSchedule[tradSchedule.length - 1].cumulativeInterest
: 0
return {
propertyValue: config.propertyValue,
downPayment: config.propertyValue * (config.downPaymentPercent / 100),
totalPrincipal,
trancheSize: config.trancheSize,
baseInterestRate: config.interestRate,
termMonths: defaultTermMonths,
borrower: {
id: 'borrower-0',
name: 'Borrower',
walletAddress: '0x' + 'b'.repeat(40),
},
tranches,
currentMonth: 0,
monthlyPayment: totalMonthly,
overpayment: config.overpayment,
overpaymentTarget: config.overpaymentTarget,
totalInterestPaid: 0,
totalPrincipalPaid: 0,
totalPrincipalRemaining: totalPrincipal,
tranchesRepaid: 0,
communityFundBalance: 0,
traditionalTotalInterest: tradTotalInterest,
traditionalMonthlyPayment: tradMonthly,
}
}
// ─── Advance Simulation by One Month ─────────────────────
export function advanceMonth(state: MortgageState): MortgageState {
const next = { ...state, tranches: state.tranches.map(t => ({ ...t, transferHistory: [...t.transferHistory] })) }
next.currentMonth += 1
const activeTranches = next.tranches.filter(t => t.status === 'active' && t.funded)
if (activeTranches.length === 0) return next
// Distribute overpayment
let overpaymentPool = next.overpayment
let communityContribution = 0
if (overpaymentPool > 0) {
switch (next.overpaymentTarget) {
case 'community_fund':
communityContribution = overpaymentPool
overpaymentPool = 0
break
case 'split':
communityContribution = overpaymentPool / 2
overpaymentPool = overpaymentPool / 2
break
case 'extra_principal':
// all goes to extra principal
break
}
}
// Extra principal distributed evenly across active tranches
const extraPrincipalPerTranche = activeTranches.length > 0
? overpaymentPool / activeTranches.length
: 0
let totalInterestThisMonth = 0
let totalPrincipalThisMonth = 0
// Track newly repaid reinvestors for reinvestment
const newlyRepaidReinvestors: MortgageTranche[] = []
for (const tranche of next.tranches) {
if (tranche.status === 'repaid' || !tranche.funded) continue
const interest = interestPortion(tranche.principalRemaining, tranche.interestRate)
const basePrincipal = tranche.monthlyPayment - interest
const extraPrincipal = extraPrincipalPerTranche
const principalPaid = Math.min(tranche.principalRemaining, basePrincipal + extraPrincipal)
tranche.principalRemaining = Math.max(0, tranche.principalRemaining - principalPaid)
tranche.monthsElapsed += 1
tranche.monthlyInterest = interest
tranche.monthlyPrincipal = principalPaid
tranche.totalInterestPaid += interest
tranche.totalPrincipalPaid += principalPaid
totalInterestThisMonth += interest
totalPrincipalThisMonth += principalPaid
// Reinvestors accumulate their received payments
if (tranche.reinvestmentRate != null) {
tranche.reinvestmentPool += principalPaid + interest
}
if (tranche.principalRemaining < 0.01) {
tranche.principalRemaining = 0
tranche.status = 'repaid'
if (tranche.reinvestmentRate != null) {
newlyRepaidReinvestors.push(tranche)
}
}
}
// Create reinvestment tranches from repaid reinvestors
for (const repaid of newlyRepaidReinvestors) {
const reinvestAmount = repaid.reinvestmentPool
if (reinvestAmount < 100) continue // minimum reinvestment threshold
const rate = repaid.reinvestmentRate!
const remainingTermMonths = Math.max(12, next.termMonths - next.currentMonth)
const mp = monthlyPayment(reinvestAmount, rate, remainingTermMonths)
const newTranche: MortgageTranche = {
id: `reinvest-${repaid.id}-m${next.currentMonth}`,
lender: repaid.lender, // same lender reinvests
principal: reinvestAmount,
principalRemaining: reinvestAmount,
interestRate: rate,
termMonths: remainingTermMonths,
monthsElapsed: 0,
monthlyPayment: mp,
monthlyPrincipal: mp - interestPortion(reinvestAmount, rate),
monthlyInterest: interestPortion(reinvestAmount, rate),
totalInterestPaid: 0,
totalPrincipalPaid: 0,
transferable: true,
listedForSale: false,
transferHistory: [],
status: 'active',
funded: true,
tierLabel: `reinvest@${(rate * 100).toFixed(0)}%`,
reinvestmentRate: rate, // continues reinvesting
isReinvested: true,
parentTrancheId: repaid.id,
reinvestmentPool: 0,
}
next.tranches.push(newTranche)
repaid.reinvestmentPool = 0
}
// Also create reinvestment tranches periodically for active reinvestors
// who've accumulated enough in their pool (annual reinvestment cycle)
if (next.currentMonth % 12 === 0) {
for (const tranche of next.tranches) {
if (tranche.status !== 'active' || tranche.reinvestmentRate == null) continue
if (tranche.reinvestmentPool < 500) continue // need at least $500 to reinvest
const reinvestAmount = tranche.reinvestmentPool
const rate = tranche.reinvestmentRate
const remainingTermMonths = Math.max(12, next.termMonths - next.currentMonth)
const mp = monthlyPayment(reinvestAmount, rate, remainingTermMonths)
const newTranche: MortgageTranche = {
id: `reinvest-${tranche.id}-m${next.currentMonth}`,
lender: tranche.lender,
principal: reinvestAmount,
principalRemaining: reinvestAmount,
interestRate: rate,
termMonths: remainingTermMonths,
monthsElapsed: 0,
monthlyPayment: mp,
monthlyPrincipal: mp - interestPortion(reinvestAmount, rate),
monthlyInterest: interestPortion(reinvestAmount, rate),
totalInterestPaid: 0,
totalPrincipalPaid: 0,
transferable: true,
listedForSale: false,
transferHistory: [],
status: 'active',
funded: true,
tierLabel: `reinvest@${(rate * 100).toFixed(0)}%`,
reinvestmentRate: rate,
isReinvested: true,
parentTrancheId: tranche.id,
reinvestmentPool: 0,
}
next.tranches.push(newTranche)
tranche.reinvestmentPool = 0
}
}
// Simulate some secondary market activity (transfers every ~6 months for a few tranches)
if (next.currentMonth % 6 === 0 && next.currentMonth > 0) {
const activeForSale = next.tranches.filter(t =>
t.status === 'active' && !t.isReinvested && t.monthsElapsed > 12
)
// ~5% of eligible tranches get transferred per cycle
const transferCount = Math.max(0, Math.floor(activeForSale.length * 0.05))
for (let i = 0; i < transferCount; i++) {
const idx = (next.currentMonth * 7 + i * 13) % activeForSale.length
const tranche = activeForSale[idx]
if (!tranche || tranche.transferHistory.length >= 3) continue
const buyerIdx = next.tranches.length + 100 + next.currentMonth + i
const buyer = generateLender(buyerIdx)
const premium = 1 + (0.02 + (idx % 5) * 0.01) // 2-6% premium
const price = tranche.principalRemaining * premium
tranche.transferHistory.push({
id: `transfer-${tranche.id}-m${next.currentMonth}`,
fromLenderId: tranche.lender.id,
toLenderId: buyer.id,
price,
principalRemaining: tranche.principalRemaining,
premiumPercent: (premium - 1) * 100,
date: next.currentMonth,
})
tranche.lender = buyer
}
}
next.totalInterestPaid += totalInterestThisMonth
next.totalPrincipalPaid += totalPrincipalThisMonth
next.totalPrincipalRemaining = next.tranches.reduce((s, t) => s + t.principalRemaining, 0)
next.tranchesRepaid = next.tranches.filter(t => t.status === 'repaid').length
next.communityFundBalance += communityContribution
// Recalculate monthly payment (decreases as tranches are repaid)
next.monthlyPayment = next.tranches
.filter(t => t.status === 'active')
.reduce((s, t) => s + t.monthlyPayment, 0)
return next
}
// ─── Run Full Simulation ─────────────────────────────────
export function runFullSimulation(config: MortgageSimulatorConfig): MortgageState[] {
const states: MortgageState[] = []
let state = initializeMortgage(config)
states.push(state)
const maxMonths = config.termYears * 12
for (let m = 0; m < maxMonths; m++) {
if (state.totalPrincipalRemaining < 0.01) break
state = advanceMonth(state)
states.push(state)
}
return states
}
// ─── Tranche Transfer (Secondary Market) ─────────────────
export function transferTranche(
state: MortgageState,
trancheId: string,
buyerLender: Lender,
price: number,
): MortgageState {
const next = { ...state, tranches: state.tranches.map(t => ({ ...t, transferHistory: [...t.transferHistory] })) }
const tranche = next.tranches.find(t => t.id === trancheId)
if (!tranche || tranche.status === 'repaid' || !tranche.transferable) return state
const premiumPercent = ((price - tranche.principalRemaining) / tranche.principalRemaining) * 100
tranche.transferHistory.push({
id: `transfer-${tranche.transferHistory.length}`,
fromLenderId: tranche.lender.id,
toLenderId: buyerLender.id,
price,
principalRemaining: tranche.principalRemaining,
premiumPercent,
date: Date.now(),
})
tranche.lender = buyerLender
tranche.listedForSale = false
tranche.askingPrice = undefined
return next
}
// ─── Yield Calculator for Secondary Market ───────────────
export function calculateBuyerYield(
tranche: MortgageTranche,
purchasePrice: number,
): { annualYield: number; totalReturn: number; monthsRemaining: number } {
const remainingPrincipal = tranche.principalRemaining
const monthsRemaining = tranche.termMonths - tranche.monthsElapsed
// Estimate total remaining payments
let totalPayments = 0
let balance = remainingPrincipal
for (let m = 0; m < monthsRemaining && balance > 0.01; m++) {
const interest = balance * (tranche.interestRate / 12)
const payment = tranche.monthlyPayment
const principal = Math.min(balance, payment - interest)
totalPayments += payment
balance -= principal
}
const totalReturn = totalPayments - purchasePrice
const annualYield = monthsRemaining > 0
? (totalReturn / purchasePrice) / (monthsRemaining / 12) * 100
: 0
return { annualYield, totalReturn, monthsRemaining }
}
// ─── Summary Stats ───────────────────────────────────────
export interface MortgageSummary {
// Myco
mycoTotalInterest: number
mycoMonthlyPayment: number
mycoPayoffMonths: number
avgLenderYield: number
communityRetained: number // interest that stays in community
// Traditional
tradTotalInterest: number
tradMonthlyPayment: number
tradPayoffMonths: number
// Delta
interestSaved: number
monthsSaved: number
}
export function computeSummary(states: MortgageState[]): MortgageSummary {
const final = states[states.length - 1]
const initial = states[0]
const mycoPayoffMonths = final.currentMonth
const tradSchedule = amortizationSchedule(
initial.totalPrincipal,
initial.baseInterestRate,
initial.termMonths,
)
const tradPayoffMonths = tradSchedule.length
// Average lender yield: weighted by tranche size
const totalWeight = final.tranches.reduce((s, t) => s + t.principal, 0)
const avgYield = totalWeight > 0
? final.tranches.reduce((s, t) => {
const yieldPct = t.principal > 0 ? (t.totalInterestPaid / t.principal) / (mycoPayoffMonths / 12) * 100 : 0
return s + yieldPct * (t.principal / totalWeight)
}, 0)
: 0
return {
mycoTotalInterest: final.totalInterestPaid,
mycoMonthlyPayment: initial.monthlyPayment,
mycoPayoffMonths,
avgLenderYield: avgYield,
communityRetained: final.totalInterestPaid, // all interest stays local
tradTotalInterest: final.traditionalTotalInterest,
tradMonthlyPayment: initial.traditionalMonthlyPayment,
tradPayoffMonths,
interestSaved: final.traditionalTotalInterest - final.totalInterestPaid,
monthsSaved: tradPayoffMonths - mycoPayoffMonths,
}
}
// ─── Lender Return Calculator ────────────────────────────
export interface LenderReturnScenario {
tierLabel: string
termYears: number
rate: number
investment: number
// Monthly liquidity: take all returns each month
liquid: {
monthlyPayment: number
totalInterest: number
totalReturn: number
effectiveYield: number // annualized
}
// Reinvest to term: compound returns at the same rate
reinvested: {
finalValue: number
totalInterest: number
totalReturn: number
effectiveYield: number // annualized
}
}
export function calculateLenderReturns(
investment: number,
tiers: { label: string; termYears: number; rate: number }[],
): LenderReturnScenario[] {
return tiers.map(tier => {
const termMonths = tier.termYears * 12
const r = tier.rate / 12
const mp = r === 0
? investment / termMonths
: investment * (r * Math.pow(1 + r, termMonths)) / (Math.pow(1 + r, termMonths) - 1)
// Liquid: lender receives mp each month, keeps interest, gets principal back over time
const liquidTotalPayments = mp * termMonths
const liquidTotalInterest = liquidTotalPayments - investment
const liquidEffectiveYield = investment > 0
? (liquidTotalInterest / investment) / tier.termYears * 100
: 0
// Reinvested: compound monthly payments at the tier rate
// Each monthly payment is reinvested and earns interest for remaining months
let reinvestAccum = 0
for (let m = 0; m < termMonths; m++) {
const monthsRemaining = termMonths - m - 1
// Future value of this month's payment compounded for remaining months
reinvestAccum += mp * Math.pow(1 + r, monthsRemaining)
}
const reinvestTotalInterest = reinvestAccum - investment
const reinvestEffectiveYield = investment > 0 && tier.termYears > 0
? (Math.pow(reinvestAccum / investment, 1 / tier.termYears) - 1) * 100
: 0
return {
tierLabel: tier.label,
termYears: tier.termYears,
rate: tier.rate,
investment,
liquid: {
monthlyPayment: mp,
totalInterest: liquidTotalInterest,
totalReturn: liquidTotalPayments,
effectiveYield: liquidEffectiveYield,
},
reinvested: {
finalValue: reinvestAccum,
totalInterest: reinvestTotalInterest,
totalReturn: reinvestAccum,
effectiveYield: reinvestEffectiveYield,
},
}
})
}
// ─── Borrower Affordability Calculator ───────────────────
/** Given a monthly payment, how much can you borrow at a given rate and term? */
export function maxPrincipal(monthlyBudget: number, annualRate: number, termMonths: number): number {
if (annualRate === 0) return monthlyBudget * termMonths
const r = annualRate / 12
return monthlyBudget * (Math.pow(1 + r, termMonths) - 1) / (r * Math.pow(1 + r, termMonths))
}
export interface AffordabilityScenario {
label: string
description: string
// Tier mix used
tiers: { label: string; termYears: number; rate: number; allocation: number }[]
// Results
maxLoan: number
propertyValue: number // with down payment
monthlyPayment: number
totalInterest: number
totalPaid: number
payoffYears: number
// Per-tier breakdown
breakdown: {
tierLabel: string
principal: number
monthlyPayment: number
totalInterest: number
termYears: number
rate: number
}[]
}
export function calculateAffordability(
monthlyBudget: number,
downPaymentPercent: number,
tiers: { label: string; termYears: number; rate: number; allocation: number }[],
): AffordabilityScenario[] {
const scenarios: AffordabilityScenario[] = []
// Helper: compute a scenario for a given tier mix
function compute(
label: string,
description: string,
mix: { label: string; termYears: number; rate: number; allocation: number }[],
): AffordabilityScenario {
// Each tier gets a fraction of the monthly budget proportional to its allocation
const totalAlloc = mix.reduce((s, t) => s + t.allocation, 0)
const breakdown = mix.map(tier => {
const allocFrac = tier.allocation / totalAlloc
const tierBudget = monthlyBudget * allocFrac
const termMonths = tier.termYears * 12
const principal = maxPrincipal(tierBudget, tier.rate, termMonths)
const mp = tierBudget
const totalPaid = mp * termMonths
const totalInterest = totalPaid - principal
return {
tierLabel: tier.label,
principal,
monthlyPayment: mp,
totalInterest,
termYears: tier.termYears,
rate: tier.rate,
}
})
const totalLoan = breakdown.reduce((s, b) => s + b.principal, 0)
const totalInterest = breakdown.reduce((s, b) => s + b.totalInterest, 0)
const totalPaid = breakdown.reduce((s, b) => s + b.monthlyPayment * b.termYears * 12, 0)
const longestTerm = Math.max(...mix.map(t => t.termYears))
const propertyValue = downPaymentPercent > 0
? totalLoan / (1 - downPaymentPercent / 100)
: totalLoan
return {
label,
description,
tiers: mix,
maxLoan: totalLoan,
propertyValue,
monthlyPayment: monthlyBudget,
totalInterest,
totalPaid,
payoffYears: longestTerm,
breakdown,
}
}
// Scenario 1: Default tier mix
if (tiers.length > 1) {
scenarios.push(compute('Blended', 'Default tier allocation across all terms', tiers))
}
// Scenario 2: Each single tier
for (const tier of tiers) {
scenarios.push(compute(
`All ${tier.label}`,
`Entire loan at ${(tier.rate * 100).toFixed(1)}% for ${tier.termYears} years`,
[{ ...tier, allocation: 1 }],
))
}
// Scenario 3: Short-heavy mix (if multiple tiers)
if (tiers.length >= 3) {
const sorted = [...tiers].sort((a, b) => a.termYears - b.termYears)
const shortHeavy = sorted.map((t, i) => ({
...t,
allocation: i === 0 ? 0.5 : (0.5 / (sorted.length - 1)),
}))
scenarios.push(compute(
'Short-Heavy',
`50% in shortest term (${sorted[0].label}), rest spread`,
shortHeavy,
))
}
// Scenario 4: Long-heavy mix (if multiple tiers)
if (tiers.length >= 3) {
const sorted = [...tiers].sort((a, b) => b.termYears - a.termYears)
const longHeavy = sorted.map((t, i) => ({
...t,
allocation: i === 0 ? 0.5 : (0.5 / (sorted.length - 1)),
}))
scenarios.push(compute(
'Long-Heavy',
`50% in longest term (${sorted[0].label}), rest spread`,
longHeavy,
))
}
// Sort by max loan descending
scenarios.sort((a, b) => b.maxLoan - a.maxLoan)
return scenarios
}