先交代一下背景:我平时看盘有个“坏习惯”——开一堆 APP/网页,然后在它们之间疯狂切换,最后得到的不是信息,而是焦虑。

所以干脆自己写一个:打开一个页面,把常用的行情、板块、分时、K 线、资金面、筛选工具都塞进去

这个项目就是 stock-dashboard:React + TypeScript + Vite 的前端看板。数据源全部来自 stock-sdk没有后端、没有 Python 定时脚本,就是纯前端直接拉数据。

体验地址我直接放这: stock-dashboard (如果你也想摸鱼,记得开小窗)。

stock-overview


先说最关键的:数据层怎么接的?

项目里我把 stock-sdk 的调用统一收口到了 src/services/sdk.ts

这里做了三件很“工程化但不装”的事:

  1. SDK 单例 + 重试
    new StockSDK({ timeout, retry }),网络抖一下、接口偶发超时这种事就交给 SDK 自己兜底(最多重试 3 次,指数退避那套)。

  2. 内存缓存(TTL)
    行业/概念列表这种 10 秒内不会突然“宇宙大爆炸”的数据,缓存一下省请求;实时行情则是 2~3 秒 TTL,既不会太旧,也不会把接口当压力测试工具。

  3. 页面只认服务层,不直接碰 SDK
    你在 src/pages/** 里基本看不到 new StockSDK(),页面只调用 getFullQuotes / getTodayTimeline / getKlineWithIndicators ... 这些封装后的方法(类型会从 stock-sdk 里导入)。

顺便贴两段“骨架代码”,后面每个功能都是围绕它转的:

1
2
3
4
5
6
7
8
9
10
// src/services/sdk.ts
export const sdk = new StockSDK({ timeout: 30000, retry: { maxRetries: 3, baseDelay: 1000, maxDelay: 10000, backoffMultiplier: 2 } });

export async function getFullQuotes(codes: string[], useCache = true) {
const key = getCacheKey('getFullQuotes', codes);
if (useCache) {
return withCache(key, DEFAULT_TTL.quotes, () => sdk.getFullQuotes(codes));
}
return sdk.getFullQuotes(codes);
}
1
2
3
4
// src/services/sdk.ts
export async function getAllAShareQuotes(options?: { batchSize?: number; concurrency?: number; onProgress?: (completed: number, total: number) => void }) {
return sdk.getAllAShareQuotes(options);
}

模块和功能:每一块都是怎么用 stock-sdk 拿数据的?

路由都在 src/router/index.tsx,页面基本按功能拆在 src/pages/*。下面按“用户能点到的地方”来讲。

1) 顶部搜索:别让我翻代码查股票

入口在 src/components/layout/Header.tsx,核心就是一行:

  • search(keyword)stock-sdksdk.search(keyword)

我做了 300ms 的输入防抖,结果列表支持股票/板块混搜;点一下直接跳转:

  • 行业板块:/boards/industry/:code
  • 概念板块:/boards/concept/:code
  • 个股:/s/:code

顺便把搜索历史塞进了 localStorage(src/services/storage.ts),这点很像“你以为你在找股票,其实你在找昨天的自己”。


2) 总览 Dashboard:打开就能看个大概(以及自选快照)

页面在 src/pages/Dashboard/Dashboard.tsx

它拿数据很直接:

  • 指数行情:getFullQuotes(MAIN_INDICES)(上证、深成指、创业板、科创 50…)
  • 行业/概念列表:getIndustryList() + getConceptList()
  • 自选快照:从 src/services/storage.ts 取自选代码,再 getFullQuotes(watchlistCodes.slice(0, 50))

然后用 usePolling 每 5 秒轮询一次(src/hooks/usePolling.ts),页面不可见还会自动暂停,避免你切到别的标签页它还在疯狂刷新。

小插曲:Dashboard 里有个“榜单”区域目前主要还是板块数据的延伸(全市场个股榜要做的话,思路就是上 getAllAShareQuotes,后面“一日持股法”已经把路走通了)。


3) 热力图 Heatmap:今天到底谁在“发热”?

页面在 src/pages/Heatmap/Heatmap.tsx,用 ECharts 的 treemap 做热力图。

stock-heatmap

按维度不同,数据来源也不同:

  • 行业热力图:getIndustryList()(每个行业自带涨跌幅、换手、领涨股等字段)
  • 概念热力图:getConceptList()
  • 自选热力图:getAllWatchlistCodes()getAllQuotesByCodes(codes.slice(0, topK))

如果你把维度切到“个股”(目前代码里留了入口但暂时没放开),我这边的思路是:

  1. getIndustryConstituents(industryCode) 拿成分股
  2. getAllQuotesByCodes(stockCodes) 拉行情
  3. 拼起来做 treemap

热力图这个功能我最喜欢的一点是:你不用盯着涨跌榜刷屏,颜色一铺开,强弱结构一眼就出来了。


4) 榜单 Rankings:卷不过就看别人怎么卷

页面在 src/pages/Rankings/Rankings.tsx

stock-rankings

这里其实很“偷懒但有效”:

  • getIndustryList() / getConceptList() 拿到板块列表
  • 然后前端按 changePercent / turnoverRate 排序,取 Top 50

也就是说它的榜单是“板块榜”,不是“全市场个股榜”。要做全市场个股榜其实也不难(后面“一日持股法”就是全市场路线)。


5) 板块列表 + 板块详情:想知道“这波是谁带的节奏”

板块列表在 src/pages/Boards/Boards.tsx

  • getIndustryList() + getConceptList() 一次拿齐
  • 切 tab 只是前端切换数组
  • 支持搜索板块名/领涨股

板块详情在 src/pages/Boards/BoardDetail.tsx,这里用到的 API 就比较“齐活”了(按行业/概念分流):

  • 详情信息:还是从 getIndustryList() / getConceptList() 里 find 出来(少一次请求)
  • 成分股:getIndustryConstituents(code) / getConceptConstituents(code)
  • 板块 K 线:getIndustryKline(code, { period }) / getConceptKline(code, { period })
  • 板块 Spot 指标:getIndustrySpot(code) / getConceptSpot(code)

板块 K 线我只保留了最近 60 根(slice(-60)),不然你拖动 dataZoom 的时候会明显感觉浏览器开始“喘”。


6) 自选 Watchlist:我盯的不是股票,是“我的偏见”

页面在 src/pages/Watchlist/Watchlist.tsx,自选分组/增删改都在 src/services/storage.ts

行情获取靠:

  • getAllQuotesByCodes(normalizedActiveCodes)

这里有个小细节:我把股票代码做了 normalizeStockCodesrc/utils/format.ts),避免 SZ000001sz000001000001 这种“同一个人换三套马甲”导致重复/取不到数据。


7) 个股详情 StockDetail:该看的都给你,看不看的也顺便给你

页面在 src/pages/StockDetail/StockDetail.tsx,这是全项目里最“重”的页面之一,因为信息密度高。

stock-detail

它分别拿这些数据:

  • 实时行情:getFullQuotes([code])
  • 分时(1 分钟):getTodayTimeline(code)
  • 分钟 K(5/15/30/60):getMinuteKline(code, { period })
  • 日/周/月 K + 技术指标:getKlineWithIndicators(code, { period, adjust: 'qfq', indicators })
  • 资金流:getFundFlow([code])
  • 盘口大单:getPanelLargeOrder([code])

我比较喜欢 getKlineWithIndicators 这个点:页面上勾选 MA/MACD/BOLL/KDJ/RSI,后端(准确说是 SDK)直接把指标结果算好塞回来了,前端只需要画线,不用自己写一坨技术指标计算(少写 bug,多活几年)。

同样配了轮询:

  • 行情 2 秒一刷
  • 分时 3 秒一刷
  • 资金 10 秒一刷

8) 信号扫描 Scanner:给我一点“量化的幻觉”

页面在 src/pages/Scanner/Scanner.tsx

扫描流程大概是:

  1. 先选股票池来源
    • 自选:本地拿代码
    • 行业/概念:用 getIndustryConstituents('BK0475') / getConceptConstituents('BK0891') 抽一批成分股
  2. 对每只股票调用:
    • getKlineWithIndicators(code, { indicators: { ma/macd/rsi/boll } })
  3. 前端用最近两根 K 线做信号判断(MA 金叉、MACD 金叉、RSI 超买超卖…)

这个功能的心理作用大于实际作用,但它确实能帮我把“我觉得它要涨”变成“它真的触发了某个条件”(哪怕条件是我自己写的)。


9) 设置 Settings:不拉行情,只管体验

页面在 src/pages/Settings/Settings.tsx

stock-settings

这页不调用 stock-sdk,它只负责把刷新频率、涨跌配色、指标默认参数这些偏好写进 localStorage(src/services/storage.ts),让你下次打开页面不至于“重置成出厂设置”。


重点:一日持股法(尾盘选股)= 前端全市场扫描的“重武器”

页面在 src/pages/EndOfDayPicker/EndOfDayPicker.tsx,我在里面实现了一个“三段式”流程,核心就是 getAllAShareQuotes

stock-last

第一步:全量拉取 A 股行情(5000+)

1
2
3
4
5
6
// src/pages/EndOfDayPicker/EndOfDayPicker.tsx
const quotes = await getAllAShareQuotes({
batchSize: 500,
concurrency: 5,
onProgress: (completed, total) => setLoadingProgress({ completed, total, stage: '获取行情数据' }),
});

这段调用背后对应的是 stock-sdk 的:

  • sdk.getAllAShareQuotes(options?: GetAllAShareQuotesOptions): Promise<FullQuote[]>
  • GetAllAShareQuotesOptions 支持:
    • batchSize:单次请求股票数量(默认 500)
    • concurrency:最大并发数(默认 7)
    • onProgress:进度回调

我这里把并发设成 5,属于“别太狂,浏览器和网速都要面子”的保守路线;配合 onProgress,页面上能实时展示进度条,不会让你以为网页卡死了。

第二步:用基础条件先砍一刀

全市场 FullQuote[] 到手后,先按这些字段筛一遍(都是从 FullQuote 里直接拿):

  • 流通市值 circulatingMarketCap
  • 量比 volumeRatio
  • 涨跌幅 changePercent
  • 换手率 turnoverRate
  • 以及是否过滤 ST/*ST

这一步在 filterStocksBasic(),做完基本能从 5000+ 砍到几十/几百只,不然后面拉分时会把自己送走。

第三步:对候选股拉分时,算“价格在均线之上”的比例

对基础筛出来的候选,我会再逐批拉分时:

  • getTodayTimeline(fullCode)(注意这里要拼 sh/sz/bj 前缀)

然后用分时里 priceavgPrice 做一个很直白的强度指标:

  • price >= avgPrice 的点数 / 总点数 = timelineAboveAvgRatio

我在 filterWithTimeline() 里把分时请求按 batchSize = 5 分批并发(不是 SDK 的那个 batchSize,是我自己控制的),防止你一口气对几百只股票同时发请求,最后浏览器先躺平。

最后结果按 timelineAboveAvgRatio 排序,页面上每只股票还带一张迷你分时图,基本就能完成“尾盘快速过一遍候选”的目标。


结尾:这玩意儿适合谁?

如果你想要的是“一个能看、能筛、还能顺手把自选管理了”的轻量看板,而且你不想为此养一个后端,那这个项目的思路就挺合适:stock-sdk 把数据能力直接带进前端,然后在 UI 里做组合、筛选和展示

本地跑起来也很简单:

1
2
yarn install
yarn dev

最后的最后:页面底部那句“仅供学习参考,不构成投资建议”我不是摆设——毕竟写代码可以自信,买股票还是得谦虚点。


相关链接

Happy Coding & Trading!