James Bachini

Pine Script Tutorial | How To Develop Real Trading Strategies On TradingView

Pine Script Tutorial

In this pine script tutorial I’ll be showing you how to get started with TradingView scripting for technical analysis and trading strategy development. We will start by looking at how pine script works and a simple example. From there we will move on to inputs and indicators before creating a complete trading strategy using pine script. Finally we will look at how to backtest, execute and publish pine script indicators and strategies.

  1. Developing Pine Script Trading Strategies [Video]
  2. How Pine Script Works On TradingView
  3. Your First Pine Script Overlay
  4. Pine Script Basics
  5. Pine Script Built In Functions
  6. Creating A Pine Script Trading Strategy
  7. Backtesting Pine Script Strategies
  8. How & Why Publish TradingView Pine Scripts
  9. Trade Execution & Strategy Deployment

Developing Pine Script Trading Strategies

Note that Pinescript v4 was used in the video, now Pinescript v5 has been released I’ve updated the code in the article below with the main difference being namespacing i.e. sma becomes ta.sma

James On YouTube

How Pine Script Works On TradingView

Pine script is the native coding language of TradingView. It’s used widely for technical analysis and algo trading strategy development.

Pine script is quite similar to Python in it’s format and layout. Developers familiar with Python or any other scripting language shouldn’t have much difficulty getting up to speed. There are some important considerations that need to be addressed before we get started.

Pine script executes once for each candle of a chart on what is known as series data. The code that you write is executed once for each data point in the series data. There might be a thousand data points (1 data point = 1 candle) on a standard chart and the code will iterate over itself each time. So when you call the plot(close) function in pine script it draws a line at the close price for each data point.

When you change the timeframe on the chart the data changes and the indicator or strategy will change completely. A 30 minute moving average is very different to a 30 day moving average and this is normally set on the chart not within the script itself.

There are two types of pine script formats indicators and strategies. Indicators are used for technical analysis to draw lines and patterns on charts. Strategies are used to develop and back test trading strategies. An indicator might be used by a trader looking to better understand the current price movements of a particular asset. A strategy might be developed to take advantage of a particular market movement or opportunity.

The free version of TradingView allows you to have up to 3 indicators on a chart at any one time. There are paid versions available as well. The Pro version allows up to 5 indicators @ $15/month and the Pro+ version up to 10 indicators @ $30/month. The paid versions also have a lot of additional features.

Check the TradingView | Go Pro Page for details on the split-screen/alerts/features and current prices.


Your First Pine Script Overlay

Let’s look at some example code for an indicator to get stuck in.

A simple moving average indicator
//@version=5
indicator('First Pine Script', overlay=true)
fast = ta.sma(close, 24)
slow = ta.sma(close, 200)
plot(fast, color=color.new(color.blue, 0))
plot(slow, color=color.new(color.yellow, 0))
  • The first line declares we are using the latest version 4 of pine script.
  • The study function declares it’s an indicator, gives it a name and sets it to overlay rather than add a separate window at the bottom of the chart.
  • We then set two variables using the built in sma() function (simple moving average). We set the fast variable to a moving average with a period of 24 and the slow variable to a period of 200. This will look back and calculate the average of the last 24 and 200 closing prices for each data point.
  • Finally we use the plot() function to print these on to the chart with different colours.

Try opening up the pine editor, adding this in and then clicking “add to chart”. You should see two lines printed on your chart for the moving averages.


Pine Script Basics

This is half introduction, half cheat sheet to get up to speed as quickly as possible before we go through some more in depth examples. It assumes some basic programming knowledge in other languages.

Pine script at it’s core just takes in time series data, passes that data through functions and outputs it as a strategy or indicator.

Functions can either be user specified or fortunately pine script comes with the vast majority of functions you’ll likely need built in. To get the simple moving average for the last 14 bar closes you can use:
sma1 = ta.sma(close,14)

Data sources or inputs are in the series format and generally available for:
open, high, low, close, volume, time

You can access the previous value in a series using the format:
close[-1]

UPDATE 10th April 2021
Pine script has introduced a new function to allow for variables to be stored between candles. You can now use varip to keep running counts and retain data across each execution or candle:
varip int count = 0

Most indicators will be customisable without digging into the code. This is done by adjusting the inputs using the little cog next to the indicator name (hover mouse over towards the top left of the chart). The code for setting variables based on inputs looks like this:
myInput1 = input(title=”Click To Turn Off”, type=input.bool, defval=true)
myInput2 = input(title=”Chance Of Success(%)”, type=input.float, defval=1, minval=1, step=0.1)
myInput3 = input(title=”Choose An Option”, defval=”A”, options=[“A”, “B”, “C”])

As default pine script will execute at the close of every candle as opposed to on each tick or price movement. To change this set the following:
calc_on_every_tick=true

Alerts can be used to send a notification or to send trades to an external API. Paid plans come with server-side alerts which can be setup to send out a message without needing to be logged in.
alert(“Wake Up”, alert.freq_once_per_bar_close)

The following data types are available:
int = integer or whole number
float = number with decimal point
bool = boolean (true or false)
color = a standard color which we use a RGBA (red, green,blue,alpha) hex format similar to CSS #FF003399
string = a line of text
line = a line on a chart
hline = a horizontal line on a chart
plot = a line or diagram on a chart
array = a data format like [“a”,”b”,”c”]

Standard operators include:
+ – * / % < <= >= > == != not and or

These can be used in statements which use a double space indented layout:
if close >= open
doSomething()

Statements can be combined and used in line. This is useful when adding filters and you want to check multiple attributes before executing a trade:
FilterOK = false
Filter1 = close > open
Filter2 = rising(volume,1)
FilterOK := Filter1 and Filter2

You can plot a line by specifying the price and any options
plot(priceVariable, color=color.yellow)

You can place a shape on a chart using the plotShape() function:
plotshape(true, style=shape.flag, color=test ? color.green : color.red)

Shapes available are:
shape.xcross, shape.cross, shape.circle, shape.triangleup, shape.triangledown, shape.flag, shape.arrowup, shape.arrowdown, shape.square, shape.diamond, shape.labelup, shape.labeldown

If you want to access or round then you’ll often want to use the current tick size for the data set which is stored in:
syminfo.mintick


Pine Script Built In Functions

Pine scripts built in functions are great and make testing and developing strategies quicker and more efficient. There are hundreds of built in functions but these are the ones I find most useful when developing strategies.

Standard Built In Functions

These are standard functions that you’ll be using a lot to when developing in pine script. For the most part you pass in data and a resulting value is passed back.

five = sqrt(25)

FunctionDescription
sqrt(9)Square root of a value
log(5)Logarithm of a value
round(54.23)Rounds a float to the nearest integer => 54
min(val1, val2, val3)Returns the lowest value
max(val1, val2, val3)Returns the highest value
crossover(ema1,ema2)Given two data series it calculates a boolean as to if they crossed over in the most recent data point.
crossunder(ema1,ema2)As above but if ema1 has crossed underneath ema2
cross(ema1,ema2)As above but returns true if over or under
valuewhen(crossover(slow, fast), close)Get value of close when a crossover or other occurrence takes place
strategy.entry(“long”, strategy.long, 100, when=strategy.position_size <= 0)Enter a trade with a long position for 100 units when conditions such as this position size is met.
strategy.exit(“exit”, “long”, stop=stopLoss, limit=takeProfit)Exit a trade based on a stop loss or take profit value
label.new(bar_index, high, syminfo.ticker)Labels can be used to print data at a specific data point
tokyoOpen = time(timeframe.period, “0000-0800”)
bgcolor(na(tokyoOpen) ? na : color.green)
You can set background colours for specific time periods on a chart based on UTC timezone.
myColor = color(#800080,0)Set a custom colour to a variable using hex format
teslaClose = security(“TSLA”, “D”, close)Data is generally set to a single asset or market such as BTCUSD for the Bitcoin US Dollar market. You can call in other data sources to look for correlations and betas with
nz(ema1, ema2)Replaces NaN values with zeros to clean up data in a series.

Time Series Built In Functions

These are slightly different functions that you can use to pass in series data such as the daily close or high and a data length or look back period to calculate a moving average or some other value based on that data.

topPriceExample = highest(close,5)

FunctionDescription
change(close,5)Difference between current value and previous. Example will show difference between current closing price and the closing price five candles back.
highest(open,10)Highest value for a set number of bars
lowest(open,10)Lowest value for a set number of bars
linreg(low,30)Linear regression curve. A best fit line for a specified time period. A linear regression curve is calculated using the least squares method.
mom(close,7)Momentum or the difference between price and price however many bars ago.
sma(high, 300)Simple moving average. The mean average of the values for a set period.
sum(close, 5)The sum of the last x values
vwma(close, 21)Volume weighted moving average
wma(close, 21)Weighted moving average
ema(close, 21)Exponential moving average. Moves faster than the sma and more useful.
atr(14)Average true range displays the average trading range between high and low for however many candles. This is useful for gauging market conditions and setting stops.
variance(close, 20)Variance calculates the squared deviation of series data from its mean average
stdev(close, 20)Standard deviation for series data for a set period
correlation(sourceA, sourceB, 200)Shows the correlation coefficient for two assets to deviate from the simple moving average.
vwap(close)Volume weighted average price. Used a lot by market makers and institutional traders
rsi(close,14)Relative strength indicator. A measure of how over bought or over sold an asset is.

Creating A Pine Script Trading Strategy

OK now everyone is up to speed let’s get started with create a basic moving average cross over strategy.

//@version=5
strategy('Pine Script Tutorial Example Strategy 1', overlay=true, initial_capital=1000, default_qty_value=100, default_qty_type=strategy.percent_of_equity)
fastEMA = ta.ema(close, 24)
slowEMA = ta.ema(close, 200)
goLongCondition1 = ta.crossover(fastEMA, slowEMA)
timePeriod = time >= timestamp(syminfo.timezone, 2020, 12, 15, 0, 0)
notInTrade = strategy.position_size <= 0
if goLongCondition1 and timePeriod and notInTrade
    stopLoss = low * 0.97
    takeProfit = high * 1.12
    strategy.entry('long', strategy.long)
    strategy.exit('exit', 'long', stop=stopLoss, limit=takeProfit)
plot(fastEMA, color=color.new(color.blue, 0))
plot(slowEMA, color=color.new(color.yellow, 0))
  • So we start by setting the pine script version and a name for our strategy and setting overlay=true to put any drawings on top of the chart. Note that we use the strategy function instead of the study function to define a strategy.
  • We set the initial capital to $1000 and default quantity to 100% of capital for backtesting within this strategy() function.
  • We then move on to calculate a fast (24 candle) and slow (200 candle) exponential moving average. This is going to be using the hourly time frame so we have an average 24hr price and a average 200hr price.
  • The goLongCondition1 variable is set to true or false depending if there is a cross over of the fast and slow moving averages
  • This is a trend following strategy so I only want to test it from the start of the most recent bull run. We set the sinceBullRun variable to true if the date is later than the 15th December 2020
  • We set notInTrade to true if we are not currently in a trade using the strategy.position_size built in variable
  • if goLongCondition1, timePeriod and notInTrade are all true, we continue to the indented code
  • A stop loss is set to 3% below the hourly low, a take profit is set to 12% above the daily high
  • strategy.entry is used to take out a long position effectively purchasing the underlying asset.
  • strategy.exit is used to set the previously declared stopLoss and takeProfit levels
  • Finally we will plot the fastEMA and slowEMA values on the chart so we can better visualise what the strategy is doing.

Backtesting Pine Script Strategies

So how does this simple moving average cross over strategy perform? When lambo? To find out we use TradingView’s StrategyTest application.

Go in to TradingView and search for asset BTCUSD, set the time frame to 1 hour, copy and paste the strategy from the previous example, click “Add To Chart”, then go into the StrategyTest tab and you should be presented with something like this:

As you can see this is performing quite well. We have a net profit of 35% which is not to be sniffed at. However when you compare it to a buy and hold strategy which returns over 50% it’s starting to look less optimal.

Using the chart you can see that there’s large sections of this bull run where we don’t have exposure and it’s taking out positions at points where we are getting stopped out quite frequently.

So what can we do to improve this strategy?

The first thing I would do is get it to execute trades whenever we are above the slow moving average rather than rely on a specific cross over point. We effectively want to be long when Bitcoin is trending up and then sell at the first signs of trouble but without getting stopped out so frequently that the strategy gets chopped to pieces.

I would also add a second condition to both the entry and exit. We want the market momentum to be in our favour whenever executing a trade and we don’t want to exit a position if it’s already turned and trending back up. One simple trick I’ve found works quite effectively for this is comparing the simple moving average with the exponential moving average for the same period. The exponential moving average puts more weight on recent data so when compared to the sma which is just the mean, it will therefore show the most recent market direction.

The other thing I’d modify is the stop-loss, to use average true range rather than a fixed percentage which will be more dynamic in volatile conditions. Let’s take a look at what this modified code looks like:

//@version=5
strategy('Pine Script Tutorial Example Strategy 2', overlay=true, shorttitle='PSTES2', initial_capital=1000, default_qty_value=100, default_qty_type=strategy.percent_of_equity, commission_value=0.025)
fastPeriod = input(title='Fast MA', defval=24)
slowPeriod = input(title='Slow MA', defval=200)
fastEMA = ta.ema(close, fastPeriod)
fastSMA = ta.sma(close, fastPeriod)
slowEMA = ta.ema(close, slowPeriod)
atr = ta.atr(14)
goLongCondition1 = fastEMA > fastSMA
goLongCondition2 = fastEMA > slowEMA
exitCondition1 = fastEMA < fastSMA
exitCondition2 = close < slowEMA
inTrade = strategy.position_size > 0
notInTrade = strategy.position_size <= 0
timePeriod = time >= timestamp(syminfo.timezone, 2020, 12, 15, 0, 0)
if timePeriod and goLongCondition1 and goLongCondition2 and notInTrade
    strategy.entry('long', strategy.long, when=notInTrade)
    stopLoss = close - atr * 3
    strategy.exit('exit', 'long', stop=stopLoss)
if exitCondition1 and exitCondition2 and inTrade
    strategy.close(id='long')
plot(fastEMA, color=color.new(color.blue, 0))
plot(slowEMA, color=color.new(color.yellow, 0))
bgcolor(notInTrade ? color.red : color.green, transp=90)
  • I’ve also added a commission value of 0.025 in the strategy set up at the top to allow for trading fees.
  • I’ve added customisable fastPeriod, slowPeriod values for the moving averages using the input() function.
  • And I’ve changed the background colour in the last line to display red or green depending on if we are in a trade or not.

Copy and paste this into TradingView with the 1HR BTCUSD chart and it will look something like this:

A BTCUSD trading strategy on TradingView built with Pine Script
A BTCUSD trading strategy on TradingView built with Pine Script

This is much more like how I would want to trade this market moving forwards. You can see from the green and red backgrounds that we are capturing the majority of the upwards momentum and avoiding some of the down trends. But more importantly it closes the position early enough so that if there was a big crash we wouldn’t lose the farm.

The return is 194% which is just slightly above a buy and hold strategy. I’d expect in production it would be roughly equal or even below a buy and hold strategy if the market continues rising. This is because the algo has been shaped, to a certain extent, by past data.

The Sharpe ratio however is improved because the risk adjusted returns on this type of strategy has improved. This strategy gives you exposure to Bitcoin gains in a trending market and gets you out before any major market crashes, where were you in 2017-18?! 😒. There’s a lot of value in capturing gains while avoiding major downturns which fitted moving average strategies aim to realise.

As soon as the market dips beyond the 200hr moving average line the position is closed preserving capital. This is exactly what I want during the mid to later stages of a parabolic bull market. If the market stopped trending up and started moving sideways for a significant amount of time this strategy would get destroyed. You’d be effectively buying high and selling low, a mean reversion strategy would be much more appropriate in that type of market conditions. In fast trending markets though this provides a simple but effective, risk-averse, trend following trading strategy.

Here are some more example code snippets that can be used to filter trades and develop strategies.

// Open Trade Filters
openCondition1 = (ta.rsi(close,14) > 40)
openCondition2 = ((strategy.closedtrades - strategy.closedtrades[24]) < 1)

// Exit Conditions
exitCondition1 = ta.ema(close, 50) < ta.ema(close, 200)
exitCondition2 = close < close[1] * 0.95
if exitCondition1 or exitCondition2
    strategy.close(id='long')

// Smoothing
smoothed = close + 0.5 * (close[1] - close)

// Adaptive Colors
line_color = color.green
if close < close[1]
    line_color := color.red
    line_color
plot(close, color=line_color)
bgcolor(line_color, transp=90)

How & Why Publish TradingView Pine Scripts

To publish a script publicly it needs to be original, useful and it needs a good description to let other traders understand what it is. This is an except from the TradingView documentation:

“Your script’s description is your opportunity to explain to the community how it is original and can be useful. If your description does not allow TradingView moderators to understand how your script is original and potentially useful, it will be moderated.”

My moving average script wouldn’t be approved because there are already a million and one other scripts just like it in the public library. This brings me to an important point about expectations for public work.

If someone has a low time frame delta neutral strategy that is consistently profitable they aren’t going to publish it, they aren’t going to sell it and they aren’t going to need your money to execute it. Things like that do exist but they are rare, extremely hard to create, don’t last forever and are highly profitable.

If someone had a strategy that makes just 5% a day consistently they could generate a return of $50 billion from an initial investment of $1000 in a year. This is obviously unrealistic and what’s more unrealistic is that they’ll sell you this strategy for just $19/month.

Having said that there are some very smart developers who publish open source algorithms. I think there is value in reviewing others work and then incorporating their ideas and methods in your own strategies and algos.

The collaboration and industry acknowledgement aspect is why many algorithms which could be successful in specific market conditions are published. There is a community of traders who use TradingView regularly and publishing original work which adds value can be beneficial to the developer and the community.

If you would like to publish your work you can click on the “Publish Script” tab within pine editor which will bring up the following interface:

Publishing The Pine Script Tutorial Trading Strategy
Publishing The Pine Script Tutorial Trading Strategy

Trade Execution & Strategy Deployment

TradingView has a broker panel where you can connect your account directly to one of the following brokers:

  • Oanda
  • TradeStation
  • Gemini
  • Capital.com
  • FXCM
  • Saxo

TradingView is great for visualising and developing trading strategies but for execution, in my opinion, we need something more robust. If the markets get busy and TradingView goes down we need our strategies to still execute.

For this reason I’d recommend migrating pine script over to either NodeJS or Python and executing via official exchange/broker API’s. Production code can be executed on a dedicated server (with a fallback server if volume permits it) to provide complete control over the process.

On a high timeframe strategy where execution efficiency doesn’t matter too much then it could well be possible to work with one of the brokers above but most quant traders will run their own bots and this is the approach I’d recommend.

If I wanted to execute the strategy discussed above I wouldn’t actually want all my funds on an exchange account buying and selling spot BTC. I would probably flip the strategy so that it opened a short position on a perpetual futures trading contract whenever the price fell below the 200hr moving average and other filters were met.

This would in effect hedge my current long position with a leveraged trade so that I’d only need to keep a reduced amount of capital on exchange for collateral. The rest of my funds could be held in a cold storage wallet and trade them only to balance out the position by closing the perp and selling spot at a later date.

So for example if my cryptocurrency portfolio had 1BTC and 20ETH in it I’d add 5 ETH and 0.25 BTC to an exchange like FTX to use as collateral. I’d then use an API to execute a leveraged short position for 1BTC and 20ETH whenever the strategy dictated.

This is what the code for something like that would look like:-

const request = require('request');
const crypto = require('crypto');
const ftx = require('lib/exchanges/ftx.js');

const account = {
  apiKey: `abc123`,
  apiSecret: `abc345`,
  subAccount: `HEDGE1`,
  inTrade: false,
}

const calculateSMA = (arr, range=false) => {
  if (!range) range = arr.length;
  let sum = 0;
  if (range > arr.length) range = arr.length;
  for (let ii = arr.length - range; ii < arr.length; ii++){
    sum += arr[ii];
  }
  return sum / range;
}

const calculateEMA = (arr,range=false) => {
  if (!range) range = arr.length;
  const yma = arr.reduce((p,n,i) => i ? p.concat(2*n/(range+1) + p[p.length-1]*(range-1)/(range+1)) : p, [arr[0]]);
  return yma[yma.length-1];
}

const checkStrategy = async () => {
  const res1Promise = fetch(`https://ftx.com/api/markets/BTC-PERP/candles?resolution=3600&limit=200`).catch(err => utils.errorLog(err));
  const res1 = await res1Promise;
  const ftxCandles = await res1.json().catch(err => utils.errorLog(err));
  const priceArray = [];
  ftxCandles.result.forEach((min,i) => {
    priceArray.push(min.close);
  });
  const fastEMA = calculateEMA(priceArray.slice(-24));
  const fastSMA = calculateSMA(priceArray.slice(-24));
  const slowEMA = calculateEMA(priceArray.slice(-200));
  const hedgeCondition1 = (fastEMA < fastSMA);
  const hedgeCondition2 = (fastEMA < slowEMA);
  if (hedgeCondition1 && hedgeCondition2 && account.inTrade === false) {
    account.inTrade = true;
    ftx.Order('SELL', 'BTC-PERP', 1, fastEMA * 0.95, account);
    // Send an alarm notification and do logic to confirm trade went through
  }
}

setInterval(() => {
  checkStrategy();
}, 60000);

This is untested and nowhere near production ready but it provides a couple of useful JavaScript functions for calculating simple and exponential moving averages. It also shows how you can grab live data from an exchange and use this to make trading decisions.

In production I would have infrastructure like this set up.

Check the TradingView | Go Pro Page


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.