//+------------------------------------------------------------------+ //| Double Top and Double Bottom.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" //--- Include the standard library for order execution #include //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum LotSizingMode { LOTS_FIXED, // Fixed lot size LOTS_RISK_PERCENT // Risk percent of balance (auto lot) }; enum TakeProfitMode { TP_MEASURED_MOVE, // Project the pattern height from the neckline TP_REWARD_RISK // Multiple of the stop distance }; enum SetupState { STATE_IDLE, // No active pattern STATE_ARMED, // Pattern found, waiting for the neckline break STATE_RETEST // Break confirmed, waiting for the pullback }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "GENERAL" input long InpMagicNumber = 1107; // Magic number (unique ID for this EA) input LotSizingMode InpLotSizingMode = LOTS_RISK_PERCENT; // Lot sizing mode input double InpFixedLots = 0.01; // Fixed lot input double InpRiskPercent = 0.5; // Risk per trade (percent of balance) input string InpOrderComment = "DTB"; // Order comment input group "PATTERN" input ENUM_TIMEFRAMES InpEntryTimeframe = PERIOD_CURRENT; // Working timeframe input int InpSwingLength = 5; // Swing pivot length (bars each side) input double InpPeakTolerancePercent = 10.0; // Max peak difference as percent of pattern height input int InpMinPatternBars = 10; // Minimum bars from first to second peak (0 = off) input double InpLegBalancePercent = 50.0; // Shorter leg as percent of longer leg, minimum (0 = off) input bool InpIncludeBreakLeg = false; // Fold the break leg into the balance test at breakout input bool InpRequirePriorTrend = false; // Require a prior trend into the pattern input group "STOP LOSS" input int InpStopBufferPoints = 3000; // Buffer beyond the pattern extreme (points) input int InpMinStopPoints = 0; // Skip the trade if the stop is closer than this (points, 0 = off) input group "TAKE PROFIT" input TakeProfitMode InpTakeProfitMode = TP_MEASURED_MOVE; // Target mode input double InpRewardRiskRatio = 1.0; // Reward to risk ratio (R:R mode) input group "ENTRY" input bool InpTradePullback = false; // Wait for the pullback to the neckline after the breakout input int InpMaxConfirmBars = 50; // Bars to wait for the neckline break input int InpMaxRetestBars = 24; // Bars to wait for the pullback input group "TRAILING STOP" input bool InpUseTrailingStop = true; // Use trailing stop input int InpMinProfitPoints = 1000; // Minimum profit to activate trailing (points) input int InpTrailPoints = 300; // Trailing distance (points) input group "LOGGING" input bool InpShowLogs = true; // Print messages to the Journal input string InpLogPrefix = "DTB> "; // Log prefix input group "VISUALS" input bool InpDrawVisuals = true; // Draw patterns on the chart input int InpMarkerSize = 10; // Peak marker size (Wingdings 3) input color InpBullColor = clrDodgerBlue; // Double bottom color input color InpBearColor = clrRed; // Double top color input color InpNecklineColor = clrGoldenrod; // Neckline color input color InpTrendColor = clrGray; // Leading trend leg color //+------------------------------------------------------------------+ //| Swing pivot point (high or low) | //+------------------------------------------------------------------+ struct SwingPoint { bool isHigh; // True for a swing high, false for a swing low double price; // Pivot price datetime time; // Pivot bar time }; //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CTrade trade; // Trade execution object int symbolDigits; // Cached symbol digits double symbolPoint; // Cached symbol point size datetime lastBarTime = 0; // Last processed bar time SwingPoint swings[]; // Rolling buffer of detected swing pivots //--- Pattern state machine int setupState = STATE_IDLE; // Current setup stage int patternDirection = 0; // Pattern direction: -1 top, +1 bottom double necklineLevel = 0.0; // Neckline price double patternExtreme = 0.0; // Pattern high (top) or low (bottom) datetime peakOneTime = 0; // Time of the first peak datetime peakTwoTime = 0; // Time of the second peak double peakOnePrice = 0.0; // Price of the first peak double peakTwoPrice = 0.0; // Price of the second peak datetime necklineTime = 0; // Time of the neckline pivot bool hasLead = false; // True when a leading trend leg exists datetime leadTime = 0; // Time of the leading trend start double leadPrice = 0.0; // Price of the leading trend start datetime necklineStartTime = 0; // Time the neckline line starts drawing datetime lastPatternEndTime = 0; // Second-peak time of the last pattern datetime breakoutTime = 0; // Time of the neckline breakout bar datetime pullbackExtremeTime = 0; // Time of the pullback extreme double pullbackExtremePrice = 0.0; // Price of the pullback extreme int barsInState = 0; // Bars elapsed in the current stage //+------------------------------------------------------------------+ //| Print a prefixed message to the Journal | //+------------------------------------------------------------------+ void Log(string message) { //--- Print only when logging is enabled if(InpShowLogs) Print(InpLogPrefix + message); } //+------------------------------------------------------------------+ //| Decide whether chart visuals may be drawn | //+------------------------------------------------------------------+ bool VisualsAllowed() { //--- Skip drawing during a non-visual backtest if(MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_VISUAL_MODE)) return false; //--- Otherwise honor the user visuals toggle return InpDrawVisuals; } //+------------------------------------------------------------------+ //| Detect the open of a new bar | //+------------------------------------------------------------------+ bool IsNewBar() { //--- Read the current bar time datetime time = iTime(_Symbol, InpEntryTimeframe, 0); //--- Report a new bar and store its time when it changes if(time != lastBarTime) { lastBarTime = time; return true; } //--- Report no new bar return false; } //+------------------------------------------------------------------+ //| Convert risk percent and stop distance into a lot size | //+------------------------------------------------------------------+ double LotsByRisk(double entry, double stop) { //--- Derive the money to risk from the account balance double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPercent / 100.0; //--- Measure the stop distance in points double stopPoints = MathAbs(entry - stop) / symbolPoint; //--- Abort on a zero stop distance if(stopPoints <= 0) return 0; //--- Read the tick value and tick size for the symbol double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); //--- Abort on invalid tick metrics if(tickValue <= 0 || tickSize <= 0) return 0; //--- Convert tick value into money per point double valuePerPoint = tickValue / tickSize * symbolPoint; //--- Abort on an invalid per-point value if(valuePerPoint <= 0) return 0; //--- Size the position so the stop loss equals the risk money double lots = riskMoney / (stopPoints * valuePerPoint); //--- Read the broker volume constraints double volumeMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double volumeMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); //--- Snap the lot size down to the volume step if(volumeStep > 0) lots = MathFloor(lots / volumeStep) * volumeStep; //--- Clamp within limits and normalize to two decimals return NormalizeDouble(MathMax(volumeMin, MathMin(volumeMax, lots)), 2); } //+------------------------------------------------------------------+ //| Resolve the lot size for a trade by the selected mode | //+------------------------------------------------------------------+ double ResolveLots(double entry, double stop) { //--- Pick fixed lots or risk-based lots by the sizing mode double lots = (InpLotSizingMode == LOTS_FIXED) ? InpFixedLots : LotsByRisk(entry, stop); //--- Read the broker volume constraints double volumeMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double volumeMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); //--- Snap the lot size down to the volume step if(volumeStep > 0) lots = MathFloor(lots / volumeStep) * volumeStep; //--- Clamp within limits and normalize to two decimals return NormalizeDouble(MathMax(volumeMin, MathMin(volumeMax, lots)), 2); } //+------------------------------------------------------------------+ //| Draw or update a trend-line segment | //+------------------------------------------------------------------+ void DrawTrend(string name, datetime t1, double p1, datetime t2, double p2, color clr, ENUM_LINE_STYLE style, int width) { //--- Create the object on first use, otherwise move both anchors if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_TREND, 0, t1, p1, t2, p2); else { //--- Move the first anchor ObjectMove(0, name, 0, t1, p1); //--- Move the second anchor ObjectMove(0, name, 1, t2, p2); } //--- Apply the line color, style and width ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_STYLE, style); ObjectSetInteger(0, name, OBJPROP_WIDTH, width); //--- Keep the line as a segment, not a ray ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false); //--- Make the object non-interactive and hidden from the list ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } //+------------------------------------------------------------------+ //| Draw or update a chart text label | //+------------------------------------------------------------------+ void DrawLabel(string name, datetime time, double price, string text, color clr, ENUM_ANCHOR_POINT anchor) { //--- Create and style the label on first use if(ObjectFind(0, name) < 0) { //--- Create the text object at the anchor point ObjectCreate(0, name, OBJ_TEXT, 0, time, price); //--- Set the font family and size ObjectSetString(0, name, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9); //--- Make the label non-interactive and hidden from the list ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } //--- Refresh the label text, color and anchor ObjectSetString(0, name, OBJPROP_TEXT, text); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor); //--- Reposition the label ObjectMove(0, name, 0, time, price); } //+------------------------------------------------------------------+ //| Draw a small up or down triangle marker | //+------------------------------------------------------------------+ void DrawMarker(string name, datetime time, double price, bool up, color clr, int anchor) { //--- Create the marker on first use, otherwise reposition it if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_TEXT, 0, time, price); else ObjectMove(0, name, 0, time, price); //--- Use the Wingdings 3 font for triangle glyphs ObjectSetString(0, name, OBJPROP_FONT, "Wingdings 3"); //--- Scale the glyph by the marker size input ObjectSetInteger(0, name, OBJPROP_FONTSIZE, InpMarkerSize); //--- Choose an up or down triangle glyph ObjectSetString(0, name, OBJPROP_TEXT, up ? "p" : "q"); //--- Apply the color and anchor ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor); //--- Make the object non-interactive and hidden from the list ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } //+------------------------------------------------------------------+ //| Draw the entry, stop and target levels with an entry arrow | //+------------------------------------------------------------------+ void DrawTradeLevels(bool isBull, datetime time, double entry, double stop, double takeProfit) { //--- Skip when visuals are disabled if(!VisualsAllowed()) return; //--- Build a per-entry id from the entry time string id = "DTB_Ent_" + IntegerToString((int)time); //--- Span the level lines a fixed number of bars to the right datetime endTime = time + (datetime)(PeriodSeconds(InpEntryTimeframe) * 30); //--- Draw the entry line DrawTrend(id + "_e", time, entry, endTime, entry, clrDodgerBlue, STYLE_SOLID, 2); //--- Draw the stop-loss line DrawTrend(id + "_sl", time, stop, endTime, stop, C'220,60,60', STYLE_DASH, 1); //--- Draw the take-profit line DrawTrend(id + "_tp", time, takeProfit, endTime, takeProfit, C'0,200,80', STYLE_DASH, 1); //--- Read the trigger bar high and low for arrow placement double barHigh = iHigh(_Symbol, InpEntryTimeframe, 1); double barLow = iLow(_Symbol, InpEntryTimeframe, 1); //--- Draw the direction arrow at the trigger bar extreme DrawMarker(id + "_a", time, isBull ? barLow : barHigh, isBull, isBull ? InpBullColor : InpBearColor, isBull ? ANCHOR_UPPER : ANCHOR_LOWER); } //+------------------------------------------------------------------+ //| Draw the full double top or bottom pattern | //+------------------------------------------------------------------+ void DrawPattern() { //--- Skip when visuals are disabled if(!VisualsAllowed()) return; //--- Build a per-pattern id from the second peak time string id = IntegerToString((int)peakTwoTime); //--- Pick the color and orientation for the pattern side color patternColor = (patternDirection < 0) ? InpBearColor : InpBullColor; bool topPattern = (patternDirection < 0); //--- Extend drawing to the current bar datetime now = iTime(_Symbol, InpEntryTimeframe, 0); //--- Draw the leading trend leg when present if(hasLead) DrawTrend("DTB_Lead_" + id, leadTime, leadPrice, peakOneTime, peakOnePrice, InpTrendColor, STYLE_DOT, 1); //--- Draw the first leg from peak one to the neckline DrawTrend("DTB_LegA_" + id, peakOneTime, peakOnePrice, necklineTime, necklineLevel, patternColor, STYLE_SOLID, 2); //--- Draw the second leg from the neckline to peak two DrawTrend("DTB_LegB_" + id, necklineTime, necklineLevel, peakTwoTime, peakTwoPrice, patternColor, STYLE_SOLID, 2); //--- Draw the horizontal neckline DrawTrend("DTB_Neck_" + id, necklineStartTime, necklineLevel, now, necklineLevel, InpNecklineColor, STYLE_DASH, 1); //--- Label the neckline DrawLabel("DTB_Neckt_" + id, necklineStartTime, necklineLevel, " Neckline", InpNecklineColor, topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); //--- Mark the first peak DrawMarker("DTB_P1_" + id, peakOneTime, peakOnePrice, !topPattern, patternColor, topPattern ? ANCHOR_LOWER : ANCHOR_UPPER); //--- Mark the second peak DrawMarker("DTB_P2_" + id, peakTwoTime, peakTwoPrice, !topPattern, patternColor, topPattern ? ANCHOR_LOWER : ANCHOR_UPPER); //--- Mark the neckline pivot DrawMarker("DTB_N_" + id, necklineTime, necklineLevel, topPattern, InpNecklineColor, topPattern ? ANCHOR_UPPER : ANCHOR_LOWER); //--- Label the first peak DrawLabel("DTB_P1t_" + id, peakOneTime, peakOnePrice, topPattern ? " First Top" : " First Bottom", patternColor, topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); //--- Label the second peak DrawLabel("DTB_P2t_" + id, peakTwoTime, peakTwoPrice, topPattern ? " Second Top" : " Second Bottom", patternColor, topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); } //+------------------------------------------------------------------+ //| Extend the neckline line to the current bar | //+------------------------------------------------------------------+ void ExtendNeckline() { //--- Skip when visuals are disabled if(!VisualsAllowed()) return; //--- Build the per-pattern id and current bar time string id = IntegerToString((int)peakTwoTime); datetime now = iTime(_Symbol, InpEntryTimeframe, 0); //--- Redraw the neckline out to the current bar DrawTrend("DTB_Neck_" + id, necklineStartTime, necklineLevel, now, necklineLevel, InpNecklineColor, STYLE_DASH, 1); } //+------------------------------------------------------------------+ //| Draw the pullback path back to the neckline | //+------------------------------------------------------------------+ void DrawPullback() { //--- Skip when visuals are disabled if(!VisualsAllowed()) return; //--- Build the per-pattern id and the touch bar time string id = IntegerToString((int)peakTwoTime); datetime touchTime = iTime(_Symbol, InpEntryTimeframe, 1); //--- Pick the pattern color color patternColor = (patternDirection < 0) ? InpBearColor : InpBullColor; //--- Draw the leg from the breakout to the pullback extreme DrawTrend("DTB_PBa_" + id, breakoutTime, necklineLevel, pullbackExtremeTime, pullbackExtremePrice, patternColor, STYLE_DASH, 1); //--- Draw the leg from the pullback extreme back to the neckline DrawTrend("DTB_PBb_" + id, pullbackExtremeTime, pullbackExtremePrice, touchTime, necklineLevel, patternColor, STYLE_DASH, 1); //--- Label the pullback DrawLabel("DTB_PBt_" + id, touchTime, necklineLevel, " Pullback", patternColor, patternDirection < 0 ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); } //+------------------------------------------------------------------+ //| Append a swing pivot and cap the rolling buffer | //+------------------------------------------------------------------+ void AddSwing(bool isHigh, double price, datetime time) { //--- Skip if this exact pivot is already stored int n = ArraySize(swings); for(int i = 0; i < n; i++) if(swings[i].time == time && swings[i].isHigh == isHigh) return; //--- Append the new pivot ArrayResize(swings, n + 1); swings[n].isHigh = isHigh; swings[n].price = price; swings[n].time = time; //--- Drop the oldest pivot once the buffer exceeds 80 entries if(ArraySize(swings) > 80) { //--- Shift entries down over the gap and shrink the array for(int i = 0; i < ArraySize(swings) - 1; i++) swings[i] = swings[i + 1]; ArrayResize(swings, ArraySize(swings) - 1); } } //+------------------------------------------------------------------+ //| Detect a confirmed swing high or low and store it | //+------------------------------------------------------------------+ void DetectSwings() { //--- Center on the candidate bar with length bars each side int length = InpSwingLength; int candidate = length + 1; //--- Require enough history to test both sides if(iBars(_Symbol, InpEntryTimeframe) < 2 * length + 2) return; //--- Read the candidate bar high and low double candidateHigh = iHigh(_Symbol, InpEntryTimeframe, candidate); double candidateLow = iLow(_Symbol, InpEntryTimeframe, candidate); //--- Assume both a high and a low pivot until a neighbor breaks it bool isHigh = true, isLow = true; //--- Compare the candidate against length bars on each side for(int k = 1; k <= length; k++) { //--- Reject the high candidate when a neighboring bar breaks it if(iHigh(_Symbol, InpEntryTimeframe, candidate - k) >= candidateHigh || iHigh(_Symbol, InpEntryTimeframe, candidate + k) > candidateHigh) isHigh = false; //--- Reject the low candidate when a neighboring bar breaks it if(iLow(_Symbol, InpEntryTimeframe, candidate - k) <= candidateLow || iLow(_Symbol, InpEntryTimeframe, candidate + k) < candidateLow) isLow = false; } //--- Store the confirmed pivot(s) datetime candidateTime = iTime(_Symbol, InpEntryTimeframe, candidate); if(isHigh) AddSwing(true, candidateHigh, candidateTime); if(isLow) AddSwing(false, candidateLow, candidateTime); } //+------------------------------------------------------------------+ //| Store a detected pattern and arm it for the neckline break | //+------------------------------------------------------------------+ void ArmPattern(int direction, double neckline, datetime neckTime, double extreme, datetime t1, double p1, datetime t2, double p2, bool lead, datetime lTime, double lPrice) { //--- Store the pattern direction patternDirection = direction; //--- Store the neckline level and its pivot time necklineLevel = neckline; necklineTime = neckTime; //--- Store the pattern extreme used as the stop anchor patternExtreme = extreme; //--- Store the two peak times and prices peakOneTime = t1; peakOnePrice = p1; peakTwoTime = t2; peakTwoPrice = p2; //--- Store the leading trend leg details hasLead = lead; leadTime = lTime; leadPrice = lPrice; //--- Remember this pattern's second peak as the last pattern end lastPatternEndTime = t2; //--- Default the neckline drawing to start at the first peak necklineStartTime = t1; //--- Move the neckline start onto the leading leg where it crosses if(hasLead && MathAbs(p1 - lPrice) > 0) { //--- Interpolate the crossing point along the leading leg double ratio = (neckline - lPrice) / (p1 - lPrice); if(ratio > 0 && ratio < 1) necklineStartTime = lTime + (datetime)((double)(t1 - lTime) * ratio); } //--- Arm the setup and reset the bar counter setupState = STATE_ARMED; barsInState = 0; //--- Log the armed pattern Log((direction < 0 ? "Double top" : "Double bottom") + " armed | neckline " + DoubleToString(neckline, symbolDigits) + " | waiting for the break"); //--- Draw the pattern DrawPattern(); } //+------------------------------------------------------------------+ //| Search the recent swings for a double top or bottom | //+------------------------------------------------------------------+ void TryDetectPattern() { //--- Only search while idle if(setupState != STATE_IDLE) return; //--- Require at least three swings to form a pattern int n = ArraySize(swings); if(n < 3) return; //--- Take the most recent swing as the second peak SwingPoint latest = swings[n - 1]; //--- Convert the peak tolerance percent to a fraction double tolerance = InpPeakTolerancePercent / 100.0; //--- Double top: the latest swing is a high if(latest.isHigh) { //--- Find the previous swing high before the latest int priorHigh = -1; for(int i = n - 2; i >= 0; i--) if(swings[i].isHigh) { priorHigh = i; break; } if(priorHigh < 0) return; //--- Reject overlap with the previous pattern if(swings[priorHigh].time < lastPatternEndTime) return; //--- Take the lowest low between the two highs as the neckline double neckline = 0; datetime neckTime = 0; bool found = false; for(int i = priorHigh + 1; i < n - 1; i++) if(!swings[i].isHigh && (!found || swings[i].price < neckline)) { neckline = swings[i].price; neckTime = swings[i].time; found = true; } if(!found) return; //--- Measure the pattern height above the neckline double peak1 = swings[priorHigh].price, peak2 = latest.price; double height = (peak1 + peak2) / 2.0 - neckline; if(height <= 0) return; //--- Reject when the two peaks differ by more than the tolerance if(MathAbs(peak1 - peak2) > tolerance * height) return; //--- Resolve the bar shifts of the two peaks and the neckline int shiftPeakOne = iBarShift(_Symbol, InpEntryTimeframe, swings[priorHigh].time, false); int shiftNeck = iBarShift(_Symbol, InpEntryTimeframe, neckTime, false); int shiftPeakTwo = iBarShift(_Symbol, InpEntryTimeframe, latest.time, false); //--- Reject when the two peaks are too close together if(InpMinPatternBars > 0 && (shiftPeakOne - shiftPeakTwo) < InpMinPatternBars) return; //--- Reject when the two legs are too unbalanced if(InpLegBalancePercent > 0) { //--- Measure the two legs in bars double legA = (double)(shiftPeakOne - shiftNeck); double legB = (double)(shiftNeck - shiftPeakTwo); if(legA <= 0 || legB <= 0) return; //--- Require the shorter leg to meet the balance percent if(100.0 * MathMin(legA, legB) / MathMax(legA, legB) < InpLegBalancePercent) return; } //--- Find a leading low before the first peak for the trend leg bool lead = false; datetime lTime = 0; double lPrice = 0; for(int i = priorHigh - 1; i >= 0; i--) if(!swings[i].isHigh) { lead = true; lTime = swings[i].time; lPrice = swings[i].price; break; } //--- Require a prior downtrend into the pattern when enabled if(InpRequirePriorTrend && (!lead || lPrice >= neckline)) return; //--- Arm the double top ArmPattern(-1, neckline, neckTime, MathMax(peak1, peak2), swings[priorHigh].time, peak1, latest.time, peak2, lead, lTime, lPrice); } //--- Double bottom: the latest swing is a low else { //--- Find the previous swing low before the latest int priorLow = -1; for(int i = n - 2; i >= 0; i--) if(!swings[i].isHigh) { priorLow = i; break; } if(priorLow < 0) return; //--- Reject overlap with the previous pattern if(swings[priorLow].time < lastPatternEndTime) return; //--- Take the highest high between the two lows as the neckline double neckline = 0; datetime neckTime = 0; bool found = false; for(int i = priorLow + 1; i < n - 1; i++) if(swings[i].isHigh && (!found || swings[i].price > neckline)) { neckline = swings[i].price; neckTime = swings[i].time; found = true; } if(!found) return; //--- Measure the pattern height below the neckline double trough1 = swings[priorLow].price, trough2 = latest.price; double height = neckline - (trough1 + trough2) / 2.0; if(height <= 0) return; //--- Reject when the two troughs differ by more than the tolerance if(MathAbs(trough1 - trough2) > tolerance * height) return; //--- Resolve the bar shifts of the two troughs and the neckline int shiftTroughOne = iBarShift(_Symbol, InpEntryTimeframe, swings[priorLow].time, false); int shiftNeck = iBarShift(_Symbol, InpEntryTimeframe, neckTime, false); int shiftTroughTwo = iBarShift(_Symbol, InpEntryTimeframe, latest.time, false); //--- Reject when the two troughs are too close together if(InpMinPatternBars > 0 && (shiftTroughOne - shiftTroughTwo) < InpMinPatternBars) return; //--- Reject when the two legs are too unbalanced if(InpLegBalancePercent > 0) { //--- Measure the two legs in bars double legA = (double)(shiftTroughOne - shiftNeck); double legB = (double)(shiftNeck - shiftTroughTwo); if(legA <= 0 || legB <= 0) return; //--- Require the shorter leg to meet the balance percent if(100.0 * MathMin(legA, legB) / MathMax(legA, legB) < InpLegBalancePercent) return; } //--- Find a leading high before the first trough for the trend leg bool lead = false; datetime lTime = 0; double lPrice = 0; for(int i = priorLow - 1; i >= 0; i--) if(swings[i].isHigh) { lead = true; lTime = swings[i].time; lPrice = swings[i].price; break; } //--- Require a prior uptrend into the pattern when enabled if(InpRequirePriorTrend && (!lead || lPrice <= neckline)) return; //--- Arm the double bottom ArmPattern(1, neckline, neckTime, MathMin(trough1, trough2), swings[priorLow].time, trough1, latest.time, trough2, lead, lTime, lPrice); } } //+------------------------------------------------------------------+ //| Reset the setup state machine to idle | //+------------------------------------------------------------------+ void ResetSetup(string reason) { //--- Log the reset with its direction and reason when active if(setupState != STATE_IDLE) Log((patternDirection < 0 ? "Double top" : "Double bottom") + " reset: " + reason + "."); //--- Clear the state, direction and bar counter setupState = STATE_IDLE; patternDirection = 0; barsInState = 0; } //+------------------------------------------------------------------+ //| Size, build SL and TP, and open the trade | //+------------------------------------------------------------------+ void OpenTrade(bool isBull) { //--- Compute the stop buffer in price double buffer = InpStopBufferPoints * symbolPoint; //--- Enter at the market on the correct side double entry = isBull ? NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), symbolDigits) : NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), symbolDigits); //--- Place the stop beyond the pattern extreme double stop = isBull ? patternExtreme - buffer : patternExtreme + buffer; //--- Reject the trade when the stop is closer than the minimum if(InpMinStopPoints > 0 && MathAbs(entry - stop) / symbolPoint < InpMinStopPoints) { ResetSetup("stop below minimum"); return; } //--- Read the broker minimum stop distance (stops level or spread) long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); long spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); double minPoints = (double)MathMax(stopsLevel, spread); if(minPoints < 1) minPoints = 1; //--- Convert the minimum distance to price double minDistance = minPoints * symbolPoint; //--- Push the stop out to the broker minimum when too tight if(isBull) { if(entry - stop < minDistance) stop = entry - minDistance; } else { if(stop - entry < minDistance) stop = entry + minDistance; } //--- Normalize the stop and measure the risk distance stop = NormalizeDouble(stop, symbolDigits); double riskDistance = MathAbs(entry - stop); //--- Abort on an invalid risk distance if(riskDistance <= 0) { ResetSetup("invalid risk distance"); return; } //--- Measure the pattern height for the measured-move target double height = MathAbs(patternExtreme - necklineLevel); //--- Build the take profit by the selected mode double takeProfit; if(InpTakeProfitMode == TP_MEASURED_MOVE) //--- Measured move: project the height from the neckline takeProfit = isBull ? necklineLevel + height : necklineLevel - height; else //--- Reward-to-risk: multiple of the stop distance takeProfit = isBull ? entry + InpRewardRiskRatio * riskDistance : entry - InpRewardRiskRatio * riskDistance; //--- Push the target out to the broker minimum when too tight if(isBull) { if(takeProfit - entry < minDistance) takeProfit = entry + minDistance; } else { if(entry - takeProfit < minDistance) takeProfit = entry - minDistance; } //--- Normalize the target takeProfit = NormalizeDouble(takeProfit, symbolDigits); //--- Resolve the lot size for this trade double lots = ResolveLots(entry, stop); //--- Abort on a lot sizing error if(lots <= 0) { ResetSetup("lot calc error"); return; } //--- Send the market order on the correct side bool ok = isBull ? trade.Buy(lots, _Symbol, entry, stop, takeProfit, InpOrderComment) : trade.Sell(lots, _Symbol, entry, stop, takeProfit, InpOrderComment); //--- Annotate and log a successful fill if(ok) { //--- Draw the entry, stop and target levels datetime now = iTime(_Symbol, InpEntryTimeframe, 0); DrawTradeLevels(isBull, now, entry, stop, takeProfit); //--- Log the fill details Log((isBull ? "BUY" : "SELL") + " filled @ " + DoubleToString(entry, symbolDigits) + " SL=" + DoubleToString(stop, symbolDigits) + " TP=" + DoubleToString(takeProfit, symbolDigits) + " lots=" + DoubleToString(lots, 2)); } else //--- Log the failure reason Log("Open failed: " + trade.ResultRetcodeDescription()); //--- Reset the setup after the attempt ResetSetup("filled"); } //+------------------------------------------------------------------+ //| Test the three legs including the break leg for balance | //+------------------------------------------------------------------+ bool BreakLegBalanced() { //--- Resolve the bar shifts of the two peaks and the neckline int shiftPeakOne = iBarShift(_Symbol, InpEntryTimeframe, peakOneTime, false); int shiftNeck = iBarShift(_Symbol, InpEntryTimeframe, necklineTime, false); int shiftPeakTwo = iBarShift(_Symbol, InpEntryTimeframe, peakTwoTime, false); //--- Measure the three legs in bars, including the break leg to now double legA = (double)(shiftPeakOne - shiftNeck); double legB = (double)(shiftNeck - shiftPeakTwo); double legC = (double)(shiftPeakTwo - 1); //--- Fail on any non-positive leg if(legA <= 0 || legB <= 0 || legC <= 0) return false; //--- Compare the shortest and longest legs double shortest = MathMin(legA, MathMin(legB, legC)); double longest = MathMax(legA, MathMax(legB, legC)); //--- Pass when the shortest leg meets the balance percent return (100.0 * shortest / longest >= InpLegBalancePercent); } //+------------------------------------------------------------------+ //| Advance the setup through the break and pullback to entry | //+------------------------------------------------------------------+ void ProgressSetup() { //--- Do nothing while idle if(setupState == STATE_IDLE) return; //--- Count another bar in the current state barsInState++; //--- Read the just-closed bar close, high and low double closePrice = iClose(_Symbol, InpEntryTimeframe, 1); double highPrice = iHigh(_Symbol, InpEntryTimeframe, 1); double lowPrice = iLow(_Symbol, InpEntryTimeframe, 1); //--- Armed stage: wait for the neckline break if(setupState == STATE_ARMED) { //--- Reset if price broke through the pattern extreme first if(patternDirection < 0 && highPrice > patternExtreme) { ResetSetup("price broke above the pattern"); return; } if(patternDirection > 0 && lowPrice < patternExtreme) { ResetSetup("price broke below the pattern"); return; } //--- Reset if the neckline break never arrived in time if(barsInState > InpMaxConfirmBars) { ResetSetup("no neckline break in time"); return; } //--- Keep the neckline extended to the current bar ExtendNeckline(); //--- Test for a close through the neckline bool confirmed = (patternDirection < 0) ? (closePrice < necklineLevel) : (closePrice > necklineLevel); //--- Handle a confirmed break if(confirmed) { //--- Optionally reject when the break leg fails the balance test if(InpIncludeBreakLeg && InpLegBalancePercent > 0 && !BreakLegBalanced()) { ResetSetup("break leg fails balance"); return; } //--- Log the confirmed break Log((patternDirection < 0 ? "Double top" : "Double bottom") + " confirmed: neckline broken at " + DoubleToString(necklineLevel, symbolDigits)); //--- Record the breakout bar time breakoutTime = iTime(_Symbol, InpEntryTimeframe, 1); //--- Draw the breakout leg and label if(VisualsAllowed()) { //--- Draw the leg from peak two to the breakout string bid = IntegerToString((int)peakTwoTime); color bcolor = (patternDirection < 0) ? InpBearColor : InpBullColor; DrawTrend("DTB_Break_" + bid, peakTwoTime, peakTwoPrice, breakoutTime, necklineLevel, bcolor, STYLE_SOLID, 2); DrawLabel("DTB_Breakt_" + bid, breakoutTime, necklineLevel, " Breakout", bcolor, patternDirection < 0 ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER); } //--- Enter immediately unless a pullback is required if(!InpTradePullback) { OpenTrade(patternDirection > 0); return; } //--- Seed the pullback extreme and wait for the retest pullbackExtremeTime = breakoutTime; pullbackExtremePrice = (patternDirection < 0) ? lowPrice : highPrice; setupState = STATE_RETEST; barsInState = 0; } //--- Stop after the armed stage return; } //--- Retest stage: wait for the pullback to the neckline if(setupState == STATE_RETEST) { //--- Reset if the pattern extreme is violated before the pullback if(patternDirection < 0 && highPrice > patternExtreme) { ResetSetup("pattern extreme violated before pullback"); return; } if(patternDirection > 0 && lowPrice < patternExtreme) { ResetSetup("pattern extreme violated before pullback"); return; } //--- Reset if the pullback never arrived in time if(barsInState > InpMaxRetestBars) { ResetSetup("no pullback in time"); return; } //--- Keep the neckline extended to the current bar ExtendNeckline(); //--- Track the deepest pullback extreme for a top if(patternDirection < 0 && lowPrice < pullbackExtremePrice) { pullbackExtremePrice = lowPrice; pullbackExtremeTime = iTime(_Symbol, InpEntryTimeframe, 1); } //--- Track the highest pullback extreme for a bottom if(patternDirection > 0 && highPrice > pullbackExtremePrice) { pullbackExtremePrice = highPrice; pullbackExtremeTime = iTime(_Symbol, InpEntryTimeframe, 1); } //--- Enter short once price pulls back up to the neckline if(patternDirection < 0 && highPrice >= necklineLevel) { DrawPullback(); OpenTrade(false); } //--- Enter long once price pulls back down to the neckline else if(patternDirection > 0 && lowPrice <= necklineLevel) { DrawPullback(); OpenTrade(true); } } } //+------------------------------------------------------------------+ //| Trail the stop on this EA's open positions | //+------------------------------------------------------------------+ void ManageTrailing() { //--- Do nothing when trailing is disabled if(!InpUseTrailingStop) return; //--- Walk every open position from last to first for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Select the position by its ticket ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) continue; //--- Skip positions from another EA if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; //--- Skip positions on another symbol if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; //--- Read the position side, entry, stop and target bool isBull = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); double entry = PositionGetDouble(POSITION_PRICE_OPEN); double curStop = PositionGetDouble(POSITION_SL); double curTP = PositionGetDouble(POSITION_TP); //--- Read the current bid and ask double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Measure open profit in points double profitPoints = isBull ? (bid - entry) / symbolPoint : (entry - ask) / symbolPoint; //--- Trail only past the activation threshold if(profitPoints >= InpMinProfitPoints + InpTrailPoints) { //--- Compute the trailed stop behind price double newStop = isBull ? bid - InpTrailPoints * symbolPoint : ask + InpTrailPoints * symbolPoint; newStop = NormalizeDouble(newStop, symbolDigits); //--- Move the stop only when it improves protection bool improves = isBull ? (newStop > curStop) : (curStop == 0 || newStop < curStop); if(improves) trade.PositionModify(ticket, newStop, curTP); } } } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Cache the symbol digits and point size symbolDigits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); symbolPoint = _Point; //--- Configure the trade object magic number and slippage trade.SetExpertMagicNumber(InpMagicNumber); trade.SetDeviationInPoints(20); //--- Clear the swing buffer ArrayResize(swings, 0); //--- Reset the setup state setupState = STATE_IDLE; patternDirection = 0; //--- Reset the pattern history anchors lastPatternEndTime = 0; necklineStartTime = 0; //--- Seed the bar-time guard lastBarTime = iTime(_Symbol, InpEntryTimeframe, 0); //--- Log a ready banner with the key settings Log("Double Top and Double Bottom EA ready on " + _Symbol + " | Magic " + IntegerToString(InpMagicNumber)); //--- Report successful initialization return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Delete our chart objects on a real removal or chart close if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_CLOSE) ObjectsDeleteAll(0, "DTB_"); //--- Clear any chart comment Comment(""); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Run per-bar logic only on a new bar if(IsNewBar()) { //--- Detect new swing pivots DetectSwings(); //--- Try to detect and arm a pattern TryDetectPattern(); //--- Advance any armed setup toward entry ProgressSetup(); //--- Flush chart updates when visuals are shown if(VisualsAllowed()) ChartRedraw(0); } //--- Trail open positions every tick ManageTrailing(); } //+------------------------------------------------------------------+