Charts

BollingerChart

Prices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.

#chart#data#trading#no-js

Demo

Preview
Soylent, Bollinger 20, 2
Soylent, Bollinger 20, 2: close and Bollinger bands (20, 2)
TimeCloseUpperMiddleLower
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
Soylent, last 50 days as candles, Bollinger 10, 1.5
Soylent, last 50 days as candles, Bollinger 10, 1.5: close and Bollinger bands (10, 1.5)
TimeCloseUpperMiddleLower
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.qwe
main.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>}
Thread
75Useful? Like it, or add to its thread below.

Thread 0 posts

On BollingerChart: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.