![基于交易模块创建多个 EA 交易](https://c.mql5.com/i/articles/overlay.png)
基于交易模块创建多个 EA 交易
简介
MetaTrader 5 客户端的技术能力及其策略测试程序确定了多币种交易系统的工作和测试。为 MetaTrader 4 开发此类系统之所以很复杂,首先是因为其受制于 MetaTrader 4 无法对多个交易工具根据订单号逐一同步测试的事实。此外,鉴于 MQL4 语言有限的语言资源,也不允许组织复杂的数据结构以及进行有效的数据管理。
随着 MQL5 的发布,情况已有所变化。自此以后,MQL5 便能支持面向对象方法、基于辅助功能开发机制,甚至具有一套标准库基类用于方便用户完成日常任务 - 从数据组织到标准系统功能的工作界面,应有尽有。
尽管策略测试程序和客户端的技术规格允许使用多币种 EA 交易,但它们并没有内置方法来并行化处理单个 EA 交易同时在多个工具或时间表上进行的操作。和之前一样,有关 EA 交易的最简单情形便是,您需要在交易品种的窗口中进行 EA 交易,且交易品种决定了交易工具的名称及其时间表。如此一来,从 MetaTrader 4 时代接受到的工作方法便无法充分利用策略测试程序和 MetaTrader 5 客户端。
这一情形因下述事实而复杂化了:每个工具仅允许有一个累积仓位等于该工具上的交易总量,毫无疑问,到净持仓量的转换既正确又及时。净持仓量几近完美地呈现了交易人员在特定市场上的利益。
然而,这样的交易组织方式却未实现交易过程的简单化和可视化。此前,让 EA 交易选择其未结订单(例如,该订单可使用幻数进行识别)并实施所需操作就已经足够了。而现在,即使交易工具缺乏净持仓量也并不意味着目前市面上没有 EA 交易的特定实例!
第三方开发人员通过各种方式使用净持仓量来解决问题 - 从编写虚拟订单的专用管理程序(请参见《用于在以仓位为中心的 MT5 环境中跟踪订单的虚拟订单管理程序》一文),到使用幻数将输入整合至总持仓量(请参见《通过指定的幻数计算总持仓量的最佳方法》或《在单一工具上使用不同的 EA 交易进行交易时 ORDER_MAGIC 的使用》)。
然而,除了总持仓量的问题,还有所谓的多币种问题,此时需要在多个工具上进行同一个 EA 交易。此问题的解决方案可在《创建一个在若干工具上交易的 EA 交易》一文中找到。
所有提出的方法都很有效并具有各自的优势。然而,这些方法的致命缺陷在于,它们都试图从各自的角度去解决问题,导致所提供的解决方案不具备普适性,例如,非常适合在单一工具上同时进行多个 EA 交易的解决方案就不适于多币种问题。
本文旨在通过一个解决方案来解决所有问题。使用该解决方案可解决单一工具上不同 EA 交易交互的多币种甚至多系统测试问题。这看上去似乎很难,甚至不可能实现,但在现实中要容易得多。
不妨想像一下您的单个 EA 交易在数十种交易策略、所有可用工具以及所有可能的时间表上同时进行的情形! 此外,EA 交易还方便使用测试程序进行测试,并且对于所有策略而言,其还包含了一个或多个资金管理工作系统。
因此,我们需要解决的主要任务列示如下:
- EA 交易需要同时基于多个交易系统进行。此外,EA 交易在单一交易系统和多个交易系统上进行必须同样容易。
- EA 交易中实施的所有交易系统必须互不冲突。每个交易系统都必须只负责其各自影响总的净持仓量的部分,且只应处理各自的订单。
- 无论是使用工具的单个时间表还是同时使用所有时间表,每个系统都应同样易于交易。
- 无论是使用单个交易工具还是同时使用所有可用工具,每个系统都应同样易于交易。
如果我们仔细研究要处理的任务列表,就会得出一个三维数组。数组的第一个维度是交易系统的数目,第二个维度是特定交易系统需要在其上操作的时间表的数目,而第三个维度是用于交易系统的交易工具数目。简单的计算表明,即便是像 MACD 示例这样简单的 EA 交易,当其同时使用 8 个主要货币对时,也会出现 152 种独立的解决方案:1 EA 交易 * 8 货币对 * 19 时间表(未包含周和月时间表)。
如果交易系统规模更大、EA 交易的投资组合范围更广,则解决方案的数量就会轻松超过 500 个,在一些情形下甚至超过 1000 个!显而易见,逐个手动配置然后再上传每种组合是不可能的。因此,有必要通过这种方式建立一个系统,使其能够自动调整每个组合,将其载入 EA 交易的内存,然后 EA 交易就可以基于该组合特定实例的规则进行交易了。
术语和概念
此处及下文中,“交易策略”这一概念将使用特殊术语交易模型(或简称模型)来替代。交易模型是根据特殊规则创建的特殊类,该模型完整说明了交易策略:交易中使用的指标、进入和退出交易的条件、资金管理方法等。每个交易模型都较为抽象,并没有为其操作定义具体参数。
一个简单的例子是基于两条移动平均线穿越的交易策略。如果快速移动平均线向上穿越慢速移动平均线,则以买入建仓;反之如果是向下穿越,则以卖出建仓。此表述足以用来编写一个在其基础上进行交易的交易模型。
然而,一旦要描述此类模型时,则有必要确定移动平均线的方法和平均周期、数据窗口的周期以及将在该模型上进行交易的工具。一般来说,该抽象模型包含了在您需要创建特定模型实例时需要填充的参数。显而易见,通过该方法,一个抽象模型可以成为该模型多个实例的父级,这些实例的参数有所不同。
对净持仓量计算的完全否定
很多交易人员会尝试跟踪总持仓量。然而,从上文得知,无论是总持仓量的大小还是其动态都与模型的具体实例没有什么相关性。模型可以是卖出持仓模型,而总持仓量可能根本不存在(总持仓量处于中性水平)。与此相反,总持仓量可能是卖出持仓,而模型将具有买入持仓。
事实上,让我们仔细讲述下这些类。假设一个工具使用三种不同的交易策略进行交易,每个交易策略都具有各自独立的资金管理系统。假设这三个系统中的第一个决定出售三个合约而未平仓,或简单地说,建一个卖出持仓,其交易量为三个合约的交易量。完成交易后,净持仓量将仅包含来自首个交易系统的交易,其交易量将减去三个合约的交易量,或三个未平仓的卖出持仓合约的交易量。一段时间后,第二个交易系统决定买入相同资产的 4 个合约。
因此,净持仓量将发生变化,并将包含 1 个买入持仓合约。这次它将涉及两个交易系统的影响。接下来,第三个交易系统会进入市场,建立相同资产的一个卖出持仓,其交易量为一个标准合约。净持仓量将变为中性,因为 -3 (卖出持仓) + 4 (买入持仓) - 1 (卖出持仓) = 0。
是否缺乏净持仓量就意味着所有三个交易系统都不在市场中呢?完全不是。三个交易系统中的两个持有四个未平仓合约,这意味着随着时间的推移合约将被平仓。另一方面,第三个系统持有 4 个买入持仓合约,这些合约都有待售回。只有当四个卖出持仓合约的全额偿付完成,且四个买入持仓合约都已平仓售出,中性持仓量才意味着所有三个系统都确实缺乏仓位。
当然,我们可以每次为各个模型重新构建整个操作序列,并由此确定其对于当前仓位大小的具体影响,不过还有个更简单的方法。这个方法很简单 - 它必须完全摒弃对总持仓量的计算,总持仓量可以是任意大小,并可以基于外部因素(如手动交易)和内部因素(EA 交易其他模型在一个工具上的操作)。由于无法将当前的总持仓量作为依据,那我们如何计算具体模型实例的行为呢?
最简单有效的方法就是为模型的每个实例提供自己的订单表,该表格将涵盖所有订单,包括挂单以及那些由交易发起或删除的订单。与订单相关的大量信息存储在交易服务器中。知道了订单的订单号,我们几乎就可以获得从建仓时间到交易量的与订单相关的任何信息。
我们唯一要做的就是将订单号链接至模型的具体实例。每个模型实例都必须包含特殊类(即订单表)的单个实例,订单表将包含该模型实例设置的当前订单的列表。
设计抽象交易模型
现在,让我们来尝试说明交易策略将参照的模型的普通抽象类。由于 EA 交易应使用多个模型(或数量无限制),显然该类应具有一个统一接口,强大的外部 EA 交易可通过该接口提供信号。
例如,该接口可以是 Processing() function。简单地说,每个 CModel 类都将具有自己的 Processing() 函数。该函数将基于每个订单号或每分钟、或是基于 Trade 类型的新事件予以调用。
下面是完成该任务的一个简单示例:
class CModel { protected: string m_name; public: void CModel(){m_name="Model base";} bool virtual Processing(void){return(true);} }; class cmodel_macd : public CModel { public: void cmodel_macd(){m_name="MACD Model";} bool Processing(){Print("Model name is ", m_name);return(true);} }; class cmodel_moving : public CModel { public: void cmodel_moving(){m_name="Moving Average";} bool Processing(){Print("Model name is ", m_name);return(true);} }; cmodel_macd *macd; cmodel_moving *moving;
我们来研究一下代码是如何工作的。CModel 基类包含一个名为 m_name 的 string 类型的受保护变量。"protected" 关键字允许类的继承子类使用该变量,因此其后代已经包含该变量。此外,基类还定义了 Processing() 虚拟函数。在本例中,"virtual" 一词表明这是一个包装程序或 EA 交易与模型具体实例之间的接口。
任何继承自 CModel 的类都将保证具有 Processing() 接口用于交互之用。实现该函数代码的工作被委托给了该函数的后代。这种委托比较明显,因为不同模型的内部工作原理可能存在较大差异,因此在 CModel 总层面上缺少一种通用归纳。
接下来是对 cmodel_macd 和 cmodel_moving 两个类的说明。这两个类都从 CModel 类生成,因此都具有自己的 Processing() 函数实例和 m_name 变量。请注意,两个模型的 Processing() 函数的内部实现是不同的。在第一个模型中,它包含 Print ("It is cmodel_macd.Model name is ", m_name),第二个模型为 Print("It is cmodel_moving.Model name is ", m_name)。接下来会创建两个指针,其中每个都指向模型的一个具体实例,一个指向 cmodel_macd 类型的类,另一个指向 cmodel_moving 类型的类。
在 OnInit 函数中,这些指针继承了动态创建的类模型,之后在 OnEvent() 函数中,每个类中包含的 Processing() 函数被调用。两个指针都在全局层面进行了声明,因此即使退出 OnInit() 函数,其中所创建的类也不会被删除,但会继续退出全局层面。现在,OnTimer() 函数会每隔五秒对两个模型轮流取样,调用其相应的 Processing() 函数。
我们刚刚创建的这种对模型取样的原始系统缺乏灵活性和可量测性。如果我们希望使用多个这样的模型,应该怎么做呢?单独使用每个模型很不方便。而如果将所有模型集中到一个较小范围(例如数组)内,就容易得多了,接下来再通过调用每个元素的 Processing() 函数来对该数组的所有元素进行迭代。
但问题在于,数组的组织方式要求存储在其中的数据为同一类型。在我们的例子中,虽然模型 cmodel_macd 和 cmodel_moving 彼此十分类似,但并非完全一样,这样就无法在数组中使用它们了。
幸运的是,数组并非是数据汇总的唯一方式,还有其他更灵活和可量测的汇总方法。其中之一就是链接表技术。其工作原理十分简单。整个列表中的每一项都包含两个指针。一个指针指向上个列表项,另一个指针指向下个列表项。
同样,了解列表项的索引号后,您就可以随时引用它。当您想要添加或删除列表项时,重建其指针以及相邻项的指针便已足够,所以它们始终互相指向对方。了解这种范围内的内部组织并不是必须的,知道其常见设备便已足够。
MetaTrader 5 的标准安装包含一个特殊的辅助类 CList,该类提供了使用链接表的机会。然而,该列表的元素只能是 CObject 类型的对象,因为只有它们才具有可使用链接表的特殊指针。就其本身而言,CObject 类相当落后,仅能用作与 CList 类交互的接口。
看一看该类的实现就可了解这一点:
//+------------------------------------------------------------------+ //| Object.mqh | //| Copyright © 2010, MetaQuotes Software Corp. | //| https://www.metaquotes.net/ | //| Revision 2010.02.22 | //+------------------------------------------------------------------+ #include "StdLibErr.mqh" //+------------------------------------------------------------------+ //| Class CObject. | //| Purpose: Base class element storage. | //+------------------------------------------------------------------+ class CObject { protected: CObject *m_prev; // previous list item CObject *m_next; // next list item public: CObject(); //--- methods of access to protected data CObject *Prev() { return(m_prev); } void Prev(CObject *node) { m_prev=node; } CObject *Next() { return(m_next); } void Next(CObject *node) { m_next=node; } //--- methods for working with files virtual bool Save(int file_handle) { return(true); } virtual bool Load(int file_handle) { return(true); } //--- method of identifying the object virtual int Type() const { return(0); } protected: virtual int Compare(const CObject *node,int mode=0) const { return(0); } }; //+------------------------------------------------------------------+ //| Constructor CObject. | //| INPUT: no. | //| OUTPUT: no. | //| REMARK: no. | //+------------------------------------------------------------------+ void CObject::CObject() { //--- initialize protected data m_prev=NULL; m_next=NULL; } //+------------------------------------------------------------------+
可以看出,该类的基础是两个指针,具备指针的典型特征。
现在是最重要的部分。由于继承机制的原因,可以将该类包含于交易模型中,这意味着可将交易模型的类包含到 CList type 列表中!让我们尝试一下。
因此,我们将使用抽象类 CModel 作为 CObject 类的后代:
class CModel : public CObject
由于类 cmodel_moving 和 cmodel_average 继承自 CModel 类,它们包含 CObject 类的数据和方法,因此,它们可包含在 CList 类型的列表中。程序创建了两个有条件的交易模型,将其置于列表中,并循序对每个订单号取样;其源代码如下所示:
//+------------------------------------------------------------------+ //| ch01_simple_model.mq5 | //| Copyright 2010, Vasily Sokolov (C-4). | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Vasily Sokolov (C-4)." #property link "https://www.mql5.com" #property version "1.00" #include <Arrays\List.mqh> // Base model class CModel:CObject { protected: string m_name; public: void CModel(){m_name="Model base";} bool virtual Processing(void){return(true);} }; class cmodel_macd : public CModel { public: void cmodel_macd(){m_name="MACD Model";} bool Processing(){Print("Processing ", m_name, "...");return(true);} }; class cmodel_moving : public CModel { public: void cmodel_moving(){m_name="Moving Average";} bool Processing(){Print("Processing ", m_name, "...");return(true);} }; //Create list of models CList *list_models; void OnInit() { int rezult; // Great two pointer cmodel_macd *m_macd; cmodel_moving *m_moving; list_models = new CList(); m_macd = new cmodel_macd(); m_moving = new cmodel_moving(); //Check valid pointer if(CheckPointer(m_macd)==POINTER_DYNAMIC){ rezult=list_models.Add(m_macd); if(rezult!=-1)Print("Model MACD successfully created"); else Print("Creation of Model MACD has failed"); } //Check valid pointer if(CheckPointer(m_moving)==POINTER_DYNAMIC){ rezult=list_models.Add(m_moving); if(rezult!=-1)Print("Model MOVING AVERAGE successfully created"); else Print("Creation of Model MOVING AVERAGE has failed"); } } void OnTick() { CModel *current_model; for(int i=0;i<list_models.Total();i++){ current_model=list_models.GetNodeAtIndex(i); current_model.Processing(); } } void OnDeinit(const int reason) { delete list_models; }
一旦程序编译并执行,表明 EA 交易正常运行的相似语句行,应出现在 EA 交易日志中。
2010.10.10 14:18:31 ch01_simple_model (EURUSD,D1) Prosessing Moving Average... 2010.10.10 14:18:31 ch01_simple_model (EURUSD,D1) Processing MACD Model... 2010.10.10 14:18:21 ch01_simple_model (EURUSD,D1) Model MOVING AVERAGE was created successfully 2010.10.10 14:18:21 ch01_simple_model (EURUSD,D1) Model MACD was created successfully
我们来仔细研究一下代码的工作原理。因此,如上文所述,我们的基本交易模型 CModel 派生自 CObject 类,该类赋予我们将基本模型的后代包含进 CList 类型的列表的权利:
rezult=list_models.Add(m_macd); rezult=list_models.Add(m_moving);
整理数据时要求使用指针。一旦具体模型的指针在 OnInit() 函数的局部层面创建并进入全局列表 list_models,对它们的需求便不复存在,它们可以随该函数的其他变量一起安全地自毁。
一般来说,建议模型具有一个显著特征,即只有全局变量(除模型类本身以外)是这些模型的动态链接表。因此,从一开始项目就支持高度封装。
如果因某些原因导致模型创建失败(例如,所需参数的值未能正确显示),则该模型不会添加至列表。这不会影响 EA 交易的整体运行,因为其只会处理那些成功添加至列表的模型。
对创建模型的取样通过 OnTick() 函数来实现。该函数包含一个循环。在该循环中,元素的数量是确定的,之后便是从循环的第一个元素 (i = 0) 到最后一个元素 (i <list_models.Total();i++) 的连续传代:
CModel *current_model; for(int i=0;i<list_models.Total();i++){ current_model=list_models.GetNodeAtIndex(i); current_model.Processing(); }
指向 CModel 基类的指针被用作通用适配器。这确保了该指标支持的任意函数都可用于派生模型。在这种情况下,我们仅需要 Processing() 函数。每个模型都有自己的 Processing() 版本,其内部实现可能与其他模型的相似函数有所不同。无需重载该函数,它只能以一种形式存在:即不包含任何输入参数,并返回布尔型值。
该函数应执行种类繁多的任务:
- 该函数应根据自身的交易模型独立确定当前的市场状况。
- 在作出进入市场的决策后,该函数应独立计算交易所需的担保金额(预付款量)、交易量、可能的最大损失值或获利水平。
- 该模型的行为应与其之前的操作相关。例如,如果一个卖出持仓被该模型所发起,则不太可能在未来对其进行进一步构建。所有这些验证均应在 Processing() 函数内进行。
- 所有这些函数都应能访问通用参数,例如账户状态。基于此数据,该函数应使用其模型中内嵌的参数来执行自己的资金管理。例如,如果一个模型的资金管理通过使用最优公式 f 来完成,则其每个模型的值都应不相同。
显然,由于 Processing() 函数须执行大量任务,将依赖于那些包含于 MetaTrader 5 以及那些专为此解决方案设计的辅助类开发装置。
可以看出,大部分工作都委托给了该模型的具体实例。EA 交易的外部层面依次对每个模型进行控制,而且它的工作也是在这里完成的。具体模型所要承担的任务将取决于其内部逻辑。
一般而言,我们构建的交互系统可通过以下图表进行说明:
请注意,尽管上述代码显示模型排序发生在 OnTick() 函数的内部,但这并不是必须的。排序循环可轻松置于任何其他所需函数内,例如 OnTrade() 或 OnTimer()。
虚拟订单表 - 模型的基础
一旦我们将所有交易模型组合为单个列表,就到了说明交易过程的时候了。让我们回到 CModel 类,并尝试为其补充一些在交易过程中应参照的其他数据和函数。
如上文所述,新的净持仓量范例详细说明了处理订单和交易的不同规则。在 MetaTrader 4 中,每个交易都随附了订单,订单从下达起直到终结或其发起的交易平仓都存在于选项卡 "Trade"(交易)中。
在 MetaTrader 5 中,挂单一直存在,直到交易实际完成的那一刻。在交易实施后,或通过其进入市场后,这些订单会传递至存储在交易服务器上的订单历史数据中。这种情况造成了不稳定性。假设 EA 交易发布了一个已执行订单。总持仓量已发生变化。一段时间过后,EA 交易需要平仓。
我们无法像在 MetaTrader 4 中那样对具体订单进行平仓,因为缺少订单平仓的概念,但我们可以实现全部平仓或部分平仓。问题是,应对仓位的哪一部分执行平仓操作。或者,我们也可以仔细查看所有历史订单,选择 EA 交易发布的订单,然后将这些订单与当前市场状况相关联,并在必要时阻止它们的回报订单。该方法实现的难度很大。
例如,我们如何确定订单在之前未被阻止?我们可以采用另一种方式,假设当前仓位只属于当前 EA 交易。本选项仅适用于您打算在一个使用一个策略进行一次 EA 交易的情形。所有这些方法都不能完美解决我们面临的挑战。
最简单明了的解决方案是在模型本身中存储与当前模型订单(即未被反向交易阻止的订单)有关的所有必要信息。
例如,如果模型发布一个订单,其订单号就会被记录在该模型内存的一个特殊区域中,例如可通过我们已熟悉的链接表系统的帮助对其进行整理。
知道了订单号,您就可以找到与其有关的几乎所有信息,因此我们只需将该订单号链接至发布它的模型。我们将该订单号存储在 CTableOrder 特殊类中。除了订单号,它还可以包含最基本的信息,例如订单的交易量、其安装时间、幻数等。
我们来看下这个类是如何构建的:
#property copyright "Copyright 2010, MetaQuotes Software Corp." #property link "https://www.mql5.com" #include <Trade\_OrderInfo.mqh> #include <Trade\_HistoryOrderInfo.mqh> #include <Arrays\List.mqh> class CTableOrders : CObject { private: ulong m_magic; // Magic number of the EA that put out the order ulong m_ticket; // Ticket of the basic order ulong m_ticket_sl; // Ticket of the simulated-Stop-Loss order, assigned with the basic order ulong m_ticket_tp; // Ticket of the simulated-Take-Profit, assigned with the basic order ENUM_ORDER_TYPE m_type; // Order type datetime m_time_setup; // Order setup time double m_price; // Order price double m_sl; // Stop Loss price double m_tp; // Take Profit price double m_volume_initial; // Order Volume public: CTableOrders(); bool Add(COrderInfo &order_info, double stop_loss, double take_profit); bool Add(CHistoryOrderInfo &history_order_info, double stop_loss, double take_profit); double StopLoss(void){return(m_sl);} double TakeProfit(void){return(m_tp);} ulong Magic(){return(m_magic);} ulong Ticket(){return(m_ticket);} int Type() const; datetime TimeSetup(){return(m_time_setup);} double Price(){return(m_price);} double VolumeInitial(){return(m_volume_initial);} }; CTableOrders::CTableOrders(void) { m_magic=0; m_ticket=0; m_type=0; m_time_setup=0; m_price=0.0; m_volume_initial=0.0; } bool CTableOrders::Add(CHistoryOrderInfo &history_order_info, double stop_loss, double take_profit) { if(HistoryOrderSelect(history_order_info.Ticket())){ m_magic=history_order_info.Magic(); m_ticket=history_order_info.Ticket(); m_type=history_order_info.Type(); m_time_setup=history_order_info.TimeSetup(); m_volume_initial=history_order_info.VolumeInitial(); m_price=history_order_info.PriceOpen(); m_sl=stop_loss; m_tp=take_profit; return(true); } else return(false); } bool CTableOrders::Add(COrderInfo &order_info, double stop_loss, double take_profit) { if(OrderSelect(order_info.Ticket())){ m_magic=order_info.Magic(); m_ticket=order_info.Ticket(); m_type=order_info.Type(); m_time_setup=order_info.TimeSetup(); m_volume_initial=order_info.VolumeInitial(); m_price=order_info.PriceOpen(); m_sl=stop_loss; m_tp=take_profit; return(true); } else return(false); } int CTableOrders::Type() const { return((ENUM_ORDER_TYPE)m_type); }
与 CModel 类相似,CTableOrders 类也继承自 CObject。正如模型的类一样,我们将 CTableOrders 的实例放入 CList类型的 ListTableOrders 列表中。
除了自己的订单号 (m_tiket),该类还包含将其发布的 EA 交易的幻数 (ORDER_MAGIC)的相关信息、其类型、开盘价、交易量以及订单的估计重叠水平等信息:止损 (m_sl) 和获利 (m_tp)。对于最后两个值,我们需要单独说明。显而易见,任何交易迟早都将通过反向交易进行平仓。反向交易可在其结束时基于当前市场状况或在仓位部分平仓时以预定价格发起。
在 MetaTrader4 中,这种“无条件退出仓位”是特殊的退出形式:止损和获利。MetaTrader 4 的显著特征是这些水平应用到了具体订单中。例如,如果一个活动订单发生止损,其不会影响该工具上的其他未结订单。
在 MetaTrader 5 中,情况有些许不同。虽然对于每个订单组合以及其他事项,您都可以指定止损和获利价格,但这些价格不会影响到具体订单;虽然这些价格是在具体订单中设置,但它们关乎该工具上的整个仓位。
假设有一个 1 标准手的 EURUSD 买入建仓,而未规定止损和获利水平。一段时间过后,发布了另一个 0.1 手的 EURUSD 买入订单,其具有设置的止损和获利水平 - 每一个都与当前价格相距 100 个点位。一段时间过后,价格会到达止损水平或获利水平。发生这种情况时,交易量为 1.1 手的 EURUSD 仓位将被平仓。
换言之,止损和获利仅可设置为与总持仓量相关,而非针对具体订单。在此基础上,将不太可能这些订单用于多系统 EA 交易上。这是显而易见的,因为如果一个系统提出自己的止损和获利,就会将其应用到所有其他系统,而这些系统的盈利已经包含在工具的总持仓量中!
因此,EA 交易的每个子系统只能分别针对每个订单使用自己的内部止损和获利。这个概念同样来自以下事实:即使在同一交易系统内,不同订单也可能具有不同的止损和获利水平,并且正如上文中已提到的,在 MetaTrader 5 中,无法将这些输出值指派给单个订单。
如果我们在虚拟订单内设置综合止损和获利水平,则一旦价格达到或超过这些水平,EA 交易将能够独立地阻止现有订单。在阻止这些订单后,就可将其安全地从活动订单列表中移除。该方法说明如下。
除自身数据外,CTableOrders 类还包含一个十分重要的 Add() 函数。该函数用于接收订单号,这需要记录到表格中。除接受订单号外,该函数还用于接收虚拟止损 和获利水平。首先,Add() 函数会尝试在历史订单中分配订单,历史订单存储在服务器上。如果能够这样做,它会将订单号上的信息输入到 history_order_info 类的实例中,然后开始通过其将信息输入到新的 TableOrders 元素中。接下来,该元素会被添加至订单列表。如果无法完成订单的选择,则我们可能正在处理挂单,因此需要使用 OrderSelect() 函数尝试从当前订单中分配该订单。如果成功选择该订单,则执行与历史订单同样的操作。
在引入描述 Trade 事件的结构前,为多系统 EA 交易处理挂单非常困难。当然,在引入该结构后,基于挂单设计 EA 交易就成为可能。此外,如果出现了订单表,则几乎任何含挂单的交易策略都可以进入市场。基于这些原因,文中出现的所有交易模型将具有市场执行(ORDER_TYPE_BUY 或 ORDER_TYPE_SELL)的功能。
CModel - 交易模型的基类
因此,在我们全部完成订单表的设计后,是时候说明基本模型 CModel 的完整版本了:
class CModel : public CObject { protected: long m_magic; string m_symbol; ENUM_TIMEFRAMES m_timeframe; string m_model_name; double m_delta; CTableOrders *table; CList *ListTableOrders; CAccountInfo m_account_info; CTrade m_trade; CSymbolInfo m_symbol_info; COrderInfo m_order_info; CHistoryOrderInfo m_history_order_info; CPositionInfo m_position_info; CDealInfo m_deal_info; t_period m_timing; public: CModel() { Init(); } ~CModel() { Deinit(); } string Name(){return(m_model_name);} void Name(string name){m_model_name=name;} ENUM_TIMEFRAMES Timeframe(void){return(m_timeframe);} string Symbol(void){return(m_symbol);} void Symbol(string set_symbol){m_symbol=set_symbol;} bool virtual Init(); void virtual Deinit(){delete ListTableOrders;} bool virtual Processing(){return (true);} double GetMyPosition(); bool Delete(ENUM_TYPE_DELETED_ORDER); bool Delete(ulong Ticket); void CloseAllPosition(); //bool virtual Trade(); protected: bool Add(COrderInfo &order_info, double stop_loss, double take_profit); bool Add(CHistoryOrderInfo &history_order_info, double stop_loss, double take_profit); void GetNumberOrders(n_orders &orders); bool SendOrder(string symbol, ENUM_ORDER_TYPE op_type, ENUM_ORDER_MODE op_mode, ulong ticket, double lot, double price, double stop_loss, double take_profit, string comment); };
来自该类的数据包含任何交易模型的基本常量。
这些基本常量包括幻数 (m_magic)、模型将在其上启动的交易品种 (m_symbol)、时间表 (m_timeframe) 以及最常交易模型的名称 (m_name)。
此外,模型包含我们已熟悉的订单表类 (CTableOrders * table) 以及表格的实例将于其中保存的列表和每个订单的一个副本 (CList*ListTableOrders)。由于所有数据必要时都将动态创建,因此对数据的操作将通过指针完成。
接下来是变量 m_delta。该变量应保留一个特别系数,用于在资金管理公式中计算当前手数。例如,对于固定份额的资本化公式,该变量可存储账户的一个份额,该份额可以具有风险,例如,对于风险为 2% 的账户,该变量必须等于 0.02。对于更为积极的方法(例如优化法),该变量可能会更大。
对于该变量而言,重要的是它允许为作为单个 EA 交易一部分的每个模型单独选择风险。如果未使用资本化公式,则不需要填入该变量。默认情况下,该变量等于 0.0。
接下来的是包含所有辅助交易类,这些类被设计用于促进所有必要信息(从账户信息到有关仓位)的接收和处理。可以理解,具体交易模型的派生物应积极使用这些辅助类,这并非 OrderSelect 或 OrderSend 类型的常规功能。
变量 m_timing 需要进行单独说明。在使用 EA 交易的过程中,有必要在特定的时间间隔内调用特定的事件。OnTimer() 函数不适于此目的,因为不同模型可能存在于不同的时间间隔内。
例如,某些事件需要在每个新柱出现时调用。对于在小时图上交易的模型,应每隔一个小时调用此类事件;对于在日图上交易的模型,则在每个新日柱出现时再调用此事件。显然,这些模型具有不同的时间设置,这些设置应分别存储在相应模型中。CModel 类中包含的 t_period 结构允许您将这些设置分别存储在其模型中。
该结构如下所示:
struct t_period { datetime m1; datetime m2; datetime m3; datetime m4; datetime m5; datetime m6; datetime m10; datetime m12; datetime m15; datetime m20; datetime m30; datetime h1; datetime h2; datetime h3; datetime h4; datetime h6; datetime h8; datetime h12; datetime d1; datetime w1; datetime mn1; datetime current; };
可以看出,该结构包含常见的时间表列表。要查看是否出现新柱,您需要将上一个柱的时间与结构 t_period 中记录的时间进行比较。如果时间不匹配,则会出现新柱,且结构中的时间应更新为当前柱的时间,并返回一个肯定结果 (true)。如果上一个柱的时间与结构中的时间相同,则表示新柱尚未出现,此时应返回一个否定结果 (false)。
下面是基于上文描述的算法实现的一个函数:
bool timing(string symbol, ENUM_TIMEFRAMES tf, t_period &timeframes) { int rez; MqlRates raters[1]; rez=CopyRates(symbol, tf, 0, 1, raters); if(rez==0) { Print("Error timing"); return(false); } switch(tf){ case PERIOD_M1: if(raters[0].time==timeframes.m1)return(false); else{timeframes.m1=raters[0].time; return(true);} case PERIOD_M2: if(raters[0].time==timeframes.m2)return(false); else{timeframes.m2=raters[0].time; return(true);} case PERIOD_M3: if(raters[0].time==timeframes.m3)return(false); else{timeframes.m3=raters[0].time; return(true);} case PERIOD_M4: if(raters[0].time==timeframes.m4)return(false); else{timeframes.m4=raters[0].time; return(true);} case PERIOD_M5: if(raters[0].time==timeframes.m5)return(false); else{timeframes.m5=raters[0].time; return(true);} case PERIOD_M6: if(raters[0].time==timeframes.m6)return(false); else{timeframes.m6=raters[0].time; return(true);} case PERIOD_M10: if(raters[0].time==timeframes.m10)return(false); else{timeframes.m10=raters[0].time; return(true);} case PERIOD_M12: if(raters[0].time==timeframes.m12)return(false); else{timeframes.m12=raters[0].time; return(true);} case PERIOD_M15: if(raters[0].time==timeframes.m15)return(false); else{timeframes.m15=raters[0].time; return(true);} case PERIOD_M20: if(raters[0].time==timeframes.m20)return(false); else{timeframes.m20=raters[0].time; return(true);} case PERIOD_M30: if(raters[0].time==timeframes.m30)return(false); else{timeframes.m30=raters[0].time; return(true);} case PERIOD_H1: if(raters[0].time==timeframes.h1)return(false); else{timeframes.h1=raters[0].time; return(true);} case PERIOD_H2: if(raters[0].time==timeframes.h2)return(false); else{timeframes.h2=raters[0].time; return(true);} case PERIOD_H3: if(raters[0].time==timeframes.h3)return(false); else{timeframes.h3=raters[0].time; return(true);} case PERIOD_H4: if(raters[0].time==timeframes.h4)return(false); else{timeframes.h4=raters[0].time; return(true);} case PERIOD_H6: if(raters[0].time==timeframes.h6)return(false); else{timeframes.h6=raters[0].time; return(true);} case PERIOD_H8: if(raters[0].time==timeframes.h8)return(false); else{timeframes.h8=raters[0].time; return(true);} case PERIOD_H12: if(raters[0].time==timeframes.h12)return(false); else{timeframes.h12=raters[0].time; return(true);} case PERIOD_D1: if(raters[0].time==timeframes.d1)return(false); else{timeframes.d1=raters[0].time; return(true);} case PERIOD_W1: if(raters[0].time==timeframes.w1)return(false); else{timeframes.w1=raters[0].time; return(true);} case PERIOD_MN1: if(raters[0].time==timeframes.mn1)return(false); else{timeframes.mn1=raters[0].time; return(true);} case PERIOD_CURRENT: if(raters[0].time==timeframes.current)return(false); else{timeframes.current=raters[0].time; return(true);} default: return(false); } }
目前还不存在对结构进行顺序排序的可能性。当您需要在于不同时间表上交易的同一交易模型的一个循环中创建多个实例时,可能会需要这种排序。因此我不得不编写一个有关 t_period 结构的特殊排序函数。
下面是该函数的源代码:
int GetPeriodEnumerator(uchar n_period) { switch(n_period) { case 0: return(PERIOD_CURRENT); case 1: return(PERIOD_M1); case 2: return(PERIOD_M2); case 3: return(PERIOD_M3); case 4: return(PERIOD_M4); case 5: return(PERIOD_M5); case 6: return(PERIOD_M6); case 7: return(PERIOD_M10); case 8: return(PERIOD_M12); case 9: return(PERIOD_M15); case 10: return(PERIOD_M20); case 11: return(PERIOD_M30); case 12: return(PERIOD_H1); case 13: return(PERIOD_H2); case 14: return(PERIOD_H3); case 15: return(PERIOD_H4); case 16: return(PERIOD_H6); case 17: return(PERIOD_H8); case 18: return(PERIOD_H12); case 19: return(PERIOD_D1); case 20: return(PERIOD_W1); case 21: return(PERIOD_MN1); default: Print("Enumerator period must be smallest 22"); return(-1); } }
所有这些函数都方便地整合在一个文件中,位于 \\Include 文件夹内。我们将其命名为 Time.mqh。
该文件将包含在我们的 CModel 基类中:
…
#incude <Time.mqh>
…
除了 Name()、Timeframe() 和 Symbol() 类型的简单函数 get/set,CModel 类还包含 Init()、GetMyPosition()、Delete()、CloseAllPosition() 和Processing() 类型的复杂函数。读者应该已经熟悉了最后一个函数的名称,我们稍后还将更详细讨论其内部结构,但现在让我们开始介绍基类 CModel 的主要函数吧。
CModel::Add() 函数动态创建CTableOrders 类的实例,然后使用相应的 CTabeOrders::Add() 函数填充该实例。它的操作原理已在上文进行了说明。填充后,该项包含在当前模型 (ListTableOrders.Add (t)) 的所有订单的总列表中。
另一方面,CModel::Delete() 函数从活动订单列表中删除 CTableOrders 类型的元素。为此,您必须指定要删除订单的订单号。其工作原理十分简单。该函数按顺序对整个订单表进行排序,用于搜索具有正确订单号的订单。如果该函数找到这样的订单,就将删除此订单。
CModel::GetNumberOrders() 函数会计算活动订单的总数。它 填充 特殊 结构 n_orders:
struct n_orders { int all_orders; int long_orders; int short_orders; int buy_sell_orders; int delayed_orders; int buy_orders; int sell_orders; int buy_stop_orders; int sell_stop_orders; int buy_limit_orders; int sell_limit_orders; int buy_stop_limit_orders; int sell_stop_limit_orders; };
可以看出,调用该函数后我们便可以得出所设置的具体类型订单的数量。例如,要获得所有卖出持仓订单的数量,您必须读取 n_orders 实例的所有 short_orders 的值。
CModel::SendOrder() 函数是实际发送订单至交易服务器的基本和唯一函数。与每个具体模型不同,前者采用的是自己的用于发送订单至服务器的算法,SendOrder() 函数定义了这些提交的一般程序。不管模型如何,发布订单发布过程都与同一检查相关,而在一个位置集中执行就要有效得多。
我们来 熟悉一下 该函数 的 源代码:
bool CModel::SendOrder(string symbol, ENUM_ORDER_TYPE op_type, ENUM_ORDER_MODE op_mode, ulong ticket, double lot, double price, double stop_loss, double take_profit, string comment) { ulong code_return=0; CSymbolInfo symbol_info; CTrade trade; symbol_info.Name(symbol); symbol_info.RefreshRates(); mm send_order_mm; double lot_current; double lot_send=lot; double lot_max=m_symbol_info.LotsMax(); //double lot_max=5.0; bool rez=false; int floor_lot=(int)MathFloor(lot/lot_max); if(MathMod(lot,lot_max)==0)floor_lot=floor_lot-1; int itteration=(int)MathCeil(lot/lot_max); if(itteration>1) Print("The order volume exceeds the maximum allowed volume. It will be divided into ", itteration, " deals"); for(int i=1;i<=itteration;i++) { if(i==itteration)lot_send=lot-(floor_lot*lot_max); else lot_send=lot_max; for(int i=0;i<3;i++) { //Print("Send Order: TRADE_RETCODE_DONE"); symbol_info.RefreshRates(); if(op_type==ORDER_TYPE_BUY)price=symbol_info.Ask(); if(op_type==ORDER_TYPE_SELL)price=symbol_info.Bid(); m_trade.SetDeviationInPoints(ulong(0.0003/(double)symbol_info.Point())); m_trade.SetExpertMagicNumber(m_magic); rez=m_trade.PositionOpen(m_symbol, op_type, lot_send, price, 0.0, 0.0, comment); // Sleeping is not to be deleted or moved! Otherwise the order will not have time to get recorded in m_history_order_info!!! Sleep(3000); if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED|| m_trade.ResultRetcode()==TRADE_RETCODE_DONE_PARTIAL|| m_trade.ResultRetcode()==TRADE_RETCODE_DONE) { //Print(m_trade.ResultComment()); //rez=m_history_order_info.Ticket(m_trade.ResultOrder()); if(op_mode==ORDER_ADD){ rez=Add(m_trade.ResultOrder(), stop_loss, take_profit); } if(op_mode==ORDER_DELETE){ rez=Delete(ticket); } code_return=m_trade.ResultRetcode(); break; } else { Print(m_trade.ResultComment()); } if(m_trade.ResultRetcode()==TRADE_RETCODE_TRADE_DISABLED|| m_trade.ResultRetcode()==TRADE_RETCODE_MARKET_CLOSED|| m_trade.ResultRetcode()==TRADE_RETCODE_NO_MONEY|| m_trade.ResultRetcode()==TRADE_RETCODE_TOO_MANY_REQUESTS|| m_trade.ResultRetcode()==TRADE_RETCODE_SERVER_DISABLES_AT|| m_trade.ResultRetcode()==TRADE_RETCODE_CLIENT_DISABLES_AT|| m_trade.ResultRetcode()==TRADE_RETCODE_LIMIT_ORDERS|| m_trade.ResultRetcode()==TRADE_RETCODE_LIMIT_VOLUME) { break; } } } return(rez); }
该函数的首要功能就是验证执行交易服务器中规定交易量的可行性。这可以通过使用 CheckLot() 函数来完成。期间可能会存在仓位大小上的一些交易限制。应该将这些限制纳入考虑范围。
应考虑下述情形:对于在两个方向上均为 15 标准手的交易仓位,存在着大小限制。当前仓位为 3 手买入持仓。基于自身资金管理系统的交易模型希望建立一个交易量为 18.6 手的买入持仓。CheckLot() 函数将返回修正后的交易量。在本例中,它将等于 12 手(因为其他交易已经占据了 15 手中的 3 手)。如果当前敞口仓位为卖出持仓而非买入持仓,则函数将返回 15 手而非 18.6 手。这是可能的最大仓位交易量。
在发布 15 手的买入订单后,在本例中,净持仓量将为 12 手(3 手为卖出,15 手为买入)。当其他模型覆盖其 3 手初始卖出持仓时,买入总持仓量将变为最大可能 - 15 手。系统将不会处理其他买入信号,直到模型覆盖其部分或全部的 15 手买入。针对要求交易的可能交易量被超出,该函数返回一个常量 EMPTY_VALUE,这样的信号必须传递。
如果对设置交易量可能性的检查成功,则计算所需预付款的值。可能账户没有足够的资金用于规定的交易量。为此,我们使用了 CheckMargin() 函数。如果预付款不足,就将尝试修改订单交易量,以便当前可用预付款允许建仓。如果预付款甚至不足以按最小量建仓,我们就会处于追加预付款状态。
如果当前没有仓位,且预付款未被使用,则只可能意味着一件事,即通过技术手段追加预付款,而在此状态下无法进行交易。如果不向账户注入资金,我们就无法继续。如果部分预付款仍在使用中,则我们只能等待使用这部分预付款的交易终止。无论如何,若缺少预付款都将返回一个常量 EMPTY_VALUE。
该函数具有一个显著特征,即能够将当前订单划分为数个独立交易订单。如果交易模型使用账户资本化系统,则所需量很容易超出所有可能的限制(例如,资本化系统可能要求进行数百,有时候数千标准手的交易)。显而易见,无法为单个交易保证如此大的交易量。通常,交易条件将最大交易规模定为 100 手,但一些交易服务器存在其他限制,例如,在 MetaQuotes Championship 2010 服务器上,这一限制为 5 手。显然,应将此类限制也考虑在内,并以此为基础正确计算交易的实际交易量。
首先计算实现设置交易量所需的订单数。如果设置交易量未超出交易的最大交易量,则只需一次传递以发布该订单。如果交易的所需交易量超出了最大可能交易量,则将该交易量分成若干部分。例如,您希望买入 11.3 手 EURUSD。该工具上交易的最大规模为 5.0 手。接下来 OrderSend 函数将该交易量拆分为三个订单:第一个订单 - 5.0 手,第二个订单 - 5.0 手,第三个订单 - 1.3 手。
因此,现在有三个订单而非一个订单。三个订单的每一个都将在订单表中列出,并具有各自独立的设置,例如止损和获利的虚拟值、幻数以及其他参数。处理这样的订单将很容易,因为交易模型正是以这样的方式设计:它们可以处理其列表中任何数量的订单。
实际上,所有订单都将具有相同的获利和止损值。它们当中的每一个都将通过 LongClose 和 ShortClose 函数依次排序。一旦出现了平仓的合适条件,或达到 SL 和 TP 阈值,它们都将被平仓。
使用 CTrade 类的 OrderSend 函数将每个订单发送至服务器。操作中最有趣的细节就隐藏在下面。
这个事实就是,订单的分配可能具有双重性。买入或卖出的订单可根据出现的信号发出,或者用于阻止之前已存在的订单。OrderSend 函数必须了解所发送订单的类型,因为该函数实际上在具体事件发生时将所有订单放入订单表或从表格中移除订单。
如果您希望添加的订单类型为 ADD_ORDER,即一个需要放入订单表的独立订单,则该函数会将这个订单的相关信息添加至订单表中。如果订单发出是为了覆盖之前下达的订单(例如发生虚拟止损),则该订单必须具有 DELETE_ORDER 类型。在订单发布后,函数 OrderSend 会手动删除该订单的相关信息,订单正是通过这些信息与订单列表实现链接。为此,除了订单类型,该函数还继承了一个订单号,并通过其进行链接。如果是 ADD_ORDER,则订单号只需用零填入。
基于穿越移动平均线的首个交易模型
我们已讨论了 CModel 基类的所有主要要素。是时候考虑具体的交易类了。
为此,我们将首先基于一个简单指标 MACD创建一个简单的交易模型。
该模型将始终具有买入持仓或卖出持仓。一旦快线向下穿越慢线,我们将建立一个卖出持仓,与此同时买入持仓(如有)将被平仓。如果是向上穿越,我们将建立一个买入持仓,与此同时卖出持仓(如有)将被平仓。在该模型中,我们不会使用保护性止损和获利水平。
#include <Models\Model.mqh> #include <mm.mqh> //+----------------------------------------------------------------------+ //| This model uses MACD indicator. | //| Buy when it crosses the zero line downward | //| Sell when it crosses the zero line upward | //+----------------------------------------------------------------------+ struct cmodel_macd_param { string symbol; ENUM_TIMEFRAMES timeframe; int fast_ema; int slow_ema; int signal_ema; }; class cmodel_macd : public CModel { private: int m_slow_ema; int m_fast_ema; int m_signal_ema; int m_handle_macd; double m_macd_buff_main[]; double m_macd_current; double m_macd_previous; public: cmodel_macd(); bool Init(); bool Init(cmodel_macd_param &m_param); bool Init(string symbol, ENUM_TIMEFRAMES timeframes, int slow_ma, int fast_ma, int smothed_ma); bool Processing(); protected: bool InitIndicators(); bool CheckParam(cmodel_macd_param &m_param); bool LongOpened(); bool ShortOpened(); bool LongClosed(); bool ShortClosed(); }; cmodel_macd::cmodel_macd() { m_handle_macd=INVALID_HANDLE; ArraySetAsSeries(m_macd_buff_main,true); m_macd_current=0.0; m_macd_previous=0.0; } //this default loader bool cmodel_macd::Init() { m_magic = 148394; m_model_name = "MACD MODEL"; m_symbol = _Symbol; m_timeframe = _Period; m_slow_ema = 26; m_fast_ema = 12; m_signal_ema = 9; m_delta = 50; if(!InitIndicators())return(false); return(true); } bool cmodel_macd::Init(cmodel_macd_param &m_param) { m_magic = 148394; m_model_name = "MACD MODEL"; m_symbol = m_param.symbol; m_timeframe = (ENUM_TIMEFRAMES)m_param.timeframe; m_fast_ema = m_param.fast_ema; m_slow_ema = m_param.slow_ema; m_signal_ema = m_param.signal_ema; if(!CheckParam(m_param))return(false); if(!InitIndicators())return(false); return(true); } bool cmodel_macd::CheckParam(cmodel_macd_param &m_param) { if(!SymbolInfoInteger(m_symbol, SYMBOL_SELECT)) { Print("Symbol ", m_symbol, " selection has failed. Check symbol name"); return(false); } if(m_fast_ema == 0) { Print("Fast EMA must be greater than 0"); return(false); } if(m_slow_ema == 0) { Print("Slow EMA must be greater than 0"); return(false); } if(m_signal_ema == 0) { Print("Signal EMA must be greater than 0"); return(false); } return(true); } bool cmodel_macd::InitIndicators() { if(m_handle_macd==INVALID_HANDLE) { Print("Load indicators..."); if((m_handle_macd=iMACD(m_symbol,m_timeframe,m_fast_ema,m_slow_ema,m_signal_ema,PRICE_CLOSE))==INVALID_HANDLE) { printf("Error creating MACD indicator"); return(false); } } return(true); } bool cmodel_macd::Processing() { //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); //if(m_account_info.TradeAllowed()==false)return(false); //if(m_account_info.TradeExpert()==false)return(false); m_symbol_info.Name(m_symbol); m_symbol_info.RefreshRates(); CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main); m_macd_current=m_macd_buff_main[0]; m_macd_previous=m_macd_buff_main[1]; GetNumberOrders(m_orders); if(m_orders.buy_orders>0) LongClosed(); else LongOpened(); if(m_orders.sell_orders!=0) ShortClosed(); else ShortOpened(); return(true); } bool cmodel_macd::LongOpened(void) { if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_SHORTONLY)return(false); if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)return(false); bool rezult, ticket_bool; double lot=0.1; mm open_mm; m_symbol_info.Name(m_symbol); m_symbol_info.RefreshRates(); CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main); m_macd_current=m_macd_buff_main[0]; m_macd_previous=m_macd_buff_main[1]; GetNumberOrders(m_orders); //Print("LongOpened"); if(m_macd_current>0&&m_macd_previous<=0&&m_orders.buy_orders==0) { //lot=open_mm.optimal_f(m_symbol, ORDER_TYPE_BUY, m_symbol_info.Ask(), 0.0, m_delta); lot=open_mm.jons_fp(m_symbol, ORDER_TYPE_BUY, m_symbol_info.Ask(), 0.1, 10000, m_delta); rezult=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_ADD, 0, lot, m_symbol_info.Ask(), 0, 0, "MACD Buy"); return(rezult); } return(false); } bool cmodel_macd::ShortOpened(void) { if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_LONGONLY)return(false); if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)return(false); bool rezult, ticket_bool; double lot=0.1; mm open_mm; m_symbol_info.Name(m_symbol); m_symbol_info.RefreshRates(); CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main); m_macd_current=m_macd_buff_main[0]; m_macd_previous=m_macd_buff_main[1]; GetNumberOrders(m_orders); if(m_macd_current<=0&&m_macd_previous>=0&&m_orders.sell_orders==0) { //lot=open_mm.optimal_f(m_symbol, ORDER_TYPE_SELL, m_symbol_info.Bid(), 0.0, m_delta); lot=open_mm.jons_fp(m_symbol, ORDER_TYPE_SELL, m_symbol_info.Bid(), 0.1, 10000, m_delta); rezult=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_ADD, 0, lot, m_symbol_info.Bid(), 0, 0, "MACD Sell"); return(rezult); } return(false); } bool cmodel_macd::LongClosed(void) { if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); CTableOrders *t; int total_elements; int rez=false; total_elements=ListTableOrders.Total(); if(total_elements==0)return(false); for(int i=total_elements-1;i>=0;i--) { if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue; t=ListTableOrders.GetNodeAtIndex(i); if(CheckPointer(t)==POINTER_INVALID)continue; if(t.Type()!=ORDER_TYPE_BUY)continue; m_symbol_info.Refresh(); m_symbol_info.RefreshRates(); CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main); if(m_symbol_info.Bid()<=t.StopLoss()&&t.StopLoss()!=0.0) { rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), m_symbol_info.Bid(), 0.0, 0.0, "MACD: buy close buy stop-loss"); } if(m_macd_current<0&&m_macd_previous>=0) { //Print("Long position closed by Order Send"); rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), m_symbol_info.Bid(), 0.0, 0.0, "MACD: buy close by signal"); } } return(rez); } bool cmodel_macd::ShortClosed(void) { if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); CTableOrders *t; int total_elements; int rez=false; total_elements=ListTableOrders.Total(); if(total_elements==0)return(false); for(int i=total_elements-1;i>=0;i--) { if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue; t=ListTableOrders.GetNodeAtIndex(i); if(CheckPointer(t)==POINTER_INVALID)continue; if(t.Type()!=ORDER_TYPE_SELL)continue; m_symbol_info.Refresh(); m_symbol_info.RefreshRates(); CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main); if(m_symbol_info.Ask()>=t.StopLoss()&&t.StopLoss()!=0.0) { rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), m_symbol_info.Ask(), 0.0, 0.0, "MACD: sell close buy stop-loss"); } if(m_macd_current>0&&m_macd_previous<=0) { rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), m_symbol_info.Ask(), 0.0, 0.0, "MACD: sell close by signal"); } } return(rez); }
CModel 基类未在其后代的内部内容上施加任何限制。其唯一设为强制性的是使用 Processing() 接口函数。该函数内部组织的所有问题都被委托给模型的一个特殊类。由于没有任何通用算法可置于 Processing() 函数中,因此也没有任何理由将其如何安排具体模型的方法加于后代。然而,几乎任何模型的内部结构都可以实现标准化。这种标准化将非常有助于理解外部甚至您的代码,并将使模型更加“公式化”。
每个模型都必须具有自己的初始化程序。模型的初始化程序负责载入模型操作所必需的适当参数,例如,为了让模型生效,我们需要选择 MACD 指标值来获取相应缓冲区的句柄,此外,当然还要确定模型的交易工具和时间表。所有这些都必须由初始化程序完成。
模型的初始化程序是模块类的简单重载方法。这些方法具有一个通用名称 Init。事实是,MQL5 不支持重载构造函数,因此无法在模型的构造函数中创建模型的初始化程序,因为重载对输入参数十分重要。虽然没有人限制我们在模型的构造函数中指定其基本参数。
每个模型都应具有三个初始化程序。第一个程序是默认初始化程序。默认情况下,该程序必须配置和加载模型,而无需提供参数。这在“现状”模式下测试时可能十分方便。例如,对于我们的模型,默认初始化程序作为模型的工具和时间表,将选择当前图表和当前时间表。
MACD 指标的设置同样将是标准设置:fast EMA = 12,slow EMA = 26,signal MA = 9;如果模型需要按特定方式配置,这样的初始化程序就不再合适了。我们需要具有参数的初始化程序。有两种类型是可取的(但不是必须的)。第一种将作为一个经典函数接收其参数:Init (类型 param1、类型 param2、...、类型 paramN)。第二种将使用一种特殊结构查找并保存模型的参数。有时该选项更加可取,因为有时参数数量可能很大,在这种情况下,通过结构传递这些参数会比较方便。
每个模型都具有一定的参数结构。该结构可以采用任意名称,但按照 modelname_param 模式命名将更为可取。配置模型是利用多时间表/多系统/多货币交易机会的极为重要的一步。正是在这个阶段确定模型如何交易以及在什么工具上交易。
我们的交易模型仅涉及四个交易函数。用于建买入持仓的函数:LongOpen;用于建卖出持仓的函数:ShortOpen;用于平买入持仓的函数:LongClosed;以及用于平卖出持仓的函数:ShortClosed。函数 LongOpen 和 ShortOpen 的功能不太重要。两个函数接收上一个柱的 MACD 指标值,然后与该柱的前两个柱的相应值进行比较。为避免“重复绘制”,将不使用当前柱(零柱)。
如果是向下穿越,则 ShortOpencalculates 函数会使用包含在头文件 mm.mqh 中的函数,之后必需的手数会发送其命令至 OrderSend 函数。相反,此时 LongClose 会结算模型中的所有买入持仓订单。这是因为该函数会按顺序对模型订单表中的所有当前未结订单进行排序。如果找到买入持仓订单,则该函数会使用回报订单结算该订单。ShortClose() 函数完成了同样的功能,不过是在相反的方向上。这些函数的作用可在上文提供的列表中找到。
我们来仔细研究下当前手数是如何在交易模型中计算得出的。
如前文所述,基于这些目的我们会使用特殊函数来实现账户的资本化。除包含资本化公式以外,这些函数还包含了对手数计算的验证,基于所使用的预付款水平和交易仓位的大小限制。可能会存在一些对仓位大小的交易限制。应该将这些限制纳入考虑范围。
应考虑下述情形:对于在两个方向上均为 15 标准手的交易仓位,存在着大小限制。当前仓位为 3 手买入持仓。基于自身资金管理系统的交易模型希望建立一个交易量为 18.6 手的买入持仓。CheckLot() 函数将返回修正后的订单交易量。在本例中,它将等于 12 手(因为其他交易已经占据了 15 手中的 3 手)。如果当前的敞口仓位为卖出持仓而不是买入持仓,则函数将返回 15 手而非 18.6 手。这是最大的可能仓位交易量。
在发布 15 手的买入订单后,在本例中,净持仓量将为 12 手(3 手为卖出,15 手为买入)。当其他模型覆盖其 3 手初始卖出持仓时,买入总持仓量将变为最大可能 - 15 手。系统将不会处理其他买入信号,直到模型覆盖其部分或全部的 15 手买入。如果请求交易的可用交易量已用完,则函数将返回一个 EMPTY_VALUE 常量。该信号必须进行传递。
如果对设置交易量可能性的检查成功,则计算所需预付款的值。可能账户没有足够的资金用于设置的交易量。为此,我们使用了 CheckMargin() 函数。如果预付款不足,就将尝试修改交易规定的交易量,以便当前可用预付款允许建仓。如果预付款甚至不足以按最小交易量建仓,我们处于追加预付款Margin-状态。
如果当前没有仓位,且预付款未被使用,则只可能意味着一件事,即通过技术手段追加预付款,而在此状态下无法进行交易。如果不向账户注入资金,我们就无法继续。如果部分预付款仍处于使用中,则我们只能等待使用这部分预付款的交易实现平仓。无论如何,若缺少预付款都将返回一个常量 EMPTY_VALUE。
控制手数大小和预付款的函数通常都不会直接调用,而是通过特殊函数调用,以便能管理资金。这些函数使用了公式,以实现账户的资本化。文件 mm.mqh 仅包含资金管理的两个基本函数,其中一个基于账户的固定份额进行计算,称为固定比例法,另一个则基于 Ryan Jones 提出的方法。
第一种方法旨在详细说明账户的一个固定部分,该部分可能会面临风险。例如,如果允许风险为账户的 2% 且账户金额等于 10,000 美元,则最大风险量为 200 美元。要计算对应于 200 美元止损的手数,您需要了解价格相对于已建仓位可达到的最大精确距离。因此,要通过该公式计算手数,我们就需要准确地确定止损和进行交易的价格水平。
Ryan Jones 提出的方法与前者不同。其本质是,资本化由通过一个二次方程特例所定义的函数来完成。
其解答如下:
x=((1.0+MathSqrt(1+4.0*d))/2)*Step;
其中:x - 过渡到下一水平的下限 d = (盈利 / delta) * 2.0 Step- delta 的一个梯级,例如 0.1 手。
delta 越小,该函数就越积极尝试增加仓位的数量。有关该函数如何构建的更多详情,读者可以参阅 Ryan Jones 的著述:《The Trading Game:Playing by the Numbers to Make Millions》。
如果未打算使用资金管理函数,则需要直接调用可控制手数和预付款的函数。
至此,我们已论述了基本 EA 交易的所有要素。是时候收获我们的努力成果了。
首先,让我们建立四个模型。我们让其中一个模型按照 EURUSD 默认参数进行交易,另一个模型同样在 EURUSD 上交易,但是却发生在 15 分钟时间表上。第三个模型将在 GBPUSD 图上启动,并采用默认参数。第四个模型在 USDCHF 的两小时图上进行交易,其参数如下:SlowEMA= 6 FastEMA = 12 SignalEMA = 9。测试周期 - H1,测试模式 - 从 01.01.2010 到 01.09.2010 的所有订单号。
在四种不同模式下运行该模型前,首先我们将尝试根据每个工具和时间表对该模型分别进行测试。
下表显示了主要测试指标:
系统 | 交易数量 | 盈利(以美元计) |
---|---|---|
MACD(9,12,26)H1 EURUSD | 123 | 1092 |
MACD (9,12,26) EURUSD M15 | 598 | -696 |
MACD(9,6,12) USDCHF H2 | 153 | -1150 |
MACD(9,12,26) GBPUSD H1 | 139 | -282 |
所有系统 | 1013 | -1032 |
上表显示所有模型的总交易数量为 1013,总盈利为 -1032 美元。
因此,如果我们同时测试这些系统,这些也同样是我们应获得的值。结果不应该有所差异,尽管仍会有一些细微差别存在。
下面是最终测试:
可以看出,上图仅有一笔较小交易,其盈利仅相差 10 美元,在 0.1 手的情况下相当于 10 个点位的差异。应该注意的是,在使用资金管理系统的情况下,组合测试的结果与每个模型的单独测试结果总和有着根本性差异。这是因为余额的动态性影响到了每个系统,因此计算出的手数值将发生变化。
尽管结果本身并未引起我们的兴趣,但我们仍创建了一个复杂但极其灵活且可管理的 EA 交易结构。让我们快速地回顾一下其结构。
为此,让我们转到下面的示意图:
该示意图显示了模型实例的基本结构。
一旦创建了交易模型的类实例,我们就会调用重载的 Init() 函数。该函数将初始化必要参数、准备数据并加载指标(如有使用)的句柄。所有这些都发生在 EA 交易的初始化阶段,即位于 OnInit() 函数的内部。请注意,数据包含基类的实例,其目的是促进交易。人们认为交易模型需要主动使用这些类,而不是使用 MQL5 的标准函数。在成功创建和初始化模型类后,该类会进入模型列表 CList。如要与该类进一步通信,可借助通用适配器 CObject 来完成。
在 OnTrade() 或 OnTick() 事件出现后,将会对列表中的所有模型实例进行顺序排序。与这些实例之间的通信会通过调用 Processing() 函数来完成。此外,它还会调用自身模型的交易函数(蓝色的函数组)。它们的清单和名称未严格定义,但使用 LongOpened()、ShortClosed() 等标准名称更为方便。这些函数基于其内嵌的逻辑选择交易完成时间,然后再发送 SendOrder() 函数的特殊格式的请求用以建仓或平仓。
后者会进行必要的检查,然后将订单输出到市场。交易函数依赖于模型的辅助函数(绿色组),而后者反过来会主动使用基本辅助类(紫色组)。在数据部分,所有辅助类都以类的实例来表示(粉色组)。组与组之间的交互通过深蓝色箭头标示。
基于布林带指标的交易模型
数据和方法的总体结构现已明晰,我们将创建基于布林带趋势指标的另一个交易模型。作为该交易模型的基础,我们采用了 Andrei Kornishkin 提出的一个简单的 EA 交易,即基于布林带的 EA 交易。布林带属于水平的一种,是相对于简单移动平均线的一定大小的标准偏差。有关如何构建该指标的更多详情,可参见附于 MetaTrader 5 客户端的“帮助”部分以用于技术分析。
交易概念的本质十分简单:价格具有返回属性,即如果价格达到一定水平,就很可能会向相反的方向变化。该理论通过任意实际交易工具的正态分布测试进行了验证:正态分布曲线可能会稍有延长。布林带确定了价格水平最可能的顶点。一旦达到(高于或低于布林带)这些顶点,价格就很可能转向相反方向。
我们对交易策略稍加简化,并且不会使用辅助指标 - 双指数移动平均线(或 DEMA)。不过我们将采取严格的保护性止损措施,即虚拟止损。这些止损措施将使交易过程更稳定,同时帮助我们理解示例,其中每个交易模型都会使用自己独立的保护性止损水平。
对于保护性止损水平,我们使用当前价格加上或减去指标值的波动 ATR。例如,如果 ATR 的当前值等于 68 点,并且有一个价格为 1.25720 的卖出信号,则该交易的虚拟止损为 1.25720 + 0.0068 = 1.26400。买入情况与此类似,但方向相反:1.25720 - 0.0068 = 1.25040。
该模型的源代码如下:
#include <Models\Model.mqh> #include <mm.mqh> //+----------------------------------------------------------------------+ //| This model use Bollinger bands. //| Buy when price is lower than lower band //| Sell when price is higher than upper band //+----------------------------------------------------------------------+ struct cmodel_bollinger_param { string symbol; ENUM_TIMEFRAMES timeframe; int period_bollinger; double deviation; int shift_bands; int period_ATR; double k_ATR; double delta; }; class cmodel_bollinger : public CModel { private: int m_bollinger_period; double m_deviation; int m_bands_shift; int m_ATR_period; double m_k_ATR; //------------Indicators Data:------------- int m_bollinger_handle; int m_ATR_handle; double m_bollinger_buff_main[]; double m_ATR_buff_main[]; //----------------------------------------- MqlRates m_raters[]; double m_current_price; public: cmodel_bollinger(); bool Init(); bool Init(cmodel_bollinger_param &m_param); bool Init(ulong magic, string name, string symbol, ENUM_TIMEFRAMES TimeFrame, double delta, uint bollinger_period, double deviation, int bands_shift, uint ATR_period, double k_ATR); bool Processing(); protected: bool InitIndicators(); bool CheckParam(cmodel_bollinger_param &m_param); bool LongOpened(); bool ShortOpened(); bool LongClosed(); bool ShortClosed(); bool CloseByStopSignal(); }; cmodel_bollinger::cmodel_bollinger() { m_bollinger_handle = INVALID_HANDLE; m_ATR_handle = INVALID_HANDLE; ArraySetAsSeries(m_bollinger_buff_main,true); ArraySetAsSeries(m_ATR_buff_main,true); ArraySetAsSeries(m_raters, true); m_current_price=0.0; } //this default loader bool cmodel_bollinger::Init() { m_magic = 322311; m_model_name = "Bollinger Bands Model"; m_symbol = _Symbol; m_timeframe = _Period; m_bollinger_period = 20; m_deviation = 2.0; m_bands_shift = 0; m_ATR_period = 20; m_k_ATR = 2.0; m_delta = 0; if(!InitIndicators())return(false); return(true); } bool cmodel_bollinger::Init(cmodel_bollinger_param &m_param) { m_magic = 322311; m_model_name = "Bollinger Model"; m_symbol = m_param.symbol; m_timeframe = (ENUM_TIMEFRAMES)m_param.timeframe; m_bollinger_period = m_param.period_bollinger; m_deviation = m_param.deviation; m_bands_shift = m_param.shift_bands; m_ATR_period = m_param.period_ATR; m_k_ATR = m_param.k_ATR; m_delta = m_param.delta; //if(!CheckParam(m_param))return(false); if(!InitIndicators())return(false); return(true); } bool cmodel_bollinger::Init(ulong magic, string name, string symbol, ENUM_TIMEFRAMES timeframe, double delta, uint bollinger_period, double deviation, int bands_shift, uint ATR_period, double k_ATR) { m_magic = magic; m_model_name = name; m_symbol = symbol; m_timeframe = timeframe; m_delta = delta; m_bollinger_period= bollinger_period; m_deviation = deviation; m_bands_shift = bands_shift; m_ATR_period = ATR_period; m_k_ATR = k_ATR; if(!InitIndicators())return(false); return(true); } /*bool cmodel_bollinger::CheckParam(cmodel_bollinger_param &m_param) { if(!SymbolInfoInteger(m_symbol, SYMBOL_SELECT)){ Print("Symbol ", m_symbol, " select failed. Check valid name symbol"); return(false); } if(m_ma == 0){ Print("Fast EMA must be bigest 0. Set MA = 12 (default)"); m_ma=12; } return(true); }*/ bool cmodel_bollinger::InitIndicators() { m_bollinger_handle=iBands(m_symbol,m_timeframe,m_bollinger_period,m_bands_shift,m_deviation,PRICE_CLOSE); if(m_bollinger_handle==INVALID_HANDLE){ Print("Error in creation of Bollinger indicator. Restart the Expert Advisor."); return(false); } m_ATR_handle=iATR(m_symbol,m_timeframe,m_ATR_period); if(m_ATR_handle==INVALID_HANDLE){ Print("Error in creation of ATR indicator. Restart the Expert Advisor."); return(false); } return(true); } bool cmodel_bollinger::Processing() { //if(timing(m_symbol,m_timeframe, m_timing)==false)return(false); //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); //if(m_account_info.TradeAllowed()==false)return(false); //if(m_account_info.TradeExpert()==false)return(false); //m_symbol_info.Name(m_symbol); //m_symbol_info.RefreshRates(); //Copy last data of moving average GetNumberOrders(m_orders); if(m_orders.buy_orders>0) LongClosed(); else LongOpened(); if(m_orders.sell_orders!=0) ShortClosed(); else ShortOpened(); if(m_orders.all_orders!=0)CloseByStopSignal(); return(true); } bool cmodel_bollinger::LongOpened(void) { //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_SHORTONLY)return(false); //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)return(false); //Print("Model Bollinger: ", m_orders.buy_orders); bool rezult, time_buy=true; double lot=0.1; double sl=0.0; double tp=0.0; mm open_mm; m_symbol_info.Name(m_symbol); m_symbol_info.RefreshRates(); //lot=open_mm.optimal_f(m_symbol,OP_BUY,m_symbol_info.Ask(),sl,delta); CopyBuffer(m_bollinger_handle,2,0,3,m_bollinger_buff_main); CopyBuffer(m_ATR_handle,0,0,3,m_ATR_buff_main); CopyRates(m_symbol,m_timeframe,0,3,m_raters); if(m_raters[1].close>m_bollinger_buff_main[1]&&m_raters[1].open<m_bollinger_buff_main[1]) { sl=NormalizeDouble(m_symbol_info.Ask()-m_ATR_buff_main[0]*m_k_ATR,_Digits); SendOrder(m_symbol,ORDER_TYPE_BUY,ORDER_ADD,0,lot,m_symbol_info.Ask(),sl,tp,"Add buy"); } return(false); } bool cmodel_bollinger::ShortOpened(void) { //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_LONGONLY)return(false); //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)return(false); bool rezult, time_sell=true; double lot=0.1; double sl=0.0; double tp; mm open_mm; m_symbol_info.Name(m_symbol); m_symbol_info.RefreshRates(); CopyBuffer(m_bollinger_handle,1,0,3,m_bollinger_buff_main); CopyBuffer(m_ATR_handle,0,0,3,m_ATR_buff_main); CopyRates(m_symbol,m_timeframe,0,3,m_raters); if(m_raters[1].close<m_bollinger_buff_main[1]&&m_raters[1].open>m_bollinger_buff_main[1]) { sl=NormalizeDouble(m_symbol_info.Bid()+m_ATR_buff_main[0]*m_k_ATR,_Digits); SendOrder(m_symbol,ORDER_TYPE_SELL,ORDER_ADD,0,lot,m_symbol_info.Ask(),sl,tp,"Add buy"); } return(false); } bool cmodel_bollinger::LongClosed(void) { if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); CTableOrders *t; int total_elements; int rez=false; total_elements=ListTableOrders.Total(); if(total_elements==0)return(false); m_symbol_info.Name(m_symbol); m_symbol_info.RefreshRates(); CopyBuffer(m_bollinger_handle,1,0,3,m_bollinger_buff_main); CopyBuffer(m_ATR_handle,0,0,3,m_ATR_buff_main); CopyRates(m_symbol,m_timeframe,0,3,m_raters); if(m_raters[1].close<m_bollinger_buff_main[1]&&m_raters[1].open>m_bollinger_buff_main[1]) { for(int i=total_elements-1;i>=0;i--) { if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue; t=ListTableOrders.GetNodeAtIndex(i); if(CheckPointer(t)==POINTER_INVALID)continue; if(t.Type()!=ORDER_TYPE_BUY)continue; m_symbol_info.Refresh(); m_symbol_info.RefreshRates(); rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), m_symbol_info.Bid(), 0.0, 0.0, "BUY: close by signal"); } } return(rez); } bool cmodel_bollinger::ShortClosed(void) { if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); CTableOrders *t; int total_elements; int rez=false; total_elements=ListTableOrders.Total(); if(total_elements==0)return(false); CopyBuffer(m_bollinger_handle,2,0,3,m_bollinger_buff_main); CopyBuffer(m_ATR_handle,0,0,3,m_ATR_buff_main); CopyRates(m_symbol,m_timeframe,0,3,m_raters); if(m_raters[1].close>m_bollinger_buff_main[1]&&m_raters[1].open<m_bollinger_buff_main[1]) { for(int i=total_elements-1;i>=0;i--) { if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue; t=ListTableOrders.GetNodeAtIndex(i); if(CheckPointer(t)==POINTER_INVALID)continue; if(t.Type()!=ORDER_TYPE_SELL)continue; m_symbol_info.Refresh(); m_symbol_info.RefreshRates(); rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), m_symbol_info.Ask(), 0.0, 0.0, "SELL: close by signal"); } } return(rez); } bool cmodel_bollinger::CloseByStopSignal(void) { if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); CTableOrders *t; int total_elements; bool rez=false; total_elements=ListTableOrders.Total(); if(total_elements==0)return(false); for(int i=total_elements-1;i>=0;i--) { if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue; t=ListTableOrders.GetNodeAtIndex(i); if(CheckPointer(t)==POINTER_INVALID)continue; if(t.Type()!=ORDER_TYPE_SELL&&t.Type()!=ORDER_TYPE_BUY)continue; m_symbol_info.Refresh(); m_symbol_info.RefreshRates(); CopyRates(m_symbol,m_timeframe,0,3,m_raters); if(m_symbol_info.Bid()<=t.StopLoss()&&t.Type()==ORDER_TYPE_BUY) { rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), m_symbol_info.Bid(), 0.0, 0.0, "BUY: close by stop"); continue; } if(m_symbol_info.Ask()>=t.StopLoss()&&t.Type()==ORDER_TYPE_SELL) { rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), m_symbol_info.Ask(), 0.0, 0.0, "SELL: close by stop"); continue; } } return(rez); }
可以看出,交易模型的代码与上一个交易策略的源代码极其相似。后者的主要变化在于出现了虚拟止损订单以及可用于这些保护性止损的 cmodel_bollinger::CloseByStopSignal() 函数。
事实上,在使用保护性止损的情况下,它们的值必须仅传递至函数 SendOrder()。而该函数会将这些水平输入到订单表中。当价格穿越或达到这些水平时,函数 CloseByStopSignal() 将使用回报订单完成交易,并将该订单从活动订单列表中移除。
将交易模型、工具和时间表组合到一个实体中
现在我们有了两个交易模型,是时候对其进行同步测试了。在我们确定模型的布局前,先确定其最有效的参数将十分有用。为此,我们必须对每个模型分别进行优化。
优化将说明模型在哪些市场和时间表上最为高效。对于每个模型,我们都将选择两个最优时间表和三个最优工具。结果我们得到 12 个独立解决方案(2 个模型 * 3 个工具 * 2 个时间表),并将其放在一起进行测试。当然,所选的优化方法受制于所谓的结果“调整”,但这对于我们的目的而言并不重要。
以下图表显示了样本的最佳结果:
1.1 MACD EURUSD M30
1.2 . MACD EURUSD H3
1.3 MACD AUDUSD H4
1.4 . MACD AUDUSD H1
1.5 MACD GBPUSD H12
1.6 MACD GBPUSD H6
2.1 Bollinger GBPUSD M15
2.2 Bollinger GBPUSD H1
2.3 Bollinger EURUSD M30
2.4 Bollinger EURUSD H4
2.5 Bollinger USDCAD M15
2.6 Bollinger USDCAD H2
现在已经知道了最佳结果,我们只需要将结果组合到一个实体中即可。
下面是函数加载程序的源代码,它创建了如上图所示的 12 个交易模型,之后 EA 交易便一直使用这些模型进行:
bool macd_default=true; bool macd_best=false; bool bollinger_default=false; bool bollinger_best=false; void InitModels() { list_model = new CList; // Initialized pointer of the model list cmodel_macd *model_macd; // Create the pointer to a model MACD cmodel_bollinger *model_bollinger; // Create the pointer to a model Bollinger //----------------------------------------MACD DEFAULT---------------------------------------- if(macd_default==true&&macd_best==false) { model_macd = new cmodel_macd; // Initialize the pointer by the model MACD // Loading of the parameters was completed successfully if(model_macd.Init(129475, "Model macd M15", _Symbol, _Period, 0.0, Fast_MA,Slow_MA,Signal_MA)) { Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " successfully created"); list_model.Add(model_macd);// Загружаем модель в список моделей } else { // The loading of parameters was completed successfully Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " creation has failed"); } } //------------------------------------------------------------------------------------------- //----------------------------------------MACD BEST------------------------------------------ if(macd_best==true&&macd_default==false) { // 1.1 EURUSD H30; FMA=20; SMA=24; model_macd = new cmodel_macd; // Initialize the pointer to the model MACD if(model_macd.Init(129475, "Model macd H30", "EURUSD", PERIOD_M30, 0.0, 20,24,9)) { Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " created successfully"); list_model.Add(model_macd);// load the model into the list of models } else {// Loading parameters was completed unsuccessfully Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " creation has failed"); } // 1.2 EURUSD H3; FMA=8; SMA=12; model_macd = new cmodel_macd; // Initialize the pointer by the model MACD if(model_macd.Init(129475, "Model macd H3", "EURUSD", PERIOD_H3, 0.0, 8,12,9)) { Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " successfully created"); list_model.Add(model_macd);// Load the model into the list of models } else {// Loading of parameters was unsuccessful Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " creation has failed"); } // 1.3 AUDUSD H1; FMA=10; SMA=18; model_macd = new cmodel_macd; // Initialize the pointer by the model MACD if(model_macd.Init(129475, "Model macd M15", "AUDUSD", PERIOD_H1, 0.0, 10,18,9)) { Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " successfully created"); list_model.Add(model_macd);// Load the model into the list of models } else {// The loading of parameters was unsuccessful Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " creation has failed"); } // 1.4 AUDUSD H4; FMA=14; SMA=15; model_macd = new cmodel_macd; // Initialize the pointer by the model MACD if(model_macd.Init(129475, "Model macd H4", "AUDUSD", PERIOD_H4, 0.0, 14,15,9)) { Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " successfully created"); list_model.Add(model_macd);// Load the model into the list of models } else{// Loading of parameters was unsuccessful Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " creation has failed"); } // 1.5 GBPUSD H6; FMA=20; SMA=33; model_macd = new cmodel_macd; // Initialize the pointer by the model MACD if(model_macd.Init(129475, "Model macd H6", "GBPUSD", PERIOD_H6, 0.0, 20,33,9)) { Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " successfully created"); list_model.Add(model_macd);// Load the model into the list of models } else {// Loading of parameters was unsuccessful Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " creation has failed"); } // 1.6 GBPUSD H12; FMA=12; SMA=30; model_macd = new cmodel_macd; // Initialize the pointer by the model MACD if(model_macd.Init(129475, "Model macd H6", "GBPUSD", PERIOD_H12, 0.0, 12,30,9)) { Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " successfully created"); list_model.Add(model_macd);// Load the model into the list of models } else {// Loading of parameters was unsuccessful Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), " on symbol ", model_macd.Symbol(), " creation has failed"); } } //---------------------------------------------------------------------------------------------- //-------------------------------------BOLLINGER DEFAULT---------------------------------------- if(bollinger_default==true&&bollinger_best==false) { model_bollinger = new cmodel_bollinger; if(model_bollinger.Init(1829374,"Bollinger",_Symbol,PERIOD_CURRENT,0, period_bollinger,dev_bollinger,0,14,k_ATR)) { Print("Model ", model_bollinger.Name(), " successfully created"); list_model.Add(model_bollinger); } } //---------------------------------------------------------------------------------------------- //--------------------------------------BOLLLINGER BEST----------------------------------------- if(bollinger_best==true&&bollinger_default==false) { //2.1 Symbol: EURUSD M30; period: 15; deviation: 2,75; k_ATR=2,75; model_bollinger = new cmodel_bollinger; if(model_bollinger.Init(1829374,"Bollinger","EURUSD",PERIOD_M30,0,15,2.75,0,14,2.75)) { Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(), ". Symbol: ", model_bollinger.Symbol(), " successfully created"); list_model.Add(model_bollinger); } //2.2 Symbol: EURUSD H4; period: 30; deviation: 2.0; k_ATR=2.25; model_bollinger = new cmodel_bollinger; if(model_bollinger.Init(1829374,"Bollinger","EURUSD",PERIOD_H4,0,30,2.00,0,14,2.25)) { Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(), ". Symbol: ", model_bollinger.Symbol(), " successfully created"); list_model.Add(model_bollinger); } //2.3 Symbol: GBPUSD M15; period: 18; deviation: 2.25; k_ATR=3.0; model_bollinger = new cmodel_bollinger; if(model_bollinger.Init(1829374,"Bollinger","GBPUSD",PERIOD_M15,0,18,2.25,0,14,3.00)) { Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(), ". Symbol: ", model_bollinger.Symbol(), " successfully created"); list_model.Add(model_bollinger); } //2.4 Symbol: GBPUSD H1; period: 27; deviation: 2.25; k_ATR=3.75; model_bollinger = new cmodel_bollinger; if(model_bollinger.Init(1829374,"Bollinger","GBPUSD",PERIOD_H1,0,27,2.25,0,14,3.75)) { Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(), ". Symbol: ", model_bollinger.Symbol(), " successfully created"); list_model.Add(model_bollinger); } //2.5 Symbol: USDCAD M15; period: 18; deviation: 2.5; k_ATR=2.00; model_bollinger = new cmodel_bollinger; if(model_bollinger.Init(1829374,"Bollinger","USDCAD",PERIOD_M15,0,18,2.50,0,14,2.00)) { Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(), ". Symbol: ", model_bollinger.Symbol(), " successfully created"); list_model.Add(model_bollinger); } //2.6 Symbol: USDCAD M15; period: 21; deviation: 2.5; k_ATR=3.25; model_bollinger = new cmodel_bollinger; if(model_bollinger.Init(1829374,"Bollinger","USDCAD",PERIOD_H2,0,21,2.50,0,14,3.25)) { Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(), ". Symbol: ", model_bollinger.Symbol(), " successfully created"); list_model.Add(model_bollinger); } } //---------------------------------------------------------------------------------------------- }
现在,我们同时对 12 个模型进行测试:
资本化结果
结果图表令人印象深刻。然而,重要的并非结果,而是所有模型同时交易这一事实 - 所有模型都使用其各自的保护性止损措施,并做到了相互独立。现在,我们尝试来资本化结果图表。为此,我们将使用标准的资本化函数:固定比例法和 Ryan Jones 所提方法。
所谓的 最优 f 是固定比例法的一个特例。该方法的本质是,为每笔交易设置一个等于账户百分比的损失限制值。保守的资本化策略通常会施加 2% 的损失限制值,即以 10,000 美元的仓位大小计算为例,损失在止损通过后不能超过 200 美元。然而,却存在一个随风险加大最后余额增长的函数。该函数为钟形函数,即一开始总盈利会随着风险的提升而增长。然而,每笔交易都存在风险阈值,在达到阈值后总盈利余额会开始下降。该阈值是所谓的最优 f。
本文并未专门讲解资金管理问题,使用固定比例法时,我们只需了解保护性止损水平和可承受风险的账户比例即可。Ryan Jones 公式的构造则有所不同。运行该公式时无需采用固定保护性止损措施。由于建议的第一个模型(MACD 模型)相当落后且没有保护性止损,我们将使用此方法来实现该模型的资本化。对于基于布林带实现的模型,我们将使用固定比例法。
要开始使用资本化公式,我们需要填入包含在基本模型中的 m_delta 变量。
使用固定比例公式时,它必须等于每笔交易的风险百分比。使用 Ryan Jones 的方法时,它等于所谓的 delta 增量,即获得更高水平的仓位交易量需赚得的资金量。
资本化图表如下所示:
可以看出,所有模型都具有自己的资本化公式(固定比例法或 Ryan Jones 方法)。
在所提供的示例中,我们在所有模型中都使用了相同的最大风险值和 delta 值。然而,对于所有模型,我们都可以选择特定参数用于资本化。对于各个模型的微调不在本文讨论的范围内。
操作挂单
上文提供的交易模型没有使用所谓的挂单。挂单是在满足特定条件或出现特定情况时执行的订单。
实际上,任何使用挂单的交易策略都可经过调整以在市场中使用订单。需要使用挂单来提升系统操作的可靠性,因为当交易机器人或挂单在其上操作的设备发生故障时,挂单仍将采取保护性止损措施,或反之亦然,即将基于之前确定的价格进入市场。
此提议的交易模型允许您使用挂单,虽然在本例中其演示的控制过程要复杂得多。要使用这些订单,我们使用 CTableOrders 类的重载方法 Add (COrderInfo & order_info, double stop_loss, double take_profit)。在本例中,该类的变量 m_type 将包含合适类型的挂单,例如 ORDER_TYPE_BUY_STOP 或 ORDER_TYPE_SELL_LIMIT。
之后,当挂单将要发布时,您需要“把握住”挂单经触发开始操作或其相关性将要到期的那一瞬间。这并没有那么容易,因为仅仅控制事件 Trade 是不够的,还要知道是什么触发了该事件。
MQL 5 语言在不断发展,目前在其中包含特殊服务结构的问题已得到了解决,该特殊结构将解释事件 Trade。但现在,我们需要在历史数据中查看订单列表。如果在历史数据中找到了与订单表中的挂单具有相同订单号的订单,则发生了一些需要反映在表格中的事件。
基本模型的代码包含一个特殊方法 CModel::ReplaceDelayedOrders。该方法基于下列算法操作。首先,检查订单表中的所有活动订单。将这些订单的订单号和历史数据 (HistoryOrderGetTicket()) 中订单的订单号进行比较。如果历史数据中订单的订单号和订单表中挂单的订单号相同,但订单状态为已执行(ORDER_STATE_PARTIAL 或 ORDER_STATE_FILLED),则订单表中挂单的状态同样更改为已执行。
此外,如果该订单未关联任何模拟止损和获利操作的挂单,该订单将被发布,且其订单号会输入到相应的表格值(TicketSL、TicketTP)中。而模拟保护性止损和获利水平的挂单,将以在 m_sl 和 m_tp 变量帮助下预先指定的价格发布,即这些价格在调用方法 ReplaceDelayedOrders 时应该是已知的。
值得注意的是,该方法适于处理以下情形,此时一个订单会发布到市场,且市场所要求的挂单属于止损和获利类型。
一般而言,所提议方法的操作并非无关紧要,并要求对方法的使用有一定认识:
bool CModel::ReplaceDelayedOrders(void) { if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false); CTableOrders *t; int total_elements; int history_orders=HistoryOrdersTotal(); ulong ticket; bool rez=false; long request; total_elements=ListTableOrders.Total(); int try=0; if(total_elements==0)return(false); // View every order in the table for(int i=total_elements-1;i>=0;i--) { if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue; t=ListTableOrders.GetNodeAtIndex(i); if(CheckPointer(t)==POINTER_INVALID)continue; switch(t.Type()) { case ORDER_TYPE_BUY: case ORDER_TYPE_SELL: for(int b=0;i<history_orders;b++) { ticket=HistoryOrderGetTicket(b); // If the ticket of the historical order is equal to one of the tickets // Stop Loss or Take Profit, then the order was blocked, and it needs to be // deleted from the table of orders if(ticket==t.TicketSL()||ticket==t.TicketTP()) { ListTableOrders.DeleteCurrent(); } } // If the orders, imitating the Stop Loss and Take Profit are not found in the history, // then perhaps they are not yet set. Therefore, they need to be inputted, // using the process for pending orders below: // the cycle keeps going, the exit 'break' does not exist!!! case ORDER_TYPE_BUY_LIMIT: case ORDER_TYPE_BUY_STOP: case ORDER_TYPE_BUY_STOP_LIMIT: case ORDER_TYPE_SELL_LIMIT: case ORDER_TYPE_SELL_STOP: case ORDER_TYPE_SELL_STOP_LIMIT: for(int b=0;i<history_orders;b++) { ticket=HistoryOrderGetTicket(b); // If the ticket of the historical order is equal to the ticket of the pending order // then the pending order has worked and needs to be put out // the pending orders, imitating the work of Stop Loss and Take Profit. // It is also necessary to change the pending status of the order in the table // of orders for the executed (ORDER_TYPE_BUY или ORDER_TYPE_SELL) m_order_info.InfoInteger(ORDER_STATE,request); if(t.Ticket()==ticket&& (request==ORDER_STATE_PARTIAL||request==ORDER_STATE_FILLED)) { // Change the status order in the table of orders: m_order_info.InfoInteger(ORDER_TYPE,request); if(t.Type()!=request)t.Type(request); //------------------------------------------------------------------ // Put out the pending orders, imitating Stop Loss an Take Profit: // The level of pending orders, imitating Stop Loss and Take Profit // should be determined earlier. It is also necessary to make sure that // the current order is not already linked with the pending order, imitating Stop Loss // and Take Profit: if(t.StopLoss()!=0.0&&t.TicketSL()==0) { // Try to put out the pending order: switch(t.Type()) { case ORDER_TYPE_BUY: // Make three attempts to put out the pending order for(try=0;try<3;try++) { m_trade.SellStop(t.VolumeInitial(),t.StopLoss(),m_symbol,0.0,0.0,0,0,"take-profit for buy"); if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE) { t.TicketTP(m_trade.ResultDeal()); break; } } case ORDER_TYPE_SELL: // Make three attempts to put up a pending order for(try=0;try<3;try++) { m_trade.BuyStop(t.VolumeInitial(),t.StopLoss(),m_symbol,0.0,0.0,0,0,"take-profit for buy"); if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE) { t.TicketTP(m_trade.ResultDeal()); break; } } } } if(t.TakeProfit()!=0.0&&t.TicketTP()==0){ // Attempt to put out the pending order, imitating Take Profit: switch(t.Type()) { case ORDER_TYPE_BUY: // Make three attempts to put out the pending order for(try=0;try<3;try++) { m_trade.SellLimit(t.VolumeInitial(),t.StopLoss(),m_symbol,0.0,0.0,0,0,"take-profit for buy"); if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE) { t.TicketTP(m_trade.ResultDeal()); break; } } break; case ORDER_TYPE_SELL: // Make three attempts to put out the pending order for(try=0;try<3;try++) { m_trade.BuyLimit(t.VolumeInitial(),t.StopLoss(),m_symbol,0.0,0.0,0,0,"take-profit for buy"); if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE) { t.TicketTP(m_trade.ResultDeal()); break; } } } } } } break; } } return(true); }
使用该方法,您可以轻松创建一个基于挂单的模型。
总结
遗憾的是,我们无法在一篇文章中对提出方法的所有细微差别、挑战和益处做到面面俱到。我们没有考虑数据的序列化 - 允许您在模型的当前状态从数据文件存储、记录和获取所有必要信息的一种方法。在我们的讨论范围之外,还有基于合成点差进行交易的交易模型。这些话题都挺有趣,当然,针对提出的概念,它们也有自己的高效解决方案。
我们的主要目标是开发一个完全动态和可管理的数据结构。链接表的概念可实现高效管理,令交易策略相互独立并可以单独定制。该方法的另一重要优势在于其完全通用。
例如,在其基础上创建两个 EA 交易并将其置于同一工具上便已足够。两个 EA 交易只会自动处理自己的订单,并仅使用自己的资金管理系统。因此该方法支持向下兼容。在一个 EA 交易中同时处理的所有工作可分配给多个机器人处理。该属性在处理净持仓量时极其重要。
我们介绍的模型并不仅仅是一个理论。它包含先进的辅助函数工具,以及用于管理资金和检查预付款要求的函数。订单发送系统对所谓的报价和滑点数效具有耐受性,其效应经常可在实际交易中常见。
交易引擎确定了仓位的最大规模和交易的最大交易量。一个特殊算法可将交易请求划分为数个独立交易,并分别处理每个交易。此外,提出的模型在 2010 年自动交易锦标赛上证明了本身的优越性 - 所有交易均根据交易计划精确执行,锦标赛中出现的交易模型在不同资金管理系统上管理着变化的风险程度。
所述方法几乎可作为参加锦标赛的完整解决方案,也可作为在多个工具和时间表上并行操作多个交易策略的完整解决方案。熟悉该方法的唯一困难在于方法本身的复杂性。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/217
![小型趋势指标、中型趋势指标和主要趋势指标](https://c.mql5.com/2/0/three_degrees_of_trend.png)
![最新的改革](https://c.mql5.com/2/0/Point_and_Figure_chart_MQL5.png)
![MQL5 中的电子表格](https://c.mql5.com/2/0/MQL5_table__1.png)
![William Blau 的 MQL5 指标与交易系统。第一部分:指标](https://c.mql5.com/2/0/MQL5_Willam_Blau_1.png)