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.
Post Reply
PTScalper
Site Admin
Posts: 7538
Joined: Mon Jul 20, 2026 1:28 pm

Re: Heikin Ashi Candles, Smoothing the Noise

Post by PTScalper »

Architectural Upgrades for .NET (cTrader)

LINQ High-Performance Arrays: Tracking the Volume Profile point of control in MQL4 required messy ArrayResize commands and manual for loop iterations every tick. Here, lines 102–109 use a standard C# Dictionary<double, long>. A single line of LINQ code sorts the dictionary by value and extracts the key with the highest liquidity in milliseconds.

Symbol.QuantityToVolumeInUnits Translation: MetaTrader calculates lots dynamically during order execution. cTrader strictly processes volume in actual underlying units (e.g., 100,000 for 1 standard lot of EURUSD). Line 149 utilizes cTrader's built-in converter, allowing you to pass standard lot inputs in the UI while executing flawlessly on the backend.

Pip Conversion on Execution: A common cTrader trap is that ExecuteMarketOrder expects the Stop Loss distance in pips, while ModifyPosition requires the exact absolute price. Lines 154 and 160 calculate the pip-distance of the HA wick mathematically using Symbol.Ask/Bid so the initial order is protected at broker-level the millisecond it executes, seamlessly switching to absolute prices later during the trailing loop.
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 »

Upgrading an algorithmic trading system from a functional mathematical script to a production-grade institutional robot requires separating the signal generation from the execution layer. A professional C# framework must dynamically manage equity risk, normalize data for the broker API, filter for liquidity vacuums, and handle execution exceptions.

Here is the refactored, enterprise-grade architecture for the cTrader HA/VWAP system.

The 4 Institutional Upgrades

Dynamic Equity Risk Sizing: Hardcoding lot sizes destroys account compounding. The bot now natively calculates volume based on a percentage of current Account Equity and the exact pip distance to the mathematical Stop Loss.

Broker Data Normalization (Symbol.NormalizeVolumeInUnits): A calculated risk volume of 12,345 units will crash the API if the broker requires step increments of 1,000. The engine now rounds mathematically down to the nearest legal broker step to prevent execution rejection.

Spread Filtration: OnBar methods trigger at the exact open of a new candle. During daily rollovers or news events, spreads widen massively. The MaxSpreadPips filter actively blocks the execution if liquidity is fractured, preventing instant slippage drawdowns.

Structured API Result Handling: Blindly firing ExecuteMarketOrder is a major flaw. The execution logic is now wrapped in a handler that intercepts the TradeResult, checks IsSuccessful, and logs specific API error codes (e.g., margin failures, requotes) directly to your cTrader journal.
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 Professional cTrader Automate (C#) Implementation

Code: Select all

using System;
using System.Linq;
using System.Collections.Generic;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class Institutional_HA_VWAP_Pro : Robot
    {
        public enum RiskModeType { FixedLots, RiskPercentage }

        #region [ 1. EXECUTION & RISK PARAMETERS ]
        [Parameter("Position Sizing Mode", DefaultValue = RiskModeType.RiskPercentage, Group = "Risk Management")]
        public RiskModeType RiskMode { get; set; }

        [Parameter("Risk Value (% or Lots)", DefaultValue = 1.0, Group = "Risk Management", MinValue = 0.01)]
        public double RiskValue { get; set; }

        [Parameter("Max Spread (Pips)", DefaultValue = 1.5, Group = "Risk Management")]
        public double MaxSpreadPips { get; set; }

        [Parameter("Max Slippage (Pips)", DefaultValue = 1.0, Group = "Risk Management")]
        public double MaxSlippagePips { get; set; }
        #endregion

        #region [ 2. STRATEGY PARAMETERS ]
        [Parameter("VWAP StDev Multiplier", DefaultValue = 2.0, Group = "Institutional VWAP")]
        public double StDevMult { get; set; }

        [Parameter("Minutes to Block After Open", DefaultValue = 30, Group = "Institutional VWAP")]
        public int SessionDelayMinutes { get; set; }

        [Parameter("Price Bin Size (Pips)", DefaultValue = 1.0, Group = "Volume Profile (HVN)")]
        public double HvnBinSizePips { get; set; }

        [Parameter("HVN Block Zone (Pips)", DefaultValue = 5.0, Group = "Volume Profile (HVN)")]
        public double HvnBlockZonePips { get; set; }

        [Parameter("Stop Buffer (Pips)", DefaultValue = 2.0, Group = "Trade Management")]
        public double StopBufferPips { get; set; }
        #endregion

        // --- INTERNAL STATE ---
        private const string BotLabel = "HA_VWAP_PRO";
        
        private double _prevHaOpen;
        private double _prevHaClose;

        private double _cumVol;
        private double _cumVolPrice;
        private double _cumVolPrice2;
        private DateTime _sessionStartTime;

        private Dictionary<double, long> _volumeProfile = new Dictionary<double, long>();
        private double _sessionPOC;

        protected override void OnBar()
        {
            var closedBar = Bars.Last(1);
            var previousClosedBar = Bars.Last(2);

            // --- SESSION TRACKING ---
            if (closedBar.OpenTime.Date != previousClosedBar.OpenTime.Date || _sessionStartTime == default)
            {
                _cumVol = 0;
                _cumVolPrice = 0;
                _cumVolPrice2 = 0;
                _sessionStartTime = closedBar.OpenTime;
                _volumeProfile.Clear();
                _sessionPOC = 0;

                _prevHaOpen = previousClosedBar.Open;
                _prevHaClose = previousClosedBar.Close;
            }

            // --- HEIKIN ASHI ENGINE ---
            double haClose = (closedBar.Open + closedBar.High + closedBar.Low + closedBar.Close) / 4.0;
            double haOpen = _prevHaOpen == 0 ? (closedBar.Open + closedBar.Close) / 2.0 : (_prevHaOpen + _prevHaClose) / 2.0;
            
            double haHigh = Math.Max(closedBar.High, Math.Max(haOpen, haClose));
            double haLow = Math.Min(closedBar.Low, Math.Min(haOpen, haClose));

            bool isHAUp = haClose > haOpen;
            bool noLowerWick = isHAUp && Math.Round(haOpen - haLow, Symbol.Digits) == 0;

            bool isHADown = haClose < haOpen;
            bool noUpperWick = isHADown && Math.Round(haHigh - haOpen, Symbol.Digits) == 0;

            _prevHaOpen = haOpen;
            _prevHaClose = haClose;

            // --- VWAP ENGINE ---
            long tickVol = (long)closedBar.TickVolume;
            _cumVol += tickVol;
            _cumVolPrice += closedBar.Close * tickVol;
            _cumVolPrice2 += tickVol * Math.Pow(closedBar.Close, 2);

            double vwapBase = _cumVolPrice / _cumVol;
            double variance = Math.Max(0, (_cumVolPrice2 / _cumVol) - Math.Pow(vwapBase, 2));
            double vwapStDev = Math.Sqrt(variance);

            double upperVwapBand = vwapBase + (vwapStDev * StDevMult);
            double lowerVwapBand = vwapBase - (vwapStDev * StDevMult);

            // --- VOLUME PROFILE (POC) ENGINE ---
            double binRounding = HvnBinSizePips * Symbol.PipSize;
            double currentBinPrice = Math.Round(closedBar.Close / binRounding) * binRounding;
            currentBinPrice = Math.Round(currentBinPrice, Symbol.Digits);

            if (_volumeProfile.ContainsKey(currentBinPrice))
                _volumeProfile[currentBinPrice] += tickVol;
            else
                _volumeProfile[currentBinPrice] = tickVol;

            _sessionPOC = _volumeProfile.OrderByDescending(kvp => kvp.Value).First().Key;

            // --- SIGNAL FILTERS ---
            bool timeFilterPass = (closedBar.OpenTime - _sessionStartTime).TotalMinutes >= SessionDelayMinutes;

            double hvnBufferPrice = HvnBlockZonePips * Symbol.PipSize;
            bool blockLongHVN = (closedBar.Close < _sessionPOC) && (closedBar.Close >= _sessionPOC - hvnBufferPrice);
            bool blockShortHVN = (closedBar.Close > _sessionPOC) && (closedBar.Close <= _sessionPOC + hvnBufferPrice);

            bool validLongSignal = noLowerWick && (closedBar.Close < upperVwapBand) && !blockLongHVN && timeFilterPass;
            bool validShortSignal = noUpperWick && (closedBar.Close > lowerVwapBand) && !blockShortHVN && timeFilterPass;

            // --- STATE ROUTING ---
            ManageTrailingStops(noLowerWick, noUpperWick, closedBar);

            if (Positions.Find(BotLabel, SymbolName) == null)
            {
                if (validLongSignal)
                    TryExecuteTrade(TradeType.Buy, closedBar.Low);
                else if (validShortSignal)
                    TryExecuteTrade(TradeType.Sell, closedBar.High);
            }
        }

        #region [ EXECUTION & MANAGEMENT METHODS ]
        
        private void TryExecuteTrade(TradeType tradeType, double signalExtremum)
        {
            // 1. Spread Check
            double currentSpread = Symbol.Spread / Symbol.PipSize;
            if (currentSpread > MaxSpreadPips)
            {
                Print($"[Filter] Spread ({currentSpread:F1} pips) exceeds maximum ({MaxSpreadPips:F1}). Execution blocked.");
                return;
            }

            // 2. Stop Loss Calculation
            double slBufferPrice = StopBufferPips * Symbol.PipSize;
            double initialSL_Price = tradeType == TradeType.Buy 
                ? Math.Round(signalExtremum - slBufferPrice, Symbol.Digits) 
                : Math.Round(signalExtremum + slBufferPrice, Symbol.Digits);

            double slPips = tradeType == TradeType.Buy 
                ? Math.Round((Symbol.Ask - initialSL_Price) / Symbol.PipSize, 1) 
                : Math.Round((initialSL_Price - Symbol.Bid) / Symbol.PipSize, 1);

            if (slPips <= 0) return;

            // 3. Dynamic Normalization
            double volumeInUnits = CalculateVolume(slPips);
            if (volumeInUnits < Symbol.VolumeInUnitsMin)
            {
                Print($"[Filter] Calculated volume ({volumeInUnits}) is below broker minimum ({Symbol.VolumeInUnitsMin}).");
                return;
            }

            // 4. API Execution
            Print($"[Executing] {tradeType} | Vol: {volumeInUnits} | SL: {slPips} pips");
            TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, BotLabel, slPips, null, MaxSlippagePips);

            if (!result.IsSuccessful)
            {
                Print($"[API Error] Trade failed. Reason: {result.Error}");
            }
        }

        private double CalculateVolume(double slPips)
        {
            if (RiskMode == RiskModeType.FixedLots)
            {
                return Symbol.NormalizeVolumeInUnits(Symbol.QuantityToVolumeInUnits(RiskValue), RoundingMode.Down);
            }
            else
            {
                double riskAmount = Account.Equity * (RiskValue / 100.0);
                double riskPerUnit = slPips * Symbol.PipValue;
                double rawVolume = riskAmount / riskPerUnit;
                
                // Normalizes down to the nearest valid lot step to prevent broker API rejection
                return Symbol.NormalizeVolumeInUnits(rawVolume, RoundingMode.Down);
            }
        }

        private void ManageTrailingStops(bool noLowerWick, bool noUpperWick, Bar closedBar)
        {
            var position = Positions.Find(BotLabel, SymbolName);
            if (position == null) return;

            double slBufferPrice = StopBufferPips * Symbol.PipSize;

            if (position.TradeType == TradeType.Buy && noLowerWick)
            {
                double newStop = Math.Round(closedBar.Low - slBufferPrice, Symbol.Digits);
                if (position.StopLoss == null || newStop > position.StopLoss)
                {
                    ModifyPosition(position, newStop, position.TakeProfit);
                }
            }
            else if (position.TradeType == TradeType.Sell && noUpperWick)
            {
                double newStop = Math.Round(closedBar.High + slBufferPrice, Symbol.Digits);
                if (position.StopLoss == null || newStop < position.StopLoss)
                {
                    ModifyPosition(position, newStop, position.TakeProfit);
                }
            }
        }
        
        #endregion
    }
}
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 »

To implement a partial scale-out based on a Risk-to-Reward (RR) multiple, you need to solve a specific state-management problem: once your trailing stop moves, the original initial risk is lost.

To handle this cleanly, you must calculate the exact target price at the moment of entry, store it in memory mapped to the Position ID, and check it continuously.

Here is the exact code required to upgrade your institutional framework.
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. Add the RR Parameter & State Dictionary

At the top of your class, add the input for the RR target and a dictionary to track active targets. The dictionary maps the Position.Id to the calculated TargetPrice.

Code: Select all

[Parameter("Scale Out at RR", DefaultValue = 2.0, Group = "Trade Management")]
public double TargetRR { get; set; }

// Tracks Position IDs and their precise scale-out price targets
private Dictionary<int, double> _scaleOutTargets = new Dictionary<int, double>();
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. Map the Target Upon Execution

Update your TryExecuteTrade method. When ExecuteMarketOrder returns a successful TradeResult, mathematically project the target price based on the initial stop loss distance, and save it to the dictionary.

Code: Select all

// Inside TryExecuteTrade(), after ExecuteMarketOrder...
TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, BotLabel, slPips, null, MaxSlippagePips);

if (result.IsSuccessful)
{
    // Calculate exact target based on entry price and initial pip risk
    double targetPrice = tradeType == TradeType.Buy 
        ? result.Position.EntryPrice + (slPips * Symbol.PipSize * TargetRR)
        : result.Position.EntryPrice - (slPips * Symbol.PipSize * TargetRR);

    _scaleOutTargets[result.Position.Id] = Math.Round(targetPrice, Symbol.Digits);
    Print($"[Scale Out Target Set] {targetPrice}");
}
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. The Upgraded ManageTrailingStops Method

This updated method evaluates both the HA trailing stop logic on the closed bar, and the exact RR target for the scale-out.

Code: Select all

private void ManageTrailingStops(bool noLowerWick, bool noUpperWick, Bar closedBar)
{
    var position = Positions.Find(BotLabel, SymbolName);
    if (position == null) return;

    // --- 1. PARTIAL SCALE OUT LOGIC ---
    if (_scaleOutTargets.TryGetValue(position.Id, out double targetPrice))
    {
        // Check if the current market price has crossed the RR target
        bool isTargetHit = position.TradeType == TradeType.Buy 
            ? Symbol.Bid >= targetPrice 
            : Symbol.Ask <= targetPrice;

        if (isTargetHit)
        {
            // Divide volume by 2 and normalize down to nearest broker-legal lot step
            double scaleOutVolume = Symbol.NormalizeVolumeInUnits(position.VolumeInUnits / 2, RoundingMode.Down);

            if (scaleOutVolume >= Symbol.VolumeInUnitsMin)
            {
                Print($"[RR Target Hit] Scaling out {scaleOutVolume} units at {targetPrice}");
                TradeResult closeResult = ClosePosition(position, scaleOutVolume);

                if (closeResult.IsSuccessful)
                {
                    // Remove from dictionary so it only scales out once
                    _scaleOutTargets.Remove(position.Id);
                }
            }
            else
            {
                // Edge case: Position is already too small to divide further
                _scaleOutTargets.Remove(position.Id);
            }
        }
    }

    // --- 2. HEIKIN ASHI TRAILING STOP LOGIC ---
    double slBufferPrice = StopBufferPips * Symbol.PipSize;

    if (position.TradeType == TradeType.Buy && noLowerWick)
    {
        double newStop = Math.Round(closedBar.Low - slBufferPrice, Symbol.Digits);
        if (position.StopLoss == null || newStop > position.StopLoss)
        {
            ModifyPosition(position, newStop, position.TakeProfit);
        }
    }
    else if (position.TradeType == TradeType.Sell && noUpperWick)
    {
        double newStop = Math.Round(closedBar.High + slBufferPrice, Symbol.Digits);
        if (position.StopLoss == null || newStop < position.StopLoss)
        {
            ModifyPosition(position, newStop, position.TakeProfit);
        }
    }
}
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 »

Architectural Guardrails

Intrabar vs. Bar Close Execution: The HA trailing stop evaluates exclusively on the close of the bar (closedBar.Low/High) to prevent repainting. However, RR targets operate best as limit orders. If you place this ManageTrailingStops logic entirely inside OnBar(), the scale-out will only trigger if the target is met at the moment the bar closes. For institutional precision, extract the "Partial Scale Out Logic" block from this method and place it inside OnTick(). This ensures the 50% partial is triggered the exact millisecond Symbol.Bid/Ask touches your target, even during a mid-candle volatility spike.

Dictionary Memory Leaks: If a trade is stopped out by the broker before hitting the RR target, its ID remains in _scaleOutTargets. To prevent this from leaking memory over thousands of backtest trades, subscribe to the Positions.Closed event in your OnStart() method and execute _scaleOutTargets.Remove(args.Position.Id);.

The RoundingMode.Down Requirement: If you are trading 0.03 lots, dividing it by 2 gives 0.015. Most brokers reject sub-micro lots. Symbol.NormalizeVolumeInUnits(..., RoundingMode.Down) forces the engine to scale out 0.01 lots and leave 0.02 running, preventing an API crash.
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 »

To persist in-memory state across VPS reboots, power failures, or broker terminal updates, you must serialize the dictionary to the local file system. In modern .NET environments, utilizing System.Text.Json to write a lightweight state file is the most robust, enterprise-standard approach.

Here is the architectural implementation to seamlessly save and load your scale-out targets.

1. Add I/O and JSON Namespaces
At the top of your file, include the standard .NET namespaces required for file operations and serialization.

Code: Select all

using System.IO;
using System.Text.Json;
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. Define the State File Path & Serialization Methods
Create a dynamic file path that includes the SymbolName and BotLabel. This prevents cross-contamination if you attach the bot to multiple charts simultaneously (e.g., EURUSD and GBPUSD).

Add these methods to handle reading and writing the JSON file.

Code: Select all

private string _stateFilePath;

private void InitializeStatePersistence()
{
    // Stores the file in the AppData/Roaming folder: e.g., HA_VWAP_PRO_EURUSD_State.json
    string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
    _stateFilePath = Path.Combine(appData, $"{BotLabel}_{SymbolName}_State.json");
}

private void SaveState()
{
    try
    {
        string json = JsonSerializer.Serialize(_scaleOutTargets);
        File.WriteAllText(_stateFilePath, json);
    }
    catch (Exception ex)
    {
        Print($"[Persistence Error] Failed to save state: {ex.Message}");
    }
}

private void LoadState()
{
    if (File.Exists(_stateFilePath))
    {
        try
        {
            string json = File.ReadAllText(_stateFilePath);
            // Rebuild the dictionary. If the file is empty/corrupt, initialize a new one.
            _scaleOutTargets = JsonSerializer.Deserialize<Dictionary<int, double>>(json) 
                               ?? new Dictionary<int, double>();
            
            Print($"[State Restored] Loaded {_scaleOutTargets.Count} active targets from disk.");
        }
        catch (Exception ex)
        {
            Print($"[Persistence Error] Failed to load state: {ex.Message}");
            _scaleOutTargets = new Dictionary<int, double>();
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply