English Русский Español Deutsch Português
preview
市場シミュレーション:MQL5におけるSQL入門(IV)

市場シミュレーション:MQL5におけるSQL入門(IV)

MetaTrader 5テスター |
14 0
Daniel Jose
Daniel Jose

はじめに

皆さん、こんにちは。これはリプレイ/シミュレーションシステムの作成方法に関する連載の続きです。

前回の「市場シミュレーション:MQL5におけるSQL入門(III)」では、MQL5で作成した実行ファイル内にSQLスクリプトを組み込む方法について説明しました。しかし、そこで終わりではありません。その後もクラスコードにいくつかの変更を加え、より簡単に使用できるように改善し、さらにコードの実装方法も少し調整しました。

前回の記事で紹介したクラスコードは、MetaTrader 5に組み込まれているSQLiteを使用してデータベースを作成する際に非常に役立ちます(この点については後ほど詳しく説明します)。しかし、このコードではMQL5からクエリを実行することができません。

そのため、この記事の主な目的は、このクラスをどのように変更すれば、クエリを実行するために必要なコードを追加できるのかを、できる限り明確に説明することです。つまり、ここではSQLiteデータベースに対してクエリを実行し、その結果を取得するための仕組みを実装します。

その方法を確認する前に、本連載で紹介している仕組みを使用する際に非常に重要な点を理解しておく必要があります。まさにこのような細かな違いがあるため、現時点では、リプレイ/シミュレーションシステムのコードにSQLiteをまだ組み込んでいません。まずはMetaTrader 5に組み込まれているSQLiteのサポートが実際にどのように動作するのかを、できる限り明確に説明し、示したいからです。

多くの人は、MetaTrader 5に含まれているSQLiteが、DLLファイルを通じてアクセスするSQLiteのバージョンと同じように動作すると考えるかもしれません。基本的な仕組みとしてはどちらも同じように動作しますが、MetaTrader 5内蔵版SQLiteを使用する場合には、いくつかの特有の注意点があります。将来的に開発者がこの仕様を変更する可能性はありますが、何も当然のこととして扱うべきではありません。もし疑問がある場合は、まず実際にテストをおこない、その後、特定のアプリケーションに組み込まれたSQLiteを使用する際に発生する可能性のある問題の解決方法を検討してください。ここで言いたいのは、特定のアプリケーションに問題があるということではありません。単に、使用しているSQLiteのバージョンが自分の想定していたものと異なる可能性があるかどうかを判断する前に、まずすべてを確認するべきだということです。


問題を理解する

この記事の執筆時点では、この問題は2023年6月22日時点のMetaTrader 5バージョン5.00ビルド3815で確認されています。はい、このコードはかなり前に作成されたものです。あなたがこの記事を読む頃には、MetaTrader 5はすでに更新されており、ここで説明している内容の一部は古くなっている可能性があります。それでも、この記事の内容を無視しないでください。なぜなら、ここで紹介する内容は、これまで考えたこともなかったような可能性や技術を学ぶきっかけになるかもしれないからです。

それでは、問題の説明に進みましょう。以下のSQLスクリプトをご覧ください。

01. PRAGMA FOREIGN_KEYS = ON;
02. 
03. DROP TABLE IF EXISTS tb_Symbols;
04. DROP TABLE IF EXISTS tb_Quotes;
05. 
06. CREATE TABLE IF NOT EXISTS tb_Symbols
07. (
08.     id PRIMARY KEY,
09.     symbol NOT NULL UNIQUE
10. );
11. 
12. CREATE TABLE IF NOT EXISTS tb_Quotes
13. (
14.     of_day NOT NULL,
15.     price NOT NULL,
16.     fk_id NOT NULL,
17.     FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id)
18. );

SQLiteのコード

このSQLスクリプトは、一連のテーブルを作成します。その中で、tb_Symbolsテーブルとtb_Quotesテーブルは関連付けられています。この関係は、17行目で定義されている外部キーによって保証されています。この内容についてよく分からない場合は、本連載の前回の記事をご覧ください。

次に、同じSQLスクリプトの3行目と4行目に注目してください。ここでは、SQLデータベースファイルからテーブルを削除しています。このスクリプトをMetaTrader 5で実行しても、SQLiteのDLLライブラリを利用して実行しても、最終的な結果は同じになります。しかし、実際には常に同じように動作するわけではありません。その理由は、17行目で定義されているtb_Symbolsとtb_Quotesのリレーションシップにあります。

「ちょっと待ってください。同じ結果にならない場合があるとはどういう意味でしょうか。MetaTrader 5に組み込まれているSQLiteは、DLLライブラリ経由で利用するSQLiteと同じように動作するのではありませんか。それなのに、なぜこのスクリプトを実行すると結果が異なると言うのでしょうか」少し説明が不十分だったかもしれません。あるいは、細かな違いを十分に説明できていなかったのかもしれません。実際のところ、最終的な処理結果そのものは同じです。しかし、tb_Symbolsとtb_Quotesの間にリレーションシップを形成しているデータが存在する場合、MetaTrader 5に組み込まれているSQLiteではテーブルを削除できず、エラーが発生します。このことは、以下のテストで確認できます。

次のスクリプトを続けて2回実行してください。1回目の実行は問題なく終了します。しかし、2回目を実行すると、コードとともに示した画像のようなエラーが表示されます。

01. PRAGMA FOREIGN_KEYS = ON;
02. 
03. DROP TABLE IF EXISTS tb_Symbols;
04. DROP TABLE IF EXISTS tb_Quotes;
05. 
06. CREATE TABLE IF NOT EXISTS tb_Symbols
07. (
08.     id PRIMARY KEY,
09.     symbol NOT NULL UNIQUE
10. );
11. 
12. CREATE TABLE IF NOT EXISTS tb_Quotes
13. (
14.     of_day NOT NULL,
15.     price NOT NULL,
16.     fk_id NOT NULL,
17.     FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id)
18. );
19. 
20. INSERT INTO tb_Symbols (id, symbol) VALUES (1, 'PETR4');
21. 
22. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('2023-07-10', '22.00', 1);

SQLiteのコード

実行間隔が3秒未満の場合、結果がまったく異なっていることが分かります。最初の実行ではSQLスクリプトは正常に実行されましたが、その直後に同じスクリプトを再度実行すると、エラーが発生して処理が終了しました。報告されたエラーコードからも分かるように、問題はテーブル間のリレーションシップに関連しています。そして、このリレーションシップはスクリプトの17行目で定義されています。

ただし、エラーそのものが17行目で発生したわけではありません。上の画像を見ると分かるように、SQLスクリプトを実行するために作成したクラスでは、SQLiteへ3行目のSQL文を送信した時点で問題が発生したことが報告されています。

ここで、1つ強調しておきたい点があります。同じSQLスクリプトをスタンドアロン版SQLiteで実行した場合には、エラーは発生しません。これはSQLプログラミングについて説明した際にも実演しました。もし疑問がある場合は、その様子を示した前回までの記事をご確認ください。

このことから、「問題はMetaTrader 5に組み込まれているSQLiteにあるのではないか」と考える方もいるかもしれません。しかし、私はこの点で議論をあおるつもりはありません。おそらく、MetaTrader 5に組み込まれているSQLiteには、有効な外部キーを持つテーブルの削除を防止する保護機能が実装されているのでしょう。スタンドアロン版SQLiteとMetaTrader 5内蔵版SQLiteとの間で、このような動作の違いが生じる正確な理由は分かりません。また、この違いはこれだけではありません。例えば、内蔵SQLiteライブラリを使用してトリガーを作成しようとすると、動作の違いがあることにも気付くでしょう。

そのため、SQLiteをDLL経由で利用するという選択をされても、それは十分に理解できます。とはいえ、私のテスト結果を見る限りでは、MetaTrader 5に組み込まれているSQLiteだけでも、多くの処理をDLLを使用せずに実現できます。動作上の違いが無視できないほど重大になった場合は、そのときに別の方法を考えればよいでしょう。現時点では、内蔵SQLiteを使って作業を進めることができます。

したがって、MetaTrader 5の内蔵SQLiteを利用していてデータベーステーブルを削除する必要がある場合は、データベースファイル自体を削除するのが最も簡単な方法です。その後、実行ファイルに組み込まれたスクリプトを再度実行すれば、競合は発生しません。

それでは、ここまでの内容が理解できたところで、次のステップへ進みましょう。ここからが最も興味深い部分です。これ以降の説明は、できるだけ集中して読んでいただくことをお勧めします。

市場シミュレーション(第25回):MQL5におけるSQL入門(I)」では、MQL5でSQLを扱うためには、利用可能な関数のうちわずか6個だけで十分であることを説明しました。前回までの記事では、そのうち3つの関数をすでに解説し、C_DB_SQLクラスにも実装しました。ここからは、残る3つの関数だけで十分である理由を学び、その仕組みを理解していきます。説明を分かりやすくするため、この内容は新しいトピックとして進めます。


残る3つの重要な関数

これから説明する内容について、たとえこれまで理解していなかったとしても、自分には能力が足りないなどと思う必要はまったくありません。すでにMQL5でデータベースを利用している方も、これから始める方も同じです。ここで紹介するのは、あくまで私自身のやり方です。それが唯一正しい方法でも、最適な方法でもありません。ただ、私が普段採用しているアプローチを紹介するだけです。

「MQL5では6つの関数だけでSQLを扱える」と言っているからといって、それ以上の関数や手続きを利用している方が間違っているという意味ではありません。むしろ、言語やツールを深く学び、その機能を十分に理解している方々には敬意を抱いています。私自身は、必要最小限の機能だけを利用するという考え方を採っています。

普段から、使用するすべての言語やツールの全関数を細部まで覚えようとはしていません。そのためには非常に多くの時間が必要になりますし、実際の開発では納期が限られていることがほとんどです。私は、作業の難易度に関係なく、期限内に目的を達成することを優先しています。

この考え方が適切であるかを確認するために、再びデータベース用スクリプトを変更してみます。今回は、硬直的なやり方を避けるために、新しいスクリプトを作成し、「script 02.sql」という名前を付けることをお勧めします。もちろん、script 01.sqlの末尾へ内容を追加しても構いません。その場合でも、得られる結果はまったく同じです。

私の方法を採用する場合に異なるのは、メインファイルを少しだけ修正する必要があることです。どこを変更すればよいのか分からない方もいると思いますので、この解決方法について詳しく説明し、正しい結果を得るための実装方法を順を追って紹介します。

ここでの目的は、後からSQLクエリを実行できるように、データベースへデータを登録することです。それでは、次のSQLスクリプトを見ていきましょう。

01. PRAGMA FOREIGN_KEYS = ON;
02. 
03. CREATE TABLE IF NOT EXISTS tb_Symbols
04. (
05.     id PRIMARY KEY,
06.     symbol NOT NULL UNIQUE
07. );
08. 
09. CREATE TABLE IF NOT EXISTS tb_Quotes
10. (
11.     of_day NOT NULL,
12.     price NOT NULL,
13.     fk_id NOT NULL,
14.     FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id)
15. );

SQLiteのコード

01. INSERT INTO tb_Symbols (id, symbol) VALUES
02.     (2, 'PETR4'),
03.     (1, 'ITUB3'),
04.     (3, 'VALE3');
05. 
06. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES
07.     ('2023-07-10', '22.00', 1),
08.     ('2023-07-11', '22.20', 1),
09.     ('2023-07-12', '22.40', 1),
10.     ('2023-07-13', '22.30', 1),
11.     ('2023-07-14', '22.60', 1),
12.     ('2023-07-10', '26.00', 2),
13.     ('2023-07-11', '26.20', 2),
14.     ('2023-07-12', '26.40', 2),
15.     ('2023-07-13', '26.30', 2),
16.     ('2023-07-14', '26.60', 2),
17.     ('2023-07-10', '62.00', 3),
18.     ('2023-07-11', '62.20', 3),
19.     ('2023-07-12', '62.40', 3),
20.     ('2023-07-13', '62.30', 3),
21.     ('2023-07-14', '62.60', 3);

SQLiteのコード

SQLスクリプトは2つに分かれていますが、それらの内容を1つにまとめても構いません。その場合は、先ほど示したメインのMQL5ファイルをそのまま使用してプログラムを実行できます。一方、2つのSQLスクリプトを別々のファイルとして管理する場合は、以下に示すようにメインのMQL5コードを修正する必要があります。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. #property description "Basic script for SQL database written in MQL5"
04. #property version   "1.00"
05. #property script_show_inputs
06. //+------------------------------------------------------------------+
07. #resource "\\Files\\Script 01.sql" as string SQL_Create
08. #resource "\\Files\\Script 02.sql" as string SQL_Insert
09. //+------------------------------------------------------------------+
10. #include <Market Replay\SQL\C_DB_SQL.mqh>
11. //+------------------------------------------------------------------+
12. input string user01 = "DataBase01";        //Database File Name
13. //+------------------------------------------------------------------+
14. C_DB_SQL *SQL;
15. //+------------------------------------------------------------------+
16. const string ExecScripts(void)
17. {
18.     string szMsg = (*SQL).ExecResourceSQL(SQL_Create);
19.     if (szMsg != NULL) return szMsg;
20.     return (*SQL).ExecResourceSQL(SQL_Insert);
21. };
22. //+------------------------------------------------------------------+
23. void OnStart()
24. {
25.     string szMsg;
26.     
27.     SQL = new C_DB_SQL(user01);
28.     
29.     szMsg = ExecScripts();
30.     Print(szMsg == NULL ? "Result of executing the SQL script: Success" : szMsg);
31. 
32.     delete SQL;
33. }
34. //+------------------------------------------------------------------+

MQL5のコード

ご覧のとおり、構成は非常にシンプルで、実用的かつ分かりやすいものです。最初のSQLスクリプトでは必要なテーブルを作成し、2番目のSQLスクリプトではデータベースへ値を追加してデータを登録します。これらのSQLスクリプトは以前にも登場していますので、それぞれがどのような処理をおこない、どのように連携して今回作成するレコードを生成しているのかは、すでにご理解いただいているでしょう。

それでは、次にMQL5のコードを見ていきます。まず、8行目が追加され、既存のリソース名もより適切な名前へ変更されていることに注目してください。29行目の処理の都合により、16行目にある関数を呼び出しています。この関数の役割は、2つのSQLスクリプトを実行することです。これらのスクリプトは、最終的にMQL5で生成される実行ファイルへ内部リソースとして組み込まれます。まず18行目で、テーブルを作成するSQLスクリプトの実行を要求します。続いて19行目で実行結果を確認し、最初のSQLスクリプトが正常に終了した場合のみ、20行目で2番目のSQLスクリプトを実行します。

2番目のSQLスクリプトの役割は、データベースへデータを挿入することです。この処理は非常にシンプルであり、特別な手間は必要ありません。これで、次の実装段階に必要な準備がすべて整いました。次は、MQL5コードからSQLデータベースへクエリを実行する機能を実装していきます。その前に、データベースファイルの内容を確認してみましょうこの確認にはMetaEditorを手軽に利用できます。

ご覧のとおり、期待どおりのデータがデータベースへ正しく追加されています。この点を確認できたので、次のステップに進みましょう。再びC_DB_SQL.mqhヘッダーファイルに戻ります。元の実装では、このクラスはクエリの実行結果を返すことができませんでした。以前、MQL5コードの説明を始める前にも説明したように、SELECT文自体は実行できます。しかし、その結果をMQL5側で利用できなければ、実用上の価値はほとんどありません。

MQL5の開発者は、SQLクエリの結果を取得するための複数の関数を用意しています。ここで改めて強調しておきますが、これから説明する内容は、すべてMetaTrader 5に組み込まれているSQLiteを利用することを前提としています。SQLiteをDLL経由で利用する場合は、手順が少し異なります。その方法については、機会があれば別の機会に説明したいと思います。現時点では、MetaTrader 5の内蔵SQLiteを使用して話を進めます。また、ソケット経由でデータベースへアクセスする場合も、まったく異なるアプローチが必要になります。これらを混同しないようにしてください。最終的な目的は同じでも、それぞれの環境に応じた実装方法が必要になります。

以上を踏まえたうえで、コードそのものに注目していきましょう。必要なのは、SQLクエリを実行し、その結果を必要に応じて簡単に利用・解析できるようにすることです。そのためには、新たに2つの関数を使用する必要があります。1つ目は、データベースクエリを実行するためのDatabasePrepare関数です。ここで重要なのは、多くの方が考えるようにDatabaseExecuteを使用すべきではないという点です。その理由は、DatabaseExecuteはSQLコマンドが正常に実行されたかどうかを返すだけだということです。クエリによって返された結果セットを取得したり、内容を確認したりすることはできません。

そのため、DatabasePrepareを使用します。この関数を利用することで、SQLクエリから返されたデータをMQL5アプリケーション側で取得し、その内容を確認できるようになります。これが最初に必要となる関数です。さらに、SQLクエリから返されたデータを読み取るための、もう1つの関数も必要になります。その説明に入る前に、まずDatabasePrepareを組み込むためにC_DB_SQLクラスがどのように変更されたのかを見ていきましょう。変更後のクラスコードを以下に示します。

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "..\Service Graphics\Support\C_Array.mqh"
005. //+------------------------------------------------------------------+
006. class C_DB_SQL
007. {
008.     private    :
009.         C_Array     m_Arr;
010.         int         m_handleDB,
011.                     m_Request;
012. //+------------------------------------------------------------------+
013.         void Convert(const char &buff[], const int size)
014.         {
015.             string sz0 = "";
016.             bool b0, b1, bs1, bs2, bc0, bc1, bc;
017.             int nLine = 1;
018.             
019.             b0 = b1 = bs1 = bs2 = bc0 = bc1 = bc = false;
020.             for (int count = 0, nC0 = nLine; count < size; count++)
021.             {
022.                 switch (buff[count])
023.                 {
024.                     case '\t':
025.                         sz0 += (bs1 || bs2 ? "\t" : "");
026.                         break;
027.                     case '\n':
028.                         nC0++;
029.                     case '\r':
030.                         bc0 = false;
031.                         break;
032.                     case ';':
033.                         b0 = (bs1 || bs2 || bc0 || bc1 ? b0 : true);
034.                     default:
035.                         switch (buff[count])
036.                         {
037.                             case '"':
038.                                 bs1 = (bs2 || bc0 || bc1 ? bs1 : !bs1);
039.                                 break;
040.                             case '\'':
041.                                 bs2 = (bs1 || bc0 || bc1 ? bs2 : !bs2);
042.                                 break;
043.                         }
044.                         if (((count + 1) < size) && (!bs1) && (!bs2))
045.                         {
046.                             if (bc = ((buff[count] == '-') && (buff[count + 1] == '-'))) bc0 = true;
047.                             if (bc = ((buff[count] == '/') && (buff[count + 1] == '*'))) bc1 = true;
048.                             if (bc = ((buff[count] == '*') && (buff[count + 1] == '/'))) bc1 = false;
049.                             if (bc)
050.                             {
051.                                 count += 1;
052.                                 bc = false;
053.                                 continue;
054.                             }
055.                         }
056.                         if (!(bc0 || bc1))
057.                         {
058.                             if ((!b1) && (buff[count] > ' '))
059.                             {
060.                                 b1 = true;
061.                                 nLine = nC0;
062.                             }
063.                             sz0 += (b1 ? StringFormat("%c", buff[count]) : "");
064.                         }
065.                 }
066.                 if (b0)
067.                 {
068.                     m_Arr.Add(sz0, nLine);
069.                     sz0 = "";
070.                     b0 = b1 = false;
071.                 }
072.             }
073.         }
074. //+------------------------------------------------------------------+
075.         const string ExecSQL(void)
076.         {
077.             string szCmd;
078.             
079.             for (int count = 0, nLine; count >= 0; count++)
080.             {
081.                 szCmd = m_Arr.At(count, nLine);
082.                 if (nLine < 0) break;
083.                 if (!ExecCommandSQL(szCmd))
084.                     return StringFormat("Execution of line %d of the SQL script failed...", nLine);
085.             }
086.             
087.             return NULL;
088.         }
089. //+------------------------------------------------------------------+
090.     public    :
091. //+------------------------------------------------------------------+
092.         C_DB_SQL(const string szFileName = ":memory:")
093.         {
094.             m_handleDB = DatabaseOpen(szFileName, DATABASE_OPEN_CREATE | DATABASE_OPEN_READWRITE);
095.             m_Request = INVALID_HANDLE;
096.         }
097. //+------------------------------------------------------------------+
098.         ~C_DB_SQL()
099.         {
100.             DatabaseClose(m_handleDB);
101.         }
102. //+------------------------------------------------------------------+
103.         const string ExecResourceSQL(const string szResource)
104.         {
105.             char buff[];
106.             int size;
107.             
108.             ArrayResize(buff, size = StringLen(szResource));
109.             StringToCharArray(szResource, buff);
110.             Convert(buff, size);
111.             ArrayFree(buff);
112.             
113.             return ExecSQL();
114.         }
115. //+------------------------------------------------------------------+
116.         const string ExecScriptSQL(const string szFileName)
117.         {
118.             int file, size;
119.             char buff[];
120.             
121.             if ((file = FileOpen(szFileName, FILE_READ | FILE_BIN)) == INVALID_HANDLE)
122.                 return StringFormat("Unable to open script file: %s", szFileName);
123.             ArrayResize(buff, size = (int) FileSize(file));
124.             FileReadArray(file, buff);                
125.             FileClose(file);
126.             Convert(buff, size);
127.             ArrayFree(buff);
128.             
129.             return ExecSQL();
130.         }
131. //+------------------------------------------------------------------+
132.         bool ExecCommandSQL(const string szCmd)
133.         {
134.             return (m_handleDB == INVALID_HANDLE ? false : DatabaseExecute(m_handleDB, szCmd));
135.         }
136. //+------------------------------------------------------------------+
137.         bool ExecRequestOfData(const string szCmd)
138.         {
139.             if (m_Request != INVALID_HANDLE) DatabaseFinalize(m_Request);
140.             return ((m_Request = DatabasePrepare(m_handleDB, szCmd)) != INVALID_HANDLE);
141.         }
142. //+------------------------------------------------------------------+
143. };
144. //+------------------------------------------------------------------+

C_DB_SQL.mqhのコード

11行目で新しい変数を追加していることに注目してください。この変数は、SQLクエリから返されるデータを読み取るために使用します。他の変数と同様に、この変数も95行目で初期化されます。ここで設定している値にも注目してください。続いて137行目へ移ります。ここではExecRequestOfData関数が実装されています。この関数は、SQLへクエリを送信するために使用します。通常、このクエリはSELECT ... FROM ...を使用して実行されますが、後で説明するように、この実装では少し特殊な使い方をすることもできます。

まず139行目に注目してください。ここでは、m_Request変数がINVALID_HANDLE以外の値を保持しているかどうかを確認しています。もしそうであれば、以前に実行したクエリがキャッシュ内に残っていることを意味します。つまり、新しいクエリを実行する前に、前回作成したプリペアドステートメントを終了し、使用していたリソースを解放する必要があります。この処理にはDatabaseFinalize関数を使用します。この関数の役割は、前回のプリペアドステートメントを終了し、そのリソースを解放することです。そのため、この関数はC_DB_SQLクラスを拡張するうえで必要となる3つ目の関数になります。

前回のプリペアドステートメントが存在する場合は、それを終了した後、140行目で新しいクエリを送信できます。ここで重要なのは、データベース名を指定する必要がないことです。データベースへの接続は、クラス生成時にすでにおこなわれています。そのため、ここでは実行するSQLコマンドだけを指定すれば十分です。実際のSQLコマンドは、この関数を呼び出す側から渡されます。その方法については後ほど説明します。クエリが正常に実行されると、DatabasePrepareは数値のハンドルを返します。この値は後から利用できるようm_Request変数へ保存されます。一方、クエリが失敗した場合はINVALID_HANDLEが返されます。その結果、ExecRequestOfDataはfalseを返し、呼び出し元へクエリ実行が失敗したことを通知します。

ここで1つ疑問があるかもしれません。「なぜExecRequestOfData関数から直接データを返さないのでしょうか。なぜ、クエリの成功・失敗だけを返す設計になっているのでしょうか。」理由は非常にシンプルです。状況によっては、特定の値や特定のテーブル項目だけを取得するクエリを実行したい場合があります。用途によって必要となるデータは異なり、それに応じて異なる方針を選んだり、別の判断を下したりすることがあります。もしクエリ実行時に結果を直接返す設計にすると、必要なデータを取得するたびに新しいSQLクエリを実行しなければならなくなります。これは間違った方法ではありませんが、不要なデータベースアクセスが増えることになります。特にクライアントサーバー構成では、このような無駄なクエリは大きな問題になります。

必要な項目は、1回のクエリでまとめて取得できます。SQLクエリを最適化できるだけでなく、MQL5側の処理速度もわずかですが向上します。このような理由から、データの読み取り処理は、専用の関数として独立させています。

一見すると、この設計はまったくナンセンスで、やや実用的でないように見えるかもしれません。しかし、クエリの実行と取得結果の読み取りを分離しているため、メインコード側では必要なフィールドだけを非常に簡単に取得できるようになります。この点は少し説明が難しいところです。SQLクエリを実行すると複数の値が返されることがありますが、実際に必要なのは、その中の1つのテーブルの1つの列だけという場合も少なくありません。この様子は、本記事の前半で示したアニメーションでも確認できます。

一方で、クエリが適切に記述されていれば、返される結果が1件だけになることもよくあります。もちろん、検索条件に一致するデータがデータベース内に存在しなければ、何も返されないこともあります。

ここには、多くの人が十分に理解していない重要なポイントがあります。データ読み取り関数の説明へ進む前に、この点について少し触れておきたいと思います。

多くの人はSQLを過小評価しがちで、あるいはSQLが実際にどのように動作するのかを十分に理解していないため、まったく使わないことさえあります。SQLデータベースへクエリを実行する際、必ずしも汎用的な答えを取得したいわけではありません。場合によっては、非常に具体的で実用的な答えだけを必要とすることもあります。もしデータベースが適切な構造とデータモデルによって設計されていれば、ほぼあらゆる種類の情報を統合することが可能です。これは非常に興味深いことです。多くの人は、データベースとは販売情報や顧客情報、商品情報などを保存するための数字や文字列の集合にすぎないと考えています。

しかし、SQLの仕組みと活用方法を正しく理解すれば、特定の銘柄について学習できるシステム、あるいは小さな学習ロボットを構築することも可能です。しかも、そのためにあらかじめ売買モデルを設計する必要はありません。価格データ(クォート)をデータベースへ蓄積し、SQLによる検索と、SQLでは実行できない計算をMQL5で組み合わせることで、市場で自律的に学習しながら取引をおこなう仕組みを構築できます。この仕組みは、自ら学習し、その銘柄の将来の値動きを予測できるようになります。

実は、このアイデアについては、リプレイ/シミュレーションシステムに関する本連載の以前の記事でも触れています。現在紹介しているこのシンプルなシステムから、どれほど多くのことが実現できるのか、まだ十分にイメージできていない方もいるかもしれません。しかし、このテーマをしっかり学べば、ここで説明しているような仕組みを実際に構築できるようになります。もちろん、そのためには数学的な知識やプログラミング技術も必要になります。こうした理由から、SQLクエリ実行時に、その結果を直接MQL5へ返す設計にはしていません。

クエリによっては、多くの人が想定するのとは異なる形で、その結果を利用することもできるからです。MQL5の開発者がこのような仕組みを提供している以上、その設計を変更する必要はないと考えています。また、取得したデータをどのように読み取るのかについては、短い説明だけでは十分に理解するのが難しい内容です。そのため、この続きとなる説明と、メインコードへ加える変更内容については、次回の記事で詳しく解説します。


最後に

今回は、いくつかの実装方法について説明しました。また、あらかじめ売買モデルを設計しなくても、市場で学習できる自動売買ロボットを構築できる可能性についても紹介しました。さらに、MetaTrader 5に組み込まれているSQLiteでは、一部のSQL機能をそのまま利用できない場合があること、そしてその回避方法についても説明しました。ただし、MQL5からSQLクエリの実行結果を取得する方法については、まだ説明していません。クエリ結果に応じて柔軟に対応できる関数を作成できるからです。これを理解していただくことで、MQL5でSQLを扱う際には、わずか6つの関数だけで十分であると説明した理由も明確になるでしょう。

最後にもう1つ付け加えておきます。MQL5には、すでにONNXモデルを利用するための仕組みが用意されています。市場で自律的に学習する自動売買ロボットを構築するのであれば、SQLよりもONNXモデルを利用する方が適しています。とはいえ、SQLを利用したモデリングによって同様の仕組みを実現することも可能です。大切なのは、知識を身に付け、それを正しく活用することです。

ファイル 説明
Experts\Expert Advisor.mq5
Chart TradeとEAの連携を示す(Mouse Studyが必要)
Indicators\Chart Trade.mq5 送信する注文を設定するウィンドウを作成する(Mouse Studyが必要)
Indicators\Market Replay.mq5 リプレイ/シミュレーションサービスと対話するためのコントロールを作成する(Mouse Studyが必要)
Indicators\Mouse Study.mq5 グラフィカルコントロールとユーザーとの相互作用を実現する(リプレイシステムと実市場での運用の両方に必要)
Services\Market Replay.mq5 市場リプレイ/シミュレーションサービス(システム全体のメインファイル)を作成および維持する
Code VS C++\Servidor.cpp C++で開発されたサーバーソケット(ミニチャット版)を作成および維持する
Code in Python\Server.py MetaTrader 5とExcel間の通信用のPythonソケットを作成および維持する
Indicators\Mini Chat.mq5 インジケータを通じてミニチャットを実装できるようにする(サーバーの使用が必要)
Experts\Mini Chat.mq5 EAを通じてミニチャットを実装できるようにする(サーバーの使用が必要)
Scripts\SQLite.mq5 MQL5でSQLスクリプトを使用する方法を示す
Files\Script 01.sql 外部キーを持つシンプルなテーブルの作成方法を示す
Files\Script 02.sql テーブルに値を追加する方法を示す

MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/13110

添付されたファイル |
Anexo.zip (571.71 KB)
初級から中級まで:オブジェクトイベント(I) 初級から中級まで:オブジェクトイベント(I)
チャート上のオブジェクトに何らかの変更が発生した際に、MetaTrader 5が生成する可能性のある6種類のイベントのうち、3つについて説明します。これらのイベントは、ユーザー操作という観点から非常に有用です。なぜなら、これらのイベントを理解していなければ、特定の目的でオブジェクトを管理しようとする際に、チャートの特定の構成を維持するために、より多くの作業を自分でおこなわなければならなくなるからです。
MQL5におけるタイムギャップ分析(第2回):時間経過に伴う流動性分布のヒートマップ作成 MQL5におけるタイムギャップ分析(第2回):時間経過に伴う流動性分布のヒートマップ作成
MetaTrader 5で、時間に対する価格分布を視覚化するヒートマップインジケータを作成する方法についての詳細なガイドです。時間密度分析の数学的な基礎に基づき、価格帯ごとの滞在時間をヒートマップとして可視化するMetaTrader 5向けインジケータの作成方法を詳しく解説します。このインジケータでは、各価格レベルの滞在時間を赤(滞在時間が最小)から青(滞在時間が最大)までの色で表現します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
時間帯別・曜日別・月の日付別の季節性インジケータ 時間帯別・曜日別・月の日付別の季節性インジケータ
金融市場における繰り返し発生する価格パターンを分析するためのツールの開発方法について説明します。分析対象となるのは、月の日付(1日~31日)、曜日(月曜日~日曜日)、または1日の時間帯(0時~23時)です。このインジケータは過去のデータを分析し、それぞれの期間における平均リターンを計算します。そして、その結果をヒストグラムと予測表示によって可視化します。また、季節性タイプ、分析するバー数、パーセント表示または絶対値表示、チャートカラーなど、ユーザーが変更可能なパラメータも備えています。