クォーター・ポイント逆張りEA:25pips刻みの流動性掃除を狙う実践的FX戦略(完全コード付き)

FX

本稿では、為替相場が25pips刻みの水準(0.00/0.25/0.50/0.75)に集まりやすいという市場参加者の行動パターンに着目し、
その水準を一時的に突破(ストップラン)した直後の反発(ミーンリバージョン)を狙う、実運用可能な短期戦略を提示します。
ルールはシンプルですが、流動性の偏り・ストップ注文の偏在・アルゴの定型的な約定動作というミクロ構造の歪みに根拠があり、
しかも自律実行(EA)しやすいのが利点です。

戦略のコンセプト

多くのトレーダーは、指値・逆指値をキリの良い価格や四半端(0.25/0.50/0.75)に集中させます。
こうしたクォーター・ポイント周辺は、流動性の“ポケット”が生まれやすく、
オーダーブックが薄くなると、短時間で水準を“掃除”するような急伸・急落が発生します。
掃除の直後は、利食い・逆張り・アルゴの在庫調整により、確率的に元のレンジへ回帰しやすくなります。

本戦略は、この掃除→回帰の非対称性を短時間で取りに行く逆張りデイトレです。

対象通貨・時間軸

  • 対象通貨:USDJPY / EURUSD / GBPUSD(スプレッドが安定かつ流動性の高い通貨)
  • 時間軸:M1~M15(筆者推奨はM5またはM15)
  • 取引時間:ロンドン~NY時間を中心に(東京早朝の板薄時間は回避設定可)

エッジの根拠(ミクロ構造)

  1. 注文の集積:四半端の手前後ろに逆指値(ブレイクアウト狙い)とストップが重なりやすい。
  2. 流動性掃除:ブローカー間のスプレッド拡大や約定優先で水準を“抜かせに行く”動きが発生。
  3. 在庫調整:掃除直後はディーラーやショートタームアルゴの在庫調整が優先し、価格は直近の公正価値へ回帰しやすい。

売買ルール(要点)

価格が直近のクォーター・ポイント(以下QP)を一度明確に突破し、短時間でQP内側に戻る動きを検知したら、
その戻り方向へ逆張りエントリを行います。

  • QP定義:価格の小数第2位までを用い、25pips刻みのレベルを自動算出(例:USDJPY 144.00/144.25/144.50/144.75)。
  • 突破判定:直近N本の高値または安値がQPをEntryPad(例:+2pips)以上抜ける。
  • 回帰判定:突破後、一定の時間窓内(例:≤ 10分)に価格が再びQPの内側へ戻る。
  • エントリ:回帰が確認できたバーの終値で逆張り方向に成行。
  • 損切り:ATRベース(例:0.8×ATR)または反対側の最新スイング+α。
  • 利確:RR 1.2~1.5、もしくはQPの中点(12.5pips)で部分利確+トレーリング。
  • フィルタ:最小ATR・最大ATR、最大スプレッド、重要指標前後の取引回避。

ポジションサイジング

リスクは1トレードあたり口座残高の0.5~1.0%を上限にします。
損切り幅(pips)からロット数を逆算し、ブローカーの最小ロット制約に合わせます。

バックテスト設計(MT4)

  1. ヒストリカル:最低3~5年、ティック精度は可能な限り向上(可ならばDukascopy等の細かいティック)。
  2. 取引コスト:スプレッド・コミッション・スリッページを現実的に上乗せ。
  3. ロジック固定:チューニングは歩留まりを見ながら最小限、過剰最適化を避ける。
  4. 評価指標:PF、勝率、期待値、平均保持時間、最大DD、連敗数、年次の安定性。

MQL4 EA(完全コード・初心者向け)

以下は本戦略をそのまま自動化するための参考EAです。必要に応じてブローカー仕様に合わせて調整してください。


//+------------------------------------------------------------------+
//|  QuarterPoint Reversal EA - Simple Edition                       |
//|  Logic: Stop-run past 0/25/50/75 levels then mean-revert entry   |
//|  Timeframe: M5/M15                                                |
//+------------------------------------------------------------------+
#property strict

input string   Symbols            = "USDJPY,EURUSD,GBPUSD"; // comma separated
input int      QuarterStepPips    = 25;
input double   EntryPadPips       = 2.0;     // how far beyond QP counts as "break"
input int      LookbackBars       = 6;       // bars to detect the break
input int      RevertWindowBars   = 6;       // bars to detect revert back inside
input int      ATR_Period         = 14;
input ENUM_TIMEFRAMES ATR_TF      = PERIOD_M15;
input double   ATR_MinPips        = 3.0;
input double   ATR_MaxPips        = 25.0;
input double   RiskPerTradePct    = 0.7;
input double   StopATR            = 0.8;
input double   TP_ATR             = 1.2;
input double   MaxSpreadPips      = 2.0;
input bool     UseTrailing        = true;
input double   TrailStartRR       = 1.0;
input double   TrailStepPips      = 2.0;
input int      NewsBlockMinutes   = 15;      // manual buffer pre/post news
input int      StartHour          = 7;       // trading window (broker time)
input int      EndHour            = 23;

double Pip(string sym){ return (MarketInfo(sym, MODE_DIGITS)==3 || MarketInfo(sym, MODE_DIGITS)==5) ? (MarketInfo(sym, MODE_POINT)*10) : MarketInfo(sym, MODE_POINT); }
double PointPip(string sym){ return Pip(sym); }

double ATR_Pips(string sym){
   int tf = (int)ATR_TF;
   int handle = iATR(sym, tf, ATR_Period);
   if(handle==INVALID_HANDLE) return 0;
   double atr = iATR(sym, tf, ATR_Period, 0);
   if(atr==0) return 0;
   return atr / PointPip(sym);
}

// compute nearest quarter point
double NearestQP(string sym, double price){
   double pips = (price - MarketInfo(sym, MODE_MINLOT)*0 + 0) / PointPip(sym);
   // price in pips from 0, use modulus by QuarterStepPips
   // safer: compute floor to nearest multiple
   double step = QuarterStepPips;
   double k = MathFloor(pips/step + 0.5);
   double qp_pips = k*step;
   return qp_pips * PointPip(sym);
}

// compute precise QP candidates around current price
double QPLevel(string sym, double price, int offsetSteps){
   double pips = price / PointPip(sym);
   double base = MathFloor(pips/QuarterStepPips) * QuarterStepPips;
   double qp = (base + offsetSteps*QuarterStepPips) * PointPip(sym);
   return qp;
}

bool SpreadOK(string sym){
   double spread = (MarketInfo(sym, MODE_ASK)-MarketInfo(sym, MODE_BID))/PointPip(sym);
   return (spread <= MaxSpreadPips);
}

bool TimeOK(){
   int h = TimeHour(TimeCurrent());
   if(StartHour <= EndHour) return (h>=StartHour && h<EndHour);
   return (h>=StartHour || h<EndHour);
}

bool NewsBlocked(){
   // Simple manual block around hour/minute ranges using global variables
   // Set a global variable "NEWSBLOCK_UNTIL" with TimeCurrent()+minutes*60 to block.
   if(GlobalVariableCheck("NEWSBLOCK_UNTIL")){
      double t = GlobalVariableGet("NEWSBLOCK_UNTIL");
      if(TimeCurrent() < t) return true;
   }
   return false;
}

int OnInit(){ return(INIT_SUCCEEDED); }
void OnDeinit(const int reason){}

void ManageTrailing(string sym, int ticket, double entryPrice, double sl, double tp, double risk_pips){
   if(!UseTrailing) return;
   if(OrderSelect(ticket, SELECT_BY_TICKET)){
      double pl_pips = 0;
      if(OrderType()==OP_BUY)  pl_pips = (Bid - entryPrice)/PointPip(sym);
      if(OrderType()==OP_SELL) pl_pips = (entryPrice - Ask)/PointPip(sym);
      if(pl_pips >= risk_pips * TrailStartRR){
         // step trail
         double newSL = sl;
         if(OrderType()==OP_BUY)  newSL = MathMax(sl, Bid - TrailStepPips*PointPip(sym));
         if(OrderType()==OP_SELL) newSL = MathMin(sl, Ask + TrailStepPips*PointPip(sym));
         if(MathAbs(newSL - sl) >= PointPip(sym)){
            OrderModify(ticket, OrderOpenPrice(), newSL, tp, 0, clrNONE);
         }
      }
   }
}

void CheckSymbol(string sym){
   if(!TimeOK() || NewsBlocked()) return;
   if(!SpreadOK(sym)) return;

   double pt = PointPip(sym);
   double atr_p = ATR_Pips(sym);
   if(atr_p==0 || atr_pATR_MaxPips) return;

   double ask = MarketInfo(sym, MODE_ASK);
   double bid = MarketInfo(sym, MODE_BID);
   double mid = (ask+bid)/2.0;

   // compute current QP above and below
   double qp0 = QPLevel(sym, mid, 0);
   if(mid < qp0) qp0 = QPLevel(sym, mid, 0); // base
   // generate nearby QPs
   double qps[5];
   for(int i=-2;i<=2;i++){ qps[i+2] = QPLevel(sym, mid, i); }

   // detect break & revert around nearest QP (use last LookbackBars highs/lows)
   int tf = Period();
   int bars = iBars(sym, tf);
   if(bars < LookbackBars+2) return;

   double hh= -1e9, ll = 1e9;
   for(int i=1;i<=LookbackBars;i++){
      hh = MathMax(hh, iHigh(sym, tf, i));
      ll = MathMin(ll, iLow(sym, tf, i));
   }

   // choose the nearest QP
   double nearest = qps[2];
   double dist = MathAbs(mid - nearest);
   for(int j=0;j<5;j++){
      double d = MathAbs(mid - qps[j]);
      if(d < dist){ dist = d; nearest = qps[j]; }
   }

   double pad = EntryPadPips*pt;

   bool brokeUp   = (hh >= nearest + pad);
   bool brokeDown = (ll <= nearest - pad);

   // revert condition: current mid back inside nearest +/- pad
   bool backInside = (mid > nearest - pad && mid < nearest + pad);

   // Entry conditions
   if(brokeUp && backInside){
      // Sell signal
      double sl = mid + StopATR*atr_p*pt;
      double tp = mid - TP_ATR*atr_p*pt;
      double risk_pips = (sl - mid)/pt;
      double lot = 0.0;
      // position sizing (rough): risk% * balance / (risk_pips * pipValue)
      double pipValue = MarketInfo(sym, MODE_TICKVALUE) * (pt/MarketInfo(sym, MODE_TICKSIZE));
      if(pipValue>0 && risk_pips>0){
         lot = (AccountBalance() * (RiskPerTradePct/100.0)) / (risk_pips * pipValue);
         double minLot = MarketInfo(sym, MODE_MINLOT);
         double lotStep = MarketInfo(sym, MODE_LOTSTEP);
         if(lot < minLot) lot = minLot;
         lot = MathFloor(lot/lotStep)*lotStep;
      }else lot = MarketInfo(sym, MODE_MINLOT);

      int ticket = OrderSend(sym, OP_SELL, lot, bid, 2, sl, tp, "QP-Reversal Sell", 0, 0, clrRed);
      if(ticket>0) ManageTrailing(sym, ticket, mid, sl, tp, risk_pips);
   }
   else if(brokeDown && backInside){
      // Buy signal
      double sl = mid - StopATR*atr_p*pt;
      double tp = mid + TP_ATR*atr_p*pt;
      double risk_pips = (mid - sl)/pt;
      double lot = 0.0;
      double pipValue = MarketInfo(sym, MODE_TICKVALUE) * (pt/MarketInfo(sym, MODE_TICKSIZE));
      if(pipValue>0 && risk_pips>0){
         lot = (AccountBalance() * (RiskPerTradePct/100.0)) / (risk_pips * pipValue);
         double minLot = MarketInfo(sym, MODE_MINLOT);
         double lotStep = MarketInfo(sym, MODE_LOTSTEP);
         if(lot < minLot) lot = minLot;
         lot = MathFloor(lot/lotStep)*lotStep;
      }else lot = MarketInfo(sym, MODE_MINLOT);

      int ticket = OrderSend(sym, OP_BUY, lot, ask, 2, sl, tp, "QP-Reversal Buy", 0, 0, clrBlue);
      if(ticket>0) ManageTrailing(sym, ticket, mid, sl, tp, risk_pips);
   }
}

int start(){
   // loop over symbols in the list
   string arr[];
   int n = StringSplit(Symbols, ',', arr);
   if(n<=0){ CheckSymbol(Symbol()); return(0); }
   for(int i=0;i<n;i++){
      string sym = StringTrim(arr[i]);
      if(sym=="") continue;
      if(Symbol()==sym || MarketInfo(sym, MODE_EXISTS)) CheckSymbol(sym);
   }
   return(0);
}
//+------------------------------------------------------------------+
    

TradingViewインジケータ(Pine Script v5)

視覚化と手動検証のために、クォーター・ポイントと“掃除”をハイライトするインジケータ例です。


//@version=5
indicator("Quarter-Point Sweep Highlighter", overlay=true)
step = input.int(25, "Quarter Step (pips)")
pad  = input.float(2.0, "Break Pad (pips)")
pips(x) => syminfo.type == "forex" ? x*syminfo.point*10 : x*syminfo.point
// compute nearest quarter level around close
qp = math.floor(close / pips(step)) * pips(step)
qpAbove = qp + pips(step)
qpBelow = qp - pips(step)

brokeUp = ta.highest(high, 6) >= qp + pips(pad)
brokeDn = ta.lowest(low, 6)   <= qp - pips(pad)
backIn  = close < qp + pips(pad) and close > qp - pips(pad)

plot(qp,  "QP",  style=plot.style_circles, linewidth=1)
plot(qpAbove, "QP+", style=plot.style_circles, linewidth=1)
plot(qpBelow, "QP-", style=plot.style_circles, linewidth=1)

bgcolor(brokeUp and backIn ? color.new(color.red, 85) : na)
bgcolor(brokeDn and backIn ? color.new(color.green, 85) : na)
    

ケーススタディ(USDJPY・想定例)

たとえばUSDJPYが144.50を急伸で144.524まで掃除した後、5分以内に144.49台へ戻る。
このとき回帰方向(下)へショート、損切りはATR×0.8、利確はATR×1.2、または144.375(中点)で部分利確。
同時にトレーリングを起動し、リスクに対する利益確率を積み重ねます。

失敗パターンと回避策

  • 本格トレンドの序章:掃除に見えて実はブレイクの初動。対策:ATR上限・下限、上位足トレンドフィルタ。
  • 指標スパイク:ニュースでの貫通。対策:重要指標カレンダー連動で一定時間の取引停止。
  • スプレッド拡大:早朝・祝日など。対策:最大スプレッド閾値でブロック。

運用チェックリスト

  1. ブローカー仕様(最小ロット・ティックバリュー・スプレッド)確認。
  2. 通貨別に最適なATRレンジを事前推定。
  3. バックテスト→フォワード→デモ運用→低ロット本番の順に段階導入。
  4. 週次でパフォーマンス分解(時間帯・通貨・ATR帯・RR・連敗数)。

拡張アイデア

  • クォーター・ポイントに時間加重VWAPバンドを重ね、回帰強度をスコア化。
  • スイープのティック速度(秒間レンジ)を閾値化。
  • マルチシンボルの同時監視と相関(USD系の同時スイープ)を利用。

まとめ

本戦略は、誰もが置きやすい価格水準に生じる構造的な歪みを、明快なルールで機械的に刈り取る逆張りデイトレです。
25pips刻みの水準、突破と回帰の二段判定、ATRを用いた動的な損益確定、スプレッドと時間帯の管理という、
初心者にも実装しやすい枠組みで構成されています。小さな期待値の積み重ねを、堅実なリスク管理とともに追求してください。

コメント

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