内容
- 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 查询
如果代码不成功,则所有操纵数据库的函数都将返回错误代码。 如果您遵循以下四个简单规则,操纵它们不会引发任何问题:
- 调用 DatabaseFinalize() 之后,应销毁所有查询控柄;
- 完毕前应利用 DatabaseClose() 关闭数据库;
- 应检查查询执行的结果;
- 如果发生错误,首先销毁查询,随后关闭数据库。
最困难的是,如果尚未创建查询,如何知晓错误是什么。 MetaEditor 允许打开 *.sqlite 文件,并利用 SQL 查询操纵。 我们以 company.sqlite 文件为例来看看如何完成此操作:
1. 打开终端公用文件夹中的 company.sqlite 文件。
2. 打开数据库后,我们可以在导航器中看到 COMPANY 数据表。 在其上双击。
3. 在状态栏中自动创建 “SELECT * FROM COMPANY” 查询。
4. 查询会自动执行。 也可以通过按 F9 或单击“执行”来执行。
5. 查看查询执行结果。
6. 如果出现问题,错误会显示在编辑器的日志当中。
SQL 查询允许获取数据表字段的统计信息,例如,总和和平均值。 我们进行查询,并检查它们是否运作。
现在我们可以在 MQL5 代码中实现这些查询:
Print("Some statistics:"); //--- prepare a new query about the sum of salaries request=DatabasePrepare(db, "SELECT SUM(SALARY) FROM COMPANY"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); return; } while(DatabaseRead(request)) { double total_salary; DatabaseColumnDouble(request, 0, total_salary); Print("Total salary=", total_salary); } //--- remove the query after use DatabaseFinalize(request); //--- prepare a new query about the average salary request=DatabasePrepare(db, "SELECT AVG(SALARY) FROM COMPANY"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); ResetLastError(); DatabaseClose(db); return; } while(DatabaseRead(request)) { double aver_salary; DatabaseColumnDouble(request, 0, aver_salary); Print("Average salary=", aver_salary); } //--- remove the query after use DatabaseFinalize(request);
比较执行结果:
一些统计: Total salary=125000.0 Average salary=31250.0
利用 DatabaseReadBind() 把查询结果自动读至结构中
DatabaseRead() 函数允许遍历所有查询结果记录,并获取结果数据表中每一列的完整数据:
- 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 应用程序中利用它进行进一步的工作。
因此,您可以按照必要格式准备任何数据,以便进一步分析,或与其他交易者交换。 在下面随附的 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 向导。 只需指定文件和数据表名,然后添加指示类型的所有必要字段即可。
接下来,您可以在数据表中填充数据,执行搜索和选取,引入 SQL 查询,等等。 因此,您不仅可以利用 MQL5 程序来处理数据库,还可以手动操纵。 不需要第三方浏览器。
在 MetaTrader 中引入 SQLite,为交易者提供了编程/手动方式处理大数据的新机会。 我们已尽力确保这些函数最加方便使用,且在速度方面与其他解决方案处于同等水平。 恭请您在工作中学习并应用 SQL 查询语言。