//+------------------------------------------------------------------+ //| Vwap to Close.mq5 | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "2.00" #property strict #include #include #include //--- ENUMS enum ENUM_VWAP_PERIOD { VWAP_DAILY, VWAP_WEEKLY, VWAP_MONTHLY }; enum ENUM_EXIT_MODE { EXIT_AT_VWAP, EXIT_TRAILING_ONLY, EXIT_OVERSHOOT_BUFFER, EXIT_LAYERED_SCALE, EXIT_STAGGERED_TRAIL }; //--- INPUTS input group "=== Trading Settings ===" input double InpBaseLot = 0.01; // Base Lot Size input double InpLotMultiplier = 2.0; // Lot Scaling Multiplier input int InpMaxLevels = 10; // Maximum Open Levels per side input int InpStopLoss = 0; // Stop Loss (Points, 0 = Disabled) input int InpTakeProfit = 0; // Take Profit (Points, 0 = Disabled) input int InpMaxSpread = 40; // Max Allowed Spread (Points, 0 = Disabled) input group "=== VWAP & Metric ===" input ENUM_VWAP_PERIOD InpVWAPPeriod = VWAP_DAILY; // VWAP Anchor Period input double InpThreshold = 0.005; // Metric Entry Step (e.g. 0.005) input int InpStartHour = 0; // Start Hour for Daily/Weekly/Monthly input int InpStartDay = 1; // Start Day (1=Mon) for Weekly input group "=== Momentum & Filters ===" input bool InpUseMomentumFilter = true; // Use Momentum Filter (M5 Direction) input int InpMomentumBars = 1; // Momentum Lookback Bars (M5) input bool InpUseNewsFilter = true; // Enable Calendar News Filter input bool InpCloseBeforeNews = false; // Close Positions Before News input int InpNewsBufferPre = 30; // Minutes Before News input int InpNewsBufferPost = 30; // Minutes After News input group "=== Exit Parameters ===" input ENUM_EXIT_MODE InpExitMode = EXIT_AT_VWAP; // Exit Strategy input int InpVWAPOvershoot = 50; // VWAP Overshoot (Points) input bool InpUseTrailing = true; // Use Trailing Stop (for applicable modes) input double InpExitThreshold = 0.002; // Metric Exit Threshold (e.g. 0.002) input double InpTrailPercent = 0.75; // Trail Percent (0.75 = 3/4th distance) input int InpTrailingStepPoints = 20; // Trailing Step (Points) input bool InpUseBreakeven = true; // Use Breakeven input int InpBreakevenTrigger = 100; // Breakeven Trigger (Points) input int InpBreakevenOffset = 10; // Breakeven Offset (Points) input group "=== Session Management ===" input bool InpCloseAtSessionEnd = true; // Close before Session End input int InpCloseBufferMinutes = 5; // Minutes before end to close input bool InpCloseOnAnchorChange = false; // Close on Anchor Reset (e.g. New Day) input group "=== Visuals (Tester Optimized) ===" input bool InpShowVisuals = true; // Show Chart Objects (VWAP, Lines) input ENUM_TIMEFRAMES InpRefreshRate = PERIOD_M10; // Visual Refresh Rate input color InpVWAPColor = clrRoyalBlue; // VWAP Line Color input color InpBuyEntryColor = clrLime; // Buy Entry Line Color input color InpSellEntryColor = clrRed; // Sell Entry Line Color //+------------------------------------------------------------------+ //| STRUCTS | //+------------------------------------------------------------------+ struct SMarketData { double vwap; double mid; double bid; double ask; int spread; double metric; datetime sessionEnd; bool isSessionEnding; bool isNewsImminent; string newsMsg; datetime anchor; }; struct SPositionSummary { int buys; int sells; ulong newestBuy; ulong newestSell; double meanBuy; double meanSell; }; //+------------------------------------------------------------------+ //| HELPERS | //+------------------------------------------------------------------+ // (Including CVwapCalc and CNewsFilter within expert scope for clean encapsulation) class CVwapCalc { private: double m_sumPriceVol, m_sumVol; datetime m_lastAnchor, m_lastBarTime; double m_currentVwap; public: CVwapCalc() : m_sumPriceVol(0), m_sumVol(0), m_lastAnchor(0), m_lastBarTime(0), m_currentVwap(0) {} double GetVWAP() { return m_currentVwap; } datetime GetAnchor() { return m_lastAnchor; } bool Update(ENUM_VWAP_PERIOD p, int h, int d) { datetime now = TimeCurrent(); datetime anchor = CalculateAnchor(now, p, h, d); if(anchor != m_lastAnchor) { m_sumPriceVol = 0; m_sumVol = 0; m_lastBarTime = 0; m_lastAnchor = anchor; } int bars = Bars(_Symbol, PERIOD_M1, m_lastBarTime > 0 ? m_lastBarTime : m_lastAnchor, now); if(bars <= 0) return (m_currentVwap > 0); MqlRates rates[]; if(CopyRates(_Symbol, PERIOD_M1, 0, bars, rates) > 0) { for(int i=0; i 0) m_currentVwap = m_sumPriceVol / m_sumVol; return (m_currentVwap > 0); } private: datetime CalculateAnchor(datetime now, ENUM_VWAP_PERIOD p, int h, int d) { MqlDateTime dt; TimeToStruct(now, dt); dt.hour = h; dt.min = 0; dt.sec = 0; datetime anchor = StructToTime(dt); if(p == VWAP_DAILY) { if(anchor > now) anchor -= 86400; } else if(p == VWAP_WEEKLY) { int offset = dt.day_of_week - d; if(offset < 0) offset += 7; anchor -= offset * 86400; if(anchor > now) anchor -= 7 * 86400; } else if(p == VWAP_MONTHLY) { dt.day = 1; anchor = StructToTime(dt); if(anchor > now) { dt.mon--; if(dt.mon < 1) { dt.mon = 12; dt.year--; } anchor = StructToTime(dt); } } return anchor; } }; class CNewsFilter { public: bool IsNewsUpcoming(int bPre, int bPost, string &msg) { MqlCalendarValue values[]; datetime now = TimeCurrent(); if(CalendarValueHistory(values, now, now + (bPre * 60)) > 0) { for(int i=0; i= 0) { msg = ev.name + " (" + co.currency + ")"; return true; } } } } if(CalendarValueHistory(values, now - (bPost * 60), now) > 0) { for(int i=0; i= 0) { msg = ev.name + " (Recent)"; return true; } } } } return false; } }; //+------------------------------------------------------------------+ //| EXPERT ENGINE CLASS | //+------------------------------------------------------------------+ class CVwapExpert { private: CTrade m_trade; CPositionInfo m_pos; CSymbolInfo m_sym; CVwapCalc m_vwap; CNewsFilter m_news; SMarketData m_data; SPositionSummary m_summary; public: CVwapExpert() {} bool Init() { if(!m_sym.Name(_Symbol)) return false; m_trade.SetExpertMagicNumber(777111); //ArrayFree(m_data.newsMsg); ZeroMemory(m_data); ZeroMemory(m_summary); return true; } void Deinit() { Comment(""); ObjectsDeleteAll(0, "VC_"); } void OnTick() { if(!UpdateMarketData()) return; PerformPreScan(); HandlePositionManagement(); HandleEntryLogic(); RefreshUI(); } private: bool UpdateMarketData() { if(!m_vwap.Update(InpVWAPPeriod, InpStartHour, InpStartDay)) return false; m_data.vwap = m_vwap.GetVWAP(); m_data.anchor = m_vwap.GetAnchor(); m_data.bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); m_data.ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); m_data.mid = (m_data.bid + m_data.ask) / 2.0; m_data.spread = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); if(m_data.mid <= 0) return false; m_data.metric = m_data.vwap / m_data.mid; // Session Tracking if(InpVWAPPeriod == VWAP_DAILY) m_data.sessionEnd = m_data.anchor + 86400; else if(InpVWAPPeriod == VWAP_WEEKLY) m_data.sessionEnd = m_data.anchor + 7 * 86400; else if(InpVWAPPeriod == VWAP_MONTHLY) { MqlDateTime dt; TimeToStruct(m_data.anchor, dt); dt.mon++; if(dt.mon > 12) { dt.mon = 1; dt.year++; } m_data.sessionEnd = StructToTime(dt); } m_data.isSessionEnding = (InpCloseAtSessionEnd && TimeCurrent() >= m_data.sessionEnd - (InpCloseBufferMinutes * 60)); // News tracking m_data.isNewsImminent = (InpUseNewsFilter) ? m_news.IsNewsUpcoming(InpNewsBufferPre, InpNewsBufferPost, m_data.newsMsg) : false; // Anchor Change Check static datetime lastAnchor = 0; if(InpCloseOnAnchorChange && lastAnchor != 0 && m_data.anchor != lastAnchor) { CloseAllPositions("Anchor Reset"); } lastAnchor = m_data.anchor; return true; } void PerformPreScan() { ZeroMemory(m_summary); datetime latestB = 0, latestS = 0; double sumVB = 0, sumPVB = 0, sumVS = 0, sumPVS = 0; for(int i=PositionsTotal()-1; i>=0; i--) { if(m_pos.SelectByIndex(i) && m_pos.Symbol() == _Symbol && m_pos.Magic() == 777111) { double v = m_pos.Volume(); double p = m_pos.PriceOpen(); if(m_pos.PositionType() == POSITION_TYPE_BUY) { m_summary.buys++; sumVB += v; sumPVB += v * p; if(m_pos.Time() > latestB) { latestB = m_pos.Time(); m_summary.newestBuy = m_pos.Ticket(); } } else { m_summary.sells++; sumVS += v; sumPVS += v * p; if(m_pos.Time() > latestS) { latestS = m_pos.Time(); m_summary.newestSell = m_pos.Ticket(); } } } } m_summary.meanBuy = (sumVB > 0) ? (sumPVB / sumVB) : 0; m_summary.meanSell = (sumVS > 0) ? (sumPVS / sumVS) : 0; } void HandlePositionManagement() { // 1. Mandatory Closes if(m_data.isSessionEnding || (m_data.isNewsImminent && InpCloseBeforeNews)) { CloseAllPositions("News/Session Buffer"); return; } // 2. Individual Position Exits for(int i=PositionsTotal()-1; i>=0; i--) { if(m_pos.SelectByIndex(i) && m_pos.Symbol() == _Symbol && m_pos.Magic() == 777111) { if(ExecuteExitsForPosition()) continue; // If closed, move to next } } // 3. Staggered Trail if(InpExitMode == EXIT_STAGGERED_TRAIL) HandleStaggeredTrail(); } bool ExecuteExitsForPosition() { ulong t = m_pos.Ticket(); ENUM_POSITION_TYPE type = m_pos.PositionType(); // A. Static Exits if(InpExitMode == EXIT_AT_VWAP) { if(type == POSITION_TYPE_BUY && m_data.bid >= m_data.vwap) return m_trade.PositionClose(t); if(type == POSITION_TYPE_SELL && m_data.ask <= m_data.vwap) return m_trade.PositionClose(t); } else if(InpExitMode == EXIT_OVERSHOOT_BUFFER) { double over = InpVWAPOvershoot * _Point; if(type == POSITION_TYPE_BUY && m_data.bid >= m_data.vwap + over) return m_trade.PositionClose(t); if(type == POSITION_TYPE_SELL && m_data.ask <= m_data.vwap - over) return m_trade.PositionClose(t); } // B. Breakeven if(InpUseBreakeven) { double open = m_pos.PriceOpen(); double sl = m_pos.StopLoss(); if(type == POSITION_TYPE_BUY && m_data.bid >= open + InpBreakevenTrigger * _Point) { double nSl = NormalizeDouble(open + InpBreakevenOffset * _Point, _Digits); if(sl < nSl) m_trade.PositionModify(t, nSl, m_pos.TakeProfit()); } else if(type == POSITION_TYPE_SELL && m_data.ask <= open - InpBreakevenTrigger * _Point) { double nSl = NormalizeDouble(open - InpBreakevenOffset * _Point, _Digits); if(sl == 0 || sl > nSl) m_trade.PositionModify(t, nSl, m_pos.TakeProfit()); } } // C. Standard Trailing / Layered if(InpExitMode == EXIT_TRAILING_ONLY || InpUseTrailing || InpExitMode == EXIT_LAYERED_SCALE) { bool isTarget = true; if(InpExitMode == EXIT_LAYERED_SCALE) { if(type == POSITION_TYPE_BUY && t != m_summary.newestBuy) isTarget = false; if(type == POSITION_TYPE_SELL && t != m_summary.newestSell) isTarget = false; } if(isTarget && IsInProfitZone(type)) { double offset = InpExitThreshold * InpTrailPercent; double sl = m_pos.StopLoss(); if(type == POSITION_TYPE_BUY) { double nSl = NormalizeDouble(m_data.vwap / (m_data.metric + offset), _Digits); if(nSl > sl + InpTrailingStepPoints * _Point) m_trade.PositionModify(t, nSl, m_pos.TakeProfit()); } else { double nSl = NormalizeDouble(m_data.vwap / (m_data.metric - offset), _Digits); if(sl == 0 || nSl < sl - InpTrailingStepPoints * _Point) m_trade.PositionModify(t, nSl, m_pos.TakeProfit()); } } } return false; } void HandleStaggeredTrail() { ENUM_POSITION_TYPE sides[2] = {POSITION_TYPE_BUY, POSITION_TYPE_SELL}; for(int s=0; s<2; s++) { if(!IsInProfitZone(sides[s])) continue; int count = (sides[s] == POSITION_TYPE_BUY) ? m_summary.buys : m_summary.sells; if(count < 1) continue; // Collect and Sort struct PData { ulong t; double v; double sl; }; PData d[]; ArrayResize(d, count); int idx=0; for(int i=0; i d[j].v){ PData tmp = d[i]; d[i]=d[j]; d[j]=tmp; } double baseOffset = InpExitThreshold * InpTrailPercent; for(int i=0; i 1) ? (2.0 - (1.5 * (double)i / (double)(count - 1))) : 1.0; double metricOffset = baseOffset * mult; if(sides[s] == POSITION_TYPE_BUY) { double nSl = NormalizeDouble(m_data.vwap / (m_data.metric + metricOffset), _Digits); if(nSl > d[i].sl) m_trade.PositionModify(d[i].t, nSl, 0); } else { double nSl = NormalizeDouble(m_data.vwap / (m_data.metric - metricOffset), _Digits); if(d[i].sl == 0 || nSl < d[i].sl) m_trade.PositionModify(d[i].t, nSl, 0); } } } } void HandleEntryLogic() { if(m_data.isSessionEnding || m_data.isNewsImminent) return; if(InpMaxSpread > 0 && m_data.spread > InpMaxSpread) return; // Buys double nextB = 1.0 + (InpThreshold * (m_summary.buys + 1)); if(m_summary.buys < InpMaxLevels && m_data.metric >= nextB) { if(!InpUseMomentumFilter || IsMomOk(POSITION_TYPE_BUY)) { double lot = NormalizeDouble(InpBaseLot * MathPow(InpLotMultiplier, m_summary.buys), 2); double sl = (InpStopLoss > 0) ? m_data.ask - InpStopLoss * _Point : 0; double tp = (InpTakeProfit > 0) ? m_data.ask + InpTakeProfit * _Point : 0; m_trade.Buy(lot, _Symbol, m_data.ask, sl, tp, "VwapExpertB" + (string)(m_summary.buys+1)); } } // Sells double nextS = 1.0 - (InpThreshold * (m_summary.sells + 1)); if(m_summary.sells < InpMaxLevels && m_data.metric <= nextS) { if(!InpUseMomentumFilter || IsMomOk(POSITION_TYPE_SELL)) { double lot = NormalizeDouble(InpBaseLot * MathPow(InpLotMultiplier, m_summary.sells), 2); double sl = (InpStopLoss > 0) ? m_data.bid + InpStopLoss * _Point : 0; double tp = (InpTakeProfit > 0) ? m_data.bid - InpTakeProfit * _Point : 0; m_trade.Sell(lot, _Symbol, m_data.bid, sl, tp, "VwapExpertS" + (string)(m_summary.sells+1)); } } } bool IsInProfitZone(ENUM_POSITION_TYPE type) { if(type == POSITION_TYPE_BUY) return (m_data.bid > m_summary.meanBuy && m_data.bid < m_data.vwap); return (m_data.ask < m_summary.meanSell && m_data.ask > m_data.vwap); } bool IsMomOk(ENUM_POSITION_TYPE type) { double p0 = iClose(_Symbol, PERIOD_M5, 0); // use current candle vs lookback double p1 = iClose(_Symbol, PERIOD_M5, InpMomentumBars); if(p1 <= 0) return false; return (type == POSITION_TYPE_BUY) ? (p0 > p1) : (p0 < p1); } void CloseAllPositions(string reason) { Print("Closing All Positions: ", reason); for(int i=PositionsTotal()-1; i>=0; i--) if(m_pos.SelectByIndex(i) && m_pos.Magic() == 777111) m_trade.PositionClose(m_pos.Ticket()); } void RefreshUI() { static datetime lastVisual = 0; static uint lastComment = 0; bool isTester = MQLInfoInteger(MQL_TESTER); bool isVisual = MQLInfoInteger(MQL_VISUAL_MODE); // Objects if(InpShowVisuals && (!isTester || isVisual)) { datetime curM = iTime(_Symbol, InpRefreshRate, 0); if(curM != lastVisual) { UpdateObjects(); lastVisual = curM; } } // Comments if(GetTickCount() - lastComment > 500) { UpdateComments(); lastComment = GetTickCount(); } } void UpdateObjects() { DrawHLine("VC_VWAP", m_data.vwap, InpVWAPColor, STYLE_SOLID, 2, "VWAP"); DrawHLine("VC_TARGET", m_data.vwap, clrGray, STYLE_DASH, 1, "Exit Target"); // effVwap is same as vwap now for(int i=1; i<=3; i++) { DrawHLine("VC_BUY_"+(string)i, m_data.vwap / (1.0 + InpThreshold * i), InpBuyEntryColor, STYLE_DOT, 1, "Buy "+(string)i); DrawHLine("VC_SELL_"+(string)i, m_data.vwap / (1.0 - InpThreshold * i), InpSellEntryColor, STYLE_DOT, 1, "Sell "+(string)i); } } void DrawHLine(string name, double price, color clr, ENUM_LINE_STYLE style, int width, string text) { if(price <= 0) return; if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_HLINE, 0, 0, price); ObjectSetDouble(0, name, OBJPROP_PRICE, price); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_STYLE, style); ObjectSetInteger(0, name, OBJPROP_WIDTH, width); ObjectSetString(0, name, OBJPROP_TEXT, text); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_BACK, true); } void UpdateComments() { string comment = StringFormat("=== Vwap Expert [v2.0] ===\n" "VWAP: %.5f | Metric: %.5f\n" "Spread: %d | Buys: %d | Sells: %d\n" "Mean Entry: B %.5f / S %.5f\n" "News: %s\n" "Session: %s", m_data.vwap, m_data.metric, m_data.spread, m_summary.buys, m_summary.sells, m_summary.meanBuy, m_summary.meanSell, (m_data.isNewsImminent ? "!!! "+m_data.newsMsg+" !!!" : "Clear"), (m_data.isSessionEnding ? "CLOSING SOON" : TimeToString(m_data.sessionEnd))); Comment(comment); } }; //+------------------------------------------------------------------+ //| GLOBAL INSTANCE AND HANDLERS | //+------------------------------------------------------------------+ CVwapExpert expert; int OnInit() { return expert.Init() ? INIT_SUCCEEDED : INIT_FAILED; } void OnDeinit(const int reason) { expert.Deinit(); } void OnTick() { expert.OnTick(); }