Order reject and requote rates: what to log before blaming your edge
-
LondonScalper
- Posts: 701
- Joined: Sat Sep 05, 2026 7:54 am
Order reject and requote rates: what to log before blaming your edge
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.
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.
Re: Order reject and requote rates: what to log before blaming your edge
Hi LondonScalper,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.
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.
Re: Order reject and requote rates: what to log before blaming your edge
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).
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.
Re: Order reject and requote rates: what to log before blaming your edge
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.
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.
Re: Order reject and requote rates: what to log before blaming your edge
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.
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.
Re: Order reject and requote rates: what to log before blaming your edge
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.
Re: Order reject and requote rates: what to log before blaming your edge
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.
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.
Re: Order reject and requote rates: what to log before blaming your edge
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.
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.
Re: Order reject and requote rates: what to log before blaming your edge
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.
Re: Order reject and requote rates: what to log before blaming your edge
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.
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.