lwOopsPatternExpert/lwOopsPatternExpert.mq5

1393 lines
44 KiB
MQL5
Raw Permalink Normal View History

2026-07-19 06:06:20 -07:00
//+------------------------------------------------------------------+
//| lwOopsPatternExpert.mq5 |
//| Copyright 2026, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd. Developer: Chacha Ian"
#property link "https://www.mql5.com/en/users/chachaian"
#property version "1.00"
#property description "Detects and trades Larry Williams' Oops gap reversal pattern."
#property description "The EA tracks qualifying gaps, confirms closed-bar reversals,"
#property description "and calculates the stop loss, take profit, and position size."
//+------------------------------------------------------------------+
//| Standard Libraries |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| Custom Enumerations |
//+------------------------------------------------------------------+
enum ENUM_OOPS_TRADE_DIRECTION
{
OOPS_TRADE_LONG_ONLY,
OOPS_TRADE_SHORT_ONLY,
OOPS_TRADE_BOTH
};
enum ENUM_LOT_SIZE_INPUT_MODE
{
MODE_MANUAL,
MODE_AUTO
};
//+------------------------------------------------------------------+
//| User Input Variables |
//+------------------------------------------------------------------+
input group "Information"
input ulong magicNumber = 254700680002;
input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT;
input group "Oops Pattern Configurations"
input double minimumGapSizePoints = 500;
input int maxGapValidityBars = 3;
input group "Trade and Risk Management"
input ENUM_OOPS_TRADE_DIRECTION tradeDirection = OOPS_TRADE_BOTH;
input double riskRewardRatio = 2.5;
input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode = MODE_AUTO;
input double riskPerTradePercent = 1.0;
input double positionSize = 0.1;
//+------------------------------------------------------------------+
//| Oops Pattern State |
//+------------------------------------------------------------------+
//| Stores the detected gap, its lifecycle, and prepared trade data. |
//+------------------------------------------------------------------+
struct OopsPatternState
{
bool gapDetected;
bool isGapUp;
bool isGapDown;
datetime gapBarTime;
double gapOpenPrice;
double gapBarHigh;
double gapBarLow;
double previousHigh;
double previousLow;
int barsSinceGap;
int maxBarsToFill;
bool gapFilled;
bool gapInvalidated;
double bullishTakeProfit;
double bearishTakeProfit;
double lotSize;
ENUM_ORDER_TYPE orderType;
double positionEntryPrice;
};
//+------------------------------------------------------------------+
//| Shared Program State |
//+------------------------------------------------------------------+
OopsPatternState oopsState; // Active Oops setup tracked across bars
CTrade Trade; // Submits orders and exposes execution results
double askPrice; // Latest verified price used for buy execution
double bidPrice; // Latest verified price used for sell execution
datetime currentTime; // Latest terminal time received by the EA
datetime lastBarOpenTime; // Opening time of the last processed bar
//+------------------------------------------------------------------+
//| Reads a double-valued symbol property safely |
//+------------------------------------------------------------------+
bool GetSymbolDoubleValue(string symbol,
ENUM_SYMBOL_INFO_DOUBLE property,
double &value,
string context)
{
//--- Clear any earlier runtime error and initialize the output
ResetLastError();
value = 0.0;
//--- Request the selected symbol property
if(!SymbolInfoDouble(symbol, property, value))
{
int errorCode = GetLastError();
PrintFormat("%s: Failed to read symbol property %s for %s. Error %d.",
context,
EnumToString(property),
symbol,
errorCode);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Reads the opening time of a selected bar safely |
//+------------------------------------------------------------------+
bool GetBarTime(string symbol,
ENUM_TIMEFRAMES tf,
int shift,
datetime &value,
string context)
{
//--- Request the opening time of the selected bar
ResetLastError();
value = iTime(symbol, tf, shift);
//--- A zero value indicates that the bar data is unavailable
if(value == 0)
{
int errorCode = GetLastError();
PrintFormat("%s: Failed to read bar time for %s, timeframe %s, "
"shift %d. Error %d.",
context,
symbol,
EnumToString(tf),
shift,
errorCode);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Reads the opening price of a selected bar safely |
//+------------------------------------------------------------------+
bool GetBarOpen(string symbol,
ENUM_TIMEFRAMES tf,
int shift,
double &value,
string context)
{
//--- Request the opening price of the selected bar
ResetLastError();
value = iOpen(symbol, tf, shift);
//--- Reject unavailable or invalid price data
if(value == 0.0)
{
int errorCode = GetLastError();
PrintFormat("%s: Failed to read bar open for %s, timeframe %s, "
"shift %d. Error %d.",
context,
symbol,
EnumToString(tf),
shift,
errorCode);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Reads the highest price of a selected bar safely |
//+------------------------------------------------------------------+
bool GetBarHigh(string symbol,
ENUM_TIMEFRAMES tf,
int shift,
double &value,
string context)
{
//--- Request the highest price of the selected bar
ResetLastError();
value = iHigh(symbol, tf, shift);
//--- Reject unavailable or invalid price data
if(value == 0.0)
{
int errorCode = GetLastError();
PrintFormat("%s: Failed to read bar high for %s, timeframe %s, "
"shift %d. Error %d.",
context,
symbol,
EnumToString(tf),
shift,
errorCode);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Reads the lowest price of a selected bar safely |
//+------------------------------------------------------------------+
bool GetBarLow(string symbol,
ENUM_TIMEFRAMES tf,
int shift,
double &value,
string context)
{
//--- Request the lowest price of the selected bar
ResetLastError();
value = iLow(symbol, tf, shift);
//--- Reject unavailable or invalid price data
if(value == 0.0)
{
int errorCode = GetLastError();
PrintFormat("%s: Failed to read bar low for %s, timeframe %s, "
"shift %d. Error %d.",
context,
symbol,
EnumToString(tf),
shift,
errorCode);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Reads the closing price of a selected bar safely |
//+------------------------------------------------------------------+
bool GetBarClose(string symbol,
ENUM_TIMEFRAMES tf,
int shift,
double &value,
string context)
{
//--- Request the closing price of the selected bar
ResetLastError();
value = iClose(symbol, tf, shift);
//--- Reject unavailable or invalid price data
if(value == 0.0)
{
int errorCode = GetLastError();
PrintFormat("%s: Failed to read bar close for %s, timeframe %s, "
"shift %d. Error %d.",
context,
symbol,
EnumToString(tf),
shift,
errorCode);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Resets the stored Oops setup to a neutral state |
//+------------------------------------------------------------------+
void ResetOopsPatternState()
{
//--- Clear the identity of the previous setup
oopsState.gapDetected = false;
oopsState.isGapUp = false;
oopsState.isGapDown = false;
oopsState.gapBarTime = 0;
oopsState.gapOpenPrice = 0.0;
//--- Clear the stored reference prices
oopsState.gapBarHigh = 0.0;
oopsState.gapBarLow = 0.0;
oopsState.previousHigh = 0.0;
oopsState.previousLow = 0.0;
//--- Restore the setup lifecycle defaults
oopsState.barsSinceGap = 0;
oopsState.maxBarsToFill = maxGapValidityBars;
oopsState.gapFilled = false;
oopsState.gapInvalidated = false;
//--- Clear prepared trade values and restore input-based defaults
oopsState.bullishTakeProfit = 0.0;
oopsState.bearishTakeProfit = 0.0;
oopsState.lotSize = positionSize;
oopsState.orderType = ORDER_TYPE_BUY;
oopsState.positionEntryPrice = 0.0;
//--- Initialize the stored entry with a verified market price
if(!GetSymbolDoubleValue(_Symbol,
SYMBOL_ASK,
oopsState.positionEntryPrice,
"ResetOopsPatternState"))
{
//--- Keep a neutral value when the symbol price is unavailable
oopsState.positionEntryPrice = 0.0;
}
}
//+------------------------------------------------------------------+
//| Configures the chart for clear visual testing |
//+------------------------------------------------------------------+
bool ConfigureChartAppearance()
{
//--- Apply a white background
ResetLastError();
if(!ChartSetInteger(0, CHART_COLOR_BACKGROUND, clrWhite))
{
Print("ConfigureChartAppearance: Failed to set chart background. Error ",
GetLastError(), ".");
return false;
}
//--- Remove the grid to reduce visual clutter
ResetLastError();
if(!ChartSetInteger(0, CHART_SHOW_GRID, false))
{
Print("ConfigureChartAppearance: Failed to hide the chart grid. Error ",
GetLastError(), ".");
return false;
}
//--- Display prices as candlesticks
ResetLastError();
if(!ChartSetInteger(0, CHART_MODE, CHART_CANDLES))
{
Print("ConfigureChartAppearance: Failed to set candle chart mode. Error ",
GetLastError(), ".");
return false;
}
//--- Use black for chart labels and price-scale text
ResetLastError();
if(!ChartSetInteger(0, CHART_COLOR_FOREGROUND, clrBlack))
{
Print("ConfigureChartAppearance: Failed to set chart foreground. Error ",
GetLastError(), ".");
return false;
}
//--- Keep both candle bodies white
ResetLastError();
if(!ChartSetInteger(0, CHART_COLOR_CANDLE_BULL, clrWhite))
{
Print("ConfigureChartAppearance: Failed to set the bullish candle color. Error ",
GetLastError(), ".");
return false;
}
ResetLastError();
if(!ChartSetInteger(0, CHART_COLOR_CANDLE_BEAR, clrWhite))
{
Print("ConfigureChartAppearance: Failed to set the bearish candle color. Error ",
GetLastError(), ".");
return false;
}
//--- Distinguish bullish and bearish candle outlines
ResetLastError();
if(!ChartSetInteger(0, CHART_COLOR_CHART_UP, clrSeaGreen))
{
Print("ConfigureChartAppearance: Failed to set the chart-up color. Error ",
GetLastError(), ".");
return false;
}
ResetLastError();
if(!ChartSetInteger(0, CHART_COLOR_CHART_DOWN, clrBlack))
{
Print("ConfigureChartAppearance: Failed to set the chart-down color. Error ",
GetLastError(), ".");
return false;
}
//--- Apply the queued chart-property changes
ResetLastError();
ChartRedraw(0);
int redrawError = GetLastError();
if(redrawError != 0)
{
Print("ConfigureChartAppearance: ChartRedraw reported error ",
redrawError, ".");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Returns true once when a new bar opens on the selected timeframe |
//+------------------------------------------------------------------+
bool IsNewBar(string symbol,
ENUM_TIMEFRAMES tf,
datetime &lastTm)
{
datetime currentTm = 0;
//--- Stop when the opening time of bar zero is unavailable
if(!GetBarTime(symbol,
tf,
0,
currentTm,
"IsNewBar"))
{
return false;
}
//--- Matching timestamps indicate that this bar was already processed
if(currentTm == lastTm)
return false;
//--- Store the verified timestamp before allowing strategy processing
lastTm = currentTm;
return true;
}
//+------------------------------------------------------------------+
//| Returns true when the current bar opens far enough below the |
//| previous bar's low to qualify as a gap-down setup |
//+------------------------------------------------------------------+
bool IsGapDown()
{
double currentOpen = 0.0;
double previousLow = 0.0;
//--- A gap cannot be evaluated without both reference prices
if(!GetBarOpen(_Symbol,
timeframe,
0,
currentOpen,
"IsGapDown"))
{
return false;
}
if(!GetBarLow(_Symbol,
timeframe,
1,
previousLow,
"IsGapDown"))
{
return false;
}
//--- Measure the distance from the current open to the previous low
double gapSize = previousLow - currentOpen;
//--- Convert the configured point threshold into a price distance
return(gapSize >= minimumGapSizePoints * _Point);
}
//+------------------------------------------------------------------+
//| Returns true when the current bar opens far enough above the |
//| previous bar's high to qualify as a gap-up setup |
//+------------------------------------------------------------------+
bool IsGapUp()
{
double currentOpen = 0.0;
double previousHigh = 0.0;
//--- A gap cannot be evaluated without both reference prices
if(!GetBarOpen(_Symbol,
timeframe,
0,
currentOpen,
"IsGapUp"))
{
return false;
}
if(!GetBarHigh(_Symbol,
timeframe,
1,
previousHigh,
"IsGapUp"))
{
return false;
}
//--- Measure the distance from the previous high to the current open
double gapSize = currentOpen - previousHigh;
//--- Convert the configured point threshold into a price distance
return(gapSize >= minimumGapSizePoints * _Point);
}
//+------------------------------------------------------------------+
//| Detects a qualifying gap and stores one complete Oops setup |
//+------------------------------------------------------------------+
void DetectAndInitializeOopsGap()
{
//--- Preserve the current setup until it confirms or expires
if(oopsState.gapDetected)
return;
//--- A gap up prepares a possible bearish reversal
if(IsGapUp())
{
datetime gapBarTime = 0;
double gapOpenPrice = 0.0;
double previousHigh = 0.0;
double previousLow = 0.0;
//--- Collect every required value before changing shared state
if(!GetBarTime(_Symbol,
timeframe,
0,
gapBarTime,
"DetectAndInitializeOopsGap"))
{
return;
}
if(!GetBarOpen(_Symbol,
timeframe,
0,
gapOpenPrice,
"DetectAndInitializeOopsGap"))
{
return;
}
if(!GetBarHigh(_Symbol,
timeframe,
1,
previousHigh,
"DetectAndInitializeOopsGap"))
{
return;
}
if(!GetBarLow(_Symbol,
timeframe,
1,
previousLow,
"DetectAndInitializeOopsGap"))
{
return;
}
//--- Commit the bearish setup only after every data read succeeds
oopsState.gapDetected = true;
oopsState.isGapUp = true;
oopsState.isGapDown = false;
oopsState.gapBarTime = gapBarTime;
oopsState.gapOpenPrice = gapOpenPrice;
oopsState.previousHigh = previousHigh;
oopsState.previousLow = previousLow;
oopsState.barsSinceGap = 0;
oopsState.maxBarsToFill = maxGapValidityBars;
oopsState.gapFilled = false;
oopsState.gapInvalidated = false;
oopsState.orderType = ORDER_TYPE_SELL;
return;
}
//--- A gap down prepares a possible bullish reversal
if(IsGapDown())
{
datetime gapBarTime = 0;
double gapOpenPrice = 0.0;
double previousHigh = 0.0;
double previousLow = 0.0;
//--- Collect every required value before changing shared state
if(!GetBarTime(_Symbol,
timeframe,
0,
gapBarTime,
"DetectAndInitializeOopsGap"))
{
return;
}
if(!GetBarOpen(_Symbol,
timeframe,
0,
gapOpenPrice,
"DetectAndInitializeOopsGap"))
{
return;
}
if(!GetBarHigh(_Symbol,
timeframe,
1,
previousHigh,
"DetectAndInitializeOopsGap"))
{
return;
}
if(!GetBarLow(_Symbol,
timeframe,
1,
previousLow,
"DetectAndInitializeOopsGap"))
{
return;
}
//--- Commit the bullish setup only after every data read succeeds
oopsState.gapDetected = true;
oopsState.isGapUp = false;
oopsState.isGapDown = true;
oopsState.gapBarTime = gapBarTime;
oopsState.gapOpenPrice = gapOpenPrice;
oopsState.previousHigh = previousHigh;
oopsState.previousLow = previousLow;
oopsState.barsSinceGap = 0;
oopsState.maxBarsToFill = maxGapValidityBars;
oopsState.gapFilled = false;
oopsState.gapInvalidated = false;
oopsState.orderType = ORDER_TYPE_BUY;
}
}
//+------------------------------------------------------------------+
//| Updates the age of the active Oops setup and removes it after |
//| the configured confirmation window expires |
//+------------------------------------------------------------------+
void UpdateOopsGapState()
{
//--- There is no lifecycle to update without an active setup
if(!oopsState.gapDetected)
return;
datetime currentBarTime = 0;
//--- Preserve the current state when bar timing cannot be verified
if(!GetBarTime(_Symbol,
timeframe,
0,
currentBarTime,
"UpdateOopsGapState"))
{
return;
}
//--- The gap bar starts the setup but is not an elapsed fill bar
if(currentBarTime == oopsState.gapBarTime)
return;
//--- Count the newly opened bar once within the new-bar workflow
oopsState.barsSinceGap++;
//--- Remove the setup after its allowed validity window is exceeded
if(oopsState.barsSinceGap > oopsState.maxBarsToFill)
{
oopsState.gapInvalidated = true;
ResetOopsPatternState();
}
}
//+------------------------------------------------------------------+
//| Returns true when an active gap-down setup confirms a bullish |
//| reversal through the close of a later completed bar |
//+------------------------------------------------------------------+
bool IsBullishSignal()
{
//--- Accept only an active, unprocessed gap-down setup
if(!oopsState.gapDetected ||
!oopsState.isGapDown ||
oopsState.gapFilled ||
oopsState.gapInvalidated)
{
return false;
}
//--- Require at least one completed bar after the original gap bar
if(oopsState.barsSinceGap < 1)
return false;
double closePrice = 0.0;
//--- Bar one is the most recently completed candle
if(!GetBarClose(_Symbol,
timeframe,
1,
closePrice,
"IsBullishSignal"))
{
return false;
}
//--- Confirm only after price closes back at or above the previous low
if(closePrice < oopsState.previousLow)
return false;
//--- Mark the setup as filled before returning the signal
oopsState.gapFilled = true;
return true;
}
//+------------------------------------------------------------------+
//| Returns true when an active gap-up setup confirms a bearish |
//| reversal through the close of a later completed bar |
//+------------------------------------------------------------------+
bool IsBearishSignal()
{
//--- Accept only an active, unprocessed gap-up setup
if(!oopsState.gapDetected ||
!oopsState.isGapUp ||
oopsState.gapFilled ||
oopsState.gapInvalidated)
{
return false;
}
//--- Require at least one completed bar after the original gap bar
if(oopsState.barsSinceGap < 1)
return false;
double closePrice = 0.0;
//--- Bar one is the most recently completed candle
if(!GetBarClose(_Symbol,
timeframe,
1,
closePrice,
"IsBearishSignal"))
{
return false;
}
//--- Confirm only after price closes back at or below the previous high
if(closePrice > oopsState.previousHigh)
return false;
//--- Mark the setup as filled before returning the signal
oopsState.gapFilled = true;
return true;
}
//+------------------------------------------------------------------+
//| Stores the gap-bar low as the stop reference for a buy setup |
//+------------------------------------------------------------------+
void UpdateBullishGapBarStopLevel()
{
//--- Locate the original gap bar using its stored opening time
ResetLastError();
int gapIndex = iBarShift(_Symbol,
timeframe,
oopsState.gapBarTime);
if(gapIndex == -1)
{
Print("UpdateBullishGapBarStopLevel: Failed to locate the gap bar. Error ",
GetLastError(), ".");
return;
}
double gapBarLow = 0.0;
//--- Read the low of the recovered gap bar
if(!GetBarLow(_Symbol,
timeframe,
gapIndex,
gapBarLow,
"UpdateBullishGapBarStopLevel"))
{
return;
}
//--- Store the structural stop reference for the bullish setup
oopsState.gapBarLow = gapBarLow;
}
//+------------------------------------------------------------------+
//| Stores the gap-bar high as the stop reference for a sell setup |
//+------------------------------------------------------------------+
void UpdateBearishGapBarStopLevel()
{
//--- Locate the original gap bar using its stored opening time
ResetLastError();
int gapIndex = iBarShift(_Symbol,
timeframe,
oopsState.gapBarTime);
if(gapIndex == -1)
{
Print("UpdateBearishGapBarStopLevel: Failed to locate the gap bar. Error ",
GetLastError(), ".");
return;
}
double gapBarHigh = 0.0;
//--- Read the high of the recovered gap bar
if(!GetBarHigh(_Symbol,
timeframe,
gapIndex,
gapBarHigh,
"UpdateBearishGapBarStopLevel"))
{
return;
}
//--- Store the structural stop reference for the bearish setup
oopsState.gapBarHigh = gapBarHigh;
}
//+------------------------------------------------------------------+
//| Calculates the take-profit level for a confirmed buy setup |
//+------------------------------------------------------------------+
void UpdateBullishTakeProfit(double entryPrice)
{
double stopLoss = oopsState.gapBarLow;
double riskDistance = entryPrice - stopLoss;
//--- Reject a stop placed at or above the intended buy entry
if(riskDistance <= 0.0)
{
PrintFormat("UpdateBullishTakeProfit: Invalid prices. Entry %.*f, "
"stop loss %.*f.",
_Digits,
entryPrice,
_Digits,
stopLoss);
return;
}
//--- Project the target above the entry by the configured risk multiple
double projectedTP = entryPrice +
(riskDistance * riskRewardRatio);
//--- Store a price normalized to the symbol's number of digits
oopsState.bullishTakeProfit = NormalizeDouble(projectedTP,
_Digits);
}
//+------------------------------------------------------------------+
//| Calculates the take-profit level for a confirmed sell setup |
//+------------------------------------------------------------------+
void UpdateBearishTakeProfit(double entryPrice)
{
double stopLoss = oopsState.gapBarHigh;
double riskDistance = stopLoss - entryPrice;
//--- Reject a stop placed at or below the intended sell entry
if(riskDistance <= 0.0)
{
PrintFormat("UpdateBearishTakeProfit: Invalid prices. Entry %.*f, "
"stop loss %.*f.",
_Digits,
entryPrice,
_Digits,
stopLoss);
return;
}
//--- Project the target below the entry by the configured risk multiple
double projectedTP = entryPrice -
(riskDistance * riskRewardRatio);
//--- Store a price normalized to the symbol's number of digits
oopsState.bearishTakeProfit = NormalizeDouble(projectedTP,
_Digits);
}
//+------------------------------------------------------------------+
//| Calculates a broker-compatible volume from the configured risk |
//+------------------------------------------------------------------+
double CalculatePositionSizeByRisk(ENUM_ORDER_TYPE orderType,
double entryPrice,
double stopLossPrice)
{
//--- Use the account balance as the base for percentage risk
ResetLastError();
double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
if(accountBalance <= 0.0)
{
Print("CalculatePositionSizeByRisk: Invalid account balance. Error ",
GetLastError(), ".");
return 0.0;
}
//--- Convert the selected percentage into a monetary risk amount
double amountAtRisk = (riskPerTradePercent / 100.0) *
accountBalance;
if(amountAtRisk <= 0.0)
{
Print("CalculatePositionSizeByRisk: The calculated risk amount is invalid.");
return 0.0;
}
//--- Estimate the loss produced by one lot at the selected stop
double lossPerLot = 0.0;
ResetLastError();
if(!OrderCalcProfit(orderType,
_Symbol,
1.0,
entryPrice,
stopLossPrice,
lossPerLot))
{
Print("CalculatePositionSizeByRisk: OrderCalcProfit failed. Error ",
GetLastError(), ".");
return 0.0;
}
lossPerLot = MathAbs(lossPerLot);
if(lossPerLot <= 0.0)
{
Print("CalculatePositionSizeByRisk: Loss per lot is invalid.");
return 0.0;
}
//--- Divide the permitted loss by the estimated one-lot loss
double volume = amountAtRisk / lossPerLot;
//--- Read the broker's volume constraints for the current symbol
double minLot = 0.0;
double maxLot = 0.0;
double lotStep = 0.0;
if(!GetSymbolDoubleValue(_Symbol,
SYMBOL_VOLUME_MIN,
minLot,
"CalculatePositionSizeByRisk"))
{
return 0.0;
}
if(!GetSymbolDoubleValue(_Symbol,
SYMBOL_VOLUME_MAX,
maxLot,
"CalculatePositionSizeByRisk"))
{
return 0.0;
}
if(!GetSymbolDoubleValue(_Symbol,
SYMBOL_VOLUME_STEP,
lotStep,
"CalculatePositionSizeByRisk"))
{
return 0.0;
}
//--- Reject inconsistent broker volume specifications
if(minLot <= 0.0 ||
maxLot <= 0.0 ||
lotStep <= 0.0 ||
minLot > maxLot)
{
Print("CalculatePositionSizeByRisk: Invalid broker volume constraints.");
return 0.0;
}
//--- Round down so normalization does not increase the intended risk
volume = MathFloor(volume / lotStep) * lotStep;
//--- Clamp the result to the broker's permitted range
if(volume < minLot)
volume = minLot;
if(volume > maxLot)
volume = maxLot;
return NormalizeDouble(volume, 2);
}
//+------------------------------------------------------------------+
//| Updates the volume prepared for the current Oops setup |
//+------------------------------------------------------------------+
void UpdateOopsPositionSize()
{
//--- Manual mode uses the fixed volume selected in the inputs
if(lotSizeMode == MODE_MANUAL)
{
oopsState.lotSize = positionSize;
return;
}
//--- Select the structural stop associated with the setup direction
double stopLossPrice = 0.0;
if(oopsState.orderType == ORDER_TYPE_BUY)
stopLossPrice = oopsState.gapBarLow;
else
if(oopsState.orderType == ORDER_TYPE_SELL)
stopLossPrice = oopsState.gapBarHigh;
//--- Calculate the volume from the prepared entry and stop prices
double calculatedLot = CalculatePositionSizeByRisk(
oopsState.orderType,
oopsState.positionEntryPrice,
stopLossPrice
);
//--- Fall back to the manual value when automatic sizing fails
if(calculatedLot <= 0.0)
{
Print("UpdateOopsPositionSize: Falling back to manual lot size.");
oopsState.lotSize = positionSize;
return;
}
oopsState.lotSize = calculatedLot;
}
//+------------------------------------------------------------------+
//| Returns true when an open buy position uses the supplied magic |
//+------------------------------------------------------------------+
bool IsThereAnActiveBuyPosition(ulong magic)
{
int totalPositions = PositionsTotal();
//--- Inspect every open position
for(int i = totalPositions - 1; i >= 0; i--)
{
ResetLastError();
//--- PositionGetTicket() also selects the position for property access
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
{
Print("IsThereAnActiveBuyPosition: Failed to select position ",
i, ". Error ", GetLastError(), ".");
continue;
}
long positionMagic = 0;
long positionType = -1;
//--- Read the identifier assigned by the opening Expert Advisor
ResetLastError();
if(!PositionGetInteger(POSITION_MAGIC, positionMagic))
{
Print("IsThereAnActiveBuyPosition: Failed to read POSITION_MAGIC "
"for ticket ", ticket, ". Error ", GetLastError(), ".");
continue;
}
//--- Read the direction of the selected position
ResetLastError();
if(!PositionGetInteger(POSITION_TYPE, positionType))
{
Print("IsThereAnActiveBuyPosition: Failed to read POSITION_TYPE "
"for ticket ", ticket, ". Error ", GetLastError(), ".");
continue;
}
//--- Stop after finding a buy position managed by this EA
if((ulong)positionMagic == magic &&
(ENUM_POSITION_TYPE)positionType == POSITION_TYPE_BUY)
{
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| Returns true when an open sell position uses the supplied magic |
//+------------------------------------------------------------------+
bool IsThereAnActiveSellPosition(ulong magic)
{
int totalPositions = PositionsTotal();
//--- Inspect every open position
for(int i = totalPositions - 1; i >= 0; i--)
{
ResetLastError();
//--- Select the position and obtain its ticket
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
{
Print("IsThereAnActiveSellPosition: Failed to select position ",
i, ". Error ", GetLastError(), ".");
continue;
}
long positionMagic = 0;
long positionType = -1;
//--- Read the identifier assigned by the opening Expert Advisor
ResetLastError();
if(!PositionGetInteger(POSITION_MAGIC, positionMagic))
{
Print("IsThereAnActiveSellPosition: Failed to read POSITION_MAGIC "
"for ticket ", ticket, ". Error ", GetLastError(), ".");
continue;
}
//--- Read the direction of the selected position
ResetLastError();
if(!PositionGetInteger(POSITION_TYPE, positionType))
{
Print("IsThereAnActiveSellPosition: Failed to read POSITION_TYPE "
"for ticket ", ticket, ". Error ", GetLastError(), ".");
continue;
}
//--- Stop after finding a sell position managed by this EA
if((ulong)positionMagic == magic &&
(ENUM_POSITION_TYPE)positionType == POSITION_TYPE_SELL)
{
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| Checks whether the trade server accepted the submitted request |
//+------------------------------------------------------------------+
bool IsTradeRequestSuccessful(string context)
{
//--- Read the result code returned by the trade server
uint retcode = Trade.ResultRetcode();
//--- Accept completed, partially completed, or placed requests
if(retcode == TRADE_RETCODE_DONE ||
retcode == TRADE_RETCODE_DONE_PARTIAL ||
retcode == TRADE_RETCODE_PLACED)
{
PrintFormat("%s: Trade request accepted. Retcode %u (%s).",
context,
retcode,
Trade.ResultRetcodeDescription());
return true;
}
//--- Report the complete server response when the request is rejected
PrintFormat("%s: Trade request rejected. Retcode %u (%s). Comment: %s.",
context,
retcode,
Trade.ResultRetcodeDescription(),
Trade.ResultComment());
return false;
}
//+------------------------------------------------------------------+
//| Sends a market buy request and verifies the server response |
//+------------------------------------------------------------------+
bool OpenBuy(double entryPrice,
double stopLoss,
double takeProfit,
double lotSize)
{
ResetLastError();
//--- Submit the market buy request with the prepared trade values
if(!Trade.Buy(lotSize,
_Symbol,
entryPrice,
stopLoss,
takeProfit))
{
PrintFormat("OpenBuy: Trade.Buy failed. Error %d. Retcode %u (%s). "
"Comment: %s.",
GetLastError(),
Trade.ResultRetcode(),
Trade.ResultRetcodeDescription(),
Trade.ResultComment());
return false;
}
//--- Confirm that the trade server accepted the submitted request
if(!IsTradeRequestSuccessful("OpenBuy"))
return false;
return true;
}
//+------------------------------------------------------------------+
//| Sends a market sell request and verifies the server response |
//+------------------------------------------------------------------+
bool OpenSell(double entryPrice,
double stopLoss,
double takeProfit,
double lotSize)
{
ResetLastError();
//--- Submit the market sell request with the prepared trade values
if(!Trade.Sell(lotSize,
_Symbol,
entryPrice,
stopLoss,
takeProfit))
{
PrintFormat("OpenSell: Trade.Sell failed. Error %d. Retcode %u (%s). "
"Comment: %s.",
GetLastError(),
Trade.ResultRetcode(),
Trade.ResultRetcodeDescription(),
Trade.ResultComment());
return false;
}
//--- Confirm that the trade server accepted the submitted request
if(!IsTradeRequestSuccessful("OpenSell"))
return false;
return true;
}
2026-07-19 06:06:20 -07:00
//+------------------------------------------------------------------+
//| Expert initialization function |
2026-07-19 06:06:20 -07:00
//+------------------------------------------------------------------+
int OnInit()
{
//--- Stop initialization if the testing chart cannot be configured
if(!ConfigureChartAppearance())
{
Print("OnInit: Failed to configure the chart appearance.");
return INIT_FAILED;
}
//--- Assign the identifier used to distinguish this EA's positions
Trade.SetExpertMagicNumber(magicNumber);
//--- Allow the first verified bar time to initialize bar tracking
lastBarOpenTime = 0;
//--- Start without an active or partially initialized setup
ResetOopsPatternState();
return INIT_SUCCEEDED;
2026-07-19 06:06:20 -07:00
}
2026-07-19 06:06:20 -07:00
//+------------------------------------------------------------------+
//| Expert deinitialization function |
2026-07-19 06:06:20 -07:00
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
//--- Record the reason supplied by MetaTrader 5 when the EA stops
Print("Program terminated! Reason code: ", reason);
2026-07-19 06:06:20 -07:00
}
2026-07-19 06:06:20 -07:00
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//--- Stop the current processing cycle when the Ask price is unavailable
if(!GetSymbolDoubleValue(_Symbol,
SYMBOL_ASK,
askPrice,
"OnTick"))
{
return;
}
//--- Stop the current processing cycle when the Bid price is unavailable
if(!GetSymbolDoubleValue(_Symbol,
SYMBOL_BID,
bidPrice,
"OnTick"))
{
return;
}
//--- Store the latest terminal time for the current event cycle
currentTime = TimeCurrent();
//--- Evaluate the strategy only once when a new candle opens
if(!IsNewBar(_Symbol,
timeframe,
lastBarOpenTime))
{
return;
}
//--- Create a new setup only when no earlier gap is active
DetectAndInitializeOopsGap();
//--- Increase the setup age or remove it after expiration
UpdateOopsGapState();
//--- Prepare and process a confirmed bullish reversal
if(IsBullishSignal())
{
Print("Bullish Signal Detected!!");
//--- Use the current Ask price as the intended buy entry
oopsState.positionEntryPrice = askPrice;
//--- Prepare the stop loss, take profit, and position size
UpdateBullishGapBarStopLevel();
UpdateBullishTakeProfit(oopsState.positionEntryPrice);
UpdateOopsPositionSize();
//--- Continue only when bullish trading is permitted
if(tradeDirection == OOPS_TRADE_BOTH ||
tradeDirection == OOPS_TRADE_LONG_ONLY)
{
//--- Block the order when another EA-managed position is active
if(!IsThereAnActiveBuyPosition(magicNumber) &&
!IsThereAnActiveSellPosition(magicNumber))
{
if(!OpenBuy(oopsState.positionEntryPrice,
oopsState.gapBarLow,
oopsState.bullishTakeProfit,
oopsState.lotSize))
{
Print("OnTick: The bullish Oops trade was not opened.");
}
}
}
//--- Complete the setup lifecycle regardless of execution outcome
ResetOopsPatternState();
}
//--- Prepare and process a confirmed bearish reversal
if(IsBearishSignal())
{
Print("Bearish Signal Detected!!");
//--- Use the current Bid price as the intended sell entry
oopsState.positionEntryPrice = bidPrice;
//--- Prepare the stop loss, take profit, and position size
UpdateBearishGapBarStopLevel();
UpdateBearishTakeProfit(oopsState.positionEntryPrice);
UpdateOopsPositionSize();
//--- Continue only when bearish trading is permitted
if(tradeDirection == OOPS_TRADE_BOTH ||
tradeDirection == OOPS_TRADE_SHORT_ONLY)
{
//--- Block the order when another EA-managed position is active
if(!IsThereAnActiveBuyPosition(magicNumber) &&
!IsThereAnActiveSellPosition(magicNumber))
{
if(!OpenSell(oopsState.positionEntryPrice,
oopsState.gapBarHigh,
oopsState.bearishTakeProfit,
oopsState.lotSize))
{
Print("OnTick: The bearish Oops trade was not opened.");
}
}
}
//--- Clear the completed setup before tracking another gap
ResetOopsPatternState();
}
2026-07-19 06:06:20 -07:00
}
//+------------------------------------------------------------------+