Page 2 of 2

Re: Forex scalping trading strategy for Gold (XAU/USD)

Posted: Mon Oct 19, 2026 6:21 pm
by PTScalper
Deployment & Strategy Tester Checklist

Attach to an XAUUSD M5 Chart: While the EA pulls D1, H4, and M15 data multi-timeframe via native handles, attach it to the M5 chart (InpTriggerTF = PERIOD_M5) so candle-close triggers evaluate every 5 minutes.

Align Server Time Offset: Check your broker's Market Watch server time offset (most Raw ECN brokers run GMT+2 / GMT+3 DST). If your server is GMT+3, shift InpLondonStartHour to 10 (10:00–13:00 server time) and InpNYStartHour to 15 (15:00–19:00 server time).

Backtest Modeling Quality: In the MT5 Strategy Tester, select "Every tick based on real ticks" to accurately model XAU/USD variable spread widening and partial-fill execution during the London–NY overlap.

Re: Forex scalping trading strategy for Gold (XAU/USD)

Posted: Mon Oct 19, 2026 6:22 pm
by PTScalper
This complete cTrader cBot (XAUUSD_LiquidityScalper.cs) ports the entire MQL5 multi-timeframe liquidity sweep and pullback engine into native cAlgo C# (.NET 6) for Raw ECN execution.

Re: Forex scalping trading strategy for Gold (XAU/USD)

Posted: Mon Oct 19, 2026 6:22 pm
by PTScalper
Key cTrader / cAlgo Architectural Differences from MQL5

Volume in Units (VolumeInUnits) vs. Lots: Unlike MetaTrader 5 where order volume is passed in lots (0.50), cTrader executes orders in raw asset units (1.00 Standard Lot of Gold = 100 Troy Ounces, minimum volume 1 Ounce = 0.01 Lots). The position sizing function automatically computes exact risk using Symbol.TickValue, Symbol.TickSize, and Symbol.LotSize, then normalizes to step units via Symbol.NormalizeVolumeInUnits(..., RoundingMode.Down).

True UTC Clock: Server.TimeInUtc is natively UTC across all cTrader brokers, removing any GMT+2/GMT+3 server-time offset ambiguity for the Asian, London, and NY session windows.

Event-Driven Bar vs. Tick Splitting: Active open positions (1.5R partial close, Breakeven + Commission stop move, M15 structural trail, and time stop) are evaluated on every tick inside OnTick(), while new entry setups evaluate strictly on closed candles inside OnBar().

Re: Forex scalping trading strategy for Gold (XAU/USD)

Posted: Mon Oct 19, 2026 6:23 pm
by PTScalper
Complete cTrader cBot Source Code (XAUUSD_LiquidityScalper.cs)

In cTrader Automate, create a new cBot named XAUUSD_LiquidityScalper, replace the template code with the source below, and press Ctrl + B to build:

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 XAUUSD_LiquidityScalper : Robot
    {
        #region 1. Strategy & Setup Filters

        [Parameter("Enable Liquidity Sweep Setup", Group = "1. Strategy & Setup Filters", DefaultValue = true)]
        public bool InpEnableSweepSetup { get; set; }

        [Parameter("Enable M15 EMA Pullback Setup", Group = "1. Strategy & Setup Filters", DefaultValue = true)]
        public bool InpEnablePullbackSetup { get; set; }

        [Parameter("M15 Fast EMA Period", Group = "1. Strategy & Setup Filters", DefaultValue = 20, MinValue = 5)]
        public int InpEmaFast { get; set; }

        [Parameter("M15 Slow EMA Period", Group = "1. Strategy & Setup Filters", DefaultValue = 50, MinValue = 10)]
        public int InpEmaSlow { get; set; }

        [Parameter("M5 ATR Period", Group = "1. Strategy & Setup Filters", DefaultValue = 14, MinValue = 5)]
        public int InpAtrPeriod { get; set; }

        [Parameter("H4 Swing Lookback (Bars)", Group = "1. Strategy & Setup Filters", DefaultValue = 10, MinValue = 3)]
        public int InpH4SwingLookback { get; set; }

        [Parameter("Max Sweep Overshoot ($ USD)", Group = "1. Strategy & Setup Filters", DefaultValue = 2.50, MinValue = 0.20, Step = 0.10)]
        public double InpMaxSweepOvershootUSD { get; set; }

        [Parameter("EMA Touch Tolerance ($ USD)", Group = "1. Strategy & Setup Filters", DefaultValue = 0.35, MinValue = 0.05, Step = 0.05)]
        public double InpTouchToleranceUSD { get; set; }

        #endregion

        #region 2. XAU/USD Stop-Loss & Target Calibration

        [Parameter("ATR Multiplier Beyond Wick", Group = "2. Stop-Loss & Targets", DefaultValue = 1.0, MinValue = 0.2, Step = 0.1)]
        public double InpAtrStopMultiplier { get; set; }

        [Parameter("Min Structural Stop ($ USD)", Group = "2. Stop-Loss & Targets", DefaultValue = 1.20, MinValue = 0.50, Step = 0.10)]
        public double InpMinStopUSD { get; set; }

        [Parameter("Max Structural Stop ($ USD)", Group = "2. Stop-Loss & Targets", DefaultValue = 3.50, MinValue = 1.50, Step = 0.10)]
        public double InpMaxStopUSD { get; set; }

        [Parameter("Partial Close & BE Trigger (R)", Group = "2. Stop-Loss & Targets", DefaultValue = 1.5, MinValue = 0.5, Step = 0.1)]
        public double InpPartialCloseRR { get; set; }

        [Parameter("Partial Close Ratio (0.5 = 50%)", Group = "2. Stop-Loss & Targets", DefaultValue = 0.50, MinValue = 0.10, MaxValue = 0.90, Step = 0.05)]
        public double InpPartialCloseRatio { get; set; }

        [Parameter("Start M15 Structural Trail (R)", Group = "2. Stop-Loss & Targets", DefaultValue = 2.0, MinValue = 1.0, Step = 0.1)]
        public double InpTrailStartRR { get; set; }

        [Parameter("Final Hard Target (R)", Group = "2. Stop-Loss & Targets", DefaultValue = 3.5, MinValue = 1.5, Step = 0.1)]
        public double InpFinalTargetRR { get; set; }

        [Parameter("Time Stop if < 0.5R (Mins, 0=Off)", Group = "2. Stop-Loss & Targets", DefaultValue = 45, MinValue = 0)]
        public int InpTimeStopMinutes { get; set; }

        #endregion

        #region 3. Risk, ECN Commission & Profit-Lock Scaling

        [Parameter("Baseline Risk Per Trade (%)", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 0.50, MinValue = 0.10, MaxValue = 5.0, Step = 0.05)]
        public double InpBaseRiskPct { get; set; }

        [Parameter("Round-Turn Commission / Lot ($)", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 7.00, MinValue = 0.0, Step = 0.50)]
        public double InpCommissionPerLotRT { get; set; }

        [Parameter("Enable Profit-Lock Scaling", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = true)]
        public bool InpUseProfitLockScaling { get; set; }

        [Parameter("Gain % to Trigger 50% Lock", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 2.00, MinValue = 0.50, Step = 0.25)]
        public double InpProfitLockTriggerPct { get; set; }

        [Parameter("% of Unlocked Buffer Added to Risk", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 25.0, MinValue = 5.0, MaxValue = 100.0, Step = 5.0)]
        public double InpUnlockedBufferRiskPct { get; set; }

        [Parameter("Hard Cap on Scaled Risk (%)", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 1.25, MinValue = 0.25, MaxValue = 5.0, Step = 0.25)]
        public double InpMaxScaledRiskPct { get; set; }

        [Parameter("Daily Max Drawdown Breaker (%)", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 1.50, MinValue = 0.50, Step = 0.25)]
        public double InpMaxDailyDrawdownPct { get; set; }

        [Parameter("Max Consecutive Losses / Day", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 3, MinValue = 1)]
        public int InpMaxDailyConsecLosses { get; set; }

        #endregion

        #region 4. Raw ECN Spread & Session Windows (UTC)

        [Parameter("Max Allowed Live Spread ($ USD)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 0.25, MinValue = 0.05, Step = 0.05)]
        public double InpMaxSpreadUSD { get; set; }

        [Parameter("Asian Start Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 0, MinValue = 0, MaxValue = 23)]
        public int InpAsianStartHour { get; set; }

        [Parameter("Asian End Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 7, MinValue = 1, MaxValue = 23)]
        public int InpAsianEndHour { get; set; }

        [Parameter("Trade London Open Window", Group = "4. Spread & Session Windows (UTC)", DefaultValue = true)]
        public bool InpTradeLondonSession { get; set; }

        [Parameter("London Start Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 7, MinValue = 0, MaxValue = 23)]
        public int InpLondonStartHour { get; set; }

        [Parameter("London End Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 10, MinValue = 1, MaxValue = 23)]
        public int InpLondonEndHour { get; set; }

        [Parameter("Trade London-NY Overlap", Group = "4. Spread & Session Windows (UTC)", DefaultValue = true)]
        public bool InpTradeNYOverlap { get; set; }

        [Parameter("NY Overlap Start Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 12, MinValue = 0, MaxValue = 23)]
        public int InpNYStartHour { get; set; }

        [Parameter("NY Overlap End Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 16, MinValue = 1, MaxValue = 23)]
        public int InpNYEndHour { get; set; }

        [Parameter("Bot Label", Group = "4. Spread & Session Windows (UTC)", DefaultValue = "XAU_Scalper_2026")]
        public string InpBotLabel { get; set; }

        #endregion

        #region Private Fields & Multi-Timeframe Series

        private Bars _m5Bars;
        private Bars _m15Bars;
        private Bars _h4Bars;
        private Bars _d1Bars;

        private ExponentialMovingAverage _emaFastM15;
        private ExponentialMovingAverage _emaSlowM15;
        private AverageTrueRange _atrM5;

        // Risk & Profit-Lock State
        private int _currentDayOfYear = -1;
        private double _dayStartEquity;
        private double _baseReferenceEq;
        private double _lockedEquityFloor;
        private int _dailyConsecLosses;
        private bool _dailyHalted;

        // Liquidity Pool Cache
        private double _pdh;
        private double _pdl;
        private double _asianHigh;
        private double _asianLow;
        private double _h4SwingHigh;
        private double _h4SwingLow;

        // Track 1R price distance and Partial-Close execution per Position ID
        private readonly HashSet<int> _partialClosedIds = new HashSet<int>();
        private readonly Dictionary<int, double> _initialRiskDistByPosId = new Dictionary<int, double>();

        #endregion

        protected override void OnStart()
        {
            // Load Multi-Timeframe Bars
            _m5Bars  = MarketData.GetBars(TimeFrame.Minute5);
            _m15Bars = MarketData.GetBars(TimeFrame.Minute15);
            _h4Bars  = MarketData.GetBars(TimeFrame.Hour4);
            _d1Bars  = MarketData.GetBars(TimeFrame.Daily);

            // Initialize Indicators on their respective timeframes
            _emaFastM15 = Indicators.ExponentialMovingAverage(_m15Bars.ClosePrices, InpEmaFast);
            _emaSlowM15 = Indicators.ExponentialMovingAverage(_m15Bars.ClosePrices, InpEmaSlow);
            _atrM5      = Indicators.AverageTrueRange(_m5Bars, InpAtrPeriod, MovingAverageType.Exponential);

            // Initialize Equity & Profit-Lock State
            double eq = Account.Equity;
            _dayStartEquity    = eq;
            _baseReferenceEq   = eq;
            _lockedEquityFloor = eq * (1.0 - InpMaxDailyDrawdownPct / 100.0);
            _currentDayOfYear  = Server.TimeInUtc.DayOfYear;

            Positions.Closed += OnPositionClosed;

            Print("XAUUSD_LiquidityScalper initialized on {0}. PipSize: {1}, TickSize: {2}, LotSize: {3}",
                SymbolName, Symbol.PipSize, Symbol.TickSize, Symbol.LotSize);
        }

        protected override void OnStop()
        {
            Positions.Closed -= OnPositionClosed;
        }

        protected override void OnTick()
        {
            // 1. Daily Reset & Circuit Breaker Audit
            CheckDailyResetAndDrawdown();

            // 2. Manage Open Positions on Every Tick (Partial Close, BE+Comm, Structural Trail, Time Stop)
            var activePositions = Positions.FindAll(InpBotLabel, SymbolName);
            if (activePositions.Length > 0)
            {
                ManageOpenPositions(activePositions);
            }
        }

        protected override void OnBar()
        {
            if (_dailyHalted)
                return;

            // One active position at a time on XAU/USD
            if (Positions.FindAll(InpBotLabel, SymbolName).Length > 0)
                return;

            // 1. Verify Active Session Window (UTC)
            if (!IsWithinActiveSession())
                return;

            // 2. Verify Live Spread Filter ($ USD)
            double liveSpreadUSD = Symbol.Ask - Symbol.Bid;
            if (liveSpreadUSD > InpMaxSpreadUSD)
                return;

            // 3. Refresh H4/D1 & Asian Session Liquidity Pools
            UpdateLiquidityPools();

            double currentAtr = _atrM5.Result.Last(1);
            if (double.IsNaN(currentAtr) || currentAtr <= 0.0)
                return;

            // 4. Evaluate Setup 1: Liquidity Sweep (False Breakout of Key Level)
            if (InpEnableSweepSetup)
            {
                if (CheckBullishSweep(currentAtr, out double buySweepSl))
                {
                    ExecuteScalpTrade(TradeType.Buy, buySweepSl, "XAU_Sweep_Buy");
                    return;
                }

                if (CheckBearishSweep(currentAtr, out double sellSweepSl))
                {
                    ExecuteScalpTrade(TradeType.Sell, sellSweepSl, "XAU_Sweep_Sell");
                    return;
                }
            }

            // 5. Evaluate Setup 2: M15 EMA 20/50 Trend Pullback + Micro BoS
            if (InpEnablePullbackSetup)
            {
                double m15Fast = _emaFastM15.Result.Last(1);
                double m15Slow = _emaSlowM15.Result.Last(1);

                if (CheckBullishPullback(m15Fast, m15Slow, currentAtr, out double buyPbSl))
                {
                    ExecuteScalpTrade(TradeType.Buy, buyPbSl, "XAU_Pullback_Buy");
                    return;
                }

                if (CheckBearishPullback(m15Fast, m15Slow, currentAtr, out double sellPbSl))
                {
                    ExecuteScalpTrade(TradeType.Sell, sellPbSl, "XAU_Pullback_Sell");
                    return;
                }
            }
        }

        #region Position Closed Event & Profit-Lock Scaling State

        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            Position pos = args.Position;
            if (pos.Label != InpBotLabel || pos.SymbolName != SymbolName)
                return;

            // Only count full position closures (when no remaining slice of the position exists)
            if (Positions.FindAll(InpBotLabel, SymbolName).Length == 0)
            {
                _partialClosedIds.Remove(pos.Id);
                _initialRiskDistByPosId.Remove(pos.Id);

                if (pos.NetProfit < -1.0)
                    _dailyConsecLosses++;
                else if (pos.NetProfit > 1.0)
                    _dailyConsecLosses = 0;
            }

            // Evaluate Exponential Profit-Lock Trigger
            double eq = Account.Equity;
            double gainPct = ((eq - _baseReferenceEq) / _baseReferenceEq) * 100.0;

            if (InpUseProfitLockScaling && gainPct >= InpProfitLockTriggerPct)
            {
                double gainedUSD   = eq - _baseReferenceEq;
                _lockedEquityFloor = _baseReferenceEq + (gainedUSD * 0.50);
                _baseReferenceEq   = eq;

                Print("Profit-Lock Triggered! New Locked Equity Floor: ${0:F2} \vert{} New Reference:${1:F2}",
                    _lockedEquityFloor, _baseReferenceEq);
            }
        }

        private void CheckDailyResetAndDrawdown()
        {
            DateTime utcNow = Server.TimeInUtc;
            double eq = Account.Equity;

            if (utcNow.DayOfYear != _currentDayOfYear)
            {
                _currentDayOfYear  = utcNow.DayOfYear;
                _dayStartEquity    = eq;
                _dailyConsecLosses = 0;
                _dailyHalted       = false;
            }

            if (_dailyHalted)
                return;

            double dailyDDPct = ((_dayStartEquity - eq) / _dayStartEquity) * 100.0;
            if (dailyDDPct >= InpMaxDailyDrawdownPct)
            {
                _dailyHalted = true;
                Print("Circuit Breaker: Max Daily Drawdown ({0:F2}%) reached. Trading halted for today.", dailyDDPct);
            }
            else if (_dailyConsecLosses >= InpMaxDailyConsecLosses)
            {
                _dailyHalted = true;
                Print("Circuit Breaker: {0} Consecutive Losses reached. Trading halted for today.", _dailyConsecLosses);
            }
        }

        #endregion

        #region Liquidity Pool Mapping (PDH/PDL, Asian Session, H4 Swings)

        private void UpdateLiquidityPools()
        {
            // 1. Previous Day High & Low (Last closed D1 bar = Last(1))
            if (_d1Bars.Count >= 2)
            {
                _pdh = _d1Bars.HighPrices.Last(1);
                _pdl = _d1Bars.LowPrices.Last(1);
            }

            // 2. H4 Structural Swing High & Low (Last N closed H4 bars)
            _h4SwingHigh = 0.0;
            _h4SwingLow  = double.MaxValue;

            int h4Count = _h4Bars.Count;
            for (int i = 1; i <= InpH4SwingLookback && (h4Count - 1 - i) >= 0; i++)
            {
                double h = _h4Bars.HighPrices.Last(i);
                double l = _h4Bars.LowPrices.Last(i);
                if (h > _h4SwingHigh) _h4SwingHigh = h;
                if (l < _h4SwingLow)  _h4SwingLow  = l;
            }
            if (_h4SwingLow == double.MaxValue) _h4SwingLow = 0.0;

            // 3. Asian Session High & Low (Calculated from closed M15 bars today in UTC)
            DateTime utcNow     = Server.TimeInUtc;
            DateTime asianStart = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, InpAsianStartHour, 0, 0, DateTimeKind.Utc);
            DateTime asianEnd   = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, InpAsianEndHour,   0, 0, DateTimeKind.Utc);

            _asianHigh = 0.0;
            _asianLow  = double.MaxValue;

            int m15Count = _m15Bars.Count;
            for (int i = 1; i <= 100 && (m15Count - 1 - i) >= 0; i++)
            {
                int idx = m15Count - 1 - i;
                DateTime barTime = _m15Bars.OpenTimes[idx];
                if (barTime < asianStart)
                    break;

                if (barTime >= asianStart && barTime < asianEnd)
                {
                    double h = _m15Bars.HighPrices[idx];
                    double l = _m15Bars.LowPrices[idx];
                    if (h > _asianHigh) _asianHigh = h;
                    if (l < _asianLow)  _asianLow  = l;
                }
            }
            if (_asianLow == double.MaxValue) _asianLow = 0.0;
        }

        #endregion

        #region Entry Setup Detection (Liquidity Sweep & M15 EMA Pullback)

        private bool CheckBullishSweep(double atr, out double outSlPrice)
        {
            outSlPrice = 0.0;

            double open1  = Bars.OpenPrices.Last(1);
            double close1 = Bars.ClosePrices.Last(1);
            double high1  = Bars.HighPrices.Last(1);
            double low1   = Bars.LowPrices.Last(1);
            double low2   = Bars.LowPrices.Last(2);
            double high2  = Bars.HighPrices.Last(2);

            double range1 = high1 - low1;
            double body1  = close1 - open1;
            if (range1 <= 0.0 || body1 <= 0.45 * range1)
                return false;

            double sweepLow = Math.Min(low1, low2);
            double[] levels = { _pdl, _asianLow, _h4SwingLow };

            foreach (double lvl in levels)
            {
                if (lvl <= 0.0) continue;

                double overshoot = lvl - sweepLow;
                if (overshoot > 0.0 && overshoot <= InpMaxSweepOvershootUSD &&
                    close1 > lvl && (close1 > high2 || body1 > 0.60 * range1))
                {
                    outSlPrice = sweepLow - (atr * InpAtrStopMultiplier);
                    return true;
                }
            }
            return false;
        }

        private bool CheckBearishSweep(double atr, out double outSlPrice)
        {
            outSlPrice = 0.0;

            double open1  = Bars.OpenPrices.Last(1);
            double close1 = Bars.ClosePrices.Last(1);
            double high1  = Bars.HighPrices.Last(1);
            double low1   = Bars.LowPrices.Last(1);
            double high2  = Bars.HighPrices.Last(2);
            double low2   = Bars.LowPrices.Last(2);

            double range1 = high1 - low1;
            double body1  = open1 - close1;
            if (range1 <= 0.0 || body1 <= 0.45 * range1)
                return false;

            double sweepHigh = Math.Max(high1, high2);
            double[] levels  = { _pdh, _asianHigh, _h4SwingHigh };

            foreach (double lvl in levels)
            {
                if (lvl <= 0.0) continue;

                double overshoot = sweepHigh - lvl;
                if (overshoot > 0.0 && overshoot <= InpMaxSweepOvershootUSD &&
                    close1 < lvl && (close1 < low2 || body1 > 0.60 * range1))
                {
                    outSlPrice = sweepHigh + (atr * InpAtrStopMultiplier);
                    return true;
                }
            }
            return false;
        }

        private bool CheckBullishPullback(double emaFast, double emaSlow, double atr, out double outSlPrice)
        {
            outSlPrice = 0.0;
            if (emaFast <= emaSlow)
                return false;

            double open1  = Bars.OpenPrices.Last(1);
            double close1 = Bars.ClosePrices.Last(1);
            double low1   = Bars.LowPrices.Last(1);
            double high2  = Bars.HighPrices.Last(2);
            double low2   = Bars.LowPrices.Last(2);

            double pullbackLow = Math.Min(low1, low2);

            bool touchedValueZone = (pullbackLow <= emaFast + InpTouchToleranceUSD) &&
                                    (pullbackLow >= emaSlow - InpTouchToleranceUSD);
            bool bullishBoS       = (close1 > open1) && (close1 > emaFast) && (close1 > high2);

            if (touchedValueZone && bullishBoS)
            {
                outSlPrice = pullbackLow - (atr * InpAtrStopMultiplier);
                return true;
            }
            return false;
        }

        private bool CheckBearishPullback(double emaFast, double emaSlow, double atr, out double outSlPrice)
        {
            outSlPrice = 0.0;
            if (emaFast >= emaSlow)
                return false;

            double open1  = Bars.OpenPrices.Last(1);
            double close1 = Bars.ClosePrices.Last(1);
            double high1  = Bars.HighPrices.Last(1);
            double high2  = Bars.HighPrices.Last(2);
            double low2   = Bars.LowPrices.Last(2);

            double pullbackHigh = Math.Max(high1, high2);

            bool touchedValueZone = (pullbackHigh >= emaFast - InpTouchToleranceUSD) &&
                                    (pullbackHigh <= emaSlow + InpTouchToleranceUSD);
            bool bearishBoS       = (close1 < open1) && (close1 < emaFast) && (close1 < low2);

            if (touchedValueZone && bearishBoS)
            {
                outSlPrice = pullbackHigh + (atr * InpAtrStopMultiplier);
                return true;
            }
            return false;
        }

        #endregion

        #region Execution & ECN Commission-Adjusted Volume Sizing

        private void ExecuteScalpTrade(TradeType tradeType, double rawSlPrice, string comment)
        {
            double entryPrice = (tradeType == TradeType.Buy) ? Symbol.Ask : Symbol.Bid;
            double slDistUSD  = Math.Abs(entryPrice - rawSlPrice);

            // Clamp Stop-Loss distance to XAU/USD structural bounds ($1.20 to $3.50)
            if (slDistUSD < InpMinStopUSD)
                slDistUSD = InpMinStopUSD;
            else if (slDistUSD > InpMaxStopUSD)
                return; // Skip trade if structural stop is too wide for a scalp

            double tpDistUSD = slDistUSD * InpFinalTargetRR;

            // Convert USD Price Delta to cTrader Pips (Symbol.PipSize)
            double slPips = Math.Round(slDistUSD / Symbol.PipSize, 1);
            double tpPips = Math.Round(tpDistUSD / Symbol.PipSize, 1);

            double volumeInUnits = CalculateVolumeInUnits(slDistUSD);
            if (volumeInUnits < Symbol.VolumeInUnitsMin)
                return;

            TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, InpBotLabel, slPips, tpPips, comment);
            if (result.IsSuccessful && result.Position != null)
            {
                _initialRiskDistByPosId[result.Position.Id] = slDistUSD;
            }
        }

        private double CalculateVolumeInUnits(double slDistUSD)
        {
            double eq      = Account.Equity;
            double riskUSD = eq * (InpBaseRiskPct / 100.0);

            // Apply Exponential Profit-Lock Scaling if active and above locked floor
            if (InpUseProfitLockScaling && eq > _lockedEquityFloor)
            {
                double unlockedProfit = Math.Max(0.0, eq - _baseReferenceEq);
                if (unlockedProfit > 0.0)
                {
                    double bonusRiskUSD = unlockedProfit * (InpUnlockedBufferRiskPct / 100.0);
                    double maxRiskUSD   = eq * (InpMaxScaledRiskPct / 100.0);
                    riskUSD = Math.Min(riskUSD + bonusRiskUSD, maxRiskUSD);
                }
            }

            if (Symbol.TickSize <= 0.0 || Symbol.TickValue <= 0.0 || Symbol.LotSize <= 0.0)
                return 0.0;

            // Symbol.TickValue in cTrader is the value of 1 tick for 1 unit (1 oz on XAUUSD)
            // Multiply by Symbol.LotSize (100 oz) to get loss per 1.00 Standard Lot
            double priceLossPerUnit = (slDistUSD / Symbol.TickSize) * Symbol.TickValue;
            double priceLossPerLot  = priceLossPerUnit * Symbol.LotSize;
            double totalLossPerLot  = priceLossPerLot + InpCommissionPerLotRT;

            if (totalLossPerLot <= 0.0)
                return 0.0;

            double rawLots      = riskUSD / totalLossPerLot;
            double rawUnits     = rawLots * Symbol.LotSize;
            double normUnits    = Symbol.NormalizeVolumeInUnits(rawUnits, RoundingMode.Down);

            if (normUnits < Symbol.VolumeInUnitsMin)
                return 0.0;
            if (normUnits > Symbol.VolumeInUnitsMax)
                normUnits = Symbol.VolumeInUnitsMax;

            return normUnits;
        }

        #endregion

        #region Position Management (Partial Close, BE+Comm, M15 Trail, Time Stop)

        private void ManageOpenPositions(Position[] positions)
        {
            foreach (Position pos in positions)
            {
                double openPrice    = pos.EntryPrice;
                double currentPrice = (pos.TradeType == TradeType.Buy) ? Symbol.Bid : Symbol.Ask;
                double profitDist   = (pos.TradeType == TradeType.Buy)
                                    ? (currentPrice - openPrice)
                                    : (openPrice - currentPrice);

                // Retrieve initial 1R price distance
                if (!_initialRiskDistByPosId.TryGetValue(pos.Id, out double initialRiskDist) || initialRiskDist <= 0.0)
                {
                    initialRiskDist = pos.TakeProfit.HasValue
                        ? Math.Abs(pos.TakeProfit.Value - openPrice) / InpFinalTargetRR
                        : InpMinStopUSD;
                    _initialRiskDistByPosId[pos.Id] = initialRiskDist;
                }

                double currentR = profitDist / initialRiskDist;

                // 1. Time Stop Check: Close if open > InpTimeStopMinutes and profit < 0.5R
                if (InpTimeStopMinutes > 0)
                {
                    double elapsedMins = (Server.TimeInUtc - pos.EntryTime).TotalMinutes;
                    if (elapsedMins >= InpTimeStopMinutes && currentR < 0.5)
                    {
                        ClosePosition(pos);
                        Print("Time Stop executed on Position #{0} after {1:F0} mins (R = {2:F2})",
                            pos.Id, elapsedMins, currentR);
                        continue;
                    }
                }

                // 2. Partial Close at +1.5R & Move Stop-Loss to Breakeven + $0.15 Commission Buffer
                if (currentR >= InpPartialCloseRR && !_partialClosedIds.Contains(pos.Id))
                {
                    double rawCloseUnits  = pos.VolumeInUnits * InpPartialCloseRatio;
                    double normCloseUnits = Symbol.NormalizeVolumeInUnits(rawCloseUnits, RoundingMode.Down);

                    if (normCloseUnits >= Symbol.VolumeInUnitsMin &&
                        (pos.VolumeInUnits - normCloseUnits) >= Symbol.VolumeInUnitsMin)
                    {
                        ClosePosition(pos, normCloseUnits);
                    }

                    // Move Stop-Loss to Breakeven + $0.15 Commission/Spread Offset
                    double commOffsetUSD = 0.15;
                    double newSlPrice = (pos.TradeType == TradeType.Buy)
                        ? Math.Round(openPrice + commOffsetUSD, Symbol.Digits)
                        : Math.Round(openPrice - commOffsetUSD, Symbol.Digits);

                    bool shouldModifySL = (pos.TradeType == TradeType.Buy)
                        ? (!pos.StopLoss.HasValue || newSlPrice > pos.StopLoss.Value)
                        : (!pos.StopLoss.HasValue || newSlPrice < pos.StopLoss.Value);

                    if (shouldModifySL)
                    {
                        ModifyPosition(pos, newSlPrice, pos.TakeProfit);
                    }

                    _partialClosedIds.Add(pos.Id);
                }

                // 3. M15 Structural Trailing Stop once trade reaches +2.0R
                if (currentR

Re: Forex scalping trading strategy for Gold (XAU/USD)

Posted: Mon Oct 19, 2026 6:23 pm
by PTScalper
cTrader Backtesting & Live Deployment Notes

Attach to an XAUUSD m5 Chart: The cBot evaluates OnBar() on your chart's primary timeframe (m5 recommended) while automatically pulling m15, h4, and D1 series in the background via MarketData.GetBars(...).

Accurate Commission Simulation in Backtesting: In the cTrader Automate Backtesting Settings tab (gear icon), set Data to "Tick data from Server (accurate)" and set Commission to 35 per million USD (or equivalent $3.50 per side / $7.00 round-turn per standard lot) so the Strategy Tester's net equity curve matches the internal sizing formula.

Mobile Notification / cTrader ID Sync: If you monitor or manage runners via cTrader Mobile while the cBot runs on a VPS or workstation, keep InpBotLabel unique (XAU_Scalper_2026) so the cBot only manages its own positions and leaves any manual mobile scalp entries untouched.

Re: Forex scalping trading strategy for Gold (XAU/USD)

Posted: Mon Oct 19, 2026 6:24 pm
by PTScalper
This extended version of the cTrader cBot (XAUUSD_LiquidityScalper.cs) adds:

Live Chart Drawings: Automatically plots and updates horizontal lines for PDH, PDL, Asian Session High/Low, and H4 Structural Swings directly on the cTrader chart with distinct color codings.

Top-Right HUD (Chart.DrawStaticText): Renders a real-time HUD displaying live spread, current equity, locked equity floor, session status, and active position metrics.

Re: Forex scalping trading strategy for Gold (XAU/USD)

Posted: Mon Oct 19, 2026 6:25 pm
by PTScalper
Extended cTrader cBot Code (XAUUSD_LiquidityScalper.cs)

Replace your cBot code in cTrader Automate with the script below and press Ctrl + B to build:

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 XAUUSD_LiquidityScalper : Robot
    {
        #region 1. Strategy & Setup Filters

        [Parameter("Enable Liquidity Sweep Setup", Group = "1. Strategy & Setup Filters", DefaultValue = true)]
        public bool InpEnableSweepSetup { get; set; }

        [Parameter("Enable M15 EMA Pullback Setup", Group = "1. Strategy & Setup Filters", DefaultValue = true)]
        public bool InpEnablePullbackSetup { get; set; }

        [Parameter("M15 Fast EMA Period", Group = "1. Strategy & Setup Filters", DefaultValue = 20, MinValue = 5)]
        public int InpEmaFast { get; set; }

        [Parameter("M15 Slow EMA Period", Group = "1. Strategy & Setup Filters", DefaultValue = 50, MinValue = 10)]
        public int InpEmaSlow { get; set; }

        [Parameter("M5 ATR Period", Group = "1. Strategy & Setup Filters", DefaultValue = 14, MinValue = 5)]
        public int InpAtrPeriod { get; set; }

        [Parameter("H4 Swing Lookback (Bars)", Group = "1. Strategy & Setup Filters", DefaultValue = 10, MinValue = 3)]
        public int InpH4SwingLookback { get; set; }

        [Parameter("Max Sweep Overshoot ($ USD)", Group = "1. Strategy & Setup Filters", DefaultValue = 2.50, MinValue = 0.20, Step = 0.10)]
        public double InpMaxSweepOvershootUSD { get; set; }

        [Parameter("EMA Touch Tolerance ($ USD)", Group = "1. Strategy & Setup Filters", DefaultValue = 0.35, MinValue = 0.05, Step = 0.05)]
        public double InpTouchToleranceUSD { get; set; }

        #endregion

        #region 2. XAU/USD Stop-Loss & Target Calibration

        [Parameter("ATR Multiplier Beyond Wick", Group = "2. Stop-Loss & Targets", DefaultValue = 1.0, MinValue = 0.2, Step = 0.1)]
        public double InpAtrStopMultiplier { get; set; }

        [Parameter("Min Structural Stop ($ USD)", Group = "2. Stop-Loss & Targets", DefaultValue = 1.20, MinValue = 0.50, Step = 0.10)]
        public double InpMinStopUSD { get; set; }

        [Parameter("Max Structural Stop ($ USD)", Group = "2. Stop-Loss & Targets", DefaultValue = 3.50, MinValue = 1.50, Step = 0.10)]
        public double InpMaxStopUSD { get; set; }

        [Parameter("Partial Close & BE Trigger (R)", Group = "2. Stop-Loss & Targets", DefaultValue = 1.5, MinValue = 0.5, Step = 0.1)]
        public double InpPartialCloseRR { get; set; }

        [Parameter("Partial Close Ratio (0.5 = 50%)", Group = "2. Stop-Loss & Targets", DefaultValue = 0.50, MinValue = 0.10, MaxValue = 0.90, Step = 0.05)]
        public double InpPartialCloseRatio { get; set; }

        [Parameter("Start M15 Structural Trail (R)", Group = "2. Stop-Loss & Targets", DefaultValue = 2.0, MinValue = 1.0, Step = 0.1)]
        public double InpTrailStartRR { get; set; }

        [Parameter("Final Hard Target (R)", Group = "2. Stop-Loss & Targets", DefaultValue = 3.5, MinValue = 1.5, Step = 0.1)]
        public double InpFinalTargetRR { get; set; }

        [Parameter("Time Stop if < 0.5R (Mins, 0=Off)", Group = "2. Stop-Loss & Targets", DefaultValue = 45, MinValue = 0)]
        public int InpTimeStopMinutes { get; set; }

        #endregion

        #region 3. Risk, ECN Commission & Profit-Lock Scaling

        [Parameter("Baseline Risk Per Trade (%)", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 0.50, MinValue = 0.10, MaxValue = 5.0, Step = 0.05)]
        public double InpBaseRiskPct { get; set; }

        [Parameter("Round-Turn Commission / Lot ($)", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 7.00, MinValue = 0.0, Step = 0.50)]
        public double InpCommissionPerLotRT { get; set; }

        [Parameter("Enable Profit-Lock Scaling", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = true)]
        public bool InpUseProfitLockScaling { get; set; }

        [Parameter("Gain % to Trigger 50% Lock", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 2.00, MinValue = 0.50, Step = 0.25)]
        public double InpProfitLockTriggerPct { get; set; }

        [Parameter("% of Unlocked Buffer Added to Risk", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 25.0, MinValue = 5.0, MaxValue = 100.0, Step = 5.0)]
        public double InpUnlockedBufferRiskPct { get; set; }

        [Parameter("Hard Cap on Scaled Risk (%)", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 1.25, MinValue = 0.25, MaxValue = 5.0, Step = 0.25)]
        public double InpMaxScaledRiskPct { get; set; }

        [Parameter("Daily Max Drawdown Breaker (%)", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 1.50, MinValue = 0.50, Step = 0.25)]
        public double InpMaxDailyDrawdownPct { get; set; }

        [Parameter("Max Consecutive Losses / Day", Group = "3. Risk & Profit-Lock Scaling", DefaultValue = 3, MinValue = 1)]
        public int InpMaxDailyConsecLosses { get; set; }

        #endregion

        #region 4. Raw ECN Spread & Session Windows (UTC)

        [Parameter("Max Allowed Live Spread ($ USD)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 0.25, MinValue = 0.05, Step = 0.05)]
        public double InpMaxSpreadUSD { get; set; }

        [Parameter("Asian Start Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 0, MinValue = 0, MaxValue = 23)]
        public int InpAsianStartHour { get; set; }

        [Parameter("Asian End Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 7, MinValue = 1, MaxValue = 23)]
        public int InpAsianEndHour { get; set; }

        [Parameter("Trade London Open Window", Group = "4. Spread & Session Windows (UTC)", DefaultValue = true)]
        public bool InpTradeLondonSession { get; set; }

        [Parameter("London Start Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 7, MinValue = 0, MaxValue = 23)]
        public int InpLondonStartHour { get; set; }

        [Parameter("London End Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 10, MinValue = 1, MaxValue = 23)]
        public int InpLondonEndHour { get; set; }

        [Parameter("Trade London-NY Overlap", Group = "4. Spread & Session Windows (UTC)", DefaultValue = true)]
        public bool InpTradeNYOverlap { get; set; }

        [Parameter("NY Overlap Start Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 12, MinValue = 0, MaxValue = 23)]
        public int InpNYStartHour { get; set; }

        [Parameter("NY Overlap End Hour (UTC)", Group = "4. Spread & Session Windows (UTC)", DefaultValue = 16, MinValue = 1, MaxValue = 23)]
        public int InpNYEndHour { get; set; }

        [Parameter("Bot Label", Group = "4. Spread & Session Windows (UTC)", DefaultValue = "XAU_Scalper_2026")]
        public string InpBotLabel { get; set; }

        #endregion

        #region Private Fields & Multi-Timeframe Series

        private Bars _m5Bars;
        private Bars _m15Bars;
        private Bars _h4Bars;
        private Bars _d1Bars;

        private ExponentialMovingAverage _emaFastM15;
        private ExponentialMovingAverage _emaSlowM15;
        private AverageTrueRange _atrM5;

        // Risk & Profit-Lock State
        private int _currentDayOfYear = -1;
        private double _dayStartEquity;
        private double _baseReferenceEq;
        private double _lockedEquityFloor;
        private int _dailyConsecLosses;
        private bool _dailyHalted;

        // Liquidity Pool Cache
        private double _pdh;
        private double _pdl;
        private double _asianHigh;
        private double _asianLow;
        private double _h4SwingHigh;
        private double _h4SwingLow;

        // Track 1R price distance and Partial-Close execution per Position ID
        private readonly HashSet<int> _partialClosedIds = new HashSet<int>();
        private readonly Dictionary<int, double> _initialRiskDistByPosId = new Dictionary<int, double>();

        #endregion

        protected override void OnStart()
        {
            // Load Multi-Timeframe Bars
            _m5Bars  = MarketData.GetBars(TimeFrame.Minute5);
            _m15Bars = MarketData.GetBars(TimeFrame.Minute15);
            _h4Bars  = MarketData.GetBars(TimeFrame.Hour4);
            _d1Bars  = MarketData.GetBars(TimeFrame.Daily);

            // Initialize Indicators
            _emaFastM15 = Indicators.ExponentialMovingAverage(_m15Bars.ClosePrices, InpEmaFast);
            _emaSlowM15 = Indicators.ExponentialMovingAverage(_m15Bars.ClosePrices, InpEmaSlow);
            _atrM5      = Indicators.AverageTrueRange(_m5Bars, InpAtrPeriod, MovingAverageType.Exponential);

            // Initialize Equity & Profit-Lock State
            double eq = Account.Equity;
            _dayStartEquity    = eq;
            _baseReferenceEq   = eq;
            _lockedEquityFloor = eq * (1.0 - InpMaxDailyDrawdownPct / 100.0);
            _currentDayOfYear  = Server.TimeInUtc.DayOfYear;

            Positions.Closed += OnPositionClosed;

            Print("XAUUSD_LiquidityScalper initialized on {0}. PipSize: {1}, TickSize: {2}, LotSize: {3}",
                SymbolName, Symbol.PipSize, Symbol.TickSize, Symbol.LotSize);
        }

        protected override void OnStop()
        {
            Positions.Closed -= OnPositionClosed;
            ClearChartDrawings();
        }

        protected override void OnTick()
        {
            // 1. Daily Reset & Circuit Breaker Audit
            CheckDailyResetAndDrawdown();

            // 2. Manage Open Positions on Every Tick
            var activePositions = Positions.FindAll(InpBotLabel, SymbolName);
            if (activePositions.Length > 0)
            {
                ManageOpenPositions(activePositions);
            }

            // 3. Update HUD Display on Every Tick for Live Spread & Equity Feedback
            UpdateChartHUD();
        }

        protected override void OnBar()
        {
            if (_dailyHalted)
                return;

            // One active position at a time on XAU/USD
            if (Positions.FindAll(InpBotLabel, SymbolName).Length > 0)
                return;

            // 1. Verify Active Session Window (UTC)
            if (!IsWithinActiveSession())
                return;

            // 2. Verify Live Spread Filter ($ USD)
            double liveSpreadUSD = Symbol.Ask - Symbol.Bid;
            if (liveSpreadUSD > InpMaxSpreadUSD)
                return;

            // 3. Refresh H4/D1 & Asian Session Liquidity Pools & Draw Chart Lines
            UpdateLiquidityPools();
            DrawLiquidityLines();

            double currentAtr = _atrM5.Result.Last(1);
            if (double.IsNaN(currentAtr) || currentAtr <= 0.0)
                return;

            // 4. Evaluate Setup 1: Liquidity Sweep
            if (InpEnableSweepSetup)
            {
                if (CheckBullishSweep(currentAtr, out double buySweepSl))
                {
                    ExecuteScalpTrade(TradeType.Buy, buySweepSl, "XAU_Sweep_Buy");
                    return;
                }

                if (CheckBearishSweep(currentAtr, out double sellSweepSl))
                {
                    ExecuteScalpTrade(TradeType.Sell, sellSweepSl, "XAU_Sweep_Sell");
                    return;
                }
            }

            // 5. Evaluate Setup 2: M15 EMA 20/50 Trend Pullback
            if (InpEnablePullbackSetup)
            {
                double m15Fast = _emaFastM15.Result.Last(1);
                double m15Slow = _emaSlowM15.Result.Last(1);

                if (CheckBullishPullback(m15Fast, m15Slow, currentAtr, out double buyPbSl))
                {
                    ExecuteScalpTrade(TradeType.Buy, buyPbSl, "XAU_Pullback_Buy");
                    return;
                }

                if (CheckBearishPullback(m15Fast, m15Slow, currentAtr, out double sellPbSl))
                {
                    ExecuteScalpTrade(TradeType.Sell, sellPbSl, "XAU_Pullback_Sell");
                    return;
                }
            }
        }

        #region Chart Drawings (Liquidity Lines & Top-Right HUD)

        private void DrawLiquidityLines()
        {
            if (_pdh > 0) Chart.DrawHorizontalLine("Line_PDH", _pdh, Color.Gold, 1, LineStyle.Lines);
            if (_pdl > 0) Chart.DrawHorizontalLine("Line_PDL", _pdl, Color.OrangeRed, 1, LineStyle.Lines);
            if (_asianHigh > 0) Chart.DrawHorizontalLine("Line_AsianHigh", _asianHigh, Color.DodgerBlue, 1, LineStyle.Dots);
            if (_asianLow > 0) Chart.DrawHorizontalLine("Line_AsianLow", _asianLow, Color.DodgerBlue, 1, LineStyle.Dots);
            if (_h4SwingHigh > 0) Chart.DrawHorizontalLine("Line_H4High", _h4SwingHigh, Color.MediumPurple, 2, LineStyle.Solid);
            if (_h4SwingLow > 0) Chart.DrawHorizontalLine("Line_H4Low", _h4SwingLow, Color.MediumPurple, 2, LineStyle.Solid);
        }

        private void ClearChartDrawings()
        {
            Chart.RemoveObject("Line_PDH");
            Chart.RemoveObject("Line_PDL");
            Chart.RemoveObject("Line_AsianHigh");
            Chart.RemoveObject("Line_AsianLow");
            Chart.RemoveObject("Line_H4High");
            Chart.RemoveObject("Line_H4Low");
            Chart.RemoveObject("XAU_Scalper_HUD");
        }

        private void UpdateChartHUD()
        {
            double liveSpreadUSD = Symbol.Ask - Symbol.Bid;
            double eq = Account.Equity;
            double gainPct = ((eq - _baseReferenceEq) / _baseReferenceEq) * 100.0;
            
            // Calculate indicative lot size for current default risk at current ATR
            double sampleAtr = _atrM5.Result.Last(1);
            double sampleLots = sampleAtr > 0 ? CalculateVolumeInUnits(sampleAtr * InpAtrStopMultiplier) / Symbol.LotSize : 0.0;

            string sessionStatus = "OUT OF SESSION";
            int hour = Server.TimeInUtc.Hour;
            bool inLondon = InpTradeLondonSession && (hour >= InpLondonStartHour && hour < InpLondonEndHour);
            bool inNY = InpTradeNYOverlap && (hour >= InpNYStartHour && hour < InpNYEndHour);
            if (inLondon) sessionStatus = "LONDON OPEN";
            if (inNY) sessionStatus = "NY OVERLAP";

            string hudText = string.Format(
                "=== XAU/USD SCALPER HUD ===\n" +
                "Session: {0} (UTC {1:D2}:00)\n" +
                "Live Spread: ${1:F2} (Max: ${2:F2})\n" +
                "Equity: ${3:F2} | Gain: {4:+0.00;-0.00}%\n" +
                "Locked Floor: ${5:F2}\n" +
                "Consec. Losses: {6}/{7} | Halted: {8}\n" +
                "Indicative Lot Size: {9:F2} Lots\n" +
                "Active Positions: {10}",
                sessionStatus,
                hour,
                liveSpreadUSD,
                InpMaxSpreadUSD,
                eq,
                gainPct,
                _lockedEquityFloor,
                _dailyConsecLosses,
                InpMaxDailyConsecLosses,
                _dailyHalted,
                sampleLots,
                Positions.FindAll(InpBotLabel, SymbolName).Length
            );

            Chart.DrawStaticText("XAU_Scalper_HUD", hudText, VerticalAlignment.Top, HorizontalAlignment.Right, Color.White);
        }

        #endregion

        #region Position Closed Event & Profit-Lock Scaling State

        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            Position pos = args.Position;
            if (pos.Label != InpBotLabel || pos.SymbolName != SymbolName)
                return;

            if (Positions.FindAll(InpBotLabel, SymbolName).Length == 0)
            {
                _partialClosedIds.Remove(pos.Id);
                _initialRiskDistByPosId.Remove(pos.Id);

                if (pos.NetProfit < -1.0)
                    _dailyConsecLosses++;
                else if (pos.NetProfit > 1.0)
                    _dailyConsecLosses = 0;
            }

            double eq = Account.Equity;
            double gainPct = ((eq - _baseReferenceEq) / _baseReferenceEq) * 100.0;

            if (InpUseProfitLockScaling && gainPct >= InpProfitLockTriggerPct)
            {
                double gainedUSD   = eq - _baseReferenceEq;
                _lockedEquityFloor = _baseReferenceEq + (gainedUSD * 0.50);
                _baseReferenceEq   = eq;

                Print("Profit-Lock Triggered! New Locked Equity Floor: ${0:F2} | New Reference: ${1:F2}",
                    _lockedEquityFloor, _baseReferenceEq);
            }
        }

        private void CheckDailyResetAndDrawdown()
        {
            DateTime utcNow = Server.TimeInUtc;
            double eq = Account.Equity;

            if (utcNow.DayOfYear != _currentDayOfYear)
            {
                _currentDayOfYear  = utcNow.DayOfYear;
                _dayStartEquity    = eq;
                _dailyConsecLosses = 0;
                _dailyHalted       = false;
            }

            if (_dailyHalted)
                return;

            double dailyDDPct = ((_dayStartEquity - eq) / _dayStartEquity) * 100.0;
            if (dailyDDPct >= InpMaxDailyDrawdownPct)
            {
                _dailyHalted = true;
                Print("Circuit Breaker: Max Daily Drawdown ({0:F2}%) reached. Trading halted for today.", dailyDDPct);
            }
            else if (_dailyConsecLosses >= InpMaxDailyConsecLosses)
            {
                _dailyHalted = true;
                Print("Circuit Breaker: {0} Consecutive Losses reached. Trading halted for today.", _dailyConsecLosses);
            }
        }

        #endregion

        #region Liquidity Pool Mapping

        private void UpdateLiquidityPools()
        {
            if (_d1Bars.Count >= 2)
            {
                _pdh = _d1Bars.HighPrices.Last(1);
                _pdl = _d1Bars.LowPrices.Last(1);
            }

            _h4SwingHigh = 0.0;
            _h4SwingLow  = double.MaxValue;

            int h4Count = _h4Bars.Count;
            for (int i = 1; i <= InpH4SwingLookback && (h4Count - 1 - i) >= 0; i++)
            {
                double h = _h4Bars.HighPrices.Last(i);
                double l = _h4Bars.LowPrices.Last(i);
                if (h > _h4SwingHigh) _h4SwingHigh = h;
                if (l < _h4SwingLow)  _h4SwingLow  = l;
            }
            if (_h4SwingLow == double.MaxValue) _h4SwingLow = 0.0;

            DateTime utcNow     = Server.TimeInUtc;
            DateTime asianStart = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, InpAsianStartHour, 0, 0, DateTimeKind.Utc);
            DateTime asianEnd   = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, InpAsianEndHour,   0, 0, DateTimeKind.Utc);

            _asianHigh = 0.0;
            _asianLow  = double.MaxValue;

            int m15Count = _m15Bars.Count;
            for (int i = 1; i <= 100 && (m15Count - 1 - i) >= 0; i++)
            {
                int idx = m15Count - 1 - i;
                DateTime barTime = _m15Bars.OpenTimes[idx];
                if (barTime < asianStart)
                    break;

                if (barTime >= asianStart && barTime < asianEnd)
                {
                    double h = _m15Bars.HighPrices[idx];
                    double l = _m15Bars.LowPrices[idx];
                    if (h > _asianHigh) _asianHigh = h;
                    if (l < _asianLow)  _asianLow  = l;
                }
            }
            if (_asianLow == double.MaxValue) _asianLow = 0.0;
        }

        #endregion

        #region Entry Setup Detection

        private bool CheckBullishSweep(double atr, out double outSlPrice)
        {
            outSlPrice = 0.0;

            double open1  = Bars.OpenPrices.Last(1);
            double close1 = Bars.ClosePrices.Last(1);
            double high1  = Bars.HighPrices.Last(1);
            double low1   = Bars.LowPrices.Last(1);
            double low2   = Bars.LowPrices.Last(2);
            double high2  = Bars.HighPrices.Last(2);

            double range1 = high1 - low1;
            double body1  = close1 - open1;
            if (range1 <= 0.0 || body1 <= 0.45 * range1)
                return false;

            double sweepLow = Math.Min(low1, low2);
            double[] levels = { _pdl, _asianLow, _h4SwingLow };

            foreach (double lvl in levels)
            {
                if (lvl <= 0.0) continue;

                double overshoot = lvl - sweepLow;
                if (overshoot > 0.0 && overshoot <= InpMaxSweepOvershootUSD &&
                    close1 > lvl && (close1 > high2 || body1 > 0.60 * range1))
                {
                    outSlPrice = sweepLow - (atr * InpAtrStopMultiplier);
                    return true;
                }
            }
            return false;
        }

        private bool CheckBearishSweep(double atr, out double outSlPrice)
        {
            outSlPrice = 0.0;

            double open1  = Bars.OpenPrices.Last(1);
            double close1 = Bars.ClosePrices.Last(1);
            double high1  = Bars.HighPrices.Last(1);
            double low1   = Bars.LowPrices.Last(1);
            double high2  = Bars.HighPrices.Last(2);
            double low2   = Bars.LowPrices.Last(2);

            double range1 = high1 - low1;
            double body1  = open1 - close1;
            if (range1 <= 0.0 || body1 <= 0.45 * range1)
                return false;

            double sweepHigh = Math.Max(high1, high2);
            double[] levels  = { _pdh, _asianHigh, _h4SwingHigh };

            foreach (double lvl in levels)
            {
                if (lvl <= 0.0) continue;

                double overshoot = sweepHigh - lvl;
                if (overshoot > 0.0 && overshoot <= InpMaxSweepOvershootUSD &&
                    close1 < lvl && (close1 < low2 || body1 > 0.60 * range1))
                {
                    outSlPrice = sweepHigh + (atr * InpAtrStopMultiplier);
                    return true;
                }
            }
            return false;
        }

        private bool CheckBullishPullback(double emaFast, double emaSlow, double atr, out double outSlPrice)
        {
            outSlPrice = 0.0;
            if (emaFast <= emaSlow)
                return false;

            double open1  = Bars.OpenPrices.Last(1);
            double close1 = Bars.ClosePrices.Last(1);
            double low1   = Bars.LowPrices.Last(1);
            double high2  = Bars.HighPrices.Last(2);
            double low2   = Bars.LowPrices.Last(2);

            double pullbackLow = Math.Min(low1, low2);

            bool touchedValueZone = (pullbackLow <= emaFast + InpTouchToleranceUSD) &&
                                    (pullbackLow >= emaSlow - InpTouchToleranceUSD);
            bool bullishBoS       = (close1 > open1) && (close1 > emaFast) && (close1 > high2);

            if (touchedValueZone && bullishBoS)
            {
                outSlPrice = pullbackLow - (atr * InpAtrStopMultiplier);
                return true;
            }
            return false;
        }

        private bool CheckBearishPullback(double emaFast, double emaSlow, double atr, out double outSlPrice)
        {
            outSlPrice = 0.0;
            if (emaFast >= emaSlow)
                return false;

            double open1  = Bars.OpenPrices.Last(1);
            double close1 = Bars.ClosePrices.Last(1);
            double high1  = Bars.HighPrices.Last(1);
            double high2  = Bars.HighPrices.Last(2);
            double low2   = Bars.LowPrices.Last(2);

            double pullbackHigh = Math.Max(high1, high2);

            bool touchedValueZone = (pullbackHigh >= emaFast - InpTouchToleranceUSD) &&
                                    (pullbackHigh <= emaSlow + InpTouchToleranceUSD);
            bool bearishBoS       = (close1 < open1) && (close1 < emaFast) && (close1 < low2);

            if (touchedValueZone && bearishBoS)
            {
                outSlPrice = pullbackHigh + (atr * InpAtrStopMultiplier);
                return true;
            }
            return false;
        }

        #endregion

        #region Execution & Volume Sizing

        private void ExecuteScalpTrade(TradeType tradeType, double rawSlPrice, string comment)
        {
            double entryPrice = (tradeType == TradeType.Buy) ? Symbol.Ask : Symbol.Bid;
            double slDistUSD  = Math.Abs(entryPrice - rawSlPrice);

            if (slDistUSD < InpMinStopUSD)
                slDistUSD = InpMinStopUSD;
            else if (slDistUSD > InpMaxStopUSD)
                return;

            double tpDistUSD = slDistUSD * InpFinalTargetRR;

            double slPips = Math.Round(slDistUSD / Symbol.PipSize, 1);
            double tpPips = Math.Round(tpDistUSD / Symbol.PipSize, 1);

            double volumeInUnits = CalculateVolumeInUnits(slDistUSD);
            if (volumeInUnits < Symbol.VolumeInUnitsMin)
                return;

            TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, InpBotLabel, slPips, tpPips, comment);
            if (result.IsSuccessful && result.Position != null)
            {
                _initialRiskDistByPosId[result.Position.Id] = slDistUSD;
            }
        }

        private double CalculateVolumeInUnits(double slDistUSD)
        {
            double eq      = Account.Equity;
            double riskUSD = eq * (InpBaseRiskPct / 100.0);

            if (InpUseProfitLockScaling && eq > _lockedEquityFloor)
            {
                double unlockedProfit = Math.Max(0.0, eq - _baseReferenceEq);
                if (unlockedProfit > 0.0)
                {
                    double bonusRiskUSD = unlockedProfit * (InpUnlockedBufferRiskPct / 100.0);
                    double maxRiskUSD   = eq * (InpMaxScaledRiskPct / 100.0);
                    riskUSD = Math.Min(riskUSD + bonusRiskUSD, maxRiskUSD);
                }
            }

            if (Symbol.TickSize <= 0.0 || Symbol.TickValue <= 0.0 || Symbol.LotSize <= 0.0)
                return 0.0;

            double priceLossPerUnit = (slDistUSD / Symbol.TickSize) * Symbol.TickValue;
            double priceLossPerLot  = priceLossPerUnit * Symbol.LotSize;
            double totalLossPerLot  = priceLossPerLot + InpCommissionPerLotRT;

            if (totalLossPerLot <= 0.0)
                return 0.0;

            double rawLots      = riskUSD / totalLossPerLot;
            double rawUnits     = rawLots * Symbol.LotSize;
            double normUnits    = Symbol.NormalizeVolumeInUnits(rawUnits, RoundingMode.Down);

            if (normUnits < Symbol.VolumeInUnitsMin)
                return 0.0;
            if (normUnits > Symbol.VolumeInUnitsMax)
                normUnits = Symbol.VolumeInUnitsMax;

            return normUnits;
        }

        #endregion

        #region Position Management

        private void ManageOpenPositions(Position[] positions)
        {
            foreach (Position pos in positions)
            {
                double openPrice    = pos.EntryPrice;
                double currentPrice = (pos.TradeType == TradeType.Buy) ? Symbol.Bid : Symbol.Ask;
                double profitDist   = (pos.TradeType == TradeType.Buy)
                                    ? (currentPrice - openPrice)
                                    : (openPrice - currentPrice);

                if (!_initialRiskDistByPosId.TryGetValue(pos.Id, out double initialRiskDist) || initialRiskDist <= 0.0)
                {
                    initialRiskDist = pos.TakeProfit.HasValue
                        ? Math.Abs(pos.TakeProfit.Value - openPrice) / InpFinalTargetRR
                        : InpMinStopUSD;
                    _initialRiskDistByPosId[pos.Id] = initialRiskDist;
                }

                double currentR = profitDist / initialRiskDist;

                if (InpTimeStopMinutes > 0)
                {
                    double elapsedMins = (Server.TimeInUtc - pos.EntryTime).TotalMinutes;
                    if (elapsedMins >= InpTimeStopMinutes && currentR < 0.5)
                    {
                        ClosePosition(pos);
                        Print("Time Stop executed on Position #{0} after {1:F0} mins (R = {2:F2})",
                            pos.Id, elapsedMins, currentR);
                        continue;
                    }
                }

                if (currentR >= InpPartialCloseRR && !_partialClosedIds.Contains(pos.Id))
                {
                    double rawCloseUnits  = pos.VolumeInUnits * InpPartialCloseRatio;
                    double normCloseUnits = Symbol.NormalizeVolumeInUnits(rawCloseUnits, RoundingMode.Down);

                    if (normCloseUnits >= Symbol.VolumeInUnitsMin &&
                        (pos.VolumeInUnits - normCloseUnits) >= Symbol.VolumeInUnitsMin)
                    {
                        ClosePosition(pos, normCloseUnits);
                    }

                    double commOffsetUSD = 0.15;
                    double newSlPrice = (pos.TradeType == TradeType.Buy)
                        ? Math.Round(openPrice + commOffsetUSD, Symbol.Digits)
                        : Math.Round(openPrice - commOffsetUSD, Symbol.Digits);

                    bool shouldModifySL = (pos.TradeType == TradeType.Buy)
                        ? (!pos.StopLoss.HasValue || newSlPrice > pos.StopLoss.Value)
                        : (!pos.StopLoss.HasValue || newSlPrice < pos.StopLoss.Value);

                    if (shouldModifySL)
                    {
                        ModifyPosition(pos, newSlPrice, pos.TakeProfit);
                    }

                    _partialClosedIds.Add(pos.Id);
                }

                if (currentR >= InpTrailStartRR && _m15Bars.Count >= 2)
                {
                    double trailBuffer = 0.20;
                    if (pos.TradeType == TradeType.Buy)
                    {
                        double m15Low = Math.Round(_m15Bars.LowPrices.Last(1) - trailBuffer, Symbol.Digits);
                        if ((!pos.StopLoss.HasValue || m15Low > pos.StopLoss.Value) && m15Low < currentPrice)
                        {
                            ModifyPosition(pos, m15Low, pos.TakeProfit);
                        }
                    }
                    else
                    {
                        double m15High = Math.Round(_m15Bars.HighPrices.Last(1) + trailBuffer, Symbol.Digits);
                        if ((!pos.StopLoss.HasValue || m15High < pos.StopLoss.Value) && m15High > currentPrice)
                        {
                            ModifyPosition(pos, m15High, pos.StopLoss); // Bugfix: preserve TP
                        }
                    }
                }
            }
        }

        private bool IsWithinActiveSession()
        {
            int hour = Server.TimeInUtc.Hour;

            bool inLondon = InpTradeLondonSession && (hour >= InpLondonStartHour && hour < InpLondonEndHour);
            bool inNY     = InpTradeNYOverlap     && (hour >= InpNYStartHour     && hour < InpNYEndHour);

            return inLondon || inNY;
        }

        #endregion
    }
}