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.
Demo
| Time | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| Apr 1, 2026 | $181.85 | $185.15 | $181.47 | $184.62 | 2,295,923 |
| Apr 2, 2026 | $184.66 | $186.05 | $182.26 | $183.65 | 1,780,525 |
| Apr 3, 2026 | $183.85 | $186.33 | $183.41 | $184.82 | 1,547,666 |
| Apr 6, 2026 | $185.05 | $186.53 | $183.78 | $186.43 | 1,733,010 |
| Apr 7, 2026 | $186.90 | $189.24 | $185.94 | $189.01 | 2,451,093 |
| Apr 8, 2026 | $189.22 | $191.37 | $187.49 | $190.27 | 2,276,077 |
| Apr 9, 2026 | $190.12 | $191.71 | $188.49 | $189.12 | 1,247,989 |
| Apr 10, 2026 | $188.89 | $189.45 | $186.75 | $188.52 | 1,705,338 |
| Apr 13, 2026 | $188.78 | $191.49 | $188.26 | $191.38 | 2,028,712 |
| Apr 14, 2026 | $191.39 | $191.76 | $189.22 | $190.96 | 1,034,749 |
| Apr 15, 2026 | $190.81 | $192.08 | $188.11 | $189.87 | 1,728,066 |
| Apr 16, 2026 | $189.49 | $190.03 | $188.27 | $188.96 | 1,547,851 |
| Apr 17, 2026 | $188.77 | $190.24 | $188.33 | $189.08 | 2,168,108 |
| Apr 20, 2026 | $189.03 | $192.75 | $187.83 | $191.53 | 1,954,015 |
| Apr 21, 2026 | $191.54 | $194.72 | $190.78 | $192.91 | 2,049,232 |
| Apr 22, 2026 | $192.62 | $193.99 | $191.98 | $193.31 | 1,525,152 |
| Apr 23, 2026 | $193.65 | $196.53 | $192.06 | $196.31 | 1,375,811 |
| Apr 24, 2026 | $196.20 | $196.35 | $191.99 | $193.92 | 1,510,769 |
| Apr 27, 2026 | $194.07 | $194.96 | $190.62 | $191.76 | 1,407,402 |
| Apr 28, 2026 | $192.25 | $196.68 | $190.55 | $194.94 | 2,503,268 |
| Apr 29, 2026 | $195.25 | $195.27 | $193.34 | $193.82 | 1,200,400 |
| Apr 30, 2026 | $193.24 | $194.58 | $191.81 | $194.13 | 1,684,990 |
| May 1, 2026 | $194.35 | $196.06 | $192.22 | $192.67 | 1,638,219 |
| May 4, 2026 | $193.13 | $194.07 | $190.64 | $190.65 | 2,479,247 |
| May 5, 2026 | $190.81 | $193.77 | $189.09 | $193.73 | 1,567,167 |
| May 6, 2026 | $194.27 | $196.14 | $193.75 | $194.82 | 1,277,189 |
| May 7, 2026 | $194.48 | $195.12 | $192.98 | $195.02 | 1,920,918 |
| May 8, 2026 | $195.27 | $195.42 | $195.05 | $195.36 | 2,153,134 |
| May 11, 2026 | $194.93 | $198.06 | $194.86 | $196.31 | 2,283,379 |
| May 12, 2026 | $196.74 | $202.15 | $195.24 | $200.24 | 1,946,792 |
| May 13, 2026 | $199.94 | $203.42 | $199.14 | $201.64 | 1,446,478 |
| May 14, 2026 | $201.52 | $205.50 | $200.81 | $204.36 | 2,119,548 |
| May 15, 2026 | $204.65 | $206.13 | $202.72 | $203.58 | 1,309,313 |
| May 18, 2026 | $203.35 | $208.97 | $202.06 | $206.91 | 2,550,141 |
| May 19, 2026 | $207.43 | $208.77 | $205.08 | $205.36 | 1,840,991 |
| May 20, 2026 | $205.43 | $209.45 | $204.44 | $207.49 | 1,423,403 |
| May 21, 2026 | $207.42 | $208.79 | $204.03 | $205.10 | 1,960,051 |
| May 22, 2026 | $205.11 | $207.81 | $203.57 | $207.06 | 1,191,817 |
| May 25, 2026 | $206.81 | $207.64 | $205.31 | $207.22 | 1,346,249 |
| May 26, 2026 | $206.89 | $209.04 | $206.73 | $208.15 | 2,107,826 |
| May 27, 2026 | $208.55 | $213.62 | $207.13 | $211.69 | 2,601,020 |
| May 28, 2026 | $211.09 | $214.42 | $209.56 | $213.14 | 2,374,780 |
| May 29, 2026 | $212.55 | $212.72 | $209.72 | $209.95 | 1,344,818 |
| Jun 1, 2026 | $209.33 | $212.20 | $208.93 | $210.21 | 1,193,928 |
| Jun 2, 2026 | $209.83 | $211.73 | $208.87 | $211.22 | 2,420,700 |
| Jun 3, 2026 | $211.12 | $214.16 | $210.28 | $212.51 | 1,639,957 |
| Jun 4, 2026 | $211.95 | $213.66 | $208.01 | $210.06 | 1,346,548 |
| Jun 5, 2026 | $210.60 | $211.03 | $210.16 | $210.77 | 1,735,327 |
| Jun 8, 2026 | $210.35 | $210.92 | $206.01 | $207.35 | 2,035,816 |
| Jun 9, 2026 | $207.33 | $211.18 | $206.47 | $211.02 | 1,805,049 |
| Jun 10, 2026 | $210.39 | $211.26 | $206.67 | $206.86 | 1,669,244 |
| Jun 11, 2026 | $207.39 | $211.11 | $206.48 | $209.51 | 2,330,170 |
| Jun 12, 2026 | $210.09 | $213.82 | $208.89 | $212.41 | 1,668,004 |
| Jun 15, 2026 | $212.29 | $213.43 | $211.09 | $212.38 | 1,885,826 |
| Jun 16, 2026 | $212.59 | $215.25 | $210.71 | $213.69 | 2,180,702 |
| Jun 17, 2026 | $214.32 | $215.36 | $211.92 | $213.97 | 2,260,466 |
| Jun 18, 2026 | $213.79 | $214.52 | $208.89 | $210.75 | 2,240,241 |
| Jun 19, 2026 | $211.05 | $216.21 | $210.61 | $214.52 | 1,665,443 |
| Jun 22, 2026 | $214.90 | $216.87 | $210.34 | $212.04 | 2,116,842 |
| Jun 23, 2026 | $212.31 | $213.63 | $210.46 | $212.69 | 1,708,900 |
| Jun 24, 2026 | $213.17 | $214.63 | $210.65 | $211.57 | 1,636,019 |
| Jun 25, 2026 | $211.17 | $212.43 | $209.27 | $211.94 | 2,108,231 |
| Jun 26, 2026 | $211.73 | $215.07 | $211.48 | $213.20 | 1,502,803 |
| Jun 29, 2026 | $213.74 | $214.02 | $209.29 | $210.12 | 1,386,417 |
| Jun 30, 2026 | $210.36 | $213.77 | $209.91 | $213.40 | 2,569,078 |
| Jul 1, 2026 | $213.66 | $213.91 | $210.02 | $211.34 | 1,214,688 |
| Jul 2, 2026 | $211.74 | $214.13 | $211.47 | $212.53 | 1,447,956 |
| Jul 3, 2026 | $212.80 | $217.48 | $211.55 | $216.30 | 2,393,645 |
| Jul 6, 2026 | $216.56 | $219.75 | $216.34 | $218.44 | 1,195,648 |
| Jul 7, 2026 | $218.59 | $219.86 | $216.06 | $216.60 | 1,446,906 |
| Jul 8, 2026 | $216.76 | $219.63 | $216.24 | $217.86 | 2,259,468 |
| Jul 9, 2026 | $217.28 | $219.73 | $215.56 | $218.84 | 1,984,384 |
| Jul 10, 2026 | $219.24 | $221.26 | $215.24 | $216.32 | 2,370,597 |
| Jul 13, 2026 | $216.22 | $220.84 | $214.65 | $219.11 | 2,486,450 |
| Jul 14, 2026 | $219.58 | $221.16 | $216.97 | $217.31 | 1,348,201 |
| Jul 15, 2026 | $217.89 | $221.60 | $217.72 | $221.47 | 1,697,279 |
| Jul 16, 2026 | $222.06 | $227.20 | $221.44 | $225.05 | 1,531,191 |
| Jul 17, 2026 | $224.42 | $225.68 | $223.98 | $224.30 | 1,350,633 |
| Jul 20, 2026 | $224.27 | $224.39 | $222.14 | $222.24 | 1,270,632 |
| Jul 21, 2026 | $222.41 | $224.21 | $220.66 | $222.28 | 1,605,336 |
| Jul 22, 2026 | $222.55 | $223.92 | $219.65 | $221.79 | 2,032,553 |
| Jul 23, 2026 | $222.40 | $225.51 | $220.61 | $224.16 | 1,344,766 |
| Jul 24, 2026 | $223.58 | $226.93 | $222.62 | $225.47 | 1,863,715 |
| Jul 27, 2026 | $225.32 | $225.66 | $225.17 | $225.39 | 1,015,247 |
| Jul 28, 2026 | $225.19 | $225.64 | $222.65 | $224.08 | 1,202,414 |
| Jul 29, 2026 | $223.48 | $225.35 | $217.79 | $219.70 | 1,416,668 |
| Jul 30, 2026 | $220.05 | $220.94 | $216.78 | $218.77 | 2,132,670 |
| Jul 31, 2026 | $218.13 | $222.47 | $216.73 | $222.01 | 1,957,473 |
| Aug 3, 2026 | $221.39 | $223.06 | $217.32 | $218.97 | 1,531,439 |
| Aug 4, 2026 | $219.44 | $222.67 | $218.43 | $221.96 | 1,613,548 |
| Aug 5, 2026 | $221.63 | $226.67 | $220.13 | $224.71 | 1,948,161 |
| Aug 6, 2026 | $225.20 | $229.69 | $223.49 | $227.84 | 2,526,493 |
| Aug 7, 2026 | $228.10 | $229.72 | $225.78 | $226.51 | 2,254,600 |
| Aug 10, 2026 | $226.05 | $230.41 | $224.21 | $228.90 | 1,870,775 |
| Aug 11, 2026 | $229.31 | $232.17 | $228.78 | $232.07 | 1,488,920 |
| Aug 12, 2026 | $231.41 | $234.04 | $229.96 | $232.16 | 2,139,659 |
| Aug 13, 2026 | $232.67 | $235.83 | $231.36 | $235.34 | 1,830,450 |
| Aug 14, 2026 | $235.18 | $237.39 | $233.96 | $235.62 | 1,468,642 |
| Aug 17, 2026 | $235.62 | $237.28 | $231.84 | $233.64 | 1,528,369 |
| Aug 18, 2026 | $233.30 | $235.64 | $231.11 | $234.35 | 1,788,795 |
| Aug 19, 2026 | $234.09 | $236.38 | $232.60 | $235.03 | 1,779,379 |
| Aug 20, 2026 | $234.55 | $234.83 | $231.51 | $232.28 | 2,017,148 |
| Aug 21, 2026 | $232.80 | $235.01 | $231.99 | $234.10 | 1,588,309 |
| Aug 24, 2026 | $233.57 | $233.69 | $229.77 | $230.87 | 1,719,524 |
| Aug 25, 2026 | $230.30 | $232.43 | $229.25 | $231.50 | 1,175,055 |
| Aug 26, 2026 | $231.97 | $232.67 | $229.98 | $230.26 | 1,372,973 |
| Aug 27, 2026 | $230.17 | $231.57 | $226.55 | $227.87 | 2,187,645 |
| Aug 28, 2026 | $228.26 | $230.30 | $226.70 | $228.32 | 1,069,883 |
| Aug 31, 2026 | $228.49 | $232.55 | $227.16 | $230.50 | 2,443,281 |
| Sep 1, 2026 | $230.21 | $234.41 | $229.97 | $234.32 | 1,797,461 |
| Sep 2, 2026 | $234.82 | $239.94 | $233.54 | $238.22 | 1,613,882 |
| Sep 3, 2026 | $238.27 | $241.15 | $236.34 | $240.70 | 2,097,522 |
| Sep 4, 2026 | $240.82 | $245.50 | $240.75 | $244.75 | 2,332,578 |
| Sep 7, 2026 | $244.77 | $247.58 | $243.10 | $245.88 | 1,083,865 |
| Sep 8, 2026 | $246.18 | $252.57 | $244.51 | $250.45 | 2,133,818 |
| Sep 9, 2026 | $250.03 | $252.73 | $248.93 | $251.30 | 1,067,656 |
| Sep 10, 2026 | $251.85 | $255.80 | $251.07 | $253.33 | 2,063,510 |
| Sep 11, 2026 | $254.00 | $256.35 | $253.50 | $254.76 | 1,307,500 |
| Sep 14, 2026 | $254.68 | $254.80 | $252.77 | $254.21 | 2,003,982 |
| Sep 15, 2026 | $254.73 | $255.51 | $249.62 | $251.16 | 1,848,564 |
| Time | Open | High | Low | Close |
|---|---|---|---|---|
| Oct 19, 10:00 | 61,066 | 61,545 | 60,906 | 61,095 |
| Oct 19, 11:00 | 60,984 | 61,658 | 60,661 | 61,343 |
| Oct 19, 12:00 | 61,199 | 61,995 | 60,922 | 61,441 |
| Oct 19, 13:00 | 61,348 | 61,547 | 61,067 | 61,130 |
| Oct 19, 14:00 | 61,142 | 61,859 | 61,095 | 61,406 |
| Oct 19, 15:00 | 61,270 | 61,834 | 60,981 | 61,548 |
| Oct 19, 16:00 | 61,449 | 61,464 | 60,494 | 61,101 |
| Oct 20, 10:00 | 61,206 | 61,740 | 60,824 | 61,269 |
| Oct 20, 11:00 | 61,223 | 61,438 | 60,671 | 61,063 |
| Oct 20, 12:00 | 60,899 | 61,027 | 60,623 | 60,931 |
| Oct 20, 13:00 | 60,819 | 61,419 | 60,181 | 60,512 |
| Oct 20, 14:00 | 60,691 | 61,117 | 60,540 | 60,654 |
| Oct 20, 15:00 | 60,834 | 61,250 | 60,376 | 60,619 |
| Oct 20, 16:00 | 60,708 | 61,201 | 59,997 | 60,485 |
| Oct 21, 10:00 | 60,314 | 60,699 | 59,832 | 60,060 |
| Oct 21, 11:00 | 60,062 | 60,151 | 59,472 | 60,089 |
| Oct 21, 12:00 | 60,185 | 60,586 | 59,477 | 60,027 |
| Oct 21, 13:00 | 60,005 | 60,601 | 59,419 | 60,320 |
| Oct 21, 14:00 | 60,426 | 61,153 | 60,089 | 60,702 |
| Oct 21, 15:00 | 60,713 | 60,922 | 60,395 | 60,584 |
| Oct 21, 16:00 | 60,608 | 60,768 | 60,311 | 60,730 |
| Oct 22, 10:00 | 60,551 | 60,944 | 60,281 | 60,708 |
| Oct 22, 11:00 | 60,572 | 60,749 | 60,282 | 60,531 |
| Oct 22, 12:00 | 60,482 | 61,213 | 60,471 | 60,645 |
| Oct 22, 13:00 | 60,762 | 60,874 | 60,519 | 60,797 |
| Oct 22, 14:00 | 60,911 | 60,957 | 60,479 | 60,843 |
| Oct 22, 15:00 | 60,753 | 60,944 | 60,307 | 60,681 |
| Oct 22, 16:00 | 60,603 | 61,089 | 60,201 | 60,969 |
| Oct 23, 10:00 | 61,073 | 61,292 | 60,676 | 60,811 |
| Oct 23, 11:00 | 60,690 | 61,476 | 60,332 | 61,033 |
| Oct 23, 12:00 | 61,029 | 61,208 | 60,705 | 60,942 |
| Oct 23, 13:00 | 60,916 | 61,131 | 60,731 | 60,797 |
| Oct 23, 14:00 | 60,764 | 60,977 | 60,094 | 60,418 |
| Oct 23, 15:00 | 60,552 | 61,450 | 60,071 | 60,863 |
| Oct 23, 16:00 | 60,867 | 61,397 | 60,164 | 60,656 |
| Time | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| Aug 5, 2026 | $221.63 | $226.67 | $220.13 | $224.71 | 1,948,161 |
| Aug 6, 2026 | $225.20 | $229.69 | $223.49 | $227.84 | 2,526,493 |
| Aug 7, 2026 | $228.10 | $229.72 | $225.78 | $226.51 | 2,254,600 |
| Aug 10, 2026 | $226.05 | $230.41 | $224.21 | $228.90 | 1,870,775 |
| Aug 11, 2026 | $229.31 | $232.17 | $228.78 | $232.07 | 1,488,920 |
| Aug 12, 2026 | $231.41 | $234.04 | $229.96 | $232.16 | 2,139,659 |
| Aug 13, 2026 | $232.67 | $235.83 | $231.36 | $235.34 | 1,830,450 |
| Aug 14, 2026 | $235.18 | $237.39 | $233.96 | $235.62 | 1,468,642 |
| Aug 17, 2026 | $235.62 | $237.28 | $231.84 | $233.64 | 1,528,369 |
| Aug 18, 2026 | $233.30 | $235.64 | $231.11 | $234.35 | 1,788,795 |
| Aug 19, 2026 | $234.09 | $236.38 | $232.60 | $235.03 | 1,779,379 |
| Aug 20, 2026 | $234.55 | $234.83 | $231.51 | $232.28 | 2,017,148 |
| Aug 21, 2026 | $232.80 | $235.01 | $231.99 | $234.10 | 1,588,309 |
| Aug 24, 2026 | $233.57 | $233.69 | $229.77 | $230.87 | 1,719,524 |
| Aug 25, 2026 | $230.30 | $232.43 | $229.25 | $231.50 | 1,175,055 |
| Aug 26, 2026 | $231.97 | $232.67 | $229.98 | $230.26 | 1,372,973 |
| Aug 27, 2026 | $230.17 | $231.57 | $226.55 | $227.87 | 2,187,645 |
| Aug 28, 2026 | $228.26 | $230.30 | $226.70 | $228.32 | 1,069,883 |
| Aug 31, 2026 | $228.49 | $232.55 | $227.16 | $230.50 | 2,443,281 |
| Sep 1, 2026 | $230.21 | $234.41 | $229.97 | $234.32 | 1,797,461 |
| Sep 2, 2026 | $234.82 | $239.94 | $233.54 | $238.22 | 1,613,882 |
| Sep 3, 2026 | $238.27 | $241.15 | $236.34 | $240.70 | 2,097,522 |
| Sep 4, 2026 | $240.82 | $245.50 | $240.75 | $244.75 | 2,332,578 |
| Sep 7, 2026 | $244.77 | $247.58 | $243.10 | $245.88 | 1,083,865 |
| Sep 8, 2026 | $246.18 | $252.57 | $244.51 | $250.45 | 2,133,818 |
| Sep 9, 2026 | $250.03 | $252.73 | $248.93 | $251.30 | 1,067,656 |
| Sep 10, 2026 | $251.85 | $255.80 | $251.07 | $253.33 | 2,063,510 |
| Sep 11, 2026 | $254.00 | $256.35 | $253.50 | $254.76 | 1,307,500 |
| Sep 14, 2026 | $254.68 | $254.80 | $252.77 | $254.21 | 2,003,982 |
| Sep 15, 2026 | $254.73 | $255.51 | $249.62 | $251.16 | 1,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.qwemain.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>}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 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 CandlestickChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.