EA 遥控方法·进阶篇
(2/3)· 当终端不在手边,靠保存模板文件反向操控图表上的 EA 启停与平仓
接上篇,我们继续深挖远程控制 EA 的工程实现。MetaTrader 5 里 EA 各自跑在独立线程,无法直接互发指令,多数人卡在“想遥控却找不到入口”这一步。本文用 fxsaber 的模板读写思路,把图表模板变成一条可编辑的命令通道。
◍ 用零价挂单当命令通道
做跨 EA 通信时,直接发市价单会搅乱结算,稳妥做法是让主控 EA 丢出挂单、被控 EA 识别后立刻删掉,既不成交也不占逻辑。关键是挂单价格必须离现价足够远,避免误触发。 移动终端没法给已开订单写注释,但桌面端能看。于是主控可以把 EA 名称和交易许可写进订单注释,实现单向播报;用户想回传指令则走不通。 既然注释不能双向用,就只能拿价格和品种编码命令。把挂单价格压到 1–5 个最小逐笔报价(接近零),理论上任何品种触到这价的概率都可忽略,卖单止损(sell stop)方向还不影响保证金占用。 实测可用的命令映射:价格 1=请求状态(挂单品种对应 EA,注释写名称与许可);价格 2=停止交易(无注释则全停,改单后跟第一条则停指定品种);价格 3=启动(同 2 逻辑);价格 4=删挂单(交易量 1 买/2 卖/3 全部);价格 5=平仓(手数规则同 4)。外汇与贵金属杠杆高,误设价格层级可能让脚本自锁,上手前先在 MT5 策略测试器跑一遍。
主控 EA 的骨架与命令路由
搭主控 EA 时直接复用 fxsaber 的「智能系统」函数库,所有方法都是公有,方便在 EA 里直接调。为了不在 EA 侧重复宏声明,把函数库末尾的 #undef 指令注释掉,避免同名宏被释放后重定义出错——但注意这些宏仍不能在函数库之外单独使用。 EA 只暴露两个外部参数:信令订单生命周期 Expirations(默认 5 分钟)和魔幻数字 Magic(默认 88888)。全局里声明 CTrade 实例、工作图表 ID、最后一笔命令单号,OnInit 里 new 出来并设偏差 0 点、绑定魔幻数字,OnDeinit 必须 delete 实例。 命令入口放在 OnTradeTransaction。一笔订单处理可能触发多次回调,所以进代码前先筛:订单单号有效、状态不是删单、魔幻数字匹配、类型仅为 sell stop、交易操作类型是添加或修改。全过才进 CheckCommand。 CheckCommand 先比对是否处理过该请求单号,命中就退;否则把挂单价格解码成命令,用 switch 分发到对应函数,结束存最后单号并删原订单。fxsaber 库里把图表模板转字符串、按标签取子串的方法在这里是关键依赖。 GetExpertsInfo 负责扫所有活动图表:跳过大控 EA 自身图表,查模板和 EA 区块,恢复 EA 名和状态写进订单注释——允许交易前缀 T,否则 S。ChangeExpertsMode 按图表迭代改 expertmode 并重载模板,删隐藏对象后应用。DeleteOrders 则按命令表解码交易量,循环删匹配订单。外汇与贵金属品种波动剧烈,这类自动管控 EA 实盘前务必在 MT5 策略测试器走一遍。
class="macro">#define FILENAME(__FILE__ + ".tpl") class="macro">#define PATH "\\Files\\" class="macro">#define STRING_END "\r\n" class="macro">#define EXPERT_BEGIN("<expert>" + STRING_END) class="macro">#define EXPERT_END("</expert>" + STRING_END) class="macro">#define EXPERT_INPUT_BEGIN("<inputs>" + STRING_END) class="macro">#define EXPERT_INPUT_END("</inputs>" + STRING_END) class="macro">#define EXPERT_CHART_BEGIN("<chart>" + STRING_END) class="macro">#define EXPERT_NAME "name=" class="macro">#define EXPERT_PATH "path=" class="macro">#define EXPERT_STOPLEVEL "stops_color=" class="macro">#define OBJECT_BEGIN("<object>" + STRING_END) class="macro">#define OBJECT_END("</object>" + STRING_END) class="macro">#define OBJECTS_NUMBER("objects=") class="macro">#define OBJECT_HIDDEN("hidden=class="num">1") class="macro">#define EXPERT_EXPERTMODE("expertmode=") class=class="str">"cmt">//class="macro">#undef EXPERT_STOPLEVEL class=class="str">"cmt">//class="macro">#undef EXPERT_PATH class=class="str">"cmt">//class="macro">#undef EXPERT_NAME class=class="str">"cmt">//class="macro">#undef EXPERT_CHART_BEGIN class=class="str">"cmt">//class="macro">#undef EXPERT_INPUT_END class=class="str">"cmt">//class="macro">#undef EXPERT_INPUT_BEGIN class=class="str">"cmt">//class="macro">#undef EXPERT_END class=class="str">"cmt">//class="macro">#undef EXPERT_BEGIN class=class="str">"cmt">//class="macro">#undef STRING_END class=class="str">"cmt">//class="macro">#undef PATH class=class="str">"cmt">//class="macro">#undef FILENAME sinput class="type">int Expirations = class="num">5; sinput class="type">class="kw">ulong Magic = class="num">88888; class="macro">#include <fxsaber\Expert.mqh> class="macro">#include <Trade\Trade.mqh> CTrade *Trade; class="type">class="kw">ulong chart; class="type">class="kw">ulong last_command; class="type">int OnInit() { class=class="str">"cmt">//--- Trade = new CTrade(); if(CheckPointer(Trade)==POINTER_INVALID) class="kw">return INIT_FAILED; Trade.SetDeviationInPoints(class="num">0); Trade.SetExpertMagicNumber(Magic); Trade.SetMarginMode();
「用挂单价格尾数遥控 EA 行为」
这段逻辑把「卖单止损挂单(Sell Stop)的挂单价格」当成指令通道:价格相对最小变动单位 tick_size 取整后,尾数 1~5 分别对应唤起 EA 信息、切换 EA 开关、删除订单、平掉持仓等动作。\n核心在 OnTradeTransaction 里只拦截 magic==0 且类型为 ORDER_TYPE_SELL_STOP 的增改事件,再丢给 CheckCommand 处理,避免和正常策略订单打架。\nCheckCommand 先用 last_command==trans.order 去重,防止同一笔订单反复触发;随后用 NormalizeDouble(trans.price/tick,0) 把价格换算成无小数的指令码。\n值得注意:命中指令后无论 case 1~5 都会执行 Trade.OrderDelete(last_command),也就是说这类「遥控挂单」本身是一次性的,触发完即从终端消失,不会真去参与成交。\n外汇与贵金属市场杠杆高、滑点随机,这种用挂单尾数发指令的做法只适合在模拟盘或极低风险仓位下验证,实盘误触可能造成意外平仓。
Trade.SetTypeFillingBySymbol(_Symbol); class=class="str">"cmt">//--- chart=ChartID(); last_command=class="num">0; class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- if(CheckPointer(Trade)!=POINTER_INVALID) class="kw">delete Trade; } class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction &trans, class="kw">const class="type">MqlTradeRequest &request, class="kw">const class="type">MqlTradeResult &result) { class=class="str">"cmt">//--- if(trans.order>class="num">0 && trans.order_state!=ORDER_STATE_REQUEST_CANCEL && request.magic==class="num">0 && trans.order_type==ORDER_TYPE_SELL_STOP && (trans.type==TRADE_TRANSACTION_ORDER_ADD || trans.type==TRADE_TRANSACTION_ORDER_UPDATE)) CheckCommand(trans,request); } class="type">void CheckCommand(class="kw">const MqlTradeTransaction &trans, class="kw">const class="type">MqlTradeRequest &request) { if(last_command==trans.order) class="kw">return; class="type">class="kw">double tick=SymbolInfoDouble(trans.symbol,SYMBOL_TRADE_TICK_SIZE); class="type">uint command=(class="type">uint)NormalizeDouble(trans.price/tick,class="num">0); class="kw">switch(command) { case class="num">1: if(StringLen(request.comment)>class="num">0 || trans.type==TRADE_TRANSACTION_ORDER_UPDATE) class="kw">return; if(trans.order<=class="num">0 || (OrderSelect(trans.order) && StringLen(OrderGetString(ORDER_COMMENT))>class="num">0)) class="kw">return; GetExpertsInfo(); class="kw">break; case class="num">2: ChangeExpertsMode(trans,request,class="kw">false); class="kw">break; case class="num">3: ChangeExpertsMode(trans,request,true); class="kw">break; case class="num">4: DeleteOrders(trans); class="kw">break; case class="num">5: ClosePosition(trans); class="kw">break; class="kw">default: class="kw">return; } last_command=trans.order; Trade.OrderDelete(last_command); } class="type">void GetExpertsInfo(class="type">void) { DeleteOrdersByMagic(Magic); class="type">long i_chart=ChartFirst(); class="kw">while(i_chart>=class="num">0 && !IsStopped()) {
◍ 用模板字符串批量挂 SellStop 的实战循环
下面这段逻辑干的事很直接:遍历当前终端里所有图表,跳过当前图表和 0 号图表,把每个图表的模板转成字符串,再从中抠出专家标记和名称,最后按最小手数挂一张 SellStop。外汇与贵金属波动剧烈,这类批量挂单在高波动时段可能瞬间触发,属高风险操作,上之前先在策略测试器跑一遍。 代码里先用 ChartNext 顺着图表 ID 往后找;若图表不是当前图、且模板转字符串失败(temp==NULL),或抠不出 EXPERT_BEGIN 到 EXPERT_END 之间的内容,就 continue 跳下一个。能走到挂单那行的,都是带有效专家标记的图表。 Expirations 参数控制挂单有效期:若大于 0,过期时间 = TimeCurrent() + Expirations*60 秒,订单类型设为 ORDER_TIME_SPECIFIED;否则用 ORDER_TIME_GTC 永久有效。成交注释用 state 区分——true 加“T ”前缀,false 加“S ”,后面拼专家名,方便在终端订单里一眼认出来源。 TemplateToString 先 SaveTemplate 到文件再 FileLoad 进 short 数组转 string,CheckExpert 为 true 时会先 Is() 校验图表是否带专家;StringBetween 则是按起止串切子串并原地缩减原串,方便连续解析。这两个静态函数配合循环,能把多图表状态压缩成可挂单的参数流。
if(i_chart==class="num">0 || i_chart==chart) { i_chart=ChartNext(i_chart); class="kw">continue; } class="type">class="kw">string temp=EXPERT::TemplateToString(i_chart,true); if(temp==NULL) { i_chart=ChartNext(i_chart); class="kw">continue; } temp=EXPERT::StringBetween(temp,EXPERT_BEGIN,EXPERT_END); if(temp==NULL) { i_chart=ChartNext(i_chart); class="kw">continue; } class="type">class="kw">string name = EXPERT::StringBetween(temp,EXPERT_NAME,STRING_END); class="type">bool state = (class="type">bool)StringToInteger(EXPERT::StringBetween(temp,EXPERT_EXPERTMODE,STRING_END)); class="type">class="kw">string comment = (state ? "T " : "S ")+name; class="type">class="kw">string symbol=ChartSymbol(i_chart); ENUM_ORDER_TYPE_TIME type=ORDER_TIME_GTC; class="type">class="kw">datetime expir=class="num">0; if(Expirations>class="num">0) { expir=TimeCurrent()+Expirations*class="num">60; type=ORDER_TIME_SPECIFIED; } Trade.SellStop(SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN),SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_SIZE),symbol,class="num">0,class="num">0,type,expir,comment); i_chart=ChartNext(i_chart); } } class="kw">static class="type">class="kw">string TemplateToString( class="kw">const class="type">long Chart_ID = class="num">0, class="kw">const class="type">bool CheckExpert = class="kw">false ) { class="type">class="kw">short Data[]; class="kw">return(((!CheckExpert || EXPERT::Is(Chart_ID)) && ::ChartSaveTemplate((class="type">class="kw">ulong)Chart_ID, PATH + FILENAME) && (::FileLoad(FILENAME, Data) > class="num">0)) ? ::ShortArrayToString(Data) : NULL); } class="kw">static class="type">class="kw">string StringBetween( class="type">class="kw">string &Str, class="kw">const class="type">class="kw">string StrBegin, class="kw">const class="type">class="kw">string StrEnd = NULL ) { class="type">int PosBegin = ::StringFind(Str, StrBegin); PosBegin = (PosBegin >= class="num">0) ? PosBegin + ::StringLen(StrBegin) : class="num">0; class="kw">const class="type">int PosEnd = ::StringFind(Str, StrEnd, PosBegin); class="kw">const class="type">class="kw">string Res = ::StringSubstr(Str, PosBegin, (PosEnd >= class="num">0) ? PosEnd - PosBegin : -class="num">1); Str = (PosEnd >= class="num">0) ? ::StringSubstr(Str, PosEnd + ::StringLen(StrEnd)) : NULL; if (Str == "") Str = NULL; class="kw">return(Res); } class="type">void ChangeExpertsMode(class="kw">const MqlTradeTransaction &trans,
用模板字符串同步图表上的专家开关
这段逻辑干的事很直接:交易事务触发后,遍历终端里所有图表,把符合特征的图表模板重新套用一遍,顺带把 ExpertMode 状态写进模板。它先取订单注释,若为空则从 OrderSelect 回查 ORDER_COMMENT,再截出第 2 字符后的专家名 exp_name,用来匹配图表。 遍历从 ChartFirst 开始,遇到主图表(0)、当前图表、或图表品种与成交品种不符的情况直接跳过。TemplateToString 读不到模板就 continue,保证只处理有效图表。 匹配 exp_name 时,用 StringBetween2 和 StringBetween 抠出原模板里专家名段,名字对不上就跳过;对得上的才拼 NewTemplate。随后检查 EXPERT_EXPERTMODE 段里的 state,若已等于目标 ExpertMode 也跳过,避免重复刷新。 最后把 ExpertMode 转整型拼进模板,DeleteHiddenObjects 清掉隐藏对象,TemplateApply 强制重画。外汇与贵金属市场杠杆高、滑点随机,这类自动套模板在跳空时可能失效,建议在 MT5 策略测试器外挂脚本手点验证一次。 下面这段是原函数尾部的订单清理开头,可见它用 volume 除以 SYMBOL_VOLUME_MIN 并 NormalizeDouble 到整数手数倍,再倒序扫 OrderGetTicket,ticket 无效就跳过。
class="kw">const class="type">MqlTradeRequest &request, class="type">bool ExpertMode) { class="type">class="kw">string comment=request.comment; if(StringLen(comment)<=class="num">0 && OrderSelect(trans.order)) comment=OrderGetString(ORDER_COMMENT); class="type">class="kw">string exp_name=(StringLen(comment)>class="num">2 ? StringSubstr(comment,class="num">2) : NULL); class="type">long i_chart=ChartFirst(); class="kw">while(i_chart>=class="num">0 && !IsStopped()) { if(i_chart==class="num">0 || i_chart==chart || (StringLen(exp_name)>class="num">0 && ChartSymbol()!=trans.symbol)) { i_chart=ChartNext(i_chart); class="kw">continue; } class="type">class="kw">string temp=EXPERT::TemplateToString(i_chart,true); if(temp==NULL) { i_chart=ChartNext(i_chart); class="kw">continue; } class="type">class="kw">string NewTemplate = NULL; if(exp_name!=NULL) { NewTemplate=EXPERT::StringBetween2(temp,NULL,EXPERT_NAME); class="type">class="kw">string name=EXPERT::StringBetween(temp,NULL,STRING_END); if(name!=exp_name) { i_chart=ChartNext(i_chart); class="kw">continue; } NewTemplate+=name+STRING_END; } class=class="str">"cmt">//--- NewTemplate+=EXPERT::StringBetween2(temp,NULL,EXPERT_EXPERTMODE); class="type">bool state = (class="type">bool)StringToInteger(EXPERT::StringBetween(temp,NULL,STRING_END)); if(state==ExpertMode) { i_chart=ChartNext(i_chart); class="kw">continue; } NewTemplate+=IntegerToString(ExpertMode)+STRING_END+temp; NewTemplate=DeleteHiddenObjects(NewTemplate); EXPERT::TemplateApply(i_chart,NewTemplate,true); class=class="str">"cmt">//--- i_chart=ChartNext(i_chart); } GetExpertsInfo(); } class="type">void DeleteOrders(class="kw">const MqlTradeTransaction &trans) { class="type">int direct=(class="type">int)NormalizeDouble(trans.volume/SymbolInfoDouble(trans.symbol,SYMBOL_VOLUME_MIN),class="num">0); for(class="type">int i=total-class="num">1;i>=class="num">0;i--) { class="type">class="kw">ulong ticket=OrderGetTicket((class="type">uint)i); if(ticket<=class="num">0) class="kw">continue;