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:
@@ -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