English
preview
サイクルベースの取引システム(DPO)の構築と最適化の方法

サイクルベースの取引システム(DPO)の構築と最適化の方法

MetaTrader 5トレーディングシステム |
15 2
Mohamed Abdelmaaboud
Mohamed Abdelmaaboud

本記事では、新しい概念を導入するにあたり、テクニカル指標を検証し、取引システム内での有用性を評価します。今回の焦点はDPO(Detrended Price Oscillator、トレンド除去価格オシレーター)です。

本連載の目的は、単独の手法としても、より広範な取引システムの構成要素としても利用できる実践的なツールを共有することにあります。読者の皆さんが、これらのツールを構築、検証、最適化することで、継続的に取引パフォーマンスを向上できるよう支援したいと考えています。ただし、各手法がご自身の取引スタイルに適しているか、また本当に価値を追加できるかどうかを判断するためには、十分な検証をおこなう必要があります。また、コーディングスキルを高めるためには、学んだことを実際に手を動かして試すことが非常に重要です。自分でコードを書き、できるだけ多く検証をおこなうことで、学習効果を最大化できます。

本記事では、以下のトピックについてDPOを扱います。

  1. DPOの定義:計算方法や特徴を理解し、DPOの仕組みを把握します。
  2. カスタムDPOインジケーター:自身の好みに合わせてインジケーターを調整し、カスタム版を実装する方法を紹介します。
  3. DPOを使った戦略:シンプルで実装しやすいDPOベースの戦略を検討し、実際の活用方法を示します。
  4. DPO取引システム:これらの戦略を用いたシステムの構築、バックテスト、最適化をおこないます。
  5. 結論

免責条項:すべての情報は「現状有姿」で提供されており、情報提供のみを目的としており、取引やアドバイスを目的としたものではありません。いかなる結果も保証するものではありません。読者がこれらの資料を自分の取引口座で使用する場合、自己責任でおこなってください。



DPOの定義

DPOは、価格のサイクルを可視化するために使用されるツールです。長期的なトレンド成分を取り除くことで、短期的なサイクルにフォーカスします。このインジケーターを使うことで、買われすぎや売られすぎの判断や、相場の転換ポイントを特定しやすくなります。

DPOは、現在の価格と、一定期間の移動平均を過去へずらした値を比較することで算出されます。目的はより短いサイクルを分離することです。この移動平均期間は、対象とするサイクルの長さに応じて設定するのが一般的です。DPOはゼロラインを中心に上下に振れるオシレーターであり、正の値は価格がシフトされた移動平均より上にあること、負の値は下にあることを示します。

このインジケーターは、サイクルの高値と安値の検出や、エントリー/エグジットのタイミング判断に利用できます。また、他のテクニカルツールと組み合わせてシグナルの精度を向上させることも可能です。

DPOの計算方法に興味がある場合は、以下の手順を確認してください。

  • DPO期間(N)を選択します。
  • 設定した期間Nの単純移動平均(SMA)を計算します。

SMA = (価格1 + 価格2 + ....) / N

  • SMAを「期間の半分 + 1」だけ左(過去)にシフトします。

Shift = (N/2)+1

  • 現在の価格から、シフトしたSMAの値を差し引きます。

DPO(今日)=今日の価格−SMA(Shift分だけ過去へずらした値)



カスタムDPOインジケーター

このセクションでは、DPOインジケーターのコードをステップごとに解説します。また、取引システムで必要となる機能や設定を組み込んだ、簡単なカスタム版を作成します。どのような機能や調整を自身のシステムに追加できるのかを理解する手助けになるはずです。

以下の手順では、DPOラインを自作するための実装方法を、行単位で説明していきます。

まず、プリプロセッサの#propertyを利用してインジケーターの説明文を定義し、識別用の値を設定します。

#property description "Custom Detrended Price Oscillator"

次に、#includeを使ってMovingAverages.mqhを読み込みます。これは後ほど移動平均関数を使用するためです。

#include <MovingAverages.mqh>

続いて、#property indicator_separate_windowを指定し、インジケーターをサブウィンドウに表示するよう設定します。

#property indicator_separate_window
インジケーター計算に使用するバッファ数をindicator_buffersで2に設定します。
#property indicator_buffers 2

プロット数をindicator_plotsで1に設定します。

#property indicator_plots   1

インジケーターラベルはindicator_label1を使用して「DPO」とします。

#property indicator_label1 "DPO"

インジケーターの種類は、indicator_typeNを使用してグラフ描画の種類を設定することで設定され、ここではDRAW_HISTOGRAMを使用します。

#property indicator_type1   DRAW_HISTOGRAM

表示されるグラフの色はindicator_colorNを使用して、clrRoyalBlueになります。

#property indicator_color1  clrRoyalBlue

描画されるグラフの幅または厚さは、indicator_widthNを使用して、2になります。

#property indicator_width1  2

indicator_levelNを使用して表示するゼロレベルを追加します。

#property indicator_level1 0

ユーザーが選択できるDPO期間をinputで定義し、デフォルト値は20にします。

input int detrendPeriodInp=20; // Period

double型配列(dpoBuffer、maBuffer)と移動平均期間(maPeriod)の変数を宣言します。

double    dpoBuffer[];
double    maBuffer[];
int       maPeriod;

OnInit()セクションでは、DPOの初期化関数を定義します。この関数は、チャートに読み込まれた際に最初の一度だけ実行されます。

void OnInit()
{

}

 このOnInit()関数内で、DPOの平滑化に使用されるサイクル長「maPeriod」変数を定義します。

maPeriod=detrendPeriodInp/2+1;

DPO値とMA値をバッファとして保存し、表示します。

   SetIndexBuffer(0,dpoBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,maBuffer,INDICATOR_CALCULATIONS);

IndicatorSetIntegerを使用して、DPOの小数点以下の桁数を設定します。そのパラメータは次のとおりです。

  • prop_id:インジケーターのプロパティを識別するための整数値で、ここではINDICATOR_DIGITSを指定します。
  • prop_value:プロパティに設定する値で、ここでは「_Digits」を使い、銘柄の桁数に1を加えた値を設定します。
IndicatorSetInteger(INDICATOR_DIGITS,_Digits+1);

PlotIndexSetIntegerを使用して、描画を開始する最初のバーとそのパラメータを設定します。

  • plot_index:描画するインデックスを定義します(0)。
  • prop_id:プロパティ識別子を定義します(PLOT_DRAW_BEGIN)。
  • prop_value:設定する値を定義します(maPeriod-1)。
PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,maPeriod-1);

インジケーター上に表示される短い名称を定義するため、StringFormatを利用してindShortNameという文字列変数を宣言します。パラメータには、インジケーター名と、ユーザーが入力した期間の値を指定します。

string indShortName=StringFormat("Custom DPO(%d)",detrendPeriodInp);
IndicatorSetStringを使用して、先ほど生成した名前をインジケーターの短縮名として設定します。引数には、プロパティ識別子であるINDICATOR_SHORTNAMEと、設定する文字列値(定義した名前)を指定します。
IndicatorSetString(INDICATOR_SHORTNAME,indShortName);

OnCalculate関数で指標を計算します。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {

  }
OnCalculate関数内で、整数型のstart変数を宣言します。別の整数型変数「index1」を宣言し、「begin+maPeriod-1」と等しくなるように定義します。
   int start;
   int index1=begin+maPeriod-1;

初回計算時の初期化処理と、計算を開始する位置の設定をおこないます。

  • ArrayInitializeを使用して、配列を0で初期化します。
  • start変数をindex1に更新します。
  • beginが0より大きい場合、PlotIndexSetIntegerを使って、対象となるインジケーターラインの該当プロパティを設定します。

それ以外の場合は、start変数を「prev_calculated - 1」に更新します。

   if(prev_calculated<index1)
     {
      ArrayInitialize(dpoBuffer,0.0);
      start=index1;
      if(begin>0)
         PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,index1);
     }
   else
      start=prev_calculated-1;

MovingAverages.mqhファイルに含まれる定義済み関数(SimpleMAOnBuffer),を使用して単純移動平均を計算します。

SimpleMAOnBuffer(rates_total,prev_calculated,begin,maPeriod,price,maBuffer);

開始からrates_totalまでの各バーをループして計算用のループを作成し、インジケーターが削除されるかターミナルがシャットダウンするまでdpoBuffer[i]を定義します。

for(int i=start; i<rates_total && !IsStopped(); i++)
   dpoBuffer[i]=price[i]-maBuffer[i];

OnCalculateが完了したら新しい値を返します。

return(rates_total);

以下は、1つのコードブロックにまとめた、カスタムDPOインジケーターの完全なコードです。

//+------------------------------------------------------------------+
//|                                                    customDPO.mq5 |
//+------------------------------------------------------------------+
#property description "Custom Detrended Price Oscillator"
#include <MovingAverages.mqh>
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   1
#property indicator_label1 "DPO"
#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  clrRoyalBlue
#property indicator_width1  2
#property indicator_level1 0
input int detrendPeriodInp=20; // Period
double    dpoBuffer[];
double    maBuffer[];
int       maPeriod;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
void OnInit()
  {
   maPeriod=detrendPeriodInp/2+1;
   SetIndexBuffer(0,dpoBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,maBuffer,INDICATOR_CALCULATIONS);
   IndicatorSetInteger(INDICATOR_DIGITS,_Digits+1);
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,maPeriod-1);
   string indShortName=StringFormat("Custom DPO(%d)",detrendPeriodInp);
   IndicatorSetString(INDICATOR_SHORTNAME,indShortName);
  }
//+------------------------------------------------------------------+
//| Detrended Price Oscillator                                       |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   int start;
   int index1=begin+maPeriod-1;
   if(prev_calculated<index1)
     {
      ArrayInitialize(dpoBuffer,0.0);
      start=index1;
      if(begin>0)
         PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,index1);
     }
   else
      start=prev_calculated-1;
   SimpleMAOnBuffer(rates_total,prev_calculated,begin,maPeriod,price,maBuffer);
   for(int i=start; i<rates_total && !IsStopped(); i++)
      dpoBuffer[i]=price[i]-maBuffer[i];
   return(rates_total);
  }
//+------------------------------------------------------------------+

エラーなしでコードをコンパイルした後、チャートに挿入すると以下のように表示されます。

DPOインジケーター



DPOを使った戦略

本セクションでは、DPOを用いてこの取引システムを構築するための2つのシンプルな戦略を紹介します。

  • DPOゼロクロスオーバー戦略
  • DPOトレンド検証戦略

DPOゼロクロスオーバー戦略

この戦略は、DPOの値とゼロレベルとのクロスオーバーに基づき、買いや売りのポジションを建てるものです。直前のDPOが0未満で、現在のDPOが0を上回った場合は買い、直前のDPOが0を上回り、現在のDPOが0を下回った場合は売ります。

シンプルに書くと以下のとおりです。

前回のDPO値<0かつ現在のDPO値>0 --> 買い

前回のDPO値>0かつ現在のDPO値<0 --> 売り

DPOトレンド検証戦略

この戦略は、DPOのゼロクロスに加え、移動平均線(MA)とのクロスを利用してシグナルを検証し、最適化するために短期的な追加フィルターを備えています。したがって、買いポジションは、終値が前の移動平均値よりも小さく、現在のバーのAsk価格が現在の移動平均値より大きく、さらにDPOの値が0より上である場合に建てられます。売りポジションは、終値が前の移動平均値より大きく、現在のバーのBid価格が現在の移動平均値より小さく、さらにDPOの値が0より下である場合に建てられます。

シンプルに書くと以下のとおりです。

終値<前回のMA値かつAsk価格>MA値かつDPO値>0 --> 買い
終値>前回のMA値かつBid価格<MA値かつDPO値<0 --> 売り

これらの各戦略を実装、テスト、最適化し、さまざまなコンセプトを試して、可能な限り最良の結果を達成します。これについては次のセクションでさらに詳しく説明します。



DPO取引システム

本セクションでは、前述の戦略に基づいた自動化されたDPOベースの取引システムをどのように作成するかを提示します。システムがどのように動作するかを理解するために手順を追って説明し、これをさらなる最適化のための新しい洞察や概念の基礎として考えることができます。これがこの種の記事の主な目的です。まず、作成する取引システムのすべてのEAの基礎として利用できるよう、チャート上にDPOの値を表示する簡単なEAを実装します。

次に、インジケーターを呼び出す際に使用する期間を指定するために、EAの入力を設定します。

input int                 period=20; // Period

dpoの整数変数を宣言します。

int dpo;

OnInit()イベントでは、iCustom関数を呼び出してカスタムDPOインジケーターを初期化します。そのパラメータは次のとおりです。

  • Symbol:銘柄名を表す文字列で、ここでは現在のチャートの銘柄に適用するために「_Symbol」を使用します。
  • Period:ENUM_TIMEFRAMESの形式で指定する時間足で、現在のチャートの時間足に適用するためにPERIOD_CURRENTを使用します。
  • Name:カスタムインジケーターの名称で、ローカルマシン上の正しいパス(folder/custom_indicator_nameの形式)を含めて指定します。この場合はインジケータ名ーであるcustomDPOになります。
  • Inputs:インジケーターに入力パラメータがある場合に指定するリストで、ここではperiodを指定します。

その後、INIT_SUCCEEDEDの戻り値を使用して初期化が成功したことを確認し、コードの次の部分に進みます。

int OnInit()
  {
   dpo = iCustom(_Symbol,PERIOD_CURRENT,"customDPO",period);
   return(INIT_SUCCEEDED);
  }

OnDeinit()イベントでは、初期化解除イベントが発生したときにEAのメッセージが削除されるように指定します。

void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }

OnTick()イベントは、EAで新しいティックイベントが発生したときに呼び出されます。

void OnTick()
  {

  }

dpoInd[]の配列をdoubleデータ型として宣言します。

double dpoInd[];

CopyBuffer関数を使用して、指定されたDPOインジケーターバッファからデータを取得します。パラメータは次の通りです。

  • indicator_handle:DPOインジケーターのハンドル
  • buffer_num:コピー元のバッファ番(DPOインジケーターの場合は0))
  • start_pos:開始位置(ここでは0)
  • count:コピーする値の数(ここでは3)
  • buffer[]:コピーされた値が格納されるターゲット配列(ここではdpoInd[])
CopyBuffer(dpo,0,0,3,dpoInd);

dpoInd[]配列に対してAS_SERIESフラグを設定し、要素を時系列としてインデックス付けします。配列名とフラグ値(true)をパラメータとして渡し、設定が成功すると関数はtrueを返します。

ArraySetAsSeries(dpoInd,true);

dpoValというdouble型の変数を宣言および定義し、現在のDPOインデックスの値を取得して小数点以下3桁に正規化します。

double dpoVal = NormalizeDouble(dpoInd[0], 3);

チャート上の現在のDPOの値をコメントします。

omment("DPO value = ",dpoVal);

1つのブロック内の完全なコードは以下のようになります。

//+------------------------------------------------------------------+
//|                                                      DPO_Val.mq5 |
//+------------------------------------------------------------------+
input int                 period=20; // Period
int dpo;
int OnInit()
  {
   dpo = iCustom(_Symbol,PERIOD_CURRENT,"customDPO",period);
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
void OnTick()
  {
    double dpoInd[];
    CopyBuffer(dpo,0,0,3,dpoInd);
    ArraySetAsSeries(dpoInd,true);
    double dpoVal = NormalizeDouble(dpoInd[0], 3);
    Comment("DPO value = ",dpoVal);
  }

エラーなしでコンパイルし、EAを接続すると、以下のようにDPO値がチャート上の動的コメントとして表示されます。

DPOVal

ご覧のとおり、チャート上のDPO値は、挿入されたインジケーターに表示されている値(1794.363)と同じです。これを他の戦略のEAの基礎として使用できます。

DPOゼロクロスオーバー戦略

前述のとおり、この戦略のロジックは、DPOインジケーターのゼロクロスをもとに自動で注文を発注できるEAを実装することです。プログラムは、各ティックごとに現在のDPO値と前回のDPO値を確認し、ゼロレベルと比較する必要があります。前回の値がゼロ未満で、現在の値がゼロを上回った場合は買い注文を発注し、前回のDPO値がゼロを上回り、現在の値がゼロを下回った場合は売り注文を発注します。

以下は、MetaTrader 5上で実行できるこの種類のプログラムの完全なコードです。

//+------------------------------------------------------------------+
//|                                           DPO_Zero_Crossover.mq5 |
//+------------------------------------------------------------------+
#include <trade/trade.mqh>
input int                 period=20; // Periods
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;
int dpo;
CTrade trade;
int barsTotal;
int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   dpo = iCustom(_Symbol,PERIOD_CURRENT,"customDPO",period);
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
void OnTick()
  {
   int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal != bars)
     {
      barsTotal=bars;
      double dpoInd[];
      CopyBuffer(dpo,0,0,3,dpoInd);
      ArraySetAsSeries(dpoInd,true);
      double dpoVal = NormalizeDouble(dpoInd[1], 6);
      double dpoPreVal = NormalizeDouble(dpoInd[2], 6);
      double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
      if(dpoPreVal<0 && dpoVal>0)
        {
         double slVal=ask - slLvl*_Point;
         double tpVal=ask + tpLvl*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }
      if(dpoPreVal>0 && dpoVal<0)
        {
         double slVal=bid + slLvl*_Point;
         double tpVal=bid - tpLvl*_Point;
         trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
        }
     }
  }
//+------------------------------------------------------------------+

上記のコードブロックを見るとわかるように、DPO値の基本コードとは違いがあります。つまり以下です。

注文を出すにはtrade.mqhファイルをインクルードします。

#include <trade/trade.mqh>

ロットサイズ、ストップロスレベル、およびテイクプロフィットレベルのデフォルト値を持つ入力をユーザーが指定できるようにします。

input int         period=20; // Periods
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;

使用する取引オブジェクトとbarsTotal整数変数を宣言します。

CTrade trade;
int barsTotal;

OnInit ()では、barsTotalをiBars関数の結果と等しく定義し、銘柄と期間における過去のバーの本数を取得します。

barsTotal=iBars(_Symbol,PERIOD_CURRENT);

OnTick()内では、barsを整数型の変数として宣言し、iBars関数で定義します。

int bars=iBars(_Symbol,PERIOD_CURRENT);

新しいバーが形成されたかを確認し、発注を制限する条件として、barsTotalがbarsと異なる場合にコードの実行を続行するように設定します。

if(barsTotal != bars)

新しいバーが形成された場合、barsTotalの値をbarsの値で更新します。

barsTotal=bars;

最後に確定したバーをインデックス1で更新し、前回のDPO値を格納するdouble型の変数をインデックス2で宣言します。

double dpoVal = NormalizeDouble(dpoInd[1], 6);
double dpoPreVal = NormalizeDouble(dpoInd[2], 6);

Ask価格とBid価格を宣言して定義します。

double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);

戦略の条件に基づき、ストップロス、テイクプロフィットを定義し、注文を出します。

if(dpoPreVal<0 && dpoVal>0)
  {
   double slVal=ask - slLvl*_Point;
   double tpVal=ask + tpLvl*_Point;
   trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
  }
if(dpoPreVal>0 && dpoVal<0)
   {
    double slVal=bid + slLvl*_Point;
    double tpVal=bid - tpLvl*_Point;
    trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
   }

エラーなしでコードをコンパイルし、EAをチャートにアタッチすると、次の例と同じようにポジションを配置できることがわかります。

買いポジション

DPO_Crossover_Buy

売りポジション

DPO_Crossover_Sell

次に、すべての戦略をEUR/USDペアで、5分足と15分足で、期間は2023年1月1日から12月31日までの間でテストする必要があります。ストップロスは300ポイント、テイクプロフィットは900ポイントを使用します。テスト結果に基づき、最適化の主なアプローチは、移動平均線を統合するなどの追加の概念やツールを試すことになります。また、異なる時間足でのテストをおこない、どの設定が最も良いパフォーマンスを示すかを確認することも有効です。

各テスト結果を比較する際の重要な指標は以下の通りです。

  • 純利益:総利益から総損失を差し引いた値で、値が大きいほど良いです。
  • 相対最大ドローダウン:取引中に口座が経験する最大損失で、値が小さいほど良いです。
  • プロフィットファクター:総利益と総損失の比率で、値が大きいほど良いです。
  • 期待損益:1回の取引あたりの平均損益で、値が大きいほど良いです。
  • リカバリーファクター:損失後に戦略がどれだけ回復するかを示す指標で、値が大きいほど良いです。
  • シャープレシオ:戦略のリターンと安定性を示す指標で、値が高いほど良いです。

15分足のテスト結果は以下の通りです。

Results_0CO_15m-700

Results2_0CO_15m-700

Results3_0CO_15m-700

上記の15分足テスト結果に基づくと、重要な数値は以下の通りです。

  • 純利益:91520.53 USD
  • 相対最大ドローダウン:35.81%
  • プロフィットファクター:1.09
  • 期待損益:21.17
  • リカバリーファクター:1.48
  • シャープレシオ:1.07

5分足のテスト結果は以下の通りです。

Results_0CO_5m-700

Results2_0CO_5m-700

Results3_0CO_5m-700

上記の15分足テスト結果に基づくと、このテストの重要な数値は以下の通りです。

  • 純利益:62258.14
  • 相対最大ドローダウン:97.34%
  • プロフィットファクター:1.02
  • 期待損益:4.78
  • リカバリーファクター:0.30
  • シャープレシオ:0.17

ご覧の通り、この戦略の15分足チャートでのテスト結果は5分足よりも良好です。しかし、依然としてドローダウンが高いため、結果の改善を目的として、別のテクニカルツールである移動平均線を追加することを検討します。

DPOトレンド検証戦略

前述のとおり、この戦略は、DPOの値の位置と移動平均線と価格の関係に基づいて自動で注文を発注できるEAをコーディングすることを含みます。これにより、移動平均線による追加の確認層が加わります。プログラムは各ティックごとに、現在および前回のDPOの値、移動平均線、終値、Bid価格を確認する必要があります。

次の場合に買い注文を出します。

  • 前日の終値が移動平均の前の値より小さい
  • Ask価格が現在の移動平均値よりも高い
  • 現在のDPO値がゼロより大きい 

次の場合に売り注文を出します。

  • 前日の終値が移動平均の前の値よりも大きい
  • Bid価格が現在の移動平均値よりも低い
  • 現在のDPO値がゼロ未満である 

以下は、MetaTrader 5上で実行できるこの種類のプログラムの完全なコードです。

//+------------------------------------------------------------------+
//|                                          DPO_trendValidation.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include <trade/trade.mqh>
input int         period=20; // Periods
input int         maPeriodInp=20; //MA Period
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;
int dpo;
int ma;
CTrade trade;
int barsTotal;
int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   dpo = iCustom(_Symbol,PERIOD_CURRENT,"customDPO",period);
   ma = iMA(_Symbol,PERIOD_CURRENT,maPeriodInp,0,MODE_SMA,PRICE_CLOSE);
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
void OnTick()
  {
   int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal != bars)
     {
      barsTotal=bars;
      double dpoInd[];
      double maInd[];
      CopyBuffer(dpo,0,0,3,dpoInd);
      CopyBuffer(ma,0,0,3,maInd);
      ArraySetAsSeries(dpoInd,true);
      ArraySetAsSeries(maInd,true);
      double dpoVal = NormalizeDouble(dpoInd[0], 6);
      double maVal= NormalizeDouble(maInd[0],5);
      double dpoPreVal = NormalizeDouble(dpoInd[1], 5);
      double maPreVal = NormalizeDouble(maInd[1],5);;
      double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
      double prevClose = iClose(_Symbol,PERIOD_CURRENT,1);
      if(prevClose<maPreVal && ask>maVal)
        {
         if(dpoVal>0)
           {
            double slVal=ask - slLvl*_Point;
            double tpVal=ask + tpLvl*_Point;
            trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
           }
        }
      if(prevClose>maPreVal && bid<maVal)
        {
         if(dpoVal<0)
           {
            double slVal=bid + slLvl*_Point;
            double tpVal=bid - tpLvl*_Point;
            trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
           }
        }
     }
  }
//+------------------------------------------------------------------+

このコードの違いは、次の通りとなります。

ユーザーが指定する移動平均期間の別の入力を追加します。

input int         maPeriodInp=20; //MA Period

maの整数変数を宣言します。

int ma;

移動平均インジケーターのハンドルを返すiMA関数を使用してma変数を定義します。

ma = iMA(_Symbol,PERIOD_CURRENT,maPeriodInp,0,MODE_SMA,PRICE_CLOSE);

maInd[]の配列をdoubleデータ型として宣言します。

double maInd[];

CopyBuffer関数を使用して、指定されたMAインジケーターバッファからデータを取得します。

CopyBuffer(ma,0,0,3,maInd);

maInd[ ]配列のAS_SERIESフラグを設定して、その要素を時系列としてインデックス付けします。

ArraySetAsSeries(maInd,true);

maValを宣言して定義し、正規化します。

double maVal= NormalizeDouble(maInd[0],5);

 iClose関数を使用して、前回の終値を宣言および定義します。

double prevClose = iClose(_Symbol,PERIOD_CURRENT,1);

売買の場合の戦略の条件は以下の通りです。

 if(prevClose<maPreVal && ask>maVal)
   {
    if(dpoVal>0)
      {
       double slVal=ask - slLvl*_Point;
       double tpVal=ask + tpLvl*_Point;
       trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
      }
   }
if(prevClose>maPreVal && bid<maVal)
   {
    if(dpoVal<0)
      {
       double slVal=bid + slLvl*_Point;
       double tpVal=bid - tpLvl*_Point;
       trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
       }
   }

この戦略のコードをエラーなしでコンパイルし、そのファイルをチャートに添付すると、次の例と同じように買いポジションと売りポジションを配置できることがわかります。

買いポジション

DPO_TV_Buy

売りポジション

DPO_TV_Sell

この戦略を、前述と同じ期間である2023年の1年間、EUR/USDペアで15分足と5分足にてテストする必要があります。15分足におけるDPOトレンド検証戦略のテスト結果は以下の通りです。

Results_TV_15m-700

Results2_TV_15m-700

Results3_TV_15m-700

上記の15分足テスト結果によると、このテストの重要な数値は以下の通りです。

  • 純利益:21866.08
  • 相対最大ドローダウン:16.22%
  • プロフィットファクター:1.19
  • 期待損益:42.79
  • リカバリーファクター:1.24
  • シャープレシオ:1.42

5分足のテスト結果は以下の通りです。

Results_TV_5m-700

Results2_TV_5m-700

Results3_TV_5m-700

上記の重要な数値に応じて、M5(5分足)のテストの結果は以下のようになります。

  • 純利益:87010.62
  • 相対最大ドローダウン:30.24%
  • プロフィットファクター:1.24
  • 期待損益:51.67
  • リカバリーファクター:2.08
  • シャープレシオ:1.36

これで、すべての結果に基づいて最良のものを確認できます。

  • 最高の純利益:91520.53(DPOゼロクロスオーバー戦略、M15)
  • 最低の相対最大ドローダウン:16.22%(DPOトレンド検証戦略、M15)
  • 最高のプロフィットファクター:1.24(DPOトレンド検証戦略、M5)
  • 最高の期待報酬:51.67(DPOトレンド検証戦略、M5)
  • 最高のリカバリーファクター:2.08(DPOトレンド検証戦略、M5)
  • 最高のシャープレシオ:1.42(DPOトレンド検証戦略、M15)

移動平均による確認を組み込んだ最適化EAは、ほとんどの指標において全体的により良い結果を示すことが明らかです。そのため、他のテクニカルツールや異なる時間足、異なる期間など、さまざまな条件でさらにテストをおこない、テスト結果に応じてより良い成果を得られるようにすることをお勧めします。



結論

この記事の最後において、DPOというテクニカル指標が何を測定するものであるか、どのように計算できるか、そしてトレーダーがどのように活用できるかを理解できたことを前提としています。さらに、自分の好みに応じてカスタムのDPOをコーディングする方法も理解でき、最も興味深い点として、以下の2つのシンプルな戦略に基づいた取引システムの作成方法も学べます。

  • DPOゼロクロスオーバー戦略
  • DPOトレンド検証戦略

また、これらを異なるコンセプトに基づいて最適化し、取引においてより良い結果を得る方法も理解できたはずです。前述の取引システムを最適化する際に使用した手法に基づき、テストのほとんどの指標において最も優れた戦略がどれであるかを確認することができました。この記事が役立ったと感じていただければ幸いです。もし、最も人気のあるテクニカル指標に基づく取引システムの構築など、私の他の記事をお読みになりたい場合は、公開ページからご覧ください。

添付のソースコードファイルは以下にあります。

ファイル名 説明
customDPO カスタムDPOインジケーター用
DPO_Val 動的DPO値のEA用
DPO_Zero_Crossover DPOゼロクロスオーバー戦略のEA
DPO_trendValidation DPOトレンド検証戦略のEA用 

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19547

添付されたファイル |
customDPO.mq5 (2.19 KB)
DPO_Val.mq5 (0.65 KB)
最後のコメント | ディスカッションに移動 (2)
Dan Dumbraveanu
Dan Dumbraveanu | 27 9月 2025 において 18:05
これは非常に興味深いのですが、理論とコードの関連付けに 少々難があります。私の理解が正しければ、現在のバーで使用するSMAを計算するには、考慮期間をN/2 + 1バー分後ろにずらし、そこからNバー分戻してSMAを計算する必要があります。私は初心者なので、インジケータのコードをよく理解しているふりはしませんが、私が解読したところでは、NはSMAの期間として名前を設定するために使用されるだけで、計算には使用されず、代わりにN/2 + 1 (コードではmaPeriod)がSMAの期間として使用され、シフトは実行されないようです。もし私がすべて間違っているのであればお許しください。
Mark Anthony Graham
Mark Anthony Graham | 29 9月 2025 において 04:06

ありがとうございます。

基本的に、私はあなたのインディケータが大好きです。

より高い時間枠、例えば週足で使用すると効果的だと思います。

EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
MQL 標準ライブラリエクスプローラー(第1回):CTrade、CiMA、CiATRによる紹介 MQL 標準ライブラリエクスプローラー(第1回):CTrade、CiMA、CiATRによる紹介
MQL5標準ライブラリは、MetaTrader 5における取引アルゴリズム開発において重要な役割を果たします。本連載では、このライブラリを使いこなし、MetaTrader 5用の効率的な取引ツールをより簡単に作成する方法を身につけることを目指します。これには、カスタムのエキスパートアドバイザー(EA)、インジケーター、その他のユーティリティが含まれます。本日はその第一歩として、CTrade、CiMA、そしてCiATR クラスを用いたトレンドフォロー型のEAを開発します。これは初心者、熟練者を問わず、すべての開発者にとって非常に重要なテーマです。ぜひ本ディスカッションにご参加いただき、理解を深めてください。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
機械学習の限界を克服する(第4回):複数ホライズン予測による既約誤差の回避 機械学習の限界を克服する(第4回):複数ホライズン予測による既約誤差の回避
機械学習は統計学や線形代数の観点から語られることが多いですが、本記事ではモデル予測を幾何学的に理解する視点に注目します。本記事で示したいのは、モデルはターゲットを直接近似しているのではなく、ターゲットを別の座標系に写像することで固有のずれを生み出し、その結果、避けがたい既約誤差が生じる点です。また本記事では、ターゲットとの直接比較ではなく、異なるホライズンにおけるモデルの予測同士を比較する複数ステップ予測の方が実務的かつ有効であることを提案します。この手法を取引モデルに適用すると、基礎モデルを変更することなく、収益性と予測精度が大幅に向上することを確認しました。