gendesign/tradein-mvp/backend/data/sql/092_sber_price_index.sql
bot-backend 19085547bc feat(tradein): СберИндекс monthly city-level price index pull (#887)
Data-layer impl of #887 (steps 1-3+5; client-visible estimator time-adjust
step 4 deferred to backtest-gated follow-up — estimator.py untouched).

- migration 092: sber_price_index table (additive, CREATE TABLE IF NOT EXISTS)
- migration 093: scrape_schedules seed (source=sber_index_pull, ON CONFLICT DO NOTHING)
- sber_index.py: base64 /api/sowa route builder + decoder + async fetch +
  idempotent upsert ON CONFLICT (city,period_month,dashboard)
- task wrapper + scheduler trigger + dispatch branch (monthly); 3 dashboards
  (deals/hedonic/asking) REALTY=2; per-series graceful errors; asking benchmark log

NOTE: REF_AREA codes EKB/MSK unresolved (sberindex TLS unreachable from CI;
643=РФ only) — TODO in SBER_REF_AREAS. 20 tests pass.

Refs #887, #794, #652
2026-05-31 15:32:53 +03:00

70 lines
3.6 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 092_sber_price_index.sql
-- Issue #887 — city-level secondary-housing price index from СберИндекс (sberindex.ru).
--
-- Context:
-- СберИндекс publishes monthly city/region-level secondary-housing price statistics
-- across three dashboards:
-- * real_estate_deals — hedonic sold-price index (руб/м²)
-- * residential_real_estate_prices — repeat-sales / asking-adjusted hedonic
-- * dinamika-tsen-obyavlenii — asking-price benchmark (объявления, руб/м²)
--
-- The pull service (app/services/sber_index.py) fetches all three series for configured
-- cities (initially РФ-643, with EKB/MSK codes TBD — see TODO in sber_index.py) via the
-- /api/sowa POST endpoint (reverse-engineered in #794 research note) and upserts rows here.
--
-- The table feeds the estimator time-adjust coefficient (step 4, #887 out-of-scope —
-- backtest-gated, separate follow-up).
--
-- Schema notes:
-- * city — REF_AREA label as returned by the API (e.g. «Россия», «Екатеринбург»).
-- * period_month — first day of the reported month (normalised from API timestamp).
-- * segment — realty type label from API (e.g. «Вторичный»).
-- * dashboard — slug used in the route (e.g. 'real_estate_deals').
-- * PRIMARY KEY (city, period_month, dashboard) — one value per city×month×series;
-- upsert on conflict keeps the freshest fetched_at.
--
-- Idempotent: CREATE TABLE IF NOT EXISTS — safe to re-apply. ADDITIVE only, no drops.
--
-- Dependencies: none (standalone table).
--
-- Deploy order: apply before deploying app/services/sber_index.py +
-- app/tasks/sber_index_pull.py + scheduler.py changes.
BEGIN;
CREATE TABLE IF NOT EXISTS sber_price_index (
city text NOT NULL,
period_month date NOT NULL,
segment text NOT NULL DEFAULT '',
dashboard text NOT NULL,
index_value_rub_m2 double precision NOT NULL,
source text NOT NULL DEFAULT 'sberindex',
fetched_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (city, period_month, dashboard)
);
COMMENT ON TABLE sber_price_index IS
'Monthly city-level secondary-housing price index from СберИндекс (sberindex.ru). '
'Three series: real_estate_deals (hedonic sold), residential_real_estate_prices '
'(repeat-sales hedonic), dinamika-tsen-obyavlenii (asking benchmark). '
'Populated by app/tasks/sber_index_pull.py via in-app scheduler (#887). '
'Estimator time-adjust wire is a separate backtest-gated follow-up (step 4, #887).';
COMMENT ON COLUMN sber_price_index.city IS
'REF_AREA label from the API response (e.g. «Россия», «Екатеринбург»).';
COMMENT ON COLUMN sber_price_index.period_month IS
'First day of the reported month (normalised from API ISO timestamp).';
COMMENT ON COLUMN sber_price_index.segment IS
'Realty type label from API (e.g. «Вторичный»). Empty string when absent.';
COMMENT ON COLUMN sber_price_index.dashboard IS
'Dataset slug used in the /dataset/v1/<dashboard> route.';
COMMENT ON COLUMN sber_price_index.index_value_rub_m2 IS
'Price index in rubles per square metre.';
COMMENT ON COLUMN sber_price_index.fetched_at IS
'Wall-clock timestamp when this row was last written/updated.';
-- Index for time-series queries (latest N months per city × dashboard)
CREATE INDEX IF NOT EXISTS idx_sber_price_index_city_dashboard_period
ON sber_price_index (city, dashboard, period_month DESC);
COMMIT;