Automating Classic Market Methods in MQL5 (Part 9): Linda Bradford Raschke's 3/10 Oscillator System
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.

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:
- The 3/10 Oscillator—Construction and Interpretation
- The Anti Setup—Theory and Rules
- The Holy Grail Setup—Theory and Rules
- Architecture—Two State Machines, One EA
- Implementation in MQL5
- Known Limitations and Design Decisions
- 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.
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.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Offline Reinforcement Learning in MQL5: Conservative Q-Learning from Historical Trade Logs Without Live Exploration
The Hilbert Spectrum in MQL5 — Live Time-Frequency-Energy Visualization of Market Cycles
Building a Prop Firm Compliance Monitor in MQL5 (Part 3): Daily Drawdown, Overall Drawdown, and Profit Targets
Online Portfolio Selection: A Provable Regret Bound, and the Cost That Eats It
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use