Advertisement IC Markets

Spread Filters for Scalping: When Not to Enter

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
Post Reply
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

Architectural Mechanics

NetProfit vs. GrossProfit: The snippet specifically calls .NetProfit rather than .GrossProfit. For short-term price action scalping, gross profit is a dangerous metric because it represents pure price movement. NetProfit automatically deducts the round-turn commissions and overnight swap fees, ensuring your calculation represents true account equity impact.

LINQ Optimization: If you prefer cleaner syntax and your system supports it, you can collapse the loops using LINQ:

Code: Select all

double totalNetPnL = History.FindAll(ltid).Sum(t => t.NetProfit) + 
                     Positions.FindAll(ltid).Sum(p => p.NetProfit);
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

Note: While LINQ is cleaner, explicit foreach loops are slightly faster in high-frequency OnTick execution environments because they avoid the overhead of delegate allocation and closure generation.

Handling Grid / Martingale Systems: Because this aggregates everything with the matching label, this exact function works flawlessly if your strategy scales in (adding multiple new positions to the same trade idea) rather than just scaling out. It will sum the entire portfolio of trades associated with that specific LTID.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

To execute a fiat-based mass closure, you must calculate the live aggregate PnL on every tick and, crucially, implement a state-lock to prevent the bot from spamming the broker with redundant close requests during the network latency window.

When a logical trade crosses your target profit, you fire asynchronous close requests for all remaining active fragments simultaneously.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

Here is the complete C# cAlgo architecture to handle basket closures based on net fiat PnL:

Code: Select all

using System.Collections.Generic;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class FiatTargetManager : Robot
    {
        [Parameter("Target Net PnL (Fiat)", Group = "Trade Management", DefaultValue = 50.0)]
        public double TargetNetPnL { get; set; }

        // State lock to prevent micro-tick spam while waiting for the broker to close positions
        private HashSet<string> _closingLtids = new HashSet<string>();

        protected override void OnStart()
        {
            Positions.Closed += OnPositionClosed;
        }

        protected override void OnTick()
        {
            // 1. Get all unique Logical Trade IDs (Labels) currently active in the market
            var activeLtids = Positions.Select(p => p.Label).Distinct();

            foreach (var ltid in activeLtids)
            {
                // Skip if this LTID is already in the process of being closed
                if (_closingLtids.Contains(ltid)) continue;

                // 2. Calculate the true Net PnL (Realized + Unrealized)
                double currentNetPnL = CalculateLogicalTradeNetPnL(ltid);

                // 3. Trigger mass closure if the target is met
                if (currentNetPnL >= TargetNetPnL)
                {
                    ExecuteMassClosure(ltid, currentNetPnL);
                }
            }
        }

        private void ExecuteMassClosure(string ltid, double triggeredPnL)
        {
            // 1. Lock the LTID immediately so subsequent ticks ignore it
            _closingLtids.Add(ltid);
            
            Print($"[Target Reached] LTID: {ltid} hit {triggeredPnL:C2}. Initiating mass closure.");

            // 2. Fetch all remaining open fragments for this LTID
            var fragmentsToClose = Positions.FindAll(ltid);

            // 3. Fire asynchronous close requests for all fragments concurrently
            foreach (var position in fragmentsToClose)
            {
                ClosePositionAsync(position, OnFragmentCloseComplete);
            }
        }

        private double CalculateLogicalTradeNetPnL(string ltid)
        {
            // Fast LINQ aggregation of realized history and unrealized active positions
            return History.FindAll(ltid).Sum(t => t.NetProfit) + 
                   Positions.FindAll(ltid).Sum(p => p.NetProfit);
        }

        private void OnFragmentCloseComplete(TradeResult result)
        {
            if (!result.IsSuccessful && result.Position != null)
            {
                // If the broker rejects a close request (e.g., off-quotes), unlock the LTID 
                // so the bot re-evaluates and tries to close it again on the next tick.
                _closingLtids.Remove(result.Position.Label);
                Print($"[Error] Failed to close fragment {result.Position.Id}. Re-arming. Error: {result.Error}");
            }
        }

        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            string closedLabel = args.Position.Label;

            // Once the final fragment of the LTID is successfully closed, clear the state lock
            if (Positions.FindAll(closedLabel).Length == 0)
            {
                _closingLtids.Remove(closedLabel);
                Print($"[Mass Closure Complete] Logical Trade {closedLabel} fully liquidated.");
            }
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

Architectural Mechanics

LINQ Grouping (Select(p => p.Label).Distinct()): If a trade has been scaled out and currently has 4 open child positions, a standard foreach loop over Positions would calculate the heavy PnL math 4 separate times per tick. Extracting the distinct labels first ensures the bot calculates the aggregate PnL only once per logical trade, significantly reducing CPU load during high-volatility tick spikes.

The Execution State Lock (_closingLtids): It takes roughly 50 to 200 milliseconds for a trade to close on a live server. During that window, cTrader will process dozens of new ticks. If you do not lock the string label in the _closingLtids hashset before calling ClosePositionAsync, the bot will flood the broker's API with redundant close commands for a position that is already processing a closure, often triggering temporary IP bans from the broker.

Concurrent Batching: The foreach loop inside ExecuteMassClosure utilizes ClosePositionAsync. It does not wait for Fragment A to close before asking the broker to close Fragment B. It fires all requests in a single millisecond batch, allowing the broker's matching engine to execute them parallelly, minimizing slippage drag.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

Risk Note on Fiat Slippage: Market orders guarantee execution, not price. If you trigger the mass closure exactly at $50.00, the actual realized net profit will likely settle slightly lower (e.g., $49.60) because you are aggressively crossing the spread and exiting at the Bid/Ask during live market movement.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

To implement a fiat-based trailing stop, you must introduce a High-Water Mark memory system. The bot needs to remember the absolute highest net profit a logical trade has achieved since it crossed the activation threshold, and constantly evaluate the current profit against that peak minus your trailing distance.

If the peak profit was $100 and your trailing distance is $10, the bot dynamically sets the hidden tripwire at $90. If the profit drops to $90, it liquidates the basket.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

Here is the complete C# cAlgo architecture for a Fiat Trailing Stop:

Code: Select all

using System;
using System.Collections.Generic;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class FiatTrailingManager : Robot
    {
        [Parameter("Activation Profit (Fiat)", Group = "Trailing Fiat", DefaultValue = 50.0)]
        public double ActivationProfit { get; set; }

        [Parameter("Trailing Distance (Fiat)", Group = "Trailing Fiat", DefaultValue = 10.0)]
        public double TrailingDistance { get; set; }

        // Tracks the highest profit achieved for each Logical Trade ID
        private Dictionary<string, double> _highWaterMarks = new Dictionary<string, double>();
        
        // Prevents redundant API calls while positions are closing
        private HashSet<string> _closingLtids = new HashSet<string>();

        protected override void OnStart()
        {
            Positions.Closed += OnPositionClosed;
        }

        protected override void OnTick()
        {
            var activeLtids = Positions.Select(p => p.Label).Distinct();

            foreach (var ltid in activeLtids)
            {
                if (_closingLtids.Contains(ltid)) continue;

                double currentNetPnL = CalculateLogicalTradeNetPnL(ltid);

                // 1. Check if the trade has crossed the activation threshold
                if (currentNetPnL >= ActivationProfit || _highWaterMarks.ContainsKey(ltid))
                {
                    ManageFiatTrail(ltid, currentNetPnL);
                }
            }
        }

        private void ManageFiatTrail(string ltid, double currentNetPnL)
        {
            // 2. Initialize or update the High-Water Mark for this trade
            if (!_highWaterMarks.ContainsKey(ltid))
            {
                _highWaterMarks[ltid] = currentNetPnL;
                Print($"[Fiat Trail Activated] LTID: {ltid} crossed {ActivationProfit:C2}. Trailing by {TrailingDistance:C2}.");
            }
            else
            {
                // Update the peak if the current profit makes a new high
                _highWaterMarks[ltid] = Math.Max(_highWaterMarks[ltid], currentNetPnL);
            }

            // 3. Calculate the dynamic tripwire (Peak Profit - Trailing Distance)
            double dynamicTripwire = _highWaterMarks[ltid] - TrailingDistance;

            // 4. Liquidate if profit pulls back to the tripwire
            if (currentNetPnL <= dynamicTripwire)
            {
                ExecuteMassClosure(ltid, currentNetPnL, dynamicTripwire);
            }
        }

        private void ExecuteMassClosure(string ltid, double executedPnL, double tripwire)
        {
            _closingLtids.Add(ltid);
            
            Print($"[Fiat Trail Hit] LTID: {ltid} | Peak: {_highWaterMarks[ltid]:C2} | Tripwire: {tripwire:C2} | Liquidating at: {executedPnL:C2}");

            var fragmentsToClose = Positions.FindAll(ltid);
            foreach (var position in fragmentsToClose)
            {
                ClosePositionAsync(position, OnFragmentCloseComplete);
            }
        }

        private double CalculateLogicalTradeNetPnL(string ltid)
        {
            return History.FindAll(ltid).Sum(t => t.NetProfit) + 
                   Positions.FindAll(ltid).Sum(p => p.NetProfit);
        }

        private void OnFragmentCloseComplete(TradeResult result)
        {
            if (!result.IsSuccessful && result.Position != null)
            {
                _closingLtids.Remove(result.Position.Label);
            }
        }

        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            string closedLabel = args.Position.Label;

            if (Positions.FindAll(closedLabel).Length == 0)
            {
                _closingLtids.Remove(closedLabel);
                
                // CRITICAL: Prevent memory leaks by removing the high-water mark once flat
                if (_highWaterMarks.ContainsKey(closedLabel))
                {
                    _highWaterMarks.Remove(closedLabel);
                }
            }
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

Architectural Mechanics

Dictionary State Tracking (Dictionary<string, double>): A HashSet only remembers if a state exists; a Dictionary remembers what the state is. _highWaterMarks maps your unique logical trade label directly to its peak fiat value.

Persistent Activation: The condition (currentNetPnL >= ActivationProfit || _highWaterMarks.ContainsKey(ltid)) is vital. If a trade hits $52 (activating the dictionary), but then drops to $45 on the next tick, it must remain actively trailed because the tripwire is locked at $42 ($52 peak minus $10 distance). The || _highWaterMarks.ContainsKey ensures the trail does not deactivate just because profit dips temporarily below the initial activation threshold.

Gap Logic / Slippage Reality: The OnTick method fires sequentially with price updates. If the market is moving smoothly, currentNetPnL will hit exactly $40.00 and trigger. However, if a massive news candle gaps price over your tripwire, currentNetPnL might jump instantly from $42.00 down to $35.00. The logic currentNetPnL <= dynamicTripwire ensures the liquidation still fires, executing at $35.00 to prevent further loss.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Spread Filters for Scalping: When Not to Enter

Post by PTScalper »

To project dynamic tracking data directly onto your chart, you should use cTrader's Chart.DrawStaticText method rather than standard chart text. Static text pins the data to the screen coordinates (like a Heads-Up Display or HUD), meaning it stays perfectly visible regardless of how far you zoom in, zoom out, or scroll back in time.

Because multiple logical trades (LTIDs) might be active at once, the most robust architecture is to build a StringBuilder that loops through your active dictionary and refreshes the HUD on every tick.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply