MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA)·进阶篇
(2/3)·手动盯亚洲盘区间常被假突破洗出去?这篇把时段框+MA过滤+动态止损写成可跑的EA
「用 OnTick 卡死交易时段边界」
EA 退出前必须释放指标句柄,否则 MT5 会残留无效资源。下面这段在 deinit 里先判断 maHandle 是否不等于 INVALID_HANDLE,成立才调用 IndicatorRelease 交还系统,图表上手动画的线则保留供复盘翻看。 真正控制交易节奏的是 OnTick。每笔报价进来先 TimeCurrent 抓服务器时间(默认当 GMT 用),再 TimeToStruct 拆成年月日时分秒存进 dt 结构,后面比大小全靠它。 判定收市很简单:dt.hour 大于 SessionEndHour,或者小时相等且 dt.min 大于等于 SessionEndMinute,就认为已过当日交易尾盘。此时新建 sesEnd 结构,把年月日抄成今天,时分秒填用户参数并把 sec 置 0,StructToTime 转回 datetime 即得精确收市点。 跨天会话要单独处理。若 SessionStartHour 晚于 SessionEndHour(或同小时但分钟更大),说明是隔夜品种,sessionStart 不能直接用今天,得拿 sessionEnd 减 86400 秒退回前一日——这一步漏了,黄金或外汇夜盘会直接错判 24 小时。
class=class="str">"cmt">//--- Release the MA handle if valid if(maHandle != INVALID_HANDLE) class=class="str">"cmt">//--- Check if MA handle exists IndicatorRelease(maHandle); class=class="str">"cmt">//--- Release the MA handle class=class="str">"cmt">//--- Drawn objects remain on the chart for historical reference } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick(){ class=class="str">"cmt">//--- Get the current server time(assumed GMT) class="type">class="kw">datetime currentTime = TimeCurrent(); class=class="str">"cmt">//--- Retrieve current time class="type">MqlDateTime dt; class=class="str">"cmt">//--- Declare a structure for time components TimeToStruct(currentTime, dt); class=class="str">"cmt">//--- Convert current time to structure class=class="str">"cmt">//--- 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)){ class=class="str">"cmt">//--- Build the session end time using today&class="macro">#x27;s date and user-defined session end time class="type">MqlDateTime sesEnd; class=class="str">"cmt">//--- Declare a structure for session end time sesEnd.year = dt.year; class=class="str">"cmt">//--- Set year sesEnd.mon = dt.mon; class=class="str">"cmt">//--- Set month sesEnd.day = dt.day; class=class="str">"cmt">//--- Set day sesEnd.hour = SessionEndHour; class=class="str">"cmt">//--- Set session end hour sesEnd.min = SessionEndMinute; class=class="str">"cmt">//--- Set session end minute sesEnd.sec = class="num">0; class=class="str">"cmt">//--- Set seconds to class="num">0 class="type">class="kw">datetime sessionEnd = StructToTime(sesEnd); class=class="str">"cmt">//--- Convert structure to class="type">class="kw">datetime class=class="str">"cmt">//--- Determine the session start time class="type">class="kw">datetime sessionStart; class=class="str">"cmt">//--- Declare variable for session start time class=class="str">"cmt">//--- If session start is later than or equal to session end, assume overnight session if(SessionStartHour > SessionEndHour || (SessionStartHour == SessionEndHour && SessionStartMinute >= SessionEndMinute)){ class="type">class="kw">datetime prevDay = sessionEnd - class="num">86400; class=class="str">"cmt">//--- Subtract class="num">24 hours to get previous day
◍ 跨日会话起点与箱体重算的判定
处理会话起点时,若 prevDay 与 sesEnd 不在同一天,就给前一天的 MqlDateTime 强行塞入 SessionStartHour / SessionStartMinute 并把秒置 0,再 StructToTime 回去;否则直接用 sesEnd 的年月日拼出当天的起点。这样能避免把「今天 08:00」误挂到昨天的 K 线上。 只有 sessionEnd 不等于 lastBoxSessionEnd 时才进 ComputeBox。该判断把重算频率压到每个会话一次,而不是每根 tick 都跑;同时把 boxCalculated 置 true、ordersPlaced 复位 false,相当于给新会话清了挂单状态。外汇与贵金属波动剧烈,这套时间拼装若写错 hour/min,箱体高低点会整体偏移,实盘前务必在 MT5 用 Print(sessionStart) 核对。
class="type">MqlDateTime dtPrev; class=class="str">"cmt">//--- Declare structure for previous day time TimeToStruct(prevDay, dtPrev); class=class="str">"cmt">//--- Convert previous day time to structure dtPrev.hour = SessionStartHour; class=class="str">"cmt">//--- Set session start hour for previous day dtPrev.min = SessionStartMinute; class=class="str">"cmt">//--- Set session start minute for previous day dtPrev.sec = class="num">0; class=class="str">"cmt">//--- Set seconds to class="num">0 sessionStart = StructToTime(dtPrev); class=class="str">"cmt">//--- Convert structure back to class="type">class="kw">datetime } else{ class=class="str">"cmt">//--- Otherwise, use today&class="macro">#x27;s date for session start class="type">MqlDateTime temp; class=class="str">"cmt">//--- Declare temporary structure temp.year = sesEnd.year; class=class="str">"cmt">//--- Set year from session end structure temp.mon = sesEnd.mon; class=class="str">"cmt">//--- Set month from session end structure temp.day = sesEnd.day; class=class="str">"cmt">//--- Set day from session end structure temp.hour = SessionStartHour; class=class="str">"cmt">//--- Set session start hour temp.min = SessionStartMinute; class=class="str">"cmt">//--- Set session start minute temp.sec = class="num">0; class=class="str">"cmt">//--- Set seconds to class="num">0 sessionStart = StructToTime(temp); class=class="str">"cmt">//--- Convert structure to class="type">class="kw">datetime } class=class="str">"cmt">//--- Recalculate the session box only if this session hasn&class="macro">#x27;t been processed before if(sessionEnd != lastBoxSessionEnd){ ComputeBox(sessionStart, sessionEnd); class=class="str">"cmt">//--- Compute session box using start and end times lastBoxSessionEnd = sessionEnd; class=class="str">"cmt">//--- Update last processed session end time boxCalculated = true; class=class="str">"cmt">//--- Set flag indicating the box has been calculated ordersPlaced = false; class=class="str">"cmt">//--- Reset flag for order placement for the new session }
用会话起止时间圈出高低点时刻
在 MT5 里做价格区间统计,第一步是把指定会话窗口内的极值和它们发生的时间抓出来。下面这段函数以 sessionStart、sessionEnd 为边界,遍历对应周期的所有 K 线,只处理落在该时间区间内的 bar。 初始化时 highVal 设为 -DBL_MAX、lowVal 设为 DBL_MAX,保证任何真实价格都能刷新极值;同时把 BoxHighTime、BoxLowTime 清零,避免上一次会话的残留时间干扰本次记录。 循环里每遇到一根时间在窗口内的 bar,就比较其 high/low 与当前极值,一旦突破就同步更新价格与 rates[i].time。这样跑完一遍,你就拿到了该会话的最高价、最低价以及它们各自触发的精确时间点,可直接用于后续绘制区间或触发突破逻辑。外汇与贵金属波动受时段流动性影响大,这类统计仅反映历史分布,实盘突破概率仍受新闻与流动性冲击,属高风险操作。 让小布替你跑这套 把 BoxTimeframe 切到 M5、sessionStart/End 填亚盘 00:00–08:00,回测一周就能看到高低点时刻是否集中在某两小时,据此调你的突破挂单时间窗。
class="type">void ComputeBox(class="type">class="kw">datetime sessionStart, class="type">class="kw">datetime sessionEnd){ class="type">int totalBars = Bars(_Symbol, BoxTimeframe); class=class="str">"cmt">//--- Get total number of bars on the specified timeframe if(totalBars <= class="num">0){ Print("No bars available on timeframe ", EnumToString(BoxTimeframe)); class=class="str">"cmt">//--- Print error if no bars available class="kw">return; class=class="str">"cmt">//--- Exit if no bars are found } class="type">MqlRates rates[]; class=class="str">"cmt">//--- Declare an array to hold bar data ArraySetAsSeries(rates, false); class=class="str">"cmt">//--- Set array to non-series order(oldest first) class="type">int copied = CopyRates(_Symbol, BoxTimeframe, class="num">0, totalBars, rates); class=class="str">"cmt">//--- Copy bar data into array if(copied <= class="num">0){ Print("Failed to copy rates for box calculation."); class=class="str">"cmt">//--- Print error if copying fails class="kw">return; class=class="str">"cmt">//--- Exit if error occurs } class="type">class="kw">double highVal = -DBL_MAX; class=class="str">"cmt">//--- Initialize high value to the lowest possible class="type">class="kw">double lowVal = DBL_MAX; class=class="str">"cmt">//--- Initialize low value to the highest possible class=class="str">"cmt">//--- Reset the times for the session extremes BoxHighTime = class="num">0; class=class="str">"cmt">//--- Reset stored high time BoxLowTime = class="num">0; class=class="str">"cmt">//--- Reset stored low time class=class="str">"cmt">//--- Loop through each bar within the session period to find the extremes for(class="type">int i = class="num">0; i < copied; i++){ if(rates[i].time >= sessionStart && rates[i].time <= sessionEnd){ if(rates[i].high > highVal){ highVal = rates[i].high; class=class="str">"cmt">//--- Update highest price BoxHighTime = rates[i].time; class=class="str">"cmt">//--- Record time of highest price } if(rates[i].low < lowVal){
「把时段高低点落进图形对象」
扫描完指定时段内的 K 线后,代码把实时刷新出的最低价 lowVal 与对应时间 BoxLowTime 存下来,最高价同理。若 highVal 仍等于 -DBL_MAX 或 lowVal 等于 DBL_MAX,说明该时段内没有任何有效 BAR,直接 Print 报错并 return 退出,避免画出空框。 确认有数据后,BoxHigh 与 BoxLow 被赋为最终高低值,并用 TimeToString 把高低点发生时间打印到日志,方便你回看某根 session 极值究竟出现在几点。随后调用 DrawSessionObjects(sessionStart, sessionEnd),把矩形、水平线、价签一次性画到图上。 DrawSessionObjects 先取 ChartGetInteger(0, CHART_SCALE, 0) 拿到 0~5 的图表缩放级,再算动态字号:dynamicFontSize = 7 + chartScale * 1,即缩放每升一级字号加 1;线宽按 dynamicLineWidth = MathRound(1 + chartScale*2.0/5) 线性插值,缩放 5 级时线宽约为 3。sessionID 用 lastBoxSessionEnd 拼成 "Sess_" 前缀,保证每个时段框对象名不冲突。外汇与贵金属波动剧烈,这类时段框仅作结构参考,实盘须自担高风险。
lowVal = rates[i].low; class=class="str">"cmt">//--- Update lowest price BoxLowTime = rates[i].time; class=class="str">"cmt">//--- Record time of lowest price } } } if(highVal == -DBL_MAX || lowVal == DBL_MAX){ Print("No valid bars found within the session time range."); class=class="str">"cmt">//--- Print error if no valid bars found class="kw">return; class=class="str">"cmt">//--- Exit if invalid data } BoxHigh = highVal; class=class="str">"cmt">//--- Store final highest price BoxLow = lowVal; class=class="str">"cmt">//--- Store final lowest price Print("Session box computed: High = ", BoxHigh, " at ", TimeToString(BoxHighTime), ", Low = ", BoxLow, " at ", TimeToString(BoxLowTime)); class=class="str">"cmt">//--- Output computed session box data class=class="str">"cmt">//--- Draw all session objects(rectangle, horizontal lines, and price labels) DrawSessionObjects(sessionStart, sessionEnd); class=class="str">"cmt">//--- Call function to draw objects using computed values } class="type">void DrawSessionObjects(class="type">class="kw">datetime sessionStart, class="type">class="kw">datetime sessionEnd){ class="type">int chartScale = (class="type">int)ChartGetInteger(class="num">0, CHART_SCALE, class="num">0); class=class="str">"cmt">//--- Retrieve the chart scale(class="num">0 to class="num">5) class="type">int dynamicFontSize = class="num">7 + chartScale * class="num">1; class=class="str">"cmt">//--- Base class="num">7, increase by class="num">2 per scale level class="type">int dynamicLineWidth = (class="type">int)MathRound(class="num">1 + (chartScale * class="num">2.0 / class="num">5)); class=class="str">"cmt">//--- Linear interpolation class=class="str">"cmt">//--- Create a unique session identifier using the session end time class="type">class="kw">string sessionID = "Sess_" + IntegerToString(lastBoxSessionEnd); class=class="str">"cmt">//--- Draw the filled rectangle(box) using the recorded high/low times and prices
◍ 把交易时段框进MT5图层的画法
在 MT5 上把某个交易时段的高低价区可视化,核心是用 ObjectCreate 按对象类型分别建矩形、趋势线和文本。下面这段逻辑给每个时段加了唯一后缀 sessionID,避免多周期重绘时对象互相覆盖。 矩形用 OBJ_RECTANGLE,以 BoxHighTime/BoxLowTime 为时间边界、BoxHigh/BoxLow 为价格边界;填充色 clrThistle、置底显示,能直观看出时段波动包裹区。上下边界再用 OBJ_TREND 画水平线,顶线蓝、底线红,线宽走 dynamicLineWidth 变量,RAY_RIGHT 关掉才不会向右无限延伸。 价格标签用 OBJ_TEXT 挂在 sessionEnd 的 BoxHigh 处,DoubleToString(BoxHigh, _Digits) 保证小数位和品种一致。外汇与贵金属波动剧烈,这类图形仅辅助辨识结构,实际突破方向仍可能反向,请自行在策略测试器验证。
class="type">class="kw">string rectName = "SessionRect_" + sessionID; class=class="str">"cmt">//--- Unique name for the rectangle if(!ObjectCreate(class="num">0, rectName, OBJ_RECTANGLE, class="num">0, BoxHighTime, BoxHigh, BoxLowTime, BoxLow)) Print("Failed to create rectangle: ", rectName); class=class="str">"cmt">//--- Print error if creation fails ObjectSetInteger(class="num">0, rectName, OBJPROP_COLOR, clrThistle); class=class="str">"cmt">//--- Set rectangle class="type">color to blue ObjectSetInteger(class="num">0, rectName, OBJPROP_FILL, true); class=class="str">"cmt">//--- Enable filling of the rectangle ObjectSetInteger(class="num">0, rectName, OBJPROP_BACK, true); class=class="str">"cmt">//--- Draw rectangle in background class=class="str">"cmt">//--- Draw the top horizontal line spanning from sessionStart to sessionEnd at the session high class="type">class="kw">string topLineName = "SessionTopLine_" + sessionID; class=class="str">"cmt">//--- Unique name for the top line if(!ObjectCreate(class="num">0, topLineName, OBJ_TREND, class="num">0, sessionStart, BoxHigh, sessionEnd, BoxHigh)) Print("Failed to create top line: ", topLineName); class=class="str">"cmt">//--- Print error if creation fails ObjectSetInteger(class="num">0, topLineName, OBJPROP_COLOR, clrBlue); class=class="str">"cmt">//--- Set line class="type">color to blue ObjectSetInteger(class="num">0, topLineName, OBJPROP_WIDTH, dynamicLineWidth); class=class="str">"cmt">//--- Set line width dynamically ObjectSetInteger(class="num">0, topLineName, OBJPROP_RAY_RIGHT, false); class=class="str">"cmt">//--- Do not extend line infinitely class=class="str">"cmt">//--- Draw the bottom horizontal line spanning from sessionStart to sessionEnd at the session low class="type">class="kw">string bottomLineName = "SessionBottomLine_" + sessionID; class=class="str">"cmt">//--- Unique name for the bottom line if(!ObjectCreate(class="num">0, bottomLineName, OBJ_TREND, class="num">0, sessionStart, BoxLow, sessionEnd, BoxLow)) Print("Failed to create bottom line: ", bottomLineName); class=class="str">"cmt">//--- Print error if creation fails ObjectSetInteger(class="num">0, bottomLineName, OBJPROP_COLOR, clrRed); class=class="str">"cmt">//--- Set line class="type">color to blue ObjectSetInteger(class="num">0, bottomLineName, OBJPROP_WIDTH, dynamicLineWidth); class=class="str">"cmt">//--- Set line width dynamically ObjectSetInteger(class="num">0, bottomLineName, OBJPROP_RAY_RIGHT, false); class=class="str">"cmt">//--- Do not extend line infinitely class=class="str">"cmt">//--- Create the top price label at the right edge of the top horizontal line class="type">class="kw">string topLabelName = "SessionTopLabel_" + sessionID; class=class="str">"cmt">//--- Unique name for the top label if(!ObjectCreate(class="num">0, topLabelName, OBJ_TEXT, class="num">0, sessionEnd, BoxHigh)) Print("Failed to create top label: ", topLabelName); class=class="str">"cmt">//--- Print error if creation fails ObjectSetString(class="num">0, topLabelName, OBJPROP_TEXT," "+DoubleToString(BoxHigh, _Digits)); class=class="str">"cmt">//--- Set label text to session high price ObjectSetInteger(class="num">0, topLabelName, OBJPROP_COLOR, clrBlack); class=class="str">"cmt">//--- Set label class="type">color to blue
用退出时间与均线过滤挂单时机
会话框算完、订单还没挂、且当前时间早于设定的交易退出时间,这三个条件同时成立时才允许进场挂单。退出时间由用户设置的小时和分钟拼到当天日期上,秒固定为 0,用 MqlDateTime 结构转换得到 datetime。 进场前还会拉一次均线数值做过滤。代码里用 CopyBuffer(maHandle,0,0,1,maBuffer) 只拷贝最新 1 根 MA 值,若返回值 <=0 直接 Print 报错并 return,避免无均线数据就盲目挂单。 外汇与贵金属杠杆高、滑点跳空频繁,这套时间+均线双重过滤只降低乱挂单概率,不保证胜率,实盘前请在 MT5 策略测试器用真实点差回测。
ObjectSetInteger(class="num">0, topLabelName, OBJPROP_FONTSIZE, dynamicFontSize); class=class="str">"cmt">//--- Set dynamic font size for label ObjectSetInteger(class="num">0, topLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); class=class="str">"cmt">//--- Anchor label to the left so text appears to right class=class="str">"cmt">//--- Create the bottom price label at the right edge of the bottom horizontal line class="type">class="kw">string bottomLabelName = "SessionBottomLabel_" + sessionID; class=class="str">"cmt">//--- Unique name for the bottom label if(!ObjectCreate(class="num">0, bottomLabelName, OBJ_TEXT, class="num">0, sessionEnd, BoxLow)) Print("Failed to create bottom label: ", bottomLabelName); class=class="str">"cmt">//--- Print error if creation fails ObjectSetString(class="num">0, bottomLabelName, OBJPROP_TEXT," "+DoubleToString(BoxLow, _Digits)); class=class="str">"cmt">//--- Set label text to session low price ObjectSetInteger(class="num">0, bottomLabelName, OBJPROP_COLOR, clrBlack); class=class="str">"cmt">//--- Set label class="type">color to blue ObjectSetInteger(class="num">0, bottomLabelName, OBJPROP_FONTSIZE, dynamicFontSize); class=class="str">"cmt">//--- Set dynamic font size for label ObjectSetInteger(class="num">0, bottomLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); class=class="str">"cmt">//--- Anchor label to the left so text appears to right } class=class="str">"cmt">//--- Build the trade exit time using user-defined hour and minute for today class="type">MqlDateTime exitTimeStruct; class=class="str">"cmt">//--- Declare a structure for exit time TimeToStruct(currentTime, exitTimeStruct); class=class="str">"cmt">//--- Use current time&class="macro">#x27;s date components exitTimeStruct.hour = TradeExitHour; class=class="str">"cmt">//--- Set trade exit hour exitTimeStruct.min = TradeExitMinute; class=class="str">"cmt">//--- Set trade exit minute exitTimeStruct.sec = class="num">0; class=class="str">"cmt">//--- Set seconds to class="num">0 class="type">class="kw">datetime tradeExitTime = StructToTime(exitTimeStruct); class=class="str">"cmt">//--- Convert exit time structure to class="type">class="kw">datetime class=class="str">"cmt">//--- 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){ class="type">class="kw">double maBuffer[]; class=class="str">"cmt">//--- Declare array to hold MA values ArraySetAsSeries(maBuffer, true); class=class="str">"cmt">//--- Set the array as series(newest first) if(CopyBuffer(maHandle, class="num">0, class="num">0, class="num">1, maBuffer) <= class="num">0){ class=class="str">"cmt">//--- Copy class="num">1 value from the MA buffer Print("Failed to copy MA buffer."); class=class="str">"cmt">//--- Print error if buffer copy fails class="kw">return; class=class="str">"cmt">//--- Exit the function if error occurs } class="type">class="kw">double maValue = maBuffer[class="num">0]; class=class="str">"cmt">//--- Retrieve the current MA value