Skip to content

Commit 2a6f8df

Browse files
feat(altair): implement indicator-rsi (#6941)
## Implementation: `indicator-rsi` - python/altair Implements the **python/altair** version of `indicator-rsi`. **File:** `plots/indicator-rsi/implementations/python/altair.py` **Parent Issue:** #3229 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25967325906)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent aacf482 commit 2a6f8df

2 files changed

Lines changed: 189 additions & 149 deletions

File tree

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
indicator-rsi: RSI Technical Indicator Chart
3-
Library: altair 6.0.0 | Python 3.13.11
4-
Quality: 91/100 | Created: 2026-01-07
3+
Library: altair 6.1.0 | Python 3.13.13
4+
Quality: 88/100 | Updated: 2026-05-16
55
"""
66

7-
import altair as alt
7+
import os
8+
import sys
9+
from importlib.machinery import SourceFileLoader
10+
811
import numpy as np
912
import pandas as pd
1013

1114

12-
# Data - Generate realistic RSI data from simulated price movements
15+
venv_path = sys.executable
16+
site_packages = os.path.join(os.path.dirname(venv_path), "..", "lib", "python3.13", "site-packages")
17+
altair_init = os.path.join(site_packages, "altair", "__init__.py")
18+
19+
loader = SourceFileLoader("altair", altair_init)
20+
alt = loader.load_module()
21+
22+
23+
THEME = os.getenv("ANYPLOT_THEME", "light")
24+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
25+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
26+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
27+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
28+
1329
np.random.seed(42)
14-
n_periods = 120
30+
n_periods = 140
1531
dates = pd.date_range(start="2024-01-01", periods=n_periods, freq="D")
1632

17-
# Simulate price changes to calculate RSI
18-
price_changes = np.random.randn(n_periods) * 2
33+
price_changes = np.random.randn(n_periods) * 4
1934

20-
# Calculate RSI using standard 14-period lookback
2135
lookback = 14
2236
gains = np.zeros(n_periods)
2337
losses = np.zeros(n_periods)
@@ -29,7 +43,6 @@
2943
else:
3044
losses[i] = abs(change)
3145

32-
# Smoothed averages using exponential moving average style
3346
avg_gain = np.zeros(n_periods)
3447
avg_loss = np.zeros(n_periods)
3548
avg_gain[lookback] = np.mean(gains[1 : lookback + 1])
@@ -39,52 +52,55 @@
3952
avg_gain[i] = (avg_gain[i - 1] * (lookback - 1) + gains[i]) / lookback
4053
avg_loss[i] = (avg_loss[i - 1] * (lookback - 1) + losses[i]) / lookback
4154

42-
# Calculate RSI
4355
with np.errstate(divide="ignore", invalid="ignore"):
4456
rs = np.where(avg_loss != 0, avg_gain / avg_loss, 0)
4557
rsi = np.where(avg_loss != 0, 100 - (100 / (1 + rs)), 100)
46-
rsi[:lookback] = 50 # Fill initial values with neutral
58+
rsi[:lookback] = 50
4759

4860
df = pd.DataFrame({"date": dates, "rsi": rsi})
4961

50-
# Overbought zone (70-100)
5162
overbought_df = pd.DataFrame({"y": [70], "y2": [100]})
52-
53-
# Oversold zone (0-30)
5463
oversold_df = pd.DataFrame({"y": [0], "y2": [30]})
5564

56-
# Overbought zone shading (red/orange tint)
5765
overbought_zone = (
58-
alt.Chart(overbought_df).mark_rect(opacity=0.15, color="#E74C3C").encode(y=alt.Y("y:Q"), y2=alt.Y2("y2:Q"))
66+
alt.Chart(overbought_df)
67+
.mark_rect(opacity=0.12, color="#E74C3C")
68+
.encode(y=alt.Y("y:Q", scale=alt.Scale(domain=[0, 100])), y2=alt.Y2("y2:Q"))
5969
)
6070

61-
# Oversold zone shading (green tint)
6271
oversold_zone = (
63-
alt.Chart(oversold_df).mark_rect(opacity=0.15, color="#27AE60").encode(y=alt.Y("y:Q"), y2=alt.Y2("y2:Q"))
72+
alt.Chart(oversold_df)
73+
.mark_rect(opacity=0.12, color="#27AE60")
74+
.encode(y=alt.Y("y:Q", scale=alt.Scale(domain=[0, 100])), y2=alt.Y2("y2:Q"))
6475
)
6576

66-
# Horizontal threshold lines
6777
threshold_df = pd.DataFrame({"y": [30, 50, 70], "label": ["Oversold (30)", "Neutral (50)", "Overbought (70)"]})
6878

6979
threshold_lines = (
7080
alt.Chart(threshold_df)
7181
.mark_rule(strokeDash=[8, 4], strokeWidth=2)
7282
.encode(
73-
y=alt.Y("y:Q"),
83+
y=alt.Y("y:Q", scale=alt.Scale(domain=[0, 100])),
7484
color=alt.Color(
7585
"label:N",
7686
scale=alt.Scale(
77-
domain=["Oversold (30)", "Neutral (50)", "Overbought (70)"], range=["#27AE60", "#95A5A6", "#E74C3C"]
87+
domain=["Oversold (30)", "Neutral (50)", "Overbought (70)"], range=["#27AE60", INK_SOFT, "#E74C3C"]
88+
),
89+
legend=alt.Legend(
90+
title="Thresholds",
91+
orient="bottom-left",
92+
titleFontSize=16,
93+
labelFontSize=14,
94+
fillColor=ELEVATED_BG,
95+
strokeColor=INK_SOFT,
7896
),
79-
legend=alt.Legend(title="Threshold Lines", orient="top-right", titleFontSize=16, labelFontSize=14),
8097
),
8198
)
8299
)
83100

84-
# RSI line chart
85101
rsi_line = (
86102
alt.Chart(df)
87-
.mark_line(strokeWidth=3, color="#306998")
103+
.mark_line(strokeWidth=3, color="#0072B2")
88104
.encode(
89105
x=alt.X("date:T", title="Date", axis=alt.Axis(format="%b %Y")),
90106
y=alt.Y("rsi:Q", title="RSI Value", scale=alt.Scale(domain=[0, 100])),
@@ -95,24 +111,34 @@
95111
)
96112
)
97113

98-
# Combine all layers
99114
chart = (
100115
alt.layer(overbought_zone, oversold_zone, threshold_lines, rsi_line)
101116
.properties(
102117
width=1600,
103118
height=900,
104119
title=alt.Title(
105-
"indicator-rsi · altair · pyplots.ai",
120+
"indicator-rsi · altair · anyplot.ai",
106121
fontSize=28,
107122
anchor="middle",
108123
subtitle="14-Period RSI with Overbought/Oversold Zones",
109124
subtitleFontSize=18,
110125
),
126+
background=PAGE_BG,
127+
)
128+
.configure_axis(
129+
labelFontSize=18,
130+
titleFontSize=22,
131+
gridOpacity=0.10,
132+
domainColor=INK_SOFT,
133+
tickColor=INK_SOFT,
134+
gridColor=INK,
135+
labelColor=INK_SOFT,
136+
titleColor=INK,
111137
)
112-
.configure_axis(labelFontSize=18, titleFontSize=22, gridOpacity=0.3)
113-
.configure_view(strokeWidth=0)
138+
.configure_title(color=INK)
139+
.configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
140+
.configure_view(strokeWidth=0, fill=PAGE_BG)
114141
)
115142

116-
# Save outputs
117-
chart.save("plot.png", scale_factor=3.0)
118-
chart.save("plot.html")
143+
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
144+
chart.save(f"plot-{THEME}.html")

0 commit comments

Comments
 (0)