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

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

MetaTrader 5测试者 | 2 十月 2024, 10:29
1 407 0
Kailash Bai Mina
Kailash Bai Mina

概述

欢迎来到我们系列文章的第二批次,“改编版 MQL5 网格对冲 EA”。我们首先回顾一下我们在第一部分涵盖的内容。在第 I 部分,我们探讨了经典的对冲策略,使用智能交易系统(EA)实现自动化,并在策略测试器中进行测试,分析了一些初步结果。这标志着我们朝着创建改编版网格对冲 EA 的旅程迈进了第一步 — 经典对冲和网格策略的混合。

我之前已提到过,第 2 部分将侧重于优化经典对冲策略。不过,由于意外延迟,我们现在将重点转移到经典的网格策略上。

在本文中,我们将深入研究经典的网格策略,利用 MQL5 的 EA 实现自动化,并在策略测试器中进行测试,分析结果,并提取有关该策略的真知灼见。

如此,我们深入研究本文,并探索经典网格策略的细微差别。

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

  1. 讨论经典网格策略
  2. 讨论我们经典网格策略的自动化
  3. 回测经典网格策略
  4. 结束语


讨论经典网格策略 

首善之事,我们来深入研究策略本身。

我们的方式由多头持仓初启,尽管由空头持仓开始同样可行。现在,我们专注于多头场景。我们从特定的初始价格开始,为简单起见,使用小手数 — 假设 0.01,即可能的最小手数。自这一刻起,有两种可能的结局:价格也许会上涨、或下跌。如果价格上涨,我们待而获利,其程度取决于上涨的幅度。然后,一旦价格达到我们预判的止盈价位,我们就会变现这笔盈利。当价格开始下跌时,挑战就出现了 — 这就是我们经典的网格策略发挥作用的地方,它提供了一种确保盈利的保障。

如果价格下跌了一定额度,为了便于解释,我们假设 15 个点数,然后我们执行另一笔多头订单。该订单放置在初始价格减去 15 个点数,于此处,我们增加手数 — 为了简单起见,我们将其翻倍,其效果是应用手数乘数 2。在放置这笔新的多头订单后,我们再次面临两种情况:价格也许会上涨、或下跌。由于手数增加,价格上涨将导致该笔新订单盈利。甚而,这种上涨不仅带来了盈利,还减轻了我们初始手数为 0.01 多头订单的亏损。当价格达到两笔订单的加权平均价位时,该点位我们的净盈利等于零。该加权平均值的计算方式如下:

在计算中,X i 代表放置多头订单的价格,w i 表示相应的权重,由手数乘数决定。截至此刻,加权平均价位 W 可以计算为:

W = 1 ×( 初始价格)+ 2 ×(初始价格 − 15 点 ),再除以 3

此处,X1 是初始价格,X2 是初始价格减去 15 点,w1 等于 1,w2 等于 2。

净盈利达到零的点位是与计算出的加权平均价位一致时。这个概念的演示如下:

注意:为简单起见,我们暂时忽略点差。

我们简化并研究初始价格为 x,于处开立了手数为 0.01 的多头订单。如果价格随后下跌 15 点,我们在 x − 15 处放置新的多头订单,并将手数翻倍至 0.02。此时的加权平均价位计算为 ( 1 × x + 2 × ( x − 15 ) ) / 3,等于 x − 10 。这比初始价位低了 10 个点。

当价格触及 x − 15 时,初始多头订单 0.01 的亏损为 $1.5(假设 EURUSD 货币对,其中 $1 相当于 10 个点)。如果价格随后上涨到 x − 10 的加权平均水平,我们就可弥补初始订单的 $0.5 亏损。此外,我们从 0.02 的新多头订单中获利,因为价格从 x − 15 上涨了 5 个点。由于手数翻倍,我们在这里的收益也翻了一番,达到 $1。因此,我们的净盈利总额等于 $0(即 -$1.5 + $0.5 + $1)。

如果价格从 x − 10 的加权平均价进一步上涨,我们将继续赚取盈利,直到达到止盈价。

如果价格从加权平均价位继续下跌,而不是上涨,我们将采用相同的策略。如果价格自 x − 15 处再次下跌 15 点,则在 x − 30 处放置新的多头订单,手数再次比前一笔翻倍,现在达到 0.04(0.02 的 2 倍)。新的加权平均价位计算为 ( 1 × x + 2 × ( x − 15 ) + 4 × ( x − 30 ) ) / 7 ,简化为 x − 21.428 ,价位比初始价位低了 21.428 点。

在 x − 30 的价位上,0.01 的初始多头订单面临 $3.0 的亏损(假设 EURUSD 货币对,其中 $1 相当于 10 个点)。第二笔 0.02 的多头订单产生的亏损是 $1.5 的 2 倍,相当于 $3.0,导致总亏损 $6.0。不过,如果价格随后上升到 x − 21.428 的加权平均价位,则初始多头订单 0.01 的亏损就能部分弥补 $0.8572。此外,第二笔订单 0.02 的亏损由 $0.8572 的 2 倍抵消,即 $1.7144。甚而,我们从 0.04 的新多头订单中获利,因为价格从 x − 30 的价位上涨了 8.572 点。由于手数翻了四倍(0.04),我们的收益也翻了两番,达到 $0.8572 的 4 倍,即 $3.4288。因此,总净盈利约为 $0( -$6 + $0.8572 + $1.7144 + $3.4288 = $0.0004)。由于在加权平均价位计算中四舍五入,这个数字并不完全为零。

如果价格从 x − 21.428 的加权平均价位进一步上涨,我们将继续积累正盈利,直到达到止盈价位。相反,如果价格再次下跌,我们重复该过程,每次迭代将手数增加一倍,并相应地调整加权平均价位。这种方式可确保每次价格触及加权平均价位时,我们能平稳达成约 $0 的净利润,之后逐渐产生正盈利。

同样的过程也适用于空头订单,每轮从初始空头订单开始。我们来将其分解:

我们从某个初始价格的空头持仓开始,为简单起见,使用小手数,例如 0.01(最小可能的大小)。自此,有两种结果可能:价格可能会上涨或下跌。如果价格下跌,我们基于下跌的程度获利,并在价格达到我们的止盈价位时了结盈利。然而,当价格开始上涨时,情况变得具有挑战性,此时采用我们的经典网格策略来确保盈利。

假设价格上涨了一定额度,为了便于解释,我们假设 15 个点。作为回应,我们在初始价格加 15 个点处再次放置空头订单,为简单起见,我们将手数增加了一倍,即手数乘数为 2。放置这笔新的空头订单后,会出现两种情况:价格可以继续上涨或开始下跌。如果价格下跌,由于手数增加,我们从这笔新的空头订单中获利。此外,这种减少不仅带来了利润,而且还消减了我们那笔手数为 0.01 的初始空头订单的损失。当价格达到两笔订单的加权平均价位时,我们的净盈利为零,我们在讨论多头订单时已探讨了这个概念。

计算加权平均数的公式保持不变,同时考虑到订单的相应价格和手数。该策略方式确保即使在波动的市场中,我们的持仓也能保证抵抗亏损,随着市场的变化,达到盈亏平衡点、继而盈利。

为简单起见,我们假设初始价格为 x,在该处放置了手数为 0.01 的空头订单。如果价格随后上涨 15 点,我们在 x + 15 处放置新的空头订单,将手数增加一倍至 0.02。该阶段的加权平均价位计算为 ( 1 × x + 2 × ( x + 15 ) ) / 3,简化为 x + 10 ,或比初始价位高出 10 个点。

当价格达到 x + 15 时,手数 0.01 的初始空头订单会产生 $1.5 的损失(假设 EURUSD 对中的 $1 相当于 10 个点)。如果价格随后跌至 x + 10 的加权平均价位,我们将抵消初始空头订单的 $0.5 亏损。此外,我们还从手数 0.02 的新空头订单中获利,因为价格从 x + 15 的水平下跌了 5 个点。由于手数翻倍,我们的收益也翻了一番,结果是 $1。因此,我们的总体净盈利等于 $0(计算为 -$1.5 + $0.5 + $1)。

如果价格继续从 x + 10 的加权平均价位下跌,我们将累积正盈利,直至达到止盈价位。该策略有效地平衡了不同价格变动的损失和收益,确保在不同的市场条件下实现净盈利、或盈亏平衡。

如果价格,抵御住下跌,继续从加权平均价位上涨,我们将采用与以前相同的策略。假设价格又比 x + 15 价位高出 15 个点。作为回应,我们在 x + 30 处放置新的空头订单,再次将手数自前一笔手数增加一倍,现在达到 0.04(2 乘以 0.02)。然后,修订后的加权平均价位计算为 ( 1 × x + 2 × ( x + 15 ) + 4 × ( x + 30 ) ) / 7,简化为 x + 21.428 ,或比初始价位高 21.428 点。

在 x + 30 的价位上,0.01 的初始空头订单面临 $3.0 的亏损(假设 EURUSD 货币对,其中 $1 相当于 10 个点)。第二笔 0.02 的空头订单产生的亏损是 $1.5 的 2 倍,相当于 $3.0,导致总损失 $6.0。不过,如果价格随后跌至 x + 21.428 的加权平均价位,我们将部分收复 0.01 手初始空头订单损失的 $0.8572。此外,我们将第二笔 0.02 手订单的损失收复了 2 乘以 $0.8572,即 $1.7144。甚至,我们从手数为 0.04 的新空头订单中获利,因为价格从 x + 30 的价位下跌了 8.572 点。由于手数翻了四倍(0.04),我们的收益也翻了两番,达到 $0.8572 的 4 倍,即 $3.4288。因此,总体净盈利约为 $0(计算公式为 -$6 + $0.8572 + $1.7144 + $3.4288 = $0.0004)。由于在加权平均价位计算中四舍五入,这个数字并不完全为零。

如果价格继续从 x + 21.428 的加权平均价位下跌,我们将积累正盈利,直至达到止盈价位。相反,如果价格进一步上涨,我们重复该过程,每次迭代将手数增加一倍,并相应地调整加权平均价位。这种方式可确保每次价格触及加权平均价位时,我们能平稳达成约 $0 的净利润,之后逐渐产生正盈利。


讨论我们经典网格策略的自动化

现在,我们来深入研究一下使用智能系统(EA)将这种经典网格策略自动化。

首先,我们将在全局空间中声明一些输入变量:

input bool initialPositionBuy = true;
input double distance = 15;
input double takeProfit = 5;
input double initialLotSize = 0.01;
input double lotSizeMultiplier = 2;
  1. initialPositionBuy:一个布尔变量,判定初始持仓的类型 — 多头或空头持仓。如果设置为 true,则初始持仓将为多头;否则,它将是空头。
  2. distance:每笔连续订单之间的固定距离,以点数为单位。
  3. takeProfit:高于所有持仓平均价格的距离,以点数为单位,在该价格上,所有持仓将获利了结,从而获得总净盈利。
  4. initialLotSize: 第一笔开仓的手数。
  5. lotSizeMultiplier: 每笔连续开仓应用的手数倍数。

这些都是输入变量,我们可出于各种目的(例如策略中的优化)而更改。现在我们在全局空间中定义更多变量:

bool gridCycleRunning = false;
double lastPositionLotSize, lastPositionPrice, priceLevelsSumation, totalLotSizeSummation;

这些变量是为以下目的:

  1. gridCycleRunning:这是一个布尔变量,如果一轮正在运行,则为 true,否则为 false,默认情况下为 false。
  2. lastPositionLotSize:这是一个双精度变量,存储每轮中任何给定时间点最后的开仓手数。
  3. lastPositionPrice:这是一个双精度变量,存储每轮中任何给定时间点最后一笔开仓价。
  4. priceLevelsSumation:这是所有持仓的开仓价总和,稍后将用于计算平均价位。
  5. totalLotSizeSummation:这是所有持仓的所有手数总和,稍后将用于计算平均价位。

现在我们已经建立了关键输入变量,我们将继续执行一个基本函数 StartGridCycle()它将处理每轮的初始化
//+------------------------------------------------------------------+
//| Hedge Cycle Intialization Function                               |
//+------------------------------------------------------------------+
void StartGridCycle()
   {
    double initialPrice = initialPositionBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);

    ENUM_ORDER_TYPE positionType = initialPositionBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
    m_trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, 0, 0);

    lastPositionLotSize = initialLotSize;
    lastPositionPrice = initialPrice;
    ObjectCreate(0, "Next Position Price", OBJ_HLINE, 0, 0, lastPositionPrice - distance * _Point * 10);
    ObjectSetInteger(0, "Next Position Price", OBJPROP_COLOR, clrRed);

    priceLevelsSumation = initialLotSize * lastPositionPrice;
    totalLotSizeSummation = initialLotSize;

    if(m_trade.ResultRetcode() == 10009)
        gridCycleRunning = true;
   }
//+------------------------------------------------------------------+

在 StartGridCycle() 函数中,我们首先创建一个双精度变量 initialPrice ,它将存储要价(Ask)或出价(Bid),具体取决于 initialPositionBuy 变量。具体来说,如果 initialPositionBuy 为 true,我们存储要价(Ask);如果为 false,则存储出价(Bid)。这种区别至关重要,因为多头持仓只能以要价开立,而空头持仓必须以出价开立。
ENUM_ORDER_TYPE positionType = initialPositionBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
CTrade m_trade;
m_trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, 0, 0);

现在,我们将根据 initialPositionBuy 的值开立多头或空头持仓。为达此目的,我们将创建一个 ENUM_ORDER_TYPE 类型的变量,名为 positionType。ENUM_ORDER_TYPE 是 MQL5 中预定义的自定义变量类型,能够获取从 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

事实上,在 ENUM_ORDER_TYPE 中,数值 0 和 1 分别对应于 ORDER_TYPE_BUY 和 ORDER_TYPE_SELL 。对于我们的目的,我们将专注于这两个。利用标识符而非它们的整数值是有利的,因为标识符更直观、且更容易记住。

因此,如果 initialPositionBuy 为 true,我们将 positionType 设置为 ORDER_TYPE_BUY;否则,我们将其设置为 ORDER_TYPE_SELL。

为了继续,我们首先在全局空间中导入标准交易库 Trade.mqh。这是用以下语句完成的:

#include <Trade/Trade.mqh>
CTrade m_trade;

为了继续,我们还定义了 CTrade 类的一个实例,我们将它命名为 m_trade 。该实例将用于管理交易操作。然后,为了开仓,我们调用 m_trade 的 PositionOpen 函数,该函数基于我们设置的参数,包括 positionType 和其它相关交易设置,处理多头或空头的实际开仓。

m_trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, 0, 0);

自 m_trade 实例调用 PositionOpen 函数时,我们为其提供了开仓所必需的几个参数:

  1. _Symbol:第一个参数是 _Symbol ,指的是当前交易品种。

  2. positionType:第二个参数是 positionType ,我们之前基于 initialPositionBuy 的数值定义了它。这决定了开仓是多头还是空头。

  3. initialLotSize:开仓手数,由我们的输入变量 initialLotSize 定义。

  4. initialPrice:开仓价格。这是由 initialPrice 变量判定的,其根据所开仓类型保存要价或出价。

  5. SL (Stop Loss) 和 TP (Take Profit):最后两个参数用于设置止损(SL)和止盈(TP)。在这个特定的策略中,我们最初不会设置这些参数,因为订单平仓将由策略逻辑决定,特别是当价格达到平均价格加上止盈值时。

现在,我们转到代码的下一部分:

lastPositionLotSize = initialLotSize;
lastPositionPrice = initialPrice;
ObjectCreate(0, "Next Position Price", OBJ_HLINE, 0, 0, lastPositionPrice - distance * _Point * 10);
ObjectSetInteger(0, "Next Position Price", OBJPROP_COLOR, clrRed);

  1. 设置 lastPositionLotSize 和 lastPositionPrice 变量

    • 我们已将全局空间中定义的 lastPositionLotSize 变量初始化为等于 initialLotSize。这个变量至关重要,因为它保持跟踪最后的开仓手数,下一笔订单会据其乘以输入乘数来计算手数。
    • 类似地,lastPositionPrice 也设置为 initialPrice。这个变量,也在全局空间定义,对于判定后续订单的开立价位至关重要。

  2. 创建可视化的水平线

    • 为了增强策略在图表上的可视化表示,我们调用 ObjectCreate 函数。该函数具有以下参数:
      • 0:表示当前图表
      • “Next Position Price” 作为对象的名称
      • OBJ_HLINE 作为对象类型,表示水平线
      • 两个额外的 0,一个代表子窗口,另一个代表日期时间,因为水平线只需要价格水平
      • 下一笔订单的计算价格水平 (lastPositionPrice - distance * _Point * 10) ,作为最后的参数
    • 若要将此水平线的颜色设置为红色,请调用 ObjectSetInteger 函数,并将 OBJPROP_COLOR 属性设置为 clrRed。

  3. 继续网格轮次中的平均价格管理

    • 完成这些初始步骤后,我们现在转到管理网格轮次内的平均价格。这涉及随着开立新持仓,和市场的演化而动态计算和调整平均价格,这是网格交易策略的关键组成部分。

我们转到代码实现的下一部分。

priceLevelsSumation = initialLotSize * lastPositionPrice;
totalLotSizeSummation = initialLotSize;

  1. 设置 priceLevelsSumation :

    • 我们在全局空间中定义了 priceLevelsSumation,是为计算所有持仓的加权平均价格。最初,由于只有一笔订单,我们将 priceLevelsSumation 设置为等于 lastPositionPrice 乘以其相应的权重,即订单的手数。这步设置准备的变量,随着开立新持仓,其会累积更多价位,每个价位乘以它们各自的手数。
  2. 初始化 totalLotSizeSummation

    • totalLotSizeSummation 变量最初设置为等于 initialLotSize。这在加权平均公式的上下文中是有意义的,其中我们需要除以总权重。开始时,只有一笔订单,总权重即是该笔订单的手数。随着您开立更多持仓,我们会将其权重(手数)添加到该总和中,从而动态更新总权重。

现在,我们继续执行 StartGridCycle() 函数的最后一部分:

if(m_trade.ResultRetcode() == 10009)
    gridCycleRunning = true;

设置 hedgeCycleRunning
  • 变量 hedgeCycleRunning 只有在成功开仓后才设置为 true。这是调用 CTrade 实例中名为 trade 的 ResultRetcode() 函数验证的。返回代码 “10009” 表示订单放置成功。(注意:对于交易请求的不同结局,应引用各种返回代码。)
  • hedgeCycleRunning 的使用对于该策略至关重要,因为它标志着网格轮次的开始。该标志的重要性将在代码的后续部分中变得更加明显。

启动网格策略 StartGridCycle() 函数完成后,您现在转到 OnTick() 函数。我们将该函数分为 5 个片段,每个片段处理交易逻辑的特定方面:

  1. 启动网格轮次
  2. 处理多头持仓
  3. 处理空头持仓
  4. 网格轮次收尾函数
  5. 处理平仓
  1. 启动网格轮次:
    double price = initialPositionBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
      
      if(!gridCycleRunning)
         {
          StartGridCycle();
         }
    1. 声明 price 变量

      • 声明一个名为 price 的新变量。它的值由 initialPositionBuy 标志确定:
        • 如果 initialPositionBuy 为 true,则 price 设置为当前要价(ASK)。
        • 如果 initialPositionBuy 为 false,则 price 设置为当前出价(BID)价格。
    2. 基于 gridCycleRunning 的条件执行

      • 下一步涉及对 gridCycleRunning 变量进行条件检查:
        • 如果 gridCycleRunning 为 false,则表示网格轮次尚未开始、或已完成其上一个轮次。在本例中,我们执行 StartGridCycle() 函数,其已在前面详细解释过了。该函数通过开立第一笔持仓,并设置必要的参数来初始化网格轮次。
        • 如果 gridCycleRunning 为 true,则表示网格轮次已经处于激活状态。在这种状况下,我们暂时选择什么都不做。该决定允许现有网格轮次继续基于其已建立的逻辑运行,而不会启动新轮次、或干扰当前轮次。

    这种方式有效地管理了网格轮次的启动和继续,确保交易策略符合其设计的操作流程。我们继续执行网格策略实现的后续步骤。


  2. 处理多头持仓
    if(initialPositionBuy && price <= lastPositionPrice - distance * _Point * 10 && gridCycleRunning)
         {
          double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
          m_trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, price, 0, 0);
      
          lastPositionLotSize *= lotSizeMultiplier;
          lastPositionPrice = price;
          ObjectCreate(0, "Next Position Price", OBJ_HLINE, 0, 0, lastPositionPrice - distance * _Point * 10);
          ObjectSetInteger(0, "Next Position Price", OBJPROP_COLOR, clrRed);
      
          priceLevelsSumation += newPositionLotSize * lastPositionPrice;
          totalLotSizeSummation += newPositionLotSize;
          ObjectCreate(0, "Average Price", OBJ_HLINE, 0, 0, priceLevelsSumation / totalLotSizeSummation);
          ObjectSetInteger(0, "Average Price", OBJPROP_COLOR, clrGreen);
         }

    此处,我们有另一个 if 语句,它有 3 个条件:

    1. initialPositionBuy 的条件

      • 仅当变量 initialPositionBuy 为 true 时,才会执行该代码段。该条件确保处理多头持仓的逻辑与空头持仓的逻辑分开。

    2. price 的条件

      • 第二个条件检查变量 price 是否小于或等于 lastPositionPrice - distance * _Point * 10 。这个条件对于判定何时开立新多头持仓至关重要。这里使用了减法(-)操作,与网格策略中为多头持仓定义的方向逻辑一致。

    3. gridCycleRunning 的条件

      • 第三个条件要求变量 gridCycleRunning 为 true,表示网格轮次当前处于激活状态。这对于确保开立新持仓仅作为正在进行的交易轮次的一部分至关重要。

    如果满足所有三个条件,则 EA 将继续开立新的多头持仓。然而,在这样做之前,它会计算新仓位的手数:

    • 声明一个新的双精度变量 newPositionLotSize,并将其设置为等于 lastPositionLotSize 乘以 lotSizeMultiplier。
    • 然后将生成的手数常规化为小数点后两位,令手数有效,因为手数必须严格是 0.01 的倍数。

    这种方法确保以相应的大小开立新持仓,并遵守网格策略的规则。随后,EA 调用名为 m_trade 的 CTrade 类实例(前面在全局空间中声明)的 PositionOpen() 函数,以计算出的手数开立多头持仓。

    继续执行逻辑,下一步涉及更新 lastPositionLotSize 变量。这对于在后续订单中保持手数的准确性至关重要:

    • lastPositionLotSize 设置为等于自身乘以 lotSizeMultiplier。至关重要的是,该步乘法不涉及常规化到小数点后两位。
    • 考虑一种状况,即 lotSizeMultiplier 为 1.5,且 initialLotSize 为 0.01 来描述避免常规化的原因。将 0.01 乘以 1.5 得到 0.015,当常规化为两位小数时,将舍入到 0.01。这将创建一个循环,其中手数始终保持在 0.01,尽管已倍增。
    • 为避免该问题,并确保手数按预期增加,lastPositionLotSize 采用自身的非规范化乘积,以及 lotSizeMultiplier 进行更新。该步骤对于网格策略的正常运行至关重要,尤其是对于分数倍数。

    经此更新,EA 可以准确跟踪和调整新持仓的手数,保持网格策略的预期进展。

    继续该过程,下一步涉及更新 lastPositionPrice,并在图表上可视化下一笔开仓价位:

    1. 更新 lastPositionPrice
      • 变量 lastPositionPrice 更新后等于 price,其由 initialPositionBuy 条件判定。由于 if 语句的第一个条件确保仅在 initialPositionBuy 为 true 时入场,因此在这种境况下,价格将对应于要价(Ask)。

    2. 可视化下一笔开仓价位
      • 在图表上绘制一条名为 “Next Position Price” 的水平线。这条线代表下一笔订单开立的价位。
      • 由于 initialPositionBuy 为 true,表明下一笔开仓将是多头,故该条线的价位设置为 lastPositionPrice(刚刚更新)减去距离(如输入中指定的)乘以 _Point,然后进一步乘以 10。
      • 该水平线是使用 ObjectCreate() 函数创建的,并调用其它对象属性函数将其颜色设置为 clrRed,以便于可视化。这种视觉辅助工具有助于轻松识别下一笔潜在多头订单的价位。

    通过更新 lastPositionPrice,并直观地标记下一笔订单价位,EA 有效地为网格轮次中的后续步骤做准备,确保每个新价位都符合策略的标准,并且可以在图表上直观地跟踪。

    现在,我们来优化平均价位的计算,特别要关注加权平均线:

    1. 更新 priceLevelsSummation 和 totalLotSizeSummation

      • 为了更新加权平均价格,我们将 lastPositionPrice 和 newPositionLotSize 的乘积添加到 priceLevelsSummation。
      • 对于 totalLotSizeSummation,我们只需加上 newPositionLotSize 的值。
      • 这些更新对于跟踪所有持仓的累积价位,以及总手数至关重要。

    2. 计算加权平均价位

      • 平均价位,在这种境况下是加权平均值,计算方法是取 priceLevelsSummation 除以 totalLotSizeSummation。
      • 该计算准确反映了所有持仓的平均价格,同时考虑了它们各自的手数。

    3. 可视化加权平均价格水平

      • 调用 ObjectCreate() 函数在图表上按计算出的平均价位创建另一条水平线。
      • 这条线的颜色设置为 clrGreen,是为了将其与指示下一笔开仓价格的另一条水平线区分开来。
      • 重点在于,该计算是 lastPositionLotSize 的常规化值乘以 lotSizeMultiplier。这确保了开新仓时考虑到真实手数,从而提供准确的加权平均值。

    通过整合这些步骤,EA 不仅可以跟踪所有持仓的平均价位,还可以在图表上直观地呈现它。这允许根据网格轮次的当前状态、以及市场状况轻松进行监控和决策。


  3. 处理空头持仓:
    if(!initialPositionBuy && price >= lastPositionPrice + distance * _Point * 10 && gridCycleRunning)
         {
          double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
          m_trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, price, 0, 0);
      
          lastPositionLotSize *= lotSizeMultiplier;
          lastPositionPrice = price;
          ObjectCreate(0, "Next Position Price", OBJ_HLINE, 0, 0, lastPositionPrice + distance * _Point * 10);
          ObjectSetInteger(0, "Next Position Price", OBJPROP_COLOR, clrRed);
      
          priceLevelsSumation += newPositionLotSize * lastPositionPrice;
          totalLotSizeSummation += newPositionLotSize;
          ObjectCreate(0, "Average Price", OBJ_HLINE, 0, 0, priceLevelsSumation / totalLotSizeSummation);
          ObjectSetInteger(0, "Average Price", OBJPROP_COLOR, clrGreen);
         }

    此处,我们有另一个 if 语句,它再次对应 3 个条件:

    1. initialPositionBuy 的条件

      • 仅当 initialPositionBuy 为 false 时,才会执行该代码段。该条件确保逻辑专门处理空头持仓,与多头持仓不同。

    2. price 的条件

      • 第二个条件检查 price 是否大于或等于 lastPositionPrice + distance * _Point * 10。这对于判定开立新空头持仓的合适时机至关重要。在这种情况下使用加法(+)运算,与您的空头持仓策略的方向逻辑保持一致。

    3. gridCycleRunning 的条件

      • 第三个条件要求 gridCycleRunning 为 true,表示网格轮次正在运行。这确保了新持仓仅作为正在进行的交易轮次的一部分。

    如果满足所有三个条件,EA 继续开一笔新的空头持仓:

    • 声明一个双精度变量 newPositionLotSize,并将其设置为等于 lastPositionLotSize 乘以 lotSizeMultiplier。
    • 然后将这个新的手数常规化为小数点后两位,令该手数有效,因为手数必须严格是 0.01 的倍数。
    • 最后,EA 调用 CTrade 类实例 m_trade 中的 PositionOpen() 函数(之前在全局空间中声明),以便按计算出的手数开立空头持仓。

    这种方式确保以相应的间隔和尺寸开立新的空头持仓,遵守网格策略的规则,并维护策略的进度。

    在处理空头持仓的下一步中,我们专注于正确更新 lastPositionLotSize 变量:

    • 变量 lastPositionLotSize 更新为等于自身乘以 lotSizeMultiplier。该步骤对于维持网格轮次中手数的进展至关重要。
    • 重点是,该乘法不涉及常规化到小数点后两位。该决定对于避免小数乘法器的潜在问题至关重要。
    • 为了清晰描述:当 lotSizeMultiplier 为 1.5,且 initialLotSize 为 0.01 时,乘法结果是 0.015。若将其常规化为小数点后两位,则四舍五入后会回到 0.01。重复该过程将永远产生 0.01 的手数,从而创建一个循环,并有损策略的意图。
    • 为了规避这个问题,lastPositionLotSize 被设置为其自身和 lotSizeMultiplier 的非常规化乘积。这种方式可确保手数适当提升,尤其是在与手数为小数的乘数打交道时。

    通过更新不进行标准化的 lastPositionLotSize,EA 可以有效地跟踪和调整新空头持仓的手数,确保网格策略按预期运行。

    1. 更新 lastPositionPrice

      • 变量 lastPositionPrice 更新后等于 price,它应当是要价或出价,具体取决于 initialPositionBuy 的值。
      • 在这种情况下,由于我们仅在 initialPositionBuy 为 false 时(根据第一个条件)才进入代码的这一段,因此 lastPositionPrice 被设置出价(BID)。

    2. 在下一笔开仓处绘制一条水平线

      • 在图表上绘制一条名为 “Next Position Price” 的水平线。该线表示下一笔订单(在本例中为空头订单)开立的价位。
      • 这条水平线的价位设置在 lastPositionPrice(其已被更新)加上输入中指定的距离,乘以 _Point,然后再乘以 10。该计算判定下一笔空头订单的相应价位。
      • 该线是调用 ObjectCreate() 函数创建的,该函数是 MQL5 中的预定义函数,用于在图表上绘制对象。
      • 该线条的颜色设置为 clrRed,以增强其可见性,并令其在图表上易于区分。

    通过适当设置 lastPositionPrice,并直观地表示下一笔订单的价位,EA 有效地为后续的空头订单做准备,确保它们符合网格策略的规则,且在图表上易于识别。

    在处理平均价位的计算时,特别是关注空头持仓的加权平均值,该过程涉及:

    1. 更新 priceLevelsSummation 和 totalLotSizeSummation

      • lastPositionPrice 的值乘以 newPositionLotSize 之后被加到 priceLevelsSummation。该步骤累积所有持仓的价位,每笔持仓按各自的手数加权。
      • newPositionLotSize 的值被加到 totalLotSizeSummation 当中。该变量跟踪所有持仓的累积手数。

    2. 计算加权平均价位

      • 平均价位是通过把 priceLevelsSummation 除以 totalLotSizeSummation 获得的。该计算得出所有持仓的加权平均价格。

    3. 可视化加权平均价格水平

      • 调用 ObjectCreate() 函数在加权平均价位上创建一条水平线。这种可视化表示有助于监控所有持仓的平均价格。
      • 该线条的颜色设置为 clrGreen,令其易于与图表上的其它线条区分开来。
      • 重点要注意,该计算是取 lastPositionLotSize 的常规化值乘以 lotSizeMultiplier。这能确保实际情况下,考虑到已开仓的真实手数,从而提供准确的加权平均计算。

    这种计算和可视化加权平均价位的方法对于有效管理网格轮次中的空头持仓至关重要,允许基于当前市场和持仓状态制定明智的决策。


  4. 网格轮次关闭函数:
    //+------------------------------------------------------------------+
    //| Stop Function for a particular Grid Cycle                        |
    //+------------------------------------------------------------------+
    void StopGridCycle()
       {
        gridCycleRunning = false;
        ObjectDelete(0, "Next Position Price");
        ObjectDelete(0, "Average Price");
        for(int i = PositionsTotal() - 1; i >= 0; i--)
           {
            ulong ticket = PositionGetTicket(i);
            if(PositionSelectByTicket(ticket))
               {
                m_trade.PositionClose(ticket);
               }
           }
       }
    //+------------------------------------------------------------------+

    1. 将 gridCycleRunning 设置为 false

      • 该函数中的第一个动作是将布尔变量 gridCycleRunning 设置为 false。该更改意味着网格轮次不再有效,并且正在关闭过程当中。

    2. 删除图表对象

      • 接下来,您调用预定义的 ObjectDelete() 函数从图表中删除两个特定对象:“Next Position Price” 和 “Average Price”。该步骤清除了这些图表上的标记,表明轮次正在结束,这些价位不再相关。

    3. 了结所有持仓

      • 然后,该函数继续遍历所有持仓。
      • 每笔持仓都按其单号单独选择。
      • 一旦选择持仓,调用 m_trade 实例的 PositionClose() 函数将其平仓。持仓的单号作为参数传递给该函数。
      • 这种系统性的方式确保作为网格轮次一部分开立的所有持仓都被平仓,从而有效地结束该特定轮次的交易活动。

    通过遵循这些步骤,网格轮次平仓函数有效地了结所有持仓,重置交易环境,并为 EA 准备好新的网格轮次。


  5. 处理平仓

    if(gridCycleRunning)
       {
        if(initialPositionBuy && price >= (priceLevelsSumation / totalLotSizeSummation) + takeProfit * _Point * 10)
            StopGridCycle();
    
        if(!initialPositionBuy && price <= (priceLevelsSumation / totalLotSizeSummation) - takeProfit * _Point * 10)
            StopGridCycle();
       }

    在最后一部分“处理平仓”中,订单平仓的过程由 if 语句控制,具体取决于某些条件:

    1. 执行任何操作的主要条件是 gridCycleRunning 必须为 true,表示激活的网格轮次。
    2. 在此范围内,还有两个基于 initialPositionBuy 值的进一步条件检查:
      • 如果 initialPositionBuy 为 true,且当前价格高于加权平均价位的止盈点(在输入变量中定义),则执行 StopGridCycle() 函数。
      • 相反,如果 initialPositionBuy 为 false,且当前价格低于加权平均价位的止盈点,则执行 StopGridCycle() 函数。

    这些条件确保网格轮次终止,即当持仓相对于加权平均价位达到指定的止盈准则时平仓。这标志着经典网格策略的自动化过程结束。


回测经典网格策略

随着我们的经典网格策略的自动化现已完成,是时候在实际场景中评估其性能了。

对于回测,将采用以下输入参数:

  • initialPositionBuy : true
  • distance : 15 pips
  • takeProfit : 5 pips
  • initialLotSize : 0.01
  • lotSizeMultiplier : 2.0

测试将依据 2023 年 11 月 1 日至 2023 年 12 月 22 日的 EURUSD 货币对进行。选择的杠杆为 1:500,起始本金为 $10,000。至于时间帧,值得注意的是,它与我们的策略无关,因此任何选择都足够了,不会影响结果。该测试虽然涵盖的时间相对较短,不到两个月,但其本意是作为覆盖较长时间的潜在结果的代表性样本。

现在,我们深入研究一下这个回测的结果:


这些结果看起来相当不错。在查看回测结果后,我们观察到图表中蓝色和绿色线条所代表的有趣趋势。我们来分解一下每条线的含义,以及它们的走势如何反映策略的绩效:

  1. 了解蓝线和绿线

    • 蓝线代表账户余额,而绿线表示净值。
    • 观察到一个值得注意的形态,即余额增加,净值减少,最终它们收敛到同一点。

  2. 余额波动解释

    • 该策略规定同时关闭一个轮次的所有订单,理想情况下,这应该会导致余额的直接增加。不过,该图形显示了余额的上升和下降形态,这需要进一步解释。
    • 这种波动归因于订单平仓的轻微延迟(几分之一秒)。即使订单几乎同时平单,该图形也会捕获余额中的增量更新。
    • 最初,余额随着盈利持仓先行平仓而增加。持仓在循环中平仓,从最后一笔(盈利最多)持仓开始,如函数 PositionsTotal() 所示。因此,可以忽略余额线的向上和短暂的向下走势,而是专注于整体的净上升趋势。

  3. 净值线变动

    • 与余额相对应,净值最初下跌,然后上升。这是因为盈利持仓先行平仓,导致净值在恢复之前暂时减少。
    • 绿色净值线的走势遵循与蓝色余额线类似的逻辑,得出整体趋势积极向上的相同结论。

总之,尽管由于平单顺序和轻微延迟,图形中捕捉到很小的波动,但总体趋势表明该策略取得了成功,余额线和净值线的最终收敛和向上走势亦证明了这一点。

回测结果表明经典网格策略的盈利能力。然而,重要的是要认识到这种策略固有的重大限制:需要占用很高的资本。

这种策略通常需要大量资金,以此承受在实现盈利之前发生的回撤。在不利的市场走势中继续持仓,而不会面临追加保证金通知,或被迫亏损平仓的能力至关重要。

解决该限制将是本系列后续部分的重点,我们将探讨该策略的优化技术。标的是提高它们的效率,并降低所需的占用资本,令该策略对于不同资本规模的交易者更容易运用,并降低风险。


结束语

在本期文章中,我们深入研究了经典网格策略的复杂性,并利用 MQL5 的智能交易系统(EA)成功地实现了自动化。我们还研究了该策略的一些初步结果,强调了其潜力和需要改进的领域。

然而,我们优化该类策略的旅程远未结束。本系列的后续部分将侧重于优调策略,特别是磨练 distance、takeProfit、initialLotSize 和 lotSizeMultiplier 等输入参数的最优值,从而最大限度地提高回报,并最大限度地减少回撤。我们即将探索的一个有趣方面是判定从多头或空头开始持仓的有效性,这可能会根据不同的货币和市场条件而有所不同。

在后续文章中,我们将发现更多有价值的见解和技术,值得期待。感谢您花时间阅读我的文章,并希望它们能为您的交易和编码索求提供知识和实际帮助。如果您希望在本系列的下一部分讨论特定主题或思路,我们随时欢迎您的建议。

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


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

附加的文件 |
开发回放系统(第 43 部分):Chart Trade 项目(II) 开发回放系统(第 43 部分):Chart Trade 项目(II)
大多数想要或梦想学习编程的人实际上并不知道自己在做什么。他们的活动包括试图以某种方式创造事物。然而,编程并不是为了定制合适的解决方案。这样做会产生更多的问题而不是解决方案。在这里,我们将做一些更高级、更与众不同的事情。
交易者基于角度的操作 交易者基于角度的操作
本文将介绍基于角度的操作。我们将研究构建角度和在交易中使用角度的方法。
神经网络变得简单(第 72 部分):噪声环境下预测轨迹 神经网络变得简单(第 72 部分):噪声环境下预测轨迹
预测未来状态的品质在“目标条件预测编码”方法中扮演着重要角色,我们曾在上一篇文章中讨论过。在本文中,我想向您介绍一种算法,它可以显著提高随机环境(例如金融市场)中的预测品质。
您应当知道的 MQL5 向导技术(第 10 部分):非常规 RBM 您应当知道的 MQL5 向导技术(第 10 部分):非常规 RBM
限制性玻尔兹曼(Boltzmann)机处于基本等级,是一个两层神经网络,擅长通过降维进行无监督分类。我们取其基本原理,并检验如果我们重新设计和训练它,我们是否可以得到一个实用的信号滤波器。