开发一个跨平台的EA交易来根据风险设置止损和获利·进阶篇
📘

开发一个跨平台的EA交易来根据风险设置止损和获利·进阶篇

第 2/3 篇

◍ MT5 与 MT4 下单分支的兼容写法

这段逻辑同时照顾了 MT5 新交易类和 MT4 旧式 OrderSend 两套接口,用预编译指令 #else 切分。MT5 侧在 IOC 成交模式下把 type_filling 设成 ORDER_FILLING_IOC,并固定 deviation=100(即允许 100 点偏差),随后用 OrderSend(mrequest,mresult) 发单。 发单成功后判断 retcode:10009 代表成交完成,10008 代表已受理排队,两者都视为成功路径;若 action 不是改挂止损止盈(TRADE_ACTION_SLTP),就按 ORDER_TYPE_BUY / ORDER_TYPE_SELL 分流,原代码里 Alert 调试行被注释掉了,实盘可取消注释看单号与价位。 MT4 分支则先把自定义枚举 mytype 映射到 OP_BUY / OP_BUYSTOP / OP_BUYLIMIT / OP_SELL 等旧类型,再调用四参数 OrderSend(sym,type,volume,price,100,sl,tp,comment,EA_Magic,expiration)。注意这里偏差硬编码为 100,若做黄金 XAUUSD 这类点值小的品种,100 点偏差可能过大,建议按品种单独调。 外汇与贵金属杠杆高、滑点风险大,deviation 设宽虽能提升成交率,但也可能在极端行情以偏离预期较多的价格成交,回测和实盘前务必在策略测试器里跑一遍验证。

MQL5 / C++
      case IOC:
            mrequest.type_filling = ORDER_FILLING_IOC;
            class="kw">break;
      }
      mrequest.deviation=class="num">100;
   }
   if(OrderSend(mrequest,mresult)){
      if(mresult.retcode==class="num">10009 || mresult.retcode==class="num">10008){
         if(action!=TRADE_ACTION_SLTP){
            class="kw">switch(type){
               case ORDER_TYPE_BUY:
class=class="str">"cmt">//                  Alert("Order Buy #:",mresult.order," sl",sl," tp",tp," p",price," !!");
                  class="kw">break;
               case ORDER_TYPE_SELL:
class=class="str">"cmt">//                  Alert("Order Sell #:",mresult.order," sl",sl," tp",tp," p",price," !!");
                  class="kw">break;
            }
         }else{
class=class="str">"cmt">//                  Alert("Order Modify SL #:",mresult.order," sl",sl," tp",tp," !!");
         }
         class="kw">return true;
      }else{
         msgErr(GetLastError(), mresult.retcode);
      }
   }
   class="macro">#else
      class="type">int type=OP_BUY;
      class="kw">switch(mytype){
         case MY_BUY:
            type=OP_BUY;
            class="kw">break;
         case MY_BUYSTOP:
            type=OP_BUYSTOP;
            class="kw">break;
         case MY_BUYLIMIT:
            type=OP_BUYLIMIT;
            class="kw">break;
         case MY_SELL:
            type=OP_SELL;
            class="kw">break;
         case MY_SELLSTOP:
            type=OP_SELLSTOP;
            class="kw">break;
         case MY_SELLLIMIT:
            type=OP_SELLLIMIT;
            class="kw">break;
      }
      
      if(OrderSend(sym, type, volume, price, class="num">100, sl, tp, comment, EA_Magic, expiration)<class="num">0){
         msgErr(GetLastError());
      }else{
         class="kw">switch(type){
            case OP_BUY:

挂单触发时的弹窗回报写法

这段代码片段出现在订单命中逻辑里,作用是当 EA 捕捉到 BUY 或 SELL 挂单被触发时,向 MT5 终端弹窗抛出一组关键价位。 Alert("Order Buy sl",sl," tp",tp," p",price," !!"); 把止损 sl、止盈 tp 与成交价 price 拼成一条提示,OP_BUY 与 OP_SELL 两个 case 结构完全对称,只是前缀文字不同。 末尾 return true 表示本次事件已被处理,若走不到该分支(例如 #ifdef 画线开关未开)则落到文件尾 return false,EA 会继续轮询下一 tick。 外汇与贵金属杠杆品种跳空频繁,这类弹窗只能作辅助提醒,不能替代风控脚本;你在 MT5 里把这段塞进 OrderSend 回调,就能直观看到每笔挂单激活时的保护位。

MQL5 / C++
               Alert("Order Buy sl",sl," tp",tp," p",price," !!");
               class="kw">break;
            case OP_SELL:
               Alert("Order Sell sl",sl," tp",tp," p",price," !!");
               class="kw">break;
            }
            class="kw">return true;
         }

   class="macro">#endif
   class="kw">return class="kw">false;
}

「EA界面元素怎么搭起来」

这套EA不在启动时留白屏,而是在 OnInit 阶段直接往图表上铺好交互控件:一条注释行(实时显示美元价差、点数、价格百分比和交易品种收盘时间)、一根红线(拖到哪止损就定在哪)、一个“Show (0) open price line”按钮、一个开仓按钮,以及后续自动冒出来的绿线(开仓价)和虚线(获利价)。红线默认选中,不用双击,直接拖就行——但拖完若没写 OnChartEvent 拦截,图表不会有任何反应。 图表上可能早就有上一轮EA残留的UI。若关闭时没清干净,或EA中途报错崩了,元素会留在图上。所以画线前必须用 ObjectFind 之类的检查,存在就复用全局变量里的价格和未结行数据,不重复创建。 所有拖拽和点击都走标准 OnChartEvent:拖线触发 CHARTEVENT_OBJECT_DRAG,点按钮触发 CHARTEVENT_OBJECT_CLICK。红线一动就调 setstopbyline 记止损位,同时自动显绿线并改按钮状态;绿线动了就挂限价单,不动就市价单。红线高于现价偏空、低于现价偏多,交易量按止损距离反推,使亏损接近输入参数设的值——外汇和贵金属杠杆高,实际滑点可能让亏损偏离预设。 时间框架一切换,EA默认重初始化,止损和开仓水平全丢。所以在 OnDeInit 里要读 reason,只有非 REASON_CHARTCHANGE 才删元素清注释,保住用户的手动设置。 下面这段是注释拼装函数,OnInit 和 OnTick 都会调,把点差算成金额和点数双格式:

MQL5 / C++
class=class="str">"cmt">/*
  在图表注释中显示点差和交易结束时间的数据
*/
class="type">void getmespread(){
   class="type">class="kw">string msg="";
   
   class=class="str">"cmt">// 取得交易品种的点差
   curSpread=lastme.ask-lastme.bid;
   
   class=class="str">"cmt">// 如果市场没有关闭,显示点差信息
   if( !isClosed ){
       if(curSpread>class="num">0){
           StringAdd(msg, langs.Label1_spread+": "+(class="type">class="kw">string) DoubleToString(curSpread, (class="type">int) SymbolInfoInteger(_Symbol, SYMBOL_DIGITS))+" "+currencyS+" ("+DoubleToString(curSpread/curPoint, class="num">0)+langs.lbl_point+")");
           StringAdd(msg, "; "+DoubleToString(((curSpread)/lastme.bid)*class="num">100, class="num">3)+"%");
       }else{
           StringAdd(msg, langs.Label1_spread+": "+langs.lblNo);
       }
       StringAdd(msg, "; ");
   }
   
   class=class="str">"cmt">// 如果我们可以确定的话,显示市场收盘时间
   if(StringLen(time_info)){
       StringAdd(msg, "  "+time_info);
   }
       
   Comment(msg);
}
SymbolInfoTick(_Symbol,lastme);
  isClosed=class="kw">false;
  class=class="str">"cmt">// 取得当前日期
  TimeToStruct(TimeCurrent(), curDay);
  class=class="str">"cmt">// 取得今日的交易品种交易时间
  if(SymbolInfoSessionTrade(_Symbol, (ENUM_DAY_OF_WEEK) curDay.day_of_week, class="num">0, dfrom, dto)){
      time_info="";
      TimeToStruct(dto, curEndTime);
      TimeToStruct(dfrom, curStartTime);
      
       isEndTime=true;

◍ K线收线时刻的字符串拼装与状态判定

这段逻辑干的事很具体:把一根K线预定的结束时间(curEndTime)格式化成 HH:MM 的可见字符串,并顺手判断当前时刻是否已经越过收线点。小时或分钟小于 10 时手动补前导零,保证 MT5 面板显示对齐,不会出现 9:5 这种歪斜写法。 判定收没收盘靠的是 curDay 与 curEndTime 的小时、分钟比较。若同小时且当前分钟已大于等于结束分钟,isClosed 置真;跨小时的情形里,结束小时为 0 或当前小时已跑过结束小时,都会标记收盘并往 time_info 追加本地化的「收盘」标签。 还有一层边界:当前小时早于起始小时(curStartTime.hour),或者同小时但分钟未到开盘分钟,不仅标收盘,还把 isEndTime 置假,意味着这根线还没进入可交易展示区间。最后按 isEndTime 分流,拼出 lblshow_TIME 或 lblshow_TIME2 两种前缀的提示串。 后面紧接着从品种属性抓结算货币与点数:currencyS 取 SYMBOL_CURRENCY_PROFIT,curPoint 取 SYMBOL_POINT。若图表上存在名为 exprefix+"_stop" 的物件(通常是止损线),就用 ObjectGetDouble 读出它的价格写进 draw_stop,供后续全局变量复用。外汇与贵金属波动受杠杆与跳空影响,这类时间判定在重大数据夜可能因流动性断裂而偏离预期,验证时建议先开 EURUSD 的 M5 手动拖一根止损线看输出。

MQL5 / C++
class="type">class="kw">string tmpmsg="";
tmp_val=curEndTime.hour;
if(tmp_val<class="num">10){
   StringAdd(tmpmsg, "class="num">0");
}
StringAdd(tmpmsg, (class="type">class="kw">string) tmp_val+":");
tmp_val=curEndTime.min;
if(tmp_val<class="num">10){
   StringAdd(tmpmsg, "class="num">0");
}
StringAdd(tmpmsg, (class="type">class="kw">string) tmp_val);
if(curEndTime.hour==curDay.hour){
   if(tmp_val>curDay.min){
   }else{
      isClosed=true;
   }
}else{
   if(curEndTime.hour==class="num">0){
   }else{
      if( curEndTime.hour>class="num">1 && (curDay.hour>curEndTime.hour || curDay.hour==class="num">0)){
         StringAdd(time_info, " ("+langs.lbl_close+")");
         isClosed=true;
      }else if(curDay.hour<curStartTime.hour ){
         StringAdd(time_info, " ("+langs.lbl_close+")");
         isEndTime=class="kw">false;
         isClosed=true;
      }else if(curDay.hour==curStartTime.hour && curDay.min<curStartTime.min ){
         StringAdd(time_info, " ("+langs.lbl_close+")");
         isEndTime=class="kw">false;
         isClosed=true;
      }
   }
}
if(isEndTime){
   StringAdd(time_info, langs.lblshow_TIME+": "+tmpmsg+time_info);
}else{
   StringAdd(time_info, langs.lblshow_TIME2+": "+tmpmsg+time_info);
}
}
currencyS=SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT);
curPoint=SymbolInfoDouble(_Symbol, SYMBOL_POINT);
class=class="str">"cmt">// 如果图表上有止损和开仓价格线
class=class="str">"cmt">// 把价格加到全局变量中
if(ObjectFind(class="num">0, exprefix+"_stop")>=class="num">0){
   draw_stop=ObjectGetDouble(class="num">0, exprefix+"_stop", OBJPROP_PRICE);

拖线改止损的挂单界面逻辑

EA 在图表上先找有没有已存在的开仓线对象,用 ObjectFind 查 exprefix+"_open",找到就直接读它的价格赋给 draw_open;找不到才走 else 分支,以最新买价 lastme.bid 为基准画一条止损水平线。 止损线不是随便画的:draw_stop 设在 draw_open 减去点差折算的 curPoint 之下,创建后用 OBJ_HLINE 挂到图表,并打开可选、选中、点线点样式(STYLE_DASHDOTDOT)和顶部锚定。这样交易者能用鼠标直接拖这条线,拖完就改止损预期。 OnChartEvent 里只截获 CHARTEVENT_OBJECT_DRAG,且 sparam 必须是那条 _stop 线,才调用 setstopbyline 和 showOpenLine,顺手把 _openbtn 状态置真。也就是说,只有拖止损线这一种交互会触发重算,别的图表事件全忽略。 setstopbyline 先取拖后线的价格 curprice,若大于 0 且不等于原 draw_stop,就按 SYMBOL_TRADE_TICK_SIZE 做对齐:tick 小于 1 时把价格除以 tick 转成字符串,若不含 ".00000000" 才用 MathFloor 把价格吸附到合法 tick 倍数。外汇和贵金属点差跳动快,这种对齐能避免下单价落在平台不接受的非整数 tick 上,但拖动改损仍属手动干预,实盘高风险,成交价可能滑向不利方向。

MQL5 / C++
if(ObjectFind(class="num">0, exprefix+"_open")>=class="num">0){
     draw_open=ObjectGetDouble(class="num">0, exprefix+"_open", OBJPROP_PRICE);
}
class=class="str">"cmt">// 否则创建整个 EA 交易的 UI
}else{
    draw_open=lastme.bid;
    draw_stop=draw_open-(SymbolInfoInteger(_Symbol, SYMBOL_SPREAD)*curPoint);
    ObjectCreate(class="num">0, exprefix+"_stop", OBJ_HLINE, class="num">0, class="num">0, draw_stop);
    ObjectSetInteger(class="num">0,exprefix+"_stop",OBJPROP_SELECTABLE,class="num">1);
    ObjectSetInteger(class="num">0,exprefix+"_stop",OBJPROP_SELECTED,class="num">1);
    ObjectSetInteger(class="num">0,exprefix+"_stop",OBJPROP_STYLE,STYLE_DASHDOTDOT);
    ObjectSetInteger(class="num">0,exprefix+"_stop",OBJPROP_ANCHOR,ANCHOR_TOP);
    class=class="str">"cmt">// 其它界面元素
}
class="type">void OnChartEvent(class="kw">const class="type">int id,      class=class="str">"cmt">// 事件 ID   
                  class="kw">const class="type">long& lparam,  class=class="str">"cmt">// 长整型的事件参数 
                  class="kw">const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度浮点型的事件参数 
                  class="kw">const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数 
  {
  class="kw">switch(id){
    case CHARTEVENT_OBJECT_DRAG:
      if(sparam==exprefix+"_stop"){
        setstopbyline();
        showOpenLine();
        ObjectSetInteger(class="num">0,exprefix+"_openbtn",OBJPROP_STATE, true);
      }
      class="kw">break;
  }
}
class=class="str">"cmt">/*
“记住”未来订单的止损等级
*/
class="type">void setstopbyline(){
  class=class="str">"cmt">// 取得止损线所在的价格
  class="type">class="kw">double curprice=ObjectGetDouble(class="num">0, exprefix+"_stop", OBJPROP_PRICE);
  class=class="str">"cmt">// 如果价格与EA发布时止损线所在的价格不同,
  if( curprice>class="num">0 && curprice != draw_stop ){
    class="type">class="kw">double tmp_double=SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
    if( tmp_double>class="num">0 && tmp_double!=class="num">1 ){
      if(tmp_double<class="num">1){
        resval=DoubleToString(curprice/tmp_double, class="num">8);
        if( StringFind(resval, ".class="num">00000000")>class="num">0 ){}else{
          curprice=MathFloor(curprice)+MathFloor((curprice-MathFloor(curprice))/tmp_double)*tmp_double;
        }
      }else{

「开仓参考线与拖拽止损的联动实现」

在面板里点开『显示开仓线』按钮后,若图表上还没有那条水平线,脚本会用 OBJ_HLINE 在 lastme.bid 位置画一条绿色虚点线,并把可选择性打开,方便你直接拖。 按钮本体用 OBJ_BUTTON 创建,XDISTANCE 设为 0、YDISTANCE 设为 33,宽度 333 像素、高度 25、字号 8;这些数值决定了它在图表左上角的占位,改 YDISTANCE 就能避开其他悬浮控件。 当你拖动名为 _stop 的止损线时,CHARTEVENT_OBJECT_DRAG 分支会先跑 setstopbyline() 重算止损价,再调用 showOpenLine() 确保开仓线存在,并把 _openbtn 的状态置为 true,界面反馈立刻跟上。 外汇与贵金属杠杆高,这类辅助线只帮你定位,不替代风控,实际成交价可能因滑点偏离画线。

MQL5 / C++
if( MathMod(curprice,tmp_double) ){
     curprice= MathFloor(curprice/tmp_double)*tmp_double;
   }
 }
}
draw_stop=STOPLOSS_PRICE=curprice;

updatebuttontext();
ChartRedraw(class="num">0);
 }
}
 if(ObjectFind(class="num">0, exprefix+"_openbtn")<class="num">0){
    ObjectCreate(class="num">0, exprefix+"_openbtn", OBJ_BUTTON, class="num">0, class="num">0, class="num">0);
    ObjectSetInteger(class="num">0,exprefix+"_openbtn",OBJPROP_XDISTANCE,class="num">0);
    ObjectSetInteger(class="num">0,exprefix+"_openbtn",OBJPROP_YDISTANCE,class="num">33);
    ObjectSetString(class="num">0,exprefix+"_openbtn",OBJPROP_TEXT, langs.btnShowOpenLine);
    ObjectSetInteger(class="num">0,exprefix+"_openbtn",OBJPROP_XSIZE,class="num">333);
    ObjectSetInteger(class="num">0,exprefix+"_openbtn",OBJPROP_FONTSIZE, class="num">8);
    ObjectSetInteger(class="num">0,exprefix+"_openbtn",OBJPROP_YSIZE,class="num">25);
 }
 case CHARTEVENT_OBJECT_CLICK:
    if (sparam==exprefix+"_openbtn"){
       updateOpenLine();
    }
    class="kw">break;
class="type">void showOpenLine(){
   if(ObjectFind(class="num">0, exprefix+"_open")<class="num">0){
     draw_open=lastme.bid;
     ObjectCreate(class="num">0, exprefix+"_open", OBJ_HLINE, class="num">0, class="num">0, draw_open);
     ObjectSetInteger(class="num">0,exprefix+"_open",OBJPROP_SELECTABLE,class="num">1);
     ObjectSetInteger(class="num">0,exprefix+"_open",OBJPROP_SELECTED,class="num">1);
     ObjectSetInteger(class="num">0,exprefix+"_open",OBJPROP_STYLE,STYLE_DASHDOTDOT);
     ObjectSetInteger(class="num">0,exprefix+"_open",OBJPROP_ANCHOR,ANCHOR_TOP);
     ObjectSetInteger(class="num">0,exprefix+"_open",OBJPROP_COLOR,clrGreen);
   }
}
 case CHARTEVENT_OBJECT_DRAG:
    if(sparam==exprefix+"_stop"){
       setstopbyline();
       showOpenLine();
       ObjectSetInteger(class="num">0,exprefix+"_openbtn",OBJPROP_STATE, true);

常见问题

用预编译宏区分平台,MT5走CTrade类、MT4走OrderSend,把开仓逻辑包成统一函数按分支调用即可。
在订单交易事件里判断挂单状态变为已成交,调用Alert或MessageBox弹出包含品种和价格的回报文本。
可以,小布能实时读取持仓的止损位变化并推送异动,你拖完线不用自己回终端确认。
用相对坐标按组排,左侧放风险输入、右侧放开关,控件间距留15像素以上,避免重叠遮挡。
在OnTick里比对当前K线时间与前一根,时间不同就说明收线,用StringFormat拼周期名加'收盘'字样做判定。