English Русский Español Deutsch 日本語 Português
preview
重构经典策略:原油

重构经典策略:原油

MetaTrader 5示例 |
854 3
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

概述

石油是地球上最重要的商品。原油在未经提炼时并无用处;然而,经过提炼后,它被广泛应用于各行各业,从简单的农业到复杂的制药业。石油是少数几个真正被所有行业需要的大宗商品之一。石油价格是衡量全球生产水平和经济增长水平的关键经济计量指标。

全球原油贸易主要以两种基准油为主导:北美基准油西德克萨斯中质原油(WTI)和世界大部分地区原油报价所参照的布伦特原油(Brent)。

在本文的讨论中,我们将重新审视一种经典的原油价差交易策略,并希望找到一种最优的机器学习策略,使这种经典策略在当今由算法主导的原油市场中能够更受欢迎。
我们将从重点强调上述两种原油基准间的差异开始本次讨论。接下来,我们将在MQL5中可视化布伦特原油与WTI原油价差,并讨论经典的价差交易策略。通过演示如何利用监测机器学习来分析西德克萨斯中质原油和布伦特原油价格之间的价差,从而为可能发现价格变化的领先指标奠定基础。阅读本文后,您将深刻理解以下内容:

  • 布伦特原油和WTI原油基准之间的差异,以及它们为何重要。
  • 如何使用MQL5的矩阵和向量函数来构建易于维护和实现的紧凑机器学习模型
  • 如何运用伪逆技术找到最小二乘解,以利用西德克萨斯中质和布伦特价差预测布伦特原油的未来价格。

全球原油基准:布伦特

当地下开采出原油时,它是一种包含一些氧、碳、氢和硫杂质的混合物。布伦特被认为是轻质且低硫的原油混合物分类。要被认定为低硫,这种混合物中的硫杂质浓度必须很低。此外,它之所以被称为“轻质”,是因为其密度较低。这些是偏理想的特性,因为它们告诉我们这种混合物将更容易精炼。关于布伦特的最后一个特性是,布伦特原油的质量低于西德克萨斯中质原油。布伦特原油主要开采自北海。开采后,它很容易被储存在大型油轮上的油桶中。这使得布伦特相对于西德克萨斯中质具有一个显著优势,即它非常便于获取。目前,布伦特原油的交易价格高于西德克萨斯中质原油。

布伦特价格

图例1:MQL5中布伦特原油的历史价格

北美原油基准:西德克萨斯中质原油(WTI)

WTI是对某一类特定混合原油的分类名称,它必须是一种“轻质低硫”原油。WTI原油在全美各地开采,但主要集中在德克萨斯州。与布伦特原油相比,WTI原油的硫含量更低、密度更小,这意味着它更容易被精炼成成品。纵观历史来看,WTI原油主要在美国的内陆地区开采,因此其可获取性远低于布伦特原油。然而,由于海湾沿岸的大规模投资和2015年石油出口禁令的废除,WTI原油现在的可获取性比以往任何时候都要高。

WTI

图例2:MQL5中WTI的历史价格

入门:可视化研究

首先,我们可以创建一个实用的脚本来可视化这两种大宗商品之间的价差。我们可以使用MQL5图形库来帮助我们轻松绘制我们想要的任何函数。图形库会为您管理图形对象,这是很有帮助的。在导入图形库后,您会注意到一个定义为“consumption”的变量。这个变量有助于我们轻松选择可用数据的全部、一半、四分之一或任何比例。

鉴于我们正在请求两种不同资产的历史数据,我们需要知道每个市场的可用柱状图总数。从那里,我们假设最少的可用柱状图数量即为总的可用柱状图数量。我们使用三元运算符来选择正确的柱状图数量。
在我们确定了要使用的正确的柱状图数量后,我们就可以绘制点差图。

//+------------------------------------------------------------------+
//|                                             Brent-WTI Spread.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Graphics\Graphic.mqh>
//Set this value between 0 and 1 to control how much data is used
double consumption = 1.0;
int brent_bars = (int) NormalizeDouble((iBars("UK Brent Oil",PERIOD_CURRENT) * consumption),0);
int wti_bars = (int) NormalizeDouble((iBars("WTI_OIL",PERIOD_CURRENT) * consumption),0);
//We want to know which symbol has the least number of bars.
int max_bars = (brent_bars < wti_bars) ? brent_bars : wti_bars;

//+------------------------------------------------------------------+
//|This event handler is only triggered when the script launches     |
//+------------------------------------------------------------------+
void OnStart()
  {
   CGraphic graphic;
   double from = 0;
   double to  = max_bars;
   double step = 1;
   graphic.Create(0,"G",0,0,0,600,200);
   CColorGenerator generator;
   uint spread = generator.Next();
   CCurve *curve = graphic.CurveAdd(SpreadFunction,from,to,step,spread,CURVE_LINES,"Blue");
   curve.Name("Spread");
   graphic.XAxis().Name("Time");
   graphic.XAxis().NameSize(12);
   graphic.YAxis().Name("Brent-WTI Spread");
   graphic.YAxis().NameSize(12);
   graphic.CurvePlotAll();
   graphic.Update();
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|This function returns the Brent-WTI spread                        |
//+------------------------------------------------------------------+
double SpreadFunction(double x)
  {
   return(iClose("UK Brent Oil",PERIOD_CURRENT,(max_bars - x)) - iClose("WTI_OIL",PERIOD_CURRENT,(max_bars - x)));
  }
//+------------------------------------------------------------------+

布伦特与WTI原油价差

图例3:在MQL5中可视化布伦特原油与WTI原油价差


我们交易策略概述:采用监督式的机器学习策略

传统原油交易策略的前提是:从长期来看,总会恢复价格均衡。传统的原油价差交易策略主张我们首先观察布伦特原油与WTI原油之间的当前价差。如果价差高于其基线(例如基线可以是20日均线),那么我们会推断出价差将在不久的将来回归到其均值。因此,如果布伦特原油价格上涨,我们会选择卖出。相反,如果布伦特原油价格下跌,我们会选择买入。 

然而,这一策略自从被开发之后,石油市场已经发生了相当大的变化。我们需要一个客观的流程来推断价差与布伦特原油未来价格之间存在何种关系。机器学习使计算机能够从任何其可以分析观察到的关系中自行学习交易规则。

为了让我们的计算机创建自己的交易策略,我们从数据矩阵A开始。

A代表我们掌握的布伦特原油的历史价格数据。我们将使用收盘价、价差以及一个常数值为1的截距项。然后,我们将构建一个单独的列向量x,它对应A中的每一列都有一个系数。这个值将直接从市场数据中计算得出,并将被我们的模型用来预测未来价格。

定义A和x

图例4:构建最小二乘问题

在创建了输入矩阵A之后,我们需要知道布伦特的收盘价与A中的每个输入是如何匹配的。我们将把输出价格存储在一个向量y中。我们的目标是找到一种方法,将输入数据矩阵A映射到输出数据向量y上,同时在我们所有的训练观测值中尽可能减小误差。这个问题的答案被称为最小二乘解。

最小二乘解说明

图例5:我们的输出向量y

关于最小二乘问题场景,存在多种有效的解决方案。下面我们将重点介绍一种称为伪逆技术的方法。伪逆技术是线性代数中的一个重要概念,它允许我们对非方阵进行求逆。我们将使用伪逆技术来找到列向量x的系数值,这些系数值能够将A映射到y上,并尽可能减小误差。 

摩尔-彭罗斯伪逆解(Moore-Penrose Pseudo-Inverse Solution)

图例6:伪逆解说明

根据上述两个方程,我们正在寻找一个x值,该值能使我们的预测值A*x与实际布伦特收盘价y之间的误差最小。请注意Ax-y周围的双竖线。这些双竖线代表L2范数。当我们在现实世界中处理实体对象时,可以问“它有多大?”。然而,当我们想知道一个向量或矩阵有多大时,我们会询问其范数。范数的计算方法有多种,最常见的是L1范数和L2范数。在我们的讨论中,仅考虑L2范数。

L2范数是将向量中的每个元素平方,然后将这些平方值相加,最后通过计算和的平方根得出。它也被称为欧几里得范数。用更简单的话来说,我们是在寻找能够减小模型所有误差的x值,而用更专业的术语来描述,“我们是在寻找能够最小化残差L2范数的最优x值”。
满足我们约束条件的x值被表示为x*。为了找到x*,我们计算矩阵A的伪逆与向量y的点积。除非您正在进行线性代数领域的练习,否则基本不需要自己去实现伪逆函数。反之,我们将依赖于MQL5中的内置函数。

//+------------------------------------------------------------------+
//|Demonstrating the pseudo-inverse solution in action.              |                                                                |
//+------------------------------------------------------------------+
void OnStart()
  {
//Training and test data
   matrix A; //A is the input data. look at the figure above if you need a reminder.
   matrix y,x; //y is the output data, x is the coefficients.
   A.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_OHLC,20,1000);
   y.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,1,1000);
   A.Reshape(1000,4);
   y.Reshape(1000,1);
   Print("Attempting Psuedoinverse Decomposition");
   Print("Attempting to calculate the Pseudoinverse Coefficients: ");
   x = A.PInv().MatMul(y);
   Print("Coefficients: ");
   Print("Open: ",x[0][0],"\nHigh: ",x[1][0],"\nLow: ",x[3][0],"\nClose: ",x[3][0]);
  }
//+------------------------------------------------------------------+

伪逆脚本

图例7:伪逆技术的一个实现示例

上述代码直观地展示了伪逆技术的应用。在这个示例中,我们的目标是使用当前的开盘价、最高价、最低价和收盘价来预测某个交易品种的收盘价。这个简单的例子包含了我们能理解的核心原理。我们首先从定义输入数据开始,这些数据被存储在矩阵A中。为了获取数据,我们使用了CopyRates函数,该函数需要按照指定的顺序提供以下参数:

  • 交易品种:我们想要进行交易的品种名称。
  • 时间周期:与我们的风险水平相匹配的交易时间周期。
  • 价格掩码:其指定了要复制哪些价格,允许我们根据需要选择,例如,只选择开盘价。
  • 起始日期:复制数据的起始日期,确保输入数据和输出数据之间存在间隔,并且输入数据从较早的日期开始。
  • 数量:要复制的K线数量。

在建立了输入数据矩阵A之后,我们重复这一步骤来构建输出数据矩阵y。接着,我们调整这两个矩阵的形状,以确保它们的大小适当且与我们打算执行的操作兼容。

然后,我们用从A和y中派生的值填充x列向量。对我们有利的一点是,MQL5的API支持矩阵操作的链式调用,这样就可以用一行代码计算出伪逆解。一旦完成后,我们就可以打印出x列向量中的系数。

我们将使用相同的步骤来开发我们的交易策略。唯一没有在这里演示的步骤是利用我们的模型进行预测,这部分内容将在我们后面的讨论中解释。基于这个基础,我们就可以开始构建我们的交易策略了。

综合以上

现在我们已经准备好定义算法的核心了。我们首先引入必要的交易库,以便我们能够打开和管理仓位。

//+------------------------------------------------------------------+
//|                                                     Brent EA.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//Libraries
#include  <Trade\Trade.mqh>
CTrade ExtTrade;
#include <TrailingStop\ATRTrailingStop3.mqh>
ATRTrailingStop ExtATRTrailingStop;

接下来,我们定义交易头寸规模和风险参数。第一个输入参数决定了每个头寸将超出最小交易量的多少倍。第二个输入参数设置了所有开仓头寸将在达到哪个盈利水平时被平仓。紧接着是限制我们在该账户上允许的总资金回撤的输入参数。最后,我们设置了每次交易时希望开设的头寸数量。

//Inputs
input double lot_multiple = 1.0;
input double profit_target = 10;
input double max_loss = 20;
input int position_size = 2;

接下来,我们需要了解每个市场可用的柱状图数量,以确保我们始终尝试复制在两个市场中都可获得的正确数量的柱状图。对于我们的情况,“正确数量”指的是可用的柱状图数量中的最小值。我们还定义了一个名为“consumption”的变量,因为它允许我们控制在代码示例中需要使用多少数据。在下面的代码示例中,我们正在使用所有可用历史数据的1%。

//Set this value between 0 and 1 to control how much data is used
double consumption = 0.01;
//We want to know which symbol has the least number of bars.
double brent_bars = (double) NormalizeDouble((iBars("UK Brent Oil",PERIOD_CURRENT) * consumption),0);
double wti_bars = (double) NormalizeDouble((iBars("WTI_OIL",PERIOD_CURRENT) * consumption),0);

下面我们需要确定哪个市场的可用柱状图数量更少,并将该柱状图的数量作为我们的限制数量。如果我们跳过了这一步,除非您的经纪商保证两个资产的历史价格数据集完全匹配,否则两个市场的日期可能无法匹配。“前瞻”是指我们的预测范围,或者我们预测未来多少步。

//Select the lowest
double max_bars = (brent_bars < wti_bars) ? brent_bars : wti_bars;
//How far into the future are we forecasting
double look_ahead = NormalizeDouble((max_bars / 4),0);
//How many bars should we fetch? 
int fetch = (int) (max_bars - look_ahead) - 1;

接下来,我们需要为我们之前标记的内容定义变量;我会附上一张图片,这样您就不必向上滚动阅读了。请记住,A是存储我们输入数据的矩阵,我们可以选择任意数量的输入,无论多少,在这个例子中,我将使用3个输入。x* 表示使我们的残差L2范数最小化的x的值。

摩尔-彭罗斯伪逆解(Moore-Penrose Pseudo-Inverse Solution)

图例6:我们之前定义过的标记

//Matrix A stores our inputs. y is the output. x is the coefficients.
matrix A = matrix::Zeros(fetch,6);
matrix y = matrix::Zeros(fetch,1);
vector wti_price = vector::Zeros(fetch);
vector brent_price = vector::Zeros(fetch);
vector spread;
vector intercept = vector::Ones(fetch);
matrix x = matrix::Zeros(6,1);
double forecast = 0;
double ask = 0;
double bid = 0;
double min_volume = 0;

我们将定义两个字符串变量来存储我们希望交易的品种名称。完成这一步骤后,就轮到OnInit函数了。在我们的案例中,这个函数很简单,我们只需要知道布伦特原油的最小允许交易量。

string brent = "UK Brent Oil";
string wti = "WTI_OIL";
bool model_initialized = false;
int OnInit()
  {
//Initialise trailing stops
   if(atr_multiple > 0)
      ExtATRTrailingStop.Init(atr_multiple);
   min_volume = SymbolInfoDouble(brent,SYMBOL_VOLUME_MIN);
   return(INIT_SUCCEEDED);
//---
  }

我们现在正在编写OnTick函数。在该函数中,我们首先更新我们正在追踪的买入价和卖出价。然后,我们要检查模型是否已经被初始化。如果模型未初始化,我们将对其进行训练和拟合;如果模型已初始化,我们将继续检查是否有开仓头寸。如果我们没有开仓头寸,我们将从模型中获取预测结果,并按照模型预测的方向进行交易。相反,如果我们有开仓头寸,我们将检查这些头寸是否未达到盈利目标或未超过最大回撤水平。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   ask = SymbolInfoDouble(brent,SYMBOL_ASK);
   bid = SymbolInfoDouble(brent,SYMBOL_BID);
   if(model_initialized)
     {
      if(PositionsTotal() == 0)
        {
         forecast = 0;
         forecast = ModelForecast();
         InterpretForecast();
        }

      else
        {
         ManageTrades();
        }
     }

   else
     {
      model_initialized = InitializeModel();
     }

  }
//+------------------------------------------------------------------+

该函数负责检查我们是否已突破风险水平或达到盈利目标。它只有在OnTick事件处理器中,且仅在我们有未平仓交易的情况下才会被调用。

void ManageTrades()
  {
   if(AccountInfoDouble(ACCOUNT_PROFIT) > profit_target)
      CloseAll();
   if(AccountInfoDouble(ACCOUNT_PROFIT) < (-1 * max_loss))
      CloseAll();
  }

当使用我们的模型做出预测时,我们会调用InterpretForecast函数来解释模型的预测结果,并根据预测结果开立相应的头寸。

void InterpretForecast()
  {
   if(forecast != 0)
     {
      if(forecast > iClose(_Symbol,PERIOD_CURRENT,0))
        {
         check_buy();
        }

      else
         if(forecast < iClose(_Symbol,PERIOD_CURRENT,0))
           {
            check_sell();
           }
     }
  }


我们有一个专门用于开立买入头寸的程序。请注意,我们之前确定的最小交易量会与手数倍数的输入值相乘,这样用户就可以控制用于开立交易的手数大小。

void check_buy()
  {
   if(PositionsTotal() == 0)
     {
      for(int i = 0; i < position_size; i++)
        {
         ExtTrade.Buy(lot_multiple * min_volume,brent,ask,0,0,"BUY");
        }
     }
  }

这里还包括了专门用于开立空头头寸的程序,为了应对我们可能发现的仅适用于某一持仓方向(多头或空头)的特定规则。

void check_sell()
  {
   if(PositionsTotal() == 0)
     {
      for(int i = 0; i < position_size; i++)
        {
         ExtTrade.Sell(lot_multiple * min_volume,brent,bid,0,0,"SELL");
        }
     }
  }

现在我们定义一个函数,用于关闭我们所有的未平仓头寸。它会遍历我们所有的未平仓头寸,并且只关闭以布伦特原油为交易标的的头寸。请注意,如果您想使用这个EA同时交易布伦特原油(Brent)和西德克萨斯中质原油(WTI),只需移除我为了确保交易标的为布伦特原油而设置的安全检查。请记住,我仅选择布伦特原油作为演示目的。您可以自由定制这个EA。

void CloseAll(void)
  {
   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetSymbol(i) == brent)
           {
            ulong ticket;
            ticket = PositionGetTicket(i);
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

现在我们将分别定义关闭多头头寸和空头头寸的两种方法。与之前一样,我们通过遍历所有头寸并获取每个头寸对应的订单编号来完成这一操作。之后,我们需要验证头寸类型是否与我们要关闭的类型相匹配。如果一切顺利,我们将关闭该头寸。

void close_buy()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal(); i++)
        {
         ticket = PositionGetTicket(i);
         type = (int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_BUY)
           {
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

void close_sell()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal(); i++)
        {
         ticket = PositionGetTicket(i);
         type = (int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_SELL)
           {
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

现在,我们将定义模型初始化的步骤:

  1. 确保两种原油交易标的都可用并已添加至市场窗口中。
  2. 将输出数据复制到矩阵y中(即从第1根K线开始的布伦特原油收盘价)。
  3. 将输入数据复制到矩阵A中(即从我们的预测期限加1开始的布伦特原油收盘价)。
  4. 对数据矩阵A进行重塑。
  5. 计算布伦特原油和西德克萨斯中质原油之间的价差,并将其添加到A中。
  6. A中添加一行全为1的元素,用于表示截距项。
  7. Ay进行转置。

完成这些步骤后,我们将检查输入数据是否有效。如果数据无效,我们将记录一条错误消息。如果数据有效,我们将继续计算x系数矩阵。

bool InitializeModel()
  {
//Try select the symbols
   if(SymbolSelect(brent,true) && SymbolSelect(wti,true))
     {
      Print("Symbols Available. Bars: ",max_bars," Fetch: ",fetch," Look ahead: ",look_ahead);
      //Get historical data on Brent , our model output
      y.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,1,fetch);
      //model input
      A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(1 + look_ahead),fetch);
      brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(1+look_ahead),fetch);
      wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,(1+look_ahead),fetch);
      //Calculate the spread
      spread = brent_price - wti_price;
      Print("The Current Spread: ",spread);
      A.Reshape(3,fetch);
      //Add the spread to the input matrix
      A.Row(spread,1);
      //Add a column for the intercept
      A.Row(intercept,2);
      //Reshape the matrices
      A = A.Transpose();
      y = y.Transpose();
      //Inspect the matrices
      if((A.Cols() == 0 || y.Cols() == 0))
        {
         Print("Error occured when copying historical data");
         Print("A rows: ",A.Rows()," y rows: ",y.Rows()," A columns: ",A.Cols()," y cols: ",y.Cols());
         Print("A");
         Print(A);
         Print("y");
         Print(y);
         return(false);
        }

      else
        {
         Print("No errors occured when copying historical data");
         x = A.PInv().MatMul(y);
         Print("Finished Fitting The Model");
         Print(x);
         return(true);
        }
     }

   Print("Faield to select symbols");
   return(false);
  }

最后,我们需要定义一个函数来预测布伦特原油收盘价的未来值。

double ModelForecast()
  {
   if(model_initialized)
     {
      //model input
      A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      //Calculate the spread
      spread = brent_price - wti_price;
      Print("The Spread: ",spread);
      A.Reshape(3,fetch);
      //Add the spread to the input matrix
      A.Row(spread,1);
      //Add a column for the intercept
      A.Row(intercept,2);
      //Reshape the matrices
      A = A.Transpose();
      double _forecast = (A[0][0]*x[0][0]) + (A[1][0]*x[1][0]) + (A[2][0]*x[2][0]);
      return(_forecast);
     }
   return(0);
  }

综上所述,这就是我们的应用程序所提供的全部功能。

//+------------------------------------------------------------------+
//|                                                     Brent EA.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//Libraries
#include  <Trade\Trade.mqh>
CTrade ExtTrade;
#include <TrailingStop\ATRTrailingStop3.mqh>
ATRTrailingStop ExtATRTrailingStop;

//Inputs
input double atr_multiple = 5.0;
input double lot_multiple = 1.0;
input double profit_target = 10;
input double max_loss = 20;
input int position_size = 2;

//Set this value between 0 and 1 to control how much data is used
double consumption = 0.01;
//We want to know which symbol has the least number of bars.
double brent_bars = (double) NormalizeDouble((iBars("UK Brent Oil",PERIOD_CURRENT) * consumption),0);
double wti_bars = (double) NormalizeDouble((iBars("WTI_OIL",PERIOD_CURRENT) * consumption),0);
//Select the lowest
double max_bars = (brent_bars < wti_bars) ? brent_bars : wti_bars;
//How far into the future are we forecasting
double look_ahead = NormalizeDouble((max_bars / 4),0);
//How many bars should we fetch?
int fetch = (int)(max_bars - look_ahead) - 1;
//Matrix A stores our inputs. y is the output. x is the coefficients.
matrix A = matrix::Zeros(fetch,6);
matrix y = matrix::Zeros(fetch,1);
vector wti_price = vector::Zeros(fetch);
vector brent_price = vector::Zeros(fetch);
vector spread;
vector intercept = vector::Ones(fetch);
matrix x = matrix::Zeros(6,1);
double forecast = 0;
double ask = 0;
double bid = 0;
double min_volume = 0;

string brent = "UK Brent Oil";
string wti = "WTI_OIL";
bool model_initialized = false;
int OnInit()
  {
//Initialise trailing stops
   if(atr_multiple > 0)
      ExtATRTrailingStop.Init(atr_multiple);
   min_volume = SymbolInfoDouble(brent,SYMBOL_VOLUME_MIN);
   return(INIT_SUCCEEDED);
//---
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   ask = SymbolInfoDouble(brent,SYMBOL_ASK);
   bid = SymbolInfoDouble(brent,SYMBOL_BID);
   if(model_initialized)
     {
      if(PositionsTotal() == 0)
        {
         forecast = 0;
         forecast = ModelForecast();
         InterpretForecast();
        }

      else
        {
         ManageTrades();
        }
     }

   else
     {
      model_initialized = InitializeModel();
     }

  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|This function closes trades if we reach our profit or loss limit  |                                                              |
//+------------------------------------------------------------------+
void ManageTrades()
  {
   if(AccountInfoDouble(ACCOUNT_PROFIT) > profit_target)
      CloseAll();
   if(AccountInfoDouble(ACCOUNT_PROFIT) < (-1 * max_loss))
      CloseAll();
  }

//+------------------------------------------------------------------+
//|This function judges if our model is giving a long or short signal|                                                                |
//+------------------------------------------------------------------+
void InterpretForecast()
  {
   if(forecast != 0)
     {
      if(forecast > iClose(_Symbol,PERIOD_CURRENT,0))
        {
         check_buy();
        }

      else
         if(forecast < iClose(_Symbol,PERIOD_CURRENT,0))
           {
            check_sell();
           }
     }
  }

//+------------------------------------------------------------------+
//|This function checks if we can open buy positions                  |
//+------------------------------------------------------------------+
void check_buy()
  {
   if(PositionsTotal() == 0)
     {
      for(int i = 0; i < position_size; i++)
        {
         ExtTrade.Buy(lot_multiple * min_volume,brent,ask,0,0,"BUY");
        }
     }
  }

//+------------------------------------------------------------------+
//|This function checks if we can open sell positions                |
//+------------------------------------------------------------------+
void check_sell()
  {
   if(PositionsTotal() == 0)
     {
      for(int i = 0; i < position_size; i++)
        {
         ExtTrade.Sell(lot_multiple * min_volume,brent,bid,0,0,"SELL");
        }
     }
  }

//+------------------------------------------------------------------+
//|This function will close all open trades                          |
//+------------------------------------------------------------------+
void CloseAll(void)
  {
   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetSymbol(i) == brent)
           {
            ulong ticket;
            ticket = PositionGetTicket(i);
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//|This function closes any open buy trades                          |
//+------------------------------------------------------------------+
void close_buy()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal(); i++)
        {
         ticket = PositionGetTicket(i);
         type = (int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_BUY)
           {
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//|This function closes any open sell trades                         |
//+------------------------------------------------------------------+
void close_sell()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal(); i++)
        {
         ticket = PositionGetTicket(i);
         type = (int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_SELL)
           {
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }


//+------------------------------------------------------------------+
//|This function initializes our model and fits it onto the data     |
//+------------------------------------------------------------------+
bool InitializeModel()
  {
//Try select the symbols
   if(SymbolSelect(brent,true) && SymbolSelect(wti,true))
     {
      Print("Symbols Available. Bars: ",max_bars," Fetch: ",fetch," Look ahead: ",look_ahead);
      //Get historical data on Brent , our model output
      y.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,1,fetch);
      //model input
      A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(1 + look_ahead),fetch);
      brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(1+look_ahead),fetch);
      wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,(1+look_ahead),fetch);
      //Calculate the spread
      spread = brent_price - wti_price;
      Print("The Current Spread: ",spread);
      A.Reshape(3,fetch);
      //Add the spread to the input matrix
      A.Row(spread,1);
      //Add a column for the intercept
      A.Row(intercept,2);
      //Reshape the matrices
      A = A.Transpose();
      y = y.Transpose();
      //Inspect the matrices
      if((A.Cols() == 0 || y.Cols() == 0))
        {
         Print("Error occured when copying historical data");
         Print("A rows: ",A.Rows()," y rows: ",y.Rows()," A columns: ",A.Cols()," y cols: ",y.Cols());
         Print("A");
         Print(A);
         Print("y");
         Print(y);
         return(false);
        }

      else
        {
         Print("No errors occured when copying historical data");
         x = A.PInv().MatMul(y);
         Print("Finished Fitting The Model");
         Print(x);
         return(true);
        }
     }

   Print("Faield to select symbols");
   return(false);
  }

//+------------------------------------------------------------------+
//|This function makes a prediction once our model has been trained  |
//+------------------------------------------------------------------+
double ModelForecast()
  {
   if(model_initialized)
     {
      //model input
      A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      //Calculate the spread
      spread = brent_price - wti_price;
      Print("The Spread: ",spread);
      A.Reshape(3,fetch);
      //Add the spread to the input matrix
      A.Row(spread,1);
      //Add a column for the intercept
      A.Row(intercept,2);
      //Reshape the matrices
      A = A.Transpose();
      double _forecast = (A[0][0]*x[0][0]) + (A[1][0]*x[1][0]) + (A[2][0]*x[2][0]);
      return(_forecast);
     }
   return(0);
  }
//+------------------------------------------------------------------+

我们现在已准备好使用内置的MetaTrader 5策略测试器对我们的交易算法进行回测。

测试我们的EA

图例7:量化交易算法的回溯测试

回测我们的EA

图例8:我们回测的历史收益率

结论

目前我们所探讨的策略仍有改进空间。例如,全球已知石油储量的67%都位于中东地区,但我们并未考虑任何波斯湾的原油基准价格。此外,还有其他一些具有预测性质且值得进一步研究的价差,比如裂解价差。裂解价差是衡量炼油厂盈利能力的指标。从历史上看,当裂解价差较高时,供应量往往会增加;而当裂解价差较低时,供应量则倾向于减少。如果您已经阅读至此,那么您应该能立刻明白裂解价差可能对原油价格产生的影响。

我们的策略是盈利的,但它容易受到不规则回落期的影响。石油市场以波动剧烈而著称,通过应用更加稳健且仍然盈利的风险管理原则,能够获得进一步的提升。

祝您平安、成功、交易盈利。


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

附加的文件 |
Brent_EA.mq5 (8.37 KB)
最近评论 | 前往讨论 (3)
linfo2
linfo2 | 7 6月 2024 在 22:13

再次感谢 Gamuchirai,这又是一篇非常有趣、文笔清晰、经过深思熟虑的文章,我想到了使用 MQL 图表模块:).很棒的东西,非常有趣的思考过程。为了帮助其他用户,我的经纪人使用 UKBENT 和 USWTI 作为符号,因此我需要修改脚本以适应(UK Brent Oil 和 WTI_OIL)。

我期待着详细测试和了解这一点。

Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 8 6月 2024 在 09:57
linfo2 #:

再次感谢 Gamuchirai,这又是一篇非常有趣、文笔清晰、经过深思熟虑的文章,我想到了使用 MQL 图表模块:).很棒的东西,非常有趣的思考过程。为了帮助其他用户,我的经纪人使用 UKBENT 和 USWTI 作为符号,因此我需要修改脚本以适应(UK Brent Oil 和 WTI_OIL)。

我期待着详细测试和了解这一点。

嘿,尼尔,很高兴听到你的消息,我很高兴能帮上忙。

你也在考虑使用图形模块的可能性有多大?

我很期待您的反馈和改进建议,如果您有任何想法的话。

附注:您也在关注澳元/日元吗?我想利用日元的基本面弱势做多。

Ahmad Kazemi
Ahmad Kazemi | 14 3月 2025 在 14:56
感谢您分享该策略。
我测试了代码,但在更新之前无法编译。但是,现在它在策略测试器中 无法打开任何交易。请再次查看代码。谢谢。
密码锁算法(CLA) 密码锁算法(CLA)
在本文中,我们将重新考虑密码锁,将它们从安全机制转变为解决复杂优化问题的工具。让我们探索密码锁的世界,不再将其视为简单的安全装置,而是作为优化问题新方法的灵感来源。我们将创建一整群“锁”,其中每把锁都代表问题的一个独特解决方案。然后,我们将开发一种算法来“破解”这些锁,并从机器学习到交易系统开发等多个领域中找到最优解。
为 MetaTrader 5 开发 MQTT 客户端:TDD 方法 - 最终篇 为 MetaTrader 5 开发 MQTT 客户端:TDD 方法 - 最终篇
本文是介绍我们针对 MQTT 5.0 协议的本机 MQL5 客户端的开发步骤系列文章的最后一部分。尽管该库尚未投入实际使用,但在此部分中,我们将使用我们的客户端来更新来自另一个经纪商的报价(或利率)的自定义交易品种。请参阅本文底部以获取有关该库的当前状态的更多信息、它与 MQTT 5.0 协议完全兼容所缺少的内容、可能的路线图以及如何关注和促进其发展。
理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统 理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统
学习面向对象的编程范式,及其在 MQL5 代码中的应用。这是第二篇文章,更深入地讲解面向对象编程的规范,并通过一个实际示例提供上手经验。您将学习如何运用 EMA 指标,和烛条价格数据,将我们早期开发的过程化价格行为智能系统转换为面向对象的代码。
构建K线图趋势约束模型(第三部分):在使用该系统时检测趋势变化 构建K线图趋势约束模型(第三部分):在使用该系统时检测趋势变化
本文探讨了经济新闻发布、投资者行为以及各种因素如何影响市场趋势的反转。文章包含一段视频解释,并接着将MQL5代码融入我们的程序中,以检测趋势反转、向我们发出警报,并根据市场条件采取相应行动。本文是在此前一系列文章基础上的扩展。