triforce-strategies: add S&P 500 vs anythingn calculator

claude helped with this
This commit is contained in:
Fabian Montero 2026-03-25 20:48:30 -06:00
parent 81b79a225f
commit d19f970d48
Signed by: fabian
GPG key ID: 3EDA9AE3937CCDE3
6 changed files with 969 additions and 2 deletions

View file

@ -10,9 +10,12 @@ Personal homepage built with Zola.
- `content/` — Markdown content
- `blog/` — Blog posts
- `lib/` — Library/reference articles
- `triforce_strategies/` — Triforce Strategies LLC content
- `triforce-strategies/` — Triforce Strategies LLC content
- `tools/` — Interactive calculators/tools
- `templates/shortcodes/` — Custom shortcodes (override theme)
- `static/js/` — JavaScript files
- `static/css/` — CSS files
- `themes/tabi-lean/` — Theme (git subtree, do not edit directly)
- `static/` — Static assets
- `config.toml` — Site configuration
## Content Conventions
@ -20,5 +23,29 @@ Personal homepage built with Zola.
- Section indices: `_index.md`
- Internal links: `@/path/to/file.md`
## Adding Interactive Tools
To add a new tool under `triforce-strategies/tools/`:
1. **Create shortcode**: `templates/shortcodes/tool_name.html`
- HTML structure (form, canvas, results container)
- Load JS via `{{ get_url(path='js/toolName.js') | safe }}`
- Include `<noscript>` fallback
2. **Create JS**: `static/js/toolName.js`
- Wrap in IIFE: `(function() { 'use strict'; ... })();`
- Use theme CSS variables for colors (--text-color, --primary-color, etc.)
- Scale canvas for retina: `window.devicePixelRatio`
3. **Create CSS**: `static/css/tool-name.css`
- Use theme CSS variables from `themes/tabi-lean/sass/main.scss`
- Responsive layout with media queries
4. **Create page**: `content/triforce-strategies/tools/tool_name.md`
- Frontmatter: `[extra] stylesheets = ["css/tool-name.css"]`
- Invoke shortcode: `{{ tool_name() }}`
Example: `snp500_vs_anything` (fund comparison calculator with Monte Carlo simulation)
## Commit Style
`type: description` (e.g., `blog: add post title`)

View file

@ -0,0 +1,5 @@
+++
title = "tools"
sort_by = "title"
paginate_by = 10
+++

View file

@ -0,0 +1,41 @@
+++
title = "S&P 500 vs lo que sea"
date = 2026-03-25
description = "Compara el crecimiento del S&P500 contra cualquier otro fondo hipotético usando una simulación Monte Carlo sencilla."
[extra]
stylesheets = ["css/fund-comparison.css"]
+++
Esta calculadora compara el crecimiento proyectado del S&P500 contra cualquier otro fondo hipotético. Usa una simulación Monte Carlo sencilla para modelar la volatilidad del mercado.
Este tipo de simulación es muy sencilla y solo sirve para ilustrar la diferencia entre el rendimiento del S&P500 en comparación a otros fondos disponibles en Costa Rica.
## Cómo usar la calculadora
0. **Idioma**: Perdón, todo está en spanglish, pero es que es el restulado de una investigación que es en inglés y español.
1. **Fondo S&P 500**: Valores por defecto representan un índice de bajo costo. Puede buscar el rendimiento real del S&P 500 y poner los valores aquí.
2. **Fondo a comparar**: Introduzca los parámetros del fondo que quiere comprarar.
3. **Parámetros de inversión**: Seleccione el tiempo de la inversión y la contribución mensual.
4. **Monte Carlo**: Active la simulación para un modelado rudimentario de volatilidad.
## Cómo leer el gráfico generado
- **Medianas**: Verde (S&P 500) y azul (fondo comparado) muestran los resultados esperados
- **Bandas sombreadas**: Rango del percentil 10 al 90 cuando la simulación Monte Carlo está activada
- **Δ (Delta)**: Diferencia entre los valores finales de ambos fondos
- **Tax loss**: (pérdida fiscal) Segmento rojo que muestra el impuesto sobre ganancias de capital deducido al retirar (cuando aplica)
### Nota acerca de impuestos
- Costarricenses no tienen que pagar impuestos a ganancias de capital obtenidas a través de inversinoes directas en USA. Más info: [taxation of NRAs](https://www.irs.gov/businesses/taxation-of-nonresident-aliens-international-tax-gap-series).
- En Costa Rica no hay renta global (aún lol)
- Por eso la calculadora no calcula una pérdida fiscal al invertir directamente en el S&P500 pero sí al invertir en un fondo de CR.
{{ fund_comparison() }}
## Notas generales
- Los rendimientos se modelan usando distribución log-normal
- Los gastos de administración y comisiones se deducen mensualmente
- El impuesto sobre ganancias de capital se aplica únicamente sobre las ganancias al momento del retiro final y se muestra como una caída roja en el gráfico
- Los rendimientos históricos del S&P 500 promedian ~10% anual con ~15% de desviación estándar
- Rendimientos pasados no garantizan resultados futuros

View file

@ -0,0 +1,181 @@
.fund-comparison-container {
width: 100%;
}
.fund-comparison-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2rem;
}
.form-section {
background: var(--bg-0);
padding: 1rem;
border-radius: 4px;
}
.form-section h3 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--text-color-high-contrast);
font-size: 1rem;
}
.form-group {
margin-bottom: 0.75rem;
}
.form-group label {
display: block;
margin-bottom: 0.25rem;
font-size: 0.85rem;
color: var(--meta-color);
}
.form-group input {
width: 100%;
padding: 0.5rem;
background: var(--bg-2);
border: 1px solid var(--divider-color);
border-radius: 3px;
color: var(--text-color);
font-family: var(--code-font);
font-size: 0.9rem;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: var(--primary-color);
}
.form-section-full {
grid-column: 1 / -1;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
}
.checkbox-group input[type="checkbox"] {
width: auto;
}
.checkbox-group label {
margin-bottom: 0;
}
.calculate-btn {
display: block;
width: 100%;
padding: 0.75rem;
margin-top: 1rem;
background: var(--primary-color);
color: var(--background-color);
border: none;
border-radius: 4px;
font-family: var(--sans-serif-font);
font-size: 1rem;
font-weight: 550;
cursor: pointer;
transition: opacity 0.2s;
}
.calculate-btn:hover {
opacity: 0.9;
}
.chart-container {
margin-bottom: 2rem;
background: var(--bg-2);
border-radius: 4px;
padding: 1rem;
}
.chart-container canvas {
width: 100%;
display: block;
}
.summary-section {
margin-bottom: 1.5rem;
}
.summary-section h3 {
margin-top: 0;
margin-bottom: 1rem;
}
.summary-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.summary-table th,
.summary-table td {
padding: 0.5rem 0.75rem;
text-align: left;
border-bottom: 1px solid var(--divider-color);
}
.summary-table th {
background: var(--bg-0);
font-weight: 550;
}
.summary-table td:not(:first-child) {
text-align: right;
font-family: var(--code-font);
}
.percentiles-details {
margin-top: 1rem;
}
.percentiles-details summary {
cursor: pointer;
color: var(--primary-color);
font-weight: 550;
}
.percentiles-table {
margin-top: 0.75rem;
}
.noscript-warning {
background: #ff9800;
color: #000;
padding: 1rem;
border-radius: 4px;
text-align: center;
}
@media (max-width: 600px) {
.fund-comparison-form {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.summary-table {
font-size: 0.8rem;
}
.summary-table th,
.summary-table td {
padding: 0.4rem 0.5rem;
}
}

567
static/js/fundComparison.js Normal file
View file

@ -0,0 +1,567 @@
(function() {
'use strict';
// Box-Muller transform for normal distribution
function boxMuller() {
let u1, u2;
do {
u1 = Math.random();
u2 = Math.random();
} while (u1 === 0);
return Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
}
// Convert annual rate to monthly
function annualToMonthly(annualRate) {
return Math.pow(1 + annualRate, 1/12) - 1;
}
// Run a single simulation
function runSimulation(params, months) {
const monthlyReturn = annualToMonthly(params.annualReturn);
const monthlyVol = params.annualVol / Math.sqrt(12);
const monthlyExpense = params.expenseRatio / 12;
let value = 0;
const values = [0];
for (let m = 1; m <= months; m++) {
// Add monthly deposit at start of month
value += params.monthlyDeposit;
// Apply log-normal return
const randomReturn = monthlyReturn + monthlyVol * boxMuller();
value *= Math.exp(randomReturn);
// Deduct expense ratio
value *= (1 - monthlyExpense);
values.push(value);
}
// Track pre-tax value
const preTaxValue = value;
// Apply capital gains tax at withdrawal if specified
const totalDeposits = params.monthlyDeposit * months;
const gains = value - totalDeposits;
let taxPaid = 0;
if (params.capitalGainsTax > 0 && gains > 0) {
taxPaid = gains * params.capitalGainsTax;
value -= taxPaid;
}
return { values, finalValue: value, preTaxValue, taxPaid };
}
// Run Monte Carlo simulation
function runMonteCarlo(params, months, numSimulations) {
const allFinalValues = [];
const allPreTaxValues = [];
const allTaxPaid = [];
const allValuesByMonth = [];
for (let i = 0; i <= months; i++) {
allValuesByMonth.push([]);
}
for (let sim = 0; sim < numSimulations; sim++) {
const result = runSimulation(params, months);
allFinalValues.push(result.finalValue);
allPreTaxValues.push(result.preTaxValue);
allTaxPaid.push(result.taxPaid);
for (let m = 0; m <= months; m++) {
allValuesByMonth[m].push(result.values[m]);
}
}
// Sort values at each month for percentile calculation
for (let m = 0; m <= months; m++) {
allValuesByMonth[m].sort((a, b) => a - b);
}
allFinalValues.sort((a, b) => a - b);
allPreTaxValues.sort((a, b) => a - b);
allTaxPaid.sort((a, b) => a - b);
// Calculate percentiles
function getPercentile(sortedArr, p) {
const idx = Math.floor(sortedArr.length * p);
return sortedArr[Math.min(idx, sortedArr.length - 1)];
}
const percentiles = {
p10: getPercentile(allFinalValues, 0.10),
p25: getPercentile(allFinalValues, 0.25),
p50: getPercentile(allFinalValues, 0.50),
p75: getPercentile(allFinalValues, 0.75),
p90: getPercentile(allFinalValues, 0.90)
};
const preTaxPercentiles = {
p50: getPercentile(allPreTaxValues, 0.50)
};
const taxPaidPercentiles = {
p50: getPercentile(allTaxPaid, 0.50)
};
// Get percentile curves for chart
const curves = {
p10: [],
p50: [],
p90: []
};
for (let m = 0; m <= months; m++) {
curves.p10.push(getPercentile(allValuesByMonth[m], 0.10));
curves.p50.push(getPercentile(allValuesByMonth[m], 0.50));
curves.p90.push(getPercentile(allValuesByMonth[m], 0.90));
}
return { percentiles, preTaxPercentiles, taxPaidPercentiles, curves };
}
// Run deterministic simulation (no volatility)
function runDeterministic(params, months) {
const monthlyReturn = annualToMonthly(params.annualReturn);
const monthlyExpense = params.expenseRatio / 12;
let value = 0;
const values = [0];
for (let m = 1; m <= months; m++) {
value += params.monthlyDeposit;
value *= (1 + monthlyReturn);
value *= (1 - monthlyExpense);
values.push(value);
}
const preTaxValue = value;
const totalDeposits = params.monthlyDeposit * months;
const gains = value - totalDeposits;
let taxPaid = 0;
if (params.capitalGainsTax > 0 && gains > 0) {
taxPaid = gains * params.capitalGainsTax;
value -= taxPaid;
}
return {
percentiles: { p10: value, p25: value, p50: value, p75: value, p90: value },
preTaxPercentiles: { p50: preTaxValue },
taxPaidPercentiles: { p50: taxPaid },
curves: { p10: values, p50: values, p90: values }
};
}
// Format currency
function formatCurrency(value) {
if (value >= 1000000) {
return '$' + (value / 1000000).toFixed(2) + 'M';
} else if (value >= 1000) {
return '$' + (value / 1000).toFixed(1) + 'K';
}
return '$' + value.toFixed(0);
}
// Chart renderer
const ChartRenderer = {
canvas: null,
ctx: null,
init: function(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.resize();
},
resize: function() {
const container = this.canvas.parentElement;
const dpr = window.devicePixelRatio || 1;
const rect = container.getBoundingClientRect();
this.canvas.width = rect.width * dpr;
this.canvas.height = 400 * dpr;
this.canvas.style.width = rect.width + 'px';
this.canvas.style.height = '400px';
this.ctx.scale(dpr, dpr);
},
clear: function() {
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
this.ctx.clearRect(0, 0, width, height);
},
draw: function(sp500Result, comparedResult, years, monthlyDeposit, useMonteCarlo) {
this.clear();
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
const padding = { top: 30, right: 100, bottom: 50, left: 80 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
const months = years * 12;
const totalDeposits = [];
for (let m = 0; m <= months; m++) {
totalDeposits.push(monthlyDeposit * m);
}
// Find max value for Y axis
let maxVal = 0;
const allCurves = [
sp500Result.curves.p90,
comparedResult.curves.p90,
totalDeposits
];
for (const curve of allCurves) {
for (const val of curve) {
if (val > maxVal) maxVal = val;
}
}
maxVal *= 1.1; // Add 10% padding
// Get computed styles for colors
const style = getComputedStyle(document.documentElement);
const textColor = style.getPropertyValue('--text-color').trim() || '#D4D4D4';
const dividerColor = style.getPropertyValue('--divider-color').trim() || '#4a4a4a';
const primaryColor = style.getPropertyValue('--primary-color').trim() || '#91e0ee';
// Draw grid
this.ctx.strokeStyle = dividerColor;
this.ctx.lineWidth = 0.5;
this.ctx.setLineDash([5, 5]);
// Y-axis grid lines
const numYLines = 5;
for (let i = 0; i <= numYLines; i++) {
const y = padding.top + (chartHeight / numYLines) * i;
this.ctx.beginPath();
this.ctx.moveTo(padding.left, y);
this.ctx.lineTo(width - padding.right, y);
this.ctx.stroke();
}
// X-axis grid lines
for (let yr = 0; yr <= years; yr++) {
const x = padding.left + (chartWidth / years) * yr;
this.ctx.beginPath();
this.ctx.moveTo(x, padding.top);
this.ctx.lineTo(x, height - padding.bottom);
this.ctx.stroke();
}
this.ctx.setLineDash([]);
// Helper to map data to canvas coordinates
const mapX = (month) => padding.left + (month / months) * chartWidth;
const mapY = (value) => padding.top + chartHeight - (value / maxVal) * chartHeight;
// Draw confidence bands if Monte Carlo
if (useMonteCarlo) {
// S&P 500 band (green)
this.ctx.fillStyle = 'rgba(76, 175, 80, 0.15)';
this.ctx.beginPath();
this.ctx.moveTo(mapX(0), mapY(sp500Result.curves.p10[0]));
for (let m = 0; m <= months; m++) {
this.ctx.lineTo(mapX(m), mapY(sp500Result.curves.p90[m]));
}
for (let m = months; m >= 0; m--) {
this.ctx.lineTo(mapX(m), mapY(sp500Result.curves.p10[m]));
}
this.ctx.closePath();
this.ctx.fill();
// Compared fund band (blue)
this.ctx.fillStyle = 'rgba(33, 150, 243, 0.15)';
this.ctx.beginPath();
this.ctx.moveTo(mapX(0), mapY(comparedResult.curves.p10[0]));
for (let m = 0; m <= months; m++) {
this.ctx.lineTo(mapX(m), mapY(comparedResult.curves.p90[m]));
}
for (let m = months; m >= 0; m--) {
this.ctx.lineTo(mapX(m), mapY(comparedResult.curves.p10[m]));
}
this.ctx.closePath();
this.ctx.fill();
}
// Draw total deposits line (dashed gray)
this.ctx.strokeStyle = '#888888';
this.ctx.lineWidth = 2;
this.ctx.setLineDash([8, 4]);
this.ctx.beginPath();
this.ctx.moveTo(mapX(0), mapY(totalDeposits[0]));
for (let m = 1; m <= months; m++) {
this.ctx.lineTo(mapX(m), mapY(totalDeposits[m]));
}
this.ctx.stroke();
this.ctx.setLineDash([]);
// Draw S&P 500 median line (green)
this.ctx.strokeStyle = '#4CAF50';
this.ctx.lineWidth = 2.5;
this.ctx.beginPath();
this.ctx.moveTo(mapX(0), mapY(sp500Result.curves.p50[0]));
for (let m = 1; m <= months; m++) {
this.ctx.lineTo(mapX(m), mapY(sp500Result.curves.p50[m]));
}
this.ctx.stroke();
// Draw compared fund median line (blue)
this.ctx.strokeStyle = '#2196F3';
this.ctx.lineWidth = 2.5;
this.ctx.beginPath();
this.ctx.moveTo(mapX(0), mapY(comparedResult.curves.p50[0]));
for (let m = 1; m <= months; m++) {
this.ctx.lineTo(mapX(m), mapY(comparedResult.curves.p50[m]));
}
this.ctx.stroke();
// Draw capital gains tax loss on compared fund (red segment)
const taxPaid = comparedResult.taxPaidPercentiles.p50;
const preTaxValue = comparedResult.preTaxPercentiles.p50;
const postTaxValue = comparedResult.percentiles.p50;
const endX = mapX(months);
if (taxPaid > 0) {
const preTaxY = mapY(preTaxValue);
const postTaxY = mapY(postTaxValue);
// Draw red line from pre-tax to post-tax
this.ctx.strokeStyle = '#F44336';
this.ctx.lineWidth = 3;
this.ctx.beginPath();
this.ctx.moveTo(endX, preTaxY);
this.ctx.lineTo(endX, postTaxY);
this.ctx.stroke();
// Draw red circle at post-tax point
this.ctx.fillStyle = '#F44336';
this.ctx.beginPath();
this.ctx.arc(endX, postTaxY, 4, 0, Math.PI * 2);
this.ctx.fill();
// Label the tax loss
this.ctx.fillStyle = '#F44336';
this.ctx.font = '10px monospace';
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('-' + formatCurrency(taxPaid) + ' tax', endX + 8, (preTaxY + postTaxY) / 2);
}
// Draw delta annotation between funds
const sp500Final = sp500Result.percentiles.p50;
const comparedFinal = comparedResult.percentiles.p50;
const delta = sp500Final - comparedFinal;
// Draw delta bracket/line
const sp500Y = mapY(sp500Final);
const comparedY = mapY(comparedFinal);
const bracketX = endX + 6;
this.ctx.strokeStyle = textColor;
this.ctx.lineWidth = 1;
this.ctx.setLineDash([]);
this.ctx.beginPath();
// Horizontal ticks
this.ctx.moveTo(endX + 2, sp500Y);
this.ctx.lineTo(bracketX, sp500Y);
this.ctx.moveTo(endX + 2, comparedY);
this.ctx.lineTo(bracketX, comparedY);
// Vertical line
this.ctx.moveTo(bracketX, sp500Y);
this.ctx.lineTo(bracketX, comparedY);
this.ctx.stroke();
// Delta label
this.ctx.fillStyle = delta >= 0 ? '#4CAF50' : '#F44336';
this.ctx.font = '11px monospace';
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
const deltaLabel = 'Δ ' + (delta >= 0 ? '+' : '') + formatCurrency(delta);
this.ctx.fillText(deltaLabel, bracketX + 4, (sp500Y + comparedY) / 2);
// Draw axes
this.ctx.strokeStyle = textColor;
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(padding.left, padding.top);
this.ctx.lineTo(padding.left, height - padding.bottom);
this.ctx.lineTo(width - padding.right, height - padding.bottom);
this.ctx.stroke();
// Y-axis labels
this.ctx.fillStyle = textColor;
this.ctx.font = '12px monospace';
this.ctx.textAlign = 'right';
this.ctx.textBaseline = 'middle';
for (let i = 0; i <= numYLines; i++) {
const value = maxVal * (1 - i / numYLines);
const y = padding.top + (chartHeight / numYLines) * i;
this.ctx.fillText(formatCurrency(value), padding.left - 10, y);
}
// X-axis labels
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'top';
for (let yr = 0; yr <= years; yr++) {
const x = padding.left + (chartWidth / years) * yr;
this.ctx.fillText(yr + 'y', x, height - padding.bottom + 10);
}
// Legend
const legendY = padding.top - 15;
const legendItems = [
{ color: '#4CAF50', label: 'S&P 500' },
{ color: '#2196F3', label: 'Compared' },
{ color: '#888888', label: 'Deposits', dashed: true },
{ color: '#F44336', label: 'Tax Loss' }
];
let legendX = padding.left;
this.ctx.font = '11px monospace';
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
for (const item of legendItems) {
// Draw line sample
this.ctx.strokeStyle = item.color;
this.ctx.lineWidth = 2;
if (item.dashed) {
this.ctx.setLineDash([4, 2]);
}
this.ctx.beginPath();
this.ctx.moveTo(legendX, legendY);
this.ctx.lineTo(legendX + 20, legendY);
this.ctx.stroke();
this.ctx.setLineDash([]);
// Draw label
this.ctx.fillStyle = textColor;
this.ctx.fillText(item.label, legendX + 25, legendY);
legendX += this.ctx.measureText(item.label).width + 50;
}
}
};
// Main calculator
const Calculator = {
form: null,
chartRenderer: null,
init: function() {
this.form = document.getElementById('fund-comparison-form');
this.chartRenderer = Object.create(ChartRenderer);
this.chartRenderer.init('fund-comparison-chart');
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.calculate();
});
window.addEventListener('resize', () => {
this.chartRenderer.resize();
this.calculate();
});
// Initial calculation
this.calculate();
},
getInputs: function() {
return {
sp500: {
annualReturn: parseFloat(document.getElementById('sp500-return').value) / 100,
annualVol: parseFloat(document.getElementById('sp500-volatility').value) / 100,
expenseRatio: parseFloat(document.getElementById('sp500-expense').value) / 100,
capitalGainsTax: 0,
monthlyDeposit: parseFloat(document.getElementById('monthly-deposit').value)
},
compared: {
annualReturn: parseFloat(document.getElementById('compared-return').value) / 100,
annualVol: parseFloat(document.getElementById('compared-volatility').value) / 100,
expenseRatio: parseFloat(document.getElementById('compared-admin-fee').value) / 100,
capitalGainsTax: parseFloat(document.getElementById('compared-capital-gains').value) / 100,
monthlyDeposit: parseFloat(document.getElementById('monthly-deposit').value)
},
years: parseInt(document.getElementById('time-frame').value),
useMonteCarlo: document.getElementById('use-monte-carlo').checked
};
},
calculate: function() {
const inputs = this.getInputs();
const months = inputs.years * 12;
const numSimulations = 1000;
let sp500Result, comparedResult;
if (inputs.useMonteCarlo) {
sp500Result = runMonteCarlo(inputs.sp500, months, numSimulations);
comparedResult = runMonteCarlo(inputs.compared, months, numSimulations);
} else {
sp500Result = runDeterministic(inputs.sp500, months);
comparedResult = runDeterministic(inputs.compared, months);
}
// Draw chart
this.chartRenderer.draw(
sp500Result,
comparedResult,
inputs.years,
inputs.sp500.monthlyDeposit,
inputs.useMonteCarlo
);
// Update summary
this.updateSummary(sp500Result, comparedResult, inputs);
},
updateSummary: function(sp500Result, comparedResult, inputs) {
const totalDeposits = inputs.sp500.monthlyDeposit * inputs.years * 12;
// Summary table
document.getElementById('total-deposits').textContent = formatCurrency(totalDeposits);
document.getElementById('sp500-final').textContent = formatCurrency(sp500Result.percentiles.p50);
const sp500Gain = sp500Result.percentiles.p50 - totalDeposits;
document.getElementById('sp500-gain').textContent = formatCurrency(sp500Gain);
document.getElementById('sp500-multiple').textContent = (sp500Result.percentiles.p50 / totalDeposits).toFixed(2) + 'x';
document.getElementById('compared-final').textContent = formatCurrency(comparedResult.percentiles.p50);
const comparedGain = comparedResult.percentiles.p50 - totalDeposits;
document.getElementById('compared-gain').textContent = formatCurrency(comparedGain);
document.getElementById('compared-multiple').textContent = (comparedResult.percentiles.p50 / totalDeposits).toFixed(2) + 'x';
// Percentiles section
const percentilesSection = document.getElementById('percentiles-section');
if (inputs.useMonteCarlo) {
percentilesSection.style.display = 'block';
document.getElementById('sp500-p10').textContent = formatCurrency(sp500Result.percentiles.p10);
document.getElementById('sp500-p25').textContent = formatCurrency(sp500Result.percentiles.p25);
document.getElementById('sp500-p50').textContent = formatCurrency(sp500Result.percentiles.p50);
document.getElementById('sp500-p75').textContent = formatCurrency(sp500Result.percentiles.p75);
document.getElementById('sp500-p90').textContent = formatCurrency(sp500Result.percentiles.p90);
document.getElementById('compared-p10').textContent = formatCurrency(comparedResult.percentiles.p10);
document.getElementById('compared-p25').textContent = formatCurrency(comparedResult.percentiles.p25);
document.getElementById('compared-p50').textContent = formatCurrency(comparedResult.percentiles.p50);
document.getElementById('compared-p75').textContent = formatCurrency(comparedResult.percentiles.p75);
document.getElementById('compared-p90').textContent = formatCurrency(comparedResult.percentiles.p90);
} else {
percentilesSection.style.display = 'none';
}
}
};
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => Calculator.init());
} else {
Calculator.init();
}
})();

View file

@ -0,0 +1,146 @@
<div class="fund-comparison-container">
<noscript>
<div class="noscript-warning">
This calculator requires JavaScript to function.
</div>
</noscript>
<form id="fund-comparison-form" class="fund-comparison-form">
<div class="form-section">
<h3>S&P 500 Index Fund</h3>
<div class="form-group">
<label for="sp500-return">Expected yearly return (%)</label>
<input type="number" id="sp500-return" value="10" step="0.1" required>
</div>
<div class="form-group">
<label for="sp500-volatility">Volatility / std dev (%)</label>
<input type="number" id="sp500-volatility" value="15" step="0.1" required>
</div>
<div class="form-group">
<label for="sp500-expense">Expense ratio (%)</label>
<input type="number" id="sp500-expense" value="0.03" step="0.01" required>
</div>
</div>
<div class="form-section">
<h3>Compared Fund</h3>
<div class="form-group">
<label for="compared-return">Expected yearly return (%)</label>
<input type="number" id="compared-return" value="7" step="0.1" required>
</div>
<div class="form-group">
<label for="compared-volatility">Volatility / std dev (%)</label>
<input type="number" id="compared-volatility" value="10" step="0.1" required>
</div>
<div class="form-group">
<label for="compared-admin-fee">Admin fee (% yearly)</label>
<input type="number" id="compared-admin-fee" value="1" step="0.01">
</div>
<div class="form-group">
<label for="compared-capital-gains">Capital gains tax at withdrawal (%)</label>
<input type="number" id="compared-capital-gains" value="0" step="0.1">
</div>
</div>
<div class="form-section form-section-full">
<h3>Investment Parameters</h3>
<div class="form-row">
<div class="form-group">
<label for="time-frame">Time frame (years)</label>
<input type="number" id="time-frame" value="20" min="1" max="50" required>
</div>
<div class="form-group">
<label for="monthly-deposit">Monthly deposit ($)</label>
<input type="number" id="monthly-deposit" value="500" min="1" required>
</div>
</div>
<div class="checkbox-group">
<input type="checkbox" id="use-monte-carlo" checked>
<label for="use-monte-carlo">Use Monte Carlo simulation (1000 runs)</label>
</div>
<button type="submit" class="calculate-btn">Calculate</button>
</div>
</form>
<div class="chart-container">
<canvas id="fund-comparison-chart"></canvas>
</div>
<div class="summary-section">
<h3>Summary</h3>
<table class="summary-table">
<thead>
<tr>
<th></th>
<th>S&P 500 Fund</th>
<th>Compared Fund</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total Deposits</td>
<td colspan="2" id="total-deposits">-</td>
</tr>
<tr>
<td>Final Value (median)</td>
<td id="sp500-final">-</td>
<td id="compared-final">-</td>
</tr>
<tr>
<td>Total Gain</td>
<td id="sp500-gain">-</td>
<td id="compared-gain">-</td>
</tr>
<tr>
<td>Return Multiple</td>
<td id="sp500-multiple">-</td>
<td id="compared-multiple">-</td>
</tr>
</tbody>
</table>
</div>
<div id="percentiles-section" class="summary-section">
<details class="percentiles-details">
<summary>Percentile Breakdown</summary>
<table class="summary-table percentiles-table">
<thead>
<tr>
<th>Percentile</th>
<th>S&P 500 Fund</th>
<th>Compared Fund</th>
</tr>
</thead>
<tbody>
<tr>
<td>10th (pessimistic)</td>
<td id="sp500-p10">-</td>
<td id="compared-p10">-</td>
</tr>
<tr>
<td>25th</td>
<td id="sp500-p25">-</td>
<td id="compared-p25">-</td>
</tr>
<tr>
<td>50th (median)</td>
<td id="sp500-p50">-</td>
<td id="compared-p50">-</td>
</tr>
<tr>
<td>75th</td>
<td id="sp500-p75">-</td>
<td id="compared-p75">-</td>
</tr>
<tr>
<td>90th (optimistic)</td>
<td id="sp500-p90">-</td>
<td id="compared-p90">-</td>
</tr>
</tbody>
</table>
</details>
</div>
</div>
<script src="{{ get_url(path='js/fundComparison.js') | safe }}"></script>