Page 8 of 9
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:55 am
by PTScalper
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.
Code: Select all
[Parameter("Enable Latency Simulator", DefaultValue = true, Group = "Slippage Simulator")]
public bool EnableLatencySim { get; set; }
[Parameter("Min Delay (ms)", DefaultValue = 100, Group = "Slippage Simulator")]
public int MinDelayMs { get; set; }
[Parameter("Max Delay (ms)", DefaultValue = 1500, Group = "Slippage Simulator")]
public int MaxDelayMs { get; set; }
// Structure to hold delayed API commands
private class PendingExecution
{
public DateTime ExecutionTime { get; set; }
public Action Command { get; set; }
}
private List<PendingExecution> _executionQueue = new List<PendingExecution>();
private HashSet<int> _queuedScaleOutIds = new HashSet<int>();
private Random _rng = new Random();
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:55 am
by PTScalper
2. The Execution Loop (OnTick)
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.
Code: Select all
protected override void OnTick()
{
// --- 1. PROCESS LATENCY QUEUE ---
for (int i = _executionQueue.Count - 1; i >= 0; i--)
{
if (Server.Time >= _executionQueue[i].ExecutionTime)
{
_executionQueue[i].Command.Invoke();
_executionQueue.RemoveAt(i);
}
}
// ... rest of your OnTick logic ...
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:56 am
by PTScalper
3. Queue the Scale-Out Instead of Firing
Replace your direct ClosePosition call with a C# closure (Lambda) that injects the command into the queue.
Code: Select all
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);
}
});
}
}
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:56 am
by PTScalper
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.
1. Add the Ledger Variables
Code: Select all
[Parameter("Worst-Case Slippage (Pips)", DefaultValue = 2.0, Group = "Slippage Simulator")]
public double AdverseSlippagePips { get; set; }
private double _shadowNetProfit = 0;
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:56 am
by PTScalper
2. Track Realized PNL vs. Shadow PNL
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.
Code: Select all
protected override void OnStart()
{
Positions.Closed += OnPositionClosed;
}
private void OnPositionClosed(PositionClosedEventArgs args)
{
if (args.Position.Label == BotLabel && args.Position.SymbolName == SymbolName)
{
// 1. Get the actual profit from the backtest fill
double actualProfit = args.Position.GrossProfit;
// 2. Calculate the financial penalty of the adverse slippage
// Penalty = Volume * PipValue * BadPips
double slippagePenalty = (args.Position.VolumeInUnits / Symbol.VolumeInUnitsMin)
* Symbol.PipValue
* AdverseSlippagePips;
// 3. Update the shadow ledger
double adjustedProfit = actualProfit - slippagePenalty;
_shadowNetProfit += adjustedProfit;
// 4. Render to chart
string ledgerText = $"Actual Backtest Equity: {Account.Equity:C2}\n" +
$"Worst-Case Shadow Equity: {(Account.Balance + _shadowNetProfit):C2}";
Chart.DrawStaticText("ShadowLedger", ledgerText, VerticalAlignment.Top, HorizontalAlignment.Left, Color.Red);
}
}
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:57 am
by PTScalper
The Institutional Testing Workflow
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.
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:58 am
by PTScalper
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.
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:58 am
by PTScalper
1. The Dynamic Spread Parameters
Add these parameters to your C# class. This creates a "corridor" of acceptable execution.
Code: Select all
[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; }
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:58 am
by PTScalper
2. The Time-Sampled State Variables
Create a standard C# Queue<double> to hold your historical data, and timestamps to throttle your data collection and log outputs.
Code: Select all
private Queue<double> _spreadBaselineQueue = new Queue<double>();
private DateTime _lastSpreadSample;
private double _currentBaselineSpread;
private DateTime _lastSpreadLogTime;
Re: Heikin Ashi Candles, Smoothing the Noise
Posted: Sat Oct 03, 2026 5:59 am
by PTScalper
3. Integrating the Filter Engine inside OnTick
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.
Code: Select all
protected override void OnTick()
{
double currentSpread = Symbol.Spread / Symbol.PipSize;
// --- 1. TIME-SAMPLED BASELINE GENERATOR ---
// Sample the spread exactly once per minute to build a stable 1-hour historical baseline
if ((Server.Time - _lastSpreadSample).TotalMinutes >= 1)
{
_spreadBaselineQueue.Enqueue(currentSpread);
if (_spreadBaselineQueue.Count > 60) // Maintain exactly 60 minutes of data
_spreadBaselineQueue.Dequeue();
// LINQ Average is fast enough here since the queue never exceeds 60 items
_currentBaselineSpread = _spreadBaselineQueue.Average();
_lastSpreadSample = Server.Time;
}
// --- 2. CALCULATE DYNAMIC EXECUTION LIMIT ---
// Prevent mathematically impossible tight limits (e.g., 0.1 pip baseline * 3 = 0.3 pip limit)
double safeBaseline = Math.Max(_currentBaselineSpread, MinBaselineSpread);
double dynamicLimit = safeBaseline * SpreadMultiplier;
// Final ceiling is the absolute max spread
double effectiveSpreadLimit = Math.Min(dynamicLimit, AbsoluteMaxSpread);
// ... [Process Latency Queue Here] ...
var position = Positions.Find(BotLabel, SymbolName);
if (position == null) return;
// --- 3. SCALE-OUT TARGET EVALUATION ---
if (_scaleOutTargets.TryGetValue(position.Id, out double targetPrice))
{
bool isTargetHit = position.TradeType == TradeType.Buy
? Symbol.Bid >= targetPrice
: Symbol.Ask <= targetPrice;
if (isTargetHit)
{
// --- THE SPREAD BLOCKER ---
if (currentSpread > effectiveSpreadLimit)
{
// Throttle the logging so you don't spam the cTrader journal 500x a second during rollover
if ((Server.Time - _lastSpreadLogTime).TotalSeconds > 1)
{
Print($"[Liquidity Fracture] Target hit, but spread ({currentSpread:F1}) exceeds limit ({effectiveSpreadLimit:F1}). Holding scale-out.");
_lastSpreadLogTime = Server.Time;
}
// Abort the scale-out for this tick. The target remains in the dictionary.
// It will continually retry on subsequent ticks until the spread compresses.
return;
}
// --- PROCEED TO LATENCY QUEUE OR EXECUTION ---
if (_queuedScaleOutIds.Contains(position.Id)) return;
double scaleOutVolume = Symbol.NormalizeVolumeInUnits(position.VolumeInUnits / 2, RoundingMode.Down);
if (scaleOutVolume >= Symbol.VolumeInUnitsMin)
{
// Add to Latency Queue or execute ClosePosition(position, scaleOutVolume) here
}
}
}
}