Files
easy_tdx_max/docs/hotspot-rolling-design.md
Justin Gu e419f911b7 feat: 盘面洞察三栏目 — 热点滚动/大盘日历/涨停生态,移除龙头池
- 热点滚动 /hotspots:交易日×板块涨跌矩阵(行业/概念/风格FG、领涨领跌镜像、今日~30日窗口),
  每日名次徽标+连板/累计/首榜统计,统计卡可点击展开成员板块;后端 /board-mac/hotspot
  两段式数据(历史日K矩阵当日缓存 + 今日实时列,周末 pre_close 未滚动去重),
  单连接约束下后台构建+进度轮询;风格轮动 /styles 为同视图 FG 路由别名
- 大盘日历 /calendar:指数全年红绿热力图,方框大小编码成交额(年内四分位),
  悬停浮框显示收盘/涨跌幅/成交额
- 涨停生态 /limitup:本地 vipdoc 日线离线回算连板天梯/首板二板分布/炸板率/跌停;
  仅统计最后一根 bar 等于全市场最新交易日的股票(防停牌/退市/未下载陈旧文件污染),
  主板 5% ST 判定带低价护栏;名称前端经 symbol-info 懒加载补齐
- 移除龙头池栏目及 /market/core-leaders 端点(screen 模块 universe=core 保留)
- 设计文档:docs/hotspot-rolling-design.md、docs/market-insights-roadmap.md
2026-09-05 02:43:02 +08:00

294 lines
18 KiB
Markdown
Raw Permalink 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.
# 市场热点滚动 页面设计(v1 草案)
> 目标:新增「热点滚动」栏目(`/hotspots`),让用户直观看到**一段时间内市场热点如何形成、
> 谁在持续领涨、谁在领跌、热点之间如何轮动**。核心表达是「交易日 × 板块」的红涨绿跌热点矩阵,
> 辅以轮动排名曲线。配色遵循 A 股惯例:红涨绿跌(`UP_COLOR=#ef4146` / `DOWN_COLOR=#18a058`)。
>
> **P1 已实施**(后端端点 + 热点矩阵 + 统计卡 + 领涨/领跌 + 后台构建进度,单测
> `tests/unit/test_board_mac_hotspot.py`);轮动曲线等见 §8 分期。
---
## 0. 可行性结论(先回答"能不能做")
**可行,且不需要任何新数据源。** 需求的本质数据是一张
「最近 N 个交易日 × 全部板块」的**每日涨跌幅矩阵**,而这条数据路径全部是已验证的现成能力:
| 现成能力 | 位置 | 在本需求中的角色 |
|---|---|---|
| 板块指数日 K881xxx/885xxxSH 市场) | `MacClient.get_stock_kline``/bars` 已被 BoardDialog 用于板块日K`get_board_change_ranking`client.py:963)内部就是逐板块拉日K再计算 | **矩阵的原始数据**:每板块一根日K序列 → 逐日 `close/pre_close-1` |
| 全量板块列表(code/name/market | `get_board_list``/board-mac/list`) | 矩阵的行全集 + 当日实时涨跌(price/pre_close |
| 当日实时口径 | `/board-mac/overview` 同款算法(15s 缓存) | 矩阵的**今日列**(实时滚动),历史列不变 |
| ECharts Line / 主题色 | `echarts-setup.ts`(LineChart 已注册,红涨绿跌常量已定义) | 轮动排名曲线(bump chart)零新增依赖 |
| BoardDialog / session 门控轮询 | `BoardOverviewView.vue:160-202` 模式 | 点击穿透 + 盘中自动刷新复用 |
**唯一缺口**:一个把「逐板块日K → 日期×板块涨跌矩阵 → 每日排名 → 入选行集合」聚合起来的后端端点。
注意**不能**循环调用现成的 `/board-mac/change-ranking?days=1&target_date=D` 来拼 N 天——
该端点每次调用都会重新串行拉取全部板块日K(client.py:1008 起逐板块 `await`),N 天就是 N 倍成本;
正确做法是**一遍拉取、一次建满矩阵**(下文 §2)。
---
## 1. 关键约束:MAC 客户端是单连接串行
`AsyncMacClient` 全程只有一条 `AsyncTdxConnection`client.py:1250),所有命令在同一 TCP 连接上排队。
这决定了两个设计决策:
1. **成本预算**(按单请求 30–100ms 估算):
- 行业 HY ≈ 86 板块 → 全量日K一次 ≈ **49s**(仅首次,当日缓存)
- 概念 GN ≈ 300–500 板块 → ≈ **2050s**(仅首次,当日缓存)
2. **构建必须放后台任务**:若在请求线程里同步建矩阵,构建期间会**占住共享连接**,
拖死同服务器的其他所有页面请求。因此首次构建走后台任务 + 进度轮询(§2.3)。
---
## 2. 数据端点设计(后端唯一新增)
### 2.1 端点
```
GET /api/v1/board-mac/hotspot?board_type=HY|HY2|GN&days=20&mode=top&per_day=5
```
| 参数 | 默认 | 说明 |
|---|---|---|
| `board_type` | `HY` | 板块类型 |
| `days` | `20` | 窗口交易日数(1–60;1=仅今日,前端「今日」档) |
| `mode` | `top` | `top`=领涨(每日最强入选)/ `bottom`=领跌(每日最弱入选) |
| `per_day` | `5` | 每日入选名次阈值(2–10) |
| `retry` | `false` | 上次构建失败后强制重建(不带此参数时 error 状态稳定返回,轮询不会冲掉错误信息) |
### 2.2 响应
```jsonc
{
"status": "ready", // ready | buildingbuilding 时只有 progress
"progress": 1.0, // building 时的构建进度 01
"board_type": "HY", "days": 20, "mode": "top", "per_day": 5,
"generated_at": 1725400000,
"session": "live", // live=今日列为盘中实时
"dates": ["2026-08-11", "...", "2026-09-05"], // 交易日轴,最后一格=今日
"today_index": 19,
"total_boards": 86, // 参与排名的板块总数
"rows": [ // 行集合 = 窗口内「每日入选」板块的并集
{
"code": "881106", "name": "存储器",
"pct": [3.2, -1.1, null, /* …对齐 datesnull=当日无K线 */],
"rank": [1, null, 23, /* …当日全类型排名,mode=top 1=涨幅第一 */],
"days_in": 12, // 上榜天数(进入每日前 per_day 的天数)
"streak": 3, // 当前连续上榜天数(截至最后一列)
"best_rank": 1,
"sum_pct": 42.1, // 窗口累计涨跌(%
"first_date": "2026-08-11" // 窗口内首次上榜日 → 热点"形成"时点
}
]
}
```
payload 规模:60 行 × 60 列 × 2 数组 ≈ 每响应几十 KB,无压力。
### 2.3 服务端算法与缓存(两段式,对应"历史不变、今日滚动")
```
第一段:历史矩阵(当日不可变)
1. get_board_list(board_type) → 全量板块
2. 逐板块 get_stock_kline(DAILY, count=days+12, adjust=NONE)
asyncio.gather + Semaphore(8);连接本身串行,信号量只做秩序与背压)
3. 交易日轴 = 数据最全板块的最后 days 个交易日
4. pct[d] = close[d]/close[d-1]-1(窗口首日的前收由多拉的缓冲 bar 提供)
5. 每日对全类型板块排名 → rank 矩阵
6. 行集合 = (每日 mode 方向前 per_day 名);补齐 days_in/streak/sum_pct/first_date
↳ 缓存:`_hotspot_history_cache[(board_type,)] = (日历日, 满60日矩阵)`
**当日全天有效**(历史 K 线收盘后不可变),跨日首个请求重建;days 只做切片不进缓存键
第二段:今日列(滚动)
7. get_board_list 默认排序(12 页,15s TTL,与 overview 同口径)取 price/pre_close
8. 盘中:作为最后一列与实时排名合并;若今日列日期 == 历史轴最后一日(休市/周末)则不重复追加
↳ 今日列随每次请求现算(廉价),`session` 字段告知前端是否 live
构建调度:
首次请求某 board_type 且无当日缓存 → 启动 asyncio 后台任务建矩阵,
立即返回 {"status":"building","progress":...};前端 1s 轮询直至 ready。
进度 = 已完成板块数/总数。构建期间该端点不占请求线程,其他页面不受阻。
```
---
## 3. 页面总体设计
```
路由(行情分组)
/hotspots 热点滚动 → HotspotView.vue
```
新组件 3 个(`components/`):
- `HotspotMatrix.vue`**热点矩阵**(核心视图):交易日×板块 DOM 网格,红涨绿跌色阶 + 每日名次徽标
- `HotspotBump.vue`**轮动曲线**:每日名次随时间流动的 bump chartECharts Liney 轴反转)
- `HotspotStatStrip.vue` — 窗口统计卡条(领涨王/持续热点/新面孔/一日游)
视图 `HotspotView.vue`:工具行 + 统计条 + [矩阵|曲线] 切换 + 轮询与弹窗编排。
### 3.1 为什么主视图选"矩阵"而不是河流图/堆叠面积
- 用户要的是"**谁**在哪几天领涨/领跌 + **怎么轮动**"——矩阵同时给出实体(板块名)、
时间(列)、强度(色阶)、名次(徽标)四个维度,扫一眼即可读出
"红色竖带从 A 列流向 B 列"这种轮动直觉;
- ThemeRiver/堆叠面积图好看但读不出精确值与名次,且需新注册 ECharts 图表类型;
- 本仓库已有先例:板块总览的 BoardTiles 用 CSS Grid 而非 ECharts heatmap——
矩阵沿用 DOM 网格,天然支持 sticky 行头/列头、单击弹窗、行内汇总列。
---
## 4. 功能设计
### 4.1 工具行
```
[行业|概念] [近10日|近20日|近30日] [领涨|领跌] [热点矩阵|轮动曲线] ⟳60s[ON] 构建于 14:32:05
```
- **领涨/领跌**切换 = `mode` 参数,整套视图(行集合、排名、统计卡、配色重心)随之镜像;
- 首次构建/切换板块类型时显示进度条("正在构建板块日K矩阵 42/86…");
- 盘中 60s 轮询仅刷新**今日列**(历史列服务端已缓存,响应本身很快);
session 门控 + visibilitychange 暂停,复用 BoardOverviewView 模式。
### 4.2 统计卡条(HotspotStatStrip,前端由 rows 派生,零额外请求)
卡片可交互:**领涨(跌)王卡单击直达该板块 BoardDialog;其余卡片数量不为 0 时可点击,
展开成员板块 chips 面板(名称 + 窗口累计,chips 单击再开弹窗)**。
| 卡片 | 计算 | 领跌模式 |
|---|---|---|
| 窗口领涨王 | `sum_pct` 最大的行 + 累计涨幅 | 窗口领跌王(`sum_pct` 最小) |
| 持续热点 | `days_in ≥ max(3, ⌈days/4⌉)` 的板块数(持续性热点的证据;真实数据下 ÷3 会常年为 0) | 持续弱势 |
| 新面孔 | `first_date` 落在最近 5 个交易日的板块数(**热点正在形成**) | 新杀跌 |
| 一日游 | `days_in == 1` 的板块数(脉冲行情占比,越高说明轮动越快) | 同义 |
### 4.3 热点矩阵(核心,HotspotMatrix
**布局**(横向可滚动,左侧行头 sticky):
```
│ 板块 │ 08-11 08-12 08-13 … 09-04 ┃ 09-05·今 │ 上榜 连榜 累计 首榜 │
│───────────┼───────────────────────────╋──────────┼───────────────────────┤
│ 存储器 │ Ⓐ1 Ⓐ2 · … Ⓐ3 ┃ Ⓐ2 │ 12 3 +42.1% 08-11 │
│ CPO │ Ⓐ4 · Ⓐ1 … · ┃ Ⓐ1 │ 9 1 +31.5% 08-12 │
│ 房地产开发│ · · · … Ⓐ5 ┃ · │ 2 0 +6.2% 09-04 │
│ …(行集合,默认按 上榜次数↓ 排序) │
```
- **单元格着色**`pct>0` 红 / `pct<0` 绿,透明度随 `|pct|` 分 5 档
00.5 / 0.51 / 12 / 23 / >3% → 0.15/0.30/0.50/0.70/0.95),与 BoardTiles 同规;
`null`(无K线)留空。**红涨绿跌,与全站一致。**
- **上榜徽标**:当日进入前 per_day 的格子加 `①②③④⑤` 名次徽标(当日前三名加粗描边)——
一行里徽标的出现/消失/位移就是该热点的形成—持续—衰退曲线;
- **今日列**:竖线分隔 + 列头「今日·实时」徽标,盘中数值随轮询滚动;
- **行尾汇总**:上榜天数 / 当前连榜 / 窗口累计 / 首次上榜(点击列头排序;
「首榜」降序 = 新热点在前,直接回答"热点如何形成");
- **交互**hover 出 tooltip(日期/板块/涨跌幅/当日第 N 名);单击格子或行头 → `BoardDialog`
(左分时/日K、右成分股,原样复用);行内筛选「只看上榜≥2 次」+ 搜索框(概念页必备);
- 行集合上限:候选行按 `days_in` 截断至 60 行,页脚注明"仅展示窗口内上榜板块"。
### 4.4 轮动曲线(HotspotBump,第二视图)
- ECharts Linex = 交易日,y = 当日名次(**y 轴反转,1 在顶部**,范围 1per_day);
- 每个板块一条折线,仅在「上榜日」有点(未上榜断线)——线的爬升/俯冲/交叉即轮动全景;
-`days_in` 前 12 的板块入图(可读性上限),图例可点选隐藏;
- tooltip axisPointer 显示当日全部入选板块及名次;红涨绿跌仅用于线的 Current #1 标注,
线本身用分类色板(名次图的颜色语义是"板块身份"而非涨跌,避免与矩阵冲突);
- 数据零新增:直接消费矩阵响应的 `rank` 数组。
### 4.5 领跌模式的镜像语义
`mode=bottom` 时:行集合 = 每日跌幅前 per_day 的并集;`rank` 1 = 当日跌幅最大;
统计卡换弱势词汇(领跌王/持续弱势/新杀跌);矩阵主色自然偏绿(数据决定,无需特殊处理)。
用户问题里"谁又在领跌"由此获得与领涨完全对等的一等公民视图。
---
## 5. UI 布局线框
```
┌────────────────────────────────────────────────────────────────────────────────┐
│ 热点滚动 [行业|概念] [近10|20|30日] [领涨|领跌] [矩阵|曲线] ⟳60s[ON] 14:32:05 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 窗口领涨王 │ │ 持续热点 │ │ 新面孔 │ │ 一日游 │ ← HotspotStatStrip │
│ │ 存储器 │ │ 5 个 │ │ 3 个 │ │ 7 个 │ │
│ │ +42.1% │ │ 上榜≥7天 │ │ 近5日首上榜 │ │ 仅上榜1天 │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
├────────────────────────────────────────────────────────────────────────────────┤
│ 行排序[上榜次数▾] [只看上榜≥2次☐] [搜索____] 图例 ▉红=涨 ▉绿=跌 ①=当日第N名 │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ 板块 │ 08-11 08-12 … ┃ 今日 │ 上榜 连榜 累计 首榜 │ │ │
│ │ 存储器 │ ①1 ②2 … ┃ ②2 │ 12 3 +42% 08-11 │ ← 红绿矩阵 │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ (轮动曲线 tab:y 反转名次折线,1 在顶,每板块一条线) │
└────────────────────────────────────────────────────────────────────────────────┘
单击板块/格子 → BoardDialog(分时/日K + 成分股 → StockDialog
```
视觉细则:
- 配色只用既有变量:涨 `#ef4146`、跌 `#18a058`、主题背景/文字沿用全局;不做新色板;
- 徽标 `①` 用行内圆点+数字,红底白字(领涨)/ 绿底白字(领跌);
- 矩阵列头 sticky(横向滚动时日期可见)、行头 sticky(纵向滚动时板块名可见);
- 空态/错误态:顶部错误条 + 重试(复用 api.ts 统一解析);构建中进度条;休市时今日列标"收盘"。
---
## 6. 改动清单
### 后端(1 个端点 + 缓存 + 单测)
| 文件 | 改动 |
|---|---|
| `src/easy_tdx/web/routers/board_mac.py` | 新增 `GET /board-mac/hotspot`:两段式算法(§2.3)+ 后台构建任务 + 当日缓存;模块级 `_hotspot_history_cache` |
| `tests/unit/test_board_mac_hotspot.py` | 单测(mock AsyncMacClient):矩阵计算口径(首日前收)/ 交易日轴 / mode=top·bottom 行集合与排名 / 今日列合并与休市去重 / 缓存跨日失效 / building→ready 状态机 |
### 前端
| 文件 | 改动 |
|---|---|
| `web-ui/src/App.vue` | 行情分组 +1 RouterLink(热点滚动) |
| `web-ui/src/router.ts` | `/hotspots``HotspotView` |
| `web-ui/src/types.ts` | `HotspotResp / HotspotRow` |
| `web-ui/src/api.ts` | `fetchBoardHotspot(boardType, days, mode, perDay)` |
| `web-ui/src/views/HotspotView.vue` | 主视图(工具行/统计条/视图切换/轮询/弹窗编排/构建进度轮询) |
| `web-ui/src/components/HotspotMatrix.vue` | 热点矩阵(sticky 双表头、色阶、徽标、tooltip、排序筛选) |
| `web-ui/src/components/HotspotBump.vue` | 轮动曲线(ECharts Liney 反转) |
| `web-ui/src/components/HotspotStatStrip.vue` | 窗口统计卡 |
---
## 7. 边界与风险
1. **单连接串行 × 概念板块量大**:GN 首次构建 20–50s——后台任务 + 进度条 + 当日缓存兜底;
默认板块类型为行业(首屏 4–9s 可接受)。文档与页面均提示"概念首次构建较慢"。
2. **部分服务器不给 88xxxx 日K**:该板块 pct 记 null、不参与当日排名;行头仍展示(有历史的日子照常着色)。
3. **新板块/长假期**:窗口首日的前收取自缓冲 bar(多拉 12 根);缓冲不足时首日 pct 置 null。
4. **休市/周末**:历史轴止于最近交易日;今日列与历史末列去重(§2.3 第 8 步),不出现重复列。
5. **盘中口径**:今日列 = price/pre_close-1(与 overview 同口径,规避 Issue #53 的 CHANGE_PCT 恒 0);
历史列 = 日K close 比值。两段口径在收盘后自然收敛一致。
6. **缓存正确性**:历史矩阵按日历日失效(跨日首个请求重建),不存在隔夜脏数据;
`days` 只切片不进缓存键,切窗口零成本。
## 8. 分期计划
| 期 | 内容 | 预估 |
|---|---|---|
| **P1MVP** | `/board-mac/hotspot` 端点 + 缓存 + 后台构建 + 单测;热点矩阵(领涨/领跌、10/20/30日、行业/概念)+ 统计卡 + BoardDialog 复用 + 构建进度条 | 22.5 天 |
| **P2** | 轮动曲线 bump chart、磁盘持久化历史矩阵(重启不重拉)、只看≥2 次/搜索/localStorage 偏好、HY2 切换 | 1 天 |
| **P3(远期)** | 概念→个股联动(点热点格子看当日成分股贡献)、热点轮动 AI 解读、自定义窗口与多窗口对比 | 另立项 |
## 9. 验收清单(P1
- [ ] 左侧导航出现「热点滚动」,路由/高亮正常
- [ ] 行业页首次进入出现构建进度条,完成后矩阵展示完整 20 列(列数=交易日数)× 上榜板块并集
- [ ] 矩阵配色红涨绿跌且色阶随幅度增强;每日前三名徽标可辨识;hover tooltip 数值与当日排名正确
- [ ] 领涨/领跌切换后行集合、排名语义、统计卡文案整体镜像
- [ ] 盘中今日列 60s 滚动且休市去重;历史列当日不重复拉取(网络面板仅 1 个 hotspot 轮询请求)
- [ ] 单击板块 → BoardDialog 全链路(分时/日K/成分股)
- [ ] 概念页首次构建期间其他页面(行情看板等)请求不受阻塞
- [ ] 断开 MAC 服务器时错误条 + 重试,页面不白屏