docs(api): add market index & ticker contract for trading-floor UI (TEC-3043)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
355
docs/api/market-index-ticker-contract.md
Normal file
355
docs/api/market-index-ticker-contract.md
Normal file
@@ -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).*
|
||||||
Reference in New Issue
Block a user