Mistake: Ignoring Spreads When Choosing Setups
Re: Mistake: Ignoring Spreads When Choosing Setups
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
cTrader Version: Spread-Protected Execution cBot (C#)
Open cTrader Automate, create a new cBot, and paste this.
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);
}
}
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
1. Update the Parameters
Replace the VolumeInLots parameter with a RiskPercentage input at the top of your class:
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; }Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
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:
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);
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
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:
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);
}Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
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.
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.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: Mistake: Ignoring Spreads When Choosing Setups
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:
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>();Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.