Quick Answer
In Pine Script, strategy.exit() attaches a stop loss, a take profit or a trailing stop to an entry, and the strategy engine manages it for you. In a cTrader cBot you have two options. For a fixed stop and target, pass the distances in pips to ExecuteMarketOrder(), or set them afterwards with ModifyPosition(). For a breakeven or a trailing stop that follows your own rules, check the open position in OnTick() or OnBar() and move the stop yourself.
The usual mistake is units. Pine's loss, profit and trail_offset are in ticks, while the cTrader stop loss and take profit parameters are in pips. On a five-digit forex symbol one pip is ten ticks, so a copied number is ten times too small or too large.
The basics of the conversion (parameters, indicators, Last(1), pips versus ticks) are in Pine Script to cTrader C#: How a Strategy Becomes a cBot, the entry side (reversals, pyramiding, pending orders) is in Pine Script strategy.entry to cTrader C#, and the MetaTrader version of this guide is Pine Script strategy.exit to MQL5.
From the author: I convert Pine Script strategies to C# cBots. Exits cause more wrong backtests than entries do, because the Pine exit looks like one line but hides three behaviors: the unit (ticks), the price it is calculated from, and the moment it is checked. This guide writes those behaviors out in C#.
Key Facts
| Pine Script strategy.exit | cTrader C# |
|---|---|
| loss / profit (ticks from the entry price) | stopLossPips / takeProfitPips in ExecuteMarketOrder() |
| stop / limit (absolute prices) | ModifyPosition(position, stopLossPrice, takeProfitPrice) |
| trail_points / trail_offset (ticks) | A trailing stop you move in code, or the position's trailing stop option |
| from_entry (which entry to exit) | The label of the position |
| Breakeven (own logic with strategy.position_avg_price) | Check position.EntryPrice and position.Pips, then move the stop |
| Exit is evaluated by the strategy engine | Your OnTick() / OnBar() code, or the server-side SL and TP |
| Ticks | Pips. Convert with Symbol.TickSize and Symbol.PipSize |
The Pine Script Version
//@version=5
strategy("EMA cross with exit", overlay = true)
fast = ta.ema(close, 10)
slow = ta.ema(close, 30)
if ta.crossover(fast, slow)
strategy.entry("Long", strategy.long)
strategy.exit("Exit Long", from_entry = "Long", loss = 200, profit = 400)
loss = 200 and profit = 400 are in ticks. On EURUSD with five digits that is 20 pips and 40 pips. If you type 200 and 400 as pips in the cBot, you get a 200-pip stop and a 400-pip target, which is a very different strategy.
Rule 1: Convert Ticks to Pips
Do the conversion from the symbol, not from a guess, because the ratio is different on JPY pairs, metals and indices:
private double TicksToPips(double ticks)
{
return ticks * Symbol.TickSize / Symbol.PipSize;
}
On a five-digit EURUSD, TickSize is 0.00001 and PipSize is 0.0001, so 200 ticks give 20 pips. Print the result for your symbol before you trade, and keep your parameters in pips in the cBot, because that is what the platform uses.
Rule 2: A Fixed Stop Loss and Take Profit
The simplest exit is to send the distances with the order:
var result = ExecuteMarketOrder(
TradeType.Buy, SymbolName, volume, Label, StopLossPips, TakeProfitPips);
if (!result.IsSuccessful)
Print("Order failed: {0}", result.Error);
The stop and the target then live on the server, so they work even if your cBot is stopped or the connection drops. This is the closest match to strategy.exit with loss and profit.
If Pine gives you absolute prices (stop and limit), set them after the order is open:
var position = result.Position;
double sl = position.EntryPrice - 20 * Symbol.PipSize; // example: 20 pips below
double tp = position.EntryPrice + 40 * Symbol.PipSize; // example: 40 pips above
ModifyPosition(position, sl, tp);
For a sell the signs are reversed. A stop on the wrong side of the price is rejected, so check the trade type before you calculate it.
Rule 3: Move the Stop to Breakeven
Pine has no built-in breakeven. A script usually checks the profit and calls strategy.exit again with a new stop. In a cBot, check the open positions of your label and move the stop once the trade is far enough in profit:
[Parameter("Breakeven trigger (pips)", DefaultValue = 15, MinValue = 1)]
public double BreakevenTriggerPips { get; set; }
[Parameter("Breakeven offset (pips)", DefaultValue = 1, MinValue = 0)]
public double BreakevenOffsetPips { get; set; }
private void ManageBreakeven()
{
foreach (var position in Positions.Where(p => p.Label == Label && p.SymbolName == SymbolName))
{
if (position.Pips < BreakevenTriggerPips)
continue;
double offset = BreakevenOffsetPips * Symbol.PipSize;
double newSl = position.TradeType == TradeType.Buy
? position.EntryPrice + offset
: position.EntryPrice - offset;
bool better = position.StopLoss == null ||
(position.TradeType == TradeType.Buy ? newSl > position.StopLoss : newSl < position.StopLoss);
if (better)
ModifyPosition(position, newSl, position.TakeProfit);
}
}
The better check makes sure the stop only moves in the profit direction and is not set again on every tick. The offset covers the spread and commission, so a breakeven trade is not a small loss. position.StopLoss can be empty (null) when no stop is set, so test it before you compare. The Where call needs using System.Linq;; without it the compiler reports CS1061, see cTrader C# error CS1061.
Rule 4: Trailing Stop
Pine's trail_points is the profit in ticks that starts the trail and trail_offset is the distance that the stop follows behind the price. In a cBot you can write the same rule yourself, which also lets you change it later:
[Parameter("Trail start (pips)", DefaultValue = 20, MinValue = 1)]
public double TrailStartPips { get; set; }
[Parameter("Trail distance (pips)", DefaultValue = 10, MinValue = 1)]
public double TrailDistancePips { get; set; }
private void ManageTrailing()
{
foreach (var position in Positions.Where(p => p.Label == Label && p.SymbolName == SymbolName))
{
if (position.Pips < TrailStartPips)
continue;
double distance = TrailDistancePips * Symbol.PipSize;
double newSl = position.TradeType == TradeType.Buy
? Symbol.Bid - distance
: Symbol.Ask + distance;
bool better = position.StopLoss == null ||
(position.TradeType == TradeType.Buy ? newSl > position.StopLoss : newSl < position.StopLoss);
if (better)
ModifyPosition(position, newSl, position.TakeProfit);
}
}
A buy position closes at the bid, so the trail follows the bid. A sell closes at the ask, so it follows the ask. The better check gives the ratchet behavior: the stop only moves toward the price, never away from it.
cTrader also has a built-in trailing stop option on the position. If you use it, do not run your own trailing code on the same position, because the two will fight over the stop. The same idea for MetaTrader, with its rules for stop distance, is in MQL5 trailing stop EA.
Rule 5: When Is the Exit Checked
Pine checks strategy.exit on the strategy engine's own schedule, and a stop is filled at the chart price. A server-side stop in cTrader is checked by the broker on every price update, and the fill can include spread and slippage. Code that moves a stop (breakeven, trailing) runs only when your handler runs:
- Put it in
OnTick()if you want the stop to follow the price closely. It runs often, so keep it light. - Put it in
OnBar()if you only need to update once per bar, as a strategy that works on closed bars does.
The closed-bar idea, once per bar and no repeated work, is explained for MetaTrader in Pine Script barstate.isnew in MQL5.
Full cBot Example
This cBot buys when the fast EMA crosses above the slow EMA. It sends a stop loss and take profit with the order, moves the stop to breakeven after a profit of BreakevenTriggerPips, and then trails it. The entry side (reversal, pyramiding limit, risk-based volume) is left out on purpose, and is written in Pine Script strategy.entry to cTrader C#.
using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class PineExitRules : Robot
{
[Parameter("Fast Period", DefaultValue = 10, MinValue = 1)]
public int FastPeriod { get; set; }
[Parameter("Slow Period", DefaultValue = 30, MinValue = 2)]
public int SlowPeriod { get; set; }
[Parameter("Volume (units)", DefaultValue = 1000, MinValue = 1)]
public double VolumeUnits { get; set; }
[Parameter("Stop Loss (pips)", DefaultValue = 20, MinValue = 1)]
public double StopLossPips { get; set; }
[Parameter("Take Profit (pips)", DefaultValue = 40, MinValue = 1)]
public double TakeProfitPips { get; set; }
[Parameter("Breakeven trigger (pips)", DefaultValue = 15, MinValue = 1)]
public double BreakevenTriggerPips { get; set; }
[Parameter("Breakeven offset (pips)", DefaultValue = 1, MinValue = 0)]
public double BreakevenOffsetPips { get; set; }
[Parameter("Trail start (pips)", DefaultValue = 25, MinValue = 1)]
public double TrailStartPips { get; set; }
[Parameter("Trail distance (pips)", DefaultValue = 10, MinValue = 1)]
public double TrailDistancePips { get; set; }
private const string Label = "PineExitRules";
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
protected override void OnStart()
{
if (FastPeriod >= SlowPeriod)
{
Print("Fast period must be smaller than slow period.");
Stop();
return;
}
_fast = Indicators.ExponentialMovingAverage(Bars.ClosePrices, FastPeriod);
_slow = Indicators.ExponentialMovingAverage(Bars.ClosePrices, SlowPeriod);
}
protected override void OnBar()
{
double fast1 = _fast.Result.Last(1);
double fast2 = _fast.Result.Last(2);
double slow1 = _slow.Result.Last(1);
double slow2 = _slow.Result.Last(2);
if (double.IsNaN(fast1) || double.IsNaN(fast2) ||
double.IsNaN(slow1) || double.IsNaN(slow2))
return;
bool crossUp = fast1 > slow1 && fast2 <= slow2;
bool hasPosition = Positions.Any(p => p.Label == Label && p.SymbolName == SymbolName);
if (crossUp && !hasPosition)
{
var volume = Symbol.NormalizeVolumeInUnits(VolumeUnits, RoundingMode.Down);
var result = ExecuteMarketOrder(
TradeType.Buy, SymbolName, volume, Label, StopLossPips, TakeProfitPips);
if (!result.IsSuccessful)
Print("Order failed: {0}", result.Error);
}
}
protected override void OnTick()
{
foreach (var position in Positions.Where(p => p.Label == Label && p.SymbolName == SymbolName))
{
ManagePosition(position);
}
}
private void ManagePosition(Position position)
{
if (position.Pips < BreakevenTriggerPips)
return;
double newSl;
if (position.Pips >= TrailStartPips)
{
double distance = TrailDistancePips * Symbol.PipSize;
newSl = position.TradeType == TradeType.Buy
? Symbol.Bid - distance
: Symbol.Ask + distance;
}
else
{
double offset = BreakevenOffsetPips * Symbol.PipSize;
newSl = position.TradeType == TradeType.Buy
? position.EntryPrice + offset
: position.EntryPrice - offset;
}
bool better = position.StopLoss == null ||
(position.TradeType == TradeType.Buy ? newSl > position.StopLoss : newSl < position.StopLoss);
if (better)
{
var result = ModifyPosition(position, newSl, position.TakeProfit);
if (!result.IsSuccessful)
Print("Modify failed: {0}", result.Error);
}
}
}
}
Run it in the cTrader backtester on a short period. Check that the first order carries the stop and target you set, that the stop moves to breakeven after the trigger, and that it only moves in the profit direction. Print the stop before and after one move and check it by hand.
What Does Not Translate
The Pine fill model. Pine fills an exit at the chart price it uses for the bar. A cBot exit is filled by the broker at the real bid or ask, so the spread, slippage and a gap can change the result.
Intrabar order of events. If a bar touches both the stop and the target, Pine uses its own assumption about which came first. A live cBot trades what actually happened tick by tick, so the two can disagree.
strategy.exit with several levels. One Pine exit can hold a stop, a target and a trail. In a cBot these are separate pieces: the server-side stop and target, plus your code for the trail.
The causes of different results between Pine and an expert advisor are collected in Why a Pine Script Strategy Gives Different Results in MQL5, and most apply to cTrader too.
Common Mistakes
- Using ticks as pips. A copied
loss = 200becomes a 200-pip stop. - Putting the stop on the wrong side. A buy needs a stop below the price, a sell needs it above.
- Moving the stop away from the price. Check that the new stop is better than the old one.
- Not handling a missing stop.
position.StopLossis empty when no stop is set. - Running two trailing systems. The built-in trailing stop and your own code fight each other.
- Trailing from the wrong price. A buy follows the bid and a sell follows the ask.
- Doing the work in the wrong handler. Use
OnTick()for a close trail andOnBar()for closed-bar logic. - Forgetting the label. Without it the bot touches positions that are not its own.
FAQ
Related Guides
- Pine Script to cTrader C#: the basics of the conversion, with a full EMA cross cBot.
- Pine Script strategy.entry to cTrader C#: reversals, the pyramiding limit and risk-based volume for the same cBot.
- Pine Script strategy.exit to MQL5: the same exit mapping for MetaTrader.
- MQL5 trailing stop EA: trailing stop rules and the stop distance limits.
- Pine Script strategy.close to MQL5: close positions safely.
- MQL5 invalid stops error 10016: why a stop is rejected, which also applies to a stop on the wrong side.
- cTrader C# error CS1061: the error behind a missing
using System.Linq;. - What to check in a trading bot before going live: a pre-live checklist for stops, lot size, spread and trade results.
Convert Exit Logic Without Guessing
Exits are where converted strategies drift: one tick-to-pip mistake and the stop is ten times too wide. CodeFlowOS translates strategies between Pine Script, MQL5 and cTrader C#, and lets you review the generated code before you test it. The platform compile check is optional and outputs are not guaranteed, so always review the result. If the cBot does not build, the Error Fixer can suggest a fix (1 credit per fix, up to 3 attempts), and the free early-bird Code Review checks a cBot for common mistakes such as a missing stop loss or a missing label. Try the converter page.
This article is educational. It does not describe trading performance and is not financial advice. Always build the code in cTrader Automate and backtest it before using it on a live account.