
构建自动运行的 EA(第 14 部分):自动化(VI)
概述
在上一篇文章构建自动运行的 EA(第 13 部分):自动化(V)当中,我解释了没有任何编程知识的交易者如何把交易系统构建为自动 EA 的基本所需。 这就是我们在本系列文章中一直在做的事情。 这些概念和信息适用于任何 EA,包括您正在构建的任何 EA。 在本文中,我们将研究完成此任务的众多途径之一。
为了真正掌握我们将在这里讨论的内容,充分理解上一篇文章的内容非常重要。 如果没有这些知识,可能很难搞懂本文的内容。 故此,如果您还没有读过上一篇文章,我建议您在继续之前阅读它。 现在,我们继续讨论本文的主题:如何将最初的手动智能 EA 转化为自动化。
C_Automaton 类的诞生
我们在上一篇文章中曾见过下图:
图例 01 - 手动模式
在此图中,我们看到了交易者如何与平台交互,从而在交易服务器上开仓和平仓。 为了自动化该过程,我们需要对此图例进行一些更改。 如此,代表手动模型的图例 01 变成了图例 02。 此图表现在不同编程阶段,根据定义的一些操作规则,EA 的自动操作。
图例 02 - 自动模式
请注意,图例 02 包含一个充当系统监管者的图像。 自动化系统绝不应在无人监督的情况下运行。 交易者必须始终控制整个过程,即使他只是在观察而并未做任何事情。
同一图还包含 EA 和 C_Manager 类之间的一个附加类,称为 C_Automaton。 本文将主要介绍该类及其与 EA 和 C_Manager 类的关系,该类代替交易者在订单簿中开仓和平仓,或下挂单。
还有需注意的重点是,我们未对现有系统进行任何修改,在我们实现 C_Automaton 类之前,它应该已经正确、安全、可靠、稳健和稳定地工作。 如果您要修改系统本身中的任何内容,您应删除 C_Automaton 类,还原 C_Mouse,并测试系统中的所有修改之处。 只有当您确定系统运行良好时,您才能再次添加 C_Automaton 类,并令 EA 无需人工干预即可运行。 但永远记住,监督是必要的。 人为干预应该是最后的手段,但监督本应持续。
在我们了解 C_Automaton 是如何编程之前,我们看一下已修改成自动运行的 EA 代码。 完整的 EA 代码将如下所示:
#property copyright "Daniel Jose" #property description "This one is an automatic Expert Advisor" #property description "for demonstration. To understand how to" #property description "develop yours in order to use a particular" #property description "operational, see the articles where there" #property description "is an explanation of how to proceed." #property version "1.14" #property link "https://www.mql5.com/pt/articles/11318" //+------------------------------------------------------------------+ #include <Generic Auto Trader\C_Automaton.mqh> //+------------------------------------------------------------------+ C_Automaton *automaton; //+------------------------------------------------------------------+ input int user01 = 1; //Leverage Factor input double user02 = 100; //Take Profit ( FINANCE ) input double user03 = 75; //Stop Loss ( FINANCE ) input bool user04 = true; //Day Trade ? input double user08 = 35; //BreakEven ( FINANCE ) //+------------------------------------------------------------------+ input string user90 = "00:00 - 00:00"; //Sunday input string user91 = "09:05 - 17:35"; //Monday input string user92 = "10:05 - 16:50"; //Tuesday input string user93 = "09:45 - 13:38"; //Wednesday input string user94 = "11:07 - 15:00"; //Thursday input string user95 = "12:55 - 16:25"; //Friday input string user96 = "00:00 - 00:00"; //Saturday //+------------------------------------------------------------------+ #define def_MAGIC_NUMBER 987654321 //+------------------------------------------------------------------+ int OnInit() { string szInfo; automaton = new C_Automaton(def_MAGIC_NUMBER, user03, user02, user01, user04, user08, PERIOD_M5); for (ENUM_DAY_OF_WEEK c0 = SUNDAY; c0 <= SATURDAY; c0++) { switch (c0) { case SUNDAY : szInfo = user90; break; case MONDAY : szInfo = user91; break; case TUESDAY : szInfo = user92; break; case WEDNESDAY : szInfo = user93; break; case THURSDAY : szInfo = user94; break; case FRIDAY : szInfo = user95; break; case SATURDAY : szInfo = user96; break; } (*automaton).SetInfoCtrl(c0, szInfo); } (*automaton).CheckToleranceLevel(); EventSetMillisecondTimer(100); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { delete automaton; EventKillTimer(); } //+------------------------------------------------------------------+ void OnTick() { } //+------------------------------------------------------------------+ void OnTimer() { (*automaton).Triggers(); } //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { switch (trans.type) { case TRADE_TRANSACTION_POSITION: (*automaton).UpdatePosition(trans.position); break; case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order == trans.position) (*automaton).PendingToPosition(); else { (*automaton).UpdatePosition(trans.position); (*automaton).EraseTicketPending(trans.order); } break; case TRADE_TRANSACTION_ORDER_UPDATE: (*automaton).UpdatePending(trans.order); break; case TRADE_TRANSACTION_REQUEST: if ((request.symbol == _Symbol) && (result.retcode == TRADE_RETCODE_DONE) && (request.magic == def_MAGIC_NUMBER)) switch (request.action) { case TRADE_ACTION_DEAL: (*automaton).UpdatePosition(request.order); break; case TRADE_ACTION_SLTP: (*automaton).UpdatePosition(trans.position); break; case TRADE_ACTION_REMOVE: (*automaton).EraseTicketPending(request.order); break; } break; } } //+------------------------------------------------------------------+
请注意,仅对这段代码进行了少量修改。 已删除 OnChartEvent 处理程序,因为不再需要它,并且 OnTime 事件已得到一个新函数。稍后将提供更多详细信息。 其余代码与之前的手动操作模式相同。 亟须注意的是,在自动化改造时,您不应修改整个 EA 代码。 您应该只进行必要的修改,在我们的例子中,这只是在定时器事件中添加一个新函数。
尽管如此,此 EA 代码并不是一个确定且不可变的代码。 取决于您打造系统的用途,可能或多或少有些用户定义的内容,我们将在此处看到。 因此,C_Automaton 类构造函数中也许存在变化。 如此,您可能需要更多或更少的参数。故此,不要认为此代码可以在所有可能的情况下使用。
但除了前面提到的几点外,它基本上不会有重大变化。 如此,我建议您研究将要使用的系统真正需要做什么。 保留某些不应由交易者调整的预配置部分,并仅允许用户根据所使用的场景调整必要的部分。
您可能已经注意到的另一件事是 C_Automaton 类继承自 C_Manager 类。 有因于此,代码实际上没有进行任何修改。 故此,如您所见,一切都将在 C_Automaton 类内发生。
如果您想使用与我展示的相同的 EA 结构和类,修改 C_Automaton 类,并在您的交易系统中调用它就足够了。 因此,创建不同功效交易系统的新 EA 将更快、更安全、更一致,因为它们之间的唯一区别是 C_Automaton 类本身。 创建类时,使用构造函数,您可以根据交易系统本身通知一些特定的事情。 这将保持高度的灵活性、可用性、健壮性和可重用性。
您的 EA 将始终拥有实际用户所需的优秀品质水平。 但无论如何,您都可以做一些改变,让事情变得更有趣。 或许将来,我会公开其中的一些修改。 在任何情况下,我建议您更改并调整系统,从而适应您的操作模式。 毕竟,这就是我撰写这些文章的本意,并允许免费访问代码。
我唯一的要求是,如果您用到它,即使只是其中的一部分,请注明内容的来源。 这没什么害羞的,而盗用或分发一些东西且不注明来源才是不好的。
现在,我们看看 C_Automaton 类这个黑匣子里有什么。
分析 C_Automaton 类代码
如上所述,EA 代码和 C_Automaton 类代码都取决于您要交易的内容、方式和时间。 但无关于此,C_Automaton 类基本上会有三个内部函数:一个构造函数,一个将由 EA 的时间事件调用的函数,以及该类的另一个内部和私密函数。
重要的是要注意这三个函数。 也许会由于其它原因,需要或多或少的函数来辅助这三个函数,但本质上我们将始终拥有这三个函数。
不仅限于理论,我们来查看下面的视频。 它也许看起来很长,但仍然值得一观。 我想强调的是,无论您利用什么系统进行交易,每个人,无一例外,都会遇到某种损失,多少不论。 但首先观看视频,以了解下面将解释的内容。
视频 01 - 自动 EA 演示(约 25 分钟)
在本视频中,我采用的是基于 9-周期指数移动平均线的交易系统,EA 基于该平均线在每个新柱线的开盘时刻建仓入场。 上一篇文章中讲述过这个系统的创建,所以我建议仔细阅读该文章,以便了解所有底层过程。 再者我们不要局限于理论。 为了了解 C_Automaton 类如何令 EA 能够创建、管理和关闭交易,我们要看一些特定设置的创建代码示例。
遵照以下每种方法的解释,了解它是如何进行开发和编码的。 以这样的方式,您几乎可以开发任何设置,尽管可能有一些非常特殊的情况需要更多的代码。 但由于它们很罕见,而且绝大多数人实际上都会选用指标,这些示例将有助于创建 100% 自动模型。 在进入代码之前,我们先看一些小细节。
共用部件、依赖部件
我们需要做一些澄清,如此便可在查看示例时不会因 C_Automaton 类而觉得困惑。
C_Automaton 类的代码非常奇特,但对于有经验的程序员来说,这都是很平常的事情。 有些部件是所有操作共有的,有些部件则是特定于特殊操作的。 请注意这一点,在描述您的方法时,应如上一篇文章所示。 因为如果您不了解所有模型都有共用的部分,您可能会认为 C_Automaton 类无法涵盖您的模型,而实际上它可以涵盖任何模型。 有时您需要添加一些变量,但我会向您展示如何以正确的方式执行此操作,如此您就可以使用任何类型的模型。
无论您要使用哪种模型,共用代码部分都将始终重复。 而依赖部件则令您的模型独一无二,并且不会在其它模型中重复。
此处就是所有模型的共用代码:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_Manager.mqh" //+------------------------------------------------------------------+ class C_Automaton : public C_Manager { protected: enum eTrigger {TRIGGER_NONE, TRIGGER_BUY, TRIGGER_SELL}; private : struct st00 { int Shift, nBars; double OverBought, OverSold; }m_Infos; double m_Buff[]; int m_nBars, m_Handle; ENUM_TIMEFRAMES m_TF; //+------------------------------------------------------------------+ static eTrigger m_Memory; //+------------------------------------------------------------------+ inline eTrigger CheckTrigger(void) { int iRet; if (((iRet = iBars(NULL, m_TF)) > m_nBars) && (m_Handle != INVALID_HANDLE)) { } return TRIGGER_NONE; } //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ C_Automaton(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trailing, const ENUM_TIMEFRAMES iPeriod, const double OverBought = 70, const double OverSold = 30, const int iShift = 1) :C_Manager(magic, FinanceStop, FinanceTake, Leverage, IsDayTrade, Trailing, true, 10), m_TF(iPeriod), m_Handle(INVALID_HANDLE) { m_Infos.Shift = iShift; m_Infos.OverBought = OverBought; m_Infos.OverSold = OverSold; ArraySetAsSeries(m_Buff, true); m_nBars = iBars(NULL, m_TF); } //+------------------------------------------------------------------+ inline virtual void Triggers(void) final { if (!CtrlTimeIsPassed()) ClosePosition(); else switch (CheckTrigger()) { case TRIGGER_BUY: if (m_Memory == TRIGGER_SELL) ClosePosition(); if (m_Memory != TRIGGER_BUY) ToMarket(ORDER_TYPE_BUY); m_Memory = TRIGGER_BUY; break; case TRIGGER_SELL: if (m_Memory == TRIGGER_BUY) ClosePosition(); if (m_Memory != TRIGGER_SELL) ToMarket(ORDER_TYPE_SELL); m_Memory = TRIGGER_SELL; break; } }; //+------------------------------------------------------------------+ }; //+------------------------------------------------------------------+ static C_Automaton::eTrigger C_Automaton::m_Memory = TRIGGER_NONE; //+------------------------------------------------------------------+
您在上述代码中看到的所有内容都是存在于任何模型中的共用代码的一部分。 无论模型如何,在其中都会始终用到此代码。 确实,可能会有一些新的变量或 C_Manager 调用,正如您将在示例代码中看到的那样。 但本质上以上代码将最大程度保持不变。
上面代码中不存在的任何内容都是与特定模型相关的代码一部分。 故此,我们来查看代码,以便了解它是如何工作的。 通过这种方式,您就能够了解代码中需要添加或不添加的内容,以便涵盖更多特殊的模型。
我们从以下代码中的类声明开始:
#include "C_Manager.mqh" //+------------------------------------------------------------------+ class C_Automaton : public C_Manager { protected: enum eTrigger {TRIGGER_NONE, TRIGGER_BUY, TRIGGER_SELL};
在此,我们声明 C_Automaton 类公开继承自 C_Manager 类。 这样就允许我们在使用 C_Automaton 类时访问 C_Manager 类的过程。 这对于初始化与控件类关联的某些成员非常重要。 如果您查看 EA 代码,您可以看到访问程序的这些调用点。 尽管这些过程未在 C_Automaton 类中声明,但它们来自其它类。 这在本系列的另一篇文章中已经解释过了。
我们还创建了一个枚举器,以便在类中进行高级编程。此枚举为已激活的触发器指示类型。 还有更多的细节:尽管此枚举器是在代码的受保护部分中声明的,且它不会在类的外部用到。 但有必要把它放在这部分,以便能够初始化静态类变量。 我们稍后会看到更多内容。
接下来,我们看看下面代码中的变量:
private : struct st00 { int Shift, nBars; double OverBought, OverSold; }m_Infos; double m_Buff[]; int m_nBars, m_Handle; ENUM_TIMEFRAMES m_TF; //+------------------------------------------------------------------+ static eTrigger m_Memory;
在这种结构中,我们几乎不需要进行修改,因为它涵盖了与指标相关的广泛情况。 然而,如果您用到多个指标,则可能需要添加更多结构。 但是您不需要修改结构本身的元素。
为了理解这一点,我们来看下面的例子:结构被当作一个变量引用。 这样就能够使用一个指标引用,无论它是什么。 但是,如果我们要用到更多个指标怎么办? 这允许使用一个指标, 在这种情况下,我们将需要实现一些补充。 它们将显示在示例当中。 至于现在,我们专注于基础,并理解这个较简单的系统。
我们还有一个变量,它将作为接收指标值的缓冲区,一个存储图表上柱线数量的变量,以及另一个指标引用的变量,和一个确定指标时间帧的变量。 最后,还有一个变量存储最后的触发状态。 静态变量在类主体外部初始化,如下面的代码所示:
static C_Automaton::eTrigger C_Automaton::m_Memory = TRIGGER_NONE;
请注意,它必须使用中性触发器进行初始化,这意味着不会对触发器系统进行任何查询。 我们不希望发生意外触发。 虽然,根据交易模型,这个静态变量不是那么有用,因为模型总是以交叉的方式生成触发器。 但为了避免在将 EA 放在图表上后立即出现随机触发,我们使用此变量来确保在启动或交易手牌变化时,不会发生任何随机事件。
接下来的要点是下面展示的类的私密函数:
inline eTrigger CheckTrigger(void) { int iRet; if (((iRet = iBars(NULL, m_TF)) > m_nBars) && (m_Handle != INVALID_HANDLE)) { }; return TRIGGER_NONE; }
默认情况下,此函数将始终返回一个空触发器,它既不是买入也不是卖出。您的交易模型的特定计算将在此函数中实现。 因此,未来的计算可能会有所不同。
但无论所需的计算类型如何,我们都将始终通过以下方式测试系统:计算仅在新柱线生成时执行;如果没有新柱线,则计算将被忽略。 此外,句柄必须指向有效内容;否则也不会计算任何内容。 重要是要明确定义这些规则,从而避免在 EA 操作期间出现问题。
以下函数是类构造函数:
C_Automaton(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trailing, const ENUM_TIMEFRAMES iPeriod, const double OverBought = 70, const double OverSold = 30, const int iShift = 1) :C_Manager(magic, FinanceStop, FinanceTake, Leverage, IsDayTrade, Trailing, true, 10), m_TF(iPeriod), m_Handle(INVALID_HANDLE) { m_Infos.Shift = iShift; m_Infos.OverBought = OverBought; m_Infos.OverSold = OverSold; ArraySetAsSeries(m_Buff, true); m_nBars = iBars(NULL, m_TF); }
彻底理解此构造函数非常重要。 我知道对于那些没有经验或刚刚入门的人来说,代码可能看起来令人困惑。 然而,理解此代码对于充分利用 C_Automaton 类,以及创建全自动 EA 至关重要。 我们首先搞清楚这个看似令人困惑的文本实际上在做什么。
由于 C_Automaton 是继承自 C_Manager,因此我们必须初始化 C_Manager 构造函数。出于此原因,它将从 EA 接收数据,这些数据会全部传递给 C_Manager 类进行初始化。
但请注意两个附加参数。如果您需要,可以将它们传递回 EA。 不过这取决于在 C_Automaton 类中创建的交易类型。 我在此提及这些参数,如此它们的含义就清楚了。 这些参数可以通过查看 C_Manager 来理解。 类构造函数的代码可以在前面的文章中找到,故我不会在这里重复。
EA 还将告知 C_Automaton 类使用哪个图表周期。 此参数用于此目的。
重要的是,无论监管者看到什么,自动化系统始终按照图表时间帧工作。 如果您所用时间帧与监管者所见的不同,则在自动化类中,我们的 EA 就可能在不对应的时间意外触发。 这是有些 EA 中非常常见的错误,程序员未意识到用户可以在 EA 工作时间更改图表时间帧。 这就会造成巨大的不便。 但按这样做,我们就能保证 EA 将始终在相同的时间帧内工作,无论交易者看到的是什么。
此刻,我们有一些拥有默认值的参数。 相应地,它们或许会在 EA 代码中声明。
使用默认值不会妨碍您在 EA 代码中指定另一个值,甚至允许交易者在启动 EA 时定义一个值。 但由于在绝大多数情况下这些值将保持不变,而在其它情况下它们不会实际用到,故我为这些参数定义一些默认值,从而避免构造函数重载。 这也令代码更紧凑,更易于分析和理解。 请注意,这些值存储在内部结构中,以供进一步使用。
一个重要的细节:如果您所用的系统可以由交易者根据所用指标类型更改这些数值,则可能需要修改此代码,以便适应更多变量。 在这种情况下,它适合与一个或多个指标一起使用,只要它们都使用相同的数据。
最后,我们初始化句柄的值,如此其就不会指向任何指标。这对于避免破坏安全性很重要,从而防止我们的句柄指向未知的东西。 此外,我们还调整了最后一个系统变量。 此构造函数是最基本的,它将会有更多代码行,具体取决于将要实现的系统类型。 但这些会在代码示例中显示。
为了完成 C_Automaton 类的基本代码,我们来查看最后一个代码部分:
inline virtual void Triggers(void) final { if (!CtrlTimeIsPassed()) ClosePosition(); else switch (CheckTrigger()) { case TRIGGER_BUY: if (m_Memory == TRIGGER_SELL) ClosePosition(); if (m_Memory != TRIGGER_BUY) ToMarket(ORDER_TYPE_BUY); m_Memory = TRIGGER_BUY; break; case TRIGGER_SELL: if (m_Memory == TRIGGER_BUY) ClosePosition(); if (m_Memory != TRIGGER_SELL) ToMarket(ORDER_TYPE_SELL); m_Memory = TRIGGER_SELL; break; } };
上述过程并非 100% 完成的代码,根据您的交易系统,它也许会发生一些小的变化。 基本上,它展示了来自 OnTime 事件每次调用时将发生的情况。 许多人也许想把它移到 OnTick 事件之中,但我已经解释了为什么不应该这样做。 我建议您阅读之前的文章来了解原因。
该段代码告诉 C_Manager 类始终遵照市场需求工作。 这就是为什么我说代码不是 100% 完成的,因为根据您实现的模型,订单系统可能会有所不同。 甚至交易方改变方式也会令此代码看起来不同。
另一件事是,上面的代码不允许 EA 在新信号出现时增加仓位。这是因为我们用到了一个记忆变量。
它可以防止 EA 在生成新入场信号时不停加仓。 这是我故意添加的,但任何有一定编程知识的人都可以绕过这个模块。 如果有人试图在没有相应知识的情况下这样做,他们肯定会造成账户的严重损失,因为 EA 可以不受控制地运行下单,在几秒钟内亏空所有可用余额。 因此,如果您不知道自己在做什么,请不要修改此段代码。
出于这个原因,我们不会太深入地解释这段代码。 不过,在继续讨论示例之前,我想提及一件事。 这是该过程的声明,乍一看似乎很奇怪和毫无意义。
我们试着了解原因。 整个系统被设计为使用类系统。 除了令代码更加可靠、安全和健壮之外,这样还允许其随着时间的推移进行扩展,最终它会比最初设计的更复杂。 但这种复杂性的增加并不涉及代码的可观增长,因为类系统允许我们将事务模块化,从而在增加复杂性的同时,减少代码。
由于 C_Automaton 并非最终的类,因此它在更大的系统中应按原样过程进行声明。 我们告诉编译器来帮助,确保此特定过程不会以任何方式在另一个类中被修改。 这样即可确保它在整个继承的类系统中是唯一的。
在处理模块化和结构非常好的代码时,这种事情非常重要。 因为无论您如何构建它们,迟早您都会犯覆盖继承过程的错误。 如果发生这种情况,它将令我们的所有代码都处于风险之中。 但幸运的是,MLQ5 语言提供了避免此类问题的工具。
为了理解 C_Automaton 类如何帮助自动化 EA,我们来看一些代码示例,我们将重点介绍如何实现交易系统。 在我们开始之前,有几件事需要澄清:
- 没有 100% 安全的自动交易系统。
- 没有系统可以保证所有操作能盈利。
- 当您看到系统开始时工作时,请不要搞错,如视频 01 所示。
- 仅使用或自动化您已经搞明白的系统。 不要试图结合若干种策略,幻想这样做您就会取得更好的结果。 简单就是一切。
- 使用此处显示的模型需要您自担风险,明白它们即可产生利润,亦会造成亏损。
为了更好地理解如何应用代码,每个模型都会附带一个特定的 EA,允许您比较代码,并据其学习。
结束语
不过,由于该主题的体量,本文仅介绍三个自动化示例。 相应的代码将在下一篇文章中讨论,这可能是此序列中的最后一篇文章。 所以下一篇文章不容错过。 在那篇文章里,我们将看到所有这些元素如何完美地组合在一起,在遵循您指定的形态的同时,EA 以全自动模式发挥作用。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11318
