资讯详情 用Pandas和Matplotlib绘制K线均线图:金叉死叉标注与性能优化
📅 2026/10/16 4:20:04
做策略复盘的时候我有个习惯先把K线图拉出来再往上叠几条移动平均线。很多信号在数字表格里看不出问题一放到图上均线发散、缠绕、拐头全部一目了然。这篇继续量化投资绘图系列讲清楚怎么用pandas加matplotlib把移动平均线画到价格图上包括数据预处理、多周期均线叠加、金叉死叉标注以及数据量大了之后如何处理绘图性能。适合已经把基础K线图跑通、想进一步把图表做扎实的朋友。我会尽量按我日常复盘时的真实顺序来写先想清楚均线画出来要干什么再处理数据和计算然后落到画图代码最后讲优化和封装经验。每一步都会说明为什么这么做而不是只给一段能跑通的代码。1. 为什么要在K线图上叠加均线画之前先想清楚1.1 均线不是预测线而是“市场共识的移动平均”移动平均线的本质并不神秘它就是一段时间收盘价的算术平均值。比如MA20就是把最近20个交易日的收盘价加总再除以20。这个值每天都随着新数据进来而更新旧数据被逐步挤出去所以叫“移动平均”。但很多人会把均线当成预测工具觉得“价格站上MA20就会涨”这是本末倒置。均线本身不预测未来它只是把价格序列里的短期噪声抹平让趋势方向更清楚。就像开车时仪表盘显示瞬时速度和里程平均速度一样你判断这段路堵不堵看平均速度比看瞬时速度靠谱得多。均线在K线图上起的就是这个作用把一根根K线的毛刺过滤掉露出相对稳定的趋势骨架。我自己在复盘时第一眼看的往往不是单根K线而是几条均线的排列。如果短期均线在长期均线上方、两条线距离拉大说明市场处于趋势加速段如果几条均线粘在一起、上下缠绕说明市场在震荡这时候策略里的趋势类信号大概率会频繁止损。这个判断过程画在图上比写在表格里直观太多。1.2 复盘和策略验证中MA图的三个用途移动平均线画到图上有三个常见用途理解它们有助于决定画什么参数、加什么标注。第一个用途是看趋势阶段。均线向上、价格在均线上方运行通常视为多头阶段反过来就是空头阶段。这个分类虽然粗糙但非常稳定。我在评估一个趋势策略时会把K线和MA20、MA60一起画出来看策略的盈利贡献主要来自哪些时间段以及这些时间段里均线排列是否符合预期。第二个用途是验证入场离场逻辑。如果你的策略是“价格上穿MA20做多、下穿MA20平仓”那复盘时必须能看到每一次穿越发生在什么位置。有些策略回测报告很漂亮但把信号点标到图上才发现大部分盈利来自某段单边行情其余时间全在来回打脸。这种问题不画图很难发现。第三个用途是观察均线缠绕与发散。均线从缠绕走向发散往往对应一波行情的启动从发散重新走向缠绕则意味着趋势进入休整。这类形态特征用程序自动识别很容易误判但人眼看图非常快。所以画MA图不是给自己找活干而是给策略分析提供一条捷径。1.3 本篇实现的前提会画基础K线图前一篇我们已经把最基础的K线图跑通了包括数据读取、日期坐标轴、蜡烛图绘制。这篇的核心是在已有K线图的基础上叠加折线所以如果你已经能画出一张干净的蜡烛图接下来只是多几条线的问题。如果你用的是第三方K线图库不少也支持直接叠加快照折线。但我还是从matplotlib讲起因为它的控制粒度最细可以自由控制线型、颜色、标注而且后续无论接回测系统还是做数据报表都能保持一致的技术栈。本篇所有代码我都按“pandas做数据处理、matplotlib做图形渲染”的思路组织不依赖任何额外复杂组件。2. 绘制MA之前的数据准备从裸数据到可计算的MA序列2.1 数据形态与清洗时间索引和缺失值画均线的第一步不是直接算mean而是把数据整理成能用于时间序列分析的结构。我常用的数据源导出是CSV至少包含日期、开高低收和成交量六列。读取时的关键参数是parse_dates否则日期会被当成字符串后面的坐标轴格式化和按时间切片都会出问题。import pandas as pd df pd.read_csv(kline_demo.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 检查数据范围和时间间隔 print(df[date].min(), df[date].max()) print(df[date].diff().value_counts().head())这里有两个细节值得注意。一是必须按日期排序否则后面的rolling计算会得到乱序均值二是要检查交易日的间隔是否规律比如日线数据里混入了周末或者节假日diff结果会出现异常。如果数据里有NaN最好先做dropna或ffill否则rolling算出来的均线会断断续续画出来就是一条条短线段看着非常难受。清洗完成后我习惯把日期列保留在原位不强行改成索引。因为后面画图时ax.plot(x, y)里x直接用日期列也能工作而保留普通列在切片和合并时更灵活。2.2 周期参数怎么选5、10、20、60背后的规律移动平均线的周期选择没有绝对标准但A股、港股、美股期货等市场共同形成了几个常用参数因为它们对应着自然的交易周期。我把常见参数和含义整理成下表方便大家对照。参数对应周期常用场景MA5一周超短线跟踪噪声偏大MA10两周短线趋势确认MA20一个月波段通用的“生命线”MA30一个半月中期趋势过滤MA60一个季度大周期方向判断MA120半年牛熊分界参考我复盘时最常用的是“2060”双均线组合。MA20代表一个月的市场持仓成本MA60代表一个季度的成本两者一快一慢。当MA20在MA60上方时中期趋势偏多当MA20下穿MA60时预示着更长时间级别的转弱。如果是做日内或分钟级别的策略参数会整体缩小比如5分钟K线用MA15、MA30。原则是一致的让短期均线能快速反映价格变化长期均线又能过滤掉一部分噪声。不要把参数给得太宽比如日线直接上MA250画出来虽然平滑但反应太钝复盘时很难和具体入场点对应起来。2.3 rolling计算时的NaN、min_periods和未来信息pandas计算移动平均非常直接rolling(window).mean()一行就能搞定。但这里埋着两个新手很容易踩的坑。第一个坑是NaN。数据最开始只有20根K线时MA20是没有值的。如果直接把整个序列的MA画到图上前面一段会出现空档。对于绘图来说这种空档通常可以接受但如果把计算范围截断到最近500根且这500根里恰好包含数据起点空档就会很明显。解决方法是计算时设置min_periodsdf[ma20] df[close].rolling(20, min_periods5).mean()min_periods5表示哪怕只有5根有效数据也先算出均值不足5根则为NaN。这样画出来的线会从第5根K线开始而不是第20根。不过要注意这种处理后的均线在数据不足时并不稳定我会把这些过早的段落在绘图时单独处理或者干脆切片掉。第二个坑是前视偏差。rolling(20).mean()的窗口包含当前这一根K线也就是说如果你在当日收盘后看这张图MA20包含当日收盘价这是合理的。但如果你用这个MA写回测策略并且用“MA20上穿MA60”作为次日开盘的入场信号那计算时就应该用shift(1)把信号点滞后一根K线否则等于使用了未来的信息。# 回测信号用昨日收盘算出的MA df[ma20_prev] df[close].rolling(20).mean().shift(1)画图时我一般不做shift因为我们要看的是“站在当下看到的均线状态”。但如果策略信号来自MA穿越画图时最好也画一条shift后的信号均线和实际成交位置对齐否则图上标的进场点会往后偏一天复盘时总感觉对不上。3. 用matplotlib把三条均线画上K线图核心代码与细节3.1 叠加折线的正确坐标轴方式主图共享y轴画均线的直觉做法是再创建一个坐标轴用twinx()画折线这样均线和K线各用各的纵轴。我早期也这么干后来发现这是一个很糟糕的习惯。twinx()会生成一个独立的右侧纵轴如果K线价格区间是5到10而MA20区间是7到8那么右侧坐标轴会拉伸或压缩导致均线的斜率看起来比实际陡峭或平缓很多。趋势判断最依赖的就是均线角度一旦纵轴比例失真图上所有斜坡和拐点都会被误读。正确的做法是让均线和K线共享同一个纵轴也就是画在同一个坐标轴对象上。均线的数值再接近、再重合它和价格本来就在同一个数量级完全不需要副轴。只有像成交量、RSI、MACD这种量纲完全不同的指标才需要放到副图或副轴。fig, ax plt.subplots(figsize(14, 7)) # 假设 draw_candles 是上一篇封装好的蜡烛图函数 draw_candles(ax, plot_df) ax.plot(plot_df[date], plot_df[ma5], color#f7b500, linewidth1.2, linestyle--, labelMA5) ax.plot(plot_df[date], plot_df[ma20], color#e8833a, linewidth1.4, labelMA20) ax.plot(plot_df[date], plot_df[ma60], color#5b8ff9, linewidth1.6, labelMA60) ax.legend(locupper left) ax.grid(alpha0.3) plt.show()3.2 中文字体、负号与日期轴格式化两张图的常见翻车现场做量化图最容易翻车的不是画线本身而是中文和日期。中文字体问题是每个matplotlib用户都会遇到一次的默认字体不支持中文时图例里“MA5”没问题一旦你写“金叉”“死叉”“移动平均线”出来的就是一个个方格。解决方式很固定在脚本开头统一设置两组参数import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] Falseaxes.unicode_minus用来解决第二个坑负号显示为方块。比如坐标轴范围出现-100如果不关掉unicode负号显示的就是乱码。很多人的图只有负号出错就是因为只设置了字体没关这个开关。日期轴是另一个高频问题。日线数据跨度大时如果让matplotlib自动分配刻度标签常常挤成一片或者一个月才标一个日期。我常用的做法是手动设置主要刻度和格式import matplotlib.dates as mdates ax.xaxis.set_major_locator(mdates.MonthLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) plt.gcf().autofmt_xdate()MonthLocator让每个月初出现一个刻度DateFormatter把刻度显示成“2024-03”的形式最后autofmt_xdate()自动旋转日期标签防止重叠。这样画出来的图横轴既不太密也不太疏。3.3 颜色、线宽和图例让一屏信息能一眼读出来均线一旦多起来最容易出现的问题不是画不出而是画出来像一团纠缠的毛线。我给自己定了一套固定规则所有MA图都按这个规则来时间长了扫一眼就知道哪条是哪条。MA5用浅橙色虚线线宽1.2MA20用橙色实线线宽1.4MA60用蓝色实线线宽1.6。K线实体用红绿表示涨跌透明度0.9到1。这样K线负责当前价格细节均线负责趋势参照视觉层级很清楚。图例放在左上角因为右侧往往是价格刚走完的最新区最容易产生视觉重点。如果图里有额外标注比如金叉死叉的箭头我也会在图例里补上对应的标记名称。注意均线的颜色尽量不用和K线涨跌色一样的红绿色否则你会不断把均线当成K线的一部分看图时产生混乱。4. 进阶标注一金叉死叉、均线斜率和多周期背离的标绘4.1 金叉死叉事件的捕捉与标注画完均线之后大家最想标注的就是金叉和死叉。金叉指短期均线上穿长期均线死叉指短期均线下穿长期均线。这两个概念本身很简单但代码实现前要先把“上穿”翻译成程序语言。我用两条均线的差值来做判断。当差值从小于等于0变为大于0就是上穿从大于等于0变为小于0就是下穿。fast df[ma20] slow df[ma60] diff fast - slow cross_up (diff 0) (diff.shift(1) 0) cross_down (diff 0) (diff.shift(1) 0)然后把这些点画到主图上。金叉我标在当日K线最低点的下方用一个向上的红色三角箭头死叉标在最高点上方用向下的绿色三角箭头。在图上它们不会干扰K线主体又能一眼看出来。ax.scatter(df[date][cross_up], df[low][cross_up] * 0.98, marker^, s60, color#d1442f, label金叉, zorder5) ax.scatter(df[date][cross_down], df[high][cross_down] * 1.02, markerv, s60, color#2f9e5b, label死叉, zorder3)如果复盘时间跨度长金叉死叉会非常多一张图上标几十个点观感很差。我通常加一个过滤条件相邻标注之间至少间隔10根K线只保留每个过滤窗口内的第一次穿越。这样图面干净表达的信息也更集中不然一堆箭头叠在一起等于没有箭头。4.2 均线斜率方向的视觉表达把“趋势变缓”画出来价格穿越均线只能说明方向不能说明力度。同样是金叉有的金叉之后均线快速发散有的金叉之后两条线又迅速粘合。直接看数值很难区分所以我常把均线按斜率着色让“趋势变强变弱”这件事可视化。实现思路是取最近一段均线序列做线性拟合用拟合系数衡量斜率。代码如下以MA20为例import numpy as np def slope_ma(series, window20): return series.rolling(window).apply( lambda x: np.polyfit(range(window), x, 1)[0], rawTrue ) df[ma20_slope] slope_ma(df[ma20], 20) df[ma20_up] df[ma20_slope] 0算出斜率正负后可以把MA20拆成两段分别绘制斜率大于0时用暖色小于0时用冷色。这样图上会直接看到均线颜色的变化斜率越大颜色越深趋势强度一目了然。这段代码唯一的问题是rolling.apply对全量数据计算比较慢几万根K线会明显卡顿。建议先在最近500根切片上算或者只对长周期均线做这个可视化短周期均线噪声太大斜率正负跳来跳去画出来意义不大。4.3 与成交量等副图指标的位置划分一主一辅的网格布局如果这一篇只画均线而不带成交量复盘时总觉得少了点东西。很多关键位置的均线穿越配合成交量放大才更有解释力。我的常规布局是上方主图画K线和均线下方副图画成交量共用x轴。fig, (ax1, ax2) plt.subplots( 2, 1, sharexTrue, figsize(14, 9), gridspec_kw{height_ratios: [3, 1]} ) # ax1 画K线和均线代码同前 # ax2 画成交量 colors [#d1442f if c o else #2f9e5b for o, c in zip(plot_df[open], plot_df[close])] ax2.bar(plot_df[date], plot_df[volume], colorcolors, alpha0.6)这里主图与副图不要画成两个独立坐标轴的上下拼图而是通过sharexTrue共享横轴这样缩放或平移时两张图始终对齐复盘时能准确看到某个放量K线对应主图上哪个位置。副图一般不需要设置日期刻度x轴刻度交给主图或最下面一个子图统一管理即可。如果还想放MACD或者RSI我建议不要无限往下堆子图一屏最多放一主一辅再多的数据视觉负荷会迅速上升反而失去了画图的效率价值。5. 数据量大时MA图的性能优化切片、降采样与矢量化取舍5.1 全量绘图的三个卡点dates解析、图形元素和文件体积当你第一次把日线图换成5分钟甚至1分钟数据时会发现代码还是那份代码但画图时间从几秒变成几十秒保存的文件也大到无法分享。这不是电脑性能差而是绘图逻辑没跟着数据量走。全量绘图通常卡在三个地方。第一是日期解析几十万个日期字符串转换成matplotlib的数值坐标需要时间。第二是图形元素数量500根K线还没什么感觉但5万根K线意味着5万根影线加5万根实体再加上三条均线SVG文件可能直奔几十MB。第三是滚动窗口计算特别是加了斜率之后每个窗口做一次回归计算量直线上升。我实际踩过一个大坑用1分钟数据画某标的三个月的走势约3万根K线直接跑全量笔记本风扇狂转最后图片保存成SVG有几十MB。从那以后我再也没有无脑全量画图。5.2 常用“先缩再画”只画最近500根K线但MA全部量计算解决大数据量绘图最有效的方法就是切片。复盘时你真正关注的往往是最近一段时间比如最近250个交易日或最近500根分钟K线而不是把上市以来所有数据全部铺在图上。切片有一个容易翻车的地方如果先切片再算MA比如只取最近500根然后用这500根数据计算MA60那么前面60根K线的MA60全部是NaN图上前面一段出现空缺面目全非。正确做法是全量数据算好MA再做可视化切片# 全量计算MA df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() df[ma60] df[close].rolling(60).mean() # 只取最近500根用于画图 plot_df df.iloc[-500:].copy()这样最后一个切片点的MA60包含了切片之前更早的数据图上没有任何空缺信息也完整。这个“先算后切”的顺序看起来微不足道但直接影响图的质量。如果连最近500根都嫌多比如分钟数据我会直接画最近200根。配合5.3的保存方式一秒钟出图完全没有压力。5.3 保存图片PNG适合分享、SVG适合文档以及rasterized绘图完成后保存格式也对体验影响很大。PNG是位图适合贴到聊天窗口或报告里文件小、通用但放大后会变模糊。SVG是矢量图放大不糊适合放在文档里做细节标注但在K线数量大时文件会膨胀得离谱而且很多看图工具对SVG支持不佳。我的折中方案是常规分享用PNG尺寸用dpi150清晰度和文件大小都能接受。fig.savefig(ma_chart.png, dpi150, bbox_inchestight)如果确实需要矢量图同时又要控制文件体积可以用rasterizedTrue参数。它的含义是这个图层在输出PDF或SVG时被栅格化为位图但坐标轴、标签、图例等文字部分保持矢量。这样文件体积大幅下降文字放大不模糊曲线部分虽然放大后会有像素感但在复盘场景里完全够用。ax.plot(plot_df[date], plot_df[ma60], color#5b8ff9, linewidth1.6, rasterizedTrue)我测试过同一张5000根K线的图SVG全矢量约20MBPNG 150dpi约300KBrasterized的PDF约500KB。文章插图、复盘分享基本都用PNG只有要交付印刷或嵌入高质量文档时才用rasterized的PDF。6. 从“画得出来”到“画得好用”我的几条实操习惯6.1 把MA绘图封装成函数一套配色存成字典技术复盘不能每次从头写代码。我现在把MA绘图封装成一个固定函数所有项目都调用同一个入口保证输出风格一致也方便团队复用。MA_STYLE { 5: dict(color#f7b500, linewidth1.2, linestyle--), 20: dict(color#e8833a, linewidth1.4), 60: dict(color#5b8ff9, linewidth1.6), } def plot_ma(df, date_coldate, ma_windows(5, 20, 60), tail500, markersTrue): plot_df df.iloc[-tail:].copy() fig, ax1 plt.subplots(figsize(14, 8)) # 画K线、均线、金叉死叉 ... return fig, ax1这里有两个设计细节。第一个是把MA_STYLE单独抽成字典新增周期时只要加一行配置不用改动绘图逻辑。第二个是tail参数作为显式开关同一份全量数据想画最近50根就传50想画最近500根就传500不用复制代码。封装完之后“快速拉一张带均线的图”变成一行代码的事。我会把plot_ma放在一个公共模块里所有策略研究共用避免每个人各写各的配色五花八门。6.2 在图上自动写参数区间、均线周期防止三个月后看裸图失忆我吃过一个亏几个月前存了一张很漂亮的均线图当时觉得信息都在脑子里过段时间再打开完全想不起来这张图是哪个标的、哪段时间、用的什么参数。后来我发现一个轻量但极好用的习惯——在图上写参数。text fMA{ma_windows} {plot_df[date].min():%Y-%m-%d} ~ {plot_df[date].max():%Y-%m-%d} ax1.text(0.02, 0.96, text, transformax1.transAxes, fontsize9, verticalalignmenttop, bboxdict(facecolorwhite, alpha0.7, edgecolornone))用transformax1.transAxes把文本位置固定在图的左上角不会因为缩放而跑掉。白底半透明框保证文字在K线密集区域也清晰可读。保存文件名我也会带上日期区间比如ma_20230101-20230630.png这样以后找图时按文件名就能定位。6.3 我踩过的三个画图小坑与对应方案最后说三个我在实际画图过程中踩得最深的小坑每一个都花了不少时间排查。第一个坑是中文负号变方块。这个在3.2提过不过我这里再强调一次axes.unicode_minus务必设为False。很多人只装字体不关负号结果坐标轴刻度里的-10、-20全部变成方框图发出去非常尴尬。第二个坑是鼠标缩放后标注和均线消失。如果你用plt.show()做交互式查看缩放或平移时ax.scatter标注和某些折线会突然不见。原因是缩放触发了坐标轴范围变化而部分图层在zorder或绘图顺序上没有跟随更新。我现在的做法是交互式查看用book这种依赖事件重绘的函数或者干脆不交互所有视图范围一次性设置好需要看局部就调整xlim再保存图片。第三个坑是没有处理复权数据。用未复权的价格画MA除权除息日会出现价格跳空均线也会跟着“假摔”或“假突破”趋势形态全变。画任何需要连续趋势判断的图我都先用前复权数据。复盘图的价值在于观察趋势和位置而不是记录真实成交价因此复权处理必不可少。这些坑并不高级但每一个都可能导致你对着图做出错误判断。做量化研究图必须是可靠的输入画图代码需要像策略代码一样被认真对待。从“叠加三条线”到“一张能直接支撑复盘决策的图表”关键不在于掌握多少函数而在于想清楚每条线、每个标注在回答什么问题。我现在的日常工作流已经离不开这套MA绘图组件希望这篇文章能把你在绘图上花掉的时间压缩下来把更多精力放回策略本身。