Quick Answer
In Pine Script, strategy.entry() does more than open a trade. With the default settings it also closes an opposite position first (a reversal), it refuses a second entry when the pyramiding limit is reached, and it fills at the open of the next bar. In a cBot none of this happens by itself. ExecuteMarketOrder() simply opens a position, so you rebuild each rule in C#.
The pieces are: one OnBar() handler that sends the order at the open of the new bar, a check that closes the opposite positions of your label, a count of open positions as the pyramiding limit, PlaceLimitOrder() or PlaceStopOrder() for pending entries, and a volume in units that comes from your risk, not from a contract count.
This guide covers those rules. The basics of the conversion (parameters, indicators, Last(1), lots versus units, pips versus ticks) are in Pine Script to cTrader C#: How a Strategy Becomes a cBot.
From the author: I convert Pine Script and MQL5 strategies to C# cBots. The part that surprises people most is not the indicator code, it is the entry behavior. A Pine strategy that "just reverses" on every signal becomes a bot that stacks positions or never exits the other side, because nobody wrote the rules that
strategy.entrywas quietly applying. This guide writes those rules out.
Key Facts
| Pine Script strategy.entry | cTrader C# |
|---|---|
| Market entry | ExecuteMarketOrder(TradeType, SymbolName, volume, label, slPips, tpPips) |
| Entry with limit | PlaceLimitOrder(...) |
| Entry with stop | PlaceStopOrder(...) |
| Entry id | label for the bot, comment for the signal name |
| Opposite entry reverses the position | Close your positions on the other side, then open |
| pyramiding limit | Count open positions of your label before you open |
| Same id replaces the pending order | Cancel the old pending order, then place the new one |
| Fills at the next bar open | Send the order in OnBar(), which runs when the new bar opens |
| qty or percent of equity | Calculate the volume in units from the risk and the stop distance |
What strategy.entry Does in Pine
Take this strategy with the default settings (pyramiding is 0, so one entry per direction):
//@version=5
strategy("EMA cross", overlay = true)
fast = ta.ema(close, 10)
slow = ta.ema(close, 30)
if ta.crossover(fast, slow)
strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
strategy.entry("Short", strategy.short)
When the fast line crosses below the slow line while the strategy is long, strategy.entry("Short", ...) does two things in one call: it closes the long position and it opens the short. The same call, repeated on the next bar, does nothing, because a short is already open and the pyramiding limit is reached. A cBot needs to do both on purpose.
Rule 1: When the Order Is Sent
By default, a Pine strategy calculates on the close of a bar and fills the order at the open of the next bar. In a cBot, OnBar() runs when a new bar opens, so an order sent there goes out at about the same moment. That makes OnBar() the natural place for entries. Read the last closed bar with Last(1) and the bar before it with Last(2), as in the basic conversion guide.
The fill itself still differs: the market order is filled at the current ask or bid, with the real spread and possible slippage, while a Pine fill uses the open price of the chart data. The same idea, once-per-bar logic and closed-bar values, is explained for MetaTrader in Pine Script barstate.isnew in MQL5.
Rule 2: Reversal
To copy a reversal, close the positions on the other side that belong to your cBot, and then open the new one. Use the label and the symbol to find your own positions, so the bot never touches trades that are not its own:
private void CloseSide(TradeType side)
{
var toClose = Positions
.Where(p => p.Label == Label && p.SymbolName == SymbolName && p.TradeType == side)
.ToList();
foreach (var position in toClose)
{
var result = ClosePosition(position);
if (!result.IsSuccessful)
Print("Close failed: {0}", result.Error);
}
}
This needs using System.Linq; at the top of the file. Without it, the compiler reports CS1061 on Where and ToList, which is explained in cTrader C# error CS1061. The ToList() is deliberate: closing a position changes the Positions collection, so copy the matching positions first and then loop over the copy.
Account type matters here. On a hedging account, an opposite order opens a second position, so you must close the first one yourself, as above. On a netting account, an opposite order reduces or reverses the existing position. The broker decides the account type, so check it before you rely on either behavior. For closing positions in detail, see Pine Script strategy.close to MQL5, where the same ideas apply.
Rule 3: The Pyramiding Limit
strategy.entry stops adding positions when the pyramiding limit is reached. In a cBot, count the open positions of your label and symbol, and return when the limit is reached:
[Parameter("Max positions", DefaultValue = 1, MinValue = 1)]
public int MaxPositions { get; set; }
private int OpenCount()
{
return Positions.Count(p => p.Label == Label && p.SymbolName == SymbolName);
}
Call OpenCount() after the reversal step. If the count is already at the limit, do not open another position.
Rule 4: Limit and Stop Entries
A strategy.entry with limit or stop becomes a pending order. In cTrader these are PlaceLimitOrder() and PlaceStopOrder(). The target price is an absolute price, and the stop loss and take profit are in pips:
double entryPrice = Symbol.Bid - 10 * Symbol.PipSize; // a buy limit 10 pips below the bid
var result = PlaceLimitOrder(
TradeType.Buy, SymbolName, volume, entryPrice, Label, StopLossPips, TakeProfitPips);
if (!result.IsSuccessful)
Print("Pending order failed: {0}", result.Error);
In Pine, calling strategy.entry again with the same id replaces the pending order. In a cBot, an old pending order stays until you cancel it, so look for it first:
private void CancelPending()
{
var old = PendingOrders
.Where(o => o.Label == Label && o.SymbolName == SymbolName)
.ToList();
foreach (var order in old)
CancelPendingOrder(order);
}
Call CancelPending() before you place a new pending order, and when the signal that created the order is no longer valid.
Rule 5: Volume From Risk, Not From Contracts
Pine sizes trades with qty or with a percent of equity. A cBot needs a volume in units. A common way is to start from the amount you accept to lose and the stop distance:
[Parameter("Risk %", DefaultValue = 1.0, MinValue = 0.1, MaxValue = 5.0)]
public double RiskPercent { get; set; }
private double VolumeFromRisk(double stopLossPips)
{
double riskMoney = Account.Balance * RiskPercent / 100.0;
double units = riskMoney / (stopLossPips * Symbol.PipValue);
return Symbol.NormalizeVolumeInUnits(units, RoundingMode.Down);
}
Symbol.PipValue is the value of one pip for one unit of volume, in the account currency. Rounding down keeps the trade at or under the amount you chose. Check the result against the minimum and maximum volume of the symbol, and print it before you trade. The same method for MetaTrader, with the formulas, is in MQL5 lot size calculation.
Full cBot Example
This cBot opens a buy when the fast EMA crosses above the slow EMA, and a sell when it crosses below. It closes the opposite side first, respects a maximum number of positions, and sizes the volume from the risk. It is the example from the basic guide, extended with the three rules above.
using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class PineEntryRules : 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("Risk %", DefaultValue = 1.0, MinValue = 0.1, MaxValue = 5.0)]
public double RiskPercent { 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("Max positions", DefaultValue = 1, MinValue = 1)]
public int MaxPositions { get; set; }
private const string Label = "PineEntryRules";
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 crossDown = fast1 < slow1 && fast2 >= slow2;
if (crossUp)
Enter(TradeType.Buy, TradeType.Sell);
else if (crossDown)
Enter(TradeType.Sell, TradeType.Buy);
}
// 'side' is the new trade, 'opposite' is the side that a reversal closes
private void Enter(TradeType side, TradeType opposite)
{
CloseSide(opposite);
if (OpenCount() >= MaxPositions)
return;
double volume = VolumeFromRisk(StopLossPips);
if (volume <= 0)
{
Print("Volume is zero. Check the risk, the stop and the balance.");
return;
}
var result = ExecuteMarketOrder(side, SymbolName, volume, Label, StopLossPips, TakeProfitPips);
if (!result.IsSuccessful)
Print("Order failed: {0}", result.Error);
}
private void CloseSide(TradeType side)
{
var toClose = Positions
.Where(p => p.Label == Label && p.SymbolName == SymbolName && p.TradeType == side)
.ToList();
foreach (var position in toClose)
{
var result = ClosePosition(position);
if (!result.IsSuccessful)
Print("Close failed: {0}", result.Error);
}
}
private int OpenCount()
{
return Positions.Count(p => p.Label == Label && p.SymbolName == SymbolName);
}
private double VolumeFromRisk(double stopLossPips)
{
double riskMoney = Account.Balance * RiskPercent / 100.0;
double units = riskMoney / (stopLossPips * Symbol.PipValue);
return Symbol.NormalizeVolumeInUnits(units, RoundingMode.Down);
}
}
}
Run it in the cTrader backtester on a short period first, and compare the first few entries with the signals of your Pine strategy. Check that a reversal closes the old position, that the number of open positions never goes above the limit, and that the volume matches the risk you set. A risk-based volume looks right on paper, so print it and check one trade by hand. Exits are covered in Pine Script strategy.exit to cTrader C#, including breakeven and trailing stops.
What Does Not Translate
Exact fills. A Pine backtest fills at chart prices and assumes no spread. A cBot trades at the real ask and bid, with spread and slippage, so entry prices and results differ.
Bar magnifier and intrabar logic. If the Pine strategy uses calc_on_every_tick or intrabar settings, the order of events inside a bar is different from a cBot that acts once per bar.
Orders without an id. Pine tracks entries by id. A cBot tracks positions by label, symbol and, if you need it, comment. Decide how you want to name your signals before you convert.
The causes of different results are collected in Why a Pine Script Strategy Gives Different Results in MQL5, and most of them apply to cTrader as well.
Common Mistakes
- Opening a new position on every signal. There is no pyramiding limit unless you write one.
- Not closing the opposite side. A "reversal" ends up with a long and a short at once on a hedging account.
- Looping over
Positionswhile closing. Copy the matching positions first withToList(). - Leaving old pending orders. Cancel them when you place a new one or when the signal is gone.
- Using a fixed volume. Pine's percent of equity needs a calculated volume.
- Not rounding the volume to the symbol's step. Use
NormalizeVolumeInUnits. - Sending entries from
OnTick(). UseOnBar()for closed-bar signals. - Skipping the label. Without it the bot cannot find its own positions.
- Forgetting
using System.Linq;.Where,Countwith a condition andToListneed it.
FAQ
Related Guides
- Pine Script to cTrader C#: the basics of the conversion, with a full EMA cross cBot.
- Pine Script strategy.exit to cTrader C#: stop loss, take profit, breakeven and trailing stop for the same cBot.
- Pine Script strategy.entry to MQL5 CTrade: the same entry mapping for MetaTrader.
- Pine Script strategy.close to MQL5: close positions safely.
- Pine Script strategy.exit to MQL5: stop loss, take profit and trailing exits.
- MQL5 lot size calculation: position sizing from risk.
- 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 Entry Logic Without Guessing
Entry rules are where converted strategies drift: one missing reversal, one missing limit, and the bot trades differently from the script. 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.