chore: bootstrap housing research platform
This commit is contained in:
134
apps/api/migrations/202606180001_init.sql
Normal file
134
apps/api/migrations/202606180001_init.sql
Normal file
@@ -0,0 +1,134 @@
|
||||
CREATE SCHEMA IF NOT EXISTS raw;
|
||||
CREATE SCHEMA IF NOT EXISTS bronze;
|
||||
CREATE SCHEMA IF NOT EXISTS silver;
|
||||
CREATE SCHEMA IF NOT EXISTS gold;
|
||||
CREATE SCHEMA IF NOT EXISTS app;
|
||||
CREATE SCHEMA IF NOT EXISTS audit;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit.data_sources (
|
||||
source_id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
source_type TEXT NOT NULL,
|
||||
url TEXT,
|
||||
cadence TEXT,
|
||||
reliability TEXT NOT NULL DEFAULT 'unknown',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit.ingestion_runs (
|
||||
run_id BIGSERIAL PRIMARY KEY,
|
||||
source_id BIGINT REFERENCES audit.data_sources(source_id),
|
||||
status TEXT NOT NULL,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
finished_at TIMESTAMPTZ,
|
||||
raw_uri TEXT,
|
||||
row_count INTEGER,
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS silver.areas (
|
||||
area_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
district TEXT NOT NULL,
|
||||
segment TEXT NOT NULL,
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS silver.area_monthly_metrics (
|
||||
area_id TEXT NOT NULL REFERENCES silver.areas(area_id),
|
||||
month TEXT NOT NULL CHECK (month ~ '^[0-9]{4}-[0-9]{2}$'),
|
||||
transaction_count INTEGER NOT NULL CHECK (transaction_count >= 0),
|
||||
transaction_price_psm DOUBLE PRECISION NOT NULL CHECK (transaction_price_psm >= 0),
|
||||
listing_count INTEGER NOT NULL CHECK (listing_count >= 0),
|
||||
listing_price_psm DOUBLE PRECISION NOT NULL CHECK (listing_price_psm >= 0),
|
||||
median_days_on_market DOUBLE PRECISION NOT NULL CHECK (median_days_on_market >= 0),
|
||||
rent_price_psm DOUBLE PRECISION NOT NULL CHECK (rent_price_psm >= 0),
|
||||
new_supply_units INTEGER NOT NULL CHECK (new_supply_units >= 0),
|
||||
land_residential_gfa_sqm DOUBLE PRECISION NOT NULL CHECK (land_residential_gfa_sqm >= 0),
|
||||
mortgage_rate_pct DOUBLE PRECISION NOT NULL CHECK (mortgage_rate_pct >= 0),
|
||||
policy_signal INTEGER NOT NULL DEFAULT 0 CHECK (policy_signal BETWEEN -2 AND 2),
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (area_id, month)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS silver.neighborhoods (
|
||||
neighborhood_id TEXT PRIMARY KEY,
|
||||
area_id TEXT NOT NULL REFERENCES silver.areas(area_id),
|
||||
name TEXT NOT NULL,
|
||||
built_year INTEGER,
|
||||
property_type TEXT NOT NULL,
|
||||
metro_distance_m INTEGER,
|
||||
school_quality TEXT,
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS silver.neighborhood_monthly_metrics (
|
||||
neighborhood_id TEXT NOT NULL REFERENCES silver.neighborhoods(neighborhood_id),
|
||||
month TEXT NOT NULL CHECK (month ~ '^[0-9]{4}-[0-9]{2}$'),
|
||||
transaction_count INTEGER NOT NULL CHECK (transaction_count >= 0),
|
||||
transaction_price_psm DOUBLE PRECISION NOT NULL CHECK (transaction_price_psm >= 0),
|
||||
listing_count INTEGER NOT NULL CHECK (listing_count >= 0),
|
||||
listing_price_psm DOUBLE PRECISION NOT NULL CHECK (listing_price_psm >= 0),
|
||||
rent_price_psm DOUBLE PRECISION NOT NULL CHECK (rent_price_psm >= 0),
|
||||
median_days_on_market DOUBLE PRECISION NOT NULL CHECK (median_days_on_market >= 0),
|
||||
available_units INTEGER NOT NULL CHECK (available_units >= 0),
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (neighborhood_id, month)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gold.area_scores (
|
||||
area_id TEXT NOT NULL REFERENCES silver.areas(area_id),
|
||||
name TEXT NOT NULL,
|
||||
district TEXT NOT NULL,
|
||||
segment TEXT NOT NULL,
|
||||
month TEXT NOT NULL CHECK (month ~ '^[0-9]{4}-[0-9]{2}$'),
|
||||
investment_score DOUBLE PRECISION NOT NULL CHECK (investment_score BETWEEN 0 AND 100),
|
||||
recommendation TEXT NOT NULL,
|
||||
liquidity_score DOUBLE PRECISION NOT NULL CHECK (liquidity_score BETWEEN 0 AND 100),
|
||||
momentum_score DOUBLE PRECISION NOT NULL CHECK (momentum_score BETWEEN 0 AND 100),
|
||||
rent_support_score DOUBLE PRECISION NOT NULL CHECK (rent_support_score BETWEEN 0 AND 100),
|
||||
safety_margin_score DOUBLE PRECISION NOT NULL CHECK (safety_margin_score BETWEEN 0 AND 100),
|
||||
credit_support_score DOUBLE PRECISION NOT NULL CHECK (credit_support_score BETWEEN 0 AND 100),
|
||||
supply_risk_score DOUBLE PRECISION NOT NULL CHECK (supply_risk_score BETWEEN 0 AND 100),
|
||||
valuation_pressure_score DOUBLE PRECISION NOT NULL CHECK (valuation_pressure_score BETWEEN 0 AND 100),
|
||||
transaction_count INTEGER NOT NULL CHECK (transaction_count >= 0),
|
||||
transaction_price_psm DOUBLE PRECISION NOT NULL CHECK (transaction_price_psm >= 0),
|
||||
listing_count INTEGER NOT NULL CHECK (listing_count >= 0),
|
||||
listing_price_psm DOUBLE PRECISION NOT NULL CHECK (listing_price_psm >= 0),
|
||||
rent_price_psm DOUBLE PRECISION NOT NULL CHECK (rent_price_psm >= 0),
|
||||
median_days_on_market DOUBLE PRECISION NOT NULL CHECK (median_days_on_market >= 0),
|
||||
annual_rent_yield_pct DOUBLE PRECISION NOT NULL CHECK (annual_rent_yield_pct >= 0),
|
||||
listing_pressure_ratio DOUBLE PRECISION NOT NULL CHECK (listing_pressure_ratio >= 0),
|
||||
discount_pct DOUBLE PRECISION NOT NULL,
|
||||
price_momentum_pct DOUBLE PRECISION NOT NULL,
|
||||
volume_momentum_pct DOUBLE PRECISION NOT NULL,
|
||||
model_version TEXT NOT NULL DEFAULT 'sample-v1',
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (area_id, month)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app.watchlist_items (
|
||||
watchlist_item_id BIGSERIAL PRIMARY KEY,
|
||||
neighborhood_id TEXT REFERENCES silver.neighborhoods(neighborhood_id),
|
||||
area_id TEXT REFERENCES silver.areas(area_id),
|
||||
target_price_psm DOUBLE PRECISION CHECK (target_price_psm >= 0),
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (neighborhood_id IS NOT NULL OR area_id IS NOT NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_area_monthly_metrics_month ON silver.area_monthly_metrics(month);
|
||||
CREATE INDEX IF NOT EXISTS idx_neighborhoods_area ON silver.neighborhoods(area_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_gold_area_scores_month ON gold.area_scores(month, investment_score DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_status ON app.watchlist_items(status);
|
||||
115
apps/api/migrations/202606180002_seed_sample.sql
Normal file
115
apps/api/migrations/202606180002_seed_sample.sql
Normal file
@@ -0,0 +1,115 @@
|
||||
INSERT INTO audit.data_sources (name, source_type, url, cadence, reliability, notes)
|
||||
VALUES
|
||||
('sample', 'manual', NULL, 'ad hoc', 'demo', 'Demonstration data. Not real market data.')
|
||||
ON CONFLICT (name) DO UPDATE
|
||||
SET source_type = EXCLUDED.source_type,
|
||||
cadence = EXCLUDED.cadence,
|
||||
reliability = EXCLUDED.reliability,
|
||||
notes = EXCLUDED.notes;
|
||||
|
||||
INSERT INTO silver.areas (area_id, name, district, segment, notes)
|
||||
VALUES
|
||||
('qiantan', '前滩', '浦东新区', '核心改善', '产业和公共配套强,供应节奏需要持续跟踪'),
|
||||
('xujiahui', '徐家汇', '徐汇区', '核心成熟', '成熟商圈和教育医疗资源强,价格弹性通常较小'),
|
||||
('danning', '大宁', '静安区', '内中环改善', '居住氛围和商业配套较均衡'),
|
||||
('zhangjiang', '张江', '浦东新区', '产业成长', '产业人口支撑强,产品分化明显'),
|
||||
('hongqiao', '大虹桥', '闵行区', '规划成长', '受商务区兑现和供应影响较大')
|
||||
ON CONFLICT (area_id) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
district = EXCLUDED.district,
|
||||
segment = EXCLUDED.segment,
|
||||
notes = EXCLUDED.notes,
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO silver.area_monthly_metrics (
|
||||
area_id,
|
||||
month,
|
||||
transaction_count,
|
||||
transaction_price_psm,
|
||||
listing_count,
|
||||
listing_price_psm,
|
||||
median_days_on_market,
|
||||
rent_price_psm,
|
||||
new_supply_units,
|
||||
land_residential_gfa_sqm,
|
||||
mortgage_rate_pct,
|
||||
policy_signal,
|
||||
source
|
||||
)
|
||||
VALUES
|
||||
('qiantan', '2026-05', 93, 122500, 380, 126500, 53, 192, 60, 0, 3.35, 1, 'sample'),
|
||||
('xujiahui', '2026-05', 67, 127500, 348, 133000, 66, 207, 20, 0, 3.35, 1, 'sample'),
|
||||
('danning', '2026-05', 103, 98200, 488, 102200, 69, 158, 110, 0, 3.35, 1, 'sample'),
|
||||
('zhangjiang', '2026-05', 126, 84600, 570, 89000, 60, 139, 180, 0, 3.35, 1, 'sample'),
|
||||
('hongqiao', '2026-05', 82, 75800, 735, 82000, 96, 119, 360, 0, 3.35, 0, 'sample')
|
||||
ON CONFLICT (area_id, month) DO UPDATE
|
||||
SET transaction_count = EXCLUDED.transaction_count,
|
||||
transaction_price_psm = EXCLUDED.transaction_price_psm,
|
||||
listing_count = EXCLUDED.listing_count,
|
||||
listing_price_psm = EXCLUDED.listing_price_psm,
|
||||
median_days_on_market = EXCLUDED.median_days_on_market,
|
||||
rent_price_psm = EXCLUDED.rent_price_psm,
|
||||
new_supply_units = EXCLUDED.new_supply_units,
|
||||
land_residential_gfa_sqm = EXCLUDED.land_residential_gfa_sqm,
|
||||
mortgage_rate_pct = EXCLUDED.mortgage_rate_pct,
|
||||
policy_signal = EXCLUDED.policy_signal,
|
||||
source = EXCLUDED.source,
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO gold.area_scores (
|
||||
area_id,
|
||||
name,
|
||||
district,
|
||||
segment,
|
||||
month,
|
||||
investment_score,
|
||||
recommendation,
|
||||
liquidity_score,
|
||||
momentum_score,
|
||||
rent_support_score,
|
||||
safety_margin_score,
|
||||
credit_support_score,
|
||||
supply_risk_score,
|
||||
valuation_pressure_score,
|
||||
transaction_count,
|
||||
transaction_price_psm,
|
||||
listing_count,
|
||||
listing_price_psm,
|
||||
rent_price_psm,
|
||||
median_days_on_market,
|
||||
annual_rent_yield_pct,
|
||||
listing_pressure_ratio,
|
||||
discount_pct,
|
||||
price_momentum_pct,
|
||||
volume_momentum_pct,
|
||||
model_version
|
||||
)
|
||||
VALUES
|
||||
('zhangjiang', '张江', '浦东新区', '产业成长', '2026-05', 72.0, '观察池', 93.0, 60.5, 69.4, 41.6, 66.7, 21.8, 62.9, 126, 84600, 570, 89000, 139, 60, 1.97, 4.52, 4.94, 1.32, 6.78, 'sample-v1'),
|
||||
('qiantan', '前滩', '浦东新区', '核心改善', '2026-05', 66.4, '观察池', 79.2, 61.6, 62.9, 25.4, 66.7, 8.1, 78.6, 93, 122500, 380, 126500, 192, 53, 1.88, 4.09, 3.16, 1.24, 8.14, 'sample-v1'),
|
||||
('danning', '大宁', '静安区', '内中环改善', '2026-05', 65.6, '观察池', 76.9, 57.9, 66.5, 32.4, 66.7, 20.0, 72.8, 103, 98200, 488, 102200, 158, 69, 1.93, 4.74, 3.91, 1.24, 5.10, 'sample-v1'),
|
||||
('xujiahui', '徐家汇', '徐汇区', '核心成熟', '2026-05', 61.9, '中性观望', 59.8, 57.1, 67.7, 34.5, 66.7, 18.5, 70.2, 67, 127500, 348, 133000, 207, 66, 1.95, 5.19, 4.14, 0.55, 8.06, 'sample-v1'),
|
||||
('hongqiao', '大虹桥', '闵行区', '规划成长', '2026-05', 55.2, '中性观望', 53.5, 50.9, 63.1, 73.4, 59.2, 99.6, 24.6, 82, 75800, 735, 82000, 119, 96, 1.88, 8.96, 7.56, 0.40, 3.80, 'sample-v1')
|
||||
ON CONFLICT (area_id, month) DO UPDATE
|
||||
SET investment_score = EXCLUDED.investment_score,
|
||||
recommendation = EXCLUDED.recommendation,
|
||||
liquidity_score = EXCLUDED.liquidity_score,
|
||||
momentum_score = EXCLUDED.momentum_score,
|
||||
rent_support_score = EXCLUDED.rent_support_score,
|
||||
safety_margin_score = EXCLUDED.safety_margin_score,
|
||||
credit_support_score = EXCLUDED.credit_support_score,
|
||||
supply_risk_score = EXCLUDED.supply_risk_score,
|
||||
valuation_pressure_score = EXCLUDED.valuation_pressure_score,
|
||||
transaction_count = EXCLUDED.transaction_count,
|
||||
transaction_price_psm = EXCLUDED.transaction_price_psm,
|
||||
listing_count = EXCLUDED.listing_count,
|
||||
listing_price_psm = EXCLUDED.listing_price_psm,
|
||||
rent_price_psm = EXCLUDED.rent_price_psm,
|
||||
median_days_on_market = EXCLUDED.median_days_on_market,
|
||||
annual_rent_yield_pct = EXCLUDED.annual_rent_yield_pct,
|
||||
listing_pressure_ratio = EXCLUDED.listing_pressure_ratio,
|
||||
discount_pct = EXCLUDED.discount_pct,
|
||||
price_momentum_pct = EXCLUDED.price_momentum_pct,
|
||||
volume_momentum_pct = EXCLUDED.volume_momentum_pct,
|
||||
model_version = EXCLUDED.model_version,
|
||||
computed_at = now();
|
||||
Reference in New Issue
Block a user