//+------------------------------------------------------------------+ //| ADShorteningOfThrust.mq5 | //| Wyckoff SOT Detection | //| | //| ArraySetAsSeries(true) — index 0 = current bar (newest). | //| Loop: from oldest needed bar down to 0 (backward in index, | //| but forward in time due to reversed array series). | //| | //| Returns: +ratio = bearish impulses shortening (bullish SOT) | //| -ratio = bullish impulses shortening (bearish SOT) | //| 0 = no SOT detected | //| | //| DRAW_LINE: line drawn across all bars (including zeros) | //+------------------------------------------------------------------+ #property copyright "AD Institutional Indicators" #property link "" #property version "1.00" #property description "Shortening of Thrust — momentum exhaustion in trends" #property indicator_separate_window #property indicator_minimum -1.0 #property indicator_maximum 1.0 #property indicator_level1 0 #property indicator_buffers 4 #property indicator_plots 4 #property indicator_label1 "SOT" #property indicator_type1 DRAW_LINE #property indicator_color1 clrWhite #property indicator_width1 1 #property indicator_label2 "SOTEffortRegime" #property indicator_type2 DRAW_LINE #property indicator_color2 clrGold #property indicator_width2 1 #property indicator_label3 "SOTConfirmation" #property indicator_type3 DRAW_LINE #property indicator_color3 clrLime #property indicator_width3 1 #property indicator_label4 "SOTPushRegime" #property indicator_type4 DRAW_LINE #property indicator_color4 clrAqua #property indicator_width4 1 //--- Input parameters input int InpThrustLookback = 30; // Lookback period (10-100) input int InpMinImpulses = 3; // Min impulses for SOT (2-6) input double InpSOTThreshold = 0.30; // SOT detection threshold (0.10-0.60) input int InpContextMode = 0; // Context mode: 0=FixedBars, 1=Session input int InpSessionType = 5; // Session type: 1..10 input int InpSessionCount = 1; // Number of sessions in context (1-20) //--- Output buffer double ExtSOTBuffer[]; double ExtSOTEffortBuffer[]; double ExtSOTConfirmationBuffer[]; double ExtSOTPushBuffer[]; //--- ZigZag state machine int zzDirection = 0; // +1 tracking up, -1 tracking down, 0 uninit double zzPivotHigh = 0; double zzPivotLow = 0; datetime zzPivotHighTime = 0; datetime zzPivotLowTime = 0; //--- Impulse tracking (newest at [count-1], oldest at [0]) double ExtImpulseDists[]; double ExtImpulseVols[]; int ExtImpulseDir[]; datetime ExtImpulseTimes[]; int ExtImpulseCount = 0; //--- Leg bookkeeping and confirmation state double ExtCurrentLegVolume = 0.0; int ExtCurrentLegBars = 0; double ExtLastPivotPrice = 0.0; bool ExtHasLastPivotPrice = false; int ExtPendingConfirmOppositeDir = 0; double ExtPendingConfirmMinDist = 0.0; double ExtPendingConfirmVolumeRef = 0.0; double ExtPendingConfirmSignal = 0.0; //--- Validated params int ExtLookback; int ExtMinImp; double ExtThreshold; int ExtContextMode; int ExtSessionType; int ExtSessionCount; //+------------------------------------------------------------------+ void OnInit() { ExtLookback = (int)MathMax(10, MathMin(100, InpThrustLookback)); ExtMinImp = (int)MathMax(2, MathMin(6, InpMinImpulses)); ExtThreshold = MathMax(0.10, MathMin(0.60, InpSOTThreshold)); ExtContextMode = (InpContextMode == 1) ? 1 : 0; ExtSessionType = (int)MathMax(1, MathMin(10, InpSessionType)); ExtSessionCount = (int)MathMax(1, MathMin(20, InpSessionCount)); SetIndexBuffer(0, ExtSOTBuffer, INDICATOR_DATA); SetIndexBuffer(1, ExtSOTEffortBuffer, INDICATOR_DATA); SetIndexBuffer(2, ExtSOTConfirmationBuffer, INDICATOR_DATA); SetIndexBuffer(3, ExtSOTPushBuffer, INDICATOR_DATA); ArraySetAsSeries(ExtSOTBuffer, true); ArraySetAsSeries(ExtSOTEffortBuffer, true); ArraySetAsSeries(ExtSOTConfirmationBuffer, true); ArraySetAsSeries(ExtSOTPushBuffer, true); int maxImp = ExtLookback + 1; ArrayResize(ExtImpulseDists, maxImp); ArrayResize(ExtImpulseVols, maxImp); ArrayResize(ExtImpulseDir, maxImp); ArrayResize(ExtImpulseTimes, maxImp); ArrayInitialize(ExtImpulseDists, 0); ArrayInitialize(ExtImpulseVols, 0); ArrayInitialize(ExtImpulseDir, 0); ArrayInitialize(ExtImpulseTimes, 0); IndicatorSetInteger(INDICATOR_DIGITS, 3); IndicatorSetString(INDICATOR_SHORTNAME, "SOT(" + IntegerToString(ExtLookback) + "," + IntegerToString(ExtMinImp) + "," + DoubleToString(ExtThreshold, 2) + ")"); PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, ExtLookback); PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, ExtLookback); PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, ExtLookback); PlotIndexSetInteger(3, PLOT_DRAW_BEGIN, ExtLookback); ResetState(); } void OnDeinit(const int reason) {} void ResetState() { zzDirection = 0; zzPivotHigh = 0; zzPivotLow = 0; zzPivotHighTime = 0; zzPivotLowTime = 0; ExtImpulseCount = 0; ExtCurrentLegVolume = 0.0; ExtCurrentLegBars = 0; ExtLastPivotPrice = 0.0; ExtHasLastPivotPrice = false; ExtPendingConfirmOppositeDir = 0; ExtPendingConfirmMinDist = 0.0; ExtPendingConfirmVolumeRef = 0.0; ExtPendingConfirmSignal = 0.0; ArrayInitialize(ExtImpulseDists, 0); ArrayInitialize(ExtImpulseVols, 0); ArrayInitialize(ExtImpulseDir, 0); ArrayInitialize(ExtImpulseTimes, 0); } //+------------------------------------------------------------------+ void AgeOutOldImpulse() { if(ExtImpulseCount > ExtLookback) { for(int k = 1; k < ExtImpulseCount; k++) { ExtImpulseDists[k - 1] = ExtImpulseDists[k]; ExtImpulseVols[k - 1] = ExtImpulseVols[k]; ExtImpulseDir[k - 1] = ExtImpulseDir[k]; ExtImpulseTimes[k - 1] = ExtImpulseTimes[k]; } ExtImpulseCount--; } } //+------------------------------------------------------------------+ void RecordImpulse(double dist, int dir, double avgVol, datetime pivotTime) { if(dist <= 0) return; //--- Ensure array capacity (mirrors Java ensureCapacity) if(ExtImpulseCount >= ArraySize(ExtImpulseDists)) { int newSize = ArraySize(ExtImpulseDists) + 1; ArrayResize(ExtImpulseDists, newSize); ArrayResize(ExtImpulseVols, newSize); ArrayResize(ExtImpulseDir, newSize); ArrayResize(ExtImpulseTimes, newSize); } ExtImpulseDists[ExtImpulseCount] = dist; ExtImpulseVols[ExtImpulseCount] = MathMax(0.0, avgVol); ExtImpulseDir[ExtImpulseCount] = dir; ExtImpulseTimes[ExtImpulseCount] = pivotTime; ExtImpulseCount++; } void TrimImpulseCount(const int maxImpulses) { int cap = MathMax(1, maxImpulses); while(ExtImpulseCount > cap) { for(int k = 1; k < ExtImpulseCount; k++) { ExtImpulseDists[k - 1] = ExtImpulseDists[k]; ExtImpulseVols[k - 1] = ExtImpulseVols[k]; ExtImpulseDir[k - 1] = ExtImpulseDir[k]; ExtImpulseTimes[k - 1] = ExtImpulseTimes[k]; } ExtImpulseCount--; } } void PruneImpulsesByStartTime(const datetime minTime) { if(minTime <= 0 || ExtImpulseCount <= 0) return; int write = 0; for(int i = 0; i < ExtImpulseCount; i++) { if(ExtImpulseTimes[i] >= minTime) { ExtImpulseDists[write] = ExtImpulseDists[i]; ExtImpulseVols[write] = ExtImpulseVols[i]; ExtImpulseDir[write] = ExtImpulseDir[i]; ExtImpulseTimes[write] = ExtImpulseTimes[i]; write++; } } ExtImpulseCount = write; } int IsZigZagSessionTypeMT5(const int sessionType) { return (sessionType == 9 || sessionType == 10); } int IsPreviousSessionTypeMT5(const int sessionType) { return (sessionType >= 1 && sessionType <= 4); } int MapBaseCalendarTypeMT5(const int sessionType) { if(sessionType == 1 || sessionType == 5) return 1; // day if(sessionType == 2 || sessionType == 6) return 2; // week if(sessionType == 3 || sessionType == 7) return 3; // month if(sessionType == 4 || sessionType == 8) return 4; // year return 1; } datetime DayStartMT5(const datetime t) { MqlDateTime dt; TimeToStruct(t, dt); dt.hour = 0; dt.min = 0; dt.sec = 0; return StructToTime(dt); } datetime AddMonthsSafeMT5(const datetime t, const int months) { MqlDateTime dt; TimeToStruct(t, dt); int m = dt.mon + months; while(m > 12) { m -= 12; dt.year++; } while(m < 1) { m += 12; dt.year--; } dt.mon = m; dt.day = 1; dt.hour = 0; dt.min = 0; dt.sec = 0; return StructToTime(dt); } datetime ResolveCalendarContextStartMT5(const datetime currentTime) { int st = ExtSessionType; int baseType = MapBaseCalendarTypeMT5(st); datetime currentStart = DayStartMT5(currentTime); if(baseType == 2) { MqlDateTime w; TimeToStruct(currentStart, w); int dow = w.day_of_week; int back = (dow == 0) ? 6 : (dow - 1); currentStart -= (datetime)(back * 86400); } else if(baseType == 3) { MqlDateTime m; TimeToStruct(currentStart, m); m.day = 1; m.hour = 0; m.min = 0; m.sec = 0; currentStart = StructToTime(m); } else if(baseType == 4) { MqlDateTime y; TimeToStruct(currentStart, y); y.mon = 1; y.day = 1; y.hour = 0; y.min = 0; y.sec = 0; currentStart = StructToTime(y); } int shift = IsPreviousSessionTypeMT5(st) ? -ExtSessionCount : -(ExtSessionCount - 1); if(baseType == 1) return currentStart + (datetime)(shift * 86400); if(baseType == 2) return currentStart + (datetime)(shift * 7 * 86400); if(baseType == 3) return AddMonthsSafeMT5(currentStart, shift); return AddMonthsSafeMT5(currentStart, shift * 12); } void ApplyContextWindow(const datetime currentTime) { if(ExtContextMode != 1) { AgeOutOldImpulse(); return; } if(IsZigZagSessionTypeMT5(ExtSessionType)) { int maxImp = MathMax(ExtMinImp * 2, ExtSessionCount * MathMax(2, ExtMinImp) * 2); TrimImpulseCount(maxImp); return; } datetime start = ResolveCalendarContextStartMT5(currentTime); PruneImpulsesByStartTime(start); int minSafetyImp = MathMax(2, ExtMinImp) * 2; int maxSafetyImp = MathMax(minSafetyImp, ExtLookback + 1); TrimImpulseCount(maxSafetyImp * MathMax(1, ExtSessionCount)); } double AverageRecentImpulseVolume(const int bars) { if(ExtImpulseCount <= 0 || bars <= 0) return 0.0; int from = MathMax(0, ExtImpulseCount - bars); double sum = 0.0; int count = 0; for(int i = from; i < ExtImpulseCount; i++) { sum += ExtImpulseVols[i]; count++; } return (count > 0) ? (sum / count) : 0.0; } double UpdateConfirmation(const int newImpulseDir, const double newImpulseDist, const double newImpulseVol) { if(ExtPendingConfirmOppositeDir == 0 || ExtPendingConfirmSignal == 0.0) return 0.0; if(newImpulseDir != ExtPendingConfirmOppositeDir) return 0.0; double distGate = MathMax(0.0, ExtPendingConfirmMinDist * 0.85); double volGate = MathMax(0.0, ExtPendingConfirmVolumeRef * 1.05); if(newImpulseDist >= distGate && newImpulseVol >= volGate) { double signal = ExtPendingConfirmSignal; ExtPendingConfirmOppositeDir = 0; ExtPendingConfirmMinDist = 0.0; ExtPendingConfirmVolumeRef = 0.0; ExtPendingConfirmSignal = 0.0; return signal; } return 0.0; } struct SOTComputation { double sotValue; double effortRegime; double pushRegime; int sotDir; double latestDist; double latestVol; }; //+------------------------------------------------------------------+ //| Compute SOT — compares adjacent same-direction swing pair | //| D_n = distance of last same-direction swing | //| D_{n-1} = distance of previous same-direction swing | //| Signal: D_n < D_{n-1} i.e., thrust of current swing is shorter | //| | //| Returns: +ratio = bearish impulses shortening (bullish SOT) | //| -ratio = bullish impulses shortening (bearish SOT) | //| 0 = no SOT detected | //+------------------------------------------------------------------+ SOTComputation ComputeSOT() { SOTComputation out; out.sotValue = 0.0; out.effortRegime = 0.0; out.pushRegime = 0.0; out.sotDir = 0; out.latestDist = 0.0; out.latestVol = 0.0; if(ExtImpulseCount < ExtMinImp) return out; int lastDir = ExtImpulseDir[ExtImpulseCount - 1]; if(lastDir == 0) return out; // Find the last two same-direction impulses. // Since zigzag alternates, same-direction swings are at // positions: [count-1], [count-3], [count-5], ... double dNewest = 0; // D_n double dPrev = 0; // D_{n-1} double vNewest = 0; double vPrev = 0; int found = 0; for(int g = ExtImpulseCount - 1; g >= 0 && found < 2; g--) { if(ExtImpulseDir[g] == lastDir) { if(found == 0) { dNewest = ExtImpulseDists[g]; // D_n vNewest = ExtImpulseVols[g]; } else { dPrev = ExtImpulseDists[g]; // D_{n-1} vPrev = ExtImpulseVols[g]; } found++; } } if(found < 2 || dPrev <= 0) return out; // Ratio: how much shorter is D_n compared to D_{n-1}? double ratio = (dPrev - dNewest) / dPrev; if(!MathIsValidNumber(ratio)) return out; double directionalSign = (lastDir == -1) ? 1.0 : -1.0; out.latestDist = dNewest; out.latestVol = vNewest; out.sotDir = lastDir; // Only signal when the newest swing is at least ExtThreshold % shorter if(ratio >= ExtThreshold) { out.sotValue = directionalSign * ratio; double volRatio = (vPrev > 0.0) ? (vNewest / vPrev) : 1.0; if(volRatio >= 1.1) out.effortRegime = directionalSign; else if(volRatio <= 0.9) out.effortRegime = directionalSign * 0.5; int sameDirCount = 0; for(int i = ExtImpulseCount - 1; i >= 0; i--) { if(ExtImpulseDir[i] == lastDir) sameDirCount++; } out.pushRegime = (sameDirCount >= 4) ? directionalSign : (directionalSign * 0.5); } return out; } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total < ExtLookback + 5) return(0); // ArraySetAsSeries: index 0 = newest bar (rightmost on chart) ArraySetAsSeries(time, true); ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(close, true); // tick_volume was missing here: every other array below is series-oriented and the main loop // indexes with a series loop variable `i` (see the "Loop backward" comment below), but // tick_volume was left in its default chronological orientation - tick_volume[i] therefore read // the wrong bar's volume (severity grows toward the oldest processed bar, where it read the // NEWEST bar's volume - a genuine future-data leak into ExtCurrentLegVolume/SOTEffortRegime/ // SOTConfirmation). Flipping it here makes it consistent with every other array in this loop. ArraySetAsSeries(tick_volume, true); ArraySetAsSeries(ExtSOTBuffer, true); int startBar; // Always recompute the full series on each call. This ensures the MT5 // output matches the Java OnBarUpdate() replay of all bars from the oldest // available bar to the newest, eliminating incremental state drift. ResetState(); startBar = rates_total - 1; ArrayInitialize(ExtSOTBuffer, 0); ArrayInitialize(ExtSOTEffortBuffer, 0); ArrayInitialize(ExtSOTConfirmationBuffer, 0); ArrayInitialize(ExtSOTPushBuffer, 0); // Loop backward: oldest → newest (i decreases but time advances) for(int i = startBar; i >= 0 && !IsStopped(); i--) { double hi = high[i]; double lo = low[i]; //--- Initialize zigzag on first bar processed (oldest in calc range) if(zzDirection == 0) { zzPivotHigh = hi; zzPivotLow = lo; zzPivotHighTime = time[i]; zzPivotLowTime = time[i]; zzDirection = 1; ExtCurrentLegVolume = MathMax((double)tick_volume[i], 0.0); ExtCurrentLegBars = 1; ExtSOTBuffer[i] = 0.0; ExtSOTEffortBuffer[i] = 0.0; ExtSOTConfirmationBuffer[i] = 0.0; ExtSOTPushBuffer[i] = 0.0; continue; } ExtCurrentLegVolume += MathMax((double)tick_volume[i], 0.0); ExtCurrentLegBars++; //--- Compute swing threshold (ATR-based, 14-period) // Uses same bar indexing as Java: t counts from 0..available-1 // where t=0 is bar i (the bar being processed). // Java: for(t=0; t tr) tr = hc; if(lc > tr) tr = lc; sumTR += tr; } double atr = sumTR / available; double swingThreshold = MathMax(atr * 0.5, (hi - lo) * 0.3); if(swingThreshold <= 0) swingThreshold = (hi - lo) * 0.5; //--- ZigZag state machine bool pivotFound = false; int newPivotDir = 0; double newPivotPrice = 0.0; if(zzDirection == 1) { if(hi > zzPivotHigh) { zzPivotHigh = hi; zzPivotHighTime = time[i]; } double moveDown = zzPivotHigh - lo; if(moveDown >= swingThreshold && zzPivotHigh > 0) { newPivotDir = 1; newPivotPrice = zzPivotHigh; pivotFound = true; zzDirection = -1; zzPivotLow = lo; zzPivotLowTime = time[i]; } } else // zzDirection == -1 { if(lo < zzPivotLow) { zzPivotLow = lo; zzPivotLowTime = time[i]; } double moveUp = hi - zzPivotLow; if(moveUp >= swingThreshold && zzPivotLow > 0) { newPivotDir = -1; newPivotPrice = zzPivotLow; pivotFound = true; zzDirection = 1; zzPivotHigh = hi; zzPivotHighTime = time[i]; } } double confirmSignal = 0.0; if(pivotFound) { double avgLegVolume = (ExtCurrentLegBars > 0) ? (ExtCurrentLegVolume / ExtCurrentLegBars) : 0.0; if(ExtHasLastPivotPrice) { double dist = MathAbs(newPivotPrice - ExtLastPivotPrice); RecordImpulse(dist, newPivotDir, avgLegVolume, time[i]); ApplyContextWindow(time[i]); confirmSignal = UpdateConfirmation(newPivotDir, dist, avgLegVolume); } ExtLastPivotPrice = newPivotPrice; ExtHasLastPivotPrice = true; ExtCurrentLegVolume = 0.0; ExtCurrentLegBars = 0; } SOTComputation sot = ComputeSOT(); double sotValue = sot.sotValue; double effortRegime = sot.effortRegime; double pushRegime = sot.pushRegime; if(sotValue != 0.0 && sot.sotDir != 0) { ExtPendingConfirmOppositeDir = -sot.sotDir; ExtPendingConfirmMinDist = sot.latestDist; ExtPendingConfirmVolumeRef = MathMax(sot.latestVol, AverageRecentImpulseVolume(MathMin(ExtImpulseCount, ExtMinImp))); ExtPendingConfirmSignal = (sot.sotDir == -1) ? 1.0 : -1.0; } if(!MathIsValidNumber(sotValue)) sotValue = 0.0; if(!MathIsValidNumber(effortRegime)) effortRegime = 0.0; if(!MathIsValidNumber(confirmSignal)) confirmSignal = 0.0; if(!MathIsValidNumber(pushRegime)) pushRegime = 0.0; ExtSOTBuffer[i] = sotValue; ExtSOTEffortBuffer[i] = effortRegime; ExtSOTConfirmationBuffer[i] = confirmSignal; ExtSOTPushBuffer[i] = pushRegime; } return(rates_total); } //+------------------------------------------------------------------+