请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3352802 新帖:2

SQLite: MQL5 原生 SQL 数据库操纵

SCSDV_d发表于:2 月 24 日 18:38回复(1)

内容

  • MetaTrader 5 中的现代算法交易
  • 操纵数据库的函数
  • 简单查询
  • 在 MetaEditor 中调试 SQL 查询
  • 利用 DatabaseReadBind() 把查询结果自动读至结构中
  • 将业务包装到 DatabaseTransactionBegin() / DatabaseTransactionCommit() 中来加速 
  • 处理交易的历史成交
  • 按策略分析组合
  • 按品种分析成交
  • 按入场时间分析成交
  • 在 DatabasePrint() 中将数据便捷地输出到 EA 的日志  
  • 数据导入/导出
  • 将优化结果保存到数据库
  • 利用索引优化查询执行
  • 将数据库处理集成到 MetaEditor 之中


MetaTrader 5 中的现代算法交易

MQL5 是算法交易的完美解决方案,因为它在语法和计算速度上都尽可能接近 C++。 MetaTrader 5 平台为用户提供了现代的专业语言,用于开发交易机器人和自定义指标,从而令他们不仅可以完成简单的交易任务,而且可以创建任意复杂度的分析系统。

除了异步交易函数和数学函数库,交易者还可以访问网络函数,将数据导入Python,在 OpenCL 中进行并行计算,原生支持 .NET 函数库“智能”导入,与 MS Visual Studio 集成,并可利用 DirectX 完成数据可视化。 如今,这些现代算法交易兵器库中不可或缺的工具能够令用户在不脱离 MetaTrader 5 交易平台的情况下解决各种任务。


操纵数据库的函数

交易策略的研发与大数据处理相关联。 以可靠且快速的 MQL5 程序形式开发的交易算法已不再胜任所有情况。 为了获得可靠的结果,交易者还需要针对各种交易工具进行大量的测试和优化,保存并处理结果,进行分析并决定下一步何处去。

现在,您可以直接在 MQL5 中运用简单且流行的 SQLite 引擎来操纵数据库。 开发者网站上的测试结果展示出 SQL 查询的执行速度很高。 In most tasks, it outperformed PostgreSQL and MySQL. 反过来,我们比较了这些测试在 MQL5 和 LLVM 9.0.0 上的执行速度,并如表所示。 给出的执行结果以毫秒为单位 — 越少越好。

名称
说明
 LLVM  
MQL5
Test 1
 1000 插入
11572
8488
Test 2
 25000 在一次业务里插入
59
60
Test 3
 25000 在已索引数据表里插入
102
105
Test 4
 100 无索引表检索
142
150
Test 5
 100 字符串比较检索
391
390
Test 6
 创建索引
43
33
Test 7
 5000 索引检索
385
307
Test 8
 1000 无索引更新
58
54
Test 9
 25000 索引更新
161
165
Test 10
 25000 索引更新文本
124
120
Test 11  从检索插入
84
84
Test 12
 无索引删除
25
74
Test 13
 索引删除
70
72
Test 14  删除大数据后再插入大数据
62
66
Test 15  众多小插入后随之大数据删除
33
33
Test 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 数据表仅包含五个字段:entry ID, name, age, address 和 salary。 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 数据表中。 字段顺序和数值会被插入到每条记录的指定字段中。 每条记录是由分离的 “INSERT....” 查询组合成的单一查询完成插入。 换言之,我们可以调用单独的 DatabaseExecute() 将每条记录逐个插入数据表中。

脚本操作完成后,数据库会保存到 company.sqlite 文件中,故我们尝试在下一次启动脚本时将具有相同 ID 的相同数据写入到 COMPANY 数据表中。 结果将导致出错。 这就是为什么我们首先删除该数据表,以便每次启动脚本时从草创开始操作的原因。

现在我们从 COMPANY 数据表中获取字段 SALARY > 15000 的所有记录。这是利用 DatabasePrepare() 函数完成的,该函数编译查询文本,并返回其控柄以供随后在 DatabaseRead() 或 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”,表示“没有更多记录”。

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

比较执行结果:

一些统计:
Total salary=125000.0
Average salary=31250.0



利用 DatabaseReadBind() 把查询结果自动读至结构中

DatabaseRead() 函数允许遍历所有查询结果记录,并获取结果数据表中每一列的完整数据:

  • DatabaseColumnName — 名称,
  • DatabaseColumnType — 数据类型,
  • DatabaseColumnSize — 数据大小(以字节为单位),
  • DatabaseColumnText — 读取文本,
  • DatabaseColumnInteger — 获取 int 类型数值,
  • DatabaseColumnLong — 获取 long 类型数值,
  • DatabaseColumnDouble — 获取 double 类型数值,
  • DatabaseColumnBlob — 获取数据数组.

这些函数能够以统一的方式操纵任何查询结果。 然而,这种益处被过多的代码所抵消。 如果查询结果的结构是事先已知的,则最好利用 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 查询的强大之处在于您无需编写代码即可轻松地排序、选择和修改源数据。 我们继续分析 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 操作的示例结果:

结果:
   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 查询令我们能够按 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 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");

结果:

按魔幻数字统计交易
    [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 个被证明是能够盈利的。 我们已收到每种策略的统计值:

  • trades — 按策略的交易数量,
  • gross_profit — 按策略的总利润(所有profit 正数值的总和),
  • gross_loss — 按策略的总亏损(所有profit 负数值的总和),
  • total_commission — 按策略的所有交易佣金总和,
  • total_swap — 按策略的所有交易掉期利率之和,
  • total_profit — gross_profit  gross_loss 之和,
  • net_profit — (gross_profit  + gross_loss + total_commission + total_swap) 之和,
  • win_trades — profit>0 的交易数量,
  • loss_trades — profit<0 的交易数量,
  • expected_payoff — 排除掉期利率和佣金的交易预期收益 = net_profit/trades,
  • win_percent — 获胜交易的百分比,
  • loss_percent — 亏损交易的百分比,
  • average_profit — 平均胜率 = gross_profit/win_trades,
  • 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");

结果:

品种统计交易
      [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");

结果:

按入场时间统计交易
     [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 Sample 的 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");
  }

该类的第二个重要方法是 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 Sample EA 里,包含 DatabaseFrames.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 的末尾添加三个仅在优化过程中调用的函数:

//+------------------------------------------------------------------+
//| 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 值。 满足此条件的数据行将显示在查询执行结果中。 如果数据表包含数百万或数千万的记录,则搜索可能需要花费很长时间。 如果我们按 rowid=5 条件而不是 position_id=51447571 进行搜索,则搜索时间将减少数千甚至数百万倍(取决于数据表的大小)。

SELECT * FROM deals WHERE rowid=5

由于 rowid=5 的数据行存储的 position_id=51447571,因此查询执行结果将相同。 事实上,rowid 值以升序排序,并且是采用二进制搜索来获取结果,因此可以实现加速。 不幸的是,按 rowid 进行的搜索不适合我们,因为我们只是对含有 position_id 值的记录感兴趣。

按索引查找

为了令查询执行更有时效,我们需要利用以下查询添加 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

执行方式有所不同。 首先,按 POSITION_ID 列对 Idx1 索引进行二进制搜索,然后找到所有与条件匹配的 rowids。 第二次二进制搜索则按照已知的 rowid值在原始 DEALS 数据表中查找所有记录。 因此,大规模数据表的单次完整扫描现在由两个连续的查找所替代 — 首先按索引,然后按数据表的行号。 如果数据表中有大量数据行,这可将此类查询的执行时间减少数千或更多倍。

一般规则:如果某些数据表字段经常用于搜索/比较/排序,则建议按这些字段创建索引。

DEALS 数据表还具有 SYMBOL,MAGIC(EA ID)和 ENTRY(入场方向)字段。 如果您需要在这些字段中取样,那么创建相应的索引很合理。 例如:

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

请记住,创建索引需要额外的内存,并且每条记录的添加/删除都需要重新进行索引。 您还可以基于多个字段创建多重索引。 例如,如果我们要选取 EA 执行的 MAGIC=500 且针对 USDCAD 的所有交易,则可以创建以下查询:

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(逻辑与)查询。 例如,假设我们要查找 EA 执行的 MAGIC=100 或针对 EURUSD 的所有成交:

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

在这种情况下,将实施两个单独的查找。 然后,将所有找到的行 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 开发者网站上的查询规划板块开始研究该主题。


将数据库处理集成到 MetaEditor 之中

MetaTrader 5 平台正在不断发展。 我们已将原生 SQL 查询支持添加到 MQL5 语言里,并将处理数据库的新功能集成到 MetaEditor 当中,包括创建数据库,插入和删除数据,以及执行批量业务。 创建数据库是标准的,并且涉及 MQL5 向导。 只需指定文件和数据表名,然后添加指示类型的所有必要字段即可。

在 MQL 向导中创建数据库

接下来,您可以在数据表中填充数据,执行搜索和选取,引入 SQL 查询,等等。 因此,您不仅可以利用 MQL5 程序来处理数据库,还可以手动操纵。 不需要第三方浏览器。

在 MetaTrader 中引入 SQLite,为交易者提供了编程/手动方式处理大数据的新机会。 我们已尽力确保这些函数最加方便使用,且在速度方面与其他解决方案处于同等水平。 恭请您在工作中学习并应用 SQL 查询语言。

全部回复

0/140

量化课程

    移动端课程