请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3364712 新帖:0

指标间的数据交换:易如反掌!

醒掌天下权发表于:4 月 17 日 18:54回复(1)

简介

我们之所以欣赏初学者,是因为他们固执地不愿意使用搜索,而诸如“常见问题解答”、“初学者指南”或“妇孺皆知的问题解答”的话题随处可见。他们真正的任务是提出问题,比如“如何...”、“有没有可能...”,但他们常常获得“没”、“不可能”等诸如此类的答案。

恒古流传的警句“永不说不”一直激励着科学家、工程师和程序员思考和推陈出新。

1. 问题定义

例如,下文引用自 MQL4 社区论坛的一个主题(译自俄语):

 ...有两个指标(我们称之为 A 和 B)。像往常一样,指标 A 使用直接来自价格图表的数据,B 使用指标 A 的数据。问题如下:如何使用指标 A 的数据(已经附加)而不是 iCustom(“指标 A”,...)动态执行 B 的计算,即,如果我更改了指标 A 的某些设置,更改应该在指标 B 的参数更改上体现出来。

或换言之:

...假设我们已将“移动平均线”附加至图表。现在要如何才能直接访问其数据缓冲区?

又如:

... 如果我使用 iCustom 调用某项指标,即使该指标在调用前已经加载也仍将重新加载。是否有什么方法防止指标重复加载?

诸如此类的问题还有很多,并且被反复问及 - 提问者不仅仅是初学者。概括而言,问题在于 MetaTrader 没有办法访问自定义指标数据而不使用 iCustom (MQL4) 或 iCustom + CopyBuffer 绑定 (MQL5)。但是用 MQL 代码编写一些函数以使用来自指定图表的数据获取数据或进行计算,对于开发下一部大作而言是十分诱人的。

使用上文中提到的标准函数并不方便:例如,在 MQL4 中,当调用 iCustom 时,将为每个调用方创建指标的副本;而对于 MQL5,通过使用句柄使问题得到了部分解决,现在仅为原本执行一次计算。但句柄也不是万灵药:如果涉及的指标占用大量的计算资源,则终端互斥等待几乎是通过每次重新初始化的十几秒钟保证。

我记得有几个提供的方法可访问原本,如下所示:

  • 在物理磁盘或内存上映射文件;
  • 经由 DLL 共享内存使用以用于数据交换;
  • 使用客户端的全局变量以用于数据交换及其存储。

此外还有属于上述方法的变体的其他一些方法,以及一些不常用的方法,如套接字、邮件槽等(有一种基本方法在“EA 交易”中经常使用,即直接将指标计算转移至“EA 交易”代码,但这远远超出了本文论述的范围)。

在笔者看来,所有这些方法都有可取之处,但它们有一个共同的劣势:数据首先是复制到某个位置,然后才分发给其他方。首先,这需要占用部分 CPU 资源;其次,产生了传输数据相关性的新问题(在此我们不做论述)。

所以,让我们尝试定义问题:

我们希望创建这样一个环境,即能够提供对附加于图表的指标的数据访问,并具有以下属性:

  • 没有数据复制(也没有数据相关性的问题);
  • 只需稍加修改我们需要使用的可用方法的代码;
  • MQL 代码优先(当然,我们必须使用 DLL,但我们将只使用一些 C++ 代码字符串)。

笔者将 C++ Builder 用于 DLL 创建和 MetaTrader 4 以及 MetaTrader 5 客户端。下面的源代码以 MQL5 编写,MQL4 代码已附于本文;我们将在下文中讨论两种代码的主要区别。

2. 数组

首先,我们需要一些理论,因为我们将要使用指标缓冲区,并且我们必须知道缓冲区的数据在内存中是如何分布的。这一信息没有适当地文档化。

MQL 中的动态数组是具有可变大小的结构,如果数组大小增加而在数组后没有空闲内存,则 MQL 如何解决数据重新分配的问题将变得十分有趣。我们有两种方法解决问题:

  1. 在可用内存的其他部分重新分配新数据(并储存该数组所有部分的地址,例如使用引用列表),或
  2. 将整个数组作为整体移动至内存新的部分,该部分有足够的空间用于分配数组。

第一种方法带来了其他一些问题,因为在这种情况下我们必须调查 MQL 编译程序创建的数据结构。以下考虑仍然证明了第二个变体(仍然较慢):当动态数组传递至外部函数时(传递至 DLL),后者获得数据第一个元素的指针,其他数组元素按照顺序排列在第一个数组元素后。

当通过引用传递时,数组数据可以更改,因此这意味着对于第一种方法,复制整个数组至单独的内存区域以传递给外部函数,然后将结果添加至源数组存在一个问题,即,执行第二种方法隐含的相同的操作。

虽然这一结论在逻辑上不是 100% 精确,我们仍可将它视为是相当可靠的(这也被我们基于此理念的产品的正确运行所证明)。

因此,我们可以假设下面的说法是正确的:

  • 在每一时刻,动态数组的数据在内存中一个接一个地连续排列;并且数组可重新分配至计算机内存的其他区域,但只能作为一个整体操作。
  • 当动态数组作为参数通过引用传递至外部函数时,第一个数组元素的地址被传递至 DLL 库。

然而我们还需要其他一些假设:所谓时刻,我们指的是调用对应指标的 OnCalculate()(MQL4 则为 start () 函数)函数的时刻;此外,为了简便,我们假设我们的数据缓冲区具有相同的维度,且类型均为 double []。

3. 必要条件

MQL 不支持指针(除了所谓的对象指针,对象指针并不是普遍意义上的指针),因为这已被 MetaQuotes Software 公司的代表反复声明和确认。因此,我们来看看事实是否如此。

什么是指针?指针并不只是带星号的的标识符,它是计算机内存单元的地址。那么什么是单元地址?单元地址是起始于某个起点的序列号。最后,什么是序列号?序列号是一个整数,对应于计算机内存的某个单元。为什么我们不能像使用整数那样使用指针?不,我们可以,因为 MQL 程序可完美处理整数!

那么如何将指针转换为整数?动态链接库可帮助我们达成目的,我们将利用 C++ 类型转换的机会。由于 C++ 指针为四字节数据类型,对我们而言,可以很方便地将 int 用作此类四字节数据类型。

符号位并不重要,我们对其不作考虑(如果它等于 1,意味着整数为负),重要的是我们可以保持所有指针位不变。当然,我们可以使用无符号整数,但 MQL5 和 MQL4 的代码相似则更为理想,因为 MQL4 未提供无符号整数。

因此,

extern "C" __declspec(dllexport) int __stdcall GetPtr(double *a)
{
        return((int)a);

}

这样,我们就有了值为数组起始地址的长型变量!现在,我们需要了解如何读取 i-th 数组元素的值:

extern "C" __declspec(dllexport) double __stdcall GetValue(int pointer,int i)
{
        return(((double*) pointer)[i]);

}

... 并写入值(在我们的示例中它仍然不是必须的...)

extern "C" __declspec(dllexport) void __stdcall SetValue(int pointer,int i,double value)
{
        ((double*) pointer)[i]=value;

}

以上便是全部内容。现在我们可以在 MQL 中使用指针。

4. 包装

我们已创建系统的内核,现在必须为其在 MQL 程序中使用方便做准备。然而,美学粉也不要感到不安,我们会向大家展现一些不同之处。

有很多种方法可用,我们将选择以下方式。让我们回想一下,客户端具有一个特别功能用于独立 MQL 程序间的数据交换 - 全局变量。最自然的方式是用它们存储指向我们将要访问的指标缓冲区的指针。我们会将这些变量作为描述符表考虑。每个描述符将具有如下所示的以字符串表示的名称:

string_identifier#buffer_number#symbol#period#buffer_length#indexing_direction#random_number

并且其值将等于计算机内存中相应缓冲区指针的整数显示。

有关描述符字段的一些细节。

  • string_identifier – 任意字符串(例如,指标的名称 - 可使用 short_name 变量等);它将被用于搜索必要的指针,我们指的是一些指标将使用相同的标识符注册描述符,并在相互间使用字段进行区分:
  • buffer_number – 将用于区分缓冲区;
  • buffer_length – 我们需要用它来控制界限,否则可能导致客户端和 Windows 崩溃Blue Screen of Death :);
  • symbol, period – 交易品种和周期用于指定图表窗口;
  • ordering_direction – 它指定了数组元素的排序方向:0 – 正常排序,1 – 逆向排序(AS_SERIES 标志为 true);
  • random_number – 如果指标有多个副本通过不同的窗口或不同的参数集附加至客户端时使用(它们的第一个和第二个字段可设置相同的值,这也是我们需要通过某些方式区分它们的原因)- 也许这不是最佳的解决方案,但它确实管用。

首先,我们需要用到一些函数来注册和删除描述符。看一看函数的第一个字符串 - 调用 UnregisterBuffer() 函数对于从全局变量列表中删除旧的描述符来说是必要的。

对于每个新柱,缓冲区大小将增加 1,因此我们必须调用 RegisterBuffer()。如果缓冲区大小改变,新的描述符将在表中创建(其大小信息包含在其名称内),旧的描述符将留在表中。这就是我们使用它的原因。

void RegisterBuffer(double &Buffer[], string name, int mode) export
{
   UnregisterBuffer(Buffer);                    //首先删除变量以防万一
   
   int direction=0;
   if(ArrayGetAsSeries(Buffer)) direction=1;    //设置正确的ordering_direction

   name=name+"#"+mode+"#"+Symbol()+"#"+Period()+"#"+ArraySize(Buffer)+"#"+direction;
   int ptr=GetPtr(Buffer);                      // 获取缓存指针

   if(ptr==0) return;
   
   MathSrand(ptr);                              //使用指针值代替当前时间,来生成随机数比较方便
                                                  
   while(true)
   {
      int rnd=MathRand();
      if(!GlobalVariableCheck(name+"#"+rnd))    //检查独特的名称 - 我们假设
      {                                         //没有人会使用更多的RAND_MAX缓存 :)
         name=name+"#"+rnd;                     
         GlobalVariableSet(name,ptr);           //写入全局变量
         break;
      }
   }   
}}
void UnregisterBuffer(double &Buffer[]) export
{
   int ptr=GetPtr(Buffer);                      //我们将在缓存的真实地址中注册
   if(ptr==0) return;
   
   int gt=GlobalVariablesTotal();               
   int i;
   for(i=gt-1;i>=0;i--)                         //遍历所有全局变量
   {                                            //并且从所有地方删除缓存
      string name=GlobalVariableName(i);        
      if(GlobalVariableGet(name)==ptr)
         GlobalVariableDel(name);
   
}      

}

详细的注释在此处是不必要的,我们只是指出第一个函数在全局变量列表中创建上述格式的新的描述符的事实;第二个函数搜索所有全局变量并删除变量和其值等于指针的描述符。

现在考虑第二个任务 - 从指标获取数据。在我们实施对数据的直接访问前,我们需要找出相应的描述符。可以使用以下函数实现这一点。它的算法如下所述:我们遍历所有全局变量,并检查是否存在描述符中指定的字段值。

如果找到,我们将其名称添加至数组,作为最后一个参数传递。因此,结果是函数返回所有匹配搜索条件的内存地址。返回值是找到的描述符的数量。

int FindBuffers(string name, int mode, string symbol, int period, string &buffers[]) export
{
   int count=0;
   int i;
   bool found;
   string name_i;
   string descriptor[];
   int gt=GlobalVariablesTotal();

   StringTrimLeft(name);                                    //整理字符串的不必要空间
   StringTrimRight(name);
   
   ArrayResize(buffers,count);                              //将大小重置为0

   for(i=gt-1;i>=0;i--)
   {
      found=true;
      name_i=GlobalVariableName(i);
      
      StringExplode(name_i,"#",descriptor);                 //分割字符串
      
      if(StringFind(descriptor[0],name)<0&&name!=NULL) found=false; //根据匹配条件检查每一个字段
                                                                     
      if(descriptor[1]!=mode&&mode>=0) found=false;
      if(descriptor[2]!=symbol&&symbol!=NULL) found=false;
      if(descriptor[3]!=period&&period>0) found=false;
      
      if(found)
      {
         count++;                                           //条件满足,将其添加到列表中
         ArrayResize(buffers,count);
         buffers[count-1]=name_i;
      
}
   
}
   
   return(count);

}

正如我们在函数代码中所看到的,某些搜索条件可以省略。例如,如果我们传递 NULL 作为名称,系统将省略对 string_identifier 的检查。其他字段也是一样:mode<0,symbol:=NULL 或 period<=0。它允许在描述符表中扩展搜索选项。

例如,您可以找出所有图表窗口中的“移动平均线”指标,或使用周期 M15 将范围限制在 EURUSD 图表中。补充说明:对 string_identifier 的检查通过函数 StringFind() 执行而不是严格的相等检查。这样做是为了能够有机会通过描述符的一部分执行搜索(也就是说,当有多个指标设置为 "MA(xxx)" 类型的字符串时,搜索可通过子字符串 "MA" 进行 - 结果是我们将找出所有注册的“移动平均线”)。

我们还使用了函数 StringExplode(string s, string separator, string &result[])。该函数使用分隔符将指定的字符串分割为子字符串,并将结果写入结果数组。

void StringExplode(string s, string separator, string &result[])
{
   int i,pos;
   ArrayResize(result,1);
   
   pos=StringFind(s,separator); 
   if(pos<0) {result[0]=s;return;}
   
   for(i=0;;i++)
   {
      pos=StringFind(s,separator); 
      if(pos>=0)
      {
         result[i]=StringSubstr(s,0,pos);
         s=StringSubstr(s,pos+StringLen(separator));
      
}
      else break;
      ArrayResize(result,ArraySize(result)+1);
   
}

}

现在,当我们获得所有必要描述符的列表时,我们便可以从指标获取数据:

double GetIndicatorValue(string descriptor, int shift) export
{
   int ptr;
   string fields[];
   int size,direction;
   if(GlobalVariableCheck(descriptor)>0)               //检查描述符的有效性
   {                
      ptr = GlobalVariableGet(descriptor);             //获取指针的值
      if(ptr!=0)
      {
         StringExplode(descriptor,"#",fields);         //将名称分割为字段
         size = fields[4];                             //我们需要当前数组大小
         direction=fields[5];                                 
         if(direction==1) shift=size-1-shift;          //如果ordering_direction反向
         if(shift>=0&&shift<size)                      //检查其有效性,以防止崩溃
            return(GetValue(MathAbs(ptr),shift));      //ok,返回值
      
}   
   
} 
   return(EMPTY_VALUE);                                //否则返回空值 

}

如您所见,它是来自 DLL 的 GetValue() 函数的包装。有必要为数组界限检查描述符的有效性,并将指标缓冲区排序考虑在内。如果不成功,函数返回 EMPTY_VALUE。

对指标缓冲区的修改类似:

bool SetIndicatorValue(string descriptor, int shift, double value) export
{
   int ptr;
   string fields[];
   int size,direction;
   if(GlobalVariableCheck(descriptor)>0)               //检查其有效性
   {                
      ptr = GlobalVariableGet(descriptor);             //获取描述符值
      if(ptr!=0)
      {
         StringExplode(descriptor,"#",fields);         //将其分割为字段
         size = fields[4];                             //我们要知道它的大小
         direction=fields[5];                                 
         if(direction==1) shift=size-1-shift;          //订单转向的情况
         if(shift>=0&&shift<size)                      //检查索引防止客户端崩溃
         {
            SetValue(MathAbs(ptr),shift,value);
            return(true);
         
}   
      
}   
   
}
   return(false);

}

如果所有的值都正确,它将从 DLL 调用 SetValue() 函数。返回值与修改结果相对应:成功将返回 true,出现错误则返回 false。

5. 检查它是如何工作的

现在,让我们尝试检查。作为目标,我们将使用来自标准包中的真实波动幅度均值 (ATR),并展示为了将其值复制到其他指标窗口我们需要修改的代码。我们还将测试其缓冲区数据的数据修改。

我们首先要做的是在 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 &TickVolume[],
                const long &Volume[],
                const int &Spread[])
  {
   if(prev_calculated!=rates_total)
      RegisterBuffer(ExtATRBuffer,"ATR",0);
…

正如我们看到的,每次我们有了新柱时都应执行新缓冲区注册 - 这种情况随着时间推移(通常)或在额外的历史数据下载时发生。

此外,我们需要在指标操作结束后从表中删除描述符。这就是我们需要在 deinit 事件句柄中添加代码的原因(但请记住,它应该返回空值并具有一个 const int reason 输入参数):

void OnDeinit(const int reason)
{
   UnregisterBuffer(ExtATRBuffer);

}

这是客户端图表窗口中我们修改后的指标(图 1)及其在全局变量列表中的描述符(图 2):

 

图 1. 真实波动幅度均值

图 2. 在客户端的全局变量列表中创建的描述符

图 2. 在客户端的全局变量列表中创建的描述符

下一步是访问 ATR 数据。让我们使用以下代码创建一个新的指标(我们将其命名为 test):

//+------------------------------------------------------------------+
//|                                                         test.mq5 |
//|                                             Copyright 2009, alsu |
//|                                                 alsufx@gmail.com |
//+------------------------------------------------------------------+
#property copyright "2009, alsu"
#property link      "alsufx@gmail.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

#include <exchng.mqh>

//----绘制ATRCopy
#property indicator_label1  "ATRCopy"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Red
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- 指标缓存
double ATRCopyBuffer[];

string atr_buffer;
string buffers[];

//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                |
//+------------------------------------------------------------------+

int OnInit()
  {
//--- 指标缓存映射
   SetIndexBuffer(0,ATRCopyBuffer,INDICATOR_DATA);
//---
   return(0);
  
}

//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
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[])
  {
//---
   int found=FindBuffers("ATR",0,NULL,0,buffers);   //在全局变量中搜索描述符
   if(found>0) atr_buffer=buffers[0];               //如果我们发现它,将它保存到atr_buffer
   else atr_buffer=NULL;
   int i;
   for(i=prev_calculated;i<rates_total;i++)
   {
      if(atr_buffer==NULL) break;
      ATRCopyBuffer[i]=GetIndicatorValue(atr_buffer,i);  //现在获取数据就简单了
      SetIndicatorValue(atr_buffer,i,i);                 //并且记录他们也方便
   
}
//--- 返回prev_calculated的值用于下一次调用
   return(rates_total);
  
}
//+------------------------------------------------------------------+

正如我们所见,这并不难。通过子字符串获取描述符列表,然后使用我们的函数读取指标缓冲区的值。最后,让我们写入一些无关紧要的内容(在我们的示例中,我们写入与数组元素索引对应的值)。

现在运行 test 指标(我们的目标 ATR 仍应附于图表上)。正如我们在图 3 中所见,ATR 的值位于底部的子窗口中(在我们的 test 指标中),我们看到将其取代的直线 - 事实上,它全部由与数组索引对应的值组成。

 

图 3. Test.mq5 运行结果

巧妙的手法 - 以上便是全部内容:)

6. 向下兼容

笔者尝试以 MQL5 编程来创建库,以尽可能地贴近 MQL4 的标准。因此,只需稍作修改便可在 MQL4 中使用。

特别是,您应该删除仅在 MQL5 中出现的导出关键字,并修改我们使用间接类型转换处的代码(MQL4 没那么灵活)。实际上,指标中使用的函数是完全相同的,不同之处仅在于新柱和历史数据添加控件的方法。

总结

关于优势的几点说明。

由此看来,对本文所述方法的使用似乎并不局限于其初衷。除了构建高速层叠指标,库可以成功应用至“EA 交易”,包括其历史数据测试情形。其他应用对笔者而言较难以想象,但我相信亲爱的读者们将乐意朝这个方向努力。

我认为下述技术要点是必要的:

  • 改进描述符表(特别是窗口指示);
  • 开发其他的表用于描述符创建排序 - 这对交易系统的稳定性十分重要。

同时,我很乐意收到其他的建议以改进库。

所有必要文件已附于本文。MQL5 和 MQL4 的代码以及 exchng.dll 库的源代码已一并呈上。由于必须位于客户端文件夹中,这些文件位置相同。

免责声明

本文所述编程技巧若使用不当可能会损坏电脑上运行的软件。虽然到目前为止尚无任何证据表明这一点,然而对随附文件的使用可导致可能的意外后果,请自行承担风险。

致谢

笔者采用了 MQL4 社区论坛 http://forum.mql4.com 中发布的问题以及下列用户的观点:igor.senych、satop、bank、StSpirit、TheXpert、jartmailru、ForexTools、marketeer、IlyaA – 如有疏漏,敬请谅解

全部回复

0/140

量化课程

    移动端课程