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