Page 3 of 4

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:02 pm
by PTScalper
Transitioning this from a passive indicator into an active execution terminal is where cTrader’s API really outshines MetaTrader. Because cAlgo is native C#, we can treat the chart exactly like a WPF canvas, layering on event-driven UI controls that intercept the order before it ever reaches the broker.

Instead of just warning you, this cBot adds BUY and SELL buttons directly to your chart. When you click execute, the C# logic pulls the live tick spread, compares it to the pip distance of your dragged Stop Loss line, and if the math violates your threshold, it drops the execution entirely and logs a rejection error to the chart.

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:02 pm
by PTScalper
cTrader Version: Spread-Protected Execution cBot (C#)

Open cTrader Automate, create a new cBot, and paste this.

Code: Select all

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

namespace cAlgo.Robots
{
    [Robot(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class SpreadProtectedTerminal : Robot
    {
        [Parameter("Max Spread/Stop Ratio (%)", DefaultValue = 15.0)]
        public double MaxSpreadRatio { get; set; }

        [Parameter("Fixed Risk (Lots)", DefaultValue = 1.0)]
        public double VolumeInLots { get; set; }

        private ChartHorizontalLine _slLine;
        private ChartHorizontalLine _tpLine;
        private TextBlock _dashboardText;

        protected override void OnStart()
        {
            double currentPrice = Symbol.Ask;
            double initialStopDist = 10 * Symbol.PipSize;
            double initialTargetDist = 20 * Symbol.PipSize;

            // 1. Draw interactive Structural Lines (Entry is assumed as Market Price)
            _slLine = Chart.DrawHorizontalLine("RiskBot_SL", currentPrice - initialStopDist, Color.Red);
            _slLine.IsInteractive = true;

            _tpLine = Chart.DrawHorizontalLine("RiskBot_TP", currentPrice + initialTargetDist, Color.LimeGreen);
            _tpLine.IsInteractive = true;

            // 2. Initialize the Dashboard Text Block
            _dashboardText = new TextBlock
            {
                ForegroundColor = Color.White,
                Margin = "10",
                FontSize = 12
            };

            // 3. Build the UI Execution Panel
            var executionPanel = new StackPanel
            {
                HorizontalAlignment = HorizontalAlignment.Right,
                VerticalAlignment = VerticalAlignment.Bottom,
                Orientation = Orientation.Horizontal,
                Margin = "20"
            };

            var buyBtn = new Button { Text = "BUY (Market)", BackgroundColor = Color.DodgerBlue, Margin = "5", Width = 120, Height = 40 };
            var sellBtn = new Button { Text = "SELL (Market)", BackgroundColor = Color.Crimson, Margin = "5", Width = 120, Height = 40 };

            // Wire up the event handlers
            buyBtn.Click += args => ExecuteProtectedTrade(TradeType.Buy);
            sellBtn.Click += args => ExecuteProtectedTrade(TradeType.Sell);

            executionPanel.AddChild(buyBtn);
            executionPanel.AddChild(sellBtn);

            // 4. Add UI to Chart
            Chart.AddControl(executionPanel);
            Chart.AddControl(_dashboardText);
            
            Chart.ObjectsUpdated += Chart_ObjectsUpdated;
            UpdateDashboard();
        }

        public override void OnTick()
        {
            // Keeps math updating as the live spread fluctuates
            UpdateDashboard();
        }

        private void Chart_ObjectsUpdated(ChartObjectsUpdatedEventArgs obj)
        {
            UpdateDashboard();
        }

        private void ExecuteProtectedTrade(TradeType tradeType)
        {
            // Calculate effective distances from the exact tick price upon button click
            double executionPrice = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
            double slPrice = _slLine.Y;
            double tpPrice = _tpLine.Y;

            double slDistPips = Math.Abs(executionPrice - slPrice) / Symbol.PipSize;
            double tpDistPips = Math.Abs(tpPrice - executionPrice) / Symbol.PipSize;
            double spreadPips = Symbol.Spread / Symbol.PipSize;

            if (slDistPips <= 0) return;

            double spreadRatio = (spreadPips / slDistPips) * 100.0;

            // THE GATEKEEPER: Block execution if spread consumes too much of the stop
            if (spreadRatio > MaxSpreadRatio)
            {
                string blockMsg = string.Format("\n[REJECTED] Spread ({0:F1} pips) is {1:F1}% of SL distance.", spreadPips, spreadRatio);
                Print("ORDER BLOCKED: " + blockMsg);
                
                // Flash warning on chart
                Chart.DrawStaticText("WarningText", blockMsg, VerticalAlignment.Center, HorizontalAlignment.Center, Color.Red);
                return;
            }

            Chart.RemoveControl("WarningText");

            // Execute asynchronously to prevent UI thread blocking
            double volumeInUnits = Symbol.QuantityToVolumeInUnits(VolumeInLots);
            ExecuteMarketOrderAsync(tradeType, SymbolName, volumeInUnits, "SpreadProtected", slDistPips, tpDistPips);
        }

        private void UpdateDashboard()
        {
            if (_slLine == null || _tpLine == null) return;

            // Use Ask for a theoretical Buy setup calculation on the dashboard
            double slDistPips = Math.Abs(Symbol.Ask - _slLine.Y) / Symbol.PipSize;
            double tpDistPips = Math.Abs(_tpLine.Y - Symbol.Ask) / Symbol.PipSize;
            double spreadPips = Symbol.Spread / Symbol.PipSize;

            double effSlPips = slDistPips + spreadPips;
            double effTpPips = tpDistPips - spreadPips;

            double nominalRR = slDistPips > 0 ? tpDistPips / slDistPips : 0;
            double effRR = effSlPips > 0 ? effTpPips / effSlPips : 0;
            double spreadRatio = slDistPips > 0 ? (spreadPips / slDistPips) * 100.0 : 0;

            string status = spreadRatio > MaxSpreadRatio ? "[UNSAFE TO EXECUTE]" : "[SAFE TO EXECUTE]";

            _dashboardText.Text = string.Format(
                "--- RISK TERMINAL ---\n" +
                "Live Spread: {0:F1} pips\n" +
                "Spread Cost: {1:F1}% of Stop\n\n" +
                "Nominal R:R: 1 : {2:F2}\n" +
                "Effective R:R: 1 : {3:F2}\n\n" +
                "Status: {4}",
                spreadPips, spreadRatio, nominalRR, effRR, status);
        }
    }
}

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:03 pm
by PTScalper
The Workflow

1.) You identify a 15-minute or 1-minute setup and attach the cBot.

2.) Two draggable lines (Red for Stop, Green for Target) appear, along with big BUY and SELL buttons locked to the bottom right of the chart.

3.) You drag the Red line right beneath your structural sweep.

4.) When you click BUY, the ExecuteProtectedTrade() lambda fires. It grabs the exact tick price at the millisecond of your click, divides the live spread by the stop distance, and checks your MaxSpreadRatio.

5.) If the spread spikes during rollover or a news event precisely when you click the button, the C# logic intercepts it, kills the market order request, and prints a red [REJECTED] warning onto the chart canvas. If the math clears, it passes the order asynchronously to the broker.

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:03 pm
by PTScalper
Calculating dynamic lot sizes is straightforward in cAlgo because the API natively exposes Account.Equity and Symbol.PipValue (which represents the value of one pip for a single unit of volume).

Instead of a fixed lot size, you define your risk as a percentage of your current equity. The logic then divides your absolute risk amount (in your account currency) by the cost of your stop loss distance for a single unit. Finally, it passes that raw unit calculation through Symbol.NormalizeVolumeInUnits to ensure the broker accepts the step size, safely rounding down to prevent violating your maximum risk threshold.

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:03 pm
by PTScalper
1. Update the Parameters

Replace the VolumeInLots parameter with a RiskPercentage input at the top of your class:

Code: Select all

[Parameter("Max Spread/Stop Ratio (%)", DefaultValue = 15.0)]
public double MaxSpreadRatio { get; set; }

[Parameter("Risk Percentage (%)", DefaultValue = 1.0, MinValue = 0.1, Step = 0.1)]
public double RiskPercentage { get; set; }

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:03 pm
by PTScalper
2. Update the Execution Logic

Replace the ExecuteProtectedTrade method with this updated version that calculates the exact position size at the moment you click the button:

Code: Select all

private void ExecuteProtectedTrade(TradeType tradeType)
{
    double executionPrice = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
    double slPrice = _slLine.Y;
    double tpPrice = _tpLine.Y;

    double slDistPips = Math.Abs(executionPrice - slPrice) / Symbol.PipSize;
    double tpDistPips = Math.Abs(tpPrice - executionPrice) / Symbol.PipSize;
    double spreadPips = Symbol.Spread / Symbol.PipSize;

    if (slDistPips <= 0) return;

    double spreadRatio = (spreadPips / slDistPips) * 100.0;

    if (spreadRatio > MaxSpreadRatio)
    {
        string blockMsg = string.Format("\n[REJECTED] Spread ({0:F1} pips) is {1:F1}% of SL.", spreadPips, spreadRatio);
        Print("ORDER BLOCKED: " + blockMsg);
        Chart.DrawStaticText("WarningText", blockMsg, VerticalAlignment.Center, HorizontalAlignment.Center, Color.Red);
        return;
    }

    Chart.RemoveControl("WarningText");

    // --- Dynamic Position Sizing ---
    double riskAmount = Account.Equity * (RiskPercentage / 100.0);
    double costPerUnit = slDistPips * Symbol.PipValue;
    
    if (costPerUnit <= 0) return;

    double rawVolume = riskAmount / costPerUnit;
    double volumeInUnits = Symbol.NormalizeVolumeInUnits(rawVolume, RoundingMode.Down);

    if (volumeInUnits < Symbol.VolumeInUnitsMin)
    {
        string sizeMsg = "\n[REJECTED] Calculated volume is below broker minimum.";
        Print("ORDER BLOCKED: " + sizeMsg);
        Chart.DrawStaticText("WarningText", sizeMsg, VerticalAlignment.Center, HorizontalAlignment.Center, Color.Red);
        return;
    }

    ExecuteMarketOrderAsync(tradeType, SymbolName, volumeInUnits, "SpreadProtected", slDistPips, tpDistPips);
}

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:04 pm
by PTScalper
3. Expose the Calculated Lot Size on the Dashboard

You want to see exactly how many lots the cBot will execute before you click the button. Update the UpdateDashboard method to calculate and display the prospective position size dynamically as you drag the stop loss line around the chart:

Code: Select all

private void UpdateDashboard()
{
    if (_slLine == null || _tpLine == null) return;

    double slDistPips = Math.Abs(Symbol.Ask - _slLine.Y) / Symbol.PipSize;
    double tpDistPips = Math.Abs(_tpLine.Y - Symbol.Ask) / Symbol.PipSize;
    double spreadPips = Symbol.Spread / Symbol.PipSize;

    double effSlPips = slDistPips + spreadPips;
    double effTpPips = tpDistPips - spreadPips;

    double nominalRR = slDistPips > 0 ? tpDistPips / slDistPips : 0;
    double effRR = effSlPips > 0 ? effTpPips / effSlPips : 0;
    double spreadRatio = slDistPips > 0 ? (spreadPips / slDistPips) * 100.0 : 0;

    // --- Dynamic Sizing Preview ---
    double riskAmount = Account.Equity * (RiskPercentage / 100.0);
    double costPerUnit = slDistPips * Symbol.PipValue;
    double volumeInUnits = costPerUnit > 0 ? Symbol.NormalizeVolumeInUnits(riskAmount / costPerUnit, RoundingMode.Down) : 0;
    double volumeInLots = Symbol.VolumeInUnitsToQuantity(volumeInUnits);

    string status = spreadRatio > MaxSpreadRatio ? "[UNSAFE TO EXECUTE]" : "[SAFE TO EXECUTE]";

    _dashboardText.Text = string.Format(
        "--- RISK TERMINAL ---\n" +
        "Live Spread: {0:F1} pips\n" +
        "Spread Cost: {1:F1}% of Stop\n\n" +
        "Target Risk: {2:F1}%\n" +
        "Proj. Size: {3:F2} Lots\n\n" +
        "Nominal R:R: 1 : {4:F2}\n" +
        "Effective R:R: 1 : {5:F2}\n\n" +
        "Status: {6}",
        spreadPips, spreadRatio, RiskPercentage, volumeInLots, nominalRR, effRR, status);
}

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:04 pm
by PTScalper
Now, as you drag the Red stop loss line tighter to your entry, the Proj. Size on your dashboard will seamlessly increase the lot size to maintain exactly 1% (or whatever you set) of your account equity. If you drag it wider, the lot size drops.

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:05 pm
by PTScalper
Handling partial closures dynamically requires managing state. Because OnTick() fires continuously, the main challenge is preventing the cBot from triggering multiple times—closing half, then half of the remainder, over and over until the position vanishes.

The cleanest C# implementation is to use a HashSet<int> to store the Position.Id of any trade that has already had its partial taken. When price hits the 1:1 mark, you split the volume, execute the partial close, optionally roll the Stop Loss to breakeven, and register the ID so the bot ignores it on future ticks.

Here is how to patch this into your existing SpreadProtectedTerminal class.

Re: Mistake: Ignoring Spreads When Choosing Setups

Posted: Tue Oct 06, 2026 10:05 pm
by PTScalper
1. Add the State Tracker

At the top of your file, ensure you have the collections namespace, and declare the HashSet inside your class variables:

Code: Select all

using System.Collections.Generic; // Add to top of file if missing

// ... inside the SpreadProtectedTerminal class:
private HashSet<int> _partiallyClosedPositions = new HashSet<int>();