SQLite: MQL5 での SQL データベースのネイティブな処理

MetaQuotes | 24 2月, 2020

目次


メタトレーダー5における現代のアルゴリズム取引

MQL5は、構文と計算速度の両方の点で可能な限りC ++に近いので、アルゴリズムトレードに最適なソリューションと言えます。 MetaTrader5プラットフォームは、ユーザーにトレードロボットとカスタムインジケータを開発するための最新の専門言語を提供し、シンプルなトレードタスクを超えて複雑な分析システムを作成できるようにしています。

非同期取引関数や数学ライブラリに加えて、トレーダーはネットワーク関数にアクセスし、Pythonにデータをインポートし、OpenCLでの並列コンピューティング、"smart"関数インポートを含む.NETライブラリのネイティブサポート、MS Visual Studioとの統合、DirectXを使用したデータビジュアライゼーションにもアクセスできます。 現代のアルゴリズム取引の武器に不可欠なこれらのツールは、現在、ユーザーがMetaTrader5取引プラットフォームを離れることなく、さまざまなタスクを解決することができます。


データベースを操作するための関数

トレード戦略の開発には、大量のデータの処理が関連しています。 MQL5 プログラムの形式でのトレードアルゴリズムは高速で信頼できますが、もはや十分とは言えない場合もあります。 信頼できる結果を得るためには、トレーダーは、さまざまなトレード商品に対して膨大な数のテストと最適化を実行し、結果を保存して処理し、分析を行い、次を決定する必要があります。

MQL5で、シンプルで人気のあるSQLiteエンジンを使用してデータベースを操作できるようになりました。 開発者の Web サイトのテスト結果は、SQL クエリの実行の高値速度を示します。 ほとんどのテストでは、PostgreSQLとMySQLを上回っています。 次に、MQL5とLLVM 9.0.0でテスト実行の速度を比較し、表に示しました。 実行結果はミリ秒単位で与えられます。低ければ低いほど良い

名前
詳細
 LLVM  
MQL5
Test 1
 1000 INSERTs
11572
8488
Test 2
 25000 トランザクション内のINSERT
59
60
Test 3
 25000 インデックス付きテーブルへの INSERT
102
105
Test 4
 100 インデックスのないSELECT
142
150
Test 5
 100 ストリング比較の場合のSELECT
391
390
Test 6
 インデックスの作成
43
33
Test 7
 5000 インデックス付きのSELECT
385
307
Test 8
 1000 インデックスのないUPDATE
58
54
Test 9
 25000 インデックス付きのUPDATE
161
165
Test 10
 インデックス付きの 25000 テキスト UPDATE
124
120
Test 11  SELECTからのINSERT
84
84
Test 12
 インデックスなしの DELETE
25
74
Test 13
 インデックスを使用してDELETE
70
72
Test 14  大きなDELETEの後の大きなINSERT
62
66
Test 15  大きな DELETE の後に多くの小さな INSERT
33
33
Test 16  DROP TABLE: finished
42
40

テストの詳細は、添付された SqLiteTest.zip ファイルで確認できます。 測定が行われたコンピュータの仕様 - Windows 10 x64、インテルXeon E5-2690 v3 @ 2.60GHz。 

この結果は、MQL5でデータベースを操作する際に最大のパフォーマンスを確実に得ることができることを示しています。 これまで SQLに触ったことがない人は、構造化クエリの言語によって、複雑なループやサンプリングを必要とせずに、多くのタスクを迅速かつ洗練的に解決できることがわかるでしょう。


シンプルなクエリ

データベースはテーブル形式で情報を保存し、新しいデータの受信/変更と追加は SQL 言語のクエリを使用して行います。 簡単なデータベースの作成と、そこからデータを取得する方法を見てみましょう。

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   string filename="company.sqlite";
//--- create or open the database in the common terminal folder
   int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      return;
     }
... working with the database


//--- close the database
   DatabaseClose(db);
  }

データベースの作成と終了は、ファイルの操作と似ています。 まず、データベースのハンドルを作成し、次にチェックし、最後に閉じます。

次に、データベース内のテーブルの存在を確認します。 テーブルが既に存在する場合は、上記の例のデータを挿入しようとすると、エラーが発生します。

//--- if the COMPANY table exists, delete it
   if(DatabaseTableExists(db, "COMPANY"))
     {
      //--- delete the table
      if(!DatabaseExecute(db, "DROP TABLE COMPANY"))
        {
         Print("Failed to drop table COMPANY with code ", GetLastError());
         DatabaseClose(db);
         return;
        }
     }
//--- create the COMPANY table 
   if(!DatabaseExecute(db, "CREATE TABLE COMPANY("
                       "ID INT PRIMARY KEY     NOT NULL,"
                       "NAME           TEXT    NOT NULL,"
                       "AGE            INT     NOT NULL,"
                       "ADDRESS        CHAR(50),"
                       "SALARY         REAL );"))
     {
      Print("DB: ", filename, " create table failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

テーブルはクエリを使用して作成および削除され、実行結果は常にチェックされます。 COMPANY テーブルには、エントリ ID、名前、年齢、住所、給与の 5 つのフィールドしかありません。 ID フィールドはキー、つまり一意のインデックスです。 インデックスは、各インプットの信頼性の高い定義を可能にし、一緒にリンクするために異なるテーブルで使用することができます。 ポジション ID が特定のポジションに関連するすべてのトレードとオーダーをリンクする方法に似ています。

これでテーブルにデータが格納されます。 これは INSERT クエリを使用して行われます。

//--- enter data to the table 
   if(!DatabaseExecute(db, "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1,'Paul',32,'California',25000.00); "
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (2,'Allen',25,'Texas',15000.00); "
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (3,'Teddy',23,'Norway',20000.00);"
                       "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (4,'Mark',25,'Rich-Mond',65000.00);"))
     {
      Print("DB: ", filename, " insert failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

見てわかるように、4 つのエントリが COMPANY テーブルに追加されます。 フィールドの順序およびこれらのフィールドに挿入される値は、各エントリ項目で指定されます。 各インプットは、1 つのクエリに結合された個別の "INSERT..." クエリによって挿入されます。 つまり、別々のDatabaseExecute()呼び出しによって、各インプットをテーブルに挿入できます。

スクリプト操作が完了すると、データベースはcompany.sqliteファイルに保存されるため、次回のスクリプトの起動時に同じ ID を持つ COMPANY テーブルに同じデータを書き込もうとします。 この場合、エラーが発生します。 このため、スクリプトが起動するたびにタスクを最初から開始できるように、最初にテーブルを削除しました。

次に、SALARY フィールド が 15000以上 の COMPANY テーブルからすべてのエントリを取得します。これは、クエリテキストをコンパイルし、その後に使用するハンドルを返すDatabasePrepare()関数かDatabaseReadBind()を使用して行われます。

//--- create a query and get a handle for it
   int request=DatabasePrepare(db, "SELECT * FROM COMPANY WHERE SALARY>15000");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }

クエリが正常に作成された後、実行結果を取得する必要があります。 これを行うには、最初の呼び出し中にクエリを実行し、結果の最初のインプットに移動する DatabaseRead() を使用します。 トレーリングの呼び出しのたびに、次のインプットを最後まで読み取ります。 この場合、'false' を返し、"no more entries" を返します。

//--- print all entries with the salary greater than 15000
   int    id, age;
   string name, address;
   double salary;
   Print("Persons with salary > 15000:");
   for(int i=0; DatabaseRead(request); i++)
     {
      //--- read the values of each field from the obtained entry
      if(DatabaseColumnInteger(request, 0, id) && DatabaseColumnText(request, 1, name) &&
         DatabaseColumnInteger(request, 2, age) && DatabaseColumnText(request, 3, address) && DatabaseColumnDouble(request, 4, salary))
         Print(i, ":  ", id, " ", name, " ", age, " ", address, " ", salary);
      else
        {
         Print(i, ": DatabaseRead() failed with code ", GetLastError());
         DatabaseFinalize(request);
         DatabaseClose(db);
         return;
        }
     }
//--- remove the query after use
   DatabaseFinalize(request);

実行結果は次のとおりです。

Persons with salary > 15000:
0:  1 Paul 32 California 25000.0
1:  3 Teddy 23 Norway 20000.0
2:  4 Mark 25 Rich-Mond  65000.0


DatabaseRead.mq5 ファイルに完全なサンプル コードがあります。

メタエディターでの SQL クエリのデバッグ

データベースを操作するためのすべての関数は、コードが失敗した場合にエラーコードを返します。 次の操作を行うと、次の 4 つのシンプルなルールに従っても問題が発生しません。

  1. すべてのクエリ ハンドルは、使用後に破棄する必要があります。
  2. データベースは完了前にデータベースを閉じる必要があります。
  3. クエリ実行結果をチェックする必要があります。
  4. エラーが発生した場合、クエリは最初に破棄され、データベースは後で閉じられます。

最も難しいのは、クエリが作成されていない場合のエラーを理解することです。 メタエディターでは、*.sqlite ファイルを開いて、SQL クエリを使用してファイルを操作できます。 例として、company.sqlite ファイルを使用してどのように行われるかを見てみましょう。

1. 共通のターミナル フォルダで、company.sqlite ファイルを開きます。

2. データベースを開くと、ナビゲーターに COMPANY テーブルが表示されます。 ダブルクリックします。

3. "SELECT * FROM COMPANY" クエリがステータスバーに自動的に作成されます。

4. クエリは自動的に実行されます。 F9 キーを押すか、または [実行] をクリックして実行することもできます。

5. クエリ実行結果を参照してください。

6. 何か問題がある場合は、エラーがエディタのJournalに表示されます。


SQL クエリを使用すると、テーブル フィールドの統計情報 (合計や平均など) を取得できます。 クエリを作成し、動作するかどうか確認しましょう。

メタエディターでのクエリの操作

MQL5 コードでクエリを実装できます。

   Print("Some statistics:");
//--- prepare a new query about the sum of salaries
   request=DatabasePrepare(db, "SELECT SUM(SALARY) FROM COMPANY");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }
   while(DatabaseRead(request))
     {
      double total_salary;
      DatabaseColumnDouble(request, 0, total_salary);
      Print("Total salary=", total_salary);
     }
//--- remove the query after use
   DatabaseFinalize(request);
 
//--- prepare a new query about the average salary
   request=DatabasePrepare(db, "SELECT AVG(SALARY) FROM COMPANY");
   if(request==INVALID_HANDLE)
     {
      Print("DB: ", filename, " request failed with code ", GetLastError());
      ResetLastError();
      DatabaseClose(db);
      return;
     }
   while(DatabaseRead(request))
     {
      double aver_salary;
      DatabaseColumnDouble(request, 0, aver_salary);
      Print("Average salary=", aver_salary);
     }
//--- remove the query after use
   DatabaseFinalize(request);

実行結果を比較します。

統計:
Total salary=125000.0
Average salary=31250.0



DatabaseReadBind()を使用して構造にクエリ結果を自動的に読み込む

DatabaseRead() 関数を使用すると、すべてのクエリ結果のインプットを調べ、結果のテーブルの各列に関する完全なデータを取得できます。

これらの関数を使用すると、クエリ結果を統一して操作できます。 ただし、この利点は、過度のコードによって相殺されます。 クエリ結果の構造があらかじめわかっている場合は、DatabaseReadBind()関数を使用して、インプット全体をすぐに構造体に読み込むことをお勧めします。 前の例を次のようにやり直すことができます — まず Person 構造体を宣言します。

struct Person
  {
   int               id;
   string            name;
   int               age;
   string            address;
   double            salary;
  };

次に、各インプットは、DatabaseReadBind(request, person)を使用してクエリ結果から読み取ります。

//--- display obtained query results
   Person person;
   Print("Persons with salary > 15000:");
   for(int i=0; DatabaseReadBind(request, person); i++)
      Print(i, ":  ", person.id, " ", person.name, " ", person.age, " ", person.address, " ", person.salary);
//--- remove the query after use
   DatabaseFinalize(request);


これにより、現在のエントリからすべてのフィールドの値をすぐに取得でき、それらを個別に読み取る必要はありません。


トランザクションをDatabaseTransactionBegin()/DatabaseTransactionCommit()にラップ 

テーブルを操作する場合は、INSERT、UPDATE、またはDELETEコマンドを使用する必要がある場合があります。 これを行う最善の方法は、トランザクションを使用することです。 トランザクションを実行する場合、データベースはまずブロックされます (DatabaseTransactionBegin) 。 一括変更コマンドが実行され保存されます (DatabaseTransactionCommit) またはエラーの場合はキャンセルされます (DatabaseTransactionRollback) 。

DatabasePrepare関数の説明には、トランザクションの使用例が含まれています。

//--- 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);

トランザクションでは、DatabaseTransactionBeginの例に示すように、テーブルの一括操作を何百回も高速化できます。

結果:
   Deals in the trading history: 2737 
   Transations WITH    DatabaseTransactionBegin/DatabaseTransactionCommit: time=48.5 milliseconds
   Transations WITHOUT DatabaseTransactionBegin/DatabaseTransactionCommit: time=25818.9 milliseconds
   Use of DatabaseTransactionBegin/DatabaseTransactionCommit provided acceleration by 532.8 times


トレードヒストリートレードの取り扱い

SQL クエリの関数は、コードを記述せずにソース データを並べ替え、選択、および変更できることにあります。 Singeクエリを使用してトレードからトレードを取得する方法を示すDatabasePrepare関数の説明から例を分析し続けましょう。 トレード関数は、ポジションのエントリー/決済の日付と価格、ならびにシンボル、方向、およびボリューム情報に関するデータを提供します。 トレード構造を見ると、エントリ/決済が共通ポジションIDによってリンクされていることがわかります。 したがって、ヘッジ口座に簡単なトレードシステムがあれば、2つのトレードを1つのトレードに組み合わせることができます。 これは次のクエリを使用して行われます。

//--- fill in the TRADES table using an SQL query based on DEALS table data
   ulong start=GetMicrosecondCount();
   if(DatabaseTableExists(db, "DEALS"))
     {
      //--- fill in the TRADES table
      if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) "
                          "SELECT "
                          "   d1.time as time_in,"
                          "   d1.position_id as ticket,"
                          "   d1.type as type,"
                          "   d1.volume as volume,"
                          "   d1.symbol as symbol,"
                          "   d1.price as price_in,"
                          "   d2.time as time_out,"
                          "   d2.price as price_out,"
                          "   d1.commission+d2.commission as commission,"
                          "   d2.swap as swap,"
                          "   d2.profit as profit "
                          "FROM DEALS d1 "
                          "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id "
                          "WHERE d1.entry=0 AND d2.entry=1"))
        {
         Print("DB: fillng the TRADES table failed with code ", GetLastError());
         return;
        }
     }
   ulong transaction_time=GetMicrosecondCount()-start;

ここでは、既存のDEALSテーブルを使用します。 このエントリーは、INNER JOIN を介して内部組み合わせを使用して、同一のDEAL_POSITION_IDを持つトレードから作成されます。 トレード口座でDatabasePrepareから操作例を示した結果:

結果:
   Deals in the trading history: 2741 
   The first 10 deals:
       [ticket] [order_ticket] [position_ticket]              [time] [type] [entry] [symbol] [volume]   [price]   [profit] [swap] [commission] [magic] [reason]
   [0] 34429573              0                 0 2019.09.05 22:39:59      2       0 ""        0.00000   0.00000 2000.00000 0.0000      0.00000       0        0
   [1] 34432127       51447238          51447238 2019.09.06 06:00:03      0       0 "USDCAD"  0.10000   1.32320    0.00000 0.0000     -0.16000     500        3
   [2] 34432128       51447239          51447239 2019.09.06 06:00:03      1       0 "USDCHF"  0.10000   0.98697    0.00000 0.0000     -0.16000     500        3
   [3] 34432450       51447565          51447565 2019.09.06 07:00:00      0       0 "EURUSD"  0.10000   1.10348    0.00000 0.0000     -0.18000     400        3
   [4] 34432456       51447571          51447571 2019.09.06 07:00:00      1       0 "AUDUSD"  0.10000   0.68203    0.00000 0.0000     -0.11000     400        3
   [5] 34432879       51448053          51448053 2019.09.06 08:00:00      1       0 "USDCHF"  0.10000   0.98701    0.00000 0.0000     -0.16000     600        3
   [6] 34432888       51448064          51448064 2019.09.06 08:00:00      0       0 "USDJPY"  0.10000 106.96200    0.00000 0.0000     -0.16000     600        3
   [7] 34435147       51450470          51450470 2019.09.06 10:30:00      1       0 "EURUSD"  0.10000   1.10399    0.00000 0.0000     -0.18000     100        3
   [8] 34435152       51450476          51450476 2019.09.06 10:30:00      0       0 "GBPUSD"  0.10000   1.23038    0.00000 0.0000     -0.20000     100        3
   [9] 34435154       51450479          51450479 2019.09.06 10:30:00      1       0 "EURJPY"  0.10000 118.12000    0.00000 0.0000     -0.18000     200        3
 
   The first 10 trades:
                 [time_in] [ticket] [type] [volume] [symbol] [price_in]          [time_out] [price_out] [commission]   [swap]  [profit]
   [0] 2019.09.06 06:00:03 51447238      0  0.10000 "USDCAD"    1.32320 2019.09.06 18:00:00     1.31761     -0.32000  0.00000 -42.43000
   [1] 2019.09.06 06:00:03 51447239      1  0.10000 "USDCHF"    0.98697 2019.09.06 18:00:00     0.98641     -0.32000  0.00000   5.68000
   [2] 2019.09.06 07:00:00 51447565      0  0.10000 "EURUSD"    1.10348 2019.09.09 03:30:00     1.10217     -0.36000 -1.31000 -13.10000
   [3] 2019.09.06 07:00:00 51447571      1  0.10000 "AUDUSD"    0.68203 2019.09.09 03:30:00     0.68419     -0.22000  0.03000 -21.60000
   [4] 2019.09.06 08:00:00 51448053      1  0.10000 "USDCHF"    0.98701 2019.09.06 18:00:01     0.98640     -0.32000  0.00000   6.18000
   [5] 2019.09.06 08:00:00 51448064      0  0.10000 "USDJPY"  106.96200 2019.09.06 18:00:01   106.77000     -0.32000  0.00000 -17.98000
   [6] 2019.09.06 10:30:00 51450470      1  0.10000 "EURUSD"    1.10399 2019.09.06 14:30:00     1.10242     -0.36000  0.00000  15.70000
   [7] 2019.09.06 10:30:00 51450476      0  0.10000 "GBPUSD"    1.23038 2019.09.06 14:30:00     1.23040     -0.40000  0.00000   0.20000
   [8] 2019.09.06 10:30:00 51450479      1  0.10000 "EURJPY"  118.12000 2019.09.06 14:30:00   117.94100     -0.36000  0.00000  16.73000
   [9] 2019.09.06 10:30:00 51450480      0  0.10000 "GBPJPY"  131.65300 2019.09.06 14:30:01   131.62500     -0.40000  0.00000  -2.62000
   TRADES テーブルの記入には12.51ミリ秒かかりました

ヘッジアカウントでこのスクリプトを起動し、ヒストリーのポジションと結果を比較します。 以前は、もしかしたらこのような結果を得るため、ループをコーディングするのに十分な知識や時間がなかった場合でも、 これで、単一の SQL クエリで実行できます。 スクリプト操作の結果は、メタエディタ で表示できます。 これを行うには、添付のtrades.sqliteファイルを開きます。


戦略別ポートフォリオ分析

上記のDatabasePrepareスクリプト操作の結果は、複数の通貨ペアで取引が行われていることを明らかにします。 また、[magic]列には100~600の値が表示されます。 トレード口座は、トレードを識別するために独自のマジックナンバーを持つそれぞれ戦略によって管理されていることを意味します。

SQL クエリを使用すると、magic値のコンテキストでトレードを分析できます。

//--- get trading statistics for Expert Advisors by Magic Number
   request=DatabasePrepare(db, "SELECT r.*,"
                           "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                           "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                           "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                           " r.gross_profit/r.win_trades average_profit、"
                           「gross_loss/loss_tradesaverage_loss」
                           "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                           "FROM "
                           "   ("
                           "   SELECT MAGIC,"
                           "   sum(case when entry =1 then 1 else 0 end) as trades,"
                           "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                           "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                           "   sum(swap) as total_swap,"
                           "   sum(commission) as total_commission,"
                           "   sum(profit) as total_profit,"
                           "   sum(profit+swap+commission) as net_profit,"
                           "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                           "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                           "   FROM DEALS "
                           "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                           "   GROUP BY MAGIC"
                           "   ) as r");

結果:

Trade statistics by Magic Number
    [magic] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[0]     100      242     2584.80000  -2110.00000          -33.36000    -93.53000      474.80000    347.91000          143            99           1.96198      59.09091       40.90909         18.07552      -21.31313         1.22502
[1]     200      254     3021.92000  -2834.50000          -29.45000    -98.22000      187.42000     59.75000          140           114           0.73787      55.11811       44.88189         21.58514      -24.86404         1.06612
[2]     300      250     2489.08000  -2381.57000          -34.37000    -96.58000      107.51000    -23.44000          134           116           0.43004      53.60000       46.40000         18.57522      -20.53078         1.04514
[3]     400      224     1272.50000  -1283.00000          -24.43000    -64.80000      -10.50000    -99.73000          131            93          -0.04687      58.48214       41.51786          9.71374      -13.79570         0.99182
[4]     500      198     1141.23000  -1051.91000          -27.66000    -63.36000       89.32000     -1.70000          116            82           0.45111      58.58586       41.41414          9.83819      -12.82817         1.08491
[5]     600      214     1317.10000  -1396.03000          -34.12000    -68.48000      -78.93000   -181.53000          116            98          -0.36883      54.20561       45.79439         11.35431      -14.24520         0.94346


6つの戦略のうち4つは収益性が高いことが判明しました。 各戦略の統計値を受け取りました。

損益計算の統計では、ポジションに対して発生したスワップとコミッションは考慮されません。 これより、正味コストを確認できます。 戦略は小さな利益をもたらすが、スワップや手数料に一般的に不採算であることが判明するかもしれません。


シンボルによるトレードの分析

シンボルによるトレードを分析することができます。 これを行うには、次のクエリを実行します。

//--- get trading statistics per symbols
   int request=DatabasePrepare(db, "SELECT r.*,"
                               "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                               "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                               "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                               " r.gross_profit/r.win_trades average_profit、"
                               「gross_loss/loss_tradesaverage_loss」
                               "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                               "FROM "
                               "   ("
                               "   SELECT SYMBOL,"
                               "   sum(case when entry =1 then 1 else 0 end) as trades,"
                               "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                               "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                               "   sum(swap) as total_swap,"
                               "   sum(commission) as total_commission,"
                               "   sum(profit) as total_profit,"
                               "   sum(profit+swap+commission) as net_profit,"
                               "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                               "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                               "   FROM DEALS "
                               "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                               "   GROUP BY SYMBOL"
                               "   ) as r");

結果:

Trade statistics by Symbol
      [名前][トレード][gross_profit][gross_loss][total_commission][total_swap][total_profit][net_profit][win_trades][loss_trades][expected_payoff][win_percent][loss_percent][average_profit][average_loss][profit_factor]
[0] "AUDUSD"      112      503.20000   -568.00000           -8.83000    -24.64000      -64.80000    -98.27000           70            42          -0.57857      62.50000       37.50000          7.18857      -13.52381         0.88592
[1] "EURCHF"      125      607.71000   -956.85000          -11.77000    -45.02000     -349.14000   -405.93000           54            71          -2.79312      43.20000       56.80000         11.25389      -13.47676         0.63512
[2] "EURJPY"      127     1078.49000  -1057.83000          -10.61000    -45.76000       20.66000    -35.71000           64            63           0.16268      50.39370       49.60630         16.85141      -16.79095         1.01953
[3] "EURUSD"      233     1685.60000  -1386.80000          -41.00000    -83.76000      298.80000    174.04000          127           106           1.28240      54.50644       45.49356         13.27244      -13.08302         1.21546
[4] "GBPCHF"      125     1881.37000  -1424.72000          -22.60000    -51.56000      456.65000    382.49000           80            45           3.65320      64.00000       36.00000         23.51712      -31.66044         1.32052
[5] "GBPJPY"      127     1943.43000  -1776.67000          -18.84000    -52.46000      166.76000     95.46000           76            51           1.31307      59.84252       40.15748         25.57145      -34.83667         1.09386
[6] "GBPUSD"      121     1668.50000  -1438.20000           -7.96000    -49.93000      230.30000    172.41000           77            44           1.90331      63.63636       36.36364         21.66883      -32.68636         1.16013
[7] "USDCAD"       99      405.28000   -475.47000           -8.68000    -31.68000      -70.19000   -110.55000           51            48          -0.70899      51.51515       48.48485          7.94667       -9.90563         0.85238
[8] "USDCHF"      206     1588.32000  -1241.83000          -17.98000    -65.92000      346.49000    262.59000          131            75           1.68199      63.59223       36.40777         12.12458      -16.55773         1.27902
[9] "USDJPY"      107      464.73000   -730.64000          -35.12000    -34.24000     -265.91000   -335.27000           50            57          -2.48514      46.72897       53.27103          9.29460      -12.81825         0.63606


統計によると、純利益は10シンボル中5個(net_profit>0)で受け取られ、利益係数は10シンボル中6個(profit_factor>1)でプラスでしました。 これはスワップと手数料がEURJPYで戦略を不採算にする場合にまさに当てはまります。


エントリー時間によるトレードの分析

トレードが単一の銘柄で行われ、単一の戦略が適用される場合でも、相場参入時間別のトレード分析は依然として有用です。 次の SQL クエリによって行われます。

//--- get trading statistics by market entry hours
   request=DatabasePrepare(db, "SELECT r.*,"
                           "   (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff,"
                           "   (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent,"
                           "   (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent,"
                           " r.gross_profit/r.win_trades average_profit、"
                           「gross_loss/loss_tradesaverage_loss」
                           "   (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor "
                           "FROM "
                           "   ("
                           "   SELECT HOUR_IN,"
                           "   count() as trades,"
                           "   sum(volume) as volume,"
                           "   sum(case when profit > 0 then profit else 0 end) as gross_profit,"
                           "   sum(case when profit < 0 then profit else 0 end) as gross_loss,"
                           "   sum(profit) as net_profit,"
                           "   sum(case when profit > 0 then 1 else 0 end) as win_trades,"
                           "   sum(case when profit < 0 then 1 else 0 end) as loss_trades "
                           "   FROM TRADES "
                           "   WHERE SYMBOL <> '' and SYMBOL is not NULL "
                           "   GROUP BY HOUR_IN"
                           "   ) as r");

結果:

Trade statistics by entry hour
     [hour_in] [trades] [volume] [gross_profit] [gross_loss] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor]
[ 0]         0       50  5.00000      336.51000   -747.47000   -410.96000           21            29          -8.21920      42.00000       58.00000         16.02429      -25.77483         0.45020
[ 1]         1       20  2.00000      102.56000    -57.20000     45.36000           12             8           2.26800      60.00000       40.00000          8.54667       -7.15000         1.79301
[ 2]         2        6  0.60000       38.55000    -14.60000     23.95000            5             1           3.99167      83.33333       16.66667          7.71000      -14.60000         2.64041
[ 3]         3       38  3.80000      173.84000   -200.15000    -26.31000           22            16          -0.69237      57.89474       42.10526          7.90182      -12.50938         0.86855
[ 4]         4       60  6.00000      361.44000   -389.40000    -27.96000           27            33          -0.46600      45.00000       55.00000         13.38667      -11.80000         0.92820
[ 5]         5       32  3.20000      157.43000   -179.89000    -22.46000           20            12          -0.70187      62.50000       37.50000          7.87150      -14.99083         0.87515
[ 6]         6       18  1.80000       95.59000   -162.33000    -66.74000           11             7          -3.70778      61.11111       38.88889          8.69000      -23.19000         0.58886
[ 7]         7       14  1.40000       38.48000   -134.30000    -95.82000            9             5          -6.84429      64.28571       35.71429          4.27556      -26.86000         0.28652
[ 8]         8       42  4.20000      368.48000   -322.30000     46.18000           24            18           1.09952      57.14286       42.85714         15.35333      -17.90556         1.14328
[ 9]         9      118 11.80000     1121.62000   -875.21000    246.41000           72            46           2.08822      61.01695       38.98305         15.57806      -19.02630         1.28154
[10]        10      206 20.60000     2280.59000  -2021.80000    258.79000          115            91           1.25626      55.82524       44.17476         19.83122      -22.21758         1.12800
[11]        11      138 13.80000     1377.02000   -994.18000    382.84000           84            54           2.77420      60.86957       39.13043         16.39310      -18.41074         1.38508
[12]        12      152 15.20000     1247.56000  -1463.80000   -216.24000           84            68          -1.42263      55.26316       44.73684         14.85190      -21.52647         0.85227
[13]        13       64  6.40000      778.27000   -516.22000    262.05000           36            28           4.09453      56.25000       43.75000         21.61861      -18.43643         1.50763
[14]        14       62  6.20000      536.93000   -427.47000    109.46000           38            24           1.76548      61.29032       38.70968         14.12974      -17.81125         1.25606
[15]        15       50  5.00000      699.92000   -413.00000    286.92000           28            22           5.73840      56.00000       44.00000         24.99714      -18.77273         1.69472
[16]        16       88  8.80000      778.55000   -514.00000    264.55000           51            37           3.00625      57.95455       42.04545         15.26569      -13.89189         1.51469
[17]        17       76  7.60000      533.92000  -1019.46000   -485.54000           44            32          -6.38868      57.89474       42.10526         12.13455      -31.85813         0.52373
[18]        18       52  5.20000      237.17000   -246.78000     -9.61000           24            28          -0.18481      46.15385       53.84615          9.88208       -8.81357         0.96106
[19]        19       52  5.20000      407.67000   -150.36000    257.31000           30            22           4.94827      57.69231       42.30769         13.58900       -6.83455         2.71129
[20]        20       18  1.80000       65.92000    -89.09000    -23.17000            9             9          -1.28722      50.00000       50.00000          7.32444       -9.89889         0.73993
[21]        21       10  1.00000       41.86000    -32.38000      9.48000            7             3           0.94800      70.00000       30.00000          5.98000      -10.79333         1.29277
[22]        22       14  1.40000       45.55000    -83.72000    -38.17000            6             8          -2.72643      42.85714       57.14286          7.59167      -10.46500         0.54408
[23]        23        2  0.20000        1.20000     -1.90000     -0.70000            1             1          -0.35000      50.00000       50.00000          1.20000       -1.90000         0.63158

取引の最大数は、9時間から16時間の間に行われることは明らかです。 他の時間中のトレードは、より少ないトレードを与え、ほとんど不採算です。 DatabaseExecute()関数の例で、 3 つのクエリタイプを持つ完全なソースコードを見つけます。


データベースのEAログへの便利なDatabasePrint()

前の例では、すべてのエントリを構造に読み込み、クエリ結果を表示するためにエントリを 1 つずつ表示する必要がありました。 テーブルまたはクエリの結果値を表示するだけの構造を作成すると、不便な場合があります。 DatabasePrint() 関数は、このような場合に追加されました。

long  DatabasePrint(
   int     database,          // database handle received in DatabaseOpen
   string  table_or_sql,      // a table or an SQL query
   uint    flags              // combination of flags
   );


既存のテーブルだけでなく、テーブルとして表すことができるクエリ実行結果も出力できます。 たとえば、次のクエリを使用して、DEALS テーブルの値を表示します。

   DatabasePrint(db,"SELECT * from DEALS",0);

結果 (最初の 10 行が表示されます):

  #|       ID ORDER_ID POSITION_ID       TIME TYPE ENTRY SYMBOL VOLUME   PRICE  PROFIT  SWAP COMMISSION MAGIC REASON
---+----------------------------------------------------------------------------------------------------------------
  1| 34429573        0           0 1567723199    2     0           0.0     0.0  2000.0   0.0        0.0     0      0 
  2| 34432127 51447238    51447238 1567749603    0     0 USDCAD    0.1  1.3232     0.0   0.0      -0.16   500      3 
  3| 34432128 51447239    51447239 1567749603    1     0 USDCHF    0.1 0.98697     0.0   0.0      -0.16   500      3 
  4| 34432450 51447565    51447565 1567753200    0     0 EURUSD    0.1 1.10348     0.0   0.0      -0.18   400      3 
  5| 34432456 51447571    51447571 1567753200    1     0 AUDUSD    0.1 0.68203     0.0   0.0      -0.11   400      3 
  6| 34432879 51448053    51448053 1567756800    1     0 USDCHF    0.1 0.98701     0.0   0.0      -0.16   600      3 
  7| 34432888 51448064    51448064 1567756800    0     0 USDJPY    0.1 106.962     0.0   0.0      -0.16   600      3 
  8| 34435147 51450470    51450470 1567765800    1     0 EURUSD    0.1 1.10399     0.0   0.0      -0.18   100      3 
  9| 34435152 51450476    51450476 1567765800    0     0 GBPUSD    0.1 1.23038     0.0   0.0       -0.2   100      3 
 10| 34435154 51450479    51450479 1567765800    1     0 EURJPY    0.1  118.12     0.0   0.0      -0.18   200      3 


データのインポート/エクスポート

データのインポート/エクスポートを簡素化するために、DatabaseImport()関数とDatabaseExport()関数が追加されました。 関数は、ZIPアーカイブ内のCSVファイルとデータを操作することができます。

指定したテーブルにデータをDatabaseImport()インポートします。 指定した名前のテーブルが存在しない場合は、自動的に作成されます。また、作成されたテーブルの名前とフィールドの種類も、ファイル データに基づいて自動的に定義されます。 

DatabaseExport() を使用すると、テーブルまたはクエリの結果をファイルに保存できます。 クエリ結果がエクスポートされる場合、SQL クエリは "SELECT" または "select" で始まる必要があります。 つまり、SQL クエリはデータベースの状態を変更できません。

MQL5 ドキュメントの関数の詳細な説明を参照してください。


最適化結果をデータベースに保存

データベースを操作するための関数は、最適化結果の処理にも使用できます。 標準配信の MACD サンプルEAを使用して、フレームを使用してテスト結果を取得し、すべての最適化基準の値を後で単一のファイルに保存する方法を説明します。 これを行うには、CDatabaseFrames クラスを作成し、そこでトレード統計を送信するための OnTester() メソッドを定義します。

//+------------------------------------------------------------------+
//| Tester function - sends trading statistics in a frame            |
//+------------------------------------------------------------------+
void               CDatabaseFrames::OnTester(const double OnTesterValue)
  {
//--- stats[] array to send data to a frame
   double stats[16];
//--- allocate separate variables for trade statistics to achieve more clarity
   int    trades=(int)TesterStatistics(STAT_TRADES);
   double win_trades_percent=0;
   if(trades>0)
      win_trades_percent=TesterStatistics(STAT_PROFIT_TRADES)*100./trades;
//--- fill in the array with test results
   stats[0]=trades;                                       // number of trades
   stats[1]=win_trades_percent;                           // percentage of profitable trades
   stats[2]=TesterStatistics(STAT_PROFIT);                // net profit
   stats[3]=TesterStatistics(STAT_GROSS_PROFIT);          // gross profit
   stats[4]=TesterStatistics(STAT_GROSS_LOSS);            // gross loss
   stats[5]=TesterStatistics(STAT_SHARPE_RATIO);          // Sharpe Ratio
   stats[6]=TesterStatistics(STAT_PROFIT_FACTOR);         // profit factor
   stats[7]=TesterStatistics(STAT_RECOVERY_FACTOR);       // recovery factor
   stats[8]=TesterStatistics(STAT_EXPECTED_PAYOFF);       // trade mathematical expectation
   stats[9]=OnTesterValue;                                // custom optimization criterion
//--- calculate built-in standard optimization criteria
   double balance=AccountInfoDouble(ACCOUNT_BALANCE);
   double balance_plus_profitfactor=0;
   if(TesterStatistics(STAT_GROSS_LOSS)!=0)
      balance_plus_profitfactor=balance*TesterStatistics(STAT_PROFIT_FACTOR);
   double balance_plus_expectedpayoff=balance*TesterStatistics(STAT_EXPECTED_PAYOFF);
   double balance_plus_dd=balance/TesterStatistics(STAT_EQUITYDD_PERCENT);
   double balance_plus_recoveryfactor=balance*TesterStatistics(STAT_RECOVERY_FACTOR);
   double balance_plus_sharpe=balance*TesterStatistics(STAT_SHARPE_RATIO);
//--- add the values of built-in optimization criteria
   stats[10]=balance;                                     // Balance
   stats[11]=balance_plus_profitfactor;                   // Balance+ProfitFactor
   stats[12]=balance_plus_expectedpayoff;                 // Balance+ExpectedPayoff
   stats[13]=balance_plus_dd;                             // Balance+EquityDrawdown
   stats[14]=balance_plus_recoveryfactor;                 // Balance+RecoveryFactor
   stats[15]=balance_plus_sharpe;                         // Balance+Sharpe
//--- create a data frame and send it to the terminal
   if(!FrameAdd(MQLInfoString(MQL_PROGRAM_NAME)+"_stats", STATS_FRAME, trades, stats))
      Print("Frame add error: ", GetLastError());
   else
      Print("Frame added, Ok");
  }

クラスの2番目の重要なメソッドはOnTesterDeinit()です。 最適化後、取得されたすべてのフレームを読み取り、統計情報をデータベースに保存します。

//+------------------------------------------------------------------+
//| TesterDeinit function - read data from frames                    |
//+------------------------------------------------------------------+
void               CDatabaseFrames::OnTesterDeinit(void)
  {
//--- take the EA name and optimization end time
   string filename=MQLInfoString(MQL_PROGRAM_NAME)+" "+TimeToString(TimeCurrent())+".sqlite";
   StringReplace(filename, ":", "."); // ":" character is not allowed in file names
//--- open/create the database in the common terminal folder
   int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON);
   if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      return;
     }
   else
      Print("DB: ", filename, " opened successful");
//--- create the PASSES table
   if(!DatabaseExecute(db, "CREATE TABLE PASSES("
                       "PASS               INT PRIMARY KEY NOT NULL,"
                       "TRADES             INT,"
                       "WIN_TRADES         INT,"
                       "PROFIT             REAL,"
                       "GROSS_PROFIT       REAL,"
                       "GROSS_LOSS         REAL,"
                       "SHARPE_RATIO       REAL,"
                       "PROFIT_FACTOR      REAL,"
                       "RECOVERY_FACTOR    REAL,"
                       "EXPECTED_PAYOFF    REAL,"
                       "ON_TESTER          REAL,"
                       "BL_BALANCE         REAL,"
                       "BL_PROFITFACTOR    REAL,"
                       "BL_EXPECTEDPAYOFF  REAL,"
                       "BL_DD              REAL,"
                       "BL_RECOVERYFACTOR  REAL,"
                       "BL_SHARPE          REAL );"))
     {
      Print("DB: ", filename, " create table failed with code ", GetLastError());
      DatabaseClose(db);
      return;
     }
//--- variables for reading frames
   string        name;
   ulong         pass;
   long          id;
   double        value;
   double        stats[];
//--- move the frame pointer to the beginning
   FrameFirst();
   FrameFilter("", STATS_FRAME); // select frames with trading statistics for further work
//--- variables to get statistics from the frame
   int trades;
   double win_trades_percent;
   double profit, gross_profit, gross_loss;
   double sharpe_ratio, profit_factor, recovery_factor, expected_payoff;
   double ontester_value;                              // custom optimization criterion
   double balance;                                     // Balance
   double balance_plus_profitfactor;                   // Balance+ProfitFactor
   double balance_plus_expectedpayoff;                 // Balance+ExpectedPayoff
   double balance_plus_dd;                             // Balance+EquityDrawdown
   double balance_plus_recoveryfactor;                 // Balance+RecoveryFactor
   double balance_plus_sharpe;                         // Balance+Sharpe
//--- block the database for the period of bulk transactions
   DatabaseTransactionBegin(db);
//--- go through frames and read data from them
   bool failed=false;
   while(FrameNext(pass, name, id, value, stats))
     {
      Print("Got pass #", pass);
      trades=(int)stats[0];
      win_trades_percent=stats[1];
      profit=stats[2];
      gross_profit=stats[3];
      gross_loss=stats[4];
      sharpe_ratio=stats[5];
      profit_factor=stats[6];
      recovery_factor=stats[7];
      expected_payoff=stats[8];
      stats[9];
      balance=stats[10];
      balance_plus_profitfactor=stats[11];
      balance_plus_expectedpayoff=stats[12];
      balance_plus_dd=stats[13];
      balance_plus_recoveryfactor=stats[14];
      balance_plus_sharpe=stats[15];
      PrintFormat("VALUES (%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%G,%.2f,%.2f,%2.f,%.2f,%.2f,%.2f,%.2f)",
                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                  balance_plus_sharpe);
      //--- write data to the table
      string request=StringFormat("INSERT INTO PASSES (PASS,TRADES,WIN_TRADES, PROFIT,GROSS_PROFIT,GROSS_LOSS,"
                                  "SHARPE_RATIO,PROFIT_FACTOR,RECOVERY_FACTOR,EXPECTED_PAYOFF,ON_TESTER,"
                                  "BL_BALANCE,BL_PROFITFACTOR,BL_EXPECTEDPAYOFF,BL_DD,BL_RECOVERYFACTOR,BL_SHARPE) "
                                  "VALUES (%d, %d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %G, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f)",
                                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                                  balance_plus_sharpe);

      //--- execute a query to add a pass to the PASSES table
      if(!DatabaseExecute(db, request))
        {
         PrintFormat("Failed to insert pass %d with code %d", pass, GetLastError());
         failed=true;
         break;
        }
     }
//--- if an error occurred during a transaction, inform of that and complete the work
   if(failed)
     {
      Print("Transaction failed, error code=", GetLastError());
      DatabaseTransactionRollback(db);
      DatabaseClose(db);
      return;
     }
   else
     {
      DatabaseTransactionCommit(db);
      Print("Transaction done successful");
     }
//--- close the database
   if(db!=INVALID_HANDLE)
     {
      Print("Close database with handle=", db);
      DatabaseClose(db);
     }

MACD サンプルEAに、データベースフレーム.mqh ファイルを含め、CDatabaseFrames クラス変数を宣言します。

#define MACD_MAGIC 1234502
//---
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\AccountInfo.mqh>
#include "DatabaseFrames.mqh"
...
CDatabaseFrames DB_Frames;

次に、最適化時にのみ呼び出されるように、EAの最後に3つの関数を追加します。

//+------------------------------------------------------------------+
//| TesterInit function                                              |
//+------------------------------------------------------------------+
int OnTesterInit()
  {
   return(DB_Frames.OnTesterInit());
  }
//+------------------------------------------------------------------+
//| TesterDeinit function                                            |
//+------------------------------------------------------------------+
void OnTesterDeinit()
  {
   DB_Frames.OnTesterDeinit();
  }
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
   double ret=0;
   //--- create a custom optimization criterion as the ratio of a net profit to a relative balance drawdown
   if(TesterStatistics(STAT_BALANCE_DDREL_PERCENT)!=0)
      ret=TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DDREL_PERCENT);
   DB_Frames.OnTester(ret);
   return(ret);
  }
//+------------------------------------------------------------------+


最適化を起動し、共通のターミナルフォルダでトレード統計を持つデータベースファイルを取得します。

CDatabaseFrames::OnTesterInit: optimization launched at 15:53:27
DB: MACD サンプル データベース 2020.01.20 15.53.sqlite が正常に開かれました
トランザクションの完了
Close database with handle=65537
ファイル'MACD Sample Database 2020.01.20 15.53.sqliteに格納されているデータベース


新しく作成されたデータベースファイルは、メタエディタで開いたり、別のMQL5アプリケーションで使用してタスクを進めることができます。

メタエディターでのデータベースの操作

したがって、他のトレーダーとのさらなる分析や交換に必要な形式のデータを準備することができます。 以下に添付されている MACD.zip アーカイブ内のソースコード、最適化パラメータを含む ini ファイル、および実行結果を見つけます。


インデックスを使用したクエリ実行の最適化

SQL の最もよい特徴は(SQLite だけでなく、すべての実装において)、手続き型言語ではなく宣言型言語であるということです。 SQL でプログラミングする場合、計算方法ではなく、何を計算するかをシステムに伝えます。 'how(方法)' を見つけ出すタスクは、SQL データベース エンジン内のクエリ プランナー サブシステムに委任されます。

任意の SQL ステートメントについて、操作を実行するアルゴリズムが数百または数千に分かれる場合があります。 これらのアルゴリズムはすべて正解を出しますが、一部のアルゴリズムは他のアルゴリズムよりも高速に実行されます。 クエリプランナーは、各 SQL ステートメントの最も高速で最も効率的なアルゴリズムを選択します。

ほとんどの場合、SQLite のクエリ プランナーは優れた役割を果たします。 ただし、クエリ プランナーは、最善を尽くすためにインデックスを必要とします。 これらのインデックスは、通常、プログラマが追加する必要があります。 場合によっては、クエリ プランナーは最適なアルゴリズムの選択を行います。 そのような場合、プログラマは、クエリプランナーがより良い仕事をするのに役立つ追加のヒントを提供したいと思うかもしれません。

インデックスを使用しない検索

指定した 14 個のフィールドを含む DEALS テーブルがあるとします。 このテーブルの最初の10インプットです。

rowid
ID ORDER_ID POSITION_ID TIME TYPE ENTRY SYMBOL VOLUME PRICE PROFIT SWAP COMMISSION MAGIC REASON
1 34429573 0 0 1567723199 2 0 0 0 2000 0 0 0 0
2 34432127 51447238 51447238 1567749603 0 0 USDCAD 0.1 1.3232 0 0 -0.16 500 3
3 34432128 51447239 51447239 1567749603 1 0 USDCHF 0.1 0.98697 0 0 -0.16 500 3
4 34432450 51447565 51447565 1567753200 0 0 EURUSD 0.1 1.10348 0 0 -0.18 400 3
5 34432456 51447571 51447571 1567753200 1 0 AUDUSD 0.1 0.68203 0 0 -0.11 400 3
6 34432879 51448053 51448053 1567756800 1 0 USDCHF 0.1 0.98701 0 0 -0.16 600 3
7 34432888 51448064 51448064 1567756800 0 0 USDJPY 0.1 106.962 0 0 -0.16 600 3
8 34435147 51450470 51450470 1567765800 1 0 EURUSD 0.1 1.10399 0 0 -0.18 100 3
9 34435152 51450476 51450476 1567765800 0 0 GBPUSD 0.1 1.23038 0 0 -0.2 100 3
10 34435154 51450479 51450479 1567765800 1 0 EURJPY 0.1 118.12 0 0 -0.18 200 3

これは取引履歴の分析に必要なディールプロパティセクション(DEAL_TIME_MSC、DEAL_COMMENT、DEAL_EXTERNAL_IDを除く)のデータを提供します。 格納されているデータとは別に、各テーブルにはrowid整数キーと続くインプットフィールドが常に含まれています。 rowidキー値は自動的に作成され、テーブル内で一意です。 新しいインプットを追加すると、値が増加します。 各エントリーを削除すると番号のギャップが生じる可能性がありますが、テーブルの行は常にrowidの昇順で格納されます。

ID=51447571など、特定のポジションに関連するトレードを見つける必要がある場合は、次のクエリを記述する必要があります。

SELECT * FROM deals WHERE position_id=51447571

この場合、テーブル全体のスキャンが実行され、すべての行が表示され、各行の値 51447571 と等しいかどうかPOSITION_IDが検査されます。 この条件を満たす行は、クエリ実行結果に表示されます。 テーブルに数百万または数千万のレコードが含まれている場合、検索に時間がかかる場合があります。 position_id=51447571 ではなく rowid=5 条件で検索を行うと、検索時間は数千回または数百万回短縮されます (テーブルサイズによって異なります)。

SELECT * FROM deals WHERE rowid=5

rowid=5 の行が position_id=51447571 を格納するので、クエリ実行結果は同じになります。 高速化はrowid値が昇順でソートされ、結果を得るためにバイナリ検索が使用するという事実に起因して達成されます。 残念ながらrowidによる検索は、必要なposition_id値を持つインプットのため、適していません。

インデックスによる検索

クエリの実行を効率化するには、次のクエリを使用してPOSITION_IDフィールド インデックスを追加する必要があります。

 CREATE INDEX Idx1 ON deals(position_id)

この場合、2 つの列を持つ別のテーブルが生成されます。 最初の列は昇順で並べ替えられたPOSITION_ID値で構成され、2 番目の列はrowidで構成されます。

POSITION_ID rowid
0 1
51447238 2
51447239 3
51447565 4
51447571 5
51448053 6
51448064 7
51450470 8
51450476 9
51450479 10

この例では、時間によってポジションを開くときと同様にPOSITION_IDが増加するため、rowidシーケンスは既に違反している可能性があります。

POSITION_IDフィールドインデックスが作成できました

SELECT * FROM deals WHERE position_id=51447571

は異なる方法で実行されます。 まず、idx1 インデックスのバイナリ検索がPOSITION_ID列によって実行され、条件に一致するすべてのrowidが見つかります。 元の DEALS テーブルの 2 番目のバイナリ検索では、既知のrowid値ですべてのインプットを詳しく見ることができます。 したがって、大きなテーブルの 1 回のフル スキャンは、2 つの連続したルックアップに置き換えられます。 これより、テーブル内の多数の行の場合に、このようなクエリの実行時間を数千回以上短縮できます。

General rule: テーブルフィールドの一部が検索/比較/ソートに頻繁に使用する場合は、フィールドによるインデックスを作成することをお勧めします。

トレードテーブルには、SYMBOL, MAGIC (EA ID)、およびENTRY (entry direction)フィールドもあります。 フィールドでサンプルを取得する必要がある場合は、適切なインデックスを作成するのが妥当です。 例えば:

CREATE INDEX Idx2 ON deals(symbol)
CREATE INDEX Idx3 ON deals(magic)
CREATE INDEX Idx4 ON deals(entry)

インデックスを作成するには追加のメモリが必要であり、インプットの追加/削除ごとにインデックスの再作成が必要です。 複数のフィールドに基づいて複数のインデックスを作成することもできます。 たとえば、USDCADでMAGIC= 500を持つEAによって実行されたすべてのトレードを選択したい場合、次のクエリを作成することができます。

SELECT * FROM deals WHERE magic=500 AND symbol='USDCAD'

この場合、MAGIC および SYMBOL フィールドによって複数インデックスを作成できます。

CREATE INDEX Idx5 ON deals(magic, symbol)

次のインデックス テーブルが作成されます (最初の 10 行はスキームに従って示されています)

MAGIC SYMBOL rowid
100 EURUSD 4
100 EURUSD 10
100 EURUSD 20
100 GBPUSD 5
100 GBPUSD 11
200 EURJPY 6
200 EURJPY 12
200 EURJPY 22
200 GBPJPY 7
200 GBPJPY 13

新しく作成されたマルチインデックスでは、インプットはまず MAGIC によってブロックでソートされ、次に SYMBOL フィールドによってソートされます。 したがって、AND クエリの場合、インデックス内の検索は最初に MAGIC カラムによって実行されます。 [シンボル] 列の値は、後でチェックされます。 両方の条件が満たされている場合、rowidは、元のテーブル検索で使用する結果セットに追加されます。 一般的に、このようなマルチインデックスは、SYMBOLが最初にチェックされるクエリには適していません

SELECT * FROM deals WHERE  symbol='USDCAD' AND magic=500 

クエリプランナーは正しく動作する方法を理解し、このような場合は正しいオーダーで検索を実行しますが、テーブルとクエリのデザインでエラーが常に自動的に修正されることを期待するのは賢明ではなくなります。

ORクエリ

複数のインデックスは AND クエリにのみ適します。 たとえば、MAGIC=100またはEURUSDでEAによって実行されたすべてのトレードを検索するとします。

SELECT * FROM deals WHERE magic=100 OR symbol='EURUSD'

この場合、2 つの個別のルックアップが実装されます。 見つかったすべての行 id は、ソース テーブルの行番号による最終的な検索の共通の選択に結合されます。

SELECT * FROM deals WHERE magic=100 
SELECT * FROM deals WHERE symbol='EURUSD'

しかし、この場合でも、OR クエリの両方のフィールドにインデックスが必要であり、それ以外の場合は検索によってテーブル全体がスキャンされます。

ソート

並べ替えの速度を上げるには、クエリ結果の配置に使用するフィールドのインデックスを用意することもお勧めします。 たとえば、EURUSD のすべてのトレードをトレード時間でソートして選択する必要があるとします。

SELECT * FROM deals symbol='EURUSD' ORDER BY time

この場合、TIME フィールドによってインデックスを作成することを検討してください。 インデックスの必要性はテーブルのサイズによって異なります。 テーブルにインプットが少ない場合、インデックスを作成しても時間を節約することはほとんどありません。

ここでは、クエリ最適化の基本のみを調べました。 理解を深めるには、SQLite開発者のウェブサイトのQuery Planningセクションから学習することをおすすめします。


データベース処理のメタエディタへの統合

MetaTrader5プラットフォームは絶えず更新されています。 SQL クエリのネイティブサポートを MQL5 言語に追加し、データベースの作成、データの挿入と削除、一括トランザクションの実行など、データベースを処理するための新関数を メタエディタ に統合しました。 データベースの作成は標準であり、MQL5ウィザードがあります。 ファイル名とテーブル名を指定し、そのタイプを示す必要なフィールドをすべて追加するだけです。

MQL ウィザードでのデータベースの作成

次に、テーブルにデータをインプットしたり、検索と選択を実行したり、SQL クエリを導入したりできます。 したがって、MQL5プログラムからだけでなく、手動でデータベースを操作することもできます。 サードパーティのブラウザは必要ありません。

メタトレーダーでのSQLiteの導入は、プログラムと手動の両方で大量のデータを処理するという点でトレーダーの新しい機会を開きます。 これらの機能がかなり便利で、スピードの面で他のソリューションと対等であることを証明するために最善を尽くしました。 ぜひ SQL クエリの言語を学習し、適用してみてください。