改编版 MQL5 网格对冲 EA(第 1 部分):制作一个简单的对冲 EA
概述
您是不是正在深入了解智能系统(EA)的交易世界,但一直遇到这句话 — “不要使用危险的对冲/网格/马丁格尔”?您也许好奇这些策略有什么大惊小怪的。为什么人们一直说它们有风险,这些说法背后的真正含义是什么?您甚至也许在想,“嘿,我们能否调整这些策略,令它们更安全吗?”另外,为什么交易者首先要为这些策略而烦恼呢?它们有什么好处和坏处?如果您脑海中闪过这些念头,那么您来对地方了。您对答案的搜索即将结束。 我们将从创建一个简单的对冲智能系统开始。可以将其视为迈向我们更大项目的第一步 — 网格对冲智能系统。这将是经典网格和对冲策略的酷炫组合。在本文结束时,您将知晓如何制定基本的对冲策略,并知晓该策略是否像某些人所说的那样有利可图。 但我们不会就此止步。在本系列中,我们将从内到外探讨这些策略。我们将探求哪些有效、哪些无效、以及如何混合搭配,从而令一些事物变得更好。我们的目标?为了看看我们是否可以采用这些传统策略,赋予它们一个全新的转折,并利用它们进行自动交易,并赚取一些可观的利润。所以,跟紧我,我们一起找出答案!以下快速概述是我们将在本文中解决的问题:
讨论经典对冲策略
首先也是最重要的,我们应在深入之前讨论该策略。
首先,我们开立多头持仓,简单讲,在 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;
- isPositionBuy 是布尔变量,它将决定接下来放置哪种持仓类型,即多头持仓或空头持仓。如果其为 true,则下一个持仓类型将为多头,否则是空头。
- buyTP 是 A 和 B 之间的距离,即多头持仓的止盈(以点为单位),其中 A、B 将在后面定义。
- sellTP 是 C 和 D 之间的距离,即空头持仓的止盈(以点为单位),其中 C、D 将在后面定义。
- buySellDiff 是 B 和 C 之间的距离,即多头价位和空头价位(以点为单位)。
- intialLotSize 是第一笔开仓的手数。
- lotSizeMultiplier 是后续开仓手数的倍数。
注意:这些变量稍后将用于优化策略。
例如,我们将 buyTP、sellTP 和 buySellDiff 设为等于 15 个点,但我们稍后会修改这些数值,看看哪些数值能给我们带来最优盈利和回撤。
这些是稍后将用于优化的输入变量。
现在,我们在全局空间中创建更多变量:
double A, B, C, D; bool isPositionBuy; bool hedgeCycleRunning = false; double lastPositionLotSize;
- 我们首先定义了 4 个价位,分别命名为 A、B、C、D 作为双精度变量:
- A:这代表了所有多头持仓的止盈价位。
- B:这代表了所有多头持仓的开仓价位,和所有空头持仓的止损价位。
- C:这代表所有空头持仓的开仓价位,和所有多头持仓的止损价位。
- D:这代表了所有空头持仓的止盈价位。
- isPositionBuy:这是一个布尔变量,可以接受 2 个值,true 和 false,其中 true 表示初始持仓为多头,false 表示初始持仓为空头。
- hedgeCycleRunning:这是一个布尔值变量,它也可以采用 2 个值,true 和 false,其中 true 表示正在运行一个对冲周期,即初始订单已开立,但我们上面定义的 A 或 D 价位尚未触及,而 false 表示价位已触及 A 或 D,然后将开始新的周期,我们会在后面看到。还有,默认情况下,该变量为 false。
- 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 变量,使用三元运算符,如下所示:
- 如果 isPositionBuy 为 True:
- A 等于 initialPrice 和 buyTP(输入变量)的总和,其中 buyTP 乘以因子(_Point*10),其中 _Point 实际上是预定义的函数 “Point()”。
- B 等于 initialPrice
- C 等于 initialPrice 减去 buySellDiff(输入变量),其中 buySellDiff 乘以因子(_Point*10)。
- D 等于 initialPrice 减去 buySellDiff 和 sellTP 之和,再乘以(_Point*10) 的因子。
- 如果 isPositionBuy 为 False:
- A 等于 initialPrice 和(buySellDiff + buyTP) 之和,后者乘以(_Point*10)的因子。
- B 等于 initialPrice 和 buySellDiff 的合计,其中 buySellDiff 乘以(_Point*10)的因子。
- C 等于 initialPrice
- D 等于 initialPrice 减去 sellTP,其中 sellTP 乘以(_Point*10)的因子。
现在,为了可视化,我们调用 ObjectCreate 在图表上绘制一些线条,表示 A、B、C、D 价位,并调用 ObjectSetInteger 将其颜色属性设置为 clrGreen(您也可以取任何其它颜色)。
现在我们需要开立初始订单,该订单可以是多头或空头,具体取决于变量 isPositionBuy。 现在为了做到这一点,我们定义了三个变量:positionType、SL、TP。
- 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,我们只需要这两者。我们将标识符而非整数值,因为它们很难记住。
-
SL:如果 isPositionBuy 为 true,则 SL 等于价位 C,否则等于 B
-
TP:如果 isPositionBuy 为true,则 TP 等于价格水平 A,否则等于 D
使用这 3 个变量,我们需要放置一笔持仓,如下所示:
首先,我们使用 #include 导入标准交易库:
#include <Trade/Trade.mqh>
现在,就在开仓之前,我们按以下方法创建 CTrade 类的实例:
CTrade trade;
trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);
使用该实例,我们在该实例中调用 PositionOpen 函数放置一笔持仓,其中包含以下参数:
- _Symbol 给出了智能系统所附加于的当前品种。
- positionType 是我们之前定义的 ENUM_ORDER_TYPE 变量。
- 初始手数取自输入变量。
- initialPrice 是订单开仓价,即要价(Ask)(针对多头持仓),或出价(Bid)(针对空头持仓)。
- 最后,我们提供 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() 函数的作用,但总的来说,它执行以下操作:
- 定义 A、B、C、D 价位。
- 在 A、B、C、D 价位上绘制水平绿线以便可视化。
- 开仓。
- 将此仓位的手数存储在全局空间中定义的 lastPositionLotSize 变量当中,以便它可以在任何地方使用。
- 将 hedgeCycleRunning 设置为 true,因为它之前是 false,这正是我们执行 StartHedgeCycle() 函数的原因。
- 最后,按照我们的策略所述,将 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 语句处理在周期之间(在放置初始持仓、和尚未结束的周期之间)的开单。
- 第一个 IF 语句:处理开立做空单。如果 Bid 变量(包含该时间点的出价)低于或等于 C 价位,并且 isPositionBuy 为 false,则我们定义一个名为 newPositionLotSize 的双精度变量。该数值设置为等于 lastPositionLotSize 乘以 lotSizeMultiplier,然后调用名为 NormalizeDouble 的预定义函数将双精度值常规化到小数点后 2 位
。
然后,我们调用 CTrade 的实例 trade 中的预定义函数 PositionOpen() 放置空头持仓,并以 newPositionLotSize 作为参数。最后,我们将 lastPositionLotSize 设置为这个新的手数(无需常规化),如此我们可以将其作为后续持仓的乘数,最后,我们将 isPositionBuy 从 true 切换到 false、或从 false 到 true。 - 第二个 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; } //+------------------------------------------------------------------+
关于如何将我们的经典对冲策略自动化的讨论到此结束。
回溯测试我们的经典对冲策略
现在我们已经创建了智能交易系统来自动遵循我们的策略,那么测试它,并查看结果是合乎逻辑的。
我将使用以下输入参数来测试我们的策略:
- initialBuyPosition: true
- buyTP: 15
- sellTP: 15
- buySellDiff: 15
- initialLotSize: 0.01
- 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) |
我们来理解这些术语:
- 余额回撤绝对值:这是初始资本(在我们的例子中为 $10,000)减去最低余额之间的差额,即最低余额(低谷余额)。
- 余额回撤最大值:这是最高余额点(峰值余额)减去最低余额点(低谷余额)之间的差额。
- 余额回撤相对值:这是余额回撤最大值相较于余额最高点(峰值余额)的百分比。
净值定义是对称的:
- 净值绝对回撤:这是初始资本(在我们的例子中为 $10,000)减去最低净值(即净值最低点)之间的差额。
- 净值回撤最大值:这是净值最高点(峰值净值)减去净值最低点(低谷净值)之间的差额。
- 净值回撤相对值:这是净值回撤最大值相较于净值最高点(峰值净值)的百分比。
以下是所有 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