734 行
26 KiB
MQL5
734 行
26 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 <Trade\Trade.mqh>
|
|
#include <Trade\PositionInfo.mqh>
|
|
#include <Trade\SymbolInfo.mqh>
|
|
|
|
//--- 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<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;
|
|
currVWAP = sumPriceVol / sumVol;
|
|
|
|
// Standard Deviation Calculation
|
|
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 - currVWAP, 2) * (double)rates[i].tick_volume;
|
|
}
|
|
|
|
currStDev = MathSqrt(sumVariance / sumVol);
|
|
return (currStDev > 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<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(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());
|
|
}
|
|
}
|
|
}
|