English Русский Español Deutsch 日本語 Português
如何为 MetaTrader 市场创建一款非标准图表的指标

如何为 MetaTrader 市场创建一款非标准图表的指标

MetaTrader 4示例 | 9 六月 2016, 09:53
11 027 0
Vladimir Karputov
Vladimir Karputov

目录

 

从日本蜡烛条到 Renko 图表

迄今为止在交易者当中最流行的图表类型是日本蜡烛条, 它有助于轻松评估当前的市场形势。蜡烛条图表能够通过单一蜡烛条得到覆盖周期时间内价格发展的良好视觉体现。但一些交易员认为, 这是一个缺点, 即图表含有时间分量, 所以他们宁愿只处理价格变化。这就是因何图表 "点线图", "Renko", "Kagi", "范围柱线", 等量图表, 等等首次出现。 

所有这些图表可以在 MetaTrader 4 里通过离线图表, 以 MQL4 编程, 以及合理意愿获得。这样就有机会创建自定义合成产品 (券商未提供或者不存在), 和平台当中未提供的非标准时间帧。为达此目的, 大多数开发者倾向于使用 DLL 调用, 且复杂的方案。在本文中, 我们将介绍如何创建不同复杂程度的 "二并一" 式的指标, 不仅无需 DLL 知识, 而且还可以很容易地在市场上作为一个产品发布, 因为它们是完全独立且完整的应用程序。

来自文章的例程可以从市场上免费下载:

  • USDx 图表 MT4 - "USDx 图表 MT4" 指标创建一个离线图表 此处绘制的是美元指数, 而非常用的柱线和蜡烛条。 
  • Renko 图表 MT4 - Renko 图表 MT4 指标创建一个离线 Renko 图表, 此处所有的柱线显示为 Renko 砖块。砖块没有阴影, 且砖块的大小在设置中指定。

创建非标准品种 和/或 周期离线图表的指标无需调用 DLL, 所有事情通过 MQL4 解决。因此, 以下操作方案已被实现: 同一指标均可操作在线和 离线图表。不过, 它将依据图表所用模式来调整其功能。

对于在线图表, 指标以维护模式运作: 它收集并汇总报价, 创建一个离线图表 (标准和非标准周期均可), 并更新它。对于离线图表, 指标操作与所有其它指标相同 — 它分析报价并构建各种对象和数字。请注意, 来自本文的例程均基于 我们的新标准脚本 PeriodConverter.mq4


1. "IndCreateOffline" 指标用于创建离线图表

我们将要调用指标 IndCreateOffline。它只有一个输入参数就是离线图表周期。我们稍后将在下面谈到它。指标 IndCreateOffline 将执行单一任务 — 创建 *.hst 文件并打开离线图表。

在此指标里将使用两个基本函数。第一个函数仅使用一次 — 用于创建 *.hst 文件和其头部。第二个所需函数将报价写入 *.hst 文件。

1.1. 编辑指标标题行

即使您现在想偷点儿懒, 它依然会首先强制添加文件描述。一段时间之后, 它将帮助我们记住此指标的目的。

#property version   "1.00"
#property description "指标创建离线图表"
#property strict
#property indicator_chart_window

理论上, 离线图表可由任何周期创建。但是, 出于安全起见, 我们将限制交易者的野望, 例如, 他可能对周期 10000000 感兴趣。让我们输入所有四个可选项的枚举 - 二, 三, 四, 六分钟:

#property indicator_chart_window
//+------------------------------------------------------------------+
//| 离线图表周期枚举                                                  |
//+------------------------------------------------------------------+
enum ENUM_OFF_TIMEFRAMES
  {
   M2=2,                      // 周期 M2
   M3=3,                      // 周期 M3
   M4=4,                      // 周期 M4
   M6=6,                      // 周期 M6
  };
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                               |

下一步涉及添加全局变量到头部 (不要与终端的全局变量混淆):

   M6=6,                      // 周期 M6
  };
//--- 输入参数
input ENUM_OFF_TIMEFRAMES  ExtOffPeriod=M6;
//---
bool     crash=false;         // false -> 代码出错
int      HandleHistory=-1;    // 已打开的 "*.hst" 文件句柄
datetime time0;               //
ulong    last_fpos=0;         //
long     last_volume=0;       //
int      periodseconds;       //
int      i_period;            //
MqlRates rate;                //
long     ChartOffID=-1;       // 离线图表的标识符
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                               |

1.2. 检查图表类型

我们的指标 IndCreateOffline 需要 在线图表模式 启动, 因为这是确保数据无误的唯一途径。为了识别指标设置的图表类型, 使用了属性 CHART_IS_OFFLINE。跟随 OnCalculate(), 函数 IsOffline 将会返回添加的图表类型:

//--- 返回 prev_calculated 的值用于下次调用
   return(rates_total);
  }
//+------------------------------------------------------------------+ 
//| 函数检查图表的离线模式                                             | 
//+------------------------------------------------------------------+ 
bool IsOffline(const long chart_ID=0)
  {
   bool offline=ChartGetInteger(chart_ID,CHART_IS_OFFLINE);
   return(offline);
  }
//+------------------------------------------------------------------+

调用 IsOffline 函数将会在 OnInit() 里执行:

int OnInit()
  {
   if(!IsOffline(ChartID()) && Period()!=PERIOD_M1)
     {
      Print("在线图表周期必须是 \"M1\"!");
      crash=true;
     }
//---
   return(INIT_SUCCEEDED);
  }

请记住, 如果指标用于在线图表 (IsOffline(ChartID())==false), 且周期不等于 PERIOD_M1, 则在此情况下 true 值被赋予 crash  变量。结果: 当 crash==true, 指标简单地保留于在线图表上, 且没有其它动作。此种情况下, 在 "专家" 栏将会出现以下消息:

IndCreateOffline EURUSD,H1: 在线图表周期必须等于 "M1"!

指标将保留于在线图表上等待, 直到用户将周期改为 PERIOD_M1。

为什么 PERIOD_M1 对于我们如此重要?这涉及两个要点。

要点 1: 生成离线图表的总周期。让我们来研究例程 PeriodConverter.mq4 脚本, 此处  离线图表总周期已被计算:

#property show_inputs
input int InpPeriodMultiplier=3; // 周期乘数
int       ExtHandle=-1;
//+------------------------------------------------------------------+
//| 脚本程序启动函数                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   datetime time0;
   ulong    last_fpos=0;
   long     last_volume=0;
   int      i,start_pos,periodseconds;
   int      cnt=0;
//---- 历史数据头
   int      file_version=401;
   string   c_copyright;
   string   c_symbol=Symbol();
   int      i_period=Period()*InpPeriodMultiplier;
   int      i_digits=Digits;
   int      i_unused[13];
   MqlRates rate;
//---  

利用这些输入参数, 加载脚本的在线图表周期等于 "PERIOD_M3"。而数值 InpPeriodMultiplier=3 则是我们期望的离线图表周期 3。不过, 实际上我们收到的周期是 9:

   i_period=Period()*InpPeriodMultiplier=3*3=9

因此, 为了得到周期 3, 在线图表必须使用 PERIOD_M1。 

要点 2: 将报价历史数据写入文件。当生成历史文件时, 使用的数据来自时间序列 Open[], Low[], High[], Volume[], Time[] 的数组。它们全部使用 当前图表, 以及当前周期 的数据。还有, 什么能比依据 "PERIOD_M1" 图表数据来生成任何人工周期更加精确?您的答案是正确的: 只有 PERIOD_M1 周期的图表。 

以上指标的修改说明可在 IndCreateOfflineStep1.mq4 文件里找到。

1.3. 创建历史文件头的函数

我们将调用负责创建历史文件头的函数, 其名为 CreateHeader():

bool CreateHeader(
   const ENUM_OFF_TIMEFRAMES offline_period // 离线图表周期
   );

参数

offline_period

[in]  离线图表周期。 

返回值

true, 如果历史文件创建成功, 或者 false — 出错的情况下。

完整函数清单:

//+------------------------------------------------------------------+ 
//| 函数检查图表的离线模式                                             | 
//+------------------------------------------------------------------+ 
bool IsOffline(const long chart_ID=0)
  {
   bool offline=ChartGetInteger(chart_ID,CHART_IS_OFFLINE);
   return(offline);
  }
//+------------------------------------------------------------------+
//| 创建历史数据头                                                    |
//+------------------------------------------------------------------+
bool CreateHeader(const ENUM_OFF_TIMEFRAMES offline_period)
  {
//---- 历史数据头
   int      file_version=401;
   string   c_copyright;
   string   c_symbol=Symbol();
   i_period=Period()*offline_period;
   int      i_digits=Digits;
   int      i_unused[13];
//---  
   ResetLastError();
   HandleHistory=FileOpenHistory(c_symbol+(string)i_period+".hst",FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ|FILE_ANSI);
   if(HandleHistory<0)
     {
      Print("出错 打开 ",c_symbol+(string)i_period,".hst 文件 ",GetLastError());
      return(false);
     }
   c_copyright="(C)opyright 2003, MetaQuotes Software Corp.";
   ArrayInitialize(i_unused,0);
//--- 输出历史文件头
   FileWriteInteger(HandleHistory,file_version,LONG_VALUE);
   FileWriteString(HandleHistory,c_copyright,64);
   FileWriteString(HandleHistory,c_symbol,12);
   FileWriteInteger(HandleHistory,i_period,LONG_VALUE);
   FileWriteInteger(HandleHistory,i_digits,LONG_VALUE);
   FileWriteInteger(HandleHistory,0,LONG_VALUE);
   FileWriteInteger(HandleHistory,0,LONG_VALUE);
   FileWriteArray(HandleHistory,i_unused,0,13);
   return(true);
  }
//+------------------------------------------------------------------+

除了创建 "*.hst" 历史文件和文件头 (开始的几个服务字串), 已创建文件的句柄记忆在  CreateHeader() 函数的 HandleHistory 变量。

1.4. 首笔报价记录至 *.hst 文件

在创建 *.hst 文件并填充其头部之后, 我们必须写入第一条记录到文件 — 即, 文件必须以当前报价填写。函数 FirstWriteHistory() (您可以在指标里看到它) 负责输出第一条记录到文件。

在我们的指标里, 何时发生 "第一条记录" 呢?逻辑上假设它发生在指标首次加载期间。

指标首次加载时可以 (且应该) 基于 prev_calculated 变量控制。变量 prev_calculated==0 表示这是首次加载。但是与此同时 prev_calculated==0 也可暗示, 这不是首次加载我们的指标, 之前或缺失的报价被加入到历史数据中。我们将会讨论当 OnCalculate() 代码更新历史数据时需要做什么。

1.5. 输出在线报价

当创建、填充 *.hst 文件头以及首条记录之后, 我们可以处理输出在线报价。函数 CollectTicks() 负责此项任务 (您可以在指标里看到它)。

以上指标的修改说明可在 IndCreateOfflineStep2.mq4 文件里找到。 

1.6. 编辑 OnCalculate() 函数

让我们来引入 first_start 变量。它将在首次启动之后保存 true 值。换言之, 若 first_start==true 我们就会知道我们的指标尚未创建 *.hst 文件。

//--- 输入参数
input ENUM_OFF_TIMEFRAMES  ExtOffPeriod=M6;
//---
bool     first_start=true;    // true -> 这是首次启动
bool     crash=false;         // false -> 代码出错

在我们的指标里 OnCalculate() 函数的算法:

算法

图例. 1. 函数 OnCalculate() 的算法 

指标的最终版本可在 IndCreateOffline.mq4 文件里找到。 

 

2. 在离线图表上启动指标

2.1. 利用 ChartSetSymbolPeriod() 更新离线图表 

重要: 对于替代 ChartRedraw() 更新离线图表, ChartSetSymbolPeriod() 应采用当前参数调用。在 CollectTicks() 函数里执行调用 ChartSetSymbolPeriod(), 其间隔不可超过每三秒钟一次。

还有一处细节应予考虑: 指标加载到离线图表上时, 其 OnCalculate() 函数在每次更新时将会收到 prev_calculated==0。这必须要记住。考虑到这种特殊性, 一种方法解释如下。 

2.2. 维护模式 

我们想要得到: 同一指标必须可以操作离线和在线图表。指标的行为变化取决于所用图表。当指标处于在线图表上时, 则其功能类似于我们前面已经研究的 IndCreateOffline.mq4 指标。然而, 在离线图表上, 它如同标准指标一样开始操作。

因此, 我们应当调用我们的指标 IndMACDDoubleDuty — 它将会基于上述 IndCreateOffline.mq4 和标准指标 MACD.mq4 创建。请准备一份未来指标的草案: 在 MetaEditor 里打开 IndCreateOffline.mq4 文件, 选择 "文件" -> "保存为..." 并输入指标的名称 — IndMACDDoubleDuty.mq4。 

指标说明也应一并添加 — 现在我们的指标创建离线图表并有一个双精度数值:

//+------------------------------------------------------------------+
//|                                            IndMACDDoubleDuty.mq4 |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property description "指标创建离线图表。"
#property description "可以操作在线和离线图形。"
#property strict
#property indicator_chart_window

如果它在周期为 PERIOD_M1 的在线图表上启动, 则指标将以维护模式运作。在此模式, 它执行以下功能:

  • 创建并填写 *.hst 文件;
  • 基于 *.hst 文件打开离线图表;
  • 添加历史数据至 *.hst 文件, 并在抵达的报价之上更新离线图表。
为了记忆指标运作于何种模式, 我们打算引入 mode_offline 变量:
//--- 输入参数
input ENUM_OFF_TIMEFRAMES  ExtOffPeriod=M6;
//---
bool     first_start=true;    // true -> 这是首次启动
bool     crash=false;         // false -> 代码出错
bool     mode_offline=true;   // true -> 处于离线图表上
int      HandleHistory=-1;    // 已打开的 "*.hst" 文件句柄
datetime time0;               //

相应地, OnInit() 将有略微变化:

int OnInit()
  {
   mode_offline=IsOffline(ChartID());
   if(!mode_offline && Period()!=PERIOD_M1)
     {
      Print("在线图表周期必须是 \"M1\"!");
      crash=true;
     }
//---
   return(INIT_SUCCEEDED);
  }

修改 OnCalculate():

//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   if(crash)
      return(rates_total);

   if(!mode_offline) // 运作于在线图表 
     {
      if(prev_calculated==0 && first_start) // 首次启动
        {
         .
         .
         .
         first_start=false;
        }
      //---
      CollectTicks();
     }
//--- 返回 prev_calculated 的值用于下次调用
   return(rates_total);
  }

在此阶段, 当指标被加载到周期为 PERIOD_M1 的在线图表时, 它切换到维护模式, 并创建离线图表。

以上指标的修改说明可在 IndMACDDoubleDutyStep1.mq4 文件里找到。 

2.3. 拷贝指标到离线图表

我们将在 IndMACDDoubleDuty 指标里添加新的功能。现在, 当运作于维护模式时, 指标必须转移其拷贝至创建的离线图表。以下函数  ChartSaveTemplateChartApplyTemplate 将用来协助它。算法 OnCalcalculate() 如下所示:

algorithm_2

Fig. 2. 函数 OnCalculate() 的算法 

让我们在 OnCalculate() 代码里加入更多的功能:

         else
            Print(__FUNCTION__,"打开离线图表 id=",ChartOffID);
         ResetLastError();
         if(!ChartSaveTemplate(0,"IndMACDDoubleDuty"))
           {
            Print("出错 保存模板: ",GetLastError());
            first_start=false;
            crash=true;
            return(rates_total);
           }
         ResetLastError();
         if(!ChartApplyTemplate(ChartOffID,"IndMACDDoubleDuty"))
           {
            Print("出错 应用模板: ",GetLastError());
            first_start=false;
            crash=true;
            return(rates_total);
           }
        }
      //---
      if(prev_calculated==0 && !first_start) // 深度历史数据下载或历史数据漏洞补全

在被放置到 PERIOD_M1 周期的在线图表上之后, 我们的指标切换到维护模式, 创建离线图表并拷贝其自身至此。

以上指标的修改说明可在 IndMACDDoubleDutyStep2.mq4 文件里找到。  

2.4. 集成到 MACD.mq4

我们的指标已经将其自身放置到离线图表上了, 但尚未显示或计数任何东西。我们将修改它: 让我们来集成我们的指标至标准的 MACD.mq4。 

首先, 我们将 MACD.mq4 指标的输入参数放置到我们的代码里:

#property strict

#include <MovingAverages.mqh>

//--- MACD 指标设置
#property  indicator_separate_window
#property  indicator_buffers 2
#property  indicator_color1  Silver
#property  indicator_color2  Red
#property  indicator_width1  2
//--- 指标参数
input int InpFastEMA=12;   // 快速 EMA 周期
input int InpSlowEMA=26;   // 慢速 EMA 周期
input int InpSignalSMA=9;  // 信号 SMA 周期
//--- 指标缓存区
double    ExtMacdBuffer[];
double    ExtSignalBuffer[];
//--- 输入参数标志
bool      ExtParameters=false;
//+------------------------------------------------------------------+
//| 离线图表周期枚举                                                  |
//+------------------------------------------------------------------+

然后我们添加代码至 OnInit()。此处应当注意的是, MACD 指标的参数初始化在任何条件下都必须执行 — 离线和在线图表都要, 即使在线图表周期不同于 PERIOD_M1 时。

int OnInit()
  {
   mode_offline=IsOffline(ChartID());
   if(!mode_offline && Period()!=PERIOD_M1)
     {
      Print("在线图表周期必须是 \"M1\"!");
      crash=true;
     }
//--- 初始 MACD 指标
   IndicatorDigits(Digits+1);
//--- 绘图设置
   SetIndexStyle(0,DRAW_HISTOGRAM);
   SetIndexStyle(1,DRAW_LINE);
   SetIndexDrawBegin(1,InpSignalSMA);
//--- 指标缓存区映射
   SetIndexBuffer(0,ExtMacdBuffer);
   SetIndexBuffer(1,ExtSignalBuffer);
//--- 数据窗口名和指标子窗口标签
   IndicatorShortName("MACD("+IntegerToString(InpFastEMA)+","+IntegerToString(InpSlowEMA)+","+IntegerToString(InpSignalSMA)+")");
   SetIndexLabel(0,"MACD");
   SetIndexLabel(1,"Signal");
//--- 检查输入参数
   if(InpFastEMA<=1 || InpSlowEMA<=1 || InpSignalSMA<=1 || InpFastEMA>=InpSlowEMA)
     {
      Print("错误的输入参数");
      ExtParameters=false;
      return(INIT_FAILED);
     }
   else
      ExtParameters=true;
//---
   return(INIT_SUCCEEDED);
  }

下一步会涉及到在 OnCalculate() 的开头略微进行一点修改。如果我们处于在线图表且其周期不等于 PERIOD_M1, 我们要有一次机会来计算 MACD 参数。它就是:

const long &volume[],
                const int &spread[])
  {
//---
   if(crash)
      return(rates_total);

   if(!mode_offline) // 运作于在线图表 
     {
      if(prev_calculated==0 && first_start) // 首次启动
        {

 以及它将是:

const long &volume[],
                const int &spread[])
  {
//---
   if(!mode_offline && !crash) // 运作于在线图表 
     {
      if(prev_calculated==0 && first_start) // 首次启动
        {

之后我们在 OnCalculate() 的结束之处添加计算 MACD 指标参数的代码:

         FirstWriteHistory(ExtOffPeriod);
         first_start=false;
        }
      //---
      CollectTicks();
     }
//---
   int i,limit;
//---
   if(rates_total<=InpSignalSMA || !ExtParameters)
      return(0);
//--- 最后计数的柱线将被重计数
   limit=rates_total-prev_calculated;
   if(prev_calculated>0)
      limit++;
//--- macd 计数在第一个缓存区
   for(i=0; i<limit; i++)
      ExtMacdBuffer[i]=iMA(NULL,0,InpFastEMA,0,MODE_EMA,PRICE_CLOSE,i)-
                       iMA(NULL,0,InpSlowEMA,0,MODE_EMA,PRICE_CLOSE,i);
//--- 信号线计数在第二个缓存区
   SimpleMAOnBuffer(rates_total,prev_calculated,0,InpSignalSMA,ExtMacdBuffer,ExtSignalBuffer);
//--- 返回 prev_calculated 的值用于下次调用
   return(rates_total);
  }

2.5. 在离线图表上重计算财经指标

我来提醒您一个在早前 章节 2 里描述过的一个要点:

还有一处细节应予考虑: 指标加载到离线图表上时, 其 OnCalculate() 函数在每次更新时将会收到 prev_calculated==0。这必须要记住。考虑到这种特殊性, 一种方法解释如下。 

还有一处要提醒: prev_calculated==0 的值在 OnCalculate() 里可以代表两种情形:

  1. 既可以是指标】的第一次启动;
  2. 或者历史数据已下载。

在两种情况里指标必须 重新计算图表上的所有柱线。当然它应该只能完成一次 (当加载时) — 这是正常的。但是在离线图表上, 我们将在每次更新时得到 prev_calculated==0 (大约每 2-3 秒钟一次), 并且指标将重新计算所有柱线。这需要极高的资源消耗。因此, 我们将使用一个技巧: 当指标处于离线图表上, 它将存储并比较柱线总数 (rates_total 变量), 以及图表最右侧柱线的时间。

步骤 1: 在 OnCalculate() 的开头, 我们将声明两个 静态变量 和一个哑元变量:

                const long &volume[],
                const int &spread[])
  {
//---
   static int static_rates_total=0;
   static datetime static_time_close=0;
   int pseudo_prev_calculated=prev_calculated;
//---
   if(!mode_offline && !crash) // 运作于在线图表 
     {

步骤 2: 我们将在计算指标数值的代码块里改变 prev_calculated 变量为 pseudo_prev_calculated:

      CollectTicks();
     }
//---
   int i,limit;
//---
   if(rates_total<=InpSignalSMA || !ExtParameters)
      return(0);
//--- 最后计数的柱线将被重计数
   limit=rates_total-pseudo_prev_calculated;
   if(pseudo_prev_calculated>0)
      limit++;
//--- macd 计数在第一个缓存区
   for(i=0; i<limit; i++)
      ExtMacdBuffer[i]=iMA(NULL,0,InpFastEMA,0,MODE_EMA,PRICE_CLOSE,i)-
                       iMA(NULL,0,InpSlowEMA,0,MODE_EMA,PRICE_CLOSE,i);
//--- 信号线计数在第二个缓存区
   SimpleMAOnBuffer(rates_total,pseudo_prev_calculated,0,InpSignalSMA,ExtMacdBuffer,ExtSignalBuffer);
//---
   if(mode_offline) // 运作于离线图表 

步骤 3: 如果指标运作于离线图表, 我们将计算哑元变量的数值。 

      CollectTicks();
     }
//---
   if(mode_offline) // 运作于离线图表 
     {
      if(time[0]>static_time_close) // 新柱线
        {
         if(static_time_close==0)
            pseudo_prev_calculated=0;
         else // 搜索 time[0]==static_time_close 时的柱线 
           {
            for(int i=0;i<rates_total;i++)
              {
               if(time[i]==static_time_close)
                 {
                  pseudo_prev_calculated=rates_total-i;
                  break;
                 }
              }
           }
        }
      else
        {
         pseudo_prev_calculated=rates_total;
        }
      //---
      static_rates_total=rates_total;
      static_time_close=time[0];
     }
//---
   int i,limit;

现在, 我们的指标在离线图表上经济地计算其数值。此外, 它可以在此模式下正确处理断线: 仅计算断线后新增柱线。

2.6. 加载数据。离线图表

现在, 我们只需要搞清楚做什么, 如果是带有我们指标的在线图表下载了历史数据 (此处 prev_calculated==0)。我建议通过 终端的全局变量 来重新解决这种情形。操作算法如下: 如果 在线图表上 收到 prev_calculated==0 (无所谓是首次启动或者是历史数据下载), 我们只创建一个全局变量。在离线图表上 的指标每次更新时检查全局变量: 如果是第一种情况 (它意味着在线图表 prev_calculated==0), 则指标将全部重计算且将删除全局变量。

我们将把变量添加在指标的头部, 此处是未来存储于终端的全局变量的名称, 并在 OnInit() 里生成这个名称:

MqlRates rate;                //
long     ChartOffID=-1;       // 离线图表的标识符
string   NameGlVariable="";   // 全局变量名
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                               |
//+------------------------------------------------------------------+
int OnInit()
  {
   mode_offline=IsOffline(ChartID());
   if(!mode_offline && Period()!=PERIOD_M1)
     {
      Print("在线图表周期必须是 \"M1\"!");
      crash=true;
     }
   NameGlVariable=Symbol()+(string)ExtOffPeriod;
//--- 初始 MACD 指标
   IndicatorDigits(Digits+1);
//--- 绘图设置

我们将添加在终端里创建全局变量的代码, 如果 prev_calculated==0 条件满足, 且指标处于在线图表上:

         if(!ChartApplyTemplate(ChartOffID,"IndMACDDoubleDuty"))
           {
            Print("出错 应用模板: ",GetLastError());
            first_start=false;
            crash=true;
            return(rates_total);
           }
         //---
         ResetLastError();
         if(GlobalVariableSet(NameGlVariable,0.0)==0) // 创建一个新的全局变量
           {
            Print("创建新的全局变量失败 ",GetLastError());
           }
        }
      //---
      if(prev_calculated==0 && !first_start) // 深度历史数据下载或历史数据漏洞补全
        {
         Print("深度历史数据下载或历史数据漏洞补全。first_start=",first_start);
         if(CreateHeader(ExtOffPeriod))
            first_start=false;
         else
           {
            crash=true;
            return(rates_total);
           }
         //---
         FirstWriteHistory(ExtOffPeriod);
         first_start=false;
         //---
         ResetLastError();
         if(GlobalVariableSet(NameGlVariable,0.0)==0) // 创建一个新的全局变量
           {
            Print("创建新的全局变量失败 ",GetLastError());
           }
        }
      //---
      CollectTicks();

最后的变化: 从离线图表的指标里检查全局变量:

      else
        {
         pseudo_prev_calculated=rates_total;
        }
      //---
      if(GlobalVariableCheck(NameGlVariable))
        {
         pseudo_prev_calculated=0;
         GlobalVariableDel(NameGlVariable);
        }
      //---
      Print("rates_total=",rates_total,"; prev_calculated=",
            prev_calculated,"; pseudo_prev_calculated=",pseudo_prev_calculated);
      static_rates_total=rates_total;
      static_time_close=time[0];
     }
//---
   int i,limit;

含有最后变化的最终指标版本: IndMACDDoubleDuty.mq4。  

 

3. 指标显示 Renko 柱线

Renko 柱线的放置所用技术如同要点 1. "IndCreateOffline" 指标将会创建一个离线图表 以及 2. 在离线图表上启动指标。最后, 我们得到一个离线图表, 但只有在这种情况下, *.hst 历史文件将包含相同大小的柱线, 并称之为砖块。砖块的大小在指标设置中设置, 并以点数测量。

Renko 柱线  

图例. 3. Renko 柱线 

在我们开始之前, 应了解一些用来绘制 Renko 柱线的 *.hst 历史文件的形成规则。

3.1. 绘制范围柱线的规则

规则 1: 写入 *.hst 历史文件的 OHLC 必须正确, 尤其是作为参考的最高价和最低价数值。否则, 终端将不会显示不正确的日志。正确和不正确地写入阳线到 *.hst 历史文件的例子:

正确和不正确的日志例子  

图例. 4. 正确和不正确的日志例子 

规则 2: 即使我们创建的范围柱线没有时间锚点, 但 *.hst 历史文件格式依然要求时间参数 — 开始的时间周期。所以它会强制写入时间参数。

规则 3: 所有柱线的时间参数应该不同。如果您为所有柱线写入相同的时间参数, 则日志将不会正确且终端不会显示图表。此处还有一个吸引人的特点: 时间参数可以精确到秒。例如, 我们写入一根柱线的时间参数=2016.02.10 09:08:00, 而下一根柱线的时间参数=2016.02.10 09:08:01

3.2. 在历史数据上生成砖块

这种方法不是一个完美的算法。我用了一种简化的方法, 因为这篇文章的主要目的是展示如何形成 *.hst 历史文件。在 "IndRange.mq4" 指标里当前周期的 High[i] 和 Low[i] 值在基于历史数据绘制砖块时已然分析过。所以如果 "IndRange.mq4" 指标加载到 M5 周期图表, 则 "IndRange.mq4" 指标将在首次启动时分析当前 M5 的历史数据。  

当然, 如果您愿意, 您可以根据历史数据将绘图算法时尚化, 并参考最低时间帧的价格走势 — М1。操作的一般规划:

  • 如果 High[i] 高于前一块砖大小的最高值, 则一块或多块砖将高于前一块砖绘制;
  • 如果 Low[i] 低于前一块砖大小的最低值, 则一块或多块砖将低于前一块砖绘制;

前一块砖为阳

图例. 5. 前一块砖为阳     


 前一块砖为阴

图例. 6. 前一块砖为阴

有趣的是: 柱线坐标 (开盘价, 最高价, 最低价, 收盘价, 时间和交易量) 保存在 rate 结构里, 这个结构在指标头部里声明。

MqlRates rate;                   //

并且这种结构不会清零, 而是简单地在 *.hst 历史文件里为每笔记录重写。由于这一点, 很容易实现如图例. 5. 和图例. 6. 的算法, 并且可以给出通用公式:

  • 当 High[i] 高于 前一块砖大小的最高值 (它是阳或阴没有区别), 新柱线的坐标相应计算:  

开盘价 = 最高价; 最低价 = 开盘价; Close = 最低价 + Renko 大小; 最高价 = 收盘价

 

  • 当 Low[i] 低于 前一块砖大小的最低值 (它是阳或阴没有区别), 新柱线的坐标相应计算:  

最低价 = 开盘价最高价 = 开盘价收盘价 = 最高价 - Renko 大小最低价 = 收盘价

这就是为何这些公式出现在 "IndRange.mq4" 指标的 FirstWriteHistory() 函数:

//+------------------------------------------------------------------+
//| 首次写入历史                                                      |
//+------------------------------------------------------------------+
bool FirstWriteHistory(const int offline_period)
  {
   int      i,start_pos;
   int      cnt=0;
//--- 写入历史文件
   periodseconds=offline_period*60;
   start_pos=Bars-1;
   rate.open=Open[start_pos];
   rate.close=Close[start_pos];
   rate.low=Low[start_pos];
   rate.high=High[start_pos];
   rate.tick_volume=(long)Volume[start_pos];
   rate.spread=0;
   rate.real_volume=0;
//--- 规范化开盘时间
   rate.time=D'1980.07.19 12:30:27';
   for(i=start_pos-1; i>=0; i--)
     {
      if(IsStopped())
         break;
      while((High[i]-rate.high)>SizeRenko*Point())
        {
         rate.time+=1;
         rate.open=rate.high;
         rate.low=rate.open;
         rate.close=NormalizeDouble(rate.low+SizeRenko*Point(),Digits);
         rate.high=rate.close;
         last_fpos=FileTell(HandleHistory);
         uint byteswritten=FileWriteStruct(HandleHistory,rate);
         //--- 检查写入的字节数量 
         if(byteswritten==0)
            PrintFormat("错误 读数据。错误代码=%d",GetLastError());
         else
            cnt++;
        }
      while((Low[i]-rate.low)<-SizeRenko*Point())
        {
         rate.time+=1;
         rate.open=rate.low;
         rate.high=rate.open;
         rate.close=NormalizeDouble(rate.high-SizeRenko*Point(),Digits);
         rate.low=rate.close;
         last_fpos=FileTell(HandleHistory);
         uint byteswritten=FileWriteStruct(HandleHistory,rate);
         //--- 检查写入的字节数量 
         if(byteswritten==0)
            PrintFormat("错误 读数据。错误代码=%d",GetLastError());
         else
            cnt++;
        }
     }
   FileFlush(HandleHistory);
   PrintFormat("%d 条记录被写入",cnt);
   return(true);
  }

 

指标 "IndRange.mq4" 形成 *.hst 历史文件名的规则如下: "当前品名"+"7"+".hst"。例如, 对于 "EURUSD", 历史文件将命名为 "EURUSD7.hst"。

3.3. 在线指标操作

当指标在线操作时, 应该分析零号 (最右侧) 柱线的 Close[0] 收盘价格, 而非 High[i] 价格:

//+------------------------------------------------------------------+
//| 收集分时价                                                        |
//+------------------------------------------------------------------+
bool CollectTicks()
  {
   static datetime last_time;//=TimeLocal()-5;
   long     chart_id=0;
   datetime cur_time=TimeLocal();
//---
   while((Close[0]-rate.high)>SizeRenko*Point())
     {
      rate.time+=1;
      rate.open=rate.high;
      rate.low=rate.open;
      rate.close=NormalizeDouble(rate.low+SizeRenko*Point(),Digits);
      rate.high=rate.close;
      last_fpos=FileTell(HandleHistory);
      uint byteswritten=FileWriteStruct(HandleHistory,rate);
      //--- 检查写入的字节数量  
      if(byteswritten==0)
         PrintFormat("错误 读数据。错误代码=%d",GetLastError());
     }
   while((Close[0]-rate.low)<-SizeRenko*Point())
     {
      rate.time+=1;
      rate.open=rate.low;
      rate.high=rate.open;
      rate.close=NormalizeDouble(rate.high-SizeRenko*Point(),Digits);
      rate.low=rate.close;
      last_fpos=FileTell(HandleHistory);
      uint byteswritten=FileWriteStruct(HandleHistory,rate);
      //--- 检查写入的字节数量 
      if(byteswritten==0)
         PrintFormat("错误 读数据。错误代码=%d",GetLastError());
     }
//--- 窗口刷行频率不要超过每 2 秒一次
   if(cur_time-last_time>=3)
     {
      FileFlush(HandleHistory);
      ChartSetSymbolPeriod(ChartOffID,Symbol(),i_period);
      last_time=cur_time;
     }
   return(true);
  }


4. 指标创建非标准品种 — USDx 美元指数

关于 "IndUSDx.mq4" 指标的算法的要点: 美元指数指标并不意味着指数计算公式的绝对正确性。对于我们来说更重要的是展示如何使用非标准品种来形成 *.hst 历史文件。作为一个结果, 我们将得到一个离线图表, 其柱线将显示计算后的美元指数。指标 "IndUSDx.mq4" 只创建一次离线图表: 无论是首次加载指标到图表, 或改变图表周之后。

计算美元指数的公式以及品种集合的选取基于代码: 简单美元指数指标。 

"EURUSD", "GBPUSD", "USDCHF", "USDJPY", "AUDUSD", "USDCAD" 和 "NZDUSD" 的时间, 开盘价, 收盘价数据必须可以得到, 它们将用于计算美元指数的公式。为了便于保存和引用数据, 引入 OHLS 结构 (在指标头部声明):

//--- 结构
struct   OHLS
  {
   datetime          ohls_time[];
   double            ohls_open[];
   double            ohls_close[];
  };

在 OHLS 结构里, 数组按时间排序, 开盘价和收盘价作为元素使用。在下面的结构描述里, 一些对象被立即声明 — OHLS 结构将用来进行已计算品种数据的排序:

//--- 结构
struct   OHLS
  {
   datetime          ohls_time[];
   double            ohls_open[];
   double            ohls_close[];
  };
OHLS     OHLS_EURUSD,OHLS_GBPUSD,OHLS_USDCHF,OHLS_USDJPY,OHLS_AUDUSD,OHLS_USDCAD,OHLS_NZDUSD;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                               |
//+------------------------------------------------------------------+
int OnInit()

函数 SelectSymbols() 在 OnInit() 里调用:

//+------------------------------------------------------------------+
//| 选择品种                                                          |
//+------------------------------------------------------------------+
bool SelectSymbols()
  {
   bool rezult=true;
   string arr_symbols[7]={"EURUSD","GBPUSD","USDCHF","USDJPY","AUDUSD","USDCAD","NZDUSD"};
   for(int i=0;i<ArraySize(arr_symbols);i++)
      rezult+=SymbolSelect(arr_symbols[i],true);
//---
   return(rezult);
  }

函数 SelectSymbols() 通过 SymbolSelect 的帮助将参与美元指数公式的品种选择到市场观察窗口。

在 OnCalculate() 里, CopyCloseSymbols() 函数在首次启动时调用。所需的已计算品种数据和品种结构均在此填写:

//+------------------------------------------------------------------+
//| 拷贝收盘品种                                                      |
//+------------------------------------------------------------------+
bool CopyCloseSymbols(const int rates)
  {
   int copied=0;
   int copy_time=0,copy_open=0,copy_close=0;
   copy_time=CopyTime("EURUSD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("EURUSD",Period(),0,rates,OHLS_EURUSD.ohls_open);
   copy_close=CopyClose("EURUSD",Period(),0,rates,OHLS_EURUSD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("品种 \"EURUSD\". 得到时间 ",copy_time,", 得到开盘价 ",copy_open,", 得到收盘价 ",copy_close," 总计 ",rates);
      return(false);
     }

   copy_time=CopyTime("GBPUSD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("GBPUSD",Period(),0,rates,OHLS_GBPUSD.ohls_open);
   copy_close=CopyClose("GBPUSD",Period(),0,rates,OHLS_GBPUSD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("品种 \"GBPUSD\". 得到时间 ",copy_time,", 得到开盘价 ",copy_open,", 得到收盘价 ",copy_close," 总计 ",rates);
      return(false);
     }

   copy_time=CopyTime("USDCHF",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("USDCHF",Period(),0,rates,OHLS_USDCHF.ohls_open);
   copy_close=CopyClose("USDCHF",Period(),0,rates,OHLS_USDCHF.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("品种 \"USDCHF\". 得到时间 ",copy_time,", 得到开盘价 ",copy_open,", 得到收盘价 ",copy_close," 总计 ",rates);
      return(false);
     }

   copy_time=CopyTime("USDJPY",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("USDJPY",Period(),0,rates,OHLS_USDJPY.ohls_open);
   copy_close=CopyClose("USDJPY",Period(),0,rates,OHLS_USDJPY.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("品种 \"USDJPY\". 得到时间 ",copy_time,", 得到开盘价 ",copy_open,", 得到收盘价 ",copy_close," 总计 ",rates);
      return(false);
     }

   copy_time=CopyTime("AUDUSD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("AUDUSD",Period(),0,rates,OHLS_AUDUSD.ohls_open);
   copy_close=CopyClose("AUDUSD",Period(),0,rates,OHLS_AUDUSD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("品种 \"AUDUSD\". 得到时间 ",copy_time,", 得到开盘价 ",copy_open,", 得到收盘价 ",copy_close," 总计 ",rates);
      return(false);
     }

   copy_time=CopyTime("USDCAD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("USDCAD",Period(),0,rates,OHLS_USDCAD.ohls_open);
   copy_close=CopyClose("USDCAD",Period(),0,rates,OHLS_USDCAD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("品种 \"USDCAD\". 得到时间 ",copy_time,", 得到开盘价 ",copy_open,", 得到收盘价 ",copy_close," 总计 ",rates);
      return(false);
     }

   copy_time=CopyTime("NZDUSD",Period(),0,rates,OHLS_EURUSD.ohls_time);
   copy_open=CopyOpen("NZDUSD",Period(),0,rates,OHLS_NZDUSD.ohls_open);
   copy_close=CopyClose("NZDUSD",Period(),0,rates,OHLS_NZDUSD.ohls_close);
   if(copy_open!=rates || copy_close!=rates || copy_time!=rates)
     {
      Print("品种 \"NZDUSD\". 得到时间 ",copy_time,", 得到开盘价 ",copy_open,", 得到收盘价 ",copy_close," 总计 ",rates);
      return(false);
     }
//---
   return(true);
  }

如果已下载的品种历史数据少于 CopyCloseSymbols() 函数给出的数值, 则显示一条消息, 包括品名和实际下载的历史数据数量。

成功填充结构的情况下, 主功能 — 调用 FirstWriteHistory() 函数以历史数据填充 *.hst 文件:

//+------------------------------------------------------------------+
//| 首次写入历史                                                      |
//+------------------------------------------------------------------+
bool FirstWriteHistory(const int rates)
  {
   int      i;
   int      cnt=0;
   rate.tick_volume=0;
   rate.spread=0;
   rate.real_volume=0;
   for(i=0;i<rates;i++)
     {
      rate.time=OHLS_EURUSD.ohls_time[i];
      rate.open=(100*MathPow(OHLS_EURUSD.ohls_open[i],0.125)+100*MathPow(OHLS_GBPUSD.ohls_open[i],0.125)+
                 100*MathPow(OHLS_USDCHF.ohls_open[i],0.125)+100*MathPow(OHLS_USDJPY.ohls_open[i],0.125)+
                 100*MathPow(OHLS_AUDUSD.ohls_open[i],0.125)+100*MathPow(OHLS_USDCAD.ohls_open[i],0.125)+
                 100*MathPow(OHLS_NZDUSD.ohls_open[i],0.125))/8.0;

      rate.close=(100*MathPow(OHLS_EURUSD.ohls_close[i],0.125)+100*MathPow(OHLS_GBPUSD.ohls_close[i],0.125)+
                  100*MathPow(OHLS_USDCHF.ohls_close[i],0.125)+100*MathPow(OHLS_USDJPY.ohls_close[i],0.125)+
                  100*MathPow(OHLS_AUDUSD.ohls_close[i],0.125)+100*MathPow(OHLS_USDCAD.ohls_close[i],0.125)+
                  100*MathPow(OHLS_NZDUSD.ohls_close[i],0.125))/8.0;

      if(rate.open>rate.close)
        {
         rate.high=rate.open;
         rate.low=rate.close;
        }
      else
        {
         rate.high=rate.close;
         rate.low=rate.open;
        }
      last_fpos=FileTell(HandleHistory);
      uint byteswritten=FileWriteStruct(HandleHistory,rate);
      //--- 检查写入的字节数量 
      if(byteswritten==0)
         PrintFormat("错误 读数据。错误代码=%d",GetLastError());
      else
         cnt++;
     }
   FileFlush(HandleHistory);
   PrintFormat("%d 条记录被写入",cnt);
   return(true);
  }

指标 "IndUSDx.mq4" 的操作结果: 

 "IndUSDx.mq4 指标

图例. 7. "IndUSDx.mq4 指标 

  

结论

事实证明, 离线和在线图表均可以用于经济地进行指标重新计算。不过, 考虑离线图表的特点, 必须输入为此目的而改变的指标代码, 即, 它应该考虑所有指标在 OnCalculate() 里更新离线图表时接收的 prev_calculate== 0 值。

本文还展示了如何生成 *.hst 历史文件, 以及使用它来彻底改变图表上显示柱线的参数。 


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

通用智能交易系统:支持挂单和对冲(第五章) 通用智能交易系统:支持挂单和对冲(第五章)
本文是对CStrategy交易引擎的进一步描述。由于交易者的广泛需要,我们向交易引擎中添加了支持挂单的相关函数。同时,最新版的MetaTrader 5现在也支持了具有对冲选项的帐户。同样的功能也添加到了CStrategy中。本文给出了使用挂单进行交易和在账户中用CStrategy类进行对冲交易的详细算法描述。
图形界面 V: 列表视图元件 (第二章) 图形界面 V: 列表视图元件 (第二章)
在前一章中,我们开发了用于创建垂直和水平滚动条的类。在本章中,我们将应用它们,我们将开发一个用于创建列表视图元件的类,它的一个组成部分将是一个垂直滚动条。
图形界面 V: 组合框控件 (第三章) 图形界面 V: 组合框控件 (第三章)
在本系列第五部分的前两章中,我们开发了用于创建滚动条和列表视图的类,在本章中,我们将讨论创建组合框(combobox)控件的类,这也是一个组合控件,包含了第五部分前面章节中讨论的一些元件。
图形界面 V: 垂直与水平滚动条 (第一章) 图形界面 V: 垂直与水平滚动条 (第一章)
我们仍然在讨论在MetaTrader环境下开发创建图形界面库的开发,在本系列第五部分的第一篇文章中,我们将开发用于创建垂直与水平滚动条的类。