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:
Deeman
2026-02-24 00:11:52 +01:00
parent 0960990373
commit 194100c577
5 changed files with 164 additions and 28 deletions

View File

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