English Русский Español Português
preview
市场模拟:MQL5 中的 SQL 入门(五)

市场模拟:MQL5 中的 SQL 入门(五)

MetaTrader 5 — 测试者 |
152 0
Daniel Jose
Daniel Jose

引言

大家好,欢迎阅读本系列关于如何创建复制/模拟系统的又一篇文章。

在上一篇文章 “市场模拟:MQL5 中的 SQL 入门(四)” 中,我们介绍了一个在 MetaTrader 5 中使用 SQLite 时应当注意的小细节。尽管这一行为在未来可能会有所改变,但作为预防措施,您在 MetaTrader 5 环境中使用 SQLite 之前,应始终先进行测试。总之,我们在文中展示了如何添加查询机制,以便在 MQL5 代码中充分利用 SQL,并使用 SQL SELECT 查询来检索结果。

但是我们还需要讨论和实现最后一个函数,即 DatabaseReadBind 函数。由于要正确理解这个函数需要稍作详细解释,因此决定不在上一篇文章中介绍,而是在今天的文章中详述。这个话题内容较多,下面我们直接进入下一部分。


实现读取查询结果的函数

实现负责读取 SQL 查询返回数据的函数相当简单,但有一个细节绝不能忘记。读取函数返回的结果正是上一次 ExecRequestOfData 调用执行的结果。因此,在读取返回的数据之前,必须始终先调用 ExecRequestOfData。

不过,这里我们需要先做一个简短的插述。为了更好地理解这一点,我们首先需要查看一下类代码。完整的 C_DB_SQL 类代码如下所示。请记住,此代码是为使用 MetaTrader 5 内置的 SQL 支持而设计的。对于其他 SQL 使用场景,此代码需要进行修改以添加所需的功能。

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

C_DB_SQL.mqh 代码

这就是完整的类代码。但由于只有一小部分尚未讨论,或者更准确地说,之前尚未展示过,因此我们不会检查其他文章中已经涉及的函数或要点。在此,我们将仅关注第 143 行的函数,即 GetRegisterOfRequest 函数。第一个也是最合理的问题是:为什么这个函数是以这种方式声明的?仅使用 MQL5 语言很难解释这一点。

为了详细理解这一点,我们需要解释一个存在于 C/C++ 中的非常复杂的概念。这个概念与泛型对象引用有关,在 C/C++ 中,它与通常与空指针一起解释的机制有关。是的,在 C/C++ 中,我们可以使用泛型引用和类似指针的机制,而不仅仅是许多人想象的具体类型。但是,由于在一篇文章中解释这个概念极其困难,亲爱的读者,我们将尽可能避免讨论 C/C++,而是试图向你展示为什么这个函数是以这种方式声明的。

首先,我们要面对的是类型依赖的问题。这个问题并非出现在第 143 行的函数声明中,而是出现在 DatabaseReadBind 函数中。这种依赖性迫使我们使用特定的数据类型,以避免在尝试编译代码时,MQL5 编译器报出一连串错误。许多程序员在打算使用 DatabaseReadBind 或尝试使用时,并没有像我们在这里一样将其封装在一个调用中。原因正是他们不了解此处产生的类型依赖。

那么,他们通常是怎么做的呢?他们会创建一个程序或函数来执行查询,分析 SQL 返回的结果,最后以常规方式将值返回给调用者。这种方法在非常特定的场景下是有效的,并且相当适用。在其他情况下,程序员只需在需要时直接在主代码中使用 DatabaseReadBind。

但是他们为何采取这种方式呢?再次强调,原因在于类型约束。在 MQL5中,处理起来并不那么容易。然而,当使用 C/C++时,情况会有所改变,因为 C/C++ 提供了处理通用对象引用的低级机制,包括空指针。我想你可能还没有完全理解这一点。那么,让我们来看看在 MQL5 中是如何声明 DatabaseReadBind 函数的。您可以在下面看到:

bool  DatabaseReadBind( 
   int    request,           // handle of the query created in DatabasePrepare 
   void&  struct_object      // reference to the structure for reading the record 
   );

我们在 MQL5 函数文档中可以找到相关内容。您可能对此不太清楚,但对于 C/C++ 程序员来说,这种声明是完全合理的。在不深入 C/C++ 细节的情况下,其基本思想如下:该函数必须接受两个参数。第一个参数是一个整数值,表示 DatabasePrepare 返回的句柄。我们在上一篇文章中已经讨论过这一点。然而,这里的关键角色是第二个参数。第二个参数的类型为 void,这意味着该函数接受一个对象的引用,而无需在签名中明确指定具体类型。

这里传递的并不是通常所说的“地址”本身,而是对内存中对象的引用。这是因为指针通常指向内存中的某个地址。然而,当我们在一个名称前使用与号(&)时,我们表示我们将引用一个变量,而不是一个位置。如果你真的想理解这一点,可以尝试学习如何在 C/C++ 中使用指针,因为这一主题在专门教授 C/C++ 的书籍中往往占据数章篇幅。这是一个相当复杂且容易让人困惑的主题。

由于 MQL5 不允许我们像 C/C++ 那样使用指针,我们需要一个小技巧,才能像 C_DB_SQL 类代码中那样正确封装并调用 DatabaseReadBind 函数。这个技巧正是使用模板类型机制,让编译器推断出适当的类型。乍一看,这可能看起来非常复杂且难以解决。幸运的是,MQL5 使我们能够使用 C/C++ 中也存在的东西,即声明:

template <typename T >

这里有一个注意事项。T 可以是任何类型,但在编程中我们通常按照约定使用 T。关键在于,这个声明放在函数名之前,告诉编译器必须执行正确的类型转换来解决类型问题。现在请注意以下几点:GetRegisterOfRequest 的第一个参数正是 T。由于我们不能使用 void 类型,这个声明实际上让我们能够泛化参数类型,而不是像低级 C/C++ 模式那样依赖于 void。

“好吧。但我仍然无法理解这种逻辑。在研究了 MQL5 文档中关于 DatabaseReadBind 的部分后,我们发现它指出第二个参数必须是一个结构。即使在演示此函数用法的示例中,第二个参数也是使用一个结构。因此,我无法理解为什么使用 template <typename T > 会如此复杂,以及第 143 行这个奇怪的声明。为什么不直接将结构体传递给这个函数呢?为什么要搞得这么复杂?”

亲爱的读者,我完全同意您的观点。你是对的。我们可以直接将结构体作为函数参数传递,这样就能大大简化第 143 行的声明。但现在我要问你们:这个结构应该有多少个字段,这些字段应该是什么类型?如果你能以一种允许逻辑普遍化的方式来回答这个问题,我肯定会以不同的方式实现第 143 行的调用。

但是,这种结构永远无法以一种能够泛化所有情况的方式构建。这是因为,即使我们知道数据库中字段的数量,这个数量迟早也会发生变化。如果发生这种情况,MQL5 代码中的整个实现都必须重新设计。因此,许多人在使用 DatabaseReadBind 时,采用的方式与此处所示有所不同。

我们展示的这个函数是一个通用函数,其目的是将SQL返回的结果返回给我们的 MQL5 代码。你很快就会明白为什么了。总之,在第 145 行,我们将进行调用,根据返回值,我们将面临两种完全不同的情况。第一种情况是 DatabaseReadBind成功 读取了 SQL 返回的一些记录。在这种情况下,将执行第 155 行。但也可能出现这种情况:这个块中已经没有剩余的记录了。那么我们将面临两种不同的情况。然而,这两种情况都是在 DatabaseReadBind 返回 false 时触发的分支中处理的。

请注意,默认情况下,GetRegisterOfRequest 函数的第二个参数的值为 true。当第 145 行的检查失败时,我们会在第 147 行检查第二个参数是否为 true。如果调用者未对其进行更改,则在第 149 行调用 DatabaseFinalize 函数。最后,在第 150 行,我们会更改 m_Request 变量的值,表示没有更多可用记录,需要向数据库发起新的查询。但是,如果调用者指定 Finish 参数为 false,则第 147 行的检查将失败,一切将保持原样。这在后续的主代码中会有用。无论如何,返回值都将是 false,这会在第 152 行进行处理。

至此,我们已完全完成了 C_DB_SQL 类的主要部分。从现在起,任何代码都可以在我们的回放和模拟系统中使用它。但在使用之前,我们需要确保一切正常工作。为此,让我们继续讨论一个新的话题,以便我们能更清晰地划分问题。


在 MetaTrader 5 中测试用于 SQLite 的 C_DB_SQL 类

我们的 C_DB_SQL 类大部分已经过测试。现在剩下的就是测试与查询相关的部分,并弄清楚执行查询时 SQL 将返回什么。

为了准确解释这一点,同时让每个人都能轻松理解,尤其是亲爱的读者你,我们将展示一段主代码,演示得到的结果,并解释发生了什么以及我们为什么会得到这样的结果。在文章的最后,我们将提供完整的代码,以便您可以在本地测试我们将要执行的操作。

处理完这部分后,接下来我们先做第一个测试。我们将使用以下代码片段来实现这一点:

18. //+------------------------------------------------------------------+
19. void SELECT_Type_01(void)
20. {
21.     struct st
22.     {
23.         int id;
24.         string symbol;
25.     }stLocal;
26. 
27.     Print("Executing type 1 data request...");
28.     if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols"))
29.     {
30.         Print("Request: OK...");
31.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++)
32.             Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol);
33.         Print("Reload...");
34.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++)
35.             Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol);
36.     };
37.     Print("Finish type 1...");
38. }
39. //+------------------------------------------------------------------+

主文件中的片段

行号与完整代码中的行号完全一致。这意味着您可以按照自己的节奏逐段阅读。该代码片段的执行结果可以在以下动画中看到:

请注意,这非常简单明了。第 21 行我们声明了一个结构体。请记住,在这个结构中我们将有两个变量。你不应该这样看待它们。不要把它们当作变量,因为那样会让你感到困惑,并使你以后更难理解其他内容。它们应该被视为 SQL 返回的响应中的字段或列。因此,我们有一个结构体,在 SQL 结果集中应该被视为一个行。这个结果行中预期有两个字段。

在第 28 行,我们尝试使用 SQL 从数据库中查询数据。请密切注意双引号(")内的内容,因为这是 SQL 将执行的命令。你必须准确写出你想传递给 SQL 执行的内容。在这个最简单的例子中,我们只需要 tb_Symbols 表中所有的值。虽然这种创建查询的方式不适合实际使用,但对于我们的例子来说完全足够,因为我们的表很小,包含的记录很少。

如果我们的查询成功,我们将首先执行第 31 行的 for 循环。现在请注意,循环结束的原因正是没有新的返回记录。还要注意,在第 31 行的这些调用中,我们告诉 GetRegisterOfRequest 我们还不想关闭或完成当前的结果块。换句话说,当没有更多可用记录时,我们将保持当前结果集的开放和可用状态。

然后,在第 32 行,我们将返回的结果打印到终端。在第 33 行,我们在终端中指示我们将再次遍历返回的数据。请注意,我们所做的几乎与第 31 行相同。然而,这次我们不会在 GetRegisterOfRequest 调用的第二个参数中传递任何内容。因此,一旦块被完全读取,SQL 查询返回的所有内容都将被丢弃,因此我们必须执行一个新的查询。

接下来的实现方法见以下片段:

39. //+------------------------------------------------------------------+
40. void SELECT_Type_02(void)
41. {
42.     struct st
43.     {
44.         string of_day;
45.         double price;
46.         string symbol;
47.     }stLocal;
48.     
49.     Print("Executing type 2 data request...");
50.     if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;"))
51.     {
52.         Print("Request: OK...");
53.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++)
54.             Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol);
55.     }
56.     Print("Finish type 2...");
57. }
58. //+------------------------------------------------------------------+

主代码片段

结果可以在以下动画中看到:

请注意,这里的情况与前一个示例略有不同。首先,在第 42 行,我们声明一个具有三个字段的结构。在前一种情况下,每一行都有两个字段。但是请注意,尽管字段数量增加了,但我们仍然使用与以前非常相似的代码。但这一次,我希望你们仔细查看第 50 行使用的 SQL 命令。在这里,我们将 tb_Quotes 表和 tb_Symbols 表连接起来,以便以某种方式呈现数据库中实际存在的内容。

现在请注意 SQL 查询中声明字段的顺序。这一点很重要,因为 DatabaseReadBind 在读取 SQL 响应数据时也会遵循相同的顺序。因此,如果我们更改 SQL 命令或第 42 行结构中的字段声明的顺序,我们可能会在返回的数据中看到完全意想不到的内容。所以不要忽略这些细节。

现在我们遇到一个乍一看似乎更复杂的情况。但不要被第一印象所误导:它其实和其他事情一样简单。我们将更详细地使用 MQL5 中提供的函数,这些函数的目的是允许在 MQL5 中使用 SQL。下面可以看到片段。

58. //+------------------------------------------------------------------+
59. void SELECT_Type_03(void)
60. {
61.     struct st0
62.     {
63.         double price;
64.         string symbol;
65.     }stLocal;
66.     struct st1
67.     {
68.         string tmp1,
69.                tmp2,
70.                date;
71.     }std;
72.     
73.     Print("Executing type 3 data request...");
74.     if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;"))
75.     {
76.         Print("Request: OK...");
77.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++)
78.             Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol);
79.         Print("Reload...");
80.         for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++)
81.             Print(" Resq ", c, " ==> < ", std.date, " > ");
82.         
83.     }
84.     Print("Finish type 3...");
85. }
86. //+------------------------------------------------------------------+

主代码片段

下面可以看到这段代码片段的执行结果。

到目前为止,我们使用的结构对应于一个包含两到三个字段的结果行。但正如我们在上一主题中所解释的,我们还可以做得更多,即让 SQL 返回超出我们所需数量的字段。而且,通过正确分析我们实际需要的字段,我们可以利用 SQL 已经返回的数据。这样,我们就不必创建新的查询来查找所需数据,因为这些数据已经包含在前一个查询中了。

这里有一个必须理解的重要细节。关键点在于,SQL 返回的每一行都包含对应字段的值。但是,正如本例所示,我们将执行与前一个片段相同的查询。然而,现在查询返回的字段顺序不同。这一点非常重要,请务必注意。原因在于,第 61 行声明的结构包含两个字段。第 66 行声明的结构包含三个字段。SQL 将返回三个字段。这是因为查询命令指示 SQL 返回这三个字段。

现在请注意,我们从结构 st0 切换到了结构 st1。然而,结构 st0 中缺少名为 date 的字段,因此当我们继续读取 SQL 返回的数据时,会忽略这个字段。这一情况出现在第 77 行,我们在那里创建了一个循环来读取 SQL 返回的数据并将其打印到终端。

但是,我们在第 80 行做了同样的事情。但这次我们忽略 SQL 查询返回的其余数据,只显示名为 date 的字段。这里没有什么复杂的问题。关键在于理解我们正在做什么以及 SQL 返回的结果。

你一定已经在想,我们可以为所欲为,而且无论我们采取什么行动,都会得到一个结果。然而,在现实中,这并不完全正确。要明白这一点,请看以下摘录:

086. //+------------------------------------------------------------------+
087. void SELECT_Type_04(void)
088. {
089.     struct st
090.     {
091.         string of_day;
092.         double price;
093.         string symbol;
094.     }stLocal;
095.     
096.     Print("Executing type 4 data request...");
097.     if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;"))
098.     {
099.         Print("Request: OK...");
100.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++)
101.             Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol);
102.     }
103.     Print("Finish type 4...");
104. }
105. //+------------------------------------------------------------------+

主代码片段

下面显示了该片段的执行结果,乍一看似乎有些奇怪,因为我们得到了一个完全出乎意料的结果。

这对你来说可能看起来相当奇怪。但其实这并没有什么不寻常的:只需看看第 89 行的声明,我们就在那里明确指出了我们期望在 SQL 查询中接收到的字段。现在看看第 97 行,以及我们向 SQL 提出的请求。请注意,我们期望接收到的结果与我们要求 SQL 返回的信息之间并不一致。

这里出现了一个问题,迫使我们再次查阅 MQL5 文档,以理解为何结果会如动画所示。因此,当我们查阅 GetRegisterOfRequest 所使用的底层函数 DatabaseReadBind 的文档时,会看到以下高亮显示的片段:

请注意

struct_object 结构中的字段数不能超过 DatabaseColumnsCount() 的值。如果 struct_object 中的字段数小于记录中的字段数,则会执行部分读取。其余数据可以通过相应的 DatabaseColumnText()、DatabaseColumnInteger() 等函数显式获取。

“但是等等,我们并没有使用 DatabaseColumnsCount。因此,这个结果没有意义。”其实,这个结果是有意义的,因为在 SQL 查询中,我们明确指定了要接收两个字段。注意,在第 97 行,我们请求了价格字段和交易品种字段,这两个字段分别来自不同的表。GetRegisterOfRequest 并没有额外的机制去重新对齐查询结果与结构体字段,因此当返回列与结构定义不匹配时,DatabaseReadBind 只会按既有规则尝试绑定。因此,结果与动画中看到的一模一样。

我们可以改进这一点,但目前将保持现状,因为我们还需要考虑另一种情况。从以下片段中可以看出这一点。

105. //+------------------------------------------------------------------+
106. void SELECT_Type_05(void)
107. {
108.     struct st
109.     {
110.         double price;
111.     }stLocal;
112.         
113.     Print("Executing type 5 data request...");
114.     if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';"))
115.     {
116.         Print("Request: OK...");
117.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++)
118.             Print(" Resq ", c, " ==> { ", stLocal.price, " } ");
119.     }
120.     Print("Finish type 5...");
121. }
122. //+------------------------------------------------------------------+

主代码片段

执行结果正是如此:

这是一个使用 SQL 的稍微极端一点的例子,我们让它执行了你可能认为在 SQL 之外应该完成的操作。但在这里,我们只是执行了一个非常具体的查询,从数据库中检索特定的记录。现在请注意第 114 行。它包含了 SQL 必须执行的命令。请注意,我们正在请求 PETR4 工具的报价,特别是针对 2023 年 7 月 11 日的报价。当然,日期遵循了 SQL 所期望的格式。但我想现在你已经了解 SQL 是如何工作的了。

如果我们查看数据库,执行 SQL 查询时,会得到与终端相同的结果。在这种情况下,只需要返回一个字段。注意第 108 行是如何声明的。

但这里可能会出现一个非常合理的问题:我们能否直接使用第 110 行,而不声明第 108 行所示的结构?很遗憾,亲爱的读者,答案是否定的。根据 GetRegisterOfRequest 的声明方式来看,并非如此。关键在于,如果我们尝试仅使用第 110行 代码而不创建结构,那么我们实际上并不会调用通过 DatabaseReadBind 执行的 read 函数。我们会使用其他方法。然而,作为程序员,我们可以编写能够处理这种情况以及前面片段中描述的情况的代码。

总之,您可以直接在 MetaEditor 中测试这些查询,或者更准确地说,测试我们在这里研究的 SQL 命令,以便进一步尝试创建旨在获得类似最后一个片段中结果的查询。但不要忘记在最终将用作 MetaTrader 5 应用程序的代码中测试相同的内容。完整的测试代码如下所示:

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. #property description "Basic script for SQL database written in MQL5"
004. #property version   "1.00"
005. #property script_show_inputs
006. //+------------------------------------------------------------------+
007. #resource "\\Files\\Script 01.sql" as string SQL_Create
008. #resource "\\Files\\Script 02.sql" as string SQL_Insert
009. //+------------------------------------------------------------------+
010. #include <Market Replay\SQL\C_DB_SQL.mqh>
011. //+------------------------------------------------------------------+
012. enum eCall {eType_00, eType_01, eType_02, eType_03, eType_04, eType_05};
013. //+------------------------------------------------------------------+
014. input string user01 = "DataBase01";       //Database File Name
015. input ECall user02 = eType_01;            //Search type
016. //+------------------------------------------------------------------+
017. C_DB_SQL *SQL;
018. //+------------------------------------------------------------------+
019. void SELECT_Type_01(void)
020. {
021.     struct st
022.     {
023.         int id;
024.         string symbol;
025.     }stLocal;
026. 
027.     Print("Executing type 1 data request...");
028.     if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols"))
029.     {
030.         Print("Request: OK...");
031.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++)
032.             Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol);
033.         Print("Reload...");
034.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++)
035.             Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol);
036.     };
037.     Print("Finish type 1...");
038. }
039. //+------------------------------------------------------------------+
040. void SELECT_Type_02(void)
041. {
042.     struct st
043.     {
044.         string of_day;
045.         double price;
046.         string symbol;
047.     }stLocal;
048.     
049.     Print("Executing type 2 data request...");
050.     if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;"))
051.     {
052.         Print("Request: OK...");
053.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++)
054.             Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol);
055.     }
056.     Print("Finish type 2...");
057. }
058. //+------------------------------------------------------------------+
059. void SELECT_Type_03(void)
060. {
061.     struct st0
062.     {
063.         double price;
064.         string symbol;
065.     }stLocal;
066.     struct st1
067.     {
068.         string tmp1,
069.                  tmp2,
070.                  date;
071.     }std;
072.     
073.     Print("Executing type 3 data request...");
074.     if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;"))
075.     {
076.         Print("Request: OK...");
077.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++)
078.             Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol);
079.         Print("Reload...");
080.         for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++)
081.             Print(" Resq ", c, " ==> < ", std.date, " > ");
082.         
083.     }
084.     Print("Finish type 3...");
085. }
086. //+------------------------------------------------------------------+
087. void SELECT_Type_04(void)
088. {
089.     struct st
090.     {
091.         string of_day;
092.         double price;
093.         string symbol;
094.     }stLocal;
095.     
096.     Print("Executing type 4 data request...");
097.     if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;"))
098.     {
099.         Print("Request: OK...");
100.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++)
101.             Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol);
102.     }
103.     Print("Finish type 4...");
104. }
105. //+------------------------------------------------------------------+
106. void SELECT_Type_05(void)
107. {
108.     struct st
109.     {
110.         double price;
111.     }stLocal;
112.         
113.     Print("Executing type 5 data request...");
114.     if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';"))
115.     {
116.         Print("Request: OK...");
117.         for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++)
118.             Print(" Resq ", c, " ==> { ", stLocal.price, " } ");
119.     }
120.     Print("Finish type 5...");
121. }
122. //+------------------------------------------------------------------+
123. const string ExecScripts(void)
124. {
125.     string szMsg = (*SQL).ExecResourceSQL(SQL_Create);
126.     if (szMsg != NULL) return szMsg;
127.     return (*SQL).ExecResourceSQL(SQL_Insert);
128. };
129. //+------------------------------------------------------------------+
130. void OnStart()
131. {
132.     string szMsg = NULL;
133.     
134.     SQL = new C_DB_SQL(user01);
135.     
136.     if (user02 == eType_00) szMsg = ExecScripts();
137.     Print(szMsg == NULL ? "Result of executing the SQL script: Success" : szMsg);
138.     
139.     if (szMsg == NULL) switch (user02)
140.     {
141.         case eType_01: SELECT_Type_01();    break;
142.         case eType_02:    SELECT_Type_02();    break;
143.         case eType_03: SELECT_Type_03(); break;
144.         case eType_04: SELECT_Type_04(); break;
145.         case eType_05: SELECT_Type_05(); break;
146.     }
147. 
148.     delete SQL;
149. }
150. //+------------------------------------------------------------------+

MQL5 主代码

请注意,所有解释都包含在代码中,并且行号相同。与我们之前文章中看到的代码唯一的小区别在于第 139 行,从第 15 行声明的变量开始,我们选择类型,或者更准确地说,选择将执行哪个片段。这样你就能看到每个动画中的相同内容。


最后的想法

在本文中,我们展示了在使用 MetaTrader 5 SQLite 时,如何读取 SQL 命令返回的数据。尽管在阅读本文时您可能已经注意到,我们仍然可以在保持 C_DB_SQL 类专注于处理 MetaTrader 5 内置 SQLite 的同时,对其进行改进。我们可以做的主要改进如下:

  • 即使我们执行的查询包含的列较少,也允许填充 MQL5 结构中定义的字段。这将避免出现 GetRegisterOfRequest 调用返回无信息的不愉快结果。
  • 另一个改进之处在于,如果查询结果是我们感兴趣的单个记录,如 SELECT_Type_05 片段中所示,我们就不必在结构体内声明返回的变量,而是可以直接将其作为常规独立变量来使用。

尽管实施这些改进颇具吸引力,但我还不能确定是否会展示具体操作方法。这是因为,在 MQL5 中编程时,我们可以采取更为谨慎的方法。无论如何,如果决定展示如何进行这些更改以更好地支持片段中所示的案例,那么将在下一篇文章中完成。尽管如此,请务必关注本系列的下一篇文章,因为这个主题每天都在变得更加有趣。

文件 描述
Experts\Expert Advisor.mq5
演示 Chart Trade 与 EA 之间的交互(交互需要 Mouse Study)。
Indicators\Chart Trade.mq5 创建一个窗口,用于配置待发送的委托(需要 Mouse Study 才能进行交互)
Indicators\Market Replay.mq5 创建用于与复制/模拟服务交互的控件(交互需要 Mouse Study)。
Indicators\Mouse Study.mq5 提供图形控件与用户之间的交互(复制/模拟系统和真实市场都需要)。
Services\Market Replay.mq5 创建并维护市场复制/模拟服务(整个系统的主文件)。
Code VS C++\Servidor.cpp 创建并维护一个用 C++ 开发的套接字服务器(迷你聊天版本)。
Code in Python\Server.py 创建并维护用于 MetaTrader 5 和 Excel 之间通信的 Python 套接字服务器。
Indicators\Mini Chat.mq5 允许通过指标实现迷你聊天(需要使用服务器)。
Experts\Mini Chat.mq5 允许使用 EA 实现迷你聊天功能(需要服务器)。
Scripts\SQLite.mq5 演示如何通过 MQL5 使用 SQL 脚本。
Files\Script 01.sql 演示如何创建带有外键的简单表。
Files\Script 02.sql 演示如何向表中插入值。

本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/13117

附加的文件 |
Anexo.zip (571.71 KB)
MQL5自优化智能交易系统(第十三部分):基于矩阵分解浅谈控制理论 MQL5自优化智能交易系统(第十三部分):基于矩阵分解浅谈控制理论
金融市场不可预测,那些在历史回测中表现盈利的交易策略,投入实盘后往往会失效。究其原因:大多数策略部署后逻辑固定,无法自适应市场,也不能从失误中学习。我们借鉴控制理论思想,利用反馈控制器监测策略与市场的交互情况,并调整策略行为,使其趋向盈利。测试结果表明:在简单移动平均策略中加入反馈控制器后,利润提升、风险降低、交易效率提高,证明该方案在交易领域具备巨大潜力。
基于协整股票的统计套利(第三部分):数据库搭建 基于协整股票的统计套利(第三部分):数据库搭建
本文提供一个 MQL5 Service(服务)的实现示例,用于保持新建数据库及时更新;该数据库作为数据分析以及一篮子协整股票交易的数据来源。文中详细阐述数据库的设计思路,并整理数据字典以供查阅。配套提供MQL5与Python脚本,用于数据库创建、表结构初始化以及行情数据写入。
MQL5交易策略自动化(第二十八部分):构建带可视化反馈的价格行为蝙蝠谐波形态 MQL5交易策略自动化(第二十八部分):构建带可视化反馈的价格行为蝙蝠谐波形态
在本文中,我们将在MQL5中开发一套蝙蝠形态交易系统,该系统借助枢轴点与斐波那契比率识别看涨、看跌蝙蝠谐波形态,并在精准的入场、止损与止盈价位触发交易;并结合图表对象实现直观的可视化展示。
神经网络在交易中的应用:概率时间序列预测(K²VAE) 神经网络在交易中的应用:概率时间序列预测(K²VAE)
我们诚邀您探索 K²VAE 框架的原始实现 — 这是一个灵活的模型,能够线性近似潜在空间中的复杂动态。本文将演示如何在 MQL5 中实现关键组件,包括参数化矩阵,以及如何在标准神经网络层之外对这些矩阵进行管理。对于任何寻求构建可解释时间序列模型实用方法的人来说,本文材料都将大有裨益。