gendesign/backend/app/services/generative/financial.py
Light1YT 3b79533f9f
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / backend-tests (push) Has been cancelled
feat(generative): движок концепций Stage 1a/1b/1c (#54 #55 #56)
Заменяет 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
2026-06-13 21:32:32 +05:00

97 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"]