基于MQL5的订单剥头皮交易系统·进阶篇

基于MQL5的订单剥头皮交易系统·进阶篇

(2/3)· 承接基础概念,这一篇把EA的OnTick主循环、仓位算法与连亏熔断拆开讲清

新手友好 第 2/3 篇
很多交易者把剥头皮EA加载上就等信号,却看不懂OnTick里订单流比较的时段窗口。参数没调过、连亏上限形同虚设,实盘遇到新闻波动就被扫。先弄清主循环每一步在算什么,比盲目优化阈值更有用。

◍ EA 启动时的参数闸门与每跳检查

MT5 上的 EA 在 OnInit 阶段就应先拦掉非法输入,否则后面算仓位和止损会直接崩。下面这段判断把所有关键参数都卡了底线:VolumeThreshold、OrderFlowPeriod、RiskPercent 等不得 ≤0,RiskPercent 还不得 >100,RSIOverbought 必须严格大于 RSIOversold,MaxConsecutiveLosses 和 MinBarsBetweenTrades 不能 <0。任一不满足就 Print 报错并返回 INIT_FAILED,EA 根本不会加载到图表上。

MQL5 / C++
if(VolumeThreshold <= class="num">0 || OrderFlowPeriod <= class="num">0 || RiskPercent <= class="num">0 || RiskPercent > class="num">100 ||
   ADXPeriod <= class="num">0 || ADXThreshold <= class="num">0 || MAPeriod <= class="num">0 || MaxLotSize <= class="num">0 || ATRPeriod <= class="num">0 ||
   ATRMultiplier <= class="num">0 || RSIPeriod <= class="num">0 || RSIOverbought <= RSIOversold || MAFastPeriod <= class="num">0 ||
   MASlowPeriod <= class="num">0 || BollingerPeriod <= class="num">0 || BollingerDeviation <= class="num">0 || MaxConsecutiveLosses < class="num">0 ||
   MinBarsBetweenTrades < class="num">0)
{
   Print("Error: Invalid class="kw">input parameters.");
   class="kw">return INIT_FAILED;
}

Print("Input parameters validated.");

class=class="str">"cmt">// Initialize global variables
consecutiveLosses = class="num">0;
lastTradeTime = class="num">0;
barsSinceLastTrade = MinBarsBetweenTrades;
逐行看:前 5 行是参数合法性 OR 判断,任一条件为真即进括号;Print 打日志说明哪类错;return INIT_FAILED 终止初始化。通过后 Print 确认参数已校验,随后把连续亏损数、上次交易时间归零,barsSinceLastTrade 预置成最小间隔棒数,相当于冷却计时器先填满。 OnTick 里首先用 IsNewBar() 过滤,不是新 K 线直接 return,避免同根棒内反复触发。接着打印当前连续亏损和冷却棒数,再走 IsTradeAllowed() 权限检查;有持仓就 ManageOpenPositions() 并 exit。无持仓才 barsSinceLastTrade++,之后过 IsRiskAcceptable() 才允许抓订单流和指标数据。
MQL5 / C++
class="type">void OnTick()
{
   if(!IsNewBar())
      class="kw">return;

   Print("Current state - Consecutive losses: ", consecutiveLosses,
         ", Bars since last trade: ", barsSinceLastTrade);

   if(!IsTradeAllowed())
   {
      Print("Trading not allowed. Check EA configuration and account permissions.");
      class="kw">return;
   }

   class=class="str">"cmt">// Check if there&class="macro">#x27;s an open position and manage it
   if(PositionExists())
   {
      ManageOpenPositions();
      class="kw">return; class=class="str">"cmt">// Exit if there&class="macro">#x27;s an open position
   }

   barsSinceLastTrade++; class=class="str">"cmt">// Increment only if there&class="macro">#x27;s no open position

   if(!IsRiskAcceptable())
   {
      Print("Risk not acceptable.");
      class="kw">return;
   }

   class="type">class="kw">double buyVolume = class="num">0, sellVolume = class="num">0;
   AnalyzeOrderFlow(buyVolume, sellVolume);

   class="type">class="kw">double adxValue[], maValue[], atrValue[], rsiValue[], maFastValue[], maSlowValue[], bollingerUpper[], bollingerLower[];
   if(!GetIndicatorData(adxValue, maValue, atrValue, rsiValue, maFastValue, maSlowValue, bollingerUpper, bollingerLower))
      class="kw">return;

   class="type">bool strongTrend = (adxValue[class="num">0] > ADXThreshold);
   class="type">bool aboveMA = (SymbolInfoDouble(_Symbol, SYMBOL_LAST) > maValue[class="num">0]);
   class="type">bool fastAboveSlow = (maFastValue[class="num">0] > maSlowValue[class="num">0]);

   class="type">int dynamicSL = (class="type">int)(atrValue[class="num">0] * ATRMultiplier / SymbolInfoDouble(_Symbol, SYMBOL_POINT));
   class="type">int dynamicTP = dynamicSL * class="num">3;   class=class="str">"cmt">// Risk/Reward ratio of class="num">1:class="num">3

   class="type">class="kw">double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   class=class="str">"cmt">// Conditions for a buy trade
拆完能看到:dynamicSL 用 ATR 乘倍数再除以点值转成整数点,dynamicTP 直接锁 1:3 盈亏比(dynamicSL*3)。外汇与贵金属波动剧烈、杠杆高风险大,这类动态止损若 ATR 突变可能使 SL 点数远超预期,上 MT5 用策略测试器跑 2023 年 XAUUSD 的 M15 就能核对实际 SL 分布。

MQL5 / C++
if(VolumeThreshold <= class="num">0 || OrderFlowPeriod <= class="num">0 || RiskPercent <= class="num">0 || RiskPercent > class="num">100 ||
   ADXPeriod <= class="num">0 || ADXThreshold <= class="num">0 || MAPeriod <= class="num">0 || MaxLotSize <= class="num">0 || ATRPeriod <= class="num">0 ||
   ATRMultiplier <= class="num">0 || RSIPeriod <= class="num">0 || RSIOverbought <= RSIOversold || MAFastPeriod <= class="num">0 ||
   MASlowPeriod <= class="num">0 || BollingerPeriod <= class="num">0 || BollingerDeviation <= class="num">0 || MaxConsecutiveLosses < class="num">0 ||
   MinBarsBetweenTrades < class="num">0)
{
   Print("Error: Invalid class="kw">input parameters.");
   class="kw">return INIT_FAILED;
}

Print("Input parameters validated.");

class=class="str">"cmt">// Initialize global variables
consecutiveLosses = class="num">0;
lastTradeTime = class="num">0;
barsSinceLastTrade = MinBarsBetweenTrades;
class="type">void OnTick()
{
   if(!IsNewBar())
      class="kw">return;

   Print("Current state - Consecutive losses: ", consecutiveLosses,
         ", Bars since last trade: ", barsSinceLastTrade);

   if(!IsTradeAllowed())
   {
      Print("Trading not allowed. Check EA configuration and account permissions.");
      class="kw">return;
   }

   class=class="str">"cmt">// Check if there&class="macro">#x27;s an open position and manage it
   if(PositionExists())
   {
      ManageOpenPositions();
      class="kw">return; class=class="str">"cmt">// Exit if there&class="macro">#x27;s an open position
   }

   barsSinceLastTrade++; class=class="str">"cmt">// Increment only if there&class="macro">#x27;s no open position

   if(!IsRiskAcceptable())
   {
      Print("Risk not acceptable.");
      class="kw">return;
   }

   class="type">class="kw">double buyVolume = class="num">0, sellVolume = class="num">0;
   AnalyzeOrderFlow(buyVolume, sellVolume);

   class="type">class="kw">double adxValue[], maValue[], atrValue[], rsiValue[], maFastValue[], maSlowValue[], bollingerUpper[], bollingerLower[];
   if(!GetIndicatorData(adxValue, maValue, atrValue, rsiValue, maFastValue, maSlowValue, bollingerUpper, bollingerLower))
      class="kw">return;

   class="type">bool strongTrend = (adxValue[class="num">0] > ADXThreshold);
   class="type">bool aboveMA = (SymbolInfoDouble(_Symbol, SYMBOL_LAST) > maValue[class="num">0]);
   class="type">bool fastAboveSlow = (maFastValue[class="num">0] > maSlowValue[class="num">0]);

   class="type">int dynamicSL = (class="type">int)(atrValue[class="num">0] * ATRMultiplier / SymbolInfoDouble(_Symbol, SYMBOL_POINT));
   class="type">int dynamicTP = dynamicSL * class="num">3;   class=class="str">"cmt">// Risk/Reward ratio of class="num">1:class="num">3

   class="type">class="kw">double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   class=class="str">"cmt">// Conditions for a buy trade

「开仓信号与风险闸门的代码落地」

这段逻辑把多空触发和风控拦截拆成了两块。先看交易侧:多头需强趋势、价在均线上、快线高于慢线,且买量超过卖量加阈值,同时 RSI 未破超买、现价低于布林上轨,并满足最小间隔柱数。 if(strongTrend && aboveMA && fastAboveSlow && buyVolume > sellVolume + VolumeThreshold && rsiValue[0] < RSIOverbought && currentPrice < bollingerUpper[0] && barsSinceLastTrade >= MinBarsBetweenTrades) { Print("Buy conditions met. Attempting to open position..."); if(ExecuteTrade(ORDER_TYPE_BUY, dynamicSL, dynamicTP)) { Print("Buy position opened successfully."); barsSinceLastTrade = 0; } } // Conditions for a sell trade else if(strongTrend && !aboveMA && !fastAboveSlow && sellVolume > buyVolume + VolumeThreshold && rsiValue[0] > RSIOversold && currentPrice > bollingerLower[0] && barsSinceLastTrade >= MinBarsBetweenTrades) { Print("Sell conditions met. Attempting to open position..."); if(ExecuteTrade(ORDER_TYPE_SELL, dynamicSL, dynamicTP)) { Print("Sell position opened successfully."); barsSinceLastTrade = 0; } } 逐行拆:前3行是多头复合条件,第4行打印准备开仓,第5行调 ExecuteTrade 下市价买单并带动态止损止盈,成功则重置间隔计数。空头分支对称,用 !aboveMA 和 !fastAboveSlow 翻转方向,RSI 改判超卖、价高于布林下轨。 bool IsRiskAcceptable() { if(IsHighImpactNews()) { Print("Risk not acceptable: High impact news detected."); return false; } if(consecutiveLosses >= MaxConsecutiveLosses) { Print("Risk not acceptable: Maximum consecutive losses reached (", consecutiveLosses, "/", MaxConsecutiveLosses, ")."); return false; } if(barsSinceLastTrade < MinBarsBetweenTrades) { Print("Risk not acceptable: Not enough bars since last trade (", barsSinceLastTrade, "/", MinBarsBetweenTrades, ")."); return false; } double equity = AccountInfoDouble(ACCOUNT_EQUITY); double balance = AccountInfoDouble(ACCOUNT_BALANCE); double drawdown = (balance - equity) / balance * 100; if(drawdown > 20) // Increased from 10% to 20% { Print("Risk not acceptable: Excessive drawdown (", DoubleToString(drawdown, 2), "%)."); return false; } Print("Risk acceptable. Consecutive losses: ", consecutiveLosses, ", Bars since last trade: ", barsSinceLastTrade, ", Current drawdown: ", DoubleToString(drawdown, 2), "%"); return true; } 风控函数先挡重大新闻,再拦连续亏损达上限,接着查柱数冷却,最后算回撤。注意回撤阈值从 10% 放宽到了 20%,意味着在贵金属或外汇直盘上,浮亏容忍度翻倍,但爆拉行情中仍可能快速触发拦截。 把 MaxConsecutiveLosses 和 MinBarsBetweenTrades 调小,能在震荡市提高交易频率;外汇与贵金属杠杆高,回测时建议把 drawdown>20 改回 10 观察曲线稳定性。

MQL5 / C++
if(strongTrend && aboveMA && fastAboveSlow && buyVolume > sellVolume + VolumeThreshold &&
   rsiValue[class="num">0] < RSIOverbought && currentPrice < bollingerUpper[class="num">0] &&
   barsSinceLastTrade >= MinBarsBetweenTrades)
{
   Print("Buy conditions met. Attempting to open position...");
   if(ExecuteTrade(ORDER_TYPE_BUY, dynamicSL, dynamicTP))
   {
      Print("Buy position opened successfully.");
      barsSinceLastTrade = class="num">0;
   }
}
class=class="str">"cmt">// Conditions for a sell trade
else if(strongTrend && !aboveMA && !fastAboveSlow && sellVolume > buyVolume + VolumeThreshold &&
        rsiValue[class="num">0] > RSIOversold && currentPrice > bollingerLower[class="num">0] &&
        barsSinceLastTrade >= MinBarsBetweenTrades)
{
   Print("Sell conditions met. Attempting to open position...");
   if(ExecuteTrade(ORDER_TYPE_SELL, dynamicSL, dynamicTP))
   {
      Print("Sell position opened successfully.");
      barsSinceLastTrade = class="num">0;
   }
}
class="type">bool IsRiskAcceptable()
{
   if(IsHighImpactNews())
   {
      Print("Risk not acceptable: High impact news detected.");
      class="kw">return false;
   }
   
   if(consecutiveLosses >= MaxConsecutiveLosses)
   {
      Print("Risk not acceptable: Maximum consecutive losses reached(", consecutiveLosses, "/", MaxConsecutiveLosses, ").");
      class="kw">return false;
   }
   
   if(barsSinceLastTrade < MinBarsBetweenTrades)
   {
      Print("Risk not acceptable: Not enough bars since last trade(", barsSinceLastTrade, "/", MinBarsBetweenTrades, ").");
      class="kw">return false;
   }
   
   class="type">class="kw">double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   class="type">class="kw">double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   class="type">class="kw">double drawdown = (balance - equity) / balance * class="num">100;
   if(drawdown > class="num">20) class=class="str">"cmt">// Increased from class="num">10% to class="num">20%
   {
      Print("Risk not acceptable: Excessive drawdown(", DoubleToString(drawdown, class="num">2), "%).");
      class="kw">return false;
   }
   
   Print("Risk acceptable. Consecutive losses: ", consecutiveLosses,
         ", Bars since last trade: ", barsSinceLastTrade,
         ", Current drawdown: ", DoubleToString(drawdown, class="num">2), "%");
   class="kw">return true;
}

按止损距离反推仓位的手数函数

做风控的人都知道,固定手数在变来变去的止损距离下就是裸奔。下面这个函数把「账户余额 × 风险百分比」换算成「最多能亏多少美元」,再除以「止损点数 × 每跳价值」,得出理论手数,属于 MT5 里最实用的仓位计算器骨架。 double CalculateLotSize(double stopLossDistance) { double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); double maxRiskAmount = accountBalance * (RiskPercent / 100); double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

if(tickValue == 0stopLossDistance == 0)

{ Print("Error: Tick value or Stop Loss distance is 0"); return 0; } double lotSize = NormalizeDouble(maxRiskAmount / (stopLossDistance * tickValue), 2); lotSize = MathFloor(lotSize / lotStep) * lotStep; double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); lotSize = MathMax(MathMin(lotSize, maxLot), minLot); lotSize = MathMin(lotSize, MaxLotSize); double margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double requiredMargin = SymbolInfoDouble(_Symbol, SYMBOL_MARGIN_INITIAL) * lotSize; if(requiredMargin > margin) { Print("Not enough free margin to open this position. Required: ", requiredMargin, " Available: ", margin); return 0; } Print("Calculated lot size: ", lotSize, " Risk: $", NormalizeDouble(lotSize * stopLossDistance * tickValue, 2)); return lotSize; } 逐行看:前四行抓账户余额、风险额度、当前品种每跳价值与手数步长;若 tickValue 或止损距离为 0 直接退出,避免除零崩脚本。lotSize 先按风险公式算到两位小数,再用 MathFloor 对齐 broker 的 lotStep,保证下单手数是合规最小步长的整数倍。 接着用 SYMBOL_VOLUME_MAX / MIN 夹一遍,再被 MaxLotSize 这条全局上限截一刀——双保险防超仓。最后查自由保证金:requiredMargin 大于可用就报错返回 0,否则打印「算出的手数 + 对应风险美元」供日志核对。外汇与贵金属杠杆高,保证金不足会直接拒单,这段代码把这种坑挡在了下单前。 void HandleTradingErrors(int errorCode) { switch(errorCode) { case TRADE_RETCODE_REQUOTE: Print("Error: Requote"); break; case TRADE_RETCODE_REJECT: Print("Error: Request rejected"); break; case TRADE_RETCODE_CANCEL: Print("Error: Request cancelled by trader"); break; case TRADE_RETCODE_PLACED: Print("Order placed successfully"); break; 另一段是错误处理壳子:把 REQUOTE、REJECT、CANCEL、PLACED 这几种回码用 switch 打印出来。实盘里报价跳空常触发 requote,光打印不够,真要稳得在调用端加重试逻辑。把这两段直接丢进 MT5 的 EA 里,RiskPercent 和 MaxLotSize 设成外部输入,就能在每次挂单前自动算手数。

MQL5 / C++
class="type">class="kw">double CalculateLotSize(class="type">class="kw">double stopLossDistance)
{
   class="type">class="kw">double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   class="type">class="kw">double maxRiskAmount = accountBalance * (RiskPercent / class="num">100);
   class="type">class="kw">double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   class="type">class="kw">double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   
   if(tickValue == class="num">0 || stopLossDistance == class="num">0)
   {
      Print("Error: Tick value or Stop Loss distance is class="num">0");
      class="kw">return class="num">0;
   }
   
   class="type">class="kw">double lotSize = NormalizeDouble(maxRiskAmount / (stopLossDistance * tickValue), class="num">2);
   lotSize = MathFloor(lotSize / lotStep) * lotStep;
   
   class="type">class="kw">double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   class="type">class="kw">double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   
   lotSize = MathMax(MathMin(lotSize, maxLot), minLot);
   lotSize = MathMin(lotSize, MaxLotSize);
   
   class="type">class="kw">double margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   class="type">class="kw">double requiredMargin = SymbolInfoDouble(_Symbol, SYMBOL_MARGIN_INITIAL) * lotSize;
   
   if(requiredMargin > margin)
   {
      Print("Not enough free margin to open this position. Required: ", requiredMargin, " Available: ", margin);
      class="kw">return class="num">0;
   }
   
   Print("Calculated lot size: ", lotSize, " Risk: $", NormalizeDouble(lotSize * stopLossDistance * tickValue, class="num">2));
   
   class="kw">return lotSize;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function for error handling                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void HandleTradingErrors(class="type">int errorCode)
{
   class="kw">switch(errorCode)
   {
      case TRADE_RETCODE_REQUOTE:
         Print("Error: Requote");
         class="kw">break;
      case TRADE_RETCODE_REJECT:
         Print("Error: Request rejected");
         class="kw">break;
      case TRADE_RETCODE_CANCEL:
         Print("Error: Request cancelled by trader");
         class="kw">break;
      case TRADE_RETCODE_PLACED:
         Print("Order placed successfully");
         class="kw">break;

◍ 把成交回码逐条打进日志

EA 发单后最怕静默失败。把交易返回码用 switch 逐条 Print,能在 MT5 Experts 日志里直接看到到底是成交了还是被 broker 拒了,不用猜。 下面这段 case 覆盖了从完全成交到订单被改的多数回码:DONE 和 DONE_PARTIAL 代表成功,其余基本都是各类报错。其中 PRICE_CHANGED 在黄金跳空时命中概率明显偏高,遇到它就该重新取价再发。 实际排查时建议把 Print 换成文件日志或推送到小布看板,否则 MT5 重启后 Experts 记录会被截断,那段报错就丢了。

MQL5 / C++
case TRADE_RETCODE_DONE:
      Print("Request completed");
      class="kw">break;
case TRADE_RETCODE_DONE_PARTIAL:
      Print("Request partially completed");
      class="kw">break;
case TRADE_RETCODE_ERROR:
      Print("Request processing error");
      class="kw">break;
case TRADE_RETCODE_TIMEOUT:
      Print("Error: Request cancelled by timeout");
      class="kw">break;
case TRADE_RETCODE_INVALID:
      Print("Error: Invalid request");
      class="kw">break;
case TRADE_RETCODE_INVALID_VOLUME:
      Print("Error: Invalid volume in request");
      class="kw">break;
case TRADE_RETCODE_INVALID_PRICE:
      Print("Error: Invalid price in request");
      class="kw">break;
case TRADE_RETCODE_INVALID_STOPS:
      Print("Error: Invalid stops in request");
      class="kw">break;
case TRADE_RETCODE_TRADE_DISABLED:
      Print("Error: Trading is disabled");
      class="kw">break;
case TRADE_RETCODE_MARKET_CLOSED:
      Print("Error: Market is closed");
      class="kw">break;
case TRADE_RETCODE_NO_MONEY:
      Print("Error: Not enough money to complete request");
      class="kw">break;
case TRADE_RETCODE_PRICE_CHANGED:
      Print("Error: Prices changed");
      class="kw">break;
case TRADE_RETCODE_PRICE_OFF:
      Print("Error: No quotes to process request");
      class="kw">break;
case TRADE_RETCODE_INVALID_EXPIRATION:
      Print("Error: Invalid order expiration date");
      class="kw">break;
case TRADE_RETCODE_ORDER_CHANGED:
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到订单流失衡与连亏预警,你专注决策而非盯日志。

常见问题

ADX用于过滤趋势强度弱的时段,RSI确认超买超卖区间,两者与订单流买卖量差共同触发,倾向降低震荡市的假信号概率。
可以,小布的品种页已内置连续亏损与新闻禁交易标记,外汇贵金属高风险,建议实盘前先对照EA参数校准预警线。
函数引入ATR衡量波动,ATR放大时仓位倾向缩小,以控制单笔风险,具体缩放系数由输入参数决定。
部分平仓锁定已有利润同时保留趋势暴露,跟踪止损随后移动,可能在快速反转中减少回撤。