feat(financial): LTC 70%/equity 30% норматив МКД РФ в модели финансирования #1895
4 changed files with 58 additions and 18 deletions
|
|
@ -102,17 +102,18 @@ Unlevered (project) DCF — NPV / IRR / PBP — остаётся HEADLINE и Н
|
|||
< net_profit`` для любого проекта с кассовым разрывом.
|
||||
* **Ставка ``annual_rate_used`` — норматив** (``_CREDIT_RATE_ANNUAL``), отдельный
|
||||
от ставки дисконтирования (см. константы), а не выведенная из проекта величина.
|
||||
* **100% покрытие разрыва** — кредитная линия покрывает весь накопительный
|
||||
кассовый разрыв; equity / LTC / лимиты банка НЕ моделируются (нет доли
|
||||
собственных средств).
|
||||
* **LTC-покрытие разрыва** — кредитная линия покрывает ``_LOAN_TO_COST_RATIO``
|
||||
(70 %) каждого ежемесячного оттока; оставшиеся 30 % (собственные средства)
|
||||
co-fund отток пропорционально и процентами не облагаются. Лимиты банка НЕ
|
||||
моделируются точно.
|
||||
* **Проценты капитализируются (compound)** — начисляются на остаток на начало
|
||||
месяца ДО движений и добавляются в тело долга.
|
||||
* **Эскроу-специфику не моделируем точно** — раскрытие средств с эскроу-счетов
|
||||
по этапам стройки, лимиты раскрытия, ставка по бридж/целевой части — всё это
|
||||
упрощено до единой капитализируемой ставки на кассовый разрыв.
|
||||
* **Levered IRR НЕ считается** в этом PR (хрупко на хардкод-100%-долге — отдельный
|
||||
PR). MVP раскрывает только агрегаты: ``peak_debt_rub`` / ``total_interest_rub``
|
||||
/ ``net_profit_after_financing_rub``; debt_schedule в схему НЕ выносится.
|
||||
* **Levered IRR НЕ считается** (отдельный PR). MVP раскрывает только агрегаты:
|
||||
``peak_debt_rub`` / ``total_interest_rub`` / ``net_profit_after_financing_rub``;
|
||||
debt_schedule в схему НЕ выносится.
|
||||
* **Unlevered NPV / IRR / PBP НЕ затронуты** overlay'ем — это всё та же
|
||||
интринсивная доходность. График наследует неточность ``schedule_is_default``:
|
||||
финансирование считается по тому же дефолт-графику фаз, поэтому
|
||||
|
|
@ -223,6 +224,10 @@ _DISCOUNT_RATE_ANNUAL: float = 0.15
|
|||
# ставка = стоимость долга (проценты). Совпадение значения (0.15) — калибровка,
|
||||
# НЕ тождество; не сливать в одну константу.
|
||||
_CREDIT_RATE_ANNUAL: float = 0.15
|
||||
# Доля банковского кредита от каждого ежемесячного оттока (LTC, Loan-to-Cost).
|
||||
# Собственные средства = (1 − LTC) co-fund отток пропорционально, без начисления %.
|
||||
# Норматив РФ МКД при проектном финансировании: 65–75 %. Используем 70 % (медиана).
|
||||
_LOAN_TO_COST_RATIO: float = 0.70
|
||||
# Длительность ПИР (проектирование/экспертиза), мес.
|
||||
_PIR_DURATION_MONTHS: int = 6
|
||||
# Длительность СМР по типу застройки, мес (точечная быстрее высотной).
|
||||
|
|
@ -329,14 +334,19 @@ def _payback_months(cf: list[float]) -> float | None:
|
|||
return None
|
||||
|
||||
|
||||
def _compute_financing(operating_cf: list[float], annual_rate: float) -> dict[str, float]:
|
||||
def _compute_financing(
|
||||
operating_cf: list[float],
|
||||
annual_rate: float,
|
||||
ltc_ratio: float = 1.0,
|
||||
) -> dict[str, float]:
|
||||
"""OVERLAY финансирования поверх unlevered cashflow (PR-5). Чистая функция.
|
||||
|
||||
``operating_cf`` — готовый unlevered cashflow (READ-ONLY, НЕ мутируется).
|
||||
Кредитная линия покрывает накопительный кассовый разрыв на 100%; проценты
|
||||
КАПИТАЛИЗИРУЮТСЯ в долг (compound) — упрощение под эскроу-логику РФ. Порядок
|
||||
внутри месяца: процент НАЧИСЛЯЕМ на остаток на начало месяца ДО draw/repay,
|
||||
затем выборка (cf<0) либо погашение притоком (cf>0, не уходя ниже 0).
|
||||
Кредитная линия покрывает ``ltc_ratio`` долю кассового разрыва; оставшийся
|
||||
(1 − ltc_ratio) — equity-взнос (без процентов). Проценты КАПИТАЛИЗИРУЮТСЯ
|
||||
в долг (compound) — упрощение под эскроу-логику РФ. Порядок внутри месяца:
|
||||
процент НАЧИСЛЯЕМ на остаток ДО draw/repay, затем выборка (cf<0) либо
|
||||
погашение притоком (cf>0, не уходя ниже 0).
|
||||
|
||||
Если поток не уходит в минус → долг не выбирается → debt=0, interest=0
|
||||
(корректно: финансирование не требуется).
|
||||
|
|
@ -354,7 +364,7 @@ def _compute_financing(operating_cf: list[float], annual_rate: float) -> dict[st
|
|||
debt += interest
|
||||
total_interest += interest
|
||||
if cf < 0:
|
||||
debt += -cf # выборка на покрытие оттока
|
||||
debt += -cf * ltc_ratio # банк покрывает ltc_ratio от оттока; equity-часть без %
|
||||
else:
|
||||
debt -= min(cf, debt) # гасим долг притоком (не уходим в минус)
|
||||
peak_debt = max(peak_debt, debt)
|
||||
|
|
@ -624,7 +634,7 @@ def compute_financial(
|
|||
# Тот же массив cashflow, что построен выше (НЕ пересобираем — защита инварианта:
|
||||
# overlay читает unlevered поток и НЕ мутирует его). Проценты — реальная стоимость
|
||||
# долга, режут прибыль: net_after < net. Levered IRR НЕ считаем (отдельный PR).
|
||||
financing = _compute_financing(cashflow, _CREDIT_RATE_ANNUAL)
|
||||
financing = _compute_financing(cashflow, _CREDIT_RATE_ANNUAL, _LOAN_TO_COST_RATIO)
|
||||
net_profit_after_financing = net_profit - financing["total_interest_rub"]
|
||||
|
||||
model = FinancialModel(
|
||||
|
|
|
|||
|
|
@ -378,6 +378,35 @@ def test_financing_enabled_on_realistic_parcel() -> None:
|
|||
assert model.peak_debt_rub > 0.0
|
||||
|
||||
|
||||
def test_financing_ltc_reduces_debt_vs_full_coverage() -> None:
|
||||
# LTC 0.70 → пиковый долг и проценты < чем при 100% покрытии (ltc=1.0).
|
||||
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"]
|
||||
# LTC 0.70 → debt = 70% от full для этого fixture (все оттоки ДО притоков,
|
||||
# поэтому peak достигается до первого погашения и масштабируется линейно).
|
||||
assert abs(ltc70["peak_debt_rub"] / full["peak_debt_rub"] - 0.70) < 0.01
|
||||
|
||||
|
||||
def test_compute_financial_uses_ltc_ratio() -> None:
|
||||
# compute_financial передаёт _LOAN_TO_COST_RATIO в _compute_financing:
|
||||
# total_interest строго < чем при 100%-долге на том же cashflow.
|
||||
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"
|
||||
)
|
||||
# Нет прямого доступа к «100%-версии», но LTC < 1.0 → peak_debt < revenue
|
||||
# (у реалистичного МКД-проекта затраты ~ 70-80% выручки, значит
|
||||
# LTC*затраты < затраты — пиковый долг не равен всем затратам).
|
||||
assert 0.0 < model.peak_debt_rub < model.cost_rub
|
||||
assert model.total_interest_rub > 0.0
|
||||
assert model.net_profit_after_financing_rub == pytest.approx(
|
||||
model.net_profit_rub - model.total_interest_rub, abs=0.01
|
||||
)
|
||||
|
||||
|
||||
# ── RANK 1: окно продаж по рыночной абсорбции района ────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -212,9 +212,10 @@ describe("ptica-adapt finance (#1881 PR-4)", () => {
|
|||
expect(interest?.field.caption).toContain("годовых");
|
||||
});
|
||||
|
||||
it("adaptFinanceDrawer: financing_is_simplified → caveat appended to summary", () => {
|
||||
it("adaptFinanceDrawer: financing_is_simplified → кредитная/собственные caveat appended to summary", () => {
|
||||
const d = adaptFinanceDrawer(financialFixture());
|
||||
expect(d.summary).toContain("упрощен");
|
||||
expect(d.summary).toContain("кредитная линия");
|
||||
expect(d.summary).toContain("собственные средства");
|
||||
expect(d.summary).toContain("эскроу");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1788,9 +1788,9 @@ export function adaptFinanceDrawer(
|
|||
|
||||
const summary =
|
||||
financial.financing_enabled && financial.financing_is_simplified
|
||||
? `${financial.caveat} Финансирование упрощено: весь кассовый разрыв ` +
|
||||
"покрыт кредитом по ставке-нормативу, проценты капитализируются, эскроу " +
|
||||
"не моделируется точно."
|
||||
? `${financial.caveat} Финансирование: 70 % — банк (кредитная линия, норматив МКД РФ), ` +
|
||||
"30 % — собственные средства инвестора (без процентов). Проценты капитализируются, " +
|
||||
"эскроу не моделируется точно."
|
||||
: financial.caveat;
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue