RsiChart
The relative strength index of closing prices with overbought and oversold lines, under a price chart or on its own.
Demo
| Time | Close | RSI |
|---|---|---|
| Apr 1, 2026 | $144.01 | — |
| Apr 2, 2026 | $143.27 | — |
| Apr 3, 2026 | $140.83 | — |
| Apr 6, 2026 | $140.60 | — |
| Apr 7, 2026 | $142.39 | — |
| Apr 8, 2026 | $143.11 | — |
| Apr 9, 2026 | $141.11 | — |
| Apr 10, 2026 | $140.45 | — |
| Apr 13, 2026 | $140.21 | — |
| Apr 14, 2026 | $139.06 | — |
| Apr 15, 2026 | $140.62 | — |
| Apr 16, 2026 | $139.05 | — |
| Apr 17, 2026 | $139.54 | — |
| Apr 20, 2026 | $140.53 | — |
| Apr 21, 2026 | $138.85 | 34.1 |
| Apr 22, 2026 | $137.86 | 32.0 |
| Apr 23, 2026 | $138.17 | 33.4 |
| Apr 24, 2026 | $137.32 | 31.5 |
| Apr 27, 2026 | $139.46 | 40.6 |
| Apr 28, 2026 | $141.76 | 48.6 |
| Apr 29, 2026 | $142.25 | 50.1 |
| Apr 30, 2026 | $141.25 | 47.0 |
| May 1, 2026 | $142.49 | 51.0 |
| May 4, 2026 | $143.16 | 53.1 |
| May 5, 2026 | $145.68 | 59.9 |
| May 6, 2026 | $144.29 | 55.2 |
| May 7, 2026 | $146.63 | 60.8 |
| May 8, 2026 | $146.44 | 60.2 |
| May 11, 2026 | $147.23 | 62.0 |
| May 12, 2026 | $145.62 | 56.3 |
| May 13, 2026 | $147.55 | 60.9 |
| May 14, 2026 | $148.31 | 62.6 |
| May 15, 2026 | $149.75 | 65.7 |
| May 18, 2026 | $148.82 | 62.2 |
| May 19, 2026 | $150.13 | 65.0 |
| May 20, 2026 | $149.45 | 62.4 |
| May 21, 2026 | $147.20 | 54.5 |
| May 22, 2026 | $148.12 | 56.9 |
| May 25, 2026 | $149.24 | 59.7 |
| May 26, 2026 | $149.71 | 60.8 |
| May 27, 2026 | $149.50 | 60.0 |
| May 28, 2026 | $149.92 | 61.1 |
| May 29, 2026 | $149.78 | 60.5 |
| Jun 1, 2026 | $148.62 | 55.6 |
| Jun 2, 2026 | $147.43 | 51.0 |
| Jun 3, 2026 | $146.52 | 47.7 |
| Jun 4, 2026 | $147.85 | 52.5 |
| Jun 5, 2026 | $146.80 | 48.7 |
| Jun 8, 2026 | $148.23 | 53.6 |
| Jun 9, 2026 | $150.43 | 60.0 |
| Jun 10, 2026 | $147.58 | 50.4 |
| Jun 11, 2026 | $148.82 | 53.8 |
| Jun 12, 2026 | $146.51 | 47.2 |
| Jun 15, 2026 | $144.85 | 43.1 |
| Jun 16, 2026 | $144.92 | 43.3 |
| Jun 17, 2026 | $144.91 | 43.3 |
| Jun 18, 2026 | $144.22 | 41.4 |
| Jun 19, 2026 | $146.85 | 50.2 |
| Jun 22, 2026 | $146.89 | 50.3 |
| Jun 23, 2026 | $146.65 | 49.6 |
| Jun 24, 2026 | $148.17 | 54.4 |
| Jun 25, 2026 | $148.55 | 55.6 |
| Jun 26, 2026 | $147.03 | 50.1 |
| Jun 29, 2026 | $145.20 | 44.5 |
| Jun 30, 2026 | $143.56 | 40.1 |
| Jul 1, 2026 | $141.03 | 34.5 |
| Jul 2, 2026 | $142.28 | 39.0 |
| Jul 3, 2026 | $140.14 | 34.6 |
| Jul 6, 2026 | $140.74 | 36.8 |
| Jul 7, 2026 | $140.04 | 35.3 |
| Jul 8, 2026 | $138.96 | 33.1 |
| Jul 9, 2026 | $138.59 | 32.4 |
| Jul 10, 2026 | $137.96 | 31.1 |
| Jul 13, 2026 | $139.25 | 36.6 |
| Jul 14, 2026 | $137.64 | 33.0 |
| Jul 15, 2026 | $137.84 | 33.9 |
| Jul 16, 2026 | $136.55 | 31.1 |
| Jul 17, 2026 | $135.72 | 29.5, oversold |
| Jul 20, 2026 | $136.69 | 33.9 |
| Jul 21, 2026 | $138.47 | 41.3 |
| Jul 22, 2026 | $136.73 | 36.9 |
| Jul 23, 2026 | $137.28 | 39.1 |
| Jul 24, 2026 | $134.81 | 33.5 |
| Jul 27, 2026 | $132.83 | 29.9, oversold |
| Jul 28, 2026 | $134.53 | 36.3 |
| Jul 29, 2026 | $133.51 | 34.3 |
| Jul 30, 2026 | $134.27 | 37.1 |
| Jul 31, 2026 | $135.53 | 41.6 |
| Aug 3, 2026 | $135.44 | 41.4 |
| Aug 4, 2026 | $134.26 | 38.4 |
| Aug 5, 2026 | $135.15 | 41.8 |
| Aug 6, 2026 | $133.90 | 38.6 |
| Aug 7, 2026 | $131.87 | 34.0 |
| Aug 10, 2026 | $132.30 | 35.8 |
| Aug 11, 2026 | $133.88 | 41.8 |
| Aug 12, 2026 | $133.47 | 40.7 |
| Aug 13, 2026 | $132.33 | 37.8 |
| Aug 14, 2026 | $130.34 | 33.4 |
| Aug 17, 2026 | $131.45 | 37.8 |
| Aug 18, 2026 | $132.02 | 40.0 |
| Aug 19, 2026 | $133.65 | 45.9 |
| Aug 20, 2026 | $134.34 | 48.2 |
| Aug 21, 2026 | $134.63 | 49.2 |
| Aug 24, 2026 | $133.37 | 45.2 |
| Aug 25, 2026 | $131.61 | 40.2 |
| Aug 26, 2026 | $131.85 | 41.2 |
| Aug 27, 2026 | $134.45 | 50.3 |
| Aug 28, 2026 | $132.27 | 44.1 |
| Aug 31, 2026 | $129.87 | 38.5 |
| Sep 1, 2026 | $130.80 | 41.6 |
| Sep 2, 2026 | $129.74 | 39.2 |
| Sep 3, 2026 | $130.47 | 41.7 |
| Sep 4, 2026 | $129.22 | 38.7 |
| Sep 7, 2026 | $128.03 | 36.1 |
| Sep 8, 2026 | $128.37 | 37.4 |
| Sep 9, 2026 | $129.44 | 41.5 |
| Sep 10, 2026 | $128.66 | 39.5 |
| Sep 11, 2026 | $127.13 | 35.8 |
| Sep 14, 2026 | $125.58 | 32.5 |
| Sep 15, 2026 | $123.52 | 28.7, oversold |
| Time | Close | RSI |
|---|---|---|
| Apr 1, 2026 | 144.01 | — |
| Apr 2, 2026 | 143.27 | — |
| Apr 3, 2026 | 140.83 | — |
| Apr 6, 2026 | 140.60 | — |
| Apr 7, 2026 | 142.39 | — |
| Apr 8, 2026 | 143.11 | 42.4 |
| Apr 9, 2026 | 141.11 | 29.8 |
| Apr 10, 2026 | 140.45 | 26.6 |
| Apr 13, 2026 | 140.21 | 25.3 |
| Apr 14, 2026 | 139.06 | 19.7, oversold |
| Apr 15, 2026 | 140.62 | 41.6 |
| Apr 16, 2026 | 139.05 | 31.0 |
| Apr 17, 2026 | 139.54 | 37.2 |
| Apr 20, 2026 | 140.53 | 48.9 |
| Apr 21, 2026 | 138.85 | 35.1 |
| Apr 22, 2026 | 137.86 | 29.0 |
| Apr 23, 2026 | 138.17 | 33.5 |
| Apr 24, 2026 | 137.32 | 27.5 |
| Apr 27, 2026 | 139.46 | 53.6 |
| Apr 28, 2026 | 141.76 | 68.7 |
| Apr 29, 2026 | 142.25 | 71.2 |
| Apr 30, 2026 | 141.25 | 59.1 |
| May 1, 2026 | 142.49 | 67.6 |
| May 4, 2026 | 143.16 | 71.6 |
| May 5, 2026 | 145.68 | 82.0, overbought |
| May 6, 2026 | 144.29 | 65.5 |
| May 7, 2026 | 146.63 | 75.8 |
| May 8, 2026 | 146.44 | 73.5 |
| May 11, 2026 | 147.23 | 77.0 |
| May 12, 2026 | 145.62 | 57.6 |
| May 13, 2026 | 147.55 | 69.2 |
| May 14, 2026 | 148.31 | 72.9 |
| May 15, 2026 | 149.75 | 78.9 |
| May 18, 2026 | 148.82 | 67.0 |
| May 19, 2026 | 150.13 | 73.9 |
| May 20, 2026 | 149.45 | 65.0 |
| May 21, 2026 | 147.20 | 43.5 |
| May 22, 2026 | 148.12 | 51.7 |
| May 25, 2026 | 149.24 | 60.4 |
| May 26, 2026 | 149.71 | 63.8 |
| May 27, 2026 | 149.50 | 60.9 |
| May 28, 2026 | 149.92 | 64.9 |
| May 29, 2026 | 149.78 | 62.2 |
| Jun 1, 2026 | 148.62 | 43.6 |
| Jun 2, 2026 | 147.43 | 31.5 |
| Jun 3, 2026 | 146.52 | 24.9 |
| Jun 4, 2026 | 147.85 | 45.7 |
| Jun 5, 2026 | 146.80 | 35.9 |
| Jun 8, 2026 | 148.23 | 53.0 |
| Jun 9, 2026 | 150.43 | 69.0 |
| Jun 10, 2026 | 147.58 | 44.5 |
| Jun 11, 2026 | 148.82 | 53.5 |
| Jun 12, 2026 | 146.51 | 38.9 |
| Jun 15, 2026 | 144.85 | 31.2 |
| Jun 16, 2026 | 144.92 | 31.9 |
| Jun 17, 2026 | 144.91 | 31.8 |
| Jun 18, 2026 | 144.22 | 27.5 |
| Jun 19, 2026 | 146.85 | 56.1 |
| Jun 22, 2026 | 146.89 | 56.4 |
| Jun 23, 2026 | 146.65 | 53.4 |
| Jun 24, 2026 | 148.17 | 67.2 |
| Jun 25, 2026 | 148.55 | 69.9 |
| Jun 26, 2026 | 147.03 | 49.2 |
| Jun 29, 2026 | 145.20 | 34.0 |
| Jun 30, 2026 | 143.56 | 25.3 |
| Jul 1, 2026 | 141.03 | 16.9, oversold |
| Jul 2, 2026 | 142.28 | 31.0 |
| Jul 3, 2026 | 140.14 | 22.7 |
| Jul 6, 2026 | 140.74 | 29.3 |
| Jul 7, 2026 | 140.04 | 26.1 |
| Jul 8, 2026 | 138.96 | 21.5 |
| Jul 9, 2026 | 138.59 | 20.0, oversold |
| Jul 10, 2026 | 137.96 | 17.4, oversold |
| Jul 13, 2026 | 139.25 | 38.0 |
| Jul 14, 2026 | 137.64 | 27.4 |
| Jul 15, 2026 | 137.84 | 30.4 |
| Jul 16, 2026 | 136.55 | 22.7 |
| Jul 17, 2026 | 135.72 | 18.9, oversold |
| Jul 20, 2026 | 136.69 | 34.9 |
| Jul 21, 2026 | 138.47 | 55.2 |
| Jul 22, 2026 | 136.73 | 40.0 |
| Jul 23, 2026 | 137.28 | 45.9 |
| Jul 24, 2026 | 134.81 | 29.6 |
| Jul 27, 2026 | 132.83 | 21.8 |
| Jul 28, 2026 | 134.53 | 39.0 |
| Jul 29, 2026 | 133.51 | 33.5 |
| Jul 30, 2026 | 134.27 | 41.2 |
| Jul 31, 2026 | 135.53 | 52.7 |
| Aug 3, 2026 | 135.44 | 51.8 |
| Aug 4, 2026 | 134.26 | 40.4 |
| Aug 5, 2026 | 135.15 | 50.6 |
| Aug 6, 2026 | 133.90 | 38.9 |
| Aug 7, 2026 | 131.87 | 26.5 |
| Aug 10, 2026 | 132.30 | 32.2 |
| Aug 11, 2026 | 133.88 | 50.1 |
| Aug 12, 2026 | 133.47 | 46.1 |
| Aug 13, 2026 | 132.33 | 36.2 |
| Aug 14, 2026 | 130.34 | 24.7 |
| Aug 17, 2026 | 131.45 | 38.4 |
| Aug 18, 2026 | 132.02 | 44.8 |
| Aug 19, 2026 | 133.65 | 59.8 |
| Aug 20, 2026 | 134.34 | 64.9 |
| Aug 21, 2026 | 134.63 | 67.1 |
| Aug 24, 2026 | 133.37 | 50.2 |
| Aug 25, 2026 | 131.61 | 34.8 |
| Aug 26, 2026 | 131.85 | 38.1 |
| Aug 27, 2026 | 134.45 | 62.9 |
| Aug 28, 2026 | 132.27 | 44.3 |
| Aug 31, 2026 | 129.87 | 31.5 |
| Sep 1, 2026 | 130.80 | 39.9 |
| Sep 2, 2026 | 129.74 | 33.9 |
| Sep 3, 2026 | 130.47 | 41.5 |
| Sep 4, 2026 | 129.22 | 33.3 |
| Sep 7, 2026 | 128.03 | 27.0 |
| Sep 8, 2026 | 128.37 | 31.7 |
| Sep 9, 2026 | 129.44 | 45.3 |
| Sep 10, 2026 | 128.66 | 38.3 |
| Sep 11, 2026 | 127.13 | 27.8 |
| Sep 14, 2026 | 125.58 | 20.7 |
| Sep 15, 2026 | 123.52 | 14.5, oversold |
The RSI line turns red when overbought and green when oversold; hover for the close and RSI.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/rsi_chart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/rsi_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/rsi_chart.qwe
// Prices from a seeded random walk (Park–Miller), the same on every render: `count`// candles from the day `from` on, skipping weekends, daily or hourly (10:00 to 16:00),// rounded to `places` decimals.fn rsi_chart_demo_walk(seed: int, count: int, start: num, from: str, hourly: bool = false, places: int = 2) -> list { let s = seed let rnd = () => { s = s * 16807 % 2147483647 s / 2147483647 } let scale = pow(10, places) let cut = (v) => round(v * scale) / scale let p = start let day = from let hour = 10 let out = [] while len(out) < count { if date.weekday(day) <= 5 { let open = cut(p * (1 + (rnd() - 0.5) * 0.006)) let close = cut(open * (1 + (rnd() - 0.485) * (hourly ? 0.012 : 0.035))) let high = cut(max([open, close]) * (1 + rnd() * 0.01)) let low = cut(min([open, close]) * (1 - rnd() * 0.01)) let volume = round((hourly ? 90000 : 900000) * (1 + rnd() * 1.5 + abs(close - open) * 30 / open)) out = out | push({ time: hourly ? `${day}T${hour < 10 ? "0" : ""}${hour}:00:00Z` : day, open, high, low, close, volume }) p = close } if hourly && hour < 16 && date.weekday(day) <= 5 { hour += 1 } else { hour = 10 day = date.add_days(day, 1) } } out}let rsi_chart_demo_daily = rsi_chart_demo_walk(29, 120, 142.8, "2026-04-01")view RsiChartDemo() = <div.demo-stack> <div id="rsi_chart-demo-price"> <RsiChart label="Stark Industries, RSI 14" show_label={true} candles={rsi_chart_demo_daily} prefix="$"/> </div> <div id="rsi_chart-demo-alone"> <RsiChart label="RSI 5, 80 and 20 lines, on its own" show_label={true} candles={rsi_chart_demo_daily} period={5} overbought={80} oversold={20} price={false}/> </div> <p.demo-note>The RSI line turns red when overbought and green when oversold; hover for the close and RSI.</p> </div>Source
ui/rsi_chart.qwe
// RsiChart: the relative strength index of closing prices over a period (14 by// default, with Wilder's smoothing), from 0 to 100, with the overbought (70) and// oversold (30) lines and the band between them; the line turns red above the one// and green below the other. Under a price chart by default, or on its own. CSS only:// hovering shows the close and the RSI at that time, and screen readers get the// numbers as a table.//// <RsiChart label="ACME, RSI 14" candles={bars} prefix="$"/>// <RsiChart label="RSI 9" candles={bars} period={9} overbought={80} oversold={20} price={false}/>// <RsiChart label="Index" closes={[4012.5, 4020.1, 4031.8]} labels={["Mon", "Tue", "Wed"]}/>//// candles: a list of { time, close } (OHLC candles do), oldest first, time a date or a// date-time; or closes: the numbers alone, with labels for the axis if wanted.// period: the RSI's (14); nothing shows until there are period + 1 closes.// overbought and oversold: where its lines are drawn (70 and 30). price: the price// chart above the RSI (true); height: its height in pixels (200); rsi_height: the RSI// pane's (110). prefix and suffix: around every price shown ("$"); decimals: digits// after the point of prices (2). label: what the chart shows; it names it for screen// readers and captions their table. show_label also writes it above the chart.import ui/theme.qwe use ui_clslet rsi_chart_css = css`.ui-rsi { --ui-rsi-line: light-dark(#6250d6, #9085e9); --ui-rsi-price: light-dark(#2a78d6, #3987e5); display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-rsi-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-rsi-frame { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; padding-top: 8px; }.ui-rsi-plot { position: relative; }.ui-rsi-pane, .ui-rsi-yp { position: relative; height: var(--h); }.ui-rsi-ipane, .ui-rsi-yi { position: relative; height: var(--ih); }.ui-rsi-pane + .ui-rsi-ipane, .ui-rsi-yp + .ui-rsi-yi { margin-top: 18px; }.ui-rsi-name { position: absolute; z-index: 1; top: 2px; left: 4px; color: var(--ui-muted); font-size: 11px; font-weight: 600; pointer-events: none; }.ui-rsi-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-rsi-band { position: absolute; left: 0; right: 0; background: color-mix(in srgb, var(--ui-rsi-line) 8%, transparent); }.ui-rsi-level { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--ui-strong); }.ui-rsi-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-rsi-svg path { fill: none; stroke-width: 1.75; stroke-linejoin: round; stroke-linecap: round; }.ui-rsi-price { stroke: var(--ui-rsi-price); }.ui-rsi-fill { fill: var(--ui-rsi-price) !important; stroke: none; opacity: .08; }.ui-rsi-line { stroke: var(--ui-rsi-line); }.ui-rsi-hot path { stroke: var(--ui-danger); }.ui-rsi-cold path { stroke: var(--ui-success); }.ui-rsi-cols { position: absolute; inset: 0; display: flex; }.ui-rsi-col { position: relative; flex: 1; min-width: 0; }.ui-rsi-col::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: var(--ui-strong); opacity: 0; }.ui-rsi-col:hover::before { opacity: .9; }.ui-rsi-dot { position: absolute; left: 50%; display: none; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%; background: var(--c); box-shadow: 0 0 0 2px var(--ui-bg); }.ui-rsi-dp { --c: var(--ui-rsi-price); } .ui-rsi-di { --c: var(--ui-rsi-line); }.ui-rsi-col:hover .ui-rsi-dot { display: block; }.ui-rsi-tip { position: absolute; z-index: 20; top: 0; left: calc(50% + 10px); display: none; min-width: 140px; padding: 7px 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); box-shadow: var(--ui-shadow); color: var(--ui-fg); font-size: 12px; pointer-events: none; }.ui-rsi-flip .ui-rsi-tip { left: auto; right: calc(50% + 10px); }.ui-rsi-col:hover .ui-rsi-tip { display: block; }.ui-rsi-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-rsi-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-rsi-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-rsi-tip .ui-rsi-state { color: var(--ui-muted); font-size: 11px; }.ui-rsi-tip .ui-rsi-over { color: var(--ui-danger); } .ui-rsi-tip .ui-rsi-under { color: var(--ui-success); }.ui-rsi-y span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-rsi-y .ui-rsi-tag { z-index: 1; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--c); color: var(--ui-bg); font-weight: 600; }.ui-rsi-x { position: relative; height: 16px; margin-top: 6px; }.ui-rsi-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-rsi-x .ui-rsi-first { transform: none; }.ui-rsi-x .ui-rsi-end { transform: translateX(-100%); }`// n (a whole number, 0 or more) with at least `width` digits.fn rsi_chart_pad(n: int, width: int) -> str { let s = str(n) len(s) < width ? repeat("0", width - len(s)) + s : s}// A whole number with commas: 1234567 is "1,234,567".fn rsi_chart_group(n: int) -> str = n < 1000 ? str(n) : rsi_chart_group(floor(n / 1000)) + "," + rsi_chart_pad(n % 1000, 3)// A value as it reads, with commas and `places` digits after the point (up to two as// needed when that is null): 48210.5 is "48,210.50" with 2. Worked out in whole// numbers, so the server and the browser write the same text.fn rsi_chart_num(v: num, places: int? = null) -> str { let digits = places == null ? 2 : (places < 0 ? 0 : (places > 6 ? 6 : places)) let scale = [1, 10, 100, 1000, 10000, 100000, 1000000][digits] let units = round(abs(v) * scale) let frac = units % scale let tail = digits == 0 || (places == null && frac == 0) ? "" : (places == null && frac % 10 == 0 ? `.${floor(frac / 10)}` : `.${rsi_chart_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${rsi_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn rsi_chart_short(v: num) -> str { let a = abs(v) let unit = a >= 1000000000 ? 1000000000 : (a >= 1000000 ? 1000000 : (a >= 1000 ? 1000 : 1)) let tenths = round(a * 10 / unit) let mark = unit == 1000000000 ? "B" : (unit == 1000000 ? "M" : "k") unit == 1 ? rsi_chart_num(v) : `${v < 0 ? "-" : ""}${rsi_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn rsi_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${rsi_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn rsi_chart_pct(n: num) -> str { let r = round(n * 100) / 100 r == floor(r) ? str(floor(r)) : str(r)}// The axis from lo to hi, widened to round ticks: about `steps` steps of 1, 2, 2.5 or 5 x 10^n.fn rsi_chart_scale(lo: num, hi: num, steps: int = 5) -> map { let span = hi - lo == 0 ? (lo == 0 ? 1 : abs(lo) / 10) : hi - lo let raw = span / steps let mag = 1 while mag * 10 <= raw { mag = mag * 10 } while mag > raw { mag = mag / 10 } let r = raw / mag let step = (r <= 1 ? 1 : (r <= 2 ? 2 : (r <= 2.5 ? 2.5 : (r <= 5 ? 5 : 10)))) * mag let a = floor(lo / step + 0.000001) let b0 = ceil(hi / step - 0.000001) let b = b0 == a ? a + 1 : b0 { lo: a * step, hi: b * step, ticks: for i in range(b - a + 1) => (a + i) * step }}// A candle's time as it reads: "Mar 2, 2026" for a date, "Mar 2, 14:30" for a date-time.fn rsi_chart_when(t) -> str = type_of(t) != "str" || !date.valid(t) ? str(t) : (len(t) > 10 ? date.format(t, "MMM D, HH:mm") : date.format(t, "MMM D, YYYY"))// Where the time axis is labelled: about `limit` of { i, text }, at the starts of years,// months or days when the times cross them ("2026", "Mar", "Mar 2"), else every few// candles ("14:30", "Mar 2").fn rsi_chart_ticks(times: list, limit: int) -> list { let n = len(times) let thin = (idx: list) => { let step = ceil(len(idx) / limit) range(len(idx)) | filter((k) => k % step == 0) | map((k) => idx[k]) } let spaced = (fmt_at) => { let step = ceil(n / limit) range(n) | filter((i) => i % step == floor(step / 2)) | map((i) => { i, text: fmt_at(i) }) } if n == 0 { return [] } if !all(times, (t) => type_of(t) == "str" && date.valid(t)) { return spaced((i) => str(times[i])) } let days = times | map((t) => slice(t, 0, 10)) let changes = (keys: list) => range(1, n) | filter((i) => keys[i] != keys[i - 1]) let day_starts = changes(days) let month_starts = changes(days | map((d) => slice(d, 0, 7))) let year_starts = changes(days | map((d) => slice(d, 0, 4))) let picked = if any(times, (t) => len(t) > 10) { len(day_starts) >= 3 ? thin(day_starts) | map((i) => { i, text: date.format(days[i], "MMM D") }) : spaced((i) => index_of(day_starts, i) != null ? date.format(days[i], "MMM D") : date.format(times[i], "HH:mm")) } else if len(year_starts) >= 3 && len(month_starts) > limit * 2 { thin(year_starts) | map((i) => { i, text: slice(days[i], 0, 4) }) } else if len(month_starts) >= 2 { thin(month_starts) | map((i) => { i, text: index_of(year_starts, i) != null ? slice(days[i], 0, 4) : date.format(days[i], "MMM") }) } else { spaced((i) => date.format(days[i], "MMM D")) } // Starts that fall close together (a short month) keep the first. let gap = n / (limit * 1.5) reduce(picked, [], (kept, t) => len(kept) == 0 || t.i - kept[-1].i >= gap ? kept + [t] : kept)}// The RSI of each close (Wilder's): null for the first `period`, then 100 - 100 / (1 + the// average gain / the average loss), the averages smoothed as (last * (period - 1) + now) / period.fn rsi_chart_rsi(xs: list, period: int) -> list { let out = [] let gain = 0 let loss = 0 for i in range(len(xs)) { let d = i == 0 ? 0 : xs[i] - xs[i - 1] let up = d > 0 ? d : 0 let down = d < 0 ? 0 - d : 0 if i > 0 && i <= period { gain += up / period loss += down / period } else if i > period { gain = (gain * (period - 1) + up) / period loss = (loss * (period - 1) + down) / period } out = out | push(i < period || period < 1 ? null : (loss == 0 ? (gain == 0 ? 50 : 100) : 100 - 100 / (1 + gain / loss))) } out}view RsiChart(candles: list = [], closes: list? = null, labels: list? = null, period: int = 14, overbought: num = 70, oversold: num = 30, price: bool = true, height: int = 200, rsi_height: int = 110, prefix: str = "", suffix: str = "", decimals: int = 2, label: str = "RSI chart", show_label: bool = false, class: str? = null) { style global rsi_chart_css let vals = closes ?? (candles | map((c) => c.close)) let times = closes != null ? (labels ?? []) : (candles | map((c) => c.time)) let n = len(vals) let rsi = rsi_chart_rsi(vals, period) let sc = rsi_chart_scale(n == 0 ? 0 : min(vals), n == 0 ? 1 : max(vals), 4) let y = (v) => (sc.hi - v) * 100 / (sc.hi - sc.lo) let ri = (v) => 100 - v let x = (i) => (i + 0.5) * 100 / n let pct = (v) => rsi_chart_pct(v) let money = (v) => `${v < 0 ? "-" : ""}${prefix}${rsi_chart_num(abs(v), decimals)}${suffix}` let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${rsi_chart_num(abs(t))}` let path = (list_vals: list, at) => { let idx = range(n) | filter((i) => list_vals[i] != null) len(idx) == 0 ? "" : "M" + join(idx | map((i) => `${pct(x(i))},${pct(at(list_vals[i]))}`), " L") } let rline = path(rsi, ri) let pline = path(vals, y) let last_rsi = n > 0 ? rsi[-1] : null let state = (v) => v == null ? "" : (v >= overbought ? "Overbought" : (v <= oversold ? "Oversold" : "")) let when = (i) => i < len(times) ? rsi_chart_when(times[i]) : `${i + 1}` let axis_width = max([4] + (sc.ticks | map((t) => len(tick(t)))) + [n > 0 ? len(money(vals[-1])) + 1 : 0]) let near = (t) => price && n > 0 && abs(y(t) - y(vals[-1])) * height < 1400 <figure class={ui_cls(["ui-rsi", class])} aria-label={label} style={`--h: ${height}px; --ih: ${rsi_height}px`}> {show_label && <figcaption.ui-rsi-title>{label}</figcaption>} <div.ui-rsi-frame aria-hidden="true"> <div.ui-rsi-plot> {price && <div.ui-rsi-pane> {for t in sc.ticks => <span.ui-rsi-grid style={`top: ${pct(y(t))}%`}></span>} <svg.ui-rsi-svg viewBox="0 0 100 100" preserveAspectRatio="none"> {n > 0 && <path.ui-rsi-fill d={`${pline} L${pct(x(n - 1))},100 L${pct(x(0))},100 Z`}/>} <path.ui-rsi-price d={pline} vector-effect="non-scaling-stroke"/> </svg> </div>} <div.ui-rsi-ipane> <span.ui-rsi-name>RSI {period}</span> <span.ui-rsi-band style={`top: ${pct(ri(overbought))}%; height: ${pct(overbought - oversold)}%`}></span> <span.ui-rsi-grid style="top: 50%"></span> <span.ui-rsi-level style={`top: ${pct(ri(overbought))}%`}></span> <span.ui-rsi-level style={`top: ${pct(ri(oversold))}%`}></span> <svg.ui-rsi-svg viewBox="0 0 100 100" preserveAspectRatio="none"> <path.ui-rsi-line d={rline} vector-effect="non-scaling-stroke"/> </svg> <svg.ui-rsi-svg.ui-rsi-hot viewBox="0 0 100 100" preserveAspectRatio="none" style={`clip-path: inset(0 0 ${pct(100 - ri(overbought))}% 0)`}> <path d={rline} vector-effect="non-scaling-stroke"/> </svg> <svg.ui-rsi-svg.ui-rsi-cold viewBox="0 0 100 100" preserveAspectRatio="none" style={`clip-path: inset(${pct(ri(oversold))}% 0 0 0)`}> <path d={rline} vector-effect="non-scaling-stroke"/> </svg> </div> <div.ui-rsi-cols> {for i in range(n) => <div class={ui_cls(["ui-rsi-col", n > 1 && i >= n / 2 && "ui-rsi-flip"])}> {price && <div.ui-rsi-pane><span.ui-rsi-dot.ui-rsi-dp style={`top: ${pct(y(vals[i]))}%`}></span></div>} <div.ui-rsi-ipane>{rsi[i] != null && <span.ui-rsi-dot.ui-rsi-di style={`top: ${pct(ri(rsi[i]))}%`}></span>}</div> <span.ui-rsi-tip> <strong>{when(i)}</strong> <p>Close<b>{money(vals[i])}</b></p> <p>RSI {period}<b>{rsi[i] == null ? "—" : rsi_chart_num(rsi[i], 1)}</b></p> {state(rsi[i]) != "" && <p class={`ui-rsi-state ${rsi[i] >= overbought ? "ui-rsi-over" : "ui-rsi-under"}`}>{state(rsi[i])}</p>} </span> </div>} </div> </div> <div.ui-rsi-y style={`width: ${axis_width}ch`}> {price && <div.ui-rsi-yp> {for t in sc.ticks => !near(t) && <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>} {n > 0 && <span.ui-rsi-tag.ui-rsi-dp style={`top: ${pct(y(vals[-1]))}%`}>{money(vals[-1])}</span>} </div>} <div.ui-rsi-yi> {for t in [overbought, 50, oversold] => !(last_rsi != null && abs(ri(t) - ri(last_rsi)) * rsi_height < 1300) && <span style={`top: ${pct(ri(t))}%`}>{rsi_chart_num(t)}</span>} {last_rsi != null && <span.ui-rsi-tag.ui-rsi-di style={`top: ${pct(ri(last_rsi))}%`}>{rsi_chart_num(last_rsi, 1)}</span>} </div> </div> <div.ui-rsi-x> {len(times) == n && (for t in rsi_chart_ticks(times, 7) => <span class={ui_cls([x(t.i) < 4 && "ui-rsi-first", x(t.i) > 96 && "ui-rsi-end"])} style={`left: ${pct(x(t.i))}%`}>{t.text}</span>)} </div> <span></span> </div> <table.ui-sr> <caption>{label}: close and RSI {period}</caption> <thead><tr><th scope="col">Time</th><th scope="col">Close</th><th scope="col">RSI</th></tr></thead> <tbody> {for i in range(n) => <tr> <th scope="row">{when(i)}</th><td>{money(vals[i])}</td> <td>{rsi[i] == null ? "—" : rsi_chart_num(rsi[i], 1)}{state(rsi[i]) != "" ? `, ${lower(state(rsi[i]))}` : ""}</td> </tr>} </tbody> </table> </figure>}Related
BollingerChartPrices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.7 views · 5 downloadsCandlestickChartOHLC candles, green up and red down, with a volume pane, a date axis, moving averages and the details of each candle on hover.5 views · 7 downloadsDepthChartAn order book's bids and asks as stepped areas of the size waiting at each price or better, around the mid price, with the spread.5 views · 8 downloadsHeikinAshiChartHeikin-Ashi candles worked out from OHLC, which smooth the noise so trends show as runs of one colour, with the real close on hover.5 views · 6 downloadsLineBreakChartA three-line break chart from closing prices: lines go on while the close makes new ends, and turn only past the last three lines.5 views · 6 downloadsLiveCandlesCandles built from ticks as they come: the open candle grows, a new one starts each interval, with the last price and the time left.6 views · 6 downloads
Thread
Thread 0 posts
On RsiChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.