English Русский Deutsch
preview
初心者からエキスパートへ:NFP発表後の市場取引におけるフィボナッチ戦略の実装

初心者からエキスパートへ:NFP発表後の市場取引におけるフィボナッチ戦略の実装

MetaTrader 5 |
82 0
Clemence Benjamin
Clemence Benjamin

内容


はじめに

主要経済指標、たとえばNFP(非農業部門雇用者数)の発表の瞬間を逃すと、多くのトレーダーはFOMO(取り残される恐怖)に駆られます。これは、動きがすでに終了しているにもかかわらず、トレーダーが急いで値動きに追随しようとすることによって起こります。多くの場合、最初のスパイクは1分未満で終了し、市場の有利な方向に大きく価格が動きます。このような状況で遅れて参入することは極めてリスクが高く、急激な反転によって大きな損失につながる可能性があります。

一方で、プロのトレーダーがこれらのスパイクを追いかけることはありません。彼らは事前に早期かつ安全なエントリーポイントを準備するか、市場がリトレースして新たな機会を提供するまで忍耐強く待ちます。彼らの優位性は、フィボナッチリトレースメントの原則を理解し、適切な条件が整うまで待つという規律を習得している点にあります。

本ディスカッションでは、経済指標発表による市場変動後の取引の課題に対処するため、フィボナッチ原則をアルゴリズム的に適用する方法について説明します。まず基礎として、フィボナッチについて初めての方にも理解できるよう簡単に紹介した後、戦略の設計および実装に進みます。

フィボナッチリトレースメントの概要

フィボナッチ数列は、数世紀前にイタリアの数学者レオナルド・フィボナッチによって提唱され、現代のテクニカル分析において最も影響力のあるツールの一つとなっています。数学上では、この数列は非常に単純で、各数が直前の2つの数の和となります(1,1,2,3,5,8,13,…)。しかし、その現れ方は自然界、建築、人間行動に至るまで幅広く確認され、世代を超えて学者たちを魅了してきました。

金融市場においては、フィボナッチ数列はフィボナッチリトレースメントレベルとして応用されます。これは、トレーダーが市場の調整局面における潜在的な反転または継続ゾーンを特定するための手法です。最も一般的に使用されるリトレースメント比率は23.6%、38.2%、50%、61.8%、78.6%で、いずれもフィボナッチ数列内の関係性から導かれています。これらのレベルは心理的な目安として機能し、トレーダーは価格が一時的に止まる、反発する、または反転することを予測します。

リトレースメントの原則は、市場は決して一直線には動かないという点にあります。強い上昇または下降の後、価格は通常一時的に押し戻されてからトレンドを再開します。フィボナッチリトレースメントは、その押し戻しの深さを測る枠組みを提供します。具体例としては次の通りです。

  • 38.2%リトレースメント:強いトレンドにおける浅い押し戻しを示唆する
  • 50%リトレースメント(正確にはフィボナッチ数ではない):中間の修正として広く使用される
  • 61.8%リトレースメント(黄金比として知られる):大きな反転が発生しやすい最も重要なレベルとされる

実務では、フィボナッチレベルは単独で使用されることはほとんどありません。価格の動き、サポートとレジスタンス、移動平均線、あるいはニュース直後のリトレースメントのようなイベント主導の市場動向と組み合わせることで、より有効に機能します。

本戦略では、NFP発表後の取引に焦点を当て、フィボナッチリトレースメントを用いて初動スパイクのノイズを除外し、構造化された高確率なエントリーポイントを導きます。トレーダーはFOMOに駆られるのではなく、「押し戻しを待ってからエントリーする」という手法を学ぶことができます。フィボナッチに関するさらに詳細な情報は、MQL5コミュニティプラットフォームでも入手可能です。

MetaTrader 5ターミナルのアクセシビリティ

MetaTrader 5の標準インターフェースでは、チャート作業ツールボックスが優先的に表示され、すぐに利用可能です。最もよく使用される分析オブジェクトはあらかじめ読み込まれており、チャート上にドラッグするだけで配置できます。描画ツール(例:フィボナッチリトレースメントオブジェクト)は、スイングポイントに頂点をドラッグして価格レベルを視覚的に表示することが可能です。トレーダーはこれらのオブジェクトを用いて、過去に価格がレベルにどのように反応したかを確認したり、将来のリトレースメントにおける反応ゾーンを予測したりします。

実践的なポイント

  • ツールを明確なスイングハイとスイングロー(またはその逆)にドラッグし、有意な参照点にリトレースメントを固定します。
  • オブジェクトの頂点ハンドルを使って配置を微調整し、ローソク足の終値やヒゲにレベルを合わせます。ルールはご自身の取引スタイルに応じて選択します。
  • よく使うテンプレートやプロファイルを保存しておくことで、フィボナッチ設定やチャートレイアウトをチャート間で即座に呼び出すことができます。

下図1は、MetaTrader 5ターミナル上に描画したフィボナッチリトレースメントの例です。

MetaTrader 5ターミナルでフィボナッチリトレースメントツールを描画した例

図1:MetaTrader 5ターミナルでフィボナッチリトレースメントツールを描画した例

2025年9月5日非農業部門雇用者数(NFP)発表を用いた概念検証

2025年9月5日のNFP発表後の価格動向を手動でレビューした結果、フィボナッチリトレースメントレベルは、指標発表後のエントリーを意味のある形でガイドできることが確認されました。添付の図では、フィボナッチリトレースメントツールを用いて、価格が初動スパイク後にどのレベルをテストし尊重したかを示しています。これにより、高確率の再エントリーポイントとノイズを区別することが可能です。

これらの観察結果をもとに、リトレースメントバンドおよび直近の基準価格(スパイク前の基準ラインとスパイクの極値)に基づいた、より規律ある注文配置およびリスク管理ルールを設計できます。明確化のため、米ドルを含む3つの通貨ペアチャートを分析しました。USDペアは、主要な米国経済指標発表時により強く、一貫した値動きを示す傾向があります。

本研究は、執筆時点で利用可能な最新のNFPリリースを参考にしています。ケーススタディとして、USD中心の3ペア(GBP/USD、USD/JPY、EUR/USD)を分析し、その後この考えをアルゴリズムへと変換しました。以下に各例のチャートスクリーンショットを示します。

NFP後のEUR/USD 05/09/25

図2:NFP発表後の価格動向(EUR/USD M5)

図2(EUR/USD、M5)では、価格がリトレースメントの法則に従っていることが明確に確認できます。マーカーAおよびBは、スパイク前の基準ラインとNFP発表によるスパイク極値を示しています。フィボナッチリトレースメントは、リリース後の最初のM5ローソク足でA(基準ライン)からB(スパイク高値)に引かれました。価格はその後、フィボナッチバンドをテストするために押し戻され、リトレースメントレベルは明確に尊重されています。この例では50%レベルが到達点となり、有意な反応ポイントとして機能しました。その後、価格は61.8%のフィボナッチリトレースメントをテストし、わずかに上抜けましたが、100%の基準ラインまでの完全なリトレースメントには至りませんでした。その後、勢いが戻り、市場はスパイク極値を上回る新高値を形成し、上昇継続を確認できました。

図3:NFP発表後の価格動向(GBP/USD M5)

図3:NFP発表後の価格動向(GBP/USD M5)

EUR/USDはGBP/USDと強く相関しており、GBP/USDも同様の上昇反応を示しました(完全に同一ではありません)。図3では、図2と同じフィボナッチオーバーレイを使用しており、GBP/USDが発表後にどのように反応したかを示しています。このことは、異なるペア間でもリトレースメント挙動の一貫性を裏付けます。

USDJPY、M5

図4:NFP発表後の価格動向(USD/JPY M5)

図4(USD/JPY)では、NFP発表後に明確な下落反応が見られます。初動の下落(A→B)の後、価格はおおよそ50%のフィボナッチリトレースメントレベルまで押し戻され、その後再び下落し新安値を形成しました。以上の3つのチャート(EUR/USD、GBP/USD、USD/JPY)から、ニューススパイク後に価格がフィボナッチゾーンまで戻る確率が高いことが示されています。方向性は通貨ペアによって異なる場合もありますが、リトレースメントの概念は一貫して観察されました。

示唆と高レベルのアプローチ(コーディング前)

EUR/USD、GBP/USD、USD/JPYの3つの例を踏まえ、以下のようにアルゴリズム化に向けたコンパクトな高レベル計画を定義できます。

定義
  • P_base:スパイク前の基準ライン(急騰・急落が始まった価格)
  • P_spike:スパイク極値(上昇スパイクの場合は最高値、下降スパイクの場合は最安値)
  • スパイクの方向:P_spike > P_baseの場合は上昇スパイク(強気)、P_spike < P_baseの場合は下降スパイク(弱気)
エントリーロジック(両方向)
1. イベント後、最初の基準ローソク足完了後に、P_baseとP_spike間にフィボナッチリトレースメントレベルを描画します。2. 選択した確認条件(ローソク足終値、ヒゲの反発、ティックボリューム正規化など)を用いて、ターゲットゾーン(例:38.2〜61.8%)への構造的リトレースメントを待ちます。3. 上昇スパイクの場合(価格が上昇後リトレース)
  • エントリー:価格が選択したフィボナッチゾーンにリトレースし、確認条件が満たされたら買い(ロング)でエントリー
  • 利食い(TP):主要TPはP_spike(スパイク極値)。拡張TPはフィボナッチエクステンションやATRの倍率を使用可能
  • 損切り(SL):P_baseまたはその直下に設定。マイクロノイズで不必要に損切されないよう、スプレッド+αピップのバッファを追加。
4. 下降スパイクの場合(価格が下落後リトレース)
  • エントリー:価格が選択したフィボナッチゾーンにリトレースし、確認条件が満たされたら売り(ショート)でエントリー
  • 利確(TP):主要TPはP_spike(下降スパイク極値)
  • 損切り(SL):P_baseまたはその直上に設定し、少量のバッファを追加
マッピングの理由
  • P_spikeをTPにする理由:最新の顕著な極値での再テストを狙うため(「スパイクへのリターン」をキャプチャ)
  • P_baseをSLにする理由:急速な値動きの起点を論理的な無効化ポイントとし、価格が基準ラインを越えた場合は、スパイク後の再エントリー仮説が崩れると判断する

実務上の調整とリスク管理

SLは小さな固定バッファ(例:数ピップス、またはスプレッド×1.5)で調整します。ロットサイズは口座リスク% × SL〜エントリー距離で計算し、絶対リスクを小さく抑えます(目標0.5%)。P_baseが遠すぎる場合は、代替のタイトSL(次のFibレベルやATR(5分)×係数)をパラメータとして選択可能にします。EAでは、P_spikeで部分TP、残ポジションのトレーリング、M分後の古いリミット注文自動キャンセル、相関ペア確認のオン/オフ切り替えをサポートします。

フィボナッチを用いた戦略概念

図5:NFP後フィボナッチ再エントリ戦略の概念図

図5は、戦略を概念的に視覚化したものです。上昇セットアップでは、SLはA(スパイク基準ライン)直下、TPはB(スパイク極値)上に設定します。下降セットアップでは、SLはA直上、TPはB下に設定(典型例:図4 USD/JPY)します。SLには小さなバッファ(数ピップまたはスプレッドx1.5)を加えて、ノイズによる早期損切を回避します。

エッジケース

価格が完全に100% (P_base)まで戻った上でさらに継続する場合は、リトレースが完了し再エントリー仮説が崩れたものと判断して当該ポジション/戦略をキャンセルすることを検討します。

スパイク方向が不明確、またはスイング幅 < 最小バー/ピップの場合は取引を中止します(ノイズが大きすぎるため)。


実装戦略

開発プロセスを簡素化するため、MetaEditor 5で専用のクラスヘッダを実装しました。このヘッダは、NFP発表のタイムスタンプ管理および、東部標準時(ET)・UTC・ブローカーサーバー時間間の正確な変換を担当します。これにより、DST(夏時間)処理、ETオフセット計算、変換ユーティリティを一元化し、他のモジュールはタイムゾーンを意識せずに利用できる設計となっています。

このヘッダを活用して、以下の機能を持つエキスパートアドバイザー(EA)を開発しました。

  • NFP発表後に確定した最初のM5ローソク足を検出する
  • そのローソク足を基準にフィボナッチリトレースメントを描画する
  • 38.2%、50.0%、61.8%レベルに指値注文を展開する
  • ブローカーに対する堅牢なチェック(最小ストップ距離、正規化、有効期限など)を適用する
  • チャートオブジェクトおよび指値注文のライフサイクルを管理する(約定または期限切れ時にクリーンアップ)

次のステップでは、開発者向けに詳細なコード解説をおこないます。まず、NFPTimeManagerヘッダについては、設計上の判断やAPI仕様、DSTおよびタイムゾーン処理のロジックを中心に解説します。続いて、EA本体については、データフロー、注文管理、そして安全性チェックの仕組みを順を追って説明します。これらの解説により、MQL5におけるシステムの設計、実装、およびテスト手法が明確に理解できる構成になっています。

手順1: NFPTimeManagerヘッダ

1.1. ファイルヘッダとメタデータ

このファイルは、標準的なヘッダブロックとMQL5の#property属性で始まります。ヘッダ部分では、著者情報やバージョン情報が記載されており、他の開発者や将来の自分がファイルの目的や由来をすぐに理解できるようになっています。また、#property属性を設定することで、MetaEditor 5上でMQL5ビルドシステムがメタデータを表示できるようになります。

//+------------------------------------------------------------------+ 
//|                                               NFPTimeManager.mqh  |
//|                        Copyright 2025, Clemence Benjamin  |
//|                                       https://www.mql5.com/ja/users/billionaire2024/seller|
//+------------------------------------------------------------------+
#property copyright "Clemence Benjamin"
#property version   "1.0"

1.2. クラス宣言とprivateタイムゾーンフィールド

NFPに関連するすべてのユーティリティは、CNFPTimeManagerクラス内にカプセル化しています。クラス内には、任意で設定可能なトレーダーのタイムゾーンとブローカー/サーバーのタイムゾーンを追跡する2つのprivate整数フィールドを持たせています。これらを内部に保持することで、変換関数の動作を決定的にし、タイムゾーン処理のロジックがコード全体に散在するのを防ぐ設計になっています。

class CNFPTimeManager
{
private:
   int      m_traderTimeZone; // Trader time zone offset from UTC (hours) - optional
   int      m_serverTimeZone; // Server time zone offset from UTC (hours)

1.3. GetETOffset:DSTを考慮した東部時間オフセットの計算

NFPは固定された東部時間(ET)で発表されます(08:30 ET)。GetETOffset関数は、指定された日付が東部標準時(UTC−5)か東部夏時間(UTC−4)かを判定してオフセットを計算します。米国のDSTルール(3月第2日曜日から11月第1日曜日)を使用しており、整数のUTCオフセットを返します。この仕組みにより、季節をまたぐカレンダー計算でも正確に日時を扱うことができます。

   // returns ET offset (-4 for EDT, -5 for EST) for a given datetime (trader time context)
   int GetETOffset(datetime time)
   {
      MqlDateTime dt;
      TimeToStruct(time, dt);
      int year = dt.year;

      // DST: 2nd Sunday of March to 1st Sunday of November (US rules)
      datetime dst_start = GetNthDayOfWeek(year, 3, 0, 2); // 2nd Sunday of March
      datetime dst_end   = GetNthDayOfWeek(year, 11, 0, 1); // 1st Sunday of November
      // Note: We interpret the DST boundaries as local US/Eastern dates at 02:00 local time,
      // but for our purpose using whole-day thresholds is acceptable (NFP at 8:30 ET).
      if(time >= dst_start && time < dst_end)
         return -4; // EDT
      return -5;    // EST
   }

1.4. GetNthDayOfWeek:安定した月次カレンダー計算

「3月の第2日曜日」のようなカレンダー算術は、意外とエラーが発生しやすいものです。GetNthDayOfWeek関数は、まず指定月の1日を基準とし、その日の曜日を取得して、目的のN番目の曜日までのオフセットを計算します。この関数は、要求された発生日の正確なdatetimeを返し、DST計算やNFPタイムスタンプ計算で再利用されます。

   // correctly compute the N-th day_of_week (0=Sunday..6=Saturday) in given month/year
   datetime GetNthDayOfWeek(int year, int month, int day_of_week, int n)
   {
      MqlDateTime dt;
      // build date for the 1st of month
      dt.year = year;
      dt.mon  = month;
      dt.day  = 1;
      dt.hour = 0;
      dt.min  = 0;
      dt.sec  = 0;
      datetime first = StructToTime(dt);
      MqlDateTime dt2;
      TimeToStruct(first, dt2);
      int dow_first = dt2.day_of_week; // 0..6
      int delta = (day_of_week - dow_first + 7) % 7;
      int days_to_nth = delta + (n - 1) * 7;
      return first + days_to_nth * 86400;
   }

1.5. コンストラクタとタイムゾーンセッター

簡易コンストラクタでは、タイムゾーンフィールドを0に初期化します。また、2つのpublicセッターを用意しており、呼び出し元のコードからトレーダーのタイムゾーンやサーバータイムゾーンを明示的に設定できます。これは、自動検出を使用しない場合や、テスト環境やリモートサーバーで検出結果を上書きする必要がある場合に便利です。

public:
   CNFPTimeManager()
   {
      m_traderTimeZone = 0;
      m_serverTimeZone = 0;
   }

   // set trader timezone if you need to convert between trader/local and server
   void SetTraderTimeZone(int timezone_hours)
   {
      m_traderTimeZone = timezone_hours;
   }

   void SetServerTimeZone(int timezone_hours)
   {
      m_serverTimeZone = timezone_hours;
   }

1.6. DetectServerTimeZoneとGetServerTime:サーバー対応ヘルパー関数

DetectServerTimeZoneは便利機能として提供されており、TimeCurrent()(サーバー時間)とTimeLocal()(ローカルマシン時間)を比較して、整数の時間オフセットを記録します。一方、GetServerTimeは単にTimeCurrent()をラップしており、他のモジュールがサーバーの現在時刻を取得する際に、一元化された呼び出し先として利用できます。

   // attempt to auto-detect server timezone (difference between server and local)
   int DetectServerTimeZone()
   {
      datetime local = TimeLocal();
      datetime server = TimeCurrent();
      int offset_seconds = (int)(server - local);
      int offset_hours = offset_seconds / 3600;
      m_serverTimeZone = offset_hours;
      return offset_hours;
   }

   datetime GetServerTime()
   {
      return TimeCurrent();
   }

1.7. GetNFPTimestampTrader:東部時間(ET)によるNFP発表時刻

このメソッドは、指定された年月の第1金曜日に発表されるNFPイベントを、08:30 ETの日時として返します。関数は「トレーダー時間」(ET実時間)で動作し、サーバー時間への変換は別途おこないます。この関心の分離により、関数はシンプルで予測可能な動作を維持できます。

   // NFP candidate time in trader (ET) zone: first Friday of month at 08:30 ET
   datetime GetNFPTimestampTrader(int year, int month)
   {
      // first Friday (5) of the month
      datetime first_friday = GetNthDayOfWeek(year, month, 5, 1);
      MqlDateTime dt;
      TimeToStruct(first_friday, dt);
      dt.hour = 8; dt.min = 30; dt.sec = 0;
      return StructToTime(dt); // this is in server timescale semantics but represents the ET timestamp
   }

1.8. TraderETtoServer:ETタイムスタンプをサーバー時間に変換する

この関数は重要な変換機能を担います。ETの壁時計タイムスタンプ(例:2025-09-05 08:30 ET)を受け取り、その日付が夏時間(DST)かどうかを判定し、UTCに変換した後、設定されたサーバータイムゾーンへシフトします。結果として得られる日時は、EAがスケジューリングやバーインデックスの基準として使用すべき絶対的なサーバー時間となります。

   // Convert a trader ET timestamp to server absolute time using ET offset and server tz
   datetime TraderETtoServer(datetime nfp_trader_time)
   {
      int et_offset = GetETOffset(nfp_trader_time); // UTC offset for ET at that date (-4/-5)
      // nfp_trader_time is interpreted in ET (i.e., wall-clock ET). To get UTC we add -ET offset:
      // UTC = ET - ET_offset_hours
      // Server local = UTC + server_tz
      int server_tz = m_serverTimeZone;
      datetime nfp_server_time = nfp_trader_time - et_offset * 3600 + server_tz * 3600;
      return nfp_server_time;
   }

1.9. GetNextNFPServerTimeとGetLastNFPServerTime:前方/後方検索

これらのヘルパー関数は、NFPのサーバータイムスタンプを前方(次回)または後方(直近)に向けて検索します。最大13か月先までスキャンし、ETの候補日時をサーバー時間に変換した上で、TimeCurrent()と比較します。年をまたぐ場合でも、月や年の増減を正しく処理するため、エッジケースでも安全に使用可能です。

   // Return the next NFP server datetime strictly greater than now (search up to 13 months)
   datetime GetNextNFPServerTime()
   {
      datetime server_time = GetServerTime();
      MqlDateTime sd; TimeToStruct(server_time, sd);
      int year = sd.year, month = sd.mon;
      for(int i=0;i<13;i++)
      {
         datetime nfp_trader = GetNFPTimestampTrader(year, month);
         datetime nfp_server = TraderETtoServer(nfp_trader);
         if(nfp_server > server_time) return(nfp_server);
         month++; if(month>12){ month=1; year++; }
      }
      return(0);
   }

   // Return most recent NFP server datetime <= now (search backwards)
   datetime GetLastNFPServerTime()
   {
      datetime server_time = GetServerTime();
      MqlDateTime sd; TimeToStruct(server_time, sd);
      int year = sd.year, month = sd.mon;
      for(int i=0;i<13;i++)
      {
         datetime nfp_trader = GetNFPTimestampTrader(year, month);
         datetime nfp_server = TraderETtoServer(nfp_trader);
         if(nfp_server <= server_time) return(nfp_server);
         month--; if(month<1){ month=12; year--; }
      }
      return(0);
   }

1.10. IsNFPEventActive:クイックアクティビティ期間チェック

この簡易ブール関数は、直近のNFP発表が設定可能な期間内(デフォルトでは±1時間)にあるかどうかを判定します。NFP期間中にコードの挙動を変更したい場合(例:取引を一時停止する、イベントをログに記録する)に便利です。

   // Detect if an NFP event is currently within ±window_seconds of server time (default ±3600s)
   bool IsNFPEventActive(int window_seconds = 3600)
   {
      datetime server_time = GetServerTime();
      datetime last_nfp = GetLastNFPServerTime();
      if(last_nfp == 0) return(false);
      if(MathAbs((long)(server_time - last_nfp)) <= window_seconds) return(true);
      return(false);
   }

1.11. FirstM5OpenAfterNFP:NFP発表時刻をM5オープンにマッピングする

取引ロジックは、NFP発表後の最初のM5ローソク足の確定に反応します。このユーティリティ関数は、イベントを含むM5の開始時刻を返します(確定時刻がタイムスタンプの直後となるローソク足)。単純な床関数計算(timestamp / 300 * 300)を用いることで、高速かつ明確に計算できます。

   // Utility: compute the M5 open time for the bar that contains the NFP or the first bar after it.
   // Returns the M5 open time for the bar whose close is the first > nfp_server_time.
   datetime FirstM5OpenAfterNFP(datetime nfp_server_time)
   {
      if(nfp_server_time <= 0) return(0);
      // PeriodSeconds for M5 = 300
      int period = PERIOD_M5;
      int psec = 60 * 5;
      // floor open time:
      long open_floor = (long)(nfp_server_time / psec) * psec;
      // if the event falls exactly at an open boundary, then the bar with open=open_floor is the one that includes event
      // its close = open_floor + psec -> that is the first close after event
      return (datetime)open_floor;
   }

1.12. GetM5BarIndexByOpenTime:開始時間をバーインデックスに変換する

最後に紹介するこのユーティリティ関数は、iBarShiftをラップして、指定されたM5開始時刻に対応するM5シリーズのバーインデックス(現在バーから0ベース)を返します。正確な一致が見つからない場合は-1を返します。この関数は、EAがCopyRatesを呼び出したり、過去のバーにインデックスアクセスしたりする際に便利です。

   // Convenience: compute the M5 bar index (iBarShift) for an M5 open time (exact match)
   int GetM5BarIndexByOpenTime(string symbol, datetime m5_open_time)
   {
      if(m5_open_time <= 0) return(-1);
      // prefer exact match - returns index where rates[].time == m5_open_time
      int idx = iBarShift(symbol, PERIOD_M5, m5_open_time, true);
      return idx; // -1 if not found
   }
};

手順2:  NFP後のフィボナッチトレーダーEA

2.1. ファイルヘッダ、インクルード、および入力パラメータ 

このトップブロックでは、EAのメタデータを宣言し、取引用ヘルパーおよびNFPTimeManagerヘッダをインクルードしています。すべての入力パラメータは、トレーダーが操作できる「調整ノブ」に相当します。具体的には、カラー設定、名前付け、EAによる自動注文の可否、ロットサイズ、SL/TP用のバッファ、厳密性、フィボナッチラインの有効期間などです。テスト時(デモ環境)ではこれらの値をシンプルに保ち、後で通貨ペアごとに調整します。

//+------------------------------------------------------------------+
//|             Post-NFP Fibonacci Trader EA                        |
//|  Post-NFP fib + pending orders with robust SL/TP buffer & object |
//|  lifecycle: destroy object when any order fills OR after expiry  |
//+------------------------------------------------------------------+
#property copyright "Clemence Benjamin"
#property version   "1.0"
#property strict

#include <Trade\Trade.mqh>
#include <NFPTimeManager.mqh>

// --- visual / fib inputs
input color  InpFiboColor = clrDodgerBlue;
input string InpPrefix = "FIBO_NFP_";
input bool   InvertFiboOrientation = true;
input bool   InpRayRight = true;

// Trading inputs
input double InpLots = 0.01;
input int    InpSlippage = 10;
input ulong  InpMagic = 123456;
input int    InpOrderExpirationMinutes = 0;
input bool   InpAutoPlaceOrders = true;
input bool   InpRequireStrictSLAt100 = false; // strict mode
input double InpSLBufferPoints = 20.0; // SL moved away from 100% by this many points
input double InpTPBufferPoints = 20.0; // TP moved away from 0% by this many points
input int    InpFibExpiryHours = 3;    // how many hours before fib auto-deletes

2.2. グローバル状態:ランタイムデータの保存場所

これらのグローバル変数は、EAが呼び出し間で保持する必要のあるライブ状態を格納します。具体的には、フィボナッチのアンカー、元のバーが上昇だったかどうか、追跡中の指値チケット、現在のフィボナッチオブジェクト名と作成時刻(有効期限管理用)、固定フィボナッチレベル、M5バーの管理情報などです。これらは開発者によって宣言され、ランタイム中に適宜更新されます。

// --- internal
CTrade trade;
CNFPTimeManager nfpManager;

double g_price0 = 0.0;
double g_price100 = 0.0;
bool   g_fibBullish = false;

ulong g_pendingTickets[];                  // tracked pending tickets
string g_currentFibName = "";              // current fib object name
datetime g_fibCreateTime = 0;              // server time when fib created

static const double S_levels[] = {38.2, 50.0, 61.8};

datetime lastM5Open = 0;
datetime lastProcessedNFP = 0;

2.3. OnInit():初期設定とNFPヘッダとの連携

EAが接続されると、OnInitはまずヘッダを使用してサーバーのタイムゾーンを検出します。これにより、ETからサーバー時間への変換が正確におこなわれます。次に、CTradeのデフォルト設定(マジックナンバーやスリッページ)を構成し、以前のチケット配列メモリをクリアします。さらに、現在のM5開始時刻を記録し、EAが次のNFP後のM5確定を検出できるよう準備します。

int OnInit()
{
   // initialize NFP manager tz
   int detected = nfpManager.DetectServerTimeZone();
   PrintFormat("EA: Detected server timezone UTC%+d", detected);

   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(InpSlippage);

   ArrayFree(g_pendingTickets);

   // initialize last M5 open time
   MqlRates r[];
   if(CopyRates(_Symbol, PERIOD_M5, 0, 1, r) == 1) lastM5Open = r[0].time;
   else lastM5Open = 0;

   Print("EA: Initialized v1.50 — monitoring NFP, will create fib on first M5 close after NFP.");

   return(INIT_SUCCEEDED);
}

2.4. OnDeinit():シャットダウンとクリーンアップ

OnDeinit()では、EAが削除された際に未処理の指値注文やチャートオブジェクトを残さないように処理します。追跡中の指値注文をすべてキャンセルし、現在のフィボナッチオブジェクト(存在する場合)を削除し、チケット配列を解放します。これは、デモテストや実運用において安全性を確保するための重要なハウスキーピング手順です。

void OnDeinit(const int reason)
{
   // cleanup: cancel tracked pending orders and delete fib
   CancelExistingPendingOrders();
   DeleteCurrentFibIfExists();
   ArrayFree(g_pendingTickets);
}

2.5. OnTradeTransaction():約定検知と即座反応

OnTradeTransaction()コールバックは、取引システムのイベントを監視します。新しい取引(約定)が発生すると、EAはその約定が追跡中の指値注文のいずれかに該当するかを確認します。該当する場合は、フィボナッチオブジェクトを削除し、残りの指値注文をキャンセルしてチケットリストをクリアします。また、外部から削除されたチケットも追跡リストから取り除きます。これにより、チャート上のフィボナッチ表示と追跡中の指値注文が実際の取引と常に同期されるようになります。

void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result)
{
   // We care about deals added (fills) and order deletions possibly made externally
   if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
   {
      ulong orderTicket = trans.order;
      if(orderTicket == 0) return;

      for(int i = ArraySize(g_pendingTickets)-1; i >= 0; i--)
      {
         if(g_pendingTickets[i] == orderTicket)
         {
            PrintFormat("EA: Detected fill for tracked order %I64u -> destroying fib and cancelling remaining pending orders.", orderTicket);
            ArrayRemove(g_pendingTickets, i);
            CancelExistingPendingOrders();
            DeleteCurrentFibIfExists();
            ArrayFree(g_pendingTickets);
            return;
         }
      }
   }
   else if(trans.type == TRADE_TRANSACTION_ORDER_DELETE)
   {
      ulong orderTicket = trans.order;
      if(orderTicket == 0) return;
      for(int i = ArraySize(g_pendingTickets)-1; i>=0; i--)
      {
         if(g_pendingTickets[i] == orderTicket)
         {
            ArrayRemove(g_pendingTickets, i);
            PrintFormat("EA: Tracked order %I64u was deleted externally — removed from internal list.", orderTicket);
            break;
         }
      }
   }
}

2.6. OnTick()ハートビートロジック - 有効期限チェックとM5確定トリガー

OnTickは、市場のティックごとに実行されます。ここでは主に2つのコアチェックをおこないます。(A)まず1つ目は、現在のフィボナッチオブジェクトが存在し、その寿命(例:3時間)が経過している場合です。この場合はフィボナッチを削除し、関連する指値注文をキャンセルします。(B)2つ目は、新しいM5バーが開いた(前のM5が確定した)ことを検出する処理です。このM5確定イベント発生時に、ヘッダから直近のNFPサーバー時間を取得し、確定したM5がNFP後の最初のM5であるかを確認します。条件が満たされれば、フィボナッチ描画と指値注文配置のルーチンを呼び出します。この設計により、フィボナッチはNFP発表後の完了したM5ローソク足に確実にアンカーされるようになっています。

void OnTick()
{
   // 1) Check for fib expiry by time
   if(StringLen(g_currentFibName) > 0 && g_fibCreateTime > 0)
   {
      datetime now = TimeCurrent();
      if(now >= (datetime)(g_fibCreateTime + InpFibExpiryHours * 3600))
      {
         PrintFormat("EA: Fib '%s' expired after %d hours -> deleting object and cancelling pending orders.",
                     g_currentFibName, InpFibExpiryHours);
         CancelExistingPendingOrders();
         DeleteCurrentFibIfExists();
      }
   }

   // 2) Detect M5 new bar open (previous M5 bar closed)
   MqlRates m5rt[];
   if(CopyRates(_Symbol, PERIOD_M5, 0, 1, m5rt) != 1) return;
   datetime currentM5Open = m5rt[0].time;
   if(currentM5Open == lastM5Open) return;

   // previous M5 open is the bar that just closed
   datetime prevOpen = lastM5Open;
   lastM5Open = currentM5Open;

   // find last NFP server time (the release to react to)
   datetime lastNFP = nfpManager.GetLastNFPServerTime();
   if(lastNFP == 0) return;

   // if already processed this NFP skip
   if(lastProcessedNFP == lastNFP) return;

   // compute bar open floor (M5) that contains the event
   int psec = 60*5;
   long floorOpen = (long)(lastNFP / psec) * psec;
   datetime firstM5OpenAfterNFP = (datetime)floorOpen;

   // we want prevOpen == firstM5OpenAfterNFP (i.e. the bar that closed is that target)
   if(prevOpen != firstM5OpenAfterNFP) return;

   // find M5 index of that open time
   int m5Index = iBarShift(_Symbol, PERIOD_M5, prevOpen, true);
   if(m5Index < 0)
   {
      PrintFormat("EA: Could not find M5 index for open time %s", TimeToString(prevOpen, TIME_DATE|TIME_MINUTES));
      return;
   }

   // update fib using that M5 bar and create pending orders
   if(!UpdateFibFromM5Bar(m5Index)) { Print("EA: UpdateFibFromM5Bar failed."); return; }
   if(InpAutoPlaceOrders) PlaceOrRefreshPendingOrders();

   // mark processed
   lastProcessedNFP = lastNFP;
   PrintFormat("EA: Processed NFP at %s (server time). Used M5 open %s as anchor.",
               TimeToString(lastNFP, TIME_DATE|TIME_MINUTES), TimeToString(prevOpen, TIME_DATE|TIME_MINUTES));
}

2.7. UpdateFibFromM5Bar():M5バーの読み取り、アンカー計算、フィボナッチ描画

この関数は、CopyRatesを用いて指定されたM5バーを読み取り、そのローソク足が上昇か下降か(終値と始値の比較)を判定します。次に、設定に基づいて0%および100%のアンカーを計算し、必要に応じて視覚的な好みに合わせて反転させます。計算されたアンカーは取引用に保存され、以前の同名フィボナッチオブジェクトは削除されます。その後、対象のバーに正確に対応する一意の名を持つOBJ_FIBOオブジェクトを作成し、作成時刻を記録して有効期限管理に使用します。

bool UpdateFibFromM5Bar(int m5Index)
{
   if(m5Index < 0) return(false);

   MqlRates r[];
   int copied = CopyRates(_Symbol, PERIOD_M5, m5Index, 1, r);
   if(copied != 1)
   {
      PrintFormat("EA: CopyRates for M5 index %d failed (copied=%d).", m5Index, copied);
      return(false);
   }

   double bar_high = r[0].high;
   double bar_low  = r[0].low;
   double bar_open = r[0].open;
   double bar_close= r[0].close;
   datetime bar_time = r[0].time;

   bool bullish = (bar_close > bar_open);
   g_fibBullish = bullish;

   // user's mapping:
   double price0 = bullish ? bar_high : bar_low;   // 0% anchor
   double price100= bullish ? bar_low  : bar_high; // 100% anchor

   if(InvertFiboOrientation) { double t = price0; price0 = price100; price100 = t; }

   // store anchors for trading
   g_price0 = price0;
   g_price100 = price100;

   // delete any existing prefix objects to avoid clutter
   int tot = ObjectsTotal(0);
   for(int i = tot-1; i >= 0; i--)
   {
      string nm = ObjectName(0, i);
      if(StringFind(nm, InpPrefix) == 0) ObjectDelete(0, nm);
   }

   // create a unique name for this fib
   g_currentFibName = InpPrefix + _Symbol + "_" + IntegerToString((int)bar_time);
   bool created = ObjectCreate(0, g_currentFibName, OBJ_FIBO, 0, bar_time, price0, (datetime)(bar_time + PeriodSeconds(PERIOD_M5)), price100);
   if(!created)
   {
      PrintFormat("EA: Failed to create Fibo '%s' (err=%d)", g_currentFibName, GetLastError());
      g_currentFibName = "";
      return(false);
   }

   ObjectSetInteger(0, g_currentFibName, OBJPROP_COLOR, InpFiboColor);
   ObjectSetInteger(0, g_currentFibName, OBJPROP_RAY_RIGHT, InpRayRight ? 1 : 0);
   ObjectSetInteger(0, g_currentFibName, OBJPROP_SELECTABLE, true);
   ObjectSetInteger(0, g_currentFibName, OBJPROP_HIDDEN, false);

   string desc = bullish ? "Fibo anchored to bullish M5 bar" : "Fibo anchored to bearish M5 bar";
   if(InvertFiboOrientation) desc += " (inverted)";
   if(InpRayRight) desc += " (ray-right)";
   ObjectSetString(0, g_currentFibName, OBJPROP_TEXT, desc);

   g_fibCreateTime = TimeCurrent();

   PrintFormat("EA: Drew Fibo '%s' on M5 index %d (open=%s) high=%.5f low=%.5f",
               g_currentFibName, m5Index, TimeToString(bar_time, TIME_DATE|TIME_MINUTES), bar_high, bar_low);

   return(true);
}

2.8. DeleteCurrentFibIfExists():フィボナッチ削除の集中管理

この小さなヘルパー関数は、現在のフィボナッチオブジェクトが存在する場合に削除し、EA内の関連状態もクリアします。削除処理を集中管理することで、処理の重複を減らし、オブジェクト名がクリアされないなどの微妙なバグを防ぐことができます。

void DeleteCurrentFibIfExists()
{
   if(StringLen(g_currentFibName) == 0) return;
   if(ObjectFind(0, g_currentFibName) >= 0)
   {
      ObjectDelete(0, g_currentFibName);
      PrintFormat("EA: Deleted fib object '%s'.", g_currentFibName);
   }
   g_currentFibName = "";
   g_fibCreateTime = 0;
}

2.9. CancelExistingPendingOrders():トラックアンドクリーン戦略

EAは、作成した指値注文のチケットをg_pendingTicketsで追跡しています。このルーチンでは、そのリストを順に処理し、各チケットに対してtrade.OrderDeleteを呼び出します。チケットを追跡して削除する方法は、シンボルやコメントで削除する方法より安全です。なぜなら、現在のEA実行で作成された注文のみを対象とするためであり、後から永続化機能を追加しない限り他の注文には影響しません。

void CancelExistingPendingOrders()
{
   if(ArraySize(g_pendingTickets) == 0) return;

   for(int i = ArraySize(g_pendingTickets)-1; i >= 0; i--)
   {
      ulong ticket = g_pendingTickets[i];
      if(ticket == 0) { ArrayRemove(g_pendingTickets, i); continue; }
      bool del = trade.OrderDelete(ticket);
      if(!del) PrintFormat("EA: Failed to delete pending ticket %I64u (err=%d)", ticket, GetLastError());
      else PrintFormat("EA: Deleted pending ticket %I64u", ticket);
      ArrayRemove(g_pendingTickets, i);
   }
}

2.10. PlaceOrRefreshPendingOrders():エントリー計算、SL/TP設定、指値注文配置(取引の核心)

この関数は、EAの取引ロジックの心臓部です。フィボナッチレベル(38.2%、50%、61.8%)ごとに、以下の処理をおこないます。

  • g_price0とg_price100の間で線形補間をおこない、エントリー価格を計算します。
  • SLとTPは、それぞれ100%と0%にアンカーして計算した後、元のM5バーが上昇か下降かに応じて、InpSLBufferPoints/InpTPBufferPointsだけエントリーから離すように調整します。これにより、上昇バーの場合はSLが100%下、TPが0%上になるよう設定され、下降バーの場合はその逆となります。
  • 計算した価格は通貨ペアの小数桁に正規化します。
  • ブローカーの制限(SYMBOL_TRADE_STOPS_LEVEL、SYMBOL_POINT)を確認し、最小ストップ距離や指値距離を確保します。必要に応じてSL/TPを調整するか、厳密モードが有効な場合はそのレベルの注文をスキップします。
  • 各レベルについて、BUY_LIMIT(市場より下での買い注文)またはSELL_LIMIT(市場より上での売り注文)のどちらが適切かを判断し、CTradeを用いて指値注文を配置します。有効期限や時間タイプが設定されていればそれも適用します。成功と失敗はログに記録し、チケットは後でクリーンアップできるよう保持します。

この方法により、ブローカーによる注文拒否を最小化しつつ、価格関係が論理的に正しい状態で維持されます。

void PlaceOrRefreshPendingOrders()
{
   // clear previously tracked tickets first
   CancelExistingPendingOrders();

   if(g_price0 == 0.0 || g_price100 == 0.0)
   {
      Print("EA: anchors not set - skipping placement");
      return;
   }

   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(InpSlippage);

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

   long stops_level = 0;
   if(!SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL, stops_level)) stops_level = 0;
   double min_stop_distance = (double)stops_level * point;
   if(min_stop_distance < point) min_stop_distance = point;
   double min_pending_distance = min_stop_distance;

   // compute buffer values in price units (points * point)
   double sl_buffer_price = InpSLBufferPoints * point;
   double tp_buffer_price = InpTPBufferPoints * point;

   ENUM_ORDER_TYPE_TIME place_time_type = (InpOrderExpirationMinutes > 0) ? ORDER_TIME_SPECIFIED : ORDER_TIME_GTC;
   datetime place_expiration = (InpOrderExpirationMinutes > 0) ? (TimeCurrent() + InpOrderExpirationMinutes * 60) : 0;

   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   int levelsCount = ArraySize(S_levels);

   for(int i=0;i<levelsCount;i++)
   {
      double pct = S_levels[i] / 100.0;
      double entry = g_price0 + pct * (g_price100 - g_price0);
      // base desired SL and TP anchored at 100% and 0%
      double sl_desired = g_price100;
      double tp_desired = g_price0;

      // Apply buffers depending on the original fib bullish/bearish orientation
      if(g_fibBullish)
      {
         sl_desired = (g_price100 - sl_buffer_price);
         tp_desired = (g_price0 + tp_buffer_price);
      }
      else
      {
         sl_desired = (g_price100 + sl_buffer_price);
         tp_desired = (g_price0 - tp_buffer_price);
      }

      // normalize values
      entry = NormalizeDouble(entry, digits);
      sl_desired = NormalizeDouble(sl_desired, digits);
      tp_desired = NormalizeDouble(tp_desired, digits);

      // decide pending type by comparing entry price with market and min_pending_distance
      bool wantBuy = false, wantSell = false;
      if(entry <= (ask - min_pending_distance)) wantBuy = true;
      else if(entry >= (bid + min_pending_distance)) wantSell = true;
      else
      {
         PrintFormat("EA: Skipping level %.2f: entry(%.5f) too close to market (Ask=%.5f Bid=%.5f) min_pending_dist=%.5f",
                     S_levels[i], entry, ask, bid, min_pending_distance);
         continue;
      }

      ENUM_ORDER_TYPE otype = (wantBuy ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_LIMIT);
      bool adjusted = false;

      // Validate/adjust stops unless strict mode requested
      if(otype == ORDER_TYPE_BUY_LIMIT)
      {
         // For BUY_LIMIT we need sl < entry < tp
         if(!(sl_desired < entry - min_stop_distance))
         {
            if(InpRequireStrictSLAt100)
            {
               PrintFormat("EA: Strict mode: skipping level %.2f because SL would be adjusted (desired SL %.5f not < entry-%.5f).",
                           S_levels[i], sl_desired, (entry - min_stop_distance));
               continue;
            }
            double old = sl_desired;
            sl_desired = NormalizeDouble(entry - min_stop_distance, digits);
            adjusted = true;
            PrintFormat("EA: Adjusted SL BUY level %.2f old=%.5f -> new=%.5f", S_levels[i], old, sl_desired);
         }
         if(!(tp_desired > entry + min_stop_distance))
         {
            if(InpRequireStrictSLAt100)
            {
               PrintFormat("EA: Strict mode: skipping level %.2f because TP would be adjusted (desired TP %.5f not > entry+%.5f).",
                           S_levels[i], tp_desired, (entry + min_stop_distance));
               continue;
            }
            double old = tp_desired;
            tp_desired = NormalizeDouble(entry + min_stop_distance, digits);
            adjusted = true;
            PrintFormat("EA: Adjusted TP BUY level %.2f old=%.5f -> new=%.5f", S_levels[i], old, tp_desired);
         }
      }
      else // SELL_LIMIT
      {
         // For SELL_LIMIT we need tp < entry < sl
         if(!(sl_desired > entry + min_stop_distance))
         {
            if(InpRequireStrictSLAt100)
            {
               PrintFormat("EA: Strict mode: skipping level %.2f because SL would be adjusted (desired SL %.5f not > entry+%.5f).",
                           S_levels[i], sl_desired, (entry + min_stop_distance));
               continue;
            }
            double old = sl_desired;
            sl_desired = NormalizeDouble(entry + min_stop_distance, digits);
            adjusted = true;
            PrintFormat("EA: Adjusted SL SELL level %.2f old=%.5f -> new=%.5f", S_levels[i], old, sl_desired);
         }
         if(!(tp_desired < entry - min_stop_distance))
         {
            if(InpRequireStrictSLAt100)
            {
               PrintFormat("EA: Strict mode: skipping level %.2f because TP would be adjusted (desired TP %.5f not < entry-%.5f).",
                           S_levels[i], tp_desired, (entry - min_stop_distance));
               continue;
            }
            double old = tp_desired;
            tp_desired = NormalizeDouble(entry - min_stop_distance, digits);
            adjusted = true;
            PrintFormat("EA: Adjusted TP SELL level %.2f old=%.5f -> new=%.5f", S_levels[i], old, tp_desired);
         }
      }

      // final relation sanity
      bool valid = true;
      if(otype == ORDER_TYPE_BUY_LIMIT) valid = (sl_desired < entry && tp_desired > entry);
      else valid = (sl_desired > entry && tp_desired < entry);
      if(!valid)
      {
         PrintFormat("EA: Skipping level %.2f due to invalid final SL/TP relation entry=%.5f SL=%.5f TP=%.5f",
                     S_levels[i], entry, sl_desired, tp_desired);
         continue;
      }

      // build order comment
      string comment = InpPrefix + DoubleToString(S_levels[i], 2);

      // Place pending with proper signature (includes time type and expiration)
      bool placed = false;
      if(otype == ORDER_TYPE_BUY_LIMIT)
         placed = trade.BuyLimit(InpLots, entry, _Symbol, sl_desired, tp_desired, place_time_type, place_expiration, comment);
      else
         placed = trade.SellLimit(InpLots, entry, _Symbol, sl_desired, tp_desired, place_time_type, place_expiration, comment);

      if(!placed)
      {
         int err = GetLastError();
         PrintFormat("EA: Failed to place %s at %.5f (lvl %.2f) err=%d ret=%d",
                     (otype==ORDER_TYPE_BUY_LIMIT ? "BUY_LIMIT":"SELL_LIMIT"),
                     entry, S_levels[i], err, (int)trade.ResultRetcode());
      }
      else
      {
         ulong ticket = trade.ResultOrder();
         PrintFormat("EA: Placed %s ticket=%I64u lvl=%.2f entry=%.5f SL=%.5f TP=%.5f adjusted=%s",
                     (otype==ORDER_TYPE_BUY_LIMIT ? "BUY_LIMIT":"SELL_LIMIT"),
                     ticket, S_levels[i], entry, sl_desired, tp_desired, (adjusted ? "yes":"no"));

         // track ticket for cleanup and fill detection
         ArrayResize(g_pendingTickets, ArraySize(g_pendingTickets) + 1);
         g_pendingTickets[ArraySize(g_pendingTickets)-1] = ticket;
      }
   } // levels loop
}

2.11. EAにおけるNFPTimeManagerヘッダの利用方法:呼び出しポイントの概要

このヘッダは、日付や時間の処理の大部分を担当します。EAでは、OnInit()内でDetectServerTimeZone()を呼び出して変換の準備をおこない、各M5確定時にGetLastNFPServerTime()を呼び出して、直近のNFP発表のサーバー時間を取得します。そのタイムスタンプをもとに、NFP後の最初のM5開始時刻を計算し、該当M5が確定したタイミングで取引ロジックを実行します。この設計により、EAのコアロジックはシンプルでタイムゾーンに安全な状態に保たれます。

// example header calls in OnInit/OnTick
int detected = nfpManager.DetectServerTimeZone();
datetime lastNFP = nfpManager.GetLastNFPServerTime();


結果

NFPTimeManagerクラスを利用することで、EAは過去のデータにおいてもNFP発表を正確に特定できるようになりました。そのため、コンパイルが成功した後は、ストラテジーテスターで戦略の実行と検証をおこなうことが可能です。以下に、ストラテジーテスターでのテスト過程を示すアニメーション画像を掲載します。

metatester64_nDEhinFAuB

図6:2024年6月7日NFP発表

図6では、2024年6月7日のNFP発表時に、EAが発表日を正しく検知し、NFP発表時刻をローカルサーバー時間に正確にマッピングできていることが確認できます(ストラテジーテスター上で確認済み)。フィボナッチリトレースメントツールも計画通りに描画と実行がおこなわれました。ただし、リトレースメントが浅く、エントリーレベルに到達しなかったため、指値注文は発動しませんでした。

また、指値注文に設定されたSLやTPの値が、意図した仕様と完全には一致していないことも観察されました。これは、コード修正または入力パラメータの調整によって改善可能です。

これらの制約はあるものの、最も重要な成果は、EAがイベントを正確に検知し、タイムゾーンを正しくマッピングし、フィボナッチ描画を自動化できたことです。これは今後の改良のための強固な基盤となります。以下は、2024年11月1日のイベントの別画像です。

 metatester64_lfPG2lDG5m

図7:2024年11月1日NFP発表



結論

フィボナッチリトレースメントツールは、NFPイベントに対して、ルールベースで構造化されたアルゴリズム取引に応用可能です。初動のスパイクを追いかけるのではなく、この手法では発表後数分間の自然な修正局面で取引をおこないます。概念的には、元の5分間の価格スパイクの30%〜60%程度を利益として捉えることが可能で、たとえトレーダーが初動を逃してもチャンスがあります。これは、フィボナッチ比率を参照することで、スパイクの全体を100%としてマッピングし、正確なリトレースメントレベルで取引配置がおこなえるためです。

特に、38.2%のフィボナッチレベルは、初動スパイクの38.2%程度の利益を得られる確率が最も高いことが多く見られます。しかし、市場が常にこの挙動に従うわけではなく、どの設定も保証されたものではありません。テストでは複数のシナリオで有望な結果が得られましたが、このアプローチは確実性ではなく、構造化された確率として扱うべきです。

本研究では、EAの広範なパフォーマンス分析はおこなわず、ストラテジーテスターでの実践的なシミュレーションを通じて概念の実証に焦点を当てました。プロトタイプには改善と最適化の余地があり、注文管理の高度化、統計的検証、他の通貨ペアやニュース以外の取引への適応などでさらに洗練できます。

同様に価値のある成果として、戦略構築過程で得られた教訓があります。ゼロから始め、モジュール化によってコードをクリーンかつ再利用可能に保ち、将来的な拡張を見据えて設計したことです。これは今後さらに発展させるための基盤であり、継続的な開発により、より強力で柔軟な取引ツールへと進化させることができます。

ご意見やフィードバックは、ぜひ下のコメント欄でお寄せください。また、添付のソースドキュメントや、実務上のポイントをまとめた主要な教訓のサマリ表もご参照ください。


重要な学び

重要な学び 説明
モジュール化されたロジック 機能は小さく焦点を絞った単位(クラスや関数)に分割します。例として、時間やDST処理専用のNFPTimeManagerを用いることで、EAのコア部分を読みやすく、再利用可能に保てます。
タイムゾーンとDSTを明示的に扱う イベント時刻(ET)をサーバー時間に変換する際は決定的なルールを用います。ここでの誤りは、過去検証やライブ稼働でアンカーの位置ずれを引き起こすため、ロジックを集中管理して十分にテストしてください。
確定バーにアンカーする 常に確定したローソク足(例:NFP後に最初に確定したM5)を基準に計算します。バー内で再計算すると不安定になり、ストラテジーテスターでの再現性も損なわれます。
ブローカー制限を尊重する 実行時にSYMBOL_TRADE_STOPS_LEVEL、SYMBOL_POINT、SYMBOL_DIGITSを確認し、SL/TP/指値の距離を検証します。これにより、注文拒否を回避できます。
価格を正規化し正しい精度を使う エントリー、SL、TPは常に通貨ペアの小数桁に正規化します。わずかな丸め誤差でもブローカーによる拒否の原因になります。
安全なフォールバック動作を設計する 厳密モードと適応モードを用意します。厳密モードではストップ調整が必要なレベルをスキップし、適応モードでは自動的にブローカー規則に合わせてストップを調整します。どちらの判断もログに記録します。
堅牢な注文配置APIを使用する CTradeヘルパーメソッドを正しいシグネチャ(有効期限や時間タイプ含む)で使用します。後から修正が不要になり、エラー処理の複雑さも軽減できます。
自分のチケットを追跡・管理する 作成したチケットを内部リストで管理し、EAが自ら作成した注文のみを確実にキャンセルまたは監視できるようにします。他のユーザー注文を誤って削除するのを避けられます。
オブジェクトライフサイクルとUIの整頓 チャートオブジェクトには短くユニークな名前(例:fib_SYMBOL_YYMMDDhhmm)を付け、詳細情報はOBJPROP_TEXTに格納します。注文が約定または期限切れになった際にはオブジェクトを削除します。
約定検知にOnTradeTransactionを使用する 約定や外部削除をトランザクションイベントで検知し、即時に対応(クリーンアップ、ログ記録)します。ポーリングによる遅延を避けられます。
トレース可能な詳細ログを残す 作成、調整、スキップ、注文配置、失敗、削除など、重要なアクションはすべてログに記録します。明確なログはデバッグ、バックテスト、規制・監査目的で必須です。
まずはストラテジーテスターとデモで検証する 歴史的NFP日付でロジックをストラテジーテスター上で検証し(決定的)、次にデモ口座で実行します。決定的なアンカー(確定済みバー)を使用することで、再現可能なテストが可能です。



添付ファイル

ファイル名 バージョン 説明
NFPTimeManager.mqh
1.0 DST対応の時間ユーティリティクラスで、NFPのタイムスタンプ計算と、ET、UTC、ブローカー/サーバー時間間の変換を集中管理します。主な機能には、サーバータイムゾーンの検出、NFP発表時刻(毎月第1金曜日08:30 ET)の計算、ET→サーバー時間変換、直近および次回のNFPサーバータイムスタンプ取得、さらにNFPの瞬間をM5開始時刻やバーインデックスにマッピングするヘルパーがあります。EAをタイムゾーンに依存しない形で設計し、再利用や単体テストが容易になるよう設計されています。
Post-NFP Fibonacci Trader EA.mq5
1.0 NFPTimeManagerを利用して、NFP発表後の最初のM5ローソク足の確定を検知し、そのバーにアンカーしたフィボナッチリトレースメントを描画、38.2%、50%、61.8%のリトレースメントレベルに指値注文を配置するフルEAです。主な機能には、SL/TP用バッファ、厳密モードと適応モードによるストップ管理、ブローカー制限の検証(SYMBOL_TRADE_STOPS_LEVEL、SYMBOL_POINT、小数桁)、チケット追跡、注文約定時や設定可能な期限経過後の自動クリーンアップ、さらにトレース可能な詳細ログ記録があります。入力パラメータで柔軟に設定可能で、ストラテジーテスターおよびデモ環境で検証済みです。

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

添付されたファイル |
MQL5におけるパイプライン MQL5におけるパイプライン
本記事では、機械学習におけるデータ準備工程の中で、重要性が急速に高まっているデータ前処理パイプラインを取り上げます。前処理パイプラインとは、生データをモデルに入力する前に通す一連の変換ステップを整理し、効率化したものです。一見地味な作業ですが、前処理(特にスケーリング)は学習時間や実行コストを削減するだけでなく、モデルの汎化性能を大きく左右します。本記事ではscikit-learnの前処理関数を中心に扱います。MQL5ウィザードはここでは使用しませんが、後続の記事で取り上げる予定です。
MQL5での取引戦略の自動化(第32回):プライスアクションに基づくファイブドライブハーモニックパターンシステムの作成 MQL5での取引戦略の自動化(第32回):プライスアクションに基づくファイブドライブハーモニックパターンシステムの作成
本記事では、MQL5においてピボットポイントとフィボナッチ比率に基づいて強気、弱気双方のファイブドライブ(5-0)ハーモニックパターンを識別し、ユーザーが選択できるカスタムエントリー、ストップロス、テイクプロフィット設定を用いて取引を実行するファイブドライブパターンシステムを開発します。また、A-B-C-D-E-Fパターン構造やエントリーレベルを表示するために、三角形やトレンドラインなどのチャートオブジェクトを使った視覚的フィードバックでトレーダーの洞察力を高めます。
カスタム口座パフォーマンス行列インジケーターの開発 カスタム口座パフォーマンス行列インジケーターの開発
このインジケーターは、口座エクイティ、損益、ドローダウンをリアルタイムで監視し、パフォーマンスダッシュボードとして可視化することで、規律の維持を促す役割を果たします。トレーダーが取引の一貫性を保ち、過剰取引を避け、自己勘定取引会社評価チャレンジ(プロップファームチャレンジ)のルールを遵守するための支援ツールとして機能します。
プライスアクション分析ツールキットの開発(第40回):Market DNA Passport プライスアクション分析ツールキットの開発(第40回):Market DNA Passport
本記事では、各通貨ペアが持つ固有のアイデンティティを、その過去のプライスアクションという視点から探ります。生物の設計図を記述するDNAの概念に着想を得て、本記事では市場にも同様の枠組みを適用し、プライスアクションを各通貨ペアのDNAとして扱います。ボラティリティ、スイング、リトレースメント、スパイク、セッション特性といった構造的挙動を分解することで、各ペアを他と区別する基礎的なプロファイルが浮かび上がります。このアプローチにより、市場行動に対するより深い洞察が得られ、トレーダーは各銘柄の特性に合った戦略を体系的に組み立てられるようになります。