基于MQL5的订单剥头皮交易系统·进阶篇
(2/3)· 承接基础概念,这一篇把EA的OnTick主循环、仓位算法与连亏熔断拆开讲清
◍ EA 启动时的参数闸门与每跳检查
MT5 上的 EA 在 OnInit 阶段就应先拦掉非法输入,否则后面算仓位和止损会直接崩。下面这段判断把所有关键参数都卡了底线:VolumeThreshold、OrderFlowPeriod、RiskPercent 等不得 ≤0,RiskPercent 还不得 >100,RSIOverbought 必须严格大于 RSIOversold,MaxConsecutiveLosses 和 MinBarsBetweenTrades 不能 <0。任一不满足就 Print 报错并返回 INIT_FAILED,EA 根本不会加载到图表上。
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
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 观察曲线稳定性。
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 == 0 | stopLossDistance == 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 设成外部输入,就能在每次挂单前自动算手数。
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 记录会被截断,那段报错就丢了。
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: