English Русский Deutsch
preview
FVGをマスターする:ブレーカーと市場構造の変化によるフォーメーション、ロジック、自動取引

FVGをマスターする:ブレーカーと市場構造の変化によるフォーメーション、ロジック、自動取引

MetaTrader 5トレーディング |
101 2
Eugene Mmene
Eugene Mmene

はじめに

FVG(Fair Value Gaps、フェアバリューギャップ)は現代の市場では非常に一般的な現象であり、しばしばスマートマネーコンセプト(SMC)の一要素として言及されるか、あるいはそもそもSMCに由来するものと考えられています。また、トレーダーが取引プラットフォームやオンラインチャートルームでチャットする際や、SNS上で目にすることも非常に一般的ですが、多くの人はこれを単なる参照ポイントとして扱うことにとどまり、FVGが発生する時間や場所に基づいてバイアスを形成したり、取引を改善したりするためのツールとして活用することはありません。

私の目的は、単にFVGについて説明するだけでなく、トレーダーがそれを活用してバイアスやナラティブを形成し、実際の取引に応用する方法を示すことにあります。さらに、FVGが最も発生しやすい場所やタイミング、発生の仕組みを分析し、エントリーや利益確定ポイントとして、さらには強力な値動き後のプルバック時における再エントリーポイントとして検証する方法についても解説します。

FVGと、それをもとに根本的なバイアスやナラティブを検証、特定するための取引ロジックを理解した後は、ブレイクアウト要因の形成や、市場構造の変化がどのようにしてバイアスやナラティブに基づく取引を検証するかについて説明します。これらのブレイクアウト要因や市場構造の変化は非常に説得力があり、長期的な取引の正当化にもつながることがあります。本記事では、特に短期・長期を問わずあらゆる時間足で発生するFVGとブレイクアウト要因の組み合わせの有効性について、明確かつ丁寧に解説します。


FVGの紹介

FVGは、価格がいずれかの方向に急激に動いたときに発生します。これは主に機関投資家による強い買い圧力や売り圧力を示しています。簡単に言えば、この取引圧力により、トレーダーが取引を発注し、実行する機会が平等に与えられない状況が生まれます。そのため強気のFVGでは、価格が勢いとスピードを伴って上昇し、売り手が取引を執行する十分な時間や機会を得られません。一方、弱気のFVGでは、価格が勢いとスピードを伴って下降方向に動き、買い手が取引を執行する十分な時間や機会を持てません。この領域は、拡大が起こった方向とは逆方向の価格変動が存在しないため、アンバランスな価格帯と呼ばれます。

この現象は、塗装工の作業に例えると最も分かりやすく説明できます。塗装工が壁を塗るとき、均一に仕上げるためには反対方向にも同じように筆を入れる必要があります。この論理は価格にも当てはまります。たとえば、強気のローソク足が価格ポイントを突破して1.3010から1.3030まで動いた場合、価格のバランスを取るためには、弱気のローソク足も同じ価格ポイントを突破する必要があります。もしそれがおこなわれなければ、その領域はFVGとなります。多くの場合、価格は最終的にこのポイントに戻ってきます。FVGは広大で利益の大きい機会を提供するため、どこで、いつ、どのように活用するかを理解することが重要です。

次の図は、FVGの例とその活用方法を示しています。

弱気FVG

どのように、いつ、なぜ形成されるのか

FVGとは何かを理解したので、次にそれがどのように形成されるのかを考えるのはずっと簡単になります。FVGは、非常に明確な状況で発生します。1つ目のシナリオは、プレスリリースが発表された場合です。これには、CPI、FOMC、NFPなどの高影響度ニュースが含まれます。その影響の大きさ次第で、市場に与えるインパクトは変わります。

多くの場合、こうしたプレスリリースは、市場の大口プレーヤーがすでに指値・逆指値注文を入れており、ボラティリティやスピード、勢いを予想したうえで発生します。そのため、市場はすぐに有利に動き、私たちが利益を狙うFVGを残すことになります。業界の一般的なルールとして、大きなプレスリリース前に取引するのは理想的ではありません。むしろ、プレスリリースとそれに伴うボラティリティを待ってから取引を実行するのが賢明です。多くの場合、早すぎる取引ではセットアップすら整わないことが多いからです。

これが形成されるもう一つのシナリオは、銀行やヘッジファンドといった大口の機関投資家が注文を発注する場合です。彼らの注文は数十億ドル規模に及ぶため、市場の価格変動に大きな影響を与えます。

こうした大口注文は、ニュース発表の影響がなくても、巨大なローソク足や一方向への鋭い急騰や急落として容易に察知できます。本記事では、こうした大きな値動きの後に、それらの値動きの最中に発生するFVGをどのように見極めるべきかを説明します。

次の図は、価格がスピードと勢いを伴ってある価格帯を突破し、その直後のローソク足が強気のローソク足を埋め戻すことなく、FVGと呼ばれる埋められていない価格の不均衡を残した様子を示しています。

FVGの上振れギャップ


FVGを分析する方法

前章まででFVGが何であり、どのような状況で発生し、どのように形成されるのかについて説明しました。本章では、その理解を踏まえ、FVGをどのように分析し、どのような取引機会を待ち、どのように仕掛けるべきかという実践的な視点に焦点を移します。

最初の基本的なルールは、ほとんどの場合、FVGそのものを根拠に取引することは避けるべきですが、FVGから離れる方向への取引は有効となる場合があります。というのも、それらはトレンドやバイアス、ナラティブの方向に沿って適切に用いられると、価格を推進する役割を果たすためです。では、強い推進力を持つブロックがすぐに反対方向に作用する可能性があるにもかかわらず、なぜ逆方向の取引をおこなうべきなのでしょうか。これは下位時間足でも上位時間足でもまったく同じであり、動き方も同様です。唯一の違いは、それが取引として具現化するまでに要する時間であり、それ以外はほとんどの場合、そして90%のケースで、このように機能します。

理想的には、最初のステップは、最も上位の時間足から最も下位の時間足へと上から下へ順に分析し、すべてのFVGを観察し、マークしていくことです。そして最も重要なステップは、トレンドの方向、バイアス、そしてナラティブを分析することです。たとえば金の場合、MN、W1、D1におけるトレンド、バイアス、ナラティブは買い方向を示しています。これは主に3つの方法で確認できます。価格が移動平均線(単純移動平均と指数平滑移動平均)の上で取引されていること、市場構造が明らかに買いの勢いを示していること、さらに、強気(緑)のローソク足が弱気(赤)のローソク足よりはるかに多いことです。

展開されているトレンドとナラティブが定義されたら、次に考慮すべき重要な側面があります。価格は常に動き続け、流動性を求めており、ナラティブ、方向性、バイアスはより長い時間足によって決定されます。流動性は主に過去の日足、週足、月足の安値および高値で発生し、ここで大きな反応が予想されます。したがって、流動性とトレンドを把握できれば、適切に取引しやすくなります。

このシナリオの一例として、金価格が前週の高値に近づいていることが市場構造や移動平均線によって確認されている状況があります。週の途中やセッションが進行する中で、価格は前週高値に向かって動き続け、その過程でFVGが形成され続けます。これらのFVGは強気としてラベル付けされ、上位時間足(H4、D1、H1など)で形成される場合はさらに良く、強気としてマークされるべきです。トレーダーは、価格がこれらのギャップへ戻ったときに買い取引を想定し、そのポイントでエントリー機会を探すべきです。

重要なのは、ほとんどの場合、あるいはほぼ常に、価格はFVGをリバランスするという点です。しかし、価格が前述のように目標(週足、月足、日足の高値など)に近い場合、この目標に到達して流動性が吸収されると、そのFVGが新たな取引に使用されない可能性が大きく低下し、それが失敗した買いになる可能性があります。したがって、このようなタイプの取引は避けるべきです。


取引とそのアドレスにどのような影響を与えるか

基本的には、あらかじめトレンドと方向(たとえば弱気)が定まっている場合、トップダウン分析を通じてFVGを探し、マークしていくという考え方です。週次、日次、または月次のターゲットが維持されている限り、価格はこれらのFVG(価格)に戻り、取引やエントリーの選択肢を提供する点に注意してください。一般的に、価格は均衡を取り戻し、FVGを埋める必要があります。したがって、価格が二次的な買い手として戻ることで、より多くのトレーダーがこの時点で新しいポジションを開き、これがペアや資産に対する強さの原動力として機能し、市場のスピードと強さを説明します。これにより、価格が力強く上方向に変動します。さらに、トレンドの方向、バイアス、ナラティブを強化します。


FVGを効果的に利用して取引エントリーを実行する方法

これはこの記事で最も重要かつ価値のある情報です。すべての基準を徹底的に検証し分析した結果、FVGを無効化または有効化する場所と方法、さらに、それらがどのように形成され、どこで発生しやすいかを理解できたからです。すべての大変な作業はすでに終わっているため、この部分は簡略化されています。

ここで必要なのは、忍耐、規律、そして集中力です。参照チャートであるD1から主に分析をおこない、FVGを探します。FVGを検出し、価格がそこに戻ってきた場合、ギャップ内に入った時点ですぐにH4に切り替え、強気の包み足(トレンド、バイアス、ナラティブが買いの場合)または弱気の包み足(バイアス、トレンド、ナラティブが売りの場合)を探します。これはFVG内のどの地点でも発生し得ますが、理想的にはロンドンやニューヨークなどの取引セッション開始時付近で発生するのが望ましいです。包み足を検出したら、H1に切り替えます。ここでは市場構造のブレイクアウトを探します。

市場構造のブレイクアウトとは、価格の流れが変化することを指します。もし売り手側だったものが買い手側へ転じたのであれば、価格の流れが変わったことになり、したがって市場構造も変化したということになります。したがって、H1ではこれを探すことになります。ストップロスはブレイクアウトの反対側の端に置く必要があり、エントリーはH1のブレイクアウト時、または60%リトレースメントレベル(発生した場合)に形成される、小さく妥当な価格のギャップでのブレイクアウトで実行できます。

ブレイクアウトとは、市場構造が変化する前に形成された最後の高値または安値を突破する動きを指します。価格が売りで、最後の高値を急速かつ力強く上抜けた場合、強気のブレイクアウトとなります。逆に、価格が買いで、市場構造が変化し、最後の安値を急速かつ力強く下抜けた場合、弱気のブレイクアウトとなります。

同じ現象は他の時間足でも発生します。H4でFVGを検出した場合、H1で包み足を探し、続いてM15で市場構造の変化を探します。この戦略は、より短い時間足のトレーダーが適用できます。同じ現象が繰り返されます。H1でFVGが発生し、M15で包み足が出現することで、M5時間足で市場構造の変化が発生します。さらに、スキャルピングではM15でFVGが発生した場合、M5で包み足、M1で市場構造の変化を探すことができます。


FVGのある取引を避ける方法とタイミング

多くのFVGは取引機会を提供しますが、上で説明したように、一部のFVGは取引に適していません。

1. トレンド、バイアス、ナラティブの方向に逆行して発生するFVGは、機能しないことが多いです。たとえば、トレンドまたはバイアスが買いで、価格が下落しプルバックの過程でFVGを残したとしても、価格が上昇トレンドを再開した際に、その上にあるFVGが尊重されることは期待できません。価格はそれを素早く無視して通過します。

2. FVGが失敗しやすい第2のシナリオは、週次、日次、月次といった全体のターゲットが達成された後です。流動性がすでに吸収されているため、価格がその方向へのトレンドを継続する理由がなくなります。大口機関の利益確定もすでにおこなわれているため、市場に残っている強さや勢いは限られています。その結果、価格は引き戻しに向かったり、場合によっては反転したりする可能性があり、FVGが機能しなくなることがあります。

3. 第3のシナリオは、前述のとおり、下位の時間足で包み足や市場構造の変化が発生しない場合です。さらに、これらは非常に活発な取引セッション中に、かつ適切な順序で発生する必要があります。

FVGがまず形成され、続いて包み足が現れ、その後に市場構造の変化が起こります。したがって、この順序が確認できた場合にのみ、最高水準の質を持つセットアップで取引を実行できます。これは、前述の要素をすべて満たしていない場合、失敗する確率が高いため、そのような条件下ではこの手法やFVGを使用すべきではないことを明確に示しています。


上位足のFVG

上位足のFVGとは、H4、D1、W1で発生するFVGを指します。これらは短期的なボラティリティや一方向の急激な動きが少なく、より安定して緩やかに推移するため、200〜500ピップ規模の値動きや長期的なスイングにも適用しやすくなります。そのため、不安や恐れを抱かずに辛抱強くポジションを保有できる長期トレーダーに適しています。


下位足のFVG

下位足のFVGとは、H1、M15、M5で発生するFVGを指します。これらのFVGは、長期トレーダーではなく、主にスキャルパーから数時間の保有で取引を完結させる短期トレーダーまで、短期志向のトレーダーによって理想的に利用されるものです。特にM15のトレンド環境で発生しやすく、活発なセッションでは非常に信頼性が高くなります。高速な執行を前提としており、100〜150ピップ程度を容易に狙うことができます。価格動向の強さとスピードに応じて、短期的な値動きや高いボラティリティを伴うのが特徴であり、迅速に判断を下し、こうした環境に強いトレーダーが求められます。


MQL5におけるFVGを用いた自動売買

この戦略を自動化するために、私は以下を作成しました。

  • FVGを検出してマッピングするためのFVGインジケーター
  • FVG検出を基に売買し、エントリーのためにMSSを組み込んだFVG + MSS EA(エキスパートアドバイザー)

FVGインジケーターのソースコード

このインジケーターは、大きく、重なりのないローソク足(不均衡が安値を上回るもの)を探すことでFVGを検出します。視覚的なギャップを示すために、インジケーターは矩形を描画します。

#property copyright "Eugene Mmene"
#property link      "EMcapital"
#property version   "1.0"

input int minPts = 100; // Minimum points for FVG gap to be valid
input int FVG_Rec_Ext_Bars = 10; // Length of FVG rectangle in bars
input bool DrawFVG = true; // Draw FVG rectangles on chart

#define FVG_Prefix "FVG_REC_"
#define CLR_UP clrLime
#define CLR_DOWN clrRed

struct TimeframeData {
   ENUM_TIMEFRAMES tf;
   datetime lastBar;
};

TimeframeData tfs[];
string eaSymbol;

void CreateRec(string objName, datetime time1, double price1, datetime time2, double price2, color clr) {
   Print("CreateRec called: objName=", objName, ", time1=", TimeToString(time1), ", price1=", DoubleToString(price1, _Digits));
   if(DrawFVG && ObjectFind(0, objName) < 0) {
      if(ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2)) {
         ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1);
         ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1);
         ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2);
         ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2);
         ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
         ObjectSetInteger(0, objName, OBJPROP_FILL, true);
         ObjectSetInteger(0, objName, OBJPROP_BACK, false);
         Print("CreateRec: Rectangle created successfully: ", objName);
      } else {
         Print("CreateRec: Failed to create rectangle: ", objName, ", Error=", GetLastError());
      }
   } else {
      Print("CreateRec: Skipped - DrawFVG=", DrawFVG, ", Object exists=", ObjectFind(0, objName) >= 0);
   }
}

void DetectFVGs() {
   Print("DetectFVGs started");
   int maxObjects = 100; // Limit total objects to prevent overload
   int currentObjects = ObjectsTotal(0, 0, OBJ_RECTANGLE);
   if(currentObjects >= maxObjects) {
      Print("DetectFVGs: Object limit reached (", currentObjects, "/", maxObjects, "), skipping FVG creation");
      return;
   }

   for(int i = 0; i < ArraySize(tfs); i++) {
      if(!NewBar(tfs[i].tf)) continue;
      double low0 = iLow(eaSymbol, tfs[i].tf, 0);
      double high2 = iHigh(eaSymbol, tfs[i].tf, 2);
      double gap_L0_H2 = NormalizeDouble((low0 - high2) / _Point, 0);
      
      double high0 = iHigh(eaSymbol, tfs[i].tf, 0);
      double low2 = iLow(eaSymbol, tfs[i].tf, 2);
      double gap_H0_L2 = NormalizeDouble((low2 - high0) / _Point, 0);
      
      if(gap_L0_H2 > minPts) {
         string fvgName = FVG_Prefix + EnumToString(tfs[i].tf) + "_" + TimeToString(iTime(eaSymbol, tfs[i].tf, 0), TIME_DATE|TIME_MINUTES);
         Print(StringFormat("%s Bullish FVG detected: Low=%s, High[2]=%s, Gap=%s points", 
               EnumToString(tfs[i].tf), DoubleToString(low0, _Digits), DoubleToString(high2, _Digits), DoubleToString(gap_L0_H2, 0)));
         CreateRec(fvgName, iTime(eaSymbol, tfs[i].tf, 0), high2, iTime(eaSymbol, tfs[i].tf, 0) + PeriodSeconds(tfs[i].tf) * FVG_Rec_Ext_Bars, low0, CLR_UP);
      }
      if(gap_H0_L2 > minPts) {
         string fvgName = FVG_Prefix + EnumToString(tfs[i].tf) + "_" + TimeToString(iTime(eaSymbol, tfs[i].tf, 0), TIME_DATE|TIME_MINUTES);
         Print(StringFormat("%s Bearish FVG detected: High=%s, Low[2]=%s, Gap=%s points", 
               EnumToString(tfs[i].tf), DoubleToString(high0, _Digits), DoubleToString(low2, _Digits), DoubleToString(gap_H0_L2, 0)));
         CreateRec(fvgName, iTime(eaSymbol, tfs[i].tf, 0), low2, iTime(eaSymbol, tfs[i].tf, 0) + PeriodSeconds(tfs[i].tf) * FVG_Rec_Ext_Bars, high0, CLR_DOWN);
      }
   }
   if(DrawFVG) ChartRedraw(0); // Single redraw per tick
   Print("DetectFVGs completed");
}

bool NewBar(ENUM_TIMEFRAMES tf) {
   int idx = TimeframeIndex(tf);
   if(idx < 0) return false;
   datetime cur = iTime(eaSymbol, tf, 0);
   if(cur != tfs[idx].lastBar) {
      tfs[idx].lastBar = cur;
      return true;
   }
   return false;
}

int TimeframeIndex(ENUM_TIMEFRAMES tf) {
   for(int i = 0; i < ArraySize(tfs); i++) {
      if(tfs[i].tf == tf) return i;
   }
   return -1;
}

int OnInit() {
   eaSymbol = _Symbol;
   if(!SymbolSelect(eaSymbol, true)) {
      Print("Error: Failed to select ", eaSymbol, " in Market Watch");
      return(INIT_FAILED);
   }

   ArrayResize(tfs, 3);
   tfs[0].tf = PERIOD_M5;
   tfs[1].tf = PERIOD_M15;
   tfs[2].tf = PERIOD_H1;
   for(int i = 0; i < 3; i++) {
      tfs[i].lastBar = 0;
   }

   Print("FVG Detector initialized for ", eaSymbol, ". Timeframes: M5, M15, H1");
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
   if(DrawFVG) ObjectsDeleteAll(0, FVG_Prefix);
   Print("FVG Detector stopped: ", reason);
}

void OnTick() {
   DetectFVGs();
}

インストール:MetaEditorでコンパイルし、チャートに適用してください。強気のFVGには緑の矩形、弱気のFVGには赤の矩形を描画します。

使用例:GOLDのM15チャートで、視覚的な分析のためにギャップを検出します。


FVG + MSS EAのソースコード

このEAはFVGを検出し、価格がギャップへ向けてプルバックするのを待ち、直近の高値/安値(LH)のブレイクアウトを確認してエントリーします。また、リスク管理として、1取引あたり2%のリスク設定を含んでいます。  

#property copyright "Eugene Mmene"
#property link      "EMcapital"
#property version   "2.27.2"

#include <Trade\Trade.mqh>

input double RiskPct = 2.0; // Base risk per trade %
input double MaxLossUSD = 110.0; // Maximum loss per trade in USD
input double RecTgt = 7000.0; // Equity recovery target
input int ATR_Prd = 14; // ATR period
input int Brk_Prd = 10; // Breakout period
input int EMA_Prd = 20; // EMA period
input string GS_Url = ""; // Google Sheets webhook URL
input bool NewsFilt = true; // News filter
input int NewsPause = 15; // Pause minutes
input double MinBrkStr = 0.1; // Min breakout strength (x ATR)
input int Vol_Prd = 1; // Volume period
input bool Bypass = true; // Bypass volume, breakout, HTF trend filters for testing
input bool useHTF = false; // Use D1 or H4 EMA trend filter
input string NewsAPI_Url = "https://www.alphavantage.co/query?function=NEWS_SENTIMENT&apikey="; // Alpha Vantage API URL
input string NewsAPI_Key = "pub_3f54bba977384ac19b6839a744444aba"; // Alpha Vantage API key
input double DailyDDLimit = 2.5; // Daily drawdown limit (%)
input double OverallDDLimit = 5.5; // Overall drawdown limit (%)
input double TargetBalanceOrEquity = 6600.0; // Target balance or equity to pass challenge ($)
input bool ResetProfitTarget = false; // Reset target to resume trading
input int minPts = 100; // Minimum points for FVG gap to be valid
input int FVG_Rec_Ext_Bars = 10; // Length of FVG rectangle in bars
input bool DrawFVG = true; // Draw FVG rectangles on chart

double CurRisk = RiskPct;
double OrigRisk = RiskPct;
double LastEqHigh = 0;
double StartingBalance = 0;
double DailyBalance = 0;
datetime LastDay = 0;
bool ProfitTargetReached = false;
bool DailyDDPaused = false;
CTrade trade;
int h_ema_d1 = INVALID_HANDLE;
int h_ema_h4 = INVALID_HANDLE;
int winStreak = 0;
int lossStreak = 0;
string eaSymbol = _Symbol;

struct TimeframeData {
   ENUM_TIMEFRAMES tf;
   int h_atr;
   int h_vol;
   int h_vol_ma;
   datetime lastSig;
   datetime lastBar;
};

TimeframeData tfs[];
struct NewsEvt { 
   datetime time; 
   string evt; 
   int impact; 
};
NewsEvt newsCal[];
int newsCnt = 0;

struct TradeLog {
   ulong ticket;
   bool isWin;
   double profit;
   double brkStr;
   double vol;
   double risk;
   ENUM_TIMEFRAMES tf;
};
TradeLog tradeHistory[];
int tradeCnt = 0;
double dynBrkStr = MinBrkStr;

#define FVG_Prefix "FVG_REC_"
#define CLR_UP clrLime
#define CLR_DOWN clrRed

void CreateRec(string objName, datetime time1, double price1, datetime time2, double price2, color clr) {
   Print("CreateRec called: objName=", objName, ", time1=", TimeToString(time1), ", price1=", DoubleToString(price1, _Digits));
   if(DrawFVG && ObjectFind(0, objName) < 0) {
      if(ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2)) {
         ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1);
         ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1);
         ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2);
         ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2);
         ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
         ObjectSetInteger(0, objName, OBJPROP_FILL, true);
         ObjectSetInteger(0, objName, OBJPROP_BACK, false);
         Print("CreateRec: Rectangle created successfully: ", objName);
      } else {
         Print("CreateRec: Failed to create rectangle: ", objName, ", Error=", GetLastError());
      }
   } else {
      Print("CreateRec: Skipped - DrawFVG=", DrawFVG, ", Object exists=", ObjectFind(0, objName) >= 0);
   }
}

void DetectFVGs() {
   Print("DetectFVGs started");
   int maxObjects = 100; // Limit total objects to prevent overload
   int currentObjects = ObjectsTotal(0, 0, OBJ_RECTANGLE);
   if(currentObjects >= maxObjects) {
      Print("DetectFVGs: Object limit reached (", currentObjects, "/", maxObjects, "), skipping FVG creation");
      return;
   }

   for(int i = 0; i < ArraySize(tfs); i++) {
      if(!NewBar(tfs[i].tf)) continue;
      double low0 = iLow(eaSymbol, tfs[i].tf, 0);
      double high2 = iHigh(eaSymbol, tfs[i].tf, 2);
      double gap_L0_H2 = NormalizeDouble((low0 - high2) / _Point, 0);
      
      double high0 = iHigh(eaSymbol, tfs[i].tf, 0);
      double low2 = iLow(eaSymbol, tfs[i].tf, 2);
      double gap_H0_L2 = NormalizeDouble((low2 - high0) / _Point, 0);
      
      if(gap_L0_H2 > minPts) {
         string fvgName = FVG_Prefix + EnumToString(tfs[i].tf) + "_" + TimeToString(iTime(eaSymbol, tfs[i].tf, 0), TIME_DATE|TIME_MINUTES);
         Print(StringFormat("%s Bullish FVG detected: Low=%s, High[2]=%s, Gap=%s points", 
               EnumToString(tfs[i].tf), DoubleToString(low0, _Digits), DoubleToString(high2, _Digits), DoubleToString(gap_L0_H2, 0)));
         CreateRec(fvgName, iTime(eaSymbol, tfs[i].tf, 0), high2, iTime(eaSymbol, tfs[i].tf, 0) + PeriodSeconds(tfs[i].tf) * FVG_Rec_Ext_Bars, low0, CLR_UP);
      }
      if(gap_H0_L2 > minPts) {
         string fvgName = FVG_Prefix + EnumToString(tfs[i].tf) + "_" + TimeToString(iTime(eaSymbol, tfs[i].tf, 0), TIME_DATE|TIME_MINUTES);
         Print(StringFormat("%s Bearish FVG detected: High=%s, Low[2]=%s, Gap=%s points", 
               EnumToString(tfs[i].tf), DoubleToString(high0, _Digits), DoubleToString(low2, _Digits), DoubleToString(gap_H0_L2, 0)));
         CreateRec(fvgName, iTime(eaSymbol, tfs[i].tf, 0), low2, iTime(eaSymbol, tfs[i].tf, 0) + PeriodSeconds(tfs[i].tf) * FVG_Rec_Ext_Bars, high0, CLR_DOWN);
      }
   }
   if(DrawFVG) ChartRedraw(0); // Single redraw per tick
   Print("DetectFVGs completed");
}

int OnInit() {
   if(AccountInfoDouble(ACCOUNT_BALANCE) < 10.0) {
      Print("Low balance: ", DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2));
      return(INIT_FAILED);
   }
   string sym = Symbol();
   bool selected;
   if(!SymbolExist(sym, selected)) {
      Print("Error: Symbol ", sym, " not found in Market Watch. Available: ", _Symbol);
      eaSymbol = _Symbol;
   } else {
      eaSymbol = sym;
      Print("Symbol validated: ", eaSymbol, ", Selected in Market Watch: ", selected);
   }
   if(!SymbolSelect(eaSymbol, true)) {
      Print("Error: Failed to select ", eaSymbol, " in Market Watch");
      return(INIT_FAILED);
   }

   Print("Please ensure ", NewsAPI_Url, " is added to allowed WebRequest URLs in MT5 settings");

   StartingBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   LastEqHigh = AccountInfoDouble(ACCOUNT_EQUITY);
   DailyBalance = StartingBalance;
   LastDay = TimeCurrent() / 86400 * 86400;
   ProfitTargetReached = ResetProfitTarget ? false : ProfitTargetReached;
   DailyDDPaused = false;
   ArrayResize(newsCal, 100);
   ArrayResize(tradeHistory, 100);
   ArrayResize(tfs, 3);
   tfs[0].tf = PERIOD_M5;
   tfs[1].tf = PERIOD_M15;
   tfs[2].tf = PERIOD_H1;
   for(int i = 0; i < 3; i++) {
      tfs[i].h_atr = iATR(eaSymbol, tfs[i].tf, ATR_Prd);
      tfs[i].h_vol = iVolumes(eaSymbol, tfs[i].tf, VOLUME_TICK);
      tfs[i].h_vol_ma = iMA(eaSymbol, tfs[i].tf, Vol_Prd, 0, MODE_SMA, PRICE_CLOSE);
      tfs[i].lastSig = 0;
      tfs[i].lastBar = 0;
      if(tfs[i].h_atr == INVALID_HANDLE || tfs[i].h_vol == INVALID_HANDLE || tfs[i].h_vol_ma == INVALID_HANDLE) {
         Print("Indicator init failed for ", EnumToString(tfs[i].tf));
         return(INIT_FAILED);
      }
   }
   h_ema_d1 = iMA(eaSymbol, PERIOD_D1, EMA_Prd, 0, MODE_EMA, PRICE_CLOSE);
   h_ema_h4 = iMA(eaSymbol, PERIOD_H4, EMA_Prd, 0, MODE_EMA, PRICE_CLOSE);
   if(h_ema_d1 == INVALID_HANDLE || h_ema_h4 == INVALID_HANDLE) {
      Print("EMA init failed");
      return(INIT_FAILED);
   }
   if(NewsFilt) FetchNewsCalendar();
   Print("EA initialized. Timeframes: M5, M15, H1, News events: ", newsCnt, ", Bypass: ", Bypass, ", UseHTF: ", useHTF, 
         ", Time时间: EAT (UTC+3), Server: ", TerminalInfoString(TERMINAL_NAME), 
         ", Starting Balance: ", DoubleToString(StartingBalance, 2), ", Target Balance/Equity: ", DoubleToString(TargetBalanceOrEquity, 2));
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
   if(h_ema_d1 != INVALID_HANDLE) IndicatorRelease(h_ema_d1);
   if(h_ema_h4 != INVALID_HANDLE) IndicatorRelease(h_ema_h4);
   for(int i = 0; i < ArraySize(tfs); i++) {
      if(tfs[i].h_atr != INVALID_HANDLE) IndicatorRelease(tfs[i].h_atr);
      if(tfs[i].h_vol != INVALID_HANDLE) IndicatorRelease(tfs[i].h_vol);
      if(tfs[i].h_vol_ma != INVALID_HANDLE) IndicatorRelease(tfs[i].h_vol_ma);
   }
   if(DrawFVG) ObjectsDeleteAll(0, FVG_Prefix);
   Print("EA stopped: ", reason);
}

void CloseAllPositions() {
   for(int i = PositionsTotal() - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket) || PositionGetString(POSITION_SYMBOL) != eaSymbol) continue;
      long magic = PositionGetInteger(POSITION_MAGIC);
      if(magic == MagicNumber(PERIOD_M5) || magic == MagicNumber(PERIOD_M15) || magic == MagicNumber(PERIOD_H1)) {
         trade.PositionClose(ticket);
         Print("Closed position: Ticket=", ticket, ", Symbol=", eaSymbol, ", Magic=", magic);
      }
   }
}

void OnTick() {
   datetime currentDay = TimeCurrent() / 86400 * 86400;
   if(currentDay > LastDay) {
      DailyBalance = AccountInfoDouble(ACCOUNT_BALANCE);
      LastDay = currentDay;
      DailyDDPaused = false;
      Print("Daily balance reset: ", DoubleToString(DailyBalance, 2), " at ", TimeToString(currentDay, TIME_DATE));
   }

   double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   if(balance >= TargetBalanceOrEquity || equity >= TargetBalanceOrEquity) {
      CloseAllPositions();
      ProfitTargetReached = true;
      Print("Trading paused: Target balance or equity reached. Balance=", DoubleToString(balance, 2), ", Equity=", DoubleToString(equity, 2), 
            ", Target=", DoubleToString(TargetBalanceOrEquity, 2), ". All positions closed. Set ResetProfitTarget=true or restart EA to resume trading.");
      return;
   }

   double dailyDD = (DailyBalance - equity) / DailyBalance * 100;
   double overallDD = (StartingBalance - equity) / StartingBalance * 100;

   if(dailyDD >= DailyDDLimit) {
      CloseAllPositions();
      DailyDDPaused = true;
      Print("Trading paused until next trading day: Daily DD=", StringFormat("%.2f", dailyDD), "% reached (Limit: ", DoubleToString(DailyDDLimit, 2), 
            "%), Equity=", DoubleToString(equity, 2), ", Daily Balance=", DoubleToString(DailyBalance, 2), ". All positions closed.");
      return;
   }

   if(overallDD >= OverallDDLimit) {
      CloseAllPositions();
      ProfitTargetReached = true;
      Print("Trading paused: Overall DD=", StringFormat("%.2f", overallDD), "% reached (Limit: ", DoubleToString(OverallDDLimit, 2), 
            "%), Equity=", DoubleToString(equity, 2), ", Starting Balance=", DoubleToString(StartingBalance, 2), ". All positions closed. Set ResetProfitTarget=true or restart EA to resume trading.");
      return;
   }

   if(ProfitTargetReached) {
      Print("Trading paused: Target or overall drawdown previously reached. Balance=", DoubleToString(balance, 2), ", Equity=", DoubleToString(equity, 2), ", Target=", DoubleToString(TargetBalanceOrEquity, 2));
      return;
   }

   if(DailyDDPaused) {
      Print("Trading paused: Daily drawdown limit previously reached. Waiting for next trading day. Equity=", DoubleToString(equity, 2), ", Daily Balance=", DoubleToString(DailyBalance, 2));
      return;
   }

   static datetime lastNewsFetch = 0;
   if(NewsFilt && TimeCurrent() >= lastNewsFetch + 4 * 3600) {
      FetchNewsCalendar();
      lastNewsFetch = TimeCurrent();
   }

   for(int i = 0; i < ArraySize(tfs); i++) {
      if(!NewBar(tfs[i].tf)) continue;

      bool hasPosition = false;
      for(int j = PositionsTotal() - 1; j >= 0; j--) {
         ulong ticket = PositionGetTicket(j);
         if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == eaSymbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber(tfs[i].tf)) {
            hasPosition = true;
            break;
         }
      }
      if(hasPosition) {
         ManageTrades(tfs[i].tf);
         continue;
      }

      if(TimeCurrent() <= tfs[i].lastSig + PeriodSeconds(PERIOD_H1)) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: Within 1-hour cooldown");
         continue;
      }

      if(NewsFilt && IsNews()) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: News pause active");
         continue;
      }

      double eq = AccountInfoDouble(ACCOUNT_EQUITY);
      if(eq > LastEqHigh) LastEqHigh = eq;
      if(eq < LastEqHigh && lossStreak >= 2) CurRisk = MathMax(OrigRisk * 0.25, 0.1);
      else if(winStreak >= 3) CurRisk = MathMin(OrigRisk * 1.25, 5.0);
      else CurRisk = OrigRisk;
      if(eq >= RecTgt) {
         CurRisk = OrigRisk;
         winStreak = 0;
         lossStreak = 0;
      }

      bool bullHTF = !useHTF || (BullTrend(PERIOD_D1) || BullTrend(PERIOD_H4));
      bool bearHTF = !useHTF || (BearTrend(PERIOD_D1) || BearTrend(PERIOD_H4));
      bool buyBrk = BuyBrk(tfs[i].tf);
      bool sellBrk = SellBrk(tfs[i].tf);
      Print(EnumToString(tfs[i].tf), " Signal check: BullHTF=", bullHTF, ", BearHTF=", bearHTF, ", BuyBrk=", buyBrk, ", SellBrk=", sellBrk, 
            ", Bid=", DoubleToString(SymbolInfoDouble(eaSymbol, SYMBOL_BID), _Digits), ", Ask=", DoubleToString(SymbolInfoDouble(eaSymbol, SYMBOL_ASK), _Digits));

      double atr[1], vol[2], vol_ma[1];
      if(CopyBuffer(tfs[i].h_atr, 0, 0, 1, atr) < 1 || 
         CopyBuffer(tfs[i].h_vol, 0, 0, 2, vol) < 2 || 
         CopyBuffer(tfs[i].h_vol_ma, 0, 1, 1, vol_ma) < 1) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: Indicator copy failed");
         continue;
      }

      double slPips = atr[0] * 2 / _Point;
      double lots = CalcLots(eq, CurRisk, slPips);
      double margReq = SymbolInfoDouble(eaSymbol, SYMBOL_MARGIN_INITIAL) * lots;
      double freeMarg = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
      Print(EnumToString(tfs[i].tf), " Lot size: ", DoubleToString(lots, 2), ", Margin required: ", DoubleToString(margReq, 2), 
            ", Free margin: ", DoubleToString(freeMarg, 2), ", SL Pips: ", DoubleToString(slPips, 2), 
            ", Contract size: ", DoubleToString(SymbolInfoDouble(eaSymbol, SYMBOL_TRADE_CONTRACT_SIZE), 0));

      if(freeMarg < margReq * 1.2) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: Margin low (", DoubleToString(freeMarg, 2), " < ", DoubleToString(margReq * 1.2, 2), ")");
         continue;
      }

      double brkStr = MathAbs(buyBrk ? SymbolInfoDouble(eaSymbol, SYMBOL_ASK) - iHigh(eaSymbol, tfs[i].tf, iHighest(eaSymbol, tfs[i].tf, MODE_HIGH, Brk_Prd, 1)) :
                                     iLow(eaSymbol, tfs[i].tf, iLowest(eaSymbol, tfs[i].tf, MODE_LOW, Brk_Prd, 1)) - SymbolInfoDouble(eaSymbol, SYMBOL_BID)) / atr[0];
      if(!Bypass && brkStr < dynBrkStr) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: Breakout strength too low (", DoubleToString(brkStr, 2), " < ", DoubleToString(dynBrkStr, 2), ")");
         continue;
      }

      if(bullHTF && buyBrk) {
         double price = SymbolInfoDouble(eaSymbol, SYMBOL_ASK);
         double sl = price - slPips * _Point;
         double tp = price + slPips * 2 * _Point;
         Print(EnumToString(tfs[i].tf), " Attempting Buy: Price=", DoubleToString(price, _Digits), ", SL=", DoubleToString(sl, _Digits), 
               ", TP=", DoubleToString(tp, _Digits), ", Lots=", DoubleToString(lots, 2));
         trade.SetExpertMagicNumber(MagicNumber(tfs[i].tf));
         if(trade.Buy(lots, eaSymbol, price, sl, tp)) {
            tfs[i].lastSig = TimeCurrent();
            LogTrd(trade.ResultOrder(), eaSymbol, price, sl, tp, "Open", brkStr, vol[1], CurRisk, tfs[i].tf);
            Print(EnumToString(tfs[i].tf), " Buy opened: Ticket=", trade.ResultOrder());
         } else {
            Print(EnumToString(tfs[i].tf), " Buy failed: Retcode=", trade.ResultRetcode(), ", Error=", GetLastError(), ", Comment=", trade.ResultComment());
         }
      } else if(bearHTF && sellBrk) {
         double price = SymbolInfoDouble(eaSymbol, SYMBOL_BID);
         double sl = price + slPips * _Point;
         double tp = price - slPips * 2 * _Point;
         Print(EnumToString(tfs[i].tf), " Attempting Sell: Price=", DoubleToString(price, _Digits), ", SL=", DoubleToString(sl, _Digits), 
               ", TP=", DoubleToString(tp, _Digits), ", Lots=", DoubleToString(lots, 2));
         trade.SetExpertMagicNumber(MagicNumber(tfs[i].tf));
         if(trade.Sell(lots, eaSymbol, price, sl, tp)) {
            tfs[i].lastSig = TimeCurrent();
            LogTrd(trade.ResultOrder(), eaSymbol, price, sl, tp, "Open", brkStr, vol[1], CurRisk, tfs[i].tf);
            Print(EnumToString(tfs[i].tf), " Sell opened: Ticket=", trade.ResultOrder());
         } else {
            Print(EnumToString(tfs[i].tf), " Sell failed: Retcode=", trade.ResultRetcode(), ", Error=", GetLastError(), ", Comment=", trade.ResultComment());
         }
      } else {
         Print(EnumToString(tfs[i].tf), " Trade skipped: No valid signal");
      }
   }

   // Run FVG detection after trade logic to ensure non-interference
   DetectFVGs();
}

void ManageTrades(ENUM_TIMEFRAMES tf) {
   for(int i = PositionsTotal() - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket) || PositionGetString(POSITION_SYMBOL) != eaSymbol || PositionGetInteger(POSITION_MAGIC) != MagicNumber(tf)) continue;
      double openPrice, sl, tp, currPrice, lots, profit;
      if(!PositionGetDouble(POSITION_PRICE_OPEN, openPrice) ||
         !PositionGetDouble(POSITION_SL, sl) ||
         !PositionGetDouble(POSITION_TP, tp) ||
         !PositionGetDouble(POSITION_VOLUME, lots) ||
         !PositionGetDouble(POSITION_PROFIT, profit)) continue;
      currPrice = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? SymbolInfoDouble(eaSymbol, SYMBOL_BID) : SymbolInfoDouble(eaSymbol, SYMBOL_ASK);
      int idx = TimeframeIndex(tf);
      if(idx < 0) continue;
      double atr[1];
      if(CopyBuffer(tfs[idx].h_atr, 0, 0, 1, atr) < 1) continue;

      if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && currPrice >= openPrice + (openPrice - sl) * 3) ||
         (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && currPrice <= openPrice - (sl - openPrice) * 3)) {
         if(lots > SymbolInfoDouble(eaSymbol, SYMBOL_VOLUME_MIN) * 2) {
            trade.PositionClosePartial(ticket, lots / 2);
            trade.PositionModify(ticket, openPrice, tp);
            Print(EnumToString(tf), " Partial close at 1:3 RR: Ticket=", ticket, ", Lots=", DoubleToString(lots / 2, 2));
         }
      }

      double trail = atr[0] * 1.5 / _Point;
      if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && currPrice > openPrice + trail * _Point && sl < currPrice - trail * _Point) {
         trade.PositionModify(ticket, currPrice - trail * _Point, tp);
         Print(EnumToString(tf), " Trailing stop updated: Ticket=", ticket, ", New SL=", DoubleToString(currPrice - trail * _Point, _Digits));
      } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && currPrice < openPrice - trail * _Point && sl > currPrice + trail * _Point) {
         trade.PositionModify(ticket, currPrice + trail * _Point, tp);
         Print(EnumToString(tf), " Trailing stop updated: Ticket=", ticket, ", New SL=", DoubleToString(currPrice + trail * _Point, _Digits));
      }

      if(profit != 0 && !PositionSelectByTicket(ticket)) {
         LogTrd(ticket, eaSymbol, openPrice, sl, tp, "Close", 0, 0, CurRisk, tf);
         Print(EnumToString(tf), " Position closed: Ticket=", ticket, ", Profit=", DoubleToString(profit, 2));
      }
   }
}

void FetchNewsCalendar() {
   string url = NewsAPI_Url + NewsAPI_Key;
   string headers = "";
   char post[], result[];
   string result_headers;
   int timeout = 5000;
   int res = WebRequest("GET", url, headers, timeout, post, result, result_headers);
   if(res != 200) {
      Print("News API request failed: HTTP ", res, ", Error=", GetLastError());
      newsCal[0].time = StringToTime("2025.07.15 14:30");
      newsCal[0].evt = "CPI";
      newsCal[0].impact = 90;
      newsCal[1].time = StringToTime("2025.07.23 20:00");
      newsCal[1].evt = "FOMC";
      newsCal[1].impact = 90;
      newsCnt = 2;
      Print("Using fallback news calendar with ", newsCnt, " events");
      return;
   }

   string response = CharArrayToString(result);
   newsCnt = 0;
   ArrayResize(newsCal, 100);

   int pos = 0;
   while(pos >= 0 && newsCnt < 100) {
      pos = StringFind(response, "\"items\":", pos);
      if(pos < 0) break;
      pos = StringFind(response, "{", pos);
      if(pos < 0) break;

      int end = StringFind(response, "}", pos);
      if(end < 0) break;
      string item = StringSubstr(response, pos, end - pos + 1);

      string evtName = ExtractJsonField(item, "\"title\":");
      string evtTime = ExtractJsonField(item, "\"time_published\":");
      string relevance = ExtractJsonField(item, "\"relevance_score\":");

      if(evtName != "" && evtTime != "") {
         string dt = StringSubstr(evtTime, 0, 4) + "." + StringSubstr(evtTime, 4, 2) + "." + StringSubstr(evtTime, 6, 2) + " " + 
                     StringSubstr(evtTime, 9, 2) + ":" + StringSubstr(evtTime, 11, 2);
         newsCal[newsCnt].time = StringToTime(dt);
         newsCal[newsCnt].evt = evtName;
         newsCal[newsCnt].impact = (relevance != "") ? (int)(StringToDouble(relevance) * 100) : 80;
         if(newsCal[newsCnt].impact > 80 && newsCal[newsCnt].time > TimeCurrent() - 7 * 86400) {
            newsCnt++;
            Print("News event loaded: ", evtName, " at ", TimeToString(newsCal[newsCnt-1].time), ", Impact=", newsCal[newsCnt-1].impact);
         }
      }
      pos = end + 1;
   }
   Print("Loaded ", newsCnt, " high-impact news events from API");
}

string ExtractJsonField(string json, string field) {
   int pos = StringFind(json, field);
   if(pos < 0) return "";
   pos += StringLen(field);
   if(StringFind(json, "\"", pos) == pos) pos++;
   int end = StringFind(json, "\"", pos);
   if(end < 0) return "";
   return StringSubstr(json, pos, end - pos);
}

bool NewBar(ENUM_TIMEFRAMES tf) {
   int idx = TimeframeIndex(tf);
   if(idx < 0) return false;
   datetime cur = iTime(eaSymbol, tf, 0);
   if(cur != tfs[idx].lastBar) {
      tfs[idx].lastBar = cur;
      return true;
   }
   return false;
}

bool BullTrend(ENUM_TIMEFRAMES tf) {
   int handle = (tf == PERIOD_D1) ? h_ema_d1 : h_ema_h4;
   double ema[2];
   if(CopyBuffer(handle, 0, 1, 2, ema) < 2) {
      Print("EMA copy failed for ", EnumToString(tf));
      return false;
   }
   Print("EMA ", EnumToString(tf), ": ", DoubleToString(ema[1], _Digits), " vs ", DoubleToString(ema[0], _Digits));
   return ema[1] > ema[0];
}

bool BearTrend(ENUM_TIMEFRAMES tf) {
   int handle = (tf == PERIOD_D1) ? h_ema_d1 : h_ema_h4;
   double ema[2];
   if(CopyBuffer(handle, 0, 1, 2, ema) < 2) {
      Print("EMA copy failed for ", EnumToString(tf));
      return false;
   }
   Print("EMA ", EnumToString(tf), ": ", DoubleToString(ema[1], _Digits), " vs ", DoubleToString(ema[0], _Digits));
   return ema[1] < ema[0];
}

bool BuyBrk(ENUM_TIMEFRAMES tf) {
   double high = iHigh(eaSymbol, tf, iHighest(eaSymbol, tf, MODE_HIGH, Brk_Prd, 1));
   double price = SymbolInfoDouble(eaSymbol, SYMBOL_ASK);
   Print(EnumToString(tf), " BuyBrk check: Price=", DoubleToString(price, _Digits), ", High=", DoubleToString(high, _Digits));
   return price > high;
}

bool SellBrk(ENUM_TIMEFRAMES tf) {
   double low = iLow(eaSymbol, tf, iLowest(eaSymbol, tf, MODE_LOW, Brk_Prd, 1));
   double price = SymbolInfoDouble(eaSymbol, SYMBOL_BID);
   Print(EnumToString(tf), " SellBrk check: Price=", DoubleToString(price, _Digits), ", Low=", DoubleToString(low, _Digits));
   return price < low;
}

double CalcLots(double eq, double riskPct, double slPips) {
   double pipVal = SymbolInfoDouble(eaSymbol, SYMBOL_TRADE_TICK_VALUE);
   double riskAmt = MathMin(eq * (riskPct / 100), MaxLossUSD);
   double lots = riskAmt / (slPips * pipVal);
   double minLot = SymbolInfoDouble(eaSymbol, SYMBOL_VOLUME_MIN);
   double maxLot = SymbolInfoDouble(eaSymbol, SYMBOL_VOLUME_MAX);
   lots = NormalizeDouble(MathMax(minLot, MathMin(maxLot, lots)), 2);
   Print("CalcLots: Equity=", DoubleToString(eq, 2), ", Risk%=", DoubleToString(riskPct, 2), ", SL Pips=", DoubleToString(slPips, 2), 
         ", PipVal=", DoubleToString(pipVal, 2), ", Lots=", DoubleToString(lots, 2), ", MinLot=", DoubleToString(minLot, 2), ", MaxLot=", DoubleToString(maxLot, 2));
   return lots;
}

bool IsNews() {
   if(newsCnt == 0 && NewsFilt) {
      Print("No news events loaded, bypassing news filter");
      return false;
   }
   datetime now = TimeCurrent();
   Print("News check: Current time=", TimeToString(now, TIME_DATE|TIME_MINUTES));
   for(int i = 0; i < newsCnt; i++) {
      if(now >= newsCal[i].time - NewsPause * 60 && now <= newsCal[i].time + NewsPause * 60 && newsCal[i].impact > 80) {
         Print("News event active: ", newsCal[i].evt, " at ", TimeToString(newsCal[i].time, TIME_DATE|TIME_MINUTES));
         return true;
      }
   }
   Print("No active news events");
   return false;
}

void LogTrd(ulong ticket, string sym, double price, double sl, double tp, string stat, double brkStr, double vol, double risk, ENUM_TIMEFRAMES tf) {
   string data = StringFormat("T=%I64u,S=%s,Tm=%s,P=%f,SL=%f,TP=%f,St=%s,BrkStr=%f,Vol=%f,Risk=%f,TF=%s",
      ticket, sym, TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES), price, sl, tp, stat, brkStr, vol, risk, EnumToString(tf));
   if(StringLen(GS_Url) > 0) Print("Webhook pending: ", data);
   Print("Trd: ", data);

   if(stat == "Close" && tradeCnt < ArraySize(tradeHistory)) {
      double profit;
      if(PositionSelectByTicket(ticket)) {
         PositionGetDouble(POSITION_PROFIT, profit);
         tradeHistory[tradeCnt].ticket = ticket;
         tradeHistory[tradeCnt].isWin = profit > 0;
         tradeHistory[tradeCnt].profit = profit;
         tradeHistory[tradeCnt].brkStr = brkStr;
         tradeHistory[tradeCnt].vol = vol;
         tradeHistory[tradeCnt].risk = risk;
         tradeHistory[tradeCnt].tf = tf;
         tradeCnt++;
         UpdateWinLossStreak();
         AdjustBreakoutStrength();
      }
   }
}

void UpdateWinLossStreak() {
   if(tradeCnt > 0) {
      if(tradeHistory[tradeCnt-1].isWin) {
         winStreak++;
         lossStreak = 0;
      } else {
         lossStreak++;
         winStreak = 0;
      }
   }
}

void AdjustBreakoutStrength() {
   if(tradeCnt < 10) return;
   int lossCnt = 0;
   double avgBrkStr = 0;
   for(int i = tradeCnt - 10; i < tradeCnt; i++) {
      if(!tradeHistory[i].isWin) lossCnt++;
      avgBrkStr += tradeHistory[i].brkStr;
   }
   avgBrkStr /= 10;
   if(lossCnt >= 5 && avgBrkStr < MinBrkStr * 1.5) dynBrkStr = MinBrkStr * 1.5;
   else if(lossCnt <= 2) dynBrkStr = MinBrkStr;
   Print("Breakout strength adjusted: dynBrkStr=", DoubleToString(dynBrkStr, 2));
}

long MagicNumber(ENUM_TIMEFRAMES tf) {
   if(tf == PERIOD_M5) return 1005;
   if(tf == PERIOD_M15) return 1015;
   if(tf == PERIOD_H1) return 1060;
   return 0;
}

int TimeframeIndex(ENUM_TIMEFRAMES tf) {
   for(int i = 0; i < ArraySize(tfs); i++) {
      if(tfs[i].tf == tf) return i;
   }
   return -1;
}

インストールとバックテスト:コンパイルしてチャートに適用します。GOLD M15 (2025)で2%リスクによってバックテストします。 


戦略テスト

この戦略は、比較的速い値動きと高いボラティリティを持つGOLDとの相性が最も良く、これは個人トレーダーの短期取引に有利に働きます。本テストでは、2025年1月1日から2025年7月29日までの期間、GOLDを15分足(M15)で取引して検証をおこないます。以下は、この戦略で選択したパラメータです。 

入力2



ストラテジーテスターの結果

ストラテジーテスターでテストした結果は以下の通りです。

  • 残高/エクイティグラフ

グラフ結果

  • バックテスト結果

テスト結果


まとめ

この記事は、FVGの検出とMSSを組み合わせて、GOLDにおける高確率の取引セットアップを特定するMetaTrader 5のEAについて説明するために書いたものです。FVGは、価格の非効率性やトレンド転換を捉えるために使用される、最も価値が高く標準的なスマートマネーコンセプトの1つです。

GOLDでEAをテストした結果、EAがM15およびH1時間足においてFVGを効率的かつ適切に検出できることが確認できました。しかし、FVGの検出は全体の一部にすぎません。MSSが発生しない場合、FVGが有効であっても取引は実行されるべきではありません。これらのMSSは、ボラティリティの高いセッションで取引の精度と質を高めるための確認要素となります。

この戦略を実装するには、望ましい結果を得るために、EAの入力パラメータを以下のとおり設定します。EAはM15またはH1チャート上のFVGをスキャンし、MSSが上位時間足(H4、D1など)のトレンドと一致していることを確認するよう設計されています。興味のあるユーザーは、GOLDを使ってデモ口座でこのEAをバックテストすることを推奨します。このEAにおける私の主な目的は、1トレードあたり0.5〜2%のリスクやトレーリングストップなどのリスク管理を組み込んだ、高確率セットアップに最適化することでした。

また、パフォーマンスログを定期的に確認し、自身の目標やリスク許容度に応じて設定や入力パラメータを調整することをお勧めします。免責事項:このEAを使用する場合は、実資金を投入する前に、まずデモ口座でテストと取引をおこない、この機関投資家型アプローチを習得して安定した利益を得られるようにしてください。


結論

本記事の主なポイントと強調点は、FVGとは何か、どこで発生するのか、どのように発生するのか、なぜ発生するのか、そして最後に、それらを分析・理解し、さらには実際の取引執行にどのように活用できるのかを明確に説明することにあります。

多くの初心者トレーダー、さらには一部の中級トレーダーでさえ、このFVGという曖昧な領域をどのように扱えばよいのか全く分からず、そこで何が起きているのかを理解できないためにフラストレーションを感じることがあります。価格がどのように繰り返し戻ってくるのか、また、どのようにこれらのギャップを利用して実際に利益を生む典型的な取引やエントリーが構築されるのかを見抜く力がないことが原因です。たとえトレーダー自身がFVGを用いて取引をおこなわない場合でも、自分の取引やセットアップ、さらにはトレンドや方向性を、この記事で共有した内容に照らして検証できるようになり、FVGがどれほど巨大かつ重要な役割を果たしているかを非常に興味深く理解できるはずです。

MQL5で自動化することにより、トレーダーは感情的バイアスを軽減し、FVG+Breaker/MSS戦略を一貫して実行できるようになります。

記事で参照されているすべてのコードは以下に添付されています。次の表では、この記事に付随するすべてのソースコードファイルについて説明します。
ファイル名 説明
Fvg_detector.mq5 FVG検出用コード
fvg-mss.mq5
EA全体、FVG検出、Market Structure Shift 

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

添付されたファイル |
fvg-mss.mq5 (54.64 KB)
Fvg_detector.mq5 (9.26 KB)
最後のコメント | ディスカッションに移動 (2)
Korrect Trades
Korrect Trades | 15 9月 2025 において 12:09
ナイス👍です。
気に入った
Eugene Mmene
Eugene Mmene | 15 9月 2025 において 12:12
Korrect Trades #:
ナイス👍です。
気に入った
ようこそ
プライスアクション分析ツールキットの開発(第40回):Market DNA Passport プライスアクション分析ツールキットの開発(第40回):Market DNA Passport
本記事では、各通貨ペアが持つ固有のアイデンティティを、その過去のプライスアクションという視点から探ります。生物の設計図を記述するDNAの概念に着想を得て、本記事では市場にも同様の枠組みを適用し、プライスアクションを各通貨ペアのDNAとして扱います。ボラティリティ、スイング、リトレースメント、スパイク、セッション特性といった構造的挙動を分解することで、各ペアを他と区別する基礎的なプロファイルが浮かび上がります。このアプローチにより、市場行動に対するより深い洞察が得られ、トレーダーは各銘柄の特性に合った戦略を体系的に組み立てられるようになります。
MQL5における単変量時系列への動的モード分解の適用 MQL5における単変量時系列への動的モード分解の適用
動的モード分解(DMD: Dynamic Mode Decomposition)は、主に高次元データセットに対して用いられる手法です。本稿では、DMDを単変量の時系列に適用し、その特性把握や予測に活用できることを示します。その過程で、MQL5に搭載されているDMDの実装、とりわけ新しい行列メソッドであるDynamicModeDecomposition()について詳しく解説します。
MQL5での取引戦略の自動化(第32回):プライスアクションに基づくファイブドライブハーモニックパターンシステムの作成 MQL5での取引戦略の自動化(第32回):プライスアクションに基づくファイブドライブハーモニックパターンシステムの作成
本記事では、MQL5においてピボットポイントとフィボナッチ比率に基づいて強気、弱気双方のファイブドライブ(5-0)ハーモニックパターンを識別し、ユーザーが選択できるカスタムエントリー、ストップロス、テイクプロフィット設定を用いて取引を実行するファイブドライブパターンシステムを開発します。また、A-B-C-D-E-Fパターン構造やエントリーレベルを表示するために、三角形やトレンドラインなどのチャートオブジェクトを使った視覚的フィードバックでトレーダーの洞察力を高めます。
MQL5での取引戦略の自動化(第31回):プライスアクションに基づくスリードライブハーモニックパターンシステムの作成 MQL5での取引戦略の自動化(第31回):プライスアクションに基づくスリードライブハーモニックパターンシステムの作成
本記事では、MQL5においてピボットポイントとフィボナッチ比率に基づいて強気、弱気双方のスリードライブハーモニックパターンを識別し、ユーザーが選択できるカスタムエントリー、ストップロス、テイクプロフィット設定を用いて取引を実行するスリードライブパターンシステムを開発します。さらに、チャートオブジェクトによる視覚的フィードバックによって、トレーダーの洞察を強化します。