显示支撑/阻力位

Slobodov Gleb | 19 二月, 2016


简介

文章涉及在 MetaTrader 4 程序中探明和指示支撑/阻力位。 基于简单的算法,方便又通用的指标 FindLevels 在符号图表创建水平支撑线。 参见下文:


本文同时探讨了一个有用的主题,即创建能够在一个工作空间显示不同时间范围结果的简单指标。 示例如下。


第一个指标显示了基于 30 分钟报价的支撑位(米黄色粗线)。 同一窗口内运行的第二个指标显示了 30 分钟位置上方基于 15 分钟时间范围的位置(紫色细短划线)。 详情请参阅“指标相互作用”部分。

本文是关于创建检测支撑位脚本的前文的续篇,不同之处在于本文的目标读者应具有编程和使用 MetaTrader 4 平台的高级水平。 这就是我建议初学者和发现本文有难度的读者先阅读之前的文章:绘制支撑/阻力位的方法


理论综述

现在来描述检测支撑/阻力位的算法,它将在 FindLevels 指标中进一步实现。 支撑/阻力位是由于外力而无法产生交叉的价格值。 外力可能由心理指标、大型交易者的影响或者该区域内的巨量止损订单导致。 显然,比起其他的非支撑线,报价与该线交叉的频率低的多。 在很多交易书籍中可以发现这种事实的佐证。

为了利用这一现象,我们必须计算交叉每个价格的柱的数量。 这是前文中所描述计算的结果。



图像中的横轴是价格,纵轴是交叉价格的柱的数量。 如图所示,图表中有很多局部最小值。 局部最小值是属于非零区间的一个点,其自身是该区间的最小值。 现在必须根据某种特征选择局部最小值。



首先,要设置区域半径常量 MaxR。 如果局部最小值不是属于 MaxR 半径区域的最小值,则不符合我们的要求。 其次,设置 MaxCrossesLevel 参数。 如果函数的最大值和最小值之差小于 MaxR 区域的 MaxCrossesLevel,则不显示该局部最小值,因为它不够显著。 这是检测支撑/阻力位的机制。 遵循这个简单的算法,我们来编写指标。


辅助函数

如上所述,FindLevels 指标设计用于处理任何时间范围的报价。 时间范围由用户设置(TimePeriod 变量)。 出于代码简单性的考虑,设置两个不需要额外解释的简单函数:

double prLow(int i)
  {
    return (iLow(NULL,TimePeriod,i));
  }
 
double prHigh(int i)
  {
    return (iHigh(NULL,TimePeriod,i));
  }

为了便于显示取决于时间范围的支撑位、宽度、颜色和指示模式,第三和第四个函数非常必要。

int Period2Int(int TmPeriod)
  {
    switch(TmPeriod)
      {
        case PERIOD_M1  : return(0);
        case PERIOD_M5  : return(1);
        case PERIOD_M15 : return(2);
        case PERIOD_M30 : return(3);
        case PERIOD_H1  : return(4);
        case PERIOD_H4  : return(5);
        case PERIOD_D1  : return(6);
        case PERIOD_W1  : return(7);
        case PERIOD_MN1 : return(8);
      }      
    return (0);
  }
 
string Period2AlpthabetString(int TmPeriod)
  {
    return(Alphabet[Period2Int(TmPeriod)]); 
  }
设置函数 Period2AlphabetString() 的意义描述见“指标互相作用” 一节,Period2Int() 函数的使用详见下节。



编写指标

从由用户设置的外部变量入手:

extern int MaxLimit = 1000;
extern int MaxCrossesLevel = 10;
extern double MaxR = 0.001;
extern int TimePeriod = 0;
extern color LineColor = White;
extern int LineWidth = 0;
extern int LineStyle = 0;
  • MaxLimit - 所使用的历史柱的数量;
  • MaxCrossesLevel – 局部最大值和局部最小值的最小差异(关于详细描述请参阅“理论综述”一节;
  • MaxR – 检出的最小值所在区域的半径;
  • TimePeriod – 用于检测支撑位的时间范围; 默认为映射窗口的时间范围;
  • LineColor – 显示的线条颜色;
  • LineWidth – 线条宽度,默认为 0;
  • LineStyle – 线型,默认为 0。

如果 LineColor、LineWidth 或 LineStyle 值由用户默认设置,则执行 Init 程序时我们会根据时间范围将其变更为其他选项。 这样不同时间范围的线条视图就不会一致,容易进行区分。

int init()
  {
    if(TimePeriod == 0)
        TimePeriod = Period();
 
    if(TimePeriod != 0 && LineWidth == 0)
        if(Period2Int(TimePeriod) - Period2Int(Period()) >= 0)
            LineWidth = Widths[Period2Int(TimePeriod) - Period2Int(Period())];
        else
          {
            LineWidth = 0;
            if(LineStyle == 0)
                LineStyle = STYLE_DASH;
          }
 
    if(TimePeriod != 0 && LineColor == White)
        LineColor = Colors[Period2Int(TimePeriod)];
 
    return(0);
  }


如果 TimePeriod 值为默认,我们在第一行进行设置。 然后确定线条宽度。 跟图表(映射窗口)时间范围相关的 TimePeriod 值越多,线条越宽。 如果 TimePeriod 小于图表时段,则线条宽度等于 0 且为短划线。 每个时间期间都有自己的颜色。


Colors[] 和 Width[] 数组定义方式如下:

color  Colors[] = {Red,Maroon, Sienna, OrangeRed, Purple,I ndigo,
                   DarkViolet, MediumBlue, DarkSlateGray};
int    Widths[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

让我们定义剩余的变量:

int CrossBarsNum[];
bool CrossBarsMin[];
double d1Num = 0.0, d2Num = 0.0;
datetime TMaxI = 0;
  • CrossBarsNum[] 数组 – 为每个价格指示柱的数量的数组;
  • CrossBarsMin[] – 跟设定价格线是否为局部最小值相对应的数组;
  • d1Num 和 d2Num – 属于从 0 到 MaxLimit 柱区间的最低价格和最高价格;
  • TMaxI – 指示最后处理柱的时间。
#define MaxLines 1000
string LineName[MaxLines];
int LineIndex = 0;
  • MaxLines - 创建线条的最大数量;

  • LineName[] – 线条名称的数组;

  • LineIndex – LineName[] 数组中空单元的索引。


现在继续讲解函数 start():

int counted_bars = IndicatorCounted();
int limit = MathMin(Bars - counted_bars, MaxLimit);
 
double d1 = prLow(iLowest(NULL, TimePeriod, MODE_LOW, limit, 0));
double d2 = prHigh(iHighest(NULL, TimePeriod, MODE_HIGH, limit, 0));

我们使用自上一次提取指标以来尚未变化的柱数计算限值变量。d1 和 d2 是从 0 到限值的区间上的最低和最高价格。

if(d1Num != d1 || d2Num != d2)
  {
    ArrayResize(CrossBarsNum, (d2 - d1)*10000);
    ArrayResize(CrossBarsMin, (d2 - d1)*10000);
 
    if(d1Num != d1 && d1Num != 0.0)
      {
        ArrayCopy(CrossBarsNum, CrossBarsNum, 0, (d1Num - d1)*10000);
        ArrayCopy(CrossBarsMin, CrossBarsMin, 0, (d1Num - d1)*10000);
      }
 
    d1Num = d1;
    d2Num = d2;
  }

在指标运行时,CrossBarsNum[] 和 CrossBarsMin[] 数组覆盖的价格缺口可能改变。 每次发生时,必须增加数组内单元的数量,必要时将其移到右侧。 如果新变量 d1 和 d2 跟上次运行函数 start() 时得到的变量 d1Num 和 d2Num 不匹配,则通常会发生。

for(double d = d1; d <= d2; d += 0.0001)
  {
    int di = (d - d1)*10000;
    for(int i = 1; i < limit; i++)
        if(d > prLow(i) && d < prHigh(i))
            CrossBarsNum[di]++;
    if(Time[limit] != TMaxI&&TMaxI != 0)
        if(d > prLow(iBarShift(NULL, 0, TMaxI)) && 
           d < prHigh(iBarShift(NULL, 0, TMaxI)))
            CrossBarsNum[di]--;
  }
TMaxI = Time[limit] - 1;

在确保数组符合必要的大小后,开始计算每个价格的新柱数,并在柱交叉价位时增大 CrossBarsNum[] 值。 由于不断出现新柱,旧柱将从 [0 : limit] 区间除去。 因此,必须检查这种柱,以在出现交叉时减小 CrossBarsNum[] 值。 然后我们将最后计算的柱的时间分配给 TmaxI 变量。

double l = MaxR*10000;
for(d = d1 + MaxR; d <= d2 - MaxR; d += 0.0001)
  {
    di = (d - d1)*10000;
    if(!CrossBarsMin[di] && CrossBarsNum[ArrayMaximum(CrossBarsNum, 2*l, di - l)] - 
       CrossBarsNum[ArrayMinimum(CrossBarsNum, 2*l, di - l)] > MaxCrossesLevel &&
       CrossBarsNum[di] == CrossBarsNum[ArrayMinimum(CrossBarsNum, 2*l, di - l)] &&
       CrossBarsNum[di-1] != CrossBarsNum[ArrayMinimum(CrossBarsNum, 2*l, di - l)])
      {
        CrossBarsMin[di] = true;
        LineName[LineIndex] = Period2AlpthabetString(TimePeriod) + TimePeriod + "_" + d;
        ObjectCreate(LineName[LineIndex], OBJ_HLINE, 0, 0, d);
        ObjectSet(LineName[LineIndex], OBJPROP_COLOR, LineColor);
        ObjectSet(LineName[LineIndex], OBJPROP_WIDTH, LineWidth);
        ObjectSet(LineName[LineIndex], OBJPROP_STYLE, LineStyle);
        LineIndex++;
      }
    if(CrossBarsMin[di] && CrossBarsNum[di] != 
       CrossBarsNum[ArrayMinimum(CrossBarsNum, 2*l, di - l)])
      {
        CrossBarsMin[di] = false;
        ObjectDelete(Period2AlpthabetString(TimePeriod) + TimePeriod + "_" + d);
      }          
  }

在 start() 程序最后,我们再次查看 CrossBarsMin[] 数组,以确定新的局部最小值并删除不再是局部最小值的旧值。 因为可能的局部最小值不止一个(在 CrossBarsMin[] 数组有多个匹配值,全部是局部最小值),我们只能提取其中之一。 我们将使用价格最低的局部最小值。

CrossBarsNum[di] == CrossBarsNum[ArrayMinimum(CrossBarsNum, 2*l, di - l)] &&
CrossBarsNum[di-1]!= CrossBarsNum[ArrayMinimum(CrossBarsNum, 2*l, di - l)]

创建一个新的图形对象——水平线,并不复杂 设置该线条的特征:宽度、线型和颜色(我们之前在 Init 过程已经对其进行设定)。 删除那些不再是支撑线的线条并不复杂。 只有一点没有阐述清楚:为什么以及出于什么目的而使用以对象命名的 Period2AlpthabetString(TimePeriod) 函数? 下一段对该问题进行了描述,之前已多次提及。



指标互相作用

如本文开头所讲,FindLevels 指标设计用于指示在一个图表内多个时间范围的支撑线。 为了达到该目的,需要满足以下条件:

  1. 应能多次运行指标,且指标应具有时间期间内的输入数据;

  2. 线条应不同,要容易区分每个支撑位所属的时段;

  3. 所有线条都应该长期和短期跟踪。

第一点没有任何问题。 我们没有全局变量。 每个时段的图形对象都有不同的名称,因为对象名称中包含时段(例如,“f30_1.25600000”中的 30 是时段),所以在运行多个指标时不会有冲突。

第二点已成功实现,每个线条都根据时段有不同的颜色(LineColor=Colors[Period2Int(TimePeriod)])。

现在只剩下第三点。 按理来讲,如果一条线为 5 分钟图表的支撑线,则也会是 30 分钟图表的支撑线。 如果这些线在价格上出现冲突,具有相同的宽度,则其中一条线就会变为不可见! 这就是不同时间范围的线条宽度应不同的原因。 我们使较长时间范围的支撑线宽度大于较短时间范围的支撑线。 这很容易理解,因为属于长时段的线条更加显著。

必须设置线条显示的适当优先级。 较细的线条应最后显示并在粗线条之上,只有这样才能看清。 在 MetaTrader 4 程序中对象按字母顺序显示。 因此有必要使较长时段的线条名称按照字母顺序出现在较短时段的线条之前。 这就是我们创建根据时段设置拉丁字母顺序函数的缘故。

string Period2AlpthabetString(int TmPeriod)
  {
    return (Alphabet[Period2Int(TmPeriod)]);
  }
Alphabet 是逆序拉丁字母的数组。 每个支撑位的全名如下: Period2AlpthabetString(TimePeriod)+TimePeriod+"_"+d。

为了便于理解,请参考文章开头的截屏:


总结

指标测试表明其行之有效。 因为可以显示不同时间范围的数据,所以容易使用。 测试期间显示,如果指标为每个 TimePeriod 显示 3-10 个支撑位,则更加方便。 为此,必须选择相应的输入条目 MaxR 和 MaxCrossesLevel。 测试过程中,MaxR 从较短时间范围的 0.0003 到较长时间范围的 0.002 不等。 MaxCrossesLevel 从 3 到 20 不等。 对指标进行配置,使其能够显示一定数量的最显著的支撑位,或许这样比较有用,但这样会使代码更加复杂。 我想那些喜欢我指标的人自己能够轻松做到。