English Deutsch
preview
ラリー・ウィリアムズの『市場の秘密』(第2回):市場構造取引システムの自動化

ラリー・ウィリアムズの『市場の秘密』(第2回):市場構造取引システムの自動化

MetaTrader 5トレーディング |
222 3
Chacha Ian Maroa
Chacha Ian Maroa

はじめに

多くのトレーダーは市場構造を視覚的に理解していますが、その理解を正確で再現性のある取引プロセスに落とし込むのに苦労しています。チャート上で後から振り返ればスイングポイントを見つけるのは容易ですが、リアルタイムで一貫した意思決定をおこなうことははるかに困難です。トレーダーが裁量を排除し、テスト可能かつ自動化可能な客観的ルールに基づこうとする場合、この課題はさらに大きくなります。

本連載の初めの記事では、ラリー・ウィリアムズの著書『Long-Term Secrets to Short-Term Trading』(ラリー・ウィリアムズの短期売買法:投資で生き残るための普遍の真理)で紹介されている概念に基づき、MQL5でカスタムの市場構造インジケーターを構築することで、この問題の一部を解決しました。このインジケーターは、短期および中期のスイングポイントをチャート上に直接特定し、トレーダーに価格変動の明確で体系的な見方を提供します。今回の第2回では、次の論理的なステップとして、視覚的な分析から完全自動化へと移行します。MQL5を用いて、インジケーターから市場構造データを読み取り、それを実行可能な取引判断へと変換するエキスパートアドバイザー(EA)を設計します。その目的は、裁量的なアイデアを明確なルールとして表現し、感情的な介入なしに自動的に実行する方法を示すことにあります。

本記事は、連載「ラリー・ウィリアムズの『市場の秘密』」の一部であり、各回では同氏の著作から一つの概念を取り上げ、それを実践的かつ検証可能な形で実装することに焦点を当てています。今回は、短期的および中期的スイングポイントに焦点を当て、構造が確認された直後に取引を開始するためにそれらをどのように活用できるかを解説します。この記事を読み終える頃には、読者はMQL5を用いて市場構造理論と実運用の自動化との間のギャップを埋める、実用的な取引システムを手にしているでしょう。


ラリー・ウィリアムズとは誰なのか?

ラリー・ウィリアムズは、トレーディング業界で最も尊敬されている人物の一人です。彼は長年の実績を持つ株式・商品トレーダーです。彼はまた、多くのトレーディング関連書籍の著者でもあります。彼の最も有名な著書の1つは、Long-Term Secrets to Short-Term Tradingです。多くのトレーダーが、市場構造とスイング分析に対する実践的なアプローチを理由にこの本を研究しており、それがこの記事の基礎となっています。

ラリー・ウィリアムズは、1987年に先物取引ワールドカップ選手権で優勝したことで、大きな名声を得ました。そのコンテストで、彼は12か月以内に1万ドルを100万ドル以上に増やしました。その記録を破った者はこれまで誰もいません。10年後、彼の娘であるミシェル・ウィリアムズも同じコンテストに出場し、優勝しました。これは、彼のアイデアが他者によって学習され、うまく応用され得ることを示しました。


戦略の概要

何かを自動化する前に、この戦略の基盤となる市場構造の概念を簡単に再確認することが重要です。これらの考え方については本連載の第1回で詳しく説明しているため、ここでは取引ロジックの理解に必要なポイントのみに焦点を当てます。

以下は、第1回に開発した市場構造インジケーターを実際の市場に適用したチャートのスクリーンショットです。

ラリー・ウィリアムズ市場構造インジケーター

この図により、短期および中期のスイングポイントがどのように特定されるかを明確に理解でき、さらに、これらのポイントがEAによって売買シグナルを生成するためにどのように使用されるかを容易に追跡できます。

ラリー・ウィリアムズは、価格変動から自然に生じるスイングポイントを用いて市場構造を定義しています。短期的なスイングローは、価格が安値をつけた際に、その両側に切り上げ安値が記録されている場合に形成されます。

短期的安値

これは、売り圧力が弱まり、価格が上昇に転じ始めたことを示しています。短期的なスイングハイは、その逆です。

短期的高値

これは、価格が高値を付けた後、その両側に切り下げ高値が続く場合に形成され、買い圧力が弱まり、価格が下降に転じ始めたことを示しています。ウィリアムズは当初、これらを「リングされた高値・安値」と呼んでいました。トレーダーがチャート上でそれらを目立たせるために丸で囲んでいたからです。

市場は単一の構造レベルにとどまりません。ラリー・ウィリアムズによれば、短期的なスイングポイントが組み合わさって、中期的なスイングポイントが形成されます。中期的安値とは、その前後の短期的安値を更新している短期的安値のことです。

中期的安値

中期的高値とは、その前後の短期的高値を更新している短期的高値のことです。

中期的高値

このようにスイングが入れ子構造になっていることで、主観的なチャート解釈をすることなく、市場の動きを機械的かつ客観的に記述することが可能になります。

ラリー・ウィリアムズが著書『Long-Term Secrets to Short-Term Trading』(ラリー・ウィリアムズの短期売買法:投資で生き残るための普遍の真理)で述べている最も重要なポイントのひとつは、これらのスイングポイントが単なる説明的なものではなく、実際に取引判断に使えるという点です。彼は、これらのスイングポイントの形成と突破をエントリー、エグジット、ストップロスレベルとして利用することで、一貫して利益を上げてきたと説明しています。彼によれば、これらのポイントは市場における最も重要なサポートとレジスタンスを表しています。それらが維持されている限りはトレンド継続の確認となり、逆にブレイクされた場合はトレンド転換の警告となるのです。

本記事の焦点は、そのアイデアを完全自動化された取引システムへと発展させることです。本連載の第2回では、意図的に短期および中期的なスイングポイントに限定して分析を進めます。長期的なスイングポイントについては、基礎が完成次第、後の記事で導入し、自動化する予定です。

取引の中核となるロジックは非常にシンプルで、ラリー・ウィリアムズの考え方に忠実に基づいています。まず、中期的なスイングポイントが、短期的なスイングポイントの形成によって確認されるのを待ちます。この確認が成立した時点で、ポジションを保有していなければ、EAは即座にエントリーします。

ロングポジションは、短期のスイング安値が中期のスイング安値を裏付けたときに開きます。これは、価格が調整局面を終え、新たな上昇の流れに入る可能性が高いことを示唆します。EAは新しいバーが形成されるたびにこの条件をチェックし、確認が取れた瞬間に成行の買い注文を出します。

ショートポジションはその逆です。短期的なスイングハイが中期的なスイングハイを確認した場合、下降トレンドの始まりと判断します。有効なポジションがない場合は、直ちに成行売り注文を出します。

すべてのシグナル判定は、新しいバーが開いた時点でのみおこなわれます。これにより、スイングポイントが確定した状態で判断でき、不完全な価格データに振り回されることを防ぎます。

基本的なエントリーロジックに加え、実運用に耐えうるよういくつかの実用的な機能も組み込まれています。

まず、ユーザーは取引の方向を制御できます。EAは、市場状況や個人の好みに応じて、買いのみ、売りのみ、またはその両方をおこなうように設定可能です。

ポジションサイズの設定は、完全に自動化することも、手動でおこなうことも可能です。EAは、ユーザーが定義した現在の口座残高に対するリスク割合に基づいてロットサイズを計算することも、トレーダーが指定した固定ロットサイズを使用することもできます。

ストップロスは、完全に市場構造に基づいて設定されます。直近の短期的スイングポイント、または直近で確定した中期的スイングポイントのいずれかを基準に選択できます。その結果、エントリーと同じロジックに沿ったリスク管理が可能です。

非現実的または望ましくない取引を避けるために、ストップロスの最小・最大距離も設定可能です。これによって、値動きに耐えられないほどタイトなストップや、リスクに見合わないほど広すぎるストップを防ぎます。

利益確定は、設定可能なリスクリワード比によって管理されます。そのため、異なる市場や時間足でも一貫した戦略運用が可能になります。

最後に、EAにはオプションでステップ型のトレーリングストップも利用できます。この機能を有効にすると、価格が有利な方向へ進むにつれて利益を段階的に確保しつつ、トレンドの伸びも狙うことができます。 

これらすべての要素が組み合わさることで、ラリー・ウィリアムズの市場構造に関する考え方は、単なる視覚的な分析ツールから、実際に運用可能なシステマティックな取引戦略へと進化します。以下のセクションでは、MQL5で各部分がどのように実装されているか、また、インジケーターとEAがどのように連携して、信頼性が高く再現性のある取引判断を生み出しているのかを詳しく解説していきます。



シグナル生成ロジック

まず、MetaEditor 5を開き、新しいEAファイルを作成して、larryWilliamsMarketStructureExpert.mq5という名前を付けます。ファイルが作成されたら、デフォルトのテンプレートコードを削除し、以下の定型文に置き換えます。

//+------------------------------------------------------------------+
//|                           larryWilliamsMarketStructureExpert.mq5 |
//|          Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian |
//|                          https://www.mql5.com/ja/users/chachaian |
//+------------------------------------------------------------------+

#property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian"
#property link      "https://www.mql5.com/ja/users/chachaian"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Standard Libraries                                               |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| User input variables                                             |
//+------------------------------------------------------------------+
input group "Information"
input ulong           magicNumber = 254700680002;                 
input ENUM_TIMEFRAMES timeframe   = PERIOD_CURRENT;

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
//--- Create a CTrade object to handle trading operations
CTrade Trade;

//--- Bid and Ask
double   askPrice;
double   bidPrice;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   //---  Assign a unique magic number to identify trades opened by this EA
   Trade.SetExpertMagicNumber(magicNumber);

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   
   //--- Notify why the program stopped running
   Print("Program terminated! Reason code: ", reason);
   
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   //--- Scope variables
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID);

}

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
{
}

//--- UTILITY FUNCTIONS

//+------------------------------------------------------------------+

この初期コードによって、これから段階的に拡張していくためのシンプルで信頼性の高い土台が整います。

まず、ヘッダセクションでは、ファイルに関する基本的な情報を定義します。これには、EA名、作成者情報、バージョン番号、およびMQL5プロファイルへのリンクが含まれます。これは標準的な手順であり、識別、バージョン管理、および将来のメンテナンスに役立ちます。

次に、標準の取引ライブラリをインクルードします。Trade.mqhファイルにはCTradeクラスが含まれており、後ほどポジションのオープン、管理、クローズを安全かつ体系的におこなうために用います。

その後、ユーザー入力変数を定義します。これらの入力により、トレーダーはEAをチャートに適用する際に重要な設定を調整できます。現時点では、このEAによって開かれた取引を一意に識別するためのマジックナンバーと、異なるチャート間で作業する際に柔軟性を持たせるための時間足パラメータのみを定義しています。

グローバル変数のセクションは以下のとおりです。ここでは、すべての取引操作を処理するCTradeオブジェクトを作成します。また、現在のBid価格とAsk価格を格納するための変数も宣言します。これらの変数はティックごとに更新され、EA全体で再利用されます。

OnInit関数は、EAがチャートに適用されたときに一度だけ実行されます。この段階では、マジックナンバーをCTradeオブジェクトに割り当てるだけです。これにより、このEAがおこなったすべての取引を個別に追跡、識別、管理できるようになります。

OnDeinit関数は、EAが削除または停止されたときに呼び出されます。現時点では、プログラムが終了した理由をメッセージとして出力するだけですが、これはテストやデバッグの際に役立ちます。

OnTick関数は、新しい価格データがターミナルに届くたびに実行されます。この初期段階では、Bid価格とAsk価格のみを更新します。シグナル検出や取引ロジックは後ほどここに追加していきますが、今はあえてシンプルで分かりやすい状態を保っています。

最後に、OnTradeTransaction関数はプレースホルダーとして含まれています。まだ使用はしませんが、約定や注文の変更、決済といった取引イベントを処理する際に、後々重要な役割を果たします。

この時点では、EAは意図的に何も実行しません。しかしそれで問題ありません。MQL5のベストプラクティスに沿った、堅牢で読みやすい基盤がすでに整っています。次のステップでは、シグナル生成ロジックを追加し、このEAを第1回で構築した市場構造インジケーターに接続します。

EAの基本的な構造が整ったので、いよいよ本格的な実装に入ります。この戦略では、市場構造をEA内部で計算するのではなく、第1回で作成した市場構造インジケーターから直接シグナルを読み取ります。

インジケーターの完全なソースコードは、larryWilliamsMarketStructureIndicator.mq5としてこの記事に添付されています。同じ環境で作業するためには、まずこのインジケーターがターミナル上で使用可能になっていることを確認してください。

これには2つの簡単な方法があります。最初の方法は、添付のソースファイルをダウンロードし、MetaEditor 5を開き、larryWilliamsMarketStructureIndicator.mq5という名前の新しい空のインジケーターファイルを作成し、そこにソースコードを貼り付けてコンパイルすることです。2つ目の選択肢はさらに簡単です。ダウンロードしたファイルを、そのままMQL5データディレクトリ内のIndicatorsフォルダにコピーします。その後ターミナルを再起動すれば、インジケーターが通常通り表示され、必要に応じて編集やコンパイルも可能です。

このEAは外部インジケーターに依存しているため、そのリソースをEAと一緒にパッケージ化しておくのが望ましいです。そうすることで、EAが常に正しくインジケーターを見つけて読み込めるようになります。そのために、既存のpropertyディレクティブのすぐ下に、次の1行を追加します。

#resource "\\Indicators\\larryWilliamsMarketStructureIndicator.ex5"

このディレクティブは、コンパイル済みのインジケーターファイルをEAの中に埋め込む役割を持ちます。その結果、EAが実行される際、ターミナルは手動でのインストールパスに依存することなく、インジケーターの場所を正確に把握できます。結果として、配布や再利用の信頼性が大きく向上します。

次に、EAとインジケーターがやり取りするための仕組みが必要になります。MQL5では、この通信は「インジケーターハンドル」を通じておこなわれます。グローバル変数セクションで、次の変数を宣言します。

//--- The Larry Williams Market Structure Indicator handle
int larryWilliamsMarketStructureIndicatorHandle;

インジケーターハンドルは単なる参照です。これは、バックグラウンドで動作しているインジケーターのインスタンスと、EAとの間をつなぐ生きた接続を表します。このハンドルがなければ、EAはインジケーターのバッファからデータを取得することができません。

続いて、インジケーターから読み取った市場構造の値を格納するために、グローバルスコープで4つの配列を宣言します。

//--- Arrays to track market structure data
double shortTermLows [];
double shortTermHighs[];
double intermediateTermLows [];
double intermediateTermHighs[];

これらの配列はコンテナとして機能します。インジケーターによって生成された最新のスイングポイントの値がここに格納され、後ほどシグナルロジックがこれらの配列を参照することで、エントリーするべきかどうかを判断します。

OnInit関数内で、これらの配列を時系列データとして扱うようにMQL5に指示します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- Treat the following arrays as timeseries (index 0 becomes the most recent bar)
   ArraySetAsSeries(shortTermLows,  true);
   ArraySetAsSeries(shortTermHighs, true);
   ArraySetAsSeries(intermediateTermLows,  true);
   ArraySetAsSeries(intermediateTermHighs, true);
   ArraySetAsSeries(closePriceMinutesData, true);

   return(INIT_SUCCEEDED);
}

この設定により、インデックス0は常に最新のバーを表すようになります。これはシグナル検出では非常に重要で、毎回すべての過去データを走査することなく、直近で形成されたスイングポイントだけを効率よく評価できるようになります。

ハンドルの宣言が完了したら、次はいよいよインジケーター本体を初期化します。これもOnInit関数内でおこないます。

int OnInit(){

   ...

   //--- Initialize larryWilliamsMarketStructureIndicator
   larryWilliamsMarketStructureIndicatorHandle    = iCustom(_Symbol, timeframe, "::Indicators\\larryWilliamsMarketStructureIndicator.ex5");
   if(larryWilliamsMarketStructureIndicatorHandle == INVALID_HANDLE){
      Print("Error while initializing Larry Williams' Market Structure Indicator: ", GetLastError());
      return(INIT_FAILED);
   }

   return(INIT_SUCCEEDED);
}

ここで、iCustomはインジケーターを読み込み、成功した場合はハンドルを返します。銘柄時間足パラメータを指定することで、EAと同じチャート環境上でインジケーターが動作するようになります。ハンドルが無効だった場合、EAは即座に停止します。これによって、信頼できるデータがない状態で戦略が動いてしまうのを防ぎます。

インジケーターの起動が確認できたら、次はそのバッファ値を効率よく取得する仕組みが必要になります。そのために、カスタムのユーティリティ関数を定義します。

//--- UTILITY FUNCTIONS
//+-------------------------------------------------------------------------------+
//| Copies the latest swing high and low data from the market structure indicator |
//+-------------------------------------------------------------------------------+
void RefreshMarketStructureBuffers(){

   //--- Get the last 200 short-term swing low points
   int copiedShortTermSwingLows = CopyBuffer(larryWilliamsMarketStructureIndicatorHandle, 0, 0, 200, shortTermLows);
   if(copiedShortTermSwingLows == -1){
      Print("Error while copying short-term swing lows: ", GetLastError());
      return;
   }
   
   //--- Get the last 200 short-term swing high points
   int copiedShortTermSwingHighs = CopyBuffer(larryWilliamsMarketStructureIndicatorHandle, 1, 0, 200, shortTermHighs);
   if(copiedShortTermSwingHighs == -1){
      Print("Error while copying short-term swing highs: ", GetLastError());
      return;
   }
   
   //--- Get the last 200 intermediate swing low points
   int copiedIntermediateSwingLows = CopyBuffer(larryWilliamsMarketStructureIndicatorHandle, 2, 0, 200, intermediateTermLows);
   if(copiedIntermediateSwingLows == -1){
      Print("Error while copying intermediate swing lows: ", GetLastError());
      return;
   }
   
   //--- Get the last 200 intermediate swing high points
   int copiedIntermediateSwingHighs = CopyBuffer(larryWilliamsMarketStructureIndicatorHandle, 3, 0, 200, intermediateTermHighs);
   if(copiedIntermediateSwingHighs == -1){
      Print("Error while copying intermediate swing highs: ", GetLastError());
      return;
   }
   
   //--- Treat the following arrays as timeseries (index 0 becomes the most recent bar)
   ArraySetAsSeries(shortTermLows,  true);
   ArraySetAsSeries(shortTermHighs, true);
   ArraySetAsSeries(intermediateTermLows,  true);
   ArraySetAsSeries(intermediateTermHighs, true);
      
}

この関数はCopyBufferを使用して、インジケーターから最新のスイングポイントデータを取得します。各呼び出しは、特定のバッファから最大200個の値を対応する配列にコピーします。

短期的な安値と高値が最初に読み込まれ、続いて中期的な安値と高値が読み込まれます。CopyBuffer呼び出しの後には毎回エラーチェックをおこない、データが有効であることを確認します。コピーが失敗した場合、その時点で関数を終了し、不完全なデータを使って処理が進まないようにしています。

関数の最後では、配列を再び時系列データとして扱うように設定します。そのため、内部のメモリ配置に変化があったとしても、インデックス0が常に最新のバーを指す状態が保たれます。

この関数のおかげで、EA全体のロジックは非常にすっきり保たれます。インジケーターデータをあちこちで個別に取得するのではなく、必要なときにこの関数を呼び出すだけで、一括して更新できるからです。

市場構造シグナルはバーが確定して初めて有効になります。そのためEAは、新しいバーが始まったタイミングでのみ反応すべきです。これを検出するために、次の関数を定義します。

//+------------------------------------------------------------------+
//| Function to check if there's a new bar on a given chart timeframe|
//+------------------------------------------------------------------+
bool IsNewBar(string symbol, ENUM_TIMEFRAMES tf, datetime &lastTm)
{

   datetime currentTm = iTime(symbol, tf, 0);
   if(currentTm != lastTm){
      lastTm       = currentTm;
      return true;
   }  
   return false;
   
}

この関数は、現在のバーの始値時刻を保存された値と比較します。時刻が異なれば、新たなバーが形成されています。その後、関数は保存されている値を更新し、trueを返します。

このロジックをサポートするために、グローバル変数を宣言します。

//--- To help track new bar open
datetime lastBarOpenTime;

この変数は、直前に処理したバーの始値時刻を記録します。OnInit内では初期値として0が設定されるため、最初のバーは必ず正しく検出されるようになっています。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- Initialize global variables
   lastBarOpenTime = 0;

   return(INIT_SUCCEEDED);
}

インジケーターの最新データが取得できるようになったことで、シグナルロジックを定義できるようになります。最初の関数では、買いシグナルを判定します。

//+---------------------------------------------------------------------------+
//| Checks whether current market structure conditions generate a buy signal  |
//+---------------------------------------------------------------------------+
bool IsBuySignal(){

   if(shortTermLows[2] == EMPTY_VALUE){
      return false;
   }
   
   int commonIndex = -1;
   for(int i = 3; i < ArraySize(shortTermLows); i++){
      if(shortTermLows[i] != EMPTY_VALUE){
         commonIndex = i;
         break;
      }
   }
   
   if(commonIndex == -1){
      return false;
   }
   
   if(intermediateTermLows[commonIndex] != EMPTY_VALUE){
      return true;
   }
   
   return false;
   
}

この関数ではまず、短期的なスイングローが直近で形成されたかどうかを確認します。これには特定のバーインデックスをチェックします。短期的なスイングローが存在しない場合、その時点で関数は即座に終了します。

bool IsBuySignal(){

   if(shortTermLows[2] == EMPTY_VALUE){
      return false;
   }
   
   ...
   
}

次に、この関数は過去方向にさかのぼって、直近で確定した短期的スイングローを探します。そのインデックスが特定されると、それが中期的スイングローの配列と照合されます。

bool IsBuySignal(){

   ...
   
   int commonIndex = -1;
   for(int i = 3; i < ArraySize(shortTermLows); i++){
      if(shortTermLows[i] != EMPTY_VALUE){
         commonIndex = i;
         break;
      }
   }
   
   if(commonIndex == -1){
      return false;
   }
   
   ...
   
}

その水準に中期的な安値が存在する場合、それは短期的な構造が中期的な安値を確認したことを意味します。その時点で、関数はtrueを返します。それ以外の場合はfalseを返します。

bool IsBuySignal(){

   ...
   
   if(intermediateTermLows[commonIndex] != EMPTY_VALUE){
      return true;
   }
   
   return false;
   
}

この論理は、ラリー・ウィリアムズの「スイングの入れ子構造」という考え方をそのまま反映しています。上位のスイングは、下位の構造によって確認されて初めて売買シグナルとして成立するという発想です。

売りシグナルの関数も同じ構造ですが、ロジックは逆になります。安値ではなく、高値を対象として処理をおこないます。

//+---------------------------------------------------------------------------+
//| Checks whether current market structure conditions generate a sell signal |
//+---------------------------------------------------------------------------+
bool IsSelSignal(){

   if(shortTermHighs[2] == EMPTY_VALUE){
      return false;
   }
   
   int commonIndex = -1;
   for(int i = 3; i < ArraySize(shortTermHighs); i++){
      if(shortTermHighs[i] != EMPTY_VALUE){
         commonIndex = i;
         break;
      }
   }
   
   if(commonIndex == -1){
      return false;
   }
   
   if(intermediateTermHighs[commonIndex] != EMPTY_VALUE){
      return true;
   }
   
   return false;
   
}

この関数は、まず短期的スイングハイが形成されているかを確認し、その中から直近で有効なものを特定します。次に、そのインデックスに対応する中期的スイングハイが存在するかどうかを検証します。存在していれば、その時点で売りシグナルが成立したと判断されます。

このロジックは対称的であるため、買いシグナルの仕組みを理解していれば、売りシグナルの動きも自然に理解できます。

実際に取引をおこなう前に、まずシグナル検出が正しく機能していることを確認することが重要です。そのために、現時点ではポジションを開く代わりに、これらの関数をOnTick内で呼び出し、結果をログとして出力するようにします。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   ...

   //--- Execute logic only when a new bar opens
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
   
      //--- Get updated market structure data
      RefreshMarketStructureBuffers();
      
      //--- Handle Buy signals
      if(IsBuySignal()){
         Print("Intermediate low confirmed!");
      }
      
      //--- Handle Sell signals
      if(IsSelSignal()){
         Print("Intermediate high confirmed!");
      }      
   }      
}

この段階では、EAは検出されたシグナルをターミナルのログに出力するだけの状態です。これにより、チャート上のインジケーターとログに表示されるメッセージを視覚的に比較し、すべてが正しく一致しているかを確認できます。

この挙動が問題なく動作することを確認できたら、次のセクションでは実際の取引ロジックへと進みます。



シグナルから取引へ

市場構造からスイングシグナルを安定して検出できるようになったので、次の自然なステップは、それを実際の取引へと変換することです。このセクションでは、EAの取引ロジックを導入します。目的は、取引の実行方法をユーザーが完全にコントロールできるようにしながら、内部ロジック自体はシンプルで構造化された状態に保つことです。

取引関数を記述する前に、それらを支える変数と設定を定義する必要があります。これらの設定によって、取引方向、ポジションサイズ、リスク、ストップロスの配置、そしてリワード目標などをユーザーが調整できるようになります。

取引方向の制御

市場環境は常に中立とは限りません。トレーダーは、トレンドの方向にのみ取引したいと判断する場合もあります。これを実現するために、許可される取引方向を定義するカスタム列挙型を導入します。

//+------------------------------------------------------------------+
//| Custom Enumerations                                              |
//+------------------------------------------------------------------+
enum ENUM_TRADE_DIRECTION  
{ 
   ONLY_LONG, 
   ONLY_SHORT, 
   TRADE_BOTH 
};

ONLY_LONGはロングポジションのみを許可します。ONLY_SHORTはショートポジションのみを許可します。TRADE_BOTHは両方を許可します。

この選択肢を入力パラメータとしてユーザーに提示します。

...

input group "Trade and Risk Management"
input ENUM_TRADE_DIRECTION            direction  = TRADE_BOTH;

デフォルトでは、このEAは両方向の取引を許可しています。EAをチャートに適用する際、ユーザーは自身の市場バイアスに応じてこの挙動を変更できます。その結果、ソースコードを変更することなく柔軟に対応することが可能になります。

ロットサイズ計算モード

次に、ポジションサイズの算出方法をユーザーが選択できるようにします。固定ロットを好むトレーダーもいれば、リスクベースでポジションサイズを決めるトレーダーもいます。そのため、2つのモードを持つ別の列挙型を定義します。

enum ENUM_LOT_SIZE_INPUT_MODE 
{ 
   MODE_MANUAL, 
   MODE_AUTO 
};

MODE_MANUALでは、固定ロットサイズを使用します。MODE_AUTOは、口座のリスクに基づいてロットサイズを計算します。

自動モードが選択されている場合、EAは口座残高に対するユーザー定義のリスクパーセンテージを基準にロットサイズを算出します。このパーセンテージは、ストップロスに到達した際に許容される最大損失額を示します。一方、手動モードが選択されている場合は、ユーザーが指定した固定ロットサイズがそのまま使用されます。

input ENUM_LOT_SIZE_INPUT_MODE      lotSizeMode  = MODE_AUTO;
input double                 riskPerTradePercent = 1.0;
input double                             lotSize = 5.0;

このアプローチにより、慎重なトレーダーと積極的なトレーダーの両方が、同じEAを無理なく利用できるようになります。

市場構造に基づくストップロス設定

このEAは市場構造に基づいて取引をおこなうため、ストップロスの設定も同様に論理的であるべきです。そのため、ストップロスの配置方法についてもユーザーが選択できるようにします。具体的には、直近の短期的スイングポイントに置くか、あるいは直近で確定した中期的スイングポイントに置くかを選択できるようにします。

enum ENUM_STOP_LOSS_STRUCTURE{
   SL_AT_SHORT_TERM_SWING,
   SL_AT_INTERMEDIATE_SWING
};

この選択は、ストップロスをどの程度タイトにするか、またはワイドにするかに影響します。短期的なスイングではよりタイトなストップとなり、中期的なスイングでは価格変動に対してより余裕を持たせることになります。ユーザーは自身の取引スタイルに最も適したものを選択します。

input ENUM_STOP_LOSS_STRUCTURE stopLossStructure = SL_AT_INTERMEDIATE_SWING;

有効なストップ距離の範囲

市場構造は動的であるため、ストップ距離が短すぎたり長すぎたりする場合があります。どちらのケースも望ましくない可能性があります。

これを制御するために、ユーザーはストップ距離の最小値と最大値をポイント単位で定義します。この範囲外となる取引はすべて無視されます。これによって、リスク特性の悪いポジションへのエントリーからEAを保護します。

input int              minimumStopDistancePoints = 100;
input int              maximumStopDistancePoints = 600;

リスクリワード設定

また、ユーザーがあらかじめ定義されたリスクリワード比率を選択できるようにしています。

enum ENUM_RISK_REWARD_RATIO   
{ 
   ONE_TO_ONE, 
   ONE_TO_ONEandHALF, 
   ONE_TO_TWO, 
   ONE_TO_THREE, 
   ONE_TO_FOUR, 
   ONE_TO_FIVE, 
   ONE_TO_SIX 
};

各オプションは、リスク1単位あたりでどれだけのリワード単位を狙うかを指定しています。この値は後の処理で使用され、テイクプロフィット水準を自動的に計算するために利用されます。

input ENUM_RISK_REWARD_RATIO     riskRewardRatio = ONE_TO_TWO;

保有ポジションの追跡

ポジションがオープンされた後は、その詳細情報を追跡する必要があります。そのため、トレーリングストップや取引管理などの将来的な機能を効果的に実装することが可能になります。 

そのために、アクティブポジションの重要な情報を保持する構造体を定義します。これには、建値、ストップロス、テイクプロフィット、ロットサイズ、そしてオープン時間が含まれます。その後、現在のポジションデータを保存するために、この構造体のグローバルインスタンスを宣言します。

//+------------------------------------------------------------------+
//| Data Structures                                                  |
//+------------------------------------------------------------------+
struct MqlTradeInfo
{
   ulong orderTicket;                 
   ENUM_ORDER_TYPE type;
   ENUM_POSITION_TYPE posType;
   double entryPrice;
   double takeProfitLevel;
   double stopLossLevel;
   datetime openTime;
   double lotSize;   
};

//--- Instantiate the trade information data structure
MqlTradeInfo tradeInfo

ポイント値の初期化

ストップ距離の検証には、銘柄ごとのポイントサイズの情報が必要です。銘柄によってポイント値は異なるため、この値はグローバル変数として保持します。

//--- The size of a point for this financial security
double pointValue;

OnInit関数内で銘柄プロパティを使用してこの値を初期化します。これにより、ストップ距離の計算が常に現在の銘柄に対して正確におこなわれるようになります。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   pointValue      = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

   return(INIT_SUCCEEDED);
}

ロングポジションを開く

OpenBuy関数は、買いの成行注文を発注します。この関数は、いくつかの重要なステップを構造的に実行します。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){

   ENUM_ORDER_TYPE action          = ORDER_TYPE_BUY;
   ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY;
   datetime currentTime            = TimeCurrent();
   double contractSize             = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double accountBalance           = AccountInfoDouble(ACCOUNT_BALANCE);
   double rewardValue              = 1.0;
   
   switch(riskRewardRatio){
      case ONE_TO_ONE: 
         rewardValue = 1.0;
         break;
      case ONE_TO_ONEandHALF:
         rewardValue = 1.5;
         break;
      case ONE_TO_TWO: 
         rewardValue = 2.0;
         break;
      case ONE_TO_THREE: 
         rewardValue = 3.0;
         break;
      case ONE_TO_FOUR: 
         rewardValue = 4.0;
         break;
      case ONE_TO_FIVE: 
         rewardValue = 5.0;
         break;
      case ONE_TO_SIX: 
         rewardValue = 6.0;
         break;
      default:
         rewardValue = 1.0;
         break;
   }
   
   double stopLevel = 0;
   
   if(stopLossStructure == SL_AT_SHORT_TERM_SWING  ){
      stopLevel = NormalizeDouble(shortTermLows[2], Digits());
   }
   
   if(stopLossStructure == SL_AT_INTERMEDIATE_SWING){
   
      for(int i = 0; i < ArraySize(intermediateTermLows); i++){
         if(intermediateTermLows[i] != EMPTY_VALUE){
            stopLevel = NormalizeDouble(intermediateTermLows[i], Digits());
            break;
         }
      }      
   }
   
   double stopDistance = NormalizeDouble(askPr - stopLevel, Digits());
   if(stopDistance > (maximumStopDistancePoints * pointValue) || stopDistance < (minimumStopDistancePoints * pointValue)){
      Print("The Stop Distance falls outside desired distance range");
      return false;
   }
   
   double targetLevel  = NormalizeDouble(askPr + (rewardValue * stopDistance), Digits());
   
   double volume       = NormalizeDouble(lotSize, 2);
   if(lotSizeMode == MODE_AUTO){
      double amountAtRisk = (riskPerTradePercent / 100.0) *  accountBalance;
      volume              = amountAtRisk / (contractSize * stopDistance);
      volume              = NormalizeDouble(volume, 2);
   }
   
   if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){
      Print("Error while opening a long position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }else{
      MqlTradeResult result = {};
      Trade.Result(result);
      tradeInfo.orderTicket                 = result.order;
      tradeInfo.type                        = action;
      tradeInfo.posType                     = positionType;
      tradeInfo.entryPrice                  = result.price;
      tradeInfo.takeProfitLevel             = targetLevel;
      tradeInfo.stopLossLevel               = stopLevel;
      tradeInfo.openTime                    = currentTime;
      tradeInfo.lotSize                     = result.volume;
      
      return true;
   }
   
   return false;
}

まず、選択されたリスクリワード比を取得し、それを数値のリワード値に変換します。この値は後にテイクプロフィット水準の計算に使用されます。

次にストップロス水準を決定します。ユーザーの選択に応じて、ストップロスは直近の短期的安値または直近の中期的安値のいずれかに設定されます。

ストップ水準が確定した後、ストップ距離を計算します。この距離はユーザーが定義した最小~最大の範囲と照合されます。範囲外の場合、そのエントリーはスキップされます。

テイクプロフィットは、リワード値とストップ距離を基に計算されます。その結果、すべての取引が選択されたリスクリワードプロファイルに従うようになります。

次に取引量を決定します。手動モードの場合は固定ロットサイズを使用し、自動モードの場合は口座残高、リスク率、契約サイズ、ストップ距離に基づいてロットサイズを計算します。

最後に、買い注文をサーバーへ送信します。取引が成功した場合、後続の処理に備えて関連するすべての取引情報が取引情報構造体に保存されます。

ショートポジションを開く

OpenSel関数は買い関数と同じロジックに従いますが、方向が逆になります。ストップロスはスイングローではなくスイングハイから取得され、価格計算もそれに応じて反転されます。

//+------------------------------------------------------------------+
//| Function used to open a market sell order.                       |   
//+------------------------------------------------------------------+
bool OpenSel( const double bidPr){

   ENUM_ORDER_TYPE action          = ORDER_TYPE_SELL;
   ENUM_POSITION_TYPE positionType = POSITION_TYPE_SELL;
   datetime currentTime            = TimeCurrent();   
   double contractSize             = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double accountBalance           = AccountInfoDouble(ACCOUNT_BALANCE);
   double rewardValue              = 1.0;
   
   switch(riskRewardRatio){
      case ONE_TO_ONE: 
         rewardValue = 1.0;
         break;
      case ONE_TO_ONEandHALF:
         rewardValue = 1.5;
         break;
      case ONE_TO_TWO: 
         rewardValue = 2.0;
         break;
      case ONE_TO_THREE: 
         rewardValue = 3.0;
         break;
      case ONE_TO_FOUR: 
         rewardValue = 4.0;
         break;
      case ONE_TO_FIVE: 
         rewardValue = 5.0;
         break;
      case ONE_TO_SIX: 
         rewardValue = 6.0;
         break;
      default:
         rewardValue = 1.0;
         break;
   }
   
   double stopLevel = 0;
   
   if(stopLossStructure == SL_AT_SHORT_TERM_SWING  ){
      stopLevel = NormalizeDouble(shortTermHighs[2], Digits());
   }
   
   if(stopLossStructure == SL_AT_INTERMEDIATE_SWING){
   
      for(int i = 0; i < ArraySize(intermediateTermHighs); i++){
         if(intermediateTermHighs[i] != EMPTY_VALUE){
            stopLevel = NormalizeDouble(intermediateTermHighs[i], Digits());
            break;
         }
      }
      
   }
   
   double stopDistance = NormalizeDouble(stopLevel - bidPr, Digits());
   if(stopDistance > (maximumStopDistancePoints * pointValue) || stopDistance < (minimumStopDistancePoints * pointValue)){
      Print("The Stop Distance falls outside desired distance range");
      return false;
   }
   
   double targetLevel  = NormalizeDouble(bidPr - (rewardValue * stopDistance), Digits());
   double volume       = NormalizeDouble(lotSize, 2);
   if(lotSizeMode == MODE_AUTO){
      double amountAtRisk = (riskPerTradePercent / 100.0) *  accountBalance;
      volume              = amountAtRisk / (contractSize * stopDistance);
      volume              = NormalizeDouble(volume, 2);
   }
   
   if(!Trade.Sell(volume, _Symbol, bidPr, stopLevel, targetLevel)){
      Print("Error while opening a short position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }else{ 
      MqlTradeResult result = {};
      Trade.Result(result);
      tradeInfo.orderTicket                 = result.order;
      tradeInfo.type                        = action;
      tradeInfo.posType                     = positionType;
      tradeInfo.entryPrice                  = result.price;
      tradeInfo.takeProfitLevel             = targetLevel;
      tradeInfo.stopLossLevel               = stopLevel;
      tradeInfo.openTime                    = currentTime;
      tradeInfo.lotSize                     = result.volume;
      
      return true;
   }
   
   return false;   
}

この構造は買いロジックをそのまま反転した設計になっているため、ロジックの重複を避けつつも挙動の一貫性を維持できます。

ポジションの存在確認

新規ポジションを開く前に、EAは既にアクティブなポジションが存在しないことを確認する必要があります。この処理のために、2つのユーティリティ関数を導入します。

//+------------------------------------------------------------------+
//| To verify whether this EA currently has an active buy position.  |                                 |
//+------------------------------------------------------------------+
bool IsThereAnActiveBuyPosition(ulong magic){
   
   for(int i = PositionsTotal() - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0){
         Print("Error while fetching position ticket ", _LastError);
         continue;
      }else{
         if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
            return true;
         }
      }
   }
   
   return false;
}

//+------------------------------------------------------------------+
//| To verify whether this EA currently has an active sell position. |                                 |
//+------------------------------------------------------------------+
bool IsThereAnActiveSellPosition(ulong magic){
   
   for(int i = PositionsTotal() - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0){
         Print("Error while fetching position ticket ", _LastError);
         continue;
      }else{
         if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
            return true;
         }
      }
   }
   
   return false;
}

一方の関数はアクティブな買いポジションの有無を確認し、もう一方の関数はアクティブな売りポジションの有無を確認します。各関数はすべての保有ポジションを走査し、マジックナンバーとポジションタイプによってフィルタリングします。

これによって、EAは常に一度に1つのポジションのみを開くようになり、競合するポジションの発生を防止します。

OnTick内での取引の実行

すべてのコンポーネントが揃ったら、最後のステップは、取引ロジックをメインの実行ループに接続することです。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   //--- Scope variables
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID);

   //--- Execute logic only when a new bar opens
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
   
      //--- Get updated market structure data
      RefreshMarketStructureBuffers();
      
      //--- Handle Buy signals
      if(IsBuySignal()){
      
         //--- Open a long position if there is no active position
         if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
            OpenBuy(askPrice);
         }
      }
      
      //--- Handle Sell signals
      if(IsSelSignal()){
         
         //--- Open a short position if there is no active position
         if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
            OpenSel(bidPrice);
         }
      }
            
   }
   
}

OnTick関数内では、まず現在のBid価格とAsk価格を更新します。次に、新しいバーが形成されたかどうかを確認します。取引ロジックは重複シグナルを避けるため、新しいバーでのみ実行されます。

買いシグナルが検出された場合、EAは既にアクティブなポジションが存在しないことを確認します。条件が満たされている場合、買い注文が出されます。

同様のロジックが売りシグナルにも適用されます。これで、市場構造の検出から取引実行までの一連のプロセスが完了します。

この時点で、EAは完全に機能し、市場構造シグナルを制御された設定可能な方法で取引できる状態になります。



ダイナミックステップトレーリングストップの追加

この段階で、EAはシグナル検出、ポジションオープン、リスク管理を正しく実行できるようになっています。最後の要素はポジション保護です。このセクションでは、価格がターゲットに向かって動くにつれて利益を段階的に確保するダイナミックなトレーリングストップを追加します。トレーリングストップはオプションで、EAをチャートに適用する際に有効化または無効化できます。これによりシステムの柔軟性が保たれ、さまざまな取引スタイルに対応できます。

トレーリングストップの有効化または無効化

まず、ブール型の入力パラメータを追加します。これはシンプルなスイッチとして機能します。

input bool                    enableTrailingStop = false;

この値が真この値がtrueの場合、EAはトレーリングストップを積極的に管理します。falseの場合は、トレードはストップロスまたはテイクプロフィットに達するまで介入なしで維持されます。この判断は完全にユーザーに委ねられます。

ステップトレーリングストップ構造の定義

トレーリングストップは段階的なシステムとして実装されます。ストップロスを連続的に移動させるのではなく、価格が目標に向かって進むにつれて、あらかじめ定義された段階で更新されます。

この動作をサポートするために、トレーリングストップ情報を保持する構造体を定義します。

//--- Instantiate the trade information data structure
MqlTradeInfo tradeInfo;

struct MqlTrailingStop
{
   double level1;
   double level2;
   double level3;
   double level4;
   double level5;
   
   double stopLevel1;
   double stopLevel2;
   double stopLevel3;
   double stopLevel4;
   double stopLevel5;
   
   bool isLevel1Active;
   bool isLevel2Active;
   bool isLevel3Active;
   bool isLevel4Active;
   bool isLevel5Active;
};

//--- Instantiate the trailing stop structure
MqlTrailingStop trailingStop;

最初の5つのフィールドは、ストップ更新が許可される前に価格が超える必要のあるレベルを保持します。これらはトリガーレベルです。続く5つのフィールドは、各トリガーが到達した際に適用されるストップロスレベルを保持します。これらはストップロスが移動される位置を示します。最後の5つのブール型フィールドは、各ステップがすでに発動済みかどうかを追跡します。これによって、EAが同じストップ更新を複数回適用することを防止します。簡単に言うと、価格は一度だけ各レベルを超えることでストップ更新が解放され、その後EAはそのステップが処理済みであることを記憶します。トレーリング全体の距離は6等分され、エントリーからテイクプロフィットまでの間に5つのトレーリングステップが作成されます。

価格クロスの検出

トレーリングロジックは、価格が特定のレベルを超えたタイミングを検出することに依存します。これを明確に処理するために、2つの小さなユーティリティ関数を導入します。

//+------------------------------------------------------------------+
//| To detect a crossover at a given price level                     |                               
//+------------------------------------------------------------------+
bool IsCrossOver(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] <= price && closePriceMinsData[0] > price){
      return true;
   }
   return false;
}


//+------------------------------------------------------------------+
//| To detect a crossunder at a given price level                    |                               
//+------------------------------------------------------------------+
bool IsCrossUnder(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] >= price && closePriceMinsData[0] < price){
      return true;
   }
   return false;
}

一方の関数は、価格があるレベルを上抜けしたかどうかを検出します。もう一方の関数は、価格がそのレベルを下抜けしたかどうかを検出します。各関数は、直前の終値と現在の終値を比較することで、クロスが発生したかどうかを判定します。これらの関数は2つの入力を受け取ります。1つ目は判定対象となるレベルであり、2つ目は直近の終値配列です。

分足レベルの価格データの保存

正確なクロス検出をサポートするために、直近の分足終値をグローバル配列に保存します。この配列は時系列として扱われるため、インデックス0が常に最新の値を表します。

//--- To store minutes data
double closePriceMinutesData [];

OnTick関数内では、この配列はすべてのティックごとに更新され、分足の時間足データが使用されます。その結果、トレーリングストップの判定が遅延したバー情報ではなく、最新の価格変動に基づいておこなわれるようになります。データのコピーに失敗した場合、EAは無効な情報に基づいて判断をおこなわないよう、早期に処理を終了します。

取引開始時にトレーリングストップレベルを準備する

トレーリングストップの各レベルは、新しいポジションが開かれた直後に計算される必要があります。これにより、EAは最初の段階から各トレーリングステップの位置を正確に把握できます。買い注文および売り注文の両方の関数内で、取引が正常に約定した後にトレーリングストップ構造体を再初期化します。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){

   ...
   
   if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){
   
   ...
   
   }else{

      ...
      
      //--- Refill the trailing Stop struct
      double targetDistance       = targetLevel - askPr;
      double trailingStep         = NormalizeDouble(targetDistance / 6,   Digits());
      trailingStop.level1         = NormalizeDouble(askPr + trailingStep, Digits());
      trailingStop.level2         = NormalizeDouble(trailingStop.level1 + trailingStep, Digits());      
      trailingStop.level3         = NormalizeDouble(trailingStop.level2 + trailingStep, Digits());
      trailingStop.level4         = NormalizeDouble(trailingStop.level3 + trailingStep, Digits());      
      trailingStop.level5         = NormalizeDouble(trailingStop.level4 + trailingStep, Digits());
      
      trailingStop.stopLevel1     = NormalizeDouble(stopLevel + trailingStep, Digits());
      trailingStop.stopLevel2     = NormalizeDouble(trailingStop.stopLevel1 + trailingStep, Digits());
      trailingStop.stopLevel3     = NormalizeDouble(trailingStop.stopLevel2 + trailingStep, Digits());
      trailingStop.stopLevel4     = NormalizeDouble(trailingStop.stopLevel3 + trailingStep, Digits());
      trailingStop.stopLevel5     = NormalizeDouble(trailingStop.stopLevel4 + trailingStep, Digits());
      
      trailingStop.isLevel1Active = false;
      trailingStop.isLevel2Active = false;
      trailingStop.isLevel3Active = false;
      trailingStop.isLevel4Active = false;
      trailingStop.isLevel5Active = false;
      
      return true;
   }
   
   return false;
}

//+------------------------------------------------------------------+
//| Function used to open a market sell order.                       |   
//+------------------------------------------------------------------+
bool OpenSel( const double bidPr){

   ...
   
   if(!Trade.Sell(volume, _Symbol, bidPr, stopLevel, targetLevel)){
      
      ...
      
      return false;
   }else{ 

      ...
      
      //--- Refill the trailing Stop struct
      double targetDistance       = bidPr - targetLevel;
      double trailingStep         = NormalizeDouble(targetDistance / 6,   Digits());
      trailingStop.level1         = NormalizeDouble(bidPr - trailingStep, Digits());
      trailingStop.level2         = NormalizeDouble(trailingStop.level1 - trailingStep, Digits());
      trailingStop.level3         = NormalizeDouble(trailingStop.level2 - trailingStep, Digits());
      trailingStop.level4         = NormalizeDouble(trailingStop.level3 - trailingStep, Digits());
      trailingStop.level5         = NormalizeDouble(trailingStop.level4 - trailingStep, Digits());
      
      trailingStop.stopLevel1     = NormalizeDouble(stopLevel - trailingStep, Digits());
      trailingStop.stopLevel2     = NormalizeDouble(trailingStop.stopLevel1 - trailingStep, Digits());
      trailingStop.stopLevel3     = NormalizeDouble(trailingStop.stopLevel2 - trailingStep, Digits());
      trailingStop.stopLevel2     = NormalizeDouble(trailingStop.stopLevel3 - trailingStep, Digits());
      trailingStop.stopLevel3     = NormalizeDouble(trailingStop.stopLevel4 - trailingStep, Digits());
      
      trailingStop.isLevel1Active = false;
      trailingStop.isLevel2Active = false;
      trailingStop.isLevel3Active = false;
      trailingStop.isLevel4Active = false;
      trailingStop.isLevel5Active = false;
      return true;
   }
   
   return false;  
}

まず、エントリーからテイクプロフィットまでの距離を計算します。この距離は6等分され、トレーリングステップのサイズとして使用されます。買いではトリガーレベルは建値より上に配置され、売りではトリガーレベルは建値より下に配置されます。対応するストップロスレベルは、元のストップロスから同じ方向に移動されます。すべてのステップの有効フラグはfalseにリセットされます。これによって、新しいポジション専用のトレーリング追跡構造が準備されます。

リアルタイムでのトレーリングストップ管理

取引がアクティブな状態では、トレーリングストップの管理は専用のユーティリティ関数によって処理されます。

//+------------------------------------------------------------------+
//| To track price action and updates the trailing stop              |   
//+------------------------------------------------------------------+
void ManageTrailingStop(){

   int totalPositions = PositionsTotal();
   //--- Loop through all open positions
   for(int i = totalPositions - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket != 0){
         // Get some useful position properties
         ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);  
         string symbol                   = PositionGetString (POSITION_SYMBOL);
         ulong magic                     = PositionGetInteger(POSITION_MAGIC);
         double targetLevel              = PositionGetDouble(POSITION_TP);
         if(positionType == POSITION_TYPE_BUY ){
            if(symbol == _Symbol && magic == magicNumber){
            
               if(IsCrossOver(trailingStop.level1, closePriceMinutesData) && !trailingStop.isLevel1Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel1, targetLevel)){
                     Print("Error while trailing SL at level 1: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel1Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level2, closePriceMinutesData) && !trailingStop.isLevel2Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel2, targetLevel)){
                     Print("Error while trailing SL at level 2: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel2Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level3, closePriceMinutesData) && !trailingStop.isLevel3Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel3, targetLevel)){
                     Print("Error while trailing SL at level 3: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel3Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level4, closePriceMinutesData) && !trailingStop.isLevel4Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel4, targetLevel)){
                     Print("Error while trailing SL at level 4: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel4Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level5, closePriceMinutesData) && !trailingStop.isLevel5Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel5, targetLevel)){
                     Print("Error while trailing SL at level 5: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel5Active = true;
                  }
               }
            }
         }
         
               
         if(positionType == POSITION_TYPE_SELL){
            if(symbol == _Symbol && magic == magicNumber){
            
               if(IsCrossUnder(trailingStop.level1, closePriceMinutesData) && !trailingStop.isLevel1Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel1, targetLevel)){
                     Print("Error while trailing SL at level 1: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel1Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level2, closePriceMinutesData) && !trailingStop.isLevel2Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel2, targetLevel)){
                     Print("Error while trailing SL at level 2: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel2Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level3, closePriceMinutesData) && !trailingStop.isLevel3Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel3, targetLevel)){
                     Print("Error while trailing SL at level 3: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel3Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level4, closePriceMinutesData) && !trailingStop.isLevel4Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel4, targetLevel)){
                     Print("Error while trailing SL at level 4: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel4Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level5, closePriceMinutesData) && !trailingStop.isLevel5Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel5, targetLevel)){
                     Print("Error while trailing SL at level 5: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel5Active = true;
                  }
               }      
            }
         }
      }
   }  
}

この関数はすべての保有ポジションをループし、シンボルとマジックナンバーによってフィルタリングします。そのため、EAが保有するポジションのみが管理対象となります。

買いポジションの場合、価格が下からトレーリングトリガーレベルを上抜けしたかどうかを確認します。あるレベルが初めてクロスされた場合、その対応するストップロスレベルへストップロスが移動され、そのステップはアクティブとしてマークされます。

売りポジションの場合も同様のロジックが逆方向で適用されます。価格がトリガーレベルを下抜けしたことを検出し、それに応じてストップロスが更新されます。

各ステップは一度のみ適用されます。価格が戻って再び同じレベルをクロスしても、それ以上の処理はおこなわれません。これにより、ストップの移動は整然と予測可能な挙動になります。

ストップロスの変更に失敗した場合は、デバッグ用にエラーメッセージが出力されます。

EAへのトレーリングストップの統合

トレーリングストップ関数は、OnTick関数内で呼び出すことができます。ただし、この処理はユーザーがトレーリングストップ機能を有効にしている場合にのみ実行されるようにします。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   ...
   
   //--- Manage trailing stop
   if(enableTrailingStop){
      ManageTrailingStop();
   }   
}

この最終的な追加により、EAはポジションのライフサイクル全体を完全に制御できるようになります。構造に基づいたシグナルを検出し、リスクを管理した上でポジションをオープンし、構造化されたトレーリングストップによって利益を保護します。これによりEAのコア開発は完了します。今後はブレークイーブンロジックや部分決済などの機能を、この安定した基盤の上に追加していくことができます。

本記事で開発した完全なソースコードは、記事の添付ファイルとして同梱されています。いつでもダウンロードして内容を確認したり、ミスの修正や自身の実装との比較に利用することができます。



テストと結果

完全な取引ロジックが実装されたため、次のステップは実際の市場環境下でEAの性能を検証することです。そのために、EAはMetaTrader 5のストラテジーテスターを使用し、履歴データ上でバックテストを実施しました。テスト対象はゴールドで、時間足はH1です。テスト期間は2025年1月1日から2025年11月30日までとし、初期口座残高は10,000ドルに設定されました。テスト中のすべての取引はEAによって完全自動で実行され、手動介入は一切ありません。

テストで使用された設定は、本記事全体で説明したストラテジーロジックを反映しています。使用した詳細な入力設定はセットファイルとして記事に添付されており、読者はそれを使用して自身の環境で同じ結果を再現することができます。

テスト期間終了時点で、EAは合計8,950.01ドルの純利益を記録しました。これは約11か月間で約80%の資産成長に相当します。この期間にわたってこのレベルのリターンを達成していることは、体系的な市場ロジックと、規律あるリスク管理、そしてルールに基づいた運用を組み合わせることの強さを示しています。

この結果は、構造化された市場ロジックと規律あるリスク管理、そしてルールベースの実行を組み合わせることの有効性を示しています。エクイティカーブは、急激な上昇や不安定な変動ではなく、滑らかで安定した推移を示しています。

エクイティカーブ

テストレポート

これは、利益が時間とともに一貫して積み上げられており、ストラテジーのルールによってドローダウンが適切に抑制されていたことを示しています。

テスト結果から、このEAはランダムなエントリーや単発的な市場状況に依存していないことが分かります。代わりに、有意な市場構造ポイントを繰り返し特定し、その条件が揃ったときのみ行動することでパフォーマンスを得ています。これはラリー・ウィリアムズの市場構造の基本概念、すなわち価格変動は純粋なランダムではなく、識別可能で再現性のあるパターンに従うという考え方を支持するものです。


結論

本記事では、完全な取引アイデアをMQL5を用いた完全動作するEAへと実装しました。ラリー・ウィリアムズによる市場構造の客観的定義を出発点とし、短期的および中期的スイングポイントをコードへと変換し、それらを用いて明確かつ再現可能な売買シグナルを生成しました。これにより、市場分析における主観的な判断を排除し、テスト、検証、改善が可能なルールベースの仕組みに置き換えています。

シグナル生成にとどまらず、実用的な取引システムも構築しました。このEAには、取引方向の制御、柔軟なリスク管理、構造に基づくストップロス配置、固定リスクリワード設定、そしてオプションのステップトレーリングストップが含まれています。各機能は明確な目的のもとで追加されており、モジュール化された設計によりロジックの可読性、テスト容易性、拡張性が保たれています。

この取り組みの重要な成果は、戦略そのものだけでなく、その構築プロセスにもあります。本記事全体を通して、責務を小さなユーティリティ関数に分割すること、ユーザー選択に列挙型を用いること、そして取引およびトレーリングロジックを管理するために明確なデータ構造を使用することなど、保守性の高いMQL5コードのベストプラクティスを採用しました。このアプローチにより、EAはデバッグや修正が容易になり、より高度なシステムの基盤としても適しています。

バックテスト結果は、市場構造のルールベース解釈が、規律あるリスク管理と組み合わさることで一貫したパフォーマンスを生み出し得ることを示しています。さらに重要なのは、本EAが読者に対して完全な実験フレームワークを提供している点です。異なる銘柄、時間足、リスクパラメータ、ストップロス構造をテストすることで、市場の挙動がどのように変化し、どの条件で戦略が最も機能するかを検証することができます。

最後に、本記事はラリー・ウィリアムズの理論における重要な考え方を再確認させるものでもあります。市場は完全にランダムではなく、識別可能な構造的スイングに従って動く傾向があります。これらの概念をEAに組み込むことで、市場行動を体系的に分析し、感情的バイアスを排除した執行が可能になります。

これにて本連載第2回は終了となります。次回の記事では、この基盤をもとに、より上位の市場構造およびMQL5を用いた非ランダム価格挙動のさらなる分析および検証方法について解説します。

本記事で使用したすべてのソースコードおよび関連ファイルは以下に提供されています。続く表では、各ファイルの内容と目的を説明します。

ファイル名 説明
larryWilliamsMarketStructureIndicator.mq5 ラリー・ウィリアムズの手法に基づき、短期および中期の市場構造スイングポイントを識別しチャート上に表示するカスタムインジケーター
larryWilliamsMarketStructureExpert.mq5 市場構造インジケーターのシグナルを読み取り、リスク管理およびステップトレーリングストップロジック付きで自動売買を実行するEA
setFile.set テストおよび実行時に使用した入力パラメータ式を含む設定ファイル

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

最後のコメント | ディスカッションに移動 (3)
William Tosolini
William Tosolini | 12 1月 2026 において 10:00
おはようございます。興味深いエキスパートアドバイザー ですが、MetaTrader 4用の完全なコードを入手することが可能かどうかお伺いしたいと思います。
また、私はMQLの使い方や書き方を知らないので、MetaTrader 4用のエキスパートアドバイザーの既成ファイルを送っていただければ、プラットフォームに貼り付けることができます。
もし可能であれば教えてください。
Chacha Ian Maroa
Chacha Ian Maroa | 12 1月 2026 において 14:28
William Tosolini エキスパート・アドバイザー ですが、MetaTrader 4用の完全なコードを入手することが可能かどうかお聞きしたいのですが。
また、私はMQLの使い方や書き方を知らないので、MetaTrader 4用のエキスパートアドバイザーのレディメイドのファイルを送っていただければ、プラットフォームに貼り付けることができます。
もし可能であれば教えてください。

親愛なるウィリアム、

ご丁寧なお言葉と私の仕事へのフォローありがとうございます。

ご要望についてですが、私はMetaTrader 5 プラットフォーム用のMQL5 開発を専門としています。MT4とMT5のアーキテクチャは大きく異なるため、コードを移植するには完全に書き直す必要があります。

残念ながら、現在のプロジェクトスケジュールの都合上、カスタムコーディングやコンバージョンのご要望にお応えすることはできません。ご理解いただき、MT5版の記事がお役に立つことを願っています。

William Tosolini
William Tosolini | 12 1月 2026 において 20:40
Chacha Ian Maroa #:

親愛なるウィリアム、

温かいお言葉、そして私の仕事についてきてくれてありがとう。

ご要望についてですが、私はMetaTrader 5 プラットフォーム向けのMQL5 開発のみを専門としています。MT4とMT5のアーキテクチャは大きく異なるため、コードの移植には完全な書き換えが必要です。

残念ながら、現在のプロジェクトスケジュールの都合上、カスタムコーディングやコンバージョンのご要望にお応えすることはできません。ご理解いただき、MT5版の記事がお役に立つことを願っています。

ご意見ありがとうございます。残念ながら、私はMetaTrader 5よりもMetaTrader 4の方が優れていると思うので、MetaTrader 4しか使っていません。
MT4用のエキスパートがあれば本当によかったのですが、とにかくありがとうございます。ご親切にお返事と理由を説明してくださいました。
データサイエンスとML(第47回):DeepARモデルによるPythonでの市場予測 データサイエンスとML(第47回):DeepARモデルによるPythonでの市場予測
DeepARと呼ばれる時系列予測のための優れたモデルを用いて、市場の予測を試みます。DeepARは、ARIMA(自己回帰和分移動平均)やVAR(ベクトル自己回帰)のようなモデルに見られる自己回帰的な性質とディープニューラルネットワークを組み合わせたモデルです。
MQL5でカスタムインジケーターを作成する(第2回):Canvasと針のメカニクスを使ったゲージ型RSIインジケーターの構築 MQL5でカスタムインジケーターを作成する(第2回):Canvasと針のメカニクスを使ったゲージ型RSIインジケーターの構築
本記事では、MQL5でゲージ型のRSIインジケーターを開発します。このインジケーターは、RSIの値を円形のスケール上の動く針で可視化し、買われすぎと売られすぎのレベルを色分けした範囲と、カスタマイズ可能な凡例を備えています。Canvasクラスを使用して、円弧、目盛り、扇形などの要素を描画し、新しいRSIデータに基づいて滑らかに更新されるようにします。
MQL5で他の言語の実用的なモジュールを実装する(第6回):MQL5におけるPython風ファイルI/O操作 MQL5で他の言語の実用的なモジュールを実装する(第6回):MQL5におけるPython風ファイルI/O操作
複雑なMQL5ファイル操作を簡素化するために、読み書きを容易にするPythonスタイルのインターフェースを構築する方法を紹介します。カスタム関数とクラスを用いて、Pythonの直感的なファイル処理パターンを再現する方法を解説します。その結果、MQL5のファイルI/Oにおいて、よりクリーンで信頼性の高いアプローチが実現しました。
共和分株式による統計的裁定取引(第9回):バックテストポートフォリオのウェイト更新 共和分株式による統計的裁定取引(第9回):バックテストポートフォリオのウェイト更新
本記事では、共和分関係にある銘柄を通じた統計的裁定取引を利用する平均回帰ベースの戦略において、ポートフォリオのウェイト更新をバックテストするためにCSVファイルを使用する方法について説明します。データベースへのローリングウィンドウ固有ベクトル比較(RWEC, Rolling Windows Eigenvector Comparison)の結果入力から、バックテストレポートの比較までを網羅します。その一方で、各RWECパラメータの役割と、それが全体的なバックテスト結果に与える影響を詳しく説明し、相対的なドローダウンの比較がこれらのパラメータをさらに改善するのにどのように役立つかを示します。