Advertisement IC Markets

Heikin Ashi Candles, Smoothing the Noise

Optimize MetaTrader 4, MetaTrader 5, cTrader, and TradingView for speed. Discuss Level II Market Depth (DOM), custom hotkeys, and volume indicators.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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();
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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 ...
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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);
            }
        });
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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;
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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);
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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; }
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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;
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post 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
            }
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply