Charts

CandlestickChart

OHLC candles, green up and red down, with a volume pane, a date axis, moving averages and the details of each candle on hover.

#chart#data#trading#no-js

Demo

Preview
ACME, daily
ACME, daily
TimeOpenHighLowCloseVolume
Apr 1, 2026$181.85$185.15$181.47$184.622,295,923
Apr 2, 2026$184.66$186.05$182.26$183.651,780,525
Apr 3, 2026$183.85$186.33$183.41$184.821,547,666
Apr 6, 2026$185.05$186.53$183.78$186.431,733,010
Apr 7, 2026$186.90$189.24$185.94$189.012,451,093
Apr 8, 2026$189.22$191.37$187.49$190.272,276,077
Apr 9, 2026$190.12$191.71$188.49$189.121,247,989
Apr 10, 2026$188.89$189.45$186.75$188.521,705,338
Apr 13, 2026$188.78$191.49$188.26$191.382,028,712
Apr 14, 2026$191.39$191.76$189.22$190.961,034,749
Apr 15, 2026$190.81$192.08$188.11$189.871,728,066
Apr 16, 2026$189.49$190.03$188.27$188.961,547,851
Apr 17, 2026$188.77$190.24$188.33$189.082,168,108
Apr 20, 2026$189.03$192.75$187.83$191.531,954,015
Apr 21, 2026$191.54$194.72$190.78$192.912,049,232
Apr 22, 2026$192.62$193.99$191.98$193.311,525,152
Apr 23, 2026$193.65$196.53$192.06$196.311,375,811
Apr 24, 2026$196.20$196.35$191.99$193.921,510,769
Apr 27, 2026$194.07$194.96$190.62$191.761,407,402
Apr 28, 2026$192.25$196.68$190.55$194.942,503,268
Apr 29, 2026$195.25$195.27$193.34$193.821,200,400
Apr 30, 2026$193.24$194.58$191.81$194.131,684,990
May 1, 2026$194.35$196.06$192.22$192.671,638,219
May 4, 2026$193.13$194.07$190.64$190.652,479,247
May 5, 2026$190.81$193.77$189.09$193.731,567,167
May 6, 2026$194.27$196.14$193.75$194.821,277,189
May 7, 2026$194.48$195.12$192.98$195.021,920,918
May 8, 2026$195.27$195.42$195.05$195.362,153,134
May 11, 2026$194.93$198.06$194.86$196.312,283,379
May 12, 2026$196.74$202.15$195.24$200.241,946,792
May 13, 2026$199.94$203.42$199.14$201.641,446,478
May 14, 2026$201.52$205.50$200.81$204.362,119,548
May 15, 2026$204.65$206.13$202.72$203.581,309,313
May 18, 2026$203.35$208.97$202.06$206.912,550,141
May 19, 2026$207.43$208.77$205.08$205.361,840,991
May 20, 2026$205.43$209.45$204.44$207.491,423,403
May 21, 2026$207.42$208.79$204.03$205.101,960,051
May 22, 2026$205.11$207.81$203.57$207.061,191,817
May 25, 2026$206.81$207.64$205.31$207.221,346,249
May 26, 2026$206.89$209.04$206.73$208.152,107,826
May 27, 2026$208.55$213.62$207.13$211.692,601,020
May 28, 2026$211.09$214.42$209.56$213.142,374,780
May 29, 2026$212.55$212.72$209.72$209.951,344,818
Jun 1, 2026$209.33$212.20$208.93$210.211,193,928
Jun 2, 2026$209.83$211.73$208.87$211.222,420,700
Jun 3, 2026$211.12$214.16$210.28$212.511,639,957
Jun 4, 2026$211.95$213.66$208.01$210.061,346,548
Jun 5, 2026$210.60$211.03$210.16$210.771,735,327
Jun 8, 2026$210.35$210.92$206.01$207.352,035,816
Jun 9, 2026$207.33$211.18$206.47$211.021,805,049
Jun 10, 2026$210.39$211.26$206.67$206.861,669,244
Jun 11, 2026$207.39$211.11$206.48$209.512,330,170
Jun 12, 2026$210.09$213.82$208.89$212.411,668,004
Jun 15, 2026$212.29$213.43$211.09$212.381,885,826
Jun 16, 2026$212.59$215.25$210.71$213.692,180,702
Jun 17, 2026$214.32$215.36$211.92$213.972,260,466
Jun 18, 2026$213.79$214.52$208.89$210.752,240,241
Jun 19, 2026$211.05$216.21$210.61$214.521,665,443
Jun 22, 2026$214.90$216.87$210.34$212.042,116,842
Jun 23, 2026$212.31$213.63$210.46$212.691,708,900
Jun 24, 2026$213.17$214.63$210.65$211.571,636,019
Jun 25, 2026$211.17$212.43$209.27$211.942,108,231
Jun 26, 2026$211.73$215.07$211.48$213.201,502,803
Jun 29, 2026$213.74$214.02$209.29$210.121,386,417
Jun 30, 2026$210.36$213.77$209.91$213.402,569,078
Jul 1, 2026$213.66$213.91$210.02$211.341,214,688
Jul 2, 2026$211.74$214.13$211.47$212.531,447,956
Jul 3, 2026$212.80$217.48$211.55$216.302,393,645
Jul 6, 2026$216.56$219.75$216.34$218.441,195,648
Jul 7, 2026$218.59$219.86$216.06$216.601,446,906
Jul 8, 2026$216.76$219.63$216.24$217.862,259,468
Jul 9, 2026$217.28$219.73$215.56$218.841,984,384
Jul 10, 2026$219.24$221.26$215.24$216.322,370,597
Jul 13, 2026$216.22$220.84$214.65$219.112,486,450
Jul 14, 2026$219.58$221.16$216.97$217.311,348,201
Jul 15, 2026$217.89$221.60$217.72$221.471,697,279
Jul 16, 2026$222.06$227.20$221.44$225.051,531,191
Jul 17, 2026$224.42$225.68$223.98$224.301,350,633
Jul 20, 2026$224.27$224.39$222.14$222.241,270,632
Jul 21, 2026$222.41$224.21$220.66$222.281,605,336
Jul 22, 2026$222.55$223.92$219.65$221.792,032,553
Jul 23, 2026$222.40$225.51$220.61$224.161,344,766
Jul 24, 2026$223.58$226.93$222.62$225.471,863,715
Jul 27, 2026$225.32$225.66$225.17$225.391,015,247
Jul 28, 2026$225.19$225.64$222.65$224.081,202,414
Jul 29, 2026$223.48$225.35$217.79$219.701,416,668
Jul 30, 2026$220.05$220.94$216.78$218.772,132,670
Jul 31, 2026$218.13$222.47$216.73$222.011,957,473
Aug 3, 2026$221.39$223.06$217.32$218.971,531,439
Aug 4, 2026$219.44$222.67$218.43$221.961,613,548
Aug 5, 2026$221.63$226.67$220.13$224.711,948,161
Aug 6, 2026$225.20$229.69$223.49$227.842,526,493
Aug 7, 2026$228.10$229.72$225.78$226.512,254,600
Aug 10, 2026$226.05$230.41$224.21$228.901,870,775
Aug 11, 2026$229.31$232.17$228.78$232.071,488,920
Aug 12, 2026$231.41$234.04$229.96$232.162,139,659
Aug 13, 2026$232.67$235.83$231.36$235.341,830,450
Aug 14, 2026$235.18$237.39$233.96$235.621,468,642
Aug 17, 2026$235.62$237.28$231.84$233.641,528,369
Aug 18, 2026$233.30$235.64$231.11$234.351,788,795
Aug 19, 2026$234.09$236.38$232.60$235.031,779,379
Aug 20, 2026$234.55$234.83$231.51$232.282,017,148
Aug 21, 2026$232.80$235.01$231.99$234.101,588,309
Aug 24, 2026$233.57$233.69$229.77$230.871,719,524
Aug 25, 2026$230.30$232.43$229.25$231.501,175,055
Aug 26, 2026$231.97$232.67$229.98$230.261,372,973
Aug 27, 2026$230.17$231.57$226.55$227.872,187,645
Aug 28, 2026$228.26$230.30$226.70$228.321,069,883
Aug 31, 2026$228.49$232.55$227.16$230.502,443,281
Sep 1, 2026$230.21$234.41$229.97$234.321,797,461
Sep 2, 2026$234.82$239.94$233.54$238.221,613,882
Sep 3, 2026$238.27$241.15$236.34$240.702,097,522
Sep 4, 2026$240.82$245.50$240.75$244.752,332,578
Sep 7, 2026$244.77$247.58$243.10$245.881,083,865
Sep 8, 2026$246.18$252.57$244.51$250.452,133,818
Sep 9, 2026$250.03$252.73$248.93$251.301,067,656
Sep 10, 2026$251.85$255.80$251.07$253.332,063,510
Sep 11, 2026$254.00$256.35$253.50$254.761,307,500
Sep 14, 2026$254.68$254.80$252.77$254.212,003,982
Sep 15, 2026$254.73$255.51$249.62$251.161,848,564
BTC-USD, hourly
BTC-USD, hourly
TimeOpenHighLowClose
Oct 19, 10:0061,06661,54560,90661,095
Oct 19, 11:0060,98461,65860,66161,343
Oct 19, 12:0061,19961,99560,92261,441
Oct 19, 13:0061,34861,54761,06761,130
Oct 19, 14:0061,14261,85961,09561,406
Oct 19, 15:0061,27061,83460,98161,548
Oct 19, 16:0061,44961,46460,49461,101
Oct 20, 10:0061,20661,74060,82461,269
Oct 20, 11:0061,22361,43860,67161,063
Oct 20, 12:0060,89961,02760,62360,931
Oct 20, 13:0060,81961,41960,18160,512
Oct 20, 14:0060,69161,11760,54060,654
Oct 20, 15:0060,83461,25060,37660,619
Oct 20, 16:0060,70861,20159,99760,485
Oct 21, 10:0060,31460,69959,83260,060
Oct 21, 11:0060,06260,15159,47260,089
Oct 21, 12:0060,18560,58659,47760,027
Oct 21, 13:0060,00560,60159,41960,320
Oct 21, 14:0060,42661,15360,08960,702
Oct 21, 15:0060,71360,92260,39560,584
Oct 21, 16:0060,60860,76860,31160,730
Oct 22, 10:0060,55160,94460,28160,708
Oct 22, 11:0060,57260,74960,28260,531
Oct 22, 12:0060,48261,21360,47160,645
Oct 22, 13:0060,76260,87460,51960,797
Oct 22, 14:0060,91160,95760,47960,843
Oct 22, 15:0060,75360,94460,30760,681
Oct 22, 16:0060,60361,08960,20160,969
Oct 23, 10:0061,07361,29260,67660,811
Oct 23, 11:0060,69061,47660,33261,033
Oct 23, 12:0061,02961,20860,70560,942
Oct 23, 13:0060,91661,13160,73160,797
Oct 23, 14:0060,76460,97760,09460,418
Oct 23, 15:0060,55261,45060,07160,863
Oct 23, 16:0060,86761,39760,16460,656
ACME, last 30 days
ACME, last 30 days
TimeOpenHighLowCloseVolume
Aug 5, 2026$221.63$226.67$220.13$224.711,948,161
Aug 6, 2026$225.20$229.69$223.49$227.842,526,493
Aug 7, 2026$228.10$229.72$225.78$226.512,254,600
Aug 10, 2026$226.05$230.41$224.21$228.901,870,775
Aug 11, 2026$229.31$232.17$228.78$232.071,488,920
Aug 12, 2026$231.41$234.04$229.96$232.162,139,659
Aug 13, 2026$232.67$235.83$231.36$235.341,830,450
Aug 14, 2026$235.18$237.39$233.96$235.621,468,642
Aug 17, 2026$235.62$237.28$231.84$233.641,528,369
Aug 18, 2026$233.30$235.64$231.11$234.351,788,795
Aug 19, 2026$234.09$236.38$232.60$235.031,779,379
Aug 20, 2026$234.55$234.83$231.51$232.282,017,148
Aug 21, 2026$232.80$235.01$231.99$234.101,588,309
Aug 24, 2026$233.57$233.69$229.77$230.871,719,524
Aug 25, 2026$230.30$232.43$229.25$231.501,175,055
Aug 26, 2026$231.97$232.67$229.98$230.261,372,973
Aug 27, 2026$230.17$231.57$226.55$227.872,187,645
Aug 28, 2026$228.26$230.30$226.70$228.321,069,883
Aug 31, 2026$228.49$232.55$227.16$230.502,443,281
Sep 1, 2026$230.21$234.41$229.97$234.321,797,461
Sep 2, 2026$234.82$239.94$233.54$238.221,613,882
Sep 3, 2026$238.27$241.15$236.34$240.702,097,522
Sep 4, 2026$240.82$245.50$240.75$244.752,332,578
Sep 7, 2026$244.77$247.58$243.10$245.881,083,865
Sep 8, 2026$246.18$252.57$244.51$250.452,133,818
Sep 9, 2026$250.03$252.73$248.93$251.301,067,656
Sep 10, 2026$251.85$255.80$251.07$253.332,063,510
Sep 11, 2026$254.00$256.35$253.50$254.761,307,500
Sep 14, 2026$254.68$254.80$252.77$254.212,003,982
Sep 15, 2026$254.73$255.51$249.62$251.161,848,564

Hover a candle for its open, high, low, close, change and volume.

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/candlestick_chart.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/candlestick_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/candlestick_chart.qwe
// Prices from a seeded random walk (Park–Miller), the same on every render.fn candlestick_chart_demo_walk(seed: int, count: int, start: num, from: str, hourly: bool = false) -> list {    let s = seed    let rnd = () => {        s = s * 16807 % 2147483647        s / 2147483647    }    let p = start    let day = from    let hour = 10    let out = []    while len(out) < count {        if date.weekday(day) <= 5 {            let open = round((p * (1 + (rnd() - 0.5) * 0.006)) * 100) / 100            let close = round((open * (1 + (rnd() - 0.485) * (hourly ? 0.012 : 0.035))) * 100) / 100            let high = round((max([open, close]) * (1 + rnd() * 0.01)) * 100) / 100            let low = round((min([open, close]) * (1 - rnd() * 0.01)) * 100) / 100            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 candlestick_chart_demo_daily = candlestick_chart_demo_walk(7, 120, 182.4, "2026-04-01")let candlestick_chart_demo_hourly = candlestick_chart_demo_walk(42, 35, 61250, "2026-10-19", true)view CandlestickChartDemo() =    <div.demo-stack>        <div id="candlestick_chart-demo-daily">            <CandlestickChart label="ACME, daily" show_label={true} candles={candlestick_chart_demo_daily}                              sma={[20]} ema={[50]} prefix="$"/>        </div>        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px 40px">            <div id="candlestick_chart-demo-hourly">                <CandlestickChart label="BTC-USD, hourly" show_label={true} candles={candlestick_chart_demo_hourly}                                  volume={false} decimals={0} height={220}/>            </div>            <div id="candlestick_chart-demo-small">                <CandlestickChart label="ACME, last 30 days" show_label={true} candles={drop(candlestick_chart_demo_daily, 90)}                                  prefix="$" height={180} volume_height={44} last_line={false}/>            </div>        </div>        <p.demo-note>Hover a candle for its open, high, low, close, change and volume.</p>    </div>

Source

ui/candlestick_chart.qwe
// CandlestickChart: a price chart of OHLC candles, green where the close is up on the// open and red where it is down, with a volume pane under it, a date axis, moving// averages if asked for, and a dashed line at the last close. CSS only: hovering a// candle shows its date, open, high, low, close, change and volume, and screen// readers get the numbers as a table.////   <CandlestickChart label="ACME, daily" candles={bars}/>//   <CandlestickChart label="ACME" candles={bars} sma={[20]} ema={[50]} prefix="$" height={360}/>//   <CandlestickChart label="BTC-USD, hourly" candles={hourly} volume={false} decimals={0}/>//// candles: a list of { time, open, high, low, close, volume }, oldest first. time is a// date ("2026-03-02") or a date-time ("2026-03-02T14:00:00Z", shown as written);// volume may be left out. The axis below is labelled at the starts of years, months// or days as the candles cross them.// sma and ema: periods of simple and exponential moving averages of the close, each// drawn as a line of its own colour and named in a legend ([20], [50, 200]).// volume: the volume pane under the candles (true); volume_height: its height in// pixels (64). height: the price pane's height in pixels (300). last_line: a dashed// line at the last close, with its price on the axis (true). 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 candlestick_chart_css = css`.ui-cs { --ui-cs-1: light-dark(#2a78d6, #3987e5); --ui-cs-2: light-dark(#eb6834, #d95926);  --ui-cs-3: light-dark(#1baf7a, #199e70); --ui-cs-4: light-dark(#eda100, #c98500);  --ui-cs-5: light-dark(#e87ba4, #d55181); --ui-cs-6: light-dark(#008300, #008300);  --ui-cs-7: light-dark(#6250d6, #9085e9); --ui-cs-8: light-dark(#e34948, #e66767);  display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-cs-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-cs-s1 { --c: var(--ui-cs-1); } .ui-cs-s2 { --c: var(--ui-cs-2); } .ui-cs-s3 { --c: var(--ui-cs-3); }.ui-cs-s4 { --c: var(--ui-cs-4); } .ui-cs-s5 { --c: var(--ui-cs-5); } .ui-cs-s6 { --c: var(--ui-cs-6); }.ui-cs-s7 { --c: var(--ui-cs-7); } .ui-cs-s8 { --c: var(--ui-cs-8); }.ui-cs-up { --k: var(--ui-success); } .ui-cs-down { --k: var(--ui-danger); }.ui-cs-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-cs-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-cs-key { width: 14px; height: 3px; border-radius: 2px; background: var(--c); flex: none; }.ui-cs-frame { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; padding-top: 8px; }.ui-cs-plot { position: relative; }.ui-cs-pane, .ui-cs-cp { position: relative; height: var(--h); }.ui-cs-vpane, .ui-cs-cv { position: relative; height: var(--vh); margin-top: 16px; }.ui-cs-vpane { border-top: 1px solid color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-cs-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-cs-svg { position: absolute; z-index: 2; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-cs-ma { fill: none; stroke: var(--c); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }.ui-cs-last { position: absolute; z-index: 1; left: 0; right: 0; border-top: 1px dashed var(--k); opacity: .75; pointer-events: none; }.ui-cs-cols { position: absolute; inset: 0; display: flex; }.ui-cs-col { position: relative; flex: 1; min-width: 0; }.ui-cs-col::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: var(--ui-strong); opacity: 0; }.ui-cs-col:hover { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-cs-col:hover::before { opacity: .8; }.ui-cs-wick { position: absolute; left: 50%; width: 1px; margin-left: -.5px; background: var(--k); }.ui-cs-body { position: absolute; left: 16%; right: 16%; min-height: 1px; border-radius: 1px; background: var(--k); }.ui-cs-vol { position: absolute; bottom: 0; left: 16%; right: 16%; min-height: 1px; border-radius: 1px 1px 0 0; background: var(--k); opacity: .35; }.ui-cs-col:hover .ui-cs-vol { opacity: .7; }.ui-cs-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-cs-flip .ui-cs-tip { left: auto; right: calc(50% + 10px); }.ui-cs-col:hover .ui-cs-tip { display: block; }.ui-cs-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-cs-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-cs-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-cs-tip .ui-cs-chg b { color: var(--k); }.ui-cs-y { position: relative; }.ui-cs-y > div { position: relative; }.ui-cs-y span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-cs-y .ui-cs-tag { z-index: 1; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--k); color: var(--ui-bg); font-weight: 600; }.ui-cs-x { position: relative; height: 16px; margin-top: 6px; }.ui-cs-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-cs-x .ui-cs-first { transform: none; }.ui-cs-x .ui-cs-end { transform: translateX(-100%); }`// n (a whole number, 0 or more) with at least `width` digits.fn candlestick_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 candlestick_chart_group(n: int) -> str =    n < 1000 ? str(n) : candlestick_chart_group(floor(n / 1000)) + "," + candlestick_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 candlestick_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)}` : `.${candlestick_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${candlestick_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn candlestick_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 ? candlestick_chart_num(v)        : `${v < 0 ? "-" : ""}${candlestick_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn candlestick_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${candlestick_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn candlestick_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 candlestick_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 candlestick_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 candlestick_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 candlestick_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 candlestick_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}view CandlestickChart(candles: list, sma: list = [], ema: list = [], volume: bool = true, height: int = 300,                      volume_height: int = 64, last_line: bool = true, prefix: str = "", suffix: str = "",                      decimals: int = 2, label: str = "Candlestick chart", show_label: bool = false,                      class: str? = null) {    style global candlestick_chart_css    let n = len(candles)    let closes = candles | map((c) => c.close)    let lines = (sma | map((p) => { name: `SMA ${p}`, values: candlestick_chart_sma(closes, p) }))        + (ema | map((p) => { name: `EMA ${p}`, values: candlestick_chart_ema(closes, p) }))    let hues = [1, 2, 7, 4, 5, 3, 6, 8]    let known = flatten(candles | map((c) => [c.low, c.high])) + flatten(lines | map((l) => l.values | filter((v) => v != null)))    let sc = candlestick_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) => candlestick_chart_pct(v)    let vols = candles | map((c) => c.volume ?? 0)    let vtop = max([0] + vols)    let price = (v) => v == null ? "—" : `${v < 0 ? "-" : ""}${prefix}${candlestick_chart_num(abs(v), decimals)}${suffix}`    let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${candlestick_chart_num(abs(t))}`    let change = (i) => {        let before = i > 0 ? candles[i - 1].close : candles[i].open        before != 0 ? (candles[i].close - before) * 100 / before : 0    }    let side = (up) => up ? "ui-cs-up" : "ui-cs-down"    let last = n > 0 ? candles[-1] : null    let axis_width = max([4] + (sc.ticks | map((t) => len(tick(t)))) + [last != null ? len(price(last.close)) + 1 : 0])    let line_path = (vals: list) => {        let at = range(n) | filter((i) => vals[i] != null)        len(at) == 0 ? "" : "M" + join(at | map((i) => `${pct(x(i))},${pct(y(vals[i]))}`), " L")    }    <figure class={ui_cls(["ui-cs", class])} aria-label={label}            style={`--h: ${height}px; --vh: ${volume ? volume_height : 0}px`}>        {show_label && <figcaption.ui-cs-title>{label}</figcaption>}        {len(lines) > 0 && <ul.ui-cs-legend aria-hidden="true">            {for [j, l] in enumerate(lines) => <li><span class={`ui-cs-key ui-cs-s${hues[j % 8]}`}></span>{l.name}</li>}        </ul>}        <div.ui-cs-frame aria-hidden="true">            <div.ui-cs-plot>                <div.ui-cs-pane>                    {for t in sc.ticks => <span.ui-cs-grid style={`top: ${pct(y(t))}%`}></span>}                    {last_line && last != null && <span class={`ui-cs-last ${side(last.close >= last.open)}`}                                                        style={`top: ${pct(y(last.close))}%`}></span>}                    {len(lines) > 0 && <svg.ui-cs-svg viewBox="0 0 100 100" preserveAspectRatio="none">                        {for [j, l] in enumerate(lines) => <path class={`ui-cs-ma ui-cs-s${hues[j % 8]}`} d={line_path(l.values)}                                                                 vector-effect="non-scaling-stroke"/>}                    </svg>}                </div>                {volume && <div.ui-cs-vpane></div>}                <div.ui-cs-cols>                    {for [i, c] in enumerate(candles) => <div class={ui_cls(["ui-cs-col", side(c.close >= c.open), n > 1 && i >= n / 2 && "ui-cs-flip"])}>                        <div.ui-cs-cp>                            <span.ui-cs-wick style={`top: ${pct(y(c.high))}%; height: ${pct(y(c.low) - y(c.high))}%`}></span>                            <span.ui-cs-body style={`top: ${pct(y(max([c.open, c.close])))}%; height: ${pct(abs(y(c.open) - y(c.close)))}%`}></span>                        </div>                        {volume && <div.ui-cs-cv>                            <span.ui-cs-vol style={`height: ${vtop > 0 ? pct(vols[i] * 100 / vtop) : 0}%`}></span>                        </div>}                        <span.ui-cs-tip>                            <strong>{candlestick_chart_when(c.time)}</strong>                            <p>Open<b>{price(c.open)}</b></p>                            <p>High<b>{price(c.high)}</b></p>                            <p>Low<b>{price(c.low)}</b></p>                            <p>Close<b>{price(c.close)}</b></p>                            <p class={`ui-cs-chg ${side(change(i) >= 0)}`}>Change<b>{candlestick_chart_signed(change(i))}</b></p>                            {volume && <p>Volume<b>{candlestick_chart_num(vols[i], 0)}</b></p>}                            {for [j, l] in enumerate(lines) => <p>                                <span class={`ui-cs-key ui-cs-s${hues[j % 8]}`}></span>{l.name}<b>{price(l.values[i])}</b>                            </p>}                        </span>                    </div>}                </div>            </div>            <div.ui-cs-y style={`width: ${axis_width}ch`}>                <div style="height: var(--h)">                    {for t in sc.ticks => !(last_line && last != null && abs(y(t) - y(last.close)) * height < 1400) &&                        <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>}                    {last_line && last != null && <span class={`ui-cs-tag ${side(last.close >= last.open)}`}                                                        style={`top: ${pct(y(last.close))}%`}>{price(last.close)}</span>}                </div>                {volume && <div style="height: var(--vh); margin-top: 16px">                    {vtop > 0 && <span style="top: 2px; transform: none">{candlestick_chart_short(vtop)}</span>}                </div>}            </div>            <div.ui-cs-x>                {for t in candlestick_chart_ticks(candles | map((c) => c.time), 7) => <span class={ui_cls([x(t.i) < 4 && "ui-cs-first", x(t.i) > 96 && "ui-cs-end"])}                                                                               style={`left: ${pct(x(t.i))}%`}>{t.text}</span>}            </div>            <span></span>        </div>        <table.ui-sr>            <caption>{label}</caption>            <thead><tr>                <th scope="col">Time</th><th scope="col">Open</th><th scope="col">High</th>                <th scope="col">Low</th><th scope="col">Close</th>{volume && <th scope="col">Volume</th>}            </tr></thead>            <tbody>                {for [i, c] in enumerate(candles) => <tr>                    <th scope="row">{candlestick_chart_when(c.time)}</th>                    <td>{price(c.open)}</td><td>{price(c.high)}</td><td>{price(c.low)}</td><td>{price(c.close)}</td>                    {volume && <td>{candlestick_chart_num(vols[i], 0)}</td>}                </tr>}            </tbody>        </table>    </figure>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.