To make this professional grade, we need to move away from a basic script that just prints text and instead build an event-driven state machine with active enforcement.
If adrenaline doesn't get a vote, the bot shouldn't just warn you — it should actively intercept and kill unauthorized manual orders during the lockdown, manage state transitions cleanly, and use a proper Canvas UI dashboard rather than legacy text drawing.
Here is the
enterprise-grade C# implementation for cTrader. It separates time evaluation from tick evaluation, implements a strict state machine, and actively defends the account.
Code: Select all
using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class ECBRiskManagerPro : Robot
{
#region Parameters
[Parameter("Event Time (Broker HH:mm)", Group = "Schedule", DefaultValue = "13:15")]
public string EventTimeString { get; set; }
[Parameter("Pre-Event Lockdown (Mins)", Group = "Schedule", DefaultValue = 5, MinValue = 1)]
public int PreEventMinutes { get; set; }
[Parameter("Stand-Aside Timer (Mins)", Group = "Schedule", DefaultValue = 15, MinValue = 1)]
public int StandAsideMinutes { get; set; }
[Parameter("Max Allowed Spread (Pips)", Group = "Microstructure", DefaultValue = 0.8)]
public double MaxSpreadPips { get; set; }
[Parameter("Kill Manual Trades During Timer", Group = "Enforcement", DefaultValue = true)]
public bool StrictEnforcement { get; set; }
#endregion
#region State Management
private enum RiskState
{
Armed,
Lockdown, // Pre-event order cancellation phase
TimerActive, // Absolute stand-aside window
SpreadCheck, // Timer expired, waiting for liquidity to normalize
Cleared // Ready for half-size entry
}
private RiskState _currentState = RiskState.Armed;
private DateTime _eventTime;
// UI Elements
private TextBlock _statusText;
private Border _uiContainer;
#endregion
protected override void OnStart()
{
InitializeEventTime();
InitializeDashboard();
// Hook into manual order events for strict enforcement
if (StrictEnforcement)
{
Positions.Opened += OnPositionOpened;
PendingOrders.Created += OnPendingOrderCreated;
}
// Run the state evaluator every second, decoupling it from tick volume
Timer.Start(TimeSpan.FromSeconds(1));
}
protected override void OnTimer()
{
EvaluateStateTransitions();
}
protected override void OnTick()
{
// Tick events only matter for spread evaluation during the SpreadCheck phase
if (_currentState == RiskState.SpreadCheck)
{
EvaluateSpread();
}
}
#region Core Logic
private void EvaluateStateTransitions()
{
var now = Server.Time;
switch (_currentState)
{
case RiskState.Armed:
if (now >= _eventTime.AddMinutes(-PreEventMinutes) && now < _eventTime)
{
TransitionTo(RiskState.Lockdown);
CancelAllWorkingOrders();
}
break;
case RiskState.Lockdown:
if (now >= _eventTime)
{
TransitionTo(RiskState.TimerActive);
}
break;
case RiskState.TimerActive:
if (now >= _eventTime.AddMinutes(StandAsideMinutes))
{
TransitionTo(RiskState.SpreadCheck);
}
break;
}
}
private void EvaluateSpread()
{
double currentSpread = Symbol.Spread / Symbol.PipSize;
if (currentSpread <= MaxSpreadPips)
{
TransitionTo(RiskState.Cleared);
}
}
private void TransitionTo(RiskState newState)
{
if (_currentState == newState) return;
_currentState = newState;
UpdateDashboard();
Print($"[ECB Manager] State transitioned to: {newState}");
}
private void CancelAllWorkingOrders()
{
var pendingOrders = PendingOrders.Where(o => o.SymbolName == SymbolName).ToList();
foreach (var order in pendingOrders)
{
CancelPendingOrderAsync(order, result =>
{
if (result.IsSuccessful)
Print($"[ECB Manager] Cancelled pending order {order.Id}");
});
}
}
#endregion
#region Strict Enforcement (The Adrenaline Blocker)
private void OnPositionOpened(PositionOpenedEventArgs args)
{
if (args.Position.SymbolName != SymbolName) return;
if (_currentState == RiskState.Lockdown || _currentState == RiskState.TimerActive || _currentState == RiskState.SpreadCheck)
{
Print("[ECB Manager] UNAUTHORIZED ENTRY BLOCKED. Closing position immediately.");
ClosePositionAsync(args.Position);
}
}
private void OnPendingOrderCreated(PendingOrderCreatedEventArgs args)
{
if (args.PendingOrder.SymbolName != SymbolName) return;
if (_currentState == RiskState.Lockdown || _currentState == RiskState.TimerActive || _currentState == RiskState.SpreadCheck)
{
Print("[ECB Manager] UNAUTHORIZED PENDING ORDER BLOCKED. Cancelling immediately.");
CancelPendingOrderAsync(args.PendingOrder);
}
}
#endregion
#region Setup & UI
private void InitializeEventTime()
{
if (!TimeSpan.TryParse(EventTimeString, out TimeSpan timeOfDay))
{
Print("CRITICAL: Invalid time format. Use HH:mm.");
Stop();
return;
}
_eventTime = Server.Time.Date.Add(timeOfDay);
if (_eventTime < Server.Time)
{
_eventTime = _eventTime.AddDays(1);
}
Print($"[ECB Manager] Armed for {_eventTime} (Broker Time)");
}
private void InitializeDashboard()
{
_statusText = new TextBlock
{
Text = "ARMED: WAITING FOR ECB",
ForegroundColor = Color.White,
FontWeight = FontWeight.ExtraBold,
Margin = new Thickness(10)
};
_uiContainer = new Border
{
BackgroundColor = Color.FromArgb(200, 30, 30, 30),
BorderColor = Color.Gray,
BorderThickness = new Thickness(2),
CornerRadius = 3,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 20, 0, 0),
Child = _statusText
};
Chart.AddControl(_uiContainer);
UpdateDashboard();
}
private void UpdateDashboard()
{
switch (_currentState)
{
case RiskState.Armed:
_statusText.Text = $"ARMED: PRE-LOCKDOWN SET FOR {_eventTime.AddMinutes(-PreEventMinutes):HH:mm}";
_uiContainer.BorderColor = Color.DarkGray;
break;
case RiskState.Lockdown:
_statusText.Text = "LOCKDOWN: FLATTENING WORKING ORDERS";
_uiContainer.BorderColor = Color.Orange;
_statusText.ForegroundColor = Color.Orange;
break;
case RiskState.TimerActive:
_statusText.Text = "STAND ASIDE: TAPE IS TOXIC";
_uiContainer.BorderColor = Color.Red;
_statusText.ForegroundColor = Color.Red;
break;
case RiskState.SpreadCheck:
_statusText.Text = "TIMER DONE: WAITING FOR SPREAD NORMALIZATION";
_uiContainer.BorderColor = Color.Yellow;
_statusText.ForegroundColor = Color.Yellow;
break;
case RiskState.Cleared:
_statusText.Text = "CLEAR TO TRADE: APPLY HALF-SIZE RULE";
_uiContainer.BorderColor = Color.LimeGreen;
_statusText.ForegroundColor = Color.LimeGreen;
break;
}
}
#endregion
}
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.