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;
}
});
}
}
}