512 lines
20 KiB
MQL5
512 lines
20 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| DualFractal.mq5 |
|
|
//| Copyright 2026, Google Gemini |
|
|
//| https://www.mql5.com |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2026, Google Gemini"
|
|
#property link "https://www.mql5.com"
|
|
#property version "2.00"
|
|
#property strict
|
|
|
|
#include <Trade\Trade.mqh>
|
|
|
|
//--- Input Parameters
|
|
input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M1; // Timeframe to Trade
|
|
input double InpLots = 0.1; // Trading Lots
|
|
input long InpMagic = 123456; // Magic Number
|
|
input int InpMinSLPips = 100; // Min SL distance from Price (Pips)
|
|
input double InpTPRatio = 2.0; // Reward-to-Risk Ratio (Fallback)
|
|
input int InpSLPaddingPoints = 100; // SL Padding Buffer (Points)
|
|
//--- Volume Profile Inputs
|
|
input int InpVPStep = 10; // VP: Price Step (Ticks)
|
|
input int InpNodeRadius = 5; // VP: Node Sensitivity (Bins)
|
|
input int InpSLPipsPast = 50; // VP: SL Pips Past Node
|
|
input int InpTPPipsBefore = 50; // VP: TP Pips Before Node
|
|
input color InpHVNColor = clrRed; // VP: HVN Color
|
|
input color InpLVNColor = clrBlue; // VP: LVN Color
|
|
//--- Strategy Optimization Inputs
|
|
input ENUM_TIMEFRAMES InpHTF = PERIOD_M5; // HTF Trend Filter
|
|
input bool InpUseBreakeven = true; // Enable Breakeven
|
|
input int InpBEPlusPips = 5; // BE Profit Buffer (Pips)
|
|
input bool InpUseTrailingHVN = true; // Enable HVN Trailing SL
|
|
|
|
//--- Global Handles
|
|
int handleFractals;
|
|
int handleHTFFractals;
|
|
|
|
//--- Trading Variable
|
|
CTrade trade;
|
|
|
|
//--- State Tracking for Avoidance of Duplicates
|
|
datetime lastSignalTime = 0;
|
|
datetime lastVPRefreshTime = 0;
|
|
|
|
//--- Volume Profile Globals
|
|
double vpBins[];
|
|
double vpPrices[];
|
|
int vpNumBins = 0;
|
|
double vpMinPrice, vpMaxPrice;
|
|
struct Node {
|
|
double price;
|
|
bool isHVN;
|
|
};
|
|
Node detectedNodes[];
|
|
int nodesCount = 0;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert initialization function |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
handleFractals = iFractals(_Symbol, InpTimeframe);
|
|
handleHTFFractals = iFractals(_Symbol, InpHTF);
|
|
|
|
if(handleFractals == INVALID_HANDLE || handleHTFFractals == INVALID_HANDLE)
|
|
{
|
|
Print("Failed to create fractal handles");
|
|
return(INIT_FAILED);
|
|
}
|
|
|
|
trade.SetExpertMagicNumber(InpMagic);
|
|
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Volume Profile |
|
|
//+------------------------------------------------------------------+
|
|
void CalculateVolumeProfile()
|
|
{
|
|
int barsToAnalyze = GetBarsInCurrentWeek();
|
|
double high[], low[];
|
|
long volume[];
|
|
ArraySetAsSeries(high, true);
|
|
ArraySetAsSeries(low, true);
|
|
ArraySetAsSeries(volume, true);
|
|
|
|
if(CopyHigh(_Symbol, InpTimeframe, 0, barsToAnalyze, high) < barsToAnalyze ||
|
|
CopyLow(_Symbol, InpTimeframe, 0, barsToAnalyze, low) < barsToAnalyze ||
|
|
CopyTickVolume(_Symbol, InpTimeframe, 0, barsToAnalyze, volume) < barsToAnalyze) return;
|
|
|
|
vpMinPrice = low[ArrayMinimum(low)];
|
|
vpMaxPrice = high[ArrayMaximum(high)];
|
|
|
|
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
double stepPrice = InpVPStep * tickSize;
|
|
|
|
vpNumBins = (int)((vpMaxPrice - vpMinPrice) / stepPrice) + 1;
|
|
ArrayResize(vpBins, vpNumBins);
|
|
ArrayResize(vpPrices, vpNumBins);
|
|
ArrayInitialize(vpBins, 0);
|
|
|
|
for(int i = 0; i < vpNumBins; i++) vpPrices[i] = vpMinPrice + i * stepPrice;
|
|
|
|
for(int i = 0; i < barsToAnalyze; i++)
|
|
{
|
|
int startBin = (int)((low[i] - vpMinPrice) / stepPrice);
|
|
int endBin = (int)((high[i] - vpMinPrice) / stepPrice);
|
|
int span = endBin - startBin + 1;
|
|
double volPerBin = (double)volume[i] / (span > 0 ? span : 1);
|
|
|
|
for(int b = startBin; b <= endBin; b++)
|
|
{
|
|
if(b >= 0 && b < vpNumBins) vpBins[b] += volPerBin;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get number of bars since the start of the current week |
|
|
//+------------------------------------------------------------------+
|
|
int GetBarsInCurrentWeek()
|
|
{
|
|
datetime weekStart = iTime(_Symbol, PERIOD_W1, 0);
|
|
if(weekStart == 0) return 300; // Fallback if week data not available
|
|
int bars = iBarShift(_Symbol, InpTimeframe, weekStart);
|
|
if(bars < 0) return 1;
|
|
return bars + 1;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Detect HVNs and LVNs from the profile |
|
|
//+------------------------------------------------------------------+
|
|
void FindNodes()
|
|
{
|
|
nodesCount = 0;
|
|
ArrayResize(detectedNodes, 0);
|
|
|
|
for(int i = InpNodeRadius; i < vpNumBins - InpNodeRadius; i++)
|
|
{
|
|
bool isMax = true;
|
|
bool isMin = true;
|
|
for(int j = -InpNodeRadius; j <= InpNodeRadius; j++)
|
|
{
|
|
if(j == 0) continue;
|
|
if(vpBins[i] < vpBins[i + j]) isMax = false;
|
|
if(vpBins[i] > vpBins[i + j]) isMin = false;
|
|
}
|
|
|
|
if(isMax || isMin)
|
|
{
|
|
ArrayResize(detectedNodes, nodesCount + 1);
|
|
detectedNodes[nodesCount].price = vpPrices[i];
|
|
detectedNodes[nodesCount].isHVN = isMax;
|
|
nodesCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Visualize Nodes with Horizontal Lines |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateNodeVisuals()
|
|
{
|
|
ObjectsDeleteAll(0, "VP_NODE_");
|
|
for(int i = 0; i < nodesCount; i++)
|
|
{
|
|
string name = "VP_NODE_" + IntegerToString(i);
|
|
ObjectCreate(0, name, OBJ_HLINE, 0, 0, detectedNodes[i].price);
|
|
ObjectSetInteger(0, name, OBJPROP_COLOR, detectedNodes[i].isHVN ? InpHVNColor : InpLVNColor);
|
|
ObjectSetInteger(0, name, OBJPROP_STYLE, detectedNodes[i].isHVN ? STYLE_SOLID : STYLE_DOT);
|
|
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
|
|
ObjectSetString(0, name, OBJPROP_TOOLTIP, (detectedNodes[i].isHVN ? "HVN: " : "LVN: ") + DoubleToString(detectedNodes[i].price, _Digits));
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert deinitialization function |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
IndicatorRelease(handleFractals);
|
|
IndicatorRelease(handleHTFFractals);
|
|
ObjectsDeleteAll(0, "VP_NODE_");
|
|
Comment(""); // Clear chart dashboard
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Manage Active Trades (Breakeven & Trailing SL) |
|
|
//+------------------------------------------------------------------+
|
|
void ManageTradeManagement()
|
|
{
|
|
if(!InpUseBreakeven && !InpUseTrailingHVN) return;
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(!PositionSelectByTicket(ticket)) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != InpMagic || PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
|
|
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
double currentTP = PositionGetDouble(POSITION_TP);
|
|
long type = PositionGetInteger(POSITION_TYPE);
|
|
double currentPrice = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
|
|
//--- 1. Breakeven Logic
|
|
if(InpUseBreakeven && currentTP > 0)
|
|
{
|
|
double totalDist = MathAbs(currentTP - openPrice);
|
|
double currentProfitDist = (type == POSITION_TYPE_BUY) ? (currentPrice - openPrice) : (openPrice - currentPrice);
|
|
|
|
if(currentProfitDist >= totalDist * 0.5) // Trigger at 50% TP distance
|
|
{
|
|
double beLevel = (type == POSITION_TYPE_BUY) ? openPrice + InpBEPlusPips * 10 * _Point : openPrice - InpBEPlusPips * 10 * _Point;
|
|
// Only modify if new SL is an improvement and not already set beyond it
|
|
bool shouldModify = false;
|
|
if(type == POSITION_TYPE_BUY && (currentSL < beLevel - _Point)) shouldModify = true;
|
|
if(type == POSITION_TYPE_SELL && (currentSL > beLevel + _Point || currentSL == 0)) shouldModify = true;
|
|
|
|
if(shouldModify)
|
|
{
|
|
if(trade.PositionModify(ticket, beLevel, currentTP))
|
|
Print("Half-TP BE Applied for ticket ", ticket, " at ", beLevel);
|
|
}
|
|
}
|
|
}
|
|
|
|
//--- 2. HVN Trailing Stop
|
|
if(InpUseTrailingHVN)
|
|
{
|
|
double nodeAbove = 0, nodeBelow = 0;
|
|
if(GetNearestNodes(currentPrice, nodeAbove, nodeBelow))
|
|
{
|
|
double slOffset = InpSLPipsPast * 10 * _Point;
|
|
if(type == POSITION_TYPE_BUY)
|
|
{
|
|
double newSL = nodeBelow - slOffset;
|
|
// Only move SL UP and ensure it doesn't move past price
|
|
if(newSL > currentSL + _Point && newSL < currentPrice - slOffset)
|
|
{
|
|
trade.PositionModify(ticket, newSL, currentTP);
|
|
Print("HVN Trailing (Buy) moved SL to ", newSL);
|
|
}
|
|
}
|
|
else // SELL
|
|
{
|
|
double newSL = nodeAbove + slOffset;
|
|
// Only move SL DOWN and ensure it doesn't move past price
|
|
if((newSL < currentSL - _Point || currentSL == 0) && newSL > currentPrice + slOffset)
|
|
{
|
|
trade.PositionModify(ticket, newSL, currentTP);
|
|
Print("HVN Trailing (Sell) moved SL to ", newSL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get Nearest HVNs Above and Below Price |
|
|
//+------------------------------------------------------------------+
|
|
bool GetNearestNodes(double price, double &nodeAbove, double &nodeBelow)
|
|
{
|
|
nodeAbove = 0;
|
|
nodeBelow = 0;
|
|
double minAbove = 999999, minBelow = 999999;
|
|
|
|
for(int i = 0; i < nodesCount; i++)
|
|
{
|
|
if(!detectedNodes[i].isHVN) continue; // Only HVNs for SL/TP nodes
|
|
|
|
double diff = detectedNodes[i].price - price;
|
|
if(diff > 0 && diff < minAbove) { nodeAbove = detectedNodes[i].price; minAbove = diff; }
|
|
if(diff < 0 && MathAbs(diff) < minBelow) { nodeBelow = detectedNodes[i].price; minBelow = MathAbs(diff); }
|
|
}
|
|
return (nodeAbove > 0 && nodeBelow > 0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert tick function |
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
// 1. Update Volume Profile and Nodes (Once per bar)
|
|
datetime currentBarTime = iTime(_Symbol, InpTimeframe, 0);
|
|
if(currentBarTime != lastVPRefreshTime)
|
|
{
|
|
CalculateVolumeProfile();
|
|
FindNodes();
|
|
UpdateNodeVisuals();
|
|
lastVPRefreshTime = currentBarTime;
|
|
}
|
|
|
|
// 2. Manage Active Trades (Trailing Stop, Breakeven)
|
|
ManageTradeManagement();
|
|
|
|
// 3. Dashboard Status
|
|
UpdateDashboard();
|
|
|
|
// 4. Only one position at a time for this EA
|
|
if(HasOpenPosition()) return;
|
|
|
|
int dir = 0;
|
|
datetime sigTime = 0;
|
|
int sigIndex = -1;
|
|
|
|
// Get last confirmed fractal on the trading timeframe
|
|
dir = GetLastFractal(_Symbol, InpTimeframe, handleFractals, sigTime, sigIndex);
|
|
|
|
// Trading Logic: Trigger Entry
|
|
if(dir != 0)
|
|
{
|
|
// Ensure we don't open multiple trades on the same fractal
|
|
if(sigTime == lastSignalTime) return;
|
|
|
|
// HTF Trend Filter Check
|
|
datetime htfTime;
|
|
int htfIdx;
|
|
int htfDir = GetLastFractal(_Symbol, InpHTF, handleHTFFractals, htfTime, htfIdx);
|
|
|
|
if(htfDir != dir) // Trend filter: HTF must match MTF direction
|
|
{
|
|
// Optional: Print("Entry Blocked: HTF Trend (", htfDir, ") does not match signal (", dir, ")");
|
|
return;
|
|
}
|
|
|
|
MqlTick lastTick;
|
|
if(!SymbolInfoTick(_Symbol, lastTick)) return;
|
|
|
|
double sl = 0, tp = 0;
|
|
double nodeAbove = 0, nodeBelow = 0;
|
|
double slOffset = InpSLPipsPast * 10 * _Point;
|
|
double tpOffset = InpTPPipsBefore * 10 * _Point;
|
|
|
|
if(GetNearestNodes(lastTick.bid, nodeAbove, nodeBelow))
|
|
{
|
|
if(dir == 1) // Signal is BUY (triggered by Down Fractal/Support)
|
|
{
|
|
sl = nodeBelow - slOffset;
|
|
tp = nodeAbove - tpOffset;
|
|
|
|
double risk = lastTick.ask - sl;
|
|
double reward = tp - lastTick.ask;
|
|
|
|
if(reward >= risk && risk > 0)
|
|
{
|
|
if(trade.Buy(InpLots, _Symbol, lastTick.ask, sl, tp, "VP Node Entry (Filtered)"))
|
|
{
|
|
lastSignalTime = sigTime;
|
|
Print("Buy Order Placed SL:", sl, " TP:", tp, " R/R:", reward/risk, " HTF Match");
|
|
}
|
|
}
|
|
}
|
|
else if(dir == -1) // Signal is SELL (triggered by Up Fractal/Resistance)
|
|
{
|
|
sl = nodeAbove + slOffset;
|
|
tp = nodeBelow + tpOffset;
|
|
|
|
double risk = sl - lastTick.bid;
|
|
double reward = lastTick.bid - tp;
|
|
|
|
if(reward >= risk && risk > 0)
|
|
{
|
|
if(trade.Sell(InpLots, _Symbol, lastTick.bid, sl, tp, "VP Node Entry (Filtered)"))
|
|
{
|
|
lastSignalTime = sigTime;
|
|
Print("Sell Order Placed SL:", sl, " TP:", tp, " R/R:", reward/risk, " HTF Match");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get direction and time of last confirmed fractal |
|
|
//+------------------------------------------------------------------+
|
|
int GetLastFractal(string sym, ENUM_TIMEFRAMES period, int handle, datetime &outTime, int &outIndex)
|
|
{
|
|
double upper[], lower[];
|
|
ArraySetAsSeries(upper, true);
|
|
ArraySetAsSeries(lower, true);
|
|
|
|
if(CopyBuffer(handle, 0, 0, 1000, upper) <= 0) return 0;
|
|
if(CopyBuffer(handle, 1, 0, 1000, lower) <= 0) return 0;
|
|
|
|
for(int i = 2; i < 1000; i++)
|
|
{
|
|
bool isUp = (upper[i] < EMPTY_VALUE);
|
|
bool isDown = (lower[i] < EMPTY_VALUE);
|
|
|
|
if(isUp || isDown)
|
|
{
|
|
outTime = iTime(sym, period, i);
|
|
outIndex = i;
|
|
return isDown ? 1 : -1; // Down Fractal (Support) = BUY (1), Up Fractal (Resistance) = SELL (-1)
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Find a fractal SL that satisfies the minimum distance rule |
|
|
//+------------------------------------------------------------------+
|
|
bool GetSafeFractalSL(ENUM_TIMEFRAMES period, int handle, int direction, double priceNow, double minDistancePoints, int paddingPoints, double &outSL)
|
|
{
|
|
// direction: 1 for Buy (look for Trough/Low), -1 for Sell (look for Peak/High)
|
|
double buf[];
|
|
ArraySetAsSeries(buf, true);
|
|
int bufferIdx = (direction == 1) ? 1 : 0; // Buy needs Lower (1), Sell needs Upper (0)
|
|
|
|
if(CopyBuffer(handle, bufferIdx, 0, 1000, buf) <= 0) return false;
|
|
|
|
for(int i = 2; i < 1000; i++)
|
|
{
|
|
if(buf[i] < EMPTY_VALUE)
|
|
{
|
|
double extreme = (direction == 1) ? iLow(_Symbol, period, i) : iHigh(_Symbol, period, i);
|
|
double slCandidate = (direction == 1) ? (extreme - paddingPoints * _Point) : (extreme + paddingPoints * _Point);
|
|
|
|
double dist = (direction == 1) ? (priceNow - slCandidate) : (slCandidate - priceNow);
|
|
if(dist >= minDistancePoints || minDistancePoints == 0)
|
|
{
|
|
outSL = slCandidate;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Manage Trailing Stop based on new confirming fractals |
|
|
//+------------------------------------------------------------------+
|
|
void CheckTrailingStop()
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(!PositionSelectByTicket(ticket)) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != InpMagic || PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
|
|
long type = PositionGetInteger(POSITION_TYPE);
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
|
|
datetime dummyTime;
|
|
int dummyIdx;
|
|
int dir = GetLastFractal(_Symbol, InpTimeframe, handleFractals, dummyTime, dummyIdx);
|
|
|
|
if(type == POSITION_TYPE_BUY && dir == -1) // Buy position, last fractal is Down (Support)
|
|
{
|
|
double newSL;
|
|
if(GetSafeFractalSL(InpTimeframe, handleFractals, 1, PositionGetDouble(POSITION_PRICE_OPEN), 0, InpSLPaddingPoints, newSL))
|
|
{
|
|
if(newSL > currentSL + _Point)
|
|
{
|
|
trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
|
|
Print("Trailing SL (Buy) moved higher to:", newSL);
|
|
}
|
|
}
|
|
}
|
|
else if(type == POSITION_TYPE_SELL && dir == 1) // Sell position, last fractal is Up (Resistance)
|
|
{
|
|
double newSL;
|
|
if(GetSafeFractalSL(InpTimeframe, handleFractals, -1, PositionGetDouble(POSITION_PRICE_OPEN), 0, InpSLPaddingPoints, newSL))
|
|
{
|
|
if(newSL < currentSL - _Point || currentSL == 0)
|
|
{
|
|
trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
|
|
Print("Trailing SL (Sell) moved lower to:", newSL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check for open positions for this EA |
|
|
//+------------------------------------------------------------------+
|
|
bool HasOpenPosition()
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(PositionSelectByTicket(ticket))
|
|
{
|
|
if(PositionGetInteger(POSITION_MAGIC) == InpMagic && PositionGetString(POSITION_SYMBOL) == _Symbol)
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update the chart dashboard |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateDashboard()
|
|
{
|
|
datetime time;
|
|
int idx;
|
|
int dir = GetLastFractal(_Symbol, InpTimeframe, handleFractals, time, idx);
|
|
string sigStr = (dir == 1) ? "BUY (Support)" : (dir == -1 ? "SELL (Resistance)" : "NONE");
|
|
|
|
int barsUsed = GetBarsInCurrentWeek();
|
|
|
|
Comment("Timeframe: ", EnumToString(InpTimeframe),
|
|
"\nBars in Week: ", barsUsed,
|
|
"\nLast Signal: ", sigStr,
|
|
"\nSignal Time: ", time);
|
|
}
|
|
//+------------------------------------------------------------------+
|