English Русский Deutsch
preview
MQL5で他の言語の実用的なモジュールを実装する(第6回):MQL5におけるPython風ファイルI/O操作

MQL5で他の言語の実用的なモジュールを実装する(第6回):MQL5におけるPython風ファイルI/O操作

MetaTrader 5トレーディングシステム |
42 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でのファイルI/Oの動作をPythonと比較して、ネイティブAPI 上に高レベル(Pythonのような)の抽象化を設計する方法を探ります。その目的は、MQL5プログラミング言語における入出力操作に対して、シンプルでありながら効果的で安全なアプローチを提供することです。


Pythonにおける入出力操作の関数を理解する

MQL5でI/O操作をおこなう関数を作成するには、Pythonと同様に、openという名前の関数の内部動作を理解する必要があります。

Pythonの組み込み関数であるopen()は、ファイルを開いて対応するファイルオブジェクトを返すために使用されます。この関数を使用すると、ファイルの読み書きが可能になり、ファイルモード(テキスト/バイナリなど)やエンコードに関するさまざまなオプションを選択できます。

以下が関数シグネチャです。

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ファイルディスクリプタを閉じるかどうかを定義するブール値true
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 (ファイルが共通ディレクトリの下にあるかMQL5データパスの下にあるかを選択する)やis_unicode(ファイルがunicodeであるかどうかを選択し、trueに設定するとUNICODE型の2バイトシンボルの文字列が含まれ、falseに設定するとANSI型の文字列1バイトシンボルの文字列が含まれている)などがあります。

関数openの最も興味深い引数はmodeです。

Pythonのファイルモード

ファイルモードは、Pythonに対してファイルに対してどのような操作(読み取り、書き込みなど)を実行したいかを指定します。

モード説明
r読み取り専用。ファイルが存在しない場合は、I/Oエラーが発生します。
r+読み書き。ファイルが存在しない場合は、I/Oエラーが発生します。
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_READフラグが存在する場合は常にFILE_SHARE_READが、FILE_WRITEフラグが存在する場合は常にFILE_SHARE_WRITEが呼び出されることに注意してください。 

これは、他のプログラムで使用されているファイルへの読み書き処理を強化するためのものです。

これをさらに良くするために、オプション変数shared_IOを用意できます (trueに設定すると、他のプログラムで使用されているファイルに対してI/O操作を実行でき、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におけるPython風Openメソッド

ファイルのモードに応じて生成されるフラグを使用することで、手持ちのあらゆるファイルを開くことができるようになりました。

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.

This is a third line after a space.
['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というpublic関数内で継承することができます。

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_handleFileOpen()によって返された有効なCSVファイルハンドル。既に開かれているCSVファイルを参照します。リーダーは、このハンドルを直接操作し、ファイルの開閉を管理しません。Req
delimiter行内のフィールドを区切るために使用される文字列文字。一般的な値としては、「,」(カンマ)、「;」(セミコロン)、および「\t」(タブ)があります。「,」
quotechar区切り文字や特殊文字を含むフィールドを引用するために使用される文字。 
一致する引用符で囲まれた部分はすべて、リテラルデータとして扱われます。
「"」
escapechar引用符で囲まれたフィールド内の特殊文字をエスケープするために使用される文字。たとえば、「\"」を使用すると、引用符で囲まれた値の中に引用符文字を含めることができます。 「\\」
doublequote引用符で囲まれたフィールド内の引用符の処理方法を制御します。trueの場合。連続する2つの引用符("")は、1つのリテラル引用符として解釈され、これは標準的なCSVの動作と一致します。「」
skipinitialspace有効にすると、区切り文字の直後の空白文字は無視されます。これは、「A, B, C」ではなく「A,B,C」のような緩やかな形式のCSVファイルを解析する場合に便利です。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.

This is a third line after a space.
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フラグを指定してファイルを開いたことがなかったためです。ファイルモード引数でこれを処理する方法がまだ確立されていないのです。

テキストファイルの場合は文字「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ファイルは2次元のデータ格納方式を採用しているため、新しいデータを書き込む際には、他のファイルとは異なる方法で処理する必要があります。以前はこのようなファイルを読み込む際に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.

This is a third line after a space.

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:ファイルの読み取りと書き込みが可能かどうかを確認します

これらの2つの小さな機能は、ファイルから情報を読み取ったり、ファイル内に情報を書き込んだりする前に役立つかもしれません。

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でよく見られるほど複雑である必要はありません。本記事では、慎重な抽象化と明確な設計目標を用いることで、MetaTrader 5環境の制約を尊重しつつ、クリーンで信頼性の高い、Python風のファイル読み書き方法を構築できることを示す。

本記事では、テキストファイルやCSVファイルといった一般的なユースケースにおけるファイル入出力の基本を解説し、ファイルモード、エンコーディングに関する考慮事項、追記動作、安全な読み書きパターンについて考察し、MQL5のネイティブファイル関数の上に高レベルの構造をどのように重ね合わせることができるかを示しました。これらの詳細を再利用可能なモジュール内にカプセル化することで、定型コードを削減し、よくあるミスを最小限に抑え、ファイル操作の理解と保守を容易にします。

ご一読、誠にありがとうございました。


添付ファイルの表

ファイル名説明と使用法
Include\PyMQL5\fileIO\fileIO.mqhMetaTrader 5のすべてのファイルタイプを操作するためのCFileクラスとCFileIO クラスの両方が含まれています。
Include\PyMQL$\fileIO\csv.mqhこのライブラリには、CSVファイルの読み取りと書き込みをおこなうためのCSVReaderとCSVWriterという2つのクラスがあります。
Test file IO.mq5この記事で説明したすべてのメソッドを網羅した最終スクリプト(プレイグラウンド)。
Files\* コードのテストに必要なファイルがすべて含まれています。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20695

添付されたファイル |
Attachments.zip (22.35 KB)
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
データサイエンスとML(第47回):DeepARモデルによるPythonでの市場予測 データサイエンスとML(第47回):DeepARモデルによるPythonでの市場予測
DeepARと呼ばれる時系列予測のための優れたモデルを用いて、市場の予測を試みます。DeepARは、ARIMA(自己回帰和分移動平均)やVAR(ベクトル自己回帰)のようなモデルに見られる自己回帰的な性質とディープニューラルネットワークを組み合わせたモデルです。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
ラリー・ウィリアムズの『市場の秘密』(第2回):市場構造取引システムの自動化 ラリー・ウィリアムズの『市場の秘密』(第2回):市場構造取引システムの自動化
MQL5でラリー・ウィリアムズの市場構造の概念を自動化する方法を学びます。スイングポイントを読み取り、売買シグナルを生成し、リスクを管理し、動的なトレーリングストップ戦略を適用する完全なエキスパートアドバイザー(EA)を構築します。