Skip to content

Commit 50eaa06

Browse files
feat(pygal): implement indicator-rsi (#3265)
## Implementation: `indicator-rsi` - pygal Implements the **pygal** version of `indicator-rsi`. **File:** `plots/indicator-rsi/implementations/pygal.py` **Parent Issue:** #3229 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20795216382)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9a81dcd commit 50eaa06

2 files changed

Lines changed: 337 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
""" pyplots.ai
2+
indicator-rsi: RSI Technical Indicator Chart
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 85/100 | Created: 2026-01-07
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Data - Generate realistic RSI values over 120 trading days
13+
np.random.seed(42)
14+
15+
n_days = 120
16+
lookback = 14
17+
18+
# Generate price changes that produce RSI entering both overbought (>70) and oversold (<30) zones
19+
# Use stronger trending to ensure RSI reaches extreme values
20+
base_changes = np.random.randn(n_days) * 0.8
21+
22+
# Strong uptrend periods (push RSI above 70) - increased magnitude
23+
base_changes[15:32] += 4.0 # Strong bullish phase
24+
base_changes[75:92] += 4.5 # Another strong bullish phase
25+
26+
# Strong downtrend periods (push RSI below 30) - increased magnitude
27+
base_changes[40:57] -= 4.0 # Strong bearish phase
28+
base_changes[100:115] -= 3.5 # Another bearish phase
29+
30+
price_changes = base_changes
31+
32+
# Calculate RSI using exponential moving average
33+
gains = np.where(price_changes > 0, price_changes, 0)
34+
losses = np.where(price_changes < 0, -price_changes, 0)
35+
36+
# Initialize EMA
37+
avg_gain = np.zeros(n_days)
38+
avg_loss = np.zeros(n_days)
39+
40+
# First average
41+
avg_gain[lookback - 1] = np.mean(gains[:lookback])
42+
avg_loss[lookback - 1] = np.mean(losses[:lookback])
43+
44+
# EMA for subsequent values
45+
alpha = 1 / lookback
46+
for i in range(lookback, n_days):
47+
avg_gain[i] = alpha * gains[i] + (1 - alpha) * avg_gain[i - 1]
48+
avg_loss[i] = alpha * losses[i] + (1 - alpha) * avg_loss[i - 1]
49+
50+
# Calculate RSI (avoid division by zero)
51+
with np.errstate(divide="ignore", invalid="ignore"):
52+
rs = np.divide(avg_gain, avg_loss, out=np.full_like(avg_gain, 100.0), where=avg_loss > 0)
53+
rsi = 100 - (100 / (1 + rs))
54+
rsi[:lookback] = 50 # Fill initial values with neutral
55+
56+
57+
# Custom style for large canvas
58+
custom_style = Style(
59+
background="white",
60+
plot_background="white",
61+
foreground="#333333",
62+
foreground_strong="#333333",
63+
foreground_subtle="#666666",
64+
colors=(
65+
"#D04040", # Overbought line (red)
66+
"#30A030", # Oversold line (green)
67+
"#888888", # Centerline (gray)
68+
"#306998", # RSI line (blue)
69+
),
70+
title_font_size=80,
71+
label_font_size=52,
72+
major_label_font_size=46,
73+
legend_font_size=52,
74+
value_font_size=40,
75+
stroke_width=6,
76+
opacity=0.95,
77+
opacity_hover=1.0,
78+
)
79+
80+
# Create chart with custom configuration for zone rendering
81+
chart = pygal.Line(
82+
width=4800,
83+
height=2700,
84+
style=custom_style,
85+
title="indicator-rsi · pygal · pyplots.ai",
86+
x_title="Trading Period (120 days, 14-period RSI lookback)",
87+
y_title="RSI Value (0-100)",
88+
show_dots=False,
89+
show_x_guides=False,
90+
show_y_guides=True,
91+
range=(0, 100),
92+
interpolate="cubic",
93+
legend_at_bottom=True,
94+
legend_box_size=40,
95+
margin=60,
96+
margin_bottom=180,
97+
show_x_labels=False,
98+
)
99+
100+
# Create RSI data with zone-based coloring approach
101+
# Since pygal doesn't support horizontal bands natively, we'll use secondary value lines
102+
# and visual indication through dashed threshold lines with clear colors
103+
104+
# Add threshold lines (constant values across all days)
105+
overbought_line = [70] * n_days
106+
oversold_line = [30] * n_days
107+
centerline = [50] * n_days
108+
109+
# Add overbought threshold line (red, dashed)
110+
chart.add("Overbought (70)", overbought_line, stroke_style={"width": 5, "dasharray": "20,10"}, show_dots=False)
111+
112+
# Add oversold threshold line (green, dashed)
113+
chart.add("Oversold (30)", oversold_line, stroke_style={"width": 5, "dasharray": "20,10"}, show_dots=False)
114+
115+
# Add centerline (gray, dotted)
116+
chart.add("Centerline (50)", centerline, stroke_style={"width": 3, "dasharray": "10,10"}, show_dots=False)
117+
118+
# Add RSI data last so it appears on top with thicker line
119+
chart.add("RSI (14)", list(rsi), stroke_style={"width": 8}, show_dots=False)
120+
121+
# Save as PNG and HTML
122+
chart.render_to_png("plot.png")
123+
chart.render_to_file("plot.html")
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
library: pygal
2+
specification_id: indicator-rsi
3+
created: '2026-01-07T20:21:37Z'
4+
updated: '2026-01-07T20:44:20Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20795216382
7+
issue: 3229
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/indicator-rsi/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/indicator-rsi/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/indicator-rsi/pygal/plot.html
13+
quality_score: 85
14+
review:
15+
strengths:
16+
- Y-axis correctly fixed 0-100 with all three reference lines (70, 30, 50) as horizontal
17+
thresholds
18+
- RSI data demonstrates excellent feature coverage entering both overbought and
19+
oversold zones multiple times
20+
- Proper RSI calculation using EMA method with 14-period lookback
21+
- Good use of pygal Style customization for large canvas rendering
22+
- Clean smooth cubic interpolation makes the RSI trend easy to follow
23+
weaknesses:
24+
- Missing zone shading/highlighting for overbought (70-100) and oversold (0-30)
25+
regions as suggested in spec notes
26+
- Red/green color scheme for threshold lines could be improved for colorblind accessibility
27+
image_description: 'The plot displays an RSI (Relative Strength Index) technical
28+
indicator chart rendered as a smooth blue line (cubic interpolation) showing RSI
29+
values over 120 trading periods. The Y-axis is correctly fixed from 0 to 100.
30+
Three horizontal reference lines are present: a red dashed line at 70 (overbought
31+
threshold), a green dashed line at 30 (oversold threshold), and a gray dotted
32+
line at 50 (centerline). The RSI line clearly enters both overbought territory
33+
(peaking at ~97) and oversold territory (dipping to ~14), demonstrating the full
34+
range of RSI momentum behavior. The title "indicator-rsi · pygal · pyplots.ai"
35+
is displayed at the top. A legend at the bottom identifies all four elements with
36+
appropriate color coding. The layout has good proportions with the plot filling
37+
the canvas well.'
38+
criteria_checklist:
39+
visual_quality:
40+
score: 34
41+
max: 40
42+
items:
43+
- id: VQ-01
44+
name: Text Legibility
45+
score: 9
46+
max: 10
47+
passed: true
48+
comment: Title, axis labels, and legend text are clearly readable with appropriately
49+
scaled font sizes for the 4800x2700 canvas
50+
- id: VQ-02
51+
name: No Overlap
52+
score: 8
53+
max: 8
54+
passed: true
55+
comment: No overlapping text elements; legend at bottom is well-spaced
56+
- id: VQ-03
57+
name: Element Visibility
58+
score: 7
59+
max: 8
60+
passed: true
61+
comment: RSI line is clearly visible with good stroke width; threshold lines
62+
appropriately styled as dashed
63+
- id: VQ-04
64+
name: Color Accessibility
65+
score: 4
66+
max: 5
67+
passed: true
68+
comment: Red/green threshold lines could be problematic for colorblind users,
69+
though dashed styling helps differentiate
70+
- id: VQ-05
71+
name: Layout Balance
72+
score: 4
73+
max: 5
74+
passed: true
75+
comment: Good canvas utilization; slight excess whitespace at bottom due to
76+
legend placement
77+
- id: VQ-06
78+
name: Axis Labels
79+
score: 2
80+
max: 2
81+
passed: true
82+
comment: Y-axis labeled with range info; X-axis labeled with trading period
83+
context
84+
- id: VQ-07
85+
name: Grid & Legend
86+
score: 0
87+
max: 2
88+
passed: false
89+
comment: Y-guides visible but grid lines are somewhat sparse
90+
spec_compliance:
91+
score: 21
92+
max: 25
93+
items:
94+
- id: SC-01
95+
name: Plot Type
96+
score: 8
97+
max: 8
98+
passed: true
99+
comment: Correct line chart for RSI oscillator
100+
- id: SC-02
101+
name: Data Mapping
102+
score: 5
103+
max: 5
104+
passed: true
105+
comment: RSI values correctly plotted on 0-100 scale over time
106+
- id: SC-03
107+
name: Required Features
108+
score: 3
109+
max: 5
110+
passed: false
111+
comment: Has threshold lines at 30, 70, and 50; missing zone shading for overbought/oversold
112+
regions
113+
- id: SC-04
114+
name: Data Range
115+
score: 3
116+
max: 3
117+
passed: true
118+
comment: Y-axis shows full 0-100 range as required
119+
- id: SC-05
120+
name: Legend Accuracy
121+
score: 2
122+
max: 2
123+
passed: true
124+
comment: Legend correctly identifies all elements
125+
- id: SC-06
126+
name: Title Format
127+
score: 2
128+
max: 2
129+
passed: true
130+
comment: Uses correct format indicator-rsi · pygal · pyplots.ai
131+
data_quality:
132+
score: 18
133+
max: 20
134+
items:
135+
- id: DQ-01
136+
name: Feature Coverage
137+
score: 8
138+
max: 8
139+
passed: true
140+
comment: RSI enters both overbought (>70, peaks ~97) and oversold (<30, dips
141+
~14) zones multiple times
142+
- id: DQ-02
143+
name: Realistic Context
144+
score: 5
145+
max: 7
146+
passed: true
147+
comment: Trading context appropriate; RSI calculation uses proper EMA method
148+
with 14-period lookback
149+
- id: DQ-03
150+
name: Appropriate Scale
151+
score: 5
152+
max: 5
153+
passed: true
154+
comment: RSI values mathematically correct (0-100 bounded) with realistic
155+
momentum patterns
156+
code_quality:
157+
score: 9
158+
max: 10
159+
items:
160+
- id: CQ-01
161+
name: KISS Structure
162+
score: 3
163+
max: 3
164+
passed: true
165+
comment: 'Clean linear structure: imports → data → RSI calculation → chart
166+
→ save'
167+
- id: CQ-02
168+
name: Reproducibility
169+
score: 3
170+
max: 3
171+
passed: true
172+
comment: Uses np.random.seed(42)
173+
- id: CQ-03
174+
name: Clean Imports
175+
score: 2
176+
max: 2
177+
passed: true
178+
comment: Only necessary imports (numpy, pygal, Style)
179+
- id: CQ-04
180+
name: No Deprecated API
181+
score: 1
182+
max: 1
183+
passed: true
184+
comment: Uses current pygal API
185+
- id: CQ-05
186+
name: Output Correct
187+
score: 0
188+
max: 1
189+
passed: false
190+
comment: Saves as plot.png and plot.html correctly
191+
library_features:
192+
score: 3
193+
max: 5
194+
items:
195+
- id: LF-01
196+
name: Distinctive Features
197+
score: 3
198+
max: 5
199+
passed: true
200+
comment: Custom Style with scaled fonts, stroke_style for dashed lines, cubic
201+
interpolation; interactive tooltips not fully leveraged
202+
verdict: APPROVED
203+
impl_tags:
204+
dependencies: []
205+
techniques:
206+
- html-export
207+
patterns:
208+
- data-generation
209+
- iteration-over-groups
210+
dataprep:
211+
- rolling-window
212+
styling:
213+
- alpha-blending
214+
- grid-styling

0 commit comments

Comments
 (0)