Page 3 of 3

Re: Most complex forex strategy

Posted: Wed Oct 14, 2026 3:37 pm
by PTScalper
Below are two complete, non-repainting implementations: a cTrader (cAlgo C# / .NET 6) indicator that also inspects intrabar M1 volume concentration inside the M15 wick, followed by a fast MetaTrader 5 (MQL5) version with iCustom-ready signal buffers.

Re: Most complex forex strategy

Posted: Wed Oct 14, 2026 3:37 pm
by PTScalper
1. cTrader Implementation (cAlgo C# / .NET 6)This indicator tracks active swing pools, validates ATR-bounded stop raids, calculates the M15 Tick-Volume $Z$-Score, and queries the underlying M1 bars inside the M15 candle to verify that heavy volume actually traded inside the swept wick zone.

Code: Select all

using System;
using System.Collections.Generic;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Indicators
{
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class LiquiditySweepAbsorption : Indicator
    {
        #region Parameters

        [Parameter("Swing Pivot Strength (Bars)", DefaultValue = 5, MinValue = 2, MaxValue = 20, Group = "1. Liquidity Pool Geometry")]
        public int SwingStrength { get; set; }

        [Parameter("Max Pool Age (M15 Bars)", DefaultValue = 120, MinValue = 20, MaxValue = 500, Group = "1. Liquidity Pool Geometry")]
        public int MaxPoolAgeBars { get; set; }

        [Parameter("Min Sweep Depth (ATR Mult)", DefaultValue = 0.10, MinValue = 0.01, MaxValue = 1.0, Step = 0.05, Group = "1. Liquidity Pool Geometry")]
        public double MinSweepAtr { get; set; }

        [Parameter("Max Sweep Depth (ATR Mult)", DefaultValue = 1.25, MinValue = 0.30, MaxValue = 3.0, Step = 0.05, Group = "1. Liquidity Pool Geometry")]
        public double MaxSweepAtr { get; set; }

        [Parameter("Volume Lookback Period", DefaultValue = 20, MinValue = 5, MaxValue = 100, Group = "2. Tick Absorption Filter")]
        public int VolumePeriod { get; set; }

        [Parameter("Min Tick Volume Z-Score", DefaultValue = 1.25, MinValue = 0.0, MaxValue = 4.0, Step = 0.1, Group = "2. Tick Absorption Filter")]
        public double MinVolumeZScore { get; set; }

        [Parameter("Min Rejection Wick Ratio (0-1)", DefaultValue = 0.42, MinValue = 0.20, MaxValue = 0.85, Step = 0.02, Group = "2. Tick Absorption Filter")]
        public double MinWickRatio { get; set; }

        [Parameter("Verify M1 Intrabar Wick Volume", DefaultValue = true, Group = "2. Tick Absorption Filter")]
        public bool UseM1IntrabarCheck { get; set; }

        [Parameter("Min M1 Wick Volume Share (0-1)", DefaultValue = 0.35, MinValue = 0.10, MaxValue = 0.80, Step = 0.05, Group = "2. Tick Absorption Filter")]
        public double MinM1WickVolumeShare { get; set; }

        [Parameter("Enable Sound Alert on Close", DefaultValue = true, Group = "3. Alerts & Visuals")]
        public bool EnableSoundAlert { get; set; }

        [Parameter("Draw Swept Liquidity Rays", DefaultValue = true, Group = "3. Alerts & Visuals")]
        public bool DrawLiquidityRays { get; set; }

        [Output("Bullish Sweep Signal", LineColor = "Lime", PlotType = PlotType.Points, Thickness = 5)]
        public IndicatorDataSeries BullishSignal { get; set; }

        [Output("Bearish Sweep Signal", LineColor = "Red", PlotType = PlotType.Points, Thickness = 5)]
        public IndicatorDataSeries BearishSignal { get; set; }

        #endregion

        #region Internal State

        private sealed class LiquidityPool
        {
            public int BarIndex { get; init; }
            public double Price { get; init; }
            public bool IsSwingHigh { get; init; }
            public bool IsMitigated { get; set; }
        }

        private readonly List<LiquidityPool> _activePools = new();
        private AverageTrueRange _atr;
        private Bars _m1Bars;
        private int _lastProcessedBar = -1;

        #endregion

        protected override void Initialize()
        {
            _atr = Indicators.AverageTrueRange(14, MovingAverageType.Exponential);

            if (UseM1IntrabarCheck && Bars.TimeFrame > TimeFrame.Minute)
            {
                _m1Bars = MarketData.GetBars(TimeFrame.Minute);
            }
        }

        public override void Calculate(int index)
        {
            BullishSignal[index] = double.NaN;
            BearishSignal[index] = double.NaN;

            // Process only once per closed bar (index - 1) to guarantee zero repainting
            int closedIndex = index - 1;
            if (closedIndex <= SwingStrength * 2 + VolumePeriod || closedIndex <= _lastProcessedBar)
                return;

            _lastProcessedBar = closedIndex;

            // 1. Register newly confirmed Swing High / Swing Low at (closedIndex - SwingStrength)
            int pivotIndex = closedIndex - SwingStrength;
            RegisterConfirmedPools(pivotIndex);

            // 2. Prune expired pools and evaluate sweeps on the newly closed bar
            EvaluatePoolSweepsAndMitigation(closedIndex);
        }

        private void RegisterConfirmedPools(int pivotIndex)
        {
            double candidateHigh = Bars.HighPrices[pivotIndex];
            double candidateLow = Bars.LowPrices[pivotIndex];
            bool isSwingHigh = true;
            bool isSwingLow = true;

            for (int offset = 1; offset <= SwingStrength; offset++)
            {
                if (Bars.HighPrices[pivotIndex - offset] >= candidateHigh ||
                    Bars.HighPrices[pivotIndex + offset] > candidateHigh)
                {
                    isSwingHigh = false;
                }

                if (Bars.LowPrices[pivotIndex - offset] <= candidateLow ||
                    Bars.LowPrices[pivotIndex + offset] < candidateLow)
                {
                    isSwingLow = false;
                }
            }

            if (isSwingHigh)
            {
                _activePools.Add(new LiquidityPool
                {
                    BarIndex = pivotIndex,
                    Price = candidateHigh,
                    IsSwingHigh = true,
                    IsMitigated = false
                });
            }

            if (isSwingLow)
            {
                _activePools.Add(new LiquidityPool
                {
                    BarIndex = pivotIndex,
                    Price = candidateLow,
                    IsSwingHigh = false,
                    IsMitigated = false
                });
            }
        }

        private void EvaluatePoolSweepsAndMitigation(int barIndex)
        {
            double open = Bars.OpenPrices[barIndex];
            double high = Bars.HighPrices[barIndex];
            double low = Bars.LowPrices[barIndex];
            double close = Bars.ClosePrices[barIndex];
            double range = high - low;
            double atr = _atr.Result[barIndex];

            if (range <= Symbol.TickSize || atr <= 0)
                return;

            double volZScore = CalculateTickVolumeZScore(barIndex);
            LiquidityPool bestBullishSweep = null;
            LiquidityPool bestBearishSweep = null;

            for (int i = _activePools.Count - 1; i >= 0; i--)
            {
                LiquidityPool pool = _activePools[i];

                // Expire old liquidity pools
                if (barIndex - pool.BarIndex > MaxPoolAgeBars || pool.IsMitigated)
                {
                    _activePools.RemoveAt(i);
                    continue;
                }

                if (pool.IsSwingHigh)
                {
                    // Full-body breakout invalidates the pool without triggering a reversal sweep
                    if (close > pool.Price)
                    {
                        pool.IsMitigated = true;
                        _activePools.RemoveAt(i);
                        continue;
                    }

                    // Check Buy-Side Liquidity Raid (High pierces above pool, Close snaps back below)
                    double penetration = high - pool.Price;
                    if (penetration >= MinSweepAtr * atr && penetration <= MaxSweepAtr * atr && close < pool.Price)
                    {
                        pool.IsMitigated = true;
                        bestBearishSweep = pool;
                        _activePools.RemoveAt(i);
                    }
                }
                else
                {
                    // Full-body breakdown invalidates the pool
                    if (close < pool.Price)
                    {
                        pool.IsMitigated = true;
                        _activePools.RemoveAt(i);
                        continue;
                    }

                    // Check Sell-Side Liquidity Raid (Low pierces below pool, Close snaps back above)
                    double penetration = pool.Price - low;
                    if (penetration >= MinSweepAtr * atr && penetration <= MaxSweepAtr * atr && close > pool.Price)
                    {
                        pool.IsMitigated = true;
                        bestBullishSweep = pool;
                        _activePools.RemoveAt(i);
                    }
                }
            }

            // 3. Apply Layer 5 Tick-Volume Absorption Verification
            if (bestBullishSweep != null)
            {
                double lowerWick = Math.Min(open, close) - low;
                double wickRatio = lowerWick / range;
                bool m1Confirmed = VerifyM1WickAbsorption(barIndex, bestBullishSweep.Price, isBullish: true);

                if (volZScore >= MinVolumeZScore && wickRatio >= MinWickRatio && m1Confirmed)
                {
                    double absorptionScore = Math.Round((volZScore + 1.0) * (wickRatio * 100.0), 1);
                    BullishSignal[barIndex] = low - (atr * 0.25);
                    RenderSweepVisuals(barIndex, bestBullishSweep, isBullish: true, absorptionScore, atr);
                }
            }

            if (bestBearishSweep != null)
            {
                double upperWick = high - Math.Max(open, close);
                double wickRatio = upperWick / range;
                bool m1Confirmed = VerifyM1WickAbsorption(barIndex, bestBearishSweep.Price, isBullish: false);

                if (volZScore >= MinVolumeZScore && wickRatio >= MinWickRatio && m1Confirmed)
                {
                    double absorptionScore = Math.Round((volZScore + 1.0) * (wickRatio * 100.0), 1);
                    BearishSignal[barIndex] = high + (atr * 0.25);
                    RenderSweepVisuals(barIndex, bestBearishSweep, isBullish: false, absorptionScore, atr);
                }
            }
        }

        private double CalculateTickVolumeZScore(int barIndex)
        {
            double sum = 0.0;
            for (int i = 0; i < VolumePeriod; i++)
                sum += Bars.TickVolumes[barIndex - i];

            double mean = sum / VolumePeriod;

            double varianceSum = 0.0;
            for (int i = 0; i < VolumePeriod; i++)
            {
                double diff = Bars.TickVolumes[barIndex - i] - mean;
                varianceSum += diff * diff;
            }

            double stdDev = Math.Sqrt(varianceSum / VolumePeriod);
            if (stdDev <= 1e-5)
                return 0.0;

            return (Bars.TickVolumes[barIndex] - mean) / stdDev;
        }

        private bool VerifyM1WickAbsorption(int m15Index, double sweptPoolPrice, bool isBullish)
        {
            if (!UseM1IntrabarCheck || _m1Bars == null || _m1Bars.Count == 0)
                return true;

            DateTime barStart = Bars.OpenTimes[m15Index];
            DateTime barEnd = m15Index + 1 < Bars.Count
                ? Bars.OpenTimes[m15Index + 1]
                : barStart.AddMinutes(15);

            int startM1Idx = _m1Bars.OpenTimes.GetIndexByTime(barStart);
            if (startM1Idx < 0 || _m1Bars.OpenTimes[startM1Idx] < barStart.AddMinutes(-15))
                return true; // Fallback gracefully if M1 history does not reach back far enough

            double totalM1Volume = 0;
            double wickZoneVolume = 0;

            for (int m1 = startM1Idx; m1 < _m1Bars.Count; m1++)
            {
                DateTime t = _m1Bars.OpenTimes[m1];
                if (t < barStart) continue;
                if (t >= barEnd) break;

                double v = _m1Bars.TickVolumes[m1];
                totalM1Volume += v;

                // Measure volume traded specifically during M1 bars that interacted with the liquidity sweep zone
                if (isBullish && _m1Bars.LowPrices[m1] <= sweptPoolPrice)
                    wickZoneVolume += v;
                else if (!isBullish && _m1Bars.HighPrices[m1] >= sweptPoolPrice)
                    wickZoneVolume += v;
            }

            if (totalM1Volume <= 0)
                return true;

            double wickVolumeShare = wickZoneVolume / totalM1Volume;
            return wickVolumeShare >= MinM1WickVolumeShare;
        }

        private void RenderSweepVisuals(int barIndex, LiquidityPool pool, bool isBullish, double score, double atr)
        {
            string idPrefix = $"LSA_{barIndex}_{(isBullish ? "B" : "S")}";
            Color color = isBullish ? Color.Lime : Color.Crimson;

            if (DrawLiquidityRays)
            {
                var line = Chart.DrawTrendLine(
                    $"{idPrefix}_Ray",
                    Bars.OpenTimes[pool.BarIndex],
                    pool.Price,
                    Bars.OpenTimes[barIndex],
                    pool.Price,
                    color,
                    1,
                    LineStyle.DotsRare);
                line.IsInteractive = false;
            }

            double labelY = isBullish
                ? Bars.LowPrices[barIndex] - (atr * 0.45)
                : Bars.HighPrices[barIndex] + (atr * 0.45);

            var text = Chart.DrawText(
                $"{idPrefix}_Txt",
                $"ABS [{score}]",
                Bars.OpenTimes[barIndex],
                labelY,
                color);
            text.HorizontalAlignment = HorizontalAlignment.Center;
            text.FontSize = 8;

            if (EnableSoundAlert && IsLastBar)
            {
                Notifications.PlaySound(SoundType.Confirmation);
                Print("{0} Liquidity Sweep + Absorption on {1} ({2}) | Pool: {3:F5} | Score: {4}",
                    isBullish ? "BULLISH" : "BEARISH",
                    Symbol.Name,
                    TimeFrame,
                    pool.Price,
                    score);
            }
        }
    }
}

Re: Most complex forex strategy

Posted: Wed Oct 14, 2026 3:37 pm
by PTScalper
2. MetaTrader 5 Implementation (MQL5)For MetaTrader 5, this script uses a fixed-size ring buffer of active swing structures inside OnCalculate(), evaluates tick_volume $Z$-Scores and ATR penetration bounds on bar close, and exposes two indicator buffers (BullishSweepBuffer and BearishSweepBuffer) so an Expert Advisor can read signals via iCustom().

Code: Select all

//+------------------------------------------------------------------+
//|                                   LiquiditySweepAbsorption.mq5   |
//|         M15 Institutional Liquidity Sweep & Tick Absorption      |
//+------------------------------------------------------------------+
#property copyright "Quant Architecture"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2

//--- Plot 1: Bullish Sweep + Absorption
#property indicator_label1  "Bullish Absorption Sweep"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrLime
#property indicator_width1  2

//--- Plot 2: Bearish Sweep + Absorption
#property indicator_label2  "Bearish Absorption Sweep"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrCrimson
#property indicator_width2  2

//--- Input Parameters
input int    InpSwingStrength   = 5;      // Swing Pivot Strength (Left/Right Bars)
input int    InpMaxPoolAge      = 120;    // Max Liquidity Pool Age (Bars)
input double InpMinSweepAtr     = 0.10;   // Min Sweep Penetration (ATR Mult)
input double InpMaxSweepAtr     = 1.25;   // Max Sweep Penetration (ATR Mult)
input int    InpVolPeriod       = 20;     // Tick Volume Z-Score Lookback
input double InpMinVolZScore    = 1.25;   // Min Tick Volume Z-Score
input double InpMinWickRatio    = 0.42;   // Min Rejection Wick Ratio (0.0 - 1.0)
input bool   InpAlertOnClose    = true;   // Trigger Alert / Push Notification

//--- Indicator Buffers
double BullishSweepBuffer[];
double BearishSweepBuffer[];

//--- Internal Liquidity Pool Struct
struct LiquidityPool
{
   int      barIndex;
   datetime barTime;
   double   price;
   bool     isSwingHigh;
   bool     active;
};

#define MAX_POOLS 256
LiquidityPool g_pools[MAX_POOLS];
int           g_atrHandle = INVALID_HANDLE;
datetime      g_lastAlertTime = 0;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   SetIndexBuffer(0, BullishSweepBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, BearishSweepBuffer, INDICATOR_DATA);

   PlotIndexSetInteger(0, PLOT_ARROW, 233); // Up Arrow
   PlotIndexSetInteger(1, PLOT_ARROW, 234); // Down Arrow

   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);

   g_atrHandle = iATR(_Symbol, _Period, 14);
   if(g_atrHandle == INVALID_HANDLE)
      return(INIT_FAILED);

   ResetPools();
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Helper to clear pool memory                                      |
//+------------------------------------------------------------------+
void ResetPools()
{
   for(int i = 0; i < MAX_POOLS; i++)
      g_pools[i].active = false;
}

//+------------------------------------------------------------------+
//| Insert new confirmed swing high/low into the pool array          |
//+------------------------------------------------------------------+
void AddPool(int barIndex, datetime barTime, double price, bool isSwingHigh)
{
   int oldestSlot = 0;
   int minBarIdx  = INT_MAX;

   for(int i = 0; i < MAX_POOLS; i++)
   {
      if(!g_pools[i].active)
      {
         oldestSlot = i;
         break;
      }
      if(g_pools[i].barIndex < minBarIdx)
      {
         minBarIdx  = g_pools[i].barIndex;
         oldestSlot = i;
      }
   }

   g_pools[oldestSlot].barIndex    = barIndex;
   g_pools[oldestSlot].barTime     = barTime;
   g_pools[oldestSlot].price       = price;
   g_pools[oldestSlot].isSwingHigh = isSwingHigh;
   g_pools[oldestSlot].active      = true;
}

//+------------------------------------------------------------------+
//| Compute Tick Volume Z-Score at index                             |
//+------------------------------------------------------------------+
double GetVolumeZScore(int index, const long &tick_volume[])
{
   double sum = 0.0;
   for(int i = 0; i < InpVolPeriod; i++)
      sum += (double)tick_volume[index - i];

   double mean = sum / InpVolPeriod;
   double varSum = 0.0;

   for(int i = 0; i < InpVolPeriod; i++)
   {
      double diff = (double)tick_volume[index - i] - mean;
      varSum += diff * diff;
   }

   double stdDev = MathSqrt(varSum / InpVolPeriod);
   if(stdDev <= 1e-5)
      return 0.0;

   return ((double)tick_volume[index] - mean) / stdDev;
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
{
   int minBars = InpSwingStrength * 2 + InpVolPeriod + 20;
   if(rates_total <= minBars)
      return(0);

   double atr[];
   if(CopyBuffer(g_atrHandle, 0, 0, rates_total, atr) <= 0)
      return(0);

   int startBar;
   if(prev_calculated == 0)
   {
      ResetPools();
      ArrayInitialize(BullishSweepBuffer, EMPTY_VALUE);
      ArrayInitialize(BearishSweepBuffer, EMPTY_VALUE);
      startBar = minBars;
   }
   else
   {
      startBar = prev_calculated - 1;
   }

   // Always clear current forming bar
   BullishSweepBuffer[rates_total - 1] = EMPTY_VALUE;
   BearishSweepBuffer[rates_total - 1] = EMPTY_VALUE;

   // Process strictly on closed bars up to (rates_total - 2)
   for(int bar = startBar; bar < rates_total - 1; bar++)
   {
      BullishSweepBuffer[bar] = EMPTY_VALUE;
      BearishSweepBuffer[bar] = EMPTY_VALUE;

      // 1. Confirm Swing Pivot at (bar - InpSwingStrength)
      int p = bar - InpSwingStrength;
      bool isHigh = true;
      bool isLow  = true;

      for(int k = 1; k <= InpSwingStrength; k++)
      {
         if(high[p - k] >= high[p] || high[p + k] > high[p]) isHigh = false;
         if(low[p - k]  <= low[p]  || low[p + k]  < low[p])  isLow  = false;
      }

      if(isHigh) AddPool(p, time[p], high[p], true);
      if(isLow)  AddPool(p, time[p], low[p],  false);

      // 2. Check active pools against bar
      double range   = high[bar] - low[bar];
      double curAtr  = atr[bar];
      if(range <= _Point || curAtr <= 0.0)
         continue;

      bool sweptBearish = false;
      bool sweptBullish = false;

      for(int i = 0; i < MAX_POOLS; i++)
      {
         if(!g_pools[i].active)
            continue;

         if(bar - g_pools[i].barIndex > InpMaxPoolAge || g_pools[i].barIndex >= bar)
         {
            g_pools[i].active = false;
            continue;
         }

         double poolPrice = g_pools[i].price;

         if(g_pools[i].isSwingHigh)
         {
            if(close[bar] > poolPrice)
            {
               g_pools[i].active = false; // Invalidated by full-body close above
               continue;
            }
            double pen = high[bar] - poolPrice;
            if(pen >= InpMinSweepAtr * curAtr && pen <= InpMaxSweepAtr * curAtr && close[bar] < poolPrice)
            {
               g_pools[i].active = false;
               sweptBearish = true;
            }
         }
         else
         {
            if(close[bar] < poolPrice)
            {
               g_pools[i].active = false; // Invalidated by full-body close below
               continue;
            }
            double pen = poolPrice - low[bar];
            if(pen >= InpMinSweepAtr * curAtr && pen <= InpMaxSweepAtr * curAtr && close[bar] > poolPrice)
            {
               g_pools[i].active = false;
               sweptBullish = true;
            }
         }
      }

      // 3. Evaluate Tick-Volume Absorption Filter
      if(sweptBullish || sweptBearish)
      {
         double volZ = GetVolumeZScore(bar, tick_volume);
         if(volZ >= InpMinVolZScore)
         {
            if(sweptBullish)
            {
               double lowerWick = MathMin(open[bar], close[bar]) - low[bar];
               if((lowerWick / range) >= InpMinWickRatio)
               {
                  BullishSweepBuffer[bar] = low[bar] - (curAtr * 0.25);
                  if(InpAlertOnClose && bar == rates_total - 2 && time[bar] != g_lastAlertTime)
                  {
                     g_lastAlertTime = time[bar];
                     Alert(StringFormat("BULLISH Sweep + Absorption on %s (Z=%.2f)", _Symbol, volZ));
                     SendNotification(StringFormat("BULLISH Sweep + Absorption on %s (Z=%.2f)", _Symbol, volZ));
                  }
               }
            }

            if(sweptBearish)
            {
               double upperWick = high[bar] - MathMax(open[bar], close[bar]);
               if((upperWick / range) >= InpMinWickRatio)
               {
                  BearishSweepBuffer[bar] = high[bar] + (curAtr * 0.25);
                  if(InpAlertOnClose && bar == rates_total - 2 && time[bar] != g_lastAlertTime)
                  {
                     g_lastAlertTime = time[bar];
                     Alert(StringFormat("BEARISH Sweep + Absorption on %s (Z=%.2f)", _Symbol, volZ));
                     SendNotification(StringFormat("BEARISH Sweep + Absorption on %s (Z=%.2f)", _Symbol, volZ));
                  }
               }
            }
         }
      }
   }

   return(rates_total);
}
//+------------------------------------------------------------------+

Re: Most complex forex strategy

Posted: Wed Oct 14, 2026 3:38 pm
by PTScalper
Key Engineering Details to Note

Zero Repainting by Design: Both indicators evaluate strictly on index - 1 (rates_total - 2 in MQL5), meaning signals only lock once the M15 candle closes back inside the range and confirms the rejection wick.

Single-Raid Mitigation: Once a swing pool is swept—or invalidated by a full candle body closing beyond the level—it is immediately purged from _activePools. This prevents stale support/resistance lines from firing false signals hours later during chop.

Re: Most complex forex strategy

Posted: Wed Oct 14, 2026 3:39 pm
by PTScalper
Below is the complete, self-contained cTrader cBot (ChronosSweepPyramidBot.cs) for .NET 6. It embeds the M15 Liquidity Sweep + Tick-Volume Absorption + M1 Intrabar Wick detector directly inside the execution engine and pairs it with a mathematically isolated House-Money Pyramiding State Machine.

Re: Most complex forex strategy

Posted: Wed Oct 14, 2026 3:40 pm
by PTScalper
How the 4-Phase "House-Money" State Machine Works

Rather than risking new core equity on add-on trades, the cBot treats your initial entry as a low-risk "probe" and forces every subsequent pyramid leg to be collateralized strictly by guaranteed profit already locked in by the trailing stop-loss.

Re: Most complex forex strategy

Posted: Wed Oct 14, 2026 3:40 pm
by PTScalper
Complete cTrader C# cBot Code (ChronosSweepPyramidBot.cs)

To deploy in cTrader:

Open the Automate tab, click New cBot, and name it ChronosSweepPyramidBot.

Replace the entire template with the code below and click Build.

Attach to an M15 chart (e.g., XAUUSD, EURUSD, or GBPUSD).

Code: Select all

using System;
using System.Collections.Generic;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class ChronosSweepPyramidBot : Robot
    {
        #region 1. Liquidity Pool & Sweep Parameters

        [Parameter("Bot Instance Label", DefaultValue = "Chronos_HMP", Group = "0. Engine Identity")]
        public string BotLabel { get; set; }

        [Parameter("Swing Pivot Strength (Bars)", DefaultValue = 5, MinValue = 2, MaxValue = 20, Group = "1. Liquidity Pool Geometry")]
        public int SwingStrength { get; set; }

        [Parameter("Max Pool Age (Bars)", DefaultValue = 120, MinValue = 20, MaxValue = 500, Group = "1. Liquidity Pool Geometry")]
        public int MaxPoolAgeBars { get; set; }

        [Parameter("Min Sweep Depth (ATR Mult)", DefaultValue = 0.10, MinValue = 0.01, MaxValue = 1.0, Step = 0.05, Group = "1. Liquidity Pool Geometry")]
        public double MinSweepAtr { get; set; }

        [Parameter("Max Sweep Depth (ATR Mult)", DefaultValue = 1.30, MinValue = 0.30, MaxValue = 3.0, Step = 0.05, Group = "1. Liquidity Pool Geometry")]
        public double MaxSweepAtr { get; set; }

        #endregion

        #region 2. Tick-Volume Absorption Parameters

        [Parameter("Volume Z-Score Lookback", DefaultValue = 20, MinValue = 5, MaxValue = 100, Group = "2. Tick Absorption Filter")]
        public int VolumePeriod { get; set; }

        [Parameter("Min Tick Volume Z-Score", DefaultValue = 1.25, MinValue = 0.0, MaxValue = 4.0, Step = 0.1, Group = "2. Tick Absorption Filter")]
        public double MinVolumeZScore { get; set; }

        [Parameter("Min Rejection Wick Ratio (0-1)", DefaultValue = 0.42, MinValue = 0.20, MaxValue = 0.85, Step = 0.02, Group = "2. Tick Absorption Filter")]
        public double MinWickRatio { get; set; }

        [Parameter("Verify M1 Intrabar Wick Volume", DefaultValue = true, Group = "2. Tick Absorption Filter")]
        public bool UseM1IntrabarCheck { get; set; }

        [Parameter("Min M1 Wick Volume Share (0-1)", DefaultValue = 0.35, MinValue = 0.10, MaxValue = 0.80, Step = 0.05, Group = "2. Tick Absorption Filter")]
        public double MinM1WickVolumeShare { get; set; }

        #endregion

        #region 3. Seed Risk & House-Money Pyramiding Parameters

        [Parameter("Seed Risk (% of Balance)", DefaultValue = 0.25, MinValue = 0.05, MaxValue = 2.0, Step = 0.05, Group = "3. House-Money Risk Engine")]
        public double SeedRiskPercent { get; set; }

        [Parameter("Stop Buffer Beyond Sweep (ATR)", DefaultValue = 0.20, MinValue = 0.05, MaxValue = 1.0, Step = 0.05, Group = "3. House-Money Risk Engine")]
        public double StopBufferAtr { get; set; }

        [Parameter("Breakeven Trigger (R-Multiple)", DefaultValue = 1.0, MinValue = 0.5, MaxValue = 3.0, Step = 0.1, Group = "3. House-Money Risk Engine")]
        public double BreakevenTriggerR { get; set; }

        [Parameter("Breakeven Cushion (Pips)", DefaultValue = 1.5, MinValue = 0.2, MaxValue = 10.0, Step = 0.1, Group = "3. House-Money Risk Engine")]
        public double BreakevenCushionPips { get; set; }

        [Parameter("Max Total Positions (Seed + Pyramids)", DefaultValue = 4, MinValue = 1, MaxValue = 10, Group = "3. House-Money Risk Engine")]
        public int MaxBasketPositions { get; set; }

        [Parameter("Pyramid Step Distance (R-Multiple)", DefaultValue = 1.6, MinValue = 1.0, MaxValue = 4.0, Step = 0.1, Group = "3. House-Money Risk Engine")]
        public double PyramidStepR { get; set; }

        [Parameter("House-Money Risk Share (0.1 - 0.8)", DefaultValue = 0.50, MinValue = 0.10, MaxValue = 0.85, Step = 0.05, Group = "3. House-Money Risk Engine")]
        public double HouseMoneyRiskFraction { get; set; }

        [Parameter("Hard Take-Profit on Seed (R-Multiple)", DefaultValue = 6.0, MinValue = 2.0, MaxValue = 20.0, Step = 0.5, Group = "3. House-Money Risk Engine")]
        public double TargetTakeProfitR { get; set; }

        #endregion

        #region 4. Layer 7 ECN Execution Guards

        [Parameter("Max Spread Multiplier (vs 60m Avg)", DefaultValue = 1.8, MinValue = 1.1, MaxValue = 4.0, Step = 0.1, Group = "4. ECN Kill Switches")]
        public double MaxSpreadMultiplier { get; set; }

        [Parameter("Max Market Range Slippage (Pips)", DefaultValue = 1.5, MinValue = 0.2, MaxValue = 10.0, Step = 0.1, Group = "4. ECN Kill Switches")]
        public double MaxSlippagePips { get; set; }

        #endregion

        #region Internal State Structures

        private sealed class LiquidityPool
        {
            public int BarIndex { get; init; }
            public double Price { get; init; }
            public bool IsSwingHigh { get; init; }
            public bool IsMitigated { get; set; }
        }

        private readonly List<LiquidityPool> _activePools = new();
        private readonly Queue<double> _rollingSpreads = new();
        private AverageTrueRange _atr;
        private Bars _m1Bars;

        // Basket tracking state
        private double _seedEntryPrice;
        private double _seedInitialRiskPips;
        private bool _isSeedBreakevenLocked;

        #endregion

        protected override void OnStart()
        {
            _atr = Indicators.AverageTrueRange(14, MovingAverageType.Exponential);

            if (UseM1IntrabarCheck && Bars.TimeFrame > TimeFrame.Minute)
            {
                _m1Bars = MarketData.GetBars(TimeFrame.Minute);
            }

            // Warm up historical liquidity pools over the last 300 bars
            int startWarmup = Math.Max(SwingStrength * 2 + VolumePeriod, Bars.Count - 300);
            for (int i = startWarmup; i < Bars.Count - 1; i++)
            {
                RegisterConfirmedPools(i - SwingStrength);
                PruneAndInvalidatePools(i);
            }

            Print("Chronos Sweep & House-Money Pyramid Engine started on {0} ({1}). Warmed up {2} active pools.",
                SymbolName, TimeFrame, _activePools.Count);
        }

        protected override void OnTick()
        {
            RecordSpreadSample();

            var basket = GetActiveBasket();
            if (basket.Count == 0)
            {
                _isSeedBreakevenLocked = false;
                return;
            }

            Position seedPos = basket[0];
            if (_seedInitialRiskPips <= 0)
                return;

            // Phase 2: Check 1R Fortress Breakeven Lock on the Seed Position
            if (!_isSeedBreakevenLocked)
            {
                double favorablePips = seedPos.TradeType == TradeType.Buy
                    ? (Symbol.Bid - seedPos.EntryPrice) / Symbol.PipSize
                    : (seedPos.EntryPrice - Symbol.Ask) / Symbol.PipSize;

                if (favorablePips >= _seedInitialRiskPips * BreakevenTriggerR)
                {
                    LockSeedAtBreakeven(seedPos);
                }
            }

            // Phase 4: Check Hard Basket Target R-Multiple
            double totalRunPips = seedPos.TradeType == TradeType.Buy
                ? (Symbol.Bid - _seedEntryPrice) / Symbol.PipSize
                : (_seedEntryPrice - Symbol.Ask) / Symbol.PipSize;

            if (totalRunPips >= _seedInitialRiskPips * TargetTakeProfitR)
            {
                FlattenBasket($"Target {TargetTakeProfitR:F1}R Reached");
            }
        }

        protected override void OnBar()
        {
            int closedIndex = Bars.Count - 2;
            if (closedIndex <= SwingStrength * 2 + VolumePeriod)
                return;

            // 1. Register newly confirmed Swing High / Swing Low pivot
            RegisterConfirmedPools(closedIndex - SwingStrength);

            // 2. Evaluate closed bar for Liquidity Sweep + Tick Absorption
            EvaluateClosedBar(closedIndex);
        }

        #region Core Strategy Logic (Sweep Detection & Pyramiding)

        private void EvaluateClosedBar(int barIndex)
        {
            double open = Bars.OpenPrices[barIndex];
            double high = Bars.HighPrices[barIndex];
            double low = Bars.LowPrices[barIndex];
            double close = Bars.ClosePrices[barIndex];
            double range = high - low;
            double atr = _atr.Result[barIndex];

            if (range <= Symbol.TickSize || atr <= 0)
                return;

            LiquidityPool bullishSweepPool = null;
            LiquidityPool bearishSweepPool = null;

            for (int i = _activePools.Count - 1; i >= 0; i--)
            {
                LiquidityPool pool = _activePools[i];

                if (barIndex - pool.BarIndex > MaxPoolAgeBars || pool.IsMitigated)
                {
                    _activePools.RemoveAt(i);
                    continue;
                }

                if (pool.IsSwingHigh)
                {
                    if (close > pool.Price)
                    {
                        pool.IsMitigated = true;
                        _activePools.RemoveAt(i);
                        continue;
                    }

                    double pen = high - pool.Price;
                    if (pen >= MinSweepAtr * atr && pen <= MaxSweepAtr * atr && close < pool.Price)
                    {
                        pool.IsMitigated = true;
                        bearishSweepPool = pool;
                        _activePools.RemoveAt(i);
                    }
                }
                else
                {
                    if (close < pool.Price)
                    {
                        pool.IsMitigated = true;
                        _activePools.RemoveAt(i);
                        continue;
                    }

                    double pen = pool.Price - low;
                    if (pen >= MinSweepAtr * atr && pen <= MaxSweepAtr * atr && close > pool.Price)
                    {
                        pool.IsMitigated = true;
                        bullishSweepPool = pool;
                        _activePools.RemoveAt(i);
                    }
                }
            }

            double volZScore = CalculateTickVolumeZScore(barIndex);
            var basket = GetActiveBasket();

            // Check Bullish Sweep Signal
            bool validBullishSweep = false;
            if (bullishSweepPool != null)
            {
                double lowerWickRatio = (Math.Min(open, close) - low) / range;
                if (volZScore >= MinVolumeZScore &&
                    lowerWickRatio >= MinWickRatio &&
                    VerifyM1WickAbsorption(barIndex, bullishSweepPool.Price, isBullish: true))
                {
                    validBullishSweep = true;
                }
            }

            // Check Bearish Sweep Signal
            bool validBearishSweep = false;
            if (bearishSweepPool != null)
            {
                double upperWickRatio = (high - Math.Max(open, close)) / range;
                if (volZScore >= MinVolumeZScore &&
                    upperWickRatio >= MinWickRatio &&
                    VerifyM1WickAbsorption(barIndex, bearishSweepPool.Price, isBullish: false))
                {
                    validBearishSweep = true;
                }
            }

            // If flat, execute Phase 1 Seed Probe
            if (basket.Count == 0)
            {
                if (validBullishSweep && IsEcnSpreadHealthy())
                {
                    double stopPrice = low - (atr * StopBufferAtr);
                    ExecuteSeedPosition(TradeType.Buy, stopPrice, bullishSweepPool);
                }
                else if (validBearishSweep && IsEcnSpreadHealthy())
                {
                    double stopPrice = high + (atr * StopBufferAtr);
                    ExecuteSeedPosition(TradeType.Sell, stopPrice, bearishSweepPool);
                }
                return;
            }

            // If a basket is active, check for opposing sweep reversal or Phase 3 House-Money Pyramiding
            TradeType basketDirection = basket[0].TradeType;
            if ((basketDirection == TradeType.Buy && validBearishSweep) ||
                (basketDirection == TradeType.Sell && validBullishSweep))
            {
                FlattenBasket("Opposing Institutional Liquidity Sweep Detected");
                return;
            }

            if (_isSeedBreakevenLocked && basket.Count < MaxBasketPositions && IsEcnSpreadHealthy())
            {
                TryExecuteHouseMoneyPyramid(barIndex, basket, atr);
            }
        }

        private void ExecuteSeedPosition(TradeType tradeType, double stopPrice, LiquidityPool pool)
        {
            double entryEstimate = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
            double stopLossPips = Math.Abs(entryEstimate - stopPrice) / Symbol.PipSize;

            if (stopLossPips < 2.0)
                return;

            double seedRiskCurrency = Account.Balance * (SeedRiskPercent / 100.0);
            double rawUnits = seedRiskCurrency / (stopLossPips * Symbol.PipValue);
            double volumeInUnits = Symbol.NormalizeVolumeInUnits(rawUnits, RoundingMode.Down);

            if (volumeInUnits < Symbol.VolumeInUnitsMin)
                return;

            double takeProfitPips = Math.Round(stopLossPips * TargetTakeProfitR, 1);

            TradeResult result = ExecuteMarketRangeOrder(
                tradeType,
                SymbolName,
                volumeInUnits,
                MaxSlippagePips,
                entryEstimate,
                BotLabel,
                Math.Round(stopLossPips, 1),
                takeProfitPips);

            if (result.IsSuccessful && result.Position != null)
            {
                _seedEntryPrice = result.Position.EntryPrice;
                _seedInitialRiskPips = stopLossPips;
                _isSeedBreakevenLocked = false;

                Chart.DrawTrendLine(
                    $"Sweep_{Bars.Count}",
                    Bars.OpenTimes[pool.BarIndex],
                    pool.Price,
                    Bars.OpenTimes[Bars.Count - 2],
                    pool.Price,
                    tradeType == TradeType.Buy ? Color.Lime : Color.Crimson,
                    2,
                    LineStyle.DotsRare);

                Print("SEED PROBE OPENED | {0} {1} units @ {2:F5} | SL: {3:F1} pips | Risk: {4:C2} ({5:F2}%)",
                    tradeType, volumeInUnits, _seedEntryPrice, stopLossPips, seedRiskCurrency, SeedRiskPercent);
            }
        }

        private void LockSeedAtBreakeven(Position seedPos)
        {
            double newStop = seedPos.TradeType == TradeType.Buy
                ? Symbol.NormalizePrice(seedPos.EntryPrice + (BreakevenCushionPips * Symbol.PipSize))
                : Symbol.NormalizePrice(seedPos.EntryPrice - (BreakevenCushionPips * Symbol.PipSize));

            bool isValidImprovement = seedPos.TradeType == TradeType.Buy
                ? (seedPos.StopLoss == null || newStop > seedPos.StopLoss.Value)
                : (seedPos.StopLoss == null || newStop < seedPos.StopLoss.Value);

            if (isValidImprovement)
            {
                var mod = ModifyPosition(seedPos, newStop, seedPos.TakeProfit);
                if (mod.IsSuccessful)
                {
                    _isSeedBreakevenLocked = true;
                    Print("1R FORTRESS LOCKED | Seed Position #{0} SL moved to {1:F5} (+{2:F1} pips cushion). Core equity risk = $0.00.",
                        seedPos.Id, newStop, BreakevenCushionPips);
                }
            }
        }

        private void TryExecuteHouseMoneyPyramid(int barIndex, List<Position> basket, double atr)
        {
            TradeType direction = basket[0].TradeType;
            double currentPrice = direction == TradeType.Buy ? Symbol.Ask : Symbol.Bid;

            // 1. Verify price has advanced to the next R-step threshold for this pyramid layer
            double requiredAdvancePips = _seedInitialRiskPips * (BreakevenTriggerR + (basket.Count * (PyramidStepR - 0.8)));
            double currentAdvancePips = direction == TradeType.Buy
                ? (currentPrice - _seedEntryPrice) / Symbol.PipSize
                : (_seedEntryPrice - currentPrice) / Symbol.PipSize;

            if (currentAdvancePips < requiredAdvancePips)
                return;

            // 2. Confirm a continuation bar in the direction of the trend with rejection structure
            bool isBullishContinuation = direction == TradeType.Buy &&
                                         Bars.ClosePrices[barIndex] > Bars.OpenPrices[barIndex] &&
                                         Bars.ClosePrices[barIndex] > Bars.HighPrices[barIndex - 1];

            bool isBearishContinuation = direction == TradeType.Sell &&
                                         Bars.ClosePrices[barIndex] < Bars.OpenPrices[barIndex] &&
                                         Bars.ClosePrices[barIndex] < Bars.LowPrices[barIndex - 1];

            if (!isBullishContinuation && !isBearishContinuation)
                return;

            // 3. Calculate the proposed new Unified Structural Stop Loss behind the continuation pullback
            double proposedStop = direction == TradeType.Buy
                ? Symbol.NormalizePrice(Math.Min(Bars.LowPrices[barIndex], Bars.LowPrices[barIndex - 1]) - (atr * StopBufferAtr))
                : Symbol.NormalizePrice(Math.Max(Bars.HighPrices[barIndex], Bars.HighPrices[barIndex - 1]) + (atr * StopBufferAtr));

            // Ensure the proposed stop strictly ratchets forward for every open leg in the basket
            foreach (Position pos in basket)
            {
                if (pos.StopLoss == null) return;
                if (direction == TradeType.Buy && proposedStop <= pos.StopLoss.Value) return;
                if (direction == TradeType.Sell && proposedStop >= pos.StopLoss.Value) return;
            }

            // 4. Calculate Total Guaranteed Locked Profit (in Account Currency) if all existing legs are trailed to proposedStop
            double totalLockedProfitCurrency = 0.0;
            foreach (Position pos in basket)
            {
                double lockedPips = direction == TradeType.Buy
                    ? (proposedStop - pos.EntryPrice) / Symbol.PipSize
                    : (pos.EntryPrice - proposedStop) / Symbol.PipSize;

                double legLockedGross = lockedPips * Symbol.PipValue * pos.VolumeInUnits;
                double legNetLocked = legLockedGross + (pos.Commissions * 2.0) + pos.Swap;
                totalLockedProfitCurrency += legNetLocked;
            }

            if (totalLockedProfitCurrency <= 0)
                return;

            // 5. Allocate only HouseMoneyRiskFraction (e.g., 50%) of the guaranteed locked profit to the new leg
            double houseMoneyRiskBudget = totalLockedProfitCurrency * HouseMoneyRiskFraction;
            double newLegStopPips = Math.Abs(currentPrice - proposedStop) / Symbol.PipSize;

            if (newLegStopPips < 2.0)
                return;

            double rawPyramidUnits = houseMoneyRiskBudget / (newLegStopPips * Symbol.PipValue);
            double pyramidVolumeUnits = Symbol.NormalizeVolumeInUnits(rawPyramidUnits, RoundingMode.Down);

            if (pyramidVolumeUnits < Symbol.VolumeInUnitsMin)
                return;

            // 6. First ratchet all existing positions to the new Unified Stop Loss
            foreach (Position pos in basket)
            {
                var mod = ModifyPosition(pos, proposedStop, pos.TakeProfit);
                if (!mod.IsSuccessful)
                    return; // Abort pyramid entry if any existing leg fails to lock its stop
            }

            // 7. Fire the House-Money Pyramid Order with the exact same unified stop
            TradeResult pyrResult = ExecuteMarketRangeOrder(
                direction,
                SymbolName,
                pyramidVolumeUnits,
                MaxSlippagePips,
                currentPrice,
                BotLabel,
                Math.Round(newLegStopPips, 1),
                null);

            if (pyrResult.IsSuccessful && pyrResult.Position != null)
            {
                // Align exact stop price with the rest of the basket
                ModifyPosition(pyrResult.Position, proposedStop, basket[0].TakeProfit);

                Print("HOUSE-MONEY PYRAMID LEG #{0} FIRED | {1} {2} units @ {3:F5} | Unified SL: {4:F5} | Locked Profit Buffer: {5:C2} | Allocated Risk: {6:C2} (Worst-case Net: +{7:C2})",
                    basket.Count + 1,
                    direction,
                    pyramidVolumeUnits,
                    pyrResult.Position.EntryPrice,
                    proposedStop,
                    totalLockedProfitCurrency,
                    houseMoneyRiskBudget,
                    totalLockedProfitCurrency - houseMoneyRiskBudget);
            }
        }

        #endregion

        #region Helper & Microstructure Verification Methods

        private void RegisterConfirmedPools(int pivotIndex)
        {
            if (pivotIndex < SwingStrength)
                return;

            double candidateHigh = Bars.HighPrices[pivotIndex];
            double candidateLow = Bars.LowPrices[pivotIndex];
            bool isSwingHigh = true;
            bool isSwingLow = true;

            for (int offset = 1; offset <= SwingStrength; offset++)
            {
                if (Bars.HighPrices[pivotIndex - offset] >= candidateHigh ||
                    Bars.HighPrices[pivotIndex + offset] > candidateHigh)
                    isSwingHigh = false;

                if (Bars.LowPrices[pivotIndex - offset] <= candidateLow ||
                    Bars.LowPrices[pivotIndex + offset] < candidateLow)
                    isSwingLow = false;
            }

            if (isSwingHigh)
                _activePools.Add(new LiquidityPool { BarIndex = pivotIndex, Price = candidateHigh, IsSwingHigh = true });

            if (isSwingLow)
                _activePools.Add(new LiquidityPool { BarIndex = pivotIndex, Price = candidateLow, IsSwingHigh = false });
        }

        private void PruneAndInvalidatePools(int barIndex)
        {
            double close = Bars.ClosePrices[barIndex];
            for (int i = _activePools.Count - 1; i >= 0; i--)
            {
                var pool = _activePools[i];
                if (barIndex - pool.BarIndex > MaxPoolAgeBars ||
                    (pool.IsSwingHigh && close > pool.Price) ||
                    (!pool.IsSwingHigh && close < pool.Price))
                {
                    _activePools.RemoveAt(i);
                }
            }
        }

        private double CalculateTickVolumeZScore(int barIndex)
        {
            double sum = 0.0;
            for (int i = 0; i < VolumePeriod; i++)
                sum += Bars.TickVolumes[barIndex - i];

            double mean = sum / VolumePeriod;
            double varSum = 0.0;

            for (int i = 0; i < VolumePeriod; i++)
            {
                double diff = Bars.TickVolumes[barIndex - i] - mean;
                varSum += diff * diff;
            }

            double stdDev = Math.Sqrt(varSum / VolumePeriod);
            return stdDev <= 1e-5 ? 0.0 : (Bars.TickVolumes[barIndex] - mean) / stdDev;
        }

        private bool VerifyM1WickAbsorption(int m15Index, double sweptPoolPrice, bool isBullish)
        {
            if (!UseM1IntrabarCheck || _m1Bars == null || _m1Bars.Count == 0)
                return true;

            DateTime barStart = Bars.OpenTimes[m15Index];
            DateTime barEnd = m15Index + 1 < Bars.Count
                ? Bars.OpenTimes[m15Index + 1]
                : barStart.AddMinutes(15);

            int startM1Idx = _m1Bars.OpenTimes.GetIndexByTime(barStart);
            if (startM1Idx < 0 || _m1Bars.OpenTimes[startM1Idx] < barStart.AddMinutes(-15))
                return true;

            double totalM1Volume = 0;
            double wickZoneVolume = 0;

            for (int m1 = startM1Idx; m1 < _m1Bars.Count; m1++)
            {
                DateTime t = _m1Bars.OpenTimes[m1];
                if (t < barStart) continue;
                if (t >= barEnd) break;

                double v = _m1Bars.TickVolumes[m1];
                totalM1Volume += v;

                if (isBullish && _m1Bars.LowPrices[m1] <= sweptPoolPrice)
                    wickZoneVolume += v;
                else if (!isBullish && _m1Bars.HighPrices[m1] >= sweptPoolPrice)
                    wickZoneVolume += v;
            }

            return totalM1Volume <= 0 || (wickZoneVolume / totalM1Volume) >= MinM1WickVolumeShare;
        }

        private void RecordSpreadSample()
        {
            _rollingSpreads.Enqueue(Symbol.Spread);
            if (_rollingSpreads.Count > 500)
                _rollingSpreads.Dequeue();
        }

        private bool IsEcnSpreadHealthy()
        {
            if (_rollingSpreads.Count < 30)
                return true;

            double avgSpread = _rollingSpreads.Average();
            return Symbol.Spread <= avgSpread * MaxSpreadMultiplier;
        }

        private List<Position> GetActiveBasket()
        {
            return Positions
                .Where(p => p.SymbolName == SymbolName && p.Label == BotLabel)
                .OrderBy(p => p.EntryTime)
                .ToList();
        }

        private void FlattenBasket(string reason)
        {
            var basket = GetActiveBasket();
            foreach (Position pos in basket)
                ClosePosition(pos);

            _isSeedBreakevenLocked = false;
            Print("BASKET FLATTENED ({0} positions) | Reason: {1}", basket.Count, reason);
        }

        #endregion
    }
}

Re: Most complex forex strategy

Posted: Wed Oct 14, 2026 3:42 pm
by PTScalper
Why This Pyramiding Math Cannot Lose Core Equity After Phase 2

Most retail "scaling-in" systems fail because adding a larger second or third position raises the average entry price so close to current spot that a normal pullback wipes out the entire sequence for a net loss.

Look at how TryExecuteHouseMoneyPyramid() prevents that in Steps 4 through 6:

Ratchet First: Before firing Leg 2, the cBot proposes a new unified structural stop-loss (proposedStop) behind the most recent continuation pullback.

Compute Net Locked Dollars: It calculates the exact dollar profit that Leg 1 is guaranteed to bank at proposedStop, subtracting double ECN commission and swap