English Deutsch
preview
古典的戦略の再構築:原油

古典的戦略の再構築:原油

MetaTrader 5 | 8 7月 2024, 12:47
712 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

はじめに

石油は地球上で最も重要な商品です。原油はそのままでは役に立ちませんが、精製されると、農業のような単純なものから医薬品のような複雑なものまで、さまざまな産業で利用されます。石油は、あらゆる産業で真に需要のある数少ない商品のひとつです。原油価格は、世界の生産水準と経済成長水準を示す重要な計量指標です。

世界の原油取引は、北米の指標であるWTI(ウエスト・テキサス・インターミディエート)と、世界の大半の原油の相場に使用されるブレントという2つの指標に支配されています。

この議論では、古典的な原油スプレッド取引戦略を再検討し、アルゴリズムが支配する現代の原油市場において、この古典的な戦略をより使いやすくする最適な機械学習戦略を見つけることができることを期待します。

まず、前述の2つの石油ベンチマークの違いを強調することから議論を始めましょう。ここからは、MQL5でブレントとWTIのスプレッドを可視化し、古典的なスプレッド取引戦略について説明します。これにより、ウェスト・テキサス・インターミディエートとブレント原油価格のスプレッドを教師付き機械学習で学習し、価格変動の先行指標を発見する方法を実証します。この記事を読めば、以下のことをしっかりと理解できるでしょう。

  • ブレントベンチマークとWTIベンチマークの違いと、これらのベンチマークがなぜ重要なのか。
  • MQL5の行列関数とベクトル関数を使用して、メンテナンスと実装が容易なコンパクトな機械学習モデルをゼロから構築する方法。
  • WTI-ブレントスプレッドを用いて、ブレント相場の将来価格を予測するための最小二乗解を求める擬似逆行法の採用方法。

世界の原油ベンチマークブレント

地中から抽出された原油は酸素、炭素、水素、硫黄の不純物の混合物です。ブレントは、軽質で甘いとされる原油のブレンドに与えられる分類です。甘くあるためには、硫黄不純物の濃度が低くなければなりません。また、密度が低いので軽いと呼ばれます。これらの特性は、ブレンドの精製が容易であることを知らせてくれるので望ましいものです。最後に強調したいブレントの品質は、ブレントは WTI よりも品質が低いということです。ブレントは主に北海で採掘されます。ブレントは抽出後、大型石油タンカーの樽に簡単に保管できるため、WTI原油よりもアクセスしやすく、WTI原油よりも明らかに有利です。ブレントは現在、WTIに対してプレミアムで取引されています。

ブレント価格

図1:MQL5のブレント価格推移

北米原油ベンチマーク:ウェスト・テキサス・インターミディエイト

WTI(ウェスト・テキサス・インターミディエート)とは、原油の特定のブレンドに与えられる分類で、「ライト スイート」でなければなりません。WTIは全米で採掘されていますが、主にテキサス州で採掘されています。WTIはブレントよりも甘く、軽いです。つまり、完成品に精製するのがより容易です。歴史的には、アメリカの内陸部で採掘されていたため、ブレントと比べてアクセスしづらくなっていました。しかし、メキシコ湾岸への大規模な投資と2015年の石油輸出禁止措置の廃止により、WTIはかつてないほど入手しやすくなりました。

ウェスト・テキサス・インターミディエイト

図2:MQL5におけるWTIの過去の価格

はじめに:スプレッドを視覚化する

手始めに、2つの商品のスプレッドを視覚化する便利なスクリプトを作成しましょう。MQL5グラフィックライブラリを使えば、どんな関数でも簡単にプロットすることができます。グラフィックライブラリは、あると便利なスケーリングを管理してくれます。グラフィックライブラリをインクルードすると、「consumption」という変数が定義されていることに気づくでしょう。この変数は、半分、4分の1、あるいは利用可能な全データの何分の1かを簡単に選択するのに役立ちます。

2つの異なる資産の履歴データを要求していることを考えると、それぞれの市場で利用可能なバーの合計数を知る必要があります。そこから、利用可能なバーの最小数を利用可能なバーの総数と仮定します。適切な数のバーを選択するために三項演算子を使用します。

使用するバーの本数が決まったら、スプレッドをプロットしましょう。

//+------------------------------------------------------------------+
//|                                             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」の切片を使用します。次に、Aの各列に1つの係数を持つ別の列ベクトルxを構築します。この値は市場データから直接計算され、モデルによって将来の価格を予測するために使用されます。

Aとxの定義

図4:最小二乗問題の枠組み

入力行列Aを作成した後、ブレントの終値がAの各入力と対になっていることを知る必要があります。出力価格をベクトルyに格納します。ゴールは、私たちが持っているすべての訓練観測において可能な限り誤差を小さくしながら、入力データ行列Aを出力データベクトルyに写像する方法を見つけることです。この問題の答えは最小二乗解と呼ばれます。

最小二乗解法の導入

図5:出力ベクトルy

最小二乗問題のシナリオには多くの有効な解決策があります。以下では、擬似逆行列法として知られるテクニックを取り上げます。擬似逆行列法は、非正方行列を反転させることができる線形代数の特徴量的な概念です。擬似逆行列法を用いて、Ayに写像する列xの係数値を、可能な限り誤差を少なくして求めます。 

ムーア-ペンローズ擬似逆解法

図6:擬似逆解法の導入

上記の2つの方程式は、まず、予測値 A * xと実際のブレント終値yとの誤差を最小化するxの値を探していることを教えてくれます。Ax-yを囲む二重の縦線にご注目ください。この二重の縦線はL2ノルムを表しています。現実の世界で物理的な物体を扱うとき、私たちはその大きさについて尋ねることができます。しかし、ベクトルや行列の大きさを知りたいときは、そのノルムを求めます。ノルムを計算する方法はいろいろありますが、L1ノルムやL2ノルムがよく使われます。この議論では、L2ノルムだけを考えることにします。

L2ノルムは、ベクトル内の各エンティティを2乗し、すべての2乗値を合計し、合計の平方根を計算することによって計算されます。ユークリッドノルムとも呼ばれます。もっと簡単な言い方をすれば、「モデルが作るすべての誤差の大きさを小さくするxの値を探している」のであり、もっと専門的な言い方をすれば、「残差のL2ノルムを最小化するxの最適値を見つける」のです。

制約を満たすxの値をx*とします。x*を求めるには、Ayの擬似逆数の内積を計算します。線形代数の練習でない限り、擬似逆関数を自分で実装する必要があることはほとんどないでしょう。それ以外の場合は、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関数を使用します。CopyRates関数は、指定された順序で以下のパラメータを必要とします。

  • Symbol name:取引する銘柄の名前
  • Timeframe:リスクレベルに見合った時間枠
  • Rates mask:これは、コピーする価格を指定するもので、例えば、必要であれば、始値だけを選択することができます。
  • From:データのコピー開始日。入力データと出力データの間にギャップがあり、入力データがより早い日付から始まることを保証します。
  • Count:コピーするローソク足の数

入力データ行列Aを設定した後、出力データ行列yについてこのプロセスを繰り返します。次に、両行列を再形成して、実行する操作に対して適切なサイズと互換性があることを確認します。

次に、x列のベクトルにAとyから得た値を入れます。幸いなことに、MQL5のAPIは行列演算の連鎖をサポートしており、1行のコードで擬似逆解を計算することができます。完了したら、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;

その後、取引ポジションのサイズとリスクパラメータを定義します。最初の入力は、各ポジションが最小ロットの何倍になるかを決定します。2番目の入力は、すべてのポジションが決済される利益水準を設定します。続いて、この口座で許容される引き出し総額を制限する入力パラメータが続きます。そして最後に、取引をおこなうたびにいくつのポジションを建てるかを設定します。

//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);

ここで、実際に利用可能なバーの数が少ない市場を決定し、そのバーの数をリミットとして使用します。このステップを省略した場合、ブローカーが両資産の過去の価格について均等に一致したデータセットを保証しない限り、2つの市場の日付が一致しない可能性があります。「Look Ahead」は、予測のホライズン、つまり何ステップ先の未来を予測しているかということ。

//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の値を表します。

ムーア-ペンローズ擬似逆解法

図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;

取引したい銘柄の名前を格納するために、2つの文字列変数を定義します。これを完了すると、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関数に取り組みます。本体内部では、まず、追跡しているBidとAskの価格を更新します。モデルが初期化されていなければ、訓練して適合させ、初期化されていれば、未決済のポジションがあるかどうかを確認します。未決済のポジションがない場合は、モデルから予測を得て、モデルが予測している方向に取引をおこないます。未決済のポジションがある場合、ポジションが利益目標または最大ドローダウンレベルを超えていないかどうかを確認します。

//+------------------------------------------------------------------+
//| 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を使用してブレントと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);
           }
        }
     }
  }

ここでは、ロングポジションとショートポジションをそれぞれ決済するための2つのメソッドを定義します。前回同様、全ポジションを反復処理し、各ポジションのチケットを取得します。そして、ポジションのタイプが探しているタイプと一致するかどうかを検証します。すべてが成功すれば、ポジションをクローズします。

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本目から)にコピーします。
  3. 入力データを行列Aにコピーします(1+予測機関のブレントの終値)
  4. データ行列Aを再形成します。
  5. ブレントとWTIのスプレッドを計算し、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)
独自のLLMをEAに統合する(第3部):CPUを使った独自のLLMの訓練 独自のLLMをEAに統合する(第3部):CPUを使った独自のLLMの訓練
今日の人工知能の急速な発展に伴い、言語モデル(LLM)は人工知能の重要な部分となっています。私たちは、強力なLLMをアルゴリズム取引に統合する方法を考える必要があります。ほとんどの人にとって、これらの強力なモデルをニーズに応じて微調整し、ローカルに展開して、アルゴリズム取引に適用することは困難です。本連載では、この目標を達成するために段階的なアプローチをとっていきます。
予測による統計的裁定取引 予測による統計的裁定取引
統計的裁定取引について調べ、共和分で相関する銘柄をPythonで検索し、ピアソン係数の指標を作成し、PythonとONNX モデルで予測をおこなって統計的裁定取引を行うEAを作成します。
MQL5取引ツールキット(第1回):ポジション管理EX5ライブラリ MQL5取引ツールキット(第1回):ポジション管理EX5ライブラリ
MQL5で様々なポジション操作を管理するための開発者用ツールキットの作成方法をご紹介します。この記事では、MQL5でポジション管理タスクを処理する際に発生するさまざまなエラーの自動処理とレポートも含め、簡単なものから高度なものまでポジション管理操作を実行する関数ライブラリ(ex5)の作成方法を紹介します。
予測による三角裁定取引 予測による三角裁定取引
この記事では、三角裁定を簡略化し、市場に慣れていない方でも、予測や専用ソフトを使用してより賢く通貨を取引する方法をご紹介します。専門知識を駆使して取引する準備はできていますか?