# Contract API – Chỉ số Thị trường & Ticker (Goodgo Platform AI) > Liên quan: [TEC-3036](/TEC/issues/TEC-3036), [TEC-3042](/TEC/issues/TEC-3042), [TEC-3043](/TEC/issues/TEC-3043). > Trạng thái: Draft v1 (chờ CTO + BE TechLead + FE TechLead duyệt). > Chủ trì: API Architect. Phối hợp: Database Architect, Backend TechLead, Frontend TechLead. Tài liệu này định nghĩa contract cho các endpoint mới phục vụ UI phong cách "sàn giao dịch" (trading-floor) của Goodgo Platform AI. Frontend **chỉ gọi API thực**, cấm mock. Các endpoint mới được thêm **không breaking** endpoint hiện có (`/analytics/*`, `/listings/*`, `/search/*`). ## 1. Nguyên tắc thiết kế - **Base path**: `/api/v1/` (prefix chung toàn platform, bearer JWT khi cần). - **Auth**: Public cho các chỉ số tổng hợp (cache-able). JWT cho các API cá nhân hoá (watchlist ticker). - **Caching**: `Cache-Control: public, max-age=30, stale-while-revalidate=60` cho chỉ số ticker. `max-age=300` cho heatmap/price-trends. - **Đơn vị**: - Tiền tệ: `VND` (integer, không thập phân). Field nào dùng VND/m² phải ghi rõ `unit: "VND_PER_SQM"`. - Thời gian: ISO-8601 UTC (`2026-04-21T03:15:00Z`). Client tự chuyển sang Asia/Ho_Chi_Minh. - % change: `number` (phần trăm, ví dụ `2.35` = `+2.35%`). `delta` là giá trị tuyệt đối cùng đơn vị với `value`. - **Response envelope**: thống nhất với module analytics hiện có: ```json { "data": { ... }, "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 30, "source": "aggregation_v1", "baselinePeriod": "PT24H" } } ``` - **Lỗi**: tuân theo `docs/api-error-codes.md` (ErrorResponse `{ code, message, details }`). - **Pagination** (nếu có): `page`, `pageSize` (max 100), response có `meta.pagination`. - **i18n**: label/description phục vụ UI trả về tiếng Việt; field system (`code`, `slug`) dùng tiếng Anh. ## 2. Phân loại endpoint | Nhóm | Mục đích UI | Cache | Auth | |------|-------------|-------|------| | Market Index | Header chỉ số GGI/khu vực | 30s | Public | | Price Trends | Biểu đồ candlestick/line | 300s | Public | | District Volume | Bar chart volume theo quận | 120s | Public | | Listing Ticker | Dải chạy real-time tin mới/giá mới | 15s + SSE | Public + (JWT cho watchlist) | | Top Movers | Bảng top tăng/giảm | 60s | Public | | Heatmap Summary | Overlay heatmap bản đồ | 300s | Public | ## 3. Đặc tả chi tiết ### 3.1. `GET /api/v1/analytics/market-index` Trả về chỉ số thị trường tổng hợp (Goodgo Market Index – GGI) theo phân loại. **Query params** | Name | Type | Required | Default | Mô tả | |------|------|----------|---------|-------| | `scope` | enum(`national`, `city`, `district`) | no | `city` | Phạm vi tính chỉ số | | `cityCode` | string | when scope≠national | `HCM` | Mã thành phố (HCM, HN, DN...) | | `districtCode` | string | when scope=district | – | Mã quận/huyện | | `propertyType` | enum(`all`, `apartment`, `house`, `land`, `commercial`) | no | `all` | Loại hình | | `baseline` | enum(`PT24H`, `P7D`, `P30D`, `P1Y`) | no | `PT24H` | Khoảng so sánh delta | **Response 200** ```json { "data": { "indexCode": "GGI-HCM-ALL", "indexLabel": "Chỉ số Goodgo TP.HCM - Tất cả", "value": 1284.35, "unit": "INDEX_POINT", "baseValue": 1000, "baselinePeriod": "PT24H", "delta": 29.62, "changePercent": 2.36, "direction": "up", "samples": 12845, "breakdown": [ { "code": "apartment", "label": "Chung cư", "value": 1311.2, "changePercent": 3.1 }, { "code": "house", "label": "Nhà phố", "value": 1198.4, "changePercent": 1.0 }, { "code": "land", "label": "Đất nền", "value": 1342.7, "changePercent": 4.2 } ], "timestamp": "2026-04-21T03:15:00Z" }, "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 30, "source": "aggregation_v1", "baselinePeriod": "PT24H" } } ``` ### 3.2. `GET /api/v1/analytics/price-trends` Chuỗi giá trung bình theo thời gian (dạng nến OHLC hoặc line tuỳ `granularity`). **Query params** | Name | Type | Required | Default | Mô tả | |------|------|----------|---------|-------| | `scope` | enum(`city`, `district`, `ward`) | yes | – | | | `scopeCode` | string | yes | – | Mã tương ứng | | `propertyType` | enum(...) | no | `all` | | | `granularity` | enum(`1h`, `1d`, `1w`, `1m`) | no | `1d` | Bucket thời gian | | `from` | ISO-8601 | no | now-30d | | | `to` | ISO-8601 | no | now | | | `series` | enum(`avg`, `ohlc`) | no | `ohlc` | | **Response 200** ```json { "data": { "scope": "district", "scopeCode": "HCM-Q1", "unit": "VND_PER_SQM", "granularity": "1d", "points": [ { "timestamp": "2026-04-20T00:00:00Z", "open": 95200000, "high": 96800000, "low": 94900000, "close": 96100000, "avg": 95600000, "volume": 184, "changePercent": 0.95 } ] }, "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 300, "source": "listing_snapshot_v1", "baselinePeriod": "P30D" } } ``` ### 3.3. `GET /api/v1/analytics/district-volume` Khối lượng tin đăng/giao dịch theo quận cho bar chart. **Query params** | Name | Type | Required | Default | |------|------|----------|---------| | `cityCode` | string | no | `HCM` | | `metric` | enum(`listings_new`, `listings_active`, `inquiries`, `transactions`) | no | `listings_new` | | `window` | enum(`PT1H`, `PT24H`, `P7D`, `P30D`) | no | `PT24H` | | `limit` | int (1..50) | no | 24 | | `sort` | enum(`value_desc`, `value_asc`, `change_desc`) | no | `value_desc` | **Response 200** ```json { "data": { "cityCode": "HCM", "metric": "listings_new", "window": "PT24H", "items": [ { "districtCode": "HCM-Q7", "districtName": "Quận 7", "value": 312, "previousValue": 284, "delta": 28, "changePercent": 9.86, "rank": 1 } ] }, "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 120, "source": "listing_stream_v1", "baselinePeriod": "PT24H" } } ``` ### 3.4. `GET /api/v1/listings/ticker` Danh sách tick cho dải chạy real-time. Có cả poll (REST) và stream (SSE/WebSocket) version. **Query params** | Name | Type | Required | Default | Mô tả | |------|------|----------|---------|-------| | `channel` | enum(`new_listing`, `price_change`, `hot`, `featured`, `watchlist`) | no | `new_listing` | `watchlist` cần JWT | | `scope` | enum(`national`, `city`, `district`) | no | `city` | | | `scopeCode` | string | cond | `HCM` | | | `propertyType` | enum(...) | no | `all` | | | `limit` | int(1..50) | no | 20 | | | `sinceTickId` | string | no | – | Lấy tick mới hơn ID này (polling incremental) | **Response 200** ```json { "data": { "channel": "new_listing", "ticks": [ { "tickId": "tick_01J...", "listingId": "lst_01J...", "symbol": "HCM-Q7-APT-0934", "title": "Căn hộ 2PN view sông Phú Mỹ Hưng", "propertyType": "apartment", "districtCode": "HCM-Q7", "districtName": "Quận 7", "priceVnd": 5200000000, "pricePerSqmVnd": 68400000, "areaSqm": 76, "changePercent": -1.45, "direction": "down", "event": "price_change", "timestamp": "2026-04-21T03:14:57Z", "url": "/listings/lst_01J..." } ], "nextSinceTickId": "tick_01J...ZZ" }, "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 15, "source": "ticker_stream_v1" } } ``` #### 3.4.1. `GET /api/v1/listings/ticker/stream` (SSE) - Content-Type: `text/event-stream`. - Event types: `tick`, `heartbeat`, `reset`. - Payload `data:` giống một phần tử `ticks[]` phía trên. - Query params như REST, thêm `heartbeatSeconds` (default 15). ### 3.5. `GET /api/v1/analytics/top-movers` Top tăng/giảm theo khu vực/loại hình. **Query params** | Name | Type | Required | Default | |------|------|----------|---------| | `direction` | enum(`gainers`, `losers`, `both`) | no | `both` | | `scope` | enum(`city`, `district`, `ward`, `project`) | no | `district` | | `cityCode` | string | no | `HCM` | | `metric` | enum(`price_per_sqm`, `index_value`, `volume`) | no | `price_per_sqm` | | `window` | enum(`PT24H`, `P7D`, `P30D`) | no | `P7D` | | `limit` | int(1..50) | no | 10 | **Response 200** ```json { "data": { "window": "P7D", "metric": "price_per_sqm", "gainers": [ { "code": "HCM-Q9", "label": "TP Thủ Đức (Q9 cũ)", "value": 72400000, "previousValue": 68900000, "delta": 3500000, "changePercent": 5.08, "rank": 1, "sampleSize": 412 } ], "losers": [ /* same schema */ ] }, "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 60, "source": "aggregation_v1", "baselinePeriod": "P7D" } } ``` ### 3.6. `GET /api/v1/analytics/heatmap-summary` Dữ liệu tóm tắt heatmap cho overlay bản đồ. **Query params** | Name | Type | Required | Default | Mô tả | |------|------|----------|---------|-------| | `cityCode` | string | no | `HCM` | | | `metric` | enum(`price_per_sqm`, `volume`, `demand_index`, `change_percent`) | no | `price_per_sqm` | | | `resolution` | enum(`district`, `ward`, `h3_r7`, `h3_r8`) | no | `district` | H3 cell khi cần granularity cao | | `window` | enum(`PT24H`, `P7D`, `P30D`) | no | `P30D` | | | `bbox` | string `minLon,minLat,maxLon,maxLat` | no | – | Bounding box bản đồ | **Response 200** ```json { "data": { "metric": "price_per_sqm", "unit": "VND_PER_SQM", "resolution": "district", "legend": { "buckets": [ { "min": 0, "max": 40000000, "color": "#1a9850", "label": "Thấp" }, { "min": 40000000, "max": 80000000, "color": "#fee08b", "label": "Trung bình" }, { "min": 80000000, "max": 140000000, "color": "#d73027", "label": "Cao" } ] }, "cells": [ { "code": "HCM-Q1", "label": "Quận 1", "centroid": { "lat": 10.776, "lon": 106.700 }, "value": 128400000, "changePercent": 1.8, "sampleSize": 532, "polygonRef": "geo/districts/HCM-Q1.geojson" } ] }, "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 300, "source": "aggregation_v1", "baselinePeriod": "P30D" } } ``` ## 4. Nguồn dữ liệu & aggregation (phối hợp Database Architect) | Endpoint | Nguồn | Aggregation chính | Refresh | |----------|-------|-------------------|---------| | `market-index` | `Listing`, `Transaction`, `PriceSnapshot` | Weighted avg price/m² chuẩn hoá về base 2025-01-01 = 1000 | 1 phút (materialized view) | | `price-trends` | `PriceSnapshot` (daily), `Listing.price_history` | OHLC theo bucket | 5 phút | | `district-volume` | `Listing.createdAt`, `Inquiry`, `Transaction` | COUNT + window function | 1 phút | | `listings/ticker` | Redis stream `listings:events` + DB fallback | Event bus (create/price_change/feature) | real-time | | `top-movers` | `PriceSnapshot` | % change theo bucket + ranking | 5 phút | | `heatmap-summary` | `Listing` + `District`/`H3Cell` precomputed | PostGIS aggregate theo polygon/H3 | 15 phút | Database Architect xác nhận: (i) thêm materialized views `mv_market_index_*`, `mv_price_snapshot_*`; (ii) Redis stream `listings:events` dùng cho ticker/SSE; (iii) bảng `H3Cell` cho heatmap r7/r8. ## 5. Phân việc cho Backend TechLead ([TEC-3042](/TEC/issues/TEC-3042)) | Endpoint | Đề xuất cấp độ BE | Ghi chú | |----------|-------------------|--------| | `market-index` | Senior | Cần design materialized view + cache invalidation | | `price-trends` | Middle | Query trên view sẵn có | | `district-volume` | Middle | Window function + Redis cache | | `listings/ticker` (REST+SSE) | Senior | Pub/sub, backpressure, auth watchlist | | `top-movers` | Middle | Dựa trên view price-trends | | `heatmap-summary` | Senior | PostGIS + H3, geojson ref | | DTO/OpenAPI sync | Junior | Viết DTO, Swagger decorators, update `docs/api-endpoints.md` | ## 6. OpenAPI & Swagger - DTO mới ở `apps/api/src/modules/analytics/presentation/dto/` và `apps/api/src/modules/listings/presentation/dto/` (ticker). - Controller: - `analytics.controller.ts`: thêm `@Get('market-index')`, `@Get('price-trends')`, `@Get('district-volume')`, `@Get('top-movers')`, `@Get('heatmap-summary')`. - `listings.controller.ts`: thêm `@Get('ticker')`, `@Get('ticker/stream')`. - Tag Swagger: `Analytics - Market`, `Listings - Ticker`. - Cập nhật `docs/api-endpoints.md` (phần Analytics & Listings) sau khi merge. ## 7. Versioning & rollout - Tất cả endpoint mới nằm dưới `/api/v1/` hiện tại — **không bump major**. - Feature flag (nếu cần) dùng `FEATURE_MARKET_TICKER` cho ticker SSE. - SLO: p95 < 250ms cho REST; SSE duy trì kết nối ≥ 5 phút, heartbeat 15s. ## 8. Checklist duyệt - [ ] CTO phê duyệt phạm vi & SLO. - [ ] BE TechLead xác nhận phân rã task & timeline. - [ ] FE TechLead xác nhận response schema đủ cho UI sàn giao dịch. - [ ] Database Architect ký nguồn dữ liệu & materialized views. - [ ] DTO/OpenAPI/Swagger & `docs/api-endpoints.md` cập nhật. --- *Revision 1 — 2026-04-21 — API Architect (TEC-3043).*