从头开始开发智能交易系统(第 18 部分):新订单系统 (I)
概述
自我们从从头开始开发智能交易系统系列最初第一篇文章开始打造这个 EA 以来,它已经历了各种变化和改进,同时保持了相同的图表订单系统模型。 它非常简单和实用。 然而,在许多情况下,它不适合真正的交易。
当然,我们可以在原始系统中添加一些东西,这样我们就可以获得一些关于订单的信息,包括持仓和挂单两者。 但这会把我们的代码变成弗兰肯斯坦(寓意:为了获得功能东拼西凑,丑陋无比兼漏洞百出),最终会让改进过程变成一场噩梦。 即使您已有了自己的方法,随着时间的推移,它也会泯灭,因为代码变得臃肿且太复杂。
因此,我们需要根据我们所用的订单模型创建一个完全不同的系统。 同时,它应该易于交易者理解,同时提供我们安全工作所需的所有信息。
重要提示
我在本文中讲述的系统是针对净持结算账户 ACCOUNT_MARGIN_MODE_RETAIL_NETTING 而设计的,它允许每个品种只有一笔持仓。 如果您所用的是对冲账户 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING,本文不会为您的 EA 提供任何多余信息,因为在这种情况下,您可以拥有您所需的任意多笔持仓,并且它们不会相互干扰。 因此,您简单地重置,并删除最终代码中的所有修改。
ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 模式最常用到,适合您希望 EA 自动运行时,且在没有您参与的情况下开仓和平仓,同时您也可以继续手动交易。 甚至,您与 EA 交易的资产能够相同。 通过对冲,您的 EA 行动不会影响您的任何交易,因此您将拥有独立的持仓。
为此原因,我将高亮显示正在添加或修改的所有代码部分。 我们将缓慢、渐进式地实现所有修改和添加,因此如果您需要从代码中删除任何内容,您将能够轻松找到相关的代码部分。
虽然修改可以删除,但有一部分是我正在验证系统。 即使您保存了我们将在此处研究的修改,您也可以在任何类型的交易账户、净持和对冲结算账户上使用此 EA,因为智能系统将检查特定模型,并进行相应调整。
1.0. 计划
事实上,首先要做的是理解订单添加到系统时会发生什么,以及在达到相应价格时如何填单。 许多交易者可能不知道这一点,或者更确切地说,他们从未想过这一点。因此,他们没有进行任何测试,从而理解这一想法,然后再实现一个充分且可靠的系统。
为了理解实际发生的情况,我们分析一个简单的例子:对于初始交易量为 1 手的某个资产,您有一笔持仓,比如说做多。 然后,您以略高的价位下一笔新的 2 手做多的挂单。 到目前为止还不错,没有什么特别的。 而一旦这 2 手成交兑现,就会发生一些事情,这就是问题所在。
一旦这两手做多成交,您就有了 3 手,但系统将更新您的初始价格,并将其设置在平均值。 此刻这似乎很清楚,每个人都明白这一点。 但新持仓的止损和止盈价位会如何演变?
许多交易员不知道答案,但他们最好思考一下。 如果您在所有交易中都使用 OCO 订单系统(一笔取消其它),您可以在每次开立或从总持仓量里面部分平仓时注意到一些有趣的事情。
在关于交叉订单的文章中,我提出了一种无需借助标准 MetaTrader 5 系统,直接在图表上放置止盈和止损的方式。 事实上,该方法几乎与 MetaTrader 5 系统相同,因为其功能非常接近平台,但要配合适当的比例。 不过,在做了一些测试之后,我发现当我们有一笔开立的 OCO 订单和一笔挂单时,OCO 订单也会被交易系统触发,因为价格达到了订单中指定的值。 除了新的平均价格外,我们还改变了止盈和止损值 — 它们被改为最后一次触发的 OCO 订单中指定的值。 因此,根据其配置方式,在交易系统报告新的止盈和止损值后 EA 会立即将其平仓。
这是由于 EA 中执行了以下检查:
inline double CheckPosition(void) { double Res = 0, last, sl; ulong ticket; last = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_LAST); for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol()) { ticket = PositionGetInteger(POSITION_TICKET); Res += PositionGetDouble(POSITION_PROFIT); sl = PositionGetDouble(POSITION_SL); if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { if (last < sl) ClosePosition(ticket); }else { if ((last > sl) && (sl > 0)) ClosePosition(ticket); } } return Res; };
高亮显示的代码行执行此检查,并在价格退出由止盈和止损值形成的通道时平单。
了解这一点很重要,因为我们讨论的不是 EA 故障、交易系统问题或缺陷。 真正的问题是,大多时候,我们没有对正在发生的事情给予应有的关注,并忽视这种情况,直到该事实发展到不再可能被视而不见。
如果您的交易方式不是成本摊平,您就不会看到这种情况发生。 但有一个非常广泛的操作范围,其中摊平成本是恰当的,甚至是必要的。 因此,在这些情况下,如果您在运行系统时不知道上述详细信息,您可以提前平仓,即使您之前相应地更改了持仓的 OCO 订单。 一旦触发了挂单 OCO,阈值(每次我谈到阈值时,我指的是之前的止盈和止损),将替换为最近触发的持仓 OCO 订单中指定的值。
有一种方式可以解决或避免这种情况:即不用 OCO 订单,至少在您已经有持仓的情况下。 传递到系统中的所有其它订单必须是简单类型,无需设置止盈和止损。
基本上,就是这样。 但当 EA 出现在图表上时,它会帮助我们,令我们的生活更轻松。 因此,如果您以后不能再用智能系统,那么为其编程则毫无意义。
2.0. 实现一个新系统
我们需要对代码略微做一些修改来实现系统,并确保它按照我们期望的方式工作 — EA 应该帮助我们,并避免错误。
这并不十分困难,但这些变化保证了我们永远不会冒着风险在不必要的时刻收到 OCO 订单,从而在我们的运营中造成真正的混乱。
让我们从以下修改开始:
2.0.1. C_Router 类的修改
C_Router 类负责解析,并向我们发送订单。 我们给它添加一个私密变量。 当发现由 EA 交易的资产有持仓时,该变量将存储相关信息。 每次 EA 想知道是否有持仓时,它都会告诉我们。
此实现如下面的代码所示。 不过,本文将进一步修改此代码。 我只想逐步地展示所有修改,以便您了解修改过程的实际情况。
//+------------------------------------------------------------------+ inline bool ExistPosition(void) const { return m_bContainsPosition; } //+------------------------------------------------------------------+ void UpdatePosition(void) { static int memPositions = 0, memOrder = 0; ulong ul; int p, o; p = PositionsTotal(); o = OrdersTotal(); if ((memPositions != p) || (memOrder != o)) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); RemoveAllsLines(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); memOrder = o; memPositions = p; m_bContainsPosition = false; }; for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); m_bContainsPosition = true; SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false); SetLineOrder(ul, PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, PositionGetDouble(POSITION_SL), HL_STOP, true); } for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { SetLineOrder(ul, OrderGetDouble(ORDER_PRICE_OPEN), HL_PRICE, true); SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true); SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true); } }; //+------------------------------------------------------------------+
高亮显示的代码行展示了运行所有检查所需的添加,这些检查将在稍后添加到 EA 代码的某些位置。
在某种程度上,我们可以只在 C_Router 类中实现所有检查和调整,但这还不够。 我稍后会解释。 现在,我们继续来修改。 创建上述检查后,我们添加一个构造函数来正确初始化新添加的变量。
C_Router() : m_bContainsPosition(false) {}
现在,我们编辑放置挂单的函数,如下所示:
ulong CreateOrderPendent(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true) { double last = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_LAST); ZeroMemory(TradeRequest); ZeroMemory(TradeResult); TradeRequest.action = TRADE_ACTION_PENDING; TradeRequest.symbol = Terminal.GetSymbol(); TradeRequest.volume = Volume; TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP)); TradeRequest.price = NormalizeDouble(Price, Terminal.GetDigits()); TradeRequest.sl = NormalizeDouble((m_bContainsPosition ? 0 : Stop), Terminal.GetDigits()); TradeRequest.tp = NormalizeDouble((m_bContainsPosition ? 0 : Take), Terminal.GetDigits()); TradeRequest.type_time = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC); TradeRequest.stoplimit = 0; TradeRequest.expiration = 0; TradeRequest.type_filling = ORDER_FILLING_RETURN; TradeRequest.deviation = 1000; TradeRequest.comment = "Order Generated by Experts Advisor."; if (!Send()) return 0; return TradeResult.order; };
高亮显示的部分是要实现的修改。
现在,我们回到更新后的代码进行新的修改。 请记住,它由 OnTrade 函数调用,每次更改订单时都会调用。 这可从下面的代码中看到:
void UpdatePosition(void) { static int memPositions = 0, memOrder = 0; ulong ul; int p, o; p = PositionsTotal(); o = OrdersTotal(); if ((memPositions != p) || (memOrder != o)) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); RemoveAllsLines(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); memOrder = o; memPositions = p; m_bContainsPosition = false; }; for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); m_bContainsPosition = true; SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false); SetLineOrder(ul, PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, PositionGetDouble(POSITION_SL), HL_STOP, true); } for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { if (m_bContainsPosition) { ModifyOrderPendent(ul, OrderGetDouble(ORDER_PRICE_OPEN), 0, 0); (OrderSelect(ul) ? 0 : 0); } SetLineOrder(ul, OrderGetDouble(ORDER_PRICE_OPEN), HL_PRICE, true); SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true); SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true); } };
现在,我们需要确保当已有持仓时,用户不会将一笔简单的挂单转换为挂单 OCO,即如果用户打开工具箱,并试图编辑止盈或止损。 当用户尝试这样做时,交易服务器将通过 OnTrade 函数通知我们,因此 EA 会立即知晓更改,并取消用户所做的更改,确保系统的可靠性。
但还有一件事也需要修改;它涉及市价订单。 这是一个非常简单的更改,因为它不需要修改任何与检查相关的内容。 新函数代码如下所示:
ulong ExecuteOrderInMarket(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true) { ZeroMemory(TradeRequest); ZeroMemory(TradeResult); TradeRequest.action = TRADE_ACTION_DEAL; TradeRequest.symbol = Terminal.GetSymbol(); TradeRequest.volume = Volume; TradeRequest.type = (IsBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL); TradeRequest.price = NormalizeDouble(Price, Terminal.GetDigits()); TradeRequest.sl = NormalizeDouble((m_bContainsPosition ? 0 : Stop), Terminal.GetDigits()); TradeRequest.tp = NormalizeDouble((m_bContainsPosition ? 0 : Take), Terminal.GetDigits()); TradeRequest.type_time = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC); TradeRequest.stoplimit = 0; TradeRequest.expiration = 0; TradeRequest.type_filling = ORDER_FILLING_RETURN; TradeRequest.deviation = 1000; TradeRequest.comment = "[ Order Market ] Generated by Experts Advisor."; if (!Send()) return 0; return TradeResult.order; };
尽管这看起来奇怪,但这些变化已经提供了足够的安全级别(至少是可接受的级别),当 EA 正在交易的同一资产已经存在持仓时,能够不错过 OCO、挂单或市价订单。 因此,EA 将实际负责订单提交系统。
好吧,这一切都太优美了,不可能是真的,对吗? 您也许认为这已经可以保证您拥有一个良好的安全边际了,但事实并非如此。 这些修改保证了当我们有持仓时,OCO 订单不会继续挂单、或不会入场。 但这些修改中有一个致命的缺陷,如果不正确排除,该缺陷可能会给您带来巨大的头痛和巨大的损失,这可能会破坏您的账户,或经纪商因保证金不足而强行平仓。
请注意,若没有检查挂单是否在持仓限价内,这是非常危险的,因为在系统当前状态下,当您在持仓限价外添加 OCO 挂单时,EA 将不允许该订单为 OCO 类型。 换言之:订单将没有限价,该订单将是一笔没有止盈或止损的订单,因此,当您平仓并触发该挂单时,您必须尽快调整这些价位。 如果您忘了做这些,那么您就有可能拥有一笔没有限价的持仓。
为了设置各个级别价位,您必须打开消息窗口,打开订单并编辑各个级别价位值。 此问题将很快得到修复。 现在,我们来修复当前的问题。
因此,我们需要改变 EA 处理挂单的方式,因为如果用户想要创建没有级别价位的订单,EA 会将其视为正常情况,但如果用户最终创建了限价订单,EA 将不得不相应调整订单:如果订单放置在持仓范围之外,则设置限价,或者如果订单被放置在持仓范围内,则移除挂单的限价。
2.0.2. 在限价内操作
我们要做的第一件事是创建验证限价。 这很简单做到。 然而,它需要大量关注细节,因为有两种可能的情况,但能扩展到更多的情况。 为了理解该怎么做,这两种情况就足够了。
第一种情况如上所示。 它将有一个超出限价的挂单(在这种情况下,限价是一个梯度区域,即我们有一笔持仓,做多或做空,并且我们有上限。 当价格达到或超过此限价时,会被平仓。 在这种情况下,挂单可以由用户配置为 OCO 订单,EA 必须接受用户配置订单的方式,无论是简单订单还是 OCO 订单 — 在此情况下,EA 不应干预。
第二种情况如下所示。 此处的挂单在持仓限价区域内。 在这种情况下,EA 必须删除用户可能配置的限价。
请注意,无论限价距离多远,无论我们做多还是做空,进入该区域的订单,EA 必须从挂单中删除限价值。 但如果它要保留在该区域,EA 必须按照用户配置的方式保留订单。
定义完后,我们需要创建一些变量,如下所示:
class C_Router : public C_HLineTrade { protected: MqlTradeRequest TradeRequest; MqlTradeResult TradeResult; private : bool m_bContainsPosition; struct st00 { double TakeProfit, StopLoss; bool IsBuy; }m_Limits; // ... Rest of the code
现在,我们有一种方法可以在 OnTrade 订单触发事件发生的阶段检查限价。 因此,我们再次修改 C_Router 类的更新函数。
// Rest of the code.... //+------------------------------------------------------------------+ #define macro_MAX(A, B) (A > B ? A : B) #define macro_MIN(A, B) (A < B ? A : B) void UpdatePosition(void) { static int memPositions = 0, memOrder = 0; ulong ul; int p, o; double price; bool bTest; p = PositionsTotal(); o = OrdersTotal(); if ((memPositions != p) || (memOrder != o)) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); RemoveAllsLines(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); memOrder = o; memPositions = p; m_bContainsPosition = false; m_Limits.StopLoss = -1; m_Limits.TakeProfit = -1; m_Limits.IsBuy = false; }; for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); m_bContainsPosition = true; SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false); SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true); m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; } for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { price = OrderGetDouble(ORDER_PRICE_OPEN); if ((m_Limits.StopLoss == -1) && (m_Limits.TakeProfit == -1)) bTest = false; else { bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price)); bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price)); bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price))); } if ((m_bContainsPosition) && (bTest)) { ModifyOrderPendent(ul, price, 0, 0); (OrderSelect(ul) ? 0 : 0); } SetLineOrder(ul, price, HL_PRICE, true); SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true); SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true); } }; #undef macro_MAX #undef macro_MIN //+------------------------------------------------------------------+ // ... The rest of the code...
现在,该类将能处理挂单的位置,以便区分它们是在不能有 OCO 挂单的区域内,还是在该区域外。 请注意,订单系统中的每个状态变更都将调用该函数。 它要做的第一件事是正确初始化变量。
m_Limits.StopLoss = -1; m_Limits.TakeProfit = -1; m_Limits.IsBuy = false;
一旦完成,我们将检查是否有持仓。 这可以在任何时候完成。 一旦我们有一笔持仓,它将划定不可能有 OCO 挂的区域 — 正是在这一点上实现的。
SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true); m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
现在我们可以检查每笔挂单,从而找出它们是否在限价区域内。 重要提示:这里我们需要知道订单是做多还是做空,因为我们可能没有止盈,但有止损。 那么,在这种情况下,有必要知道持仓的类型。 若要理解这一点,请参见下图:
如果是做空持仓,止损标志着上限...
对于做多持仓,止损则是一个下限。
换言之,在某些情况下,如果订单被放置在最大处,则挂单可能属于 OCO 类型。 在其它情况下,应将其置于最低处以下。 但也可能存在另一种情况,即挂单也可以是 OCO 类型,如下所示:
超出限价的挂单,这是典型的情况。
为了检查这一点,我们用到以下片段
price = OrderGetDouble(ORDER_PRICE_OPEN); if ((m_Limits.StopLoss == -1) && (m_Limits.TakeProfit == -1)) bTest = false; else { bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price)); bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price)); bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price))); }
它将检查是否有任何持仓。 若无,EA 必须遵守用户指定的订单设置。 如果发现一笔持仓,将按以下顺序执行以下检查:
- 如果我们是做空,则挂单的价格必须大于持仓的止损值;
- 如果我们是做多,则放置挂单的价格必须低于持仓的止损值;
- 如果系统仍将订单作为 OCO 类型接受,我们将进行最后一次测试,以便查看订单是否错位。
一旦完成,我们将确保挂单可以保留,也可以不按用户配置的方式保留,生活将继续... 但在我们进入下一步之前,还有最后一项补充。 实际上,这是我在文章开头提到的最后一次检查,这在下面的片段中:
void UpdatePosition(void) { // ... Internal code... for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); m_bContainsPosition = true; SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false); SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true); m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; } if (AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) m_bContainsPosition = false; for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { price = OrderGetDouble(ORDER_PRICE_OPEN); if (m_bContainsPosition) { if ((m_Limits.StopLoss == -1) && (m_Limits.TakeProfit == -1)) bTest = false; else { bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price)); bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price)); bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price))); } if (bTest) { ModifyOrderPendent(ul, price, 0, 0); (OrderSelect(ul) ? 0 : 0); } } SetLineOrder(ul, price, HL_PRICE, true); SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true); SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true); } };
在高亮显示的部分,我们检查账户类型是否为对冲。 若是的话,即使变量表明我们必须使用限价,但此刻它也将表明(对冲特性)我们不需要使用限价。 因此,EA 将忽略可能出现的任何限价,并按照用户配置的方式处理订单。 这是一个非常简单的检查,但需要在现阶段完成,以确保整个系统正常运行。
2.1.0. 调整仓位系统
虽然通过调整 C_Router 对象类解决了大多数问题,但我们仍然没有足以胜任的系统。 还有一个问题需要解决:通过鼠标校正仓位系统,这是另一个同样重要的步骤。 它有若干个含义,因为我们必须定义 C_OrderView 类中存在的系统应该如何操作。
最大的问题是,取决于您实际想要如何操作,C_OrderView 类在创建挂单是应否保留持仓的限价。
尽管每次都这么做很诱人,但在做出这个决定时必须考虑到一些事情。 但是,我们按照开膛手杰克所说的部分来进行。 基本上,我们在 C_OrderView 类中实际需要做的唯一更改如下所示:
inline void MoveTo(uint Key) { static double local = 0; datetime dt; bool bEClick, bKeyBuy, bKeySell, bCheck; double take = 0, stop = 0, price; bEClick = (Key & 0x01) == 0x01; //Left click bKeyBuy = (Key & 0x04) == 0x04; //SHIFT pressed bKeySell = (Key & 0x08) == 0x08; //CTRL pressed Mouse.GetPositionDP(dt, price); if (bKeyBuy != bKeySell) { Mouse.Hide(); bCheck = CheckLimits(price); } else Mouse.Show(); ObjectMove(Terminal.Get_ID(), m_Infos.szHLinePrice, 0, 0, price = (bKeyBuy != bKeySell ? price : 0)); ObjectMove(Terminal.Get_ID(), m_Infos.szHLineTake, 0, 0, take = (bCheck ? 0 : price + (m_Infos.TakeProfit * (bKeyBuy ? 1 : -1)))); ObjectMove(Terminal.Get_ID(), m_Infos.szHLineStop, 0, 0, stop = (bCheck ? 0 : price + (m_Infos.StopLoss * (bKeyBuy ? -1 : 1)))); if((bEClick) && (bKeyBuy != bKeySell) && (local == 0)) CreateOrderPendent(bKeyBuy, m_Infos.Volume, local = price, take, stop, m_Infos.IsDayTrade); local = (local != price ? 0 : local); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE)); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineTake, OBJPROP_COLOR, (take > 0 ? m_Infos.cTake : clrNONE)); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE)); };
全部就这些吗? 是的,这就是我们需要做的。 所有其余的逻辑都在 C_Router 类中。 未修改部分则由 MetaTrader 5 自己的消息传递系统执行,因为当订单列表(挂单或持仓)发生变化时,将调用 OnTrade 例程。 当这种情况发生时,它将触发 C_Router 类中的更新例程,该例程将进行必要的调整。 但是这个例程中出现了一段代码,它会让您疯狂地寻找它的所在。 事实上,它在 C_Router 类中,如下所示:
#define macro_MAX(A, B) (A > B ? A : B) #define macro_MIN(A, B) (A < B ? A : B) inline bool CheckLimits(const double price) { bool bTest = false; if ((!m_bContainsPosition) || ((m_Limits.StopLoss == -1) && (m_Limits.TakeProfit == -1))) return bTest; bTest = ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price)); if (m_Limits.TakeProfit == 0) { bTest = (bTest ? bTest : (!m_Limits.IsBuy) && (m_Limits.StopLoss > price)); bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price)); } return bTest; }; #undef macro_MAX #undef macro_MIN
此代码位于 C_Router 类的更新函数当中。 它已从该处被删除,并替换为调用...
2.2.0. 限价还是不限价,这是个问题
我们的工作几乎完成了,但最后一个问题仍有待解决,这可能是目前最棘手的问题。 如果您一直遵循并理解内容,您可能已经注意到系统运行得很好,但无论何时,配置为 OCO 的挂单进入持仓的限价,该订单都会丢失为其配置的限价。 这将始终发生。
但是,如果交易者偶然改变了持仓限价,或者如果一个原本是 OCO 且现在是简单订单的订单超出了这些限价,则它仍然是简单订单。 所以,我们有一个潜在的问题。
另一个重要细节:EA 应该如何处理? 它是否应该通知交易者资产的简单订单刚刚出现? 还是应该简单地为订单设置限价,并将其转化为 OCO?
如果您真的希望 EA 帮助您进行交易,这个问题极端相关。 EA 发出警告,告知我们发生了什么,这将是一件好事。 但是,如果您在一项资产中,在高波动性的时候,让 EA 自动为订单创建一些限价也是不错的,如此它就不会松散地呆在那里,这可能会在我们意识到发生了什么之前造成巨大的损失。
因此,为了解决这个问题,系统进行了最后一次修改,但正如我在上面解释的,您应该认真研究如何实际处理这个问题。 下面是我如何实现一个可能的解决方案。
首先,我添加了一个新变量,交易者可以通过该变量通知 EA 将执行哪个程序。 它如下所示:
// ... Code ... input group "Chart Trader" input int user20 = 1; //Leverage factor input int user21 = 100; //Take Profit (financial) input int user22 = 75; //Stop Loss (financial) input color user23 = clrBlue; //Price line color input color user24 = clrForestGreen; //Take Profit line color input color user25 = clrFireBrick; //Stop Loss line color input bool user26 = true; //Day Trade? input bool user27 = true; //Always set loose order limits // ... Rest of the code... void OnTrade() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, OrderView.UpdateRoof(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY]); OrderView.UpdatePosition(user27); } // ... Rest of the code...
现在我们需要回到 C_Router 类,并向其添加 3 个新函数。 它们如下所示:
//+------------------------------------------------------------------+ void SetFinance(const int Contracts, const int Take, const int Stop) { m_Limits.Contract = Contracts; m_Limits.FinanceTake = Take; m_Limits.FinanceStop = Stop; } //+------------------------------------------------------------------+ inline double GetDisplacementTake(const bool IsBuy, const double Vol) const { return (Terminal.AdjustPrice(m_Limits.FinanceTake * (Vol / m_Limits.Contract) * Terminal.GetAdjustToTrade() / Vol) * (IsBuy ? 1 : -1)); } //+------------------------------------------------------------------+ inline double GetDisplacementStop(const bool IsBuy, const double Vol) const { return (Terminal.AdjustPrice(m_Limits.FinanceStop * (Vol / m_Limits.Contract) * Terminal.GetAdjustToTrade() / Vol) * (IsBuy ? -1 : 1)); } //+------------------------------------------------------------------+
代码将保持图表交易者中通知的值,如下图所示,但也会按比例修正我们应在 OCO 挂单中用作限价的值。
也就是说,我们已经有了获取所需值的位置,如此 EA 可以在触发挂单时最小限度地配置 OCO 订单。 然而,正如您可能怀疑的,我们将不得不对 C_Router 类的更新代码进行新的修改。 修改如下所示:
void UpdatePosition(bool bAdjust) { static int memPositions = 0, memOrder = 0; ulong ul; int p, o; long info; double price, stop, take, vol; bool bIsBuy, bTest; p = PositionsTotal(); o = OrdersTotal(); if ((memPositions != p) || (memOrder != o)) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); RemoveAllsLines(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); memOrder = o; memPositions = p; m_bContainsPosition = false; m_Limits.StopLoss = -1; m_Limits.TakeProfit = -1; m_Limits.IsBuy = false; }; for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); m_bContainsPosition = true; SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false); SetLineOrder(ul, take = PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, stop = PositionGetDouble(POSITION_SL), HL_STOP, true); m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; m_Limits.TakeProfit = (m_Limits.TakeProfit < 0 ? take : (m_Limits.IsBuy ? (m_Limits.TakeProfit > take ? m_Limits.TakeProfit : take) : (take > m_Limits.TakeProfit ? m_Limits.TakeProfit : take))); m_Limits.StopLoss = (m_Limits.StopLoss < 0 ? stop : (m_Limits.IsBuy ? (m_Limits.StopLoss < stop ? m_Limits.StopLoss : stop) : (stop < m_Limits.StopLoss ? m_Limits.StopLoss : stop))); } if ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) m_bContainsPosition = false; for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { price = OrderGetDouble(ORDER_PRICE_OPEN); take = OrderGetDouble(ORDER_TP); stop = OrderGetDouble(ORDER_SL); bTest = CheckLimits(price); if ((take == 0) && (stop == 0) && (bAdjust) && (!bTest)) { info = OrderGetInteger(ORDER_TYPE); vol = OrderGetDouble(ORDER_VOLUME_CURRENT); bIsBuy = ((info == ORDER_TYPE_BUY_LIMIT) || (info == ORDER_TYPE_BUY_STOP) || (info == ORDER_TYPE_BUY_STOP_LIMIT) || (info == ORDER_TYPE_BUY)); take = price + GetDisplacementTake(bIsBuy, vol); stop = price + GetDisplacementStop(bIsBuy, vol); ModifyOrderPendent(ul, price, take, stop); } if ((take != 0) && (stop != 0) && (bTest)) ModifyOrderPendent(ul, price, take = 0, stop = 0); SetLineOrder(ul, price, HL_PRICE, true); SetLineOrder(ul, take, HL_TAKE, true); SetLineOrder(ul, stop, HL_STOP, true); } };
高亮显示的代码行将检查订单是否为自由订单,以及 EA 是否应干预订单。 如果 EA 介入,将根据图表交易者中显示的财务价值和挂单交易量进行计算。 然后,简单挂单将基于收集的信息接收计算出的限价。 EA 将创建一行来通知将创建限价,从而将简单挂单转换为 OCO 挂单。
结束语
尽管所有的尝试都是为了测试系统,看看它是否将账户识别为对冲,但我在这个阶段没有成功。 EA 始终报告该账户处于净持结算模式,即使 MetaTrader 5 平台报告该账户正是对冲。 因此,您应该小心。 虽然它按我们所希望的那样工作,但即使是在对冲账户上,也会调整挂单,就像它是一个净持账户一样...
视频清晰地展示了上述内容。 正如您所看到的,这个系统使用起来非常有趣。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10462