English Русский Español Deutsch 日本語 Português
preview
手工制图表和交易工具箱(第三部分)。 优化和全新工具

手工制图表和交易工具箱(第三部分)。 优化和全新工具

MetaTrader 5示例 | 24 一月 2022, 08:43
2 170 0
Oleh Fedorov
Oleh Fedorov

概述

在以前的文章(第一部分第二部分)中,我描述了快捷方式函数库,并以智能交易系统形式展示了一个函数库用例。 在某种程度上,函数库类似于一个鲜活的有机体。 它出生后,会向公众展示,并与它所“生存”的环境相适应。 然而,环境在变化,且自有其规律。 主要规则之一是“进化”。 如此,任何事物必须持续发展和改进。 本文展示了在此改进过程里取得的一些结果。

故此,我们的函数库由五个文件组成。

主文件是 Shortcuts.mqh。 该文件存储击键处理逻辑。 甚而,这是唯一直接包含在智能交易系统或指标中的文件。 相应地,它包含其余文件,并对其进行初始化。

文件 GlobalVariables.mqh 则包含所有全局变量。 这些设置主要用于设置线条颜色、线条厚度、线条延伸系数、等等。

文件 Mouse.mqh 包含处理鼠标移动的类定义。 它存储当前光标坐标,含像素和“价格-时间”坐标,以及当前主线编号。

Utilites.mqh 包含辅助函数。 它计算柱线图极值、指标线交点和其它有用参数,这些参数也许与绘图没有直接关系,但可判断图形的所有方面。

文件 Graphics.mqh 负责依据其它文件中的数据进行绘图。 来自该文件中的主要函数调用 Shortcuts.mqh 文件。

我并不总是严格地将函数分组。 某些计算是在绘图函数中进行的。 到目前为止,于我来讲,开发和维护这个结构还很方便。 也许有一天我会改进总体布局。

此实现展示了如何在指标中运用函数库代码。


优化函数库性能

此处这是略微有点修改。

为什么我在最初的函数库版本中选择采用智能交易系统,而非指标? 这很简单。 每个智能交易系统都在自己的执行线程中运行。 理想情况下,它们不会相互影响,因此,如果我们需要在多个图表上处理键盘快捷键,终端不会变慢太多。

不过,智能交易系统的目的是交易,而该程序并不执行任何交易操作。 甚而,当一个指标附加到图表上时,在图表上运行另一个智能交易系统也更容易一些。 因此,我决定实现一款指标。 这里会浮现一个性能速度的问题。 如果用户有多个打开的窗口,这一点尤其重要。 例如,如果用户有 40 个打开的选项卡(可以有更多),那么若所有图表同时处理击键,则很难进行击键检查。

然后我就悟了:为什么我们要处理所有的图表? 所有检查只需在活动窗口中执行。

代码非常简单。

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
 //...

  if(ChartGetInteger(0,CHART_BRING_TO_TOP)==false)
   {
    return;
   }
 
 //...

在函数伊始,我们需要加入一个检查,检查该图表是否处于活动状态,即图表是否位于前景上。 如果为假,什么都不要做。

我没有以百分比的形式来衡量真正的性能、或速度增益。 但是,根据下载了该函数库,且真实用到大量选项卡的用户评论,该应用程序变为指标的形式,甚至更具响应性。 这恰恰正是我们需要的。

另外,不要忘记应用程序的目的。

首先,它设计用于执行偶发功能(因此,它不会在每次即时报价中被调用),故它只会在非常有限的时间内消耗资源。

第二,如果您的电脑不够强大,图形本身就是速度问题的根源。 图表上的对象越多,处理它们就越困难。 尽管如此,它是一个图形库,因此您必须接受绘图成本,并仔细控制正在绘制的内容。

第三,程序中资源最密集的函数是极值搜索函数。 但我不知道如何更快地实现它。 故此,我认为当前的实现是最优的。 无论如何,这个函数并不会被经常调用,只有在绘制直线和其它一些有用的造型时才会被调用,因此可忽略它的性能至目前并非最佳。

所有其它函数的调用次数更要少得多,且它们的操作速度足够快,故此无需讨论它们。


代码重构:管理连接性

前几篇文章中介绍的代码版本,当时假定的应用程序很单调,且其各个部分不会单独使用。 这就是为什么代码直接采用全局设置,而一些实用程序依赖于鼠标类。

有因于此,我在编写代码时可以更快捷,但从重用的角度来看,这很不经济。 如果我需要将已存在的实用程序文件连接到不用鼠标或图形的新项目,我仍然需要连接全局设置文件和鼠标类。

这是错误和不方便的。

这就是我决定稍微修改代码的原因。 所有全局变量仍在使用。 我们无法删除它们,因为它们是作为设定。

我已经将包含这些变量副本的私密字段添加到主类之中。 为了存储这些值,必须添加特殊的“公开”函数。 还需要它们来读取数值。

看起来是这样的:

private:
  /* Fields */
  //---
  static int          m_TrendLengthCoefficient;

public:
  /* Methods */
  //---
  static int          TrendLengthCoefficient(void) {return m_TrendLengthCoefficient;}
  //---
  static void         TrendLengthCoefficient(int _coefficient) {m_TrendLengthCoefficient=_coefficient;}

考虑到现有设定的数量,这个过程似乎漫长而乏味。

但优势是巨大的。 首先,该类独立于外部文件。 如果任何人想要使用该类,他们只能使用所需的变量,并根据需要设置它们的值。

其次,这些变量可以在运行时更改。 例如,若有人想编写一个函数,从基于单个端点构建一个扇形线。 每条线的长度是前一条线的两倍,它们以不同的角度岔开。 如何做到这一点? 使用 CUtilites 类的当前实现:在每次绘图之前,设置参数说明,示例 - TrendLengthCoefficient,将起点放置在相同的坐标上,而终点应放置在某些任意半径的圆上。

第三,类内部的数据可以按照任何便捷的方式分组。 您可以创建单独的存储结构,甚至完整的类,例如,与矩形相关的数据、与对角线相关的数据和关于级别的数据。 从最终用户的角度来看,界面(访问数据的方式)没未改变。

第四,数据不一定要存储在内存当中。有时变量可以存储在终端的全局变量之中,或通常存储在文件、甚或数据库之中。 可以根据其它参数动态计算某些参数。 通过这种“正确”的组织方式访问数据,如上面的示例所示,最终用户可以最大限度重用代码,而不必担心直接实现数据结构。 无论如何,它非常方便,尽管编写多余代码、调用不必要的函数,都需要额外的工作量,且每次创建实例时都必须初始化所需的变量。

因此,我已经重写了当前函数库版本中的所有内容,以便能够匹配新风格,从而实用程序文件现在可以在任何项目中“按原样”使用。

鼠标类最初包含所有相关函数,因此没有需要修复的内容。 如果没有这些实用工具,绘图类就毫无用处。 无论如何,我已经为新的字段形式修改了其中的所有外部设置。

那么,这就是我们所拥有的。 鼠标和实用工具程序是非常独立的类,可以单独或组合使用。 绘图类同时用到这两个文件,但它独立于其它外部文件,不过初始化该类的派发器除外。 包含键盘快捷键的类是管理类,即,派发器另整个代码根据需要操作。 因此,类之间的互连现在被削弱很多,从而导致上述众多益处。


"Crosshair(交叉线)" 工具

在之前的函数库版本中,当绘制趋势线时,在其末尾会绘制交叉线以便确定时间和价位。 为了创建它,我采用两条简单的线,垂直线和水平线。 然而,为了在图表的任意点上显示它,我必须按下 H 和 I 两个键。有时这很方便,但有时您可能希望更省事。 因此,我添加了交叉线工具。

该工具按照一种常见的方式操作。 将光标移动到所需位置,然后按 X 键 - 如此生成交叉线。 下面是函数代码。


/* Graphics.mqh */

//+------------------------------------------------------------------+
//| Draws a crosshair at specified coordinates. If the coordinates   |
//|   are not set, the mouse pointer coordinates are used.           |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   datetime _time - crosshair time                                |
//|   double _price - price level                                    |
//+------------------------------------------------------------------+
void CGraphics::DrawCross(datetime _time=-1,double _price=-1)
 {
  datetime time;
  double price;
//---
  if(_time==-1)
   {
    time=CMouse::Time();
   }
  else
   {
    time=_time;
   }

  if(_price==-1)
   {
    price=CMouse::Price();
   }
  else
   {
    price=NormalizeDouble(_price,Digits());
   }
  DrawSimple(OBJ_HLINE,time,price);
  DrawSimple(OBJ_VLINE,time,price);
  
 }

对于那些熟悉上一版本代码的人来说,没啥特别之处。 首先,设置坐标。 如果这些坐标是经由参数传递而来,则完全采用这些值。 如果设置了默认参数,则采用鼠标指针的坐标。

进而,利用第二篇文章中讲述的函数绘制指示线

若要令此函数完全按照所讲述的方式操作,应依据某些事件从 Shortcuts.mqh 文件里调用它 - 按下 X 键。

/* GlobalVariables.mqh */
  
  // ...
  
  input string   Cross_Key="X";                       // Crosshair where the mouse was clicked
  
  // ...
  /* Shortcuts.mqh */
  
  void CShortcuts::OnChartEvent( /* ... */ )
    switch(id)
     {
       case CHARTEVENT_KEYDOWN:
       
       // ... 
       
       //--- Draw a crosshair
       if(CUtilites::GetCurrentOperationChar(Cross_Key) == lparam)
        {
         m_graphics.DrawCross();
        }
     }

通过任意极值工具绘制趋势线

依据左侧和右侧指定数量柱线的极值来创建趋势线的功能非常方便。 不过,有时您可能希望通过任意极值来绘制一条线。 这可以使用 Q 命令来完成

下面的 gif 图片展示了此函数如何操作的示例。

经由任意极值绘制趋势线的一个例子

因为我的屏幕捕捉应用程序有一些特别的功能,所以每次画图之前我都必须点击图表。 在实际条件下,您只需激活图表,然后就可以根据需要绘制多条指示线。

这条指示线要分两步来绘制。 在第一步中,您应该按下 Q 键。 它激活了任意绘制指示线模式,并标记第一个点位 - 其明显表示该命令已被执行。

如果您不想使用此极值点(标记所在位置),可以再次按下 Q — 切换模式,并取消绘图。 (有一天,我可能会改变这种行为,将 Esc 键设置为取消,尽管我个人觉得现在的工作方式也很舒适)。

如果第一个点是正确的,则通过单击第二个极值附近的来选择下一个点。 如果有作用,则不再需要标记。 它将被删除,并绘制趋势线。

“任意”指示线的参数不依赖于 T 模式,因此您可以配置,譬如说,T 绘制一条 4px 宽的粗体线,长度为极值间距的四倍,而 Q 绘制一条细线,其长度是间距的两倍。

如往常一样,代码被切分到多个文件之中。

我们从末尾开始,从 CHARTEVENT_KEYDOWN 事件的处理开始:

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
   //...
   
   switch(id)
   {
   
   //...
   
     case CHARTEVENT_KEYDOWN:
      if(CUtilites::GetCurrentOperationChar(Free_Line_Key) == lparam)
       {
        m_graphics.ToggleFreeLineMode();
        if(m_graphics.IsFreeLineMode()){
          m_graphics.DrawFreeLine(CMouse::Bar(),CMouse::Above());
        }
       } 
    
    //...

如果程序断定按下了 Q 键(字母存储在 Free_Line_Key 外部变量中),则它将切换到绘图模式。 如果在模式切换后,模式打开,则会生成一条命令来执行划线功能

单击将在事件中处理

/* Shortcuts.mqh */

        //...
        
    case CHARTEVENT_CLICK:
        ChartClick_Handler();
      break;
      
      //...
      
}

//+------------------------------------------------------------------+
//| Processing a click on a free chart field                         |
//+------------------------------------------------------------------+
void CShortcuts::ChartClick_Handler()
 {
  
//---
  if(m_graphics.IsFreeLineMode()){
    m_graphics.DrawFreeLine(
      CMouse::Bar(),CMouse::Above()
    );
  }
  
 }

再次提请注意,当按键时,绘图模式立即切换,甚至在开始任何绘图之前(我的函数名称以 Toggle 开头)。 此状态会保持,直到再次按键切换,或指示线绘制完毕。当您单击时,程序首先检查是否有需要绘制的内容。 若有,它就绘图,并切换到中立模式。

ChartClick_Handler 函数是单独实现的,因为我计划加入更多需要在图表上单击的模式。 例如,删除复杂对象(如前一篇文章中讲述的交叉线或垂直标高)的模式,也许有时需要单击图表以取消菜单。 到目前为止,单独实现单击功能似乎可以简化进一步的开发。 但所有这些功能将在稍后实现。

至于现在,我们继续研究绘图如何操作。

/* Graphics.mqh */


//+------------------------------------------------------------------+
//|  Draws a line by arbitrary specified extrema. In the current     |
//|    implementation, the first extremum is set by a hot key        |
//|    (Q by default), the second is set by clicking near the        |
//|    required top                                                  |
//+------------------------------------------------------------------+
//|  Parameters:                                                     |
//|    int _bar - bar to start search at                             |
//|    bool _isUp - top or bottom?                                   |
//|    int _fractalSizeRight - number of bars to the right of extr   |
//|    int _fractalSizeLeft -  number of bars to the left of extremum|
//+------------------------------------------------------------------+
void CGraphics::DrawFreeLine(
  int _bar,
  bool _isUp,
  int _fractalSizeRight=1,
  int _fractalSizeLeft=1
)
 {
//--- Variables
  double    selectedPrice,countedPrice,trendPrice1,trendPrice2;
  datetime  selectedTime,countedTime,trendTime1,trendTime2;
  int       selectedBar,countedBar;
  int       bar1,bar2;

  string trendName="",trendDescription="p2;";
  int fractalForFirstSearch = MathMax(_fractalSizeRight,_fractalSizeLeft)* 2;

//--- Search for a bar that meets the extremum criteria
  selectedBar = CUtilites::GetNearesExtremumSearchAround(
    _bar,
    _isUp,
    _fractalSizeLeft,
    _fractalSizeRight
  );

//--- Building the starting marker
  if(0==m_Clicks_Count)
   {
    m_Clicks_Count=1;
    if(_isUp)
     {
      m_First_Point_Price=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      m_First_Point_Price=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    m_First_Point_Time=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //---
    m_First_Point_Time=CUtilites::DeepPointSearch(
                         m_First_Point_Time,
                         _isUp,
                         ENUM_TIMEFRAMES(Period())
                       );
    //---
    DrawFirstPointMarker(_isUp);
   
   }
//--- Processing a click on the chart
  else
   {
    ObjectDelete(0,m_First_Point_Marker_Name);
    if(_isUp)
     {
      countedPrice=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      countedPrice=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    countedTime=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //--- Move a point in time on smaller timeframes
    countedTime=CUtilites::DeepPointSearch(countedTime,_isUp,ENUM_TIMEFRAMES(Period()));

    //--- The line is always drawn from left to right. 
    //--- If it is not convenient, you can comment this part
    //---   up to the next comment
    if(countedTime<m_First_Point_Time)
     {
      trendTime1=countedTime;
      trendPrice1=countedPrice;
      trendTime2=m_First_Point_Time;
      trendPrice2=m_First_Point_Price;
     }
    else
     {
      trendTime2=countedTime;
      trendPrice2=countedPrice;
      trendTime1=m_First_Point_Time;
      trendPrice1=m_First_Point_Price;
     }
    //--- Set the description for future correction
    trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());

    //selectedPrice=CUtilites::EquationDirect(
    //                trendTime1,trendPrice1,trendTime2,trendPrice2,selectedTime
    //              );
    trendName=CUtilites::GetCurrentObjectName(allPrefixes[0],OBJ_TREND);
    
    TrendCreate(
      0,                    // Chart ID
      trendName,            // Line name
      0,                    // Subwindow number
      trendTime1,           // time of the first point
      trendPrice1,          // price of the first point
      trendTime2,           // time of the second point
      trendPrice2,          // price of the second point
      CUtilites::GetTimeFrameColor(
        CUtilites::GetAllLowerTimeframes()
      ),                    // line color
      Trend_Line_Style,     // line style
      Trend_Line_Width,     // line width
      false,                // background object
      true,                 // is the line selected
      true                  // ray to the right
    );
    
    bar1=iBarShift(NULL,0,trendTime1);
    bar2=iBarShift(NULL,0,trendTime2);
    selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );
    selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);
    ObjectSetInteger(0,trendName,OBJPROP_RAY,IsRay());
    ObjectSetInteger(0,trendName,OBJPROP_RAY_RIGHT,IsRay());
    ObjectMove(0,trendName,1,selectedTime,selectedPrice);
    //---
    m_Clicks_Count=0;
    ToggleFreeLineMode();
   }

  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);
  ChartRedraw();
 }

这个函数相当长,所以我以后可能会将其拆分为几个较小的函数。 我希望高亮显示和注释能辅助理解它是如何操作的。

在此实现中,该函数检查两个信号:指示第一个点已开始绘图的事件,和已发现第二个点的通知,以及绘图已开始。 引入 m_Clicks_Count 变量来区分这些事件。 根据开头的字母 “m_”,很明显该变量与该类的全局变量相对应,其生存期等于对象实例的生存期。

如果是第一次函数调用(即按下一个键),则必须找到第一个点,以便绘制标记

如果是第二次调用,则必须删除标记,去找到第二个点,并绘制一条线。 这些是主要的五个模块,而所有其它模块都是实现它们所必需的。

在当前实现中,未来的价格是借助直线本身来判定的。 通常,这并不是一个好主意,因为在绘图时,终端必须先绘制射线,然后将线的末端移动到期望的点位,并决定是否绘制射线(取决于外部设置)。 一般的话,我利用 Igor Kim(Kim IV)的著名函数进行初步计算,该函数也包含在函数库当中。 代码的粉红色部分在调用函数时给予了注释。 不过,在这种情况下,如果点数是按时间顺序计算的,我们可能会因周末的相关数据缺口而产生错误,我希望避免这个错误。 当然,这种错误可以轻易地避免,方法是逐根柱线数计算指示线,然后依据这些数字重新计算实际日期。 然而,目前的实现对我来说似乎更清晰明了。

故此,粉红色高亮显示的代码中,已经找到了基准极值。 我们现在需要做的是绘制指标线。 首先,我们在两个基准极值点之间画一条线 — 此处,我们应该启用“射线”属性,从而这条线可延伸到未来(本模块开头的 TrendCreate 函数)。

根据以下设置计算所需的未来时间:

selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );

然后,调用标准函数获得所需的价格。

selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);

之后,我们只需要将直线的第二个端点移动到所需的坐标,并设置真实的射线属性(默认情况下,使用“射线”中的 R 键切换此属性)。

一旦画线完毕,应关闭“单击等待”状态 - 这在如下行中完成: 

    m_Clicks_Count=0;
    ToggleFreeLineMode();

该函数其它模块中的代码略微复杂一些。 于此,我添加了一些有用的特性来提升直线的可用性。

首先提供的是在较小时间帧上指标线偏移效应有关。 如果以常规方式画线,则在时间帧之间切换时会出现类似的情况:

D1 线尾端 H4 线尾端

与 D1 上的极值完全重合的直线左边缘在 H4 上向左偏移了,因此它并未与极值重合。 这是一个显而易见的影响,因为某一天的极值未必会落在该日的开始。 如果您需要更高的精度,在手工绘制时,您可以大致绘制一条线,然后切换到较低的时间帧去调整边缘。

如果您有一个或两个图表,就可以采用此解决方案。 如果您有 20 个呢? 甚至 100? 这可能很烦人。

既然该程序具有自动绘图功能,我们可在绘制每个对象时将此任务委托给该程序。

基于这些思路,我决定创建 DeepPointSearch 函数。


DeepPointSearch 函数

在任意画线函数中,调用 DeepPointSearch 函数两次,每个端点调用一次。 该函数以提供在实用程序文件之中。 其代码如下:

//+------------------------------------------------------------------+
//| Search for a given point on lower timeframes                     |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   datetime _neededTime - start time on a higher timeframe        |
//|   bool _isUp - search by highs or by lows                        |
//|   ENUM_TIMEFRAMES _higher_TF - the highest period                |
//+------------------------------------------------------------------+
//| Return value:                                                    |
//|   More accurate date (on the lowest possible timeframe)          |
//+------------------------------------------------------------------+
datetime CUtilites::DeepPointSearch(
  datetime _neededTime,
  bool _isUp,
  ENUM_TIMEFRAMES _higher_TF=PERIOD_CURRENT
)
 {
//---
  //--- As a result it gets the most accurate time available
  datetime deepTime=0;
  //--- current timeframe
  ENUM_TIMEFRAMES currentTF;
  //--- The number of the highest timeframe in the list of all available periods
  int highTFIndex = GetTimeFrameIndexByPeriod(_higher_TF); 
  //--- The higher period in seconds
  int highTFSeconds = PeriodSeconds(_higher_TF);
  //--- Current interval in seconds
  int currentTFSeconds;
  //--- Counter
  int i;
  //--- Bar number on a higher timeframe
  int highBar=iBarShift(NULL,_higher_TF,_neededTime);
  //--- Bar number on the current timeframe
  int currentBar;
  //--- The total number of bars on the current timeframe
  int tfBarsCount;
  //--- How many bars of a lower TF fit into one bar of a higher TF
  int lowerBarsInHigherPeriod;
  //--- Maximum allowed number of bars in the terminal
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

//--- Loop sequentially through all timeframes
  for(i=0; i<highTFIndex; i++)
   {
    //--- Get a timeframe by a number in the list
    currentTF=GetTimeFrameByIndex(i);
//--- Check if this timeframe has the required time.
    tfBarsCount=iBars(NULL,currentTF);
    if(tfBarsCount>terminalMaxBars-1)
     {
      tfBarsCount=terminalMaxBars-1;
     }
    deepTime=iTime(NULL,currentTF,tfBarsCount-1);
//--- If it has, find it.
    if(deepTime>0 && deepTime<_neededTime)
     {
      currentTFSeconds=PeriodSeconds(currentTF);
      
      //--- Search for the required bar only within the higher TF candlestick
      lowerBarsInHigherPeriod=highTFSeconds/currentTFSeconds;
      currentBar = iBarShift(NULL,currentTF,_neededTime);
      
      if(_isUp)
       {
        currentBar = iHighest(
                       NULL,currentTF,MODE_HIGH,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );

       }
      else
       {
        currentBar = iLowest(
                       NULL,currentTF,MODE_LOW,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );
       }
      deepTime=iTime(NULL,currentTF,currentBar);
      //--- Once the required time is found, stop the search
      break;
     }
   }
//--- If reached the end of the loop
  if(i==highTFIndex)
   {
    //--- then the required time is only available on the higher timeframe.
    deepTime=_neededTime;
   }
//---
  return (deepTime);
 }

对我来说,主要的难处是理解主搜索片段应该如何工作。 顺理成章,首先要做的是判定历史上是否存在所需的时间。 您也许知道,较低的时间帧通常不包含较高时间帧上拥有的一些信息。 标准 iBars 函数计算历史记录中的柱线数量。 然而这还不够,因为终端只能显示有限数量的柱线。 在最开始,我们利用以下代码来了解终端可以显示多少根柱线

//--- Maximum allowed number of bars in the terminal
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

如果历史记录里包含的柱线太多,则将其限制为可显示的柱线数。

接下来,利用 iTime 函数定义历史记录中最后一根柱线的时间。 如果这个时间大于所需的时间,那么就没有必要再进一步查看,因为最高可用日期是最近的日期,所以只需切换到下一个更高的时间帧。 如果终端中最后一个可用的烛条早于我们正在寻找的烛条,那么我们很可能已经找到了最深层的位置,这一点也很有意义。

例行程序在所有检查之后开始。 所需的点位是更高时间帧的烛条范围内最极端的点位。 我们只需要判定应该分析多少根烛条。 然后,标准函数帮助判定最极端的极值,基于该点位我们可以计算时间,并完成工作。

在函数库的当前实现中,此函数仅适用于由 T 键和 Q 键调用的指标线。 不过,在下一版本中,此功能将适用于所有金融工具。 此外,我还计划为每种金融工具单独定制。


时间校正

此实现的第二个具体功能是按时间校正指标线。 下面的动画解释了这个问题。

注意最后一个矩形的缩窄。 这条线的末端距离中间的矩形有一天多的距离,实际结果证明它非常接近。 相应地,上述端点也发生了偏移(注意顶部附近直线的行为)。 当线缩窄时,会出现新的突破,这也许会影响交易策略。

这对外汇市场来说可能无关紧要,因为常会出现峰值,比如说,一周一次。 但在股票市场上,这种时间缺口每天都可能发生,具体取决于交易所,而且通常在一天之内多次出现。

这就是自动化派上用场的地方!

为了另零部件按预期工作,我们应该以某种方式保存“正确”的坐标,然后再根据需要进行调整。

我选择利用直线的描述来保存坐标,因为大多数交易者在创建自动对象时不使用描述。 或者,如果指标线太多,我们可以用带有指标线清单的文件,或终端全局变量。

/* Graphics.mqh */

void CGraphics::DrawFreeLine(//...)
 {

//...
  string trendDescription="p2;";

//...
  trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());
  
//...
  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);

接下来,将前面描述的操作应用于“物理”指标线的坐标。 我想下面的代码已经非常清晰。

/* Utilites.mqh */

//+------------------------------------------------------------------+
//|  Adjusts the position of line end in the future in case of price |
//|   gaps                                                           |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   string _line_name - the name of the line to be corrected       |
//+------------------------------------------------------------------+
void CUtilites::CorrectTrendFutureEnd(string _line_name)
 {
//---
  if(ObjectFind(0,_line_name)<0)
   {
    PrintDebugMessage(__FUNCTION__+" _line_name="+_line_name+": Object does not exist");
    //--- If there is no object to search, there is nothing more to do.
    return;
   }
  //--- Get a description
  string line_text=ObjectGetString(0,_line_name,OBJPROP_TEXT);
  
  string point_components[]; // array for point description fragments
  string name_components[];  // array containing line name fragments
  string helpful_name="Helpful line"; // the name of the auxiliary line
  string vertical_name=""; // the name of the corresponding vertical from the crosshair
  
  //--- Get the point time and price in string form
  int point_components_count=StringSplit(line_text,StringGetCharacter(";",0),point_components);
  
  datetime time_of_base_point; // time of the basic point
  datetime time_first_point,time_second_point; // the time of the first and the second point
  datetime time_far_ideal; // estimated time in the future
  double price_of_base_point; // the price of the basic point
  double price_first_point,price_second_point; // the prices of the first and the second point
  int i; // counter

//--- Check if the line is needed
  if(line_text=="" || point_components_count<3 || point_components[0]!="p2")
   {
    PrintDebugMessage(__FUNCTION__+" Error: the line cannot be used");
    return;
   }
//--- Get the coordinates of the "basic" point from the line description
  time_of_base_point=StringToTime(point_components[1]);
  price_of_base_point=StringToDouble(point_components[2]);
  if(time_of_base_point==0 || price_of_base_point==0)
   {
    PrintDebugMessage(__FUNCTION__+" Error: Unusable description");
    return;
   }
//--- Get the real coordinates of the line
  time_first_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,0);
  time_second_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,1);
  price_first_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,0);
  price_second_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,1);

//--- Create an auxiliary line (from the starting point to the base one)
  MakeHelpfulLine(
    time_first_point,
    price_first_point,
    time_of_base_point,
    price_of_base_point
  );

//--- Calculate the correct time for the current situation
  time_far_ideal=ObjectGetTimeByValue(0,helpful_name,price_second_point);
//---
  if(time_second_point != time_far_ideal)
   {
    //--- move the free end of the trend line
    ObjectMove(0,_line_name,1,time_far_ideal,price_second_point);
    //--- and the corresponding vertical
    StringSplit(_line_name,StringGetCharacter("_",0),name_components);
    for(i=0; i<ObjectsTotal(0,-1,OBJ_VLINE); i++)
     {
      vertical_name = ObjectName(0,i,-1,OBJ_VLINE);
      if(name_components[0]==StringSubstr(vertical_name,0,StringFind(vertical_name,"_",0)))
       {
        if((datetime)ObjectGetInteger(0,vertical_name,OBJPROP_TIME,0)==time_second_point)
         {
          ObjectMove(0,vertical_name,0,time_far_ideal,price_second_point);
          break;
         }
       }
     }
   }
  // Delete the auxiliary line
  RemoveHelpfulLine();
 }

应该每隔一段时间调用此代码。 我把它设定在每隔一个新小时的开始。

/* Shortcuts.mq5 */

int OnCalculate(/*...*/)
 {
   //...
   if(CUtilites::IsNewBar(First_Start_True,PERIOD_H1))
   {
    for(i=0; i<all_lines_count; i++)
     {
      line_name=ObjectName(0,i,-1,OBJ_TREND);
      CUtilites::CorrectTrendFutureEnd(line_name);
      ChartRedraw();
     }
   }
   //...
 }


当前函数库实现中用到的按键

动作
 按键 含义
 依据主要时间帧(来自时间帧面板)向上选择时间帧  U  Up(向上)
 向下选择时间帧  D  Down(向下)
 更改图表 Z 的等级 (图表是否位于所有对象的上层)  Z  Z 层序
 基于距离鼠标最近的两个单向极值点绘制一条倾斜趋势线  T  Trend line(趋势线)
 为新指标线切换射线模式
 R 键  Ray(射线)
 绘制一条简单的垂直线
 I(i) [只有垂直线可视]
 绘制一条简单的水平线
 H  Horizontal(水平)
 绘制安德鲁草叉
 P  Pitchfork(草叉)
 绘制斐波那契扇形 (VFun)
 F 键  Fun(扇形)
 绘制一条短水平价位线
 S  Short(短)
 绘制一条扩展水平价位线
 L 键  Long(长)
 绘制一条带价位标记的垂直线
 V  Vertical(垂直线)
 绘制交叉线
 X  [只有交叉线可视]
 依据任意极值点绘制一条线
 Q  [不一致... “L” 和 “T” 分别处理]
 绘制一组矩形
 B  Box(箱型)


结束语

希望这篇材料能对您有所帮助。 如果您有任何建议,或改进意见,请在文章的评论中分享。

进而,我计划实现的功能,不仅可基于严格的极值点,且能基于曲线切点处画线。

另外,我想针对通道实现一些东西。 目前我只想过操控等距通道。 不过,如果有人留言或私信我,建议在函数库原则之外绘制别的东西,我也会考虑这样的建议。

作为进一步的改进,稍后我将实现在文件中保存设置(而不是通过与指标设置一起的输入变量),并添加图形界面,从而允许在图表修改设置。


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/9914

附加的文件 |
处理时间(第二部分):函数 处理时间(第二部分):函数
自动判定经纪商时移和 GMT。 与其请求您的经纪商的支持,您可能会从他们那里得到一个不充分的答案(他们很愿意解释时间错位),我们只需自行查看在时间变化的几周内他们如何计算价格 — 但手工操作极其繁琐,我们让程序来做这件事 — 毕竟这就是为什么我们要有一台 PC。
MQL5 酷宝书 – 财经日历 MQL5 酷宝书 – 财经日历
本文着重于财经日历的编程特性,并研究创建一个类来简化访问日历属性和接收事件值。 运用 CFTC(商品期货交易委员会)的非商业净持仓规则来开发指标作为一个实际例子。
更好的程序员(第 06 部分):9 个导致有效编码的习惯 更好的程序员(第 06 部分):9 个导致有效编码的习惯
并非有关编写代码的所有事情总是导致有效编码。 在我的从业经历中,我发现了一些会导致有效编码的习惯。 我们将在本文中详细讨论其中的一些。 对于每一位想要以更少的麻烦来提高自己编写复杂算法的能力的程序员来说,这是一篇必须阅读的文章。
开发 EA 构造函数的一次尝试 开发 EA 构造函数的一次尝试
在本文中,我把自己的一套交易函数以成品 EA 的形式提供给大家。 这种方法能够通过简单地添加指标和改变输入来获得多种交易策略。