教程 · 单套解锁 教程 · EA量化教学

EA02 MQL4订单管理进阶

使用手册 + 操作教程 共 21 章全部免费开放,一步一步把软件用起来;核心知识点 6 章,¥69 一次解锁讲透原理。

作者 枢汇安全课堂 更新 2026-10-13 阅读 90 收录 EA量化教学 · 27 章
本课程 · 单套解锁
¥69¥104
单品永久解锁 · 会员可阅 · 一次购买终身使用
✓使用手册 + 操作教程 共 21 章全文免费 ✓核心知识点 6 章,¥69 一次解锁 ✓支持电脑 / 手机 · 永久免费更新
注册后解锁核心知识点
免费学习区 使用手册 + 操作教程 共 21 章全部免费开放 · 核心知识点 6 章为付费内容 ¥69 解锁核心知识点
PART ONE

使用手册

功能在哪 · 是什么 · 怎么调出来

一、ECN 账户:先下单、后设损

1、问题:单子刚发就被 130 拒绝

部分 ECN / STP 券商不允许在 OrderSend 里同时携带止损止盈——市价单发出时还不知道成交价,带着 SL/TP 会直接报 130(ERR_INVALID_STOPS)。标准解法是两步走:

// 第一步:裸单进场(sl=0, tp=0)
int ticket = OrderSend(Symbol(), OP_BUY, Lots, Ask, 30, 0, 0,
                       "ecn", 12345, 0, Blue);
if(ticket < 0)
{
   Print("ECN下单失败 error=", GetLastError());
   return;
}

// 第二步:选中刚开的单,立刻补挂 SL/TP
if(OrderSelect(ticket, SELECT_BY_TICKET))
{
   RefreshRates();
   double sl = NormalizeDouble(OrderOpenPrice() - StopLossPts * Point, Digits);
   double tp = NormalizeDouble(OrderOpenPrice() + TakeProfitPts * Point, Digits);
   if(!OrderModify(ticket, OrderOpenPrice(), sl, tp, 0, Blue))
      Print("补挂SL/TP失败 error=", GetLastError());
// 这里失败不能撤单了事:单子已经进场,必须记录并在下个tick重试补挂
}

2、两步之间的危险窗口

裸单和补挂之间有时间差,这几秒里这单是"无止损裸奔"。工程上的补救:

  • 补挂失败时把 ticket 存进待处理列表,OnTick 里持续重试;
  • EA 重启时扫描所有"本魔术号但 SL=TP=0"的持仓单,统一补挂——这一步就是第十八章对账逻辑的一部分。
白话注解:先判断你的账户是不是 ECN 模式很简单——普通账户带着 SL/TP 能下单就没问题;如果实盘反复 130 而距离明明够宽,八成是 ECN。这套两步写法在两种账户上都能跑,干脆统一用它。
PART TWO

操作教程

一步一步 · 跟着做完一件事

二、本课地图:从"会下单"到"管得住仓"

第 1 课你已经能写一个会自动开平仓的 EA。但真实市场里,下单只占 20% 的工作量,剩下 80% 是订单管理:

  • 浮盈越来越多,止损怎么自动往上挪(追踪止损);
  • 赚了一半想先落袋,怎么平掉部分仓位(部分平仓);
  • 下单被服务器拒绝,怎么知道错在哪、要不要重试(错误码);
  • 网线拔了又插上,EA 怎么确认自己没漏单(断线对账)。

本课全部围绕 MQL4 的交易函数族展开:OrderSend、OrderSelect、OrderModify、OrderClose、OrderDelete、OrderCloseBy。这 6 个函数就是你操纵订单池的全部入口,本课会逐个用透。

白话注解:第 1 课像学开车点火,本课学的是变道、倒车、刹车。不会订单管理的 EA,等于只会踩油门不会刹车。

三、订单池与遍历的标准姿势

1、一个账户就是一个订单池

MT4 里账户的所有订单(持仓单 + 挂单)都放在一个池子里。OrdersTotal() 返回池子里的总数,想操作其中任何一单,必须先用 OrderSelect() 把它"选中"。

2、OrderSelect 的两种选法、两个池

参数取值含义
selectSELECT_BY_POS按位置选,index 是 0 起的下标
selectSELECT_BY_TICKET按订单号选,index 传订单号;此时 pool 参数被忽略
poolMODE_TRADES(默认)当前持仓 + 挂单("持仓"标签页)
poolMODE_HISTORY已平仓 + 已删除订单("账户历史"标签页,受历史深度显示范围限制)

3、为什么平仓循环必须倒序

池子是个列表。你平掉了下标 2 的单子,后面所有单子的下标立刻前移。正序循环会跳单、越界。标准写法:

for(int i = OrdersTotal() - 1; i >= 0; i--)
{
   if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
   // ... 过滤 + 操作
}
白话注解:像拆一排积木,从最后一块往前拆,前面积木的位置就不会乱。只读遍历(比如统计数量)倒序正序都行,但养成倒序习惯,写平仓时就不会翻车。

四、订单属性函数全家桶

OrderSelect 选中一单后,下面这些函数返回的就是这一单的属性。这是 MQL4 用的最高频的一组函数:

函数返回备注
OrderTicket()int 订单号账户内唯一,永不变
OrderSymbol()string 品种名过滤别的品种的单
OrderType()int 订单类型OP_BUY / OP_SELL / OP_BUYLIMIT / OP_SELLLIMIT / OP_BUYSTOP / OP_SELLSTOP
OrderLots()double 手数
OrderOpenPrice()double 开仓价挂单为委托价
OrderOpenTime()datetime 开仓时间
OrderStopLoss()double 当前止损价没设为 0
OrderTakeProfit()double 当前止盈价没设为 0
OrderClosePrice()double 平仓价持仓单返回当前市价
OrderCloseTime()datetime 平仓时间>0 表示已平仓(历史池)
OrderProfit()double 浮动盈亏不含点差手续费
OrderSwap()double 库存费隔夜利息
OrderCommission()double 手续费
OrderMagicNumber()int 魔术号EA 的"签名",见第十九章
OrderComment()string 注释券商可能改写,不可作为唯一依据
OrderExpiration()datetime 挂单过期时间仅挂单有效
注意:所有 Order 属性函数都作用于"当前被选中的订单"。没选中就调用,行为不可预期(对应错误 4105 ERR_NO_ORDER_SELECTED)。

五、追踪止损(Trailing Stop)

1、原理

每来一个 tick,遍历持仓:

  • 多单浮盈超过 N 点后,把止损挪到"当前价 − N 点";
  • 只往有利方向挪,绝不往回退。

2、代码(加固版)

//+------------------------------------------------------------------+
//| 追踪止损:每个 tick 调用一次 |
//+------------------------------------------------------------------+
void TrailingStop(int trailingPoints)
{
   if(OrdersTotal() == 0) return;

   // 最小止损距离(点),见第十四章
   double stopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL);
   if(trailingPoints < stopLevel)
   {
      Print("追踪距离 ", trailingPoints, " 点小于 STOPLEVEL ", stopLevel, ",会被拒单");
      return;
   }

   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol() != Symbol()) continue;
      if(OrderMagicNumber() != 12345) continue;
      if(OrderType() != OP_BUY && OrderType() != OP_SELL) continue;

      double newSL = 0;

      if(OrderType() == OP_BUY)
      {
         double profitPoints = (Bid - OrderOpenPrice()) / Point;
         if(profitPoints >= trailingPoints)
         {
            newSL = NormalizeDouble(Bid - trailingPoints * Point, Digits);
            // 只上移不下移;无止损(0)时直接设
            if(newSL > OrderStopLoss() || OrderStopLoss() == 0)
            {
               if(!OrderModify(OrderTicket(), OrderOpenPrice(), newSL,
                               OrderTakeProfit(), 0, Blue))
                  Print("修改多单止损失败, error=", GetLastError());
            }
         }
      }
      else if(OrderType() == OP_SELL)
      {
         double profitPoints = (OrderOpenPrice() - Ask) / Point;
         if(profitPoints >= trailingPoints)
         {
            newSL = NormalizeDouble(Ask + trailingPoints * Point, Digits);
            if(newSL < OrderStopLoss() || OrderStopLoss() == 0)
            {
               if(!OrderModify(OrderTicket(), OrderOpenPrice(), newSL,
                               OrderTakeProfit(), 0, Orange))
                  Print("修改空单止损失败, error=", GetLastError());
            }
         }
      }
   }
}

3、三个不能省的细节

  • 方向判断:newSL > OrderStopLoss()(多单)不能省,否则价格回撤时止损会被改回去,追踪止损变成"追跌止损";
  • NormalizeDouble:新止损价必须按 Digits 取整,否则可能出现 1.234560000001 这种浮点尾巴,直接报 129/130;
  • 止损距离:追踪距离必须 ≥ 券商的 STOPLEVEL,否则每次改单都报 130(见第十四章)。
注意:OrderModify 里传的价格参数是"挂单新开仓价",对持仓单固定传 OrderOpenPrice(),不要自己另算。

六、保本移损(Break Even)

追踪止损之外的另一个标配:浮盈到 N 点后,把止损推到开仓价附近,这笔单最差也是保本出局。

void MoveToBreakEven(int triggerPoints, int bufferPoints)
{
   double spreadPts = MarketInfo(Symbol(), MODE_SPREAD); // 点差(点)

   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol() != Symbol()) continue;
      if(OrderMagicNumber() != 12345) continue;

      if(OrderType() == OP_BUY)
      {
         if((Bid - OrderOpenPrice()) / Point >= triggerPoints)
         {
            // 多单保本价 = 开仓价 + 点差缓冲,平掉才真不亏
            double be = NormalizeDouble(OrderOpenPrice() + (spreadPts + bufferPoints) * Point, Digits);
            if(be > OrderStopLoss() || OrderStopLoss() == 0)
               if(!OrderModify(OrderTicket(), OrderOpenPrice(), be,
                               OrderTakeProfit(), 0, Blue))
                  Print("保本移损失败, error=", GetLastError());
         }
      }
      else if(OrderType() == OP_SELL)
      {
         if((OrderOpenPrice() - Ask) / Point >= triggerPoints)
         {
            double be = NormalizeDouble(OrderOpenPrice() - (spreadPts + bufferPoints) * Point, Digits);
            if(be < OrderStopLoss() || OrderStopLoss() == 0)
               if(!OrderModify(OrderTicket(), OrderOpenPrice(), be,
                               OrderTakeProfit(), 0, Orange))
                  Print("保本移损失败, error=", GetLastError());
         }
      }
   }
}
// OnTick 里:MoveToBreakEven(30, 2); // 浮盈30点后,止损推到开仓价+点差+2点
白话注解:多单开在 1.1000,平仓价是 Bid。就算价格回到 1.1000 你平仓,拿到手也是 Bid≈1.0998(扣点差),其实是小亏。所以保本价要加一点缓冲,把点差盖住。

七、部分平仓:先平一半锁利润

1、基本写法

OrderClose 的手数参数允许小于该单总手数,就是部分平仓:

bool PartialCloseHalf(int ticket)
{
   if(!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES)) return false;

   double closeLots = NormalizeLots(OrderLots() / 2.0); // 手数规范化,见第十六章
   if(closeLots <= 0) return false;

   RefreshRates();
   double price = (OrderType() == OP_BUY) ? Bid : Ask;
   return OrderClose(ticket, closeLots, price, 30, Red);
}

2、关键真相:剩余仓位会换订单号

很多教程(包括本课旧版)都写错了一个事实,这里按实际行为纠正:

  • 部分平仓成功后,被平掉的部分按原订单号进历史;
  • 剩余仓位会得到一个新订单号继续留在持仓池里;
  • 剩余仓位的 Magic Number 不变(靠魔术号管理的 EA 不受影响);
  • 剩余仓位的 Comment 通常被改写成 "from #原订单号",但注释可能被券商覆写,不可全信;
  • 剩余仓位的开仓价、开仓时间仍保留原值。

这意味着:部分平仓后不能用旧 ticket 继续管理这单,必须重新扫描订单池。官方论坛公认的稳妥做法是:平仓前记录池内所有 ticket,平仓后重扫一遍,找到那个"多出来的新 ticket"。

// 记录平仓前的全部 ticket
int before[]; int n = 0;
ArrayResize(before, OrdersTotal());
for(int i = 0; i < OrdersTotal(); i++)
   if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      before[n++] = OrderTicket();

OrderClose(ticket, halfLots, price, 30, Red);

// 重新扫描,找出不在 before 里的新 ticket
for(int i = 0; i < OrdersTotal(); i++)
{
   if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
   bool isNew = true;
   for(int j = 0; j < n; j++)
      if(before[j] == OrderTicket()) { isNew = false; break; }
   if(isNew) Print("剩余仓位的新订单号: ", OrderTicket());
}
注意:不要用"原订单号 + 1"去猜新订单号。回测器里单号确实连号,但实盘单号没有可靠的连续规律,多账户多终端同时操作时必然猜错。

3、防零碎仓位

反复"平一半"会平出 0.01、0.02 手的零碎单,可能低于部分平仓的最低手数限制而被拒。触发前要判断:剩余手数和要平的手数都必须 ≥ MODE_MINLOT 且是 MODE_LOTSTEP 的整数倍(见第十六章)。

八、多持仓管理:计数、限仓、加仓间隔

1、统计自己 EA 的持仓

int CountPositions(int type)
{
   int count = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol() != Symbol()) continue;
      if(OrderMagicNumber() != 12345) continue;
      if(OrderType() == type) count++;
   }
   return count;
}

double TotalLots()
{
   double lots = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol() != Symbol()) continue;
      if(OrderMagicNumber() != 12345) continue;
      if(OrderType() == OP_BUY || OrderType() == OP_SELL)
         lots += OrderLots();
   }
   return lots;
}

2、开仓前的三道闸

// 三道闸:单笔数量、总手数、加仓间隔
if(CountPositions(OP_BUY) < 3 // 最多 3 笔多单
&& TotalLots() <= 1.0 // 总手数上限
&& TimeCurrent() - LastOpenTime(OP_BUY) >= 3600) // 距上次开仓至少1小时
{
   // OrderSend(...)
}
白话注解:加仓类策略不做数量上限,等于给自己装了个资金绞肉机。行情单边走,EA 会一根接一根地加仓,直到保证金被吃光。三道闸缺一不可。

九、下单前的环境自检

1、三个检查函数,各管一件事(别混用)

函数管什么返回 false 的典型场景
IsConnected()终端与服务器的网络连接断网、服务器失联
IsTradeAllowed()交易许可:自动交易按钮 + EA 属性"允许实时交易",且交易上下文空闲AutoTrading 按钮没开;EA 设置未勾允许;正有别的 EA 在交易
IsTradeContextBusy()交易上下文是否被占用同一时刻全终端只有一个交易线程,别的 EA 正在下单
纠正一个常见误解:不能用 IsTradeContextBusy() 判断断线。它检测的是"终端内部的交易线程忙不忙",和网络连接毫无关系。判断断线只能用 IsConnected()。本课旧版在这里犯过错,特此更正。

2、标准自检流程

bool TradingAllowed()
{
if(!IsConnected()) { Print("未连接服务器"); return false; }
if(IsTradeContextBusy()) { Print("交易上下文忙,稍后再试"); return false; }
if(!IsTradeAllowed()) { Print("自动交易未开启或EA无交易权限"); return false; }

   // 该品种当前是否允许交易(休市/节假日)
   if(!MarketInfo(Symbol(), MODE_TRADEALLOWED))
{ Print("品种休市"); return false; }
   return true;
}

另有两个常用的只读检查:IsDemo() 判断是否模拟账户;IsTesting() 判断是否在策略测试器里运行(有些日志输出可以只在实盘打印)。

十、保证金预检:AccountFreeMarginCheck

错误 134(资金不足)最好在发出订单之前就拦下来。MQL4 提供了官方预检函数:

double AccountFreeMarginCheck(
string symbol, // 品种
int cmd, // 只能 OP_BUY 或 OP_SELL
double volume // 手数
);

返回"这笔单开出去之后还剩多少可用保证金"。官方文档给出的用法就是一行:

if(AccountFreeMarginCheck(Symbol(), OP_BUY, Lots) <= 0 || GetLastError() == 134)
   return; // 保证金不够,放弃本笔
  • 返回值 ≤ 0:开了这笔单保证金就穿底,直接放弃;
  • 资金不足时该函数会置错误码 134(ERR_NOT_ENOUGH_MONEY);
  • 想算"一手要占多少保证金",用 MarketInfo(Symbol(), MODE_MARGINREQUIRED)。
白话注解:与其让服务器打回 134,不如下单前自己算清楚。尤其加仓网格类 EA,第 5 笔、第 6 笔单往往就是被 134 卡死的,预检可以让它优雅停手而不是疯狂重试。

十一、标准重试模板

把第八、九章的检查和第十章的错误码组合起来,就是一个能上实盘的下单模板:

int SafeOrderSend(string symbol, int cmd, double lots, double price,
                  int slippage, double sl, double tp,
                  string comment, int magic, datetime expiration, color clr)
{
   // 1) 环境自检
   if(!IsConnected() || !IsTradeAllowed()) return -1;

   // 2) 交易上下文忙就等待(最多约2.5秒)
   int wait = 0;
   while(IsTradeContextBusy() && wait < 2500)
   {
      Sleep(100);
      wait += 100;
   }

   // 3) 最多重试3次,只对"可重试"错误重试
   int ticket = -1;
   for(int attempt = 0; attempt < 3; attempt++)
   {
      if(IsTradeContextBusy()) break;

      ticket = OrderSend(symbol, cmd, lots, price, slippage, sl, tp,
                         comment, magic, expiration, clr);
      if(ticket > 0) return ticket;

      int err = GetLastError();
      Print("下单失败 attempt=", attempt, " error=", err);

      if(err == 135 || err == 136 || err == 138 || err == 129)
      {
         // 价格类错误:刷新报价,重取价再试
         Sleep(500);
         if(!RefreshRates()) break;
         price = (cmd == OP_BUY) ? Ask : Bid;
         continue;
      }
      if(err == 146)
      {
         Sleep(1000);
         continue;
      }
      // 130/131/134/148 等属于参数或资金问题,重试没有意义
      break;
   }
   return ticket;
}
两条铁律:① 收到 128/142/143 这类"结果未知"的码,重试前必须先扫一遍订单池确认没成交,否则可能开出双倍仓位;② 重试循环里每次都检查 IsTradeContextBusy()——MT4 全终端共用一个交易线程,重试时别的 EA 可能已经插队。
白话注解:Sleep() 在 EA 里可用(指标里不行)。Sleep 之后 Bid/Ask 缓存就是旧价了,必须 RefreshRates() 再取——顺序不能反。

十二、时间控制:时段过滤与收盘全平

1、判断当前服务器时间

MQL4 里 TimeCurrent() 是服务器时间(不是你电脑的本地时间)。两种取法:

// 写法一:经典函数
int h = TimeHour(TimeCurrent());
int m = TimeMinute(TimeCurrent());

// 写法二:结构体(build 600+ 之后与 MQL5 语法一致)
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
// dt.hour / dt.min / dt.day_of_week ...

2、时段过滤:只在该交易的时间交易

bool InTradingSession(int startHour, int endHour)
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   if(startHour <= endHour)
      return (dt.hour >= startHour && dt.hour < endHour);
   // 跨零点时段,例如 22 点到次日 5 点
   return (dt.hour >= startHour || dt.hour < endHour);
}
// if(!InTradingSession(9, 23)) return; // 非 9-23 点不开新仓

3、收盘全平:只执行一次,不要每 tick 都触发

void CloseAllAtTime(int hour, int minute)
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   if(dt.hour != hour || dt.min != minute) return;

   // 关键:用"天"做标记,同一时刻最多执行一轮,防止这一分钟内几千个tick反复触发
   static int lastCloseDay = -1;
   if(lastCloseDay == dt.day) return;
   lastCloseDay = dt.day;

   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol() != Symbol()) continue;
      if(OrderMagicNumber() != 12345) continue;

      RefreshRates();
      if(OrderType() == OP_BUY)
         OrderClose(OrderTicket(), OrderLots(), Bid, 30, Red);
      else if(OrderType() == OP_SELL)
         OrderClose(OrderTicket(), OrderLots(), Ask, 30, Red);
      else if(OrderType() >= OP_BUYLIMIT)
OrderDelete(OrderTicket()); // 挂单一起撤掉
   }
   Print("收盘全平完成");
}
白话注解:静态变量 lastCloseDay 是"一次性开关"。没有它, minute 等于收盘分钟的那整整 60 秒里,每个 tick 都会跑一遍全平循环——好在第二次全是报错,但这既刷日志又有踩中重复下单边角情况的风险。

十三、STOPLEVEL 与 FREEZELEVEL:止损距离的两道闸

1、两道闸是什么

两个都是通过 MarketInfo 查询的品种属性,单位都是"点":

常量值管什么
MODE_STOPLEVEL14新设 SL/TP、挂单价与现价的最小距离
MODE_FREEZELEVEL33已有订单进入"冻结区"后,不允许修改/删除/平仓

查询方式:

double stopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL); // 点
double freezeLevel = MarketInfo(Symbol(), MODE_FREEZELEVEL); // 点

2、官方文档点名的 STOPLEVEL 陷阱

文档明确说:MODE_STOPLEVEL 为 0 有两种含义——要么真的没有最小距离限制,要么券商在用服务端动态机制控距离,客户端查不到。后者被称为"浮动 stoplevel":你查到 0,实际下单还是可能报 130。所以不要写"查到 0 就随便贴价"的代码,保持一个固定的安全距离缓冲。

3、FREEZELEVEL 对管理逻辑的影响

价格逼近你的止损/止盈时,这单进入冻结区,此时 OrderModify / OrderClose 会被拒绝(可能报 145 或 130)。追踪止损、部分平仓这类"贴着市场改单"的逻辑,动手前应该先确认不在冻结区:

bool NotFrozen()
{
   double freeze = MarketInfo(Symbol(), MODE_FREEZELEVEL) * Point;
   if(freeze == 0) return true;

   // 选中的单:检查其 SL/TP/挂单价是否已进入冻结区
   if(OrderType() == OP_BUY || OrderType() == OP_SELL)
   {
if(OrderStopLoss() > 0 && MathAbs(Bid - OrderStopLoss()) < freeze) return false;
      if(OrderTakeProfit() > 0 && MathAbs(Bid - OrderTakeProfit()) < freeze) return false;
   }
   return true;
}
白话注解:STOPLEVEL 管"新的距离够不够远",FREEZELEVEL 管"快成交的单别再动"。冻结区里改单必然失败,与其撞墙重试,不如先查再动。

十四、点与 pip:三位 / 五位小数换算

1、Point ≠ pip

老行情(4 位小数报价)里 1 pip = 1 Point;现代 5 位小数报价里 1 pip = 10 Point。同一句"追踪 50 点",在 5 位报价的账户上实际只追了 5 pip,止损贴脸。统一写法:

double PipPoint()
{
   // 3/5 位小数:1 pip = 10 * Point;2/4 位小数:1 pip = 1 * Point
   if(Digits == 3 || Digits == 5) return Point * 10;
   return Point;
}
// 用 pip 思维写参数,内部换算成 Point
double trailing = 50 * PipPoint(); // 追踪 50 pip

2、滑点参数同理

OrderSend / OrderClose 的 slippage 参数单位也是 Point。老代码里的 3 在 5 位报价账户上等于 0.3 pip,几乎必然被拒:

int slippagePoints = 3;
if(Digits == 3 || Digits == 5) slippagePoints *= 10; // 3 pip → 30 Point
注意:Digits 和 Point 是当前图表品种的预定义变量;操作非当前图表品种时,用 MarketInfo(sym, MODE_DIGITS) 和 MarketInfo(sym, MODE_POINT) 取对应品种的值,别拿图表品种的值去算别的品种。

十五、手数规范化:MINLOT / LOTSTEP / MAXLOT

1、三个常量

double minLot = MarketInfo(Symbol(), MODE_MINLOT); // 最小手数
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT); // 最大手数
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP); // 手数步长(常见0.01或0.1)

131(无效手数)的全部成因都在这三个数里:低于最小值、不是步长整数倍、超过最大值。

2、通用规范化函数

double NormalizeLots(double lots)
{
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
   double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);

   if(lotStep > 0)
lots = MathFloor(lots / lotStep) * lotStep; // 向下取整到合法步长
   lots = MathMax(minLot, MathMin(maxLot, lots));
   return NormalizeDouble(lots, 2);
}
// 例:算出 0.137 手 → LOTSTEP 0.01 → 规范成 0.13;算出 0.004 → 提到最小 0.01
白话注解:部分平仓最容易踩这里——1.00 手平一半 0.50 没问题;但 0.03 手平一半算出 0.015,直接 131 被拒。所有"算出来的手数"都必须过一遍 NormalizeLots。向下取整是为了宁可少平不多平。

十六、挂单管理:OrderModify / OrderDelete / 过期时间

1、挂单六兄弟

常量含义触发方向
OP_BUYLIMIT限价买价格跌到委托价以下买进
OP_SELLLIMIT限价卖价格涨到委托价以上卖出
OP_BUYSTOP突破买价格涨到委托价以上买进
OP_SELLSTOP突破卖价格跌到委托价以下卖出

判断是否挂单:OrderType() > OP_SELL(即值大于 1)。

2、改价与撤单

// 挂单改价/改SL/TP/过期时间,都用 OrderModify
// 注意第二个参数此时是"新委托价",与持仓单含义不同
if(!OrderModify(ticket, newPrice, newSL, newTP, newExpiration, Yellow))
   Print("挂单修改失败 error=", GetLastError());

// 撤单用 OrderDelete,不是 OrderClose
if(!OrderDelete(ticket))
   Print("撤单失败 error=", GetLastError());

3、过期时间(expiration)

  • OrderSend / OrderModify 都能设过期时间,到点券商自动撤单;
  • 不是所有券商都支持:不支持时挂单报 147(ERR_TRADE_EXPIRATION_DENIED),此时 expiration 传 0(永不过期),用第十二章的定时逻辑自己撤;
  • 挂单到期前的策略逻辑:突破单如果一直没触发,行情逻辑已经变了,与其让它"守株待兔",不如定期清理超过 N 根K线的旧挂单。

十七、断线重连与状态对账

1、检测断线:IsConnected

断线期间 EA 收不到 tick,OnTick 不会被调用——这正是问题所在:恢复后的第一个 tick 才是补救的时机。

void OnTick()
{
   static bool wasConnected = true;
bool nowConnected = IsConnected(); // 用 IsConnected,不是 IsTradeContextBusy

   if(wasConnected && !nowConnected)
   {
      // 通常收不到这个打印:断线时没有 tick,OnTick 不进来
      Print("连接断开");
   }
   else if(!wasConnected && nowConnected)
   {
      Print("重连成功,开始对账");
      ResyncPositions();
   }
   wasConnected = nowConnected;

   // ... 正常交易逻辑
}

2、对账要查什么

断线期间的"未知状态"主要有四种,重连后逐一核对:

未知状态核对方法
重发的订单有没有实际成交按 comment/魔术号扫 MODE_TRADES,比对下单前的 ticket 快照
持仓里有没有"裸单"(SL=TP=0)扫出魔术号匹配但 OrderStopLoss()==0 且 OrderTakeProfit()==0 的单,按第十三章补挂
该撤的挂单还在不在按时间/价格条件重扫挂单,过期的撤掉
本地缓存的 ticket 表还准不准全部作废,从 OrdersTotal() 重建
void ResyncPositions()
{
int bare = 0; // 裸单计数
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderMagicNumber() != 12345) continue;

      if((OrderType() == OP_BUY || OrderType() == OP_SELL)
         && OrderStopLoss() == 0 && OrderTakeProfit() == 0)
      {
         bare++;
         // TODO: 按第十三章两步法补挂 SL/TP
      }
   }
   Print("对账完成:本EA持仓 ", CountPositions(OP_BUY) + CountPositions(OP_SELL),
         " 笔,其中裸单 ", bare, " 笔待补挂");
}
注意:重连后立刻下单可能撞上 128 超时或 142/143 状态码(连接还在恢复中),对账逻辑先跑,交易动作缓一两个 tick 再恢复,并配合第十一章"结果未知先查成交"的铁律。

十八、多 EA 共存:Magic Number 与 Comment 规范

1、魔术号是唯一可靠的"所有权标记"

账户里同时跑多个 EA、还有手工单,怎么分清谁的单?唯一的可靠依据是 Magic Number:

  • 每个 EA 用一个专属整数(如 12345、20260102),写进 OrderSend 的 magic 参数;
  • 遍历时用 OrderMagicNumber() != 我的魔术号 过滤;
  • 部分平仓后魔术号不变(第六章),所以所有权标记终身有效;
  • 魔术号开单后无法修改——别指望开单后"改名"。

2、Comment 只能当参考

comment 参数看似好用,实际有三个坑:

  • 部分平仓时 comment 通常被改写成 "from #原单号",你自己写的内容被覆盖;
  • 部分券商在出入金调整、服务器事件时也会改写注释;
  • 不同券商对 comment 长度和字符的限制不同。

结论:comment 只用于人类阅读的备注,任何业务判断都不要依赖它。

白话注解:魔术号是身份证,comment 是胸前名牌。身份证跟人一辈子,名牌随时可能被别人摘了换掉。认身份证,别认名牌。

十九、防重复下单:一个信号只开一单

1、问题根源:一根 K 线几千个 tick

信号是"均线上穿"这种状态,它在这根 K 线的每个 tick 都成立。不做防护,一根 K 线能给你开出几十单。

2、方案一:每根 K 线只评估一次

void OnTick()
{
   static datetime lastBar = 0;
if(Time[0] == lastBar) return; // 还是同一根K线,直接跳过
   lastBar = Time[0];

   // 到这里说明是新K线的第一个tick,信号判断只在这里做
   // ...
}

3、方案二:信号触发即锁定,直到信号消失

static bool buySignalActive = false;

bool crossUp = (FastMA[1] > SlowMA[1] && FastMA[2] <= SlowMA[2]);

if(crossUp && !buySignalActive)
{
buySignalActive = true; // 锁定
SafeOrderSend(...); // 下单
}
if(!crossUp) buySignalActive = false; // 信号消失才解锁

4、方案三:持仓数量兜底

前两个方案都依赖程序状态,EA 重启后状态清零。最硬的兜底是查池子(第七章):

if(CountPositions(OP_BUY) == 0) // 池子里没有我的多单才开
{
   // OrderSend(...)
}
白话注解:三道防线各挡一层——按K线节流、按信号锁定、按持仓兜底。重启不丢状态的只有第三道,因为它查的是服务器里的真实订单池,不是 EA 自己的记忆。

二十、OnTimer:无 tick 时段的自救

1、问题:冷门品种半夜没有 tick

OnTick 只在有新报价时触发。凌晨冷清时段、流动性极差的品种,可能几分钟没有 tick——你的断线检测、到期全平、补挂裸单逻辑全部停摆,因为它们都挂在 OnTick 里。

2、解法:定时器事件

int OnInit()
{
EventSetTimer(1); // 每 1 秒触发一次 OnTimer
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
EventKillTimer(); // 必须释放,否则EA卸载后定时器残留
}

void OnTimer()
{
   // 放"不依赖tick"的维护任务:
   if(!IsConnected()) { Print("定时器发现断线"); return; }

   // 补挂裸单、检查收盘全平时间、清理过期挂单……
   ResyncPositions();
}

void OnTick()
{
   // 放交易信号等对时效敏感的逻辑
}
  • EventSetTimer(seconds) 单位是秒;EventSetMillisecondTimer(ms) 可到毫秒级(耗资源,一般用不上);
  • 定时器事件与 tick 事件不会同时执行(同一 EA 内部串行),不用额外加锁;
  • 回测器里定时器事件的模拟支持有限,OnTimer 逻辑要在实盘/模拟盘上单独验证。
白话注解:OnTick 是"来活儿才干",OnTimer 是"闹钟到点必干"。断线检测这种性命攸关的事,交给闹钟比交给"等行情叫你"可靠得多。

二十一、下一课预告

第 3 课《MQL4 迁移 MQL5 指南》已上线:一个订单池变三个集合、CTrade 类、OnTradeTransaction、净持仓与锁仓的区别。如果你要升级 MT5,从那一课开始。

本站固定免责声明:本教程仅讲解软件编程与操作方法,不构成投资建议或策略推荐。回测业绩不代表未来收益,EA自动交易风险大,请谨慎决策。
付费专享 · 核心知识点 6 章

继续阅读:核心知识点,搞懂原理

EA02 MQL4订单管理进阶 — 共 27 章,已免费阅读 21 章,剩余 6 章核心知识点讲透「为什么这样、点了会怎样」。

二十二、错误码全表:OrderSend 失败后先看这里
二十三、常见坑 Top 10
二十四、FAQ
二十五、速查表
二十六、学习路线
二十七、快速上手
✍️ 注册后扫码购买 🔐 已有账号,去登录 单套 ¥69 永久 · 会员可阅
SERVICE & PRICING
服务收费说明 · 明码标价,无隐藏收费
全部课程统一 ¥39-69,买一套永久使用。以下为购买与使用的完整流程。
01先免费试读

每套教程的「使用手册」部分均可免费阅读,先确认内容是否适合自己再决定,不必盲买。

02扫码付款

选定课程后点「立即解锁」,页面展示收款二维码与客服微信,扫码转账即可,无需注册海外支付。

03获取卡密

付款完成后客服第一时间发送卡密号与卡密密码,站内消息与微信同步通知。

04兑换解锁

在用户中心「兑换卡密」输入卡密号与密码,系统即时解锁全部章节,无需二次付款。

一次付费,终身可用 · 解锁后课程永久留在账号里,不限次数、不限时间阅读 持续免费更新 · 平台改版、规则变化时课程同步修订
RECOMMEND
相关教程推荐 · 同栏目更多上手机会

💬 用户评价 (0)

🔒 登录并成为会员后可发表评价
还没有评价 · 成为第一个评价的人吧 🎯