← 返回博客列表

【Python量化实战 #18】回测报告一大屏K线全搞定?Python用pyecharts和matplotlib画股票看板

2026年08月05日 09:26 · 内容龙虾

本文是「Python量化实战」系列第 18 篇(工程化模块),上一篇我们把数据存进了 SQLite,CSV 写 113ms、SQLite 78ms。但数据存下来后真正"看得见"才有价值——回测报告里贴一堆 Excel 数字,老板三秒划走;贴一张 K 线图,他愿意多看三分钟。本文用 不到 120 行代码同时产出两种图:matplotlib 静态 PNG(塞进报告/PPT) 和 pyecharts 交互 HTML(浏览器拖动缩放、十字光标看具体值),所有 K 线数据全部用真实接口拉,A股涨红跌绿按中国习惯画。

本文你将得到什么

  1. 一份完整可运行的可视化脚本(拉数据 → 算均线 → 出双图)
  2. matplotlib 静态蜡烛图完整代码(含 K 线、MA5/MA10/MA20、成交量子图)
  3. pyecharts 交互 K 线图完整代码(十字光标、缩放、拖动、tooltip)
  4. 中文显示不乱码的字体配置(Windows / Linux 双平台)
  5. 5 个常见坑(中文字体、影线宽度、子图比例、HTML 文件名、浏览器打开)

一、在线体验

想先在线试试接口效果?打开 API Playground 即可直接调用测试: https://mairuiapi.com/playground

本文用到 stock_history 接口拉日 K,绘制后用浏览器查看 HTML 交互图。

二、环境准备

本文代码使用 mairui SDK 获取股票数据,安装方法如下:

pip install mairui
pip install matplotlib pyecharts   # 本次新增,可视化依赖

SDK 的完整接口文档与使用说明请查阅 GitHub 仓库:https://github.com/MaiRuiApi/mairui

接口的详细参数说明请查阅官网 API 文档:https://mairuiapi.com/hsdata

运行环境: - Python 3.9+ - mairui SDK 1.0.0 - pandas 2.2+ - matplotlib 3.9+ - pyecharts 2.0+

import os
import mairui

# 证书从环境变量读取(官网注册后获取),不要写死在代码里
api = mairui.Client(os.environ["MAIRUI_LICENCE"])

本文以 600519 贵州茅台为例,区间 2025-07-01 至 2026-07-29 共 263 根日 K。

三、第一步:拉数据 + 算均线

import pandas as pd

def fetch_kline(api, code):
    rows = api.stock_history(code, "d", "n", st="20250701", et="20260729")
    df = pd.DataFrame(rows)
    df["t"] = pd.to_datetime(df["t"])
    df = df.sort_values("t").reset_index(drop=True)
    # 计算 MA5 / MA10 / MA20
    for n in (5, 10, 20):
        df[f"ma{n}"] = df["c"].rolling(n).mean()
    return df

真实输出:

样本: 263 行 x 12 列
起始: 2025-07-01  收盘: 1405.1
终止: 2026-07-29  收盘: 1321.0
区间涨跌幅: -5.99%

⚠️ 格式坑stock_historyst / et 参数是 YYYYMMDD(不带横杠),传 "2025-07-01" 会报参数错误——这是上一篇文章已经踩过、本系列反复出现的经典坑。

四、matplotlib 静态 K 线图(120 行内)

matplotlib 没有现成的"蜡烛图"接口,得自己用矩形画实体、竖线画影线。A股涨红跌绿(与美股相反),所以代码里涨用红、跌用绿:

import matplotlib
matplotlib.use("Agg")   # 无显示器的服务器必加
import matplotlib.pyplot as plt
from matplotlib import rcParams

# 中文字体(Windows 自带 SimHei;Linux 需装 noto-cjk)
rcParams["font.sans-serif"] = ["SimHei"]
rcParams["axes.unicode_minus"] = False

def draw_matplotlib(df, png_path):
    fig, (ax_price, ax_vol) = plt.subplots(
        2, 1, figsize=(14, 8), sharex=True,
        gridspec_kw={"height_ratios": [3, 1]},   # 上面 3 份价格、下面 1 份成交量
    )

    up, down = df[df["c"] >= df["o"]], df[df["c"] < df["o"]]
    up_color, down_color = "#d62728", "#2ca02c"   # 红涨 / 绿跌

    for sub_df, color in ((up, up_color), (down, down_color)):
        # 影线:低-高 之间的竖线
        ax_price.vlines(sub_df["t"], sub_df["l"], sub_df["h"],
                        color=color, linewidth=0.8)
        # 实体:开收差值的矩形
        height = (sub_df["c"] - sub_df["o"]).abs()
        bottom = pd.concat([sub_df["c"], sub_df["o"]], axis=1).min(axis=1)
        ax_price.bar(sub_df["t"], height, bottom=bottom,
                     width=0.6, color=color, edgecolor=color)

    # 均线叠加
    ax_price.plot(df["t"], df["ma5"],  color="#ff7f0e", label="MA5")
    ax_price.plot(df["t"], df["ma10"], color="#1f77b4", label="MA10")
    ax_price.plot(df["t"], df["ma20"], color="#9467bd", label="MA20")
    ax_price.set_title("贵州茅台(600519)  日K · 2025-07~2026-07", fontsize=14)
    ax_price.legend()
    ax_price.grid(alpha=0.3)

    # 成交量子图:按涨/跌染色
    vol_colors = ["#d62728" if c >= o else "#2ca02c"
                  for c, o in zip(df["c"], df["o"])]
    ax_vol.bar(df["t"], df["v"], color=vol_colors, width=0.6)
    ax_vol.grid(alpha=0.3)

    fig.autofmt_xdate()
    fig.tight_layout()
    fig.savefig(png_path, dpi=120, bbox_inches="tight")

实际生成的图(已自验 PASS):

贵州茅台K线图

  • 上图:红色蜡烛=涨、绿色蜡烛=跌;橙色/蓝/紫三条均线
  • 下图:成交量柱按当日涨跌染色
  • 区间 -5.99% 走势清晰可见(2025-09 高点 ~1550 → 2026-06 低点 ~1170)

五、pyecharts 交互式 K 线图

matplotlib 出图漂亮但只能看不能摸——pyecharts 把图渲成 HTML,浏览器里可以缩放、拖动、十字光标看具体值,做回测报告/策略说明时给老板一份交互版比贴 PNG 体验好得多:

from pyecharts import options as opts
from pyecharts.charts import Kline, Bar, Line, Grid

def draw_pyecharts(df, html_path):
    # pyecharts 需要 [日期, 开, 收, 低, 高] 顺序
    kdata = df[["t", "o", "c", "l", "h"]].copy()
    kdata["t"] = kdata["t"].dt.strftime("%Y-%m-%d")
    kline_data = kdata.values.tolist()

    kline = (
        Kline(init_opts=opts.InitOpts(width="1400px", height="600px"))
        .add_xaxis(kdata["t"].tolist())
        .add_yaxis("K线", kline_data,
                   itemstyle_opts=opts.ItemStyleOpts(
                       color="#d62728", color0="#2ca02c",         # A股涨红跌绿
                       border_color="#d62728", border_color0="#2ca02c"))
        .set_global_opts(
            title_opts=opts.TitleOpts(title="贵州茅台(600519)", pos_left="center"),
            xaxis_opts=opts.AxisOpts(type_="category", is_scale=True),
            yaxis_opts=opts.AxisOpts(is_scale=True),
            datazoom_opts=[opts.DataZoomOpts(type_="inside"),  # 鼠标滚轮缩放
                           opts.DataZoomOpts()],               # 下方拖动条
            tooltip_opts=opts.TooltipOpts(trigger="axis", axis_pointer_type="cross"),
        )
    )

    # 均线叠加到 K 线
    line = (
        Line()
        .add_xaxis(kdata["t"].tolist())
        .add_yaxis("MA5",  df["ma5"].fillna(0).round(2).tolist(),  is_smooth=True, symbol="none")
        .add_yaxis("MA10", df["ma10"].fillna(0).round(2).tolist(), is_smooth=True, symbol="none")
        .add_yaxis("MA20", df["ma20"].fillna(0).round(2).tolist(), is_smooth=True, symbol="none")
    )
    kline.overlap(line)

    # 成交量子图
    bar = (Bar()
        .add_xaxis(kdata["t"].tolist())
        .add_yaxis("成交量", df["v"].tolist())
        .set_global_opts(
            xaxis_opts=opts.AxisOpts(type_="category", grid_index=1),
            yaxis_opts=opts.AxisOpts(grid_index=1),
        ))

    # 上下排版:K 线占 60%、成交量占 18%
    grid = (Grid(init_opts=opts.InitOpts(width="1400px", height="800px"))
        .add(kline, grid_opts=opts.GridOpts(pos_left="6%", pos_right="2%", height="60%"))
        .add(bar,   grid_opts=opts.GridOpts(pos_left="6%", pos_right="2%", pos_top="72%", height="18%")))
    grid.render(html_path)

关键功能: - 鼠标滚轮缩放 / 拖动时间区间 - 十字光标显示当根 K 线的 OHLC + 成交量 - 下方 datazoom 拖动条快速跳转区间 - 移动端可触摸

六、完整可运行示例

# -*- coding: utf-8 -*-
"""K 线可视化:matplotlib 静态 + pyecharts 交互。"""
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib import rcParams
import pandas as pd
import mairui
from pyecharts import options as opts
from pyecharts.charts import Kline, Bar, Line, Grid

CODE, NAME = "600519", "贵州茅台"
ST, ET = "20250701", "20260729"

def main():
    api = mairui.Client(os.environ["MAIRUI_LICENCE"])
    df = pd.DataFrame(api.stock_history(CODE, "d", "n", st=ST, et=ET))
    df["t"] = pd.to_datetime(df["t"])
    for n in (5, 10, 20):
        df[f"ma{n}"] = df["c"].rolling(n).mean()

    # matplotlib 静态图
    rcParams["font.sans-serif"] = ["SimHei"]; rcParams["axes.unicode_minus"] = False
    fig, ax = plt.subplots(figsize=(14, 6))
    up, down = df[df["c"] >= df["o"]], df[df["c"] < df["o"]]
    for sub, color in ((up, "#d62728"), (down, "#2ca02c")):
        ax.vlines(sub["t"], sub["l"], sub["h"], color=color, linewidth=0.8)
        h = (sub["c"] - sub["o"]).abs()
        b = pd.concat([sub["c"], sub["o"]], axis=1).min(axis=1)
        ax.bar(sub["t"], h, bottom=b, width=0.6, color=color)
    ax.plot(df["t"], df["ma5"], label="MA5")
    ax.plot(df["t"], df["ma10"], label="MA10")
    ax.plot(df["t"], df["ma20"], label="MA20")
    ax.set_title(f"{NAME}({CODE})  日K"); ax.legend(); ax.grid(alpha=0.3)
    fig.savefig(f"{CODE}_kline.png", dpi=120, bbox_inches="tight")
    plt.close(fig)

    # pyecharts 交互图
    kd = df[["t","o","c","l","h"]].copy()
    kd["t"] = kd["t"].dt.strftime("%Y-%m-%d")
    kline = (Kline(init_opts=opts.InitOpts(width="1400px", height="600px"))
        .add_xaxis(kd["t"].tolist())
        .add_yaxis("K线", kd.values.tolist())
        .set_global_opts(datazoom_opts=[opts.DataZoomOpts(), opts.DataZoomOpts(type_="inside")]))
    line = (Line().add_xaxis(kd["t"].tolist())
        .add_yaxis("MA5",  df["ma5"].fillna(0).round(2).tolist(), symbol="none")
        .add_yaxis("MA10", df["ma10"].fillna(0).round(2).tolist(), symbol="none")
        .add_yaxis("MA20", df["ma20"].fillna(0).round(2).tolist(), symbol="none"))
    kline.overlap(line)
    kline.render(f"{CODE}_kline.html")
    print("OK")

if __name__ == "__main__":
    main()

七、避坑与进阶

  • 坑 1:服务器没显示器 matplotlib 报错。Linux 服务器跑无头环境必须加 matplotlib.use("Agg"),否则 ImportError: cannot load any usable tkinter
  • 坑 2:中文字体乱码方块。Windows 用 "SimHei" / "Microsoft YaHei",Linux 用 "Noto Sans CJK SC",macOS 用 "PingFang SC"画图前一定 rcParams["font.sans-serif"]
  • 坑 3:负号显示成方块rcParams["axes.unicode_minus"] = False 一行解决——matplotlib 默认用 Unicode 负号,部分字体不带这个字符。
  • 坑 4:HTML 文件名含中文。pyecharts 的 render() 不支持中文文件名(截断会变乱码),文件名全用英文+下划线(如 600519_kline.html)。
  • 坑 5:datazoom 缩放不工作。必须设 xaxis_opts=opts.AxisOpts(type_="category", is_scale=True)——type 不是 category 时拖动条无法对应到具体日期。
  • 进阶方向:把多只股票画成"对比图"(每只一条归一化曲线),或加上买卖点标记effect_scatter)——回测报告里一眼就能看到策略实际触发位置。下一篇就讲这个:盘中盯盘脚本,把"画完放着看"变成"画了还能动"——实时拉数据 + 自动刷图 + 触发提醒。

八、总结与延伸

可视化是数据 → 决策的最后一公里。本文双图方案:

  • 静态 PNG → 进报告 / PPT / 公众号,一图胜千言
  • 交互 HTML → 浏览器拖动缩放、自助查询具体值,更适合策略复盘

工程化模块到这里覆盖了"批量拉 → 高效存 → 直观看"的完整链路。下一篇(#19)从"事后看"升级到"盘中盯"——实时行情 + 阈值告警 + 微信/钉钉推送,让策略真的"动起来"。

延伸阅读: - 在线体验更多接口:https://mairuiapi.com/playground - 查看完整 API 文档:https://mairuiapi.com/hsdata - SDK 文档与源码:https://github.com/MaiRuiApi/mairui - 关注公众号获取本系列更新


本文为技术演示,所有数据来自公开股票接口,文中可视化方案不构成投资建议。

QQ 客服 3826425416 咨询时请提供证书号或订单号,便于快速处理
咨询