mql5/Experts/DualFractal.mq5

512 Zeilen
20 KiB
MQL5

2026-01-29 16:38:34 +03:00
//+------------------------------------------------------------------+
//| 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)
2026-01-30 15:40:15 +03:00
input double InpTPRatio = 2.0; // Reward-to-Risk Ratio (Fallback)
2026-01-29 16:38:34 +03:00
input int InpSLPaddingPoints = 100; // SL Padding Buffer (Points)
2026-01-30 15:40:15 +03:00
//--- 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
2026-01-29 16:38:34 +03:00
//--- Global Handles
int handleFractals;
2026-01-30 15:40:15 +03:00
int handleHTFFractals;
2026-01-29 16:38:34 +03:00
//--- Trading Variable
CTrade trade;
//--- State Tracking for Avoidance of Duplicates
datetime lastSignalTime = 0;
2026-01-30 15:40:15 +03:00
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;
2026-01-29 16:38:34 +03:00
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
handleFractals = iFractals(_Symbol, InpTimeframe);
2026-01-30 15:40:15 +03:00
handleHTFFractals = iFractals(_Symbol, InpHTF);
2026-01-29 16:38:34 +03:00
2026-01-30 15:40:15 +03:00
if(handleFractals == INVALID_HANDLE || handleHTFFractals == INVALID_HANDLE)
2026-01-29 16:38:34 +03:00
{
2026-01-30 15:40:15 +03:00
Print("Failed to create fractal handles");
2026-01-29 16:38:34 +03:00
return(INIT_FAILED);
}
trade.SetExpertMagicNumber(InpMagic);
return(INIT_SUCCEEDED);
}
2026-01-30 15:40:15 +03:00
//+------------------------------------------------------------------+
//| 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));
}
}
2026-01-29 16:38:34 +03:00
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
IndicatorRelease(handleFractals);
2026-01-30 15:40:15 +03:00
IndicatorRelease(handleHTFFractals);
ObjectsDeleteAll(0, "VP_NODE_");
2026-01-29 16:38:34 +03:00
Comment(""); // Clear chart dashboard
}
2026-01-30 15:40:15 +03:00
//+------------------------------------------------------------------+
//| 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);
}
2026-01-29 16:38:34 +03:00
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
2026-01-30 15:40:15 +03:00
// 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();
2026-01-29 16:38:34 +03:00
2026-01-30 15:40:15 +03:00
// 3. Dashboard Status
2026-01-29 16:38:34 +03:00
UpdateDashboard();
2026-01-30 15:40:15 +03:00
// 4. Only one position at a time for this EA
2026-01-29 16:38:34 +03:00
if(HasOpenPosition()) return;
int dir = 0;
datetime sigTime = 0;
int sigIndex = -1;
2026-01-30 15:40:15 +03:00
// Get last confirmed fractal on the trading timeframe
2026-01-29 16:38:34 +03:00
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;
2026-01-30 15:40:15 +03:00
// 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;
}
2026-01-29 16:38:34 +03:00
MqlTick lastTick;
if(!SymbolInfoTick(_Symbol, lastTick)) return;
double sl = 0, tp = 0;
2026-01-30 15:40:15 +03:00
double nodeAbove = 0, nodeBelow = 0;
double slOffset = InpSLPipsPast * 10 * _Point;
double tpOffset = InpTPPipsBefore * 10 * _Point;
2026-01-29 16:38:34 +03:00
2026-01-30 15:40:15 +03:00
if(GetNearestNodes(lastTick.bid, nodeAbove, nodeBelow))
2026-01-29 16:38:34 +03:00
{
2026-01-30 15:40:15 +03:00
if(dir == 1) // Signal is BUY (triggered by Down Fractal/Support)
2026-01-29 16:38:34 +03:00
{
2026-01-30 15:40:15 +03:00
sl = nodeBelow - slOffset;
tp = nodeAbove - tpOffset;
double risk = lastTick.ask - sl;
double reward = tp - lastTick.ask;
if(reward >= risk && risk > 0)
2026-01-29 16:38:34 +03:00
{
2026-01-30 15:40:15 +03:00
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");
}
2026-01-29 16:38:34 +03:00
}
}
2026-01-30 15:40:15 +03:00
else if(dir == -1) // Signal is SELL (triggered by Up Fractal/Resistance)
2026-01-29 16:38:34 +03:00
{
2026-01-30 15:40:15 +03:00
sl = nodeAbove + slOffset;
tp = nodeBelow + tpOffset;
double risk = sl - lastTick.bid;
double reward = lastTick.bid - tp;
if(reward >= risk && risk > 0)
2026-01-29 16:38:34 +03:00
{
2026-01-30 15:40:15 +03:00
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");
}
2026-01-29 16:38:34 +03:00
}
}
}
}
}
//+------------------------------------------------------------------+
//| 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;
2026-01-30 15:40:15 +03:00
return isDown ? 1 : -1; // Down Fractal (Support) = BUY (1), Up Fractal (Resistance) = SELL (-1)
2026-01-29 16:38:34 +03:00
}
}
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);
2026-01-30 15:40:15 +03:00
string sigStr = (dir == 1) ? "BUY (Support)" : (dir == -1 ? "SELL (Resistance)" : "NONE");
int barsUsed = GetBarsInCurrentWeek();
2026-01-29 16:38:34 +03:00
Comment("Timeframe: ", EnumToString(InpTimeframe),
2026-01-30 15:40:15 +03:00
"\nBars in Week: ", barsUsed,
2026-01-29 16:38:34 +03:00
"\nLast Signal: ", sigStr,
"\nSignal Time: ", time);
}
//+------------------------------------------------------------------+