feat(financial): DCF-график продаж по реальному темпу поглощения (rank 1, #1881) #1890

Merged
bot-backend merged 1 commit from feat/dcf-velocity-schedule into main 2026-06-23 22:08:09 +00:00
Collaborator

Summary

Финмодель брала фиксированное окно продаж 30 мес независимо от рынка (schedule_is_default всегда true). Теперь окно считается из локального темпа поглощения, который уже вычисляется в том же /analyze (compute_velocity), но финмодель его игнорировала — единственный потребитель, который его не использовал.

Корректность абсорбции (ключевое — легко сделать неправильно)

velocity.monthly_velocity_sqm = Σ поглощения ВСЕГО конкурентного набора в радиусе (м²/мес), НЕ темп одного проекта. Наивное residential / monthly_velocity_sqm означало бы, что новый проект забирает весь рыночный темп → дико оптимистично.
per-project absorption = monthly_velocity / max(n_with_sales, 1) (темп одного типичного локального продавца). Поле project_absorption_sqm_per_month добавлено в VelocityResult (objective-путь); rosreestr-fallback и вырожденные пути → None (поквартальный count без по-проектной декомпозиции не должен задавать график).

Изменения (11 файлов, +404/−21)

  • velocity.py: project_absorption_sqm_per_month (+as_dict).
  • financial.py: окно = clamp(ceil(residential/velocity), MIN=6, MAX=120); эскроу-инвариант sales_end=max(sales_start+base, constr_end) сверху; _build_monthly_cashflow_CashflowResult(cashflow, realized, schedule_is_market); schedule_is_default флипается; новое поле sales_duration_months.
  • parcel_financial.py / parcels.py: проброс velocity в финмост.
  • concept.py: sales_duration_months (additive).
  • Generative §1c (placement.py/concepts.py): передаёт None → дефолт 30 мес (out of scope, follow-up).
  • frontend api-types.ts (regen) + site-finder.ts + mock.

Инвариант

Σ undiscounted cashflow == net_profit держится (перенос выручки во времени не меняет сумму; маркетинг+НДС+налог прорейтятся на признание выручки). Чистая функция.

Test plan

  • +13 тестов: None→дефолт+инвариант; рыночная velocity; клампы MIN/MAX; эскроу-floor; non-finite/0/neg→fallback; rosreestr→None; инвариант по размерам окна; регресс PR-3 (ровно одна смена знака + IRR не proxy на коротком окне).
  • Полный бэкенд: 3414 passed, 0 failed. ruff + mypy(strict financial.py) чисто. psycopg3-grep чист (SQL не трогал).
  • api-types перегенерены авторитетно (openapi-codegen-check пройдёт).
  • code-review: 2× approve, 0 majors (adversarial correctness-lens подтвердил семантику абсорбции, инвариант, не-proxy IRR, клампы, rosreestr-None).

⚠️ Контекст видимости (важно)

Прод-замер показал: financial_estimate сейчас фаерит ~0% (резолв ПЗЗ-зоны проваливается на ~96% участков — разрывы тер-зон 45–86 м). Этот PR корректен и улучшает точность когда финмодель показывается, но реальный рычаг видимости — фикс резолва зоны (отдельная задача, выше приоритетом). См. vault inbox/2026-06-24-...zoning-rootcause.

Refs #1881

🤖 Generated with Claude Code

## Summary Финмодель брала **фиксированное окно продаж 30 мес** независимо от рынка (`schedule_is_default` всегда `true`). Теперь окно считается из **локального темпа поглощения**, который уже вычисляется в том же `/analyze` (`compute_velocity`), но финмодель его игнорировала — единственный потребитель, который его не использовал. ## Корректность абсорбции (ключевое — легко сделать неправильно) `velocity.monthly_velocity_sqm` = **Σ поглощения ВСЕГО конкурентного набора** в радиусе (м²/мес), НЕ темп одного проекта. Наивное `residential / monthly_velocity_sqm` означало бы, что новый проект забирает весь рыночный темп → дико оптимистично. → **per-project absorption = `monthly_velocity / max(n_with_sales, 1)`** (темп одного типичного локального продавца). Поле `project_absorption_sqm_per_month` добавлено в `VelocityResult` (objective-путь); rosreestr-fallback и вырожденные пути → `None` (поквартальный count без по-проектной декомпозиции не должен задавать график). ## Изменения (11 файлов, +404/−21) - `velocity.py`: `project_absorption_sqm_per_month` (+as_dict). - `financial.py`: окно = `clamp(ceil(residential/velocity), MIN=6, MAX=120)`; эскроу-инвариант `sales_end=max(sales_start+base, constr_end)` сверху; `_build_monthly_cashflow` → `_CashflowResult(cashflow, realized, schedule_is_market)`; `schedule_is_default` флипается; новое поле `sales_duration_months`. - `parcel_financial.py` / `parcels.py`: проброс velocity в финмост. - `concept.py`: `sales_duration_months` (additive). - Generative §1c (`placement.py`/`concepts.py`): передаёт `None` → дефолт 30 мес (out of scope, **follow-up**). - frontend `api-types.ts` (regen) + `site-finder.ts` + mock. ## Инвариант Σ undiscounted cashflow == net_profit держится (перенос выручки во времени не меняет сумму; маркетинг+НДС+налог прорейтятся на признание выручки). Чистая функция. ## Test plan - [x] +13 тестов: None→дефолт+инвариант; рыночная velocity; клампы MIN/MAX; эскроу-floor; non-finite/0/neg→fallback; rosreestr→None; инвариант по размерам окна; **регресс PR-3** (ровно одна смена знака + IRR не proxy на коротком окне). - [x] Полный бэкенд: **3414 passed, 0 failed**. ruff + mypy(strict `financial.py`) чисто. psycopg3-grep чист (SQL не трогал). - [x] api-types перегенерены авторитетно (openapi-codegen-check пройдёт). - [x] code-review: **2× approve, 0 majors** (adversarial correctness-lens подтвердил семантику абсорбции, инвариант, не-proxy IRR, клампы, rosreestr-None). ## ⚠️ Контекст видимости (важно) Прод-замер показал: `financial_estimate` сейчас фаерит ~0% (резолв ПЗЗ-зоны проваливается на ~96% участков — разрывы тер-зон 45–86 м). Этот PR **корректен и улучшает точность когда финмодель показывается**, но реальный рычаг видимости — фикс резолва зоны (отдельная задача, выше приоритетом). См. vault `inbox/2026-06-24-...zoning-rootcause`. Refs #1881 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-06-23 21:57:19 +00:00
feat(financial): DCF-график продаж по реальному темпу поглощения (rank 1)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 1m10s
CI / openapi-codegen-check (pull_request) Successful in 2m3s
CI / backend-tests (pull_request) Successful in 10m10s
b6b9aa9929
Финмодель брала фиксированное окно продаж 30 мес независимо от рынка
(schedule_is_default всегда true). Теперь окно считается из ЛОКАЛЬНОГО темпа
поглощения, который уже вычисляется в том же /analyze, но финмодель его
игнорировала.

Корректность абсорбции (ключевое): velocity.monthly_velocity_sqm — это СУММА
поглощения ВСЕГО конкурентного набора в радиусе (м²/мес), НЕ темп одного
проекта. Поэтому per-project absorption = monthly_velocity / max(n_with_sales,1)
(темп одного типичного локального продавца) — иначе модель считала бы, что новый
проект забирает весь рыночный темп (дико оптимистично). Поле
project_absorption_sqm_per_month добавлено в VelocityResult (objective-путь);
rosreestr-fallback и вырожденные пути → None (поквартальный count без
по-проектной декомпозиции не может задавать график).

financial.py: окно = clamp(ceil(residential/velocity), MIN=6, MAX=120) при
конечной velocity>0; иначе дефолт 30. Эскроу-инвариант сохранён:
sales_end=max(sales_start+base, constr_end). Инвариант Σ cashflow == net_profit
держится (перенос выручки во времени не меняет сумму). schedule_is_default
флипается в false когда график рыночный; новое поле sales_duration_months
(реализованное окно) для UI/PDF.

Wiring: parcels.py → synthesize_parcel_financial(velocity_sqm_per_month) →
compute_financial(market_velocity_sqm_per_month). Generative §1c путь пока
передаёт None (out of scope, follow-up).

Тесты: +13 (None→дефолт+инвариант; рыночная velocity; клампы MIN/MAX; эскроу;
non-finite→fallback; rosreestr→None; инвариант по размерам окна; регресс PR-3 —
ровно одна смена знака на коротком окне). Полный бэкенд: 3414 passed, 0 failed.
ruff+mypy(strict financial.py) чисто. api-types перегенерены.

Code-review: 2× approve, 0 majors (adversarial correctness-lens подтвердил
семантику абсорбции, инвариант, не-proxy IRR, клампы, rosreestr-None).

Refs #1881

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bot-backend merged commit d2b745e240 into main 2026-06-23 22:08:09 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#1890
No description provided.