本稿では、為替相場が25pips刻みの水準(0.00/0.25/0.50/0.75)に集まりやすいという市場参加者の行動パターンに着目し、
その水準を一時的に突破(ストップラン)した直後の反発(ミーンリバージョン)を狙う、実運用可能な短期戦略を提示します。
ルールはシンプルですが、流動性の偏り・ストップ注文の偏在・アルゴの定型的な約定動作というミクロ構造の歪みに根拠があり、
しかも自律実行(EA)しやすいのが利点です。
戦略のコンセプト
多くのトレーダーは、指値・逆指値をキリの良い価格や四半端(0.25/0.50/0.75)に集中させます。
こうしたクォーター・ポイント周辺は、流動性の“ポケット”が生まれやすく、
オーダーブックが薄くなると、短時間で水準を“掃除”するような急伸・急落が発生します。
掃除の直後は、利食い・逆張り・アルゴの在庫調整により、確率的に元のレンジへ回帰しやすくなります。
本戦略は、この掃除→回帰の非対称性を短時間で取りに行く逆張りデイトレです。
対象通貨・時間軸
- 対象通貨:USDJPY / EURUSD / GBPUSD(スプレッドが安定かつ流動性の高い通貨)
- 時間軸:M1~M15(筆者推奨はM5またはM15)
- 取引時間:ロンドン~NY時間を中心に(東京早朝の板薄時間は回避設定可)
エッジの根拠(ミクロ構造)
- 注文の集積:四半端の手前後ろに逆指値(ブレイクアウト狙い)とストップが重なりやすい。
- 流動性掃除:ブローカー間のスプレッド拡大や約定優先で水準を“抜かせに行く”動きが発生。
- 在庫調整:掃除直後はディーラーやショートタームアルゴの在庫調整が優先し、価格は直近の公正価値へ回帰しやすい。
売買ルール(要点)
価格が直近のクォーター・ポイント(以下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)
- ヒストリカル:最低3~5年、ティック精度は可能な限り向上(可ならばDukascopy等の細かいティック)。
- 取引コスト:スプレッド・コミッション・スリッページを現実的に上乗せ。
- ロジック固定:チューニングは歩留まりを見ながら最小限、過剰最適化を避ける。
- 評価指標: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上限・下限、上位足トレンドフィルタ。
- 指標スパイク:ニュースでの貫通。対策:重要指標カレンダー連動で一定時間の取引停止。
- スプレッド拡大:早朝・祝日など。対策:最大スプレッド閾値でブロック。
運用チェックリスト
- ブローカー仕様(最小ロット・ティックバリュー・スプレッド)確認。
- 通貨別に最適なATRレンジを事前推定。
- バックテスト→フォワード→デモ運用→低ロット本番の順に段階導入。
- 週次でパフォーマンス分解(時間帯・通貨・ATR帯・RR・連敗数)。
拡張アイデア
- クォーター・ポイントに時間加重VWAPバンドを重ね、回帰強度をスコア化。
- スイープのティック速度(秒間レンジ)を閾値化。
- マルチシンボルの同時監視と相関(USD系の同時スイープ)を利用。
まとめ
本戦略は、誰もが置きやすい価格水準に生じる構造的な歪みを、明快なルールで機械的に刈り取る逆張りデイトレです。
25pips刻みの水準、突破と回帰の二段判定、ATRを用いた動的な損益確定、スプレッドと時間帯の管理という、
初心者にも実装しやすい枠組みで構成されています。小さな期待値の積み重ねを、堅実なリスク管理とともに追求してください。
コメント