MQL5 交易策略自动化(第24篇):集成风险管理与移动止损的伦敦时段突破系统
引言
在上一篇文章(第 23 篇)中,我们对包络线趋势交易的区间修复系统进行了功能升级,基于 MQL5 语言新增移动止损与多篮子交易管理功能,更好地实现利润保护与信号管理。在第 24 篇中,我们将开发一套伦敦时段突破交易系统:能够识别盘前震荡区间、自动设置挂单,并集成风险管理工具,包含盈亏比、回撤限制,以及用于实时监控的控制面板。本文将包括几个方面:
学完本章后,你将拥有一套功能完整、自带高级风控的 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 字符代码的选择可根据你的界面偏好决定。以下是可供选择的编码列表。

接下来,我们实现 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
经典策略重构(第14部分):多策略分析
新手在交易中的10个基本错误
MetaTrader 5 机器学习蓝图(第一部分):数据泄露与时间戳修正
第二个问题是控制面板中的最高/最低水平和买入/卖出水平没有更新。
在图表上可以清楚地找到高/低范围水位,因此我猜测买入/卖出水位也应该显示在图表上,并在控制面板中更新,因为它们是直接从高/低范围水位导出的。
,您有什么建议可以让它正常工作?
,在此先表示感谢。
至于您的第二个问题,文章中已经解释过了,但假设您的问题是由于测试数据不佳造成的,并给出提示,当范围处于计算状态时,您将始终看到 "计算中... "状态,直到有足够的数据来设置伦敦范围会话或您在输入中定义的会话。假设您使用的是默认设置,伦敦时间前为 3 小时,而您从共享截图中看到的时间是 2 月 13 日,22:00 之后的 2 个条形图是 2*15 分钟 = 30,因此给出的 22:30 在范围计算时间之外,所以面板上的数据应该仍然可见,因为之前设置的范围仍在起作用,除非尚未找到第一个时段,并且从午夜开始达到范围计算时将被清除。见下图:
您可能需要查看以下查找范围的逻辑
以及如何设置。
请看下图,虽然我们不知道您测试的年份,但我们会取 2025 年,如果像您的情况一样是 2020 年,我们没有这方面的高质量数据,所以无论如何,我们都使用 2025 年,因此范围计算应从午夜开始。
从图中可以看到,23:55 时的数据仍然完好无损。但是,当午夜来临时,我们需要重新设置。请看下图。
可以看到,我们在午夜重置了其他范围计算的数据。实际上,当范围计算完成后,可视化可以帮助你了解真正发生了什么。例如,在您使用默认设置的情况下,我们将看到从 0300 时到 0800 时的运行条形图,因为这是我们定义的。见下图:
希望这能再次说明问题。您可以根据自己的交易风格调整一切。为避免您遇到的问题,建议您使用可靠的测试数据。谢谢。
至于您的第二个问题,文章中已有解释,但假设您的问题源于测试数据不佳并给出提示,那么当范围处于计算状态时,您将始终看到 "计算中... "状态,直到有足够的数据来设置伦敦范围会话或您在输入中定义的会话。假设您使用的是默认设置,伦敦前时间为 3 小时,而您从共享截图中看到的时间是 2 月 13 日,22:00 之后的 2 个条形图是 2*15 分钟 = 30,因此给出的 22:30 在范围计算时间之外,所以面板上的数据应该仍然可见,因为之前设置的范围仍在起作用,除非尚未找到第一个时段,并且会在午夜达到范围计算时被清除。请参见下文:
您可能需要查看以下查找范围的逻辑
以及如何设置。
请看下图,虽然我们不知道您测试的年份,但我们会选择 2025 年,如果像您的情况一样是 2020 年,我们没有这方面的高质量数据,所以无论如何,我们都会使用 2025 年,因此范围计算应从午夜开始。
从图中可以看到,23:55 时的数据仍然完好无损。但是,当午夜来临时,我们需要重新设置。请看下图。
你可以看到,为了进行其他范围计算,我们在午夜重置了数据。实际上,当范围计算完成后,可视化可以帮助你了解真正发生了什么。例如,在您使用默认设置的情况下,我们将看到从 0300 时到 0800 时的运行条形图,因为这是我们定义的。见下图:
希望这能再次说明问题。您可以根据自己的交易风格调整一切。为避免您遇到的问题,建议您使用可靠的测试数据。谢谢。
非常感谢你的全面答复。
是的,我确实读了这篇文章,并跟着自己的拷贝编码,直到我遇到了我所概述的问题。我看到的是面板没有更新,即使在默认时间内也是如此。我截图的目的是想说明,虽然图表上已经画出了方框,数据也已收集,但面板却没有更新。此外,日志中没有关于无效价格或水平的错误信息。
,我在我的版本中添加了日志信息;从中我可以看到,当范围过大或过小时,面板不会更新;因此这可能是部分原因。
,我会仔细检查测试数据的质量。谢谢你指出了你测试的货币对;我一定会对我选择的货币对进行调整。
非常感谢你的帮助。
非常感谢你的全面答复。
是的,我确实读了这篇文章,并跟着自己的拷贝编码,直到我遇到了我概述的问题。我看到的是面板没有更新,即使在默认时间内也是如此。我截图的目的是想说明,虽然图表上已经画了方框,数据也已收集,但面板却没有更新。此外,日志中没有关于无效价格或水平的错误信息。
,我在我的版本中添加了日志信息;从中我可以看到,当范围过大或过小时,面板不会更新;因此这可能是部分原因。
,我会仔细检查测试数据的质量。谢谢你指出了你测试的货币对;我一定会对我选择的货币对进行调整。
非常感谢你的帮助。
当然,欢迎您。
感谢您与我们分享您的代码。
由于我自己也编写了会话依赖型 EA,我可以告诉您,只有当您的经纪商始终处于 GMT+1 时区并且使用英国夏令时 时,代码才会起作用。
在所有其他情况下,您的开始时间将不起作用。为什么?因为伦敦会议的开始时间是英国时间上午 8:00。冬天是格林尼治标准时间 8:00,夏天是格林尼治标准时间 7:00。
TimeCurrent() 不会返回您的本地时间,而总是返回交易服务器的时间。