feat(tradein): sales-vs-listings endpoint (PR K, foundation #564) #597

Merged
Light1YT merged 1 commit from feat/tradein-sales-vs-listings-endpoint into main 2026-05-27 11:55:45 +00:00

1 commit

Author SHA1 Message Date
086603cf25 feat(tradein): sales-vs-listings endpoint (PR K — foundation #564 Phase 1)
Связь ДКП Росреестра с listings (avito/cian/yandex/n1) через street
normalization + area window + listing-date window. Foundation для
sales-vs-asks comparison (discount_pct per pair).

DDL:
- data/sql/067_v_street_sales_vs_listings.sql:
  * table-valued function street_sales_vs_listings(street, area, rooms,
    window_days, area_tol) для filtered query с indexes
  * view v_street_sales_vs_listings — full crossjoin для analytical queries
    (used в reporting; для transactional path use TVF)
  * Encapsulated JOIN logic: street ILIKE + area ±15% + listing_date в окне
    [deal_date - window_days, deal_date]
  * Computed columns: days_listing_to_deal, discount_pct

API:
- GET /api/v1/sales-vs-listings?address=...&area_m2=...&rooms=...
  * Pydantic SalesVsListingsResponse с street/period/total_deals/
    deals_with_listings/linkage_rate_pct/median_discount_pct/pairs list
  * Использует extract_street_name() из existing estimator code

Schema:
- schemas/trade_in.py: SalesListingPair + SalesVsListingsResponse

Production verification:
- Migration applied на проде: CREATE FUNCTION + CREATE VIEW
- View total: 50,139 (deal, listing) pairs
- Sample case: «Бисертская» — ДКП 6.3 млн (86 м²) vs listing 8.5 млн (79 м²)
  с listing_date за 4 дня до сделки → discount_pct = −25.88%

Tests: 8/8 pass в test_sales_vs_listings.py — happy path, no matches,
LEFT JOIN behavior, median discount, defaults, response shape.
Ruff clean.

Out of scope (separate PR L+M):
- Frontend extend StreetDealsCard «Связанные ASK»
- Estimator revert NOTE 2026-05-24 (rosreestr_deals в actual_deals)

Refs #564 (Phase 1 — backend foundation).
2026-05-27 16:55:20 +05:00