ロールオーバースパイクをフェードアウト:初心者向けの実践的な平均回帰型EA

EA戦略

結論:ロールオーバー直後(ニューヨーク時間17:00=多くのブローカーでサーバー日付が切り替わる瞬間)には、流動性が一時的に低下し、スプレッドが急拡大しやすく、価格が一方向に“スパイク”することがあります。本稿では、この一過性の歪みを統計的に捉え、スプレッドが通常水準に戻った後に“スパイク方向と逆向き”へ小さく取りにいくミーンリバージョン戦略(以下「ロールオーバー・フェード戦略」)を、初心者がそのまま動かせるMQL4の完全EA付きで解説します。

1. 戦略の狙いと全体像

ロールオーバー直後は市場参加者が薄く、クォート提供者のヘッジコスト・在庫調整によってスプレッドが一時的に拡大し、チャート上では“ひげ”の長い足や上下に素早く伸びるバーが発生しやすい時間帯です。ここで本命は値動きそのものではなく「歪み」です。歪み(=瞬間的な価格の行き過ぎ)は、スプレッドが落ち着いた数分後に平常値へ戻る傾向があり、その平均回帰(ミーンリバージョン)を小さく狙います。

本戦略のコアは次の3条件です。

  1. 時間条件:ロールオーバー後の限定ウィンドウ(例:サーバー時間で0:05〜0:20)。
  2. スパイク条件:M1の直近バーの値幅が同期間のATR(例:14)に対して閾値以上(例:1.6倍以上)。または直近30分レンジの外側へ一方向に拡張。
  3. スプレッド条件:スプレッドが通常水準に復帰(例:2.0pips以下など、銘柄・ブローカー依存)してからエントリー。

上記を満たしたら、スパイク方向の逆向きに小さく入り、利確はスパイク幅の一部(例:0.4倍)損切りはスパイク極値の外側へ小さめ(例:0.6倍 + バッファ)という設計にします。持ち越しは避け、時間切れ(例:20分)でクローズします。

2. 対象銘柄と時間の考え方

対象は流動性が高く、かつロールオーバーの歪みが観測しやすいペアが適します。代表例はUSDJPY、EURUSD、GBPUSDです。金(XAUUSD)はスプレッド拡大が大きくなりがちで難易度が上がるため、初心者は主要通貨から始めるのが無難です。

ロールオーバーのサーバー時刻はブローカーによって異なります。EAでは「RolloverHour」を外部パラメータ化し、ブローカーのサーバー時間に合わせて設定できるようにします(多くは0時開始)。

3. 具体的な売買ルール

3.1 エントリー判定

  • 時間窓:RolloverHour + 5分 〜 +20分(例:00:05〜00:20)。
  • スパイク検出:M1の直近バーの高安幅 > ATR(14, M1) × SpikeFactor(例:1.6)。
  • 方向判定:直近バーが陽線で終値が直近30分高値を更新 → 逆向きの売り。陰線で直近30分安値を更新 → 逆向きの買い
  • スプレッド復帰:現在スプレッド ≤ MaxSpread(例:2.0pips)。
  • 1日1トレード制限:過剰エントリー回避のため、その日の初回のみ。

3.2 エグジット設計

  • 利確(TP):検出したスパイク幅 × TPFactor(例:0.4)。
  • 損切り(SL):スパイク極値の外側に SpikeWidth × SLFactor + Buffer(例:0.6 × 幅 + 2.0pips)。
  • 時間切れクローズ:最大保有時間(例:20分)に到達したら成行クローズ。

4. リスク管理とポジションサイズ

本戦略は“小さく取ってすぐ離す”思想です。典型値として1トレードの想定損失を口座残高の0.3〜0.7%に制限し、固定比率法かATR連動のロット調整を行います。ボラが高い日(祝日前後やビッグイベントの翌日など)はトレード回避のルールも有効です。

5. バックテストの進め方

  1. 期間分割:学習期間(例:2018/01〜2022/12)と検証期間(例:2023/01〜2025/06)を明確に分離します。
  2. 先にロジック固定:パラメータ最適化の前に、時間窓・1日1回制限・スプレッド復帰条件などの非価格条件を固定します。
  3. 少数パラメータ最適化:SpikeFactor, TP/SL比, MaxSpreadのみ軽く調整し、汎化性能(PF、期待値/MAE、ドローダウン/平均損など)を重視します。
  4. 曜日別・銘柄別の頑健性:曜日ごと・銘柄ごとに結果が崩れていないかを確認します。

6. よくある失敗と回避策

  • ロールオーバー直後のスプレッドが十分に戻る前に成行で入ってしまい、約定コストで優位性が消える。
  • 複数回エントリーで負けを取り戻そうとし、ロールオーバーの例外的なトレンド日に捕まる。
  • イベント翌日など、ボラティリティが異常に高い日にいつも通りのサイズで入ってしまう。

7. 実装:初心者向けMQL4 EA(完全版)

以下のEAは「RolloverHour」を外部入力に持ち、サーバー時間の0時を基準として5〜20分の時間帯にのみ、スプレッドが閾値以下で、かつM1スパイク検出条件を満たした場合にその逆方向へ1日1回だけエントリーします。TP/SLはスパイク幅に連動し、最大保有時間に達したら決済します。


//+------------------------------------------------------------------+
//|  FadeRolloverSpikeEA.mq4                                         |
//|  Simple mean-reversion around rollover for beginners             |
//+------------------------------------------------------------------+
#property strict

extern int    RolloverHour      = 0;     // Broker server hour of rollover
extern int    StartMinuteOffset = 5;     // Start after rollover (minutes)
extern int    EndMinuteOffset   = 20;    // End after rollover (minutes)
extern double MaxSpreadPips     = 2.0;   // Entry only if spread <= this
extern double SpikeATRMult      = 1.6;   // Spike threshold vs ATR(14, M1)
extern double TPFactor          = 0.4;   // Take profit as fraction of spike
extern double SLFactor          = 0.6;   // Stop loss as fraction of spike
extern double SLBufferPips      = 2.0;   // Extra buffer outside spike
extern double RiskPerTradePct   = 0.5;   // % of balance risked per trade
extern int    MaxHoldMinutes    = 20;    // Max holding time
extern int    Magic             = 905015; // Magic number

datetime lastTradeDay = 0;
double   entryPrice   = 0;
datetime entryTime    = 0;

// Helper: get current spread in pips
double GetSpreadPips() {
   double pt = MarketInfo(Symbol(), MODE_POINT);
   int    dg = (int)MarketInfo(Symbol(), MODE_DIGITS);
   double spr_points = MarketInfo(Symbol(), MODE_SPREAD);
   double pip = (dg==3 || dg==5) ? 10*pt : pt;
   return spr_points * pt / pip;
}

// Helper: ATR on M1
double ATR_M1(int period=14){
   double atr[];
   if(iATR(Symbol(), PERIOD_M1, period, 0) == 0) return 0;
   ArraySetAsSeries(atr, true);
   int copied = CopyBuffer(iATR(Symbol(), PERIOD_M1, period), 0, 0, 2, atr);
   if(copied <= 0) return 0;
   return iATR(Symbol(), PERIOD_M1, period, 0);
}

// Get last completed M1 bar info
bool GetLastM1(double &open, double &high, double &low, double &close){
   int shift = 1; // last completed bar
   open  = iOpen(Symbol(), PERIOD_M1, shift);
   high  = iHigh(Symbol(), PERIOD_M1, shift);
   low   = iLow(Symbol(), PERIOD_M1, shift);
   close = iClose(Symbol(), PERIOD_M1, shift);
   return (high > 0 && low > 0);
}

// Highest high / lowest low over recent minutes (e.g. 30)
double HighestHighM1(int bars=30){
   double hh= -1e10;
   for(int i=1; i<=bars; i++){
      double h = iHigh(Symbol(), PERIOD_M1, i);
      if(h > hh) hh = h;
   }
   return hh;
}
double LowestLowM1(int bars=30){
   double ll= 1e10;
   for(int i=1; i<=bars; i++){
      double l = iLow(Symbol(), PERIOD_M1, i);
      if(l < ll) ll = l;
   }
   return ll;
}

// Calculate lot size based on SL distance and risk%
double CalcLotsByRisk(double stopPips){
   if(stopPips <= 0) return 0.0;
   double balance = AccountBalance();
   double riskAmt = balance * (RiskPerTradePct/100.0);
   double pt = MarketInfo(Symbol(), MODE_POINT);
   int    dg = (int)MarketInfo(Symbol(), MODE_DIGITS);
   double pip = (dg==3 || dg==5) ? 10*pt : pt;
   double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
   // Approx: value per pip for 1 lot
   double pipValuePerLot = (tickValue/MarketInfo(Symbol(), MODE_TICKSIZE)) * pip;
   double lots = riskAmt / (stopPips * pipValuePerLot);
   double minLot = MarketInfo(Symbol(), MODE_MINLOT);
   double lotStep= MarketInfo(Symbol(), MODE_LOTSTEP);
   // normalize
   double steps = MathFloor(lots/lotStep);
   double adj   = steps * lotStep;
   if(adj < minLot) adj = minLot;
   return NormalizeDouble(adj, 2);
}

// Check time window (server time)
bool InWindow(){
   datetime now = TimeCurrent();
   int h = TimeHour(now);
   int m = TimeMinute(now);
   if(h != RolloverHour) return false;
   if(m >= StartMinuteOffset && m <= EndMinuteOffset) return true;
   return false;
}

// One trade per day control
bool TradedToday(){
   datetime today = DateOfDay(TimeCurrent());
   return (lastTradeDay == today);
}
void MarkTraded(){
   lastTradeDay = DateOfDay(TimeCurrent());
}

datetime DateOfDay(datetime t){
   int y = TimeYear(t), mo = TimeMonth(t), d = TimeDay(t);
   return StrToTime(StringFormat("%04d.%02d.%02d 00:00", y, mo, d));
}

// Close by time
void CheckTimeExit(){
   if(OrdersTotal() == 0) return;
   for(int i=OrdersTotal()-1; i>=0; i--){
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderMagicNumber() == Magic && OrderSymbol() == Symbol()){
         if((TimeCurrent() - entryTime) >= MaxHoldMinutes*60){
            OrderClose(OrderTicket(), OrderLots(), Bid, 5);
         }
      }
   }
}

int start(){
   // Ensure only 1 open position per symbol/magic
   for(int i=0; i<OrdersTotal(); i++){
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && OrderMagicNumber() == Magic && OrderSymbol() == Symbol()){
         CheckTimeExit();
         return(0);
      }
   }

   // Time-window and one-trade-per-day
   if(!InWindow() || TradedToday()) return(0);

   // Spread check
   double spr = GetSpreadPips();
   if(spr > MaxSpreadPips) return(0);

   // Spike detection
   double o,h,l,c;
   if(!GetLastM1(o,h,l,c)) return(0);
   double atr = iATR(Symbol(), PERIOD_M1, 14, 1);
   if(atr <= 0) return(0);
   double width = h - l;
   if(width < SpikeATRMult * atr) return(0);

   // Breakout vs last 30min
   double hh = HighestHighM1(30);
   double ll = LowestLowM1(30);

   int    dir = 0; // +1 = buy, -1 = sell
   if(c >= hh && c > o){ dir = -1; }      // Up spike -> fade with sell
   else if(c <= ll && c < o){ dir = +1; }   // Down spike -> fade with buy
   else return(0);

   // Compute SL/TP distances (pips)
   double pt = MarketInfo(Symbol(), MODE_POINT);
   int    dg = (int)MarketInfo(Symbol(), MODE_DIGITS);
   double pip = (dg==3 || dg==5) ? 10*pt : pt;

   double tpPips = (width * TPFactor) / pip;
   double slPips = (width * SLFactor) / pip + SLBufferPips;
   if(tpPips <= 0 || slPips <= 0) return(0);

   double lots = CalcLotsByRisk(slPips);
   if(lots <= 0) return(0);

   int    slip = 5;
   double price, sl, tp;

   if(dir == -1){ // sell
      price = Bid;
      sl = price + slPips * pip;
      tp = price - tpPips * pip;
      if(OrderSend(Symbol(), OP_SELL, lots, price, slip, sl, tp, "FadeRolloverSpike", Magic, 0, clrRed) > 0){
         entryPrice = price;
         entryTime  = TimeCurrent();
         MarkTraded();
      }
   }else if(dir == +1){ // buy
      price = Ask;
      sl = price - slPips * pip;
      tp = price + tpPips * pip;
      if(OrderSend(Symbol(), OP_BUY, lots, price, slip, sl, tp, "FadeRolloverSpike", Magic, 0, clrBlue) > 0){
         entryPrice = price;
         entryTime  = TimeCurrent();
         MarkTraded();
      }
   }
   return(0);
}
//+------------------------------------------------------------------+
  

使い方は簡単です。MetaTrader4のナビゲーターから「エキスパートアドバイザ」を右クリック → 「新規作成」で本コードを貼り付け、コンパイルします。EAを対象通貨のM1またはM5チャートに適用し、RolloverHourをブローカーのサーバー時間に合わせて設定してください。最初はデモ口座で挙動と約定品質を確認し、ブローカーのロールオーバー特性に合わせてMaxSpreadPipsSpikeATRMultを調整すると安定します。

8. バリエーションと拡張

  • レンジ回帰型TP:直近N分のミドル((HH+LL)/2)やボリンジャーバンドの中心線にTPを置く。
  • 曜日フィルター:曜日ごとの勝率差が顕著なら、勝ちやすい曜日のみ許可する。
  • ニュース回避:ロールオーバー直後に主要指標や要人発言が重なる日はスキップ。

9. 運用手順(チェックリスト)

  1. 対象ブローカーのロールオーバー時刻を確認し、EAのRolloverHourを合わせる。
  2. デモ口座で1〜2週間、スプレッド復帰の速度約定滑りを観察する。
  3. MaxSpreadPips・SpikeATRMult・TP/SL比を軽く最適化(やりすぎ厳禁)。
  4. 1回あたりのリスクを残高の0.3〜0.7%に固定。
  5. 実弾移行後も損益・滑り・勝率が崩れていないかを週次でレビュー。

10. まとめ

ロールオーバーは“理由のある歪み”が発生しやすい時間帯です。本戦略はその歪みの平均回帰だけを狙う、設計のシンプルさが強みです。時間窓・スプレッド・スパイクの3条件と、1日1回の自制ルールを堅持すれば、初心者でも過剰リスクを避けつつ、統計的優位性に基づいた運用がしやすくなります。

コメント

タイトルとURLをコピーしました