English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
preview
SQLite: MQL5로 SQL 데이터베이스의 처리

SQLite: MQL5로 SQL 데이터베이스의 처리

MetaTrader 5테스터 | 10 10월 2022, 14:27
354 0
MetaQuotes
MetaQuotes

콘텐츠


MetaTrader 5의 모던 알고리즘 거래

MQL5는 알고리즘 거래를 위한 완벽한 솔루션입니다 왜냐하면 구문과 계산 속도의 측면에서 C++에 가깝기 때문입니다. MetaTrader 5 플랫폼은 거래 로봇 및 맞춤형 지표를 개발하기 위한 최신의 특수 언어를 사용자에게 제공함으로써 단순한 거래 작업을 넘어 모든 복잡한 분석 시스템을 생성할 수 있도록 합니다.

비동기 거래 기능 및 수학 라이브러리 외에도 트레이더는 Python으로 데이터 가져오기와 OpenCL에서 병렬 컴퓨팅 그리고 기본 네트워크 기능에 액세스와 "스마트" 기능 가져오기와 NET 라이브러리에 대한 네이티브 지원이 가능하고, MS Visual Studio와의 통합과 DirectX를 사용한 데이터 시각화가 가능합니다. 현대의 알고리즘 거래에서 이러한 필수 도구는 현재 사용자가 MetaTrader 5 거래 플랫폼을 떠나지 않고 다양한 작업을 해결할 수 있도록 합니다.


데이터베이스 작업을 위한 함수

트레이딩 전략을 개발하는 일은 많은 양의 데이터를 처리하는 일과 관련이 있습니다. 안정적이고 빠른 MQL5 프로그램 형태의 거래 알고리즘으로는 충분하지 않습니다. 신뢰할 수 있는 결과를 얻으려면 트레이더는 다양한 거래 도구에 대해 수많은 테스트와 최적화를 수행하고 결과를 저장하고 처리하고 분석하고 그 다음 단계를 결정해야 합니다.

이제 MQL5에서 간단하고 인기 있는SQLite엔진을 사용하여 데이터베이스로 작업할 수 있습니다. 개발자 웹사이트의 테스트 결과에 따르면SQL 쿼리 실행 속도가 상당히 빠르다는 것을 알 수 있습니다. 대부분의 작업에서 PostgreSQL 및 MySQL을 능가했습니다. MQL5 및LLVM 9.0.0에서 이러한 테스트의 실행 속도를 비교하고 표에 표시했습니다. 실행 결과는 밀리초 단위로 제공됩니다. - 작을수록 좋습니다.

이름
설명
 LLVM  
MQL5
테스트 1
 1000 INSERTs
11572
8488
테스트 2
 하나의 트랜잭션에서 25000개의 INSERT
59
60
테스트 3
 인덱싱된 테이블에 25000개의 INSERT
102
105
테스트 4
 인덱스가 없는 100개의 SELECT
142
150
테스트 5
 문자열 비교에서 100개의 SELECT
391
390
테스트 6
 인덱스 생성
43
33
테스트 7
 인덱스가 있는 5000개의 SELECT
385
307
테스트 8
 인덱스가 없는 1000개의 업데이트
58
54
테스트 9
 인덱스가 있는 25000개의 업데이트
161
165
테스트 10
 인덱스가 있는 25000개의 텍스트 업데이트
124
120
테스트 11  SELECT에서 INSERT
84
84
테스트 12
 인덱스 없는 DELETE
25
74
테스트 13
 인덱스가 있는 DELETE
70
72
테스트 14  큰 DELETE 후 큰 INSERT
62
66
테스트 15  많은 작은 INSERT가 뒤따르는 큰 DELETE
33
33
테스트 16  드롭 테이블: 완료
42
40

첨부된 SqLiteTest.zip 파일에서 테스트의 세부 정보를 찾을 수 있습니다. 측정이 수행된 컴퓨터 사양 — Windows 10 x64, Intel 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;
     }

보시다시피 COMPANY 테이블에 4개의 항목이 추가됩니다. 필드의 순서와 이 필드에 삽입할 값은 각 항목별로 지정됩니다. 각 항목은 단일 쿼리로 결합된 별도의 "INSERT...." 쿼리에 의해 삽입됩니다. 즉, 별도의 DatabaseExecute() 호출을 통해 각 항목을 테이블에 삽입할 수 있습니다.

스크립트 작업이 완료되면 데이터베이스가company.sqlite파일에 저장되므로 다음 스크립트 실행 시 동일한 ID를 가진 COMPANY 테이블에 동일한 데이터를 쓰려고 할 것입니다. 그러면 오류가 발생합니다. 그래서 스크립트가 실행될 때마다 처음부터 작업을 시작할 수 있도록 하기 위해 앞에서 테이블을 삭제한 것입니다

이제 SALARY 필드 > 15000인 COMPANY 테이블에서 모든 항목을 가져오겠습니다. 이것은 쿼리 텍스트를 컴파일하고DatabaseRead() 또는 DatabaseReadBind()에서 계속적인 사용을 위해 핸들을 반환하는DatabasePrepare() 함수를 사용하여 수행됩니다.

//--- 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'를 반환하는데 이는 "더 이상 항목이 없음"을 의미합니다.

//--- 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 파일에서 전체 샘플 코드를 얻을 수 있습니다.

MetaEditor에서 SQL 쿼리 디버깅

데이터베이스 작업을 위한 모든 함수는 코드가 실패한 경우 오류 코드를 반환합니다. 네 가지 간단한 규칙을 따르면 함수를 사용하는 데 문제가 발생하지 않습니다.

  1. 모든 쿼리 핸들은 DatabaseFinalize()에서 사용한 후에 삭제되어야 합니다.
  2. 데이터베이스는 완료 전에 DatabaseClose()로 닫혀야 합니다.
  3. 쿼리 실행 결과를 확인해야 합니다.
  4. 오류가 발생하면 쿼리가 먼저 소멸되고 데이터베이스는 나중에 닫힙니다.

가장 어려운 것은 쿼리가 생성되지 않은 경우에 오류가 무엇인지를 알아보는 것입니다. MetaEditor를 사용하면 *.sqlite 파일을 열고 SQL 쿼리를 사용하여 작업할 수 있습니다. 예를 들어 company.sqlite 파일을 사용하여 이 작업을 수행하는 방법에 대해 알아보겠습니다.

1. 공통 터미널 폴더에서 company.sqlite 파일을 엽니다.

2. 데이터베이스를 열면 내비게이터에서 COMPANY 테이블을 볼 수 있습니다. 그것을 두 번 클릭하십시오.

3. 상태 표시줄에 "SELECT * FROM COMPANY" 쿼리가 자동으로 생성됩니다.

4. 쿼리가 자동으로 실행됩니다. F9 키를 누르거나 실행을 클릭하여 실행할 수도 있습니다.

5. 쿼리 실행 결과를 참조하십시오.

6. 뭔가 잘못되면 저널에 오류가 표시됩니다.


SQL 쿼리를 사용하면 합계 및 평균과 같은 테이블 필드에 대한 통계를 얻을 수 있습니다. 쿼리를 만들고 작동하는지 확인합시다.

작업하기MetaEditor에서 쿼리로 작업하기

이제 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);

실행 결과를 비교합니다:

Some statistics:
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예에서 볼 수 있듯이 트랜잭션을 통해 대량 테이블 작업을 수백 번 가속화할 수 있습니다.

Result:
   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 쿼리의 장점은 코드를 작성하지 않고도 소스 데이터를 쉽게 정렬하거나 선택 및 수정할 수 있다는 사실에 있습니다. 단일 쿼리를 통해 딜에서 거래를 얻는 방법을 보여주는DatabasePrepare함수를 설명한 예제를 계속 분석해 보겠습니다. 거래는 포지션 진입/종료 날짜 및 가격, 심볼, 방향 및 거래량 정보에 대한 데이터를 제공합니다. 거래 구조를 보면 진입/청산 거래가 공통 위치 ID로 연결되어 있음을 알 수 있습니다. 따라서 헤징 계정에 간단한 거래 시스템이 있다면 두 거래를 단일 거래로 쉽게 결합할 수 있습니다. 이는 다음 쿼리를 사용하여 수행됩니다.

//--- 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의 예제 작업 결과:

Result:
   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
   Filling the TRADES table took 12.51 milliseconds

헤징 계정에서 이 스크립트를 실행하고 그 결과를 히스토리상에 있는 포지션과 비교하십시오. 이전에는 이러한 결과를 얻기 위해 필요한 루프를 코딩할 지식이나 시간이 충분하지 않았을지도 모릅니다. 이제 단일 SQL 쿼리로 이를 수행할 수 있습니다. MetaEditor에서 스크립트 작업 결과를 볼 수 있습니다. 그렇게 하려면 첨부된 trades.sqlite 파일을 엽니다.


전략별 포트폴리오 분석

위에 표시된DatabasePrepare스크립트 작업의 결과는 거래가 여러 통화 쌍에서 수행된다는 것을 분명히 합니다. 또한 [magic] 열은 100에서 600까지의 값을 보여줍니다. 즉 거래 계정은 거래를 식별하기 위해 고유한 매직 넘버가 있는 여러 전략으로 관리됩니다.

SQL 쿼리를 사용하면매직값의 컨텍스트에서 거래를 분석할 수 있습니다:

//--- 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 as average_profit,"
                           "   r.gross_loss/r.loss_trades as average_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가지가 수익성이 있는 것으로 판명되었습니다. 또한 각 전략에 대한 통계 값을 얻었습니다.

  • 거래 — 전략별 거래 수,
  • Gross_profit — 전략별 총 이익(모든 양수이익값의 합계),
  • gross_loss — 전략별 총 손실(모든 음수이익값의 합계),
  • total_commission — 전략 거래별 모든 수수료의 합계,
  • total_swap — 전략 거래별 모든 스왑 합계,
  • total_profit — 총 이익 및총 손실합계,
  • net_profit — 합계( 총수익 +총손실 +total_commission +total_swap),
  • win_trades —이익>0인 거래 수,
  • loss_trades —이익<0인 거래 수,
  • expected_payoff — 스왑 및 커미션을 제외한 거래에 대한 예상 수익 =net_profit/trades,
  • win_percent — 거래에서 승리한 비율,
  • loss_percent — 손실 거래 비율,
  • average_profit — average win = gross_profit/win_trades,
  • average_loss — average loss = gross_loss /loss_trades,
  • profit_factor — 이익 계수 = gross_profit/gross_loss.

손익 계산을 위한 통계는 스왑 및 커미션을 고려하지 않습니다. 이를 통해 순 비용을 볼 수 있습니다. 전략이 약간의 이익을 가져오지만 일반적으로 스왑 및 수수료로 인해 수익성이 없는 것으로 판명될 수 있습니다.


심볼별 거래 분석

우리는 심볼별 거래를 분석할 수 있습니다. 이렇게 하려면 다음 쿼리를 수행합니다.

//--- 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 as average_profit,"
                               "   r.gross_loss/r.loss_trades as average_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
      [name] [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] "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 as average_profit,"
                           "   r.gross_loss/r.loss_trades as average_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()함수와 관련한 예제에서 이러한 세 가지 쿼리 유형이 포함된 전체 소스 코드를 찾을수 있습니다.


DatabasePrint()에서 EA 로그로 데이터 출력

이전 예에서는 쿼리 결과를 표시하기 위해 구조의 모든 항목을 읽고 항목을 하나씩 표시해야 했습니다. 테이블이나 쿼리 결과 값만 보기 위해 구조를 만드는 것은 종종 불편할 수 있습니다. 그러한 경우를 방지하기 위해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 쿼리는 데이터베이스의 상태를 변경할 수 없습니다. 그렇지 않으면 DatabaseExport()가 오류와 함께 실패합니다.

MQL5 문서에서 함수와 관련한 전체 설명을 참조하십시오.


데이터베이스에 최적화 결과 저장

데이터베이스 작업을 위한 함수는 최적화 결과를 처리하는 데에도 사용할 수 있습니다. 기본으로 제공하는MACD 샘플 EA를 사용하는 예에서 프레임을 사용하여 테스트 결과를 얻고 모든 최적화 기준의 값을 단일 파일에 저장하는 방법을 설명하겠습니다. 이렇게 하려면 거래 통계를 보내기 위해 OnTester() 메서드에서 정의한 CDatabaseFrames 클래스를 만듭니다.

//+------------------------------------------------------------------+
//| 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");
  }

클래스의 두 번째 중요한 메서드는 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에서 DatabaseFrames.mqh 파일을 include하고 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의 끝에 세 가지의 함수를 추가합니다:

//+------------------------------------------------------------------+
//| 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 Sample Database 2020.01.20 15.53.sqlite opened successful
Transaction done successful
Close database with handle=65537
Database stored in file 'MACD Sample Database 2020.01.20 15.53.sqlite'

새로 생성된 데이터베이스 파일은 MetaEditor에서 열수도 있고 추가 작업을 위해 다른 MQL5 애플리케이션에서 사용할 수도 있습니다.

작업하기MetaEditor에서 데이터베이스 작업하기

따라서 추가적인 분석을 위해서 혹은 다른 트레이더와 주고 받기 위해 필요한 형식으로 모든 데이터를 준비할 수 있습니다. 아래 첨부된 MACD.zip 아카이브에서 소스 코드, 최적화 매개변수가 포함된 ini 파일 및 실행 결과를 찾으십시오.


인덱스를 사용하여 쿼리 실행 최적화

SQL(SQLite 뿐만 아니라 모든 구현에서)의 가장 좋은 기능은 절차적 언어가 아니라 선언적 언어라는 것입니다. SQL로 프로그래밍할 때는 계산 방법이 아니라 무엇을 계산할지를 시스템에 알려야 합니다. '방법'을 파악하는 작업은 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

이 경우 전체 테이블 스캔이 수행됩니다. 모든 행이 표시되고 POSITION_ID가 각 행에서 51447571 값과 같은지 확인합니다. 이 조건을 만족하는 행이 쿼리 실행 결과에 표시됩니다. 테이블에 수백만 또는 수천만 개의 레코드가 포함된 경우 검색 시간이 오래 걸릴 수 있습니다. position_id=51447571이 아닌 rowid=5 조건으로 검색하면 검색 시간이 수천 배 또는 수백만 배(테이블 크기에 따라 다름) 줄어듭니다.

SELECT * FROM deals WHERE rowid=5

rowid=5인 행이 position_id=51447571을 저장하므로 쿼리 실행 결과는 동일합니다. rowid 값이 오름차순으로 정렬되고 결과를 얻기 위해 이진 검색이 사용되기 때문에 가속이 달성됩니다. 우리는 필요한 position_id 값을 갖는 항목에 관심이 있습니다. 그러므로 rowid로 검색하는 것은 적합하지 않습니다.

인덱스로 조회

쿼리 실행 시간을 보다 효율적으로 만들려면 다음 쿼리를 사용하여 POSITION_ID 필드 인덱스를 추가해야 합니다.

 CREATE INDEX Idx1 ON deals(position_id)

이 경우 두 개의 열이 있는 별도의 테이블이 생성됩니다. 첫 번째 열은 오름차순으로 정렬된 POSITION_ID 값으로 구성되고 두 번째 열은rowid로 구성됩니다.

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

rowid 시퀀스는 이미 위반되었을 수 있습니다. 이 예에서는 보존되지만 시간별로 포지션 진입을 하면 POSITION_ID도 함께 증가하기 때문입니다.

이제 POSITION_ID 필드 인덱스가 있으므로 쿼리는

SELECT * FROM deals WHERE position_id=51447571

다르게 수행됩니다. 먼저 Idx1 인덱스에서 POSITION_ID 열로 이진 검색을 수행하고 조건과 일치하는 모든rowid를 찾습니다. 원래 DEALS 테이블의 두 번째 이진 검색은 rowid값으로 모든 항목을 찾습니다. 따라서 큰 테이블을 한벙에 전체 스캔하는 과정은 이제 두 개의 연속적인 조회로 대체됩니다. - 첫 번째는 인덱스, 그 다음은 테이블 행 번호입니다. 이를 통해 테이블의 행 수가 많은 경우에도 쿼리의 실행 시간을 수천 배 이상 줄일 수 있습니다.

일반 룰: 일부 테이블 필드가 검색/비교/정렬에 자주 사용되는 경우 이러한 필드로 인덱스를 생성하는 것이 좋습니다.

DEALS 테이블에는 SYMBOL, MAGIC(EA ID) 및 ENTRY(진입 방향) 필드도 있습니다. 이 필드에서 샘플을 가져와야 하는 경우 적절한 인덱스를 만드는 것이 좋습니다. 예를 들어:

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 열에 의해 먼저 수행됩니다. 이후에 SYMBOL 열의 값을 확인합니다. 두 조건이 모두 충족되면 결과 집합에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'

이 경우 두 개의 개별적인 조회가 구현됩니다. 그런 다음 발견된 모든 rowid는 원본 테이블의 행 번호로 최종 검색을 하기 위한 공통 선택으로 결합됩니다.

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

그러나 이 경우에도 OR 쿼리의 두 필드 모두에 인덱스가 있어야 합니다. 그렇지 않으면 검색 시 전체 테이블을 스캔하는 형상이 발생하게 됩니다.

정렬

정렬 속도를 높이려면 쿼리 결과를 정렬하는 데 사용되는 필드별로 색인을 만드는 것이 좋습니다. 예를 들어 거래 시간별로 정렬된 EURUSD의 모든 거래를 선택해야 한다고 가정합니다:

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

이 경우 TIME 필드별로 인덱스를 생성하는 것을 고려해야 합니다. 인덱스의 필요성은 테이블 크기에 따라 다릅니다. 테이블에 항목이 거의 없으면 인덱싱으로 시간을 절약하는 것은 미미합니다.

여기에서는 쿼리 최적화의 아주 기본적인 것만 살펴보았습니다. 더 나은 이해를 위해 SQLite 개발자 웹 사이트의 쿼리 계획섹션부터 시작하여 주제를 공부하는 것이 좋습니다.


MetaEditor에 데이터베이스 처리 통합

MetaTrader 5 플랫폼은 지속적으로 개발됩니다. SQL 쿼리에 대한 네이티브 지원을 MQL5 언어에 추가하고 데이터베이스 생성, 데이터 삽입 및 삭제, 대량 트랜잭션 수행등 데이터베이스 처리를 위한 새로운 기능을 MetaEditor에 통합했습니다. 데이터베이스를 생성하는 것은 표준으로 MQL5 마법사를 통해 가능합니다. 파일 및 테이블 이름을 지정하고 유형을 나타내는 모든 필수 필드를 추가하기만 하면 됩니다.

에서 데이터베이스 생성MQL 마법사 데이터베이스 생성

다음으로 테이블을 데이터로 채우고 검색 및 선택을 수행하고 SQL 쿼리 등을 수행할 수 있습니다. 따라서 MQL5 프로그램으로 수행하거나 수동으로도 데이터베이스 작업을 수행할 수 있습니다. 이를 위해 별도의 브라우저가 필요하지 않습니다.

MetaTrader에 SQLite를 도입하면 프로그래밍 방식과 수동 방식 두가지 방식 모두에서 대량의 데이터를 처리하는 것이 필요한 거래자에게 새로운 기회가 열립니다. 우리는 이러한 기능이 가장 사용하기 편리하고 속도 면에서 다른 솔루션과 동등한 위치에 있도록 최선을 다했습니다. 여러분의 작업에서 SQL 쿼리의 언어를 연구하고 적용해 보시기 바랍니다.

MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/7463

파일 첨부됨 |
SqLiteTest.zip (2708.45 KB)
trades.sqlite (340 KB)
MACD.zip (8.27 KB)
DatabaseRead.mq5 (10.11 KB)
DatabasePrepare.mq5 (35.02 KB)
DatabaseExecute.mq5 (64.83 KB)
시각화! R 언어의 'plot'과 유사한 MQL5 그래픽 라이브러리 시각화! R 언어의 'plot'과 유사한 MQL5 그래픽 라이브러리
트레이딩의 로직을 연구할 때 그래프의 형태로 표시되는 시각적 표현은 매우 중요합니다. 과학 관련 커뮤니티에서 널리 사용되는 여러 프로그래밍 언어(예: R 및 Python)에는 시각화에 사용되는 특수한 '플롯' 함수가 있습니다. 이 함수들이 선, 점 분포 및 히스토그램을 그려서 패턴을 시각화 할 수 있습니다. MQL5에서는 CGraphics 클래스를 사용하여 동일한 작업을 수행할 수 있습니다.
Expert Advisor 개발 기초부터 (파트 9): 개념적 도약(II) Expert Advisor 개발 기초부터 (파트 9): 개념적 도약(II)
이 기사에서는 차트 트레이드를 플로팅 창에 배치합니다. 이전 파트에서는 플로팅 창 내에서 템플릿을 사용할 수 있는 기본 시스템을 만들었습니다.
MetaTrader 5에서 DirectX를 사용하여 3D 그래픽을 만드는 방법 MetaTrader 5에서 DirectX를 사용하여 3D 그래픽을 만드는 방법
3D 그래픽은 숨겨진 패턴을 시각화 할 수 있습니다. 그러므로 방대한 양의 데이터를 분석하는 데 탁월합니다 이러한 작업은 MQL5에서 직접 해결할 수 있는데 DireсtX 함수를 사용하면 3차원 객체를 만들 수 있습니다. 따라서 MetaTrader 5용 3D 게임과 같은 복잡한 프로그램을 만드는 것도 가능합니다. 간단한 3차원 도형을 그리는 것으로 3D 그래픽을 배워보세요.
파라볼릭 SAR 기반의 트레이딩 시스템을 설계하는 방법 알아보기 파라볼릭 SAR 기반의 트레이딩 시스템을 설계하는 방법 알아보기
이 기사에서는 가장 인기 있는 지표를 사용하여 거래 시스템을 설계하는 방법에 대한 시리즈를 계속할 것입니다. 이 기사에서는 파라볼릭 SAR 지표에 대해 자세히 알아보고 몇 가지 간단한 전략을 사용하여 MetaTrader 5에서 사용할 거래 시스템을 설계하는 방법을 배웁니다.