PythonとMQL5による多銘柄分析(第3回):三角為替レート
金融市場は本質的にノイズが多いものです。トレーダーがポジションを早まって取ってしまい、誤ったシグナルによって不要なドローダウンを被るのはよくあることです。この問題を踏まえて、様々な取引戦略や原則が開発されてきました。これらの原則の多くは、即座に行動するのではなく、他の確認手段や強さの兆候を探すようにトレーダーに助言します。
しかし、こうしたルールは確認シグナルを得るまでの時間的な制限がなく、時には「機会損失」となることもあります。こうした原則に従うトレーダーは、結局エントリーすることになる取引の多くにおいて、好条件のエントリーレベルを逃してしまう傾向があるのです。
このような状況を踏まえ、市場の強さをできる限りリアルタイムに測定できるような戦略の構築が必要であることは明らかです。これを実現するためには、関連市場を利用し、理にかなった予測可能なパターンを見出すことを目指すべきです。つまり、予測可能であると自分が信じる市場間パターンに注目することで、トレードのたびに無駄にpipsを失い、損失が蓄積してしまう事態を防ぐことができるのです。
市場間パターンの考え方にまだ慣れていない読者のために、簡単にその概念を紹介します。この考え方を理解すれば、なぜ一部のトレーダーが市場間パターンは予測可能であり、もし見極めることができれば非常に堅牢なものになると言うのかが分かるでしょう。
世界のコモディティのおよそ90%は米ドル建てで取引されていますが、中には非常に流動性の高い商品もあり、複数通貨で同時に価格が提示されているものもあります。
貴金属、特に銀は、その代表例であり、米ドル(XAGUSD)とユーロ(XAGEUR)の両方で同時に価格が提示されることがよくあります。本稿では銀を例に取ります。多くのブローカーでは、銀のドル建て価格(XAGUSD)とユーロ建て価格(XAGEUR)が同時に提示されているはずです。
ここで、XAGUSDを取引したいと思ったとき、その価格がXAGEURと本質的に連動しているという理解を持っていれば、どのようにその関係性を活かして、永遠に確認を待つことなく、取引の可否を判断することができるのでしょうか。
このとき、ユーロとドルの為替レート(EURUSD)を考慮に入れることで、ドル建て銀価格、ユーロ建て銀価格、そしてユーロ・ドルの公正な為替レートの間に、予測可能な三角裁定の関係を構築することができます。これによって、市場のノイズに強く、MetaTrader 5端末で受け取る通常の価格情報から「隠れた」市場センチメントを見抜くことができる、堅牢な取引戦略を読者に提供することが目標です。
取引戦略の概要
本戦略の詳細に入る前に、まずは読者全員が通貨ペアにおける「ベース(基軸通貨)」と「クオート(相手通貨)」の基本的な理解を持っていることを確認しておきたいと思います。たとえば、EURUSDを例に取ると、EUR(ユーロ)が「ベース通貨」となります。通貨ペアにおいて、左側に表示されている略称がベース通貨です。チャート上で為替レートが0から離れて上昇していく場合、それはベース通貨の価値が上昇していることを意味します。つまり、EURUSDのチャートが上昇しているとき、1ユーロを得るために、より多くの米ドルを支払う必要があるということになります。

図1:ペアのベースとクオートの違いを理解する
一方で、2つ目の通貨が「クオート通貨」です。クオート通貨の価値は、チャート上の為替レートが0に近づく(=下落する)につれて上昇していると解釈できます。つまり、EURUSDのチャートが下がっているときは、1ユーロを得るために必要な米ドルの量が減っていることを意味します。

図2:クオートに注目する
ここまでの説明で、すべての読者が基礎的な概念を理解できたことを前提に、いよいよ今回の具体的な取引戦略について見ていきましょう。この戦略では、3つの異なる市場を同時に観察しながら、実際に取引をおこなうのはそのうちの1つだけです。取引対象とするのはXAGUSDです。狙いは、XAGUSDの今後の方向性を予測するために、まずEURUSDの為替レートを確認することから始めます。
EURUSDの為替レートが上昇している状況は、ユーロが価値を増しており、同時にドルの価値が下落していることを意味します。このとき、ユーロの購買力は上がっているため、ユーロ建ての銀価格は相対的に割安に見える可能性があります。一方で、ドルは購買力を失っているため、ドル建ての銀価格は相対的に割高に見えることになります。
この思考実験によって、こうした取引戦略にまったく馴染みのない読者でも、特定の前提のもとで3つの市場が互いに「鏡」のように反応し合う可能性があるという考え方をつかんでいただけたのではないかと思います。
これら3つの市場の動きを同時に分析することで、必ずしも確認シグナルを待たずにエントリーできるような戦略を構築することが可能になります。もし市場同士が相互に連動しているという前提が正しければ、このアプローチは十分に実用的であり、読者が関心を持つ他の市場にも応用可能な戦略の枠組みとなるでしょう。
この取引戦略の視覚的なマインドマップを図4に示します。一般的に、EURUSDの為替レートが下落している場合、ユーロ建てで価格が表示される商品は高くなり、ドル建ての商品は安くなることが予想されます。この戦略では、EURUSDの為替レート、ユーロ建ての銀価格、そしてドル建ての銀価格を同時に観察し、こうした特定のパターンが存在するかどうかを確認します。そして、このクロスマーケットパターンが観測された場合には、ドル建ての銀価格(XAGUSD)をショートします。

図3:XAUUSDをショートするためのルール
同じルールは、ドル建ての銀価格をロングで狙いたい場合にも逆の方向で適用されます。すなわち、我々はFX市場でのドルの強さが、銀のヨーロッパ市場とアメリカ市場の両方における価格動向を裏付けていることを確認したいと考えます。市場間の連動が見られない、こうした裏付けのない値動きは、脆弱であり簡単に反転してしまう可能性があります。市場に対する仮定が正しければ、この戦略は理にかなっており、確認を「待つ必要性」をある程度代替する手法となり得ます。クロスマーケットの分析がポートフォリオ戦略と整合するのであれば、これは一つの有効なアプローチになるでしょう。

図4:XAGUSDのロングポジションを取るための取引ルール
読者は理解しておくべきですが、実際には銀の価格が上下する背景には、おそらく非常に多くの複雑な要因が絡んでいます。ここでおこなっているのは、そうした複雑な関係性をすべて捉えようとしているのではなく、より単純な関係性を通じて、それらを要約しようとしているに過ぎません。
バックテスト期間の概要
2023年11月1日から2025年1月1日までの期間で戦略のテストをおこないます。これに先立ち、2022年11月1日から2023年10月末までのデータは、アプリケーションの学習用データとして使用します。将来的にこの連載記事の改訂版では、市場の相互関係をより厳密に捉えるために、現在の単純なモデルに代わって、コンピュータが自律的に学習するモデルを導入する予定です。したがって、今回はこの学習用データを直接使用することはありませんが、今後の議論においては重要な役割を果たすことになります。

図5:議論のためのバックテスト期間
MQL5を始める
本戦略では、3つの市場を同時に追跡する必要があります。したがって、まずはシステム上で、私たちが関心を持つそれぞれの市場を管理するための定数を定義することから始めましょう。このような定数を用意することで、各市場間を簡単に切り替えたり、それぞれのパフォーマンスを比較したりすることが可能になります。
//+------------------------------------------------------------------+ //| Baseline Model.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System Constants | //+------------------------------------------------------------------+ #define SYMBOL_ONE "XAGUSD" //--- Our primary symbol, the price of Silver in USD #define SYMBOL_TWO "XAGEUR" //--- Our secondary symbol, the price of Silver in EUR #define SYMBOL_THREE "EURUSD" //--- Our EURUSD exchange rate. #define FETCH 24 //--- How many bars of data should we fetch? #define TF_1 PERIOD_H1 //--- Our intended time frame #define VOLUME SymbolInfoDouble(SYMBOL_ONE,SYMBOL_VOLUME_MIN) * 10 //--- Our trading volume
MQL5のベクトル型を用いて、市場データを取得し、それを素早く変換・処理していきます。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ vector eurusd,xagusd,xageur; double eurusd_growth,xagusd_growth,xageur_growth,bid,ask; double sl_width = 3e2 * _Point;
取引ライブラリを使用すると、取引を開いて管理することができます。今回の演習では、このライブラリが必要不可欠です。
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
MQL5言語は、効率的に取引をおこなうために設計されています。市場で起こりうるあらゆる出来事が、それぞれ特定のイベントに対応付けられています。たとえば、新しい価格を受け取ることもひとつのイベントです。このイベントが検知されると、OnTick()というイベントハンドラが呼び出されます。そして、そのハンドラ内にある関数が実行されます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our technical indicators setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- New prices have been quoted new_quotes_received(); } //+------------------------------------------------------------------+
新しいティックを受信するたびに、まず最初に新しいローソク足が形成されたかどうかを確認する必要があります。新しいローソク足が形成されている場合、その時点でシステム変数を更新し、取引の機会を探る処理をおこないます。
//+------------------------------------------------------------------+ //| Custom functions | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Updates system variables accordingly | //+------------------------------------------------------------------+ void new_quotes_received(void) { static datetime time_stamp; datetime time = iTime(SYMBOL_ONE,TF_1,0); if(time_stamp != time) { time_stamp = time; update(); } }
システムを初めて起動した際には、取引対象となるすべての市場が正しく有効化され、すぐに利用できる状態にあることを確認したいものです。
//+------------------------------------------------------------------+ //| Setup our technical indicators and select the symbols we need | //+------------------------------------------------------------------+ void setup(void) { //--- Select the symbols we need SymbolSelect(SYMBOL_ONE,true); SymbolSelect(SYMBOL_TWO,true); SymbolSelect(SYMBOL_THREE,true); }
新しいローソク足が形成されたら、追跡している各市場から最新の価格を読み込みます。市場の成長率を測るには、現在の価格を過去の価格で割る方法を使います。この割り算の結果が1未満であれば、市場の価値は下落していることを意味し、1以上であれば市場の価値が上昇していると判断できます。
//+------------------------------------------------------------------+ //| Update our system setup | //+------------------------------------------------------------------+ void update(void) { //--- Fetch updated prices xagusd.CopyRates(SYMBOL_ONE,TF_1,COPY_RATES_CLOSE,1,FETCH); xageur.CopyRates(SYMBOL_TWO,TF_1,COPY_RATES_CLOSE,1,FETCH); eurusd.CopyRates(SYMBOL_THREE,TF_1,COPY_RATES_CLOSE,1,FETCH); //--- Calculate the growth in market prices eurusd_growth = eurusd[0] / eurusd[FETCH - 1]; xageur_growth = xageur[0] / xageur[FETCH - 1]; xagusd_growth = xagusd[0] / xagusd[FETCH - 1]; //--- Update system variables SymbolSelect(SYMBOL_ONE,true); bid = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_BID); ask = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_ASK); //--- Check if we need to setup a new position if(PositionsTotal() == 0) find_setup(); //--- Check if we need to manage our positions if(PositionsTotal() > 0) manage_setup(); //--- Give feedback on the market growth Comment("EURUSD Growth: ",eurusd_growth,"\nXAGEUR Growth: ",xageur_growth,"\nXAGUSD Grwoth: ",xagusd_growth); }
私たちの取引設定は、市場同士の関係性において特定の構成パターンを見つけ出すことだとイメージできます。つまり、XAGUSD市場の値動きが、EURUSDおよびXAGEUR市場の動きによって裏付けられているかどうかを確認したいのです。以下に示すルールは、先に示した図4および図5のビジュアルイラストと同等の内容を表しています。
//+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { //--- Check if the current market setup matches our expectations for selling if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth < 1)) { Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } //--- Check if the current market setup matches our expectations for buying if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1)) { Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } }
ポジションを持った後は、利益を守るためにトレーリングストップロスを設定して取引を管理します。
//+------------------------------------------------------------------+ //| Manage setup | //+------------------------------------------------------------------+ void manage_setup(void) { //--- Select our open position if(PositionSelect(SYMBOL_ONE)) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); //--- Buy setup if(current_sl < current_tp) { if((bid - sl_width) > current_sl) Trade.PositionModify(SYMBOL_ONE,(bid - sl_width),(bid + sl_width)); } //--- Sell setup if(current_sl > current_tp) { if((ask + sl_width) < current_sl) Trade.PositionModify(SYMBOL_ONE,(ask + sl_width),(ask - sl_width)); } } }最後に、先ほど定義したシステム定数を未定義にします。
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef TF_1 #undef SYMBOL_ONE #undef SYMBOL_TWO #undef SYMBOL_THREE #undef VOLUME #undef FETCH
システムのすべてのコンポーネントを組み合わせると、アプリケーションが完成します。
//+------------------------------------------------------------------+ //| Baseline Model.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System Constants | //+------------------------------------------------------------------+ #define SYMBOL_ONE "XAGUSD" //--- Our primary symbol, the price of Silver in USD #define SYMBOL_TWO "XAGEUR" //--- Our secondary symbol, the price of Silver in EUR #define SYMBOL_THREE "EURUSD" //--- Our EURUSD exchange rate. #define FETCH 24 //--- How many bars of data should we fetch? #define TF_1 PERIOD_H1 //--- Our intended time frame #define VOLUME SymbolInfoDouble(SYMBOL_ONE,SYMBOL_VOLUME_MIN) * 10 //--- Our trading volume //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ vector eurusd,xagusd,xageur; double eurusd_growth,xagusd_growth,xageur_growth,bid,ask; double sl_width = 3e2 * _Point; //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our technical indicators setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- New prices have been quoted new_quotes_received(); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Custom functions | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Updates system variables accordingly | //+------------------------------------------------------------------+ void new_quotes_received(void) { static datetime time_stamp; datetime time = iTime(SYMBOL_ONE,TF_1,0); if(time_stamp != time) { time_stamp = time; update(); } } //+------------------------------------------------------------------+ //| Setup our technical indicators and select the symbols we need | //+------------------------------------------------------------------+ void setup(void) { //--- Select the symbols we need SymbolSelect(SYMBOL_ONE,true); SymbolSelect(SYMBOL_TWO,true); SymbolSelect(SYMBOL_THREE,true); } //+------------------------------------------------------------------+ //| Update our system setup | //+------------------------------------------------------------------+ void update(void) { //--- Fetch updated prices xagusd.CopyRates(SYMBOL_ONE,TF_1,COPY_RATES_CLOSE,1,FETCH); xageur.CopyRates(SYMBOL_TWO,TF_1,COPY_RATES_CLOSE,1,FETCH); eurusd.CopyRates(SYMBOL_THREE,TF_1,COPY_RATES_CLOSE,1,FETCH); //--- Calculate the growth in market prices eurusd_growth = eurusd[0] / eurusd[FETCH - 1]; xageur_growth = xageur[0] / xageur[FETCH - 1]; xagusd_growth = xagusd[0] / xagusd[FETCH - 1]; //--- Update system variables SymbolSelect(SYMBOL_ONE,true); bid = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_BID); ask = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_ASK); //--- Check if we need to setup a new position if(PositionsTotal() == 0) find_setup(); //--- Check if we need to manage our positions if(PositionsTotal() > 0) manage_setup(); //--- Give feedback on the market growth Comment("EURUSD Growth: ",eurusd_growth,"\nXAGEUR Growth: ",xageur_growth,"\nXAGUSD Grwoth: ",xagusd_growth); } //+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { //--- Check if the current market setup matches our expectations for selling if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth < 1)) { Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } //--- Check if the current market setup matches our expectations for buying if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1)) { Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } } //+------------------------------------------------------------------+ //| Manage setup | //+------------------------------------------------------------------+ void manage_setup(void) { //--- Select our open position if(PositionSelect(SYMBOL_ONE)) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); //--- Buy setup if(current_sl < current_tp) { if((bid - sl_width) > current_sl) Trade.PositionModify(SYMBOL_ONE,(bid - sl_width),(bid + sl_width)); } //--- Sell setup if(current_sl > current_tp) { if((ask + sl_width) < current_sl) Trade.PositionModify(SYMBOL_ONE,(ask + sl_width),(ask - sl_width)); } } } //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef TF_1 #undef SYMBOL_ONE #undef SYMBOL_TWO #undef SYMBOL_THREE #undef VOLUME #undef FETCH
冒頭で述べたように、バックテストは2023年11月1日から2025年1月までの期間でおこないます。テストにはH1(1時間枠)を使用します。H1には、日足などのより長い時間枠よりも多くの取引機会を提供してくれることが期待できる一方で、M1(1分足)のようなより短い時間枠で取引する際に感じるような過剰なノイズに惑わされにくいというメリットがあります。

図6:XAGUSD戦略のバックテストに使用する日付
取引条件は、実際の取引環境を忠実に再現することを目指しています。具体的には、過去の実際のティックデータに基づいて、ティックごとにランダムな遅延を設定します。これにより、過去に市場で経験されたリアルな取引状況をより正確にシミュレーションすることが可能になります。

図7:実際のティックを使用した取引条件は、提供される最も現実的な設定である
私たちの戦略は、利益が急増する期間と、その後に続く長期の損失期間が交互に現れるようなエクイティカーブを描きました。戦略自体は利益を生み出していますが、このままの形では安定性に欠けると言えます。利益期と損失期を「周期的に」行き来していることから、この周期的な挙動を見極めることができれば、戦略の安定性を高める助けになると考えられます。

図8:取引戦略によって生成されたエクイティカーブ
取引戦略を用いたバックテストの結果、システムは本来もっと高い利益を上げられた可能性があることが分かりました。3つの市場を活用して1つの市場をうまく取引するという目標は、十分に達成可能な範囲にあります。ただし、現在の取引ルールは見直しが必要かもしれません。将来的には、コンピュータがデータから自律的に生成するルールと比較・検証しながら、ルールを改善していくことが考えられます。これが、アルゴリズムの安定性に関する懸念を解決する糸口となるでしょう。

図9:取引戦略のパフォーマンスの詳細な分析
初期パフォーマンスの改善
最初の戦略には、まだ大きな改善の余地があります。今回は、不安定だった振る舞いを改善し、利益が出る期間と損失が続く期間が周期的に入れ替わる現象を抑え、より安定した結果を目指します。以下に、戦略のパフォーマンス向上のために加えた主な変更点を簡潔にまとめます。
| 提案された改善 | 目的・意図 |
|---|---|
| 追加確認のためのテクニカル指標の統合 | MetaTrader 5端末上の指標から得られるサポートを活用し、市場のノイズを大幅に減らして意思決定の遅れを軽減します。 |
| 追跡している各市場の個別の統計モデルの構築 | 市場の動きをモデル化することで、市場の方向性やボラティリティの変化を予測し、不確実な状況ではポジションサイズを適切に調整することが可能になります。 |
モデルの学習を始めるには、まず過去の市場データを取得し、学習用データとして準備する必要があります。 統計モデルを使う場合、どの入力変数の組み合わせが最適なモデルを生み出すかは初めから分からないため、できるだけ多くの特徴量を用意しておき、その後で効果の高いものに絞り込んでいくのが一般的な手法です。
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- File name string file_name = "XAGEUR XAGUSD EURUSD Triangular Exchange Rates.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time","XAGUSD Open","XAGUSD High","XAGUSD Low","XAGUSD Close","XAGEUR Open","XAGEUR High","XAGEUR Low","XAGEUR Close","EURUSD Open","EURUSD High","EURUSD Low","EURUSD Close","Open Squared","High Squared","Low Squared","Close Squared","Open Cubed","High Cubed","Low Cubed","Close Cubed","Open Squre Root","High Square Root","Low Square Root","Close Square Root","Open Growth","High Growth","Low Grwoth","Close Growth","O / H","O / L","O / C","H / L","Log Open Growth","Log High Grwoth","Log Low Growth","Log Close Grwoth","Sin H / L","Cos O / C"); } else { FileWrite(file_handle, iTime("XAGUSD",PERIOD_CURRENT,i), iOpen("XAGUSD",PERIOD_CURRENT,i), iHigh("XAGUSD",PERIOD_CURRENT,i), iLow("XAGUSD",PERIOD_CURRENT,i), iClose("XAGUSD",PERIOD_CURRENT,i), iOpen("XAGEUR",PERIOD_CURRENT,i), iHigh("XAGEUR",PERIOD_CURRENT,i), iLow("XAGEUR",PERIOD_CURRENT,i), iClose("XAGEUR",PERIOD_CURRENT,i), iOpen("EURUSD",PERIOD_CURRENT,i), iHigh("EURUSD",PERIOD_CURRENT,i), iLow("EURUSD",PERIOD_CURRENT,i), iClose("EURUSD",PERIOD_CURRENT,i), MathPow(iOpen("XAGUSD",PERIOD_CURRENT,i),2), MathPow(iHigh("XAGUSD",PERIOD_CURRENT,i),2), MathPow(iLow("XAGUSD",PERIOD_CURRENT,i),2), MathPow(iClose("XAGUSD",PERIOD_CURRENT,i),2), MathPow(iOpen("XAGUSD",PERIOD_CURRENT,i),3), MathPow(iHigh("XAGUSD",PERIOD_CURRENT,i),3), MathPow(iLow("XAGUSD",PERIOD_CURRENT,i),3), MathPow(iClose("XAGUSD",PERIOD_CURRENT,i),3), MathSqrt(iOpen("XAGUSD",PERIOD_CURRENT,i)), MathSqrt(iHigh("XAGUSD",PERIOD_CURRENT,i)), MathSqrt(iLow("XAGUSD",PERIOD_CURRENT,i)), MathSqrt(iClose("XAGUSD",PERIOD_CURRENT,i)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iOpen("XAGUSD",PERIOD_CURRENT,i+1)), (iHigh("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)), (iLow("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)), (iClose("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)), (iHigh("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)), MathLog10(iOpen("XAGUSD",PERIOD_CURRENT,i) / iOpen("XAGUSD",PERIOD_CURRENT,i+1)), MathLog10(iHigh("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)), MathLog10(iLow("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)), MathLog10(iClose("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)), (MathSin(iHigh("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i))), (MathCos(iOpen("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i))) ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
Pythonでデータを分析する
学習データが揃ったら、いよいよ統計モデルの構築に取りかかります。まず、必要なPythonライブラリをインポートします。
#Import libraries we need import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt
次にデータにラベル付けをおこないます。H1(1時間枠)を使って取引をおこなうため、ラベルは「24時間(=1営業日)における価格変動」と設定しましょう。
#Clean up the data LOOK_AHEAD = 24 data = pd.read_csv("../XAGEUR XAGUSD EURUSD Triangular Exchange Rates.csv") data["Target"] = data["XAGUSD Close"].shift(-LOOK_AHEAD) - data["XAGUSD Close"] data.dropna(inplace=True) data.reset_index(inplace=True,drop=True)
これは今回のプロセスにおいて最も重要なステップです。私たちのデータには、最初はバックテスト期間と重複する市場の価格データが含まれていますが、これは戦略の真の価値を正確に評価したい実務者にとって望ましくありません。
したがって、バックテスト期間と重複するすべての市場データを削除する必要があります。図6ではバックテスト期間が2023年11月1日から始まることが明確に示されており、図10では学習用データが2023年10月31日で終了していることを思い出してください。
#Drop the dates corresponding to our backtest _ = data.iloc[-((24 * 365) - 918):,:] #Keep the dates before our backtest data = data.iloc[:-((24 * 365) - 918),:] data

図10:統計モデルに使用する学習用データに将来の情報が漏洩していないことを確認する
XAGUSD市場とXAGEUR市場の成長を視覚化すると、両者のスプレッドが時間とともに拡大したり縮小したりしていることが分かります。これは、両市場の間に裁定取引の機会が存在する可能性を示唆しています。もし裁定取引の機会が一切なければ、赤線と緑線は歴史の始まりから現在に至るまで完全に重なり合い、決して離れることはないはずです。しかし実際にはそうなっておらず、両市場は一定期間分離した後に調整される、という明確なパターンが観察されます。
plt.title("Comparing XAGUSD & XAGEUR Growth") plt.plot((data['XAGUSD Close'] / data.loc[0,"XAGUSD Close"]) / (data['XAGUSD Close'].max() - data['XAGUSD Close'].min()),color="red") plt.plot((data['XAGEUR Close'] / data.loc[0,"XAGEUR Close"]) / (data['XAGEUR Close'].max() - data['XAGEUR Close'].min()),color="green") plt.ylabel("Commodity Growth") plt.xlabel("Time") plt.legend(["XAGUSD","XAGEUR"]) plt.grid()

図11:2つの金融市場を重ね合わせて視覚化すると、活用できる裁定取引の機会が明らかになる
入力とターゲットにラベル付けをおこないます。
X = data.iloc[:,1:-1].columns y = "Target"
データに対して勾配ブースティングツリーを適合させ、そのモデルをONNX形式でエクスポートする準備を進めます。勾配ブースティングツリーは、与えられたデータセット内での複雑な相互作用を検出する能力に優れていることで知られており、その強力なパターン認識機能を活かして、取引戦略をより効果的なものに強化することを目指します。
import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorTypea from sklearn.ensemble import GradientBoostingRegressor
それでは、関心のあるそれぞれの市場ごとに専用の統計モデルを構築していきましょう。これにより、誤ったブレイクアウトをフィルタリングする手助けになることが期待されます。各市場のデータに対してモデルを学習させ、そのモデルをONNX形式でエクスポートします。こうして得られた3つのモデルは、後ほどMetaTrader 5のアプリケーションにインポートして活用できるようにします。
まず最初に、XAGUSD市場のモデルをレンダリングすることから始めましょう。
model = GradientBoostingRegressor() model.fit(data.loc[:,["XAGUSD Open","XAGUSD High","XAGUSD Low","XAGUSD Close"]],data.loc[:,y]) initial_types = [("float_input",FloatTensorType([1,4]))] xagusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) onnx.save(xagusd_model_proto,"../XAGUSD State Model.onnx")
XAGEUR市場が続きます。
model = GradientBoostingRegressor() model.fit(data.loc[:,["XAGEUR Open","XAGEUR High","XAGEUR Low","XAGEUR Close"]],data.loc[:,"XAGEUR Target"]) initial_types = [("float_input",FloatTensorType([1,4]))] xageur_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) onnx.save(xageur_model_proto,"../XAGEUR State Model.onnx")
最後に、EURUSD市場の統計モデルをエクスポートします。
model = GradientBoostingRegressor() model.fit(data.loc[:,["EURUSD Open","EURUSD High","EURUSD Low","EURUSD Close"]],data.loc[:,"EURUSD Target"]) initial_types = [("float_input",FloatTensorType([1,4]))] eurusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) onnx.save(eurusd_model_proto,"../EURUSD State Model.onnx")
システムは、3つすべてのモデルによって裏付けられた市場の動きに対して、より大きな重みを与えます。まずは図3と図4で示したパターンが市場で形成されていることを確認し、その後に3つのモデルがそれらのパターンがすぐに消え去るのではなく、時間をかけて持続すると予測することを待ちます。これにより、私たちが期待する市場の動きを支える強さについて、合理的な自信の尺度を得ることができます。
MQL5での改善点の実装
いよいよ、元の取引戦略に対して改良を加えていきます。まずは、追加のテクニカル指標を導入し、コンピュータがより正確にトレンドを捉えられるようにサポートします。移動平均線のクロスオーバーは、その目的に適した代表的な戦略ですが、今回は取引シグナルの遅延を最小限に抑えた、より応答性の高いバージョンを使用します。この移動平均線クロスオーバー戦略の詳細に興味がある方は、こちらでさらに学ぶことができます。
#define XAGUSD_MA_PERIOD 8
先ほど構築したONNXモデルをシステムリソースとして読み込みます。
//+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\XAGUSD State Model.onnx" as uchar xagusd_onnx_buffer[] #resource "\\Files\\XAGEUR State Model.onnx" as uchar xageur_onnx_buffer[] #resource "\\Files\\EURUSD State Model.onnx" as uchar eurusd_onnx_buffer[]
導入する移動平均インジケーターに対応する新しいグローバル変数と、ONNXモデルを完成させるために必要な個々のコンポーネントが必要になります。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ vector eurusd,xagusd,xageur; double eurusd_growth,xagusd_growth,xageur_growth,bid,ask; double sl_width = 3e2 * _Point; int xagusd_f_ma_handler,xagusd_s_ma_handler; double xagusd_f[],xagusd_s[]; vectorf model_output = vectorf::Zeros(1); long onnx_model; vectorf xageur_model_output = vectorf::Zeros(1); long xageur_onnx_model; vectorf eurusd_model_output = vectorf::Zeros(1); long eurusd_onnx_model;
いくつかの関数は、新たな要件に合わせてリファクタリングが必要です。まず最初に、取引アプリケーションの使用が終了した際には、ONNXモデルや2つの移動平均線に割り当てられたリソースを安全に解放する処理を実装する必要があります。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- OnnxRelease(onnx_model); OnnxRelease(xageur_onnx_model); OnnxRelease(eurusd_onnx_model); IndicatorRelease(xagusd_f_ma_handler); IndicatorRelease(xagusd_s_ma_handler); Print("System deinitialized"); }
さらに、セットアップ手順にも手を加える必要があります。ONNXモデルやテクニカル指標が新たに導入されたため、それらが正しく読み込まれ、正しく指定されているかどうかを検証する処理を追加しなければなりません。もし、指定したモデルやインジケーターの読み込みに失敗した場合には、初期化処理を中断し、ユーザーに何が問題だったのかを明確に報告する必要があります。
//+------------------------------------------------------------------+ //| Setup our technical indicators and select the symbols we need | //+------------------------------------------------------------------+ bool setup(void) { //--- Select the symbols we need SymbolSelect(SYMBOL_ONE,true); SymbolSelect(SYMBOL_TWO,true); SymbolSelect(SYMBOL_THREE,true); //--- Setup the moving averages xagusd_f_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,0,MODE_SMA,PRICE_OPEN); xagusd_s_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,0,MODE_SMA,PRICE_CLOSE); if((xagusd_f_ma_handler == INVALID_HANDLE) || (xagusd_s_ma_handler == INVALID_HANDLE)) { Comment("Failed to load our technical indicators correctly. ", GetLastError()); return(false); } //--- Setup our statistical models onnx_model = OnnxCreateFromBuffer(xagusd_onnx_buffer,ONNX_DEFAULT); xageur_onnx_model = OnnxCreateFromBuffer(xageur_onnx_buffer,ONNX_DEFAULT); eurusd_onnx_model = OnnxCreateFromBuffer(eurusd_onnx_buffer,ONNX_DEFAULT); if(onnx_model == INVALID_HANDLE) { Comment("Failed to create our XAGUSD ONNX model correctly. ",GetLastError()); return(false); } if(xageur_onnx_model == INVALID_HANDLE) { Comment("Failed to create our XAGEUR ONNX model correctly. ",GetLastError()); return(false); } if(eurusd_onnx_model == INVALID_HANDLE) { Comment("Failed to create our EURUSD ONNX model correctly. ",GetLastError()); return(false); } ulong input_shape[] = {1,4}; ulong output_shape[] = {1,1}; if(!(OnnxSetInputShape(onnx_model,0,input_shape))) { Comment("Failed to specify XAGUSD model input shape. ",GetLastError()); return(false); } if(!(OnnxSetInputShape(xageur_onnx_model,0,input_shape))) { Comment("Failed to specify XAGEUR model input shape. ",GetLastError()); return(false); } if(!(OnnxSetInputShape(eurusd_onnx_model,0,input_shape))) { Comment("Failed to specify EURUSD model input shape. ",GetLastError()); return(false); } if(!(OnnxSetOutputShape(onnx_model,0,output_shape))) { Comment("Failed to specify XAGUSD model output shape. ",GetLastError()); return(false); } if(!(OnnxSetOutputShape(xageur_onnx_model,0,output_shape))) { Comment("Failed to specify XAGEUR model output shape. ",GetLastError()); return(false); } if(!(OnnxSetOutputShape(eurusd_onnx_model,0,output_shape))) { Comment("Failed to specify EURUSD model output shape. ",GetLastError()); return(false); } Print("System initialized succefully"); //--- If we have gotten this far, everything went fine. return(true); }
また、ONNXモデルから予測値を取得するための専用の関数も必要になります。この関数では、まずモデルに渡す入力データを適切な形式で準備し、その後にOnnxRun関数を呼び出してモデルから予測を取得します。
//+------------------------------------------------------------------+ //| Fetch a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { vectorf model_inputs = { (float) iOpen(SYMBOL_ONE,TF_1,1), (float) iClose(SYMBOL_ONE,TF_1,1)}; OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output); Print(StringFormat("Model forecast: %d",model_output)); }
取引のセットアップを見つける手順も見直す必要があります。まず最初のステップは、ONNXモデルから予測値を取得することです。その予測が完了したら、次に移動平均線クロスオーバーから追加の確認を取ります。私たちが使用する移動平均クロスオーバー戦略では、一方の移動平均を始値に、もう一方を終値に基づいて計算しています。両方の移動平均は同じ期間を使います。始値に基づく移動平均が終値に基づく移動平均の上にある場合、それはショートシグナルと解釈します。そうでない場合はロングシグナルと解釈します。
最終的な条件としては、勾配ブースティング回帰モデルが、移動平均線から観測されたシグナルと一致する価格の動きを予測しているかどうかを確認します。この条件が満たされる場合、そのセットアップは高い確率で成功すると判断し、ロットサイズを2倍にします。そうでない場合は、より慎重に取引を実行します。
//+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { model_predict(); //--- Check if the current market setup matches our expectations for selling if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth < 1)) { if(xagusd_s[0] < xagusd_f[0]) { if(model_output[0] < 0) { //--- If all our systems align, we have a high probability trade setup Trade.Sell(VOLUME * 2,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } //--- Otherwise, we should trade conservatively Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } } //--- Check if the current market setup matches our expectations for buying if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1)) { if(xagusd_s[0] > xagusd_f[0]) { if(model_output[0] > 0) { Trade.Buy(VOLUME * 2,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } } }
先ほどおこなったのと同じテストを、今度は改良された取引戦略を用いて繰り返してみましょう。改めて思い出していただきたいのは、今回のバックテストでは、統計モデルの学習に使用した期間と一切重複しない日付範囲を使用しているという点です。

図12:両方のテストで同じテスト期間を使用し、統計モデルではこれまでこのデータは扱われていない
予想どおり、両方の取引戦略を公平に比較するために、すべての設定は一定に保たれます。

図13:理想的には、これらの設定は、実行する両方のテストで同じにしておく必要がある
それでは、得られた結果を分析してみましょう。初回のバックテストでは、シャープレシオが0.14であったのに対し、改良された戦略では1.85というシャープレシオを記録しました。これは非常に大きな改善であり、リスクを適切に管理しながら利益性を高めることに成功したことを示しています。一般に、シャープレシオが低い場合は、リターンが小さいにもかかわらず変動が大きくなることが多いです。
さらに、平均損失は取引あたり約115ドルから約109ドルに減少し、対して平均利益は約188ドルから約213ドルに増加しました。これは私たちにとって、取引ごとの期待値が改善されているという前向きな結果です。総利益についても、戦略の初期バージョンでは395ドルだったのに対し、改良バージョンでは1,449ドルに増加しています。しかもこの成果は、手動で設定したルールベースの戦略よりも少ない取引回数で達成されている点に注目すべきです。

図14:XAGUSD市場における取引戦略の過去のパフォーマンスの詳細な要約
今回システムに加えた変更は、初期バージョンの戦略で見られた不安定な残高の大きな揺れを改善する効果がありました。バックテストの結果によれば、新しい戦略は1回の取引ごとに損失よりも利益を上げる傾向が強いことがわかります。その結果、新しい損益曲線では、損失が発生する期間の谷が浅くなっており、初期のよりリスクの高い戦略で見られた深いドローダウンとは対照的です。

図15:改訂版の取引戦略によって生成された損益曲線
結論
本記事を読み終えた読者は、相互に関連する市場を対象としたアルゴリズム戦略を得ることができたはずです。読者は、自身の市場に関する知識を活かしながら、三角関係にある市場を発見し、それらを収益に結びつける手法についての理解を深められたことでしょう。統計モデルを活用することで、読者は自身が求めている具体的な取引セットアップをより正確に特定することが可能になります。また、相互に関連する市場構造を活かすことで、私たちはリアルタイムで本当の市場の強さを示すインジケーターを作り出すことができます。
| 添付ファイル名 | 詳細 |
|---|---|
| Baseline_Model | 三角裁定取引戦略の初期バージョン |
| Second Version | より収益性の高い改訂バージョン |
| EURUSD State Model | EURUSD市場の統計モデル |
| XAGEUR State Model | XAGEUR市場の統計モデル |
| XAGUSD State Model | XAGUSD市場の統計モデル |
| Triangular Exchange Rates | 市場データを分析し、市場の統計モデルを構築するために使用したJupyter Notebook |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17258
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5入門(第13回):初心者のためのカスタムインジケーター作成ガイド(II)
外国為替平均回帰戦略のためのカルマンフィルター
HarmonyOS NEXTデバイスにMetaTrader 5などのMetaQuotesアプリをインストールする
MQL5での取引戦略の自動化(第10回):トレンドフラットモメンタム戦略の開発
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索