//+------------------------------------------------------------------+ //| VWMA_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_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 "VWMA Setup" input int InpVWMAPeriod = 20; // VWMA Rolling Period (Bars) input ENUM_TIMEFRAMES InpTimeframe = PERIOD_CURRENT; // Calculation Timeframe input group "Time Filter" input int InpStartHour = 8; // Start Hour for Entries input int InpEndHour = 20; // End Hour for Entries 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 InpVWMAColor = clrWhite; // VWMA Line Color input color InpUpperBandColor = clrRed; // Upper Band Color input color InpLowerBandColor = clrAqua; // Lower Band Color //--- GLOBALS CTrade trade; CPositionInfo posInfo; CSymbolInfo symInfo; double currVWMA = 0; double currStDev = 0; double lastEntryPrice = 0; double lastEntryStDev = 0; //--- CACHED POSITION DATA (Optimized for speed) struct SPositionSummary { int buys; int sells; double meanBuy; double meanSell; double lastPrice; datetime latestTime; } posSummary; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(!symInfo.Name(_Symbol)) return INIT_FAILED; trade.SetExpertMagicNumber(654321); // Unique Magic for VWMA return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ObjectsDeleteAll(0, "VWMA_"); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // 1. GLOBAL EQUITY PROTECTION CheckGlobalProtection(); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); static datetime lastBarTime = 0; datetime currentBarTime = iTime(_Symbol, InpTimeframe, 0); bool isNewBar = (currentBarTime != lastBarTime); bool isTester = (bool)MQLInfoInteger(MQL_TESTER); // 2. CORE CALCULATION (Must happen before any trading logic) if(isNewBar || currVWMA == 0) { if(UpdateVWMAData()) { lastBarTime = currentBarTime; if(InpShowLevels) DrawLevels(); } else { // If data update fails, we must return and try again on next tick return; } } // 3. CACHE POSITION DATA UpdatePositionSummary(); // 4. EXIT MANAGEMENT if(InpExitMode == EXIT_MIDPOINT_TRAIL) UpdateTrailingStop(bid, ask); else if(InpExitMode == EXIT_LAYERED_SCALE) HandleGradualExits(bid, ask); else UpdateStaggeredTrailingStop(bid, ask); // 5. HARD TREND EXIT CheckHardTrendExit(bid, ask); // 6. CHECK ENTRIES if(IsTimeAllowed()) { CheckEntries(bid, ask); } // 7. VISUALS (Throttled) if(!isTester) { static uint lastVisualUpdate = 0; if(GetTickCount() - lastVisualUpdate > 500) { lastVisualUpdate = GetTickCount(); if(InpShowLevels) DrawLevels(); UpdateComments(bid, ask); } } else if(isNewBar) { 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 drawdown = (equity > 0) ? (AccountInfoDouble(ACCOUNT_BALANCE) - equity) / AccountInfoDouble(ACCOUNT_BALANCE) * 100.0 : 0; double depositLoad = (equity > 0) ? (margin / equity) * 100.0 : 0; string spacingStatus = (depositLoad > InpMaxLoadThresh) ? "THROTTLED (Increased)" : "Normal"; string hardExitInfo = ""; if(posSummary.buys >= InpHardExitLevel && posSummary.lastPrice > 0) hardExitInfo = StringFormat("\nHard Exit (Buy): Give up below %.5f", posSummary.lastPrice - (currStDev * InpHardExitDistanceStDev)); else if(posSummary.sells >= InpHardExitLevel && posSummary.lastPrice > 0) hardExitInfo = StringFormat("\nHard Exit (Sell): Give up above %.5f", posSummary.lastPrice + (currStDev * InpHardExitDistanceStDev)); string comment = StringFormat("--- VWMA EA STATUS ---\n" "Auto-Trading: %s\n" "Drawdown: %.2f%%\n" "Deposit Load: %.2f%% (Spacing: %s)\n" "VWMA Mode: Rolling %d Bars\n" "VWMA: %.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, drawdown, depositLoad, spacingStatus, InpVWMAPeriod, currVWMA, currStDev, posSummary.buys, posSummary.sells, hardExitInfo, posSummary.lastPrice, lastEntryStDev, posSummary.meanBuy, posSummary.meanSell); Comment(comment); } //+------------------------------------------------------------------+ //| Check for Hard Trend Exit (Safety Close-All) | //+------------------------------------------------------------------+ void CheckHardTrendExit(double bid, double ask) { if(InpHardExitLevel <= 0) return; // BUY SIDE if(posSummary.buys >= InpHardExitLevel && posSummary.lastPrice > 0) { double giveUpPrice = posSummary.lastPrice - (currStDev * InpHardExitDistanceStDev); if(ask <= giveUpPrice) { PrintFormat("HARD EXIT: %d Buys reached. Price %.5f crossed Give-up Level %.5f", posSummary.buys, ask, giveUpPrice); CloseAllPositions(); } } // SELL SIDE if(posSummary.sells >= InpHardExitLevel && posSummary.lastPrice > 0) { double giveUpPrice = posSummary.lastPrice + (currStDev * InpHardExitDistanceStDev); if(bid >= giveUpPrice) { PrintFormat("HARD EXIT: %d Sells reached. Price %.5f crossed Give-up Level %.5f", posSummary.sells, bid, giveUpPrice); CloseAllPositions(); } } } //+------------------------------------------------------------------+ //| Check if trading is allowed based on hour filter | //+------------------------------------------------------------------+ bool IsTimeAllowed() { MqlDateTime dt; TimeCurrent(dt); return (dt.hour >= InpStartHour && dt.hour <= InpEndHour); } //+------------------------------------------------------------------+ //| Calculate rolling VWMA and Standard Deviation | //+------------------------------------------------------------------+ bool UpdateVWMAData() { int bars = InpVWMAPeriod; MqlRates rates[]; if(CopyRates(_Symbol, InpTimeframe, 1, bars, rates) < bars) return false; double sumPriceVol = 0; double sumVol = 0; // VWMA Calculation for(int i=0; i 0); } //+------------------------------------------------------------------+ //| Updates the cached position summary | //+------------------------------------------------------------------+ void UpdatePositionSummary() { ZeroMemory(posSummary); double totalVolBuy = 0, totalWeightBuy = 0; double totalVolSell = 0, totalWeightSell = 0; for(int i=PositionsTotal()-1; i>=0; i--) { if(posInfo.SelectByIndex(i) && posInfo.Symbol() == _Symbol && posInfo.Magic() == 654321) { if(posInfo.PositionType() == POSITION_TYPE_BUY) { posSummary.buys++; totalVolBuy += posInfo.Volume(); totalWeightBuy += posInfo.Volume() * posInfo.PriceOpen(); } else { posSummary.sells++; totalVolSell += posInfo.Volume(); totalWeightSell += posInfo.Volume() * posInfo.PriceOpen(); } if(posInfo.Time() > posSummary.latestTime) { posSummary.latestTime = (datetime)posInfo.Time(); posSummary.lastPrice = posInfo.PriceOpen(); } } } posSummary.meanBuy = (totalVolBuy > 0) ? (totalWeightBuy / totalVolBuy) : 0; posSummary.meanSell = (totalVolSell > 0) ? (totalWeightSell / totalVolSell) : 0; // Update globals for compatibility with legacy functions if needed lastEntryPrice = posSummary.lastPrice; } //+------------------------------------------------------------------+ //| Check for new entries at standard deviation levels | //+------------------------------------------------------------------+ void CheckEntries(double bid, double ask) { if(currVWMA == 0 || currStDev == 0) return; 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 VWMA) if(posSummary.sells < InpMaxLevels) { double nextSellLevel; if(posSummary.sells == 0) nextSellLevel = currVWMA + (currStDev * effectiveStDevStep); else nextSellLevel = posSummary.lastPrice + (lastEntryStDev * effectiveStDevStep); if(bid > nextSellLevel) { double lot = CalculateLotSize(posSummary.sells); if(trade.Sell(lot, _Symbol, bid, 0, 0, "VWMA Level " + (string)(posSummary.sells+1))) { lastEntryPrice = bid; lastEntryStDev = currStDev; UpdatePositionSummary(); // Update immediately after trade } } } // BUY Entries (Mean Reversion - below VWMA) if(posSummary.buys < InpMaxLevels) { double nextBuyLevel; if(posSummary.buys == 0) nextBuyLevel = currVWMA - (currStDev * effectiveStDevStep); else nextBuyLevel = posSummary.lastPrice - (lastEntryStDev * effectiveStDevStep); if(ask < nextBuyLevel) { double lot = CalculateLotSize(posSummary.buys); if(trade.Buy(lot, _Symbol, ask, 0, 0, "VWMA Level " + (string)(posSummary.buys+1))) { lastEntryPrice = ask; lastEntryStDev = currStDev; UpdatePositionSummary(); // Update immediately after trade } } } } //+------------------------------------------------------------------+ //| Draw VWMA and active bands on the chart | //+------------------------------------------------------------------+ void DrawLevels() { UpdateLine("VWMA_Center", currVWMA, InpVWMAColor, STYLE_SOLID, 2); for(int i=1; i<=InpMaxLevels; i++) { double up = currVWMA + (currStDev * i * InpStDevStep); double dn = currVWMA - (currStDev * i * InpStDevStep); UpdateLine("VWMA_Up_"+(string)i, up, InpUpperBandColor, STYLE_DOT, 1); UpdateLine("VWMA_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 if(posSummary.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() == 654321) { if(posInfo.Time() > latestTime) { latestTime = posInfo.Time(); lastBuyTicket = posInfo.Ticket(); lastBuyOpenPrice = posInfo.PriceOpen(); } } } if(lastBuyTicket > 0) { double exitLevel = lastBuyOpenPrice + (currStDev * InpStDevStep * 0.5); if(bid >= exitLevel) { PrintFormat("Gradual Exit: Closing Buy Layer at %.5f (Entry: %.5f)", bid, lastBuyOpenPrice); trade.PositionClose(lastBuyTicket); UpdatePositionSummary(); } } } // SELL POSITIONS if(posSummary.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() == 654321) { if(posInfo.Time() > latestTime) { latestTime = posInfo.Time(); lastSellTicket = posInfo.Ticket(); lastSellOpenPrice = posInfo.PriceOpen(); } } } if(lastSellTicket > 0) { double exitLevel = lastSellOpenPrice - (currStDev * InpStDevStep * 0.5); if(ask <= exitLevel) { PrintFormat("Gradual Exit: Closing Sell Layer at %.5f (Entry: %.5f)", ask, lastSellOpenPrice); trade.PositionClose(lastSellTicket); UpdatePositionSummary(); } } } } //+------------------------------------------------------------------+ //| 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 = (type == POSITION_TYPE_BUY) ? posSummary.buys : posSummary.sells; if(count <= 0) return; double meanEntry = (type == POSITION_TYPE_BUY) ? posSummary.meanBuy : posSummary.meanSell; // 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() == 654321) { 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(posSummary.buys > 0) { double meanEntry = posSummary.meanBuy; if(bid > meanEntry + (currStDev * InpTrailingStartStDev)) { double midpoint = NormalizeDouble((bid + meanEntry) / 2.0, _Digits); ModifyGroupStopLoss(POSITION_TYPE_BUY, midpoint); } } // SELL Positions if(posSummary.sells > 0) { double meanEntry = posSummary.meanSell; 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) { PrintFormat("CRITICAL: Max Equity Drawdown reached (%.2f%% >= %.2f%%). Closing all positions.", drawdown, InpMaxDrawdownPct); CloseAllPositions(); } } // (Helpers removed or replaced by UpdatePositionSummary) //+------------------------------------------------------------------+ //| 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() == 654321) { 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() == 654321) { trade.PositionClose(posInfo.Ticket()); } } }