Because cTrader operates on C# and .NET, the implementation is significantly cleaner than MQL. We can leverage event-driven architecture (Positions.Closed), LINQ for the statistical modeling, and a Queue<double> for memory-efficient rolling arrays.
A critical nuance in the cTrader API is that once a position hits its Stop Loss, the Position.StopLoss property is often nullified in the closed event. To bypass this, this cBot caches the active Stop Loss of all open positions in a Dictionary, allowing it to accurately compare the expected execution against the actual ClosingPrice found in the History ledger.
cTrader cBot: Quantitative Slippage & State Machine
Code: Select all
using System;
using System.Linq;
using System.Collections.Generic;
using cAlgo.API;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class InstitutionalCircuitBreaker : Robot
{
public enum ExecutionState
{
Active, // Full Risk
Locked, // Hard Lockout
Recovery // Reduced Risk
}
// --- System Parameters ---
[Parameter("Lockout Duration (Min)", Group = "Risk Management", DefaultValue = 15)]
public int LockoutMinutes { get; set; }
[Parameter("Z-Score Limit (StdDev)", Group = "Risk Management", DefaultValue = 2.0)]
public double StdDevLimit { get; set; }
[Parameter("Min Slip to Track (Pips)", Group = "Risk Management", DefaultValue = 1.0)]
public double MinSlipPips { get; set; }
[Parameter("Rolling Array Size", Group = "Risk Management", DefaultValue = 50)]
public int MaxHistorySize { get; set; }
// --- State Machine ---
private ExecutionState _currentState = ExecutionState.Active;
private DateTime _lockoutEndTime;
// --- Statistical Arrays & Cache ---
private readonly Queue<double> _slipHistory = new Queue<double>();
private readonly Dictionary<int, double> _activeStops = new Dictionary<int, double>();
protected override void OnStart()
{
// Subscribe to position events
Positions.Opened += OnPositionOpened;
Positions.Modified += OnPositionModified;
Positions.Closed += OnPositionClosed;
// Use a 1-second timer to manage state transitions and UI updates
Timer.Start(1);
Print("Execution Architecture Initialized.");
}
protected override void OnTimer()
{
// 1. Manage State Unlocking
if (_currentState == ExecutionState.Locked && Server.Time >= _lockoutEndTime)
{
_currentState = ExecutionState.Recovery;
Print("SYSTEM UNLOCKED: Entering Recovery State (Fractional Risk).");
}
// 2. Update UI Dashboard
UpdateDashboard();
}
// --- Stop Loss Caching Engine ---
private void OnPositionOpened(PositionOpenedEventArgs args) => CacheStopLoss(args.Position);
private void OnPositionModified(PositionModifiedEventArgs args) => CacheStopLoss(args.Position);
private void CacheStopLoss(Position position)
{
if (position.SymbolName != SymbolName || position.Label != "Institutional_PA") return;
if (position.StopLoss.HasValue)
_activeStops[position.Id] = position.StopLoss.Value;
else
_activeStops.Remove(position.Id);
}
// --- Execution Audit & Variance Math ---
private void OnPositionClosed(PositionClosedEventArgs args)
{
var position = args.Position;
if (position.SymbolName != SymbolName) return;
// Clean up the cache
_activeStops.TryGetValue(position.Id, out double expectedStop);
_activeStops.Remove(position.Id);
// Fetch actual execution price from History ledger
var historicalTrade = History.LastOrDefault(x => x.PositionId == position.Id);
if (historicalTrade == null) return;
double closingPrice = historicalTrade.ClosingPrice;
// Audit only losing trades with a defined Stop Loss
if (historicalTrade.NetProfit < 0 && expectedStop > 0)
{
// Verify the closure was likely a Stop Loss hit (not manual)
bool wasStoppedOut = (position.TradeType == TradeType.Buy && closingPrice <= expectedStop) ||
(position.TradeType == TradeType.Sell && closingPrice >= expectedStop);
if (wasStoppedOut)
{
double slipDistance = Math.Abs(expectedStop - closingPrice);
double slipPips = slipDistance / Symbol.PipSize;
if (slipPips > MinSlipPips)
{
UpdateStatisticalModel(slipPips);
}
}
}
else if (historicalTrade.NetProfit > 0 && _currentState == ExecutionState.Recovery)
{
// Successful execution in Recovery resets state to Active
_currentState = ExecutionState.Active;
Print("RECOVERY COMPLETE: System Restored to Full Risk.");
}
}
private void UpdateStatisticalModel(double newSlip)
{
// Enforce Queue Size
if (_slipHistory.Count >= MaxHistorySize)
{
_slipHistory.Dequeue();
}
_slipHistory.Enqueue(newSlip);
if (_slipHistory.Count > 5)
{
double mean = _slipHistory.Average();
double sumOfSquares = _slipHistory.Sum(val => Math.Pow(val - mean, 2));
double stdDev = Math.Sqrt(sumOfSquares / _slipHistory.Count);
double zScore = (stdDev > 0) ? (newSlip - mean) / stdDev : 0.0;
if (zScore > StdDevLimit)
{
_currentState = ExecutionState.Locked;
_lockoutEndTime = Server.Time.AddMinutes(LockoutMinutes);
string msg = string.Format("CIRCUIT BREAKER: Slipped {0:F1} pips. Z-Score: {1:F2}. System Locked.", newSlip, zScore);
Print(msg);
}
}
}
// --- Price Action Execution Framework ---
protected override void OnTick()
{
if (_currentState == ExecutionState.Locked) return;
// bool validLongSetup = ... (Insert Raw PA Logic Here)
// bool validShortSetup = ... (Insert Raw PA Logic Here)
// double riskMultiplier = _currentState == ExecutionState.Recovery ? 0.5 : 1.0;
// if (validLongSetup)
// {
// ExecuteMarketOrder(TradeType.Buy, SymbolName, VolumeInUnits * riskMultiplier, "Institutional_PA", stopLossPips, takeProfitPips);
// }
}
// --- UI Rendering ---
private void UpdateDashboard()
{
string stateTxt = _currentState == ExecutionState.Active ? "ACTIVE (FULL RISK)" :
_currentState == ExecutionState.Locked ? "LOCKED (NO EXECUTION)" :
"RECOVERY (HALF RISK)";
string color = _currentState == ExecutionState.Active ? "LimeGreen" :
_currentState == ExecutionState.Locked ? "Red" :
"Orange";
string timeToUnlock = _currentState == ExecutionState.Locked
? Math.Max(0, (_lockoutEndTime - Server.Time).TotalMinutes).ToString("F1") + " min"
: "N/A";
string dashboardText = $"<tspan fill=\"Gray\">SYSTEM STATE:</tspan> <tspan fill=\"{color}\" font-weight=\"bold\">{stateTxt}</tspan>\n" +
$"<tspan fill=\"Gray\">Avg Slip (Pips):</tspan> {(_slipHistory.Any() ? _slipHistory.Average().ToString("F2") : "0.00")}\n" +
$"<tspan fill=\"Gray\">Unlock In:</tspan> {timeToUnlock}";
Chart.DrawStaticText("SysDash", dashboardText, VerticalAlignment.Bottom, HorizontalAlignment.Right, Color.White);
}
}
}