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.
Vivir hoy de forma eficiente mientras construyes el capital para tu hogar definitivo.
Tu ahorro líquido supera el umbral mínimo de seguridad. La liquidez mensual es razonable.
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 |
|---|
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 = `
`; 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 = `
`; 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();