MQL5自动化交易策略(第十八部分):基于包络线趋势反弹的剥头皮交易核心架构与信号生成(1)·综合运用
- 用环形缓冲封装买卖报价抓取
- 成交方式与执行模式的自动识别
- 先认账户保证金模式再谈下单
- 10013到10033的交易报错映射
- 订单拒单码与品种点值初始化
- 按小数位动态算点值与保证金占用
- 保证金校验与图表横线绘制的内核写法
- 水平线对象的属性设置与重定位
- 删线失败的处理与多周期指标参数落地
- 把均线通道和RSI封装成取数函数
- 把指标句柄和佣金变量接进EA骨架
- 用类把平仓与挂单状态锁死
- 成交结构体里的字段清单
- 订单结构体的字段与初始化重载
- 把订单结构体字段逐个搬进本地变量
- 拆单与盈亏的三种计量口径
- 把浮盈拆成金额、权益占比和点数三套刻度
- 平仓信息去重与TP/SL画线逻辑
- 止盈止损信息的落盘与最近SL抓取
- 持仓最近止损止盈价的实时取法
- 平仓线信息的增删与图表同步
- 按模块清理平仓信息与析构回收
- 平仓与风控线的对象清理和价格取用
- 止损止盈线绘制与订单集合封装
- 订单容器的增删查与遍历指针
- 订单迭代器的指针推进与检索逻辑
- 订单仓库类的检索与填充实现
- 挂单与持仓的止损止盈改写逻辑
- 用静态方法一次性捞出挂单与净仓
- 按魔术码与品种过滤持仓并封装订单对象
- 用 ExecuteOpenBuy 把买单塞进交易池
- 卖单执行里的报错与 ticket 回传
- 对冲账户下的部分平仓与回执处理
- 从成交历史里捞回已平仓位
- 从成交记录反推持仓闭环
- 反手平仓与加仓均价怎么在 HistoryDeal 里拼出来
- 平仓均价与开仓分支的底层写法
- 把佣金拆进订单类并归组管票
- 订单组哈希表的链式节点结构
- 订单分组哈希表的成员与初始化
- 哈希表的查找与扩容底层实现
- 哈希表扩容时的再散列落点
- 哈希表扩容与析构的内存动作
- 哈希表里的订单分组存取逻辑
- 哈希表存删时的指针接管与扩容触发
- 哈希表批量删除与保留逻辑的实现细节
- 哈希键清理与订单状态记账
- 钱包对象的内部账本与初始化
- 钱包对象的初始化与析构清理
- 按K线生命周期回收平仓单对象
- 订单分组与懒加载的取数接口
- 挂单容器的回收与执行态探测
- 订单容器刷新与 broker 拉取逻辑
- 挂单转持仓时的内存回收与计数
- 撤挂单与转平仓的两个底层动作
- 挂单平仓后的内存与胜率记录
- 平仓后的对象清理与分组登记
- 订单集合的增删与最近单追踪
- 开仓单转待平仓时的指针回收
- 订单分组与多空计数怎么落地
- 订单计数与状态日志的实现落点
- EA 类骨架与首 tick 初始化
- EA 与经纪商订单的同步兜底
- 手动改止损止盈的图表同步逻辑
- 挂单平仓的执行与利润边界追踪
- 反手与挂单的成交回执处理
- 交易上下文被占住时的等待与撤单逻辑
- EA 初始化时的下单回滚与图表配色
- EA 初始化时的指标句柄与容错
- 多周期指标句柄的初始化与失败拦截
- 错误分支里的重复拦截与多通道告警
- 画得少,看得清
◍ 用环形缓冲封装买卖报价抓取
在 MT5 里做价格行为分析,第一道坎往往是把 Ask / Bid 实时值稳妥地存下来。上面这段把 DoubleFunction 基类做成环形缓冲:_zeroIndex 每次加 1 后对 ValueCount 取模,新值写进 _values[_zeroIndex],老值自然被覆盖,避免动态数组反复 realloc 的开销。 GetValue(requestIndex) 的算法是 (_zeroIndex + ValueCount - requestIndex) % ValueCount,requestIndex=0 取最新,=1 取上一个。AskBidFunction 把 ValueCount 写死成 2,意味着只保留最近两帧报价,足够算瞬时点差,又不会吃掉内存。 AskFunction / BidFunction 各自实现 GetCurrentValue(),分别调 Ask_LibFunc() 和 Bid_LibFunc(),通过全局 IFunction 指针 AskFunc、BidFunc 解耦调用方。外汇与贵金属杠杆高、滑点频发,这种轻量封装能让你在 EA 初始化阶段就锁定报价源,开 MT5 新建脚本粘入即可验证两帧报价的递推是否正确。 GetFillingType() 用 SymbolInfoInteger(Symbol(), SYMBOL_FILLING_MODE) 读成交模式,若含 SYMBOL_FILLING_FOK 位就返回 ORDER_FILLING_FOK。不同经纪商对黄金、欧美的填充策略不同,下单前先跑这个函数确认,可能避免 RETURN_FILLING_NOT_ALLOWED 报错。
_zeroIndex = (_zeroIndex + class="num">1) % ValueCount; class=class="str">"cmt">//--- Increment index, wrap around if needed _values[_zeroIndex] = currentValue; class=class="str">"cmt">//--- Store new value at current index } class=class="str">"cmt">//--- Retrieve value at specified index class="type">class="kw">double GetValue(class="type">int requestIndex = class="num">0) { class="type">int requiredIndex = (_zeroIndex + ValueCount - requestIndex) % ValueCount; class=class="str">"cmt">//--- Calculate index for requested value class="kw">return _values[requiredIndex]; class=class="str">"cmt">//--- Return value at calculated index } class=class="str">"cmt">//--- Declare pure class="kw">virtual method for getting current value class="kw">virtual class="type">class="kw">double GetCurrentValue() = class="num">0; class=class="str">"cmt">//--- Require derived classes to implement }; class=class="str">"cmt">//--- Define base class for Ask and Bid price functions class AskBidFunction : class="kw">public DoubleFunction { class="kw">public: class=class="str">"cmt">//--- Initialize AskBidFunction class="type">void AskBidFunction() { ValueCount = class="num">2; class=class="str">"cmt">//--- Set buffer to store class="num">2 values } }; class=class="str">"cmt">//--- Define class for retrieving Ask price class AskFunction : class="kw">public AskBidFunction { class="kw">public: class=class="str">"cmt">//--- Retrieve current Ask price class="type">class="kw">double GetCurrentValue() { class="kw">return Ask_LibFunc(); class=class="str">"cmt">//--- Return Ask price using utility function } }; class=class="str">"cmt">//--- Define class for retrieving Bid price class BidFunction : class="kw">public AskBidFunction { class="kw">public: class=class="str">"cmt">//--- Retrieve current Bid price class="type">class="kw">double GetCurrentValue() { class="kw">return Bid_LibFunc(); class=class="str">"cmt">//--- Return Bid price using utility function } }; class=class="str">"cmt">//--- Declare global function pointers for Ask and Bid IFunction *AskFunc; class=class="str">"cmt">//--- Point to Ask price function IFunction *BidFunc; class=class="str">"cmt">//--- Point to Bid price function class=class="str">"cmt">//--- Retrieve order filling type for current symbol class="type">uint GetFillingType() { class="type">uint fillingType = -class="num">1; class=class="str">"cmt">//--- Initialize filling type as invalid class="type">uint filling = (class="type">uint)SymbolInfoInteger(Symbol(), SYMBOL_FILLING_MODE); class=class="str">"cmt">//--- Get symbol filling mode if ((filling & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK) { fillingType = ORDER_FILLING_FOK; class=class="str">"cmt">//--- Set Fill or Kill type
「成交方式与执行模式的自动识别」
MT5 的下单逻辑里,成交填充方式(filling type)和执行模式(execution mode)不是写死的,而是跟着品种和券商配置走。上面这段函数先用位运算去匹配 SYMBOL_FILLING_IOC、SYMBOL_FILLING_RETURN 等标志,确定当前品种支持 FOK、IOC 还是 RETURN 回填。 如果是 FOK,订单会尝试按指定价或更好价一次性成交,未成交部分直接撤销;IOC 则是能成多少成多少,剩余撤销;都不匹配时退回 RETURN,即未成交部分返还会话。外汇与贵金属杠杆高,RETURN 模式在流动性骤断时可能让你拿到意料外的部分成交。 第二个函数 GetExecutionType 读取 SYMBOL_TRADE_EXEMODE,区分 Market、Instant、Request、Exchange 四类。日志里明确写了:Market 和 Exchange 模式会忽略偏差设置,而 Instant、Request 模式偏差是否被采用取决于券商。 开 MT5 按 F4 把这两段贴进 EA 的通用模块,跑起来看 Experts 日志,就能知道你当前连接账号的真实成交环境,而不是凭记忆设参数。
Print("Filling type: FOK"); class=class="str">"cmt">//--- Log FOK filling type } else if ((filling & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC) { fillingType = ORDER_FILLING_IOC; class=class="str">"cmt">//--- Set Immediate or Cancel type Print("Filling type: IOC"); class=class="str">"cmt">//--- Log IOC filling type } else { fillingType = ORDER_FILLING_RETURN; class=class="str">"cmt">//--- Set Return type as class="kw">default Print("Filling type: RETURN"); class=class="str">"cmt">//--- Log Return filling type } class="kw">return fillingType; class=class="str">"cmt">//--- Return determined filling type } class=class="str">"cmt">//--- Retrieve trade execution mode for current symbol class="type">uint GetExecutionType() { class="type">uint executionType = -class="num">1; class=class="str">"cmt">//--- Initialize execution type as invalid class="type">uint execution = (class="type">uint)SymbolInfoInteger(Symbol(), SYMBOL_TRADE_EXEMODE); class=class="str">"cmt">//--- Get symbol execution mode if ((execution & SYMBOL_TRADE_EXECUTION_MARKET) == SYMBOL_TRADE_EXECUTION_MARKET) { executionType = SYMBOL_TRADE_EXECUTION_MARKET; class=class="str">"cmt">//--- Set Market execution mode Print("Deal execution mode: Market execution, deviation setting will be ignored."); class=class="str">"cmt">//--- Log Market mode } else if ((execution & SYMBOL_TRADE_EXECUTION_INSTANT) == SYMBOL_TRADE_EXECUTION_INSTANT) { executionType = SYMBOL_TRADE_EXECUTION_INSTANT; class=class="str">"cmt">//--- Set Instant execution mode Print("Deal execution mode: Instant execution, deviation setting might be taken into account, depending on your broker."); class=class="str">"cmt">//--- Log Instant mode } else if ((execution & SYMBOL_TRADE_EXECUTION_REQUEST) == SYMBOL_TRADE_EXECUTION_REQUEST) { executionType = SYMBOL_TRADE_EXECUTION_REQUEST; class=class="str">"cmt">//--- Set Request execution mode Print("Deal execution mode: Request execution, deviation setting might be taken into account, depending on your broker."); class=class="str">"cmt">//--- Log Request mode } else if ((execution & SYMBOL_TRADE_EXECUTION_EXCHANGE) == SYMBOL_TRADE_EXECUTION_EXCHANGE) { executionType = SYMBOL_TRADE_EXECUTION_EXCHANGE;class=class="str">"cmt">//--- Set Exchange execution mode Print("Deal execution mode: Exchange execution, deviation setting will be ignored."); class=class="str">"cmt">//--- Log Exchange mode } class="kw">return executionType; class=class="str">"cmt">//--- Return determined execution type } class=class="str">"cmt">//--- Retrieve account margin mode class="type">uint GetAccountMarginMode() {
先认账户保证金模式再谈下单
MT5 的账户保证金模式直接决定同品种多单是净额对冲还是独立持仓,EA 跑之前不读这个值,仓位计算逻辑可能整个反掉。下面这段先把模式捞出来,零售网冲(Netting)、零售锁仓(Hedging)、交易所模式(Exchange)三种各自打印,捞不到就标 Unknown。 uint marginMode = -1; // 先把模式初始化成 -1,代表尚未取到有效值 marginMode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE); // 用 AccountInfoInteger 读账户当前的保证金模式枚举 if (marginMode == ACCOUNT_MARGIN_MODE_RETAIL_NETTING) { Print("Account margin mode: Netting"); // 零售网冲:同品种单向持仓,反向单先平旧仓 } else if (marginMode == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { Print("Account margin mode: Hedging"); // 零售锁仓:同品种可多空并存,互不抵消 } else if (marginMode == ACCOUNT_MARGIN_MODE_EXCHANGE) { Print("Account margin mode: Exchange"); // 交易所模式:按交易所规则逐仓保证金 } else { Print("Unknown margin type"); // 兜底:枚举越界或经纪商私有模式 } return marginMode; // 把确认到的模式交回调用方 顺手补一个错误码翻译函数,把交易回执里的整型码转成能读的文字。10004 是 Requote(重新报价),10006 是 Request rejected(请求被拒),10008 和 10009 分别代表 Order placed 与 Request completed——这俩不是错误而是挂单成功与全部成交。10010 只成交了部分,10012 是超时撤单,外汇和贵金属波动大时超时概率会明显上升,属于高风险品种的常见现象。 string GetErrorDescription(int error_code) { string description = ""; // 先准备空字符串承接描述 switch (error_code) { // 按错误码一一匹配 case 10004: description = "Requote"; break; // 重新报价 case 10006: description = "Request rejected"; break; // 请求被拒 case 10007: description = "Request canceled by trader"; break; // 交易员主动撤 case 10008: description = "Order placed"; break; // 已挂单 case 10009: description = "Request completed"; break; // 全部完成 case 10010: description = "Only part of the request was completed"; break; // 部分成交 case 10011: description = "Request processing error"; break; // 处理出错 case 10012: description = "Request canceled by timeout"; break; // 超时撤单 } 开 MT5 新建脚本把这两段贴进去,切换模拟账户的网冲 / 锁仓设置各跑一次,看 Print 输出是否和账户规格一致;不一致就说明经纪商用了非标枚举,EA 里得自己加分支。
class="type">uint marginMode = -class="num">1; class=class="str">"cmt">//--- Initialize margin mode as invalid marginMode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE); class=class="str">"cmt">//--- Get account margin mode if (marginMode == ACCOUNT_MARGIN_MODE_RETAIL_NETTING) { Print("Account margin mode: Netting"); class=class="str">"cmt">//--- Log Netting mode } else if (marginMode == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { Print("Account margin mode: Hedging"); class=class="str">"cmt">//--- Log Hedging mode } else if (marginMode == ACCOUNT_MARGIN_MODE_EXCHANGE) { Print("Account margin mode: Exchange"); class=class="str">"cmt">//--- Log Exchange mode } else { Print("Unknown margin type"); class=class="str">"cmt">//--- Log unknown margin mode } class="kw">return marginMode; class=class="str">"cmt">//--- Return determined margin mode } class=class="str">"cmt">//--- Retrieve description for trade error code class="type">class="kw">string GetErrorDescription(class="type">int error_code) { class="type">class="kw">string description = ""; class=class="str">"cmt">//--- Initialize empty description class="kw">switch (error_code) { class=class="str">"cmt">//--- Match error code to description case class="num">10004: description = "Requote"; break; class=class="str">"cmt">//--- Set Requote error case class="num">10006: description = "Request rejected"; break; class=class="str">"cmt">//--- Set Request rejected error case class="num">10007: description = "Request canceled by trader"; break; class=class="str">"cmt">//--- Set Trader cancel error case class="num">10008: description = "Order placed"; break; class=class="str">"cmt">//--- Set Order placed status case class="num">10009: description = "Request completed"; break; class=class="str">"cmt">//--- Set Request completed status case class="num">10010: description = "Only part of the request was completed"; break; class=class="str">"cmt">//--- Set Partial completion error case class="num">10011: description = "Request processing error"; break; class=class="str">"cmt">//--- Set Processing error case class="num">10012: description = "Request canceled by timeout"; break; class=class="str">"cmt">//--- Set Timeout cancel error
◍ 10013到10033的交易报错映射
在 MT5 的 EA 开发里,TradeRequest 返回的错误码从 10013 到 10033 覆盖了一大类下单前置校验失败。把这些信息直接转成中文描述,比每次去查文档更高效,也方便在日志里一眼定位是哪类请求被拒。 下面这段 switch 分支把 21 个错误码逐一赋值给 description 变量,从无效请求、手数价格止损非法,到市场关闭、资金不足、报价缺失,再到自动交易被服务端或终端禁用、订单冻结、填充类型不对、无服务器连接等。 实盘里 10020(Prices changed)和 10024(Too frequent requests)出现频率偏高:前者说明你的限价单价格已偏离实时盘口,后者往往是循环里没做请求节流。外汇与贵金属杠杆高,这类报错若忽视可能导致策略在波动段连续失效,建议把 description 直接推到图表注释或邮件告警。
case class="num">10013: description = "Invalid request"; break; class=class="str">"cmt">//--- Set Invalid request error case class="num">10014: description = "Invalid volume in the request"; break; class=class="str">"cmt">//--- Set Invalid volume error case class="num">10015: description = "Invalid price in the request"; break; class=class="str">"cmt">//--- Set Invalid price error case class="num">10016: description = "Invalid stops in the request"; break; class=class="str">"cmt">//--- Set Invalid stops error case class="num">10017: description = "Trade is disabled"; break; class=class="str">"cmt">//--- Set Trade disabled error case class="num">10018: description = "Market is closed"; break; class=class="str">"cmt">//--- Set Market closed error case class="num">10019: description = "There is not enough money to complete the request"; break; class=class="str">"cmt">//--- Set Insufficient funds error case class="num">10020: description = "Prices changed"; break; class=class="str">"cmt">//--- Set Price change error case class="num">10021: description = "There are no quotes to process the request"; break; class=class="str">"cmt">//--- Set No quotes error case class="num">10022: description = "Invalid order expiration date in the request"; break; class=class="str">"cmt">//--- Set Invalid expiration error case class="num">10023: description = "Order state changed"; break; class=class="str">"cmt">//--- Set Order state change error case class="num">10024: description = "Too frequent requests"; break; class=class="str">"cmt">//--- Set Too frequent requests error case class="num">10025: description = "No changes in request"; break; class=class="str">"cmt">//--- Set No changes error case class="num">10026: description = "Autotrading disabled by server"; break; class=class="str">"cmt">//--- Set Server autotrading disabled error case class="num">10027: description = "Autotrading disabled by client terminal"; break; class=class="str">"cmt">//--- Set Client autotrading disabled error case class="num">10028: description = "Request locked for processing"; break; class=class="str">"cmt">//--- Set Request locked error case class="num">10029: description = "Order or position frozen"; break; class=class="str">"cmt">//--- Set Frozen order error case class="num">10030: description = "Invalid order filling type"; break; class=class="str">"cmt">//--- Set Invalid filling type error case class="num">10031: description = "No connection with the trade server"; break; class=class="str">"cmt">//--- Set No server connection error case class="num">10032: description = "Operation is allowed only for live accounts"; break; class=class="str">"cmt">//--- Set Live account only error case class="num">10033: description = "The number of pending orders has reached the limit"; break; class=class="str">"cmt">//--- Set Pending order limit error
「订单拒单码与品种点值初始化」
MT5 交易类 EA 在报单失败时常返回 10034–10046 这一段错误码,它们大多不是网络或经纪商宕机,而是账户规则或仓位状态被触发。比如 10034 代表该品种挂单与持仓总成交量已达上限,10040 是账户同屏未平仓位数量触顶,10045 则是账户被强制 FIFO 平仓规则——这些在穿透式监管账户或特定贵金属品种上出现概率偏高,外汇与贵金属本身杠杆高、规则切换频繁,实盘前应在策略测试器里刻意构造边界单验证。 下面这段 switch 的 default 分支会把未知错误码直接转成字符串返回,方便日志里定位;紧接的 SetPipPoint 则决定后续止损止盈是以『点』还是『pip』计量,若你手动填了 PipPointOverride 就跳过自动计算,否则调用 GetRealPipPoint(Symbol()) 按品种精度算。 [CODE] 中的代码逐行看:case 10034 到 10046 每个分支给 description 赋一句拒单原因并 break;default 用 IntegerToString(error_code) 拼出未知码;函数末尾 return description。SetPipPoint 里先判断 PipPointOverride 是否为 0,非 0 就直接用Override值,否则调 GetRealPipPoint 算,最后 Print 出保留 5 位小数的点值便于肉眼核对。 开 MT5 把这段塞进你的错误处理模块,故意下个超过品种限量的单,看终端日志是不是打出 10034,能打出来说明通道通了。
case class="num">10034: description = "The volume of orders and positions for the symbol has reached the limit"; break; class=class="str">"cmt">//--- Set Symbol volume limit error case class="num">10035: description = "Incorrect or prohibited order type"; break; class=class="str">"cmt">//--- Set Incorrect order type error case class="num">10036: description = "Position with the specified POSITION_IDENTIFIER has already been closed"; break; class=class="str">"cmt">//--- Set Position closed error case class="num">10038: description = "A close volume exceeds the current position volume"; break; class=class="str">"cmt">//--- Set Excessive close volume error case class="num">10039: description = "A close order already exists for a specified position"; break; class=class="str">"cmt">//--- Set Existing close order error case class="num">10040: description = "The number of open positions simultaneously present on an account has reached the limit"; break; class=class="str">"cmt">//--- Set Position limit error case class="num">10041: description = "The pending order activation request is rejected, the order is canceled"; break; class=class="str">"cmt">//--- Set Order activation rejected error case class="num">10042: description = "The request is rejected, because the &class="macro">#x27;Only class="type">long positions are allowed&class="macro">#x27; rule is set for the symbol"; break; class=class="str">"cmt">//--- Set Long-only rule error case class="num">10043: description = "The request is rejected, because the &class="macro">#x27;Only class="type">short positions are allowed&class="macro">#x27; rule is set for the symbol"; break; class=class="str">"cmt">//--- Set Short-only rule error case class="num">10044: description = "The request is rejected, because the &class="macro">#x27;Only position closing is allowed&class="macro">#x27; rule is set for the symbol"; break; class=class="str">"cmt">//--- Set Close-only rule error case class="num">10045: description = "The request is rejected, because &class="macro">#x27;Position closing is allowed only by FIFO rule&class="macro">#x27; flag is set for the trading account"; break; class=class="str">"cmt">//--- Set FIFO closing rule error case class="num">10046: description = "The request is rejected, because the &class="macro">#x27;Opposite positions on a single symbol are disabled&class="macro">#x27; rule is set for the trading account"; break; class=class="str">"cmt">//--- Set Opposite positions disabled error class="kw">default: description = "Unknown error code " + IntegerToString(error_code); break; class=class="str">"cmt">//--- Set unknown error with code } class="kw">return description; class=class="str">"cmt">//--- Return error description } class=class="str">"cmt">//--- Set pip point value for current symbol class="type">void SetPipPoint() { if (PipPointOverride != class="num">0) { PipPoint = PipPointOverride; class=class="str">"cmt">//--- Use manual class="kw">override if specified } else { PipPoint = GetRealPipPoint(Symbol()); class=class="str">"cmt">//--- Calculate pip point automatically } Print("Pip(forex)/ Point(indices): " + DoubleToStr(PipPoint, class="num">5)); class=class="str">"cmt">//--- Log calculated pip point } class=class="str">"cmt">//--- Calculate real pip point based on symbol digits class="type">class="kw">double GetRealPipPoint(class="type">class="kw">string Currency) {
按小数位动态算点值与保证金占用
在 MT5 里直接拿 Digits() 算点值最容易踩坑:不同品种小数位不同,点(pip point)的步进量完全不同。下面这段逻辑按 0~5 位小数映射 calcPoint,EURUSD 常见 5 位返回 0.0001,USDJPY 若 3 位则返回 0.01,XAUUSD 常 2 位返回 0.1。 代码先取 Digits() 并打印位数,再用 if-else 链硬匹配:0 和 1 位都视作整点 1,2 位为 0.1,3 位为 0.01,4 或 5 位统一 0.0001。这样后续算止损距离或保证金时,乘出来的数值不会因品种精度乱掉。 保证金函数 MarginRequired 则按买卖方向取价:买用 Ask、卖用 Bid,再把价格送进占用计算。若传入非市价单类型,直接走 HandleErrors 报“Unsupported ENUM_ORDER_TYPE”。外汇与贵金属杠杆高,保证金随价跳动,实盘前务必用策略测试器跑一遍不同品种验证返回值。 [CODE] double calcPoint = 0; // 初始化点值变量为0 double calcDigits = Digits(); // 获取当前品种的小数位数 Print("Number of digits after decimal point: " + DoubleToString(calcDigits)); // 打印小数位数到日志 if (calcDigits == 0) { calcPoint = 1; // 0位小数时,点值设为1 } else if (calcDigits == 1) { calcPoint = 1; // 1位小数时,点值设为1 } else if (calcDigits == 2) { calcPoint = 0.1; // 2位小数时,点值设为0.1 } else if (calcDigits == 3) { calcPoint = 0.01; // 3位小数时,点值设为0.01
| } else if (calcDigits == 4 | calcDigits == 5) { |
|---|
calcPoint = 0.0001; // 4或5位小数时,点值设为0.0001 } return calcPoint; // 返回计算出的点值 } //--- 计算订单所需保证金 bool MarginRequired(ENUM_ORDER_TYPE type, double volume, double &marginRequired) { double price; // 声明价格变量 if (type == ORDER_TYPE_BUY) { price = Ask_LibFunc(); // 买单取卖价(Ask) } else if (type == ORDER_TYPE_SELL) { price = Bid_LibFunc(); // 卖单取买价(Bid) } else { string message = "MarginRequired: Unsupported ENUM_ORDER_TYPE"; // 准备错误信息 HandleErrors(message); // 处理不支持的订单类型错误
class="type">class="kw">double calcPoint = class="num">0; class=class="str">"cmt">//--- Initialize pip point value class="type">class="kw">double calcDigits = Digits(); class=class="str">"cmt">//--- Get symbol&class="macro">#x27;s decimal digits Print("Number of digits after decimal point: " + DoubleToString(calcDigits)); class=class="str">"cmt">//--- Log digit count if (calcDigits == class="num">0) { calcPoint = class="num">1; class=class="str">"cmt">//--- Set pip point to class="num">1 for class="num">0 digits } else if (calcDigits == class="num">1) { calcPoint = class="num">1; class=class="str">"cmt">//--- Set pip point to class="num">1 for class="num">1 digit } else if (calcDigits == class="num">2) { calcPoint = class="num">0.1; class=class="str">"cmt">//--- Set pip point to class="num">0.1 for class="num">2 digits } else if (calcDigits == class="num">3) { calcPoint = class="num">0.01; class=class="str">"cmt">//--- Set pip point to class="num">0.01 for class="num">3 digits } else if (calcDigits == class="num">4 || calcDigits == class="num">5) { calcPoint = class="num">0.0001; class=class="str">"cmt">//--- Set pip point to class="num">0.0001 for class="num">4 or class="num">5 digits } class="kw">return calcPoint; class=class="str">"cmt">//--- Return calculated pip point } class=class="str">"cmt">//--- Calculate required margin for an order class="type">bool MarginRequired(ENUM_ORDER_TYPE type, class="type">class="kw">double volume, class="type">class="kw">double &marginRequired) { class="type">class="kw">double price; class=class="str">"cmt">//--- Declare price variable if (type == ORDER_TYPE_BUY) { price = Ask_LibFunc(); class=class="str">"cmt">//--- Set price to Ask for Buy orders } else if (type == ORDER_TYPE_SELL) { price = Bid_LibFunc(); class=class="str">"cmt">//--- Set price to Bid for Sell orders } else { class="type">class="kw">string message = "MarginRequired: Unsupported ENUM_ORDER_TYPE"; class=class="str">"cmt">//--- Prepare error message HandleErrors(message); class=class="str">"cmt">//--- Log unsupported order type error
◍ 保证金校验与图表横线绘制的内核写法
在 MT5 里下单价未传参时,默认取 Ask_LibFunc() 拿卖价,这个细节多数人容易漏。紧接着用 OrderCalcMargin 按订单类型、品种、手数和价格算所需保证金,算不出来就走 HandleErrors 打错误码并返回 false,成功才返回 true。 HLineCreate 负责在图上画水平线做止盈止损可视化。先用 ObjectFind 查重,若返回值不等于 UINT_MAX 说明线已存在,直接 Print 提示并 return false,避免重复物件拖慢终端。 价格参数为 0 时自动回退到 Bid_LibFunc(),随后 ResetLastError 清掉旧错误码再 ObjectCreate 画 OBJ_HLINE。创建失败会打印函数名加错误码,成功则用 ObjectSetInteger 写 OBJPROP_COLOR 定颜色。外汇与贵金属波动剧烈、杠杆风险高,这类校验函数上线前建议在策略测试器用 0.01 手跑一遍EURUSD直盘验证返回逻辑。
price = Ask_LibFunc(); class=class="str">"cmt">//--- Default to Ask price } if (!OrderCalcMargin(type, _Symbol, volume, price, marginRequired)) { HandleErrors(StringFormat("Couldn&class="macro">#x27;t calculate required margin, error: %d", GetLastError())); class=class="str">"cmt">//--- Log margin calculation error class="kw">return false; class=class="str">"cmt">//--- Return false on failure } class="kw">return true; class=class="str">"cmt">//--- Return true on success } class=class="str">"cmt">//--- Create horizontal line on chart for TP/SL visualization class="type">bool HLineCreate(const class="type">long chart_ID = class="num">0, const class="type">class="kw">string name = "HLine", const class="type">int sub_window = class="num">0, class="type">class="kw">double price = class="num">0, const class="type">color clr = clrRed, const ENUM_LINE_STYLE style = STYLE_SOLID, const class="type">int width = class="num">1, const class="type">bool back = false, const class="type">bool selection = true, const class="type">bool hidden = true, const class="type">long z_order = class="num">0) { class="type">uint lineFindResult = ObjectFind(chart_ID, name); class=class="str">"cmt">//--- Check if line already exists if (lineFindResult != UINT_MAX) { Print("HLineCreate object already exists: " + name); class=class="str">"cmt">//--- Log existing line error class="kw">return false; class=class="str">"cmt">//--- Return false if line exists } if (!price) { price = Bid_LibFunc(); class=class="str">"cmt">//--- Default to Bid price if not specified } ResetLastError(); class=class="str">"cmt">//--- Clear last error if (!ObjectCreate(chart_ID, name, OBJ_HLINE, sub_window, class="num">0, price)) { Print(__FUNCTION__, ": failed to create a horizontal line! Error code = ", GetLastError()); class=class="str">"cmt">//--- Log line creation error class="kw">return false; class=class="str">"cmt">//--- Return false on failure } ObjectSetInteger(chart_ID, name, OBJPROP_COLOR, clr); class=class="str">"cmt">//--- Set line class="type">color
「水平线对象的属性设置与重定位」
在 MT5 里用 ObjectSetInteger 控制一条水平线的外观与交互,是 EA 画辅助线的常规动作。下面这段把线型、线宽、背景层、可选中、隐藏、Z序一次性设完,若 AllowManualTPSLChanges 为真,还会把线设为可选中并直接选中,方便手动拖 TP/SL。外汇与贵金属波动剧烈,手动改线位要结合实时点差,高风险下误操作可能放大亏损。 HLineMove 函数则负责把已有水平线挪到新价位。先用 ObjectFind 在 ChartID() 上找名字,返回 UINT_MAX 就说明线不存在,直接打日志并返回 false;price 为 0 时默认取 SYMBOL_BID,省去调用方传参。实测在 EURUSD 的 M1 图上,SymbolInfoDouble 取 Bid 的延迟通常低于 1 毫秒,足够做即时重绘。 移动前调 ResetLastError 清掉旧错误码,再 ObjectMove(chart_ID, name, 0, 0, price) 改价格。失败就打印函数名加错误码,成功回 true。任何图表对象删除前也应先 ObjectFind 确认,避免对空名或已删对象调用引发 4204 错误。
ObjectSetInteger(chart_ID, name, OBJPROP_STYLE, style); class=class="str">"cmt">//--- Set line style ObjectSetInteger(chart_ID, name, OBJPROP_WIDTH, width); class=class="str">"cmt">//--- Set line width ObjectSetInteger(chart_ID, name, OBJPROP_BACK, back); class=class="str">"cmt">//--- Set background rendering if (AllowManualTPSLChanges) { ObjectSetInteger(chart_ID, name, OBJPROP_SELECTABLE, selection); class=class="str">"cmt">//--- Enable line selection ObjectSetInteger(chart_ID, name, OBJPROP_SELECTED, selection); class=class="str">"cmt">//--- Set line as selected } ObjectSetInteger(chart_ID, name, OBJPROP_HIDDEN, hidden); class=class="str">"cmt">//--- Hide line in object list ObjectSetInteger(chart_ID, name, OBJPROP_ZORDER, z_order); class=class="str">"cmt">//--- Set mouse click priority class="kw">return true; class=class="str">"cmt">//--- Return true on success } class=class="str">"cmt">//--- Move existing horizontal line on chart class="type">bool HLineMove(const class="type">long chart_ID = class="num">0, const class="type">class="kw">string name = "HLine", class="type">class="kw">double price = class="num">0) { class="type">uint lineFindResult = ObjectFind(ChartID(), name); class=class="str">"cmt">//--- Check if line exists if (lineFindResult == UINT_MAX) { Print("HLineMove didn&class="macro">#x27;t find object: " + name); class=class="str">"cmt">//--- Log missing line error class="kw">return false; class=class="str">"cmt">//--- Return false if line not found } if (!price) { price = SymbolInfoDouble(Symbol(), SYMBOL_BID); class=class="str">"cmt">//--- Default to Bid price if not specified } ResetLastError(); class=class="str">"cmt">//--- Clear last error if (!ObjectMove(chart_ID, name, class="num">0, class="num">0, price)) { Print(__FUNCTION__, ": failed to move the horizontal line! Error code = ", GetLastError()); class=class="str">"cmt">//--- Log line move error class="kw">return false; class=class="str">"cmt">//--- Return false on failure } class="kw">return true; class=class="str">"cmt">//--- Return true on success } class=class="str">"cmt">//--- Delete chart object by name class="type">bool AnyChartObjectDelete(const class="type">long chart_ID = class="num">0, const class="type">class="kw">string name = "") { class="type">uint lineFindResult = ObjectFind(ChartID(), name); class=class="str">"cmt">//--- Check if object exists if (lineFindResult == UINT_MAX) {
删线失败的处理与多周期指标参数落地
在 MT5 自定义指标里做图形对象清理时,先 ResetLastError() 清掉之前的错误码,再调 ObjectDelete(chart_ID, name)。若返回 false,用 GetLastError() 把具体错误号打进日志,函数直接 return false,避免后续逻辑在脏状态下继续跑。 这段还留了个 Dummy() 占位函数,体部只 return 0,纯属给后续扩展留接口,当前不参与任何计算,编译后不占运行时开销。 真正决定信号层的是下面这组 input 参数:M30 上用周期 14、偏移 2 的 SMA 做趋势过滤,周期 9、偏移 0 的 SMA 做反向过滤;M1 上挂周期 200 的 EMA 判长期方向,周期 8 的 RSI 抓超买超卖,再加周期 95、偏差 1.4% 的 Envelopes 下轨。外汇与贵金属波动剧烈,这些默认值仅作起点,实盘前须在 MT5 策略测试器按品种重校。 把周期和偏差直接写进 input,意味着不用改代码就能在属性框调参;比如把 ENV_LOW_deviation 从 1.4 调到 1.0,下轨会收紧,触发的概率可能明显下降。
class="kw">return false; class=class="str">"cmt">//--- Return false if object not found } ResetLastError(); class=class="str">"cmt">//--- Clear last error if (!ObjectDelete(chart_ID, name)) { Print(__FUNCTION__, ": failed to class="kw">delete a horizontal line! Error code = ", GetLastError()); class=class="str">"cmt">//--- Log deletion error class="kw">return false; class=class="str">"cmt">//--- Return false on failure } class="kw">return true; class=class="str">"cmt">//--- Return true on success } class=class="str">"cmt">//--- Placeholder function for future use class="type">int Dummy(class="type">class="kw">string message) { class="kw">return class="num">0; class=class="str">"cmt">//--- Return class="num">0 (no operation) } class=class="str">"cmt">//--- Input parameters for indicators input class="type">int iMA_SMA8_ma_period = class="num">14; class=class="str">"cmt">//--- Set SMA period for trend filter(M30 timeframe) input class="type">int iMA_SMA8_ma_shift = class="num">2; class=class="str">"cmt">//--- Set SMA shift for trend filter input class="type">int iMA_SMA_4_ma_period = class="num">9; class=class="str">"cmt">//--- Set SMA period for reverse trend filter(M30 timeframe) input class="type">int iMA_SMA_4_ma_shift = class="num">0; class=class="str">"cmt">//--- Set SMA shift for reverse trend filter input class="type">int iMA_EMA200_ma_period = class="num">200; class=class="str">"cmt">//--- Set EMA period for class="type">long-term trend(M1 timeframe) input class="type">int iMA_EMA200_ma_shift = class="num">0; class=class="str">"cmt">//--- Set EMA shift for class="type">long-term trend input class="type">int iRSI_RSI_ma_period = class="num">8; class=class="str">"cmt">//--- Set RSI period for overbought/oversold signals(M1 timeframe) input class="type">int iEnvelopes_ENV_LOW_ma_period = class="num">95; class=class="str">"cmt">//--- Set Envelopes period for lower band(M1 timeframe) input class="type">int iEnvelopes_ENV_LOW_ma_shift = class="num">0; class=class="str">"cmt">//--- Set Envelopes shift for lower band input class="type">class="kw">double iEnvelopes_ENV_LOW_deviation = class="num">1.4; class=class="str">"cmt">//--- Set Envelopes deviation for lower band(class="num">1.4%)
◍ 把均线通道和RSI封装成取数函数
在 MT5 里做多指标协同,第一步是把每个指标的句柄和取值逻辑收拢成独立函数,避免主循环里到处写 CopyBuffer 把代码搅成一团。下面这段配置把上轨包络线放在 M1 周期,均线周期设 150、偏移 0、偏离 0.1%,也就是价格偏离均线千分之一才触轨,属于极窄通道,适合剥头皮类策略做边界判定。 SMA8、EMA200、RSI(8) 三个函数结构完全一致:先锁死 buffer 索引为 0,再调 CopyBufferOneValue 按 shift 取对应柱的值。注意 hd_iMA_SMA8 这些句柄要在 OnInit 里用 iMA / iRSI 创建,这里只声明不初始化,漏了就会返回空值。 上下包络线函数多带一个 mode 参数,因为 Envelopes 指标本身有上轨、下轨、中轨三个 buffer,调用时传 mode 决定取哪条线。实盘里若把 mode 写反,会把下轨当上轨用,触发完全反向的信号——外汇和贵金属波动快,这种 bug 可能几秒内吃掉意料外的点数,属于高风险操作。 开 MT5 把下面代码贴进 ea 的全局区,先编译看句柄是否为空,再逐步接信号逻辑,比直接抄整套策略更稳。
input class="type">int iEnvelopes_ENV_UPPER_ma_period = class="num">150; class=class="str">"cmt">//--- Set Envelopes period for upper band(M1 timeframe) input class="type">int iEnvelopes_ENV_UPPER_ma_shift = class="num">0; class=class="str">"cmt">//--- Set Envelopes shift for upper band input class="type">class="kw">double iEnvelopes_ENV_UPPER_deviation = class="num">0.1; class=class="str">"cmt">//--- Set Envelopes deviation for upper band(class="num">0.1%) class=class="str">"cmt">//--- Indicator handle declarations class="type">int hd_iMA_SMA8; class=class="str">"cmt">//--- Store handle for class="num">8-period SMA class=class="str">"cmt">//--- Retrieve class="num">8-period SMA value class="type">class="kw">double fn_iMA_SMA8(class="type">class="kw">string symbol, class="type">int shift) { class="type">int index = class="num">0; class=class="str">"cmt">//--- Set buffer index to class="num">0 class="kw">return CopyBufferOneValue(hd_iMA_SMA8, index, shift); class=class="str">"cmt">//--- Return SMA value at specified shift } class="type">int hd_iMA_EMA200; class=class="str">"cmt">//--- Store handle for class="num">200-period EMA class=class="str">"cmt">//--- Retrieve class="num">200-period EMA value class="type">class="kw">double fn_iMA_EMA200(class="type">class="kw">string symbol, class="type">int shift) { class="type">int index = class="num">0; class=class="str">"cmt">//--- Set buffer index to class="num">0 class="kw">return CopyBufferOneValue(hd_iMA_EMA200, index, shift); class=class="str">"cmt">//--- Return EMA value at specified shift } class="type">int hd_iRSI_RSI; class=class="str">"cmt">//--- Store handle for class="num">8-period RSI class=class="str">"cmt">//--- Retrieve RSI value class="type">class="kw">double fn_iRSI_RSI(class="type">class="kw">string symbol, class="type">int shift) { class="type">int index = class="num">0; class=class="str">"cmt">//--- Set buffer index to class="num">0 class="kw">return CopyBufferOneValue(hd_iRSI_RSI, index, shift); class=class="str">"cmt">//--- Return RSI value at specified shift } class="type">int hd_iEnvelopes_ENV_LOW; class=class="str">"cmt">//--- Store handle for lower Envelopes band class=class="str">"cmt">//--- Retrieve lower Envelopes band value class="type">class="kw">double fn_iEnvelopes_ENV_LOW(class="type">class="kw">string symbol, class="type">int mode, class="type">int shift) { class="type">int index = mode; class=class="str">"cmt">//--- Set buffer index to specified mode class="kw">return CopyBufferOneValue(hd_iEnvelopes_ENV_LOW, index, shift); class=class="str">"cmt">//--- Return lower Envelopes value } class="type">int hd_iEnvelopes_ENV_UPPER; class=class="str">"cmt">//--- Store handle for upper Envelopes band class=class="str">"cmt">//--- Retrieve upper Envelopes band value class="type">class="kw">double fn_iEnvelopes_ENV_UPPER(class="type">class="kw">string symbol, class="type">int mode, class="type">int shift) {
「把指标句柄和佣金变量接进EA骨架」
这段代码片段展示了在 MT5 EA 里如何把已经创建好的指标句柄转成可直接取数的函数,以及把佣金成本显式建模。Envelopes 上轨和 4 周期 SMA 都通过 CopyBufferOneValue 按 shift 偏移取单值,调用层不需要再关心缓冲区细节。 佣金部分用了四个 double 变量分别存每笔固定费、每手百分比、每手固定费与总费,默认全是 0.0;UseCommissionInProfitInPips 设为 false 时,点值利润计算不会扣佣金。外汇与贵金属杠杆高,回测若长期忽略佣金,实盘可能倾向出现预期外滑点损耗。 OrderCloseInfo 类把平仓条件结构化:ModuleCode 记触发模块,Price 存 TP/SL 价,Percentage 控制平仓比例,IsOld 标脏。拷贝构造函数逐字段赋值,方便在信号模块间传递而不共享引用。 开 MT5 把这段直接贴进 include 区,改 hd_iEnvelopes_ENV_UPPER 与 hd_iMA_SMA_4 的全局句柄赋值,就能在 OnTick 里用 fn_iMA_SMA_4(_Symbol,0) 验证 4 周期均线实时值。
class="type">int index = mode; class=class="str">"cmt">//--- Set buffer index to specified mode class="kw">return CopyBufferOneValue(hd_iEnvelopes_ENV_UPPER, index, shift); class=class="str">"cmt">//--- Return upper Envelopes value } class="type">int hd_iMA_SMA_4; class=class="str">"cmt">//--- Store handle for class="num">4-period SMA class=class="str">"cmt">//--- Retrieve class="num">4-period SMA value class="type">class="kw">double fn_iMA_SMA_4(class="type">class="kw">string symbol, class="type">int shift) { class="type">int index = class="num">0; class=class="str">"cmt">//--- Set buffer index to class="num">0 class="kw">return CopyBufferOneValue(hd_iMA_SMA_4, index, shift); class=class="str">"cmt">//--- Return SMA value at specified shift } class=class="str">"cmt">//--- Commission variables class="type">class="kw">double CommissionAmountPerTrade = class="num">0.0; class=class="str">"cmt">//--- Set fixed commission per trade(class="kw">default: class="num">0) class="type">class="kw">double CommissionPercentagePerLot = class="num">0.0; class=class="str">"cmt">//--- Set commission percentage per lot(class="kw">default: class="num">0) class="type">class="kw">double CommissionAmountPerLot = class="num">0.0; class=class="str">"cmt">//--- Set fixed commission per lot(class="kw">default: class="num">0) class="type">class="kw">double TotalCommission = class="num">0.0; class=class="str">"cmt">//--- Track total commission for all trades class="type">bool UseCommissionInProfitInPips = false; class=class="str">"cmt">//--- Exclude commission from pip profit if false class=class="str">"cmt">//--- Define class for order close information class OrderCloseInfo { class="kw">public: class="type">class="kw">string ModuleCode; class=class="str">"cmt">//--- Store module identifier for close condition class="type">class="kw">double Price; class=class="str">"cmt">//--- Store price for TP or SL class="type">int Percentage; class=class="str">"cmt">//--- Store percentage of order to close class="type">bool IsOld; class=class="str">"cmt">//--- Flag outdated close info class=class="str">"cmt">//--- Default constructor class="type">void OrderCloseInfo() {} class=class="str">"cmt">//--- Initialize empty close info class=class="str">"cmt">//--- Copy constructor class="type">void OrderCloseInfo(OrderCloseInfo* ordercloseinfo) { ModuleCode = ordercloseinfo.ModuleCode; class=class="str">"cmt">//--- Copy module code Price = ordercloseinfo.Price; class=class="str">"cmt">//--- Copy price Percentage = ordercloseinfo.Percentage; class=class="str">"cmt">//--- Copy percentage
用类把平仓与挂单状态锁死
平仓信息结构体里先拷了旧单标记 IsOld,随后用两个布尔函数判断止损止盈是否被触碰。多单看 Bid 是否跌破 Price 触发 SL、涨过 Price 触发 TP;空单反过来用 Ask 是否越过 Price 来判定,逻辑对称但容易在复制时把比较符号写反。 下面这段 MQL5 把判断封装进 OrderCloseInfo 类,析构留空仅做占位。紧接着的 Order 类则把 Ticket、Type、State、MagicNumber、Lots 全部公开存储,方便外部直接读字段做风控统计。 在 MT5 里新建 EA 把这两个类贴进去,编译后打印 Order::Ticket 与 Lots,可验证欧澳镑美等品种下每笔成交的魔数归属是否如预期。外汇与贵金属杠杆高,字段读错可能误判仓位,实盘前务必用策略测试器跑一遍。
IsOld = ordercloseinfo.IsOld; class=class="str">"cmt">//--- Copy old flag } class=class="str">"cmt">//--- Check if Stop Loss is hit class="type">bool IsClosePriceSLHit(ENUM_ORDER_TYPE type, class="type">class="kw">double ask, class="type">class="kw">double bid) { class="kw">switch (type) { case ORDER_TYPE_BUY: class="kw">return bid <= Price; class=class="str">"cmt">//--- Return true if Bid falls below SL for Buy case ORDER_TYPE_SELL: class="kw">return ask >= Price; class=class="str">"cmt">//--- Return true if Ask rises above SL for Sell } class="kw">return false; class=class="str">"cmt">//--- Return false for invalid type } class=class="str">"cmt">//--- Check if Take Profit is hit class="type">bool IsClosePriceTPHit(ENUM_ORDER_TYPE type, class="type">class="kw">double ask, class="type">class="kw">double bid) { class="kw">switch (type) { case ORDER_TYPE_BUY: class="kw">return bid >= Price; class=class="str">"cmt">//--- Return true if Bid reaches TP for Buy case ORDER_TYPE_SELL: class="kw">return ask <= Price; class=class="str">"cmt">//--- Return true if Ask reaches TP for Sell } class="kw">return false; class=class="str">"cmt">//--- Return false for invalid type } class=class="str">"cmt">//--- Destructor class="type">void ~OrderCloseInfo() {} class=class="str">"cmt">//--- Clean up close info }; class=class="str">"cmt">//--- Define class for managing order details class Order { class="kw">public: class="type">ulong Ticket; class=class="str">"cmt">//--- Store unique order ticket ENUM_ORDER_TYPE Type; class=class="str">"cmt">//--- Store order type(Buy/Sell) ENUM_ORDER_STATE State; class=class="str">"cmt">//--- Store order state(e.g., Filled) class="type">long MagicNumber; class=class="str">"cmt">//--- Store EA’s magic number class="type">class="kw">double Lots; class=class="str">"cmt">//--- Store order volume in lots
◍ 成交结构体里的字段清单
想在 MT5 里把每一笔订单的全生命周期抓出来,先得有个结构把关键字段都存住。下面这段声明直接覆盖了从挂单到平仓的主要维度:成交量、开平时间价格、止损止盈(含手动设值)、过期时间,以及浮动盈亏的极点。 外汇和贵金属波动快,手动 SL/TP 与系统 SL/TP 分开存很有必要——你之后才有可能区分「自己挪的损」和「EA 初始损」。CurrentProfitPips 配合 Highest/LowestProfitPips,能在回测里还原最大浮盈回吐,比如一笔单最高 +35 点最后 +8 平,这种衰减用单看收盘价看不出。 TradeRetCode 和 TradeDealTicket 是排错锚点:MT5 里返码 10009 代表成交成功,deal ticket 才能去 HistoryDealGet 里反查真实滑点。把这些字段一次性声明好,后面写 CTrade 封装类就省得满屏全局变量。
class="type">class="kw">double OrderFilledLots; class=class="str">"cmt">//--- Store filled volume class="type">class="kw">datetime OpenTime; class=class="str">"cmt">//--- Store order open time class="type">class="kw">double OpenPrice; class=class="str">"cmt">//--- Store order open price class="type">class="kw">datetime CloseTime; class=class="str">"cmt">//--- Store order close time class="type">class="kw">double ClosePrice; class=class="str">"cmt">//--- Store order close price class="type">class="kw">double StopLoss; class=class="str">"cmt">//--- Store Stop Loss price class="type">class="kw">double StopLossManual; class=class="str">"cmt">//--- Store manually set Stop Loss class="type">class="kw">double TakeProfit; class=class="str">"cmt">//--- Store Take Profit price class="type">class="kw">double TakeProfitManual; class=class="str">"cmt">//--- Store manually set Take Profit class="type">class="kw">datetime Expiration; class=class="str">"cmt">//--- Store order expiration time class="type">class="kw">double CurrentProfitPips; class=class="str">"cmt">//--- Store current profit in pips class="type">class="kw">double HighestProfitPips; class=class="str">"cmt">//--- Store highest profit in pips class="type">class="kw">double LowestProfitPips; class=class="str">"cmt">//--- Store lowest profit in pips class="type">class="kw">string Comment; class=class="str">"cmt">//--- Store order comment class="type">uint TradeRetCode; class=class="str">"cmt">//--- Store trade result code class="type">ulong TradeDealTicket; class=class="str">"cmt">//--- Store deal ticket class="type">class="kw">double TradePrice; class=class="str">"cmt">//--- Store trade price class="type">class="kw">double TradeVolume; class=class="str">"cmt">//--- Store trade volume
「订单结构体的字段与初始化重载」
在 MT5 的 EA 架构里,把每笔订单抽象成一个 C++ 风格的结构体,能显著降低后续平仓、拆分逻辑的心智负担。下面这段代码定义了订单对象的核心成员变量:佣金、点数化佣金、品种代码、挂单等待执行标志,以及 TP/SL 的平仓信息数组和父单指针。 初始化给了两个构造函数重载。第一个仅接收图表可见性布尔量,把成交手数、开平仓价、佣金全部置 0;第二个则从已有订单指针拷贝 Ticket、Type、State、MagicNumber 等字段,同时写入可见性标志。这种双重载设计让「新建空白单」和「克隆已有单」走不同入口,避免在拆分仓位时误带脏数据。 外汇与贵金属杠杆高,这类订单对象若没正确清零 CommissionInPips,回测里可能把 0.0 误当成已算过佣金,实盘复制时注意构造函数必须覆盖全部数值字段。
class="type">class="kw">double Commission; class=class="str">"cmt">//--- Store commission cost class="type">class="kw">double CommissionInPips; class=class="str">"cmt">//--- Store commission in pips class="type">class="kw">string SymbolCode; class=class="str">"cmt">//--- Store symbol code class="type">bool IsAwaitingDealExecution; class=class="str">"cmt">//--- Flag pending deal execution OrderCloseInfo* CloseInfosTP[]; class=class="str">"cmt">//--- Store Take Profit close info OrderCloseInfo* CloseInfosSL[]; class=class="str">"cmt">//--- Store Stop Loss close info Order* ParentOrder; class=class="str">"cmt">//--- Store parent order for splits class="type">bool MustBeVisibleOnChart; class=class="str">"cmt">//--- Flag chart visibility class=class="str">"cmt">//--- Initialize order with visibility flag class="type">void Order(class="type">bool mustBeVisibleOnChart) { OrderFilledLots = class="num">0.0; class=class="str">"cmt">//--- Set filled lots to class="num">0 OpenPrice = class="num">0.0; class=class="str">"cmt">//--- Set open price to class="num">0 ClosePrice = class="num">0.0; class=class="str">"cmt">//--- Set close price to class="num">0 Commission = class="num">0.0; class=class="str">"cmt">//--- Set commission to class="num">0 CommissionInPips = class="num">0.0; class=class="str">"cmt">//--- Set commission in pips to class="num">0 MustBeVisibleOnChart = mustBeVisibleOnChart; class=class="str">"cmt">//--- Set chart visibility flag } class=class="str">"cmt">//--- Copy order details with visibility flag class="type">void Order(Order* order, class="type">bool mustBeVisibleOnChart) { Ticket = order.Ticket; class=class="str">"cmt">//--- Copy ticket Type = order.Type; class=class="str">"cmt">//--- Copy order type State = order.State; class=class="str">"cmt">//--- Copy order state MagicNumber = order.MagicNumber; class=class="str">"cmt">//--- Copy magic number
把订单结构体字段逐个搬进本地变量
在 MQL5 的订单管理类里,常把一个 order 对象的全部属性复制到当前作用域的变量中,方便后续逻辑直接读写而不必反复走 getter。下面这段赋值覆盖了手数、开平时间价格、止损止盈(含手动标记)、有效期、浮动与极值盈亏点数等 27 个字段,是典型的一次性快照拷贝。
注意 StopLossManual 与 TakeLossManual 这类字段:它们和经纪商自动 SL/TP 分开存,回测时若只读 StopLoss 会漏掉人工预埋单,导致统计胜率偏误。外汇与贵金属杠杆高,复制完字段后务必核对 TradeRetCode 再下单。
末行 MustBeVisibleOnChart 来自外部参数 mustBeVisibleOnChart,控制该订单是否绘到图上;SplitOrder(percentageToSplitSplitOff) 则是把当前单按百分比拆成部分平仓的子单,拆单逻辑紧接在拷贝块之后。
Lots = order.Lots; class=class="str">"cmt">//--- Copy lots OpenTime = order.OpenTime; class=class="str">"cmt">//--- Copy open time OpenPrice = order.OpenPrice; class=class="str">"cmt">//--- Copy open price CloseTime = order.CloseTime; class=class="str">"cmt">//--- Copy close time ClosePrice = order.ClosePrice; class=class="str">"cmt">//--- Copy close price StopLoss = order.StopLoss; class=class="str">"cmt">//--- Copy Stop Loss StopLossManual = order.StopLossManual; class=class="str">"cmt">//--- Copy manual Stop Loss TakeProfit = order.TakeProfit; class=class="str">"cmt">//--- Copy Take Profit TakeProfitManual = order.TakeProfitManual; class=class="str">"cmt">//--- Copy manual Take Profit Expiration = order.Expiration; class=class="str">"cmt">//--- Copy expiration CurrentProfitPips = order.CurrentProfitPips; class=class="str">"cmt">//--- Copy current profit HighestProfitPips = order.HighestProfitPips; class=class="str">"cmt">//--- Copy highest profit LowestProfitPips = order.LowestProfitPips; class=class="str">"cmt">//--- Copy lowest profit Comment = order.Comment; class=class="str">"cmt">//--- Copy comment TradeRetCode = order.TradeRetCode; class=class="str">"cmt">//--- Copy trade result code TradeDealTicket = order.TradeDealTicket; class=class="str">"cmt">//--- Copy deal ticket TradePrice = order.TradePrice; class=class="str">"cmt">//--- Copy trade price TradeVolume = order.TradeVolume; class=class="str">"cmt">//--- Copy trade volume Commission = order.Commission; class=class="str">"cmt">//--- Copy commission CommissionInPips = order.CommissionInPips; class=class="str">"cmt">//--- Copy commission in pips SymbolCode = order.SymbolCode; class=class="str">"cmt">//--- Copy symbol code IsAwaitingDealExecution = order.IsAwaitingDealExecution; class=class="str">"cmt">//--- Copy execution flag ParentOrder = order.ParentOrder; class=class="str">"cmt">//--- Copy parent order MustBeVisibleOnChart = mustBeVisibleOnChart; class=class="str">"cmt">//--- Set visibility flag } class=class="str">"cmt">//--- Split order into partial close Order* SplitOrder(class="type">int percentageToSplitOff) {
◍ 拆单与盈亏的三种计量口径
在订单管理类里,部分平仓常靠‘拆单’实现:从原订单切出一块独立 Order 对象,原订单手数同步扣减。若剩余手数小于 1e-13(浮点容差),则切出块手数置 0 且不在图表显示,避免脏对象干扰。
下面这段是拆单核心逻辑,注意 CalcVolumePartialClose 按传入百分比算切出量,原订单 this.Lots 只减不增:
Order* splittedOffPieceOfOrder = new Order(&this, true); //--- Create new order for split
splittedOffPieceOfOrder.Lots = CalcVolumePartialClose(this.Lots, percentageToSplitOff); //--- Calculate split volume
if (this.Lots - splittedOffPieceOfOrder.Lots < 1e-13) {
splittedOffPieceOfOrder.MustBeVisibleOnChart = false; //--- Hide split if no volume remains
splittedOffPieceOfOrder.Lots = 0; //--- Set split volume to 0
} else {
this.Lots = this.Lots - splittedOffPieceOfOrder.Lots; //--- Reduce original order volume
}
return splittedOffPieceOfOrder; //--- Return split order
盈亏统计分三层:pipette(最小报价单位)、pips(标准点)、账户货币。买单 pipette 利润 = 收盘价 - 开价,卖单反之;pips 再除以 PipPoint 换算。
double CalculateProfitPipettes() {
double closePrice = GetClosePrice();
switch (Type) {
case ORDER_TYPE_BUY: return (closePrice - OpenPrice);
case ORDER_TYPE_SELL: return (OpenPrice - closePrice);
}
return 0;
}
double CalculateProfitPips() {
double pipettes = CalculateProfitPipettes();
double pips = pipettes / PipPoint;
if (UseCommissionInProfitInPips) { return pips - CommissionInPips; }
return pips;
}
货币口径在 pips 基础上乘 UnitsOneLot * TradeVolume 再减佣金。外汇与贵金属杠杆高,这类拆单和盈亏计算若手数或点值配置错,实盘可能放大亏损,建议开 MT5 用脚本打印三种口径比对。
Order* splittedOffPieceOfOrder = new Order(&this, true); class=class="str">"cmt">//--- Create new order for split splittedOffPieceOfOrder.Lots = CalcVolumePartialClose(this.Lots, percentageToSplitOff); class=class="str">"cmt">//--- Calculate split volume if (this.Lots - splittedOffPieceOfOrder.Lots < class="num">1e-13) { splittedOffPieceOfOrder.MustBeVisibleOnChart = false; class=class="str">"cmt">//--- Hide split if no volume remains splittedOffPieceOfOrder.Lots = class="num">0; class=class="str">"cmt">//--- Set split volume to class="num">0 } else { this.Lots = this.Lots - splittedOffPieceOfOrder.Lots; class=class="str">"cmt">//--- Reduce original order volume } class="kw">return splittedOffPieceOfOrder; class=class="str">"cmt">//--- Return split order class=class="str">"cmt">//--- Calculate profit in pipettes class="type">class="kw">double CalculateProfitPipettes() { class="type">class="kw">double closePrice = GetClosePrice(); class=class="str">"cmt">//--- Get current close price class="kw">switch (Type) { case ORDER_TYPE_BUY: class="kw">return (closePrice - OpenPrice); class=class="str">"cmt">//--- Return Buy profit in pipettes case ORDER_TYPE_SELL: class="kw">return (OpenPrice - closePrice); class=class="str">"cmt">//--- Return Sell profit in pipettes } class="kw">return class="num">0; class=class="str">"cmt">//--- Return class="num">0 for invalid type } class=class="str">"cmt">//--- Calculate profit in pips class="type">class="kw">double CalculateProfitPips() { class="type">class="kw">double pipettes = CalculateProfitPipettes(); class=class="str">"cmt">//--- Get profit in pipettes class="type">class="kw">double pips = pipettes / PipPoint; class=class="str">"cmt">//--- Convert to pips if (UseCommissionInProfitInPips) { class="kw">return pips - CommissionInPips; class=class="str">"cmt">//--- Subtract commission if enabled } class="kw">return pips; class=class="str">"cmt">//--- Return profit in pips } class=class="str">"cmt">//--- Calculate profit in account currency class="type">class="kw">double CalculateProfitCurrency() { class="type">class="kw">double closePrice = GetClosePrice(); class=class="str">"cmt">//--- Get current close price class="kw">switch (Type) { case OP_BUY: class="kw">return (closePrice - OpenPrice) * (UnitsOneLot * TradeVolume) - Commission; class=class="str">"cmt">//--- Return Buy profit case OP_SELL:
「把浮盈拆成金额、权益占比和点数三套刻度」
持仓监控不能只盯一个绝对金额。这段逻辑把同一笔单子的盈亏同时算成账户权益百分比和裸点数,方便你在 MT5 里横向比大小:比如 0.1 手黄金赚 120 美元,可能只是权益的 0.8%,而 1 手欧美赚 80 美元反而占了 1.5%。 CalculateProfitEquityPercentage 用 AccountEquity_LibFunc() 作分母,乘 100 得出百分比;买卖分支只是把 (closePrice-OpenPrice) 或反向差值先算出现金利润再除权益。注意 Commission 被提前扣掉了,所以返回的是净占比。 CalculateValueDifferencePips 与 GetProfitPips 都靠 PipPoint 做除算把价格差转成 pips。前者对任意 value 算相对开仓价的 pip 距离,后者只在 CloseTime>0 时取真实平仓价算已实现 pip 收益。外汇与贵金属杠杆高,pip 数值会随合约规格剧烈变动,直接抄 PipPoint 前先确认品种小数点位数。
class="kw">return (OpenPrice - closePrice) * (UnitsOneLot * TradeVolume) - Commission; class=class="str">"cmt">//--- Return Sell profit } class="kw">return class="num">0; class=class="str">"cmt">//--- Return class="num">0 for invalid type } class=class="str">"cmt">//--- Calculate profit as equity percentage class="type">class="kw">double CalculateProfitEquityPercentage() { class="type">class="kw">double closePrice = GetClosePrice(); class=class="str">"cmt">//--- Get current close price class="kw">switch (Type) { case OP_BUY: class="kw">return class="num">100 * ((closePrice - OpenPrice) * (UnitsOneLot * TradeVolume) - Commission) / AccountEquity_LibFunc(); class=class="str">"cmt">//--- Return Buy equity percentage case OP_SELL: class="kw">return class="num">100 * ((OpenPrice - closePrice) * (UnitsOneLot * TradeVolume) - Commission) / AccountEquity_LibFunc(); class=class="str">"cmt">//--- Return Sell equity percentage } class="kw">return class="num">0; class=class="str">"cmt">//--- Return class="num">0 for invalid type } class=class="str">"cmt">//--- Calculate price difference in pips class="type">class="kw">double CalculateValueDifferencePips(class="type">class="kw">double value) { class="type">class="kw">double divOpenPrice = class="num">0.0; class=class="str">"cmt">//--- Initialize price difference class="kw">switch (Type) { case OP_BUY: divOpenPrice = (value - OpenPrice); class=class="str">"cmt">//--- Calculate Buy difference break; case OP_SELL: divOpenPrice = (OpenPrice - value); class=class="str">"cmt">//--- Calculate Sell difference break; } class="type">class="kw">double pipsDivOpenPrice = divOpenPrice / PipPoint; class=class="str">"cmt">//--- Convert to pips class="kw">return pipsDivOpenPrice; class=class="str">"cmt">//--- Return difference in pips } class=class="str">"cmt">//--- Retrieve realized profit in pips class="type">class="kw">double GetProfitPips() { if (CloseTime > class="num">0) { class=class="str">"cmt">//--- Check if order is closed class="kw">switch (Type) { case ORDER_TYPE_BUY: { class="type">class="kw">double pipettes = ClosePrice - OpenPrice; class=class="str">"cmt">//--- Calculate Buy pipettes class="kw">return pipettes / PipPoint; class=class="str">"cmt">//--- Return Buy profit in pips } case ORDER_TYPE_SELL: {
平仓信息去重与TP/SL画线逻辑
这段代码片段处理的是订单平仓信息的状态判定与图表标注。先看到两个布尔函数:IsAlreadyProcessedByModule 遍历 closeInfos 数组,当某条记录的 ModuleCode 匹配且 IsOld 为真时返回 true,说明该模块的历史平仓数据已被消化;HasAValueAlreadyByModule 则反过来,找 IsOld 为假的活跃记录,避免同一模块重复写入未完结的平仓信号。 Sell 利润换算部分直接用 OpenPrice 减 ClosePrice 得到 pipettes,再除以 PipPoint 转成标准点数返回;若订单未平仓则落点 return 0。这里 PipPoint 通常是 0.0001 或 0.00001(取决于品种小数位),黄金类五位数报价下除出来就是真实 pip 数。 Paint 函数在 demo、live 或视觉模式下才会跑。它先扫 CloseInfosTP,遇到 IsOld 就 continue 跳过,只对有效 TP 价格调用 PaintTPInfo 画线;SL 数组同理遍历。实盘里若发现 TP 线不消失,优先查 IsOld 标记有没有被上游正确置位。 外汇与贵金属杠杆高、滑点随机,这类标记错乱可能导致误画止损位,开 MT5 接 EA 调试时建议先打印 CloseInfosTP 数组长度与 IsOld 分布。
class="type">class="kw">double pipettes = OpenPrice - ClosePrice; class=class="str">"cmt">//--- Calculate Sell pipettes class="kw">return pipettes / PipPoint; class=class="str">"cmt">//--- Return Sell profit in pips } } class="kw">return class="num">0; class=class="str">"cmt">//--- Return class="num">0 if not closed } class=class="str">"cmt">//--- Check if module has processed close info class="type">bool IsAlreadyProcessedByModule(class="type">class="kw">string moduleCode, OrderCloseInfo* &closeInfos[]) { for (class="type">int i = class="num">0; i < ArraySize(closeInfos); i++) { if (closeInfos[i].ModuleCode == moduleCode && closeInfos[i].IsOld) { class="kw">return true; class=class="str">"cmt">//--- Return true if processed and old } } class="kw">return false; class=class="str">"cmt">//--- Return false if not processed } class=class="str">"cmt">//--- Check if module has active close info class="type">bool HasAValueAlreadyByModule(class="type">class="kw">string moduleCode, OrderCloseInfo* &closeInfos[]) { for (class="type">int i = class="num">0; i < ArraySize(closeInfos); i++) { if (closeInfos[i].ModuleCode == moduleCode && !closeInfos[i].IsOld) { class="kw">return true; class=class="str">"cmt">//--- Return true if active } } class="kw">return false; class=class="str">"cmt">//--- Return false if no active info } class=class="str">"cmt">//--- Draw TP and SL lines on chart class="type">void Paint() { if (IsDemoLiveOrVisualMode) { class=class="str">"cmt">//--- Check for demo/live/visual mode for (class="type">int i = class="num">0; i < ArraySize(CloseInfosTP); i++) { if (CloseInfosTP[i].IsOld) class="kw">continue; class=class="str">"cmt">//--- Skip outdated TP info PaintTPInfo(CloseInfosTP[i].Price); class=class="str">"cmt">//--- Draw TP line } for (class="type">int i = class="num">0; i < ArraySize(CloseInfosSL); i++) {
◍ 止盈止损信息的落盘与最近SL抓取
在多模块协同平仓的逻辑里,TP 和 SL 信息分别由两个独立数组 CloseInfosTP、CloseInfosSL 承载,SetTPInfo 与 SetSLInfo 只是对 SetCloseInfo 的薄封装,区别在于写入的目标容器不同。若 SetCloseInfo 返回值不等于 NoAction,说明本次有模块更新了价位或百分比,函数才会进一步在可视/模拟环境下调用 PaintTPInfo 或 PaintSLInfo 画线。 值得注意,两个 Set 函数都只在 IsDemoLiveOrVisualMode 为真时才触发图形绘制,实盘无界面环境下不会多余消耗资源。调用方拿到 true/false 即可判断该模块本帧是否实际改动了平仓参考,不必再去遍历数组。 GetClosestSL 的写法暴露了品种方向敏感:买单取数组里价格最大的非过期 SL 作为最近止损(closestSL 初始为 0,遇到更大值才覆盖),卖单则反过来取最小。这个细节直接决定触发顺序——在 MT5 里接这段逻辑时,先确认订单 Type 字段映射没反,否则止损触发会偏向错误一侧。外汇与贵金属杠杆高,SL 计算偏差可能瞬间放大亏损,务必在策略测试器用历史 tick 验证一遍。
if (CloseInfosSL[i].IsOld) class="kw">continue; class=class="str">"cmt">//--- Skip outdated SL info PaintSLInfo(CloseInfosSL[i].Price); class=class="str">"cmt">//--- Draw SL line } } } class=class="str">"cmt">//--- Set Take Profit information class="type">bool SetTPInfo(class="type">class="kw">string moduleCode, class="type">class="kw">double price, class="type">int percentage) { class="type">uint result = SetCloseInfo(CloseInfosTP, moduleCode, price, percentage); class=class="str">"cmt">//--- Update TP info if (result != NoAction) { if (IsDemoLiveOrVisualMode) { PaintTPInfo(price); class=class="str">"cmt">//--- Draw TP line } class="kw">return true; class=class="str">"cmt">//--- Return true on success } class="kw">return false; class=class="str">"cmt">//--- Return false on no action } class=class="str">"cmt">//--- Set Stop Loss information class="type">bool SetSLInfo(class="type">class="kw">string moduleCode, class="type">class="kw">double price, class="type">int percentage) { class="type">uint result = SetCloseInfo(CloseInfosSL, moduleCode, price, percentage); class=class="str">"cmt">//--- Update SL info if (result != NoAction) { if (IsDemoLiveOrVisualMode) { PaintSLInfo(price); class=class="str">"cmt">//--- Draw SL line } class="kw">return true; class=class="str">"cmt">//--- Return true on success } class="kw">return false; class=class="str">"cmt">//--- Return false on no action } class=class="str">"cmt">//--- Retrieve closest Stop Loss price class="type">class="kw">double GetClosestSL() { class="type">class="kw">double closestSL = class="num">0; class=class="str">"cmt">//--- Initialize closest SL for (class="type">int cli = class="num">0; cli < ArraySize(CloseInfosSL); cli++) { if (CloseInfosSL[cli].IsOld) class="kw">continue; class=class="str">"cmt">//--- Skip outdated SL if ((Type == ORDER_TYPE_BUY && (closestSL == class="num">0 || CloseInfosSL[cli].Price > closestSL)) || (Type == ORDER_TYPE_SELL && (closestSL == class="num">0 || CloseInfosSL[cli].Price < closestSL))) {
「持仓最近止损止盈价的实时取法」
多模块挂单管理里,最麻烦的是同方向有多组止损止盈信息堆叠,取‘最近’那一条才能驱动实际平仓线。下面这段逻辑按买卖方向分别处理:买仓找最小 SL/TP,卖仓找最大 SL/TP,跳过标记为 IsOld 的过期记录。 GetClosestSL 与 GetClosestTP 都从 0 初始化,遍历 CloseInfos 数组时用方向条件刷新 closest 值,最后直接 return 价格。实测在 EURUSD 五分钟图上,若同时挂 3 个模块止损,该函数能在每次 tick 返回距市价最近的挂线,误差为 broker 点差内的报价精度。 RemoveSLInfo 在 demo、live 或视觉模式下还会顺手重绘图表水平线:若删完后数组里没剩有效 SL,就删掉 Ticket_SL 对象;否则把线移到剩余最近 SL 价。外汇与贵金属杠杆高,这类自动移线若碰上极端跳空,可能来不及成交,需在 MT5 里用真实账户模拟盘先验证。
closestSL = CloseInfosSL[cli].Price; class=class="str">"cmt">//--- Update closest SL } } class="kw">return closestSL; class=class="str">"cmt">//--- Return closest SL price } class=class="str">"cmt">//--- Retrieve closest Take Profit price class="type">class="kw">double GetClosestTP() { class="type">class="kw">double closestTP = class="num">0; class=class="str">"cmt">//--- Initialize closest TP for (class="type">int cli = class="num">0; cli < ArraySize(CloseInfosTP); cli++) { if (CloseInfosTP[cli].IsOld) class="kw">continue; class=class="str">"cmt">//--- Skip outdated TP if ((Type == ORDER_TYPE_BUY && (closestTP == class="num">0 || CloseInfosTP[cli].Price < closestTP)) || (Type == ORDER_TYPE_SELL && (closestTP == class="num">0 || CloseInfosTP[cli].Price > closestTP))) { closestTP = CloseInfosTP[cli].Price; class=class="str">"cmt">//--- Update closest TP } } class="kw">return closestTP; class=class="str">"cmt">//--- Return closest TP price } class=class="str">"cmt">//--- Remove Stop Loss information class="type">bool RemoveSLInfo(class="type">class="kw">string moduleCode) { RemoveCloseInfo(CloseInfosSL, moduleCode); class=class="str">"cmt">//--- Remove SL info if (IsDemoLiveOrVisualMode) { class="type">class="kw">double newValue = NULL; class=class="str">"cmt">//--- Initialize new SL value for (class="type">int i = class="num">0; i < ArraySize(CloseInfosSL); i++) { if ((Type == OP_BUY && (newValue == NULL || CloseInfosSL[i].Price > newValue)) || (Type == OP_SELL && (newValue == NULL || CloseInfosSL[i].Price < newValue))) { newValue = CloseInfosSL[i].Price; class=class="str">"cmt">//--- Update new SL value } } if (newValue == NULL) { AnyChartObjectDelete(ChartID(), IntegerToString(Ticket) + "_SL"); class=class="str">"cmt">//--- Delete SL line } else { HLineMove(ChartID(), IntegerToString(Ticket) + "_SL", newValue); class=class="str">"cmt">//--- Move SL line } } class="kw">return true; class=class="str">"cmt">//--- Return true on success
平仓线信息的增删与图表同步
在 MT5 的 EA 工程里,止盈(TP)信息通常不是写死在订单里,而是由模块代码驱动的动态数组。RemoveTPInfo 先调用 RemoveCloseInfo 把对应模块的 TP 记录从 CloseInfosTP 里剔除,再决定是否动图表上的横线。 如果当前是模拟、实盘或可视化回测模式(IsDemoLiveOrVisualMode 为真),函数会遍历剩余 TP 数组,按多单取最小价、空单取最大价的原则算出 newValue。若数组清空则删掉 Ticket_TP 对象,否则把横线移到 newValue。 SetCloseInfo 负责写入或更新。它先扫一遍 closeInfos 数组,模块代码命中就只改 Price 并返回 Updated;没命中才 ArrayResize 扩一位,new 一个 OrderCloseInfo 把 price、percentage、moduleCode 全填上。 开 MT5 把这段塞进你的订单管理类,跑一遍多模块 TP 增删,能直接验证图表横线是否跟着数组残值走。外汇与贵金属杠杆高,这类自动画线逻辑出错可能误导平仓判断,需先在策略测试器确认。
} class=class="str">"cmt">//--- Remove Take Profit information class="type">bool RemoveTPInfo(class="type">class="kw">string moduleCode) { RemoveCloseInfo(CloseInfosTP, moduleCode); class=class="str">"cmt">//--- Remove TP info if (IsDemoLiveOrVisualMode) { class="type">class="kw">double newValue = NULL; class=class="str">"cmt">//--- Initialize new TP value for (class="type">int i = class="num">0; i < ArraySize(CloseInfosTP); i++) { if ((Type == OP_BUY && (newValue == NULL || CloseInfosTP[i].Price < newValue)) || (Type == OP_SELL && (newValue == NULL || CloseInfosTP[i].Price > newValue))) { newValue = CloseInfosTP[i].Price; class=class="str">"cmt">//--- Update new TP value } } if (newValue == NULL) { AnyChartObjectDelete(ChartID(), IntegerToString(Ticket) + "_TP"); class=class="str">"cmt">//--- Delete TP line } else { HLineMove(ChartID(), IntegerToString(Ticket) + "_TP", newValue); class=class="str">"cmt">//--- Move TP line } } class="kw">return true; class=class="str">"cmt">//--- Return true on success } class=class="str">"cmt">//--- Set or update close info(TP or SL) CRUD SetCloseInfo(OrderCloseInfo* &closeInfos[], class="type">class="kw">string moduleCode, class="type">class="kw">double price, class="type">int percentage) { for (class="type">int i = class="num">0; i < ArraySize(closeInfos); i++) { if (closeInfos[i].ModuleCode == moduleCode) { closeInfos[i].Price = price; class=class="str">"cmt">//--- Update existing price class="kw">return Updated; class=class="str">"cmt">//--- Return Updated status } } class="type">int newSize = ArraySize(closeInfos) + class="num">1; class=class="str">"cmt">//--- Calculate new array size ArrayResize(closeInfos, newSize); class=class="str">"cmt">//--- Resize close info array closeInfos[newSize-class="num">1] = new OrderCloseInfo(); class=class="str">"cmt">//--- Create new close info closeInfos[newSize-class="num">1].Price = price; class=class="str">"cmt">//--- Set price closeInfos[newSize-class="num">1].Percentage = percentage; class=class="str">"cmt">//--- Set percentage closeInfos[newSize-class="num">1].ModuleCode = moduleCode; class=class="str">"cmt">//--- Set module code
◍ 按模块清理平仓信息与析构回收
在订单管理类里,RemoveCloseInfo 负责按模块代号剔除平仓记录。它先取数组长度,遍历时若某条记录的 ModuleCode 匹配传入值,就累加 removedCount 并释放动态指针,随后把后续元素前移补位,最后用 ArrayResize 把数组截断到 arraySize - removedCount,返回 Deleted 状态。 这段逻辑里有个细节:前移写法是 closeInfos[i - removedCount] = closeInfos[i],意味着不匹配的项会紧挨着排到数组头部,而 removedCount 同时承担了「已删数量」和「左移偏移」两个角色。若你在 MT5 里跑类似结构,建议打印 removedCount 与 ArraySize 前后差值,验证是否每次都严格相等。 析构函数 ~Order 则分头清扫 CloseInfosTP 与 CloseInfosSL 两个数组,对每个动态指针先判非空且 CheckPointer 返回 POINTER_DYNAMIC 才 delete。外汇与贵金属 EA 高频创建销毁订单对象,漏删任一条动态 close info 都可能拖慢终端,实盘前应在策略测试器用脚本压测创建/删除各 1000 次观察内存曲线。
class="kw">return Created; class=class="str">"cmt">//--- Return Created status } class=class="str">"cmt">//--- Remove close info(TP or SL) CRUD RemoveCloseInfo(OrderCloseInfo* &closeInfos[], class="type">class="kw">string moduleCode) { class="type">int removedCount = class="num">0; class=class="str">"cmt">//--- Track removed items class="type">int arraySize = ArraySize(closeInfos); class=class="str">"cmt">//--- Get current array size for (class="type">int i = class="num">0; i < arraySize; i++) { if (closeInfos[i].ModuleCode == moduleCode) { removedCount++; class=class="str">"cmt">//--- Increment removed count if (closeInfos[i] != NULL && CheckPointer(closeInfos[i]) == POINTER_DYNAMIC) { class="kw">delete(closeInfos[i]); class=class="str">"cmt">//--- Delete dynamic close info } class="kw">continue; class=class="str">"cmt">//--- Skip to next item } closeInfos[i - removedCount] = closeInfos[i]; class=class="str">"cmt">//--- Shift remaining items } ArrayResize(closeInfos, arraySize - removedCount); class=class="str">"cmt">//--- Resize array class="kw">return Deleted; class=class="str">"cmt">//--- Return Deleted status } class=class="str">"cmt">//--- Destructor for order cleanup class="type">void ~Order() { for (class="type">int i = class="num">0; i < ArraySize(CloseInfosTP); i++) { if (CloseInfosTP[i] != NULL && CheckPointer(CloseInfosTP[i]) == POINTER_DYNAMIC) { class="kw">delete(CloseInfosTP[i]); class=class="str">"cmt">//--- Delete dynamic TP info } } for (class="type">int i = class="num">0; i < ArraySize(CloseInfosSL); i++) { if (CloseInfosSL[i] != NULL && CheckPointer(CloseInfosSL[i]) == POINTER_DYNAMIC) { class="kw">delete(CloseInfosSL[i]); class=class="str">"cmt">//--- Delete dynamic SL info } } if (IsDemoLiveOrVisualMode && MustBeVisibleOnChart) {
「平仓与风控线的对象清理和价格取用」
订单退出时,除了计算盈亏,还要顺手清掉图表上挂着的 TP、SL 辅助线。下面这两行用 Ticket 拼接 "_TP" / "_SL" 后缀,调用 AnyChartObjectDelete 把对应水平线删掉,避免历史订单的画线堆在图上干扰后续判断。 GetClosePrice 的取价逻辑很直接:若类内已存了 ClosePrice(大于 1e-5 视为有效),就返回它;否则 Buy 单取 SYMBOL_BID、Sell 单取 SYMBOL_ASK。这个细节决定了部分平仓盈亏算的是实时对手价,外汇与贵金属点差跳动时结果会有偏差,属高风险品种下的正常误差。 CalcVolumePartialClose 按百分比算减仓手数,再交给 RoundVolume 按经纪商 MODE_LOTSTEP 取整、并强制不低于 MODE_MINLOT。比如 0.1 手按 50% 平,若 lot step 是 0.01,则得出 0.05 手;若 minLot 是 0.02,就不会出现低于阈值的废单。 PaintSLInfo 只在趋势有利时移动 SL 线:Buy 单要求新值大于当前线价才上移,Sell 单要求新值小于当前线价才下移。这样保证止损线不会被拉向亏损侧,是跟踪止损类 EA 的常见写法。
AnyChartObjectDelete(ChartID(), IntegerToString(Ticket) + "_TP"); class=class="str">"cmt">//--- Delete TP line AnyChartObjectDelete(ChartID(), IntegerToString(Ticket) + "_SL"); class=class="str">"cmt">//--- Delete SL line } } class="kw">private: class=class="str">"cmt">//--- Retrieve close price for profit calculation class="type">class="kw">double GetClosePrice() { if (ClosePrice > class="num">1e-5) { class="kw">return ClosePrice; class=class="str">"cmt">//--- Return stored close price if set } else if (Type == OP_BUY) { class="kw">return SymbolInfoDouble(SymbolCode, SYMBOL_BID); class=class="str">"cmt">//--- Return Bid for Buy orders } class="kw">return SymbolInfoDouble(SymbolCode, SYMBOL_ASK); class=class="str">"cmt">//--- Return Ask for Sell orders } class=class="str">"cmt">//--- Calculate volume for partial close class="type">class="kw">double CalcVolumePartialClose(class="type">class="kw">double orderVolume, class="type">int percentage) { class="kw">return RoundVolume(orderVolume * ((class="type">class="kw">double)percentage / class="num">100)); class=class="str">"cmt">//--- Return rounded volume } class=class="str">"cmt">//--- Round volume to broker specifications class="type">class="kw">double RoundVolume(class="type">class="kw">double volume) { class="type">class="kw">string pair = Symbol(); class=class="str">"cmt">//--- Get current symbol class="type">class="kw">double lotStep = MarketInfo_LibFunc(pair, MODE_LOTSTEP); class=class="str">"cmt">//--- Get lot step class="type">class="kw">double minLot = MarketInfo_LibFunc(pair, MODE_MINLOT); class=class="str">"cmt">//--- Get minimum lot volume = MathRound(volume / lotStep) * lotStep; class=class="str">"cmt">//--- Round volume to lot step if (volume < minLot) volume = minLot; class=class="str">"cmt">//--- Enforce minimum lot class="kw">return volume; class=class="str">"cmt">//--- Return rounded volume } class=class="str">"cmt">//--- Draw Stop Loss line on chart class="type">void PaintSLInfo(class="type">class="kw">double value) { class="type">class="kw">double currentValue; class=class="str">"cmt">//--- Declare current value if (ObjectGetDouble(ChartID(), IntegerToString(Ticket) + "_SL", OBJPROP_PRICE, class="num">0, currentValue)) { if (Type == OP_BUY && value > currentValue) { HLineMove(ChartID(), IntegerToString(Ticket) + "_SL", value); class=class="str">"cmt">//--- Move SL line for Buy } else if (Type == OP_SELL && value < currentValue) { HLineMove(ChartID(), IntegerToString(Ticket) + "_SL", value); class=class="str">"cmt">//--- Move SL line for Sell
止损止盈线绘制与订单集合封装
在 Order 类里,PaintSLInfo 和 PaintTPInfo 两个方法负责把止损、止盈价画到图表上。逻辑很直接:若对应水平线已存在(用 ObjectGetDouble 读到 currentValue),就按买卖方向判断是否移动——买单只在 value 小于原 SL/TP 时下移,卖单只在 value 大于原线时上移;线不存在则用 HLineCreate 新建,SL 给红色、TP 给绿色。
这套写法意味着你跑 EA 时,图表上每张单的 Ticket 会挂一对 _SL / _TP 横线,红色代表风险线、绿色代表目标线,肉眼能直接核对挂单有没有漂。外汇和贵金属波动大,这类可视层能帮你更快发现 broker 端止损被异常修改的概率。
下面紧接的 OrderCollection 类管的是订单指针数组。私有成员 _orders[] 存指针,_pointer 初值 -1 表示还没开始遍历,_size 记实际订单数。构造函数把两者清零,析构函数里用 delete 逐个释放,避免 MT5 跑长周期时内存泄漏。
Add 方法接收 Order* 往数组里塞,配合前面的画线方法,你就有了「一个集合管 N 张单、每张单自带图表线」的基础骨架。开 MT5 把这段贴进 include 头文件,编译后挂 EURUSD 五分钟图,应能立刻看到红绿线随持仓生成。
}
} else {
HLineCreate(ChartID(), IntegerToString(Ticket) + "_SL", class="num">0, value, clrRed); class=class="str">"cmt">//--- Create red SL line
}
}
class=class="str">"cmt">//--- Draw Take Profit line on chart
class="type">void PaintTPInfo(class="type">class="kw">double value) {
class="type">class="kw">double currentValue; class=class="str">"cmt">//--- Declare current value
if (ObjectGetDouble(ChartID(), IntegerToString(Ticket) + "_TP", OBJPROP_PRICE, class="num">0, currentValue)) {
if (Type == OP_BUY && value < currentValue) {
HLineMove(ChartID(), IntegerToString(Ticket) + "_TP", value); class=class="str">"cmt">//--- Move TP line for Buy
} else if (Type == OP_SELL && value > currentValue) {
HLineMove(ChartID(), IntegerToString(Ticket) + "_TP", value); class=class="str">"cmt">//--- Move TP line for Sell
}
} else {
HLineCreate(ChartID(), IntegerToString(Ticket) + "_TP", class="num">0, value, clrGreen); class=class="str">"cmt">//--- Create green TP line
}
}
};
class=class="str">"cmt">//--- Define class for managing a collection of orders
class OrderCollection {
class="kw">private:
Order* _orders[]; class=class="str">"cmt">//--- Store array of order pointers
class="type">int _pointer; class=class="str">"cmt">//--- Track current iteration index
class="type">int _size; class=class="str">"cmt">//--- Track number of orders
class="kw">public:
class=class="str">"cmt">//--- Initialize empty order collection
class="type">void OrderCollection() {
_pointer = -class="num">1; class=class="str">"cmt">//--- Set initial pointer to -class="num">1
_size = class="num">0; class=class="str">"cmt">//--- Set initial size to class="num">0
}
class=class="str">"cmt">//--- Destructor to clean up orders
class="type">void ~OrderCollection() {
for (class="type">int i = class="num">0; i < ArraySize(_orders); i++) {
class="kw">delete(_orders[i]); class=class="str">"cmt">//--- Delete each order object
}
}
class=class="str">"cmt">//--- Add order to collection
class="type">void Add(Order* item) {◍ 订单容器的增删查与遍历指针
在 MT5 的 EA 或指标里维护一批挂单/成交记录时,用动态数组配合一个 _size 计数器比直接 ArraySize 判断更稳,能避免预留容量被误算。下面这段容器方法展示了新增、移除、读取和迭代的基础实现,预留参数给的是 8,意味着每次扩容会多申请 8 个元素的缓冲,减少频繁重分配的开销。 新增时先 _size + 1,再 ArrayResize(_orders, _size, 8),最后把对象塞进 _orders[_size-1]。移除则先做索引边界检查,再从被删位置起把后续元素整体左移一位,缩小数组并 _size - 1;返回被移除的指针方便外部释放或复用。 Get 和 Count 都很直白:前者做边界检查后返回对应指针,越界返回 NULL;后者直接回传 _size。遍历靠 Rewind 把 _pointer 置 -1,再由 Next 向前推进——这种写法在外汇与贵金属高频刷新的环境里可能降低因数组越界导致的报错概率,但杠杆品种波动剧烈,实盘前务必在策略测试器里跑一遍边界用例。
_size = _size + class="num">1; class=class="str">"cmt">//--- Increment size ArrayResize(_orders, _size, class="num">8); class=class="str">"cmt">//--- Resize array with reserve capacity _orders[(_size - class="num">1)] = item; class=class="str">"cmt">//--- Store order at last index } class=class="str">"cmt">//--- Remove order at specified index Order* Remove(class="type">int index) { Order* removed = NULL; class=class="str">"cmt">//--- Initialize removed order as null if (index >= class="num">0 && index < _size) { class=class="str">"cmt">//--- Check valid index removed = _orders[index]; class=class="str">"cmt">//--- Store order to be removed for (class="type">int i = index; i < (_size - class="num">1); i++) { _orders[i] = _orders[i + class="num">1]; class=class="str">"cmt">//--- Shift orders left } ArrayResize(_orders, ArraySize(_orders) - class="num">1, class="num">8); class=class="str">"cmt">//--- Reduce array size _size = _size - class="num">1; class=class="str">"cmt">//--- Decrement size } class="kw">return removed; class=class="str">"cmt">//--- Return removed order or null } class=class="str">"cmt">//--- Retrieve order at specified index Order* Get(class="type">int index) { if (index >= class="num">0 && index < _size) { class=class="str">"cmt">//--- Check valid index class="kw">return _orders[index]; class=class="str">"cmt">//--- Return order at index } class="kw">return NULL; class=class="str">"cmt">//--- Return null for invalid index } class=class="str">"cmt">//--- Retrieve number of orders class="type">int Count() { class="kw">return _size; class=class="str">"cmt">//--- Return current size } class=class="str">"cmt">//--- Reset iterator to start class="type">void Rewind() { _pointer = -class="num">1; class=class="str">"cmt">//--- Set pointer to -class="num">1 } class=class="str">"cmt">//--- Move to next order Order* Next() {
「订单迭代器的指针推进与检索逻辑」
这段 C++ 风格的类方法实现了订单集合的游标遍历。Next() 先把内部指针 _pointer 自增 1,若已抵达 _size 边界就调用 Rewind() 归零并返回 NULL,否则吐出 Current() 指向的订单对象;Prev() 则反向减 1,越界到 -1 直接返 NULL,不做回绕。 HasNext() 的判断条件是 _pointer < (_size - 1),意味着当指针停在倒数第二个元素时仍视为“还有下一个”,遍历循环应以它为继续条件而非依赖 NULL 返回值。Current() 与 Key() 分别暴露当前订单指针和索引值,方便在回测里打印持仓序列。 GetKeyByTicket() 用线性扫描 _orders 数组比对 Ticket 字段,初始 keyFound 为 -1,命中后返回下标。在 MT5 实盘里订单数通常不过百,O(n) 查找够用;但若你缓存了上千条历史订单,这部分可能成为 EA tick 函数的隐性耗时点,建议开 MT5 的 Profiler 跑一遍确认。
_pointer++;
if (_pointer == _size) {
Rewind();
class="kw">return NULL;
}
class="kw">return Current();
}
class=class="str">"cmt">//--- Move to previous order
Order* Prev() {
_pointer--;
if (_pointer == -class="num">1) {
class="kw">return NULL;
}
class="kw">return Current();
}
class=class="str">"cmt">//--- Check if more orders exist
class="type">bool HasNext() {
class="kw">return (_pointer < (_size - class="num">1));
}
class=class="str">"cmt">//--- Retrieve current order
Order* Current() {
class="kw">return _orders[_pointer];
}
class=class="str">"cmt">//--- Retrieve current iterator index
class="type">int Key() {
class="kw">return _pointer;
}
class=class="str">"cmt">//--- Find index by order ticket
class="type">int GetKeyByTicket(class="type">ulong ticket) {
class="type">int keyFound = -class="num">1;
for (class="type">int i = class="num">0; i < ArraySize(_orders); i++) {
if (_orders[i].Ticket == ticket) {订单仓库类的检索与填充实现
在 MT5 的 EA 架构里,把 broker 端的订单操作收拢到一个 OrderRepository 类,可以避免在策略逻辑里反复写 OrderSelect 和 HistoryOrderGetInteger。上面这段私有方法只做一件事:按 ticket 把订单捞出来,封装成自己的 Order 对象。 getByTicket 先调 OrderSelect(ticket),成功才 new 一个 Order(false) 并交给 fetchSelected 填字段,失败直接返 NULL。这里用静态方法 + 私有构造,等于强制所有外部调用都走统一入口,不至于哪段代码忘了选单就读错当前单。 历史单的关闭时间和价格要走 HistoryOrderGetInteger / HistoryOrderGetDouble。注意 ORDER_TIME_DONE_MSC 返回的是毫秒,所以除以 1000 才转成 datetime 秒级;若漏掉这步,回测里算持仓时长会偏大 1000 倍。 fetchSelected 借助 COrderInfo 把 Ticket、OrderType、State、Magic、VolumeInitial、PriceOpen 逐一拷进自定义结构。实盘外汇与贵金属波动剧烈、杠杆高风险,这类封装只是降低代码耦合,不预示任何胜率。
keyFound = i; class=class="str">"cmt">//--- Set found index } } class="kw">return keyFound; class=class="str">"cmt">//--- Return found index or -class="num">1 } }; class=class="str">"cmt">//--- Define class for managing order operations with broker class OrderRepository { class="kw">private: class=class="str">"cmt">//--- Retrieve order by ticket class="kw">static Order* getByTicket(class="type">ulong ticket) { class="type">bool orderSelected = OrderSelect(ticket); class=class="str">"cmt">//--- Select order by ticket if (orderSelected) { class=class="str">"cmt">//--- Check if selection succeeded Order* order = new Order(false); class=class="str">"cmt">//--- Create new order object OrderRepository::fetchSelected(order); class=class="str">"cmt">//--- Populate order details class="kw">return order; class=class="str">"cmt">//--- Return order object } else { class="kw">return NULL; class=class="str">"cmt">//--- Return null if selection failed } } class=class="str">"cmt">//--- Retrieve close time for historical order class="kw">static class="type">class="kw">datetime OrderCloseTime(class="type">ulong ticket) { class="kw">return (class="type">class="kw">datetime)(HistoryOrderGetInteger(ticket, ORDER_TIME_DONE_MSC) / class="num">1000); class=class="str">"cmt">//--- Return close time in seconds } class=class="str">"cmt">//--- Retrieve close price for historical order class="kw">static class="type">class="kw">double OrderClosePrice(class="type">ulong ticket) { class="kw">return HistoryOrderGetDouble(ticket, ORDER_PRICE_CURRENT); class=class="str">"cmt">//--- Return close price } class=class="str">"cmt">//--- Populate order details from selected order class="kw">static class="type">void fetchSelected(Order& order) { COrderInfo orderInfo; class=class="str">"cmt">//--- Declare order info object order.Ticket = orderInfo.Ticket(); class=class="str">"cmt">//--- Set order ticket order.Type = orderInfo.OrderType(); class=class="str">"cmt">//--- Set order type order.State = orderInfo.State(); class=class="str">"cmt">//--- Set order state order.MagicNumber = orderInfo.Magic(); class=class="str">"cmt">//--- Set magic number order.Lots = orderInfo.VolumeInitial(); class=class="str">"cmt">//--- Set initial volume order.OpenPrice = orderInfo.PriceOpen(); class=class="str">"cmt">//--- Set open price
◍ 挂单与持仓的止损止盈改写逻辑
在 MT5 的 EA 工程里,把历史订单或实时订单的结构体字段逐一回填,是后续统计与风控的前提。下面这段赋值把止损、止盈、到期时间、开平时间、品种、初始手数以及佣金都从 orderInfo 对象搬进自定义 order 结构,最后用 CalculateAndSetCommision 补算手续费。 修改止损止盈时不能一刀切:pending 单走 OrderModify,已成交持仓走 PositionModify。代码里用 order.State 判断——等于 ORDER_STATE_PLACED 改挂单,等于 ORDER_STATE_FILLED 改持仓,传入 NULL 则沿用原 SL/TP。 外汇与贵金属杠杆高,modify 失败不会抛异常只回 false,实盘里必须判返回值再决定要不要重发指令,否则可能留下未保护仓位。开 MT5 把这段塞进你的订单管理类,跑一轮回测看改单成功率。
order.StopLoss = orderInfo.StopLoss(); class=class="str">"cmt">//--- Set Stop Loss order.TakeProfit = orderInfo.TakeProfit(); class=class="str">"cmt">//--- Set Take Profit order.Expiration = orderInfo.TimeExpiration(); class=class="str">"cmt">//--- Set expiration time order.Comment = orderInfo.Comment(); class=class="str">"cmt">//--- Set comment order.OpenTime = orderInfo.TimeSetup(); class=class="str">"cmt">//--- Set open time order.CloseTime = OrderCloseTime(order.Ticket); class=class="str">"cmt">//--- Set close time order.SymbolCode = orderInfo.Symbol(); class=class="str">"cmt">//--- Set symbol code order.TradeVolume = orderInfo.VolumeInitial(); class=class="str">"cmt">//--- Set trade volume CalculateAndSetCommision(order); class=class="str">"cmt">//--- Calculate and set commission } class=class="str">"cmt">//--- Modify order’s Stop Loss or Take Profit class="kw">static class="type">bool modify(class="type">ulong ticket, class="type">class="kw">double stopLoss = NULL, class="type">class="kw">double takeProfit = NULL) { CTrade trade; class=class="str">"cmt">//--- Declare trade object Order* order = OrderRepository::getByTicket(ticket); class=class="str">"cmt">//--- Retrieve order by ticket class="type">class="kw">double price = order.OpenPrice; class=class="str">"cmt">//--- Set price to open price stopLoss = (stopLoss == NULL) ? order.StopLoss : stopLoss; class=class="str">"cmt">//--- Use existing SL if null takeProfit = (takeProfit == NULL) ? order.TakeProfit : takeProfit; class=class="str">"cmt">//--- Use existing TP if null class="type">class="kw">datetime expiration = order.Expiration; class=class="str">"cmt">//--- Set expiration class="type">bool result = false; class=class="str">"cmt">//--- Initialize result as false if (order.State == ORDER_STATE_PLACED) { class=class="str">"cmt">//--- Check if order is pending result = trade.OrderModify(ticket, price, stopLoss, takeProfit, ORDER_TIME_SPECIFIED, expiration, class="num">0); class=class="str">"cmt">//--- Modify pending order } else if (order.State == ORDER_STATE_FILLED) { class=class="str">"cmt">//--- Check if order is filled result = trade.PositionModify(ticket, stopLoss, takeProfit); class=class="str">"cmt">//--- Modify position } if (CheckPointer(order) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check if order is dynamic class="kw">delete(order); class=class="str">"cmt">//--- Delete order object } class="kw">return result; class=class="str">"cmt">//--- Return modification result }
「用静态方法一次性捞出挂单与净仓」
在 MT5 的 EA 或指标里,想同时掌握当前挂单和已开净仓,最省事的做法是写一个静态工厂方法,按 magic、订单类型、交易品种三层过滤后塞进同一个集合。下面这段代码给出的是具体实现骨架,你在自己的 OrderCollection 类里补上 Add / fetchSelected 就能直接编译跑。 [CODE] static OrderCollection* GetOpenOrders(int magic = NULL, int type = NULL, string symbolCode = NULL) { OrderCollection* orders = new OrderCollection(); for (int orderIndex = 0; orderIndex < OrdersTotal(); orderIndex++) { bool orderSelected = OrderSelect(OrderGetTicket(orderIndex)); if (orderSelected) { Order* order = new Order(false); OrderRepository::fetchSelected(order);
| if ((magic == NULL | magic == order.MagicNumber) && |
|---|---|
| (type == NULL | type == order.Type) && |
| (symbolCode == NULL | symbolCode == order.SymbolCode)) { |
orders.Add(order); } else { if (CheckPointer(order) == POINTER_DYNAMIC) { delete(order); } } } } int total = PositionsTotal(); for (int i = total - 1; i >= 0; i--) { ulong position_ticket = PositionGetTicket(i); string position_symbol = PositionGetString(POSITION_SYMBOL); long position_magicNumber = PositionGetInteger(POSITION_MAGIC); double volume = PositionGetDouble(POSITION_VOLUME); double open_price = PositionGetDouble(POSITION_PRICE_OPEN); datetime open_time = (datetime)PositionGetInteger(POSITION_TIME); [/CODE] 逐行拆解:函数开头 new 一个 OrderCollection 作为容器;OrdersTotal 返回挂单总数,用 OrderGetTicket 按序号取 ticket 再 OrderSelect 选中,选中失败就跳过。Order 对象建好后由 fetchSelected 填字段,三重 NULL 判断决定要不要收进集合,不符合的立刻 delete 防内存泄漏。 下半段处理净仓系统里的已开仓位:PositionsTotal 拿到总数,循环从尾到头(i 从 total-1 到 0)倒序遍历,依次取 ticket、品种、magic、成交量、开仓价、开仓时间。倒序是为了避免在删除或改动仓位时索引错位。外汇与贵金属杠杆高,实盘调用前先在策略测试器用历史数据验证集合内容是否符合预期,可能比直接上真金白银更稳。
class="kw">static OrderCollection* GetOpenOrders(class="type">int magic = NULL, class="type">int type = NULL, class="type">class="kw">string symbolCode = NULL) { OrderCollection* orders = new OrderCollection(); for (class="type">int orderIndex = class="num">0; orderIndex < OrdersTotal(); orderIndex++) { class="type">bool orderSelected = OrderSelect(OrderGetTicket(orderIndex)); if (orderSelected) { Order* order = new Order(false); OrderRepository::fetchSelected(order); if ((magic == NULL || magic == order.MagicNumber) && (type == NULL || type == order.Type) && (symbolCode == NULL || symbolCode == order.SymbolCode)) { orders.Add(order); } else { if (CheckPointer(order) == POINTER_DYNAMIC) { class="kw">delete(order); } } } } class="type">int total = PositionsTotal(); for (class="type">int i = total - class="num">1; i >= class="num">0; i--) { class="type">ulong position_ticket = PositionGetTicket(i); class="type">class="kw">string position_symbol = PositionGetString(POSITION_SYMBOL); class="type">long position_magicNumber = PositionGetInteger(POSITION_MAGIC); class="type">class="kw">double volume = PositionGetDouble(POSITION_VOLUME); class="type">class="kw">double open_price = PositionGetDouble(POSITION_PRICE_OPEN); class="type">class="kw">datetime open_time = (class="type">class="kw">datetime)PositionGetInteger(POSITION_TIME);
按魔术码与品种过滤持仓并封装订单对象
在 EA 遍历持仓时,先通过 PositionGetInteger(POSITION_TYPE) 拿到持仓方向,再比对 MagicNumber 是否等于脚本自身的标识。只有魔术码匹配,才会 new 一个 Order 对象把 ticket、手数、开仓价、开仓时间等字段逐一搬进去,避免把别的策略单子误纳入管理。 下面这段逻辑里,Buy 持仓映射成 ORDER_TYPE_BUY,Sell 持仓映射成 ORDER_TYPE_SELL;随后用 magic、type、symbolCode 三个可空参数做二次过滤。三者任一为 NULL 就表示该维度不限制,全部命中才 orders.Add(order),否则立刻 delete 掉临时对象释放内存。 实盘里若 MagicNumber 设错一位,EA 可能完全扫不到自己的单,回测中这类静默失败往往要跑完几百根 bar 才被发现。外汇与贵金属杠杆高,扫单逻辑写错会直接引发重复平仓或漏平,建议开 MT5 用策略测试器单步跟一遍这段。
class="type">ENUM_POSITION_TYPE positionType = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- Get position type if (position_magicNumber == MagicNumber) { class=class="str">"cmt">//--- Check matching magic number Order* order = new Order(false); class=class="str">"cmt">//--- Create new order object order.Ticket = position_ticket; class=class="str">"cmt">//--- Set order ticket if (positionType == POSITION_TYPE_BUY) { class=class="str">"cmt">//--- Check if Buy position order.Type = ORDER_TYPE_BUY; class=class="str">"cmt">//--- Set Buy type } else if (positionType == POSITION_TYPE_SELL) { class=class="str">"cmt">//--- Check if Sell position order.Type = ORDER_TYPE_SELL; class=class="str">"cmt">//--- Set Sell type } order.Lots = volume; class=class="str">"cmt">//--- Set order volume order.TradeVolume = volume; class=class="str">"cmt">//--- Set trade volume order.OpenPrice = open_price; class=class="str">"cmt">//--- Set open price order.OpenTime = open_time; class=class="str">"cmt">//--- Set open time order.MagicNumber = position_magicNumber; class=class="str">"cmt">//--- Set magic number order.SymbolCode = position_symbol; class=class="str">"cmt">//--- Set symbol code if ((magic == NULL || magic == order.MagicNumber) && (type == NULL || type == order.Type) && (symbolCode == NULL || symbolCode == order.SymbolCode)) { class=class="str">"cmt">//--- Filter by magic, type, symbol orders.Add(order); class=class="str">"cmt">//--- Add order to collection } else { if (CheckPointer(order) == POINTER_DYNAMIC) { order.Ticket = -class="num">1; class=class="str">"cmt">//--- Invalidate ticket class="kw">delete(order); class=class="str">"cmt">//--- Delete unused order object } } }
◍ 用 ExecuteOpenBuy 把买单塞进交易池
这段静态函数负责把一条 Buy 指令真正丢进 MT5 执行通道。它先给订单票号赋 ULONG_MAX 占位,代表尚未成交的无效状态,再逐项填 MqlTradeRequest 结构。 ticket 初始化为 ULONG_MAX 是个好习惯:后续若 OrderSend 失败,调用方靠这个值就能立刻识别出单没进去,不用再去查交易池。 request.action 写死 TRADE_ACTION_DEAL,说明这是市价成交而非挂单;price 取 Ask_LibFunc() 而非直接 Ask,意味着你可能在函数里做了点差过滤或报价延迟处理。 type_filling 由外部 OrderFillingType 强转而来,不同券商对 FOK / IOC / RETURN 的支持不一样,跑之前先在策略测试器里确认当前品种允许的填充策略,否则 retcode 可能直接返回 10030。 成功分支里把 IsAwaitingDealExecution 置 true,是把‘已发单但成交回执未回’和‘完全没发单’区分开,后续轮询逻辑就靠这个布尔量判断该不该查 result.order。
class="kw">static class="type">ulong ExecuteOpenBuy(Order* order) { class="type">ulong orderTicket = ULONG_MAX; class="type">MqlTradeRequest request = {}; class="type">MqlTradeResult result = {}; request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = order.Lots; request.type = ORDER_TYPE_BUY; request.price = Ask_LibFunc(); request.deviation = MaxDeviationSlippage; request.magic = MagicNumber; request.comment = order.Comment; request.type_filling = (ENUM_ORDER_TYPE_FILLING)OrderFillingType; ResetLastError(); if (OrderSend(request, result)) { if (result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED) { orderTicket = result.order; order.Ticket = orderTicket; order.IsAwaitingDealExecution = true; } else { Print(StringFormat("OrderSend: retcode=%u", result.retcode)); } } else {
「卖单执行里的报错与 ticket 回传」
在 MT5 的 EA 封装里,卖单函数 ExecuteOpenSell 和买单对称,但几个字段决定它能不能真实成交:request.type 必须置为 ORDER_TYPE_SELL,request.price 取 Bid_LibFunc() 返回的实时买价,而不是 Ask。若 deviation(最大滑点)设得太小,在黄金跳空时段 OrderSend 可能直接返回 TRADE_RETCODE_DONE 以外的代码。 函数开头先把 orderTicket 初始化成 ULONG_MAX,这是个无效票号标记;只有 result.retcode 等于 TRADE_RETCODE_DONE 或 TRADE_RETCODE_PLACED 时,才把 result.order 写回 order.Ticket,并把 IsAwaitingDealExecution 置 true。其余情况走 else 分支,打印错误日志后依然返回 ULONG_MAX。 调用层拿到 ULONG_MAX 就可以判定下单失败,不用再解析 retcode。外汇与贵金属杠杆高,滑点参数设错可能让策略在大数据行情里连续丢单,建议开 MT5 用黄金 M5 回测时把 MaxDeviationSlippage 从 10 调到 30 点观察拒单率变化。
Print(StringFormat("OrderSend: error %d: %s", GetLastError(), GetErrorDescription(result.retcode))); class=class="str">"cmt">//--- Log error } class="kw">return orderTicket; class=class="str">"cmt">//--- Return order ticket } class=class="str">"cmt">//--- Execute Sell order class="kw">static class="type">ulong ExecuteOpenSell(Order* order) { class="type">ulong orderTicket = ULONG_MAX; class=class="str">"cmt">//--- Initialize ticket as invalid class="type">MqlTradeRequest request = {}; class=class="str">"cmt">//--- Declare trade request class="type">MqlTradeResult result = {}; class=class="str">"cmt">//--- Declare trade result request.action = TRADE_ACTION_DEAL; class=class="str">"cmt">//--- Set action to deal request.symbol = Symbol(); class=class="str">"cmt">//--- Set symbol to current request.volume = order.Lots; class=class="str">"cmt">//--- Set volume request.type = ORDER_TYPE_SELL; class=class="str">"cmt">//--- Set Sell type request.price = Bid_LibFunc(); class=class="str">"cmt">//--- Set price to Bid request.deviation = MaxDeviationSlippage; class=class="str">"cmt">//--- Set maximum slippage request.magic = MagicNumber; class=class="str">"cmt">//--- Set magic number request.comment = order.Comment; class=class="str">"cmt">//--- Set order comment request.type_filling = (ENUM_ORDER_TYPE_FILLING)OrderFillingType; class=class="str">"cmt">//--- Set filling type ResetLastError(); class=class="str">"cmt">//--- Clear last error if (OrderSend(request, result)) { class=class="str">"cmt">//--- Send trade request if (result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED) { class=class="str">"cmt">//--- Check success orderTicket = result.order; class=class="str">"cmt">//--- Store order ticket order.Ticket = orderTicket; class=class="str">"cmt">//--- Update order ticket order.IsAwaitingDealExecution = true; class=class="str">"cmt">//--- Flag awaiting execution } else {
对冲账户下的部分平仓与回执处理
在 MT5 对冲账户里平掉某一笔持仓,不能直接用整平接口糊弄,得先按持仓索引倒序遍历 PositionsTotal(),用 CPositionInfo::SelectByIndex 锁定 ticket 一致的仓。 找到目标后调用 CTrade::PositionClosePartial,手数必须 NormalizeDouble(order.Lots, 2) 保留两位小数,滑点上限传 MaxDeviationSlippage。返回 true 只代表订单挂进去了,不代表成交。 真正成败看 m_trade.ResultRetcode():等于 TRADE_RETCODE_DONE 或 TRADE_RETCODE_PLACED 才算成功,此时用 ResultOrder() 拿新订单 ticket,把 order.Ticket 更新掉,并置 IsAwaitingDealExecution = true 等待 deal 回执。外汇与贵金属杠杆高,部分平仓也可能因流动性在滑点内拒单,需在日志里打 MagicNumber 方便排查 EA 行为。
class="kw">static class="type">bool ClosePosition(Order* order) { CPositionInfo m_position; class=class="str">"cmt">//--- Declare position info object CTrade m_trade; class=class="str">"cmt">//--- Declare trade object class="type">bool foundPosition = false; class=class="str">"cmt">//--- Initialize position found flag for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate positions if (m_position.SelectByIndex(i)) { class=class="str">"cmt">//--- Select position by index if (m_position.Ticket() == order.Ticket) { class=class="str">"cmt">//--- Check matching ticket foundPosition = true; class=class="str">"cmt">//--- Set position found class="type">uint returnCode = class="num">0; class=class="str">"cmt">//--- Initialize class="kw">return code if (m_trade.PositionClosePartial(order.Ticket, NormalizeDouble(order.Lots, class="num">2), MaxDeviationSlippage)) { class=class="str">"cmt">//--- Attempt partial close returnCode = m_trade.ResultRetcode(); class=class="str">"cmt">//--- Get class="kw">return code if (returnCode == TRADE_RETCODE_DONE || returnCode == TRADE_RETCODE_PLACED) { class=class="str">"cmt">//--- Check success class="type">ulong orderTicket = m_trade.ResultOrder(); class=class="str">"cmt">//--- Get new order ticket order.Ticket = orderTicket; class=class="str">"cmt">//--- Update order ticket order.IsAwaitingDealExecution = true; class=class="str">"cmt">//--- Flag awaiting execution Print(StringFormat("Successfully created a close order(%d) by EA(%d). Awaiting execution.", orderTicket, MagicNumber)); class=class="str">"cmt">//--- Log success
◍ 从成交历史里捞回已平仓位
这段静态方法 GetLastClosedOrders 负责把当前品种、当前魔术码(或魔术码为0)的已平仓成交捞出来。它先用 HistorySelect(0, TimeCurrent()) 把从账户开启到当下的整段交易历史拉进内存,若调用失败则集合为空。 循环从 HistoryDealsTotal()-1 倒序走到 0,每笔成交先取 dealId、magicNumber 和 symbol。过滤逻辑很直白:magicNumber 既不是本 EA 的 MagicNumber 也不是 0,或者 symbol 不等于 Symbol(),直接 continue 跳过。 真正判定“平仓”靠 DEAL_ENTRY == DEAL_ENTRY_OUT。拿到成交时间 closetime 后,如果传入的 startDatetime 比 closetime 更晚,说明已经翻过了时间窗口,break 跳出循环。这意味着你传一个具体起点时间,就能只取该时间之后的平仓记录,对复盘最近 N 小时离场行为有用。 外汇与贵金属杠杆高,历史抓取仅用于统计与验证,不代表任何未来胜负倾向。把这段代码贴进 MT5 的 include 类里,配合 Print 输出 lastClosedOrders 长度,能立刻看到过滤是否按预期生效。
class="kw">static OrderCollection* GetLastClosedOrders(class="type">class="kw">datetime startDatetime = NULL) { OrderCollection* lastClosedOrders = new OrderCollection(); class="type">long positionIds[]; if (HistorySelect(class="num">0, TimeCurrent())) { for (class="type">int i = HistoryDealsTotal() - class="num">1; i >= class="num">0; i--) { class="type">ulong dealId = HistoryDealGetTicket(i); class="type">long magicNumber = HistoryDealGetInteger(dealId, DEAL_MAGIC); class="type">class="kw">string symbol = HistoryDealGetString(dealId, DEAL_SYMBOL); if ((magicNumber != MagicNumber && magicNumber != class="num">0) || symbol != Symbol()) { class="kw">continue; } if (HistoryDealGetInteger(dealId, DEAL_ENTRY) == DEAL_ENTRY_OUT) { class="type">class="kw">datetime closetime = (class="type">class="kw">datetime)HistoryDealGetInteger(dealId, DEAL_TIME); if (startDatetime > closetime) { break; }
「从成交记录反推持仓闭环」
在 MT5 历史成交里,一笔持仓往往对应多张 deal 记录(开仓 + 若干平仓),直接遍历容易重复统计。上面这段逻辑先用 DEAL_POSITION_ID 把同一持仓的 ID 收集进数组,遇到重复就 continue 跳过,保证每个持仓只处理一次。 收集完 positionIds 后,用 HistorySelectByPosition 按持仓 ID 拉取该笔的完整历史,再新建 Order 对象并初始化 currentOutVolume=0 来累计已平量。内层循环里靠 DEAL_ENTRY 区分是 DEAL_ENTRY_IN(开仓)还是出场,开仓侧才去取 openTime、openPrice、lots。 注意 order.Ticket==0 的判断:它只在第一次碰到该持仓的开仓 deal 时,把 DEAL_ORDER 写进 order.Ticket,避免后续平仓 deal 覆盖掉主单号。外汇与贵金属杠杆高,历史回测中持仓拆分和平仓顺序可能和实盘不一致,这套去重逻辑在统计胜率或持仓时长时建议先开 MT5 跑一遍验证。
class="type">long positionId = HistoryDealGetInteger(dealId, DEAL_POSITION_ID); class=class="str">"cmt">//--- Get position ID for (class="type">int pi = class="num">0; pi < ArraySize(positionIds); pi++) { class=class="str">"cmt">//--- Check existing IDs if (positionIds[pi] == positionId) { class=class="str">"cmt">//--- Skip duplicates class="kw">continue; } } class="type">int size = ArraySize(positionIds); class=class="str">"cmt">//--- Get current ID array size ArrayResize(positionIds, size + class="num">1); class=class="str">"cmt">//--- Add new ID positionIds[size] = positionId; class=class="str">"cmt">//--- Store position ID } } } for (class="type">int i = class="num">0; i < ArraySize(positionIds); i++) { class=class="str">"cmt">//--- Process each position if (HistorySelectByPosition(positionIds[i])) { class=class="str">"cmt">//--- Select position history Order* order = new Order(false); class=class="str">"cmt">//--- Create new order object class="type">class="kw">double currentOutVolume = class="num">0; class=class="str">"cmt">//--- Track closed volume for (class="type">int j = class="num">0; j < HistoryDealsTotal(); j++) { class=class="str">"cmt">//--- Iterate deals class="type">ulong ticket = HistoryDealGetTicket(j); class=class="str">"cmt">//--- Get deal ticket if (HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN) { class=class="str">"cmt">//--- Check if open deal class="type">class="kw">datetime openTime = (class="type">class="kw">datetime)HistoryDealGetInteger(ticket, DEAL_TIME); class=class="str">"cmt">//--- Get open time class="type">class="kw">double openPrice = HistoryDealGetDouble(ticket, DEAL_PRICE); class=class="str">"cmt">//--- Get open price class="type">class="kw">double lots = HistoryDealGetDouble(ticket, DEAL_VOLUME); class=class="str">"cmt">//--- Get volume if (order.Ticket == class="num">0) { class=class="str">"cmt">//--- Check if first deal order.Ticket = HistoryDealGetInteger(ticket, DEAL_ORDER); class=class="str">"cmt">//--- Set order ticket
反手平仓与加仓均价怎么在 HistoryDeal 里拼出来
遍历成交历史时,先靠 HistoryDealGetInteger 取 DEAL_TYPE,决定反向平仓的挂单方向:原单是买就置 SELL,原单是卖就置 BUY,类型异常直接 Alert 弹窗报「Unknown order.Type」。 开仓阶段若同方向已存在记录,用 (原均价×原手数 + 新价×新手数) / (原手数+新手数) 算加权平均开仓价,再把 Lots 累加,这段逻辑决定加仓后的成本线。 平仓分支靠 DEAL_ENTRY_OUT 识别,首次平仓时把 DEAL_TIME 转 datetime 写进 CloseTime,并记下 ClosePrice 与 currentOutVolume;外汇与贵金属波动剧烈,手数累加和均价刷新若没校验精度,可能在点差扩大时偏移成本,建议开 MT5 用真实回测核对。
class="type">long dealType = HistoryDealGetInteger(ticket, DEAL_TYPE); class=class="str">"cmt">//--- Get deal type if (dealType == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Check if Buy order.Type = ORDER_TYPE_SELL; class=class="str">"cmt">//--- Set Sell type(reversed for close) } else if (dealType == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- Check if Sell order.Type = ORDER_TYPE_BUY; class=class="str">"cmt">//--- Set Buy type(reversed for close) } else { Alert("Unknown order.Type in GetLastClosedOrder"); class=class="str">"cmt">//--- Log unknown type } order.OpenTime = openTime; class=class="str">"cmt">//--- Set open time order.OpenPrice = openPrice; class=class="str">"cmt">//--- Set open price order.Lots = lots; class=class="str">"cmt">//--- Set volume } else { class="type">class="kw">double averagePrice = ((order.OpenPrice * order.Lots) + (openPrice * lots)) / (order.Lots + lots); class=class="str">"cmt">//--- Calculate average price order.Lots = order.Lots + lots; class=class="str">"cmt">//--- Add volume order.OpenPrice = averagePrice; class=class="str">"cmt">//--- Update open price } } else if (HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT) { class=class="str">"cmt">//--- Check if close deal class="type">class="kw">double dealLots = HistoryDealGetDouble(ticket, DEAL_VOLUME); class=class="str">"cmt">//--- Get close volume class="type">class="kw">double dealClosePrice = HistoryDealGetDouble(ticket, DEAL_PRICE); class=class="str">"cmt">//--- Get close price if (order.CloseTime == class="num">0) { class=class="str">"cmt">//--- Check if first close order.CloseTime = (class="type">class="kw">datetime)HistoryDealGetInteger(ticket, DEAL_TIME); class=class="str">"cmt">//--- Set close time order.ClosePrice = dealClosePrice; class=class="str">"cmt">//--- Set close price currentOutVolume = dealLots; class=class="str">"cmt">//--- Set initial close volume
◍ 平仓均价与开仓分支的底层写法
这段逻辑处理的是部分平仓场景下的订单对象更新:当一笔平仓成交回来时,若与该订单已平量叠加,需要重算整体平仓均价而非简单覆盖。代码里用 (order.ClosePrice * currentOutVolume + dealClosePrice * dealLots) / (currentOutVolume + dealLots) 做加权平均,currentOutVolume 每次累加 dealLots,CloseTime 取成交回报的 DEAL_TIME。外汇与贵金属保证金交易杠杆高,滑点可能让实际均价偏离预期,建议在 MT5 策略测试器里打印 averagePrice 对照成交明细。 开仓函数 OpenOrder 走 switch 分流 ORDER_TYPE_BUY / ORDER_TYPE_SELL,分别调 ExecuteOpenBuy / ExecuteOpenSell,成功判定为 ticketId != ULONG_MAX。这里没有重试与冲突处理,实盘若遇报价冻结可能直接返回失败,需要自行补熔断。 佣金初始化放在 CalculateAndSetCommision,先把 Commission 与 CommissionInPips 置 0.0,后续再按品种合约规模填值。复制下面片段到 EA 工程里,能直接看到平仓均价刷新路径。
} else { class="type">class="kw">double averagePrice = ((order.ClosePrice * currentOutVolume) + (dealClosePrice * dealLots)) / (currentOutVolume + dealLots); class=class="str">"cmt">//--- Calculate average close price order.CloseTime = (class="type">class="kw">datetime)HistoryDealGetInteger(ticket, DEAL_TIME); class=class="str">"cmt">//--- Update close time order.ClosePrice = averagePrice; class=class="str">"cmt">//--- Update close price currentOutVolume += dealLots; class=class="str">"cmt">//--- Add close volume } } } lastClosedOrders.Add(order); class=class="str">"cmt">//--- Add order to collection } } class="kw">return lastClosedOrders; class=class="str">"cmt">//--- Return closed orders collection } class=class="str">"cmt">//--- Open order(Buy or Sell) class="kw">static class="type">bool OpenOrder(Order* order) { class="type">class="kw">double price = NULL; class=class="str">"cmt">//--- Initialize price class="type">ulong ticketId = -class="num">1; class=class="str">"cmt">//--- Initialize ticket class="kw">switch (order.Type) { case ORDER_TYPE_BUY: ticketId = ExecuteOpenBuy(order); class=class="str">"cmt">//--- Execute Buy order break; case ORDER_TYPE_SELL: ticketId = ExecuteOpenSell(order); class=class="str">"cmt">//--- Execute Sell order break; } class="type">bool success = ticketId != ULONG_MAX; class=class="str">"cmt">//--- Check if order was opened if (success) { Print(StringFormat("Successfully opened an order(%d) by EA(%d)", ticketId, MagicNumber)); class=class="str">"cmt">//--- Log success } class="kw">return success; class=class="str">"cmt">//--- Return true if successful } class=class="str">"cmt">//--- Calculate and set commission for order class="kw">static class="type">void CalculateAndSetCommision(Order& order) { order.Commission = class="num">0.0; class=class="str">"cmt">//--- Initialize commission order.CommissionInPips = class="num">0.0; class=class="str">"cmt">//--- Initialize commission in pips
「把佣金拆进订单类并归组管票」
回测里佣金若只写个固定值,很容易低估高频剥头皮的磨损。下面这段把每笔佣金拆成三部分:每笔固定往返 2.0 单位、按手数百分比计收、以及每标准手固定额,三者相加写入 order.Commission。 只有当手数大于 1e-5 且佣金大于 1e-5 时才计算 pip 化佣金:order.Commission / (order.Lots * UnitsOneLot * PipPoint)。这样小于 0.00001 手的碎单不会污染统计。 订单分组用 OrderGroupData 类承载,内部维护 ulong 数组 OrderTicketIds。Add 方法先取当前大小,ArrayResize 扩 1,再把 ticket 写到最后一位;Remove 用 counter 与 counterFound 双指针,遇到目标 ticket 只增 counterFound 并 continue,其余前移覆盖,实现 O(n) 原地删。 开 MT5 把这段塞进你的回测框架,重点看 CommissionInPips 在 0.1 手与 1.0 手下的差值,外汇与贵金属杠杆高,佣金占比波动可能直接改变策略盈亏边界。
order.Commission = class="num">2.0 * CommissionAmountPerTrade + class=class="str">"cmt">//--- Add roundtrip fixed commission CommissionPercentagePerLot * order.Lots * UnitsOneLot + class=class="str">"cmt">//--- Add percentage commission CommissionAmountPerLot * order.Lots; class=class="str">"cmt">//--- Add per-lot commission if (order.Lots > class="num">1.0e-5 && order.Commission > class="num">1.0e-5) { class=class="str">"cmt">//--- Check valid volume and commission order.CommissionInPips = order.Commission / (order.Lots * UnitsOneLot * PipPoint); class=class="str">"cmt">//--- Calculate commission in pips } } }; class=class="str">"cmt">//--- Define class for grouping order tickets class OrderGroupData { class="kw">public: class="type">ulong OrderTicketIds[]; class=class="str">"cmt">//--- Store array of order ticket IDs class=class="str">"cmt">//--- Default constructor class="type">void OrderGroupData() {} class=class="str">"cmt">//--- Initialize empty group class=class="str">"cmt">//--- Copy constructor class="type">void OrderGroupData(OrderGroupData* ordergroupdata) {} class=class="str">"cmt">//--- Initialize from existing group(empty) class=class="str">"cmt">//--- Add ticket to group class="type">void Add(class="type">ulong ticketId) { class="type">int size = ArraySize(OrderTicketIds); class=class="str">"cmt">//--- Get current array size ArrayResize(OrderTicketIds, size + class="num">1); class=class="str">"cmt">//--- Resize array OrderTicketIds[size] = ticketId; class=class="str">"cmt">//--- Store ticket at last index } class=class="str">"cmt">//--- Remove ticket from group class="type">void Remove(class="type">ulong ticketId) { class="type">int size = ArraySize(OrderTicketIds); class=class="str">"cmt">//--- Get current array size class="type">int counter = class="num">0; class=class="str">"cmt">//--- Track new array position class="type">int counterFound = class="num">0; class=class="str">"cmt">//--- Track removed tickets for (class="type">int i = class="num">0; i < size; i++) { if (OrderTicketIds[i] == ticketId) { class=class="str">"cmt">//--- Check matching ticket counterFound++; class=class="str">"cmt">//--- Increment found count class="kw">continue; class=class="str">"cmt">//--- Skip to next } else { OrderTicketIds[counter] = OrderTicketIds[i]; class=class="str">"cmt">//--- Shift ticket
订单组哈希表的链式节点结构
在订单组管理里,用哈希表做索引时,每个桶里挂的不是裸数据,而是一个封装好的链表节点。下面这段类定义把键、值、下一节点指针打包成 OrderGroupHashEntry,让哈希冲突时能用 _next 顺链查找。 默认构造函数把 _key、_val、_next 全置为 NULL,相当于空节点占位;带参构造函数接收 string key 与 OrderGroupData* val,只赋值不连链,_next 仍留空。 析构函数是重点:先判 _val 非空且 CheckPointer(_val)==POINTER_DYNAMIC,确认是动态对象才 delete(_val)。外汇与贵金属 EA 跑长周期时,订单组对象若漏删会缓慢吃内存,这个判断能避开删栈对象的崩溃。 对照上一段的 OrderGroupData,counterFound>0 才 ArrayResize 收缩ticket数组;这里节点析构独立回收 value,两套生命周期别混为一谈。开 MT5 把这段塞进你的订单管理类,断点看 _next 在冲突时是否如期挂上。
class OrderGroupHashEntry { class="kw">public: class="type">class="kw">string _key; class=class="str">"cmt">//--- Store entry key OrderGroupData* _val; class=class="str">"cmt">//--- Store order group data OrderGroupHashEntry* _next; class=class="str">"cmt">//--- Point to next entry class=class="str">"cmt">//--- Default constructor OrderGroupHashEntry() { _key = NULL; class=class="str">"cmt">//--- Set key to null _val = NULL; class=class="str">"cmt">//--- Set value to null _next = NULL; class=class="str">"cmt">//--- Set next to null } class=class="str">"cmt">//--- Constructor with key and value OrderGroupHashEntry(class="type">class="kw">string key, OrderGroupData* val) { _key = key; class=class="str">"cmt">//--- Set key _val = val; class=class="str">"cmt">//--- Set value _next = NULL; class=class="str">"cmt">//--- Set next to null } class=class="str">"cmt">//--- Destructor ~OrderGroupHashEntry() { if (_val != NULL && CheckPointer(_val) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check if value is dynamic class="kw">delete(_val); class=class="str">"cmt">//--- Delete value } } };
◍ 订单分组哈希表的成员与初始化
在 MT5 的订单分组管理里,用哈希表把同组订单快速归类,先得看清结构体内这几个核心字段。_hashSlots 记录桶数量,_hashEntryCount 跟踪实际插入的键值对数,_resizeThreshold 控制何时触发扩容,_buckets 是存放链地址法节点的动态数组,_adoptValues 决定析构时是否释放值对象,后面 _foundIndex / _foundEntry / _foundPrev 则是查找过程中的游标。 init 函数接收 size 与 adoptValues 两个参数,先把槽位和计数清零,再赋值 adoption 标志,最后调 rehash(size) 真正分配桶空间。这里 size 若传 0,rehash 内部通常会按最小基数(如 16)起建,避免空表裸奔。 hash 函数用经典 djb2 思路:合法字符串先以 5381 作为初值,再逐字符做 h = h*33 + c。下面这段截取的声明与初始化代码,可直接贴进 MQ5 头文件对照字段含义。 别在 EA 里手改 _hashSlots 却不重算 _resizeThreshold,扩容判定会失效,分组查找可能退化为 O(n) 线性扫桶。
class="type">uint _hashSlots; class=class="str">"cmt">//--- Store number of hash slots class="type">int _resizeThreshold; class=class="str">"cmt">//--- Store resize threshold class="type">int _hashEntryCount; class=class="str">"cmt">//--- Track number of entries OrderGroupHashEntry* _buckets[]; class=class="str">"cmt">//--- Store hash buckets class="type">bool _adoptValues; class=class="str">"cmt">//--- Flag value adoption class="type">uint _foundIndex; class=class="str">"cmt">//--- Store found index OrderGroupHashEntry* _foundEntry; class=class="str">"cmt">//--- Store found entry OrderGroupHashEntry* _foundPrev; class=class="str">"cmt">//--- Store previous entry class=class="str">"cmt">//--- Initialize hash map class="type">void init(class="type">uint size, class="type">bool adoptValues) { _hashSlots = class="num">0; class=class="str">"cmt">//--- Set initial slots to class="num">0 _hashEntryCount = class="num">0; class=class="str">"cmt">//--- Set initial entry count to class="num">0 _adoptValues = adoptValues; class=class="str">"cmt">//--- Set value adoption flag rehash(size); class=class="str">"cmt">//--- Resize hash map } class=class="str">"cmt">//--- Calculate hash for key class="type">uint hash(class="type">class="kw">string s) { class="type">uchar c[]; class=class="str">"cmt">//--- Declare character array class="type">uint h = class="num">0; class=class="str">"cmt">//--- Initialize hash if (s != NULL) { class=class="str">"cmt">//--- Check if key is valid h = class="num">5381; class=class="str">"cmt">//--- Set initial hash value class="type">int n = StringToCharArray(s, c); class=class="str">"cmt">//--- Convert class="type">class="kw">string to chars
「哈希表的查找与扩容底层实现」
在订单分组管理里,用字符串 key 定位记录不能靠线性扫描,否则品种一多延迟就崩。下面这段 MT5 代码把哈希索引、桶遍历和动态扩容三件事压在同一个类里,值得直接抄进你的 EA 工程。
哈希函数把字符串逐字符左移累加,最后对槽位数取模:h = ((h << 5) + h) + c[i] 等价于 h*33 累加,分布较均匀;返回 h % _hashSlots 决定落哪个桶。若你的 _hashSlots 设成 64,那么理论冲突概率随键数上升而明显增加,实测 500 个品种名时单桶链长可能到 7~8。
find() 从桶头往后走链表,命中即存 _foundEntry 并记 _foundPrev,方便后续删除不重算。注意原代码判断 if (_foundIndex <= _hashSlots) 用了小于等于,而数组下标最大只能是 _hashSlots-1,这是个越界隐患,复制时建议改成 < _hashSlots。
rehash() 开头先把旧表存进 oldTable[] 本地数组,再准备按 newSize 重建桶。外汇与贵金属交易高风险,这类底层结构在实盘前务必用策略测试器跑满不同品种数验证稳定性。
for (class="type">int i = class="num">0; i < n; i++) { h = ((h << class="num">5) + h) + c[i]; class=class="str">"cmt">//--- Update hash } } class="kw">return h % _hashSlots; class=class="str">"cmt">//--- Return hash modulo slots } class=class="str">"cmt">//--- Find entry by key class="type">bool find(class="type">class="kw">string keyName) { class="type">bool found = false; class=class="str">"cmt">//--- Initialize found flag _foundPrev = NULL; class=class="str">"cmt">//--- Set previous to null _foundIndex = hash(keyName); class=class="str">"cmt">//--- Calculate hash index if (_foundIndex <= _hashSlots) { class=class="str">"cmt">//--- Check valid index for (OrderGroupHashEntry* e = _buckets[_foundIndex]; e != NULL; e = e._next) { class=class="str">"cmt">//--- Iterate bucket if (e._key == keyName) { class=class="str">"cmt">//--- Check key match _foundEntry = e; class=class="str">"cmt">//--- Store found entry found = true; class=class="str">"cmt">//--- Set found flag break; class=class="str">"cmt">//--- Exit loop } _foundPrev = e; class=class="str">"cmt">//--- Update previous } } class="kw">return found; class=class="str">"cmt">//--- Return found status } class=class="str">"cmt">//--- Retrieve number of slots class="type">uint getSlots() { class="kw">return _hashSlots; class=class="str">"cmt">//--- Return slot count } class=class="str">"cmt">//--- Resize hash map class="type">bool rehash(class="type">uint newSize) { class="type">bool ret = false; class=class="str">"cmt">//--- Initialize class="kw">return flag OrderGroupHashEntry* oldTable[]; class=class="str">"cmt">//--- Declare old table
哈希表扩容时的再散列落点
在订单分组哈希表的扩容逻辑里,newSize 必须大于当前 getSlots() 返回的槽位数,否则直接返回 false 不做任何动作。这一步是防止缩容或等长重分配导致的数据覆盖风险。 真正执行时先对 _buckets 和 oldTable 两个数组做 ArrayResize,任意一次返回值不等于目标长度就置 ret=false 退出。MT5 的 ArrayResize 在堆内存紧张时可能失败,实盘跑高频订单管理得留意这个返回码。 扩容成功后把旧桶逐个拷进 oldTable,再把新 _buckets 全清为 NULL,并更新 _hashSlots 与 _resizeThreshold(后者取槽位数的 3/4)。随后按旧 hashCode 遍历 oldTable,用 hash(e._key) 重算落点写入新桶——这就是负载因子超 0.75 触发的再散列路径。 别把正态当圣经:阈值写死 3/4 未必适配你的订单峰值,EURUSD 剥头皮策略在新闻秒里可能瞬间灌满,建议开 MT5 把 _resizeThreshold 改成按历史最大挂单数动态算。
class="type">uint oldSize = _hashSlots; class=class="str">"cmt">//--- Store current size if (newSize <= getSlots()) { class=class="str">"cmt">//--- Check if resize is needed ret = false; class=class="str">"cmt">//--- Set failure } else if (ArrayResize(_buckets, newSize) != newSize) { class=class="str">"cmt">//--- Resize buckets ret = false; class=class="str">"cmt">//--- Set failure } else if (ArrayResize(oldTable, oldSize) != oldSize) { class=class="str">"cmt">//--- Resize old table ret = false; class=class="str">"cmt">//--- Set failure } else { class="type">uint i = class="num">0; class=class="str">"cmt">//--- Initialize index for (i = class="num">0; i < oldSize; i++) { class=class="str">"cmt">//--- Copy buckets oldTable[i] = _buckets[i]; class=class="str">"cmt">//--- Store old bucket } for (i = class="num">0; i < newSize; i++) { class=class="str">"cmt">//--- Clear new buckets _buckets[i] = NULL; class=class="str">"cmt">//--- Set to null } _hashSlots = newSize; class=class="str">"cmt">//--- Update slot count _resizeThreshold = (class="type">int)_hashSlots / class="num">4 * class="num">3; class=class="str">"cmt">//--- Set resize threshold for (class="type">uint oldHashCode = class="num">0; oldHashCode < oldSize; oldHashCode++) { class=class="str">"cmt">//--- Rehash entries OrderGroupHashEntry* next = NULL; class=class="str">"cmt">//--- Initialize next for (OrderGroupHashEntry* e = oldTable[oldHashCode]; e != NULL; e = next) { class=class="str">"cmt">//--- Iterate old bucket next = e._next; class=class="str">"cmt">//--- Store next entry class="type">uint newHashCode = hash(e._key); class=class="str">"cmt">//--- Calculate new hash
◍ 哈希表扩容与析构的内存动作
上面这段是订单分组哈希表在 rehash 收尾和对象销毁时的关键 C++/MQL5 代码。扩容时先把旧桶里的节点链到新桶:e._next 指向新桶当前头节点,再把 _buckets[newHashCode] 更新为该节点,最后把旧桶置 NULL,循环结束置 ret=true 表示成功。
默认构造只用 13 个槽位(init(13, false)),若传 adoptValues=true 则析构时要负责释放 value 指向的动态对象。析构函数双层循环遍历所有桶和冲突链:先暂存 nextEntry,若开启 adopt 且 value 是动态指针(CheckPointer==POINTER_DYNAMIC)就 delete entry._val,随后无条件 delete entry。
在 MT5 策略测试器里若你自定义了订单分组容器,记得确认 adopt 标志与构造参数一致;错配可能导致内存泄漏或重复释放,EA 跑几小时后可能报 'out of memory' 或直接崩溃。
e._next = _buckets[newHashCode]; class=class="str">"cmt">//--- Link to new bucket _buckets[newHashCode] = e; class=class="str">"cmt">//--- Store in new bucket } oldTable[oldHashCode] = NULL; class=class="str">"cmt">//--- Clear old bucket } ret = true; class=class="str">"cmt">//--- Set success } class="kw">return ret; class=class="str">"cmt">//--- Return resize result } class="kw">public: class=class="str">"cmt">//--- Default constructor OrderGroupHashMap() { init(class="num">13, false); class=class="str">"cmt">//--- Initialize with class="num">13 slots, no adoption } class=class="str">"cmt">//--- Constructor with adoption flag OrderGroupHashMap(class="type">bool adoptValues) { init(class="num">13, adoptValues); class=class="str">"cmt">//--- Initialize with class="num">13 slots } class=class="str">"cmt">//--- Constructor with size OrderGroupHashMap(class="type">int size) { init(size, false); class=class="str">"cmt">//--- Initialize with specified size, no adoption } class=class="str">"cmt">//--- Constructor with size and adoption OrderGroupHashMap(class="type">int size, class="type">bool adoptValues) { init(size, adoptValues); class=class="str">"cmt">//--- Initialize with size and adoption } class=class="str">"cmt">//--- Destructor ~OrderGroupHashMap() { for (class="type">uint i = class="num">0; i < _hashSlots; i++) { class=class="str">"cmt">//--- Iterate buckets OrderGroupHashEntry* nextEntry = NULL; class=class="str">"cmt">//--- Initialize next for (OrderGroupHashEntry* entry = _buckets[i]; entry != NULL; entry = nextEntry) { class=class="str">"cmt">//--- Iterate entries nextEntry = entry._next; class=class="str">"cmt">//--- Store next entry if (_adoptValues && entry._val != NULL && CheckPointer(entry._val) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check if value is dynamic class="kw">delete entry._val; class=class="str">"cmt">//--- Delete value } class="kw">delete entry; class=class="str">"cmt">//--- Delete entry
「哈希表里的订单分组存取逻辑」
这段 MQL5 片段实现了一个基于哈希桶的订单分组容器,核心动作是查、取、全取、存。ContainsKey 直接复用内部 find 方法,命中即返回 true,外部调用前不必再手写遍历。 Get 方法在 key 存在时把 _foundEntry._val 交给调用方,否则返回 NULL;意味着取数据前应用 ContainsKey 判空,否则解引用空指针会直接让 EA 在 MT5 里报错退出。 GetAllData 用双层循环扫 _hashSlots 个桶,对每个非空 _val 做 ArrayResize 后追加进 data[]。若分组数量在百级以内,这种动态扩容开销可忽略;桶数设得过小则冲突链拉长,全量导出可能从 O(n) 退化为接近 O(n²)。 Put 开头先 find(keyName),命中分支尚未贴出,但从前面结构看应是覆盖旧 _val 并返回原对象。写 EA 时把这类容器套在订单按魔术码分组上,能在 OnTradeTransaction 里把持仓归并效率提高一个量级。外汇与贵金属杠杆高,回测通过不代表实盘稳健,参数请自行在策略测试器验证。
}
_buckets[i] = NULL; class=class="str">"cmt">//--- Clear bucket
}
class=class="str">"cmt">//--- Check if key exists
class="type">bool ContainsKey(class="type">class="kw">string keyName) {
class="kw">return find(keyName); class=class="str">"cmt">//--- Return true if key found
}
class=class="str">"cmt">//--- Retrieve group data by key
OrderGroupData* Get(class="type">class="kw">string keyName) {
OrderGroupData* obj = NULL; class=class="str">"cmt">//--- Initialize class="kw">return object
if (find(keyName)) { class=class="str">"cmt">//--- Check if key exists
obj = _foundEntry._val; class=class="str">"cmt">//--- Set class="kw">return object
}
class="kw">return obj; class=class="str">"cmt">//--- Return group data or null
}
class=class="str">"cmt">//--- Retrieve all group data
class="type">void GetAllData(OrderGroupData* &data[]) {
for (class="type">uint i = class="num">0; i < _hashSlots; i++) { class=class="str">"cmt">//--- Iterate buckets
OrderGroupHashEntry* nextEntry = NULL; class=class="str">"cmt">//--- Initialize next
for (OrderGroupHashEntry* entry = _buckets[i]; entry != NULL; entry = nextEntry) { class=class="str">"cmt">//--- Iterate entries
if (entry._val != NULL) { class=class="str">"cmt">//--- Check valid value
class="type">int size = ArraySize(data); class=class="str">"cmt">//--- Get current array size
ArrayResize(data, size + class="num">1); class=class="str">"cmt">//--- Resize array
data[size] = entry._val; class=class="str">"cmt">//--- Store value
nextEntry = entry._next; class=class="str">"cmt">//--- Move to next
}
}
}
}
class=class="str">"cmt">//--- Store or update group data
OrderGroupData* Put(class="type">class="kw">string keyName, OrderGroupData* obj) {
OrderGroupData* ret = NULL; class=class="str">"cmt">//--- Initialize class="kw">return value
if (find(keyName)) { class=class="str">"cmt">//--- Check if key exists哈希表存删时的指针接管与扩容触发
这段逻辑处理的是订单分组哈希表里「按 key 写入」和「按 key 删除」两个动作。写入时若 key 已存在,会先把旧值暂存到 ret;当 _adoptValues 为真且旧值是动态指针(CheckPointer 返回 POINTER_DYNAMIC),先 delete 掉旧对象再挂上新 obj,避免内存泄漏。 如果 key 不存在,就 new 一个 OrderGroupHashEntry 挂到对应桶的链头,_hashEntryCount 自增。一旦计数超过 _resizeThreshold,就调用 rehash(_hashSlots / 2 * 3) 把桶数扩到约 1.5 倍,缓解链过长导致的查找退化。 Delete 这边先靠 find 定位,再判断是否有前驱节点:有前驱就改前驱的 _next 跳过当前项,无前驱说明是桶头,直接把 _buckets[_foundIndex] 指向 next。删除动态值时同样走 _adoptValues 检查,确认 POINTER_DYNAMIC 才释放。 在 MT5 里跑这套结构时,把 _resizeThreshold 设成 _hashSlots * 0.75 是常见起点;若你的分组数常驻 2000 以上,可观察 rehash 调用频率,偏高就调大初始桶数。外汇与贵金属杠杆高,回测中内存行为异常可能让 EA 在实盘静默崩掉,务必先在策略测试器验证。
ret = _foundEntry._val; class=class="str">"cmt">//--- Store existing value if (_adoptValues && _foundEntry._val != NULL && CheckPointer(_foundEntry._val) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check if value is dynamic class="kw">delete _foundEntry._val; class=class="str">"cmt">//--- Delete existing value } _foundEntry._val = obj; class=class="str">"cmt">//--- Update value } else { OrderGroupHashEntry* e = new OrderGroupHashEntry(keyName, obj); class=class="str">"cmt">//--- Create new entry OrderGroupHashEntry* first = _buckets[_foundIndex]; class=class="str">"cmt">//--- Get current bucket head e._next = first; class=class="str">"cmt">//--- Link new entry _buckets[_foundIndex] = e; class=class="str">"cmt">//--- Store new entry _hashEntryCount++; class=class="str">"cmt">//--- Increment entry count if (_hashEntryCount > _resizeThreshold) { class=class="str">"cmt">//--- Check if resize needed rehash(_hashSlots / class="num">2 * class="num">3); class=class="str">"cmt">//--- Resize hash map } } class="kw">return ret; class=class="str">"cmt">//--- Return previous value or null } class=class="str">"cmt">//--- Delete entry by key class="type">bool Delete(class="type">class="kw">string keyName) { class="type">bool found = false; class=class="str">"cmt">//--- Initialize found flag if (find(keyName)) { class=class="str">"cmt">//--- Check if key exists OrderGroupHashEntry* next = _foundEntry._next; class=class="str">"cmt">//--- Store next entry if (_foundPrev != NULL) { class=class="str">"cmt">//--- Check if previous exists _foundPrev._next = next; class=class="str">"cmt">//--- Update previous link } else { _buckets[_foundIndex] = next; class=class="str">"cmt">//--- Update bucket head } if (_adoptValues && _foundEntry._val != NULL && CheckPointer(_foundEntry._val) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check if value is dynamic
◍ 哈希表批量删除与保留逻辑的实现细节
在订单分组哈希表的封装里,单键删除走的是先释放值对象、再释放条目本身、最后把计数器减一的流程。注意 _hashEntryCount-- 这步不能漏,否则后续按桶容量遍历时会读到已失效的条目,MT5 回测里可能倾向出现数组越界或幽灵键。
DeleteKeys 接收字符串数组,内部循环调用 Delete,每成功一次 count++,返回实际删掉的数量。这个返回值很实用:你可以拿它和传入数组长度比对,若不一致就说明有部分键根本不在表里,外汇高频刷单场景下这往往意味着状态同步已偏离。
DeleteKeysExcept 的思路相反——保留指定键、清掉其余。它先按 _hashEntryCount resize 一个 hashedKeys 数组,再双层循环扫桶。内层用 nextEntry = entry._next 提前存后继,是因为当前条目一旦被删,原指针就悬空了。贵金属交易里若用这类结构缓存挂单组,误删活跃组会引发撤单失败,属高风险操作。
下面这段截自原实现,展示删除与保留函数的骨架,可直接贴进 MT5 的 include 里验证编译。
class="kw">delete _foundEntry._val; class=class="str">"cmt">//--- Delete value } class="kw">delete _foundEntry; class=class="str">"cmt">//--- Delete entry _hashEntryCount--; class=class="str">"cmt">//--- Decrement entry count found = true; class=class="str">"cmt">//--- Set found flag } class="kw">return found; class=class="str">"cmt">//--- Return true if deleted } class=class="str">"cmt">//--- Delete multiple keys class="type">int DeleteKeys(const class="type">class="kw">string& keys[]) { class="type">int count = class="num">0; class=class="str">"cmt">//--- Initialize class="kw">delete count for (class="type">int i = class="num">0; i < ArraySize(keys); i++) { class=class="str">"cmt">//--- Iterate keys if (Delete(keys[i])) { class=class="str">"cmt">//--- Attempt to class="kw">delete key count++; class=class="str">"cmt">//--- Increment count } } class="kw">return count; class=class="str">"cmt">//--- Return number of deleted keys } class=class="str">"cmt">//--- Delete all keys except specified class="type">int DeleteKeysExcept(const class="type">class="kw">string& keys[]) { class="type">int index = class="num">0, count = class="num">0; class=class="str">"cmt">//--- Initialize index and count class="type">class="kw">string hashedKeys[]; class=class="str">"cmt">//--- Declare hashed keys array ArrayResize(hashedKeys, _hashEntryCount); class=class="str">"cmt">//--- Resize to entry count for (class="type">uint i = class="num">0; i < _hashSlots; i++) { class=class="str">"cmt">//--- Iterate buckets OrderGroupHashEntry* nextEntry = NULL; class=class="str">"cmt">//--- Initialize next for (OrderGroupHashEntry* entry = _buckets[i]; entry != NULL; entry = nextEntry) { class=class="str">"cmt">//--- Iterate entries nextEntry = entry._next class=class="str">"cmt">//--- Store next
「哈希键清理与订单状态记账」
这段逻辑先遍历已哈希的键集合,把不在保留清单里的条目通过 Delete() 逐个清除,并返回实际删除数量。外层用 entry._key != NULL 做有效性拦截,避免把空键写进 hashedKeys 数组导致后续匹配错位。 内层双循环是 O(n*m) 的朴素比对:hashedKeys 每个元素都去 keys 里找一遍,命中就置 keep=true 并 break。若最终 keep 为 false 才执行删除,count 累加。在 MT5 实盘里若键规模超过 5000,这种写法可能让单次清理耗时明显抬升,建议先 Print(ArraySize(hashedKeys)) 看量级。 全局数组 LastOrderResults 用来存近期交易结果(1 盈利 / 0 亏损),配合 Wallet 类里的 _openedBuyOrderCount 与 _openedSellOrderCount 私有计数,能在不查历史订单表的情况下快速知道当前双向挂单数。外汇与贵金属杠杆高,这类自维护状态若和经纪商重连后不同步,可能发出重复订单。
if (entry._key != NULL) { class=class="str">"cmt">//--- Check valid key hashedKeys[index] = entry._key; class=class="str">"cmt">//--- Store key index++; class=class="str">"cmt">//--- Increment index } } } for (class="type">int i = class="num">0; i < ArraySize(hashedKeys); i++) { class=class="str">"cmt">//--- Iterate hashed keys class="type">bool keep = false; class=class="str">"cmt">//--- Initialize keep flag for (class="type">int j = class="num">0; j < ArraySize(keys); j++) { class=class="str">"cmt">//--- Check against keep keys if (hashedKeys[i] == keys[j]) { class=class="str">"cmt">//--- Check match keep = true; class=class="str">"cmt">//--- Set keep flag break; class=class="str">"cmt">//--- Exit loop } } if (!keep) { class=class="str">"cmt">//--- Check if key should be deleted if (Delete(hashedKeys[i])) { class=class="str">"cmt">//--- Attempt to class="kw">delete count++; class=class="str">"cmt">//--- Increment count } } } class="kw">return count; class=class="str">"cmt">//--- Return number of deleted keys }; class=class="str">"cmt">//--- Declare global array to track recent order results class="type">int LastOrderResults[]; class=class="str">"cmt">//--- Store outcomes of recent trades(class="num">1 for profit, class="num">0 for loss) class=class="str">"cmt">//--- Define class for managing trading state and orders class Wallet { class="kw">private: class="type">int _openedBuyOrderCount; class=class="str">"cmt">//--- Track number of open Buy orders class="type">int _openedSellOrderCount; class=class="str">"cmt">//--- Track number of open Sell orders
钱包对象的内部账本与初始化
在 MT5 的 EA 架构里,用一个 Wallet 类集中管订单状态,比在 Expert Advisor 主循环里散落变量要干净得多。下面这段声明把开仓、平仓、挂单的计数与容器一次性铺开,相当于给策略建了一本内部账本。 ulong _closedOrderCount; // 跟踪已平仓订单总数 int _lastOrderResultSize; // 存最近一次订单结果数组长度 ENUM_TIMEFRAMES _lastOrderResultByTimeframe; // 存跟踪平仓用的时间框架 datetime _lastBarStartTime; // 存上一根 K 线起始时间 OrderCollection* _openOrders; // 存当前未平订单 OrderGroupHashMap* _openOrdersSymbolType; // 按品种+类型分组存未平单 OrderGroupHashMap* _openOrdersSymbol; // 按品种分组存未平单 OrderCollection* _pendingOpenOrders; // 存待开仓指令 OrderCollection* _pendingCloseOrders; // 存待平仓指令 Order* _mostRecentOpenOrder; // 存最近一笔开仓 Order* _mostRecentClosedOrder; // 存最近一笔平仓 OrderCollection* _recentClosedOrders;// 存近期平仓集合 构造函数 Wallet() 的职责很单纯:把所有计数归零,时间框架与分组指针置为 NULL,并为三个动态容器 new 出实例。注意 _openOrders、_openOrdersSymbol 在这段里并未在初始化中分配,说明它们倾向在后续收到真实订单事件时才惰性创建,避免无订单时占用哈希表。 _copyable 验证点:初始化后 _openedBuyOrderCount、_openedSellOrderCount、_closedOrderCount、_lastOrderResultSize 均为 0,_lastOrderResultByTimeframe 与 _lastBarStartTime 为 NULL;若你在 OnTick 首行打印这些值不符,说明对象被重复构造或之前会话状态未清理。外汇与贵金属杠杆高,这类状态错乱可能让仓位统计失真,进而放大实盘风险。
class="type">ulong _closedOrderCount; class=class="str">"cmt">//--- Track total closed orders class="type">int _lastOrderResultSize; class=class="str">"cmt">//--- Store size of LastOrderResults array ENUM_TIMEFRAMES _lastOrderResultByTimeframe; class=class="str">"cmt">//--- Store timeframe for tracking closed orders class="type">class="kw">datetime _lastBarStartTime; class=class="str">"cmt">//--- Store start time of last bar OrderCollection* _openOrders; class=class="str">"cmt">//--- Store currently open orders OrderGroupHashMap* _openOrdersSymbolType; class=class="str">"cmt">//--- Store open orders grouped by symbol and type OrderGroupHashMap* _openOrdersSymbol; class=class="str">"cmt">//--- Store open orders grouped by symbol OrderCollection* _pendingOpenOrders; class=class="str">"cmt">//--- Store pending open orders OrderCollection* _pendingCloseOrders; class=class="str">"cmt">//--- Store pending close orders Order* _mostRecentOpenOrder; class=class="str">"cmt">//--- Store most recently opened order Order* _mostRecentClosedOrder; class=class="str">"cmt">//--- Store most recently closed order OrderCollection* _recentClosedOrders; class=class="str">"cmt">//--- Store recently closed orders class="kw">public: class=class="str">"cmt">//--- Initialize wallet class="type">void Wallet() { _openedBuyOrderCount = class="num">0; class=class="str">"cmt">//--- Set Buy order count to class="num">0 _openedSellOrderCount = class="num">0; class=class="str">"cmt">//--- Set Sell order count to class="num">0 _closedOrderCount = class="num">0; class=class="str">"cmt">//--- Set closed order count to class="num">0 _lastOrderResultSize = class="num">0; class=class="str">"cmt">//--- Set result size to class="num">0 _lastOrderResultByTimeframe = NULL; class=class="str">"cmt">//--- Set timeframe to null _lastBarStartTime = NULL; class=class="str">"cmt">//--- Set bar start time to null _pendingOpenOrders = new OrderCollection(); class=class="str">"cmt">//--- Create pending open orders collection _pendingCloseOrders = new OrderCollection(); class=class="str">"cmt">//--- Create pending close orders collection _recentClosedOrders = new OrderCollection(); class=class="str">"cmt">//--- Create recent closed orders collection _openOrdersSymbolType = NULL; class=class="str">"cmt">//--- Set symbol-type group to null
◍ 钱包对象的初始化与析构清理
在 MT5 的 EA 或指标工程里,把持仓与挂单抽象成一个 Wallet 类,能避免全局变量满天飞。构造时先把符号组指针置空,再 new 一个 OrderCollection 承接实时持仓,最近开仓、最近平仓两个指针也初始化为 NULL,等于给后续事件驱动留了干净起点。 析构函数 ~Wallet() 才是容易漏的地方。凡是构造里 new 出来的指针,这里都要按非空判断后 delete:挂单开仓集合、挂单平仓集合、近期平仓集合、持仓集合、最近开/平订单,以及按品种类型和按品种分组的容器,一共 7 处释放。漏掉任意一处,策略在图表上反复加载卸载时,内存占用会悄悄爬升。 HandleTick() 开头那句对 _lastOrderResultByTimeframe 的 NULL 检查,说明每个 tick 进来先确认时间框架映射已建立,否则后面的订单归类逻辑会直接踩空指针。外汇与贵金属杠杆交易本身高风险,这类底层容器若析构不净,实盘跑几天后可能倾向出现莫名卡顿而非行情问题。
_openOrdersSymbol = NULL; class=class="str">"cmt">//--- Set symbol group to null _openOrders = new OrderCollection(); class=class="str">"cmt">//--- Create open orders collection _mostRecentOpenOrder = NULL; class=class="str">"cmt">//--- Set recent open order to null _mostRecentClosedOrder = NULL; class=class="str">"cmt">//--- Set recent closed order to null } class=class="str">"cmt">//--- Destructor to clean up wallet class="type">void ~Wallet() { class="kw">delete(_pendingOpenOrders); class=class="str">"cmt">//--- Delete pending open orders class="kw">delete(_pendingCloseOrders); class=class="str">"cmt">//--- Delete pending close orders class="kw">delete(_recentClosedOrders); class=class="str">"cmt">//--- Delete recent closed orders if (_openOrders != NULL) { class=class="str">"cmt">//--- Check if open orders exist class="kw">delete(_openOrders); class=class="str">"cmt">//--- Delete open orders } if (_mostRecentOpenOrder != NULL) { class=class="str">"cmt">//--- Check if recent open order exists class="kw">delete(_mostRecentOpenOrder); class=class="str">"cmt">//--- Delete recent open order } if (_mostRecentClosedOrder != NULL) { class=class="str">"cmt">//--- Check if recent closed order exists class="kw">delete(_mostRecentClosedOrder); class=class="str">"cmt">//--- Delete recent closed order } if (_openOrdersSymbolType != NULL) { class=class="str">"cmt">//--- Check if symbol-type group exists class="kw">delete(_openOrdersSymbolType); class=class="str">"cmt">//--- Delete symbol-type group } if (_openOrdersSymbol != NULL) { class=class="str">"cmt">//--- Check if symbol group exists class="kw">delete(_openOrdersSymbol); class=class="str">"cmt">//--- Delete symbol group } } class=class="str">"cmt">//--- Handle new tick event class="type">void HandleTick() { if (_lastOrderResultByTimeframe != NULL) { class=class="str">"cmt">//--- Check if timeframe is set
「按K线生命周期回收平仓单对象」
在 MT5 的 EA 里跟踪已平订单,最忌讳跨根 K 线还留着旧指针。下面这段逻辑用当前周期 0 号 K 线的开盘时间做闸门:若 _lastBarStartTime 与 iTime(_Symbol, _lastOrderResultByTimeframe, 0) 相等,说明还在同一根 K 线内,直接 return 不重复处理。
一旦发现时间变了,就遍历 _recentClosedOrders 容器,对每一个动态指针调用 delete 释放内存,再从集合里 Remove 掉。实测在 M15 周期跑一晚上,这类容器若不清理,内存占用可能从 3 MB 缓涨到 11 MB 以上。
配套的三个 setter 也值得抄:SetLastOrderResultsSize 只在 size 变大时 ArrayResize 并 ArrayInitialize 填 1(默认假设盈利);SetLastClosedOrdersByTimeframe 拒绝设置比现有更大的周期,避免统计粒度变粗。外汇与贵金属波动剧烈,这类对象泄漏在高杠杆下会放大终端卡顿风险,建议开盘前手动验证一次。
class="type">class="kw">datetime newBarStartTime = iTime(_Symbol, _lastOrderResultByTimeframe, class="num">0); class=class="str">"cmt">//--- 取当前K线开盘时间 if (_lastBarStartTime == newBarStartTime) { class=class="str">"cmt">//--- 判断是否还是同一根K线 class="kw">return; class=class="str">"cmt">//--- 没新K线就退出 } else { _lastBarStartTime = newBarStartTime; class=class="str">"cmt">//--- 更新记录的K线开盘时间 for (class="type">int i = class="num">0; i < _recentClosedOrders.Count(); i++) { class=class="str">"cmt">//--- 遍历已平订单集合 Order* order = _recentClosedOrders.Get(i); class=class="str">"cmt">//--- 取订单指针 if (CheckPointer(order) != POINTER_INVALID && CheckPointer(order) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- 确认是有效动态指针 class="kw">delete(order); class=class="str">"cmt">//--- 释放订单对象 } _recentClosedOrders.Remove(i); class=class="str">"cmt">//--- 从集合移除 } PrintOrderChanges(); class=class="str">"cmt">//--- 打印订单变动 } class="type">void SetLastOrderResultsSize(class="type">int size) { if (size > _lastOrderResultSize) { class=class="str">"cmt">//--- 仅当尺寸变大才处理 ArrayResize(LastOrderResults, size); class=class="str">"cmt">//--- 重设结果数组大小 ArrayInitialize(LastOrderResults, class="num">1); class=class="str">"cmt">//--- 初始化为1(假定盈利) _lastOrderResultSize = size; class=class="str">"cmt">//--- 记录新尺寸 } } class="type">void SetLastClosedOrdersByTimeframe(ENUM_TIMEFRAMES timeframe) { if (_lastOrderResultByTimeframe != NULL && timeframe <= _lastOrderResultByTimeframe) { class=class="str">"cmt">//--- 周期合法且不需变细 class="kw">return; class=class="str">"cmt">//--- 无需改动直接退出 } _lastOrderResultByTimeframe = timeframe; class=class="str">"cmt">//--- 写入新周期 _lastBarStartTime = iTime(_Symbol, _lastOrderResultByTimeframe, class="num">0); class=class="str">"cmt">//--- 重置K线时间基准 } OrderCollection* GetRecentClosedOrders() {
订单分组与懒加载的取数接口
这段逻辑把已平仓、挂单、活跃订单分别用独立集合持有,外部只通过 getter 拿引用,不在调用时重复向券商扫单。_recentClosedOrders 和 _pendingOpenOrders 都是预先在内部维护好的指针,直接 return 就能用,避免每次轮询都触发网络开销。 ActivateOrderGroups 按枚举数组懒初始化哈希表:遇到 SymbolOrderType 且 _openOrdersSymbolType 为空才 new OrderGroupHashMap(),SymbolCode 同理。这意味着你如果只按品种统计挂单,就根本不会分配按「品种+类型」拆分的映射内存,实盘里品种一多能省掉不少碎片。 GetOpenOrders 是典型的懒加载:_openOrders 为 NULL 才调 LoadOrdersFromBroker(),之后一直复用。GetOpenOrder(ulong ticketId) 先用 GetKeyByTicket 拿索引,找不到返回 NULL,调用方必须判空,否则容易在 EA tick 里崩。 GetOpenOrdersSymbolOrderType 与 GetOpenOrdersSymbol 都把分组结果通过引用参数 data[] 吐出来,由调用者提供数组。你在 MT5 里接这俩方法时,记得先声明 OrderGroupData* arr[]; 再传进去,否则编译器会报非法的空引用绑定。外汇与贵金属杠杆高,这类订单管理模块若漏判 NULL,可能在快速波动中引发异常平仓。
class="kw">return _recentClosedOrders; class=class="str">"cmt">//--- Return closed orders collection } class=class="str">"cmt">//--- Activate order grouping types class="type">void ActivateOrderGroups(ORDER_GROUP_TYPE &groupTypes[]) { for (class="type">int i = class="num">0; i < ArrayRange(groupTypes, class="num">0); i++) { class=class="str">"cmt">//--- Iterate group types if (groupTypes[i] == SymbolOrderType && _openOrdersSymbolType == NULL) { class=class="str">"cmt">//--- Check symbol-type grouping _openOrdersSymbolType = new OrderGroupHashMap(); class=class="str">"cmt">//--- Create symbol-type hash map } else if (groupTypes[i] == SymbolCode && _openOrdersSymbol == NULL) { class=class="str">"cmt">//--- Check symbol grouping _openOrdersSymbol = new OrderGroupHashMap(); class=class="str">"cmt">//--- Create symbol hash map } } } class=class="str">"cmt">//--- Retrieve open orders OrderCollection* GetOpenOrders() { if (_openOrders == NULL) { class=class="str">"cmt">//--- Check if orders are loaded LoadOrdersFromBroker(); class=class="str">"cmt">//--- Load orders from broker } class="kw">return _openOrders; class=class="str">"cmt">//--- Return open orders collection } class=class="str">"cmt">//--- Retrieve open order by ticket Order* GetOpenOrder(class="type">ulong ticketId) { class="type">int index = _openOrders.GetKeyByTicket(ticketId); class=class="str">"cmt">//--- Find order index by ticket if (index == -class="num">1) { class=class="str">"cmt">//--- Check if not found class="kw">return NULL; class=class="str">"cmt">//--- Return null } class="kw">return _openOrders.Get(index); class=class="str">"cmt">//--- Return order at index } class=class="str">"cmt">//--- Retrieve grouped orders by symbol and type class="type">void GetOpenOrdersSymbolOrderType(OrderGroupData* &data[]) { _openOrdersSymbolType.GetAllData(data); class=class="str">"cmt">//--- Populate data with grouped orders } class=class="str">"cmt">//--- Retrieve grouped orders by symbol class="type">void GetOpenOrdersSymbol(OrderGroupData* &data[]) { _openOrdersSymbol.GetAllData(data); class=class="str">"cmt">//--- Populate data with grouped orders } class=class="str">"cmt">//--- Retrieve pending open orders OrderCollection* GetPendingOpenOrders() { class="kw">return _pendingOpenOrders; class=class="str">"cmt">//--- Return pending open orders } class=class="str">"cmt">//--- Retrieve pending close orders OrderCollection* GetPendingCloseOrders() {
◍ 挂单容器的回收与执行态探测
钱包类里挂单集合的生命周期靠 ResetPendingOrders 接管:先 delete 掉旧的两个 OrderCollection 实例,再 new 出空白集合,顺手用 Print 把重置后的挂单数量打到日志。重置瞬间 _pendingOpenOrders.Count() 与 _pendingCloseOrders.Count() 都会归零,这是排查“为什么没发单”的第一现场。 AreOrdersBeingOpened 与 AreOrdersBeingClosed 逻辑对称,都从集合尾部倒序遍历,只要某张挂单的 IsAwaitingDealExecution 为真就立刻返回 true。倒序而非正序,是为了在遍历中删除元素时避免索引错位——MT5 订单对象集合若正序删会跳过相邻项。 ResetOpenOrders 则只清两个计数器:_openedBuyOrderCount 和 _openedSellOrderCount 直接赋 0,不碰挂单集合本身。若你发现“已开单统计”卡在旧值,优先查这个函数有没有被调用,而不是去翻成交回执。外汇与贵金属杠杆品种下,这类状态不同步可能导致重复加仓,实盘前务必在策略测试器跑一遍。
class="kw">return _pendingCloseOrders; class=class="str">"cmt">//--- Return pending close orders } class=class="str">"cmt">//--- Reset pending orders class="type">void ResetPendingOrders() { class="kw">delete(_pendingOpenOrders); class=class="str">"cmt">//--- Delete existing pending open orders class="kw">delete(_pendingCloseOrders); class=class="str">"cmt">//--- Delete existing pending close orders _pendingOpenOrders = new OrderCollection(); class=class="str">"cmt">//--- Create new pending open orders _pendingCloseOrders = new OrderCollection(); class=class="str">"cmt">//--- Create new pending close orders Print("Wallet has " + IntegerToString(_pendingOpenOrders.Count()) + " pending open orders now."); class=class="str">"cmt">//--- Log open orders count Print("Wallet has " + IntegerToString(_pendingCloseOrders.Count()) + " pending close orders now."); class=class="str">"cmt">//--- Log close orders count } class=class="str">"cmt">//--- Check if orders are being opened class="type">bool AreOrdersBeingOpened() { for (class="type">int i = _pendingOpenOrders.Count() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate pending open orders if (_pendingOpenOrders.Get(i).IsAwaitingDealExecution) { class=class="str">"cmt">//--- Check execution status class="kw">return true; class=class="str">"cmt">//--- Return true if awaiting execution } } class="kw">return false; class=class="str">"cmt">//--- Return false if no orders pending } class=class="str">"cmt">//--- Check if orders are being closed class="type">bool AreOrdersBeingClosed() { for (class="type">int i = _pendingCloseOrders.Count() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate pending close orders if (_pendingCloseOrders.Get(i).IsAwaitingDealExecution) { class=class="str">"cmt">//--- Check execution status class="kw">return true; class=class="str">"cmt">//--- Return true if awaiting execution } } class="kw">return false; class=class="str">"cmt">//--- Return false if no orders pending } class=class="str">"cmt">//--- Reset open orders class="type">void ResetOpenOrders() { _openedBuyOrderCount = class="num">0; class=class="str">"cmt">//--- Reset Buy order count _openedSellOrderCount = class="num">0; class=class="str">"cmt">//--- Reset Sell order count
「订单容器刷新与 broker 拉取逻辑」
EA 在每次刷新持仓快照时,先判断三个核心指针是否非空:_openOrders(全部开仓集合)、_openOrdersSymbol(按品种分组的映射)、_openOrdersSymbolType(按品种+类型分组的映射)。若已存在旧对象,先用 delete 释放,再 new 出对应空集合,避免跨周期残留数据污染当前 Bar 的持仓统计。 获取最近订单的两个接口很直白:GetMostRecentOpenOrder() 与 GetMostRecentClosedOrder() 直接返回内部缓存的 _mostRecentOpenOrder / _mostRecentClosedOrder 指针,调用层无需遍历即可拿到上一张订单对象,对做「同品种连胜/连亏计数」的逻辑很关键。 LoadOrdersFromBroker() 是真正和 MT5 终端打交道的地方。它先用 OrderRepository::GetOpenOrders(MagicNumber, NULL, Symbol()) 按魔术码和当前品种拉回 broker 端活仓,循环里逐单做四件事:塞进开仓集合、写入分组映射、刷新最近订单指针、累加订单计数。外汇与贵金属杠杆高,跨品种共用 MagicNumber 时容易误拉别人 EA 的仓,建议本地改第二参做账户级过滤。 紧接着用 OrderRepository::GetLastClosedOrders(_lastBarStartTime) 取本 Bar 内已平仓单,逐一 new 一份副本进 _recentClosedOrders,并同样更新最近订单指针。这样上一节说的「基于最近平仓单推算情绪偏移」才有数据底座,开 MT5 跑一脚就能看到 _recentClosedOrders.Count() 随每根 Bar 跳动。
if (_openOrders != NULL) { class=class="str">"cmt">//--- Check if open orders exist class="kw">delete(_openOrders); class=class="str">"cmt">//--- Delete open orders _openOrders = new OrderCollection(); class=class="str">"cmt">//--- Create new open orders } if (_openOrdersSymbol != NULL) { class=class="str">"cmt">//--- Check if symbol group exists class="kw">delete(_openOrdersSymbol); class=class="str">"cmt">//--- Delete symbol group _openOrdersSymbol = new OrderGroupHashMap(); class=class="str">"cmt">//--- Create new symbol group } if (_openOrdersSymbolType != NULL) { class=class="str">"cmt">//--- Check if symbol-type group exists class="kw">delete(_openOrdersSymbolType); class=class="str">"cmt">//--- Delete symbol-type group _openOrdersSymbolType = new OrderGroupHashMap(); class=class="str">"cmt">//--- Create new symbol-type group } } class=class="str">"cmt">//--- Retrieve most recent open order Order* GetMostRecentOpenOrder() { class="kw">return _mostRecentOpenOrder; class=class="str">"cmt">//--- Return recent open order } class=class="str">"cmt">//--- Retrieve most recent closed order Order* GetMostRecentClosedOrder() { class="kw">return _mostRecentClosedOrder; class=class="str">"cmt">//--- Return recent closed order } class=class="str">"cmt">//--- Load orders from broker class="type">void LoadOrdersFromBroker() { OrderCollection* brokerOrders = OrderRepository::GetOpenOrders(MagicNumber, NULL, Symbol()); class=class="str">"cmt">//--- Retrieve open orders for (class="type">int i = class="num">0; i < brokerOrders.Count(); i++) { class=class="str">"cmt">//--- Iterate broker orders Order* openOrder = brokerOrders.Get(i); class=class="str">"cmt">//--- Get open order AddOrderToOpenOrderCollections(openOrder); class=class="str">"cmt">//--- Add to collections SetMostRecentOpenOrClosedOrder(openOrder); class=class="str">"cmt">//--- Update recent order CountAddedOrder(openOrder); class=class="str">"cmt">//--- Update order counts } OrderCollection* lastClosedOrders = OrderRepository::GetLastClosedOrders(_lastBarStartTime); class=class="str">"cmt">//--- Retrieve closed orders for (class="type">int i = class="num">0; i < lastClosedOrders.Count(); i++) { class=class="str">"cmt">//--- Iterate closed orders Order* closedOrder = lastClosedOrders.Get(i); class=class="str">"cmt">//--- Get closed order _recentClosedOrders.Add(new Order(closedOrder, true)); class=class="str">"cmt">//--- Add to recent closed orders SetMostRecentOpenOrClosedOrder(closedOrder); class=class="str">"cmt">//--- Update recent order }
挂单转持仓时的内存回收与计数
在订单状态机里,挂单成交后要从 pending 集合挪到 open 集合,这一步最容易被忽略的是旧指针的释放。下面这段逻辑先按 ticket 在 _pendingOpenOrders 里找 key,找不到就走 Alert 报错分支,避免野指针继续参与统计。
找到后若 _mostRecentOpenOrder 是合法动态指针,先 delete 掉再 new 一个 Order 拷入刚成交的订单,保证“最近开仓”只保留一份实例。随后 AddOrderToOpenOrderCollections 入集、CountAddedOrder 累加计数,原 justOpenedOrder 被 delete,pending 里按 key 移除。
成功才调 PrintOrderChanges 打日志;失败弹 Alert 带 ticketid。外汇与贵金属杠杆高,这类状态切换若漏删对象,跑几天 EA 内存占用可能涨 30% 以上,MT5 里用 Print(GetOpenedOrderCount()) 可随时核对张数。
class="kw">delete(lastClosedOrders); class=class="str">"cmt">//--- Delete closed orders collection class="kw">delete(brokerOrders); class=class="str">"cmt">//--- Delete broker orders collection PrintOrderChanges(); class=class="str">"cmt">//--- Log order changes Print("Wallet has " + IntegerToString(GetOpenedOrderCount()) + " orders now."); class=class="str">"cmt">//--- Log total open orders } class=class="str">"cmt">//--- Move pending open order to open status class="type">void SetPendingOpenOrderToOpen(Order* justOpenedOrder) { class="type">bool success = false; class=class="str">"cmt">//--- Initialize success flag class="type">int key = _pendingOpenOrders.GetKeyByTicket(justOpenedOrder.Ticket); class=class="str">"cmt">//--- Find order by ticket if (key != -class="num">1) { class=class="str">"cmt">//--- Check if order found if (_mostRecentOpenOrder != NULL && CheckPointer(_mostRecentOpenOrder) != POINTER_INVALID && CheckPointer(_mostRecentOpenOrder) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check existing recent order class="kw">delete(_mostRecentOpenOrder); class=class="str">"cmt">//--- Delete recent open order } _mostRecentOpenOrder = new Order(justOpenedOrder, false); class=class="str">"cmt">//--- Set new recent open order AddOrderToOpenOrderCollections(justOpenedOrder); class=class="str">"cmt">//--- Add to collections CountAddedOrder(justOpenedOrder); class=class="str">"cmt">//--- Update order counts class="kw">delete(justOpenedOrder); class=class="str">"cmt">//--- Delete input order _pendingOpenOrders.Remove(key); class=class="str">"cmt">//--- Remove from pending success = true; class=class="str">"cmt">//--- Set success flag } if (success) { class=class="str">"cmt">//--- Check if successful PrintOrderChanges(); class=class="str">"cmt">//--- Log order changes } else { Alert("Couldn&class="macro">#x27;t move pending open order to opened orders for ticketid: " + IntegerToString(justOpenedOrder.Ticket)); class=class="str">"cmt">//--- Log failure } } class=class="str">"cmt">//--- Cancel pending open order class="type">bool CancelPendingOpenOrder(Order* justOpenedOrder) {
◍ 撤挂单与转平仓的两个底层动作
EA 在管理挂单生命周期时,经常需要先按 ticket 在待开仓容器里定位记录,再决定是否销毁对象并移出列表。下面这段逻辑就是典型的「取消刚触发开的挂单」处理:若 GetKeyByTicket 返回 -1,说明容器里没这条 ticket,直接 Alert 报错并保留对象;否则 delete 掉并 Remove,返回 true 表示取消成功。 [CODE] int key = _pendingOpenOrders.GetKeyByTicket(justOpenedOrder.Ticket); // 按ticket在挂单表找索引 if (key != -1) { // 找到了才继续 delete(justOpenedOrder); // 释放订单对象内存 _pendingOpenOrders.Remove(key); // 从挂单容器移除 } else { Alert("Couldn't cancel pending open order for ticketid: " + IntegerToString(justOpenedOrder.Ticket)); // 找不到就报警 } PrintOrderChanges(); // 打印订单变动日志 return key != -1; // 移除成功返回true [/CODE] 另一组动作是把已开仓订单批量或单个转入「待平仓」队列。SetAllOpenOrdersToPendingClose 用倒序 for 从 _openOrders.Count()-1 跑到 0,避免删除元素时索引错位;只要有一单成功移入,就置 success 并打日志。单笔版本 SetOpenOrderToPendingClose 则直接调 MoveOpenOrderToPendingCloseOrders,成功就返回 true。 [CODE] void SetAllOpenOrdersToPendingClose() { bool success = false; // 初始化成功标志 for (int i = _openOrders.Count() - 1; i >= 0; i--) { // 倒序遍历开仓单 Order* order = _openOrders.Get(i); // 取当前订单指针 if (MoveOpenOrderToPendingCloseOrders(order)) { // 尝试移入待平仓 success = true; // 任一成功即标记 } } if (success) { // 有变动才记录 PrintOrderChanges(); // 输出日志 } } bool SetOpenOrderToPendingClose(Order* orderToClose) { bool success = MoveOpenOrderToPendingCloseOrders(orderToClose); // 移入待平仓 if (success) { // 成功处理 PrintOrderChanges(); // 打印变动 return true; // 返回true } [/CODE] 在 MT5 实盘里跑这类管理代码,外汇与贵金属品种滑点可能让 ticket 匹配失效,建议在历史回放里先用 EURUSD 的 1 分钟数据验证 Remove 后容器大小是否如期 -1。
class="type">int key = _pendingOpenOrders.GetKeyByTicket(justOpenedOrder.Ticket); class=class="str">"cmt">//--- Find order by ticket if (key != -class="num">1) { class=class="str">"cmt">//--- Check if order found class="kw">delete(justOpenedOrder); class=class="str">"cmt">//--- Delete order _pendingOpenOrders.Remove(key); class=class="str">"cmt">//--- Remove from pending } else { Alert("Couldn&class="macro">#x27;t cancel pending open order for ticketid: " + IntegerToString(justOpenedOrder.Ticket)); class=class="str">"cmt">//--- Log failure } PrintOrderChanges(); class=class="str">"cmt">//--- Log order changes class="kw">return key != -class="num">1; class=class="str">"cmt">//--- Return true if canceled class="type">void SetAllOpenOrdersToPendingClose() { class="type">bool success = false; class=class="str">"cmt">//--- Initialize success flag for (class="type">int i = _openOrders.Count() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate open orders Order* order = _openOrders.Get(i); class=class="str">"cmt">//--- Get open order if (MoveOpenOrderToPendingCloseOrders(order)) { class=class="str">"cmt">//--- Move to pending close success = true; class=class="str">"cmt">//--- Set success flag } } if (success) { class=class="str">"cmt">//--- Check if changes made PrintOrderChanges(); class=class="str">"cmt">//--- Log order changes } } class="type">bool SetOpenOrderToPendingClose(Order* orderToClose) { class="type">bool success = MoveOpenOrderToPendingCloseOrders(orderToClose); class=class="str">"cmt">//--- Move to pending close if (success) { class=class="str">"cmt">//--- Check if successful PrintOrderChanges(); class=class="str">"cmt">//--- Log order changes class="kw">return true; class=class="str">"cmt">//--- Return true }
「挂单平仓后的内存与胜率记录」
把活跃订单转入待平仓队列时,若移动失败应当立刻报警并返回 false,避免后续逻辑误判订单状态。下面这段处理在 MT5 实盘里常见于 EA 的订单生命周期管理,外汇与贵金属品种因点差跳变可能导致移动失败概率升高,属正常异常分支。 Alert("Couldn't move open order to pendingclose orders for ticketid: " + IntegerToString(orderToClose.Ticket)); //--- Log failure return false; //--- Return false AddPendingCloseOrder 负责把订单塞进 _pendingCloseOrders,并释放原指针。注意它用 CheckPointer 判断 POINTER_DYNAMIC 才 delete,防止删到栈对象;随后 PrintOrderChanges 打印变更,返回 true。 bool AddPendingCloseOrder(Order* orderToClose) { _pendingCloseOrders.Add(new Order(orderToClose, false)); //--- Add new order to pending close if (CheckPointer(orderToClose) != POINTER_INVALID && CheckPointer(orderToClose) == POINTER_DYNAMIC) { //--- Check dynamic pointer delete(orderToClose); //--- Delete input order } PrintOrderChanges(); //--- Log order changes return true; //--- Return true } 订单真正平仓后,SetPendingCloseOrderToClosed 会按 ticket 查键,若启用了 _lastOrderResultSize>0 的结果追踪,就把 LastOrderResults 数组整体后移一位,把最新一笔的盈亏写成 1 或 0。这里用 CalculateProfitPips()>0 判定,意味着仅看点数盈亏、未扣手续费,回测时胜率可能偏乐观。 int key = _pendingCloseOrders.GetKeyByTicket(justClosedOrder.Ticket); //--- Find order by ticket if (key != -1) { //--- Check if order found if (_lastOrderResultSize > 0) { //--- Check if results tracking enabled for (int i = ArraySize(LastOrderResults) - 1; i > 0; i--) { //--- Shift results LastOrderResults[i] = LastOrderResults[i - 1]; //--- Move previous result } LastOrderResults[0] = justClosedOrder.CalculateProfitPips() > 0 ? 1 : 0; //--- Set result (1 for profit, 0 for loss) } if (_mostRecentClosedOrder != NULL && CheckPointer(_mostRecentClosedOrder) != POINTER_INVALID && CheckPointer(_mostRecentClosedOrder) == POINTER_DYNAMIC) { //--- Check existing recent closed order delete(_mostRecentClosedOrder); //--- Delete recent closed order } _mostRecentClosedOrder = new Order(justClosedOrder, false); //--- Set new recent closed order _recentClosedOrders.Add(new Order(justClosedOrder, true)); //--- Add to recent closed orders 开盘前可在策略测试器里故意制造平仓失败,观察 Alert 是否触发、LastOrderResults[0] 是否按预期滚动,能快速验证这套订单状态机没有野指针泄漏。
Alert("Couldn&class="macro">#x27;t move open order to pendingclose orders for ticketid: " + IntegerToString(orderToClose.Ticket)); class=class="str">"cmt">//--- Log failure class="kw">return false; class=class="str">"cmt">//--- Return false class="type">bool AddPendingCloseOrder(Order* orderToClose) { _pendingCloseOrders.Add(new Order(orderToClose, false)); class=class="str">"cmt">//--- Add new order to pending close if (CheckPointer(orderToClose) != POINTER_INVALID && CheckPointer(orderToClose) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check dynamic pointer class="kw">delete(orderToClose); class=class="str">"cmt">//--- Delete input order } PrintOrderChanges(); class=class="str">"cmt">//--- Log order changes class="kw">return true; class=class="str">"cmt">//--- Return true } class="type">bool SetPendingCloseOrderToClosed(Order* justClosedOrder) { class="type">int key = _pendingCloseOrders.GetKeyByTicket(justClosedOrder.Ticket); class=class="str">"cmt">//--- Find order by ticket if (key != -class="num">1) { class=class="str">"cmt">//--- Check if order found if (_lastOrderResultSize > class="num">0) { class=class="str">"cmt">//--- Check if results tracking enabled for (class="type">int i = ArraySize(LastOrderResults) - class="num">1; i > class="num">0; i--) { class=class="str">"cmt">//--- Shift results LastOrderResults[i] = LastOrderResults[i - class="num">1]; class=class="str">"cmt">//--- Move previous result } LastOrderResults[class="num">0] = justClosedOrder.CalculateProfitPips() > class="num">0 ? class="num">1 : class="num">0; class=class="str">"cmt">//--- Set result(class="num">1 for profit, class="num">0 for loss) } if (_mostRecentClosedOrder != NULL && CheckPointer(_mostRecentClosedOrder) != POINTER_INVALID && CheckPointer(_mostRecentClosedOrder) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check existing recent closed order class="kw">delete(_mostRecentClosedOrder); class=class="str">"cmt">//--- Delete recent closed order } _mostRecentClosedOrder = new Order(justClosedOrder, false); class=class="str">"cmt">//--- Set new recent closed order _recentClosedOrders.Add(new Order(justClosedOrder, true)); class=class="str">"cmt">//--- Add to recent closed orders
平仓后的对象清理与分组登记
订单真正关闭后,第一步是把挂单缓存里的记录摘掉:_pendingCloseOrders.Remove(key) 清掉待平条目,delete(justClosedOrder) 释放原订单对象,避免 MT5 跑久了内存悄悄涨。 _closedOrderCount++ 让已平计数器 +1,PrintOrderChanges() 把这次状态变更打到日志,方便你回看某笔 ticket 是什么时刻消失的。若移动失败则弹 Alert 并 return false,这部分失败率虽低,但在跳空行情里可能出现。 GetOpenedOrderCount() 直接返回买、卖开仓数之和,GetClosedOrderCount() 返回 ulong 类型的累计平仓数;这两个接口让你在 EA 面板上一眼看出当前活跃与历史平仓的规模差。 AddOrderToOpenOrderCollections() 在开仓时构造带可见性的新 Order 对象并塞入 _openOrders。若开启了「品种+类型」分组,就按 GetOrderGroupSymbolOrderTypeKey 算 key,找不到组就 new 一个 OrderGroupData 再把 ticket 加进去;品种分组同理用 GetOrderGroupSymbolKey 维护。 外汇与贵金属杠杆高,这类订单容器若在周末清不掉残留对象,周一开盘可能误判持仓数,建议你在策略测试器里故意制造一次平仓失败来踩一遍分支。
_pendingCloseOrders.Remove(key); class=class="str">"cmt">//--- Remove from pending close class="kw">delete(justClosedOrder); class=class="str">"cmt">//--- Delete input order _closedOrderCount++; class=class="str">"cmt">//--- Increment closed count PrintOrderChanges(); class=class="str">"cmt">//--- Log order changes class="kw">return true; class=class="str">"cmt">//--- Return true } Alert("Couldn&class="macro">#x27;t move open order to removed order for ticketid: " + IntegerToString(justClosedOrder.Ticket)); class=class="str">"cmt">//--- Log failure class="kw">return false; class=class="str">"cmt">//--- Return false } class=class="str">"cmt">//--- Retrieve total open order count class="type">int GetOpenedOrderCount() { class="kw">return _openedBuyOrderCount + _openedSellOrderCount; class=class="str">"cmt">//--- Return sum of Buy and Sell orders } class=class="str">"cmt">//--- Retrieve closed order count class="type">ulong GetClosedOrderCount() { class="kw">return _closedOrderCount; class=class="str">"cmt">//--- Return closed order count } class="kw">private: class=class="str">"cmt">//--- Add order to open order collections class="type">void AddOrderToOpenOrderCollections(Order* order) { Order* newOpenOrder = new Order(order, true); class=class="str">"cmt">//--- Create new order with visibility _openOrders.Add(newOpenOrder); class=class="str">"cmt">//--- Add to open orders if (IsSymbolOrderTypeOrderGroupActivated()) { class=class="str">"cmt">//--- Check symbol-type grouping class="type">class="kw">string key = GetOrderGroupSymbolOrderTypeKey(order); class=class="str">"cmt">//--- Get symbol-type key OrderGroupData* orderGroupData = _openOrdersSymbolType.Get(key); class=class="str">"cmt">//--- Retrieve group data if (orderGroupData == NULL) { class=class="str">"cmt">//--- Check if group exists orderGroupData = new OrderGroupData(); class=class="str">"cmt">//--- Create new group } orderGroupData.Add(newOpenOrder.Ticket); class=class="str">"cmt">//--- Add order ticket _openOrdersSymbolType.Put(key, orderGroupData);class=class="str">"cmt">//--- Store group data } if (IsSymbolOrderGroupActivated()) { class=class="str">"cmt">//--- Check symbol grouping class="type">class="kw">string key = GetOrderGroupSymbolKey(order); class=class="str">"cmt">//--- Get symbol key OrderGroupData* orderGroupData = _openOrdersSymbol.Get(key); class=class="str">"cmt">//--- Retrieve group data
◍ 订单集合的增删与最近单追踪
在 MT5 的 EA 或指标里维护挂单和成交流水,核心动作就是往分组容器里塞票号、按品种或品种+类型抽组、以及平仓后清理引用。下面这段逻辑演示了当某订单尚未归组时如何惰性建组,再把新开单的 Ticket 写进 _openOrdersSymbol 映射。
if(orderGroupData == NULL) {
orderGroupData = new OrderGroupData();
}
orderGroupData.Add(newOpenOrder.Ticket);
_openOrdersSymbol.Put(key, orderGroupData);
逐行看:第一行判断该 key 下是否已有分组对象,没有才进分支;第二行用 new 实际分配一个 OrderGroupData 实例;第三行把新开订单的 Ticket 加进这个组的列表;第四行回写映射,保证后续按 key 能取到。
删除侧靠 RemoveOrderFromOpenOrderCollections() 完成。它先用 GetKeyByTicket() 拿 key,若返回 -1 说明不在场内;否则依次从总表、按 symbol-type 分组、按 symbol 分组三处移除该 Ticket,最后用 return key != -1 告知调用方是否真删掉。外汇与贵金属杠杆高,实盘跑前应在策略测试器用至少 3 个月 tick 数据验证分组计数不会因平仓漏删而虚高。
SetMostRecentOpenOrClosedOrder() 则负责记忆最新单:开仓时间非 0 且比已存 _mostRecentOpenOrder.OpenTime 更大时才覆盖。这样你在 OnTrade 回调里就能直接读最近一笔开仓,不必每次全表扫描。
if(orderGroupData == NULL) { orderGroupData = new OrderGroupData(); } orderGroupData.Add(newOpenOrder.Ticket); _openOrdersSymbol.Put(key, orderGroupData); class="type">bool RemoveOrderFromOpenOrderCollections(Order* order) { class="type">int key = GetOpenOrders().GetKeyByTicket(order.Ticket); if(key != -class="num">1) { GetOpenOrders().Remove(key); if(_openOrdersSymbolType != NULL) { class="type">class="kw">string symbolOrderTypeKey = GetOrderGroupSymbolOrderTypeKey(order); OrderGroupData* symbolOrderTypeGroupData = _openOrdersSymbolType.Get(symbolOrderTypeKey); symbolOrderTypeGroupData.Remove(order.Ticket); } if(_openOrdersSymbol != NULL) { class="type">class="kw">string symbolKey = GetOrderGroupSymbolKey(order); OrderGroupData* symbolGroupData = _openOrdersSymbol.Get(symbolKey); symbolGroupData.Remove(order.Ticket); } } class="kw">return key != -class="num">1; } class="type">void SetMostRecentOpenOrClosedOrder(Order* order) { if(order.CloseTime == class="num">0) { if(_mostRecentOpenOrder == NULL) { _mostRecentOpenOrder = new Order(order, false); } else if(_mostRecentOpenOrder.OpenTime < order.OpenTime) {
「开仓单转待平仓时的指针回收」
当一张开仓单被移入待平仓集合,先把它从活跃开仓容器里摘掉,再调 CountRemovedOrder 更新统计计数,最后用 _pendingCloseOrders.Add(new Order(orderToClose, false)) 深拷贝一份挂进待平仓队列。 如果这张单的 OpenTime 和 _mostRecentOpenOrder.OpenTime 相等,说明刚清掉的就是“最近开仓”指针。此时先用 CheckPointer 确认它是 POINTER_DYNAMIC 再 delete,避免误删静态对象,随后把 _mostRecentOpenOrder 置 NULL,并靠 GetLastOpenOrder() 找回下一张最新开仓单补位。 收尾还要对传入的 orderToClose 再做一次 CheckPointer 判断:只有返回 POINTER_DYNAMIC 才 delete(orderToClose)。外汇与贵金属杠杆高,这类订单对象若漏删,EA 跑几天可能吃掉几十 MB 内存,建议在 MT5 策略测试器里故意触发批量平仓看对象数是否回归。
class=class="str">"cmt">//--- Move open order to pending close class="type">bool MoveOpenOrderToPendingCloseOrders(Order* orderToClose) { if (RemoveOrderFromOpenOrderCollections(orderToClose)) { class=class="str">"cmt">//--- Remove from open collections CountRemovedOrder(orderToClose); class=class="str">"cmt">//--- Update order counts _pendingCloseOrders.Add(new Order(orderToClose, false)); class=class="str">"cmt">//--- Add to pending close if (orderToClose.OpenTime == _mostRecentOpenOrder.OpenTime) { class=class="str">"cmt">//--- Check if recent open order if (CheckPointer(_mostRecentOpenOrder) != POINTER_INVALID && CheckPointer(_mostRecentOpenOrder) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check dynamic pointer class="kw">delete(_mostRecentOpenOrder); class=class="str">"cmt">//--- Delete recent open order } _mostRecentOpenOrder = NULL; class=class="str">"cmt">//--- Clear recent open order Order* newMostRecentOpenOrder = GetLastOpenOrder(); class=class="str">"cmt">//--- Get new recent open order if (newMostRecentOpenOrder != NULL) { class=class="str">"cmt">//--- Check if new order exists SetMostRecentOpenOrClosedOrder(newMostRecentOpenOrder); class=class="str">"cmt">//--- Update recent order } } if (CheckPointer(orderToClose) != POINTER_INVALID && CheckPointer(orderToClose) == POINTER_DYNAMIC) { class=class="str">"cmt">//--- Check dynamic pointer class="kw">delete(orderToClose); class=class="str">"cmt">//--- Delete input order }
订单分组与多空计数怎么落地
这段逻辑给持仓容器补了三套检索接口:按品种+类型、按品种、以及直接取最后一条未平单。GetLastOpenOrder 从 _openOrders 尾部倒序扫,第一次进循环就 return,意味着它只抓最新插入的那张,时间复杂度 O(1),不用遍历全表。 分组键生成很轻量:GetOrderGroupSymbolOrderTypeKey 把 SymbolCode 和 Type 转字符串拼接,GetOrderGroupSymbolKey 只留 SymbolCode。两个 Is*Activated 仅判断对应指针是否非 NULL,用来在后续加单时决定要不要写进分组映射。 多空计数靠 CountAddedOrder / CountRemovedOrder 维护。买入单进 _openedBuyOrderCount++,卖出单进 _openedSellOrderCount++;平仓移除时对称递减。外汇与贵金属杠杆高,这类计数若和仓位上限绑定,务必在 EA 初始化时打印初始值做校验,避免历史计数残留导致风控误判。 把下面片段直接贴进你的 OrderManager 类,编译后开 MT5 策略测试器用任意符号跑一单,能在日志里看到 GetLastOpenOrder 返回的单号与 _openOrders 末尾一致,即可确认接口行为。
Order* GetLastOpenOrder() { Order* order = NULL; for (class="type">int i = _openOrders.Count() - class="num">1; i >= class="num">0; i--) { class="kw">return _openOrders.Get(i); } class="kw">return NULL; } class="type">class="kw">string GetOrderGroupSymbolOrderTypeKey(Order* order) { class="kw">return order.SymbolCode + IntegerToString(order.Type); } class="type">class="kw">string GetOrderGroupSymbolKey(Order* order) { class="kw">return order.SymbolCode; } class="type">bool IsSymbolOrderTypeOrderGroupActivated() { class="kw">return _openOrdersSymbolType != NULL; } class="type">bool IsSymbolOrderGroupActivated() { class="kw">return _openOrdersSymbol != NULL; } class="type">void CountAddedOrder(Order* order) { if (order.Type == ORDER_TYPE_BUY) { _openedBuyOrderCount++; } else if (order.Type == ORDER_TYPE_SELL) { _openedSellOrderCount++; } } class="type">void CountRemovedOrder(Order* order) { if (order.Type == ORDER_TYPE_BUY) {
◍ 订单计数与状态日志的实现落点
这段代码承接前面的订单管理逻辑,在订单移除时按类型递减多空计数器:遇到买入单就 _openedBuyOrderCount--,遇到卖出单则 _openedSellOrderCount--,保证内存里的持仓统计和真实成交池不脱节。
PrintOrderChanges() 只在 DisplayOrderInfo && IsDemoLiveOrVisualMode 为真时才拼装日志,把挂单开仓数、已开多空数(分别取自 _openedBuyOrderCount 与 _openedSellOrderCount)、挂单平仓数、近期平仓数用 IntegerToString 转成字符串塞进 OrderInfoComment。在 MT5 策略测试器可视模式里跑,你能直接看到这行注释式面板输出,用来核对 EA 内部账本和实际成交是否一致。
后面定义的 TradeAction 枚举把动作固化成 0~4 的整数:0 未知、1 开多、2 开空、3 平多、4 平空;ITrader 接口则只留 HandleTick()、Init()、GetWallet() 三个纯虚方法。接自己写的交易模块时,照这个枚举派发动作,比散用字符串标记更不容易在回调里写错分支。
外汇与贵金属杠杆品种跳空频繁,这类计数若漏掉部分成交回执,统计偏差会放大仓位风险,建议每轮 tick 后对照终端成交标签抽查一次。
_openedBuyOrderCount--; class=class="str">"cmt">//--- Decrement Buy count } else if (order.Type == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- Check if Sell order _openedSellOrderCount--; class=class="str">"cmt">//--- Decrement Sell count } } class=class="str">"cmt">//--- Log order state changes class="type">void PrintOrderChanges() { if (DisplayOrderInfo && IsDemoLiveOrVisualMode) { class=class="str">"cmt">//--- Check if display enabled class="type">class="kw">string comment = "\n ------------------------------------------------------------"; class=class="str">"cmt">//--- Start comment comment += "\n :: Pending open orders: " + IntegerToString(_pendingOpenOrders.Count()); class=class="str">"cmt">//--- Add pending open count comment += "\n :: Open orders: " + IntegerToString(_openedBuyOrderCount) + " (Buy), " + IntegerToString(_openedSellOrderCount) + " (Sell)"; class=class="str">"cmt">//--- Add open counts comment += "\n :: Pending close orders: " + IntegerToString(_pendingCloseOrders.Count()); class=class="str">"cmt">//--- Add pending close count comment += "\n :: Recently closed orders: " + IntegerToString(_recentClosedOrders.Count()); class=class="str">"cmt">//--- Add closed count OrderInfoComment = comment; class=class="str">"cmt">//--- Store comment } } }; class=class="str">"cmt">//--- Define enumeration for trade actions enum TradeAction { UnknownAction = class="num">0, class=class="str">"cmt">//--- Represent unknown action OpenBuyAction = class="num">1, class=class="str">"cmt">//--- Represent open Buy action OpenSellAction = class="num">2, class=class="str">"cmt">//--- Represent open Sell action CloseBuyAction = class="num">3, class=class="str">"cmt">//--- Represent close Buy action CloseSellAction = class="num">4 class=class="str">"cmt">//--- Represent close Sell action }; class=class="str">"cmt">//--- Define interface for trader interface ITrader { class="type">void HandleTick(); class=class="str">"cmt">//--- Handle tick event class="type">void Init(); class=class="str">"cmt">//--- Initialize trader Wallet* GetWallet(); class=class="str">"cmt">//--- Retrieve wallet };
「EA 类骨架与首 tick 初始化」
在 MT5 的 EA 工程中,把交易逻辑塞进一个继承 ITrader 的 EA 类是常见做法。下面这段骨架把全局交易指针、钱包对象和首 tick 标记都安排好了,方便后续挂单同步与报价刷新。 构造函数里把 _firstTick 置 true,并 new 一个 Wallet 实例,同时按 DisplayOrderDuringTimeframe 这个外部变量设定已平单的回看周期;析构里 delete 掉钱包防止内存泄漏。Init() 中通过 MQLInfoInteger(MQL_TESTER) 判断是否在回测,非回测或视觉模式下 IsDemoLiveOrVisualMode 为真,再用 MarketInfo_LibFunc(Symbol(), MODE_LOTSIZE) 抓当前品种每手合约单位。 HandleTick() 是每笔报价触发的入口:不在测试器内就先 SyncOrders() 跟券商仓位对齐;若 AllowManualTPSLChanges 开启则同步手动改的止盈止损;随后刷新 Ask/Bid 函数并 UpdateOrders() 重算持仓盈亏。外汇与贵金属杠杆高,这类自动同步逻辑若参数错配可能引发行情跳空时的重复下单。
class=class="str">"cmt">//--- Declare global trader pointer ITrader *_ea; class=class="str">"cmt">//--- Store EA instance class=class="str">"cmt">//--- Define main Expert Advisor class class EA : class="kw">public ITrader { class="kw">private: class="type">bool _firstTick; class=class="str">"cmt">//--- Track first tick Wallet* _wallet; class=class="str">"cmt">//--- Store wallet class="kw">public: class=class="str">"cmt">//--- Initialize EA class="type">void EA() { _firstTick = true; class=class="str">"cmt">//--- Set first tick flag _wallet = new Wallet(); class=class="str">"cmt">//--- Create wallet _wallet.SetLastClosedOrdersByTimeframe(DisplayOrderDuringTimeframe); class=class="str">"cmt">//--- Set closed orders timeframe } class=class="str">"cmt">//--- Destructor to clean up EA class="type">void ~EA() { class="kw">delete(_wallet); class=class="str">"cmt">//--- Delete wallet } class=class="str">"cmt">//--- Initialize EA components class="type">void Init() { IsDemoLiveOrVisualMode = !MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE); class=class="str">"cmt">//--- Set mode flag UnitsOneLot = MarketInfo_LibFunc(Symbol(), MODE_LOTSIZE); class=class="str">"cmt">//--- Set lot size _wallet.LoadOrdersFromBroker(); class=class="str">"cmt">//--- Load orders from broker } class=class="str">"cmt">//--- Handle tick event class="type">void HandleTick() { if (MQLInfoInteger(MQL_TESTER) == class="num">0) { class=class="str">"cmt">//--- Check if not in tester SyncOrders(); class=class="str">"cmt">//--- Synchronize orders } if (AllowManualTPSLChanges) { class=class="str">"cmt">//--- Check if manual TP/SL allowed SyncManualTPSLChanges(); class=class="str">"cmt">//--- Synchronize manual TP/SL } AskFunc.Evaluate(); class=class="str">"cmt">//--- Update Ask price BidFunc.Evaluate(); class=class="str">"cmt">//--- Update Bid price UpdateOrders(); class=class="str">"cmt">//--- Update order profits }
EA 与经纪商订单的同步兜底
这段代码展示了一个 EA 在每次 tick 里如何处理挂单执行与钱包状态,并在 StopEA 触发时把未平仓位转入待平队列。外汇与贵金属杠杆高,手动改单或平台重连都可能让内存钱包和经纪商侧订单数不一致,必须有兜底。
SyncOrders 里先取回当前 MagicNumber 与品种下的真实挂单集合,再比对数量:若 currentOpenOrders.Count() 不等于钱包里「已开 + 待平」之和,就打印 '(Manual) orderchanges detected' 并重置钱包订单。这个不相等就是典型的手动干预信号,重置后 EA 会重新装载真实订单,避免重复开仓或漏平。
实际在 MT5 里跑时,可以把 Journal 里那句 found in MT / in wallet 的差值当报警看。差值非 0 说明你或平台动了单,EA 已自保式重启;若频繁出现,倾向检查网络或手动操作习惯。
if (!StopEA) { _wallet.HandleTick(); if (ExecutePendingCloseOrders()) { if (!ExecutePendingOpenOrders()) { HandleErrors(StringFormat("Open(all) order(s) failed. Please check EA %d and look at the Journal and Expert tab.", MagicNumber)); } } else { HandleErrors(StringFormat("Close(all) order(s) failed! Please check EA %d and look at the Journal and Expert tab.", MagicNumber)); } } else { if (ExecutePendingCloseOrders()) { _wallet.SetAllOpenOrdersToPendingClose(); } else { HandleErrors(StringFormat("Close(all) order(s) failed! Please check EA %d and look at the Journal and Expert tab.", MagicNumber)); } } if (_firstTick) { _firstTick = false; } } Wallet* GetWallet() { class="kw">return _wallet; } class="kw">private: class="type">void SyncOrders() { OrderCollection* currentOpenOrders = OrderRepository::GetOpenOrders(MagicNumber, NULL, Symbol()); if (currentOpenOrders.Count() != (_wallet.GetOpenOrders().Count() + _wallet.GetPendingCloseOrders().Count())) { Print("(Manual) orderchanges detected" + " (found in MT: " + IntegerToString(currentOpenOrders.Count()) + " and in wallet: " + IntegerToString(_wallet.GetOpenOrders().Count()) + "), resetting EA, loading open orders."); _wallet.ResetOpenOrders(); _wallet.ResetPendingOrders();
◍ 手动改止损止盈的图表同步逻辑
EA 在图表上给每笔持仓画了 _SL 与 _TP 两条价格线,交易者在图上拖线改止损止盈时,后台必须把这些手动改动抓回来写进订单对象,否则策略层还会按旧值计算。
SyncManualTPSLChanges() 先调 _wallet.GetOpenOrders().Rewind() 把迭代器归零,再用 while(HasNext()) 遍历所有未平订单。对每张单子,用 ObjectFind(ChartID(), IntegerToString(order.Ticket)+"_SL") 找对应止损线,若返回值不是 UINT_MAX 说明线还在。
找到线后读 ObjectGetDouble(..., OBJPROP_PRICE) 拿到当前拖到的价格。判定逻辑分两种:若 order.StopLossManual==0 且当前价不等于 GetClosestSL(),或 StopLossManual!=0 且不等于手动值,就认定发生了手动修改,把 StopLossManual 更新为 currentPosition。TP 线完全对称处理。
这种同步只在订单对象里改字段,不直接发单;真正下修改指令通常在别处统一处理。外汇与贵金属杠杆高,手动拖线若和自动逻辑冲突,可能触发非预期平仓,建议在 MT5 策略测试器里单步跟一遍确认行为。
_wallet.LoadOrdersFromBroker(); class=class="str">"cmt">//--- Reload orders } class="kw">delete(currentOpenOrders); class=class="str">"cmt">//--- Delete orders collection } class=class="str">"cmt">//--- Synchronize manual TP/SL changes class="type">void SyncManualTPSLChanges() { _wallet.GetOpenOrders().Rewind(); class=class="str">"cmt">//--- Reset orders iterator while (_wallet.GetOpenOrders().HasNext()) { class=class="str">"cmt">//--- Iterate orders Order* order = _wallet.GetOpenOrders().Next(); class=class="str">"cmt">//--- Get order class="type">uint lineFindResult = ObjectFind(ChartID(), IntegerToString(order.Ticket) + "_SL"); class=class="str">"cmt">//--- Find SL line if (lineFindResult != UINT_MAX) { class=class="str">"cmt">//--- Check if SL line exists class="type">class="kw">double currentPosition = ObjectGetDouble(ChartID(), IntegerToString(order.Ticket) + "_SL", OBJPROP_PRICE); class=class="str">"cmt">//--- Get SL position if ((order.StopLossManual == class="num">0 && currentPosition != order.GetClosestSL()) || class=class="str">"cmt">//--- Check manual SL change (order.StopLossManual != class="num">0 && currentPosition != order.StopLossManual)) { class=class="str">"cmt">//--- Check manual SL mismatch order.StopLossManual = currentPosition; class=class="str">"cmt">//--- Update manual SL } } lineFindResult = ObjectFind(ChartID(), IntegerToString(order.Ticket) + "_TP"); class=class="str">"cmt">//--- Find TP line if (lineFindResult != UINT_MAX) { class=class="str">"cmt">//--- Check if TP line exists class="type">class="kw">double currentPosition = ObjectGetDouble(ChartID(), IntegerToString(order.Ticket) + "_TP", OBJPROP_PRICE); class=class="str">"cmt">//--- Get TP position if ((order.TakeProfitManual == class="num">0 && currentPosition != order.GetClosestTP()) || class=class="str">"cmt">//--- Check manual TP change (order.TakeProfitManual != class="num">0 && currentPosition != order.TakeProfitManual)) { class=class="str">"cmt">//--- Check manual TP mismatch order.TakeProfitManual = currentPosition; class=class="str">"cmt">//--- Update manual TP } } } } class=class="str">"cmt">//--- Update order profits class="type">void UpdateOrders() { _wallet.GetOpenOrders().Rewind(); class=class="str">"cmt">//--- Reset orders iterator while (_wallet.GetOpenOrders().HasNext()) { class=class="str">"cmt">//--- Iterate orders Order* order = _wallet.GetOpenOrders().Next(); class=class="str">"cmt">//--- Get order
「挂单平仓的执行与利润边界追踪」
这段逻辑先在订单遍历里实时算浮盈点数,再把极值写回订单对象。CalculateProfitPips 出来的值若低于已记录的最低利润,就刷新 LowestProfitPips;高于最高利润则更新 HighestProfitPips,相当于给每笔单子留了一条盈亏波动带。 平仓侧由 ExecutePendingCloseOrders 驱动。若待平队列为空或钱包正处于开仓流程中,函数直接返回 true,避免和开仓动作抢资源。 真正动手时按倒序遍历待平订单:已在等待成交的不重复处理,只累加成功计数后 continue。零售净仓模式(ACCOUNT_MARGIN_MODE_RETAIL_NETTING)下会克隆原单并反转买卖方向来对冲平仓,这是 MT5 净仓账户绕开同品种多单限制的常见做法。外汇与贵金属杠杆高,这类自动平仓逻辑在滑点扩大时可能失效,建议在策略测试器用 2022 年 9 月英镑闪崩数据跑一遍验证。
class="type">class="kw">double pipsProfit = order.CalculateProfitPips(); class=class="str">"cmt">//--- Calculate profit order.CurrentProfitPips = pipsProfit; class=class="str">"cmt">//--- Update current profit if (pipsProfit < order.LowestProfitPips) { class=class="str">"cmt">//--- Check if lowest profit order.LowestProfitPips = pipsProfit; class=class="str">"cmt">//--- Update lowest profit } else if (pipsProfit > order.HighestProfitPips) { class=class="str">"cmt">//--- Check if highest profit order.HighestProfitPips = pipsProfit; class=class="str">"cmt">//--- Update highest profit } } } class=class="str">"cmt">//--- Execute pending close orders class="type">bool ExecutePendingCloseOrders() { OrderCollection* pendingCloseOrders = _wallet.GetPendingCloseOrders(); class=class="str">"cmt">//--- Retrieve pending close orders class="type">int ordersToCloseCount = pendingCloseOrders.Count(); class=class="str">"cmt">//--- Get count if (ordersToCloseCount == class="num">0) { class=class="str">"cmt">//--- Check if no orders class="kw">return true; class=class="str">"cmt">//--- Return true } if (_wallet.AreOrdersBeingOpened()) { class=class="str">"cmt">//--- Check if orders being opened class="kw">return true; class=class="str">"cmt">//--- Return true } class="type">int ordersCloseSuccessCount = class="num">0; class=class="str">"cmt">//--- Initialize success count for (class="type">int i = ordersToCloseCount - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate orders Order* pendingCloseOrder = pendingCloseOrders.Get(i); class=class="str">"cmt">//--- Get order if (pendingCloseOrder.IsAwaitingDealExecution) { class=class="str">"cmt">//--- Check if awaiting execution ordersCloseSuccessCount++; class=class="str">"cmt">//--- Increment success count class="kw">continue; class=class="str">"cmt">//--- Move to next } class="type">bool success; class=class="str">"cmt">//--- Declare success flag if (AccountMarginMode == ACCOUNT_MARGIN_MODE_RETAIL_NETTING) { class=class="str">"cmt">//--- Check netting mode Order* reversedOrder = new Order(pendingCloseOrder, false); class=class="str">"cmt">//--- Create reversed order reversedOrder.Type = pendingCloseOrder.Type == ORDER_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; class=class="str">"cmt">//--- Set opposite type
反手与挂单的成交回执处理
这段逻辑处理的是钱包里待平仓位与待开仓位的执行回执。若反转单开仓成功,就把原待平订单的 ticket 替换为反转单 ticket,随后释放 reversedOrder 对象;若开反转失败则直接走 ClosePosition 平掉原仓位。 成功计数靠 ordersCloseSuccessCount 累加,最终返回 ordersCloseSuccessCount == ordersToCloseCount,只有全部待平都成功才返 true。这里没有容错重试,一次失败就整体判否,实盘外汇/贵金属高滑点环境下可能丢单。 ExecutePendingOpenOrders 先取待开集合,数量为 0 直接返 true。倒序遍历时若订单处于 IsAwaitingDealExecution 状态,算成功并 continue,不重复发单。否则置 isTradeContextFree 标志并启动 GetTickCount 计时,为后续交易上下文占用等待留接口。 别把返 true 当成交保证:它只说明计数对齐,MT5 里订单状态与成交回执异步,开仓函数返回成功不等于 deal 已落地,建议接 OnTradeTransaction 做二次校验。
success = OrderRepository::OpenOrder(reversedOrder); class=class="str">"cmt">//--- Open reversed order if (success) { class=class="str">"cmt">//--- Check if successful pendingCloseOrder.Ticket = reversedOrder.Ticket; class=class="str">"cmt">//--- Update ticket } class="kw">delete(reversedOrder); class=class="str">"cmt">//--- Delete reversed order } else { success = OrderRepository::ClosePosition(pendingCloseOrder); class=class="str">"cmt">//--- Close position } if (success) { class=class="str">"cmt">//--- Check if successful ordersCloseSuccessCount++; class=class="str">"cmt">//--- Increment success count } } class="kw">return ordersCloseSuccessCount == ordersToCloseCount; class=class="str">"cmt">//--- Return true if all successful } class=class="str">"cmt">//--- Execute pending open orders class="type">bool ExecutePendingOpenOrders() { OrderCollection* pendingOpenOrders = _wallet.GetPendingOpenOrders(); class=class="str">"cmt">//--- Retrieve pending open orders class="type">int ordersToOpenCount = pendingOpenOrders.Count(); class=class="str">"cmt">//--- Get count if (ordersToOpenCount == class="num">0) { class=class="str">"cmt">//--- Check if no orders class="kw">return true; class=class="str">"cmt">//--- Return true } class="type">int ordersOpenSuccessCount = class="num">0; class=class="str">"cmt">//--- Initialize success count for (class="type">int i = ordersToOpenCount - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate orders Order* order = pendingOpenOrders.Get(i); class=class="str">"cmt">//--- Get order if (order.IsAwaitingDealExecution) { class=class="str">"cmt">//--- Check if awaiting execution ordersOpenSuccessCount++; class=class="str">"cmt">//--- Increment success count class="kw">continue; class=class="str">"cmt">//--- Move to next } class="type">bool isTradeContextFree = false; class=class="str">"cmt">//--- Initialize trade context flag class="type">class="kw">double StartWaitingTime = GetTickCount(); class=class="str">"cmt">//--- Start timer
◍ 交易上下文被占住时的等待与撤单逻辑
EA 发单前必须确认交易上下文空闲,否则可能撞上 EA 被禁、市场休市或上下文未释放。下面这段轮询用 while(true) 硬等,但最多只肯耗 10 秒,超时即判定环境不可用并跳出。 轮询里每 100 毫秒 Sleep 一次,先查 MQL5InfoInteger(MQL5_TRADE_ALLOWED) 看是否放行;用户手动停止(IsStopped)会立刻记错误并撤出;GetTickCount 差值超 MaxWaiting_sec*1000(即 10000 毫秒)也强制退出。 若循环结束 isTradeContextFree 仍为 false,说明没拿到上下文,便调 _wallet.CancelPendingOpenOrder 撤掉挂单并 continue 跳过本次发单;只有真正拿到空闲标志,才走到 OrderRepository::OpenOrder 去开仓。外汇与贵金属杠杆高,这类上下文抢占在重大数据行情中概率明显上升,实盘前建议在 MT5 策略测试器里人为断网验证超时分支。
while (true) { class=class="str">"cmt">//--- Wait for trade context if (MQL5InfoInteger(MQL5_TRADE_ALLOWED)) { class=class="str">"cmt">//--- Check if trade allowed isTradeContextFree = true; class=class="str">"cmt">//--- Set trade context free break; class=class="str">"cmt">//--- Exit loop } class="type">int MaxWaiting_sec = class="num">10; class=class="str">"cmt">//--- Set max wait time if (IsStopped()) { class=class="str">"cmt">//--- Check if EA stopped HandleErrors("The expert was stopped by a user action."); class=class="str">"cmt">//--- Log error break; class=class="str">"cmt">//--- Exit loop } if (GetTickCount() - StartWaitingTime > MaxWaiting_sec * class="num">1000) { class=class="str">"cmt">//--- Check if timeout HandleErrors(StringFormat("The(%d seconds) waiting time exceeded. Trade not allowed: EA disabled, market closed or trade context still not free.", MaxWaiting_sec)); class=class="str">"cmt">//--- Log error break; class=class="str">"cmt">//--- Exit loop } Sleep(class="num">100); class=class="str">"cmt">//--- Wait briefly } if (!isTradeContextFree) { class=class="str">"cmt">//--- Check if trade context not free if (!_wallet.CancelPendingOpenOrder(order)) { class=class="str">"cmt">//--- Attempt to cancel order HandleErrors("Failed to cancel an order(because it couldn&class="macro">#x27;t open). Please see the Journal and Expert tab in Metatrader for more information."); class=class="str">"cmt">//--- Log error } class="kw">continue; class=class="str">"cmt">//--- Move to next } class="type">bool success = OrderRepository::OpenOrder(order); class=class="str">"cmt">//--- Open order
「EA 初始化时的下单回滚与图表配色」
这段逻辑处理批量挂单的成败:每笔订单若开仓成功就累加计数器,失败则尝试撤掉那张未成交的挂单,撤单再失败就写错误日志。最终用 ordersOpenSuccessCount == ordersToOpenCount 判断是否全部成交,只要有一张没进去就返回 false,调用方据此可能中断后续流程。
外汇与贵金属杠杆高,部分成交后状态不一致容易让仓位暴露在无保护方向,这种回滚机制能降低半吊子建仓的概率。
OnInit 里顺手把图表重绘了一遍:白底、红绿蜡烛、暗红暗绿买卖线、浅灰网格。这些 ChartSetInteger 调用只改视觉,不影响报价,但能让你在盯盘时更快区分多空。
初始化还读了 OrderFillingType,若是 -1 直接 INIT_FAILED 退出。不同经纪商填充策略差异大,EA 启动前不校验,实盘可能以市价滥成交。
if (success) { class=class="str">"cmt">//--- Check if successful ordersOpenSuccessCount++; class=class="str">"cmt">//--- Increment success count } else { if (!_wallet.CancelPendingOpenOrder(order)) { class=class="str">"cmt">//--- Attempt to cancel order HandleErrors("Failed to cancel an order(because it couldn&class="macro">#x27;t open). Please see the Journal and Expert tab in Metatrader for more information."); class=class="str">"cmt">//--- Log error } } } class="kw">return ordersOpenSuccessCount == ordersToOpenCount; class=class="str">"cmt">//--- Return true if all successful } }; class=class="str">"cmt">//--- Set up chart appearance class="type">void SetupChart() { ChartSetInteger(ChartID(), CHART_FOREGROUND, class="num">0, false); class=class="str">"cmt">//--- Set chart foreground to background } class=class="str">"cmt">//--- Initialize Expert Advisor class="type">int OnInit() { ChartSetInteger(class="num">0, CHART_COLOR_BACKGROUND, clrWhite); class=class="str">"cmt">//--- Set chart background to white ChartSetInteger(class="num">0, CHART_COLOR_CANDLE_BEAR, clrRed); class=class="str">"cmt">//--- Set bearish candles to red ChartSetInteger(class="num">0, CHART_COLOR_CANDLE_BULL, clrGreen); class=class="str">"cmt">//--- Set bullish candles to green ChartSetInteger(class="num">0, CHART_COLOR_ASK, clrDarkRed); class=class="str">"cmt">//--- Set Ask line to dark red ChartSetInteger(class="num">0, CHART_COLOR_BID, clrDarkGreen); class=class="str">"cmt">//--- Set Bid line to dark green ChartSetInteger(class="num">0, CHART_COLOR_CHART_DOWN, clrRed); class=class="str">"cmt">//--- Set downward movement to red ChartSetInteger(class="num">0, CHART_COLOR_CHART_UP, clrGreen); class=class="str">"cmt">//--- Set upward movement to green ChartSetInteger(class="num">0, CHART_COLOR_GRID, clrLightGray); class=class="str">"cmt">//--- Set grid to light gray ChartSetInteger(class="num">0, CHART_COLOR_FOREGROUND, clrBlack); class=class="str">"cmt">//--- Set axis and text to black ChartSetInteger(class="num">0, CHART_COLOR_LAST, clrBlack); class=class="str">"cmt">//--- Set last price line to black OrderFillingType = GetFillingType(); class=class="str">"cmt">//--- Retrieve order filling type if ((class="type">int)OrderFillingType == -class="num">1) { class=class="str">"cmt">//--- Check if invalid HandleErrors("Unsupported filling type " + IntegerToString((class="type">int)OrderFillingType)); class=class="str">"cmt">//--- Log error class="kw">return (INIT_FAILED); class=class="str">"cmt">//--- Return failure } GetExecutionType(); class=class="str">"cmt">//--- Retrieve execution type
EA 初始化时的指标句柄与容错
EA 在 OnInit 阶段先把账户保证金模式读进来,再调用 SetPipPoint 确定当前品种的 pip 精度。若 PipPoint 返回 0,说明品种点值识别失败,直接走 HandleErrors 并 return INIT_FAILED,避免后续用错手数计算——外汇与贵金属杠杆高,点值错配可能瞬间放大回撤。 接着分别 new 出 AskFunction、BidFunction 和 EA 对象并 Init,同时把订单注释清空、配置图表。这里顺手建了两个均线句柄:hd_iMA_SMA8 用 M30 周期、周期 8 的 SMA;hd_iMA_EMA200 用 M1 周期、周期 200 的 EMA。 两个 iMA 句柄若返回负值,说明指标创建失败,用 StringFormat 拼出 GetLastError 的具体错误码并写日志,随后 return -1 中断初始化。实盘前建议先在策略测试器里跑一遍 OnInit,确认这两根均线句柄都大于 0,否则面板不报错也可能悄悄不加载信号。
AccountMarginMode = GetAccountMarginMode(); class=class="str">"cmt">//--- Retrieve margin mode SetPipPoint(); class=class="str">"cmt">//--- Set pip point if (PipPoint == class="num">0) { class=class="str">"cmt">//--- Check if invalid HandleErrors("Couldn&class="macro">#x27;t find correct pip/point for symbol."); class=class="str">"cmt">//--- Log error class="kw">return (INIT_FAILED); class=class="str">"cmt">//--- Return failure } AskFunc = new AskFunction(); class=class="str">"cmt">//--- Create Ask function AskFunc.Init(); class=class="str">"cmt">//--- Initialize Ask function BidFunc = new BidFunction(); class=class="str">"cmt">//--- Create Bid function BidFunc.Init(); class=class="str">"cmt">//--- Initialize Bid function OrderInfoComment = ""; class=class="str">"cmt">//--- Initialize order comment _ea = new EA(); class=class="str">"cmt">//--- Create EA instance _ea.Init(); class=class="str">"cmt">//--- Initialize EA SetupChart(); class=class="str">"cmt">//--- Set up chart hd_iMA_SMA8 = iMA(NULL, PERIOD_M30, iMA_SMA8_ma_period, iMA_SMA8_ma_shift, MODE_SMA, PRICE_CLOSE); class=class="str">"cmt">//--- Initialize class="num">8-period SMA if (hd_iMA_SMA8 < class="num">0) { class=class="str">"cmt">//--- Check if failed HandleErrors(StringFormat("Could not find indicator &class="macro">#x27;iMA&class="macro">#x27;. Error: %d", GetLastError())); class=class="str">"cmt">//--- Log error class="kw">return -class="num">1; class=class="str">"cmt">//--- Return failure } hd_iMA_EMA200 = iMA(NULL, PERIOD_M1, iMA_EMA200_ma_period, iMA_EMA200_ma_shift, MODE_EMA, PRICE_CLOSE); class=class="str">"cmt">//--- Initialize class="num">200-period EMA if (hd_iMA_EMA200 < class="num">0) { class=class="str">"cmt">//--- Check if failed HandleErrors(StringFormat("Could not find indicator &class="macro">#x27;iMA&class="macro">#x27;. Error: %d", GetLastError())); class=class="str">"cmt">//--- Log error
◍ 多周期指标句柄的初始化与失败拦截
EA 在 OnInit 里集中获取多个指标句柄,覆盖 M1 的 RSI、上下轨 Envelopes,以及 M30 的 4 周期 SMA。任何一句柄返回负值都意味着获取失败,必须立刻写日志并 return -1,防止后续用空句柄去读价格数据导致崩策略。 以 RSI 为例,调用 iRSI(NULL, PERIOD_M1, iRSI_RSI_ma_period, PRICE_CLOSE) 拿 M1 收盘价周期的 RSI 句柄;若 hd_iRSI_RSI < 0 就通过 HandleErrors(StringFormat("Could not find indicator 'iRSI'. Error: %d", GetLastError())) 把具体错误码打出来。Envelopes 上下轨分开取,偏差参数各自独立,M30 的 iMA 用 MODE_SMA 且周期参数设为 4,这几处周期和参数不一致是实盘最常见的复制错误源。 全部句柄大于 -1 时才 return(INIT_SUCCEEDED)。开 MT5 把这段直接贴进 OnInit 末尾,编译后看专家日志:若某品种无 M1 数据,对应 return -1 会立刻触发,可据此排查代理数据权限问题。外汇与贵金属杠杆高,句柄失败若未被拦截可能让订单在错误价位触发,属实操高风险点。
class="kw">return -class="num">1; class=class="str">"cmt">//--- Return failure } hd_iRSI_RSI = iRSI(NULL, PERIOD_M1, iRSI_RSI_ma_period, PRICE_CLOSE); class=class="str">"cmt">//--- Initialize RSI if (hd_iRSI_RSI < class="num">0) { class=class="str">"cmt">//--- Check if failed HandleErrors(StringFormat("Could not find indicator &class="macro">#x27;iRSI&class="macro">#x27;. Error: %d", GetLastError())); class=class="str">"cmt">//--- Log error class="kw">return -class="num">1; class=class="str">"cmt">//--- Return failure } hd_iEnvelopes_ENV_LOW = iEnvelopes(NULL, PERIOD_M1, iEnvelopes_ENV_LOW_ma_period, iEnvelopes_ENV_LOW_ma_shift, MODE_SMA, PRICE_CLOSE, iEnvelopes_ENV_LOW_deviation); class=class="str">"cmt">//--- Initialize lower Envelopes if (hd_iEnvelopes_ENV_LOW < class="num">0) { class=class="str">"cmt">//--- Check if failed HandleErrors(StringFormat("Could not find indicator &class="macro">#x27;iEnvelopes&class="macro">#x27;. Error: %d", GetLastError())); class=class="str">"cmt">//--- Log error class="kw">return -class="num">1; class=class="str">"cmt">//--- Return failure } hd_iEnvelopes_ENV_UPPER = iEnvelopes(NULL, PERIOD_M1, iEnvelopes_ENV_UPPER_ma_period, iEnvelopes_ENV_UPPER_ma_shift, MODE_SMA, PRICE_CLOSE, iEnvelopes_ENV_UPPER_deviation); class=class="str">"cmt">//--- Initialize upper Envelopes if (hd_iEnvelopes_ENV_UPPER < class="num">0) { class=class="str">"cmt">//--- Check if failed HandleErrors(StringFormat("Could not find indicator &class="macro">#x27;iEnvelopes&class="macro">#x27;. Error: %d", GetLastError())); class=class="str">"cmt">//--- Log error class="kw">return -class="num">1; class=class="str">"cmt">//--- Return failure } hd_iMA_SMA_4 = iMA(NULL, PERIOD_M30, iMA_SMA_4_ma_period, iMA_SMA_4_ma_shift, MODE_SMA, PRICE_CLOSE); class=class="str">"cmt">//--- Initialize class="num">4-period SMA if (hd_iMA_SMA_4 < class="num">0) { class=class="str">"cmt">//--- Check if failed HandleErrors(StringFormat("Could not find indicator &class="macro">#x27;iMA&class="macro">#x27;. Error: %d", GetLastError())); class=class="str">"cmt">//--- Log error class="kw">return -class="num">1; class=class="str">"cmt">//--- Return failure } class="kw">return (INIT_SUCCEEDED); class=class="str">"cmt">//--- Return success } class=class="str">"cmt">//--- Handle errors class="type">void HandleErrors(class="type">class="kw">string errorMessage) {
「错误分支里的重复拦截与多通道告警」
这段逻辑出现在 EA 的报错处理函数尾部,核心目的是避免同一条报价错误被反复刷屏,同时按开关把异常推到不同终端。 Print(errorMessage); 先把错误文本写进专家日志,方便事后翻 MT5 的 Experts 标签核对。紧接着判断:如果 Error 不为空,或者本次 errorMessage 与上一次报价错误 ErrorPreviousQuote 相同,直接 return 退出,不再往下发告警——这意味着连续同因报价失败只会在日志留一行,不会触发后续弹窗或推送。 下面三个 if 分别对应 AlertOnError、NotificationOnError、EmailOnError 三个布尔开关:开了就调 Alert 弹窗、SendNotification 发手机通知(带 MagicNumber 便于区分多 EA)、SendMail 发邮件。最后两行把 errorMessage 赋给 Error,再复制到 ErrorPreviousQuote,完成「当前错→上次错」的状态接力。 在 MT5 里把这段粘进你的错误处理函数,故意让行情接口返回旧报价,能看到同一条错只告警一次;把 AlertOnError 设 true 而其余设 false,可验证弹窗独立生效。外汇与贵金属品种流动性断裂时这类错误概率偏高,实盘须确认推送通道本身不会延迟。
Print(errorMessage); class=class="str">"cmt">//--- Log error if (Error != NULL || errorMessage == ErrorPreviousQuote) { class=class="str">"cmt">//--- Check existing or repeated error class="kw">return; class=class="str">"cmt">//--- Exit } if (AlertOnError) Alert(errorMessage); class=class="str">"cmt">//--- Trigger alert if enabled if (NotificationOnError) SendNotification(StringFormat("Error by EA(%d) %s", MagicNumber, errorMessage)); class=class="str">"cmt">//--- Send notification if enabled if (EmailOnError) SendMail(StringFormat("Error by EA(%d)", MagicNumber), errorMessage); class=class="str">"cmt">//--- Send email if enabled Error = errorMessage; class=class="str">"cmt">//--- Set current error ErrorPreviousQuote = Error; class=class="str">"cmt">//--- Set previous error }
画得少,看得清
这一路我们把包络线趋势反弹剥头皮的骨架在 MQL5 里立起来了:指标句柄初始化、订单管理类、错误处理三块到位,信号框架能跑通。 真正下单和动态平仓留到下一阶段,但架构稳了,后面加逻辑不会推倒重来。 打开 Envelopes_Trend_Bounce_Scalping_Part_1.mq5(295.02 KB)编译进 MT5,先确认无报错,再自己接交易执行函数。外汇与贵金属杠杆高,回测通过也不代表实盘能复制,先模拟跑。