我看到有人不时说,这种事情发生的几率是 500 万分之一,但即使百分比很低,这种可能性仍然存在。 既然我们知道这种情况可能发生,为什么不创造方法来最大限度地减少这种情况的损害或副作用呢? 为什么忽略可能发生的事情,而不修复它,或以某种方式阻止它,只是因为机会很低?
如果您正在关注这篇关于如何构建自动化 EA 的系列文章,您一定已经注意到创建手动使用的 EA 非常快速和简单。 但对于 100% 自动化的 EA,事情则并没有那么简单。 您也许已经注意到,我从未表达过我们将拥有一个 100% 万无一失的系统,且该系统可以在没有任何监督的情况下使用的想法。 事实上,我相信它已经变得很清楚,与许多人对自动 EA 的这个前提认知恰恰相反,您以为打开它,就可以放任它在那里滚动,而无需真正明白它正在做什么。
当我们谈论 EA 的 100% 自动化时,一切都变得严肃而复杂。 更重要的是,我们将始终受到运行时错误的影响,并且我们必须制作一个可以实时工作的系统。 这两件事结合在一起,再加上我们在系统中可能存在一些漏洞或故障,令那些将在 EA 运行期间需要监督 EA 的人来说,这项工作非常累人。
但作为一名程序员,您应该始终关注一些可能产生潜在问题的关键点,即使一切都看似完美和谐。 我并不是说您应该寻找可能不存在的问题。 这就是一位仔细和认真的专业人士理应会做的事情 — 在一个乍看没有缺陷的系统中寻找缺陷。
在当前开发阶段我们的 EA 旨在手动和半自动使用(使用盈亏平衡和尾随停止),没有像 Blockbuster(DC 超级反派)那样的破坏性漏洞。 然而,如果我们以 100% 自动化使用它,情况就会发生变化,并且存在潜在危险故障的风险。
在上一篇文章中,我提出了这个问题,并留给您了解这个缺陷在哪里,以及它如何导致问题,如此我们还无法 100% 自动化我们的 EA。 您是否设法了解故障在哪里?以及如何触发故障? 好吧,如果答案是否定的,那没关系。 并不是每个人都能通过查看代码,并在手动或半自动使用它的过程中真正注意到故障。 但如果您尝试自动化代码,就会遇到严重的问题。 这个缺陷当然是最简单的缺陷,但却不是那么容易纠正,这是 100% 自动化 EA 所必需的。
因此,若要了解它的内容,我们需将事情划分为几个主题。 我认为您会更容易注意到一些看似不重要的东西,这可能会给您带来很大的烦恼。
理解问题
当我们为 EA 设置每天可以交易的最大交易量限制时,问题就开始了。 不要将每日最大交易量与可操作交易量混淆。 现在我们主要对每日最大交易量感兴趣。
为清晰起见,我们假设交易量是最小交易的 100 倍。 也就是说,EA 能够进行尽可能多的操作,直到达到此交易量。 因此,在 C_Manager 类中添加的最后一条规则是交易量不超过 100。
现在,我来们看看实际发生了什么。 为此,我们需分析我们允许交易的代码:
inline bool IsPossible(const bool IsPending) { if (!CtrlTimeIsPassed()) return false; if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return false; if ((IsPending) && (m_TicketPending > 0)) return false; if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage) { Print("Request denied, as it would violate the maximum volume allowed for the EA."); return false; } return true; }
上面的代码可防止超过交易量。 但它实际上是如何发生的呢?
假设交易者以所需最小交易量的 3 倍手数启动 EA,EA 代码中定义的最大交易量为 100 倍(这是在代码编译期间完成的)。 这在之前的文章中都已经解释过。 经过 33 笔交易后,EA 将达到最小交易量的 99 倍,这意味着我们还可以再进行一笔交易。 然而,由于上面代码中高亮显示的行,交易者必须将交易量改为 1 倍才能达到最大限制。 否则,EA 将无法执行该操作。
这个想法是限制最大交易量,如此 EA 的损失就不会超过先前指定的参数(这必须始终是主要和最重要的问题)。 因为即使 EA 开仓量并未远高于规定交易量,我们仍然会有亏损,但这些都能以某种方式得到控制。
但您可能会想:我看不出这段代码有任何漏洞。 事实上,这段代码没有任何漏洞。 使用它的函数(如下所示)能够将 EA 的交易量限制在指定的最大限额。
//+------------------------------------------------------------------+ void CreateOrder(const ENUM_ORDER_TYPE type, const double Price) { if (!IsPossible(true)) return; m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade); } //+------------------------------------------------------------------+ void ToMarket(const ENUM_ORDER_TYPE type) { ulong tmp; if (!IsPossible(false)) return; tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade); m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp)); } //+------------------------------------------------------------------+
但是错误在哪里呢? 实际上,这并不容易理解。 若要明白这个问题,我们需要思考 EA 如何不执行发送订单的操作。 因为 C_Manager 类的这段代码将设法防止 EA 触发。 当我们组合不同的订单类型时,就会出现问题。 此刻,我们将触发碎块问题。 有一些途径可以限制,或者避免触发这种类型。 它们如下:
- 选择订单发送类型或型号。 在这种情况下,EA 将仅支持市价单或挂单。 这种类型的解决方案适用于某些自动交易模型,因为在某些情况下,仅按市价进行操作更合适、且更常见。 但在这种情况下,我们的系统所拥有的交易类型有限。 但此处的想法是展示如何创建一个可以覆盖尽可能多的案例的系统,故无需担心使用哪种订单类型。
- 避免触发的另一种方式是更深入地卡频率,已入场的那些持仓,以及成为持仓哪些内容将会变更(在本例中为挂单)。 此解决方案是更好的选择,但它有一些激进因素,其令编程变得复杂。
- 另一种方式(我们将要实现)是利用已开发的系统作为基础,如此我们将考虑正在做的事情,但我们不会对可以做什么做出假设。 但即便如此,我们也会尝试以某种方式令 EA 预测一天中整个时间段内的交易量。 以这种方式,我们可以避免触发,而不必限制 EA 系统可以支持的交易类型。
现在,考虑到当我们有订单组合时会出现问题的事实,我们要了解事情是如何真正发生的。 回到手数乘数 3 的例子,其中已经发生了 33 次操作,而每日限制为 100 倍,如果 EA 只处理市价单,一切都将完美控制。 但无论出于何种原因,如果我们在交易服务器上有一笔挂单,情况就不同了。 如果此挂单在最小交易量上再增加 3 个单位,那么它一旦倍激活,它将超过 EA 中允许的最大交易量。
您可以想象,这 2 个单位的交易量超过了 100 倍的限制并不多,但它并没有那么简单。 想想交易者将 EA 配置为交易 50 手的情况。 我们可以执行 2 笔市价订单,这超过了 100 的限制。 发送市价单后,EA 又发送一笔挂单,以便增加持仓量。 现在它将达到 100 的交易量。 但无论出于何种原因,此订单尚未执行,因为它仍然是挂单。 在某个时刻,EA 决定它可以发送另一个相同 50 手交易量的市价单。
C_Manager 类立即注意到 EA 已达到每日限量,因为它将已有 100 个单位。 但 C_Manager 类尽管知道有一笔挂单,但并不真正知道如何处理它。 在这种情况下,当该挂单在服务器上被执行时,EA 将超过 100 的规则,交易量为 150 个单位。 您明白问题所在吗? 前段时间我们放置了一个锁,以防止 EA 在账簿上有太多挂单,或以某个交易量开仓太多。 这个障碍被一个简单的事实打破了,即自动化 EA 的触发器没有预见到这种情况会发生。 有一个假设,即 C_Manager 类理应阻挡 EA,防止其超过定义的最大交易量。 但是该类失能了,因为我们把挂单和市价单结合起来了。
许多程序员会简单地通过仅用市价单,或仅用挂单来解决此问题。 但这并不能让问题消失。 每个新的自动 EA 都必须经过相同的测试和分析模式,从而防止触发触发器。
尽管即使手动系统也可以观察到上述问题,但交易者犯错误的可能性要小得多。 交易者应受到谴责,而不是保护系统。 但对于 100% 自动化的系统,这是完全不可接受的。 因此,源于这般和其它一些原因,即使您已经对其进行了编程,也永远不要让 100% 自动化 EA 在没有监督的情况下运行。 您也许是一名优秀的程序员,但绝不建议任其独自行动。
如果您认为我在胡说八道,请考虑以下几点。 飞机上有自动驾驶系统,可以让它在没有任何人为干预的情况下起飞、行驶和降落。 但即便如此,机舱内总有合格的飞行员来操作飞机。 您想想为什么会这样? 该行业不会花费大量资金只为了开发自动驾驶仪,而必须培训一名飞行员来操作飞机。 因为这是没有意义的,除非该行业本身不依赖自动驾驶仪。 好好想想这一点。
修复崩溃
实际上,仅示意错误并不能解决它。 需要的不仅如此。 但事实上,我们知道缺陷,并明白它是如何触发的,及其后果,这意味着实际上我们可以尝试生成某种解决方案。
我们相信,C_Manager 类将能够避免违反交易量,这一事实令一切变得不同。 为了解决这个问题,我们需要在代码中做一些事情。 首先,我们往系统里添加一个新变量:
struct st01 { ulong Ticket; double SL, TP, PriceOpen, Gap; bool EnableBreakEven, IsBuy; uint Leverage; }m_Position, m_Pending; ulong m_TicketPending;
通过生成某种预测,这个新变量能帮助我们解决交易量问题。 因为它的出现,代码的另一处就被删除了。
新变量加入后会进行一系列修改。 但我只会强调新添的部分。 您可以在附带的代码中更详细地查看所有修改。 首先要做的是创建一个例程来捕获挂单数据:
inline void SetInfoPending(void) { ENUM_ORDER_TYPE eLocal = (ENUM_ORDER_TYPE) OrderGetInteger(ORDER_TYPE); m_Pending.Leverage = (uint)(OrderGetDouble(ORDER_VOLUME_CURRENT) / GetTerminalInfos().VolMinimal); m_Pending.IsBuy = ((eLocal == ORDER_TYPE_BUY) || (eLocal == ORDER_TYPE_BUY_LIMIT) || (eLocal == ORDER_TYPE_BUY_STOP) || (eLocal == ORDER_TYPE_BUY_STOP_LIMIT)); m_Pending.PriceOpen = OrderGetDouble(ORDER_PRICE_OPEN); m_Pending.SL = OrderGetDouble(ORDER_SL); m_Pending.TP = OrderGetDouble(ORDER_TP); }
这与捕获持仓数据的例程不同。 主要区别在于我们要检查我们是买入还是卖出。 这是通过检查类型,来指明买入订单。 函数的其余部分是不言自明的。
我们需要另一个新函数:
void UpdatePending(const ulong ticket) { if ((ticket == 0) || (ticket != m_Pending.Ticket) || (m_Pending.Ticket == 0)) return; if (OrderSelect(m_Pending.Ticket)) SetInfoPending(); }
当挂单从服务器收到任何更新,并将信息传递给 EA 时,它就会更新数据。
为了令 EA 能够执行上述调用,我们需要向 OnTradeTransaction 处理程序添加一个新事件:
void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { switch (trans.type) { case TRADE_TRANSACTION_POSITION: manager.UpdatePosition(trans.position); break; case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order == trans.position) (*manager).PendingToPosition(); else (*manager).UpdatePosition(trans.position); break; case TRADE_TRANSACTION_ORDER_UPDATE: (*manager).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: (*manager).UpdatePosition(request.order); break; case TRADE_ACTION_SLTP: (*manager).UpdatePosition(trans.position); break; case TRADE_ACTION_REMOVE: (*manager).EraseTicketPending(request.order); break; } break; } }
上面高亮显示的行将调用 C_Manager 类。
如此,我们回到 C_Manager 类,继续实现交易量问题的解决方案。
如本节所述,我们需要创建一个更正系统,从而令系统能够达到更充分的安全性级别。 我们已注意到并忽略了很长时间的错误,就是负责更新开单的交易量。 这不会影响手动系统,但对于自动化系统来说则是致命的: 因此,要修复此错误,我们必须添加以下代码行:
inline void LoadPositionValid(void) { ulong value; for (int c0 = PositionsTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--) { if ((value = PositionGetTicket(c0)) == 0) continue; if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if (PositionGetInteger(POSITION_MAGIC) != GetMagicNumber()) continue; if ((m_bAccountHedging) && (m_TicketPending > 0)) { C_Orders::ClosePosition(value); continue; } if (m_Position.Ticket > 0) SetUserError(ERR_Unknown); else { m_Position.Ticket = value; SetInfoPositions(); m_StaticLeverage = m_Position.Leverage; } } }
设计全自动系统是一项具有挑战性的任务。 对于手动或半自动系统,有没有上述代码行无丝毫区别。 但对于自动化系统来说,任何故障,无论多么小,都可能意味着发生灾难的可能性。 如若交易者不监督 EA,或不知道它实际在做什么,那就更是如此。 这肯定会让您在市场上赔钱。
下一步是修改订单发送和行情请求函数。 我们需要它们能够向调用方返回数值,同时还能够通知调用方正在发生的事情。 这就是代码:
//+------------------------------------------------------------------+ bool CreateOrder(const ENUM_ORDER_TYPE type, const double Price) { bool bRet = false; if (!IsPossible(true)) return bRet; m_Pending.Ticket = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade); if (m_Pending.Ticket > 0) bRet = OrderSelect(m_Pending.Ticket); if (bRet) SetInfoPending(); return bRet; } //+------------------------------------------------------------------+ bool ToMarket(const ENUM_ORDER_TYPE type) { ulong tmp; bool bRet = false; if (!IsPossible(false)) return bRet; tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade); m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp)); if (m_Position.Ticket > 0) bRet = PositionSelectByTicket(m_Position.Ticket); if (!bRet) ZeroMemory(m_Position); return bRet; } //+------------------------------------------------------------------+
请注意,我正在进行测试来检查单号是否仍然有效。原因是在净持账户中,您可以通过发送与持仓相等的交易量来平仓。 在这种情况下,如果平仓,我们需要从中删除数据,以提高系统的安全性和可靠性。 这是针对 100% 自动化的 EA,而对于手动 EA,这些事情是不必要的。
接下来,我们需要添加一种方法让 EA 知道要发送的交易量。 如果您想逆转当前系统中的持仓,您可以执行此操作,但是需要两次调用,或者更确切地说,需要向服务器提交两次请求,而不是单次请求。 目前您需要平仓,然后发送请求开新仓。 知道开仓交易量后,EA 就可知道要发送哪个交易量。
const uint GetVolumeInPosition(void) const { return m_Position.Leverage; }
上面的这段简单代码足以实现这一点。 但是在类代码中没有办法真正反转持仓。
为了实现这一点,我们将再次修改订单发送函数。 但请注意,这不是手动或半自动 EA 需要具备的。 我们正在进行这些更改,因为我们需要这些来生成自动化 EA。 此外,在某些点上放置某种消息很有趣,这样监督 EA 的交易者就可以了解 EA 的实际操作。 即使我们在演示代码中不这样做,您也应该认真考虑正在做的这样事情。 因为盲目自大,仅仅观看图表,实际上不足以注意到一些奇怪的 EA 行为。
在实现所有这些更改之后,我们到达了我们的关键点,其确能将解决我们正在处理的问题。 我们为 EA 添加了一种能够将任意交易量发送到服务器的方法。 这仅在两次调用中完成,其中 C_Manager 类将授予对 EA 的访问权限。 因此,我们修复了 EA 最终可能超越代码中所指定最大交易量的问题。 现在我们将开始预测交易量,这可能会是将要入场或已开持仓的离场。
新的调用代码如下所示
//
因此,很明显,C_Manager 类要求在更改开仓交易量时,应不存在挂单。 另一个原因是,如果存在多头持仓,并且您打算反转它,这可通过下一笔交易量大于持仓交易量的订单来完成。 如果挂单(最初是止损单)若继续存在,则在订单执行时也许会出现问题。 交易量将进一步增加。 所以,我们多了一个原因,即 C_Manager 在修改交易量实际上要求没有挂单。
我希望现在很清楚为什么该类在更改交易量时需要删除挂单。 不删除这一行,EA 不会向服务器发送请求。
现在我们有一个相当奇怪的计算,和一个更奇怪的测试,如果您不真正理解计算,却试图理解它,最终可能会让您头疼。 最后,还有另一个乍一看没有任何意义的测试。
我们仔细分析这一刻,以便每个人都能真正理解这种彻底的数学疯狂。 为了方便起见,我们看一些示例。 请在阅读每个示例时要非常小心,以便能够理解所有高亮显示的代码。
示例 1:
假设没有持仓,则 i1 值将等于 Leverage 变量中包含的绝对值;这是最简单的情况。 那么,我们是否在订单簿中开仓或下订单并不重要。 如果 i1 与 EA 累积值的总和小于指定的最大值,则请求将被发送到服务器。
示例 2:
假设我们有一笔交易量 X 的空头持仓,并且我们没有挂单。 在这种情形下,我们可以有若干种不同的场景:
- 如果 EA 发送卖出交易量 Y 的订单,则 i1 值将是 X 和 Y 的总和。在这种情况下,订单可能无法成交,因为我们增加了空头持仓。
- 如果 EA 发送买入交易量 Y 的订单,并且它小于交易量 X,则 i1 值将等于 X 和 Y 之间的差值。在这种情况下,订单因降低空头持仓而通过。
- 如果 EA 的订单是买入 Y,而 Y 等于 X,则 i1 将为零。 在这种情况下,订单通过,且空头持仓平仓。
- 如果 EA 发送买入交易量 Y 的订单,并且它大于交易量 X,则 i1 值将等于 X 和 Y 之间的差值。在这种情况下,订单可能不允许,因为我们将持仓由空头变成多头。
多头持仓也是如此。 然而,EA 的请求也会被修改,如此在最终我们将具有相同的行为,如 i1 变量所示。 请注意,在测试中,我们检查 i1 和 EA 累积值的总和是否小于允许的限制,我们调用 MathAbs 函数。 我们这样做是因为在某些情况下,我们会有负数值的 i1。 但我们要求它是正数值,如此才能令测试正常运行。
但我们仍有最后一个问题需要解决。 当我们反转持仓时,将无法正确更新交易量。 那为了解决这个问题,我们需要在分析系统里进行一个小的修改。 修改如下所示:
inline int SetInfoPositions(void) { double v1, v2; uint tmp = m_Position.Leverage; bool tBuy = m_Position.IsBuy; m_Position.Leverage = (uint)(PositionGetDouble(POSITION_VOLUME) / GetTerminalInfos().VolMinimal); m_Position.IsBuy = ((ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE)) == POSITION_TYPE_BUY; m_Position.TP = PositionGetDouble(POSITION_TP); v1 = m_Position.SL = PositionGetDouble(POSITION_SL); v2 = m_Position.PriceOpen = PositionGetDouble(POSITION_PRICE_OPEN); if (m_InfosManager.IsOrderFinish) if (m_Pending.Ticket > 0) v1 = m_Pending.PriceOpen; m_Position.EnableBreakEven = (m_InfosManager.IsOrderFinish ? m_Pending.Ticket == 0 : m_Position.EnableBreakEven) || (m_Position.IsBuy ? (v1 < v2) : (v1 > v2)); m_Position.Gap = FinanceToPoints(m_Trigger, m_Position.Leverage); return (int)(tBuy == m_Position.IsBuy ? m_Position.Leverage - tmp : m_Position.Leverage); }
首先,我们保存系统在更新之前的持仓。 在那之后,我们不会干涉任何事情,直到最后,直到我们检查持仓方向是否有任何变化。这个思路就是检查我们的持仓是多头还是空头。 这同样适用于相反的情况。 如果有变化,我们将返回开仓交易量。 否则,我们正常执行计算,以便搞清当前开仓的交易量。 以这种方式,我们就可以正确了解和更新 EA 已完成交易的交易量。