Sail away calculator

SailAway Calculator - Stop Dreaming. Start Calculating. Then Actually Go.

⛵ SailAway Calculator

Stop Dreaming. Start Calculating. Then Actually Go.

Your Sailing Inputs

👤 Profile

$
$
$

💰 Income & Savings

$
$

⛵ Sailing Costs

Low $1,700
Medium $4,500
High $8,000+
$4,500

Global Defaults

Stress Testing

Enable Stress Mode
Randomize stress year-by-year
🚀F*** It, Go Now

Financing Strategy

HELOC
Boat Loan
HELOC + Boat
Other
None
Remote Work While Sailing
Departure Age45
Runway (years)
Point of No Return
Oh-Sh!t Year (stress)
Boat Equity at End$0

Cash Balance vs Age

Balance Sheet (Annual)

Remaining at Runway End

Loan Balance
$0
Boat Value
$0
Regret Probability
5%
// ===== Timing function getDepartureOffsetMonths(path){ const userAge = parseFloat(document.getElementById('userAge').value)||45; if (path==='A') return 0; if (path==='B') return (state.paths.B.yearsToSave||5)*12; if (path==='C'){ const retAge = state.paths.C.retirementAge||60; return Math.max(0, (retAge-userAge)*12); } return 0; } // ===== Small helpers used across UI function getNumber(v){ const n = parseFloat(String(v).replace(/[^0-9.-]/g,'')); return isNaN(n)?0:n; } function getActiveBoatPrice(path){ const lt = state.paths[path].loan; if (lt === 'boat' || lt === 'both') return state.loanParams.boat.price || 0; if (lt === 'heloc') return state.loanParams.heloc.boatPrice || state.loanParams.boat.price || 0; return 0; } function setBoatEquityLabel(path, departureAge, runwayYears){ const labelEl = document.getElementById(`path${path}BoatEquity`)?.closest('.stat-item')?.querySelector('.stat-label'); if (!labelEl) return; if (runwayYears >= 50) { labelEl.textContent = 'Boat Equity at End (open-ended)'; } else { const saleAge = Math.round((departureAge + runwayYears) * 10) / 10; labelEl.textContent = `Boat Equity at End (Age ${saleAge})`; } } function getHelocDraw(){ const p = state.loanParams.heloc; return Math.max(0, Number(p.drawAmount) || 0); } // ===== Monthly simulation engine function simulateRunwayMonthly(path, useStress=false){ const investReturn = (parseFloat(document.getElementById('investmentReturn')?.value)||6)/100; const infl = (parseFloat(document.getElementById('inflationRate')?.value) || 3) / 100; const rInvMonthly = investReturn/12; const landMonthly = getMoney('monthlyLandCost') || 3000; const baseCushion = landMonthly * 6; // today's dollars const basePNRBuffer = landMonthly * 12; // today's dollars const sailingBase = (Number(state.sailingBasePreset) + Number(state.sailingAdjustment)) || 0; // start cash (growth pre-departure) const liquid = getMoney('liquidSavings') || 0; const portfolio = getMoney('investablePortfolio') || 0; const annualIncome = getMoney('annualIncome') || 0; const savingsRate = (parseFloat(document.getElementById('savingsRate').value)||0)/100; const depOffset = getDepartureOffsetMonths(path); let startCash = liquid + portfolio; if (depOffset > 0){ const monthlySave = (annualIncome * savingsRate) / 12; for (let m=1; m<=depOffset; m++){ startCash = startCash*(1+rInvMonthly) + monthlySave; } } // Loan setup const loanType = state.paths[path].loan; const b = state.loanParams.boat; const h = state.loanParams.heloc; const o = state.loanParams.other; let boatPrincipal = 0, boatTerm = 0, boatRate = 0, boatInsuranceM = 0; let helocPrincipal = 0, helocRate = 0, helocIOM = 0, helocAmortM = 0; let otherPrincipal = 0, otherRate = 0, otherTerm = 0; const boatPrice = getActiveBoatPrice(path); if (loanType === 'boat' || loanType === 'both'){ const downAmt = boatPrice * (b.downPercent||0)/100; boatPrincipal = Math.max(0, boatPrice - downAmt); boatTerm = b.termMonths||0; boatRate = b.rate||0; boatInsuranceM = (b.insuranceType==='percent') ? (boatPrice * (b.insurancePercent||0)/100)/12 : (b.insuranceBump||0); } if (loanType === 'heloc' || loanType === 'both'){ helocPrincipal = getHelocDraw(); helocRate = h.rate||0; helocIOM = h.ioMonths||0; helocAmortM = h.amortMonths||0; } if (loanType === 'other'){ otherPrincipal = o.principal||0; otherRate = o.rate||0; otherTerm = o.termMonths||0; } // Remote & passive income const applyStress = useStress || !!document.getElementById('stressTestToggle')?.checked; const passiveM = (getMoney('passiveIncome') || 0); const remoteBaseM = state.paths[path].remoteWork ? (getMoney(`path${path}RemoteIncome`)||0) : 0; const purchasePriceForValue = boatPrice>0 ? boatPrice : 0; // Simulation loop (cap to 50 years => 600 months) let cash = startCash; let months = 0; let runwayMonths = null; let pnrMonths = null; const yearlyRows = []; let yearTracker = { startCash: cash, growth: 0, burn: 0 }; while (months < 600){ const yearIdx = Math.floor(months/12); const {costMult, incomeMult} = getStressMultipliers(yearIdx, applyStress); const inflFactor = Math.pow(1 + infl, Math.max(0, yearIdx)); const cushionNow = baseCushion * inflFactor; const pnrBufferNow = basePNRBuffer * inflFactor; // Costs let sailCostM = sailingBase * inflFactor; if (applyStress) sailCostM *= costMult; if (applyStress && isStressShockYear(yearIdx)) { sailCostM *= (1 + state.stress.shockCostBumpPct/100); } // Loan payments (month-specific) let loanPayM = 0; if (boatPrincipal > 0 && months < boatTerm){ const mBoat = fixedMonthlyPayment(boatPrincipal, boatRate, boatTerm); const insM = (b.insuranceType==='percent') ? (boatPrice * (b.insurancePercent||0)/100)/12 * inflFactor : boatInsuranceM * inflFactor; loanPayM += mBoat + insM; } if (helocPrincipal > 0){ const due = helocMonthlyPayment(helocPrincipal, helocRate, months+1, helocIOM, helocAmortM); loanPayM += due; } if (otherPrincipal > 0 && months < otherTerm){ loanPayM += fixedMonthlyPayment(otherPrincipal, otherRate, otherTerm); } // Income let remoteM = remoteBaseM; if (applyStress) remoteM *= incomeMult; const inflow = passiveM + remoteM; const outflow = sailCostM + loanPayM; // Apply investment growth on cash *before* flows const growth = cash * rInvMonthly; cash += growth + inflow - outflow; yearTracker.growth += growth; yearTracker.burn += Math.max(0, outflow - inflow); // PNR and runway checks (inflation-adjusted) if (pnrMonths === null && cash <= cushionNow + pnrBufferNow){ pnrMonths = months + 1; } if (cash <= cushionNow){ runwayMonths = months + 1; if (pnrMonths === null || pnrMonths > runwayMonths) pnrMonths = runwayMonths; break; } months++; // close out an annual row each 12 months const endOfYear = months % 12 === 0; if (endOfYear){ yearlyRows.push({ year: yearIdx, startCash: yearTracker.startCash, growth: yearTracker.growth, burn: yearTracker.burn, endCash: cash }); yearTracker = { startCash: cash, growth: 0, burn: 0 }; } } if (runwayMonths === null){ runwayMonths = 600; if (pnrMonths === null) pnrMonths = 600; } const boatValue = purchasePriceForValue>0 ? boatValueAtMonths(purchasePriceForValue, runwayMonths) : 0; // Remaining balances at runway end let totalLoanBalance = 0; if (loanType === 'boat' || loanType === 'both'){ totalLoanBalance += fixedBalance(boatPrincipal, boatRate, boatTerm, Math.min(runwayMonths, boatTerm)); } if (loanType === 'heloc' || loanType === 'both'){ totalLoanBalance += helocBalance(helocPrincipal, helocRate, Math.min(runwayMonths, (helocIOM+helocAmortM)), helocIOM, helocAmortM); } if (loanType === 'other'){ totalLoanBalance += fixedBalance(otherPrincipal, otherRate, otherTerm, Math.min(runwayMonths, otherTerm)); } const boatEquity = Math.max(0, boatValue - totalLoanBalance); return { departureCash: startCash, runwayYears: Math.min(runwayMonths/12, 50), pnrYears: Math.min(pnrMonths/12, runwayMonths/12, 50), finalCash: cash, yearlyRows, boatValue, loanBalanceAtEnd: totalLoanBalance, boatEquity }; } // ===== Rendering helpers function simulateBoth(path){ const base = simulateRunwayMonthly(path,false); const stress = simulateRunwayMonthly(path,true); return { base, stress }; } function updateChart(path, departureAge, sim){ const canvas = document.getElementById(`path${path}Chart`); if (!canvas) return; const ctx = canvas.getContext('2d'); if (state.charts[path]) { state.charts[path].destroy(); state.charts[path] = null; } const labels = []; const cash = []; const stress = []; const base = sim.base; const stressSim = sim.stress; const yearsToShow = Math.min( Math.max(base.yearlyRows.length, stressSim.yearlyRows.length) || 1, 20 ); for (let i=0;i<=yearsToShow;i++){ labels.push((Math.round((departureAge + i)*10)/10).toString()); const br = base.yearlyRows[i] || base.yearlyRows[base.yearlyRows.length-1]; const sr = stressSim.yearlyRows[i] || stressSim.yearlyRows[stressSim.yearlyRows.length-1]; const safeBase = Number.isFinite(br?.endCash) ? br.endCash : (Number.isFinite(base.finalCash) ? base.finalCash : 0); const safeStress = Number.isFinite(sr?.endCash) ? sr.endCash : (Number.isFinite(stressSim.finalCash) ? stressSim.finalCash : 0); cash.push(safeBase); stress.push(safeStress); } state.charts[path] = new Chart(ctx,{ type:'line', data:{ labels, datasets:[ {label:'Cash Balance', data:cash, borderColor:'#3498db', backgroundColor:'rgba(52,152,219,0.1)', borderWidth:3, tension:.4}, {label:'Stress Scenario', data:stress, borderColor:'#e74c3c', backgroundColor:'rgba(231,76,60,0.1)', borderWidth:2, borderDash:[5,5], tension:.4} ] }, options:{ responsive:true, maintainAspectRatio:false, plugins:{ legend:{position:'top'}, tooltip:{callbacks:{label:(c)=>`${c.dataset.label}: ${formatCurrency(c.parsed.y)}`}} }, scales:{ x:{ title:{display:true,text:'Age'}, ticks:{autoSkip:false, maxTicksLimit:20} }, y:{ title:{display:true,text:'Value ($)'}, ticks:{callback:(v)=>formatCurrency(v)} } } } }); } function renderBalanceSheet(path, departureAge, sim){ const landMonthly = getMoney('monthlyLandCost') || 3000; const baseCushion = landMonthly * 6; const infl = (parseFloat(document.getElementById('inflationRate')?.value) || 3) / 100; const rows = sim.base.yearlyRows; if (!rows.length){ document.getElementById(`path${path}Sheet`).innerHTML = '
No runway available.
'; return; } const maxYears = Math.min(rows.length, 20); let html = ` `; for (let i=0;i `; } html += `
Year Age Start Cash Growth Annual Burn End Cash
${i} ${age} ${formatCurrency(Math.max(r.startCash, cushionNow))} +${formatCurrency(r.growth)} ${formatCurrency(r.burn)} ${formatCurrency(Math.max(r.endCash, cushionNow))}
`; const headerEl = document.querySelector(`#path${path} .sheet-header`); if (headerEl) headerEl.textContent = 'Balance Sheet (Annual)'; document.getElementById(`path${path}Sheet`).innerHTML = html; } // ===== Loan UI (snapshots + inputs) function updateLoanInputs(path, loanType){ const container = document.getElementById(`path${path}LoanInputs`); container.classList.toggle('active', loanType!=='none'); const priceFor = () => { if (loanType === 'boat' || loanType === 'both') return state.loanParams.boat.price || 0; if (loanType === 'heloc') return state.loanParams.heloc.boatPrice || state.loanParams.boat.price || 0; return 0; }; const fmt = formatCurrency; const b = state.loanParams.boat; const h = state.loanParams.heloc; const o = state.loanParams.other; // Boat loan snapshot let boatSnap = null; if (loanType === 'boat' || loanType === 'both'){ const boatPrice = priceFor(); const downPct = Number(b.downPercent)||0; const downAmt = boatPrice * (downPct/100); const principal = Math.max(0, boatPrice - downAmt); const mBoat = fixedMonthlyPayment(principal, Number(b.rate)||0, Number(b.termMonths)||0); const insMonthly = b.insuranceType==='percent' ? (boatPrice * (Number(b.insurancePercent)||0)/100)/12 : (Number(b.insuranceBump)||0); const totalBoatM = mBoat + insMonthly; boatSnap = { boatPrice, downPct, downAmt, principal, mBoat, insMonthly, totalBoatM }; } // HELOC snapshot let helocSnap = null; if (loanType === 'heloc' || loanType === 'both'){ const principal = Math.max(0, Number(h.drawAmount)||0); const r = (Number(h.rate)||0)/100/12; const ioPay = principal * r; const amortPay = helocMonthlyPayment(principal, Number(h.rate)||0, (Number(h.ioMonths)||0)+1, Number(h.ioMonths)||0, Number(h.amortMonths)||0); helocSnap = { principal, ioPay, amortPay }; } // Other loan snapshot let otherSnap = null; if (loanType === 'other'){ const principal = Math.max(0, Number(o.principal)||0); const otherPay = fixedMonthlyPayment(principal, Number(o.rate)||0, Number(o.termMonths)||0); otherSnap = { principal, otherPay }; } let html = ''; // HELOC if(loanType==='heloc' || loanType==='both'){ html += `
🏠 HELOC Configuration
${helocSnap ? `
Loan Snapshot
Draw Amount
${fmt(helocSnap.principal)}
Payment (Interest-Only)
${fmt(helocSnap.ioPay)}
Payment (Amortization)
${fmt(helocSnap.amortPay)}
Note: IO phase lasts ${nf0.format(Number(h.ioMonths)||0)} months, then switches to amortized payment for ${nf0.format(Number(h.amortMonths)||0)} months.
` : ''}
$
$
$
`; } // BOAT if(loanType==='boat' || loanType==='both'){ html += `
⛵ Boat Loan Configuration
${boatSnap ? `
Loan Snapshot
Boat Price
${fmt(boatSnap.boatPrice)}
Down Payment (${nf0.format(boatSnap.downPct)}%)
${fmt(boatSnap.downAmt)}
Loan Principal
${fmt(boatSnap.principal)}
Monthly Payment
${fmt(boatSnap.mBoat)}
Insurance / mo
${fmt(boatSnap.insMonthly)}
Total Debt Service / mo
${fmt(boatSnap.totalBoatM)}
` : ''}
$
${state.loanParams.boat.insuranceType==='percent' ? `` : `
$
` }
`; } // OTHER if(loanType==='other'){ html += `
📄 Other Loan Configuration
${otherSnap ? `
Loan Snapshot
Principal
${fmt(otherSnap.principal)}
Monthly Payment
${fmt(otherSnap.otherPay)}
` : ''}
$
`; } container.innerHTML = html; wireMoneyInputs(container); } // ===== Orchestration per path function updatePath(path){ const userAge = parseFloat(document.getElementById('userAge').value)||45; const depOffset = getDepartureOffsetMonths(path); const departureAge = userAge + (depOffset/12); const sim = simulateBoth(path); // Stats document.getElementById(`path${path}DepartureAge`).textContent = Math.round(departureAge*10)/10; const runwayYears = sim.base.runwayYears; const pnrYears = sim.base.pnrYears; const runwayText = runwayYears >= 50 ? '∞' : `${Math.round(runwayYears*10)/10} years`; const pnrText = pnrYears >= 50 ? 'Never' : `Year ${Math.round(pnrYears*10)/10}`; const ohshitText = sim.stress.pnrYears >= 50 ? 'Never' : `Year ${Math.round(sim.stress.pnrYears*10)/10}`; document.getElementById(`path${path}Runway`).textContent = runwayText; document.getElementById(`path${path}PNR`).textContent = pnrText; document.getElementById(`path${path}OhShit`).textContent = ohshitText; // Boat equity / balances at end of base runway document.getElementById(`path${path}BoatEquity`).textContent = formatCurrency(sim.base.boatEquity); document.getElementById(`path${path}LoanBalance`).textContent = formatCurrency(sim.base.loanBalanceAtEnd); document.getElementById(`path${path}BoatValue`).textContent = formatCurrency(sim.base.boatValue); if (path==='B'){ const netWorth = sim.base.departureCash - sim.base.loanBalanceAtEnd + sim.base.boatEquity; document.getElementById('pathBNetWorth').textContent = formatCurrency(Math.max(0, netWorth)); } if (path==='C'){ document.getElementById('pathCPortfolio').textContent = formatCurrency(sim.base.departureCash); } setBoatEquityLabel(path, departureAge, runwayYears); updateChart(path, departureAge, sim); renderBalanceSheet(path, departureAge, sim); } function updateAllPaths(){ // remote toggles -> show/hide inputs ['A','B','C'].forEach(p => { const chk = document.getElementById(`path${p}RemoteWork`); if (chk){ state.paths[p].remoteWork = !!chk.checked; const box = document.getElementById(`path${p}RemoteInputs`); if (box) box.style.display = chk.checked ? 'block' : 'none'; } }); updatePath('A'); updatePath('B'); updatePath('C'); } // ===== UI handlers function togglePanel(){ document.getElementById('inputPanel').classList.toggle('collapsed'); } function toggleAdvanced(){ document.getElementById('advancedContent').classList.toggle('show'); } function selectSailingPreset(el, preset, amount){ state.sailingBasePreset = Number(amount); state.sailingAdjustment = 0; updateSailingCostDisplay(); el.closest('.radio-group').querySelectorAll('.radio-option').forEach(x=>x.classList.remove('selected')); el.classList.add('selected'); updateAllPaths(); } function adjustSailingCost(amount){ state.sailingAdjustment = Number(state.sailingAdjustment) + Number(amount); updateSailingCostDisplay(); updateAllPaths(); } function updateSailingCostDisplay(){ const total = Number(state.sailingBasePreset) + Number(state.sailingAdjustment); document.getElementById('sailingCostDisplay').textContent = formatCurrency(total); } function selectLoan(el, path, loanType){ state.paths[path].loan = loanType; el.closest('.loan-options').querySelectorAll('.loan-option').forEach(o=>o.classList.remove('selected')); el.classList.add('selected'); const warn = document.getElementById(`path${path}Warning`); if (warn) warn.style.display = (loanType==='heloc' || loanType==='both') ? 'block' : 'none'; updateLoanInputs(path, loanType); updatePath(path); } // ===== FAQ toggle function toggleFAQ(){ const wrap = document.getElementById('faqPanel'); const caret = document.getElementById('faqCaret'); wrap.classList.toggle('open'); const open = wrap.classList.contains('open'); caret.textContent = open ? '▼' : '▶'; } // ===== Event wiring & initial render document.addEventListener('DOMContentLoaded', () => { wireMoneyInputs(document); // Inputs that should trigger recompute ['userAge','partnerAge','savingsRate','inflationRate','investmentReturn'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('input', scheduleRecalc); }); ['stressTestToggle','stressRandomizeToggle','incomeVolatility','stressSeed','costUncertainty'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('input', scheduleRecalc); }); // Remote toggles ['A','B','C'].forEach(p => { const chk = document.getElementById(`path${p}RemoteWork`); if (chk){ chk.addEventListener('change', updateAllPaths); } const inc = document.getElementById(`path${p}RemoteIncome`); if (inc){ inc.addEventListener('input', () => { formatMoneyInput(inc); scheduleRecalc(); }); inc.addEventListener('blur', () => { formatMoneyInput(inc); updateAllPaths(); }); } }); // Path B: years to save const yrs = document.getElementById('pathBYearsToSave'); if (yrs){ yrs.addEventListener('input', () => { state.paths.B.yearsToSave = parseInt(yrs.value)||1; document.getElementById('pathBYearsDisplay').textContent = state.paths.B.yearsToSave; updateAllPaths(); }); } // Path C: retirement age const rAge = document.getElementById('pathCRetirementAge'); if (rAge){ rAge.addEventListener('input', () => { state.paths.C.retirementAge = parseInt(rAge.value)||60; document.getElementById('pathCRetirementDisplay').textContent = state.paths.C.retirementAge; updateAllPaths(); }); } // Regret sliders (just update label) [['A','pathARegret','pathARegretValue'],['B','pathBRegret','pathBRegretValue'],['C','pathCRegret','pathCRegretValue']].forEach(([p,sliderId,labelId])=>{ const s = document.getElementById(sliderId); const l = document.getElementById(labelId); if (s && l){ s.addEventListener('input', ()=>{ l.textContent = `${s.value}%`; }); } }); // Build initial loan inputs for defaults updateLoanInputs('A', state.paths.A.loan); updateLoanInputs('B', state.paths.B.loan); updateLoanInputs('C', state.paths.C.loan); // First render updateSailingCostDisplay(); updateAllPaths(); });
Scroll to Top