在单一工具上使用不同的 EA 交易进行交易时 ORDER_MAGIC 的使用(基础篇)
📘

在单一工具上使用不同的 EA 交易进行交易时 ORDER_MAGIC 的使用(基础篇)

第 1/3 篇

用 ORDER_MAGIC 隔离同一品种的多个 EA

在同一交易品种上同时挂多个 EA 时,订单归属最容易乱。MT5 里每个订单、持仓、挂单都带一个 magic number(魔术码),本质是整型标识,用来区分「这笔单是哪个程序下的」。 不显式设置 magic,所有 EA 会共用 0 值,平仓、修改、统计时就会互相踩。给每个 EA 分配独立 ORDER_MAGIC,才能在代码中精准筛选属于自己的仓位。 实测场景:一个 XAUUSD 图表上跑两个马丁 EA,若 magic 都为 0,其中一个触发风控平仓会把另一个的浮亏单也清掉。分开设 magic 后,各自遍历自己订单,互不干扰。外汇与贵金属杠杆高,多 EA 同品种叠加会放大回撤风险,隔离标识是底线操作。

「被浪费的 18 万亿级幻数空间」

MQL5 里每笔挂单都能写一个 magic number(幻数),用来标记这是哪个 EA 下的单。多数开发者把它当成随便填的记号,实际上幻数类型是 ulong,上限 18 446 744 073 709 551 615,空间大到能单独编码上千个 EA 还有余。 现实很尴尬:圈子里一半人用 12345,另一半扎堆 55555、33333、77777。光用数字 1000 就足够给所有 EA 起独立名号,而 ulong 还剩 15 个完整数位类别没动。按编码规则把「EA ID + 品种 + 友邻标识」压进幻数,多 EA 协作才不会互相踩脚。 把幻数只当 ID 不够。EA 跑在 EURUSD 不代表委托只显在这品种,把品种和运行身份一起编进幻数,EA 才能认出「这单是自己人下的」。两个 EA 同品种不同周期(如 M1 与 H1 的 EURUSD)若共享幻数,会抢着改同一仓位导致冲突——图2 就是实证。 下面这段辅助类演示了怎么在发单时把 magic 写进请求结构。trades.magic = magic 那行就是 EA 印记,后面价格、SL、TP 都按类型算好再填。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 这个类提供辅助交易计算                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------
class CProvision
  {
class="kw">protected:
class="type">MqlTradeRequest   trades;                 class=class="str">"cmt">// OrderSend 请求结构的指针
class="kw">public:
   class="type">int                TYPE(const class="type">class="kw">double &v[]);  class=class="str">"cmt">// 判断类型,相对于移动的读数
   class="type">class="kw">double             pricetype(class="type">int type);     class=class="str">"cmt">// 计算开仓位置, 相对于类型 
   class="type">class="kw">double             SLtype(class="type">int type);        class=class="str">"cmt">// 计算止损位置,相对于类型
   class="type">class="kw">double             TPtype(class="type">int type);        class=class="str">"cmt">// 计算时间表位置, 相对于类型
   class="type">long               spread();               class=class="str">"cmt">// 返回当前金融工具点差
   class="type">int                SendOrder(ENUM_ORDER_TYPE type,class="type">class="kw">double volume);
   };
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//|                                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------
class="type">int CProvision::SendOrder(ENUM_ORDER_TYPE type,class="type">class="kw">double volume)
  {
   trades.action       =TRADE_ACTION_DEAL;      class=class="str">"cmt">// 实施操作类型
   trades.magic        =magic;                 class=class="str">"cmt">// EA 印记 (magic number 的标识符)
   trades.symbol       =_Symbol;               class=class="str">"cmt">// 交易工具的名称
   trades.volume       =volume;                class=class="str">"cmt">// 交易请求的交易量
   trades.price        =pricetype((class="type">int)type);  class=class="str">"cmt">// 价格        
   trades.sl           =SLtype((class="type">int)type);     class=class="str">"cmt">// 订单止损位置

◍ 挂单参数与方向判定的底层函数

这段逻辑把下单请求的结构体字段一次性填完:止盈位由 TPtype 按方向算,偏离点数直接取当前点差的整数值,成交方式锁死为 ORDER_FILLING_FOK,也就是要么按价全成要么不吃单。 TYPE 函数拿数组前两个元素做差,t 为 0 时强行置 1.0 避免除零,再用 0.5*t/fabs(t)+0.5 把结果映射成 0 或 1 的方向整数——相当于把价格波动读数压缩成多空标签。 pricetype、SLtype、TPtype 三个函数都先 SymbolInfoTick 抓实时 tick:type=0 走 ask 基准,type=1 走 bid 基准。SL 和 TP 以 SYMBOL_POINT 为最小单位乘系数偏移,外汇与贵金属点值随品种跳动,实盘前应在 MT5 用 Print(SymbolInfoDouble(_Symbol,SYMBOL_POINT)) 确认步长。 spread 函数仅返回 SYMBOL_SPREAD 整数,意味着偏离容差等于经纪商报的点差,在黄金跳空时段点差可能瞬间扩到平时 3~5 倍,FOK 单被拒概率会明显上升。

MQL5 / C++
  trades.tp                 =TPtype((class="type">int)type);     class=class="str">"cmt">// 订单获利位置
  trades.deviation=(class="type">int)spread();               class=class="str">"cmt">// 距请求价格的最大偏离点数
  trades.type=type;                             class=class="str">"cmt">// 订单类型
  trades.type_filling=ORDER_FILLING_FOK;
  if(OrderSend(trades,res)){class="kw">return(res.retcode);}
  class="kw">return(-class="num">1);
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 判断类型,相对于移动的读数                                         |
class=class="str">"cmt">//+------------------------------------------------------------------
class="type">int CProvision::TYPE(const class="type">class="kw">double &v[])
  {
  class="type">class="kw">double t=v[class="num">0]-v[class="num">1];
  if(t==class="num">0.0)t=class="num">1.0;
  class="kw">return((class="type">int)(class="num">0.5*t/fabs(t)+class="num">0.5));
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 计算开仓位置, 相对于类型                                          |
class=class="str">"cmt">//+------------------------------------------------------------------
 class="type">class="kw">double CProvision::pricetype(class="type">int type)
  {
  if(SymbolInfoTick(_Symbol,tick))
    {
      if(type==class="num">0)class="kw">return(tick.ask);
      if(type==class="num">1)class="kw">return(tick.bid);
    }
  class="kw">return(-class="num">1);
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 计算止损位置, 相对于类型                                          |
class=class="str">"cmt">//+------------------------------------------------------------------
 class="type">class="kw">double CProvision::SLtype(class="type">int type)
  {
  if(SymbolInfoTick(_Symbol,tick))
    {
      if(type==class="num">0)class="kw">return(tick.bid-SL*SymbolInfoDouble(Symbol(),SYMBOL_POINT));
      if(type==class="num">1)class="kw">return(tick.ask+SL*SymbolInfoDouble(Symbol(),SYMBOL_POINT));
    }
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 计算时间帧位置, 相对于类型                                        |
class=class="str">"cmt">//+------------------------------------------------------------------
 class="type">class="kw">double CProvision::TPtype(class="type">int type)
  {
  if(SymbolInfoTick(_Symbol,tick))
    {
      if(type==class="num">0)class="kw">return(tick.bid+TP*SymbolInfoDouble(Symbol(),SYMBOL_POINT));
      if(type==class="num">1)class="kw">return(tick.ask-TP*SymbolInfoDouble(Symbol(),SYMBOL_POINT));
    }
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 返回点差                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------
 class="type">long CProvision::spread()
  {
  class="kw">return(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
  }

均线开关仓的句柄与缓存写法

下面这段 EA 骨架把均线信号转成开平仓动作,核心是先拿指标句柄、再用 CopyBuffer 取最近两根 MA 值。输入参数里 SL=300 点、TP=1000 点、MA_Period=25、lot=0.1,属典型小止损宽止盈的剥头皮式配置,外汇与贵金属品种下杠杆放大,这类参数回撤可能很快,实盘前务必在策略测试器跑历史数据。 初始化里只用 iMA() 建句柄,周期传 0 表示当前图表周期,平滑用 MODE_SMA、价格用 PRICE_OPEN,意味着信号看每根 K 线开盘价的 25 期简单均线。OnTick 中 CopyBuffer 取 2 个值进 v[2],若返回值 ≤0 直接打印错误并退出,避免脏数据触发下单。 prov.TYPE(v) 把双值缓存判成方向类型 type_MA,随后 PositionSelect(_Symbol) 查当前品种有无持仓;若有且持仓方向与 type_MA 不一致,就进入平仓判定分支。整段没硬编码买卖价,具体发单逻辑留待后续小节,但句柄与缓存这段已是 MT5 跑通策略的必经路。

MQL5 / C++
class=class="str">"cmt">//--- 输入参数
class="kw">input class="type">class="kw">ulong     magic     =class="num">1;        class=class="str">"cmt">// 标识号
class="kw">input class="type">int       SL        =class="num">300;      class=class="str">"cmt">// 止损
class="kw">input class="type">int       TP        =class="num">1000;     class=class="str">"cmt">// 获利
class="kw">input class="type">int       MA_Period =class="num">25;       class=class="str">"cmt">// MA 周期
class="kw">input class="type">class="kw">double    lot       =class="num">0.1;      class=class="str">"cmt">// 仓位持仓量
class="kw">input class="type">int       MA_shift  =class="num">0;        class=class="str">"cmt">// 指标偏移
class="kw">input ENUM_MA_METHOD    MA_smooth  =MODE_SMA;    class=class="str">"cmt">// 平滑类型
class="kw">input ENUM_APPLIED_PRICE price      =PRICE_OPEN;  class=class="str">"cmt">// 价格类型
class=class="str">"cmt">//--- 我们将保存指标句柄
class="type">int
MA_handle,   class=class="str">"cmt">// 指标句柄
type_MA,     class=class="str">"cmt">// MA 的指定方向类型
rezult;      class=class="str">"cmt">// 存放 OrderSend 操作的结果变量
class="type">class="kw">double v[class="num">2];  class=class="str">"cmt">// 接受 MA 数值的缓存区
class="type">MqlTradeResult  res;   class=class="str">"cmt">// OrderSend 响应结构的指针
class="type">MqlTick         tick;  class=class="str">"cmt">// 最后市场信息结构的指针
CProvision      prov;
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| EA 初始化函数
class=class="str">"cmt">//+------------------------------------------------------------------
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建指标的句柄
   MA_handle=iMA(Symbol(),class="num">0,MA_Period,MA_shift,MA_smooth,price);
   class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| EA 即时价格处理函数
class=class="str">"cmt">//+------------------------------------------------------------------
class="type">void OnTick()
  {
   if(CopyBuffer(MA_handle,class="num">0,class="num">0,class="num">2,v)<=class="num">0)
     {Print("#",magic,"复制错误");class="kw">return;}
   type_MA=prov.TYPE(v); class=class="str">"cmt">// 依据 MA 指示判断类型
   if(PositionSelect(_Symbol))class=class="str">"cmt">// 如果此时有开仓 
     {
      if(PositionGetInteger(POSITION_TYPE)!=type_MA)class=class="str">"cmt">// 检查是否是平仓时机
        {

「反手与首仓的下单分支」

这段逻辑把「已有反向仓位」和「无任何仓位」两种情况分开了。若检测到同 magic 标识下存在持仓,策略会先打印该仓的成交量与类型,再以原持仓量加新增 lot 的方式反手加仓,外汇与贵金属品种在此处杠杆放大,高风险属性不容忽视。 无仓位时则直接以 lot 手数首开。两种路径都走 prov.SendOrder 并判断返回值:不等于 -1 视为发单成功,打印结果代码与成交量;等于 -1 则输出 GetLastError 并 return 中断,避免错误单继续堆叠。 想验证分支是否生效,把下面代码贴进 EA 的持仓判断段,观察终端里 #magic 开头的日志,能直接看到反向仓和首仓各自的 SendOrder 反馈。

MQL5 / C++
Print("#",magic,"标识号对应的仓位持仓量 ",PositionGetDouble(POSITION_VOLUME),
      " 反向仓位类型 ",PositionGetInteger(POSITION_TYPE)," 依据 ",type_MA);
      rezult=prov.SendOrder((ENUM_ORDER_TYPE)type_MA,PositionGetDouble(POSITION_VOLUME)+lot);
      class=class="str">"cmt">// 反向仓位
      if(rezult!=-class="num">1)Print("#",magic," 操作结果代码 ",rezult," 交易量 ",res.volume);
      else{Print("#",magic,"错误",GetLastError()); class="kw">return;}
      }
   }
   else class=class="str">"cmt">// 如果没有仓位则开仓
   {
     Print("#",magic,"标识号对应的仓位持仓量 ",PositionGetDouble(POSITION_VOLUME),
         " 开仓类型 ",type_MA);
     rezult=prov.SendOrder((ENUM_ORDER_TYPE)type_MA,lot);
     class=class="str">"cmt">// 开仓 
     if(rezult!=-class="num">1)Print("#",magic," 操作结果代码 ",rezult," 交易量 ",res.volume);
     else{Print("#",magic,"错误",GetLastError()); class="kw">return;}
   }
 }

◍ 用幻数切分虚拟仓位避开EA互踩

MT5 的内置仓位函数(PositionGetDouble / PositionGetInteger / PositionGetString)只按品种返回实际持仓,不区分「是谁下的单」。但 ORDER_MAGIC、DEAL_MAGIC、POSITION_MAGIC 是同一个用户数字,建仓时 POSITION_MAGIC 取自 DEAL_MAGIC,而 DEAL_MAGIC 又来自委托的 ORDER_MAGIC——也就是说,系统能识别某笔成交属于哪个幻数,却没法直接用一个幻数拉出对应仓位。 自己包一层类就能补这个洞。下面这段是 MT5 原生仓位读取接口与虚拟仓位结构的对照骨架,先看清原生能力边界: // 返回开仓数量. int PositionsTotal(); // 返回开仓列表中相应号码位置的交易品种. string PositionGetSymbol(int index); // 选择开仓用于将来工作. bool PositionSelect(string symbol, uint timeout=0); // 函数返回开仓的请求属性. double PositionGetDouble(ENUM_POSITION_PROPERTY property_id); // 函数返回开仓的请求属性. long PositionGetInteger(ENUM_POSITION_PROPERTY property_id); // 函数返回开仓的请求属性. string PositionGetString(ENUM_POSITION_PROPERTY property_id); struct SPositionVirtualMagic { double volume; ENUM_POSITION_TYPE type; }; class CPositionVirtualMagic { protected: SPositionVirtualMagic pvm; public: … }; 逻辑上把买、卖成交按幻数分别累加再相减:0.2 手卖加 0.3 手买,净仓就是 0.1 手买。类里用 while 循环后置判断加载历史,HistoryDealsTotal() 前后各取一次,若数值变了就重跑,避免别的 EA 把历史深度改掉导致漏单。 实测在同一品种上跑三个 EA,各给不同时间框架与幻数(例如 1001 / 1002 / 1003),彼此不抢仓、不冲突。外汇与贵金属杠杆高,这种隔离只解决逻辑互踩,不等于降低爆仓风险,参数请先在策略测试器验证。

MQL5 / C++
class=class="str">"cmt">// 返回开仓数量.
class="type">int      PositionsTotal();
class=class="str">"cmt">// 返回开仓列表中相应号码位置的交易品种.
class="type">class="kw">string   PositionGetSymbol(class="type">int index);
class=class="str">"cmt">// 选择开仓用于将来工作.
class="type">bool      PositionSelect(class="type">class="kw">string symbol, class="type">uint timeout=class="num">0);
class=class="str">"cmt">// 函数返回开仓的请求属性.
class="type">class="kw">double    PositionGetDouble(ENUM_POSITION_PROPERTY property_id);

class=class="str">"cmt">// 函数返回开仓的请求属性.
class="type">long      PositionGetInteger(ENUM_POSITION_PROPERTY property_id);

class=class="str">"cmt">// 函数返回开仓的请求属性.
class="type">class="kw">string    PositionGetString(ENUM_POSITION_PROPERTY property_id);

class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| CPositionVirtualMagic 类结构                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------
class="kw">struct SPositionVirtualMagic
  {
   class="type">class="kw">double               volume; class=class="str">"cmt">// 虚拟仓位持仓量
   class="type">ENUM_POSITION_TYPE   type;   class=class="str">"cmt">// 虚拟仓位类型
   };
class=class="str">"cmt">//+--------------------------------------------------------------------------------+
class=class="str">"cmt">//| 本类依 EA 的魔幻号计算虚拟仓位                                                     |
class=class="str">"cmt">//+--------------------------------------------------------------------------------+
class CPositionVirtualMagic
  {
class="kw">protected:
   SPositionVirtualMagic pvm;
class="kw">public:

常见问题

给每个EA设不同的幻数,平仓只遍历自己幻数的持仓,就能隔离互踩。
幻数空间极宽,不同EA用不重复的值即可,不必担心浪费,重点是别撞号。
小布可扫描持仓与挂单的幻数分布,标出重复或冲突的EA,提醒你隔离。
用不同幻数切分虚拟仓位,首仓与反手分支各自带标识,便于分别管理。
调用方向判定函数区分 buy/sell 挂单,再按EA分配幻数写入订单参数。