preview
Automating Classic Market Methods in MQL5 (Part 9): Linda Bradford Raschke's 3/10 Oscillator System

Automating Classic Market Methods in MQL5 (Part 9): Linda Bradford Raschke's 3/10 Oscillator System

MetaTrader 5 — Examples |
236 0
Tola Moses Hector
Tola Moses Hector

Introduction

Linda Bradford Raschke began trading futures on the floor in 1981, moved to screen trading in the mid-1980s, and published "Street Smarts" with Larry Connors in 1996. She has traded her own account continuously since then. The methods in that book—particularly the 3/10 oscillator and the holy grail setup—have remained in active use among short-term swing traders for nearly thirty years.

Raschke's methods differ from the systems in Parts 5–8 in timeframe and required precision. Those systems—Turtle rules, Livermore's market key, Darvas boxes, and Seykota's EMA system—operate on daily or weekly bars and require patience measured in weeks. Raschke's methods operate on H1 or H4 bars and require precision measured in sessions. Signals are more frequent, stops are tighter, and exits are more specific.

This article automates two of her setups: the "Anti," which enters on the first oscillator pullback after a trend change confirmed by the signal line crossing zero; and the "Holy Grail," which enters on the first price pullback to the 20-period EMA in a strong, rising ADX trend.

RaschkeEA demonstration

Fig. 1. System demonstration.

This EA answers two questions:

  • Does the 3/10 oscillator confirm a trend change and then trigger an Anti entry with price confirmation?
  • Is ADX above 30 and rising for at least two bars, with price pulling back to the 20‑period EMA for a first‑pullback entry?

We will cover the following topics:

  1. The 3/10 Oscillator—Construction and Interpretation
  2. The Anti Setup—Theory and Rules
  3. The Holy Grail Setup—Theory and Rules
  4. Architecture—Two State Machines, One EA
  5. Implementation in MQL5
  6. Known Limitations and Design Decisions
  7. Conclusion


The 3/10 Oscillator—Construction and Interpretation

Raschke described the 3/10 oscillator as a tool she had used since 1981. It is a MACD variant with different periods and a critical construction difference: it uses simple moving averages throughout, not exponential ones. The fast line is SMA(fast) minus SMA(slow) of closing prices—3 and 10 periods by default. This difference oscillates around zero and captures short-term momentum. The signal line is SMA(signal) of the fast line—16 periods by default. It is slower and smoother.

When the fast line is above the signal line, short-term momentum is bullish. When it is below, momentum is bearish. The signal line crossing zero is the trend change confirmation. When the signal line crosses from below zero to above, the overall momentum has shifted bullish. When it crosses from above to below, it has shifted bearish.

This 3/10/16 construction produces faster cycles than the standard MACD (12, 26, 9) and is tuned specifically for swing trading on hourly (H1) and 4-hour (H4) bars. On a standard MACD, the signal line zero cross on H1 data may lag the actual price move by many bars. On the 3/10 oscillator, the lag is substantially reduced.

In MQL5, the 3/10 oscillator is computed manually from price arrays because the signal line is an SMA of the fast line—not a native MetaTrader indicator type. This article calculates both lines from price arrays on each bar, avoiding additional file dependencies.


The Anti Setup—Theory and Rules

The "Anti" is Raschke's own name for the setup. It enters against the most recent short-term direction—buying a pullback during a newly confirmed bullish trend, or selling a rally during a newly confirmed bearish trend.

The conditions are sequential, and all must be satisfied in order.

  • Step 1—Signal line zero cross: the 16-period signal line crosses from below to above zero (bullish confirmation) or from above to below zero (bearish confirmation). This does not trigger an entry; it opens a window for one.
  • Step 2—Fast line pullback: after the zero cross, the fast line moves against the new trend direction until it touches or crosses the signal line. For a bullish confirmation, the fast line dips to at or below the signal line. This is the slingshot loading phase. This EA requires the fast line to touch or cross the signal line—approaches that do not reach it are not counted as valid pullbacks.
  • Step 3—Fast line reversal cross: the fast line crosses back through the signal line in the direction of the new trend (above, for bull Anti). This is the trigger. Note: if this cross occurs but price confirmation fails (Step 4), the window closes entirely. The EA does not wait for a second cross—only the first reversal cross after a valid pullback qualifies.
  • Step 4—Price confirmation: the most recently closed bar must have closed in the direction of the new trend. For a bull Anti, the close must be higher than the previous bar's close. For a bear Anti, it must be lower.
  • Step 5—Entry: on the first tick of the next bar, using a market order. The EA enters at the then-current Ask (buy) or Bid (sell), which may differ slightly from the bar’s open price.

Stop: at the pullback extreme. For a bullish Anti, use the lowest low of the pullback; for a bearish Anti, use the highest high.

An ATR buffer can be added via input parameter (default: zero).

Exit: when the fast line crosses back through the signal line in the direction opposite to the trade.


The Holy Grail Setup—Theory and Rules

The "Holy Grail" requires a genuine, strengthening trend—not just a trend, but a trend gaining momentum—and enters at the moment where price offers its lowest-risk pullback point within that trend.

  • Step 1—Strong trend: ADX must be at or above 30 and must have been rising for at least two consecutive bars (ADX current > ADX previous > ADX two bars ago). Above 30 alone is not sufficient; the trend must be actively strengthening.
  • Step 2—EMA direction: the 20-period EMA must be sloping in the direction of the intended trade. Direction is determined by comparing the current bar's EMA value to the previous bar's value. One bar of EMA slope is admittedly a noisy signal; the ADX condition in Step 1 is the primary trend-quality filter.
  • Step 3—Pullback to EMA: the price must pull back to touch the 20-period EMA. For a long, the bar's low must be at or below the EMA. For a short, the bar's high must be at or above the EMA.
  • Step 4—First pullback only: only the first qualifying pullback after an ADX confirmation is taken. A second entry is allowed only after ADX drops below 30 and later returns above 30 with two consecutive rising bars. This reset is tracked explicitly in state; the EA does not allow re-entry simply because the prior trade closed.
  • Step 5—Entry: on the first tick of the next bar after the pullback bar, using a market order at the then-current Ask (buy) or Bid (sell), which may differ slightly from the bar’s open price.

Stop: at the extreme of the pullback bar—the low of the bar for longs, the high for shorts. An ATR buffer can be added via an input parameter (default: zero).

Exit 1: when ADX drops below 20—the trend is considered ended.

Exit 2: when the closing price crosses the 20-period EMA in the direction against the trade.


Architecture—Two State Machines, One EA

Both setups run as independent state machines in a single EA called "RaschkeEA.mq5." On each new bar, the EA runs both state machines. Either can generate a trade; however, only one position is open at a time.

The two state machines maintain separate direction variables.

The "g_anti_is_long" stores the "Anti's" direction; "g_hg_is_long" stores the "Holy Grail's" direction. Using a single shared direction variable would create the risk that one state machine's direction assignment overwrites the other's while the latter is in a waiting state.

  • Processing order: when both setups are enabled and no position is open, one must be evaluated first. By default, the "Anti" is processed first. If both setups simultaneously reach their entry condition on the same bar, the Anti takes priority. This behavior is configurable via "InpAntiFirst" and is disclosed in the log.
  • Ensuring one open position across restarts: on "OnInit()," "RecoverState()" scans all open positions on the account by symbol and magic number, identifies whether the recovered position belongs to the "Anti" or "Holy Grail" setup, and restores the appropriate state machine to its in-trade state. The EA does not assume "g_ticket == 0" means no position exists; it verifies by scanning open positions.

The "Anti" state machine has four states:

  • "ANTI_IDLE"—watching for signal line zero cross.
  • "ANTI_WAITING_PULLBACK"—zero cross confirmed, watching for fast line to touch or cross signal.
  • "ANTI_WAITING_REVERSAL"—pullback confirmed, watching for fast line to cross signal in trend direction.
  • "ANTI_IN_TRADE"—position open, watching for oscillator exit cross.

The "Holy Grail" state machine has three states:

  • "HG_IDLE"—watching for ADX above 30 rising for two bars with EMA sloping.
  • "HG_WAITING_PULLBACK"— ADX confirmed, watching for price to touch 20 EMA.
  • "HG_IN_TRADE"—position open, watching for EMA cross or ADX drop below 20.

Every state transition is logged via "Print()."


Implementation in MQL5

The EA is built section by section.

Includes, Enumerations, and Inputs

//+------------------------------------------------------------------+
//|                                                    RaschkeEA.mq5 |
//|                                Copyright 2026, Tola Moses Hector |
//|                                          https://t.me/tolahector |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Tola Moses Hector"
#property link      "https://t.me/tolahector"
#property version   "1.00"
#property description "Automating Classic Market Methods in MQL5 Part 9"
#property description "Linda Bradford Raschke — 3/10 Oscillator Anti and Holy Grail"
#property description "H4 timeframe recommended"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Anti State Machine                                               |
//+------------------------------------------------------------------+
enum ENUM_ANTI_STATE
  {
   ANTI_IDLE,              // Watching for signal line zero cross
   ANTI_WAITING_PULLBACK,  // Zero cross confirmed — fast line not yet at signal
   ANTI_WAITING_REVERSAL,  // Fast line touched signal — waiting for cross back
   ANTI_IN_TRADE           // Position open — watching for exit cross
  };

//+------------------------------------------------------------------+
//| Holy Grail State Machine                                         |
//+------------------------------------------------------------------+
enum ENUM_HG_STATE
  {
   HG_IDLE,               // Watching for ADX >= 30 rising 2 bars, EMA sloping
   HG_WAITING_PULLBACK,   // ADX confirmed — watching for price to touch 20 EMA
   HG_IN_TRADE            // Position open — watching for EMA cross or ADX drop
  };

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== 3/10 Oscillator (Anti Setup) ==="
input int    InpFastSMA     = 3;     // Fast SMA period
input int    InpSlowSMA     = 10 ;     // Slow SMA period
input int    InpSignalSMA   = 16;     // Signal line SMA period
input bool   InpAntiEnabled = true;   // Enable the Anti setup

input group "=== Holy Grail Setup ==="
input int    InpEMAPeriod    = 20;    // EMA period for pullback
input int    InpADXPeriod    = 14;    // ADX period
input double InpADXThreshold = 30.0;  // Minimum ADX level (must be rising for 2 bars)
input bool   InpHGEnabled    = true;  // Enable the Holy Grail setup

input group "=== Risk ==="
input double InpRiskPercent  = 1.0;   // Risk per trade (% of balance)
input int    InpATRPeriod    = 14;    // ATR period
input double InpATRStopMult  = 0.0;   // ATR buffer added to swing stop (0 = exact swing stop)

input group "=== General ==="
input int    InpMagicNumber  = 100901;
input int    InpSlippage     = 10;
input bool   InpAntiFirst    = true;  // true = Anti evaluated first; false = Holy Grail first
input bool   InpShowLabels   = true;

Global Variables and Indicator Handles

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
//--- Indicator handles
int g_ema_handle = INVALID_HANDLE;
int g_adx_handle = INVALID_HANDLE;
int g_atr_handle = INVALID_HANDLE;

//--- State machines
ENUM_ANTI_STATE g_anti_state = ANTI_IDLE;
ENUM_HG_STATE   g_hg_state   = HG_IDLE;

//--- Separate direction per state machine (critical for independence)
bool g_anti_is_long = true;
bool g_hg_is_long   = true;

//--- Shared position tracking (only one position at a time)
ulong    g_ticket     = 0;
double   g_stop_price = 0;
datetime g_last_bar   = 0;
CTrade   g_trade;

//--- Anti state data
double g_anti_swing_stop = 0;

//--- Holy Grail state data
double g_hg_pb_extreme      = 0;
bool   g_hg_needs_adx_reset = false;  // After a trade, require ADX to drop below threshold

Utility Functions

"PipSize()" and "CalcLots()" handle instrument-correct pip values. Lot size is calculated from the confirmed stop distance—after any broker minimum distance adjustment—to ensure the actual risk matches the configured percentage.

//+------------------------------------------------------------------+
//| Returns pip size for the current symbol                          |
//+------------------------------------------------------------------+
double PipSize()
  {
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   return (digits == 3 || digits == 5) ? _Point * 10.0 : _Point;
  }

//+------------------------------------------------------------------+
//| Computes lot size from risk percent and stop distance in pips    |
//+------------------------------------------------------------------+
double CalcLots(double sl_pips)
  {
   if(sl_pips <= 0)
      return 0;
   double balance   = AccountInfoDouble(ACCOUNT_BALANCE);
   double risk_amt  = balance * InpRiskPercent / 100.0;
   double tick_val  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   double pip_size  = PipSize();
   if(tick_size <= 0 || tick_val <= 0)
      return 0;
   double pip_value = (pip_size / tick_size) * tick_val;
   double lots      = risk_amt / (sl_pips * pip_value);
   double step      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double min_lot   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double max_lot   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   lots = MathFloor(lots / step) * step;
   return MathMax(min_lot, MathMin(max_lot, lots));
  }

State Recovery After Restart

When the EA restarts—due to a terminal reboot, VPS interruption, or manual disable-and-enable—all in-memory state is lost. Without recovery, the EA would not know a position is already open, risking duplicate entries. "RecoverState()" solves this by scanning all open positions by symbol and magic number on startup and restoring the correct state machine to its in-trade state.

//+------------------------------------------------------------------+
//| State recovery                                                   |
//+------------------------------------------------------------------+
void RecoverState()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket))
         continue;
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)
         continue;
      if(PositionGetInteger(POSITION_MAGIC)  != (long)InpMagicNumber)
         continue;

      g_ticket     = ticket;
      g_stop_price = PositionGetDouble(POSITION_SL);
      bool is_long = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
      string comment = PositionGetString(POSITION_COMMENT);

      if(StringFind(comment, "Anti") >= 0)
        {
         g_anti_is_long = is_long;
         g_anti_state   = ANTI_IN_TRADE;
         g_hg_state     = HG_IDLE;
         Print(StringFormat("RaschkeEA: Recovered Anti position | %s | Ticket:%I64u",
                            (is_long ? "LONG" : "SHORT"), ticket));
        }
      else
         if(StringFind(comment, "HolyGrail") >= 0)
           {
            g_hg_is_long         = is_long;
            g_hg_state           = HG_IN_TRADE;
            g_hg_needs_adx_reset = true;
            g_anti_state         = ANTI_IDLE;
            Print(StringFormat("RaschkeEA: Recovered HolyGrail position | %s | Ticket:%I64u",
                               (is_long ? "LONG" : "SHORT"), ticket));
           }
      break; // Only one position tracked
     }
  }

Chart Drawing Helpers

//+------------------------------------------------------------------+
//| Places a text label at the specified time and price              |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr)
  {
   if(!InpShowLabels)
      return;
   string obj = "LBR_" + name;
   ObjectDelete(0, obj);
   ObjectCreate(0, obj, OBJ_TEXT, 0, time, price);
   ObjectSetString(0,  obj, OBJPROP_TEXT, text);
   ObjectSetInteger(0, obj, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9);
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Removes all chart objects created by this EA                     |
//+------------------------------------------------------------------+
void ClearLabels()
  {
   int total = ObjectsTotal(0);
   for(int i = total - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);
      if(StringFind(name, "LBR_") == 0)
         ObjectDelete(0, name);
     }
   ChartRedraw(0);
  }

Computing the 3/10 Oscillator

"Compute310()" calculates the fast line and signal line values manually from closing price arrays. Both current and previous values are returned so state machine comparisons have what they need without re-reading data.

//+------------------------------------------------------------------+
//| Computes 3/10 oscillator fast line and signal line               |
//| Uses simple moving averages — not exponential                    |
//| Returns false if insufficient bars available                     |
//+------------------------------------------------------------------+
bool Compute310(double &fast_curr, double &fast_prev,
                double &sig_curr,  double &sig_prev)
  {
   int needed = InpSlowSMA + InpSignalSMA + 5;
   double close[];
   ArraySetAsSeries(close, true);
   if(CopyClose(_Symbol, PERIOD_CURRENT, 1, needed, close) < needed)
      return false;

//--- Fast line: SMA(3) minus SMA(10) for each recent bar
   double fast_vals[];
   ArrayResize(fast_vals, InpSignalSMA + 2);
   for(int i = 0; i < InpSignalSMA + 2; i++)
     {
      double sma3 = 0, sma10 = 0;
      for(int j = 0; j < InpFastSMA;  j++)
         sma3  += close[i + j];
      for(int j = 0; j < InpSlowSMA;  j++)
         sma10 += close[i + j];
      fast_vals[i] = (sma3 / InpFastSMA) - (sma10 / InpSlowSMA);
     }
   fast_curr = fast_vals[0];
   fast_prev = fast_vals[1];

//--- Signal line: 16-period SMA of the fast line
   double sc = 0, sp = 0;
   for(int i = 0; i < InpSignalSMA; i++)
     {
      sc += fast_vals[i];
      sp += fast_vals[i + 1];
     }
   sig_curr = sc / InpSignalSMA;
   sig_prev = sp / InpSignalSMA;
   return true;
  }

Opening and Closing a Trade

"OpenTrade()" handles both setup entries. The stop is placed at the provided swing extreme. If "InpATRStopMult" exceeds zero, an ATR buffer is added—this departs from Raschke's exact specification, which places the stop at the swing extreme only. Setting "InpATRStopMult" to zero (the default) matches her description precisely.

//+------------------------------------------------------------------+
//| Opens a trade for either setup                                   |
//+------------------------------------------------------------------+
bool OpenTrade(bool is_long, double swing_stop, double atr, string label)
  {
   double sl_buffer = atr * InpATRStopMult;
   double sl        = is_long
                      ? NormalizeDouble(swing_stop - sl_buffer, _Digits)
                      : NormalizeDouble(swing_stop + sl_buffer, _Digits);

   long   stop_lv  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double min_dist = stop_lv * _Point;

   bool ok = false;
   if(is_long)
     {
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      if(ask - sl < min_dist)
         sl = NormalizeDouble(ask - min_dist - _Point, _Digits);
      double sl_pips = (ask - sl) / PipSize();
      double lots    = CalcLots(sl_pips);
      if(lots <= 0)
        {
         Print("RaschkeEA: CalcLots returned 0 — skipping entry.");
         return false;
        }
      ok = g_trade.Buy(lots, _Symbol, ask, sl, 0, label);
     }
   else
     {
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      if(sl - bid < min_dist)
         sl = NormalizeDouble(bid + min_dist + _Point, _Digits);
      double sl_pips = (sl - bid) / PipSize();
      double lots    = CalcLots(sl_pips);
      if(lots <= 0)
        {
         Print("RaschkeEA: CalcLots returned 0 — skipping entry.");
         return false;
        }
      ok = g_trade.Sell(lots, _Symbol, bid, sl, 0, label);
     }

   if(!ok)
     {
      Print(StringFormat("RaschkeEA: Order failed | Retcode:%d | Error:%d",
                         g_trade.ResultRetcode(), GetLastError()));
      return false;
     }

//--- Reliably get position ticket via DEAL_POSITION_ID
   ulong deal = g_trade.ResultDeal();
   if(deal > 0 && HistoryDealSelect(deal))
      g_ticket = (ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID);
   else
     {
      g_ticket = g_trade.ResultOrder(); // Fallback — order ticket may differ from position ticket
      Print("RaschkeEA: ResultDeal() unavailable — using ResultOrder() as ticket fallback.");
     }

   g_stop_price = sl;

   datetime t = iTime(_Symbol, PERIOD_CURRENT, 1);
   DrawLabel(label, t,
             is_long ? iLow(_Symbol, PERIOD_CURRENT, 1)  - PipSize() * 5
             : iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 5,
             label, is_long ? clrDodgerBlue : clrOrangeRed);

   Print(StringFormat("RaschkeEA: %s [%s] | SL:%.5f | SwingStop:%.5f | ATRBuf:%.5f | Ticket:%I64u",
                      (is_long ? "LONG" : "SHORT"), label, sl, swing_stop, sl_buffer, g_ticket));
   return true;
  }

//+------------------------------------------------------------------+
//| Closes the current open position                                 |
//+------------------------------------------------------------------+
void CloseTrade(string reason)
  {
   if(!PositionSelectByTicket(g_ticket))
     {
      g_ticket = 0;
      return;
     }
   double profit = PositionGetDouble(POSITION_PROFIT);
   if(g_trade.PositionClose(g_ticket))
     {
      Print(StringFormat("RaschkeEA: Closed | Reason:%s | P&L:%.2f", reason, profit));
      g_ticket     = 0;
      g_stop_price = 0;
      ClearLabels();
     }
  }

Processing the Anti Setup

"ProcessAnti()" implements the four-state machine. Four design decisions are worth explaining explicitly:

  • Pullback detection (Step 2): the fast line must touch or cross the signal line. Approaches that come close but do not reach the signal line are not counted. This is a conservative interpretation of Raschke's "toward or below" wording, chosen for unambiguous implementation.
  • Failed price confirmation resets to IDLE (Steps 3–4): if the fast line produces a valid reversal cross but the trigger bar's close does not confirm the direction, the window closes entirely. The EA resets to "ANTI_IDLE" rather than waiting for a subsequent cross. This enforces the "first cross only" rule—the first reversal cross after a valid pullback is the only qualifying entry signal.
  • Swing stop tracks the deepest pullback bar: during "ANTI_WAITING_REVERSAL," if the fast line continues below the signal line (pullback extends), the swing stop updates to track the most extreme bar reached. The stop is placed at the lowest bar during the entire pullback, not just the first bar.

All state transitions are logged with the oscillator values and direction at the time of the transition.

//+------------------------------------------------------------------+
//| Processes the Anti setup state machine for one bar               |
//+------------------------------------------------------------------+
void ProcessAnti(double fast_curr, double fast_prev,
                 double sig_curr,  double sig_prev,
                 double atr)
  {
   switch(g_anti_state)
     {
      //--- IDLE: watching for signal line to cross zero
      case ANTI_IDLE:
        {
         bool bull_zero = (sig_prev <= 0.0 && sig_curr > 0.0);
         bool bear_zero = (sig_prev >= 0.0 && sig_curr < 0.0);
         if(bull_zero || bear_zero)
           {
            g_anti_state      = ANTI_WAITING_PULLBACK;
            g_anti_is_long    = bull_zero;
            g_anti_swing_stop = 0.0;
            Print(StringFormat(
                     "RaschkeEA [ANTI]: IDLE ? WAITING_PULLBACK | %s | Fast:%.6f Sig:%.6f",
                     (g_anti_is_long ? "BULL" : "BEAR"), fast_curr, sig_curr));
           }
        }
      break;

      //--- WAITING_PULLBACK: zero cross confirmed — fast line not yet at signal
      case ANTI_WAITING_PULLBACK:
        {
         //--- Abort if signal reverses through zero (prior confirmation invalidated)
         if(g_anti_is_long && sig_curr < 0.0)
           {
            Print("RaschkeEA [ANTI]: WAITING_PULLBACK ? IDLE (signal reversed below zero)");
            g_anti_state = ANTI_IDLE;
            break;
           }
         if(!g_anti_is_long && sig_curr > 0.0)
           {
            Print("RaschkeEA [ANTI]: WAITING_PULLBACK ? IDLE (signal reversed above zero)");
            g_anti_state = ANTI_IDLE;
            break;
           }

         //--- Pullback: fast line touches or crosses the signal line
         bool bull_pb = (g_anti_is_long  && fast_curr <= sig_curr);
         bool bear_pb = (!g_anti_is_long && fast_curr >= sig_curr);
         if(bull_pb || bear_pb)
           {
            double low1  = iLow(_Symbol,  PERIOD_CURRENT, 1);
            double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1);
            g_anti_swing_stop = g_anti_is_long ? low1 : high1;
            g_anti_state      = ANTI_WAITING_REVERSAL;
            Print(StringFormat(
                     "RaschkeEA [ANTI]: WAITING_PULLBACK ? WAITING_REVERSAL | %s | Fast:%.6f Sig:%.6f | SwingStop:%.5f",
                     (g_anti_is_long ? "BULL" : "BEAR"), fast_curr, sig_curr, g_anti_swing_stop));
           }
        }
      break;

      //--- WAITING_REVERSAL: pullback confirmed — watching for fast to cross back through signal
      case ANTI_WAITING_REVERSAL:
        {
         //--- Abort if signal reverses through zero
         if(g_anti_is_long && sig_curr < 0.0)
           {
            Print("RaschkeEA [ANTI]: WAITING_REVERSAL ? IDLE (signal reversed below zero)");
            g_anti_state = ANTI_IDLE;
            break;
           }
         if(!g_anti_is_long && sig_curr > 0.0)
           {
            Print("RaschkeEA [ANTI]: WAITING_REVERSAL ? IDLE (signal reversed above zero)");
            g_anti_state = ANTI_IDLE;
            break;
           }

         //--- If the pullback extends further, track the deepest extreme for stop placement
         double low1  = iLow(_Symbol,  PERIOD_CURRENT, 1);
         double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1);
         if(g_anti_is_long  && fast_curr <= sig_curr && low1  < g_anti_swing_stop)
            g_anti_swing_stop = low1;
         if(!g_anti_is_long && fast_curr >= sig_curr && high1 > g_anti_swing_stop)
            g_anti_swing_stop = high1;

         //--- Reversal: fast line crosses back through signal in trend direction
         bool bull_rev = (g_anti_is_long  && fast_prev <= sig_prev && fast_curr > sig_curr);
         bool bear_rev = (!g_anti_is_long && fast_prev >= sig_prev && fast_curr < sig_curr);
         if(!bull_rev && !bear_rev)
            break;

         //--- Price confirmation: trigger bar closed in trade direction
         double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);
         double close2 = iClose(_Symbol, PERIOD_CURRENT, 2);
         bool price_ok = g_anti_is_long ? (close1 > close2) : (close1 < close2);
         if(!price_ok)
           {
            //--- First reversal cross without confirmation — window closes entirely
            Print("RaschkeEA [ANTI]: WAITING_REVERSAL ? IDLE | Reversal cross failed price confirmation — first cross only rule applied");
            g_anti_state = ANTI_IDLE;
            break;
           }

         if(g_ticket != 0)
            break; // Another setup already in trade

         if(OpenTrade(g_anti_is_long, g_anti_swing_stop, atr, "Anti"))
           {
            Print("RaschkeEA [ANTI]: WAITING_REVERSAL ? IN_TRADE");
            g_anti_state = ANTI_IN_TRADE;
            g_hg_state   = HG_IDLE; // Holy Grail blocked while Anti is in trade
           }
        }
      break;

      //--- IN_TRADE: watching for oscillator exit cross
      case ANTI_IN_TRADE:
        {
         if(!PositionSelectByTicket(g_ticket))
           {
            Print("RaschkeEA [ANTI]: IN_TRADE ? IDLE (position closed externally)");
            g_ticket     = 0;
            g_anti_state = ANTI_IDLE;
            break;
           }

         bool exit_long  = (g_anti_is_long  && fast_prev >= sig_prev && fast_curr < sig_curr);
         bool exit_short = (!g_anti_is_long && fast_prev <= sig_prev && fast_curr > sig_curr);
         if(exit_long || exit_short)
           {
            CloseTrade("Anti exit — oscillator cross");
            Print("RaschkeEA [ANTI]: IN_TRADE ? IDLE (oscillator cross exit)");
            g_anti_state = ANTI_IDLE;
           }
        }
      break;
     }
  }

Processing the Holy Grail Setup

"ProcessHolyGrail()" implements the three-state machine. Three design decisions are explicit here:

  • ADX rising for two consecutive bars: the code reads three ADX values and confirms "adx_curr > adx_prev > adx_prev2." A single bar of rising ADX does not qualify. This matches the stated rule precisely.
  • Reset uses InpADXThreshold exactly: the reset trigger is "adx_curr < InpADXThreshold," not "adx_curr < InpADXThreshold - 5." Using a gap below the threshold is an undisclosed modification of the rule; this version uses the threshold as stated.
  • First pullback enforced across trade boundaries: after any "Holy Grail" trade closes, "g_hg_needs_adx_reset" is set to "true." The EA will not enter a new "Holy Grail" trade until ADX has actually dropped below "InpADXThreshold" at least once. This means the EA genuinely waits for the ADX to reset before re-entering—it does not re-enter simply because the prior trade ended.

//+------------------------------------------------------------------+
//| Processes the Holy Grail state machine for one bar               |
//+------------------------------------------------------------------+
void ProcessHolyGrail(double atr)
  {
   double ema_buf[], adx_buf[];
   ArraySetAsSeries(ema_buf, true);
   ArraySetAsSeries(adx_buf, true);
   if(CopyBuffer(g_ema_handle, 0, 1, 3, ema_buf) < 3)
      return;
   if(CopyBuffer(g_adx_handle, 0, 1, 3, adx_buf) < 3)
      return;

   double ema_curr  = ema_buf[0];
   double ema_prev  = ema_buf[1];
   double adx_curr  = adx_buf[0];
   double adx_prev  = adx_buf[1];
   double adx_prev2 = adx_buf[2];

   bool ema_rising  = (ema_curr > ema_prev);
   bool ema_falling = (ema_curr < ema_prev);

//--- ADX must be at threshold AND rising for two consecutive bars
   bool adx_rising2 = (adx_curr > adx_prev && adx_prev > adx_prev2);
   bool adx_strong  = (adx_curr >= InpADXThreshold && adx_rising2);

   switch(g_hg_state)
     {
      //--- IDLE: waiting for strong rising ADX with EMA trending
      case HG_IDLE:
        {
         //--- If a prior trade requires ADX reset, wait for it
         if(g_hg_needs_adx_reset)
           {
            if(adx_curr < InpADXThreshold)
              {
               g_hg_needs_adx_reset = false;
               Print(StringFormat(
                        "RaschkeEA [HG]: ADX dropped below %.1f — first pullback flag cleared",
                        InpADXThreshold));
              }
            break; // Do not look for new setups until reset is confirmed
           }

         if(adx_strong && (ema_rising || ema_falling))
           {
            g_hg_is_long = ema_rising;
            g_hg_state   = HG_WAITING_PULLBACK;
            Print(StringFormat(
                     "RaschkeEA [HG]: IDLE ? WAITING_PULLBACK | ADX:%.1f (rising 2 bars) | Dir:%s | EMA:%.5f",
                     adx_curr, (g_hg_is_long ? "LONG" : "SHORT"), ema_curr));
           }
        }
      break;

      //--- WAITING_PULLBACK: ADX confirmed — watching for price to touch 20 EMA
      case HG_WAITING_PULLBACK:
        {
         //--- Reset if ADX drops below threshold (trend weakening)
         if(adx_curr < InpADXThreshold)
           {
            Print(StringFormat(
                     "RaschkeEA [HG]: WAITING_PULLBACK ? IDLE (ADX %.1f below threshold %.1f)",
                     adx_curr, InpADXThreshold));
            g_hg_state = HG_IDLE;
            break;
           }

         //--- Reset if EMA changes direction (trend premise invalidated)
         if(g_hg_is_long && ema_falling)
           {
            Print("RaschkeEA [HG]: WAITING_PULLBACK ? IDLE (EMA direction changed to falling)");
            g_hg_state = HG_IDLE;
            break;
           }
         if(!g_hg_is_long && ema_rising)
           {
            Print("RaschkeEA [HG]: WAITING_PULLBACK ? IDLE (EMA direction changed to rising)");
            g_hg_state = HG_IDLE;
            break;
           }

         double bar1_low  = iLow(_Symbol,  PERIOD_CURRENT, 1);
         double bar1_high = iHigh(_Symbol, PERIOD_CURRENT, 1);
         bool long_touch  = (g_hg_is_long  && bar1_low  <= ema_curr);
         bool short_touch = (!g_hg_is_long && bar1_high >= ema_curr);

         if((long_touch || short_touch) && g_ticket == 0)
           {
            g_hg_pb_extreme = g_hg_is_long ? bar1_low : bar1_high;
            if(OpenTrade(g_hg_is_long, g_hg_pb_extreme, atr, "HolyGrail"))
              {
               g_hg_state           = HG_IN_TRADE;
               g_hg_needs_adx_reset = true; // Require ADX to drop before next entry
               g_anti_state         = ANTI_IDLE;
               Print("RaschkeEA [HG]: WAITING_PULLBACK ? IN_TRADE");
              }
           }
        }
      break;

      //--- IN_TRADE: watching for EMA cross exit or ADX drop
      case HG_IN_TRADE:
        {
         if(!PositionSelectByTicket(g_ticket))
           {
            Print("RaschkeEA [HG]: IN_TRADE ? IDLE (position closed externally)");
            g_ticket   = 0;
            g_hg_state = HG_IDLE;
            break;
           }

         //--- Exit 1: ADX drops below 20 — trend is considered ended
         if(adx_curr < 20.0)
           {
            CloseTrade("HG exit — ADX below 20");
            Print("RaschkeEA [HG]: IN_TRADE ? IDLE (ADX below 20)");
            g_hg_state = HG_IDLE;
            break;
           }

         //--- Exit 2: closing price crosses the 20 EMA in the wrong direction
         double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);
         bool ema_exit = (g_hg_is_long  && close1 < ema_curr) ||
                         (!g_hg_is_long && close1 > ema_curr);
         if(ema_exit)
           {
            CloseTrade("HG exit — EMA cross");
            Print("RaschkeEA [HG]: IN_TRADE ? IDLE (EMA cross exit)");
            g_hg_state = HG_IDLE;
           }
        }
      break;
     }
  }

OnInit, OnDeinit, and OnTick

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_ema_handle = iMA(_Symbol, PERIOD_CURRENT, InpEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   g_adx_handle = iADX(_Symbol, PERIOD_CURRENT, InpADXPeriod);
   g_atr_handle = iATR(_Symbol, PERIOD_CURRENT, InpATRPeriod);

   if(g_ema_handle == INVALID_HANDLE ||
      g_adx_handle == INVALID_HANDLE ||
      g_atr_handle == INVALID_HANDLE)
     {
      Print("RaschkeEA: Indicator handle creation failed.");
      return INIT_FAILED;
     }

   g_trade.SetExpertMagicNumber(InpMagicNumber);
   g_trade.SetDeviationInPoints(InpSlippage);

   g_anti_state         = ANTI_IDLE;
   g_hg_state           = HG_IDLE;
   g_ticket             = 0;
   g_last_bar           = 0;
   g_anti_swing_stop    = 0;
   g_anti_is_long       = true;
   g_hg_is_long         = true;
   g_hg_needs_adx_reset = false;
   g_hg_pb_extreme      = 0;
   g_stop_price         = 0;

   RecoverState(); // Restore state from any open positions

   Print(StringFormat(
            "RaschkeEA initialized | Symbol:%s | TF:%s | Magic:%d | Priority:%s first",
            _Symbol, EnumToString(Period()), InpMagicNumber,
            (InpAntiFirst ? "Anti" : "HolyGrail")));
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(g_ema_handle);
   IndicatorRelease(g_adx_handle);
   IndicatorRelease(g_atr_handle);
   ClearLabels();
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(current_bar == g_last_bar)
      return;
   g_last_bar = current_bar;

//--- Compute oscillator once per bar
   double fast_curr = 0, fast_prev = 0, sig_curr = 0, sig_prev = 0;
   if(!Compute310(fast_curr, fast_prev, sig_curr, sig_prev))
      return;

//--- ATR for stop calculation
   double atr_buf[];
   ArraySetAsSeries(atr_buf, true);
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
      return;
   double atr = atr_buf[0];

//--- Check for externally closed position
   if(g_ticket != 0 && !PositionSelectByTicket(g_ticket))
     {
      Print("RaschkeEA: Position closed externally | Ticket:", g_ticket);
      g_ticket     = 0;
      g_stop_price = 0;
      g_anti_state = ANTI_IDLE;
      g_hg_state   = HG_IDLE;
      ClearLabels();
     }

//--- Run setups in configured priority order
//--- Note: whichever setup is processed first has priority when both signal simultaneously.
//--- This is configurable via InpAntiFirst and disclosed in the initialization log.
   if(InpAntiFirst)
     {
      if(InpAntiEnabled && (g_ticket == 0 || g_anti_state == ANTI_IN_TRADE))
         ProcessAnti(fast_curr, fast_prev, sig_curr, sig_prev, atr);
      if(InpHGEnabled   && (g_ticket == 0 || g_hg_state   == HG_IN_TRADE))
         ProcessHolyGrail(atr);
     }
   else
     {
      if(InpHGEnabled   && (g_ticket == 0 || g_hg_state   == HG_IN_TRADE))
         ProcessHolyGrail(atr);
      if(InpAntiEnabled && (g_ticket == 0 || g_anti_state == ANTI_IN_TRADE))
         ProcessAnti(fast_curr, fast_prev, sig_curr, sig_prev, atr);
     }
  }
//+------------------------------------------------------------------+

What to Expect

The "Anti" generates more signals than the "Holy Grail" across any given period. Zero line crosses on the 3/10 oscillator occur multiple times per month on H4, and most produce an "Anti" setup. The "Holy Grail" requires ADX above 30 with two bars of confirmed rising momentum simultaneously with a clean EMA trend—a less frequent condition.

Both setups accept that missing the initial move is the cost of getting the entry right. The "Anti" does not chase a zero cross; it waits for the pull and the confirmed reversal. The "Holy Grail" does not enter on a strong trend signal; it waits for price to return to the EMA before entering.

On average, winners should exceed losers. Losers are stopped at the pullback extreme, while winners ride the trend until an oscillator/EMA exit signal appears. Whether the average trade statistics confirm this on a specific instrument and period is a testing question, not a theoretical guarantee.

The journal will log every state transition for both setups, making it straightforward to follow exactly how each trade was generated and why each exit fired.


Known Limitations and Design Decisions

Several points where this implementation departs from Raschke's original description, or where the implementation has inherent constraints, are stated explicitly here.

Entry is on the first tick of the next bar, not at the close of the trigger bar. Raschke’s rules specify entry at the close of the trigger bar, or alternatively, a buy stop one tick above the bar’s high. This EA processes signals on the first tick of a new bar and sends a market order at the then-current Ask or Bid, so the fill price may differ slightly from the bar’s open price. In practice on H4, the difference is about one bar of lag.

The "Holy Grail" ADX rising check uses two bars. The code confirms "adx_curr > adx_prev > adx_prev2." This is one interpretation of "rising for at least two bars"—it requires two consecutive one-bar rises. An alternative would be a regression slope over several bars, which would be less sensitive to single-bar ADX spikes. The two-bar consecutive rise is documented here so the reader can adjust if their testing reveals premature trigger entries.

The "Anti" pullback requires the fast line to touch or cross the signal line. Raschke's wording—"toward or below"—allows for a pullback that approaches but does not reach the signal line. This EA requires an actual touch or cross ("fast_curr <= sig_curr" for a bull Anti). Approaches that stop short are not detected. This is a conservative choice that reduces signal frequency and is noted here for clarity.

A failed price confirmation resets the Anti to IDLE. The "first cross only" rule is enforced strictly: if the fast line produces a reversal cross but the trigger bar's close does not confirm direction, the opportunity is abandoned entirely. The EA does not wait for a subsequent reversal cross. This is stricter than some interpretations of Raschke's description.

The EMA direction for the Holy Grail uses one bar of slope. One bar of EMA slope (current vs. previous) is a noisy signal. In choppy markets where the EMA oscillates slightly from bar to bar, the direction signal may flip repeatedly. The ADX threshold provides the primary trend quality filter; the EMA slope confirms the trade direction within that trend.

Risk is calculated from the final, adjusted stop distance. In "OpenTrade()," the stop is first moved to satisfy the broker’s minimum stop distance if necessary, and only then is the distance to the entry price passed to "CalcLots()." The configured risk percentage is therefore applied to the actual stop placed with the order, so the minimum stop distance does not cause risk to exceed the configured value. Small deviations can still arise from lot rounding to the broker’s volume step, from the minimum lot size (if the calculated volume is below it, the minimum is used and risk may be higher), and from slippage between the Ask/Bid used for the calculation and the actual fill price.

Position tracking via "ResultOrder()" as a fallback. The primary method for obtaining a position ticket is "DEAL_POSITION_ID" from the completed deal. The fallback ("ResultOrder()") may not match the position ticket in all broker configurations. State recovery via "RecoverState()" provides a secondary mechanism for the EA to find its position after a restart even if the ticket reference was lost.

Signal frequency is an estimate. The range of 20–40 "Anti" signals and 10–20 "Holy Grail" signals per year on EURUSD H4 is an approximate estimate based on observation of the oscillator's behavior, not a formally verified backtest claim. Actual signal frequency depends on market regime, the specific period tested, and spread assumptions.

The two state machines are architecturally independent but share a position ticket. Each setup has its own state, its own direction variable, and its own swing stop. They share "g_ticket" because only one position is open at a time—this is a shared execution constraint, not a shared state. The setups do not affect each other's internal state except through the "g_ticket == 0" gate that prevents simultaneous entries.


Conclusion

The 3/10 oscillator identifies momentum cycles with a lag substantially shorter than the standard MACD. The "Anti" enters whereby a new cycle has been confirmed, the slingshot has been loaded by the pullback, and price has confirmed the resumption. The "Holy Grail" enters whereby a powerful, strengthening trend offers its lowest-risk pullback opportunity.

The automation challenge here is not just implementing the rules—it is implementing them accurately and being precise about where the implementation departs from the original description. The version of this EA presented in this article makes those departures explicit: entry lag of one bar, conservative pullback detection, strict first-cross enforcement, and an honest account of where the risk calculation has minor imprecision.

Both setups can be run independently or together. The "InpAntiFirst" parameter controls which setup is given priority when both signals are sent simultaneously. Each state machine logs every transition, making it straightforward to trace exactly how each trade was generated, why each exit fired, and what state the machine was in when any unexpected behavior occurred.

All code was compiled and tested in MetaTrader 5. Copy "RaschkeEA.mq5" to "MQL5\Experts\" and compile in MetaEditor with no additional dependencies. Recommended for EURUSD on H4. Always test on a demo account before live deployment.

Attached files |
RaschkeEA.mq5 (27.19 KB)
Offline Reinforcement Learning in MQL5: Conservative Q-Learning from Historical Trade Logs Without Live Exploration Offline Reinforcement Learning in MQL5: Conservative Q-Learning from Historical Trade Logs Without Live Exploration
Build and deploy a conservative, offline‑trained policy entirely in MQL5. Logged trades become a fixed dataset for a two‑layer Q‑network trained with Conservative Q‑Learning, which pushes down Q‑values for poorly supported actions to limit extrapolation error. The EA loads the frozen weights and applies an out‑of‑distribution guard using saved feature statistics. This yields reproducible training and execution without live exploration.
The Hilbert Spectrum in MQL5 — Live Time-Frequency-Energy Visualization of Market Cycles The Hilbert Spectrum in MQL5 — Live Time-Frequency-Energy Visualization of Market Cycles
The article guides you through building a real‑time Hilbert Spectrum indicator in MQL5: EMD produces IMFs, a discrete FIR Hilbert transform yields analytical signals, and a CCanvas renderer draws a time–frequency–energy heatmap. Implementation details include extrema detection, SD stopping, phase unwrapping, Nyquist clipping, and bin accumulation. Use the panel to identify active cycle bands and regime shifts while keeping trade rules separate.
Building a Prop Firm Compliance Monitor in MQL5 (Part 3): Daily Drawdown, Overall Drawdown, and Profit Targets Building a Prop Firm Compliance Monitor in MQL5 (Part 3): Daily Drawdown, Overall Drawdown, and Profit Targets
This part turns stored account references into a financial monitoring engine for MetaTrader 5. It calculates daily drawdown from the confirmed session balance, overall drawdown from the initial challenge balance with a static equity floor, and profit‑target progress from realized balance. The EA derives usage, remaining allowance, and status for each rule, then aggregates them into a single guard state where risk conditions override target completion.
Online Portfolio Selection: A Provable Regret Bound, and the Cost That Eats It Online Portfolio Selection: A Provable Regret Bound, and the Cost That Eats It
The article implements Online Portfolio Selection in MQL5: exact simplex projection, four allocators (EG, Universal Portfolio, PAMR, OLMAR), and a BCRP solver on a concave objective. We verify weight paths against the published Python reference and self-check the optimizer. A drift-aware transaction cost model measures turnover and net wealth on a multi-asset basket. Readers get a tested library and evidence on when regret-bounded methods lose to costs.