Blog
Pine Script to cTrader C#: How a Strategy Becomes a cBot
Quick Answer
A Pine Script strategy becomes a cBot, which is a C# class that inherits from Robot in cTrader Automate. The Pine script's inputs turn into [Parameter] properties, its ta.* calls turn into indicators created in OnStart(), and its once-per-bar logic goes into OnBar().
The bar rule is the same as in MQL5. When OnBar() runs, the bar that just closed is Last(1), so Pine's close maps to Bars.ClosePrices.Last(1) and Pine's close[1] maps to Bars.ClosePrices.Last(2).
Key Facts
| Pine Script | cTrader Automate (C#) |
|---|---|
| input.int(10) | [Parameter("Fast", DefaultValue = 10)] public int Fast { get; set; } |
| close (logic on bar close) | Bars.ClosePrices.Last(1) |
| close[1] | Bars.ClosePrices.Last(2) |
| ta.ema(close, n) | Indicators.ExponentialMovingAverage(Bars.ClosePrices, n) |
| ta.rsi(close, n) | Indicators.RelativeStrengthIndex(Bars.ClosePrices, n) |
| strategy.entry("Long", strategy.long) | ExecuteMarketOrder(TradeType.Buy, SymbolName, volume, "Long") |
| strategy.exit with loss and profit | Stop loss and take profit arguments, or ModifyPosition |
| strategy.close | ClosePosition(position) |
| Logic once per bar | protected override void OnBar() |
| request.security | MarketData.GetBars(TimeFrame.Hour4) |
Always confirm the exact method names and signatures in the current cTrader Automate documentation, because the API evolves between versions.
The Pine Script Version
//@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)
How Bar Indexing Works in cTrader
In cTrader Automate, a price series such as Bars.ClosePrices is read with Last(n):
Last(0)is the newest bar, which is still forming.Last(1)is the last closed bar.Last(2)is the bar before that.
OnBar() runs when a new bar opens, so at that moment the bar that Pine would call "current" is Last(1), and its [1] is Last(2). This is the same off-by-one rule as in MQL5, where the last closed bar is shift 1. If you want the full explanation of why, read Pine Script close[1] in MQL5, and map shift 1 to Last(1).
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 keeps one position at a time and sets a stop loss and a take profit in pips.
using System;
using cAlgo.API;
using cAlgo.API.Indicators;
namespace cAlgo.Robots
{
[Robot(AccessRights = AccessRights.None)]
public class PineEmaCross : 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 (lots)", DefaultValue = 0.1, MinValue = 0.01)]
public double Lots { 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; }
private const string Label = "PineEmaCross";
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()
{
// Last(1) is the last closed bar (Pine: current bar).
// Last(2) is the bar before it (Pine: [1]).
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; // ta.crossover
bool crossDown = fast1 < slow1 && fast2 >= slow2; // ta.crossunder
if (crossUp)
Open(TradeType.Buy);
else if (crossDown)
Open(TradeType.Sell);
}
private void Open(TradeType tradeType)
{
// One position at a time for this label and symbol
if (Positions.FindAll(Label, SymbolName).Length > 0)
return;
double volume = Symbol.QuantityToVolumeInUnits(Lots);
volume = Symbol.NormalizeVolumeInUnits(volume);
var result = ExecuteMarketOrder(
tradeType, SymbolName, volume, Label, StopLossPips, TakeProfitPips);
if (!result.IsSuccessful)
Print("Order failed: {0}", result.Error);
}
}
}
The crossover condition matches Pine's ta.crossover: the fast line is above the slow line on the last closed bar, and was at or below it on the bar before.
Volume: Lots vs Units
Pine strategies size trades in contracts or percent of equity. A cBot's ExecuteMarketOrder takes the volume in units, not lots. Convert lots with Symbol.QuantityToVolumeInUnits(lots) and round to a valid size with Symbol.NormalizeVolumeInUnits(...), as in the example. Sending a raw lot number such as 0.1 as the volume would be an extremely small order, and it would likely be rejected.
Stop Loss and Take Profit: Pips, Not Ticks
Pine's loss and profit in strategy.exit are measured in ticks, or as prices. The ExecuteMarketOrder overload in the example takes the stop loss and take profit in pips. A pip and a tick are different units, so convert before you reuse a number from Pine. Symbol.PipSize and Symbol.TickSize give you the two sizes. If you need an exact price level, set it with ModifyPosition on the open position instead.
Common Mistakes
- Using
Last(0)for a closed-bar rule. It is the forming bar and changes with every tick. - Passing lots as volume. The API wants units.
- Treating pips and ticks as the same. The stop ends up the wrong size.
- Creating indicators inside
OnBar(). Create them once inOnStart(). - Not checking for
NaN. Early bars may not have indicator values yet. - Forgetting a label. Without a label you cannot tell your cBot's positions from others.
- Expecting identical results to TradingView. Different data, fills and spreads change results. See why a Pine Script strategy gives different results.
Which Platform Should You Pick?
If you are deciding between targets, the Pine Script vs MQL5 comparison covers the MetaTrader side. For the step-by-step process of moving a strategy across platforms, see the Pine Script to MQL5 guide. The same ideas (bar shifts, once-per-bar logic, unit conversion) carry over to cTrader.
FAQ
Can I convert Pine Script to cTrader?
Yes. A Pine strategy can be rewritten as a cBot in C#. Inputs become parameters, indicators are created in OnStart(), and the trading logic runs in OnBar().
What is the cTrader equivalent of close[1]?
Bars.ClosePrices.Last(1) is the last closed bar. If your logic runs in OnBar() and the Pine script reads close, use Last(1) for close and Last(2) for close[1].
Does cTrader use lots or units?
Order volume in a cBot is in units. Convert lots with Symbol.QuantityToVolumeInUnits.
Are Pine ticks the same as cTrader pips?
No. Convert them using the symbol's tick size and pip size.
How do I run logic once per bar in cTrader?
Put it in OnBar(), which runs when a new bar opens.
Skip the Manual Rewrite
Rewriting each Pine call, shift, unit and order by hand is slow, and small mistakes change the signals. CodeFlowOS translates strategies between Pine Script, MQL5 and cTrader C#, and lets you review the generated code before you test it. You can try it on the converter page.
This article is educational. It does not describe trading performance and is not financial advice. Always build and test a cBot in cTrader's backtester before you use it on a live account.