We can also leverage cTrader's WPF-style UI framework to build a proper dark-mode, institutional Grid layout rather than placing objects by pixel coordinates.
Here is the Pro Risk Desk cBot for cTrader, engineered with asynchronous flattener methods, strict state management, and real-time environment overrides.
Code: Select all
using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
using cAlgo.API.Models;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class ProRiskDesk : Robot
{
// =========================================================================
// 1. RISK PARAMETERS (Sunday Prep)
// =========================================================================
[Parameter("Daily Loss Limit ($)", Group = "Capital Risk", DefaultValue = 500.0, MinValue = 0, Step = 50)]
public double MaxDailyLoss { get; set; }
[Parameter("Value of 1R ($)", Group = "Capital Risk", DefaultValue = 100.0, MinValue = 1, Step = 10)]
public double R_Value { get; set; }
[Parameter("Max Trades Per Day", Group = "Behavioral Circuit Breakers", DefaultValue = 10, MinValue = 1)]
public int MaxTrades { get; set; }
[Parameter("Max Consecutive Losses", Group = "Behavioral Circuit Breakers", DefaultValue = 3, MinValue = 1)]
public int MaxStreak { get; set; }
[Parameter("Flash Chart Override", Group = "Environment", DefaultValue = true)]
public bool FlashChart { get; set; }
// =========================================================================
// 2. STATE TRACKING
// =========================================================================
private double _peakDailyEquity;
private DateTime _currentDay;
private bool _isHalted;
private string _haltReason;
private Color _originalBgColor;
private bool _alertFired;
// UI Components
private Border _mainBorder;
private TextBlock _uiTitle;
private Border _titleBorder;
private TextBlock _uiPnL;
private TextBlock _uiTargetR;
private TextBlock _uiDrawdown;
private TextBlock _uiTrades;
private TextBlock _uiStreak;
protected override void OnStart()
{
_originalBgColor = Chart.ColorSettings.BackgroundColor;
ResetDailyState();
InitializeHUD();
// Sub-second enforcement independent of tick volume
Timer.Start(TimeSpan.FromSeconds(1));
}
protected override void OnStop()
{
// Clean up environment on removal
Chart.ColorSettings.BackgroundColor = _originalBgColor;
Chart.RemoveControl(_mainBorder);
}
protected override void OnTick() { EnforceRiskLimits(); }
protected override void OnTimer() { EnforceRiskLimits(); }
// =========================================================================
// 3. CORE LOGIC & LINQ PARSING
// =========================================================================
private void EnforceRiskLimits()
{
// Session Rollover
if (Server.Time.Date != _currentDay)
{
ResetDailyState();
}
// High-Water Mark Tracking
_peakDailyEquity = Math.Max(_peakDailyEquity, Account.Equity);
double peakDrawdown = _peakDailyEquity - Account.Equity;
// Isolate today's executions
var tradesToday = History.Where(t => t.ClosingTime >= _currentDay).ToList();
int totalTrades = tradesToday.Count;
// PnL Math
double realizedPnL = tradesToday.Sum(t => t.NetProfit); // NetProfit natively includes swap/commission
double floatingPnL = Positions.Sum(p => p.NetProfit);
double totalPnL = realizedPnL + floatingPnL;
double pnlInR = totalPnL / Math.Max(R_Value, 1);
// Behavioral Metric: Consecutive Losses using LINQ
int consecLosses = 0;
foreach (var trade in tradesToday.OrderByDescending(t => t.ClosingTime))
{
if (trade.NetProfit < 0) consecLosses++;
else break; // Streak broken
}
// Halt Triggers
bool limitHit = totalPnL <= -MaxDailyLoss;
bool tiltHit = consecLosses >= MaxStreak;
bool overtradeHit = totalTrades >= MaxTrades;
if (limitHit || tiltHit || overtradeHit)
{
_isHalted = true;
_haltReason = limitHit ? "HARD LIMIT MET" : (tiltHit ? "TILT: MAX LOSS STREAK" : "OVERTRADING CAP MET");
FlattenAccountAsync();
if (!_alertFired)
{
Print($"RISK DESK OVERRIDE: {_haltReason}");
_alertFired = true;
// Physical environment override
if (FlashChart)
{
Chart.ColorSettings.BackgroundColor = limitHit ? Color.FromHex("#3A0000") : Color.FromHex("#2A0033");
}
}
}
UpdateHUD(totalPnL, pnlInR, peakDrawdown, totalTrades, consecLosses);
}
private void ResetDailyState()
{
_currentDay = Server.Time.Date;
_peakDailyEquity = Account.Equity;
_isHalted = false;
_alertFired = false;
_haltReason = string.Empty;
Chart.ColorSettings.BackgroundColor = _originalBgColor;
}
// =========================================================================
// 4. ASYNC EXECUTION INTERCEPT
// =========================================================================
private void FlattenAccountAsync()
{
// Non-blocking async closure is critical for high-frequency scalping safety
foreach (var pos in Positions)
{
ClosePositionAsync(pos);
}
foreach (var order in PendingOrders)
{
CancelPendingOrderAsync(order);
}
}
// =========================================================================
// 5. WPF-STYLE UI RENDERING
// =========================================================================
private void InitializeHUD()
{
_uiTitle = CreateText("RISK DESK: ACTIVE", Color.White, 11, FontWeight.ExtraBold);
_uiTitle.HorizontalAlignment = HorizontalAlignment.Center;
_uiTitle.Margin = new Thickness(0, 5, 0, 5);
_titleBorder = new Border { BackgroundColor = Color.FromHex("#00ACC1"), Child = _uiTitle };
_uiPnL = CreateValueText("$0.00");
_uiTargetR = CreateValueText("0.00 R");
_uiDrawdown = CreateValueText("$0.00");
_uiTrades = CreateValueText("0 / 0");
_uiStreak = CreateValueText("0 / 0");
var layoutPanel = new StackPanel { Orientation = Orientation.Vertical };
layoutPanel.AddChild(_titleBorder);
layoutPanel.AddChild(CreateDataRow("Daily PnL", _uiPnL));
layoutPanel.AddChild(CreateDataRow("Target (R)", _uiTargetR));
layoutPanel.AddChild(CreateDataRow("Peak Drawdown", _uiDrawdown));
layoutPanel.AddChild(CreateDataRow("Trades Today", _uiTrades));
layoutPanel.AddChild(CreateDataRow("Loss Streak", _uiStreak));
_mainBorder = new Border
{
BackgroundColor = Color.FromHex("#131722"),
BorderColor = Color.FromHex("#2A2E39"),
BorderThickness = new Thickness(1),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Bottom,
Margin = new Thickness(20),
Width = 240,
Child = layoutPanel
};
Chart.AddControl(_mainBorder);
}
private Grid CreateDataRow(string label, TextBlock valueBlock)
{
var lbl = CreateText(label, Color.DarkGray, 11, FontWeight.Normal);
lbl.HorizontalAlignment = HorizontalAlignment.Left;
valueBlock.HorizontalAlignment = HorizontalAlignment.Right;
var rowGrid = new Grid(1, 2) { Margin = new Thickness(10, 5, 10, 5) };
rowGrid.AddChild(lbl, 0, 0);
rowGrid.AddChild(valueBlock, 0, 1);
return rowGrid;
}
private TextBlock CreateText(string text, Color color, double size, FontWeight weight)
{
return new TextBlock
{
Text = text,
ForegroundColor = color,
FontSize = size,
FontWeight = weight,
FontFamily = "Consolas" // Enforces tabular monospacing
};
}
private TextBlock CreateValueText(string text)
{
return CreateText(text, Color.White, 11, FontWeight.Bold);
}
private void UpdateHUD(double pnl, double pnlInR, double peakDrawdown, int tradesToday, int consecLosses)
{
if (_isHalted)
{
_uiTitle.Text = _haltReason;
_titleBorder.BackgroundColor = Color.FromHex("#8B0000"); // Dark Red
_mainBorder.BackgroundColor = Color.Black;
}
else
{
_uiTitle.Text = "RISK DESK: ACTIVE";
_titleBorder.BackgroundColor = Color.FromHex("#00ACC1"); // Teal
}
_uiPnL.Text = $"{(pnl >= 0 ? "+" : "-")}${Math.Abs(pnl):F2}";
_uiPnL.ForegroundColor = pnl >= 0 ? Color.LimeGreen : Color.OrangeRed;
_uiTargetR.Text = $"{(pnlInR >= 0 ? "+" : "")}{pnlInR:F2} R";
_uiTargetR.ForegroundColor = pnlInR >= 0 ? Color.LimeGreen : Color.OrangeRed;
_uiDrawdown.Text = $"${peakDrawdown:F2}";
_uiDrawdown.ForegroundColor = Color.DarkGray;
_uiTrades.Text = $"{tradesToday} / {MaxTrades}";
_uiTrades.ForegroundColor = tradesToday >= MaxTrades ? Color.Red : Color.White;
_uiStreak.Text = $"{consecLosses} / {MaxStreak}";
_uiStreak.ForegroundColor = consecLosses >= MaxStreak ? Color.Red : Color.White;
}
}
}