开发一个跨平台的EA交易来根据风险设置止损和获利·进阶篇
◍ 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 设宽虽能提升成交率,但也可能在极端行情以偏离预期较多的价格成交,回测和实盘前务必在策略测试器里跑一遍验证。
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 回调,就能直观看到每笔挂单激活时的保护位。
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 都会调,把点差算成金额和点数双格式:
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 手动拖一根止损线看输出。
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 上,但拖动改损仍属手动干预,实盘高风险,成交价可能滑向不利方向。
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,界面反馈立刻跟上。 外汇与贵金属杠杆高,这类辅助线只帮你定位,不替代风控,实际成交价可能因滑点偏离画线。
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);