In this guide, I'll walk you through everything I wish someone had told me when I started. We'll build two real indicators from scratch, cover the v6 syntax basics, and I'll share the mistakes that cost me hours of debugging.
What Is Pine Script?
Pine Script is TradingView's domain-specific programming language for creating custom technical indicators, strategies, and alerts. It runs directly on TradingView's servers, which means:
- Zero setup — no IDE, no packages, no environment issues
- Instant visualization — your code draws on the chart immediately
- Built-in data — access to OHLCV, volume, and hundreds of built-in functions
- Community sharing — publish your indicators for others (or keep them private)
Pine Script vs. MQL4 vs. Python
Before we dive in, let's be honest about where Pine Script fits:
| Feature | Pine Script v6 | MQL4 | Python (backtrader) |
|---|---|---|---|
| Learning curve | 30 minutes | Days | Hours |
| Setup required | None | MT4 install | Python + libs |
| Visualization | Built-in | MT4 only | matplotlib |
| Broker integration | Alerts only | Direct trading | Via API |
| External APIs | ❌ No | ✅ Yes | ✅ Yes |
| Backtesting | Built-in | Built-in | Flexible |
| Community | Massive | Large | Large |
Getting Started: The Pine Script Editor
1. Go to TradingView and open any chart
2. Click "Pine Editor" at the bottom of the screen 3. You'll see a default script — delete it, we're starting freshPine Script v6 Basics
As of 2026, v6 is the current version. Always start your scripts with the version declaration:
//@version=6
Key syntax rules:
- Indentation matters for readability but isn't enforced like Python
- Variables are typed — Pine infers types, but you can declare them explicitly
- Series vs. simple — most values are "series" (one value per bar), which is the core concept
- No loops over bars — Pine processes one bar at a time, chronologically
//@version=6
indicator("My First Indicator", overlay=true)
// Variables
length = 14
src = close
// Built-in functions
smaValue = ta.sma(src, length)
rsiValue = ta.rsi(src, length)
// Plotting
plot(smaValue, "SMA", color.blue, 2)
The indicator() function declares your script as an indicator (as opposed to a strategy() for backtesting). The overlay=true parameter draws it on the price chart instead of a separate pane.
Project 1: SMA Crossover Indicator
Let's build a classic — the Simple Moving Average crossover. When a fast SMA crosses above a slow SMA, it's a bullish signal. When it crosses below, bearish.
//@version=6
indicator("SMA Crossover", overlay=true)
// Input parameters — users can adjust these in the settings panel
fastLength = input.int(9, "Fast SMA Length", minval=1)
slowLength = input.int(21, "Slow SMA Length", minval=1)
// Calculate SMAs
fastSMA = ta.sma(close, fastLength)
slowSMA = ta.sma(close, slowLength)
// Detect crossovers
bullishCross = ta.crossover(fastSMA, slowSMA)
bearishCross = ta.crossunder(fastSMA, slowSMA)
// Plot the moving averages
plot(fastSMA, "Fast SMA", color.green, 2)
plot(slowSMA, "Slow SMA", color.red, 2)
// Plot signals as shapes on the chart
plotshape(bullishCross, "Buy Signal", shape.triangleup, location.belowbar, color.green, size=size.small)
plotshape(bearishCross, "Sell Signal", shape.triangledown, location.abovebar, color.red, size=size.small)
// Background color on crossover bars
bgcolor(bullishCross ? color.new(color.green, 90) : bearishCross ? color.new(color.red, 90) : na)
What's Happening Here
input.int()creates adjustable parameters in the indicator settings. Users can tweak the SMA lengths without editing code.ta.crossover()returnstrueon the bar where the first series crosses above the second. It's a built-in that saves you from writingfastSMA > slowSMA and fastSMA[1] <= slowSMA[1].plotshape()draws triangles on the chart when conditions are met. Thelocation.belowbarputs buy signals under the candle.color.new(color.green, 90)creates a color with 90% transparency (0 = opaque, 100 = invisible).
Project 2: Momentum + Signal Indicator
Now let's build something closer to what I actually use for USDJPY trading. This indicator combines momentum measurement with signal generation:
//@version=6
indicator("Momentum Signal", overlay=false)
// Inputs
momentumLength = input.int(60, "Momentum Length (bars)", minval=10)
signalLength = input.int(9, "Signal Smoothing", minval=1)
overbought = input.float(2.0, "Overbought Threshold")
oversold = input.float(-2.0, "Oversold Threshold")
// Calculate momentum as percentage change
momentum = ((close - close[momentumLength]) / close[momentumLength]) * 100
// Signal line (smoothed momentum)
signal = ta.ema(momentum, signalLength)
// Histogram
hist = momentum - signal
// Conditions
bullishMomentum = momentum > 0 and momentum > signal
bearishMomentum = momentum < 0 and momentum < signal
strongBull = momentum > overbought
strongBear = momentum < oversold
// Colors
histColor = hist >= 0 ? (hist > hist[1] ? color.green : color.new(color.green, 50)) : (hist < hist[1] ? color.red : color.new(color.red, 50))
// Plot
plot(momentum, "Momentum", color.blue, 2)
plot(signal, "Signal", color.orange, 1)
plot(hist, "Histogram", histColor, style=plot.style_columns)
// Reference lines
hline(0, "Zero", color.gray, hline.style_dashed)
hline(overbought, "Overbought", color.red, hline.style_dotted)
hline(oversold, "Oversold", color.green, hline.style_dotted)
// Background highlights for strong conditions
bgcolor(strongBull ? color.new(color.green, 85) : strongBear ? color.new(color.red, 85) : na)
Breaking It Down
This indicator sits in a separate pane below the chart (overlay=false). Here's why each piece matters:
1. Momentum calculation: close - close[momentumLength] — the [momentumLength] syntax accesses the value from N bars ago. We express it as a percentage for cross-asset comparison.
2. Signal line: An EMA of the momentum smooths out noise. When momentum crosses above its signal, the trend is accelerating.
3. Histogram coloring: The histogram uses four colors — bright green (expanding bullish), dim green (contracting bullish), dim red (contracting bearish), bright red (expanding bearish). This is the MACD-style visualization that makes trend changes visible at a glance.
4. hline(): Draws horizontal reference lines. These don't move — they mark your overbought/oversold thresholds.
For my USDJPY setup, I use a 60-bar momentum on the daily chart. When momentum is positive in February, June, or October (historically strong months for USDJPY longs), I look for entries. This is the indicator that generates the signals my automated system acts on.
Backtesting Basics
Pine Script has a built-in backtesting engine. To use it, change indicator() to strategy():
//@version=6
strategy("SMA Crossover Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)
fastLength = input.int(9, "Fast SMA")
slowLength = input.int(21, "Slow SMA")
fastSMA = ta.sma(close, fastLength)
slowSMA = ta.sma(close, slowLength)
// Entry and exit conditions
if ta.crossover(fastSMA, slowSMA)
strategy.entry("Long", strategy.long)
if ta.crossunder(fastSMA, slowSMA)
strategy.close("Long")
plot(fastSMA, "Fast", color.green)
plot(slowSMA, "Slow", color.red)
After adding this to your chart, click the "Strategy Tester" tab to see:
- Net profit and percent profitable
- Max drawdown — critical for risk management
- Trade list — every entry and exit with P&L
- Equity curve — visual representation of your strategy's performance
Backtesting Caveats
Be honest with yourself about backtesting limitations:
- Look-ahead bias: Your script only sees data up to the current bar. Pine Script prevents this by design, but be careful with
request.security()on higher timeframes. - Slippage and commissions: Add
commission_valueandslippageparameters tostrategy()for realistic results. - Overfitting: If you optimize 10 parameters to fit 2 years of data, you've curve-fitted, not discovered an edge. Keep it simple.
strategy("Realistic Backtest", overlay=true,
commission_type=strategy.commission.percent,
commission_value=0.1,
slippage=2,
default_qty_type=strategy.percent_of_equity,
default_qty_value=95)
Setting Up Alerts
Alerts are where Pine Script becomes truly powerful for active traders. You can trigger notifications based on any condition in your script:
//@version=6
indicator("Alert Example", overlay=true)
fast = ta.sma(close, 9)
slow = ta.sma(close, 21)
bullish = ta.crossover(fast, slow)
bearish = ta.crossunder(fast, slow)
plot(fast, "Fast", color.green)
plot(slow, "Slow", color.red)
// Alert conditions
alertcondition(bullish, "Bullish Crossover", "SMA 9/21 bullish crossover on {{ticker}}")
alertcondition(bearish, "Bearish Crossover", "SMA 9/21 bearish crossover on {{ticker}}")
After adding the indicator to your chart:
1. Click the "Alerts" button (clock icon) 2. Choose your indicator as the condition 3. Select the specific alert condition 4. Set delivery: push notification, email, webhook, or SMSWebhook Alerts for Automation
The webhook option is the bridge between Pine Script and automated trading. When an alert fires, TradingView sends an HTTP POST to your URL:
Alert message: {"action": "buy", "ticker": "{{ticker}}", "price": {{close}}}
Webhook URL: https://your-server.com/webhook
This is how I connect TradingView signals to my Python execution script. Pine Script generates the signal, the webhook delivers it, and Python places the order on Interactive Brokers.
Common Mistakes (And How to Avoid Them)
After a year of daily Pine Script usage, here are the mistakes I see most often:
1. Repainting Indicators
// BAD — this repaints! The current bar's close changes until the bar closes
signal = close > ta.sma(close, 20)
// BETTER — use the previous bar's close for confirmed signals
signal = close[1] > ta.sma(close, 20)[1]
Repainting means your indicator changes historical signals after the fact. It looks profitable in backtests but fails in live trading.
2. Forgetting na Checks
// This will error if myValue is na
result = myValue > 0
// Safe version
result = not na(myValue) and myValue > 0
// Or use nz() to replace na with zero
result = nz(myValue) > 0
3. Exceeding Execution Limits
Pine Script runs on TradingView's servers, and there are hard limits:
- 500ms execution time per bar
- 40
request.security()calls maximum - Limited memory for arrays and matrices
4. Misunderstanding request.security()
// Getting daily close on a 1H chart
dailyClose = request.security(syminfo.tickerid, "D", close)
// CAUTION: This looks ahead by default in v6
// Use barmerge.lookahead_off for backtesting accuracy
dailyClose_safe = request.security(syminfo.tickerid, "D", close, lookahead=barmerge.lookahead_off)
Pine Script Limitations — Be Realistic
I love Pine Script, but you need to know its walls:
1. No external API calls — You can't fetch data from REST APIs, databases, or external services. Your data is limited to what TradingView provides.
2. No direct order execution — Pine Script can't place trades. It can send alerts (including webhooks), but you need external infrastructure to act on them.
3. Server-side constraints — Execution time limits, memory limits, and a maximum number of bars in history. Complex machine learning models won't work here.
4. Limited debugging — No breakpoints, no step-through. You debug with plot(), label.new(), and log.info(). It's primitive but workable.
5. Vendor lock-in — Your scripts only run on TradingView. If you leave the platform, your code doesn't come with you (conceptually, at least — you still own the logic).
For my workflow, Pine Script handles about 60% of the job (signal generation, visualization, alerts), and Python handles the rest (order execution, risk management, portfolio tracking). They complement each other.
Tips for Learning Faster
1. Read the official docs — TradingView's Pine Script reference is excellent. The v6 migration guide is essential if you find older v4/v5 code online.
2. Study community scripts — Go to Indicators → Community Scripts and read the source code of popular indicators. You'll learn patterns and idioms quickly.
3. Start by modifying — Don't write from scratch initially. Take a working script, change one thing, see what happens. Build intuition through experimentation.
4. Use log.info() — When something isn't working, print values to the Pine Script console. It's your only debugger.
5. Keep scripts short — If your script is over 200 lines, it's probably doing too much. Split it into multiple indicators.
What's Next?
You've now built two working indicators and understand the core concepts. Here's your progression path:
1. Week 1: Build and customize the SMA crossover. Try different lengths. Add RSI as a filter.
2. Week 2: Build the momentum indicator. Apply it to your favorite asset. Adjust thresholds. 3. Week 3: Convert your best indicator into a strategy. Backtest it. Be brutally honest about the results. 4. Week 4: Set up webhook alerts. Connect them to a simple logging script to see the signals in real-time.Pine Script won't make you profitable by itself — no tool will. But it gives you the ability to test ideas quickly, visualize your edge, and automate your alert workflow. For a trader, that's worth its weight in gold.
The USDJPY momentum indicator I built took me about 2 hours in Pine Script. The same thing in Python would have taken a full day, plus another day wiring up the visualization. That speed difference compounds when you're iterating on ideas daily.
---
*Ready to start building your own indicators? Try TradingView Free — the Pine Editor is available on all plans, including the free tier. Open a chart, paste in the SMA crossover code above, and see it working in under 60 seconds.*