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:
- 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.
- 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.
- 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.
| Category | Gemini 2.0 Flash | Claude 3.5 Sonnet | ChatGPT-4o |
|---|---|---|---|
| Pine Script v5 syntax | Good, occasional v4 slip | Consistently v5 | Good, rare v4 slip |
| Basic indicators (EMA, RSI) | Correct on first try | Correct on first try | Correct on first try |
| Complex indicators (divergence) | Logic correct, rendering bugs | Usually correct | Logic sometimes wrong |
| Custom/novel indicators | Decent design instincts | Strong design instincts | Tends toward boilerplate |
ta.valuewhen usage | Correct but verbose | Correct and clean | Sometimes misuses it |
plotshape locations | Hallucinates location.top/bottom | Usually correct | Usually correct |
| Table/label rendering | Good | Good | Occasionally buggy |
| Alert conditions | Always included when asked | Always included when asked | Sometimes forgets |
| Chart screenshot understanding | Strong (multimodal) | Not supported natively | Supported but less precise |
| Code style/readability | Clean, well-commented | Very clean | Verbose, over-commented |
| Free tier availability | Generous (Gemini Flash) | Limited | Limited |
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 ofindicator() - Using
security()instead ofrequest.security() - Using
input()instead ofinput.int()orinput.float() - Referencing
color.lime(deprecated) instead ofcolor.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 functionta.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:
- Start in Gemini. Describe the indicator in plain language. If I have a chart pattern in mind, paste a screenshot.
- Copy the output to Cursor. Paste the generated Pine Script into a
.pinefile in Cursor IDE. - Fix compile errors in Cursor. Use Cursor’s AI to fix the specific errors TradingView reports. Cursor is better at targeted fixes than Gemini.
- 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.
- 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
- Try Cursor instead — See how Cursor IDE handles Pine Script generation in our Cursor Pine Script tutorial
- See real results — Check out the 3 AI indicators I actually use after testing dozens of AI-generated indicators