mql5/Experts/VWMA_MeanReversion_EA.mq5

688 行
24 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>
//--- 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<bars; i++)
{
double typicalPrice = (rates[i].high + rates[i].low + rates[i].close) / 3.0;
sumPriceVol += typicalPrice * (double)rates[i].tick_volume;
sumVol += (double)rates[i].tick_volume;
}
if(sumVol == 0) return false;
currVWMA = sumPriceVol / sumVol;
// Standard Deviation Calculation (Volume Weighted)
double sumVariance = 0;
for(int i=0; i<bars; i++)
{
double typicalPrice = (rates[i].high + rates[i].low + rates[i].close) / 3.0;
sumVariance += MathPow(typicalPrice - currVWMA, 2) * (double)rates[i].tick_volume;
}
currStDev = MathSqrt(sumVariance / sumVol);
return (currStDev > 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<count-1; i++)
{
for(int j=i+1; j<count; j++)
{
if(pData[i].volume > 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<count; i++)
{
double ratio = 0.25;
if(count > 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());
}
}
}