
MQL5自动化交易策略(第二部分):基于一目均衡表与动量震荡器的云突破交易系统
概述
在前文(本系列的第一部分)中,我们演示了如何将威廉姆斯的“交易混沌”理论实现自动化。而在本文(第二部分)中,我们将展示如何将云图突破策略转化为MetaQuotes语言5(MQL5)的完整功能EA。云图突破策略基于 一目均衡表指标 ,通过分析价格相对于云图(由先行带A和先行带B构成的动态支撑阻力区)的走势,识别潜在的趋势反转与延续信号。通过引入动量震荡指标作为趋势确认工具,该策略可有效过滤虚假信号,提升交易进出点的准确性。此策略深受希望捕捉强趋势驱动市场波动的交易者青睐。
本文将逐步解析策略逻辑编码、交易管理,以及通过追踪止损强化风险控制的方法。读完本文后,您将清晰掌握:如何自动化实现该策略、如何使用MQL5策略测试工具验证其表现和如果优化参数以获得最佳效果。为便于理解,我们将内容分为以下章节。
- 云图突破策略概述
- MQL5中云图突破策略的实现
- 策略测试与优化
- 结论
云图突破策略概述
云图突破策略是一种趋势跟踪方法,旨在捕捉价格突破云图边界后的趋势性行情。云图,又称“云层”,是一目均衡表指标中由先行带A与先行带B构成的阴影区域,充当动态支撑与阻力位。当价格上破云图时,预示着潜在的看涨趋势;当价格下破云图时,预示着潜在的看跌趋势。该指标的参数设置如下:Tenkan-sen(转换线)= 8;Kijun-sen(基准线)= 29;Senkou-span B(先行带B)= 34。参数配置如下:
为过滤虚假信号,该策略还整合了动量震荡指标 ,为交易入场提供了附加确认。动量震荡指标通过计算34周期与5周期简单移动平均线(基于中价)的差值,识别动量方向变化。当震荡指标由负值上穿零轴时,确认买入信号;当震荡指标由正值下穿零轴时,确认卖出信号。通过将云图突破与动量震荡指标的动量确认相结合,该策略旨在减少虚假信号,提高交易成功率。
当两者完全结合时,图表呈现如下效果。
平仓逻辑基于动量方向变化。当震荡指标由正值下穿零轴时,表明看涨动量发生变化,我们将平掉所有多头头寸。同理,当震荡指标由负值上穿零轴时,表明看跌动量发生变化,我们将平掉所有空头头寸。因此,我们可以通过扩大时间范围或增加事件搜索范围(在此情况下,可将其扩展为一天)来增加搜索力度。图示如下:
该策略在趋势明显的市场中表现尤为突出(此时动量强劲)。但在价格盘整阶段可能因云图与震荡指标内价格波动杂乱无章,从而产生虚假信号。为此,可通过叠加额外的过滤条件或采用动态追踪止损等风险管理手段,降低潜在回撤风险。深入理解这些核心原则,是成功将该策略转化为自动化EA的关键前提。
MQL5中云图突破策略的实现
在全面掌握云图突破交易策略的理论体系后,我们将基MQL5为MetaTrader 5平台开发自动化EA。
要创建一个EA,在您的MetaTrader 5终端上,点击工具(Tools)选项卡并检查MetaQuotes语言编辑器,或者直接按键盘上的F4键。另外,您也可以点击工具栏上的IDE(集成开发环境)图标。这样就会打开MetaQuotes语言编辑器环境,该环境允许用户编写自动交易、技术指标、脚本和函数库。打开MetaEditor后,在工具栏上,点击“文件”选项卡,然后勾选“新建文件”,或者直接按CTRL + N键,以创建一个新文档。或者,您也可以点击工具栏选项卡上的“新建”图标。这将弹出一个MQL向导窗口。
在弹出的向导中,选中EA(模板),然后单击下一步。在EA的一般属性中,在名称部分,提供您的文件名称。请注意,如果要指定或创建一个不存在的文件夹,您需要在EA名称前使用反斜杠。例如,这里我们默认“Experts\”。这意味着我们的EA将被创建在Experts文件夹中,我们可以去那里找。其他部分相对直观,但您可以按照向导底部的链接了解如何精准地执行该过程。
在输入您希望的EA文件名后,依次点击“下一步”、再“下一步”,然后点击“完成”。在完成上述所有操作后,我们现在可以开始编写和实现我们的策略了。
首先,我们从定义一些关于EA的基础数据开始。包括EA的名称、版权信息以及指向MetaQuotes网站的链接。我们还指定了EA的版本号,设置为“1.00”。
//+------------------------------------------------------------------+ //| 1. Kumo Breakout EA.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00"
当加载程序时,系统将自动显示元数据信息。随后,我们可在程序中定义并使用全局变量。首先,我们在源代码的开头使用#include指令来引入一个交易实例。获得了CTrade类的访问权限后,我们将使用该类来创建一个交易对象。这一步至关重要,因为我们需要使用它开立交易。
#include <Trade/Trade.mqh>
CTrade obj_Trade;
预处理器会将#include<Trade/Trade.mqh>这一行替换为Trade.mqh文件的内容。尖括号表示Trade.mqh文件将从标准目录中获取(通常路径为terminal_installation_directory\MQL5\Include)。当前目录不会被包含在搜索范围内。这条指令可以放在程序的任何地方,但通常所有的包含指令都放在源代码的开头,这样可以使代码结构更清晰,也便于引用。声明CTrade类的对象obj_Trade,可以让我们轻松地访问该类中包含的方法,这要归功于MQL5开发者的努力。
完成上述步骤后,我们需在交易系统中声明多个关键指标句柄,以供后续调用。
int handle_Kumo = INVALID_HANDLE; //--- Initialize the Kumo indicator handle to an invalid state int handle_AO = INVALID_HANDLE; //--- Initialize the Awesome Oscillator handle to an invalid state
在此,我们声明两个整型变量——"handle_Kumo"和"handle_AO",分别用于存储云图指标和动量震荡(AO)指标的句柄。我们将这两个变量初始化为INVALID_HANDLE(MQL5中预定义的常量,表示无效或未初始化的句柄)。这一操作至关重要,因为当我们创建指标时,系统会返回一个句柄,用于后续与该指标交互。若句柄值为"INVALID_HANDLE",则表明指标创建失败或未正确初始化。通过将句柄初始化为INVALID_HANDLE,我们可以在后续代码中检测初始化问题,并妥善处理错误。
接下来,我们需要初始化数组,用于存储从指标中获取的数据值。
double senkouSpan_A[]; //--- Array to store Senkou Span A values double senkouSpan_B[]; //--- Array to store Senkou Span B values double awesome_Oscillator[]; //--- Array to store Awesome Oscillator values
在全局作用域中,我们声明三个数组:"senkouSpan_A"、"senkouSpan_B"和"awesome_Oscillator",分别用于存储先行带A、先行带B以及动量震荡指标的值。我们将这些数组定义为double类型,即双精度浮点数类型,以适配指标计算结果的存储需求(指标值通常包含小数部分)。其中,"senkouSpan_A"和"senkouSpan_B"数组用于存储一目均衡指标中先行带A和B的数值。而awesome_Oscillator数组则存储动量震荡指标的计算值。通过声明这些数组,我们为后续存储指标数据做好准备,以便在交易逻辑中调用和使用这些值。
以上就是我们在全局作用域中所需声明的全部变量。接下来,我们可以在OnInit事件处理函数中初始化指标句柄,因为OnInit是负责处理初始化流程的专用函数。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- return(INIT_SUCCEEDED); //--- Return successful initialization }
这是一个事件处理函数,无论指标因何种原因被初始化,该函数都会被调用。我们会使用该函数对指标句柄进行初始化。首先从云图句柄开始初始化。
//--- Initialize the Ichimoku Kumo indicator handle_Kumo = iIchimoku(_Symbol,_Period,8,29,34); if (handle_Kumo == INVALID_HANDLE){ //--- Check if Kumo indicator initialization failed Print("ERROR: UNABLE TO INITIALIZE THE KUMO INDICATOR HANDLE. REVERTING NOW!"); //--- Log error return (INIT_FAILED); //--- Return initialization failure }
在此,我们通过调用iIchimoku函数初始化"handle_Kumo",该函数会为当前交易品种(_Symbol)和时间周期(_Period)创建一目均衡图指标的实例。我们使用以下特定参数配置一目均衡图指标,分别是:转换线8周期,基准线29周期,下降云34周期。
调用iIchimoku函数后,该函数会返回一个指标句柄,我们将其存储在变量"handle_Kumo"中。随后,我们检查"handle_Kumo"是否等于INVALID_HANDLE(无效句柄值),若相等则表明指标初始化失败。若句柄无效,我们使用"Print"函数记录错误信息,明确说明失败原因,并返回INIT_FAILED常量,表示初始化过程未成功。类似地,我们以相同方式初始化振荡器指标。
//--- Initialize the Awesome Oscillator handle_AO = iAO(_Symbol,_Period); if (handle_AO == INVALID_HANDLE){ //--- Check if AO indicator initialization failed Print("ERROR: UNABLE TO INITIALIZE THE AO INDICATOR HANDLE. REVERTING NOW!"); //--- Log error return (INIT_FAILED); //--- Return initialization failure }
要初始化振荡器指标,我们调用iAO函数,并仅传入交易品种和时间周期作为默认参数。随后,我们沿用与云图句柄相同的逻辑格式,完成剩余的初始化流程。初始化完成后,即可将存储数组设置为时间序列,以便后续按时间顺序访问数据。
ArraySetAsSeries(senkouSpan_A,true); //--- Set Senkou Span A array as a time series ArraySetAsSeries(senkouSpan_B,true); //--- Set Senkou Span B array as a time series ArraySetAsSeries(awesome_Oscillator,true); //--- Set Awesome Oscillator array as a time series
我们使用ArraySetAsSeries函数将数组"senkouSpan_A"、"senkouSpan_B"和"awesome_Oscillator"设置为时间序列数组。通过将数组设置为时间序列,可确保最新值存储在数组开头(索引0位置),而较旧的值依次向后移动。这一特性在MQL5中至关重要,因为时间序列数据的默认组织方式是最新数据优先访问(索引0对应最新值),从而更便捷地获取实时数据以支持交易决策。
我们对每个数组调用ArraySetAsSeries,并传入true作为第二个参数以启用时间序列模式。这使得数据的操作方式与典型交易策略需求一致——优先访问最新数据,避免手动计算偏移量。最后,当所有初始化步骤均成功完成时,我们在日志上打印一条消息,表明系统准备就绪。
Print("SUCCESS. ",__FILE__," HAS BEEN INITIALIZED."); //--- Log successful initialization
在初始化成功后,我们使用Print函数记录一条日志消息,表明初始化流程已顺利完成。该消息包含字符串 "SUCCESS.",后跟特殊预定义变量 __FILE__(表示当前源代码文件名)。通过使用 __FILE__,可动态将文件名插入日志消息中,这在涉及多个文件的大型项目中有助于调试或跟踪初始化过程。打印消息至终端或日志文件,确认初始化已成功完成。此步骤可确保我们获得初始化状态的明确反馈,从而更轻松地定位代码中的潜在问题。
完整的初始化代码段如下:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- Initialize the Ichimoku Kumo indicator handle_Kumo = iIchimoku(_Symbol,_Period,8,29,34); if (handle_Kumo == INVALID_HANDLE){ //--- Check if Kumo indicator initialization failed Print("ERROR: UNABLE TO INITIALIZE THE KUMO INDICATOR HANDLE. REVERTING NOW!"); //--- Log error return (INIT_FAILED); //--- Return initialization failure } //--- Initialize the Awesome Oscillator handle_AO = iAO(_Symbol,_Period); if (handle_AO == INVALID_HANDLE){ //--- Check if AO indicator initialization failed Print("ERROR: UNABLE TO INITIALIZE THE AO INDICATOR HANDLE. REVERTING NOW!"); //--- Log error return (INIT_FAILED); //--- Return initialization failure } ArraySetAsSeries(senkouSpan_A,true); //--- Set Senkou Span A array as a time series ArraySetAsSeries(senkouSpan_B,true); //--- Set Senkou Span B array as a time series ArraySetAsSeries(awesome_Oscillator,true); //--- Set Awesome Oscillator array as a time series Print("SUCCESS. ",__FILE__," HAS BEEN INITIALIZED."); //--- Log successful initialization //--- return(INIT_SUCCEEDED); //--- Return successful initialization }
得到的输出如下。
由于我们已完成了对数据存储数组和句柄的初始化,因此在程序终止时,需及时释放这些资源,避免持续占用不必要的内存或句柄。这一清理操作通过OnDeinit事件处理函数实现——无论程序因何种原因终止,均会自动调用该函数。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- Free memory allocated for Senkou Span A and B arrays ArrayFree(senkouSpan_A); ArrayFree(senkouSpan_B); //--- Free memory allocated for the Awesome Oscillator array ArrayFree(awesome_Oscillator); }
在OnDeinit函数中,我们执行清理任务,释放程序初始化过程中分配的所有内存资源。具体而言,我们使用ArrayFree函数释放数组"senkouSpan_A"、"senkouSpan_B"和"awesome_Oscillator"所占用的内存。这些数组此前用于存储一目均衡表云图指标和动量震荡指标的计算值。由于它们已不再需要,我们主动释放内存以防止资源泄漏。通过这一操作,我们确保程序能够高效管理系统资源,并在EA停止运行后避免不必要的内存占用。
接下来仅需处理交易逻辑:我们需获取指标数值,并对其进行分析以做出交易决策。这一过程在OnTick事件处理器中完成——每当市场出现新tick(即价格变动)时,该函数会被自动触发调用。第一步是从已初始化的指标句柄中提取数据点,并将其存储至数组中,以便后续分析使用。
//--- Copy data for Senkou Span A from the Kumo indicator if (CopyBuffer(handle_Kumo,SENKOUSPANA_LINE,0,2,senkouSpan_A) < 2){ Print("ERROR: UNABLE TO COPY REQUESTED DATA FROM SENKOUSPAN A LINE. REVERTING NOW!"); //--- Log error return; //--- Exit if data copy fails } //--- Copy data for Senkou Span B from the Kumo indicator if (CopyBuffer(handle_Kumo,SENKOUSPANB_LINE,0,2,senkouSpan_B) < 2){ Print("ERROR: UNABLE TO COPY REQUESTED DATA FROM SENKOUSPAN B LINE. REVERTING NOW!"); //--- Log error return; //--- Exit if data copy fails }
这里,我们使用CopyBuffer函数将一目均衡表指标中的先行带A和先行带B的数据,分别复制到数组"senkouSpan_A"和 "senkouSpan_B"中。传递给CopyBuffer的第一个参数"handle_Kumo"是已初始化的一目均衡表指标句柄(标识符)。第二个参数用于指定要复制的数据线:"SENKOUSPANA_LINE"对应先行带A, "SENKOUSPANB_LINE"对应先行带B。第三个参数是开始复制的起始索引,设为0表示从最新数据开始。第四个参数指定要复制的数据点数量,本例中为2。最后一个参数是存储数据的目标数组,即"senkouSpan_A"或"senkouSpan_B"。
调用CopyBuffer后,我们会检查函数返回值是否小于2。若返回值小于2,则表示请求的数据未成功复制。若发生此类错误,我们使用Print函数记录错误信息,明确指出未能从对应的云图先行带复制数据,随后通过return语句退出当前函数。这种处理方式确保在数据复制失败时,程序能以平稳的方式处理错误:记录问题详情并终止函数后续执行。
我们采用相同的逻辑来获取振荡器的值。
//--- Copy data from the Awesome Oscillator if (CopyBuffer(handle_AO,0,0,3,awesome_Oscillator) < 3){ Print("ERROR: UNABLE TO COPY REQUESTED DATA FROM AWESOME OSCILLATOR. REVERTING NOW!"); //--- Log error return; //--- Exit if data copy fails }
我们使用CopyBuffer函数将动量震荡指标的数据复制到"awesome_Oscillator"数组中。第一个参数是指标句柄"handle_AO",指向已初始化的Awesome Oscillator指标。第二个参数指定要复制的数据线或缓冲区索引,此处为0(因为Awesome Oscillator仅有一个数据缓冲区)。第三个参数是起始索引,设置为0,表示从最新数据开始复制。第四个参数指定要复制的数据点数量,此处设为3,即复制最近3个值。最后一个参数是目标数组"awesome_Oscillator",用于存储复制的数据。若实际获取的数据量少于请求值(如未复制到 3 个数据点),我们会记录错误日志并直接返回,终止后续操作。
如果成功获取到所需数据,则可以继续处理。此时需定义一个逻辑,确保仅在生成完整新K线时分析数据,而非在每个报价变动(Tick)时重复分析。我们将此逻辑封装到一个独立函数中。
//+------------------------------------------------------------------+ //| IS NEW BAR FUNCTION | //+------------------------------------------------------------------+ bool isNewBar(){ static int prevBars = 0; //--- Store previous bar count int currBars = iBars(_Symbol,_Period); //--- Get current bar count for the symbol and period if (prevBars == currBars) return (false); //--- If bars haven't changed, return false prevBars = currBars; //--- Update previous bar count return (true); //--- Return true if new bar is detected }
我们定义了一个布尔型函数"isNewBar",用于检测指定交易品种和时间周期的图表上是否出现了新K线。在该函数内部,我们声明了一个静态变量 "prevBars",用于存储上一次检查时的K线数量。static关键字确保该变量在函数调用间保持其值不变。
随后,我们使用iBars函数获取指定交易品种(_Symbol)和时间周期(_Period)图表上的当前K线总数。其结果存储在变量"currBars" 中如果K线数量未发生变化(即"prevBars" 等于"currBars"),则返回false,表示未检测到新K线生成。若K线数量发生变化,则将"prevBars"更新为当前K线总数,并返回true,表明已检测到新K线。通过此函数,我们可在报价事件处理器(tick事件)中调用它,并基于返回值进一步分析。
//--- Check if a new bar has formed if (isNewBar()){ //--- Determine if the AO has crossed above or below zero bool isAO_Above = awesome_Oscillator[1] > 0 && awesome_Oscillator[2] < 0; bool isAO_Below = awesome_Oscillator[1] < 0 && awesome_Oscillator[2] > 0; //--- }
在此,我们通过调用"isNewBar"函数来检测是否生成了新K线。如果检测到新K线(即"isNewBar"返回true),则继续分析动量震荡(AO)指标的行为。
我们定义两个布尔型变量:"isAO_Above"和"isAO_Below"。当动量震荡指标的前一个值(awesome_Oscillator[1])大于0,且前前一个值(awesome_Oscillator[2])小于0时,变量"isAO_Above"被设为true。此条件用于判断AO是否上穿零轴,暗示潜在的看涨信号。同理,若前一个AO值(awesome_Oscillator[1])小于0,且前前一个值(awesome_Oscillator[2])大于0,则"isAO_Below"被设为 true,表明AO下穿零轴,可能预示看跌信号。后续可通过相同方法扩展其他逻辑条件。
//--- Determine if the Kumo is bullish or bearish bool isKumo_Above = senkouSpan_A[1] > senkouSpan_B[1]; bool isKumo_Below = senkouSpan_A[1] < senkouSpan_B[1]; //--- Determine buy and sell signals based on conditions bool isBuy_Signal = isAO_Above && isKumo_Below && getClosePrice(1) > senkouSpan_A[1] && getClosePrice(1) > senkouSpan_B[1]; bool isSell_Signal = isAO_Below && isKumo_Above && getClosePrice(1) < senkouSpan_A[1] && getClosePrice(1) < senkouSpan_B[1];
在此,我们确定看涨或看跌的云图(即一目均衡表)形态条件。首先,定义两个布尔型变量:"isKumo_Above"和"isKumo_Below"。当上一根K线的先行带A(senkouSpan_A[1])的值大于上一根K线的先行带B(senkouSpan_B[1])的值时,变量"isKumo_Above"被设为 true,表明云图呈现看涨形态(市场情绪偏多)。反之,若先行带A的值小于先行带B的值,则"isKumo_Below"被设为 true,表明云图呈现看跌形态(市场情绪偏空)。
接下来,定义潜在的买入和卖出信号条件。买入信号("isBuy_Signal")在以下条件都满足时设置为 true:动量震荡指标上穿零轴(isAO_Above为true);云图处于看跌形态(isKumo_Below为true);上一根K线的收盘价同时高于先行带A和先行带B。这暗示尽管云图看跌,但价格可能向上突破。卖出信号("isSell_Signal")在以下条件满足时设为true:动量震荡指标下穿零轴(isAO_Below为true);云图处于看涨形态(isKumo_Above为true);上一根K线的收盘价同时低于先行带A和先行带B。这暗示尽管云图看涨,但价格可能向下突破。
您可能注意到,我们使用了新函数来获取收盘价数据。以下是所有所需函数的完整逻辑说明。
//+------------------------------------------------------------------+ //| FUNCTION TO GET CLOSE PRICES | //+------------------------------------------------------------------+ double getClosePrice(int bar_index){ return (iClose(_Symbol, _Period, bar_index)); //--- Retrieve the close price of the specified bar } //+------------------------------------------------------------------+ //| FUNCTION TO GET ASK PRICES | //+------------------------------------------------------------------+ double getAsk(){ return (NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits)); //--- Get and normalize the Ask price } //+------------------------------------------------------------------+ //| FUNCTION TO GET BID PRICES | //+------------------------------------------------------------------+ double getBid(){ return (NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits)); //--- Get and normalize the Bid price }
在此,我们定义了三个函数,用于获取不同类型的价格数据:
- "getClosePrice"函数:该函数用于获取指定K线的收盘价。它接受一个参数"bar_index",该参数表示需要获取收盘价的K线索引。函数通过调用内置函数iClose ,传入交易品种(_Symbol)、时间周期(_Period)以及K线索引,以获取目标K线的收盘价。最终,获取到的价格返回双精度类型。
- "getAsk" 函数:该函数用于获取指定交易品种的当前卖出价。它通过调用内置函数SymbolInfoDouble并传入SYMBOL_ASK常量,来获取目标交易品种的实时卖出价。随后,使用NormalizeDouble函数对结果进行标准化处理,确保价格根据交易品种的_Digits属性(小数位数)四舍五入到正确的精度。最终,返回标准化后的卖出价,类型为双精度型。
- "getBid"函数:该函数用于获取指定交易品种的当前买入价。与"getAsk" 函数类似,它通过调用SymbolInfoDouble函数并传入SYMBOL_BID常量来获取目标交易品种的实时买入价,随后使用NormalizeDouble函数对结果进行标准化处理,确保价格根据交易品种的 _Digits 属性(小数位数)四舍五入到正确的精度。最终,返回标准化后的买入价,类型为双精度型。
这两个函数为交易程序提供了便捷且标准化的价格获取方式,确保价格精度与交易品种一致。随后,我们可以利用计算得出的交易信号,针对现有信号开立相应的持仓头寸。
if (isBuy_Signal){ //--- If buy signal is generated Print("BUY SIGNAL GENERATED @ ",iTime(_Symbol,_Period,1),", PRICE: ",getAsk()); //--- Log buy signal obj_Trade.Buy(0.01,_Symbol,getAsk()); //--- Execute a buy trade } else if (isSell_Signal){ //--- If sell signal is generated Print("SELL SIGNAL GENERATED @ ",iTime(_Symbol,_Period,1),", PRICE: ",getBid()); //--- Log sell signal obj_Trade.Sell(0.01,_Symbol,getBid()); //--- Execute a sell trade }
我们检查是否已生成买入或卖出信号,并执行相应的交易操作。如果"isBuy_Signal"为 true(表示已触发买入信号),我们首先使用Print函数记录该事件。记录内容包含上一根K线的时间戳(通过iTime函数获取)以及当前卖出价(通过 "getAsk"函数获取)。此日志可追溯买入信号的触发时间及对应价格。记录完成后,我们通过调用"obj_Trade.Buy(0.01, _Symbol, getAsk())"执行买入交易,即以当前卖出价下单0.01手。
类似地,如果"isSell_Signal"为 true(表示已触发卖出信号),我们同样使用Print函数记录事件,日志中包含上一根K线的时间戳及当前买入价(通过"getBid"函数获取)。记录完成后,通过调用"obj_Trade.Sell(0.01, _Symbol, getBid())"执行卖出交易,即以当前买入价下单0.01手。此流程确保在满足买入或卖出信号条件时立即执行交易,并清晰记录所有的操作痕迹。
最后,我们仅需监测动量(趋势强度)变化,并将对应的持仓平仓。逻辑说明如下:
if (isAO_Above || isAO_Below){ //--- If AO crossover occurs if (PositionsTotal() > 0){ //--- If there are open positions for (int i=PositionsTotal()-1; i>=0; i--){ //--- Loop through open positions ulong posTicket = PositionGetTicket(i); //--- Get the position ticket if (posTicket > 0){ //--- If ticket is valid if (PositionSelectByTicket(posTicket)){ //--- Select position by ticket ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- Get position type if (posType == POSITION_TYPE_BUY){ //--- If position is a buy if (isAO_Below){ //--- If AO indicates bearish crossover Print("CLOSING THE BUY POSITION WITH #",posTicket); //--- Log position closure obj_Trade.PositionClose(posTicket); //--- Close the buy position } } else if (posType == POSITION_TYPE_SELL){ //--- If position is a sell if (isAO_Above){ //--- If AO indicates bullish crossover Print("CLOSING THE SELL POSITION WITH #",posTicket); //--- Log position closure obj_Trade.PositionClose(posTicket); //--- Close the sell position } } } } } } }
在此模块中,我们检测动量震荡指标的零轴穿越信号(上穿或下穿),并据此管理现有持仓。如果"isAO_Above"或"isAO_Below"其中一个为 true(表明已触发AO穿越信号),则调用PositionsTotal函数来检查当前是否存在未平仓头寸。如果存在持仓(即"PositionsTotal"返回值大于0),则从最新持仓开始逆向遍历所有持仓(索引从PositionsTotal()-1递减至0)。
在循环中,我们通过PositionGetTicket函数获取持仓编号。若持仓编号有效(即返回值大 0),则调用PositionSelectByTicket函数选中该持仓。随后,调用PositionGetInteger函数确定持仓类型。如果持仓为多头(POSITION_TYPE_BUY),则检查变量"isAO_Below"是否为true(即是否触发看跌穿越信号)。如果条件成立,使用Print函数记录多头持仓的平仓操作,并通过"obj_Trade.PositionClose(posTicket)"执行平仓。
同理,如果持仓为空头(POSITION_TYPE_SELL),则检查变量 "isAO_Above"是否为 true(即是否触发看涨穿越信号)。如果条件成立,记录空头持仓的平仓操作日志,并通过"obj_Trade.PositionClose(posTicket)"执行平仓。这样可以确保我们根据AO穿越信号有效管理持仓——当市场动量发生转变时,能够及时平仓。程序运行后,输出如下:
卖出头寸确认。
市场动量转向时的空头持仓平仓确认:
从上述示例中可以看出,我们已经成功达成了预期目标。接下来,我们进入程序测试与优化阶段。相关内容将在下一节展开说明。
策略测试与优化
本节将对该策略进行测试与优化,使其更适应不同市场环境。优化重点在于风险管理模块:当持仓已实现盈利时,我们将引入跟踪止损机制,替代被动等待市场动量完全反转的原有逻辑,从而主动锁定利润。为实现这一功能,我们将构建一个动态函数,专门处理跟踪止损的触发逻辑。
//+------------------------------------------------------------------+ //| FUNCTION TO APPLY TRAILING STOP | //+------------------------------------------------------------------+ void applyTrailingSTOP(double slPoints, CTrade &trade_object,int magicNo=0){ double buySL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-slPoints,_Digits); //--- Calculate SL for buy positions double sellSL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+slPoints,_Digits); //--- Calculate SL for sell positions for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Iterate through all open positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket > 0){ //--- If ticket is valid if (PositionGetString(POSITION_SYMBOL) == _Symbol && (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check symbol and magic number if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && buySL > PositionGetDouble(POSITION_PRICE_OPEN) && (buySL > PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for buy position if conditions are met trade_object.PositionModify(ticket,buySL,PositionGetDouble(POSITION_TP)); } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && sellSL < PositionGetDouble(POSITION_PRICE_OPEN) && (sellSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for sell position if conditions are met trade_object.PositionModify(ticket,sellSL,PositionGetDouble(POSITION_TP)); } } } } }
在此,我们实现一个函数,用于为持仓订单添加跟踪止损。该函数命名为"applyTrailingSTOP",接受三个参数:"slPoints"表示止损点数(Stop-Loss Points);"trade_object"用于修改订单的交易对象引用;"magicNo"(可选)通过magic编号标识特定订单。首先,我们分别计算多头(买入)和空头(卖出)持仓的止损价位。对于多头持仓,止损价 = 当前买入价 — 指定的"slPoints";对于空头持仓,止损价 = 当前卖出价 + 指定的"slPoints"。所有止损价位均通过NormalizeDouble函数进行标准化处理,以匹配交易品种的精度要求(由变量_Digits定义)。
接下来,我们通过PositionsTotal函数遍历所有持仓订单,按从最新到最旧的顺序处理。对于每个持仓订单,使用PositionGetTicket函数获取订单编号,并验证其有效性。之后,检查订单的交易品种是否与当前品种(_Symbol)匹配,并且检查订单的magic编号是否与传入的"magicNo" 参数一致(如果magic编号为0,则处理所有订单)。
如果持仓为多头头寸(POSITION_TYPE_BUY),则执行以下检查:计算的多头止损价("buySL")是否高于开仓价(POSITION_PRICE_OPEN);新止损价大于当前止损价(POSITION_SL),或当前未设置止损("POSITION_SL" = 0)。如果满足上述条件,我们调用"trade_object.PositionModify(ticket, buySL, PositionGetDouble(POSITION_TP))"更新持仓的止损价,同时保持止盈价("POSITION_TP")不变。
若持仓为空头头寸(POSITION_TYPE_SELL),则采用类似逻辑:检查计算出的空头止损价("sellSL")是否低于开仓价(POSITION_PRICE_OPEN),且低于当前止损价(POSITION_SL)或当前未设置止损。如果满足上述条件,则调用"trade_object.PositionModify(ticket, sellSL, PositionGetDouble(POSITION_TP))"更新止损。
函数定义完成后,只需在OnTick函数中调用即可。传入相应参数即可执行,实现方式如下:
if (PositionsTotal() > 0){ //--- If there are open positions applyTrailingSTOP(3000*_Point,obj_Trade,0); //--- Apply a trailing stop }
若当前存在任何持仓订单,则调用"applyTrailingSTOP"函数为这些订单应用追踪止损策略。该函数需传入以下三个参数:
- 追踪止损点数:止损距离计算公式为"3000 * _Point",其中_Point表示当前交易品种的最小价格变动单位。这意味着止损价将动态设置为距离当前市场价格3000个点。
- 交易对象:传入"obj_Trade"(一个交易操作对象的实例),用于修改订单的止损(SL)和止盈(TP)水平。
- magic编号:第三个参数设为 0,表示函数将对所有持仓订单应用追踪止损,无论其magic编号是多少。
应用追踪止损后,系统将生成以下输出结果:
从可视化图表中可以看出,我们无需被动等待市场动能发生转变,而是通过在价格朝有利方向移动时逐步调整止损价位,主动锁定利润并最大化收益。最终策略回测结果如下:
测试图表:
结论
总体而言,本文详细阐述了如何基于云图突破系统构建一款MQL5智能交易系统(EA)。通过整合一目均衡表云图 指标与动量震荡指标(AO),我们构建了一套可检测市场动能转换及突破信号的交易框架。核心步骤包括:配置指标句柄、提取关键数值、自动化交易执行(含追踪止损与仓位管理),最终形成一套具备稳健交易逻辑的策略驱动型EA。
免责声明:本文为基于指标信号开发MQL5智能交易系统的教学指南。尽管云图突破系统是广受欢迎的交易策略,但其有效性无法保证适用于所有市场环境。交易存在金融风险,历史表现不代表未来结果。实盘交易前需进行充分测试并制定完善的风险管理策略。
通过学习本指南,您可以提升MQL5开发能力,构建更复杂的交易系统。文中展示的指标整合、信号逻辑与交易自动化等核心概念,可迁移至其他策略开发,助力您在算法交易领域持续探索与创新。祝开发顺利,交易成功!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16657
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。




说得好
非常感谢。我真的很感谢你的善意反馈。