BollingerChart
Prices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.
Demo
| Time | Close | Upper | Middle | Lower |
|---|---|---|---|---|
| Apr 6, 2026 | $94.36 | — | — | — |
| Apr 7, 2026 | $95.55 | — | — | — |
| Apr 8, 2026 | $96.16 | — | — | — |
| Apr 9, 2026 | $97.58 | — | — | — |
| Apr 10, 2026 | $98.95 | — | — | — |
| Apr 13, 2026 | $99.17 | — | — | — |
| Apr 14, 2026 | $99.22 | — | — | — |
| Apr 15, 2026 | $99.31 | — | — | — |
| Apr 16, 2026 | $100.61 | — | — | — |
| Apr 17, 2026 | $100.73 | — | — | — |
| Apr 20, 2026 | $99.67 | — | — | — |
| Apr 21, 2026 | $100.13 | — | — | — |
| Apr 22, 2026 | $100.96 | — | — | — |
| Apr 23, 2026 | $99.86 | — | — | — |
| Apr 24, 2026 | $99.71 | — | — | — |
| Apr 27, 2026 | $99.31 | — | — | — |
| Apr 28, 2026 | $99.87 | — | — | — |
| Apr 29, 2026 | $101.29 | — | — | — |
| Apr 30, 2026 | $101.83 | — | — | — |
| May 1, 2026 | $100.65 | $103.03 | $99.25 | $95.46 |
| May 4, 2026 | $101.02 | $102.70 | $99.58 | $96.46 |
| May 5, 2026 | $102.88 | $102.80 | $99.95 | $97.09 |
| May 6, 2026 | $103.32 | $102.96 | $100.30 | $97.65 |
| May 7, 2026 | $104.18 | $103.49 | $100.63 | $97.78 |
| May 8, 2026 | $105.18 | $104.31 | $100.95 | $97.58 |
| May 11, 2026 | $104.05 | $104.71 | $101.19 | $97.67 |
| May 12, 2026 | $104.86 | $105.21 | $101.47 | $97.73 |
| May 13, 2026 | $104.96 | $105.65 | $101.75 | $97.86 |
| May 14, 2026 | $104.51 | $105.98 | $101.95 | $97.92 |
| May 15, 2026 | $105.39 | $106.44 | $102.18 | $97.93 |
| May 18, 2026 | $105.79 | $106.86 | $102.49 | $98.12 |
| May 19, 2026 | $106.62 | $107.39 | $102.81 | $98.23 |
| May 20, 2026 | $104.81 | $107.58 | $103.00 | $98.43 |
| May 21, 2026 | $102.79 | $107.50 | $103.15 | $98.81 |
| May 22, 2026 | $100.83 | $107.40 | $103.21 | $99.01 |
| May 25, 2026 | $100.53 | $107.26 | $103.27 | $99.27 |
| May 26, 2026 | $100.79 | $107.17 | $103.31 | $99.46 |
| May 27, 2026 | $100.46 | $107.23 | $103.27 | $99.31 |
| May 28, 2026 | $101.94 | $107.23 | $103.28 | $99.33 |
| May 29, 2026 | $100.14 | $107.28 | $103.25 | $99.23 |
| Jun 1, 2026 | $101.08 | $107.27 | $103.26 | $99.24 |
| Jun 2, 2026 | $102.23 | $107.26 | $103.22 | $99.18 |
| Jun 3, 2026 | $101.98 | $107.23 | $103.16 | $99.08 |
| Jun 4, 2026 | $103.35 | $107.16 | $103.11 | $99.07 |
| Jun 5, 2026 | $101.41 | $106.92 | $102.93 | $98.93 |
| Jun 8, 2026 | $102.48 | $106.82 | $102.85 | $98.88 |
| Jun 9, 2026 | $101.98 | $106.58 | $102.70 | $98.83 |
| Jun 10, 2026 | $100.69 | $106.31 | $102.49 | $98.67 |
| Jun 11, 2026 | $102.04 | $106.08 | $102.37 | $98.65 |
| Jun 12, 2026 | $103.94 | $105.82 | $102.29 | $98.77 |
| Jun 15, 2026 | $102.09 | $105.25 | $102.11 | $98.97 |
| Jun 16, 2026 | $102.61 | $104.29 | $101.91 | $99.53 |
| Jun 17, 2026 | $102.69 | $103.82 | $101.80 | $99.79 |
| Jun 18, 2026 | $101.05 | $103.70 | $101.72 | $99.73 |
| Jun 19, 2026 | $101.91 | $103.72 | $101.77 | $99.82 |
| Jun 22, 2026 | $101.46 | $103.68 | $101.82 | $99.95 |
| Jun 23, 2026 | $102.39 | $103.72 | $101.90 | $100.07 |
| Jun 24, 2026 | $101.50 | $103.66 | $101.95 | $100.24 |
| Jun 25, 2026 | $102.01 | $103.66 | $101.95 | $100.24 |
| Jun 26, 2026 | $101.47 | $103.54 | $102.02 | $100.50 |
| Jun 29, 2026 | $103.03 | $103.63 | $102.12 | $100.60 |
| Jun 30, 2026 | $104.33 | $104.02 | $102.22 | $100.42 |
| Jul 1, 2026 | $102.91 | $104.08 | $102.27 | $100.45 |
| Jul 2, 2026 | $104.02 | $104.22 | $102.30 | $100.38 |
| Jul 3, 2026 | $105.85 | $104.94 | $102.52 | $100.11 |
| Jul 6, 2026 | $107.43 | $106.00 | $102.77 | $99.54 |
| Jul 7, 2026 | $107.99 | $106.99 | $103.07 | $99.15 |
| Jul 8, 2026 | $106.39 | $107.37 | $103.36 | $99.34 |
| Jul 9, 2026 | $105.82 | $107.65 | $103.54 | $99.44 |
| Jul 10, 2026 | $104.48 | $107.69 | $103.57 | $99.45 |
| Jul 13, 2026 | $103.99 | $107.73 | $103.67 | $99.60 |
| Jul 14, 2026 | $105.21 | $107.89 | $103.80 | $99.71 |
| Jul 15, 2026 | $106.83 | $108.26 | $104.00 | $99.74 |
| Jul 16, 2026 | $106.31 | $108.41 | $104.27 | $100.12 |
| Jul 17, 2026 | $107.38 | $108.75 | $104.54 | $100.33 |
| Jul 20, 2026 | $105.99 | $108.77 | $104.77 | $100.76 |
| Jul 21, 2026 | $105.80 | $108.81 | $104.94 | $101.06 |
| Jul 22, 2026 | $104.73 | $108.64 | $105.10 | $101.55 |
| Jul 23, 2026 | $105.12 | $108.50 | $105.25 | $102.01 |
| Jul 24, 2026 | $103.70 | $108.22 | $105.37 | $102.52 |
| Jul 27, 2026 | $102.83 | $108.24 | $105.36 | $102.47 |
| Jul 28, 2026 | $103.61 | $108.27 | $105.32 | $102.37 |
| Jul 29, 2026 | $103.09 | $108.25 | $105.33 | $102.41 |
| Jul 30, 2026 | $102.37 | $108.40 | $105.25 | $102.10 |
| Jul 31, 2026 | $100.91 | $108.66 | $105.00 | $101.34 |
| Aug 3, 2026 | $100.56 | $108.61 | $104.66 | $100.70 |
| Aug 4, 2026 | $99.59 | $108.46 | $104.24 | $100.01 |
| Aug 5, 2026 | $98.46 | $108.63 | $103.84 | $99.05 |
| Aug 6, 2026 | $96.85 | $108.97 | $103.39 | $97.81 |
| Aug 7, 2026 | $96.46 | $109.30 | $102.99 | $96.67 |
| Aug 10, 2026 | $96.87 | $109.46 | $102.63 | $95.80 |
| Aug 11, 2026 | $96.46 | $109.42 | $102.20 | $94.97 |
| Aug 12, 2026 | $97.49 | $108.90 | $101.73 | $94.56 |
| Aug 13, 2026 | $96.15 | $108.46 | $101.22 | $93.98 |
| Aug 14, 2026 | $96.00 | $107.65 | $100.65 | $93.65 |
| Aug 17, 2026 | $94.39 | $107.13 | $100.07 | $93.01 |
| Aug 18, 2026 | $94.04 | $106.49 | $99.48 | $92.47 |
| Aug 19, 2026 | $95.91 | $105.78 | $99.04 | $92.30 |
| Aug 20, 2026 | $96.65 | $104.82 | $98.62 | $92.42 |
| Aug 21, 2026 | $95.07 | $104.11 | $98.19 | $92.27 |
| Aug 24, 2026 | $95.71 | $103.44 | $97.83 | $92.22 |
| Aug 25, 2026 | $95.17 | $102.46 | $97.41 | $92.36 |
| Aug 26, 2026 | $96.62 | $101.42 | $97.09 | $92.75 |
| Aug 27, 2026 | $95.44 | $100.38 | $96.74 | $93.10 |
| Aug 28, 2026 | $96.31 | $99.61 | $96.51 | $93.41 |
| Aug 31, 2026 | $96.53 | $98.79 | $96.31 | $93.83 |
| Sep 1, 2026 | $96.44 | $98.13 | $96.15 | $94.18 |
| Sep 2, 2026 | $95.81 | $97.69 | $96.02 | $94.35 |
| Sep 3, 2026 | $95.63 | $97.59 | $95.96 | $94.32 |
| Sep 4, 2026 | $96.03 | $97.55 | $95.94 | $94.32 |
| Time | Close | Upper | Middle | Lower |
|---|---|---|---|---|
| Jun 29, 2026 | $103.03 | — | — | — |
| Jun 30, 2026 | $104.33 | — | — | — |
| Jul 1, 2026 | $102.91 | — | — | — |
| Jul 2, 2026 | $104.02 | — | — | — |
| Jul 3, 2026 | $105.85 | — | — | — |
| Jul 6, 2026 | $107.43 | — | — | — |
| Jul 7, 2026 | $107.99 | — | — | — |
| Jul 8, 2026 | $106.39 | — | — | — |
| Jul 9, 2026 | $105.82 | — | — | — |
| Jul 10, 2026 | $104.48 | $107.72 | $105.23 | $102.73 |
| Jul 13, 2026 | $103.99 | $107.65 | $105.32 | $102.99 |
| Jul 14, 2026 | $105.21 | $107.69 | $105.41 | $103.13 |
| Jul 15, 2026 | $106.83 | $107.78 | $105.80 | $103.82 |
| Jul 16, 2026 | $106.31 | $107.80 | $106.03 | $104.26 |
| Jul 17, 2026 | $107.38 | $108.05 | $106.18 | $104.31 |
| Jul 20, 2026 | $105.99 | $107.80 | $106.04 | $104.28 |
| Jul 21, 2026 | $105.80 | $107.29 | $105.82 | $104.35 |
| Jul 22, 2026 | $104.73 | $107.17 | $105.65 | $104.14 |
| Jul 23, 2026 | $105.12 | $107.11 | $105.58 | $104.06 |
| Jul 24, 2026 | $103.70 | $107.19 | $105.51 | $103.82 |
| Jul 27, 2026 | $102.83 | $107.37 | $105.39 | $103.41 |
| Jul 28, 2026 | $103.61 | $107.36 | $105.23 | $103.10 |
| Jul 29, 2026 | $103.09 | $107.02 | $104.86 | $102.69 |
| Jul 30, 2026 | $102.37 | $106.76 | $104.46 | $102.17 |
| Jul 31, 2026 | $100.91 | $106.10 | $103.82 | $101.53 |
| Aug 3, 2026 | $100.56 | $105.70 | $103.27 | $100.84 |
| Aug 4, 2026 | $99.59 | $105.23 | $102.65 | $100.07 |
| Aug 5, 2026 | $98.46 | $104.98 | $102.02 | $99.07 |
| Aug 6, 2026 | $96.85 | $104.52 | $101.20 | $97.87 |
| Aug 7, 2026 | $96.46 | $104.15 | $100.47 | $96.80 |
| Aug 10, 2026 | $96.87 | $103.67 | $99.88 | $96.08 |
| Aug 11, 2026 | $96.46 | $102.73 | $99.16 | $95.59 |
| Aug 12, 2026 | $97.49 | $101.63 | $98.60 | $95.57 |
| Aug 13, 2026 | $96.15 | $100.52 | $97.98 | $95.44 |
| Aug 14, 2026 | $96.00 | $99.70 | $97.49 | $95.28 |
| Aug 17, 2026 | $94.39 | $98.89 | $96.87 | $94.86 |
| Aug 18, 2026 | $94.04 | $98.19 | $96.32 | $94.44 |
| Aug 19, 2026 | $95.91 | $97.60 | $96.06 | $94.52 |
| Aug 20, 2026 | $96.65 | $97.56 | $96.04 | $94.52 |
| Aug 21, 2026 | $95.07 | $97.47 | $95.90 | $94.34 |
| Aug 24, 2026 | $95.71 | $97.27 | $95.79 | $94.30 |
| Aug 25, 2026 | $95.17 | $97.13 | $95.66 | $94.19 |
| Aug 26, 2026 | $96.62 | $96.83 | $95.57 | $94.31 |
| Aug 27, 2026 | $95.44 | $96.73 | $95.50 | $94.27 |
| Aug 28, 2026 | $96.31 | $96.79 | $95.53 | $94.27 |
| Aug 31, 2026 | $96.53 | $96.94 | $95.74 | $94.55 |
| Sep 1, 2026 | $96.44 | $96.85 | $95.98 | $95.12 |
| Sep 2, 2026 | $95.81 | $96.84 | $95.98 | $95.11 |
| Sep 3, 2026 | $95.63 | $96.68 | $95.87 | $95.06 |
| Sep 4, 2026 | $96.03 | $96.67 | $95.97 | $95.26 |
Rings mark closes outside the bands; hover for the bands, %B and their width.
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/bollinger_chart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/bollinger_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/bollinger_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 bollinger_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 bollinger_chart_demo_daily = bollinger_chart_demo_walk(61, 110, 96.2, "2026-04-06")view BollingerChartDemo() = <div.demo-stack> <div id="bollinger_chart-demo-line"> <BollingerChart label="Soylent, Bollinger 20, 2" show_label={true} candles={bollinger_chart_demo_daily} prefix="$"/> </div> <div id="bollinger_chart-demo-bars"> <BollingerChart label="Soylent, last 50 days as candles, Bollinger 10, 1.5" show_label={true} candles={drop(bollinger_chart_demo_daily, 60)} period={10} deviations={1.5} bars={true} prefix="$" height={220}/> </div> <p.demo-note>Rings mark closes outside the bands; hover for the bands, %B and their width.</p> </div>Source
ui/bollinger_chart.qwe
// BollingerChart: prices with Bollinger bands: the moving average of the close over a// period (20) and bands a number of standard deviations (2) above and below it, the// space between them shaded; closes outside the bands are marked. The close is a line,// or candles. CSS only: hovering shows the close, the bands, %B (where the close sits// between the bands, 0 to 1) and the bands' width, and screen readers get the numbers// as a table.//// <BollingerChart label="ACME, Bollinger 20, 2" candles={bars} prefix="$"/>// <BollingerChart label="ACME" candles={bars} period={10} deviations={1.5} bars={true}/>//// candles: a list of { time, close } (with open, high and low for bars), oldest first,// time a date or a date-time; or closes: the numbers alone, with labels for the axis.// period: the moving average's (20); deviations: how many standard deviations (of the// same closes) the bands are from it (2); the bands start at the period-th close.// bars: draws each candle instead of a line of closes (false). height: the plot's// height in pixels (300). 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 bollinger_chart_css = css`.ui-bb { --ui-bb-price: light-dark(#2a78d6, #3987e5); --ui-bb-band: light-dark(#6250d6, #9085e9); display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-bb-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-bb-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-bb-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-bb-key { width: 14px; height: 3px; border-radius: 2px; background: var(--c); }.ui-bb-kb { height: 10px; width: 14px; border: 1px solid var(--ui-bb-band); background: color-mix(in srgb, var(--ui-bb-band) 14%, transparent); }.ui-bb-cp { --c: var(--ui-bb-price); } .ui-bb-cb { --c: var(--ui-bb-band); }.ui-bb-up { --k: var(--ui-success); } .ui-bb-down { --k: var(--ui-danger); }.ui-bb-frame { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; padding-top: 8px; }.ui-bb-plot, .ui-bb-y { position: relative; height: var(--h); }.ui-bb-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-bb-svg { position: absolute; z-index: 1; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-bb-svg path { fill: none; stroke-linejoin: round; stroke-linecap: round; }.ui-bb-fill { fill: var(--ui-bb-band) !important; opacity: .1; }.ui-bb-edge { stroke: var(--ui-bb-band); stroke-width: 1.25; }.ui-bb-mid { stroke: var(--ui-bb-band); stroke-width: 1.25; stroke-dasharray: 4 3; }.ui-bb-close { stroke: var(--ui-bb-price); stroke-width: 1.75; }.ui-bb-cols { position: absolute; inset: 0; display: flex; }.ui-bb-col { position: relative; flex: 1; min-width: 0; }.ui-bb-col::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: var(--ui-strong); opacity: 0; }.ui-bb-col:hover::before { opacity: .9; }.ui-bb-wick { position: absolute; z-index: 2; left: 50%; width: 1px; margin-left: -.5px; background: var(--k); }.ui-bb-body { position: absolute; z-index: 2; left: 18%; right: 18%; min-height: 1px; border-radius: 1px; background: var(--k); }.ui-bb-out { position: absolute; z-index: 3; left: 50%; width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; border: 2px solid var(--k); border-radius: 50%; background: var(--ui-bg); }.ui-bb-dot { position: absolute; z-index: 3; left: 50%; display: none; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%; background: var(--ui-bb-price); box-shadow: 0 0 0 2px var(--ui-bg); }.ui-bb-col:hover .ui-bb-dot { display: block; }.ui-bb-tip { position: absolute; z-index: 20; top: 0; left: calc(50% + 10px); display: none; min-width: 150px; 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-bb-flip .ui-bb-tip { left: auto; right: calc(50% + 10px); }.ui-bb-col:hover .ui-bb-tip { display: block; }.ui-bb-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-bb-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-bb-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-bb-tip em { color: var(--k); font-style: normal; font-weight: 600; }.ui-bb-y span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-bb-y .ui-bb-tag { z-index: 1; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--ui-bb-price); color: var(--ui-bg); font-weight: 600; }.ui-bb-x { position: relative; height: 16px; margin-top: 6px; }.ui-bb-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-bb-x .ui-bb-first { transform: none; }.ui-bb-x .ui-bb-end { transform: translateX(-100%); }`// n (a whole number, 0 or more) with at least `width` digits.fn bollinger_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 bollinger_chart_group(n: int) -> str = n < 1000 ? str(n) : bollinger_chart_group(floor(n / 1000)) + "," + bollinger_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 bollinger_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)}` : `.${bollinger_chart_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${bollinger_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn bollinger_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 ? bollinger_chart_num(v) : `${v < 0 ? "-" : ""}${bollinger_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn bollinger_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${bollinger_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn bollinger_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 bollinger_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 bollinger_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 bollinger_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 simple moving average of `period` values, null until there are that many.fn bollinger_chart_sma(xs: list, period: int) -> list { let out = [] let total = 0 for i in range(len(xs)) { total += xs[i] if i >= period { total -= xs[i - period] } out = out | push(i + 1 >= period ? total / period : null) } out}// The exponential moving average of `period` values (k = 2 / (period + 1)), started// from the simple average of the first `period`; null before that.fn bollinger_chart_ema(xs: list, period: int) -> list { let k = 2 / (period + 1) let out = [] let prev = null for i in range(len(xs)) { if i + 1 == period { prev = sum(take(xs, period)) / period } else if i + 1 > period { prev = xs[i] * k + prev * (1 - k) } out = out | push(prev) } out}// The bands: { mid, upper, lower }, lists null for the first period - 1 closes; the// deviation is the population standard deviation of the same `period` closes.fn bollinger_chart_bands(xs: list, period: int, k: num) -> map { let mid = bollinger_chart_sma(xs, period) let dev = for i in range(len(xs)) => mid[i] == null ? null : sqrt(sum(slice(xs, i + 1 - period, i + 1) | map((v) => (v - mid[i]) * (v - mid[i]))) / period) { mid, upper: for i in range(len(xs)) => mid[i] == null ? null : mid[i] + k * dev[i], lower: for i in range(len(xs)) => mid[i] == null ? null : mid[i] - k * dev[i], }}view BollingerChart(candles: list = [], closes: list? = null, labels: list? = null, period: int = 20, deviations: num = 2, bars: bool = false, height: int = 300, prefix: str = "", suffix: str = "", decimals: int = 2, label: str = "Bollinger bands", show_label: bool = false, class: str? = null) { style global bollinger_chart_css let vals = closes ?? (candles | map((c) => c.close)) let times = closes != null ? (labels ?? []) : (candles | map((c) => c.time)) let candle_bars = bars && closes == null let n = len(vals) let bb = bollinger_chart_bands(vals, period < 1 ? 1 : period, deviations) let known = vals + (bb.upper | filter((v) => v != null)) + (bb.lower | filter((v) => v != null)) + (candle_bars ? flatten(candles | map((c) => [c.low ?? c.close, c.high ?? c.close])) : []) let sc = bollinger_chart_scale(n == 0 ? 0 : min(known), n == 0 ? 1 : max(known), 5) let y = (v) => (sc.hi - v) * 100 / (sc.hi - sc.lo) let x = (i) => (i + 0.5) * 100 / n let pct = (v) => bollinger_chart_pct(v) let money = (v) => v == null ? "—" : `${v < 0 ? "-" : ""}${prefix}${bollinger_chart_num(abs(v), decimals)}${suffix}` let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${bollinger_chart_num(abs(t))}` let pts = (list_vals: list, idx: list) => join(idx | map((i) => `${pct(x(i))},${pct(y(list_vals[i]))}`), " L") let banded = range(n) | filter((i) => bb.mid[i] != null) let path = (list_vals: list) => len(banded) == 0 ? "" : "M" + pts(list_vals, banded) let band = len(banded) == 0 ? "" : `M${pts(bb.upper, banded)} L${pts(bb.lower, reverse(banded))} Z` let pb = (i) => bb.mid[i] == null || bb.upper[i] == bb.lower[i] ? null : (vals[i] - bb.lower[i]) / (bb.upper[i] - bb.lower[i]) let outside = (i) => bb.mid[i] != null && (vals[i] > bb.upper[i] || vals[i] < bb.lower[i]) let when = (i) => i < len(times) ? bollinger_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]) <figure class={ui_cls(["ui-bb", class])} aria-label={label} style={`--h: ${height}px`}> {show_label && <figcaption.ui-bb-title>{label}</figcaption>} <ul.ui-bb-legend aria-hidden="true"> {!candle_bars && <li><span.ui-bb-key.ui-bb-cp></span>Close</li>} <li><span.ui-bb-key.ui-bb-cb></span>SMA {period}</li> <li><span.ui-bb-kb></span>Bands ±{bollinger_chart_num(deviations)}σ</li> </ul> <div.ui-bb-frame aria-hidden="true"> <div.ui-bb-plot> {for t in sc.ticks => <span.ui-bb-grid style={`top: ${pct(y(t))}%`}></span>} <svg.ui-bb-svg viewBox="0 0 100 100" preserveAspectRatio="none"> <path.ui-bb-fill d={band}/> <path.ui-bb-edge d={path(bb.upper)} vector-effect="non-scaling-stroke"/> <path.ui-bb-edge d={path(bb.lower)} vector-effect="non-scaling-stroke"/> <path.ui-bb-mid d={path(bb.mid)} vector-effect="non-scaling-stroke"/> {!candle_bars && n > 0 && <path.ui-bb-close d={"M" + pts(vals, range(n))} vector-effect="non-scaling-stroke"/>} </svg> <div.ui-bb-cols> {for i in range(n) => <div class={ui_cls(["ui-bb-col", n > 1 && i >= n / 2 && "ui-bb-flip", candle_bars && (vals[i] >= (candles[i].open ?? vals[i]) ? "ui-bb-up" : "ui-bb-down")])}> {candle_bars && [ <span.ui-bb-wick style={`top: ${pct(y(candles[i].high ?? vals[i]))}%; height: ${pct(y(candles[i].low ?? vals[i]) - y(candles[i].high ?? vals[i]))}%`}></span>, <span.ui-bb-body style={`top: ${pct(y(max([candles[i].open ?? vals[i], vals[i]])))}%; height: ${pct(abs(y(candles[i].open ?? vals[i]) - y(vals[i])))}%`}></span> ]} {outside(i) && <span class={`ui-bb-out ${vals[i] > bb.upper[i] ? "ui-bb-down" : "ui-bb-up"}`} style={`top: ${pct(y(vals[i]))}%`}></span>} {!candle_bars && <span.ui-bb-dot style={`top: ${pct(y(vals[i]))}%`}></span>} <span.ui-bb-tip> <strong>{when(i)}</strong> <p>Close<b>{money(vals[i])}</b></p> <p>Upper<b>{money(bb.upper[i])}</b></p> <p>SMA {period}<b>{money(bb.mid[i])}</b></p> <p>Lower<b>{money(bb.lower[i])}</b></p> <p>%B<b>{pb(i) == null ? "—" : bollinger_chart_num(pb(i), 2)}</b></p> <p>Width<b>{bb.mid[i] == null || bb.mid[i] == 0 ? "—" : `${bollinger_chart_num((bb.upper[i] - bb.lower[i]) * 100 / bb.mid[i], 1)}%`}</b></p> {outside(i) && <p class={vals[i] > bb.upper[i] ? "ui-bb-down" : "ui-bb-up"}><em>{vals[i] > bb.upper[i] ? "Above the upper band" : "Below the lower band"}</em></p>} </span> </div>} </div> </div> <div.ui-bb-y style={`width: ${axis_width}ch`}> {for t in sc.ticks => !(n > 0 && abs(y(t) - y(vals[-1])) * height < 1400) && <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>} {n > 0 && <span.ui-bb-tag style={`top: ${pct(y(vals[-1]))}%`}>{money(vals[-1])}</span>} </div> <div.ui-bb-x> {len(times) == n && (for t in bollinger_chart_ticks(times, 7) => <span class={ui_cls([x(t.i) < 4 && "ui-bb-first", x(t.i) > 96 && "ui-bb-end"])} style={`left: ${pct(x(t.i))}%`}>{t.text}</span>)} </div> <span></span> </div> <table.ui-sr> <caption>{label}: close and Bollinger bands ({period}, {bollinger_chart_num(deviations)})</caption> <thead><tr><th scope="col">Time</th><th scope="col">Close</th><th scope="col">Upper</th><th scope="col">Middle</th><th scope="col">Lower</th></tr></thead> <tbody> {for i in range(n) => <tr> <th scope="row">{when(i)}</th><td>{money(vals[i])}</td><td>{money(bb.upper[i])}</td><td>{money(bb.mid[i])}</td><td>{money(bb.lower[i])}</td> </tr>} </tbody> </table> </figure>}Related
CandlestickChartOHLC 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 · 7 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 downloadsMacdChartMACD from closing prices: the MACD and signal lines and the histogram between them, under a price chart or on its own.7 views · 5 downloads
Thread
Thread 0 posts
On BollingerChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.