ラリー・ウィリアムズの『市場の秘密』(第5回):MQL5におけるボラティリティブレイクアウト戦略の自動化
はじめに
市場がトレンドを形成するのは偶然ではありません。ラリー・ウィリアムズによれば、持続的な価格の動きは、多くの場合、それまでの通常の状態を超えるような極端なボラティリティの瞬間から生まれます。このボラティリティブレイクアウトは、まるで市場に火をつけるスパークのような役割を果たし、その後の反対の力が現れるまで、同じ方向への動きを継続させる傾向があります。この考え方は、短期的な細かい値動きを追いかける一般的なアプローチとは異なり、価格が本当に意味のある動きを見せる瞬間に注目するものです。
著書『Long-Term Secrets to Short-Term Trading』の中で、ラリー・ウィリアムズは、ボラティリティが急激に拡大したとき、価格はまるで運動を始めた物体のように振る舞い、その方向に動き続ける傾向があると説明しています。しかし多くのトレーダーにとって難しいのは、この概念を理解することではなく、それを感情に左右されず、一貫して実行することです。ボラティリティを正しく測定し、適切なタイミングで反応し、規律を持ってリスクを管理することは、日々手作業でおこなうには容易ではありません。
そこで本記事では、ラリー・ウィリアムズのボラティリティブレイクアウトの概念をMQL5で自動化する実用的な方法を解説します。目的はシンプルです。強力な取引アイデアを、テスト可能で検証可能な客観的手順へと変換することです。これにより、裁量的な判断や感情的な介入を減らし、ストラテジーの優位性を時間の経過とともに一貫して発揮できるようにします。
ボラティリティブレイクアウトの概念の理解
短期取引の世界において、ラリー・ウィリアムズはボラティリティブレイクアウトの重要性を強調しています。この概念の中心には、ある市場が1日の中で大きなレンジを形成した場合、それが新しいトレンドの始まりを示す可能性が高いという考え方があります。このセクションでは、そのボラティリティの測定方法と、それが取引戦略にどのように影響するかを説明します。
前日の値幅の測定
まず、前日の値幅を確認します。これは単純に、前の取引日の高値と安値の差です。

この値幅は、潜在的なブレイクアウトを特定するためのベンチマークとして機能します。
前日の値幅が重要な理由
前日の値幅は基準値として機能します。今日の価格がその値幅の一定割合を超えて動いた場合、市場はその方向に継続的に動く可能性が高いと考えられます。
今日の始値からブレイクアウトレベルを導き出す方法
前日の値幅が分かったら、その一定割合を当日の始値に加算または減算することで、買いと売りのエントリーレベルを決定します。ここで重要なのは、市場を予測することではなく、ボラティリティの拡大に反応するという姿勢です。
このアプローチでは、天井や底を当てようとすることではなく、大きな価格拡張を捉えることを重視します。これにより、トレーダーは市場のモメンタムに沿った形で取引し、より有利な値動きを捉えることができます。
次のセクションでは、エントリーとエグジットのモデル、さらにストップロスやテイクプロフィットの設計について詳しく解説し、この戦略を実際にどのように適用するかを包括的に説明していきます。
取引ルール
この段階では、理論から実行可能なルールへと移行します。これまでに説明したすべての内容は、ここで少数の明確で再現性のある取引ルールへと変換されます。これらのルールは予測に基づくものではなく、ラリー・ウィリアムズが意図したとおり、ボラティリティ拡大への反応に基づいています。最初のステップは常に同じです。新しい取引日の開始時点で、まず前日の値幅を測定します。この値幅は直近の市場活動とボラティリティを反映しており、前のセッションでどの程度の値動きがあったかを示しています。
値幅が確定したら、当日の始値を基準点とします。この始値から2つのレベルを設定します。1つは買いの可能性を示す上側のレベル、もう1つは売りの可能性を示す下側のレベルです。それぞれのレベルは、前日のレンジに対してユーザーが設定した割合を適用して計算されます。これらの投影レベルはボラティリティの「境界」として機能し、価格がそこまで拡張しない限り取引は許可されません。 
価格がこれらのレベルのいずれかに到達した場合のみ、エントリーが発生します。事前の予測や早すぎるエントリーは一切おこないません。その日中に価格がレベルに到達しなければ、その取引機会は破棄され、取引はおこなわれません。次の取引日は新しい計算から再スタートします。
エントリーが発生した瞬間に、リスクは即座に定義されます。ストップロスはエントリー価格から一定の距離に配置され、その距離は前日のレンジに対する割合で計算されます。これにより、リスクは恣意的なポイント値ではなく、直近の市場環境に連動する形になります。ボラティリティが高い市場ではストップは自然に広くなり、静かな市場では狭くなります。
次にテイクプロフィットはリスクリワードモデルに基づいて決定されます。建値とストップロスの距離がリスクとなり、ユーザーが設定したリワード倍率によって利益目標がその何倍になるかが決まります。これにより、すべての取引に対して計画的で一貫したペイオフ構造が確保されます。
同時に保有できるポジションは1つだけです。この設計は、ボラティリティブレイクアウトが方向性を持つイベントであるという前提に立っています。市場が一方向に動き出すなら、システムも同じ方向に従います。ポジションの積み増しやヘッジはおこないません。
このルールの本質は非常にシンプルです。ボラティリティを測定し、その拡大を待ち、そのボラティリティを基準にリスクを定義し、価格の動きそのものに取引の可否を判断させることです。このシンプルさこそが、自動化に適しており、さまざまな市場環境でも安定して機能する理由です。
ボラティリティブレイクアウト戦略のコーディング
実際のロジックを書き始める前に、開発をスムーズに進めるための準備が整っていることを確認しておくことが重要です。このセクションは理論ではなく実装に焦点を当てているため、いくつかの前提知識が必要になります。
このパートを無理なく進めるためには、MQL5の基本的な知識、たとえば変数、制御構文、データ構造、一般的なプログラミング概念などを理解している必要があります。また、MetaTrader 5の操作にも慣れており、エキスパートアドバイザー(EA)をチャートに適用する方法やストラテジーテスターの使い方も知っていることが前提です。さらに、MetaEditorの基本操作、つまりソースファイルの作成、コードの記述、コンパイル、エラーの確認、基本的なデバッグができることも必要です。
これらの内容に不安がある場合は、公式のMQL5リファレンスを参照することを強く推奨します。これは言語仕様、取引環境、APIの詳細まで網羅している、非常に詳しくまとまったドキュメントです。
本記事では、EAを段階的に開発していきます。理解と再現性を高めるために、最終的に完成するコードはlwVolatilityBreakoutExpert.mq5という単一のMQL5ファイルとして提供されます。実際にコードを書きながら進めることで、ロジックの理解が深まり、後からの修正や拡張もしやすくなります。
ここからは実際に手を動かしていきます。MetaEditor 5を開き、新しいEAを作成し、空のテンプレートを選択します。その後、任意の名前を付けて作成し、提供されたソースコードを貼り付けます。
//+------------------------------------------------------------------+ //| lwVolatilityBreakoutExpert.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 = TRADE_BOTH; 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 //+------------------------------------------------------------------+
このファイルは、戦略全体を構築するための土台となる定型文として機能します。
定型文の構造を理解する
このソースファイルはすでに論理的なセクションごとに整理されており、それぞれがEAのライフサイクルの中で特定の役割を担っています。
まずヘッダ部分には、ファイル名、作成者情報、バージョン番号、リンクなどのメタデータが含まれています。
//+------------------------------------------------------------------+ //| lwVolatilityBreakoutExpert.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"
これは取引の動作そのものには影響しませんが、特にコードを共有したり公開したりする場合には良いプラクティスです。
標準ライブラリのセクションには、Trade.mqhライブラリが含まれています。
//+------------------------------------------------------------------+ //| Standard Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh>
これにより、CTradeクラスを使用できるようになり、注文の実行や取引管理を大幅に簡略化できます。標準ライブラリのクラスを使用することは推奨されており、MetaQuotesによって最適化および保守されているため信頼性も高くなっています。
カスタム列挙型のセクションでは、入力として選択可能な値を制御するための定義がおこなわれています。
//--- CUSTOM ENUMERATIONS enum ENUM_TRADE_DIRECTION { ONLY_LONG, ONLY_SHORT, TRADE_BOTH }; enum ENUM_LOT_SIZE_INPUT_MODE { MODE_MANUAL, MODE_AUTO };
たとえば、取引方向はロングのみ、ショートのみ、または両方といった形に制限されています。同様に、ロットサイズのモードも、手動でのポジションサイズ指定と、リスクに基づいた自動サイズ計算の間で切り替えられるようになっています。列挙型を使うことで、ユーザー入力をより安全かつ正確に管理できます。
ユーザー入力パラメータとその役割
入力変数は、EAの設定パネル内で分かりやすく整理されています。
まず「Information」グループには、マジックナンバーと時間足が含まれています。
//+------------------------------------------------------------------+ //| User input variables | //+------------------------------------------------------------------+ input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT;
マジックナンバーは、このEAによって開かれた取引を一意に識別するためのものであり、プログラムでポジションを管理するうえで非常に重要です。時間足の入力は、ボラティリティを測定する際にどのチャートデータを使用するかを決定します。これは、前日のレンジが戦略の中核となるラリー・ウィリアムズの考え方と直接結びついています。
Volatility Breakout Parametersグループには、戦略の中核となる設定が含まれています。
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;
買いと売りの値幅倍率は、エントリーがおこなわれる前に、価格が当日の始値からどれだけ離れて動く必要があるかを定義します。この距離は前日の値幅に対する割合として表現されます。これは、ランダムな値動きではなく、意味のある拡大が起きた後にのみエントリーするというラリー・ウィリアムズの考え方を反映しています。
ストップレンジ乗数は、同じボラティリティの測定値を基にストップロスの距離をどのように決定するかを制御します。固定のポイントではなくボラティリティに基づいてストップを設定することで、市場環境の変化に自然に適応できるようになります。
リワード値はリスクリワードの関係を定義します。ラリー・ウィリアムズの手法では、エグジットは恣意的なものではなく、成功したボラティリティ拡大が複数の負け取引をカバーできるように設計されています。
Trade and Risk Managementグループでは、取引の管理方法とポジションサイズの決定方法を定義します。
input group "Trade and Risk Management" input ENUM_TRADE_DIRECTION direction = TRADE_BOTH; input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode = MODE_AUTO; input double riskPerTradePercent = 1.0; input double positionSize = 0.1;
取引方向入力では、戦略を上昇(ロング)、下降(ショート)、またはその両方の市場状況に限定することができます。これは、より大きな市場のバイアスや上位足のコンテキストに合わせてEAを調整する際に有効です。
ロットサイズモードは、ポジションサイズを手動で固定するか、あらかじめ定義したリスクに基づいて自動計算するかを決定します。自動サイジングを使用する場合、1取引あたりのリスク割合が、口座残高に対してどの程度の割合をリスクにさらすかを定義します。これは、規律あるリスク管理によって生き残るというラリー・ウィリアムズの重要な原則のひとつを強化するものです。手動のポジションサイズ入力は、自動サイジングが無効な場合にのみ使用されます。
これらの入力はすべて、後のセクションでボラティリティレベル、エントリー価格、ストップロス、テイクプロフィットを計算する際に使用されます。
グローバル変数とコア関数
グローバル変数のセクションでは、EA全体で共有されるオブジェクトやデータを初期化します。CTradeオブジェクトはすべての取引操作を担当します。また、実行時に効率よくアクセスできるよう、Bid価格とAsk価格もグローバルに保持されます。
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ //--- Create a CTrade object to handle trading operations CTrade Trade; //--- Bid and Ask double askPrice; double bidPrice;
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); }
OnDeinit関数は、EAがチャートから削除されたときや、プラットフォームが終了するときに実行されます。終了理由をログに記録しておくことは、テストやデバッグの際に役立ちます。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- Notify why the program stopped running Print("Program terminated! Reason code: ", reason); }
OnTick関数は、EAが新しい価格データを受け取るたびに反応する中心的な処理部分です。この段階では、単に現在のBid価格とAsk価格を取得するだけの役割を持っています。しかし今後、この関数は戦略の中核となる部分へと発展していきます。ここでボラティリティレベルの計算が行われ、取引条件が評価されるようになります。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Retrieve current market prices for trade execution askPrice = SymbolInfoDouble (_Symbol, SYMBOL_ASK); bidPrice = SymbolInfoDouble (_Symbol, SYMBOL_BID); }
OnTradeTransaction関数は、取引に関連するイベントを処理するために用意されています。
//+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { }
現時点では空ですが、将来的には約定の監視やポジションのクローズ、さらには取引管理ロジックの実装において役立つようになります。
最後に、Utility Functionsのセクションでは、今後カスタムの補助関数を段階的に追加していきます。
//--- UTILITY FUNCTIONS //+------------------------------------------------------------------+
ここには、ボラティリティの計算、ポジションサイズのロジック、そして再利用可能なコンポーネントが配置されます。これらの関数を分離しておくことで、可読性が向上し、テストもしやすくなり、長期的な保守性も高まります。
新しいバーでの日次ボラティリティレベルの再計算
前に説明した通り、ボラティリティブレイクアウト手法は、各取引日の開始時に再計算されるレベルに依存しています。これらのレベルはその日を通して固定され、エントリー、ストップロスの配置、利益目標といったすべての判断の基準となります。そして新しい日が始まると、古いレベルは破棄され、直近で確定したバーを基に新しいレベルが再計算されます。
この戦略では7つの重要な価格レベルを計算します。最初は前日の値幅で、これは前のバーの高値と安値の差です。この値幅をもとに、ロングとショートの建値を導き出します。続いて、それぞれの方向に対応するストップロスレベルを計算します。最後に、あらかじめ設定されたリワード係数を使ってテイクプロフィットレベルを求めます。これらのレベルは1日に1回だけ再計算する必要があるため、選択した時間足内で新しいバーが形成されたことを正確に検出する仕組みが必要になります。
新しいバーの検出方法
MQL5では、新しいバーの開始を直接通知するイベントは提供されていません。その代わりに、現在のバーの開始時刻と、前回処理したバーの開始時刻を比較することで、この状態を手動で検出します。これを実現するために、 IsNewBarという小さなユーティリティ関数を定義します。この関数はすべてのティックで実行されますが、各バーにつき一度だけtrueを返すように設計されています。
//--- 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; }
この関数は、iTime関数を使って直近のバーの開始時刻を取得します。そして、その値を「最後に処理したバーの時刻」として保存しているタイムスタンプと比較します。時刻が異なれば、新しいバーが形成されたと判断できます。この場合、関数は保存されているタイムスタンプを更新し、trueを返して変化を通知します。
この関数の第3引数は参照渡しで渡されます。これは重要なポイントで、この仕組みによって関数内で更新したバーの時刻が外部にも反映され、ティックをまたいでも値が保持されます。このロジックを支えるために、lastBarOpenTimeというグローバルなdatetime変数を定義します。
//--- To help track new bar open datetime lastBarOpenTime;
この変数は、最後に処理されたバーの開始時刻を追跡する役割を持ち、同じバーに対して再計算が行われないようにするための「メモリ」として機能します。
EAの初期化時には、この変数は明示的に0に設定されます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Initialize global variables lastBarOpenTime = 0; return(INIT_SUCCEEDED); }
これを初期化しておくことで、EAが起動したときの挙動が安定し、古い値が誤って再利用されることを防ぐことができます。
日次ボラティリティレベルのメモリ保存
計算されたレベルは取引中ずっと再利用される必要があるため、毎回再計算するのではなく、構造化されたコンテナに保存します。この目的のために、MqlLwVolatilityLevelsというカスタム構造体を定義します。この構造体はボラティリティに関連するすべての価格レベルを1つの論理的な単位としてまとめる役割を持ちます。具体的には、前日のレンジ、ロングとショートそれぞれの建値、ストップロスレベル、そしてテイクプロフィットレベルを格納します。
//--- Holds all price levels derived from Larry Williams' volatility breakout calculations struct MqlLwVolatilityLevels { double yesterdayRange; double buyEntryPrice; double sellEntryPrice; double bullishStopLoss; double bearishStopLoss; double bullishTakeProfit; double bearishTakeProfit; }; MqlLwVolatilityLevels lwVolatilityLevels;
構造体を定義した後、そのグローバルインスタンスとしてlwVolatilityLevelsを1つ作成します。このインスタンスには当日のアクティブなレベルが保存され、EA全体から簡単にアクセスできるようになります。
初期化時にはZeroMemory関数を使って、この構造体のすべてのフィールドをゼロに設定します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Reset Larry Williams' volatility levels ZeroMemory(lwVolatilityLevels); return(INIT_SUCCEEDED); }
このステップでは、不要なデータをすべてクリアし、各フィールドが必ず既知の値から開始されるようにします。これは、未初期化メモリによって発生する微妙なバグを防ぐうえで重要なプラクティスです。
前日の値幅の計算
この戦略の基盤となるのが前日の値幅です。これを計算するために、GetBarRangeというユーティリティ関数を定義します。
//+------------------------------------------------------------------+ //| Returns the price range (high - low) of a bar at the given index | //+------------------------------------------------------------------+ double GetBarRange(const string symbol, ENUM_TIMEFRAMES tf, int index){ double high = iHigh(symbol, tf, index); double low = iLow (symbol, tf, index); if(high == 0.0 || low == 0.0){ return 0.0; } return NormalizeDouble(high - low, Digits()); }
この関数は、iHighとiLowを使って指定されたインデックスのバーの高値と安値を取得します。前日の値幅を求める場合はインデックスとして1を渡し、これは直近で確定したバーを意味します。高値または安値のどちらかが取得できない場合は、安全のために0を返します。そうでない場合は、高値から安値を差し引くことで値幅を計算し、その結果を銘柄の価格精度に合わせて正規化します。この値は前日の市場の値動きの大きさを示しており、すべてのブレイクアウトレベルを決定する基準となります。
ブレイクアウト建値の計算
利用可能な値幅をもとに、ブレイクアウトの建値を計算します。買い建値は、当日の始値に前日の値幅の一定割合を加算することで導出されます。このロジックはCalculateBuyEntryPrice関数で実装されます。
//+------------------------------------------------------------------+ //| Calculates the bullish breakout entry price using today's open and yesterday's range | //+------------------------------------------------------------------+ double CalculateBuyEntryPrice(double todayOpen, double yesterdayRange, double buyMultiplier){ return todayOpen + (yesterdayRange * buyMultiplier); }
この乗数入力によって、ブレイクアウトレベルの設定をよりアグレッシブにするか、あるいは保守的にするかをトレーダーが調整できるようになります。
同様に、売り建値は、当日の始値から前日の値幅の一定割合を引くことで計算されます。このロジックは CalculateSellEntryPrice関数で処理されます。
//+------------------------------------------------------------------+ //| Calculates the bearish breakout entry price using today's open and yesterday's range | //+------------------------------------------------------------------+ double CalculateSellEntryPrice(double todayOpen, double yesterdayRange, double sellMultiplier){ return todayOpen - (yesterdayRange * sellMultiplier); }
合わせて、これら2つの価格は、市場がエントリーを許可するに値するだけの強さや弱さを示す必要があるポイントを定義しています。
ストップロスレベルの計算
リスク管理は、ラリー・ウィリアムズの手法において中心的な要素です。ストップロスレベルは、建値に対してボラティリティ指標を基に相対的に設定されます。
買いの場合、ストップロスは建値の下側に、前日の値幅の一定割合を用いて配置されます。この計算はCalculateBullishStopLoss関数によって処理されます。
//+------------------------------------------------------------------+ //| Calculates the stop-loss price for a bullish position based on entry price and yesterday's range | //+------------------------------------------------------------------+ double CalculateBullishStopLoss(double entryPrice, double yesterdayRange, double stopMultiplier){ return entryPrice - (yesterdayRange * stopMultiplier); }
売りの場合も同様のロジックが適用され、ストップロスはエントリー価格の上側に配置されます。この計算はCalculateBearishStopLoss関数で実装されています。
//+------------------------------------------------------------------+ //| Calculates the stop-loss price for a bearish position based on entry price and yesterday's range | //+------------------------------------------------------------------+ double CalculateBearishStopLoss(double entryPrice, double yesterdayRange, double stopMultiplier){ return entryPrice + (yesterdayRange * stopMultiplier); }
ボラティリティに基づくストップロス注文を使用することで、リスクが固定されるのではなく、変化する市場状況に適応することが保証されます。
テイクプロフィットレベルの計算
テイクプロフィットレベルは、任意の価格目標ではなく、リスクリワード比に基づいて計算されます。買いの場合、建値とストップロスの間の距離がリスクとして定義されます。この距離にリワード係数を掛けることで利益目標が決定されます。CalculateBullishTakeProfit関数はこのロジックを適用し、テイクプロフィットを建値の上側に配置します。
//+------------------------------------------------------------------+ //| 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()); }
売りの場合も同様のロジックが逆方向に適用されます。CalculateBearishTakeProfit関数はリワード距離を計算し、それをエントリー価格から差し引くことでテイクプロフィットを求めます。
//+------------------------------------------------------------------+ //| 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関数の中で統合します。ターミナルに新しい価格データが到着するたびに、まずEAは現在のBid価格とAsk価格を更新します。その後、IsNewBar関数を使って新しいバーが形成されたかどうかを確認します。
新しいバーが検出された場合、EAはすべてのボラティリティレベルを明確かつ論理的な順序で再計算します。
//+------------------------------------------------------------------+ //| 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.yesterdayRange = GetBarRange(_Symbol, timeframe, 1); lwVolatilityLevels.buyEntryPrice = CalculateBuyEntryPrice (askPrice, lwVolatilityLevels.yesterdayRange, inpBuyRangeMultiplier ); lwVolatilityLevels.sellEntryPrice = CalculateSellEntryPrice(bidPrice, lwVolatilityLevels.yesterdayRange, inpSellRangeMultiplier); lwVolatilityLevels.bullishStopLoss = CalculateBullishStopLoss(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.yesterdayRange, inpStopRangeMultiplier); lwVolatilityLevels.bearishStopLoss = CalculateBearishStopLoss(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.yesterdayRange, inpStopRangeMultiplier); lwVolatilityLevels.bullishTakeProfit = CalculateBullishTakeProfit(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.bullishStopLoss, inpRewardValue); lwVolatilityLevels.bearishTakeProfit = CalculateBearishTakeProfit(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.bearishStopLoss, inpRewardValue); } }
前日の値幅は最初に計算されます。この値幅をもとに建値が算出され、その後にストップロスレベル、そして最後にテイクプロフィットレベルが計算されます。各計算結果はすべて lwVolatilityLevels構造体に保存されます。ここから先は、これらの値は1日を通して一定に保たれ、取引実行ロジックから効率的に参照されることになります。
この設計により、ボラティリティレベルは1日1回だけ計算され、一日を通して一貫性を持ち続け、ラリー・ウィリアムズのボラティリティブレイクアウトの本質的な考え方を反映しています。
次のセクションでは、これらの保存されたレベルを使って、取引の実行とポジション管理を構造的かつ規律ある形で制御していきます。
取引ロジックの完成と取引の実行
この開発段階では、EAはすでに「何を」「どこで」取引するかを理解しています。前日の値幅から導出されたボラティリティベースのレベルは1日1回計算され、その後は新しいバーが形成されるまで有効な状態が維持されます。残る課題は、価格がこれらのレベルに到達したときにEAがどのように反応するかを定義することです。
ラリー・ウィリアムズのボラティリティブレイクアウトロジックは実行面では非常にシンプルです。価格があらかじめ定義されたレベルを超えて拡大したとき、市場はその方向への意図を示しています。ここでは方向を予測するのではなく、拡大に対して反応します。つまりEAは、価格がロングまたはショートのエントリーレベルを明確に突破するまでは行動しません。この動作を正確に実現するために、まず価格が特定のレベルを下から上へクロスしたことを検出する仕組みを導入する必要があります。
クロスオーバーの検出
最初に導入するユーティリティ関数は、価格が特定のレベルを下から上へクロスした瞬間を検出するものです。これが買いエントリーをおこなうトリガーとなります。
//+------------------------------------------------------------------+ //| 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つの連続した終値を比較します。直前の終値がそのレベル以下であり、直近の終値がそのレベルを上回った場合、強気のクロスオーバーが発生したと判断されます。この方法では、単に価格がレベルに触れただけでは反応しません。代わりに、価格が明確にそのレベルを突破したことを確認します。実務的には、これはラリー・ウィリアムズの「拡大こそが重要である」という考え方と一致しています。真のブレイクアウトはタッチではなく、推進的な動きです。
クロスアンダーの検出
2つ目のユーティリティ関数はその逆の役割を持ちます。価格があらかじめ定義されたレベルを下回ってクロスした瞬間を検出します。
//+------------------------------------------------------------------+ //| 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分足の終値配列を基に動作します。この配列は常にインデックス0が最新のバーとなるように扱う必要があります。
これを実現するために、この配列はグローバルスコープで宣言され、EA全体から参照できるようにされています。
//--- To store minutes data double closePriceMinutesData [];
EAの初期化時には、この配列は明示的に時系列配列として設定されます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Treat the following arrays as timeseries (index 0 becomes the most recent bar) ArraySetAsSeries(closePriceMinutesData, true); return(INIT_SUCCEEDED); }
これにより、インデックス0が常に最新データを表し、インデックス1がその前のバー、というように一貫した構造が保証されます。この設計により、EAは上位時間足をベースとしたロジックであっても、短期的な価格変動に対して迅速かつ正確に反応できるようになります。
ティック関数の内部では、この配列は少量の履歴データを使って継続的に更新されます。クロス検出には直近のバーだけが必要ですが、少し多めのデータをコピーしておくことで安定性が高まり、エッジケースによるエラーも防ぐことができます。
//+------------------------------------------------------------------+ //| 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; } }
価格データが取得できない場合、EAはそのティックの処理を停止します。これにより、取引ロジックが不完全または無効なデータに基づいて動作するのを防ぎます。
同時ポジションの防止
ラリー・ウィリアムズのボラティリティブレイクアウトモデルは、一方向への一貫したコミットメントを前提としています。ブレイクアウトが発生した場合、トレーダーはロングかショートのどちらか一方のみを保有し、両方を同時に持つことはありません。このルールをプログラムで強制するために、EAは常に既存のポジションの有無を把握している必要があります。
この責任は2つのユーティリティ関数によって処理されます。最初の関数はすべてのポジションを走査し、このEAによって開かれたアクティブな買いポジションが存在するかどうかを確認します。
//+------------------------------------------------------------------+ //| 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; }
これは、ポジションのマジックナンバーとポジションタイプを比較することで判定されます。一致する買いポジションが見つかった場合、この関数はtrueを返します。2つ目の関数は、同様のロジックを売りポジションに対して適用します。
//+------------------------------------------------------------------+ //| 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つの取引だけが存在する状態が保証されます。
ポジションサイズの自動計算
リスク管理は、ラリー・ウィリアムズの手法において重要な要素です。任意のロットサイズで取引するのではなく、リスクは口座残高に対する割合として定義されます。ポジションサイズ計算関数はこの考え方を直接実装したものです。
//+------------------------------------------------------------------+ //| 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); }
まず、口座残高と設定されたリスクパーセンテージに基づいて、トレーダーが許容するリスク金額を計算します。次に、そのリスクを取引対象の銘柄の契約サイズとストップロスの距離を考慮してボリュームに変換します。これにより、市場のボラティリティやストップロスの大きさに関係なく、常に一定割合のリスクで取引がおこなわれるようになります。最後に、この値はブローカーのボリューム仕様に適合するように精度調整されます。
買いポジションのオープン
買いエントリーの実行関数は、買いポジションを開くするために必要なすべての処理を担当します。
//+------------------------------------------------------------------+ //| 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; }
自動ポジションサイズが有効な場合、この関数は先ほど定義したリスクロジックを使って適切なロットサイズを計算します。この計算に使用されるストップロス距離は、ボラティリティベースのレベルから直接取得されており、リスクが常に市場環境に連動するようになっています。その後、取引オブジェクトを使って成行の買い注文を送信します。約定に失敗した場合は、原因の特定に役立つ詳細なエラー情報を出力します。約定が成功した場合は、その成功を確認し、EAへ制御を戻します。
売りポジションのオープン
売り注文の実行関数は、買いのロジックと同じ構造を持ちながら、方向だけが逆になっています。
//+------------------------------------------------------------------+ //| 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; }
売り注文では、弱気側のストップ距離を用いてポジションサイズを計算し、適切な建値、ストップロス、テイクプロフィットレベルとともに成行の売り注文を送信します。この対称的な設計により、ロングとショートの両方が一貫したルールで扱われるため、ロジックの誤りが減り、コードの保守性も高まります。
ティック関数で全体を統合する
すべてのコンポーネントが揃ったことで、ティック関数はEAの制御の中心として機能するようになります。
//+------------------------------------------------------------------+ //| 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); } } } }
まず、価格データが最新の状態であることを確認します。その後、ユーザー設定に基づいてロングとショートのロジックをそれぞれ独立して評価します。買いエントリーの場合、まず買いが許可されているかをチェックします。次に、買いエントリーレベルで強気のクロスオーバーが発生しているかを確認します。クロスオーバーが検出され、かつ既存のポジションが存在しない場合にのみ、買いエントリーがおこなわれます。
同じ処理は売りにも適用され、売りエントリーレベルと弱気のクロスオーバー検出を使用します。この構造により、EAは有効なブレイクアウトイベントにのみ反応し、方向設定を尊重しながら、常に単一ポジションルールを維持します。
テスト時の視覚的な見やすさの改善
テストとバックテストに進む前に、最後の改善としてチャート設定を追加します。専用のユーティリティ関数を使ってチャートの表示を調整し、視認性を向上させます。
//+------------------------------------------------------------------+ //| 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; } return(INIT_SUCCEEDED); }
チャート設定が失敗した場合、EAは読み込みを停止します。これにより、取引を開始する前に環境が正しく準備されていることが保証されます。
この時点でEAは完全に実装されています。ソースコードをコンパイルしてください。すべての手順が正しくおこなわれていれば、コンパイルはエラーなく完了するはずです。問題が発生した場合は、付属のlwVolatilityBreakoutExpert.mq5ファイルを参照し、正確性を確認してください。
バックテスト結果
戦略を現実的な環境で検証するために、このEAはゴールド(XAUUSD)の日足でテストされました。バックテスト期間は2025年1月1日から2025年11月30日までで、執筆時点での直近11か月間の市場データをカバーしています。
結果は良好なものとなっています。初期資金1万米ドルから開始し、テスト期間中にシステムは60%強の成長を達成しました。実現利益は6,203ドル49セントです。

さらに重要なのは、エクイティカーブが極端な変動や不安定なドローダウンなしに、安定して推移している点です。

このような滑らかな資産推移は、ボラティリティブレイクアウト戦略の思想と一致しています。この戦略は短期的なノイズを追うのではなく、持続的なトレンドの流れに参加することを目的としています。
再現性を高めるために、この記事には2つのファイルが添付されています。configurations.iniにはテスト環境の設定が含まれており、parameters.setにはバックテストで使用した入力値が保存されています。これらはMetaTrader 5のストラテジーテスターに直接読み込むことができ、同様のテストを再現できます。
どの取引戦略も、すべての市場や環境で同一の結果を出すわけではありません。ゴールドには独自のボラティリティ特性と値動きの性質があり、同じロジックでも通貨、指数、コモディティなどで結果は異なる可能性があります。これは戦略が脆弱であることを意味するものではなく、テストと適応の重要性を示しています。
この文章の主目的は最適化ではありません。ラリー・ウィリアムズのボラティリティブレイクアウトの概念を、明確で構造化された自動売買システムとして実装することに重点があります。入力パラメータは意図的に柔軟に設計されており、各自の観察やリスク許容度に応じて試行、検証、改善できるようになっています。この探求のプロセスこそが、個々のエッジを形成する重要な部分です。
次のステップとしては、異なる銘柄や時間足で追加のバックテストをおこない、値幅乗数、ストップ設定、リワード値を調整し、それらの変更がパフォーマンスにどのような影響を与えるかを観察することが推奨されます。規律ある検証と分析を通じて、この自動化フレームワークを自身の取引目標や市場特性に適応させていくことができます。
結論
この記事では、ラリー・ウィリアムズのボラティリティブレイクアウトの原則を、概念から実行可能な形へと落とし込み、完全に機能するテスト可能なMQL5 EAへと変換する一連の流れを紹介しました。まず、異常なレンジ拡大がなぜ重要な値動きの始まりを示すのかを理解するところから始め、その後、ボラティリティを測定し、客観的なエントリーおよびエグジットレベルを定義し、規律あるリスク管理を実現するための論理的なフレームワークを慎重に設計しました。段階的に理論をコードへと変換し、それぞれの計算、条件、判断がすべてラリー・ウィリアムの原理に基づいた明確な目的を持つように構築しています。
この手順を通じて、読者は検証可能で柔軟に拡張できるEA構造とともに、実践的な取引アイデアの自動化の設計図を手にしました。さらに重要なのは、この戦略を適用する際に手動の判断や主観に依存する必要がなくなったという点です。代わりに、バックテストやフォワードテストを通じて客観的に評価可能な体系的アプローチを持つことができます。
この記事の本質は、単に一つの取引システムを構築することではありません。取引ロジックをコードに変換する際に明確に考えること、リスクを尊重すること、そして自分自身のエッジを発見する余地を持つことを学ぶ点にあります。この基盤をもとに、ボラティリティベースの取引アイデアに対してさらなる改良、追加フィルター、より深い研究へと進む準備が整っています。
以下の表は、この記事に添付されているすべてのファイルの簡単な説明です。
| ファイル名 | 説明 | |
|---|---|---|
| 1 | lwVolatilityBreakoutExpert.mq5 | 本記事で段階的に構築したボラティリティブレイクアウトEAの完全なMQL5ソースコード |
| 2 | configurations.ini | バックテスト時に使用したストラテジーテスターの設定ファイル(検証環境を再現するために使用) |
| 3 | parameters.set | 本記事のバックテスト結果で使用された入力パラメータファイル |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20745
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5入門(第33回):MQL5のAPIとWebRequest関数の習得(VII)
MetaTrader 5用シグマスコアインジケーター:単純な統計的異常検出器
MQL5でカスタムインジケーターを作成する(第4回):デュアルオシレーター搭載Smart WaveTrend Crossover
データベースは簡単(第1回):SQLiteを用いたMQL5向け軽量ORMフレームワーク
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
新しい記事をご覧ください:ラリー・ウィリアムズ相場の秘密(パート5):MQL5でボラティリティ・ブレイクアウト戦略を自動化する。
著者チャチャ・イアン・マロア
さて、私のように、あなたの継続的な知識の収集、レッスン、あなたが提示するすべてのトピックにわたる発見の共有を十分に楽しんでいる多くの人が読んでいると思われます。
ありがとうございます。