//+------------------------------------------------------------------+ //| 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 #include #include //--- 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 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()); } } }