IC Markets

Always Check the Higher Timeframe Trend First

Master exponential money management, position sizing calculators, strict daily stop-loss limits, and overcoming FOMO on micro-timeframes.
FTtrader
Posts: 309
Joined: Mon Aug 03, 2026 2:43 pm

Re: Always Check the Higher Timeframe Trend First

Post by FTtrader »

Here is the complete translation of the professional strategy into a cTrader cBot (C#).

Since cTrader is a true event-driven platform (unlike TradingView, which evaluates historical arrays on every tick), this architecture handles signal generation natively on bar closes (OnBar) while managing the aggressive ATR trailing stop asynchronously tick-by-tick (OnTick).

Pro HTF Scalper with ATR Trail (cTrader C#)

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    public enum ExitStrategy
    {
        FixedPips,
        ATRTrailingStop
    }

    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class ProHTFScalper : Robot
    {
        // =====================================================================
        // 1. HIGHER TIMEFRAME CONTEXT
        // =====================================================================
        [Parameter("Primary HTF", Group = "1. Higher Timeframe Context", DefaultValue = "Hour")]
        public TimeFrame Htf1TimeFrame { get; set; }

        [Parameter("Secondary HTF", Group = "1. Higher Timeframe Context", DefaultValue = "Hour4")]
        public TimeFrame Htf2TimeFrame { get; set; }

        [Parameter("HTF Trend EMA", Group = "1. Higher Timeframe Context", DefaultValue = 50)]
        public int HtfEmaPeriod { get; set; }

        // =====================================================================
        // 2. SCALP TRIGGERS (LTF)
        // =====================================================================
        [Parameter("Fast EMA Trigger", Group = "2. Scalp Triggers", DefaultValue = 9)]
        public int FastEmaPeriod { get; set; }

        [Parameter("Slow EMA Trigger", Group = "2. Scalp Triggers", DefaultValue = 21)]
        public int SlowEmaPeriod { get; set; }

        // =====================================================================
        // 3. RISK MANAGEMENT & EXITS
        // =====================================================================
        [Parameter("Volume (Lots)", Group = "3. Risk Management", DefaultValue = 0.1)]
        public double VolumeInLots { get; set; }

        [Parameter("Exit Mode", Group = "3. Risk Management", DefaultValue = ExitStrategy.ATRTrailingStop)]
        public ExitStrategy ExitMode { get; set; }

        [Parameter("Fixed Stop Loss (Pips)", Group = "3. Risk Management", DefaultValue = 10)]
        public double FixedSlPips { get; set; }

        [Parameter("Fixed Take Profit (Pips)", Group = "3. Risk Management", DefaultValue = 20)]
        public double FixedTpPips { get; set; }

        [Parameter("ATR Length", Group = "3. Risk Management", DefaultValue = 14)]
        public int AtrLength { get; set; }

        [Parameter("ATR Multiplier", Group = "3. Risk Management", DefaultValue = 2.0)]
        public double AtrMultiplier { get; set; }

        // =====================================================================
        // 4. TRADING WINDOW
        // =====================================================================
        [Parameter("Enable Time Filter", Group = "4. Trading Window", DefaultValue = true)]
        public bool UseSession { get; set; }

        [Parameter("Start Hour (Server Time)", Group = "4. Trading Window", DefaultValue = 8)]
        public int SessionStart { get; set; }

        [Parameter("End Hour (Server Time)", Group = "4. Trading Window", DefaultValue = 17)]
        public int SessionEnd { get; set; }

        // =====================================================================
        // 5. DISPLAY
        // =====================================================================
        [Parameter("Show On-Chart HUD", Group = "5. Display", DefaultValue = true)]
        public bool ShowHud { get; set; }

        // --- Core Objects ---
        private Bars _htf1Bars, _htf2Bars;
        private ExponentialMovingAverage _htf1Ema, _htf2Ema;
        private ExponentialMovingAverage _fastEma, _slowEma;
        private AverageTrueRange _atr;
        private const string Label = "ProHTFScalper";

        protected override void OnStart()
        {
            // Initialize Multi-Timeframe Data
            _htf1Bars = MarketData.GetBars(Htf1TimeFrame);
            _htf2Bars = MarketData.GetBars(Htf2TimeFrame);

            _htf1Ema = Indicators.ExponentialMovingAverage(_htf1Bars.ClosePrices, HtfEmaPeriod);
            _htf2Ema = Indicators.ExponentialMovingAverage(_htf2Bars.ClosePrices, HtfEmaPeriod);

            // Initialize Current Timeframe Data (LTF)
            _fastEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, FastEmaPeriod);
            _slowEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, SlowEmaPeriod);
            _atr = Indicators.AverageTrueRange(AtrLength, MovingAverageType.Simple);
        }

        protected override void OnBar()
        {
            ManageSession();
            UpdateHud();

            if (UseSession && !InSession()) return;
            if (Positions.FindAll(Label, SymbolName).Length > 0) return; // Wait until flat

            // 1. Check HTF Structure (Using shift 1 for strictly closed candles)
            bool isHtf1Bull = _htf1Bars.ClosePrices.Last(1) > _htf1Ema.Result.Last(1);
            bool isHtf2Bull = _htf2Bars.ClosePrices.Last(1) > _htf2Ema.Result.Last(1);
            
            bool isHtf1Bear = _htf1Bars.ClosePrices.Last(1) < _htf1Ema.Result.Last(1);
            bool isHtf2Bear = _htf2Bars.ClosePrices.Last(1) < _htf2Ema.Result.Last(1);

            bool htfUptrend = isHtf1Bull && isHtf2Bull;
            bool htfDowntrend = isHtf1Bear && isHtf2Bear;

            // 2. LTF Trigger conditions (Crossover on closed bar)
            bool buyTrigger = _fastEma.Result.Last(1) > _slowEma.Result.Last(1) && _fastEma.Result.Last(2) <= _slowEma.Result.Last(2);
            bool sellTrigger = _fastEma.Result.Last(1) < _slowEma.Result.Last(1) && _fastEma.Result.Last(2) >= _slowEma.Result.Last(2);

            double volume = Symbol.QuantityToVolumeInUnits(VolumeInLots);
            double currentAtr = _atr.Result.Last(1);

            // 3. Execution
            if (htfUptrend && buyTrigger)
            {
                if (ExitMode == ExitStrategy.FixedPips)
                {
                    ExecuteMarketOrder(TradeType.Buy, SymbolName, volume, Label, FixedSlPips, FixedTpPips);
                }
                else // ATR Trail starting stop
                {
                    double initialSl = (currentAtr * AtrMultiplier) / Symbol.PipSize;
                    ExecuteMarketOrder(TradeType.Buy, SymbolName, volume, Label, initialSl, null);
                }
            }
            else if (htfDowntrend && sellTrigger)
            {
                if (ExitMode == ExitStrategy.FixedPips)
                {
                    ExecuteMarketOrder(TradeType.Sell, SymbolName, volume, Label, FixedSlPips, FixedTpPips);
                }
                else
                {
                    double initialSl = (currentAtr * AtrMultiplier) / Symbol.PipSize;
                    ExecuteMarketOrder(TradeType.Sell, SymbolName, volume, Label, initialSl, null);
                }
            }
        }

        protected override void OnTick()
        {
            // Dynamically trail the stop loss with tick precision based on the live ATR
            if (ExitMode == ExitStrategy.ATRTrailingStop)
            {
                var positions = Positions.FindAll(Label, SymbolName);
                if (positions.Length == 0) return;

                double atrValue = _atr.Result.LastValue;

                foreach (var pos in positions)
                {
                    if (pos.TradeType == TradeType.Buy)
                    {
                        double newSl = Symbol.Bid - (atrValue * AtrMultiplier);
                        if (!pos.StopLoss.HasValue || newSl > pos.StopLoss.Value)
                        {
                            ModifyPositionAsync(pos, newSl, pos.TakeProfit);
                        }
                    }
                    else if (pos.TradeType == TradeType.Sell)
                    {
                        double newSl = Symbol.Ask + (atrValue * AtrMultiplier);
                        if (!pos.StopLoss.HasValue || newSl < pos.StopLoss.Value)
                        {
                            ModifyPositionAsync(pos, newSl, pos.TakeProfit);
                        }
                    }
                }
            }
        }

        // --- Utility Methods ---
        private bool InSession()
        {
            var hour = Server.Time.Hour;
            return hour >= SessionStart && hour < SessionEnd;
        }

        private void ManageSession()
        {
            if (UseSession && !InSession())
            {
                var openPositions = Positions.FindAll(Label, SymbolName);
                foreach (var pos in openPositions)
                {
                    ClosePositionAsync(pos);
                }
            }
        }

        private void UpdateHud()
        {
            if (!ShowHud) return;

            bool isHtf1Bull = _htf1Bars.ClosePrices.Last(1) > _htf1Ema.Result.Last(1);
            bool isHtf2Bull = _htf2Bars.ClosePrices.Last(1) > _htf2Ema.Result.Last(1);
            bool isHtf1Bear = _htf1Bars.ClosePrices.Last(1) < _htf1Ema.Result.Last(1);
            bool isHtf2Bear = _htf2Bars.ClosePrices.Last(1) < _htf2Ema.Result.Last(1);

            string htf1Status = isHtf1Bull ? "BULL" : (isHtf1Bear ? "BEAR" : "CHOP");
            string htf2Status = isHtf2Bull ? "BULL" : (isHtf2Bear ? "BEAR" : "CHOP");
            
            bool isUptrend = isHtf1Bull && isHtf2Bull;
            bool isDowntrend = isHtf1Bear && isHtf2Bear;
            string masterStatus = isUptrend ? "LONG ONLY" : (isDowntrend ? "SHORT ONLY" : "NO TRADE");

            string hudText = $"HTF FILTER\n------------------\n" +
                             $"{Htf1TimeFrame}: {htf1Status}\n" +
                             $"{Htf2TimeFrame}: {htf2Status}\n\n" +
                             $"MASTER: {masterStatus}";

            Chart.DrawText("HudPanel", hudText, Server.Time, Chart.TopY, Color.LightGray);
        }
    }
}
FTtrader
Posts: 309
Joined: Mon Aug 03, 2026 2:43 pm

Re: Always Check the Higher Timeframe Trend First

Post by FTtrader »

Architectural Notes for cTrader:

Asynchronous Modification: Notice the use of ModifyPositionAsync() and ClosePositionAsync() in the trailing stop and session management blocks. This ensures that rapid ticks during high-volatility sessions (like news events) do not bottleneck the local thread while the cBot waits for the server to confirm order modifications.

Tick-Level Trail Ratcheting (OnTick): In Pine Script, evaluating intra-bar trailing stops relies on the simulated backtester engine. In C#, we place the _atr.Result.LastValue evaluation and the math check (newSl > pos.StopLoss.Value for longs) directly inside OnTick(). It compares real-time Bid/Ask streams against the live ATR dynamically, ensuring true Chandelier-style ratcheting.

Multi-Timeframe Handling: The objects _htf1Bars and _htf2Bars run entirely independently from your chart's timeframe. .Last(1) guarantees it only references completely closed H1 and H4 candles, effectively eliminating any risk of repainting backtest results in cTrader Automate.
FTtrader
Posts: 309
Joined: Mon Aug 03, 2026 2:43 pm

Re: Always Check the Higher Timeframe Trend First

Post by FTtrader »

And last version is for Meta Trader 5 traders in MQL5.

Here is the translation into MetaTrader 5 (MQL5).

Because MT5 operates on a fundamentally different, event-driven architecture than MT4, this institutional-grade setup utilizes the standard <Trade\Trade.mqh> library for execution. Furthermore, multi-timeframe indicator data is no longer pulled natively using shift variables; instead, we must create indicator handles (iMA, iATR) in the OnInit() function and dynamically query their buffers using CopyBuffer().

Pro HTF Scalper with ATR Trail (MQL5)

Code: Select all

//+------------------------------------------------------------------+
//|                                              ProHTFScalper.mq5   |
//|                     H1/H4 Trend Filter with ATR Trailing Stop    |
//+------------------------------------------------------------------+
#property copyright "Custom MT5 Strategy"
#property version   "1.00"

#include <Trade\Trade.mqh>

//--- Enums for Dropdowns
enum ENUM_EXIT_MODE
{
    EXIT_FIXED_POINTS=0,    // Fixed Points
    EXIT_ATR_TRAIL=1        // ATR Trailing Stop
};

//--- Group 1: Higher Timeframe Context
input group "1. Higher Timeframe Context"
input ENUM_TIMEFRAMES InpHtf1TimeFrame = PERIOD_H1;  // Primary HTF
input ENUM_TIMEFRAMES InpHtf2TimeFrame = PERIOD_H4;  // Secondary HTF
input int             InpHtfEmaPeriod  = 50;         // HTF Trend EMA

//--- Group 2: Scalp Triggers (Current Chart)
input group "2. Scalp Triggers (LTF)"
input int             InpFastEmaPeriod = 9;          // Fast EMA Trigger
input int             InpSlowEmaPeriod = 21;         // Slow EMA Trigger

//--- Group 3: Risk Management & Exits
input group "3. Risk Management & Exits"
input double          InpLots          = 0.1;        // Volume (Lots)
input ENUM_EXIT_MODE  InpExitMode      = EXIT_ATR_TRAIL; // Exit Mode
input int             InpFixedSlPoints = 100;        // Fixed Stop Loss (Points)
input int             InpFixedTpPoints = 200;        // Fixed Take Profit (Points)
input int             InpAtrLength     = 14;         // ATR Length
input double          InpAtrMultiplier = 2.0;        // ATR Multiplier
input ulong           InpMagicNumber   = 80808;      // Magic Number

//--- Group 4: Trading Window
input group "4. Trading Window"
input bool            InpUseSession    = true;       // Enable Time Filter
input int             InpSessionStart  = 8;          // Start Hour (Server Time)
input int             InpSessionEnd    = 17;         // End Hour (Server Time)

//--- Group 5: Display
input group "5. Display"
input bool            InpShowHud       = true;       // Show On-Chart HUD

//--- Global Variables & Handles
CTrade         trade;
int            htf1EmaHandle;
int            htf2EmaHandle;
int            fastEmaHandle;
int            slowEmaHandle;
int            atrHandle;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    trade.SetExpertMagicNumber(InpMagicNumber);
    
    // Create Indicator Handles
    htf1EmaHandle = iMA(_Symbol, InpHtf1TimeFrame, InpHtfEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);
    if(htf1EmaHandle == INVALID_HANDLE) return INIT_FAILED;
    
    htf2EmaHandle = iMA(_Symbol, InpHtf2TimeFrame, InpHtfEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);
    if(htf2EmaHandle == INVALID_HANDLE) return INIT_FAILED;
    
    fastEmaHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);
    if(fastEmaHandle == INVALID_HANDLE) return INIT_FAILED;
    
    slowEmaHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);
    if(slowEmaHandle == INVALID_HANDLE) return INIT_FAILED;
    
    atrHandle = iATR(_Symbol, PERIOD_CURRENT, InpAtrLength);
    if(atrHandle == INVALID_HANDLE) return INIT_FAILED;
    
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    IndicatorRelease(htf1EmaHandle);
    IndicatorRelease(htf2EmaHandle);
    IndicatorRelease(fastEmaHandle);
    IndicatorRelease(slowEmaHandle);
    IndicatorRelease(atrHandle);
    
    Comment(""); // Clear HUD
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // 1. Time & Session Management
    MqlDateTime time;
    TimeToStruct(TimeCurrent(), time);
    int hour = time.hour;
    
    bool inSession = (!InpUseSession || (hour >= InpSessionStart && hour < InpSessionEnd));

    if(InpUseSession && !inSession)
    {
        // Flatten positions if session ends
        for(int i = PositionsTotal() - 1; i >= 0; i--)
        {
            ulong ticket = PositionGetTicket(i);
            if(PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber)
            {
                 trade.PositionClose(ticket);
            }
        }
        UpdateHUD(false, false, false, false, "OUT OF SESSION");
        return; 
    }

    // 2. Continuous Trailing Stop Execution
    TrailATR();

    // 3. Higher Timeframe Context (Check shift 1)
    bool htf1Bull = IsHtfTrend(InpHtf1TimeFrame, htf1EmaHandle, true);
    bool htf2Bull = IsHtfTrend(InpHtf2TimeFrame, htf2EmaHandle, true);
    bool htf1Bear = IsHtfTrend(InpHtf1TimeFrame, htf1EmaHandle, false);
    bool htf2Bear = IsHtfTrend(InpHtf2TimeFrame, htf2EmaHandle, false);

    bool isUptrend = htf1Bull && htf2Bull;
    bool isDowntrend = htf1Bear && htf2Bear;
    
    string masterStatus = isUptrend ? "LONG ONLY" : (isDowntrend ? "SHORT ONLY" : "NO TRADE");
    UpdateHUD(htf1Bull, htf1Bear, htf2Bull, htf2Bear, masterStatus);

    // 4. Execution State Checks
    int myPositions = 0;
    for(int i = 0; i < PositionsTotal(); i++)
    {
        ulong ticket = PositionGetTicket(i);
        if(PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber)
        {
            myPositions++;
        }
    }
    if(myPositions > 0) return; // Wait until flat before looking for new entries

    // 5. LTF Triggers & Order Execution
    bool buyTrigger = CheckCrossover(true);
    bool sellTrigger = CheckCrossover(false);

    if(isUptrend && buyTrigger)
    {
        double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double sl = 0.0, tp = 0.0;
        
        if(InpExitMode == EXIT_FIXED_POINTS)
        {
            sl = NormalizeDouble(ask - (InpFixedSlPoints * _Point), _Digits);
            tp = NormalizeDouble(ask + (InpFixedTpPoints * _Point), _Digits);
        }
        else 
        {
            double atr[];
            if(CopyBuffer(atrHandle, 0, 1, 1, atr) > 0)
                sl = NormalizeDouble(ask - (atr[0] * InpAtrMultiplier), _Digits);
        }
        
        trade.Buy(InpLots, _Symbol, ask, sl, tp, "ProHTFScalper");
    }
    else if(isDowntrend && sellTrigger)
    {
        double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        double sl = 0.0, tp = 0.0;
        
        if(InpExitMode == EXIT_FIXED_POINTS)
        {
            sl = NormalizeDouble(bid + (InpFixedSlPoints * _Point), _Digits);
            tp = NormalizeDouble(bid - (InpFixedTpPoints * _Point), _Digits);
        }
        else 
        {
            double atr[];
            if(CopyBuffer(atrHandle, 0, 1, 1, atr) > 0)
                sl = NormalizeDouble(bid + (atr[0] * InpAtrMultiplier), _Digits);
        }
        
        trade.Sell(InpLots, _Symbol, bid, sl, tp, "ProHTFScalper");
    }
}

//+------------------------------------------------------------------+
//| Core Engine Methods                                              |
//+------------------------------------------------------------------+
void TrailATR()
{
    if(InpExitMode != EXIT_ATR_TRAIL || PositionsTotal() == 0) return;
    
    double atr[];
    // Copy the shift 1 ATR value (closed bar) to avoid stop repainting intra-bar
    if(CopyBuffer(atrHandle, 0, 1, 1, atr) <= 0) return;
    double atrVal = atr[0];
    
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;

        double currentSL = PositionGetDouble(POSITION_SL);
        double currentTP = PositionGetDouble(POSITION_TP);
        long type = PositionGetInteger(POSITION_TYPE);
        
        if(type == POSITION_TYPE_BUY)
        {
            double newSL = SymbolInfoDouble(_Symbol, SYMBOL_BID) - (atrVal * InpAtrMultiplier);
            newSL = NormalizeDouble(newSL, _Digits);
            
            // Ratchet Up Only (allow a 1-point buffer to avoid server flooding)
            if(currentSL == 0.0 || newSL > currentSL + _Point) 
                trade.PositionModify(ticket, newSL, currentTP);
        }
        else if(type == POSITION_TYPE_SELL)
        {
            double newSL = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + (atrVal * InpAtrMultiplier);
            newSL = NormalizeDouble(newSL, _Digits);
            
            // Ratchet Down Only
            if(currentSL == 0.0 || newSL < currentSL - _Point) 
                trade.PositionModify(ticket, newSL, currentTP);
        }
    }
}

bool IsHtfTrend(ENUM_TIMEFRAMES tf, int handle, bool isBull)
{
    double close[], ema[];
    // Protect against uninitialized MTF charts during initial load
    if(CopyClose(_Symbol, tf, 1, 1, close) <= 0) return false;
    if(CopyBuffer(handle, 0, 1, 1, ema) <= 0) return false;
    
    if(isBull) return close[0] > ema[0];
    return close[0] < ema[0];
}

bool CheckCrossover(bool isBuy)
{
    double fast[2], slow[2];
    if(CopyBuffer(fastEmaHandle, 0, 1, 2, fast) <= 0) return false;
    if(CopyBuffer(slowEmaHandle, 0, 1, 2, slow) <= 0) return false;
    
    // CopyBuffer copies oldest to newest: index 0 is shift 2, index 1 is shift 1
    if(isBuy)
        return (fast[1] > slow[1] && fast[0] <= slow[0]);
    else
        return (fast[1] < slow[1] && fast[0] >= slow[0]);
}

void UpdateHUD(bool htf1Bull, bool htf1Bear, bool htf2Bull, bool htf2Bear, string masterStatus)
{
    if(!InpShowHud) 
    {
        Comment("");
        return;
    }
    
    string htf1Status = htf1Bull ? "BULL" : (htf1Bear ? "BEAR" : "CHOP");
    string htf2Status = htf2Bull ? "BULL" : (htf2Bear ? "BEAR" : "CHOP");
    
    string hud = "======================\n";
    hud += "     PRO HTF SCALPER\n";
    hud += "======================\n";
    hud += "Primary HTF:  " + htf1Status + "\n";
    hud += "Secondary HTF: " + htf2Status + "\n";
    hud += "----------------------\n";
    hud += "MASTER: " + masterStatus + "\n";
    hud += "======================";
    
    Comment(hud);
}
//+------------------------------------------------------------------+
FTtrader
Posts: 309
Joined: Mon Aug 03, 2026 2:43 pm

Re: Always Check the Higher Timeframe Trend First

Post by FTtrader »

Key Differences to Note in MT5:

Asynchronous Loading: When running multi-timeframe EAs in MT5, background charts are generated asynchronously. The IsHtfTrend method accounts for this by checking CopyBuffer outputs. If the terminal hasn't compiled the 4-Hour data block yet, it will seamlessly skip execution without crashing until the data stream resolves.

CopyBuffer Arrays: In MT5 CopyBuffer, array indexes flow from oldest to newest by default. Thus, when we copy count=2 from start_pos=1 in CheckCrossover, fast[0] represents shift 2 (the older candle), and fast[1] represents shift 1 (the recent closed candle).

On-Chart Display: The UpdateHUD function utilizes the Comment() engine to dynamically render the master trend dashboard at the top-left of the chart.
Post Reply