English Русский Deutsch 日本語
preview
MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA)

MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA)

MetaTrader 5交易 |
298 0
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

在前一篇文章(第八部分)中,我们通过MetaQuotes Language 5(MQL5)构建了一款基于蝴蝶谐波形态的EA,利用精确的斐波那契比例实现反转交易策略。而在本文(第九部分)中,我们将焦点转向亚洲盘突破策略——该策略通过识别关键交易时段(亚洲时段)的高低价形成突破区间,结合移动平均线进行趋势过滤,并融入动态风险管理机制。

在本文中,我们将涵盖以下内容:

  1. 策略设计方案
  2. 在MQL5中的实现
  3. 回测与优化
  4. 结论

    最终,您将获得一款功能完备的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一整年使用默认设置,经过完整的回测后,我们得到以下结果:

    回测图:

    图形1

    由上图可见,当前图形效果已较为理想,但通过引入追踪止损机制后,可进一步优化其表现。我们通过以下逻辑实现这一功能:

    //+------------------------------------------------------------------+
    //|        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

    附加的文件 |
    在 MetaTrader 5 中交易的可视评估和调整 在 MetaTrader 5 中交易的可视评估和调整
    策略测试器允许您所做的不光是优化交易机器人的参数。我将展示如何在事后评估您账户的交易历史,并通过在测试器中更改持仓的止损来调整您的交易。
    价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本 价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本
    支撑位与阻力位是预示潜在趋势反转和延续的关键价位。尽管识别这些价位颇具挑战性,但一旦精准定位,您便能从容应对市场波动。如需进一步辅助,请参阅本文介绍的四分位绘图工具,该工具可帮助您识别主要及次要支撑位与阻力位。
    MQL5 交易工具包(第 5 部分):使用仓位函数扩展历史管理 EX5 库 MQL5 交易工具包(第 5 部分):使用仓位函数扩展历史管理 EX5 库
    了解如何创建可导出的 EX5 函数,以高效查询和保存历史仓位数据。在本分步指南中,我们将通过开发检索最近平仓的关键属性的模块来扩展历史管理 EX5 库。这些属性包括净利润、交易持续时间、基于点的止损、止盈、利润值以及其他各种重要细节。
    使用MQL5经济日历进行交易(第六部分):利用新闻事件分析和倒计时器实现交易入场自动化 使用MQL5经济日历进行交易(第六部分):利用新闻事件分析和倒计时器实现交易入场自动化
    在本文中,我们将借助MQL5经济日历实现交易入场自动化,具体方法是应用用户自定义的筛选条件和时差偏移量来识别符合条件的新闻事件。我们通过对比预测值和前值,来确定是开立买入(BUY)单还是卖出(SELL)订单。动态倒计时器会显示距离新闻发布剩余的时间,并且在完成一笔交易后自动重置。