Page 2 of 5

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:21 pm
by PTScalper
MT4 (MQL4) Pro Implementation

MT4 handles asynchronous data poorly compared to MT5, and iBarShift can easily return -1 if history is missing. This version implements strict error checking to prevent array crashes.

Code: Select all

//+------------------------------------------------------------------+
//|                                          PRO_HTF_Regime_Bias.mq4 |
//+------------------------------------------------------------------+
#property strict
#property indicator_separate_window
#property indicator_minimum 0
#property indicator_maximum 1
#property indicator_buffers 4

#property indicator_color1 clrTeal       // Bullish
#property indicator_color2 clrMaroon     // Bearish
#property indicator_color3 clrGray       // Chop
#property indicator_color4 clrGoldenrod  // Exhausted

#property indicator_width1 4
#property indicator_width2 4
#property indicator_width3 4
#property indicator_width4 4

extern int    InpHTF           = PERIOD_D1; // Higher Timeframe
extern int    InpMAType        = MODE_EMA;  // MA Type (0=SMA, 1=EMA, 2=SMMA, 3=LWMA)
extern int    InpMAPeriod      = 20;        // MA Period
extern int    InpATRPeriod     = 14;        // ATR Period
extern double InpATRNeutral    = 0.5;       // Chop Zone Multiplier
extern double InpATRExhaustion = 2.5;       // Exhaustion Multiplier

double BullBuffer[], BearBuffer[], ChopBuffer[], ExhaustBuffer[];

int OnInit() {
    SetIndexStyle(0, DRAW_HISTOGRAM); SetIndexBuffer(0, BullBuffer);
    SetIndexStyle(1, DRAW_HISTOGRAM); SetIndexBuffer(1, BearBuffer);
    SetIndexStyle(2, DRAW_HISTOGRAM); SetIndexBuffer(2, ChopBuffer);
    SetIndexStyle(3, DRAW_HISTOGRAM); SetIndexBuffer(3, ExhaustBuffer);
    
    IndicatorShortName("Pro Regime Bias");
    return(INIT_SUCCEEDED);
}

int OnCalculate(const int rates_total, const int prev_calculated, 
                const datetime &time[], const double &open[], 
                const double &high[], const double &low[], 
                const double &close[], const long &tick_volume[], 
                const long &volume[], const int &spread[]) {
                
    // MT4 Data Check: Ensure HTF data exists
    if(iBars(Symbol(), InpHTF) < InpMAPeriod + InpATRPeriod) {
        return 0; // Wait for history download
    }

    int limit = rates_total - prev_calculated;
    if (prev_calculated == 0) limit = rates_total - 1;

    for (int i = limit; i >= 0; i--) {
        int htf_shift = iBarShift(Symbol(), InpHTF, time[i], false);
        
        // Failsafe: if MT4 returns -1 for a missing historical bar, skip
        if(htf_shift < 0) continue; 
        
        int target_shift = htf_shift + 1; 

        double htf_c   = iClose(Symbol(), InpHTF, target_shift);
        double htf_ma  = iMA(Symbol(), InpHTF, InpMAPeriod, 0, InpMAType, PRICE_CLOSE, target_shift);
        double htf_atr = iATR(Symbol(), InpHTF, InpATRPeriod, target_shift);

        // Clear buffers on this index
        BullBuffer[i] = 0; BearBuffer[i] = 0; 
        ChopBuffer[i] = 0; ExhaustBuffer[i] = 0;

        // Failsafe against empty ATR/MA values during early history loading
        if(htf_ma == 0 || htf_atr == 0) continue;

        double upper_chop = htf_ma + (htf_atr * InpATRNeutral);
        double lower_chop = htf_ma - (htf_atr * InpATRNeutral);
        
        double upper_exhaust = htf_ma + (htf_atr * InpATRExhaustion);
        double lower_exhaust = htf_ma - (htf_atr * InpATRExhaustion);

        if (htf_c > upper_exhaust || htf_c < lower_exhaust) {
            ExhaustBuffer[i] = 1.0;
        } else if (htf_c > upper_chop) {
            BullBuffer[i] = 1.0;
        } else if (htf_c < lower_chop) {
            BearBuffer[i] = 1.0;
        } else {
            ChopBuffer[i] = 1.0;
        }
    }
    return(rates_total);
}

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:23 pm
by PTScalper
cTrader's modern C# API handles multi-timeframe architectures far more elegantly than MetaTrader's older C++ structures. The cAlgo.API framework allows us to directly instantiate indicators against a secondary Bars object, eliminating the need to wrangle handles or copy buffers manually.

However, the repainting trap still exists. To solve it, we must map the current execution timeframe's timestamp back to the higher timeframe's index using GetIndexByTime(), and explicitly offset it by -1 to lock the logic to the last fully closed candle.

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:23 pm
by PTScalper
Here is the production-grade cAlgo implementation. It utilizes double.NaN on inactive states so the histogram cleanly plots only the active regime without leaving a messy 0.0 baseline track.

Code: Select all

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

namespace cAlgo
{
    [Indicator(IsOverlay = false, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class ProRegimeBias : Indicator
    {
        [Parameter("Higher Timeframe", DefaultValue = "Daily", Group = "Regime Parameters")]
        public TimeFrame Htf { get; set; }

        [Parameter("MA Period", DefaultValue = 20, MinValue = 1, Group = "Regime Parameters")]
        public int MaPeriod { get; set; }

        [Parameter("ATR Period", DefaultValue = 14, MinValue = 1, Group = "Volatility Bands")]
        public int AtrPeriod { get; set; }

        [Parameter("Chop Multiplier", DefaultValue = 0.5, MinValue = 0.1, Group = "Volatility Bands")]
        public double ChopMultiplier { get; set; }

        [Parameter("Exhaustion Multiplier", DefaultValue = 2.5, MinValue = 1.0, Group = "Volatility Bands")]
        public double ExhaustMultiplier { get; set; }

        // Muted Teal
        [Output("Bullish", LineColor = "#008080", PlotType = PlotType.Histogram, Thickness = 4)]
        public IndicatorDataSeries Bullish { get; set; }

        // Muted Maroon
        [Output("Bearish", LineColor = "#800000", PlotType = PlotType.Histogram, Thickness = 4)]
        public IndicatorDataSeries Bearish { get; set; }

        // Grey
        [Output("Chop", LineColor = "#808080", PlotType = PlotType.Histogram, Thickness = 4)]
        public IndicatorDataSeries Chop { get; set; }

        // Goldenrod
        [Output("Exhausted", LineColor = "#DAA520", PlotType = PlotType.Histogram, Thickness = 4)]
        public IndicatorDataSeries Exhausted { get; set; }

        private Bars _htfBars;
        private ExponentialMovingAverage _htfMa;
        private AverageTrueRange _htfAtr;

        protected override void Initialize()
        {
            // 1. Fetch the higher timeframe data series natively
            _htfBars = MarketData.GetBars(Htf);
            
            // 2. Initialize the HTF indicators by passing the secondary HTF data series
            _htfMa = Indicators.ExponentialMovingAverage(_htfBars.ClosePrices, MaPeriod);
            _htfAtr = Indicators.AverageTrueRange(_htfBars, AtrPeriod, MovingAverageType.Simple);
        }

        public override void Calculate(int index)
        {
            // 3. Map the current execution timeframe's time to the corresponding HTF index
            var htfIndex = _htfBars.OpenTimes.GetIndexByTime(Bars.OpenTimes[index]);

            // 4. Lock to the last fully closed HTF bar to mathematically prevent repainting
            var closedHtfIndex = htfIndex - 1;

            // Failsafe: Ensure enough history exists on the HTF before calculating
            if (closedHtfIndex < Math.Max(MaPeriod, AtrPeriod))
            {
                ResetOutputs(index);
                return;
            }

            // 5. Extract static historical values
            double htfClose = _htfBars.ClosePrices[closedHtfIndex];
            double htfMaValue = _htfMa.Result[closedHtfIndex];
            double htfAtrValue = _htfAtr.Result[closedHtfIndex];

            // Failsafe against uninitialized indicator values at the beginning of the series
            if (double.IsNaN(htfMaValue) || double.IsNaN(htfAtrValue))
            {
                ResetOutputs(index);
                return;
            }

            // 6. Define Volatility Bands
            double upperChop = htfMaValue + (htfAtrValue * ChopMultiplier);
            double lowerChop = htfMaValue - (htfAtrValue * ChopMultiplier);

            double upperExhaust = htfMaValue + (htfAtrValue * ExhaustMultiplier);
            double lowerExhaust = htfMaValue - (htfAtrValue * ExhaustMultiplier);

            // 7. Regime Logic Mapping (Reset first to clear previous ticks)
            ResetOutputs(index);

            if (htfClose > upperExhaust || htfClose < lowerExhaust)
            {
                Exhausted[index] = 1.0; 
            }
            else if (htfClose > upperChop)
            {
                Bullish[index] = 1.0;
            }
            else if (htfClose < lowerChop)
            {
                Bearish[index] = 1.0;
            }
            else
            {
                Chop[index] = 1.0;
            }
        }

        /// <summary>
        /// Assigns double.NaN to all outputs so the histogram correctly renders empty space 
        /// rather than drawing a line at 0.0 during inactive states.
        /// </summary>
        private void ResetOutputs(int index)
        {
            Bullish[index] = double.NaN;
            Bearish[index] = double.NaN;
            Chop[index] = double.NaN;
            Exhausted[index] = double.NaN;
        }
    }
}

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:24 pm
by PTScalper
To elevate this to true enterprise-grade C# architecture, we need to address CPU efficiency and extensible state management.

When you lock your bias to a closed higher-timeframe candle, the result becomes immutable for the duration of the current execution candle. Recalculating the moving average, the ATR, and the volatility bands on every single tick of a 1-minute or 15-minute chart is a massive waste of CPU cycles.

A production-level script short-circuits tick-level execution, uses a strongly typed State Machine (enum) rather than floating-point logic to track the bias, and exposes that state so a cBot can subscribe to it later. It also implements a minimalist HUD (Heads-Up Display) directly on the chart, meaning you can eventually delete the sub-window entirely to reclaim screen real estate.

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:24 pm
by PTScalper
The C# cAlgo Implementation: Pro State Machine

This version implements cycle optimization (calculating only on bar open), an internal state tracker, and a static chart watermark.

Code: Select all

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

namespace cAlgo
{
    // 1. Strongly typed State Machine for clean cBot integration
    public enum RegimeState
    {
        Bullish,
        Bearish,
        Chop,
        Exhausted,
        Initializing
    }

    [Indicator(IsOverlay = false, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class ProRegimeBias : Indicator
    {
        [Parameter("Higher Timeframe", DefaultValue = "Daily", Group = "Regime Parameters")]
        public TimeFrame Htf { get; set; }

        [Parameter("MA Period", DefaultValue = 20, MinValue = 1, Group = "Regime Parameters")]
        public int MaPeriod { get; set; }

        [Parameter("ATR Period", DefaultValue = 14, MinValue = 1, Group = "Volatility Bands")]
        public int AtrPeriod { get; set; }

        [Parameter("Chop Multiplier", DefaultValue = 0.5, MinValue = 0.1, Group = "Volatility Bands")]
        public double ChopMultiplier { get; set; }

        [Parameter("Exhaustion Multiplier", DefaultValue = 2.5, MinValue = 1.0, Group = "Volatility Bands")]
        public double ExhaustMultiplier { get; set; }

        [Output("Bullish", LineColor = "#008080", PlotType = PlotType.Histogram, Thickness = 4)]
        public IndicatorDataSeries BullishLine { get; set; }

        [Output("Bearish", LineColor = "#800000", PlotType = PlotType.Histogram, Thickness = 4)]
        public IndicatorDataSeries BearishLine { get; set; }

        [Output("Chop", LineColor = "#808080", PlotType = PlotType.Histogram, Thickness = 4)]
        public IndicatorDataSeries ChopLine { get; set; }

        [Output("Exhausted", LineColor = "#DAA520", PlotType = PlotType.Histogram, Thickness = 4)]
        public IndicatorDataSeries ExhaustedLine { get; set; }

        private Bars _htfBars;
        private ExponentialMovingAverage _htfMa;
        private AverageTrueRange _htfAtr;
        
        private int _lastCalculatedIndex = -1;
        public RegimeState CurrentRegime { get; private set; } = RegimeState.Initializing;

        protected override void Initialize()
        {
            _htfBars = MarketData.GetBars(Htf);
            _htfMa = Indicators.ExponentialMovingAverage(_htfBars.ClosePrices, MaPeriod);
            _htfAtr = Indicators.AverageTrueRange(_htfBars, AtrPeriod, MovingAverageType.Simple);
        }

        public override void Calculate(int index)
        {
            // 2. Cycle Optimization: 
            // Since the HTF value is locked to the previous closed bar, 
            // it cannot change during the formation of the current execution bar. 
            // We calculate once on bar open and ignore all subsequent ticks.
            if (index == _lastCalculatedIndex) return;
            _lastCalculatedIndex = index;

            var htfIndex = _htfBars.OpenTimes.GetIndexByTime(Bars.OpenTimes[index]);
            var closedHtfIndex = htfIndex - 1;

            if (closedHtfIndex < Math.Max(MaPeriod, AtrPeriod))
            {
                SetRegime(RegimeState.Initializing, index);
                return;
            }

            double htfClose = _htfBars.ClosePrices[closedHtfIndex];
            double htfMaValue = _htfMa.Result[closedHtfIndex];
            double htfAtrValue = _htfAtr.Result[closedHtfIndex];

            if (double.IsNaN(htfMaValue) || double.IsNaN(htfAtrValue))
            {
                SetRegime(RegimeState.Initializing, index);
                return;
            }

            double upperChop = htfMaValue + (htfAtrValue * ChopMultiplier);
            double lowerChop = htfMaValue - (htfAtrValue * ChopMultiplier);
            double upperExhaust = htfMaValue + (htfAtrValue * ExhaustMultiplier);
            double lowerExhaust = htfMaValue - (htfAtrValue * ExhaustMultiplier);

            // 3. Evaluate State
            RegimeState newState;
            if (htfClose > upperExhaust || htfClose < lowerExhaust)
            {
                newState = RegimeState.Exhausted;
            }
            else if (htfClose > upperChop)
            {
                newState = RegimeState.Bullish;
            }
            else if (htfClose < lowerChop)
            {
                newState = RegimeState.Bearish;
            }
            else
            {
                newState = RegimeState.Chop;
            }

            // 4. State Transition & UI Update Logging
            if (newState != CurrentRegime)
            {
                CurrentRegime = newState;
                if (IsRealTime)
                {
                    UpdateChartHUD();
                }
            }

            SetRegime(newState, index);
        }

        private void SetRegime(RegimeState state, int index)
        {
            BullishLine[index] = double.NaN;
            BearishLine[index] = double.NaN;
            ChopLine[index] = double.NaN;
            ExhaustedLine[index] = double.NaN;

            switch (state)
            {
                case RegimeState.Bullish: BullishLine[index] = 1.0; break;
                case RegimeState.Bearish: BearishLine[index] = 1.0; break;
                case RegimeState.Chop: ChopLine[index] = 1.0; break;
                case RegimeState.Exhausted: ExhaustedLine[index] = 1.0; break;
            }
        }

        private void UpdateChartHUD()
        {
            string text = $"HTF BIAS: {CurrentRegime.ToString().ToUpper()}";
            Color textColor;

            switch (CurrentRegime)
            {
                case RegimeState.Bullish: textColor = Color.Teal; break;
                case RegimeState.Bearish: textColor = Color.Maroon; break;
                case RegimeState.Exhausted: textColor = Color.Goldenrod; break;
                default: textColor = Color.Gray; break;
            }

            // Draws a minimalist text watermark on the main chart
            Chart.DrawStaticText("RegimeHUD", text, VerticalAlignment.Bottom, HorizontalAlignment.Right, textColor);
        }
    }
}

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:25 pm
by PTScalper
To integrate the custom indicator into a cBot, the architecture requires an Execution Facade. Instead of scattering ExecuteMarketOrder calls throughout your scalping logic, you route all potential entries through a single validation method.

This creates a hard boundary between your signal generation (e.g., your 1-minute or 15-minute price action triggers) and your execution layer. If the trade direction contradicts the state machine, the facade blocks the execution and logs the specific rejection reason.

The C# cBot Template: Regime Execution Guard

To use this, ensure your ProRegimeBias indicator is compiled in your cTrader workspace. The cBot instantiates the indicator natively and reads its exposed CurrentRegime property.

Code: Select all

using System;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
// Note: Ensure you have referenced the project or namespace containing the ProRegimeBias indicator
// using cAlgo.Indicators; 

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class RegimeExecutionGuard : Robot
    {
        // --- Indicator Parameters Passthrough ---
        [Parameter("Higher Timeframe", DefaultValue = "Daily", Group = "HTF Bias")]
        public TimeFrame Htf { get; set; }

        [Parameter("MA Period", DefaultValue = 20, MinValue = 1, Group = "HTF Bias")]
        public int MaPeriod { get; set; }

        [Parameter("ATR Period", DefaultValue = 14, MinValue = 1, Group = "HTF Bias")]
        public int AtrPeriod { get; set; }

        [Parameter("Chop Multiplier", DefaultValue = 0.5, MinValue = 0.1, Group = "HTF Bias")]
        public double ChopMultiplier { get; set; }

        [Parameter("Exhaustion Multiplier", DefaultValue = 2.5, MinValue = 1.0, Group = "HTF Bias")]
        public double ExhaustMultiplier { get; set; }

        [Parameter("Standard Lot Size", DefaultValue = 1, MinValue = 0.01, Group = "Risk")]
        public double LotSize { get; set; }

        // The indicator instance
        private ProRegimeBias _biasIndicator;

        protected override void OnStart()
        {
            // Instantiate the custom indicator via the cAlgo API
            _biasIndicator = Indicators.GetIndicator<ProRegimeBias>(
                Htf, 
                MaPeriod, 
                AtrPeriod, 
                ChopMultiplier, 
                ExhaustMultiplier
            );
        }

        protected override void OnTick()
        {
            // Example: Your execution timeframe logic (e.g., M1 or M15 liquidity sweeps) goes here.
            // When your local price action condition is met, call the facade instead of executing directly.
            
            /* 
            if (BullishPriceActionSetupTriggered)
            {
                TryExecuteTrade(TradeType.Buy, "PriceAction_Scalp_Long");
            }
            if (BearishPriceActionSetupTriggered)
            {
                TryExecuteTrade(TradeType.Sell, "PriceAction_Scalp_Short");
            }
            */
        }

        /// <summary>
        /// The Execution Facade: Evaluates the requested trade against the HTF Regime State.
        /// Rejects and logs the attempt if it violates the bias rules.
        /// </summary>
        private bool TryExecuteTrade(TradeType requestedDirection, string label)
        {
            RegimeState currentState = _biasIndicator.CurrentRegime;
            bool isAllowed = false;
            string rejectionReason = string.Empty;

            // Strict routing logic based on the indicator's state machine
            switch (currentState)
            {
                case RegimeState.Bullish:
                    isAllowed = requestedDirection == TradeType.Buy;
                    rejectionReason = isAllowed ? "" : "Counter-trend: Short attempted in a Bullish HTF regime.";
                    break;
                
                case RegimeState.Bearish:
                    isAllowed = requestedDirection == TradeType.Sell;
                    rejectionReason = isAllowed ? "" : "Counter-trend: Long attempted in a Bearish HTF regime.";
                    break;
                
                case RegimeState.Chop:
                    isAllowed = false;
                    rejectionReason = "Chop Zone: HTF is consolidating. Mandated stand aside.";
                    break;
                
                case RegimeState.Exhausted:
                    isAllowed = false;
                    rejectionReason = "Exhausted: HTF is overextended. High risk of liquidity sweep.";
                    break;
                
                case RegimeState.Initializing:
                    isAllowed = false;
                    rejectionReason = "Initializing: Awaiting asynchronous HTF data synchronization.";
                    break;
            }

            if (!isAllowed)
            {
                // Logs the automated rejection to the cBot journal
                Print($"[ORDER REJECTED] Direction: {requestedDirection} | Reason: {rejectionReason}");
                return false;
            }

            // If validation passes, calculate volume and execute
            double volumeInUnits = Symbol.QuantityToVolumeInUnits(LotSize);
            ExecuteMarketOrderAsync(requestedDirection, SymbolName, volumeInUnits, label);
            
            Print($"[ORDER EXECUTED] Direction: {requestedDirection} | Regime aligned: {currentState}");
            return true;
        }
    }
}

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:27 pm
by PTScalper
Moving rejection logs out of the platform journal and into a structured database or telemetry pipeline transforms them from dead text into actionable data. By analyzing this data post-session, you can mathematically identify which structural setups you are misreading most often before the market bails you out.

Because cTrader operates within a sandboxed .NET environment, connecting to external endpoints (like an MS SQL database or an Azure REST API) requires elevating the cBot's permissions.

You must change the access rights in the bot's header from AccessRights.None to AccessRights.FullAccess.

Here is the enterprise architecture to handle this cleanly without blocking the execution thread.

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:27 pm
by PTScalper
1. The Logger Interface

Instead of hardcoding a specific database connection into the trading logic, define a contract. This allows you to swap between local MS SQL during development and an Azure webhook in production.

Code: Select all

public interface IRejectionLogger
{
    void LogRejectionAsync(string symbol, string direction, string regime, string reason);
}

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:27 pm
by PTScalper
2. Implementation A: Local MS SQL Server

This uses standard ADO.NET (System.Data.SqlClient) to push logs to a local SQL Server. We wrap the execution in Task.Run so the network latency doesn't stall the cBot's OnTick execution loop during high volatility.

Code: Select all

using System;
using System.Data.SqlClient;
using System.Threading.Tasks;

public class SqlRejectionLogger : IRejectionLogger
{
    private readonly string _connectionString;

    public SqlRejectionLogger(string connectionString)
    {
        _connectionString = connectionString;
    }

    public void LogRejectionAsync(string symbol, string direction, string regime, string reason)
    {
        // Fire and forget to keep the tick engine running
        Task.Run(() =>
        {
            try
            {
                using (var connection = new SqlConnection(_connectionString))
                {
                    connection.Open();
                    string query = @"
                        INSERT INTO TradeRejections (Timestamp, Symbol, Direction, Regime, Reason) 
                        VALUES (@time, @sym, @dir, @regime, @reason)";

                    using (var command = new SqlCommand(query, connection))
                    {
                        command.Parameters.AddWithValue("@time", DateTime.UtcNow);
                        command.Parameters.AddWithValue("@sym", symbol);
                        command.Parameters.AddWithValue("@dir", direction);
                        command.Parameters.AddWithValue("@regime", regime);
                        command.Parameters.AddWithValue("@reason", reason);
                        command.ExecuteNonQuery();
                    }
                }
            }
            catch (Exception ex)
            {
                // Fallback to internal cTrader log if the DB is unreachable
                cAlgo.API.Logger.Print($"SQL Logging Failed: {ex.Message}");
            }
        });
    }
}

Re: The one indicator I still trust for bias only

Posted: Fri Sep 18, 2026 7:28 pm
by PTScalper
3. Implementation B: Azure Webhook (REST API)

If you prefer routing the data to an Azure Logic App, Azure Function, or a custom microservice, use HttpClient. This is ideal if you want to trigger real-time alerts or aggregate telemetry in the cloud.

Code: Select all

using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

public class WebhookRejectionLogger : IRejectionLogger
{
    private readonly string _webhookUrl;
    private static readonly HttpClient _httpClient = new HttpClient();

    public WebhookRejectionLogger(string webhookUrl)
    {
        _webhookUrl = webhookUrl;
    }

    public void LogRejectionAsync(string symbol, string direction, string regime, string reason)
    {
        Task.Run(async () =>
        {
            try
            {
                // Construct a lightweight JSON payload manually to avoid heavy dependencies
                string jsonPayload = $@"{{
                    ""timestamp"": ""{DateTime.UtcNow:O}"",
                    ""symbol"": ""{symbol}"",
                    ""direction"": ""{direction}"",
                    ""regime"": ""{regime}"",
                    ""reason"": ""{reason}""
                }}";

                var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
                await _httpClient.PostAsync(_webhookUrl, content);
            }
            catch (Exception ex)
            {
                cAlgo.API.Logger.Print($"Webhook Logging Failed: {ex.Message}");
            }
        });
    }
}