Spread Filters for Scalping: When Not to Enter
Re: Spread Filters for Scalping: When Not to Enter
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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("=================================================");
}
}
}
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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("=================================================");
}
}
}
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Spread Filters for Scalping: When Not to Enter
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:
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; }Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.