Заменяет generative stubs детерминированным end-to-end пайплайном: - 1a geometry: WGS84-parse, метрическая AEQD-репроекция, setback-буфер, placement grid - 1b placement: greedy section-fill + STRtree коллизии (full-gap), 3 стратегии, coverage cap - 1c teap/financial: площади/квартиры/FAR/паркинг; выручка/затраты/маржа + IRR-proxy - exporters: ezdxf DXF (геометрия) + WeasyPrint ТЭП/фин PDF (lazy import) generate() заменил generate_stub в route (422 на degenerate). mypy-strict + ruff clean, 31 generative-тест + full suite 3078 passed. ConceptVariant заполняется реальными числами. Closes #54 Closes #55 Closes #56
97 lines
3.8 KiB
Python
97 lines
3.8 KiB
Python
"""Generative Design — Stage 1c: simplified financial model.
|
||
|
||
From the Stage 1c ``TEAP`` block we derive the ``FinancialModel`` contract:
|
||
|
||
* ``revenue_rub`` = residential_area_sqm * sale price per sqm (by housing class).
|
||
* ``cost_rub`` = total_floor_area_sqm * construction cost per sqm + land cost.
|
||
* ``gross_margin_rub`` = revenue - cost.
|
||
* ``irr`` = simplified proxy (margin-on-cost / project years), NO time
|
||
discounting — this is a static stand-in until the Phase 1 cashflow model lands.
|
||
|
||
Prices/costs are coarse RU-market proxies for an MVP (см. константы ниже); they are
|
||
deliberately conservative round numbers, not a calibrated pricing engine. The IRR
|
||
field is a *proxy*: a real internal rate of return needs a dated cashflow series,
|
||
which is out of MVP scope — we return an annualised margin ratio so the field is
|
||
populated with a plausible, monotonic number rather than zero.
|
||
|
||
Детерминированно, без LLM / внешних API / БД.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Literal
|
||
|
||
from app.schemas.concept import TEAP, FinancialModel
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
HousingClass = Literal["econom", "comfort", "business"]
|
||
|
||
# ── Цена продажи жилья, руб/кв.м (proxy рынка ЕКБ/региона, упрощённо) ──────────
|
||
_SALE_PRICE_PER_SQM: dict[HousingClass, float] = {
|
||
"econom": 110_000.0,
|
||
"comfort": 145_000.0,
|
||
"business": 210_000.0,
|
||
}
|
||
|
||
# ── Себестоимость СМР, руб/кв.м общей площади (выше класс -> дороже отделка/инж) ─
|
||
_CONSTRUCTION_COST_PER_SQM: dict[HousingClass, float] = {
|
||
"econom": 72_000.0,
|
||
"comfort": 88_000.0,
|
||
"business": 120_000.0,
|
||
}
|
||
|
||
# Условный горизонт проекта (лет) для аннуализации margin-on-cost в IRR-proxy.
|
||
_PROJECT_YEARS: float = 3.0
|
||
|
||
|
||
def compute_financial(
|
||
*,
|
||
teap: TEAP,
|
||
housing_class: HousingClass,
|
||
land_cost_rub: float | None,
|
||
) -> FinancialModel:
|
||
"""Свести ТЭП + класс + стоимость земли в :class:`FinancialModel`.
|
||
|
||
Args:
|
||
teap: Stage 1c ТЭП (берём residential_area_sqm и total_floor_area_sqm).
|
||
housing_class: задаёт цену продажи и себестоимость СМР.
|
||
land_cost_rub: стоимость участка (опционально); None -> 0 в затратах.
|
||
"""
|
||
sale_price = _SALE_PRICE_PER_SQM[housing_class]
|
||
construction_cost = _CONSTRUCTION_COST_PER_SQM[housing_class]
|
||
|
||
revenue = teap.residential_area_sqm * sale_price
|
||
construction = teap.total_floor_area_sqm * construction_cost
|
||
land = land_cost_rub if land_cost_rub is not None else 0.0
|
||
cost = construction + land
|
||
gross_margin = revenue - cost
|
||
|
||
# IRR-proxy: аннуализированная маржа-на-затраты. НЕ настоящий IRR (нет дисконта/
|
||
# дат денежных потоков — отложено в Phase 1). Защита от деления на ноль и
|
||
# клампинг в разумный диапазон, чтобы поле было монотонным и читаемым.
|
||
if cost > 0:
|
||
margin_on_cost = gross_margin / cost
|
||
irr = margin_on_cost / _PROJECT_YEARS
|
||
else:
|
||
irr = 0.0
|
||
irr = max(-1.0, min(1.0, irr))
|
||
|
||
model = FinancialModel(
|
||
revenue_rub=round(revenue, 2),
|
||
cost_rub=round(cost, 2),
|
||
gross_margin_rub=round(gross_margin, 2),
|
||
irr=round(irr, 4),
|
||
)
|
||
logger.info(
|
||
"financial: revenue=%.0f cost=%.0f margin=%.0f irr_proxy=%.3f",
|
||
model.revenue_rub,
|
||
model.cost_rub,
|
||
model.gross_margin_rub,
|
||
model.irr,
|
||
)
|
||
return model
|
||
|
||
|
||
__all__ = ["HousingClass", "compute_financial"]
|