/** * 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 }