在MQL5中的三维建模(基础篇)
「在 MT5 里搭时间序列的三维投影」
MT5 原生没有三维建模接口,想看价格序列的立体形态只能从坐标系统和几何投影自己搭。时间序列本质是动态系统,按等差或随机节奏持续接收值(订单 tick、K线柱、分形等),把它投到三维空间,比普通 2D 图表多出一个可旋转的观测维度。 做这件事分两步:先建模型,把时间序列写成三维数学模型;再渲染,用特定函数把模型投到一个平面上出图。2017 年那篇社区示例里,作者用这套流程画出了时间序列的三维表现,实测可读出 2D 图里被压平的层次关系。 MQL5 目前仍不提供现成 3D 方案,相关算法虽写在三维主题下,但其中坐标变换和投影思路,在非线性指标降维、聚类可视化里也能直接搬用。外汇与贵金属市场波动剧烈、杠杆风险高,这类可视化仅作分析辅助,不预示任何方向。
把图表对象做成可交互的基类
在 MT5 里做三维可视化,第一步不是画立方体,而是先定义一套能自我保全、能响应鼠标和图表事件的图形对象基类。原文把这类对象叫 IGO(可交互图形对象),它必须同时满足四点:接口精简、生命周期持久、环境自适应、支持旋转和拖拽交互。 持久性是最容易被忽略的坑。MQL5 的图形对象随时可能被用户删掉、被切周期清空类字段,但 IGO 的实例要活到所属 EA/指标退出为止。做法是在 OnChartEvent 里拦截删除事件,用保存好的属性立刻重建,而不是等实例也跟着死。 下面这段是 CIGO 基类骨架,只留了最小字段集:事件开关、所属图层、名称、基础锚点价格和投射角度。Create / Redraw / OnEvent 都标了 virtual,方便子类按三维需求重载。 [CODE] class CIGO { protected : bool on_event; // 事件处理标志 int m_layer; // IGO 所属的图层 //--- double SetPrice( double def, int prop_modifier= 0 ); public : string m_name; // IGO 对象的名称 double m_price; // IGO 对象的基本锚点[价格] double m_angle; // IGO 投射角度 [度数] CIGO(); ~CIGO(); //--- virtual // 方法: 创建 IGO void Create( string name) {on_event= true ;} virtual // 方法: 重绘 IGO void Redraw() { ChartRedraw ();} virtual // OnChartEvent 处理方法 bool OnEvent( const int id, // 事件 ID const long &lparam, // long 类型(长整数型)的事件参数 const double &dparam, // double 类型(双精度浮点数型)的事件参数 const string &sparam, // string 类型(字符串型)的事件参数 int iparamemr= 0 , // IGO 事件标识符 double dparametr= 0.0 ); // IGO double 类型的事件参数 }; [/CODE] OnEvent 是交互的核心。它处理四类标准事件:对象被删(立刻 Create 恢复)、图表属性或尺寸变(调 Redraw 重绘)、对象被拖(读 OBJPROP_PRICE 更新 m_price 并返回 true)、在别处点击(取消自身选中避免误拖)。注意切周期时类实例会重初始化,字段丢光但图上的对象还在,所以要用 ObjectGetDouble 把属性读回来填进 m_price。 [CODE] bool CIGO::OnEvent( const int id, const long &lparam, const double &dparam, const string &sparam, int iparamemr= 0 , double dparametr= 0.0 ) { bool res= false ; if (on_event) // 允许事件的处理 { // 删除一个图形对象 if (( ENUM_CHART_EVENT )id== CHARTEVENT_OBJECT_DELETE && sparam==m_name) { Create(m_name); } // 改变图表的大小或者使用属性对话框改变图表属性 if (( ENUM_CHART_EVENT )id== CHARTEVENT_CHART_CHANGE ) { Redraw(); } // 移动一个图形对象 if (( ENUM_CHART_EVENT )id== CHARTEVENT_OBJECT_DRAG ) if ( ObjectGetInteger ( 0 ,sparam, OBJPROP_SELECTED )== 1 && sparam==m_name) { m_price= ObjectGetDouble ( 0 ,m_name, OBJPROP_PRICE ); Redraw(); res= true ; // 通知基础锚点有了改变 } // 在图形对象外点击了鼠标 if (( ENUM_CHART_EVENT )id== CHARTEVENT_OBJECT_CLICK && sparam!=m_name) { ObjectSetInteger ( 0 ,m_name, OBJPROP_SELECTED , 0 ); ChartRedraw (); } } return (res); } [/CODE] SetPrice 解决锚点初始化问题。类刚建或切周期时 m_price 是 NULL,先尝试从同名图形对象读价格属性;读不到才用传入的 def 默认值兜底。这样对象在图表上残留也能续上锚点,不在就落默认位。 [CODE] double CIGO::SetPrice( double def, int prop_modifier= 0 ) { if (m_price== NULL ) // 如果变量没有值 m_price= ObjectGetDouble ( 0 ,m_name, OBJPROP_PRICE ,prop_modifier); if (m_price== NULL ) // 如果没有坐标 m_price=def; // 默认值 return (m_price); } [/CODE] 外汇和贵金属图表上跑这套对象,要清楚:对象持久化靠事件拦截,不是 MT5 原生保证,切周期逻辑写漏就会闪退或丢锚点,属于高风险定制开发。先把这段基类丢进 MT5 编译,挂个空 EA 拖两下对象,验证删除自恢复和拖拽改价是否生效,再往下写三维子类。
class CIGO { class="kw">protected : class="type">bool on_event; class=class="str">"cmt">// 事件处理标志 class="type">int m_layer; class=class="str">"cmt">// IGO 所属的图层 class=class="str">"cmt">//--- class="type">class="kw">double SetPrice( class="type">class="kw">double def, class="type">int prop_modifier= class="num">0 ); class="kw">public : class="type">class="kw">string m_name; class=class="str">"cmt">// IGO 对象的名称 class="type">class="kw">double m_price; class=class="str">"cmt">// IGO 对象的基本锚点[价格] class="type">class="kw">double m_angle; class=class="str">"cmt">// IGO 投射角度 [度数] CIGO(); ~CIGO(); class=class="str">"cmt">//--- class="kw">virtual class=class="str">"cmt">// 方法: 创建 IGO class="type">void Create( class="type">class="kw">string name) {on_event= true ;} class="kw">virtual class=class="str">"cmt">// 方法: 重绘 IGO class="type">void Redraw() { ChartRedraw();} class="kw">virtual class=class="str">"cmt">// OnChartEvent 处理方法 class="type">bool OnEvent( class="kw">const class="type">int id, class=class="str">"cmt">// 事件 ID class="kw">const class="type">long &lparam, class=class="str">"cmt">// class="type">long 类型(长整数型)的事件参数 class="kw">const class="type">class="kw">double &dparam, class=class="str">"cmt">// class="type">class="kw">double 类型(双精度浮点数型)的事件参数 class="kw">const class="type">class="kw">string &sparam, class=class="str">"cmt">// class="type">class="kw">string 类型(字符串型)的事件参数 class="type">int iparamemr= class="num">0 , class=class="str">"cmt">// IGO 事件标识符 class="type">class="kw">double dparametr= class="num">0.0 ); class=class="str">"cmt">// IGO class="type">class="kw">double 类型的事件参数 }; class="type">bool CIGO::OnEvent( class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam, class="type">int iparamemr= class="num">0 , class="type">class="kw">double dparametr= class="num">0.0 ) { class="type">bool res= class="kw">false ; if (on_event) class=class="str">"cmt">// 允许事件的处理 { class=class="str">"cmt">// 删除一个图形对象 if (( ENUM_CHART_EVENT )id== CHARTEVENT_OBJECT_DELETE && sparam==m_name) { Create(m_name); } class=class="str">"cmt">// 改变图表的大小或者使用属性对话框改变图表属性 if (( ENUM_CHART_EVENT )id== CHARTEVENT_CHART_CHANGE ) { Redraw(); } class=class="str">"cmt">// 移动一个图形对象 if (( ENUM_CHART_EVENT )id== CHARTEVENT_OBJECT_DRAG ) if ( ObjectGetInteger( class="num">0 ,sparam, OBJPROP_SELECTED )== class="num">1 && sparam==m_name) { m_price= ObjectGetDouble( class="num">0 ,m_name, OBJPROP_PRICE ); Redraw(); res= true ; class=class="str">"cmt">// 通知基础锚点有了改变 } class=class="str">"cmt">// 在图形对象外点击了鼠标 if (( ENUM_CHART_EVENT )id== CHARTEVENT_OBJECT_CLICK && sparam!=m_name) { ObjectSetInteger( class="num">0 ,m_name, OBJPROP_SELECTED , class="num">0 ); ChartRedraw(); } } class="kw">return (res); } class="type">class="kw">double CIGO::SetPrice( class="type">class="kw">double def, class="type">int prop_modifier= class="num">0 ) { if (m_price== NULL ) class=class="str">"cmt">// 如果变量没有值 m_price= ObjectGetDouble( class="num">0 ,m_name, OBJPROP_PRICE ,prop_modifier); if (m_price== NULL ) class=class="str">"cmt">// 如果没有坐标 m_price=def; class=class="str">"cmt">// 默认值 class="kw">return (m_price); }
◍ 右侧价格标签对象的类封装与坐标锚定
C_OBJ_ARROW_RIGHT_PRICE 派生于 CIGO 基类,专门封装 MT5 里的 OBJ_ARROW_RIGHT_PRICE 图形对象,也就是图表右侧贴价格轴的箭头标签。它的实例默认绑定当前主图表,并落在 Z 轴方向上,适合拿来做三维坐标系统的中心参照。 创建时把标签价格定为可视区间中值:用 ChartGetDouble 取 CHART_PRICE_MAX 与 CHART_PRICE_MIN,相加除以 2 作为 m_price,这样标签一出生就钉在画面纵向正中。对象设为可选、线宽 1、颜色 clrISO,时间参数填 T(0),价格参数填 m_price,最后 ChartRedraw 并打开事件开关。 Redraw 方法只重设时间锚点为 T(0) 再刷图,不改动价格,因此标签会随主图时间轴平移但始终贴在原价格上。外汇与贵金属杠杆高、滑点大,这类对象仅作视觉辅助,信号本身不预示涨跌。 [CODE]class C_OBJ_ARROW_RIGHT_PRICE: public CIGO { public : virtual // 方法: 创建对象 void Create(string name); virtual // 方法: 重绘对象 void Redraw(); }; void C_OBJ_ARROW_RIGHT_PRICE::Create(string name) { m_name=name; // 存对象名 m_price=SetPrice((ChartGetDouble(0,CHART_PRICE_MAX)+ChartGetDouble(0,CHART_PRICE_MIN))/2); // 取可见价格区间中值 ObjectCreate(0,m_name,OBJ_ARROW_RIGHT_PRICE,0,0,0); // 建右侧价格标签对象 ObjectSetInteger(0,m_name,OBJPROP_SELECTABLE,true); // 允许选中 ObjectSetInteger(0,m_name,OBJPROP_WIDTH,1); // 线宽1 ObjectSetInteger(0,m_name,OBJPROP_COLOR,clrISO); // 设颜色 ObjectSetInteger(0,m_name,OBJPROP_TIME,0,T(0)); // 时间锚点T(0) ObjectSetDouble(0,m_name,OBJPROP_PRICE,0,m_price); // 价格锚点中值 ChartRedraw(); // 重绘图表 on_event=true; // 允许事件处理 } void C_OBJ_ARROW_RIGHT_PRICE::Redraw() { ObjectSetInteger(0,m_name,OBJPROP_TIME,0,T(0)); // 重设时间锚点 ChartRedraw(); // 刷图 }[/CODE] 开 MT5 把这段塞进 EA 的 include 链,改 clrISO 为你自己的调色板常量,就能在副图指标里快速标出中价参考线。
class C_OBJ_ARROW_RIGHT_PRICE: class="kw">public CIGO { class="kw">public : class="kw">virtual class=class="str">"cmt">// 方法: 创建对象 class="type">void Create(class="type">class="kw">string name); class="kw">virtual class=class="str">"cmt">// 方法: 重绘对象 class="type">void Redraw(); }; class="type">void C_OBJ_ARROW_RIGHT_PRICE::Create(class="type">class="kw">string name) { m_name=name; m_price=SetPrice((ChartGetDouble(class="num">0,CHART_PRICE_MAX)+ChartGetDouble(class="num">0,CHART_PRICE_MIN))/class="num">2); ObjectCreate(class="num">0,m_name,OBJ_ARROW_RIGHT_PRICE,class="num">0,class="num">0,class="num">0); ObjectSetInteger(class="num">0,m_name,OBJPROP_SELECTABLE,true); ObjectSetInteger(class="num">0,m_name,OBJPROP_WIDTH,class="num">1); ObjectSetInteger(class="num">0,m_name,OBJPROP_COLOR,clrISO); ObjectSetInteger(class="num">0,m_name,OBJPROP_TIME,class="num">0,T(class="num">0)); ObjectSetDouble(class="num">0,m_name,OBJPROP_PRICE,class="num">0,m_price); ChartRedraw(); on_event=true; } class="type">void C_OBJ_ARROW_RIGHT_PRICE::Redraw() { ObjectSetInteger(class="num">0,m_name,OBJPROP_TIME,class="num">0,T(class="num">0)); ChartRedraw(); }
「用趋势线类搭一条Z轴基线」
C_OBJ_TREND 直接继承 CIGO,只暴露 Create 与 Redraw 两个虚方法,属于刻意做薄的封装。对三维价格建模来说,它提供的刚好是最小可用集:一条可双向延伸、锁定在图表背景、不可被选中的趋势线。 Create 里先把 name 存进成员,再用 ChartGetDouble 取当前图表 CHART_PRICE_MAX 与 CHART_PRICE_MIN 做平均,得到 m_price 作为中线锚点。随后 ObjectCreate 建 OBJ_TREND,时间参数暂填 0,价格填 m_price,第二点价格抬 1 点,两端都开 RAY_RIGHT 与 RAY_LEFT,等于画出一条横贯左右的无斜率参考线。 Redraw 只重设两个端点的时间为 T(0) 并刷 ChartRedraw,说明这条线不参与动态价格跟随,纯粹当静态坐标系用。外汇与贵金属波动大,这类背景线仅作空间参照,实盘信号仍须结合量价确认,杠杆品种高风险。 想验证就开 MT5 新建 EA,把下面代码原样贴进类定义,调 styleISO 与 clrISO 看渲染差异。
class C_OBJ_TREND: class="kw">public CIGO { class="kw">public : class="kw">virtual class=class="str">"cmt">// 方法: 创建对象 class="type">void Create(class="type">class="kw">string name); class="kw">virtual class=class="str">"cmt">// 方法: 重绘对象 class="type">void Redraw(); }; class="type">void C_OBJ_TREND::Create(class="type">class="kw">string name) { m_name=name; m_price=(ChartGetDouble(class="num">0,CHART_PRICE_MAX)+ChartGetDouble(class="num">0,CHART_PRICE_MIN))/class="num">2; ObjectCreate(class="num">0,m_name,OBJ_TREND,class="num">0,class="num">0,class="num">0); ObjectSetInteger(class="num">0,m_name,OBJPROP_COLOR,clrISO); ObjectSetInteger(class="num">0,m_name,OBJPROP_WIDTH,class="num">0); ObjectSetInteger(class="num">0,m_name,OBJPROP_STYLE,styleISO); ObjectSetDouble(class="num">0,m_name,OBJPROP_PRICE,class="num">0,m_price); ObjectSetInteger(class="num">0,m_name,OBJPROP_TIME,class="num">0,T(class="num">0)); ObjectSetDouble(class="num">0,m_name,OBJPROP_PRICE,class="num">1,m_price+class="num">1); ObjectSetInteger(class="num">0,m_name,OBJPROP_TIME,class="num">1,T(class="num">0)); ObjectSetInteger(class="num">0,m_name,OBJPROP_RAY_RIGHT,true); ObjectSetInteger(class="num">0,m_name,OBJPROP_RAY_LEFT,true); ObjectSetInteger(class="num">0,m_name,OBJPROP_BACK,true); ObjectSetInteger(class="num">0,m_name,OBJPROP_SELECTABLE,class="kw">false); ChartRedraw(); on_event=true; } class="type">void C_OBJ_TREND::Redraw() { ObjectSetInteger(class="num">0,m_name,OBJPROP_TIME,class="num">0,T(class="num">0)); ObjectSetInteger(class="num">0,m_name,OBJPROP_TIME,class="num">1,T(class="num">0)); ChartRedraw(); }