English Русский Deutsch 日本語
preview
MQL5 交易策略自动化(第24篇):集成风险管理与移动止损的伦敦时段突破系统

MQL5 交易策略自动化(第24篇):集成风险管理与移动止损的伦敦时段突破系统

MetaTrader 5交易 |
44 14
Allan Munene Mutiiria
Allan Munene Mutiiria

引言

上一篇文章(第 23 篇)中,我们对包络线趋势交易的区间修复系统进行了功能升级,基于 MQL5 语言新增移动止损与多篮子交易管理功能,更好地实现利润保护与信号管理。在第 24 篇中,我们将开发一套伦敦时段突破交易系统:能够识别盘前震荡区间、自动设置挂单,并集成风险管理工具,包含盈亏比、回撤限制,以及用于实时监控的控制面板。本文将包括几个方面:

  1. 伦敦时段突破交易策略解析
  2. 在MQL5中的实现
  3. 回测
  4. 结论

学完本章后,你将拥有一套功能完整、自带高级风控的 MQL5 突破策略程序,可直接用于回测与迭代优化,接下来我们正式开始讲解!


伦敦时段突破交易策略解析

伦敦时段突破策略专门利用伦敦开盘时段市场波动率放大的特点:先识别伦敦开盘前形成的价格区间,再挂单,捕捉价格突破该区间的交易机会。该策略的价值在于:伦敦交易时段通常流动性充足、行情波动剧烈,能提供稳定的盈利机会;但同时必须做好严谨的风控,规避假突破与账户回撤风险。

本策略实现逻辑:计算伦敦盘前的最高价与最低价,设置带偏移量的Buy Stop 与 Sell Stop 挂单;结合盈亏比设置止盈、用移动止损锁定浮盈;同时限制持仓数量与单日最大回撤,以此保护本金安全。我们还将配备控制面板用于实时行情监控,并加入时段专属校验逻辑,确保仅在规定价格区间内开仓,让系统能够适配不同的市场环境。简而言之,下图即为我们所要搭建的整套交易系统架构示意。

策略的具体实现


在MQL5中的实现

要在 MQL5 中创建程序,请打开MetaEditor,在 Navigator 中找到 Indicators 文件夹,点击 ‘New’ 选项卡,并按提示创建文件。进入编程环节后,我们首先会定义一批输入参数结构体,让整个交易程序更灵活、更具动态适配能力。

//+------------------------------------------------------------------+
//|                                        London Breakout EA.mq5    |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh> //--- Include Trade library for trading operations

//--- Enumerations
enum ENUM_TRADE_TYPE {     //--- Enumeration for trade types
   TRADE_ALL,              // All Trades (Buy and Sell)
   TRADE_BUY_ONLY,         // Buy Trades Only
   TRADE_SELL_ONLY         // Sell Trades Only
};

//--- Input parameters
sinput group "General EA Settings"
input double inpTradeLotsize = 0.01; // Lotsize
input ENUM_TRADE_TYPE TradeType = TRADE_ALL; // Trade Type Selection
sinput int MagicNumber = 12345;     // Magic Number
input double RRRatio = 1.0;        // Risk to Reward Ratio
input int StopLossPoints = 500;    // Stop loss in points
input int OrderOffsetPoints = 10;   // Points offset for Orders
input bool DeleteOppositeOrder = true; // Delete opposite order when one is activated?
input bool UseTrailing = false;    // Use Trailing Stop?
input int TrailingPoints = 50;     // Trailing Points (distance)
input int MinProfitPoints = 100;   // Minimum Profit Points to start trailing

sinput group "London Session Settings"
input int LondonStartHour = 9;        // London Start Hour
input int LondonStartMinute = 0;      // London Start Minute
input int LondonEndHour = 8;          // London End Hour
input int LondonEndMinute = 0;        // London End Minute
input int MinRangePoints = 100;       // Min Pre-London Range in points
input int MaxRangePoints = 300;       // Max Pre-London Range in points

sinput group "Risk Management"
input int MaxOpenTrades = 2;       // Maximum simultaneous open trades
input double MaxDailyDrawdownPercent = 5.0; // Max daily drawdown % to stop trading

//--- Structures
struct PositionInfo {      //--- Structure for position information
   ulong ticket;           // Position ticket
   double openPrice;      // Entry price
   double londonRange;    // Pre-London range in points for this position
   datetime sessionID;    // Session identifier (day)
   bool trailingActive;   // Trailing active flag
};

我们将开始实现伦敦时段突破交易系统,首先引入 <Trade\Trade.mqh> 库文件,并定义核心枚举类型、输入参数,以及用于持仓跟踪的结构体。我们引入 <Trade\Trade.mqh> 库,是为了使用 CTrade 类 来执行下单、修改持仓等各类交易操作。我们定义 ENUM_TRADE_TYPE 枚举类型,包含三种可选模式:TRADE_ALL:允许多单与空单双向交易;TRADE_BUY_ONLY:仅允许做多;TRADE_SELL_ONLY:仅允许做空。通过该枚举可灵活限制策略的交易方向。

随后按分组在“EA 通用设置”中设置输入参数:交易手数 inpTradeLotsize:默认 0.01;交易方向 TradeType:使用上述枚举,默认 TRADE_ALL;MagicNumber:默认 12345(用于识别 EA 自动交易);盈亏比 RRRatio:默认 1.0;止损点数 StopLossPoints:默认 500;入场偏移点数 OrderOffsetPoints:默认 10;删除反向挂单 DeleteOppositeOrder:默认开启(true);启用移动止损 UseTrailing:默认关闭(false);移动止损点数 TrailingPoints:默认 50;启动移动止损最小盈利点数 MinProfitPoints:默认 100。

在“伦敦时段设置”中设置:伦敦时段开始小时 LondonStartHour:9;伦敦时段开始分钟 LondonStartMinute:0;伦敦时段结束小时 LondonEndHour:8;伦敦时段结束分钟 LondonEndMinute:0;伦敦盘前最小有效区间点数 MinRangePoints:100;伦敦盘前最大有效区间点数 MaxRangePoints:300。在“风险控制设置”中设置:最大同时持仓数 MaxOpenTrades:2;单日最大回撤百分比 MaxDailyDrawdownPercent:5.0(超出则停止交易)。最后,我们定义 PositionInfo 结构体用于跟踪持仓信息,包含字段:ticket:订单编号;openPrice:开仓价格;londonRange:伦敦盘前价格区间;sessionID:交易日唯一标识;trailingActive:布尔值,标记移动止损是否已激活。编译后,我们得到了以下输出。

输入设置

通过这种结构化方式设置好输入参数后,我们现在可以定义一些全局变量,供整个程序通用。

//--- Global variables
CTrade obj_Trade;                 //--- Trade object
double PreLondonHigh = 0.0;       //--- Pre-London session high
double PreLondonLow = 0.0;        //--- Pre-London session low
datetime PreLondonHighTime = 0;   //--- Time of Pre-London high
datetime PreLondonLowTime = 0;    //--- Time of Pre-London low
ulong buyOrderTicket = 0;         //--- Buy stop order ticket
ulong sellOrderTicket = 0;        //--- Sell stop order ticket
bool panelVisible = true;         //--- Panel visibility flag
double LondonRangePoints = 0.0;   //--- Current session's Pre-London range
PositionInfo positionList[];      //--- Array to store position info
datetime lastCheckedDay = 0;      //--- Last checked day
bool noTradeToday = false;        //--- Flag to prevent trading today
bool sessionChecksDone = false;   //--- Flag for session checks completion
datetime analysisTime = 0;        //--- Time for London analysis
double dailyDrawdown = 0.0;       //--- Current daily drawdown
bool isTrailing = false;          //--- Global flag for any trailing active
const int PreLondonStartHour = 3; //--- Fixed Pre-London Start Hour
const int PreLondonStartMinute = 0; //--- Fixed Pre-London Start Minute

在这里,我们为程序定义全局变量:obj_Trade 是用于执行交易的 CTrade 类对象;PreLondonHigh 和 PreLondonLow 是用于记录区间的双精度浮点数;PreLondonHighTime 和 PreLondonLowTime 是用于记录时间的日期时间类型;buyOrderTicket 和sellOrderTicket 是用于记录订单编号的无符号长整型数;panelVisible 设为 true,用于控制面板显示;LondonRangePoints 初始值为 0.0,用于记录当前区间;positionList 是用于存储持仓信息的 PositionInfo 数组;lastCheckedDay 初始值为 0,用于每日跟踪;noTradeToday 和sessionChecksDone 设为 false,作为交易控制标记;analysisTime 初始值为 0,用于记录时段分析时间;dailyDrawdown 初始值为 0.0,用于风险监控;isTrailing 设为 false,用于标记移动止损状态;以及常量 PreLondonStartHour 设为 3,PreLondonStartMinute 设为 0。

完成这些定义后,我们将开始创建控制面板,这是最简单的步骤,之后再进入更复杂的交易逻辑编写。让我们从必需的创建函数开始。

//+------------------------------------------------------------------+
//| Create a rectangle label for the panel background                |
//+------------------------------------------------------------------+
bool createRecLabel(string objName, int xD, int yD, int xS, int yS,
                    color clrBg, int widthBorder, color clrBorder = clrNONE,
                    ENUM_BORDER_TYPE borderType = BORDER_FLAT, ENUM_LINE_STYLE borderStyle = STYLE_SOLID) {
    ResetLastError();              //--- Reset last error
    if (!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create rectangle label
        Print(__FUNCTION__, ": failed to create rec label! Error code = ", _LastError); //--- Log creation failure
        return false;              //--- Return failure
    }
    ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-distance
    ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-distance
    ObjectSetInteger(0, objName, OBJPROP_XSIZE, xS); //--- Set x-size
    ObjectSetInteger(0, objName, OBJPROP_YSIZE, yS); //--- Set y-size
    ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
    ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrBg); //--- Set background color
    ObjectSetInteger(0, objName, OBJPROP_BORDER_TYPE, borderType); //--- Set border type
    ObjectSetInteger(0, objName, OBJPROP_STYLE, borderStyle); //--- Set border style
    ObjectSetInteger(0, objName, OBJPROP_WIDTH, widthBorder); //--- Set border width
    ObjectSetInteger(0, objName, OBJPROP_COLOR, clrBorder); //--- Set border color
    ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
    ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Set state
    ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selectable
    ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Disable selected
    ChartRedraw(0);                //--- Redraw chart
    return true;                   //--- Return success
}

//+------------------------------------------------------------------+
//| Create a text label for panel elements                           |
//+------------------------------------------------------------------+
bool createLabel(string objName, int xD, int yD,
                 string txt, color clrTxt = clrBlack, int fontSize = 10,
                 string font = "Arial") {
    ResetLastError();              //--- Reset last error
    if (!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) { //--- Create label
        Print(__FUNCTION__, ": failed to create the label! Error code = ", _LastError); //--- Log creation failure
        return false;              //--- Return failure
    }
    ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-distance
    ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-distance
    ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
    ObjectSetString(0, objName, OBJPROP_TEXT, txt); //--- Set text
    ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set color
    ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
    ObjectSetString(0, objName, OBJPROP_FONT, font); //--- Set font
    ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
    ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Set state
    ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selectable
    ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Disable selected
    ChartRedraw(0);                //--- Redraw chart
    return true;                   //--- Return success
}

在这里,我们实现工具函数,用于创建控制面板用户界面(UI)元素 —— 使用矩形标签作为背景,文本标签用于信息展示。我们从 createRecLabel 函数开始,该函数用于生成面板背景的矩形标签,包含多个参数。我们使用 ResetLastError 函数重置错误记录,通过ObjectCreate创建OBJ_RECTANGLE_LABEL 类型的对象;若创建失败,则通过 Print 打印日志,并返回 false。我们使用 ObjectSetInteger 函数设置 OBJPROP_XDISTANCE(X 轴偏移距离)等所有必需的整数类型属性,随后调用 ChartRedraw 刷新图表,并返回 true。

接下来,我们创建 createLabel 函数,用于在面板中生成文本标签,接收参数包括:objName(对象名称)、xD(X 坐标)、yD(Y 坐标)、txt(显示文字)、clrTxt(文字颜色)、fontSize(字体大小)和 font(字体)。我们使用 ResetLastError 重置错误,通过 ObjectCreate 创建 OBJ_LABEL 类型的文本对象;若创建失败,则通过 Print 打印日志,并返回 false。我们像之前矩形标签函数一样,使用 ObjectSetInteger 函数设置属性,除此之外,还额外使用 ObjectSetString 函数设置 OBJPROP_TEXT(显示文本)和 OBJPROP_FONT(字体)属性,随后刷新图表并返回 true。这些函数能让我们搭建一个动态控制面板,用于监控交易时段数据和程序运行状态。现在,我们可以使用这些函数来创建并更新控制面板。

string panelPrefix = "LondonPanel_"; //--- Prefix for panel objects

//+------------------------------------------------------------------+
//| Create the information panel                                     |
//+------------------------------------------------------------------+
void CreatePanel() {
   createRecLabel(panelPrefix + "Background", 10, 10, 270, 200, clrMidnightBlue, 1, clrSilver); //--- Create background
   createLabel(panelPrefix + "Title", 20, 15, "London Breakout Control Center", clrGold, 12); //--- Create title
   createLabel(panelPrefix + "RangePoints", 20, 40, "Range (points): ", clrWhite, 10); //--- Create range label
   createLabel(panelPrefix + "HighPrice", 20, 60, "High Price: ", clrWhite); //--- Create high price label
   createLabel(panelPrefix + "LowPrice", 20, 80, "Low Price: ", clrWhite); //--- Create low price label
   createLabel(panelPrefix + "BuyLevel", 20, 100, "Buy Level: ", clrWhite); //--- Create buy level label
   createLabel(panelPrefix + "SellLevel", 20, 120, "Sell Level: ", clrWhite); //--- Create sell level label
   createLabel(panelPrefix + "AccountBalance", 20, 140, "Balance: ", clrWhite); //--- Create balance label
   createLabel(panelPrefix + "AccountEquity", 20, 160, "Equity: ", clrWhite); //--- Create equity label
   createLabel(panelPrefix + "CurrentDrawdown", 20, 180, "Drawdown (%): ", clrWhite); //--- Create drawdown label
   createRecLabel(panelPrefix + "Hide", 250, 10, 30, 22, clrCrimson, 1, clrNONE); //--- Create hide button
   createLabel(panelPrefix + "HideText", 258, 12, CharToString(251), clrWhite, 13, "Wingdings"); //--- Create hide text
   ObjectSetInteger(0, panelPrefix + "Hide", OBJPROP_SELECTABLE, true); //--- Make hide selectable
   ObjectSetInteger(0, panelPrefix + "Hide", OBJPROP_STATE, true); //--- Set hide state
}

//+------------------------------------------------------------------+
//| Update panel with current data                                   |
//+------------------------------------------------------------------+
void UpdatePanel() {
   string rangeText = "Range (points): " + (LondonRangePoints > 0 ? DoubleToString(LondonRangePoints, 0) : "Calculating..."); //--- Format range text
   ObjectSetString(0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); //--- Update range text
   
   string highText = "High Price: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonHigh, _Digits) : "N/A"); //--- Format high text
   ObjectSetString(0, panelPrefix + "HighPrice", OBJPROP_TEXT, highText); //--- Update high text
   
   string lowText = "Low Price: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonLow, _Digits) : "N/A"); //--- Format low text
   ObjectSetString(0, panelPrefix + "LowPrice", OBJPROP_TEXT, lowText); //--- Update low text
   
   string buyText = "Buy Level: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonHigh + OrderOffsetPoints * _Point, _Digits) : "N/A"); //--- Format buy text
   ObjectSetString(0, panelPrefix + "BuyLevel", OBJPROP_TEXT, buyText); //--- Update buy text
   
   string sellText = "Sell Level: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonLow - OrderOffsetPoints * _Point, _Digits) : "N/A"); //--- Format sell text
   ObjectSetString(0, panelPrefix + "SellLevel", OBJPROP_TEXT, sellText); //--- Update sell text
   
   string balanceText = "Balance: " + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2); //--- Format balance text
   ObjectSetString(0, panelPrefix + "AccountBalance", OBJPROP_TEXT, balanceText); //--- Update balance text
   
   string equityText = "Equity: " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2); //--- Format equity text
   ObjectSetString(0, panelPrefix + "AccountEquity", OBJPROP_TEXT, equityText); //--- Update equity text
   
   string ddText = "Drawdown (%): " + DoubleToString(dailyDrawdown, 2); //--- Format drawdown text
   ObjectSetString(0, panelPrefix + "CurrentDrawdown", OBJPROP_TEXT, ddText); //--- Update drawdown text
   ObjectSetInteger(0, panelPrefix + "CurrentDrawdown", OBJPROP_COLOR, dailyDrawdown > MaxDailyDrawdownPercent / 2 ? clrYellow : clrWhite); //--- Set drawdown color
}

在这里,我们定义字符串变量 panelPrefix 为 "LondonPanel_",作为所有面板对象名称的前缀,确保控制面板的对象能够被有序识别。我们创建 CreatePanel 函数,用于构建信息面板的用户界面。我们调用 createRecLabel 函数来创建面板背景,对象名为 panelPrefix + "Background",位置在 (10, 10),尺寸为 270x200,背景色为 clrMidnightBlue(深蓝色),边框宽度为 1,边框颜色为银色。我们使用 createLabel 函数添加标题 “London Breakout Control Center”,位置在 (20, 15),颜色为金色,字体大小 12;并在对应位置添加白色、字体大小 10 的文本标签,分别显示:价格区间、最高价、最低价、买入价位、卖出价位、账户余额、账户净值和回撤幅度。

对于隐藏按钮,我们调用 createRecLabel 创建对象 panelPrefix + "Hide",位置在 (250, 10),尺寸 30x22,背景色为 clrCrimson(深红色);同时调用 createLabel 创建对象 panelPrefix + "HideText",使用 Wingdings 字体中的字符 CharToString(251),位置在 (258, 12),颜色白色,字号 13。我们通过 ObjectSetInteger 函数,将隐藏按钮的 OBJPROP_SELECTABLE(可选中)和 OBJPROP_STATE(状态)属性设置为 true,使其具备交互功能。Wingdings 字符代码的选择可根据你的界面偏好决定。以下是可供选择的编码列表。

WINGDINGS CODES

接下来,我们实现 UpdatePanel 函数,用于用最新数据刷新控制面板。我们使用 DoubleToString 格式化 rangeText 文本,显示 LondonRangePoints;如果区间值为 0,则显示 “Calculating…”(计算中…),并通过 ObjectSetString 函数更新 panelPrefix + "RangePoints" 的文本内容。我们以同样的方式格式化并更新以下文本:最高价、最低价、买入价位(在 PreLondonHigh 基础上增加 OrderOffsetPoints * _Point)、卖出价位(在 PreLondonLow 基础上减去OrderOffsetPoints * _Point)、通过 AccountInfoDouble(ACCOUNT_BALANCE) 获取账户余额、通过 AccountInfoDouble(ACCOUNT_EQUITY) 获取账户净值,以及通过 dailyDrawdown 显示回撤值。

我们使用 ObjectSetInteger 设置回撤文本的颜色:如果日内回撤超过MaxDailyDrawdownPercent / 2,则显示黄色,否则显示白色。为了让这些函数生效,我们在初始化函数中按如下方式调用它们。

//+------------------------------------------------------------------+
//| Initialize EA                                                    |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(MagicNumber); //--- Set magic number
   ArrayFree(positionList);           //--- Free position list
   CreatePanel();                     //--- Create panel
   panelVisible = true;               //--- Set panel visible
   return(INIT_SUCCEEDED);            //--- Return success
}

//+------------------------------------------------------------------+
//| Deinitialize EA                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, "LondonPanel_"); //--- Delete panel objects
   ArrayFree(positionList);           //--- Free position list
}

OnInit 初始化事件处理函数中,我们通过交易对象设置magic编号,使用 ArrayFree 函数释放持仓列表数组,调用 CreatePanel 函数创建控制面板,并在创建完成后将面板显示标记设为 true。然后,在 OnDeinit 处理函数中,我们使用 ObjectsDeleteAll 函数删除所有带指定前缀的图表对象,并释放持仓列表数组(因为不再需要使用)。编译代码后,我们会得到如下效果。

初始化面板

既然控制面板已经创建完成,接下来我们为其增加交互效果:让隐藏按钮支持点击响应,点击后即可关闭整个面板。我们将在 OnChartEvent 图表事件处理函数中实现该逻辑。

//+------------------------------------------------------------------+
//| Handle chart events (e.g., panel close)                          |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_OBJECT_CLICK && sparam == panelPrefix + "Hide") { //--- Check hide click
      panelVisible = false;           //--- Set panel hidden
      ObjectsDeleteAll(0, "LondonPanel_"); //--- Delete panel objects
      ChartRedraw(0);                 //--- Redraw chart
   }
}

在 OnChartEvent 图表事件处理函数中,我们先判断事件类型是否为对象点击,且点击的对象是隐藏 / 关闭按钮;若条件成立,则将面板显示标记设为 false。随后删除面板相关对象,并刷新图表,使修改生效。编译后,我们得到以下结果。

关闭面板

从演示效果可以看到,控制面板现已全部搭建完毕。接下来我们对其进行完善,实现所有模块的初始化功能。我们还需要编写相关函数,用来设定每日价格区间,并完成回撤指标的计算。

//+------------------------------------------------------------------+
//| Check if it's a new trading day                                  |
//+------------------------------------------------------------------+
bool IsNewDay(datetime currentBarTime) {
   MqlDateTime barTime;               //--- Bar time structure
   TimeToStruct(currentBarTime, barTime); //--- Convert time
   datetime currentDay = StringToTime(StringFormat("%04d.%02d.%02d", barTime.year, barTime.mon, barTime.day)); //--- Get current day
   if (currentDay != lastCheckedDay) { //--- Check new day
      lastCheckedDay = currentDay;    //--- Update last day
      sessionChecksDone = false;      //--- Reset checks
      noTradeToday = false;           //--- Reset no trade
      buyOrderTicket = 0;             //--- Reset buy ticket
      sellOrderTicket = 0;            //--- Reset sell ticket
      LondonRangePoints = 0.0;        //--- Reset range
      return true;                    //--- Return new day
   }
   return false;                      //--- Return not new day
}

//+------------------------------------------------------------------+
//| Update daily drawdown                                            |
//+------------------------------------------------------------------+
void UpdateDailyDrawdown() {
   static double maxEquity = 0.0;     //--- Max equity tracker
   double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Get equity
   if (equity > maxEquity) maxEquity = equity; //--- Update max equity
   dailyDrawdown = (maxEquity - equity) / maxEquity * 100; //--- Calculate drawdown
   if (dailyDrawdown >= MaxDailyDrawdownPercent) noTradeToday = true; //--- Set no trade if exceeded
}

在这里,我们实现 IsNewDay 函数,用于检测是否进入新的交易日。我们创建一个 MqlDateTime 结构体 barTime,并通过 TimeToStruct 函数将 currentBarTime 时间转换为结构体格式。我们利用 StringFormat 拼接年、月、日,再通过 StringToTime 计算出 currentDay(当前日期)。如果 currentDay 与 lastCheckedDay 不同,我们就更新 lastCheckedDay,将sessionChecksDone 和 noTradeToday 重置为 false,清空 buyOrderTicket 和 sellOrderTicket 为 0,设置 LondonRangePoints 为 0.0,并返回 true;否则返回 false。该函数确保每日自动重置时段分析与交易标记。

接下来,我们实现 UpdateDailyDrawdown 函数,用于监控日内风险。我们使用一个初始值为 0.0 的静态变量 maxEquity 来记录账户净值峰值。通过 AccountInfoDouble 函数,使用 ACCOUNT_EQUITY 获取当前账户净值 equity,若净值创新高则更新 maxEquity,并计算从峰值回落的百分比 dailyDrawdown。如果 dailyDrawdown 大于或等于 MaxDailyDrawdownPercent,我们将 noTradeToday 设为 true,停止当日交易,实现关键的回撤保护功能。为了让这些函数生效,我们在 OnTick 事件处理函数中调用它们,让控制面板实时刷新最新数据。

//+------------------------------------------------------------------+
//| Main tick handler                                                |
//+------------------------------------------------------------------+
void OnTick() {
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   IsNewDay(currentBarTime);          //--- Check new day
   
   UpdatePanel();                     //--- Update panel
   UpdateDailyDrawdown();             //--- Update drawdown
}

在这里,我们只需分别调用新交易日判断函数来设置区间参数、更新面板数据,以及刷新日内回撤水平。当我们现在运行程序时,会得到如下效果。

已初始化面板

从效果图中可以看到,控制面板现在已加载最新数据并显示当前状态。接下来,我们进入更复杂的逻辑部分:定义交易时段区间。我们先检查交易条件,如果条件满足就执行下单,之后再处理持仓管理。因此,我们需要先编写一些函数,用于定义区间、可视化显示区间,然后再执行挂单操作。以下是实现该功能的代码。

//+------------------------------------------------------------------+
//| Fixed lot size                                                   |
//+------------------------------------------------------------------+
double CalculateLotSize(double entryPrice, double stopLossPrice) {
   return NormalizeDouble(inpTradeLotsize, 2); //--- Normalize lot size
}

//+------------------------------------------------------------------+
//| Calculate session range (high-low) in points                     |
//+------------------------------------------------------------------+
double GetRange(datetime startTime, datetime endTime, double &highVal, double &lowVal, datetime &highTime, datetime &lowTime) {
   int startBar = iBarShift(_Symbol, _Period, startTime, true); //--- Get start bar
   int endBar = iBarShift(_Symbol, _Period, endTime, true); //--- Get end bar
   if (startBar == -1 || endBar == -1 || startBar < endBar) return -1; //--- Invalid bars

   int highestBar = iHighest(_Symbol, _Period, MODE_HIGH, startBar - endBar + 1, endBar); //--- Get highest bar
   int lowestBar = iLowest(_Symbol, _Period, MODE_LOW, startBar - endBar + 1, endBar); //--- Get lowest bar
   highVal = iHigh(_Symbol, _Period, highestBar); //--- Set high value
   lowVal = iLow(_Symbol, _Period, lowestBar); //--- Set low value
   highTime = iTime(_Symbol, _Period, highestBar); //--- Set high time
   lowTime = iTime(_Symbol, _Period, lowestBar); //--- Set low time
   return (highVal - lowVal) / _Point; //--- Return range in points
}

//+------------------------------------------------------------------+
//| Place pending buy/sell stop orders                               |
//+------------------------------------------------------------------+
void PlacePendingOrders(double preLondonHigh, double preLondonLow, datetime sessionID) {
   double buyPrice = preLondonHigh + OrderOffsetPoints * _Point; //--- Calculate buy price
   double sellPrice = preLondonLow - OrderOffsetPoints * _Point; //--- Calculate sell price
   double slPoints = StopLossPoints; //--- Set SL points
   double buySL = buyPrice - slPoints * _Point; //--- Calculate buy SL
   double sellSL = sellPrice + slPoints * _Point; //--- Calculate sell SL
   double tpPoints = slPoints * RRRatio; //--- Calculate TP points
   double buyTP = buyPrice + tpPoints * _Point; //--- Calculate buy TP
   double sellTP = sellPrice - tpPoints * _Point; //--- Calculate sell TP
   double lotSizeBuy = CalculateLotSize(buyPrice, buySL); //--- Calculate buy lot
   double lotSizeSell = CalculateLotSize(sellPrice, sellSL); //--- Calculate sell lot

   if (TradeType == TRADE_ALL || TradeType == TRADE_BUY_ONLY) { //--- Check buy trade
      obj_Trade.BuyStop(lotSizeBuy, buyPrice, _Symbol, buySL, buyTP, 0, 0, "Buy Stop - London"); //--- Place buy stop
      buyOrderTicket = obj_Trade.ResultOrder(); //--- Get buy ticket
   }

   if (TradeType == TRADE_ALL || TradeType == TRADE_SELL_ONLY) { //--- Check sell trade
      obj_Trade.SellStop(lotSizeSell, sellPrice, _Symbol, sellSL, sellTP, 0, 0, "Sell Stop - London"); //--- Place sell stop
      sellOrderTicket = obj_Trade.ResultOrder(); //--- Get sell ticket
   }
}

//+------------------------------------------------------------------+
//| Draw session ranges on the chart                                 |
//+------------------------------------------------------------------+
void DrawSessionRanges(datetime preLondonStart, datetime londonEnd) {
   string sessionID = "Sess_" + IntegerToString(lastCheckedDay); //--- Session ID

   string preRectName = "PreRect_" + sessionID; //--- Rectangle name
   ObjectCreate(0, preRectName, OBJ_RECTANGLE, 0, PreLondonHighTime, PreLondonHigh, PreLondonLowTime, PreLondonLow); //--- Create rectangle
   ObjectSetInteger(0, preRectName, OBJPROP_COLOR, clrTeal); //--- Set color
   ObjectSetInteger(0, preRectName, OBJPROP_FILL, true); //--- Enable fill
   ObjectSetInteger(0, preRectName, OBJPROP_BACK, true); //--- Set background

   string preTopLineName = "PreTopLine_" + sessionID; //--- Top line name
   ObjectCreate(0, preTopLineName, OBJ_TREND, 0, preLondonStart, PreLondonHigh, londonEnd, PreLondonHigh); //--- Create top line
   ObjectSetInteger(0, preTopLineName, OBJPROP_COLOR, clrBlack); //--- Set color
   ObjectSetInteger(0, preTopLineName, OBJPROP_WIDTH, 1); //--- Set width
   ObjectSetInteger(0, preTopLineName, OBJPROP_RAY_RIGHT, false); //--- Disable ray
   ObjectSetInteger(0, preTopLineName, OBJPROP_BACK, true); //--- Set background

   string preBotLineName = "PreBottomLine_" + sessionID; //--- Bottom line name
   ObjectCreate(0, preBotLineName, OBJ_TREND, 0, preLondonStart, PreLondonLow, londonEnd, PreLondonLow); //--- Create bottom line
   ObjectSetInteger(0, preBotLineName, OBJPROP_COLOR, clrRed); //--- Set color
   ObjectSetInteger(0, preBotLineName, OBJPROP_WIDTH, 1); //--- Set width
   ObjectSetInteger(0, preBotLineName, OBJPROP_RAY_RIGHT, false); //--- Disable ray
   ObjectSetInteger(0, preBotLineName, OBJPROP_BACK, true); //--- Set background
}

为了实现区间计算、订单执行和图表绘制,我们先从 CalculateLotSize 函数开始,用于计算固定手数;该函数接收entryPrice(入场价格)和stopLossPrice(止损价格)作为参数(在固定手数模式下未使用)。我们使用 NormalizeDouble 函数将 inpTradeLotsize 标准化保留 2 位小数后返回,确保所有交易使用统一的手数。你可以根据自己的账户类型选择其他小数位数。

接下来,我们创建 GetRange 函数,用于计算伦敦盘前时段的价格区间。我们通过 iBarShift 函数,根据 startTime(开始时间)和 endTime(结束时间)获取 startBar(起始 K 线)和 endBar(结束 K 线);如果数据无效或 startBar 小于 endBar,则返回 -1。我们在该 K 线区间内,使用 iHighest 函数(参数为 MODE_HIGH)找出最高价 K 线,使用 iLowest 函数(参数为 MODE_LOW)找出最低价 K 线。我们通过 iHigh 获取最高价 K 线的价格 highVal,通过 iLow 获取最低价 K 线的价格 lowVal,通过 iTime 获取最高价与最低价对应的时间 highTime 和 lowTime。最后返回区间大小,计算公式为:(highVal - lowVal) / _Point

随后,我们定义 PlacePendingOrders 函数,用于设置买入止损单和卖出止损单。我们计算:买入价格 buyPrice = 伦敦盘前最高价 + 偏移点数 × 最小点值;卖出价格 sellPrice = 伦敦盘前最低价 − 偏移点数 × 最小点值。我们设置:止损点数 slPoints = StopLossPoints;多单止损 buySL = buyPrice − slPoints × _Point;空单止损 sellSL = sellPrice + slPoints × _Point;止盈点数 tpPoints = 止损点数 × 盈亏比;多单止盈 buyTP = buyPrice + tpPoints × _Point;空单止盈 sellTP = sellPrice − tpPoints × _Point。我们通过 CalculateLotSize 函数计算多单与空单的交易手数 lotSizeBuy 和 lotSizeSell。

如果交易方向 TradeType 为双向交易 TRADE_ALL 或仅做多 TRADE_BUY_ONLY,我们就通过 obj_Trade.BuyStop 下达买入止损单,使用计算好的手数、买入价、止损价、止盈价,备注为 “Buy Stop - London”,并将订单编号存入 buyOrderTicket。同理,如果交易方向满足条件,我们也会下达卖出止损单。

最后,我们实现 DrawSessionRanges 函数,用于在图表上可视化绘制交易时段区间。我们使用 IntegerToString 函数,将 lastCheckedDay 转换为字符串,并与 "Sess_" 拼接,生成 sessionID。对于矩形区域 preRectName(名称为 "PreRect_" + sessionID),我们使用 ObjectCreate 创建 OBJ_RECTANGLE 对象,坐标从 伦敦盘前最高价时间、最高价 延伸至 伦敦盘前最低价时间、最低价;并设置:OBJPROP_COLOR 为青蓝色 clrTeal;OBJPROP_FILL为 true;OBJPROP_BACK为 true。

对于顶部线条 preTopLineName(名称为 "PreTopLine_" + sessionID),我们创建 OBJ_TREND 对象,从伦敦盘前开始时间、最高价 延伸至 伦敦时段结束时间、最高价;并设置:颜色 OBJPROP_COLOR 为 clrBlack ,线宽 OBJPROP_WIDTH 为1、OBJPROP_RAY_RIGHT 为 false,OBJPROP_BACK 为 true。同理,我们创建底部线条 preBotLineName(名称为 "PreBottomLine_" + sessionID),从伦敦盘前开始时间、最低价 延伸至伦敦时段结束时间、最低价,颜色为红色。现在,我们可以定义一个函数,利用上述所有函数来检查交易条件。

//+------------------------------------------------------------------+
//| Check trading conditions and place orders                        |
//+------------------------------------------------------------------+
void CheckTradingConditions(datetime currentTime) {
   MqlDateTime timeStruct;            //--- Time structure
   TimeToStruct(currentTime, timeStruct); //--- Convert time
   datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); //--- Get today

   datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; //--- Pre-London start
   datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; //--- London start
   datetime londonEnd = today + LondonEndHour * 3600 + LondonEndMinute * 60; //--- London end
   analysisTime = londonStart;        //--- Set analysis time

   if (currentTime < analysisTime) return; //--- Exit if before analysis

   double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); //--- Get range
   if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { //--- Check range limits
      noTradeToday = true;            //--- Set no trade
      sessionChecksDone = true;       //--- Set checks done
      DrawSessionRanges(preLondonStart, londonEnd); //--- Draw ranges
      return;                         //--- Exit
   }

   LondonRangePoints = preLondonRange; //--- Set range points
   PlacePendingOrders(PreLondonHigh, PreLondonLow, today); //--- Place orders
   noTradeToday = true;               //--- Set no trade
   sessionChecksDone = true;          //--- Set checks done
   DrawSessionRanges(preLondonStart, londonEnd); //--- Draw ranges
}

我们实现 CheckTradingConditions 函数,用于在伦敦时段突破系统中判断交易条件并执行挂单。我们创建一个 MqlDateTime 结构体 timeStruct,并通过 TimeToStruct 函数将当前时间转换为结构体格式。我们使用 StringFormat 拼接年月日,再通过 StringToTime 计算出 today(今日日期)。我们设置:preLondonStart(伦敦盘前开始时间)= 今日日期 + 盘前小时与分钟(换算为秒);londonStart(伦敦开盘时间)= 今日日期 + 伦敦开盘小时与分钟;londonEnd(伦敦收盘时间)= 今日日期 + 伦敦收盘小时与分钟。我们将 analysisTime 赋值为伦敦开盘时间;如果当前时间早于该时间,直接退出函数。

我们调用 GetRange 函数获取 preLondonRange(伦敦盘前区间),传入盘前开始时间、当前时间,以及 PreLondonHigh、PreLondonLow、PreLondonHighTime、PreLondonLowTime 的引用参数。如果 preLondonRange 小于最小区间 MinRangePoints 或大于最大区间 MaxRangePoints,我们将 noTradeToday 和 sessionChecksDone 设为 true,调用 DrawSessionRanges 绘制区间,然后退出函数。否则,我们将 LondonRangePoints 赋值为有效区间,调用 PlacePendingOrders 执行挂单,设置 noTradeToday 和 sessionChecksDone 为 true,并调用 DrawSessionRanges 绘制区间,确保只在有效时段区间内交易。现在我们可以在 OnTick 事件处理函数中调用该函数,以触发交易信号。

if (!noTradeToday && !sessionChecksDone) { //--- Check trading conditions
   CheckTradingConditions(TimeCurrent()); //--- Check conditions
}

如果今日尚未交易、且未完成时段条件校验,我们就调用函数检查当前时间的交易条件。运行程序后,我们得到如下效果。

区间已设置

可以看到,我们已成功设置价格区间并挂单。当前区间为 100 点,符合我们的交易条件。现在我们可以进入订单管理环节;首先,我们需要在其中一个挂单触发时,检查并删除另一张未触发的挂单,并将持仓订单加入持仓列表以便管理。

//+------------------------------------------------------------------+
//| Delete opposite pending order when one is filled                 |
//+------------------------------------------------------------------+
void CheckAndDeleteOppositeOrder() {
   if (!DeleteOppositeOrder || TradeType != TRADE_ALL) return; //--- Exit if not applicable

   bool buyOrderExists = false;       //--- Buy exists flag
   bool sellOrderExists = false;      //--- Sell exists flag

   for (int i = OrdersTotal() - 1; i >= 0; i--) { //--- Iterate through orders
      ulong orderTicket = OrderGetTicket(i); //--- Get ticket
      if (OrderSelect(orderTicket)) { //--- Select order
         if (OrderGetString(ORDER_SYMBOL) == _Symbol && OrderGetInteger(ORDER_MAGIC) == MagicNumber) { //--- Check symbol and magic
            if (orderTicket == buyOrderTicket) buyOrderExists = true; //--- Set buy exists
            if (orderTicket == sellOrderTicket) sellOrderExists = true; //--- Set sell exists
         }
      }
   }

   if (!buyOrderExists && sellOrderExists && sellOrderTicket != 0) { //--- Check delete sell
      obj_Trade.OrderDelete(sellOrderTicket); //--- Delete sell order
   } else if (!sellOrderExists && buyOrderExists && buyOrderTicket != 0) { //--- Check delete buy
      obj_Trade.OrderDelete(buyOrderTicket); //--- Delete buy order
   }
}

//+------------------------------------------------------------------+
//| Add position to tracking list when opened                        |
//+------------------------------------------------------------------+
void AddPositionToList(ulong ticket, double openPrice, double londonRange, datetime sessionID) {
   if (londonRange <= 0) return;      //--- Exit if invalid range
   int index = ArraySize(positionList); //--- Get current size
   ArrayResize(positionList, index + 1); //--- Resize array
   positionList[index].ticket = ticket; //--- Set ticket
   positionList[index].openPrice = openPrice; //--- Set open price
   positionList[index].londonRange = londonRange; //--- Set range
   positionList[index].sessionID = sessionID; //--- Set session ID
   positionList[index].trailingActive = false; //--- Set trailing inactive
}

我们首先实现 CheckAndDeleteOppositeOrder 函数,用于在其中一个挂单被触发成交时,删除另一张反向挂单。如果 DeleteOppositeOrder 设置为 false,或者交易模式 TradeType 不是双向交易 TRADE_ALL,我们直接提前退出函数。我们将 buyOrderExists 和 sellOrderExists 初始化为 false。我们通过 OrdersTotal 获取总订单数,并结合 OrderGetTicket 反向遍历所有订单,使用 OrderSelect 选中每一张订单。如果通过 OrderGetString 和 OrderGetInteger 判断,订单品种为当前品种 _Symbol 且指纹号 MagicNumber 一致,我们再根据订单编号是否匹配 buyOrderTicket 或 sellOrderTicket,来标记对应的挂单是否存在。

如果买单已不存在而卖单仍存在,我们通过 obj_Trade.OrderDelete 删除 sellOrderTicket,同理,如果卖单已消失而买单仍存在,则删除买单。该函数能确保挂单触发后,只保留一个方向的交易。

接下来,我们创建 AddPositionToList 函数,用于跟踪已开仓的持仓订单。如果 londonRange(伦敦区间)小于或等于 0,说明区间尚未设置,我们直接退出函数。我们通过ArraySize 获取 positionList 数组当前的长度,将其作为下标 index,再使用 ArrayResize 调整数组大小,增加一个存储位置;然后为 positionList[index] 设置以下内容:订单编号 ticket、开仓价格 openPrice、伦敦区间 londonRange、时段标识 sessionID、移动止损状态 trailingActive 设为 false。这样可以维护一个持仓列表,用于管理移动止损和时段专属数据。现在我们可以在TICK 事件处理函数中实现这段逻辑。

CheckAndDeleteOppositeOrder();     //--- Delete opposite order

// Add untracked positions
for (int i = 0; i < PositionsTotal(); i++) { //--- Iterate through positions
   ulong ticket = PositionGetTicket(i); //--- Get ticket
   if (PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Check position
      bool tracked = false;        //--- Tracked flag
      for (int j = 0; j < ArraySize(positionList); j++) { //--- Check list
         if (positionList[j].ticket == ticket) tracked = true; //--- Set tracked
      }
      if (!tracked) {              //--- If not tracked
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open price
         AddPositionToList(ticket, openPrice, LondonRangePoints, lastCheckedDay); //--- Add to list
      }
   }
}

在这里,我们调用 CheckAndDeleteOppositeOrder 函数来管理挂单,确保当一个方向的订单成交后,反向的挂单会被自动删除(遵循 DeleteOppositeOrder 参数设置),从而避免出现方向冲突的交易。

接下来,我们将未跟踪的持仓添加到 positionList 数组中,确保所有相关的持仓订单都被监控,以便执行移动止损。我们通过 PositionsTotal 获取总持仓数,并使用 PositionGetTicket 遍历所有持仓,获取每一个订单的编号 ticket。如果 PositionSelectByTicket 选中持仓成功,且通过 PositionGetString 和 PositionGetInteger 判断,该持仓属于当前交易品种 _Symbol 且 MagicNumber 一致,我们就将 tracked(已跟踪)标记设为 false,并通过 ArraySize 获取数组长度,再用内层循环检查该订单编号是否已存在于 positionList[j].ticket 中。如果该持仓未被跟踪,我们就通过 PositionGetDouble(POSITION_PRICE_OPEN) 获取开仓价格 openPrice,并调用 AddPositionToList 函数,传入订单编号、开仓价格、伦敦区间点数和上一次检查日期。这样可以确保所有符合条件的持仓都会被添加到管理列表中,且不会重复添加。结果如下。

挂单被删除

到这里为止,所有功能都已经完善,现在我们可以定义持仓管理函数了,具体实现如下。

//+------------------------------------------------------------------+
//| Remove position from tracking list when closed                   |
//+------------------------------------------------------------------+
void RemovePositionFromList(ulong ticket) {
   for (int i = 0; i < ArraySize(positionList); i++) { //--- Iterate through list
      if (positionList[i].ticket == ticket) { //--- Match ticket
         for (int j = i; j < ArraySize(positionList) - 1; j++) { //--- Shift elements
            positionList[j] = positionList[j + 1]; //--- Copy next
         }
         ArrayResize(positionList, ArraySize(positionList) - 1); //--- Resize array
         break;                   //--- Exit loop
      }
   }
}

//+------------------------------------------------------------------+
//| Manage trailing stops                                            |
//+------------------------------------------------------------------+
void ManagePositions() {
   if (PositionsTotal() == 0 || !UseTrailing) return; //--- Exit if no positions or no trailing
   isTrailing = false;                //--- Reset trailing flag

   double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get bid
   double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get ask
   double point = _Point;             //--- Get point value

   for (int i = 0; i < ArraySize(positionList); i++) { //--- Iterate through positions
      ulong ticket = positionList[i].ticket; //--- Get ticket
      if (!PositionSelectByTicket(ticket)) { //--- Select position
         RemovePositionFromList(ticket); //--- Remove if not selected
         continue;                    //--- Skip
      }

      if (PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip if not matching

      double openPrice = positionList[i].openPrice; //--- Get open price
      long positionType = PositionGetInteger(POSITION_TYPE); //--- Get type
      double currentPrice = (positionType == POSITION_TYPE_BUY) ? currentBid : currentAsk; //--- Get current price
      double profitPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / point : (openPrice - currentPrice) / point; //--- Calculate profit points

      if (profitPoints >= MinProfitPoints + TrailingPoints) { //--- Check for trailing
         double newSL = 0.0;          //--- New SL variable
         if (positionType == POSITION_TYPE_BUY) { //--- Buy position
            newSL = currentPrice - TrailingPoints * point; //--- Calculate new SL
         } else {                     //--- Sell position
            newSL = currentPrice + TrailingPoints * point; //--- Calculate new SL
         }
         double currentSL = PositionGetDouble(POSITION_SL); //--- Get current SL
         if ((positionType == POSITION_TYPE_BUY && newSL > currentSL + point) || (positionType == POSITION_TYPE_SELL && newSL < currentSL - point)) { //--- Check move condition
            if (obj_Trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), PositionGetDouble(POSITION_TP))) { //--- Modify position
               positionList[i].trailingActive = true; //--- Set trailing active
               isTrailing = true;        //--- Set global trailing
            }
         }
      }
   }
}

在这里,我们实现了从跟踪列表中移除已平仓持仓并管理移动止损的函数。我们首先编写 RemovePositionFromList 函数,用于在持仓平仓时清理 positionList 数组,该函数接收 ticket(订单编号)作为参数。我们通过 ArraySize 遍历 positionList 数组,如果找到 positionList[i].ticket 与传入的订单编号匹配,就通过内层循环将后续元素向前移位(把 positionList[j + 1] 复制到 positionList[j]),然后使用 ArrayResize 将数组大小减 1,最后跳出循环。该函数确保持仓列表保持最新状态,避免对已平仓订单进行无效检查,尤其在执行移动止损和平仓操作时非常重要。

接下来,我们创建 ManagePositions 函数,用于处理持仓订单的移动止损。如果 PositionsTotal(持仓总数) 为 0 或 UseTrailing(启用移动止损) 为 false,函数直接提前退出。我们将 isTrailing 重置为 false,通过 SymbolInfoDouble 获取当前买价 currentBid 和卖价 currentAsk,并获取最小点值 point。我们通过 ArraySize 遍历 positionList,获取订单编号 ticket 并通过 PositionSelectByTicket 函数选中该持仓。如果选中失败,就调用 RemovePositionFromList 移除该订单并继续遍历。如果通过 PositionGetString 和 PositionGetInteger 判断,持仓不属于当前品种 _Symbol 或 MagicNumber 不匹配,则跳过处理。我们从 positionList[i] 中获取开仓价格 openPrice,通过 PositionGetInteger 获取持仓类型 positionType,根据类型确定当前价格 currentPrice,并计算盈利点数 profitPoints。

如果 profitPoints 大于或等于 MinProfitPoints + TrailingPoints,我们就计算新止损价 newSL:多单为 currentPrice - TrailingPoints * point,空单为 currentPrice + TrailingPoints * point。我们通过 PositionGetDouble 获取当前止损 currentSL,如果 newSL 比 currentSL 至少优化一个点值,就通过 obj_Trade.PositionModify 修改持仓,使用标准化后的 newSL 和通过 PositionGetDouble 获取的当前止盈价。修改成功后,我们将 positionList[i].trailingActive 和 isTrailing 设为 true,实现动态调整止损,以锁定利润,并让盈利继续奔跑。现在,只需在每个报价刷新时调用该函数进行持仓管理即可。编译完成后,我们得到了以下结果。

持仓管理

从界面中可以看到,程序已实现交易条件检查、自动挂单、策略化持仓管理的完整功能,成功达成了我们的开发目标。剩下的事情就是对该程序进行回测,这将在下一节中处理。


回测

经过彻底的回测后,我们得到以下结果。

回测结果图形:

图形

回测报告:

报告


结论

总之,我们使用 MQL5 开发了一套伦敦时段突破交易系统。该系统通过分析盘前价格区间来执行挂单,支持自定义盈亏比、移动止损和多笔交易数量限制,并配套控制面板,可实时监控价格区间、交易价位和账户回撤。通过 PositionInfo 结构体等模块化组件,本程序为突破交易提供了一套规范化的执行方案;你可以通过调整交易时段或风险参数,对策略进行进一步优化。

免责声明:本文仅用于教学目的。交易存在重大财务风险,市场波动可能导致亏损。在将本程序应用于实盘交易前,充分的回测与严谨的风险管理至关重要。

通过运用本文讲解的理念与实现方法,你可以将这套突破系统适配为自己的交易风格,为你的自动化交易策略提供强大支持。祝您交易愉快! 

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18867

附加的文件 |
最近评论 | 前往讨论 (14)
Allan Munene Mutiiria
Allan Munene Mutiiria | 10 8月 2025 在 22:20
Kyle Young Sangster 策略测试器 按原样运行。它每天都能找到范围,并在图表上画出方框。但是,它并没有每天进行交易(假设它应该这样做)。在 1 个半月的时间里,它只进行了 3 次交易。



第二个问题是控制面板中的最高/最低水平和买入/卖出水平没有更新。


在图表上可以清楚地找到高/低范围水位,因此我猜测买入/卖出水位也应该显示在图表上,并在控制面板中更新,因为它们是直接从高/低范围水位导出的。

,您有什么建议可以让它正常工作?

,在此先表示感谢。

至于您的第二个问题,文章中已经解释过了,但假设您的问题是由于测试数据不佳造成的,并给出提示,当范围处于计算状态时,您将始终看到 "计算中... "状态,直到有足够的数据来设置伦敦范围会话或您在输入中定义的会话。假设您使用的是默认设置,伦敦时间前为 3 小时,而您从共享截图中看到的时间是 2 月 13 日,22:00 之后的 2 个条形图是 2*15 分钟 = 30,因此给出的 22:30 在范围计算时间之外,所以面板上的数据应该仍然可见,因为之前设置的范围仍在起作用,除非尚未找到第一个时段,并且从午夜开始达到范围计算时将被清除。见下图:

const int PreLondonStartHour = 3; //--- 伦敦会议前的固定开始时间
const int PreLondonStartMinute = 0; //--- 固定的伦敦前起始分钟数

您可能需要查看以下查找范围的逻辑

//+------------------------------------------------------------------+
//| 检查交易条件并下单
//+------------------------------------------------------------------+
void CheckTradingConditions(datetime currentTime) {
   MqlDateTime timeStruct;            //--- 时间结构
   TimeToStruct(currentTime, timeStruct); //--- 转换时间
   datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); //--- 今天获取

   datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; //--伦敦奥运会开幕前
   datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; //--- 伦敦开始
   datetime londonEnd = today + LondonEndHour * 3600 + LondonEndMinute * 60; //--- 伦敦结束
   analysisTime = londonStart;        //--- 设置分析时间

   if (currentTime < analysisTime) return; //--- 如果在分析之前退出

   double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); //--- 获取范围
   if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { //--- 检查范围限制
      noTradeToday = true;            //--- 设置无交易
      sessionChecksDone = true;       //--- 完成设置检查
      DrawSessionRanges(preLondonStart, londonEnd); //--- 绘制范围
      return;                         //--- 退出
   }

   LondonRangePoints = preLondonRange; //--- 设置范围点
   PlacePendingOrders(PreLondonHigh, PreLondonLow, today); //--- 下订单
   noTradeToday = true;               //--- 设置无交易
   sessionChecksDone = true;          //--- 完成设置检查
   DrawSessionRanges(preLondonStart, londonEnd); //--- 绘制范围
}

以及如何设置。

//+------------------------------------------------------------------+
// 使用当前数据更新面板|
//+------------------------------------------------------------------+
void UpdatePanel() {
   string rangeText = "Range (points): " + (LondonRangePoints > 0 ? DoubleToString(LondonRangePoints, 0) : "Calculating..."); //--- 格式化范围文本
   ObjectSetString(0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); //-- 更新范围文本

   //---

}

请看下图,虽然我们不知道您测试的年份,但我们会取 2025 年,如果像您的情况一样是 2020 年,我们没有这方面的高质量数据,所以无论如何,我们都使用 2025 年,因此范围计算应从午夜开始。

23:55

从图中可以看到,23:55 时的数据仍然完好无损。但是,当午夜来临时,我们需要重新设置。请看下图。

午夜数据 00:00

可以看到,我们在午夜重置了其他范围计算的数据。实际上,当范围计算完成后,可视化可以帮助你了解真正发生了什么。例如,在您使用默认设置的情况下,我们将看到从 0300 时到 0800 时的运行条形图,因为这是我们定义的。见下图:

范围小时数

希望这能再次说明问题。您可以根据自己的交易风格调整一切。为避免您遇到的问题,建议您使用可靠的测试数据。谢谢。

Kyle Young Sangster
Kyle Young Sangster | 10 8月 2025 在 22:34
您在代码中的什么地方打算使用变量 "MaxOpenTrades"?它被定义了,但从未被引用过。
Kyle Young Sangster
Kyle Young Sangster | 10 8月 2025 在 22:52
Allan Munene Mutiiria #:

至于您的第二个问题,文章中已有解释,但假设您的问题源于测试数据不佳并给出提示,那么当范围处于计算状态时,您将始终看到 "计算中... "状态,直到有足够的数据来设置伦敦范围会话或您在输入中定义的会话。假设您使用的是默认设置,伦敦前时间为 3 小时,而您从共享截图中看到的时间是 2 月 13 日,22:00 之后的 2 个条形图是 2*15 分钟 = 30,因此给出的 22:30 在范围计算时间之外,所以面板上的数据应该仍然可见,因为之前设置的范围仍在起作用,除非尚未找到第一个时段,并且会在午夜达到范围计算时被清除。请参见下文:

您可能需要查看以下查找范围的逻辑

以及如何设置。

请看下图,虽然我们不知道您测试的年份,但我们会选择 2025 年,如果像您的情况一样是 2020 年,我们没有这方面的高质量数据,所以无论如何,我们都会使用 2025 年,因此范围计算应从午夜开始。


从图中可以看到,23:55 时的数据仍然完好无损。但是,当午夜来临时,我们需要重新设置。请看下图。

你可以看到,为了进行其他范围计算,我们在午夜重置了数据。实际上,当范围计算完成后,可视化可以帮助你了解真正发生了什么。例如,在您使用默认设置的情况下,我们将看到从 0300 时到 0800 时的运行条形图,因为这是我们定义的。见下图:

希望这能再次说明问题。您可以根据自己的交易风格调整一切。为避免您遇到的问题,建议您使用可靠的测试数据。谢谢。

非常感谢你的全面答复。

是的,我确实读了这篇文章,并跟着自己的拷贝编码,直到我遇到了我所概述的问题。我看到的是面板没有更新,即使在默认时间内也是如此。我截图的目的是想说明,虽然图表上已经画出了方框,数据也已收集,但面板却没有更新。此外,日志中没有关于无效价格或水平的错误信息。

,我在我的版本中添加了日志信息;从中我可以看到,当范围过大或过小时,面板不会更新;因此这可能是部分原因。

,我会仔细检查测试数据的质量。谢谢你指出了你测试的货币对;我一定会对我选择的货币对进行调整。

非常感谢你的帮助。

Allan Munene Mutiiria
Allan Munene Mutiiria | 10 8月 2025 在 23:49
Kyle Young Sangster #:

非常感谢你的全面答复。

是的,我确实读了这篇文章,并跟着自己的拷贝编码,直到我遇到了我概述的问题。我看到的是面板没有更新,即使在默认时间内也是如此。我截图的目的是想说明,虽然图表上已经画了方框,数据也已收集,但面板却没有更新。此外,日志中没有关于无效价格或水平的错误信息。

,我在我的版本中添加了日志信息;从中我可以看到,当范围过大或过小时,面板不会更新;因此这可能是部分原因。

,我会仔细检查测试数据的质量。谢谢你指出了你测试的货币对;我一定会对我选择的货币对进行调整。

非常感谢你的帮助。

当然,欢迎您。

Torsten Busch
Torsten Busch | 11 11月 2025 在 21:01

感谢您与我们分享您的代码。

由于我自己也编写了会话依赖型 EA,我可以告诉您,只有当您的经纪商始终处于 GMT+1 时区并且使用英国夏令时 时,代码才会起作用。

在所有其他情况下,您的开始时间将不起作用。为什么?因为伦敦会议的开始时间是英国时间上午 8:00。冬天是格林尼治标准时间 8:00,夏天是格林尼治标准时间 7:00。

TimeCurrent() 不会返回您的本地时间,而总是返回交易服务器的时间。

交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
经典策略重构(第14部分):多策略分析 经典策略重构(第14部分):多策略分析
在本文中,我们继续探讨如何构建多策略组合体系,并使用 MT5 遗传算法优化器对策略参数进行调优。本次我们使用 Python 对数据进行分析,结果表明:我们的模型能更准确地预判哪一个策略会表现更优,其预测精度高于直接预测市场收益率。然而,当我们使用这些统计模型对应用程序进行测试时,性能却大幅下滑。我们随后发现,遗憾的是,遗传优化器偏向了高度相关的策略,这促使我们修改方案:将投票权重固定,转而让优化器专注于优化指标参数。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
MetaTrader 5 机器学习蓝图(第一部分):数据泄露与时间戳修正 MetaTrader 5 机器学习蓝图(第一部分):数据泄露与时间戳修正
在开始将机器学习用于 MetaTrader 5 交易之前,必须先处理一个常被忽视的关键问题:数据泄露。本文深入剖析了数据泄露,尤其是 MetaTrader 5 时间戳陷阱,说明它如何扭曲模型表现并导致不可靠的交易信号。通过深入研究这一问题的机理并提出预防策略,我们为构建稳健的机器学习模型铺平了道路,这些模型能够在实时交易环境中提供值得信赖的预测结果。