English Русский Deutsch
preview
ラリー・ウィリアムズの『市場の秘密』(第12回):相場環境に基づくスマッシュデー反転トレード

ラリー・ウィリアムズの『市場の秘密』(第12回):相場環境に基づくスマッシュデー反転トレード

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

はじめに

スマッシュデーパターンは魅力的なトレードシグナルですが、明確な構造なしに取引すると、ノイズの多いシグナルや不必要な損失につながる可能性があります。このパターンは頻繁に出現するため、セットアップの有効性や市場環境を判断する明確なルールがなければ、有意義なリバーサルと単なる偶然の値動きを区別することは困難です。ラリー・ウィリアムズは、スマッシュデーパターンを単独で利用するのではなく、より包括的な判断プロセスの一部として活用すべきであると繰り返し強調していました。

本記事では、この問題に対応するため、スマッシュデー反転を客観的でルールベースのシステムへ変換し、自動化および検証可能な形にします。視覚的な判断に依存するのではなく、すべての判断基準を測定可能で一貫したロジックとして定義し、さまざまな市場や時間足で評価できるようにします。

本記事は、スマッシュデーパターンを自動化し、検証し、客観的に研究したいアルゴリズムトレーダーやMQL5開発者を対象としています。また、ラリー・ウィリアムズの考え方を、裁量判断を排除した明確な実行ルールへ変換する方法を学びたいトレーダーにも適しています。

本記事では、オプションのコンテキストフィルタを備えたスマッシュデー反転EAを構築します。Supertrendインジケーターによるトレンド方向の制御、曜日による取引制限、そして各セットアップに対する固定された有効期間を実装します。これらの要素は、ノイズを減少させ、不必要なトレードを制限し、バックテスト結果をより正確に分析できるように設計されています。この記事を読み終える頃には、Hidden Smash Dayパターンは単独のシグナルではなく、検証・変更・拡張が可能な体系的なトレードフレームワークの一部として扱えるようになります。


Smash Day EAの戦略ルール

このEAは、ラリー・ウィリアムズのスマッシュデー反転概念を、厳密で検証可能なルールとして実装しています。目的は、目に付くすべてのパターンを取引することではありません。曖昧さを排除し、異なる市場や時間足でも一貫して評価できる条件を定義することです。

スマッシュデーパターンはシグナルのトリガーとして機能します。その他のルールは、タイミング、相場環境、リスク管理を制御するために存在し、結果がランダムな売買ではなく、体系的な意思決定を反映するようにします。

スマッシュデーパターンの定義

このシステムでは、スマッシュデー買い反転とスマッシュデー売り反転の2種類のパターンをサポートしています。スマッシュデー買いセットアップは、指定した過去バー本数の安値を下回る価格でバーが終値を形成した場合に検出されます。このルックバック期間はユーザーが変更可能であり、異なる下落ブレイクの強さがリバーサルに与える影響を検証できます。ただし、そのバーはアウトサイドバーであってはいけません。アウトサイドバーは上下両方向へ大きく動く可能性があり、方向性の意図よりもノイズを表している場合があるためです。

スマッシュデー売りセットアップは逆の条件で成立します。指定した過去バー本数の高値を上回って終値を形成し、かつアウトサイドバーではないことが条件となります。検出されたスマッシュデーバーは、即時の売買シグナルではなく、将来的なリバーサル候補として扱われます。

セットアップの有効期間とタイミング制御

スマッシュデーセットアップは永久に有効とは考えません。検出された各セットアップには、ユーザーが指定する以降のバー数による限定的な有効期間が設定されます。この期間内にエントリー条件が成立しなかった場合、そのセットアップは自動的に破棄されます。これは、反転圧力が時間の経過とともに弱まり、遅れたエントリーでは優位性を失いやすいという市場の性質を反映しています。また、このルールにより、基本パターン自体を変更することなく、タイミングの重要性を客観的に検証できます。
エントリー条件と確認方法

トレードは、スマッシュデーレベルを価格が期待方向へ突破した場合のみ検討されます。買い反転の場合、価格はスマッシュデーバーの高値を上抜ける必要があります。売り反転の場合、価格はスマッシュデーバーの安値を下抜ける必要があります。エントリー方式は2種類用意されています。1つ目は、価格がブレイクアウトレベルを突破した瞬間にエントリーする方式です。2つ目は、レベルを超えてバーが確定するのを待ちます。これにより、より早いエントリーと、確定を待つエントリーの比較検証が可能になります。なお、スマッシュデーパターン自体の定義は変更しません。

方向性制御

このシステムは、買い注文のみ、売り注文のみ、またはその両方でトレードするように設定できます。選択したモードに関係なく、EAは同時保有ポジションを1つだけに制限します。スマッシュデー反転は明確なイベントとして発生するため、複数ポジションを許可すると、各シグナルの結果や有効性の分析が不明確になります。この設計により、バックテスト時の因果関係を明確に保ち、結果をより正確に評価できます。

コンテキストフィルタ

市場環境を考慮するためのフィルタは、オプションとして明示的に利用できます。Supertrendフィルタを有効化すると、取引方向を主要なトレンド方向に合わせることができます。有効な場合、買い注文はSupertrendが上昇方向の場合のみ許可され、売り注文はSupertrendが下降方向の場合のみ許可されます。Supertrendの時間足、ATR期間、ATR倍率はすべて入力パラメータで変更可能です。

また、曜日フィルタも利用できます。選択した曜日のみ取引を実行できるため、特定曜日や市場セッションによる傾向を分析できます。両方のフィルタは、それぞれ独立して有効・無効を切り替え可能です。

リスク管理とトレード管理

リスク管理はスマッシュデーパターンの構造に基づいています。買い反転の場合、ストップロスはスマッシュデーバーの安値に設定されます。売り反転の場合、ストップロスはスマッシュデーバーの高値に設定されます。テイクプロフィットは、ユーザーが指定する固定リスクリワード比率に基づいて計算されます。これにより、市場ごとの違いに影響されにくい、客観的で拡張可能な決済ルールになります。

ポジションサイズは、手動設定または口座残高に対するリスク割合による自動計算を選択できます。自動モードでは、ストップロスまでの距離を基準としてロットサイズを算出し、ボラティリティに関係なく一定のリスク量を維持します。


EAの新機能

今回のSmash Day EAでは、従来の実装を発展させ、タイミング管理、コンテキストフィルタリング、そして再現性に重点を置いています。最も大きな追加機能は、セットアップの有効期間を管理する仕組みです。スマッシュデー反転は、もはや即座にチャンスとはみなされません。各セットアップは、ユーザーが指定した一定本数のバーの間だけ有効となり、その期間内にエントリー条件が満たされなかった場合は自動的に無効化されます。これにより、タイミングの遅れたエントリーを防ぐだけでなく、モメンタムの減衰が戦略に与える影響を体系的に検証できるようになります。

コンテキストフィルタリングについても、オプションのSupertrendフィルタによって機能が拡張されています。取引を市場の主要なトレンド方向に限定できるようになり、Supertrendの時間足や計算パラメータも自由に設定できます。その結果、「相場環境(コンテキスト)」という概念を裁量判断ではなく、明確なルールとして定義し、客観的に検証できるようになりました。

時間に基づくフィルタリングとしては、従来どおり曜日フィルタも利用できます。戦略の基本ロジックを変更することなく、取引を特定の曜日だけに制限できるため、市場セッションや曜日ごとの特性を集中的に検証できます。

これまでのバージョンで採用していたエントリー方式も維持されています。価格がブレイクアウトレベルを突破した時点でエントリーする方法と、ローソク足が確定してからエントリーする方法の両方を選択できます。また、取引方向は「買いのみ」「売りのみ」「買い・売り両方」から選択でき、リスク管理では固定ロットと口座残高に対するリスク率に基づく自動ロット計算の両方をサポートしています。

これらの追加機能により、本EAは単にスマッシュデーパターンを売買するツールではなく、戦略を客観的かつ体系的に検証するためのツールへと発展しました。スマッシュデーシグナルは引き続きトレードの起点となりますが、実際に取引するかどうか、そしてどのように実行するかは、タイミング、市場環境、そしてリスク管理の各ルールによって決定されます。


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

このセクションでは、実際にEAの開発を始めます。ここでの目的は、すぐに売買ロジックを実装することではありません。後続のすべての機能を支える、整理された信頼性の高い基盤を構築することです。この段階で追加するコードは、一見すると目的が分かりにくいものもありますが、それぞれが後の実装に必要な役割を持っています。

先に進む前に、いくつかの前提条件を明確にしておきましょう。本記事では、読者がMQL5言語とその基本的な構文を理解していることを前提としています。変数、関数、列挙型、ループ処理、および標準ライブラリといった基本的な概念については、すでに習熟しているものとします。また、MetaTrader 5ターミナルの基本的な操作にも慣れていることを想定しています。具体的には、EAをチャートへ適用する方法やストラテジーテスターを用いたバックテストの実行方法を理解していることを前提としています。さらに、ソースコードの作成、コンパイル、および必要に応じたデバッグを行うために、MetaEditorの基本的な使用方法についても理解している必要があります。

実際に手を動かしながら学習できるよう、本記事には完成版EAのソースコード lwSmashDayTrendFilteredExpert.mq5を添付しています。チュートリアルに沿ってコーディングを進めながら完成版と比較することで、それぞれのコードがシステム全体の中でどのような役割を果たしているのかを理解しやすくなります。

それでは、MetaEditorで新しい空のEAファイルを作成し、最初のソースコードを貼り付けるところから始めましょう。

//+------------------------------------------------------------------+
//|                                lwSmashDayTrendFilteredExpert.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"
#resource "\\Indicators\\supertrend.ex5"

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

//+------------------------------------------------------------------+
//| Custom Enumerations                                              |
//+------------------------------------------------------------------+
enum ENUM_TDW_MODE
{
   TDW_ALL_DAYS,     
   TDW_SELECTED_DAYS
};

enum ENUM_SMASH_ENTRY_MODE{
   ENTRY_ON_LEVEL_CROSS,
   ENTRY_ON_BAR_CLOSE
};

enum ENUM_SMASH_TRADE_MODE
{
   SMASH_TRADE_BUY_ONLY,
   SMASH_TRADE_SELL_ONLY,
   SMASH_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 "Smash Day Pattern Rules"
input int smashBuyLookbackBars   = 1;
input int smashSellLookbackBars  = 1;
input int smashSetupValidityBars = 3;

input group "Smash Day Entry Settings"
input ENUM_SMASH_ENTRY_MODE smashEntryMode = ENTRY_ON_LEVEL_CROSS;
input ENUM_SMASH_TRADE_MODE smashTradeMode = SMASH_TRADE_BOTH;

input group "Supertrend configuration parameters"
input bool useSupertrendFilter            = false;
input ENUM_TIMEFRAMES supertrendTimeframe = PERIOD_CURRENT;
input int32_t supertrendAtrPeriod         = 10;
input double  supertrendAtrMultiplier     = 1.5;

input group "TDW filters"
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 "Trade and Risk Management"
input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode  = MODE_AUTO;
input double riskPerTradePercent            = 1.0;
input double positionSize                   = 0.1;
input double riskRewardRatio                = 3.0;

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

//--- To hep track current market prices for Buying (Ask) and Selling (Bid)
double askPrice;
double bidPrice;

//--- To store current time
datetime currentTime;

//--- Supertrend values 
int    supertrendIndicatorHandle;
double upperBandValues[];
double lowerBandValues[];

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

struct MqlSmashDayPatternState{

   //--- Pattern detection flags
   bool hasBuySmashSetup;
   bool hasSellSmashSetup;

   //--- Reference breakout levels from smash bar
   double buyBreakoutLevel;
   double sellBreakoutLevel;

   //--- Pattern bar reference data
   datetime smashBarTime;

   //--- Entry status tracking
   bool entryPending;
   
   //--- Tracks the number of bars elapsed after the occurence of the smash bar
   int barsSinceSmash;
   
   //--- Stop-Loss levels
   double buyStopLoss;
   double sellStopLoss;
   
};

MqlSmashDayPatternState smashState;

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

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

   //---  Assign a unique magic number to identify trades opened by this EA
   Trade.SetExpertMagicNumber(magicNumber);
   
   // Initialize the Supertrend Indicator
   supertrendIndicatorHandle = iCustom(_Symbol, supertrendTimeframe, "::Indicators\\supertrend.ex5", supertrendAtrPeriod, supertrendAtrMultiplier);
   if(supertrendIndicatorHandle == INVALID_HANDLE){
      Print("Error while initializing the Supertrend indicator", GetLastError());
      return(INIT_FAILED);
   }
   
   //--- Treat the following arrays as timeseries (index 0 becomes the most recent bar)
   ArraySetAsSeries(closePriceMinutesData, true);
   ArraySetAsSeries(upperBandValues, true);
   ArraySetAsSeries(lowerBandValues, true);
   
   //--- Initialize global variables
   lastBarOpenTime = 0;
   
   //--- Reset
   ZeroMemory(smashState);

   return(INIT_SUCCEEDED);
}

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

   //--- Release Supertrend
   if(supertrendIndicatorHandle != INVALID_HANDLE){
      IndicatorRelease(supertrendIndicatorHandle);
   }

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

//--- UTILITY FUNCTIONS

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

このコードは、システムの構造的な基盤を形成します。

ファイルヘッダーとプログラム識別子

//+------------------------------------------------------------------+
//|                                lwSmashDayTrendFilteredExpert.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"
#resource "\\Indicators\\supertrend.ex5"

冒頭のセクションでは、プログラム名、作者情報、およびバージョン情報を定義します。これはMQL5では一般的な記述であり、特にEAを複数回にわたって改良・更新していく場合に、長期的な保守性を高めるうえで役立ちます。

また、このセクションではSupertrendインジケータをリソースとしてEAに組み込みます。これにより、EAは外部からインジケーターをチャートへ追加することなく、内部から常にSupertrendへアクセスできるようになります。本バージョンでは、トレンドフィルタリングが戦略の中核機能となっているため、この段階でインジケータをリソースとして組み込んでおくことが重要です。

標準ライブラリとTradeへのアクセス

次に、Tradeライブラリを含めます。

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

これにより、CTradeクラスを利用できるようになります。CTradeクラスは、注文の発注、ポジション管理、エラーハンドリングといった取引処理を簡潔に実装できるようにするための標準クラスです。このライブラリを使用することで、売買ロジックを読みやすく保ちながら、システム全体を通して一貫した方法で取引処理を実装できます。

この段階では、まだ実際に注文を発行するわけではありません。ここでおこなっているのは、後の実装で安全かつ制御されたトレードを実行するために必要な基盤を準備しているだけです。

設定を分かりやすくするための列挙型

//+------------------------------------------------------------------+
//| Custom Enumerations                                              |
//+------------------------------------------------------------------+
enum ENUM_TDW_MODE
{
   TDW_ALL_DAYS,     
   TDW_SELECTED_DAYS
};

enum ENUM_SMASH_ENTRY_MODE{
   ENTRY_ON_LEVEL_CROSS,
   ENTRY_ON_BAR_CLOSE
};

enum ENUM_SMASH_TRADE_MODE
{
   SMASH_TRADE_BUY_ONLY,
   SMASH_TRADE_SELL_ONLY,
   SMASH_TRADE_BOTH
};

enum ENUM_LOT_SIZE_INPUT_MODE 
{ 
   MODE_MANUAL, 
   MODE_AUTO 
};

ソースファイルの前半では、複数のカスタム列挙型を定義します。これらの列挙型は、さまざまな設定に応じてEAの動作を切り替えるために使用されます。TDWモードは、すべての曜日で取引を許可するか、あるいはユーザーが選択した曜日のみで取引をおこなうかを制御します。Smash Entryモードは、価格がブレイクアウトレベルを突破した時点で即座にエントリーするか、それともローソク足の終値によるブレイク確認を待ってからエントリーするかを決定します。Smash Tradeモードは、取引方向を制御するための設定です。「買いのみ」「売りのみ」、または「買い・売り両方」のいずれかを選択できます。Lot Sizeモードは、ポジションサイズを固定ロットで指定するか、それとも口座残高に対するリスク率に基づいて自動計算するかを決定します。このように、数値を直接使用するのではなく列挙型を利用することで、コードの可読性が向上するだけでなく、設定ミスや誤った値の指定を防ぐことができます。

ユーザー入力パラメータ

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

input group "Smash Day Pattern Rules"
input int smashBuyLookbackBars   = 1;
input int smashSellLookbackBars  = 1;
input int smashSetupValidityBars = 3;

input group "Smash Day Entry Settings"
input ENUM_SMASH_ENTRY_MODE smashEntryMode = ENTRY_ON_LEVEL_CROSS;
input ENUM_SMASH_TRADE_MODE smashTradeMode = SMASH_TRADE_BOTH;

input group "Supertrend configuration parameters"
input bool useSupertrendFilter            = false;
input ENUM_TIMEFRAMES supertrendTimeframe = PERIOD_CURRENT;
input int32_t supertrendAtrPeriod         = 10;
input double  supertrendAtrMultiplier     = 1.5;

input group "TDW filters"
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 "Trade and Risk Management"
input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode  = MODE_AUTO;
input double riskPerTradePercent            = 1.0;
input double positionSize                   = 0.1;
input double riskRewardRatio                = 3.0;

入力パラメータのセクションでは、EAのすべての設定項目を、ユーザーが分かりやすく変更できるよう整理された形で定義します。基本設定では、マジックナンバーと動作時間足を指定します。スマッシュデーパターンに関する設定では、反転パターンの検出条件や、検出後にセットアップを有効とみなす期間を指定します。エントリー設定では、どのような条件で、いつトレードを実行するかを定義します。Supertrend関連の設定では、トレンドフィルタを使用するかどうかに加え、トレンド計算に使用する時間足や各種パラメータを指定します。TDWの設定では、取引を許可する曜日を選択できます。リスク管理の設定では、ポジションサイズの計算方法や、目標とするリスクリワード比率を定義します。この段階ではまだ何も実行されません。これらの入力パラメータは、EAが市場データに反応し始める前に、戦略全体のルールを定義する役割を果たします。

グローバル変数と共有状態

続いて、価格情報、時刻、インジケータの値、およびEA内部で使用する状態を保持するためのグローバル変数を宣言します。

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

//--- To hep track current market prices for Buying (Ask) and Selling (Bid)
double askPrice;
double bidPrice;

//--- To store current time
datetime currentTime;

//--- Supertrend values 
int    supertrendIndicatorHandle;
double upperBandValues[];
double lowerBandValues[];

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

struct MqlSmashDayPatternState{

   //--- Pattern detection flags
   bool hasBuySmashSetup;
   bool hasSellSmashSetup;

   //--- Reference breakout levels from smash bar
   double buyBreakoutLevel;
   double sellBreakoutLevel;

   //--- Pattern bar reference data
   datetime smashBarTime;

   //--- Entry status tracking
   bool entryPending;
   
   //--- Tracks the number of bars elapsed after the occurence of the smash bar
   int barsSinceSmash;
   
   //--- Stop-Loss levels
   double buyStopLoss;
   double sellStopLoss;
   
};

MqlSmashDayPatternState smashState;

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

CTradeオブジェクトは一度だけ生成され、その後のすべての売買処理で再利用されます。また、注文を正確に執行するために、Ask価格とBid価格を保持する変数も用意します。さらに、Supertrendインジケータから取得したトレンド情報を格納するためのバッファを準備します。

続いて、スマッシュデーセットアップの状態を管理するために、独自の構造体を定義します。この構造体には、検出されたセットアップに関する情報、ブレイクアウトレベル、ストップロスレベル、そしてスマッシュデーバーが形成されてから経過したバー数などを保存します。このように関連する情報を1つの構造体にまとめることで、ロジックを整理しやすくなり、複数のグローバルフラグを個別に管理する必要がなくなります。

また、1分足の終値を保存するための小さな配列も用意します。この配列は後の処理で、価格が指定したレベルを正確に通過したかどうかを判定するために使用します。

EAの初期化処理

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

   //---  Assign a unique magic number to identify trades opened by this EA
   Trade.SetExpertMagicNumber(magicNumber);
   
   // Initialize the Supertrend Indicator
   supertrendIndicatorHandle = iCustom(_Symbol, supertrendTimeframe, "::Indicators\\supertrend.ex5", supertrendAtrPeriod, supertrendAtrMultiplier);
   if(supertrendIndicatorHandle == INVALID_HANDLE){
      Print("Error while initializing the Supertrend indicator", GetLastError());
      return(INIT_FAILED);
   }
   
   //--- Treat the following arrays as timeseries (index 0 becomes the most recent bar)
   ArraySetAsSeries(closePriceMinutesData, true);
   ArraySetAsSeries(upperBandValues, true);
   ArraySetAsSeries(lowerBandValues, true);
   
   //--- Reset
   ZeroMemory(smashState);

   return(INIT_SUCCEEDED);
}

初期化関数では、市場データを受信する前にEAを準備します。まず、マジックナンバーをCTradeオブジェクトへ設定します。これにより、このEAが発注したポジションを他のEAや手動取引と確実に区別できるようになります。次に、iCustom関数を使用してSupertrendインジケータを初期化し、その生成結果を直ちに確認します。インジケータを正常に読み込めなかった場合、EAはトレンド情報なしで取引を実行することを避けるため、初期化を中止して終了します。その後、各配列を時系列として設定します。これにより、インデックス0が常に最新のデータを表すようになり、市場データを扱う処理をより直感的に実装できます。最後に、スマッシュデーセットアップを管理する構造体を初期状態へリセットし、EAが常にクリーンな状態から動作を開始できるようにします。

この一連の初期化処理が完了すると、EAはすべての準備を終えた状態になります。ただし、この時点ではまだ取引判断は下しません。実際の売買ロジックは、市場からティックデータを受信し始めてから実行されます。

初期化解除とリソースの解放

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

   //--- Release Supertrend
   if(supertrendIndicatorHandle != INVALID_HANDLE){
      IndicatorRelease(supertrendIndicatorHandle);
   }
to identify positions opened by this EA reliably
   //--- Notify why the program stopped running
   Print("Program terminated! Reason code: ", reason);
}

EAの動作が終了すると、初期化解除関数が呼び出されます。この関数では、Supertrendインジケータのハンドルを解放し、EAが終了した理由をログへ記録します。これにより、不要なメモリ使用を防ぐとともに、使用していたリソースをMetaTrader 5ターミナルへ適切に返却できます。

ティック処理の基本構造

//+------------------------------------------------------------------+
//| 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, 7, closePriceMinutesData) == -1){
      Print("Error while copying minutes datas ", GetLastError());
      return;
   }   
}

ボイラープレートの最後に実装するのが、ティック処理関数です。この段階では、現在のAsk価格とBid価格、および直近の1分足データを取得するだけで、まだ売買ロジックは実装しません。このように処理を分離しているのは意図的です。最初のOnTick関数をできるだけシンプルに保つことで、後から機能を段階的に追加してもコード全体の流れが分かりやすくなり、複雑さを抑えながら開発を進められます。

ここまでで、EAの基本的な構造は完成しました。設定項目、内部状態の管理、インジケーターへのアクセス、そして市場データを取得するための準備はすべて整っています。次のステップでは、この基盤の上に、スマッシュデーパターンの検出、セットアップの有効期間管理、Supertrendによるトレンドフィルタリング、そして制御された売買実行ロジックを順番に追加していきます。

以降で実装する各機能は、ここまで構築してきた基盤と直接結び付きながら、EA全体を完成へと導いていきます。


新しいバーの検出と日次処理の制御

EAでは、新しい日足が始まったタイミングで重要な判断を下すように設計されています。そのため、最初に実装するユーティリティ機能は、選択した時間足で新しいバーが形成されたことを確実に検出する仕組みです。まずは、直近のバーのタイムスタンプが変化したかどうかを確認するヘルパー関数を追加します。

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

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

この関数は、ソースファイルのユーティリティ関数セクションに配置します。関数は、現在のバーの始値時刻と、保存してある前回のバー始値時刻を比較します。両者が異なっていれば新しいバーが形成されたと判断し、保存している時刻を現在のバーの時刻へ更新します。このシンプルな仕組みにより、EAはバーごとに一度だけ日次ロジックを実行できるようになり、ティックを受信するたびに同じ判定が繰り返されることを防げます。

この処理を実現するため、最後に確認したバーの始値時刻を保持するグローバル変数を追加します。

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

この変数はティックをまたいで値を保持する必要があるため、グローバル変数として宣言します。EAの初期化時には、この変数は明示的に0に設定されます。

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

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

   return(INIT_SUCCEEDED);
}

これにより、EA起動後に最初に検出したバーは必ず新しいバーとして認識され、開始直後から正しく動作するようになります。この仕組みはEA全体の実行フローを支える重要な基盤となります。後に実装するスマッシュデーパターンの検出やセットアップ管理は、すべてこの正確な新規バー検出に基づいて動作します。

アウトサイドバーを除外して無効なスマッシュバーをフィルタリングする

本EAでは、スマッシュバーはアウトサイドバーであってはならないという設計を採用しています。アウトサイドバーは、市場の過度なボラティリティや不安定な価格形成を示すことが多く、スマッシュデー反転が意図する値動きとは一致しません。このルールを実装するため、指定したバーが直前のバーの値幅を完全に包み込んでいるかどうかを判定する関数を作成します。

//+-----------------------------------------------------------------------------+
//| Checks whether the bar at the given index fully engulfs the prior bar range |
//+-----------------------------------------------------------------------------+
bool IsOutsideBar(string symbol, ENUM_TIMEFRAMES tf, int index){

   double high0 = iHigh(symbol, tf, index);
   double low0  = iLow (symbol, tf, index);

   double high1 = iHigh(symbol, tf, index + 1);
   double low1  = iLow (symbol, tf, index + 1);

   return (high0 > high1 && low0 < low1);
}

この関数では、現在のバーの高値と安値を、1本前のバーの高値と安値と比較します。現在のバーが前のバーよりも高い高値を付け、かつ低い安値も付けている場合、そのバーはアウトサイドバーと判定され、それ以降の評価対象から除外されます。この関数は意図的に小さく、単一の役割だけを持つよう設計されています。スマッシュデーパターンそのものを検出するのではなく、後続の判定関数が利用するフィルタとして機能します。このように処理を分離することで、コードの見通しが良くなり、同じ判定処理を何度も記述する必要がなくなります。

スマッシュデー反転パターンの検出

アウトサイドバーを除外できるようになったので、次に有効なスマッシュデー反転を検出するロジックを実装します。買い反転判定関数では、スマッシュバーの終値が、ユーザーが指定した本数分の過去バーの安値を下回っているかどうかを確認します。

//+-----------------------------------------------------------------------------------+
//| Detects a Smash Day Buy Reversal where the close breaks below multiple prior lows |
//+-----------------------------------------------------------------------------------+
bool IsSmashDayBuyReversal(string symbol,
                           ENUM_TIMEFRAMES tf,
                           int index,
                           int lookbackBars)
{
   // Bar must not be an outside bar
   if(IsOutsideBar(symbol, tf, index))
      return false;

   double close1 = iClose(symbol, tf, index);

   // Validate close breaks below N prior lows
   for(int i = 2; i <= lookbackBars + 1; i++)
   {
      double priorLow = iLow(symbol, tf, i);

      if(close1 >= priorLow)
         return false;
   }

   return true;
}

ルックバック期間はユーザーが自由に設定できるため、パターンの厳しさを柔軟に調整できます。この関数はまずアウトサイドバーかどうかを確認し、その後、終値がルックバック期間内のすべての安値を下回っているかを検証します。一つでも条件を満たさない安値が存在すれば、そのパターンは無効と判定されます。

売り反転判定関数は、これと完全に対称のロジックを持ちます。

//+-------------------------------------------------------------------------------------+
//| Detects a Smash Day Sell Reversal where the close breaks above multiple prior highs |
//+-------------------------------------------------------------------------------------+
bool IsSmashDaySellReversal(string symbol,
                            ENUM_TIMEFRAMES tf,
                            int index,
                            int lookbackBars)
{
   // Bar must not be an outside bar
   if(IsOutsideBar(symbol, tf, index))
      return false;

   double close1 = iClose(symbol, tf, index);

   // Validate close breaks above N prior highs
   for(int i = 2; i <= lookbackBars + 1; i++)
   {
      double priorHigh = iHigh(symbol, tf, i);

      if(close1 <= priorHigh)
         return false;
   }

   return true;
}

こちらでは、スマッシュバーの終値が複数の過去高値を上回っているかを確認します。この2つの関数が、本EAにおけるシグナル生成ロジックの中核となります。後に行われるすべての売買判断は、最終的にこれらのパターン判定関数へとたどることができます。

Supertrendデータの更新とトレンド判定

本EAではトレンドフィルターをサポートしているため、Supertrendインジケーターの値を取得し、その状態を判定する必要があります。まず、初期化時に作成したインジケータハンドルを利用して、Supertrendの上側バンドと下側バンドを取得する専用関数を実装します。

//+------------------------------------------------------------------+
//| Fetches recent Supertrend upper and lower band values            |
//+------------------------------------------------------------------+
void UpdateSupertrendBandValues(){

   //--- Get a few Supertrend upper band values
   int copiedUpper = CopyBuffer(supertrendIndicatorHandle, 5, 0, 5, upperBandValues);
   if(copiedUpper == -1)
   {
      Print("Error while copying Supertrend upper band values: ", GetLastError());
      return;
   }

   //--- Get a few Supertrend lower band values
   int copiedLower = CopyBuffer(supertrendIndicatorHandle, 6, 0, 5, lowerBandValues);
   if(copiedLower == -1)
   {
      Print("Error while copying Supertrend lower band values: ", GetLastError());
      return;
   }
   
   if(copiedUpper < 5 || copiedLower < 5){
      Print("Insufficient Supertrend indicator data!");
      return;
   }   
}

この関数は、最新のSupertrendデータを内部配列へ更新します。取得後にはデータ数も確認し、十分な本数を取得できていない場合は処理を中断します。これにより、不完全なインジケータデータによって誤ったトレンド判定が下されることを防げます。

続いて、Supertrendの状態を判定する2つのヘルパー関数を用意します。

//+------------------------------------------------------------------+
//| Returns true if Supertrend is currently in a bullish trend state |
//+------------------------------------------------------------------+
bool IsSupertrendCurrentlyBullish(){

   if(lowerBandValues[1] != EMPTY_VALUE){
      return true;
   }

   return false;
}

//+------------------------------------------------------------------+
//| Returns true if Supertrend is currently in a bearish trend state |
//+------------------------------------------------------------------+
bool IsSupertrendCurrentlyBearish(){

   if(upperBandValues[1] != EMPTY_VALUE){
      return true;
   }

   return false;
}

下側バンドが有効であれば上昇トレンド、上側バンドが有効であれば下降トレンドと判断します。データ取得とトレンド判定を別々の関数へ分離することで、コードの見通しが良くなり、再利用もしやすくなります。そのため、後続のエントリーロジックでは、Supertrendの判定方法を意識することなく、「現在が上昇トレンドか」「下降トレンドか」だけを問い合わせればよくなります。

曜日フィルタ

ラリー・ウィリアムズは、市場のリズムやタイミングの重要性をたびたび強調しています。その考え方を反映するため、本EAには曜日による取引フィルタを実装しています。まず、datetime型から曜日を数値として取得するヘルパー関数を用意します。

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

このフィルタには2つの動作モードがあります。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;
}

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

これらの関数は、1分足の確定データを利用して動作し、価格が指定したレベルを下方向から上抜けた場合、または上方向から下抜けた場合を検出します。ロジック自体はシンプルですが、正確性を重視して設計されています。これにより、1回の突破イベントにつき1度だけトレード判断が下されるようになります。これらのユーティリティ関数は、エントリーロジックと直接連携して使用されます。

常に1つのポジションのみを保有する

過剰なリスクを防ぐため、本EAは同時に保有できるポジションを1つだけに制限する設計となっています。この制御を実現するため、指定したマジックナンバーに対応する買いポジションまたは売りポジションがすでに存在するかを確認する2つのユーティリティ関数を定義します。

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

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

各関数は、すべての保有ポジションを確認し、該当するポジションが見つかった場合はtrueを返します。新しいトレードを実行する前には、必ずこれらのチェックがおこなわれます。これにより、ポジション管理を厳密に制御でき、同じシグナルに対して複数ポジションが重複して建てられることを防止できます。

リスクに基づいたポジションサイジングと利益目標の計算

リスク管理は、このシステムにおける中心的な要素です。本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);
}

この計算では、ストップロスまでの距離と契約サイズを基準として、設定した許容リスク額に一致する適切な取引数量(ロットサイズ)を算出します。さらに、ユーザーが設定したリスクリワード比率に基づいて、買いエントリー用および売りエントリー用のテイクプロフィット価格を計算する専用関数をそれぞれ用意します。

//+--------------------------------------------------------------------------------------------------+
//| Computes the bullish take profit level based on entry price, stop loss, and risk to reward ratio |
//+--------------------------------------------------------------------------------------------------+
double GetBuyTakeProfit(double entryPrice, double stopLoss){
   double riskDistance = entryPrice - stopLoss;
   double rewardDistance = riskDistance * riskRewardRatio;
   rewardDistance = MathAbs(rewardDistance);
   return NormalizeDouble((entryPrice + rewardDistance), Digits());
}

//+--------------------------------------------------------------------------------------------------+
//| Computes the bearish take profit level based on entry price, stop loss, and risk to reward ratio |
//+--------------------------------------------------------------------------------------------------+
double GetSellTakeProfit(double entryPrice, double stopLoss){
   double riskDistance = stopLoss - entryPrice;
   double rewardDistance = riskDistance * riskRewardRatio;
   rewardDistance = MathAbs(rewardDistance);
   return NormalizeDouble((entryPrice - rewardDistance), Digits());
}

この設計により、利益目標は固定値ではなく、各トレードのストップロス幅に応じて自動的に調整されます。結果として、市場環境や銘柄ごとの価格特性に左右されにくい、統一されたリスク管理ルールを適用できます。

成行注文の実行

2つの実行関数が、買い注文と売り注文を処理します。

//+------------------------------------------------------------------+
//| Function to open a market buy position                           |
//+------------------------------------------------------------------+
bool OpenBuy(double entryPrice, double stopLoss, double takeProfit, double lotSize){
   
   if(lotSizeMode == MODE_AUTO){
      lotSize = CalculatePositionSizeByRisk(entryPrice - stopLoss);
   }
   
   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;
}

//+------------------------------------------------------------------+
//| Function to open a market sell position                          |
//+------------------------------------------------------------------+
bool OpenSel(double entryPrice, double stopLoss, double takeProfit, double lotSize){
   
   if(lotSizeMode == MODE_AUTO){
      lotSize = CalculatePositionSizeByRisk(stopLoss - entryPrice);
   }
   
   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;
}

各関数は、手動ロット設定と自動ロット設定の両方に対応しています。自動モードが有効になっている場合、ポジションサイズは、先に定義したリスクベースの計算関数を使用して動的に算出されます。実行エラーはデバッグやテストを容易にするため、明確にログへ記録されます。これらの関数はトレード処理の最終段階として機能し、検証済みのシグナルを受け取り、安全に注文を実行します。

ティック関数で全体を統合する

//+------------------------------------------------------------------+
//| 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, 7, closePriceMinutesData) == -1){
      Print("Error while copying minutes datas ", GetLastError());
      return;
   }
   
   //--- Update Supertrend
   UpdateSupertrendBandValues();
   
   //--- Execute this block on new bar formation
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
      
      //---
      if(!smashState.hasBuySmashSetup && !smashState.hasSellSmashSetup){
         if(IsSmashDayBuyReversal(_Symbol, timeframe, 1, smashBuyLookbackBars)){
            smashState.hasBuySmashSetup = true;
            smashState.buyBreakoutLevel = iHigh(_Symbol, timeframe, 1);
            smashState.smashBarTime     = iTime(_Symbol, timeframe, 1);
            smashState.entryPending     = true;
            smashState.buyStopLoss      = iLow(_Symbol, timeframe, 1);
         }
               
         //---
         if(IsSmashDaySellReversal(_Symbol, timeframe, 1, smashSellLookbackBars)){
            smashState.hasSellSmashSetup = true;
            smashState.sellBreakoutLevel = iLow(_Symbol, timeframe, 1);
            smashState.smashBarTime      = iTime(_Symbol, timeframe, 1);
            smashState.entryPending      = true;
            smashState.sellStopLoss      = iHigh(_Symbol, timeframe, 1);
         }
      }else{
      
         smashState.barsSinceSmash = smashState.barsSinceSmash + 1;
      
         if(smashState.barsSinceSmash > smashSetupValidityBars){
            ZeroMemory(smashState);
         }
      
      }
      
      //---
      if(smashState.hasBuySmashSetup && smashState.entryPending){
         if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
            if(smashTradeMode == SMASH_TRADE_BUY_ONLY || smashTradeMode == SMASH_TRADE_BOTH){
               if(askPrice > smashState.buyBreakoutLevel){
                  if(useSupertrendFilter){
                     if(IsSupertrendCurrentlyBullish()){
                        if(tradeDayMode == TDW_SELECTED_DAYS){
                           if(IsTradingDayAllowed(currentTime)){
                              OpenBuy(askPrice, smashState.buyStopLoss, GetBuyTakeProfit(askPrice, smashState.buyStopLoss), positionSize);
                              ZeroMemory(smashState);
                           }
                        }else{
                           OpenBuy(askPrice, smashState.buyStopLoss, GetBuyTakeProfit(askPrice, smashState.buyStopLoss), positionSize);
                           ZeroMemory(smashState);
                        }
                     }
                  }else{
                     if(tradeDayMode == TDW_SELECTED_DAYS){
                        if(IsTradingDayAllowed(currentTime)){
                           OpenBuy(askPrice, smashState.buyStopLoss, GetBuyTakeProfit(askPrice, smashState.buyStopLoss), positionSize);
                           ZeroMemory(smashState);
                        }
                     }else{
                        OpenBuy(askPrice, smashState.buyStopLoss, GetBuyTakeProfit(askPrice, smashState.buyStopLoss), positionSize);
                        ZeroMemory(smashState);
                     }
                  }
               }
            }
         }
      }
      
      //---
      if(smashState.hasSellSmashSetup && smashState.entryPending){
         if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
            if(smashTradeMode == SMASH_TRADE_SELL_ONLY || smashTradeMode == SMASH_TRADE_BOTH){
               if(bidPrice < smashState.sellBreakoutLevel){
                  if(useSupertrendFilter){
                     if(IsSupertrendCurrentlyBearish()){
                        if(tradeDayMode == TDW_SELECTED_DAYS){
                           if(IsTradingDayAllowed(currentTime)){
                              OpenSel(bidPrice, smashState.sellStopLoss, GetSellTakeProfit(bidPrice, smashState.sellStopLoss), positionSize);
                              ZeroMemory(smashState);
                           }
                        }else{
                              OpenSel(bidPrice, smashState.sellStopLoss, GetSellTakeProfit(bidPrice, smashState.sellStopLoss), positionSize);
                              ZeroMemory(smashState);
                        }
                     }
                  }else{
                     if(tradeDayMode == TDW_SELECTED_DAYS){
                        if(IsTradingDayAllowed(currentTime)){
                           OpenSel(bidPrice, smashState.sellStopLoss, GetSellTakeProfit(bidPrice, smashState.sellStopLoss), positionSize);
                           ZeroMemory(smashState);
                        }
                     }else{
                        OpenSel(bidPrice, smashState.sellStopLoss, GetSellTakeProfit(bidPrice, smashState.sellStopLoss), positionSize);
                        ZeroMemory(smashState);
                     }
                  }
               }
            }
         }
      }     
   }
   
   //---
   if(smashEntryMode == ENTRY_ON_LEVEL_CROSS){
   
      //---
      if(smashState.hasBuySmashSetup && smashState.entryPending){
         // ---
         if(IsCrossOver(smashState.buyBreakoutLevel, closePriceMinutesData)){
            if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
               if(smashTradeMode == SMASH_TRADE_BUY_ONLY  || smashTradeMode == SMASH_TRADE_BOTH){
                  if(useSupertrendFilter){
                     if(IsSupertrendCurrentlyBullish()){
                        if(tradeDayMode == TDW_SELECTED_DAYS){
                           if(IsTradingDayAllowed(currentTime)){
                              OpenBuy(askPrice, smashState.buyStopLoss, GetBuyTakeProfit(askPrice, smashState.buyStopLoss), positionSize);
                              ZeroMemory(smashState);
                           }
                        }else{
                           OpenBuy(askPrice, smashState.buyStopLoss, GetBuyTakeProfit(askPrice, smashState.buyStopLoss), positionSize);
                           ZeroMemory(smashState);
                        }
                     }
                  }else{
                     if(tradeDayMode == TDW_SELECTED_DAYS){
                        if(IsTradingDayAllowed(currentTime)){
                           OpenBuy(askPrice, smashState.buyStopLoss, GetBuyTakeProfit(askPrice, smashState.buyStopLoss), positionSize);
                           ZeroMemory(smashState);
                        }
                     }else{
                        OpenBuy(askPrice, smashState.buyStopLoss, GetBuyTakeProfit(askPrice, smashState.buyStopLoss), positionSize);
                        ZeroMemory(smashState);
                     }
                  }
               }
            }
         }
      }
      
      //---
      if(smashState.hasSellSmashSetup && smashState.entryPending){
         // ---
         if(IsCrossUnder(smashState.sellBreakoutLevel, closePriceMinutesData)){
            if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
               if(smashTradeMode == SMASH_TRADE_SELL_ONLY || smashTradeMode == SMASH_TRADE_BOTH){
                  if(useSupertrendFilter){
                     if(IsSupertrendCurrentlyBearish()){
                        if(tradeDayMode == TDW_SELECTED_DAYS){
                           if(IsTradingDayAllowed(currentTime)){
                              OpenSel(bidPrice, smashState.sellStopLoss, GetSellTakeProfit(bidPrice, smashState.sellStopLoss), positionSize);
                              ZeroMemory(smashState);
                           }
                        }else{
                           OpenSel(bidPrice, smashState.sellStopLoss, GetSellTakeProfit(bidPrice, smashState.sellStopLoss), positionSize);
                           ZeroMemory(smashState);
                        }
                     }
                  }else{
                     if(tradeDayMode == TDW_SELECTED_DAYS){
                        if(IsTradingDayAllowed(currentTime)){
                           OpenSel(bidPrice, smashState.sellStopLoss, GetSellTakeProfit(bidPrice, smashState.sellStopLoss), positionSize);
                           ZeroMemory(smashState);
                        }
                     }else{
                        OpenSel(bidPrice, smashState.sellStopLoss, GetSellTakeProfit(bidPrice, smashState.sellStopLoss), positionSize);
                        ZeroMemory(smashState);
                     }
                  }
               }
            }
         }
      }   
   }   
}

この段階で、必要なすべての構成要素が揃いました。ここからティック関数を拡張し、戦略全体を制御する役割を持たせます。

これにより、テスト中にスマッシュバーや価格の動きを観察しやすくなります。この関数は、EAが起動するたびに一貫した表示になるよう、EA初期化時に呼び出されます。

開発フェーズの終了

ここまでで、EAは完全に構築され、テストを開始できる状態になりました。完成したソースコードはlwSmashDayTrendFilteredExpert.mq5として添付されており、読者は記事内に大量のコードリストを掲載することなく、最終的な実装内容を確認できます。これで開発フェーズは終了です。ここまで追加したすべての関数は、それぞれ明確な役割を持ち、システム全体の中で論理的に連携しています。


過去データに基づいた戦略の検証

EAが実際の市場環境でどのように動作するかを評価するため、過去の価格データを使用してバックテストを実施しました。この段階は重要です。なぜなら、主観的な判断を入れることなく、収益性、一貫性、ドローダウンなどのパフォーマンス特性を確認できるためです。

テストは、XAUUSD(ゴールド) を対象に実施しました。時間足には日足を採用しています。これは、ラリー・ウィリアムズが説明した元来のスマッシュデーロジックとの一貫性を維持するためです。テスト期間は、2025年1月1日から2025年12月30日までの1年間です。これにより、1年間の中で発生したさまざまな市場環境を含む、十分なサンプルを取得できます。

今回のテストでは、スマッシュデー買いセットアップのみを有効化しました。つまり、システムはロングポジションのみを取るよう制限されており、ショート取引の影響を排除することで、強気方向のスマッシュデー動作だけを評価できるようにしています。ポジションサイズは自動設定モードを使用し、各トレードでは口座残高の1.0%を正確にリスクとして設定しました。

テストレポート

初期口座残高は1万ドルに設定されました。テスト期間終了時点で、システムは832.97ドルの純利益を達成し、投資収益率は8%をわずかに上回りました。記録された勝率は75.00パーセントでした。利益額だけを見ると控えめに見えるかもしれませんが、より重要なのはエクイティカーブの推移です。

テスト期間を通してエクイティカーブは滑らかに推移し、大きなドローダウンや不安定な資産変動は確認されませんでした。これは、リスク管理が適切に機能し、安定した実行が行われたことを示しています。この動きを視覚的に確認しやすくするため、このセクションにはエクイティカーブのスクリーンショットを掲載しています。

エクイティ成長カーブ

再現性を高めるため、本記事には2つのファイルを添付しています。1つ目のファイルには、ストラテジーテスター環境の設定が含まれています。2つ目のファイルには、このテストで使用した正確な入力パラメータ設定が含まれています。これらのファイルを使用することで、同じ条件で曖昧さなくテストを再現できます。

今回のテストは、1つの設定と1つの市場を対象とした結果にすぎません。本EAは、入力パラメータによって柔軟に調整できるよう意図的に設計されています。異なるルックバック期間、エントリーモード、リスク設定、フィルター条件などを変更することで、さまざまなアイデアを検証できます。他の銘柄や時間足で追加の実験を行うことで、異なるトレードスタイルやリスク許容度に適した特徴が見つかる可能性があります。

独自のテストによって発見された結果、改善点、または観察事項は、いずれも価値のある情報です。こうした知見を記事のコメント欄で共有することで、この研究は単一の実装例を超え、固定された解決策ではなく、共同で学習を進めるためのツールへと発展していきます。


結論

本記事では、明確な課題の解決を目的としていました。スマッシュデーパターンを相場環境を考慮せずに取引すると、ノイズの多いシグナルや一貫性のない結果につながります。そこで、このパターンを自動化に適した、体系的かつ検証可能なシステムへ変換することを目標としました。

その目標は達成されました。本記事では、パターンの検出、確認ロジック、セットアップの有効期間制限、そして制御されたエントリー条件を含む、客観的なスマッシュデールール一式を提供しています。さらに、Supertrendによるトレンド方向やTrade Day of the Weekによる曜日選択などのオプション式コンテキストフィルターもシステムへ直接統合されており、入力パラメータによって有効・無効を切り替えることができます。

実用面では、読者は定義されたルール通りにスマッシュデーリバーサルを自動化する、完全に機能するMQL5 EAを手にすることになります。このEAは、同時保有ポジションを1つに制限し、一貫したリスク管理を適用し、すべての前提条件を変更可能な入力パラメータとして公開しています。また、添付されたパラメータファイルおよび設定ファイルを利用することで、ソースコードを変更することなくテスト結果を再現し、さらに検証を進めることができます。

アルゴリズムトレーダーにとって、このEAは異なる条件下でスマッシュデーの動作を研究するための実用的な検証ツールとなります。MQL5開発者にとっては、裁量的な価格パターンを、明確で検証可能なロジックへ変換する方法を示す実例となります。最も重要なのは、本記事が冒頭で掲げた目的を達成していることです。つまり、単独のパターンから出発し、客観的で相場環境を考慮したトレードシステムへ発展させ、信頼性の高い方法でテストおよび評価できる明確な道筋を示しています。


添付ファイル

ファイル名 説明
lwSmashDayTrendFilteredExpert.mq5 本記事で開発したEA
configurations.ini 本記事のテスト実行時にストラテジーテスター環境を設定するためのファイル
parameters.set 本記事のテストで使用したユーザー入力パラメータを設定するためのファイル


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

添付されたファイル |
configurations.ini (2.67 KB)
parameters.set (2.55 KB)
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
Neuro-Structural Trading Engine (NSTE)(第I回):プロップファーム対応マルチ口座システムの構築方法 Neuro-Structural Trading Engine (NSTE)(第I回):プロップファーム対応マルチ口座システムの構築方法
プロップファームの制約を遵守しながら、MetaTrader 5上で暗号資産CFDを運用するマルチ口座アルゴリズム取引システムアーキテクチャを提示します。システムは、「固定ドルリスク」「1口座につき1スクリプト」「設定の一元管理」という3つの基本原則を定義した上で、PythonとMQL5の役割分担、60秒ごとの処理ループ、JSONベースのシグナル連携について詳しく解説します。さらに、ロットサイズの実践的な計算方法、安全性を確保するためのチェック機構、ポジション管理パターンについても紹介し、信頼性の高い運用を実現するための構成を示します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5でのスイング極値とプルバック(第1回):マルチタイムフレームインジケータの開発 MQL5でのスイング極値とプルバック(第1回):マルチタイムフレームインジケータの開発
Swing Extremes and Pullbacksインジケータを自動化します。このインジケータは、下位足(LTF)生の値動きを、市場の意図を構造化して示すマップへと変換します。リアルタイムでスイングハイ・スイングロー・調整フェーズを正確に検出し、マイクロストラクチャーの変化をプログラムによって追跡することで、完全な反転が形成される前に潜在的な転換ポイントを予測します。これにより、市場ノイズを実用的なシグナルへ変換し、より精度の高い意思決定を可能にします。