English Deutsch
preview
ラリー・ウィリアムズの『市場の秘密』(第6回):市場変動を利用したボラティリティブレイクアウトの測定

ラリー・ウィリアムズの『市場の秘密』(第6回):市場変動を利用したボラティリティブレイクアウトの測定

MetaTrader 5トレーディングシステム |
21 0
Chacha Ian Maroa
Chacha Ian Maroa

はじめに

ボラティリティはブレイクアウト取引の根幹をなす要素ですが、しばしば単一の尺度として捉えられがちです。前回の記事では、ラリー・ウィリアムズが直近の取引期間のレンジを用いてボラティリティを測定する手法を取り上げ、その考え方をMQL5で完全自動化されたエキスパートアドバイザー(EA)として実装する方法を示しました。このアプローチは有効である一方、市場の値動きの拡大を捉えるための一つの視点に過ぎません。

本記事では視点を変え、市場のスイングを通じてボラティリティを測定する方法に焦点を当てます。ラリー・ウィリアムズは、単一期間のレンジではなく価格スイングに着目することで、市場参加者のポジショニングをより的確に捉えられるとしています。直近数日間において、価格が主要なスイングポイント間をどれだけ移動したかを分析することで、ボラティリティ拡大の兆候を事前に把握することが可能になります。

本記事の目的は、最適化やカーブフィッティング、あるいは取引のフィルタリングではありません。提示されたコンセプトそのものを正しく理解し、自動化することに重点を置きます。また、実装は柔軟性を保ちつつ、特定の銘柄に依存しない汎用的な設計とします。スイングベースのボラティリティ計算を段階的に分解し、それを明確な取引ルールへと落とし込み、再利用可能で構造化されたMQL5のEAとして実装していきます。 


スイングベースのボラティリティ測定の理解

ラリー・ウィリアムズは、ATRや標準偏差といった指標に依存するのではなく、最近の価格スイングを測定することで短期的なボラティリティを推定する代替的な方法を提案しています。基本的な考え方はシンプルで、直近の方向性のある価格変動は、次の取引セッションにおいて市場がどれだけ動く可能性があるかを実用的な推定手段となります。

単一のスイングを測定するのではなく、直前に確定したバーの価格データを用いて2つの異なるスイング距離を算出します。これらのスイングは、新しいバーが開始した瞬間に評価され、取引判断がおこなわれる前に計算されます。

ウィリアムズは2つの明確なスイング測定を定義しています。1つ目のスイングは、3取引日前に記録された高値から直近の取引日の安値までの距離を測定します。

レンジ1

これは、直近の取引ウィンドウにおける価格の下落幅を捉えたものです。

2つ目のスイングは、1日前のバーに記録された高値(直近バーの直前のバー)から、3日前のバーに記録された安値までの距離を測定します。

レンジ2

これは、同じ3日構造内における反対方向の価格変動を捉えたものです。

その他の高値や安値は一切使用されません。特に、この2つ目の計算では直近で確定したバーの高値は使用されません。

両方のスイング値は方向性を持つ値ではなく、あくまでレンジとして扱われます。そのため、市場が上昇しているか下降しているかにかかわらず、結果が変動幅の大きさのみを反映するように絶対値が用いられます。

両方のスイング距離が計算された後、より大きい値のみが採用されます。ラリー・ウィリアムズはこの値を現在のボラティリティの指標として扱っており、それは方向性に依存せず、直近の取引構造において観測された最も大きな価格変動を表すためです。 

この選択されたスイング値は、新しい取引期間の開始時点におけるブレイクアウト取引のエントリーレベルを算出するために使用されます。

エントリー

売買の閾値は、始値からこのスイングレンジの一定割合を加算または減算することで算出されます。取引は、価格がこれらの設定されたレベルを突破した場合にのみ発生し、将来の価格変動を予測するのではなく、実際の変動を確認する形で実行されます。


コンセプトを取引ルールに落とし込む

選択した時間足で新しいバーが形成されるたびに、EAは、先に説明したラリー・ウィリアムズのスイング測定手法を用いてスイングレンジを計算します。このスイングレンジは、その取引期間におけるすべての意思決定の基盤となります。この値をもとに、EAは2つの重要な価格レベルを算出します。買いエントリーレベルは、現在価格にスイングレンジの一定割合を加算して計算されます。売りエントリーレベルは、同じくスイングレンジの一定割合を現在価格から減算して算出されます。これらのレベルは内部で保持され、新しいバーが形成されるまで更新されることはありません。

取引期間中はティックごとに価格を監視します。価格が買いエントリーレベルを上抜けた場合、EAは成行買い注文を実行します。逆に、売りエントリーレベルを下抜けた場合は、成行売り注文が実行されます。なお、指値・逆指値などは使用しません。すべての取引はブレイクアウト発生時に成行で執行されます。

リスク管理はスイングレンジと直接連動しています。各取引のストップロスは、エントリーに使用したスイングレンジに対してユーザーが指定した割合で設定されます。テイクプロフィットは、リスクリワード比に基づいて算出されます。エントリーレベルとストップロスの距離がリスクとなり、そのリスク幅に設定されたリスクリワード比を掛けることでテイクプロフィットレベルが決定されます。これにより、すべての取引が一貫したリスク構造のもとで管理されます。

同時に保有できる取引は1つのみです。ポジション保有中は、その取引が決済されるまで新規取引は発生しません。また、取引期間内にエントリーレベルへ到達しなかった場合、その期間では取引はおこなわれません。新しいバーが形成されると、それまでに計算されたすべてのレベルは破棄され、新しい取引期間用のレベルが再計算されます。
EAは取引方向の制御にも対応しています。ユーザーは買いのみ、売りのみ、または両方向の取引を許可するよう設定できます。この機能は裁量的なトレンド分析をおこなうトレーダーにとって有用であり、主な市場方向にのみ従って取引を行う運用が可能になります。

ポジションサイズは2つのロット計算モードで柔軟に管理されます。手動モードでは、ユーザーが指定した固定ロットサイズで取引がおこなわれます。自動モードでは、口座残高に対する一定割合に基づいてロットサイズが計算されます。自動モードでは、価格変動や口座残高の増減にかかわらず、各取引で一定のリスク割合が維持されるよう、ロットサイズが動的に調整されます。


EAの段階的構築

このセクションから、EAの実装フェーズに入ります。ここからは理論ではなく、実際の構築作業が中心となります。スムーズに進めるためには、読者がすでにMQL5の基本的な使用経験を持っていることを前提とします。具体的には、MetaTrader 5プラットフォームの操作、チャートへのエキスパートアドバイザーの適用、ストラテジーテスターでのバックテスト実行といった基本操作が含まれます。また、MetaEditorの使用に慣れており、コードの記述、コンパイル、エラー確認、必要に応じたデバッグがおこなえることも想定しています。プログラミングは読むことで身につくものではなく、実際に手を動かして初めて理解が深まります。このセクションは、受動的に読むのではなく、実際に手を動かしながら進めることを前提としています。

本記事で作成する完成版のソースファイルはlwVolatilitySwingBreakoutExpert.mq5として添付されています。途中でEAの構築に問題が発生した場合は、このファイルと比較することで、実装内容を正しく一致させることができます。作業を開始する前にダウンロードしておくことを強く推奨します。

MetaEditorを開き、新しいEAファイルを作成してください。ファイル名は任意で構いません。作成後、以下のボイラープレートコードを貼り付けてください。

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

#property copyright "Copyright 2026, 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>

//--- CUSTOM ENUMERATIONS
enum ENUM_TRADE_DIRECTION  
{ 
   ONLY_LONG, 
   ONLY_SHORT, 
   TRADE_BOTH 
};

enum ENUM_LOT_SIZE_INPUT_MODE 
{ 
   MODE_MANUAL, 
   MODE_AUTO 
};

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

input group "Volatility Breakout Parameters"
input double inpBuyRangeMultiplier   = 0.50;   
input double inpSellRangeMultiplier  = 0.50;   
input double inpStopRangeMultiplier  = 0.50;
input double inpRewardValue          = 4.0;

input group "Trade and Risk Management"
input ENUM_TRADE_DIRECTION direction        = ONLY_LONG;
input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode  = MODE_AUTO;
input double riskPerTradePercent            = 1.0;
input double positionSize                   = 0.1;

//+------------------------------------------------------------------+
//| 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(){

   //--- Retrieve current market prices for trade execution
   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

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

この初期コードは、今後構築していくベースとなる構造を定義しています。

ボイラープレート構造の理解

ボイラープレートコードは、明確に分割された複数のセクションで構成されており、それぞれが特定の役割を持っています。ヘッダ部分では、著作権情報やバージョン情報が定義されています。これらは取引ロジックには直接影響しませんが、ファイルの識別や作成者の明示に役立ちます。

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

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

標準ライブラリのインクルードにより、CTradeクラスが使用可能になります。このクラスは取引の実行および取引管理を簡素化するものであり、後の取引処理において注文の発注時に使用されます。

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

次に、カスタム列挙型を定義します。

//--- CUSTOM ENUMERATIONS
enum ENUM_TRADE_DIRECTION  
{ 
   ONLY_LONG, 
   ONLY_SHORT, 
   TRADE_BOTH 
};

enum ENUM_LOT_SIZE_INPUT_MODE 
{ 
   MODE_MANUAL, 
   MODE_AUTO 
};

これらの設定により、ユーザーは数値ではなく分かりやすいオプションを用いて取引方向やポジションサイズの挙動を制御できるようになります。これにより、EAの設定時における可読性と安全性が向上します。

入力変数のセクションでは、すべての設定可能なパラメータがユーザーに公開されています。

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

input group "Volatility Breakout Parameters"
input double inpBuyRangeMultiplier   = 0.50;   
input double inpSellRangeMultiplier  = 0.50;   
input double inpStopRangeMultiplier  = 0.50;
input double inpRewardValue          = 4.0;

input group "Trade and Risk Management"
input ENUM_TRADE_DIRECTION direction        = ONLY_LONG;
input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode  = MODE_AUTO;
input double riskPerTradePercent            = 1.0;
input double positionSize                   = 0.1;

これらの入力パラメータは、取引方向、ボラティリティ倍率、ストップロスの挙動、リワードの想定値、およびポジションサイズのロジックを制御します。これらの各設定は、ラリー・ウィリアムズのボラティリティ概念がどのように実行可能な取引ルールへと変換されるかに直接影響します。

続いて、グローバル変数の定義に移ります。

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

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

ここでは、取引の実行に使用するCTradeオブジェクトを作成し、現在のBidおよびAsk価格を格納する変数を定義します。これらの価格はティックごとに更新され、正確な取引計算に使用されます。

OnInit関数は、EAの起動時に一度だけ実行されます。

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

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

   return(INIT_SUCCEEDED);
}

この役割はシンプルかつ重要です。CTradeオブジェクトに一意のマジックナンバーを設定することで、このEAによって発注されたすべての取引を確実に識別できるようにします。将来的には、この関数を利用して、初期値が明確に定義されたグローバル変数の初期化もおこなう予定です。

OnDeinit関数は、EAが削除または停止された際に実行されます。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){

   //--- Notify why the program stopped running
   Print("Program terminated! Reason code: ", reason);

}

これは単純に終了理由を記録するものであり、取引ロジックには影響しません。

OnTick関数は、すべての市場ティックごとに呼び出されます。

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

   //--- Retrieve current market prices for trade execution
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID); 
}

現時点では、BidAsk価格の更新のみをおこないます。この関数は後に、取引戦略ロジックを実行するための中心的な制御ポイントへと拡張されます。

ユーティリティ関数のセクションは、初期状態では意図的に空のままにされています。

//--- UTILITY FUNCTIONS

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

ここには、メインの取引ロジックを補助するすべてのカスタムヘルパー関数を配置します。

新しいバーの検出

この戦略では、各取引期間ごとにレベルを再計算する必要があります。そのため、選択した時間足において新しいバーが開始されたタイミングを検出する必要があります。

この目的のために、ユーティリティセクションへ専用のカスタム関数を追加します。

//--- UTILITY FUNCTIONS
//+------------------------------------------------------------------+
//| 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;   
}

この関数は、現在のバーの開始時刻と、前回記録されたバーの時刻を比較します。両者が異なる場合、新しいバーが形成されたと判断されます。

この関数は3つのパラメータを受け取ります。銘柄時間足は、どのチャートを監視するかを指定します。3つ目のパラメータは参照渡しされ、最後に処理されたバーの開始時刻を保持します。新しいバーが検出された場合、この値は自動的に更新されます。

このロジックを支えるために、datetime型のグローバル変数を宣言します。

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

この変数は、直近で処理されたバーの開始時刻を追跡します。OnInit関数内では、この変数を0に初期化し、クリーンな初期状態を確保します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   
   ...
   
   //--- Initialize global variables
   lastBarOpenTime = 0;

   return(INIT_SUCCEEDED);
}

日次ボラティリティレベルの保存

各取引期間では、次のバーが開始されるまで有効となる固定の価格レベルセットを保持する必要があります。これには、スイングレンジ、エントリーレベル、ストップロスレベル、および両方向の取引に対応するテイクプロフィットレベルが含まれます。

これらの値を整理して管理するために、グローバルスコープにカスタム構造体を定義します。

//--- Holds all price levels derived from Larry Williams' volatility breakout calculations
struct MqlLwVolatilityLevels
{
   double dominantSwingRange;      
   double buyEntryPrice;       
   double sellEntryPrice;   
   double bullishStopLoss;   
   double bearishStopLoss;    
   double bullishTakeProfit;
   double bearishTakeProfit;
};

MqlLwVolatilityLevels lwVolatilityLevels;

この構造体は、関連するすべての価格レベルを一つの論理的な単位としてまとめる役割を持ちます。この定義の直後に、この構造体のインスタンスが作成されます。

OnInit関数内では、この構造体インスタンスがZeroMemory関数を用いてリセットされ、すべての値が初期状態にクリアされます。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   
   ...
   
   //--- Reset Larry Williams' volatility levels 
   ZeroMemory(lwVolatilityLevels);

   return(INIT_SUCCEEDED);
}

これにより、すべてのフィールドが既知の初期値から開始されることが保証され、未初期化データによって発生する予期しない挙動を防ぐことができます。

スイングベースのボラティリティレンジ計算

最初のカスタム計算関数では、ラリー・ウィリアムズが提唱するスイングベースのボラティリティレンジを算出します。

//+------------------------------------------------------------------+
//| Calculates Larry Williams swing-based volatility range           |
//+------------------------------------------------------------------+
double CalculateLwSwingVolatilityRange(const string symbol, ENUM_TIMEFRAMES tf){
   
   //--- Retrieve required highs and lows
   double high_3_days_ago = iHigh(symbol, tf, 4);
   double low_yesterday   = iLow (symbol, tf, 1);

   double high_1_day_ago  = iHigh(symbol, tf, 2);
   double low_3_days_ago  = iLow (symbol, tf, 4);

   //--- Validate data
   if(high_3_days_ago == 0.0 || low_yesterday == 0.0 ||
      high_1_day_ago  == 0.0 || low_3_days_ago == 0.0)
   {
      return 0.0;
   }

   //--- Calculate swing distances using absolute values
   double swingRangeA = MathAbs(high_3_days_ago - low_yesterday);
   double swingRangeB = MathAbs(high_1_day_ago  - low_3_days_ago);

   //--- Select the dominant swing
   double usableRange = MathMax(swingRangeA, swingRangeB);

   //--- Normalize for symbol precision
   return NormalizeDouble(usableRange, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
}

この関数では、選択された時間足における過去のバーから特定の高値と安値を取得します。2つのスイング距離を絶対値で計算することで、価格の上下関係に依存せず正確な値を得られるようにしています。そのうえで、2つのスイング距離のうち大きい方を優勢なスイングレンジとして採用します。

この値は、直近における市場の最も大きな価格拡張を表しており、その後のすべての計算におけるボラティリティの代理指標として機能します。結果は、銘柄の価格精度に合わせて正規化されたうえで返されます。

エントリーレベルの計算

買いエントリーレベルは、本日の始値に対してスイングレンジのユーザー指定割合を加算することで算出されます。これにより、市場の上方ブレイクアウトレベルが投影されます。

//+--------------------------------------------------------------------------------+
//| Calculates the bullish breakout entry price using today's open and swing range |
//+--------------------------------------------------------------------------------+
double CalculateBuyEntryPrice(double todayOpen, double swingRange, double buyMultiplier){

   return todayOpen + (swingRange * buyMultiplier);
}

売りエントリーレベルは、本日の始値からスイングレンジのユーザー指定割合を減算することで算出されます。これにより、市場の下方ブレイクアウトレベルが投影されます。

//+--------------------------------------------------------------------------------+
//| Calculates the bearish breakout entry price using today's open and swing range |
//+--------------------------------------------------------------------------------+
double CalculateSellEntryPrice(double todayOpen, double swingRange, double sellMultiplier){

   return todayOpen - (swingRange * sellMultiplier);
}

両関数とも意図的にシンプルな設計となっています。ボラティリティを、不要な複雑さを加えることなく、実際の取引に活用可能な価格レベルへと変換する役割を担っています。

ストップロスレベルの計算

ストップロスレベルは、エントリーレベルから直接算出されます。

//+--------------------------------------------------------------------------------------------+
//| Calculates the stop-loss price for a bullish position based on entry price and swing range |
//+--------------------------------------------------------------------------------------------+
double CalculateBullishStopLoss(double entryPrice, double swingRange, double stopMultiplier){

   return entryPrice - (swingRange * stopMultiplier);
}

 
//+--------------------------------------------------------------------------------------------+
//| Calculates the stop-loss price for a bearish position based on entry price and swing range |
//+--------------------------------------------------------------------------------------------+
double CalculateBearishStopLoss(double entryPrice, double swingRange, double stopMultiplier){

   return entryPrice + (swingRange * stopMultiplier);
}

買いでは、ストップロスは買いエントリーレベルの下側に、スイングレンジのユーザー指定割合分だけ離して設定されます。売りでは、同様のロジックに基づき、ストップロスは売りエントリーレベルの上側に配置されます。これにより、リスクは直近のボラティリティに比例した形となり、すべての取引において一貫性が保たれます。

テイクプロフィットレベルの計算

テイクプロフィットレベルは、リスクリワード比に基づいて算出されます。

//+--------------------------------------------------------------------------+
//| Calculates take-profit level for a bullish trade using risk-reward logic |                               
//+--------------------------------------------------------------------------+
double CalculateBullishTakeProfit(double entryPrice, double stopLossPrice, double rewardValue){

   double stopDistance   = entryPrice - stopLossPrice;
   double rewardDistance = stopDistance * rewardValue;
   return NormalizeDouble(entryPrice + rewardDistance, Digits());
}

//+--------------------------------------------------------------------------+
//| Calculates take-profit level for a bearish trade using risk-reward logic |                               
//+--------------------------------------------------------------------------+
double CalculateBearishTakeProfit(double entryPrice, double stopLossPrice, double rewardValue){

   double stopDistance   = stopLossPrice - entryPrice;
   double rewardDistance = stopDistance * rewardValue;
   return NormalizeDouble(entryPrice - rewardDistance, Digits());
}

買いの場合、エントリーレベルとストップロスの間の距離がリスクを定義します。この距離にリスクリワード比を乗じることで、エントリーレベルの上側にテイクプロフィットが算出されます。

売りの場合は、同じロジックを反対方向に適用します。算出されたテイクプロフィットレベルは、シンボルの価格精度に合わせて正規化されたうえで返されます。

OnTick関数への統合

これらの補助関数がすべて揃ったことで、OnTick関数が全体の処理を統合する役割を担います。

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

   ...  
   
   //--- Run this block only when a new bar is detected on the selected timeframe
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
      lwVolatilityLevels.dominantSwingRange = CalculateLwSwingVolatilityRange(_Symbol, timeframe);
      lwVolatilityLevels.buyEntryPrice      = CalculateBuyEntryPrice (askPrice, lwVolatilityLevels.dominantSwingRange, inpBuyRangeMultiplier );
      lwVolatilityLevels.sellEntryPrice     = CalculateSellEntryPrice(bidPrice, lwVolatilityLevels.dominantSwingRange, inpSellRangeMultiplier);
      lwVolatilityLevels.bullishStopLoss    = CalculateBullishStopLoss(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.dominantSwingRange,  inpStopRangeMultiplier);
      lwVolatilityLevels.bearishStopLoss    = CalculateBearishStopLoss(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.dominantSwingRange, inpStopRangeMultiplier);
      lwVolatilityLevels.bullishTakeProfit  = CalculateBullishTakeProfit(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.bullishStopLoss,  inpRewardValue);
      lwVolatilityLevels.bearishTakeProfit  = CalculateBearishTakeProfit(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.bearishStopLoss, inpRewardValue);
   }
}

ティックが更新されるたびに、このEAはまずBidAskの価格を更新します。その後、選択した時間足で新しいバーが確定したかどうかを確認します。新しいバーが確認できない場合、それ以上の処理はおこないません。

新しいバーが確定した場合のみ、ボラティリティに基づく各種レベルを再計算します。まずスイングレンジを算出し、その値を基に、エントリーレベル、ストップロス、テイクプロフィットレベルを順に導出していきます。

これらの値はグローバル構造体に保存され、次に新しいバーが確定するまで変更されません。この仕組みにより、各取引期間中の判断は、毎回変動する値ではなく、事前に算出された固定レベルに基づいておこなわれます。

この時点で、このEAは取引実行に必要なすべての価格レベルを計算し保持するためのフレームワークを備えています。次のセクションでは、これらのレベルを用いて、事前に定義したルールに従って取引の発生とポジション管理をおこないます。

取引ロジックの完成と注文実行

ここまでで、取引判断に必要な要素はすべて揃っています。日次のボラティリティレベルは計算され、メモリに保存されており、新しいバーが確定するまで有効です。ここからは、どのタイミングで取引を発生させるか、重複ポジションをどのように防ぐか、そして注文をどのように一貫した方法で実行するかを定義します。

基本的な考え方はシンプルです。価格が事前に定義したエントリーレベルのいずれかをブレイクした時点で、その方向にポジションを取ります。買いレベルを先に上抜けた場合は買い、売りレベルを先に下抜けた場合は売りでエントリーします。ただし、常に1つのルールを厳格に守ります。同時に保有できるポジションは常に1つのみです。

このロジックを分かりやすく実装するため、処理は小さなユーティリティ関数に分割します。

価格クロスの検出

最初に解決すべきなのは、「価格が特定のレベルをクロスしたかどうか」の判定です。単に価格がそのレベルに触れただけでは不十分で、レベルの片側から反対側へ明確に抜けたことを確認する必要があります。そのために、2つのユーティリティ関数を定義します。まず、IsCrossOver関数は、価格が下から上へレベルを上抜けた場合を検出します。

//+------------------------------------------------------------------+
//| 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;
}

この関数では、連続する2本の1分足の終値を対象レベルと比較します。インデックス1の値は直前に確定した1分足、インデックス0の値は直近のバーを表します。前回の終値がレベル以下にあり、現在の終値がレベルを上回った場合に「上抜け(クロスオーバー)」と判定されます。このシンプルな比較により、価格がレベルを明確に上抜けたことを信頼性高く検出できます。

IsCrossUnder関数はこれとは逆の判定をおこない、価格の下抜けを検出します。

//+------------------------------------------------------------------+
//| 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分足データの保持

これらのクロス判定関数はいずれも1分足の終値データを使用します。そのため、このデータを保持するためのグローバル配列を定義します。

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

この配列は時系列データとして扱う必要があります。MQL5では、配列はデフォルトでは時系列として動作しません。そのままではインデックス0が最新のバーを指さないため、クロス判定ロジックが正しく機能しなくなります。このため、初期化処理の中でインデックスの並びを明示的に設定します。

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

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

この指示によりインデックスの順序が反転され、インデックス0が常に最新のバーを指すようになります。この設定をおこなわない場合、クロス判定が誤った価格データを参照してしまい、EAが予測不能な挙動を示す原因となります。

1分足データの毎ティック更新

OnTick関数内では、新しい価格データが到着するたびに1分足データ配列を更新します。

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

   ...
   
   //--- Get some minutes data
   if(CopyClose(_Symbol, PERIOD_M1, 0, 7, closePriceMinutesData) == -1){
      Print("Error while copying minutes datas ", GetLastError());
      return;
   }
}

直近の1分足の終値を7本分コピーします。クロス判定自体には直近2本のデータだけで十分ですが、少し余分に取得しておくことで安全性を確保でき、パフォーマンスへの影響もありません。

データ取得が失敗した場合、そのティックでの処理は中断します。不完全または欠損した価格データに基づいて判断を行うと、取引の信頼性が損なわれるためです。

複数ポジションの防止

価格は特にボラティリティが高い局面では、同じレベルを複数回クロスすることがあります。適切な制御がない場合、短時間に複数の取引が発生してしまう可能性があります。

これを防ぐために、この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;
}

この関数は、ターミナル内のすべての保有ポジションを走査します。各ポジションに対して2つの条件を確認します。まず、マジックナンバーがこのEAのマジックナンバーと一致している必要があります。これにより、このEAによって開かれた取引のみを対象として判定できます。次に、ポジションタイプが買いであることを確認します。この2つの条件を満たすポジションが見つかった場合、その時点で関数はtrueを返します。該当するポジションが存在しない場合は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;
}

この関数は前述のものと同じ構造を持ちますが、ショートポジションのみを明示的に確認する点が異なります。これら2つのチェックを組み合わせることで、同時に複数のポジションを保有することを確実に防止しています。

リスクに基づく自動ポジションサイズ計算

自動ロットサイズに対応するため、口座残高の一定割合に基づいてポジションサイズを算出する関数を定義します。

//+----------------------------------------------------------------------------------+
//| Calculates position size based on a fixed percentage risk of the account balance |
//+----------------------------------------------------------------------------------+
double CalculatePositionSizeByRisk(double stopDistance){
   double amountAtRisk = (riskPerTradePercent / 100.0) * AccountInfoDouble(ACCOUNT_BALANCE);
   double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double volume       = amountAtRisk / (contractSize * stopDistance);
   return NormalizeDouble(volume, 2);
}

ロジックはシンプルです。まず、1回の取引で許容するリスク金額を計算します。これは口座残高と設定されたリスク割合に基づいて算出されます。次に、現在の銘柄における契約サイズを取得します。これにより、1ロットあたりの価値が分かります。最後に、リスク金額をストップロスまでの距離で割ることで、リスクルールに整合したポジションサイズが決定されます。算出された値はブローカーの要件に従い、小数点以下2桁に正規化されます。

買い注文の執行

ここから、成行買い注文を実行する関数を定義します。

//+------------------------------------------------------------------+
//| Function to open a market buy position                           |
//+------------------------------------------------------------------+
bool OpenBuy(double entryPrice, double stopLoss, double takeProfit, double lotSize){
   
   if(lotSizeMode == MODE_AUTO){
      lotSize = CalculatePositionSizeByRisk(lwVolatilityLevels.buyEntryPrice - lwVolatilityLevels.bullishStopLoss);
   }
   
   if(!Trade.Buy(lotSize, _Symbol, entryPrice, stopLoss, takeProfit)){
      Print("Error while executing a market buy order: ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }
   return true;
}

自動ロットサイズが有効な場合、この関数はエントリーレベルとストップロスの距離に基づいてポジションサイズを算出します。その後、CTradeクラスを使用して注文を実行します。注文が失敗した場合は、詳細なエラー情報をログに記録します。注文が成功した場合、この関数はtrueを返します。

売り注文の関数も同じ構造で実装されています。

//+------------------------------------------------------------------+
//| Function to open a market sell position                          |
//+------------------------------------------------------------------+
bool OpenSel(double entryPrice, double stopLoss, double takeProfit, double lotSize){
   
   if(lotSizeMode == MODE_AUTO){
      lotSize = CalculatePositionSizeByRisk(lwVolatilityLevels.bearishStopLoss - lwVolatilityLevels.sellEntryPrice);
   }
   
   if(!Trade.Sell(lotSize, _Symbol, entryPrice, stopLoss, takeProfit)){
      Print("Error while executing a market sell order: ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }
   return true;
}

違いは、ストップロスまでの距離の方向と、売り注文を使用する点のみです。構造自体は一貫しており、これによりコードの保守性が高まります。

OnTick関数への統合

すべての構成要素が揃ったことで、OnTick関数内で全体の処理を統合します。

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

   ...
   
   //--- Long position logic
   if(direction == TRADE_BOTH || direction == ONLY_LONG){
      if(IsCrossOver(lwVolatilityLevels.buyEntryPrice, closePriceMinutesData)){
         if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
            OpenBuy(askPrice, lwVolatilityLevels.bullishStopLoss, lwVolatilityLevels.bullishTakeProfit, positionSize);
         }
      }
   }
   
   //--- Short position logic
   if(direction == TRADE_BOTH || direction == ONLY_SHORT){
      if(IsCrossUnder(lwVolatilityLevels.sellEntryPrice, closePriceMinutesData)){
         if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
            OpenSel(bidPrice, lwVolatilityLevels.bearishStopLoss, lwVolatilityLevels.bearishTakeProfit, positionSize);
         }
      }
   }
}

このブロックでは、買いが許可されているかを確認します。許可されている場合、買いエントリーレベルを上抜けるクロスオーバーを検出します。クロスオーバーが発生し、かつ既存のポジションが存在しない場合、事前に計算された各レベルを使用して買い注文を出します。売りのロジックも同様の構造で処理されます。このように構成することで、ロジックの明確性と制御性を確保し、戦略ルールへの厳密な準拠を実現しています。

チャート表示の設定

テスト前に、チャートの可読性を向上させるため表示設定を調整します。以下のカスタムユーティリティ関数を定義します。

//+------------------------------------------------------------------+
//| This function configures the chart's appearance.                 |
//+------------------------------------------------------------------+
bool ConfigureChartAppearance()
{
   if(!ChartSetInteger(0, CHART_COLOR_BACKGROUND, clrWhite)){
      Print("Error while setting chart background, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_SHOW_GRID, false)){
      Print("Error while setting chart grid, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_MODE, CHART_CANDLES)){
      Print("Error while setting chart mode, ", GetLastError());
      return false;
   }

   if(!ChartSetInteger(0, CHART_COLOR_FOREGROUND, clrBlack)){
      Print("Error while setting chart foreground, ", GetLastError());
      return false;
   }

   if(!ChartSetInteger(0, CHART_COLOR_CANDLE_BULL, clrSeaGreen)){
      Print("Error while setting bullish candles color, ", GetLastError());
      return false;
   }
      
   if(!ChartSetInteger(0, CHART_COLOR_CANDLE_BEAR, clrBlack)){
      Print("Error while setting bearish candles color, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_COLOR_CHART_UP, clrSeaGreen)){
      Print("Error while setting bearish candles color, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_COLOR_CHART_DOWN, clrBlack)){
      Print("Error while setting bearish candles color, ", GetLastError());
      return false;
   }
   
   return true;
}

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

この関数では、チャートの背景を白に設定し、グリッドを非表示にし、ローソク足表示を強制し、さらに強気と弱気ローソク足に明確な色を適用します。いずれかの設定に失敗した場合はエラーを記録し、処理を停止します。この関数は初期化処理の中で呼び出されます。

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

   ...
   
   //--- To configure the chart's appearance
   if(!ConfigureChartAppearance()){
      Print("Error while configuring chart appearance", GetLastError());
      return INIT_FAILED;
   } 
}

この時点で、取引が開始される前のチャート準備はすべて完了しています。

以上でEAの実装は完了です。ソースコードをコンパイルし、正しく実装されていればエラーなくビルドされるはずです。エラーが発生した場合は、添付のソースファイルlwVolatilitySwingBreakoutExpert.mq5を参照してください。


EAのテスト

テストでは、取引ロジックが想定通りに動作するかを検証し、戦略が過去の市場環境でどのように機能するかを評価します。このセクションでは、MetaTrader 5のストラテジーテスターを用いたバックテストに焦点を当てます。

バックテスト環境と設定

バックテストは、ゴールド(XAUUSD)の日足チャートで実施しました。期間は2025年1月1日から2025年12月31日までの1年間です。これにより、異なる市場環境を含む十分なサンプルを確保しています。

EAはONLY_LONGモードで設定されており、ロングポジションのみが許可されています。売りは完全に無効化されています。各取引のリスクは口座残高の1%に固定されており、前述の自動ポジションサイズ計算ロジックが使用されています。

結果の再現性を確保するため、本記事には2つの重要なファイルが添付されています。1つ目はconfigurations.iniで、銘柄、時間足、期間などストラテジーテスター環境設定が含まれています。2つ目はparameters.setで、リスク設定やボラティリティ倍率など、テストに使用したすべての入力パラメータが記録されています。これらをストラテジーテスターに読み込むことで、同一条件での再現テストが可能です。

バックテスト結果の概要

バックテスト開始時の口座残高は10,000ドルでした。テスト期間終了時点で、純利益は4,450.23ドルとなり、約44%を超える年間リターンとなりました。

テスターレポート

このシステムは勝率48.39%を記録しました。一見すると控えめに見えるかもしれませんが、この結果は戦略のリスクリワード構造と整合しています。収益性は高い勝率によってではなく、リスク管理の徹底とリワード設計の優位性によって実現されています。

パフォーマンス画面に示されている資産曲線は、滑らかで安定した推移を示しています。

エクイティカーブ

急激なドローダウンや極端な資産崩壊は見られません。この挙動は、ドローダウンが抑制されており、取引ルールが一貫して守られていることを示しています。

ここで強調しておきたいのは、このテスト結果はあくまで1つの設定、そして1つの市場条件に基づくものだという点です。この戦略は意図的に柔軟性を持たせて設計されています。リスク割合、ボラティリティ係数、取引方向といった入力パラメータはすべて調整可能であり、トレーダーは自身の考えに合わせて挙動を変更できます。

本記事の主目的は、完成された最適化済みの取引システムを提示することではありません。ラリー・ウィリアムズの手法を、実際に動作するアルゴリズムへと変換するプロセスを示し、それを読者が学習、改変、拡張できる形で提示することにあります。読者は自身の環境でテストをおこない、異なる銘柄、時間軸、パラメータの組み合わせを試すことで、より適したバリエーションを見つけることができます。こうした試行錯誤を通じて、個々の取引スタイルやリスク許容度に適合した形へと発展させることが可能になります。

ぜひ読者の皆さんにも、テスト結果や気づき、改善アイデアをコメント欄で共有していただければと思います。こうした集合的な検証と議論の中から、単一のバックテストでは見えない発見が生まれることがあります。


結論

本記事では、ラリー・ウィリアムズのスイングベースのボラティリティブレイクアウト手法を、MetaTrader 5で動作する完全なEAへと実装することに成功しました。書籍における原初の解説から出発し、スイング測定の意味を丁寧に解釈し、その実務的な意義を明確化したうえで、自動化に適したルールベースの計算へと落とし込みました。

本記事では、段階的なプロセスを通じて、完全な取引システムの設計と実装をおこないました。新しい取引期間の検出、支配的なスイングレンジの算出、ブレイクアウトのエントリーレベルの導出、ストップロスおよびテイクプロフィットの設定、そして厳格な取引管理ルールの適用までを一貫して構築しています。各コンポーネントは明確な目的に基づいて設計されており、その結果、構造が整理され、可読性が高く、拡張性にも優れたEAとなっています。

戦略ロジックに加え、本記事ではMQL5における実装面での基本的な設計原則についても示しました。構造体を用いた状態管理、グローバル変数の安全な初期化、信頼性の高いクロス判定、マジックナンバーによるポジションフィルタリング、さらに手動およびリスクベースのポジションサイジングへの対応などを扱っています。これらはいずれも、本格的なアルゴリズム取引システムを構築するうえで不可欠な要素です。

バックテストの結果からは、適切なリスク管理と規律ある運用のもとで、本戦略が安定的に機能し得ることが示されました。さらに重要なのは、本システムが意図的に柔軟性を持たせて設計されている点です。主要なパラメータを外部から調整可能とすることで、トレーダーはさまざまな市場環境に応じてロジックを適応させ、自身のエッジを検証させ、発展させることができます。

本記事は、完成された最適化済みの戦略を提示するものではありません。むしろ、裁量的なトレードアイデアを、構造化され、検証可能で、再現性のあるアルゴリズムへと変換するプロセスを示すことを目的としています。ここまで読み進めた読者は、実際に動作するボラティリティブレイクアウトEAを手にするとともに、ラリー・ウィリアムズの手法に対する理解を深め、今後の研究や改良に活用できる堅牢な基盤を得ることができるでしょう。

以下の表では、本記事に付属する補助ファイルと、それぞれの用途を簡潔にまとめています。これらのファイルは、本記事で示した結果の再現および実装内容の正確な追跡を支援するために提供されています。



ファイル名 説明
1 lwVolatilitySwingBreakoutExpert.mq5 本記事で解説および構築したEAの完全なソースコード
2 configurations.ini バックテストに使用したストラテジーテスターの環境設定
3 parameters.set バックテスト時に適用した入力パラメータ式を記録した設定ファイル

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

添付されたファイル |
configurations.ini (1.62 KB)
parameters.set (1.5 KB)
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
プライスアクション分析ツールキットの開発(第54回):EMAと平滑化された価格変動によるトレンドのフィルタリング プライスアクション分析ツールキットの開発(第54回):EMAと平滑化された価格変動によるトレンドのフィルタリング
取引の明確さとタイミングを向上させるために、平均足による平滑化とEMA20の高値および安値のバンド、さらにEMA50のトレンドフィルターを組み合わせた手法を解説します。これらのツールにより、トレーダーは真のモメンタムを見極め、ノイズを排除し、ボラティリティの高い局面やトレンド相場により適切に対応できます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
トレンド強度の最適化:方向と強さに沿った取引戦略 トレンド強度の最適化:方向と強さに沿った取引戦略
短期および長期の分析を組み合わせ、全体的なトレンドとその強さに基づいて取引判断および執行をおこなう、トレンドフォロー型のエキスパートアドバイザー(EA)です。本記事では、忍耐力と規律を備え、集中力を維持しながら、トレンドの強さと方向に一致する場合にのみ取引を実行し、特にトレンドに逆らう取引や頻繁なバイアス変更を避け、テイクプロフィットに到達するまでポジションを保持できるトレーダー向けに設計されたEAについて詳しく解説します。