MQL5 交易策略自动化(第十部分):开发趋势盘整动量策略
引言
在上一篇文章(第九部分)中,我们开发了一个EA,通过使用关键时段水平和动态风险管理,在MetaQuotes 语言 5 (MQL5) 中实现了亚洲突破策略的自动化。现在,在第十部分中,我们将重点转向趋势盘整动量策略——该方法结合了双移动平均线交叉与相对强弱指数 (RSI) 和商品通道指数(CCI) 等动量过滤器,以精准捕捉趋势运动。我们将通过以下主题来构建本文:
到文末,我们将得到一个功能完备EA,用于自动化趋势盘整动量策略。让我们开始深入探讨。
策略的具体实现
趋势盘整动量策略旨在通过将一个简单的移动平均线交叉系统与稳健的动量过滤相结合,来捕捉市场趋势。其核心思想是,当快速移动平均线向上穿越慢速移动平均线时(暗示看涨趋势)产生买入信号,同时使用动量指标——即一个RSI和两个不同周期的CCI——来确认该信号。相反,当慢速移动平均线超过快速移动平均线,并且动量指标确认了看跌条件时,则发出做空信号。指标设置如下:
- 商品通道指数 (CCI) (36周期, 收盘价)
- 商品通道指数 (CCI) (55周期, 收盘价)
- 慢速相对强弱指数 (RSI) (27周期, 收盘价)
- 快速移动平均线 (11周期, 平滑, 中位价)
- 慢速移动平均线 (25周期, 平滑, 中位价)
至于退出策略,对于多头交易,我们将止损设在前一个波段低点;对于空头交易,则设在前一个波段高点。止盈将设在一个预定水平,即距离入场价格300点。这种多层面的方法将有助于过滤掉假信号,并通过确保趋势方向和动量保持一致,旨在提高交易入场质量。简而言之,下图描绘了简化的策略计划。

在MQL5中的实现
要在 MQL5 中创建程序,请打开MetaEditor,转到“导航器”窗口,找到“指标”文件夹,点击“新建”选项卡,然后按照提示创建文件。文件创建完成后,在代码编环境下,我们需要声明一些将在整个程序中使用的全局变量。
//+------------------------------------------------------------------+ //| Copyright 2025, Forex Algo-Trader, Allan. | //| "https://t.me/Forex_Algo_Trader" | //+------------------------------------------------------------------+ #property copyright "Forex Algo-Trader, Allan" #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property description "This EA trades based on Trend Flat Momentum Strategy" #property strict #include <Trade\Trade.mqh> //--- Include the Trade library for order management. CTrade obj_Trade; //--- Create an instance of the CTrade class to handle trading operations. // Input parameters input int InpCCI36Period = 36; //--- CCI period 1 input int InpCCI55Period = 55; //--- CCI period 2 input int InpRSIPeriod = 27; //--- RSI period input int InpMAFastPeriod = 11; //--- Fast MA period input int InpMASlowPeriod = 25; //--- Slow MA period input double InpRSIThreshold = 58.0; //--- RSI threshold for Buy signal (Sell uses 100 - Threshold) input int InpTakeProfitPoints = 300; //--- Take profit in points input double InpLotSize = 0.1; //--- Trade lot size // Pivot parameters for detecting swing highs/lows input int PivotLeft = 2; //--- Number of bars to the left for pivot detection input int PivotRight = 2; //--- Number of bars to the right for pivot detection // Global indicator handles int handleCCI36; //--- Handle for the CCI indicator with period InpCCI36Period int handleCCI55; //--- Handle for the CCI indicator with period InpCCI55Period int handleRSI; //--- Handle for the RSI indicator with period InpRSIPeriod int handleMA11; //--- Handle for the fast moving average (MA) with period InpMAFastPeriod int handleMA25; //--- Handle for the slow moving average (MA) with period InpMASlowPeriod // Global dynamic storage buffers double ma11_buffer[]; //--- Dynamic array to store fast MA values double ma25_buffer[]; //--- Dynamic array to store slow MA values double rsi_buffer[]; //--- Dynamic array to store RSI values double cci36_buffer[]; //--- Dynamic array to store CCI values (period 36) double cci55_buffer[]; //--- Dynamic array to store CCI values (period 55) // To detect a new bar datetime lastBarTime = 0; //--- Variable to store the time of the last processed bar
我们首先使用 #include 包含 “Trade\Trade.mqh” 文件,以访问内置的交易函数,并创建 “CTrade” 类的一个实例 “obj_Trade”,用于执行买入和卖出订单。我们为策略配置定义了关键的 输入 参数,包括用于 “CCI” 指标的 “InpCCI36Period” 和 “InpCCI55Period”,用于 “RSI” 的 “InpRSIPeriod”,以及用于两条移动平均线的 “InpMAFastPeriod” 和 “InpMASlowPeriod”。“InpRSIThreshold” 设置了交易过滤的条件,而 “InpTakeProfitPoints” 决定了固定的止盈水平,“InpLotSize” 则控制仓位大小。
为了改进交易执行,我们引入了 “PivotLeft” 和 “PivotRight”,它们定义了用于检测波段高点和低点以设置止损的K线数量。全局指标句柄,如 “handleCCI36”、“handleCCI55”、“handleRSI”、“handleMA11” 和 “handleMA25”,使我们能够高效地检索指标值。动态数组将这些值存储在 “ma11_buffer”、“ma25_buffer”、“rsi_buffer”、“cci36_buffer” 和 “cci55_buffer” 中,确保了数据的顺畅处理。最后,“lastBarTime” 跟踪最后处理过的K线,以防止在同一根K线上进行多次交易,从而确保交易执行的准确性。然后,我们就可以在 OnInit 事件处理程序中初始化这些指标了。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create CCI handle for period InpCCI36Period using the close price. handleCCI36 = iCCI(_Symbol, _Period, InpCCI36Period, PRICE_CLOSE); //--- Create the CCI36 indicator handle. if (handleCCI36 == INVALID_HANDLE) { //--- Check if the CCI36 handle is valid. Print("Error creating CCI36 handle"); //--- Print an error message if invalid. return (INIT_FAILED); //--- Return failure if handle creation failed. } //--- Create CCI handle for period InpCCI55Period using the close price. handleCCI55 = iCCI(_Symbol, _Period, InpCCI55Period, PRICE_CLOSE); //--- Create the CCI55 indicator handle. if (handleCCI55 == INVALID_HANDLE) { //--- Check if the CCI55 handle is valid. Print("Error creating CCI55 handle"); //--- Print an error message if invalid. return (INIT_FAILED); //--- Return failure if handle creation failed. } //--- Create RSI handle for period InpRSIPeriod using the close price. handleRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE); //--- Create the RSI indicator handle. if (handleRSI == INVALID_HANDLE) { //--- Check if the RSI handle is valid. Print("Error creating RSI handle"); //--- Print an error message if invalid. return (INIT_FAILED); //--- Return failure if handle creation failed. } //--- Create fast MA handle using MODE_SMMA on the median price with period InpMAFastPeriod. handleMA11 = iMA(_Symbol, _Period, InpMAFastPeriod, 0, MODE_SMMA, PRICE_MEDIAN); //--- Create the fast MA handle. if (handleMA11 == INVALID_HANDLE) { //--- Check if the fast MA handle is valid. Print("Error creating MA11 handle"); //--- Print an error message if invalid. return (INIT_FAILED); //--- Return failure if handle creation failed. } //--- Create slow MA handle using MODE_SMMA on the median price with period InpMASlowPeriod. handleMA25 = iMA(_Symbol, _Period, InpMASlowPeriod, 0, MODE_SMMA, PRICE_MEDIAN); //--- Create the slow MA handle. if (handleMA25 == INVALID_HANDLE) { //--- Check if the slow MA handle is valid. Print("Error creating MA25 handle"); //--- Print an error message if invalid. return (INIT_FAILED); //--- Return failure if handle creation failed. } //--- Set the dynamic arrays as time series (index 0 = most recent closed bar). ArraySetAsSeries(ma11_buffer, true); //--- Set ma11_buffer as a time series. ArraySetAsSeries(ma25_buffer, true); //--- Set ma25_buffer as a time series. ArraySetAsSeries(rsi_buffer, true); //--- Set rsi_buffer as a time series. ArraySetAsSeries(cci36_buffer, true); //--- Set cci36_buffer as a time series. ArraySetAsSeries(cci55_buffer, true); //--- Set cci55_buffer as a time series. return (INIT_SUCCEEDED); //--- Return success after initialization. }
在 OnInit 事件处理程序中,我们通过创建和验证指标句柄来初始化EA。我们使用 iCCI 函数创建周期为 “InpCCI36Period” 和 “InpCCI55Period” 的 CCI 句柄,使用 iRSI 函数创建 RSI 句柄,并使用 iMA 函数创建周期为 “InpMAFastPeriod” 和 “InpMASlowPeriod” 的快速和慢速 SMMA(平滑移动平均线)句柄。如果任何句柄无效(INVALID_HANDLE),我们将打印一条错误信息并返回失败(INIT_FAILED)。最后,我们使用 ArraySetAsSeries 函数将缓冲区设置为时间序列格式,并在成功初始化后返回成功。为了节省资源,当程序被移除时,我们需要按如下方式释放已创建的句柄。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (handleCCI36 != INVALID_HANDLE) { //--- If the CCI36 handle is valid, IndicatorRelease(handleCCI36); //--- release the CCI36 indicator. } if (handleCCI55 != INVALID_HANDLE) { //--- If the CCI55 handle is valid, IndicatorRelease(handleCCI55); //--- release the CCI55 indicator. } if (handleRSI != INVALID_HANDLE) { //--- If the RSI handle is valid, IndicatorRelease(handleRSI); //--- release the RSI indicator. } if (handleMA11 != INVALID_HANDLE) { //--- If the fast MA handle is valid, IndicatorRelease(handleMA11); //--- release the fast MA indicator. } if (handleMA25 != INVALID_HANDLE) { //--- If the slow MA handle is valid, IndicatorRelease(handleMA25); //--- release the slow MA indicator. } }
在这里,我们通过在 OnDeinit 中释放指标资源来处理EA的反初始化。我们检查每个指标句柄——“CCI36”、“CCI55”、“RSI”、快速移动平均线和慢速移动平均线——是否有效。如果有效,我们就使用 IndicatorRelease 函数释放已分配的资源,以确保高效的内存管理。现在我们可以继续处理 OnTick 事件处理程序,所有的数据处理和决策都将在此进行。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Get the time of the current bar. datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Retrieve the current bar's time. if (currentBarTime != lastBarTime) { //--- If a new bar has formed, lastBarTime = currentBarTime; //--- update lastBarTime with the current bar's time. OnNewBar(); //--- Process the new bar. } }
在这里,我们使用 OnTick 事件处理程序来监控价格更新并检测新的K线。我们使用 iTime 函数获取当前K线的时间,并将其与存储的 “lastBarTime” 值进行比较。如果检测到新的K线,我们就更新 “lastBarTime”,确保我们每根K线只交易一次,并调用 “OnNewBar” 函数来处理新K线的数据。这个函数负责处理所有的信号生成,其内容如下。
//+------------------------------------------------------------------+ //| Function called on every new bar (bar close) | //+------------------------------------------------------------------+ void OnNewBar() { //--- Resize the dynamic arrays to hold 2 values (last closed bar and the one before). ArrayResize(ma11_buffer, 2); //--- Resize ma11_buffer to 2 elements. ArrayResize(ma25_buffer, 2); //--- Resize ma25_buffer to 2 elements. ArrayResize(rsi_buffer, 2); //--- Resize rsi_buffer to 2 elements. ArrayResize(cci36_buffer, 2); //--- Resize cci36_buffer to 2 elements. ArrayResize(cci55_buffer, 2); //--- Resize cci55_buffer to 2 elements. //--- Copy indicator values into the dynamic arrays. if (CopyBuffer(handleMA11, 0, 1, 2, ma11_buffer) != 2) { //--- Copy 2 values from the fast MA indicator. return; //--- Exit the function if copying fails. } if (CopyBuffer(handleMA25, 0, 1, 2, ma25_buffer) != 2) { //--- Copy 2 values from the slow MA indicator. return; //--- Exit the function if copying fails. } if (CopyBuffer(handleRSI, 0, 1, 2, rsi_buffer) != 2) { //--- Copy 2 values from the RSI indicator. return; //--- Exit the function if copying fails. } if (CopyBuffer(handleCCI36, 0, 1, 2, cci36_buffer) != 2) { //--- Copy 2 values from the CCI36 indicator. return; //--- Exit the function if copying fails. } if (CopyBuffer(handleCCI55, 0, 1, 2, cci55_buffer) != 2) { //--- Copy 2 values from the CCI55 indicator. return; //--- Exit the function if copying fails. } //--- For clarity, assign the values from the arrays. //--- Index 0: last closed bar, Index 1: bar before. double ma11_current = ma11_buffer[0]; //--- Fast MA value for the last closed bar. double ma11_previous = ma11_buffer[1]; //--- Fast MA value for the previous bar. double ma25_current = ma25_buffer[0]; //--- Slow MA value for the last closed bar. double ma25_previous = ma25_buffer[1]; //--- Slow MA value for the previous bar. double rsi_current = rsi_buffer[0]; //--- RSI value for the last closed bar. double cci36_current = cci36_buffer[0]; //--- CCI36 value for the last closed bar. double cci55_current = cci55_buffer[0]; //--- CCI55 value for the last closed bar. }
在我们创建的 void 函数 “OnNewBar” 中,我们首先使用 ArrayResize 函数调整存放指标值的动态数组的大小,以便存储最近的两根已收盘K线。然后,我们使用 CopyBuffer 函数为快速移动平均线、慢速移动平均线、RSI 和两个 CCI 指标检索指标值。如果这些操作中的任何一个失败,函数将退出以防止错误。一旦值被成功复制,我们将它们分配给变量以便于引用,并区分最后一根已收盘K线和它之前的那根K线。这种设置确保我们始终拥有最新的市场数据,以便做出交易决策。如果我们成功检索到数据,就可以继续进行交易决策。我们从买入逻辑开始。
//--- Check for Buy Conditions: bool maCrossoverBuy = (ma11_previous < ma25_previous) && (ma11_current > ma25_current); //--- True if fast MA crosses above slow MA. bool rsiConditionBuy = (rsi_current > InpRSIThreshold); //--- True if RSI is above the Buy threshold. bool cci36ConditionBuy = (cci36_current > 0); //--- True if CCI36 is positive. bool cci55ConditionBuy = (cci55_current > 0); //--- True if CCI55 is positive. if (maCrossoverBuy) { //--- If crossover for MA Buy is true... bool conditionsOk = true; //--- Initialize a flag to track if all conditions are met. //--- Check RSI condition for Buy. if (!rsiConditionBuy) { //--- If the RSI condition is not met... Print("Buy signal rejected: RSI condition not met. RSI=", rsi_current, " Threshold=", InpRSIThreshold); //--- Notify the user. conditionsOk = false; //--- Mark the conditions as not met. } //--- Check CCI36 condition for Buy. if (!cci36ConditionBuy) { //--- If the CCI36 condition is not met... Print("Buy signal rejected: CCI36 condition not met. CCI36=", cci36_current); //--- Notify the user. conditionsOk = false; //--- Mark the conditions as not met. } //--- Check CCI55 condition for Buy. if (!cci55ConditionBuy) { //--- If the CCI55 condition is not met... Print("Buy signal rejected: CCI55 condition not met. CCI55=", cci55_current); //--- Notify the user. conditionsOk = false; //--- Mark the conditions as not met. }
在这里,我们评估进入多头交易的条件。“maCrossoverBuy” 变量检查快速移动平均线(“ma11”)是否已向上穿越慢速移动平均线(“ma25”),这表明一个潜在的买入信号。“rsiConditionBuy” 确保 RSI 值高于定义的 “InpRSIThreshold”,以确认强劲的看涨动量。“cci36ConditionBuy” 和 “cci55ConditionBuy” 检查两个 CCI 指标是否均为正值,这表明市场处于有利的趋势中。如果 “maCrossoverBuy” 条件为真,我们继续验证其余的条件。如果任何一个条件失败,我们会打印一条消息,说明买入信号被拒绝的原因,并将 “conditionsOk” 标志设置为 false,以阻止进一步的交易执行。这种全面的检查确保只有具有强劲看涨确认的交易才会被执行。如果我们检测到这样一个信号,就可以继续开仓。
if (conditionsOk) { //--- If all Buy conditions are met... //--- Get stop loss from previous swing low. double stopLoss = GetPivotLow(PivotLeft, PivotRight); //--- Use pivot low as the stop loss. if (stopLoss <= 0) { //--- If no valid pivot low is found... stopLoss = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Fallback to current bid price. } double entryPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Determine entry price as current ask price. double tp = entryPrice + InpTakeProfitPoints * _Point; //--- Calculate take profit based on fixed points. //--- Print the swing point (pivot low) used as stop loss. Print("Buy signal: Swing Low used as Stop Loss = ", stopLoss); //--- Notify the user of the pivot low used. if (obj_Trade.Buy(InpLotSize, NULL, entryPrice, stopLoss, tp, "Buy Order")) { //--- Attempt to open a Buy order. Print("Buy order opened at ", entryPrice); //--- Notify the user if the order is opened successfully. } else { Print("Buy order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Notify the user if the order fails. } return; //--- Exit after processing a valid Buy signal. } else { Print("Buy signal not executed due to failed condition(s)."); //--- Notify the user if Buy conditions failed. }
在确认所有买入条件都满足后,我们继续确定该交易的止损。我们使用 “GetPivotLow” 函数来找到前一个波段低点,并将其设置为止损。如果未找到有效的波段低点(即止损小于或等于 0),则使用当前的价格(bid)作为后备方案。入场价格通过 SymbolInfoDouble 函数并使用 SYMBOL_ASK 参数从当前要价获取。我们通过将指定的 “InpTakeProfitPoints” 加到入场价格上来计算止盈(“tp”),并根据市场的点值(_Point)进行调整。
一旦确定了入场价格、止损和止盈,我们便尝试使用 “obj_Trade.Buy” 方法下达买入订单。如果买入订单成功开仓,我们会使用 Print 函数输出一条确认消息。如果订单失败,我们会使用 “obj_Trade.ResultRetcodeDescription” 提供失败消息和错误描述。如果买入条件不满足,则会打印一条表示买入信号被拒绝的消息,并且不会开仓。我们使用了一个 “GetPivotLow” 自定义函数,其实现如下。
//+------------------------------------------------------------------+ //| Function to find the most recent swing low (pivot low) | //+------------------------------------------------------------------+ double GetPivotLow(int left, int right) { MqlRates rates[]; //--- Declare an array to store rate data. int copied = CopyRates(_Symbol, _Period, 0, 100, rates); //--- Copy the last 100 bars into the rates array. if (copied <= (left + right)) { //--- Check if sufficient data was copied. return (0); //--- Return 0 if there are not enough bars. } //--- Loop through the bars to find a pivot low. for (int i = left; i <= copied - right - 1; i++) { bool isPivot = true; //--- Assume the current bar is a pivot low. double currentLow = rates[i].low; //--- Get the low value of the current bar. for (int j = i - left; j <= i + right; j++) { //--- Loop through neighboring bars. if (j == i) { //--- Skip the current bar. continue; } if (rates[j].low <= currentLow) { //--- If any neighbor's low is lower or equal, isPivot = false; //--- then the current bar is not a pivot low. break; } } if (isPivot) { //--- If a pivot low is confirmed, return (currentLow); //--- return the low value of the pivot. } } return (0); //--- Return 0 if no pivot low is found. }
在该函数中,我们旨在通过使用 CopyRates 函数扫描最近 100 根 K 线的市场数据,来识别最近的摆动(波段)低点。MqlRates 结构体的数组 “rates” 存储了价格数据,我们通过检查复制的数据量是否大于或等于 “left” 和 “right” 参数之和,来确保有足够的 K 线进行计算。然后,函数遍历这些 K 线,通过将当前 K 线的低点值与指定 “left” 和 “right” 范围内的相邻 K 线进行比较,来检查是否存在波段低点。如果任何相邻 K 线的低点低于或等于当前 K 线的低点,则当前 K 线不是波段低点。如果找到一个波段低点,则返回其低点值。如果在检查完所有 K 线后仍未找到波段低点,则函数返回 0。
要获取波段高点,我们使用类似的方法,只是逻辑相反。
//+------------------------------------------------------------------+ //| Function to find the most recent swing high (pivot high) | //+------------------------------------------------------------------+ double GetPivotHigh(int left, int right) { MqlRates rates[]; //--- Declare an array to store rate data. int copied = CopyRates(_Symbol, _Period, 0, 100, rates); //--- Copy the last 100 bars into the rates array. if (copied <= (left + right)) { //--- Check if sufficient data was copied. return (0); //--- Return 0 if there are not enough bars. } //--- Loop through the bars to find a pivot high. for (int i = left; i <= copied - right - 1; i++) { bool isPivot = true; //--- Assume the current bar is a pivot high. double currentHigh = rates[i].high; //--- Get the high value of the current bar. for (int j = i - left; j <= i + right; j++) { //--- Loop through neighboring bars. if (j == i) { //--- Skip the current bar. continue; } if (rates[j].high >= currentHigh) { //--- If any neighbor's high is higher or equal, isPivot = false; //--- then the current bar is not a pivot high. break; } } if (isPivot) { //--- If a pivot high is confirmed, return (currentHigh); //--- return the high value of the pivot. } } return (0); //--- Return 0 if no pivot high is found. }
有了这些函数,我们就可以使用与处理买入头寸类似的反向方法,来处理卖出交易信号。
//--- Check for Sell Conditions: bool maCrossoverSell = (ma11_previous > ma25_previous) && (ma11_current < ma25_current); //--- True if fast MA crosses below slow MA. bool rsiConditionSell = (rsi_current < (100.0 - InpRSIThreshold)); //--- True if RSI is below the Sell threshold. bool cci36ConditionSell = (cci36_current < 0); //--- True if CCI36 is negative. bool cci55ConditionSell = (cci55_current < 0); //--- True if CCI55 is negative. if (maCrossoverSell) { //--- If crossover for MA Sell is true... bool conditionsOk = true; //--- Initialize a flag to track if all conditions are met. //--- Check RSI condition for Sell. if (!rsiConditionSell) { //--- If the RSI condition is not met... Print("Sell signal rejected: RSI condition not met. RSI=", rsi_current, " Required below=", (100.0 - InpRSIThreshold)); //--- Notify the user. conditionsOk = false; //--- Mark the conditions as not met. } //--- Check CCI36 condition for Sell. if (!cci36ConditionSell) { //--- If the CCI36 condition is not met... Print("Sell signal rejected: CCI36 condition not met. CCI36=", cci36_current); //--- Notify the user. conditionsOk = false; //--- Mark the conditions as not met. } //--- Check CCI55 condition for Sell. if (!cci55ConditionSell) { //--- If the CCI55 condition is not met... Print("Sell signal rejected: CCI55 condition not met. CCI55=", cci55_current); //--- Notify the user. conditionsOk = false; //--- Mark the conditions as not met. } if (conditionsOk) { //--- If all Sell conditions are met... //--- Get stop loss from previous swing high. double stopLoss = GetPivotHigh(PivotLeft, PivotRight); //--- Use pivot high as the stop loss. if (stopLoss <= 0) { //--- If no valid pivot high is found... stopLoss = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Fallback to current ask price. } double entryPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Determine entry price as current bid price. double tp = entryPrice - InpTakeProfitPoints * _Point; //--- Calculate take profit based on fixed points. //--- Print the swing point (pivot high) used as stop loss. Print("Sell signal: Swing High used as Stop Loss = ", stopLoss); //--- Notify the user of the pivot high used. if (obj_Trade.Sell(InpLotSize, NULL, entryPrice, stopLoss, tp, "Sell Order")) { //--- Attempt to open a Sell order. Print("Sell order opened at ", entryPrice); //--- Notify the user if the order is opened successfully. } else { Print("Sell order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Notify the user if the order fails. } return; //--- Exit after processing a valid Sell signal. } else { Print("Sell signal not executed due to failed condition(s)."); //--- Notify the user if Sell conditions failed. } }
在这里,我们通过反转用于买入信号的逻辑来检查卖出信号的条件。我们首先检查快速移动平均线是否向下穿越慢速移动平均线,这由 “maCrossoverSell” 条件捕获。接下来,我们验证 RSI 是否低于卖出阈值,并检查 CCI36 和 CCI55 的值是否均为负数。
如果所有条件都满足,我们使用 “GetPivotHigh” 函数计算止损,以找到最近的波段高点,并根据固定的点数距离确定止盈。然后,我们尝试使用 “obj_Trade.Sell” 方法下达卖出订单。如果订单成功,我们打印一条确认消息;如果不成功,则显示一条错误消息。如果任何一个条件失败,我们会通知用户卖出信号已被拒绝。在编译并运行程序后,我们得到以下结果。

从图中我们可以看到,程序识别并验证了所有的入场条件,如果验证通过,就会使用相应的入场参数开仓,从而实现了我们的目标。剩下的事情就是对程序进行回测,这将在下一节中处理。
回测
在对程序进行密集回测时,我们确实注意到,在寻找波段顶点时,我们首先使用最旧的数据进行比较,这有时(尽管很少)会导致无效的波段顶点,从而造成止损设置无效。

为了缓解这个问题,我们采用了一种方法,即使用 ArraySetAsSeries 函数将搜索数据设置为时间序列,这样我们就在存储数组的第一个位置拥有最新的数据,因此我们首先使用最新的数据进行分析,如下所示。
//+------------------------------------------------------------------+ //| Function to find the most recent swing low (pivot low) | //+------------------------------------------------------------------+ double GetPivotLow(int left, int right) { MqlRates rates[]; //--- Declare an array to store rate data. ArraySetAsSeries(rates, true); //--- }
为了证实我们进行了进一步测试,得到如下结果。

从图中我们可以看到,我们正确地获取了实际的近期波段顶点,从而消除了“无效止损”错误。因此,我们不会再被交易拒之门外。经过从 2023 年到 2024 年的彻底测试,我们得到了以下结果。
回测结果图形:

回测报告:

结论
总之,我们成功开发了一款 MQL5 EA,旨在自动化一个全面的趋势盘整动量交易策略,该策略结合了多个趋势和动量指标,用于生成买入和卖出信号。通过纳入指标交叉和阈值检查等关键条件,我们创建了一个能够对市场趋势做出反应的动态系统,具备精确的入场和出场点。
免责声明:本文仅用于教学目的。交易涉及重大的财务风险,且市场状况可能迅速变化。尽管所提供的策略为交易提供了一种结构化的方法,但它并不保证盈利。在实时环境中应用此系统之前,彻底的回测和健全的风险管理至关重要。
通过实现这些概念,您可以提升您的算法交易技能,并完善您的技术分析方法。祝您在持续开发和改进交易策略的道路上好运!