平仓:全部和部分

从技术上讲,平仓可以视为与开仓相反的交易操作。例如,要退出买入,需要进行卖出操作(type 字段中的 ORDER_TYPE_SELL),要退出卖出,需要进行卖出运算(type 字段中的 ORDER_TYPE_BUY)。

MqlTradeTransaction 结构体的 action 字段中的交易操作类型保持不变:TRADE_ACTION_DEAL。

在对冲账户上,必须在 position 字段中使用订单号指定要平仓的仓位。对于净额结算账户,只能在 symbol 字段中指定交易品种名称,因为它们只能有一个交易品种仓位。不过,也可以在此处用订单号平仓。

为了统一代码,无论账户类型如何,都应填写 positionsymbol 字段。

此外,请确保在 volume 字段中设置交易量。如果等于持仓量,则完全平仓。但是,通过指定一个较低的值,可以部分平仓。

在下表中,所有必需的结构体字段都标有星号,可选字段标有加号。

字段

净额结算

对冲

action

*

*

symbol

*

+

position

+

*

type

*

*

type_filling

*

*

volume

*

*

price

*'

*'

deviation

±

±

magic

+

+

comment

+

+

标记的 price 字段带有一个带勾号的星号,因为它仅对于具有 RequestInstant 执行模式的交易品种是必需的,而对于 ExchangeMarket 执行,不会考虑结构体中的价格。

出于类似的原因,deviation 字段标有 '±'。该字段仅对 InstantRequest 模式有效。

为了简化平仓的编程实现,我们回到 MqlTradeSync.mqh 文件中的扩展结构体 MqlTradeRequestSync。按订单号的平仓方法有以下代码。

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   double partial// volume after partial closing
   ...
   bool close(const ulong ticketconst double lot = 0)
   {
      if(!PositionSelectByTicket(ticket)) return false;
      
      position = ticket;
      symbol = PositionGetString(POSITION_SYMBOL);
      type = (ENUM_ORDER_TYPE)(PositionGetInteger(POSITION_TYPE) ^ 1);
      price = 0
      ...

此处,我们首先检查仓位是否存在(通过调用 PositionSelectByTicket 函数)。此外,该调用可在终端的交易环境中选择仓位,这允许你使用 后续函数读取其特性。具体来说,我们可从 POSITION_SYMBOL 特性中找出仓位的交易品种,并将其类型从 POSITION_TYPE“反转”到相反的类型,以获得所需的订单类型。

ENUM_POSITION_TYPE 枚举中的仓位类型为 POSITION_TYPE_BUY(值 0)和 POSITION_TYPE_SELL(值 1)。在订单类型 ENUM_ORDER_TYPE 的枚举中,市场操作占据完全相同的值:ORDER_TYPE_BUY 和 ORDER_TYPE_SELL。这就是为什么我们可以将第一个枚举带到第二个枚举的原因,为了获得相反的交易方向,只需使用异或运算 ('^') 来切换 0 位:我们可从 0 得到 1,从 1 得到 0。

price 字段置零意味着在发送请求之前会自动选择正确的当前价格(AskBid):这是稍后在辅助方法 setVolumePrices 中完成的,该方法与来自 market 方法的算法一起调用。

_market 方法调用发生在下面几行。_market 方法可为整个或部分交易量生成市场订单,并考虑结构体中所有已完成的字段。

      const double total = lot == 0 ? PositionGetDouble(POSITION_VOLUME) : lot;
      partial = PositionGetDouble(POSITION_VOLUME) - total;
      return _market(symboltotal);
   }

与当前源代码相比,这个片段稍微简化了一些。完整的代码包含处理一种罕见但可能发生的情况,即仓位交易量超过每个交易品种一个订单中允许的最大交易量(SYMBOL_VOLUME_MAX 特性)。在这种情况下,必须通过几个订单分批平仓。

还要注意的是,由于可以部分平仓,我们必须向 partial 结构体添加一个字段,在该字段中放置运算后的计划量余额。当然,对于完全平仓则为 0。必须用此信息来进一步验证操作的完成。

对于净额结算账户,有一个版本的 close 方法,通过交易品种名称识别仓位。该方法通过交易品种选择一个仓位,得到其订单号,然后引用以前版本的 close

   bool close(const string nameconst double lot = 0)
   {
      if(!PositionSelect(name)) return false;
      return close(PositionGetInteger(POSITION_TICKET), lot);
   }

MqlTradeRequestSync 结构体中,我们有 completed 方法,该方法在必要时为运算的完成提供同步等待。现在我们需要在 action 等于 TRADE_ACTION_DEAL 的分支中将其补充到平仓。我们将通过 position 字段的零值来区分开仓和平仓:开仓时没有订单号,平仓时有订单号。

   bool completed()
   {
      if(action == TRADE_ACTION_DEAL)
      {
         if(position == 0)
         {
            const bool success = result.opened(timeout);
            if(successposition = result.position;
            return success;
         }
         else
         {
            result.position = position;
            result.partial = partial;
            return result.closed(timeout);
         }
      }

为了检查实际平仓情况,我们可将 closed 方法添加到 MqlTradeResultSync 结构体中。在调用该方法之前,我们可在 result.position 字段中写入仓位订单号,以便产生的结构体可以跟踪相应订单号从终端的交易环境中消失的时刻,或者在部分平仓的情况下交易量等于 result.partial 的时刻。

此处是 closed 方法。该方法基于一个众所周知的原则:首先检查服务器返回代码是否成功,然后使用 wait 方法等待某个条件满足。

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   bool closed(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE)
      {
         return false;
      }
      if(!wait(positionRemovedmsc))
      {
         Print("Position removal timeout: P=" + (string)position);
      }
      
      return true;
   }

在这种情况下,为了检查仓位消失的条件,我们必须实现一个新的函数 positionRemoved

   static bool positionRemoved(MqlTradeResultSync &ref)
   {
      if(ref.partial)
      {
         return PositionSelectByTicket(ref.position)
            && TU::Equal(PositionGetDouble(POSITION_VOLUME), ref.partial);
      }
      return !PositionSelectByTicket(ref.position);
   }

我们可使用 EA 交易 TradeClose.mq5 来测试平仓操作,其实现了一个简单的交易策略:如果有两个连续的柱线同向,则进入市场,一旦下一个柱线以与前一个趋势相反的方向收盘,我们就退出市场。连续趋势期间的重复信号将被忽略,即市场中最多有一个仓位(最小手数)或没有。

EA 交易没有任何可调参数:只有 (Deviation) 和一个唯一的数字 (Magic)。隐含的参数是图表的时间范围和工作交易品种。

为了跟踪已经开仓的仓位是否存在,我们使用前面示例 TradeTrailing.mq5 中的GetMyPosition 函数:该函数通过交易品种和 EA 交易号在仓位中搜索,如果找到合适的仓位,则返回逻辑 true

我们还采用几乎没有变化的函数 OpenPosition:该函数可根据单个参数中传递的市场订单类型开仓。此处,该参数将来自趋势检测算法,在更早的时候(在 TrailingStop.mq5 中),订单类型是由用户通过输入变量设置的。

实现平仓的一个新函数是 ClosePosition。因为头文件 MqlTradeSync.mqh 接管了整个例程,所以我们只需要为提交的仓位订单号调用 request.close(ticket) 方法,然后等待 request.completed() 完成删除。

从理论上讲,如果 EA 交易在每一个分时报价处分析情况,后者是可以避免的。在这种情况下,删除仓位的潜在问题将在下一个分时报价处迅速暴露出来,并且 EA 交易可以再次尝试删除仓位。但是,该 EA 交易采用基于柱线的交易逻辑,因此分析每一个分时报价是没有意义的。接下来,我们实现了一个特殊的机制来完成逐柱线工作,在这方面,我们可同步地控制移除,否则,仓位将在整个柱线上保持“悬置”。

ulong LastErrorCode = 0;
   
ulong ClosePosition(const ulong ticket)
{
   MqlTradeRequestSync request// empty structure
   
   // optional fields are filled directly in the structure
   request.magic = Magic;
   request.deviation = Deviation;
   
   ResetLastError();
   // perform close and wait for confirmation
   if(request.close(ticket) && request.completed())
   {
      Print("OK Close Order/Deal/Position");
   }
   else // print diagnostics in case of problems
   {
      Print(TU::StringOf(request));
      Print(TU::StringOf(request.result));
      LastErrorCode = request.result.retcode;
      return 0// error, code to parse in LastErrorCode
   }
   
   return request.position// non-zero value - success
}

我们可以强制 ClosePosition 函数在成功删除仓位的情况下返回 0,否则返回一个错误代码。这种看似有效的方法会使 OpenPositionClosePosition 两个函数的行为有所不同:在调用代码中,需要将这些函数的调用嵌套在意义相反的逻辑表达式中,而这会引入混乱。此外,我们在任何情况下都需要全局变量 LastErrorCode,以便在 OpenPosition 函数中添加有关错误的信息。同样,此外,if(condition) 检查比 if(!condition) 更容易被解释为成功。

根据上述策略生成交易信号的函数称为 GetTradeDirection

ENUM_ORDER_TYPE GetTradeDirection()
{
   if(iClose(_Symbol_Period1) > iClose(_Symbol_Period2)
      && iClose(_Symbol_Period2) > iClose(_Symbol_Period3))
   {
      return ORDER_TYPE_BUY// open a long position
   }
   
   if(iClose(_Symbol_Period1) < iClose(_Symbol_Period2)
      && iClose(_Symbol_Period2) < iClose(_Symbol_Period3))
   {
      return ORDER_TYPE_SELL// open a short position
   }
   
   return (ENUM_ORDER_TYPE)-1// close
}

该函数可返回一个 ENUM_ORDER_TYPE 类型的值,该值具有两个分别触发买入和卖出的标准元素(ORDER_TYPE_BUY 和 ORDER_TYPE_SELL)。特殊值 - 1(不在枚举中)可用作平仓信号。

为了激活基于交易算法的 EA 交易,我们使用了 OnTick 处理程序。我们都还记得,其他选项适用于其他策略,例如,计时器可用于新闻交易,市场深度事件可用于交易量。

首先,我们以简化的形式分析该函数,不处理潜在的错误。在最开始,有一个程序块可确保仅当一个新的柱线平仓时,才会触发进一步的算法。

void OnTick()
{
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBarreturn;
   lastBar = iTime(_Symbol_Period0);
   ...

接下来,我们从 GetTradeDirection 函数中获取当前信号。

   const ENUM_ORDER_TYPE type = GetTradeDirection();

如果有一个仓位,我们可检查是否已经收到其平仓信号,并根据需要调用 ClosePosition。如果还没有仓位,但有进场信号,我们称之为 OpenPosition

   if(GetMyPosition(_SymbolMagic))
   {
      if(type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL)
      {
         ClosePosition(PositionGetInteger(POSITION_TICKET));
      }
   }
   else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
   {
      OpenPosition(type);
   }
}

要分析错误,需要将 OpenPositionClosePosition 调用包含在条件语句中,并采取一些措施来恢复程序的工作状态。在最简单的情况下,只需要在下一个分时报价处重复该请求,但是最好只重复有限的次数。因此,我们将创建带有计数器和错误限制的静态变量。

void OnTick()
{
   static int errors = 0;
   static const int maxtrials = 10// no more than 10 attempts per bar
   
   // expect a new bar to appear if there were no errors
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBar && errors == 0return;
   lastBar = iTime(_Symbol_Period0);
   ...

如果出现错误,则逐柱线机制会被暂时禁用,因为需要尽快处理这些错误。

ClosePositionOpenPosition 的条件语句中,统计错误的数量。

   const ENUM_ORDER_TYPE type = GetTradeDirection();
   
   if(GetMyPosition(_SymbolMagic))
   {
      if(type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL)
      {
         if(!ClosePosition(PositionGetInteger(POSITION_TICKET)))
         {
            ++errors;
         }
         else
         {
            errors = 0;
         }
      }
   }
   else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
   {
      if(!OpenPosition(type))
      {
         ++errors;
      }
      else
      {
         errors = 0;
      }
   }
 // too many errors per bar
   if(errors >= maxtrialserrors = 0;
 // error serious enough to pause
   if(IS_TANGIBLE(LastErrorCode)) errors = 0;
}

errors 变量设置为 0 将再次打开逐柱线机制,并停止重复请求的尝试,直到下一个柱线。

宏 IS_TANGIBLE 在 TradeRetcode.mqh 中定义为:

#define IS_TANGIBLE(T) ((T) >= TRADE_RETCODE_ERROR)

值较小的错误代码是可运行的,即在某种意义上是正常的。值较大的错误代码需要分析和不同的操作,具体取决于问题的原因:不正确的请求参数、交易环境中的永久或临时禁令、缺乏资金等等。我们将在 挂单修改章节介绍了改进错误分类器。

我们从 2022 年初开始,在 XAUUSD,H1 的测试程序中运行 EA 交易,模拟真实的分时报价。下一幅图展示了一个包含交易的图表片段以及余额曲线。

对 XAUUSD,H1 运行 TradeClose 的测试结果

对 XAUUSD,H1 运行 TradeClose 的测试结果

基于报告和日志,我们可以看到,我们简单的交易逻辑和开仓与平仓两个运算的结合在正常工作。

除了简单的平仓,该平台还支持对冲账户上 两个相反仓位的相互平仓