Method 1: The Tick-Latency Queue (Real-World Simulation)
Instead of using Thread.Sleep()—which just freezes your CPU and does not advance market time in a backtest—you build a time-based execution queue. When the scale-out target is hit, the bot calculates a random latency (e.g., 200ms to 1,500ms) and queues the order. As Server.Time advances, the market ticks forward. When the delay expires, the order fires, subjecting you to true historical slippage.
1. Add the Queue and Parameters
At the top of your class, add the slippage simulator inputs and a structured queue.
At the very beginning of your OnTick method, process the queue before evaluating new signals. Using a reverse for loop prevents collection-modification exceptions when removing fired items.
if (isTargetHit)
{
// Ensure we don't queue the same scale-out 500 times while waiting for the delay
if (_queuedScaleOutIds.Contains(position.Id)) return;
double scaleOutVolume = Symbol.NormalizeVolumeInUnits(position.VolumeInUnits / 2, RoundingMode.Down);
if (scaleOutVolume >= Symbol.VolumeInUnitsMin)
{
_queuedScaleOutIds.Add(position.Id);
// If simulator is off, delay is 0
int delay = EnableLatencySim ? _rng.Next(MinDelayMs, MaxDelayMs) : 0;
DateTime executionTime = Server.Time.AddMilliseconds(delay);
Print($"[Target Hit] Queuing {scaleOutVolume} units. Broker Lag: {delay}ms.");
// Inject the API command into the queue
_executionQueue.Add(new PendingExecution
{
ExecutionTime = executionTime,
Command = () =>
{
// Verify position wasn't closed by a hard stop during the delay
var activePos = Positions.Find(BotLabel, SymbolName);
if (activePos != null)
{
TradeResult closeResult = ClosePosition(activePos, scaleOutVolume);
if (closeResult.IsSuccessful)
{
Print($"[Scale-Out Filled] Simulated slippage captured.");
_scaleOutTargets.Remove(activePos.Id);
// Execute Breakeven logic here...
}
}
_queuedScaleOutIds.Remove(position.Id);
}
});
}
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Method 2: The Shadow Ledger (Mathematical Worst-Case)
The Latency Queue mimics reality, but sometimes you want a brutal, mathematical worst-case baseline: "What if I am forced to pay exactly 2 pips of adverse slippage on every single scale-out and exit?"
Because you cannot deduct funds from the cTrader backtester balance, you track a shadow ledger in memory and print it directly to your chart interface.
Subscribe to the Positions.Closed event. When a trade closes, cTrader provides the actual Gross Profit. You calculate the mathematical penalty of your AdverseSlippagePips multiplied by the volume closed, deduct it from the gross, and update the UI.
When running your visual backtest, combine these two methods:
Set Max Delay to 2,000ms: Watch how your scale-outs behave during high-impact news. The price will hit the target line, and you will physically see the candle continue to stretch for 2 seconds before the API closure fires, resulting in deeply negative (or occasionally positive) fills.
Monitor the Shadow Ledger: Even if the latency queue randomly gives you positive slippage, the Shadow Ledger on the top left of your screen will continuously penalize your strategy by your strict 2.0 pip mathematical rule. If the strategy remains profitable on the Shadow Ledger, it possesses a massive edge.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
The flaw with a static spread filter (e.g., MaxSpread = 2.0 pips) is that it is blind to market context. During the Asian session, a 2.0 pip spread might be a severe liquidity fracture. During high-impact news, a 2.0 pip spread might be the best fill you can legally get.
To build a Dynamic Spread Expansion Filter, you must establish a rolling baseline of what is "normal" for the current session, and multiply it.
The architectural trap here is calculating your baseline tick-by-tick. A daily rollover lasts 5 minutes and can generate thousands of ticks. If you use a tick-based queue, your baseline will rapidly adapt to the rollover, defeating the filter.
You must use a Time-Sampled Baseline Generator—sampling the spread once per minute over a 60-minute rolling window, ensuring the baseline remains anchored to true session liquidity.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
[Parameter("Dynamic Spread Multiplier", DefaultValue = 3.0, Group = "Liquidity Protection",
ToolTip = "Halts execution if current spread is X times larger than the 1-hour average.")]
public double SpreadMultiplier { get; set; }
[Parameter("Absolute Max Spread (Pips)", DefaultValue = 5.0, Group = "Liquidity Protection",
ToolTip = "The hard ceiling failsafe. Execution is blocked if spread crosses this, regardless of the baseline.")]
public double AbsoluteMaxSpread { get; set; }
[Parameter("Minimum Baseline Spread (Pips)", DefaultValue = 0.5, Group = "Liquidity Protection",
ToolTip = "Protects against math errors if the broker provides a 0.0 pip spread. Prevents the dynamic limit from becoming too tight.")]
public double MinBaselineSpread { get; set; }
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Inject this engine at the very top of your OnTick method. It calculates the live spread, maintains the 60-minute time-sampled queue, and establishes the exact effectiveSpreadLimit for the current millisecond.