MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA)
概述
在前一篇文章(第八部分)中,我们通过MetaQuotes Language 5(MQL5)构建了一款基于蝴蝶谐波形态的EA,利用精确的斐波那契比例实现反转交易策略。而在本文(第九部分)中,我们将焦点转向亚洲盘突破策略——该策略通过识别关键交易时段(亚洲时段)的高低价形成突破区间,结合移动平均线进行趋势过滤,并融入动态风险管理机制。
在本文中,我们将涵盖以下内容:
最终,您将获得一款功能完备的EA,可全自动执行亚洲盘突破策略,并可直接用于实盘测试与优化。让我们开始深入实践!
策略设计方案
要创建该程序,我们将设计一种方法,利用亚洲交易时段形成的关键价格区间作为策略核心。第一步是定义时段区间框:通过捕捉特定时间窗口(通常为格林尼治标准时间(GMT)23:00至03:00)内的最高价和最低价来划定范围。然而,这些时间参数可完全根据您的需求自定义调整。这一划定的区间代表了价格盘整区域,我们预期后续将从该区域产生突破行情。
接下来,我们将在该价格区间的边界处设置突破水平位。如果市场条件确认上涨趋势(使用移动平均线(如50周期MA)进行趋势验证),我们将在区间顶部上方稍远处放置一个买入止损挂单。反之,如果趋势为下跌,则将在区间底部下方稍远处放置一个卖出止损挂单。这一双向布局将确保EA在价格突破时,能够立即捕捉任意方向的显著行情。
风险管理是我们策略的核心环节。我们将在价格区间边界外侧设置止损订单,以防范假突破或趋势反转,同时根据预设的风险回报比确定止盈水平。此外,我们将实施基于时间的退出策略:如果持仓在指定退出时间(如GMT13:00)后仍未平仓,系统将自动关闭所有未结订单。总体而言,该策略通过精准的时段区间识别、趋势过滤机制和稳健的风险管理,构建了一款能够捕捉市场显著突破行情的EA。简言之,以下是我们要实现的完整策略的可视化框架:

在MQL5中的实现
要在MQL5中创建该程序,请打开MetaEditor,进入导航器,找到“指标”文件夹,点击“新建”选项卡,并按照提示创建文件。文件创建完成后,在编码环境中,我们需要声明一些将在整个程序中使用到的全局变量。
//+------------------------------------------------------------------+ //| Copyright 2025, Forex Algo-Trader, Allan. | //| "https://t.me/Forex_Algo_Trader" | //+------------------------------------------------------------------+ #property copyright "Forex Algo-Trader, Allan" #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property description "This EA trades based on ASIAN BREAKOUT Strategy" #property strict #include <Trade\Trade.mqh> //--- Include trade library CTrade obj_Trade; //--- Create global trade object //--- Global indicator handle for the moving average int maHandle = INVALID_HANDLE; //--- Global MA handle //==== Input parameters //--- Trade and indicator settings input double LotSize = 0.1; //--- Trade lot size input double BreakoutOffsetPips = 10; //--- Offset in pips for pending orders input ENUM_TIMEFRAMES BoxTimeframe = PERIOD_M15; //--- Timeframe for box calculation (15 or 30 minutes) input int MA_Period = 50; //--- Moving average period for trend filter input ENUM_MA_METHOD MA_Method = MODE_SMA; //--- MA method (Simple Moving Average) input ENUM_APPLIED_PRICE MA_AppliedPrice = PRICE_CLOSE; //--- Applied price for MA (Close price) input double RiskToReward = 1.3; //--- Reward-to-risk multiplier (1:1.3) input int MagicNumber = 12345; //--- Magic number (used for order identification) //--- Session timing settings (GMT) with minutes input int SessionStartHour = 23; //--- Session start hour input int SessionStartMinute = 00; //--- Session start minute input int SessionEndHour = 03; //--- Session end hour input int SessionEndMinute = 00; //--- Session end minute input int TradeExitHour = 13; //--- Trade exit hour input int TradeExitMinute = 00; //--- Trade exit minute //--- Global variables for storing session box data datetime lastBoxSessionEnd = 0; //--- Stores the session end time of the last computed box bool boxCalculated = false; //--- Flag: true if session box has been calculated bool ordersPlaced = false; //--- Flag: true if orders have been placed for the session double BoxHigh = 0.0; //--- Highest price during the session double BoxLow = 0.0; //--- Lowest price during the session //--- Variables to store the exact times when the session's high and low occurred datetime BoxHighTime = 0; //--- Time when the highest price occurred datetime BoxLowTime = 0; //--- Time when the lowest price occurred
在此步骤中,我们通过 "#include <Trade\Trade.mqh>"引入交易库,以调用内置交易函数,并创建一个名为"obj_Trade"的全局交易对象。我们定义了一个全局指标句柄"maHandle",将其初始化为INVALID_HANDLE,同时为用户设置交易和指标参数的输入接口——包括交易设置,如“手数(LotSize)”、“突破偏移点数(BreakoutOffsetPips)”、“区间时间框架(BoxTimeframe)”(使用ENUM_TIMEFRAMES类型),以及MA参数(“MA周期(MA_Period)”、“MA计算方法(MA_Method)”、“MA应用价格(MA_AppliedPrice)”)和风险管理参数(“风险回报比(RiskToReward)”“magic数字(MagicNumber)”)。
此外,我们允许用户以小时和分钟为单位指定交易时段(通过输入参数如“时段起始小时(SessionStartHour)”、“时段起始分钟(SessionStartMinute)”、“时段结束小时(SessionEndHour)”、“时段结束分钟(SessionEndMinute)”“交易退出小时(TradeExitHour)”和“交易退出分钟(TradeExitMinute)”),并声明全局变量以存储区间数据(“区间高点(BoxHigh)”、“区间低点(BoxLow)”)及这些极值出现的精确时间(“区间高点时间(BoxHighTime)”、“区间低点时间(BoxLowTime)”),同时设置标识位(“区间已计算(boxCalculated)”和“订单已下达(ordersPlaced)”)来控制程序逻辑。接下来,轮到OnInit事件处理器并初始化句柄。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- Set the magic number for all trade operations obj_Trade.SetExpertMagicNumber(MagicNumber); //--- Set magic number globally for trades //--- Create the Moving Average handle with user-defined parameters maHandle = iMA(_Symbol, 0, MA_Period, 0, MA_Method, MA_AppliedPrice); //--- Create MA handle if(maHandle == INVALID_HANDLE){ //--- Check if MA handle creation failed Print("Failed to create MA handle."); //--- Print error message return(INIT_FAILED); //--- Terminate initialization if error occurs } return(INIT_SUCCEEDED); //--- Return successful initialization }
在OnInit事件处理器中,我们通过调用"obj_Trade.SetExpertMagicNumber(MagicNumber)" 方法设置交易对象的Magic数字,确保所有交易订单均被唯一标识。接下来,我们使用iMA函数并传入用户定义的参数(“MA 周期(MA_Period)”、“MA 计算方法(MA_Method)”和“MA 应用价格(MA_AppliedPrice)”),创建MA句柄。随后,我们通过检查"maHandle"是否等于INVALID_HANDLE来验证句柄是否创建成功。如果相等,则打印错误信息并返回INIT_FAILED,表示初始化失败;否则返回INIT_SUCCEEDED,表示初始化成功。最后,我们需要在程序闲置时释放已创建的句柄,以节省系统资源。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- Release the MA handle if valid if(maHandle != INVALID_HANDLE) //--- Check if MA handle exists IndicatorRelease(maHandle); //--- Release the MA handle //--- Drawn objects remain on the chart for historical reference }
在OnDeinit函数 中,我们检查移动平均线句柄"maHandle"是否有效(即不等于INVALID_HANDLE)。如果句柄有效,则调用IndicatorRelease函数释放该句柄,以释放系统资源。现在,我们可以进入主事件处理器OnTick,后续的所有控制逻辑均将基于此函数展开。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Get the current server time (assumed GMT) datetime currentTime = TimeCurrent(); //--- Retrieve current time MqlDateTime dt; //--- Declare a structure for time components TimeToStruct(currentTime, dt); //--- Convert current time to structure //--- Check if the current time is at or past the session end (using hour and minute) if(dt.hour > SessionEndHour || (dt.hour == SessionEndHour && dt.min >= SessionEndMinute)){ //--- Build the session end time using today's date and user-defined session end time MqlDateTime sesEnd; //--- Declare a structure for session end time sesEnd.year = dt.year; //--- Set year sesEnd.mon = dt.mon; //--- Set month sesEnd.day = dt.day; //--- Set day sesEnd.hour = SessionEndHour; //--- Set session end hour sesEnd.min = SessionEndMinute; //--- Set session end minute sesEnd.sec = 0; //--- Set seconds to 0 datetime sessionEnd = StructToTime(sesEnd); //--- Convert structure to datetime //--- Determine the session start time datetime sessionStart; //--- Declare variable for session start time //--- If session start is later than or equal to session end, assume overnight session if(SessionStartHour > SessionEndHour || (SessionStartHour == SessionEndHour && SessionStartMinute >= SessionEndMinute)){ datetime prevDay = sessionEnd - 86400; //--- Subtract 24 hours to get previous day MqlDateTime dtPrev; //--- Declare structure for previous day time TimeToStruct(prevDay, dtPrev); //--- Convert previous day time to structure dtPrev.hour = SessionStartHour; //--- Set session start hour for previous day dtPrev.min = SessionStartMinute; //--- Set session start minute for previous day dtPrev.sec = 0; //--- Set seconds to 0 sessionStart = StructToTime(dtPrev); //--- Convert structure back to datetime } else{ //--- Otherwise, use today's date for session start MqlDateTime temp; //--- Declare temporary structure temp.year = sesEnd.year; //--- Set year from session end structure temp.mon = sesEnd.mon; //--- Set month from session end structure temp.day = sesEnd.day; //--- Set day from session end structure temp.hour = SessionStartHour; //--- Set session start hour temp.min = SessionStartMinute; //--- Set session start minute temp.sec = 0; //--- Set seconds to 0 sessionStart = StructToTime(temp); //--- Convert structure to datetime } //--- Recalculate the session box only if this session hasn't been processed before if(sessionEnd != lastBoxSessionEnd){ ComputeBox(sessionStart, sessionEnd); //--- Compute session box using start and end times lastBoxSessionEnd = sessionEnd; //--- Update last processed session end time boxCalculated = true; //--- Set flag indicating the box has been calculated ordersPlaced = false; //--- Reset flag for order placement for the new session } } }
在EA的OnTick事件处理器中,我们首先调用TimeCurrent函数获取当前服务器时间,然后使用TimeToStruct函数将其转换为MqlDateTime结构体,以便访问时间的各个组成部分(如时、分、秒等)。接着,我们将当前时间的小时和分钟与用户定义的“SessionEndHour”(会话结束小时)和“SessionEndMinute”(会话结束分钟)进行比较。若当前时间已达到或超过会话结束时间,则构建一个“sesEnd”结构体,并通过StructToTime将其转换为日期时间型。
根据会话开始时间是早于还是晚于会话结束时间,我们确定正确的“sessionStart”时间(使用当日日期或针对跨夜会话进行调整)。若当前“sessionEnd”与上一次记录的“lastBoxSessionEnd”不同,则调用"ComputeBox"函数重新计算会话区间,同时更新“lastBoxSessionEnd”并重置“boxCalculated”(区间已计算)和“ordersPlaced”(订单已下发)标识位。我们通过自定义函数计算区间属性,代码段如下:
//+------------------------------------------------------------------+ //| Function: ComputeBox | //| Purpose: Calculate the session's highest high and lowest low, and| //| record the times these extremes occurred, using the | //| specified session start and end times. | //+------------------------------------------------------------------+ void ComputeBox(datetime sessionStart, datetime sessionEnd){ int totalBars = Bars(_Symbol, BoxTimeframe); //--- Get total number of bars on the specified timeframe if(totalBars <= 0){ Print("No bars available on timeframe ", EnumToString(BoxTimeframe)); //--- Print error if no bars available return; //--- Exit if no bars are found } MqlRates rates[]; //--- Declare an array to hold bar data ArraySetAsSeries(rates, false); //--- Set array to non-series order (oldest first) int copied = CopyRates(_Symbol, BoxTimeframe, 0, totalBars, rates); //--- Copy bar data into array if(copied <= 0){ Print("Failed to copy rates for box calculation."); //--- Print error if copying fails return; //--- Exit if error occurs } double highVal = -DBL_MAX; //--- Initialize high value to the lowest possible double lowVal = DBL_MAX; //--- Initialize low value to the highest possible //--- Reset the times for the session extremes BoxHighTime = 0; //--- Reset stored high time BoxLowTime = 0; //--- Reset stored low time //--- Loop through each bar within the session period to find the extremes for(int i = 0; i < copied; i++){ if(rates[i].time >= sessionStart && rates[i].time <= sessionEnd){ if(rates[i].high > highVal){ highVal = rates[i].high; //--- Update highest price BoxHighTime = rates[i].time; //--- Record time of highest price } if(rates[i].low < lowVal){ lowVal = rates[i].low; //--- Update lowest price BoxLowTime = rates[i].time; //--- Record time of lowest price } } } if(highVal == -DBL_MAX || lowVal == DBL_MAX){ Print("No valid bars found within the session time range."); //--- Print error if no valid bars found return; //--- Exit if invalid data } BoxHigh = highVal; //--- Store final highest price BoxLow = lowVal; //--- Store final lowest price Print("Session box computed: High = ", BoxHigh, " at ", TimeToString(BoxHighTime), ", Low = ", BoxLow, " at ", TimeToString(BoxLowTime)); //--- Output computed session box data //--- Draw all session objects (rectangle, horizontal lines, and price labels) DrawSessionObjects(sessionStart, sessionEnd); //--- Call function to draw objects using computed values }
在此,我们定义一个void类型的"ComputeBox"函数,用于计算会话区间内的极值(最高价与最低价)。首先,通过Bars函数获取指定时间周期上的总K线数,随后使用CopyRates函数将K线数据复制到MqlRates数组中。我们初始化变量"highVal"为-DBL_MAX(负最大双精度浮点数),"lowVal"为DBL_MAX(正最大双精度浮点数),以确保任何有效价格均能更新这些极值。接下来,遍历会话区间内的每一根K线:如果某根K线的"high"(最高价)超过"highVal",则更新"highVal"并记录该K线的对应时间至"BoxHighTime";同理,若某柱线的"low"(最低价)低于"lowVal",则更新"lowVal"并记录时间至"BoxLowTime"。
如果数据处理完成后,"highVal"仍为"-DBL_MAX"或"lowVal"仍为DBL_MAX,则打印一条错误信息,提示未找到有效K线;否则,将计算得到的极值分别赋值给"BoxHigh"和"BoxLow",并使用TimeToString函数将记录的时间转换为可读格式后打印。最后,调用"DrawSessionObjects"函数,传入会话开始与结束时间,在图表上可视化显示会话区间及相关图形对象。该函数的实现代码如下:
//+----------------------------------------------------------------------+ //| Function: DrawSessionObjects | //| Purpose: Draw a filled rectangle spanning from the session's high | //| point to its low point (using exact times), then draw | //| horizontal lines at the high and low (from sessionStart to | //| sessionEnd) with price labels at the right. Dynamic styling | //| for font size and line width is based on the current chart | //| scale. | //+----------------------------------------------------------------------+ void DrawSessionObjects(datetime sessionStart, datetime sessionEnd){ int chartScale = (int)ChartGetInteger(0, CHART_SCALE, 0); //--- Retrieve the chart scale (0 to 5) int dynamicFontSize = 7 + chartScale * 1; //--- Base 7, increase by 2 per scale level int dynamicLineWidth = (int)MathRound(1 + (chartScale * 2.0 / 5)); //--- Linear interpolation //--- Create a unique session identifier using the session end time string sessionID = "Sess_" + IntegerToString(lastBoxSessionEnd); //--- Draw the filled rectangle (box) using the recorded high/low times and prices string rectName = "SessionRect_" + sessionID; //--- Unique name for the rectangle if(!ObjectCreate(0, rectName, OBJ_RECTANGLE, 0, BoxHighTime, BoxHigh, BoxLowTime, BoxLow)) Print("Failed to create rectangle: ", rectName); //--- Print error if creation fails ObjectSetInteger(0, rectName, OBJPROP_COLOR, clrThistle); //--- Set rectangle color to blue ObjectSetInteger(0, rectName, OBJPROP_FILL, true); //--- Enable filling of the rectangle ObjectSetInteger(0, rectName, OBJPROP_BACK, true); //--- Draw rectangle in background //--- Draw the top horizontal line spanning from sessionStart to sessionEnd at the session high string topLineName = "SessionTopLine_" + sessionID; //--- Unique name for the top line if(!ObjectCreate(0, topLineName, OBJ_TREND, 0, sessionStart, BoxHigh, sessionEnd, BoxHigh)) Print("Failed to create top line: ", topLineName); //--- Print error if creation fails ObjectSetInteger(0, topLineName, OBJPROP_COLOR, clrBlue); //--- Set line color to blue ObjectSetInteger(0, topLineName, OBJPROP_WIDTH, dynamicLineWidth); //--- Set line width dynamically ObjectSetInteger(0, topLineName, OBJPROP_RAY_RIGHT, false); //--- Do not extend line infinitely //--- Draw the bottom horizontal line spanning from sessionStart to sessionEnd at the session low string bottomLineName = "SessionBottomLine_" + sessionID; //--- Unique name for the bottom line if(!ObjectCreate(0, bottomLineName, OBJ_TREND, 0, sessionStart, BoxLow, sessionEnd, BoxLow)) Print("Failed to create bottom line: ", bottomLineName); //--- Print error if creation fails ObjectSetInteger(0, bottomLineName, OBJPROP_COLOR, clrRed); //--- Set line color to blue ObjectSetInteger(0, bottomLineName, OBJPROP_WIDTH, dynamicLineWidth); //--- Set line width dynamically ObjectSetInteger(0, bottomLineName, OBJPROP_RAY_RIGHT, false); //--- Do not extend line infinitely //--- Create the top price label at the right edge of the top horizontal line string topLabelName = "SessionTopLabel_" + sessionID; //--- Unique name for the top label if(!ObjectCreate(0, topLabelName, OBJ_TEXT, 0, sessionEnd, BoxHigh)) Print("Failed to create top label: ", topLabelName); //--- Print error if creation fails ObjectSetString(0, topLabelName, OBJPROP_TEXT," "+DoubleToString(BoxHigh, _Digits)); //--- Set label text to session high price ObjectSetInteger(0, topLabelName, OBJPROP_COLOR, clrBlack); //--- Set label color to blue ObjectSetInteger(0, topLabelName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set dynamic font size for label ObjectSetInteger(0, topLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Anchor label to the left so text appears to right //--- Create the bottom price label at the right edge of the bottom horizontal line string bottomLabelName = "SessionBottomLabel_" + sessionID; //--- Unique name for the bottom label if(!ObjectCreate(0, bottomLabelName, OBJ_TEXT, 0, sessionEnd, BoxLow)) Print("Failed to create bottom label: ", bottomLabelName); //--- Print error if creation fails ObjectSetString(0, bottomLabelName, OBJPROP_TEXT," "+DoubleToString(BoxLow, _Digits)); //--- Set label text to session low price ObjectSetInteger(0, bottomLabelName, OBJPROP_COLOR, clrBlack); //--- Set label color to blue ObjectSetInteger(0, bottomLabelName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set dynamic font size for label ObjectSetInteger(0, bottomLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Anchor label to the left so text appears to right }
在"DrawSessionObjects"函数中,我们首先通过ChartGetInteger函数配合参数CHART_SCALE(返回0至5之间的数值)获取当前图表缩放比例,随后计算动态样式参数:动态字体大小公式为"7 + chartScale * 1" (基础大小为 7,每级缩放增加 1);动态线宽使用MathRound函数进行线性插值,确保当缩放比例为5时,线宽为3。接下来,我们将"lastBoxSessionEnd"转换为字符串并添加前缀 "Sess_",生成唯一的会话标识符,确保每个会话的图形对象名称不重复。随后,调用ObjectCreate函数绘制填充矩形,其类型为OBJ_RECTANGLE,矩形边界由会话极值的时间与价格确定("BoxHighTime"、"BoxHigh"为上边界,"BoxLowTime"、"BoxLow"为下边界),设置颜色为 "clrThistle"(淡紫红色),启用OBJPROP_FILL进行填充,并通过OBJPROP_BACK将矩形置于背景层。
完成上述步骤后,我们绘制两条水平趋势线——一条位于会话高点,另一条位于会话低点,均从"sessionStart"延伸至"sessionEnd"。其中,顶部趋势线颜色设为"clrBlue",底部趋势线颜色设为"clrRed",两条线均采用动态计算的线宽,且不无限延伸("OBJPROP_RAY_RIGHT"参数设置为false)。最后,我们在图表右侧边缘("sessionEnd"时间点)创建文本标签,分别显示会话高点与低点价格。通过DoubleToString函数格式化价格数值,使用交易品种的精度(_Digits变量),文本颜色设为 clrBlack,并应用动态字体大小。文本锚点设为左侧,使标签显示在锚点右侧。编译后,我们得到以下结果:

由上图可见,我们已成功识别出该区间,并已将其成功绘制。接下来,我们可以着手在已识别区间的边界附近挂设待成交订单。为实现这一目标,我们采用以下逻辑:
//--- Build the trade exit time using user-defined hour and minute for today MqlDateTime exitTimeStruct; //--- Declare a structure for exit time TimeToStruct(currentTime, exitTimeStruct); //--- Use current time's date components exitTimeStruct.hour = TradeExitHour; //--- Set trade exit hour exitTimeStruct.min = TradeExitMinute; //--- Set trade exit minute exitTimeStruct.sec = 0; //--- Set seconds to 0 datetime tradeExitTime = StructToTime(exitTimeStruct); //--- Convert exit time structure to datetime //--- If the session box is calculated, orders are not placed yet, and current time is before trade exit time, place orders if(boxCalculated && !ordersPlaced && currentTime < tradeExitTime){ double maBuffer[]; //--- Declare array to hold MA values ArraySetAsSeries(maBuffer, true); //--- Set the array as series (newest first) if(CopyBuffer(maHandle, 0, 0, 1, maBuffer) <= 0){ //--- Copy 1 value from the MA buffer Print("Failed to copy MA buffer."); //--- Print error if buffer copy fails return; //--- Exit the function if error occurs } double maValue = maBuffer[0]; //--- Retrieve the current MA value double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get current bid price bool bullish = (currentPrice > maValue); //--- Determine bullish condition bool bearish = (currentPrice < maValue); //--- Determine bearish condition double offsetPrice = BreakoutOffsetPips * _Point; //--- Convert pips to price units //--- If bullish, place a Buy Stop order if(bullish){ double entryPrice = BoxHigh + offsetPrice; //--- Set entry price just above the session high double stopLoss = BoxLow - offsetPrice; //--- Set stop loss below the session low double risk = entryPrice - stopLoss; //--- Calculate risk per unit double takeProfit = entryPrice + risk * RiskToReward; //--- Calculate take profit using risk/reward ratio if(obj_Trade.BuyStop(LotSize, entryPrice, _Symbol, stopLoss, takeProfit, ORDER_TIME_GTC, 0, "Asian Breakout EA")){ Print("Placed Buy Stop order at ", entryPrice); //--- Print order confirmation ordersPlaced = true; //--- Set flag indicating an order has been placed } else{ Print("Buy Stop order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Print error if order fails } } //--- If bearish, place a Sell Stop order else if(bearish){ double entryPrice = BoxLow - offsetPrice; //--- Set entry price just below the session low double stopLoss = BoxHigh + offsetPrice; //--- Set stop loss above the session high double risk = stopLoss - entryPrice; //--- Calculate risk per unit double takeProfit = entryPrice - risk * RiskToReward; //--- Calculate take profit using risk/reward ratio if(obj_Trade.SellStop(LotSize, entryPrice, _Symbol, stopLoss, takeProfit, ORDER_TIME_GTC, 0, "Asian Breakout EA")){ Print("Placed Sell Stop order at ", entryPrice); //--- Print order confirmation ordersPlaced = true; //--- Set flag indicating an order has been placed } else{ Print("Sell Stop order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Print error if order fails } } }
这里,我们通过声明一个名为"exitTimeStruct"的MqlDateTime结构体来构建交易退出时间。随后,调用TimeToStruct函数将当前时间拆解为时、分、秒等组成部分,并将用户定义的"TradeExitHour" (退出小时)和"TradeExitMinute"(退出分钟)赋值给"exitTimeStruct"(秒数设为 0)。接着,通过调用StructToTime 函数将该结构体转换回日期时间类型的值,生成"tradeExitTime"(交易退出时间)。完成上述步骤后,如果满足:会话区间已计算完成、当前无任何已挂订单、当前时间早于"tradeExitTime",就可以执行挂单操作。
我们声明一个名为"maBuffer"的数组,用于存储MA数值,并调用ArraySetAsSeries函数,确保数组以最新数据为索引首项(即数据按时间倒序排列)。随后,使用CopyBuffer函数从MA指标(通过"maHandle"调用)中提取最新数值,存入"maBuffer"数组中。接着,将该MA数值与当前买入价(通过SymbolInfoDouble函数获取)进行比较,以此判断市场处于看涨还是看跌趋势。根据这一判断结果,利用"BreakoutOffsetPips"参数计算合适的入场价、止损价和止盈价,并通过以下方法挂设待成交订单:如果是看涨信号,调用"obj_Trade.BuyStop"方法挂设买入止损单;如果是看跌信号,调用"obj_Trade.SellStop" 方法挂设卖出止损单。
最后,如果订单成功挂设,则打印确认消息;反之如果失败,则打印错误消息,并相应更新"ordersPlaced"标识位。程序运行后,我们得到如下结果:

从函数中可见,一旦触发突破信号,系统将根据MA过滤器的方向挂设待成交订单,并同步设置对应的止损订单。当前唯一需要处理的是:如果当前交易时间超出预设交易时段时,需要平仓现有头寸或删除未成交的待挂订单。
//--- If current time is at or past trade exit time, close positions and cancel pending orders if(currentTime >= tradeExitTime){ CloseOpenPositions(); //--- Close all open positions for this EA CancelPendingOrders(); //--- Cancel all pending orders for this EA boxCalculated = false; //--- Reset session box calculated flag ordersPlaced = false; //--- Reset order placed flag }
这里,系统会检查当前时间是否达到或超过预设的平仓时间。如果条件成立,则调用CloseOpenPositions函数关闭与EA关联的所有持仓头寸,随后调用"CancelPendingOrders"函数取消所有待成交订单。上述函数执行完毕后,将"boxCalculated"和"ordersPlaced"标识位重置为false,为程序下一轮交易周期做好准备。以下是所用自定义函数的详细说明:
//+------------------------------------------------------------------+ //| Function: CloseOpenPositions | //| Purpose: Close all open positions with the set magic number | //+------------------------------------------------------------------+ void CloseOpenPositions(){ int totalPositions = PositionsTotal(); //--- Get total number of open positions for(int i = totalPositions - 1; i >= 0; i--){ //--- Loop through positions in reverse order ulong ticket = PositionGetTicket(i); //--- Get ticket number for each position if(PositionSelectByTicket(ticket)){ //--- Select position by ticket if(PositionGetInteger(POSITION_MAGIC) == MagicNumber){ //--- Check if position belongs to this EA if(!obj_Trade.PositionClose(ticket)) //--- Attempt to close position Print("Failed to close position ", ticket, ": ", obj_Trade.ResultRetcodeDescription()); //--- Print error if closing fails else Print("Closed position ", ticket); //--- Confirm position closed } } } } //+------------------------------------------------------------------+ //| Function: CancelPendingOrders | //| Purpose: Cancel all pending orders with the set magic number | //+------------------------------------------------------------------+ void CancelPendingOrders(){ int totalOrders = OrdersTotal(); //--- Get total number of pending orders for(int i = totalOrders - 1; i >= 0; i--){ //--- Loop through orders in reverse order ulong ticket = OrderGetTicket(i); //--- Get ticket number for each order if(OrderSelect(ticket)){ //--- Select order by ticket int type = (int)OrderGetInteger(ORDER_TYPE); //--- Retrieve order type if(OrderGetInteger(ORDER_MAGIC) == MagicNumber && //--- Check if order belongs to this EA (type == ORDER_TYPE_BUY_STOP || type == ORDER_TYPE_SELL_STOP)){ if(!obj_Trade.OrderDelete(ticket)) //--- Attempt to delete pending order Print("Failed to cancel pending order ", ticket); //--- Print error if deletion fails else Print("Canceled pending order ", ticket); //--- Confirm pending order canceled } } } }
在 "CloseOpenPositions"函数中,系统首先通过PositionsTotal函数获取当前持仓头寸的总数量,随后以逆序循环遍历每一笔持仓。对于每笔持仓,系统通过PositionGetTicket函数获取其订单编号,并调用PositionSelectByTicket函数选中该持仓。接着,系统检查持仓的POSITION_MAGIC 属性值是否与用户自定义的"MagicNumber"匹配,以确认该持仓属于当前EA。如果匹配成功,则调用"obj_Trade.PositionClose"函数尝试平仓,并根据操作结果输出确认信息或错误描述(通过"obj_Trade.ResultRetcodeDescription"获取)。
在 "CancelPendingOrders"函数中,系统首先通过OrdersTotal函数获取当前待成交订单的总数量,并以逆序循环遍历每一笔订单。对于每笔订单,系统通过OrderGetTicket函数获取其订单编号,并调用OrderSelect函数选中该订单。接着,系统检查订单的ORDER_MAGIC属性值是否与用户自定义的"MagicNumber"匹配,同时验证订单类型是否为"ORDER_TYPE_BUY_STOP"(买入止损单) 或 ORDER_TYPE_SELL_STOP(卖出止损单)。如果上述两个条件均满足,则调用"obj_Trade.OrderDelete"函数尝试取消该订单,并根据取消操作的结果输出成功信息或错误描述。程序运行后,我们将得到如下结果:

由可视化图表可见,系统会识别亚洲交易时段,将其成功标注,并根据移动平均线方向放置待成交订单;如果超出用户设定的交易时间后订单或已触发的持仓仍存在,则系统会取消这些订单或平仓,从而实现策略目标。接下来需完成的工作是程序回测,相关内容将在下一章节详细阐述。
回测与优化
在2023一整年使用默认设置,经过完整的回测后,我们得到以下结果:
回测图:

由上图可见,当前图形效果已较为理想,但通过引入追踪止损机制后,可进一步优化其表现。我们通过以下逻辑实现这一功能:
//+------------------------------------------------------------------+ //| FUNCTION TO APPLY TRAILING STOP | //+------------------------------------------------------------------+ void applyTrailingSTOP(double slPoints, CTrade &trade_object,int magicNo=0){ double buySL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-slPoints,_Digits); //--- Calculate SL for buy positions double sellSL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+slPoints,_Digits); //--- Calculate SL for sell positions for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Iterate through all open positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket > 0){ //--- If ticket is valid if (PositionGetString(POSITION_SYMBOL) == _Symbol && (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check symbol and magic number if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && buySL > PositionGetDouble(POSITION_PRICE_OPEN) && (buySL > PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for buy position if conditions are met trade_object.PositionModify(ticket,buySL,PositionGetDouble(POSITION_TP)); } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && sellSL < PositionGetDouble(POSITION_PRICE_OPEN) && (sellSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for sell position if conditions are met trade_object.PositionModify(ticket,sellSL,PositionGetDouble(POSITION_TP)); } } } } } //---- CALL THE FUNCTION IN THE TICK EVENT HANDLER if (PositionsTotal() > 0){ //--- If there are open positions applyTrailingSTOP(30*_Point,obj_Trade,0); //--- Apply a trailing stop }
应用该功能并且测试后,新结果如下:
回测图:

回测报告:

结论
总体而言,我们已成功开发出一款基于MQL5的EA,可精准自动化执行亚洲时段突破策略。该系统通过基于交易时段的区间识别、MA趋势过滤以及动态风险管理,能够高效识别关键盘整区域,并自动执行突破交易。
免责声明:本文仅用于教学目的。交易涉及重大财务风险,且市场行情具有不可预测性。尽管所述策略为突破交易提供了结构化框架,但其无法保证盈利。在实盘环境中部署此程序前,必须进行全面的回测并实施严格的风险管理。
通过应用这些技术,您可提升算法交易能力、精进技术分析技能,并进一步优化交易策略。祝您交易顺利!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/17239
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本
MQL5 交易工具包(第 5 部分):使用仓位函数扩展历史管理 EX5 库
使用MQL5经济日历进行交易(第六部分):利用新闻事件分析和倒计时器实现交易入场自动化