Advertisement IC Markets

Scalping AUDNZD: A Cross Driven by Relative Rates and Quiet Ranges

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
FTtrader
Posts: 1008
Joined: Mon Aug 03, 2026 2:43 pm

Re: Scalping AUDNZD: A Cross Driven by Relative Rates and Quiet Ranges

Post by FTtrader »

Since you are building in C# for cAlgo, we can leverage the full power of the .NET framework and LINQ to make this architecture significantly cleaner than the MQL versions.

In cTrader, we expose the data to your cBot using [Output] arrays (equivalent to IndicatorDataSeries) and rely on Chart.DrawRectangle and Chart.DrawTrendLine for visual projections. I have implemented a state machine for the Asian session tracking and used LINQ to calculate the 14-Day ADR dynamically from the Daily timeframe.

Here is the native cTrader C# implementation.
Recommended broker for automated trading & scalping IC Markets
FTtrader
Posts: 1008
Joined: Mon Aug 03, 2026 2:43 pm

Re: Scalping AUDNZD: A Cross Driven by Relative Rates and Quiet Ranges

Post by FTtrader »

C# cTrader Implementation: Institutional Liquidity Matrix

Save this as AUDNZD_AsianMatrix_Pro.cs in your cAlgo Indicators folder.

Code: Select all

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

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

        [Parameter("Session Start (Server Time)", DefaultValue = "22:00", Group = "Session Timing")]
        public string SessionStartStr { get; set; }

        [Parameter("Session End (Server Time)", DefaultValue = "06:00", Group = "Session Timing")]
        public string SessionEndStr { get; set; }

        [Parameter("Trade Window End Hour", DefaultValue = 17, Group = "Session Timing")]
        public int TradeWindowEndHour { get; set; }

        [Parameter("Max Spread (Pips)", DefaultValue = 2.0, Group = "Microstructure Filters")]
        public double MaxSpreadPips { get; set; }

        [Parameter("Min Range (Pips)", DefaultValue = 15.0, Group = "Microstructure Filters")]
        public double MinRangePips { get; set; }

        [Parameter("Max Range (Pips)", DefaultValue = 60.0, Group = "Microstructure Filters")]
        public double MaxRangePips { get; set; }

        [Parameter("Require Vol Spike", DefaultValue = true, Group = "Microstructure Filters")]
        public bool RequireVolSpike { get; set; }

        [Parameter("Vol Multiplier vs SMA20", DefaultValue = 1.15, Group = "Microstructure Filters")]
        public double VolMultiplier { get; set; }

        [Parameter("SL Buffer (Pips)", DefaultValue = 2.5, Group = "Microstructure Filters")]
        public double SLBufferPips { get; set; }

        [Parameter("Box Color", DefaultValue = "#1E152528", Group = "Visuals")]
        public string BoxColorStr { get; set; }

        [Parameter("Show HUD", DefaultValue = true, Group = "Visuals")]
        public bool ShowHud { get; set; }

        #endregion

        #region Outputs for cBot Consumption

        [Output("Short Signal", LineColor = "Crimson", PlotType = PlotType.Points, Thickness = 4)]
        public IndicatorDataSeries BufferShortSignal { get; set; }

        [Output("Long Signal", LineColor = "MediumSeaGreen", PlotType = PlotType.Points, Thickness = 4)]
        public IndicatorDataSeries BufferLongSignal { get; set; }

        [Output("Calculated SL", LineColor = "Transparent")]
        public IndicatorDataSeries BufferCalculatedSL { get; set; }

        [Output("Target TP1", LineColor = "Transparent")]
        public IndicatorDataSeries BufferTargetTP1 { get; set; }

        [Output("Target TP2", LineColor = "Transparent")]
        public IndicatorDataSeries BufferTargetTP2 { get; set; }

        #endregion

        #region Internal State

        private TimeSpan _sessionStart;
        private TimeSpan _sessionEnd;
        private SimpleMovingAverage _volSma;
        private Bars _dailyBars;
        private Color _boxColor;

        // Session state
        private double _sessHigh = double.MinValue;
        private double _sessLow = double.MaxValue;
        private DateTime _sessStartTime;
        private bool _isAccumulating;

        // Alert state
        private int _lastAlertIndex = -1;

        #endregion

        protected override void Initialize()
        {
            if (!TimeSpan.TryParse(SessionStartStr, out _sessionStart) || !TimeSpan.TryParse(SessionEndStr, out _sessionEnd))
            {
                Print("Invalid Session Time format. Use HH:MM");
            }

            _boxColor = Color.FromHex(BoxColorStr);
            _volSma = Indicators.SimpleMovingAverage(Bars.TickVolumes, 20);
            _dailyBars = MarketData.GetBars(TimeFrame.Daily);
        }

        public override void Calculate(int index)
        {
            // Initialize empty buffers (cTrader standard for no-signal)
            BufferShortSignal[index] = double.NaN;
            BufferLongSignal[index] = double.NaN;
            BufferCalculatedSL[index] = double.NaN;
            BufferTargetTP1[index] = double.NaN;
            BufferTargetTP2[index] = double.NaN;

            if (index < 20) return;

            DateTime barTime = Bars.OpenTimes[index];
            bool inSession = IsInAccumulation(barTime);

            // Handle Session State Transitions
            if (inSession)
            {
                if (!_isAccumulating)
                {
                    // Session just started
                    _isAccumulating = true;
                    _sessStartTime = barTime;
                    _sessHigh = Bars.HighPrices[index];
                    _sessLow = Bars.LowPrices[index];
                }
                else
                {
                    // Update ongoing session
                    _sessHigh = Math.Max(_sessHigh, Bars.HighPrices[index]);
                    _sessLow = Math.Min(_sessLow, Bars.LowPrices[index]);
                }
            }
            else
            {
                if (_isAccumulating)
                {
                    // Session just ended -> Draw Geometries
                    _isAccumulating = false;
                    DrawGeometries(index - 1);
                }

                // --- Signal Evaluation Phase (Outside Asian Session) ---
                if (_sessHigh == double.MinValue || _sessLow == double.MaxValue) return;

                double rangePips = (_sessHigh - _sessLow) / Symbol.PipSize;
                double sessEQ = (_sessHigh + _sessLow) / 2.0;

                // Time and Range Gates
                if (barTime.Hour < TradeWindowEndHour && rangePips >= MinRangePips && rangePips <= MaxRangePips)
                {
                    double currentSpreadPips = Symbol.Spread / Symbol.PipSize;

                    if (currentSpreadPips <= MaxSpreadPips)
                    {
                        bool volPass = !RequireVolSpike || (Bars.TickVolumes[index] >= (_volSma.Result[index] * VolMultiplier));

                        // Bearish Rejection (BSL Sweep)
                        if (Bars.HighPrices[index] > _sessHigh && Bars.ClosePrices[index] < _sessHigh && Bars.OpenPrices[index] < _sessHigh && volPass)
                        {
                            BufferShortSignal[index] = Bars.HighPrices[index] + (5 * Symbol.PipSize);
                            BufferCalculatedSL[index] = Bars.HighPrices[index] + (SLBufferPips * Symbol.PipSize);
                            BufferTargetTP1[index] = sessEQ;
                            BufferTargetTP2[index] = _sessLow;

                            TriggerAlert(index, "SELL", Bars.ClosePrices[index], BufferCalculatedSL[index], sessEQ, currentSpreadPips);
                        }

                        // Bullish Rejection (SSL Sweep)
                        if (Bars.LowPrices[index] < _sessLow && Bars.ClosePrices[index] > _sessLow && Bars.OpenPrices[index] > _sessLow && volPass)
                        {
                            BufferLongSignal[index] = Bars.LowPrices[index] - (5 * Symbol.PipSize);
                            BufferCalculatedSL[index] = Bars.LowPrices[index] - (SLBufferPips * Symbol.PipSize);
                            BufferTargetTP1[index] = sessEQ;
                            BufferTargetTP2[index] = _sessHigh;

                            TriggerAlert(index, "BUY", Bars.ClosePrices[index], BufferCalculatedSL[index], sessEQ, currentSpreadPips);
                        }
                    }
                }
            }

            // HUD execution (Only update on the latest bar to save CPU cycles)
            if (IsLastBar && ShowHud)
            {
                RenderDashboard();
            }
        }

        private bool IsInAccumulation(DateTime t)
        {
            TimeSpan timeOfDay = t.TimeOfDay;
            if (_sessionStart <= _sessionEnd)
                return timeOfDay >= _sessionStart && timeOfDay < _sessionEnd;
            else
                return timeOfDay >= _sessionStart || timeOfDay < _sessionEnd; // Wrap around midnight
        }

        private void DrawGeometries(int endIdx)
        {
            string dateId = _sessStartTime.ToString("yyyyMMdd");
            DateTime endTime = Bars.OpenTimes[endIdx];

            // Draw Accumulation Box
            Chart.DrawRectangle($"Box_{dateId}", _sessStartTime, _sessHigh, endTime, _sessLow, _boxColor).IsFilled = true;

            // Project Rays forward by 12 hours
            DateTime projectionEnd = endTime.AddHours(12);
            double eq = (_sessHigh + _sessLow) / 2.0;

            var bsl = Chart.DrawTrendLine($"BSL_{dateId}", endTime, _sessHigh, projectionEnd, _sessHigh, Color.Crimson);
            bsl.LineStyle = LineStyle.Lines;

            var ssl = Chart.DrawTrendLine($"SSL_{dateId}", endTime, _sessLow, projectionEnd, _sessLow, Color.MediumSeaGreen);
            ssl.LineStyle = LineStyle.Lines;

            var eqLine = Chart.DrawTrendLine($"EQ_{dateId}", endTime, eq, projectionEnd, eq, Color.DarkGray);
            eqLine.LineStyle = LineStyle.Dots;
        }

        private void TriggerAlert(int index, string side, double close, double sl, double tp1, double spread)
        {
            // Only trigger notifications for the live bar closing
            if (IsLastBar && _lastAlertIndex != index)
            {
                _lastAlertIndex = index;
                string payload = $"[AUDNZD Matrix] {side} Sweep Rejected | Spread: {spread:F1} | Close: {close:F5} | SL: {sl:F5} | TP: {tp1:F5}";
                Print(payload);
                
                // Uncomment to enable desktop notifications
                // Notifications.PlaySound(SoundType.Doorbell);
            }
        }

        private void RenderDashboard()
        {
            // Calculate 14-Day ADR using LINQ
            double adrPips = 0;
            if (_dailyBars.Count >= 14)
            {
                adrPips = _dailyBars.Skip(_dailyBars.Count - 15).Take(14)
                                    .Average(b => b.High - b.Low) / Symbol.PipSize;
            }

            double curSpread = Symbol.Spread / Symbol.PipSize;
            string spreadStatus = curSpread > MaxSpreadPips ? "[FILTERED]" : "[NOMINAL]";

            string hud = $"== AUDNZD INSTITUTIONAL MATRIX ==\n" +
                         $"Spread    : {curSpread:F1} pips {spreadStatus}\n" +
                         $"14-Day ADR: {adrPips:F1} pips\n" +
                         $"Vol Filter: {(RequireVolSpike ? "Enabled (SMA20 Base)" : "Disabled")}\n" +
                         $"Engine    : C# Runtime Active";

            Chart.DrawText("HUD", hud, StaticPosition.TopRight, Color.LightGray);
        }
    }
}
FTtrader
Posts: 1008
Joined: Mon Aug 03, 2026 2:43 pm

Re: Scalping AUDNZD: A Cross Driven by Relative Rates and Quiet Ranges

Post by FTtrader »

Consumption Blueprint for your cBot

Since you are writing the execution logic in C#, consuming this indicator inside a cBot is highly streamlined. cTrader will automatically inject the calculated buffers based on the state machine inside the indicator.

In your cBot class:

Code: Select all

using cAlgo.API;
using cAlgo.API.Indicators;

namespace cAlgo.Robots
{
    [Robot(AccessRights = AccessRights.None)]
    public class AUDNZD_MatrixExecutor : Robot
    {
        private AUDNZD_AsianMatrix_Pro _matrixIndi;

        protected override void OnStart()
        {
            // Initialize the indicator internally
            _matrixIndi = Indicators.GetIndicator<AUDNZD_AsianMatrix_Pro>(
                "22:00", "06:00", 17, 2.0, 15.0, 60.0, true, 1.15, 2.5, "#1E152528", false
            );
        }

        protected override void OnBar()
        {
            // Check the previous closed bar (index - 1) for a fresh signal
            int checkIndex = Bars.Count - 2; 

            double shortSignal = _matrixIndi.BufferShortSignal[checkIndex];
            double longSignal  = _matrixIndi.BufferLongSignal[checkIndex];

            if (!double.IsNaN(shortSignal))
            {
                double sl  = _matrixIndi.BufferCalculatedSL[checkIndex];
                double tp1 = _matrixIndi.BufferTargetTP1[checkIndex];

                // Calculate risk and execute Market or Limit order here
                ExecuteMarketOrder(TradeType.Sell, SymbolName, 10000, "SweepShort", sl, tp1);
            }
            
            if (!double.IsNaN(longSignal))
            {
                double sl  = _matrixIndi.BufferCalculatedSL[checkIndex];
                double tp1 = _matrixIndi.BufferTargetTP1[checkIndex];

                ExecuteMarketOrder(TradeType.Buy, SymbolName, 10000, "SweepLong", sl, tp1);
            }
        }
    }
}
FTtrader
Posts: 1008
Joined: Mon Aug 03, 2026 2:43 pm

Re: Scalping AUDNZD: A Cross Driven by Relative Rates and Quiet Ranges

Post by FTtrader »

To elevate this to an institutional standard—especially since you are working in C# and can leverage object-oriented principles—we must move beyond a simple monolithic script. A professional execution engine separates signal generation from risk management, order execution, and trade lifecycle management.

Here is an enterprise-grade cBot architecture that consumes the matrix indicator. It introduces asynchronous execution, dynamic risk-based position sizing, split-target scaling (TP1 and TP2), and an automated break-even manager.
FTtrader
Posts: 1008
Joined: Mon Aug 03, 2026 2:43 pm

Re: Scalping AUDNZD: A Cross Driven by Relative Rates and Quiet Ranges

Post by FTtrader »

The Institutional cBot Architecture (C#)

This architecture splits the trade into two tranches. Tranche A secures profit at the 50% Equilibrium line (TP1). Once TP1 is hit, the engine automatically moves Tranche B's stop-loss to Break-Even and rides the rest of the move to the opposite side of the Asian range (TP2).

Save this in your cAlgo Robots folder as AUDNZD_Matrix_Executor.cs.

Code: Select all

using System;
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 AUDNZD_Matrix_Executor : Robot
    {
        #region Execution & Risk Parameters

        [Parameter("Risk Per Trade (%)", DefaultValue = 1.0, Group = "Risk Management", MinValue = 0.1, Step = 0.1)]
        public double RiskPercent { get; set; }

        [Parameter("Max Spread at Execution (Pips)", DefaultValue = 1.5, Group = "Risk Management")]
        public double MaxExecutionSpread { get; set; }

        [Parameter("Split Position (50/50)", DefaultValue = true, Group = "Trade Lifecycle")]
        public bool SplitPosition { get; set; }

        [Parameter("Move SL to BE on TP1", DefaultValue = true, Group = "Trade Lifecycle")]
        public bool BreakEvenOnTP1 { get; set; }

        #endregion

        #region Indicator Parameters

        [Parameter("Session Start", DefaultValue = "22:00", Group = "Matrix Indicator")]
        public string SessionStart { get; set; }

        [Parameter("Session End", DefaultValue = "06:00", Group = "Matrix Indicator")]
        public string SessionEnd { get; set; }

        #endregion

        // Dependencies
        private AUDNZD_AsianMatrix_Pro _matrix;
        
        // State Tracking
        private string _botLabel = "AZN_PRO";
        private DateTime _lastTradeTime = DateTime.MinValue;

        protected override void OnStart()
        {
            // Inject the Indicator
            // Ensure the parameters map exactly to the indicator's constructor signature
            _matrix = Indicators.GetIndicator<AUDNZD_AsianMatrix_Pro>(
                SessionStart, SessionEnd, 17, 2.0, 15.0, 60.0, true, 1.15, 2.5, "#1E152528", false
            );

            // Subscribe to position events for lifecycle management
            Positions.Closed += OnPositionClosed;
        }

        protected override void OnBar()
        {
            // Evaluate signals on the closed bar
            int index = Bars.Count - 2;

            double shortSignal = _matrix.BufferShortSignal[index];
            double longSignal = _matrix.BufferLongSignal[index];

            // Prevent multiple entries in the same session
            if (Bars.OpenTimes[index].Date == _lastTradeTime.Date)
                return;

            if (!double.IsNaN(shortSignal))
            {
                ExecuteSetup(TradeType.Sell, _matrix.BufferCalculatedSL[index], _matrix.BufferTargetTP1[index], _matrix.BufferTargetTP2[index]);
            }
            else if (!double.IsNaN(longSignal))
            {
                ExecuteSetup(TradeType.Buy, _matrix.BufferCalculatedSL[index], _matrix.BufferTargetTP1[index], _matrix.BufferTargetTP2[index]);
            }
        }

        #region Trade Execution Engine

        private void ExecuteSetup(TradeType tradeType, double slPrice, double tp1Price, double tp2Price)
        {
            double spreadPips = Symbol.Spread / Symbol.PipSize;
            if (spreadPips > MaxExecutionSpread)
            {
                Print($"[GATED] Signal ignored. Current spread ({spreadPips:F1} pips) exceeds maximum allowed ({MaxExecutionSpread:F1}).");
                return;
            }

            double currentPrice = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
            double slPips = Math.Abs(currentPrice - slPrice) / Symbol.PipSize;

            if (slPips <= 0) return;

            // Calculate total volume based on strictly defined account risk
            double totalVolume = CalculateVolume(slPips);
            if (totalVolume < Symbol.VolumeInUnitsMin)
            {
                Print("[ERROR] Calculated risk volume is below broker minimum. Skipping trade.");
                return;
            }

            _lastTradeTime = Server.Time.Date;

            if (SplitPosition)
            {
                // Normalize volumes to broker step sizes
                double halfVolume = Symbol.NormalizeVolumeInUnits(totalVolume / 2, RoundingMode.Down);

                // Dispatch asynchronous orders to minimize slippage latency
                ExecuteMarketOrderAsync(tradeType, SymbolName, halfVolume, _botLabel + "_T1", slPips, Math.Abs(currentPrice - tp1Price) / Symbol.PipSize);
                ExecuteMarketOrderAsync(tradeType, SymbolName, halfVolume, _botLabel + "_T2", slPips, Math.Abs(currentPrice - tp2Price) / Symbol.PipSize);
                
                Print($"[EXECUTED] Split {tradeType} dispatched. Risk: {RiskPercent}%. T1 EQ Target: {tp1Price:F5}.");
            }
            else
            {
                double volume = Symbol.NormalizeVolumeInUnits(totalVolume, RoundingMode.Down);
                ExecuteMarketOrderAsync(tradeType, SymbolName, volume, _botLabel + "_Full", slPips, Math.Abs(currentPrice - tp1Price) / Symbol.PipSize);
            }
        }

        private double CalculateVolume(double stopLossPips)
        {
            double riskAmount = Account.Equity * (RiskPercent / 100);
            double tickValue = Symbol.TickValue;
            
            // Standard risk formula: Risk$ / (SL_Pips * PipValue)
            // Note: Symbol.TickValue is for 1 TickVolume (which in cTrader is usually equivalent to Symbol.VolumeInUnitsMin)
            double pipValuePerUnit = tickValue / Symbol.TickSize * Symbol.PipSize;
            double volume = riskAmount / (stopLossPips * pipValuePerUnit);

            return volume;
        }

        #endregion

        #region Trade Lifecycle Management

        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            var closedPos = args.Position;

            // Intercept when Tranche 1 (TP1) hits its take profit successfully
            if (BreakEvenOnTP1 && closedPos.Label == _botLabel + "_T1" && closedPos.GrossProfit > 0)
            {
                // Find the sister position (Tranche 2)
                var remainingPosition = Positions.Find(_botLabel + "_T2", SymbolName);
                
                if (remainingPosition != null)
                {
                    // Move SL to Break Even + 0.5 pips to cover commissions
                    double beOffset = 0.5 * Symbol.PipSize;
                    double newSl = remainingPosition.TradeType == TradeType.Buy 
                        ? remainingPosition.EntryPrice + beOffset 
                        : remainingPosition.EntryPrice - beOffset;

                    ModifyPositionAsync(remainingPosition, newSl, remainingPosition.TakeProfit, hasResult => 
                    {
                        if (hasResult.IsSuccessful)
                            Print("[LIFECYCLE] TP1 Hit. Tranche 2 Stop Loss moved to Break-Even.");
                    });
                }
            }
        }

        #endregion
    }
}
FTtrader
Posts: 1008
Joined: Mon Aug 03, 2026 2:43 pm

Re: Scalping AUDNZD: A Cross Driven by Relative Rates and Quiet Ranges

Post by FTtrader »

The Professional Upgrades

Decoupled Asynchronous Execution: The bot uses ExecuteMarketOrderAsync and ModifyPositionAsync. This prevents the cBot thread from locking while waiting for the broker's FIX API to confirm the trade, which is critical when sweeping liquidity where milliseconds dictate slippage.

Strict Risk Normalization: The CalculateVolume() method dynamically assesses your live Account Equity and translates the required stop-loss distance into an exact lot size that represents exactly 1.0% (or your input choice) risk. It normalizes this against the broker's VolumeInUnitsMin and RoundingMode.Down to prevent API rejection errors on weird lot sizes.

Event-Driven Lifecycle Management (Break-Even): Instead of looping through all open trades on every single tick (which eats CPU cycles), it subscribes to the Positions.Closed event listener. When "Tranche 1" hits its TP at the 50% Equilibrium line, the engine automatically catches the event, locates "Tranche 2", and modifies its Stop Loss to Break-Even + 0.5 pips to secure a risk-free runner.

Millisecond Spread Gating: Even though the indicator filters historical signals by spread, spreads widen drastically in the milliseconds surrounding execution. The ExecuteSetup method queries Symbol.Spread live before constructing the payload. If the spread spikes to 2.5 pips on the tick of entry, the engine aborts the trade to protect expected value.

State Lockout: _lastTradeTime prevents the algorithm from firing multiple consecutive orders if price action chops around the liquidity line during the same session, enforcing a "one clean shot per session" institutional rule.
Post Reply