mql5/Vwap to Close.mq5

486 satır
21 KiB
MQL5

//+------------------------------------------------------------------+
//| Vwap to Close.mq5 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link "https://www.mql5.com"
#property version "2.00"
#property strict
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>
//--- 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<bars; i++) {
if(rates[i].time <= m_lastBarTime) continue;
double typ = (rates[i].high + rates[i].low + rates[i].close) / 3.0;
m_sumPriceVol += typ * (double)rates[i].tick_volume;
m_sumVol += (double)rates[i].tick_volume;
m_lastBarTime = rates[i].time;
}
}
if(m_sumVol > 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<ArraySize(values); i++) {
MqlCalendarEvent ev; CalendarEventById(values[i].event_id, ev);
if(ev.importance == CALENDAR_IMPORTANCE_HIGH) {
MqlCalendarCountry co; CalendarCountryById(ev.country_id, co);
if(StringFind(_Symbol, co.currency) >= 0) { msg = ev.name + " (" + co.currency + ")"; return true; }
}
}
}
if(CalendarValueHistory(values, now - (bPost * 60), now) > 0) {
for(int i=0; i<ArraySize(values); i++) {
MqlCalendarEvent ev; CalendarEventById(values[i].event_id, ev);
if(ev.importance == CALENDAR_IMPORTANCE_HIGH) {
MqlCalendarCountry co; CalendarCountryById(ev.country_id, co);
if(StringFind(_Symbol, co.currency) >= 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<PositionsTotal(); i++) if(m_pos.SelectByIndex(i) && m_pos.Magic() == 777111 && m_pos.PositionType() == sides[s])
{ d[idx].t=m_pos.Ticket(); d[idx].v=m_pos.Volume(); d[idx].sl=m_pos.StopLoss(); idx++; }
for(int i=0; i<count-1; i++) for(int j=i+1; j<count; j++) if(d[i].v > d[j].v){ PData tmp = d[i]; d[i]=d[j]; d[j]=tmp; }
double baseOffset = InpExitThreshold * InpTrailPercent;
for(int i=0; i<count; i++) {
double mult = (count > 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(); }