From 4ec762a2027a44fe371963f1ad1dd7298b00d9fd Mon Sep 17 00:00:00 2001 From: Light1YT Date: Fri, 5 Jun 2026 14:09:55 +0500 Subject: [PATCH] =?UTF-8?q?feat(site-finder):=20forecast=20Section=206=20(?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B3=D0=BD=D0=BE=D0=B7/=D1=81=D1=86=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=D1=80=D0=B8=D0=B8/confidence)=20on=20analysis=20sc?= =?UTF-8?q?reen=20(#998)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders the §22 SiteFinderReport (future_market/scenarios/confidence) as a new Section 6 on the modern analysis page (/site-finder/analysis/[cad]), mirroring the Section1-5 pattern. Polls GET /parcels/{cad}/forecast (202 pending → 200 ready) via TanStack Query. Render-only (horizon selector = separate #996). - 6.1 прогноз по горизонтам (deficit_index, MOI, demand/supply, ставка) - 6.2 сценарии base/aggressive/conservative + rate_path - 6.3 уверенность: level pill + явный список drivers (confidence.factors) - types/forecast.ts typed against real prod report; fixture parcel-forecast.json (run #68) - reuses design tokens + Badge/KpiCard; advisory disclaimer always shown Part of EPIC #958. Deps 955-A2/A4 (backend §13, merged). --- .../analysis/[cad]/AnalysisPageContent.tsx | 13 + .../analysis/ForecastConfidenceBlock.tsx | 186 ++ .../analysis/ForecastHorizonsBlock.tsx | 177 ++ .../site-finder/analysis/ScenariosBlock.tsx | 237 ++ .../site-finder/analysis/Section6Forecast.tsx | 362 +++ .../site-finder/analysis/forecast-helpers.ts | 69 + frontend/src/lib/mocks/parcel-forecast.json | 2270 +++++++++++++++++ frontend/src/lib/site-finder-api.ts | 42 +- frontend/src/types/forecast.ts | 181 ++ 9 files changed, 3536 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/site-finder/analysis/ForecastConfidenceBlock.tsx create mode 100644 frontend/src/components/site-finder/analysis/ForecastHorizonsBlock.tsx create mode 100644 frontend/src/components/site-finder/analysis/ScenariosBlock.tsx create mode 100644 frontend/src/components/site-finder/analysis/Section6Forecast.tsx create mode 100644 frontend/src/components/site-finder/analysis/forecast-helpers.ts create mode 100644 frontend/src/lib/mocks/parcel-forecast.json create mode 100644 frontend/src/types/forecast.ts diff --git a/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx b/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx index 03b193f3..5a8471af 100644 --- a/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx +++ b/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx @@ -7,6 +7,7 @@ import { Section2NetworksUtilities } from "@/components/site-finder/analysis/Sec import { Section3SettingsAndCompetitors } from "@/components/site-finder/analysis/Section3SettingsAndCompetitors"; import { Section4Estimate } from "@/components/site-finder/analysis/Section4Estimate"; import { Section5Atmosphere } from "@/components/site-finder/analysis/Section5Atmosphere"; +import { Section6Forecast } from "@/components/site-finder/analysis/Section6Forecast"; import { useParcelAnalyzeQuery } from "@/lib/site-finder-api"; import type { ParcelAnalysis } from "@/types/site-finder"; @@ -134,6 +135,9 @@ export function AnalysisPageContent({ cad }: Props) { {/* Section 5 — IMPLEMENTED in A11 */} + + {/* Section 6 — IMPLEMENTED in 958-B3 (§22 forecast) */} + @@ -215,6 +219,15 @@ const NAV_ITEMS = [ }, { id: "section-4", label: "4. Оценка" }, { id: "section-5", label: "5. Атмосфера" }, + { + id: "section-6", + label: "6. Прогноз", + children: [ + { id: "section-6-1", label: "6.1 Прогноз по горизонтам" }, + { id: "section-6-2", label: "6.2 Сценарии" }, + { id: "section-6-3", label: "6.3 Уверенность" }, + ], + }, ]; function AnalysisSidebarNav() { diff --git a/frontend/src/components/site-finder/analysis/ForecastConfidenceBlock.tsx b/frontend/src/components/site-finder/analysis/ForecastConfidenceBlock.tsx new file mode 100644 index 00000000..341ac56a --- /dev/null +++ b/frontend/src/components/site-finder/analysis/ForecastConfidenceBlock.tsx @@ -0,0 +1,186 @@ +"use client"; + +/** + * 6.3 Уровень уверенности — level pill + EXPLICIT drivers list from + * confidence.factors (DoD: «явный список drivers»). + * + * The factors map is mostly { note, level, value } entries, but the prod + * envelope also carries a stray boolean `advisory_capped` — narrowed out via a + * runtime type guard so we never render a non-factor. + */ + +import { Badge } from "@/components/ui/Badge"; +import type { ConfidenceFactor, ReportConfidence } from "@/types/forecast"; +import { CONFIDENCE_RU, confidenceVariant } from "./forecast-helpers"; + +interface Props { + confidence: ReportConfidence; +} + +// Human-readable labels for known factor keys; unknown keys fall back to the key. +const FACTOR_RU: Record = { + deal_count: "Объём сделок", + analog_count: "Аналоги (ЖК)", + domrf_coverage: "Покрытие ДОМ.РФ", + history_months: "Глубина истории", + component: "Компонент", +}; + +function isConfidenceFactor(v: unknown): v is ConfidenceFactor { + if (typeof v !== "object" || v === null) return false; + const o = v as Record; + return ( + typeof o.note === "string" && + (o.level === "high" || o.level === "medium" || o.level === "low") + ); +} + +function factorLabel(key: string): string { + if (FACTOR_RU[key]) return FACTOR_RU[key]; + // component_2..N → «Компонент N» (note уже несёт «вкладывающий сервис: …») + const m = key.match(/^component_(\d+)$/); + if (m) return `Компонент ${m[1]}`; + return key; +} + +function formatFactorValue(value: number | null): string | null { + if (value == null) return null; + // Fractions in 0..1 read as percent (e.g. coverage); integers/large stay raw. + if (value > 0 && value < 1) return `${Math.round(value * 100)}%`; + return Math.round(value).toLocaleString("ru"); +} + +export function ForecastConfidenceBlock({ confidence }: Props) { + const level = confidence.level; + + const drivers = Object.entries(confidence.factors) + .filter((entry): entry is [string, ConfidenceFactor] => + isConfidenceFactor(entry[1]), + ) + // Sort high → medium → low so the strongest evidence reads first. + .sort((a, b) => LEVEL_RANK[a[1].level] - LEVEL_RANK[b[1].level]); + + return ( +
+ {/* Level pill + rationale */} +
+
+ + Уровень уверенности + + {level ? ( + + {CONFIDENCE_RU[level]} + + ) : ( + + не определена + + )} +
+ {confidence.rationale && ( +

+ {confidence.rationale} +

+ )} +
+ + {/* Explicit drivers list */} + {drivers.length > 0 ? ( +
+ + Факторы достоверности ({drivers.length}) + +
+ {drivers.map(([key, factor]) => { + const valueStr = formatFactorValue(factor.value); + return ( +
+ + {CONFIDENCE_RU[factor.level]} + +
+
+ {factorLabel(key)} + {valueStr != null && ( + + · {valueStr} + + )} +
+
+ {factor.note} +
+
+
+ ); + })} +
+
+ ) : ( +

+ Факторы достоверности недоступны. +

+ )} +
+ ); +} + +const LEVEL_RANK: Record = { + high: 0, + medium: 1, + low: 2, +}; diff --git a/frontend/src/components/site-finder/analysis/ForecastHorizonsBlock.tsx b/frontend/src/components/site-finder/analysis/ForecastHorizonsBlock.tsx new file mode 100644 index 00000000..a8ea991a --- /dev/null +++ b/frontend/src/components/site-finder/analysis/ForecastHorizonsBlock.tsx @@ -0,0 +1,177 @@ +"use client"; + +/** + * 6.1 Прогноз по горизонтам — compact table over future_market.forecasts_by_horizon. + * + * Columns: горизонт (мес) · индекс дефицита (color-coded) · месяцы предложения · + * прогноз спроса/предложения · ставка (% годовых) · фраза чувствительности к ставке. + * + * Deficit semantics (HARD, see frontend rules): + * deficit_index > 0 → недонасыщенность (повод строить) → success/green + * deficit_index < 0 → затоварка (предложения > спроса) → warn/danger + * deficit_index ≈ 0 → баланс → neutral + */ + +import { Badge } from "@/components/ui/Badge"; +import type { DemandSupplyForecast } from "@/types/forecast"; +import { + DEFICIT_BALANCE_EPS, + deficitVariant, + deficitWord, + fmtNum, +} from "./forecast-helpers"; + +interface Props { + forecasts: DemandSupplyForecast[]; +} + +const TH_STYLE: React.CSSProperties = { + textAlign: "left", + padding: "8px 12px", + fontSize: 11, + fontWeight: 500, + letterSpacing: "0.04em", + textTransform: "uppercase", + color: "var(--fg-secondary)", + whiteSpace: "nowrap", + borderBottom: "1px solid var(--border-card)", +}; + +const TD_STYLE: React.CSSProperties = { + padding: "10px 12px", + fontSize: 13, + color: "var(--fg-primary)", + verticalAlign: "top", + borderBottom: "1px solid var(--border-soft)", + fontVariantNumeric: "tabular-nums", +}; + +const NUM_TD: React.CSSProperties = { ...TD_STYLE, textAlign: "right" }; + +export function ForecastHorizonsBlock({ forecasts }: Props) { + if (forecasts.length === 0) { + return ( +

+ Прогноз по горизонтам недоступен. +

+ ); + } + + const rows = [...forecasts].sort( + (a, b) => a.horizon_months - b.horizon_months, + ); + + return ( +
+
+ + + + + + + + + + + + + {rows.map((f) => { + const di = f.deficit_index; + const balanced = Math.abs(di) < DEFICIT_BALANCE_EPS; + return ( + + + + + + + + + ); + })} + +
ГоризонтИндекс дефицита + Мес. предложения + Прогноз спроса + Прогноз предложения + Ставка
{f.horizon_months} мес. + + {di > 0 ? "+" : ""} + {fmtNum(di, 2)} + + + {deficitWord(di)} + + + {balanced ? "—" : fmtNum(f.months_of_inventory, 1)} + + {Math.round(f.projected_demand_units).toLocaleString("ru")} + + {Math.round(f.projected_supply_units).toLocaleString("ru")} + {fmtNum(f.rate_future, 2)}%
+
+ + {/* Rate-sensitivity phrase — shared across horizons in current data; show + the distinct phrases once each to avoid noisy repetition. */} + +
+ ); +} + +function RateSensitivityNotes({ + forecasts, +}: { + forecasts: DemandSupplyForecast[]; +}) { + const phrases = Array.from( + new Set( + forecasts + .map((f) => f.rate_sensitivity_phrase?.trim()) + .filter((p): p is string => !!p), + ), + ); + if (phrases.length === 0) return null; + + return ( +
+ + Чувствительность к ключевой ставке + + {phrases.map((p, i) => ( + + {p.charAt(0).toUpperCase() + p.slice(1)} + {/\.$/.test(p) ? "" : "."} + + ))} +
+ ); +} diff --git a/frontend/src/components/site-finder/analysis/ScenariosBlock.tsx b/frontend/src/components/site-finder/analysis/ScenariosBlock.tsx new file mode 100644 index 00000000..60f14488 --- /dev/null +++ b/frontend/src/components/site-finder/analysis/ScenariosBlock.tsx @@ -0,0 +1,237 @@ +"use client"; + +/** + * 6.2 Сценарии — conservative / base / aggressive side by side. + * + * For each scenario shows the deficit_index at the target horizon (preferring + * the scenario's own forecast row, falling back to scenarios_summary) and the + * rate_path across all horizons (ключевая ставка, % годовых). + */ + +import { Badge } from "@/components/ui/Badge"; +import type { + DemandSupplyForecast, + RatePath, + ReportScenarios, + ScenarioKey, + ScenariosSummary, +} from "@/types/forecast"; +import { + DEFICIT_BALANCE_EPS, + deficitVariant, + deficitWord, + fmtNum, +} from "./forecast-helpers"; + +interface Props { + scenarios: ReportScenarios; + scenariosSummary: ScenariosSummary | null; + targetHorizon: number; +} + +// Display order: conservative → base → aggressive (worst-to-best rate path). +const SCENARIO_ORDER: ScenarioKey[] = ["conservative", "base", "aggressive"]; + +const SCENARIO_RU: Record = { + conservative: "Консервативный", + base: "Базовый", + aggressive: "Агрессивный", +}; + +const SCENARIO_HINT: Record = { + conservative: "ставка выше", + base: "ставка без изменений", + aggressive: "ставка ниже", +}; + +function pickHorizonForecast( + forecasts: DemandSupplyForecast[], + targetHorizon: number, +): DemandSupplyForecast | null { + if (forecasts.length === 0) return null; + const exact = forecasts.find((f) => f.horizon_months === targetHorizon); + if (exact) return exact; + // Fall back to the longest available horizon. + return [...forecasts].sort((a, b) => b.horizon_months - a.horizon_months)[0]; +} + +function deficitForScenario( + key: ScenarioKey, + scenarios: ReportScenarios, + scenariosSummary: ScenariosSummary | null, + targetHorizon: number, +): number | null { + const sc = scenarios.by_scenario[key]; + const fc = sc ? pickHorizonForecast(sc.forecasts, targetHorizon) : null; + if (fc) return fc.deficit_index; + if (scenariosSummary) return scenariosSummary[key]; + return null; +} + +export function ScenariosBlock({ + scenarios, + scenariosSummary, + targetHorizon, +}: Props) { + const present = SCENARIO_ORDER.filter( + (k) => scenarios.by_scenario[k] != null || scenariosSummary != null, + ); + + if (present.length === 0) { + return ( +

+ Сценарный прогноз недоступен. +

+ ); + } + + return ( +
+
+ {present.map((key) => { + const sc = scenarios.by_scenario[key]; + const di = deficitForScenario( + key, + scenarios, + scenariosSummary, + targetHorizon, + ); + return ( + + ); + })} +
+

+ Индекс дефицита приведён на целевом горизонте {targetHorizon} мес. + Конверт ставки — путь ключевой ставки (% годовых) по горизонтам. +

+
+ ); +} + +function ScenarioCard({ + name, + hint, + deficitIndex, + ratePath, + targetHorizon, +}: { + name: string; + hint: string; + deficitIndex: number | null; + ratePath: RatePath | undefined; + targetHorizon: number; +}) { + const horizons = ratePath + ? Object.keys(ratePath) + .map((h) => Number(h)) + .sort((a, b) => a - b) + : []; + + return ( +
+
+
+ {name} +
+
+ {hint} +
+
+ + {/* Deficit at target horizon */} +
+ + Индекс дефицита · {targetHorizon} мес. + + {deficitIndex != null ? ( +
+ + {deficitIndex > 0 ? "+" : ""} + {fmtNum(deficitIndex, 2)} + + + {Math.abs(deficitIndex) < DEFICIT_BALANCE_EPS + ? "баланс" + : deficitWord(deficitIndex)} + +
+ ) : ( + + )} +
+ + {/* Rate path */} + {horizons.length > 0 && ratePath && ( +
+ + Конверт ставки + +
+ {horizons.map((h) => ( + + {h} мес.: {fmtNum(ratePath[String(h)], 2)}% + + ))} +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/site-finder/analysis/Section6Forecast.tsx b/frontend/src/components/site-finder/analysis/Section6Forecast.tsx new file mode 100644 index 00000000..12773718 --- /dev/null +++ b/frontend/src/components/site-finder/analysis/Section6Forecast.tsx @@ -0,0 +1,362 @@ +"use client"; + +/** + * Section6Forecast — "6. Прогноз" (958-B3 / §22). + * + * Renders the async §22 demand/supply forecast, scenarios and confidence on the + * Site Finder analysis screen. Self-fetches via useParcelForecastQuery, which + * POLLs GET /api/v1/parcels/{cad}/forecast every 4s until status === "ready" + * (the forecast is enqueued by useParcelAnalyzeQuery on page mount — render-only + * here, no trigger). + * + * Layout mirrors Section1-5: dark HeadlineBar + section root id="section-6". + * exec_summary banner (headline + verdict + KPI row) + * 6.1 Прогноз по горизонтам → ForecastHorizonsBlock + * 6.2 Сценарии → ScenariosBlock + * 6.3 Уверенность → ForecastConfidenceBlock + * advisory disclaimer (footer) + */ + +import { LineChart } from "lucide-react"; + +import { HeadlineBar } from "@/components/ui/HeadlineBar"; +import { KpiCard } from "@/components/site-finder/KpiCard"; +import { useParcelForecastQuery } from "@/lib/site-finder-api"; +import type { ForecastReport } from "@/types/forecast"; + +import { ForecastHorizonsBlock } from "./ForecastHorizonsBlock"; +import { ScenariosBlock } from "./ScenariosBlock"; +import { ForecastConfidenceBlock } from "./ForecastConfidenceBlock"; +import { CONFIDENCE_RU, deficitVariant, fmtNum } from "./forecast-helpers"; + +interface Props { + cad: string; +} + +const ADVISORY_DISCLAIMER = + "Оценка advisory — не основание для инвест-решения."; + +const SCROLL_MARGIN = 72; + +// KpiCard color enum (site-finder variant) mapped from Badge variant words. +function kpiColorForDeficit(deficitIndex: number): "green" | "red" | "neutral" { + const v = deficitVariant(deficitIndex); + if (v === "success") return "green"; + if (v === "danger") return "red"; + return "neutral"; +} + +// ── Section6Forecast ───────────────────────────────────────────────────────── + +export function Section6Forecast({ cad }: Props) { + const { data, isLoading, error } = useParcelForecastQuery(cad); + + const isPending = + isLoading || (data != null && data.status !== "ready") || data == null; + + // ── Pending: forecast still computing (poll in flight) ───────────────────── + if (isPending && !error) { + return ( +
+ +
+
+ Прогноз рассчитывается… +
+
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+
+
+ ); + } + + // ── Error ────────────────────────────────────────────────────────────────── + if (error || !data || data.status !== "ready" || !data.report) { + return ( +
+ +
+ + Данные прогноза недоступны +
+
+ ); + } + + return ; +} + +// ── ForecastReady (report present) ─────────────────────────────────────────── + +function ForecastReady({ report }: { report: ForecastReport }) { + const exec = report.exec_summary; + const fm = report.future_market; + + // Target horizon: prefer future_supply.horizon_months, else median of meta + // horizons, else 12. + const targetHorizon = + fm.future_supply?.horizon_months ?? + medianHorizon(report.meta.horizons) ?? + 12; + + const kn = exec.key_numbers; + + const hasHorizons = fm.forecasts_by_horizon.length > 0; + const hasScenarios = + Object.keys(report.scenarios.by_scenario).length > 0 || + fm.scenarios_summary != null; + const hasConfidence = + report.confidence.level != null || + Object.keys(report.confidence.factors).length > 0; + + const hasAny = hasHorizons || hasScenarios || hasConfidence; + + // Headline subtitle = future_market summary (one sentence «откуда / что значит»). + const subtitle = fm.summary ?? undefined; + + return ( +
+ + +
+ {/* exec_summary verdict + KPI row */} + {exec.verdict && ( +

+ {exec.verdict} +

+ )} + +
+ 0 ? "+" : ""}${fmtNum(kn.deficit_index, 2)}` + : "—" + } + color={ + kn.deficit_index != null + ? kpiColorForDeficit(kn.deficit_index) + : "neutral" + } + /> + + + +
+ + {!hasAny && ( +
+ Данные прогноза недоступны +
+ )} + + {/* 6.1 Прогноз по горизонтам */} + {hasHorizons && ( + + + + )} + + {/* 6.2 Сценарии */} + {hasScenarios && ( + + + + )} + + {/* 6.3 Уверенность */} + {hasConfidence && ( + + + + )} + + {/* Advisory disclaimer */} +

+ {ADVISORY_DISCLAIMER} +

+
+
+ ); +} + +// ── SubBlock wrapper (6.1 / 6.2 / 6.3) ─────────────────────────────────────── + +function SubBlock({ + id, + title, + children, +}: { + id: string; + title: string; + children: React.ReactNode; +}) { + return ( +
+

+ {title} +

+
+ {children} +
+
+ ); +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function medianHorizon(horizons: number[]): number | null { + if (horizons.length === 0) return null; + const sorted = [...horizons].sort((a, b) => a - b); + return sorted[Math.floor(sorted.length / 2)]; +} + +function confidenceKpiColor( + level: "high" | "medium" | "low" | null, +): "green" | "amber" | "red" | "neutral" { + if (level === "high") return "green"; + if (level === "medium") return "amber"; + if (level === "low") return "red"; + return "neutral"; +} diff --git a/frontend/src/components/site-finder/analysis/forecast-helpers.ts b/frontend/src/components/site-finder/analysis/forecast-helpers.ts new file mode 100644 index 00000000..069b2202 --- /dev/null +++ b/frontend/src/components/site-finder/analysis/forecast-helpers.ts @@ -0,0 +1,69 @@ +/** + * Shared helpers for Section 6 (forecast) blocks — deficit semantics, confidence + * mapping, RU number formatting. Kept framework-agnostic (no JSX) so the three + * forecast blocks share one source of truth. + */ + +import type { BadgeVariant } from "@/components/ui/Badge"; +import type { ConfidenceLevel } from "@/types/forecast"; + +/** Below this |deficit_index| we treat the market as balanced (neutral). */ +export const DEFICIT_BALANCE_EPS = 0.05; + +/** + * Deficit semantics (HARD): >0 недонасыщенность (повод строить, success); + * <0 затоварка (warn/danger); ≈0 баланс (neutral). + */ +export function deficitVariant(deficitIndex: number): BadgeVariant { + if (deficitIndex > DEFICIT_BALANCE_EPS) return "success"; + if (deficitIndex < -DEFICIT_BALANCE_EPS) return "danger"; + return "neutral"; +} + +export function deficitWord(deficitIndex: number): string { + if (deficitIndex > DEFICIT_BALANCE_EPS) return "недонасыщенность"; + if (deficitIndex < -DEFICIT_BALANCE_EPS) return "затоварка"; + return "баланс"; +} + +// ── Confidence ──────────────────────────────────────────────────────────────── + +export const CONFIDENCE_RU: Record = { + high: "высокая", + medium: "средняя", + low: "низкая", +}; + +export function confidenceVariant(level: ConfidenceLevel): BadgeVariant { + if (level === "high") return "success"; + if (level === "medium") return "warning"; + return "danger"; +} + +/** + * ConfidenceBadge expects a numeric 0..1 value but the report carries a level + * only. Derive a representative midpoint so the numeric badge reads sensibly + * if/when reused. + */ +export function confidenceLevelToValue(level: ConfidenceLevel): number { + if (level === "high") return 0.85; + if (level === "medium") return 0.55; + return 0.25; +} + +// ── Number formatting ───────────────────────────────────────────────────────── + +/** + * Fixed-decimal RU number (тонкая неразрывная группировка тысяч). Нормализует + * ведущий знак минуса в Unicode «−» (U+2212) per ui-microcopy (≥ ≤ ± Unicode). + * `.replace` бьёт только первое вхождение = ведущий знак (RU-группировка — NBSP, + * не дефис), так что числовая часть не затрагивается. + */ +export function fmtNum(value: number, digits = 1): string { + return value + .toLocaleString("ru", { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }) + .replace("-", "−"); +} diff --git a/frontend/src/lib/mocks/parcel-forecast.json b/frontend/src/lib/mocks/parcel-forecast.json new file mode 100644 index 00000000..c02b9f80 --- /dev/null +++ b/frontend/src/lib/mocks/parcel-forecast.json @@ -0,0 +1,2270 @@ +{ + "status": "ready", + "run_id": 68, + "created_at": "2026-06-04T18:30:00+00:00", + "report": { + "meta": { + "cad_num": "66:41:0702048:27", + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "district": "Кировский", + "horizons": [ + 6, + 12, + 18, + 24 + ], + "generated_at": null, + "schema_version": "1.0", + "generated_advisory": true + }, + "scoring": { + "overall": 0.42, + "product_scores": { + "scores": { + "demand": { + "key": "demand", + "value": 0.985, + "reason": "Темп поглощения ~511.0 ед./мес по локации.", + "source": "market_metrics", + "confidence": "high" + }, + "infra_fit": { + "key": "infra_fit", + "value": 0.652, + "reason": "Сумма весов близких POI ~0.113 (метро/школы/ТЦ B6).", + "source": "poi_score", + "confidence": "medium" + }, + "commercial": { + "key": "commercial", + "value": null, + "reason": "Коммерческий сигнал §10.4 недоступен (нет данных по нежилому).", + "source": "unavailable", + "confidence": "low" + }, + "confidence": { + "key": "confidence", + "value": 0.617, + "reason": "Сводная надёжность: 7/9 скоров доступно, вкладывающие сервисы ~low.", + "source": "aggregate", + "confidence": "low" + }, + "market_fit": { + "key": "market_fit", + "value": 0.0, + "reason": "Затоварка (индекс -1.0) — предложения больше спроса.", + "source": "demand_supply_forecast", + "confidence": "low" + }, + "supply_risk": { + "key": "supply_risk", + "value": 0.631, + "reason": "Риск избытка предложения (давление+затоварка) ~0.37 — низкий.", + "source": "future_supply+market_metrics", + "confidence": "low" + }, + "differentiation": { + "key": "differentiation", + "value": 0.0, + "reason": "0 дефицит-ниш (USP, §10.5) — white-space не выражен.", + "source": "recommendation", + "confidence": "low" + }, + "price_feasibility": { + "key": "price_feasibility", + "value": 0.326, + "reason": "Прокси платежа ~124 тыс.₽/мес (субсид. ставка, §7.9 — доступность относительная).", + "source": "affordability", + "confidence": "low" + }, + "future_competition": { + "key": "future_competition", + "value": 0.329, + "reason": "33 релевантных будущих конкурентов (плотность ~0.67) — конкуренция высокая.", + "source": "competitors", + "confidence": "medium" + }, + "mortgage_sensitivity": { + "key": "mortgage_sensitivity", + "value": null, + "reason": "Чувствительность к ключевой ставке не определена (§9.6).", + "source": "unavailable", + "confidence": "low" + } + }, + "overall": 0.42, + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "horizon_months": 12 + }, + "special_indices": { + "indices": { + "product_void": { + "key": "product_void", + "label": "нет белых пятен", + "value": 0.0, + "detail": { + "n_void": 0, + "n_ranked": 15, + "threshold": 0.25, + "top_void_segments": [] + }, + "method": "deficit_threshold_share", + "advisory": true, + "confidence": "low" + }, + "cost_of_error": { + "key": "cost_of_error", + "label": "риск дорогой ошибки", + "value": 0.377, + "detail": { + "ref_area_m2": 50.0, + "risk_source": "overstock_index", + "price_per_m2": 153140.0, + "avg_ticket_rub": 7657019.0, + "oversupply_risk": 0.738 + }, + "method": "oversupply_x_ticket", + "advisory": true, + "confidence": "medium" + }, + "launch_window": { + "key": "launch_window", + "label": "6 мес", + "value": 0.0, + "detail": { + "deficit_by_horizon": { + "6": -1.0, + "12": -1.0, + "18": -1.0, + "24": -1.0 + }, + "best_horizon_months": 6 + }, + "method": "deficit_peak_scan", + "advisory": true, + "confidence": "low" + }, + "cannibalization": { + "key": "cannibalization", + "label": "Комфорт", + "value": 0.608, + "detail": { + "radius_km": 1.0, + "n_same_class": 17, + "target_class": "Комфорт", + "n_competitors": 33 + }, + "method": "same_class_relevance_share", + "advisory": true, + "confidence": "medium" + }, + "artificial_demand": { + "key": "artificial_demand", + "label": null, + "value": null, + "detail": { + "reason": "нет проданных лотов сегмента — доля ипотеки неизмерима (НЕ фабрикуем)" + }, + "method": "unavailable", + "advisory": true, + "confidence": "low" + }, + "competitor_strength": { + "key": "competitor_strength", + "label": "топ-5 конкурентов", + "value": 0.773, + "detail": { + "top_n": 5, + "radius_km": 1.0, + "n_competitors": 33 + }, + "method": "relevance_weight_top_n_mean", + "advisory": true, + "confidence": "medium" + } + }, + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "district": "Кировский", + "confidence": "low" + } + }, + "advisory": true, + "scenarios": { + "summary": "Сценарии (3): conservative, base, aggressive — разброс по конверту ставки.", + "by_scenario": { + "base": { + "advisory": true, + "scenario": "base", + "forecasts": [ + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 14.5, + "balance_ratio": 0.054, + "balance_units": -40622.4, + "deficit_index": -1.0, + "horizon_months": 6, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.947, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.75, + "velocity_per_month": 5.67 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 0.0, + "months_of_inventory": 116.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 2330.8, + "projected_supply_units": 42953.2, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 14.5, + "balance_ratio": 0.115, + "balance_units": -35960.7, + "deficit_index": -1.0, + "horizon_months": 12, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.935, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.741, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 0.0, + "months_of_inventory": 116.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 4661.6, + "projected_supply_units": 40622.4, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 14.5, + "balance_ratio": 0.181, + "balance_units": -31693.1, + "deficit_index": -1.0, + "horizon_months": 18, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.922, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.731, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 394.0, + "months_of_inventory": 117.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 6992.5, + "projected_supply_units": 38685.5, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 14.5, + "balance_ratio": 0.256, + "balance_units": -27031.4, + "deficit_index": -1.0, + "horizon_months": 24, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.91, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.721, + "velocity_per_month": 5.67 + } + ], + "future_online_units": 394.0, + "months_of_inventory": 117.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 9323.3, + "projected_supply_units": 36354.7, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + } + ], + "rate_path": { + "6": 14.5, + "12": 14.5, + "18": 14.5, + "24": 14.5 + } + }, + "aggressive": { + "advisory": true, + "scenario": "aggressive", + "forecasts": [ + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 10.75, + "balance_ratio": 0.054, + "balance_units": -40622.4, + "deficit_index": -1.0, + "horizon_months": 6, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.947, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.75, + "velocity_per_month": 5.67 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 0.0, + "months_of_inventory": 116.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 2330.8, + "projected_supply_units": 42953.2, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 10.0, + "balance_ratio": 0.115, + "balance_units": -35960.7, + "deficit_index": -1.0, + "horizon_months": 12, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.935, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.741, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 0.0, + "months_of_inventory": 116.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 4661.6, + "projected_supply_units": 40622.4, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 9.25, + "balance_ratio": 0.181, + "balance_units": -31693.1, + "deficit_index": -1.0, + "horizon_months": 18, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.922, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.731, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 394.0, + "months_of_inventory": 117.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 6992.5, + "projected_supply_units": 38685.5, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 8.5, + "balance_ratio": 0.256, + "balance_units": -27031.4, + "deficit_index": -1.0, + "horizon_months": 24, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.91, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.721, + "velocity_per_month": 5.67 + } + ], + "future_online_units": 394.0, + "months_of_inventory": 117.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 9323.3, + "projected_supply_units": 36354.7, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + } + ], + "rate_path": { + "6": 10.75, + "12": 10.0, + "18": 9.25, + "24": 8.5 + } + }, + "conservative": { + "advisory": true, + "scenario": "conservative", + "forecasts": [ + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 17.0, + "balance_ratio": 0.054, + "balance_units": -40622.4, + "deficit_index": -1.0, + "horizon_months": 6, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.947, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.75, + "velocity_per_month": 5.67 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 0.0, + "months_of_inventory": 116.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 2330.8, + "projected_supply_units": 42953.2, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 17.5, + "balance_ratio": 0.115, + "balance_units": -35960.7, + "deficit_index": -1.0, + "horizon_months": 12, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.935, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.741, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 0.0, + "months_of_inventory": 116.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 4661.6, + "projected_supply_units": 40622.4, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 18.0, + "balance_ratio": 0.181, + "balance_units": -31693.1, + "deficit_index": -1.0, + "horizon_months": 18, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.922, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.731, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 394.0, + "months_of_inventory": 117.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 6992.5, + "projected_supply_units": 38685.5, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 18.5, + "balance_ratio": 0.256, + "balance_units": -27031.4, + "deficit_index": -1.0, + "horizon_months": 24, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.91, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.721, + "velocity_per_month": 5.67 + } + ], + "future_online_units": 394.0, + "months_of_inventory": 117.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 9323.3, + "projected_supply_units": 36354.7, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + } + ], + "rate_path": { + "6": 17.0, + "12": 17.5, + "18": 18.0, + "24": 18.5 + } + } + } + }, + "confidence": { + "level": "low", + "factors": { + "component": { + "note": "вкладывающий сервис: high", + "level": "high", + "value": null + }, + "deal_count": { + "note": "34083 сделок — достаточно", + "level": "high", + "value": 34083 + }, + "component_2": { + "note": "вкладывающий сервис: low", + "level": "low", + "value": null + }, + "component_3": { + "note": "вкладывающий сервис: low", + "level": "low", + "value": null + }, + "component_4": { + "note": "вкладывающий сервис: low", + "level": "low", + "value": null + }, + "component_5": { + "note": "вкладывающий сервис: low", + "level": "low", + "value": null + }, + "component_6": { + "note": "вкладывающий сервис: low", + "level": "low", + "value": null + }, + "component_7": { + "note": "вкладывающий сервис: low", + "level": "low", + "value": null + }, + "component_8": { + "note": "вкладывающий сервис: low", + "level": "low", + "value": null + }, + "analog_count": { + "note": "32 ЖК-аналога — достаточно", + "level": "high", + "value": 32 + }, + "domrf_coverage": { + "note": "покрытие domrf↔objective 40.0% — умеренно", + "level": "medium", + "value": 0.4 + }, + "history_months": { + "note": "6 мес истории — мало", + "level": "low", + "value": 6 + }, + "advisory_capped": false + }, + "rationale": "Low потому что 6 мес истории — мало / вкладывающий сервис: low." + }, + "market_now": { + "summary": "Текущий рынок: абсорбция ~511.0 ед./мес, средняя цена ~193 925 ₽/м², 32 ЖК-конкурентов рядом.", + "competitors": [ + { + "obj_id": 64701, + "dev_name": "СЗ МАЛЕВИЧ", + "ready_dt": "2027-09-30", + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.04197015, + "flat_count": 244, + "units_sold": 1135, + "avg_area_pd": 45.5, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": 1753, + "units_available": 646, + "avg_price_per_m2_rub": 116671 + }, + { + "obj_id": 59095, + "dev_name": "Ривьера Инвест Екатеринбург", + "ready_dt": "2026-09-30", + "comm_name": "ЖК Галактика", + "obj_class": "Бизнес", + "distance_m": 567.98073461, + "flat_count": 256, + "units_sold": 279, + "avg_area_pd": 59.0, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": 830, + "units_available": 652, + "avg_price_per_m2_rub": 194483 + }, + { + "obj_id": 53537, + "dev_name": "ЛСР", + "ready_dt": "2026-09-30", + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.43194204, + "flat_count": 871, + "units_sold": 3278, + "avg_area_pd": 43.1, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": 3974, + "units_available": 1198, + "avg_price_per_m2_rub": 197769 + }, + { + "obj_id": 59830, + "dev_name": "ТЭН", + "ready_dt": "2027-06-30", + "comm_name": "Жилой комплекс \"ОК: Премиум\"", + "obj_class": "Премиум", + "distance_m": 695.45034527, + "flat_count": 376, + "units_sold": 2473, + "avg_area_pd": 47.4, + "site_status": "Строящиеся", + "district_name": "Орджоникидзевский", + "lots_with_price": 3720, + "units_available": 3268, + "avg_price_per_m2_rub": 159388 + }, + { + "obj_id": 67529, + "dev_name": "Ривьера Инвест Екатеринбург", + "ready_dt": "2029-03-31", + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 697.74380602, + "flat_count": 649, + "units_sold": 4115, + "avg_area_pd": 49.1, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": 6121, + "units_available": 2032, + "avg_price_per_m2_rub": 155875 + }, + { + "obj_id": 59835, + "dev_name": "ТЭН", + "ready_dt": "2027-06-30", + "comm_name": "Жилой комплекс \"ОК: Премиум\"", + "obj_class": null, + "distance_m": 729.61364806, + "flat_count": 0, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Орджоникидзевский", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 67389, + "dev_name": "ЛСР", + "ready_dt": "2027-12-31", + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.61242881, + "flat_count": 326, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 59833, + "dev_name": "ТЭН", + "ready_dt": "2027-06-30", + "comm_name": "Жилой комплекс \"ОК: Премиум\"", + "obj_class": "Премиум", + "distance_m": 778.18109036, + "flat_count": 309, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Орджоникидзевский", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 59834, + "dev_name": "ТЭН", + "ready_dt": "2027-06-30", + "comm_name": "Жилой комплекс \"ОК: Премиум\"", + "obj_class": "Премиум", + "distance_m": 877.12507407, + "flat_count": 239, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Орджоникидзевский", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 59832, + "dev_name": "ТЭН", + "ready_dt": "2027-06-30", + "comm_name": "Жилой комплекс \"ОК: Премиум\"", + "obj_class": "Премиум", + "distance_m": 888.18514236, + "flat_count": 309, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Орджоникидзевский", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 59831, + "dev_name": "ТЭН", + "ready_dt": "2027-06-30", + "comm_name": "Жилой комплекс \"ОК: Премиум\"", + "obj_class": "Премиум", + "distance_m": 888.18514236, + "flat_count": 309, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Орджоникидзевский", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 64296, + "dev_name": "Самолет", + "ready_dt": "2027-12-31", + "comm_name": "Квартал Ауруум", + "obj_class": "Комфорт", + "distance_m": 1076.33796946, + "flat_count": 631, + "units_sold": 884, + "avg_area_pd": 49.6, + "site_status": "Строящиеся", + "district_name": "Кировский", + "lots_with_price": 1542, + "units_available": 1461, + "avg_price_per_m2_rub": 188349 + }, + { + "obj_id": 70303, + "dev_name": "ТЭН", + "ready_dt": "2027-12-31", + "comm_name": null, + "obj_class": null, + "distance_m": 1206.0329853, + "flat_count": 0, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 64409, + "dev_name": "ТЭН", + "ready_dt": "2026-06-30", + "comm_name": "Клубный дом инженера Ятеса", + "obj_class": null, + "distance_m": 1268.00007871, + "flat_count": 0, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 50617, + "dev_name": "ТЭН", + "ready_dt": "2026-06-30", + "comm_name": "Клубный дом инженера Ятеса", + "obj_class": "Элит", + "distance_m": 1309.13499873, + "flat_count": 80, + "units_sold": 27, + "avg_area_pd": 101.6, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": 109, + "units_available": 218, + "avg_price_per_m2_rub": 365477 + }, + { + "obj_id": 67549, + "dev_name": "Синара-Девелопмент", + "ready_dt": "2028-12-31", + "comm_name": "Жилой квартал «Тихий центр» 1 оч. 2 этап", + "obj_class": "Комфорт", + "distance_m": 1604.45420794, + "flat_count": 397, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 67548, + "dev_name": "Синара-Девелопмент", + "ready_dt": "2027-12-31", + "comm_name": "Жилой квартал «Тихий центр» 1 оч. 1 этап", + "obj_class": "Комфорт", + "distance_m": 1685.17793396, + "flat_count": 231, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Железнодорожный", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 50756, + "dev_name": "СКМ Девелопмент", + "ready_dt": "2027-12-31", + "comm_name": "Б-26 Дом на высоте", + "obj_class": null, + "distance_m": 2019.63164955, + "flat_count": 0, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Кировский", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 50755, + "dev_name": "СКМ Девелопмент", + "ready_dt": "2027-12-31", + "comm_name": "Б-26 Дом на высоте", + "obj_class": "Комфорт", + "distance_m": 2019.63164955, + "flat_count": 267, + "units_sold": null, + "avg_area_pd": null, + "site_status": "Строящиеся", + "district_name": "Кировский", + "lots_with_price": null, + "units_available": null, + "avg_price_per_m2_rub": null + }, + { + "obj_id": 50754, + "dev_name": "СКМ Девелопмент", + "ready_dt": "2026-12-31", + "comm_name": "Б.26", + "obj_class": null, + "distance_m": 2019.63164955, + "flat_count": 0, + "units_sold": 1993, + "avg_area_pd": 51.8, + "site_status": "Строящиеся", + "district_name": "Кировский", + "lots_with_price": 2545, + "units_available": 1068, + "avg_price_per_m2_rub": 173390 + } + ], + "supply_layers": { + "rows": [ + { + "layer": 1, + "method": "objective_lots: available (not sold) lots per district×class", + "source": "objective", + "obj_class": "бизнес", + "complex_id": null, + "confidence": "high", + "area_estimate": 72213.8, + "district_name": "Втузгородок", + "snapshot_date": null, + "dev_group_name": null, + "units_estimate": 922, + "expected_online_date": null + }, + { + "layer": 1, + "method": "objective_lots: available (not sold) lots per district×class", + "source": "objective", + "obj_class": "комфорт", + "complex_id": null, + "confidence": "high", + "area_estimate": 853517.6, + "district_name": "Втузгородок", + "snapshot_date": null, + "dev_group_name": null, + "units_estimate": 23218, + "expected_online_date": null + }, + { + "layer": 1, + "method": "objective_lots: available (not sold) lots per district×class", + "source": "objective", + "obj_class": "стандарт", + "complex_id": null, + "confidence": "high", + "area_estimate": 210052.4, + "district_name": "Втузгородок", + "snapshot_date": null, + "dev_group_name": null, + "units_estimate": 3361, + "expected_online_date": null + }, + { + "layer": 1, + "method": "objective_lots: available (not sold) lots per district×class", + "source": "objective", + "obj_class": "комфорт", + "complex_id": null, + "confidence": "high", + "area_estimate": 449293.3, + "district_name": "ЖБИ", + "snapshot_date": null, + "dev_group_name": null, + "units_estimate": 16526, + "expected_online_date": null + }, + { + "layer": 1, + "method": "objective_lots: available (not sold) lots per district×class", + "source": "objective", + "obj_class": "стандарт", + "complex_id": null, + "confidence": "high", + "area_estimate": 46027.2, + "district_name": "ЖБИ", + "snapshot_date": null, + "dev_group_name": null, + "units_estimate": 1257, + "expected_online_date": null + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='BAZA Development'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 21874.1, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "BAZA Development", + "units_estimate": 437, + "expected_online_date": "2028-12-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='Архитектурно-строительный центр Правобережный'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 15109.9, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "Архитектурно-строительный центр Правобережный", + "units_estimate": 296, + "expected_online_date": "2028-03-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='Атомстройкомплекс'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 99442.4, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "Атомстройкомплекс", + "units_estimate": 1660, + "expected_online_date": "2027-12-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='ЛСР'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 7436.8, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "ЛСР", + "units_estimate": 224, + "expected_online_date": "2027-09-30" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='Паритет Девелопмент'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 28633.6, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "Паритет Девелопмент", + "units_estimate": 691, + "expected_online_date": "2027-12-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='Первостроитель'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 18385.0, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "Первостроитель", + "units_estimate": 360, + "expected_online_date": "2027-12-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='Практика'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 17962.6, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "Практика", + "units_estimate": 389, + "expected_online_date": "2027-12-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='Самолет'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 31253.3, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "Самолет", + "units_estimate": 631, + "expected_online_date": "2027-12-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='СКМ Девелопмент'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 12504.3, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "СКМ Девелопмент", + "units_estimate": 267, + "expected_online_date": "2027-12-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='Стройтэк'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 37331.9, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "Стройтэк", + "units_estimate": 693, + "expected_online_date": "2027-06-30" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name='Эфес'", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 32325.6, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": "Эфес", + "units_estimate": 698, + "expected_online_date": "2027-12-31" + }, + { + "layer": 3, + "method": "domrf_kn_objects: ready_dt beyond horizon & ~no listed flats; grouped by dev_group_name=None", + "source": "domrf_multiphase", + "obj_class": null, + "complex_id": null, + "confidence": "low", + "area_estimate": 20349.5, + "district_name": "Кировский", + "snapshot_date": null, + "dev_group_name": null, + "units_estimate": 394, + "expected_online_date": "2027-09-30" + } + ], + "n_rows": 17, + "open_units": 45284, + "future_units": 6740, + "hidden_units": 0 + }, + "market_metrics": { + "n_lots": 79367, + "n_sold": 34083, + "district": "Кировский", + "obj_count": 32, + "confidence": "high", + "n_available": 45284, + "premise_kind": "квартира", + "area_velocity": 22918.4, + "unit_velocity": 511.0, + "window_months": 6, + "absorption_rate": 0.0113, + "liquidity_index": { + "1": 2.51, + "2": 1.205, + "3": 0.295, + "4": 0.01, + "студия": 0.98 + }, + "overstock_index": 0.738, + "months_of_supply": 88.6, + "sell_through_pct": 42.9, + "price_sensitivity": -1.5, + "demand_concentration": 0.352, + "price_sensitivity_source": "fallback" + } + }, + "product_tz": { + "mix": [ + { + "bucket": "2-1-к", + "obj_class": "бизнес", + "deficit_index": -1.0 + }, + { + "bucket": "3-2-к", + "obj_class": "бизнес", + "deficit_index": -1.0 + }, + { + "bucket": "4-3-к", + "obj_class": "бизнес", + "deficit_index": -1.0 + }, + { + "bucket": "5-80+ м²", + "obj_class": "бизнес", + "deficit_index": -1.0 + }, + { + "bucket": "1-Студия", + "obj_class": "бизнес", + "deficit_index": -1.0 + }, + { + "bucket": "2-1-к", + "obj_class": "комфорт", + "deficit_index": -1.0 + }, + { + "bucket": "3-2-к", + "obj_class": "комфорт", + "deficit_index": -1.0 + }, + { + "bucket": "4-3-к", + "obj_class": "комфорт", + "deficit_index": -1.0 + }, + { + "bucket": "5-80+ м²", + "obj_class": "комфорт", + "deficit_index": -1.0 + }, + { + "bucket": "1-Студия", + "obj_class": "комфорт", + "deficit_index": -1.0 + }, + { + "bucket": "2-1-к", + "obj_class": "эконом", + "deficit_index": -1.0 + }, + { + "bucket": "3-2-к", + "obj_class": "эконом", + "deficit_index": -1.0 + }, + { + "bucket": "4-3-к", + "obj_class": "эконом", + "deficit_index": -1.0 + }, + { + "bucket": "5-80+ м²", + "obj_class": "эконом", + "deficit_index": -1.0 + }, + { + "bucket": "1-Студия", + "obj_class": "эконом", + "deficit_index": -1.0 + } + ], + "usp": [], + "reasons": [ + { + "why": "Сегмент класс (бизнес): deficit_index −1.00 на горизонте 12 мес.", + "drivers": [ + { + "value": -1.0, + "factor": "deficit_index", + "direction": "−" + } + ], + "advisory": true, + "rejected": [ + { + "reason": "затоварка", + "alternative": "класс (комфорт)", + "deficit_index": -1.0 + }, + { + "reason": "затоварка", + "alternative": "класс (эконом)", + "deficit_index": -1.0 + } + ], + "confidence": "low", + "what_would_change": [ + "Рост ключевой ставки на ≥1 п.п. снижает нормализованный спрос (§9.4) → индекс падает.", + "Выход скрытого запаса (Layer2, §9.3) в радиусе участка → предложение растёт → индекс падает.", + "Сужение горизонта с 12 до 6 мес → меньше поглощённого спроса → индекс может вырасти." + ] + } + ], + "summary": "Продукт: рекомендован класс «бизнес».", + "obj_class": "бизнес", + "commercial": { + "caveat": "коммерция: нет достаточных данных (premise_kind=нежилое, горизонт 12 мес)", + "advisory": true, + "available": false + } + }, + "exec_summary": { + "verdict": "Сводный советующий вердикт: индекс дефицита −1.00 на целевом горизонте; ≈116.6 мес конкурирующего предложения; итоговый продуктовый скор 0.42; уверенность низкая. Оценка advisory — не основание для инвест-решения.", + "headline": "Рассмотреть «бизнес» как целевой класс продукта.", + "key_numbers": { + "confidence": "low", + "deficit_index": -1.0, + "overall_score": 0.42, + "months_of_inventory": 116.6 + }, + "overall_confidence": "low" + }, + "future_market": { + "summary": "Прогноз: затоварка (предложения больше спроса), индекс дефицита −1.00, ≈116.6 мес конкурирующего предложения на горизонте.", + "future_supply": { + "index": 0.0, + "district": "Кировский", + "breakdown": { + "index": 0.0, + "open_units": 45284, + "hidden_units": 0, + "months_of_pressure": 0.0, + "future_units_by_horizon": 0.0, + "monthly_absorption_units": 511.0 + }, + "confidence": "low", + "premise_kind": "квартира", + "horizon_months": 12 + }, + "scenarios_summary": { + "base": -1.0, + "aggressive": -1.0, + "conservative": -1.0 + }, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.935, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.741, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "forecasts_by_horizon": [ + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 14.5, + "balance_ratio": 0.054, + "balance_units": -40622.4, + "deficit_index": -1.0, + "horizon_months": 6, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.947, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.75, + "velocity_per_month": 5.67 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 0.0, + "months_of_inventory": 116.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 2330.8, + "projected_supply_units": 42953.2, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 14.5, + "balance_ratio": 0.115, + "balance_units": -35960.7, + "deficit_index": -1.0, + "horizon_months": 12, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.935, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.741, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 0.0, + "months_of_inventory": 116.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 4661.6, + "projected_supply_units": 40622.4, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 14.5, + "balance_ratio": 0.181, + "balance_units": -31693.1, + "deficit_index": -1.0, + "horizon_months": 18, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.922, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.731, + "velocity_per_month": 5.67 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + } + ], + "future_online_units": 394.0, + "months_of_inventory": 117.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 6992.5, + "projected_supply_units": 38685.5, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + }, + { + "segment": { + "district": "Кировский", + "obj_class": "Комфорт", + "room_bucket": null, + "price_bucket": null + }, + "advisory": true, + "confidence": "low", + "open_units": 45284, + "rate_future": 14.5, + "balance_ratio": 0.256, + "balance_units": -27031.4, + "deficit_index": -1.0, + "horizon_months": 24, + "macro_coefficient": 0.7602, + "future_competitors": [ + { + "obj_id": 64701, + "comm_name": "ЖК \"Малевич\", Жилой комплекс \"Малевич\"", + "obj_class": "Комфорт", + "distance_m": 132.0, + "flats_total": 244, + "relevance_weight": 0.91, + "velocity_per_month": 2.0 + }, + { + "obj_id": 53727, + "comm_name": "ЖК Космос", + "obj_class": "Комфорт", + "distance_m": 521.5, + "flats_total": 219, + "relevance_weight": 0.744, + "velocity_per_month": 0.0 + }, + { + "obj_id": 67389, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 757.6, + "flats_total": 326, + "relevance_weight": 0.723, + "velocity_per_month": 0.0 + }, + { + "obj_id": 3687, + "comm_name": "Жилой комплекс \"Малевич\" (ЖК \"Малевич\")", + "obj_class": "Комфорт", + "distance_m": 178.3, + "flats_total": 348, + "relevance_weight": 0.722, + "velocity_per_month": 0.0 + }, + { + "obj_id": 53537, + "comm_name": "ЖК \"Азина 16\"", + "obj_class": "Комфорт", + "distance_m": 636.4, + "flats_total": 871, + "relevance_weight": 0.721, + "velocity_per_month": 5.67 + } + ], + "future_online_units": 394.0, + "months_of_inventory": 117.6, + "hidden_release_units": 0.0, + "base_pace_units_per_mo": 511.0, + "projected_demand_units": 9323.3, + "projected_supply_units": 36354.7, + "demand_norm_coefficient": 1.0, + "rate_sensitivity_phrase": "недостаточно данных для distributed-lag оценки чувствительности к ставке" + } + ] + }, + "schema_version": "1.0" + } +} \ No newline at end of file diff --git a/frontend/src/lib/site-finder-api.ts b/frontend/src/lib/site-finder-api.ts index a7136d8e..5903e53b 100644 --- a/frontend/src/lib/site-finder-api.ts +++ b/frontend/src/lib/site-finder-api.ts @@ -9,7 +9,7 @@ */ import { useQuery } from "@tanstack/react-query"; -import { apiFetch } from "@/lib/api"; +import { apiFetch, apiFetchWithStatus } from "@/lib/api"; import { MOCK_PARCELS_BBOX, MOCK_RECENT_PARCELS, @@ -19,6 +19,8 @@ import { import fixtureParcels from "@/lib/mocks/parcels-bbox.json"; import fixtureAnalyze from "@/lib/mocks/parcel-analyze.json"; import fixturePoiScore from "@/lib/mocks/poi-score.json"; +import fixtureForecast from "@/lib/mocks/parcel-forecast.json"; +import type { ForecastEnvelope } from "@/types/forecast"; // ── Types ───────────────────────────────────────────────────────────────────── @@ -391,3 +393,41 @@ export function useParcelPoiScoreQuery(cad: string) { retry: 1, }); } + +// ── Hook: useParcelForecastQuery (958-B3 / §22) ────────────────────────────── + +/** + * Polls GET /api/v1/parcels/{cad}/forecast until the async §22 forecast is + * ready. The forecast is enqueued fire-and-forget by useParcelAnalyzeQuery on + * page mount (POST /analyze) — this hook does NOT trigger it, only reads. + * + * Endpoint contract: + * 200 → { status: "ready", run_id, created_at, report } + * 202 → { status: "pending" } (also returned gracefully on any error — never 404/500) + * + * Polls every 4s while pending; stops once status === "ready". Mock mode + * (MOCK_ANALYZE) returns the committed prod fixture immediately. + */ +export function useParcelForecastQuery(cad: string) { + return useQuery({ + queryKey: ["parcel-forecast", cad], + queryFn: async (): Promise => { + if (MOCK_ANALYZE) { + // Fixture is raw JSON (loose shape: factors carries a stray + // `advisory_capped` boolean) — cast via unknown, runtime guards in the + // confidence block narrow factor entries. + return fixtureForecast as unknown as ForecastEnvelope; + } + // apiFetchWithStatus tolerates the 202 path (Accepted) without throwing. + const { body } = await apiFetchWithStatus( + `/api/v1/parcels/${encodeURIComponent(cad)}/forecast`, + ); + return body; + }, + enabled: !!cad, + // Stop polling once ready; otherwise re-poll every 4s while pending. + refetchInterval: (query) => + query.state.data?.status === "ready" ? false : 4000, + retry: 1, + }); +} diff --git a/frontend/src/types/forecast.ts b/frontend/src/types/forecast.ts new file mode 100644 index 00000000..b7dcfc9e --- /dev/null +++ b/frontend/src/types/forecast.ts @@ -0,0 +1,181 @@ +/** + * §22 forecast / scenarios / confidence — TypeScript contract. + * + * Ground truth: the committed prod report at + * `src/lib/mocks/parcel-forecast.json` (run #68, parcel 66:41:0702048:27). + * Types describe ONLY the sub-shapes Section 6 renders; the §13 report is + * larger (scoring, market_now, product_tz, …) — fields not consumed here are + * intentionally left as optional/loose to avoid over-coupling to the backend. + * + * The endpoint envelope (GET /api/v1/parcels/{cad}/forecast): + * 200 → { status: "ready", run_id, created_at, report } + * 202 → { status: "pending" } + */ + +export type ConfidenceLevel = "high" | "medium" | "low"; + +/** Scenario keys produced by the §22 envelope generator. */ +export type ScenarioKey = "base" | "aggressive" | "conservative"; + +// ── Segment ───────────────────────────────────────────────────────────────── + +export interface ForecastSegment { + district: string | null; + obj_class: string | null; + room_bucket: string | null; + price_bucket: string | null; +} + +// ── Future competitor (forward-looking, distinct from market_now competitors) ─ + +export interface FutureCompetitor { + obj_id: number; + comm_name: string | null; + obj_class: string | null; + distance_m: number; + flats_total: number; + relevance_weight: number; + velocity_per_month: number; +} + +// ── Demand / supply forecast (one per horizon) ─────────────────────────────── + +export interface DemandSupplyForecast { + segment: ForecastSegment; + advisory: boolean; + confidence: ConfidenceLevel; + open_units: number; + rate_future: number; + balance_ratio: number; + balance_units: number; + /** −1..1 — see deficit semantics: >0 недонасыщенность, <0 затоварка. */ + deficit_index: number; + horizon_months: number; + macro_coefficient: number; + future_competitors: FutureCompetitor[]; + future_online_units: number; + months_of_inventory: number; + hidden_release_units: number; + base_pace_units_per_mo: number; + projected_demand_units: number; + projected_supply_units: number; + demand_norm_coefficient: number; + rate_sensitivity_phrase: string; +} + +// ── Exec summary ───────────────────────────────────────────────────────────── + +export interface ReportExecSummaryKeyNumbers { + confidence: ConfidenceLevel | null; + deficit_index: number | null; + overall_score: number | null; + months_of_inventory: number | null; +} + +export interface ReportExecSummary { + headline: string | null; + verdict: string | null; + key_numbers: ReportExecSummaryKeyNumbers; + overall_confidence: ConfidenceLevel | null; +} + +// ── Future market ──────────────────────────────────────────────────────────── + +export interface FutureSupplyBreakdown { + index: number; + open_units: number; + hidden_units: number; + months_of_pressure: number; + future_units_by_horizon: number; + monthly_absorption_units: number; +} + +export interface FutureSupply { + index: number; + district: string | null; + breakdown: FutureSupplyBreakdown; + confidence: ConfidenceLevel; + premise_kind: string | null; + horizon_months: number; +} + +/** deficit_index per scenario at the target horizon. */ +export interface ScenariosSummary { + base: number; + aggressive: number; + conservative: number; +} + +export interface ReportFutureMarket { + summary: string | null; + forecasts_by_horizon: DemandSupplyForecast[]; + future_supply: FutureSupply | null; + scenarios_summary: ScenariosSummary | null; + future_competitors: FutureCompetitor[]; +} + +// ── Scenarios ──────────────────────────────────────────────────────────────── + +/** Key path = horizon months (as string), value = key rate (% годовых). */ +export type RatePath = Record; + +export interface ScenarioForecast { + advisory: boolean; + scenario: string; + forecasts: DemandSupplyForecast[]; + rate_path: RatePath; +} + +export interface ReportScenarios { + summary: string | null; + by_scenario: Partial>; +} + +// ── Confidence ─────────────────────────────────────────────────────────────── + +export interface ConfidenceFactor { + note: string; + level: ConfidenceLevel; + value: number | null; +} + +export interface ReportConfidence { + level: ConfidenceLevel | null; + rationale: string | null; + /** The explicit drivers map (DoD: «явный список drivers»). */ + factors: Record; +} + +// ── Meta ───────────────────────────────────────────────────────────────────── + +export interface ReportMeta { + cad_num: string; + segment: ForecastSegment; + district: string | null; + horizons: number[]; + generated_at: string | null; + schema_version: string; + generated_advisory: boolean; +} + +// ── Report (only the sections Section 6 renders are typed precisely) ────────── + +export interface ForecastReport { + meta: ReportMeta; + /** Always true for §22 — drives the advisory disclaimer. */ + advisory: boolean; + exec_summary: ReportExecSummary; + future_market: ReportFutureMarket; + scenarios: ReportScenarios; + confidence: ReportConfidence; + schema_version?: string; +} + +// ── Envelope ───────────────────────────────────────────────────────────────── + +export interface ForecastEnvelope { + status: "ready" | "pending"; + run_id?: number; + created_at?: string; + report?: ForecastReport; +} -- 2.45.3