English Русский Español Deutsch 日本語 Português
preview
改编版 MQL5 网格对冲 EA(第 1 部分):制作一个简单的对冲 EA

改编版 MQL5 网格对冲 EA(第 1 部分):制作一个简单的对冲 EA

MetaTrader 5交易系统 | 5 八月 2024, 11:08
375 0
Kailash Bai Mina
Kailash Bai Mina

概述

您是不是正在深入了解智能系统(EA)的交易世界,但一直遇到这句话 — “不要使用危险的对冲/网格/马丁格尔”?您也许好奇这些策略有什么大惊小怪的。为什么人们一直说它们有风险,这些说法背后的真正含义是什么?您甚至也许在想,“嘿,我们能否调整这些策略,令它们更安全吗?”另外,为什么交易者首先要为这些策略而烦恼呢?它们有什么好处和坏处?如果您脑海中闪过这些念头,那么您来对地方了。您对答案的搜索即将结束。

我们将从创建一个简单的对冲智能系统开始。可以将其视为迈向我们更大项目的第一步 — 网格对冲智能系统。这将是经典网格和对冲策略的酷炫组合。在本文结束时,您将知晓如何制定基本的对冲策略,并知晓该策略是否像某些人所说的那样有利可图。

但我们不会就此止步。在本系列中,我们将从内到外探讨这些策略。我们将探求哪些有效、哪些无效、以及如何混合搭配,从而令一些事物变得更好。我们的目标?为了看看我们是否可以采用这些传统策略,赋予它们一个全新的转折,并利用它们进行自动交易,并赚取一些可观的利润。所以,跟紧我,我们一起找出答案!

以下快速概述是我们将在本文中解决的问题:

  1. 讨论经典对冲策略
  2. 经典对冲策略的自动化
  3. 回溯测试我们的经典对冲策略
  4. 结束语


讨论经典对冲策略

首先也是最重要的,我们应在深入之前讨论该策略。

首先,我们开立多头持仓,简单讲,在 1000 价位,并在 950 处设置止损,在 1050 处设止盈。也就是说,如果我们触及止损,我们将损失 $50,而如果我们触及止盈,则我们将赚取 $50。现在我们触及止盈,策略到此结束,我们带着盈利回家。但如果它触及止损,我们将损失 $50。现在我们要做的是立即在 950 放置空头持仓,并在 900 设置止盈,在 1000 设置止损。如果这笔新的空头持仓触及止盈,...我们赚取了 $50,但问题是我们之前已经损失了 $50,所以我们的净收益为 $0,如果它触及止损,即价位 1000,则我们再次亏损 $50,因此我们的总亏损为 $100,但在目前这一点上,我们再次放置一笔多头持仓,其止盈和止损与之前的多头持仓相同。如果这笔新的多头持仓触及目标价,我们赚取 $50,我们的净收益总额为 -$50 - $50 + $50 = -$50,即亏损 $50,如果触及止损,我们的总亏损为 -$50 - $50 - $50 = -$150,即总亏损 $150。

为简单起见,我们暂时忽略点差和佣金。

现在您也许会想,“这里发生了什么,我们将如何像这样获利 100%?“但您错过了一件主要的事情:手数。如果我们增加连续持仓的手数会怎样?所以现在,我们回顾一下我们的策略。

我们在 1000 开立 0.01 手(可能的最低手数)的多头持仓:

  • 如果我们触及止盈 (1050),我们带着 $50的盈利回家,且策略到此结束。
  • 如果我们触及止损(950),我们将损失 $50。

如果我们触及止损,那么策略就不会按照上述规则结束。根据我们的策略,我们将立即在 950 处开立 0.02(翻倍)手数的空头持仓:

  • 如果我们触及止盈(900),则我们将赚取总净利润 -$50 + $100 = $50,策略到此结束。
  • 如果我们触及止损(1000),我们将总共亏损 $50 + $100 = $150。

如果我们触及止损,那么策略就不会按照上述规则结束。根据我们的策略,我们将立即在 1000 处开立 0.04(再次翻倍)手数的多头持仓:

  • 如果我们触及止盈(1050),我们将赚取总净利润 -$50 - $100 + $200 = $50,策略到此结束。
  • 如果我们触及止损(1000),我们将总共亏损 $50 + $100 = $150。

如果我们触及止损,那么策略就不会按照上述规则结束。根据我们的策略,我们将立即在 950 处开立 0.08(再次翻倍)手数的空头持仓:

  • 如果我们触及止盈(1050),我们将赚取总净利润 -$50 - $100 + $200 = $50,策略到此结束。
  • 如果我们触及止损(1000),我们将总共亏损 $50 + $100 = $150。

... 

您也许已经注意到,任何情况下,当策略结束时,我们将赚取 $50的盈利。如果没有,则策略就会继续。该策略将一直持续到我们在 900 或 1050 触及止盈;价格最终将触及这两个点之一,我们肯定会保证赚取 $50的利润。

在上述情况下,我们首先放置多头持仓,但并非必须从多头持仓开始。替代方案,我们的策略可以从 0.01 的空头持仓开始(在我们的例子中)。

实际上,这种从空头持仓开始的替代方案非常重要,因为我们稍后将修改策略,以便获得尽可能多的灵活性,例如,我们可能需要为上述周期定义一个入场点(在我们的例子中为初始买入),但将该入场点限制为仅多头持仓会有问题,因为我们也许会定义入场点,而其会因最初放置空头持仓受益。

从空头持仓开始的策略将与上述从多头持仓开始的情况完全对称。为了更清楚地解释这一点,我们的策略将如下所示:

  • 如果我们触及止盈(900),我们带着 $50的盈利回家,策略到此结束。
  • 如果我们触及止损(1000),我们将亏损 $50。

如果我们触及止损,那么策略就不会按照上述规则结束。根据我们的策略,我们将立即在 1000 处开立 0.02(翻倍)手数的多头持仓:

  • 如果我们触及止盈(1050),我们将赚取总净利润 -$50 + $100 = $50,策略到此结束。
  • 如果我们触及止损(950),我们将总共亏损 $50 + $100 = $150。

如果我们触及止损,那么策略就不会按照上述规则结束。根据我们的策略,我们将立即在 950 处开立 0.04(再次翻倍)手数的空头持仓:

  • 如果我们触及止盈 (900),我们将赚取总净利润 -$50 - $100 + $200 = $50,策略到此结束。
  • 如果我们触及止损(1000),我们将总共亏损 $50 + $100 + $200 = $350。

... 等等。

再次,正如我们所见,只有当我们触及 900 或 1050 价位时,该策略才会结束,从而肯定会赚取 $50 的盈利。如果我们没有触及这些价位,该策略将继续下去,直到我们最终达到它们。

注意:将手数增加 2 倍不是强制性的。我们可以按任何系数增加它,尽管任何小于 2 的倍数都不能保证上述两种情况的盈利。为了简单起见,我们选择了 2,在以后优化策略时可能会改变这一点。

如此,我们有关经典对冲策略的讨论到此结束。


我们的经典对冲策略的自动化

首先,我们需要讨论如何继续创建智能系统的计划。实际上,有很多方式可以做到这一点。主要有两种方式:

  • 方式 #1:按照策略的规定定义四个价位(变量),并在价格这些价位时开仓,正如我们的策略所规定的那样。
  • 方式 #2:使用挂单,并检测挂单何时执行,并在发生这种情况时放置进一步的挂单。

    这两种方式几乎是等同的,哪一种稍微好一点是值得商榷的,但我将只讨论方式 #1,因为它更容易编码和理解。


    经典对冲策略的自动化

    首先,我们将在全局空间中声明几个变量:

    input bool initialPositionBuy = true;
    input double buyTP = 15;
    input double sellTP = 15;
    input double buySellDiff = 15;
    input double initialLotSize = 0.01;
    input double lotSizeMultiplier = 2;

    1. isPositionBuy 是布尔变量,它将决定接下来放置哪种持仓类型,即多头持仓或空头持仓。如果其为 true,则下一个持仓类型将为多头,否则是空头。
    2. buyTP 是 A 和 B 之间的距离,即多头持仓的止盈(以点为单位),其中 A、B 将在后面定义。
    3. sellTP 是 C 和 D 之间的距离,即空头持仓的止盈(以点为单位),其中 C、D 将在后面定义。
    4. buySellDiff 是 B 和 C 之间的距离,即多头价位和空头价位(以点为单位)。
    5. intialLotSize 是第一笔开仓的手数。
    6. lotSizeMultiplier 是后续开仓手数的倍数。
    A、B、C、D 基本上是价位,从上到下依次递减。

    注意:这些变量稍后将用于优化策略。

    例如,我们将 buyTP、sellTP 和 buySellDiff 设为等于 15 个点,但我们稍后会修改这些数值,看看哪些数值能给我们带来最优盈利和回撤。

    这些是稍后将用于优化的输入变量。

    现在,我们在全局空间中创建更多变量:

    double A, B, C, D;
    bool isPositionBuy;
    bool hedgeCycleRunning = false;
    double lastPositionLotSize;

    1. 我们首先定义了 4 个价位,分别命名为 A、B、C、D 作为双精度变量:
      • A:这代表了所有多头持仓的止盈价位。
      • B:这代表了所有多头持仓的开仓价位,和所有空头持仓的止损价位。
      • C:这代表所有空头持仓的开仓价位,和所有多头持仓的止损价位。
      • D:这代表了所有空头持仓的止盈价位。
    2. isPositionBuy:这是一个布尔变量,可以接受 2 个值,true 和 false,其中 true 表示初始持仓为多头,false 表示初始持仓为空头。
    3. hedgeCycleRunning:这是一个布尔值变量,它也可以采用 2 个值,true 和 false,其中 true 表示正在运行一个对冲周期,即初始订单已开立,但我们上面定义的 A 或 D 价位尚未触及,而 false 表示价位已触及 A 或 D,然后将开始新的周期,我们会在后面看到。还有,默认情况下,该变量为 false。
    4. lastPositionLotSize:顾名思义,这个双精度类型变量始终包含最后开单的手数,如果周期尚未开始,它de1取值等于我们稍后将设置的输入变量 initialLotSize

    现在,我们创建以下函数: 
    //+------------------------------------------------------------------+
    //| Hedge Cycle Intialization Function                               |
    //+------------------------------------------------------------------+
    void StartHedgeCycle()
       {
        isPositionBuy = initialPositionBuy;
        double initialPrice = isPositionBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        A = isPositionBuy ? initialPrice + buyTP * _Point * 10 : initialPrice + (buySellDiff + buyTP) * _Point * 10;
        B = isPositionBuy ? initialPrice : initialPrice + buySellDiff * _Point * 10;
        C = isPositionBuy ? initialPrice - buySellDiff * _Point * 10 : initialPrice;
        D = isPositionBuy ? initialPrice - (buySellDiff + sellTP) * _Point * 10 : initialPrice - sellTP * _Point * 10;
    
        ObjectCreate(0, "A", OBJ_HLINE, 0, 0, A);
        ObjectSetInteger(0, "A", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "B", OBJ_HLINE, 0, 0, B);
        ObjectSetInteger(0, "B", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "C", OBJ_HLINE, 0, 0, C);
        ObjectSetInteger(0, "C", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "D", OBJ_HLINE, 0, 0, D);
        ObjectSetInteger(0, "D", OBJPROP_COLOR, clrGreen);
    
        ENUM_ORDER_TYPE positionType = isPositionBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
        double SL = isPositionBuy ? C : B;
        double TP = isPositionBuy ? A : D;
        CTrade trade;
        trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);
        
        lastPositionLotSize = initialLotSize;
        if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
        isPositionBuy = isPositionBuy ? false : true;
       }
    //+------------------------------------------------------------------+

    函数类型是 void,这意味着我们不需要它返回任何内容。该函数的工作原理如下:

    首先,我们将 isPositionBuy 变量(bool) 设置为等于输入变量 initialPositionBuy,这将告诉我们在每个周期开始时要放置哪种持仓类型。您也许好奇,若它们都相同,为什么我们需要两个变量,但请注意,我们将交替更改 isPositionBuy(上述代码模块的最后一行)。不过,initialPositionBuy 始终是固定的,我们不会更改它。

    然后,我们定义一个名为 initialPrice 的新变量(double 类型),我们使用三元运算符将其设置为等于要价(Ask)或出价(Bid)。如果 isPositionBuy 为 true,则 initialPrice 等于该时间点的要价(Ask),否则等于出价(Bid)。

    然后我们定义之前简要讨论的变量(double 类型),即 A、B、C、D 变量,使用三元运算符,如下所示:

    1. 如果 isPositionBuy 为 True:
      • A 等于 initialPricebuyTP(输入变量)的总和,其中 buyTP 乘以因子(_Point*10),其中 _Point 实际上是预定义的函数 “Point()”。
      • B 等于 initialPrice
      • C 等于 initialPrice 减去 buySellDiff(输入变量),其中 buySellDiff 乘以因子(_Point*10)。
      • D 等于 initialPrice 减去 buySellDiffsellTP 之和,再乘以(_Point*10) 的因子。

    2. 如果 isPositionBuy 为 False:
      • A 等于 initialPrice(buySellDiff + buyTP) 之和,后者乘以(_Point*10)的因子。
      • B 等于 initialPricebuySellDiff 的合计,其中 buySellDiff 乘以(_Point*10)的因子。
      • C 等于 initialPrice
      • D 等于 initialPrice 减去 sellTP,其中 sellTP 乘以(_Point*10)的因子。

    现在,为了可视化,我们调用 ObjectCreate 在图表上绘制一些线条,表示 A、B、C、D 价位,并调用 ObjectSetInteger 将其颜色属性设置为 clrGreen(您也可以取任何其它颜色)。

    现在我们需要开立初始订单,该订单可以是多头或空头,具体取决于变量 isPositionBuy。 现在为了做到这一点,我们定义了三个变量:positionType、SL、TP。

    1. positionType:该变量的类型为 ENUM_ORDER _TYPE,它是预定义的自定义变量类型,可以根据下表取 0 到 8 之间的整数值:

      整数值 标识符
       0 ORDER_TYPE_BUY
       1 ORDER_TYPE_SELL
       2 ORDER_TYPE_BUY_LIMIT
       3 ORDER_TYPE_SELL_LIMIT
       4 ORDER_TYPE_BUY_STOP
       5 ORDER_TYPE_SELL_STOP
       6 ORDER_TYPE_BUY_STOP_LIMIT
       7 ORDER_TYPE_SELL_STOP_LIMIT
       8 ORDER_TYPE_CLOSE_BY

      如您所见,0 代表 ORDER_TYPE_BUY,1 代表 ORDER_TYPE_SELL,我们只需要这两者。我们将标识符而非整数值,因为它们很难记住。

    2. SL:如果 isPositionBuy 为 true,则 SL 等于价位 C,否则等于 B

    3. TP:如果 isPositionBuy 为true,则 TP 等于价格水平 A,否则等于 D

    使用这 3 个变量,我们需要放置一笔持仓,如下所示:

    首先,我们使用 #include 导入标准交易库:

    #include <Trade/Trade.mqh>
    

    现在,就在开仓之前,我们按以下方法创建 CTrade 类的实例:

    CTrade trade;
    
    trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);

    使用该实例,我们在该实例中调用 PositionOpen 函数放置一笔持仓,其中包含以下参数:

    1. _Symbol 给出了智能系统所附加于的当前品种。
    2. positionType 是我们之前定义的 ENUM_ORDER_TYPE 变量。
    3. 初始手数取自输入变量。
    4. initialPrice 是订单开仓价,即要价(Ask)(针对多头持仓),或出价(Bid)(针对空头持仓)。
    5. 最后,我们提供 SL 和 TP 价位。

    这样,就可放置多头或空头持仓。现在,在放置持仓后,我们将其手数存储在全局空间中定义的名为 lastPositionLotSize 的变量当中,以便我们可用该手数和输入的倍数来计算后续开仓的手数大小。

    这样一来,我们还有两件事要做:

    if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
    isPositionBuy = isPositionBuy ? false : true;

    在此,我们仅在成功放置持仓时将 hedgeCycleRunning 的值设置为 true。这是由名为 trade 的 CTrade 实例中的 ResultRetcode() 函数判定的,该函数返回 “10009” 表示放置成功(您可以在此处查看所有这些返回代码)。使用 hedgeCycleRunning 的原因将在后续的代码里进行解释。

    最后一件事是,我们使用三元运算符来交替 isPositionBuy 的值。如果它是 false,它就会变成 true,反之亦然。我们之所以这样做,是因为我们的策略指出,一旦开立初始持仓,会在做多之后做空,在做空之后做多,这意味着它会交替出现。

    我们对基本重要的函数 StartHedgeCycle() 的讨论到此结束,因为我们将一次又一次地调用该函数。

    现在,我们继续最后一段代码。

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
       {
        double _Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double _Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    
        if(!hedgeCycleRunning)
           {
            StartHedgeCycle();
           }
    
        if(_Bid <= C && !isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
            lastPositionLotSize = newPositionLotSize;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Ask >= B && isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
            lastPositionLotSize = newPositionLotSize;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Bid >= A || _Ask <= D)
           {
            hedgeCycleRunning = false;
           }
       }
    //+------------------------------------------------------------------+

    前两行自言其意,它们只是定义了在那个时间点存储要价和出价的 _Ask 和 _Bid(双精度变量)。

    然后,如果 hedgeCycleRunning 变量为 false,我们使用 if 语句调用 StartHedgeCycle() 函数开始对冲周期。我们已经知道 StartHedgeCycle() 函数的作用,但总的来说,它执行以下操作:

    1. 定义 A、B、C、D 价位。
    2. 在 A、B、C、D 价位上绘制水平绿线以便可视化。
    3. 开仓。
    4. 将此仓位的手数存储在全局空间中定义的 lastPositionLotSize 变量当中,以便它可以在任何地方使用。
    5. hedgeCycleRunning 设置为 true,因为它之前是 false,这正是我们执行 StartHedgeCycle() 函数的原因。
    6. 最后,按照我们的策略所述,将 isPositionBuy 变量从 true 切换到 false,从 false 切换到 true

    我们只执行 StartHedgeCycle() 一次,因为如果 hedgeCycleRunning 为 false,我们就会执行它,并且在函数结束时,我们将其更改为 false。因此,除非我们再次将 hedgeCycleRunning 设置为 false,否则 StartHedgeCycle() 将不会再次执行。

    我们暂时跳过接下来的两个 if 语句,稍后我们会回到它们。我们看看最终的 if 语句:

    if(_Bid >= A || _Ask <= D)
       {
        hedgeCycleRunning = false;
       }

    这将重新启动处理周期。正如我们之前所讨论的,如果我们将 hedgeCycleRunning 设置为 true,则循环将重新开始,我们之前讨论的所有内容都将再次发生。此外,我已确保当周期重新开始时,上一个周期的所有持仓都将以止盈平仓(无论是多头、还是空头持仓)。

    如此,我们处理完了周期开始、周期结束和重启,但我们仍然缺少主要部分,即当价格从下方触及 B 价位、或从上方触及 C 价位时的开单处理。开仓类型也必须是交替的,其中多头仅在 B 价位开仓,空头仅在 C 价位开仓。

    我们跳过了处理此问题的代码,那么我们回到它。

    if(_Bid <= C && !isPositionBuy)
       {
        double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
        CTrade trade;
        trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
        lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
        isPositionBuy = isPositionBuy ? false : true;
       }
    
    if(_Ask >= B && isPositionBuy)
       {
        double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
        CTrade trade;
        trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
        lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
        isPositionBuy = isPositionBuy ? false : true;
       }

     如此,这两个 if 语句处理在周期之间(在放置初始持仓、和尚未结束的周期之间)的开单。

    1. 第一个 IF 语句:处理开立做空单。如果 Bid 变量(包含该时间点的出价)低于或等于 C 价位,并且 isPositionBuy 为 false,则我们定义一个名为 newPositionLotSize 的双精度变量。该数值设置为等于 lastPositionLotSize 乘以 lotSizeMultiplier,然后调用名为 NormalizeDouble 的预定义函数将双精度值常规化到小数点后 2 位

      然后,我们调用 CTrade 的实例 trade 中的预定义函数 PositionOpen() 放置空头持仓,并以 newPositionLotSize 作为参数。最后,我们将 lastPositionLotSize 设置为这个新的手数(无需常规化),如此我们可以将其作为后续持仓的乘数,最后,我们将 isPositionBuy 从 true 切换到 false、或从 false 到 true。
    2. 第二个 IF 语句:处理开立多头订单。如果 Ask(包含该时间点的要价的变量)等于或高于 B 价位,并且 isPositionBuy 为 true,则我们定义一个名为 newPositionLotSize 的双精度变量。我们将 newPositionLotSize 设置为等于 lastPositionLotSize 乘以 lotSizeMultiplier,并像以前一样调用预定义的函数 NormalizeDouble 将双精度值常规化到小数点后两位。

      然后,我们调用 CTrade 实例 trade 中的预定义函数 PositionOpen() 放置多头持仓,并以 newPositionLotSize 作为参数。最后,我们将 lastPositionLotSize 设置为这个新的手数(无需常规化),以便我们可以将其作为后续持仓的乘数。最后,我们将 isPositionBuy 从 true 切换到 false,或从 false 到 true。

    这里有两个要点需要注意:

    • 在第一条 IF 语句中,我们使用了 “Bid”,并表示当 “Ask” 低于或等于 “C”,且 “isBuyPosition” 为 false 时开仓。为什么我们在这里使用 “Bid”?

      假设我们采用要价,那么有可能之前的多头持仓会被平仓,但新的空头持仓却未开仓。这是因为我们知道做多应取要价开仓,并以出价平仓,因此当出价从上方穿过或等于 C 价位线时,多头可能会被平仓。多头持仓应通过我们之前在开仓时设置的止损来平仓,但空头尚未开仓。如此,如果要价和出价两者都上涨,那么我们的策略就没有得到贯彻。这就是我们采用出价(Bid)替代要价(Ask)的原因。

      对称地,在第二个 IF 语句中,我们采用了要价,并指出当要价高于或等于 B,且 isBuyPosition 为 true 时,我们将开仓。我们为什么在这里采用要价?

      假设我们采用出价,那么有可能之前的空头持仓会被平仓,但新的多头持仓尚未开仓。我们知道,空头按出价开仓,并按要价平仓,这就有可能在出价从下方穿过或等于 B 价位线时平仓,从而空头持仓按我们之前在开仓时设置的止损平仓。不过,多头尚未开仓。故此,如果要价和出价两者都下跌,那么我们的策略就没有得到贯彻执行。这就是为什么我们采用要价替代出价。

      故此,关键是,如果多头/空头持仓被平仓,则必须立即开立后续的空头/多头持仓,如此才能正确遵循策略。

    • 在这两个 IF 语句中,我们都提到,在设置 lastPositionLotSize 的值时,我们将其等同于(lastPositionLotSize * lotSizeMultiplier),而非 newPositionLotSize,后者等于使用预定义的 NormalizeDouble() 函数将(lastPositionLotSize * lotSizeMultiplier)的数值常规化为最多两位小数。
      NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2)
      那么,我们为什么要这样做呢?实际上,如果我们将其等于常规化数值,我们的策略才能得到正确的遵循,举例来说,假设我们将初始手数设置为 0.01 以及乘数 1.5,那么第一笔手数当然是 0.01,接下来将是 0.01*1.5 = 0.015,现在,我们肯定不能开仓,因为经纪商不允许手数 0.015,即乘积必须是 0.01 的整数倍,而 0.015 不是,这就是为什么我们将手数常规化为小数点后 2 位,因此开仓手数仍为 0.01,现在给 lastPositionLotSize 什么值,我们有 2 个选项,要么是 0.01(0.010),要么是 0.015,假设我们选择 0.01(0.010),则下次我们建仓时,常规化之后,我们将使用 0.01*1.5 = 0.015,它会变为 0.01,这种情况会继续下去。如此,我们使用乘数 1.5,并从 0.01 手数开始,但手数并未增加,我们陷入了循环,所有仓位的手数均为 0.01,这意味着我们不能将 lastPositionLotSize 等同于 0.01(0.010),因此我们选择另一个选项 0.015,即我们选择常规化之前的数值。

      这就是为什么我们将 lastPositionLotSize 设置为等于(lastPositionLotSize * lotSizeMultiplier),而非 NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2)

    最后,我们的完整代码如下所示:

    #include <Trade/Trade.mqh>
    
    input bool initialPositionBuy = true;
    input double buyTP = 15;
    input double sellTP = 15;
    input double buySellDiff = 15;
    input double initialLotSize = 0.01;
    input double lotSizeMultiplier = 2;
    
    
    
    double A, B, C, D;
    bool isPositionBuy;
    bool hedgeCycleRunning = false;
    double lastPositionLotSize;
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
       {
        return(INIT_SUCCEEDED);
       }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
       {
        ObjectDelete(0, "A");
        ObjectDelete(0, "B");
        ObjectDelete(0, "C");
        ObjectDelete(0, "D");
       }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
       {
        double _Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double _Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    
        if(!hedgeCycleRunning)
           {
            StartHedgeCycle();
           }
    
        if(_Bid <= C && !isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            CTrade trade;
            trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
            lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Ask >= B && isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            CTrade trade;
            trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
            lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
    if(_Bid >= A || _Ask <= D)
       {
        hedgeCycleRunning = false;
       }
       }
    //+------------------------------------------------------------------+
    
    //+------------------------------------------------------------------+
    //| Hedge Cycle Intialization Function                               |
    //+------------------------------------------------------------------+
    void StartHedgeCycle()
       {
        isPositionBuy = initialPositionBuy;
        double initialPrice = isPositionBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        A = isPositionBuy ? initialPrice + buyTP * _Point * 10 : initialPrice + (buySellDiff + buyTP) * _Point * 10;
        B = isPositionBuy ? initialPrice : initialPrice + buySellDiff * _Point * 10;
        C = isPositionBuy ? initialPrice - buySellDiff * _Point * 10 : initialPrice;
        D = isPositionBuy ? initialPrice - (buySellDiff + sellTP) * _Point * 10 : initialPrice - sellTP * _Point * 10;
    
        ObjectCreate(0, "A", OBJ_HLINE, 0, 0, A);
        ObjectSetInteger(0, "A", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "B", OBJ_HLINE, 0, 0, B);
        ObjectSetInteger(0, "B", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "C", OBJ_HLINE, 0, 0, C);
        ObjectSetInteger(0, "C", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "D", OBJ_HLINE, 0, 0, D);
        ObjectSetInteger(0, "D", OBJPROP_COLOR, clrGreen);
    
        ENUM_ORDER_TYPE positionType = isPositionBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
        double SL = isPositionBuy ? C : B;
        double TP = isPositionBuy ? A : D;
        CTrade trade;
        trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);
        
        lastPositionLotSize = initialLotSize;
        if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
        isPositionBuy = isPositionBuy ? false : true;
       }
    //+------------------------------------------------------------------+
    

    关于如何将我们的经典对冲策略自动化的讨论到此结束。


    回溯测试我们的经典对冲策略

    现在我们已经创建了智能交易系统来自动遵循我们的策略,那么测试它,并查看结果是合乎逻辑的。

    我将使用以下输入参数来测试我们的策略:

    1. initialBuyPosition: true
    2. buyTP: 15
    3. sellTP: 15
    4. buySellDiff: 15
    5. initialLotSize: 0.01
    6. lotSizeMultiplier: 2.0

    我将依据 2023 年 1 月 1 日至 2023 年 12 月 6 日,杠杆 1:500,本金 $10,000 ,在 EURUSD 上进行测试,如果您想知道时间帧,它与我们的策略无关,所以我会选择任意(它根本不会影响我们的结果),我们来看看下面的结果:


    仅通过查看图形,您可能会认为这是一个有利可图的策略,但我们看一下其它数据,并讨论图形中的几个要点:

    如您所见,我们的净利润为 $1470.62,其中毛利润为 $13,153.68,毛亏损为 $11683.06。

    另外,我们看一下余额和净值回撤:

    余额回撤绝对值 $1170.10
    余额回撤最大值 $1563.12 (15.04%)
    余额回撤相对值 15.04% ($1563.13)
    净值回撤绝对值 $2388.66
    净值回撤最大值 $2781.97 (26.77%)
    净值回撤相对值 26.77% ($2781.97)

    我们来理解这些术语:

    1. 余额回撤绝对值:这是初始资本(在我们的例子中为 $10,000)减去最低余额之间的差额,即最低余额(低谷余额)。
    2. 余额回撤最大值:这是最高余额点(峰值余额)减去最低余额点(低谷余额)之间的差额。
    3. 余额回撤相对值:这是余额回撤最大值相较于余额最高点(峰值余额)的百分比。

    净值定义是对称的:

    1. 净值绝对回撤:这是初始资本(在我们的例子中为 $10,000)减去最低净值(即净值最低点)之间的差额。
    2. 净值回撤最大值:这是净值最高点(峰值净值)减去净值最低点(低谷净值)之间的差额。
    3. 净值回撤相对值:这是净值回撤最大值相较于净值最高点(峰值净值)的百分比。

    以下是所有 6 个统计数据的公式:


    分析上述数据,余额回撤将是我们最不关心的问题,因为净值回撤涵盖了这一点。从某种意义上说,我们可以说余额回撤是净值回撤的子集。此外,在遵循我们的策略时,净值回撤是我们最大的问题,因为我们在每笔订单中将手数翻倍,这导致手数呈指数级增长,这可以通过下表看到:

    开仓数量 下一笔持仓的手数(尚未开仓) 下一笔持仓所需的保证金(EURUSD) 
    0 0.01 $2.16 
    1 0.02 $4.32 
    2 0.04 $8.64
    3 0.08 $17.28
    4 0.16 $34.56
    5 0.32 $69.12
    6 0.64 $138.24
    7 1.28 $276.48
    8 2.56 $552.96
    9 5.12 $1105.92
    10 10.24 $2211.84
    11 20.48 $4423.68
    12 40.96 $8847.36
    13 80.92 $17694.72
    14 163.84 $35389.44

    在我们的探索中,我们目前正在使用 EURUSD 作为我们的交易货币对。重点要注意的是,0.01 手数大小所需的保证金为 $2.16,尽管这个数字可能会发生变化。

    随着我们深入研究,我们观察到一个值得注意的趋势:后续开仓所需的保证金呈指数级增长。举例,在第 12 笔开单之后,我们遇到了财务瓶颈。所需的保证金飙升至 $17,694.44,考虑到我们的初始投资是 $10,000,这个数字远远超出了我们的能力。这种场景甚至没有考虑到我们的止损。

    我们进一步分解它。如果我们包括止损,设置为每笔交易 15 个点,并且在前 12 笔交易中亏损,我们的累积手数将是惊人的 81.91(一连串之和:0.01+0.02+0.04+...+20.48+40.96)。这相当于总亏损 $12,286.5,使用 EURUSD 价值每 10 点 $1 计算,手数为 0.01。这是一个简单的计算:(81.91/0.01) * 1.5 = $12,286.5。亏损不仅超过了我们的初始资本,而且使得在 EURUSD 投资 $10,000的情况下,无法在一个周期内维持 12 笔持仓。

    我们来研究一个稍微不同的场景:我们能否设法用 $10,000 维持 EURUSD 的总共 11 笔持仓?

    想象一下,我们已经达到了 10 笔持仓。这意味着我们已经在 9 笔持仓上遇到了亏损,并且第 10 笔 持仓即将亏损。如果我们计划开立第 11 笔持仓,则 10 笔持仓的总手数将为 10.23,导致亏损 $1,534.5。考虑到 EURUSD 汇率和手数,这是按以前相同的方式计算的。下一笔持仓所需的保证金为 $4,423.68。将这些金额相加,我们得到 $5,958.18,远低于我们的 $10,000 阈值。因此,在总共 10 笔持仓中生存,并开立第 11 笔持仓是可行的。

    然而,问题出现了:是否有可能用相同的 $10,000 扩展到总共 12 笔持仓?

    为此,我们假设我们已经达到了 11 笔持仓的边缘。在此,我们已经在 10 笔持仓上遭受了亏损,并且第 11 笔持仓即将亏损。这 11 笔持仓的总手数为 20.47,亏损 $3,070.5。加上第 12 笔持仓所需的保证金,即 $8,847.36,我们的总支出飙升至 $11,917.86,超过了我们的初始投资。因此,很明显,在已经有 11 笔持仓的情况下开立第 12 笔持仓在财务上是站不住脚的。我们已经亏损了 $3,070.5,只剩下 $6,929.5。

    从回测统计数据中,我们观察到,即使在 EURUSD 等相对稳定的货币对上投资 $10,000,该策略也岌岌可危地接近崩盘。最大连续亏损为 10 笔,表明我们距离灾难性的第 11 笔只有几个点。如果第 11 笔持仓也触及止损,该策略将瓦解,导致重大损失。

    在我们的报告中,绝对回撤标记为 $2,388.66。如果我们达到第 11 笔持仓止损,我们的亏损将跃升到 $3,070.5。这将令我们距离完全策略失败仅还差 $681.84($3,070.5 - $2,388.66)。

    然而,到目前为止,我们忽略了一个关键因素 — 点差。这个变量会对盈利产生重大影响,正如我们报告中的两个具体实例所证明的那样,如下图所示。

    请注意图形中的红色圆圈。在这些情况下,尽管赢得了交易(获胜等同于在最后一笔交易中获得最高手数),但我们未能实现任何盈利。这种异常现象归因于点差。它的可变性令我们的策略进一步复杂化,因此有必要在本系列的下一部分进行更深入的分析。

    我们还必须考虑经典对冲策略的局限性。一个显著的缺点是,如果不及早达到止盈价位,则需要大量的持有能力来维持大量订单。只有当手数乘数为 2 或更大时,该策略才能保证盈利(如果 buyTP = sellTP = buySellDiff,忽略点差)。如果小于 2,则随着订单数量的增加,存在产生负利润的风险。我们将在即将到来的系列文章中探讨这些动态,以及如何优化经典对冲策略,从而赚取最大回报。


    结束语

    如此,我们在系列的第一部分讨论了一个相当复杂的策略,并且我们还利用 MQL5 实现了智能系统自动化。这可能是一个有利可图的策略,尽管一个人必须具有很高的持有能力才能放置更高的持仓手数,这对投资者来说并不总是可行的,而且风险也很大,因为他可能会面临巨幅的回撤。为了克服这些限制,我们必须优化策略,这将在本系列的下一部分完成。

    到目前为止,我们一直在 lotSizeMultiplier、initialLotSize、buySellDiff、sellTP、buyTP 中使用任意固定数值,但我们可以优化此策略,并找到这些输入参数的最优值,从而为我们提供最大的可能回报。此外,我们还将发现最初从做多或做空建仓开始是否得益,这也可能取决于不同的货币对和市场条件。故此,我们将在本系列的下一部分介绍很多实用的内容,敬请期待。 

    感谢您抽出宝贵时间阅读我的文章。我希望您发现它们对您的努力既有益又有帮助。如果您对希望在我的下一篇文章中看到的内容有任何想法或建议,请随时分享。

    祝您编码愉快!祝您交易愉快!


    本文由MetaQuotes Ltd译自英文
    原文地址: https://www.mql5.com/en/articles/13845

    附加的文件 |
    MQL5 简介(第 2 部分):浏览预定义变量、通用函数和控制流语句 MQL5 简介(第 2 部分):浏览预定义变量、通用函数和控制流语句
    通过我们的 MQL5 系列第二部分,开启一段启迪心灵的旅程。这些文章不仅是教程,还是通往魔法世界的大门,在那里,编程新手和魔法师将团结在一起。是什么让这段旅程变得如此神奇?我们的 MQL5 系列第二部分以令人耳目一新的简洁性脱颖而出,使复杂的概念变得通俗易懂。与我们互动,我们会回答您的问题,确保您获得丰富和个性化的学习体验。让我们建立一个社区,让理解 MQL5 成为每个人的冒险。欢迎来到魔法世界!
    开发回放系统(第 40 部分):启动第二阶段(一) 开发回放系统(第 40 部分):启动第二阶段(一)
    今天我们将讨论回放/模拟器系统的新阶段。在这个阶段,谈话才会变得真正有趣,内容也相当丰富。我强烈建议您仔细阅读本文并使用其中提供的链接。这将帮助您更好地理解内容。
    数据科学和机器学习(第 16 部分):全新面貌的决策树 数据科学和机器学习(第 16 部分):全新面貌的决策树
    在我们的数据科学和机器学习系列的最新一期中,深入到错综复杂的决策树世界。本文专为寻求策略洞察的交易者量身定制,全面回顾了决策树在分析市场趋势中所发挥的强大作用。探索这些算法树的根和分支,解锁它们的潜力,从而强化您的交易决策。加入我们,以全新的视角审视决策树,并探索它们如何在复杂的金融市场航行中成为您的盟友。
    开发回放系统(第 39 部分):铺平道路(三) 开发回放系统(第 39 部分):铺平道路(三)
    在进入开发的第二阶段之前,我们需要修正一些想法。您知道如何让 MQL5 满足您的需求吗?您是否尝试过超出文档所包含的范围?如果没有,那就做好准备吧。因为我们将做一些大多数人通常不会做的事情。