TradeObjects: 基于 MetaTrader 图形对象的自动化交易·进阶篇
(2/3)· 多数图形对象 EA 要么功能阉割要么价格劝退,本文给出可改写的开源中庸方案
接上篇,我们继续深挖如何把图表上的趋势线、通道和斐波那契真正变成下单指令。很多交易者以为拖一根线就能让 EA 自动盯盘,结果要么对象属性读错,要么测试器里根本没法交互调试。MT5 可视测试器目前不支持交互式对象操作,这套逻辑只能在 MT4 里闭环验证。
用事件驱动接住图表上的手画线
想在 MT5 上自动识别交易者手动画的趋势线,第一步不是去扫价格,而是先把图表事件通道打开。下面这段代码在 OnInit 里做了三件事:允许图表对象被删除时触发事件、挂上 1 秒定时器、再调一次初始化。 [CODE]void OnInit(){ ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); EventSetTimer(1); to.handleInit(); }[/CODE] OnChartEvent 把图表交互(含画线、删线)转交给一个叫 to 的实例处理;OnTimer 和 OnTick 则分别按秒和按报价驱动状态刷新。这样手画一条线,最快 1 秒内就可能被脚本感知,不需要等下一根 K 线。 内部用 LineObject 结构记录每条线的名字和状态,attachObject 负责去重挂载:若数组中已存在同名线就把 status 置 1,否则数组扩容一位再写入并返回 true。detectLines 则遍历当前图表全部对象(ObjectsTotal 取数量),对合规对象调用 attachObject,新线会触发 describe 并打印 "New lines: N"。外汇与贵金属波动剧烈,手画线被秒删秒画是常态,这种事件+定时双触发能降低漏检概率。
class="type">void OnInit() { ChartSetInteger(class="num">0, CHART_EVENT_OBJECT_DELETE, true); EventSetTimer(class="num">1); to.handleInit(); } class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { to.handleChart(id, lparam, dparam, sparam); } class="type">void OnTimer() { to.handleTimer(); } class="type">void OnTick() { to.handleTick(); } class="kw">private: class="kw">struct LineObject { class="type">class="kw">string name; class="type">int status; class="type">void class="kw">operator=(class="kw">const LineObject &o) { name = o.name; status = o.status; } }; LineObject objects[]; class="kw">protected: class="type">bool attachObject(class="kw">const class="type">class="kw">string name) { class="type">bool found = false; class="type">int n = ArraySize(objects); for(class="type">int i = class="num">0; i < n; i++) { if(objects[i].name == name) { objects[i].status = class="num">1; found = true; class="kw">break; } } if(!found) { ArrayResize(objects, n + class="num">1); objects[n].name = name; objects[n].status = class="num">1; class="kw">return true; } class="kw">return false; } class="type">bool detectLines() { startRefresh(); class="type">int n = ObjectsTotal(ChartID(), class="num">0); class="type">int count = class="num">0; for(class="type">int i = class="num">0; i < n; i++) { class="type">class="kw">string obj = ObjectName(ChartID(), i, class="num">0); if(checkObjectCompliance(obj)) { if(attachObject(obj)) { describe(obj); count++; } } } if(count > class="num">0) Print("New lines: ", count); class="type">bool changes = stopRefresh() || (count > class="num">0); if(changes) { display(); } }
「用对象属性过滤出可触发线」
想在 MT5 图表上自动识别「自己画的线」并触发提醒,第一步是给对象做合规性过滤。下面这段逻辑用前缀、类型、线型、颜色四个维度卡掉无关图形,只留你计划监控的那批。 checkObjectCompliance 先判断对象名是否带通用前缀(CommonPrefit 为空则不限),再查它的 OBJPROP_TYPE 是否落在 _ln 集合里——这里只收 OBJ_HLINE、OBJ_VLINE、OBJ_TREND、OBJ_CHANNEL、OBJ_FIBO 五种。线型必须匹配 _st 里的 InstantType、PendingType、CloseStopLossTakeProfitType,颜色必须命中 _cc 里的 BuyColor、SellColor、ActivationColor,三者同时为真才返回 true。 processLines 里遍历 objects 数组,用 ObjectGetInteger(ChartID(), name, OBJPROP_BACK) 跳过背景层对象;随后取出 style、clr、text、last 等属性。当 clr == ActivationColor 且 style == InstantType 时,若 checkActivation(price) 通过,就 disableLine(i) 并依 text 前缀发 Alert / Push / Mail——text 以 "Alert:" 开头截第 6 字符往后播报,"Push:" 与 "Mail:" 截第 5 字符往后推送。外汇与贵金属波动剧烈,这类自动提醒只降低漏看概率,不保证信号有效,实盘前请在模拟盘验证触发逻辑。 把 _ln / _st / _cc 三个 Set 的参数改成你模板里的实际枚举值,编译后拖到图表,画一条 ActivationColor 的瞬时线就能测通通知链路。
class="type">bool checkObjectCompliance(class="kw">const class="type">class="kw">string obj) { if(CommonPrefit == "" || StringFind(obj, CommonPrefit) == class="num">0) { if(_ln[ObjectGetInteger(class="num">0, obj, OBJPROP_TYPE)] && _st[ObjectGetInteger(class="num">0, obj, OBJPROP_STYLE)] && _cc[(class="type">color)ObjectGetInteger(class="num">0, obj, OBJPROP_COLOR)]) { class="kw">return true; } } class="kw">return false; } class="macro">#include <Set.mqh> Set<ENUM_OBJECT> _ln(OBJ_HLINE, OBJ_VLINE, OBJ_TREND, OBJ_CHANNEL, OBJ_FIBO); Set<ENUM_LINE_STYLE> _st(InstantType, PendingType, CloseStopLossTakeProfitType); Set<class="type">color> _cc(BuyColor, SellColor, ActivationColor); class="type">void processLines() { class="type">int n = ArraySize(objects); for(class="type">int i = class="num">0; i < n; i++) { class="type">class="kw">string name = objects[i].name; if(ObjectGetInteger(ChartID(), name, OBJPROP_BACK)) class="kw">continue; class="type">int style = (class="type">int)ObjectGetInteger(class="num">0, name, OBJPROP_STYLE); class="type">color clr = (class="type">color)ObjectGetInteger(class="num">0, name, OBJPROP_COLOR); class="type">class="kw">string text = ObjectGetString(class="num">0, name, OBJPROP_TEXT); class="type">class="kw">datetime last = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0, name, OBJPROP_ZORDER); class="type">class="kw">double aux = class="num">0, auxf = class="num">0; class="type">class="kw">double price = getCurrentPrice(name, aux, auxf); ... if(clr == ActivationColor) { if(style == InstantType) { if(checkActivation(price)) { disableLine(i); if(StringFind(text, "Alert:") == class="num">0) Alert(StringSubstr(text, class="num">6)); else if(StringFind(text, "Push:") == class="num">0) SendNotification(StringSubstr(text, class="num">5)); else if(StringFind(text, "Mail:") == class="num">0) SendMail("TradeObjects", StringSubstr(text, class="num">5));
◍ 从图表对象解析买单的止损止盈
这段代码承接了上一节对图形标签的识别逻辑,专门处理当对象颜色被判定为 BuyColor 且样式为 InstantType 时的市价买单生成路径。它先调用 checkMarket 比较当前价与上一tick价格得出方向,若方向为0且 checkTime 通过,则按颜色强制赋予 +1(买)或 -1(卖)方向。 手数取自图表对象文本,用 StringToDouble 解析 OBJPROP_TEXT,若解析为0则回退到默认 Lot 变量;这一设计让你可以在对象上直接写「2」代表2%风险手数,不写就跑默认仓。 aux 参数决定 sl/tp 的计算方式:aux 大于 Ask 时视为止盈价,否则视为止损价;若 aux 为0,则统一用 DefaultStopLoss / DefaultTakeProfit 经 getPointsForLotAndRisk 换算点数再乘 _Point。所有 sl、tp 最后经 NormalizeDouble 按 _Digits 对齐报价精度,避免 MT5 拒单。 外汇与贵金属杠杆高,这类由图表对象触发的自动下单若 aux 写错方向,可能把止损当止盈挂,实盘前务必在策略测试器用历史数据跑一遍 checkMarket 与 aux 分支。
else Print(text); } } else if(clr == BuyColor) { if(style == InstantType) { class="type">int dir = checkMarket(price, last); if((dir == class="num">0) && checkTime(name)) { if(clr == BuyColor) dir = +class="num">1; else if(clr == SellColor) dir = -class="num">1; } if(dir > class="num">0) { class="type">class="kw">double lot = StringToDouble(ObjectGetString(class="num">0, name, OBJPROP_TEXT)); class=class="str">"cmt">// lot[%] if(lot == class="num">0) lot = Lot; class="type">class="kw">double sl = class="num">0.0, tp = class="num">0.0; if(aux != class="num">0) { if(aux > Ask) { tp = aux; if(DefaultStopLoss != class="num">0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point; } else { sl = aux; if(DefaultTakeProfit != class="num">0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point; } } else { if(DefaultStopLoss != class="num">0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point; if(DefaultTakeProfit != class="num">0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point; } sl = NormalizeDouble(sl, _Digits); tp = NormalizeDouble(tp, _Digits); class="type">int ticket = e.placeMarketOrder(OP_BUY, lot, sl, tp);
用反手市价单平掉空头头寸
当图形对象被识别为 CloseStopLossTakeProfitType(空头平仓、空头止损或空头止盈线)且触发条件满足时,脚本走另一条分支:先由 checkMarket(price) 或 checkTime(name) 给出方向信号 dir,非 0 才继续。 若从对象文本里解析出的 lot 大于 0,就按该手数调用 e.placeMarketOrder(OP_BUY) 反手买入,由系统内部触发 OrderCloseBy() 对销空头;返回票号不等于 -1 即视为成功并 disableLine(i),否则弹「Partial sell close failed」提示。 lot 解析为 0 或负值时,改为 e.closeMarketOrders(e.mask(OP_SELL)) 一把平掉所有空头,返回值大于 0 才禁用该线,否则报「Complete sell close failed」。外汇与贵金属杠杆高,这类自动对销在滑点行情中可能部分失效,务必在 MT5 策略测试器里先跑一遍。 checkActivation() 用 EventHotSpot * _Point 构造一个双侧容差带:Bid 落进 [price - 热点, price + 热点] 直接返回 true;同时保留前一根 Bid 上穿 price 的判定,避免只在盘口粘连时误触。
if(ticket != -class="num">1) class=class="str">"cmt">// success { disableLine(i); } else { showMessage("Market buy failed with &class="macro">#x27;" + name + "&class="macro">#x27;"); } } } else if(style == CloseStopLossTakeProfitType) class=class="str">"cmt">// 空头平仓, 空头止损, 空头止盈 { class="type">int dir = checkMarket(price) || checkTime(name); if(dir != class="num">0) { class="type">class="kw">double lot = StringToDouble(ObjectGetString(class="num">0, name, OBJPROP_TEXT)); class=class="str">"cmt">// lot if(lot > class="num">0) { if(e.placeMarketOrder(OP_BUY, lot) != -class="num">1) class=class="str">"cmt">// 将触发 OrderCloseBy(); { disableLine(i); } else { showMessage("Partial sell close failed with &class="macro">#x27;" + name + "&class="macro">#x27;"); } } else { if(e.closeMarketOrders(e.mask(OP_SELL)) > class="num">0) { disableLine(i); } else { showMessage("Complete sell close failed with &class="macro">#x27;" + name + "&class="macro">#x27;"); } } } } class="type">bool checkActivation(class="kw">const class="type">class="kw">double price) { if(Bid >= price - EventHotSpot * _Point && Bid <= price + EventHotSpot * _Point) { class="kw">return true; } if((PreviousBid < price && Bid >= price)
「用趋势线坐标反推实时价位」
上面那段 checkMarket 只是判断方向,真正要把画线对象变成可监控的价位,还得靠 getCurrentPrice 把趋势线的时间锚点换算成 BAR 索引。
它先读对象的 OBJPROP_TYPE,确认是 OBJ_TREND 才继续;随后取两个端点的 OBJPROP_TIME,用 iBarShift(NULL,0,dt,true) 找对应 K 线位置。若 i1<=i2 或任一返回 -1,说明线画反了或超出历史,直接 Print("Incorrect line") 并返 0,避免后续误判。
在 MT5 里手动画一条趋势线,把这段代码挂到 EA 的 OnChartEvent 里,就能实时拿到线对应的报价值,再喂给前面的 checkMarket 做穿越判定。外汇与贵金属杠杆高,这类自动监控只作辅助,信号失效可能随时发生。
class="type">class="kw">double getCurrentPrice(class="kw">const class="type">class="kw">string name, class="type">class="kw">double &auxiliary, class="type">class="kw">double &auxiliaryFibo) { class="type">int type = (class="type">int)ObjectGetInteger(class="num">0, name, OBJPROP_TYPE); if(type == OBJ_TREND) { class="type">class="kw">datetime dt1 = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0, name, OBJPROP_TIME, class="num">0); class="type">class="kw">datetime dt2 = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0, name, OBJPROP_TIME, class="num">1); class="type">int i1 = iBarShift(NULL, class="num">0, dt1, true); class="type">int i2 = iBarShift(NULL, class="num">0, dt2, true); if(i1 <= i2 || i1 == -class="num">1 || i2 == -class="num">1) { Print("Incorrect line: ", name); class="kw">return class="num">0; }