信息的存储和阅览

Andrey Khatimlianskii | 19 一月, 2016

1. 简介

您是否花过很多时间来尝试在您EA交易所写的记录文件中寻找重要信息?或者, 也许您瞪着Comment()函数所显示的小小的单色字母而疲惫不堪?其实, 它们在交易中是很重要的. 如果您了解我的意思, 这篇文章就是为您而写.

以下就是我写这篇文章所要解决的主要问题:


2. EA交易自己的记录文件

就像我说的那样, Print()函数在记录EA交易运行时生成的信息时并不总是那么方便. 特别是当有几个EA交易同时在一个相同终端上运行时就更加明显. 每个EA交易都把它自己的信息写到记录文件中, 这样就很难在其中找东西了. 这种情况下, 对此类信息的分析就不用提了:这非常费力和麻烦.

这个问题可以用比较简单的方法解决. 每个EA交易必须有它自己的记录文件. 信息都会被写进这些独立的记录文件, 而不是写进一个通用的文件中. 为了尽可能简单地使用此方式, 让我们把代码写成一个函数.

以下就是描述这个函数必须做到的:

清晰明了. 唯一有争议的一点是是否有必要在写完每个记录时关闭文件. 一方面, 这样就可以在EA交易运行过程中使用其他应用程序打开此文件了. 但是, 另一方面, 这样对EA交易进行下一条记录可能是不利的, 因为文件可能被其他应用程序使用了. 如果出现这种情况, 信息可能就直接丢掉了. 这是不能允许的, 特别是有些程序打开文件只是为了读取, 而不愿影响 MetaTrader 的运行.

因为打开和关闭文件只进行一次, 对应的代码可以分别放到 init() 和 deinit() 函数中. 为了让它只占用最小的空间, 让我们把它们也做成函数:

int log_handle = -1;
 
//+------------------------------------------------------------------+
// void log_open( string ExpertName = "Expert" )
//
// 用于打开EA交易独立记录文件的函数.
// 创建文件的目录:
// "...\MetaTrader 4\experts\files\logs\ExpertName\"
// 文件名就是文件的记录日期, 格式为 "YYYY.MM.DD"
//+------------------------------------------------------------------+
void log_open( string ExpertName = "Expert" )
 {
     string log_name = "logs\\" + ExpertName + " (" + Symbol() + ", " + 
                   strPeriod( Period() ) + ")\\" + TimeToStr( LocalTime(),
                   TIME_DATE ) + ".txt";
  log_handle = FileOpen ( log_name, FILE_READ | FILE_WRITE, " " );
    
  if ( log_handle < 0 )
     { 
         int _GetLastError = GetLastError();
    Print( "FileOpen( ", log_name, ", FILE_READ | FILE_WRITE, \" \" ) - 错误 #", 
                     _GetLastError );
    return(-1);
   }
 }
string strPeriod( int intPeriod )
 {
     switch ( intPeriod )
     {
          case PERIOD_MN1: return("Monthly");
    case PERIOD_W1:  return("Weekly");
    case PERIOD_D1:  return("Daily");
    case PERIOD_H4:  return("H4");
    case PERIOD_H1:  return("H1");
    case PERIOD_M30: return("M30");
    case PERIOD_M15: return("M15");
    case PERIOD_M5:  return("M5");
    case PERIOD_M1:  return("M1");
    default:        return("UnknownPeriod");
   }
 }
 
//+------------------------------------------------------------------+
// log_close()
//
// 用于关闭EA交易自身记录文件的函数.
//+------------------------------------------------------------------+
void log_close()
 {
     if ( log_handle > 0 ) FileClose( log_handle );
 }
现在我们有了一个打开的文件, 可以在里面写信息了. 为此:

以下就是满足上面需求的函数:

//+------------------------------------------------------------------+
// log( string text )
//
// 把文本行写入EA交易自身记录文件的函数.
//+------------------------------------------------------------------+
void log( string text )
 {
     int _GetLastError = 0;
  if ( log_handle < 0 )
     {
         Print( "写入记录错误!文本: ", text );
    return(-1);
   }
    
     //---- 把文件指针移动到文件末尾
    if ( !FileSeek ( log_handle, 0, SEEK_END ) )
      {
         _GetLastError = GetLastError();
    Print( "FileSeek ( " + log_handle + ", 0, SEEK_END ) - 错误 #", 
          _GetLastError );
    return(-1);
  }
    //---- 如果EA要写的一行内容不包含换行符, 
    //---- 在一行的开始部分增加记录时间
    if( text != "\n" && text != "\r\n" )
         text = StringConcatenate( TimeToStr( LocalTime(), TIME_SECONDS ), 
                             " - - - ", text );
    if( FileWrite ( log_handle, text ) < 0 )
          {
             _GetLastError = GetLastError();
     Print( "FileWrite ( ", log_handle, ", ", text, " ) - Error #", 
           _GetLastError );
     return(-1);
    }
    
    //---- 把写下的文字写到磁盘上
    FileFlush( log_handle );
 }

现在, Print 这个词可以在所有的EA交易中使用log来替换了, 另外不要忘记调用 log_open 和 log_close 函数.

这是一个使用log.mq4包含文件的非常简单的EA交易:

#include <log.mq4>
 
int init()
  {
    log_open( "log_test" );
    log( "记录文件已经被成功打开, EA交易开始工作..." );
    return(0);
  }
int deinit()
  {
    log( "关闭记录文件, EA交易结束工作..." );
    log_close();
    return(0);
  }
int start()
  {
    log( "新订单: Bid = " + DoubleToStr( Bid, Digits ) );
    return(0);
  }

3. 信息的显示

现在, 当我们解决了记录文件的问题之后, 我们可以开始"装饰"显示的信息了.

首先, 让我们考虑实现这个任务的所有可能方法. 在 MQL4 中, Comment() 函数是用于显示信息的, 但是它不适合以上描述的需求. 所以我们必须找到其他的解决方案. 一个很好的例子是含有文本的对象. 有两类这样的对象: "文本(Text)" 和 "文本标签(Text Label)". 它们之间的基本区别就是, "文本"绑定于图表的坐标(价格和时间), 而"文本标签"绑定的是窗口坐标. 因为我们需要信息在图表移动或者缩放改变的时候依然停留在原点上, 所以我们会使用"文本标签".

在MQL4中有一些函数用于创建和控制对象, 所有的名字都以Object开头. 让我们看看它们如何服务于我们的目标:

这就是我们要做的:

我们将会再一次得到3个函数, 每个都完成自己的任务.


在写代码之前, 我想先说一下"文本标签"使用中的一个令人不愉快的局限. 它只能是一行, 也就是说, 它不能包含任何换行字符. 但是如果信息能够分成几行显示会更好. 这就是为什么我们会创建多个对象, 然后在其中分配数据. 我制作了5"行", 但是您可以使用任何其他的行数.

另外, 显示文字的长度也有一个限制. 所以我增加了第二"列", 也就是说, 右侧又多了5行.

这就是用于创建对象的 info_init() 函数看起来的样子:

/////////////////////////////////////////////////////////////////////////////////
// void info_init()
//
// 创建用于显示信息的对象
/////////////////////////////////////////////////////////////////////////////////
void info_init()
  {
    for( int row = 0; row <= 4; row ++ )
      {
        _LabelCreate( StringConcatenate( "InfoLabel_0", row ),   4, 15 + 15*row );
        _LabelCreate( StringConcatenate( "InfoLabel_1", row ), 270, 15 + 15*row );
      }
  }
 
/////////////////////////////////////////////////////////////////////////////////
// void _LabelCreate ( string _Name, int _XDistance, int _YDistance, int _Corner = 0 )
//
// 创建 "Text Label" 名字为 _Name.
// 坐标: х = _XDistance, у = _YDistance, corner = _Corner.
/////////////////////////////////////////////////////////////////////////////////
void _LabelCreate ( string _Name, int _XDistance, int _YDistance, int _Corner = 0 )
  {
    int _GetLastError;
 
    if( !ObjectCreate( _Name, OBJ_LABEL, 0, 0, 0 ) )
      {
        _GetLastError = GetLastError();
        if ( _GetLastError != 4200 )
          {
            Print( "ObjectCreate( \"", _Name, "\", OBJ_LABEL,0,0,0 ) - 错误 #",
                   _GetLastError );
            return(-1);
          }
      }
    if( !ObjectSet( _Name, OBJPROP_CORNER, _Corner ) )
      {
        _GetLastError = GetLastError();
        Print( "ObjectSet( \"", _Name, "\", OBJPROP_CORNER, ", _Corner, 
                                       " ) - 错误 #", _GetLastError );
      }
    if( !ObjectSet( _Name, OBJPROP_XDISTANCE, _XDistance ) )
      {
        _GetLastError = GetLastError();
        Print( "ObjectSet( \"", _Name, "\", OBJPROP_XDISTANCE, ", _XDistance, 
                                             " ) - 错误 #", _GetLastError );
      }
    if( !ObjectSet( _Name, OBJPROP_YDISTANCE, _YDistance ) )
      {
        _GetLastError = GetLastError();
        Print( "ObjectSet( \"", _Name, "\", OBJPROP_YDISTANCE, ", _YDistance, 
                                             " ) - 错误 #", _GetLastError );
      }
    if( !ObjectSetText ( _Name, "", 10 ) )
      {
        _GetLastError = GetLastError();
        Print( "ObjectSetText( \"", _Name, "\", \"\", 10 ) - Error #", _GetLastError );
      }
  }

您可以看到, 创建对象的名称为 "InfoLabel_" + 对象编号 (左边一列从 00 到 04 而右边一列从10 到 14). 对象的锚点是左上角. 前面有意留了一些空间, 因为很多用户习惯在那里查看 OHLC (当前柱的信息). 我创建的行间距离是15, 这对通常大小的文字是足够的. 这样, 初始化过程就结束了, 让我们开始去初始化. 它们看起来很接近:

/////////////////////////////////////////////////////////////////////////////////
// void info_deinit()
//
// 删除由 info_init() 函数创建的对象
/////////////////////////////////////////////////////////////////////////////////
void info_deinit()
 {
     int _GetLastError;
   for ( int row = 0; row <= 4; row ++ )
      {
          if ( !ObjectDelete( StringConcatenate( "InfoLabel_0", row ) ) )
           {
                   _GetLastError = GetLastError();
       Print( "ObjectDelete( \"", StringConcatenate( "InfoLabel_0", row ), 
                                        "\" ) - 错误 #", _GetLastError );
      }
          if( !ObjectDelete( StringConcatenate( "InfoLabel_1", row ) ) )
               {
                  _GetLastError = GetLastError();
       Print( "ObjectDelete( \"", StringConcatenate( "InfoLabel_1", row ), 
                                       "\" ) - 错误 #", _GetLastError );
      }
       }
 }

现在, 最有趣的事情就是显示信息了.

这样, 我们有了10个对象, 它们已经可以"接受"文字并显示它们了. 让我们考虑一下怎样才能使显示函数的使用尽可能简单.

首先, 使用指定数字, 而不是对象名称会更加简单.

其次, 只有两个参数必须使用: 对象编号和显示文字的编号. 其他参数都是可选的, 因为文本颜色或字体大小不是经常需要改变. 在此, 我们必须考虑如果参数被跳过应该怎样处理. 有两个选择:

我想第二个选择会更有用: 默认参数值可能在每个EA交易的代码中都会改变, 而最后使用的参数值会在每次使用函数后被自动保存.

因而, 这就是我们的函数:

void info( int LabelNumber, string Text, color Color = -1, 
              double FontSize = -1.0, string Font = "-1" )
 {
     //---- 定义对象名称对象名称
      string LabelName;
  if ( LabelNumber < 10 )
          LabelName = StringConcatenate( "InfoLabel_0", LabelNumber );
  else
         LabelName = StringConcatenate( "InfoLabel_" , LabelNumber );
 
  //---- 如果没有指定额外参数, 
    //---- 给他们设为最后使用值
    if ( Color < 0 ) 
          Color = lastusedColor;
  if ( FontSize < 0 ) 
     FontSize = lastusedFontSize;
  if ( Font == "-1" ) 
     Font = lastusedFont;
 
  //---- 保存最后使用的值
    lastusedColor = Color;
  lastusedFontSize = FontSize;
  lastusedFont = Font;

为了避免最后使用的变量值出现空值, 让我们在它们的声明后立即赋值:

color lastusedColor = Black;
double lastusedFontSize = 9.0;
string lastusedFont = "Arial";

您也许会注意到, 最后使用的变量声明于函数体外. 这样可以防止它们的数值在每次调用info()函数时被清零.

现在让我们显示新的文字并重绘对象. 这必须马上做到, 因为当有变化发生时, 对象的显示也要马上更新:

 //---- 显示新的文字
    if( !ObjectSetText( LabelName, Text, FontSize, Font, Color ) )
      {
        int _GetLastError = GetLastError();
        Print( "ObjectSetText( \"", LabelName,"\", \"", Text, "\", ", FontSize, ", ", Font, 
                                             ", ", Color, " ) - 错误 #", _GetLastError );
      }
    //---- 重绘对象
    ObjectsRedraw();
  }

全部三个函数的完整代码在附件文件中, 它的名字是 info.mq4.

现在让我们看看得到了什么:




我想这非常好.

最终, 我们可以再多创造一个便利 - 一个用于清除全部信息的函数. 它被称为 info_clear(). 我想您知道怎样使用它.

void info_clear()
 {
     for ( int n = 0;  n < 5;  n ++ ) 
       info( n, "" );
  for (     n = 10; n < 15; n ++ ) 
       info( n, "" );
 }

4. 总结

本文描述了使用记录文件以及显示信息的替代性方法. 我们创建了两个包含文件并安放于"experts/include" 文件夹: log.mq4 和 info.mq4. 它们可以在任何EA交易中使用.