
逆フェアバリューギャップ取引戦略
はじめに
逆フェアバリューギャップ(IFVG)とは、価格が過去に特定されたフェアバリューギャップ(FVG)へ回帰した際に、通常想定されるサポートまたはレジスタンスとしての反応を示さず、その水準を無視して通過してしまう現象を指します。このような失敗は、市場の方向性の変調を示すサインである可能性があり、逆張り志向の取引アプローチにおいて優位性をもたらすシグナルとなることがあります。本記事では、MetaTrader 5エキスパートアドバイザー(EA)の戦略として、この逆フェアバリューギャップを定量的に捉え、取引ロジックに組み込むために私が独自に開発したアプローチを紹介します。
戦略の動機
まずはフェアバリューギャップ(FVG)の理解から
逆フェアバリューギャップという概念を深く理解するためには、まず通常のフェアバリューギャップ(FVG)が何を意味するのかを正確に把握する必要があります。フェアバリューギャップとは、一般に3本のローソク足によって形成される価格パターンです。
FVGは、ローソク足Bの実体(および多くの場合髭も含めて)が、価格を急激に上または下に押し上げる/押し下げることで、チャート上に「ギャップ」が生じたときに発生します。より具体的には、強い上昇局面において、ローソク足Cの安値がローソク足Aの高値よりも高い場合、この2点間に生まれる価格帯がフェアバリューギャップと見なされます。このギャップは、市場における「非効率」または「不均衡」な領域を表しており、価格が一方向に急速に動いたために、売買のバランスが取れた取引がおこなわれなかったゾーンであると解釈されます。トレーダーの間では、このような価格の飛躍は、機関投資家の大口注文が背景にあるとされ、いわば「大口資金の足跡」が残されたエリアとして認識されています。
一般的なロジックでは、価格はある時点でこうしたギャップに戻ってきて「埋める」傾向があるとされます。このギャップの埋め戻しは、先に片側に偏っていたオーダーフローのバランスを取るための市場の動きであると解釈されます。この考え方に従うトレーダーは、価格がギャップを再訪するのを待ち、その際の反応によって、元のトレンド方向への継続を確認するか、あるいは反転の兆しとして判断します。
逆フェアバリューギャップとは何か?
「逆フェアバリューギャップ(IFVG)」という概念は、フェアバリューギャップ(FVG)の基本的な考え方を基盤にしつつ、それを逆張り的、あるいはリバースエンジニアリング的な視点から捉え直したものです。通常、FVGは市場が一方向に動く際の不均衡を表し、価格が再びそのゾーンに戻ったときに反発や反転の根拠として利用されることが多いですが、逆FVG戦略では、むしろそのギャップが機能しないことを手がかりに、市場がその方向に「追随できない」ポイントを特定し、反転を予測します。
たとえば、弱気の逆FVGを見つけるには、以下の手順を踏みます。
- 強気のFVGを識別する。
- 価格がそのFVGゾーンに戻ってくるのを確認する。
- 価格がそのゾーンをサポートとして機能させるかどうかを観察する。価格が上方向への反発に失敗し、FVGの中をそのまま下抜けていくような動きを見せた場合、この失敗は、市場の勢いが反転する兆候であると捉えることができます。
- 売りエントリーを検討する。FVGを上昇の足掛かりとして利用できないという事実は、市場が下落に転じる可能性を示唆していると判断します。
逆フェアバリューギャップの直感的理解
- 大口資金の足跡と失敗ポイント :フェアバリューギャップの基本的な前提は、大口で高度なプレイヤーが最初の不均衡を作り出したということです。価格がこれらのゾーンに戻ると、それは多くの場合テストの場となります。大口プレイヤーが依然としてその価格に価値を見出している場合、彼らの残存注文が価格を支持または抵抗し、反応を引き起こすことがあります。一方で、価格が強い反発や継続を伴わずにFVGを突破してしまう場合、その大口注文は既に約定・キャンセルされているか、もはやそのゾーンを守っていないことを示唆しています。これは市場参加者の意図が変化したことを示唆する可能性があります。
- 弱さや強さを早期に察知する :価格がギャップに戻った際に「起こらなかったこと」に注目することで、トレーダーは根底にある強弱の微妙な手がかりを掴むことができます。強気相場が既知の非効率ゾーン(強気のFVG)から押し上げを得られなければ、それは強気の勢いが失われつつある早期警告となるかもしれません。
- 従来のFVG戦略を補完する :従来のFVG戦略は、リバランス後に元の方向への継続があると仮定しています。しかし市場は動的であり、すべてのギャップ埋めがトレンド再開につながるわけではありません。逆FVGアプローチは、通常のパターンが失敗する局面を見つけることでトレーダーに新たな優位性を提供し、逆張りの動きがより高確率で、リスクリワードも良好となる可能性があります。
逆フェアバリューギャップの考え方は、市場が過去の不均衡領域を絶えず試し、再評価しているという認識に基づいています。従来のFVG取引が成功したリバランスとトレンド継続に注目するのに対し、逆FVGはこのリバランスが期待通りに進まなかった局面を捉えて優位を獲得します。この視点の転換によって、見逃していたチャンスや損失の可能性があった場面が、逆張りの高い優位性を持つセットアップに変わります。予想外の動きを予測することが報われる市場環境において、逆FVGのコンセプトはトレーダーのテクニカル分析ツールの重要な一つとして機能します。
戦略開発
裁量トレーダーがフェアバリューギャップ(FVG)を取引判断に活用するのと同様に、逆フェアバリューギャップ(逆FVG)も、厳密な条件を満たした場合には信頼性の高いシグナルとなるため、一部のトレーダーによって積極的に活用されています。すべての逆フェアバリューギャップを区別せずに取引すると、以前に述べた戦略的直感と一致しないギャップがほとんどであるため、ランダムウォークのようなパフォーマンスに陥る可能性が高くなります。裁量トレーダーが考慮する機能設定を定量化するために、私は広範な機能テストを実施し、次のルールを確立しました。
-
マクロトレンドとの整合性 :価格は、400期間の移動平均線に対する位置関係によって決まる全体的なマクロトレンドに一致している必要があります。
-
適切な時間枠の選択 :「注文の履行」という概念は短時間で発生するため、1分から5分のような低い時間足を使用する必要があります。本記事では、3分足を使用します。
-
最新のフェアバリューギャップに注目 :現在の市場状況を最も正確に反映しているため、直近のフェアバリューギャップ(FVG)のみを対象とします。
-
フェアバリューギャップサイズの検証 :FVGは、周囲のローソク足と比べて大きすぎても小さすぎてもいけません。小さすぎるギャップは、信頼できるサポートまたはレジスタンスとしての有効性に欠け、大きすぎるギャップはニュースイベントによって発生した可能性が高く、反転シグナルの遅れにつながります。FVGが有効であることを確認するために、各ギャップには特定の閾値を設定しています。
-
ブレイクアウトキャンドルのサイズ管理 :同様に、エントリーはローソク足の終値に基づいておこなわれるため、ブレイクアウトキャンドルが極端に大きくなってはいけません。ローソク足が大きすぎると、シグナルが遅れてしまい、この戦略の核心である「タイムリーなエントリー」が機能しなくなります。
-
タイムリーな価格反転とブレイクアウト :FVGの形成後、所定の時間内に価格がギャップに戻り、反対側の端からローソク足の終値でブレイクアウトする必要があります。これは、短期間の遡及範囲内で直近のFVGのみを検出することで実現します。
-
ブレイクアウト強度の確認 :FVGは過去のリジェクションレベルと一致している必要があります。これにより、FVGのブレイクアウトがその方向への勢いの強まりを示していることが確認されます。
それでは、コードを見ていきましょう。
まず、必要なグローバル変数を宣言します。これらのグローバル変数には、フェアバリューギャップ(FVG)の追跡、現在の保有ポジション、そしてシステムの状態を管理するための重要なデータが格納されます。たとえば、previousGapHigh、previousGapLow、lastGapIndexなどの変数は、直近に識別されたFVGの位置を記録するために使用されます。handleMaには移動平均のハンドルが格納されます。buyposとsellposは保有中の取引チケットを追跡し、currentFVGstatusとnewFVGformedは最後に検出されたFVGの状態を管理します。
string previousGapObjName = ""; double previousGapHigh = 0.0; double previousGapLow = 0.0; int LastGapIndex = 0; double gapHigh = 0.0; double gapLow = 0.0; double gap = 0.0; double lott= 0.1; ulong buypos = 0, sellpos = 0; double anyGap = 0.0; double anyGapHigh = 0.0; double anyGapLow = 0.0; int barsTotal = 0; int newFVGformed = 0; int currentFVGstatus = 0; int handleMa; #include <Trade/Trade.mqh> CTrade trade;
次に、テイクプロフィットとストップロスを設定してエントリーを実行し、各取引の注文チケットを管理するための以下の関数を宣言します。
//+------------------------------------------------------------------+ //| Store order ticket number into buypos/sellpos variables | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } //+------------------------------------------------------------------+ //| Execute sell trade function | //+------------------------------------------------------------------+ void executeSell() { if (IsWithinTradingHours()){ double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double tp = bid - tpp * _Point; tp = NormalizeDouble(tp, _Digits); double sl = bid + slp * _Point; sl = NormalizeDouble(sl, _Digits); trade.Sell(lott,_Symbol,bid,sl,tp); sellpos = trade.ResultOrder(); } } //+------------------------------------------------------------------+ //| Execute buy trade function | //+------------------------------------------------------------------+ void executeBuy() { if (IsWithinTradingHours()){ double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double tp = ask + tpp * _Point; tp = NormalizeDouble(tp, _Digits); double sl = ask - slp * _Point; sl = NormalizeDouble(sl, _Digits); trade.Buy(lott,_Symbol,ask,sl,tp); buypos= trade.ResultOrder(); } } //+------------------------------------------------------------------+ //| Check if is trading hours | //+------------------------------------------------------------------+ bool IsWithinTradingHours() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; if (currentHour >= startHour && currentHour < endHour) return true; else return false; }
次に、これら2つの関数を使用してフェアバリューギャップを検証します。IsReacted()は、遡及期間内に現在のFVGの価格レンジ内に少なくとも2本のローソク足の髭が存在するかを確認し、これをFVGの過去のリジェクションのサインと解釈します。続いてIsGapValid()は、ギャップのサイズが所定の範囲内にあるかを検証し、真または偽を返します。
//+------------------------------------------------------------------+ //| Function to validate the FVG gap | //+------------------------------------------------------------------+ bool IsGapValid(){ if (anyGap<=gapMaxPoint*_Point && anyGap>=gapMinPoint*_Point&&IsReacted()) return true; else return false; } //+------------------------------------------------------------------+ //| Check for gap reaction to validate its strength | //+------------------------------------------------------------------+ bool IsReacted(){ int count1 = 0; int count2 = 0; for (int i = 4; i < lookBack; i++){ double aLow = iLow(_Symbol,PERIOD_CURRENT,i); double aHigh = iHigh(_Symbol,PERIOD_CURRENT,i); if (aHigh<anyGapHigh&&aHigh>anyGapLow&&aLow<anyGapLow){ count1++; } else if (aLow<anyGapHigh&&aLow>anyGapLow&&aHigh>anyGapHigh){ count2++; } } if (count1>=2||count2>=2) return true; else return false; }
その後、これらの関数を使用して、最後のFVGに現在ブレイクアウトがあるかどうかを確認します。
//+------------------------------------------------------------------+ //| Check if price broke out to the upside of the gap | //+------------------------------------------------------------------+ bool IsBrokenUp(){ int lastClosedIndex = 1; double lastOpen = iOpen(_Symbol, PERIOD_CURRENT, lastClosedIndex); double lastClose = iClose(_Symbol, PERIOD_CURRENT, lastClosedIndex); if (lastOpen < gapHigh && lastClose > gapHigh&&(lastClose-gapHigh)<maxBreakoutPoints*_Point) { if(currentFVGstatus==-1){ return true;} } return false; } //+------------------------------------------------------------------+ //| Check if price broke out to the downside of the gap | //+------------------------------------------------------------------+ bool IsBrokenLow(){ int lastClosedIndex = 1; double lastOpen = iOpen(_Symbol, PERIOD_CURRENT, lastClosedIndex); double lastClose = iClose(_Symbol, PERIOD_CURRENT, lastClosedIndex); if (lastOpen > gapLow && lastClose < gapLow&&(gapLow -lastClose)<maxBreakoutPoints*_Point) { if(currentFVGstatus==1){ return true;} } return false; }
最後に、これら2つの関数を使ってIsGapValid()でギャップの有効性を確認します。有効であれば、グローバル変数を更新し、そのFVGを新しいものとしてマークし、チャートに描画します。getFVG()関数は戦略全体のコーディングに不可欠なもので、新しいバーが立つたびに呼び出して、有効なFVGが存在するかをチェックします。FVGが有効な場合は、前回保存したものと異なるかどうかを確認し、異なればグローバル変数に保存して状態を更新します。
//+------------------------------------------------------------------+ //| To get the most recent Fair Value Gap (FVG) | //+------------------------------------------------------------------+ void getFVG() { // Loop through the bars to find the most recent FVG for (int i = 1; i < 3; i++) { datetime currentTime = iTime(_Symbol,PERIOD_CURRENT, i); datetime previousTime = iTime(_Symbol,PERIOD_CURRENT, i + 2); // Get the high and low of the current and previous bars double currentLow = iLow(_Symbol,PERIOD_CURRENT, i); double previousHigh = iHigh(_Symbol,PERIOD_CURRENT, i+2); double currentHigh = iHigh(_Symbol,PERIOD_CURRENT, i); double previousLow = iLow(_Symbol,PERIOD_CURRENT, i+2); anyGap = MathAbs(previousLow - currentHigh); // Check for an upward gap if (currentLow > previousHigh) { anyGapHigh = currentLow; anyGapLow = previousHigh; //Check for singular if (LastGapIndex != i){ if (IsGapValid()){ gapHigh = currentLow; gapLow = previousHigh; gap = anyGap; currentFVGstatus = 1;//bullish FVG DrawGap(previousTime,currentTime,gapHigh,gapLow); LastGapIndex = i; newFVGformed =1; return; } } } // Check for a downward gap else if (currentHigh < previousLow) { anyGapHigh = previousLow; anyGapLow = currentHigh; if (LastGapIndex != i){ if(IsGapValid()){ gapHigh = previousLow; gapLow = currentHigh; gap = anyGap; currentFVGstatus = -1; DrawGap(previousTime,currentTime,gapHigh,gapLow); LastGapIndex = i; newFVGformed =1; return; } } } } } //+------------------------------------------------------------------+ //| Function to draw the FVG gap on the chart | //+------------------------------------------------------------------+ void DrawGap(datetime timeStart, datetime timeEnd, double gaphigh, double gaplow) { // Delete the previous gap object if it exists if (previousGapObjName != "") { ObjectDelete(0, previousGapObjName); } // Generate a new name for the gap object previousGapObjName = "FVG_" + IntegerToString(TimeCurrent()); // Create a rectangle object to highlight the gap ObjectCreate(0, previousGapObjName, OBJ_RECTANGLE, 0, timeStart, gaphigh, timeEnd, gaplow); // Set the properties of the rectangle ObjectSetInteger(0, previousGapObjName, OBJPROP_COLOR, clrRed); ObjectSetInteger(0, previousGapObjName, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, previousGapObjName, OBJPROP_WIDTH, 2); ObjectSetInteger(0, previousGapObjName, OBJPROP_RAY, false); // Update the previous gap information previousGapHigh = gaphigh; previousGapLow = gaplow; }
そして、このようにすべての戦略ルールをOnTick関数に統合すれば完了です。
//+------------------------------------------------------------------+ //| OnTick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; double ma[]; double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); CopyBuffer(handleMa,BASE_LINE,1,1,ma); if (IsBrokenLow()&&sellpos == buypos&&newFVGformed ==1&&bid<ma[0]){ executeSell(); newFVGformed =0; } else if (IsBrokenUp()&&sellpos == buypos&&newFVGformed ==1&&ask>ma[0]){ executeBuy(); newFVGformed =0; } getFVG(); if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } }
補足ですが、これはOnTick関数の冒頭で呼び出しており、新しいバーが形成された後にのみ残りの処理を実行するようにしています。この方法により、計算リソースの節約が可能です。
int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars;
また、同時に1つのエントリーに限定したいため、このロジックではbuyposとsellposの両方が0のときにのみエントリーを許可しています。これは、この特定のEAによる保有ポジションが存在しないことを確認するためです。
if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; }
簡単な要約
- グローバル宣言と入力:環境、変数、およびユーザーが設定可能なパラメーターを準備します。
- 初期化(OnInit):移動平均フィルターの準備とマジックナンバーの設定をおこないます。
- OnTickロジック:メインの処理フローで、新しいバーの確認、FVGの検出、ブレイクアウトの判定、条件を満たした場合のエントリーをおこないます。
- FVG検出(getFVG、IsGapValid、IsReacted):フェアバリューギャップの特定と有効性、および市場の反応を検証します。
- ブレイクアウトチェック(IsBrokenUp、IsBrokenLow):取引エントリーのためのブレイクアウト方向を確認します。
- 取引管理(OnTradeTransaction、executeBuy、executeSell):注文チケットの管理と取引の正確な実行を担います。
- チャート作成(DrawGap):特定したFVGを可視化します。
- 時間フィルタリング(IsWithinTradingHours):取引を特定の時間帯に制限します。
戦略テスト
この戦略は、比較的スプレッドが狭くボラティリティが高い株価指数において最も効果的に機能します。これらの特徴は個人のデイトレードに有利です。今回は2020年1月1日から2024年12月1日までの期間で、3分足(M3)のナスダック100指数を対象にこの戦略をテストします。以下は、この戦略で選択したパラメータです。
以下は戦略のパラメータ設定に関するいくつかの推奨事項です。
- 市場のボラティリティが高い時間帯、つまり通常は株式市場が開いている時間に取引時間を設定してください。この時間帯は利用しているブローカーのサーバー時間に依存します。たとえば、私のサーバー時間(GMT+0)では、株式市場はおおよそ14:00から19:00まで開いています。
- マクロトレンドに乗るため、1を超えた報酬対リスク比を推奨します。また、利益確定および損切り(TPSL)の設定は、高すぎたり低すぎたりしないようにしてください。TPSLが大きすぎると短期パターンのシグナルを十分に捉えられず、逆に小さすぎるとスプレッドの影響で取引に悪影響を及ぼします。
- ギャップの閾値やブレイクアウトキャンドルの閾値、遡及期間の値は過度にチューニングしないでください。エントリー対象の価格レンジに対して妥当な範囲内に収めることで過剰な最適化を避けられます。
バックテストの結果は次のとおりです。
過去5年間にわたり、この戦略は非常に安定したパフォーマンスを示しており、収益性の可能性があることがわかります。
ストラテジーテスターのビジュアル部分での典型的な取引は、以下のようになります。
読者の皆さんには、この戦略のフレームワークを基に創意工夫を加え、より良いものに発展させていただきたいと思います。以下は私からのいくつかの提案です。
- IFVGの強さは、FVG周辺のリジェクションキャンドルの数によって判断できます。これらの数の差を評価基準のひとつとして活用してみてください。
- 本記事では最大のブレイクアウトポイントに注目しましたが、時にはブレイクアウトキャンドルが小さすぎてブレイクアウトの強さが弱いこともあり、トレンドの継続に影響を与える場合があります。最低限のブレイクアウトポイントに対する閾値を設定することも検討してみてください。
- エグジットルールは利益確定と損切りで定めていますが、代わりに遡及期間内の重要なレベルを基にエグジットポイントを決めたり、固定のエグジット時間を設定する方法もあります。
結論
この記事では、MetaTrader 5エキスパートアドバイザーの戦略として逆フェアバリューギャップを定量化し活用するための私自身が開発したアプローチを紹介し、戦略の動機、開発、テストについて解説しました。この戦略は過去5年間で400回以上の取引を通じて安定したパフォーマンスを示しており、高い収益性が期待できます。また、この戦略はさまざまな銘柄や時間枠に対応できるよう、さらに改良することも可能です。完全なコードは以下に添付しておりますので、皆さんの取引開発にぜひお役立てください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16659





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索