交易机器人的虚假触发保护

Aleksandr Masterskikh | 12 九月, 2016

概论

本文讨论了多种方式来增加交易机器人的操作稳定性, 譬如消减可能的重复触发 (抖动): 既可以分别使用入场和离场算法,也可将它们连结。


问题的本质

若是当前蜡烛条幅度过高, 并且在交易机器人里没有提供预防抖动措施, 则虚假触发问题在暴跌暴涨行情中尤为突出。它会导致在当前蜡烛条上连续重复开单、平仓。

依据行情的特殊算法以及交易机器人的开发者所设置的参数, 其结算后果是变化的。在所有情况下, 交易者的点差开销会随着抖动期间触发数量而成比例增加。

在本文中, 我不会涉及金融工具 (技术和基本面特征) 分析的话题, 这能够影响智能交易程序操作的稳定性, 并有助于避免散射 (这是一个单独的话题 — 我是脉冲均衡理论及其应用系统的作者)。在此, 我们重点关注那些软件手段, 而非直接依赖金融市场分析的方法。

所以, 让我们来着手解决问题。作为一个示例, 我将使用来自 МetaТrader 4 客户端标准集里提供的 "MACD 样本" EA。

如图例所示 EURUSD 价格在当年的十月份第二天飙升 (М15 时间帧, "MACD 样本" EA 省缺设置), 它可直观解释散射问题:

屏幕截图清楚显示在单根蜡烛条里有 8 个连串的触发 (买进入场)。它们之中只有 1 个是正确的 (按照正常的行情逻辑条件), 其余 7 个是散射。

在这种特殊情况下虚假触发背后的原因是:

我们已经同意, 过滤行情波动的事项不是考虑的目地 (因为每位交易者有自己的入场和离场算法), 所以为了解决问题, 我们考虑以下更普遍的因素:


入场算法里的解决方案

最简单同时也是最可靠地固定入场点的方法是通过时间因素, 其原因有以下几点:

这种方式, 主要因素是入场算法的触发时刻, 更加具体的是, 开仓所需的订单触发时刻 (OrderSend), 因为这两个时刻也许不相符, 如果在算法里有一些特别的开单延迟。

因此, 我们要记住开仓的时刻 (当前时间)。但如何在入场算法里使用这个参数, 以便在指定的蜡烛条上禁止随后的重复入场?我们无法预先知道这一时刻 (其绝对值), 所以我们不能在入场算法里预先输入它。算法应考虑 (包括) 一些通常的条件来解决在蜡烛条上的首次入场, 且无需计算触发即可禁止在蜡烛条上的后续入场 (我们之前拒绝的带计数器的选项)。

此解决方案是相当简单的。首先, 我将会编写一些带注释的代码, 然后将会澄清更多细节。这是一段辅助代码 (以黄色加亮), 需要放置于交易 EA 的算法里 (参看 MACD_Sample_plus1.mq4):

//+------------------------------------------------------------------+
//|                                                  MACD Sample.mq4 |
//|                            版权所有 2005-2014, MetaQuotes 软件公司|
//|                                              https://www.mql4.com |
//+------------------------------------------------------------------+
#property copyright   "2005-2014, MetaQuotes 软件公司"
#property link        "https://www.mql4.com"

input double TakeProfit    =50;
input double Lots          =0.1;
input double TrailingStop  =30;
input double MACDOpenLevel =3;
input double MACDCloseLevel=2;
input int    MATrendPeriod =26;
//--- 输入新变量 (此时间帧内一根柱线的秒数, 对于 М15 等于 60 с х 15 = 900 с)
datetime Time_open=900;
//--- 输入新变量 (柱线开盘时间, 首次入场)
datetime Time_bar = 0;

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   double MacdCurrent,MacdPrevious;
   double SignalCurrent,SignalPrevious;
   double MaCurrent,MaPrevious;
   int    cnt,ticket,total;
//---
// 初始数据检查
// 它对于确保程序能在正常图表上工作十分重要
// 而且用户在设置外部变量时不可出错 
// (Lots, StopLoss, TakeProfit, 
// TrailingStop), 在我们的例子中, 我们检查止盈
// 在图表上是否小于 100 根柱线
//---
   if(Bars<100)
     {
      Print("柱线数小于 100");
      return;
     }
   if(TakeProfit<10)
     {
      Print("TakeProfit 小于 10");
      return;
     }
//--- 为了简化代码, 并加速存取, 数据被放到内部变量
   MacdCurrent=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_MAIN,0);
   MacdPrevious=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_MAIN,1);
   SignalCurrent=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_SIGNAL,0);
   SignalPrevious=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_SIGNAL,1);
   MaCurrent=iMA(NULL,0,MATrendPeriod,0,MODE_EMA,PRICE_CLOSE,0);
   MaPrevious=iMA(NULL,0,MATrendPeriod,0,MODE_EMA,PRICE_CLOSE,1);

   total=OrdersTotal();
   if(total<1)
     {
      //--- 未识别出已开订单
      if(AccountFreeMargin()<(1000*Lots))
        {
         Print("我们没有资金。Free Margin = ",AccountFreeMargin());
         return;
        }
      //--- 检查多头仓位 (买入) 的可行性
      
      //--- 输入新字符串 (若新柱线开盘, 删除重复入场禁止标志)
      if( (TimeCurrent() - Time_bar) > 900 ) Time_open = 900; 
      
      if(MacdCurrent<0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious && 
         MathAbs(MacdCurrent)>(MACDOpenLevel*Point) && MaCurrent>MaPrevious && 
         (TimeCurrent()-Time[0])<Time_open) //输入新字符串至入场算法 (仅执行一次, 此蜡烛条上的条件以后不能完成)
        {
         ticket=OrderSend(Symbol(),OP_BUY,Lots,Ask,3,0,Ask+TakeProfit*Point,"macd sample",16384,0,Green);
         if(ticket>0)
           {
            if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES))
             {
              Print("买入订单已开 : ",OrderOpenPrice());
              Time_open = TimeCurrent()-Time[0]; //输入新字符串 (保存入场时的柱线开盘时间到离场时刻的间隔)
              Time_bar = Time[0]; //输入新字符串 (记住柱线开盘时间已有首次入场)
             }
           }
         else
            Print("买入开单出错 : ",GetLastError());
         return;
        }


阅读更多:

替代绝对时间 (入场时可), 我们使用相对时间 —自当前蜡烛条开盘时刻至入场时刻的时间缺口。此数值与预先设定的进行比较, 较大时间值 (整根蜡烛条的长度), 允许触发首次入场。在开仓时刻, 我们修改 (降低) Time_open 变量的数值, 写入自蜡烛条开盘到实际收盘时刻之间的缺口值。并且由于在随后的任意时刻, 数值 (TimeCurrent() - Time[0]) 将会超出我们写入的入场点数值, 则 (TimeCurrent() - Time[0]) < Time_open 条件仍将是不可能的, 即通过阻塞此蜡烛条上随后的入场来达成。

这样, 无需任何入场数量计数器, 以及分析价格变动的幅度, 我们就解决了虚假触发的问题。

以下是 EA 的初始入场算法经过简单改进后的结果 ("MACD Sample_plus1"):

我们看到, 在一根蜡烛条上只有一次入场, 不存在任何虚假触发, 且散射完全消除。省缺设置全部保存, 所以很显然, 这个问题在不改变 EA 设置的协助下得以解决。

现在入场的散射问题得以解决, 我们将改进入场算法以便排除快速平仓时可能的散射, 在这种特殊情况下增加盈利 (脉冲很不错, 快速离场, 早发)。


离场算法里的解决方案

由于最初的问题涉及如何消除交易机器人的散射可能性, 而非增加盈利, 那么在此话题里我将不会考虑分析动态金融工具的相关问题, 并通过固定选择的参数限制我自己, 这种动态不予考虑。

之前, 我们已经使用了一个安全性参数和时间因素, 我们将用它再次严格规范依照时间平仓的时刻, 具体而言, 紧随蜡烛条开盘的关键点 (入场之后)。在离场算法中的这一时刻, 我们将显示为:

if(!OrderSelect(cnt,SELECT_BY_POS,MODE_TRADES))
         continue;
      if(OrderType()<=OP_SELL &&   // 检查已开仓位 
         OrderSymbol()==Symbol())  // 检查品种
        {
         //--- 已开多头仓位
         if(OrderType()==OP_BUY)
           {
            //--- 应平仓否?
            if(/* MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && // 删除 MACD 离场触发代码, 不去干扰平仓的新条件 (看之后)
               MacdCurrent>(MACDCloseLevel*Point) &&
             */
               Bid > OrderOpenPrice() &&  // enter new string - optional (price in a positive area in regards to the entry level)
               TimeCurrent() == Time[0] ) // 输入新字符串 (立场算法的简单实现: 离场限制在当前蜡烛条的开盘时刻)
              {
               //--- 平仓退出
               if(!OrderClose(OrderTicket(),OrderLots(),Bid,3,Violet))
                  Print("平仓错误 ",GetLastError());

               return;
              }


如此小的修改可令入场算法起作用 (已开仓位, 无条件平仓), 持仓直到 TimeCurrent() == Time[0] 时刻并在新蜡烛条开始的脉冲到达时并行平仓。最终, 不仅散射得到保护, 我们还赢得了良好的收益 (参看图片 "MACD Sample_plus2"):

为此目的, 我们不得不从离场算法里去除由 MACD 触发, 否则离场的必要条件将不能发生。

因此, 看来该散射问题可以分别在入场和离场的算法里解决。现在, 我们来讨论如何通过连接这些开仓和平仓算法来解决问题。


连接开仓和平仓算法

连接意味着整个过程的初步建模: 开仓 - 管理 - 平仓。这也反映在入场和离场算法里如何选择指标和函数。

例如, 如果您在离场算法里使用 TimeCurrent() = Time[0] 条件, 且离场点的设置限制在当前蜡烛条的开始点, 则入场算法应在之前的完整柱线上测试, 所以离场条件可满足。因此, 为了在 TimeCurrent() = Time[0] 条件且无其它附加条件下平仓, 完整的比较算法 (离场) 有必要在之前 (完成) 的柱线上执行。在指标的设置里应有一个抵消等于 1, 参与数值比较。在这种情况下, 数值比较将会是正确的, 并且当前蜡烛条的开始将是离场算法逻辑的终结。

这样, 离场与入场的连接算法也与时间因素链接。


结论

通过在入场算法里使用时间因素, 智能交易程序的虚假触发问题得以有效解决。此外, 通过固定离场点 (例如, 依照时间), EA 操作的稳定性也得以达成, 并通过触发和抵消的主要逻辑的初步建模连接入场和离场算法 (指标的一根柱线或是函数将被计算)。

以下是 EA 代码: 初始那个 (MACD_Sample.mq4), 含有入场改进 (MACD_Sample_plus1.mq4), 含有离场改进 (MACD_Sample_plus2.mq4)。只有买入通道有所提高, 而卖出通道依旧没有改变, 这是为了刻意比较初始和改进算法。

而且, 当然, 所有介绍的 EA 均用于演示目的, 而非针对金融市场的实盘交易。