fix(calculator): 10 financial model fixes — IRR, NPV, OPEX growth, value bridge
Part B: Calculator improvements Fix 1 — annualRevGrowth was dead code. Now applied as compound multiplier from Year 2 onwards across all revenue streams (court, ancillary, membership, F&B, coaching, retail). Fix 2 — IRR initial outflow bug (HIGH). Was using -capex but NCFs are post-debt-service (levered). Using capex as denominator while using levered cash flows understates equity returns by the leverage ratio. Fix: use -equity as outflow for equity IRR, add separate projectIrr (unlevered, uses -capex with EBITDA flows). Fix 3 — NPV at hurdle rate. Discounts equity NCFs and exit proceeds at hurdleRate (default 12%). Reports npv and npvPositive. NPV > 0 iff equity IRR > hurdle rate. Added hurdleRate slider (5–35%) to Exit settings. Fix 4 — Remaining loan: replaces heuristic with correct amortization math (PV of remaining payments: monthlyPayment × (1 - (1+r)^-(n-k)) / r). Fix 5 — Exit EBITDA: uses terminal year EBITDA (holdYears - 1) instead of hardcoded Year 3. exitValue now reflects actual exit year, not always Y3. Fix 6 — MOIC: moic is now equity MOIC (total equity CFs / equity invested). projectMoic is the project-level multiple. Waterfall updated to show both. Fix 7 — Return decomposition / value bridge. Standard PE attribution: EBITDA growth value (operational alpha) + debt paydown (financial leverage). Displayed in tab_returns.html as an attribution table. Fix 8 — OPEX growth rate. annualOpexGrowth (default 2%) inflates utilities, staff, insurance from Year 2 onwards. Without this Y4-Y5 EBITDA was systematically overstated. Added annualOpexGrowth slider to Exit settings. Fix 9 — LTV and DSCR warnings. ltvWarning (>75%) and dscrWarning (<1.25x) with inline warnings in tab_metrics.html. Fix 10 — Interest-only period. interestOnlyMonths (0–24) reduces early NCF drag. Added slider to Financing section. Updated test: test_stab_ebitda_is_year3 → test_stab_ebitda_is_exit_year. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,9 @@ DEFAULTS = {
|
||||
"holdYears": 5,
|
||||
"exitMultiple": 6,
|
||||
"annualRevGrowth": 2,
|
||||
"annualOpexGrowth": 2,
|
||||
"hurdleRate": 12,
|
||||
"interestOnlyMonths": 0,
|
||||
"budgetTarget": 0,
|
||||
"country": "DE",
|
||||
"permitsCompliance": 12000,
|
||||
@@ -336,6 +339,9 @@ def calc(s: dict, lang: str = "en") -> dict:
|
||||
d["netCFMonth"] = d["ebitdaMonth"] - d["monthlyPayment"]
|
||||
|
||||
# -- 60-month cash flow projection --
|
||||
# Fix 1: annualRevGrowth applied to all revenue streams.
|
||||
# Fix 8: annualOpexGrowth applied to all operating costs (utilities, staff, insurance inflate).
|
||||
# Fix 10: interest-only period — first N months pay only interest, not P+I.
|
||||
months: list[dict] = []
|
||||
for m in range(1, 61):
|
||||
cm = (m - 1) % 12
|
||||
@@ -345,19 +351,32 @@ def calc(s: dict, lang: str = "en") -> dict:
|
||||
eff_util = (s["utilTarget"] / 100) * ramp * seas
|
||||
avail = s["hoursPerDay"] * dpm * total_courts if seas > 0 else 0
|
||||
booked = avail * eff_util
|
||||
court_rev = booked * w_rate
|
||||
|
||||
# Revenue growth compounds from Year 2 onwards (Year 1 = base)
|
||||
rev_growth = math.pow(1 + s["annualRevGrowth"] / 100, max(0, yr - 1))
|
||||
court_rev = booked * w_rate * rev_growth
|
||||
fees = -court_rev * (s["bookingFee"] / 100)
|
||||
ancillary = booked * (
|
||||
(s["racketRentalRate"] / 100) * s["racketQty"] * s["racketPrice"]
|
||||
+ (s["ballRate"] / 100) * (s["ballPrice"] - s["ballCost"])
|
||||
)
|
||||
membership = total_courts * s["membershipRevPerCourt"] * (ramp if seas > 0 else 0)
|
||||
fb = total_courts * s["fbRevPerCourt"] * (ramp if seas > 0 else 0)
|
||||
coaching = total_courts * s["coachingRevPerCourt"] * (ramp if seas > 0 else 0)
|
||||
retail = total_courts * s["retailRevPerCourt"] * (ramp if seas > 0 else 0)
|
||||
) * rev_growth
|
||||
membership = total_courts * s["membershipRevPerCourt"] * (ramp if seas > 0 else 0) * rev_growth
|
||||
fb = total_courts * s["fbRevPerCourt"] * (ramp if seas > 0 else 0) * rev_growth
|
||||
coaching = total_courts * s["coachingRevPerCourt"] * (ramp if seas > 0 else 0) * rev_growth
|
||||
retail = total_courts * s["retailRevPerCourt"] * (ramp if seas > 0 else 0) * rev_growth
|
||||
total_rev = court_rev + fees + ancillary + membership + fb + coaching + retail
|
||||
opex_val = -d["opex"]
|
||||
|
||||
# OPEX inflates from Year 2 onwards (utilities, staff, insurance)
|
||||
opex_growth = math.pow(1 + s["annualOpexGrowth"] / 100, max(0, yr - 1))
|
||||
opex_val = -(d["opex"] * opex_growth)
|
||||
|
||||
# Fix 10: interest-only period — lower debt service during construction/ramp
|
||||
if m <= s["interestOnlyMonths"] and d["loanAmount"] > 0:
|
||||
# Interest-only payment: loan balance × monthly rate
|
||||
loan = -(d["loanAmount"] * s["interestRate"] / 100 / 12)
|
||||
else:
|
||||
loan = -d["monthlyPayment"]
|
||||
|
||||
ebitda = total_rev + opex_val
|
||||
ncf = ebitda + loan
|
||||
prev = months[-1] if months else None
|
||||
@@ -387,29 +406,95 @@ def calc(s: dict, lang: str = "en") -> dict:
|
||||
d["annuals"] = annuals
|
||||
|
||||
# -- Returns & exit --
|
||||
y3_ebitda = annuals[2]["ebitda"] if len(annuals) >= 3 else 0
|
||||
d["stabEbitda"] = y3_ebitda
|
||||
d["exitValue"] = y3_ebitda * s["exitMultiple"]
|
||||
d["remainingLoan"] = d["loanAmount"] * max(0, 1 - s["holdYears"] / (max(s["loanTerm"], 1) * 1.5))
|
||||
# Fix 5: use terminal year EBITDA (exit year), not hardcoded Year 3
|
||||
exit_yr_idx = min(s["holdYears"] - 1, len(annuals) - 1)
|
||||
d["stabEbitda"] = annuals[exit_yr_idx]["ebitda"]
|
||||
d["exitValue"] = d["stabEbitda"] * s["exitMultiple"]
|
||||
|
||||
# Fix 4: remaining loan via amortization math (PV of remaining payments),
|
||||
# replacing the heuristic loanAmount * max(0, 1 - holdYears / (loanTerm * 1.5))
|
||||
k = s["holdYears"] * 12 # number of P+I payments made (after interest-only period)
|
||||
n = max(s["loanTerm"], 1) * 12
|
||||
r_monthly_loan = s["interestRate"] / 100 / 12
|
||||
if r_monthly_loan > 0 and d["loanAmount"] > 0 and n > k:
|
||||
d["remainingLoan"] = _round(
|
||||
d["monthlyPayment"] * (1 - math.pow(1 + r_monthly_loan, -(n - k))) / r_monthly_loan
|
||||
)
|
||||
elif d["loanAmount"] > 0 and n > k:
|
||||
# Zero-interest loan: straight-line amortization
|
||||
d["remainingLoan"] = _round(d["loanAmount"] * (n - k) / n)
|
||||
else:
|
||||
d["remainingLoan"] = 0
|
||||
|
||||
d["netExit"] = d["exitValue"] - d["remainingLoan"]
|
||||
|
||||
irr_cfs = [-d["capex"]]
|
||||
# Fix 2: equity IRR — use equity invested as initial outflow (not full capex).
|
||||
# NCFs are already post-debt-service (levered), so the denominator must match.
|
||||
# Using capex here would produce a hybrid metric that's neither equity IRR
|
||||
# nor project IRR — it systematically understates returns for leveraged deals.
|
||||
irr_cfs = [-d["equity"]]
|
||||
for y in range(s["holdYears"]):
|
||||
ycf = annuals[y]["ncf"] if y < len(annuals) else (annuals[-1]["ncf"] if annuals else 0)
|
||||
if y == s["holdYears"] - 1:
|
||||
irr_cfs.append(ycf + d["netExit"])
|
||||
else:
|
||||
irr_cfs.append(ycf)
|
||||
|
||||
d["irr"] = calc_irr(irr_cfs)
|
||||
|
||||
# Project IRR (unlevered): uses full capex as outflow and EBITDA as cash flows.
|
||||
# Useful for lender analysis and comparing across capital structures.
|
||||
unlevered_cfs = [-d["capex"]]
|
||||
for y in range(s["holdYears"]):
|
||||
ya = annuals[y] if y < len(annuals) else annuals[-1]
|
||||
if y == s["holdYears"] - 1:
|
||||
unlevered_cfs.append(ya["ebitda"] + d["netExit"])
|
||||
else:
|
||||
unlevered_cfs.append(ya["ebitda"])
|
||||
d["projectIrr"] = calc_irr(unlevered_cfs)
|
||||
|
||||
# Fix 3: NPV at hurdle rate (discounts equity NCFs + exit at hurdleRate)
|
||||
r_hurdle_monthly = math.pow(1 + s["hurdleRate"] / 100, 1 / 12) - 1
|
||||
pv_ncf = sum(m["ncf"] / math.pow(1 + r_hurdle_monthly, m["m"]) for m in months)
|
||||
pv_exit = d["netExit"] / math.pow(1 + s["hurdleRate"] / 100, s["holdYears"])
|
||||
d["npv"] = _round(-d["equity"] + pv_ncf + pv_exit)
|
||||
d["npvPositive"] = d["npv"] >= 0
|
||||
|
||||
d["totalReturned"] = sum(irr_cfs[1:])
|
||||
d["moic"] = d["totalReturned"] / d["capex"] if d["capex"] > 0 else 0
|
||||
|
||||
# Fix 6: leveraged MOIC (equity cash flows / equity invested — what the investor earns).
|
||||
# Also keep project MOIC (total returns / capex) for reference.
|
||||
equity_cfs = irr_cfs[1:]
|
||||
d["moic"] = sum(equity_cfs) / d["equity"] if d["equity"] > 0 else 0
|
||||
d["projectMoic"] = d["totalReturned"] / d["capex"] if d["capex"] > 0 else 0
|
||||
|
||||
# Fix 7: return decomposition / value bridge (PE-style attribution).
|
||||
# Shows what drove equity returns: operational improvement vs. financial leverage.
|
||||
entry_ebitda = annuals[0]["ebitda"] if annuals else 0
|
||||
ebitda_growth_value = (d["stabEbitda"] - entry_ebitda) * s["exitMultiple"]
|
||||
deleverage_value = d["loanAmount"] - d["remainingLoan"]
|
||||
d["valueDrivers"] = {
|
||||
"ebitda_growth": _round(ebitda_growth_value),
|
||||
"deleverage": _round(deleverage_value),
|
||||
"entry_equity": d["equity"],
|
||||
"exit_equity": _round(d["netExit"]),
|
||||
}
|
||||
|
||||
d["dscr"] = [
|
||||
{"year": a["year"], "dscr": a["ebitda"] / a["ds"] if a["ds"] > 0 else 999}
|
||||
for a in annuals
|
||||
]
|
||||
|
||||
# Fix 9: LTV and DSCR warnings for lender compliance thresholds
|
||||
d["ltvWarning"] = d["ltv"] > 0.75 # above typical commercial RE lending limit
|
||||
d["dscrWarning"] = any(row["dscr"] < 1.25 for row in d["dscr"] if row["dscr"] < 999)
|
||||
d["dscrMinYear"] = None
|
||||
if d["dscrWarning"]:
|
||||
d["dscrMinYear"] = min(
|
||||
(row["year"] for row in d["dscr"] if row["dscr"] < 999),
|
||||
key=lambda yr: next(r["dscr"] for r in d["dscr"] if r["year"] == yr),
|
||||
default=None,
|
||||
)
|
||||
|
||||
payback_idx = -1
|
||||
for i, m in enumerate(months):
|
||||
if m["cum"] >= 0:
|
||||
|
||||
@@ -61,11 +61,17 @@
|
||||
<div class="metric-card__label">DSCR (Y3) <span class="ti">i<span class="tp">{{ t.tip_result_dscr }}</span></span></div>
|
||||
<div class="metric-card__value {{ 'c-green' if y3_dscr >= 1.2 else 'c-red' }}">{{ '∞' if y3_dscr > 99 else y3_dscr | fmt_x }}</div>
|
||||
<div class="metric-card__sub">Min 1.2x for banks</div>
|
||||
{% if d.dscrWarning %}
|
||||
<div class="metric-card__warn c-red" style="font-size:10px;margin-top:4px">⚠ DSCR < 1.25x in Y{{ d.dscrMinYear }} — bank covenant breach risk</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="metric-card metric-card-sm">
|
||||
<div class="metric-card__label">LTV</div>
|
||||
<div class="metric-card__value c-head">{{ d.ltv | fmt_pct }}</div>
|
||||
<div class="metric-card__value {{ 'c-amber' if d.ltvWarning else 'c-head' }}">{{ d.ltv | fmt_pct }}</div>
|
||||
<div class="metric-card__sub">Loan ÷ Total Investment</div>
|
||||
{% if d.ltvWarning %}
|
||||
<div class="metric-card__warn c-amber" style="font-size:10px;margin-top:4px">⚠ LTV > 75% — above typical commercial lending limit</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="metric-card metric-card-sm">
|
||||
<div class="metric-card__label">Debt Yield <span class="ti">i<span class="tp">{{ t.tip_result_debt_yield }}</span></span></div>
|
||||
@@ -101,7 +107,7 @@
|
||||
<div class="metric-card metric-card-sm">
|
||||
<div class="metric-card__label">Exit Value</div>
|
||||
<div class="metric-card__value c-head">{{ d.exitValue | fmt_k }}</div>
|
||||
<div class="metric-card__sub">{{ s.exitMultiple }}x Y3 EBITDA</div>
|
||||
<div class="metric-card__sub">{{ s.exitMultiple }}x Y{{ s.holdYears }} EBITDA</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<div class="grid-4 mb-4">
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">{{ t.card_irr }} <span class="ti">i<span class="tp">{{ t.tip_result_irr }}</span></span></div>
|
||||
<div class="metric-card__label">{{ t.card_irr }} (Equity) <span class="ti">i<span class="tp">{{ t.tip_result_irr }}</span></span></div>
|
||||
<div class="metric-card__value {{ 'c-green' if d.irr_ok and d.irr > 0.2 else 'c-red' }}">{{ d.irr | fmt_pct if d.irr_ok else 'N/A' }}</div>
|
||||
<div class="metric-card__sub">{{ '✓ Above 20%' if d.irr_ok and d.irr > 0.2 else '✗ Below target' }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">{{ t.card_moic }} <span class="ti">i<span class="tp">{{ t.tip_result_moic }}</span></span></div>
|
||||
<div class="metric-card__label">{{ t.card_moic }} (Equity) <span class="ti">i<span class="tp">{{ t.tip_result_moic }}</span></span></div>
|
||||
<div class="metric-card__value {{ 'c-green' if d.moic > 2 else 'c-red' }}">{{ d.moic | fmt_x }}</div>
|
||||
<div class="metric-card__sub">{{ '✓ Above 2.0x' if d.moic > 2 else '✗ Below 2.0x' }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">NPV <span class="ti">i<span class="tp">At {{ s.hurdleRate }}% hurdle rate</span></span></div>
|
||||
<div class="metric-card__value {{ 'c-green' if d.npvPositive else 'c-red' }}">{{ d.npv | fmt_k }}</div>
|
||||
<div class="metric-card__sub">{{ '✓ Value-creating' if d.npvPositive else '✗ Destroys value' }} at {{ s.hurdleRate }}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">{{ t.card_break_even }} <span class="ti">i<span class="tp">{{ t.tip_result_break_even }}</span></span></div>
|
||||
<div class="metric-card__value {{ 'c-green' if d.breakEvenUtil < 0.35 else 'c-amber' }}">{{ d.breakEvenUtil | fmt_pct }}</div>
|
||||
<div class="metric-card__sub">{{ d.breakEvenHrsPerCourt | round(1) }} hrs/court/day</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">{{ t.card_cash_on_cash }} <span class="ti">i<span class="tp">{{ t.tip_result_coc }}</span></span></div>
|
||||
<div class="metric-card__value {{ 'c-green' if d.cashOnCash > 0.15 else 'c-amber' }}">{{ d.cashOnCash | fmt_pct }}</div>
|
||||
<div class="metric-card__sub">Year 3 NCF ÷ Equity</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mb-4">
|
||||
@@ -33,8 +33,9 @@
|
||||
(t.wf_net_exit, d.netExit | int | fmt_currency, 'c-green' if d.netExit > 0 else 'c-red'),
|
||||
(t.wf_cum_cf, (d.totalReturned - d.netExit) | int | fmt_currency, 'c-head'),
|
||||
(t.wf_total_returns, d.totalReturned | int | fmt_currency, 'c-green' if d.totalReturned > 0 else 'c-red'),
|
||||
(t.wf_investment, d.capex | fmt_currency, 'c-head'),
|
||||
(t.wf_moic, d.moic | fmt_x, 'c-green' if d.moic > 2 else 'c-red'),
|
||||
('Equity invested', d.equity | fmt_currency, 'c-head'),
|
||||
('Equity MOIC', d.moic | fmt_x, 'c-green' if d.moic > 2 else 'c-red'),
|
||||
('Project MOIC (on CAPEX)', d.projectMoic | fmt_x, 'c-head'),
|
||||
] %}
|
||||
{% for label, value, cls in wf_rows %}
|
||||
<div class="waterfall-row">
|
||||
@@ -44,12 +45,50 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-container__label" style="font-size:10px">Value Bridge (Equity Attribution)</div>
|
||||
<div id="valueBridge" style="margin-top:10px">
|
||||
{% set vd = d.valueDrivers %}
|
||||
<div class="waterfall-row">
|
||||
<span class="waterfall-row__label">Equity invested</span>
|
||||
<span class="waterfall-row__value c-head">{{ vd.entry_equity | fmt_currency }}</span>
|
||||
</div>
|
||||
<div class="waterfall-row">
|
||||
<span class="waterfall-row__label">+ EBITDA growth value <span class="ti" style="font-size:10px">i<span class="tp">Improvement in EBITDA × exit multiple</span></span></span>
|
||||
<span class="waterfall-row__value {{ 'c-green' if vd.ebitda_growth >= 0 else 'c-red' }}">{{ vd.ebitda_growth | fmt_currency }}</span>
|
||||
</div>
|
||||
<div class="waterfall-row">
|
||||
<span class="waterfall-row__label">+ Debt paydown <span class="ti" style="font-size:10px">i<span class="tp">Loan balance reduction over hold period</span></span></span>
|
||||
<span class="waterfall-row__value c-green">{{ vd.deleverage | fmt_currency }}</span>
|
||||
</div>
|
||||
<div class="waterfall-row" style="border-top:1px solid var(--c-border);margin-top:4px;padding-top:4px">
|
||||
<span class="waterfall-row__label"><b>Net exit proceeds</b></span>
|
||||
<span class="waterfall-row__value {{ 'c-green' if vd.exit_equity > vd.entry_equity else 'c-red' }}"><b>{{ vd.exit_equity | fmt_currency }}</b></span>
|
||||
</div>
|
||||
<div class="waterfall-row" style="margin-top:8px">
|
||||
<span class="waterfall-row__label">Project IRR (unlevered)</span>
|
||||
<span class="waterfall-row__value c-head">{{ d.projectIrr | fmt_pct }}</span>
|
||||
</div>
|
||||
<div class="waterfall-row">
|
||||
<span class="waterfall-row__label">Equity IRR (levered)</span>
|
||||
<span class="waterfall-row__value {{ 'c-green' if d.irr_ok and d.irr > 0.2 else 'c-red' }}">{{ d.irr | fmt_pct if d.irr_ok else 'N/A' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mb-4">
|
||||
<div class="chart-container">
|
||||
<div class="chart-container__label">{{ t.planner_chart_dscr }}</div>
|
||||
<div class="chart-h-44 chart-container__canvas"><canvas id="chartDSCR"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-container__label">Cash Flow Cumulative</div>
|
||||
<div class="chart-h-44 chart-container__canvas"><canvas id="chartCum"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/json" id="chartDSCR-data">{{ d.dscr_chart | tojson }}</script>
|
||||
<script type="application/json" id="chartCum-data">{{ d.cum_chart | tojson }}</script>
|
||||
|
||||
<div class="mb-section">
|
||||
<div class="section-header"><h3>{{ t.planner_section_util_sensitivity }}</h3></div>
|
||||
|
||||
@@ -329,6 +329,7 @@
|
||||
{{ slider('interestRate', t.sl_interest_rate, 0, 15, 0.1, s.interestRate, t.tip_interest_rate) }}
|
||||
{{ slider('loanTerm', t.sl_loan_term, 0, 30, 1, s.loanTerm, t.tip_loan_term) }}
|
||||
{{ slider('constructionMonths', t.sl_construction_months, 0, 24, 1, s.constructionMonths, t.tip_construction_months) }}
|
||||
{{ slider('interestOnlyMonths', t.sl_interest_only_months|default('Interest-Only Period (mo)'), 0, 24, 1, s.interestOnlyMonths, t.tip_interest_only_months|default('Months of interest-only payments before P+I amortization begins. Reduces early cash flow drag during ramp.')) }}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -338,6 +339,8 @@
|
||||
{{ slider('holdYears', t.sl_hold_years, 1, 20, 1, s.holdYears, t.tip_hold_years) }}
|
||||
{{ slider('exitMultiple', t.sl_exit_multiple, 0, 20, 0.5, s.exitMultiple, t.tip_exit_multiple) }}
|
||||
{{ slider('annualRevGrowth', t.sl_annual_rev_growth, 0, 15, 0.5, s.annualRevGrowth, t.tip_annual_rev_growth) }}
|
||||
{{ slider('annualOpexGrowth', t.sl_annual_opex_growth|default('Annual OpEx Growth (%)'), 0, 10, 0.5, s.annualOpexGrowth, t.tip_annual_opex_growth|default('Annual cost inflation for utilities, staff, and insurance. 2% matches Western European CPI. Without this, Year 4–5 EBITDA is overstated.')) }}
|
||||
{{ slider('hurdleRate', t.sl_hurdle_rate|default('Hurdle Rate (%)'), 5, 35, 1, s.hurdleRate, t.tip_hurdle_rate|default('Minimum equity return required. NPV is positive when equity IRR exceeds this rate. 12% is typical for mid-market sports venues in Western Europe.')) }}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -298,8 +298,11 @@ class TestCalcDefaultScenario:
|
||||
month_rev = sum(m["totalRev"] for m in d["months"] if m["yr"] == y)
|
||||
assert annual["revenue"] == approx(month_rev)
|
||||
|
||||
def test_stab_ebitda_is_year3(self, d):
|
||||
assert d["stabEbitda"] == d["annuals"][2]["ebitda"]
|
||||
def test_stab_ebitda_is_exit_year(self, d):
|
||||
# stabEbitda uses the terminal year (holdYears - 1), not hardcoded Year 3.
|
||||
s = default_state()
|
||||
exit_idx = min(s["holdYears"] - 1, len(d["annuals"]) - 1)
|
||||
assert d["stabEbitda"] == d["annuals"][exit_idx]["ebitda"]
|
||||
|
||||
def test_exit_value(self, d):
|
||||
assert d["exitValue"] == approx(d["stabEbitda"] * 6)
|
||||
|
||||
Reference in New Issue
Block a user