feat(site-finder): geo-radius market-price calibration in /analyze (#1881 follow-up) #1899
6 changed files with 290 additions and 7 deletions
|
|
@ -134,6 +134,17 @@ def _wind_label(deg: float) -> str:
|
|||
EKB_CENTER_LAT: float = 56.838011
|
||||
EKB_CENTER_LON: float = 60.597474
|
||||
|
||||
# ── Гео-радиусная калибровка цены продажи (geo_radius_price) ────────────────────
|
||||
# Покрывает пробел name-match district_reference: только 4 из 9 админ-районов ЕКБ
|
||||
# матчатся с Objective-данными по имени, остальные 5 проваливались в district_reference/
|
||||
# class_norm без реальной гео-цены. Берём медиану price_per_m2_rub новостроек Objective
|
||||
# (objective_lots) в радиусе вокруг центроида участка (коорды — через complexes.lat/lon).
|
||||
_GEO_PRICE_RADIUS_M: float = 3000.0 # 3 км — городской радиус сопоставимых ЖК (ЕКБ)
|
||||
# Минимум лотов для устойчивой медианы — зеркалит порог n≥10 квартальной MV
|
||||
# (mv_quarter_price_per_m2). Дополнительно требуем ≥2 ЖК, чтобы один ЖК не доминировал.
|
||||
_GEO_PRICE_MIN_LOTS: int = 10
|
||||
_GEO_PRICE_MIN_COMPLEXES: int = 2
|
||||
|
||||
# Эмпирические пороги score для ЕКБ: средний диапазон 15-30, max редко >40.
|
||||
SCORE_THRESHOLDS: dict[str, float] = {"плохо": 5.0, "средне": 15.0, "хорошо": 25.0, "отлично": 40.0}
|
||||
SCORE_MAX_REFERENCE: float = 40.0
|
||||
|
|
@ -1820,6 +1831,9 @@ def analyze_parcel(
|
|||
# Defensive: centroid_row может существовать, но с NULL lat/lon (вырожденная/
|
||||
# невалидная геометрия → ST_Centroid NULL). Проверяем именно значения, а не только
|
||||
# наличие строки — иначе float(None) → 500. Fallback = центр ЕКБ.
|
||||
centroid_is_fallback: bool = (
|
||||
centroid_row is None or centroid_row["lat"] is None or centroid_row["lon"] is None
|
||||
)
|
||||
centroid_lat: float = (
|
||||
float(centroid_row["lat"]) if centroid_row and centroid_row["lat"] is not None else 56.838
|
||||
)
|
||||
|
|
@ -2691,6 +2705,100 @@ def analyze_parcel(
|
|||
logger.warning("market_price query failed for %s: %s", cad_num, e)
|
||||
market_price = {"deals_count": 0, "source": "no_data"}
|
||||
|
||||
# 10d-pre1b) Гео-радиусная цена — медиана price_per_m2_rub новостроек Objective в
|
||||
# радиусе вокруг центроида участка (ST_DWithin). Закрывает пробел district_reference:
|
||||
# только 4 из 9 админ-районов ЕКБ матчатся с Objective по имени, остальные 5 без неё
|
||||
# проваливались в class_norm. У objective_lots нет geom — коорды берём через
|
||||
# complexes (latitude/longitude), join по ol.complex_id = c.id (паттерн competitors.py).
|
||||
geo_radius_price: dict[str, Any]
|
||||
# Вырожденная геометрия → центроид свалился на хардкод-центр ЕКБ. Гео-радиусная
|
||||
# медиана тогда = «3км вокруг центра города», но выдаётся за калиброванную рыночную
|
||||
# цену КОНКРЕТНОГО участка (price_is_calibrated=true). Это вводит в заблуждение —
|
||||
# пропускаем запрос, цена честно падает в district/class_norm.
|
||||
if not centroid_is_fallback:
|
||||
try:
|
||||
with db.begin_nested():
|
||||
grp_row = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT
|
||||
percentile_cont(0.5) WITHIN GROUP (ORDER BY ol.price_per_m2_rub)
|
||||
AS median,
|
||||
count(*) AS n,
|
||||
count(DISTINCT c.id) AS n_complexes
|
||||
FROM complexes c
|
||||
JOIN objective_lots ol
|
||||
ON ol.complex_id = c.id
|
||||
AND ol.price_per_m2_rub IS NOT NULL
|
||||
WHERE c.latitude IS NOT NULL
|
||||
AND c.longitude IS NOT NULL
|
||||
AND ST_DWithin(
|
||||
ST_SetSRID(
|
||||
ST_MakePoint(c.longitude, c.latitude), 4326
|
||||
)::geography,
|
||||
ST_SetSRID(
|
||||
ST_MakePoint(
|
||||
CAST(:lon AS float), CAST(:lat AS float)
|
||||
), 4326
|
||||
)::geography,
|
||||
CAST(:radius_m AS float)
|
||||
)
|
||||
"""),
|
||||
{
|
||||
"lon": centroid_lon,
|
||||
"lat": centroid_lat,
|
||||
"radius_m": _GEO_PRICE_RADIUS_M,
|
||||
},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
_grp_n = int(grp_row["n"]) if grp_row and grp_row["n"] is not None else 0
|
||||
_grp_nc = (
|
||||
int(grp_row["n_complexes"])
|
||||
if grp_row and grp_row["n_complexes"] is not None
|
||||
else 0
|
||||
)
|
||||
# Порог устойчивости медианы: n≥10 лотов И ≥2 ЖК (один ЖК не должен доминировать).
|
||||
if (
|
||||
grp_row
|
||||
and grp_row["median"] is not None
|
||||
and _grp_n >= _GEO_PRICE_MIN_LOTS
|
||||
and _grp_nc >= _GEO_PRICE_MIN_COMPLEXES
|
||||
):
|
||||
geo_radius_price = {
|
||||
"median": float(grp_row["median"]),
|
||||
"lots_count": _grp_n,
|
||||
"complexes_count": _grp_nc,
|
||||
"radius_m": _GEO_PRICE_RADIUS_M,
|
||||
"source": "objective_geo_radius",
|
||||
}
|
||||
else:
|
||||
geo_radius_price = {
|
||||
"median": None,
|
||||
"lots_count": _grp_n,
|
||||
"complexes_count": _grp_nc,
|
||||
"radius_m": _GEO_PRICE_RADIUS_M,
|
||||
"source": "no_data",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("geo_radius_price query failed for %s: %s", cad_num, e)
|
||||
geo_radius_price = {
|
||||
"median": None,
|
||||
"lots_count": 0,
|
||||
"complexes_count": 0,
|
||||
"radius_m": _GEO_PRICE_RADIUS_M,
|
||||
"source": "no_data",
|
||||
}
|
||||
else:
|
||||
geo_radius_price = {
|
||||
"median": None,
|
||||
"lots_count": 0,
|
||||
"complexes_count": 0,
|
||||
"radius_m": _GEO_PRICE_RADIUS_M,
|
||||
"source": "no_data_centroid_fallback",
|
||||
}
|
||||
|
||||
# 10d-pre2) #105 Phase 5: РНС/РВЭ в квартале (cad_num quarter prefix match).
|
||||
# Spatial filter через geom недоступен до Phase 3 (geocoding → geom).
|
||||
# Используем quarter prefix (первые 3 сегмента кад. номера) как прокси.
|
||||
|
|
@ -3014,6 +3122,9 @@ def analyze_parcel(
|
|||
area_m2=_fin_area_m2,
|
||||
nspd_zoning=nspd_dump_data["nspd_zoning"],
|
||||
market_price=market_price,
|
||||
# Гео-радиусная медиана новостроек — приоритет МЕЖДУ квартальной MV и
|
||||
# district_reference: закрывает 5 не-name-match районов ЕКБ (≈line 2693).
|
||||
geo_radius_price_median=geo_radius_price.get("median"),
|
||||
district_price_median=district_price_block["district_price_per_m2_median"],
|
||||
cadastral_value_rub=_fin_cad_value,
|
||||
gate_verdict=gate_verdict,
|
||||
|
|
@ -3089,6 +3200,8 @@ def analyze_parcel(
|
|||
"market_trend": market_trend,
|
||||
# #33 D2: квартальная ценовая статистика из mv_quarter_price_per_m2
|
||||
"market_price": market_price,
|
||||
# Гео-радиусная медиана цены новостроек Objective вокруг центроида (transparency)
|
||||
"geo_radius_price": geo_radius_price,
|
||||
# #29 G2: кадастровые метаданные участка (ВРИ, категория, кад. стоимость)
|
||||
"parcel_meta": parcel_meta.model_dump() if parcel_meta is not None else None,
|
||||
# #105 Phase 5: РНС/РВЭ в квартале (quarter prefix match; после Phase 3 → spatial 500m)
|
||||
|
|
|
|||
|
|
@ -541,8 +541,10 @@ def compute_financial(
|
|||
``None`` → fallback на норматив класса ``_SALE_PRICE_PER_SQM``. Калибруем
|
||||
ТОЛЬКО цену жилья; цена паркинга и себестоимость СМР остаются нормативом.
|
||||
price_source: метка источника цены для honest-флага в UI/PDF — одно из
|
||||
"objective_district_median" / "district_reference" / "class_norm".
|
||||
Игнорируется (форсится "class_norm"), если ``market_price_per_sqm is None``.
|
||||
"objective_district_median" / "district_reference" / "class_norm" (concepts-путь
|
||||
через ``_lookup_market_price``), либо "objective_geo_radius" — этот лейбл
|
||||
приходит ТОЛЬКО из site-finder-моста (``synthesize_parcel_financial``), не из
|
||||
concepts. Игнорируется (форсится "class_norm"), если ``market_price_per_sqm is None``.
|
||||
development_type: "spot"/"mid_rise"/"high_rise" — задаёт длительность СМР в
|
||||
DCF-графике (``_CONSTRUCTION_DURATION_MONTHS``). ``None``/неизвестный →
|
||||
дефолт mid_rise (36 мес). График — типовое допущение (schedule_is_default).
|
||||
|
|
|
|||
|
|
@ -166,12 +166,24 @@ def synthesize_parcel_financial(
|
|||
district_price_median: float | None,
|
||||
cadastral_value_rub: float | None,
|
||||
gate_verdict: dict[str, Any] | None,
|
||||
geo_radius_price_median: float | None = None,
|
||||
velocity_sqm_per_month: float | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Мост buildability участка → ОРИЕНТИРОВОЧНАЯ финмодель (DCF) или ``None``.
|
||||
|
||||
Gate-guard: считаем ТОЛЬКО когда gate допускает МКД и есть числа зоны + площадь.
|
||||
|
||||
Цена продажи калибруется по приоритету (от точного рыночного к нормативу):
|
||||
|
||||
1. ``market_price.median`` — квартальная MV перепродаж (resale) →
|
||||
``price_source="objective_district_median"``.
|
||||
2. ``geo_radius_price_median`` — медиана новостроек Objective в гео-радиусе вокруг
|
||||
центроида участка → ``price_source="objective_geo_radius"``. Закрывает пробел
|
||||
district_reference (только 4 из 9 админ-районов ЕКБ матчатся по имени).
|
||||
3. ``district_price_median`` — name-match медиана админ-района →
|
||||
``price_source="district_reference"``.
|
||||
4. норматив класса → ``price_source="class_norm"``.
|
||||
|
||||
Args:
|
||||
area_m2: площадь участка, кв.м (из parcel_meta/EGRN или geometry_suitability).
|
||||
nspd_zoning: блок зоны НСПД/ПЗЗ — берём ``max_far`` / ``max_building_pct`` /
|
||||
|
|
@ -180,6 +192,9 @@ def synthesize_parcel_financial(
|
|||
district_price_median: медиана цены по району, руб/кв.м (fallback к market).
|
||||
cadastral_value_rub: кадастровая стоимость участка → land_cost (НЕ рыночная).
|
||||
gate_verdict: can-build-MKD вердикт — гейт на расчёт.
|
||||
geo_radius_price_median: медиана цены новостроек Objective (``price_per_m2_rub``)
|
||||
в гео-радиусе вокруг центроида участка, руб/кв.м. Приоритет МЕЖДУ квартальной
|
||||
MV и ``district_price_median``. ``None`` → слой пропускается (back-compat).
|
||||
velocity_sqm_per_month: RANK 1 — темп абсорбции ОДНОГО типового конкурента
|
||||
района (``velocity.project_absorption_sqm_per_month``), м²/мес. Прокидывается
|
||||
в :func:`compute_financial` как ``market_velocity_sqm_per_month`` → драйвит
|
||||
|
|
@ -210,12 +225,17 @@ def synthesize_parcel_financial(
|
|||
if area_m2 is None or area_m2 <= 0:
|
||||
return None
|
||||
|
||||
# ── Цена продажи: market median → district median (рыночный прокси) ─────────
|
||||
# ── Цена продажи: quarter-MV → гео-радиус → district median (рыночный прокси) ─
|
||||
# Приоритет от точного рынка к нормативу. Гео-радиус вставлен МЕЖДУ квартальной MV
|
||||
# и district_reference: даёт реальную цену 5 не-name-match районам ЕКБ.
|
||||
price: float | None = None
|
||||
price_source = "class_norm"
|
||||
if market_price and _as_float(market_price.get("median")) is not None:
|
||||
price = _as_float(market_price.get("median"))
|
||||
price_source = "objective_district_median"
|
||||
elif _as_float(geo_radius_price_median) is not None:
|
||||
price = _as_float(geo_radius_price_median)
|
||||
price_source = "objective_geo_radius"
|
||||
elif district_price_median is not None:
|
||||
price = float(district_price_median)
|
||||
price_source = "district_reference"
|
||||
|
|
|
|||
|
|
@ -320,6 +320,75 @@ def test_financial_price_source_class_norm_when_no_market() -> None:
|
|||
assert fin["housing_class_inferred"] == "comfort"
|
||||
|
||||
|
||||
# ── geo_radius_price: приоритет МЕЖДУ quarter-MV и district_reference ──────────
|
||||
|
||||
|
||||
def test_financial_quarter_mv_wins_over_geo_radius() -> None:
|
||||
"""Есть quarter-MV median → geo-радиус игнорируется (объективный приоритет MV)."""
|
||||
fin = synthesize_parcel_financial(
|
||||
area_m2=5000.0,
|
||||
nspd_zoning=_zoning_far(),
|
||||
market_price={"median": 150_000.0},
|
||||
geo_radius_price_median=200_000.0,
|
||||
district_price_median=140_000.0,
|
||||
cadastral_value_rub=50_000_000.0,
|
||||
gate_verdict=_gate_ok(),
|
||||
)
|
||||
assert fin is not None
|
||||
assert fin["price_source"] == "objective_district_median"
|
||||
assert fin["price_per_sqm_used"] == 150_000.0
|
||||
|
||||
|
||||
def test_financial_price_source_geo_radius_when_no_quarter_mv() -> None:
|
||||
"""Нет quarter-MV, но есть geo-радиус → source=objective_geo_radius, цена из него."""
|
||||
fin = synthesize_parcel_financial(
|
||||
area_m2=5000.0,
|
||||
nspd_zoning=_zoning_far(),
|
||||
market_price={"deals_count": 0, "source": "no_data"},
|
||||
geo_radius_price_median=185_000.0,
|
||||
district_price_median=140_000.0,
|
||||
cadastral_value_rub=50_000_000.0,
|
||||
gate_verdict=_gate_ok(),
|
||||
)
|
||||
assert fin is not None
|
||||
assert fin["price_source"] == "objective_geo_radius"
|
||||
assert fin["price_is_calibrated"] is True
|
||||
assert fin["price_per_sqm_used"] == 185_000.0
|
||||
assert fin["housing_class_inferred"] == "business" # 185k → business (>=177k)
|
||||
|
||||
|
||||
def test_financial_district_fallback_when_no_quarter_mv_no_geo_radius() -> None:
|
||||
"""Нет quarter-MV и geo-радиуса, но есть district → district_reference (без регресса)."""
|
||||
fin = synthesize_parcel_financial(
|
||||
area_m2=5000.0,
|
||||
nspd_zoning=_zoning_far(),
|
||||
market_price={"deals_count": 0, "source": "no_data"},
|
||||
geo_radius_price_median=None,
|
||||
district_price_median=130_000.0,
|
||||
cadastral_value_rub=50_000_000.0,
|
||||
gate_verdict=_gate_ok(),
|
||||
)
|
||||
assert fin is not None
|
||||
assert fin["price_source"] == "district_reference"
|
||||
assert fin["price_per_sqm_used"] == 130_000.0
|
||||
|
||||
|
||||
def test_financial_class_norm_when_all_price_sources_absent() -> None:
|
||||
"""Все источники цены отсутствуют (incl. geo-радиус) → class_norm."""
|
||||
fin = synthesize_parcel_financial(
|
||||
area_m2=5000.0,
|
||||
nspd_zoning=_zoning_far(),
|
||||
market_price=None,
|
||||
geo_radius_price_median=None,
|
||||
district_price_median=None,
|
||||
cadastral_value_rub=50_000_000.0,
|
||||
gate_verdict=_gate_ok(),
|
||||
)
|
||||
assert fin is not None
|
||||
assert fin["price_source"] == "class_norm"
|
||||
assert fin["price_is_calibrated"] is False
|
||||
|
||||
|
||||
# ── интеграция: реалистичный участок ──────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
60
data/sql/168_complexes_makepoint_geog_idx.sql
Normal file
60
data/sql/168_complexes_makepoint_geog_idx.sql
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
-- 168_complexes_makepoint_geog_idx.sql
|
||||
-- Функциональный geography-GIST на complexes для гео-радиусной калибровки цены в /analyze.
|
||||
-- Ветка: feat/geo-radius-price-calibration.
|
||||
--
|
||||
-- КОРНЕВАЯ ПРИЧИНА: новый блок «гео-радиусная цена» (parcels.py:2705, секция 10d-pre1b)
|
||||
-- считает медиану price_per_m2_rub новостроек Objective в 3км вокруг центроида участка.
|
||||
-- У objective_lots нет geom → коорды берутся через complexes (latitude/longitude),
|
||||
-- join по ol.complex_id = c.id. Предикат — точный метровый ST_DWithin по geography:
|
||||
--
|
||||
-- ST_DWithin(
|
||||
-- ST_SetSRID(ST_MakePoint(c.longitude, c.latitude), 4326)::geography,
|
||||
-- ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography,
|
||||
-- 3000)
|
||||
--
|
||||
-- Существующий complexes_geom_idx — ПЛОСКИЙ geom-GIST (geometry, SRID 4326). Планировщик
|
||||
-- НЕ может им воспользоваться для geography-выражения над longitude/latitude → Seq Scan
|
||||
-- на complexes на КАЖДЫЙ /analyze (prod EXPLAIN ANALYZE подтвердил). Это тот же урок,
|
||||
-- что в 157 (#157): для geography-предиката нужен функциональный GIST на ИМЕННО том
|
||||
-- выражении, которое стоит в WHERE. Здесь выражение — не `geom::geography`, а
|
||||
-- `ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography`, поэтому индекс
|
||||
-- строится на нём байт-в-байт (планировщик матчит индекс по точному выражению).
|
||||
--
|
||||
-- ИЗМЕРЕНИЯ (prod, EKB-центр lon=60.605 lat=56.838, radius 3000м; «после» — индекс
|
||||
-- создан в BEGIN…ROLLBACK для проверки плана):
|
||||
-- ДО: Seq Scan on complexes, ~111 ms
|
||||
-- ПОСЛЕ: Index Scan на (ST_SetSRID(ST_MakePoint(longitude,latitude),4326)::geography),
|
||||
-- ~53 ms (≈2x). Снимает Seq-Scan-нагрузку с /analyze (timeout-давление, #1850).
|
||||
--
|
||||
-- Плоский complexes_geom_idx НЕ трогаем — он остаётся для geometry-веток (ST_Intersects
|
||||
-- и т.п.). Этот индекс — дополнительный, под конкретный geography-предикат.
|
||||
--
|
||||
-- WHERE-условие частичного индекса (latitude/longitude IS NOT NULL) повторяет фильтр
|
||||
-- запроса (c.latitude/c.longitude IS NOT NULL) → индекс не раздувается строками без
|
||||
-- координат и планировщик его подбирает под тот же предикат.
|
||||
--
|
||||
-- LOCK-ПРОФИЛЬ: обычный (НЕ CONCURRENTLY) CREATE INDEX берёт SHARE на complexes —
|
||||
-- блокирует только writes на миллисекунды. complexes крошечная (~2023 строки), build
|
||||
-- мгновенный. CONCURRENTLY здесь НЕВОЗМОЖЕН: deploy.yml гоняет .sql в транзакции
|
||||
-- (BEGIN/COMMIT), а CREATE INDEX CONCURRENTLY в tx запрещён. Тот же класс риска, что 157.
|
||||
--
|
||||
-- Dependencies: complexes (latitude, longitude, id), objective_lots (complex_id) — обе
|
||||
-- существуют. PostgreSQL 16 + PostGIS 3.4.
|
||||
-- Idempotent: CREATE INDEX IF NOT EXISTS (re-apply безопасен).
|
||||
-- Applied: автоматически через deploy.yml в NN-порядке (трекер _schema_migrations).
|
||||
-- Backend code change НЕ требуется — индекс прозрачен для существующего запроса.
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_complexes_makepoint_geog
|
||||
ON complexes USING gist ((ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography))
|
||||
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
|
||||
|
||||
COMMENT ON INDEX idx_complexes_makepoint_geog IS
|
||||
'Geography GIST для гео-радиусной калибровки цены в /analyze (#168, ветка '
|
||||
'feat/geo-radius-price-calibration): ST_DWithin('
|
||||
'ST_SetSRID(ST_MakePoint(longitude,latitude),4326)::geography, …, 3000м). '
|
||||
'Плоский complexes_geom_idx не обслуживает это geography-выражение → был Seq Scan '
|
||||
'~111мс, стал Index Scan ~53мс. Тот же урок, что 157. См. parcels.py:2705.';
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -131,6 +131,23 @@ const _DEVELOPMENT_TYPE_RU: Record<string, string> = {
|
|||
high_rise: "высотная",
|
||||
};
|
||||
|
||||
// Honest RU caption per backend price_source token. Backend отдаёт латиницей;
|
||||
// в UI финдиректор должен видеть, откуда взята цена, человеческим языком — не
|
||||
// сырой токен «objective_geo_radius». Неизвестный/пустой → "" (suffix опускаем,
|
||||
// сырой токен не печатаем). См. .claude/rules/ui-microcopy.md.
|
||||
const _PRICE_SOURCE_RU: Record<string, string> = {
|
||||
objective_district_median: "медиана сделок квартала",
|
||||
objective_geo_radius: "медиана рынка в радиусе 3 км",
|
||||
district_reference: "ориентир по району",
|
||||
class_norm: "норматив класса",
|
||||
};
|
||||
|
||||
/** Источник цены → честная RU-подпись в «ёлочках»; неизвестный/пустой → "". */
|
||||
function priceSourceCaption(source: string): string {
|
||||
const ru = _PRICE_SOURCE_RU[source];
|
||||
return ru ? `«${ru}»` : "";
|
||||
}
|
||||
|
||||
// ── НСПД-регламент (ПЗЗ) helpers ──────────────────────────────────────────────
|
||||
|
||||
/** КСИТ (max_far) as a plain RU number — e.g. "3,5". */
|
||||
|
|
@ -1742,11 +1759,10 @@ export function adaptFinanceDrawer(
|
|||
}
|
||||
|
||||
const teap = financial.teap_synth;
|
||||
const priceSourceSuffix = priceSourceCaption(financial.price_source);
|
||||
const priceLabel = financial.price_is_calibrated
|
||||
? "рынок (Objective)"
|
||||
: `норматив класса${
|
||||
financial.price_source ? ` · ${financial.price_source}` : ""
|
||||
}`;
|
||||
: `норматив класса${priceSourceSuffix ? ` · ${priceSourceSuffix}` : ""}`;
|
||||
const irrLabel = financial.irr_is_proxy
|
||||
? `${formatPctFromFraction(financial.irr)} % · оценочный (proxy)`
|
||||
: `${formatPctFromFraction(financial.irr)} % · DCF`;
|
||||
|
|
@ -2123,9 +2139,12 @@ export function adaptInvestmentClearance(
|
|||
};
|
||||
}
|
||||
|
||||
const priceSourceSuffix = priceSourceCaption(financial.price_source);
|
||||
const priceCaption = financial.price_is_calibrated
|
||||
? "цена: рынок (Objective)"
|
||||
: "цена: норматив класса";
|
||||
: `цена: норматив класса${
|
||||
priceSourceSuffix ? ` · ${priceSourceSuffix}` : ""
|
||||
}`;
|
||||
const irrCaption = financial.irr_is_proxy ? "оценочный (proxy)" : "DCF";
|
||||
const paybackValue =
|
||||
financial.payback_months != null
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue