# 市场热点滚动 页面设计(v1 草案) > 目标:新增「热点滚动」栏目(`/hotspots`),让用户直观看到**一段时间内市场热点如何形成、 > 谁在持续领涨、谁在领跌、热点之间如何轮动**。核心表达是「交易日 × 板块」的红涨绿跌热点矩阵, > 辅以轮动排名曲线。配色遵循 A 股惯例:红涨绿跌(`UP_COLOR=#ef4146` / `DOWN_COLOR=#18a058`)。 > > **P1 已实施**(后端端点 + 热点矩阵 + 统计卡 + 领涨/领跌 + 后台构建进度,单测 > `tests/unit/test_board_mac_hotspot.py`);轮动曲线等见 §8 分期。 --- ## 0. 可行性结论(先回答"能不能做") **可行,且不需要任何新数据源。** 需求的本质数据是一张 「最近 N 个交易日 × 全部板块」的**每日涨跌幅矩阵**,而这条数据路径全部是已验证的现成能力: | 现成能力 | 位置 | 在本需求中的角色 | |---|---|---| | 板块指数日 K(881xxx/885xxx,SH 市场) | `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一次 ≈ **4–9s**(仅首次,当日缓存) - 概念 GN ≈ 300–500 板块 → ≈ **20–50s**(仅首次,当日缓存) 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 | building(building 时只有 progress) "progress": 1.0, // building 时的构建进度 0–1 "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, /* …对齐 dates,null=当日无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 默认排序(1–2 页,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 chart(ECharts Line,y 轴反转) - `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 档 (0–0.5 / 0.5–1 / 1–2 / 2–3 / >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 Line,x = 交易日,y = 当日名次(**y 轴反转,1 在顶部**,范围 1–per_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 Line,y 反转) | | `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. 分期计划 | 期 | 内容 | 预估 | |---|---|---| | **P1(MVP)** | `/board-mac/hotspot` 端点 + 缓存 + 后台构建 + 单测;热点矩阵(领涨/领跌、10/20/30日、行业/概念)+ 统计卡 + BoardDialog 复用 + 构建进度条 | 2–2.5 天 | | **P2** | 轮动曲线 bump chart、磁盘持久化历史矩阵(重启不重拉)、只看≥2 次/搜索/localStorage 偏好、HY2 切换 | 1 天 | | **P3(远期)** | 概念→个股联动(点热点格子看当日成分股贡献)、热点轮动 AI 解读、自定义窗口与多窗口对比 | 另立项 | ## 9. 验收清单(P1) - [ ] 左侧导航出现「热点滚动」,路由/高亮正常 - [ ] 行业页首次进入出现构建进度条,完成后矩阵展示完整 20 列(列数=交易日数)× 上榜板块并集 - [ ] 矩阵配色红涨绿跌且色阶随幅度增强;每日前三名徽标可辨识;hover tooltip 数值与当日排名正确 - [ ] 领涨/领跌切换后行集合、排名语义、统计卡文案整体镜像 - [ ] 盘中今日列 60s 滚动且休市去重;历史列当日不重复拉取(网络面板仅 1 个 hotspot 轮询请求) - [ ] 单击板块 → BoardDialog 全链路(分时/日K/成分股) - [ ] 概念页首次构建期间其他页面(行情看板等)请求不受阻塞 - [ ] 断开 MAC 服务器时错误条 + 重试,页面不白屏