MQL5での定量分析:有望なアルゴリズムの実装
金融市場における定量分析とは
金融市場における定量分析とは何でしょうか。定量分析は、機械学習の前身として登場したもので、実際には統計学習の一部です。コンピュータが登場し始め、部屋全体を占めてパンチカードで動いていた頃、進歩的な頭脳はビッグデータや統計の分析にコンピュータを適応させようとしていました。当時は、統計的な操作や価格計算が可能な関数の種類は極めて少なく、関数自体も単純で、パターンも特に複雑なものではありませんでした。
これらの研究は、データ中の特定の関係(ほとんどが線形)を特定するための単純な計算でした。
金融市場における定量分析の最もシンプルで習得しやすい方法は、関連資産間のスプレッドの分析です。例えば、相関のある2つの資産間のスプレッドをプロットし、定量分析を使用してこのスプレッドの平均値、最大値、中央値偏差を求めることができます。データを定量的に記述することで、一方の資産が他方の資産からどれだけ乖離しているかを理解し、両資産間の乖離が解消されたとき(両資産が互いに近づくとき)に、両資産が確実にリターンを得る均衡状態を大まかに理解することができます。一般的に、ペアトレードにおける定量分析の活用は非常に興味深いトピックです。この点については今後の記事で必ず触れます。
ヘッジファンドが定量分析を活用する方法
定量分析の最初の試みはエドワード・O・ソープの実践で、彼は1970年代に株式とその株式のワラントのスプレッドを分析し、資産がワラントに対してどれだけ割高か割安かを計算しました。当時のソープのコンピュータは部屋全体を占め、パンチカードで動いていました。エドワード・O・ソープは、一般的にコンピュータによる定量分析を金融市場に応用した最初の人物です。当時としては画期的なことで、世界中に認められました。ソープは世界初の「クオンツ系」ヘッジファンドを創設しました。
おわかりのように、株式市場における定量分析の例としてまず思い浮かぶのは、ペア取引やバスケット取引での応用です。これらのオプションは必ず検討しますが、今日の定量分析アルゴリズムは別の原則に基づいています。
主要な市場参加者は定量分析を他にどのように利用しているのでしょうか。
統計的裁定取引によって、異なる市場や異なる時点における金融商品の価格の違いを検出することができます。これにより、ファンドは様々な関連市場において収益性の高い取引機会を特定し、それを利用することができます。さらに、定量的モデルは、ヘッジファンドが統計データに基づいて将来の市場の動きを予測し、十分な情報に基づいた取引の意思決定をおこなうのに役立ちます。
リスク管理もまた、定量分析の極めて重要な応用です。ヘッジファンドは、ポートフォリオを評価し、リスクを管理するためにモデルを使用します。リスクに応じて資産構成を最適化し、潜在的な損失を最小限に抑えます。例えば、マーコウィッツ・ポートフォリオ理論によるポートフォリオの最適化(ポートフォリオの乖離が潜在的利益を超えないようにリスクに基づいている)や、VaRシステムによるリスク管理などです。後者はドローダウンを計算できるユニークなモデルで、99%の確率でそれを超えることはありません。
もちろん、現実の市場は数学で説明するのが難しいこともあるので、否定的な例もあります。1998年のLTCMヘッジファンドは、そのポジションが大きな損失をもたらすことはないと計算し、定量的分析に基づいて長期債と短期債のスプレッドをターゲットにした裁定取引戦略に入りました。ロシアはデフォルトに陥り、アジアは危機に陥り、その結果、バタフライ効果によって米国国債市場はパニックに陥りました。LTCMファンドは、スプレッドが異常に高く、価格は間違いなく反対方向に「ロールバック」し、ファンドのポジションは間違いなく利益で決済されることを示唆するモデルを使用しました。
その結果、ファンドは平均法を適用し、極めて積極的にレバレッジを大きくし、資産で負債を積み上げ、爆発したのですが、同社のスタッフにはノーベル賞受賞者がいて、そのような結果はあり得ないと語っていました。VaRと題されたある定量分析モデルが米国市場全体をほぼ壊滅させたときがそうでした。FRBのアラン・グリーンスパン議長は、ファンドの限界ポジションを買い取るため、緊急にアメリカの大手銀行のトップに促さなければなりませんでした。そうでなければ、これほど巨大な資産プールを「市場に」売却することで、アメリカの株式市場は即座にリセットされ、大恐慌よりもひどいパニックが起こっていたでしょう。
したがって、定量分析やあらゆる指標の平均化をおこなう際には、正規確率分布の尾について覚えておくことが重要です。金融市場の場合、ベル型の確率曲線には「太い尾」があり、これは「ブラック・スワン」とも呼ばれる重大な乖離を反映しています。一方では、統計的にその可能性は極めて低いですが、他方では、こうした出来事の規模と威力は、投資家のポートフォリオやヘッジファンドのポートフォリオを破壊し、限界的なポジションを排除し、市場を破壊し、新しいサイクルごとに市場を変える可能性があります。これは、1998年、2008年、2020年、そして2022年に起こりました。さらに、このようなことは今後何度も起こるでしょう。
定量分析はヘッジファンドに多くのものを与え、日々の業務で常に活用されています。しかし、何百万人もの人々の決断やパニック、ある出来事に対する反応を計算できるような機能は存在しないことを忘れてはならありません。また、正規分布の尾について覚えておくことも重要です。それによって、積極的な取引戦術を用いた場合、保証金が台無しになる可能性があります。
アルゴリズムの基礎:動きの波を数える
私たちのアイデアの基礎は、トレーダーのArtem Zvezdinによって最初に表現されました。彼は、資産がそれ自体と比較してどの程度過大評価または過小評価されているかを理解するために、価格変動の波の大きさを計算します。例えば、過去500から5000のバーで強気と弱気の波を数え、それぞれの小さなサイクルで価格がどこまで動いたかを理解します。値動きの各サイクルは、誰かのポジション、誰かの資金、売り買いの判断を反映しています。新しいサイクルは、市場の新しい誕生と死です。ロールバックのない値動きを、上から下へ分析するという考え方を用います。これは、ほぼ同じ行動をとる別の参加者の集合であるため、サイクルの長さは常にほぼ同じになるという仮説を建てます。MetaTrader 5の標準端末パッケージに含まれているZigZagインジケータを使用して、平均値動きを計算します。
この記事の一部として作成したエキスパートアドバイザー(EA)を見てみましょう。まず、EAのヘッダー部分を見てみましょう。ここでの設定はいたって簡単です。取引には標準的なTradeライブラリを使用します。ロット設定には、固定ロットで取引するロット指定と、残高値からロット計算するロット指定があります。終値の利益が0より大きい場合、EAは合計利益に基づいて取引を終了します。損切りと利食いはATR値に基づいて計算されます。つまり、商品の現在のボラティリティに依存します。EAの計算のためのジグザグの設定は、一般的に標準的なものです。また、EAテンプレートは多通貨で、様々な資産に対応できることにご注意ください。将来のEAバージョンで、関連資産のバスケット取引によって全体的なリスクを減らすために、これが必要なのです。現在のバージョン0.90では、1つの銘柄でしか動作しません。
//+------------------------------------------------------------------+ //| QuantAnalysisSample.mq5 | //| Copyright 2023 | //| Evgeniy Koshtenko | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, Evgeniy Koshtenko" #property link "https://www.mql5.com" #property version "0.90" #property strict #include <Trade\Trade.mqh> #include <Graphics\Graphic.mqh> #include <Math\Stat\Normal.mqh> #include <Math\Stat\Math.mqh> CTrade trade; //--- Inputs input double Lots = 0.1; // lot input double Risk = 0.1; // risk input double Profit = 0; // profit input int StopLoss = 0; // ATR stop loss input int TakeProfit = 0; // ATR take profit input string Symbol1 = "EURUSD"; input int Magic = 777; // magic number //--- Indicator inputs input uint InpDepth = 120; // ZigZag Depth input uint InpDeviation = 50; // ZigZag Deviation input uint InpBackstep = 30; // ZigZag Backstep input uchar InpPivotPoint = 1; // ZigZag pivot point datetime t=0; double last=0; double countMovements; double currentMovement; // Global variable for storing the indicator descriptor int zigzagHandle;
では、EAの残りの機能を見てみましょう。初期化と非初期化の機能は一般的にシンプルで理解しやすいです。EAのマジックナンバーを設定します。これは、EAが他の注文と区別できるようにするための一意の識別子です。同時に追加の自作関数にハンドルを設定します。OnInitで多通貨ハンドルを直接読み込むと、EAがエラーをスローするからです。だからこそ、このシンプルで簡単な解決策を使用しているのです。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert initialization function custom | //+------------------------------------------------------------------+ int OnIniti(string symb) {// Loading the ZigZag indicator zigzagHandle = iCustom(symb, _Period, "ZigZag", InpDepth, InpDeviation, InpBackstep, InpPivotPoint); if (zigzagHandle == INVALID_HANDLE) { Print("Error loading the ZigZag indicator: ", GetLastError()); return(INIT_FAILED); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { Comment(""); }
EAの他の機能を見てみましょう。次に、すべてのポジションの合計利益を計算する関数と、すべての注文を完全に決済する関数があります。
//+------------------------------------------------------------------+ //| Position Profit | //+------------------------------------------------------------------+ double AllProfit(int type=-1) { double p=0; for(int i=PositionsTotal()-1; i>=0; i--) { if(PositionSelectByTicket(PositionGetTicket(i))) { if(PositionGetInteger(POSITION_MAGIC)==Magic) { if(PositionGetInteger(POSITION_TYPE)==type || type==-1) p+=PositionGetDouble(POSITION_PROFIT); } } } return(p); } //+------------------------------------------------------------------+ //| CloseAll | //+------------------------------------------------------------------+ void CloseAll(int type=-1) { for(int i=PositionsTotal()-1; i>=0; i--) { if(PositionSelectByTicket(PositionGetTicket(i))) { if(PositionGetInteger(POSITION_MAGIC)==Magic) { if(PositionGetInteger(POSITION_TYPE)==type || type==-1) trade.PositionClose(PositionGetTicket(i)); } } } }
次は、ロット計算関数と未決済ポジション数計算関数です。
//+------------------------------------------------------------------+ //| CountTrades | //+------------------------------------------------------------------+ int CountTrades(string symb) { int count=0; for(int i=PositionsTotal()-1; i>=0; i--) { if(PositionSelectByTicket(PositionGetTicket(i))) { if(PositionGetString(POSITION_SYMBOL)==symb) { count++; } } } return(count); } //+------------------------------------------------------------------+ //| Lot | //+------------------------------------------------------------------+ double Lot() { double lot=Lots; if(Risk>0) lot=AccountInfoDouble(ACCOUNT_BALANCE)*Risk/100000; return(NormalizeDouble(lot,2)); }
また、買いと売りの最終取引価格を計算する関数(後で使用)と、ポジションの方向を決定する関数もあります。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double FindLastBuyPrice(string symb) { double pr=0; for(int i=PositionsTotal()-1; i>=0; i--) { if(PositionSelectByTicket(PositionGetTicket(i)) && PositionGetInteger(POSITION_TYPE)==0) { if(PositionGetString(POSITION_SYMBOL)==symb) { pr=PositionGetDouble(POSITION_PRICE_OPEN); break; } } } return(pr); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double FindLastSellPrice(string symb) { double pr=0; for(int i=PositionsTotal()-1; i>=0; i--) { if(PositionSelectByTicket(PositionGetTicket(i)) && PositionGetInteger(POSITION_TYPE)==1) { if(PositionGetString(POSITION_SYMBOL)==symb) { pr=PositionGetDouble(POSITION_PRICE_OPEN); break; } } } return(pr); } //+------------------------------------------------------------------+ //| PositionType | //+------------------------------------------------------------------+ int PositionType(string symb) { int type=8; for(int i=PositionsTotal()-1; i>=0; i--) { if(PositionSelectByTicket(PositionGetTicket(i))) { if(PositionGetString(POSITION_SYMBOL)==symb) { type=(int)PositionGetInteger(POSITION_TYPE); break; } } } return(type); }
そしてもちろん、最も重要な機能は、平均と現在の動きを計算する機能です。ポイントではなく、便宜上、価格単位の移動量で計算されます。これは簡単です。「カスタム初期化」を呼び出し、バッファをコピーし、forループでZigZagの頂点から最後の極値までの値動きの大きさを計算します。この関数は、価格の動きと平均の動きを単位として現在の動きを出力します。
//+------------------------------------------------------------------+ //| CalculateAverageMovement | //+------------------------------------------------------------------+ void CalculateAverageMovement(string symb, double &averageMovement, double ¤tMovement) { const int lookback = 500; // Number of bars for analysis double sumMovements = 0.0; int countMovements = 0; double lastExtremePrice = 0.0; double zigzagArray[500]; // Array to store ZigZag values OnIniti(symb); // Copy ZigZag values to array if (CopyBuffer(zigzagHandle, 0, 0, lookback, zigzagArray) <= 0) { Print("Error copying indicator data"); averageMovement = -1; currentMovement = -1; return; } // Copy ZigZag values to array if (CopyBuffer(zigzagHandle, 0, 0, lookback, zigzagArray) <= 0) { Print("Error copying indicator data"); averageMovement = -1; currentMovement = -1; return; } for (int i = 0; i < lookback; i++) { if (zigzagArray[i] != 0 && zigzagArray[i] != lastExtremePrice) { if (lastExtremePrice != 0) { // Determine the movement direction double movement = zigzagArray[i] - lastExtremePrice; sumMovements += movement; countMovements++; } lastExtremePrice = zigzagArray[i]; } } // Calculate the current movement double lastMovement = iClose(symb, _Period, 0) - lastExtremePrice; currentMovement = lastMovement; // Calculate the average movement averageMovement = countMovements > 0 ? sumMovements / countMovements : 0.0; // Print the result Print("Average movement: ", averageMovement); Print("Current movement: ", currentMovement); // Release resources IndicatorRelease(zigzagHandle); }
もう1つの重要な機能は、現在の値動きが平均値を超えたことを示すシグナルに基づく多通貨取引機能です。利食いと損切りはATRに基づいて設定されます。また、グリッドステップ(平均化)にはATRが使用されます。取引は新しいバーで開始されます。これは私たちにとって重要なことだ。この関数はOnTickで呼び出され、1つまたは複数の銘柄に対して動作します。すでに述べたように、EAを起動する銘柄は1つだけ使用しますが、まだいくつかの銘柄でEAを正常に実行できていません。この銘柄はEAの設定で指定する必要があります。
//+------------------------------------------------------------------+ //| Expert Trade unction | //+------------------------------------------------------------------+ void Trade(string symb) { double averageMovement = 0; double currentMovement = 0; double pr=0,sl=0,tp=0,hi=0,lo=0; // Call function for calculation CalculateAverageMovement(symb, averageMovement, currentMovement); // Use results double Ask = SymbolInfoDouble(symb, SYMBOL_ASK); double Bid = SymbolInfoDouble(symb, SYMBOL_BID); int dg=(int)SymbolInfoInteger(symb,SYMBOL_DIGITS); double pp=SymbolInfoDouble(symb,SYMBOL_POINT); double atr = iATR(symb, PERIOD_CURRENT, 3); // Here define your logic for buying and selling bool sell = currentMovement > -averageMovement; // Buy condition bool buy = -currentMovement > averageMovement; // Sell condition if(AllProfit()>Profit && Profit>0) CloseAll(); if(t!=iTime(symb,PERIOD_CURRENT,0)) { if(buy && CountTrades(symb)<1) { if(StopLoss>0) sl=NormalizeDouble(Bid-(atr*StopLoss)*Point(),_Digits); if(TakeProfit>0) tp=NormalizeDouble(Bid+(atr*TakeProfit)*Point(),_Digits); pr=NormalizeDouble(Bid,dg); trade.Buy(Lot(),symb,pr,sl,tp,""); last=pr; } if(sell && CountTrades(symb)<1) { if(StopLoss>0) sl=NormalizeDouble(Ask+(atr*StopLoss)*Point(),_Digits); if(TakeProfit>0) tp=NormalizeDouble(Ask-(atr*TakeProfit)*Point(),_Digits); pr=NormalizeDouble(Ask,dg); trade.Sell(Lot(),symb,Ask,sl,tp,""); last=pr; } if(CountTrades(symb)>0) { if(PositionType(symb)==0 && (FindLastBuyPrice(symb)-Ask)/pp>=atr*30) { if(StopLoss>0) sl=NormalizeDouble(Bid-(atr*StopLoss)*Point(),_Digits); if(TakeProfit>0) tp=NormalizeDouble(Bid+(atr*TakeProfit)*Point(),_Digits); trade.Buy(Lot(),symb,Ask,sl,tp); } if(PositionType(symb)==1 && (Bid-FindLastSellPrice(symb))/pp>=atr*30) { if(StopLoss>0) sl=NormalizeDouble(Ask+(atr*StopLoss)*Point(),_Digits); if(TakeProfit>0) tp=NormalizeDouble(Ask-(atr*TakeProfit)*Point(),_Digits); trade.Sell(Lot(),symb,Bid,sl,tp); } } t=iTime(symb,0,0); } }
モデルのテスト
いよいよお楽しみの時間です。モデルを実際の市場でテストしましょう。ループベースの計算はかなり処理負荷が高いので、始値のみでEAを実行する方が理にかなっていることにご注意ください。EURUSD、建値、H1時間枠、2020年1月1日から2023年12月6日までで一回テストしてみましょう。
一回のテストは利益を生むが、ドローダウンが大きいです。取引の際に余計なリスクを負いたい人はいません。利益ベースの決算もあることをお忘れなく。ネッティング口座でテストを実施できます。
利益ベースの決済でテストを実行するには、決済を0を超える利益で設定します。テストしてみましょう。おそらく安定したテストができるでしょう。始値で同じ資産のEAを実行します。口座タイプはヘッジです。次が表示されます。
このEAは、平均化のために非常に高リスクであることが判明しました。ネッティング口座で同じテストをしてみましょう。
ここでもドローダウンが大きく、利益がリスクにまったく見合っていません。コードを修正してみましょう。今回はシグナルによる決済を実施します(強気シグナルが弱気シグナルに変わると、それまでのポジションは決済される)。次のコードを使用して、利益による決済を追加します。
if (CloseSig) { if (buy) CloseAll(1); if (sell) CloseAll(0); }
そしてこの設定を追加します。
input bool CloseSig = 1; // close by signal
テストを繰り返します。結果はまたしても良くありません。
一般的にテストは理想的とは言えません。ドローダウンは大きく、ネッティング口座でもヘッジ口座でも大きなドローダウンがあります。さらに、シグナルに基づく決済はプラスの結果を生まず、一般的に利益を生みません。これはかなり腹立たしいです。
結論
MQL5で基本的でシンプルな定量分析アルゴリズムを作成する簡単な例を見てきました。値動きの波を数え、平均値と比較し、そのデータに基づいて売買の判断を下します。残念ながら、アイデアの基礎はかなり優れていたにもかかわらず、これは赤字のアルゴリズムとなりました。今後の記事では、定量分析の探求を続けていきます。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/13835
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索