//+------------------------------------------------------------------+ //| 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 }; enum ENUM_EXIT_MODE { EXIT_MIDPOINT_TRAIL, // Midpoint Trailing Stop (All at once) EXIT_LAYERED_SCALE, // Layered Scale-Out (One by one) EXIT_STAGGERED_TRAIL // Staggered Trailing Stop (Volume-based spacing) }; //--- 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 ENUM_EXIT_MODE InpExitMode = EXIT_LAYERED_SCALE; // Exit Logic Mode 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 double InpMaxLoadThresh = 10.0; // Throttle Entries at Deposit Load % input double InpLoadDistanceMultiplier = 1.5; // Throttling Multiplier (Distance) input int InpHardExitLevel = 5; // Trigger Hard Exit at (N Positions) input double InpHardExitDistanceStDev = 1.0; // Distance Past Last Entry (StDev) 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(); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); // EXIT MANAGEMENT (Run every tick) if(InpExitMode == EXIT_MIDPOINT_TRAIL) UpdateTrailingStop(bid, ask); else if(InpExitMode == EXIT_LAYERED_SCALE) HandleGradualExits(bid, ask); else UpdateStaggeredTrailingStop(bid, ask); // HARD TREND EXIT (Safety) CheckHardTrendExit(bid, ask); // UPDATE ENTRY DATA & CHECK ENTRIES (Run every tick to hit levels exactly) UpdateLastEntryData(); if(IsTimeAllowed()) { CheckEntries(bid, ask); } // 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; if(InpShowLevels) DrawLevels(); UpdateComments(bid, ask); } //+------------------------------------------------------------------+ //| Updates the chart commentary with status and diagnostics | //+------------------------------------------------------------------+ void UpdateComments(double bid, double ask) { if(!InpShowComments) { Comment(""); return; } string autoTradeStatus = (TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && MQLInfoInteger(MQL_TRADE_ALLOWED)) ? "ENABLED" : "!!! DISABLED (Check Terminal Settings/Button) !!!"; double margin = AccountInfoDouble(ACCOUNT_MARGIN); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double depositLoad = (equity > 0) ? (margin / equity) * 100.0 : 0; string spacingStatus = (depositLoad > InpMaxLoadThresh) ? "THROTTLED (Increased)" : "Normal"; string hardExitInfo = ""; int buys = GetPositionsCount(POSITION_TYPE_BUY); int sells = GetPositionsCount(POSITION_TYPE_SELL); if(buys >= InpHardExitLevel && lastEntryPrice > 0) hardExitInfo = StringFormat("\nHard Exit (Buy): Give up below %.5f", lastEntryPrice - (currStDev * InpHardExitDistanceStDev)); else if(sells >= InpHardExitLevel && lastEntryPrice > 0) hardExitInfo = StringFormat("\nHard Exit (Sell): Give up above %.5f", lastEntryPrice + (currStDev * InpHardExitDistanceStDev)); string comment = StringFormat("--- VWAP EA STATUS ---\n" "Auto-Trading: %s\n" "Deposit Load: %.2f%% (Spacing: %s)\n" "VWAP Mode: %s\n" "VWAP: %.5f | StDev: %.5f\n" "Buys: %d | Sells: %d%s\n" "Last Entry: %.5f (StDev: %.5f)\n" "Mean Entry Buy: %.5f\n" "Mean Entry Sell: %.5f", autoTradeStatus, depositLoad, spacingStatus, EnumToString(InpVWAPPeriod), currVWAP, currStDev, buys, sells, hardExitInfo, lastEntryPrice, lastEntryStDev, GetMeanEntryPrice(POSITION_TYPE_BUY), GetMeanEntryPrice(POSITION_TYPE_SELL)); Comment(comment); } //+------------------------------------------------------------------+ //| Check for Hard Trend Exit (Safety Close-All) | //+------------------------------------------------------------------+ void CheckHardTrendExit(double bid, double ask) { if(InpHardExitLevel <= 0) return; // BUY SIDE int buys = GetPositionsCount(POSITION_TYPE_BUY); if(buys >= InpHardExitLevel && lastEntryPrice > 0) { double giveUpPrice = lastEntryPrice - (currStDev * InpHardExitDistanceStDev); if(ask <= giveUpPrice) { PrintFormat("HARD EXIT: %d Buys reached. Price %.5f crossed Give-up Level %.5f", buys, ask, giveUpPrice); CloseAllPositions(); } } // SELL SIDE int sells = GetPositionsCount(POSITION_TYPE_SELL); if(sells >= InpHardExitLevel && lastEntryPrice > 0) { double giveUpPrice = lastEntryPrice + (currStDev * InpHardExitDistanceStDev); if(bid >= giveUpPrice) { PrintFormat("HARD EXIT: %d Sells reached. Price %.5f crossed Give-up Level %.5f", sells, bid, giveUpPrice); CloseAllPositions(); } } } //+------------------------------------------------------------------+ //| 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 && posInfo.Magic() == 123456) { 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); double margin = AccountInfoDouble(ACCOUNT_MARGIN); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double depositLoad = (equity > 0) ? (margin / equity) * 100.0 : 0; double effectiveStDevStep = InpStDevStep; if(depositLoad > InpMaxLoadThresh) effectiveStDevStep *= InpLoadDistanceMultiplier; // SELL Entries (Mean Reversion - above VWAP) if(sells < InpMaxLevels) { double nextSellLevel; if(sells == 0) nextSellLevel = currVWAP + (currStDev * effectiveStDevStep); else nextSellLevel = lastEntryPrice + (lastEntryStDev * effectiveStDevStep); 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 * effectiveStDevStep); else nextBuyLevel = lastEntryPrice - (lastEntryStDev * effectiveStDevStep); 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); } //+------------------------------------------------------------------+ //| Handle Gradual Exits (Layered Scale-Out) | //+------------------------------------------------------------------+ void HandleGradualExits(double bid, double ask) { // Logic: Find the most recent (outermost) position. // If price has moved 1 StDev step back towards the mean from its entry level, close it. // BUY POSITIONS int buys = GetPositionsCount(POSITION_TYPE_BUY); if(buys > 0) { ulong lastBuyTicket = 0; double lastBuyOpenPrice = 0; datetime latestTime = 0; // Find the most recent buy for(int i=PositionsTotal()-1; i>=0; i--) { if(posInfo.SelectByIndex(i) && posInfo.Symbol() == _Symbol && posInfo.PositionType() == POSITION_TYPE_BUY && posInfo.Magic() == 123456) { if(posInfo.Time() > latestTime) { latestTime = posInfo.Time(); lastBuyTicket = posInfo.Ticket(); lastBuyOpenPrice = posInfo.PriceOpen(); } } } if(lastBuyTicket > 0) { // Calculate target exit for this specific layer // If we are at level 3, we exit when price reaches level 2 double exitLevel = lastBuyOpenPrice + (currStDev * InpStDevStep * 0.5); // Exit halfway back to previous level if(bid >= exitLevel) { PrintFormat("Gradual Exit: Closing Buy Layer at %.5f (Entry: %.5f)", bid, lastBuyOpenPrice); trade.PositionClose(lastBuyTicket); } } } // SELL POSITIONS int sells = GetPositionsCount(POSITION_TYPE_SELL); if(sells > 0) { ulong lastSellTicket = 0; double lastSellOpenPrice = 0; datetime latestTime = 0; // Find the most recent sell for(int i=PositionsTotal()-1; i>=0; i--) { if(posInfo.SelectByIndex(i) && posInfo.Symbol() == _Symbol && posInfo.PositionType() == POSITION_TYPE_SELL && posInfo.Magic() == 123456) { if(posInfo.Time() > latestTime) { latestTime = posInfo.Time(); lastSellTicket = posInfo.Ticket(); lastSellOpenPrice = posInfo.PriceOpen(); } } } if(lastSellTicket > 0) { double exitLevel = lastSellOpenPrice - (currStDev * InpStDevStep * 0.5); // Exit halfway back if(ask <= exitLevel) { PrintFormat("Gradual Exit: Closing Sell Layer at %.5f (Entry: %.5f)", ask, lastSellOpenPrice); trade.PositionClose(lastSellTicket); } } } } //+------------------------------------------------------------------+ //| Update Staggered Trailing Stop | //+------------------------------------------------------------------+ void UpdateStaggeredTrailingStop(double bid, double ask) { HandleStaggeredTrailSide(POSITION_TYPE_BUY, bid); HandleStaggeredTrailSide(POSITION_TYPE_SELL, ask); } //+------------------------------------------------------------------+ //| Helper for Staggered Trail calculation for one side | //+------------------------------------------------------------------+ void HandleStaggeredTrailSide(ENUM_POSITION_TYPE type, double currentPrice) { int count = GetPositionsCount(type); if(count <= 0) return; double meanEntry = GetMeanEntryPrice(type); // Check if the overall basket is in profit bool inProfit = (type == POSITION_TYPE_BUY) ? (currentPrice > meanEntry) : (currentPrice < meanEntry); if(!inProfit) return; double totalDist = MathAbs(currentPrice - meanEntry); struct PosData { ulong ticket; double volume; }; PosData pData[]; ArrayResize(pData, count); int index = 0; // Collect all positions for this side for(int i=PositionsTotal()-1; i>=0; i--) { if(posInfo.SelectByIndex(i) && posInfo.Symbol() == _Symbol && posInfo.PositionType() == type && posInfo.Magic() == 123456) { pData[index].ticket = posInfo.Ticket(); pData[index].volume = posInfo.Volume(); index++; } } // Sort by volume ascending (Smallest volume first) for(int i=0; i pData[j].volume) { PosData temp = pData[i]; pData[i] = pData[j]; pData[j] = temp; } } } // Apply SLs: Smallest volume (index 0) gets 1/4 distance, Largest (index count-1) gets 3/4 for(int i=0; i 1) ratio = 0.25 + (0.5 * (double)i / (double)(count - 1)); else ratio = 0.5; // Midpoint if only one position double offset = totalDist * ratio; double newSL = (type == POSITION_TYPE_BUY) ? (meanEntry + offset) : (meanEntry - offset); newSL = NormalizeDouble(newSL, _Digits); if(posInfo.SelectByTicket(pData[i].ticket)) { if(type == POSITION_TYPE_BUY) { if(newSL > posInfo.StopLoss() || posInfo.StopLoss() == 0) trade.PositionModify(pData[i].ticket, newSL, posInfo.TakeProfit()); } else { if(newSL < posInfo.StopLoss() || posInfo.StopLoss() == 0) trade.PositionModify(pData[i].ticket, newSL, posInfo.TakeProfit()); } } } } //+------------------------------------------------------------------+ //| 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 && posInfo.Magic() == 123456) 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 && posInfo.Magic() == 123456) { 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 && posInfo.Magic() == 123456) { 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 && posInfo.Magic() == 123456) { trade.PositionClose(posInfo.Ticket()); } } }