Текстовые файлы для хранения входных параметров советников, индикаторов и скриптов

Andrei Novichkov | 5 июля, 2016

Введение

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


Как хранить параметры в текстовых файлах

Чтобы дополнить возможности имеющегося метода создания и хранения входных параметров, можно использовать самые обычные текстовые файлы. В них можно помещать все, что угодно, они легко редактируются и перемещаются. Их структуру можно организовать по образцу INI-файлов. Например, вот так может выглядеть сохраненный в текстовом файле массив типа integer:

/*размер массива*/
{array_size},5
/*собственно, сам массив*/
{array_init},0,1,2,3,4

В приведенном примере в начале строки записывается «якорь» или «название секции», затем через запятую — содержимое этой секции. «Якорем» может быть любая уникальная строка символов. Такой файл создается и сохраняется в «песочнице» терминала. Далее в блоке инициализации индикатора, советника или в коде скрипта открываем данный файл для чтения, как файл формата CSV.

Когда приходит время прочитать сохраненный массив, ищем в файле «якорь» по известному имени{array_size}. Переставляем файловый указатель в начало файла вызовом FileSeek(handle,0,SEEK_SET). Ищем следующий якорь, также с известным именем {array_init}, а когда находим его — просто прочитываем строки нужное число раз, преобразуя их согласно типу массива, в данном случае — в integer. В подключаемом файле ConfigFiles.mqh содержится несколько простых функций, реализующих процесс поиска «якоря» и чтения данных.

Другими словами, в общем случае объект, который мы сохраняем в файле, должен быть описан «якорем» с последующей строкой с данными, разделенными запятыми, как требует формат CSV. «Якорей» и строк данных, следующих за ними, может быть сколько угодно, но с одним условием: на один «якорь» полагается только одна строка.

Комментарии, разнообразные пояснения, заметки, даже инструкции могут быть записаны в этот же файл практически в любой форме и в любом месте.  Есть одно очевидное требование: текст не должен нарушать последовательности «якорь» — данные. Еще одно желательное условие — любой объемный текст нужно располагать в конце файла. Это ускорит поиск «якорей».

Вот таким образом можно было бы организовать хранение расписания работы советника, о котором мы говорили выше:

….......
{d0},0,0,22
{d1},0,0,22
{d2},1,0,22
…........
{d6},0,0,22

Здесь имя «якоря» условно отражает то, что речь идет о дне недели под определенным номером (т. е., {d1} — это день номер один, и так далее). Далее идет значение типа bool, определяющее, торгует советник в этот день или нет (в данном случае d1 не торгует). Если торговли не происходит, то последующие значения можно и не читать, но здесь они оставлены. Наконец, последние два значения типа integer означают начало и конец работы советника в часах.

На открытии дневной свечи советник читает расписание работы на новый день. Конечно же, строка данных для каждого дня может быть изменена и дополнена другими необходимыми значениями, в зависимости от логики работы. Таким образом, в памяти сохраняются только данные на текущий день.

Имя «якоря» может быть любым, но желательно руководствоваться здравым смыслом. Нецелесообразно давать "якорям" абстрактные названия, из практических соображений лучше именовать их осознанно. Нужно, чтобы имена, с одной стороны, несли смысловую нагрузку, а с другой стороны, были уникальны. Нужно помнить, что по именам выполняется поиск данных. Очевидно, что возможна ситуация, когда в одном файле будет несколько «якорей» с одинаковым именем, но это уже вопрос конкретной реализации.

Разберем фрагменты кода, взятого из полностью рабочего индикатора. Итак, нашему индикатору для работы необходимы данные по нескольким валютным парам. Индикатор запрашивает данные по таймеру, потом обрабатывает их в соответствии со своей логикой (в данном случае особенности этой логики для нас неинтересны). Необходимо помнить, что брокеры иной раз прибавляют к имени валютной пары различные суффиксы и префиксы, так что, к примеру, из EURUSD может получиться #.EURUSD.ch. Это обстоятельство нужно обязательно учесть, чтобы в коде корректно обращаться к другим валютным парам. Последовательность наших действий будет такой.

1. Создаем текстовый файл broker.cfg со следующим содержимым: 

[PREFIX_SUFFIX],#.,.ch
2. Создаем текстовый файл <имя_индикатора>.cfg со следующим содержимым:
/*Количество валютных пар в строке с «якорем» [CHARTNAMES] */
[CHARTCOUNT],7
/*Имена валютных пар, с графиков которых индикатор читает данные*/
[CHARTNAMES],USDCAD,AUDCAD,NZDCAD,GBPCAD,EURCAD,CADCHF,CADJPY
/*Первой или второй в валютной паре идет основная валюта (в данном случае CAD)*/
[DIRECT],0,0,0,0,0,1,1
3. Помещаем оба файла в песочницу.

4. В коде индикатора определяем несколько вспомогательных функций (либо подключаем файл ConfigFiles.mqh):

// Открывает файл по заданному имени, как файл формата CSV и возвращает его хэндл
   int __OpenConfigFile(string name)
     {
      int h= FileOpen(name,FILE_READ|FILE_SHARE_READ|FILE_CSV,',');
      if(h == INVALID_HANDLE) PrintFormat("Invalid filename %s",name);
      return (h);
     }

//+------------------------------------------------------------------+
//| Функция читает одно значение iRes типа long в секции с именем    |
//| "якоря" strSec в файле с хэндлом handle. В случае успеха         |
//| возвращается true, в случае ошибки возвращается false.           |
//+------------------------------------------------------------------+
   bool _ReadIntInConfigSection(string strSec,int handle,long &iRes)
     {
      if(!FileSeek(handle,0,SEEK_SET) ) return (false);
      string s;
      while(!FileIsEnding(handle))
        {
         if(StringCompare(FileReadString(handle),strSec)==0)
           {
            iRes=StringToInteger(FileReadString(handle));
            return (true);
           }
        }
      return (false);
     }

//+------------------------------------------------------------------+
//| Функция читает массив строк sArray размером count в секции с     |
//| именем "якоря" strSec в файле с хэндлом handle. В случае успеха  |
//| возвращается true, в случае ошибки возвращается false.           |
//+------------------------------------------------------------------+
   bool _ReadStringArrayInConfigSection(string strSec,int handle,string &sArray[],int count)
     {
      if(!FileSeek(handle,0,SEEK_SET) ) return (false);
      while(!FileIsEnding(handle))
        {
         if(StringCompare(FileReadString(handle),strSec)==0)
           {
            ArrayResize(sArray,count);
            for(int i=0; i<count; i++) sArray[i]=FileReadString(handle);
            return (true);
           }
        }
      return (false);
     }

//+------------------------------------------------------------------+
//| Функция читает массив bArray типа bool  размером count в секции  |
//| с именем "якоря" strSec в файле с хэндлом handle. В случае       |
//| успеха возвращается true, в случае ошибки возвращается false.    |
//+------------------------------------------------------------------+
   bool _ReadBoolArrayInConfigSection(string strSec,int handle,bool &bArray[],int count)
     {
      string sArray[];
      if(!_ReadStringArrayInConfigSection(strSec, handle, sArray, count) ) return (false);
      ArrayResize(bArray,count);
      for(int i=0; i<count; i++)
        {
         bArray[i]=(bool)StringToInteger(sArray[i]);
        }
      return (true);
     }
Кроме того, включаем и такой код в индикатор:
   …..
   input string strBrokerFname  = "broker.cfg";       // Имя файла с данными о брокере
   input string strIndiPreFname = "some_name.cfg";    // Имя файла с настройками индикатора
   …..

   string strName[];         // Массив с именами валютных пар ([CHARTNAMES])
   int    iCount;            // Количество валютных пар
   bool   bDir[];            // Массив с «порядком следования» ([DIRECT])

   …..
   int OnInit(void) 
     {

      string prefix,suffix;     // Префикс и суффикс. Локальные переменные, обязательные к 
                                // инициализации, но употребляемые только один раз.
      prefix= ""; suffix = "";
      int h = _OpenConfigFile(strBrokerFname);
      // Читаем данные о префиксе и суффиксе из конфигурационного файла. Если фиксируются 
      // ошибки, то продолжаем, оставляя дефолтные значения.
      if(h!=INVALID_HANDLE) 
        {
         if(!_GotoConfigSection("[PREFIX_SUFFIX]",h)) 
           {
            PrintFormat("Error in config file %s",strBrokerFname);
              } else {
            prefix = FileReadString(h);
            suffix = FileReadString(h);
           }
         FileClose(h);
        }
      ….
      // Читаем настройки индикатора. 
      if((h=__OpenConfigFile(strIndiPreFname))==INVALID_HANDLE)
         return (INIT_FAILED);

      // читаем количество валютных пар
      if(!_ReadIntInConfigSection("CHARTCOUNT]",h,iCount)) 
        {
         FileClose(h);
         return (INIT_FAILED);
        }
      // читаем массив с именами валютных пар, уже прочитав их количество
      if(!_ReadStringArrayInConfigSection("[CHARTNAMES]",h,strName,iCount)) 
        {
         FileClose(h);
         return (INIT_FAILED);
        }

      // Приводим имена валютных пар к требуемому виду
      for(int i=0; i<iCount; i++) 
        {
         strName[i]=prefix+strName[i]+suffix;
        }

      // читаем массив параметров типа bool
      if(!_ReadBoolArrayInConfigSection("[DIRECT]",h,bDir,iCount)) 
        {
         FileClose(h);
         return (INIT_FAILED);
        }
      ….
      return(INIT_SUCCEEDED);
     }

В результате выполнения кода проинициализированы два динамических массива и две жизненно важные локальные переменные. Созданный файл с именем broker.cfg пригодится еще не раз, чтобы постоянно не вводить вручную префикс и суффикс.

Возможные области применения. Недостатки и альтернативные варианты

Кроме уже описанных ситуаций, предлагаемыйспособ удобен для управления настройками нескольких копий индикатора или советника, работающих на разных валютных парах и аналогичных задач, требующих одного «центра управления». Полезен он и просто в том случае, когда для удаленной настройки можно ограничиться редактированием или заменой текстового файла и не задействовать доступ к самому терминалу. Ведь довольно часто встречаются случаи, когда трейдер имеет доступ к компьютеру с установленным терминалом только по FTP.

Еще один пример —когда трейдер управляет десятком терминалов, находящихся в разных местах, может быть, даже в разных странах. Достаточно один раз подготовить файл с настройками и разослать его по «песочницам» всех терминалов. Если вспомнить упомянутый выше пример с расписанием, то вполне реально организовать рассылку такого расписания раз в неделю, с тем, чтобы советники на всех компьютерах работали синхронно. Индикатор, фрагменты кода которого были приведены чуть выше, работает на двадцати восьми парах и синхронно управляется всего двумя файлами, упомянутыми в тексте.

Еще один интересный способ применения — одновременное хранение переменной в файле и в области свойств. При этом появляется возможность реализации логики некоего приоритета чтения. Для иллюстрации рассмотрим фрагмент псевдокода на примере инициализации значения магического номера:

…..
extern int Magic=0;
…..
int OnInit(void) 
  {
   …..
   if(Magic==0) 
     {
      // Это означает, что пользователь не инициализировал Magic, и осталось
      // значение по дефолту. Тогда ищем значение переменной Magic в файле
      …...
     }
   if(Magic==0) 
     {
      // По-прежнему ноль, следовательно, в файле такой переменной нет
      // Здесь обрабатываем создавшуюся ситуацию,
      // в данном случае выходим с ошибкой
      return (INIT_FAILED);
     }

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

Справедливости ради, перечислим и недостатки метода.

Первый — невысокая скорость.  С этим можно смириться, если данный способ использовать только на этапе инициализации индикатора/советника, либо периодически, например, на открытии новой дневной свечи.

Второй — необходимость очень тщательного и внимательного подхода к сопроводительной документации. Бывает очень непросто рассказать и показать трейдеру, как управлять инструментами, настраиваемыми таким образом. И это еще один довод к тому, чтобы хранить в таких файлах настройки, которые меняются редко. И, конечно же, самое пристальное внимание приходится оказывать подготовке и редактированию самих текстовых файлов. Ошибки на этом этапе чреваты серьезнейшими финансовыми последствиями.

Обратим внимание на альтернативные способы решения задач, примеры которых были приведены.

Первый из таких способов — это INI-файлы. Это действительно хороший способ хранения данных, проверенный и надежный. INI-файлы используются, когда не хочется писать в реестр, и это можно только приветствовать. Их структура известна и естественна, органически понятна. Самый первый пример с расписанием работы, записанный в INI-файле, мог бы выглядеть так:

….......
[d0]
Allowed=0
BeginWork=0
EndWork=22
.........
[d2]
Allowed=1
BeginWork=0
EndWork=22

И в остальном логика работы с INI фалами абсолютно такая же — их надо помещать в "песочницу", им так же будет присуща "медлительность", хотя и меньшая, чем при работе с вышеописанными файлами формата CSV. Обращаться к ним, как и к CSV-файлам,  имеет смысл во время инициализации или по открытии новой свечи. Но комментарии в тексте INI-файла уже нельзя будет располагать так, как хочется — только так, как разрешено для INI файлов. Однако даже с учетом этого никаких трудностей с сопутствующей документацией, связанных с особенностями формата файла, быть не должно.

Недостаток данного формата в том, что придется подключать сторонние библиотеки. В языках MQL нет средств для непосредственной работы с файлами такого формата, поэтому придется импортировать их из kernell32.dll. На данном сайте представлены сразу несколько библиотек, выполняющих эту операцию, поэтому нет смысла писать еще одну и прикреплять её здесь. Такие библиотеки просты, ведь речь идет об импорте всего двух функций. Но всегда нужно допускать, что библиотеки могут содержать ошибки и помнить о том, что неизвестно, будет ли вся конструкция работоспособна под Linux. Те же пользователи, которых не пугают последствия подключения сторонних библиотек, вполне могут использовать INI-файлы наравне с файлами формата CSV, которым посвящена статья.

Помимо INI-файлов, есть и другие возможные способы. Перечислим их вкратце.

  1. Использовать системный реестр. На мой взгляд, это чрезвычайно спорный, сомнительный вариант. Реестр — это критическая часть для функционирования и быстродействия операционной системы. Неужели будет правильно разрешить писать в него скриптам и индикаторам? Мне кажется,  что это будет стратегически неверное решение.
  2. Использовать базу данных. Да, это проверенный, надежный способ хранения любых данных и в любом объеме. Но нужно ли хранить значительный объем данных советнику или индикатору? В подавляющем большинстве случаев — нет. Базы данных обладают явно избыточной функциональностью для достижения целей, рассматриваемых здесь. Вместе с тем, о базе данных не стоит забывать, если действительно возникнет необходимость хранения нескольких гигабайтов данных.
  3. Использовать XML. По сути, это те же текстовые файлы, но с другим синтаксисом, в котором придется разбираться отдельно. Плюс ко всему,  чтобы получить возможность работать с XML-файлами, нужно будет написать библиотеку (или скачать готовую). Но даже не в этом состоит самая трудная задача: в конце работы нужно будет изготовить сопроводительную документацию. Оправданы ли будут трудозатраты разработчика? В данном случае это сомнительно.

Заключение

В заключение хотелось бы заметить, что текстовые файлы уже привычно используются: взять хотя бы механизмы копирования сделок. Сами терминалы MetaTrader создают и используют множество различных текстовых файлов. Среди них — и файлы конфигурации, и различные логи, и почта, и шаблоны.

В статье же предпринята попытка расширить применение такого простого средства, как обычные текстовые файлы формата CSV, на область конфигурирования инструментов трейдинга — советников, индикаторов и скриптов. На нескольких простых примерах было показано, какие новые возможности приобретаются с помощью таких файлов. Сделана попытка проанализировать возможные альтернативные способы и перечислены выявленные недостатки.

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