Categorías: Uncategorized

Calculadora viabilidad financiera

Calculadora Financiera de Viabilidad

Evalúa cuánto efectivo conservarías cada mes, cuánto capital acumularías para la vivienda definitiva
y cuál sería tu patrimonio proyectado a 3 años viviendo en una Ovalt autosuficiente con retorno progresivo.

Concepto
Vivienda eficiente, 100% sostenible y cero emisiones
Horizonte
Proyección automática a 36 meses
Modelo
Alquiler con retorno progresivo
Enfoque
Liquidez mensual + capital acumulado para compra

Panel de control

Vivir hoy de forma eficiente mientras construyes el capital para tu hogar definitivo.

Ahorro en Cuenta
514,00 €
Efectivo mensual que queda tras pagar alquiler y gastos de vida.
Capital para la Casa Definitiva
650,00 €
Importe que regresa a tu hucha patrimonial cada mes.
Patrimonio Total Mensual
1.164,00 €
Suma del ahorro líquido más el retorno acumulado.
Proyección a 3 años
41.904,00 €
Capital total estimado acumulado en 36 meses.
% de retorno aplicado
50,00%
Cashback progresivo interpolado linealmente según el alquiler.

Flujo mensual actual

Ingresos netos
2.654,00 €
Alquiler Ovalt
1.300,00 €
Gastos de vida
840,00 €
Ahorro líquido
514,00 €
Hucha mensual
650,00 €

Anillo de retorno

50,00%
retorno aplicado sobre el alquiler mensual

Salud financiera

Indicador de liquidez mensual

Tu ahorro líquido supera el umbral mínimo de seguridad. La liquidez mensual es razonable.

Liquidez razonable

Tabla comparativa de escenarios

Comparativa automática de los cuatro niveles definidos: Sensato, Seguro, Inversor y Límite.

Escenario Alquiler % Retorno Hucha Mensual Ahorro Líquido Patrimonio Total Mensual Proyección 3 años Salud financiera

Fórmulas integradas

La calculadora aplica exactamente la lógica financiera definida para el programa Ovalt + Step Home.

Concepto Fórmula
Porcentaje de retorno 0,50 + ((alquiler − 1300) / (1814 − 1300)) × (0,70 − 0,50), limitado entre 50% y 70%
Hucha mensual alquiler × % de retorno
Ahorro líquido ingresos netos − alquiler − gastos de vida
Patrimonio total mensual ahorro líquido + hucha mensual
Proyección a 3 años patrimonio total mensual × 36

Si el ahorro líquido baja de 100 €, el sistema marca automáticamente zona de riesgo de liquidez.
Si además es negativo, se muestra como déficit mensual.

const scenarios = [ { rent: 1300, name: "Sensato", className: "s-sensato" }, { rent: 1450, name: "Seguro", className: "s-seguro" }, { rent: 1600, name: "Inversor", className: "s-inversor" }, { rent: 1814, name: "Límite", className: "s-limite" } ];

const els = { incomeInput: document.getElementById("incomeInput"), rentInput: document.getElementById("rentInput"), rentSlider: document.getElementById("rentSlider"), expensesInput: document.getElementById("expensesInput"), rentDisplayTop: document.getElementById("rentDisplayTop"),

cashSavingsValue: document.getElementById("cashSavingsValue"), houseCapitalValue: document.getElementById("houseCapitalValue"), monthlyTotalValue: document.getElementById("monthlyTotalValue"), projectionValue: document.getElementById("projectionValue"), returnRateValue: document.getElementById("returnRateValue"), returnMetric: document.getElementById("returnMetric"),

flowIncomeLabel: document.getElementById("flowIncomeLabel"), flowRentLabel: document.getElementById("flowRentLabel"), flowLivingLabel: document.getElementById("flowLivingLabel"), flowCashLabel: document.getElementById("flowCashLabel"), flowReturnLabel: document.getElementById("flowReturnLabel"),

flowIncomeBar: document.getElementById("flowIncomeBar"), flowRentBar: document.getElementById("flowRentBar"), flowLivingBar: document.getElementById("flowLivingBar"), flowCashBar: document.getElementById("flowCashBar"), flowReturnBar: document.getElementById("flowReturnBar"),

ring: document.getElementById("returnRing"), ringValue: document.getElementById("ringValue"),

healthBadge: document.getElementById("healthBadge"), healthDescription: document.getElementById("healthDescription"),

comparisonTableBody: document.getElementById("comparisonTableBody"), scenarioChart: document.getElementById("scenarioChart") };

const euro = new Intl.NumberFormat("es-ES", { style: "currency", currency: "EUR", minimumFractionDigits: 2, maximumFractionDigits: 2 });

const percentFmt = new Intl.NumberFormat("es-ES", { style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2 });

function clamp(value, min, max) { return Math.min(Math.max(value, min), max); }

function sanitizePositiveNumber(value, fallback = 0) { const n = Number(value); if (!Number.isFinite(n) || n < 0) return fallback; return n; } function calculateReturnRate(rent) { const raw = 0.50 + ((rent - MIN_RENT) / (MAX_RENT - MIN_RENT)) * (0.70 - 0.50); return clamp(raw, 0.50, 0.70); } function calculateScenario(income, rent, expenses) { const returnRate = calculateReturnRate(rent); const monthlyReturn = rent * returnRate; const liquidSavings = income - rent - expenses; const monthlyWealth = liquidSavings + monthlyReturn; const projection3Years = monthlyWealth * 36; return { income, rent, expenses, returnRate, monthlyReturn, liquidSavings, monthlyWealth, projection3Years }; } function formatMoney(value) { return euro.format(value); } function formatPercent(value) { return percentFmt.format(value); } function getHealthState(liquidSavings) { if (liquidSavings < 0) { return { label: "Déficit mensual", className: "status-deficit", text: "Tus ingresos no cubren completamente alquiler y gastos fijos. Existe déficit de caja y este escenario no es sostenible sin ingresos adicionales o reducción de costes.", dotClass: "dot-deficit" }; } if (liquidSavings < 100) { return { label: "Zona de riesgo de liquidez", className: "status-risk", text: "Tu ahorro líquido queda por debajo de 100 €. Aunque el patrimonio mensual siga creciendo por el retorno, la liquidez diaria queda en una zona frágil.", dotClass: "dot-risk" }; } return { label: "Liquidez razonable", className: "status-good", text: "Tu ahorro líquido supera el umbral mínimo de seguridad. La liquidez mensual es razonable para sostener el modelo con mayor tranquilidad.", dotClass: "dot-good" }; } function updateInputsFromRent(rent) { const safeRent = clamp(Math.round(rent), MIN_RENT, MAX_RENT); els.rentInput.value = safeRent; els.rentSlider.value = safeRent; els.rentDisplayTop.textContent = formatMoney(safeRent); } function setBarWidth(element, value, base) { const pct = base <= 0 ? 0 : clamp((value / base) * 100, 0, 100); element.style.width = pct + "%"; } function renderDashboard(data) { els.cashSavingsValue.textContent = formatMoney(data.liquidSavings); els.houseCapitalValue.textContent = formatMoney(data.monthlyReturn); els.monthlyTotalValue.textContent = formatMoney(data.monthlyWealth); els.projectionValue.textContent = formatMoney(data.projection3Years); els.returnRateValue.textContent = formatPercent(data.returnRate); els.flowIncomeLabel.textContent = formatMoney(data.income); els.flowRentLabel.textContent = formatMoney(data.rent); els.flowLivingLabel.textContent = formatMoney(data.expenses); els.flowCashLabel.textContent = formatMoney(data.liquidSavings); els.flowReturnLabel.textContent = formatMoney(data.monthlyReturn); const maxBase = Math.max(data.income, data.monthlyReturn, data.rent, data.expenses, Math.abs(data.liquidSavings), 1); setBarWidth(els.flowIncomeBar, data.income, maxBase); setBarWidth(els.flowRentBar, data.rent, maxBase); setBarWidth(els.flowLivingBar, data.expenses, maxBase); setBarWidth(els.flowCashBar, Math.abs(data.liquidSavings), maxBase); setBarWidth(els.flowReturnBar, data.monthlyReturn, maxBase); const returnPct0to100 = data.returnRate * 100; els.ring.style.setProperty("--progress", returnPct0to100.toFixed(2)); els.ringValue.textContent = formatPercent(data.returnRate); const health = getHealthState(data.liquidSavings); els.healthBadge.className = "status-badge " + health.className; els.healthBadge.textContent = health.label; els.healthDescription.textContent = health.text; els.cashSavingsValue.style.color = data.liquidSavings < 0 ? "var(--danger)" : "var(--text)"; els.returnMetric.classList.toggle("danger", data.liquidSavings < 100); } function renderComparisonTable(income, expenses) { els.comparisonTableBody.innerHTML = ""; scenarios.forEach((scenario) => { const data = calculateScenario(income, scenario.rent, expenses); const health = getHealthState(data.liquidSavings);

const row = document.createElement("tr"); row.innerHTML = `

${scenario.name} ${formatMoney(data.rent)} ${formatPercent(data.returnRate)} ${formatMoney(data.monthlyReturn)} ${formatMoney(data.liquidSavings)} ${formatMoney(data.monthlyWealth)} ${formatMoney(data.projection3Years)} ${health.label}

`; els.comparisonTableBody.appendChild(row); }); }

function renderScenarioChart(income, expenses) { els.scenarioChart.innerHTML = "";

const dataRows = scenarios.map(s => { const data = calculateScenario(income, s.rent, expenses); return { ...s, ...data }; });

const maxWealth = Math.max(...dataRows.map(x => Math.max(x.monthlyWealth, 0)), 1);

dataRows.forEach((row) => { const safeCash = Math.max(row.liquidSavings, 0); const totalPositive = safeCash + row.monthlyReturn; const totalWidth = clamp((Math.max(row.monthlyWealth, 0) / maxWealth) * 100, 0, 100); const cashWidth = totalPositive > 0 ? (safeCash / totalPositive) * totalWidth : 0; const returnWidth = totalPositive > 0 ? (row.monthlyReturn / totalPositive) * totalWidth : 0;

const item = document.createElement("div"); item.className = "scenario-row"; item.innerHTML = `

${row.name}
Patrimonio mensual: ${formatMoney(row.monthlyWealth)}
Ahorro en cuenta: ${formatMoney(row.liquidSavings)} Capital retornado: ${formatMoney(row.monthlyReturn)}

`; els.scenarioChart.appendChild(item); }); }

function update() { const income = sanitizePositiveNumber(els.incomeInput.value, 2654); const expenses = sanitizePositiveNumber(els.expensesInput.value, 840); const rent = clamp(sanitizePositiveNumber(els.rentInput.value, MIN_RENT), MIN_RENT, MAX_RENT);

els.incomeInput.value = Math.round(income); els.expensesInput.value = Math.round(expenses); updateInputsFromRent(rent);

const current = calculateScenario(income, rent, expenses);

renderDashboard(current); renderComparisonTable(income, expenses); renderScenarioChart(income, expenses); }

els.incomeInput.addEventListener("input", update); els.expensesInput.addEventListener("input", update);

els.rentInput.addEventListener("input", (e) => { const value = clamp(sanitizePositiveNumber(e.target.value, MIN_RENT), MIN_RENT, MAX_RENT); els.rentSlider.value = value; els.rentDisplayTop.textContent = formatMoney(value); update(); });

els.rentSlider.addEventListener("input", (e) => { const value = Number(e.target.value); els.rentInput.value = value; els.rentDisplayTop.textContent = formatMoney(value); update(); });

document.querySelectorAll(".pill").forEach((btn) => { btn.addEventListener("click", () => { const rent = Number(btn.dataset.rent); updateInputsFromRent(rent); update(); }); });

update();

Pedro1SB