
开发回放系统 — 市场模拟(第 26 部分):智能交易系统项目 — C_Terminal 类
概述
在上一篇文章《开发回放系统 — 市场模拟(第 25 部分):为下一阶段做准备》之中,我们准备好了基本用途的回放/模拟系统。然而,我们所需的不仅仅是分析过去走势或潜在动作。意即,我们需要一种工具,能让我们有针对性的研究,就如我们在真正的市场上运作一样。为此目的,我们需要创建一款智能系统,进行更深入的研究。此外,我们有意向开发一款通用智能系统,能适用于各种市场(股票和外汇),并且还与我们的回放/模拟系统兼容。
考虑到项目的规模,这项任务预计非常艰巨。不过,开发的复杂性并不像看起来那么高难度,因为在之前的文章中我们已经涵盖了大部分过程。其中包括:《从头开始开发智能交易系统(第 31 部分):迈向未来(IV)》和《创建自动工作的 EA(第 15 部分):自动化(VII)》。在这些文章中,我详细讲述了如何创建一款完全自动化的智能系统。尽管有了这些素材,于此,我们仍面临着一个独特且更加困难的挑战:模拟 MetaTrader 5 平台与交易服务器的连接,增进逼真的开放市场模拟。这项任务无疑是相当复杂的。
尽管如此,我们不应被最初的复杂性所吓倒。我们需要从某处开始,否则我们最终只会空想一项任务的难度,甚至没有尝试去克服它。这就是编程的全部意义:通过学习、测试和广泛的研究来攻克障碍。在我们开始之前,我想说的是,在概括和解释事物是如何真实诞生时,我得到很多乐趣。我相信许多人已经从这个系列文章中学到了超越往常的基础知识和测试,我们可以达成比某些人想象的还要多的成就。
智能系统实现概念
您也许已经注意到,我是面向对象编程(OOP)的忠实粉丝。这是由于 OOP 提供的丰富能力。它还提供了一种从头开始创建健壮、安全和可靠代码的途径。一开始,我们要得到如何组织项目的结构来完成我们所需的初步思路。作为经验丰富的用户和程序员,我意识到要令智能交易系统真正有效,它所用的资源必须始终对我们可用:键盘和鼠标。鉴于 MetaTrader 5 平台基于图表,故使用鼠标与图形元素进行交互是必不可少的。但键盘在辅助各个方面也扮演着关键角色。不过,讨论超出了鼠标和键盘的用途,故将在自动化系列中涵盖。在某些情况下,无需这些工具即可实现完全自动化,但当选择使用它们时,参考正在执行的操作性质就很重要。因此,并非所有的智能交易系统都适合所有类型的资产。
这是因为某些资产的价格变动为 0.01。其它的也许为 0.5,而有些也许为 5。就外汇而言,这些数值与上述示例的区别很明显。由于数值的多样性,一些程序员选择专门为特定资产开发 EA。原因很清楚:交易服务器不接受随意数值;我们需要遵守服务器设置的规则。同样的原则也应用于回放/模拟系统。我们不允许 EA 执行含有随机数值的订单。
引入这种限制不仅是必要的,而且是极其必要的。如果在实盘账户上交易时系统的行为完全不同,那么据回放/模拟功能进行训练是没有意义的。因此,系统必须维持确定的标准化,并尽可能适配实盘账户的真实情况。因此,无论在何种境遇下,都有必要开发一款 EA,就像它与交易服务器直接交互一样工作。
我们从第一个类开始:C_Terminal 类
虽然经常可在没有特定指导的情况下编写所有代码,但对于潜在变得非常庞大和复杂的项目而言,并不建议用此方式。我们仍然不知道这个项目将会如何开发,但重点是要始终从紧盯最佳编程实践开始。否则,如果缺乏正确的项目规划,我们得到的就是大量混乱的代码。因此,重点是从开始就要有大局观,即使项目摊开后并不那么宏或复杂。实现最佳实践令我们即使在小型项目中也更有条理,并教会我们遵循稳定的方法。我们从开发第一个类开始。为此,我们将创建一个新头文件,名为 C_Terminal.mqh。给文件起名时最好与类同名;这样令我们在需要用到它时更容易找到它。代码的开头如下:
class C_Terminal { protected: private : public : };
在文章《创建自动运行的 EA(第 05 部分):手动触发器(II)》中,我们研究了有关类和保留字的一些基本概念。如果您还不熟悉创建自动化 EA 系列,那么它值得一观,因为它包含许多我们将于此用到的元素。尽管该代码已过时,并完全被淘汰。此处,我们将看到一段代码,它将提供一种新的交互形式,因为我们需要解决某些问题,并令系统更加健壮、可靠和高效。开始编码后,类代码中实际出现的第一件事是结构,下面将详细讲述:
class C_Terminal { protected: //+------------------------------------------------------------------+ struct st_Terminal { long ID; string szSymbol; int Width, Height, nDigits; double PointPerTick, ValuePerPoint, VolumeMinimal, AdjustToTrade; }; //+------------------------------------------------------------------+
应当注意的是,这种结构是在代码的私密部分内声明的,这对我们将要做的事情至关重要。有趣的是,我们在这里没有声明任何变量。事实上,类中的任何全局变量都必须在代码的私密部分中声明,这提供了最高级别的安全性和信息封装。贯穿实现过程,我们将不时回到这个主题,以便更深入地理解。为了获得最佳编程实践,绝不允许从不属于类代码的其它部分访问内部类变量。
现在我们看看类变量是如何声明的:
private :
st_Terminal m_Infos;
目前,我们在 C_Terminal 类中只有一个全局性且私密变量,我们将在其中存储相应的数据。稍后我们将看到如何从类外访问该数据。在这一点上,重点是要记住,未经许任何信息都不应泄露,或进入类内部。遵循这个概念至关重要。许多新程序员允许类外部的代码更改内部变量的值,这是一个大忌,即便编译器并未指出其为错误。这种做法会损害封装,令代码的安全性和可管理性大大降低,因为在不知道类内部情况下更改值,可能会导致难以检测和修复的错误和崩溃。
在此之后,您需要创建一个新的头文件,从而保持结构井井有条。这个名为 Macros.mqh 的文件最初只包含一行。
#define macroGetDate(A) (A - (A % 86400))
该行是为高亮显示日期信息。选择宏替代函数看似很不寻常,但在许多状况下使用宏更有意义。这是因为宏将作为内联函数插入到代码当中,令其尽执行可能地快速。使用宏还可以减少出现重大编程错误的可能性,尤其是当一个或多个重构需要多次重复代码之时。
注意:在该系统中,我会在某些位置尝试使用高级编程语言,令其更易于阅读和理解,特别是对于那些刚开始学习编程的人。使用高级语言并不意味着代码会变慢,反而会更容易阅读。我将向您展示如何在我们的代码应用它。
尽可能尝试使用高级语言编写代码,因为这会令调试和改进变得更加容易。还要记住,代码并不仅仅是为机器编写的,其它程序员也需要理解它。
在创建了定义所有全局宏的 Macros.mqh 头文件之后,我把这个文件包含在 C_Terminal.mqh 头文件当中。它包括如下:
#include "Macros.mqh"
注意,头文件名用双引号括起来。为什么以这种方式指示,而不是放在小于符号和大于符号(< >)之间?这有什么特殊原因吗?是的,确实有。使用双引号,我们告诉编译器头文件的路径应该从当前头文件所在的目录开始,在本例中为 C_Terminal.mqh。由于未指定特定路径,编译器将从 C_Terminal.mqh 文件相同的目录中查找 Macros.mqh 文件。因此,如果项目的目录结构发生了变化,但只要我们将 Macros.mqh 文件保存在与 C_Terminal.mqh 文件相同的目录中,那么我们不需要告诉编译器新路径。
而用小于号和大号(< >)括起来的名称,我们告诉编译器从构建系统上的预定义目录中开始查找文件。对于 MQL5,该目录是 INCLUDE。因此,任何寻址 Macros.mqh 文件的路径,必须从位于 MQL5 文件夹中的目录 INCLUDE 开始。如果项目的目录结构发生更改,则必须重新定义所有路径,如此编译器才可找到头文件。虽然这看似是一个次要细节,但选择特定方法可能会产生很大的不同。
现在我们明白了这种区别,我们来看看 C_Terminal 类中的第一段代码。该代码是类私密的,因此无法从外部访问:
void CurrentSymbol(void) { MqlDateTime mdt1; string sz0, sz1; datetime dt = macroGetDate(TimeCurrent(mdt1)); enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER; sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3); for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS); switch (eTS) { case DOL: case WDO: sz1 = "FGHJKMNQUVXZ"; break; case IND: case WIN: sz1 = "GJMQVZ"; break; default : return; } for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0)) if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break; }
所呈现的代码看似很复杂和令人困惑,但它有一个相当具体的功能:生成资产名称,如此它就可以在交叉订单系统中使用。为了理解如何做到这一点,我们来仔细分析这个过程。目前的关注点是根据 B3(巴西证券交易所)规则为指数期货和美元期货交易创建名称。理解了创建这些名称背后的逻辑,就可以调整代码为任何未来合约生成名称,允许这些合约经由交叉订单系统操作,如前面文章《从头开始开发交易 EA(第 11 部分):交叉订单系统》中所述。不过,此处的目标是扩展此功能,如此 EA 就能适配不同的条件、场景、资产、或市场。这将要求 EA 能够判定它所要处理的资产类型,这也许会导致在代码中包含更多不同类型的资产。为了更好地解释它,我们将其分解为更小的部分。
MqlDateTime mdt1; string sz0, sz1; datetime dt = macroGetDate(TimeCurrent(mdt1));
这三行是将在代码中用到的变量。此处的主要问题也许与这些变量的初始化方式有关。TimeCurrent 函数在一个步骤中初始化两个不同的变量。第一个变量 mdt1 是 MqlDateTime 类型的结构,它在 mdt1 变量中存储详细的日期和时间信息,而 TimeCurrent 还返回存储在 mdt1 中的值。第二个变量 dt 使用宏来提取日期值,并存储它,允许在一行代码中完全初始化两个变量。
enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;
这一行也许看似不同寻常,因为我们同时创建枚举、声明变量、并为其分配初始值。搞明白这一行是理解函数其它一部分的关键。请注意以下几点: 在 MQL5 中,没有名称就无法创建枚举,故我们在一开始就要指定名称。在枚举内,默认情况下,我们的元素从零值开始。这是可以更改的,但我们稍后再处理它。现在请记住:默认情况下,枚举从零开始。因此,WIN 的值为零,IND 为 1,WDO 为 2,依此类推。至于原因,稍后将解释,无论我们想要包含多少元素,最后一个元素都必须是 OTHER。枚举定义之后,我们声明一个变量,它使用从枚举中选取的数据,该变量值一开始取最后一个元素值,即OTHER.
重要说明:查看枚举的声明。您是不是看似很熟悉?请注意,名称也以大写字母声明,这非常重要。会发生什么情况:如果我们想添加要用到的期货资产,我们必须在 OTHER 元素之前加入合约名称的前三个字符来实现,如此函数就可正确生成当前合约的名称。例如,如果我们想添加一个 OX 合约,我们必须在枚举中插入 BGI 值,这是第一步。我们稍后将讨论另一个步骤。另一个例子:如果我们想添加玉米期货合约,我们要添加 CCM 值,等等,且总在 OTHER 之前。否则,枚举将无法按预期工作。
现在研究以下代码段。连同上述枚举,我们将完成第一个周期的工作。
sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3); for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS); switch (eTS) { case DOL: case WDO: sz1 = "FGHJKMNQUVXZ"; break; case IND: case WIN: sz1 = "GJMQVZ"; break; default : return; }
第一个动作是将资产名称保存在私密全局类变量之中。为了简化该过程,我们调用 StringSubstr 函数捕获执行类代码的资产名称的前三个字母,并将它们保存在 sz0 变量当中。这是最简单的阶段。现在我们来做一些非常不寻常但可能的事情:用枚举来判定合约将采用哪个命名规则。为此,我们用到 for 循环。在该循环中使用的表达式也许看似很奇怪,但我们正在做的是遍历枚举,寻找最初在枚举中定义的合约名称,正如我上面解释的那样。鉴于枚举默认从零开始,因此我们的局部循环变量也将从零开始。无论哪个元素是第一个,循环都将从那里开始并继续,直到找到 OTHER 元素,或直至 eTS 变量与 OTHER 不同。在每次迭代中,我们会递增枚举中的位置。现在到了有趣的部分:在 MQL5 中,每次循环迭代中我们都调用 EnumToString 函数将枚举值转换为字符串,并将其与 sz0 变量中的值进行比较。当这些值匹配时,该位置将保存在 eTS 变量中,因其结果与 OTHER 不同。这个函数非常有趣,它表明 MQL5 中的枚举处理方式不同于其它编程语言。在此,您可以将枚举视为字符串数组,与其它语言相比,它提供了更多的功能和实用性。
在 eTS 变量中定义所需值后,下一步是为每个合约定义特定的命名规则,这需要对变量 sz1 进行相应的初始化。遵从此处介绍的方法,根据我们想要添加到命名规则中的特定合约,选择 sz1 中的下一个字母。
如果枚举中未包含该资产,并且未找到相应的规则,则函数结束。当我们在回放/模拟模式下使用一个资产时尤其如此,因为这种类型的资产本质上是个性化和特殊的。针对这些情况,函数到此结束。
现在我们将研究另一个循环,这个阶段是“一切都变得越来越复杂”。这个循环的复杂性可能会让许多程序员感到困惑,因其功能难以理解。因此,需要更加注意以下解释。这是该循环的代码:
for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0)) if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break;
虽然代码初看好似迷惑和复杂,但实际上很简单。代码被缩短,以便令其更有效率。为了令事情更简单,并避免不必要的复杂性,我们使用 IF 语句,尽管这并非绝对必要。理论上,整个命令可以包含在 FOR 循环之中,但这会令解释变得有点复杂。这就是为什么我们在这个循环中使用 IF 来检查生成的合约名称,及与交易服务器上存在的名称之间的匹配,从而判定哪个期货合约是最相关的。若要搞明白该过程,重点是知晓创建合约名称的命名约定。举例,我们看看在巴西证券交易所交易的迷你美元期货合约会发生什么,该合约遵循特定的命名规则:
- 合同名称的前 3 个字符将是 WDO。到期日无所谓,亦或它是否为历史合约。
- 接下来,我们有一个品名表示到期月份。
- 此后,我们有一个两位数的值,表示到期年份。
因此,我们按数学规则构造了合约名称,这就是这个循环的作用。使用简单的数学规则和循环,我们创建一个合约名称,并检查其有效性。因此,重点是要遵循解释,从而明白它是如何做到的。
首先,我们在循环中初始化三个局部变量,它们将充当我们需要的记账单位。循环执行其第一次迭代,特别有趣的是,它并未发生在循环主体内部,而是发生在 if 语句中,不过,与 if 语句中的相同代码可以放在 for 循环中的冒号 (;) 之间,而循环的操作雷同。我们来了解一下在交互中发生了什么。首先,我们按其特定规则的形式创建合约名称。调用 StringFormat 函数,我们得到所需的名称,保存该品种名称,我们稍后会访问它。当我们已拥有合约名称时,我们从交易服务器请求该资产的一个属性— 合约到期时间,即 SYMBOL_EXPIRATION_TIME 枚举。SymbolInfoInteger 函数将返回一个值,而我们只对日期感兴趣。为了准确提取该值,我们用到了一个宏,其允许我们将到期日期与当前日期进行比较。如果返回值是将来的日期,则循环将结束,因为我们在变量中已定义了最近合约。不过,在初始阶段,这不太可能发生,因为年份开始于 2000,也就是说,已经过去了,故需新的迭代。重复所述的整个过程之前,我们需要增加位置,以便创建一个新的合约名称。此处务必小心,因为增加必须首先在到期代码中完成。仅当到期代码里没有满足条件的年份时,我们才会增加年份。该动作分三个阶段执行。在代码中,我们使用两个三元运算符来执行此增长。
在循环再次迭代之前,甚至在执行三元运算符之前,我们在到期月份里增加品名。增加之后,我们利用第一个三元运算符检查该值是否在可接受的范围内。因此,索引将始终指示到期月份的有效值之一。下一步是利用第二个三元运算符检查到期月份。如果到期月份索引为零,则所有月份均已检查。然后,我们递增当前年份以便重新尝试查找有效合约,并且此检查会在 if 命令中再次发生。该过程本身会重复,直至找到有效的合约,这段演示系统如何查找当前合约的名称。这并非魔术,而是数学和编程的结合。
我希望这些解释能帮助您理解该过程的代码是如何工作的。尽管这段文字复杂且冗长,但我的目标是以一种易于理解的方式解释它,如此您就可用相同的概念来实现其它期货合约的功能,允许它们依据历史操作。这很重要,因为无论合约是否存在,代码都始终使用正确的合约。
现在我们来分析以下代码,它参照了我们的类构造函数:
C_Terminal() { m_Infos.ID = ChartID(); CurrentSymbol(); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false); m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint; }
该段代码可确保正确初始化结构中的全局变量值。注意改变 MetaTrader 5 平台行为的部分。这是发生的事情。我们要求 MetaTrader 5 平台在该段代码执行处,在图表上不要生成对象的描述。在另一行中,我们示意每当从图表中删除对象时,MetaTrader 5 平台应生成一个事件,通知哪个对象已被删除,在此行中,我们指示删除的时间刻度。这就是现阶段我们所需要的全部。其它行将收集有关资产的信息。
下一段代码是类的析构函数:
~C_Terminal() { ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, true); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false); }
在该段析构函数代码中,我们重置了执行类构造函数代码之前的存在条件,令图表恢复到其原始状态。好吧,这也许不完全是原始状态,但时间刻度将再次在图表上可见。现在我们来解决图表行为可被类更改的情况。我们将创建一个小型结构,并修改构造函数和析构函数代码,从而真正将图表返回到它于类修改之前的状态。具体操作如下:
private : st_Terminal m_Infos; struct mem { long Show_Descr, Show_Date; }m_Mem; //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ C_Terminal() { m_Infos.ID = ChartID(); CurrentSymbol(); m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR); m_Mem.Show_Date = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false); m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint; } //+------------------------------------------------------------------+ ~C_Terminal() { ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false); }
这个全局变量代表我们存储数据的结构。以此方式,类就能够知道图表在被代码修改之前的样子了。我们在此捕获更改之前的数据,并将图表恢复到该时刻的原始状态。注意,一个简单的代码修改可令系统变得更好,且对我们来说更便利。值得注意的是,全局变量存储的数据,可涵盖类的整个生命周期。然而,为了理解这一点,我们不应仅仅把一个类看作是一个代码集合 — 重点是把一个类当作一个对象或一个特殊变量。当创建实例时,执行构造函数代码;而当删除或不再需要它时,调用析构函数代码。这都是自动完成的。如果您还未完全明白这是如何工作的,不要担心,这个概念会在一些日子后变得清晰。现在,您需要了解以下内容:类不仅仅是一堆代码,而实际上是一些特殊的东西,理应如此对待。
在结束本主题之前,我们快速视察一下另两个函数。我们将在下一篇文章中详细考察它们,但现在我们将看到它们的部分代码。代码如下:
//+------------------------------------------------------------------+ inline const st_Terminal GetInfoTerminal(void) const { return m_Infos; } //+------------------------------------------------------------------+ virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { switch (id) { case CHARTEVENT_CHART_CHANGE: m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); break; } } //+------------------------------------------------------------------+
这两个函数在各个方面都很特别。我只加一点它们的简要解释,因为当我们用到它们时,会有更好的解释。该函数允许类主体外部的任何代码访问类中包含的全局变量数据。在我们将要开发的代码中,将会广泛涵盖这方面。选择采用这种方式,我们确保在类不知情的情况下更改变量值不会有风险,因为我们将利用编译器来帮助我们避免这些类型的问题。不够,这里有一个问题,我们会在以后解决。该函数已用于在图表变化时更新类数据。当我们在图表上绘制某些内容时,这些值通常会用到代码的其它部分。同样,我们在以后会看到它的细节。
结束语
从本文涵盖的素材中,我们已经得到了我们的基类 C_Terminal。不过,我们仍需要讨论一个函数。这就是我们将在下一篇文章中要做的事情,于其中我们将创建 C_Mouse 类。我们在此涵盖的内容允许我们使用该类来创建实用的东西。我不会在此附上任何相关代码,因为我们的工作才刚刚开始。现在提供的任何代码都没有实际用途。在下一篇文章中,我们将创建一些非常实用的东西来帮助您在图表上开展工作。我们将开发指标和其它工具,从而支持模拟账户和实盘账户的操作,甚至是回放/模拟系统当中。下一篇文章与您再见。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11328
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



哇,好酷的项目!
我很好奇,因为我不是程序员,我想知道你是否打算将它作为产品出售。
祝贺你的作品。
谢谢。