从头开始开发智能交易系统(第 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