Advertisement IC Markets

Order reject and requote rates: what to log before blaming your edge

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
Post Reply
LondonScalper
Posts: 701
Joined: Sat Sep 05, 2026 7:54 am

Order reject and requote rates: what to log before blaming your edge

Post by LondonScalper »

Before you blame the edge, count rejects and requotes properly.

A strategy can be fine and still look broken if you are silently eating rejects, requotes, or "filled somewhere else." For a while I folded all of that into "bad luck."

Log fields that helped
1. Reject vs requote vs no-response (separate)
2. Session minute and symbol
3. Order type and size
4. Whether I spam-clicked (yes -- tag it)

Weekly: sort by reject rate. If one window or one pair dominates, fix process first -- limit orders, smaller size, stand-aside -- before rewriting entries.

Blaming "the market" or "the broker" in general is how you avoid a specific rule. Numbers make the rule obvious.

What is your threshold where a reject cluster becomes a hard filter rather than a shrug?

I also compare reject rates to my own spam behaviour. If rejects spike only when I am chasing, that is not a venue story. If they spike on calm limit orders in a specific minute of the open, that minute gets a filter. The journal has to be able to tell those apart or you will "fix" the wrong thing for months.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

LondonScalper wrote: Mon Sep 14, 2026 7:38 pm Before you blame the edge, count rejects and requotes properly.

A strategy can be fine and still look broken if you are silently eating rejects, requotes, or "filled somewhere else." For a while I folded all of that into "bad luck."

Log fields that helped
1. Reject vs requote vs no-response (separate)
2. Session minute and symbol
3. Order type and size
4. Whether I spam-clicked (yes -- tag it)

Weekly: sort by reject rate. If one window or one pair dominates, fix process first -- limit orders, smaller size, stand-aside -- before rewriting entries.

Blaming "the market" or "the broker" in general is how you avoid a specific rule. Numbers make the rule obvious.

What is your threshold where a reject cluster becomes a hard filter rather than a shrug?

I also compare reject rates to my own spam behaviour. If rejects spike only when I am chasing, that is not a venue story. If they spike on calm limit orders in a specific minute of the open, that minute gets a filter. The journal has to be able to tell those apart or you will "fix" the wrong thing for months.
Hi LondonScalper,

A reject cluster shifts from a "shrug" to a hard filter when it violates the baseline statistical behavior of a venue for that specific time of day, divorced from trader-induced errors.

Here is exactly where that line is drawn:

The 3-in-5 Rule (Venue Health): Three execution failures (rejects, timeouts, or requotes out of tolerance) within a rolling 5-minute window. This indicates a structural liquidity gap or API routing issue. The hard filter response is a 15-minute suspension for that specific symbol.

Latency Degradation (Pre-Reject Warning): If the round-trip ping to the broker jumps from a baseline of ~50ms to >500ms on two consecutive orders, a reject cluster is imminent. You filter the next 10 minutes before the rejects actually hit.

The Tilt Lockout (Trader Health): If the reject cluster aligns with a "spam click" flag (e.g., >2 order requests within 1500ms), the venue isn't broken—the trader is. The filter here is a hard 60-second software lockout to snap the operator out of a chase.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

Below is a cTrader cBot designed to act as an execution overlay. It creates a custom UI panel on your chart to route your manual clicks. By routing through the bot, it can measure latency, tag your spam-clicks, log the exact reject reasons to a CSV, and enforce the hard filter thresholds automatically.

cTrader Execution & Journaling cBot (C#)

Note: Because this writes to a local CSV file, you must set the bot's Access Rights to FileSystem in cTrader (Manage > Access Rights).

Code: Select all

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
using cAlgo.API.Requests;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.FileSystem)]
    public class ExecutionJournalBot : Robot
    {
        [Parameter("Max Rejects (Threshold)", DefaultValue = 3, MinValue = 1)]
        public int MaxRejects { get; set; }

        [Parameter("Cluster Window (Minutes)", DefaultValue = 5)]
        public int ClusterWindowMin { get; set; }

        [Parameter("Hard Filter Pause (Minutes)", DefaultValue = 15)]
        public int FilterPauseMin { get; set; }

        [Parameter("Spam Click Threshold (ms)", DefaultValue = 1500)]
        public int SpamThresholdMs { get; set; }

        [Parameter("Default Volume", DefaultValue = 100000)]
        public double DefaultVolume { get; set; }

        private string _logFilePath;
        private List<DateTime> _rejectTimestamps = new List<DateTime>();
        private DateTime _lastClickTime = DateTime.MinValue;
        private DateTime _hardFilterExpiration = DateTime.MinValue;

        protected override void OnStart()
        {
            // Initialize CSV Log
            string documentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            _logFilePath = Path.Combine(documentsFolder, "cTrader_Execution_Journal.csv");

            if (!File.Exists(_logFilePath))
            {
                File.WriteAllText(_logFilePath, "Timestamp,MinuteOfSession,Symbol,OrderType,Size,SpamClicked,Status,LatencyMS,ErrorReason\n");
            }

            DrawTradingPanel();
        }

        private void DrawTradingPanel()
        {
            var stackPanel = new StackPanel
            {
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Top,
                BackgroundColor = Color.FromArgb(200, 30, 30, 30),
                Margin = new Thickness(10)
            };

            var btnBuy = new Button { Text = "BUY MKT", Margin = new Thickness(5), ForegroundColor = Color.LightGreen };
            btnBuy.Click += args => ExecuteTrackedOrder(TradeType.Buy);

            var btnSell = new Button { Text = "SELL MKT", Margin = new Thickness(5), ForegroundColor = Color.IndianRed };
            btnSell.Click += args => ExecuteTrackedOrder(TradeType.Sell);

            stackPanel.AddChild(btnBuy);
            stackPanel.AddChild(btnSell);
            Chart.AddControl(stackPanel);
        }

        private void ExecuteTrackedOrder(TradeType tradeType)
        {
            DateTime currentTime = Server.Time;

            // 1. Check Hard Filter
            if (currentTime < _hardFilterExpiration)
            {
                Print($"Hard Filter Active. Execution blocked until {_hardFilterExpiration:HH:mm:ss}");
                return;
            }

            // 2. Spam Detection
            bool isSpam = false;
            if ((currentTime - _lastClickTime).TotalMilliseconds < SpamThresholdMs)
            {
                isSpam = true;
                Print("Spam click detected. Tagging order.");
            }
            _lastClickTime = currentTime;

            // 3. Execution & Latency Tracking
            var stopwatch = new System.Diagnostics.Stopwatch();
            stopwatch.Start();
            
            TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, DefaultVolume, "JournalBot");
            
            stopwatch.Stop();
            long latency = stopwatch.ElapsedMilliseconds;

            // 4. Result Processing
            string status = result.IsSuccessful ? "Filled" : "Rejected";
            string errorReason = result.IsSuccessful ? "None" : result.Error.ToString();

            if (!result.IsSuccessful)
            {
                ProcessReject(currentTime);
            }

            // 5. Write to Journal
            WriteToJournal(currentTime, tradeType, isSpam, status, latency, errorReason);
        }

        private void ProcessReject(DateTime currentTime)
        {
            _rejectTimestamps.Add(currentTime);

            // Clean up old rejects outside the rolling window
            _rejectTimestamps.RemoveAll(t => (currentTime - t).TotalMinutes > ClusterWindowMin);

            // Trigger Hard Filter if threshold met
            if (_rejectTimestamps.Count >= MaxRejects)
            {
                _hardFilterExpiration = currentTime.AddMinutes(FilterPauseMin);
                _rejectTimestamps.Clear(); // Reset cluster counter
                Print($"REJECT CLUSTER DETECTED. Hard filter engaged for {FilterPauseMin} minutes.");
            }
        }

        private void WriteToJournal(DateTime time, TradeType type, bool isSpam, string status, long latency, string error)
        {
            // Session Minute (Total minutes since midnight to spot specific open/close behaviors)
            int sessionMinute = (time.Hour * 60) + time.Minute;
            
            string logLine = $"{time:yyyy-MM-dd HH:mm:ss.fff},{sessionMinute},{SymbolName},{type},{DefaultVolume},{isSpam},{status},{latency},{error}\n";
            
            try
            {
                File.AppendAllText(_logFilePath, logLine);
            }
            catch (Exception ex)
            {
                Print("Failed to write to log: " + ex.Message);
            }
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

How to use this for your weekly review

By outputting to cTrader_Execution_Journal.csv in your Documents folder, you generate a dataset perfectly shaped for the exact analysis you described.

When you open the CSV at the end of the week, you can immediately pivot by:

MinuteOfSession vs Status: Look for localized clusters. If 13:30 (NY Open) has a 40% reject rate across 20 attempts, but 13:45 has 0%, you write a rule to stand aside until 13:35.

SpamClicked vs Status: If 90% of your rejected orders have SpamClicked = TRUE, your broker's throttle limits are rejecting you, not the market.

LatencyMS vs Status: If your filled orders average 40ms, but your requoted/rejected orders average 800ms, it gives you a technical metric to monitor via API to detect toxic venue conditions before submitting the order.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

To elevate this to an institutional standard, we must move away from a simple script and build an Execution Telemetry & Circuit Breaker Engine.

Professionals don’t just count rejects; they monitor state machines, categorize error codes, ensure UI threads are never blocked by network calls, and display real-time telemetry so the trader knows the venue's exact health before clicking.

Here are the architectural upgrades in this "Pro" version:

Asynchronous Execution: Replaces blocking calls with ExecuteMarketOrderAsync. The UI and price feed will never freeze while waiting for the broker's server to respond.

Asynchronous I/O (Log Queue): File writing is offloaded to a queue flushed on a timer. Writing directly to a disk on a UI thread click can cause micro-stutters, skewing your latency measurements.

Error Categorization: Differentiates between Trader Faults (Margin/Risk) and Venue Faults (Off-Quotes/Timeouts). Only venue faults trigger the Circuit Breaker.

Real-Time HUD: A Heads-Up Display on the chart showing current latency, circuit breaker state, and rolling reject counts.

Debounce Logic: Completely ignores spam clicks below a certain threshold rather than just tagging them, protecting the API rate limit.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

Pro cTrader Execution Telemetry Engine (C#)

Code: Select all

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.FileSystem)]
    public class ExecutionTelemetryEngine : Robot
    {
        [Header("Circuit Breaker Dynamics")]
        [Parameter("Max Venue Rejects", DefaultValue = 3, MinValue = 1)]
        public int MaxRejects { get; set; }

        [Parameter("Rolling Window (Minutes)", DefaultValue = 5)]
        public int WindowMinutes { get; set; }

        [Parameter("Breaker Cooldown (Minutes)", DefaultValue = 15)]
        public int CooldownMinutes { get; set; }

        [Header("Order Flow Limits")]
        [Parameter("Order Size (Lots)", DefaultValue = 1.0)]
        public double OrderLots { get; set; }

        [Parameter("API Rate Limit / Debounce (ms)", DefaultValue = 500)]
        public int DebounceMs { get; set; }

        // State Management
        private enum SystemState { Operational, Warning, Halted }
        private SystemState _currentState = SystemState.Operational;
        private DateTime _breakerExpiration = DateTime.MinValue;
        private DateTime _lastClickTime = DateTime.MinValue;
        private readonly List<DateTime> _venueRejectTimestamps = new List<DateTime>();
        
        // I/O & Telemetry
        private string _logFilePath;
        private readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>();
        private long _lastLatencyMs = 0;

        // UI Elements
        private TextBlock _uiStateText;
        private TextBlock _uiLatencyText;
        private TextBlock _uiRejectText;

        protected override void OnStart()
        {
            InitializeTelemetryStorage();
            DrawHUD();
            Timer.Start(1); // 1-second timer to flush logs and update HUD state
        }

        private void InitializeTelemetryStorage()
        {
            string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "QuantTelemetry");
            Directory.CreateDirectory(dir);
            _logFilePath = Path.Combine(dir, $"{SymbolName}_ExecutionLog_{Server.TimeInUtc:yyyyMMdd}.csv");

            if (!File.Exists(_logFilePath))
            {
                _logQueue.Enqueue("TimestampUTC,SessionMinute,Symbol,Type,Lots,LatencyMS,IsSpam,Status,ErrorCode,FaultOwner");
            }
        }

        private void DrawHUD()
        {
            var grid = new Grid(4, 2)
            {
                BackgroundColor = Color.FromArgb(220, 20, 20, 20),
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Top,
                Margin = new Thickness(10)
            };

            // Styles
            var headerStyle = new Style(DefaultStyles.TextBlockStyle);
            headerStyle.Set(ControlProperty.ForegroundColor, Color.Gray);
            headerStyle.Set(ControlProperty.Margin, new Thickness(5));

            var valueStyle = new Style(DefaultStyles.TextBlockStyle);
            valueStyle.Set(ControlProperty.ForegroundColor, Color.White);
            valueStyle.Set(ControlProperty.Margin, new Thickness(5, 5, 15, 5));
            valueStyle.Set(ControlProperty.FontWeight, FontWeight.Bold);

            // HUD Labels
            grid.AddChild(new TextBlock { Text = "Engine State:", Style = headerStyle }, 0, 0);
            grid.AddChild(_uiStateText = new TextBlock { Text = "OPERATIONAL", ForegroundColor = Color.LimeGreen, Style = valueStyle }, 0, 1);

            grid.AddChild(new TextBlock { Text = "Last Latency:", Style = headerStyle }, 1, 0);
            grid.AddChild(_uiLatencyText = new TextBlock { Text = "0 ms", Style = valueStyle }, 1, 1);

            grid.AddChild(new TextBlock { Text = "Rolling Rejects:", Style = headerStyle }, 2, 0);
            grid.AddChild(_uiRejectText = new TextBlock { Text = "0", Style = valueStyle }, 2, 1);

            // Execution Buttons
            var btnBuy = new Button { Text = "BUY MKT", BackgroundColor = Color.SeaGreen, Margin = new Thickness(5) };
            btnBuy.Click += args => RequestExecution(TradeType.Buy);
            grid.AddChild(btnBuy, 3, 0);

            var btnSell = new Button { Text = "SELL MKT", BackgroundColor = Color.Firebrick, Margin = new Thickness(5) };
            btnSell.Click += args => RequestExecution(TradeType.Sell);
            grid.AddChild(btnSell, 3, 1);

            Chart.AddControl(grid);
        }

        private void RequestExecution(TradeType tradeType)
        {
            DateTime now = Server.TimeInUtc;

            // 1. Debounce (Protect API from hardware faults / extreme trader tilt)
            if ((now - _lastClickTime).TotalMilliseconds < DebounceMs)
            {
                QueueTelemetry(now, tradeType, 0, true, "Blocked", "RateLimit", "Trader");
                return;
            }
            _lastClickTime = now;

            // 2. Circuit Breaker Check
            if (now < _breakerExpiration)
            {
                QueueTelemetry(now, tradeType, 0, false, "Blocked", "CircuitBreakerActive", "System");
                return;
            }

            // 3. Dispatch Async Order
            double volume = Symbol.QuantityToVolumeInUnits(OrderLots);
            Stopwatch sw = Stopwatch.StartNew();

            ExecuteMarketOrderAsync(tradeType, SymbolName, volume, "ProJournal", result => 
            {
                sw.Stop();
                ProcessExecutionResult(result, now, sw.ElapsedMilliseconds, tradeType, volume);
            });
        }

        private void ProcessExecutionResult(TradeResult result, DateTime requestTime, long latency, TradeType type, double volume)
        {
            _lastLatencyMs = latency;
            string status = result.IsSuccessful ? "Filled" : "Rejected";
            string errorCode = result.IsSuccessful ? "None" : result.Error.ToString();
            string faultOwner = "None";

            if (!result.IsSuccessful)
            {
                // Categorize Fault
                if (IsTraderFault(result.Error))
                {
                    faultOwner = "Trader";
                }
                else
                {
                    faultOwner = "Venue";
                    UpdateBreakerLogic(requestTime);
                }
            }

            QueueTelemetry(requestTime, type, latency, false, status, errorCode, faultOwner);
            UpdateUI();
        }

        private bool IsTraderFault(ErrorCode? error)
        {
            // If margin is exhausted or position limit hit, venue health is fine. 
            // Do not trip the circuit breaker for user errors.
            return error == ErrorCode.NoMoney || error == ErrorCode.EntityNotFound || error == ErrorCode.InvalidRequest;
        }

        private void UpdateBreakerLogic(DateTime rejectTime)
        {
            _venueRejectTimestamps.Add(rejectTime);
            _venueRejectTimestamps.RemoveAll(t => (rejectTime - t).TotalMinutes > WindowMinutes);

            if (_venueRejectTimestamps.Count >= MaxRejects)
            {
                _breakerExpiration = Server.TimeInUtc.AddMinutes(CooldownMinutes);
                _currentState = SystemState.Halted;
                _venueRejectTimestamps.Clear();
                Print($"CIRCUIT BREAKER TRIPPED: Venue halted for {CooldownMinutes} minutes.");
            }
            else if (_venueRejectTimestamps.Count >= MaxRejects - 1)
            {
                _currentState = SystemState.Warning;
            }
        }

        private void QueueTelemetry(DateTime time, TradeType type, long latency, bool isSpam, string status, string error, string faultOwner)
        {
            int sessionMinute = (time.Hour * 60) + time.Minute;
            string logLine = $"{time:yyyy-MM-dd HH:mm:ss.fff},{sessionMinute},{SymbolName},{type},{OrderLots},{latency},{isSpam},{status},{error},{faultOwner}";
            _logQueue.Enqueue(logLine);
        }

        protected override void OnTimer()
        {
            // Background Log Flush
            if (!_logQueue.IsEmpty)
            {
                try
                {
                    using (StreamWriter sw = File.AppendText(_logFilePath))
                    {
                        while (_logQueue.TryDequeue(out string line))
                        {
                            sw.WriteLine(line);
                        }
                    }
                }
                catch (Exception ex)
                {
                    Print($"Telemetry I/O Error: {ex.Message}");
                }
            }

            // State Recovery
            if (_currentState == SystemState.Halted && Server.TimeInUtc >= _breakerExpiration)
            {
                _currentState = SystemState.Operational;
            }

            UpdateUI();
        }

        private void UpdateUI()
        {
            BeginInvokeOnMainThread(() =>
            {
                _uiLatencyText.Text = $"{_lastLatencyMs} ms";
                _uiRejectText.Text = _venueRejectTimestamps.Count.ToString();

                switch (_currentState)
                {
                    case SystemState.Operational:
                        _uiStateText.Text = "OPERATIONAL";
                        _uiStateText.ForegroundColor = Color.LimeGreen;
                        break;
                    case SystemState.Warning:
                        _uiStateText.Text = "WARNING (Elevated Rejects)";
                        _uiStateText.ForegroundColor = Color.Orange;
                        break;
                    case SystemState.Halted:
                        _uiStateText.Text = $"HALTED until {_breakerExpiration:HH:mm}";
                        _uiStateText.ForegroundColor = Color.Red;
                        break;
                }
            });
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

How to Analyze the Professional Output

Because we added the FaultOwner column, your data analytics become much cleaner.

When you sort your CSV in Excel or Python, filter out FaultOwner = Trader. If you get a "NoMoney" reject because you forgot to adjust your lot size after a drawdown, that is a trader operations error, not an edge failure.

By isolating FaultOwner = Venue, you identify structural toxic conditions:

The Latency Spike Before the Reject: Sort by LatencyMS. You will likely notice a pattern where orders that take >400ms eventually lead to a cluster of VenueFault rejects moments later. This tells you to tighten your timeout limits or adjust your strategy's sensitivity to high-latency environments.

API Rate Limiting Mapping: If you see RateLimit flags clustered heavily around rapid news events (e.g., FOMC, NFP), it means your finger is faster than the broker's FIX gateway configuration allows. The debounce logic prevents you from being temporarily banned by the venue's anti-DDoS protections.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

To take this to an institutional, quantitative desk level, we must transition from reactive tracking to proactive risk mitigation.

At a professional level, rejects are only one symptom of a toxic execution environment. The silent killer of edge is Slippage, and the leading indicator of a reject is Latency Degradation.

In this ultimate "Pro" version, we are transforming the script into a Multi-Vector Risk Engine.

The Institutional Upgrades:

The Slippage Matrix: Captures the exact Bid/Ask at the millisecond of your click and compares it to the broker's fill price.

Multi-Vector Circuit Breaker: The engine no longer just halts on rejects. It halts if Slippage exceeds your tolerance, or if Latency spikes above a threshold (predicting a reject before it happens).

Memory-Optimized Telemetry: Uses StringBuilder and batched file writes to ensure the logger itself creates zero micro-stutter on the trading thread.

Separation of Risk State: The UI, Execution, and Risk Management are cleanly separated, allowing the Risk Engine to independently lock out the Execution Engine.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

Pro Multi-Vector Risk Engine (C#)

Code: Select all

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.FileSystem)]
    public class InstitutionalRiskEngine : Robot
    {
        [Header("Vector 1: Reject Limits")]
        [Parameter("Max Venue Rejects", DefaultValue = 3, MinValue = 1)]
        public int MaxRejects { get; set; }

        [Header("Vector 2: Latency Limits")]
        [Parameter("Max Latency (ms)", DefaultValue = 400)]
        public long MaxLatencyMs { get; set; }

        [Header("Vector 3: Slippage Limits")]
        [Parameter("Max Avg Slippage (Pips)", DefaultValue = 2.0)]
        public double MaxSlippagePips { get; set; }

        [Header("Circuit Breaker Settings")]
        [Parameter("Rolling Window (Minutes)", DefaultValue = 5)]
        public int WindowMinutes { get; set; }

        [Parameter("Breaker Cooldown (Minutes)", DefaultValue = 15)]
        public int CooldownMinutes { get; set; }

        [Header("Order Flow")]
        [Parameter("Order Size (Lots)", DefaultValue = 1.0)]
        public double OrderLots { get; set; }

        [Parameter("API Debounce (ms)", DefaultValue = 500)]
        public int DebounceMs { get; set; }

        // Architecture: State & Risk
        private enum State { Operational, Warning, Halted }
        private State _engineState = State.Operational;
        private string _haltReason = "N/A";
        private DateTime _breakerExpiration = DateTime.MinValue;
        private DateTime _lastClickTime = DateTime.MinValue;
        
        // Architecture: Metrics Buffers (Rolling Windows)
        private readonly List<DateTime> _rejectStamps = new List<DateTime>();
        private readonly List<Tuple<DateTime, long>> _latencyBuffer = new List<Tuple<DateTime, long>>();
        private readonly List<Tuple<DateTime, double>> _slippageBuffer = new List<Tuple<DateTime, double>>();

        // Architecture: I/O
        private string _logFilePath;
        private readonly ConcurrentQueue<string> _telemetryQueue = new ConcurrentQueue<string>();
        
        // Architecture: UI
        private TextBlock _uiStateText, _uiReasonText, _uiLatencyText, _uiSlippageText, _uiRejectText;

        protected override void OnStart()
        {
            InitializeDataLake();
            ConstructDashboard();
            Timer.Start(1); // Flush telemetry & evaluate risk decay every second
        }

        private void InitializeDataLake()
        {
            string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "QuantRiskDesk");
            Directory.CreateDirectory(dir);
            _logFilePath = Path.Combine(dir, $"{SymbolName}_ExecData_{Server.TimeInUtc:yyyyMMdd}.csv");

            if (!File.Exists(_logFilePath))
            {
                _telemetryQueue.Enqueue("TimestampUTC,Symbol,Side,RequestedPrice,FillPrice,SlippagePips,LatencyMS,Status,RiskVector,FaultOwner");
            }
        }

        private void ConstructDashboard()
        {
            var grid = new Grid(6, 2)
            {
                BackgroundColor = Color.FromArgb(230, 15, 15, 15),
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Top,
                Margin = new Thickness(10)
            };

            var hdrStyle = new Style(DefaultStyles.TextBlockStyle);
            hdrStyle.Set(ControlProperty.ForegroundColor, Color.Gray);
            hdrStyle.Set(ControlProperty.Margin, new Thickness(8, 4, 8, 4));

            var valStyle = new Style(DefaultStyles.TextBlockStyle);
            valStyle.Set(ControlProperty.ForegroundColor, Color.White);
            valStyle.Set(ControlProperty.FontWeight, FontWeight.Bold);
            valStyle.Set(ControlProperty.Margin, new Thickness(8, 4, 15, 4));

            grid.AddChild(new TextBlock { Text = "ENGINE STATUS:", Style = hdrStyle }, 0, 0);
            grid.AddChild(_uiStateText = new TextBlock { Text = "OPERATIONAL", ForegroundColor = Color.LimeGreen, Style = valStyle }, 0, 1);

            grid.AddChild(new TextBlock { Text = "Halt Reason:", Style = hdrStyle }, 1, 0);
            grid.AddChild(_uiReasonText = new TextBlock { Text = "-", Style = valStyle }, 1, 1);

            grid.AddChild(new TextBlock { Text = "Avg Latency (5m):", Style = hdrStyle }, 2, 0);
            grid.AddChild(_uiLatencyText = new TextBlock { Text = "0 ms", Style = valStyle }, 2, 1);

            grid.AddChild(new TextBlock { Text = "Avg Slippage (5m):", Style = hdrStyle }, 3, 0);
            grid.AddChild(_uiSlippageText = new TextBlock { Text = "0.0 pips", Style = valStyle }, 3, 1);

            grid.AddChild(new TextBlock { Text = "Venue Rejects (5m):", Style = hdrStyle }, 4, 0);
            grid.AddChild(_uiRejectText = new TextBlock { Text = "0", Style = valStyle }, 4, 1);

            var btnPanel = new WrapPanel { Margin = new Thickness(0, 10, 0, 0) };
            var btnBuy = new Button { Text = " BUY MKT ", BackgroundColor = Color.SeaGreen, Margin = new Thickness(5) };
            var btnSell = new Button { Text = " SELL MKT ", BackgroundColor = Color.Firebrick, Margin = new Thickness(5) };
            
            btnBuy.Click += args => DispatchOrder(TradeType.Buy);
            btnSell.Click += args => DispatchOrder(TradeType.Sell);
            
            btnPanel.AddChild(btnBuy);
            btnPanel.AddChild(btnSell);
            grid.AddChild(btnPanel, 5, 0, 1, 2); // Span 2 columns

            Chart.AddControl(grid);
        }

        private void DispatchOrder(TradeType side)
        {
            DateTime now = Server.TimeInUtc;

            if ((now - _lastClickTime).TotalMilliseconds < DebounceMs) return; // Silent debounce
            _lastClickTime = now;

            if (now < _breakerExpiration)
            {
                LogTelemetry(now, side, 0, 0, 0, 0, "Blocked", "CircuitBreaker", "System");
                return;
            }

            // Capture exact expected price at moment of click
            double expectedPrice = side == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
            double volume = Symbol.QuantityToVolumeInUnits(OrderLots);
            Stopwatch sw = Stopwatch.StartNew();

            ExecuteMarketOrderAsync(side, SymbolName, volume, "RiskEngine", result => 
            {
                sw.Stop();
                ProcessExecution(result, now, sw.ElapsedMilliseconds, side, expectedPrice);
            });
        }

        private void ProcessExecution(TradeResult result, DateTime requestTime, long latency, TradeType side, double expectedPrice)
        {
            double fillPrice = 0;
            double slippagePips = 0;
            string status = "Rejected";
            string faultOwner = "Venue";
            string riskVector = "None";

            if (result.IsSuccessful)
            {
                status = "Filled";
                faultOwner = "None";
                fillPrice = result.Position.EntryPrice;
                
                // Calculate directional slippage
                double rawSlippage = side == TradeType.Buy ? (fillPrice - expectedPrice) : (expectedPrice - fillPrice);
                slippagePips = Math.Round(rawSlippage / Symbol.PipSize, 2);

                _latencyBuffer.Add(new Tuple<DateTime, long>(requestTime, latency));
                _slippageBuffer.Add(new Tuple<DateTime, double>(requestTime, slippagePips));
            }
            else
            {
                if (result.Error == ErrorCode.NoMoney || result.Error == ErrorCode.EntityNotFound)
                {
                    faultOwner = "Trader";
                    riskVector = "Margin/Risk";
                }
                else
                {
                    _rejectStamps.Add(requestTime);
                    riskVector = "Reject";
                }
            }

            LogTelemetry(requestTime, side, expectedPrice, fillPrice, slippagePips, latency, status, riskVector, faultOwner);
            EvaluateRiskMetrics(requestTime);
        }

        private void EvaluateRiskMetrics(DateTime now)
        {
            // Clean old data from buffers
            DateTime cutoff = now.AddMinutes(-WindowMinutes);
            _rejectStamps.RemoveAll(t => t < cutoff);
            _latencyBuffer.RemoveAll(t => t.Item1 < cutoff);
            _slippageBuffer.RemoveAll(t => t.Item1 < cutoff);

            // Calculate rolling metrics
            int rejects = _rejectStamps.Count;
            double avgLatency = _latencyBuffer.Count > 0 ? _latencyBuffer.Average(x => x.Item2) : 0;
            double avgSlippage = _slippageBuffer.Count > 0 ? _slippageBuffer.Average(x => x.Item2) : 0;

            // Circuit Breaker Logic
            if (rejects >= MaxRejects)
            {
                TripBreaker(now, $"Venue Rejects ({rejects})");
            }
            else if (avgLatency > MaxLatencyMs)
            {
                TripBreaker(now, $"Latency Spike ({avgLatency:F0}ms)");
            }
            else if (avgSlippage > MaxSlippagePips)
            {
                TripBreaker(now, $"Toxic Slippage ({avgSlippage:F1} pips)");
            }
            else if (rejects >= MaxRejects - 1 || avgLatency > (MaxLatencyMs * 0.8))
            {
                _engineState = State.Warning;
            }

            UpdateUI(rejects, avgLatency, avgSlippage);
        }

        private void TripBreaker(DateTime now, string reason)
        {
            _breakerExpiration = now.AddMinutes(CooldownMinutes);
            _engineState = State.Halted;
            _haltReason = reason;
            
            // Clear buffers so it starts fresh after cooldown
            _rejectStamps.Clear();
            _latencyBuffer.Clear();
            _slippageBuffer.Clear();
            
            Print($"🛑 RISK ENGINE HALT: {reason}. Trading suspended for {CooldownMinutes}m.");
        }

        private void LogTelemetry(DateTime t, TradeType side, double reqPrice, double fillPrice, double slip, long lat, string stat, string vector, string fault)
        {
            // Thread-safe append to memory queue
            string logLine = $"{t:yyyy-MM-dd HH:mm:ss.fff},{SymbolName},{side},{reqPrice},{fillPrice},{slip},{lat},{stat},{vector},{fault}";
            _telemetryQueue.Enqueue(logLine);
        }

        protected override void OnTimer()
        {
            // I/O Offload: Write batch to disk once per second
            if (!_telemetryQueue.IsEmpty)
            {
                try
                {
                    var sb = new StringBuilder();
                    while (_telemetryQueue.TryDequeue(out string line))
                    {
                        sb.AppendLine(line);
                    }
                    File.AppendAllText(_logFilePath, sb.ToString());
                }
                catch (Exception ex) { Print($"I/O Error: {ex.Message}"); }
            }

            // State Recovery
            if (_engineState == State.Halted && Server.TimeInUtc >= _breakerExpiration)
            {
                _engineState = State.Operational;
                _haltReason = "-";
                UpdateUI(0, 0, 0); // Reset displays
            }
        }

        private void UpdateUI(int rejects, double avgLatency, double avgSlippage)
        {
            BeginInvokeOnMainThread(() =>
            {
                _uiRejectText.Text = rejects.ToString();
                _uiLatencyText.Text = $"{avgLatency:F0} ms";
                _uiSlippageText.Text = $"{avgSlippage:F1} pips";
                _uiReasonText.Text = _haltReason;

                switch (_engineState)
                {
                    case State.Operational:
                        _uiStateText.Text = "OPERATIONAL";
                        _uiStateText.ForegroundColor = Color.LimeGreen;
                        break;
                    case State.Warning:
                        _uiStateText.Text = "WARNING (Degraded)";
                        _uiStateText.ForegroundColor = Color.Orange;
                        break;
                    case State.Halted:
                        _uiStateText.Text = $"HALTED until {_breakerExpiration:HH:mm}";
                        _uiStateText.ForegroundColor = Color.Red;
                        break;
                }
            });
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order reject and requote rates: what to log before blaming your edge

Post by PTScalper »

Why this changes your workflow:

The Slippage Vector (MaxSlippagePips): Often, during rollover (e.g., 5 PM EST) or minor news, the broker won't reject you—they will just slip you 3 pips on a trade where your take-profit was only 5 pips to begin with. This engine calculates a rolling average of slippage. If the venue is silently eating your edge via bad fills, the breaker trips, and you are saved from "death by a thousand cuts."

The Latency Vector (MaxLatencyMs): You no longer have to wait for 3 rejects to know the venue is broken. If your normal latency is 40ms, and it suddenly spikes to 600ms on two consecutive fills, the API is lagging. A reject is mathematically imminent. The engine halts trading before the reject cluster even happens.

Data Lake Architecture: By writing Expected Price vs Filled Price to the CSV, your weekend review becomes entirely objective. You don't have to guess if "spreads were wide." You will have a precise log showing exactly what price you asked for versus what the liquidity provider gave you, allowing you to explicitly measure your real-world execution cost against your backtest edge.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply