Charts

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.

#chart#data#trading#no-js

Demo

Preview
Globex, Heikin-Ashi
Globex, Heikin-Ashi
TimeHA openHA highHA lowHA closeCloseVolume
Apr 15, 2026$48.06$48.63$47.41$48.04$47.671,677,098
Apr 16, 2026$48.05$48.05$46.58$47.21$46.871,952,736
Apr 17, 2026$47.63$47.77$46.59$47.26$47.721,646,951
Apr 20, 2026$47.44$47.71$47.16$47.51$47.551,670,801
Apr 21, 2026$47.48$48.36$47.39$47.86$48.101,818,564
Apr 22, 2026$47.67$48.01$47.10$47.62$47.371,420,791
Apr 23, 2026$47.64$47.64$47.45$47.53$47.572,200,826
Apr 24, 2026$47.58$47.88$47.20$47.59$47.691,506,187
Apr 27, 2026$47.59$48.23$47.58$47.87$47.932,337,109
Apr 28, 2026$47.73$48.22$47.73$48.02$48.05989,966
Apr 29, 2026$47.88$49.17$47.88$48.53$48.941,732,408
Apr 30, 2026$48.20$49.36$48.09$48.76$48.531,711,314
May 1, 2026$48.48$49.25$48.46$48.79$48.821,946,303
May 4, 2026$48.63$48.82$47.78$48.39$48.162,183,082
May 5, 2026$48.51$48.93$48.15$48.50$48.751,479,429
May 6, 2026$48.50$49.14$48.50$48.89$49.072,014,236
May 7, 2026$48.70$49.79$48.52$49.21$49.612,584,303
May 8, 2026$48.95$50.72$48.95$49.95$50.381,914,764
May 11, 2026$49.45$50.38$49.17$49.86$49.572,117,068
May 12, 2026$49.66$49.74$49.45$49.58$49.551,653,451
May 13, 2026$49.62$50.29$49.46$49.81$49.991,884,771
May 14, 2026$49.71$50.26$49.68$49.91$49.831,554,873
May 15, 2026$49.81$50.35$49.25$49.77$49.491,660,926
May 18, 2026$49.79$50.22$49.23$49.67$49.782,429,948
May 19, 2026$49.73$49.73$49.11$49.46$49.371,892,181
May 20, 2026$49.59$49.73$49.24$49.44$49.471,391,719
May 21, 2026$49.52$50.56$49.11$49.86$50.332,565,590
May 22, 2026$49.69$50.68$49.69$50.46$50.481,515,826
May 25, 2026$50.07$51.78$50.07$50.99$51.372,196,772
May 26, 2026$50.53$51.85$50.53$51.17$50.941,428,739
May 27, 2026$50.85$51.45$50.01$50.69$50.311,722,773
May 28, 2026$50.77$51.56$50.15$50.77$51.061,943,369
May 29, 2026$50.77$52.47$50.77$51.59$51.972,229,993
Jun 1, 2026$51.18$52.77$51.18$52.27$52.631,796,330
Jun 2, 2026$51.72$52.54$51.72$52.30$52.271,864,565
Jun 3, 2026$52.01$52.91$52.01$52.56$52.851,852,567
Jun 4, 2026$52.28$53.02$52.28$52.65$52.552,310,460
Jun 5, 2026$52.47$53.44$52.29$52.85$52.962,118,054
Jun 8, 2026$52.66$53.78$52.64$53.21$53.351,561,395
Jun 9, 2026$52.93$54.43$52.93$53.82$54.132,581,081
Jun 10, 2026$53.38$54.62$53.10$53.89$53.611,995,529
Jun 11, 2026$53.63$54.11$53.23$53.61$53.431,721,313
Jun 12, 2026$53.62$53.62$52.34$52.96$52.752,544,294
Jun 15, 2026$53.29$53.29$52.58$52.70$52.611,559,372
Jun 16, 2026$53.00$53.00$51.56$52.22$51.821,836,964
Jun 17, 2026$52.61$52.61$51.21$51.64$51.511,131,506
Jun 18, 2026$52.12$52.12$51.51$51.66$51.611,398,973
Jun 19, 2026$51.89$52.68$51.55$52.12$52.542,474,169
Jun 22, 2026$52.00$52.87$51.50$52.16$51.791,788,072
Jun 23, 2026$52.08$53.08$51.64$52.36$52.801,605,887
Jun 24, 2026$52.22$53.06$51.41$52.23$51.791,801,490
Jun 25, 2026$52.22$52.55$51.45$52.01$52.211,424,041
Jun 26, 2026$52.12$52.24$50.92$51.67$51.372,076,186
Jun 29, 2026$51.89$52.62$50.92$51.76$52.142,583,008
Jun 30, 2026$51.83$53.34$51.83$52.65$52.832,431,114
Jul 1, 2026$52.24$53.57$52.24$53.11$53.551,923,734
Jul 2, 2026$52.67$53.76$52.67$53.30$53.121,939,689
Jul 3, 2026$52.99$53.31$52.15$52.82$52.581,321,837
Jul 6, 2026$52.90$53.08$51.96$52.55$52.701,537,122
Jul 7, 2026$52.72$52.97$51.84$52.33$51.841,692,987
Jul 8, 2026$52.52$52.52$51.10$51.52$51.391,459,186
Jul 9, 2026$52.02$52.02$50.94$51.26$50.992,098,214
Jul 10, 2026$51.64$51.64$50.05$50.69$50.312,051,435
Jul 13, 2026$51.16$51.16$49.79$50.11$49.872,142,840
Jul 14, 2026$50.64$50.99$49.80$50.30$50.491,354,889
Jul 15, 2026$50.47$50.66$49.33$50.04$49.792,449,316
Jul 16, 2026$50.26$50.26$49.79$49.93$49.982,203,616
Jul 17, 2026$50.09$50.51$49.75$50.09$50.002,093,556
Jul 20, 2026$50.09$50.52$49.61$50.06$50.111,744,654
Jul 21, 2026$50.07$50.69$49.98$50.31$50.381,804,094
Jul 22, 2026$50.19$50.62$49.55$50.17$50.022,087,542
Jul 23, 2026$50.18$50.46$49.65$50.00$49.881,003,365
Jul 24, 2026$50.09$50.09$49.11$49.57$49.501,793,220
Jul 27, 2026$49.83$49.83$48.44$49.03$48.771,788,971
Jul 28, 2026$49.43$49.43$48.66$48.95$48.911,841,254
Jul 29, 2026$49.19$49.19$48.30$48.62$48.491,484,087
Jul 30, 2026$48.90$48.90$47.60$48.18$47.762,620,849
Jul 31, 2026$48.54$48.54$47.14$47.58$47.502,026,847
Aug 3, 2026$48.06$48.06$46.77$47.09$46.841,804,095
Aug 4, 2026$47.57$47.57$46.51$46.95$47.052,253,140
Aug 5, 2026$47.26$47.26$46.90$47.08$47.051,488,410
Aug 6, 2026$47.17$47.75$46.66$47.23$47.491,344,832
Aug 7, 2026$47.20$47.88$46.85$47.35$47.251,093,394
Aug 10, 2026$47.28$47.75$46.88$47.31$47.422,154,132
Aug 11, 2026$47.29$48.41$46.87$47.69$48.161,742,791
Aug 12, 2026$47.49$48.38$47.49$48.13$48.301,374,057
Aug 13, 2026$47.81$48.73$47.81$48.42$48.571,664,161
Aug 14, 2026$48.11$49.12$48.11$48.62$48.791,448,772
Aug 17, 2026$48.36$49.90$48.36$49.19$49.592,487,141
Aug 18, 2026$48.78$49.84$48.78$49.63$49.511,160,989
Aug 19, 2026$49.20$50.73$49.18$49.98$50.371,574,061
Aug 20, 2026$49.59$50.78$49.59$50.25$50.162,000,778
Aug 21, 2026$49.92$50.83$49.92$50.38$50.512,381,206
Aug 24, 2026$50.15$50.63$49.39$50.06$49.732,408,059
Aug 25, 2026$50.10$50.10$49.74$49.86$49.932,052,349
Aug 26, 2026$49.98$51.19$49.74$50.44$50.781,602,548
Aug 27, 2026$50.21$51.23$50.21$50.78$50.711,486,508
Aug 28, 2026$50.49$51.15$50.49$50.83$50.771,830,991
Aug 31, 2026$50.66$51.00$49.98$50.52$50.321,687,029
Sep 1, 2026$50.59$50.59$49.63$50.16$50.101,085,271
Globex, last 40 days, with EMA 10
Globex, last 40 days, with EMA 10
TimeHA openHA highHA lowHA closeClose
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.qwe
main.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>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.