Key Behavioral Enhancements
State Hijacking (PivotoPressConference): Even if the bot has cleared you at 13:32 to take a half-size post-statement scalp, the evaluator checks now >= _pressConfTime.AddMinutes(-PreEventMinutes) on every 1-second pulse. At 13:40 sharp, it overrides Cleared, enters Lockdown, cancels any resting working orders that weren't triggered, and re-engages the adrenaline blocker.
Distinct Stand-Aside Clocks: Statement prints get a quick, sharp window (StatementStandAsideMinutes = 15), while the press conference receives an extended duration (PressConfStandAsideMinutes = 45) because the opening statement read-through and initial journalist Q&A routinely inject secondary volatility waves.
Visual Context on the Tape: During the interim clearance window (between 13:30 and 13:40), the UI banner explicitly warns: [STATEMENT] CLEAR TO TRADE: HALF SIZE ONLY (PRESSER AT 13:45).
Seconds after ECB rate decision on EURUSD: my stand-aside timer
Re: Seconds after ECB rate decision on EURUSD: my stand-aside timer
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Seconds after ECB rate decision on EURUSD: my stand-aside timer
To enforce a true hard-stop where the bot forcefully yanks you out of the market if you linger too long in that interim 13:30–13:40 window, we need to replace the CancelAllWorkingOrders() method with a comprehensive FlattenAllExposure() method.
This new method targets both PendingOrders and open Positions on the current symbol. We then call this method whenever the state transitions into Lockdown.
This new method targets both PendingOrders and open Positions on the current symbol. We then call this method whenever the state transitions into Lockdown.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Seconds after ECB rate decision on EURUSD: my stand-aside timer
Here is the fully updated code. The key architectural changes are the new FlattenAllExposure() method and the updated dashboard state which now reads "FLATTENING EXPOSURE".
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 ECBDualPhaseRiskManager : Robot
{
#region Parameters
[Parameter("Statement Time (Broker HH:mm)", Group = "Schedule", DefaultValue = "13:15")]
public string StatementTimeString { get; set; }
[Parameter("Press Conf Time (Broker HH:mm)", Group = "Schedule", DefaultValue = "13:45")]
public string PressConfTimeString { get; set; }
[Parameter("Pre-Event Purge Window (Mins)", Group = "Schedule", DefaultValue = 5, MinValue = 1)]
public int PreEventMinutes { get; set; }
[Parameter("Statement Stand-Aside (Mins)", Group = "Timers", DefaultValue = 15, MinValue = 1)]
public int StatementStandAsideMinutes { get; set; }
[Parameter("Press Conf Stand-Aside (Mins)", Group = "Timers", DefaultValue = 45, MinValue = 1)]
public int PressConfStandAsideMinutes { get; set; }
[Parameter("Max Allowed Spread (Pips)", Group = "Microstructure", DefaultValue = 0.8)]
public double MaxSpreadPips { get; set; }
[Parameter("Kill Manual Trades During Lockdown", Group = "Enforcement", DefaultValue = true)]
public bool StrictEnforcement { get; set; }
#endregion
#region State Machine
private enum EventPhase
{
Statement,
PressConference,
Complete
}
private enum RiskState
{
Armed,
Lockdown, // All exposure flattened, trading blocked
TimerActive, // Absolute stand-aside window
SpreadCheck, // Timer expired, waiting for liquidity to normalize
Cleared // Half-size window open
}
private EventPhase _currentPhase = EventPhase.Statement;
private RiskState _currentState = RiskState.Armed;
private DateTime _statementTime;
private DateTime _pressConfTime;
private DateTime _activeTargetTime;
private int _activeStandAsideMinutes;
// UI Elements
private TextBlock _statusText;
private Border _uiContainer;
#endregion
protected override void OnStart()
{
InitializeSchedules();
InitializeDashboard();
if (StrictEnforcement)
{
Positions.Opened += OnPositionOpened;
PendingOrders.Created += OnPendingOrderCreated;
}
Timer.Start(TimeSpan.FromSeconds(1));
}
protected override void OnTimer()
{
EvaluateStateTransitions();
}
protected override void OnTick()
{
if (_currentState == RiskState.SpreadCheck)
{
EvaluateSpread();
}
}
#region Transition Engine
private void EvaluateStateTransitions()
{
var now = Server.Time;
// Phase 1 -> Phase 2 Transition Check
if (_currentPhase == EventPhase.Statement && now >= _pressConfTime.AddMinutes(-PreEventMinutes))
{
PivotoPressConference();
return;
}
// Standard State Lifecycle
switch (_currentState)
{
case RiskState.Armed:
if (now >= _activeTargetTime.AddMinutes(-PreEventMinutes) && now < _activeTargetTime)
{
TransitionTo(RiskState.Lockdown);
FlattenAllExposure();
}
break;
case RiskState.Lockdown:
if (now >= _activeTargetTime)
{
TransitionTo(RiskState.TimerActive);
}
break;
case RiskState.TimerActive:
if (now >= _activeTargetTime.AddMinutes(_activeStandAsideMinutes))
{
TransitionTo(RiskState.SpreadCheck);
}
break;
case RiskState.Cleared:
if (_currentPhase == EventPhase.PressConference && now >= _pressConfTime.AddMinutes(_activeStandAsideMinutes + 60))
{
_currentPhase = EventPhase.Complete;
UpdateDashboard();
}
break;
}
}
private void PivotoPressConference()
{
Print("[ECB Engine] PIVOT: Entering Press Conference Phase. Revoking permissions & liquidating.");
_currentPhase = EventPhase.PressConference;
_activeTargetTime = _pressConfTime;
_activeStandAsideMinutes = PressConfStandAsideMinutes;
TransitionTo(RiskState.Lockdown);
FlattenAllExposure();
}
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 Engine] [{_currentPhase}] State -> {newState}");
}
// --- NEW COMPREHENSIVE FLATTEN METHOD ---
private void FlattenAllExposure()
{
// 1. Purge Pending Orders
var pendingOrders = PendingOrders.Where(o => o.SymbolName == SymbolName).ToList();
foreach (var order in pendingOrders)
{
CancelPendingOrderAsync(order, res =>
{
if (res.IsSuccessful)
Print($"[ECB Engine] Working order cancelled: {order.Id}");
});
}
// 2. Liquidate Open Positions
var openPositions = Positions.Where(p => p.SymbolName == SymbolName).ToList();
foreach (var position in openPositions)
{
ClosePositionAsync(position, res =>
{
if (res.IsSuccessful)
Print($"[ECB Engine] Interim open position force-closed: {position.Id}");
else
Print($"[ECB Engine] ERROR: Failed to close position {position.Id} - {res.Error}");
});
}
}
#endregion
#region Strict Enforcement
private void OnPositionOpened(PositionOpenedEventArgs args)
{
if (args.Position.SymbolName != SymbolName) return;
if (_currentState != RiskState.Cleared && _currentState != RiskState.Armed)
{
Print("[ECB Engine] UNAUTHORIZED POSITION DURING LOCKDOWN. Liquidating.");
ClosePositionAsync(args.Position);
}
}
private void OnPendingOrderCreated(PendingOrderCreatedEventArgs args)
{
if (args.PendingOrder.SymbolName != SymbolName) return;
if (_currentState != RiskState.Cleared && _currentState != RiskState.Armed)
{
Print("[ECB Engine] UNAUTHORIZED PENDING ORDER CREATED. Purging.");
CancelPendingOrderAsync(args.PendingOrder);
}
}
#endregion
#region Initialization & UI
private void InitializeSchedules()
{
if (!TimeSpan.TryParse(StatementTimeString, out TimeSpan stmtTime) ||
!TimeSpan.TryParse(PressConfTimeString, out TimeSpan pressTime))
{
Print("CRITICAL: Invalid HH:mm format.");
Stop();
return;
}
_statementTime = Server.Time.Date.Add(stmtTime);
_pressConfTime = Server.Time.Date.Add(pressTime);
if (_statementTime < Server.Time && Server.Time < _pressConfTime)
{
_currentPhase = EventPhase.PressConference;
_activeTargetTime = _pressConfTime;
_activeStandAsideMinutes = PressConfStandAsideMinutes;
}
else if (_statementTime < Server.Time && _pressConfTime < Server.Time)
{
_statementTime = _statementTime.AddDays(1);
_pressConfTime = _pressConfTime.AddDays(1);
_activeTargetTime = _statementTime;
_activeStandAsideMinutes = StatementStandAsideMinutes;
}
else
{
_activeTargetTime = _statementTime;
_activeStandAsideMinutes = StatementStandAsideMinutes;
}
Print($"[ECB Engine] Armed: Statement at {_statementTime:HH:mm}, Presser at {_pressConfTime:HH:mm}");
}
private void InitializeDashboard()
{
_statusText = new TextBlock
{
Text = "ARMED",
ForegroundColor = Color.White,
FontWeight = FontWeight.ExtraBold,
Margin = new Thickness(10)
};
_uiContainer = new Border
{
BackgroundColor = Color.FromArgb(220, 20, 20, 20),
BorderThickness = new Thickness(2),
CornerRadius = 4,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 25, 0, 0),
Child = _statusText
};
Chart.AddControl(_uiContainer);
UpdateDashboard();
}
private void UpdateDashboard()
{
string phaseTag = _currentPhase == EventPhase.Statement ? "[STATEMENT]" : "[PRESS CONF]";
switch (_currentState)
{
case RiskState.Armed:
_statusText.Text = $"{phaseTag} ARMED: PURGE AT {_activeTargetTime.AddMinutes(-PreEventMinutes):HH:mm}";
_uiContainer.BorderColor = Color.DarkGray;
_statusText.ForegroundColor = Color.White;
break;
case RiskState.Lockdown:
_statusText.Text = $"{phaseTag} LOCKDOWN: FLATTENING EXPOSURE & BLOCKING ENTRIES";
_uiContainer.BorderColor = Color.Orange;
_statusText.ForegroundColor = Color.Orange;
break;
case RiskState.TimerActive:
_statusText.Text = $"{phaseTag} STAND ASIDE: CLOCK RUNNING";
_uiContainer.BorderColor = Color.Red;
_statusText.ForegroundColor = Color.Red;
break;
case RiskState.SpreadCheck:
_statusText.Text = $"{phaseTag} TIMER ELAPSED: WAITING FOR SPREAD NORMALIZATION";
_uiContainer.BorderColor = Color.Yellow;
_statusText.ForegroundColor = Color.Yellow;
break;
case RiskState.Cleared:
string warning = _currentPhase == EventPhase.Statement ? $" (PRESSER AT {_pressConfTime:HH:mm})" : "";
_statusText.Text = $"{phaseTag} CLEAR TO TRADE: HALF SIZE ONLY{warning}";
_uiContainer.BorderColor = Color.LimeGreen;
_statusText.ForegroundColor = Color.LimeGreen;
break;
}
if (_currentPhase == EventPhase.Complete)
{
_statusText.Text = "ECB COMPLETE: STANDARD PROTOCOLS RESTORED";
_uiContainer.BorderColor = Color.Gray;
_statusText.ForegroundColor = Color.Gray;
}
}
#endregion
}
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Seconds after ECB rate decision on EURUSD: my stand-aside timer
What happens now:
If you take a post-Statement trade at 13:33, and it gets chopped around rather than hitting your TP before 13:40 (assuming a 5-minute PreEventMinutes window), the moment the clock hits 13:40:00, the PivotoPressConference() function fires. It transitions the state to Lockdown, triggers FlattenAllExposure(), and sends an asynchronous market order to the server to immediately close that open position, ensuring you have absolute zero exposure going into Lagarde's speech.
If you take a post-Statement trade at 13:33, and it gets chopped around rather than hitting your TP before 13:40 (assuming a 5-minute PreEventMinutes window), the moment the clock hits 13:40:00, the PivotoPressConference() function fires. It transitions the state to Lockdown, triggers FlattenAllExposure(), and sends an asynchronous market order to the server to immediately close that open position, ensuring you have absolute zero exposure going into Lagarde's speech.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.