构建自动运行的 EA(第 02 部分):开始编码·进阶篇
⚙️

构建自动运行的 EA(第 02 部分):开始编码·进阶篇

(2/3)· 从空文件起步写出可发送市价单与挂单的 C_Orders 类,不再被标准库封装绕晕

实战向 第 2/3 篇

很多交易者直接调用标准库发单,却看不懂订单请求在终端里到底走了哪一步。一旦报错只会对着日志发呆,因为从没见过黑匣子里的结构。本篇接上篇概念,带你从零写一个自己的订单类。

◍ EA 下单类该从哪下手

写交易类库的人常有个误区:以为要一口气把移单、删单、平仓、图表标记全包了。其实 MT5 终端的「交易」标签和图表本身已经处理了这些,EA 真正缺的只是绕过手动下单界面、直接往服务器发指令的通道。 所以 C_Orders 类早期唯一刚需是「发单函数」。但为了少调标准库,先把资产上下文缓存成私有变量:报价精度、最小成交量、跳步、每跳价值、点值、以及图表是按 BID 还是 LAST 绘制。这些用 private 声明,类外不可见,只在类内流通。 初始化别靠记忆。默认构造函数 C_Orders() 不收参,但必须把上面每个字段填上,否则后面算手数或挂单价时容易溢出或读脏值。对应地要写析构函数 ~C_Orders(),名字同类名前加波浪号,二者都不能有返回值,否则编译直接报错。 外汇和股票不是同一套绘图逻辑。外汇图表基于 BID,LAST 价格恒为 0;股票用 LAST,BID/ASK 差始终大于 0。若 EA 不判别当前是哪种绘制系统,挂单可能填错类型——外汇里发基于 LAST 的挂单会被拒,股票里误用 BID 算价会卡在点差外。构造函数里顺手读 SYMBOL_DIGITS 等,就是给跨市场移植留后路。 下面这段是类骨架与构造初始化的最小可编译形态,注意 m_Infos 结构体把跨市场判别字段 PlotLast 也收进去了:

MQL5 / C++
class C_Orders
{
    class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
        class="type">MqlTradeRequest m_TradeRequest;
        class="kw">struct st00
        {
            class="type">int     nDigits;
            class="type">class="kw">double  VolMinimal,
                    VolStep,
                    PointPerTick,
                    ValuePerPoint,
                    AdjustToTrade;
            class="type">bool    PlotLast;
        }m_Infos;
        C_Orders()
        {
            m_Infos.nDigits        = (class="type">int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
            m_Infos.VolMinimal     = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
        }
};

把合约规格塞进结构体再算调整系数

EA 初始化时先把当前品种的合约参数读进自定义结构体,后面下单和画图都直接复用,不反复调系统函数。 下面这段把交易量最小步长、每跳点值、每跳金额分别存好,再用金额除以点值得到 AdjustToTrade,相当于把报价单位换算成账户货币的系数。 m_Infos.VolStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); m_Infos.PointPerTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.AdjustToTrade = m_Infos.ValuePerPoint / m_Infos.PointPerTick; m_Infos.PlotLast = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST); 最后一行判断图表用的是收盘价还是最新价模式,PlotLast 为真时画线贴最新成交价,否则贴收盘价。外汇与贵金属杠杆高,点值随品种和杠杆浮动,实盘前应在 MT5 用具体品种跑一遍看 AdjustToTrade 是否和手册一致。

MQL5 / C++
m_Infos.VolStep       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
m_Infos.PointPerTick  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
m_Infos.ValuePerPoint = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
m_Infos.AdjustToTrade = m_Infos.ValuePerPoint / m_Infos.PointPerTick;
m_Infos.PlotLast      = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST);

「把挂单请求安全地递给服务器」

在 MQL5 里直接向交易服务器发挂单,核心就是 OrderSend。但这个函数最好藏进类的私有段,EA 只调公开封装函数,避免到处手写请求结构而出错。 下面这段是请求发送前的标准动作:先清掉上一次的错误码,再把检查结构和结果结构清零,然后跑一次 OrderCheck 看请求合不合法。

MQL5 / C++
class="type">class="kw">ulong ToServer(class="type">void)
{
   MqlTradeCheckResult TradeCheck;
   class="type">MqlTradeResult     TradeResult;
   class="type">bool bTmp;

   ResetLastError();
   ZeroMemory(TradeCheck);
   ZeroMemory(TradeResult);
   bTmp = OrderCheck(m_TradeRequest, TradeCheck);
逐行拆解:第 1 行定义返回订单号的函数;第 3–4 行声明检查结果与成交结果容器;第 6 行 ResetLastError 把 _LastError 归零,方便定位本次错误;第 7–8 行 ZeroMemory 清空两个结构,防止残留字段干扰;第 9 行 OrderCheck 用私有变量 m_TradeRequest 做预检,返回布尔值存进 bTmp。 OrderSend 只回两种值:成功给订单单号(ulong),失败给 0。真要查哪错了,去读 _LastError,而不是只看返回假。 服务器不会接受随便填的价格。外汇里点数是 0.00001,股票可能完全不同;若用规范化还不够,要做一次换算把价格校正到服务器能收的区间。填 MqlTradeRequest 是手工和自动 EA 翻车的高发区,用类做抽象能少踩坑。 发挂单前先想清几个量:买卖方向、挂单价、交易量、订单类型(buy limit / buy stop / sell limit / sell stop)、市场(外汇或股票)、止损与止盈。若财务参数传 0,函数就不生成止损止盈;买入单的负止损、卖出单的负止盈会被直接忽略,只用正数。 财务值转点数靠 FinanceToPoints:以最小交易量 100、倍数 x3、点值 0.01 为例,若用户愿担 250 风险,算出来偏移就是 250 点(或折算 2.5 个计价单位)。外汇点数是 0.00001,B3 的美元合约点数可能是 10,抓错点值就会算歪。波动大时让 EA 秒算规模,比手工估更不易超风险限额。外汇与贵金属杠杆高,错填参数会导致订单拒单或意外敞口,务必在策略测试器里先跑一遍。

MQL5 / C++
class="type">class="kw">ulong ToServer(class="type">void)
{
   MqlTradeCheckResult TradeCheck;
   class="type">MqlTradeResult     TradeResult;
   class="type">bool bTmp;

   ResetLastError();
   ZeroMemory(TradeCheck);
   ZeroMemory(TradeResult);
   bTmp = OrderCheck(m_TradeRequest, TradeCheck);

◍ 挂单价格的对齐与请求封装

挂单能不能顺利递到 MT5 交易服务器,第一道坎是价格对齐。AdjustPrice 用 MathRound(value / m_Infos.PointPerTick) * m_Infos.PointPerTick 把任意输入价吸附到 tick 粒度,避免因为小数位超出品种精度被经纪商拒单。 CreateOrder 里先调 AdjustPrice(Price) 规整入场价,再按 m_Infos.PlotLast 决定取 SYMBOL_LAST 还是 SYMBOL_BID 作基准,ask 在非 PlotLast 模式下单独取 SYMBOL_ASK。这套逻辑让 EA 在现货黄金(XAUUSD)这类点值特殊的品种上,挂单偏移计算倾向更稳定。 随后 ZeroMemory 清空请求结构,把 action 置为 TRADE_ACTION_PENDING、symbol 填为 _Symbol,准备发挂单。外汇与贵金属杠杆波动剧烈,错误价格对齐可能瞬间触发滑点或拒单,开 MT5 用策略测试器跑一段 XAUUSD 的 M1 回测就能验证对齐效果。

MQL5 / C++
if (_LastError == ERR_SUCCESS) bTmp = OrderSend(m_TradeRequest, TradeResult);
if (_LastError != ERR_SUCCESS) MessageBox(StringFormat("Error Number: %d", GetLastError()), "Order System", MB_OK);

class="kw">return (_LastError == ERR_SUCCESS ? TradeResult.order : class="num">0);
}
class="kw">inline class="type">class="kw">double AdjustPrice(class="kw">const class="type">class="kw">double value)
  {
  class="kw">return MathRound(value / m_Infos.PointPerTick) * m_Infos.PointPerTick;
  }
  class="type">class="kw">ulong CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="type">class="kw">double Price, class="kw">const class="type">class="kw">double FinanceStop, class="kw">const class="type">class="kw">double FinanceTake, class="kw">const class="type">uint Leverage, class="kw">const class="type">bool IsDayTrade)
  {
  class="type">class="kw">double  bid, ask, Desloc;

  Price = AdjustPrice(Price);
  bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
  ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
  ZeroMemory(m_TradeRequest);
  m_TradeRequest.action       = TRADE_ACTION_PENDING;
  m_TradeRequest.symbol       = _Symbol;

挂单与止损止盈的字段拼装逻辑

这段赋值块把一笔待发订单的所有核心字段一次性填好,重点在 volume、type、sl、tp 四个量的推导方式,复制进 EA 的订单构建函数即可直接编译跑。 volume 用 VolMinimal 加 VolStep 乘 (Leverage-1) 再 NormalizeDouble 到品种小数位,意味着杠杆每升一档就多跳一个最小步长,而不是按资金比例算手数。 type 的判断很直白:买向时若 ask>=Price 挂 BUY_LIMIT,否则 BUY_STOP;卖向时 bid<Price 挂 SELL_LIMIT,否则 SELL_STOP,价格落在市价哪一侧决定限价还是停损。 sl 与 tp 都先由 FinanceToPoints 把金额止损/止盈转成点值 Desloc,为 0 就填 0 不挂;买单 sl 在 Price 减 Desloc、tp 加 Desloc,卖单符号反向,偏离写死 1000 点容错。 外汇与贵金属杠杆放大波动,这类自动拼单在极端跳空时可能触发滑点以外的拒单,上 MT5 用策略测试器改 Leverage 和 FinanceStop 看成交率变化最直观。

MQL5 / C++
m_TradeRequest.volume       = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - class="num">1)), m_Infos.nDigits);
m_TradeRequest.type         = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) :
                              (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
m_TradeRequest.price        = NormalizeDouble(Price, m_Infos.nDigits);
Desloc = FinanceToPoints(FinanceStop, Leverage);
m_TradeRequest.sl           = NormalizeDouble(Desloc == class="num">0 ? class="num">0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -class="num">1 : class="num">1)), m_Infos.nDigits);
Desloc = FinanceToPoints(FinanceTake, Leverage);
m_TradeRequest.tp           = NormalizeDouble(Desloc == class="num">0 ? class="num">0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? class="num">1 : -class="num">1)), m_Infos.nDigits);
m_TradeRequest.type_time    = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
m_TradeRequest.type_filling = ORDER_FILLING_RETURN;
m_TradeRequest.deviation   = class="num">1000;
m_TradeRequest.comment     = "Order Generated by Experts Advisor.";
把发单逻辑交给小布验证
这些订单类的请求构造思路,小布盯盘已内置到品种页的诊断里,你写完后可直接比对实盘回执差异,把重复劳动交给小布,你专注决策。

常见问题

标准库封装了细节,便于快速发单,但隐藏了请求结构。自己写能看清参数如何组装、服务器如何响应,后续定制风控和重试更可控。
市价单用 ORDER_TYPE_BUY/SELL 配合即时成交,挂单需指定 ORDER_TYPE_BUY_LIMIT 等类型和挂单价,且请求字段里的成交条件不同。
向导生成的类带预定义格式,本系列为讲清底层,要求从头写,避免被模板代码干扰对结构的理解。
可以,小布盯盘的品种页内置了订单请求诊断,能对照你 C_Orders 类的构造看回执是否一致,省去手动比对。
篇面对新手做了简化解释,但需投入精力理解结构。外汇贵金属自动交易高风险,先弄懂再上模拟盘更稳妥。