Page 4 of 4

Re: Post-central bank speech review template for scalpers

Posted: Tue Oct 06, 2026 9:46 pm
by PTScalper
Key cTrader Advantages in this Port:

LINQ for Order Tracking: Instead of running heavy for loops across the entire terminal account history on every tick (like in MT4), we use Positions.FindAll(Tp1Label).Any(). This makes checking if breakeven is secured extremely lightweight.

Perfect Volume Normalization: cTrader natively scales your risk perfectly into units using Symbol.NormalizeVolumeInUnits(). It completely removes the complex MT4 ticket value/size conversions that often break on indices or crypto.

Clean Event Delegation: Because cTrader provides OnBar(), we can cleanly trigger our entry logic only when the candle closes, completely eliminating the if(time != lastTime) workaround required in MQL.

Non-Blocking Box Drawing: The visual box handles cleanly within Chart.DrawRectangle, overlaying directly on the time series without creating heavy chart objects.

Re: Post-central bank speech review template for scalpers

Posted: Tue Oct 06, 2026 9:47 pm
by PTScalper
The previous cTrader script included the core Ultra mechanics (split volume, auto-breakeven, trailing runner, and HUD), but to make it a truly enterprise-grade execution tool for news events, we need to leverage cTrader's native asynchronous architecture.

During central bank speeches, liquidity drops and broker servers get slammed. Firing synchronous orders blocks the main thread; if the broker takes 800ms to confirm the TP1 order, your TP2 order suffers 800ms of slippage.

This upgraded Ultra cBot uses ExecuteMarketOrderAsync to fire both the TP1 and TP2 orders concurrently. It also introduces a Maximum Spread Filter—a critical protection mechanism that rejects the entry trigger if the post-news spread hasn't compressed back to a tradable level.

Re: Post-central bank speech review template for scalpers

Posted: Tue Oct 06, 2026 9:48 pm
by PTScalper
The Asynchronous Ultra-Pro cBot (C#)

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 UltraProNewsScalperAsync : Robot
    {
        // =========================================================================
        // 1. SESSION & REACTION
        // =========================================================================
        [Parameter("News Start Time (HH:mm)", Group = "1. Session", DefaultValue = "14:00")]
        public string NewsStartTimeStr { get; set; }

        [Parameter("News End Time (HH:mm)", Group = "1. Session", DefaultValue = "14:15")]
        public string NewsEndTimeStr { get; set; }

        // =========================================================================
        // 2. TRIGGER & FILTERS
        // =========================================================================
        [Parameter("Pullback EMA", Group = "2. Triggers & Filters", DefaultValue = 9)]
        public int EmaLength { get; set; }

        [Parameter("Max Bars to Wait", Group = "2. Triggers & Filters", DefaultValue = 30)]
        public int MaxWaitBars { get; set; }

        [Parameter("Max Allowed SL (Pips)", Group = "2. Triggers & Filters", DefaultValue = 30.0)]
        public double MaxSlPips { get; set; }

        [Parameter("Max Spread (Pips)", Group = "2. Triggers & Filters", DefaultValue = 3.0)]
        public double MaxSpreadPips { get; set; }

        [Parameter("SL Wick Buffer (Pips)", Group = "2. Triggers & Filters", DefaultValue = 1.5)]
        public double SlBufferPips { get; set; }

        // =========================================================================
        // 3. RISK & MANAGEMENT
        // =========================================================================
        [Parameter("Total Risk Per Trade (%)", Group = "3. Risk & Management", DefaultValue = 1.0)]
        public double RiskPercent { get; set; }

        [Parameter("Target 1 (R:R)", Group = "3. Risk & Management", DefaultValue = 1.0)]
        public double Tp1Rr { get; set; }

        [Parameter("Target 1 Exit Size (%)", Group = "3. Risk & Management", DefaultValue = 50, MinValue = 1, MaxValue = 99)]
        public int Tp1QtyPct { get; set; }

        [Parameter("Target 2 / Runner (R:R)", Group = "3. Risk & Management", DefaultValue = 2.5)]
        public double Tp2Rr { get; set; }

        [Parameter("Auto-Breakeven Runner?", Group = "3. Risk & Management", DefaultValue = true)]
        public bool AutoBreakeven { get; set; }

        [Parameter("Trail Runner?", Group = "3. Risk & Management", DefaultValue = true)]
        public bool UseTrailingStop { get; set; }

        [Parameter("Trailing Stop (Pips)", Group = "3. Risk & Management", DefaultValue = 15.0)]
        public double TrailingStopPips { get; set; }

        [Parameter("Trailing Step (Pips)", Group = "3. Risk & Management", DefaultValue = 2.0)]
        public double TrailingStepPips { get; set; }

        // State Variables
        private TimeSpan _startTime, _endTime;
        private ExponentialMovingAverage _ema;
        
        private double _initOpen, _initHigh, _initLow, _pivotExtreme = double.NaN;
        private int _trendDir = 0, _barsSinceNews = 0;
        private bool _pullbackValid = false, _wasInNews = false;
        private DateTime _sessionStartDateTime;
        
        private const string Tp1Label = "NewsScalp_TP1";
        private const string Tp2Label = "NewsScalp_TP2";
        private const string BoxName = "NewsBox";

        protected override void OnStart()
        {
            if (!TimeSpan.TryParse(NewsStartTimeStr, out _startTime) || !TimeSpan.TryParse(NewsEndTimeStr, out _endTime))
            {
                Print("Invalid Time Format. Please use HH:mm");
                Stop();
            }

            _ema = Indicators.ExponentialMovingAverage(Bars.ClosePrices, EmaLength);
            _wasInNews = IsInNewsWindow(Server.Time.TimeOfDay);
        }

        private bool IsInNewsWindow(TimeSpan currentTime)
        {
            if (_startTime < _endTime)
                return currentTime >= _startTime && currentTime < _endTime;
            return currentTime >= _startTime || currentTime < _endTime; // Cross-midnight support
        }

        protected override void OnTick()
        {
            bool inNews = IsInNewsWindow(Server.Time.TimeOfDay);

            ManageBreakevenAndTrailing();
            UpdateHUD(inNews);

            // 1. Session Start (Box Setup)
            if (inNews && !_wasInNews)
            {
                _initOpen = Bars.OpenPrices.LastValue;
                _initHigh = Bars.HighPrices.LastValue;
                _initLow = Bars.LowPrices.LastValue;
                _trendDir = 0;
                _pullbackValid = false;
                _barsSinceNews = 0;
                _sessionStartDateTime = Bars.OpenTimes.LastValue;

                Chart.DrawRectangle(BoxName, _sessionStartDateTime, _initHigh, Bars.OpenTimes.LastValue, _initLow, Color.FromArgb(50, Color.DarkGray));
            }

            // 2. Session Update (Expand Box vertically/horizontally)
            if (inNews)
            {
                _initHigh = Math.Max(_initHigh, Bars.HighPrices.LastValue);
                _initLow = Math.Min(_initLow, Bars.LowPrices.LastValue);
                Chart.DrawRectangle(BoxName, _sessionStartDateTime, _initHigh, Bars.OpenTimes.LastValue, _initLow, Color.FromArgb(50, Color.DarkGray));
            }

            // 3. Session End (Determine Trend & Color Box)
            if (!inNews && _wasInNews)
            {
                double initClose = Bars.ClosePrices.Last(1); 
                
                if (initClose > _initOpen)
                {
                    _trendDir = 1;
                    Chart.DrawRectangle(BoxName, _sessionStartDateTime, _initHigh, Bars.OpenTimes.LastValue, _initLow, Color.FromArgb(50, Color.Teal));
                }
                else if (initClose < _initOpen)
                {
                    _trendDir = -1;
                    Chart.DrawRectangle(BoxName, _sessionStartDateTime, _initHigh, Bars.OpenTimes.LastValue, _initLow, Color.FromArgb(50, Color.Maroon));
                }
                else _trendDir = 0;

                _pivotExtreme = _trendDir == 1 ? Bars.LowPrices.Last(1) : Bars.HighPrices.Last(1);
            }

            // Continuous Stop-Loss Pivot Tracking
            if (!inNews && _trendDir != 0 && !HasActivePosition())
            {
                if (_trendDir == 1 && Bars.LowPrices.LastValue < _pivotExtreme) _pivotExtreme = Bars.LowPrices.LastValue;
                if (_trendDir == -1 && Bars.HighPrices.LastValue > _pivotExtreme) _pivotExtreme = Bars.HighPrices.LastValue;
            }

            _wasInNews = inNews;
        }

        protected override void OnBar()
        {
            if (IsInNewsWindow(Server.Time.TimeOfDay) || _trendDir == 0 || HasActivePosition()) 
                return;

            _barsSinceNews++;
            
            if (_barsSinceNews > MaxWaitBars)
            {
                _trendDir = 0;
                _pullbackValid = false;
                return;
            }

            double close1 = Bars.ClosePrices.Last(1);
            double ema1 = _ema.Result.Last(1);
            double slBuffer = Symbol.PipSize * SlBufferPips;

            // LONG SETUP
            if (_trendDir == 1)
            {
                if (close1 < ema1) _pullbackValid = true;

                if (_pullbackValid && close1 > ema1)
                {
                    double sl = Math.Round(_pivotExtreme - slBuffer, Symbol.Digits);
                    ExecuteSplitOrderAsync(TradeType.Buy, sl);
                }
            }

            // SHORT SETUP
            if (_trendDir == -1)
            {
                if (close1 > ema1) _pullbackValid = true;

                if (_pullbackValid && close1 < ema1)
                {
                    double sl = Math.Round(_pivotExtreme + slBuffer, Symbol.Digits);
                    ExecuteSplitOrderAsync(TradeType.Sell, sl);
                }
            }
        }

        private void ExecuteSplitOrderAsync(TradeType tradeType, double sl)
        {
            // Spread Filter
            if (Symbol.Spread / Symbol.PipSize > MaxSpreadPips)
            {
                Print("Entry rejected: Spread too high.");
                return;
            }

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

            if (slPips <= 0 || slPips > MaxSlPips) return; 

            // Exact Unit Normalization
            double riskAmount = Account.Balance * (RiskPercent / 100.0);
            double exactVolume = riskAmount / (slPips * Symbol.PipValue);
            double totalVolume = Symbol.NormalizeVolumeInUnits(exactVolume, RoundingMode.Down);

            if (totalVolume < Symbol.VolumeInUnitsMin) return;

            double vol1 = Symbol.NormalizeVolumeInUnits(totalVolume * (Tp1QtyPct / 100.0), RoundingMode.Down);
            double vol2 = Symbol.NormalizeVolumeInUnits(totalVolume - vol1, RoundingMode.Down);

            if (vol1 > 0 && vol2 > 0)
            {
                double tp1Offset = slPips * Tp1Rr;
                double tp2Offset = slPips * Tp2Rr;

                // Concurrent Non-Blocking Execution
                ExecuteMarketOrderAsync(tradeType, SymbolName, vol1, Tp1Label, slPips, tp1Offset, (result1) => 
                {
                    if(!result1.IsSuccessful) Print($"TP1 Order Failed: {result1.Error}");
                });
                
                ExecuteMarketOrderAsync(tradeType, SymbolName, vol2, Tp2Label, slPips, tp2Offset, (result2) => 
                {
                    if(!result2.IsSuccessful) Print($"TP2 Order Failed: {result2.Error}");
                });

                _trendDir = 0; 
                _pullbackValid = false;
            }
        }

        private void ManageBreakevenAndTrailing()
        {
            var tp1Exists = Positions.FindAll(Tp1Label, SymbolName).Any();
            var runners = Positions.FindAll(Tp2Label, SymbolName);

            foreach (var runner in runners)
            {
                // 1. Auto-Breakeven
                if (AutoBreakeven && !tp1Exists)
                {
                    if (runner.TradeType == TradeType.Buy && runner.StopLoss < runner.EntryPrice)
                        ModifyPositionAsync(runner, runner.EntryPrice, runner.TakeProfit);
                    else if (runner.TradeType == TradeType.Sell && runner.StopLoss > runner.EntryPrice)
                        ModifyPositionAsync(runner, runner.EntryPrice, runner.TakeProfit);
                }

                // 2. Trailing Stop
                if (UseTrailingStop && !tp1Exists)
                {
                    double trailPips = TrailingStopPips * Symbol.PipSize;
                    double stepPips = TrailingStepPips * Symbol.PipSize;

                    if (runner.TradeType == TradeType.Buy && Symbol.Bid - runner.EntryPrice > trailPips)
                    {
                        double newSl = Math.Round(Symbol.Bid - trailPips, Symbol.Digits);
                        if (runner.StopLoss == null || runner.StopLoss < newSl - stepPips)
                            ModifyPositionAsync(runner, newSl, runner.TakeProfit);
                    }
                    else if (runner.TradeType == TradeType.Sell && runner.EntryPrice - Symbol.Ask > trailPips)
                    {
                        double newSl = Math.Round(Symbol.Ask + trailPips, Symbol.Digits);
                        if (runner.StopLoss == null || runner.StopLoss > newSl + stepPips)
                            ModifyPositionAsync(runner, newSl, runner.TakeProfit);
                    }
                }
            }
        }

        private bool HasActivePosition()
        {
            return Positions.FindAll(Tp1Label, SymbolName).Any() || Positions.FindAll(Tp2Label, SymbolName).Any();
        }

        private void UpdateHUD(bool inNews)
        {
            string bias = inNews ? "Awaiting News..." : (_trendDir == 1 ? "Bullish Pullback" : (_trendDir == -1 ? "Bearish Pullback" : "Flat / No Setup"));
            string pos = HasActivePosition() ? "ACTIVE" : "WAITING";
            double spread = Math.Round(Symbol.Spread / Symbol.PipSize, 1);

            string hudText = $"=== ULTRA SCALPER HUD ===\nStatus: {bias}\nPosition: {pos}\nSpread: {spread} pips";
            Chart.DrawStaticText("HUD", hudText, VerticalAlignment.Top, HorizontalAlignment.Right, Color.White);
        }
    }
}

Re: Post-central bank speech review template for scalpers

Posted: Tue Oct 06, 2026 9:48 pm
by PTScalper
By pushing position modifications (Breakeven and Trailing) to ModifyPositionAsync, the OnTick handler continues to process incoming market data uninterrupted, preventing the bot from "freezing" while waiting for the broker to confirm the trailing stop adjustment.