Operational Deployment
Instance: Attach this cBot to a blank, dedicated chart (e.g., a daily chart) so it stays out of the way of your active 1m/5m execution charts.
Asynchronous Execution: The flattener uses ClosePositionAsync and CancelPendingOrderAsync. In a fast-moving scalping environment, you want the cBot firing off the close commands instantly without waiting for the server to confirm each one sequentially.
The Intercept: Because the OnTimer loop runs every 1 second, if you attempt to override the limit by manually hitting "Buy" on the platform after you've hit max loss, the cBot will instantly detect the negative daily balance and kill the new trade in under a second.
Sticky-note daily loss limit that faces the screen
Re: Sticky-note daily loss limit that faces the screen
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.
cTrader: Pro Risk Desk (cBot)
Here is the Pro Risk Desk cBot for cTrader, engineered with asynchronous flattener methods, strict state management, and real-time environment overrides.
cTrader: Pro Risk Desk (cBot)
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;
}
}
}Re: Sticky-note daily loss limit that faces the screen
Why this is a superior architecture for C#:
True Async Execution: Notice the use of ClosePositionAsync and CancelPendingOrderAsync. In MQL, closing trades is a synchronous, blocking action that queues up. If you are scalping raw PA and need the circuit breaker to trigger instantly while 4 orders are open, .Async drops them all simultaneously without hanging the thread.
LINQ over Loops: Pulling your daily trade count and consecutive losses takes exactly 2 lines of clean LINQ (Where, OrderByDescending, Sum) rather than tracking reverse index positions through an arbitrary historical array pool.
WPF UI Engine: Leveraging cTrader’s internal Automate UI wrapper (Border, StackPanel, Grid, TextBlock), we can build a dynamic UI that scales cleanly with the platform's DPI settings, rather than manually painting pixels onto a chart axis like in MetaTrader.
Run this on a clean cTrader chart (e.g., Daily timeframe). If you hit a behavioral tilt limit (e.g., 3 consecutive losses), the background goes dark purple and the engine locks out. If you hit the hard cash limit, the chart flashes maroon, terminating the session.
True Async Execution: Notice the use of ClosePositionAsync and CancelPendingOrderAsync. In MQL, closing trades is a synchronous, blocking action that queues up. If you are scalping raw PA and need the circuit breaker to trigger instantly while 4 orders are open, .Async drops them all simultaneously without hanging the thread.
LINQ over Loops: Pulling your daily trade count and consecutive losses takes exactly 2 lines of clean LINQ (Where, OrderByDescending, Sum) rather than tracking reverse index positions through an arbitrary historical array pool.
WPF UI Engine: Leveraging cTrader’s internal Automate UI wrapper (Border, StackPanel, Grid, TextBlock), we can build a dynamic UI that scales cleanly with the platform's DPI settings, rather than manually painting pixels onto a chart axis like in MetaTrader.
Run this on a clean cTrader chart (e.g., Daily timeframe). If you hit a behavioral tilt limit (e.g., 3 consecutive losses), the background goes dark purple and the engine locks out. If you hit the hard cash limit, the chart flashes maroon, terminating the session.
-
LondonScalper
- Posts: 965
- Joined: Sat Sep 05, 2026 7:54 am
Re: Sticky-note daily loss limit that faces the screen
Hardware in the peripheral vision still beats a menu I can edit mid-spiral. The sticky note is in local currency and in R, written before the open, and it does not have an “override for this ticket” button.
I tried the platform setting alone for a year. It worked until the day I wanted a second ticket and quietly told myself the limit was a guideline. The note does not accept that conversation. If the number is hit, the session is over — no temporary raise, no “one more at half size.”
Ugly, visible, slightly embarrassing on a desk photo. That is why it works.
I tried the platform setting alone for a year. It worked until the day I wanted a second ticket and quietly told myself the limit was a guideline. The note does not accept that conversation. If the number is hit, the session is over — no temporary raise, no “one more at half size.”
Ugly, visible, slightly embarrassing on a desk photo. That is why it works.
-
FXS Prop Desk
- Posts: 1483
- Joined: Wed Oct 07, 2026 9:52 am
Re: Sticky-note daily loss limit that faces the screen
Physical friction works for the reason quoted: there is nothing to negotiate with. A sticky note, though, slowly turns into wallpaper. After a few weeks the eye stops reading it. Changing the colour or the position of the note at the start of each month, or writing the day's limit by hand every morning, keeps it visible. Writing the number fresh each day also forces a quick check of the account size, so the limit stays tied to the current balance rather than to whatever it was when the note first went up.
FXS Official Team | The Idea Lab & Strategy