feat(financial): levered (equity) IRR — capstone финмодели #1881 #1900

Merged
bot-backend merged 2 commits from feat/levered-equity-irr into main 2026-06-25 09:06:46 +00:00
11 changed files with 335 additions and 30 deletions

View file

@ -44,6 +44,10 @@ class FinancialModel(BaseModel):
``irr_is_proxy=True``. VAT is a documented simplification (see docstring), not
exact НК РФ mechanics.
``levered_irr`` IRR на собственные средства (equity cashflow при LTC): тот же
DCF-IRR + roi-proxy fallback, но по equity-потоку графика долга. При положительном
леверидже выше unlevered ``irr``; вырожденные случаи 0.0 + ``levered_irr_is_proxy``.
The legacy summary fields (``revenue_rub`` / ``cost_rub`` /
``gross_margin_rub`` / ``irr``) are kept for backward-compat; the new fields
expose the full cascade, the net P&L and the DCF metrics.
@ -108,6 +112,13 @@ class FinancialModel(BaseModel):
net_profit_after_financing_rub: float = 0.0 # net_profit total_interest
# honest-флаг: 100% покрытие разрыва, compound %, эскроу не моделируется точно.
financing_is_simplified: bool = True
# Levered (equity) IRR: доходность на собственные средства (equity cashflow при LTC).
# При положительном леверидже > unlevered ``irr``. Считается DCF-IRR'ом equity-потока
# того же графика долга, с тем же roi-proxy fallback'ом, что и unlevered IRR.
levered_irr: float = 0.0 # IRR на собственные средства (equity cashflow при LTC)
# honest-флаг: True → levered_irr — roi-proxy (вырожденный equity-поток / нет equity
# под риском при LTC>=1 / финансирование выключено), не настоящий DCF-IRR.
levered_irr_is_proxy: bool = True # True → levered_irr — roi-proxy, не DCF
# ── Price calibration (PR-2, эпик #1881) ───────────────────────────────────
# Цена продажи жилья, руб/кв.м, фактически использованная в выручке. Либо

View file

@ -116,9 +116,16 @@ Unlevered (project) DCF — NPV / IRR / PBP — остаётся HEADLINE и Н
* **Эскроу-специфику не моделируем точно** раскрытие средств с эскроу-счетов
по этапам стройки, лимиты раскрытия, ставка по бридж/целевой части всё это
упрощено до единой капитализируемой ставки на кассовый разрыв.
* **Levered IRR НЕ считается** (отдельный PR). MVP раскрывает только агрегаты:
``peak_debt_rub`` / ``total_interest_rub`` / ``net_profit_after_financing_rub``;
debt_schedule в схему НЕ выносится.
* **Levered (equity) IRR** считается по equity-потоку того же графика долга
(``_FinancingResult.equity_cashflow``): отток = equity-доля (1LTC) каждого
кассового разрыва, приток = остаток притока после обслуживания долга. При
положительном леверидже (доходность проекта > стоимости долга 15 %) levered IRR
выше unlevered. Тот же proxy-fallback, что у unlevered (нет смены знака roi-style
proxy + ``levered_irr_is_proxy=True``). Вырожденные случаи (LTC>=1 equity не под
риском, финансирование выключено, нет equity-оттока) ``levered_irr=0.0`` /
``levered_irr_is_proxy=True`` (не выдумываем доходность). debt_schedule в схему НЕ
выносится. MVP-агрегаты: ``peak_debt_rub`` / ``total_interest_rub`` /
``net_profit_after_financing_rub`` (без изменений).
* **Unlevered NPV / IRR / PBP НЕ затронуты** overlay'ем — это всё та же
интринсивная доходность. График наследует неточность ``schedule_is_default``:
финансирование считается по тому же дефолт-графику фаз, поэтому
@ -339,11 +346,29 @@ def _payback_months(cf: list[float]) -> float | None:
return None
@dataclass(frozen=True)
class _FinancingResult:
"""Результат overlay'я финансирования: агрегаты долга + ПОМЕСЯЧНЫЙ equity-поток.
Агрегаты (``peak_debt_rub`` / ``total_interest_rub`` / ``ending_debt_rub``)
сохраняют прежний READ-ONLY-контракт PR-5 (значения не изменились). ``equity_cashflow``
НОВЫЙ поток держателя equity по тому же графику долга (для levered IRR, отдельный PR):
отток = equity-доля (1ltc) каждого кассового разрыва (со знаком ), приток =
остаток притока после обслуживания долга. Σ equity_cashflow == net_profit_after_financing
(equity получает всю прибыль за вычетом процентов; банк только тело + проценты).
"""
peak_debt_rub: float
total_interest_rub: float
ending_debt_rub: float
equity_cashflow: list[float]
def _compute_financing(
operating_cf: list[float],
annual_rate: float,
ltc_ratio: float = 1.0,
) -> dict[str, float]:
) -> _FinancingResult:
"""OVERLAY финансирования поверх unlevered cashflow (PR-5). Чистая функция.
``operating_cf`` готовый unlevered cashflow (READ-ONLY, НЕ мутируется).
@ -356,28 +381,37 @@ def _compute_financing(
Если поток не уходит в минус долг не выбирается debt=0, interest=0
(корректно: финансирование не требуется).
Возвращает агрегаты (без debt_schedule только итоговые числа для MVP):
``peak_debt_rub`` пиковая задолженность, ``total_interest_rub`` суммарные
капитализированные проценты, ``ending_debt_rub`` остаток долга на конец.
ВОЗВРАТ :class:`_FinancingResult`: агрегаты (``peak_debt_rub`` пиковая
задолженность, ``total_interest_rub`` суммарные капитализированные проценты,
``ending_debt_rub`` остаток долга на конец) ПЛЮС ``equity_cashflow``
помесячный поток держателя equity по тому же графику долга (для levered IRR,
отдельный PR; см. :class:`_FinancingResult`). Агрегаты не изменились контракт цел.
"""
monthly_rate = (1.0 + annual_rate) ** (1.0 / 12.0) - 1.0
debt = 0.0
peak_debt = 0.0
total_interest = 0.0
equity_cashflow: list[float] = []
for cf in operating_cf:
interest = debt * monthly_rate # процент на остаток на начало месяца (до движений)
debt += interest
total_interest += interest
if cf < 0:
debt += -cf * ltc_ratio # банк покрывает ltc_ratio от оттока; equity-часть без %
# Equity вносит (1ltc) долю оттока (со знаком , как и cf): свои деньги под риском.
equity_cashflow.append(cf * (1.0 - ltc_ratio))
else:
debt -= min(cf, debt) # гасим долг притоком (не уходим в минус)
repay = min(cf, debt) # гасим долг притоком (не уходим в минус)
debt -= repay
# Остаток притока после обслуживания долга достаётся equity (приток, +).
equity_cashflow.append(cf - repay)
peak_debt = max(peak_debt, debt)
return {
"peak_debt_rub": round(peak_debt, 2),
"total_interest_rub": round(total_interest, 2),
"ending_debt_rub": round(debt, 2),
}
return _FinancingResult(
peak_debt_rub=round(peak_debt, 2),
total_interest_rub=round(total_interest, 2),
ending_debt_rub=round(debt, 2),
equity_cashflow=equity_cashflow,
)
def _spread_linear(total: float, start: int, duration: int, horizon: int) -> list[float]:
@ -646,9 +680,35 @@ def compute_financial(
# ── Финансирование: OVERLAY поверх unlevered cashflow (PR-5) ───────────────
# Тот же массив cashflow, что построен выше (НЕ пересобираем — защита инварианта:
# overlay читает unlevered поток и НЕ мутирует его). Проценты — реальная стоимость
# долга, режут прибыль: net_after < net. Levered IRR НЕ считаем (отдельный PR).
# долга, режут прибыль: net_after < net.
financing = _compute_financing(cashflow, _CREDIT_RATE_ANNUAL, _LOAN_TO_COST_RATIO)
net_profit_after_financing = net_profit - financing["total_interest_rub"]
net_profit_after_financing = net_profit - financing.total_interest_rub
# ── Levered (equity) IRR — доходность на собственные средства ───────────────
# IRR equity-потока того же графика долга. equity_invested = Σ |отрицательных
# equity_cf| (суммарный отток собственных средств). Вырожденные случаи (LTC>=1 —
# equity не под риском, нет equity-оттока) → levered_irr=0.0 / proxy (не выдумываем
# доходность). Иначе — тот же DCF-IRR + roi-proxy fallback, что у unlevered.
equity_cf = financing.equity_cashflow
equity_invested = sum(-c for c in equity_cf if c < 0.0)
if equity_invested <= 0.0:
# LTC>=1.0 (банк покрывает весь разрыв) ИЛИ нет кассового разрыва → equity не
# рискует → доходность equity не определена. Honest: 0.0 + proxy-флаг.
levered_irr = 0.0
levered_irr_is_proxy = True
else:
monthly_levered_irr = _irr_monthly(equity_cf)
if monthly_levered_irr is not None:
# Настоящий levered IRR: месячный → годовой эффективный (как unlevered).
levered_irr = (1.0 + monthly_levered_irr) ** 12 - 1.0
levered_irr_is_proxy = False
else:
# Вырожденный equity-поток (нет смены знака). Fallback на roi-style proxy:
# прибыль на equity после процентов / equity / горизонт, клампинг ±1 — точно
# как unlevered делает irr=roi/_PROJECT_YEARS.
levered_roi = net_profit_after_financing / equity_invested
levered_irr = max(-1.0, min(1.0, levered_roi / _PROJECT_YEARS))
levered_irr_is_proxy = True
model = FinancialModel(
# legacy summary
@ -688,10 +748,13 @@ def compute_financial(
# financing overlay (PR-5)
financing_enabled=True,
annual_rate_used=_CREDIT_RATE_ANNUAL,
peak_debt_rub=financing["peak_debt_rub"],
total_interest_rub=financing["total_interest_rub"],
peak_debt_rub=financing.peak_debt_rub,
total_interest_rub=financing.total_interest_rub,
net_profit_after_financing_rub=round(net_profit_after_financing, 2),
financing_is_simplified=True,
# levered (equity) IRR — доходность на собственные средства при LTC
levered_irr=round(levered_irr, 4),
levered_irr_is_proxy=levered_irr_is_proxy,
# price calibration (PR-2)
price_per_sqm_used=round(sale_price, 2),
price_is_calibrated=price_is_calibrated,
@ -700,7 +763,7 @@ def compute_financial(
logger.info(
"financial: revenue=%.0f cost=%.0f net=%.0f roi=%.3f npv=%.0f "
"irr=%.3f%s pbp=%s price=%.0f/m2 source=%s "
"peak_debt=%.0f total_interest=%.0f net_after_fin=%.0f",
"peak_debt=%.0f total_interest=%.0f net_after_fin=%.0f levered_irr=%.3f%s",
model.revenue_rub,
model.cost_rub,
model.net_profit_rub,
@ -714,6 +777,8 @@ def compute_financial(
model.peak_debt_rub,
model.total_interest_rub,
model.net_profit_after_financing_rub,
model.levered_irr,
" (proxy)" if model.levered_irr_is_proxy else "",
)
return model

View file

@ -175,6 +175,9 @@ def test_concepts_response_matches_contract_keys() -> None:
"total_interest_rub",
"net_profit_after_financing_rub",
"financing_is_simplified",
# levered (equity) IRR
"levered_irr",
"levered_irr_is_proxy",
# price calibration (PR-2)
"price_per_sqm_used",
"price_is_calibrated",

View file

@ -356,17 +356,17 @@ def test_financing_draws_debt_on_cash_gap() -> None:
# Ранний отток / поздняя выручка → нужна кредитная линия → peak_debt>0, %>0.
cf = [-1000.0, -1000.0, 0.0, 1500.0, 1500.0]
res = _compute_financing(cf, 0.15)
assert res["peak_debt_rub"] > 0.0
assert res["total_interest_rub"] > 0.0
assert res.peak_debt_rub > 0.0
assert res.total_interest_rub > 0.0
def test_financing_zero_when_no_cash_gap() -> None:
# Поток без минуса → финансирование не требуется → debt=0, interest=0.
cf = [100.0, 200.0, 300.0]
res = _compute_financing(cf, 0.15)
assert res["peak_debt_rub"] == 0.0
assert res["total_interest_rub"] == 0.0
assert res["ending_debt_rub"] == 0.0
assert res.peak_debt_rub == 0.0
assert res.total_interest_rub == 0.0
assert res.ending_debt_rub == 0.0
def test_financing_interest_capitalises_compound() -> None:
@ -377,9 +377,9 @@ def test_financing_interest_capitalises_compound() -> None:
monthly_rate = (1.0 + 0.15) ** (1.0 / 12.0) - 1.0
# Мес 0: % с нуля = 0, выборка 1000. Мес 1: % = 1000·r. Мес 2: % = (1000+1000·r)·r.
expected = 1000.0 * monthly_rate + (1000.0 + 1000.0 * monthly_rate) * monthly_rate
assert abs(res["total_interest_rub"] - round(expected, 2)) < 0.5
assert abs(res.total_interest_rub - round(expected, 2)) < 0.5
# Compound строго больше простого 2× месячного % от исходного тела долга.
assert res["total_interest_rub"] > 2.0 * 1000.0 * monthly_rate
assert res.total_interest_rub > 2.0 * 1000.0 * monthly_rate
def test_financing_interest_monotonic_in_rate() -> None:
@ -387,8 +387,8 @@ def test_financing_interest_monotonic_in_rate() -> None:
cf = [-1000.0, -1000.0, 0.0, 1500.0, 1500.0]
low = _compute_financing(cf, 0.10)
high = _compute_financing(cf, 0.20)
assert high["total_interest_rub"] > low["total_interest_rub"]
assert high["peak_debt_rub"] >= low["peak_debt_rub"]
assert high.total_interest_rub > low.total_interest_rub
assert high.peak_debt_rub >= low.peak_debt_rub
def test_net_profit_after_financing_equals_net_minus_interest() -> None:
@ -439,11 +439,11 @@ def test_financing_ltc_reduces_debt_vs_full_coverage() -> None:
cf = [-1000.0, -1000.0, -500.0, 0.0, 1500.0, 1500.0]
full = _compute_financing(cf, 0.15, ltc_ratio=1.0)
ltc70 = _compute_financing(cf, 0.15, ltc_ratio=0.70)
assert ltc70["peak_debt_rub"] < full["peak_debt_rub"]
assert ltc70["total_interest_rub"] < full["total_interest_rub"]
assert ltc70.peak_debt_rub < full.peak_debt_rub
assert ltc70.total_interest_rub < full.total_interest_rub
# LTC 0.70 → debt = 70% от full для этого fixture (все оттоки ДО притоков,
# поэтому peak достигается до первого погашения и масштабируется линейно).
assert abs(ltc70["peak_debt_rub"] / full["peak_debt_rub"] - 0.70) < 0.01
assert abs(ltc70.peak_debt_rub / full.peak_debt_rub - 0.70) < 0.01
def test_compute_financial_uses_ltc_ratio() -> None:
@ -463,6 +463,97 @@ def test_compute_financial_uses_ltc_ratio() -> None:
)
# ── LEVERED (EQUITY) IRR ─────────────────────────────────────────────────────────
def test_equity_cashflow_shape_invested_matches_outflow_share() -> None:
# equity_invested = Σ |отрицательных equity_cf| ≈ (1ltc) × Σ оттоков unlevered.
cf = [-1000.0, -1000.0, -500.0, 0.0, 1500.0, 1500.0]
ltc = 0.70
res = _compute_financing(cf, 0.15, ltc_ratio=ltc)
equity_cf = res.equity_cashflow
assert len(equity_cf) == len(cf)
equity_invested = sum(-c for c in equity_cf if c < 0.0)
total_outflow = sum(-c for c in cf if c < 0.0)
# Equity-доля оттоков = (1ltc) от каждого разрыва → Σ масштабируется линейно.
assert equity_invested == pytest.approx((1.0 - ltc) * total_outflow, rel=1e-9)
def test_equity_cashflow_sum_equals_net_after_financing() -> None:
# Σ equity_cashflow == net_profit_after_financing: equity получает всю прибыль за
# вычетом процентов (банк — только тело долга + проценты). Sanity на equity-поток.
t = _teap(residential=10_000.0, gfa=11_000.0, parking=50)
model = financial.compute_financial(
teap=t, housing_class="comfort", land_cost_rub=200_000_000.0, development_type="mid_rise"
)
cf = _rebuild_cashflow(model, construction_months=36, residential_area_sqm=10_000.0)
res = _compute_financing(cf, financial._CREDIT_RATE_ANNUAL, financial._LOAN_TO_COST_RATIO)
assert abs(sum(res.equity_cashflow) - model.net_profit_after_financing_rub) < 1.0
def test_levered_irr_exceeds_unlevered_on_profitable_project() -> None:
# Положительный леверидж: доходность проекта > стоимости долга 15% → equity-возврат
# выше проектного. levered_irr > irr на профитном leveraged-проекте.
t = _teap(residential=11_000.0, gfa=12_000.0, parking=80)
model = financial.compute_financial(
teap=t, housing_class="business", land_cost_rub=100_000_000.0, development_type="spot"
)
assert model.net_profit_rub > 0
assert model.irr_is_proxy is False
assert model.levered_irr_is_proxy is False
# Положительный леверидж → equity-IRR строго выше unlevered project-IRR.
assert model.levered_irr > model.irr
def test_levered_irr_proxy_on_degenerate_equity_flow() -> None:
# Вырожденный equity-поток (нет смены знака) → IRR не брекетится → proxy fallback.
# Equity-поток без смены знака: только оттоки (нет притока сверх обслуживания долга).
cf = [-1000.0, -1000.0, -1000.0] # всегда отток → equity_cf тоже всегда отток
equity_cf = _compute_financing(cf, 0.15, ltc_ratio=0.70).equity_cashflow
assert _irr_monthly(equity_cf) is None # вырожден → бисекция не брекетит
def test_levered_irr_zero_proxy_when_ltc_full() -> None:
# LTC=1.0 → банк покрывает весь разрыв → equity не под риском (equity_invested=0)
# → levered_irr=0.0 / proxy (не выдумываем доходность). Проверяем через прямой поток.
cf = [-1000.0, -1000.0, 0.0, 1500.0, 1500.0]
res = _compute_financing(cf, 0.15, ltc_ratio=1.0)
equity_invested = sum(-c for c in res.equity_cashflow if c < 0.0)
assert equity_invested == pytest.approx(0.0, abs=1e-9)
def test_compute_financial_levered_irr_fields_present() -> None:
# Интеграция: профитный участок → levered_irr посчитан, поля присутствуют/типизированы.
# business/spot — проектный IRR ВЫШЕ стоимости долга 15% → положительный леверидж
# (у marginal-проекта с проектным IRR < 15% леверидж отрицателен — это корректно,
# см. test_levered_irr_below_unlevered_on_marginal_project ниже).
t = _teap(residential=11_000.0, gfa=12_000.0, parking=80)
model = financial.compute_financial(
teap=t, housing_class="business", land_cost_rub=100_000_000.0, development_type="spot"
)
assert isinstance(model.levered_irr, float)
assert isinstance(model.levered_irr_is_proxy, bool)
# Профитный leveraged-проект (LTC=0.70, проектный IRR > 15%): levered IRR настоящий,
# выше unlevered (положительный финансовый рычаг).
assert model.levered_irr_is_proxy is False
assert model.levered_irr > model.irr
def test_levered_irr_below_unlevered_on_marginal_project() -> None:
# ОТРИЦАТЕЛЬНЫЙ леверидж — sanity на корректность знака: marginal comfort/mid_rise
# проект, где проектный IRR (~4.6%) НИЖЕ стоимости долга 15% → долг разрушает
# стоимость equity → levered_irr < irr. Модель НЕ выдумывает положительный рычаг,
# когда проект не отбивает стоимость кредита.
t = _teap(residential=10_000.0, gfa=11_000.0, parking=50)
model = financial.compute_financial(
teap=t, housing_class="comfort", land_cost_rub=200_000_000.0, development_type="mid_rise"
)
assert model.net_profit_rub > 0 # статически проект прибыльный
assert model.irr < model.annual_rate_used # но проектный IRR ниже стоимости долга
# Отрицательный рычаг: долг дороже доходности проекта → equity-возврат хуже проектного.
assert model.levered_irr < model.irr
# ── RANK 1: окно продаж по рыночной абсорбции района ────────────────────────────

View file

@ -68,6 +68,16 @@ function formatPct(fraction: number): string {
return `${(fraction * 100).toFixed(1)}%`;
}
/**
* Знаковый процент для метрик, которые честно могут быть отрицательными
* (IRR на собственные средства при негативном леверидже). Отрицательное число
* печатается с Unicode-минусом «» (U+2212), не ASCII «-» (ui-microcopy).
*/
function formatSignedPct(fraction: number): string {
const body = `${(Math.abs(fraction) * 100).toFixed(1)}%`;
return fraction < 0 ? `${body}` : body;
}
/** Срок окупаемости: «28.5 мес» или «не окупается» (null). */
function formatPayback(months: number | null): string {
if (months === null) return "не окупается";
@ -313,6 +323,17 @@ function VariantPanel({ parcel, variant }: PanelProps) {
"эскроу не моделируется точно. Проценты — реальная стоимость " +
"(чистая прибыль после финансирования ниже прибыли без него)."
: ""}
{financial.financing_enabled
? " IRR на собственные средства — доходность на собственные " +
"средства инвестора при кредите LTC 70 %. Может быть выше " +
"проектной IRR при положительном леверидже или ниже неё " +
"(вплоть до отрицательной), если проект не покрывает стоимость " +
"долга." +
(financial.levered_irr_is_proxy
? " Помечена как оценочная: посчитана по вырожденному " +
"equity-потоку (proxy), а не DCF."
: "")
: ""}
</p>
</div>
</details>
@ -463,6 +484,15 @@ function FinancialCascadeTable({ financial }: { financial: FinancialModel }) {
)}
emphasis
/>
{/* IRR на собственные средства (equity) при LTC 70 %. Может быть выше
проектной IRR (положительный леверидж) или ниже / отрицательной
(проект не покрывает стоимость долга) знак показываем честно. */}
<CascadeRow
label={`IRR на собственные средства (с учётом кредита, LTC 70%)${
financial.levered_irr_is_proxy ? ", оценочный" : ""
}`}
value={formatSignedPct(financial.levered_irr)}
/>
</>
) : null}
</tbody>

View file

@ -140,6 +140,9 @@ function financialFixture(): ParcelFinancialEstimate {
total_interest_rub: 320_000_000,
net_profit_after_financing_rub: 780_000_000,
financing_is_simplified: true,
// Levered (equity) IRR — доходность на собственные средства при LTC 70 %.
levered_irr: 0.284,
levered_irr_is_proxy: false,
};
}
@ -228,6 +231,42 @@ describe("ptica-adapt finance (#1881 PR-4)", () => {
expect(keys).not.toContain("Пиковый долг");
expect(keys).not.toContain("Проценты по кредиту");
expect(keys).not.toContain("Чистая прибыль после финанс.");
// Levered (equity) IRR is a financing-only row → absent when no financing.
expect(keys).not.toContain("IRR на собственные средства");
});
// Levered (equity) IRR — доходность на собственные средства при LTC 70 %.
it("adaptFinanceDrawer: financing_enabled + DCF levered_irr → real row with LTC caption", () => {
const d = adaptFinanceDrawer(financialFixture()); // levered_irr_is_proxy: false
const row = d.rows.find((r) => r.k === "IRR на собственные средства");
expect(row?.field.isReal).toBe(true);
// 0.284 → «28,4 %» (ru запятая-разделитель).
expect(row?.field.value).toBe("28,4 %");
expect(row?.field.caption).toContain("LTC 70");
expect(row?.field.caption).toContain("проектной IRR");
});
it("adaptFinanceDrawer: levered_irr_is_proxy → row not real + «оценка (proxy)» caption", () => {
const d = adaptFinanceDrawer({
...financialFixture(),
levered_irr_is_proxy: true,
});
const row = d.rows.find((r) => r.k === "IRR на собственные средства");
// A proxy levered IRR must never read as a live DCF measurement.
expect(row?.field.isReal).toBe(false);
expect(row?.field.caption).toContain("proxy");
});
it("adaptFinanceDrawer: negative levered_irr → Unicode minus, never dressed positive", () => {
// Negative leverage (project return < cost of debt) is correct finance — the
// value is shown honestly with «−», not hidden or flipped to positive.
const d = adaptFinanceDrawer({
...financialFixture(),
levered_irr: -0.116,
});
const row = d.rows.find((r) => r.k === "IRR на собственные средства");
expect(row?.field.value).toBe("11,6 %");
expect(row?.field.value.startsWith("")).toBe(true);
});
it("adaptFinanceDrawer: sales_duration_months shown with schedule source", () => {

View file

@ -117,6 +117,18 @@ function formatPctFromFraction(fraction: number, digits = 1): string {
});
}
/**
* Знаковый процент из доли для метрик, которые честно могут быть
* отрицательными (levered/equity IRR при негативном леверидже). Отрицательное
* число печатается с Unicode-минусом «» (U+2212), не ASCII «-», как требует
* ui-microcopy: «11,6 %». Форматируем модуль и сами ставим «», чтобы знак не
* зависел от ICU-сборки рантайма (Node даёт ASCII-hyphen).
*/
function formatSignedPctFromFraction(fraction: number, digits = 1): string {
const body = formatPctFromFraction(Math.abs(fraction), digits);
return fraction < 0 ? `${body}` : body;
}
// #1881 PR-4: backend отдаёт inferred-токены латиницей (housing_class /
// development_type) — локализуем для финмодель-drawer, чтобы финдиректор не
// видел "high_rise" в русском UI. Неизвестный токен → как есть (fallback).
@ -1799,6 +1811,31 @@ export function adaptFinanceDrawer(
`${formatBln(financial.net_profit_after_financing_rub)} млрд ₽`,
),
},
{
// IRR на собственные средства (equity) при кредитном плече LTC 70 %.
// Может быть ВЫШЕ проектной IRR (положительный леверидж) ИЛИ НИЖЕ /
// отрицательной (проект не покрывает стоимость долга) — это корректная
// финансовая картина, не прячем знак. isReal только когда финмодель
// посчитала equity-IRR по DCF; proxy/выкл. финансирование → не real.
k: "IRR на собственные средства",
field: financial.levered_irr_is_proxy
? {
value: `${formatSignedPctFromFraction(financial.levered_irr)} %`,
isReal: false,
caption:
"доходность на собственные средства при кредите LTC 70 % · " +
"оценка (proxy), не DCF · может быть ниже проектной IRR, если " +
"проект не покрывает стоимость долга",
}
: {
value: `${formatSignedPctFromFraction(financial.levered_irr)} %`,
isReal: true,
caption:
"доходность на собственные средства при кредите LTC 70 % · " +
"может быть ниже проектной IRR, если проект не покрывает " +
"стоимость долга",
},
},
]
: [];

View file

@ -3529,6 +3529,10 @@ export interface components {
* ``irr_is_proxy=True``. VAT is a documented simplification (see docstring), not
* exact НК РФ mechanics.
*
* ``levered_irr`` IRR на собственные средства (equity cashflow при LTC): тот же
* DCF-IRR + roi-proxy fallback, но по equity-потоку графика долга. При положительном
* леверидже выше unlevered ``irr``; вырожденные случаи 0.0 + ``levered_irr_is_proxy``.
*
* The legacy summary fields (``revenue_rub`` / ``cost_rub`` /
* ``gross_margin_rub`` / ``irr``) are kept for backward-compat; the new fields
* expose the full cascade, the net P&L and the DCF metrics.
@ -3626,6 +3630,16 @@ export interface components {
* @default true
*/
financing_is_simplified: boolean;
/**
* Levered Irr
* @default 0
*/
levered_irr: number;
/**
* Levered Irr Is Proxy
* @default true
*/
levered_irr_is_proxy: boolean;
/** Price Per Sqm Used */
price_per_sqm_used: number;
/**

View file

@ -107,6 +107,14 @@ export interface FinancialModel {
* ставке-нормативу, проценты капитализируются, эскроу не моделируется точно.
*/
financing_is_simplified: boolean;
/**
* IRR на собственные средства (equity) при кредитном плече LTC 70 %. При
* положительном леверидже > проектной (unlevered) `irr`; при отрицательном
* ниже неё или отрицательная (проект не покрывает стоимость долга).
*/
levered_irr: number;
/** true → levered_irr — оценка (roi-proxy / вырожденный equity-поток), не DCF. */
levered_irr_is_proxy: boolean;
}
export type PriceSource =

View file

@ -170,6 +170,8 @@
"total_interest_rub": 224442467.76,
"net_profit_after_financing_rub": 7180188.49,
"financing_is_simplified": true,
"levered_irr": 0.1182,
"levered_irr_is_proxy": false,
"price_per_sqm_used": 165000.0,
"price_is_calibrated": true,
"price_source": "objective_district_median",

View file

@ -542,6 +542,11 @@ export interface ParcelFinancialEstimate {
total_interest_rub: number;
net_profit_after_financing_rub: number;
financing_is_simplified: boolean;
// Levered (equity) IRR — доходность на собственные средства (equity cashflow при
// LTC). При положительном леверидже > unlevered irr. levered_irr_is_proxy=true →
// roi-proxy (вырожденный equity-поток / equity не под риском при LTC>=1), не DCF.
levered_irr: number;
levered_irr_is_proxy: boolean;
}
export type PoiCategory =