Advertisement IC Markets

Beating moving stops as a scalper: rules that stuck

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

Re: Beating moving stops as a scalper: rules that stuck

Post by PTScalper »

Moving from MetaTrader to cTrader gives us a massive architectural advantage for this exact problem. Because cAlgo is built on C# and .NET, we don't have to poll the server on every single tick just to see if you dragged a line.

Instead, we can use a highly efficient, event-driven architecture. By subscribing directly to the Positions.Modified event, the cBot acts as an asynchronous watchman. The moment the cTrader UI attempts to register a stop loss modification with the server, the cBot intercepts the event, checks the math, and immediately fires a modification request to overwrite your manual change if you widened the stop.

Because you are working in C#, we can handle state cleanly using memory dictionaries rather than relying on clunky object names on the chart. We also explicitly set IsInteractive = false on the drawn lines so the chart objects physically ignore your mouse clicks.

Here is the complete cBot implementation.
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: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Beating moving stops as a scalper: rules that stuck

Post by PTScalper »

cTrader C# Implementation (GhostAnchor_cBot.cs)

Code: Select all

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

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class GhostAnchorInvalidation : Robot
    {
        [Parameter("Warning Buffer (%)", Group = "Risk & Alerts", DefaultValue = 15.0, MinValue = 1, MaxValue = 50)]
        public double BufferPercent { get; set; }

        [Parameter("Hardcore Mode (Snap Back)", Group = "Execution", DefaultValue = true)]
        public bool HardcoreMode { get; set; }

        // State Management 
        private Dictionary<int, double> _originalStopLosses = new Dictionary<int, double>();
        private Dictionary<int, double> _bufferPrices = new Dictionary<int, double>();
        private Dictionary<int, bool> _alertFired = new Dictionary<int, bool>();

        protected override void OnStart()
        {
            Print("Ghost Anchor Initialized. Strict Execution Active.");

            // Register Event Handlers
            Positions.Opened += OnPositionOpened;
            Positions.Modified += OnPositionModified;
            Positions.Closed += OnPositionClosed;

            // Initialize any pre-existing positions on the chart
            foreach (var position in Positions)
            {
                if (position.SymbolName == SymbolName)
                    RegisterAnchor(position);
            }
        }

        private void OnPositionOpened(PositionOpenedEventArgs args)
        {
            if (args.Position.SymbolName == SymbolName)
                RegisterAnchor(args.Position);
        }

        private void RegisterAnchor(Position position)
        {
            if (!position.StopLoss.HasValue) return; // Ignore trades without hard invalidation

            double originalSl = Math.Round(position.StopLoss.Value, Symbol.Digits);
            _originalStopLosses[position.Id] = originalSl;

            // Calculate Buffer Zone
            double riskRange = Math.Abs(position.EntryPrice - originalSl);
            double bufferDist = riskRange * (BufferPercent / 100.0);
            
            double bufferPrice = position.TradeType == TradeType.Buy 
                ? originalSl + bufferDist 
                : originalSl - bufferDist;

            bufferPrice = Math.Round(bufferPrice, Symbol.Digits);
            
            _bufferPrices[position.Id] = bufferPrice;
            _alertFired[position.Id] = false;

            // Render Immutable Chart Objects
            var anchorLine = Chart.DrawHorizontalLine("Anchor_" + position.Id, originalSl, Color.Red, 2, LineStyle.Solid);
            var bufferLine = Chart.DrawHorizontalLine("Buffer_" + position.Id, bufferPrice, Color.DarkOrange, 1, LineStyle.Lines);

            // Friction: Remove interactivity so they cannot be selected or dragged
            anchorLine.IsInteractive = false;
            bufferLine.IsInteractive = false;
        }

        private void OnPositionModified(PositionModifiedEventArgs args)
        {
            var position = args.Position;
            if (position.SymbolName != SymbolName || !position.StopLoss.HasValue) return;
            if (!_originalStopLosses.ContainsKey(position.Id)) return;

            double currentSl = Math.Round(position.StopLoss.Value, Symbol.Digits);
            double originalSl = _originalStopLosses[position.Id];

            if (currentSl != originalSl)
            {
                bool isWidened = (position.TradeType == TradeType.Buy && currentSl < originalSl) ||
                                 (position.TradeType == TradeType.Sell && currentSl > originalSl);

                if (isWidened)
                {
                    Print($"VIOLATION: Position {position.Id} stop loss widened. Original: {originalSl}, Attempted: {currentSl}");

                    if (HardcoreMode)
                    {
                        Print($"HARDCORE ENFORCEMENT: Snapping SL back to structural anchor {originalSl}.");
                        // Asynchronously snap the stop loss back to prevent UI freezing
                        ModifyPositionAsync(position, originalSl, position.TakeProfit);
                    }
                }
                else 
                {
                    // Allowed: Trailing stop / reducing risk
                    // We update the anchor dictionary so trailing doesn't get flagged later, 
                    // but we do NOT move the visual red line (it marks original invalidation).
                    _originalStopLosses[position.Id] = currentSl; 
                }
            }
        }

        protected override void OnTick()
        {
            // Monitor price for buffer breaches
            foreach (var position in Positions)
            {
                if (position.SymbolName != SymbolName || !_bufferPrices.ContainsKey(position.Id)) continue;
                if (_alertFired[position.Id]) continue; // Already warned

                double bufferPrice = _bufferPrices[position.Id];
                double currentPrice = position.TradeType == TradeType.Buy ? Symbol.Bid : Symbol.Ask;

                bool breached = (position.TradeType == TradeType.Buy && currentPrice <= bufferPrice) ||
                                (position.TradeType == TradeType.Sell && currentPrice >= bufferPrice);

                if (breached)
                {
                    string msg = $"EXECUTION ALERT: Price entered the {BufferPercent}% buffer zone for {SymbolName} (Pos: {position.Id}). DO NOT WIDEN STOP.";
                    
                    // Display on screen and play system sound
                    Chart.DrawStaticText("AlertTxt_" + position.Id, msg, VerticalAlignment.Top, HorizontalAlignment.Center, Color.Orange);
                    Notifications.PlaySound(SoundType.Warning);
                    
                    _alertFired[position.Id] = true;
                }
            }
        }

        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            int id = args.Position.Id;

            // Clean up state
            _originalStopLosses.Remove(id);
            _bufferPrices.Remove(id);
            _alertFired.Remove(id);

            // Clean up UI
            Chart.RemoveObject("Anchor_" + id);
            Chart.RemoveObject("Buffer_" + id);
            Chart.RemoveObject("AlertTxt_" + id);
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Beating moving stops as a scalper: rules that stuck

Post by PTScalper »

Key differences in the cAlgo implementation:

Asynchronous Enforcement: If HardcoreMode is triggered, it uses ModifyPositionAsync(). This ensures that your attempt to drag the line is instantly overwritten by the server without locking up your local cTrader UI thread.

Trailing Stop Allowances: I added a block that checks which way you modified the stop. If you moved the stop to reduce risk (e.g., trailing it to breakeven), the script allows it and updates the internal state memory, but it leaves the red Ghost Anchor at the original structural invalidation point.

No Polling Loop: Because it listens directly to Positions.Modified, it only evaluates the math exactly when a change occurs, making it incredibly lightweight compared to looping through tickets every tick.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Beating moving stops as a scalper: rules that stuck

Post by PTScalper »

To make this "pro," we need to implement standard software engineering principles:

State Encapsulation: Ditching loose parallel arrays/dictionaries for a unified PositionContext state object.

Thread Safety: Implementing a ConcurrentDictionary to guarantee state integrity during rapid event firing.

Event Filtering: The Positions.Modified event triggers on any change (including partial closes or take-profit edits). We must isolate the exact mutation before executing the logic to save CPU cycles.

Separation of Concerns (SoC): Decoupling the UI rendering logic from the execution logic.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Beating moving stops as a scalper: rules that stuck

Post by PTScalper »

Here is the refactored, production-ready C# architecture for cTrader.

cTrader C# Implementation (GhostAnchor_Pro.cs)

Code: Select all

using System;
using System.Collections.Concurrent;
using cAlgo.API;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class GhostAnchorPro : Robot
    {
        // --- [ PARAMETERS ] ---
        [Parameter("Risk Buffer (%)", Group = "Risk Management", DefaultValue = 15.0, MinValue = 1, MaxValue = 50)]
        public double BufferPercent { get; set; }

        [Parameter("Enforce Invalidation (Snap-Back)", Group = "Execution Control", DefaultValue = true)]
        public bool HardcoreMode { get; set; }

        // --- [ STATE MANAGEMENT ] ---
        // Encapsulating position state rather than using parallel collections
        private class PositionContext
        {
            public double OriginalSL { get; set; }
            public double BufferPrice { get; set; }
            public bool AlertTriggered { get; set; }
            public string AnchorId => $"Anchor_{PositionId}";
            public string BufferId => $"Buffer_{PositionId}";
            public int PositionId { get; set; }
        }

        // Thread-safe dictionary for high-frequency tick environments
        private readonly ConcurrentDictionary<int, PositionContext> _activeStates = new ConcurrentDictionary<int, PositionContext>();

        protected override void OnStart()
        {
            Log("Ghost Anchor Subsystem Initialized. Strict Execution enforced.");

            Positions.Opened += OnPositionOpened;
            Positions.Modified += OnPositionModified;
            Positions.Closed += OnPositionClosed;

            // Hydrate state for pre-existing positions
            foreach (var position in Positions)
            {
                if (position.SymbolName == SymbolName)
                    InitializeContext(position);
            }
        }

        // --- [ EVENT HANDLERS ] ---
        private void OnPositionOpened(PositionOpenedEventArgs args)
        {
            if (args.Position.SymbolName == SymbolName)
                InitializeContext(args.Position);
        }

        private void OnPositionModified(PositionModifiedEventArgs args)
        {
            var position = args.Position;
            if (position.SymbolName != SymbolName || !position.StopLoss.HasValue) return;
            if (!_activeStates.TryGetValue(position.Id, out var context)) return;

            double currentSl = Math.Round(position.StopLoss.Value, Symbol.Digits);
            
            // Event Filtering: Ignore modifications that aren't SL changes (e.g., taking partials)
            if (currentSl == context.OriginalSL) return;

            bool isWidened = (position.TradeType == TradeType.Buy && currentSl < context.OriginalSL) ||
                             (position.TradeType == TradeType.Sell && currentSl > context.OriginalSL);

            if (isWidened)
            {
                Log($"VIOLATION [Pos {position.Id}]: Invalidation widened. Attempted: {currentSl}, Anchor: {context.OriginalSL}");

                if (HardcoreMode)
                {
                    Log($"ENFORCEMENT: Snapping Stop Loss back to structural anchor.");
                    // Fire-and-forget async modification prevents UI thread blocking
                    ModifyPositionAsync(position, context.OriginalSL, position.TakeProfit);
                }
            }
            else
            {
                // State Mutation: Allow risk reduction (trailing SL) without moving the original Anchor line
                context.OriginalSL = currentSl;
                Log($"RISK REDUCTION: Position {position.Id} trailing SL updated to {currentSl}. Anchor remains intact.");
            }
        }

        protected override void OnTick()
        {
            foreach (var position in Positions)
            {
                if (position.SymbolName != SymbolName) continue;
                if (!_activeStates.TryGetValue(position.Id, out var context)) continue;
                if (context.AlertTriggered) continue;

                double currentPx = position.TradeType == TradeType.Buy ? Symbol.Bid : Symbol.Ask;
                
                bool isBreached = (position.TradeType == TradeType.Buy && currentPx <= context.BufferPrice) ||
                                  (position.TradeType == TradeType.Sell && currentPx >= context.BufferPrice);

                if (isBreached)
                {
                    FireBufferAlert(position, context);
                }
            }
        }

        private void OnPositionClosed(PositionClosedEventArgs args)
        {
            int id = args.Position.Id;

            // Cleanly dispose of state and UI components
            if (_activeStates.TryRemove(id, out var context))
            {
                Chart.RemoveObject(context.AnchorId);
                Chart.RemoveObject(context.BufferId);
                Chart.RemoveObject($"AlertTxt_{id}");
            }
        }

        // --- [ CORE LOGIC & UI ] ---
        private void InitializeContext(Position position)
        {
            if (!position.StopLoss.HasValue) return;

            double slPrice = Math.Round(position.StopLoss.Value, Symbol.Digits);
            double riskSpan = Math.Abs(position.EntryPrice - slPrice);
            double bufferOffset = riskSpan * (BufferPercent / 100.0);
            
            double bufferPx = position.TradeType == TradeType.Buy 
                ? slPrice + bufferOffset 
                : slPrice - bufferOffset;

            var ctx = new PositionContext
            {
                PositionId = position.Id,
                OriginalSL = slPrice,
                BufferPrice = Math.Round(bufferPx, Symbol.Digits),
                AlertTriggered = false
            };

            if (_activeStates.TryAdd(position.Id, ctx))
            {
                RenderImmutableUI(ctx);
            }
        }

        private void RenderImmutableUI(PositionContext context)
        {
            // Strongly typed chart objects for reliable property assignment
            var anchor = Chart.DrawHorizontalLine(context.AnchorId, context.OriginalSL, Color.Crimson, 2, LineStyle.Solid);
            var buffer = Chart.DrawHorizontalLine(context.BufferId, context.BufferPrice, Color.DarkOrange, 1, LineStyle.Dots);

            // True UI Friction
            anchor.IsInteractive = false;
            buffer.IsInteractive = false;
        }

        private void FireBufferAlert(Position position, PositionContext context)
        {
            context.AlertTriggered = true;
            string msg = $"CRITICAL: {SymbolName} breached {BufferPercent}% risk buffer. Do not move SL.";
            
            Log(msg);
            Chart.DrawStaticText($"AlertTxt_{position.Id}", msg, VerticalAlignment.Top, HorizontalAlignment.Center, Color.OrangeRed);
            Notifications.PlaySound(SoundType.Warning);
        }

        // --- [ TELEMETRY ] ---
        private void Log(string message)
        {
            // Structured logging prefix for easier terminal filtering
            Print($"[GhostAnchor] {message}");
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3027
Joined: Mon Jul 20, 2026 1:28 pm

Re: Beating moving stops as a scalper: rules that stuck

Post by PTScalper »

The Architectural Upgrades:

Event Filtering: The script now checks if (currentSl == context.OriginalSL) return; inside OnPositionModified. If you scale out of a position (taking 50% off the table), cTrader fires the Modified event because the volume changed. The original script would waste CPU cycles recalculating the stop loss logic. This version instantly filters out irrelevant mutations.

ConcurrentDictionary & PositionContext: Replacing parallel memory arrays with a strongly typed object (PositionContext) managed by a ConcurrentDictionary. This ensures that if the market experiences extreme volatility and throws dozens of ticks and execution events in a millisecond, the memory state remains locked and thread-safe.

Structured Telemetry: Instead of generic Print() commands, the log output is now standardized with a [GhostAnchor] prefix. This makes debugging much easier if you are combing through the cTrader Automate log panel after a session.

Memory Leak Prevention: The OnPositionClosed event now utilizes .TryRemove() which guarantees the state object and all associated chart UI elements are safely dumped from RAM the moment the trade concludes.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
LondonScalper
Posts: 701
Joined: Sat Sep 05, 2026 7:54 am

Re: Beating moving stops as a scalper: rules that stuck

Post by LondonScalper »

PTScalper wrote:Widening a stop changes the trade into a larger loss; invalidation belongs to market structure, not pain tolerance. Engineered friction — an alert ahead of the stop instead of easy chart dragging — forces conscious intervention.
That’s the discipline that sticks. Once you drag the stop for comfort, you are no longer managing the original trade — you are managing regret with more risk.

My counter-rules stay simple: the stop only tightens or stays; every edit is tagged stop_edit; and a time stop sits alongside structural invalidation so a dead tape does not become an open-ended hold. Winners after a widened stop are luck that borrowed extra risk — I still log them as process faults.

Concrete friction on the desk: I place an alert a few ticks before the hard stop and disable one-click drag on the chart during the session. If I want to intervene, I have to type the new level. That pause kills most impulse widens.

Rule: structure sets invalidation; pain never moves the stop outward. Do you keep the engineered alert on every scalp, or only on tickets where the stop sits close to obvious liquidity?
PropScalpDesk
Posts: 273
Joined: Sat Sep 19, 2026 7:50 pm

Re: Beating moving stops as a scalper: rules that stuck

Post by PropScalpDesk »

PTScalper wrote:Regarding visibility and execution friction: retail trading platform UIs are built for convenience, making drag-and-drop order edits dangerously seamless. Engineering your own operational friction is a necessary defense mechanism. Below is a more advanced, institutional-grade Pine Script solution .
Fiddling with your stop-loss once a trade is live is almost always a manifestation of fear rather than a tactical adjustment. Making a habit of choking the trade usually guarantees you will get stopped out by normal market noise before the setup can mature. I maintain a strict, non-negotiable policy: I only allow a stop move if the actual market structure—the specific technical level that invalidates my original thesis—has definitively shifted. I never touch a stop just because watching a red or fluctuating P&L makes me uncomfortable.

In the heat of the moment, when the tape is moving fast and volatility spikes, your instincts will often lie to you. That is exactly when a cold, pre-written rule will always outperform a gut feeling. You have to seriously question your own mechanics: under what extreme, verifiable condition do you ever allow yourself to edit a stop intrabar? If you cannot answer that with objective, pre-defined criteria, your hands should not be anywhere near the keyboard.

Discipline is not just about the trades you take; it is heavily about the ones you actively skip. I make it a point to meticulously log my "refused tickets"—setups I analyzed but ultimately rejected. By tracking these passed opportunities, sitting flat officially counts as productive work. If you do not mentally categorize patience as active execution, the mind gets bored, and the desk inevitably invents phantom activity just to feel busy, leading to forced, low-probability trades.

Operating within the strictures of a funded account provides a necessary psychological boundary. The funded trailing drawdown (DD) acts as an impartial, external referee. It does not care about your excuses, your near-misses, or your fundamental bias; it simply enforces the rules and keeps the desk brutally honest when self-discipline starts to fray.

To maintain this structural integrity, I always refer back to a core topic note from my tracking sheet for t=12392: keep your risk sizing absolutely unchanged until the sample data explicitly dictates otherwise. Tinkering with risk parameters based on the emotional high or low of the last two trades is a recipe for disaster. You only adjust your exposure when a statistically significant block of data mathematically proves it is time to adapt.
Last edited by PropScalpDesk on Thu Sep 24, 2026 7:05 pm, edited 1 time in total.
LondonNewsTrader
Posts: 79
Joined: Mon Sep 21, 2026 9:30 am

Re: Beating moving stops as a scalper: rules that stuck

Post by LondonNewsTrader »

PTScalper wrote:Regarding visibility and execution friction: retail trading platform UIs are built for convenience, making drag-and-drop order edits dangerously seamless. Engineering your own operational friction is a necessary defense mechanism. Below is a more advanced, institutional-grade Pine Script solution .
The confirm=true approach is clever. Making the only route to a change go through the settings dialog is exactly the kind of friction the opening post asks for.

The limitation is that the lines are a copy of your plan on TradingView, while the stop that matters sits on the broker's server. Drag the real stop in MT5 or cTrader and the matrix stays put, and it can even reassure you in the wrong way: the chart still shows the original invalidation while the live order no longer does. So I'd pair it with a habit. When the warning alert fires at 15% of risk, the task is to check that the broker stop still matches the anchored one, not to decide whether to move it.

It would be easy to add the opener's third rule, the time stop, as well. An input for maximum holding minutes, with the lines turning grey and an alert firing when time runs out, puts all three rules in one place.

A 15% warning buffer is reasonable on majors. On gold around US data, price can travel from the warning level through the stop inside one M1 bar, so the alert arrives after the fact. For those windows I'd simply not hold a position.
Post Reply