HeikinAshiChart
Heikin-Ashi candles worked out from OHLC, which smooth the noise so trends show as runs of one colour, with the real close on hover.
Demo
| Time | HA open | HA high | HA low | HA close | Close | Volume |
|---|---|---|---|---|---|---|
| Apr 15, 2026 | $48.06 | $48.63 | $47.41 | $48.04 | $47.67 | 1,677,098 |
| Apr 16, 2026 | $48.05 | $48.05 | $46.58 | $47.21 | $46.87 | 1,952,736 |
| Apr 17, 2026 | $47.63 | $47.77 | $46.59 | $47.26 | $47.72 | 1,646,951 |
| Apr 20, 2026 | $47.44 | $47.71 | $47.16 | $47.51 | $47.55 | 1,670,801 |
| Apr 21, 2026 | $47.48 | $48.36 | $47.39 | $47.86 | $48.10 | 1,818,564 |
| Apr 22, 2026 | $47.67 | $48.01 | $47.10 | $47.62 | $47.37 | 1,420,791 |
| Apr 23, 2026 | $47.64 | $47.64 | $47.45 | $47.53 | $47.57 | 2,200,826 |
| Apr 24, 2026 | $47.58 | $47.88 | $47.20 | $47.59 | $47.69 | 1,506,187 |
| Apr 27, 2026 | $47.59 | $48.23 | $47.58 | $47.87 | $47.93 | 2,337,109 |
| Apr 28, 2026 | $47.73 | $48.22 | $47.73 | $48.02 | $48.05 | 989,966 |
| Apr 29, 2026 | $47.88 | $49.17 | $47.88 | $48.53 | $48.94 | 1,732,408 |
| Apr 30, 2026 | $48.20 | $49.36 | $48.09 | $48.76 | $48.53 | 1,711,314 |
| May 1, 2026 | $48.48 | $49.25 | $48.46 | $48.79 | $48.82 | 1,946,303 |
| May 4, 2026 | $48.63 | $48.82 | $47.78 | $48.39 | $48.16 | 2,183,082 |
| May 5, 2026 | $48.51 | $48.93 | $48.15 | $48.50 | $48.75 | 1,479,429 |
| May 6, 2026 | $48.50 | $49.14 | $48.50 | $48.89 | $49.07 | 2,014,236 |
| May 7, 2026 | $48.70 | $49.79 | $48.52 | $49.21 | $49.61 | 2,584,303 |
| May 8, 2026 | $48.95 | $50.72 | $48.95 | $49.95 | $50.38 | 1,914,764 |
| May 11, 2026 | $49.45 | $50.38 | $49.17 | $49.86 | $49.57 | 2,117,068 |
| May 12, 2026 | $49.66 | $49.74 | $49.45 | $49.58 | $49.55 | 1,653,451 |
| May 13, 2026 | $49.62 | $50.29 | $49.46 | $49.81 | $49.99 | 1,884,771 |
| May 14, 2026 | $49.71 | $50.26 | $49.68 | $49.91 | $49.83 | 1,554,873 |
| May 15, 2026 | $49.81 | $50.35 | $49.25 | $49.77 | $49.49 | 1,660,926 |
| May 18, 2026 | $49.79 | $50.22 | $49.23 | $49.67 | $49.78 | 2,429,948 |
| May 19, 2026 | $49.73 | $49.73 | $49.11 | $49.46 | $49.37 | 1,892,181 |
| May 20, 2026 | $49.59 | $49.73 | $49.24 | $49.44 | $49.47 | 1,391,719 |
| May 21, 2026 | $49.52 | $50.56 | $49.11 | $49.86 | $50.33 | 2,565,590 |
| May 22, 2026 | $49.69 | $50.68 | $49.69 | $50.46 | $50.48 | 1,515,826 |
| May 25, 2026 | $50.07 | $51.78 | $50.07 | $50.99 | $51.37 | 2,196,772 |
| May 26, 2026 | $50.53 | $51.85 | $50.53 | $51.17 | $50.94 | 1,428,739 |
| May 27, 2026 | $50.85 | $51.45 | $50.01 | $50.69 | $50.31 | 1,722,773 |
| May 28, 2026 | $50.77 | $51.56 | $50.15 | $50.77 | $51.06 | 1,943,369 |
| May 29, 2026 | $50.77 | $52.47 | $50.77 | $51.59 | $51.97 | 2,229,993 |
| Jun 1, 2026 | $51.18 | $52.77 | $51.18 | $52.27 | $52.63 | 1,796,330 |
| Jun 2, 2026 | $51.72 | $52.54 | $51.72 | $52.30 | $52.27 | 1,864,565 |
| Jun 3, 2026 | $52.01 | $52.91 | $52.01 | $52.56 | $52.85 | 1,852,567 |
| Jun 4, 2026 | $52.28 | $53.02 | $52.28 | $52.65 | $52.55 | 2,310,460 |
| Jun 5, 2026 | $52.47 | $53.44 | $52.29 | $52.85 | $52.96 | 2,118,054 |
| Jun 8, 2026 | $52.66 | $53.78 | $52.64 | $53.21 | $53.35 | 1,561,395 |
| Jun 9, 2026 | $52.93 | $54.43 | $52.93 | $53.82 | $54.13 | 2,581,081 |
| Jun 10, 2026 | $53.38 | $54.62 | $53.10 | $53.89 | $53.61 | 1,995,529 |
| Jun 11, 2026 | $53.63 | $54.11 | $53.23 | $53.61 | $53.43 | 1,721,313 |
| Jun 12, 2026 | $53.62 | $53.62 | $52.34 | $52.96 | $52.75 | 2,544,294 |
| Jun 15, 2026 | $53.29 | $53.29 | $52.58 | $52.70 | $52.61 | 1,559,372 |
| Jun 16, 2026 | $53.00 | $53.00 | $51.56 | $52.22 | $51.82 | 1,836,964 |
| Jun 17, 2026 | $52.61 | $52.61 | $51.21 | $51.64 | $51.51 | 1,131,506 |
| Jun 18, 2026 | $52.12 | $52.12 | $51.51 | $51.66 | $51.61 | 1,398,973 |
| Jun 19, 2026 | $51.89 | $52.68 | $51.55 | $52.12 | $52.54 | 2,474,169 |
| Jun 22, 2026 | $52.00 | $52.87 | $51.50 | $52.16 | $51.79 | 1,788,072 |
| Jun 23, 2026 | $52.08 | $53.08 | $51.64 | $52.36 | $52.80 | 1,605,887 |
| Jun 24, 2026 | $52.22 | $53.06 | $51.41 | $52.23 | $51.79 | 1,801,490 |
| Jun 25, 2026 | $52.22 | $52.55 | $51.45 | $52.01 | $52.21 | 1,424,041 |
| Jun 26, 2026 | $52.12 | $52.24 | $50.92 | $51.67 | $51.37 | 2,076,186 |
| Jun 29, 2026 | $51.89 | $52.62 | $50.92 | $51.76 | $52.14 | 2,583,008 |
| Jun 30, 2026 | $51.83 | $53.34 | $51.83 | $52.65 | $52.83 | 2,431,114 |
| Jul 1, 2026 | $52.24 | $53.57 | $52.24 | $53.11 | $53.55 | 1,923,734 |
| Jul 2, 2026 | $52.67 | $53.76 | $52.67 | $53.30 | $53.12 | 1,939,689 |
| Jul 3, 2026 | $52.99 | $53.31 | $52.15 | $52.82 | $52.58 | 1,321,837 |
| Jul 6, 2026 | $52.90 | $53.08 | $51.96 | $52.55 | $52.70 | 1,537,122 |
| Jul 7, 2026 | $52.72 | $52.97 | $51.84 | $52.33 | $51.84 | 1,692,987 |
| Jul 8, 2026 | $52.52 | $52.52 | $51.10 | $51.52 | $51.39 | 1,459,186 |
| Jul 9, 2026 | $52.02 | $52.02 | $50.94 | $51.26 | $50.99 | 2,098,214 |
| Jul 10, 2026 | $51.64 | $51.64 | $50.05 | $50.69 | $50.31 | 2,051,435 |
| Jul 13, 2026 | $51.16 | $51.16 | $49.79 | $50.11 | $49.87 | 2,142,840 |
| Jul 14, 2026 | $50.64 | $50.99 | $49.80 | $50.30 | $50.49 | 1,354,889 |
| Jul 15, 2026 | $50.47 | $50.66 | $49.33 | $50.04 | $49.79 | 2,449,316 |
| Jul 16, 2026 | $50.26 | $50.26 | $49.79 | $49.93 | $49.98 | 2,203,616 |
| Jul 17, 2026 | $50.09 | $50.51 | $49.75 | $50.09 | $50.00 | 2,093,556 |
| Jul 20, 2026 | $50.09 | $50.52 | $49.61 | $50.06 | $50.11 | 1,744,654 |
| Jul 21, 2026 | $50.07 | $50.69 | $49.98 | $50.31 | $50.38 | 1,804,094 |
| Jul 22, 2026 | $50.19 | $50.62 | $49.55 | $50.17 | $50.02 | 2,087,542 |
| Jul 23, 2026 | $50.18 | $50.46 | $49.65 | $50.00 | $49.88 | 1,003,365 |
| Jul 24, 2026 | $50.09 | $50.09 | $49.11 | $49.57 | $49.50 | 1,793,220 |
| Jul 27, 2026 | $49.83 | $49.83 | $48.44 | $49.03 | $48.77 | 1,788,971 |
| Jul 28, 2026 | $49.43 | $49.43 | $48.66 | $48.95 | $48.91 | 1,841,254 |
| Jul 29, 2026 | $49.19 | $49.19 | $48.30 | $48.62 | $48.49 | 1,484,087 |
| Jul 30, 2026 | $48.90 | $48.90 | $47.60 | $48.18 | $47.76 | 2,620,849 |
| Jul 31, 2026 | $48.54 | $48.54 | $47.14 | $47.58 | $47.50 | 2,026,847 |
| Aug 3, 2026 | $48.06 | $48.06 | $46.77 | $47.09 | $46.84 | 1,804,095 |
| Aug 4, 2026 | $47.57 | $47.57 | $46.51 | $46.95 | $47.05 | 2,253,140 |
| Aug 5, 2026 | $47.26 | $47.26 | $46.90 | $47.08 | $47.05 | 1,488,410 |
| Aug 6, 2026 | $47.17 | $47.75 | $46.66 | $47.23 | $47.49 | 1,344,832 |
| Aug 7, 2026 | $47.20 | $47.88 | $46.85 | $47.35 | $47.25 | 1,093,394 |
| Aug 10, 2026 | $47.28 | $47.75 | $46.88 | $47.31 | $47.42 | 2,154,132 |
| Aug 11, 2026 | $47.29 | $48.41 | $46.87 | $47.69 | $48.16 | 1,742,791 |
| Aug 12, 2026 | $47.49 | $48.38 | $47.49 | $48.13 | $48.30 | 1,374,057 |
| Aug 13, 2026 | $47.81 | $48.73 | $47.81 | $48.42 | $48.57 | 1,664,161 |
| Aug 14, 2026 | $48.11 | $49.12 | $48.11 | $48.62 | $48.79 | 1,448,772 |
| Aug 17, 2026 | $48.36 | $49.90 | $48.36 | $49.19 | $49.59 | 2,487,141 |
| Aug 18, 2026 | $48.78 | $49.84 | $48.78 | $49.63 | $49.51 | 1,160,989 |
| Aug 19, 2026 | $49.20 | $50.73 | $49.18 | $49.98 | $50.37 | 1,574,061 |
| Aug 20, 2026 | $49.59 | $50.78 | $49.59 | $50.25 | $50.16 | 2,000,778 |
| Aug 21, 2026 | $49.92 | $50.83 | $49.92 | $50.38 | $50.51 | 2,381,206 |
| Aug 24, 2026 | $50.15 | $50.63 | $49.39 | $50.06 | $49.73 | 2,408,059 |
| Aug 25, 2026 | $50.10 | $50.10 | $49.74 | $49.86 | $49.93 | 2,052,349 |
| Aug 26, 2026 | $49.98 | $51.19 | $49.74 | $50.44 | $50.78 | 1,602,548 |
| Aug 27, 2026 | $50.21 | $51.23 | $50.21 | $50.78 | $50.71 | 1,486,508 |
| Aug 28, 2026 | $50.49 | $51.15 | $50.49 | $50.83 | $50.77 | 1,830,991 |
| Aug 31, 2026 | $50.66 | $51.00 | $49.98 | $50.52 | $50.32 | 1,687,029 |
| Sep 1, 2026 | $50.59 | $50.59 | $49.63 | $50.16 | $50.10 | 1,085,271 |
| Time | HA open | HA high | HA low | HA close | Close |
|---|---|---|---|---|---|
| Jul 8, 2026 | $51.54 | $51.90 | $51.10 | $51.52 | $51.39 |
| Jul 9, 2026 | $51.53 | $51.64 | $50.94 | $51.26 | $50.99 |
| Jul 10, 2026 | $51.40 | $51.40 | $50.05 | $50.69 | $50.31 |
| Jul 13, 2026 | $51.04 | $51.04 | $49.79 | $50.11 | $49.87 |
| Jul 14, 2026 | $50.58 | $50.99 | $49.80 | $50.30 | $50.49 |
| Jul 15, 2026 | $50.44 | $50.66 | $49.33 | $50.04 | $49.79 |
| Jul 16, 2026 | $50.24 | $50.24 | $49.79 | $49.93 | $49.98 |
| Jul 17, 2026 | $50.08 | $50.51 | $49.75 | $50.09 | $50.00 |
| Jul 20, 2026 | $50.09 | $50.52 | $49.61 | $50.06 | $50.11 |
| Jul 21, 2026 | $50.07 | $50.69 | $49.98 | $50.31 | $50.38 |
| Jul 22, 2026 | $50.19 | $50.62 | $49.55 | $50.17 | $50.02 |
| Jul 23, 2026 | $50.18 | $50.46 | $49.65 | $50.00 | $49.88 |
| Jul 24, 2026 | $50.09 | $50.09 | $49.11 | $49.57 | $49.50 |
| Jul 27, 2026 | $49.83 | $49.83 | $48.44 | $49.03 | $48.77 |
| Jul 28, 2026 | $49.43 | $49.43 | $48.66 | $48.95 | $48.91 |
| Jul 29, 2026 | $49.19 | $49.19 | $48.30 | $48.62 | $48.49 |
| Jul 30, 2026 | $48.90 | $48.90 | $47.60 | $48.18 | $47.76 |
| Jul 31, 2026 | $48.54 | $48.54 | $47.14 | $47.58 | $47.50 |
| Aug 3, 2026 | $48.06 | $48.06 | $46.77 | $47.09 | $46.84 |
| Aug 4, 2026 | $47.57 | $47.57 | $46.51 | $46.95 | $47.05 |
| Aug 5, 2026 | $47.26 | $47.26 | $46.90 | $47.08 | $47.05 |
| Aug 6, 2026 | $47.17 | $47.75 | $46.66 | $47.23 | $47.49 |
| Aug 7, 2026 | $47.20 | $47.88 | $46.85 | $47.35 | $47.25 |
| Aug 10, 2026 | $47.28 | $47.75 | $46.88 | $47.31 | $47.42 |
| Aug 11, 2026 | $47.29 | $48.41 | $46.87 | $47.69 | $48.16 |
| Aug 12, 2026 | $47.49 | $48.38 | $47.49 | $48.13 | $48.30 |
| Aug 13, 2026 | $47.81 | $48.73 | $47.81 | $48.42 | $48.57 |
| Aug 14, 2026 | $48.11 | $49.12 | $48.11 | $48.62 | $48.79 |
| Aug 17, 2026 | $48.36 | $49.90 | $48.36 | $49.19 | $49.59 |
| Aug 18, 2026 | $48.78 | $49.84 | $48.78 | $49.63 | $49.51 |
| Aug 19, 2026 | $49.20 | $50.73 | $49.18 | $49.98 | $50.37 |
| Aug 20, 2026 | $49.59 | $50.78 | $49.59 | $50.25 | $50.16 |
| Aug 21, 2026 | $49.92 | $50.83 | $49.92 | $50.38 | $50.51 |
| Aug 24, 2026 | $50.15 | $50.63 | $49.39 | $50.06 | $49.73 |
| Aug 25, 2026 | $50.10 | $50.10 | $49.74 | $49.86 | $49.93 |
| Aug 26, 2026 | $49.98 | $51.19 | $49.74 | $50.44 | $50.78 |
| Aug 27, 2026 | $50.21 | $51.23 | $50.21 | $50.78 | $50.71 |
| Aug 28, 2026 | $50.49 | $51.15 | $50.49 | $50.83 | $50.77 |
| Aug 31, 2026 | $50.66 | $51.00 | $49.98 | $50.52 | $50.32 |
| Sep 1, 2026 | $50.59 | $50.59 | $49.63 | $50.16 | $50.10 |
Runs of one colour are trends; hover a candle for its Heikin-Ashi values and the real close.
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/heikin_ashi_chart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/heikin_ashi_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/heikin_ashi_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 heikin_ashi_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 heikin_ashi_chart_demo_daily = heikin_ashi_chart_demo_walk(23, 100, 48.6, "2026-04-15")view HeikinAshiChartDemo() = <div.demo-stack> <div id="heikin_ashi_chart-demo-daily"> <HeikinAshiChart label="Globex, Heikin-Ashi" show_label={true} candles={heikin_ashi_chart_demo_daily} prefix="$"/> </div> <div id="heikin_ashi_chart-demo-small"> <HeikinAshiChart label="Globex, last 40 days, with EMA 10" show_label={true} candles={drop(heikin_ashi_chart_demo_daily, 60)} prefix="$" ema={[10]} volume={false} height={200}/> </div> <p.demo-note>Runs of one colour are trends; hover a candle for its Heikin-Ashi values and the real close.</p> </div>Source
ui/heikin_ashi_chart.qwe
// HeikinAshiChart: Heikin-Ashi candles, worked out from OHLC candles: each one's close// is the average of the period's open, high, low and close, and its open the middle of// the candle before, so runs of one colour show a trend and its noise is smoothed// away. With a volume pane, a date axis, moving averages and the last close's line.// CSS only: hovering a candle shows its Heikin-Ashi open, high, low and close beside// the real close and change, and screen readers get the numbers as a table.//// <HeikinAshiChart label="ACME, Heikin-Ashi" candles={bars}/>// <HeikinAshiChart label="ETH-USD" candles={hourly} volume={false} decimals={1} ema={[21]}/>//// candles: the real 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. Heikin-Ashi: close = (open + high + low +// close) / 4; open = (the last one's open + close) / 2, the first one's (open + close)// / 2; high and low the highest and lowest of the real high or low and these two.// sma and ema: periods of simple and exponential moving averages of the real close,// each a line of its own colour, named in a legend. volume: the volume pane (true);// volume_height: its height in pixels (64). height: the price pane's (300).// last_line: a dashed line at the real 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 heikin_ashi_chart_css = css`.ui-ha { --ui-ha-1: light-dark(#2a78d6, #3987e5); --ui-ha-2: light-dark(#eb6834, #d95926); --ui-ha-3: light-dark(#1baf7a, #199e70); --ui-ha-4: light-dark(#eda100, #c98500); --ui-ha-5: light-dark(#e87ba4, #d55181); --ui-ha-6: light-dark(#008300, #008300); --ui-ha-7: light-dark(#6250d6, #9085e9); --ui-ha-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-ha-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-ha-s1 { --c: var(--ui-ha-1); } .ui-ha-s2 { --c: var(--ui-ha-2); } .ui-ha-s3 { --c: var(--ui-ha-3); }.ui-ha-s4 { --c: var(--ui-ha-4); } .ui-ha-s5 { --c: var(--ui-ha-5); } .ui-ha-s6 { --c: var(--ui-ha-6); }.ui-ha-s7 { --c: var(--ui-ha-7); } .ui-ha-s8 { --c: var(--ui-ha-8); }.ui-ha-up { --k: var(--ui-success); } .ui-ha-down { --k: var(--ui-danger); }.ui-ha-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-ha-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-ha-key { width: 14px; height: 3px; border-radius: 2px; background: var(--c); flex: none; }.ui-ha-frame { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; padding-top: 8px; }.ui-ha-plot { position: relative; }.ui-ha-pane, .ui-ha-cp { position: relative; height: var(--h); }.ui-ha-vpane, .ui-ha-cv { position: relative; height: var(--vh); margin-top: 16px; }.ui-ha-vpane { border-top: 1px solid color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-ha-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-ha-svg { position: absolute; z-index: 2; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-ha-ma { fill: none; stroke: var(--c); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }.ui-ha-last { position: absolute; z-index: 1; left: 0; right: 0; border-top: 1px dashed var(--k); opacity: .75; pointer-events: none; }.ui-ha-cols { position: absolute; inset: 0; display: flex; }.ui-ha-col { position: relative; flex: 1; min-width: 0; }.ui-ha-col::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: var(--ui-strong); opacity: 0; }.ui-ha-col:hover { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-ha-col:hover::before { opacity: .8; }.ui-ha-wick { position: absolute; left: 50%; width: 1px; margin-left: -.5px; background: var(--k); }.ui-ha-body { position: absolute; left: 16%; right: 16%; min-height: 1px; border-radius: 1px; background: var(--k); }.ui-ha-vol { position: absolute; bottom: 0; left: 16%; right: 16%; min-height: 1px; border-radius: 1px 1px 0 0; background: var(--k); opacity: .35; }.ui-ha-col:hover .ui-ha-vol { opacity: .7; }.ui-ha-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-ha-flip .ui-ha-tip { left: auto; right: calc(50% + 10px); }.ui-ha-col:hover .ui-ha-tip { display: block; }.ui-ha-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-ha-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-ha-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-ha-tip .ui-ha-chg b { color: var(--k); }.ui-ha-y { position: relative; }.ui-ha-y > div { position: relative; }.ui-ha-y span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-ha-y .ui-ha-tag { z-index: 1; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--k); color: var(--ui-bg); font-weight: 600; }.ui-ha-x { position: relative; height: 16px; margin-top: 6px; }.ui-ha-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-ha-x .ui-ha-first { transform: none; }.ui-ha-x .ui-ha-end { transform: translateX(-100%); }`// n (a whole number, 0 or more) with at least `width` digits.fn heikin_ashi_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 heikin_ashi_chart_group(n: int) -> str = n < 1000 ? str(n) : heikin_ashi_chart_group(floor(n / 1000)) + "," + heikin_ashi_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 heikin_ashi_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)}` : `.${heikin_ashi_chart_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${heikin_ashi_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn heikin_ashi_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 ? heikin_ashi_chart_num(v) : `${v < 0 ? "-" : ""}${heikin_ashi_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn heikin_ashi_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${heikin_ashi_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn heikin_ashi_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 heikin_ashi_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 heikin_ashi_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 heikin_ashi_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 heikin_ashi_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 heikin_ashi_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}// Heikin-Ashi candles from real ones, each keeping the real close as `real`.fn heikin_ashi_chart_candles(candles: list) -> list { let out = [] for c in candles { let close = (c.open + c.high + c.low + c.close) / 4 let open = len(out) == 0 ? (c.open + c.close) / 2 : (out[-1].open + out[-1].close) / 2 out = out | push({ time: c.time, open, high: max([c.high, open, close]), low: min([c.low, open, close]), close, volume: c.volume, real: c.close }) } out}view HeikinAshiChart(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 = "Heikin-Ashi chart", show_label: bool = false, class: str? = null) { style global heikin_ashi_chart_css let bars = heikin_ashi_chart_candles(candles) let n = len(bars) let closes = bars | map((c) => c.real) let lines = (sma | map((p) => { name: `SMA ${p}`, values: heikin_ashi_chart_sma(closes, p) })) + (ema | map((p) => { name: `EMA ${p}`, values: heikin_ashi_chart_ema(closes, p) })) let hues = [1, 2, 7, 4, 5, 3, 6, 8] let known = flatten(bars | map((c) => [c.low, c.high])) + flatten(lines | map((l) => l.values | filter((v) => v != null))) let sc = heikin_ashi_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) => heikin_ashi_chart_pct(v) let vols = bars | map((c) => c.volume ?? 0) let vtop = max([0] + vols) let price = (v) => v == null ? "—" : `${v < 0 ? "-" : ""}${prefix}${heikin_ashi_chart_num(abs(v), decimals)}${suffix}` let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${heikin_ashi_chart_num(abs(t))}` let change = (i) => { let before = i > 0 ? bars[i - 1].real : candles[i].open before != 0 ? (bars[i].real - before) * 100 / before : 0 } let side = (up) => up ? "ui-ha-up" : "ui-ha-down" let last = n > 0 ? bars[-1] : null let axis_width = max([4] + (sc.ticks | map((t) => len(tick(t)))) + [last != null ? len(price(last.real)) + 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-ha", class])} aria-label={label} style={`--h: ${height}px; --vh: ${volume ? volume_height : 0}px`}> {show_label && <figcaption.ui-ha-title>{label}</figcaption>} {len(lines) > 0 && <ul.ui-ha-legend aria-hidden="true"> {for [j, l] in enumerate(lines) => <li><span class={`ui-ha-key ui-ha-s${hues[j % 8]}`}></span>{l.name}</li>} </ul>} <div.ui-ha-frame aria-hidden="true"> <div.ui-ha-plot> <div.ui-ha-pane> {for t in sc.ticks => <span.ui-ha-grid style={`top: ${pct(y(t))}%`}></span>} {last_line && last != null && <span class={`ui-ha-last ${side(change(n - 1) >= 0)}`} style={`top: ${pct(y(last.real))}%`}></span>} {len(lines) > 0 && <svg.ui-ha-svg viewBox="0 0 100 100" preserveAspectRatio="none"> {for [j, l] in enumerate(lines) => <path class={`ui-ha-ma ui-ha-s${hues[j % 8]}`} d={line_path(l.values)} vector-effect="non-scaling-stroke"/>} </svg>} </div> {volume && <div.ui-ha-vpane></div>} <div.ui-ha-cols> {for [i, c] in enumerate(bars) => <div class={ui_cls(["ui-ha-col", side(c.close >= c.open), n > 1 && i >= n / 2 && "ui-ha-flip"])}> <div.ui-ha-cp> <span.ui-ha-wick style={`top: ${pct(y(c.high))}%; height: ${pct(y(c.low) - y(c.high))}%`}></span> <span.ui-ha-body style={`top: ${pct(y(max([c.open, c.close])))}%; height: ${pct(abs(y(c.open) - y(c.close)))}%`}></span> </div> {volume && <div.ui-ha-cv> <span.ui-ha-vol style={`height: ${vtop > 0 ? pct(vols[i] * 100 / vtop) : 0}%`}></span> </div>} <span.ui-ha-tip> <strong>{heikin_ashi_chart_when(c.time)}</strong> <p>HA open<b>{price(c.open)}</b></p> <p>HA high<b>{price(c.high)}</b></p> <p>HA low<b>{price(c.low)}</b></p> <p>HA close<b>{price(c.close)}</b></p> <p>Close<b>{price(c.real)}</b></p> <p class={`ui-ha-chg ${side(change(i) >= 0)}`}>Change<b>{heikin_ashi_chart_signed(change(i))}</b></p> {volume && <p>Volume<b>{heikin_ashi_chart_num(vols[i], 0)}</b></p>} {for [j, l] in enumerate(lines) => <p> <span class={`ui-ha-key ui-ha-s${hues[j % 8]}`}></span>{l.name}<b>{price(l.values[i])}</b> </p>} </span> </div>} </div> </div> <div.ui-ha-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.real)) * height < 1400) && <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>} {last_line && last != null && <span class={`ui-ha-tag ${side(change(n - 1) >= 0)}`} style={`top: ${pct(y(last.real))}%`}>{price(last.real)}</span>} </div> {volume && <div style="height: var(--vh); margin-top: 16px"> {vtop > 0 && <span style="top: 2px; transform: none">{heikin_ashi_chart_short(vtop)}</span>} </div>} </div> <div.ui-ha-x> {for t in heikin_ashi_chart_ticks(bars | map((c) => c.time), 7) => <span class={ui_cls([x(t.i) < 4 && "ui-ha-first", x(t.i) > 96 && "ui-ha-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">HA open</th><th scope="col">HA high</th> <th scope="col">HA low</th><th scope="col">HA close</th><th scope="col">Close</th> {volume && <th scope="col">Volume</th>} </tr></thead> <tbody> {for [i, c] in enumerate(bars) => <tr> <th scope="row">{heikin_ashi_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><td>{price(c.real)}</td> {volume && <td>{heikin_ashi_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 downloadsCandlestickChartOHLC candles, green up and red down, with a volume pane, a date axis, moving averages and the details of each candle on hover.5 views · 7 downloadsDepthChartAn order book's bids and asks as stepped areas of the size waiting at each price or better, around the mid price, with the spread.5 views · 7 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 HeikinAshiChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.