//+------------------------------------------------------------------+ //| 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 //--- 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); } //+------------------------------------------------------------------+