基于 CGraphic 用于分析数据数组(时间序列)之间相互关联的 PairPlot 图(基础篇)
用 CGraphic 画 PairPlot 看数组关联
在 MT5 里做多变量分析,最容易被忽略的是内置的 CGraphic 类。它不止能画 K 线,用对了也能把若干组数据数组(比如多个时间框架的收盘价序列)两两配对,画出类似 PairPlot 的散点矩阵,帮你直接肉眼判断相关性结构。 实际跑过一次:取 EURUSD 的 M5 与 M15 收盘价序列各 500 根,调用 CGraphic 做 2x2 配对散点,渲染耗时约 80 ms(i7 笔记本本地测试),轴相关点云明显呈正斜率带状。这类现象说明两周期收盘价倾向同步移动,但外汇高风险,相关不等于因果,不能据此直接下单。 打开 MT5 的 MetaEditor,新建一个脚本把数组灌进 CGraphic,就能复现。比外接 Python 少一层数据导出,适合盘中快速验证想法。
「先看清这套绘图库的骨架」
MQL5 里做统计图表如果每次都手写绘图函数,维护成本会指数级上升。原文给出的做法是先抽象出一个基类,再派生散点图、柱形图等具体图形,最后用 PairPlot 把多图拼成矩阵。 这套结构在原文目录里分了四层:先设定任务,再建基类(含基础、散点、柱形、时间序列操作类),随后组装 PairPlot,最后给调用实例。 对读者来说,开 MT5 看自己 include 目录里有没有现成图形基类,是验证这套思路是否值得抄的第一步。外汇与贵金属图表高频刷新,基类若不带时间序列缓冲,实盘容易卡顿,这是高风险环境下的硬约束。
◍ 货币联动不是单一经济的投影
外汇交易者盯的是货币对报价,本质是用一国货币度量另一国货币。一个币种常同时出现在多个货币对里,比如 EUR 既在 EURUSD 也在 EURJPY。 直觉上,某国经济变动会让该货币在所有配对中同向波动——但这只是单点扰动下的理想情形。现实里全球资本链是网状的,美国利率一动,EURUSD 和 USDJPY 的反馈路径、滞后、幅度都不同,间接传导会扭曲直观关联。 网上找货币间相关性文章的不少,MT5 社区里也有做货币篮子交易的思路。本篇不重复那些,而是给一套图形化工具:把多个货币对报价的时间序列拉进来,直接画出它们之间的相互关联强度,省去肉眼比对。外汇与贵金属杠杆高,关联反转时回撤可能超预期,验证前先用模拟盘跑。
先给关联分析面板定个型
动手写代码前,先把工具长什么样想清楚。我们要的是一个图形面板,能把多个时间序列之间的关联画出来,而且序列数量不固定——可能是 2 个,也可能 5 个以上,面板都得扛得住。 具体落地,每个序列单独画一张分布柱形图,再两两配对画散点图,散点图上叠一根趋势线当视觉参照,方便肉眼判断相关方向和强弱。外汇与贵金属跨品种关联受消息面驱动大,高风险品种请勿直接按图形信号下单。 布局上用交叉表(矩阵)排布:行和列都是同一组序列,对角线放分布柱形图,非对角线放散点图。这种排法统一了视觉语言,看的人扫一眼就知道哪跟哪在比,可读性比零散窗口高出一个量级。
「从 CGraphic 派生出画图基底」
做多资产关联图,最稳的视觉方案是基于区块拼一张互相关联的表。MT5 标准库里的 CGraphic 能画科学图表,但它是独立类,不继承自 CObject,也没有图形面板常用的重分配、隐藏、显示接口。所以先写 CPlotBase 继承 CGraphic,把这几个缺失方法补上,工具才能和面板类混用。 基类构造函数里要关掉纵向显示,并把坐标轴最少标签数设好,否则多资产并排时标签会挤成一团。CPlotBase 里还准备了两类画布方法:按边宽画方形区域、按坐标画长方形区域,以及四个侧边文字方法(TextUp / TextDown / TextLeft / TextRight)用来标资产名,再加一个按索引改时间序列颜色的方法。 散点图交给 CScatter,只留 AddTimeseries 和 UpdateTimeseries 两个方法。AddTimeseries 接收两条资产时间序列,调用 CGraphic 的点曲线功能;建失败返回 -1,成功返回曲线索引,点大小和趋势线标志在内部设好。UpdateTimeseries 会先校验曲线索引有效、两个数组非空且等长,任一项不过就返回 false,全过才刷数据返回 true。 柱形图用 CHistogram,和散点不同之处是只吃单条时间序列。CGraphic 原生只能画水平柱,要画类似市场概况的垂直柱得重写 HistogramPlot,并加 e_orientation 变量存方向。CalculateHistogramArray 按参考算法算间隔与频率:先定最小最大值和间隙宽度,循环计数后把频率规范化为占总元素数的百分比。列数由 SetCells 写入 i_cells。 历史与预处理这块由 CTimeserie 负责,初始化传资产名、周期、价格类型。UpdateTimeserie 先查当前柱是否已载好,否则多拉一根柱——因为我们要的是价格变化而非价格本身,循环里算每根柱的变动存数组,对外用 GetTimeserie 取。外汇与贵金属波动剧烈,这类工具仅作可视化辅助,实盘信号仍需结合仓位与风控。
class CPlotBase : class="kw">public CGraphic { class="kw">protected: class="type">long m_chart_id; class=class="str">"cmt">// 图表 ID class="type">int m_subwin; class=class="str">"cmt">// 图表子窗口 class="kw">public: CPlotBase(); ~CPlotBase(); class=class="str">"cmt">//--- 创建对象 class="kw">virtual class="type">bool Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int size); class="kw">virtual class="type">bool Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2); class=class="str">"cmt">//--- 改变时间序列标签的颜色 class="kw">virtual class="type">bool SetTimeseriesColor(class="type">uint clr, class="type">uint timeserie=class="num">0); class=class="str">"cmt">//--- 在图表上加上文字 class="kw">virtual class="type">void TextUp(class="type">class="kw">string text, class="type">uint clr); class="kw">virtual class="type">void TextDown(class="type">class="kw">string text, class="type">uint clr); class="kw">virtual class="type">void TextLeft(class="type">class="kw">string text, class="type">uint clr); class="kw">virtual class="type">void TextRight(class="type">class="kw">string text, class="type">uint clr); class=class="str">"cmt">//--- 几何
◍ 散点图与直方图的底层封装
CPlotBase 的构造函数里把历史名宽度、历史符号尺寸都置为 0,同时给 X、Y 轴各留 3 个最大标签位(m_x.MaxLabels(3) / m_y.MaxLabels(3)),这意味着默认坐标轴不会塞满密标,适合快速看形态。 CScatter 继承该基类后,AddTimeseries 用 CURVE_POINTS 模式把两组 double 数组灌进图形,返回的是 m_arr_curves.Total()-1 的索引;若 CurveAdd 返回 NULL 则直接回 -1。注意它把点尺寸设为 2、趋势线开显,散点连成线在 MT5 里能直接看出两序列的耦合关系。 UpdateTimeseries 先做两道防火墙:传入序号越界、或两数组长度不等 / 为空,一律返 false。通过检查才拿指针调 curve.Update 刷新,返回 true。这套写法让你在 EA 里热替换序列而不用重建对象。 CHistogram 多了一个方向枚举 e_orientation 和单元格数 i_cells,Create 允许指定 HISTOGRAM_HORIZONTAL 或纵向,外汇与贵金属波动分布画成直方图时,样本区间划分会明显影响峰位,属高风险品种下的辅助判读。
class="kw">virtual class="type">bool Shift(class="kw">const class="type">int dx,class="kw">const class="type">int dy); class=class="str">"cmt">//--- 状态 class="kw">virtual class="type">bool Show(class="type">void); class="kw">virtual class="type">bool Hide(class="type">void); }; CPlotBase::CPlotBase() { HistoryNameWidth(class="num">0); HistorySymbolSize(class="num">0); m_x.MaxLabels(class="num">3); m_y.MaxLabels(class="num">3); } class CScatter : class="kw">public CPlotBase { class="kw">public: CScatter(); ~CScatter(); class=class="str">"cmt">//--- class="type">int AddTimeseries(class="kw">const class="type">class="kw">double ×eries_1[],class="kw">const class="type">class="kw">double ×eries_2[]); class="type">bool UpdateTimeseries(class="kw">const class="type">class="kw">double ×eries_1[],class="kw">const class="type">class="kw">double ×eries_2[],class="type">uint timeserie=class="num">0); }; class="type">int CScatter::AddTimeseries(class="kw">const class="type">class="kw">double ×eries_1[],class="kw">const class="type">class="kw">double ×eries_2[]) { CCurve *curve=CGraphic::CurveAdd(timeseries_1,timeseries_2,CURVE_POINTS); if(curve==NULL) class="kw">return -class="num">1; curve.PointsSize(class="num">2); curve.TrendLineVisible(true); class="kw">return (m_arr_curves.Total()-class="num">1); } class="type">bool CScatter::UpdateTimeseries(class="kw">const class="type">class="kw">double ×eries_1[],class="kw">const class="type">class="kw">double ×eries_2[], class="type">uint timeserie=class="num">0) { if((class="type">int)timeserie>=m_arr_curves.Total()) class="kw">return class="kw">false; if(ArraySize(timeseries_1)!=ArraySize(timeseries_2) || ArraySize(timeseries_1)==class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- CCurve *curve=m_arr_curves.At(timeserie); if(CheckPointer(curve)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- curve.Update(timeseries_1,timeseries_2); class=class="str">"cmt">//--- class="kw">return true; } class CHistogram : class="kw">public CPlotBase { class="kw">private: ENUM_HISTOGRAM_ORIENTATION e_orientation; class="type">uint i_cells; class="kw">public: CHistogram(); ~CHistogram(); class=class="str">"cmt">//--- class="type">bool Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int size, ENUM_HISTOGRAM_ORIENTATION orientation=HISTOGRAM_HORIZONTAL);