English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
从头开始开发一款智能交易系统

从头开始开发一款智能交易系统

MetaTrader 5交易 | 22 三月 2022, 09:49
5 620 0
Daniel Jose
Daniel Jose

概述

金融市场的新用户数量不断增加。 或许他们当中的许多人甚至不知道订单系统是如何工作的。 然而,也有一些用户真的想知道发生了什么。 他们试图了解这一切是如何运作的,从而能够掌控局势。

当然,MetaTrader 5 提供了对交易持仓的高度控制。 然而,对于经验不足的用户来说,仅用手动功能下订单可能会非常困难和危险。 甚至,如果有人想交易期货合约,在几乎没有时间下单的情况下,这样交易很可能会演变为一场噩梦;因为您必须及时正确地填写所有字段,但这显然需要时间,而若填写出错,您可能会错失良机,甚至赔钱。

现在,如果我们使用智能交易系统(EA)能否让事情变得更容易呢? 在这种情况下,您可以指定一些细节,例如杠杆率,或者您所能承受的损失,以及您想赚多少(以货币计算,而非含义模糊的“点数”或“点值”)。 然后在图表上用鼠标指定入场的价位,并标记是买入还是卖出...


计划

创造事物最困难的部分是弄清楚事物应该如何运作。 这个思路应该表述得非常清楚,如此我们就能按需创建最低代码,因为若是创建的代码越复杂,出现运行时错误的可能性就越大。 考虑到这一点,我尝试让代码变得非常简单,但依旧最大可能地利用 MetaTrader 5 提供的功能。 该平台非常可靠,它在不断进行测试,故此错误不会出现在平台一端。

代码将采用 OOP(面向对象编程)。 这种方法能够隔离代码,并促进其维护和未来的开发,预防我们想要添加新功能,并进行改进。

尽管本文讨论的 EA 是出于在 B3(巴西交易所)上进行交易而设计的,特别是为期货(迷你指数和迷你美元)交易而设计的,但只需略微修改即可扩展到所有市场。 为了另事情变得更简单,且不必列举或检查交易资产,我们将使用以下枚举:

enum eTypeSymbolFast {WIN, WDO, OTHER};


如果您想交易其它资产,需用到某些特殊功能,请将其添加到枚举之中。 这也需要在代码中做一些微小的修改,但用枚举会更容易一些,因为它还降低了出错的可能性。 代码中一个有趣的部分是 AdjustPrice 函数:

   double AdjustPrice(const double arg)
     {
      double v0, v1;
      if(m_Infos.TypeSymbol == OTHER)
         return arg;
      v0 = (m_Infos.TypeSymbol == WDO ? round(arg * 10.0) : round(arg));
      v1 = fmod(round(v0), 5.0);
      v0 -= ((v1 != 0) || (v1 != 5) ? v1 : 0);
      return (m_Infos.TypeSymbol == WDO ? v0 / 10.0 : v0);
     };

此函数将调整价格中用到的数值,从而在图表准确定位价格线。 为什么我们不能简单地在图表上放一条线呢? 这是因为一些资产在价格之间存在一定的阶梯。 对于 WDO (迷你美元) 这个阶梯是 0.5 个点。 对于 WIN (迷你指数) 个阶梯是 5 个点,而对于股票,它是 0.01 个点。 换言之,不同资产的点数值不同。 它会把价格调整为正确的即时报价数值,从而该数值能在订单中正确使用,否则填写有错的订单会被服务器拒绝。

若无此函数,可能很难知道订单中所采用的数值是否正确。 故而,服务器就会通知订单填写错误,并阻止其执行。 现在,我们继续讨论智能交易系统的核心函数:CreateOrderPendent。 函数如下:

   ulong CreateOrderPendent(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true)
     {
      double last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST);
      ZeroMemory(TradeRequest);
      ZeroMemory(TradeResult);
      TradeRequest.action        = TRADE_ACTION_PENDING;
      TradeRequest.symbol        = m_szSymbol;
      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, m_Infos.nDigits);
      TradeRequest.sl            = NormalizeDouble(Stop, m_Infos.nDigits);
      TradeRequest.tp            = NormalizeDouble(Take, m_Infos.nDigits);
      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(!OrderSend(TradeRequest, TradeResult))
        {
         MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA");
         return 0;
        };
      return TradeResult.order;
     };

该函数非常简单,就是为了安全而设计的。 我们将在这里创建一个 OCO(一笔取消其它)订单,该订单将被发送到交易服务器。 请注意,我们使用的是 LIMIT(限价)STOP(破位) 订单。 这是因为这类订单更简单,即使在价格突然波动的情况下也能保证执行。

所采用用的订单类型取决于交易工具的执行价格和当前价格,以及您入场操作是买入还是卖出。 这是通过以下方式实现的:

TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));

通过在以下代码行中指定交易工具,也可以创建 CROSS(交叉)订单:

TradeRequest.symbol = m_szSymbol;

但在这样做时,您还需要添加一些代码,以便通过交叉订单系统处理持仓或挂单,因为您会有一个“错误”的图表。 我们来看一个示例。 您可以在完整指数图表(IND)上交易迷你指数(WIN),但若您在 IND 图表上使用 MetaTrader 5 时,它不会显示持仓或 WIN 挂单。 因此,有必要添加代码,从而令订单可见。 这可以通过读取持仓数值,并在图表上用线条示意来实现。 这在交易和跟踪品种交易历史时非常有用。 例如,当您使用 CROSS(交叉)订单时,您可以依据 WIN$ 图表(迷你指数历史图表)交易 WIN(迷你指数)。

接下来,请注意以下代码行:

      TradeRequest.price         = NormalizeDouble(Price, m_Infos.nDigits);
      TradeRequest.sl            = NormalizeDouble(Stop, m_Infos.nDigits);
      TradeRequest.tp            = NormalizeDouble(Take, m_Infos.nDigits);

这三行将创建OCO订单止损水平和持仓未平仓价格。 如果您交易的是短线订单(可能只持续几秒钟),不使用 OCO 订单是不可取的,因为波动会令价格在点位间跳转时,没有明确的方向。 当您采用 OCO 时,交易服务器自身会关注我们的仓位。 OCO 订单如下所示。

在编辑窗口中,相同的订单如下所示:

一旦填完所有必填字段后,服务器将接管订单。 一旦达到最大盈利最大亏损,系统将平仓。 但若您没有指定最大盈利或最大亏损,订单可能会一直保持,直到另一个事件发生。 如果订单类型设置为日内交易,系统将在交易日结束时关闭。 否则,该笔持仓将继续持有,直到您手动平仓,或者直到没有更多资金来保有持仓。

一些智能交易系统使用订单来平仓:一旦开仓,就会发送一笔逆反的订单,在指定的点位平仓,且交易量相同。 但在某些情况下,这可能不起作用,因为如果资产在交易期间出于某种原因进入拍卖,则挂单可能会被取消,并应予以替换。 这将另 EA 操作复杂化,因为您需要加入检查哪些订单处于有效状态,哪些订单处于无效状态;如果出现任何错误,若无任何标准则 EA 将会一笔接一笔地发送订单。

   void Initilize(int nContracts, int FinanceTake, int FinanceStop, color cp, color ct, color cs, bool b1)
     {
      string sz0 = StringSubstr(m_szSymbol = _Symbol, 0, 3);
      double v1 = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
      m_Infos.Id = ChartID();
      m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));
      m_Infos.nDigits = (int) SymbolInfoInteger(m_szSymbol, SYMBOL_DIGITS);
      m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN));
      m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume);
      m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);
      m_Infos.IsDayTrade = b1;
      CreateHLine(m_Infos.szHLinePrice, m_Infos.cPrice = cp);
      CreateHLine(m_Infos.szHLineTake, m_Infos.cTake = ct);
      CreateHLine(m_Infos.szHLineStop, m_Infos.cStop = cs);
      ChartSetInteger(m_Infos.Id, CHART_COLOR_VOLUME, m_Infos.cPrice);
      ChartSetInteger(m_Infos.Id, CHART_COLOR_STOP_LEVEL, m_Infos.cStop);
     };

上面的例程负责初始化用户指示的 EA 数据 — 它创建一笔 OCO 订单。 我们只需要在这个程序中做以下修改。

m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));

在此,如果您需要一些特定的信息,我们将在当前品种的基础上添加交易品种类型。

      m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN));
      m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume);
      m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);

以上三行是为了正确创建订单而进行的必要调整。 nContracts 是一个杠杆系数,选取 1、2、3 等值。 换句话说,您不需要知道要交易品种的最小交易量。 您真正需要的就是指出这个最小交易量的杠杆系数。 例如,如果所需的最小交易量为 5 份合同,并且您指定的杠杆系数为 3,则系统将开立 15 份合约的订单。 基于用户指定的参数,另外两行相应地设置了止盈止损。 级别随订单交易量调整:如果订单增加,级别降低,反之亦然。 有了这段代码,您在开仓时就不必进行计算 — EA 会自行计算所有东西:您指示 EA 交易的金融工具,杠杆系数,您想赚多少钱,准备亏损多少钱,而 EA 将为您创建一笔相应的订单。

   inline void MoveTo(int X, int Y, uint Key)
     {
      int w = 0;
      datetime dt;
      bool bEClick, bKeyBuy, bKeySell;
      double take = 0, stop = 0, price;
      bEClick  = (Key & 0x01) == 0x01;    //Left mouse button click
      bKeyBuy  = (Key & 0x04) == 0x04;    //Pressed SHIFT
      bKeySell = (Key & 0x08) == 0x08;    //Pressed CTRL
      ChartXYToTimePrice(m_Infos.Id, X, Y, w, dt, price);
      ObjectMove(m_Infos.Id, m_Infos.szHLinePrice, 0, 0, price = (bKeyBuy != bKeySell ? AdjustPrice(price) : 0));
      ObjectMove(m_Infos.Id, m_Infos.szHLineTake, 0, 0, take = price + (m_Infos.TakeProfit * (bKeyBuy ? 1 : -1)));
      ObjectMove(m_Infos.Id, m_Infos.szHLineStop, 0, 0, stop = price + (m_Infos.StopLoss * (bKeyBuy ? -1 : 1)));
      if((bEClick) && (bKeyBuy != bKeySell))
         CreateOrderPendent(bKeyBuy, m_Infos.Volume, price, take, stop, m_Infos.IsDayTrade);
      ObjectSetInteger(m_Infos.Id, m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE));
      ObjectSetInteger(m_Infos.Id, m_Infos.szHLineTake, OBJPROP_COLOR, (take > 0 ? m_Infos.cTake : clrNONE));
      ObjectSetInteger(m_Infos.Id, m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE));
     };

上述代码将显示要创建的订单。 它使用鼠标来显示订单将要放置的价位。 您还要通知 EA 是想买入(按住 SHIFT 键),还是想卖出(按住 CTRL 键)。 一旦单击鼠标左键后,此时将创建一笔挂单。

如果您需要显示更多数据,例如盈亏平衡点,请将相关对象添加到代码之中。

现在我们拥有了一个完整的 EA,它可以工作,并创建 OCO 订单。 但这里的一切并非都是完美的...


问题出在 OCO 订单

OCO 订单存在一个问题,这并非 MetaTrader 5 系统或交易服务器的故障。 它与市场中不断出现的波动性本身有关。 从理论上讲,价格应该是线性波动的,没有回滚;但有时我们会遇到高波动性,这会在烛条内部造成跳空缺口。 当这些跳空缺口出现在止损或止盈订单的价位时,这些点位将不会被触发,因此,将不会平仓。 当用户移动这些点位时,价格也可能超出止损和止盈形成的走廊。 在这种情况下,订单也不会平仓。 这是一种非常危险的状况,无法预测。 作为一名程序员,您必须提供一个相应的机制,以尽量减少可能的危害。

为了刷新价格,并试图将其维持在走廊内,我们将使用两个子例程。 第一个如下:

   void UpdatePosition(void)
     {
      for(int i0 = PositionsTotal() - 1; i0 >= 0; i0--)
         if(PositionGetSymbol(i0) == m_szSymbol)
           {
            m_Take      = PositionGetDouble(POSITION_TP);
            m_Stop      = PositionGetDouble(POSITION_SL);
            m_IsBuy     = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
            m_Volume    = PositionGetDouble(POSITION_VOLUME);
            m_Ticket    = PositionGetInteger(POSITION_TICKET);
           }
     };

它将在 OnTrade 中被调用,即 MetaTrader 5 在每次持仓变化时调用的函数。 下一个要用到的子例程则由 OnTick 调用。 它检查并确保价格在走廊范围内,或在 OCO 订单的范围内。 其如下所示:

   inline bool CheckPosition(const double price = 0, const int factor = 0)
     {
      double last;
      if(m_Ticket == 0)
         return false;
      last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST);
      if(m_IsBuy)
        {
         if((last > m_Take) || (last < m_Stop))
            return ClosePosition();
         if((price > 0) && (price >= last))
            return ClosePosition(factor);
        }
      else
        {
         if((last < m_Take) || (last > m_Stop))
            return ClosePosition();
         if((price > 0) && (price <= last))
            return ClosePosition(factor);
        }
      return false;
     };

这个代码片段非常关键,因为它将在每次即时报价变化时执行,因此它必须尽可能简单,以便尽可能高效地执行计算和测试。 请注意,虽然我们将价格维持在走廊内,但我们也会检查一些有趣的东西;如果需要,可以删除这些东西。 我将在下一章节中解释这个附加测试。 在这个子程序中,我们有以下函数调用:

   bool ClosePosition(const int arg = 0)
     {
      double v1 = arg * m_VolMinimal;
      if(!PositionSelectByTicket(m_Ticket))
         return false;
      ZeroMemory(TradeRequest);
      ZeroMemory(TradeResult);
      TradeRequest.action     = TRADE_ACTION_DEAL;
      TradeRequest.type       = (m_IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY);
      TradeRequest.price      = SymbolInfoDouble(m_szSymbol, (m_IsBuy ? SYMBOL_BID : SYMBOL_ASK));
      TradeRequest.position   = m_Ticket;
      TradeRequest.symbol     = m_szSymbol;
      TradeRequest.volume     = ((v1 == 0) || (v1 > m_Volume) ? m_Volume : v1);
      TradeRequest.deviation  = 1000;
      if(!OrderSend(TradeRequest, TradeResult))
        {
         MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA");
         return false;
        }
      else
         m_Ticket = 0;
      return true;
     };

该函数将按指定交易量平仓,并起到保护作用。 然而,请不要忘记,您必须连接服务器,因为该函数在 MetaTrader 5 客户端中运行 — 如果与服务器的连接失败,该函数将完全无效。

查看最后两段代码,从中可以发现我们能够在某个点位按照给定的交易量了结。 如此这般操作,我们即可部分平仓,亦可降低爆仓。 我们来看看如何使用这个函数。


处理部分订单


部分订单是许多交易者喜欢并利用的东西。 智能交易系统允许部分平仓操作,但我不会展示如何实现这样的代码,因为部分订单应该是另一个问题的主题。 然而,如果您打算实现部分平仓的操作,只需调用 CheckPosition 例程,指定执行订单的价格和交易量,而 EA 则完成其余的操作。

我要说的是,部分订单是一种特殊情况,因为它们是非常独立的,很难创建一个通用的解决方案来满足所有人。 在此使用动态数组并不合适,因为您可能会摇摆不定 — 它在日间交易时,您不能关闭 EA。 如果出于任何原因需要关闭 EA,则数组解决方案将无法工作。 您需要用到一些存储介质,其中的数据格式将取决于您将如何处理这些数据。

无论如何,您都应该尽可能避免利用开仓订单来部分关闭,因为如此做会导致头痛的风险巨大。 我来解释一下:假设您用 3 倍杠杆开立多头仓位,且您打算以 1 倍杠杆持仓的同时,赚取 2 倍盈利。 这可以通过以 2 倍杠杆做空来实现。 然而,如果您的 EA 发送了一笔市价卖单,那么可能会发生这种情况,在卖单实际执行之前,波动会导致价格上涨,并触发您的止盈。 在这种情况下,EA 将在不利方向开立一笔新的空头持仓。 或者,您可以发送一笔 Sell LimitSell Stop,以以 2 倍杠杆降低持仓。 这似乎是一个适当的解决方案。 但是,如果在价格触及部分平仓点位之前发出了另一笔订单,您可能遭遇一个非常不爽的惊喜:持仓将被停止,且稍晚一点,订单再次开立持仓,并会增加损失。 如果波动性变得很强,情况将与我们前面提到的一样。

因此,在我看来,作为一名程序员,部分订单的最佳选择是模仿发送市价订单。 但您务必非常小心,不要超过持仓的交易量。 在该 EA 中,我就是这样做的。 如果您愿意,可以实现其它得部分平仓方法。


结束语

为交易创建一款智能交易系统并不像一些人想象的那么简单;与编程时经常遇到的其它一些问题相比,这或许非常简单,然而,构建足够稳定和可靠的系统来从事冒险是一项艰巨的任务。 在本文中,我提出了一些建议,可以让那些开始使用 MetaTrader 5,但不具备编写 EA 所需知识之人的生活更轻松。 这是一个良好的开端,因为该 EA 不开单,只是以更可靠的方式帮助下订单。 一旦放下订单,EA 不会进行任何操作,接下来 MetaTrader 5 接手操作,除了上面提到的代码片段。

本文中介绍的智能交易系统可以遵照各种方式进行改进,以便处理参数集合,但这将需要更多代码,使其更独立于MetaTrader 5。

该 EA 的巨大成功在于它利用 MetaTrader 5 本身来执行代码中没有的动作,因此它非常稳定可靠。





本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10085

附加的文件 |
EA_Nano_rvl_1.1.mq5 (23.44 KB)
优化结果的可视化评估 优化结果的可视化评估
在本文中,我们将研究如何建立所有优化通测的图形,以及选择最优结果的自定义准则。 我们还将看到如何利用网站上发表的文章和论坛评论,在几乎不了解 MQL5 的情况下创建所需的解决方案。
DoEasy 函数库中的图形(第九十一部分):标准图形对象事件。 对象名称更改历史记录 DoEasy 函数库中的图形(第九十一部分):标准图形对象事件。 对象名称更改历史记录
在本文中,我将改进基本功能,从而能够基于函数库程序来控制图形对象事件。 我一开始将以“对象名称”属性为例,实现存储图形对象更改历史的功能。
DoEasy 函数库中的图形(第九十二部分):标准图形对象记忆类。 对象属性变更历史记录 DoEasy 函数库中的图形(第九十二部分):标准图形对象记忆类。 对象属性变更历史记录
在本文中,我将创建标准图形对象记忆类,能够在对象修改其属性时保存其过往状态。 反之,这样就能够溯源以前的图形对象状态。
MQL5 中的矩阵和向量 MQL5 中的矩阵和向量
运用特殊的数据类型“矩阵”和“向量”,可以创建非常贴合数学符号本意的代码。 运用这些方法,您可以避免创建嵌套循环,或在计算中分心记忆正确的数组索引。 因此,矩阵和向量方法的运用能为开发复杂程序提高可靠性和速度。