Title: Image-to-code visual matching fails for complex chart patterns (Pine Script)

Resolved 💬 2 comments Opened Apr 2, 2026 by githubpalam Closed Apr 5, 2026

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

  1. Show Claude a TradingView chart with a Bullish Pennant pattern (trendlines + fill)
  2. Ask Claude to write Pine Script that draws the same shape
  3. Claude produces: bowtie shapes, disconnected triangles, wrong slope direction,

tiny shapes in wrong location

  1. 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:

  1. Describe geometry first (anchor points, directions, intersections) before writing code
  2. Recognize linefill fills BOTH sides if lines cross — stop lines at apex
  3. Lock bar indices as var so they don't shift per bar
  4. 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_

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗