市场模拟:MQL5 中的 SQL 入门(四)
概述
欢迎大家阅读本系列关于如何创建回放/模拟系统的又一篇文章。
在上一篇文章“市场模拟:MQL5 中的 SQL 入门(三)”中,我们展示了如何在 MQL5 创建的可执行文件中嵌入 SQL 脚本。然而,我们并没有就此止步。我们还对类代码进行了一些修改,使其使用起来更加简便,当然,也对代码的实现方式进行了小幅调整。
虽然上一篇文章中介绍的类代码对于使用 MetaTrader 5 中的内置 SQLite 创建数据库非常有用(我们稍后会讨论),但该代码不允许我们从 MQL5 执行查询。
因此,本文的主要目标是尽可能清晰地解释如何修改这个类,以便添加执行查询所需的代码。换句话说,我们将在本文中实现一个用于查询 SQLite 数据库并检索结果的系统。
在我们了解如何操作之前,需要先了解使用本系列内容时需要注意的一些重要事项。正是因为存在这些细微差别,我们目前尚未将 SQLite 与回放/模拟系统代码结合使用,因为我们希望尽可能清晰地解释和展示 MetaTrader 5 内置的 SQLite 支持的实际工作原理。
许多人可能认为,MetaTrader 5 内置的 SQLite 与通过 DLL 文件访问的版本工作方式相同。尽管这两种选择本质上工作方式相同,但在 MetaTrader 5 中使用内置 SQLite 版本时存在一些特殊之处。开发人员未来可能会对此进行更改,但任何事情都不应被视为理所当然。如果您有任何疑问,请先进行测试,然后研究如何解决在特定应用程序中使用嵌入式 SQLite 时可能出现的任何问题。我并不是说问题出在任何特定应用程序上。我只是说,你应该先检查所有内容,然后再得出结论,确定所使用的 SQLite 版本是否与你预期的不同。
理解问题
在撰写本文时,该问题在 2023 年 6 月 22 日发布的 5.00 版本 Build 3815 中有所体现。没错,这段代码是很久以前创建的。当你读到这些文字时,我向你保证,MetaTrader 5 已经更新,所有这些内容都将过时。尽管如此,请不要忽视这些文章的内容,因为它们可能会让你了解到一些你甚至从未想过可以做到的事情。
现在让我们继续讨论这个问题。请查看下面的 SQL 脚本。
01. PRAGMA FOREIGN_KEYS = ON; 02. 03. DROP TABLE IF EXISTS tb_Symbols; 04. DROP TABLE IF EXISTS tb_Quotes; 05. 06. CREATE TABLE IF NOT EXISTS tb_Symbols 07. ( 08. id PRIMARY KEY, 09. symbol NOT NULL UNIQUE 10. ); 11. 12. CREATE TABLE IF NOT EXISTS tb_Quotes 13. ( 14. of_day NOT NULL, 15. price NOT NULL, 16. fk_id NOT NULL, 17. FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id) 18. );
SQLite 代码
此 SQL 脚本创建了一组表,其中 tb_Symbols 表和 tb_Quotes 表之间存在关系。这种关系由第 17 行定义的外键来保证。如果您不确定我们在谈论什么,请参阅本系列之前的文章。
现在来看同一段 SQL 脚本的第 3 行和第 4 行。它们会从 SQL 数据库文件中删除表。无论是在 MetaTrader 5 中运行,还是使用 SQLite DLL 库,我们都会得到相同的结果。然而,情况并非总是如此。原因就在于第 17 行,该行建立了 tb_Symbols 和 tb_Quotes 中值之间的关系。
“等等,你说我们不会总是得到相同的结果,这是什么意思?MetaTrader 5 中的 SQLite 不是和通过 DLL 库使用时一样吗?为什么你说运行这个脚本会产生不同的结果?”或许我表达得不够清楚,或者至少没有恰当地解释细节。事实上,执行操作都会导致相同的结果;但是,如果表中存在建立了 tbSymbols 与 tbQuotes 关联关系的数据,MetaTrader 5 内置的 SQLite 就无法删除这些表,并会报错,您可以通过以下测试来验证这一点。
连续运行以下脚本两次。第一次运行将顺利完成,不会出现任何错误。但第二次运行时,我们将看到代码后面所示的图像。
01. PRAGMA FOREIGN_KEYS = ON; 02. 03. DROP TABLE IF EXISTS tb_Symbols; 04. DROP TABLE IF EXISTS tb_Quotes; 05. 06. CREATE TABLE IF NOT EXISTS tb_Symbols 07. ( 08. id PRIMARY KEY, 09. symbol NOT NULL UNIQUE 10. ); 11. 12. CREATE TABLE IF NOT EXISTS tb_Quotes 13. ( 14. of_day NOT NULL, 15. price NOT NULL, 16. fk_id NOT NULL, 17. FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id) 18. ); 19. 20. INSERT INTO tb_Symbols (id, symbol) VALUES (1, 'PETR4'); 21. 22. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('2023-07-10', '22.00', 1);
SQLite 代码

请注意,执行间隔少于三秒时,结果会完全不同。第一次尝试时,SQL 脚本执行成功;但紧接着再次运行同一脚本时,操作却以错误告终。报告的错误代码清楚地表明,问题与表之间的关系有关,而这一关系恰恰是在脚本的第 17 行创建的。
然而,该错误并非由第 17 行直接引起。请注意上图:我们用于执行 SQL 脚本的类的代码报告称,在 SQLite 中发送第 3 行执行时发生了问题。
然而,有一点必须强调。如果我们向 SQLite 发送相同的 SQL 脚本进行独立执行,该脚本将无错误运行。我们在讨论 SQL 编程时已经证明了这一点;如果您有任何疑问,可以查看我之前以可视化方式展示此内容的文章。
有人可能会认为问题出在 MetaTrader 5 内置的 SQLite 上,但我不想挑起争议。或许这个内置的 SQLite 版本包含一个内置的安全机制,可以防止删除具有活动外键的表。我不知道独立 SQLite 和 MetaTrader 5 内置 SQLite 之间这种明显不对称的确切原因。这并不是唯一的差异:你还会注意到一些不一致之处,例如,在尝试使用内置的 SQLite 库创建触发器时。
因此,如果你决定通过动态链接库(DLL)来使用 SQLite,我不会怪你。不过,根据我的测试,内置的 SQLite 可以在不使用 DLL 的情况下解决许多任务。如果行为差异变得过于严重,我们将另寻他法。目前,我们可以继续使用内置的 SQLite 版本。
因此,如果我们在 MetaTrader 5 中使用内置的 SQLite 数据库,并且需要删除数据库表,最简单的方法就是删除数据库文件。当嵌入在可执行文件中的脚本再次运行时,将不会发生冲突。
好的,如果以上内容都清楚了,那么我们进入下一步 — 最有意思的部分现在开始。我建议你尽量集中注意力阅读下面的讲解。
在文章《市场模拟(第 25 部分):MQL5 中的 SQL 入门(一)》中,我们提到,在 MQL5 中,仅使用六个函数就足以处理 SQL。在之前的文章中,我们已经探讨了其中的三个,并且它们已经在 C_DB_SQL 类中实现了。现在是时候学习和理解为什么我们只需要其他三个函数了。为了使解释更加清晰,我们接下来将讨论一个新的话题。
最后三个必需函数
接下来我要解释的内容,绝不应让你因为之前没有理解而感到不足,无论你是已经在 MQL5 中使用数据库,还是刚刚开始接触。我将简单地展示我是如何操作的:这既不是最正确的方法,也不是最不合适的方法,而仅仅是我处理工作的方式。
我说仅需六个 MQL5 函数就足以访问 SQL,这并不是说你使用更多函数或程序是不对的。相反,我钦佩那些深入研究语言或工具的人。我的做法是只使用所需的最基本功能。
我通常不会记住我使用的每种语言或工具的每个函数的每个细节;那会很费时间,而可用的截止日期往往是有限的。我更喜欢按时完成任务,无论任务的复杂程度如何。
为了检验我的方法是否正确,我们将再次对数据库脚本进行修改。我建议采用不同的方式进行操作,以避免采用僵化的技术:创建一个新的脚本,并将其命名为 script 02.sql。如果你愿意,可以将它的内容添加到 script 01.sql 文件的末尾;这样会得到完全相同的结果。
如果你采纳我的建议,唯一的区别就是你需要对主文件进行一些微调。由于并非每个人都能准确知道需要做出哪些改变,我们将详细探讨这一解决方案,并展示如何实施以获得正确结果。
此处的目标是将数据插入数据库,以便后续执行查询。请查看以下 SQL 脚本。
01. PRAGMA FOREIGN_KEYS = ON; 02. 03. CREATE TABLE IF NOT EXISTS tb_Symbols 04. ( 05. id PRIMARY KEY, 06. symbol NOT NULL UNIQUE 07. ); 08. 09. CREATE TABLE IF NOT EXISTS tb_Quotes 10. ( 11. of_day NOT NULL, 12. price NOT NULL, 13. fk_id NOT NULL, 14. FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id) 15. );
SQLite 代码
01. INSERT INTO tb_Symbols (id, symbol) VALUES
02. (2, 'PETR4'),
03. (1, 'ITUB3'),
04. (3, 'VALE3');
05.
06. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES
07. ('2023-07-10', '22.00', 1),
08. ('2023-07-11', '22.20', 1),
09. ('2023-07-12', '22.40', 1),
10. ('2023-07-13', '22.30', 1),
11. ('2023-07-14', '22.60', 1),
12. ('2023-07-10', '26.00', 2),
13. ('2023-07-11', '26.20', 2),
14. ('2023-07-12', '26.40', 2),
15. ('2023-07-13', '26.30', 2),
16. ('2023-07-14', '26.60', 2),
17. ('2023-07-10', '62.00', 3),
18. ('2023-07-11', '62.20', 3),
19. ('2023-07-12', '62.40', 3),
20. ('2023-07-13', '62.30', 3),
21. ('2023-07-14', '62.60', 3); SQLite 代码
虽然有两个文件,但你可以将它们的内容合并,并使用已经显示的主 MQL5 文件运行程序。如果您决定将它们分开,则必须按如下所示修改主 MQL5 代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Basic script for SQL database written in MQL5" 04. #property version "1.00" 05. #property script_show_inputs 06. //+------------------------------------------------------------------+ 07. #resource "\\Files\\Script 01.sql" as string SQL_Create 08. #resource "\\Files\\Script 02.sql" as string SQL_Insert 09. //+------------------------------------------------------------------+ 10. #include <Market Replay\SQL\C_DB_SQL.mqh> 11. //+------------------------------------------------------------------+ 12. input string user01 = "DataBase01"; //Database File Name 13. //+------------------------------------------------------------------+ 14. C_DB_SQL *SQL; 15. //+------------------------------------------------------------------+ 16. const string ExecScripts(void) 17. { 18. string szMsg = (*SQL).ExecResourceSQL(SQL_Create); 19. if (szMsg != NULL) return szMsg; 20. return (*SQL).ExecResourceSQL(SQL_Insert); 21. }; 22. //+------------------------------------------------------------------+ 23. void OnStart() 24. { 25. string szMsg; 26. 27. SQL = new C_DB_SQL(user01); 28. 29. szMsg = ExecScripts(); 30. Print(szMsg == NULL ? "Result of executing the SQL script: Success" : szMsg); 31. 32. delete SQL; 33. } 34. //+------------------------------------------------------------------+
MQL5 的代码
你可以看到,一切都很简单、实用且清晰。第一个 SQL 脚本创建所需的表;第二个脚本通过添加一些值将数据插入数据库。由于这些脚本之前已经出现过,您可能已经知道它们各自的作用以及它们如何相互作用来创建我们将要形成的记录。
好了,现在来看 MQL5 代码。请注意,我们新增了第 8 行,并将现有资源的名称更改为更合适的名称。由于第 29 行的一些特殊情况,我们调用位于第 16 行的函数。该函数的目的是执行两个 SQL 脚本,这两个脚本实际上是 MQL5 中创建的最终可执行文件的内部资源。首先,在第 18 行,我们请求执行创建表的 SQL 脚本。然后,在第 19 行,我们检查结果。如果第一个 SQL 脚本执行成功,我们将在第 20 行运行第二个脚本。
第二个 SQL 脚本的任务是将数据插入数据库。这是一项简单的任务,执行起来并不需要付出太多努力。这样一来,我们就具备了进入下一阶段所需的一切:开发将从 MQL5 代码向 SQL 数据库执行查询的部分。但首先,让我们检查数据库文件的内容;为此,我们可以方便地使用 MetaEditor,如下所示:

可以看到,预期内容确实已添加到数据库中。既然我们已经确认了这一点,那么让我们进入下一步。让我们回到 C_DB_SQL.mqh 头文件。请记住,在原始形式下,该类无法返回查询结果。虽然可以运行 SELECT 命令,正如我们在开始使用 MQL5 代码之前所解释的那样,但如果我们不能直接在 MQL5 本身中使用结果,那么这样做就没什么用处。
MQL5 开发人员提供了多个函数用于获取SQL查询结果。让我们再次强调,接下来我们将讨论的所有内容都假设在 MetaTrader 5 中使用内置的 SQLite。如果通过 DLL 使用 SQLite,则操作流程会有所不同;或许我们稍后会解释具体方法。目前,我们将使用内置的 SQLite 数据库。同样,通过套接字访问数据库需要采用不同的方法。不要混淆它们:每种情况都需要不同的解决方案,即使最终结果看起来可能相同。
考虑到上述情况,我们可以专注于代码本身。我们需要确保查询能够执行,并且其结果在需要时能够被轻松解析。为此,我们需要使用两个新函数。第一个是 DatabasePrepare,用于执行数据库查询。请注意,我们不应该像许多人可能假设或认为的那样使用 DatabaseExecute。原因是 DatabaseExecute 只告诉我们 SQL 命令是否成功执行。它不提供任何方式来检查查询返回的结果集。
这正是我们使用 DatabasePrepare 的原因,因为这个函数使我们能够检查查询返回给 MQL5 应用程序的数据。这是我们需要的第一个函数,也是第一部分。此外,我们还需要另一个函数来读取 SQL 查询返回的数据。但首先,让我们看看为了包含 DatabasePrepare,类代码发生了哪些变化。类代码如下所示。
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. }; 144. //+------------------------------------------------------------------+
C_DB_SQL.mqh 代码
请注意,我们在第 11 行添加了一个新变量。该变量将用于读取 SQL 查询返回的数据。像所有变量一样,它将在第 95 行进行初始化。请注意这里使用的是 INVALID_HANDLE 这个值。现在让我们转到第 137 行,那里实现了 ExecRequestOfData 函数。我们将使用此函数向 SQL 发送查询命令。通常,这类查询会使用 SELECT … FROM 语句,但正如我们稍后将看到的,在这里它可能非常具体。
总之,请注意,在第 139 行,我们检查 m_Request 变量的值是否为 INVALID_HANDLE 以外的值。如果确认了这一点,就意味着缓存中存在之前的查询。换句话说,我们希望在执行新查询之前,先结束之前的预处理语句并释放其资源。为此,我们使用另一个函数:DatabaseFinalize。它的目的是结束之前的预处理语句并释放其资源,因此,它是我们在扩展 C_DB_SQL 类的这个阶段所需的第三个函数。
在完成之前的预处理语句(如果有的话)之后,可以在第 140 行发送新的查询。请注意,无需指定数据库名称。这是在创建类时已经完成的。我们只需要指定要使用的命令,而命令本身将由调用者提供,我们稍后会对此进行介绍。如果查询成功,DatabasePrepare 将返回一个数值。该数据将存储在 m_Request 变量中,以备后用。如果查询失败,则返回 INVALID_HANDLE。结果,ExecRequestOfData 将返回 false,告诉调用者查询执行失败。
好吧,但这里有一个细微之处:为什么不直接从 ExecRequestOfData 函数本身返回数据呢?为什么我们只提供查询成功或失败的反馈?原因很简单。出于某种原因,我们可能希望进行一个返回特定值或表字段的查询。根据特定领域的具体情况,我们可能会选择不同的方向或做出不同的决策。这将迫使我们对数据库执行新的查询。虽然这并不是错误,但这种方法会导致冗余的数据库查询,这在客户端-服务器架构中尤其关键。
所有必需的字段都可以通过一次查询获得。除了优化对数据库的 SQL 查询外,这也有助于我们稍微加快主代码的运行速度。因此,数据读取被分离到一个专门用于此目的的专用函数中。
这听起来可能完全是胡说八道,甚至有些不切实际。但请记住,由于查询执行与读取获取的结果是分离的,因此可以非常轻松地调整主代码以精确读取特定字段。这可能听起来很难解释,因为在执行 SQL 查询时,我们仅使用其中一个表的一列,就可以获得多个返回值。本文前面的动画演示了这一点。
但如果查询语句编写正确且目标定位恰当,我们通常会获得一个结果。尽管也有可能一个结果都得不到。这是因为查询中使用的条件在数据库中找不到。
这正是很少有人真正理解的一个细微差别所在。在展示数据读取功能之前,我想借此机会解释一下这一点,因为这个话题你可能会感兴趣。
许多人往往低估了SQL,甚至根本不使用它,因为他们并不完全了解它的实际工作原理。当我们查询 SQL 数据库时,并不总是寻求一个通用的答案。在某些情况下,我们可能需要一个非常具体且实用的答案。如果数据库的结构和数据模型设计得当,几乎任何类型的信息都可以被整合到其中。这很有意思,因为人们通常认为数据库仅仅是一组数字和字母数字字符,用于存储有关销售、客户、产品等的数据。
但如果你正确理解了 SQL 的工作原理以及如何使用它,你就可以创建一个系统,或者更准确地说,是一个小型机器人,它能够学习如何处理给定的交易品种。而且这一切都不需要创建交易模型。我们只需在数据库中输入一系列报价,并使用 SQL 进行查询,同时结合 MQL5 本身来执行 SQL 无法完成的一些计算,从而创建一个能够在全自动模式下学习进行市场交易的机制。这个机制将自行学习,以预测该交易品种未来可能的走势。
我之前在同一系列关于回放/模拟系统的文章中已经提到过这一点。也许你还没有看到或注意到,这个简单的系统能实现多少功能,我现在正在演示它的创建过程。但是,如果你能正确研究这个主题,你就能创造出我们一直在谈论的机制。当然,为了获得理想的结果,我们需要使用特定的数学方法和编程技巧。这正是我们在查询执行时,不直接将 SQL 的查询结果返回给 MQL5 的原因,如上文代码所示。
事实证明,这个查询的编写方式使得我们能够以一种与许多人预期完全不同的方式来使用结果。既然 MQL5 开发者允许我们使用这种机制,我认为改变它的工作方式是不合理的。而且,由于很难用几句话来准确解释读取过程,我们将在下一篇文章中保留这一解释以及需要对主代码进行的更改。
最后的想法
在本文中,我们讨论了如何完成某些任务。我们提到,无需为机器人设计交易模型,也有可能创建一个能够学习交易技能的机器人。我们探讨了当尝试直接在 MetaTrader 5 内置的 SQLite 中使用某些 SQL 函数时会出现的问题,并展示了如何解决这些问题。然而,尽管如此,我们仍未展示如何使用 MQL5 本身实际获取查询执行结果。原因在于,我们可以创建一个函数,该函数能够自动适应预期的查询结果。这样,你就能理解为什么我们说在使用 SQL 时,只需要 MQL5 允许我们使用的六个函数。
最后还有一个细节:MQL5 已经具备使用 ONNX 模型的机制。在创建能够独立学习市场交易的机器人方面,使用 ONNX 模型比使用 SQL 更为合适。但你完全可以使用基于 SQL 的建模来实现这一目的。关键在于学习和正确应用所获得的知识。
| 文件 | 描述 |
|---|---|
| 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++ 开发的服务器套接字(MiniChat 版本)。 |
| 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/13110
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
新手在交易中的10个基本错误
基于Mamba选择性状态空间模型的神经网络交易机器人开发