在 MetaTrader 5 里使用 HedgeTerminal (对冲终端) 面板进行双向交易和仓位对冲, 第二部分·综合运用
- 分形指标与挂单触发的初始化逻辑
- 遍历对冲仓位并触发止盈平仓
- 对冲单的止损止盈修正逻辑
- 从分形与反转柱线推算卖单止损
- 用分形重设买仓止损的逻辑
- 从持仓备注反推挂单方向与止盈对称
- 挂单埋点与过期清理的实操写法
- 挂单寿命与新柱识别的底层写法
- 极值判定与动作日志的函数实现
- 回撤柱抓取与浮点比较的坑
- 把下单回执打到日志里
- HedgeTerminal API 的底层调用链
- 对账双向仓位与挂单的属性枚举
- 对冲接口的平仓标记与请求结构
- 同步与异步发单的线程分水岭
- 异步发单的耗时与报错实况
- 异步下单的日志时间戳拆解
- 用共享文件在多个 EA 之间搬报价
- 跨经纪商报价差怎么抓
- 附件文件怎么摆进 MT5 目录
- 画得少,看得清
「分形指标与挂单触发的初始化逻辑」
EA 在 OnInit 里先创建分形指标句柄:用 iFractals(Symbol(), NULL) 抓当前品种,失败就打印错误码。这一步若漏掉,后续所有基于分形的极值判断都会失效。 魔幻数字按时间帧动态拼接:minPeriod = PeriodSeconds()/60 算出当前周期分钟数,把 Magic 和 minPeriod 转成字符串相加再转回整数。比如 Magic 原值 888、跑在 M15 上,实际魔幻数会变成 88815,避免多周期同品种订单混淆。 OnTick 只在 NewBarDetect() 为真时跑核心逻辑。取前一根已收盘 K 线,用 GetCloseRate 算出一个 0~100 的 closeRate;若 ≤30 且前 N 根内出现最高极值,删旧挂单后下 1 张空头挂单(BAR_TYPE_BEARISH);若 ≥70 且出现最低极值,则下多头挂单。外汇与贵金属杠杆高,这类极值反转挂单在震荡市可能频繁触发止损。 OnDeinit 里用 IndicatorRelease 释放分形句柄,防止 EA 重载时指标句柄泄漏。下面这段是原文 OnInit 到 OnTick 的核心代码,逐行对应上面说的动作。
class="type">int OnInit() { class=class="str">"cmt">//--- 创建 &class="macro">#x27;分形&class="macro">#x27; 指标 ---// hFractals=iFractals(Symbol(),NULL); if(hFractals==INVALID_HANDLE) printf("警告!指标 &class="macro">#x27;分形&class="macro">#x27; 未能创建。原因: "+ (class="type">class="kw">string)GetLastError()); class=class="str">"cmt">//--- 以时间帧调整魔幻数字 ---// class="type">int minPeriod=PeriodSeconds()/class="num">60; class="type">class="kw">string strMagic=(class="type">class="kw">string)Magic+(class="type">class="kw">string)minPeriod; Magic=StringToInteger(strMagic); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- 删除指标 &class="macro">#x27;分形&class="macro">#x27; ---// if(hFractals!=INVALID_HANDLE) IndicatorRelease(hFractals); class=class="str">"cmt">//--- } class="type">void OnTick() { class=class="str">"cmt">//--- 仅当新柱线开盘时运行逻辑。---// class="type">int totals=SupportPositions(); if(NewBarDetect()==true) { class="type">MqlRates rates[]; CopyRates(Symbol(),NULL,class="num">1,class="num">1,rates); class="type">MqlRates prevBar=rates[class="num">0]; class=class="str">"cmt">//--- 设置新挂单 ---// class="type">class="kw">double closeRate=GetCloseRate(prevBar); if(closeRate<=class="num">30 && BarIsExtremum(class="num">1,N,TYPE_EXTREMUM_HIGHEST)) { DeleteOldPendingOrders(class="num">0); SetNewPendingOrder(class="num">1,BAR_TYPE_BEARISH); } else if(closeRate>=class="num">70 && BarIsExtremum(class="num">1,N,TYPE_EXTREMUM_LOWEST)) { DeleteOldPendingOrders(class="num">0); SetNewPendingOrder(class="num">1,BAR_TYPE_BULLISH); } DeleteOldPendingOrders(OldPending); } class=class="str">"cmt">//--- }
遍历对冲仓位并触发止盈平仓
在 MT5 净ting模式下做多币种对冲,第一步是把当前所有活跃仓位捞出来筛一遍。下面这段代码用 TransactionsTotal() 拿到事务总数,按位置索引逐个 TransactionSelect,只保留 TRANS_HEDGE_POSITION 类型且 Magic 匹配的仓位,冻结状态的直接跳过,避免后续读写报错。 筛完之后,对 ENTRY_BUY1 / ENTRY_SELL1 这类首仓,先比对当前 SL 与算法算出的 GetStopLossLevel() 是否一致,不一致就置 modify 标记准备改单;同时取 SYMBOL_ASK / SYMBOL_BID 实时报价。 止盈判定很直接:买仓的 tp 小于 bid 且 tp 非零,或卖仓的 tp 大于 ask,就走 REQUEST_CLOSE_POSITION 且 close_type 标成 CLOSE_AS_TAKE_PROFIT,注释写「由 EA 止盈平仓」。外汇与贵金属杠杆高,这种自动平仓逻辑在滑点行情中可能失效,实盘前务必在策略测试器跑通。 别把正态当圣经:HedgePositionGetInteger 的 MAGIC 过滤若写错,会把别的 EA 仓位误判成自己的,count 统计和改单都会乱套。开 MT5 把 Magic 参数和你实盘 EA 对齐再验证。
class="type">int count=class="num">0; class=class="str">"cmt">//--- 分析活跃仓位... ---// for(class="type">int i=class="num">0; i<TransactionsTotal(); i++) class=class="str">"cmt">// 获取仓位总数。 { class=class="str">"cmt">//--- 选择主要活跃仓位 ---// if(!TransactionSelect(i, SELECT_BY_POS, MODE_TRADES))class="kw">continue; class=class="str">"cmt">// 选择活跃事务 if(TransactionType() != TRANS_HEDGE_POSITION)class="kw">continue; class=class="str">"cmt">// 仅选择对冲仓位 if(HedgePositionGetInteger(HEDGE_POSITION_MAGIC) != Magic) class=class="str">"cmt">// 以魔幻数字选择主要仓位 if(HedgePositionGetInteger(HEDGE_POSITION_STATE) == POSITION_STATE_FROZEN) class=class="str">"cmt">// 如果仓位为零 - 继续 class="kw">continue; class=class="str">"cmt">// 稍后尝试访问仓位 count++; class=class="str">"cmt">//--- 我们要选择哪个仓位?... ---// ENUM_ENTRY_TYPE type=IdentifySelectPosition(); class="type">bool modify=false; class="type">class="kw">double sl = class="num">0.0; class="type">class="kw">double tp = class="num">0.0; class="kw">switch(type) { case ENTRY_BUY1: case ENTRY_SELL1: { class=class="str">"cmt">//--- 检查止损, 止盈位并在需要时修改它。---// class="type">class="kw">double currentStop=HedgePositionGetDouble(HEDGE_POSITION_SL); sl=GetStopLossLevel(); if(!DoubleEquals(sl,currentStop)) modify=true; tp=GetTakeProfitLevel(); class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); class="type">class="kw">double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); class=class="str">"cmt">//--- 如果价格超越止盈位, 按止盈平仓 class="type">bool isBuyTp=tp<bid && !DoubleEquals(tp,class="num">0.0) && type==ENTRY_BUY1; class="type">bool isSellTp=tp>ask && type==ENTRY_SELL1; if(isBuyTp || isSellTp) { HedgeTradeRequest request; request.action=REQUEST_CLOSE_POSITION; request.exit_comment="由 EA 止盈平仓"; request.close_type=CLOSE_AS_TAKE_PROFIT; if(!SendTradeRequest(request)) { ENUM_HEDGE_ERR error=GetHedgeError(); class="type">class="kw">string logs=error==HEDGE_ERR_TASK_FAILED ?". 打印日志..." : "";
◍ 对冲单的止损止盈修正逻辑
这段逻辑处理的是对冲仓位在运行过程中,何时需要把止损或止盈推到新位置。核心是一个 modify 标志位:只要新算出来的 tp / sl 和当前持仓参数不一致,就把 modify 置为 true,随后统一发修改请求。 对于 ENTRY_BUY1 / ENTRY_SELL1 类型,代码比对期望止盈 tp 与当前持仓的 HEDGE_POSITION_TP,不等就改;对于 ENTRY_BUY2 / ENTRY_SELL2,则检查尾随止损:买型要求新 sl 大于当前 sl 才修改,卖型在新 sl 更小或当前根本没挂 SL(usingSL 为 false)时修改。 修改请求通过 HedgeTradeRequest 结构发出,action 固定为 REQUEST_MODIFY_SLTP,并按入口类型写入离场注释——「由止盈位离场」或「由尾随止损离场」。SendTradeRequest 失败时会打印错误枚举与可能的任务日志,HEDGE_ERR_TASK_FAILED 才触发 PrintTaskLog(),随后 ResetHedgeError() 清状态。 在 MT5 里跑这套,重点看 modify 触发频率:若行情横盘导致 sl/tp 反复微动,可能频繁发修改请求,增加点差与拒绝概率。外汇与贵金属杠杆高,这类自动修正须先在策略测试器用真实点差回测。
printf("因止盈失败平仓。原因: "+EnumToString(error)+" "+logs); if(error==HEDGE_ERR_TASK_FAILED) PrintTaskLog(); ResetHedgeError(); } else class="kw">break; } class="type">class="kw">double currentTakeProfit=HedgePositionGetDouble(HEDGE_POSITION_TP); if(!DoubleEquals(tp,currentTakeProfit)) modify=true; class="kw">break; } case ENTRY_BUY2: { class=class="str">"cmt">//--- 检查止损位并设置修改标志。---// sl=GetStopLossLevel(); class="type">class="kw">double currentStop=HedgePositionGetDouble(HEDGE_POSITION_SL); if(sl>currentStop) modify=true; class="kw">break; } case ENTRY_SELL2: { class=class="str">"cmt">//--- 检查止损位并设置修改标志。---// sl=GetStopLossLevel(); class="type">class="kw">double currentStop=HedgePositionGetDouble(HEDGE_POSITION_SL); class="type">bool usingSL=HedgePositionGetInteger(HEDGE_POSITION_USING_SL); if(sl<currentStop || !usingSL) modify=true; class="kw">break; } } class=class="str">"cmt">//--- 如果需要修改止损, 止盈位 - 修改它。---// if(modify) { HedgeTradeRequest request; request.action=REQUEST_MODIFY_SLTP; request.sl = sl; request.tp = tp; if(type==ENTRY_BUY1 || type==ENTRY_SELL1) request.exit_comment="由止盈位离场"; else request.exit_comment="由尾随止损离场"; if(!SendTradeRequest(request)) { ENUM_HEDGE_ERR error=GetHedgeError(); class="type">class="kw">string logs=error==HEDGE_ERR_TASK_FAILED ?". 打印日志..." : ""; printf("修改止损或止盈失败。原因: "+EnumToString(error)+" "+logs); if(error==HEDGE_ERR_TASK_FAILED) PrintTaskLog(); ResetHedgeError(); } else class="kw">break;
「从分形与反转柱线推算卖单止损」
在 MT5 对冲仓位逻辑里,止损不是写死的数字,而是随市场结构浮动。GetStopLossLevel() 先取当前品种 tick size 的 3 倍作为最小偏移 point,再加载反转柱线 ReversalBar,失败就直接返回 0.0 空止损。 当识别为 ENTRY_SELL2 且仓位已带止损时,函数会读取原有 SL,并从反转柱线时间起复制到当前时间的上分形缓冲。循环从数组倒数第 4 个开始往前扫,跳过等于 DBL_MAX 或与原 SL 重合的分形,只保留低于原 SL 的上分形。 若某个分形低于原 SL,且高于卖价加冻结层级(SYMBOL_TRADE_FREEZE_LEVEL 换算的 freeze),就把 SL 重设为该分形加 point。这样止损可能贴着最近的结构位,但永远在冻结区之外。 ENTRY_SELL1 则简单得多:直接取反转柱线的高点加 point 作为 SL。外汇与贵金属杠杆高,这类动态止损仍可能因跳空失效,实盘前请在策略测试器用 2023 年 XAUUSD 数据跑一遍验证。
class="type">class="kw">double GetStopLossLevel() { class=class="str">"cmt">//--- class="type">class="kw">double point=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*class="num">3; class="type">class="kw">double fractals[]; class="type">class="kw">double sl=class="num">0.0; class="type">MqlRates ReversalBar; if(!LoadReversalBar(ReversalBar)) { printf("反转柱线加载失败。"); class="kw">return sl; } class=class="str">"cmt">//--- 我们要选择哪个仓位?... ---// class="kw">switch(IdentifySelectPosition()) { case ENTRY_SELL2: { if(HedgePositionGetInteger(HEDGE_POSITION_USING_SL)) { sl=NormalizeDouble(HedgePositionGetDouble(HEDGE_POSITION_SL),Digits()); CopyBuffer(hFractals,UPPER_LINE,ReversalBar.time,TimeCurrent(),fractals); for(class="type">int i=ArraySize(fractals)-class="num">4; i>=class="num">0; i--) { if(DoubleEquals(fractals[i],DBL_MAX))class="kw">continue; if(DoubleEquals(fractals[i],sl))class="kw">continue; if(fractals[i]<sl) { class="type">class="kw">double price= SymbolInfoDouble(Symbol(),SYMBOL_ASK); class="type">int ifreeze =(class="type">int)SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL); class="type">class="kw">double freeze=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*ifreeze; if(fractals[i]>price+freeze) sl=NormalizeDouble(fractals[i]+point,Digits()); } } class="kw">break; } } case ENTRY_SELL1: sl=ReversalBar.high+point; class="kw">break;
用分形重设买仓止损的逻辑
在 ENTRY_BUY2 分支里,若持仓已带止损,程序会先取出原止损价 sl 并做精度归一化,再拉取分形指标的下轨缓冲。 CopyBuffer 从反转 K 线时间拉到当前时间,把 fractals 数组填满;随后从倒数第 4 根开始向前扫,跳过等于 DBL_MAX 或与原 sl 相等的无效分形值。 当某个下分形价高于原 sl,且低于「卖价减冻结水平」时,就把 sl 重设为该分形减一个点——这等于把止损拖到最近的有效支撑下方。外汇与贵金属波动剧烈,这种拖损方式只是概率上提高抗噪性,不保证不被扫。 最后 ENTRY_BUY1 直接以反转 K 低点减一点作为 sl,统一归一化后返回。下方代码逐行对应上述过程。
case ENTRY_BUY2: if(HedgePositionGetInteger(HEDGE_POSITION_USING_SL)) { sl=NormalizeDouble(HedgePositionGetDouble(HEDGE_POSITION_SL),Digits()); CopyBuffer(hFractals,LOWER_LINE,ReversalBar.time,TimeCurrent(),fractals); for(class="type">int i=ArraySize(fractals)-class="num">4; i>=class="num">0; i--) { if(DoubleEquals(fractals[i],DBL_MAX))class="kw">continue; if(DoubleEquals(fractals[i],sl))class="kw">continue; if(fractals[i]>sl) { class="type">class="kw">double price= SymbolInfoDouble(Symbol(),SYMBOL_BID); class="type">int ifreeze =(class="type">int)SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL); class="type">class="kw">double freeze=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*ifreeze; if(fractals[i]<price-freeze) sl=NormalizeDouble(fractals[i]-point,Digits()); } } class="kw">break; } case ENTRY_BUY1: sl=ReversalBar.low-point; } sl=NormalizeDouble(sl,Digits()); class="kw">return sl;
◍ 从持仓备注反推挂单方向与止盈对称
这段逻辑先把止损距离做成止盈的对称基准:用 MathAbs 取 openPrice 与 sl 之差的绝对值,再经 NormalizeDouble 按当前品种 Digits 精度裁剪,得到 deltaStopLoss。若持仓类型标记为 ENTRY_BUY1,则在开盘价上方加该距离作为 tp;ENTRY_SELL1 则在下方减,返回计算出的止盈价,其余情况回 0.0。 仓位类型的识别靠持仓备注尾巴的两个字符。函数取 HedgePositionGetString 拿到的备注,截最后 2 位:"#0" 配多向返回 ENTRY_BUY1、空向返回 ENTRY_SELL1;"#1" 同理映射到 ENTRY_BUY2 / ENTRY_SELL2,匹配不上就回 ENTRY_BAD_COMMENT。实盘里若你手动改了备注,这层识别会直接失效。 挂单生成函数 SetNewPendingOrder 用 CopyRates 按 index_bar 拉 1 根柱线数据,并把 request.volume 初始化为 SYMBOL_VOLUME_MIN。外汇与贵金属杠杆高、滑点跳空频繁,这种按柱线挂单的方式在重大数据发布时可能反复触发或漏单,请在 MT5 策略测试器用极值点差跑一遍验证。
class="type">class="kw">double deltaStopLoss=MathAbs(NormalizeDouble(openPrice-sl,Digits())); if(type==ENTRY_BUY1) tp=openPrice+deltaStopLoss; if(type==ENTRY_SELL1) tp=openPrice-deltaStopLoss; class="kw">return tp; } else class="kw">return class="num">0.0; class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 识别选择的仓位类型。 |//| 结果 | class=class="str">"cmt">//| 返回仓位类型。参阅 ENUM_ENTRY_TYPE | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_ENTRY_TYPE IdentifySelectPosition() { class=class="str">"cmt">//--- class="type">class="kw">string comment=HedgePositionGetString(HEDGE_POSITION_ENTRY_COMMENT); class="type">int pos=StringLen(comment)-class="num">2; class="type">class="kw">string subStr=StringSubstr(comment,pos); ENUM_TRANS_DIRECTION posDir=(ENUM_TRANS_DIRECTION)HedgePositionGetInteger(HEDGE_POSITION_DIRECTION); if(subStr=="#class="num">0") { if(posDir==TRANS_LONG) class="kw">return ENTRY_BUY1; if(posDir==TRANS_SHORT) class="kw">return ENTRY_SELL1; } else if(subStr=="#class="num">1") { if(posDir==TRANS_LONG) class="kw">return ENTRY_BUY2; if(posDir==TRANS_SHORT) class="kw">return ENTRY_SELL2; } class="kw">return ENTRY_BAD_COMMENT; class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 依据 index_bar 所在柱线设置或高或低的挂单 | class=class="str">"cmt">//| 输入参数 | class=class="str">"cmt">//| index_bar - 柱线索引。 | class=class="str">"cmt">//| barType - 柱线类型。参阅枚举 ENUM_BAR_TYPE。 | class=class="str">"cmt">//| 结果 | class=class="str">"cmt">//| True 如果新订单成功设置, 否则 false。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool SetNewPendingOrder(class="type">int index_bar,ENUM_BAR_TYPE barType) { class=class="str">"cmt">//--- class="type">MqlRates rates[class="num">1]; CopyRates(Symbol(),NULL,index_bar,class="num">1,rates); class="type">MqlTradeRequest request={class="num">0}; request.volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
「挂单埋点与过期清理的实操写法」
在 MT5 里用挂单跟随价格行为,核心是把当前 K 线的高低点向外偏移几个 tick 再挂单。下面这段逻辑按柱线方向决定挂单类型:阴线就在 rates[0].low 减去 3 倍 tick size 挂 SELL_STOP,阳线就在 rates[0].high 加上同样偏移挂 BUY_STOP,偏移量直接取 SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE)*3,不写死点数,换品种也不用改。 request.type_filling 设成 ORDER_FILLING_FOK,type_time 设成 ORDER_TIME_GTC,意味着挂单要么全额成交要么不成交、且一直挂到主动删除。代码里对同一种方向连续发两次 OrderSend,comment 末尾拼了 #0 和 #1,方便后面按注释特征精准删除,不至于误撤别的 EA 的单。 DeleteOldPendingOrders 这个函数遍历 OrdersTotal(),用 OrderGetTicket 按索引拿ticket再 OrderSelect,只处理 Magic 和 Symbol 都匹配的挂单。判断过时的依据是 ORDER_TIME_SETUP 返回的挂单创建时间,若早于当前 n_bars 根 K 线之前就清掉——外汇和贵金属波动跳空频繁,这种自动清理能降低陈旧挂单被秒杀的概率,但无法消除滑点风险,实盘前请在策略测试器用真实点差回测。
class="type">class="kw">double vol=request.volume; request.symbol = Symbol(); request.action = TRADE_ACTION_PENDING; request.type_filling=ORDER_FILLING_FOK; request.type_time=ORDER_TIME_GTC; request.magic=Magic; class="type">class="kw">double point=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*class="num">3; class="type">class="kw">string comment=""; if(barType==BAR_TYPE_BEARISH) { request.price=rates[class="num">0].low-point; comment="空头柱线空单入场"; request.type=ORDER_TYPE_SELL_STOP; } else if(barType==BAR_TYPE_BULLISH) { request.price=rates[class="num">0].high+point; comment="多头柱线多单入场"; request.type=ORDER_TYPE_BUY_STOP; } class="type">MqlTradeResult result={class="num">0}; class=class="str">"cmt">//--- 发送挂单两次... for(class="type">int i=class="num">0; i<class="num">2; i++) { request.comment=comment+" #"+(class="type">class="kw">string)i; class=class="str">"cmt">// 按照注释删除订单; if(!OrderSend(request,result)) { printf("交易错误 #"+(class="type">class="kw">string)result.retcode+" "+ result.comment); class="kw">return false; } } class="kw">return true; class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除旧挂单。如果挂单设置旧于 | class=class="str">"cmt">//| n_bars 之前, 挂单将被删除。 | class=class="str">"cmt">//| 输入参数 | class=class="str">"cmt">//| period - 柱线计数。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DeleteOldPendingOrders(class="type">int n_bars) { class=class="str">"cmt">//--- for(class="type">int i=class="num">0; i<OrdersTotal(); i++) { class="type">ulong ticket = OrderGetTicket(i); class=class="str">"cmt">// 按照索引获取订单号。 if(!OrderSelect(ticket)) class=class="str">"cmt">// 如果未选择继续。 class="kw">continue; if(Magic!=OrderGetInteger(ORDER_MAGIC)) class=class="str">"cmt">// 如果魔幻数字不是主体。 class="kw">continue; if(OrderGetString(ORDER_SYMBOL)!=Symbol()) class=class="str">"cmt">// 如果品名不是主体继续。 class="kw">continue; class=class="str">"cmt">//--- 过时计数 ---// class="type">class="kw">datetime timeSetup=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TIME_SETUP);
挂单寿命与新柱识别的底层写法
EA 里挂单不能无限躺着,得按时间或 K 线根数清场。下面这段用 TimeCurrent() 减去挂单建立时刻算出已过的秒数,再跟 PeriodSeconds()*n_bars 比大小,超了就发 TRADE_ACTION_REMOVE 把单子撤掉,撤单失败会直接 printf 打出 retcode 和 comment,方便你上 MT5 终端看日志排错。 新柱判断别依赖定时器,直接用 CopyTime 抓最新一根的 open time 存进 lastTime,每次不等就说明换柱了,返回 true。这种写法比 iTime 配合全局变量轻,回测和实盘都不会漏掉跳空后的第一根。 GetCloseRate 干一件事:把收盘相对最低价的位移占全柱高度的比例算成百分比。highLowDelta 为 0 时走 DoubleEquals 保护,避免贵金属秒级报价重合时除零崩 EA。返回的 percentClose 后续接混沌策略的 ENUM_TYPE_BAR 分类,0 是当前柱、1 是前一柱,你复制去测时记得把 bar 结构从 CopyRates 填好再传参。 外汇和贵金属杠杆高,这类自动撤单和柱线识别逻辑先在策略测试器跑够历史样本,再上模拟盘验证延迟环境下的撤单成功率。
class="type">int secElapsed=(class="type">int)(TimeCurrent()-timeSetup); class=class="str">"cmt">//--- 删除旧挂单 ---// if(secElapsed>=PeriodSeconds() *n_bars) { class="type">MqlTradeRequest request={class="num">0}; class="type">MqlTradeResult result={class="num">0}; request.action= TRADE_ACTION_REMOVE; request.order = ticket; if(!OrderSend(request,result)) printf("删除挂单失败。原因 #"+(class="type">class="kw">string)result.retcode+" "+result.comment); } } class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 检测新柱线 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool NewBarDetect(class="type">void) { class=class="str">"cmt">//--- class="type">class="kw">datetime timeArray[class="num">1]; CopyTime(Symbol(),NULL,class="num">0,class="num">1,timeArray); if(lastTime!=timeArray[class="num">0]) { lastTime=timeArray[class="num">0]; class="kw">return true; } class="kw">return false; class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取收盘价。在混沌交易策略里定义的柱线类型 | class=class="str">"cmt">//| 并等于枚举 &class="macro">#x27;ENUM_TYPE_BAR&class="macro">#x27;. | class=class="str">"cmt">//| 输入参数 | class=class="str">"cmt">//| index - 柱线系列索引。例如: | class=class="str">"cmt">//| &class="macro">#x27;class="num">0&class="macro">#x27; - 当前柱线。class="num">1 - 前一柱线。 | class=class="str">"cmt">//| 结果 | class=class="str">"cmt">//| ENUM_TYPE_BAR 类型。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetCloseRate(const class="type">MqlRates &bar) { class=class="str">"cmt">//--- class="type">class="kw">double highLowDelta = bar.high-bar.low; class=class="str">"cmt">// 计算柱线高度。 class="type">class="kw">double lowCloseDelta = bar.close - bar.low; class=class="str">"cmt">// 计算收盘价 - 最低价增量。 class="type">class="kw">double percentClose=class="num">0.0; if(!DoubleEquals(lowCloseDelta, class="num">0.0)) class=class="str">"cmt">// 除零保护。 percentClose = lowCloseDelta/highLowDelta*class="num">100.0; class=class="str">"cmt">// 计算 &class="macro">#x27;lowCloseDelta&class="macro">#x27; 与 &class="macro">#x27;highLowDelta&class="macro">#x27; 的百分比。 class="kw">return percentClose; class=class="str">"cmt">//--- }
◍ 极值判定与动作日志的函数实现
判断某根柱线是否为指定周期内的极值,是价格行为过滤噪音的基础。下面这个函数以索引和周期入参,对当前品种做 CopyRates 后逐根比较高低点,命中即返回 true,否则 false。 函数里 rates[0] 代表入参 index 指向的柱线,循环从 i=1 开始扫后续 N 根。若 type 为 TYPE_EXTREMUM_HIGHEST 且 rates[0].high 小于任一 rates[i].high,直接 return false;最低点逻辑对称。注意原代码用 N+1 作为拷贝长度,N 需在外部定义,否则编译期报未声明。 被注释的 tick_volume 比较段是可选成交量确认:解开后,若当前柱成交量小于历史任一根,也判为非极值。外汇与贵金属波动受流动性影响大,成交量过滤在高风险时段可能显著改变信号数量。 PrintTaskLog 则遍历对冲仓位的动作总数,用 GetActionResult 取出每步的类型与返回码并 printf。实盘跑策略时,把这段代码挂到测试尾端,能直接看到每笔挂单或平仓动作的执行结果,方便在 MT5 策略测试器里核对逻辑是否按预期触发。
class="type">bool BarIsExtremum(const class="type">int index,const class="type">int period,ENUM_TYPE_EXTREMUM type) { class=class="str">"cmt">//--- 拷贝汇率 --- // class="type">MqlRates rates[]; ArraySetAsSeries(rates,true); CopyRates(Symbol(),NULL,index,N+class="num">1,rates); class=class="str">"cmt">//--- 搜索极值 --- // for(class="type">int i=class="num">1; i<ArraySize(rates); i++) { class=class="str">"cmt">//--- 如果您打算包含交易量分析则重置注释。---// class=class="str">"cmt">//if(rates[class="num">0].tick_volume<rates[i].tick_volume) class=class="str">"cmt">// class="kw">return false; if(type==TYPE_EXTREMUM_HIGHEST && rates[class="num">0].high<rates[i].high) class="kw">return false; if(type==TYPE_EXTREMUM_LOWEST && rates[class="num">0].low>rates[i].low) class="kw">return false; } class="kw">return true; class=class="str">"cmt">//--- } class="type">void PrintTaskLog() { class=class="str">"cmt">//--- class="type">uint totals=(class="type">uint)HedgePositionGetInteger(HEDGE_POSITION_ACTIONS_TOTAL); for(class="type">uint i = class="num">0; i<totals; i++) { class="type">uint retcode=class="num">0; ENUM_TARGET_TYPE type; GetActionResult(i,type,retcode); printf("---> 动作 #"+(class="type">class="kw">string)i+"; "+EnumToString(type)+"; 返回码: "+(class="type">class="kw">string)retcode); } class=class="str">"cmt">//--- }
「回撤柱抓取与浮点比较的坑」
EA 里要定位对冲单建仓前的那根反转柱,靠 LoadReversalBar() 从持仓入场毫秒时间戳反推。时间换算把 HEDGE_POSITION_ENTRY_TIME_SETUP_MSC 除以 1000 再加 1 秒,用 CopyRates 取该时间点起 2 根柱,最后返回倒数第一根(索引 size-1)给 bar 引用。 double 别直接写 a==b。DoubleEquals() 用 fabs(a-b)<=16*DBL_EPSILON*fmax(fabs(a),fabs(b)) 做容差判等,DBL_EPSILON 约 2.22e-16,乘 16 倍仍远小于报价小数点后四位精度,能躲开二进制浮点误差导致的误判。 修改 SL/TP 时区分首单与尾随:ENTRY_BUY1/ENTRY_SELL1 离场备注写“由止盈位离场”,其余写“由尾随止损离场”。SendTradeRequest 失败就抓 HEDGE_ERR 枚举打印,若是 TASK_FAILED 再调 PrintTaskLog() 把每笔动作回码(GetActionResult 遍历 HEDGE_POSITION_ACTIONS_TOTAL)吐出来。 外汇与贵金属杠杆高、滑点随机,这套容差和日志逻辑在实盘前务必用策略测试器跑一遍,否则报错时只能盲调。
class=class="str">"cmt">//| 输出参数 | class=class="str">"cmt">//| bar - class="type">MqlRates 柱线。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool LoadReversalBar(class="type">MqlRates &bar) { class=class="str">"cmt">//--- class="type">class="kw">datetime time=(class="type">class="kw">datetime)(HedgePositionGetInteger(HEDGE_POSITION_ENTRY_TIME_SETUP_MSC)/class="num">1000+class="num">1); class="type">MqlRates rates[]; ArraySetAsSeries(rates,true); CopyRates(Symbol(),NULL,time,class="num">2,rates); class="type">int size=ArraySize(rates); if(size==class="num">0)class="kw">return false; bar=rates[size-class="num">1]; class="kw">return true; class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 比较两个双精度数字。 | class=class="str">"cmt">//| 结果 | class=class="str">"cmt">//| True 如果两个双精度数字相等, 否则 false。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool DoubleEquals(const class="type">class="kw">double a,const class="type">class="kw">double b) { class=class="str">"cmt">//--- class="kw">return(fabs(a-b)<=class="num">16*DBL_EPSILON*fmax(fabs(a),fabs(b))); class=class="str">"cmt">//--- } ... if(modify) { HedgeTradeRequest request; request.action=REQUEST_MODIFY_SLTP; request.sl = sl; request.tp = tp; if(type==ENTRY_BUY1 || type==ENTRY_SELL1) request.exit_comment="由止盈位离场"; else request.exit_comment="由尾随止损离场"; if(!SendTradeRequest(request)) { ENUM_HEDGE_ERR error=GetHedgeError(); class="type">class="kw">string logs=error==HEDGE_ERR_TASK_FAILED ?". 打印日志..." : ""; printf("修改止损或止盈失败。原因: "+EnumToString(error)+" "+logs); if(error==HEDGE_ERR_TASK_FAILED) PrintTaskLog(); ResetHedgeError(); } else class="kw">break; } ... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 打印当前错误和结果。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PrintTaskLog() { class=class="str">"cmt">//--- class="type">uint totals=(class="type">uint)HedgePositionGetInteger(HEDGE_POSITION_ACTIONS_TOTAL); for(class="type">uint i = class="num">0; i<totals; i++) { class="type">uint retcode=class="num">0; ENUM_TARGET_TYPE type; GetActionResult(i,type,retcode);
把下单回执打到日志里
在 MQL5 里批量发单后,最怕的是静默失败。上面这段把动作序号、订单类型枚举和服务器返回码拼成一行打到终端,能让你在策略测试器里逐条核对哪一笔被拒。 EnumToString(type) 直接把 ENUM_ORDER_TYPE 转成可读字符串,比只看数字返回码直观得多;retcode 非 0 时基本意味着该笔动作被经纪商拒绝或触发了风控,外汇与贵金属品种在高波动时段这种概率会明显抬升。 开 MT5 把这段塞进你的下单循环末尾,跑一遍历史回测,翻日志看是否有连续 retcode=10019(余额不足)或 10030(无效请求)的聚集现象,比盲信 EA 面板绿灯更靠谱。
printf("---> 动作 #"+(class="type">class="kw">string)i+"; "+EnumToString(type)+"; 返回码: "+(class="type">class="kw">string)retcode); } class=class="str">"cmt">//--- }
◍ HedgeTerminal API 的底层调用链
做双向仓位自动化,第一步永远是先把对象选出来。TransactionsTotal() 给你事务池里可用事务的总数,默认数据源是 MODE_TRADES,遍历所有事务只能靠它打底。 TransactionSelect() 按索引或独有 ID 选一笔事务:按索引选复杂度是 O(1),按 ID 选逐渐趋向 O(log2(n))。选完之后想动仓位里的订单,得用 HedgeOrderSelect(),想动成交得再用 HedgeDealSelect() 且订单得先被选过。任何一步失败直接看 GetHedgeError(),别靠猜。 取属性有一套对称函数:HedgePositionGetInteger/Double/String 对应仓位,HedgeOrderGetInteger/Double 对应订单,HedgeDealGetInteger/Double 对应成交。Integer 类返回 ulong,必须显式转换成你想要的 int/long/datetime/bool,不转编译期就给你脸色。 异步请求容易卡死,HedgePropertySetInteger() 可设仓位锁定时间。例程里写的是发异步请求后 30 秒没收到服务器响应就解锁阻塞仓位;HedgePropertyGetInteger() 则把实际阻塞时间捞回来显示。外汇和贵金属杠杆高,异步失败不回滚,这个超时值建议你在 MT5 里实测一遍再定。 改仓位的唯一入口是 SendTradeRequest(),但仓位必须先用 TransactionSelect() 预选,请求结构填 HedgeTradeRequest。异步模式返回 true 只代表任务排上队,不代表成交;同步模式才是单线程跑完能拿结果控成败。失败后用 TotalActionsTask() 拿子任务总数,再挨个 GetActionResult() 按索引查 target_type 和 retcode,才能定位是哪笔子任务被服务器拒了。
「对账双向仓位与挂单的属性枚举」
在 MT5 里做对冲终端开发,活跃与历史事务列表里的挂单、双向仓位都能被程序抓取分析。TransactionSelect() 靠 ENUM_MODE_SELECT 决定用索引还是单号定位,再用 ENUM_MODE_TRADES / MODE_HISTORY 切数据源——这套选择逻辑直接决定你后面读到的属性是不是实时仓。 双向仓位的市场方向由 ENUM_TRANS_DIRECTION 给出:TRANS_LONG 为买入、TRANS_SHORT 为卖出,经纪商入金类操作则是 TRANS_NDEF。仓位状态分两层,ENUM_HEDGE_POSITION_STATUS 区分活跃栏与历史栏,ENUM_HEDGE_POSITION_STATE 里的 POSITION_STATE_FROZEN 表示被锁定不可改,此时只能等解锁。 具体数值靠三类 Get 函数拿:HedgePositionGetInteger() 取开平仓毫秒时间戳(自 1970-01-01 起)、魔幻数、SL/TP 使用标志等;HedgePositionGetDouble() 取成交量、加权开平仓价、佣金、滑点。注意滑点定义为最佳入场价与加权入场价的差,不是成交价减请求价。 订单侧 HedgeOrderGetInteger() 能拿到 HEDGE_ORDER_TIME_EXECUTED_MSC,文档明确它等于订单最后成交时间;HEDGE_ORDER_VOLUME_REJECTED 则是初始量减执行量的拒单量。出错时 GetHedgeError() 返回 ENUM_HEDGE_ERR,比如 HEDGE_ERR_POS_FROZEN 对应仓位正被修改,HEDGE_ERR_WRONG_VOLUME 是量纲传错。 任务追踪用 ENUM_TASK_STATUS:TASK_STATUS_EXECUTING 表示改仓指令在飞,TASK_STATUS_FAILED 就是这次改仓废了。外汇与贵金属杠杆高,冻结态误判可能造成重复发单,实盘前务必在策略测试器用历史数据跑一遍状态机。
对冲接口的平仓标记与请求结构
在 MT5 对冲账户模式下,修改已有仓位的止损止盈或离场注释,靠的是 REQUEST_MODIFY_SLTP 与 REQUEST_MODIFY_COMMENT 两类动作标识。前者直接改写 SL/TP 价位,后者只动 exit_comment,不影响风控线,实盘里常用来给策略打标签便于复盘。 ENUM_CLOSE_TYPE 这个枚举专门描述双向仓位被平掉的原因,不是给你下指令用的,而是回读成交明细时判断离场性质。它只有三个值:CLOSE_AS_MARKET 表示没挂 SL/TP 或都没触达而被市价平掉;CLOSE_AS_STOP_LOSS 是扫损;CLOSE_AS_TAKE_PROFIT 是触盈。统计刷单或网格策略的胜率时,拿这个字段区分「被动止损」和「主动止盈」比看盈亏额更干净。 下面的 HedgeTradeRequest 结构是把所有对冲交易动作打包的容器。注意它的默认构造函数里 deviation 写死为 3 点、asynch_mode 为 false、action 预置 REQUEST_CLOSE_POSITION——如果你要做异步批量平仓,不显式改 asynch_mode 就会阻塞当前线程。外汇与贵金属杠杆高,SL/TP 改写失败可能瞬间扩大敞口,建议先在策略测试器里跑通再上真仓。 SetTimeOut 那段演示了 HedgePropertySetInteger(HEDGE_PROP_TIMEOUT,30) 把接口超时设为 30 秒,返回 bool 告知是否生效。GetTimeOut 则回读该值。网络抖动时把超时从默认调大到 30 秒,能降低因 broker 响应慢导致的误判撤单。
class="type">int TransactionsTotal(ENUM_MODE_TRADES pool = MODE_TRADES); ENUM_TRANS_TYPE TransactionType(class="type">void); class="type">bool TransactionSelect(class="type">int index, ENUM_MODE_SELECT select = SELECT_BY_POS, ENUM_MODE_TRADES pool=MODE_TRADES ); class="type">bool HedgeOrderSelect(ENUM_HEDGE_ORDER_SELECTED_TYPE type); class="type">bool HedgeDealSelect(class="type">int index); class="type">ulong HedgePositionGetInteger(ENUM_HEDGE_POSITION_PROP_INTEGER class="kw">property); class="type">ulong HedgePositionGetDouble(ENUM_HEDGE_POSITION_PROP_DOUBLE class="kw">property); class="type">ulong HedgePositionGetString(ENUM_HEDGE_POSITION_PROP_STRING class="kw">property); class="type">ulong HedgeOrderGetInteger(ENUM_HEDGE_ORDER_PROP_INTEGER class="kw">property); class="type">class="kw">double HedgeOrderGetDouble(ENUM_HEDGE_ORDER_PROP_DOUBLE class="kw">property); class="type">ulong HedgeOrderGetInteger(ENUM_HEDGE_DEAL_PROP_INTEGER class="kw">property); class="type">ulong HedgeOrderGetDouble(ENUM_HEDGE_DEAL_PROP_DOUBLE class="kw">property); class="type">bool HedgePropertySetInteger(ENUM_HEDGE_PROP_INTEGER class="kw">property, class="type">long value); class="type">void SetTimeOut() { class="type">bool res=HedgePropertySetInteger(HEDGE_PROP_TIMEOUT,class="num">30); if(res) printf("属性设置成功"); else printf("属性未设置"); } class="type">long HedgePropertyGetInteger(ENUM_HEDGE_PROP_INTEGER class="kw">property); class="type">void GetTimeOut() { class="type">int seconds=HedgePropertyGetInteger(HEDGE_PROP_TIMEOUT); printf("超时为 "+(class="type">class="kw">string) 秒); } ENUM_HEDGE_ERR GetHedgeError(class="type">void); class="type">void ResetHedgeError(class="type">void); class="type">uint TotalActionsTask(class="type">void); class="type">void GetActionResult(class="type">uint index, ENUM_TARGET_TYPE& target_type, class="type">uint& retcode); class="type">bool SendTradeRequest(HedgeTradeRequest& request); class="kw">struct HedgeTradeRequest { ENUM_REQUEST_TYPE action; class=class="str">"cmt">// 动作类型 class="type">class="kw">double volume; class=class="str">"cmt">// 持仓量 ENUM_CLOSE_TYPE close_type; class=class="str">"cmt">// 平仓标记 class="type">class="kw">double sl; class=class="str">"cmt">// 止损位 class="type">class="kw">double tp; class=class="str">"cmt">// 止盈位 class="type">class="kw">string exit_comment; class=class="str">"cmt">// 离场注释 class="type">uint retcode; class=class="str">"cmt">// 最后执行操作的返回代码 class="type">bool asynch_mode; class=class="str">"cmt">// true 如果需异步执行, 否则 false class="type">ulong deviation; class=class="str">"cmt">// 偏离的价格步长 HedgeTradeRequest() class=class="str">"cmt">// 省缺参数 { action=REQUEST_CLOSE_POSITION; asynch_mode=false; volume=class="num">0.0; sl = class="num">0.0; tp = class="num">0.0; retcode=class="num">0; deviation=class="num">3; } };
◍ 同步与异步发单的线程分水岭
MT5 给 EA 两套发单函数:OrderSend() 走同步,OrderSendAsync() 走异步。同步函数把请求塞进 MqlTradeRequest 后,会卡在调用线程里等服务器回执,再通过 MqlTradeResult 把结果带回来;基本校验没过就直接返负数,连网都不出。 异步的 OrderSendAsync() 只在终端内部做基础验证,验证一过就把请求甩给主线程和服务器并行跑,自定义线程几乎瞬间拿到返回值。但真实成交结果得在 OnTradeTransaction() 或 OnTrade() 拉起的新线程里捞,没法在 OnTick() 里「发完立马查」。 实测能看清差别:1000 美金账户开 15 手,同步模式 79 毫秒后报 10019(资金不足),说明请求已上服务器;同样 15 手走异步,EA 日志当时显示无错,必须去 OnTradeTransaction() 里才收到 10019。外汇和贵金属杠杆高,这种错误延迟捕获可能让仓位状态判断错位,属高风险操作误区。 别把「零毫秒返回」当真快 OrderSendAsync() 自身耗时小于 1 毫秒,但那是骗人的局部时间。真实执行速度要从 OnTradeTransaction() 收到服务器响应那一刻算,单笔其实和同步差不多。只有在批量发单时才显出优势:高频连发、跨品种同时建仓、或 UI 线程不能卡顿的场景。 构建 1010 之后,事务缓存上限 64 笔,其中 4 笔留给用户,EA 最多同时扔 60 笔异步请求。缓存满了就得等服务器逐笔确认释放。实测均值:50 笔约 50 毫秒,100 笔 180 毫秒,200 笔骤到 2100 毫秒,500 笔 9000 毫秒,1000 笔 23000 毫秒——数量增长让耗时非线性膨胀。开 MT5 跑这段 EA,把 UsingAsynchMode 切 true/false 对比日志就能验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| AsynchExp.mq5 | class=class="str">"cmt">//| Copyright class="num">2014, Vasiliy Sokolov(C-class="num">4). | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2014, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" input class="type">bool UsingAsynchMode=true; class="type">bool sendFlag=false; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if(sendFlag)class="kw">return; printf("订单形成并发送到服务器..."); class="type">MqlTradeRequest request={class="num">0}; request.magic=class="num">12345; request.symbol = Symbol(); request.volume = class="num">0.1;
「异步发单的耗时与报错实况」
在 MT5 里走异步下单,核心是把请求塞进 OrderSendAsync,函数本身只负责把包发到服务器,不阻塞当前 tick。下面这段是实际挂单逻辑里摘出来的关键片段,先看清它怎么测耗时。 request.type=ORDER_TYPE_BUY; request.comment="异步测试"; request.action = TRADE_ACTION_DEAL; request.type_filling=ORDER_FILLING_FOK; MqlTradeResult result; uint tiks= GetTickCount(); bool res = false; if(UsingAsynchMode) res=OrderSendAsync(request,result); else res=OrderSend(request,result); uint delta=GetTickCount()-tiks; 上面逐行拆一下:前四行填的是买价成交、备注、市价成交动作、FOK 填充策略;result 接回执;tiks 记下发单前 tick 数;根据开关走异步或同步;delta 就是发单函数返回所用的毫秒。 日志里能直接读出差异。2014.11.06 17:49:28 那笔成功单,发送交易请求时间 94ms;18:03:58 那笔成功单只用了 0ms,说明异步发送在某些时刻几乎零等待。外汇与贵金属杠杆高,发单延迟波动会直接吃掉滑点空间,需自己跑一遍看本地网络下的分布。 报错也藏着信息。把 volume 设成 DBL_MAX 后,17:54:15 返回 10014(参数无效),耗时 0ms;17:59:22 返回 10019(订单被拒),耗时 78ms。retcode 不零不代表服务器成交,只是本地发单通道的状态。 真正等服务器兜底要在 OnTradeTransaction 里算。回调里再用 GetTickCount()-tiks 打出"服务器应答"耗时,才能把「发出去」和「服务器认了」两段时间分开看,避免把异步发送快当成成交快。
request.type=ORDER_TYPE_BUY; request.comment= "异步测试"; request.action = TRADE_ACTION_DEAL; request.type_filling=ORDER_FILLING_FOK; class="type">MqlTradeResult result; class="type">uint tiks= GetTickCount(); class="type">bool res = false; if(UsingAsynchMode) res=OrderSendAsync(request,result); else res=OrderSend(request,result); class="type">uint delta=GetTickCount()-tiks; if(OrderSendAsync(request,result)) { printf("订单已成功"+ "发送到服务器。"); } else { printf("订单无法发货。"+ " 原因: "+(class="type">class="kw">string)result.retcode); } printf("发送交易请求时间: "+(class="type">class="kw">string)delta); sendFlag=true; class=class="str">"cmt">//--- } class="type">void OnTradeTransaction(const MqlTradeTransaction&trans, const class="type">MqlTradeRequest&request, const class="type">MqlTradeResult&result) { class="type">uint delta = GetTickCount() - tiks; printf("服务器应答: " + (class="type">class="kw">string)result.retcode + "; 时间: " + (class="type">class="kw">string)delta); }
异步下单的日志时间戳拆解
在 AUDCAD 的 H1 周期上跑异步下单,日志能直接暴露请求链路耗时。上面这段记录发生在 2014.11.06 18:17:00,从订单形成到收到服务器应答,总跨度约 92 毫秒(00.851 到 00.943)。 具体看三行关键节点:00.851 订单开始形成并发送,00.854 显示已成功发到服务器且本地发送耗时为 0,00.943 收到服务器应答码 10019,中间等待应答耗时 94 毫秒。 外汇与贵金属交易存在高杠杆与滑点风险,异步模式的应答延迟可能因经纪商服务器负载波动。开 MT5 用 Expert Advisor 跑一单 AUDCAD,把日志级别调成全部,就能复现这套毫秒级时间线。
class="num">2014.11.class="num">06 class="num">18:class="num">17:class="num">00.943 AsynchExp(AUDCAD,H1) 服务器应答: class="num">10019; 时间: class="num">94 class="num">2014.11.class="num">06 class="num">18:class="num">17:class="num">00.854 AsynchExp(AUDCAD,H1) 发送交易请求时间: class="num">0 class="num">2014.11.class="num">06 class="num">18:class="num">17:class="num">00.854 AsynchExp(AUDCAD,H1) 订单已成功发送到服务器。 class="num">2014.11.class="num">06 class="num">18:class="num">17:class="num">00.851 AsynchExp(AUDCAD,H1) 订单形成并发送到服务器...
◍ 用共享文件在多个 EA 之间搬报价
MQL 程序跑在各自终端里,互相拿不到对方内存里的数据。唯一能跨终端通信的通道,是 MetaTrader 共享目录下的文件——一个 EA 把报价写进去,别的终端上的 EA 再读出来。不挂外部 DLL 的话,这就是 MQL5 仅有的手段。 我们做个报价收集器 UnitedExchangeQuotes:假设 6~7 家经纪商都给 EURUSD 报价,每行一家,列出自家 Ask/Bid,再算出自家与当前最优价的差值 D-ASK、D-BID。截图上同一时刻最优 Ask 来自 Alpari Limited,最贵的是 Bank VTB 24,价差肉眼可见。 难点在并发:A 终端的 EA 正写 Quotes.xml,B 终端的 EA 同时想读,结果可能崩,也可能悄悄显示错价。解决办法是把读写锁死成一个原子动作——任何程序在读或写时,其他程序都不能碰这个文件。 下面这段 TryGetHandle() 就是抢文件句柄的核心。它最多试 10 次,每次失败冻 15 毫秒再试,拿到句柄才交给 LoadQuotes() 去解析 XML、刷新报价数组、回写覆盖。整个读写周期通常不到 1 毫秒,比一次慢速交易操作的 80~150 毫秒轻得多。
class="type">int CQuoteList::TryGetHandle(class="type">void) { class="type">int attempts = class="num">10; class="type">int handle = INVALID_HANDLE; class=class="str">"cmt">// 我们尝试打开 &class="macro">#x27;attemps&class="macro">#x27; 次数 for(att = class="num">0; att < attempts; att++) { handle = FileOpen("Quotes.xml", FILE_WRITE|FILE_READ|FILE_BIN|FILE_COMMON); if(handle == INVALID_HANDLE) { Sleep(class="num">15); class="kw">continue; } class="kw">break; } class="kw">return handle; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) class="kw">return; if(!QuotesList.LoadQuotes()) class="kw">return; PrintQuote quote = {class="num">0}; Panel.DrawAccess(QuotesList.CountAccess()); class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
int attempts = 10;定义最大重试次数为 10。int handle = INVALID_HANDLE;先置句柄为无效。for(att = 0; att < attempts; att++)开始重试循环。
| - `handle = FileOpen("Quotes.xml", FILE_WRITE | FILE_READ | FILE_BIN | FILE_COMMON);` 以读写二进制+共享目录方式开文件。 |
|---|
if(handle == INVALID_HANDLE)没抢到就进分支。Sleep(15);冻 15 毫秒避开冲突。continue;回到循环再试。break;抢到了立刻跳出。return handle;把句柄交出去。
OnTick 里先校验账户允许 EA 交易,再 LoadQuotes() 拉数据,失败直接 return,随后取 SYMBOL_ASK 做面板绘制。 演示 EA 跑不满一小时就能刷出 90,000+ 次 Quotes.xml 读写,面板顶部的 I/O Rewrite 计总数、fps 记两次间隔速率、Avrg 记每秒均值。机械盘这么造对寿命不友好,最好丢到 RAM 磁盘。HedgeTerminal 只把全局变量够不到的仓位变化写进 ActivePositions.xml,读写量小得多,普通盘就能扛。外汇与贵金属杠杆高,跨终端套利价差转瞬即逝,实盘前务必在模拟环境验证文件锁逻辑。
class="type">int CQuoteList::TryGetHandle(class="type">void) { class="type">int attempts = class="num">10; class="type">int handle = INVALID_HANDLE; class=class="str">"cmt">// 我们尝试打开 &class="macro">#x27;attemps&class="macro">#x27; 次数 for(att = class="num">0; att < attempts; att++) { handle = FileOpen("Quotes.xml", FILE_WRITE|FILE_READ|FILE_BIN|FILE_COMMON); if(handle == INVALID_HANDLE) { Sleep(class="num">15); class="kw">continue; } class="kw">break; } class="kw">return handle; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) class="kw">return; if(!QuotesList.LoadQuotes()) class="kw">return; PrintQuote quote = {class="num">0}; Panel.DrawAccess(QuotesList.CountAccess()); class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
「跨经纪商报价差怎么抓」
在 MT5 里做跨经纪商比价,核心是先拿到本账户当前品种的买价与经纪商名,再遍历 QuotesList 里的所有经纪商逐个比对。下面这段逻辑直接取本地 SYMBOL_BID 和 ACCOUNT_COMPANY,作为基准锚点。 遍历时若 BrokerSelect 或 SymbolSelect 失败就跳过,避免脏数据进面板。成功选中后,分别取对方经纪商的 QUOTE_ASK / QUOTE_BID,用本地 ask 减对方 ask、对方 bid 减本地 bid,得到双向价差 delta。 价差 delta_ask 与 delta_bid 为正,说明本地在该侧报价更优;为负则对方更优。外汇与贵金属点差随流动性秒级跳动,跨经纪商套利窗口可能瞬间关闭,属高风险操作,实盘前务必在策略测试器用历史 tick 验证。 把算出的 broker_name、index 和价差交给 Panel.DrawBroker 渲染,你就能在自定义面板上直观看到哪家经纪商在哪个方向便宜了零点几个点。
class="type">class="kw">double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); class="type">class="kw">string brokerName = AccountInfoString(ACCOUNT_COMPANY); for(class="type">int i = class="num">0; i < QuotesList.BrokersTotal(); i++) { if(!QuotesList.BrokerSelect(i)) class="kw">continue; if(!QuotesList.SymbolSelect(Symbol())) class="kw">continue; quote.ask = QuotesList.QuoteInfoDouble(QUOTE_ASK); quote.bid = QuotesList.QuoteInfoDouble(QUOTE_BID); quote.delta_ask = ask - quote.ask; quote.delta_bid = quote.bid - bid; quote.broker_name = QuotesList.BrokerName(); quote.index = i; Panel.DrawBroker(quote); }
附件文件怎么摆进 MT5 目录
这套例程的附件不是丢进终端就能用,每个文件都有固定的落点。Prototypes.mqh 只是 HedgeTerminalAPI 的函数原型说明,本身不含逻辑,作用是让 EA 在编译期知道有哪些函数、修饰符和返回值可用。 把它拷到 C:\Program Files\MetaTrader 5\MQL5\Include(前面那段是你的 MT5 安装根目录),之后在代码里写一句包含指令,脚本才认得程序库接口。原型文件会随程序库版本变动,所以更新 hedge 终端库后,最好顺手从附件里覆盖一份新的 Prototypes.mqh,否则编译可能报找不到声明。 Chaos2.mqh 是混沌 II EA 源码,编译前提是两个东西就位:原型文件在 \Include,而 HedgeTerminalAPI 库文件 hedgeterminalapi.ex5 放在 C:\Program Files\MetaTrader 5\MQL5\Market。少一个都会链接失败。 多线程那个 unitedexchangequotes.zip 拆开后有明确分工:UnitedExchangeQuotes.mq5 进 \Experts 编译;MultiThreadXML.mqh 连同 XmlBase / XmlDocument / XmlAttribute / XmlElement 四个子文件进 \Include,它改自 ya-sha 的 XML 库以适配跨线程交换;Panel.mqh 和 EA 主文件同目录,也就是 \Experts。所有路径都是相对终端数据文件夹的,比如 Experts 下的 mq5 就应落在 C:\Program Files\MetaTrader 5\MQL5\Experts\UnitedExchangeQuotes.mq5。 下面这段是引用原型文件的最小脚本骨架,黄底那行就是关键包含指令:
class="macro">#include <Prototypes.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- class=class="str">"cmt">//... class=class="str">"cmt">// 此处是您的程序内容。 class=class="str">"cmt">//... }
◍ 画得少,看得清
HedgeTerminal 把 MT5 原生不直给的双向仓位数据在启动时就算好了,EA 不必自己跑匹配逻辑。比如选中一笔事务后,直接查属性就能拿到该仓位的滑点和内部成交笔数,这类数值在裸 MT5 环境里要写复杂分析才能凑出来。 它的 API 顺序跟 MT4 处理订单几乎一致:先 TransactionSelect 选仓,再读属性或下止损、平仓。面板和 EA 共用一套数据表现,EA 改了仓位,面板立刻反映,反过来也能从面板控 EA。 对外汇和贵金属这类高波动品种,用这套机制做对冲仓管理,能少写不少胶水代码,但杠杆风险仍在,仓位失控一样会放大亏损。