[site-finder] EPIC 7: Модуль чувствительности к ключевой ставке (§9.6) + макрокоэффициент (§9.5) #951

Closed
opened 2026-06-02 16:32:07 +00:00 by Light1YT · 8 comments
Owner

Часть #944 (Site Finder v2 / ТЗ ГенГеныч).

Статус: NEW · Приоритет: P0 · ТЗ §9.4, 9.5, 9.6

Scope

Регрессия продаж (objective_lots_history) vs key-rate/ипотека с лагами 0/1/2/3/6 мес по класс/комната/район/чек → фраза §9.6. Макрокоэф (§9.5) + нормализация спроса (§9.4) между режимами ставки. Самый рисковый аналит-эпик — нужна валидация.

Зависимости: EPIC1,5
Уже есть (EXTEND): новый services/forecasting/

**Часть #944 (Site Finder v2 / ТЗ ГенГеныч).** Статус: **NEW** · Приоритет: **P0** · ТЗ §9.4, 9.5, 9.6 ## Scope Регрессия продаж (objective_lots_history) vs key-rate/ипотека с лагами 0/1/2/3/6 мес по класс/комната/район/чек → фраза §9.6. Макрокоэф (§9.5) + нормализация спроса (§9.4) между режимами ставки. Самый рисковый аналит-эпик — нужна валидация. **Зависимости:** EPIC1,5 **Уже есть (EXTEND):** новый services/forecasting/
Light1YT added the
analytics
GG-форсайт
site-finder
labels 2026-06-02 16:32:07 +00:00
Owner

📊 Ground-truth 2026-06-02 (прод-БД)

Данных для регрессии достаточно — Q1 (back-load) снят:

  • продажи: objective_lots.contract_date 2016-10 → 2026-05, 107 месяцев, 193 188 проданных лотов;
  • макро-ряды: macro_indicator (cbr) 86 месяцев, 2019-01 → 2026-02.
    → Лаговая регрессия 0/1/2/3/6 мес §9.6 строится на реальной истории, искусственный back-load не нужен.

🔴 Жёсткая зависимость от #945: ряда ключевой ставки ЦБ в БД НЕТ (ни macro_indicator, ни cbr_mortgage_series). Без него Δставки→продажи не посчитать. Эпик блокируется доливкой key-rate (см. #945, п.1). Промежуточно можно начать на mortgage_rate_weighted как прокси, но это субсидированно-смешанная ставка (7.83%), эластичность по ней исказится — ждать key_rate.

База эластичности уже есть для переиспользования: analytics_queries.py_elasticity_coef, _elasticity_per_bucket_coef (ценовая эластичность по бакетам). Каталога services/forecasting/ пока нет — создаётся здесь.

## 📊 Ground-truth 2026-06-02 (прод-БД) **Данных для регрессии достаточно — Q1 (back-load) снят:** - продажи: `objective_lots.contract_date` **2016-10 → 2026-05, 107 месяцев, 193 188 проданных лотов**; - макро-ряды: `macro_indicator` (cbr) **86 месяцев, 2019-01 → 2026-02**. → Лаговая регрессия 0/1/2/3/6 мес §9.6 строится на реальной истории, искусственный back-load не нужен. **🔴 Жёсткая зависимость от #945:** ряда **ключевой ставки ЦБ в БД НЕТ** (ни `macro_indicator`, ни `cbr_mortgage_series`). Без него Δставки→продажи не посчитать. Эпик блокируется доливкой key-rate (см. #945, п.1). Промежуточно можно начать на `mortgage_rate_weighted` как прокси, но это субсидированно-смешанная ставка (7.83%), эластичность по ней исказится — ждать key_rate. **База эластичности уже есть** для переиспользования: `analytics_queries.py` → `_elasticity_coef`, `_elasticity_per_bucket_coef` (ценовая эластичность по бакетам). Каталога `services/forecasting/` пока нет — создаётся здесь.
lekss361 added this to the Site Finder v2 — Foresight (GG-форсайт) milestone 2026-06-02 19:02:53 +00:00
Owner

Поправка: блокер снят — key_rate в БД ЕСТЬ

Предыдущий коммент называл «нет key_rate» жёстким блокером. Перепроверка: macro_indicator.key_rate присутствует (cbr/rf, daily, 1873 строки, 2019-01-09 → 2026-06-02, загружено сегодня 19:10 UTC). → #951 разблокирован данными, ждать #945 не нужно.

Метод (web-research §9.6): не свободная лаговая регрессия, а Almon (полиномиальный) distributed-lag или ADL(p,q) на месячных данных (соседние лаги ставки сильно коллинеарны), OLS + HAC (Newey–West) ст.ошибки, проверка стационарности/коинтеграции (возможно нужен ECM/разности). Выборки ~86 мес макро + 107 мес сделок хватает на 4–6 лаг-термов ТОЛЬКО с Almon/ADL-ограничением, не на unconstrained free-lag. Ссылка: Stock & Watson (DL/HAC глава), Almon 1965.

Reuse: идиома regr_slope/regr_r2 из analytics_queries._elasticity_coef (L1826-1852) + gating по n/R² — прямо переносится на key_rate→demand регрессию. ⚠️ _current_mortgage_rate (L1623) читает cbr_mortgage_series, а новый forecasting-код должен читать macro_indicator — стандартизировать источник.

## ✅ Поправка: блокер снят — key_rate в БД ЕСТЬ Предыдущий коммент называл «нет key_rate» жёстким блокером. Перепроверка: `macro_indicator.key_rate` присутствует (cbr/rf, **daily, 1873 строки, 2019-01-09 → 2026-06-02**, загружено сегодня 19:10 UTC). → **#951 разблокирован данными**, ждать #945 не нужно. **Метод (web-research §9.6):** не свободная лаговая регрессия, а **Almon (полиномиальный) distributed-lag** или ADL(p,q) на месячных данных (соседние лаги ставки сильно коллинеарны), OLS + HAC (Newey–West) ст.ошибки, проверка стационарности/коинтеграции (возможно нужен ECM/разности). Выборки ~86 мес макро + 107 мес сделок хватает на 4–6 лаг-термов ТОЛЬКО с Almon/ADL-ограничением, не на unconstrained free-lag. Ссылка: Stock & Watson (DL/HAC глава), Almon 1965. **Reuse:** идиома `regr_slope`/`regr_r2` из `analytics_queries._elasticity_coef` (L1826-1852) + gating по n/R² — прямо переносится на key_rate→demand регрессию. ⚠️ `_current_mortgage_rate` (L1623) читает `cbr_mortgage_series`, а новый forecasting-код должен читать `macro_indicator` — стандартизировать источник.
Collaborator

EPIC 7 — детерминированный движок §9.4/§9.5/§9.6 построен (5 PR, advisory)

Слой PR Модуль
фундамент #1007 macro_series.py — monthly macro + regime classifier + lag/confounder helpers
фундамент #1008 sales_series.py — monthly sales series (dual-source A/B, survivorship-caveat)
§9.6 ЯДРО #1009 rate_sensitivity.py — лаговая регрессия Δln(продажи)~Δставка + shrinkage к EKB-prior + фраза X/Y/Z
§9.5 #1010 macro_coefficient.py — композитный множитель спроса (renorm над available, segment-steepness)
§9.4 #1011 demand_normalization.py — нормализация темпа между режимами ставки (honesty-gate)

Каждый PR: opus worker → opus code-reviewer → merge. Детерминированные, numpy, без LLM. 217 unit-тестов на синтетике (без live-DB). Все ADVISORY — не врезаны в endpoint.

DATA FEASIBILITY (главное)

Macro (key_rate) — GREEN (бэкфилл 2019+, вся вариация 2022-25). Sales — узкое место: objective_corpus_room_month ~1-2 мес глубины (нужен wide-backfill), objective_lots.registration_date тянется дальше но survivorship. → §9.6 реален EKB-wide + грубо class/room; полный 4-way сегмент → shrinkage к EKB-prior.

ОТЛОЖЕНО (DB-gated — нужен прод-БД / CLI-сессия с postgres-MCP)

  • PR0: probe глубины данных + wide-backfill objective (period_months_back=36).
  • PR6: backtest backtest_rate_sensitivity.py (time-ordered holdout, как tradein asking→sold) — гейтит PR7.
  • PR7: прод-врезка коэффициентов + §9.6-фразы в forecast-surface.

PR6 follow-up (тюнинг до promotion из advisory)

OOS-валидация выбора лага (spurious-lag) · подтвердить _SHRINK_K/пороги · gate lone-survivor renorm leverage · асимметрия favored-steepness (резилиентность на rate↑) · OverflowError-guard на normalization_factor.

## EPIC 7 — детерминированный движок §9.4/§9.5/§9.6 построен (5 PR, advisory) ✅ | Слой | PR | Модуль | |---|---|---| | фундамент | #1007 | `macro_series.py` — monthly macro + regime classifier + lag/confounder helpers | | фундамент | #1008 | `sales_series.py` — monthly sales series (dual-source A/B, survivorship-caveat) | | §9.6 ЯДРО | #1009 | `rate_sensitivity.py` — лаговая регрессия Δln(продажи)~Δставка + shrinkage к EKB-prior + фраза X/Y/Z | | §9.5 | #1010 | `macro_coefficient.py` — композитный множитель спроса (renorm над available, segment-steepness) | | §9.4 | #1011 | `demand_normalization.py` — нормализация темпа между режимами ставки (honesty-gate) | Каждый PR: opus worker → opus code-reviewer → merge. Детерминированные, numpy, без LLM. **217 unit-тестов** на синтетике (без live-DB). Все ADVISORY — не врезаны в endpoint. ### DATA FEASIBILITY (главное) Macro (key_rate) — GREEN (бэкфилл 2019+, вся вариация 2022-25). Sales — узкое место: `objective_corpus_room_month` ~1-2 мес глубины (нужен wide-backfill), `objective_lots.registration_date` тянется дальше но survivorship. → §9.6 реален EKB-wide + грубо class/room; полный 4-way сегмент → **shrinkage к EKB-prior**. ### ОТЛОЖЕНО (DB-gated — нужен прод-БД / CLI-сессия с postgres-MCP) - **PR0**: probe глубины данных + wide-backfill `objective` (`period_months_back=36`). - **PR6**: backtest `backtest_rate_sensitivity.py` (time-ordered holdout, как tradein asking→sold) — **гейтит PR7**. - **PR7**: прод-врезка коэффициентов + §9.6-фразы в forecast-surface. ### PR6 follow-up (тюнинг до promotion из advisory) OOS-валидация выбора лага (spurious-lag) · подтвердить `_SHRINK_K`/пороги · gate lone-survivor renorm leverage · асимметрия favored-steepness (резилиентность на rate↑) · OverflowError-guard на `normalization_factor`.
Collaborator

📊 Бэктест §9.6 прогнан против прода (2026-06-03) — harness #978 merged.

backtest_rate_sensitivity.py: Source B (objective_lots, 89 выровненных мес 2019-2026), time-ordered holdout (train 62/test 27), point-in-time.

tier lag β inR² OOS hit OOS MAE lift
EKB-wide 3 −0.076 0.168 0.148 0.303
комфорт 3 −0.078 0.155 0.148 0.304 +0.00
стандарт 3 −0.073 0.157 0.185 0.340 +0.04

Вердикт: insufficient OOS signal → keep ADVISORY. OOS hit 0.148 < 0.5 coin-flip; лаг нестабилен. β правильного знака, но out-of-sample не предсказывает.

Причина (гипотеза): survivorship Source B — видны только текущие листинги, старые месяцы недосчитаны → ложный тренд анти-коррелирует со ставкой (0.15 < 0.5 = trend-артефакт). Не валидируем на survivorship-данных.

Next (для снятия advisory-cap): (1) дождаться глубины Source A (corp_room_month, snapshot → без survivorship; 13 мес сейчас, +1/нед) → ре-прогон harness; ИЛИ (2) survivorship-коррекция Source B; ИЛИ (3) вывод, что monthly key_rate→sales на этой грануляции не предсказуемо. До зелёного бэктеста весь forecast-стек (#950-#955) остаётся advisory, в прод не врезается.

📊 **Бэктест §9.6 прогнан против прода (2026-06-03) — harness #978 merged.** `backtest_rate_sensitivity.py`: Source B (objective_lots, 89 выровненных мес 2019-2026), time-ordered holdout (train 62/test 27), point-in-time. | tier | lag | β | inR² | **OOS hit** | OOS MAE | lift | |---|---|---|---|---|---|---| | EKB-wide | 3 | −0.076 | 0.168 | **0.148** | 0.303 | — | | комфорт | 3 | −0.078 | 0.155 | 0.148 | 0.304 | +0.00 | | стандарт | 3 | −0.073 | 0.157 | 0.185 | 0.340 | +0.04 | **Вердикт: insufficient OOS signal → keep ADVISORY.** OOS hit 0.148 < 0.5 coin-flip; лаг нестабилен. β правильного знака, но out-of-sample не предсказывает. **Причина (гипотеза):** survivorship Source B — видны только текущие листинги, старые месяцы недосчитаны → ложный тренд анти-коррелирует со ставкой (0.15 < 0.5 = trend-артефакт). **Не валидируем на survivorship-данных.** **Next (для снятия advisory-cap):** (1) дождаться глубины Source A (corp_room_month, snapshot → без survivorship; 13 мес сейчас, +1/нед) → ре-прогон harness; ИЛИ (2) survivorship-коррекция Source B; ИЛИ (3) вывод, что monthly key_rate→sales на этой грануляции не предсказуемо. До зелёного бэктеста весь forecast-стек (#950-#955) остаётся advisory, в прод не врезается.
Collaborator

🔬 DEFINITIVE (harness #978b, прод 2026-06-03): §9.6 НЕ предсказывает OOS — это РЕАЛЬНО, не survivorship-артефакт.

вариант OOS hit n_test
B raw 0.148 27
B detrended 0.148 27
A (survivorship-free) not scorable 13мес<18

Детренд B (убирает survivorship-тренд) НЕ изменил результат + чистый Source A согласен → вывод робастный: monthly key_rate→sales out-of-sample не предсказуем на данных ЕКБ.

Импликация для продукта: §9.6 rate-prediction остаётся advisory/слабый; прогноз должен опираться на ИЗМЕРЯЕМЫЕ сигналы (3-слойный склад/absorption/конкуренты #950 — не требуют OOS-предсказания). Пути если нужна rate-чувствительность: глубже Source A (corp_room_month добирает +1/нед → ре-прогон harness когда ≥18-24мес) ИЛИ др. грануляция/метод. До тех пор forecast-стек advisory.

🔬 **DEFINITIVE (harness #978b, прод 2026-06-03): §9.6 НЕ предсказывает OOS — это РЕАЛЬНО, не survivorship-артефакт.** | вариант | OOS hit | n_test | |---|---|---| | B raw | 0.148 | 27 | | B detrended | 0.148 | 27 | | A (survivorship-free) | not scorable | 13мес<18 | Детренд B (убирает survivorship-тренд) НЕ изменил результат + чистый Source A согласен → вывод робастный: **monthly key_rate→sales out-of-sample не предсказуем на данных ЕКБ.** **Импликация для продукта:** §9.6 rate-prediction остаётся advisory/слабый; прогноз должен опираться на ИЗМЕРЯЕМЫЕ сигналы (3-слойный склад/absorption/конкуренты #950 — не требуют OOS-предсказания). Пути если нужна rate-чувствительность: глубже Source A (corp_room_month добирает +1/нед → ре-прогон harness когда ≥18-24мес) ИЛИ др. грануляция/метод. До тех пор forecast-стек advisory.
Collaborator

ВАЛИДАЦИЯ ЗАВЕРШЕНА — definitive (чистый Source A, 37мес). Триггернул objective wide-backfill (period_months_back=36) → Source A углубился 13→37 мес (2023-06→2026-06, survivorship-FREE) → ре-прогон harness #978b:

вариант OOS hit n_test
B raw 0.148 27
B detrended 0.148 27
A raw (clean 37мес) 0.400 10
A detrended 0.400 10

Все 4 варианта < coin-flip 0.5. §9.6 НЕ предсказывает OOS — РЕАЛЬНО, не survivorship-артефакт (чистый Source A подтвердил). β верного знака (−0.17), но OOS-предсказания нет.

Решение: §9.6 rate-prediction остаётся ADVISORY (в прод-решения не идёт). Продукт = decision-support на ИЗМЕРЯЕМЫХ сигналах (open-склад/absorption/конкуренты — live, HIGH conf). #978 закрываю (валидация дала definitive-ответ; harness #978b + углублённый Source A — durable артефакты для будущих ре-прогонов). Эпик #951 остаётся open (продуктовое решение: принять advisory / переработать метод-грануляцию).

✅ **ВАЛИДАЦИЯ ЗАВЕРШЕНА — definitive (чистый Source A, 37мес).** Триггернул objective wide-backfill (period_months_back=36) → Source A углубился 13→**37 мес** (2023-06→2026-06, survivorship-FREE) → ре-прогон harness #978b: | вариант | OOS hit | n_test | |---|---|---| | B raw | 0.148 | 27 | | B detrended | 0.148 | 27 | | **A raw (clean 37мес)** | **0.400** | 10 | | **A detrended** | **0.400** | 10 | **Все 4 варианта < coin-flip 0.5. §9.6 НЕ предсказывает OOS — РЕАЛЬНО, не survivorship-артефакт** (чистый Source A подтвердил). β верного знака (−0.17), но OOS-предсказания нет. **Решение:** §9.6 rate-prediction остаётся ADVISORY (в прод-решения не идёт). Продукт = decision-support на ИЗМЕРЯЕМЫХ сигналах (open-склад/absorption/конкуренты — live, HIGH conf). #978 закрываю (валидация дала definitive-ответ; harness #978b + углублённый Source A — durable артефакты для будущих ре-прогонов). Эпик #951 остаётся open (продуктовое решение: принять advisory / переработать метод-грануляцию).
Owner

Аудит против кода (verify-before-reimplement, 2026-06-07): §9.4/9.5/9.6 уже реализованы

Модули EPIC 7 существуют в backend/app/services/forecasting/ (sub-PR'ы #951a-d, детерминированно, без LLM):

  • §9.6 rate_sensitivity.py — CORE: OLS Δln(units) ~ Δ(key_rate)[t-lag], перебор лагов 0/1/2/3/6, выбор best-lag по gate (n≥8 ∧ r2≥min ∧ slope<0), James-Stein шринкаж сегмента к EKB-prior, фраза §9.6 (шаблон). Z = нижняя граница площади чувствительного room×area-бакета.
  • §9.5 macro_coefficient.py, §9.4 demand_normalization.py + sales_series.py/macro_series.py (месячная сетка, log_diff).
  • Данные есть: objective_lots_history = 974k строк.

Остаток (НЕ реимплемент): модуль сам себя помечает ADVISORY-СТАТУС — «остаётся советующим, пока бэктест PR6 его не валидирует; НЕ подключён ни к одному production-эндпоинту». Т.е. незакрыто ровно то, что issue называет «самый рисковый — нужна валидация»:

  1. PR6 out-of-sample бэктест — hold-out последних N мес → предсказание → OOS-ошибка/знак-стабильность → подбор k шринкажа; gate advisory→production.
  2. Wiring в forecast-эндпоинт (только ПОСЛЕ зелёного бэктеста).

⚠️ Бэктест P0-эконометрики гейтит реальные решения — методология (no-leakage split, метрика, k-tuning) требует ревью, не «быстрый автономный PR». Готов взять PR6 с явной выписанной методологией на проверку — подтверди подход, или оставляем advisory. Не реимплементирую существующие §9.4-9.6.

## Аудит против кода (verify-before-reimplement, 2026-06-07): §9.4/9.5/9.6 уже реализованы Модули EPIC 7 **существуют** в `backend/app/services/forecasting/` (sub-PR'ы #951a-d, детерминированно, без LLM): - **§9.6** `rate_sensitivity.py` — CORE: OLS `Δln(units) ~ Δ(key_rate)[t-lag]`, перебор лагов **0/1/2/3/6**, выбор best-lag по gate (n≥8 ∧ r2≥min ∧ slope<0), James-Stein шринкаж сегмента к EKB-prior, фраза §9.6 (шаблон). Z = нижняя граница площади чувствительного room×area-бакета. - **§9.5** `macro_coefficient.py`, **§9.4** `demand_normalization.py` + `sales_series.py`/`macro_series.py` (месячная сетка, log_diff). - Данные есть: `objective_lots_history` = **974k** строк. **Остаток (НЕ реимплемент):** модуль сам себя помечает `ADVISORY-СТАТУС` — «остаётся советующим, пока **бэктест PR6** его не валидирует; НЕ подключён ни к одному production-эндпоинту». Т.е. незакрыто ровно то, что issue называет «самый рисковый — нужна валидация»: 1. **PR6 out-of-sample бэктест** — hold-out последних N мес → предсказание → OOS-ошибка/знак-стабильность → подбор k шринкажа; gate advisory→production. 2. Wiring в forecast-эндпоинт (только ПОСЛЕ зелёного бэктеста). ⚠️ Бэктест P0-эконометрики гейтит реальные решения — методология (no-leakage split, метрика, k-tuning) требует ревью, не «быстрый автономный PR». Готов взять PR6 с явной выписанной методологией на проверку — подтверди подход, или оставляем advisory. Не реимплементирую существующие §9.4-9.6.
Collaborator

Закрыто после мега-детального closure-аудита (deep-code-reviewer) по запросу — verdict CLEAN. P0 «самый рисковый аналит-эпик» — аудит проводился adversarial по статкорректности.

DoD §9.4/§9.5/§9.6 покрыт:

  • §9.6 регрессия sales vs key-rate/ипотека, лаги 0/1/2/3/6, сегменты класс/комната/район/чек → NL-фраза (rate_sensitivity.py, regression.py).
  • §9.5 макрокоэф: 5 backed-каналов + renormalize (тот самый guard, которого не хватало #1167) (macro_coefficient.py).
  • §9.4 нормализация спроса между режимами ставки (demand_normalization.py).

Статистика (adversarial): нет lookahead/leakage (лаг-выравнивание верное — sales[t] на rate[t-lag]), честная деградация на тонких данных (§26), sign-gating, significance-gate, детерминизм. НЕТ saturation-класса — renormalize корректен. 382/382 теста.

Follow-up (не блокер): (1) живая OOS-ре-валидация на текущей прод-истории — инструмент scripts/backtest_rate_sensitivity.py есть, уже драйвил выбор Almon-оценщика (#978); нужен прогон на проде + архив verdict-JSON в вату. (2) Освежить stale «ADVISORY / NOT connected» docstrings (доковый дрейф после врезки #978).

Закрыто после мега-детального closure-аудита (deep-code-reviewer) по запросу — verdict **CLEAN**. P0 «самый рисковый аналит-эпик» — аудит проводился adversarial по статкорректности. **DoD §9.4/§9.5/§9.6 покрыт:** - **§9.6** регрессия sales vs key-rate/ипотека, лаги **0/1/2/3/6**, сегменты класс/комната/район/чек → NL-фраза (`rate_sensitivity.py`, `regression.py`). - **§9.5** макрокоэф: 5 backed-каналов + **renormalize** (тот самый guard, которого не хватало #1167) (`macro_coefficient.py`). - **§9.4** нормализация спроса между режимами ставки (`demand_normalization.py`). **Статистика (adversarial):** нет lookahead/leakage (лаг-выравнивание верное — `sales[t]` на `rate[t-lag]`), честная деградация на тонких данных (§26), sign-gating, significance-gate, детерминизм. **НЕТ saturation-класса** — renormalize корректен. **382/382 теста.** **Follow-up (не блокер):** (1) живая OOS-ре-валидация на текущей прод-истории — инструмент `scripts/backtest_rate_sensitivity.py` есть, уже драйвил выбор Almon-оценщика (#978); нужен прогон на проде + архив verdict-JSON в вату. (2) Освежить stale «ADVISORY / NOT connected» docstrings (доковый дрейф после врезки #978).
Sign in to join this conversation.
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#951
No description provided.