Page 2 of 13

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:24 pm
by PTScalper
TradingView (Pine Script)

TradingView does not use tick data for backtesting; it uses OHLC data and makes optimistic assumptions about intrabar price movement.

Slippage: You must use the slippage argument in the strategy() declaration. Setting strategy("My Algo", slippage=2) penalizes every market order by 2 ticks (points).

Commission & Spread: Combine slippage with a fixed commission rate to create a synthetic barrier that replicates the cost of variable spreads.

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:24 pm
by PTScalper
3. Programmatic Slippage Injection (The Custom Approach)

If your platform (like cTrader or custom Python backtesters) assumes perfect fills, you must write a penalty directly into your trade execution logic during backtesting.

Instead of taking the exact fill price, apply a synthetic Volatility-Adjusted Slippage Penalty.

Calculate ATR: Measure the 14-period Average True Range on the M1 or M5 chart.

Scale the Penalty: Slippage is rarely static; it expands during momentum. Multiply the ATR by a small fraction (e.g., 5%) to represent the expected slippage.

Adjust the Fill Price: When a backtest trade opens, artificially worsen the entry price by this calculated amount. If going Long at 1.10500, and the ATR penalty is 0.4 pips, record the entry as 1.10504.

This programmatic approach ensures that trades executed during calm periods suffer minimal simulated slippage, while trades executed during news spikes or high-momentum breakouts are heavily penalized, mirroring real order-book liquidity vacuums.

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:25 pm
by PTScalper
Because cTrader’s Position.EntryPrice is strictly read-only and market orders fill instantly at the quoted backtest tick, you cannot physically override the fill price of a MarketOrder once it executes.

For C# cAlgo, the most mathematically sound approach is Shadow Accounting. You allow the backtester to execute at the ideal tick price, but you dynamically calculate the volatility-based slippage penalty at the exact millisecond of entry. You track this monetary cost in the background and deduct it from your gross backtest equity at the end of the run.

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:25 pm
by PTScalper
Here is a complete, compile-ready cBot snippet demonstrating this architecture:

Code: Select all

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

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class VolatilitySlippageSimulator : Robot
    {
        [Parameter("ATR Periods", Group = "Slippage Settings", DefaultValue = 14)]
        public int AtrPeriods { get; set; }

        [Parameter("Slippage (% of ATR)", Group = "Slippage Settings", DefaultValue = 5.0, MinValue = 0.1, MaxValue = 100)]
        public double AtrSlippagePercent { get; set; }

        [Parameter("Minimum Slippage (Pips)", Group = "Slippage Settings", DefaultValue = 0.2)]
        public double MinSlippagePips { get; set; }

        private AverageTrueRange _atr;
        private double _totalSlippageCost; // Running tally of lost fiat equity

        protected override void OnStart()
        {
            // Initialize ATR on the 1-Minute timeframe to capture micro-volatility
            _atr = Indicators.AverageTrueRange(MarketData.GetBars(TimeFrame.Minute), AtrPeriods, MovingAverageType.Simple);
            _totalSlippageCost = 0;
        }

        /// <summary>
        /// Calculates the current market slippage penalty in pips based on recent volatility.
        /// </summary>
        private double GetVolatilitySlippageInPips()
        {
            // Get the latest completed ATR value
            double currentAtr = _atr.Result.Last(1); 
            
            // Convert raw ATR price value to standard Pips (handles JPY vs standard pairs automatically)
            double atrInPips = currentAtr / Symbol.PipSize;
            
            // Calculate penalty as a percentage of the ATR
            double calculatedSlippage = atrInPips * (AtrSlippagePercent / 100.0);
            
            // Ensure slippage never drops below the baseline broker minimum
            return Math.Max(calculatedSlippage, MinSlippagePips); 
        }

        /// <summary>
        /// Custom execution wrapper that executes the trade and logs the shadow penalty.
        /// </summary>
        public TradeResult ExecuteSlippageAdjustedOrder(TradeType tradeType, double volumeInUnits, string label, double slPips, double tpPips)
        {
            // 1. Calculate slippage at the exact moment before execution
            double slippagePips = GetVolatilitySlippageInPips();
            
            // 2. Execute the order (fills at cTrader's ideal backtest tick)
            TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, label, slPips, tpPips);

            if (result.IsSuccessful && IsBacktesting)
            {
                // 3. Convert pip penalty to account currency fiat value
                // Symbol.PipValue is the fiat value of 1 pip for 1 unit of volume
                double monetaryPenalty = slippagePips * Symbol.PipValue * volumeInUnits;
                
                // 4. Add to the shadow ledger
                _totalSlippageCost += monetaryPenalty;

                Print($"[Slippage Applied] Position {result.Position.Id} | Penalty: {slippagePips:F1} pips | Fiat Cost: {monetaryPenalty:C2}");
            }

            return result;
        }

        // Example Trigger
        protected override void OnTick()
        {
            // Basic example: Execute once to demonstrate mechanics
            if (Positions.Count == 0 && Bars.ClosePrices.Last(1) > Bars.OpenPrices.Last(1))
            {
                ExecuteSlippageAdjustedOrder(TradeType.Buy, 100000, "VolatilityTest", 10, 20);
            }
        }

        protected override void OnStop()
        {
            if (IsBacktesting)
            {
                Print("=================================================");
                Print("            BACKTEST SLIPPAGE REPORT             ");
                Print("=================================================");
                Print($"Ideal Gross Equity:          {Account.Equity:C2}");
                Print($"Total Slippage Penalty:      -{_totalSlippageCost:C2}");
                Print("-------------------------------------------------");
                Print($"TRUE ADJUSTED EQUITY:        {(Account.Equity - _totalSlippageCost):C2}");
                Print("=================================================");
            }
        }
    }
}

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:26 pm
by PTScalper
Key Architectural Mechanics

TimeFrame Decoupling: The _atr indicator is explicitly hard-coded to TimeFrame.Minute, regardless of the chart the cBot runs on. If you are trading off a 15-minute chart, you do not want your slippage based on a 15-minute ATR (which would be far too massive). Slippage happens at the micro-structural level.

Fiat Conversion Math: The calculation slippagePips * Symbol.PipValue * volumeInUnits translates the pip penalty into precise fiat currency (e.g., USD or EUR), adapting automatically to cross-pairs like GBP/JPY where pip values fluctuate based on exchange rates.

The Minimum Floor: The MinSlippagePips parameter ensures that even in periods of absolute flatline volatility (where ATR approaches zero), the system still penalizes the trade by the minimum latency gap you expect from your live broker.

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:26 pm
by PTScalper
To apply slippage to exits, you must intercept the execution engine at the exact millisecond a trade closes. In cAlgo, Stop Losses are executed as Market Orders once the trigger price is hit, making them highly susceptible to order book vacuums during news events.

You can capture this by subscribing to the Positions.Closed event and checking the PositionCloseReason enumeration. By splitting the shadow ledger into "Entry" and "Exit" costs, you can isolate exactly how much capital is bleeding at trade initiation versus trade liquidation.

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:26 pm
by PTScalper
Here is the updated architecture with the event-driven exit penalty added:

Code: Select all

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

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class VolatilitySlippageSimulator : Robot
    {
        [Parameter("ATR Periods", Group = "Slippage Settings", DefaultValue = 14)]
        public int AtrPeriods { get; set; }

        [Parameter("Slippage (% of ATR)", Group = "Slippage Settings", DefaultValue = 5.0, MinValue = 0.1, MaxValue = 100)]
        public double AtrSlippagePercent { get; set; }

        [Parameter("Minimum Slippage (Pips)", Group = "Slippage Settings", DefaultValue = 0.2)]
        public double MinSlippagePips { get; set; }

        private AverageTrueRange _atr;
        
        // Split ledgers for granular post-test analytics
        private double _entrySlippageCost; 
        private double _exitSlippageCost;

        protected override void OnStart()
        {
            _atr = Indicators.AverageTrueRange(MarketData.GetBars(TimeFrame.Minute), AtrPeriods, MovingAverageType.Simple);
            _entrySlippageCost = 0;
            _exitSlippageCost = 0;

            // Subscribe to the global position closed event
            Positions.Closed += OnPositionClosed;
        }

        private double GetVolatilitySlippageInPips()
        {
            double currentAtr = _atr.Result.Last(1); 
            double atrInPips = currentAtr / Symbol.PipSize;
            double calculatedSlippage = atrInPips * (AtrSlippagePercent / 100.0);
            
            return Math.Max(calculatedSlippage, MinSlippagePips); 
        }

        public TradeResult ExecuteSlippageAdjustedOrder(TradeType tradeType, double volumeInUnits, string label, double slPips, double tpPips)
        {
            double slippagePips = GetVolatilitySlippageInPips();
            TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, label, slPips, tpPips);

            if (result.IsSuccessful && IsBacktesting)
            {
                double monetaryPenalty = slippagePips * Symbol.PipValue * volumeInUnits;
                _entrySlippageCost += monetaryPenalty;

                Print($"[ENTRY Penalty] Pos {result.Position.Id} | Slippage: {slippagePips:F1} pips | Cost: {monetaryPenalty:C2}");
            }

            return result;
        }

        /// <summary>
        /// Event handler that processes all closed positions and penalizes Stop Loss / Stop Out executions.
        /// </summary>
        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            if (!IsBacktesting) return;

            // Take Profits are typically Limit Orders and do not suffer negative slippage.
            // We only penalize Market closures, Stop Losses, and Margin Calls (Stop Outs).
            if (args.Reason == PositionCloseReason.StopLoss || 
                args.Reason == PositionCloseReason.StopOut || 
                args.Reason == PositionCloseReason.Closed) // Covers standard market exits via code
            {
                double slippagePips = GetVolatilitySlippageInPips();
                
                // Fetch the specific symbol of the closed position to ensure correct pip valuation
                Symbol posSymbol = Symbols.GetSymbol(args.Position.SymbolName);
                double monetaryPenalty = slippagePips * posSymbol.PipValue * args.Position.VolumeInUnits;

                _exitSlippageCost += monetaryPenalty;

                Print($"[EXIT Penalty - {args.Reason}] Pos {args.Position.Id} | Slippage: {slippagePips:F1} pips | Cost: {monetaryPenalty:C2}");
            }
        }

        protected override void OnStop()
        {
            if (IsBacktesting)
            {
                double totalPenalty = _entrySlippageCost + _exitSlippageCost;
                
                Print("=================================================");
                Print("      BACKTEST SLIPPAGE & LIQUIDITY REPORT       ");
                Print("=================================================");
                Print($"Ideal Gross Equity:          {Account.Equity:C2}");
                Print($"Entry Execution Drag:       -{_entrySlippageCost:C2}");
                Print($"Exit (SL/Mkt) Drag:         -{_exitSlippageCost:C2}");
                Print("-------------------------------------------------");
                Print($"TRUE ADJUSTED EQUITY:        {(Account.Equity - totalPenalty):C2}");
                Print("=================================================");
            }
        }
    }
}

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:27 pm
by PTScalper
Architectural Details

PositionCloseReason Filtering: The event listener checks exactly how the trade was closed. PositionCloseReason.TakeProfit is deliberately omitted because TPs are executed as Limit Orders, which fill at the requested price or better (positive slippage). Stop Losses (StopLoss) and margin calls (StopOut) are Market Orders and take the liquidity hit.

Manual Market Exits Included: PositionCloseReason.Closed is included. If your algorithmic logic closes a trade early based on a technical signal rather than a hard SL, it executes a Market Order. This also suffers slippage in live trading and must be penalized in the test.

Symbol Safety: Symbols.GetSymbol(args.Position.SymbolName) guarantees that if the bot manages positions across multiple pairs concurrently, the fiat penalty calculation dynamically fetches the correct PipValue for the specific asset being closed, maintaining strict mathematical integrity.

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:27 pm
by PTScalper
To prevent premature stop-outs during liquidity vacuums, you inject a volatility buffer directly into your Stop Loss calculation before sending the order to the broker.

Because cTrader’s ExecuteMarketOrder function processes Stop Loss as an absolute distance in pips (rather than a raw price level), the math is highly efficient. You calculate the expected slippage, optionally multiply it by a safety factor, and simply add it to your baseline Stop Loss distance. This automatically handles the directionality for both Buy and Sell orders.

Re: Spread Filters for Scalping: When Not to Enter

Posted: Thu Oct 08, 2026 1:27 pm
by PTScalper
Here is the code to integrate this into the previous snippet:

1. Add the Risk Parameters

Add these new parameters to the top of your cBot so you can optimize the buffer distance during backtesting:

Code: Select all

[Parameter("Use Dynamic SL Buffer", Group = "Risk Management", DefaultValue = true)]
public bool UseDynamicSl { get; set; }

[Parameter("SL Volatility Multiplier", Group = "Risk Management", DefaultValue = 1.5, MinValue = 0.0)]
public double SlVolatilityMultiplier { get; set; }