
改编版 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




在事情进一步发展之前,我赞成你的观点。
套期保值与反向订单完全无关。它与相关市场和衍生品有关。欧元兑美元的交易可以用欧元债券、国债、其期货和期权进行对冲。可以同时对冲,也可以单独对冲。但欧元兑美元的对冲交易不行。反向交易是一种锁定,是对交易商/经纪人的自愿馈赠。前提是您是他的亲戚,而且今天是他的生日。
要检查该策略/原则 "是否愚蠢或一厢情愿",您需要看看在净额结算会计和多货币篮子中一切看起来如何。
好吧,我承认我的错误,但我只是想与读者沟通。我做过很多自由职业者的工作,没有人,绝对没有人称这种策略为 "雪崩 "或 "翻转交易系统"。每个人都把这种策略称为对冲策略。因此,我认为对冲策略能更好地将我与读者联系起来。
因此,从技术上讲,我可能是错的,但我相信我已经实现了与读者建立联系的目标。另外,由于这个术语在整个系列中一直被使用,因此要在所有情况下都对其进行修改是很有挑战性的。
此外,许多读者亲自与我联系,提出他们的疑问,从他们的问题来看,我收到的总体反馈表明,读者熟悉并理解所描述的策略。因此,我认为保留现有术语更有利于确保一致性和所有读者的理解。
无论如何,非常感谢你让我知道我的技术性错误。
好吧,我承认我错了,但我只是想和读者们交流一下。我做过很多自由职业者,没有人,绝对没有人把这个系统称为雪崩或翻转交易系统。每个人都称这个策略为对冲策略。因此,我认为对冲策略能更好地将我与读者联系起来。
所以,是的,从技术上讲,我可能错了,但我相信我实现了与读者建立联系的目标。此外,由于这个术语在整个系列中使用一致,因此很难在所有情况下都进行修改。
此外,许多读者亲自向我提出了他们关心的问题,从他们提出的问题来看,我收到的普遍反馈表明,读者熟悉并理解所描述的策略。因此,我认为保留现有术语更有利于确保一致性和所有读者的理解。
无论如何,非常感谢你指出我的技术性错误。
نعم إنه كذلك.ولكن هذا ليس سوى الجزء الأول، والفكرة الرئيسية هي متعددة العدد في هذا، والتي يتم تغطيتها في المزيد من المقالات وما يؤثر على التقدم.
谢谢