- 热点滚动 /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
18 KiB
市场热点滚动 页面设计(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 连接上排队。
这决定了两个设计决策:
- 成本预算(按单请求 30–100ms 估算):
- 行业 HY ≈ 86 板块 → 全量日K一次 ≈ 4–9s(仅首次,当日缓存)
- 概念 GN ≈ 300–500 板块 → ≈ 20–50s(仅首次,当日缓存)
- 构建必须放后台任务:若在请求线程里同步建矩阵,构建期间会占住共享连接, 拖死同服务器的其他所有页面请求。因此首次构建走后台任务 + 进度轮询(§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 响应
{
"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. 边界与风险
- 单连接串行 × 概念板块量大:GN 首次构建 20–50s——后台任务 + 进度条 + 当日缓存兜底; 默认板块类型为行业(首屏 4–9s 可接受)。文档与页面均提示"概念首次构建较慢"。
- 部分服务器不给 88xxxx 日K:该板块 pct 记 null、不参与当日排名;行头仍展示(有历史的日子照常着色)。
- 新板块/长假期:窗口首日的前收取自缓冲 bar(多拉 12 根);缓冲不足时首日 pct 置 null。
- 休市/周末:历史轴止于最近交易日;今日列与历史末列去重(§2.3 第 8 步),不出现重复列。
- 盘中口径:今日列 = price/pre_close-1(与 overview 同口径,规避 Issue #53 的 CHANGE_PCT 恒 0); 历史列 = 日K close 比值。两段口径在收盘后自然收敛一致。
- 缓存正确性:历史矩阵按日历日失效(跨日首个请求重建),不存在隔夜脏数据;
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 服务器时错误条 + 重试,页面不白屏