Хранение и отображение информации

Andrey Khatimlianskii | 15 июня, 2006

1. Введение

Вы никогда не тратили несколько часов на поиски какой-нибудь важной информации, предусмотрительно записанной вашим экспертом в лог-файл? Или, может, вам надоело всматриваться в мелкие одноцветные буковки, выводимые на экран функцией Comment()? А ведь они часто могут иметь очень большое значение для трейдинга. Если вам знакомо то, о чём я говорю, значит, эта статья для вас.

Основные задачи, которые я поставил перед собой при написании этой статьи:


2. Личный лог-файл эксперта

Как я уже говорил, функция Print() не всегда удобна для записи информации, генерируемой экспертом во время работы. Особенно заметно это становится, когда на одном терминале одновременно торгует несколько экспертов. Каждый эксперт записывает в лог-файл свою информацию, и потом там очень сложно что-то найти. Про конструктивный анализ такой информации речь просто не идёт – это очень трудоёмкий и утомительный процесс.

Решить эту проблему достаточно просто. Надо каждому эксперту завести свой личный лог-файл. Вместо записи в общий лог-файл записывать информацию в личный. Для того чтобы было просто пользоваться такой возможностью, давайте сразу оформим код в виде функции.

Итак, что должна делать функция:

Всё просто и однозначно. Единственное, о чём можно было бы поспорить, – это необходимость закрытия файла после каждой записи. С одной стороны, это дало бы возможность открывать файл в другом приложении во время работы данного эксперта, а с другой стороны, могло бы навредить, если эксперт не сможет открыть файл для очередной записи, так как файл может быть занят. При этом информация может просто потеряться. Такого допускать нельзя, тем более, что есть программы, позволяющие открывать файлы только для просмотра, не мешая MetaTrader'у с ними работать.

Поскольку открытие и закрытие файла необходимо выполнить всего один раз, код, занимающийся этим, мы поместим в функции init() и deinit() соответственно. Чтобы он занимал как можно меньше места, оформим его тоже в виде функций:

int log_handle = -1;
 
//+---------------------------------------------------------------+
// void log_open( string ExpertName = "Expert" )
//
// Функция, открывающая личный логфайл эксперта.
// Директория, в которой будет создан файл:
// "...\MetaTrader 4\experts\files\logs\ExpertName\"
// Имя файла - дата записи файла в формате "гггг.мм.дд"
//+---------------------------------------------------------------+
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, \" \" ) - Error #", 
          _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()
//
// Функция, закрывающая личный логфайл эксперта.
//+---------------------------------------------------------------+
void log_close()
 {
      if( log_handle > 0 ) FileClose( log_handle );
 }

Теперь, когда у нас есть открытый файл, можно записывать в него информацию. Для этого:

В результате у нас должна получиться вот такая функция:

//+---------------------------------------------------------------+
// log( string text )
//
// Функция, записывающая строку text в личный логфайл эксперта.
//+------------------------------------------------------------------+
void log( string text )
 {
     int _GetLastError = 0;
   if( log_handle < 0 )
      {
             Print( "Log write error! Text: ", text );
     return(-1);
    }
    
     //---- Перемещаем файловый указатель в конец файла
     if( !FileSeek ( log_handle, 0, SEEK_END ) )
     {
          _GetLastError = GetLastError();
    Print( "FileSeek ( " + log_handle + ", 0, SEEK_END ) - Error #", 
           _GetLastError );
    return(-1);
   }
 
    //---- Если строка, которую хочет записать эксперт, это не символ 
 //переноса строки, 
    //---- добавляем в начало строки время записи
    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 можем смело менять на слово log, не забыв при этом добавить вызовы функций log_open и log_close.

Вот простой пример эксперта, использующего включаемый файл log.mq4:

#include <log.mq4>
 
int init()
  {
    log_open( "log_test" );
    log( "Логфайл открыт успешно, эксперт начал работу..." );
    return(0);
  }
int deinit()
  {
    log( "Закрываем логфайл, эксперт завершает работу..." );
    log_close();
    return(0);
  }
int start()
  {
    log( "Новый тик: Bid = " + DoubleToStr( Bid, Digits ) );
    return(0);
  }


3. Вывод информации на экран

Теперь, когда вопрос с лог-файлом решён, можно приступить к украшению выводимой на экран информации.

Для начала рассмотрим все возможные способы реализации этой задачи. В языке MQL4 отображение информации на экране реализует функция Comment(), но она нас не устраивает по описанным выше причинам. Поэтому надо искать другое решение. Удачным примером такового могут стать объекты, содержащие текст. Их всего два – "Текст" и "Текстовая метка". Принципиальное отличие между ними состоит в том, что "Текст" привязывается к координатам графика (времени и цене), а "Текстовая метка" – к координатам окна. Поскольку нам необходимо, чтобы информация оставалась на месте при смещении графика или изменении его масштаба, мы будем использовать именно "Текстовую метку".

Созданию и управлению объектами в MQL4 посвящено несколько функций, все они начинаются со слова Object. Давайте посмотрим, какие из них могут быть нам полезными:

Итак, что нам необходимо сделать:

У нас опять получится 3 функции, выполняющих каждая свою задачу.

Перед написанием кода следует упомянуть об одном неприятном ограничении, связанном с использованием "Текстовой метки". Она может быть только однострочной, то есть не может содержать символов переноса строки. А информацию намного проще воспринимать, когда она отображена в несколько строк. Поэтому нам необходимо создать несколько объектов и потом распределять информацию между ними. Я сделал пять "строк", но вы можете изменить это количество под собственные нужды.

Кроме того, есть ограничение на длину выводимого текста. Поэтому я добавил второй "столбец", то есть добавил справа ещё пять строк.

Вот так выглядит функция 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 )
//
// Создание объекта "Текстовая метка" с именем _Name.
// Координаты: х = _XDistance, у = _YDistance, угол = _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 ) - Error #",
                 _GetLastError );
           return(-1);
          }
      }
    if( !ObjectSet( _Name, OBJPROP_CORNER, _Corner ) )
      {
        _GetLastError = GetLastError();
        Print("ObjectSet( \"", _Name, "\", OBJPROP_CORNER, ",
              _Corner, " ) - Error #", _GetLastError );
      }
    if( !ObjectSet( _Name, OBJPROP_XDISTANCE, _XDistance ) )
      {
        _GetLastError = GetLastError();
        Print("ObjectSet( \"", _Name, "\", OBJPROP_XDISTANCE, ", 
              _XDistance, " ) - Error #", _GetLastError );
      }
    if( !ObjectSet( _Name, OBJPROP_YDISTANCE, _YDistance ) )
      {
        _GetLastError = GetLastError();
        Print("ObjectSet( \"", _Name, "\", OBJPROP_YDISTANCE, ", 
              _YDistance, " ) - Error #", _GetLastError );
      }
    if( !ObjectSetText ( _Name, "", 10 ) )
      {
        _GetLastError = GetLastError();
        Print("ObjectSetText( \"", _Name, "\", \"\", 10 ) - Error #",
              _GetLastError );
      }
  }

Как видите, объекты будут называться "InfoLabel_" + номер объекта (от 00 до 04 – левый "столбец", и от 10 до 14 – правый). Привязка объектов производится к левому верхнему углу. Сверху специально сделан отступ, так как многие привыкли видеть там OHLC – информацию о текущем баре. Вертикальный промежуток между строками я сделал равным пятнадцати, этого хватит для текста среднего размера. С инициализацией закончили, давайте сразу сделаем деинициализацию – они будут очень похожи:

/////////////////////////////////////////////////////////////////////////////////
// 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 ), 
             "\" ) - Error #", _GetLastError );
      }
          if( !ObjectDelete( StringConcatenate( "InfoLabel_1", row ) ) )
           {
                   _GetLastError = GetLastError();
       Print( "ObjectDelete( \"", StringConcatenate( "InfoLabel_1", row ), 
             "\" ) - Error #", _GetLastError );
      }
      }
 }

А теперь самое интересное – собственно вывод информации.

Итак, у нас есть 10 объектов, которые стоят на своих местах и готовы "принять" текст для отображения. Давайте задумаемся, как сделать использование функции вывода информации максимально простым.

Во-первых, вместо имени объекта будет проще указывать его номер.

Во-вторых, необходимо сделать обязательным использование только двух параметров: номера объекта и выводимого текста. Остальные параметры можно сделать необязательными, так как цвет текста, начертание и размер шрифта не понадобится менять часто. Здесь же следует подумать, как поступать, если параметры опущены. Есть два варианта:

Мне кажется, второй вариант более удобный – умолчательные значения пришлось бы менять в коде для каждого эксперта, а последние используемые параметры будут запоминаться сами при каждом применении функции.

Итак, вот как у нас выглядит начало функции:

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;

Для того, чтобы избежать ситуации, в которой lastused-переменные будут содержать пустые значения, присвоим им значения сразу при объявлении:

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

Вы, наверное, обратили внимание, что lastused-переменные объявлены вне функций. Благодаря этому их значения не будут обнуляться при каждом вызове функции info().

А теперь, собственно, отображаем новый текст и перерисовываем объекты. Это необходимо для того, чтоб изменения, внесённые нами в объект, сразу отобразились на экране:

    //---- отображаем новый текст
    if ( !ObjectSetText( LabelName, Text, FontSize, Font, Color ) )
      {
        int _GetLastError = GetLastError();
        Print("ObjectSetText( \"", LabelName,"\", \"", Text, "\", ", 
              FontSize, ", ", Font, ", ", Color, " ) - Error #", 
              _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. Последние можно использовать из любого эксперта.