Page 3 of 13
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:27 pm
by PTScalper
2. Update the Execution Wrapper
Modify your ExecuteSlippageAdjustedOrder method to intercept the base Stop Loss, calculate the buffer, and execute with the newly widened parameters.
Code: Select all
/// <summary>
/// Custom execution wrapper that dynamically widens the SL based on volatility and logs the shadow penalty.
/// </summary>
public TradeResult ExecuteSlippageAdjustedOrder(TradeType tradeType, double volumeInUnits, string label, double baseSlPips, double tpPips)
{
// 1. Calculate the current market slippage expectation in pips
double slippagePips = GetVolatilitySlippageInPips();
// 2. Calculate the final Stop Loss distance
double finalSlPips = baseSlPips;
if (UseDynamicSl)
{
// Multiply the expected slippage by your safety factor
double slBuffer = slippagePips * SlVolatilityMultiplier;
// Widen the stop loss distance
finalSlPips += slBuffer;
Print($"[Risk Manager] Base SL: {baseSlPips} pips | ATR Buffer: +{slBuffer:F1} pips | Final SL: {finalSlPips:F1} pips");
}
// 3. Execute the order using the widened SL
TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, label, finalSlPips, tpPips);
if (result.IsSuccessful && IsBacktesting)
{
// 4. Log the monetary entry penalty for the shadow ledger
double monetaryPenalty = slippagePips * Symbol.PipValue * volumeInUnits;
_entrySlippageCost += monetaryPenalty;
Print($"[ENTRY Penalty] Pos {result.Position.Id} | Executed Slippage: {slippagePips:F1} pips | Fiat Cost: {monetaryPenalty:C2}");
}
return result;
}
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:28 pm
by PTScalper
The Architectural Trade-Off: Position Sizing
While widening your Stop Loss protects you from spread spikes, it introduces a critical variable into your risk management: variable capital exposure.
If your base Stop Loss is 10 pips, trading 1 standard lot (100,000 units) risks roughly $100. If an ATR spike widens your dynamic Stop Loss to 14 pips, executing that same 1 standard lot now risks $140. If you do not account for this, your bot will take disproportionately large losses during high-volatility market regimes.
To maintain a flat risk profile (e.g., risking exactly 1% of account equity per trade), you must mathematically reduce your volumeInUnits in real-time to offset the widened finalSlPips before triggering ExecuteMarketOrder.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:29 pm
by PTScalper
An ATR-based trailing stop acts as a dynamic "ratchet." Instead of trailing price by a fixed number of pips—which often leads to premature stop-outs during volatile spikes—it trails price by a multiple of the current Average True Range. When volatility expands, the stop automatically gives the trade more breathing room. When volatility contracts, the stop tightens.
The most critical engineering rule for algorithmic trailing stops is the Modification Threshold. You must only send a modification request to the broker server if the new Stop Loss improves the current one by a meaningful minimum distance (e.g., 1 full pip). Without this, the bot will hammer the broker's server with modification requests on every micro-tick, leading to API throttling or account suspension.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:29 pm
by PTScalper
Here is a complete C# cAlgo implementation designed for safe, asynchronous execution on live servers.
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 AtrTrailingStop : Robot
{
[Parameter("Trailing Label", Group = "Position Filters", DefaultValue = "MyAlgo")]
public string TargetLabel { get; set; }
[Parameter("ATR Timeframe", Group = "ATR Settings")]
public TimeFrame AtrTimeFrame { get; set; }
[Parameter("ATR Periods", Group = "ATR Settings", DefaultValue = 14)]
public int AtrPeriods { get; set; }
[Parameter("ATR Multiplier", Group = "ATR Settings", DefaultValue = 2.0, MinValue = 0.1)]
public double AtrMultiplier { get; set; }
[Parameter("Min Update Step (Pips)", Group = "Server Safety", DefaultValue = 1.0)]
public double MinUpdatePips { get; set; }
private AverageTrueRange _atr;
private double _minUpdateDistance;
protected override void OnStart()
{
// Initialize ATR on the explicitly selected timeframe, regardless of the chart the bot is attached to
_atr = Indicators.AverageTrueRange(MarketData.GetBars(AtrTimeFrame), AtrPeriods, MovingAverageType.Simple);
// Convert the pip setting to raw price value for fast mathematical comparison in OnTick
_minUpdateDistance = MinUpdatePips * Symbol.PipSize;
}
protected override void OnTick()
{
// Loop only through open positions that match this specific bot's label and symbol
var openPositions = Positions.FindAll(TargetLabel, SymbolName);
foreach (var position in openPositions)
{
ManageAtrTrailingStop(position);
}
}
private void ManageAtrTrailingStop(Position position)
{
// 1. Calculate the dynamic trail distance based on the latest closed ATR bar
double currentAtr = _atr.Result.Last(1);
double trailDistance = currentAtr * AtrMultiplier;
if (position.TradeType == TradeType.Buy)
{
// 2. Calculate the proposed new SL (Current Bid minus the ATR buffer)
double proposedSl = Symbol.Bid - trailDistance;
proposedSl = Math.Round(proposedSl, Symbol.Digits);
// 3. The Ratchet Logic: Only move the SL UP.
// If there is no SL, or the new SL is higher than the current SL by at least our safety threshold:
if (!position.StopLoss.HasValue || (proposedSl - position.StopLoss.Value >= _minUpdateDistance))
{
// Execute asynchronously to prevent thread-blocking if managing multiple positions
ModifyPositionAsync(position, proposedSl, position.TakeProfit);
}
}
else if (position.TradeType == TradeType.Sell)
{
// 2. Calculate the proposed new SL (Current Ask plus the ATR buffer)
double proposedSl = Symbol.Ask + trailDistance;
proposedSl = Math.Round(proposedSl, Symbol.Digits);
// 3. The Ratchet Logic: Only move the SL DOWN.
if (!position.StopLoss.HasValue || (position.StopLoss.Value - proposedSl >= _minUpdateDistance))
{
ModifyPositionAsync(position, proposedSl, position.TakeProfit);
}
}
}
}
}
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:29 pm
by PTScalper
Key Architectural Mechanics
Asynchronous Modification (ModifyPositionAsync): Standard ModifyPosition() forces the bot's main thread to pause and wait for the broker to confirm the modification before moving to the next line of code. If you are trailing 5 positions simultaneously, network latency will cause the bot to miss ticks. ModifyPositionAsync fires the request and instantly moves on, maintaining tick-level precision.
Timeframe Decoupling: The ATR calculation explicitly asks for AtrTimeFrame rather than using the default MarketData.GetBars(TimeFrame). If you are scalping on a 1-minute chart, you might want your trailing stop anchored to the structural volatility of the 15-minute or 1-hour chart to avoid being shaken out by micro-noise.
The Ratchet Condition: The math (proposedSl - position.StopLoss.Value >= _minUpdateDistance) strictly guarantees that the Stop Loss can never widen to give back profits, and it prevents the bot from making hundreds of 0.1-pip adjustments.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:30 pm
by PTScalper
To dynamically size your positions based on a variable ATR Stop Loss, you must calculate the fiat risk amount and divide it by the monetary cost of the stop loss distance.
In C# cAlgo, you do not need to write complex exchange-rate conversion logic for cross-pairs (e.g., trading EUR/JPY on a USD account). The API natively handles this through the Symbol.PipValue property, which represents the account-currency fiat value of a 1-pip move for exactly 1 unit of volume.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:31 pm
by PTScalper
Here is the isolated, production-ready function to calculate exact lot sizing:
Code: Select all
/// <summary>
/// Calculates the required volume to risk a specific percentage of the account balance.
/// </summary>
/// <param name="riskPercentage">The percentage of the account to risk (e.g., 1.0 for 1%)</param>
/// <param name="stopLossPips">The dynamic ATR Stop Loss distance in pips</param>
/// <returns>Normalized volume in units, safe for broker execution.</returns>
private double CalculateVolumeByRisk(double riskPercentage, double stopLossPips)
{
if (stopLossPips <= 0)
{
Print("Warning: Stop loss must be greater than zero. Returning minimum volume.");
return Symbol.VolumeInUnitsMin;
}
// 1. Calculate the absolute fiat amount you are willing to lose
// Note: Use Account.Equity instead of Account.Balance if you want floating drawdowns factored in
double maxRiskFiat = Account.Balance * (riskPercentage / 100.0);
// 2. Calculate the fiat cost of a 1-pip move for a single unit of volume
double costPerPipPerUnit = Symbol.PipValue;
// 3. Calculate the exact mathematical volume required
double exactVolume = maxRiskFiat / (stopLossPips * costPerPipPerUnit);
// 4. Broker Normalization (The Strict Enforcer)
// We strictly round DOWN. If math dictates 14,800 units, but the broker step is 1,000,
// it rounds to 14,000. Rounding up to 15,000 would exceed the 1% risk limit.
double safeVolume = Symbol.NormalizeVolumeInUnits(exactVolume, RoundingMode.Down);
// 5. Ensure the calculated volume does not fall below the broker's absolute minimum
return Math.Max(safeVolume, Symbol.VolumeInUnitsMin);
}
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:31 pm
by PTScalper
Architectural Mechanics
The RoundingMode.Down Protocol: This is the most critical safeguard in the method. Brokers enforce strict lot size increments (Symbol.VolumeInUnitsStep), usually in increments of 1,000 units (0.01 lots). If your mathematical risk allows for exactly 12,900 units, rounding to the nearest step (13,000) means you will risk slightly more than 1.0%. Forcing RoundingMode.Down ensures you never violate your maximum risk parameter.
Account.Balance vs. Account.Equity: The snippet uses Account.Balance, which calculates risk based on your closed capital. If you frequently hold multiple overlapping trades, change this to Account.Equity. Using Equity ensures that if you are currently in a 5% floating drawdown on other trades, your next position's 1% risk is sized against your true remaining capital, preventing exponential margin decay.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:31 pm
by PTScalper
Integration with your Execution Engine
Here is how you wire the volume calculator to the ATR Stop Loss logic before sending the order to the broker:
Code: Select all
[Parameter("Risk % Per Trade", Group = "Risk Management", DefaultValue = 1.0, MinValue = 0.1)]
public double RiskPercentage { get; set; }
private void ExecuteAtrRiskAdjustedTrade(TradeType tradeType, string label)
{
// 1. Fetch current ATR value and convert to pips
double currentAtr = _atr.Result.Last(1);
double atrStopLossPips = currentAtr / Symbol.PipSize;
// Optional: Add the slippage buffer from our previous discussion
double slBuffer = GetVolatilitySlippageInPips() * SlVolatilityMultiplier;
double finalSlPips = atrStopLossPips + slBuffer;
// 2. Calculate the exact volume required to risk 1% based on the final widened SL
double volumeInUnits = CalculateVolumeByRisk(RiskPercentage, finalSlPips);
// 3. Optional: Set Take Profit based on Risk-to-Reward ratio (e.g., 2.0R)
double targetPips = finalSlPips * 2.0;
Print($"[Execution] Final SL: {finalSlPips:F1} pips | Required Vol: {volumeInUnits} units");
// 4. Execute the order
ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, label, finalSlPips, targetPips);
}
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 1:32 pm
by PTScalper
To implement partial profit-taking, you must track the state of the position in the cBot's memory. If you do not explicitly record that a position has already been scaled out, the bot will continuously slice the remaining volume in half on every micro-tick once the 1R price is breached.
In C# cAlgo, the safest way to manage this state is by storing the Position.Id in a HashSet<int>.