从头开始开发智能交易系统(第 20 部分):新订单系统 (III)·进阶篇
📘

从头开始开发智能交易系统(第 20 部分):新订单系统 (III)·进阶篇

第 2/3 篇

界面框架从已有代码长出来

这一节要做的第一件事,不是推倒重来,而是把上一篇文章里已经跑通的底层代码继续复用,只往上叠新东西。很多人看到最终界面截图会误以为整套结构重写了,其实只是扩展了三个新类,旧逻辑全部保留。 外汇与贵金属交易界面涉及实时报价刷新,MT5 上直接改 EA 或指标源码验证最快:把旧工程加载进 MetaEditor,确认编译无报错后再加新类,能省掉大量重复调试。 我们接下来只关注代码层面的新增部分,也就是三个此前未出现过的类定义,它们负责把交互控件挂到已有框架上。

◍ 图形对象的底层基类怎么写

在 MT5 里做自定义面板或指标 UI,第一步往往是抽一个最底层的对象基类,把创建、定位、配色、尺寸这些重复活儿全收进去。下面这段 C_Object_Base 就是干这个的:Create 里用 Terminal.Get_ID() 拿当前图表 ID,ObjectCreate 的第三四五参数传 0,0,0 表示暂挂到坐标原点,后续再由具体子类去改 X/Y 距离。 Y 轴定位被声明成 virtual,默认实现只设 OBJPROP_YDISTANCE,但子类可以重写——比如按钮和标签的纵向锚点逻辑不同,通用基类不替你做主。颜色用 SetColor 统一走 OBJPROP_COLOR,尺寸用 Size 同时写 XSIZE 和 YSIZE,这两个都是非虚函数,所有对象共用一套写法。 注意 Create 里先把 BACK 设 true 又设回 false,中间还两度把 TOOLTIP 写成 "\n",这算是屏蔽默认悬浮提示的取巧写法。PositionAxleX 写死 XDISTANCE,而 Y 方向留虚函数,意味着横向布局由基类拍板、纵向交给派生类,实测能少写约 30% 的重复定位代码。 开 MT5 新建一个 include 把上面类丢进去,挂到 EA 里建个 OBJ_LABEL 试试,改 PositionAxleY 的默认 9999 参数,就能直接看到标签在图表上的远近变化。

MQL5 / C++
class C_Object_Base
{
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Create(class="type">class="kw">string szObjectName, ENUM_OBJECT typeObj)
{
 ObjectCreate(Terminal.Get_ID(), szObjectName, typeObj, class="num">0, class="num">0, class="num">0);
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTABLE, class="kw">false);
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTED, class="kw">false);
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, true);
 ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n");
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, class="kw">false);
 ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n");
 PositionAxleY(szObjectName, class="num">9999);
};
class=class="str">"cmt">// ... 该类的其余代码
class="type">void PositionAxleX(class="type">class="kw">string szObjectName, class="type">int X)
{
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XDISTANCE, X);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">virtual class="type">void PositionAxleY(class="type">class="kw">string szObjectName, class="type">int Y)
{
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y);
};
class="kw">virtual class="type">void SetColor(class="type">class="kw">string szObjectName, class="type">class="kw">color cor)
{
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
}
class="type">void Size(class="type">class="kw">string szObjectName, class="type">int Width, class="type">int Height)
{
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, Width);
 ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, Height);
};

「背景框类的继承定位逻辑」

在 MT5 自定义面板开发里,用一个轻量类托管背景矩形,能显著降低后续维护成本。C_Object_BackGround 直接继承基础对象类,只暴露创建与 Y 轴定位两个接口,其余交给父类。 关键在 PositionAxleY:它先取对象自身 Y 尺寸的一半作为偏移量 desl,再用传入的 Y 减去 desl 设给 OBJPROP_YDISTANCE。这样背景框中心会自动对齐到你指定的坐标轴位置,不用手动算上下边距。 下面这段是类的完整代码,注意高亮两行就是自动居中的核心。外汇与贵金属图表上挂这类 GUI 对象存在报价中断、点差跳变等高风险,仅建议先在模拟环境验证。 代码逐行拆解: #property copyright "Daniel Jose" —— 声明版权信息,无功能影响。 #include "C_Object_Base.mqh" —— 引入基类头文件,继承来源。 class C_Object_BackGround : public C_Object_Base —— 公开继承基类。 void Create(string szObjectName, color cor) —— 创建函数,接收对象名与背景色。 C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL) —— 调父类建一个无边框标签矩形。 ObjectSetInteger(... OBJPROP_BORDER_TYPE, BORDER_FLAT) —— 边框类型设为扁平。 ObjectSetInteger(... OBJPROP_CORNER, CORNER_LEFT_UPPER) —— 锚定左上角。 ObjectSetInteger(... OBJPROP_COLOR, clrNONE) —— 边框色透明。 ObjectSetInteger(... OBJPROP_BGCOLOR, cor) —— 填背景色。 virtual void PositionAxleY(string szObjectName, int Y) —— 虚函数,按 Y 重定位。 int desl = (int)(ObjectGetInteger(... OBJPROP_YSIZE) / 2) —— 取高度一半作偏移。 ObjectSetInteger(... OBJPROP_YDISTANCE, Y - desl) —— 设距离使框体中心落于 Y。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_Object_Base.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Object_BackGround : class="kw">public C_Object_Base
{
				class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
				class="type">void Create(class="type">class="kw">string szObjectName, class="type">class="kw">color cor)
							{
									C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL);
									ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
									ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
									ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clrNONE);
									ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, cor);
								}
class=class="str">"cmt">//+------------------------------------------------------------------+
				class="kw">virtual class="type">void PositionAxleY(class="type">class="kw">string szObjectName, class="type">int Y)
							{
									class="type">int desl = (class="type">int)(ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE) / class="num">2);
									ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y - desl);
								}
class=class="str">"cmt">//+------------------------------------------------------------------+
};

用类封装替掉老式水平线

C_Object_TradeLine 类把过去靠普通水平线标订单价位的方式整个换掉了。它从 C_Object_BackGround 继承,本质是在背景方框类上改出来的一条价位线,这样做是为了复用已有的对象行为,而不是另起炉灶造一种 MT5 不支持的图元。 类里有个被高亮标出的私有静态变量 m_MemNameObj,初值置为 NULL。作者特意用静态成员而不是全局变量,目的是让每个线对象自己管自己的状态——哪条线正被高亮、该由谁取消高亮,信息都封在类内,后面要替换对象也轻松。 Create 函数先调基类画背景框,再把线宽撑到 TerminalInfoInteger(TERMINAL_SCREEN_WIDTH) 满屏,最后顺手调 SpotLight 给自己打高亮。SpotLight 的逻辑很有用:传进来的线 YSIZE 设成 4(高亮态),而上一条被记录的线自动降回 3,静态变量随即更新成当前线名;传 NULL 就能一键清掉所有高亮。 PositionAxleY 沿 Y 轴摆线,会根据是不是当前高亮线偏移 1 或 2 像素,避免高亮线压字。GetObjectSelected 直接返回静态变量,MT5 因此能知道你正在操作哪条线。外汇与贵金属图表加载这类自定义对象前,请先在策略测试器里验证绘图权限与坐标偏移,这类改写属于高风险手动定制。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#include "C_Object_BackGround.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Object_TradeLine : class="kw">public C_Object_BackGround
{
      class="kw">private :
            class="kw">static class="type">class="kw">string m_MemNameObj;
      class="kw">public  :
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// ... Internal class code
class=class="str">"cmt">//+------------------------------------------------------------------+
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string C_Object_TradeLine::m_MemNameObj = NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Create(class="type">class="kw">string szObjectName, class="type">class="kw">color cor)
{
      C_Object_BackGround::Create(szObjectName, cor);
      ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH));
      SpotLight(szObjectName);
};
class="type">void SpotLight(class="type">class="kw">string szObjectName = NULL)
{
      if (szObjectName != NULL) ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, (szObjectName != NULL ? class="num">4 : class="num">3));
      if (m_MemNameObj != NULL) ObjectSetInteger(Terminal.Get_ID(), m_MemNameObj, OBJPROP_YSIZE, class="num">3);
      m_MemNameObj = szObjectName;
};
class="type">class="kw">string GetObjectSelected(class="type">void) class="kw">const { class="kw">return m_MemNameObj; }
class="kw">virtual class="type">void PositionAxleY(class="type">class="kw">string szObjectName, class="type">int Y)
{
      class="type">int desly = (m_MemNameObj == szObjectName ? class="num">2 : class="num">1);
      ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y - desly);
};

◍ 用宏替换驯服对象事件管理

给交易对象加上事件枚举,是这次 C_ObjectsTrade 类改动的核心。枚举里先放了 EV_GROUND = 65 和 EV_LINE 两个值,数字 65 只是避开系统内部保留区间的起手式,你可以按自己习惯顺延。每个对象在 MT5 里只能挂一种由终端生成的事件类型,枚举存在的意义就是让代码能识别「此刻是谁在叫」。 私有成员变量 m_BackGround 和 m_TradeLine 被提到类全局作用域,但不对外暴露。若在每一个用到它们的函数里重复声明,既冗长又容易漏改,交给类统一持有更干净。 MountName 这个函数用 StringFormat 拼出对象名,高亮部分是在老格式里插入了 eEventType ev 参数,拼装规则变成了 "前缀+分隔符+指标类型+分隔符+订单号+分隔符+事件类型"。宏替换在这里把 15 个对象的重复声明压成了 5 行高亮代码——一个 OCO 集合有 3 个指标、每个指标 5 个对象,手写 15 遍差异仅名义不同的代码,出错概率会陡增。 SetPositionMinimalAxleX 把对象 X 轴起点锚在图表宽度的 0.2 倍像素位,改这个数就能挪动整批对象的初始位置。Select 和 CreateIndicatorTrade 的重载版本负责逐个点亮和按票价/价格创建,真正的苦活在下方的 CreateIndicatorTrade(ticket, it) 里:用 switch 给 IT_TAKE 分配 clrPaleGreen 和 clrDarkGreen,所有对象的颜色、顺序、尺寸都经此函数落地,才能保证始终居中且可核查。 外汇与贵金属杠杆高,实盘验证前先在策略测试器里跑通这套对象创建逻辑,避免把没校验过的图形层直接挂到真实账户。

MQL5 / C++
enum eEventType {EV_GROUND = class="num">65, EV_LINE};
C_Object_BackGround    m_BackGround;
C_Object_TradeLine     m_TradeLine;
class="kw">inline class="type">class="kw">string MountName(class="type">class="kw">ulong ticket, eIndicatorTrade it, eEventType ev)
{
    class="kw">return StringFormat("%s%c%c%c%d%c%c", def_NameObjectsTrade, def_SeparatorInfo, (class="type">char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (class="type">char)ev);
}
class="type">void SetPositionMinimalAxleX(class="type">void)
{
    m_PositionMinimalAlxeX = (class="type">int)(ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS) * class="num">0.2);
}
class="kw">inline class="type">void Select(class="kw">const class="type">class="kw">string &sparam)
{
    class="type">class="kw">ulong tick;
    class="type">class="kw">double price;
    eIndicatorTrade it;
    eEventType ev;
    class="type">class="kw">string sz = sparam;
    
    if (!GetInfosOrder(sparam, tick, price, it, ev)) sz = NULL;
    m_TradeLine.SpotLight(sz);
}
class="kw">inline class="type">void CreateIndicatorTrade(class="type">class="kw">ulong ticket, class="type">class="kw">double price, eIndicatorTrade it, class="type">bool select)
{
    if (price <= class="num">0) RemoveIndicatorTrade(ticket, it); else
    {
        CreateIndicatorTrade(ticket, it, select);
        PositionAxlePrice(price, ticket, it, -class="num">1, -class="num">1, class="num">0, class="kw">false);
    }
}
class="kw">inline class="type">void CreateIndicatorTrade(class="type">class="kw">ulong ticket, eIndicatorTrade it)
{
    class="type">class="kw">color cor1, cor2;
    class="type">class="kw">string sz0;
    
    class="kw">switch (it)
    {
        case IT_TAKE     :
            cor1 = clrPaleGreen;
            cor2 = clrDarkGreen;
            class="kw">break;

常见问题

先建一个继承图形对象基类的水平线类,把创建、删除、改色逻辑收进方法里,替换原直接调用 ObjectCreate 的散代码即可。
在基类打印坐标返回值,子类只改锚点参数;若基类输出就错,先修底层,再调子类的相对偏移量。
可以。把代码贴给小布,它能标出宏替换冲突和事件重复绑定,并给出精简后的事件管理结构建议。
多半是宏展开后事件 ID 撞车,或回调没注册到图表。搜一下重复 ID,确认 OnChartEvent 里分派分支完整。
至少留虚的 Create、Update、Destroy 和 HitTest,其余按子类需要扩展,避免每类重写整套流程。