|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
7 | | -import altair as alt |
| 7 | +import os |
| 8 | +import sys |
| 9 | +from importlib.machinery import SourceFileLoader |
| 10 | + |
8 | 11 | import numpy as np |
9 | 12 | import pandas as pd |
10 | 13 |
|
11 | 14 |
|
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 | + |
13 | 29 | np.random.seed(42) |
14 | | -n_periods = 120 |
| 30 | +n_periods = 140 |
15 | 31 | dates = pd.date_range(start="2024-01-01", periods=n_periods, freq="D") |
16 | 32 |
|
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 |
19 | 34 |
|
20 | | -# Calculate RSI using standard 14-period lookback |
21 | 35 | lookback = 14 |
22 | 36 | gains = np.zeros(n_periods) |
23 | 37 | losses = np.zeros(n_periods) |
|
29 | 43 | else: |
30 | 44 | losses[i] = abs(change) |
31 | 45 |
|
32 | | -# Smoothed averages using exponential moving average style |
33 | 46 | avg_gain = np.zeros(n_periods) |
34 | 47 | avg_loss = np.zeros(n_periods) |
35 | 48 | avg_gain[lookback] = np.mean(gains[1 : lookback + 1]) |
|
39 | 52 | avg_gain[i] = (avg_gain[i - 1] * (lookback - 1) + gains[i]) / lookback |
40 | 53 | avg_loss[i] = (avg_loss[i - 1] * (lookback - 1) + losses[i]) / lookback |
41 | 54 |
|
42 | | -# Calculate RSI |
43 | 55 | with np.errstate(divide="ignore", invalid="ignore"): |
44 | 56 | rs = np.where(avg_loss != 0, avg_gain / avg_loss, 0) |
45 | 57 | rsi = np.where(avg_loss != 0, 100 - (100 / (1 + rs)), 100) |
46 | | -rsi[:lookback] = 50 # Fill initial values with neutral |
| 58 | +rsi[:lookback] = 50 |
47 | 59 |
|
48 | 60 | df = pd.DataFrame({"date": dates, "rsi": rsi}) |
49 | 61 |
|
50 | | -# Overbought zone (70-100) |
51 | 62 | overbought_df = pd.DataFrame({"y": [70], "y2": [100]}) |
52 | | - |
53 | | -# Oversold zone (0-30) |
54 | 63 | oversold_df = pd.DataFrame({"y": [0], "y2": [30]}) |
55 | 64 |
|
56 | | -# Overbought zone shading (red/orange tint) |
57 | 65 | 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")) |
59 | 69 | ) |
60 | 70 |
|
61 | | -# Oversold zone shading (green tint) |
62 | 71 | 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")) |
64 | 75 | ) |
65 | 76 |
|
66 | | -# Horizontal threshold lines |
67 | 77 | threshold_df = pd.DataFrame({"y": [30, 50, 70], "label": ["Oversold (30)", "Neutral (50)", "Overbought (70)"]}) |
68 | 78 |
|
69 | 79 | threshold_lines = ( |
70 | 80 | alt.Chart(threshold_df) |
71 | 81 | .mark_rule(strokeDash=[8, 4], strokeWidth=2) |
72 | 82 | .encode( |
73 | | - y=alt.Y("y:Q"), |
| 83 | + y=alt.Y("y:Q", scale=alt.Scale(domain=[0, 100])), |
74 | 84 | color=alt.Color( |
75 | 85 | "label:N", |
76 | 86 | 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, |
78 | 96 | ), |
79 | | - legend=alt.Legend(title="Threshold Lines", orient="top-right", titleFontSize=16, labelFontSize=14), |
80 | 97 | ), |
81 | 98 | ) |
82 | 99 | ) |
83 | 100 |
|
84 | | -# RSI line chart |
85 | 101 | rsi_line = ( |
86 | 102 | alt.Chart(df) |
87 | | - .mark_line(strokeWidth=3, color="#306998") |
| 103 | + .mark_line(strokeWidth=3, color="#0072B2") |
88 | 104 | .encode( |
89 | 105 | x=alt.X("date:T", title="Date", axis=alt.Axis(format="%b %Y")), |
90 | 106 | y=alt.Y("rsi:Q", title="RSI Value", scale=alt.Scale(domain=[0, 100])), |
|
95 | 111 | ) |
96 | 112 | ) |
97 | 113 |
|
98 | | -# Combine all layers |
99 | 114 | chart = ( |
100 | 115 | alt.layer(overbought_zone, oversold_zone, threshold_lines, rsi_line) |
101 | 116 | .properties( |
102 | 117 | width=1600, |
103 | 118 | height=900, |
104 | 119 | title=alt.Title( |
105 | | - "indicator-rsi · altair · pyplots.ai", |
| 120 | + "indicator-rsi · altair · anyplot.ai", |
106 | 121 | fontSize=28, |
107 | 122 | anchor="middle", |
108 | 123 | subtitle="14-Period RSI with Overbought/Oversold Zones", |
109 | 124 | subtitleFontSize=18, |
110 | 125 | ), |
| 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, |
111 | 137 | ) |
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) |
114 | 141 | ) |
115 | 142 |
|
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