
从头开始开发智能交易系统(第 20 部分):新订单系统 (III)
概述
在上一篇文章从头开始开发智能交易系统(第 19 部分)中,我们重点讨论了实现新订单系统操作的代码更改。 鉴于这些变化均已实现,我可以 100% 关注真正的问题。 这是为了实现订单系统,对于那些不知道何为跳价值,或在哪里下单能赚钱,或在哪里设置止损才能不亏损的交易者来说,该系统是 100% 可视化和可理解的。
创建这样的一个系统需要熟练地掌握 MQL5,以及了解 MetaTrader 5 平台的实际工作方式,及其提供的资源。
1.0. 计划
1.0.1. 设计指标
此处的想法,好在不仅是个想法,我实际上要做的是在本文中展示如何实现该系统。 我们将创建类似于下图所示的内容:
即使没有我的解释,它也很容易理解。 这里有一个平仓按钮、一个值和一个点,能轻松地拖动和下订单。 但这还不是全部。 当停损变成停止增益时,系统将按如下方式处理:
因此,我们可以很容易地知道什么何时、多少、何处、以及是否值得持有一笔确定仓位。
上图仅显示了 OCO 订单或 OCO 仓位限制对象,但我没有忘记与开盘价相关的部分,因为这同样重要。
对于挂单,如下所示:
对于一笔持仓,这看起来有点不同:
然而,比例不是很令人鼓舞... 但这是将要实现的想法。 至于颜色,我将采用这里显示的颜色。 但您可选择您喜欢的。
若我们继续规划时,我们可以注意到每个指标中基本上有 5 个对象。 这意味着 MetaTrader 5 必须同时为每个指标处理 5 个对象。 在 OCO 订单的情况下,MetaTrader 5 必须处理 15 个对象,就像 OCO 仓位一样,MetaTrader5 必须处理每笔订单或仓位的 15 个对象。 这意味着,如果您有 4 笔待处理的 OCO 订单,和 1 笔 OCO 持仓,MetaTrader 5 必须要处理 25 个对象,来自其它对象的也要在图表上。 且如果您在平台中只用到一种资产。
我这样说是因为对于您将要交易的金融产品,了解其每笔订单可能需要的内存和处理非常重要。 好吧,对于现代计算机来说,这不是一个问题,但有必要知道我们对硬件的具体需求。 以前,对于订单的每个点,屏幕上只有一个对象。 现在每个点中都有 5 个对象,它们必须以某种方式保持连接。 这个连接将由平台实现,而我们只需说明它们应该如何连接,以及当每个对象触发时应该发生什么。
1.0.2. 选择对象
下一个问题是我们将要用到的对象选择。 这也许看似是一个简单的问题,但它却是一个非常重要的问题,因为由它判定实现将如何真正进行。 第一种选择基于对象在屏幕上的定位方式。
我们有两种方式来实现这一点。 幸运的是,MetaTrader 5 涵盖了这两个方面:第一个是用时间和价格坐标来定位;第二个是用笛卡尔 X 和 Y 坐标。
然而,在我们详细讨论其中一个之前,我立即放弃了采用时间和价格坐标的模型。 尽管乍一看它是理想的,但当我们处理如此多的对象时,它们彼此将相互连接,并且必须保持在一起,而这毫无用处。 因此,我们必须采用笛卡尔系统。
在前面的一篇文章中,我们曾研究过这个系统,并讨论了如何选择对象。 有关详细信息,请参阅一个图表上的多个指标(第 05 部分)。
我们已经完成了规划,现在我们终于可以开始编码了:是时候在实践中变现了。
2.0. 实现
我的目的不单是实现系统,还要解释系统中到底发生了什么,以便您也可以基于研究过的系统创建自己的系统,我将一点一点地提供详细信息。 这将有助您了解它是如何创建的。 不要忘记,与挂单相关的任何功能也适用于持仓,因为系统遵循相同的原则,并有通用代码。
2.0.1. 创建界面框架
第一步的结果如下所示。 这就是我展示这些益处的方式,如此您就会感受到如我在开发并决定与大家分享这些代码时一样的兴奋。 我希望这将成为那些想要学习编程,或开发更深层主题知识的人的动机。
查看上图,您也许会想到该功能是以常规方式创建的,派出了迄今为止创建的所有代码。 但并非如此,我们将用到迄今为止所有已经构建的内容。
那么,我们将使用上一篇文章中讲述的代码,并对其进行一些修改。 好了,我们来关注代码中的新内容。 首先,我们将添加三个新类。
2.0.1.1. C_Object_Base 类
我们将首先创建一个新类 — C_Object_Base。 这是我们系统中的最底层类。 该类的第一段代码如下所示:
class C_Object_Base { public : //+------------------------------------------------------------------+ void Create(string szObjectName, ENUM_OBJECT typeObj) { ObjectCreate(Terminal.Get_ID(), szObjectName, typeObj, 0, 0, 0); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTABLE, false); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTED, false); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, true); ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n"); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, false); ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n"); PositionAxleY(szObjectName, 9999); }; // ... 该类的其余代码
请注意,我们有通用代码,这令我们的生活更加轻松。 在同一个类中,我们有标准的 X 和 Y 定位代码。
void PositionAxleX(string szObjectName, int X) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XDISTANCE, X); }; //+------------------------------------------------------------------+ virtual void PositionAxleY(string szObjectName, int Y) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y); };
Y 定位代码将取决于特定对象,但即使该对象没有特定代码,该类也能提供通用代码。 我们有一种为对象指定颜色的通用方法,如下所示。
virtual void SetColor(string szObjectName, color cor) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor); }
且此处是定义物体维度的方式。
void Size(string szObjectName, int Width, int Height) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, Width); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, Height); };
这就是 C_Object_Base 类的所有内容,但我们将在稍后讨论。
2.0.1.2. C_Object_BackGround 类
现在,我们创建另外两个类,来支持两个图形对象。 它们当中头一个是 C_Object_BackGround。 它创建一个背景框来容纳其它元素。 其代码非常简单。 您可以在下面看到它的全部:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_Object_Base.mqh" //+------------------------------------------------------------------+ class C_Object_BackGround : public C_Object_Base { public: //+------------------------------------------------------------------+ void Create(string szObjectName, color cor) { C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_TYPE, BORDER_FLAT); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clrNONE); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, cor); } //+------------------------------------------------------------------+ virtual void PositionAxleY(string szObjectName, int Y) { int desl = (int)(ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE) / 2); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y - desl); } //+------------------------------------------------------------------+ };
注意,我们利用继承来组装对象,如此代码量就可降至最少。 因此,我们得到的能够根据需要修改和为自身建模,故以后不需要再进行这样的调整。 这些都可以从高亮显示的代码中看到,该类只需知道 Y 轴的值即可自动将自身定位在正确的位置 — 它将检查大小并将自身定位,如此它就能定位至我们传递给它的坐标轴中间。
2.0.1.3. C_Object_TradeLine 类
C_Object_TradeLine 类负责替换先前用于指示订单价格线所处位置的水平线。 这个类非常有趣,所以请看一下它的代码:它有一个私密静态变量,如您在下面的代码所见。
#property copyright "Daniel Jose" #include "C_Object_BackGround.mqh" //+------------------------------------------------------------------+ class C_Object_TradeLine : public C_Object_BackGround { private : static string m_MemNameObj; public : //+------------------------------------------------------------------+ // ... Internal class code //+------------------------------------------------------------------+ }; //+------------------------------------------------------------------+ string C_Object_TradeLine::m_MemNameObj = NULL; //+------------------------------------------------------------------+
它以高亮显示来展示如何声明它,以及如何正确初始化它。 我们可以创建一个全局变量来替换静态变量的所作所为,但我想保持对事情的控制:这样每个对象都能拥有它所需要的一切,且信息都存储在其中。 如果我们想用另一个对象替换其它对象,我们很容易就能做到。
接下来要注意的是对象的创建代码。
void Create(string szObjectName, color cor) { C_Object_BackGround::Create(szObjectName, cor); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH)); SpotLight(szObjectName); };
为了正确地实现它,我们用到 C_Object_BackGround 类,在该类中,我们实际上创建了一个方框作为等级线的边框。 再者,这是因为如果采用另一种类型的对象,我们将不会有与现在相同的行为,并且我们需要的仅有对象是 C_object_Background 类中存在的对象。 那么,我们将修改它,从而适应我们的需求,并因此创建一条等级线。
接下来,我们将看到负责高亮显示一条等级线的代码。
void SpotLight(string szObjectName = NULL) { if (szObjectName != NULL) ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, (szObjectName != NULL ? 4 : 3)); if (m_MemNameObj != NULL) ObjectSetInteger(Terminal.Get_ID(), m_MemNameObj, OBJPROP_YSIZE, 3); m_MemNameObj = szObjectName; };
这段代码非常有趣,因为当我们高亮显示一条等级线时,我们不需要知道高亮显示的是哪一条,而对象本身会为我们做到。 当有新的一条等级线值得高亮显示时,已高亮显示的等级线将自动失去该状态,并由新等级线将取代它。 现在,如果没有一条等级线应当高亮显示,我们只需调用该函数,它将负责从任何一条等级线中去除高亮显示。
知道了这一点,上面的代码将和下面的代码一起,取代旧的选择代码。 这样,MetaTrader 5 就可让我们知道我们正在操纵哪个指标。
string GetObjectSelected(void) const { return m_MemNameObj; }
还有另一个函数值得注意。 它沿 Y 轴定位一条等级线。 我们将在下面看到。
virtual void PositionAxleY(string szObjectName, int Y) { int desly = (m_MemNameObj == szObjectName ? 2 : 1); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y - desly); };
如同在背景中显示对象的函数,该函数也会根据线是否高亮显示来调整自身的正确锚点。
我们已经彻底完成了两个对象。 但在屏幕上实际看到它们之前(如上所示),我们还需要在 C_ObjectsTrade 类中做一些事情。
2.0.2. C_ObjectsTrade 类的修改
要进行的修改乍一看并不复杂,但我们需要重复相同代码的次数有时可能会有点令人沮丧,所以我试图找到一种方法来解决这个问题。 我们要做的第一件事是创建一个事件枚举。这是利用宏替换,但如果您发现跟踪完整的宏替换代码令人困惑,那么请随意从宏替换切换到函数或过程,在极端情况下,请用适当的内部代码替换宏替换。 我更喜欢使用宏替换,因为我已经这样做了很多年了。
首先,我们创建一个事件枚举。
enum eEventType {EV_GROUND = 65, EV_LINE};
如同创建对象时,我们必须在这里添加新事件,且它们必须是一些重要的事件。 然而,每个对象只有一种类型的事件,该事件将由 MetaTrader 5 生成。 或者代码将仅确保事件会得到正确处理。
一旦完成,我们将创建变量,这些变量将提供对每个对象的访问。
C_Object_BackGround m_BackGround; C_Object_TradeLine m_TradeLine;
它们位于类的全局范围内,但它们是私密的。 我们可以在用到它们的每个函数中声明它们,但这没有多大意义,因为整个类会关注对象。
如此,我们针对来自上一篇文章中的代码进行了相应的修改。
inline string MountName(ulong ticket, eIndicatorTrade it, eEventType ev) { return StringFormat("%s%c%c%c%d%c%c", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)ev); }
高亮显示的部分在以前的版本中不存在,但现在它们将帮助 MetaTrader 5 让我们了解正在发生的情况。
此外,我们还有一个新函数。
void SetPositionMinimalAxleX(void) { m_PositionMinimalAlxeX = (int)(ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS) * 0.2); }
它沿 X 轴为对象创建起点。 每个对象都有一个特定的锚点,但在此我们提供了一个初始参考。 您可以简单地修改上面代码中的锚点来更改它的起始位置。
选择功能经历了很多变化,但稍后会有更多变化。 在此刻,它应如下。
inline void Select(const string &sparam) { ulong tick; double price; eIndicatorTrade it; eEventType ev; string sz = sparam; if (!GetInfosOrder(sparam, tick, price, it, ev)) sz = NULL; m_TradeLine.SpotLight(sz); }
另一个已修改的函数是创建指标的函数。
inline void CreateIndicatorTrade(ulong ticket, double price, eIndicatorTrade it, bool select) { if (price <= 0) RemoveIndicatorTrade(ticket, it); else { CreateIndicatorTrade(ticket, it, select); PositionAxlePrice(price, ticket, it, -1, -1, 0, false); } }
但上面的代码并不重要。 真正完成所有艰苦工作的代码,如下所示。
inline void CreateIndicatorTrade(ulong ticket, eIndicatorTrade it) { color cor1, cor2; string sz0; switch (it) { case IT_TAKE : cor1 = clrPaleGreen; cor2 = clrDarkGreen; break; case IT_STOP : cor1 = clrCoral; cor2 = clrMaroon; break; case IT_PENDING: default: cor1 = clrGold; cor2 = clrDarkGoldenrod; break; } m_TradeLine.Create(MountName(ticket, it, EV_LINE), cor2); if (ticket == def_IndicatorTicket0) m_TradeLine.SpotLight(MountName(ticket, IT_PENDING, EV_LINE)); m_BackGround.Create(sz0 = MountName(ticket, it, EV_GROUND), cor1); switch (it) { case IT_TAKE: case IT_STOP: m_BackGround.Size(sz0, 92, 22); break; case IT_PENDING: m_BackGround.Size(sz0, 110, 22); break; } }
在这个函数中,我们判定对象的颜色和创建顺序,以及它们的大小。 添加到指标中的任何对象都应经由此函数放置,如此所有对象均始终居中,并可检查。 如果您开始制作一个函数来创建指标,那么最终将得到一种难以维护的代码类型,并且可能缺少相应的验证。 您也许认为一切都很好,工作正常,您把它用于一个实盘账户上 — 只有这样,它才会被实际验证,您也许会突然意识到有些东西不能正常工作。 这里有一个建议:始终尝试在做相同工作的东西内部组装函数;即使一开始看起来毫无意义,但随着时间的推移,它会变得有意义,因为您能始终验证已变化的事物。
下面是下一个已修改的函数。
#define macroDelete(A) { \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND)); \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE)); \ } inline void RemoveIndicatorTrade(ulong ticket, eIndicatorTrade it = IT_NULL) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); if ((it != NULL) && (it != IT_PENDING) && (it != IT_RESULT)) macroDelete(it) else { macroDelete(IT_PENDING); macroDelete(IT_RESULT); macroDelete(IT_TAKE); macroDelete(IT_STOP); } ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); } #undef macroDelete
和下一个一样,它有点无聊。 该函数应该能工作,如此您就可以逐个选择每个创建的对象。 这应适用于每个指标。 想想看。 如果我们不用宏替换,令任务更容易一些,则这将会演变为一场噩梦。 编写此函数的代码非常繁琐,因为代码末尾的每个指标都有 5 个对象。 已知 OCO 订单中的每个集合将有 3 个指标,这将导致我们必须用到 15 个对象,在这种情况下,犯错误的可能性(因为它们之间的差异只是名义上的)会急剧放大。 因此,借助宏替换,代码被简化为代码中高亮显示的内容:我们只需在最后编码 5 个对象。 但这只是获得上述结果的第一阶段。
为了完成第一阶段,我们还有另一个同样乏味的特性。 如果我们不使用宏替换,我们可以使用过程来替代宏替换。 但我们已选定了这种方式。
#define macroSetAxleY(A) { \ m_BackGround.PositionAxleY(MountName(ticket, A, EV_GROUND), y); \ m_TradeLine.PositionAxleY(MountName(ticket, A, EV_LINE), y); \ } #define macroSetAxleX(A, B) { \ m_BackGround.PositionAxleX(MountName(ticket, A, EV_GROUND), B); \ m_TradeLine.PositionAxleX(MountName(ticket, A, EV_LINE), B); \ } inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy) { double ad; int x, y; ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price, x, y); macroSetAxleY(it); macroSetAxleX(it, m_PositionMinimalAlxeX); if (Leverange == 0) return; if (it == IT_PENDING) { ad = Terminal.GetAdjustToTrade() / (Leverange * Terminal.GetVolumeMinimal()); ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price + Terminal.AdjustPrice(FinanceTake * (isBuy ? ad : (-ad))), x, y); macroSetAxleY(IT_TAKE); macroSetAxleX(IT_TAKE, m_PositionMinimalAlxeX + 120); ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price + Terminal.AdjustPrice(FinanceStop * (isBuy ? (-ad) : ad)), x, y); macroSetAxleY(IT_STOP); macroSetAxleX(IT_STOP, m_PositionMinimalAlxeX + 220); } } #undef macroSetAxleX #undef macroSetAxleY
如果您认为前一个功能很无聊,请查看这个。 此处,工作量将加倍,但感谢高亮显示的代码,事情变得可以接受。
好吧,还有其它一些次要变化必将发生,但它们并不值得认真一提,所以当我们运行这段代码时,我们得到了我们所期望的,即指示出现在屏幕上。
结束语
在系统完成之前,它还需要一点,即能够直接在图表上显示完整订单。 但是现在我们必须一次性完成这一切,因为必须在代码中的其它地方进行足够重要的修改。
因此,我们将在下一篇文章中讨论这个问题,因为变化将非常深刻。 如果有些东西引发出错,则必须回退一步,然后重试,直到可以按照您想要的方式更改系统。 按照这种方式,您可以自定义系统,令您感到舒适。 在下一篇文章中,我们会拥有如下所示的系统:
看似很容易实现,不是吗? 但相信我,还有很多需要改变。 所以,与您下一篇文章相会。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10497


