mql5/VWAP_MeanReversion_EA.mq5

477 lines
16 KiB
MQL5
Raw Permalink Normal View History

2026-02-24 21:00:32 +00:00
//+------------------------------------------------------------------+
//| VWAP_MeanReversion_EA.mq5 |
//| Copyright 2024, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link "https://www.mql5.com"
#property version "1.20"
#property strict
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>
//--- ENUMS
enum ENUM_SCALING_TYPE
{
SCALING_LINEAR, // Linear
SCALING_EXPONENTIAL // Exponential
};
enum ENUM_VWAP_PERIOD
{
VWAP_DAILY, // Daily
VWAP_WEEKLY, // Weekly
VWAP_MONTHLY // Monthly
};
//--- INPUTS
input group "Trading Parameters"
input double InpBaseLot = 0.01; // Base Lot Size
input double InpStDevStep = 1.0; // Standard Deviation Entry Step (1.0, 2.0...)
input ENUM_SCALING_TYPE InpScalingType = SCALING_LINEAR; // Scaling Type
input double InpScalingMultiplier = 2.0; // Scaling Multiplier (for Exponential)
input int InpMaxLevels = 5; // Maximum Levels to Scale
input group "VWAP & Time Setup"
input ENUM_VWAP_PERIOD InpVWAPPeriod = VWAP_WEEKLY; // VWAP Anchor Period
input int InpStartHour = 8; // Start Hour for Entries
input int InpStartDay = 1; // Start Day (1=Mon, 2=Tue... for Weekly)
input int InpStartDate = 1; // Start Date (1-31, for Monthly)
input group "Protection"
input double InpTrailingStartStDev = 1.0; // Start Trailing at (StDev from Mean Entry)
input double InpMaxDrawdownPct = 10.0; // Max Drawdown %
input group "Visuals"
input bool InpShowLevels = true; // Show Levels on Chart
input bool InpShowComments = true; // Show Comments on Chart
input color InpVWAPColor = clrWhite; // VWAP Line Color
input color InpUpperBandColor = clrRed; // Upper Band Color
input color InpLowerBandColor = clrAqua; // Lower Band Color
//--- GLOBALS
CTrade trade;
CPositionInfo posInfo;
CSymbolInfo symInfo;
double currVWAP = 0;
double currStDev = 0;
double lastEntryPrice = 0;
double lastEntryStDev = 0;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
if(!symInfo.Name(_Symbol)) return INIT_FAILED;
trade.SetExpertMagicNumber(123456);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, "VWAP_");
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// GLOBAL EQUITY PROTECTION (Always check every tick)
CheckGlobalProtection();
// CORE LOGIC EXECUTION (Only on new M1 bar)
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(_Symbol, PERIOD_M1, 0);
if(currentBarTime == lastBarTime) return;
lastBarTime = currentBarTime;
if(!UpdateVWAPData()) return;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
UpdateLastEntryData(); // Update lastEntryPrice and lastEntryStDev from open positions
if(IsTimeAllowed())
{
CheckEntries(bid, ask);
}
UpdateTrailingStop(bid, ask);
if(InpShowLevels) DrawLevels();
if(InpShowComments)
{
Comment(StringFormat("VWAP Mode: %s\nVWAP: %.5f\nStDev: %.5f\nBuys: %d, Sells: %d\nLast Entry: %.5f (StDev: %.5f)\nMean Entry Buy: %.5f\nMean Entry Sell: %.5f",
EnumToString(InpVWAPPeriod), currVWAP, currStDev, GetPositionsCount(POSITION_TYPE_BUY), GetPositionsCount(POSITION_TYPE_SELL),
lastEntryPrice, lastEntryStDev,
GetMeanEntryPrice(POSITION_TYPE_BUY), GetMeanEntryPrice(POSITION_TYPE_SELL)));
}
else
{
Comment("");
}
}
//+------------------------------------------------------------------+
//| Check if trading is allowed based on period development |
//+------------------------------------------------------------------+
bool IsTimeAllowed()
{
MqlDateTime dt;
TimeCurrent(dt);
if(InpVWAPPeriod == VWAP_DAILY)
{
return (dt.hour >= InpStartHour);
}
else if(InpVWAPPeriod == VWAP_WEEKLY)
{
// If we are past the start day, trading is always allowed
if(dt.day_of_week > InpStartDay) return true;
// If we are on the start day, check the hour
if(dt.day_of_week == InpStartDay) return (dt.hour >= InpStartHour);
// If we are before the start day (e.g. Sunday when Monday is start), blocked
return false;
}
else if(InpVWAPPeriod == VWAP_MONTHLY)
{
if(dt.day > InpStartDate) return true;
if(dt.day == InpStartDate) return (dt.hour >= InpStartHour);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Calculate VWAP and Standard Deviation for the selected period |
//+------------------------------------------------------------------+
bool UpdateVWAPData()
{
// Optimization: Only recalculate on a new M1 bar for backtesting speed
static datetime lastCalcBar = 0;
datetime currentBar = iTime(_Symbol, PERIOD_M1, 0);
if(currentBar == lastCalcBar && currVWAP > 0) return true;
lastCalcBar = currentBar;
MqlDateTime dt;
datetime currentTime = TimeCurrent();
TimeToStruct(currentTime, dt);
datetime anchorTime = 0;
if(InpVWAPPeriod == VWAP_DAILY)
{
dt.hour = 0; dt.min = 0; dt.sec = 0;
anchorTime = StructToTime(dt);
}
else if(InpVWAPPeriod == VWAP_WEEKLY)
{
dt.hour = 0; dt.min = 0; dt.sec = 0;
datetime startOfDay = StructToTime(dt);
anchorTime = startOfDay - (dt.day_of_week * 86400);
}
else if(InpVWAPPeriod == VWAP_MONTHLY)
{
dt.day = 1; dt.hour = 0; dt.min = 0; dt.sec = 0;
anchorTime = StructToTime(dt);
}
int bars = Bars(_Symbol, PERIOD_M1, anchorTime, currentTime);
if(bars <= 0) return false;
MqlRates rates[];
if(CopyRates(_Symbol, PERIOD_M1, 0, bars, rates) <= 0) return false;
double sumPriceVol = 0;
double sumVol = 0;
// VWAP Calculation
for(int i=0; i<bars; i++)
{
double typicalPrice = (rates[i].high + rates[i].low + rates[i].close) / 3.0;
sumPriceVol += typicalPrice * (double)rates[i].tick_volume;
sumVol += (double)rates[i].tick_volume;
}
if(sumVol == 0) return false;
currVWAP = sumPriceVol / sumVol;
// Standard Deviation Calculation
double sumVariance = 0;
for(int i=0; i<bars; i++)
{
double typicalPrice = (rates[i].high + rates[i].low + rates[i].close) / 3.0;
sumVariance += MathPow(typicalPrice - currVWAP, 2) * (double)rates[i].tick_volume;
}
currStDev = MathSqrt(sumVariance / sumVol);
return (currStDev > 0);
}
//+------------------------------------------------------------------+
//| Tracks the last entry to use its StDev for future spacing |
//+------------------------------------------------------------------+
void UpdateLastEntryData()
{
double latestTime = 0;
lastEntryPrice = 0;
for(int i=PositionsTotal()-1; i>=0; i--)
{
if(posInfo.SelectByIndex(i) && posInfo.Symbol() == _Symbol)
{
datetime openTime = (datetime)posInfo.Time();
if(openTime > latestTime)
{
latestTime = (double)openTime;
lastEntryPrice = posInfo.PriceOpen();
}
}
}
if(latestTime == 0)
{
lastEntryPrice = 0;
lastEntryStDev = 0;
}
}
//+------------------------------------------------------------------+
//| Check for new entries at standard deviation levels |
//+------------------------------------------------------------------+
void CheckEntries(double bid, double ask)
{
int buys = GetPositionsCount(POSITION_TYPE_BUY);
int sells = GetPositionsCount(POSITION_TYPE_SELL);
// SELL Entries (Mean Reversion - above VWAP)
if(sells < InpMaxLevels)
{
double nextSellLevel;
if(sells == 0)
nextSellLevel = currVWAP + (currStDev * InpStDevStep);
else
nextSellLevel = lastEntryPrice + (lastEntryStDev * InpStDevStep);
if(bid > nextSellLevel)
{
double lot = CalculateLotSize(sells);
if(trade.Sell(lot, _Symbol, bid, 0, 0, "VWAP Level " + (string)(sells+1)))
{
lastEntryPrice = bid;
lastEntryStDev = currStDev;
}
}
}
// BUY Entries (Mean Reversion - below VWAP)
if(buys < InpMaxLevels)
{
double nextBuyLevel;
if(buys == 0)
nextBuyLevel = currVWAP - (currStDev * InpStDevStep);
else
nextBuyLevel = lastEntryPrice - (lastEntryStDev * InpStDevStep);
if(ask < nextBuyLevel)
{
double lot = CalculateLotSize(buys);
if(trade.Buy(lot, _Symbol, ask, 0, 0, "VWAP Level " + (string)(buys+1)))
{
lastEntryPrice = ask;
lastEntryStDev = currStDev;
}
}
}
}
//+------------------------------------------------------------------+
//| Draw VWAP and active bands on the chart |
//+------------------------------------------------------------------+
void DrawLevels()
{
UpdateLine("VWAP_Center", currVWAP, InpVWAPColor, STYLE_SOLID, 2);
for(int i=1; i<=InpMaxLevels; i++)
{
double up = currVWAP + (currStDev * i * InpStDevStep);
double dn = currVWAP - (currStDev * i * InpStDevStep);
UpdateLine("VWAP_Up_"+(string)i, up, InpUpperBandColor, STYLE_DOT, 1);
UpdateLine("VWAP_Dn_"+(string)i, dn, InpLowerBandColor, STYLE_DOT, 1);
}
}
//+------------------------------------------------------------------+
//| Helper to create or move a horizontal line |
//+------------------------------------------------------------------+
void UpdateLine(string name, double price, color clr, ENUM_LINE_STYLE style, int width)
{
if(ObjectFind(0, name) < 0)
{
ObjectCreate(0, name, OBJ_HLINE, 0, 0, price);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_STYLE, style);
ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
}
else
{
ObjectMove(0, name, 0, 0, price);
}
}
//+------------------------------------------------------------------+
//| Calculate lot size based on scaling type |
//+------------------------------------------------------------------+
double CalculateLotSize(int currentCount)
{
if(currentCount == 0) return InpBaseLot;
double lot = InpBaseLot;
if(InpScalingType == SCALING_LINEAR)
lot = InpBaseLot * (currentCount + 1);
else
lot = InpBaseLot * MathPow(InpScalingMultiplier, currentCount);
return NormalizeDouble(lot, 2);
}
//+------------------------------------------------------------------+
//| Update Trailing Stop at the midpoint of price and mean entry |
//+------------------------------------------------------------------+
void UpdateTrailingStop(double bid, double ask)
{
// BUY Positions
if(GetPositionsCount(POSITION_TYPE_BUY) > 0)
{
double meanEntry = GetMeanEntryPrice(POSITION_TYPE_BUY);
if(bid > meanEntry + (currStDev * InpTrailingStartStDev))
{
double midpoint = NormalizeDouble((bid + meanEntry) / 2.0, _Digits);
ModifyGroupStopLoss(POSITION_TYPE_BUY, midpoint);
}
}
// SELL Positions
if(GetPositionsCount(POSITION_TYPE_SELL) > 0)
{
double meanEntry = GetMeanEntryPrice(POSITION_TYPE_SELL);
if(ask < meanEntry - (currStDev * InpTrailingStartStDev))
{
double midpoint = NormalizeDouble((ask + meanEntry) / 2.0, _Digits);
ModifyGroupStopLoss(POSITION_TYPE_SELL, midpoint);
}
}
}
//+------------------------------------------------------------------+
//| Check Global Protection Drawdown |
//+------------------------------------------------------------------+
void CheckGlobalProtection()
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(balance <= 0) return;
double drawdown = (balance - equity) / balance * 100.0;
if(drawdown >= InpMaxDrawdownPct)
{
Print("CRITICAL: Max Equity Drawdown reached. Closing all positions.");
CloseAllPositions();
}
}
//+------------------------------------------------------------------+
//| Helper: Get number of positions for a side |
//+------------------------------------------------------------------+
int GetPositionsCount(ENUM_POSITION_TYPE type)
{
int count = 0;
for(int i=PositionsTotal()-1; i>=0; i--)
{
if(posInfo.SelectByIndex(i))
{
if(posInfo.Symbol() == _Symbol && posInfo.PositionType() == type)
count++;
}
}
return count;
}
//+------------------------------------------------------------------+
//| Helper: Get mean entry price for a side |
//+------------------------------------------------------------------+
double GetMeanEntryPrice(ENUM_POSITION_TYPE type)
{
double totalVolume = 0;
double totalWeight = 0;
for(int i=PositionsTotal()-1; i>=0; i--)
{
if(posInfo.SelectByIndex(i))
{
if(posInfo.Symbol() == _Symbol && posInfo.PositionType() == type)
{
totalVolume += posInfo.Volume();
totalWeight += posInfo.Volume() * posInfo.PriceOpen();
}
}
}
return (totalVolume > 0) ? (totalWeight / totalVolume) : 0;
}
//+------------------------------------------------------------------+
//| Helper: Modify Stop Loss for all positions in a group |
//+------------------------------------------------------------------+
void ModifyGroupStopLoss(ENUM_POSITION_TYPE type, double newSL)
{
for(int i=PositionsTotal()-1; i>=0; i--)
{
if(posInfo.SelectByIndex(i))
{
if(posInfo.Symbol() == _Symbol && posInfo.PositionType() == type)
{
if(type == POSITION_TYPE_BUY)
{
if(newSL > posInfo.StopLoss() || posInfo.StopLoss() == 0)
trade.PositionModify(posInfo.Ticket(), newSL, posInfo.TakeProfit());
}
else
{
if(newSL < posInfo.StopLoss() || posInfo.StopLoss() == 0)
trade.PositionModify(posInfo.Ticket(), newSL, posInfo.TakeProfit());
}
}
}
}
}
//+------------------------------------------------------------------+
//| Helper: Close all positions for this symbol |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
for(int i=PositionsTotal()-1; i>=0; i--)
{
if(posInfo.SelectByIndex(i) && posInfo.Symbol() == _Symbol)
{
trade.PositionClose(posInfo.Ticket());
}
}
}