理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统·综合运用
◍ 按魔术码与品种过滤平仓
在 EA 的持仓遍历逻辑里,先通过 PositionGetInteger 和 PositionGetString 把当前持仓的魔术码、品种、类型抓出来,再用 SymbolInfoInteger 拿该品种的报价小数位,决定后续 PrintFormat 里价格显示几位。 过滤条件写死为 positionMagic == magicNumber && positionSymbol == _Symbol,意味着只处理本 EA 自己下的、且品种与当前图表一致的单;其余来源的仓位一律不动,避免误平手动单或其他 EA 的仓。 平仓动作调用 myTrade.PositionClose(positionTicket, SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * 3),第二个参数是允许滑点,取当前点差乘以 3。外汇与贵金属点差随流动性跳动,实盘里这个 3 倍系数可能在某些时段偏紧导致平仓失败,建议你在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍看成交率。 成功后若 enableAlerts 为 true 会弹 Alert 提示,并 PrintFormat 输出 ticket 与类型;失败则进入 else 分支处理。下面这段是原文核心片段,逐行对应上面逻辑。
positionMagic = PositionGetInteger(POSITION_MAGIC); positionSymbol = PositionGetString(POSITION_SYMBOL); positionType = PositionGetInteger(POSITION_TYPE); class="type">int positionDigits= (class="type">int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS); class="type">class="kw">double positionVolume = PositionGetDouble(POSITION_VOLUME); class="type">ENUM_POSITION_TYPE positionType = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); if(positionMagic == magicNumber && positionSymbol == _Symbol) class=class="str">"cmt">//close the position { class=class="str">"cmt">//print the position details Print("*********************************************************************"); PrintFormat( "#%I64u %s %s %.2f %s [%I64d]", positionTicket, positionSymbol, EnumToString(positionType), positionVolume, DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), positionDigits), positionMagic ); class=class="str">"cmt">//print the position close details PrintFormat("Close #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); class=class="str">"cmt">//send the tradeRequest if(myTrade.PositionClose(positionTicket, SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * class="num">3)) class=class="str">"cmt">//success, position has been closed { if(enableAlerts) { Alert( _Symbol + " PROFIT LIQUIDATION: Just successfully closed POSITION(#" + IntegerToString(positionTicket) + "). Check the EA journal for more details." ); } PrintFormat("Just successfully closed position: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); myTrade.PrintResult(); } else class=class="str">"cmt">//trade tradeRequest failed
「平仓报错与手数推演的实盘代码片段」
EA 在利润平仓失败时,会先判断 enableAlerts 开关,若开启则弹窗报警,把品种名、持仓 ticket 号拼进提示语,方便你立刻在终端定位是哪一笔单子没平掉。 随后用 PrintFormat 把平仓失败的单号、符号、多空类型以及 GetLastError 的错误码打到日志,这一步是排查 MT5 交易类故障的最低成本手段,外汇与贵金属品种在高波动时滑点拒单概率明显上升,属正常现象但需留痕。 BuySellPosition 里手数不是写死的:用权益除以 10000 再乘最小交易量,归一化到 2 位小数;若已有空单体积大于该值且保证金水平 >200,就补仓式地把空单体积加上去再归一。 手数还有上下限兜底——低于 0.01 取 SYMBOL_VOLUME_MIN,高于 SYMBOL_VOLUME_MAX 就截断为最大值,避免品种限制导致 OrderSend 直接报错。 TP/SL 以点数乘 _Point 偏移计算,买单价加 TP、减 SL,再用 _Digits 对齐报价精度,复制下面代码到 MT5 能直接验证你的品种小数位逻辑。
{
class=class="str">"cmt">//print the information about the operation
if(enableAlerts)
{
Alert(
_Symbol + " ERROR ** PROFIT LIQUIDATION: closing POSITION(#" +
IntegerToString(positionTicket) + "). Check the EA journal for more details."
);
}
PrintFormat("Position clossing failed: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
PrintFormat("OrderSend error %d", GetLastError());class=class="str">"cmt">//print the error code
}
}
}
}
}
class="type">bool CEmaExpertAdvisor::BuySellPosition(class="type">int positionType, class="type">class="kw">string positionComment)
{
class="type">class="kw">double volumeLot = NormalizeDouble(((SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) * AccountInfoDouble(ACCOUNT_EQUITY)) / class="num">10000), class="num">2);
class="type">class="kw">double tpPrice = class="num">0.0, slPrice = class="num">0.0, symbolPrice;
if(positionType == POSITION_TYPE_BUY)
{
if(sellPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > class="num">200)
{
volumeLot = NormalizeDouble((sellPositionsVol + volumeLot), class="num">2);
}
if(volumeLot < class="num">0.01)
{
volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
}
if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))
{
volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
}
volumeLot = NormalizeDouble(volumeLot, class="num">2);
symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(TP > class="num">0)
{
tpPrice = NormalizeDouble(symbolPrice + (TP * _Point), _Digits);
}
if(SL > class="num">0)
{
slPrice = NormalizeDouble(symbolPrice - (SL * _Point), _Digits);
}开仓函数里的手数校准与报错分支
这段逻辑处理的是卖单分支下的手数重算与下单回执。当持仓类型为 POSITION_TYPE_SELL 时,若已有买仓体积 buyPositionsVol 大于本次计划 volumeLot,且账户保证金水平 AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) 超过 200,就把 volumeLot 重算为两者之和并保留两位小数。 手数不会裸奔出边界:低于 0.01 时回退到 SYMBOL_VOLUME_MIN,高于 SYMBOL_VOLUME_MAX 时截断到上限,最后再 NormalizeDouble 一次到 2 位精度。外汇与贵金属杠杆高,保证金水平阈值设 200 只是例子,实盘可能触发强平,需按经纪商规则复核。 下单价以 SYMBOL_BID 为基准,TP/SL 为点数时分别向下减、向上加 _Point 后按 _Digits 规范化。myTrade.Sell 返回真就弹 Alert 并打印结果,假则报 SymbolInfoDouble(SYMBOL_ASK) 处的错误并打出 GetLastError 代码,方便你直接在 MT5 Experts 日志里定位。
if(positionType == POSITION_TYPE_SELL) { if(buyPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > class="num">200) { volumeLot = NormalizeDouble((buyPositionsVol + volumeLot), class="num">2); } if(volumeLot < class="num">0.01) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); } volumeLot = NormalizeDouble(volumeLot, class="num">2); symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(TP > class="num">0) { tpPrice = NormalizeDouble(symbolPrice - (TP * _Point), _Digits); } if(SL > class="num">0) { slPrice = NormalizeDouble(symbolPrice + (SL * _Point), _Digits); } if(myTrade.Sell(volumeLot, NULL, class="num">0.0, slPrice, tpPrice, positionComment)) class=class="str">"cmt">//successfully openend position { if(enableAlerts) { Alert(_Symbol, " Successfully openend SELL POSITION!"); } myTrade.PrintResult(); class="kw">return(true); }
◍ EA 参数注入与卖单报错回退
卖单下单失败时,代码先判断 enableAlerts 开关:开着就弹 Alert 报出品种名和当前 SYMBOL_ASK 价格,方便你立刻在 MT5 终端看到是哪一根tick卡住。 随后用 PrintFormat 把 GetLastError() 的错误码打到日志,return(false) 直接终止本次交易函数,避免重复发单放大滑点风险。外汇与贵金属波动剧烈,这类回退逻辑能防止错单连环触发。 下半段是 EA 的 input 区:magicNumber 默认 101,tradingTimeframe 用 H1,emaPeriod 15、emaShift 0;账户盈利目标 6.0%、亏损停手 10.0%,maxPositions 限 3 单同向,TP 5000 点、SL 500 点。 这些变量通过 CEmaExpertAdvisor ea(...) 构造函数注入,OnInit 里若 ea.GetInit()<=0 就返回 INIT_FAILED,EA 不会启动;OnDeinit 只调 ea.GetDeinit() 做清理。把 SL 从 500 改成 200 能在 MT5 里直观看到止损收紧后的持仓时长变化。
else { if(enableAlerts) { Alert(_Symbol, " ERROR opening a SELL POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK)); } PrintFormat("ERROR: Opening a SELL POSITION: ErrorCode = %d",GetLastError());class=class="str">"cmt">//OrderSend failed, output the error code class="kw">return(class="kw">false); } } class="kw">return(class="kw">false); } class=class="str">"cmt">// Include the CEmaExpertAdvisor file so that it&class="macro">#x27;s code is available in this EA class="macro">#include "EmaExpertAdvisor.mqh" class=class="str">"cmt">//--User input variables input class="type">long magicNumber = class="num">101;class=class="str">"cmt">//Magic Number(Set class="num">0 [Zero] to disable input group "" input ENUM_TIMEFRAMES tradingTimeframe = PERIOD_H1;class=class="str">"cmt">//Trading Timeframe input class="type">int emaPeriod = class="num">15;class=class="str">"cmt">//Moving Average Period input class="type">int emaShift = class="num">0;class=class="str">"cmt">//Moving Average Shift input group "" input class="type">bool enableTrading = true;class=class="str">"cmt">//Enable Trading input class="type">bool enableAlerts = class="kw">false;class=class="str">"cmt">//Enable Alerts input group "" input class="type">class="kw">double accountPercentageProfitTarget = class="num">6.0;class=class="str">"cmt">//Account Percentage(%) Profit Target input class="type">class="kw">double accountPercentageLossTarget = class="num">10.0;class=class="str">"cmt">//Account Percentage(%) Loss Target input group "" input class="type">int maxPositions = class="num">3;class=class="str">"cmt">//Max Positions(Max open positions in one direction) input class="type">int TP = class="num">5000;class=class="str">"cmt">//TP(Take Profit Points/Pips [Zero(class="num">0) to diasable]) input class="type">int SL = class="num">500;class=class="str">"cmt">//SL(Stop Loss Points/Pips [Zero(class="num">0) to diasable]) class=class="str">"cmt">//Create an instance/object of the included CEmaExpertAdvisor class class=class="str">"cmt">//with the user inputed data as the specified constructor parameters CEmaExpertAdvisor ea( magicNumber, tradingTimeframe, emaPeriod, emaShift, enableTrading, enableAlerts, accountPercentageProfitTarget, accountPercentageLossTarget, maxPositions, TP, SL ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- if(ea.GetInit() <= class="num">0) { class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- ea.GetDeinit(); }
「把交易逻辑塞进 OnTick 主循环」
EA 的所有实时动作都从 OnTick() 开始跑,每来一个报价就触发一次。上面这段是典型骨架:先抓 EMA 数值和持仓数据,再判断环境是否允许交易,允许才下单并管损益,最后把状态画到图上。 具体顺序不能乱:ea.GetEma() 拿快慢线,ea.GetPositionsData() 刷新当前仓位,这两步是后面判断的原料。若 TradingIsAllowed() 返回 false,后面整段交易代码直接跳过,只留 PrintOnChart() 刷界面,避免乱单。 在 MT5 里新建 EA 时,把这段直接贴进模板的 OnTick 函数体,就能跑通“指标→判断→交易→展示”的最小闭环。外汇与贵金属杠杆高,实盘前务必用策略测试器以 2023 年 XAUUSD 的 M15 数据回测,观察成交密度与回撤。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- ea.GetEma(); ea.GetPositionsData(); if(ea.TradingIsAllowed()) { ea.TradeNow(); ea.ManageProfitAndLoss(); } ea.PrintOnChart(); }
把这条线请下神坛
随文附带的 ZIP 里压了 8 个文件:PhoneClass.mqh 3.25 KB、SmartPhoneClass.mqh 1.66 KB,以及把过程化 EMA 策略重写后的 OOP_PriceActionEMA.mq5 仅 3 KB,对照 Procedural_PriceActionEMA.mq5 的 23.33 KB,体量差出近 7 倍,模块拆开之后单文件确实轻了。 开 MT5 把 OOP_Article_-_All_Source_Code_Files.zip 解到 MQL5/Include 与 Experts 下,直接编译 OOP_PriceActionEMA.mq5,你能直观看到类封装把原来缠在一起的均线逻辑归到了 EmaExpertAdvisor.mqh 里。 外汇与贵金属杠杆高、回撤可能超预期,跑任何 EA 前先在策略测试器用历史数据验证,别把封装漂亮当成胜率保证。 OOP 只是把现实实体映射成代码结构的工具,不是圣杯;哪天你写 EA 卡在状态管理乱成一团,回来翻这几个 mqh 怎么分层的,比追新框架实在。