English Русский Español Deutsch 日本語 Português
如何在莫斯科交易所安全地使用您的 EA 进行交易

如何在莫斯科交易所安全地使用您的 EA 进行交易

MetaTrader 5示例 | 2 十二月 2015, 09:37
1 833 1
Vasiliy Sokolov
Vasiliy Sokolov

目录


介绍

所有在金融市场里交易的人都要承受经济损失的风险。这些风险的性质不同, 但结局一致 - 赔了钱, 浪费了时间以及持久的挫败感。为了避免这些不愉快的事情, 我们应该遵循一些简单的规则: 管理我们的风险 (资金管理), 制定可靠的交易算法, 并使用有利可图的交易系统。这些规则涉及到不同的交易领域, 我们应该将它们结合起来, 令我们有可能希望可靠的正面交易结果。

目前, 您可以找到大量涵盖资金管理问题的书籍和文章, 以及可以在日常交易活动中使用的交易系统。不幸的是, 在市场交易安全规则的基础上这些全是不真实的。

本文旨在通过说明上述在市场进行交易时必须遵守的安全规则, 来改变这种状况。这些规则由方法和交易实施组成, 可令您避免因价格尖刺、流动性缺乏和其它不可抗力导致的巨额经济损失。本文的重点在于技术风险, 撇开交易策略的开发与风险管理的话题。

它带来了一些实践方法, 其交易原理在文章 "Principles of Exchange Pricing through the Example of Moscow Exchange's Derivatives Market - 莫斯科交易所衍生产品市场为例的定价原则" 里描述。尽管所提到的文章涉及交易所定价原理, 本文描述的是当一些危险的交易价格要素导致偶然的金融崩溃时, 保护您和您的 EA 的机制。


第 1 章. 价格流动的离散性以及如何面对它


1.1. 价格流动的离散性。价格缺口

流动性是股票市场的主要概念之一。它是市场按照最接近价格从您这里买入或卖给您商品的能力。市场流动性越高, 追随者的市场价格越接近。定价是一个离散的过程。这意味着, 我们所采用的价格由高速完成的多笔交易构成。交易的流动是由报价形成, 或分时价图表, 之后重组为任意时间帧的蜡烛或柱线图表。从交易者的角度来看, 这些图表是连续的。在任意给定的时刻, 柱线或蜡烛具有确定的价格。这可如下方式所示:


图示. 1. 价格柱线和其连续价格功能

无论我们从何点取值, 它都有自己的价位, 如红色线。这正是在 MetaTrader 策略测试器 "每笔分时价" 模式中柱线所呈现的。在此模式, 价格是连续且顺序生成。例如, 如果步长为 1, 价格从 10 移动到 15, 则价格 11, 12, 13, 和 14 在移动期间对于我们也同样存在。在实际当中, 价格是离散的, 以很小的跳跃变化。此外, 这些价格变化可能并不总是一致的, 规则的。有时, 价格可能会一次跳过若干级别。让我们以更实际 (离散) 的价格变化来审视一下相同柱线:

图示. 2. 价格柱线和其离散价格功能

正如我们所见, 实际上没有连续价格这回事儿 (如红色短线所示)。这意味着您的市价单 (尤其是止损单) 可能在一个意想不到的价格被触发!这是市场订单的一个非常危险的特征。让我们来观察一笔高位买挂单如何在此根柱线里被触发。假设价格抵达价位 64 203 或更高时我们发送一个市场请求 (蓝色短线穿越价格柱线)。然而, 这个价格可能在柱线里根本就不存在。在此情况下, 我们的订单在下一个明显高于 64 203 的价位被激活:

图示. 3. 订单在离散的价位被激活

在我们的例子中, 实际的订单执行在 64 220 点处发生, 而这比我们请求的价格要差 13 个点。这些价格的差别形成了点差。如果市场流动性足够, 离散价格在密集流动时会平滑地依次移动。然而, 如果价格价格变化很迅速, 即使是高流动性市场, 依然会有价格缺口。这不可能通过观察一般的价格图表看到缺口, 但我们应该意识到它们的存在。


1.2. 价格尖刺

由于缺乏流动性, 价格缺口可能到达很高的数值而产生 价格尖刺 (在距市场偏离很高的价位成交)。无论对于手工交易者或自动交易系统, 它们都是很危险的。这样的尖刺会在非常不利的价位触发停止挂单的执行。

然我们来想象一个简单的情况: 假设我们交易一笔 RUB/USD 期货合约, 且在 64 200 之处放置一笔高位买订单。止损放置于 64 100。我们期望价格上移, 即使它没有发生, 我们的止损在 64 100, 限定我们的亏损为 100 点。我们的风险好似受控, 但实际上这不是真的。让我们观察发生价格尖刺的情况, 我们的挂单在十分不同的价位被激活:


图示. 4. 分时价图表示一个尖刺, 以及高位买订单执行

在此分时价图表上, 我们看到分时价之一距其它价格的位置十分遥远, 形成了一个尖刺。这笔分时价在 64 440 价位触发了我们的高位买订单。在下一笔分时价中, 价格返回了当前范围, 并在 64 100 价位触发了我们的止损单。在小于一秒钟之内, 我们的挂单可以被触发并以止损平仓, 留给我们的是巨幅亏损。代替我们计算的 100 点亏损, 我们的亏损是 340 点。

而实际上, 尖刺可能更大。所以, 一个单一的巨型尖刺足以令我们爆仓, 无论账户规模的大小!为了避免这样的灾难, 您需要遵守以下描述的简单保护规则。

请注意, 在策略测试器的 "每笔分时价" 模式, 这种尖峰发生所模拟的价格, 可能比在真正的市场中更好。如果我们按照图示里显示的价格间隔测试我们的策略, 我们的挂单应经历一个最小 (如果任意) 滑点。正如我们所知, 在策略测试器里一根柱线内部的价格流比较连续的, 这意味着测试器执行我们订单的价格与我们所设置的极其接近, 几乎没有滑点。其实, 这样的情况在策略测试器里也应考虑。为此, 您应选择一个特别的测试模式。我们将在第 3 章的专门章节讨论有关它。 


1.3. 使用限价单管理最大滑点

我们已经发现市价单和停止单对于滑点都无法保护。也许是流动性不足以满足我们的要求, 或者也许是市场短期内失去了流动性导致价格尖刺。此外, 这种的尖刺发生在低流动性的市场很常见, 如 FORTS 衍生物市场。不过, 您可以使用 限价订单 替代市价单和停止单来避免此种情况。

限价单始终在指定的价格执行, 不会有很高的差价。在交易所执行模式限价单有一个有趣的特性, 它有能力在当前价位执行, 即使其价位或高或低于指定的订单价格。

例如, 如果 RUB/USD 期货合约的当前价格是 64 200, 我们可以在 64 220 设置一笔限价买订单。这意味着我们同意在价格不高于 64 220 时买入。由于当前价格 64200 优于订单中的设置, 所以我们的订单在放置后立即执行。因此, 我们能够管理的最大滑点值。如果出于某种原因在 64 220 的价位没有足够的流动性, 我们的订单会有一部分根本不会被执行。

请注意, 您只能使用限价单来管理滑点。在交易所执行模式下, 通常市价单不允许您设置最大滑点级别。所以, 在低流动性市场中限价单是保持安全的唯一方式。

使用限价单入场和离场是合理的。即使您的策略需要在当前市价入场或离场, 您都可以放置它们。分别用限价卖和限价卖替换买和卖订单。例如, 如果您打算在当前价位买入, 放置一笔最大执行价格略高于当前市价的限价订单。对于卖出同理。在那种情况下, 放置您的限价订单时执行价格略低于当前市价。在限价订单里设置的价格和当前价格之间的差价即是您可接受的最大滑点。

让我们看看以下的例子。假设我们在 1.1356 买入大手数的 ED-3.15 EUR/USD 期货合约。当前流动性十分低。有意选择这种时刻来示意使用限价单入场的益处。我们入场之时恰逢一个价格尖刺, 这可以从 M1 图表上看出:

图示. 5. 入场之时恰好价格尖刺发生, ED-3.15

这很明显, 市场的切入点是完全不利的。让我们来分析分时价图表上的那一刻:


图示. 6. 分时价图表和流动性突破期间的限价单执行

我们的限价单执行以白色大圆圈显示 (分时价): . 分时价被绘制为蓝色圆点。如果我们在市价 1.1356 之处买入, 我们的市场请求将分若干笔事务填充, 起始价格从 1.1356 而结束价格是 1.1398。这会导致强烈的滑点, 而我们的平均入场价格显然会比 1.1356 更差。需要更多的事务来填充我们的请求, 入场价更糟。

在我们的情况里, 这种巨大的价格缺口是由低流动性造成的, 当限价请求出于各种原因消失, 且价格进行无序的宽幅振荡。但限价单有一个内在的保护。如果当前价格超过 1.1356 它根本就不会执行。例如, 我们的限价单由七笔事务执行 - 它们在图上以白色大圆圈显示。在这些事务之间还有其它价格, 单全都比 1.1356 更差。所以, 它们直接被忽略。过一段时间, 价格趋于稳定, 我们的订单终于完整执行。


1.4. 手工设置限价单管理最大滑点

现在, 我们已经涵盖了限价单的激活原理, 是时候加入一些实践, 并在真实的市场环境里使用我们的知识。假设我们的帐户已连接到莫斯科交换所。让我们在当前价格略差一点的价位放置一笔限价订单。此外, 我们就近选择 EUR/USD 期货合约 (ED-6.15) 作为我们的操作品种。调用开仓窗口并在当前卖价 略高 一点的价位设置一笔限价卖:

图示. 7. 在交易所执行模式手工放置一笔限价订单

图示. 7. 在交易所执行模式手工放置一笔限价订单

正如我们在截图中所见, 当前卖价是 1.1242, 此时我们已经在 1.1245 设置了一笔挂单。在我们的价格和最佳出价之间的差价是 0.0003 点 (1.1245 - 1.1242 = 0.0003)。此数值是我们准备暴露的最大滑点。在交易所执行模式, 这种限价订单等同于发送一笔具有最大滑点 (偏离) 的普通买或卖订单:

图示. 8. 按照指定的偏离执行市价单

图示. 8. 按照指定的偏离执行市价单

由于最大滑点在交易所执行模式不可用, 指定偏离的唯一方式是按照图示.7 所示方法设置一笔限价订单。


1.5. 设置使用 EA 时交易执行模式的最大滑点

现在, 让我们用程序来放置一笔限价单。为此, 我们要编写一个简单的面板包含以下元素:

  • BUY 按钮 – 使用限价买订单买入;
  • SELL 按钮 – 使用限价卖订单卖出;
  • 最大滑点字段 (单位点数) 将会稍会加入;
  • 买或卖的交易量也将会在面板的下一个版本里加入。

以下截图是面板的第一个版本:

图示. 9. 在 DeviationPanel 里设置最大滑点

面板利用 CDevPanel 类制作。以下是它的源代码:

//+------------------------------------------------------------------+
//|                                                       Panel.mqh  |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Trade\Trade.mqh>
#define OP_BUY 0
#define OP_SELL 1
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CDevPanel
  {
private:
   CTrade            Trade;
   string            m_descr_dev;
   string            m_buy_button_name;
   string            m_sell_button_name;
   string            m_deviation_name;
   string            m_volume_name;
   string            m_bg_fon;
   int               m_deviation;
   void              OnObjClick(string sparam);
   void              OnEndEdit(string sparam);
   double            CalcCurrentPrice(int op_type);

public:
                     CDevPanel();
                    ~CDevPanel();
   void              OnChartEvent(const int id,
                                  const long &lparam,
                                  const double &dparam,
                                  const string &sparam);
  };
//+------------------------------------------------------------------+
//| CDevPanel 类                                                     |
//+------------------------------------------------------------------+
CDevPanel::CDevPanel(): m_buy_button_name("buy_button"),
                        m_sell_button_name("sell_button"),
                        m_deviation_name("deviation"),
                        m_volume_name("volume"),
                        m_bg_fon("bg_fon"),
                        m_descr_dev("descr_dev"),
                        m_deviation(3)
  {
//--- 背景
   ObjectCreate(0,m_bg_fon,OBJ_RECTANGLE_LABEL,0,0,0);
   ObjectSetInteger(0,m_bg_fon,OBJPROP_YSIZE,80);
   ObjectSetInteger(0,m_bg_fon,OBJPROP_XSIZE,190);
   ObjectSetInteger(0,m_bg_fon,OBJPROP_BGCOLOR,clrWhiteSmoke);

//--- 买入按钮
   ObjectCreate(0,m_buy_button_name,OBJ_BUTTON,0,0,0);
   ObjectSetInteger(0,m_buy_button_name,OBJPROP_XDISTANCE,100);
   ObjectSetInteger(0,m_buy_button_name,OBJPROP_YDISTANCE,50);
   ObjectSetInteger(0,m_buy_button_name,OBJPROP_XSIZE,80);
   ObjectSetInteger(0,m_buy_button_name,OBJPROP_BGCOLOR,clrAliceBlue);
   ObjectSetString(0,m_buy_button_name,OBJPROP_TEXT,"BUY");

//--- 卖出按钮
   ObjectCreate(0,m_sell_button_name,OBJ_BUTTON,0,0,0);
   ObjectSetInteger(0,m_sell_button_name,OBJPROP_XDISTANCE,10);
   ObjectSetInteger(0,m_sell_button_name,OBJPROP_YDISTANCE,50);
   ObjectSetInteger(0,m_sell_button_name,OBJPROP_XSIZE,80);
   ObjectSetInteger(0,m_sell_button_name,OBJPROP_BGCOLOR,clrPink);
   ObjectSetString(0,m_sell_button_name,OBJPROP_TEXT,"SELL");

//--- 偏离
   ObjectCreate(0,m_deviation_name,OBJ_EDIT,0,0,0);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_XDISTANCE,120);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_YDISTANCE,20);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_XSIZE,60);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_BGCOLOR,clrWhite);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_COLOR,clrBlack);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_ALIGN,ALIGN_RIGHT);
   ObjectSetString(0,m_deviation_name,OBJPROP_TEXT,(string)m_deviation);

//--- 描述
   ObjectCreate(0,m_descr_dev,OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,m_descr_dev,OBJPROP_XDISTANCE,12);
   ObjectSetInteger(0,m_descr_dev,OBJPROP_YDISTANCE,20);
   ObjectSetInteger(0,m_descr_dev,OBJPROP_XSIZE,80);
   ObjectSetInteger(0,m_descr_dev,OBJPROP_BGCOLOR,clrWhite);
   ObjectSetString(0,m_descr_dev,OBJPROP_TEXT,"Deviation (pips):");
   ObjectSetInteger(0,m_descr_dev,OBJPROP_COLOR,clrBlack);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CDevPanel::~CDevPanel(void)
  {
   ObjectDelete(0,m_buy_button_name);
   ObjectDelete(0,m_sell_button_name);
   ObjectDelete(0,m_bg_fon);
   ObjectDelete(0,m_deviation_name);
   ObjectDelete(0,m_descr_dev);
  }
//+------------------------------------------------------------------+
//| 事件函数                                                          |
//+------------------------------------------------------------------+
void CDevPanel::OnChartEvent(const int id,
                             const long &lparam,
                             const double &dparam,
                             const string &sparam)
  {
   switch(id)
     {
      case CHARTEVENT_OBJECT_CLICK:
         OnObjClick(sparam);
         break;
      case CHARTEVENT_OBJECT_ENDEDIT:
         OnEndEdit(sparam);
     }
  }
//+------------------------------------------------------------------+
//| 编辑检测结束                                                      |
//+------------------------------------------------------------------+
void CDevPanel::OnEndEdit(string sparam)
  {
   if(sparam != m_deviation_name)return;
   int value = (int)ObjectGetString(0, m_deviation_name, OBJPROP_TEXT);
   if(value <= 0)
      ObjectSetString(0,m_deviation_name,OBJPROP_TEXT,(string)m_deviation);
   else
      m_deviation=value;
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| 对象点击结束                                                      |
//+------------------------------------------------------------------+
void CDevPanel::OnObjClick(string sparam)
  {
   if(sparam==m_buy_button_name)
      Trade.BuyLimit(1,CalcCurrentPrice(OP_BUY));
   if(sparam==m_sell_button_name)
      Trade.SellLimit(1,CalcCurrentPrice(OP_SELL));
   ObjectSetInteger(0,sparam,OBJPROP_STATE,false);
   Sleep(100);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| 计算价位                                                          |
//+------------------------------------------------------------------+
double CDevPanel::CalcCurrentPrice(int op_type)
  {
   if(op_type==OP_BUY)
     {
      double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      return ask + (m_deviation * Point());
     }
   else if(op_type==OP_SELL)
     {
      double bid=SymbolInfoDouble(Symbol(),SYMBOL_BID);
      return bid - (m_deviation * Point());
     }
   return 0.0;
  }
//+------------------------------------------------------------------+

面板可以设置市价执行订单的最大滑点 (单位点数)。实际入场/离场使用限价单执行。

面板只有当经纪商提供市价执行订单时可工作。否则, 当输入限价单价格时代码会引发一个标准错误价格:

2015.04.15 14:08:39.709 Trades  '58406864': failed buy limit 0.10 EURUSD at 1.05927 [Invalid price]


1.6. 高位限价买和低位限价卖订单作为高位买和低位卖订单的替代

限价订单为滑点提供了一个方便、自然的防御。但有时候, 有必要使用挂单, 并在突破某一确定级别之时被触发。一笔止损单是最明显的例子。此外, 一些策略在价格离开确定通道时应该响应。它们也需要停止单入场。不过, 正如我们所知, 停止单目的是滑点, 但对于流动性问题不能提供保护。此外, 您不能为它们设置最大滑点值。

在这种情况下, 我们应使用 高位限价买低位限价卖 订单。这些是用于 MetaTrader 5 的算法订单。它们不在市场内执行, 而在 MetaTrader 服务器端实施。让我们来看看官方文档:

  • 高位限价买— 这个类型组合了两类, [限价买和高位买] , 是置于停止位的一笔限价买订单。只要未来的卖价抵达订单指定的停止级别 (价格字段), 一笔限价买订单将置于停止限价字段指定的价位。
  • 低位限价卖 — 这个类型是置于停止位的一笔限价卖订单。只要未来的买价抵达订单指定的停止级别 (价格字段), 一笔限价卖订单将置于停止限价字段指定的价位。

文档也提供了图片 (图示. 10) 描绘了订单在 MetaTrader 5 内的操作原理。黄色框标记的两种订单类型, 是我们目前感兴趣的:


图示. 10. 在 MetaTrader 5 中的订单类型

所以, 这些限价订单在价格抵达一个确定的停止位时被放置于市场内。对于高位限价买订单, 停止价位高于当前卖价, 而对于低位限价卖, 它低于当前买价。在交易所执行模式下限价单价格即可高于或低于这些订单的停止价。这个特点允许我们配置 滑点可控的特别停止订单。下图示意它如何工作:

图示. 11. 通过放置高位限价买订单设置最大滑点

我们可以放置一笔高位限价买订单, 其限价超过停止价。一旦抵达停止价位, 限价买订单被立即执行, 因为限价比当前的停止价更差。停止价和限价之间的差价形成了我们决定在订单里设置的最大滑点。低位限价卖订单的操作方式类似, 虽然在此情况下限价应低于停止价。

现在, 让我们来进行一些实践, 并手工放置一笔高位限价买订单。


1.7. 手工设置高位限价买和低位限价卖订单替代止损

假设我们想用停止单来保护我们的持仓。但低流动性的市场十分危险, 且无法预测如何使用停止或市价单。一笔停止单 (例如, 一笔止损) 不能在无限滑点时进行保护。因此, 巨大的价格缺口或尖刺可能会令我们彻底爆仓。为了避免这种情况, 我们应该以一笔停止限价订单替代一笔停止单。

让我们看看以下的例子。假设我们有一笔多头仓位 Si-6.15。止损位是 56 960。我们应设置最大五点的滑点, 因此停止限价位是 56 960 - 5 = 56 955 点:

图示. 12. 放置一笔低位限价卖订单作为一笔多头仓位的停止位。

图示. 12. 放置一笔低位限价卖订单作为一笔多头仓位的停止位。

正如我们所看到的, 这样的低位限价卖订单配置在交易所执行模式成为可能。若当前的价格达到 56960, 限价订单在 56955 价位被放置。由于当前 56960 的价格比限价单指定的价格更好, 它会立即在 56960 执行。如果在此价位没有足够的流动性, 则在随后的价格降低到 56955 时执行。限价单不会在 56 955 更坏的价位执行, 确保最大五个点的滑点: 56 960 - 56 955 = 5。

现在, 让我们以同样的方式来保护我们的空头仓位。为了通过止损来为空头仓位平仓, 我们需要执行一笔反向操作 — 我们应使用一笔高位限价买订单买入。假设我们的空头仓位的止损位是 56 920, 则我们应使用以下高位限价买订单配置来提供最大五个点的滑点:

图示. 13. 放置高位限价买订单作为空头仓位的止损位

图示. 13. 放置高位限价买订单作为空头仓位的止损位

这次, 停止限价位字段相较于 56 925 超出五个点。


1.8. 在 EA 中采用高位限价买和低位限价卖订单替代止损

让我们返回第 1.5 节所述的面板。我们应该修改它, 令其可以使用高位限价买和低位限价卖订单来放置保护性停止。为此, 让我们加入一个新字段称为 Stop-Loss。现在, 我们的面板如下所示:

图示. 14. 在 DevaitionPanel 里放置止损位

此处代码里有两个明显的变化: CDevPanel 类现在有一个新方法负责放置高位限价买和低位限价卖订单。用于开新仓位的方法 OnObjClick 已经被修改。方法的源代码如下:

//+------------------------------------------------------------------+
//| 对象点击结束                                                      |
//+------------------------------------------------------------------+
void CDevPanel::OnObjClick(string sparam)
  {
   if(sparam==m_buy_button_name)
     {
      if(Trade.BuyLimit(1,CalcCurrentPrice(OP_BUY)))
         SendStopLoss(OP_BUY);
     }
   if(sparam==m_sell_button_name)
     {
      if(Trade.SellLimit(1,CalcCurrentPrice(OP_SELL)))
         SendStopLoss(OP_SELL);
     }
   ObjectSetInteger(0,sparam,OBJPROP_STATE,false);
   Sleep(100);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| 发送 SL 订单                                                      |
//+------------------------------------------------------------------+
bool CDevPanel::SendStopLoss(int op_type)
  {
   if(op_type==OP_BUY)
     {
      double bid=SymbolInfoDouble(Symbol(),SYMBOL_BID);
      if(m_sl_level>=0.0 && m_sl_level<bid)
        {
         MqlTradeRequest request={0};
         request.action = TRADE_ACTION_PENDING;
         request.symbol = Symbol();
         request.volume = 1.0;
         request.price=m_sl_level;
         request.stoplimit=m_sl_level -(m_deviation*Point());
         request.type=ORDER_TYPE_SELL_STOP_LIMIT;
         request.type_filling=ORDER_FILLING_RETURN;
         request.type_time=ORDER_TIME_DAY;
         MqlTradeResult result;
         bool res=OrderSend(request,result);
         if(!res)
            Print("设置 S/L 错误。原因: "+(string)GetLastError());
         return res;
        }
     }
   else if(op_type==OP_SELL)
     {
      double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      if(m_sl_level>=0.0 && m_sl_level>ask)
        {
         MqlTradeRequest request={0};
         request.action = TRADE_ACTION_PENDING;
         request.symbol = Symbol();
         request.volume = 1.0;
         request.price=m_sl_level;
         request.stoplimit=m_sl_level+(m_deviation*Point());
         request.type=ORDER_TYPE_BUY_STOP_LIMIT;
         request.type_filling=ORDER_FILLING_RETURN;
         request.type_time=ORDER_TIME_DAY;
         MqlTradeResult result;
         bool res=OrderSend(request,result);
         if(!res)
            Print("设置 S/L 错误。原因: "+(string)GetLastError());
         return res;
        }
      if(CharToStr(StringGetChar(data,strlen-1))=='.')
         StringSetChar(data,strlen-1,'');
     }
   return false;
  }

这些方法之外, 面板类代码现在包括初始化和用于输入止损相关的字段。现在, 如果我们在点击 BUY 或 SELL 之前填入止损字段, 新的市价单伴随着一个特殊的保护性高位限价买或低位限价卖订单 (根据仓位方向)。


第 2 章. 市场流动性分析


2.1入场之前计算滑点

股票市场的特征是中央集中式交易。因此, 所有限价买/卖订单都可以从市场深度中观察到。如果我们返回在文章 "Principles of Exchange Pricing through the Example of Moscow Exchange's Derivatives Market - 莫斯科交易所衍生产品市场为例的定价原则" 里提到的定义, 我们可以看到位于市场深度里的限价订单提供了 市场流动性 (在 最后的 成交价附近买卖确定交易量的能力)。

我们希望买卖的交易量越大, 市场深度里的越多订单触发, 则会增加滑点, 因为我们不得不吸收更多距当前价十分遥远的价位流动性提供者。您可以在上述 "交易所定价原则" 一文里找到更多有关滑点如何工作的描述。让我们看看下面的简单例子, 使问题更加清晰。

在任意特定时刻, 我们有市场深度描述的买/卖交易量。当下, 我们审查 Si-6.15 USD/RUB 期货合约的市场深度:


图示. 15. Si-6.15 期货合约的市场深度

如果我们买入 2 份合约, 我们会在最佳卖价执行成交且无滑点: 51 931。但是如果我们买 4 份合约, 我们的平均价格相较于 51 931 会有不同: (2*51 931+2*51 932)/4 = 51 931.5。我们在 51 931 买入两份合约, 而剩余的两份 – 在 51 932。51 931.5 是加权平均入场价。它与最佳卖价之间的差价形成了我们的滑点

现在, 我们可以依据我们的成交量安排流动性表格, 定义滑点值。在 1 或 2 份合约时, 我们的成交执行在最佳卖价 (51 931) 且无滑点。在 4 c份合约情况下, 滑点是 0.5 点 (51 931.5 - 51 931.0)。公式十分简单: 从加权平均入场价里减去最佳卖价或买价 (依据成交方向)。

流动性表格如下所示:

交易量 价格 成交
加权平均
入场价
滑点
2  51 938 25 51 934.5 3.5
9  51 936  23 51 934.2 3.2
3  51 935  14 51 933.0 2.0
7  51 933  11 51 932.5 1.5
2  51 932  4 51 931.5 0.5
2  51 931  2 51 931.0 0.0

表 1. 计算加权平均入场价及相应的滑点

此表应自下向上检查, 与市场深度的卖价类似。正如我们所见, 两份合约的交易量并没有滑点。四份合约成交量有 0.5 点的滑点。25 份合约成交量有 3.5 点滑点, 且其加权平均价是 51934.5。

中央集中式市场和市场深度可令我们得出如下结论:

知道市场深度的状态 , 我们可以在实施一笔成交之前计算潜在的滑点。

所以, 我们能够管理我们的风险。无论我们是否采用手工或交易机器人, 我们可以在入场前定义市场深度。在此实例中, 我们可以将一名交易者比做潜水员。在跳入水中之前, 潜水员应该知道泳池的深度。潜水员越大, 泳池应该越深。与此类似, 成交量越大, 我们就需要更多的流动性。当然, 市场深度可以在我们入场之前改变。但即使略为过时, 剩余的计算精度对于成交执行依然足够。


2.2. 实时潜在计算滑点

现在, 是时候将理论付诸实践了。手工计算潜在滑点是不可能的, 因为市场深度变化太快, 而计算本身是相当麻烦的。所以, 我们需要将之自动化。为了便于计算, 我们实现了一个特殊的类 CMarketBook 来操作市场深度。开发这样的类是一件很困难的任务, 值得另起一篇文章。在此没必要讲述它的操作原理。代之, 我们将使用其方法之一: GetDeviationByVol。让我们来看看它是如何工作的:

//+------------------------------------------------------------------+
//| 通过交易量获取偏差值。返回 -1.0 如果偏离是                          |
//| 无穷大 (流动性不足)                                               |
//+------------------------------------------------------------------+
double CMarketBook::GetDeviationByVol(long vol,ENUM_MBOOK_SIDE side)
  {
   int best_ask = InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   int last_ask = InfoGetInteger(MBOOK_LAST_ASK_INDEX);
   int best_bid = InfoGetInteger(MBOOK_BEST_BID_INDEX);
   int last_bid = InfoGetInteger(MBOOK_LAST_BID_INDEX);
   double avrg_price=0.0;
   long volume_exe=vol;
   if(side==MBOOK_ASK)
     {
      for(int i=best_ask; i>=last_ask; i--)
        {
         long currVol=MarketBook[i].volume<volume_exe ?
                      MarketBook[i].volume : volume_exe;
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe<=0)break;
        }
     }
   else
     {
      for(int i=best_bid; i<=last_bid; i++)
        {
         long currVol=MarketBook[i].volume<volume_exe ?
                      MarketBook[i].volume : volume_exe;
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe<=0)break;
        }
     }
   if(volume_exe>0)
      return -1.0;
   avrg_price/=(double)vol;
   double deviation=0.0;
   if(side==MBOOK_ASK)
      deviation=avrg_price-MarketBook[best_ask].price;
   else
      deviation=MarketBook[best_bid].price-avrg_price;
   return deviation;
  }

当调用方法时, 它参考市场深度。它从最佳价格开始遍历市场深度并计算此处可用的交易量。一旦可用交易量等于或超过需求, 方法停止搜索并计算预检交易量相对应的加权平均价。计算出的加权均价与最佳卖价或买价之间的差价形成了我们的滑点。

如果出于某些原因, 市场深度的流动性对于指定的交易量不足, 方法返回 -1.0 指明潜在滑点无法计算。

现在我们有了潜在滑点计算方法, 我们需要可视化的获得结果。显然, 滑点值与在市场上买入或卖出的交易量有关。交易量越大, 滑点越大。因此, 我们需要在面板上添加一行新输入字段称为 Volume (交易量):

图示. 16. 拥有交易量的面板

现在, 我们的面板能够买入或卖出任意交易量。例如, 如果我们按市价买入 5 份合约, 我们应简单地在 Volume 字段输入 5并点击 BUY。这不是唯一的创新。正如已经提到的, 我们可以在入场时管理滑点, 感谢 GetDeviationVol 方法。

出于更多的可见性, 让我们直接在 BUY 和 SELL 按钮上显示计算值。我们指定滑点以点为单位。该值每次市场深度变化时重新计算。当流动性增加, 则滑点值下降, 反之亦然。如果我们希望只买入或卖出一份合约, 则根本没有滑点, 因为 1 手的交易量不会超出最佳买价/卖价的交易量。

我推荐您实时察看更新的面板。以下视频示意实时计算 RTS-6.15 期货合约的潜在滑点:


开始, 在 Volume 字段输入一份合约。正如预期的那样, BUY 和 SELL 按钮显示 0。这意味着我们入场时不会引起滑点。在交易量增加到 100 份合约时, 平均买卖滑点已增加到 10-20 点。当交易量增加到 500 份合约时, 平均滑点变为 60-80 点。最后, 在我们设置交易量达到 1 500 份合约时, 我们经历了流动性不足 - 在 BUY 按钮上显示数值 1.0 (滑点无法定义)。需求的流动性依然充足, 尽管卖出如此巨量合约将会导致 100-130 点的滑点。

操作市场深度的类, 以及 DeviationPanel 最终版的源代码附加在下面。


2.3. 使用 SpreadRecord 点差指标作为入场过滤器

在入场前分析当前市场流动性是有用且合理的习惯。一款开发良好的交易机器人为您执行复杂的计算, 令您在危险的滑点时安全。但是, 这还不够。

另一个交易者需要应对的问题是可靠的 点差 宽度确定。点差是最佳卖价和买价之间的差价。点差是主要的相对参数, 因为巨量交易会影响正常的市场深度流动性, 甚至超过点差宽度本身。然而, 交易者通常无法访问市场深度历史, 所以访问一笔交易合约的已往流动性十分困难。另一方面, 点差与品种的流动性呈负相关。当点差收窄, 流动性较高, 反之亦然。

考虑到这一特性, 我们可以开发一个点差指标显示过去的点差值。这个指标在交易中极端有用, 因为它可让我们可视化地评估已往的流动性和品种的点差宽度。知道了平均价差值, 我们就可以在流动性急剧变化, 点差显著扩大时限制我们的交易。

所以, 让我们来建立这个指标。它会在图表下半部窗口里显示其值的柱线。平均点差水平在柱线的合适级别显示为绿色的圆点。指标计算以下点差值:

  • 柱线开盘时刻的点差;
  • 柱线周期内抵达的最大点差;
  • 柱线周期内抵达的最小点差;
  • 柱线收盘时刻的点差;
  • 柱线周期内的平均点差;

指标不保存点差值, 且在终端重启后, 从最后一根柱线开始绘制。指标的源代码如下所示:

//+------------------------------------------------------------------+
//|                                                Spread Record.mq4 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com/ru/users/c-4"
#property version   "1.00"
#property description "记录点差并显示它。"
#property indicator_separate_window
#property indicator_buffers 5
#property indicator_plots   5
#property indicator_type1   DRAW_BARS
#property indicator_type2   DRAW_ARROW
#property indicator_color1   clrBlack
#property indicator_color2   clrBlack
double spread_open[];
double spread_high[];
double spread_low[];
double spread_close[];
double spread_avrg[];
int elements;
double avrg_current;
int count;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                               |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 指标缓存区映射
   SetIndexBuffer(0,spread_open,INDICATOR_DATA);
   SetIndexBuffer(1,spread_high,INDICATOR_DATA);
   SetIndexBuffer(2,spread_low,INDICATOR_DATA);
   SetIndexBuffer(3,spread_close,INDICATOR_DATA);
   SetIndexBuffer(4,spread_avrg,INDICATOR_DATA);
   IndicatorSetInteger(INDICATOR_DIGITS,1);
   PlotIndexSetInteger(1,PLOT_ARROW,0x9f);
   PlotIndexSetInteger(0,PLOT_LINE_COLOR,clrRed);
   PlotIndexSetInteger(1,PLOT_LINE_COLOR,clrGreen);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   printf("DEINIT");
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                 |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   if(prev_calculated==0)
     {
      printf("INITIALIZE INDICATORS "+TimeToString(TimeCurrent()));
      double init_value=EMPTY_VALUE;
      ArrayInitialize(spread_high,init_value);
      ArrayInitialize(spread_low,init_value);
      ArrayInitialize(spread_open,init_value);
      ArrayInitialize(spread_close,init_value);
      ArrayInitialize(spread_avrg,init_value);
      elements=ArraySize(spread_high);
      InitNewBar(elements-1);
     }
//--- 新柱线初始化
   for(; elements<ArraySize(spread_high); elements++)
      InitNewBar(elements);
   double d=GetSpread();
   for(int i=rates_total-1; i<rates_total; i++)
     {
      if(d>spread_high[i])
         spread_high[i]=d;
      if(d<spread_low[i])
         spread_low[i]= d;
      spread_close[i] = d;
      avrg_current+=d;
      count++;
      spread_avrg[i]=avrg_current/count;
     }
//--- 返回 prev_calculated 值用于下次调用
   return(rates_total-1);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double GetSpread()
  {
   double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
   return NormalizeDouble((ask-bid)/Point(), 0);
  }
//+------------------------------------------------------------------+
//| 初始化新柱线                                                      |
//+------------------------------------------------------------------+
void InitNewBar(int index)
  {
   spread_open[index] = GetSpread();
   spread_high[index] = 0.0;
   spread_low[index]=DBL_MAX;
   avrg_current=0.0;
   count=0;
  }

让我们来尝试在 Si-6.15 分钟图表上运行这个指标。启动之后的短暂时间, 它显示如下结果:

图示. 17. SpreadRecord 指标启动在 Si-6.15 分钟图表

我们可以看到在分析期间, Si-6.15 的点差在 1 和 21 点之间振荡。在每分钟里, 至少有一个时刻的点差对应于最小 1 点值。平均值大概 3 点。如上所述, 它在指标窗口以绿色点显示。


2.4. 当点差急剧扩大时手工和自动交易的限制

现在, 我们要了解如何使用这个指标来管理我们的风险。我们能做的最简单事情就是当指标值太高时限制我们的交易活动。在选择的时间段, 指标值主要在 1-9 点的范围内。这片区域可称为 "绿色"。此处可以交易。如果点差上升到 9 点, 我们步入红色地段, 此处应禁止交易。这可如下方式所示:


图示. 18. 指标定义的启用和禁用交易地段

在手工交易限制之外, 我们也需要教导我们的 EA 来接收指标值, 若当前点差超出指定限制, 则限制其交易动作。您可以从 EA 里使用 iCustom 函数来调用指标做到这一点。该函数允许您直接从 EA 里调用任意用户指标并获取它们的数值。以下是利用指标管理点差的 EA 模板:

//+------------------------------------------------------------------+
//|                                          SpreadRecordControl.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#define OPEN  0
#define HIGH  1
#define LOW   2
#define CLOSE 3
#define AVRG  4

input int MaxSpread=9;

int h_spread_record=INVALID_HANDLE;       // 指标 SpreadRecord 的句柄
bool print_disable = false;
//+------------------------------------------------------------------+
//| 程序初始化函数                                                    |
//+------------------------------------------------------------------+
int OnInit()
  {
   h_spread_record=iCustom(Symbol(),Period(),"Spread Record");
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 程序逆初函数                                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(h_spread_record);
  }
//+------------------------------------------------------------------+
//| 程序分时价函数                                                    |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(IsTradeDisable(MaxSpread))return;
   //
   // 交易逻辑...
   //
  }
//+------------------------------------------------------------------+
//| 如果禁止交易返回 true, 否则返回 false                              |
//+------------------------------------------------------------------+
bool IsTradeDisable(int max_spread)
  {
   if(h_spread_record==INVALID_HANDLE)
      return false;
   double close[];
   if(CopyBuffer(h_spread_record, CLOSE, 0, 1, close) < 1)return false;
   if(close[0]>MaxSpread)
     {
      if(!print_disable)
         printf("交易禁止");
      print_disable=true;
      return true;
     }
   if(print_disable)
      printf("交易允许");
   print_disable=false;
   return false;
  }

函数 IsTradeDisable 主要负责定义是否允许交易。如果点差太高它返回 true 并禁止交易。如果点差正常, 函数返回 false。该函数基于调用 SpreadRecord 指标, 并使用 CopyBuffer 函数拷贝当前数值。在 EA 里指定 MaxSpread 参数等于阀值。如果值超出, 则 EA 阻断其交易行为。如果点差再次降至指定边界以下, 则 EA 继续其操作。函数 IsTradeDisable 通过相应消息指明从一个状态到另一个状态的过渡: "交易允许" 和 "交易禁止":

2015.05.27 16:57:08.238 SpreadRecordControl (Si-6.15,H1)        交易允许
2015.05.27 16:57:08.218 SpreadRecordControl (Si-6.15,H1)        交易禁止
2015.05.27 16:56:49.411 SpreadRecordControl (Si-6.15,H1)        交易允许
2015.05.27 16:56:49.401 SpreadRecordControl (Si-6.15,H1)        交易禁止
2015.05.27 16:56:36.478 SpreadRecordControl (Si-6.15,H1)        交易允许
2015.05.27 16:56:36.452 SpreadRecordControl (Si-6.15,H1)        交易禁止

您可以将这款 EA 的原型用于您的交易系统, 因此在低流动性和大滑点期间避免入场。

该 EA 和 SpreadRecord 指标的源代码附加于下。


第 3 章. 安全交易和 EA 测试模式


3.1. 使用 "休眠模式" 替代分时价控制

正如章节 1.1 "价格流动的离散性。价格缺口" 里提到的, 市场报价可比作连续的价格流。所以, 如果一个股票价格从 $10 改变到 $15, 意即价格在某个时刻会分别移动到 11, 12, 13 和 14 美元。然而, 我们已经发现, 事实并不总是如此。

价格经常大幅移动, 而我们的交易方式通常是基于报价持续渐变这样的假设。当我们设置了一个止损, 我们认为我们的仓位将在遭受重大亏损前于止损位平仓。然而, 许多停止订单的基础是认可当超出确定级别之后的任何价位买入或卖出。在离散价格情况下, 这种停止订单变为潜在的亏损制造者。如果当前价格比之我们在停止订单里指定的价格差好几倍, 止损被执行, 留给我们的是高出很多的亏损。

在另一方面, 如果一款 EA 在每笔分时价来临时检查市场状况, 它也有在极其不利价格平仓的风险: 在低流动性情况下, 最后一笔交易生成的分时价, 其中的一部分可能会沿其方向达到不可思议的价位并产生一个价格尖刺。

所以, 替代跟踪每次市场分时价, 运用一些 "退敏" 策略更加合理, 这样的 EA 交易逻辑称为每个确定时间周期调用一次 (譬如, 每分钟一次) 而并非每笔分时价使用停止单毫无疑问也如此。代之, 每个确定时间周期检查一次激活条件作为算法 (虚拟) 停止是更明智的。

可能看上去这种交易逻辑退敏将明显扭曲交易结果, 但事实并非如此。当然, 价格可能在一分钟内从一个潜在的离场或入场价格移动很远的距离, 但成交执行远离价格反转点也是更有利的条件。

让我们来考察真实的市场案例, 观察 5 月 28 日的 Si-6.15 RUB/USD 期货合约。一个可观的尖刺发生在 10:03 (莫斯科时间)。假设此时我们有一笔多头仓位, 价格在 53 040 止损位在 52 740 (300 点)。在此情况下, 我们的止损将在比我们指定的止损位更低的价格被触发。

由实践表明, 在价格尖刺期间, 止损通常在接近最差的那些价格被触发。在此情况下, 它会是 52 493, 带给我们每份合约 53 040 - 52 493 = 547 卢布的亏损 (而不是我们指定的止损位 300 卢布)。此案例如下图 A 所示。如果我们每分钟检查一次止损, 则价格尖刺会被我们的策略忽略掉, 止损不会被触发, 而且最终我们的交易将获利结束 (图表 B):

图示. 19. 该策略的行为依据是否使用真实的、或虚拟的停止订单而有很大不同。

图示. 19. 该策略的行为依据是否使用真实的、或虚拟的停止订单而有很大不同。

此处显示的价格尖刺是比较小的。但有时, 它甚至可能达到期货合约的价格限制。价格限制通常处于距当前价格 5% 的位置。所以, 如果我们使用 1:1 的杠杆, 我们的风险是止损执行期间本金亏损 5%。如果我们使用 1:10 的杠杆, 损失将是本金的 50% !


3.2. 基于移动均线及每周期一次交易逻辑的 EA 例程

利用两条均线交叉操作的 EA 例程是个好的例子, 令您可以制作每周期一次检查市场条件的 EA。最后的均线值 (MA) 跟随最后一根柱线的收盘价变化而持续变化。

依据两条均线的经典策略为众多交易者所熟知。当快速 MA 上穿慢速 MA 时 EA 买入, 当快速 MA 下穿慢速 MA 时卖出。下图描述了此策略的多头和空头入场信号:

图示. 20. 均线策略的多头和空头入场信号

图示. 20. 均线策略的多头和空头入场信号

正如已经提到的, 最后一根柱线的 MA 是不断变化的。在此情况下, 一条快速 MA 在一根柱线周期内反复穿越慢速 MA 若干次, 使得 EA 多次翻转执行, 而价格几乎立定不动。我们也已经知道, 在期货市场, 不必在每笔分时价来临时检查市场情况是合理的。因此, 我们制作的 EA 每分钟检查一次交易条件。在这种情况下, EA 检查前一根 (已完整) 柱线而非当前柱线, 这样最后一根柱线上的均线重绘不影响 EA 的行为。

均线 EA 的代码如下所示:

//+------------------------------------------------------------------+
//|                                                MovingAverage.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Trade\Trade.mqh>

input int FastMAPeriod = 10;     // 快速 MA 周期
input int SlowMAPeriod = 20;     // 慢速 MA 周期
input double Volume = 1.0;       // 交易量
int FastMA = INVALID_HANDLE;     // 快速 MA 指标的句柄。
int SlowMA = INVALID_HANDLE;     // 慢速 MA 指标的句柄。
datetime TimeLastBar;
CTrade Trade;
//+------------------------------------------------------------------+
//| 程序初始化函数                                                     |
//+------------------------------------------------------------------+
int OnInit()
  {
   FastMA = iMA(Symbol(), Period(), FastMAPeriod, MODE_SMA, 1, PRICE_CLOSE);
   SlowMA = iMA(Symbol(), Period(), SlowMAPeriod, MODE_SMA, 1, PRICE_CLOSE);
   if(FastMA==POINTER_INVALID || SlowMA==POINTER_INVALID)
     {
      printf("指标句柄未能创建");
      return(INIT_FAILED);
     }
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 程序逆初函数                                                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(FastMA);
   IndicatorRelease(SlowMA);
  }
//+------------------------------------------------------------------+
//| 程序分时价函数                                                     |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(!NewBarDetect())return;
   if(CrossOver())
      Trade.Buy(GetVolume());
   else if(CrossUnder())
      Trade.Sell(GetVolume());
  }
//+------------------------------------------------------------------+
//| 返回 true 如果快速 ma 穿越慢速 ma 向上。否则返回                      |
//| false。                                                          |
//+------------------------------------------------------------------+
bool CrossOver()
  {
   double fast_ma[];
   double slow_ma[];
   if(CopyBuffer(FastMA, 0, 1, 2, fast_ma) < 1)return false;
   if(CopyBuffer(SlowMA, 0, 1, 2, slow_ma) < 1)return false;
   bool is_over=fast_ma[1]>slow_ma[1] && fast_ma[0]<slow_ma[0];
   return is_over;
  }
//+------------------------------------------------------------------+
//| 返回 true 如果快速 ma 穿越慢速 ma 向下。否则返回                      |
//| false。                                                          |
//+------------------------------------------------------------------+
bool CrossUnder()
  {
   double fast_ma[];
   double slow_ma[];
   if(CopyBuffer(FastMA, 0, 1, 2, fast_ma) < 1)return false;
   if(CopyBuffer(SlowMA, 0, 1, 2, slow_ma) < 1)return false;
   bool is_under=fast_ma[0]>slow_ma[0] && fast_ma[1]<slow_ma[1];
   return is_under;
  }
//+------------------------------------------------------------------+
//| 返回交易量计数。                                                   |
//+------------------------------------------------------------------+
double GetVolume()
  {
   if(PositionSelect(Symbol()))return Volume*2.0;
   return Volume;
  }
//+------------------------------------------------------------------+
//| 返回 true 如果检测到新柱线, 负责返回 false。                         |
//+------------------------------------------------------------------+
bool NewBarDetect()
  {
   datetime times[];
   if(CopyTime(Symbol(),Period(),0,1,times)<1)
      return false;
   if(times[0] == TimeLastBar)return false;
   TimeLastBar = times[0];
   return true;
  }
//+------------------------------------------------------------------+

 主要的 EA 功能是检查新柱线的来临:

void OnTick()
{
   if(!NewBarDetect())return;
   ...
}

当前 EA 版本没有止损。不过, 如果使用它, 检查存在仓位的止损依然将位于新柱线临来检查功能之后, 因此止损仅在新柱线出现之后才被触发。

这令我们仅在新柱线开盘之后再分析市场条件, 从而避免潜在的价格尖刺。当然, 价格尖刺可能恰好发生在新柱线抵达时刻, 但与每笔分时价都检查市场条件相比低了数百次。


3.3. 使用完整的柱线模式替代分时价来测试 EA

最后, 让我们来研究 MetaTrader 5 策略测试器中提供的很有趣的一种 EA 和指标测试模式。这种模式称为 "仅用开盘价". 启动策略测试器 (查看 --> 策略测试器) 并在测试器窗口的执行部分选择它。

图示. 21. 选择 "仅用开盘价" 模式

图示. 21. 选择 "仅用开盘价" 模式

交易者通常会低估这种模式, 认为它太不准确了。此外, 只有极少数的 EA 可以有效地使用此模式。然而, 很少有人知道, 其实, 它才是最精确和快速的测试模式, 尤其是相对于那个 "每笔分时价" 。

只有使用开盘价才能实现高精准。所有新柱线的价格在它完整后才可靠, 因而用历史报价中的前一根柱线。

与之对比, "每笔分时价" 模式以特殊方式从可用的较小时间帧获取数据, 并使用分时价发生器形成每根柱线。因为 MetaTrader 5 不保存分时价历史, 生成器不能模拟一分钟柱线内的价格缺口。所以, 有可能开发出一款 "圣杯", 即在测试器里展现优异结果, 但在实盘中一败涂地。

每笔分时价测试模式是基于价位突破策略的最大凶险。此外, 挂单也会扭曲真实的结果。让我们考虑一个策略, 放置一笔高位卖挂单, 并等待一个强劲的上涨行情。在 2015 年 5 月 25 日19:00 (恰好在晚间清算之后), SBRF-6.15 期货合约在一分钟内从 7 473 升至 7 530 卢比。如果我们在 7485 有一笔挂单, 它会在策略测试器里按照指定价格被触发, 并在若干根柱线之后获利平仓。

图示. 22. 挂单激活

然而, 实际情况截然不同。我们对一分钟柱线内的价格一无所知。换言之, 订单可能会在极差的价位执行。以下视频示意订单是如何在 "每笔分时价" 模式里执行的:


正如我们所看到的, 策略测试器在处理我们指定的价格时没有问题。但是让我们看看分钟蜡烛的分时图表:

图示. 23. 分钟蜡烛的分时图表

在此分钟内价格经历了颇具戏剧性的变化。分时价图表具有较大的价格差距和剧烈运动。因此, 我们的停止单在真实市场条件下很难在理想的价位执行。实际执行价格将很可能在 7 510 - 7 520 范围。


分析每笔分时价并使用市场价格来替代挂单将没有区别。因为策略测试器的分时价发生器按顺序产生分时价, 我们的订单将在卖价抵达我们指定的价位时第一时间触发。在现实中, 我们的订单在指定价格成交这本是不可能实现的。

所以, 使用 "每笔分时价" 模式要谨慎。您应该了解您的策略是否对价格尖刺特别敏感。

完整的柱线测试模式则更为安全。我们不应在此模式下使用挂单, 以确保高精准。如果我们的策略要求在 7495 入场, 它应在每根柱线开盘时检查开盘价, 等到超过必要价位时在当前价格开仓。在完整柱线模式, 我们会发现, 只有一根在 19:01 时开盘的新柱线开盘价高于期望价格, 因为19:00 那根柱线的开盘价仍低于 7495 卢布。因此, 在完整柱线模式下, 我们的交易看起来如下所示:

 

图示. 24. 在完整柱线模式中的实际交易

虽然最终的结果仍然是负值, 但它有一个巨大的优势:

按照完整柱线测试, 能确保所有交易都按实际价格执行。因此, 该模式可用于低流动性市场的策略测试。

如果您的策略工作在高于 M1 的时间帧, 且不能每个周期检查一次交易条件, 尝试 "1 分钟 OHLC" 测试模式。在此模式下, 每根柱线仅基于 M1 图表价格产生。由于所有的 M1 图表价格均为历史, 这种模式也具有绝对的精确度, 并且可以推荐作为中线策略的合理测试模式。

对于低流动性市场, 我不建议在测试策略时, 采用"每笔分时价" 模式。此外, 这种策略不该用于激活停止单。

您可能会说, 入场精确性对于交易系统是至关重要的, 甚至几个点都可能显著影响最终的结果。然而, 如果我们采取大数法则, 我们会看到理论和实际入场点之间的差异纯粹是噪音分量。在很多情况下, 入场价比理论计算值都要差, 尽管有时, 极端价格突破计算值后在同一根柱线内又折返回来。

如果我们使用 "每笔分时价" 模式, 我们将在此刻遭遇损失。但是, 如果我们使用完整柱线, 这种条件下, 我们还没入场。换言之, 在更差的价位入场将由其它 (正面) 影响进行补偿。通常情况下, 差价会被完全消除, 其结果将完全依赖于实现的策略, 而不在于入场时刻的价位。


结论

现在, 是时候来总结主要思路:

  • 市场价格具有离散性。价格由多笔成交组成, 形成的市场图表掩盖了离散性。当分析价格柱线时, 我们无法可靠地判断价格发生了什么, 直到其完成。在最初的近似里, 假设柱线的流动性是无限的, 且每根柱线的范围是均匀分布的, 每个价位都能反映出一笔真实成交。

  • 在低流动性的市场, 市场价格的离散化可以非常高。因此, 建议使用限价单入场和离场。限价单让我们克服市场价格的离散性, 避免过多的滑点。

  • MetaTrader 5 的特别功能高位限价买和低位限价卖订单可以替代标准止损价位。停止限价订单是安全的, 可被用来管理最大点差, 即使终端没有运行。

  • 除了它们的离散性, 市场价格有一定的流动性, 有时必须考虑在内。当前的流动性影响滑点值。了解市场深度的状态, 我们可以计算潜在的滑点。

  • 管理当前点差是一个较简单的方法来评估潜在的流动性, 而无需使用市场深度。一个点差级别通常依赖于品种的流动性。如果点差过大, 等待更好时机进行交易比较合理。

  • "休眠" 模式令您的 EA 能可靠对抗尖刺。在此模式下, EA 的交易逻辑是当一个新柱线抵达时检查一次。此外, 在此模式开发的 EA 可兼容于完整柱线模式测试。

  • 完整柱线模式是最准确的测试模式, 因为在它工作时可适用于离散和实际的历史数据。另外, 此模式具有较高的测试速度。完整柱线和 "1 分钟 OHLC" 是用于低流动性市场的唯一合理的测试模式。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/1683

附加的文件 |
MarketBook.mqh (10.83 KB)
PanelSL.mqh (12.59 KB)
MovingAverage.mq5 (3.99 KB)
Spread_Record.mq5 (4.38 KB)
最近评论 | 前往讨论 (1)
Little---Prince
Little---Prince | 14 12月 2015 在 08:03

KVB昆仑国际可交易股指沪深300,高返佣。并且能参加IPHONE手机电脑活动。有需要可加QQ177891875

 

外汇黄金最高杠杆200倍,代理还有赠金活动

使用 MQL5 绘制阻力和支撑级别 使用 MQL5 绘制阻力和支撑级别
本文介绍一种查找四个极点并在此基础上绘制支撑和阻力级别的方法。为了在当前货币对的图表上查找极点, 使用 RSI 指标。作为例子, 我们提供了一款指标的代码显示支撑和阻力级别。
MQL5秘笈之:采用关联数组或字典实现快速数据访问 MQL5秘笈之:采用关联数组或字典实现快速数据访问
本文介绍一种能够通过key来访问元素的特殊算法。任何基本数据类型都可以被当作key。例如它可以是一个字符串或一个整型值。这样的数据容器通常被称为字典或这关联数组。这为解决问题提供了便捷。
怎样开发可以获利的交易策略 怎样开发可以获利的交易策略
本文为这样的问题提供解答: "是否可以通过神经网络技术,基于历史数据来构建自动交易策略?".
在 GUI 控件中使用布局和容器: CBox 类 在 GUI 控件中使用布局和容器: CBox 类
本文介绍一种基于布局和容器来创建 GUI (图形用户界面) 的替代方法, 使用一个布局管理器 — CBox 类。类 CBox class 是一个辅助控件, 在 GUI 面板里充当一个基本控件的容器。它可令图形面板设计更加简便, 并且在某些场合, 减少编写代码时间。