Wick Exhaustion Scalper EA — ヒゲ比率で狙うシンプル反転スキャルピング(初心者向け・完全実装)

EA
要点:ヒゲが長く「行き過ぎた」直後は、短期的に価格が戻りやすい——この素朴な現象を、
ローソク足のヒゲ比率 Wick Exhaustion Ratio(WER) で定量化し、EAで自動執行します。

1. 戦略のコンセプト

多くの初心者が最初に学ぶのは移動平均やRSIですが、値動きの根源は「注文フロー」です。
勢いよく一方向に走った後、上髭下髭が極端に伸びたローソクは、
しばしば「最後の追い込み(exhaustion)」を示唆します。つまり、流動性を吸って一旦反転しやすい。

本稿では、この「行き過ぎ→小反転」を機械的に捉えるため、直近確定足のヒゲと実体の比率を使った独自の
指標 WER(Wick Exhaustion Ratio) を定義し、ルールに落とし込みます。

2. WER(Wick Exhaustion Ratio)の定義

確定足(シフト=1)の価格要素を Open[1], High[1], Low[1], Close[1] とします。
実体の上端 top = max(Open[1], Close[1])、下端 bottom = min(Open[1], Close[1])

  • 上髭:upper = High[1] - top
  • 下髭:lower = bottom - Low[1]
  • 実体:body = top - bottom

WERは「髭/実体」。安定化のために微小値epsを加えます。

WER_upper = upper / (body + eps)
WER_lower = lower / (body + eps)

発想は単純ですが、ボラティリティ正規化スプレッド/時間帯フィルタを併用することで
無駄打ちを抑えられます。

3. 売買ルール(初期版)

3.1 エントリー

  • 買い(反転狙い):直近確定足が長い下髭(WER_lower ≥ θ)かつ下落方向に拡張後、
    新バー開始時に成行で買い。
  • 売り(反転狙い):直近確定足が長い上髭(WER_upper ≥ θ)かつ上昇方向に拡張後、
    新バー開始時に成行で売り。

推奨の併用フィルタ:

  1. ATRフィルタ:確定足のレンジがATR(14)の0.6〜1.5倍に入っていること
    (極端すぎる髭や極小値動きを除外)。
  2. ボディ閾値BodyPips ≥ 2 など最小実体を保証。
  3. スプレッドSpreadPips ≤ MaxSpread
  4. 時間帯:ロンドン/NY時間中心(例:08:00–23:00 サーバ時刻)。

3.2 エグジット

  • 固定SL/TP:例)SL=8–15 pips, TP=8–20 pips
  • 時間切れ:ポジション寿命(例:bars_alive ≥ 8 で成行決済)。
  • トレーリング(任意):TrailingStart 到達後、TrailingStepで更新。

4. リスク管理とロット計算

EAは口座残高に対するリスク%からロットを自動算定します。例えば、リスク1%・SL=10pipsなら、
損失が1%を超えないロットに調整します。ボラが上がればロットは自然に下がり、下がれば増えます。

推奨:1トレードのリスクは口座の0.25〜1.0%。連敗時はEAの自動停止やロットの段階縮小を
組み込み、破綻確率を抑えます。

5. どの通貨・足で使うか

初心者は EURUSD・GBPUSDのM5 から開始が無難です。流動性が高く、スプレッドが安定しているためです。
スキャルピングなので超低スプレッド口座が前提です。最初はスプレッド上限を2.0pipsに厳しめ設定し、
慣れたら3.0pipsまで拡張すると良いでしょう。

6. バックテスト手順(MT4)

  1. MT4の「ストラテジーテスター」を開く。
  2. モデルは「毎ティック」。通貨はEURUSD、期間は1〜2年、時間足はM5。
  3. スプレッドはブローカー実勢に近い値(例:Spread=12 = 1.2pips)。
  4. 可変スプレッド環境なら、可変のまま実行して現実に寄せる。
  5. レポートでは、PF、ドローダウン、連敗数、平均勝ち負けpipsを確認。

7. 改良の方向性

  • セッション識別:ロンドン/NYで閾値を変える。
  • ラウンドナンバー:00/50付近の反転は強い傾向。距離フィルタを追加。
  • 多時間足:上位足のトレンド方向と逆張り閾値を連動。
  • クールダウン:同方向の連続シグナルを制限(例:同一方向2連続まで)。
  • ヒゲの比率の平滑化:直近数本のWERをEMAでならし、スパイクを除外。

8. フルコード(MQL4 EA)

そのままMT4に貼り付けて動かせる最小実装です。ロジックを読みやすく整理し、パラメータで調整できるようにしています。

//+------------------------------------------------------------------+
//| Wick Exhaustion Scalper EA (WER-based Reversal)                 |
//| Timeframe: M5 (recommend)                                       |
//| Symbols : EURUSD, GBPUSD (low spread)                           |
//+------------------------------------------------------------------+
#property strict

input double RiskPercent = 0.5;       // % risk per trade
input int    SL_Pips     = 10;
input int    TP_Pips     = 12;
input double WER_Thresh  = 2.0;       // wick/body threshold
input double MinBodyPips = 2.0;
input double AtrMulMin   = 0.6;       // range >= ATR(14)*AtrMulMin
input double AtrMulMax   = 1.5;       // range <= ATR(14)*AtrMulMax
input int    MaxSpread   = 20;        // 2.0 pips if Digits=5
input int    Magic       = 20250905;
input bool   UseTimeFilter = true;
input int    StartHour   = 8;         // server time
input int    EndHour     = 23;
input bool   UseTrailing = true;
input int    TrailStart  = 8;         // pips
input int    TrailStep   = 4;         // pips
input int    MaxBarsAlive = 8;

datetime last_bar_time = 0;

double Pip()
{
   if(Digits==3 || Digits==5) return(0.1*Point);
   return(Point);
}

int DigitsPip()
{
   return (Digits==3 || Digits==5) ? 1 : 0;
}

double ToPips(double price_diff)
{
   return price_diff / Pip();
}

double FromPips(double pips)
{
   return pips * Pip();
}

bool NewBar()
{
   datetime t = Time[0];
   if(t != last_bar_time) { last_bar_time = t; return true; }
   return false;
}

bool TimeAllowed()
{
   if(!UseTimeFilter) return true;
   int h = TimeHour(TimeCurrent());
   if(StartHour <= EndHour) return (h >= StartHour && h < EndHour);
   // overnight wrap
   return (h >= StartHour || h < EndHour);
}

double LotsForRisk(double stop_pips)
{
   if(stop_pips <= 0.0) return 0.0;
   double risk_money = AccountBalance() * (RiskPercent/100.0);
   double tickval = MarketInfo(Symbol(), MODE_TICKVALUE);
   double per_lot_per_pip = (tickval / Pip());
   if(per_lot_per_pip <= 0) return 0.01;
   double lots = risk_money / (stop_pips * per_lot_per_pip);
   double lotstep = MarketInfo(Symbol(), MODE_LOTSTEP);
   double minlot = MarketInfo(Symbol(), MODE_MINLOT);
   double maxlot = MarketInfo(Symbol(), MODE_MAXLOT);
   lots = MathFloor(lots/lotstep) * lotstep;
   lots = MathMax(minlot, MathMin(lots, maxlot));
   return NormalizeDouble(lots, 2);
}

double iATRpips(int period=14, int shift=1)
{
   double atr = iATR(Symbol(), Period(), period, shift);
   return ToPips(atr);
}

void TrailPositions()
{
   if(!UseTrailing) return;
   for(int i=OrdersTotal()-1; i>=0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol()!=Symbol() || OrderMagicNumber()!=Magic) continue;
      double profit_pips = 0;
      if(OrderType()==OP_BUY)  profit_pips = ToPips(Bid - OrderOpenPrice());
      if(OrderType()==OP_SELL) profit_pips = ToPips(OrderOpenPrice() - Ask);
      if(profit_pips >= TrailStart)
      {
         double new_sl = OrderStopLoss();
         if(OrderType()==OP_BUY)
         {
            double candidate = Bid - FromPips(TrailStep);
            if(candidate > new_sl)
               OrderModify(OrderTicket(), OrderOpenPrice(), candidate, OrderTakeProfit(), 0);
         }
         else if(OrderType()==OP_SELL)
         {
            double candidate = Ask + FromPips(TrailStep);
            if(candidate < new_sl || new_sl==0)
               OrderModify(OrderTicket(), OrderOpenPrice(), candidate, OrderTakeProfit(), 0);
         }
      }
   }
}

int BarsAliveOf(int ticket)
{
   if(!OrderSelect(ticket, SELECT_BY_TICKET)) return 0;
   datetime open_time = OrderOpenTime();
   int bars = 0;
   for(int i=0; i<Bars; i++)
   {
      if(Time[i] <= open_time) break;
      bars++;
   }
   return bars;
}

void CloseOldPositions()
{
   for(int i=OrdersTotal()-1; i>=0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol()!=Symbol() || OrderMagicNumber()!=Magic) continue;
      int barsAlive = BarsAliveOf(OrderTicket());
      if(barsAlive >= MaxBarsAlive)
      {
         if(OrderType()==OP_BUY)  OrderClose(OrderTicket(), OrderLots(), Bid, 10);
         if(OrderType()==OP_SELL) OrderClose(OrderTicket(), OrderLots(), Ask, 10);
      }
   }
}

int ActivePositions()
{
   int n=0;
   for(int i=0; i<OrdersTotal(); i++)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol()==Symbol() && OrderMagicNumber()==Magic) n++;
   }
   return n;
}

int OnInit(){ last_bar_time = 0; return(INIT_SUCCEEDED); }
int OnDeinit(){ return(0); }

int start()
{
   if(NewBar())
   {
      CloseOldPositions();
      if(!TimeAllowed()) return(0);
      if(ActivePositions()>=1) return(0);

      // Spread filter
      int spread_points = MarketInfo(Symbol(), MODE_SPREAD);
      if(spread_points > MaxSpread) return(0);

      // candle[1]
      double o = Open[1], h = High[1], l = Low[1], c = Close[1];
      double top = MathMax(o,c);
      double bottom = MathMin(o,c);
      double upper = h - top;
      double lower = bottom - l;
      double body  = top - bottom;
      double eps   = FromPips(0.1);

      double upper_p = ToPips(upper);
      double lower_p = ToPips(lower);
      double body_p  = ToPips(body);

      // body filter
      if(body_p < MinBodyPips) return(0);

      // ATR filter using candle range
      double range_p = ToPips(h - l);
      double atr14_p = iATRpips(14, 1);
      if(range_p < atr14_p*AtrMulMin || range_p > atr14_p*AtrMulMax) return(0);

      double WER_up = upper_p / (body_p + 0.1);
      double WER_lo = lower_p / (body_p + 0.1);

      double sl_p = SL_Pips;
      double tp_p = TP_Pips;
      double lots = LotsForRisk(sl_p);
      if(lots <= 0.0) return(0);

      int ticket = -1;
      // BUY signal: long lower wick exhaustion
      if(WER_lo >= WER_Thresh && Close[1] < Open[1])
      {
         double sl = Bid - FromPips(sl_p);
         double tp = Bid + FromPips(tp_p);
         ticket = OrderSend(Symbol(), OP_BUY, lots, Ask, 10, sl, tp, "WER BUY", Magic, 0, clrBlue);
      }
      // SELL signal: long upper wick exhaustion
      else if(WER_up >= WER_Thresh && Close[1] > Open[1])
      {
         double sl = Ask + FromPips(sl_p);
         double tp = Ask - FromPips(tp_p);
         ticket = OrderSend(Symbol(), OP_SELL, lots, Bid, 10, sl, tp, "WER SELL", Magic, 0, clrRed);
      }
   }
   TrailPositions();
   return(0);
}
//+------------------------------------------------------------------+

9. 補助インジケータ(MQL4)

チャートにWERを可視化するインジケータです。閾値以上の足にマークを表示します。

//+------------------------------------------------------------------+
//| Wick Exhaustion Ratio (WER) Indicator                           |
//+------------------------------------------------------------------+
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_color1 Blue
#property indicator_color2 Red

input double WER_Thresh = 2.0;
double upBuf[];
double loBuf[];

int OnInit()
{
   SetIndexBuffer(0, upBuf);
   SetIndexStyle(0, DRAW_HISTOGRAM, STYLE_SOLID, 2);
   SetIndexLabel(0, "WER_upper");

   SetIndexBuffer(1, loBuf);
   SetIndexStyle(1, DRAW_HISTOGRAM, STYLE_SOLID, 2);
   SetIndexLabel(1, "WER_lower");
   return(INIT_SUCCEEDED);
}

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   int start = (prev_calculated==0) ? 1 : prev_calculated-1;
   for(int i=start; i<rates_total; i++)
   {
      double top = MathMax(open[i], close[i]);
      double bottom = MathMin(open[i], close[i]);
      double body = top - bottom + Point*0.1;
      double upper = high[i] - top;
      double lower = bottom - low[i];
      double WU = upper / body;
      double WL = lower / body;
      upBuf[i] = WU;
      loBuf[i] = WL;
   }
   return(rates_total);
}
//+------------------------------------------------------------------+

10. ビジュアル検証用(補足)Pine Script v5

TradingViewでWERを重ねて挙動を直感的に確認したい場合の補助スクリプトです(売買や自動執行はEA側で行ってください)。

//@version=5
indicator("Wick Exhaustion Ratio (WER)", overlay=false)
eps = syminfo.mintick * 0.1
body = math.abs(close - open) + eps
upper = high - math.max(open, close)
lower = math.min(open, close) - low
WER_up = upper / body
WER_lo = lower / body
plot(WER_up, title="WER_upper", style=plot.style_histogram)
plot(WER_lo, title="WER_lower", style=plot.style_histogram, color=color.new(color.red, 0))
hline(2.0, "th=2.0")

11. 期待値の源泉と限界

WERの根拠は「流動性狩り後の短期的な反転バイアス」。ただし万能ではありません。
強トレンド押し戻り中の髭は反転せず続伸・続落することも多い。よってATRフィルタや時間帯、
ラウンドナンバーの距離などの文脈フィルタが効きます。

12. パラメータの初期ガイド

項目 推奨初期値 意図
WER_Thresh 2.0 「極端な髭」に限定
SL/TP 10/12 pips スプレッド耐性と期待値の妥協点
AtrMulMin/Max 0.6/1.5 極端レンジを除外
RiskPercent 0.5% 破綻確率の抑制
MaxBarsAlive 8 時間切れ決済

13. 実運用の注意点

スリッページ・約定拒否・可変スプレッドはスキャルに致命的です。VPSと低レイテンシ、
低スプレッド口座、原則固定or狭スプレッド環境を前提に。重要なのは「約定品質」です。
必ずデモ→小額→段階的に。

14. まとめ

WERは、初心者でも理解しやすく、機械化しやすい値動きの素朴な偏りを狙います。
本稿のEAは最小構成なので、時間帯やラウンドナンバー、上位足判定などを足しながら、
自分のブローカー特性に合う「現実解」を見つけてください。

コメント

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