English Deutsch
preview
ラリー・ウィリアムズの『市場の秘密』(第8回):ボラティリティ、ストラクチャー、時間フィルターの組み合わせ

ラリー・ウィリアムズの『市場の秘密』(第8回):ボラティリティ、ストラクチャー、時間フィルターの組み合わせ

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

はじめに

多くの短期取引システムが失敗する理由は単純です。すべての市場状態、すべての曜日、すべての時間帯を同一条件として扱ってしまうためです。エントリーは文脈を伴わずに発生し、ストップは構造を考慮せずに配置され、決済は場当たり的に決定されます。その結果、市場価格には反応しているものの、価格の意味を捉えていない機械的な戦略が生まれます。

ラリー・ウィリアムズは市場を異なる視点から捉えました。彼はインジケーターや固定ルールから出発するのではなく、市場の挙動を起点に思考しています。価格がどのように拡大するか、ボラティリティがどのようにトレンドに先行するか、そして極端な感情状態がどのように機会を生むか、さらに時間そのものが市場変動にどのような影響を与えるかに注目しました。彼のアプローチは単一のシステムではなく、市場が実際に何をしているのかに基づいた取引のロジックを構築する考え方です。

本記事では、ラリー・ウィリアムズの主要な概念をいくつか統合し、1つの検証可能な取引モデルとして構築します。短期的な市場構造とボラティリティベースのエントリーを組み合わせ、さらに市場のバイアスを反映する時間フィルターを加えます。また、柔軟なストップ配置と適応的な利確ロジックも導入します。そして、それらすべてを設定可能なエキスパートアドバイザー(EA)としてまとめ、研究、テスト、改変、拡張が可能な形にします。
これは単なる戦略の固定テンプレートではありません。フレームワークです。エントリー、エグジット、リスク、タイミングを独立したルールではなく相互に関連する構成要素として捉えるための方法論です。すべての主要な意思決定ポイントはユーザー設定として提供され、すべてのロジックは主観ではなくテストによって検証可能な形で実装されます。

本稿の目的は明確です。プロフェッショナルな取引思想を、どのようにして整理されたMQL5ロジックへ変換できるかを示すことです。さらに、その基盤を提供することで、追加のフィルター、より優れたエグジットロジック、そして新たな研究アイデアを段階的に重ねていける構造を提示します。


戦略の概要

この取引モデルは、シンプルな考え方に基づいて構築されています。トレンドはボラティリティから始まり、ボラティリティがエントリーを生み、フィルターが取引の質を高め、選択性が生存率を向上させるというものです。

ランダムな価格変動に反応するのではなく、EAはまず市場構造の明確な変化を待ちます。短期的なスイングローが3本の連続したローソク足によって形成されると、強気シグナルが成立します。同様に、短期的なスイングハイが3本の連続したローソク足によって形成されると、弱気シグナルが成立します。これらのスイングポイントは、価格が一方向への進行に失敗し、反転し始めた領域を示しており、次の拡張(ブレイクアウト)のための構造的な文脈を提供します。

有効なスイングシグナルが新しいバーの始値で発生しても、EAは即座にはエントリーしません。代わりに、ボラティリティモデルのいずれかを用いてブレイクアウトのエントリーレベルを算出します。1番目のモデルは直近で確定したローソク足のレンジを基準とする方法であり、2番目のモデルはラリー・ウィリアムズのスイングベースのボラティリティ手法で、過去のスイング距離を比較し、優勢な値を基準レンジとして採用します。どちらを使用するかはユーザー設定によって制御されます。

この基準レンジから、買いおよび売りの予測レベルが算出されます。これらは価格の閾値として機能し、価格が買いレベルを上抜けするか、売りレベルを下抜けした場合のみ取引が実行されます。いずれのレベルにも到達しない場合、そのセットアップは無効として破棄されます。遅延エントリーはおこなわれず、すべての取引は構造とボラティリティ拡張の両方によってトリガーされる必要があります。

ストップロスの配置も同様に、柔軟性と構造的認識に基づいています。EAは2つのストップ配置モードをサポートします。1番目のモードでは、ストップロスは基準レンジの一定割合として計算され、エントリー価格に対して配置されます。2番目のモードでは、スイング極値に配置されます。買いでは中間スイングバーの安値、売りでは中間スイングバーの高値が使用されます。どちらのモデルを使用するかはユーザー設定によって制御されます。

利確ロジックも複数の選択肢として構成されています。EAは3つのエグジットモードをサポートします。1番目は、含み益が発生した最初のタイミングで決済する方法です。2番目は、指定されたバー数経過後に決済する方法です。3番目は、リスクリワード比に基づいてテイクプロフィットを設定する方法で、エントリーとストップロスの距離を基準に計算されます。これにより、同一戦略ロジックを異なる取引管理手法で検証することが可能になります。

時間フィルターはオプションで適用されます。取引は曜日(Trade Day of the Week)や時間帯によって制限することができ、市場行動が時間帯ごとにどのように変化するかを分析し、より高品質なシグナルが得られる期間を抽出できます。これらのフィルターは独立して有効化または無効化でき、すべての取引実行前に適用されます。

リスク管理はエントリーロジックに統合されています。EAは手動ロット指定と、口座残高に対する一定割合に基づく自動ポジションサイズ設定の両方をサポートします。自動モードでは、ストップロス距離に基づいてロットサイズが計算されるため、取引間で一貫したリスクエクスポージャーが維持されます。

同時保有ポジションは1つに制限されています。このルールにより取引管理が簡素化され、各セットアップが独立して評価されます。EAはロング専用、ショート専用、または両方向の取引に設定可能です。

このモデルの本質的は、ロジックの順序にあります。エントリーはボラティリティ単独ではなく、まず構造によって条件が成立し、その後にボラティリティが確認要素として機能します。フィルターは後付けではなく、取引品質を形成する中核要素として扱われます。エグジットは固定ではなく、検証可能で調整可能な設計になっています。

このEAの設計目標は、単一の最適化された戦略を提供することではありません。ボラティリティ、構造、時間の相互作用を体系的にテストするための柔軟なフレームワークを提供することです。すべての意思決定ポイントはユーザーによって変更可能であり、各コンセプトは修正および検証可能な論理構造として提示されています。その結果、このEAは単なる取引ツールではなく、プロフェッショナルな短期取引戦略を構築し、検証するための研究基盤となっています。


EA構築のステップバイステップ解説

本セクションのコードを正しく理解するためには、事前にいくつかの基礎知識が必要です。
まず、MQL5プログラミング言語の基本的な理解を前提とします。変数、関数、条件分岐、ループ、列挙型、構造体、標準ライブラリの使用といった概念は、すでに理解していることを想定しています。もしこれらに不慣れな場合は、続行する前に公式のMQL5リファレンスを参照することを推奨します。

次に、MetaTrader 5プラットフォームの使用経験を前提とします。具体的には、チャートの基本操作、EAのチャートへの適用、ストラテジーテスターでのバックテスト実行などが含まれます。

さらに、MetaEditorの操作に関する実務的な知識も必要です。新規ソースファイルの作成、コードの記述、コンパイル、エラー発生時の確認といった基本操作ができることを前提としています。

本セクションは実践的な構築プロセスとして設計されています。プログラミングは受動的に読むよりも、実際に手を動かすことで最も効果的に習得できます。そのため、完成済みのソースファイルlwVolatilityStructureTimeFilterExpert.mq5を本記事に添付しています。学習中はこのファイルをダウンロードし、参照用として別タブで開きながら、同じロジックを段階的に構築していくことを推奨します。

EAの基盤を構築する

まずMetaEditorを開き、新しい空のEA用ソースファイルを作成します。ファイル名は任意ですが、混乱を避けるため、添付ファイルと同じ名前を使用します。その後、以下のボイラープレートコードを新規ファイルに貼り付けます。

//+------------------------------------------------------------------+
//|                        lwVolatilityStructureTimeFilterExpert.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_VOLATILITY_ENTRY_MODE
{
   VOL_SIMPLE_PREVIOUS_RANGE,
   VOL_SWING_BASED
};

enum ENUM_STOP_LOSS_MODE
{
   SL_BY_RANGE_PERCENT,
   SL_AT_SWING_EXTREME
};

enum ENUM_TAKE_PROFIT_MODE
{
   TP_FIRST_PROFITABLE_OPEN,
   TP_AFTER_N_CANDLES,
   TP_BY_RISK_REWARD 
};

enum ENUM_TDW_MODE
{
   TDW_ALL_DAYS,     
   TDW_SELECTED_DAYS
};

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 ENUM_VOLATILITY_ENTRY_MODE volatilityEntryMode = VOL_SIMPLE_PREVIOUS_RANGE;
input double inpBuyRangeMultiplier                   = 0.50;   
input double inpSellRangeMultiplier                  = 0.50;   
input double inpStopRangeMultiplier                  = 0.50;

input group "TDW filter"
input ENUM_TDW_MODE tradeDayMode = TDW_SELECTED_DAYS;
input bool tradeSunday           = false;
input bool tradeMonday           = true;
input bool tradeTuesday          = false;
input bool tradeWednesday        = false;
input bool tradeThursday         = false;
input bool tradeFriday           = false;
input bool tradeSaturday         = false;

input group "Time of Day filter"
input bool useTimeFilter         = false; 
input double startTime           = 9.30; 
input double endTime             = 16.00;

input group "Trade & Risk Management"
input ENUM_TRADE_DIRECTION direction       = ONLY_LONG;
input ENUM_STOP_LOSS_MODE stopLossMode     = SL_BY_RANGE_PERCENT;
input ENUM_TAKE_PROFIT_MODE takeProfitMode = TP_BY_RISK_REWARD;
input double riskRewardRatio               = 3.0;
input int exitAfterCandles                 = 3;
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;
datetime currentTime;

//+------------------------------------------------------------------+
//| 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);
   currentTime   = TimeCurrent();

}

//--- UTILITY FUNCTIONS

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

この基盤部分は、EAのアイデンティティを定義し、標準取引ライブラリをインクルードし、すべての設定可能なモードのためのカスタム列挙型を宣言し、さらにボラティリティロジック、フィルター、リスク管理、執行ロジックを制御するユーザー入力パラメータを提供します。

この段階では、まだ一切の取引はおこなわれていません。ここで定義しているのは「何ができるか」であり、「何をおこなうか」ではありません。以降のステップで、この骨組みに段階的に実際の意思決定機能が付与されていきます。

新しいバーの開始検出

この戦略ロジックは、選択された時間足で新しいバーが開始したタイミングで動作します。これは重要な前提です。スイングポイント、ボラティリティレンジ、そして予測エントリーレベルは、必ず1本のローソク足が完全に確定した後にのみ再計算されるべきだからです。

この挙動を実現するために、以下の関数を定義します。

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

この関数は、最新のバーの始値を取得し、それを保持している値と比較します。時間が変化している場合、それは新しいバーが形成されたことを意味します。保持されている値は参照によって更新されるため、次回の呼び出し時に次のバーの変化を検出できるようになります。このロジックを支えるために、グローバル変数を宣言します。

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

この変数は、最後に検出されたバーの始値を保持するものであり、EAの初期化時に0として初期化されます。

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

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

   return(INIT_SUCCEEDED);
}

この変数以降、各バーにつき一度だけ実行すべきロジックは、この条件の中に安全にまとめることができます。

ボラティリティレベルを格納するコンテナの作成

新しいバーの開始時におこなう最も重要な処理のひとつは、予測エントリーレベル、ストップロス、テイクプロフィットの各水準を計算することです。これらの値を整理し、EA全体で再利用可能にするために、専用の構造体を定義します。

//--- Holds all price levels derived from Larry Williams' volatility breakout calculations
struct MqlLwVolatilityLevels
{
   double range;      
   double buyEntryPrice;       
   double sellEntryPrice;   
   double bullishStopLoss;   
   double bearishStopLoss;    
   double bullishTakeProfit;
   double bearishTakeProfit;
   double bullishStopDistance;
   double bearishStopDistance;
};
各フィールドは、ラリー・ウィリアムズのボラティリティロジックを構成する特定の要素を表しています。
  • rangeフィールドは、予測計算に使用される基準となるボラティリティレンジを保持します。
  • buyEntryPriceおよびsellEntryPriceフィールドは、ブレイクアウトのエントリーレベルを保持します。
  • ストップロスのフィールドは、プロテクティブストップとしての水準を格納します。
  • テイクプロフィットのフィールドは、リスクリワードモードが有効な場合の目標価格を保持します。
  • ストップ距離のフィールドは、動的なポジションサイズ計算に使用されるリスク距離を保持します。

その後、この構造体のインスタンスを定義の直下に1つ作成します。

MqlLwVolatilityLevels lwVolatilityLevels;

このインスタンスはEA全体で共有されるメモリ領域として機能します。これにより、各バーにつき一度だけレベルを計算し、それらをエントリー、リスク管理、エグジットロジック全体で一貫して再利用することが可能になります。

また、EAの初期化時には、すべてのフィールドを0に初期化します。

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

   ...
   
   //--- Reset Larry Williams' volatility levels 
   ZeroMemory(lwVolatilityLevels);

   return(INIT_SUCCEEDED);
}
これにより、以前の実行から引き継がれた古い値が残らないことが保証されます。

ラリー・ウィリアムズ式短期スイングポイントの検出

この戦略ロジックは構造認識から始まります。強気シグナルは短期スイングローが形成された場合にのみ有効となり、弱気シグナルは短期スイングハイが形成された場合にのみ有効となります。

短期スイングハイを検出するために、以下を定義します。

//+------------------------------------------------------------------+
//| Detects a Larry Williams short-term high on the last three bars  |
//| Bar index 2 must be a swing high with lower highs on both sides  |
//| Bar 2 must NOT be an outside bar                                 |
//| Bar 1 must NOT be an inside bar                                  |
//+------------------------------------------------------------------+
bool IsLarryWilliamsShortTermHigh(string symbol, ENUM_TIMEFRAMES tf){

   //--- Price data for the three bars
   double high1 = iHigh(symbol, tf, 1);
   double low1  = iLow (symbol, tf, 1);

   double high2 = iHigh(symbol, tf, 2);
   double low2  = iLow (symbol, tf, 2);

   double high3 = iHigh(symbol, tf, 3);
   double low3  = iLow (symbol, tf, 3);

   //--- Condition 1: Bar 2 must be a swing high
   bool isSwingHigh =
      (high2 > high1) &&
      (high2 > high3);

   if(!isSwingHigh){
      return false;
   }      

   //--- Condition 2: Bar 2 must NOT be an outside bar relative to bar 3
   bool isOutsideBar =
      (high2 > high3) &&
      (low2  < low3);

   if(isOutsideBar){
      return false;
   }

   //--- Condition 3: Bar 1 must NOT be an inside bar relative to bar 2
   bool isInsideBar =
      (high1 < high2) &&
      (low1  > low2);

   if(isInsideBar){
      return false;
   }

   return true;
}

この関数は、直近で確定した3本のローソク足の高値と安値を取得します。そのうえで、3つの構造条件をチェックします。第一に、インデックス2のバーがスイングハイである必要があります。このバーの高値は、隣接する両方のバーよりも高くなければなりません。第二に、インデックス2のバーがインデックス3のバーに対してアウトサイドバーになっていないことが条件となります。これにより、極端にレンジの広い不安定なバーを除外します。第三に、インデックス1のバーがインデックス2のバーに対してインサイドバーになっていないことが必要です。これにより、圧縮された継続パターンを排除し、明確なスイングとして成立しない構造を除外します。これら3つの条件がすべて満たされた場合にのみ、trueを返します。

短期スイングローのロジックも、この構造を対称的に反転させたものとして同様に実装されます。

//+------------------------------------------------------------------+
//| Detects a Larry Williams short-term low on the last three bars   |
//| Bar index 2 must be a swing low with higher lows on both sides   |
//| Bar 2 must NOT be an outside bar                                 |
//| Bar 1 must NOT be an inside bar                                  |
//+------------------------------------------------------------------+
bool IsLarryWilliamsShortTermLow(string symbol, ENUM_TIMEFRAMES tf){

   //--- Price data for the three bars
   double high1 = iHigh(symbol, tf, 1);
   double low1  = iLow (symbol, tf, 1);

   double high2 = iHigh(symbol, tf, 2);
   double low2  = iLow (symbol, tf, 2);

   double high3 = iHigh(symbol, tf, 3);
   double low3  = iLow (symbol, tf, 3);

   //--- Condition 1: Bar 2 must be a swing low
   bool isSwingLow =
      (low2 < low1) &&
      (low2 < low3);

   if(!isSwingLow){
      return false;
   }
      
   //--- Condition 2: Bar 2 must NOT be an outside bar relative to bar 3
   bool isOutsideBar =
      (high2 > high3) &&
      (low2  < low3);

   if(isOutsideBar){
      return false;
   }

   //--- Condition 3: Bar 1 must NOT be an inside bar relative to bar 2
   bool isInsideBar =
      (high1 < high2) &&
      (low1  > low2);

   if(isInsideBar){
      return false;
   }

   return true;
}

ここでは、インデックス2のバーがスイングローである必要があります。このバーの安値は、両隣のバーよりも低くなければなりません。アウトサイドバーおよびインサイドバーの除外条件も同様に適用されます。これら2つの関数によって、有効な市場構造シグナルが成立するかどうかが定義されます。以降のすべてのエントリーロジックは、まずこの条件が満たされていることを前提として動作します。

ラリー・ウィリアムズ式2つのボラティリティモデルによる計測

構造シグナルが成立した後は、エントリーレベルを設定するためにボラティリティを計測する必要があります。EAでは2種類のボラティリティモデルをサポートしています。1番目のモデルはスイングベースのボラティリティです。

//+------------------------------------------------------------------+
//| 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つの過去スイング距離を計測します。1つ目の距離は「3日前の高値から昨日の安値」までの値幅です。2つ目の距離は「1日前の高値から3日前の安値」までの値幅です。両方の距離の絶対値を取り、大きい方を選択します。これにより、方向に依存することなく、直近で最も支配的な拡張を常に使用することが保証されます。選択されたレンジは、基準となるボラティリティ指標として機能します。2番目のモデルは、単純な直近レンジベースのボラティリティです。

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

この関数は、選択されたバーの高値と安値の差を返します。本ケースでは、インデックス1のバーを使用し、前日のレンジを測定します。これら2つの関数は、異なるボラティリティの考え方を支えています。一方は直近スイングの拡張を捉え、もう一方は純粋な日次の値動きを捉えます。

エントリーレベルの設定

基準となるレンジが得られたら、次にエントリーレベルを設定します。強気ブレイクアウトの場合:

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

   return todayOpen + (range * buyMultiplier);
}
本日の始値にレンジの一定割合を加算します。これが、上昇モメンタムを確認するために、価格が上抜ける必要のあるブレイクアウトレベルになります。

弱気ブレイクアウトの場合:

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

   return todayOpen - (range * sellMultiplier);
}

本日の始値からレンジの一定割合を差し引きます。これら2つの関数は、ボラティリティを実行可能なブレイクアウトレベルへと変換します。

ストップロスとテイクプロフィットの設定

ストップロスは2つのモデルで設定できます。

レンジベースのモデル:

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

   return entryPrice - (range * stopMultiplier);
}
 
//+--------------------------------------------------------------------------------------------------+
//| Calculates the stop-loss price for a bearish position based on entry price and yesterday's range |
//+--------------------------------------------------------------------------------------------------+
double CalculateBearishStopLoss(double entryPrice, double range, double stopMultiplier){

   return entryPrice + (range * stopMultiplier);
}

これらの関数は、ストップロスをエントリー価格から基準レンジの一定割合分だけ離して配置します。一方でスイング極値モデルでは、インデックス2のバーの安値または高値がプロテクティブストップとして使用されます。

テイクプロフィットのレベルは、リスクリワードモードが有効な場合にのみ設定されます。

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

これらの関数は、ストップ距離に設定されたリワード係数を掛け合わせ、その結果に基づいてターゲット価格を設定します。

ボラティリティ計算の一元化

すべてのボラティリティロジックは、単一の関数に統合されています。

//+--------------------------------------------------------------------------------------------------------------+
//| Calculates and updates all volatility-based entry, stop, and take-profit levels based on the selected models |                             
//+--------------------------------------------------------------------------------------------------------------+
void UpdateVolatilityEntryLevels(){

   if(volatilityEntryMode == VOL_SWING_BASED){
      lwVolatilityLevels.range              = CalculateLwSwingVolatilityRange(_Symbol, timeframe);
      lwVolatilityLevels.buyEntryPrice      = CalculateBuyEntryPrice (askPrice, lwVolatilityLevels.range, inpBuyRangeMultiplier );
      lwVolatilityLevels.sellEntryPrice     = CalculateSellEntryPrice(bidPrice, lwVolatilityLevels.range, inpSellRangeMultiplier);
      if(stopLossMode == SL_BY_RANGE_PERCENT){
         lwVolatilityLevels.bullishStopLoss = CalculateBullishStopLoss(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.range,  inpStopRangeMultiplier);
         lwVolatilityLevels.bearishStopLoss = CalculateBearishStopLoss(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.range, inpStopRangeMultiplier);                   
      }
      if(stopLossMode == SL_AT_SWING_EXTREME){
         lwVolatilityLevels.bullishStopLoss = iLow(_Symbol,  timeframe, 2);
         lwVolatilityLevels.bearishStopLoss = iHigh(_Symbol, timeframe, 2);                  
      }
      lwVolatilityLevels.bullishTakeProfit   = CalculateBullishTakeProfit(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.bullishStopLoss,  riskRewardRatio);
      lwVolatilityLevels.bearishTakeProfit   = CalculateBearishTakeProfit(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.bearishStopLoss, riskRewardRatio);
      lwVolatilityLevels.bullishStopDistance = lwVolatilityLevels.buyEntryPrice - lwVolatilityLevels.bullishStopLoss;
      lwVolatilityLevels.bearishStopDistance = lwVolatilityLevels.bearishStopLoss - lwVolatilityLevels.sellEntryPrice;
   }
   
   if(volatilityEntryMode == VOL_SIMPLE_PREVIOUS_RANGE){
      lwVolatilityLevels.range              = GetBarRange(_Symbol, timeframe, 1);
      lwVolatilityLevels.buyEntryPrice      = CalculateBuyEntryPrice (askPrice, lwVolatilityLevels.range, inpBuyRangeMultiplier );
      lwVolatilityLevels.sellEntryPrice     = CalculateSellEntryPrice(bidPrice, lwVolatilityLevels.range, inpSellRangeMultiplier);
      if(stopLossMode == SL_BY_RANGE_PERCENT){
         lwVolatilityLevels.bullishStopLoss = CalculateBullishStopLoss(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.range,  inpStopRangeMultiplier);
         lwVolatilityLevels.bearishStopLoss = CalculateBearishStopLoss(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.range, inpStopRangeMultiplier);                   
      }
      if(stopLossMode == SL_AT_SWING_EXTREME){
         lwVolatilityLevels.bullishStopLoss = iLow(_Symbol,  timeframe, 2);
         lwVolatilityLevels.bearishStopLoss = iHigh(_Symbol, timeframe, 2);                  
      }
      lwVolatilityLevels.bullishTakeProfit   = CalculateBullishTakeProfit(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.bullishStopLoss,  riskRewardRatio);
      lwVolatilityLevels.bearishTakeProfit   = CalculateBearishTakeProfit(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.bearishStopLoss, riskRewardRatio);
      lwVolatilityLevels.bullishStopDistance = lwVolatilityLevels.buyEntryPrice - lwVolatilityLevels.bullishStopLoss;
      lwVolatilityLevels.bearishStopDistance = lwVolatilityLevels.bearishStopLoss - lwVolatilityLevels.sellEntryPrice;
   }
}

この関数は、どのボラティリティ・エントリーモデルが選択されているかを判定します。そのうえで、基準レンジ、エントリーレベル、ストップロスレベル、テイクプロフィットレベル、そしてストップ距離を計算します。すべての値は共有構造体の中に格納されます。

この関数は、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)){
      
      //--- Recalculate volatility entry levels on new bar based on the selected entry and stop models
      UpdateVolatilityEntryLevels();
   }   
}

日中ブレイクアウトの1分足データによる追跡

ブレイクアウトの確認にバーの確定は使用しません。代わりに、1分足データを用いてリアルタイムの価格変動を追跡します。この処理を支えるために、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分足終値を比較し、価格が予測されたレベルを上抜けまたは下抜けしたかどうかを判定します。このロジックを支えるために、グローバル配列を定義します。

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

これを時系列データとして扱い、CopyCloseを用いて毎ティックごとに更新します。

取引曜日および時間帯フィルタリング

取引曜日フィルターを実装するために、以下を定義します。

//+------------------------------------------------------------------------------------+
//| Returns the day of the week (0 = Sunday, 6 = Saturday) for the given datetime value|                               
//+------------------------------------------------------------------------------------+
int TimeDayOfWeek(datetime time){
   MqlDateTime timeStruct = {};
   if(!TimeToStruct(time, timeStruct)){
      Print("TimeDayOfWeek: TimeToStruct failed");
      return -1;
   }      
   return timeStruct.day_of_week;
}

//+-----------------------------------------------------------------------------------------------------+
//| Determines whether trading is permitted for the given datetime based on the selected trade-day mode |                               
//+-----------------------------------------------------------------------------------------------------+
bool IsTradingDayAllowed(datetime time)
{
   // Baseline mode: no filtering
   if(tradeDayMode == TDW_ALL_DAYS){
      return true;
   }

   int day = TimeDayOfWeek(time);

   switch(day)
   {
      case 0: return tradeSunday;
      case 1: return tradeMonday;
      case 2: return tradeTuesday;
      case 3: return tradeWednesday;
      case 4: return tradeThursday;
      case 5: return tradeFriday;
      case 6: return tradeSaturday;
   }

   return false;
}

最初の関数は、datetime値から曜日を抽出します。2つ目の関数は、選択されたモードとユーザー設定に基づいて、その曜日に取引が許可されているかどうかを判定します。

時間帯フィルターを実装するために、以下を定義します。

//+------------------------------------------------------------------+
//| To parse time                                                    |                               
//+------------------------------------------------------------------+
bool ParseTime(double hhmm, int &hours, int &minutes){
    
    // Validate input range (0.00 to 23.59)
    if(hhmm < 0.0 || hhmm >= 24.00){
      return false;
    }
    
    hours = (int)hhmm;
    double fractional = hhmm - hours;
    
    // Handle floating-point precision by rounding
    minutes = (int)MathRound(fractional * 100);
    
    // Validate minutes (0-59)
    if(minutes < 0 || minutes > 59){
      return false;
    } 
        
    // Handle cases like 12.60 becoming 13:00
    if(minutes >= 60){
        hours += minutes / 60;
        minutes %= 60;
    }
    
    // Final validation (hours might have incremented)
    if(hours < 0 || hours > 23){
      return false;
    }
    
    return true;
}

//+------------------------------------------------------------------+
//| Returns true if current time is within allowed trading hours     |                               
//+------------------------------------------------------------------+
bool IsTimeWithinTradingHours(){

   datetime currentTm = currentTime;
   MqlDateTime currentTimeStruct;
   if(!TimeToStruct(currentTm, currentTimeStruct)){
      Print("Error while converting datetime to MqlDateTime struct: ", GetLastError());
      return false;
   }
   
   int startHour;
   int startMins;
   ParseTime(startTime, startHour, startMins);
   int endHour;
   int endMins;
   ParseTime(endTime, endHour, endMins);
   
   MqlDateTime startTimeStruct = currentTimeStruct;
   startTimeStruct.hour        = startHour;
   startTimeStruct.min         = startMins;
   startTimeStruct.sec         = 0;
   MqlDateTime endTimeStruct   = currentTimeStruct;
   endTimeStruct.hour          = endHour;
   endTimeStruct.min           = endMins;
   endTimeStruct.sec           = 0;
   
   datetime startTm          = StructToTime(startTimeStruct);
   datetime endTm            = StructToTime(endTimeStruct);    

   if(currentTm >= startTm && currentTm <= endTm){
      return true;
   }
      
   return false;
}

これらの関数は、入力された時間値を構造化されたdatetime値へ変換し、現在時刻が許可された時間帯ウィンドウ内に含まれているかどうかを判定します。

ポジションを常に1つに制限する仕組み

同時に1つのポジションのみを維持するために、以下を定義します。

//+------------------------------------------------------------------+
//| 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のマジックナンバーを持つポジションがすでに存在する場合にはtrueを返します。

ハードなテイクプロフィットを持たないポジションを適切に管理するために、利益確定条件が満たされた時点でそれらのポジションを決済する必要があります。そのために、以下のカスタム関数を定義します。

//+------------------------------------------------------------------+
//| To close all position with a specified magic number              |   
//+------------------------------------------------------------------+
void ClosePositionsByMagic(ulong magic) {
    
    for (int i = PositionsTotal() - 1; i >= 0; i--) {
        ulong ticket = PositionGetTicket(i);
        if (PositionSelectByTicket(ticket)) {
            if (PositionGetInteger(POSITION_MAGIC) == magic) {
                ulong positionType = PositionGetInteger(POSITION_TYPE);
                double volume = PositionGetDouble(POSITION_VOLUME);
                if (positionType == POSITION_TYPE_BUY) {
                    Trade.PositionClose(ticket);
                } else if (positionType == POSITION_TYPE_SELL) {
                    Trade.PositionClose(ticket);
                }
            }
        }
    }    
}

この関数は、取引口座上のすべての現在の保有ポジションを走査し、その中からこのEAによって開かれたポジションのみを決済します。ポジションを識別し、このEAのインスタンスに属するものだけを分離するために、マジックナンバーを使用します。

取引の開始とポジションサイズの計算

動的なポジションサイズ設定は、以下の方法で実装されます。

//+----------------------------------------------------------------------------------+
//| 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 stopLoss, double takeProfit, double lotSize){
   
   if(lotSizeMode == MODE_AUTO){
      lotSize = CalculatePositionSizeByRisk(lwVolatilityLevels.bullishStopDistance);
   }
   
   if(takeProfitMode == TP_BY_RISK_REWARD){
      if(!Trade.Buy(lotSize, _Symbol, askPrice, lwVolatilityLevels.bullishStopLoss, lwVolatilityLevels.bullishTakeProfit)){
         Print("Error while executing a market buy order: ", GetLastError());
         Print(Trade.ResultRetcode());
         Print(Trade.ResultComment());
         return false;
      }
      return true;
   }
   
   if(!Trade.Buy(lotSize, _Symbol, askPrice, lwVolatilityLevels.bullishStopLoss)){
      Print("Error while executing a market buy order: ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| Function to open a market sell position                          |
//+------------------------------------------------------------------+
bool OpenSel(double stopLoss, double takeProfit, double lotSize){
   
   if(lotSizeMode == MODE_AUTO){
      lotSize = CalculatePositionSizeByRisk(lwVolatilityLevels.bearishStopDistance);
   }
   
   if(takeProfitMode == TP_BY_RISK_REWARD){
      if(!Trade.Sell(lotSize, _Symbol, bidPrice, lwVolatilityLevels.bearishStopLoss, lwVolatilityLevels.bearishTakeProfit)){
         Print("Error while executing a market buy order: ", GetLastError());
         Print(Trade.ResultRetcode());
         Print(Trade.ResultComment());
         return false;
      }
      return true;
   }
   
   if(!Trade.Sell(lotSize, _Symbol, bidPrice, lwVolatilityLevels.bearishStopLoss)){
      Print("Error while executing a market buy order: ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }
   
   return true;
}
これらの関数は、固定ロットサイズとダイナミックロットサイズの両方をサポートしており、選択された利益モードに基づいてストップロスおよびテイクプロフィットのロジックを適用します。

エントリーシグナルの管理

すべてのエントリーロジックは、以下の関数に統合されています。

//+---------------------------------------------------------------------------------------------------------+
//| Evaluates swing-based entry signals, applies filters, and executes trades when conditions are satisfied |
//+---------------------------------------------------------------------------------------------------------+
void EvaluateAndExecuteEntrySignals(){

   bool timeAllowed = true;
   
   if(useTimeFilter){
      timeAllowed = IsTimeWithinTradingHours();
   }

   //--- Handle bullish entry signals
   if(IsLarryWilliamsShortTermLow(_Symbol, timeframe)){
      if(IsCrossOver(lwVolatilityLevels.buyEntryPrice,   closePriceMinutesData)){
         if(timeAllowed){
            if(tradeDayMode == TDW_SELECTED_DAYS){
               if(IsTradingDayAllowed(currentTime)){
                  if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
                     if(direction == TRADE_BOTH || direction == ONLY_LONG){
                        OpenBuy(lwVolatilityLevels.bullishStopLoss, lwVolatilityLevels.bullishTakeProfit, positionSize);
                        if(takeProfitMode  == TP_AFTER_N_CANDLES){
                           barsSinceEntry = 1;
                        }
                     }
                  }
               }
            }else{
               if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
                  if(direction == TRADE_BOTH || direction == ONLY_LONG){
                     OpenBuy(lwVolatilityLevels.bullishStopLoss, lwVolatilityLevels.bullishTakeProfit, positionSize);
                     if(takeProfitMode  == TP_AFTER_N_CANDLES){
                        barsSinceEntry = 1;
                     }
                  }
               }
            }
         }
      }
   }
   
   //--- Handle bearish entry signals
   if(IsLarryWilliamsShortTermHigh(_Symbol, timeframe)){
      if(IsCrossUnder(lwVolatilityLevels.sellEntryPrice, closePriceMinutesData)){
         if(timeAllowed){
            if(tradeDayMode == TDW_SELECTED_DAYS){
               if(IsTradingDayAllowed(currentTime)){
                  if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
                     if(direction == TRADE_BOTH || direction == ONLY_SHORT){
                        OpenSel(lwVolatilityLevels.bearishStopLoss, lwVolatilityLevels.bearishTakeProfit, positionSize);
                        if(takeProfitMode  == TP_AFTER_N_CANDLES){
                           barsSinceEntry = 1;
                        }
                     }
                  }
               }
            }else{
               if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
                  if(direction == TRADE_BOTH || direction == ONLY_SHORT){
                     OpenSel(lwVolatilityLevels.bearishStopLoss, lwVolatilityLevels.bearishTakeProfit, positionSize);
                     if(takeProfitMode  == TP_AFTER_N_CANDLES){
                        barsSinceEntry = 1;
                     }
                  }
               }
            }
         }
      }
   }
}

この関数は、構造的なスイングシグナルをチェックし、ボラティリティブレイクアウトを評価し、時間および曜日ベースのフィルターを適用し、トレード方向ルールを強制したうえで、すべての条件が満たされた場合にポジションを開きます。また、テイクプロフィットモードが時間ベースの決済を必要とする場合には、バーカウンタを初期化します。

エグジットロジックの管理

ハードなテイクプロフィットを持たずに開かれたポジションは、以下の関数によって管理されます。

//+-------------------------------------------------------------------------------------------+
//| Manages exit logic for the currently open position based on the selected take-profit mode |
//+-------------------------------------------------------------------------------------------+
void ManageOpenPositionExits(){

   if(takeProfitMode == TP_FIRST_PROFITABLE_OPEN){
      for(int i = PositionsTotal() - 1; i >= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if(ticket == 0){
            Print("Error while fetching position ticket ", GetLastError());
            continue;
         }else{
            if(PositionGetDouble(POSITION_PROFIT) > 0 ){
               ClosePositionsByMagic(magicNumber);
            }
         }
      }
   }
   
   if(takeProfitMode == TP_AFTER_N_CANDLES){
      if(barsSinceEntry > exitAfterCandles){
         ClosePositionsByMagic(magicNumber);
         barsSinceEntry = 0;
      }
   }   
}

この関数は、ポジションを「最初の含み益が出たタイミング」で決済するか、または指定されたバー数が経過した後に決済します。

時間に基づいたエグジットをサポートするために、グローバルスコープで以下を定義します。

//--- Tracks the number of completed bars elapsed since the current trade was opened
int barsSinceEntry;

この変数は、ポジションが建てられてから経過した確定足の数を記録します。

全体の統合

最後に、OnTick関数を完成させます。

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

   //--- Retrieve current market prices for trade execution
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID);
   currentTime   = TimeCurrent();
   
   //--- Get some minutes data
   if(CopyClose(_Symbol, PERIOD_M1, 0, 5, closePriceMinutesData) == -1){
      Print("Error while copying minutes datas ", GetLastError());
      return;
   }
   
   //--- Run this block only when a new bar is detected on the selected timeframe
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
      
      //--- Recalculate volatility entry levels on new bar based on the selected entry and stop models
      UpdateVolatilityEntryLevels();     
      
      //--- Increment the number of completed bars since the position was opened
      if(barsSinceEntry > 0){
         barsSinceEntry = barsSinceEntry + 1;
      }
      
      //--- Handle exit conditions for the currently active position based on the configured take-profit mode
      if(takeProfitMode == TP_FIRST_PROFITABLE_OPEN){
         ManageOpenPositionExits();
      }
      
   }
   
   //--- Check for valid entry signals and place trades if all rules are met
   EvaluateAndExecuteEntrySignals();
   
   //--- Handle exit conditions for the currently active position based on the configured take-profit mode
   if(takeProfitMode == TP_AFTER_N_CANDLES){
      ManageOpenPositionExits();
   }
}

これは、ライブ価格を取得し、1分足データを更新し、新しいバーが形成された際にボラティリティレベルを再計算し、エントリーシグナルを評価し、バーカウンターを更新し、エグジットロジックを適用します。この時点で、EAのすべてのコンポーネントは単一の論理システムとして統合されて動作します。

完成したソースコードは、lwVolatilityStructureTimeFilterExpert.mq5というファイルにまとめて添付されています。ここでは完全なコードを貼り付けるのではなく、このビルドの最終成果物として参照します。

これでEA開発は終了となります。すべての関数は明確な役割を持ち、それぞれのロジックブロックはラリー・ウィリアムズの手法の特定の側面を支えています。構造がシグナルを定義し、ボラティリティがエントリーを決定し、フィルターが執行の質を高め、リスク管理が資金を保護し、エグジットロジックが規律を強制します。

テストに移る前に、チャート環境がプライスアクションと取引挙動を明確に表示できるようにする必要があります。クリーンで一貫したチャートレイアウトは、ストラテジーテスト中のエントリー、エグジット、および全体の挙動を視覚的に確認するうえで非常に重要です。このEAは研究および分析用途を想定しているため、チャートの可読性を改善することは小さくても重要な工程です。

これを実現するために、EA初期化時にチャート外観を設定するカスタムユーティリティ関数を定義します。この関数は、ローソク足、背景、価格変動がテストおよびリプレイ中に明確に識別できるよう、一連の視覚設定を適用します。以下にその関数定義を示します。このコードはカスタムMQL5関数セクションに配置してください。

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

この関数は、一連のChartSetIntegerを呼び出すことで動作します。各呼び出しで、現在アクティブなチャートの特定の視覚プロパティを変更します。

まず、チャートの背景色を白に設定します。明るい背景はコントラストを高め、ローソク足の色をより明確に際立たせます。次に、チャートのグリッドを無効化します。グリッドを削除することで視覚的なノイズを減らし、補助線ではなく価格アクションそのものに集中できるようになります。その後、チャート表示モードはローソク足表示に設定します。ローソク足はラインチャートよりも多くの情報を提供し、ボラティリティ、構造、イントラデイの挙動を分析するのに適しています。

続いて、前景色を黒に設定します。これにより、白い背景上でもチャートのテキストや価格スケールが明確に視認できるようになります。その後、関数は強気および弱気のローソク足に対する色を定義します。強気のローソク足は上昇を視覚的に強調するためにシーグリーンで表示され、弱気のローソク足は可読性を保つために黒で表示されます。同じ色のロジックは、上昇および下降を示すバーのアウトラインにも適用され、ローソク足の実体と枠線の視覚的一貫性が維持されます。

ChartSetIntegerの呼び出しはそれぞれ成功判定され、いずれかの設定ステップが失敗した場合はエラーメッセージがログに記録され、関数は即座にfalseを返します。これにより、EAは誤ったチャート設定のまま動作することを防ぎ、問題を早期に検出できます。すべての設定が正常に適用された場合のみtrueを返し、チャートがテストおよび分析に適した状態であることを示します。

関数を定義した後は、EA初期化関数内から呼び出します。これにより、EAの起動時に一度だけチャートの外観設定が適用されることが保証されます。

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

OnInit関数内では、チャート設定ルーチンを呼び出し、その結果を検証します。設定に失敗した場合、EAは初期化を中断し、問題を報告します。成功した場合は、そのまま通常の初期化処理を継続します。

このアプローチにより、すべてのテストは常にクリーンで視認性の高いチャートレイアウトから開始されます。その結果、取引挙動の評価、過去パフォーマンスの検証、そして戦略ロジックがテスト環境上で意図通りに機能しているかの視覚的確認が容易になります。


ゴールドに対する戦略のバックテスト

コアロジックが完成したため、次のステップとして実際の市場環境におけるモデルの挙動を検証します。テストの再現性を確保し、条件を明確にするため、単一の市場と固定された期間を使用します。対象銘柄はゴールド(XAUUSD)、時間足は日足です。テスト期間は2025年1月1日から2025年12月30日までであり、執筆時点における1年間の市場データを対象としています。

このテストでは買いのみを有効化し、取引方向をONLY_LONGに設定しています。これにより、現段階ではショートロジックを導入せず、ボラティリティブレイクアウト戦略の上昇側の挙動に集中できます。ポジションサイズは自動モードに設定され、各トレードごとに口座残高の2%を固定リスクとしています。ストップロスは基準レンジの割合として設定されており、固定距離ではなく市場ボラティリティに連動する形でリスクが調整されます。

結果の完全な再現性を確保するために、本記事には2つの補助ファイルが添付されています。1つ目のconfigurations.iniはストラテジーテスターの環境設定を保存したものです。2つ目のparameters.setは今回のテストで使用されたすべての入力パラメータを保持しています。これらを使用することで、同一条件を最小限の手間で再現できます。

バックテストは初期資金10,000ドルから開始されました。テスト期間終了時点で、システムは合計純利益3,710.55ドルを達成しました。

テスト結果

これは年間ベースで約35%強のリターンに相当します。勝率は54.55%でした。決して高くはない数値ですが、頻繁な勝率ではなく、リスクリワードの非対称性によって利益を得るボラティリティブレイクアウト戦略の特性と整合しています。

今回の結果で特に注目すべき点はエクイティカーブの形状です。添付されたスクリーンショットでは、急激なドローダウンや崩壊的な損失がなく、滑らかな資産成長が確認できます。

エクイティカーブ

これは、ボラティリティベースのエントリー、構造確認、そして規律あるリスクサイジングの組み合わせが安定して機能していることを示しています。高い勝率に依存せずに収益性を維持できており、エクイティの挙動は攻撃的な複利ではなく、制御されたエクスポージャー構造を反映しています。

これらの結果は戦略の最終的な評価ではなく、単一の設定、単一の市場、単一の時間枠における結果に過ぎません。このEAはパラメータを柔軟に変更できる設計になっており、さらなる検証を前提としています。ボラティリティ倍率、ストップロスモデル、テイクプロフィットモード、時間フィルター、曜日フィルターなどを変更することで、各要素がパフォーマンスに与える影響を検証できます。

現時点で最も重要なのは独立した再テストです。異なる銘柄、時間足、市場環境で同じモデルを動かすことで、観測された挙動が構造的なものなのか、それとも特定市場依存なのかを判断できます。わずかなパラメータ変更によって、パフォーマンス改善や安定性のトレードオフが見つかる可能性もあります。有意な結果や興味深い変化をもたらす発見、観察、バリエーションについては、本記事のコメント欄で共有する価値があります。そうすることで、この研究を単一のテストケースにとどめることなく、より広い分析へと発展させることができます。


結論

本記事では、理論から実装へと踏み込んだ実践的な一歩を示しました。出発点として、ラリー・ウィリアムズの中心的な考え方である「ボラティリティは機会を生み出し、構造は方向性を与え、時間ベースのフィルターは選択性を高める」という概念を取り上げました。これらを独立したテクニックとして扱うのではなく、単一の検証可能な取引モデルへと統合しました。そして、そのモデルを実際に動作するEAへと変換する一連のプロセスを記録しました。

ここで達成したのは、単に取引をおこなうEAではなく、柔軟な研究基盤です。本システムは2種類のボラティリティモデルを用いてエントリーレベルを設定でき、リスクを市場構造またはボラティリティ由来の距離のいずれかに基づいて定義できます。また、時間ベース、利益ベース、リスクリワード比ベースのいずれの条件でもエグジット可能であり、曜日や時間帯によるフィルタリングも備えています。これらすべての要素は入力パラメータとして提供されており、コードを変更せずにアイデアを検証できる設計になっています。

ゴールドでのバックテストは、このフレームワークの単純な構成であっても、安定した成長と制御されたドローダウンを実現できることを示しました。勝率は控えめでしたが、リスクが固定距離ではなく市場の振る舞いに整合していたため、十分なリターンが生まれました。さらに重要なのは、エクイティカーブが少数の極端な勝ちトレードに依存していない点です。構造と時間によってフィルタリングされたボラティリティ拡張への一貫した参加が、その成長を支えています。

本質的に、本記事はさらなる研究のための基盤を提供するものです。EAは完成されたシステムとして提示されているのではなく、実験として提示されています。その設計は、再現、修正、拡張を前提としています。同じフレームワークは他の市場、他の時間足、そして異なるパラメータ条件にも適用でき、その根底にあるロジックがどこで機能し、どこで破綻するのかを検証することができます。

この研究の真の価値は単一のバックテスト結果にあるのではありません。それは「方法論」にあります。私たちは、ボラティリティ、構造、時間が実市場でどのように相互作用するのかを体系的に研究するための枠組みを手にしました。それは短期的な最適化ではなく、長期的な学習を支えるためのツールです。

以下の表には、本記事に添付されたすべての補助ファイルと、それぞれの目的の簡潔な説明を示します。これらのファイルは、結果の再現性を確保し、実装内容を正確に追跡できるように提供されております。
ファイル名 説明
lwVolatilityStructureTimeFilterExpert.mq5
ラリー・ウィリアムズのボラティリティブレイクアウトモデル、スイング構造検出、時間およびTDWフィルター、取引およびリスク管理ルールを含む完全なEAのメインソースファイルで
configurations.ini
バックテストに使用したストラテジーテスターの環境設定
parameters.set
本記事で提示されたバックテスト結果を再現するための固定入力パラメータを含むMT5設定ファイル

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

MQL5取引ツール(第12回):相関行列ダッシュボードのインタラクティブ機能の強化 MQL5取引ツール(第12回):相関行列ダッシュボードのインタラクティブ機能の強化
MQL5における相関行列ダッシュボードを強化し、パネルのドラッグ操作、最小化と最大化、ボタンや時間足に対するホバー効果、マウスイベント処理などを追加することで、ユーザー体験の向上を図ります。さらに、相関の強さに基づく銘柄の並び替え(昇順、降順)、相関値表示とp値表示の切り替え、ライトテーマとダークテーマの切り替え、動的なカラー更新も実装します。
プライスアクション分析ツールキットの開発(第56回):CPIを用いたセッションの受容と拒否の解読 プライスアクション分析ツールキットの開発(第56回):CPIを用いたセッションの受容と拒否の解読
時間で区切られた市場セッションとCandle Pressure Index (CPI)を組み合わせ、確定足データと明確に定義されたルールに基づき、セッション境界での受容と拒否の挙動を分類するセッションに基づいた分析手法を提示します。
プライスアクション分析ツールキットの開発(第55回):CPIミニローソク足オーバーレイによるバー内圧力の可視化 プライスアクション分析ツールキットの開発(第55回):CPIミニローソク足オーバーレイによるバー内圧力の可視化
価格チャート上にバー内の買い圧力と売り圧力を可視化するCLVベースのオーバーレイであるCandle Pressure Index(CPI、ローソク足圧力指数)の設計とMetaTrader 5への実装について解説します。本記事では、ローソク足の構造、圧力分類および可視化の仕組み、そして時間足や銘柄に依存せず一貫した動作を維持する、リペイントなしの遷移ベースアラートシステムに焦点を当てます。
共和分株式による統計的裁定取引(第10回):構造変化の検出 共和分株式による統計的裁定取引(第10回):構造変化の検出
本記事では、ペア関係における構造変化を検出するためのChow検定と、構造変化の監視および早期検出のための累積平方和(CUSUM)の適用について解説します。例として、NvidiaとIntelの提携発表および米国政府による対外貿易関税の発表を取り上げ、それぞれ「傾きの反転」と「切片のシフト」の事例として説明します。すべてのPythonテストスクリプトも提供します。