From 2f7d749596cc917676cfdc80b58716daff225a01 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Tue, 21 Apr 2026 01:37:02 +0700 Subject: [PATCH] docs(api): add market index & ticker contract for trading-floor UI (TEC-3043) Co-Authored-By: Paperclip --- docs/api/market-index-ticker-contract.md | 355 +++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 docs/api/market-index-ticker-contract.md diff --git a/docs/api/market-index-ticker-contract.md b/docs/api/market-index-ticker-contract.md new file mode 100644 index 0000000..dafe979 --- /dev/null +++ b/docs/api/market-index-ticker-contract.md @@ -0,0 +1,355 @@ +# 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).*