Title: Image-to-code visual matching fails for complex chart patterns (Pine Script)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Title: Image-to-code visual matching fails for complex chart patterns (Pine Script)
Body:
Summary
When a user provides a reference image of a visual pattern and asks Claude to
implement matching code, Claude repeatedly fails — even after 10+ correction rounds.
Reproduction Steps
- Show Claude a TradingView chart with a Bullish Pennant pattern (trendlines + fill)
- Ask Claude to write Pine Script that draws the same shape
- Claude produces: bowtie shapes, disconnected triangles, wrong slope direction,
tiny shapes in wrong location
- User corrects — Claude changes code but still doesn't match
Root Causes Observed
| Problem | What happened |
|---------|--------------|
| Bowtie / X shape | linefill filled both sides of crossing lines — lines extended past apex |
| Wrong slope direction | Used ascending slope for descending trendline |
| Shifting anchor bars | bar_index + ta.highestbars() recalculates every bar — coordinates drift |
| Apex formula wrong | Solved intersection with mismatched line origins |
| Small triangle in wrong place | Pivot detection triggered on wrong bars |
Expected Behavior
Claude should:
- Describe geometry first (anchor points, directions, intersections) before writing code
- Recognize linefill fills BOTH sides if lines cross — stop lines at apex
- Lock bar indices as
varso they don't shift per bar - Ask clarifying questions about spatial layout before generating drawing code
Suggested Fix
Before writing coordinate-based drawing code from an image, Claude should:
- Enumerate each line's (x1,y1) → (x2,y2) explicitly
- Confirm slope signs (negative = descending, positive = ascending)
- Check whether lines intersect and if fill should be one-sided
Impact
High — users lose significant time in correction loops on a clearly-defined task.
Environment
- Claude Code claude-sonnet-4-6
- Pine Script v6 / TradingView
- Windows 11
What Should Happen?
Claude should create a pine script for bullish pennant , provide a sample of image , how the image should be with line informaotion
Error Messages/Logs
Steps to Reproduce
//@version=6
// © Rkpala — Bullish Pennant Detector
// Detects: flagpole (sharp up move) + pennant (converging highs/lows) + breakout
indicator("Bullish Pennant Detector", overlay=true)
//========================
// Inputs
//========================
grpFlagpole = "Flagpole Settings"
poleMinPct = input.float(3.0, "Min flagpole rise (%)", minval=0.5, step=0.5, group=grpFlagpole)
poleBars = input.int(10, "Flagpole lookback (bars)", minval=3, maxval=30, group=grpFlagpole)
grpPennant = "Pennant Settings"
pennantBars = input.int(15, "Max pennant bars", minval=5, maxval=40, group=grpPennant)
pivotLen = input.int(3, "Pivot length", minval=2, maxval=7, group=grpPennant)
minContraction= input.float(0.3, "Min high contraction (%)", minval=0.1, step=0.1, group=grpPennant)
grpBreakout = "Breakout Settings"
volConfirm = input.bool(true, "Require volume confirmation", group=grpBreakout)
volMult = input.float(1.2, "Volume multiplier vs avg", minval=1.0, step=0.1, group=grpBreakout)
volLen = input.int(20, "Volume MA length", minval=5, group=grpBreakout)
grpVis = "Visuals"
showTarget = input.bool(true, "Show breakout target", group=grpVis)
showPole = input.bool(true, "Show flagpole line", group=grpVis)
alertOn = input.bool(true, "Enable alerts", group=grpVis)
//========================
// Core logic
//========================
// 1. Flagpole: find a sharp rise in last poleBars bars
poleHigh = ta.highest(high, poleBars)
poleLow = ta.lowest(low, poleBars)
poleRise = (poleHigh - poleLow) / poleLow * 100
hasFlagpole = poleRise >= poleMinPct
// 2. Pivot highs and lows for pennant trendlines
ph = ta.pivothigh(high, pivotLen, pivotLen)
pl = ta.pivotlow(low, pivotLen, pivotLen)
// Track last two pivot highs and lows
var float ph1 = na // most recent pivot high
var float ph2 = na // second most recent pivot high
var int ph1_bar = na
var int ph2_bar = na
var float pl1 = na
var float pl2 = na
var int pl1_bar = na
var int pl2_bar = na
if not na(ph)
ph2 := ph1
ph2_bar := ph1_bar
ph1 := ph
ph1_bar := bar_index - pivotLen
if not na(pl)
pl2 := pl1
pl2_bar := pl1_bar
pl1 := pl
pl1_bar := bar_index - pivotLen
// 3. Pennant conditions:
// - Lower highs (ph1 < ph2) → descending upper trendline
// - Higher lows (pl1 > pl2) → ascending lower trendline (converging)
// - Both pivots within pennantBars
// - High contraction confirms squeeze
higherLows = not na(pl1) and not na(pl2) and pl1 > pl2
lowerHighs = not na(ph1) and not na(ph2) and ph1 < ph2
recentPivots = not na(ph1_bar) and not na(pl1_bar) and
(bar_index - ph2_bar) <= pennantBars and
(bar_index - pl2_bar) <= pennantBars
highContraction = not na(ph1) and not na(ph2) and
(ph2 - ph1) / ph2 * 100 >= minContraction
isPennant = hasFlagpole and higherLows and lowerHighs and recentPivots and highContraction
// 4. Breakout: close breaks above descending upper trendline (ph1 level)
volAvg = ta.sma(volume, volLen)
volOK = volConfirm ? volume > volAvg * volMult : true
// Upper trendline approximation: use ph1 as resistance level
breakout = isPennant and close > ph1 and volOK and barstate.isconfirmed
// 5. Target: flagpole height added to breakout point
poleHeight = poleHigh - poleLow
targetPrice = close + poleHeight
//========================
// Tracking state
//========================
var float breakoutPrice = na
var float breakoutTarget = na
var int breakoutBar = na
if breakout
breakoutPrice := close
breakoutTarget := targetPrice
breakoutBar := bar_index
//========================
// Trendlines + Fill
//========================
hasTwoPH = not na(ph2_bar) and not na(ph1_bar) and not na(ph2) and not na(ph1)
hasTwoPL = not na(pl2_bar) and not na(pl1_bar) and not na(pl2) and not na(pl1)
// Store pole anchor points as absolute bar indices (fixed, don't shift)
var int poleTopBar = na // absolute bar_index of flagpole high
var int poleBaseBar = na // absolute bar_index of flagpole low
var float poleTopVal = na
var float poleBaseVal = na
if hasTwoPH and hasTwoPL and lowerHighs and higherLows
poleTopBar := bar_index + ta.highestbars(high, poleBars)
poleBaseBar := bar_index + ta.lowestbars(low, poleBars)
poleTopVal := poleHigh
poleBaseVal := poleLow
var line line1_flagpole = na
var line line2_upperTL = na
var line line3_lowerTL = na
var line line4_target = na
var linefill pennantFill = na
tlColor = color.new(color.teal, 0)
fillColor = color.new(color.teal, 75)
if hasTwoPH and hasTwoPL and lowerHighs and higherLows and
not na(poleTopBar) and not na(poleBaseBar)
// Slopes from pivot highs/lows (pennant convergence rate)
upperSlope = (ph1 - ph2) / math.max(ph1_bar - ph2_bar, 1) // negative
lowerSlope = (pl1 - pl2) / math.max(pl1_bar - pl2_bar, 1) // positive
// Apex: where upper line (from poleTop) meets lower line (from poleBase)
// poleTopVal + upperSlope(x - poleTopBar) = poleBaseVal + lowerSlope(x - poleBaseBar)
// → x = (poleTopVal - poleBaseVal + lowerSlopepoleBaseBar - upperSlopepoleTopBar) / (lowerSlope - upperSlope)
denom = lowerSlope - upperSlope // positive (lower>0, upper<0)
numer = poleTopVal - poleBaseVal + lowerSlope poleBaseBar - upperSlope poleTopBar
apexBar = int(numer / denom)
apexBar := math.max(math.min(apexBar, bar_index + 80), bar_index)
apexPrice = poleTopVal + upperSlope * (apexBar - poleTopBar)
// Line 1 — Flagpole: poleBase → poleTop (diagonal ascending)
line.delete(line1_flagpole)
line1_flagpole := line.new(poleBaseBar, poleBaseVal, poleTopBar, poleTopVal,
color=tlColor, width=2, style=line.style_solid, extend=extend.none)
// Line 2 — Upper TL: poleTop → apex (descending from flagpole high)
line.delete(line2_upperTL)
line2_upperTL := line.new(poleTopBar, poleTopVal, apexBar, apexPrice,
color=tlColor, width=2, style=line.style_solid, extend=extend.none)
// Line 3 — Lower TL: poleBase → apex (ascending from flagpole low)
line.delete(line3_lowerTL)
line3_lowerTL := line.new(poleBaseBar, poleBaseVal, apexBar, apexPrice,
color=tlColor, width=2, style=line.style_solid, extend=extend.none)
// Fill the full triangle between upper and lower TL
linefill.delete(pennantFill)
pennantFill := linefill.new(line2_upperTL, line3_lowerTL, color=fillColor)
// Breakout signal
plotshape(breakout, title="Bullish Pennant Breakout",
style=shape.labelup, location=location.belowbar,
color=tlColor, textcolor=color.white,
text="Pennant\nBreakout", size=size.normal)
// Target horizontal line after breakout (teal dashed)
if breakout and showTarget
line.delete(line4_target)
line4_target := line.new(bar_index, targetPrice, bar_index + 20, targetPrice,
color=tlColor, width=2, style=line.style_dashed, extend=extend.none)
//========================
// Labels
//========================
if breakout
label.new(bar_index, low,
text="Bullish Pennant\nEntry: " + str.tostring(close, "##.##") +
"\nTarget: " + str.tostring(targetPrice, "##.##") +
"\nSL: " + str.tostring(pl1, "##.##"),
style=label.style_label_up,
color=color.teal, textcolor=color.white, size=size.small)
//========================
// Alerts
//========================
alertcondition(breakout, title="Bullish Pennant Breakout",
message="{{ticker}} Bullish Pennant breakout at {{close}} — target pole height above")
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
Claude Code claude-sonnet-4-6
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
VS Code integrated terminal
Additional Information
_No response_
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗