How Gemini Writes Pine Script (and Where It Gets It Wrong)

intermediate 35 min · · By Alpha Guy · gemini pine-script

Why I Tried Gemini for Pine Script

I had been using Claude and ChatGPT to generate TradingView indicators for a while, and I got curious about Google Gemini. Three things caught my attention:

  1. The free tier is generous. Gemini 2.0 Flash gives you a high rate limit at no cost. For Pine Script generation where you are iterating on prompts, that matters.
  2. It is multimodal. You can paste a chart screenshot and say “write an indicator that detects this pattern.” Neither Claude nor ChatGPT (at the time of my testing) handled this as naturally.
  3. The context window is huge. You can paste an entire Pine Script library and ask Gemini to modify it without hitting token limits.

I decided to run a fair test: three indicators of increasing complexity, same prompt structure, and I would document exactly what Gemini produced and what I had to fix.

Test 1: Simple EMA Crossover with Alerts

This is the “hello world” of Pine Script indicators. If an AI cannot get this right, nothing else matters.

The Prompt

Write a TradingView Pine Script v5 indicator that:
- Plots a 9-period EMA and a 21-period EMA on the chart
- Colors the background green when the 9 EMA is above the 21 EMA
- Colors the background red when the 9 EMA is below the 21 EMA
- Shows a "BUY" label when the 9 EMA crosses above the 21 EMA
- Shows a "SELL" label when the 9 EMA crosses below the 21 EMA
- Includes alert conditions for both crossovers
- Make all periods configurable via inputs

What Gemini Generated

//@version=5
indicator("EMA Crossover", overlay=true)

// Inputs
fastLength = input.int(9, "Fast EMA Period", minval=1)
slowLength = input.int(21, "Slow EMA Period", minval=1)

// Calculate EMAs
fastEMA = ta.ema(close, fastLength)
slowEMA = ta.ema(close, slowLength)

// Plot EMAs
plot(fastEMA, "Fast EMA", color=color.blue, linewidth=2)
plot(slowEMA, "Slow EMA", color=color.orange, linewidth=2)

// Background coloring
isBullish = fastEMA > slowEMA
bgcolor(isBullish ? color.new(color.green, 90) : color.new(color.red, 90))

// Crossover detection
bullCross = ta.crossover(fastEMA, slowEMA)
bearCross = ta.crossunder(fastEMA, slowEMA)

// Labels
if bullCross
    label.new(bar_index, low, "BUY",
         style=label.style_label_up,
         color=color.green,
         textcolor=color.white,
         size=size.small)

if bearCross
    label.new(bar_index, high, "SELL",
         style=label.style_label_down,
         color=color.red,
         textcolor=color.white,
         size=size.small)

// Alerts
alertcondition(bullCross, "Bullish EMA Crossover", "Fast EMA crossed above Slow EMA")
alertcondition(bearCross, "Bearish EMA Crossover", "Fast EMA crossed below Slow EMA")

Verdict: Almost Perfect

This was clean. It compiled on the first paste into TradingView’s Pine Editor with zero errors. The EMA calculations were correct, the ta.crossover and ta.crossunder functions were used properly, and the alert conditions worked.

One minor issue: the labels accumulate on the chart over time. On a 5-minute chart with months of data, you end up with hundreds of labels, and TradingView has a limit of around 500 drawing objects. It did not add max_labels_count to the indicator() call. Easy fix:

indicator("EMA Crossover", overlay=true, max_labels_count=50)

Not a big deal. For a simple indicator, Gemini nailed it.

Test 2: RSI Divergence Detector

This is where things get more interesting. Divergence detection requires comparing pivot points on both price and RSI, which means working with ta.pivotlow, ta.pivothigh, and ta.valuewhen — functions that trip up AI models regularly.

The Prompt

Write a TradingView Pine Script v5 indicator that detects RSI divergences:

- Calculate RSI with configurable period (default 14)
- Detect bullish divergence: price makes a lower low while RSI makes
  a higher low
- Detect bearish divergence: price makes a higher high while RSI makes
  a lower high
- Use pivot points (configurable lookback, default 5) to identify the
  highs and lows
- Draw lines on the RSI panel connecting the divergent points
- Plot the RSI in a separate panel with overbought (70) and oversold
  (30) levels
- Add alert conditions for both divergence types
- Show the divergence count in a table in the top-right corner

What Gemini Generated

//@version=5
indicator("RSI Divergence Detector", overlay=false)

// Inputs
rsiPeriod = input.int(14, "RSI Period", minval=1)
pivotLookback = input.int(5, "Pivot Lookback", minval=2)

// RSI
rsi = ta.rsi(close, rsiPeriod)

// Plot RSI
plot(rsi, "RSI", color=color.purple, linewidth=2)
hline(70, "Overbought", color=color.red, linestyle=hline.style_dashed)
hline(30, "Oversold", color=color.green, linestyle=hline.style_dashed)
hline(50, "Midline", color=color.gray, linestyle=hline.style_dotted)

// Pivot detection
pricePivotLow = ta.pivotlow(low, pivotLookback, pivotLookback)
pricePivotHigh = ta.pivothigh(high, pivotLookback, pivotLookback)
rsiPivotLow = ta.pivotlow(rsi, pivotLookback, pivotLookback)
rsiPivotHigh = ta.pivothigh(rsi, pivotLookback, pivotLookback)

// Store previous pivot values
prevPriceLow = ta.valuewhen(not na(pricePivotLow), low[pivotLookback], 1)
prevRsiLow = ta.valuewhen(not na(rsiPivotLow), rsi[pivotLookback], 1)
currPriceLow = ta.valuewhen(not na(pricePivotLow), low[pivotLookback], 0)
currRsiLow = ta.valuewhen(not na(rsiPivotLow), rsi[pivotLookback], 0)

prevPriceHigh = ta.valuewhen(not na(pricePivotHigh), high[pivotLookback], 1)
prevRsiHigh = ta.valuewhen(not na(rsiPivotHigh), rsi[pivotLookback], 1)
currPriceHigh = ta.valuewhen(not na(pricePivotHigh), high[pivotLookback], 0)
currRsiHigh = ta.valuewhen(not na(rsiPivotHigh), rsi[pivotLookback], 0)

// Divergence conditions
bullishDiv = not na(pricePivotLow) and currPriceLow < prevPriceLow and currRsiLow > prevRsiLow
bearishDiv = not na(pricePivotHigh) and currPriceHigh > prevPriceHigh and currRsiHigh < prevRsiHigh

// Draw divergence lines on RSI
var int bullCount = 0
var int bearCount = 0

if bullishDiv
    line.new(
         bar_index[pivotLookback] - pivotLookback, prevRsiLow,
         bar_index[pivotLookback], currRsiLow,
         color=color.green, width=2)
    bullCount += 1

if bearishDiv
    line.new(
         bar_index[pivotLookback] - pivotLookback, prevRsiHigh,
         bar_index[pivotLookback], currRsiHigh,
         color=color.red, width=2)
    bearCount += 1

// Signals
plotshape(bullishDiv, "Bullish Divergence",
     shape.triangleup, location.bottom,
     color=color.green, size=size.small)
plotshape(bearishDiv, "Bearish Divergence",
     shape.triangledown, location.top,
     color=color.red, size=size.small)

// Stats table
var table statsTable = table.new(
     position.top_right, 2, 3,
     bgcolor=color.new(color.black, 80))
if barstate.islast
    table.cell(statsTable, 0, 0, "Divergences",
         text_color=color.white, text_size=size.small)
    table.cell(statsTable, 0, 1, "Bullish",
         text_color=color.green, text_size=size.small)
    table.cell(statsTable, 1, 1, str.tostring(bullCount),
         text_color=color.green, text_size=size.small)
    table.cell(statsTable, 0, 2, "Bearish",
         text_color=color.red, text_size=size.small)
    table.cell(statsTable, 1, 2, str.tostring(bearCount),
         text_color=color.red, text_size=size.small)

// Alerts
alertcondition(bullishDiv, "Bullish RSI Divergence", "Bullish divergence detected")
alertcondition(bearishDiv, "Bearish RSI Divergence", "Bearish divergence detected")

What Worked

The overall structure was solid. The RSI calculation, pivot detection, and divergence logic were conceptually correct. The stats table was a nice addition. The ta.valuewhen usage to compare current pivots against previous pivots was the right approach.

What Did Not Work

Problem 1: The divergence line coordinates were wrong. The line.new call tried to draw from the previous pivot to the current pivot, but the x-coordinates (bar indices) were calculated incorrectly. bar_index[pivotLookback] - pivotLookback does not give you the bar index of the previous pivot. You need to use ta.valuewhen for the bar index as well, or store it in a variable when the pivot is detected.

Problem 2: The location.bottom and location.top in plotshape are not valid. The correct values for an RSI panel are location.belowbar and location.abovebar, but since this indicator has overlay=false, you actually want to place them relative to the RSI plot. Using location.bottom throws a compile error. The fix is to use the actual RSI value as the placement:

plotshape(bullishDiv, "Bullish Divergence",
     shape.triangleup, location.absolute,
     color=color.green, size=size.small, offset=-pivotLookback)

Wait, that is not quite right either. After some experimentation, the cleanest approach is:

plotshape(bullishDiv ? rsi[pivotLookback] : na, "Bullish Divergence",
     shape.triangleup, location.absolute,
     color=color.green, size=size.small, offset=-pivotLookback)

Problem 3: Line drawing accumulation. Same issue as the labels in Test 1 — the script needed max_lines_count set. Without it, old lines get deleted silently after 500 drawings and you lose divergence history.

I spent about 15 minutes fixing the line coordinates and plot shapes. The divergence detection logic itself was correct, which is the harder part. The rendering issues were annoying but not intellectually difficult to fix.

Test 3: Volume-Weighted Momentum Oscillator

For the final test, I asked Gemini to build something that does not exist as a standard indicator. I wanted to see how it handles novel indicator design.

The Prompt

Write a TradingView Pine Script v5 indicator — a Volume-Weighted
Momentum Oscillator (VWMO) with these rules:

1. Calculate rate of change (ROC) over a configurable period
   (default 14)
2. Weight the ROC by relative volume: current volume divided by the
   SMA of volume over the same period
3. Apply an EMA smoothing to the result (configurable, default 5)
4. Plot the oscillator as a histogram:
   - Green bars when the value is positive and increasing
   - Light green when positive but decreasing
   - Red when negative and decreasing
   - Light red (salmon) when negative but increasing
5. Add a signal line (EMA of the oscillator, default period 9)
6. Add a zero line
7. Show crosses of the oscillator and signal line with dots
8. Add a divergence detection feature comparing the oscillator peaks
   and troughs to price
9. Alert conditions for: zero-line cross, signal-line cross, and
   divergence

What Gemini Generated

//@version=5
indicator("Volume-Weighted Momentum Oscillator", shorttitle="VWMO")

// Inputs
rocPeriod = input.int(14, "ROC Period", minval=1)
smoothPeriod = input.int(5, "Smoothing Period", minval=1)
signalPeriod = input.int(9, "Signal Period", minval=1)

// Rate of Change
roc = ta.roc(close, rocPeriod)

// Volume weighting
avgVolume = ta.sma(volume, rocPeriod)
relativeVolume = volume / avgVolume
vwmo_raw = roc * relativeVolume

// Smoothing
vwmo = ta.ema(vwmo_raw, smoothPeriod)
signal = ta.ema(vwmo, signalPeriod)

// Histogram coloring
isIncreasing = vwmo > vwmo[1]
histColor = vwmo >= 0
     ? (isIncreasing ? color.green : color.new(color.green, 50))
     : (isIncreasing ? color.new(color.red, 50) : color.red)

// Plots
plot(vwmo, "VWMO", style=plot.style_histogram,
     color=histColor, linewidth=3)
plot(signal, "Signal", color=color.orange, linewidth=2)
hline(0, "Zero Line", color=color.gray)

// Signal line crosses
bullCross = ta.crossover(vwmo, signal)
bearCross = ta.crossunder(vwmo, signal)
plot(bullCross ? vwmo : na, "Bull Cross", style=plot.style_circles,
     color=color.green, linewidth=4)
plot(bearCross ? vwmo : na, "Bear Cross", style=plot.style_circles,
     color=color.red, linewidth=4)

// Zero line crosses
zeroBull = ta.crossover(vwmo, 0)
zeroBear = ta.crossunder(vwmo, 0)

// Divergence detection
pivotLB = input.int(5, "Divergence Lookback", minval=2)
oscLow = ta.pivotlow(vwmo, pivotLB, pivotLB)
oscHigh = ta.pivothigh(vwmo, pivotLB, pivotLB)

prevOscLow = ta.valuewhen(not na(oscLow), vwmo[pivotLB], 1)
currOscLow = ta.valuewhen(not na(oscLow), vwmo[pivotLB], 0)
prevPriceLow = ta.valuewhen(not na(oscLow), low[pivotLB], 1)
currPriceLow = ta.valuewhen(not na(oscLow), low[pivotLB], 0)

prevOscHigh = ta.valuewhen(not na(oscHigh), vwmo[pivotLB], 1)
currOscHigh = ta.valuewhen(not na(oscHigh), vwmo[pivotLB], 0)
prevPriceHigh = ta.valuewhen(not na(oscHigh), high[pivotLB], 1)
currPriceHigh = ta.valuewhen(not na(oscHigh), high[pivotLB], 0)

bullDiv = not na(oscLow) and currPriceLow < prevPriceLow
     and currOscLow > prevOscLow
bearDiv = not na(oscHigh) and currPriceHigh > prevPriceHigh
     and currOscHigh < prevOscHigh

plotshape(bullDiv, "Bullish Divergence", shape.diamond,
     location.bottom, color=color.green, size=size.tiny,
     offset=-pivotLB)
plotshape(bearDiv, "Bearish Divergence", shape.diamond,
     location.top, color=color.red, size=size.tiny,
     offset=-pivotLB)

// Alerts
alertcondition(zeroBull, "VWMO Zero Cross Up", "VWMO crossed above zero")
alertcondition(zeroBear, "VWMO Zero Cross Down", "VWMO crossed below zero")
alertcondition(bullCross, "VWMO Signal Cross Up", "VWMO crossed above signal line")
alertcondition(bearCross, "VWMO Signal Cross Down", "VWMO crossed below signal line")
alertcondition(bullDiv, "VWMO Bullish Divergence", "Bullish divergence on VWMO")
alertcondition(bearDiv, "VWMO Bearish Divergence", "Bearish divergence on VWMO")

What Worked

The core oscillator logic was genuinely clever. Multiplying ROC by relative volume is a reasonable way to weight momentum by conviction. The histogram coloring logic using a ternary for four states (positive increasing, positive decreasing, negative increasing, negative decreasing) was clean and correct. The signal line cross detection worked properly. All six alert conditions were present as requested.

What Did Not Work

Problem 1: location.bottom and location.top again. Same mistake as Test 2. These are not valid Pine Script v5 location constants for plotshape in a non-overlay indicator. Gemini seems to consistently hallucinate these. The fix is the same — use location.absolute with the actual value, or just omit the location and let Pine Script default it.

Problem 2: Multi-line boolean expressions broke. The bullDiv and bearDiv assignments split across lines without proper continuation. Pine Script v5 does allow multi-line expressions, but only when the line break comes after an operator. Having and at the start of the new line should work in v5, but Gemini placed it in a way that sometimes confuses the Pine Script compiler. I reformatted them as single lines:

bullDiv = not na(oscLow) and currPriceLow < prevPriceLow and currOscLow > prevOscLow
bearDiv = not na(oscHigh) and currPriceHigh > prevPriceHigh and currOscHigh < prevOscHigh

Problem 3: No max_bars_back annotation. With the divergence detection using ta.valuewhen and deep lookbacks, TradingView sometimes throws a “max_bars_back” runtime error on lower timeframes with limited history. Adding max_bars_back(vwmo, 500) after the VWMO calculation fixed this.

Problem 4: The histogram and signal line overlapped visually. The histogram is plotted first with linewidth=3, and the signal line sits on top. This looks fine when the signal is near zero, but when both are at extreme values, the signal line gets buried under thick histogram bars. I swapped the plot order and reduced the histogram width:

plot(signal, "Signal", color=color.orange, linewidth=2)
plot(vwmo, "VWMO", style=plot.style_histogram,
     color=histColor, linewidth=2)

After fixes, the indicator looked good on chart and the oscillator behaved as expected — showing stronger readings during high-volume moves and dampened readings during low-volume drift.

Comparison: Gemini vs Claude vs ChatGPT for Pine Script

I ran similar tests across all three models over the course of two weeks. Here is what I found.

CategoryGemini 2.0 FlashClaude 3.5 SonnetChatGPT-4o
Pine Script v5 syntaxGood, occasional v4 slipConsistently v5Good, rare v4 slip
Basic indicators (EMA, RSI)Correct on first tryCorrect on first tryCorrect on first try
Complex indicators (divergence)Logic correct, rendering bugsUsually correctLogic sometimes wrong
Custom/novel indicatorsDecent design instinctsStrong design instinctsTends toward boilerplate
ta.valuewhen usageCorrect but verboseCorrect and cleanSometimes misuses it
plotshape locationsHallucinates location.top/bottomUsually correctUsually correct
Table/label renderingGoodGoodOccasionally buggy
Alert conditionsAlways included when askedAlways included when askedSometimes forgets
Chart screenshot understandingStrong (multimodal)Not supported nativelySupported but less precise
Code style/readabilityClean, well-commentedVery cleanVerbose, over-commented
Free tier availabilityGenerous (Gemini Flash)LimitedLimited

The takeaway is that none of these models produce flawless Pine Script on the first try for anything beyond simple indicators. But the types of errors differ. Gemini tends to get the logic right and mess up the rendering. Claude tends to get both right more often but costs more to use at volume. ChatGPT occasionally gets the logic wrong, which is a harder problem to debug.

Gemini’s Unique Strength: Chart Screenshots

This is the feature that kept me coming back to Gemini. I took a screenshot of a chart showing a specific pattern — in this case, a “spring” pattern at a support level with a volume spike — and pasted it into Gemini with this prompt:

I've attached a chart screenshot. The pattern I want to detect is:
- Price drops below a support level briefly (the "spring")
- Volume spikes on the drop
- Price quickly recovers back above the support level
- The recovery candle has a long lower wick

Write a Pine Script v5 indicator that detects this pattern and
marks it on the chart.

Gemini correctly identified the visual pattern and produced an indicator that:

  • Tracked dynamic support levels using recent pivot lows
  • Detected when price broke below support and recovered within 3 bars
  • Checked for a volume spike (>1.5x average)
  • Verified the recovery candle had a lower wick > 60% of the candle range
  • Plotted a label below the bar where the spring was detected

Was the code perfect? No. The support level detection was too simple (used a fixed lookback rather than proper S/R calculation), and the wick ratio threshold was hardcoded instead of being an input. But the fact that it went from a screenshot to a working indicator skeleton in one prompt was genuinely useful.

Neither Claude nor ChatGPT offered this workflow at the time I tested. You would need to describe the pattern in words, which is harder and less precise.

Gemini’s Weaknesses

Pine Script v4 Syntax Leaks

About one in five prompts, Gemini would slip in v4 syntax. Common examples:

  • Using study() instead of indicator()
  • Using security() instead of request.security()
  • Using input() instead of input.int() or input.float()
  • Referencing color.lime (deprecated) instead of color.green

These are easy to fix if you know Pine Script, but frustrating if you are a beginner who does not know which version is which. Adding “Use Pine Script v5 syntax only, NOT v4” to your prompt helps but does not eliminate the issue entirely.

Hallucinated Function Names

Twice during my testing, Gemini invented functions that do not exist:

  • ta.volumeprofile() — not a built-in Pine Script function
  • ta.ichimoku() — also not real; you have to calculate Ichimoku components manually

When I pointed out the error, Gemini corrected itself and provided the manual calculation. But if you did not know the function was fake, you would get a confusing compile error with no clear fix.

Inconsistent Error Recovery

When I pasted a TradingView error message back to Gemini and asked it to fix the code, the results were hit or miss. About half the time, it correctly identified and fixed the issue. The other half, it would make unrelated changes that introduced new problems. Claude was noticeably more reliable at targeted error fixes.

My Workflow: Gemini for First Draft, Fix in Cursor

After two weeks of testing, I settled on this workflow:

  1. Start in Gemini. Describe the indicator in plain language. If I have a chart pattern in mind, paste a screenshot.
  2. Copy the output to Cursor. Paste the generated Pine Script into a .pine file in Cursor IDE.
  3. Fix compile errors in Cursor. Use Cursor’s AI to fix the specific errors TradingView reports. Cursor is better at targeted fixes than Gemini.
  4. Iterate in TradingView. Once it compiles, add it to a chart and check if the visual output matches what I wanted. Go back to Cursor for refinements.
  5. Test across timeframes. An indicator that looks great on the daily chart might fire too often on 15-minute. Adjust the parameters.

This hybrid approach plays to each tool’s strengths: Gemini for creative ideation and screenshot understanding, Cursor for precise code editing, and TradingView for visual verification.

Tips for Better Gemini Prompts

After dozens of iterations, here is what I have found produces the cleanest Pine Script from Gemini.

Always Specify the Version

Start every prompt with “Write a TradingView Pine Script v5 indicator.” Without this, Gemini will sometimes default to v4 patterns. Keep the Pine Script v5 documentation open as a reference when reviewing generated code.

Describe the Visual Output Explicitly

Do not just say “plot the indicator.” Describe exactly what you want to see:

  • What color are the lines?
  • Where do the signals appear (above bar, below bar, on the indicator panel)?
  • Do you want a histogram, a line, or an area fill?
  • What goes in the indicator panel vs. the price chart?

Request Inputs for Every Magic Number

If you do not say “make the period configurable via an input,” Gemini will hardcode it. I always include a line like: “Make all numeric parameters configurable via input.int() or input.float() with sensible defaults.”

Ask for Error Handling

Pine Script has some nasty edge cases: division by zero, na values on the first bars of a chart, and max_bars_back limits. I add this to complex indicator prompts: “Handle edge cases: check for na values, avoid division by zero, and add max_bars_back if needed.”

Give It Examples of What You Do Not Want

Negative constraints help. “Do NOT use security() — use request.security(). Do NOT use study() — use indicator(). Do NOT use Pine Script v4 syntax.”

One Indicator Per Prompt

When I asked Gemini to generate three indicators in a single prompt, the quality of each one dropped significantly. The third indicator was always the worst. One indicator per conversation keeps the output quality high.

Is Gemini Good Enough for Pine Script?

For simple to moderately complex indicators, yes. It gets the logic right more often than it gets it wrong, and the errors it does make are usually in rendering and syntax rather than in the underlying math. The screenshot feature is a genuine differentiator that I find myself using regularly.

For highly complex indicators — things with multiple timeframe analysis, dynamic arrays, or custom drawing libraries — I would reach for Claude instead. Claude produces cleaner code with fewer syntax issues, and it handles Pine Script v5’s newer features (like maps and user-defined types) more reliably.

But considering Gemini Flash is free, the value proposition is hard to beat for quick indicator prototyping. I generate the first draft in Gemini, fix it in Cursor, and move on. The whole cycle from idea to working indicator takes 10-15 minutes for something that would take me an hour to write from scratch.

The honest summary: Gemini will not write production-ready Pine Script on the first try. But it will get you 80% of the way there in 30 seconds, and that remaining 20% is the easy part if you know what to look for.

Next Steps

Disclaimer: This article is for educational purposes only and is not financial advice. Trading cryptocurrencies involves substantial risk of loss. Past performance does not guarantee future results. Always do your own research before making any trading decisions. Read full disclaimer →
Alpha Guy
Alpha Guy

Founder of VibeTradingLab. Ex-Goldman Sachs engineer, 2025 Binance Top 1% Trader. Writes about using AI tools to build trading systems that actually work. Currently nomading between Bali, Dubai, and the Mediterranean.

Got stuck? Have questions?

Join our Telegram group to ask questions, share your bots, and connect with other AI traders.

Join Telegram