Skip to content

Commit eaad117

Browse files
feat(highcharts): implement indicator-rsi (#3281)
## Implementation: `indicator-rsi` - highcharts Implements the **highcharts** version of `indicator-rsi`. **File:** `plots/indicator-rsi/implementations/highcharts.py` **Parent Issue:** #3229 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20796525489)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 44eb932 commit eaad117

2 files changed

Lines changed: 458 additions & 9 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
""" pyplots.ai
2+
indicator-rsi: RSI Technical Indicator Chart
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 92/100 | Created: 2026-01-07
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
11+
12+
import numpy as np
13+
import pandas as pd
14+
from highcharts_core.chart import Chart
15+
from highcharts_core.options import HighchartsOptions
16+
from highcharts_core.options.series.area import LineSeries
17+
from selenium import webdriver
18+
from selenium.webdriver.chrome.options import Options
19+
20+
21+
# Data - Generate synthetic stock prices and calculate RSI
22+
np.random.seed(42)
23+
n_periods = 120
24+
dates = pd.date_range("2024-01-01", periods=n_periods, freq="D")
25+
26+
# Generate realistic price movements
27+
price_changes = np.random.normal(0, 2, n_periods)
28+
price_changes[20:35] = np.random.normal(1.5, 1.5, 15) # Uptrend (will create overbought)
29+
price_changes[50:65] = np.random.normal(-1.5, 1.5, 15) # Downtrend (will create oversold)
30+
price_changes[85:95] = np.random.normal(1.2, 1.2, 10) # Another uptrend
31+
prices = 100 + np.cumsum(price_changes)
32+
33+
# Calculate RSI (14-period)
34+
delta = pd.Series(prices).diff()
35+
gain = delta.where(delta > 0, 0)
36+
loss = (-delta).where(delta < 0, 0)
37+
38+
avg_gain = gain.rolling(window=14, min_periods=14).mean()
39+
avg_loss = loss.rolling(window=14, min_periods=14).mean()
40+
41+
rs = avg_gain / avg_loss
42+
rsi = 100 - (100 / (1 + rs))
43+
rsi = rsi.fillna(50)
44+
45+
# Prepare data for Highcharts (timestamps in milliseconds)
46+
timestamps = [int(d.timestamp() * 1000) for d in dates]
47+
rsi_data = list(zip(timestamps, rsi.tolist(), strict=True))
48+
49+
# Create chart
50+
chart = Chart(container="container")
51+
chart.options = HighchartsOptions()
52+
53+
# Chart settings
54+
chart.options.chart = {
55+
"type": "line",
56+
"width": 4800,
57+
"height": 2700,
58+
"backgroundColor": "#ffffff",
59+
"spacingTop": 60,
60+
"spacingBottom": 100,
61+
"spacingLeft": 80,
62+
"spacingRight": 120,
63+
}
64+
65+
# Title
66+
chart.options.title = {
67+
"text": "indicator-rsi · highcharts · pyplots.ai",
68+
"style": {"fontSize": "56px", "fontWeight": "bold"},
69+
"margin": 40,
70+
}
71+
72+
chart.options.subtitle = {
73+
"text": "Relative Strength Index (14-period) · Synthetic Stock Data",
74+
"style": {"fontSize": "36px", "color": "#666666"},
75+
}
76+
77+
# X-axis (datetime)
78+
chart.options.x_axis = {
79+
"type": "datetime",
80+
"title": {"text": "Date", "style": {"fontSize": "36px"}, "margin": 25},
81+
"labels": {"style": {"fontSize": "28px"}},
82+
"gridLineWidth": 1,
83+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
84+
"tickInterval": 14 * 24 * 3600 * 1000, # 2-week ticks
85+
"dateTimeLabelFormats": {"day": "%b %d"},
86+
}
87+
88+
# Y-axis (0-100 fixed with plot lines and bands)
89+
chart.options.y_axis = {
90+
"title": {"text": "RSI Value", "style": {"fontSize": "36px"}, "margin": 25},
91+
"labels": {"style": {"fontSize": "28px"}},
92+
"min": 0,
93+
"max": 100,
94+
"tickInterval": 10,
95+
"gridLineWidth": 1,
96+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
97+
"plotLines": [
98+
{
99+
"value": 70,
100+
"color": "#D97706",
101+
"width": 4,
102+
"dashStyle": "Dash",
103+
"zIndex": 5,
104+
"label": {
105+
"text": "Overbought (70)",
106+
"align": "left",
107+
"style": {"fontSize": "28px", "color": "#D97706", "fontWeight": "bold"},
108+
"x": 10,
109+
},
110+
},
111+
{
112+
"value": 30,
113+
"color": "#2563EB",
114+
"width": 4,
115+
"dashStyle": "Dash",
116+
"zIndex": 5,
117+
"label": {
118+
"text": "Oversold (30)",
119+
"align": "left",
120+
"style": {"fontSize": "28px", "color": "#2563EB", "fontWeight": "bold"},
121+
"x": 10,
122+
},
123+
},
124+
{
125+
"value": 50,
126+
"color": "#6B7280",
127+
"width": 3,
128+
"dashStyle": "Dot",
129+
"zIndex": 5,
130+
"label": {
131+
"text": "Centerline (50)",
132+
"align": "left",
133+
"style": {"fontSize": "24px", "color": "#6B7280"},
134+
"x": 10,
135+
},
136+
},
137+
],
138+
"plotBands": [
139+
{
140+
"from": 70,
141+
"to": 100,
142+
"color": "rgba(217, 119, 6, 0.15)",
143+
"label": {
144+
"text": "Overbought Zone",
145+
"style": {"fontSize": "24px", "color": "#B45309"},
146+
"align": "right",
147+
"x": -20,
148+
"y": 30,
149+
},
150+
},
151+
{
152+
"from": 0,
153+
"to": 30,
154+
"color": "rgba(37, 99, 235, 0.15)",
155+
"label": {
156+
"text": "Oversold Zone",
157+
"style": {"fontSize": "24px", "color": "#1D4ED8"},
158+
"align": "right",
159+
"x": -20,
160+
"y": -10,
161+
},
162+
},
163+
],
164+
}
165+
166+
# Legend
167+
chart.options.legend = {"enabled": True, "itemStyle": {"fontSize": "32px"}, "margin": 30}
168+
169+
# Tooltip
170+
chart.options.tooltip = {
171+
"xDateFormat": "%B %d, %Y",
172+
"valueSuffix": "",
173+
"style": {"fontSize": "28px"},
174+
"valueDecimals": 1,
175+
}
176+
177+
# Plot options
178+
chart.options.plot_options = {
179+
"line": {"lineWidth": 5, "marker": {"enabled": False}, "states": {"hover": {"lineWidth": 6}}}
180+
}
181+
182+
# RSI Series
183+
series = LineSeries()
184+
series.name = "RSI (14)"
185+
series.data = rsi_data
186+
series.color = "#306998"
187+
188+
chart.add_series(series)
189+
190+
# Disable credits
191+
chart.options.credits = {"enabled": False}
192+
193+
# Export to PNG via Selenium
194+
highcharts_url = "https://code.highcharts.com/highcharts.js"
195+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
196+
highcharts_js = response.read().decode("utf-8")
197+
198+
html_str = chart.to_js_literal()
199+
html_content = f"""<!DOCTYPE html>
200+
<html>
201+
<head>
202+
<meta charset="utf-8">
203+
<script>{highcharts_js}</script>
204+
</head>
205+
<body style="margin:0;">
206+
<div id="container" style="width: 4800px; height: 2700px;"></div>
207+
<script>{html_str}</script>
208+
</body>
209+
</html>"""
210+
211+
# Save HTML version for interactive viewing
212+
with open("plot.html", "w", encoding="utf-8") as f:
213+
cdn_html = (
214+
"""<!DOCTYPE html>
215+
<html>
216+
<head>
217+
<meta charset="utf-8">
218+
<script src="https://code.highcharts.com/highcharts.js"></script>
219+
</head>
220+
<body style="margin:0;">
221+
<div id="container" style="width: 100%; height: 600px;"></div>
222+
<script>"""
223+
+ html_str
224+
+ """</script>
225+
</body>
226+
</html>"""
227+
)
228+
f.write(cdn_html)
229+
230+
# Take screenshot with headless Chrome
231+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
232+
f.write(html_content)
233+
temp_path = f.name
234+
235+
chrome_options = Options()
236+
chrome_options.add_argument("--headless")
237+
chrome_options.add_argument("--no-sandbox")
238+
chrome_options.add_argument("--disable-dev-shm-usage")
239+
chrome_options.add_argument("--disable-gpu")
240+
chrome_options.add_argument("--window-size=5000,3000")
241+
242+
driver = webdriver.Chrome(options=chrome_options)
243+
driver.get(f"file://{temp_path}")
244+
time.sleep(5)
245+
246+
# Screenshot the chart container element for exact dimensions
247+
container = driver.find_element("id", "container")
248+
container.screenshot("plot.png")
249+
driver.quit()
250+
251+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)