English Deutsch
preview
MQL5で他の言語の実用的なモジュールを実装する(第1回):Pythonにヒントを得たSQLite3ライブラリの構築

MQL5で他の言語の実用的なモジュールを実装する(第1回):Pythonにヒントを得たSQLite3ライブラリの構築

MetaTrader 5トレーディングシステム |
143 0
Omega J Msigwa
Omega J Msigwa

内容


はじめに

これまでに、「お気に入りのモジュールやライブラリ、フレームワークなどを、他のプログラミング言語(特にMQL5)でも使えたらいいのに」と思ったことはありませんか。私自身、そのように感じたことが何度もあります。

MQL5コミュニティには、さまざまなプログラミング背景を持つ開発者が多く存在します。たとえば、私のようにWeb開発出身の方、Android開発出身の方、そしてその他にもさまざまな分野から来たプログラマーがいます。つまり、多くの開発者がJavaScript、Java、Python、C++、C#など、複数の言語に精通しているのです。

それぞれの言語には便利なツール(モジュールやライブラリ)があり、「どこでも使いたい」と思うような有用なものが少なくありません。たとえば、私はPythonのNumPyモジュールを数学的計算に使うのが大好きで、以前それに似たライブラリをMQL5上で実装したこともあります(その内容については別の記事で紹介しました)。

もちろん、ある言語のモジュールやフレームワークなどを別の言語(この場合はMQL5)に実装する試みは、言語仕様の違いにより、完全に同一の機能や結果を再現できるとは限りません。しかし、似たような構文や操作感を持たせることができれば、他言語経験者にとってMQL5上での開発がより簡単で、かつ楽しいものになるでしょう。さらに、その過程でプログラミング全般における知識を深めることも期待できます。

本記事から始まる新連載では、「他言語のあらゆるモジュールをMQL5へ移植する」わけではなく、MQL5上で実用的に活用できるモジュールに限定して実装をおこなっていきます。たとえば、数学計算、データ保存、データ分析などの分野で役立つモジュールが対象です。

連載第1回として、Pythonに標準搭載されているsqlite3モジュールをMQL5上で再現していきます。


Pythonで提供されているSqlite3モジュールとは

まずはSQLiteデータベースについて理解していきましょう。

SQLiteデータベースとは、軽量で自己完結型、かつサーバーレスのSQLデータベースエンジンです。シンプルでローカルなデータ保存が必要なアプリケーションで広く利用されています。SQLiteはファイルベースのデータベースであり、スキーマ、テーブル、インデックス、データといったすべての情報を単一の「.sqlite」または「.db」ファイルに格納します。

MySQLPostgreSQLのように、サーバーや設定、管理作業を必要とするデータベースとは異なり、SQLiteはそれらを一切必要としません。ディスクへの読み書きを直接おこなうため、すぐに利用を開始することができます。

MQL5プログラミング言語には、SQLiteデータベースを操作するための組み込み関数がすでに用意されています。これらの関数だけでも十分に動作しますが、Pythonのsqlite3モジュールと比べると、使い勝手はそれほど良くありません。

たとえば、Pythonでsqlite3モジュールを使えば、非常にシンプルな方法で「example」というデータベースを作成し、usersというテーブルに情報を挿入することができます。しかも、そのテーブルが存在しない場合には、自動的に作成されます

Python sqliteモジュールの使用

import sqlite3

conn = sqlite3.connect("example.db")

cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        age INTEGER,
        email TEXT UNIQUE
    )
""") # Execute a query

try:    
    cursor.execute("INSERT INTO users (name, age, email) VALUES (?, ?, ?)", 
                   ("Bruh", 30, "bruh@example.com"))

    conn.commit() # commit the transaction | save the new information to a database
    
except sqlite3.DatabaseError as e:
    print("Insert failed:", e)

conn.close() # closing the database

ネイティブMQL5関数の使用

void OnStart()
  {
//---
    
   int db_handle = DatabaseOpen("example.db", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if (db_handle == INVALID_HANDLE)
     {
       printf("Failed to open a database. Error = %s",ErrorDescription(GetLastError()));
       return;
     }
    
    string sql =        
       "   CREATE TABLE IF NOT EXISTS users ("
       "   id INTEGER PRIMARY KEY AUTOINCREMENT,"
       "   name TEXT NOT NULL,"
       "   age INTEGER,"
       "   email TEXT UNIQUE"
       ")";
      
    if (!DatabaseExecute(db_handle, sql)) //Execute a sql query
      {
         printf("Failed to execute a query to the database. Error = %s",ErrorDescription(GetLastError()));
         return;
      }  
    
    if (!DatabaseTransactionBegin(db_handle)) //Begin the transaction
      {
         printf("Failed to begin a transaction. Error = %s",ErrorDescription(GetLastError()));
         return;
      }   
    
    sql = "INSERT INTO users (name, age, email) VALUES ('Bruh', 30, 'bruh@example.com')";
    
    if (!DatabaseExecute(db_handle, sql)) //Execute a query
      {
         printf("Failed to execute a query to the database. Error = %s",ErrorDescription(GetLastError()));
         return;
      }
     
   if (!DatabaseTransactionCommit(db_handle)) //Commit the transaction | push the changes to a database
      {
         printf("Failed to commit a transaction. Error = %s",ErrorDescription(GetLastError()));
         return;
      } 
    
    DatabaseClose(db_handle); //Close the database
 }

私たちは皆、Pythonでsqlite3を使用することで、ネイティブのMQL5関数を使用する場合よりもコードが格段にすっきりしたという点に同意できると思います。ネイティブのMQL5関数では、エラー処理や返される情報、データベースへの追加処理などを自分で管理する必要があります。

一方、sqlite3モジュールは、コマンドの実行やトランザクション管理における多くの不要な手順を自動的に処理してくれるため、ユーザーがSQLiteデータベースからデータを取得・挿入する作業を、手間なく簡単におこなうことができます。

そこで、MQL5においても同様のモジュールを実装してみましょう。


SQLiteデータベースへの接続

Pythonのsqlite3におけるconnectメソッドは、指定したデータベース名が存在しない場合には新しいデータベースを作成し、ディスク上のデータベースへの接続を表す接続オブジェクトを返します。

import sqlite3
con = sqlite3.connect("example.db")

MQL5においては、この接続はデータベースハンドルに相当します。したがって、技術的には、MQL5ライブラリ内でハンドルを返す必要はありません。クラス内のすべての関数でこのハンドルを内部的に使用するためです。

class CSqlite3
  {
protected:

   int m_request;
   
public:
                     int m_db_handle;
 //... Other functions
 }

bool CSqlite3::connect(const string db_name, const bool common=false, const bool database_in_memory=false)
 {   
   int flags = DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE;

   if (common) //Open the database from the common folder
      flags |= DATABASE_OPEN_COMMON;

   if (database_in_memory) //Open the database in memory 
      flags |= DATABASE_OPEN_MEMORY;

   m_db_handle = DatabaseOpen(db_name, flags);
   
//---
   
   if (m_db_handle == INVALID_HANDLE)
     {
       printf("func=%s line=%d, Failed to open a database. Error = %s",__FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
       return false;
     }
      
   return true;
 }

MQL5では、データをデータパス(フォルダ)または共通データパス(フォルダ)のいずれかに保存するオプションが用意されています。common引数をtrueに設定すると、通常のデータパスではなく、共通データパスからデータベースが読み込まれます。

また、ユーザーに対して、データベースをメモリ(RAM)上で開くか、ディスク上で開くかを選択できるようにすることも可能です。変数database_in_memorytrueに設定されている場合、データベースはディスクではなくRAM上に作成され、逆の場合はディスク上に作成されます。

ここでは、データベースを読み取り専用モードで開くためのフラグ、すなわちDATABASE_OPEN_READONLYを無視しています。その理由は単純です。データベースに書き込みを行いたくない場合は、そもそも「INSERT」クエリを実行しなければよいからです。ご理解いただけますでしょうか。 


SQL文の実行

これは、SQLiteデータベースを扱う際に最も重要な関数のひとつです。この関数は、データベース内の情報を取得したり、データの挿入、更新、削除、変更をおこなったりできるようにします。

この関数は、データベース上でSQL文やコマンドを直接実行するためのものです。

Pythonのsqlite3モジュールにおけるexecute()関数は、非常にスムーズかつシームレスに動作します。この関数は、情報の取得や挿入など、あらゆるコマンドを受け取り、自動的に「いつ」「何を」返すべきか、あるいは返さないべきかを判断します。

一方、MQL5には、DatabaseExecuteという組み込み関数が用意されており、これはPythonのsqlite3におけるexecute()関数と似た働きをします。両者とも、指定されたデータベースまたはテーブルに対してリクエストを実行する機能を持っています。ただし、MQL5のDatabaseExecuteはすべてのリクエストの実行に対応していますが、「SELECT」キーワードを含むデータの読み取りリクエストに関しては適していません。 

データベースから情報を効果的に読み取るためには、DatabasePrepare関数を使用します。この関数はリクエストのハンドルを作成し、その後DatabaseReadを使用して実行することができます。

MQL5で、クエリの種類に依存せず柔軟に実行できる関数を作成するためには、SQLクエリの種類を判別し、それぞれのクエリに応じて正しい情報を返すように設計する必要があります。

execute_res_structure CSqlite3::execute(const string sql)
 {
   execute_res_structure res;   
   
   string trimmed_sql = sql;
   StringTrimLeft(trimmed_sql); //Trim the leading white space
   
   string first_word = trimmed_sql;
   
   // Find the index of the first space (to isolate the first SQL keyword)
   int space_index = StringFind(trimmed_sql, " ");
   if (space_index != -1)
      first_word = StringSubstr(trimmed_sql, 0, space_index); // Extract the first word from the query
   
   StringToUpper(first_word); //Convert the first word in the query to uppercase for comparison
   
   if (first_word == "SELECT")
    {
      // SELECT query – prepare and expect data 
      m_request = DatabasePrepare(m_db_handle, sql);
      res.request = m_request;
      
      return res;
    }
   else
    {
      // INSERT/UPDATE/DELETE – execute directly
       if (!m_transaction_active)
         {
            if (!this.begin())
               return res;
            else
                m_transaction_active_auto = true; //The transaction was started automatically by the execute function
         }
         
       ResetLastError();          
       if (!DatabaseExecute(m_db_handle, sql))
         {
            printf("func=%s line=%d, Failed to execute a query to the database. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
         }
    }
   
   return res;
 }

Pythonのexecute関数と同様に、すべての非SELECT文は、begin関数を使用して暗黙的にトランザクションを開始します。このトランザクションは、変更内容をデータベースに保存する前にコミットする必要があります。

execute関数を呼び出してデータベースに情報を挿入した後はcommitメソッドを呼び出して変更をデータベースに保存する必要があります。この関数については、後ほど本記事内で詳しく説明します

SELECT以外の文またはクエリでこの関数を呼び出した場合、データベース内の情報を更新・変更・挿入する処理をおこなうため、値を返しません。一方、SELECTタイプのクエリで呼び出された場合には、データ構造を返します。ここでは、そのデータ構造について詳しく見ていきましょう。

1. fetchoneメソッド

Pythonで提供されているsqlite3モジュールは、SQL文の結果として返される情報の一部または全体を動的に取得することが可能です。

まず最初に、データベースから1行分の情報を返す機能について説明します。

import sqlite3
conn = sqlite3.connect("example.db")

cursor = conn.cursor()
print(conn.execute("SELECT * FROM users").fetchone())

SQL文でSQLiteデータベース内のすべての情報を要求した場合でも、fetchone関数は、execute関数がデータベースから1行以上のデータを返すことを制限します。

以下がデータベースです。

以下が出力です。

(mcm-env) C:\Users\Omega Joctan\OneDrive\Desktop\MCM>C:/Anaconda/envs/mcm-env/python.exe "c:/Users/Omega Joctan/OneDrive/Desktop/MCM/sqlite_test.py"
(1, 'Alice', 30, 'alice@example.com')

MQL5のクラス内でも同様の関数が必要です。この関数は、execute関数によって返されるexecute_res_structureという構造体の内部で実装されます。

struct execute_res_structure
  {
   int request;
   CLabelEncoder le;
      
   vector fetchone()
      {
         int cols = DatabaseColumnsCount(request);         
         vector row = vector::Zeros(cols);
         
         while (DatabaseRead(request))  // Essential, read the entire database
          {
            string row_string = "(";  
            
            for (int i = 0; i < cols; i++)
             {
               int int_val; //Integer variable
               double dbl_val; //double      
               string str_val; //string variable
      
               ENUM_DATABASE_FIELD_TYPE col_type = DatabaseColumnType(request, i);
               string col_name;
      
               if (!DatabaseColumnName(request, i, col_name))
                {
                  printf("func=%s line=%d, Failed to read database column name. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                  continue;
                }
      
                 switch (col_type) //Detect a column datatype and assign the value read from every row of that column to the suitable variable
                  {
                     case DATABASE_FIELD_TYPE_INTEGER:
                        if (!DatabaseColumnInteger(request, i, int_val))
                           printf("func=%s line=%d, Failed to read Integer. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {
                             row_string += StringFormat("%d", int_val); 
                             row[i] = int_val;
                           }
                        break;
         
                     case DATABASE_FIELD_TYPE_FLOAT:
                        if (!DatabaseColumnDouble(request, i, dbl_val))
                           printf("func=%s line=%d, Failed to read Double. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {
                            row_string += StringFormat("%.5f", dbl_val);
                            row[i] = dbl_val;
                          }
                        break;
         
                     case DATABASE_FIELD_TYPE_TEXT:
                        if (!DatabaseColumnText(request, i, str_val))
                           printf("func=%s line=%d, Failed to read Text. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {                  
                             row_string += "'" + str_val + "'";    
                             row[i] = (double)str_val;
                          }
                        break;
         
                     default:
                             if (MQLInfoInteger(MQL_DEBUG))
                                PrintFormat("%s = <Unknown or Unsupported column Type by this Class>", col_name);
                        break;
                  }
               
               // Add comma if not last element
               if (i < cols - 1)
                  row_string += ", ";
             }
            
            row_string += ")";
            if (MQLInfoInteger(MQL_DEBUG))
               Print(row_string);  // Print the full row once 
               
            break;
          }
         
         DatabaseFinalize(request);
         return row;  // Replace with actual parsed return if needed
      }

 //... Other functions
}

MetaEditorの同様のデータベースから

以下は、データベーステーブルで利用可能なすべての情報を要求するクエリを送信し、返される情報の量を1行のデータに制限する方法を示しています。

#include <sqlite3.mqh>

CSqlite3 sqlite3;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
    
    sqlite3.connect("example.db"); 
    Print(sqlite3.execute("SELECT * FROM users").fetchone());
    
    sqlite3.close(); 
  }

以下が出力です。

RJ      0       13:25:04.402    sqlite3 test (XAUUSD,H1)        (1, 'Alice', 30, 'zero@example.com')
DQ      0       13:25:04.402    sqlite3 test (XAUUSD,H1)        [1,0,30,0]

素晴らしいことに、これでデータベースから1行分のデータをほとんど手間なく取得できるようになりました。しかし、現在の関数ではすべてのバイナリ値を無視しており、文字列の処理やエンコードも行われません。そのため、返されるベクター内では、すべての「TEXT」型または文字列型のデータがゼロとして割り当てられています。

また、Pythonの配列は異なるデータ型の値を格納できますが、MQL5のベクトルや配列ではそれができません。これについては後ほど、対応方法を見ていきます。

2. fetchallメソッド

fetchoneメソッドとは異なり、このメソッドはexecuteメソッドで指定されたSQL文に基づき、要求されたすべての情報を受け取ります。

struct execute_res_structure
  {
   int request;
  //... Other functions

   matrix fetchall()
      {
         int cols = DatabaseColumnsCount(request);         
         vector row = vector::Zeros(cols);
         
         int CHUNK_SIZE = 1000; //For optimized matrix handling
         matrix results_matrix = matrix::Zeros(CHUNK_SIZE, cols);  
         int rows_found = 0; //for counting the number of rows seen in the database
         
         while (DatabaseRead(request))  // Essential, read the entire database
          {
            string row_string = "("; //for printing purposes only. Similar to how Python prints
            
            for (int i = 0; i < cols; i++)
             {
               int int_val; //Integer variable
               double dbl_val; //double variable
               string str_val; //string variable
      
               ENUM_DATABASE_FIELD_TYPE col_type = DatabaseColumnType(request, i);
               string col_name;
      
               if (!DatabaseColumnName(request, i, col_name))
                {
                  printf("func=%s line=%d, Failed to read database column name. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                  continue;
                }
      
                 switch (col_type) //Detect a column datatype and assign the value read from every row of that column to the suitable variable
                  {
                     case DATABASE_FIELD_TYPE_INTEGER:
                        if (!DatabaseColumnInteger(request, i, int_val))
                           printf("func=%s line=%d, Failed to read Integer. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {
                             row_string += StringFormat("%d", int_val);  //For printing purposes only
                             row[i] = int_val;
                           }
                        break;
         
                     case DATABASE_FIELD_TYPE_FLOAT:
                        if (!DatabaseColumnDouble(request, i, dbl_val))
                           printf("func=%s line=%d, Failed to read Double. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {
                            row_string += StringFormat("%.5f",dbl_val);  //For printing purposes only
                            row[i] = dbl_val;
                          }
                        break;
         
                     case DATABASE_FIELD_TYPE_TEXT:
                        if (!DatabaseColumnText(request, i, str_val))
                           printf("func=%s line=%d, Failed to read Text. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {  
                             row_string += "'" + str_val + "'";                                
                             row[i] = (double)str_val;
                          }
                        break;
         
                     default:
                             if (MQLInfoInteger(MQL_DEBUG))
                                PrintFormat("%s = <Unknown or Unsupported column Type by this Class>", col_name);
                        break;
                  }
                  
               // Add comma if not last element
               if (i < cols - 1)
                  row_string += ", ";
             }
          
          //---
          
            row_string += ")";
            if (MQLInfoInteger(MQL_DEBUG))
               Print(row_string);  // Print the full row once
            
          //---
            
            rows_found++; //Increment the rows counter
            if (rows_found > (int)results_matrix.Rows()) 
               results_matrix.Resize(results_matrix.Rows()+CHUNK_SIZE, cols); //Resizing the array after 1000 rows
            
            results_matrix.Row(row, rows_found-1); //Insert a row into the matrix
          }
         
         results_matrix.Resize(rows_found, cols); //Resize the matrix according to the number of unknown rows found in the database | Final trim
         
         DatabaseFinalize(request); //Removes a request created in DatabasePrepare().
         return results_matrix;  // return the final matrix
      }

  //... Other lines of code
}

今回はベクトルではなく行列を返すようにして、二次元lテーブル全体を格納できるようにしました。

この関数の難しい部分は、結果として得られる行列を動的にリサイズする処理の取り扱いです。データベースのテーブルは非常に大きくなることがあり(10万行以上を含むこともあります)、行数という観点でデータベースやテーブルのサイズを事前に把握することは困難です。そのため、各反復ごとに結果の行列をリサイズすると、行をループするたびに関数が非常に遅くなってしまいます。これは、MQL5においてResize関数が最も計算コストの高い関数のひとつであるためです。

このため、上記の関数ではResizeメソッドを呼び出す回数を減らすために、1000回の反復ごとに行列をリサイズするようにしました。

以下が関数の使用法です。

#include <sqlite3.mqh>
CSqlite3 sqlite3;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
    
    sqlite3.connect("example.db");
    Print(sqlite3.execute("SELECT * FROM users").fetchall());
    
    sqlite3.close(); 
  }

MetaTrader 5の[エキスパート]タブには、以下が出力されます。

IF      0       13:30:33.649    sqlite3 test (XAUUSD,H1)        (1, 'Alice', 30, 'zero@example.com')
FR      0       13:30:33.649    sqlite3 test (XAUUSD,H1)        (2, 'Alice', 30, 'alice@example.com')
FD      0       13:30:33.649    sqlite3 test (XAUUSD,H1)        (3, 'Alice', 30, 'bro@example.com')
QQ      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (4, 'Alice', 30, 'ishowspeed@example.com')
MO      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (5, 'Alice', 30, 'damn@example.com')
MD      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (6, 'Alice', 30, 'wth@example.com')
QN      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (7, 'Bruh', 30, 'stubborn@example.com')
NO      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (8, 'Bruh', 30, 'whathehelly@example.com')
ED      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (9, 'Bruh', 30, 'huh@example.com')
PO      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (10, 'Bruh', 30, 'whatsgoingon@example.com')
FS      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (11, 'Bruh', 30, 'bruh@example.com')
FF      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        (12, 'Bruh', 30, 'how@example.com')
JO      0       13:30:33.650    sqlite3 test (XAUUSD,H1)        [[1,0,30,0]
RG      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [2,0,30,0]
QN      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [3,0,30,0]
LE      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [4,0,30,0]
KL      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [5,0,30,0]
NS      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [6,0,30,0]
MJ      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [7,0,30,0]
HQ      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [8,0,30,0]
GH      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [9,0,30,0]
OL      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [10,0,30,0]
RE      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [11,0,30,0]
JS      0       13:30:33.650    sqlite3 test (XAUUSD,H1)         [12,0,30,0]]

データベースに存在する全12行が、execute関数によって返されました。

なお、MQL5の配列は単一の配列内で異なる型の変数を保持できないため、現時点では、データベーステーブルに文字列が含まれる行についてはすべてゼロとして扱われます。

3. fetchmanyメソッド

fetchallメソッドと同様に、この関数はテーブルから取得した値を含む行列を返します。ただし、このメソッドでは返される行数を制御することが可能です。

struct execute_res_structure
  {
   int request;

//... other lines of code

   matrix fetchmany(uint size)
      {
         int cols = DatabaseColumnsCount(request);         
         vector row = vector::Zeros(cols);
         
         matrix results_matrix = matrix::Zeros(size, cols);  
         int rows_found = 0;
         
         while (DatabaseRead(request))  // Essential, read the entire database
          {
            string row_string = "("; 
            
            for (int i = 0; i < cols; i++)
             {
               int int_val; //Integer variable
               double dbl_val; //double variable
               string str_val; //string variable
      
               ENUM_DATABASE_FIELD_TYPE col_type = DatabaseColumnType(request, i);
               string col_name;
      
               if (!DatabaseColumnName(request, i, col_name))
                {
                  printf("func=%s line=%d, Failed to read database column name. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                  continue;
                }
      
                 switch (col_type) //Detect a column datatype and assign the value read from every row of that column to the suitable variable
                  {
                     case DATABASE_FIELD_TYPE_INTEGER:
                        if (!DatabaseColumnInteger(request, i, int_val))
                           printf("func=%s line=%d, Failed to read Integer. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {
                             row_string += StringFormat("%d", int_val);
                             row[i] = int_val;
                           }
                        break;
         
                     case DATABASE_FIELD_TYPE_FLOAT:
                        if (!DatabaseColumnDouble(request, i, dbl_val))
                           printf("func=%s line=%d, Failed to read Double. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {
                            row_string += StringFormat("%.5f", dbl_val);
                            row[i] = dbl_val;
                          }
                        break;
         
                     case DATABASE_FIELD_TYPE_TEXT:
                        if (!DatabaseColumnText(request, i, str_val))
                           printf("func=%s line=%d, Failed to read Text. Error = %s", __FUNCTION__, __LINE__, ErrorDescription(GetLastError()));
                        else
                          {
                             row_string += "'" + str_val + "'";   
                             row[i] = (double)str_val;
                          }
                        break;
         
                     default:
                             if (MQLInfoInteger(MQL_DEBUG))
                                PrintFormat("%s = <Unknown or Unsupported column Type by this Class>", col_name);
                        break;
                  }
             }
            
            results_matrix.Row(row, rows_found);
            rows_found++;
      
            if (rows_found >= (int)size)
               break;
      
            row_string += ")";
            if (MQLInfoInteger(MQL_DEBUG))
               Print(row_string);  // Print the full row once
          }
         
         results_matrix.Resize(rows_found, cols); //Resize the matrix according to the number of unknown rows found in the database | Final trim
         
         DatabaseFinalize(request); //Removes a request created in DatabasePrepare().
         return results_matrix;  // return the final matrix
      }

  //... other functions
}

以下が関数の使用法です。

#include <sqlite3.mqh>
CSqlite3 sqlite3;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
    
    sqlite3.connect("example.db");
    Print(sqlite3.execute("SELECT * FROM users").fetchmany(5));
    
    sqlite3.close(); 
  }

以下が出力です。

EG      0       13:45:25.480    sqlite3 test (XAUUSD,H1)        (1'Alice'30'zero@example.com')
IR      0       13:45:25.481    sqlite3 test (XAUUSD,H1)        (2'Alice'30'alice@example.com')
EJ      0       13:45:25.481    sqlite3 test (XAUUSD,H1)        (3'Alice'30'bro@example.com')
NS      0       13:45:25.481    sqlite3 test (XAUUSD,H1)        (4'Alice'30'ishowspeed@example.com')
CD      0       13:45:25.481    sqlite3 test (XAUUSD,H1)        [[1,0,30,0]
KR      0       13:45:25.481    sqlite3 test (XAUUSD,H1)         [2,0,30,0]
LI      0       13:45:25.481    sqlite3 test (XAUUSD,H1)         [3,0,30,0]
QP      0       13:45:25.481    sqlite3 test (XAUUSD,H1)         [4,0,30,0]
EJ      0       13:45:25.481    sqlite3 test (XAUUSD,H1)         [5,0,30,0]]

これらの3つの関数はいずれも、行列に格納するデータ量を制御することが可能ですが、最終的にはSQL文に依存しています。どの情報を返すかを決定しているのは、あくまでSQL文そのものです。ここまでで説明した3つの関数は、SQL文で要求されたデータを受け取るための単なるゲートウェイに過ぎません。

4. 追加メソッド:execute関数のステータスの確認

デフォルトでは、この関数は「SELECT」タイプのSQL文実行後に取得したデータを返す際に便利な関数群が格納された構造体を返します。しかし、非SELECTタイプのSQL文でexecuteメソッドを呼び出すことも多くあります。この場合、この関数は成功したかどうかを確認するために使用できるデータを返さないようになっています。 

この状態を確認するためには、execute関数が返す構造体内のbooleanという変数を使用する必要があります。 

{
    sqlite3.connect("indicators.db");
    
    sqlite3.execute(
       "   CREATE TABLE IF NOT EXISTS EURUSD ("
       "   id INTEGER PRIMARY KEY AUTOINCREMENT,"
       "   example_indicator FLOAT,"
       ")"
    ); 
    
    if (!sqlite3.execute(StringFormat("INSERT INTO USDJPY (example_indicator) VALUES (%.5f)",(double)rand())).boolean) //A SQL query with a purposefully placed error
      printf("The execute function failed!");
 }

ブール変数はbool型の変数であり、関数が成功した場合はtrueになり、失敗した場合はfalseになります。

以下が出力です。

QK      2       18:27:00.575    sqlite3 test (XAUUSD,H1)        database error, near ")": syntax error
DN      0       18:27:00.576    sqlite3 test (XAUUSD,H1)        func=CSqlite3::execute line=402, Failed to execute a query to the database. Error = Generic database error
GE      2       18:27:00.578    sqlite3 test (XAUUSD,H1)        database error, no such table: USDJPY
PS      0       18:27:00.578    sqlite3 test (XAUUSD,H1)        func=CSqlite3::execute line=402, Failed to execute a query to the database. Error = Generic database error
IS      0       18:27:00.578    sqlite3 test (XAUUSD,H1)        The execute function failed!

「SELECT」タイプのクエリでデータを返す場合に、関数が正常に実行されたかどうかを確認するには、返された行列またはベクトルのサイズを評価する必要があります。

もし行列の行数が0 (rows == 0)またはベクトルのサイズが0 (size == 0)であった場合、これはexecute関数の実行は失敗したことを意味します。


データベースからのテキスト(文字列)値の操作

前の章で説明した通り、簡易化されたexecute関数では、行列やベクトル内で文字列を返すことはできません。しかし、文字列や文字列値は他の変数と同じように有用です。そのため、データベースから取得した全ての値を行列に格納した後、文字列を含む列については別途抽出して処理する必要があります。 

#include <sqlite3.mqh>
CSqlite3 sqlite3;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
    
    sqlite3.connect("example.db");
    
    Print("database matrix:\n",sqlite3.execute("SELECT * FROM users").fetchall());
    
    string name_col[];
    sqlite3.execute("SELECT name FROM users").fetch_column("name", name_col);
    
    ArrayPrint(name_col);
}

この文字列値の配列をもとに、それらを行列が受け入れ可能な変数(double、floatなど)にエンコードする方法を考え、再度行列に組み込むことができます。

以下が出力です。

LS      0       12:48:12.456    sqlite3 test (XAUUSD,H1)        (1, 'Alice', 30, 'zero@example.com')
KG      0       12:48:12.456    sqlite3 test (XAUUSD,H1)        (2, 'Alice', 30, 'alice@example.com')
KH      0       12:48:12.456    sqlite3 test (XAUUSD,H1)        (3, 'Alice', 30, 'bro@example.com')
OM      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (4, 'Alice', 30, 'ishowspeed@example.com')
GH      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (5, 'Alice', 30, 'damn@example.com')
KH      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (6, 'Alice', 30, 'wth@example.com')
OR      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (7, 'Bruh', 30, 'stubborn@example.com')
LJ      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (8, 'Bruh', 30, 'whathehelly@example.com')
OG      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (9, 'Bruh', 30, 'huh@example.com')
RS      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (10, 'Bruh', 30, 'whatsgoingon@example.com')
PF      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (11, 'Bruh', 30, 'bruh@example.com')
DS      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (12, 'Bruh', 30, 'how@example.com')
NL      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (13, 'John', 83, 'johndoe@example.com')
IE      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (14, 'John', 83, 'johndoe2@example.com')
KP      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (15, 'John', 83, 'johndoe3@example.com')
IN      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        (16, 'John', 83, 'johndoe4@example.com')
KD      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        database matrix:
HP      0       12:48:12.457    sqlite3 test (XAUUSD,H1)        [[1,0,30,0]
PF      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [2,0,30,0]
OM      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [3,0,30,0]
ND      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [4,0,30,0]
MK      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [5,0,30,0]
LR      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [6,0,30,0]
KI      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [7,0,30,0]
JP      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [8,0,30,0]
IG      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [9,0,30,0]
QM      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [10,0,30,0]
LF      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [11,0,30,0]
KO      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [12,0,30,0]
RP      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [13,0,83,0]
MI      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [14,0,83,0]
PR      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [15,0,83,0]
DF      0       12:48:12.457    sqlite3 test (XAUUSD,H1)         [16,0,83,0]]
LQ      0       12:48:12.458    sqlite3 test (XAUUSD,H1)        "Alice" "Alice" "Alice" "Alice" "Alice" "Alice" "Bruh"  "Bruh"  "Bruh"  "Bruh"  "Bruh"  "Bruh"  "John"  "John"  "John"  "John" 


データベーストランザクション制御

Pythonのsqlite3モジュールは、データベーストランザクションがいつ、どのように開始・終了されるかを制御する複数のメソッドを提供しています。これと同様の形式を自作クラスに取り入れることで、SQLitデータベースを扱う際のトランザクション管理が容易になります。

関数説明備考
bool CSqlite3::commit(void)
現在のトランザクションをコミットし、データベースへの変更を確定します。
autocommitfalseの場合、INSERTUPDATEDELETEタイプのSQLクエリ実行後に呼び出す必要があります。
bool CSqlite3::rollback(void) 
現在のトランザクションをロールバックし、未コミットの変更をすべて取り消します。エラー処理に役立ちます。
bool CSqlite3::begin(void)
後でデータベースにコミットされるトランザクションを開始します。 データベースに変更を加える前に明示的な構文として使用します。autocommitがtrueの場合、execute()関数が自動的に呼び出されます。
bool CSqlite3::in_transaction()
トランザクションがアクティブかどうかを確認するブール関数トランザクションがアクティブな場合にtrueを返します。
CSqlite3(bool autocommit=false)

任意ですが推奨はされません。クラスコンストラクタ内でautocommitの値を変更することで、execute関数内でトランザクションを自動コミットするかどうかを設定できます。

begin、rollback、commitの3つの関数は、MQL5の組み込み関数をベースにトランザクション処理の上に構築されています。

bool CSqlite3::commit(void)
 {
   if (!DatabaseTransactionCommit(m_db_handle))
      {
         printf("func=%s line=%d, Failed to commit a transaction. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
         return false;
      }
   
   m_transaction_active = false; //Reset the transaction after commit    
   m_transaction_active_auto = false;
   
   return true;
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CSqlite3::begin(void) 
  { 
   if (m_transaction_active)
     {
       if (!m_transaction_active_auto) //print only if the user started the transaction not when it was started automatically by the execute() function
         printf("Can not begin, already in a transaction. Call the function rollback() to disregard it, or commit() to save the changes");
       return false;
     }
     
//---

   if (!DatabaseTransactionBegin(m_db_handle))
      {
         printf("func=%s line=%d, Failed to begin a transaction. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
         return false;
      }
   
   m_transaction_active = true;   
   m_transaction_active_auto = false;
   
   return m_transaction_active;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CSqlite3::rollback(void) 
 { 
   if (!DatabaseTransactionRollback(m_db_handle))
      {
         printf("func=%s line=%d, Failed to rollback a transaction. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
         return false;
      }
      
   m_transaction_active = false; //Reset the transaction after rollback 
   m_transaction_active_auto = false;
   
   return true;
 }


sqlite3モジュールのその他のメソッド

このモジュールには使用頻度は低いものの、さまざまなタスクで便利に使える関数がいくつかあります。

1. executemanyメソッド

この関数を使うと、複数の行を一度にデータベーステーブルに挿入することができます。

例えば、複数行を格納した行列があり、各列に特定の種類のインジケーター値が含まれていて、それらをまとめてデータベースに挿入したい場合、この関数はまさにその用途に適しています。

#include <sqlite3.mqh>
CSqlite3 sqlite3;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
    
    sqlite3.connect("indicators.db");
    
    sqlite3.execute(
       "   CREATE TABLE IF NOT EXISTS EURUSD ("
       "   id INTEGER PRIMARY KEY AUTOINCREMENT,"
       "   INDICATOR01 FLOAT,"
       "   INDICATOR02 FLOAT,"
       "   INDICATOR03 FLOAT"
       ")"
    ); 
    
    matrix data = {{101, 25, 001},
                   {102, 32, 002},
                   {103, 29, 003}};
    
    sqlite3.executemany("INSERT INTO EURUSD (INDICATOR01, INDICATOR02, INDICATOR03) VALUES (?,?,?)", data);
    sqlite3.commit();
    sqlite3.close();
  }

以下が出力です。

executemany関数を使用する際、VALUESキーワードの後にある疑問符の数は、params行列の列数と同じでなければなりません。そうしないと、エラーが発生します。

また、行列は単一の型(同種の変数)しか格納できないという制限があるため、この関数を使用して異なるデータ型を同時にデータベースに追加することはできません。たとえば、文字列とdouble型の列を同時に挿入することはできません。

2. executescriptメソッド

この関数は、複数のSQL文をまとめて実行したい場合に便利です。たとえば、テーブルの作成や複数行の挿入などを1文で実行できます。以下は、この関数の主な特徴です。

void OnStart()
  {
//---
     sqlite3.connect("indicators.db");
    
     // Use executescript to log actions
     sqlite3.executescript(
              "CREATE TABLE IF NOT EXISTS logs ("
              "id INTEGER PRIMARY KEY AUTOINCREMENT,"
              "event TEXT NOT NULL"
          ");"
      
          "INSERT INTO logs (event) VALUES ('Users batch inserted');"
      );
    
     sqlite3.close();
  }

  • セミコロン(;)で区切られた複数のSQL文を1つの文字列として受け取ります。
  • execute()やexecutemany()のようにパラメータ化や解析はおこなわず、すべての文を一度に実行します。
  • 実行結果は返さず、コマンドのバッチを単に実行するだけです。

以下が出力です。

executescript関数は開いているトランザクションを自動的にコミットするため、特別な制御が必要な場合を除き、通常は明示的にcommit関数を呼び出す必要はありません。

3. 追加メソッド:print_tableメソッド

Pythonのsqlite3モジュールで提供されるcursor.descriptionのように、SQLクエリやデータベース、テーブルの構造を取得する機能を再現することができます。

MQL5では、組み込み関数DatabasePrintを使用することで、同様の結果を得ることが可能です。

void CSqlite3::print_table(const string table_name_or_sql, const int flags=0) // Prints a table or an SQL request execution result in the Experts journal.
  {
    if (DatabasePrint(m_db_handle, table_name_or_sql, flags)<0)
       printf("func=%s line=%d, Failed to print the table or query result. Error = %s",__FUNCTION__,__LINE__, ErrorDescription(GetLastError()));
  }

以下が使用法です。

sqlite3.print_table("SELECT * FROM users");

以下が出力です。

CM      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         #| id name  age email                   
PJ      0       13:17:19.028    sqlite3 test (XAUUSD,H1)        --+--------------------------------------
MH      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         1|  1 Alice  30 zero@example.com         
KS      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         2|  2 Alice  30 alice@example.com        
NO      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         3|  3 Alice  30 bro@example.com          
NI      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         4|  4 Alice  30 ishowspeed@example.com   
IL      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         5|  5 Alice  30 damn@example.com         
LO      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         6|  6 Alice  30 wth@example.com          
EE      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         7|  7 Bruh   30 stubborn@example.com     
EM      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         8|  8 Bruh   30 whathehelly@example.com  
ER      0       13:17:19.028    sqlite3 test (XAUUSD,H1)         9|  9 Bruh   30 huh@example.com          
HK      0       13:17:19.028    sqlite3 test (XAUUSD,H1)        10| 10 Bruh   30 whatsgoingon@example.com 
IQ      0       13:17:19.028    sqlite3 test (XAUUSD,H1)        11| 11 Bruh   30 bruh@example.com         
LQ      0       13:17:19.028    sqlite3 test (XAUUSD,H1)        12| 12 Bruh   30 how@example.com          
GG      0       13:17:19.028    sqlite3 test (XAUUSD,H1)        13| 13 John   83 johndoe@example.com      
GK      0       13:17:19.028    sqlite3 test (XAUUSD,H1)        14| 14 John   83 johndoe2@example.com     
NQ      0       13:17:19.028    sqlite3 test (XAUUSD,H1)        15| 15 John   83 johndoe3@example.com     
QF      0       13:17:19.028    sqlite3 test (XAUUSD,H1)        16| 16 John   83 johndoe4@example.com     


取引をデータベースに記録する

これまで、SQL文を実行してデータベースに値を追加する簡単な例などを見てきました。ここで、ようやく実用的な内容に進みましょう。MetaTrader 5の取引履歴にあるすべての取引をSQLiteデータベースに挿入する方法を解説します。

この例はこちらの記事から引用したものです。

sqlite3を使用しない場合

//--- auxiliary variables
   ulong    deal_ticket;         // deal ticket
   long     order_ticket;        // a ticket of an order a deal was executed by
   long     position_ticket;     // ID of a position a deal belongs to
   datetime time;                // deal execution time
   long     type ;               // deal type
   long     entry ;              // deal direction
   string   symbol;              // a symbol a deal was executed for
   double   volume;              // operation volume
   double   price;               // price
   double   profit;              // financial result
   double   swap;                // swap
   double   commission;          // commission
   long     magic;               // Magic number (Expert Advisor ID)
   long     reason;              // deal execution reason or source
//--- go through all deals and add them to the database
   bool failed=false;
   int deals=HistoryDealsTotal();
// --- lock the database before executing transactions
   DatabaseTransactionBegin(database);
   for(int i=0; i<deals; i++)
     {
      deal_ticket=    HistoryDealGetTicket(i);
      order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
      position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
      time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
      type=           HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
      entry=          HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
      symbol=         HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
      volume=         HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
      price=          HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
      profit=         HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
      swap=           HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
      commission=     HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
      magic=          HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
      reason=         HistoryDealGetInteger(deal_ticket, DEAL_REASON);
      //--- add each deal to the table using the following query
      string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                       "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d)",
                                       deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
      if(!DatabaseExecute(database, request_text))
        {
         PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError());
         PrintFormat("i=%d: deal #%d  %s", i, deal_ticket, symbol);
         failed=true;
         break;
        }
     }
//--- check for transaction execution errors
   if(failed)
     {
      //--- roll back all transactions and unlock the database
      DatabaseTransactionRollback(database);
      PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError());
      return(false);
     }
//--- all transactions have been performed successfully - record changes and unlock the database
   DatabaseTransactionCommit(database);

sqlite3を使用した場合

#include <sqlite3.mqh>
CSqlite3 sqlite3;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
    sqlite3.connect("Trades_database.db");
    
//--- auxiliary variables

    ulong    deal_ticket;         // deal ticket
    long     order_ticket;        // a ticket of an order a deal was executed by
    long     position_ticket;     // ID of a position a deal belongs to
    datetime time;                // deal execution time
    long     type ;               // deal type
    long     entry ;              // deal direction
    string   symbol;              // a symbol a deal was executed for
    double   volume;              // operation volume
    double   price;               // price
    double   profit;              // financial result
    double   swap;                // swap
    double   commission;          // commission
    long     magic;               // Magic number (Expert Advisor ID)
    long     reason;              // deal execution reason or source
    
//--- go through all deals and add them to the database
   
   HistorySelect(0, TimeCurrent());
   int deals=HistoryDealsTotal();
   
   sqlite3.execute("CREATE TABLE IF NOT EXISTS DEALS ("
                     "ID          INT KEY NOT NULL,"
                     "ORDER_ID    INT     NOT NULL,"
                     "POSITION_ID INT     NOT NULL,"
                     "TIME        INT     NOT NULL,"
                     "TYPE        INT     NOT NULL,"
                     "ENTRY       INT     NOT NULL,"
                     "SYMBOL      CHAR(10),"
                     "VOLUME      REAL,"
                     "PRICE       REAL,"
                     "PROFIT      REAL,"
                     "SWAP        REAL,"
                     "COMMISSION  REAL,"
                     "MAGIC       INT,"
                     "REASON      INT );"
   ); //Creates a table if it doesn't exist
   
   sqlite3.begin(); //Start the transaction
   
// --- lock the database before executing transactions

   for(int i=0; i<deals; i++) //loop through all deals
     {
      deal_ticket=    HistoryDealGetTicket(i);
      order_ticket=   HistoryDealGetInteger(deal_ticket, DEAL_ORDER);
      position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID);
      time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
      type=           HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
      entry=          HistoryDealGetInteger(deal_ticket, DEAL_ENTRY);
      symbol=         HistoryDealGetString(deal_ticket, DEAL_SYMBOL);
      volume=         HistoryDealGetDouble(deal_ticket, DEAL_VOLUME);
      price=          HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
      profit=         HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
      swap=           HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
      commission=     HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
      magic=          HistoryDealGetInteger(deal_ticket, DEAL_MAGIC);
      reason=         HistoryDealGetInteger(deal_ticket, DEAL_REASON);
      
      //--- add each deal to the table using the following query
      string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)"
                                       "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d)",
                                       deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason);
      
      sqlite3.execute(request_text);
     }
    
    sqlite3.commit(); //Commit all deals to the database at once
    sqlite3.close(); //close the database
  }

以下が結果です。


最後に

Pythonのsqlite3モジュールをMQL5で再現することは、やりがいのある挑戦でした。これにより、Pythonと比較した場合のMQL5の柔軟性と制限の両方が浮き彫りになりました。MQL5はSQLiteデータベース用のオーソライザーやコンテキストマネージャなどの高度な機能を標準でサポートしていませんが、適切な抽象化とメソッド設計を行うことで、非常に近い開発体験を実現することができます。

このカスタムCSqlite3クラスにより、MQL5開発者はSQLiteデータベースと構造化されたPython的な方法で対話できるようになりました。クエリ実行、トランザクション管理、commit / rollback制御、fetchone()、fetchmany()、fetchall()などの取得操作もサポートされています。

Pythonから移行してくる方にとっても、このモジュールは馴染みやすく、使いやすいものになっているはずです。

お疲れ様でした。


添付ファイルの表

ファイル名説明と使用法
Include\errordescription.mqhMQL5およびMetaTrader 5で発生するすべてのエラーコードの説明を含むファイル
Include\sqlite3.mqhSQLiteデータベースをPython風に扱うためのCSqlite3クラスを含むファイル
Scripts\sqlite3 test.mq5 CSqlite3クラスのテスト用スクリプト 

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

添付されたファイル |
Attachments.zip (12.89 KB)
共和分株式による統計的裁定取引(第1回):エングル=グレンジャーおよびジョハンセンの共和分検定 共和分株式による統計的裁定取引(第1回):エングル=グレンジャーおよびジョハンセンの共和分検定
本記事は、トレーダー向けに、最も一般的な共和分検定を優しく紹介し、その結果の理解方法を簡単に解説することを目的としています。エングル=グレンジャーおよびジョハンセンの共和分検定は、長期的なダイナミクスを共有する統計的に有意な資産のペアやグループを特定するのに有効です。特にジョハンセン検定は、3つ以上の資産を含むポートフォリオに対して有用で、複数の共和分ベクトルの強さを一度に評価できます。
プライスアクション分析ツールキットの開発(第30回):コモディティチャンネル指数(CCI)、Zero Line EA プライスアクション分析ツールキットの開発(第30回):コモディティチャンネル指数(CCI)、Zero Line EA
プライスアクション分析の自動化は、今後の方向性を示す重要なステップです。本記事では、デュアルCCIインジケーター、ゼロラインクロスオーバー戦略、EMA、そしてプライスアクションを組み合わせ、ATRを用いて売買シグナルを生成し、ストップロス(SL)およびテイクプロフィット(TP)を設定するツールを開発します。CCI Zero Line EAの開発手法について学ぶために、ぜひお読みください。
MQL5での取引戦略の自動化(第22回):Envelopes Trend取引のためのZone Recoveryシステムの作成 MQL5での取引戦略の自動化(第22回):Envelopes Trend取引のためのZone Recoveryシステムの作成
本記事では、Envelopes Trend取引戦略と統合されたZone Recoveryシステムを開発します。RSI (Relative Strength Index)とEnvelopesインジケーターを用いて取引を自動化し、損失を抑えるリカバリーゾーンを効果的に管理するためのアーキテクチャを詳述します。実装とバックテストを通じて、変動する市場環境に対応できる効果的な自動取引システムの構築方法を示します。
知っておくべきMQL5ウィザードのテクニック(第72回):教師あり学習でMACDとOBVのパターンを活用する 知っておくべきMQL5ウィザードのテクニック(第72回):教師あり学習でMACDとOBVのパターンを活用する
前回の記事で紹介したMACDとOBVのインジケーターペアをフォローアップし、今回はこのペアを機械学習でどのように強化できるかを見ていきます。MACDとOBVは、それぞれトレンド系と出来高系という補完的なペアです。私たちの機械学習アプローチでは、畳み込みニューラルネットワーク(CNN)を使い、カーネルとチャンネルのサイズを調整する際に指数カーネルを利用して、このインジケーターペアの予測をファインチューニングします。今回もこれまでと同様に、MQL5ウィザードでエキスパートアドバイザー(EA)を組み立てられるようにしたカスタムシグナルクラスファイル内で実装しています。