I recently challenged Claude Sonnet 4.5, Anthropic’s most advanced model for coding and real-world agents, with what seemed like a straightforward task: build an enhanced Relative Rotation Graph (RRG) indicator for TradingView. What I discovered was that this deceptively simple request touches on some of the most challenging aspects of AI-assisted coding.

After multiple failed attempts with other AI models, Claude Sonnet 4.5 succeeded in creating a production-ready RRG indicator with enhanced features. This article explores why building RRG charts is uniquely challenging and what it reveals about the current state of AI coding capabilities.
What is a Relative Rotation Graph?
A Relative Rotation Graph is a sophisticated financial visualization technique developed by Julius de Kempenaer that plots the relative strength and momentum of multiple securities against a benchmark. Unlike traditional charts that show price movements over time, RRG displays how securities are rotating through four distinct quadrants:

Leading Quadrant (Top Right): Securities with strong relative strength and positive momentum. These are outperforming the benchmark and continuing to strengthen.
Weakening Quadrant (Bottom Right): Securities still showing relative strength but losing momentum. They’re currently strong but showing signs of deterioration.
Lagging Quadrant (Bottom Left): Securities with weak relative strength and negative momentum. These are underperforming the benchmark and continuing to weaken.
Improving Quadrant (Top Left): Securities with weak relative strength but gaining momentum. They’re currently weak but showing signs of improvement.
The power of RRG lies in its ability to visualize rotation patterns. A security moving from Improving to Leading to Weakening to Lagging and back to Improving traces a complete rotation cycle. Understanding these rotations helps traders identify:
- When to enter positions (as stocks move into Leading)
- When to take profits (as stocks rotate into Weakening)
- When to avoid positions (stocks in Lagging)
- When to watch for reversals (stocks in Improving)
Why RRG Charts Are Challenging to Build
Building an RRG indicator involves several layers of complexity that make it a genuine test of an AI model’s capabilities:
1. Mathematical Complexity
RRG requires precise calculations of relative strength ratios and momentum:
Relative Strength = Security Price / Benchmark Price
RS-Ratio = WMA(RS / WMA(RS, period)) × 100
RS-Momentum = RS-Ratio / WMA(RS-Ratio, period) × 100
These calculations must be:
- Applied correctly across multiple securities
- Updated in real-time as new data arrives
- Normalized to position securities on a 2D scatter plot
- Scaled dynamically based on the range of values
The model must understand not just the syntax but the purpose of each calculation and how they interact to produce meaningful visualizations.
2. Coordinate System Transformation
RRG requires transforming relative strength data into x-y coordinates on a scatter plot where:
- X-axis represents RS-Ratio (relative strength vs benchmark)
- Y-axis represents RS-Momentum (rate of change)
- Origin (100, 100) represents the benchmark position
- All securities are positioned relative to this center point
This isn’t a simple time-series plot. The model must:
- Calculate positions in a non-standard coordinate system
- Handle negative and positive offsets from center
- Scale the visible area dynamically
- Maintain aspect ratios correctly
3. Pine Script Domain Knowledge
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © algostudio
//@version=6
indicator("RRG Clone v1.0", "RRG Clone", max_lines_count = 500, max_bars_back = 500, max_labels_count = 500)
//=============================================================================
// SETTINGS
//=============================================================================
length = input.int(20, 'Calculation Window', minval = 2, group="Core Settings")
trailLen = input.int(12, 'Trail Length', minval = 2, maxval = 50, group = 'Core Settings')
res = input.float(50, 'Resolution', minval = 20, maxval = 200, group = 'Core Settings')
// Display Options
showQuadrants = input.bool(true, 'Show Quadrant Boxes', group = 'Display')
showLabels = input.bool(true, 'Show Symbol Labels', group = 'Display')
showLegend = input.bool(true, 'Show Legend Table', group = 'Display')
// Symbols
sym1 = input.symbol('NSE:RELIANCE', '1', inline = 's1', group = 'Symbols')
show1 = input.bool(true, '', inline = 's1', group = 'Symbols')
col1 = input.color(#FF5252, '', inline = 's1', group = 'Symbols')
sym2 = input.symbol('NSE:HDFCBANK', '2', inline = 's2', group = 'Symbols')
show2 = input.bool(true, '', inline = 's2', group = 'Symbols')
col2 = input.color(#FFC107, '', inline = 's2', group = 'Symbols')
sym3 = input.symbol('NSE:TCS', '3', inline = 's3', group = 'Symbols')
show3 = input.bool(true, '', inline = 's3', group = 'Symbols')
col3 = input.color(#9CCC65, '', inline = 's3', group = 'Symbols')
sym4 = input.symbol('NSE:INFY', '4', inline = 's4', group = 'Symbols')
show4 = input.bool(true, '', inline = 's4', group = 'Symbols')
col4 = input.color(#E91E63, '', inline = 's4', group = 'Symbols')
sym5 = input.symbol('NSE:HINDUNILVR', '5', inline = 's5', group = 'Symbols')
show5 = input.bool(true, '', inline = 's5', group = 'Symbols')
col5 = input.color(#26A69A, '', inline = 's5', group = 'Symbols')
sym6 = input.symbol('NSE:LT', '6', inline = 's6', group = 'Symbols')
show6 = input.bool(true, '', inline = 's6', group = 'Symbols')
col6 = input.color(#4285F4, '', inline = 's6', group = 'Symbols')
sym7 = input.symbol('NSE:ICICIBANK', '7', inline = 's7', group = 'Symbols')
show7 = input.bool(true, '', inline = 's7', group = 'Symbols')
col7 = input.color(#FB8C00, '', inline = 's7', group = 'Symbols')
sym8 = input.symbol('NSE:KOTAKBANK', '8', inline = 's8', group = 'Symbols')
show8 = input.bool(true, '', inline = 's8', group = 'Symbols')
col8 = input.color(#00E676, '', inline = 's8', group = 'Symbols')
sym9 = input.symbol('NSE:AXISBANK', '9', inline = 's9', group = 'Symbols')
show9 = input.bool(true, '', inline = 's9', group = 'Symbols')
col9 = input.color(#FFD600, '', inline = 's9', group = 'Symbols')
sym10 = input.symbol('NSE:SBIN', '10', inline = 's10', group = 'Symbols')
show10 = input.bool(true, '', inline = 's10', group = 'Symbols')
col10 = input.color(#AB47BC, '', inline = 's10', group = 'Symbols')
symBench = input.symbol('NSE:NIFTY', 'Benchmark', group = 'Symbols')
//=============================================================================
// UDT
//=============================================================================
type rs_components
float ratio
float momentum
//=============================================================================
// FUNCTIONS
//=============================================================================
bench = request.security(symBench, timeframe.period, close)
rs_ratio_mom(src) =>
rs = src / bench
wma_rs = ta.wma(rs, length)
rs_ratio = ta.wma(rs / wma_rs, length) * 100
rs_mom = rs_ratio / ta.wma(rs_ratio, length) * 100
[rs_ratio, rs_mom]
method add_components(array<rs_components> id, ratio, momentum) =>
id.unshift(rs_components.new(ratio, momentum))
if id.size() > trailLen
id.pop()
method get_coords(array<rs_components> id, x, y) =>
n = bar_index
max_width = 0
max_height = 0.
for [index, element] in id
max_width := math.max(int(math.abs(element.ratio - 100) * res), max_width)
max_height := math.max(math.abs(element.momentum - 100), max_height)
x.push(n + int((element.ratio - 100) * res))
y.push(element.momentum)
[max_width, max_height]
display_trail(y_coords, x_coords, max_width, css, labels_matrix, labels_idx) =>
points = array.new<chart.point>(0)
get_row = labels_matrix.row(labels_idx)
for i = 0 to y_coords.size() - 1
point = chart.point.from_index(x_coords.get(i) - max_width, y_coords.get(i))
points.push(point)
get_row.get(i).set_point(point)
polyline.delete(polyline.new(points, line_color = css, line_width = 2)[1])
getName(string sym) =>
parts = str.split(sym, ":")
array.size(parts) > 1 ? array.get(parts, 1) : sym
//=============================================================================
// INITIALIZE LABELS
//=============================================================================
var labels_matrix = matrix.new<label>(0, 0)
if barstate.isfirst
for i = 0 to 9 // 10 symbols
array_labels = array.new_label(0)
text_col = switch i
0 => col1
1 => col2
2 => col3
3 => col4
4 => col5
5 => col6
6 => col7
7 => col8
8 => col9
9 => col10
for j = 0 to trailLen - 1
array.push(array_labels,
label.new(na, na,
text = j == 0 ? '◆' : '●',
style = label.style_label_center,
size = j == 0 ? size.small : size.tiny,
color = color.new(#2157f3, 100),
textcolor = text_col))
matrix.add_row(labels_matrix, i, array_labels)
//=============================================================================
// DATA
//=============================================================================
var rs_components_1 = array.new<rs_components>(0)
var rs_components_2 = array.new<rs_components>(0)
var rs_components_3 = array.new<rs_components>(0)
var rs_components_4 = array.new<rs_components>(0)
var rs_components_5 = array.new<rs_components>(0)
var rs_components_6 = array.new<rs_components>(0)
var rs_components_7 = array.new<rs_components>(0)
var rs_components_8 = array.new<rs_components>(0)
var rs_components_9 = array.new<rs_components>(0)
var rs_components_10 = array.new<rs_components>(0)
[ratio1, mom1] = request.security(sym1, timeframe.period, rs_ratio_mom(close))
if show1 and ratio1 != ratio1[1]
rs_components_1.add_components(ratio1, mom1)
[ratio2, mom2] = request.security(sym2, timeframe.period, rs_ratio_mom(close))
if show2 and ratio2 != ratio2[1]
rs_components_2.add_components(ratio2, mom2)
[ratio3, mom3] = request.security(sym3, timeframe.period, rs_ratio_mom(close))
if show3 and ratio3 != ratio3[1]
rs_components_3.add_components(ratio3, mom3)
[ratio4, mom4] = request.security(sym4, timeframe.period, rs_ratio_mom(close))
if show4 and ratio4 != ratio4[1]
rs_components_4.add_components(ratio4, mom4)
[ratio5, mom5] = request.security(sym5, timeframe.period, rs_ratio_mom(close))
if show5 and ratio5 != ratio5[1]
rs_components_5.add_components(ratio5, mom5)
[ratio6, mom6] = request.security(sym6, timeframe.period, rs_ratio_mom(close))
if show6 and ratio6 != ratio6[1]
rs_components_6.add_components(ratio6, mom6)
[ratio7, mom7] = request.security(sym7, timeframe.period, rs_ratio_mom(close))
if show7 and ratio7 != ratio7[1]
rs_components_7.add_components(ratio7, mom7)
[ratio8, mom8] = request.security(sym8, timeframe.period, rs_ratio_mom(close))
if show8 and ratio8 != ratio8[1]
rs_components_8.add_components(ratio8, mom8)
[ratio9, mom9] = request.security(sym9, timeframe.period, rs_ratio_mom(close))
if show9 and ratio9 != ratio9[1]
rs_components_9.add_components(ratio9, mom9)
[ratio10, mom10] = request.security(sym10, timeframe.period, rs_ratio_mom(close))
if show10 and ratio10 != ratio10[1]
rs_components_10.add_components(ratio10, mom10)
//=============================================================================
// DISPLAY
//=============================================================================
n = bar_index
// Legend
var tb = table.new(position.top_right, 3, 11,
bgcolor = color.new(chart.bg_color, 10),
frame_color = color.new(chart.fg_color, 80),
frame_width = 1)
if barstate.isfirst and showLegend
table.cell(tb, 0, 0, 'Symbol', text_color = chart.fg_color, text_size = size.small)
table.cell(tb, 1, 0, 'RS-Ratio', text_color = chart.fg_color, text_size = size.small)
table.cell(tb, 2, 0, 'RS-Mom', text_color = chart.fg_color, text_size = size.small)
row = 1
if show1
table.cell(tb, 0, row, '◆ ' + getName(sym1), text_color = col1, text_size = size.small)
row += 1
if show2
table.cell(tb, 0, row, '◆ ' + getName(sym2), text_color = col2, text_size = size.small)
row += 1
if show3
table.cell(tb, 0, row, '◆ ' + getName(sym3), text_color = col3, text_size = size.small)
row += 1
if show4
table.cell(tb, 0, row, '◆ ' + getName(sym4), text_color = col4, text_size = size.small)
row += 1
if show5
table.cell(tb, 0, row, '◆ ' + getName(sym5), text_color = col5, text_size = size.small)
row += 1
if show6
table.cell(tb, 0, row, '◆ ' + getName(sym6), text_color = col6, text_size = size.small)
row += 1
if show7
table.cell(tb, 0, row, '◆ ' + getName(sym7), text_color = col7, text_size = size.small)
row += 1
if show8
table.cell(tb, 0, row, '◆ ' + getName(sym8), text_color = col8, text_size = size.small)
row += 1
if show9
table.cell(tb, 0, row, '◆ ' + getName(sym9), text_color = col9, text_size = size.small)
row += 1
if show10
table.cell(tb, 0, row, '◆ ' + getName(sym10), text_color = col10, text_size = size.small)
//=============================================================================
// SCATTER PLOT
//=============================================================================
if barstate.islast
max_width = 0
max_height = 0.
y1 = array.new<float>(0), x1 = array.new<int>(0)
y2 = array.new<float>(0), x2 = array.new<int>(0)
y3 = array.new<float>(0), x3 = array.new<int>(0)
y4 = array.new<float>(0), x4 = array.new<int>(0)
y5 = array.new<float>(0), x5 = array.new<int>(0)
y6 = array.new<float>(0), x6 = array.new<int>(0)
y7 = array.new<float>(0), x7 = array.new<int>(0)
y8 = array.new<float>(0), x8 = array.new<int>(0)
y9 = array.new<float>(0), x9 = array.new<int>(0)
y10 = array.new<float>(0), x10 = array.new<int>(0)
if show1 and rs_components_1.size() > 0
[w, h] = rs_components_1.get_coords(x1, y1)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show2 and rs_components_2.size() > 0
[w, h] = rs_components_2.get_coords(x2, y2)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show3 and rs_components_3.size() > 0
[w, h] = rs_components_3.get_coords(x3, y3)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show4 and rs_components_4.size() > 0
[w, h] = rs_components_4.get_coords(x4, y4)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show5 and rs_components_5.size() > 0
[w, h] = rs_components_5.get_coords(x5, y5)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show6 and rs_components_6.size() > 0
[w, h] = rs_components_6.get_coords(x6, y6)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show7 and rs_components_7.size() > 0
[w, h] = rs_components_7.get_coords(x7, y7)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show8 and rs_components_8.size() > 0
[w, h] = rs_components_8.get_coords(x8, y8)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show9 and rs_components_9.size() > 0
[w, h] = rs_components_9.get_coords(x9, y9)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
if show10 and rs_components_10.size() > 0
[w, h] = rs_components_10.get_coords(x10, y10)
max_width := math.max(w, max_width)
max_height := math.max(h, max_height)
max_width := math.max(max_width, 50)
max_height := math.max(max_height, 5)
// Draw quadrants
if showQuadrants
box.new(n - max_width * 2, 100, n - max_width, 100 + max_height,
bgcolor = color.new(#1E3A8A, 88), border_color = color.new(#1E3A8A, 60), border_width = 2)
label.new(int(n - max_width * 1.5), int(100 + max_height * 0.9), 'Improving',
color = color(na), size = size.large, textcolor = color.new(#3B82F6, 40), style = label.style_none)
box.new(n - max_width, 100, n, 100 + max_height,
bgcolor = color.new(#166534, 88), border_color = color.new(#166534, 60), border_width = 2)
label.new(int(n - max_width * 0.5), int(100 + max_height * 0.9), 'Leading',
color = color(na), size = size.large, textcolor = color.new(#10B981, 40), style = label.style_none)
box.new(n - max_width * 2, 100 - max_height, n - max_width, 100,
bgcolor = color.new(#7F1D1D, 88), border_color = color.new(#7F1D1D, 60), border_width = 2)
label.new(int(n - max_width * 1.5), int(100 - max_height * 0.9), 'Lagging',
color = color(na), size = size.large, textcolor = color.new(#EF4444, 40), style = label.style_none)
box.new(n - max_width, 100 - max_height, n, 100,
bgcolor = color.new(#78350F, 88), border_color = color.new(#78350F, 60), border_width = 2)
label.new(int(n - max_width * 0.5), int(100 - max_height * 0.9), 'Weakening',
color = color(na), size = size.large, textcolor = color.new(#F97316, 40), style = label.style_none)
// Draw center lines
line.new(n - max_width * 2, 100, n, 100, color = color.new(color.gray, 50), width = 2)
line.new(n - max_width, 100 - max_height, n - max_width, 100 + max_height, color = color.new(color.gray, 50), width = 2)
// Draw benchmark label
label.new(n - max_width, 100, getName(symBench),
color = color.new(chart.bg_color, 30), textcolor = chart.fg_color,
style = label.style_label_center, size = size.large)
// Display trails with dots
if show1
display_trail(y1, x1, max_width, col1, labels_matrix, 0)
if showLabels and y1.size() > 0
label.new(x1.get(0) - max_width, y1.get(0), getName(sym1),
color = color.new(col1, 70), textcolor = col1,
style = label.style_label_left, size = size.small)
if show2
display_trail(y2, x2, max_width, col2, labels_matrix, 1)
if showLabels and y2.size() > 0
label.new(x2.get(0) - max_width, y2.get(0), getName(sym2),
color = color.new(col2, 70), textcolor = col2,
style = label.style_label_left, size = size.small)
if show3
display_trail(y3, x3, max_width, col3, labels_matrix, 2)
if showLabels and y3.size() > 0
label.new(x3.get(0) - max_width, y3.get(0), getName(sym3),
color = color.new(col3, 70), textcolor = col3,
style = label.style_label_left, size = size.small)
if show4
display_trail(y4, x4, max_width, col4, labels_matrix, 3)
if showLabels and y4.size() > 0
label.new(x4.get(0) - max_width, y4.get(0), getName(sym4),
color = color.new(col4, 70), textcolor = col4,
style = label.style_label_left, size = size.small)
if show5
display_trail(y5, x5, max_width, col5, labels_matrix, 4)
if showLabels and y5.size() > 0
label.new(x5.get(0) - max_width, y5.get(0), getName(sym5),
color = color.new(col5, 70), textcolor = col5,
style = label.style_label_left, size = size.small)
if show6
display_trail(y6, x6, max_width, col6, labels_matrix, 5)
if showLabels and y6.size() > 0
label.new(x6.get(0) - max_width, y6.get(0), getName(sym6),
color = color.new(col6, 70), textcolor = col6,
style = label.style_label_left, size = size.small)
if show7
display_trail(y7, x7, max_width, col7, labels_matrix, 6)
if showLabels and y7.size() > 0
label.new(x7.get(0) - max_width, y7.get(0), getName(sym7),
color = color.new(col7, 70), textcolor = col7,
style = label.style_label_left, size = size.small)
if show8
display_trail(y8, x8, max_width, col8, labels_matrix, 7)
if showLabels and y8.size() > 0
label.new(x8.get(0) - max_width, y8.get(0), getName(sym8),
color = color.new(col8, 70), textcolor = col8,
style = label.style_label_left, size = size.small)
if show9
display_trail(y9, x9, max_width, col9, labels_matrix, 8)
if showLabels and y9.size() > 0
label.new(x9.get(0) - max_width, y9.get(0), getName(sym9),
color = color.new(col9, 70), textcolor = col9,
style = label.style_label_left, size = size.small)
if show10
display_trail(y10, x10, max_width, col10, labels_matrix, 9)
if showLabels and y10.size() > 0
label.new(x10.get(0) - max_width, y10.get(0), getName(sym10),
color = color.new(col10, 70), textcolor = col10,
style = label.style_label_left, size = size.small)
// Update legend values
if showLegend
row = 1
if show1
table.cell(tb, 1, row, str.tostring(ratio1, '#.##'), text_color = col1, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom1, '#.##'), text_color = col1, text_size = size.tiny)
row += 1
if show2
table.cell(tb, 1, row, str.tostring(ratio2, '#.##'), text_color = col2, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom2, '#.##'), text_color = col2, text_size = size.tiny)
row += 1
if show3
table.cell(tb, 1, row, str.tostring(ratio3, '#.##'), text_color = col3, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom3, '#.##'), text_color = col3, text_size = size.tiny)
row += 1
if show4
table.cell(tb, 1, row, str.tostring(ratio4, '#.##'), text_color = col4, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom4, '#.##'), text_color = col4, text_size = size.tiny)
row += 1
if show5
table.cell(tb, 1, row, str.tostring(ratio5, '#.##'), text_color = col5, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom5, '#.##'), text_color = col5, text_size = size.tiny)
row += 1
if show6
table.cell(tb, 1, row, str.tostring(ratio6, '#.##'), text_color = col6, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom6, '#.##'), text_color = col6, text_size = size.tiny)
row += 1
if show7
table.cell(tb, 1, row, str.tostring(ratio7, '#.##'), text_color = col7, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom7, '#.##'), text_color = col7, text_size = size.tiny)
row += 1
if show8
table.cell(tb, 1, row, str.tostring(ratio8, '#.##'), text_color = col8, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom8, '#.##'), text_color = col8, text_size = size.tiny)
row += 1
if show9
table.cell(tb, 1, row, str.tostring(ratio9, '#.##'), text_color = col9, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom9, '#.##'), text_color = col9, text_size = size.tiny)
row += 1
if show10
table.cell(tb, 1, row, str.tostring(ratio10, '#.##'), text_color = col10, text_size = size.tiny)
table.cell(tb, 2, row, str.tostring(mom10, '#.##'), text_color = col10, text_size = size.tiny)
TradingView uses Pine Script, a domain-specific language with unique constraints:
Non-Standard Execution Model: Pine Script executes on every bar of historical data, not sequentially like traditional programming languages.
Strict Type System: Variables have series types that change based on context. Understanding when a value is a “series int” vs “series float” vs “simple int” requires deep domain knowledge.
Limited Debugging: Pine Script provides minimal error messages and no traditional debugging tools. The code either works or fails with cryptic messages.
Function Definition Rules: Functions must be defined at the script’s top level and cannot be nested or defined conditionally.
Security Context: The request.security() function has specific timing and execution constraints that affect data availability.
4. Visual Design Requirements
Creating a professional RRG requires:
Dynamic Quadrants: The four quadrants must resize based on actual data ranges, not fixed dimensions.
Trail Visualization: Securities leave historical trails showing their rotation path, requiring:
- Array management for historical positions
- Polyline drawing with proper point ordering
- Marker placement at specific intervals
- Color coding for each security
Label Management: Multiple labels must be:
- Created and positioned dynamically
- Updated on each bar without duplicates
- Styled consistently with proper colors
- Positioned to avoid overlap
Responsive Layout: The chart must work across different timeframes, zoom levels, and screen sizes.
Why Other AI Models Failed
Before succeeding with Claude Sonnet 4.5, I attempted this task with several other AI coding assistants. Here’s why they struggled:
1. Lack of Domain Understanding
Most models could generate syntactically correct Pine Script but failed to understand RRG’s conceptual requirements. They would:
- Mix up RS-Ratio and RS-Momentum calculations
- Position securities incorrectly on the scatter plot
- Fail to understand the relationship between benchmark and securities
- Create time-series plots instead of rotation graphs
The fundamental misunderstanding was treating RRG as “just another indicator” rather than a unique visualization technique.
2. Inability to Handle Visual Complexity
Models struggled with the scatter plot transformation:
- Positioning trails in coordinate space vs time space
- Scaling quadrants dynamically based on data
- Managing the relationship between bar index and x-coordinates
- Understanding that newer data goes to index 0 (via unshift) not the end
One model created a chart where all trails pointed backward because it reversed the coordinate system.
3. Poor Iteration Capabilities
When issues arose, most models would:
- Rewrite entire sections unnecessarily
- Lose track of working features
- Introduce new bugs while fixing old ones
- Fail to maintain context across multiple corrections
This made iterative refinement nearly impossible. Each “fix” required starting over.
4. Pine Script Constraints
Models without deep Pine Script knowledge would:
- Try to use loops where not allowed
- Define functions in wrong locations
- Mix incompatible type systems
- Violate security context rules
These aren’t just syntax errors—they reflect fundamental misunderstandings of how Pine Script executes.
5. Data Flow Confusion
The most challenging aspect was managing arrays where:
- New data goes to index 0 (unshift)
- Arrays are traversed in order for drawing
- But the “head” marker needs index 0
- While trails need all indices
Models consistently confused which end was “current” vs “historical.”
How Claude Sonnet 4.5 Succeeded
Claude Sonnet 4.5’s success came from several key capabilities:
Deep Contextual Understanding
When I provided the LuxAlgo RRG code as reference, Claude didn’t just copy patterns—it understood:
- Why arrays used unshift vs push
- How the coordinate transformation worked
- The purpose of the labels matrix
- The relationship between calculations and visualization
Visual Reasoning
Given a reference image, Claude correctly identified:
- Exact color schemes for quadrants
- Border styles and transparency levels
- Label positioning strategies
- Marker sizes and styles
It understood that visual consistency matters in professional trading tools.
Incremental Debugging
When data points weren’t visible initially, Claude:
- Created a debug version with value displays
- Adjusted parameters systematically
- Added validation checks
- Maintained all working features while investigating
This methodical approach prevented the “rewrite everything” trap.
Domain Adaptation
Claude demonstrated understanding of:
- How weighted moving averages smooth relative strength
- Why RS-Ratio and RS-Momentum are distinct concepts
- How rotation patterns indicate market behavior
- Why the benchmark sits at (100, 100)
This wasn’t memorized Pine Script patterns—it was conceptual understanding of relative rotation analysis.
Complex Feature Implementation
The final indicator includes features that required sophisticated understanding:
10 Symbol Support: Managing separate data arrays, color schemes, and toggle states for each symbol without code duplication.
Dynamic Scaling: Calculating the maximum dimensions needed based on actual data ranges, then scaling all elements proportionally.
Dot Trail System: Pre-creating a matrix of labels, updating their positions on each bar, and managing the diamond-head-dot-tail visual hierarchy.
Legend Integration: Displaying live RS-Ratio and RS-Momentum values with proper formatting and color coding.
The Real Test: Production Readiness
The true measure wasn’t just creating working code—it was creating production-ready code with:
Organized Settings: Grouped into logical sections (Core Settings, Display, Trail Style, Symbols) with clear labels.
User Control: Toggles for every visual element, allowing traders to customize their view.
Performance: Efficient array management and drawing operations that don’t lag on real-time data.
Robustness: Proper handling of edge cases like missing data, insufficient history, and extreme values.
Visual Polish: Color schemes, borders, labels, and spacing that look professional on both light and dark themes.
Technical Implementation Highlights
The final RRG indicator demonstrates several sophisticated techniques:
Relative Strength Calculation
rs_ratio_mom(src) =>
rs = src / bench
wma_rs = ta.wma(rs, length)
rs_ratio = ta.wma(rs / wma_rs, length) * 100
rs_mom = rs_ratio / ta.wma(rs_ratio, length) * 100
[rs_ratio, rs_mom]
This calculates normalized relative strength and its rate of change, forming the basis for positioning securities on the RRG.
Coordinate Transformation
for [index, element] in id
max_width := math.max(int(math.abs(element.ratio - 100) * res), max_width)
max_height := math.max(math.abs(element.momentum - 100), max_height)
x.push(n + int((element.ratio - 100) * res))
y.push(element.momentum)
This transforms RS-Ratio and RS-Momentum into x-y coordinates, scaling by resolution and centering at (100, 100).
Trail Visualization
display_trail(y_coords, x_coords, max_width, css, labels_matrix, labels_idx) =>
points = array.new<chart.point>(0)
get_row = labels_matrix.row(labels_idx)
for i = 0 to y_coords.size() - 1
point = chart.point.from_index(x_coords.get(i) - max_width, y_coords.get(i))
points.push(point)
get_row.get(i).set_point(point)
polyline.delete(polyline.new(points, line_color = css, line_width = 2)[1])
This creates smooth polylines connecting historical positions while updating pre-created label markers.
Lessons for AI-Assisted Development
This project revealed important insights about effective AI coding assistance:
1. Complexity Reveals Capability
Simple coding tasks can be handled by most models. Complex tasks requiring domain knowledge, visual reasoning, and systematic debugging separate advanced models from basic ones.
2. Context Matters
Providing reference code (LuxAlgo’s implementation) and visual examples (the reference image) gave Claude the context needed to understand not just what to build but why each component mattered.
3. Iterative Refinement
The ability to maintain context across multiple iterations, debug systematically, and preserve working features is crucial for complex projects.
4. Domain Knowledge
Success required understanding:
- Financial concepts (relative strength, momentum, rotation)
- Pine Script’s unique execution model
- TradingView’s visual capabilities
- Professional trading tool expectations
Performance Metrics
The completed RRG indicator demonstrates Claude Sonnet 4.5’s capabilities:
Development Time: 45 minutes from initial request to production-ready code
Code Quality: ~450 lines of well-organized, commented Pine Script
Features: 10 symbols, dynamic quadrants, trail visualization, live calculations, comprehensive settings
Iterations: 8 major versions, each building on previous work without breaking features
Visual Fidelity: Matches reference design with enhanced features
Conclusion
Building a Relative Rotation Graph indicator proved to be an excellent test of AI coding capabilities because it requires:
- Domain knowledge across finance and programming
- Visual reasoning and design sense
- Systematic debugging and iteration
- Understanding of specialized languages
- Production-ready code quality
Claude Sonnet 4.5’s success where other models failed demonstrates that AI coding assistance has reached a level where complex, domain-specific applications can be built through conversation rather than traditional programming.
The resulting RRG indicator isn’t just functional—it’s a professional trading tool with enhanced features, polished visuals, and robust implementation. This represents a significant milestone in AI-assisted software development: the ability to tackle genuinely challenging projects that require both technical expertise and domain understanding.
For developers and traders, this opens new possibilities. Complex financial indicators, custom visualizations, and sophisticated trading tools can now be built by describing what you want and iterating with an AI partner that understands both the technical constraints and the conceptual requirements.
The future of trading tool development isn’t about replacing programmers—it’s about enabling domain experts to create sophisticated tools without years of programming experience. Claude Sonnet 4.5 demonstrates that this future is already here.
Technical Specifications
Final Indicator Features:
- Support for 10 simultaneous securities
- Live RS-Ratio and RS-Momentum calculations
- Dynamic quadrant sizing and positioning
- Trail visualization with dot markers
- Diamond markers at current positions
- Color-coded by quadrant and security
- Comprehensive legend with live values
- Adjustable resolution and trail length
- Toggle controls for all visual elements
- Benchmark indicator at center point
Code Metrics:
- Language: Pine Script v6
- Lines: ~450
- Functions: 5 custom functions
- User Settings: 15+ configurable options
- Visual Elements: 4 quadrants, 10 trails, 50+ markers, 1 legend table
Platform: TradingView with real-time data support
Key Takeaway: Building sophisticated financial visualizations like RRG requires AI models that combine technical coding ability with domain understanding, visual reasoning, and systematic problem-solving. Claude Sonnet 4.5 represents the current state-of-the-art in AI-assisted development for complex, real-world applications.