基于 CGraphic 用于分析数据数组(时间序列)之间相互关联的 PairPlot 图·进阶篇
(2/3)· 当多个货币对报价塞进同一面板,散点加柱形如何不糊成一团
「直方图类的统计内核怎么写」
在 MT5 里做自定义直方图,核心不是画图而是先把数据分桶。下面这段 CHistogram 的成员声明给出了接口骨架:Create 负责在指定图表子窗口按坐标建对象,AddTimeserie 灌入序列,UpdateTimeserie 按索引刷新,SetCells 直接改桶数 i_cells。 CalculateHistogramArray 有个硬约束容易踩坑:样本量必须 ≥ 桶数 × 10,否则直接返回 false。比如 i_cells=20 时,data 长度至少得 200,否则直方图不绘制——这能避免小样本把分布画成噪声。 分桶逻辑本身不复杂:先取数组极小极大值得 range,width=range/i_cells;间隔中心取 minv+(i+0.5)*width,最后把频率乘 100.0/size 归一成百分比。末段 HistogramPlot 从曲线取 Y 值喂给该函数,失败就提前 return,直方图宽度至少锁 2 像素防不可见。 外汇与贵金属波动聚集,用这类直方图看回报分布时须记高风险:样本区间选错,分布形态可能完全误导仓位判断。
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, ENUM_HISTOGRAM_ORIENTATION orientation=HISTOGRAM_HORIZONTAL); class="type">int AddTimeserie(class="kw">const class="type">class="kw">double ×erie[]); class="type">bool UpdateTimeserie(class="kw">const class="type">class="kw">double ×erie[],class="type">uint timeserie=class="num">0); class="type">bool SetCells(class="type">uint value) { i_cells=value; } class="kw">protected: class="kw">virtual class="type">void HistogramPlot(CCurve *curve); class="type">bool CalculateHistogramArray(class="kw">const class="type">class="kw">double &data[],class="type">class="kw">double &intervals[],class="type">class="kw">double &frequency[], class="type">class="kw">double &maxv,class="type">class="kw">double &minv); }; class="type">bool CHistogram::CalculateHistogramArray(class="kw">const class="type">class="kw">double &data[],class="type">class="kw">double &intervals[],class="type">class="kw">double &frequency[], class="type">class="kw">double &maxv,class="type">class="kw">double &minv) { class="type">int size=ArraySize(data); if(size<(class="type">int)i_cells*class="num">10) class="kw">return (class="kw">false); minv=data[ArrayMinimum(data)]; maxv=data[ArrayMaximum(data)]; class="type">class="kw">double range=maxv-minv; class="type">class="kw">double width=range/i_cells; if(width==class="num">0) class="kw">return class="kw">false; ArrayResize(intervals,i_cells); ArrayResize(frequency,i_cells); class=class="str">"cmt">//--- 设置间隔中心 for(class="type">uint i=class="num">0; i<i_cells; i++) { intervals[i]=minv+(i+class="num">0.5)*width; frequency[i]=class="num">0; } class=class="str">"cmt">//--- 填充间隔和频率 for(class="type">int i=class="num">0; i<size; i++) { class="type">uint ind=class="type">int((data[i]-minv)/width); if(ind>=i_cells) ind=i_cells-class="num">1; frequency[ind]++; } class=class="str">"cmt">//--- 把频率规范化为百分比 for(class="type">uint i=class="num">0; i<i_cells; i++) frequency[i]*=(class="num">100.0/(class="type">class="kw">double)size); class="kw">return (true); } CHistogram::HistogramPlot(CCurve *curve) { class="type">class="kw">double data[],intervals[],frequency[]; class="type">class="kw">double max_value, min_value; curve.GetY(data); if(!CalculateHistogramArray(data,intervals,frequency,max_value,min_value)) class="kw">return; class=class="str">"cmt">//--- 柱形图参数 class="type">int histogram_width=fmax(curve.HistogramWidth(),class="num">2); class=class="str">"cmt">//--- 检查
直方图坐标轴的两种摆法
在 MT5 自定义指标里画频率直方图,先得防住空数组:若 frequency 或 intervals 长度为 0,直接 return,否则后面取 ArrayMaximum 会踩坑。 横向和纵向两种朝向,本质只是把 X/Y 轴职责对调。横向时 X 轴吃 intervals 的最小最大值,标签只留 3 个、格式 "%.0f";Y 轴从 0 到频率峰值,格式 "%.2f"。纵向反过来,Y 轴承载区间、X 轴承载频率。 画图前用 m_height-m_down 和 m_width-m_right 算出生图原点,再用 ScaleY(0.0)/ScaleX(0.0) 拿到零值屏幕坐标。循环里每根柱子先过 MathIsValidNumber 校验,无效就 continue 跳过。 横向柱体以区间坐标 xc 为中心向两侧各展 histogram_width/2,纵向则从 yc 向上下展。柱底默认贴 originalY,若 yc0 在有效范围内则贴 yc0。最后 FillRectangle 按计算出的对角坐标填充曲线色 clr。
if(ArraySize(frequency)==class="num">0 || ArraySize(intervals)==class="num">0) class="kw">return; class=class="str">"cmt">//--- class="kw">switch(e_orientation) { case HISTOGRAM_HORIZONTAL: m_y.AutoScale(class="kw">false); m_x.Min(intervals[ArrayMinimum(intervals)]); m_x.Max(intervals[ArrayMaximum(intervals)]); m_x.MaxLabels(class="num">3); m_x.ValuesFormat("%.0f"); m_y.Min(class="num">0); m_y.Max(frequency[ArrayMaximum(frequency)]); m_y.ValuesFormat("%.2f"); class="kw">break; case HISTOGRAM_VERTICAL: m_x.AutoScale(class="kw">false); m_y.Min(intervals[ArrayMinimum(intervals)]); m_y.Max(intervals[ArrayMaximum(intervals)]); m_y.MaxLabels(class="num">3); m_y.ValuesFormat("%.0f"); m_x.Min(class="num">0); m_x.Max(frequency[ArrayMaximum(frequency)]); m_x.ValuesFormat("%.2f"); class="kw">break; } class=class="str">"cmt">//--- CalculateXAxis(); CalculateYAxis(); class=class="str">"cmt">//--- 计算 y 的起点 class="type">int originalY=m_height-m_down; class="type">int originalX=m_width-m_right; class="type">int yc0=ScaleY(class="num">0.0); class="type">int xc0=ScaleX(class="num">0.0); class=class="str">"cmt">//--- 取得曲线颜色 class="type">uint clr=curve.Color(); class=class="str">"cmt">//--- 画图 for(class="type">uint i=class="num">0; i<i_cells; i++) { class=class="str">"cmt">//--- 检查坐标 if(!MathIsValidNumber(frequency[i]) || !MathIsValidNumber(intervals[i])) class="kw">continue; if(e_orientation==HISTOGRAM_HORIZONTAL) { class="type">int xc=ScaleX(intervals[i]); class="type">int yc=ScaleY(frequency[i]); class="type">int xc1 = xc - histogram_width/class="num">2; class="type">int xc2 = xc + histogram_width/class="num">2; class="type">int yc1 = yc; class="type">int yc2 = (originalY>yc0 && yc0>class="num">0) ? yc0 : originalY; class=class="str">"cmt">//--- if(yc1>yc2) yc2++; else yc2--; class=class="str">"cmt">//--- m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr); } else { class="type">int yc=ScaleY(intervals[i]); class="type">int xc=ScaleX(frequency[i]); class="type">int yc1 = yc - histogram_width/class="num">2; class="type">int yc2 = yc + histogram_width/class="num">2; class="type">int xc1 = xc;
◍ 画布坐标对齐与序列类封装
在 MT5 自定义指标里做图形叠加时,经常要先算好矩形两端的 x 坐标再交给画布。下面这段逻辑就是典型处理:当原始坐标 originalX 大于基准 xc0 且 xc0 为正时取 xc0,否则回落到 originalX;随后比较 xc1 与 xc2,前者大就给 xc2 加 1,否则减 1,最后用 FillRectangle 把区块涂满。 xc2 = (originalX>xc0 && xc0>0) ? xc0 : originalX; if(xc1>xc2) xc2++; else xc2--; m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr); 这一处 ±1 的微调很关键,它能避免两根坐标线重合时矩形宽度为 0 导致什么都不显示。实盘加载到 XAUUSD 的 M5 图,若 xc0 取 20、originalX 取 35,xc2 会落到 20 再按 xc1 大小偏移 1 像素。 真正可复用的是把行情数据收进 CTimeserie 类。它继承 CObject,内部存了品种、周期、应用价格和双精度数组 d_timeserie[],对外暴露 SetBars / Symbol / Timeframe / Price 四个 setter,每次改品种或周期都会把 dt_last_load 清 0,强制下次 UpdateTimeserie 重新拉数。 GetTimeserie 用 ArrayCopy 把内部数组拷出去,返回值大于 0 才算拿到数据。你在 EA 里调这个类,只要 Create("XAUUSD",PERIOD_M15,PRICE_CLOSE) 后周期切到 PERIOD_H1,dt_last_load 变 0,旧缓存不会污染新序列——外汇和贵金属杠杆高,重拉数据前务必确认品种权限已开通。
class="type">int xc2 = (originalX>xc0 && xc0>class="num">0) ? xc0 : originalX; class=class="str">"cmt">//--- if(xc1>xc2) xc2++; else xc2--; class=class="str">"cmt">//--- m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr); } } class=class="str">"cmt">//--- } class CTimeserie : class="kw">public CObject { class="kw">protected: class="type">class="kw">string s_symbol; ENUM_TIMEFRAMES e_timeframe; ENUM_APPLIED_PRICE e_price; class="type">class="kw">double d_timeserie[]; class="type">int i_bars; class="type">class="kw">datetime dt_last_load; class="kw">public: CTimeserie(class="type">void); ~CTimeserie(class="type">void); class="type">bool Create(class="kw">const class="type">class="kw">string symbol=NULL, class="kw">const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE); class=class="str">"cmt">//--- 改变时间序列的设置 class="type">void SetBars(class="kw">const class="type">int value) { i_bars=value; } class="type">void Symbol(class="type">class="kw">string value) { s_symbol=value; dt_last_load=class="num">0; } class="type">void Timeframe(ENUM_TIMEFRAMES value) { e_timeframe=value; dt_last_load=class="num">0; } class="type">void Price(ENUM_APPLIED_PRICE value) { e_price=value; dt_last_load=class="num">0; } class=class="str">"cmt">//--- class="type">class="kw">string Symbol(class="type">void) { class="kw">return s_symbol; } ENUM_TIMEFRAMES Timeframe(class="type">void) { class="kw">return e_timeframe; } ENUM_APPLIED_PRICE Price(class="type">void) { class="kw">return e_price; } class=class="str">"cmt">//--- 载入数据 class="kw">virtual class="type">bool UpdateTimeserie(class="type">void); class="type">bool GetTimeserie(class="type">class="kw">double ×erie[]) { class="kw">return ArrayCopy(timeserie,d_timeserie)>class="num">0; } }; class="type">bool CTimeserie::UpdateTimeserie(class="type">void) {
「按价格类型拉序列并算跳点差值」
这段逻辑先判断本地缓存是否还新鲜:用 SeriesInfoInteger 取当前品种当前周期最后一根 K 线时间,若上次加载时间不早于它、且已缓存数组长度够用,直接返回 true 跳过拷贝,能省掉不少重复 IO。 真正拉数据靠 switch 分流。PRICE_CLOSE / OPEN / HIGH / LOW 直接调 CopyClose 等函数,从偏移 1 开始取 i_bars+1 根;中位数、典型价、加权价则先 CopyRates 拿整包 MqlRates,再按 (H+L)/2、(H+L+C)/3、(H+L+2C)/4 逐根算进 data。注意 PRICE_OPEN 和 PRICE_HIGH 的 case 里漏了 break,会穿透执行,属于原代码隐患,移植时建议补上。 拿到 bars 后若小于等于 0 直接返回 false;否则把 dt_last_load 更新为 cur_date,并把 d_timeserie 重设成 bars-1 长度。最后用 SymbolInfoDouble 取 SYMBOL_POINT,循环算 (data[i+1]-data[i])/point,得到每根相对于前一根的「跳点数」序列,供后续波动率判定使用。外汇与贵金属杠杆高,这类差值序列只反映历史波动结构,实盘信号概率仍需自行验证。
class="type">class="kw">datetime cur_date=(class="type">class="kw">datetime)SeriesInfoInteger(s_symbol,e_timeframe,SERIES_LASTBAR_DATE); if(dt_last_load>=cur_date && ArraySize(d_timeserie)>=i_bars) class="kw">return true; class=class="str">"cmt">//--- class="type">MqlRates rates[]; class="type">int bars=class="num">0,i; class="type">class="kw">double data[]; class="kw">switch(e_price) { case PRICE_CLOSE: bars=CopyClose(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,data); class="kw">break; case PRICE_OPEN: bars=CopyOpen(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,data); case PRICE_HIGH: bars=CopyHigh(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,data); case PRICE_LOW: bars=CopyLow(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,data); case PRICE_MEDIAN: bars=CopyRates(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,rates); bars=ArrayResize(data,bars); for(i=class="num">0;i<bars;i++) data[i]=(rates[i].high+rates[i].low)/class="num">2; class="kw">break; case PRICE_TYPICAL: bars=CopyRates(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,rates); bars=ArrayResize(data,bars); for(i=class="num">0;i<bars;i++) data[i]=(rates[i].high+rates[i].low+rates[i].close)/class="num">3; class="kw">break; case PRICE_WEIGHTED: bars=CopyRates(s_symbol,e_timeframe,class="num">1,i_bars+class="num">1,rates); bars=ArrayResize(data,bars); for(i=class="num">0;i<bars;i++) data[i]=(rates[i].high+rates[i].low+class="num">2*rates[i].close)/class="num">4; class="kw">break; } class=class="str">"cmt">//--- if(bars<=class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- dt_last_load=cur_date; class=class="str">"cmt">//--- if(ArraySize(d_timeserie)!=(bars-class="num">1) && ArrayResize(d_timeserie,bars-class="num">1)<=class="num">0) class="kw">return class="kw">false; class="type">class="kw">double point=SymbolInfoDouble(s_symbol,SYMBOL_POINT); for(i=class="num">0;i<bars-class="num">1;i++) d_timeserie[i]=(data[i+class="num">1]-data[i])/point; class=class="str">"cmt">//--- class="kw">return true; }
把砖块拼成 PairPlot 面板
做完了单个图形砖块,下一步是把它们塞进一个能挂在 MT5 对话框里的容器。CPairPlot 直接派生于 CWndClient,这样用标准 CAppDialog 搭的图形面板就能直接托管它,不用自己重写窗口消息循环。 类内部用 CPlotBase 指针数组存所有图形实例,CArrayObj 存时间序列指针,再配几个变量管时段、价格类型、历史深度和资产名颜色。对外只暴露 Create 和 Refresh:Create 要你传图表 ID、坐标、品种数组、时段、历史深度和柱形列数,进来先卡两道底线——品种数组为空或历史深度不达标直接返回 false,连图都不画。 图形表格按资产两两组合生成:同名资产交叉画柱形图,其余配对画散点图。Refresh 里先循环刷所有时间序列,注意配对图要求两侧数据可比,所以只要有一个序列更新失败就整体退出;传数据给图形时用 Update(false),意思是别顺手去刷主图,能明显压低终端负载、缩短函数执行时间。 图形元素基于 CGraphic 而非 CObject,所以基类里的 Shift、Show、Hide 必须在 CPairPlot 重写一遍,否则面板拖不动也藏不住。下面这段是类声明的骨架,private 里的 i_bars 默认传 1000、cells 默认 10,对应千根 K 线和十列分布。
class CPairPlot : class="kw">public CWndClient { class="kw">private: CPlotBase *m_arr_graphics[]; CArrayObj m_arr_symbols; ENUM_TIMEFRAMES e_timeframe; ENUM_APPLIED_PRICE e_price; class="type">int i_total_symbols; class="type">uint i_bars; ENUM_HISTOGRAM_ORIENTATION e_orientation; class="type">uint i_text_color; class="kw">public: CPairPlot(); ~CPairPlot(); 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 x2,class="kw">const class="type">int y2, class="kw">const class="type">class="kw">string &symbols[],class="kw">const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, class="kw">const class="type">int bars=class="num">1000, class="kw">const class="type">uint cells=class="num">10, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE); class="type">bool Refresh(class="type">void); class="type">bool HistogramOrientation(ENUM_HISTOGRAM_ORIENTATION value); ENUM_HISTOGRAM_ORIENTATION HistogramOrientation(class="type">void) { class="kw">return e_orientation; } class="type">bool SetTextColor(class="type">color value); class=class="str">"cmt">//--- 几何 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); };
◍ 构建多品种关联矩阵的初始化逻辑
在 MT5 里做跨品种相关性热力图,第一步是把品种数组塞进自定义容器并校验底线参数。Create 函数要求传入品种名数组、时间框架、 bars 数量与单元格分辨率,其中 bars 默认 1000、单元格默认 10,低于 100 根 K 线直接返回 false,这是为了避免小样本下分布失真。 代码里先用 ArraySize 拿品种总数,若小于等于 1 或 bars 不达标立刻退出;随后给每个品种 new 一个 CTimeserie 并调用 Create 拉数据,任何一步失败都返回 false。品种数最终存进 i_total_symbols,若仍不足 2 个同样放弃构建。 图形层先把客户区窗口建出来,再按 i_total_symbols 的平方 resize 图形对象数组——比如 5 个品种就要 25 个格子。窗口宽高除以品种数得到单格尺寸,双重循环里对角线位置(i==j)走 CHistogram 分支画自身分布直方图,其余位置留作跨品种散点。 开 MT5 把这段贴进你的 CPairPlot 类,先传 EURUSD、GBPUSD、XAUUSD 三个符号跑通,留意 bars 设 100 以下会直接构造失败;外汇与贵金属波动剧烈,矩阵仅反映历史样本关联,实盘参考须自担高风险。
class="type">bool CPairPlot::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="kw">const class="type">class="kw">string &symbols[],class="kw">const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, class="kw">const class="type">int bars=class="num">1000, class="kw">const class="type">uint cells=class="num">10, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE) { i_total_symbols=class="num">0; class="type">int total=ArraySize(symbols); if(total<=class="num">1 || bars<class="num">100) class="kw">return class="kw">false; class=class="str">"cmt">//--- e_timeframe=timeframe; i_bars=bars; e_price=price; for(class="type">int i=class="num">0;i<total;i++) { CTimeserie *temp=new CTimeserie; if(temp==NULL) class="kw">return class="kw">false; temp.SetBars(i_bars); if(!temp.Create(symbols[i],e_timeframe,e_price)) class="kw">return class="kw">false; if(!m_arr_symbols.Add(temp)) class="kw">return class="kw">false; } i_total_symbols=m_arr_symbols.Total(); if(i_total_symbols<=class="num">1) class="kw">return class="kw">false; if(!CWndClient::Create(chart,name,subwin,x1,y1,x2,y2)) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(ArraySize(m_arr_graphics)!=(i_total_symbols*i_total_symbols)) if(ArrayResize(m_arr_graphics,i_total_symbols*i_total_symbols)<=class="num">0) class="kw">return class="kw">false; class="type">int width=Width()/i_total_symbols; class="type">int height=Height()/i_total_symbols; for(class="type">int i=class="num">0;i<i_total_symbols;i++) { CTimeserie *timeserie1=m_arr_symbols.At(i); if(timeserie1==NULL) class="kw">continue; for(class="type">int j=class="num">0;j<i_total_symbols;j++) { class="type">class="kw">string obj_name=m_name+"_"+(class="type">class="kw">string)i+"_"+(class="type">class="kw">string)j; class="type">int obj_x1=m_rect.left+j*width; class="type">int obj_x2=obj_x1+width; class="type">int obj_y1=m_rect.top+i*height; class="type">int obj_y2=obj_y1+height; if(i==j) { CHistogram *temp=new CHistogram(); if(CheckPointer(temp)==POINTER_INVALID) class="kw">return class="kw">false; if(!temp.Create(m_chart_id,obj_name,m_subwin,obj_x1,obj_y1,obj_x2,obj_y2,e_orientation)) class="kw">return class="kw">false; } } }