【应用】基于 Next.js 16 + Python mplfinance的金融K线图与技术指标可视化平台(二)

文章目录

    • [四、后端 API 设计](#四、后端 API 设计)
      • [4.1 API 路由总览](#4.1 API 路由总览)
      • [4.2 `/api/stock-data` --- 行情数据接口](#4.2 /api/stock-data — 行情数据接口)
      • [4.3 `/api/chart` --- 图表渲染接口](#4.3 /api/chart — 图表渲染接口)
        • [4.3.1 子进程通信协议](#4.3.1 子进程通信协议)
        • [4.3.2 超时保护](#4.3.2 超时保护)
        • [4.3.3 Python 路径与运行时](#4.3.3 Python 路径与运行时)
        • [4.3.4 错误处理链](#4.3.4 错误处理链)
    • [五、Python 图表引擎设计](#五、Python 图表引擎设计)
      • [5.1 渲染管线总览](#5.1 渲染管线总览)
      • [5.2 中文字体处理](#5.2 中文字体处理)
      • [5.3 主题与配色体系](#5.3 主题与配色体系)
      • [5.4 多面板布局系统](#5.4 多面板布局系统)
      • [5.5 市场颜色与样式组装](#5.5 市场颜色与样式组装)
      • [5.6 图片输出与 Base64 编码](#5.6 图片输出与 Base64 编码)
      • [5.7 统计数据组装](#5.7 统计数据组装)
    • 六、技术指标算法原理
      • [6.1 指标总览](#6.1 指标总览)
      • [6.2 MA --- 简单移动平均](#6.2 MA — 简单移动平均)
      • [6.3 EMA --- 指数移动平均](#6.3 EMA — 指数移动平均)
      • [6.4 MACD --- 异同移动平均线](#6.4 MACD — 异同移动平均线)
      • [6.5 RSI --- 相对强弱指标(Wilder 法)](#6.5 RSI — 相对强弱指标(Wilder 法))
      • [6.6 BOLL --- 布林带](#6.6 BOLL — 布林带)
      • [6.7 指标渲染参数](#6.7 指标渲染参数)

四、后端 API 设计

4.1 API 路由总览

项目定义了三个 API 路由,全部位于 src/app/api/ 下,使用 Next.js App Router 的文件路由约定:

路由 方法 Runtime 功能 缓存策略
/api GET Node.js 健康检查返回 {message:"Hello, world!"}
/api/stock-data POST Node.js 生成/获取模拟行情数据 + 返回标的列表 内存级 Map 缓存
/api/chart POST Node.js 调度 Python 子进程渲染 K 线图 无缓存(每次实时渲染)

4.2 /api/stock-data --- 行情数据接口

该接口负责根据 symbol 返回对应的 OHLCV 日线序列。核心设计是用内存 Map 做缓存------同一标的的数据只生成一次,后续请求直接命中缓存,避免重复执行随机数生成逻辑。
#mermaid-svg-JwITqzNDbuVeLbMA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JwITqzNDbuVeLbMA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JwITqzNDbuVeLbMA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JwITqzNDbuVeLbMA .error-icon{fill:#552222;}#mermaid-svg-JwITqzNDbuVeLbMA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JwITqzNDbuVeLbMA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JwITqzNDbuVeLbMA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JwITqzNDbuVeLbMA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JwITqzNDbuVeLbMA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JwITqzNDbuVeLbMA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JwITqzNDbuVeLbMA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JwITqzNDbuVeLbMA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JwITqzNDbuVeLbMA .marker.cross{stroke:#333333;}#mermaid-svg-JwITqzNDbuVeLbMA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JwITqzNDbuVeLbMA p{margin:0;}#mermaid-svg-JwITqzNDbuVeLbMA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JwITqzNDbuVeLbMA .cluster-label text{fill:#333;}#mermaid-svg-JwITqzNDbuVeLbMA .cluster-label span{color:#333;}#mermaid-svg-JwITqzNDbuVeLbMA .cluster-label span p{background-color:transparent;}#mermaid-svg-JwITqzNDbuVeLbMA .label text,#mermaid-svg-JwITqzNDbuVeLbMA span{fill:#333;color:#333;}#mermaid-svg-JwITqzNDbuVeLbMA .node rect,#mermaid-svg-JwITqzNDbuVeLbMA .node circle,#mermaid-svg-JwITqzNDbuVeLbMA .node ellipse,#mermaid-svg-JwITqzNDbuVeLbMA .node polygon,#mermaid-svg-JwITqzNDbuVeLbMA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JwITqzNDbuVeLbMA .rough-node .label text,#mermaid-svg-JwITqzNDbuVeLbMA .node .label text,#mermaid-svg-JwITqzNDbuVeLbMA .image-shape .label,#mermaid-svg-JwITqzNDbuVeLbMA .icon-shape .label{text-anchor:middle;}#mermaid-svg-JwITqzNDbuVeLbMA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JwITqzNDbuVeLbMA .rough-node .label,#mermaid-svg-JwITqzNDbuVeLbMA .node .label,#mermaid-svg-JwITqzNDbuVeLbMA .image-shape .label,#mermaid-svg-JwITqzNDbuVeLbMA .icon-shape .label{text-align:center;}#mermaid-svg-JwITqzNDbuVeLbMA .node.clickable{cursor:pointer;}#mermaid-svg-JwITqzNDbuVeLbMA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JwITqzNDbuVeLbMA .arrowheadPath{fill:#333333;}#mermaid-svg-JwITqzNDbuVeLbMA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JwITqzNDbuVeLbMA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JwITqzNDbuVeLbMA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JwITqzNDbuVeLbMA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JwITqzNDbuVeLbMA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JwITqzNDbuVeLbMA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JwITqzNDbuVeLbMA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JwITqzNDbuVeLbMA .cluster text{fill:#333;}#mermaid-svg-JwITqzNDbuVeLbMA .cluster span{color:#333;}#mermaid-svg-JwITqzNDbuVeLbMA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JwITqzNDbuVeLbMA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JwITqzNDbuVeLbMA rect.text{fill:none;stroke-width:0;}#mermaid-svg-JwITqzNDbuVeLbMA .icon-shape,#mermaid-svg-JwITqzNDbuVeLbMA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JwITqzNDbuVeLbMA .icon-shape p,#mermaid-svg-JwITqzNDbuVeLbMA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JwITqzNDbuVeLbMA .icon-shape .label rect,#mermaid-svg-JwITqzNDbuVeLbMA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JwITqzNDbuVeLbMA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JwITqzNDbuVeLbMA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JwITqzNDbuVeLbMA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中
未命中
POST /api/stock-data

body: {symbol}
cache.has(symbol)?
直接返回缓存序列
getStock(symbol) 获取元信息
generateSeries(meta)

mulberry32 种子 → 520 根日线
cache.set(symbol, series)
组装响应 JSON
返回 {ok, meta, data, stocks}

响应结构包含四个字段:ok(成功标志)、meta(标的元信息)、data(OHLCV 数组)、stocks(全部可选标的列表,供前端下拉填充)。

4.3 /api/chart --- 图表渲染接口

这是整个系统最核心的 API,承担 Node.js ↔ Python 的桥接职责。

4.3.1 子进程通信协议

Node.js 通过 child_process.spawn 启动 Python 进程,以 stdin/stdout JSON 管道进行双向通信:
浏览器 kline_chart.py (Python) /api/chart (Node.js) 浏览器 kline_chart.py (Python) /api/chart (Node.js) #mermaid-svg-ciVr8VJcjCfSDsAd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ciVr8VJcjCfSDsAd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ciVr8VJcjCfSDsAd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ciVr8VJcjCfSDsAd .error-icon{fill:#552222;}#mermaid-svg-ciVr8VJcjCfSDsAd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ciVr8VJcjCfSDsAd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ciVr8VJcjCfSDsAd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ciVr8VJcjCfSDsAd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ciVr8VJcjCfSDsAd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ciVr8VJcjCfSDsAd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ciVr8VJcjCfSDsAd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ciVr8VJcjCfSDsAd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ciVr8VJcjCfSDsAd .marker.cross{stroke:#333333;}#mermaid-svg-ciVr8VJcjCfSDsAd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ciVr8VJcjCfSDsAd p{margin:0;}#mermaid-svg-ciVr8VJcjCfSDsAd .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ciVr8VJcjCfSDsAd text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ciVr8VJcjCfSDsAd .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ciVr8VJcjCfSDsAd .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ciVr8VJcjCfSDsAd .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ciVr8VJcjCfSDsAd .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ciVr8VJcjCfSDsAd #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ciVr8VJcjCfSDsAd .sequenceNumber{fill:white;}#mermaid-svg-ciVr8VJcjCfSDsAd #sequencenumber{fill:#333;}#mermaid-svg-ciVr8VJcjCfSDsAd #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ciVr8VJcjCfSDsAd .messageText{fill:#333;stroke:none;}#mermaid-svg-ciVr8VJcjCfSDsAd .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ciVr8VJcjCfSDsAd .labelText,#mermaid-svg-ciVr8VJcjCfSDsAd .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ciVr8VJcjCfSDsAd .loopText,#mermaid-svg-ciVr8VJcjCfSDsAd .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ciVr8VJcjCfSDsAd .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ciVr8VJcjCfSDsAd .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ciVr8VJcjCfSDsAd .noteText,#mermaid-svg-ciVr8VJcjCfSDsAd .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ciVr8VJcjCfSDsAd .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ciVr8VJcjCfSDsAd .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ciVr8VJcjCfSDsAd .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ciVr8VJcjCfSDsAd .actorPopupMenu{position:absolute;}#mermaid-svg-ciVr8VJcjCfSDsAd .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ciVr8VJcjCfSDsAd .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ciVr8VJcjCfSDsAd .actor-man circle,#mermaid-svg-ciVr8VJcjCfSDsAd line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ciVr8VJcjCfSDsAd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 校验 body.data.length ≥ 5 spawn('/home/z/.venv/bin/python3', 'kline_chart.py') 设置 env MPLBACKEND=Agg stdin.write(JSON {data, config}) stdin.end() json.loads(stdin) DataFrame 构建 + 指标计算 mplfinance.plot(savefig=BytesIO) base64(image) + 组装 stats stdout.write(JSON {ok,image,stats,error}) JSON.parse(stdout) NextResponse.json(result)

4.3.2 超时保护

渲染请求设置了 90 秒超时(maxDuration = 120 为 Next.js 函数级上限,内部实际使用 90s 的 setTimeout)。超时后向 Python 进程发送 SIGKILL 强制终止,防止 matplotlib 在极端数据下卡死拖垮整个服务:

typescript 复制代码
const timer = setTimeout(() => {
  proc.kill('SIGKILL')
  reject(new Error('图表渲染超时(90s)'))
}, 90_000)
4.3.3 Python 路径与运行时

开发环境中 Python 解释器路径硬编码为 /home/z/.venv/bin/python3,这是沙箱预装的虚拟环境。部署环境中则由 start.sh 动态设置 PYTHONPATHPATH,指向打包产物内的 site-packages

4.3.4 错误处理链

#mermaid-svg-b5tBNzwSpicQHEo7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b5tBNzwSpicQHEo7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b5tBNzwSpicQHEo7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b5tBNzwSpicQHEo7 .error-icon{fill:#552222;}#mermaid-svg-b5tBNzwSpicQHEo7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b5tBNzwSpicQHEo7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b5tBNzwSpicQHEo7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b5tBNzwSpicQHEo7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b5tBNzwSpicQHEo7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b5tBNzwSpicQHEo7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b5tBNzwSpicQHEo7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b5tBNzwSpicQHEo7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b5tBNzwSpicQHEo7 .marker.cross{stroke:#333333;}#mermaid-svg-b5tBNzwSpicQHEo7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b5tBNzwSpicQHEo7 p{margin:0;}#mermaid-svg-b5tBNzwSpicQHEo7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-b5tBNzwSpicQHEo7 .cluster-label text{fill:#333;}#mermaid-svg-b5tBNzwSpicQHEo7 .cluster-label span{color:#333;}#mermaid-svg-b5tBNzwSpicQHEo7 .cluster-label span p{background-color:transparent;}#mermaid-svg-b5tBNzwSpicQHEo7 .label text,#mermaid-svg-b5tBNzwSpicQHEo7 span{fill:#333;color:#333;}#mermaid-svg-b5tBNzwSpicQHEo7 .node rect,#mermaid-svg-b5tBNzwSpicQHEo7 .node circle,#mermaid-svg-b5tBNzwSpicQHEo7 .node ellipse,#mermaid-svg-b5tBNzwSpicQHEo7 .node polygon,#mermaid-svg-b5tBNzwSpicQHEo7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-b5tBNzwSpicQHEo7 .rough-node .label text,#mermaid-svg-b5tBNzwSpicQHEo7 .node .label text,#mermaid-svg-b5tBNzwSpicQHEo7 .image-shape .label,#mermaid-svg-b5tBNzwSpicQHEo7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-b5tBNzwSpicQHEo7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-b5tBNzwSpicQHEo7 .rough-node .label,#mermaid-svg-b5tBNzwSpicQHEo7 .node .label,#mermaid-svg-b5tBNzwSpicQHEo7 .image-shape .label,#mermaid-svg-b5tBNzwSpicQHEo7 .icon-shape .label{text-align:center;}#mermaid-svg-b5tBNzwSpicQHEo7 .node.clickable{cursor:pointer;}#mermaid-svg-b5tBNzwSpicQHEo7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-b5tBNzwSpicQHEo7 .arrowheadPath{fill:#333333;}#mermaid-svg-b5tBNzwSpicQHEo7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-b5tBNzwSpicQHEo7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-b5tBNzwSpicQHEo7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b5tBNzwSpicQHEo7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-b5tBNzwSpicQHEo7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b5tBNzwSpicQHEo7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-b5tBNzwSpicQHEo7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-b5tBNzwSpicQHEo7 .cluster text{fill:#333;}#mermaid-svg-b5tBNzwSpicQHEo7 .cluster span{color:#333;}#mermaid-svg-b5tBNzwSpicQHEo7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-b5tBNzwSpicQHEo7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-b5tBNzwSpicQHEo7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-b5tBNzwSpicQHEo7 .icon-shape,#mermaid-svg-b5tBNzwSpicQHEo7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b5tBNzwSpicQHEo7 .icon-shape p,#mermaid-svg-b5tBNzwSpicQHEo7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-b5tBNzwSpicQHEo7 .icon-shape .label rect,#mermaid-svg-b5tBNzwSpicQHEo7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b5tBNzwSpicQHEo7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-b5tBNzwSpicQHEo7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-b5tBNzwSpicQHEo7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

超时
spawn 失败
正常




POST 请求到达
body.data ≥ 5 根?
400: K线数据不足
spawn Python
进程正常退出?
kill SIGKILL → 500: 超时
500: 进程异常
stdout 可解析为 JSON?
500: Python 进程异常退出
result.ok?
500: 渲染失败
200: 返回图片+统计


五、Python 图表引擎设计

python/kline_chart.py 是整个项目的渲染核心,298 行代码完成了从 JSON 输入到 Base64 PNG 输出的全部工作。

5.1 渲染管线总览

#mermaid-svg-AHWQLCqGGv6ljENI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AHWQLCqGGv6ljENI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AHWQLCqGGv6ljENI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AHWQLCqGGv6ljENI .error-icon{fill:#552222;}#mermaid-svg-AHWQLCqGGv6ljENI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AHWQLCqGGv6ljENI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AHWQLCqGGv6ljENI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AHWQLCqGGv6ljENI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AHWQLCqGGv6ljENI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AHWQLCqGGv6ljENI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AHWQLCqGGv6ljENI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AHWQLCqGGv6ljENI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AHWQLCqGGv6ljENI .marker.cross{stroke:#333333;}#mermaid-svg-AHWQLCqGGv6ljENI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AHWQLCqGGv6ljENI p{margin:0;}#mermaid-svg-AHWQLCqGGv6ljENI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AHWQLCqGGv6ljENI .cluster-label text{fill:#333;}#mermaid-svg-AHWQLCqGGv6ljENI .cluster-label span{color:#333;}#mermaid-svg-AHWQLCqGGv6ljENI .cluster-label span p{background-color:transparent;}#mermaid-svg-AHWQLCqGGv6ljENI .label text,#mermaid-svg-AHWQLCqGGv6ljENI span{fill:#333;color:#333;}#mermaid-svg-AHWQLCqGGv6ljENI .node rect,#mermaid-svg-AHWQLCqGGv6ljENI .node circle,#mermaid-svg-AHWQLCqGGv6ljENI .node ellipse,#mermaid-svg-AHWQLCqGGv6ljENI .node polygon,#mermaid-svg-AHWQLCqGGv6ljENI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AHWQLCqGGv6ljENI .rough-node .label text,#mermaid-svg-AHWQLCqGGv6ljENI .node .label text,#mermaid-svg-AHWQLCqGGv6ljENI .image-shape .label,#mermaid-svg-AHWQLCqGGv6ljENI .icon-shape .label{text-anchor:middle;}#mermaid-svg-AHWQLCqGGv6ljENI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AHWQLCqGGv6ljENI .rough-node .label,#mermaid-svg-AHWQLCqGGv6ljENI .node .label,#mermaid-svg-AHWQLCqGGv6ljENI .image-shape .label,#mermaid-svg-AHWQLCqGGv6ljENI .icon-shape .label{text-align:center;}#mermaid-svg-AHWQLCqGGv6ljENI .node.clickable{cursor:pointer;}#mermaid-svg-AHWQLCqGGv6ljENI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AHWQLCqGGv6ljENI .arrowheadPath{fill:#333333;}#mermaid-svg-AHWQLCqGGv6ljENI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AHWQLCqGGv6ljENI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AHWQLCqGGv6ljENI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AHWQLCqGGv6ljENI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AHWQLCqGGv6ljENI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AHWQLCqGGv6ljENI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AHWQLCqGGv6ljENI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AHWQLCqGGv6ljENI .cluster text{fill:#333;}#mermaid-svg-AHWQLCqGGv6ljENI .cluster span{color:#333;}#mermaid-svg-AHWQLCqGGv6ljENI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AHWQLCqGGv6ljENI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AHWQLCqGGv6ljENI rect.text{fill:none;stroke-width:0;}#mermaid-svg-AHWQLCqGGv6ljENI .icon-shape,#mermaid-svg-AHWQLCqGGv6ljENI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AHWQLCqGGv6ljENI .icon-shape p,#mermaid-svg-AHWQLCqGGv6ljENI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AHWQLCqGGv6ljENI .icon-shape .label rect,#mermaid-svg-AHWQLCqGGv6ljENI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AHWQLCqGGv6ljENI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AHWQLCqGGv6ljENI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AHWQLCqGGv6ljENI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 渲染输出
指标计算与 addplot 组装
主题选择
输入解析




stdin JSON
提取 config + data
pd.DataFrame 构建

date 索引 + OHLCV 列
astype(float) + dropna()
theme == dark?
使用 DARK 配色表
使用 LIGHT 配色表
colorScheme == intl?
叠加 INTL 覆盖

(绿涨红跌)
保持原配色
布林带 → 3 条 addplot(panel=0)
均线 MA → 最多 4 条 addplot(panel=0)
MACD → hist柱+DIF+DEA+零线 addplot(panel=N)
RSI → 线+30/70参考线 addplot(panel=N+1)
最新价水平虚线 addplot(panel=0)
make_mpf_style()

marketcolors + rc 参数
计算 panel_ratios
mpf.plot(df, savefig=BytesIO)
base64(buffer)
组装 stats 统计数据
stdout JSON 输出

5.2 中文字体处理

matplotlib 默认不支持中文字符,项目通过 rcParams 显式指定 WenQuanYi Zen Hei (文泉驿正黑)作为首选字体,DejaVu Sans 作为回退。同时设置 axes.unicode_minus = False 防止负号显示为方块:

python 复制代码
rcParams["font.sans-serif"] = ["WenQuanYi Zen Hei", "DejaVu Sans"]
rcParams["axes.unicode_minus"] = False

这套字体配置在 style 对象的 rc 参数中再次声明,确保 mplfinance 内部创建的 Axes 也能正确渲染中文。

5.3 主题与配色体系

项目定义了 DARKLIGHT 两套配色表,每套包含 16 个颜色键值,覆盖了面板背景、网格、K 线涨跌色、均线色、指标线色等全部视觉元素。通过 get_theme(name, scheme) 函数组合主题与配色方案:

配色键 DARK 值 LIGHT 值 用途
face #131722 #ffffff 面板背景色
edge #2a2e39 #e0e3eb 面板边框色
fig #131722 #ffffff 画布背景色
grid #2a2e39 #e0e3eb 网格线色
text #d1d4dc #131722 文字色
up #ef5350 #e03131 上涨色(CN)
down #26a69a #0ca678 下跌色(CN)
ma_colors 4 色 4 色 MA 均线渐变色
last #f7b32b #e8a30c 最新价标注线

colorScheme == "intl" 时,使用 INTL 覆盖表将涨跌色互换(绿涨红跌),与欧美市场的视觉习惯一致。

5.4 多面板布局系统

mplfinance 支持通过 panel 参数将不同指标分配到不同的子图区域。项目动态计算面板编号和高度比例:
#mermaid-svg-QwRVyJH6C4yBwyEl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QwRVyJH6C4yBwyEl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QwRVyJH6C4yBwyEl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QwRVyJH6C4yBwyEl .error-icon{fill:#552222;}#mermaid-svg-QwRVyJH6C4yBwyEl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QwRVyJH6C4yBwyEl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QwRVyJH6C4yBwyEl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QwRVyJH6C4yBwyEl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QwRVyJH6C4yBwyEl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QwRVyJH6C4yBwyEl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QwRVyJH6C4yBwyEl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QwRVyJH6C4yBwyEl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QwRVyJH6C4yBwyEl .marker.cross{stroke:#333333;}#mermaid-svg-QwRVyJH6C4yBwyEl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QwRVyJH6C4yBwyEl p{margin:0;}#mermaid-svg-QwRVyJH6C4yBwyEl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QwRVyJH6C4yBwyEl .cluster-label text{fill:#333;}#mermaid-svg-QwRVyJH6C4yBwyEl .cluster-label span{color:#333;}#mermaid-svg-QwRVyJH6C4yBwyEl .cluster-label span p{background-color:transparent;}#mermaid-svg-QwRVyJH6C4yBwyEl .label text,#mermaid-svg-QwRVyJH6C4yBwyEl span{fill:#333;color:#333;}#mermaid-svg-QwRVyJH6C4yBwyEl .node rect,#mermaid-svg-QwRVyJH6C4yBwyEl .node circle,#mermaid-svg-QwRVyJH6C4yBwyEl .node ellipse,#mermaid-svg-QwRVyJH6C4yBwyEl .node polygon,#mermaid-svg-QwRVyJH6C4yBwyEl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QwRVyJH6C4yBwyEl .rough-node .label text,#mermaid-svg-QwRVyJH6C4yBwyEl .node .label text,#mermaid-svg-QwRVyJH6C4yBwyEl .image-shape .label,#mermaid-svg-QwRVyJH6C4yBwyEl .icon-shape .label{text-anchor:middle;}#mermaid-svg-QwRVyJH6C4yBwyEl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QwRVyJH6C4yBwyEl .rough-node .label,#mermaid-svg-QwRVyJH6C4yBwyEl .node .label,#mermaid-svg-QwRVyJH6C4yBwyEl .image-shape .label,#mermaid-svg-QwRVyJH6C4yBwyEl .icon-shape .label{text-align:center;}#mermaid-svg-QwRVyJH6C4yBwyEl .node.clickable{cursor:pointer;}#mermaid-svg-QwRVyJH6C4yBwyEl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QwRVyJH6C4yBwyEl .arrowheadPath{fill:#333333;}#mermaid-svg-QwRVyJH6C4yBwyEl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QwRVyJH6C4yBwyEl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QwRVyJH6C4yBwyEl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QwRVyJH6C4yBwyEl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QwRVyJH6C4yBwyEl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QwRVyJH6C4yBwyEl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QwRVyJH6C4yBwyEl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QwRVyJH6C4yBwyEl .cluster text{fill:#333;}#mermaid-svg-QwRVyJH6C4yBwyEl .cluster span{color:#333;}#mermaid-svg-QwRVyJH6C4yBwyEl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QwRVyJH6C4yBwyEl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QwRVyJH6C4yBwyEl rect.text{fill:none;stroke-width:0;}#mermaid-svg-QwRVyJH6C4yBwyEl .icon-shape,#mermaid-svg-QwRVyJH6C4yBwyEl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QwRVyJH6C4yBwyEl .icon-shape p,#mermaid-svg-QwRVyJH6C4yBwyEl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QwRVyJH6C4yBwyEl .icon-shape .label rect,#mermaid-svg-QwRVyJH6C4yBwyEl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QwRVyJH6C4yBwyEl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QwRVyJH6C4yBwyEl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QwRVyJH6C4yBwyEl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} panel_ratios 动态计算
ratio: 4 或 5
ratio: 1
ratio: 1.4
ratio: 1.1
Panel 0: 主图

K线 + BOLL + MA + 最新价
Panel 1: 成交量

(showVolume=true 时)
Panel 2: MACD

hist柱 + DIF + DEA
Panel 3: RSI

RSI线 + 30/70参考线
底部

面板编号的动态计算逻辑:

python 复制代码
# MACD 面板编号 = 成交量面板之后
macd_panel = 1 + (1 if show_volume else 0)

# RSI 面板编号 = MACD 面板之后
rsi_panel = (macd_panel + 1) if macd_panel is not None else (1 + (1 if show_volume else 0))

高度比例 panel_ratios 也根据启用的面板动态拼装:主图固定为 4 或 5(有无成交量),成交量为 1,MACD 为 1.4,RSI 为 1.1。这种比例分配确保主图占据主要视觉空间,副图既能清晰展示又不喧宾夺主。

5.5 市场颜色与样式组装

mplfinance 的样式系统通过 make_marketcolorsmake_mpf_style 两层组装。marketcolors 定义 K 线实体、边框、影线、成交量的涨跌色;mpf_style 则在此基础上叠加背景色、网格、字体大小等全局样式。项目以 nightclouds(深色)和 yahoo(浅色)作为基础样式,再通过 rc 参数覆盖字体、刻度、标题等颜色:

python 复制代码
style = mpf.make_mpf_style(
    marketcolors=mc,
    base_mpf_style="nightclouds" if theme_name == "dark" else "yahoo",
    facecolor=T["face"], edgecolor=T["edge"], figcolor=T["fig"],
    gridcolor=T["grid"], gridstyle="--", gridaxis="both",
    rc={...font and color overrides...},
)

5.6 图片输出与 Base64 编码

渲染结果写入 io.BytesIO 内存缓冲区而非磁盘文件,避免 I/O 开销和临时文件清理问题。mplfinance 的 savefig 参数指定 format="png"dpi=132,并使用 bbox_inches="tight" 自动裁剪空白边距。最终通过 base64.b64encode 编码为 ASCII 字符串,与统计数据一起组成 JSON 响应。

5.7 统计数据组装

Python 端在渲染完成后,同步计算并返回一份 stats 对象,包含最新价、涨跌额、涨跌幅、今开/最高/最低、成交量、区间极值以及各指标的最新读数。这些数据让前端无需自行重复计算指标,直接展示在 StatsBarIndicatorReadouts 中:

python 复制代码
stats = {
    "last": ..., "change": ..., "changePct": ...,
    "open": ..., "high": ..., "low": ...,
    "rangeHigh": ..., "rangeLow": ...,
    "volume": ..., "avgVolume": ..., "bars": ...,
    "ma": {...}, "boll": {...}, "macd": {...}, "rsi": ...,
}

六、技术指标算法原理

6.1 指标总览

指标 全称 原理简述 参数
MA Simple Moving Average 收盘价的 N 日算术平均 maPeriods 默认 5,10,20,60
EMA Exponential Moving Average 赋予近期数据更大权重的指数加权平均 MACD 内部使用
BOLL Bollinger Bands MA ± k × 标准差,刻画价格波动通道 bollPeriod 默认 20, bollStd 默认 2.0
MACD Moving Average Convergence Divergence 快慢 EMA 差值(DIF)及其信号线(DEA)的差离值 macdFast 12, macdSlow 26, macdSignal 9
RSI Relative Strength Index 一段时间内平均涨幅与平均跌幅的比值,衡量超买超卖 rsiPeriod 默认 14

6.2 MA --- 简单移动平均

MA 是最基础的趋势跟踪指标。取最近 N 个交易日的收盘价做算术平均,随时间窗口滑动。Pandas 的 rolling(n, min_periods=n).mean() 天然实现了这一计算,min_periods=n 确保前 N-1 个不完整窗口返回 NaN,不会产生虚假的短均线值。

python 复制代码
def sma(s: pd.Series, n: int) -> pd.Series:
    return s.rolling(n, min_periods=n).mean()

6.3 EMA --- 指数移动平均

EMA 对越近期的数据赋予越大的权重,权重按指数衰减。Pandas 的 ewm(span=n, adjust=False).mean() 实现了标准的 EMA 计算,adjust=False 表示使用递推公式(第一日起即以当日价格为初始 EMA),与金融行业主流计算方式一致:

python 复制代码
def ema(s: pd.Series, n: int) -> pd.Series:
    return s.ewm(span=n, adjust=False).mean()

6.4 MACD --- 异同移动平均线

MACD 利用快慢两条 EMA 的差值来捕捉趋势的方向与动量变化。计算分为三步:
#mermaid-svg-TjOgUqjTacLYpPFH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TjOgUqjTacLYpPFH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TjOgUqjTacLYpPFH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TjOgUqjTacLYpPFH .error-icon{fill:#552222;}#mermaid-svg-TjOgUqjTacLYpPFH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TjOgUqjTacLYpPFH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TjOgUqjTacLYpPFH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TjOgUqjTacLYpPFH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TjOgUqjTacLYpPFH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TjOgUqjTacLYpPFH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TjOgUqjTacLYpPFH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TjOgUqjTacLYpPFH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TjOgUqjTacLYpPFH .marker.cross{stroke:#333333;}#mermaid-svg-TjOgUqjTacLYpPFH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TjOgUqjTacLYpPFH p{margin:0;}#mermaid-svg-TjOgUqjTacLYpPFH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TjOgUqjTacLYpPFH .cluster-label text{fill:#333;}#mermaid-svg-TjOgUqjTacLYpPFH .cluster-label span{color:#333;}#mermaid-svg-TjOgUqjTacLYpPFH .cluster-label span p{background-color:transparent;}#mermaid-svg-TjOgUqjTacLYpPFH .label text,#mermaid-svg-TjOgUqjTacLYpPFH span{fill:#333;color:#333;}#mermaid-svg-TjOgUqjTacLYpPFH .node rect,#mermaid-svg-TjOgUqjTacLYpPFH .node circle,#mermaid-svg-TjOgUqjTacLYpPFH .node ellipse,#mermaid-svg-TjOgUqjTacLYpPFH .node polygon,#mermaid-svg-TjOgUqjTacLYpPFH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TjOgUqjTacLYpPFH .rough-node .label text,#mermaid-svg-TjOgUqjTacLYpPFH .node .label text,#mermaid-svg-TjOgUqjTacLYpPFH .image-shape .label,#mermaid-svg-TjOgUqjTacLYpPFH .icon-shape .label{text-anchor:middle;}#mermaid-svg-TjOgUqjTacLYpPFH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TjOgUqjTacLYpPFH .rough-node .label,#mermaid-svg-TjOgUqjTacLYpPFH .node .label,#mermaid-svg-TjOgUqjTacLYpPFH .image-shape .label,#mermaid-svg-TjOgUqjTacLYpPFH .icon-shape .label{text-align:center;}#mermaid-svg-TjOgUqjTacLYpPFH .node.clickable{cursor:pointer;}#mermaid-svg-TjOgUqjTacLYpPFH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TjOgUqjTacLYpPFH .arrowheadPath{fill:#333333;}#mermaid-svg-TjOgUqjTacLYpPFH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TjOgUqjTacLYpPFH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TjOgUqjTacLYpPFH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TjOgUqjTacLYpPFH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TjOgUqjTacLYpPFH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TjOgUqjTacLYpPFH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TjOgUqjTacLYpPFH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TjOgUqjTacLYpPFH .cluster text{fill:#333;}#mermaid-svg-TjOgUqjTacLYpPFH .cluster span{color:#333;}#mermaid-svg-TjOgUqjTacLYpPFH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TjOgUqjTacLYpPFH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TjOgUqjTacLYpPFH rect.text{fill:none;stroke-width:0;}#mermaid-svg-TjOgUqjTacLYpPFH .icon-shape,#mermaid-svg-TjOgUqjTacLYpPFH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TjOgUqjTacLYpPFH .icon-shape p,#mermaid-svg-TjOgUqjTacLYpPFH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TjOgUqjTacLYpPFH .icon-shape .label rect,#mermaid-svg-TjOgUqjTacLYpPFH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TjOgUqjTacLYpPFH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TjOgUqjTacLYpPFH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TjOgUqjTacLYpPFH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 收盘价 Close
EMA(fast=12)
EMA(slow=26)
DIF = EMA_fast - EMA_slow
DEA = EMA(DIF, signal=9)
HIST = (DIF - DEA) × 2
红绿柱状图
DIF 线(黄)
DEA 线(粉)

  • DIF(差离值):快线 EMA 减慢线 EMA,反映短期趋势相对长期趋势的偏离程度

  • DEA(信号线):DIF 的 9 日 EMA,作为 DIF 的平滑参考

  • HIST(柱状图)(DIF - DEA) × 2,乘以 2 是中国市场的惯例写法(国际标准通常不乘 2),柱状图的正负变化是金叉/死叉的直观信号

python 复制代码
def macd(close, fast, slow, signal):
    dif = ema(close, fast) - ema(close, slow)
    dea = ema(dif, signal)
    hist = (dif - dea) * 2
    return dif, dea, hist

6.5 RSI --- 相对强弱指标(Wilder 法)

RSI 衡量一段时间内平均涨幅与平均跌幅的比值,取值范围 0-100。项目采用 Wilder 平滑法(也称 Wilder's Smoothing),这是与 J. Welles Wilder 原始定义一致的实现方式:
#mermaid-svg-5qxwDJDvAhqSvEHH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5qxwDJDvAhqSvEHH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5qxwDJDvAhqSvEHH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5qxwDJDvAhqSvEHH .error-icon{fill:#552222;}#mermaid-svg-5qxwDJDvAhqSvEHH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5qxwDJDvAhqSvEHH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5qxwDJDvAhqSvEHH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5qxwDJDvAhqSvEHH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5qxwDJDvAhqSvEHH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5qxwDJDvAhqSvEHH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5qxwDJDvAhqSvEHH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5qxwDJDvAhqSvEHH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5qxwDJDvAhqSvEHH .marker.cross{stroke:#333333;}#mermaid-svg-5qxwDJDvAhqSvEHH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5qxwDJDvAhqSvEHH p{margin:0;}#mermaid-svg-5qxwDJDvAhqSvEHH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5qxwDJDvAhqSvEHH .cluster-label text{fill:#333;}#mermaid-svg-5qxwDJDvAhqSvEHH .cluster-label span{color:#333;}#mermaid-svg-5qxwDJDvAhqSvEHH .cluster-label span p{background-color:transparent;}#mermaid-svg-5qxwDJDvAhqSvEHH .label text,#mermaid-svg-5qxwDJDvAhqSvEHH span{fill:#333;color:#333;}#mermaid-svg-5qxwDJDvAhqSvEHH .node rect,#mermaid-svg-5qxwDJDvAhqSvEHH .node circle,#mermaid-svg-5qxwDJDvAhqSvEHH .node ellipse,#mermaid-svg-5qxwDJDvAhqSvEHH .node polygon,#mermaid-svg-5qxwDJDvAhqSvEHH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5qxwDJDvAhqSvEHH .rough-node .label text,#mermaid-svg-5qxwDJDvAhqSvEHH .node .label text,#mermaid-svg-5qxwDJDvAhqSvEHH .image-shape .label,#mermaid-svg-5qxwDJDvAhqSvEHH .icon-shape .label{text-anchor:middle;}#mermaid-svg-5qxwDJDvAhqSvEHH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5qxwDJDvAhqSvEHH .rough-node .label,#mermaid-svg-5qxwDJDvAhqSvEHH .node .label,#mermaid-svg-5qxwDJDvAhqSvEHH .image-shape .label,#mermaid-svg-5qxwDJDvAhqSvEHH .icon-shape .label{text-align:center;}#mermaid-svg-5qxwDJDvAhqSvEHH .node.clickable{cursor:pointer;}#mermaid-svg-5qxwDJDvAhqSvEHH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5qxwDJDvAhqSvEHH .arrowheadPath{fill:#333333;}#mermaid-svg-5qxwDJDvAhqSvEHH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5qxwDJDvAhqSvEHH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5qxwDJDvAhqSvEHH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5qxwDJDvAhqSvEHH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5qxwDJDvAhqSvEHH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5qxwDJDvAhqSvEHH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5qxwDJDvAhqSvEHH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5qxwDJDvAhqSvEHH .cluster text{fill:#333;}#mermaid-svg-5qxwDJDvAhqSvEHH .cluster span{color:#333;}#mermaid-svg-5qxwDJDvAhqSvEHH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5qxwDJDvAhqSvEHH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5qxwDJDvAhqSvEHH rect.text{fill:none;stroke-width:0;}#mermaid-svg-5qxwDJDvAhqSvEHH .icon-shape,#mermaid-svg-5qxwDJDvAhqSvEHH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5qxwDJDvAhqSvEHH .icon-shape p,#mermaid-svg-5qxwDJDvAhqSvEHH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5qxwDJDvAhqSvEHH .icon-shape .label rect,#mermaid-svg-5qxwDJDvAhqSvEHH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5qxwDJDvAhqSvEHH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5qxwDJDvAhqSvEHH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5qxwDJDvAhqSvEHH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 日收益率 diff = close.diff()
gain = max(diff, 0)
loss = max(-diff, 0)
avg_gain = gain.ewm(alpha=1/n, adjust=False)
avg_loss = loss.ewm(alpha=1/n, adjust=False)
RS = avg_gain / avg_loss
RSI = 100 - 100/(1+RS)

Wilder 法与简单移动平均的关键区别在于 ewmalpha 参数。Wilder 使用的 alpha = 1/n 等价于 span = 2n - 1,这意味着 14 日 Wilder RSI 实际上等效于 27 日的标准 EMA。这种平滑方式衰减更慢,信号更稳定,是金融行业 RSI 计算的事实标准。

项目还处理了两个边界情况:当 avg_loss == 0(连续上涨无跌幅)时 RSI 直接取 100,当 avg_gain == 0(连续下跌无涨幅)时 RSI 取 0。这两个极端值在实践中极少出现但必须正确处理。

python 复制代码
def rsi_wilder(close, n):
    diff = close.diff()
    gain = diff.clip(lower=0.0)
    loss = (-diff).clip(lower=0.0)
    avg_gain = gain.ewm(alpha=1.0/n, adjust=False, min_periods=n).mean()
    avg_loss = loss.ewm(alpha=1.0/n, adjust=False, min_periods=n).mean()
    rs = avg_gain / avg_loss.replace(0, np.nan)
    out = 100 - 100 / (1 + rs)
    out[avg_loss == 0] = 100.0
    out[avg_gain == 0] = 0.0
    return out

6.6 BOLL --- 布林带

布林带由三条线组成:中轨(N 日 MA)、上轨(中轨 + k × 标准差)、下轨(中轨 - k × 标准差)。标准差使用 ddof=0(总体标准差),与 John Bollinger 的原始定义一致:

python 复制代码
def bollinger(close, n, k):
    mid = sma(close, n)
    std = close.rolling(n, min_periods=n).std(ddof=0)
    return mid + k * std, mid, mid - k * std

上下轨之间的通道宽度反映了波动率:通道收窄意味着波动率降低(可能酝酿变盘),通道扩张意味着波动率升高。项目还使用 fill_between 在上下轨之间填充半透明色带,增强通道的视觉辨识度。

6.7 指标渲染参数

各指标在 mplfinance 中的渲染方式汇总:

指标 addplot 类型 panel 线宽 透明度 特殊效果
BOLL 上轨 线 0 1.0 0.9 虚线 linestyle="--"
BOLL 中轨 线 0 1.2 0.9 虚线
BOLL 下轨 线 0 1.0 0.9 虚线
BOLL 色带 fill_between 0 --- 0.07 半透明填充
MA(N) 线 0 1.3 --- 4 色循环
MACD hist bar N 0.7 0.85 正负分色
DIF / DEA 线 N 1.3 --- 黄/粉双色
零线 线 N 0.6 --- 点线 linestyle=":"
RSI 线 N+1 1.3 --- 单色
30/70 参考线 线 N+1 0.7 --- 点线
最新价 线 0 0.9 0.75 虚线水平标注

相关推荐
nangonghen22 分钟前
Agent如何传递不同类型的ID给MCP Server
开发语言·python
weixin_3988316625 分钟前
佛山AI推广GEO哪家做的专业
人工智能·python
taller_200035 分钟前
【006】何时用 Python?何时用 Excel 公式?一文讲清
python·excel·公式·pie·py
云起SAAS35 分钟前
Token贷聚合导航源码 金融助贷营销获客系统,产品聚合、额度测算、线索跟进全流程闭环
金融·token贷聚合导航源码
雪芽蓝域zzs37 分钟前
第五节:Vue‑Router4 路由配置,布局嵌套路由
前端·javascript·vue.js
雪芽蓝域zzs40 分钟前
第十二节:完整用户管理 CRUD 页面(表格分页、新增 / 编辑弹窗、删除、Mock 接口)
前端·javascript·vue.js
2601_962381581 小时前
物联网场景下的 Python 边缘计算轻量化方案解析与实战落地
python·物联网·边缘计算·数据处理·轻量化方案
YHHLAI1 小时前
[特殊字符] 从 JSX 到 TypeScript:React 开发的进化之路
javascript·react.js·typescript
whcyhhh1 小时前
头歌实践教学平台:大数据存储2023(十三1)
大数据·数据库·python