初心者からエキスパートへ:時間フィルタ付き取引
内容
はじめに
MQL5の機能を活用することで、取引したい時間帯を正確に定義し、それに応じてアラートを受け取ることが可能です。前回の議論では、セッション可視化の基礎を確立し、各取引セッションの構造(実体(ボディ)、上ヒゲ、下ヒゲ)を明らかにしました。このプロジェクトにより、上位時間足セッションの広範な市場構造を視覚化できるようになり、下位時間足データと同期させることで、内部の価格変動や形成中のパターンを拡大して観察できるようになりました。
これらの概念は網羅的ではありませんが、より高度な市場分析ツールを設計し、可視化したい研究者にとって貴重なステップとなります。価格は本質的に時系列現象であり、ティックごとに連続して進化します。私たちのエキスパートアドバイザー(EA)やインジケーターは、この変動を絶え間なく監視し、検出したパターンや条件に反応するよう設計されています。しかし、時間ベースのフィルタを導入しなければ、技術的条件が満たされるたびに無差別に動作してしまい、流動性が低い時間帯、スプレッドの乱高下、取引量の低い時間帯でも注文を出してしまいます。
このような状況を防ぐために、アルゴリズムを明確に定義された取引ウィンドウやセッションに紐付けることが重要です。これにより、市場が最も活発で構造的に意味のあるタイミングのみに動作させることができます。時間フィルタリングは、単なる戦略論理だけでなく、時間のリズム自体に沿った取引実行を保証する規律的な枠組みとして機能します。
既知のカレンダーイベントは、時間フィルタ付き取引の強力な参照点となります。非農業部門雇用者数(NFP)、消費者物価指数(CPI)、FOMC決定、GDP発表などの経済指標は時間的に進行するため、精密な取引ウィンドウの定義に利用できます。これにより、ツールが注文やシグナル生成を許可される時間帯、制限される時間帯を指定可能です。
セッションマーカーが市場のリズムを定義するのと同じように、経済カレンダーマーカーはイベント駆動のボラティリティゾーンを定義します。たとえば、高インパクトイベントの30分前に自動で取引を停止し、ボラティリティが安定した後に再開するアルゴリズムを組むことも可能です。MQL5では、MqlCalendarValue()やCalendarEventById()といった関数を通じて、EAが次の経済指標を検出し、それに合わせて操作を同期できます。
イベント駆動型のスケジューリングに加えて、時計の時刻そのものも重要な制御要素です。トレーダーや研究者は、たとえば10:00〜12:00など固定の時間帯をシグナル生成や実行用に定義できます。これにより、ノイズを減らすと同時に、統計的に有利な時間帯を切り出すことができます。

カレンダーイベントに基づくフィルタと時間帯に基づくウィンドウを組み合わせることで、システムは二重の時間認識を持つことになります。1つはスケジュールされた経済活動に基づく時間、もう1つは日中の構造的リズムに基づく時間です。これらを統合することで、EAは価格条件だけでなく時間的文脈に基づいて行動できるようになります。自動売買において、精度と偶然性の差を生む重要な要素は、まさにこの時間的文脈です。
本記事では時間フィルタ付き取引に焦点を当てます。前回のプロジェクトで設定した時間参照ポイントを活用し、取引頻度の制御やシグナル発生の制限をおこなう方法を探ります。アルゴリズムを使うことで、取引が許可される特定の時間帯を定義でき、自動売買戦略の精度、制御性、柔軟性を高めることができます。 単なる条件ベースのトリガーではなく、時間認識を持つ取引を実現します。つまり、市場の動きだけでなく、そのタイミングも尊重することが可能です。
MQL5のフレームワークを活用することで、次のようなシステムを作ることができます。
- 選択したセッション(例:ロンドン、ニューヨーク)でのみ取引
- 重要指標発表前や流動性が低い時間帯には取引を停止
- 時計と条件が一致した場合のみアラート、シグナル、エントリーを実行
時間フィルタリングの背後にある概念
時間フィルタリングは、戦略ロジックを特定の時間帯に限定して実行する手法です。EAやインジケーターを連続稼働させるのではなく、現在の市場時間が希望する取引期間内かどうかをチェックしてから実行させます。
このアプローチは以下の場合に有効です。- 流動性が低い時間帯の回避(例:ニューヨーククローズ後)
- セッション間のボラティリティを捉える(例:ロンドン–ニューヨーク重複)
- シグナル頻度の制御によるノイズ低減や過剰取引防止
今回のアプローチでは、焦点を完全に実装に移します。この段階で、Market Periods Synchronizerに対するさらなるアップグレードはおこないません。その代わり、前回の公開で確立した作業を基盤として構築し、現在の研究の土台とします。
具体的には、SessionVisualizerヘッダファイルを参照します。このモジュールは、市場セッション期間を定義し、マークする役割を担っています。このモジュール内で作成された時間マーカーは、時間フィルタ付き取引の新しい概念における重要な参照ポイントとして機能します。これらのマーカーを活用することで、セッションの開始前後や終了前後に発生する取引ウィンドウを定義し、制御できるようになります。
次のセクションでは、このアイデアの詳細な実装を探り、時間ベースの制御を取引ロジックに直接統合する方法を示します。以下の手順で実装を進めます。
- モジュラー構造のTimeFilterレイヤーを構築し、SessionVisualizerと連携してセッションベース、時計ベース、(オプションで)イベントベースの取引ウィンドウを提供する。
- サンプルのEAを作成し、アクティブな時間フィルタが取引を許可している場合にのみ取引を実行する。
- サンプルのインジケーターを作成し、許可された時間フィルタされたウィンドウ内のみでシグナルをプロットまたはトリガーする。
実装
ここからは、TimeFilterクラスの実装から始め、それを2つのサンプルプロジェクト(EAとインジケーター)に統合します。最後に、テスト結果と完全なソースファイルを提示します。
TimeFilter
1. モジュールヘッダとインクルードガード
まず、このファイルを単なる使い捨てコードではなく、再利用可能なライブラリとして扱います。「#property strict」を使用することで、より安全なコンパイルルールを適用します。また、インクルードガードを設置することで、複数のプロジェクトで同じファイルを使用した場合でも、重複定義を防ぐことができます。さらに、SessionVisualizer.mqhをインクルードします。これは、TimeFilterが既存のセッション可視化ロジックと連携できるように設計されているためです。このような構造的な規律をコードベースに組み込むことで、プロジェクト全体のスケーラビリティを高めることができます。
// ============================================================================ // TimeFilters.mqh — modular helpers for time-filtered trading // Author—Clemence Benjamin // ============================================================================ #property strict #ifndef __TIMEFILTERS_MQH__ #define __TIMEFILTERS_MQH__ #include "SessionVisualizer.mqh"
2. 入力パラメータとユーザー制御設定
次に、入力パラメータを通じて小さなコントロールパネルを提供します。挙動をハードコーディングする代わりに、トレーダー(またはテスター)がセッション、固定時間、または経済イベントによるフィルタリングを選択できるようにしています。時計の開始/終了時刻、セッション前後の余白(padding)、将来的なニュースフィルタなども、ソースコードに手を加えずに調整可能です。各パラメータの名前には一貫してInp接頭辞を付け、すべての入力にはドキュメントが付与されています。これは、MetaTrader 5の[Inputs]タブでモジュールを読みやすく表示させると同時に、コード内でも自己説明的にするためのベストプラクティスです。
// --------------------------- Inputs / Settings ------------------------------ input bool InpUseSessionFilter = true; // gate by session times input bool InpUseClockFilter = true; // gate by fixed clock window input bool InpUseEventFilter = false; // gate by economic calendar (stubbed safe) input int InpClockStartHour = 10; // e.g., 10:00 input int InpClockEndHour = 12; // e.g., 12:00 input int InpPreSessionMins = 0; // allow N minutes before session start input int InpPostSessionMins = 0; // allow N minutes after session end // Event filter parameters (used when you enable InpUseEventFilter) input int InpCalLookAheadMin = 60; input int InpCalBlockBeforeMin = 30; input int InpCalBlockAfterMin = 30; input int InpCalMinImportance = 2; // 0=low,1=medium,2=high
3. 時間フィルタコンテキストラッパー
EA全体にグローバル変数を散在させる代わりに、時間に関連する状態をCTimeFilterContextにカプセル化します。このクラスは、オプションでCSessionVisualizerへのポインタや、アクティブなgmt_offsetを保持できます。つまり、どのEAやインジケーターも、軽量な単一のコンテキストオブジェクトを渡すだけで、すべての時間関連の判断が一貫しておこなえるようになります。SetGMTOffsetメソッドは、Visualizerが存在する場合にオフセットを転送するため、描画とフィルタリングを同期させつつ、ロジックの重複を避けることができます。
// --------------------------- Context wrapper -------------------------------- class CTimeFilterContext { public: CSessionVisualizer *viz; // optional visualizer pointer int gmt_offset; CTimeFilterContext() : viz(NULL), gmt_offset(0) {} void AttachVisualizer(CSessionVisualizer &ref) { viz = &ref; } void SetGMTOffset(int off) { gmt_offset = off; if(viz != NULL) { // safest explicit dereference form (*viz).SetGMTOffset(off); } } };
4. コア時計ウィンドウフィルタ
時計ウィンドウは、最初で最も単純な時間制約です。「この時間帯だけ動作する」という制御をおこないます。ここでは、サーバー時刻を構造化された形式に変換し、ユーザーが設定した時間範囲と照合します。注目すべき挙動が2つあります。「startHour == endHour」の場合は「制限なし」と解釈し、「startHour > endHour」の場合は、ウィンドウが日付をまたぐ(例:22:00〜02:00)と解釈します。多くの初心者は、この日付をまたぐケースを忘れがちです。ここで正しく処理しておくことで、以降のロジックはこのユーティリティを安心して利用できるようになります。
// --------------------------- Utility: Clock Window -------------------------- bool InClockWindow(const int startHour, const int endHour) { MqlDateTime t; TimeToStruct(TimeCurrent(), t); if(startHour == endHour) return true; // treat as "always on" if(startHour < endHour) return (t.hour >= startHour && t.hour < endHour); // wrap across midnight, e.g., 22..02 return (t.hour >= startHour || t.hour < endHour); }
5. セッションベースのウィンドウフィルタ
この関数は、時間フィルタエンジンをセッションロジックに接続する役割を持ちます。引数として、CTimeFilterContext(設定用)とSESSION_TYPE(Sydney、Tokyo、London、New Yorkのいずれか)を受け取ります。実装によっては、GetTodaySessionWindow(SessionVisualizer内)が当日のセッション開始時刻と終了時刻を返します。さらに、セッション前後の余白を分単位で加算します。ここには、トレーダーがよく使うルールを組み込むことができます(例:「ロンドン開始の15分前から監視を開始する」「ニューヨーク終了30分前は取引を避ける」)。注意点として、失敗時は制限しない(fail open)設計になっています。もしセッションウィンドウを取得できなかった場合はtrueを返します。これにより、Visualizerや設定が欠落していても、すべての取引が意図せず停止することを防ぐことができます。
// --------------------------- Utility: Session Window ------------------------ bool InSessionWindow(CTimeFilterContext &ctx, const SESSION_TYPE sType, const int preMins = 0, const int postMins = 0) { if(ctx.viz == NULL) return true; // no visualizer attached -> don't block datetime ss, se; if(!(*ctx.viz).GetTodaySessionWindow(sType, ss, se)) return true; // session not available -> don't block // Apply pre/post-minute padding ss -= (preMins * 60); se += (postMins * 60); datetime now = TimeCurrent(); return (now >= ss && now <= se); }
6. イベントフィルタのスタブ(拡張用フック)
ここでは、EventWindowAllowedを安全なスタブとして定義します。現状では、常にtrueを返す仕様です。この設計は意図的なものです。多くの読者は、経済カレンダー機能なしでブローカーやテスターを使用している場合があります。経済指標カレンダーへの強い依存があると、移植性が失われ、EAが動作しなくなる可能性があります。このフックを正しいシグネチャで提供することで、後から実際のカレンダーに基づくロジック(例:NFP、FOMC、CPIなどの前後で取引を制限する)を、既存のIsTradingAllowedを利用しているEAコードを変更せずに簡単に組み込むことができます。
// --------------------------- Utility: Economic Calendar --------------------- bool EventWindowAllowed(const int /*lookAheadMin*/, const int /*blockBeforeMin*/, const int /*blockAfterMin*/, const int /*minImportance*/) { // Stubbed: always allow for safe compilation return true; }
7. 複合決定:取引は許可されているか
最後に、すべての要素が1つのきれいな関数「IsTradingAllowed」に統合されます。この関数は、EAやインジケーターが毎ティックごと、またはシグナル発生ごとに呼び出すべきメソッドです。有効になっているフィルタを順番に適用します。
- 時計フィルタが有効 → 許可された時間内であること
- セッションフィルタが有効 → 少なくとも1つの設定されたセッションウィンドウ内にいること(余白付き)
- イベントフィルタが有効 → 将来的に設定されるイベントルールをクリアしていること
「ok = ok && ...」という形で論理を連鎖させることで、読みやすい構造を維持しています。1つのフィルタが失敗すると、それ以降も特別な分岐なしで false を返し続けます。この結果、単一の呼び出しで、元の戦略を時間意識型の戦略に変換することができる、クリーンなブールゲートが完成します。
// --------------------------- Composite Gate -------------------------------- bool IsTradingAllowed(CTimeFilterContext &ctx) { bool ok = true; if(InpUseClockFilter) ok = ok && InClockWindow(InpClockStartHour, InpClockEndHour); if(InpUseSessionFilter) { bool any = false; any = any || InSessionWindow(ctx, SESSION_LONDON, InpPreSessionMins, InpPostSessionMins); any = any || InSessionWindow(ctx, SESSION_NEWYORK, InpPreSessionMins, InpPostSessionMins); any = any || InSessionWindow(ctx, SESSION_TOKYO, InpPreSessionMins, InpPostSessionMins); any = any || InSessionWindow(ctx, SESSION_SYDNEY, InpPreSessionMins, InpPostSessionMins); ok = ok && any; } if(InpUseEventFilter) ok = ok && EventWindowAllowed(InpCalLookAheadMin, InpCalBlockBeforeMin, InpCalBlockAfterMin, InpCalMinImportance); return ok; } #endif // __TIMEFILTERS_MQH__
時間フィルタを使用したEAの例
1. 1. EAヘッダと高レベルの意図
すべてのプロフェッショナルなEAは、冒頭で自身を明確に紹介するべきです。ここでは、所有者の宣言、目的の説明、バージョンの固定をおこないます。これは単なる装飾ではありません。数か月後に自分で見返す場合、他の人がCodeBaseからあなたのソースをダウンロードした場合、これらの情報により、このEAがただのランダムな実験ではなく、構造化され、文書化されたシステムであり、時間フィルタ付き取引とCTradeを使ったクリーンな取引操作を目的に設計されていることが一目で分かります。
// TimeFilteredEA.mq5 - #property copyright "Clemence Benjamin" #property description "Professional Time-Filtered EA with streamlined CTrade usage." #property version "1.0"
2. 戦略的インクルード:取引、銘柄情報、セッション、時間フィルタ
次に、このEAが本当に依存するビルディングブロックだけを選択的にインクルードします。Trade.mqhは高レベルのCTradeラッパーを提供し、SymbolInfo.mqhはブローカー固有の銘柄プロパティを取得し、SessionVisualizer.mqhは視覚的コンテキストを管理し、TimeFilters.mqhは中心的なIsTradingAllowedゲートを提供します。挙動を明確に定義されたモジュールから組み立てており、すべてを一つの巨大で冗長なファイル内で作り直しているわけではありません。
#include <Trade/Trade.mqh> #include <Trade/SymbolInfo.mqh> #include <SessionVisualizer.mqh> #include <TimeFilters.mqh>
3. 公開入力パラメータ:設定可能
ここでは、トレーダー向けに整理された入力パネルを提供します。ロットサイズ、ストップロス、可視化用フラグなどをロジック内に埋め込むのではなく、入力変数として宣言することで、EA設定ダイアログに表示されるようにしています。各入力には、その目的を明確に説明するコメントが付いている点にも注目してください。さらに、専用のマジックナンバーと許容スリッページを使用しています。これは、複数EA環境で安全に注文を管理するための基本的なベストプラクティスです。
input int InpGMTOffsetHours = 0; // GMT offset for session alignment input bool InpDrawSessions = true; // Enable session visualization input int InpLookbackDays = 5; // Days to draw sessions input double InpLotSize = 0.01; // Fixed lot size input int InpStopLossPips = 50; // SL in pips (0 = none; TP auto-set to 2x if >0) input int InpTakeProfitPips = 0; // TP in pips (0 = auto 2x SL if SL>0) input int InpMagicNumber = 12345; // Magic number for trades input int InpDeviationPips = 10; // Max slippage in pips
4. コアオブジェクトと状態
ここでは、EAで使用する主要オブジェクトを宣言します。CTradeは注文実行用、CSymbolInfoはスプレッド、ティック、出来高データ取得用、CSessionVisualizerはセッションブロック描画用、そしてCTimeFilterContextは時間に関連する判断の調整用です。さらに、MAハンドルやgLastSignalBarも準備しており、同じローソク足でシグナルが二重に発生するのを防ぎます。このパターンは整理されており、環境レベルのツールはグローバルに保持され、取引ロジックやフィルタはモジュール化されたままです。
CTrade trade; CSymbolInfo gSymbolInfo; CSessionVisualizer gSV("TF_SESS_"); CTimeFilterContext gCTX; // MA handles int gFastMAHandle = INVALID_HANDLE; int gSlowMAHandle = INVALID_HANDLE; datetime gLastSignalBar = 0;
5. プロフェッショナル取引マネージャー:実行ロジックのカプセル化
EAのあちこちで直接trade.Buy()を呼び出すのではなく、CTradeManagerを導入します。このクラスは、ロットの正規化、スリッページ、マジックナンバーの割り当て、ストップロスとテイクプロフィットの計算、口座の検証などを一元管理します。こうすることで、戦略コードをクリーンに保つことができます。シグナルロジックは単に「買いたい」と要求するだけで、マネージャーがすべての詳細処理を一か所で担当し、失敗理由のログも堅牢に記録します。
//+------------------------------------------------------------------+ //| Professional Trade Manager Wrapper | //+------------------------------------------------------------------+ class CTradeManager { private: CTrade* m_trade; int m_magic; int m_deviation; double m_minVolume; double m_maxVolume; double m_volumeStep; int m_digits; double m_point; public: CTradeManager() : m_trade(NULL), m_magic(0), m_deviation(0) {} ~CTradeManager() {} bool Init(int magic, int deviation) { m_trade = new CTrade(); if(m_trade == NULL) return false; m_magic = magic; m_deviation = deviation; m_trade.SetExpertMagicNumber(m_magic); m_trade.SetDeviationInPoints(m_deviation); m_trade.LogLevel(LOG_LEVEL_ERRORS); // Match reference: log errors only // Cache symbol props (no filling set - default to broker/symbol) m_minVolume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); m_maxVolume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); m_volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); m_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); m_point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); Print("TradeManager initialized: Magic=", m_magic, " Deviation=", m_deviation, "pts (default filling)"); return true; } void Deinit() { if(m_trade != NULL) { delete m_trade; m_trade = NULL; } }
6. 取引量の安全性と口座検証
取引をおこなう前に、堅牢なEAは必ず2つの質問に答える必要があります。「この銘柄に対してこの取引量は有効か?」そして「この口座は健全な状態にあるか?」です。IsValidVolumeとNormalizeVolumeは、要求された取引量をブローカーのSYMBOL_VOLUME_STEP制約に合わせます。ValidateAccountは、口座の残高、証拠金、余剰証拠金が無意味な値になっていないことを確認します。
bool IsValidVolume(double volume) { if(volume < m_minVolume || volume > m_maxVolume) return false; double normalized = NormalizeDouble(volume / m_volumeStep, 0) * m_volumeStep; return (MathAbs(volume - normalized) < m_point); } double NormalizeVolume(double volume) { return NormalizeDouble( MathMax(m_minVolume, MathMin(m_maxVolume, NormalizeDouble(volume / m_volumeStep, 0) * m_volumeStep)), 2); } bool ValidateAccount() { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); if(balance <= 0 || equity <= 0 || freeMargin < 0) { Print("TradeManager: Invalid account state - Balance=", balance, " Equity=", equity, " FreeMargin=", freeMargin); return false; } Print("TradeManager: Account validated - Balance=", balance, " FreeMargin=", freeMargin); return true; }
7. ExecuteBuy:SL/TPロジック、スプレッド認識、エラー報告
ExecuteBuyは、すべての要素が統合される箇所です。口座の検証をおこない、取引量を正規化・確認し、最新のシンボル価格を取得した上で、スプレッド、ピップサイズ、最小ストップレベルを考慮したストップロスとテイクプロフィットを計算します。もしストップロスのみが指定され、テイクプロフィットがゼロの場合は、EAが自動的に1:2のリスクリワードレシオでテイクプロフィットを算出します。これにより、フレームワークに直接、良い取引習慣を組み込むことができます。そして、何か問題が発生した場合は、ResultRetcodeDescription()とGetLastError()の両方を出力して、詳細なエラー報告をおこないます。
bool ExecuteBuy(double volume, double sl = 0, double tp = 0, string comment = "") { if(!ValidateAccount()) return false; volume = NormalizeVolume(volume); if(!IsValidVolume(volume)) { Print("TradeManager: Invalid volume ", volume); return false; } gSymbolInfo.Name(_Symbol); gSymbolInfo.RefreshRates(); double ask = gSymbolInfo.Ask(); double bid = gSymbolInfo.Bid(); double spread = ask - bid; double price = NormalizeDouble(ask, m_digits); double stoploss = 0.0; double takeprofit = 0.0; double pipValue = (m_digits == 3 || m_digits == 5) ? m_point * 10 : m_point; // --- SL/TP logic (spread-aware, 1:2 RR auto if desired) --- if(InpStopLossPips > 0) { if(spread >= InpStopLossPips * m_point) { PrintFormat("StopLoss (%d points) <= current spread = %.0f points. Spread value will be used", InpStopLossPips, spread / m_point); stoploss = NormalizeDouble(price - spread, m_digits); } else { stoploss = NormalizeDouble(price - InpStopLossPips * pipValue, m_digits); } if(InpTakeProfitPips == 0) { takeprofit = NormalizeDouble(price + (InpStopLossPips * 2 * pipValue), m_digits); Print("TradeManager: Auto-set TP for 1:2 RR: ", takeprofit); } else { if(spread >= InpTakeProfitPips * m_point) { PrintFormat("TakeProfit (%d points) < current spread = %.0f points. Spread value will be used", InpTakeProfitPips, spread / m_point); takeprofit = NormalizeDouble(price + spread, m_digits); } else { takeprofit = NormalizeDouble(price + InpTakeProfitPips * pipValue, m_digits); } } } else if(InpTakeProfitPips > 0) { if(spread >= InpTakeProfitPips * m_point) { PrintFormat("TakeProfit (%d points) < current spread = %.0f points. Spread value will be used", InpTakeProfitPips, spread / m_point); takeprofit = NormalizeDouble(price + spread, m_digits); } else { takeprofit = NormalizeDouble(price + InpTakeProfitPips * pipValue, m_digits); } } // Directional sanity checks if(stoploss > 0 && stoploss >= price) { Print("TradeManager: Invalid SL for BUY - resetting to 0"); stoploss = 0; } if(takeprofit > 0 && takeprofit <= price) { Print("TradeManager: Invalid TP for BUY - resetting to 0"); takeprofit = 0; } // Respect broker minimum stop levels long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); if(stopsLevel > 0) { double minDist = stopsLevel * m_point; if(stoploss > 0 && (price - stoploss) < minDist) { stoploss = NormalizeDouble(price - minDist, m_digits); Print("TradeManager: SL adjusted to min dist: ", stoploss); } if(takeprofit > 0 && (takeprofit - price) < minDist) { takeprofit = NormalizeDouble(price + minDist, m_digits); Print("TradeManager: TP adjusted to min dist: ", takeprofit); } } Print("TradeManager: Executing BUY - Entry=", price, " Vol=", volume, " SL=", stoploss, " TP=", takeprofit); ResetLastError(); bool result = m_trade.Buy(volume, _Symbol, price, stoploss, takeprofit, comment); if(!result) { uint retcode = m_trade.ResultRetcode(); string ret_desc = m_trade.ResultRetcodeDescription(); PrintFormat("Failed %s buy %G at %G (sl=%G tp=%G) Retcode=%u (%s) MQL Error=%d", _Symbol, volume, price, stoploss, takeprofit, retcode, ret_desc, GetLastError()); m_trade.PrintResult(); Print(" "); } else { Print("TradeManager: BUY success - Deal=", m_trade.ResultDeal(), " Price=", m_trade.ResultPrice()); } return result; } };
8. 初期化:時間フィルタ、ビジュアライザー、取引マネージャー、EMAの接続
OnInitは、EAが「動き出す」箇所です。CSessionVisualizerをgCTXに接続し、GMTオフセットを設定することで、ビジュアル表示とロジックの時間が一致するようにします。また、CTradeManagerをマジックナンバーと許容スリッページで初期化します。口座の検証もおこない(特にストラテジーテスターで有用です)、シグナルロジックを駆動するためのMAハンドルも作成します。表示されるメッセージは意図的で、ユーザーにEAが「準備完了」であることだけでなく、使用されているリスクモデルや設定内容も知らせます。
// Global Trade Manager CTradeManager gTradeMgr; //+------------------------------------------------------------------+ //| OnInit | //+------------------------------------------------------------------+ int OnInit() { Print("=== PIONEER EA INITIALIZATION (v2.11) ==="); // Time filter setup gCTX.AttachVisualizer(gSV); gCTX.SetGMTOffset(InpGMTOffsetHours); if(InpDrawSessions) gSV.RefreshSessions(InpLookbackDays); // Initialize Trade Manager if(!gTradeMgr.Init(InpMagicNumber, InpDeviationPips)) { Print("FAIL: TradeManager initialization failed"); return INIT_FAILED; } // Validate account early if(!gTradeMgr.ValidateAccount()) { Print("FAIL: Account validation failed - Check deposit in tester"); return INIT_FAILED; } // MA indicators gFastMAHandle = iMA(_Symbol, _Period, 9, 0, MODE_EMA, PRICE_CLOSE); gSlowMAHandle = iMA(_Symbol, _Period, 21, 0, MODE_EMA, PRICE_CLOSE); if(gFastMAHandle == INVALID_HANDLE || gSlowMAHandle == INVALID_HANDLE) { Print("FAIL: MA handles creation failed"); return INIT_FAILED; } Print("SUCCESS: Pioneer EA ready - Clean CTrade integration active (default filling)"); Print("RR Logic: SL=", InpStopLossPips, " pips; TP=", (InpStopLossPips > 0 && InpTakeProfitPips == 0 ? InpStopLossPips * 2 : InpTakeProfitPips), " pips (1:2 auto if TP=0)"); return INIT_SUCCEEDED; }
9. クリーンアップ:リソース管理
OnDeinitでは、インジケーターハンドルを解放し、取引マネージャーを終了処理し、セッションオブジェクトをクリアします。このEAは、自分の使用したリソースをきちんと後片付けします。
//+------------------------------------------------------------------+ //| OnDeinit | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(gFastMAHandle != INVALID_HANDLE) IndicatorRelease(gFastMAHandle); if(gSlowMAHandle != INVALID_HANDLE) IndicatorRelease(gSlowMAHandle); gTradeMgr.Deinit(); gSV.ClearAll(); }
10. エントリーロジック:EMAクロスオーバーと1ローソク足1シグナル制御
シグナルロジックは意図的にシンプルで、9EMAが21EMAを上抜けることを条件としています。重要なのは、その実装方法です。各MAバッファから2つの値を取得し、過去値と現在値を比較してクロスを検出します。その後、gLastSignalBarで保護することで、同じローソク足で複数のエントリーが発生するのを防ぎます。
//+------------------------------------------------------------------+ //| Signal Detection: EMA Crossover | //+------------------------------------------------------------------+ bool EntrySignalDetected() { double fast[2], slow[2]; if(CopyBuffer(gFastMAHandle, 0, 0, 2, fast) != 2 || CopyBuffer(gSlowMAHandle, 0, 0, 2, slow) != 2) return false; bool crossover = (fast[1] <= slow[1] && fast[0] > slow[0]); if(!crossover) return false; datetime barTime = iTime(_Symbol, _Period, 0); if(barTime == gLastSignalBar) return false; gLastSignalBar = barTime; return true; }
11. マジックナンバーと銘柄によるポジションフィルタリング
HasOpenPositionは、保有中のポジションをスキャンし、銘柄とマジックナンバーの両方を確認します。これにより、EAは自分自身の取引にのみ反応し、他のシステムには干渉しないようになります。戦略を分離するために、常にマジックナンバーを利用することが重要です。口座上のすべてのポジションを「自分のもの」と仮定してはいけません。
//+------------------------------------------------------------------+ //| Position Check | //+------------------------------------------------------------------+ bool HasOpenPosition() { int total = PositionsTotal(); for(int i = 0; i < total; i++) { if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) return true; } return false; }
12. OnTick:まず時間フィルタ、その後ロジック
OnTickの本体は意図的に短く読みやすくなっており、これはすべての抽象化の恩恵です。まず、セッションのビジュアルを更新します(有効な場合)。次に、時間フィルタのゲートを通します。IsTradingAllowed(gCTX)がfalseを返した場合、EAは単に「Trading OFF」と表示して終了します。時間条件が満たされた場合にのみ、ポジションがないこととEMAシグナルがあることを確認します。両方が成立すれば、実行をgTradeMgr.ExecuteBuyに渡します。
//+------------------------------------------------------------------+ //| OnTick | //+------------------------------------------------------------------+ void OnTick() { if(InpDrawSessions) gSV.RefreshSessions(InpLookbackDays); if(!IsTradingAllowed(gCTX)) { Comment("Pioneer EA: Trading OFF"); return; } Comment("Pioneer EA: Trading ON | Positions: ", PositionsTotal()); if(!HasOpenPosition() && EntrySignalDetected()) { Print("=== SIGNAL: EMA Crossover Detected ==="); gTradeMgr.ExecuteBuy(InpLotSize, 0, 0, "Pioneer Buy"); } }
TimeFiltersを利用したサンプルインジケーター
1. インジケーター概要:時間フィルタEAの視覚的補助
まず、チャートウィンドウに表示されるインジケーターを宣言し、プロットを2つ用意します。1つは強気シグナル用、もう1つは弱気シグナル用です。プロパティ設定により、MetaTrader 5に対して、別ウィンドウではなく価格上に直接矢印を描画することを指示しています。
// ============================================================================ // Author: Clemence Benjamin // File: TimeFilteredSignal.mq5 // Purpose: Time-filtered RSI signal + alerts indicator // Notes : Uses large Wingdings arrows for visual clarity // Signals only appear when IsTradingAllowed(iCTX) == true // ============================================================================ #property strict #property indicator_chart_window // We use two plots: one for bullish arrows, one for bearish arrows #property indicator_plots 2 #property indicator_buffers 2 // --- Plot 1: Bullish RSI signal (up arrow) --- #property indicator_type1 DRAW_ARROW #property indicator_color1 clrLime #property indicator_width1 2 #property indicator_label1 "TF_RSI_Bull" // --- Plot 2: Bearish RSI signal (down arrow) --- #property indicator_type2 DRAW_ARROW #property indicator_color2 clrRed #property indicator_width2 2 #property indicator_label2 "TF_RSI_Bear"
2. 時間インフラの再利用:セッションとフィルタ、再発明せず
ここでは、EAと同じビルディングブロック、SessionVisualizer.mqhとTimeFilters.mqhをインクルードします。これが重要なパターンです。一元化された時間ポリシーを作り、多くの利用者(EA、インジケーター、ダッシュボード)が共有するという構造です。また、セッション描画用の入力やRSIの設定、アラート挙動も宣言します。セッションのオン/オフ切り替え、RSIレベルの調整、通知の多さや静かさの選択も可能です。
#include <SessionVisualizer.mqh> #include <TimeFilters.mqh> // --------------------------- Inputs ----------------------------------------- // Visual session context (indicator-side) input bool InpDrawSessions_i = true; input int InpGMTOffsetHours_i = 0; input int InpLookbackDays_i = 5; // RSI configuration input int InpRSIPeriod = 14; input int InpRSIOverbought = 70; input int InpRSIOversold = 30; // Alert behavior input bool InpAlertOnCross = true; // enable popup alert on RSI cross input bool InpSendPush = false; // send mobile push input bool InpSendEmail = false; // send email input string InpAlertPrefix = "TF-RSI"; // prefix tag in messages
3. 共有コンテキストとバッファ:1つの時間の頭脳、2つの矢印
このインジケーターは、先ほど設計したCTimeFilterContextを使用し、自身のCSessionVisualizerインスタンスを指し示します。これにより、EAと実装の一貫性が保たれ、どちらも「許可された時間」という同じ概念を参照します。さらに、2つのバッファを割り当てます。BuffUpは強気の矢印用、BuffDnは弱気の矢印用です。この分離により、チャート上で即座に解釈できます。緑の上矢印はフィルタ済み強気イベントを、赤の下矢印はフィルタ済み弱気イベントを示します。
// --------------------------- Globals ---------------------------------------- CSessionVisualizer iSV("TFI_SESS_"); CTimeFilterContext iCTX; // Two buffers: one for bullish arrows, one for bearish arrows double BuffUp[]; double BuffDn[]; int rsiHandle = INVALID_HANDLE; datetime gLastRSIAlertBarTime = 0; // avoid duplicate alerts per bar
4. アラートヘルパー:1つのメッセージ、複数チャネル
ループ内にAlert()を散りばめるのではなく、通知ロジックをFireRSIAlert.に集約します。これにより、シグナルコードはクリーンに保たれ、拡張(接頭辞の変更、フォーマット調整、追加データの送信など)も簡単におこなえます。このパターンは拡張性があります。現在はRSIのメッセージですが、明日は「オーダーフロークラスタ検出」や「カスタム相場変化の確認」といった通知にも、インジケーター全体をリファクタリングせずに対応可能です。
// --------------------------- Alert helper ----------------------------------- void FireRSIAlert(const string direction, const double rsiValue, const datetime barTime) { string timeStr = TimeToString(barTime, TIME_DATE|TIME_SECONDS); string msg = StringFormat("%s | %s | %s | RSI=%.2f | Time=%s (inside allowed window)", InpAlertPrefix, _Symbol, direction, rsiValue, timeStr); Alert(msg); if(InpSendPush) SendNotification(msg); if(InpSendEmail) SendMail(InpAlertPrefix + " " + _Symbol, msg); Print("TimeFilteredSignal: ", msg); }
5. OnInit:矢印をバッファに紐付け、時間をセッションに接続
OnInitでは、構造的な配線をおこないます。
- 2つのプロットにバッファを割り当て、
- Wingdingsの矢印コード(上向き233、下向き234)を選択します。
- 次に、ビジュアライザーをコンテキストに接続し、GMTオフセットを適用します。
- 必要に応じてセッションを描画し、
- RSIハンドルを作成してバッファを初期化します。
コアリソース(RSIや銘柄情報など)が作成できない場合は、インジケーターは速やかに失敗する(fail fast)ように設計されています。
// --------------------------- OnInit ----------------------------------------- int OnInit() { // Bind buffers SetIndexBuffer(0, BuffUp, INDICATOR_DATA); SetIndexBuffer(1, BuffDn, INDICATOR_DATA); // Use large Wingdings arrows: // 233 = up arrow, 234 = down arrow PlotIndexSetInteger(0, PLOT_ARROW, 233); // Bullish arrow up PlotIndexSetInteger(1, PLOT_ARROW, 234); // Bearish arrow down // Time filter context iCTX.AttachVisualizer(iSV); iCTX.SetGMTOffset(InpGMTOffsetHours_i); if(InpDrawSessions_i) iSV.RefreshSessions(InpLookbackDays_i); // Create RSI handle rsiHandle = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE); if(rsiHandle == INVALID_HANDLE) { Print("TimeFilteredSignal: Failed to create RSI handle. Error = ", GetLastError()); return(INIT_FAILED); } // Initialize buffers as empty ArrayInitialize(BuffUp, EMPTY_VALUE); ArrayInitialize(BuffDn, EMPTY_VALUE); return(INIT_SUCCEEDED); }
6. OnCalculate:時間を意識したRSIクロスと矢印表示
ここが、このインジケーターがその名に値する部分です。バーを順に処理しますが、ロジックは意図的にシンプルかつ層構造になっています。
- まず、デフォルトで矢印をクリアします(EMPTY_VALUEで非表示にします)。
- 新しいバーが到着した場合は、必要に応じてセッションのビジュアルを更新します。
- RSIの値はCopyBufferを通じて取得します。
- まず、IsTradingAllowed(iCTX)をチェックし、時間フィルタにより取引不可の場合はスキップします。
- 次にRSIクロスを検出します。
- 強気の場合は売られすぎゾーンから上方向に抜けた場合にバー下に緑の上矢印を描画します。
- 弱気の場合は買われすぎゾーンから下方向に抜けた場合にバー上に赤の下矢印を描画します。
最新バーに対してのみ、gLastRSIAlertBarTimeを使ってアラートを一度だけ発生させます。
このシグナルは単なる「RSIクロス」ではなく、「プロフェッショナルな取引時間、セッション、そして将来的なイベントブロックを尊重したRSIクロス」として機能します。
// --------------------------- OnCalculate ------------------------------------ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total <= 2 || rsiHandle == INVALID_HANDLE) return(rates_total); // Refresh session drawings when new bars appear if(InpDrawSessions_i && rates_total != prev_calculated) iSV.RefreshSessions(InpLookbackDays_i); // Prepare RSI buffer static double rsiBuffer[]; ArrayResize(rsiBuffer, rates_total); int copied = CopyBuffer(rsiHandle, 0, 0, rates_total, rsiBuffer); if(copied <= 0) return(prev_calculated > 0 ? prev_calculated : rates_total); // Recalc from prev-1 so last bar updates smoothly int start = (prev_calculated > 1 ? prev_calculated - 1 : 1); for(int i = start; i < rates_total; ++i) { // Default: hide both arrows on this bar BuffUp[i] = EMPTY_VALUE; BuffDn[i] = EMPTY_VALUE; // Respect global time filters (same logic as EA) if(!IsTradingAllowed(iCTX)) continue; double rsi_prev = rsiBuffer[i - 1]; double rsi_curr = rsiBuffer[i]; bool bullCross = (rsi_prev < InpRSIOversold && rsi_curr >= InpRSIOversold); bool bearCross = (rsi_prev > InpRSIOverbought && rsi_curr <= InpRSIOverbought); // Bullish RSI recovery: big green up arrow below bar if(bullCross) { BuffUp[i] = low[i] - (_Point * 5); if(InpAlertOnCross && i == rates_total - 1 && time[i] != gLastRSIAlertBarTime) { FireRSIAlert("RSI cross UP from oversold", rsi_curr, time[i]); gLastRSIAlertBarTime = time[i]; } } // Bearish RSI rejection: big red down arrow above bar if(bearCross) { BuffDn[i] = high[i] + (_Point * 5); if(InpAlertOnCross && i == rates_total - 1 && time[i] != gLastRSIAlertBarTime) { FireRSIAlert("RSI cross DOWN from overbought", rsi_curr, time[i]); gLastRSIAlertBarTime = time[i]; } } } return(rates_total); }
拡張性:RSIを超えて
RSIは単なるプレースホルダーに過ぎません。構築したこの構造は、ほとんど変更なしで任意のシグナルエンジンをサポートします。
RSIブロックの代わりに次のようなものを組み込むことが可能です。
- 移動平均クロスクラスタ
- ブレイクアウト条件(セッションの高値/安値、VWAPからの乖離)
- ローソク足パターン(ピンバー、包み足)(ロンドン時間のみ)
- ボラティリティ体制(ATRフィルタ)
- 注文板やプロキシ指標、機械学習の出力など、何でも
その他はすべてそのまま維持できます。
- IsTradingAllowed(iCTX)を汎用の時間ゲートとして利用
- チャート上の矢印(またはマーカー)
- FireRSIAlertを汎用化してFireSignalAlertに改名すれば、任意の通知も可能
この例では、RSIクロスを使用しましたが、それは認識が簡単だからです。本当の価値はオシレーターの選択にあるのではなく、フレームワーク自体にあります。どんなシグナルでも同じTimeFilterレイヤーを通して扱うことができ、戦略が発言してよいタイミングを定義し、その上で何を伝えるかを差し込むことができるのです。
テスト
ソースファイルのコンパイルに成功した後、TimeFilteredEAとTimeFilteredSignalインジケーターの両方をストラテジーテスターに配置し、フレームワーク全体の挙動を検証しました。テスト実行はやや遅く感じられました。これは、時間フィルタの多層処理、セッション描画、シグナルロジックが同時に動作していることを考えれば驚くべきことではありません。しかし、テストは確実に完了し、明確で解釈可能な結果を出力しました。以下のスクリーンショットは、取引やRSIベースの矢印が許可された時間ウィンドウ内でのみトリガーされた様子を示しており、設計通りに動作していることが分かります。

図1:ストラテジーテスターでTimeFilteredEAをテストする

図2:TimeFilteredSignalインジケーターのテスト
結論
本プロジェクトで開発したフレームワークのような構造化されたMQL5時間フィルタを実装することで、取引システムが動作してよいタイミングを確実に制御することは十分可能です。条件を無作為なif文に散りばめるのではなく、クリーンな抽象化、共有コンテキスト、セッション対応ユーティリティを用いることで、挙動を予測可能かつ再利用可能にしました。ロジックを明確な参照点(マーケットセッション、時計範囲、必要に応じて経済カレンダーのウィンドウ)に紐付けることで、最小限の労力で正確な取引およびシグナル時間を定義できます。
これらの時間ウィンドウが設定されると、シグナル側で本当の創造性が発揮されます。RSI、移動平均、構造のブレイク、ボラティリティフィルタ、オーダーフローなど、どんな戦略ロジックも24時間・週5日監視するのではなく、規律あるスケジュールに差し込むことが可能になります。このアプローチにより、連続的でコンテキストを無視した監視によるノイズが直接減少し、実行品質の向上に集中できるようになります。下のまとめ表では、本稿で学んだ主要な教訓を強調しており、全文ソースコードは記事末に添付しています。これにより、学習、改変、さらに自身のアイデアによる拡張が可能です。
重要なポイント
| 重要なポイント | 説明 |
|---|---|
| 1. 時間フィルタロジックの集中化 | 専用のTimeFilterレイヤーを実装することで(無作為なif文に散らばらせるのではなく)、複数のEAやインジケーター間で同じセッション、時計、イベントルールを再利用しやすくなり、挙動の一貫性が確保され、メンテナンスも容易になります。 |
| 2. グローバル変数の乱用を避け、コンテキストオブジェクトを使用する | CTimeFilterContextパターンは、GMTオフセットやビジュアライザーへのリンクなどの設定をひとつのオブジェクトにまとめる方法を示します。これにより、グローバル依存を減らし、コードベースをモジュール化、テスト可能、かつスケーラブルに保つことができます。 |
| 3. シグナルロジックと実行ロジックを分離する | 取引実行をCTradeManagerに、シグナル検出を専用関数やインジケーターに配置することで、「何がトリガーになるか」と「どのように注文が送信されるか」を明確に分離できます。これにより、可読性、デバッグ容易性、再利用性が向上します。 |
| 4. 視覚的なフィードバックと診断を考慮した設計 | SessionVisualizerとWingdings矢印シグナルにより、時間フィルタやシグナルが期待通りに動作しているかを即座に視覚的に確認できます。これは複雑なロジックのデバッグや、トレーダーがシステムに信頼感を持つために不可欠です。 |
| 5.拡張可能なシグナルフレームワークを構築する | RSIを時間フィルタ付きフレームワーク内のプラグインモジュールとして使用する例は、スケーラブルな設計を示しています。将来的には、プライスアクション、移動平均クロス、ボラティリティフィルタ、カスタムモデルなど、どんな手法も同じIsTradingAllowed()ゲートの背後に統合でき、コード変更は最小限で済みます。 |
添付ファイル
| ソースファイル | バージョン | 説明 |
|---|---|---|
| SessionVisualizer.mqh | 1.02 | モジュール化されたセッションマッピングおよび描画ライブラリ。主要なFX取引セッションをチャート上に描画し、高レベルツール用に再利用可能な時間ウィンドウ参照を提供します。 |
| TimeFilters.mqh | 1.00 | コアとなる時間フィルタフレームワーク。時計、セッション、(拡張可能な)イベントベースのチェックを、統一されたIsTradingAllowed()インターフェースと共有コンテキストオブジェクトを通じて提供します。 |
| TimeFilteredEA.mq5 | 1.0 | 例示用のEA。TimeFiltersとSessionVisualizerを利用し、EMAベースのエントリーを許可された時間ウィンドウ内でのみ実行する構造化された取引管理をおこないます。 |
| TimeFilteredSignal.mq5 | 1.0 | チャートインジケーター。大きなWingdings矢印を描画し、RSIベースのアラートを定義された時間フィルタウィンドウ内でのみ生成します。視覚的かつ拡張可能なシグナルレイヤーとして機能します。 |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20037
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5 MVCパラダイムのテーブルのビューコンポーネント:基本グラフィック要素
MQL5入門(第26回):サポートおよびレジスタンスゾーンを使ったEAの構築
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
初心者からエキスパートへ:予測価格経路
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索