
掌握市场动态:创建有关支撑与阻力位策略的EA
引言
在这篇文章中,我们将讨论纯粹的价格行为交易背景下的支撑位与阻力位外汇交易策略,以及基于该策略创建EA的方法。我们将探讨该策略的定义、类型、描述,以及使用MetaQuotes Language 5(MQL5)为MetaTrader 5(MT5)开发该策略的过程。我们不仅将讨论该策略背后的理论,还将探讨其相关概念、分析和识别方法,以及如何在图表上进行可视化,这将使该策略成为交易者学习、提高预测市场动向能力、做出更好决策并最终熟练掌握风险管理的有用工具。我们将通过以下主题来实现上述目标:
- 支撑位(Support)与阻力位(Resistance)的定义
- 支撑位与阻力位描述
- 支撑位与阻力位的类型
- 交易策略描述
- 交易策略设计图
- 在MetaQuotes Language 5 (MQL5)中的实现
- 策略测试器结果
- 结论
在这段旅程中,我们将广泛使用MetaQuotes Language 5(MQL5)作为我们的基础集成开发环境(IDE)编码环境,并在MetaTrader 5(MT5)交易终端上执行文件。因此,拥有上述软件将是至关重要的。那么,让我们开始吧。
支撑位(Support)与阻力位(Resistance)的定义
外汇交易中的支撑位与阻力位策略是一种基础分析工具,被许多外汇交易者用来分析和识别市场更可能暂停或反转的价格水平。从技术上讲,这些水平往往会被历史价格所拒绝,这使得这些水平随着时间的推移而变得重要,因为价格一旦触及这些水平就会暂停或反转,因此得名支撑位和阻力位。这些水平的构建通常表现为价格多次在这些关键水平附近反转,这表明存在强烈的买入或卖出兴趣。
支撑位与阻力位描述
支撑位与阻力位策略的描述主要围绕其在交易场景中的应用。支撑位通常表示价格难以突破的下限,这表明存在集中的需求,而阻力位则表示上限,表明存在集中的供给。买家通常在支撑位进入市场,价格可能会上涨,因此这是交易者考虑买入或做多的好时机。另一方面,卖家在阻力位进入市场,价格可能会下跌,从而允许交易者卖出或做空。以下是我们所描述内容的可视化展示:
尽管有两种基本方式进行交易,但个人进入市场的时机总是动态的,并取决于每个人的偏好。一些交易者喜欢当价格跌至支撑位时买入,当价格升至阻力位时卖出。相反,其他交易者则喜欢当价格突破阻力位时买入,当价格跌破支撑位时卖出。因此,人们可以选择逆势交易突破(Fade the Break)或顺势交易突破(Trade the Break)。
支撑位与阻力位的类型
支撑位和阻力位有四种类型。
- 整数位支撑和阻力位:这些价位是由价格在同一价位附近反弹而形成的,从而导致了一个水平价格通道。例如,市场的摆动低点可能具有相同的价位,如0.65432、0.65435和0.65437。通常,这些价位基本相同,倾斜角度可忽略不计,表明价格需求集中。
- 趋势线通道支撑和需求位:由上升趋势线或下降趋势线形成的摆动点创造了供求区域,价格往往会对这些区域做出反应。
- 斐波那契支撑和阻力位:交易者使用斐波那契来识别价格反转区域,这些区域往往作为支撑位和阻力位的供求区域。
- 指标支撑和阻力位:技术指标,如移动平均线,提供了价格往往会产生反应的区域,这些区域为支撑位和阻力位创造了枢纽点。
交易策略描述
正如我们所见,在外汇领域存在不同类型的支撑和阻力策略。对于本文,我们将选择并使用水平整数位类型,然后可以将相同的概念应用于其他类型并进行调整。
首先,我们将分析图表并获取支撑位和阻力位的坐标。一旦确定了各自的坐标,我们就会在图表上绘制这些价位。再次强调,每个交易者都有两种交易这些价位的选择,即逆势交易或顺势交易突破。在我们的案例中,我们将选择逆势交易突破。当支撑位被突破时,我们将开立买入头寸;当阻力位被突破时,我们将开立卖出头寸。就这么简单。
交易策略设计图
为了更容易地理解我们传达的概念,让我们通过设计图来直观展示。
- 阻力位:
- 支撑位:
在MetaQuotes Language 5 (MQL5)中的实现
在学习了关于支撑和阻力交易策略的所有理论之后,让我们将这些理论自动化,并在MetaQuotes Language 5 (MQL5)中为MetaTrader 5 (MT5)编写一个EA。
要在MetaTrader 5终端中创建EA,请点击“工具”选项卡并选择“MetaQuotes语言编辑器”,或者简单地在键盘上按F4键。另外,您还可以点击工具栏上的IDE(集成开发环境)图标。这将打开MetaQuotes语言编辑器环境,允许您编写EA、技术指标、脚本和函数库。
一旦MetaEditor被打开,在工具栏上,导航到“文件”选项卡并选择“新建文件”,或者简单地按CTRL + N,来创建一个新文档。另外,您也可以点击工具栏上的“新建”图标。这将弹出一个MQL向导(MQL Wizard)窗口。
在弹出的向导中,选择“Expert Advisor (template) ”并点击“下一步”。
在EA的一般属性中,在名称部分,输入您的EA文件的名称。请注意,如果要指定或创建一个不存在的文件夹,您需要在EA名称前使用反斜杠。例如,这里我们默认有“Experts\”。这意味着我们的EA将被创建在Experts文件夹中,我们可以在那里找到它。其他部分都很直接,但您可以按照向导底部的链接了解如何精确地进行该过程。
在提供了您想要的EA文件名后,点击“下一步”,再点击“下一步”,然后点击“完成”。 完成这些步骤后,我们现在就可以开始编写和规划我们的策略了。
首先,我们在源代码的开头使用#include包含一个交易实例。这使我们能够访问CTrade类,我们将使用该类来创建一个交易对象。这非常关键,因为我们需要它来开设交易。
#include <Trade/Trade.mqh>
CTrade obj_Trade;
预处理器会用文件Trade.mqh的内容替换#include <Trade/Trade.mqh>这一行。尖括号表示Trade.mqh文件将从标准目录(通常为:terminal_installation_directory\MQL5\Include)中获取。当前目录不会包含在搜索路径中。这行代码可以放在程序的任何位置,但通常,为了代码结构更好和引用更方便,所有的包含指令都放在源代码的开头。声明CTrade类的obj_Trade对象将使我们能够轻松访问该类中包含的方法,这得益于MQL5开发者的设计。
在全局作用域中,我们需要定义数组来存储我们的最高价和最低价数据,稍后我们将对这些数据进行操作和分析,以找出支撑位和阻力位。在找到这些价位水平后,我们还需要将它们存储在数组中,因为当然会不止一个价位水平。
double pricesHighest[], pricesLowest[]; double resistanceLevels[2], supportLevels[2];
在这里,我们声明了两个双精度数组,用于存储规定数量的数据的最高价和最低价,以及另外两个额外的数组,用于存储已识别和排序的支撑位和阻力位数据。这通常是每个价位的两个坐标。请注意,价格变量是空的,这使它们成为没有预定义大小的动态数组,意味着它们可以根据提供的数据存储任意数量的元素。相反,价位变量具有两个的固定大小,这使它们成为静态数组,意味着它们每个可以精确地存储两个元素。如果您打算使用更多的坐标,可以将其大小增加到您认为合适的特定点数。
最后,一旦我们确定了这些价格水平,就需要将它们绘制在图表上以进行可视化。因此,我们必须定义线条的名称、它们各自分配的颜色以及它们各自的前缀,以便在同一个交易账户上有多个EA时更容易进行识别和区分。这使得EA能够与其他EA兼容,因为它将有效地且独立地识别其价格水平并与之协同工作。
#define resLine "RESISTANCE LEVEL" #define colorRes clrRed #define resline_prefix "R" #define supLine "SUPPORT LEVEL" #define colorSup clrBlue #define supline_prefix "S"
我们使用#define关键字来定义一个名为“resLine”的宏,其值为“RESISTANCE LEVEL”,以便轻松地存储我们的阻力位名称。这样,在每次创建阻力位时,我们就不必重复键入名称,从而大大节省了时间并降低了提供错误名称的可能性。因此,基本上,宏在编译期间用于文本替换。
同样地,我们将阻力位的颜色定义为红色,并最终为阻力位定义前缀“R”,以便在图表上标记和识别阻力线。与阻力位类似,我们按照相同的标准定义支撑位。
一旦我们初始化了EA,就需要将我们的数据设置在时间序列中,因此我们将首先处理最新的数据,并准备我们的存储数组来保存这些数据。这是在OnInit事件处理器中完成的。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- ArraySetAsSeries(pricesHighest,true); ArraySetAsSeries(pricesLowest,true); // define the size of the arrays ArrayResize(pricesHighest,50); ArrayResize(pricesLowest,50); //--- return(INIT_SUCCEEDED); }
这里发生了两件不同的事情。首先,我们使用MQL5内置函数ArraySetAsSeries将我们的价格存储数组设置为时间序列。这个函数接受两个参数:目标数组和一个布尔标志,在这个情况下是true,以接受转换。这意味着数组将以最大索引存储最早的数据,而最新数据则存储在索引0处。此为一个示例。假设我们从2020年到2024年检索数据。我们接收到的数据格式如下:
年份 | 数据 |
---|---|
2020 | 0 |
2021 | 1 |
2022 | 2 |
2023 | 3 |
2024 | 4 |
上述格式的数据使用起来不太方便,因为它按时间顺序排列,最早的数据位于第一个索引处,这意味着它是首先被使用的。然而,在分析中首先使用最新数据更为方便,因此,我们需要将数据按逆时间顺序排列,以获得如下结果:
年份 | 数据 |
---|---|
2024 | 4 |
2023 | 3 |
2022 | 2 |
2021 | 1 |
2020 | 0 |
为了以编程方式实现上述格式,我们使用了之前说明的ArraySetAsSeries函数。其次,我们使用ArrayResize函数来定义数组的大小,并指定每个数组包含五十个元素。这只是一个我们随意选择的值,你可以选择忽略它。然而,出于正式性的考虑,我们的数组中不需要太多的数据,因为我们计划对接收到的价格数据进行排序,只保留最重要的前十个数据,而额外的空间将被保留。所以你可以理解为什么在我们的数组中设置更大的大小没有意义。
在OnDeinit事件处理器中,我们需要从计算机内存中删除之前使用的存储数据。这将有助于节省资源。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- ArrayFree(pricesHighest); ArrayFree(pricesLowest); //ArrayFree(resistanceLevels); // cannot be used for static allocated array //ArrayFree(supportLevels); // cannot be used for static allocated array ArrayRemove(resistanceLevels,0,WHOLE_ARRAY); ArrayRemove(supportLevels,0,WHOLE_ARRAY); }
我们使用ArrayFree函数来释放占用计算机内存的数据,因为当EA不再使用时,这些数据将变得无用。该函数是一个返回类型为void的函数,它仅接受一个参数或自变量,即动态数组,并释放其缓冲区,同时将零维的大小设置为0。然而,对于存储支撑位和阻力位价格的静态数组,我们不能使用ArrayFree函数。这并不意味着我们无法删除这些数据。我们调用另一个函数ArrayRemove来删除我们想要丢弃的数据。该函数是一个返回类型为布尔值的函数,它接受三个参数,用于从数组中删除指定数量的元素。我们指定目标数组变量,并提供开始删除操作的索引(在我们的情况下是0,因为我们想要删除所有内容),以及最后要删除的元素数量(在这种情况下,是整个数组,以删除所有内容)。
我们的大部分活动都将在OnTick事件处理器上执行。这将是纯粹的价格行为分析,并且我们将严重依赖这个事件处理器。因此,让我们来看一下除了该函数本身之外,它还需要哪些参数,因为它是这段代码的核心。
void OnTick(){ //--- }
如我们所见,这是一个简单但至关重要的函数,它不接受任何参数也不返回任何内容。它只是一个void函数,意味着它不需要返回任何内容。这个函数在EA中使用,当特定商品的价格报价发生新的变动(即新的tick)时执行。
既然我们已经了解到OnTick函数是在价格报价每次变化时生成的,我们需要定义一些控制逻辑,以便我们能够仅在每个柱状图(bar)上执行一次代码,而不是在每个tick上执行,从而至少避免不必要的代码运行,进而节省设备内存。在寻找支撑位和阻力位时,这将是必要的。我们不需要在每个tick上搜索这些位置,因为如果我们仍然处于同一根K线上,我们总是会得到相同的结果。以下是其逻辑:
int currBars = iBars(_Symbol,_Period); static int prevBars = currBars; if (prevBars == currBars) return; prevBars = currBars;
首先,我们声明一个整数变量“currBars”,用于存储指定交易品种和周期(或您可能听说过的“时间框架”)的图表上当前计算出的柱状图数量。这是通过使用iBars函数实现的,该函数仅接受两个参数,即品种和周期。
然后,我们声明另一个静态整数变量“prevBars”,用于存储在新柱状图生成时图表上的前一个柱状图总数,并在函数的第一次运行时将其初始化为图表上的当前柱状图数量。我们将使用它来比较当前柱状图数量与前一个柱状图数量,以确定图表上新柱状图生成的情况。
最后,我们使用条件语句来检查当前柱状图数量是否等于前一个柱状图数量。如果它们相等,则意味着没有新的柱状图形成,因此我们终止进一步的执行并返回。否则,如果当前和前一个柱状图数量不相等,则表示已形成了一个新的柱状图。在这种情况下,我们将前一个柱状图变量更新为当前柱状图,以便在下一个tick时,除非我们进入了一个新的柱状图,否则它将等于图表上的柱状图数量。
要考虑用于分析的柱状图仅是图表上可见的柱状图。这是因为我们不需要考虑最旧的数据,比如一千万个柱状图,因为这将是无用的。想象一下,您有一个可以追溯到去年的支撑位的情况。这没有意义,对吧?所以,我们现在只考虑图表上可见的柱状图,因为这是当前市场条件下最近的有效数据。为了做到这一点,我们使用以下逻辑:
int visible_bars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS);
我们声明了一个整数变量 visible_bars,并使用 ChartGetInteger 函数来获取图表上可见K线的总数。该函数返回的数据类型是长整型(long),因此我们在函数前添加 (int) 来将其类型转换为整数。当然,我们也可以将目标变量定义为长整型(long),但我们不需要分配这么多的内存字节。
为了搜索这些位置,我们需要遍历每一个K线。要实现这一点,for 循环是必不可少的。
for (int i=1; i<=visible_bars-1; i++){ ... }
循环是通过首先将循环计数器整数变量i初始化为1来完成的,这指定了循环的起点。这里的1表示我们从当前K线之前的那个K线开始,因为当前K线正在形成中,因此尚未确定。它的结果可以是任何值。接下来是循环继续运行必须满足的条件。只要i小于或等于正在考虑的K线总数减1,循环就会继续执行。最后,每次循环运行时,我们都会将循环计数器“i”增加1。简单地说,i++与i=i+1是等价的。我们也可以使用递减循环,即循环计数器使用--运算符,这样分析会从最早的K线开始到当前K线结束,但我们选择使用递增循环,以便从最新的K线开始到最早的K线结束进行分析。
在每次循环中,我们都会选择一个K线,因此我们需要获取该K线的属性。在这种情况下,我们只关心K线的开盘价、最高价、最低价、收盘价和时间属性。
double open = iOpen(_Symbol,_Period,i); double high = iHigh(_Symbol,_Period,i); double low = iLow(_Symbol,_Period,i); double close = iClose(_Symbol,_Period,i); datetime time = iTime(_Symbol,_Period,i);
在这里,我们声明了相应数据类型的变量,并将它们初始化为对应的数据。例如,使用iOpen函数来获取K线的开盘价,需要提供交易对象的名称、其周期以及目标K线的索引。
在获取到K线的数据后,我们需要将这些数据与前面的K线数据进行比较,以找到与选定K线数据相同的K线,这将表明价格在过去多次反转的位置。然而,有两个连续的价位构成支撑位或阻力位是没有意义的。这些价格位置至少应该相隔一定距离。让我们首先定义这一点。
int diff_i_j = 10;
我们定义了一个整数变量,该变量将存储当前K线与应考虑用于检查相同价位是否匹配的K线之间的K线差异个数。在本例中,设置为10。用可视化方式表达效果如下。
现在我们可以启动一个包含所需逻辑的循环。
for (int j=i+diff_i_j; j<=visible_bars-1; j++){ ... }
对于内层的for循环,我们使用了计数器整数变量j,它从当前K线开始往前数第十个K线处开始,一直遍历到倒数第二个K线。为了在我们继续之前更容易地理解这一点并获得令人满意的结果,让我们通过打印输出来可视化它。
//Print in the outer loop Print(":: BAR NO: ",i); //Print in the inner loop Print("BAR CHECK NO: ",j);
您可以看到,例如,对于索引为15的选定K线,我们从当前选定的K线开始,往前数10个K线来初始化循环。从数学上讲,这就是15+10=25。然后从第25根K线开始,循环一直执行到倒数第二根K线,在我们的例子中是第33根K线。
现在我们已经能够根据需要正确地选择K线的区间,我们也可以获取选定K线的属性了。
double open_j = iOpen(_Symbol,_Period,j); double high_j = iHigh(_Symbol,_Period,j); double low_j = iLow(_Symbol,_Period,j); double close_j = iClose(_Symbol,_Period,j); datetime time_j = iTime(_Symbol,_Period,j);
同样的逻辑也适用于外层循环中的属性检索。唯一的区别是,我们在定义变量时添加了一个额外的下划线j,以表示这些属性是用于内层for循环的,并且K线的目标索引变为了j。
既然我们现在已经有了所有所需的价格数据,我们就可以继续检查支撑位和阻力位了。
// CHECK FOR RESISTANCE double high_diff = NormalizeDouble((MathAbs(high-high_j)/_Point),0); bool is_resistance = high_diff <= 10; // CHECK FOR SUPPORT double low_diff = NormalizeDouble((MathAbs(low-low_j)/_Point),0); bool is_support = low_diff <= 10;
为了检查阻力位,我们定义了一个双精度浮点数变量high_diff,它将存储外层循环中当前选定K线的最高价和内层循环中当前选定K线的最高价之间的差异数据。我们使用MathAbs函数来确保结果是一个正数,无论哪个价格更高,它都返回输入的绝对值或模值。例如,我们可能有0.65432 - 0.05456 = -0.00024。我们的答案包含负数,但该函数会忽略负号并输出0.00024。再次,将结果除以点值(一种金融工具的最小可能价格变动),以得到差异的点数形式。以我们的示例再次说明,这将是0.00024/0.00001 = 24.0。最后,为了精确起见,我们使用NormalizeDouble函数将浮点数格式化为指定的小数位数。在这种情况下,我们有0(小数点后位数),这意味着我们的输出将是一个整数。再次以我们的示例说明,我们将得到24而没有小数点。
然后,我们检查差异是否小于或等于10(一个预定义的可接受范围),并将结果存储在布尔变量is_resistance中。相同的逻辑适用于支撑位的检查。
如果我们的水平条件满足,则我们的支撑位和阻力位将为真(true),否则它们将为假(false)。为了查看我们是否能够识别这些价位,让我们将它们打印到日志中。为了核对一切,我们打印阻力位坐标的价格,以及它们的价格差异,以确保它们满足我们的条件。
if (is_resistance){ Print("RESISTANCE AT BAR ",i," (",high,") & ",j," (",high_j,"), Pts = ",high_diff); ... }
这就是我们得到的结果。
我们可以识别出价位,但它们只是任意的价格位置。由于我们想要的是处于最高或最低点的位置,因此我们需要一些额外的控制逻辑来确保我们只考虑最重要的价格位置。为了实现这一点,我们需要复制所考虑K线的最高价和最低价,分别按升序和降序对它们进行排序,然后分别取所需数量的第一个和最后一个K线的价格。我们在for循环之前做这件事。
ArrayFree(pricesHighest); ArrayFree(pricesLowest); int copiedBarsHighs = CopyHigh(_Symbol,_Period,1,visible_bars,pricesHighest); int copiedBarsLows = CopyLow(_Symbol,_Period,1,visible_bars,pricesLowest);
在存储之前,我们先清空数组中的所有数据。然后,我们将K线的最高价复制到目标数组中。这是通过使用CopyHigh整型数据类型函数来实现的,该函数需要提供交易品种、时间段、要复制的K线的起始索引、K线的数量以及目标存储数组。函数返回的结果(即复制的K线数量)被赋值给整型变量copiedBarsHighs。对于最低价,我们也执行相同的操作。为了确保我们获取到了数据,我们使用ArrayPrint函数将数组打印到日志中。
ArrayPrint(pricesHighest); ArrayPrint(pricesLowest);
这些是我们得到的结果。
然后,我们使用ArraySort函数将数据按升序排序,并再次打印结果。
// sort the array in ascending order ArraySort(pricesHighest); ArraySort(pricesLowest); ArrayPrint(pricesHighest); ArrayPrint(pricesLowest);
这就是我们得到的。
最后,我们需要获取前十个价格(即最高的十个价格)和后十个价格(即最低的十个价格),这些数据将构成我们的极端点。
ArrayRemove(pricesHighest,10,WHOLE_ARRAY);
为了获取价格最高K线的前十个最高价格,我们使用了ArrayRemove函数,并提供了目标数组、开始移除的索引(在我们的情况下是第十个元素之后),以及要移除的元素数量(在我们的情况下是剩余的所有数据)。
然而,为了获取K线的十个最近的最低价,我们采取了类似但更复杂且不那么直接的方法。
ArrayRemove(pricesLowest,0,visible_bars-10);
我们使用的是相同的函数,但这次我们的起始索引是零,因为我们对前面的值不感兴趣,而计数则是所考虑K线的总数减去十。当我们打印数据时,我们得到以下输出。
既然我们现在有了最高价格K线的数据,我们就可以继续用这些数据来检查各个水平位,并确定有效的形态设置。我们启动了另一个for循环来执行这项操作。
for (int k=0; k<ArraySize(pricesHighest); k++){ ... }
这次,我们的计数器变量k从零开始,因为我们想要考虑数组中的所有价格。
为了找到价格匹配项,我们在for循环之外声明了布尔类型的存储变量,用于存储匹配结果的标志,并将它们初始化为false。
bool matchFound_high1 = false, matchFound_low1 = false; bool matchFound_high2 = false, matchFound_low2 = false;
如果所选的存储价格等于第一个循环中K线的最高价,我们将第一个找到的最高价的标志设置为true,并打印该情况。同样地,如果所选的存储价格等于第二个循环中K线的最高价,我们将第二个找到的最高价的标志设置为true,并打印该情况。
if (pricesHighest[k]==high){ matchFound_high1 = true; Print("> RES H1(",high,") FOUND @ ",k," (",pricesHighest[k],")"); } if (pricesHighest[k]==high_j){ matchFound_high2 = true; Print("> RES H2(",high_j,") FOUND @ ",k," (",pricesHighest[k],")"); }
如果找到了两个坐标的匹配项,但当前的阻力位等于这些价格,那么就意味着我们已经有了这些阻力位。因此,我们不需要继续创建更多的阻力位。我们通知这一情况,将stop_processing标志设置为true,并提前跳出循环。
if (matchFound_high1 && matchFound_high2){ if (resistanceLevels[0]==high || resistanceLevels[1]==high_j){ Print("CONFIRMED BUT This is the same resistance level, skip updating!"); stop_processing = true; // Set the flag to stop processing break; // stop the inner loop prematurely } ... }
stop_processing标志是在for循环的上部之外定义的,并被纳入第一个for循环的执行逻辑中,以确保我们节省资源。
bool stop_processing = false; // Flag to control outer loop //... for (int i=1; i<=visible_bars-1 && !stop_processing; i++){ ... }
否则,如果找到了两个坐标的匹配项,但它们不等于当前的价格,这意味着我们发现了另一个新的阻力位,并且我们可以将其更新为最新的数据。
else { Print(" ++++++++++ RESISTANCE LEVELS CONFIRMED @ BARS ",i, "(",high,") & ",j,"(",high_j,")"); resistanceLevels[0] = high; resistanceLevels[1] = high_j; ArrayPrint(resistanceLevels); ... }
这是对所得结果的图形化展示。
为了将阻力位可视化,让我们将它们映射到图表上。
draw_S_R_Level(resLine,high,colorRes,5); draw_S_R_Level_Point(resline_prefix,high,time,218,-1,colorRes,90); draw_S_R_Level_Point(resline_prefix,high,time_j,218,-1,colorRes,90); stop_processing = true; // Set the flag to stop processing break;
我们使用两个函数来完成这一任务。第一个函数draw_S_R_Level接收要绘制的线条的名称、价格、颜色以及线条的宽度作为参数。
void draw_S_R_Level(string levelName,double price,color clr,int width){ if (ObjectFind(0,levelName) < 0){ ObjectCreate(0,levelName,OBJ_HLINE,0,TimeCurrent(),price); ObjectSetInteger(0,levelName,OBJPROP_COLOR,clr); ObjectSetInteger(0,levelName,OBJPROP_WIDTH,width); } else { ObjectSetDouble(0,levelName,OBJPROP_PRICE,price); } ChartRedraw(0); }
该函数是void数据类型,意味着它不需要返回任何内容。接下来,我们使用条件语句结合ObjectFind函数来检查对象是否存在。如果对象不存在,ObjectFind函数会返回一个负整数。在对象不存在的情况下,我们会创建一个标识为OBJ_HLINE的对象,该对象以当前时间和指定价格为坐标,因为水平线在图表上只需要一个垂直方向上的位置来确定。然后,我们设置这条水平线的颜色和宽度。如果对象已经存在,我们只需要更新其价格为指定的新价格,并重新绘制图表以使当前更改生效。这个函数的功能仅仅是在图表上绘制一条简单的水平线。这就是我们得到的。
void draw_S_R_Level_Point(string objName,double price,datetime time, int arrowcode,int direction,color clr,double angle){ //objName = " "; StringConcatenate(objName,objName," @ \nTime: ",time,"\nPrice: ",DoubleToString(price,_Digits)); if (ObjectCreate(0,objName,OBJ_ARROW,0,time,price)) { ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrowcode); ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); ObjectSetInteger(0,objName,OBJPROP_FONTSIZE,10); if (direction > 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_TOP); if (direction < 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_BOTTOM); } string prefix = resline_prefix; string txt = "\n"+prefix+"("+DoubleToString(price,_Digits)+")"; string objNameDescription = objName + txt; if (ObjectCreate(0,objNameDescription,OBJ_TEXT,0,time,price)) { // ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "" + txt); ObjectSetInteger(0,objNameDescription,OBJPROP_COLOR,clr); ObjectSetDouble(0,objNameDescription,OBJPROP_ANGLE, angle); ObjectSetInteger(0,objNameDescription,OBJPROP_FONTSIZE,10); if (direction > 0) { ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_LEFT); ObjectSetString(0,objNameDescription,OBJPROP_TEXT, " " + txt); } if (direction < 0) { ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_BOTTOM); ObjectSetString(0,objNameDescription,OBJPROP_TEXT, " " + txt); } } ChartRedraw(0); }
自定义函数draw_S_R_Level_Point设计了七个参数,以增强其复用性。这些参数的功能如下所述:
- objName:字符串类型,代表将要创建的图形对象的名称。
- price:双精度浮点数类型,代表图形对象应该放置的价格坐标。
- time:日期时间类型,指示图形对象应该放置的时间坐标。
- arrowCode:整型,指定箭头对象的箭头代码。
- direction:整型,指示文本标签的放置方向(向上或向下)。
- clr:颜色值(例如,clrBlue、clrRed等),用于图形对象。
- angle:描述标签的旋转角度。
该函数首先会将对象的名称与时间和价格进行拼接,以便区分不同的价格水平点。这样做可以确保当我们将鼠标悬停在标签上时,会弹出一个描述框,显示该坐标点的具体唯一时间和价格。
然后,函数会检查图表上是否已存在具有指定名称的对象。如果不存在,它将继续创建对象。对象的创建是通过使用内置的“ObjectCreate”函数来实现的,该函数需要指定要绘制的对象类型,在本例中为标识为“OBJ_ARROW”的箭头对象,以及时间和价格,它们构成了对象创建点的坐标。之后,我们设置对象的属性,包括箭头代码、颜色、字体大小和锚点。对于箭头代码,MQL5已经预定义了一些Wingdings字体的字符,可以直接使用。以下是一个指定这些字符的表格:
到目前为止,我们只是在图表上绘制了指定的箭头,具体过程如下:
我们可以看到,我们已经成功使用指定的箭头代码(在这个例子中是箭头代码218)绘制了阻力点,但这些点还没有描述信息。因此,为了添加相应的描述,我们将箭头与文本进行拼接。我们创建了另一个指定为“OBJ_TEXT”的文本对象,并设置了其相应的属性。文本标签通过提供关于阻力点的额外上下文或信息,作为与阻力点相关的描述性注释,使它们对交易者和分析师来说更具信息量。我们选择将文本的值设置为指定的价格,以表示这是一个阻力点。
然后,通过将原始“objName”与描述性文本进行拼接,创建了变量“objNameDescription”。然后,通过将原始“objName”与描述性文本进行拼接,创建了变量“objNameDescription”。以下代码段用于实现这一点:
string prefix = resline_prefix; string txt = "\n"+prefix+"("+DoubleToString(price,_Digits)+")"; string objNameDescription = objName + txt; if (ObjectCreate(0,objNameDescription,OBJ_TEXT,0,time,price)) { // ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "" + txt); ObjectSetInteger(0,objNameDescription,OBJPROP_COLOR,clr); ObjectSetDouble(0,objNameDescription,OBJPROP_ANGLE, angle); ObjectSetInteger(0,objNameDescription,OBJPROP_FONTSIZE,10); if (direction > 0) { ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_LEFT); ObjectSetString(0,objNameDescription,OBJPROP_TEXT, " " + txt); } if (direction < 0) { ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_BOTTOM); ObjectSetString(0,objNameDescription,OBJPROP_TEXT, " " + txt); } }
将阻力点与它们的描述进行拼接后,我们得到这样的结果。
同时,为了绘制支撑水平,我们应用相同的逻辑但条件相反。
else if (is_support){ //Print("SUPPORT AT BAR ",i," (",low,") & ",j," (",low_j,"), Pts = ",low_diff); for (int k=0; k<ArraySize(pricesLowest); k++){ if (pricesLowest[k]==low){ matchFound_low1 = true; //Print("> SUP L1(",low,") FOUND @ ",k," (",pricesLowest[k],")"); } if (pricesLowest[k]==low_j){ matchFound_low2 = true; //Print("> SUP L2(",low_j,") FOUND @ ",k," (",pricesLowest[k],")"); } if (matchFound_low1 && matchFound_low2){ if (supportLevels[0]==low || supportLevels[1]==low_j){ Print("CONFIRMED BUT This is the same support level, skip updating!"); stop_processing = true; // Set the flag to stop processing break; // stop the inner loop prematurely } else { Print(" ++++++++++ SUPPORT LEVELS CONFIRMED @ BARS ",i, "(",low,") & ",j,"(",low_j,")"); supportLevels[0] = low; supportLevels[1] = low_j; ArrayPrint(supportLevels); draw_S_R_Level(supLine,low,colorSup,5); draw_S_R_Level_Point(supline_prefix,low,time,217,1,colorSup,-90); draw_S_R_Level_Point(supline_prefix,low,time_j,217,1,colorSup,-90); stop_processing = true; // Set the flag to stop processing break; } } } }
基于识别了各个价位水平并将其相应地映射到图表上,这些里程碑任务的完成,我们得到的最终结果如下:
接着,我们会持续监控这些水平。如果某个水平超出了可见柱状图(K线图)的邻近范围,我们就会认为该阻力水平已失效并将其删除。为了实现这一点,我们需要找到对象水平线,并在找到后检查其有效性的条件。
if (ObjectFind(0,resLine) >= 0){ double objPrice = ObjectGetDouble(0,resLine,OBJPROP_PRICE); double visibleHighs[]; ArraySetAsSeries(visibleHighs,true); CopyHigh(_Symbol,_Period,1,visible_bars,visibleHighs); //Print("Object Found & visible bars is: ",ArraySize(visibleHighs)); //ArrayPrint(visibleHighs); bool matchHighFound = false; ... }
在这里,我们检查是否找到了阻力线对象,如果找到了,我们就获取它的价格。接着,我们再次复制图表上可见K线图的最高点,并将它们存储在visibleHighs这个双精度浮点型数组变量中。
之后,我们遍历这些最高点价格,尝试找到当前选中的柱状图的价格与阻力线价格是否匹配。如果找到了匹配项,我们就将matchHighFound标志设置为true,并终止循环。
for (int i=0; i<ArraySize(visibleHighs); i++){ if (visibleHighs[i] == objPrice){ Print("> Match price for resistance found at bar # ",i+1," (",objPrice,")"); matchHighFound = true; break; } }
如果没有找到匹配项,那就意味着阻力水平已经超出了邻近范围。在这种情况下,我们会通知这一情况,并使用一个自定义函数来删除该对象。
if (!matchHighFound){ Print("(",objPrice,") > Match price for the resistance line not found. Delete!"); deleteLevel(resLine); }
自定义函数deleteLevel仅接受一个参数,即要删除的支撑或阻力线的名称,并使用ObjectDelete函数来删除指定的对象。
void deleteLevel(string levelName){ ObjectDelete(0,levelName); ChartRedraw(0); }
相同的逻辑也适用于支撑水平线,但条件相反。
if (ObjectFind(0,supLine) >= 0){ double objPrice = ObjectGetDouble(0,supLine,OBJPROP_PRICE); double visibleLows[]; ArraySetAsSeries(visibleLows,true); CopyLow(_Symbol,_Period,1,visible_bars,visibleLows); //Print("Object Found & visible bars is: ",ArraySize(visibleLows)); //ArrayPrint(visibleLows); bool matchLowFound = false; for (int i=0; i<ArraySize(visibleLows); i++){ if (visibleLows[i] == objPrice){ Print("> Match price for support found at bar # ",i+1," (",objPrice,")"); matchLowFound = true; break; } } if (!matchLowFound){ Print("(",objPrice,") > Match price for the support line not found. Delete!"); deleteLevel(supLine); } }
到目前为止,如果阻力和支撑水平仍然在图表范围内,那么它们就是有效的。因此,我们可以继续创建一个逻辑,以确定这些水平是否被突破,并据此开立市场价头寸。首先,我们来考虑阻力水平的突破。
由于价格可能会多次突破阻力水平,从而导致多次信号生成,我们需要一个逻辑来确保,一旦我们突破了一个阻力位并生成了一个信号,那么即使之后再次突破这个位置,也不会再次触发信号(如果这是同一个价位的话)。为了实现这一点,我们声明一个静态双精度浮点型变量来存储我们的信号价格。这个变量将保持其值不变,直到我们收到另一个不同的信号。
static double ResistancePriceTrade = 0;
if (ObjectFind(0,resLine) >= 0){ double ResistancePriceLevel = ObjectGetDouble(0,resLine,OBJPROP_PRICE); if (ResistancePriceTrade != ResistancePriceLevel){ ... }
然后,我们检查阻力线是否存在,如果存在,我们就获取其价格。通过条件语句,我们检查信号是否不等于阻力线的价格,这意味着我们还没有为该特定水平生成信号,并且我们可以继续检查突破信号。为了进行检查,我们还需要前一根K线的数据以及最新的价格报价。
double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); double open1 = iOpen(_Symbol,_Period,1); double high1 = iHigh(_Symbol,_Period,1); double low1 = iLow(_Symbol,_Period,1); double close1 = iClose(_Symbol,_Period,1);
接下来,我们使用条件语句来检查价格是否突破了阻力水平。如果是这样,我们通过向日志打印信息来通知出现卖出信号。然后,我们使用交易对象(trade object)和点运算符(dot operator)来访问卖出开仓方法,并提供必要的参数。最后,我们更新信号变量的值为当前的阻力位,这样我们就不会因为相同的阻力位而再次生成信号。
if (open1 > close1 && open1 < ResistancePriceLevel && high1 > ResistancePriceLevel && Bid < ResistancePriceLevel){ Print("$$$$$$$$$$$$ SELL NOW SIGNAL!"); obj_Trade.Sell(0.01,_Symbol,Bid,Bid+350*5*_Point,Bid-350*_Point); ResistancePriceTrade = ResistancePriceLevel; }
相同的逻辑也适用于支撑位突破的逻辑,但条件相反。
static double SupportPriceTrade = 0; if (ObjectFind(0,supLine) >= 0){ double SupportPriceLevel = ObjectGetDouble(0,supLine,OBJPROP_PRICE); if (SupportPriceTrade != SupportPriceLevel){ double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); double open1 = iOpen(_Symbol,_Period,1); double high1 = iHigh(_Symbol,_Period,1); double low1 = iLow(_Symbol,_Period,1); double close1 = iClose(_Symbol,_Period,1); if (open1 < close1 && open1 > SupportPriceLevel && low1 < SupportPriceLevel && Ask > SupportPriceLevel){ Print("$$$$$$$$$$$$ BUY NOW SIGNAL!"); obj_Trade.Buy(0.01,_Symbol,Ask,Ask-350*5*_Point,Ask+350*_Point); SupportPriceTrade = SupportPriceLevel; } } }
以下是我们上述工作里程碑式的展示。
以下是使用MQL5编写的一个完整代码示例,用于创建外汇交易中的支撑位和阻力位交易策略。该策略能够识别这些水平,在图表上标记它们,并相应地开立市场头寸。
//+------------------------------------------------------------------+ //| RESISTANCE AND SUPPORT.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 <Trade/Trade.mqh> CTrade obj_Trade; //bool stop_processing = false; double pricesHighest[], pricesLowest[]; double resistanceLevels[2], supportLevels[2]; #define resLine "RESISTANCE LEVEL" #define colorRes clrRed #define resline_prefix "R" #define supLine "SUPPORT LEVEL" #define colorSup clrBlue #define supline_prefix "S" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- ArraySetAsSeries(pricesHighest,true); ArraySetAsSeries(pricesLowest,true); // define the size of the arrays ArrayResize(pricesHighest,50); ArrayResize(pricesLowest,50); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- ArrayFree(pricesHighest); ArrayFree(pricesLowest); //ArrayFree(resistanceLevels); // cannot be used for static allocated array //ArrayFree(supportLevels); // cannot be used for static allocated array ArrayRemove(resistanceLevels,0,WHOLE_ARRAY); ArrayRemove(supportLevels,0,WHOLE_ARRAY); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- int currBars = iBars(_Symbol,_Period); static int prevBars = currBars; if (prevBars == currBars) return; prevBars = currBars; int visible_bars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS); bool stop_processing = false; // Flag to control outer loop bool matchFound_high1 = false, matchFound_low1 = false; bool matchFound_high2 = false, matchFound_low2 = false; ArrayFree(pricesHighest); ArrayFree(pricesLowest); int copiedBarsHighs = CopyHigh(_Symbol,_Period,1,visible_bars,pricesHighest); int copiedBarsLows = CopyLow(_Symbol,_Period,1,visible_bars,pricesLowest); //ArrayPrint(pricesHighest); //ArrayPrint(pricesLowest); // sort the array in ascending order ArraySort(pricesHighest); ArraySort(pricesLowest); //ArrayPrint(pricesHighest); //ArrayPrint(pricesLowest); ArrayRemove(pricesHighest,10,WHOLE_ARRAY); ArrayRemove(pricesLowest,0,visible_bars-10); //Print("FIRST 10 HIGHEST PRICES:"); //ArrayPrint(pricesHighest); //Print("LAST 10 LOWEST PRICES:"); //ArrayPrint(pricesLowest); for (int i=1; i<=visible_bars-1 && !stop_processing; i++){ //Print(":: BAR NO: ",i); double open = iOpen(_Symbol,_Period,i); double high = iHigh(_Symbol,_Period,i); double low = iLow(_Symbol,_Period,i); double close = iClose(_Symbol,_Period,i); datetime time = iTime(_Symbol,_Period,i); int diff_i_j = 10; for (int j=i+diff_i_j; j<=visible_bars-1; j++){ //Print("BAR CHECK NO: ",j); double open_j = iOpen(_Symbol,_Period,j); double high_j = iHigh(_Symbol,_Period,j); double low_j = iLow(_Symbol,_Period,j); double close_j = iClose(_Symbol,_Period,j); datetime time_j = iTime(_Symbol,_Period,j); // CHECK FOR RESISTANCE double high_diff = NormalizeDouble((MathAbs(high-high_j)/_Point),0); bool is_resistance = high_diff <= 10; // CHECK FOR SUPPORT double low_diff = NormalizeDouble((MathAbs(low-low_j)/_Point),0); bool is_support = low_diff <= 10; if (is_resistance){ //Print("RESISTANCE AT BAR ",i," (",high,") & ",j," (",high_j,"), Pts = ",high_diff); for (int k=0; k<ArraySize(pricesHighest); k++){ if (pricesHighest[k]==high){ matchFound_high1 = true; //Print("> RES H1(",high,") FOUND @ ",k," (",pricesHighest[k],")"); } if (pricesHighest[k]==high_j){ matchFound_high2 = true; //Print("> RES H2(",high_j,") FOUND @ ",k," (",pricesHighest[k],")"); } if (matchFound_high1 && matchFound_high2){ if (resistanceLevels[0]==high || resistanceLevels[1]==high_j){ Print("CONFIRMED BUT This is the same resistance level, skip updating!"); stop_processing = true; // Set the flag to stop processing break; // stop the inner loop prematurily } else { Print(" ++++++++++ RESISTANCE LEVELS CONFIRMED @ BARS ",i, "(",high,") & ",j,"(",high_j,")"); resistanceLevels[0] = high; resistanceLevels[1] = high_j; ArrayPrint(resistanceLevels); draw_S_R_Level(resLine,high,colorRes,5); draw_S_R_Level_Point(resline_prefix,high,time,218,-1,colorRes,90); draw_S_R_Level_Point(resline_prefix,high,time_j,218,-1,colorRes,90); stop_processing = true; // Set the flag to stop processing break; } } } } else if (is_support){ //Print("SUPPORT AT BAR ",i," (",low,") & ",j," (",low_j,"), Pts = ",low_diff); for (int k=0; k<ArraySize(pricesLowest); k++){ if (pricesLowest[k]==low){ matchFound_low1 = true; //Print("> SUP L1(",low,") FOUND @ ",k," (",pricesLowest[k],")"); } if (pricesLowest[k]==low_j){ matchFound_low2 = true; //Print("> SUP L2(",low_j,") FOUND @ ",k," (",pricesLowest[k],")"); } if (matchFound_low1 && matchFound_low2){ if (supportLevels[0]==low || supportLevels[1]==low_j){ Print("CONFIRMED BUT This is the same support level, skip updating!"); stop_processing = true; // Set the flag to stop processing break; // stop the inner loop prematurely } else { Print(" ++++++++++ SUPPORT LEVELS CONFIRMED @ BARS ",i, "(",low,") & ",j,"(",low_j,")"); supportLevels[0] = low; supportLevels[1] = low_j; ArrayPrint(supportLevels); draw_S_R_Level(supLine,low,colorSup,5); draw_S_R_Level_Point(supline_prefix,low,time,217,1,colorSup,-90); draw_S_R_Level_Point(supline_prefix,low,time_j,217,1,colorSup,-90); stop_processing = true; // Set the flag to stop processing break; } } } } if (stop_processing){break;} } if (stop_processing){break;} } if (ObjectFind(0,resLine) >= 0){ double objPrice = ObjectGetDouble(0,resLine,OBJPROP_PRICE); double visibleHighs[]; ArraySetAsSeries(visibleHighs,true); CopyHigh(_Symbol,_Period,1,visible_bars,visibleHighs); //Print("Object Found & visible bars is: ",ArraySize(visibleHighs)); //ArrayPrint(visibleHighs); bool matchHighFound = false; for (int i=0; i<ArraySize(visibleHighs); i++){ if (visibleHighs[i] == objPrice){ Print("> Match price for resistance found at bar # ",i+1," (",objPrice,")"); matchHighFound = true; break; } } if (!matchHighFound){ Print("(",objPrice,") > Match price for the resistance line not found. Delete!"); deleteLevel(resLine); } } if (ObjectFind(0,supLine) >= 0){ double objPrice = ObjectGetDouble(0,supLine,OBJPROP_PRICE); double visibleLows[]; ArraySetAsSeries(visibleLows,true); CopyLow(_Symbol,_Period,1,visible_bars,visibleLows); //Print("Object Found & visible bars is: ",ArraySize(visibleLows)); //ArrayPrint(visibleLows); bool matchLowFound = false; for (int i=0; i<ArraySize(visibleLows); i++){ if (visibleLows[i] == objPrice){ Print("> Match price for support found at bar # ",i+1," (",objPrice,")"); matchLowFound = true; break; } } if (!matchLowFound){ Print("(",objPrice,") > Match price for the support line not found. Delete!"); deleteLevel(supLine); } } static double ResistancePriceTrade = 0; if (ObjectFind(0,resLine) >= 0){ double ResistancePriceLevel = ObjectGetDouble(0,resLine,OBJPROP_PRICE); if (ResistancePriceTrade != ResistancePriceLevel){ double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); double open1 = iOpen(_Symbol,_Period,1); double high1 = iHigh(_Symbol,_Period,1); double low1 = iLow(_Symbol,_Period,1); double close1 = iClose(_Symbol,_Period,1); if (open1 > close1 && open1 < ResistancePriceLevel && high1 > ResistancePriceLevel && Bid < ResistancePriceLevel){ Print("$$$$$$$$$$$$ SELL NOW SIGNAL!"); obj_Trade.Sell(0.01,_Symbol,Bid,Bid+350*5*_Point,Bid-350*_Point); ResistancePriceTrade = ResistancePriceLevel; } } } static double SupportPriceTrade = 0; if (ObjectFind(0,supLine) >= 0){ double SupportPriceLevel = ObjectGetDouble(0,supLine,OBJPROP_PRICE); if (SupportPriceTrade != SupportPriceLevel){ double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); double open1 = iOpen(_Symbol,_Period,1); double high1 = iHigh(_Symbol,_Period,1); double low1 = iLow(_Symbol,_Period,1); double close1 = iClose(_Symbol,_Period,1); if (open1 < close1 && open1 > SupportPriceLevel && low1 < SupportPriceLevel && Ask > SupportPriceLevel){ Print("$$$$$$$$$$$$ BUY NOW SIGNAL!"); obj_Trade.Buy(0.01,_Symbol,Ask,Ask-350*5*_Point,Ask+350*_Point); SupportPriceTrade = SupportPriceLevel; } } } } //+------------------------------------------------------------------+ void draw_S_R_Level(string levelName,double price,color clr,int width){ if (ObjectFind(0,levelName) < 0){ ObjectCreate(0,levelName,OBJ_HLINE,0,TimeCurrent(),price); ObjectSetInteger(0,levelName,OBJPROP_COLOR,clr); ObjectSetInteger(0,levelName,OBJPROP_WIDTH,width); } else { ObjectSetDouble(0,levelName,OBJPROP_PRICE,price); } ChartRedraw(0); } void deleteLevel(string levelName){ ObjectDelete(0,levelName); ChartRedraw(0); } void draw_S_R_Level_Point(string objName,double price,datetime time, int arrowcode,int direction,color clr,double angle){ //objName = " "; StringConcatenate(objName,objName," @ \nTime: ",time,"\nPrice: ",DoubleToString(price,_Digits)); if (ObjectCreate(0,objName,OBJ_ARROW,0,time,price)) { ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrowcode); ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); ObjectSetInteger(0,objName,OBJPROP_FONTSIZE,10); if (direction > 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_TOP); if (direction < 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_BOTTOM); } string prefix = resline_prefix; string txt = "\n"+prefix+"("+DoubleToString(price,_Digits)+")"; string objNameDescription = objName + txt; if (ObjectCreate(0,objNameDescription,OBJ_TEXT,0,time,price)) { // ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "" + txt); ObjectSetInteger(0,objNameDescription,OBJPROP_COLOR,clr); ObjectSetDouble(0,objNameDescription,OBJPROP_ANGLE, angle); ObjectSetInteger(0,objNameDescription,OBJPROP_FONTSIZE,10); if (direction > 0) { ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_LEFT); ObjectSetString(0,objNameDescription,OBJPROP_TEXT, " " + txt); } if (direction < 0) { ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_BOTTOM); ObjectSetString(0,objNameDescription,OBJPROP_TEXT, " " + txt); } } ChartRedraw(0); }
恭喜我们!现在,我们已经基于支撑位和阻力位的外汇交易策略创建了一个纯粹的价格行为交易系统。这个系统不仅能够生成交易信号,还能根据生成的信号开立市场头寸。
策略测试器结果
在策略测试器上进行测试后,我们得到了以下结果:
- 余额/净值图:
- 回测结果:
结论
总结而言,我们已见证支撑位和阻力位外汇交易策略的自动化是可行且易行的。只需对该策略及其框架有清晰的理解,并运用这些知识实现突破即可。我们自信地利用了MQL5语言的强大功能,打造出了精确高效的交易策略。分析和EA的创建表明,自动化不仅能节省宝贵时间,还能通过减少人为错误和情感干扰来提高交易效率。
我们衷心希望本文在自动化支撑位和阻力位EA方面对您有所启发和帮助。随着金融市场的进一步发展,此类自动化系统集成的频率肯定会增加,为交易者提供处理市场动态各方面的尖端工具。随着MQL5等技术不断进步,为更复杂、更智能的交易解决方案打开大门,交易的未来看起来光明无限。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15107
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



在使用 ArraySetAsSeries 对数组进行修改时使用 ArraySort 的注意事项:
如果通过ArraySetAsSeries 对数组进行了修改,ArraySort 将按 DESCENDING 顺序对数组进行排序!!!
要获得 ASCENDING 顺序,请将数组传递给 ArrayReverse。这样就可以轻松获得前 10 个元素:
谢谢,祝您编码愉快。
要获得按顺时针顺序排列的元素,可将数组传递给ArrayReverse。这样就可以轻松获得前 10 个元素:
谢谢,祝你编码好运。
黄色突出显示--没有混淆吗?
弗拉基米尔
在使用 ArraySetAsSeries 对数组进行修改时使用 ArraySort 的注意事项:
如果通过ArraySetAsSeries 对数组进行了修改,ArraySort 将按 DESCENDING 顺序对数组进行排序!!!
要获得 ASCENDING 顺序,请将数组传递给 ArrayReverse。这样就可以轻松获得前 10 个元素:
谢谢,祝您编码愉快。
黄色突出显示 - 没有混淆?
敬上,弗拉基米尔。
我无法编辑我的原帖,所以在此回复。
最初的意图是从系列中获取价格最低的 n 个数。使用 ArraySetAsSeries 设置数组 "作为系列 "并使用 ArraySort 排序后,价格数组是降序排列的。根据 ArraySort 文档,我希望它们是升序。因此,我将排序后的数组通过 ArrayReverse 转换为升序。然后,我使用 ArrayRemove 删除前 n 项以外的所有内容。(在我的示例中,n = 10)。
还有什么问题吗?
谢谢您的反馈。
还有什么问题吗?
我现在明白了。谢谢。
问候,弗拉基米尔。
在使用 ArraySetAsSeries 对数组进行修改时使用 ArraySort 的注意事项:
如果通过ArraySetAsSeries 对数组进行了修改,ArraySort 将按 DESCENDING 顺序对数组进行排序!!!
要获得 ASCENDING 顺序,请将数组传递给 ArrayReverse。这样就可以轻松获得前 10 个元素:
如果你已经使用ArraySetAsSeries 改变了数组的逻辑方向,就没有必要再使用ArrayReverse 了--更有效、更符合逻辑的方法是再次调用ArraySetAsSeries 还原方向标志。