
从头开始开发智能交易系统(第 19 部分):新订单系统 (II)
概述
在上一篇文章中,从头开始开发交易专家顾问(第 18 部分),我们在订单系统中实现了一些修复、更改和调整,旨在创建一个系统,能够在净持和对冲不同结算类型的账户里执行交易,因为账户操作存在差异。 对于净持结算类型,系统生成一个平均价格,且每种资产只能有一笔持仓。 在对冲账户中,您可以有多笔持仓,每笔持仓都有各自的限定。 您可以同时买卖相同的资产。 这样的操作只能在对冲账户上进行。 这是了解期权交易的基础。
但现在是时候令订单系统完全可视化了,这样我们就可以剔除消息框,且在不借助它的情况下分析每笔持仓的数值。 我们可以通过新的订单系统来做到这一点。 这将允许我们一次调整若干件事情。 此外,由于 EA 将实时显示相关信息,不需要任何额外计算,我们就能够轻松了解 OCO 订单或挂单的盈亏限额。
虽然这只是第一部分的实现,但我们并不是从头开始:我们将修改现有系统,往正在交易的资产图表里添加更多对象和事件。
1.0. 计划
我们在这里所用的系统规划并不特别困难:我们将修改现有系统,只更改图表上表示订单的系统。 这是主要思路,看起来十分简单。 但实际上,这需要很强的创造力,因为我们要操纵和数据建模,从而 MetaTrader 5 平台能替我们完成所有的艰苦工作。
有若干种方法可对数据建模,每种方法都有其优缺点。
- 第一种方法是使用列表。 它可以是单循环、双循环,甚至是哈希系统。 使用任何这些方式的优点是系统易于实现。 然而,缺点则是要防止数据操纵或限制订单数量。 甚而,在这种情况下,我们必须创建保存列表的所有额外逻辑。
- 第二种方式是创建类数组,而类将包含并维护所有新创建的对象。 在这种情况下,数组将像列表一样工作,但我们必须编写少量代码,因为 MQL5 已经支持在使用列表的情况下原来必须编写代码的一些东西。 然而,我们会有其它问题,例如事件处理,在这种状况下,事情会变得非常困难。
- 第三种方式是我们将要采取的方式。 我们将强制以 MQL5 创建代码来支持动态对象。 这似乎看起来有些不现实,但如果我们针对所用数据进行正确建模,那么 MQL5 语言将令我们能够创建一个系统,且其对屏幕上的对象数量没有限制。 甚至,所有对象都将能够生成和接收事件。 尽管它们有各自的特点,但平台将看到它们都链接起来,就像它们在列表或数组索引中一样。
如果您认为这不太容易实现,请查看 C_HLineTrade 类的以下代码部分:
inline void SetLineOrder(ulong ticket, double price, eHLineTrade hl, bool select) { string sz0 = def_NameHLineTrade + (string)hl + (string)ticket, sz1; ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, 0, 0, 0); //... The rest of the code....
高亮显示的部分准确地向我们展示了可以创建任意数量的水平线,它们将以完全独立的方式接收事件。 我们所有需要做的就是根据每一行的名称实现事件,因为名称是唯一的。 MetaTrader 5 平台将负责剩下的工作。 结果大约如下所示:
虽然这看起来已经很理想了,但这样建模不足以实现我们真正需要的结果。 这个思路已经可以实现。 但 EA 中当前可用的数据建模并不理想,因为我们不能基于一个名称拥有无限数量的对象。 我们需要进行一些修改,而这需要相当深入地修改代码。
我们现在将开始实现这种新的数据建模方法,但我们只是简单地修改必要的内容,同时保持整体代码的稳定,因为它应该尽可能稳定地继续工作。 所有操作将由 MetaTrader 5 平台执行,我们仅指示平台应如何理解我们的建模。
2.0. 实现
第一处修改是将 C_HLineTrade 更改为新的 C_ObjectsTrade 类。 这个新类将能够支持我们所需要的 — 能够链接无限数量对象的一种方法。
我们先看看下面代码中的原始定义。
class C_ObjectsTrade { //+------------------------------------------------------------------+ #define def_NameObjectsTrade "SMD_OT" #define def_SeparatorInfo '*' #define def_IndicatorTicket0 1 //+------------------------------------------------------------------+ protected: enum eIndicatorTrade {IT_NULL, IT_STOP= 65, IT_TAKE, IT_PRICE}; //+------------------------------------------------------------------+ // ... The rest of the class code
此处我们有了我们将要实现的初始基础。 它在未来将得到扩展,但目前我希望系统保持稳定,尽管它正在被修改,并具有新的数据建模。
即使在受保护部分声明中,我们也拥有以下函数:
inline double GetLimitsTake(void) const { return m_Limits.TakeProfit; } //+------------------------------------------------------------------+ inline double GetLimitsStop(void) const { return m_Limits.StopLoss; } //+------------------------------------------------------------------+ inline bool GetLimitsIsBuy(void) const { return m_Limits.IsBuy; } //+------------------------------------------------------------------+ inline void SetLimits(double take, double stop, bool isbuy) { m_Limits.IsBuy = isbuy; m_Limits.TakeProfit = (m_Limits.TakeProfit < 0 ? take : (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 : (isbuy ? (m_Limits.StopLoss < stop ? m_Limits.StopLoss : stop) : (stop < m_Limits.StopLoss ? m_Limits.StopLoss : stop))); } //+------------------------------------------------------------------+ inline int GetBaseFinanceLeveRange(void) const { return m_BaseFinance.Leverange; } //+------------------------------------------------------------------+ inline int GetBaseFinanceIsDayTrade(void) const { return m_BaseFinance.IsDayTrade; } //+------------------------------------------------------------------+ inline int GetBaseFinanceTakeProfit(void) const { return m_BaseFinance.FinanceTake; } //+------------------------------------------------------------------+ inline int GetBaseFinanceStopLoss(void) const { return m_BaseFinance.FinanceStop; }
目前,这些函数只是作为未来我们将要实现的另一个方案的保险措施。 尽管我们可以在另一个位置实现数据和解析,但最好尽可能在继承链底层保留一些东西。 即使返回值仅由派生类取用,我也不愿允许这样直接:我不想让派生类访问 C_ObjectsTrade 对象类中的值,因为这将打破对象类封装的思想,如果派生类在不经由过程调用的情况下就能变更相关基类的值,这令未来的修改或错误修复变得困难。
为了尽可能减少调用重叠,所有函数都声明为内联:这稍微增加了可执行文件的大小,但会导致更安全的系统。
现在我们来到私密声明。
//+------------------------------------------------------------------+ private : string m_SelectObj; struct st00 { double TakeProfit, StopLoss; bool IsBuy; }m_Limits; struct st01 { int FinanceTake, FinanceStop, Leverange; bool IsDayTrade; }m_BaseFinance; //+------------------------------------------------------------------+ string MountName(ulong ticket, eIndicatorTrade it) { return StringFormat("%s%c%c%c%d", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket); } //+------------------------------------------------------------------+
最重要的部分是高亮显示的片段,它将建模对象的名称。 我保留了一些基础,它们在系统中仍然可用。 这是因为我们首先创建和修改建模,保持系统稳定。 然后我们将添加新对象,而这将非常容易、快速地完成。 甚至,我们将保持已经实现的稳定。
虽然代码经历了比这里所示更多的变化,但我只关注新函数,以及与以前代码相比变化相当大的部分。
第一个函数如下所示:
inline string CreateIndicatorTrade(ulong ticket, eIndicatorTrade it, bool select) { string sz0 = MountName(ticket, it); ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, 0, 0, 0); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_COLOR, (it == IT_PRICE ? clrBlue : (it == IT_STOP ? clrFireBrick : clrForestGreen))); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_WIDTH, 1); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_STYLE, STYLE_DASHDOT); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTABLE, select); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTED, false); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_BACK, true); ObjectSetString(Terminal.Get_ID(), sz0, OBJPROP_TOOLTIP, (string)ticket + " "+StringSubstr(EnumToString(it), 3, 10)); return sz0; }
目前,它只会创建一条水平线。 注意名称生成代码;还要注意,颜色现在将由代码内部定义,而不是由用户定义。
然后我们重载相同的函数,如下所示。
inline string CreateIndicatorTrade(ulong ticket, double price, eIndicatorTrade it, bool select) { if (price <= 0) { RemoveIndicatorTrade(ticket, it); return NULL; } string sz0 = CreateIndicatorTrade(ticket, it, select); ObjectMove(Terminal.Get_ID(), sz0, 0, 0, price); return sz0; }
不要把这两个函数混淆,因为尽管它们看起来一样,但实际上不同。 重载非常常见:我们创建一个简单的函数,然后往其中添加新参数,以便累积某种类型的建模。 如果我们没有通过重载实现它,我们有时将不得不重复相同的代码。 这很危险,因为我们会忘记声明一些东西。 此外,这也不太实用,因此我们重载的函数只需调用一个,取代调用若干个。
于此应提到的一件事是第二个版本中高亮显示的部分。 不需要在此处创建它,我们可以在其它地方做这件事。 但是,正如所见,当我们尝试以零价格创建一些对象时,实际上它必须被销毁。
为了实际查看发生这种情况的时刻,请查看下面的代码:
class C_Router : public C_ObjectsTrade { // ... Internal class code .... void UpdatePosition(int iAdjust = -1) { // ... Internal function code ... for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); m_bContainsPosition = true; CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false); CreateIndicatorTrade(ul, take = PositionGetDouble(POSITION_TP), IT_TAKE, true); CreateIndicatorTrade(ul, stop = PositionGetDouble(POSITION_SL), IT_STOP, true); // ... The rest of the code...
每次 EA 收到 OnTrade 事件时,它将执行上述函数,并尝试在选定点上创建一个指标,但如果用户删除限制,则该指标将变为零。 因此,当调用时,它实际上会从图表中删除指标,内存中已无用的对象也可为我们节省下来。 由此,我们在某些方面有所收获,因为检查将在创建时刻正确完成。
但我们仍然有重载的问题,因为有些人可能不完全理解在实际中如何使用代码。 为了理解这一点,请查看以下两段代码:
class C_OrderView : public C_Router { private : //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ void InitBaseFinance(int nContracts, int FinanceTake, int FinanceStop, bool b1) { SetBaseFinance(nContracts, FinanceTake, FinanceStop, b1); CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false); CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE, false); CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP, false); } //+------------------------------------------------------------------+ // ... Rest of the code... class C_Router : public C_ObjectsTrade { // ... Class code ... void UpdatePosition(int iAdjust = -1) { // ... Function code .... for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); m_bContainsPosition = true; CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false); // ... The rest of the code...
请注意,在这两种情况下,我们所用的函数名称相同。 此外,它们都是同一个 C_ObjectsTrade 类的一部分。 然而,即使在这种情况下,编译器也可能区分它们,这是由于参数的数量。 如果您仔细观察,就会发现唯一的区别是一个额外的 “price” 参数,但也许还有其它一些参数。 正如您所看到的,调用一个 N 合一的重载版本要比复制所有代码要容易得多,如此最终我们得到了更干净的代码,更易于维护。
现在我们回到 C_ObjectsTrade 类。 下一个我们需要理解的函数如下:
bool GetInfosOrder(const string &sparam, ulong &ticket, double &price, eIndicatorTrade &it) { string szRet[]; char szInfo[]; if (StringSplit(sparam, def_SeparatorInfo, szRet) < 2) return false; if (szRet[0] != def_NameObjectsTrade) return false; StringToCharArray(szRet[1], szInfo); it = (eIndicatorTrade)szInfo[0]; ticket = (ulong) StringToInteger(szRet[2]); price = ObjectGetDouble(Terminal.Get_ID(), sparam, OBJPROP_PRICE); return true; }
事实上,它是整个新系统的心脏、大脑和身体。 虽然它看起来很简单,但它所做的工作对于整个 EA 来说是至关重要的,因为我们的新建模系统需要它。
密切注意高亮显示的代码,特别是 StringSplit 函数。 如果它在 MQL5 中不存,我们就必须为其进行编码。 幸运的是,MQL5 已拥有它,因此我们就可充分利用这个函数。 它所做的就是将对象的名称分解为所需的数据。 当创建对象名称时,它以非常特定的方式建模,因此我们可以撤消此编码模型,因此 StringSplit 将撤消 StringFormat 函数所做的工作。
函数的其余部分捕获对象名称中存在的数据,以便我们稍后测试和使用。 也就是说,MetaTrader 5 为我们生成数据,我们对数据进行分解,以便探知发生了什么,然后告诉 MetaTrader 5 应该采取哪些步骤。 我们的目的是让 MetaTrader 5 为我们工作。 我不会从头开始创建模型;取而代之,我会从零开始为界面和 EA 建模。 因此,我们应该从 MetaTrader 5 提供的支持当中受益,而不必寻找外部解决方案。
在下面的代码中,我们将完成与上面所做的非常类似的操作:
inline void RemoveAllsIndicatorTrade(bool bFull) { string sz0, szRet[]; int i0 = StringLen(def_NameObjectsTrade); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); for (int c0 = ObjectsTotal(Terminal.Get_ID(), -1, -1); c0 >= 0; c0--) { sz0 = ObjectName(Terminal.Get_ID(), c0, -1, -1); if (StringSubstr(sz0, 0, i0) == def_NameObjectsTrade) { if (!bFull) { StringSplit(sz0, def_SeparatorInfo, szRet); if (StringToInteger(szRet[2]) == def_IndicatorTicket0) continue; } }else continue; ObjectDelete(Terminal.Get_ID(), sz0); } ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); }
每次我们从图表中删除一条线,无论是要平仓,亦或是要删除的限价等级,都必须删除相应的对象,就像从图表中移除 EA 一样。 我们需要删除这些对象,但我们也要有一组标线,除非绝对必要,否则不应删除:这是 Ticket0,除非非常必要,否则不得删除。 为了避免将其删除,我们要把代码高亮显示。 如果没有这个,我们每次都需要重新创建这个 Ticket0,因为这个 ticket 在另一个代码部分中非常重要,我们将在后面讨论。
在所有其它时间,我们需要删除一些特定的内容。 为此,我们将调用另一个移除函数,如下所示。
inline void RemoveIndicatorTrade(ulong ticket, eIndicatorTrade it = IT_NULL) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); if ((it != NULL) && (it != IT_PRICE)) ObjectDelete(Terminal.Get_ID(), MountName(ticket, it)); else { ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_PRICE)); ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_TAKE)); ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_STOP)); } ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); }
下一个新例程如下所示:
inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy) { double ad = Terminal.GetAdjustToTrade() / (Leverange * Terminal.GetVolumeMinimal()); ObjectMove(Terminal.Get_ID(), MountName(ticket, it), 0, 0, price); if (it == IT_PRICE) { ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_TAKE), 0, 0, price + Terminal.AdjustPrice(FinanceTake * (isBuy ? ad : (-ad)))); ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_STOP), 0, 0, price + Terminal.AdjustPrice(FinanceStop * (isBuy ? (-ad) : ad))); } }
它将在价格轴上放置对象。 但不要太依附于它,因为它很快会因为各种原因而不复存在。 它们当中之一是我们在本系列的另一篇文章中谈到的:一个图表上的多个指标(第 05 部分):将 MetaTrader 5 转换为 RAD(I)系统。 本文带有一个表格,显示了可以使用笛卡尔坐标进行定位的对象,这些坐标是 X 和 Y。价格和时间坐标尽管在某些情况下很有用,但并非始终方便:当我们想要在屏幕上的某些点上定位元素时,尽管使用价格和时间座标开发东西会更快,它们比 X 和 Y 系统更难精确定位。
我们将在下一次进行修改,而现在我们的目的是创建一个替代目前所用的系统。
接下来,我们在 C_ObjectsTrade 类中有最后一个重要函数。 代码如下所示:
inline double GetDisplacement(const bool IsBuy, const double Vol, eIndicatorTrade it) const { int i0 = (it == IT_TAKE ? m_BaseFinance.FinanceTake : m_BaseFinance.FinanceStop), i1 = (it == IT_TAKE ? (IsBuy ? 1 : -1) : (IsBuy ? -1 : 1)); return (Terminal.AdjustPrice(i0 * (Vol / m_BaseFinance.Leverange) * Terminal.GetAdjustToTrade() / Vol) * i1); }
此函数会把在图表交易者中指定的挂单或市价开仓的数值进行转换。
所有这些修改都是为了实现将 C_HLineTrade 函数转换为 C_ObjectsTrade。 然而,这些变更还需要一些其它修改。 例如,有些类也发生了显著变化,如 C_ViewOrder。 这个类的某些部分不复存在,因为它们的存在没有意义,而其余的函数已经改变。 某些值得特别注意的函数如下高亮所示。
第一个是初始化来自图表交易者的数据的函数。
void InitBaseFinance(int nContracts, int FinanceTake, int FinanceStop, bool b1) { SetBaseFinance(nContracts, FinanceTake, FinanceStop, b1); CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false); CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE, false); CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP, false); }
高亮显示的部分是实际创建 Ticket0 的地方。 此 ticket 可用来根据鼠标和键盘放置挂单:(SHIFT)买入,(CTRL)卖出。 以前,在这一点上创建了等级线,这些等级线用于指示订单的位置。 现在事情简单得多:当我们看到一笔订单时,我们也会看到一笔挂单或一笔持仓。 这意味着我们将始终检查系统。 这就像您要组装一辆汽车,您所有的时间都在检查它的制动器,如此当您真的需要用到它们时,您就会知道它的行为。
冗长代码的最大问题是,当我们创建函数时,我们只能在实际使用时才知道它能否工作。 但现在系统总是在检查 — 即使我们并未调用所有的函数,但因代码在不同的地方重用,故它们也会不断被检查。
我在本文中要提到的最后一个例程如下所示。 它将放置一笔挂单。 请注意,与前几篇文章中的相同函数相比,它变得极致紧凑。
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; //Let mouse button click bKeyBuy = (Key & 0x04) == 0x04; //Pressed SHIFT bKeySell = (Key & 0x08) == 0x08; //Pressed CTRL Mouse.GetPositionDP(dt, price); if (bKeyBuy != bKeySell) { Mouse.Hide(); bCheck = CheckLimits(price); } else Mouse.Show(); PositionAxlePrice((bKeyBuy != bKeySell ? price : 0), def_IndicatorTicket0, IT_PRICE, (bCheck ? 0 : GetBaseFinanceTakeProfit()), (bCheck ? 0 : GetBaseFinanceStopLoss()), GetBaseFinanceLeveRange(), bKeyBuy); if((bEClick) && (bKeyBuy != bKeySell) && (local == 0)) CreateOrderPendent(bKeyBuy, local = price); local = (local != price ? 0 : local); }
原因是,现在系统中会有一个新规则,因此函数“减轻了一些负重”,变得更加紧凑。
结束语
我已在这里介绍了一些将在下一篇文章中用到的变更。 所有这些的目的是令它们更简单,并在不同时间展现出可能不同的事物。 我的想法是,每个人都学习并遵循如何编写 EA,从而帮助您进行操作,这就是为什么我不单只是介绍一个完整并可立即使用的系统。 我想表明,有一些问题需要解决,并介绍我在解决开发过程中出现的问题,和解决问题时所采取的途径。 我希望您能理解这一点。 因为如果打算创建一个系统,并以完备待用的形式呈现,那我最好这样做,并推销这个想法,但这并非我的意图...
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10474
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


