MQL5 中的交易操作 - 很简单·进阶篇
⚙️

MQL5 中的交易操作 - 很简单·进阶篇

(2/3)·从手写 OrderSend 到封装类调用,少踩坑的多单空单挂单实战拆解

案例拆解 第 2/3 篇
很多人在 MQL5 里直接拼 MqlTradeRequest 结构发单,字段填错就静默失败。其实标准库里的 CTrade 已经把校验和重试包好了,调用一行就能跑通基本买卖。

◍ 用 CTrade 把下单从结构填表变成一行调用

MQL5 底层只靠 OrderSend() 与 OrderSendAsync() 两个函数发交易请求,二者都接收一个含十多个字段的 MqlTradeRequest 结构。其中 5 个字段必须取预定义枚举的正确值,填错或留空会直接拦在本地、请求根本不往服务器发。对外汇与贵金属这类高波动品种来说,手写这套结构极易因字段遗漏导致下单静默失败。 CTrade 类把上述细节封装掉了。以市价单为例,Buy(0.1) 不传品种时默认用当前图表品种,多货币策略则必须显式写 Buy(0.1,"GBPUSD")。限价单 BuyLimit 开盘价须低于时价、SellLimit 高于时价;止损单 BuyStop 反之。回测时若参数写反,CTrade 通常会自动打印错误,不用自己接日志。 初始化阶段就该把魔术数字与滑点钉死:SetExpertMagicNumber(123456) 与 SetDeviationInPoints(10) 这类调用一次即可,后续每笔单不再重复传。异步模式 SetAsyncMode(true) 适合不想阻塞 OnTick 的机器人,但订单回报要靠 ResultRetcode() 自行轮询。 持仓改 SL/TP 用 PositionModify(),挂单改参用 OrderModify()——后者必须带原订单 ticket,且新开盘价相对时价方向不能写反。OrderOpen() 是万能挂单入口,参数比专用方法多,习惯裸写结构的老手可能更顺手。

MQL5 / C++
class="macro">#include<Trade\Trade.mqh>
class=class="str">"cmt">//--- 执行交易操作的对象
CTrade  trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易初始化函数                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 设置MagicNumber来识别你的订单
   class="type">int MagicNumber=class="num">123456;
   trade.SetExpertMagicNumber(MagicNumber);
class=class="str">"cmt">//--- 当买/卖时,设置可用滑点
   class="type">int deviation=class="num">10;
   trade.SetDeviationInPoints(deviation);
class=class="str">"cmt">//--- 订单填充模式,要使用服务器允许的模式
   trade.SetTypeFilling(ORDER_FILLING_RETURN);
class=class="str">"cmt">//--- 日志模式:最好不要声明这个方法,类会自动选择最佳模式。
   trade.LogLevel(class="num">1);
class=class="str">"cmt">//--- 哪个函数用来交易:true  - OrderSendAsync(), false - OrderSend()
trade.SetAsyncMode(true);
class=class="str">"cmt">//---
   class="kw">return(class="num">0);

}
class=class="str">"cmt">//--- class="num">1. 在当前交易品种上进行买入的例子
   if(!trade.Buy(class="num">0.1))
     {
      class=class="str">"cmt">//--- 报错信息
      Print("Buy() method failed. Return code=",trade.ResultRetcode(),
            ". Code description: ",trade.ResultRetcodeDescription());

}
   else
     {
      Print("Buy() method executed successfully. Return code=",trade.ResultRetcode(),
            " (",trade.ResultRetcodeDescription(),")");

}
class=class="str">"cmt">//--- class="num">2. 在指定交易品种上进行买入的例子
   if(!trade.Buy(class="num">0.1,"GBPUSD"))
     {
      class=class="str">"cmt">//--- 报错信息
      Print("Buy() method failed. Return code=",trade.ResultRetcode(),
            ". Code description: ",trade.ResultRetcodeDescription());

}
   else
     {
      Print("Buy() method executed successfully. Return code=",trade.ResultRetcode(),
            " (",trade.ResultRetcodeDescription(),")");

}
class=class="str">"cmt">//--- class="num">3. 指定交易品种和指定获利止损的买入例子
   class="type">class="kw">double volume=class="num">0.1;        class=class="str">"cmt">// 指定交易量
   class="type">class="kw">string symbol="GBPUSD";   class=class="str">"cmt">//指定交易操作的交易品种

市价买入与限价挂单的参数拼装

在 MT5 用 CTrade 类下单前,先要把品种的小数位、点值、实时报价抓出来,否则止损止盈会被服务器拒掉。下面这段把 GBPUSD 的 digits 和 point 取出来后,用 bid±1000*point 算出裸止损获利,再 NormalizeDouble 到合规精度——外汇和贵金属杠杆高,价格跳空可能让止损失效,参数一定要按品种规范来。 [CODE]int digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); // 小数位数 double point=SymbolInfoDouble(symbol,SYMBOL_POINT); // 点值 double bid=SymbolInfoDouble(symbol,SYMBOL_BID); // 平买入持仓的当前价格 double SL=bid-1000*point; // 未标准化的止损值 SL=NormalizeDouble(SL,digits); // 标准化止损 double TP=bid+1000*point; // 未标准化的获利值 TP=NormalizeDouble(TP,digits); // 标准化获利 //--- 接收买入持仓的当前开仓价 double open_price=SymbolInfoDouble(symbol,SYMBOL_ASK); string comment=StringFormat("Buy %s %G lots at %s, SL=%s TP=%s", symbol,volume, DoubleToString(open_price,digits), DoubleToString(SL,digits), DoubleToString(TP,digits)); if(!trade.Buy(volume,symbol,open_price,SL,TP,comment)) { //--- 报错信息 Print("Buy() method failed. Return code=",trade.ResultRetcode(), ". Code description: ",trade.ResultRetcodeDescription()); } else { Print("Buy() method executed successfully. Return code=",trade.ResultRetcode(), " (",trade.ResultRetcodeDescription(),")"); } //--- 1. 下买入限价挂单的例子 string symbol="GBPUSD"; // 指定下单的交易品种 int digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); // 小数位数 double point=SymbolInfoDouble(symbol,SYMBOL_POINT); // 点值 double ask=SymbolInfoDouble(symbol,SYMBOL_ASK); // 当前买入价 double price=1000*point; // 未标准化的开盘价 price=NormalizeDouble(price,digits); // 标准化开盘价 //--- 一切准备就绪,将买入限价挂单发送到服务器 if(!trade.BuyLimit(0.1,price)) {[/CODE] 逐行看:SymbolInfoInteger 拿 SYMBOL_DIGITS 是避免 5 位平台写出 1.234567 这种非法价格;SL/TP 用 ±1000*point 意味着距市价 1000 点,GBPUSD 点值若 0.0001 就是 0.0100 的跨度,回测里这种宽止损倾向降低被噪音扫掉的概率。BuyLimit 只传了 0.1 手和 price,说明其余字段用了类内默认值,实盘前建议显式填 SL/TP 注释。 把 symbol 写死成 "GBPUSD" 后,digits 和 point 重新取一遍,说明每个订单前都要按品种刷新精度。开 MT5 按 F4 贴这段代码,改 symbol 和 1000 系数,能直接验证不同品种的标准化结果。

MQL5 / C++
class="type">int    digits=(class="type">int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 小数位数
class="type">class="kw">double point=SymbolInfoDouble(symbol,SYMBOL_POINT);          class=class="str">"cmt">// 点值
class="type">class="kw">double bid=SymbolInfoDouble(symbol,SYMBOL_BID);              class=class="str">"cmt">// 平买入持仓的当前价格
class="type">class="kw">double SL=bid-class="num">1000*point;                                    class=class="str">"cmt">// 未标准化的止损值
SL=NormalizeDouble(SL,digits);                               class=class="str">"cmt">// 标准化止损
class="type">class="kw">double TP=bid+class="num">1000*point;                                    class=class="str">"cmt">// 未标准化的获利值
TP=NormalizeDouble(TP,digits);                               class=class="str">"cmt">// 标准化获利
class=class="str">"cmt">//--- 接收买入持仓的当前开仓价
class="type">class="kw">double open_price=SymbolInfoDouble(symbol,SYMBOL_ASK);
class="type">class="kw">string comment=StringFormat("Buy %s %G lots at %s, SL=%s TP=%s",
                            symbol,volume,
                            DoubleToString(open_price,digits),
                            DoubleToString(SL,digits),
                            DoubleToString(TP,digits));
if(!trade.Buy(volume,symbol,open_price,SL,TP,comment))
   {
   class=class="str">"cmt">//--- 报错信息
   Print("Buy() method failed. Return code=",trade.ResultRetcode(),
         ". Code description: ",trade.ResultRetcodeDescription());
   }
else
   {
   Print("Buy() method executed successfully. Return code=",trade.ResultRetcode(),
         " (",trade.ResultRetcodeDescription(),")");
   }
class=class="str">"cmt">//--- class="num">1. 下买入限价挂单的例子
   class="type">class="kw">string symbol="GBPUSD";   class=class="str">"cmt">// 指定下单的交易品种
   class="type">int    digits=(class="type">int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 小数位数
   class="type">class="kw">double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         class=class="str">"cmt">// 点值
   class="type">class="kw">double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);             class=class="str">"cmt">// 当前买入价
   class="type">class="kw">double price=class="num">1000*point;                                    class=class="str">"cmt">// 未标准化的开盘价
   price=NormalizeDouble(price,digits);                        class=class="str">"cmt">// 标准化开盘价
class=class="str">"cmt">//--- 一切准备就绪,将买入限价挂单发送到服务器
   if(!trade.BuyLimit(class="num">0.1,price))
   {

「带完整参数的 BuyLimit 挂单落地」

在 MT5 里用 CTrade 类下买入限价单,光写 trade.BuyLimit() 不够,价格、止损、获利都必须按品种精度归一化,否则服务器会直接拒单。下面这段例子把 GBPUSD 的挂单参数一次性算齐:0.1 手、挂单价取 1000 倍 point、止损 300 点、获利 500 点、过期时间设为服务端时间加一根 D1 周期秒数。 注意 price=1000*point 只是演示用的未标准化价,必须用 NormalizeDouble(price,digits) 收口;SL 和 TP 同理,不然和 SymbolInfoInteger 取到的 SYMBOL_DIGITS 对不上,返码会报错而非成交。 回测或实盘里若返回非零 Retcode,优先 Print 出 ResultRetcodeDescription() 看原文描述,比查文档快。外汇与贵金属杠杆高,挂单未成交前也可能因跳空直接触发,实盘使用前先在策略测试器跑一遍。

MQL5 / C++
class="type">class="kw">double volume=class="num">0.1;
class="type">class="kw">string symbol="GBPUSD";      class=class="str">"cmt">// 指定下单的交易品种
class="type">int    digits=(class="type">int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 小数位数
class="type">class="kw">double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         class=class="str">"cmt">// 点值
class="type">class="kw">double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);             class=class="str">"cmt">// 当前买入价
class="type">class="kw">double price=class="num">1000*point;                                    class=class="str">"cmt">// 未标准化的开盘价
price=NormalizeDouble(price,digits);                        class=class="str">"cmt">// 标准化开盘价
class="type">int SL_pips=class="num">300;                                             class=class="str">"cmt">// 止损点数
class="type">int TP_pips=class="num">500;                                             class=class="str">"cmt">// 获利点数
class="type">class="kw">double SL=price-SL_pips*point;                              class=class="str">"cmt">// 未标准化的止损值
SL=NormalizeDouble(SL,digits);                              class=class="str">"cmt">// 标准化止损
class="type">class="kw">double TP=price+TP_pips*point;                              class=class="str">"cmt">// 未标准化的获利值
TP=NormalizeDouble(TP,digits);                              class=class="str">"cmt">// 标准化获利
class="type">class="kw">datetime expiration=TimeTradeServer()+PeriodSeconds(PERIOD_D1);
class="type">class="kw">string comment=StringFormat("Buy Limit %s %G lots at %s, SL=%s TP=%s",
                            symbol,volume,
                            DoubleToString(price,digits),
                            DoubleToString(SL,digits),
                            DoubleToString(TP,digits));
if(!trade.BuyLimit(volume,price,symbol,SL,TP,ORDER_TIME_GTC,expiration,comment))
  {
  Print("BuyLimit() method failed. Return code=",trade.ResultRetcode(),
        ". Code description: ",trade.ResultRetcodeDescription());
  }
else
  {
  Print("BuyLimit() method executed successfully. Return code=",trade.ResultRetcode(),
        " (",trade.ResultRetcodeDescription(),")");
  }

◍ 用 BuyStop 挂单把止损止盈一次写死

在 CTrade 封装里,BuyStop 有两种用法:只传 volume 和 price 的简化版,以及把 SL、TP、偏离、过期时间全塞进去的完备版。下面这段直接以 USDJPY 为例,先取品种小数位和 point,再把挂单价定为离市价 1000 个 point 的位置,最后 NormalizeDouble 到该品种精度——外汇与贵金属杠杆高,挂单价格不规范会直接被服务器拒单。 简化调用里 volume=0.1、price 由 1000*point 算出并标准化;若 trade.BuyStop 返回 false,就打印 ResultRetcode 和 Description,成功则回显状态码。想验证就开 MT5 把 symbol 改成自己盯的币种,跑完看 Experts 日志里的 Return code 是不是 0。 完备版多了 SL_pips=300、TP_pips=500 两个变量,意味着止损 300 点、获利 500 点随挂单一起提交。回测或实盘里这类参数最好按 ATR 倍数动态算,硬编码 300/500 只适合震荡市,趋势行情可能被扫损。

MQL5 / C++
  Print("BuyLimit() method failed. Return code=",trade.ResultRetcode(),
         ". Code description: ",trade.ResultRetcodeDescription());
 }
  else
  {
   Print("BuyLimit() method executed successfully. Return code=",trade.ResultRetcode(),
         " (",trade.ResultRetcodeDescription(),")");
 }
class=class="str">"cmt">//--- class="num">1. 下买入停止挂单的例子
   class="type">class="kw">string symbol="USDJPY";    class=class="str">"cmt">// 指定下单的交易品种
   class="type">int    digits=(class="type">int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 小数位数
   class="type">class="kw">double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         class=class="str">"cmt">// 点值
   class="type">class="kw">double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);             class=class="str">"cmt">// 当前买入价
   class="type">class="kw">double price=class="num">1000*point;                                    class=class="str">"cmt">// 未标准化的开盘价
   price=NormalizeDouble(price,digits);                        class=class="str">"cmt">// 标准化开盘价
class=class="str">"cmt">//--- 一切准备就绪,将买入停止挂单发送到服务器 
   if(!trade.BuyStop(class="num">0.1,price))
   {
   class=class="str">"cmt">//--- 报错信息
    Print("BuyStop() method failed. Return code=",trade.ResultRetcode(),
         ". Code description: ",trade.ResultRetcodeDescription());
   }
   else
   {
    Print("BuyStop() method executed successfully. Return code=",trade.ResultRetcode(),
         " (",trade.ResultRetcodeDescription(),")");
   }
class=class="str">"cmt">//--- class="num">2. 下指定所有参数的买入停止挂单的例子
   class="type">class="kw">double volume=class="num">0.1;
   class="type">class="kw">string symbol="USDJPY";    class=class="str">"cmt">// 指定下单的交易品种
   class="type">int    digits=(class="type">int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 小数位数
   class="type">class="kw">double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         class=class="str">"cmt">// 点值
   class="type">class="kw">double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);             class=class="str">"cmt">// 当前买入价
   class="type">class="kw">double price=class="num">1000*point;                                    class=class="str">"cmt">// 未标准化的开盘价
   price=NormalizeDouble(price,digits);                        class=class="str">"cmt">// 标准化开盘价
   class="type">int SL_pips=class="num">300;                                             class=class="str">"cmt">// 止损点数
   class="type">int TP_pips=class="num">500;                                             class=class="str">"cmt">// 获利点数

挂单与市价单的下单收口逻辑

把止损获利算出来之后,直接丢给服务器前一定要过一遍 NormalizeDouble。MT5 里 SYMBOL_DIGITS 对 XAUUSD 通常是 2、对 EURUSD 是 5,若不标准化,price-1000*point 可能算出 1900.123456 这种脏值,下单请求会被拒。 下面这段先挂 Buy Stop:SL 取卖价减去 SL_pips 个点、TP 取加上 TP_pips 个点,都 NormalizeDouble 到 digits 位;expiration 用 TimeTradeServer()+PeriodSeconds(PERIOD_D1) 留一个次日失效。注释串用 StringFormat 拼好品种、手数、价格、SL、TP,最后 trade.BuyStop 发单,失败就 Print 回 retcode 和描述。 [CODE] double SL=price-SL_pips*point; // 未标准化的止损值 SL=NormalizeDouble(SL,digits); // 标准化止损 double TP=price+TP_pips*point; // 未标准化的获利值 TP=NormalizeDouble(TP,digits); // 标准化获利 datetime expiration=TimeTradeServer()+PeriodSeconds(PERIOD_D1); string comment=StringFormat("Buy Stop %s %G lots at %s, SL=%s TP=%s", symbol,volume, DoubleToString(price,digits), DoubleToString(SL,digits), DoubleToString(TP,digits)); //--- 一切准备就绪,将买入停止挂单发送到服务器 if(!trade.BuyStop(volume,price,symbol,SL,TP,ORDER_TIME_GTC,expiration,comment)) { //--- 报错信息 Print("BuyStop() method failed. Return code=",trade.ResultRetcode(), ". Code description: ",trade.ResultRetcodeDescription()); } else { Print("BuyStop() method executed successfully. Return code=",trade.ResultRetcode(), " (",trade.ResultRetcodeDescription(),")"); } [/CODE] 市价单更直白:digits 和 point 从 SymbolInfoInteger / SymbolInfoDouble 拿,price 取 SYMBOL_ASK;SL、TP 写死 1000 点距做演示,NormalizeDouble 收尾后 trade.PositionOpen 吃 0.1 手。外汇和贵金属杠杆高,1000 点距在黄金上约 10 美元每标准手,实盘请按自己风控改。 [CODE] //--- 小数位数 int digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS); //--- 点值 double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT); //--- 接收买入价格 double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- 计算并标准化止损和获利 double SL=NormalizeDouble(price-1000*point,digits); double TP=NormalizeDouble(price+1000*point,digits); //--- 填充备注 string comment="Buy "+_Symbol+" 0.1 at "+DoubleToString(price,digits); //--- 一切就绪,尝试建立买入持仓 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,0.1,price,SL,TP,comment)) { //--- 报错信息 Print("PositionOpen() method failed. Return code=",trade.ResultRetcode(), ". Code description: ",trade.ResultRetcodeDescription()); } else { Print("PositionOpen() method executed successfully. Return code=",trade.ResultRetcode(), " (",trade.ResultRetcodeDescription(),")"); } [/CODE] 开 MT5 新建 EA 把两段分别跑一下,看 Experts 日志里 retcode=0 是不是都打出来;若返回 4753 之类,基本就是 digits 没对齐或点距超品种限制。

MQL5 / C++
class="type">class="kw">double SL=price-SL_pips*point;                      class=class="str">"cmt">// 未标准化的止损值
SL=NormalizeDouble(SL,digits);                      class=class="str">"cmt">// 标准化止损
class="type">class="kw">double TP=price+TP_pips*point;                      class=class="str">"cmt">// 未标准化的获利值
TP=NormalizeDouble(TP,digits);                      class=class="str">"cmt">// 标准化获利
class="type">class="kw">datetime expiration=TimeTradeServer()+PeriodSeconds(PERIOD_D1);
class="type">class="kw">string comment=StringFormat("Buy Stop %s %G lots at %s, SL=%s TP=%s",
                            symbol,volume,
                            DoubleToString(price,digits),
                            DoubleToString(SL,digits),
                            DoubleToString(TP,digits));
class=class="str">"cmt">//--- 一切准备就绪,将买入停止挂单发送到服务器
if(!trade.BuyStop(volume,price,symbol,SL,TP,ORDER_TIME_GTC,expiration,comment))
  {
   class=class="str">"cmt">//--- 报错信息
   Print("BuyStop() method failed. Return code=",trade.ResultRetcode(),
         ". Code description: ",trade.ResultRetcodeDescription());
  }
else
  {
   Print("BuyStop() method executed successfully. Return code=",trade.ResultRetcode(),
         " (",trade.ResultRetcodeDescription(),")");
  }

class=class="str">"cmt">//--- 小数位数
class="type">int     digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
class=class="str">"cmt">//--- 点值
class="type">class="kw">double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
class=class="str">"cmt">//--- 接收买入价格
class="type">class="kw">double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
class=class="str">"cmt">//--- 计算并标准化止损和获利
class="type">class="kw">double SL=NormalizeDouble(price-class="num">1000*point,digits);
class="type">class="kw">double TP=NormalizeDouble(price+class="num">1000*point,digits);
class=class="str">"cmt">//--- 填充备注
class="type">class="kw">string comment="Buy "+_Symbol+" class="num">0.1 at "+DoubleToString(price,digits);
class=class="str">"cmt">//--- 一切就绪,尝试建立买入持仓
if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,class="num">0.1,price,SL,TP,comment))
  {
   class=class="str">"cmt">//--- 报错信息
   Print("PositionOpen() method failed. Return code=",trade.ResultRetcode(),
         ". Code description: ",trade.ResultRetcodeDescription());
  }
else
  {
   Print("PositionOpen() method executed successfully. Return code=",trade.ResultRetcode(),
         " (",trade.ResultRetcodeDescription(),")");
  }
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到订单流异常和挂单密度提示,你专注决策而非盯底层函数返回码。

常见问题

都走 MqlTradeRequest,但市价单填 ORDER_TYPE_BUY/SELL,挂单要选 BUY_LIMIT、SELL_STOP 等枚举并设离市价的距离,异步发单用 OrderSendAsync 不阻塞。
小布盯盘不编译代码,但能比对实盘成交与策略信号的偏离,辅助你判断是 CTrade 封装问题还是经纪商执行滑点。
CTrade 内部处理了请求初始化、错误码读取和部分重试,你只需调 Buy、Sell、BuyLimit 等方法,代码可读性更高,外汇贵金属高杠杆下少写一行少一处bug。
BUY LIMIT 到价直接挂限价买;BUY STOP LIMIT 是先到触发价挂出 STOP,再到限价位转 LIMIT,适合突破后回踩的复杂入场,概率上更防追高。