English Deutsch
preview
Внедрение в MQL5 практических модулей из других языков (Часть 06): Операции файлового ввода-вывода в MQL5, как в Python

Внедрение в MQL5 практических модулей из других языков (Часть 06): Операции файлового ввода-вывода в MQL5, как в Python

MetaTrader 5Торговые системы |
134 0
Omega J Msigwa
Omega J Msigwa

Разделы


Введение

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

Язык программирования MQL5 содержит множество встроенных способов чтения и записи для бесчисленного количества типов файлов, но их не всегда достаточно.

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

В MQL5:

void OnStart()
  {
//---
    
    string filename = "readme.txt";
    int handle = FileOpen(filename,FILE_READ|FILE_TXT|FILE_ANSI, "", CP_UTF8);
    if (handle == INVALID_HANDLE)
      {
         printf("Failed to open '%s' Error = %d",filename,GetLastError());
         return;
      }
    
    while (!FileIsEnding(handle))
      {
         string data = FileReadString(handle);
         Print(data);
      }
  }

В Python:

with open(f"{files_path}\\readme.txt", "r") as file:
    for line in file:
        print(line.rstrip())

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

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


Понимание функции для операций ввода-вывода в Python

Для создания MQL5-функции для операций ввода-вывода, как в Python, нам необходимо понять внутреннюю работу функции с именем open.

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

Сигнатура функции.

open(
    file, 
    mode="r", 
    buffering=-1,
    encoding=None,
    errors=None,
    newline=None,
    closefd=True,
    opener=None
)

Аргументы.

АргументОписаниеЗначение по умолчанию
fileОбъект, подобный пути и содержащий путь к файлу, который необходимо открыть.Необходимый.
modeСтрока для указания режима открытия файла (например, 'r', 'w', 'b' и т. д.).'r'.
bufferingЦелочисленное значение, используемое для настройки правил буферизации.-1
encodingНазвание метода кодирования, используемого для кодирования или декодирования файла.Нет.
newlineСтрока, определяющая способ обработки новых символов из потока.Нет.
closefdЛогическое значение, определяющее, следует ли закрывать хэндл файла.Верно.
openerВызываемая функция, используемая в качестве пользовательского средства открытия целевого файла.Нет.

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

int CFileIO::open(const string filename, 
                  const string mode, 
                  uint cp_encoding = CP_UTF8, 
                  const bool common = false, 
                  const string newline = "", 
                  bool is_unicode=false);

Дополнительные переменные, такие как common (для выбора, находится ли файл в каталоге common или в пути к данным MQL5), и переменная is_unicode (для выбора, является файл юникодным, то есть содержит строки типа UNICODE (двухбайтовые символы) при значении true или содержит строки типа ANSI (однобайтовые символы) при значении false).

Наиболее интересный аргумент функции open является mode.

Режимы файлов в языке Python

Режим работы с файлом указывает языку Python, какие операции (чтение, запись и т. д.) вы хотите выполнить с файлом.

РежимОписание
'r'Только чтение. Вызывает ошибку ввода-вывода, если файл не существует.
'r+'Чтение и запись. Вызывает ошибку ввода-вывода, если файл не существует.
'w'Только запись. Перезаписывает файл, если он существует; в противном случае создает новый.
'w+'Чтение и запись. Перезаписывает файл или создает новый.
'a'Только добавление. Добавляет данные в конец. Создает файл, если он не существует.
'a+'Чтение и добавление. Указатель в конце. Создает файл, если он не существует.
'rb'Чтение в бинарном режиме. Файл должен существовать.
'rb+'Чтение и запись в бинарном режиме. Файл должен существовать.
'wb'Запись в бинарном режиме. Перезаписывает или создает новый файл.
'wb+' Чтение и запись в бинарном режиме. Перезаписывает или создает новый файл. 
'ab' Добавление в бинарном режиме. Создает файл, если он не существует. 
'ab+' Чтение и добавление данных в бинарном режиме. Создает файл, если он не существует. 

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


Автоматический выбор флагов файла 

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

int CFileIO::flagsgen(const string file_mode, bool &is_append)
  {
//--- default flag(s) for txt files

   int flags = 0;
   string mode = file_mode;
   StringToLower(mode);

   for(int i = 0; i < (int)mode.Length(); i++)
     {
      switch(StringGetCharacter(mode, i))
        {
         case 'r':
            flags |= (FILE_READ | FILE_SHARE_READ);
            break;
         case 'w':
            flags |= (FILE_WRITE | FILE_SHARE_WRITE);
            break;
         case 'a':
           {
            flags |= FILE_WRITE;
            is_append = true;
            break;
           }
         case '+':
            flags |=  FILE_READ | FILE_WRITE | FILE_SHARE_READ | FILE_SHARE_WRITE;
            break;
         case 'b':
            flags |= FILE_BIN;
            break;
         case 'x':
            flags |= (FILE_REWRITE | FILE_WRITE | FILE_SHARE_WRITE);
            break;
        }
     }

   return flags;
  }

Переменная is_append полезна для вызова метода FileSeek при добавлении информации в конец файла.

Обратите внимание, что у нас есть FILE_SHARE_READ всякий раз при наличии флага FILE_READ и FILE_SHARE_WRITE — всякий раз, когда присутствует флаг FILE_WRITE. 

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

Чтобы сделать этот процесс еще эффективнее, можем добавить необязательную переменную shared_IO (если ее значение установлено как true, это означает, что мы можем выполнять операции ввода-вывода с файлами, используемыми другими программами, и другие программы могут делать то же самое, если файл открыт в MetaTrader 5).

int CFileIO::flagsgen(const string file_mode, bool &is_append, bool shared_IO=true)
  {
//--- default flag(s) for txt files

   int flags = 0;
   string mode = file_mode;
   StringToLower(mode);

   for(int i = 0; i < (int)mode.Length(); i++)
     {
      switch(StringGetCharacter(mode, i))
        {
         case 'r':
            flags |= FILE_READ;
            
            if (shared_IO)
               flags |= FILE_SHARE_READ;
            break;
         case 'w':
            flags |= FILE_WRITE;
            
            if (shared_IO)
               flags |= FILE_SHARE_WRITE;
            break;
         case 'a':
           {
            flags |= FILE_WRITE;
            is_append = true;
            break;
           }
         case '+':
            flags |=  FILE_READ | FILE_WRITE;
            
            if (shared_IO)
               flags |= FILE_SHARE_READ | FILE_SHARE_WRITE;
            break;
         case 'b':
            flags |= FILE_BIN;
            break;
         case 'x':
            flags |= FILE_REWRITE | FILE_WRITE;
            
            if (shared_IO)
               flags |= FILE_SHARE_WRITE;
            break;
        }
     }

   return flags;
  }

Значение передается непосредственно из функции с именем open.

   static int        open(const string filename, 
                          const string mode, 
                          uint cp_encoding = CP_UTF8, 
                          const bool common = false, 
                          const string newline = "", 
                          bool is_unicode=false,
                          bool shared_IO=true);


MQL5-метод Open, аналогичный такому же методу в Python

Используя сгенерированные в зависимости от режима файла флаги, мы теперь можем открыть любой доступный файл.

int CFileIO::open(const string filename, 
                  const string mode, 
                  uint cp_encoding = CP_UTF8, 
                  const bool common = false, 
                  const string newline = "", 
                  bool is_unicode=false,
                  bool shared_IO=true)
  {
//---

   bool is_append = false;
   int flags = flagsgen(mode, is_append, shared_IO);
   string file_extension = getFileExtension(filename);
   
//---

   if (file_extension=="")
     return INVALID_HANDLE;
   
//--- we add select a file from the common folder if commo=true

   if(common)
      flags |= FILE_COMMON;

//---
   
   bool is_binary = (flags & FILE_BIN) != 0;
   if (!is_binary) //Avoid unicode and ANSI flags during a binary mode
    {
      if (is_unicode)
         flags |= FILE_UNICODE;
      else
         flags |= FILE_ANSI;
    }
   
//--- Open a file for either reading or writing

   int h = FileOpen(filename, flags, newline, cp_encoding);
   if(h == INVALID_HANDLE)
     {
      printf("Failed to read '%s', Error = %s", filename, fileErrorsDescription(GetLastError()));
      return INVALID_HANDLE;
     }

//---

   if(is_append)
      FileSeek(h, 0, SEEK_END);

   return h;
  }

Недостаточно генерировать флаги в зависимости от режима файла; к основным нам необходимо добавить еще несколько очень полезных флагов. Вот где пригодятся эти флаги:

01. Определение местоположения файла (либо в пути передачи данных MQL5, либо в общей папке)

   if(common)
      flags |= FILE_COMMON;

02. Принятие решения о необходимости использовать кодировку ANSI или UNICODE для чтения байтовых символов.

   bool is_binary = (flags & FILE_BIN) != 0;
   if (!is_binary) //Avoid unicode and ANSI flags during a binary mode
    {
      if (is_unicode)
         flags |= FILE_UNICODE;
      else
         flags |= FILE_ANSI;
    }

Похоже, что при наличии бинарного флага FILE_BIN язык MQL5 обрабатывает файл как поток байтов; нет необходимости особо беспокоиться о флагах UNICODE и ANSI.

Теперь мы можем использовать эту универсальную функцию для открытия файлов различных типов в MetaTrader 5.

#include <PyMQL5\\fileIO\\fileIO.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
    
    CFileIO::open("readme.txt", "r+"); //open the file in read/write mode
    CFileIO::open("MT5.log", "r"); //readonly 
    CFileIO::open("mydata.csv", "r+"); //read/write mode for a CSV file
    CFileIO::open("mydata.xlsx", "r"); //A less common filetype
    CFileIO::open("tiny-cat.jpg", "rb"); //an image file, readonly binary file mode
    CFileIO::open("array.bin", "w+b"); //Read and write mode for a binary file
 }

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

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

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

Класс (объект) CFile

class CFile
  {
protected:

   int               m_handle;
   string            m_filename;
   int               m_flags;

   bool              isHandleOk(string func)
     {
      if(m_handle == INVALID_HANDLE)
        {
         printf("%s Invalid file handle received", func);
         return false;
        }
      return true;
     }

public:
                     CFile(void)
     {
      m_handle = INVALID_HANDLE;
      m_flags  = 0;
      m_filename = "";
     };

                    ~CFile(void)
     {

     };

   //--- configurations

   void              Config(const string filename, const int handle, const int flags)   
     {
      m_filename = filename;
      m_handle = handle;
      m_flags = flags;
     }

   void              close();
};

Теперь у нас должен быть удобный способ обработки и манипулирования открытым файлом.

void OnStart()
  {
//---

    CFile f = CFileIO::open("readme.txt", "r"); //open the file in read-only mode
    f.close(); //closing after you are done with it

    f = CFileIO::open("MT5.log", "r"); //readonly 
    f.close();

    f = CFileIO::open("mydata.csv", "r+"); //read/write mode for a CSV file
    f.close();

    f = CFileIO::open("array.bin", "wb+");
    f.close();  
  }


Чтение данных/информации из файлов

Самое замечательное в файловых операциях в Python — это то, что они предоставляют пользователям контроль над чтением и интерпретацией информации, получаемой из файлов.

import csv

files_path = r"C:\Users\omega\AppData\Roaming\MetaQuotes\Terminal\FB9A56D617EDDDFE29EE54EBEFFE96C1\MQL5\Files"

with open(f"{files_path}\\readme.txt", "r") as file:
    for line in file: # reading a file line by line
        print(line.rstrip())
        

with open(f"mydata.csv", "r", encoding="utf-8-sig", newline='') as csvfile:
    csvreader = csv.reader(csvfile, delimiter=',')
    for row in csvreader: # reading a csv file row by row
        print(row)

Результаты.

hello, this is a readme file with plenty of information to read from.

Это третья строка после пробела.
['DateTime', 'Open', 'High', 'Low', 'Close']
['12/27/2023 19:00', '2081.72', '2082.53', '2079.52', '2081.94']
['12/27/2023 18:00', '2078.97', '2082.41', '2076.73', '2081.69']
['12/27/2023 17:00', '2070.29', '2081.88', '2069.01', '2078.93']
['12/27/2023 16:00', '2068.33', '2071.62', '2066.6', '2070.3']

Хотя MQL5 также предоставляет нам способ отслеживать информацию по строкам файла внутри цикла while, который проходит по всем строкам файла, код на Python представляется гораздо более удобным. Реализуем аналогичную функциональность в MQL5.

Поскольку в MQL5 имеется несколько функций для чтения данных из файлов, таких как FileReadString, FileReadDouble, FileReadLong и т. д., мы можем использовать шаблон, чтобы функция работала со всеми поддерживаемыми типами данных, что избавляет пользователя от необходимости вручную подбирать функцию чтения для типа передаваемой по ссылке переменной, поскольку они будут получать тип результирующих данных в зависимости от типа такой переменной.

template <typename T>
T CFile::__readline__()
  {
   T datatype = T(0);

// string
   if(typename(T) == typename(string))
      datatype = (T)FileReadString(m_handle);

// int
   if(typename(T) == typename(int))
      datatype = (T)FileReadInteger(m_handle);

// long
   if(typename(T) == typename(long))
      datatype = (T)FileReadLong(m_handle);

// double
   if(typename(T) == typename(double))
      datatype = (T)FileReadDouble(m_handle);

// float (read as double and cast)
   if(typename(T) == typename(float))
      datatype = (T)FileReadDouble(m_handle);

// bool (read as int and cast)
   if(typename(T) == typename(bool))
      datatype = (T)FileReadInteger(m_handle);

// datetime (read as long and cast)
   if(typename(T) == typename(datetime))
      datatype = (T)FileReadLong(m_handle);

   return datatype;
  }

Затем эта функция может быть унаследована в публичной функции под названием readline.

template <typename T>
bool CFile::readline(T &line)
  {

   if(!isHandleOk(__FUNCTION__))
      return false;

//---

   while(!FileIsEnding(m_handle))
     {
      line = __readline__<T>();
      return true;
     }

   return false;
  }

Пример использования:

#include <PyMQL5\\fileIO\\fileIO.mqh>
#include <PyMQL5\\fileIO\\csv.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Reading a text file

    CFile f = CFileIO::open("readme.txt", "r"); //open the file in read-only mode
    
    string text;
    while(f.readline(text))
       Print(text);
    
    f.close(); //closing after you are done with it
 }

Результаты.

ND      0       22:34:35.591    Test file IO (EURUSD,H1)        hello, this is a readme file with a plenty of information to read from.
DD      0       22:34:35.591    Test file IO (EURUSD,H1)        
GG      0       22:34:35.591    Test file IO (EURUSD,H1)        This is a third line after a space.

Эта функция даже способна читать бинарные файлы.

void OnStart()
  {
    f = CFileIO::open("array.bin", "wb+");
    
    int value, count = 0;
    while (f.readline(value)) 
     {
       printf("array[%d]: %d",count,value);
       count++;
     }
      
    f.close();  
 }

Результаты.

PI      0       17:27:44.966    Test file IO (EURUSD,H1)        array[0]: 1
RR      0       17:27:44.966    Test file IO (EURUSD,H1)        array[1]: 2
PK      0       17:27:44.966    Test file IO (EURUSD,H1)        array[2]: 3
ND      0       17:27:44.966    Test file IO (EURUSD,H1)        array[3]: 4
PM      0       17:27:44.966    Test file IO (EURUSD,H1)        array[4]: 5
RF      0       17:27:44.966    Test file IO (EURUSD,H1)        array[5]: 6

Эта функция (readline) прекрасно работает с различными типами файлов. При работе с CSV-файлами нам необходимы специальные функции для анализа строк и безопасного извлечения содержимого из всех строк.

В языке Python есть небольшой модуль csv, который отвечает за чтение и запись в CSV-файлы соответственно.

import csv

with open(f"mydata.csv", "r", encoding="utf-8-sig", newline='') as csvfile:
    csvreader = csv.reader(csvfile, delimiter=',')
    for row in csvreader: # reading a csv file row by row
        print(row)

Результаты.

['DateTime', 'Open', 'High', 'Low', 'Close']
['12/27/2023 19:00', '2081.72', '2082.53', '2079.52', '2081.94']
['12/27/2023 18:00', '2078.97', '2082.41', '2076.73', '2081.69']
['12/27/2023 17:00', '2070.29', '2081.88', '2069.01', '2078.93']
['12/27/2023 16:00', '2068.33', '2071.62', '2066.6', '2070.3']
['12/27/2023 15:00', '2067.68', '2069.73', '2066.15', '2068.38']

Обратили внимание, как Python считывает все значения из каждой строки CSV-файла как строковые данные?

Это замечательно, потому что из всех переменных строковые безопаснее всего преобразовывать в другие переменные, не говоря уже о том, что CSV-файлы обычно содержат данные разных типов. Отличная идея — хранить их все вместе в массиве, отформатированном в виде строки.

Мы можем создать аналогичный класс в MQL5.

#include "fileIO.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CSVReader
  {
protected:
   int               m_handle;
   string            m_delimiter;
   char              m_quote;
   bool              m_doublequote;
   bool              m_skipinitialspace;
   char              m_escape;
   uint              cols_found;
   
   string StringTrim(string s)
     { 
       StringTrimLeft(s); 
       StringTrimRight(s); 
       return s; 
     }
   
   void ParseCSVLine(string line, string &fields[]);
   
public:
                     CSVReader(CFile &file,
                               const string delimiter = ",",
                               const char quotechar = '"',
                               const char escapechar = '\\',
                               const bool doublequote = true,
                               const bool skipinitialspace = false
                              );

                    ~CSVReader(void);
                    bool readRow(string &row[]);
  };

Чтобы предотвратить открытие некоторых больших файлов, мы можем добавить ряд проверок.

Проверяем, превышает ли размер файла какой-либо заранее определенный размер.

#define MAX_FILE_SIZE_MB 200

//--- Getting the file size in MegaBytes

   double file_size_MB = (double)FileSize(m_handle) / (double)1e6;
   printf("%s Filesize in ~ MB [%.3f]", __FUNCTION__, file_size_MB);

   if((uint)file_size_MB > MAX_FILE_SIZE_MB)
     {
      printf("%s Failed, CSV filesize [%.3f] in MBs is greater than the maximum file size accepted [%I64u] in MBs. To pass this limit, change the variable 'MAX_FILE_SIZE_MB'", 
             __FUNCTION__, file_size_MB, MAX_FILE_SIZE_MB);
      return;
     }

Кроме того, мы проверяем, достаточно ли памяти для хранения файла, который мы пытаемся открыть.

//--- Ensuring the CSV file size doesn't exceed available memory for the Terminal

   ulong free_ram_MB = (ulong)TerminalInfoInteger(TERMINAL_MEMORY_AVAILABLE);
   printf("Free Terminal RAM ~ %I64u MB", free_ram_MB);

//--- The CSV file isn't supposed to be greater in size than half of the available memory

   if(file_size_MB >= free_ram_MB)
     {
      printf("Filesize in MB [%.3f] is greater than available memory [%I64u] in the Terminal", file_size_MB, free_ram_MB);
      return;
     }

Все эти проверки находятся внутри конструктора класса.

CSVReader::CSVReader(CFile &file,
                     const string delimiter = ",",
                     const char quotechar = '"',
                     const char escapechar = '\\',
                     const bool doublequote = true,
                     const bool skipinitialspace = false)
  {
//---

   m_handle = file.getHandle();
   m_delimiter = delimiter;
   m_quote = quotechar;
   m_doublequote = doublequote;
   m_skipinitialspace = skipinitialspace;
   m_escape = escapechar;
   
//--- Getting the file size in MegaBytes

   double file_size_MB = (double)FileSize(m_handle) / (double)1e6;
   printf("%s Filesize in ~ MB [%.3f]", __FUNCTION__, file_size_MB);

   if((uint)file_size_MB > MAX_FILE_SIZE_MB)
     {
      printf("%s Failed, CSV filesize [%.3f] in MBs is greater than the maximum file size accepted [%I64u] in MBs. To pass this limit, change the variable 'MAX_FILE_SIZE_MB'", 
             __FUNCTION__, file_size_MB, MAX_FILE_SIZE_MB);
      return;
     }

//--- Ensuring the CSV file size doesn't exceed available memory for the Terminal

   ulong free_ram_MB = (ulong)TerminalInfoInteger(TERMINAL_MEMORY_AVAILABLE);
   printf("Free Terminal RAM ~ %I64u MB", free_ram_MB);

//--- The CSV file isn't supposed to be greater than half of the available memory

   if(file_size_MB >= free_ram_MB)
     {
      printf("Filesize in MB [%.3f] is greater than available memory [%I64u] in the Terminal", file_size_MB, free_ram_MB);
      return;
     }
  }

Аргументы конструктора

АргументОписаниеПо умолчанию
csv_handleДействительный хэндл CSV-файла, возвращаемый функцией FileOpen(). Речь идет об уже открытом CSV-файле. Программа считывания работает непосредственно с этим хэндлом и не управляет открытием или закрытием файла.Req
delimiterСтроковый символ, используемый для разделения полей внутри строки. К распространенным значениям относятся запятая (,), точка с запятой (;) и знак табуляции (\t).","
quotecharСимвол, используемый для полей с кавычками, содержащих разделители или специальные символы.
Все, что находится внутри совпадающих символов котировок, рассматривается как литеральные данные.
' " '
escapecharСимвол, используемый во избежание специальных символов внутри заключенного в кавычки поля. Например, символ \" позволяет разместить символ кавычек внутри значения, заключенного в кавычки. '\\'
doublequoteЭтот символ управляет обработкой кавычек внутри полей, заключенных в кавычки. При значении true. Два последовательных символа кавычек ("") интерпретируются как одна литеральная кавычка, что соответствует стандартному поведению CSV-файлов.""
skipinitialspaceЕсли эта функция включена, следующие непосредственно за разделителем пробелы игнорируются. Это полезно для анализа CSV-файлов с нестрогим форматированием, таких как "A,B,C" вместо "A, B, C".false

После открытия CSV-файла мы создадим объект CSVReader и присвоим его переменной с именем reader. Затем создадим массив с именем row[], и все строки из CSV-файла будут итеративно сохраняться в этот массив.

void OnStart()
  {
    int csv_file = CFileIO::open("mydata.csv", "r+"); //read/write mode for a CSV file
    
    CSVReader reader(csv_file, ",");
    
    string row[];
    while(reader.readRow(row))
      ArrayPrint(row);
    
    CFileIO::close(csv_file);
  }

Результаты.

CP      0       00:51:24.810    Test file IO (EURUSD,H1)        CSVReader::CSVReader Filesize in ~ MB [0.001]
CD      0       00:51:24.815    Test file IO (EURUSD,H1)        Free Terminal RAM ~ 32245 MB
IJ      0       00:51:24.816    Test file IO (EURUSD,H1)        "ÿDateTime"      "Open"           "High"           "Low"            "Close"          "Strings Column"
HS      0       00:51:24.816    Test file IO (EURUSD,H1)        [0] "12/27/2023 19:00"                       "2081.72"                                "2082.53"                               
GJ      0       00:51:24.816    Test file IO (EURUSD,H1)        [3] "2079.52"                                "2081.94"                                "Yes, this column has text with commas."
DM      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 18:00" "2078.97"          "2082.41"          "2076.73"          "2081.69"          "None"            
DQ      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 17:00" "2070.29"          "2081.88"          "2069.01"          "2078.93"          "None"            
DH      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 16:00" "2068.33"          "2071.62"          "2066.6"           "2070.3"           "Some value"      
PK      0       00:51:24.816    Test file IO (EURUSD,H1)        [0] "12/27/2023 15:00"          "2067.68"                   "2069.73"                  
NE      0       00:51:24.816    Test file IO (EURUSD,H1)        [3] "2066.15"                   "2068.38"                   "Another value, with comma"
PI      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 14:00" "2068.21"          "2070.29"          "2064.37"          "2067.69"          "None"            
CM      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 13:00" "2064.73"          "2068.87"          "2064.62"          "2068.19"          "None"            
EL      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 12:00" "2068.38"          "2068.72"          "2061.51"          "2064.75"          "Some value"      
HE      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 11:00" "2067.39"          "2069.28"          "2067.31"          "2068.38"          "None"            
DH      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 10:00" "2066.09"          "2068.31"          "2065.85"          "2067.38"          "None"            
CM      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 9:00" "2065.06"         "2066.38"         "2064.81"         "2066.09"         "None"           
KO      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 8:00" "2064.7"          "2067.43"         "2064.44"         "2065.07"         "None"           
GR      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 7:00" "2065.88"         "2066.26"         "2064.42"         "2064.7"          "None"           
KE      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 6:00" "2064.6"          "2066"            "2064.11"         "2065.88"         "None"           
NI      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 5:00" "2065.44"         "2066.59"         "2064.44"         "2064.62"         "None"           
CK      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 4:00" "2066.74"         "2067.28"         "2064.8"          "2065.44"         "None"           
HO      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 3:00" "2065.58"         "2067.89"         "2064.95"         "2066.74"         "None"           
RQ      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 2:00" "2066.2"          "2066.52"         "2063.97"         "2065.63"         "None"           
RD      0       00:51:24.816    Test file IO (EURUSD,H1)        "12/27/2023 1:00" "2068.08"         "2068.62"         "2066.05"         "2066.2"          "None"           


Запись данных/информации в файлы

Запись данных в файлы требует несколько иного подхода, чем чтение.

Мы можем использовать метод FileWrite, который принимает переменные любого типа данных.

template <typename T>
static bool CFileIO::write(int file_handle, T info)
  {
   if(FileWrite(file_handle, info) == 0)
     {
      printf("%s failed to write to a file. Error = %s", __FUNCTION__, fileErrorsDescription(GetLastError()));
      return false;
     }

   return true;
  }

Попробуем записать новые данные в конец уже существующего файла.

В файловом режиме: символ r предназначен для чтения, + предназначен для чтения и записи и a для вставки (добавления) новой информации в конец файла.

void OnStart()
  {
    CFile f = CFileIO::open("readme.txt", "r+a");
    f.write("Newly added data | "+string(TimeLocal()));
    f.close();
  }

После нескольких запусков скрипта получился приведенный ниже файл readme.txt с новыми строками данных.

hello, this is a readme file with a plenty of information to read from.

Это третья строка после пробела.
Newly added data | 2025.12.22 06:32:35
Newly added data | 2025.12.22 06:33:05
Newly added data | 2025.12.22 06:33:19

Метод FileWrite, если ему передана динамическая (шаблонная) переменная, может работать со всеми переменными, кроме переменных массива. 

Для записи массивов с данными в файл можно использовать функцию FileWriteArray.

template <typename T>
bool  CFile::write(T &info[])
  {
   if(!isHandleOk(__FUNCTION__))
      return false;

//---

   if(FileWriteArray(m_handle, info) == 0)
     {
      printf("%s failed to write an array to a file. Error = %s", __FUNCTION__, fileErrorsDescription(GetLastError()));
      return false;
     }

   return true;
  }

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

void OnStart()
  {
    CFile f = CFileIO::open("array.txt", "wt");
    
    string data[] = {"data01", "data02", "data03", "data04"};
    f.write( data);
    f.close();
 }

Результаты.

2025.12.22 06:46:16.324 Test file IO (EURUSD,H1)        CFile::write<string> failed to write an array to a file. Error = The file must be opened as a text

Получаем сообщение об ошибке, указывающее на то, что наш файл следует открывать как текстовый файл. 

Это связано с тем, что, хотя мы и могли читать и записывать данные в текстовые файлы, мы никогда фактически не открывали их с флагом FILE_TXT; у нас пока нет способа обрабатывать это в аргументе file mode.

Для текстовых файлов нам необходимо принимать букву 't', что затем активирует флаг FILE_TXT.

int CFileIO::flagsgen(const string file_mode, bool &is_append, bool shared_IO = true)
  {
//--- default flag(s) for txt files

   int flags = 0;
   string mode = file_mode;
   StringToLower(mode);

   for(int i = 0; i < (int)mode.Length(); i++)
     {
      switch(StringGetCharacter(mode, i))
        {
         case 'r':
            flags |= FILE_READ;

            if(shared_IO)
               flags |= FILE_SHARE_READ;
            break;
         case 'w':
            flags |= FILE_WRITE;

            if(shared_IO)
               flags |= FILE_SHARE_WRITE;
            break;

         //--- other cases

         case 't': //Additional text mode 
            flags |= FILE_TXT;
            break;
        }
     }

   return flags;
  }

Таким образом, чтобы обойти ошибки, подобные описанной выше, достаточно указать символ "t" при открытии текстового файла или файла на основе текста.

void OnStart()
  {
    CFile f = CFileIO::open("array.txt", "wt");
    
    string data[] = {"data01", "data02", "data03", "data04"};
    f.write( data);
    f.close();
 }

Результаты.

Запись в CSV-файл

Поскольку в CSV-файле используется двухмерный подход к хранению данных, нам необходимо по-другому подходить к записи в него новых данных. Для чтения подобных файлов мы использовали программу для чтения CSV-файлов; на этот раз мы воспользуемся программой для записи CSV-файлов.

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

class CSVWriter
{
protected:
   int    m_handle;
   string m_delimiter;
   char   m_quote;
   char   m_escape;
   bool   m_doublequote;

   string EscapeField(const string value);

public:

   CSVWriter(CFile &file,
             const string delimiter = ",",
             const char quotechar = '"',
             const char escapechar = '\\',
             const bool doublequote = true);

   bool writeRow(const string &row[]);
};
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CSVWriter::CSVWriter(CFile &file,
                     const string delimiter,
                     const char quotechar,
                     const char escapechar,
                     const bool doublequote)
{
   m_handle      = file.getHandle();
   m_delimiter   = delimiter;
   m_quote       = quotechar;
   m_escape      = escapechar;
   m_doublequote = doublequote;
}

Перед безопасной записью в CSV-файл необходимо экранировать все полученные данные (поля).

string CSVWriter::EscapeField(const string value)
{
   bool must_quote = false;
   string out = "";

   int len = StringLen(value);
   for(int i = 0; i < len; i++)
   {
      char ch = (char)StringGetCharacter(value, i);

      // Detect if quoting is needed
      if(ch == m_quote ||
         ch == '\n' ||
         ch == '\r' ||
         CharToString(ch) == m_delimiter)
      {
         must_quote = true;
      }

      // Quote escaping
      if(ch == m_quote)
      {
         if(m_doublequote)
            out += CharToString(m_quote) + CharToString(m_quote); // ""
         else
            out += CharToString(m_escape) + CharToString(m_quote); // \"
      }
      else
      {
         out += CharToString(ch);
      }
   }

   if(must_quote)
      return CharToString(m_quote) + out + CharToString(m_quote);

   return out;
}

Функция writeRow отвечает за запись значений в CSV-файл.

bool CSVWriter::writeRow(const string &row[])
{
   string line = "";
   int cols = ArraySize(row);

   for(int i = 0; i < cols; i++)
   {
      if(i > 0)
         line += m_delimiter;

      line += EscapeField(row[i]);
   }

   FileWriteString(m_handle, "\n"+line);
   return true;
}

Попробуем вставить новые строки в файл mydata.csv.

void OnStart()
  {
   CFile f = CFileIO::open("mydata.csv","w+a");
   CSVWriter writer(f, ",");
   
   double open = iOpen(Symbol(), Period(), 0);
   double high = iHigh(Symbol(), Period(), 0);
   double low = iLow(Symbol(), Period(), 0);
   double close = iClose(Symbol(), Period(), 0);
   
   string row[] = {string(TimeCurrent()), (string)open, (string)high, (string)low, (string)close};
   
   writer.writeRow(row);
   f.close();
  }

Результаты.


Дополнительные методы

Встроенный механизм ввода-вывода в языке Python содержит несколько полезных методов, которые значительно упрощают чтение и запись данных.

Следует отметить, что не все методы, перечисленные здесь и во всей статье, заимствованы из Python; разработка некоторых из них вдохновлена самим языком MQL5.

01. Метод read()

Этот метод используется для чтения всего содержимого файла в виде строки.

with open(f"{files_path}\\readme.txt", "r") as file:
    print(file.read())    

Результаты.

(venv) python main.py
hello, this is a readme file with a plenty of information to read from.

Это третья строка после пробела.

В MQL5 мы считываем все данные из текстового файла (по умолчанию), добавляя значения в одну большую строку, разделенную кодом новой строки ("\n").

string CFile::read(int size = -1)
  {
   if(!isHandleOk(__FUNCTION__))
      return "";

//---

   string result = "";
   if(size < 0) // read entire file
     {
      while(!FileIsEnding(m_handle))
        {
         result += FileReadString(m_handle);

         if(FileIsLineEnding(m_handle))
            result += "\n";
        }
     }
   else
     {
      result = FileReadString(m_handle, size);
     }

   return result; //but not here
  }

Пример использования.

void OnStart()
  {
   CFile f = CFileIO::open("readme.txt", "rt");
   Print(f.read());
   
   f.close();
 }

Результаты.

NQ      0       08:32:45.949    Test file IO (EURUSD,H1)        hello, this is a readme file with a plenty of information to read from.
DQ      0       08:32:45.949    Test file IO (EURUSD,H1)        
GJ      0       08:32:45.949    Test file IO (EURUSD,H1)        This is a third line after a space.

02. Метод tell()

Эта функция возвращает текущую позицию файлового дескриптора в байтах от начала файла.

int CFile::tell()
  {
   if(!isHandleOk(__FUNCTION__))
      return -1;

   return (int)FileTell(m_handle);
  }

03. Метод flush()

Записывает на диск все данные, оставшиеся в буфере входного/выходного файла.

void CFile::flush()
  {
   if(!isHandleOk(__FUNCTION__))
      return;

   FileFlush(m_handle);
  }

04. Метод seek()

Функция перемещает положение указателя файла на указанное количество байтов относительно заданной позиции.

void CFile::seek(const long offset, const ENUM_FILE_POSITION origin)
  {
   //--- check handle
   if (!isHandleOk(__FUNCTION__))
     return;
   
   FileSeek(m_handle,offset,origin);
  }

05. Проверка доступности файла для чтения и записи

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

Для isreadable() проверяем, присутствует ли флаг FILE_READ среди флагов файла.

bool              isreadable() { return (m_flags & FILE_READ) != 0; }

Для функции iswritable() мы проверяем, присутствует ли флаг FILE_WRITE среди флагов файла. 

bool              iswritable() { return (m_flags & FILE_WRITE) != 0; }

Пример использования.

void OnStart()
  {
    CFile f = CFileIO::open("readme.txt", "r"); //open the file in read-only mode
    
    printf("Reading a text file line by line....");
    string text;
    while(f.readline(text))
       Print(text);
    
    Print("is writable: ", f.iswritable());
    Print("is readable: ", f.isreadable());  
    
    f.close(); //closing after you are done with it
  }

Результаты.

CG      0       16:54:21.159    Test file IO (EURUSD,H1)        is writable: false
FQ      0       16:54:21.159    Test file IO (EURUSD,H1)        is readable: true


Заключительные мысли

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

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

С наилучшими пожеланиями.


Таблица вложений

Имя файлаОписание и использование
Include\PyMQL5\fileIO\fileIO.mqhСодержит классы CFile и CFileIO для работы со всеми типами файлов в MetaTrader 5.
Include\PyMQL$\fileIO\csv.mqhОн содержит два класса, CSVReader и CSVWriter, для чтения и записи CSV-файлов соответственно.
Test file IO.mq5Итоговый скрипт (тестовая площадка) для всех методов, рассмотренных в этой статье.
Files\* Здесь собраны все файлы, необходимые для тестирования нашего кода.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20695

Прикрепленные файлы |
Attachments.zip (22.35 KB)
Разработка инструментария для анализа Price Action (Часть 27): Инструмент выявления снятия ликвидности с MA-фильтром Разработка инструментария для анализа Price Action (Часть 27): Инструмент выявления снятия ликвидности с MA-фильтром
Понимание тонких механизмов, стоящих за движением цены, может дать вам серьезное преимущество. Одно из таких явлений – снятие ликвидности, то есть целенаправленный прием, который крупные трейдеры, особенно институциональные участники, используют, чтобы провести цену через ключевые уровни поддержки или сопротивления. Эти уровни часто совпадают со скоплениями стоп-лоссов розничных трейдеров, создавая зоны ликвидности, которые крупные игроки могут использовать для входа в крупные позиции или выхода из них с минимальным проскальзыванием.
Код, слёзы и Algo Forge Код, слёзы и Algo Forge
В статье рассматривается переход на MQL5 Algo Forge как современный и удобный формат публикации программного кода и вложений к статьям. Использование репозиториев вместо классических ZIP-архивов и исходных кодов позволяет поддерживать проекты в актуальном состоянии, оперативно вносить правки и профессионально взаимодействовать с аудиторией. Приводятся рекомендации по быстрой миграции наработок в облачную среду через интерфейс MetaEditor.
Торговые инструменты на MQL5 (Часть 19): Создание интерактивной палитры инструментов графической разметки Торговые инструменты на MQL5 (Часть 19): Создание интерактивной палитры инструментов графической разметки
В этой статье мы создадим интерактивную палитру инструментов в MQL5 для рисования графиков с возможностью перетаскивания, изменения размера панелей и переключения тем. Мы добавим кнопки для таких инструментов, как перекрестие, линии трендов, прямые, прямоугольники, числа Фибоначчи, текст и стрелки, обрабатывающих события мыши для активации и получения указаний. Эта система улучшает анализ торговли благодаря настраиваемому пользовательскому интерфейсу, поддерживающему взаимодействие с графиками в режиме реального времени
Самооптимизирующиеся советники на MQL5 (Часть 12): Построение линейных классификаторов с использованием факторизации матриц Самооптимизирующиеся советники на MQL5 (Часть 12): Построение линейных классификаторов с использованием факторизации матриц
В данной статье рассматривается важная роль факторизации матриц в алгоритмической торговле, в частности в приложениях MQL5. От регрессионных моделей до многоклассовых классификаторов — мы рассмотрим практические примеры, демонстрирующие, насколько легко эти методы можно интегрировать с помощью встроенных функций MQL5. Независимо от того, занимаетесь ли вы прогнозированием направления движения цен или моделированием поведения индикаторов, данное руководство заложит прочную основу для создания интеллектуальных торговых систем с использованием матричных методов.