从头开始开发智能交易系统(第 21 部分):新订单系统 (IV)(基础篇)
📘

从头开始开发智能交易系统(第 21 部分):新订单系统 (IV)(基础篇)

第 1/3 篇

◍ 用 CTrade 封装把下单代码砍掉一半

在 MT5 里手写 OrderSend 系列函数,光参数就要填十来个,稍不留神就返回 10030(无效参数)。MQL5 标准库里的 CTrade 类把这些脏活全包了,下单从『填表』变成『说人话』。 下面这段是最小可跑的市价买开仓,注意它连 Symbol() 和小数位都不用你手动拼: [CODE] #include <Trade/Trade.mqh> CTrade trade; trade.Buy(0.1, _Symbol); [/CODE] 逐行看:第一行把 Trade.mqh 拉进工程,CTrade 的所有方法才有定义;第二行声明一个 trade 对象,它内部已持有当前账户、当前品种上下文;第三行直接叫它 Buy,手数 0.1、品种用 _Symbol 指代当前图表的标的,滑点等走默认。 对比原生 OrderSend,这套写法少写至少 6 个参数,回测时也更不易因参数错位导致 4756 报错。外汇与贵金属杠杆高,实盘前务必在策略测试器用 tick 级数据跑一遍,验证开仓返回 true 的概率与预期一致。

MQL5 / C++
class="macro">#include <Trade/Trade.mqh>
CTrade trade;
trade.Buy(class="num">0.1, _Symbol);

给订单挂上平仓撤单的开关

这一节先把可视化订单系统里最基础的交互缺口补上:在每笔订单旁加一个平仓或撤单按钮。上一篇文章已经把主体框架改得能跑,但真正要让交易者在图表上点一下就动作,还差这一层 UI 控件。 负责这个按钮的类并不复杂,本质是在原有订单对象上挂一个可点击区域,并把点击事件路由到对应的平仓/撤单逻辑。下面这段 MQL5 类定义就是实现核心,复制到你的 EA 工程里就能接着调。 外汇和贵金属市场杠杆高、滑点随机,这类一键平仓按钮在剧烈行情里可能触发在非预期价位,实盘前务必在 MT5 策略测试器里用历史数据跑一遍验证响应延迟。

MQL5 / C++
class COrderButton
{
class="kw">private:
   class="type">int            m_ticket;
   class="type">class="kw">string         m_label;
   class="type">bool           m_is_active;
class="kw">public:
   COrderButton(class="type">int ticket, class="type">class="kw">string label);
   ~COrderButton();
   class="type">void           OnClick(class="type">void);
   class="type">bool           IsActive(class="type">void) class="kw">const { class="kw">return m_is_active; }
   class="type">void           Draw(class="type">void);
};

「图表位图按钮的类封装与事件空缺」

在 MT5 图表上做交互控件时,位图按钮是最轻量的入口。把按钮逻辑收进一个派生类,可以避免在每处交易对象里重写 BMP 资源绑定代码,基类只留通用定位与创建接口,子类只关心按钮特有的状态读取。 下面这段代码定义了 C_Object_BtnBitMap,它继承 C_Object_Base,用 OBJ_BITMAP_LABEL 类型把两张位图(常态/按下态)挂到图表对象上。注意第 0、1 索引分别对应未按下与按下资源,若只传一张图则两态共用。 按钮虽然能在图上显示并触发 MetaTrader 5 的图表事件,但类里只提供了 GetStateButton 读状态,还没写事件处理分支——也就是说你现在跑起来,肉眼能看到按钮、MT5 也报事件,但 EA 不会真的去关仓位。这处逻辑留到后面章节补。 把定位代码上移到基类后,整个背景类能删掉重复片段,编译体量更小,后续改基类时所有控件同步受益,稳定性倾向更好。

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="macro">#define def_BtnClose    "Images\\NanoEA-SIMD\\Btn_Close.bmp"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\" + def_BtnClose
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Object_BtnBitMap : 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">string szResource1, class="type">class="kw">string szResource2 = NULL)
            {
               C_Object_Base::Create(szObjectName, OBJ_BITMAP_LABEL);
               ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_BMPFILE, class="num">0, "::" + szResource1);
               ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_BMPFILE, class="num">1, "::" + (szResource2 == NULL ? szResource1 : szResource2));
               ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE, class="kw">false);
            };
class=class="str">"cmt">//+------------------------------------------------------------------+
            class="type">bool GetStateButton(class="type">class="kw">string szObjectName) class="kw">const
            {
               class="kw">return (class="type">bool) ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE);
            }
class=class="str">"cmt">//+------------------------------------------------------------------+
};
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_Object_TradeLine.mqh"
class="macro">#include "C_Object_BtnBitMap.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_ObjectsTrade
{
class=class="str">"cmt">// ... Class code ...
}
enum eEventType {EV_GROUND = class="num">65, EV_LINE, EV_CLOSE};
C_Object_BackGround     m_BackGround;
C_Object_TradeLine      m_TradeLine;
C_Object_BtnBitMap      m_BtnClose;
class="kw">inline class="type">void CreateIndicatorTrade(class="type">class="kw">ulong ticket, eIndicatorTrade it)

◍ 正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">color</span> cor1, cor2; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">string</span> sz0; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">// ... Internal function code ...</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">switch</span> (it) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">case</span> IT_TAKE: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&n

挂单对象删除与坐标宏的收口写法

在图表对象被手动删除的事件里,先关掉 CHART_EVENT_OBJECT_DELETE 通知,避免递归触发。代码先用 ChartSetInteger 把该事件置为 false,再判断指标类型 it:若不是空指针、不是挂单占位(IT_PENDING)也不是结果占位(IT_RESULT),就只删这一个;否则把挂单、结果、止盈(IT_TAKE)、止损(IT_STOP)四个对象一次性清掉,最后把删除事件监听恢复为 true。 清完对象后紧接着用宏重排坐标。macroSetAxleY 把背景、交易线、关闭按钮三者的 Y 轴对齐到同一 y;macroSetAxleX 里关闭按钮的 X 轴比主线多偏移 3 像素(B + 3),这个写死的小偏移能让叉号按钮不压住价格线,肉眼更好点。 把这两段直接拷进你的 EA 面板类里,编译后开 MT5 拖一根对象看删除是否不再报递归错误;外汇和贵金属波动大、杠杆高,这类图形对象管理不当可能在快速行情里漏删导致重叠显示。

MQL5 / C++
ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, class="kw">false);
if ((it != NULL) && (it != IT_PENDING) && (it != IT_RESULT)) macroDelete(it)
else
  {
    macroDelete(IT_PENDING);
    macroDelete(IT_RESULT);
    macroDelete(IT_TAKE);
    macroDelete(IT_STOP);
  }
ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
class="macro">#undef macroDelete
class="macro">#define macroSetAxleY(A)      {                                                                     \
    m_BackGround.PositionAxleY(MountName(ticket, A, EV_GROUND), y); \
    m_TradeLine.PositionAxleY(MountName(ticket, A, EV_LINE), y);     \
    m_BtnClose.PositionAxleY(MountName(ticket, A, EV_CLOSE), y);     \
  }
class="macro">#define macroSetAxleX(A, B)   {                                                                     \
    m_BackGround.PositionAxleX(MountName(ticket, A, EV_GROUND), B); \
    m_TradeLine.PositionAxleX(MountName(ticket, A, EV_LINE), B);     \
    m_BtnClose.PositionAxleX(MountName(ticket, A, EV_CLOSE), B + class="num">3); \
  }
class="kw">inline class="type">void PositionAxlePrice(class="type">class="kw">double price, class="type">class="kw">ulong ticket, eIndicatorTrade it, class="type">int FinanceTake, class="type">int FinanceStop, class="type">int Leverange, class="type">bool isBuy)

「收尾时清理宏定义避免命名污染」

在指标或脚本的绘制逻辑收口处,常会看到一段被标记为内部实现的代码块,外层用大括号包裹,里面仅留注释说明「Internal code...」,真正干活的部分在编译期由宏展开。 这类结构跑完之后,必须紧跟着用 #undef 把临时宏撤掉。上面这段就连续解除了 macroSetAxleX 与 macroSetAxleY 两个宏——若不清理,同程序后续文件若也用同名宏,可能静默复用旧定义,引发坐标轴计算错乱。 开 MT5 自己写坐标轴绘制模块时,记得在最后一个右大括号后立刻 #undef,这是低成本但容易漏的一步。外汇与贵金属图表高频重绘,宏残留导致的偏移问题往往只在特定分辨率下暴露,排查成本不低。

MQL5 / C++
               {
class=class="str">"cmt">// ... Internal code...
               }
class="macro">#undef macroSetAxleX
class="macro">#undef macroSetAxleY

常见问题

检查是否忘了在 OnInit 里初始化 CTrade 对象,以及下单前是否校验了品种可选性和最小止损距离,否则会静默失败。
开关只监听交易服务器回执事件,手动在图表点关闭不会触发;需在图表按钮事件里显式调用撤单方法。
可以,小布能读取你的 EA 代码结构并标出未绑定撤单事件的挂单对象,提醒你补上图表交互回调。
类里只画了按钮但没把 OBJEVENT_CLICK 转发到主回调,需要在 ChartEvent 里把控件 ID 派发给自己类的 OnClick。
不清理会在多文件编译时撞名,导致坐标宏被别的模块覆盖;在文件末尾 #undef 对应宏最稳妥。