StockChart
A full trading chart: candles, line or area, volume, range buttons, a crosshair with the OHLC readout, wheel zoom and drag pan, indicators and RSI and MACD panes.
Demo
ACME · hourly
$203.42-0.93%past 3M · Jun 15, 10:00 – Sep 11, 15:00Drag to pan and use the wheel to zoom; focused, ← → move the crosshair, + − zoom, Esc lets go.
| Time | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| Jun 15, 10:00 | $205.33 | $207.41 | $203.75 | $206.00 | 389,170 |
| Jun 15, 13:00 | $205.43 | $206.98 | $202.64 | $205.43 | 542,563 |
| Jun 15, 16:00 | $205.81 | $208.04 | $204.42 | $205.69 | 411,831 |
| Jun 16, 12:00 | $205.31 | $207.84 | $203.70 | $205.30 | 482,152 |
| Jun 16, 15:00 | $204.83 | $208.02 | $203.87 | $206.82 | 459,752 |
| Jun 17, 11:00 | $207.18 | $209.02 | $204.99 | $207.47 | 474,206 |
| Jun 17, 14:00 | $207.94 | $212.68 | $206.38 | $210.74 | 447,077 |
| Jun 18, 10:00 | $210.13 | $212.77 | $209.48 | $210.39 | 376,689 |
| Jun 18, 13:00 | $210.17 | $212.43 | $206.94 | $208.86 | 527,841 |
| Jun 18, 16:00 | $208.34 | $209.58 | $204.69 | $205.52 | 438,369 |
| Jun 19, 12:00 | $205.10 | $207.78 | $203.56 | $206.89 | 397,654 |
| Jun 19, 15:00 | $206.37 | $208.35 | $203.94 | $207.48 | 500,623 |
| Jun 22, 11:00 | $207.26 | $209.65 | $204.68 | $205.83 | 547,211 |
| Jun 22, 14:00 | $206.17 | $210.10 | $204.71 | $208.64 | 438,295 |
| Jun 23, 10:00 | $208.42 | $211.57 | $206.74 | $209.33 | 455,098 |
| Jun 23, 13:00 | $208.85 | $209.06 | $205.18 | $207.02 | 625,623 |
| Jun 23, 16:00 | $206.97 | $210.06 | $205.36 | $208.86 | 435,827 |
| Jun 24, 12:00 | $208.96 | $210.74 | $207.08 | $209.82 | 365,080 |
| Jun 24, 15:00 | $210.00 | $210.67 | $207.84 | $208.15 | 477,506 |
| Jun 25, 11:00 | $208.01 | $210.75 | $206.53 | $210.31 | 439,908 |
| Jun 25, 14:00 | $210.21 | $211.84 | $208.85 | $210.63 | 437,410 |
| Jun 26, 10:00 | $210.66 | $213.74 | $208.73 | $211.93 | 609,588 |
| Jun 26, 13:00 | $212.20 | $213.04 | $209.21 | $210.43 | 517,918 |
| Jun 26, 16:00 | $210.53 | $212.47 | $209.29 | $210.48 | 397,312 |
| Jun 29, 12:00 | $210.10 | $211.73 | $207.67 | $209.42 | 478,922 |
| Jun 29, 15:00 | $209.62 | $210.75 | $207.00 | $208.25 | 533,441 |
| Jun 30, 11:00 | $208.73 | $212.01 | $207.80 | $210.97 | 542,837 |
| Jun 30, 14:00 | $211.07 | $212.81 | $208.12 | $210.20 | 501,256 |
| Jul 1, 10:00 | $210.60 | $211.49 | $207.74 | $209.87 | 554,308 |
| Jul 1, 13:00 | $209.62 | $211.22 | $208.68 | $208.73 | 427,385 |
| Jul 1, 16:00 | $208.36 | $208.75 | $205.24 | $207.40 | 446,861 |
| Jul 2, 12:00 | $207.53 | $208.67 | $203.92 | $205.93 | 403,876 |
| Jul 2, 15:00 | $206.28 | $207.95 | $204.66 | $206.78 | 570,426 |
| Jul 3, 11:00 | $206.26 | $208.44 | $205.05 | $205.93 | 631,830 |
| Jul 3, 14:00 | $205.52 | $208.62 | $204.02 | $207.64 | 544,426 |
| Jul 6, 10:00 | $207.47 | $209.87 | $205.28 | $208.74 | 466,994 |
| Jul 6, 13:00 | $208.23 | $210.36 | $205.77 | $208.44 | 487,248 |
| Jul 6, 16:00 | $208.97 | $211.43 | $206.45 | $209.54 | 552,631 |
| Jul 7, 12:00 | $208.92 | $210.71 | $206.15 | $207.79 | 383,055 |
| Jul 7, 15:00 | $207.28 | $208.55 | $205.22 | $207.04 | 459,026 |
| Jul 8, 11:00 | $207.26 | $209.72 | $205.03 | $208.04 | 466,939 |
| Jul 8, 14:00 | $208.19 | $209.06 | $205.52 | $207.19 | 514,876 |
| Jul 9, 10:00 | $206.68 | $210.70 | $205.12 | $210.00 | 521,216 |
| Jul 9, 13:00 | $210.27 | $214.04 | $208.67 | $212.64 | 577,917 |
| Jul 9, 16:00 | $212.57 | $213.60 | $209.23 | $210.08 | 436,484 |
| Jul 10, 12:00 | $209.93 | $211.29 | $207.35 | $209.81 | 451,136 |
| Jul 10, 15:00 | $210.12 | $211.22 | $206.82 | $208.39 | 508,731 |
| Jul 13, 11:00 | $207.83 | $210.54 | $205.93 | $209.46 | 523,771 |
| Jul 13, 14:00 | $209.77 | $211.52 | $207.33 | $209.54 | 429,506 |
| Jul 14, 10:00 | $209.34 | $210.58 | $205.62 | $207.32 | 423,789 |
| Jul 14, 13:00 | $207.67 | $209.71 | $206.05 | $206.84 | 395,729 |
| Jul 14, 16:00 | $207.01 | $208.66 | $205.63 | $205.99 | 447,116 |
| Jul 15, 12:00 | $205.51 | $208.08 | $204.62 | $207.06 | 404,341 |
| Jul 15, 15:00 | $207.01 | $208.28 | $205.23 | $206.97 | 428,359 |
| Jul 16, 11:00 | $207.29 | $209.00 | $205.02 | $206.95 | 457,467 |
| Jul 16, 14:00 | $207.10 | $208.61 | $206.01 | $207.70 | 384,284 |
| Jul 17, 10:00 | $207.67 | $210.10 | $206.86 | $208.96 | 414,363 |
| Jul 17, 13:00 | $208.37 | $209.19 | $206.20 | $208.72 | 414,992 |
| Jul 17, 16:00 | $209.03 | $210.18 | $207.39 | $209.58 | 480,170 |
| Jul 20, 12:00 | $209.46 | $211.26 | $206.36 | $208.35 | 377,837 |
| Jul 20, 15:00 | $208.92 | $211.42 | $207.04 | $208.49 | 359,569 |
| Jul 21, 11:00 | $208.47 | $209.98 | $205.14 | $207.07 | 496,584 |
| Jul 21, 14:00 | $207.66 | $208.38 | $204.74 | $206.67 | 650,102 |
| Jul 22, 10:00 | $206.06 | $208.52 | $205.38 | $206.74 | 411,684 |
| Jul 22, 13:00 | $206.75 | $209.30 | $204.83 | $205.98 | 414,123 |
| Jul 22, 16:00 | $205.39 | $207.52 | $202.95 | $204.45 | 579,112 |
| Jul 23, 12:00 | $204.04 | $205.96 | $201.39 | $203.70 | 404,129 |
| Jul 23, 15:00 | $203.89 | $205.92 | $202.49 | $204.66 | 477,758 |
| Jul 24, 11:00 | $205.17 | $207.04 | $203.43 | $205.63 | 544,013 |
| Jul 24, 14:00 | $205.46 | $208.55 | $203.70 | $205.61 | 436,764 |
| Jul 27, 10:00 | $205.41 | $207.93 | $203.71 | $205.67 | 503,989 |
| Jul 27, 13:00 | $205.81 | $207.15 | $203.19 | $204.80 | 367,338 |
| Jul 27, 16:00 | $204.51 | $204.97 | $201.68 | $202.88 | 433,370 |
| Jul 28, 12:00 | $202.53 | $203.15 | $199.60 | $201.12 | 531,550 |
| Jul 28, 15:00 | $201.56 | $204.69 | $201.00 | $203.95 | 550,025 |
| Jul 29, 11:00 | $204.09 | $205.57 | $202.62 | $203.65 | 451,265 |
| Jul 29, 14:00 | $204.11 | $207.39 | $203.04 | $203.81 | 520,764 |
| Jul 30, 10:00 | $203.33 | $203.87 | $201.00 | $202.71 | 429,598 |
| Jul 30, 13:00 | $202.19 | $205.18 | $200.51 | $202.87 | 549,967 |
| Jul 30, 16:00 | $202.54 | $202.61 | $198.82 | $200.11 | 380,319 |
| Jul 31, 12:00 | $200.52 | $202.77 | $198.80 | $200.23 | 455,050 |
| Jul 31, 15:00 | $200.40 | $203.32 | $198.81 | $201.10 | 392,447 |
| Aug 3, 11:00 | $200.75 | $203.31 | $198.91 | $201.20 | 455,064 |
| Aug 3, 14:00 | $201.43 | $203.31 | $199.97 | $201.21 | 484,685 |
| Aug 4, 10:00 | $200.66 | $202.04 | $198.37 | $200.47 | 510,445 |
| Aug 4, 13:00 | $200.00 | $201.76 | $198.63 | $199.88 | 394,390 |
| Aug 4, 16:00 | $200.39 | $201.48 | $197.53 | $199.38 | 393,072 |
| Aug 5, 12:00 | $199.71 | $203.57 | $197.79 | $201.76 | 505,546 |
| Aug 5, 15:00 | $201.67 | $203.99 | $200.07 | $203.12 | 502,302 |
| Aug 6, 11:00 | $203.51 | $204.41 | $200.01 | $201.97 | 606,516 |
| Aug 6, 14:00 | $202.02 | $202.77 | $199.83 | $201.56 | 650,543 |
| Aug 7, 10:00 | $200.96 | $203.83 | $199.55 | $200.23 | 491,647 |
| Aug 7, 13:00 | $200.42 | $201.28 | $198.64 | $199.27 | 501,993 |
| Aug 7, 16:00 | $199.71 | $201.38 | $198.14 | $199.89 | 611,813 |
| Aug 10, 12:00 | $199.84 | $201.92 | $198.18 | $198.80 | 592,060 |
| Aug 10, 15:00 | $198.42 | $199.23 | $196.09 | $198.70 | 437,100 |
| Aug 11, 11:00 | $199.07 | $200.69 | $197.01 | $198.80 | 486,889 |
| Aug 11, 14:00 | $199.07 | $200.09 | $195.04 | $196.13 | 479,915 |
| Aug 12, 10:00 | $195.78 | $197.56 | $193.10 | $194.48 | 458,877 |
| Aug 12, 13:00 | $194.95 | $196.97 | $194.14 | $195.21 | 464,605 |
| Aug 12, 16:00 | $195.70 | $198.18 | $194.51 | $195.66 | 552,940 |
| Aug 13, 12:00 | $195.42 | $196.38 | $192.24 | $195.26 | 485,423 |
| Aug 13, 15:00 | $195.56 | $197.43 | $193.89 | $195.54 | 518,947 |
| Aug 14, 11:00 | $196.11 | $196.13 | $192.00 | $193.73 | 618,150 |
| Aug 14, 14:00 | $193.63 | $194.49 | $190.36 | $193.11 | 526,194 |
| Aug 17, 10:00 | $193.35 | $195.77 | $191.71 | $193.43 | 478,544 |
| Aug 17, 13:00 | $193.60 | $194.98 | $192.22 | $193.26 | 502,313 |
| Aug 17, 16:00 | $193.71 | $196.48 | $192.48 | $194.72 | 473,482 |
| Aug 18, 12:00 | $194.36 | $198.13 | $192.97 | $196.48 | 502,733 |
| Aug 18, 15:00 | $196.56 | $198.62 | $195.82 | $196.49 | 483,257 |
| Aug 19, 11:00 | $196.39 | $198.53 | $195.23 | $198.14 | 607,092 |
| Aug 19, 14:00 | $198.52 | $201.13 | $197.05 | $198.16 | 460,676 |
| Aug 20, 10:00 | $197.60 | $199.19 | $195.94 | $198.27 | 552,396 |
| Aug 20, 13:00 | $198.33 | $199.91 | $194.40 | $195.34 | 473,418 |
| Aug 20, 16:00 | $195.19 | $196.81 | $193.53 | $195.39 | 517,623 |
| Aug 21, 12:00 | $195.32 | $197.18 | $193.36 | $194.36 | 485,337 |
| Aug 21, 15:00 | $193.89 | $195.46 | $193.10 | $193.97 | 585,465 |
| Aug 24, 11:00 | $194.39 | $194.69 | $192.04 | $193.37 | 438,174 |
| Aug 24, 14:00 | $193.68 | $196.03 | $192.61 | $193.99 | 557,874 |
| Aug 25, 10:00 | $193.77 | $196.83 | $191.98 | $193.98 | 605,190 |
| Aug 25, 13:00 | $194.54 | $197.51 | $192.71 | $195.05 | 370,976 |
| Aug 25, 16:00 | $195.00 | $196.10 | $192.31 | $194.86 | 493,266 |
| Aug 26, 12:00 | $195.00 | $195.40 | $190.51 | $192.25 | 522,897 |
| Aug 26, 15:00 | $192.33 | $195.42 | $192.11 | $194.60 | 460,592 |
| Aug 27, 11:00 | $194.66 | $198.63 | $193.19 | $197.37 | 576,259 |
| Aug 27, 14:00 | $197.79 | $199.60 | $194.87 | $198.69 | 489,414 |
| Aug 28, 10:00 | $199.10 | $201.01 | $196.65 | $199.83 | 543,538 |
| Aug 28, 13:00 | $199.48 | $201.05 | $198.17 | $199.29 | 405,072 |
| Aug 28, 16:00 | $199.76 | $200.27 | $195.46 | $197.14 | 436,355 |
| Aug 31, 12:00 | $197.45 | $201.61 | $195.78 | $200.05 | 425,909 |
| Aug 31, 15:00 | $200.33 | $202.22 | $197.79 | $199.22 | 442,773 |
| Sep 1, 11:00 | $199.32 | $201.26 | $198.18 | $200.88 | 486,233 |
| Sep 1, 14:00 | $201.32 | $203.50 | $198.36 | $199.87 | 450,486 |
| Sep 2, 10:00 | $199.68 | $202.29 | $197.73 | $200.22 | 513,599 |
| Sep 2, 13:00 | $199.68 | $201.95 | $199.12 | $201.08 | 484,469 |
| Sep 2, 16:00 | $201.16 | $203.99 | $200.29 | $202.22 | 490,762 |
| Sep 3, 12:00 | $202.03 | $205.73 | $201.04 | $203.75 | 485,690 |
| Sep 3, 15:00 | $204.31 | $205.46 | $200.76 | $202.08 | 499,932 |
| Sep 4, 11:00 | $201.65 | $204.23 | $200.29 | $202.18 | 573,302 |
| Sep 4, 14:00 | $201.94 | $205.62 | $201.12 | $204.34 | 508,336 |
| Sep 7, 10:00 | $204.36 | $206.39 | $202.77 | $203.32 | 416,630 |
| Sep 7, 13:00 | $203.10 | $204.51 | $201.23 | $203.75 | 413,519 |
| Sep 7, 16:00 | $203.35 | $206.07 | $201.80 | $203.82 | 495,846 |
| Sep 8, 12:00 | $203.56 | $206.87 | $201.67 | $205.42 | 444,656 |
| Sep 8, 15:00 | $205.34 | $208.24 | $203.84 | $206.30 | 456,896 |
| Sep 9, 11:00 | $206.75 | $209.89 | $205.13 | $208.22 | 391,923 |
| Sep 9, 14:00 | $208.58 | $209.89 | $205.80 | $206.87 | 444,345 |
| Sep 10, 10:00 | $206.61 | $206.95 | $203.50 | $205.02 | 545,919 |
| Sep 10, 13:00 | $204.76 | $206.17 | $202.93 | $205.09 | 511,708 |
| Sep 10, 16:00 | $205.12 | $206.79 | $200.40 | $202.99 | 511,855 |
| Sep 11, 12:00 | $203.45 | $205.25 | $201.76 | $202.81 | 413,866 |
| Sep 11, 15:00 | $202.77 | $204.14 | $200.60 | $203.42 | 322,990 |
Showing 3M.
Globex · daily, line with RSI and MACD
$56.20+16.94%in view · Oct 1, 2025 – Sep 28, 2026RSI 14 39.5
MACD 12 26 9 -0.6467
Drag to pan and use the wheel to zoom; focused, ← → move the crosshair, + − zoom, Esc lets go.
| Time | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| Oct 1, 2025 | $48.06 | $48.73 | $47.62 | $47.81 | 4,003,400 |
| Oct 3, 2025 | $47.93 | $48.19 | $47.50 | $47.94 | 2,842,421 |
| Oct 7, 2025 | $47.82 | $48.27 | $47.42 | $47.73 | 2,868,087 |
| Oct 9, 2025 | $47.74 | $48.70 | $47.32 | $47.78 | 4,441,731 |
| Oct 13, 2025 | $47.70 | $48.71 | $47.33 | $47.82 | 3,102,422 |
| Oct 15, 2025 | $47.92 | $48.09 | $47.51 | $47.77 | 3,424,771 |
| Oct 17, 2025 | $47.78 | $48.12 | $47.22 | $47.57 | 3,738,316 |
| Oct 21, 2025 | $47.57 | $48.74 | $47.30 | $48.62 | 4,446,059 |
| Oct 23, 2025 | $48.51 | $49.29 | $48.16 | $48.98 | 3,175,256 |
| Oct 27, 2025 | $48.88 | $49.33 | $47.98 | $48.15 | 3,641,353 |
| Oct 29, 2025 | $48.20 | $48.41 | $46.93 | $47.25 | 4,091,924 |
| Oct 31, 2025 | $47.33 | $48.22 | $47.12 | $48.19 | 3,382,236 |
| Nov 4, 2025 | $48.18 | $48.44 | $47.57 | $48.34 | 3,072,533 |
| Nov 6, 2025 | $48.32 | $48.68 | $47.62 | $48.09 | 3,585,453 |
| Nov 10, 2025 | $48.22 | $49.36 | $47.86 | $48.96 | 4,120,348 |
| Nov 12, 2025 | $48.91 | $49.77 | $48.42 | $49.62 | 3,071,517 |
| Nov 14, 2025 | $49.54 | $50.01 | $48.39 | $48.75 | 2,492,919 |
| Nov 18, 2025 | $48.80 | $49.13 | $47.98 | $48.42 | 2,918,577 |
| Nov 20, 2025 | $48.49 | $48.51 | $47.72 | $47.84 | 2,855,397 |
| Nov 24, 2025 | $47.80 | $48.35 | $47.73 | $48.27 | 3,312,269 |
| Nov 26, 2025 | $48.25 | $48.89 | $47.88 | $48.61 | 2,453,326 |
| Nov 28, 2025 | $48.72 | $48.83 | $47.41 | $47.75 | 2,946,453 |
| Dec 2, 2025 | $47.73 | $48.14 | $47.21 | $47.69 | 3,623,788 |
| Dec 4, 2025 | $47.64 | $47.98 | $46.76 | $47.03 | 3,332,626 |
| Dec 8, 2025 | $47.00 | $47.60 | $46.34 | $47.24 | 3,778,052 |
| Dec 10, 2025 | $47.10 | $47.51 | $46.02 | $46.08 | 3,373,279 |
| Dec 12, 2025 | $46.09 | $46.31 | $44.60 | $44.78 | 4,895,950 |
| Dec 16, 2025 | $44.85 | $46.28 | $44.69 | $45.86 | 3,593,573 |
| Dec 18, 2025 | $45.99 | $46.43 | $44.88 | $45.50 | 3,881,614 |
| Dec 22, 2025 | $45.40 | $46.31 | $45.04 | $45.26 | 3,991,818 |
| Dec 24, 2025 | $45.25 | $46.09 | $45.03 | $45.26 | 3,566,202 |
| Dec 26, 2025 | $45.34 | $46.31 | $44.91 | $45.94 | 3,437,960 |
| Dec 30, 2025 | $45.82 | $46.89 | $45.55 | $46.45 | 3,268,652 |
| Jan 1, 2026 | $46.39 | $47.43 | $46.28 | $46.95 | 3,105,655 |
| Jan 5, 2026 | $46.91 | $47.18 | $46.22 | $46.50 | 3,294,038 |
| Jan 7, 2026 | $46.52 | $46.63 | $45.75 | $46.08 | 4,184,454 |
| Jan 9, 2026 | $45.98 | $46.15 | $44.99 | $45.10 | 3,613,736 |
| Jan 13, 2026 | $45.09 | $45.13 | $44.36 | $44.85 | 4,154,950 |
| Jan 15, 2026 | $44.97 | $45.15 | $44.45 | $44.82 | 3,664,768 |
| Jan 19, 2026 | $44.93 | $45.68 | $44.68 | $45.11 | 3,531,391 |
| Jan 21, 2026 | $45.23 | $45.36 | $43.82 | $44.01 | 4,194,944 |
| Jan 23, 2026 | $43.89 | $44.23 | $43.41 | $43.68 | 2,748,345 |
| Jan 27, 2026 | $43.62 | $43.74 | $43.21 | $43.46 | 3,456,250 |
| Jan 29, 2026 | $43.44 | $43.83 | $42.88 | $43.26 | 3,720,010 |
| Feb 2, 2026 | $43.36 | $44.71 | $43.06 | $44.29 | 2,693,097 |
| Feb 4, 2026 | $44.17 | $45.04 | $43.83 | $44.91 | 3,523,597 |
| Feb 6, 2026 | $44.79 | $44.90 | $44.22 | $44.48 | 2,667,761 |
| Feb 10, 2026 | $44.50 | $45.00 | $44.11 | $44.69 | 3,692,994 |
| Feb 12, 2026 | $44.72 | $45.81 | $44.54 | $45.70 | 4,112,354 |
| Feb 16, 2026 | $45.70 | $46.29 | $45.12 | $45.49 | 3,778,287 |
| Feb 18, 2026 | $45.38 | $46.81 | $45.30 | $46.44 | 3,150,073 |
| Feb 20, 2026 | $46.47 | $47.85 | $46.42 | $47.56 | 3,632,196 |
| Feb 24, 2026 | $47.48 | $48.08 | $47.32 | $47.53 | 3,293,943 |
| Feb 26, 2026 | $47.40 | $48.43 | $47.07 | $47.74 | 4,470,325 |
| Mar 2, 2026 | $47.77 | $48.44 | $47.37 | $48.02 | 3,147,931 |
| Mar 4, 2026 | $47.93 | $48.53 | $47.61 | $48.40 | 2,324,389 |
| Mar 6, 2026 | $48.38 | $48.56 | $46.67 | $46.83 | 3,743,224 |
| Mar 10, 2026 | $46.75 | $47.27 | $46.07 | $46.28 | 2,743,590 |
| Mar 12, 2026 | $46.23 | $47.16 | $45.54 | $46.82 | 3,565,180 |
| Mar 16, 2026 | $46.77 | $47.64 | $46.51 | $47.33 | 2,563,098 |
| Mar 18, 2026 | $47.25 | $47.61 | $46.40 | $47.08 | 3,230,180 |
| Mar 20, 2026 | $47.08 | $47.38 | $46.39 | $46.47 | 3,525,049 |
| Mar 24, 2026 | $46.42 | $47.88 | $46.34 | $47.43 | 4,152,610 |
| Mar 26, 2026 | $47.33 | $47.92 | $47.11 | $47.37 | 4,474,808 |
| Mar 30, 2026 | $47.51 | $47.69 | $46.54 | $47.28 | 3,878,900 |
| Apr 1, 2026 | $47.17 | $47.88 | $46.83 | $47.83 | 3,303,289 |
| Apr 3, 2026 | $47.85 | $48.18 | $47.41 | $47.57 | 3,135,197 |
| Apr 7, 2026 | $47.60 | $48.03 | $46.53 | $46.82 | 3,837,456 |
| Apr 9, 2026 | $46.95 | $47.49 | $46.79 | $47.26 | 3,556,314 |
| Apr 13, 2026 | $47.25 | $48.20 | $46.84 | $47.84 | 3,606,917 |
| Apr 15, 2026 | $47.71 | $48.08 | $47.06 | $47.84 | 3,680,880 |
| Apr 17, 2026 | $47.77 | $49.08 | $47.52 | $48.71 | 2,703,485 |
| Apr 21, 2026 | $48.76 | $49.68 | $48.44 | $48.63 | 3,659,139 |
| Apr 23, 2026 | $48.68 | $48.99 | $48.32 | $48.90 | 4,119,642 |
| Apr 27, 2026 | $49.02 | $49.77 | $48.97 | $49.52 | 3,337,790 |
| Apr 29, 2026 | $49.38 | $50.20 | $49.23 | $49.89 | 3,259,150 |
| May 1, 2026 | $49.79 | $50.34 | $49.54 | $49.95 | 3,009,964 |
| May 5, 2026 | $49.88 | $50.89 | $49.87 | $50.37 | 3,123,092 |
| May 7, 2026 | $50.46 | $51.19 | $50.08 | $51.01 | 2,963,583 |
| May 11, 2026 | $51.02 | $51.78 | $50.25 | $51.34 | 3,339,406 |
| May 13, 2026 | $51.19 | $52.41 | $50.99 | $52.38 | 3,769,254 |
| May 15, 2026 | $52.25 | $53.90 | $52.04 | $53.58 | 3,835,103 |
| May 19, 2026 | $53.60 | $54.03 | $52.86 | $53.29 | 3,065,713 |
| May 21, 2026 | $53.26 | $54.54 | $53.00 | $54.06 | 3,335,218 |
| May 25, 2026 | $54.10 | $55.17 | $53.70 | $54.69 | 3,617,439 |
| May 27, 2026 | $54.70 | $55.54 | $54.17 | $55.31 | 2,988,318 |
| May 29, 2026 | $55.38 | $56.46 | $54.98 | $56.44 | 4,173,303 |
| Jun 2, 2026 | $56.33 | $57.99 | $55.78 | $56.89 | 4,621,580 |
| Jun 4, 2026 | $57.03 | $57.11 | $55.64 | $56.23 | 2,869,670 |
| Jun 8, 2026 | $56.39 | $57.63 | $56.04 | $56.54 | 3,737,876 |
| Jun 10, 2026 | $56.69 | $57.33 | $56.16 | $56.57 | 2,812,886 |
| Jun 12, 2026 | $56.41 | $57.17 | $56.08 | $56.53 | 3,152,256 |
| Jun 16, 2026 | $56.44 | $56.94 | $54.72 | $55.16 | 4,037,230 |
| Jun 18, 2026 | $55.02 | $55.62 | $54.69 | $54.83 | 2,846,949 |
| Jun 22, 2026 | $54.67 | $56.58 | $54.61 | $56.14 | 4,071,706 |
| Jun 24, 2026 | $56.26 | $56.40 | $54.93 | $55.34 | 2,677,140 |
| Jun 26, 2026 | $55.39 | $56.26 | $55.05 | $56.21 | 4,771,589 |
| Jun 30, 2026 | $56.32 | $56.80 | $55.49 | $55.85 | 3,797,111 |
| Jul 2, 2026 | $55.93 | $56.63 | $55.58 | $56.41 | 3,337,880 |
| Jul 6, 2026 | $56.27 | $56.73 | $54.79 | $54.96 | 3,604,465 |
| Jul 8, 2026 | $55.03 | $56.90 | $54.90 | $56.80 | 3,325,856 |
| Jul 10, 2026 | $56.70 | $57.24 | $55.44 | $55.52 | 3,200,763 |
| Jul 14, 2026 | $55.64 | $56.06 | $54.55 | $55.71 | 4,168,877 |
| Jul 16, 2026 | $55.61 | $56.80 | $55.34 | $55.91 | 4,916,578 |
| Jul 20, 2026 | $55.90 | $56.15 | $54.82 | $55.72 | 3,400,282 |
| Jul 22, 2026 | $55.77 | $56.99 | $55.72 | $56.08 | 3,542,425 |
| Jul 24, 2026 | $56.14 | $57.09 | $55.85 | $56.75 | 2,930,121 |
| Jul 28, 2026 | $56.71 | $58.15 | $56.62 | $57.96 | 2,992,583 |
| Jul 30, 2026 | $57.83 | $58.47 | $57.20 | $57.64 | 4,259,940 |
| Aug 3, 2026 | $57.63 | $59.60 | $57.42 | $59.18 | 3,235,493 |
| Aug 5, 2026 | $59.06 | $60.77 | $58.82 | $60.55 | 3,868,656 |
| Aug 7, 2026 | $60.59 | $61.59 | $60.07 | $60.51 | 2,959,913 |
| Aug 11, 2026 | $60.48 | $62.04 | $60.18 | $60.68 | 3,339,835 |
| Aug 13, 2026 | $60.82 | $61.78 | $60.22 | $61.63 | 3,627,353 |
| Aug 17, 2026 | $61.62 | $62.07 | $59.81 | $60.37 | 3,783,811 |
| Aug 19, 2026 | $60.42 | $60.95 | $59.34 | $60.49 | 4,543,698 |
| Aug 21, 2026 | $60.63 | $60.98 | $59.71 | $60.53 | 3,403,106 |
| Aug 25, 2026 | $60.40 | $61.38 | $59.87 | $60.29 | 3,424,215 |
| Aug 27, 2026 | $60.42 | $61.07 | $60.04 | $60.61 | 2,347,049 |
| Aug 31, 2026 | $60.79 | $61.13 | $59.62 | $59.76 | 3,712,492 |
| Sep 2, 2026 | $59.63 | $60.43 | $59.27 | $59.83 | 3,633,156 |
| Sep 4, 2026 | $59.67 | $59.99 | $57.95 | $58.29 | 4,139,036 |
| Sep 8, 2026 | $58.15 | $58.15 | $56.26 | $56.76 | 2,944,749 |
| Sep 10, 2026 | $56.92 | $57.30 | $55.07 | $55.59 | 2,807,156 |
| Sep 14, 2026 | $55.49 | $57.44 | $55.16 | $57.03 | 3,431,098 |
| Sep 16, 2026 | $57.00 | $58.16 | $56.72 | $56.97 | 4,052,518 |
| Sep 18, 2026 | $56.87 | $57.29 | $55.97 | $57.29 | 3,043,195 |
| Sep 22, 2026 | $57.42 | $57.95 | $57.07 | $57.30 | 3,398,372 |
| Sep 24, 2026 | $57.37 | $57.67 | $56.15 | $56.88 | 3,125,174 |
| Sep 28, 2026 | $57.01 | $57.58 | $55.84 | $56.20 | 3,210,502 |
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/stock_chart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/stock_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/stock_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 stock_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}// A year and a quarter of hourly candles (314 trading days of 7), made in the browser as on the server, so the// page does not carry them.component StockChartDemoLive() { let hourly = stock_chart_demo_walk(77, 2198, 141.2, "2025-07-01", true) state chosen = "3M" <div.demo-stack id="stock_chart-demo-main"> <StockChart label="ACME · hourly" candles={hourly} prefix="$" indicators={["sma20", "ema50"]} on_range={(r) => { chosen = r }}/> <p.demo-note>{chosen == "" ? "Zoomed or panned by hand." : `Showing ${chosen}.`}</p> </div>}let stock_chart_demo_daily = stock_chart_demo_walk(5, 260, 48.2, "2025-10-01")view StockChartDemo() = <div.demo-stack> <StockChartDemoLive/> <div id="stock_chart-demo-daily"> <StockChart label="Globex · daily, line with RSI and MACD" candles={stock_chart_demo_daily} kind="line" period="All" indicators={["bb", "rsi", "macd"]} prefix="$" height={240}/> </div> </div>Source
ui/stock_chart.qwe
// StockChart: a full trading chart. Candles, a line or an area (a switch), volume// under the price, range buttons (1D 1W 1M 3M 1Y All), a crosshair that follows the// pointer with the open, high, low, close, change and volume where it is, the wheel to// zoom and a drag to pan, indicators to turn on and off (SMA 20, EMA 50, Bollinger// bands, an RSI pane and a MACD pane), and the last price as a line and a label. The// keyboard does it all too: with the chart focused, the arrow keys move the crosshair// (and pan at the ends), + and − zoom, Home and End go to the first and last candle,// and Escape lets go; screen readers hear the candle the crosshair is on.//// <StockChart label="ACME" candles={bars} prefix="$"/>// <StockChart label="BTC-USD" candles={hourly} kind="line" period="1W" indicators={["ema50", "rsi"]}// decimals={0} on_range={(r) => { picked = r }}/>//// 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).// When more candles are in view than fit (about 160), neighbours are drawn as one// (the first open, the highest high, the lowest low, the last close, the volumes// added), and the readout says how many it holds. kind: "candles" (default), "line"// or "area" to start with. period: the range to start with ("3M"); a range that the// candles cannot fill (1D of daily candles) is disabled. indicators: those on to start// with, of "sma20", "ema50", "bb", "rsi" and "macd". height: the price pane's height// in pixels (340). prefix and suffix: around every price ("$"); decimals: digits after// the point of prices (2). label: what the chart shows, for screen readers and the// heading. on_range(range): called with the range picked ("1M"), or "" when the wheel,// a drag or the keyboard leaves the ranges.import ui/theme.qwe use ui_clslet stock_chart_css = css`.ui-stk { --ui-stk-1: light-dark(#2a78d6, #3987e5); --ui-stk-2: light-dark(#eb6834, #d95926); --ui-stk-3: light-dark(#1baf7a, #199e70); --ui-stk-7: light-dark(#6250d6, #9085e9); display: grid; gap: 10px; min-width: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-stk-up { --k: var(--ui-success); } .ui-stk-down { --k: var(--ui-danger); }.ui-stk-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }.ui-stk-name { margin: 0; font-size: 15px; font-weight: 650; }.ui-stk-last { font-size: 20px; font-weight: 650; font-variant-numeric: tabular-nums; }.ui-stk-move { color: var(--k); font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-stk-span { color: var(--ui-muted); font-size: 12px; }.ui-stk-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }.ui-stk-group { display: inline-flex; flex-wrap: wrap; gap: 2px; padding: 2px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-stk-group button { height: 26px; min-width: 34px; padding: 0 9px; border: 0; border-radius: 4px; background: none; color: var(--ui-muted); font: 500 12px/1 var(--ui-font); cursor: pointer; }.ui-stk-group button:hover:not(:disabled) { color: var(--ui-fg); }.ui-stk-group button[aria-pressed="true"] { background: var(--ui-surface); color: var(--ui-fg); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); }.ui-stk-group button:disabled { opacity: .4; cursor: not-allowed; }.ui-stk-group button:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-stk-ind button[aria-pressed="true"] { color: var(--c, var(--ui-fg)); }.ui-stk-chart { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; }.ui-stk-panes { position: relative; touch-action: none; cursor: crosshair; user-select: none; border-radius: 2px; }.ui-stk-panes:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-stk-dragging { cursor: grabbing; }.ui-stk-pane { position: relative; height: var(--ph); border-bottom: 1px solid var(--ui-border); }.ui-stk-pane + .ui-stk-pane { border-top: 0; }.ui-stk-pname { position: absolute; z-index: 3; left: 6px; top: 4px; color: var(--ui-muted); font-size: 11px; font-weight: 600; pointer-events: none; }.ui-stk-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 65%, transparent); }.ui-stk-zero { background: var(--ui-strong); }.ui-stk-level { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--ui-strong); }.ui-stk-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-stk-svg path { fill: none; stroke-linejoin: round; stroke-linecap: round; }.ui-stk-svg .ui-stk-body { fill: var(--k); stroke: none; }.ui-stk-svg .ui-stk-wick { stroke: var(--k); stroke-width: 1; }.ui-stk-svg .ui-stk-vol { fill: var(--k); stroke: none; opacity: .28; }.ui-stk-svg .ui-stk-close { stroke: var(--ui-stk-1); stroke-width: 2; }.ui-stk-svg .ui-stk-fill { fill: var(--ui-stk-1); opacity: .1; stroke: none; }.ui-stk-svg .ui-stk-sma { stroke: var(--ui-stk-2); stroke-width: 1.5; }.ui-stk-svg .ui-stk-ema { stroke: var(--ui-stk-7); stroke-width: 1.5; }.ui-stk-svg .ui-stk-bb { stroke: var(--ui-stk-3); stroke-width: 1.25; }.ui-stk-svg .ui-stk-bbmid { stroke: var(--ui-stk-3); stroke-width: 1; stroke-dasharray: 4 3; }.ui-stk-svg .ui-stk-band { fill: var(--ui-stk-3); opacity: .09; stroke: none; }.ui-stk-svg .ui-stk-rsi { stroke: var(--ui-stk-7); stroke-width: 1.5; }.ui-stk-svg .ui-stk-macd { stroke: var(--ui-stk-1); stroke-width: 1.5; }.ui-stk-svg .ui-stk-signal { stroke: var(--ui-stk-2); stroke-width: 1.5; }.ui-stk-svg .ui-stk-hist { fill: var(--k); stroke: none; opacity: .55; }.ui-stk-band-rsi { position: absolute; left: 0; right: 0; background: color-mix(in srgb, var(--ui-stk-7) 8%, transparent); }.ui-stk-lastline { position: absolute; z-index: 1; left: 0; right: 0; border-top: 1px dashed var(--k); opacity: .8; pointer-events: none; }.ui-stk-vline { position: absolute; z-index: 4; top: 0; bottom: 0; width: 0; border-left: 1px dashed var(--ui-muted); pointer-events: none; }.ui-stk-hline { position: absolute; z-index: 4; left: 0; right: 0; height: 0; border-top: 1px dashed var(--ui-muted); pointer-events: none; }.ui-stk-read { position: absolute; z-index: 5; top: 6px; left: 6px; right: 6px; display: flex; flex-wrap: wrap; gap: 2px 10px; pointer-events: none; color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-stk-read > span { padding: 1px 0; background: color-mix(in srgb, var(--ui-bg) 75%, transparent); white-space: nowrap; }.ui-stk-read b { color: var(--ui-fg); font-weight: 600; }.ui-stk-read .ui-stk-up b, .ui-stk-read .ui-stk-down b { color: var(--k); }.ui-stk-read .ui-stk-when { color: var(--ui-fg); font-weight: 600; }.ui-stk-axis { position: relative; }.ui-stk-ax { position: relative; height: var(--ph); }.ui-stk-ax span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-stk-ax .ui-stk-tag { z-index: 2; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--k); color: var(--ui-bg); font-weight: 600; }.ui-stk-ax .ui-stk-cross { z-index: 3; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--ui-fg); color: var(--ui-bg); font-weight: 600; }.ui-stk-x { position: relative; height: 18px; margin-top: 4px; }.ui-stk-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-stk-x .ui-stk-first { transform: none; }.ui-stk-x .ui-stk-end { transform: translateX(-100%); }.ui-stk-x .ui-stk-cross { z-index: 2; padding: 1px 5px; border-radius: 3px; background: var(--ui-fg); color: var(--ui-bg); font-weight: 600; }.ui-stk-help { margin: 0; color: var(--ui-muted); font-size: 12px; }.ui-stk-help kbd { padding: 0 4px; border: 1px solid var(--ui-border); border-radius: 3px; background: var(--ui-subtle); font: 11px/1.4 var(--ui-mono); }`// n (a whole number, 0 or more) with at least `width` digits.fn stock_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 stock_chart_group(n: int) -> str = n < 1000 ? str(n) : stock_chart_group(floor(n / 1000)) + "," + stock_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 stock_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)}` : `.${stock_chart_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${stock_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn stock_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 ? stock_chart_num(v) : `${v < 0 ? "-" : ""}${stock_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn stock_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${stock_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn stock_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 stock_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 stock_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 stock_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 stock_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 stock_chart_ema(xs: list, period: int) -> list { let k = 2 / (period + 1) let out = [] let prev = null for i in range(len(xs)) { if i + 1 == period { prev = sum(take(xs, period)) / period } else if i + 1 > period { prev = xs[i] * k + prev * (1 - k) } out = out | push(prev) } out}// The RSI of each close (Wilder's smoothing), null for the first `period`.fn stock_chart_rsi(xs: list, period: int) -> list { let out = [] let gain = 0 let loss = 0 for i in range(len(xs)) { let d = i == 0 ? 0 : xs[i] - xs[i - 1] let up = d > 0 ? d : 0 let down = d < 0 ? 0 - d : 0 if i > 0 && i <= period { gain += up / period loss += down / period } else if i > period { gain = (gain * (period - 1) + up) / period loss = (loss * (period - 1) + down) / period } out = out | push(i < period ? null : (loss == 0 ? (gain == 0 ? 50 : 100) : 100 - 100 / (1 + gain / loss))) } out}// Bollinger bands: { mid, upper, lower }, `k` population deviations of `period` closes.fn stock_chart_bands(xs: list, period: int, k: num) -> map { let mid = stock_chart_sma(xs, period) let dev = for i in range(len(xs)) => mid[i] == null ? null : sqrt(sum(slice(xs, i + 1 - period, i + 1) | map((v) => (v - mid[i]) * (v - mid[i]))) / period) { mid, upper: for i in range(len(xs)) => mid[i] == null ? null : mid[i] + k * dev[i], lower: for i in range(len(xs)) => mid[i] == null ? null : mid[i] - k * dev[i], }}// MACD: { macd, signal, hist }, the fast EMA less the slow, its EMA, and their gap.fn stock_chart_macd(xs: list, fast: int, slow: int, signal: int) -> map { let a = stock_chart_ema(xs, fast) let b = stock_chart_ema(xs, slow) let macd = for i in range(len(xs)) => a[i] == null || b[i] == null ? null : a[i] - b[i] let from = index_of(macd | map((v) => v != null), true) let sig0 = from == null ? [] : stock_chart_ema(drop(macd, from), signal) let sig = for i in range(len(xs)) => from == null || i < from ? null : sig0[i - from] { macd, signal: sig, hist: for i in range(len(xs)) => macd[i] == null || sig[i] == null ? null : macd[i] - sig[i] }}// A candle's time in milliseconds since 1970 (0 when it is not a date).fn stock_chart_ms(t) -> num = type_of(t) == "str" && date.valid(t) ? date.ms(t) : (type_of(t) == "int" || type_of(t) == "float" ? t : 0)// The candles of the last `days` days, as { start, count }; null when that is fewer than two// candles or every one of them (the range adds nothing).fn stock_chart_window(ms: list, days: num) -> map? { let n = len(ms) if n < 2 { return null } let from = ms[n - 1] - days * 86400000 let start = index_of(ms | map((m) => m > from), true) ?? n let count = n - start count < 2 || count >= n ? null : { start, count }}// The candles from `start`, `count` of them, as at most about `most` bars: neighbours// drawn as one when there are more, each { i0, i1, time, open, high, low, close, volume }.fn stock_chart_buckets(candles: list, start: int, count: int, most: int) -> list { let k = count > most ? ceil(count / most) : 1 for g in range(ceil(count / k)) => { let i0 = start + g * k let i1 = (g + 1) * k > count ? start + count - 1 : i0 + k - 1 let part = slice(candles, i0, i1 + 1) { i0, i1, time: candles[i0].time, open: candles[i0].open, close: candles[i1].close, high: max(part | map((c) => c.high)), low: min(part | map((c) => c.low)), volume: sum(part | map((c) => c.volume ?? 0)) } }}component StockChart(candles: list = [], kind: str = "candles", period: str = "3M", indicators: list = [], height: int = 340, prefix: str = "", suffix: str = "", decimals: int = 2, label: str = "Stock chart", on_range = null, class: str? = null) { style global stock_chart_css let n = len(candles) let ms = candles | map((c) => stock_chart_ms(c.time)) let closes = candles | map((c) => c.close) let sma20 = stock_chart_sma(closes, 20) let ema50 = stock_chart_ema(closes, 50) let bb = stock_chart_bands(closes, 20, 2) let rsi14 = stock_chart_rsi(closes, 14) let macd = stock_chart_macd(closes, 12, 26, 9) let ranges = [{ name: "1D", days: 1 }, { name: "1W", days: 7 }, { name: "1M", days: 30 }, { name: "3M", days: 91 }, { name: "1Y", days: 365 }, { name: "All", days: 0 }] let window_of = (name: str) => { let r = find(ranges, (x) => x.name == name) r == null ? null : (r.days == 0 ? { start: 0, count: n } : stock_chart_window(ms, r.days)) } let kinds = [{ id: "candles", name: "Candles" }, { id: "line", name: "Line" }, { id: "area", name: "Area" }] let studies = [{ id: "sma20", name: "SMA 20", hue: 2 }, { id: "ema50", name: "EMA 50", hue: 7 }, { id: "bb", name: "Bollinger", hue: 3 }, { id: "rsi", name: "RSI", hue: 7 }, { id: "macd", name: "MACD", hue: 1 }] state shape = kind state picked = window_of(period) != null ? period : "All" state start = (window_of(period) ?? { start: 0 }).start state count = (window_of(period) ?? { count: n }).count state on = indicators state hover = null state at_y = null state drag = null state keyed = false let fewest = n < 5 ? n : 5 let c = count < fewest ? fewest : (count > n ? n : count) let s = start < 0 ? 0 : (start > n - c ? n - c : start) let bars = stock_chart_buckets(candles, s, c, 160) let nb = len(bars) let per = c > 160 ? ceil(c / 160) : 1 let has = (id: str) => index_of(on, id) != null let at = (vals: list) => bars | map((b) => vals[b.i1]) let known = (vals: list) => vals | filter((v) => v != null) let in_view = flatten(bars | map((b) => shape == "candles" ? [b.low, b.high] : [b.close])) + (has("sma20") ? known(at(sma20)) : []) + (has("ema50") ? known(at(ema50)) : []) + (has("bb") ? known(at(bb.upper)) + known(at(bb.lower)) : []) let sc = stock_chart_scale(nb == 0 ? 0 : min(in_view), nb == 0 ? 1 : max(in_view), 6) // Prices take the pane from 3% to 79% down; the volume, the bottom 18%. let y = (v) => 3 + (sc.hi - v) * 76 / (sc.hi - sc.lo) let x = (j) => (j + 0.5) * 100 / nb let w = nb == 0 ? 1 : 100 / nb let pct = (v) => stock_chart_pct(v) let price = (v) => v == null ? "—" : `${v < 0 ? "-" : ""}${prefix}${stock_chart_num(abs(v), decimals)}${suffix}` let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${stock_chart_num(abs(t))}` let up = (b) => b.close >= b.open let side = (ok: bool) => ok ? "ui-stk-up" : "ui-stk-down" // The price pane's paths: bodies, wicks and volume of up or down bars, and lines. let bodies = (rise: bool) => join(range(nb) | filter((j) => up(bars[j]) == rise) | map((j) => { let b = bars[j] let tall = abs(y(b.open) - y(b.close)) `M${pct(x(j) - w * 0.36)},${pct(y(max([b.open, b.close])))} h${pct(w * 0.72)} v${pct(tall < 0.4 ? 0.4 : tall)} h-${pct(w * 0.72)} Z` }), " ") let wicks = (rise: bool) => join(range(nb) | filter((j) => up(bars[j]) == rise) | map((j) => `M${pct(x(j))},${pct(y(bars[j].high))} V${pct(y(bars[j].low))}`), " ") let vtop = max([1] + (bars | map((b) => b.volume))) let vols = (rise: bool) => join(range(nb) | filter((j) => up(bars[j]) == rise) | map((j) => { let tall = bars[j].volume * 18 / vtop `M${pct(x(j) - w * 0.36)},100 h${pct(w * 0.72)} v-${pct(tall)} h-${pct(w * 0.72)} Z` }), " ") let line_in = (vals: list, to) => { let js = range(nb) | filter((j) => vals[j] != null) len(js) == 0 ? "" : "M" + join(js | map((j) => `${pct(x(j))},${pct(to(vals[j]))}`), " L") } let close_line = line_in(bars | map((b) => b.close), y) let band = () => { let hi = at(bb.upper) let lo = at(bb.lower) let js = range(nb) | filter((j) => hi[j] != null) len(js) == 0 ? "" : "M" + join(js | map((j) => `${pct(x(j))},${pct(y(hi[j]))}`), " L") + " L" + join(reverse(js) | map((j) => `${pct(x(j))},${pct(y(lo[j]))}`), " L") + " Z" } // The RSI and MACD panes. let ri = (v) => 100 - v let mvals = known(at(macd.macd) + at(macd.signal) + at(macd.hist)) let reach = max([0.000001] + (mvals | map((v) => abs(v)))) let msc = stock_chart_scale(0 - reach, reach, 4) let my = (v) => (msc.hi - v) * 100 / (msc.hi - msc.lo) let hists = (rise: bool) => join(range(nb) | filter((j) => macd.hist[bars[j].i1] != null && (macd.hist[bars[j].i1] >= 0) == rise) | map((j) => { let v = macd.hist[bars[j].i1] `M${pct(x(j) - w * 0.32)},${pct(my(v > 0 ? v : 0))} h${pct(w * 0.64)} v${pct(abs(my(v) - my(0)))} h-${pct(w * 0.64)} Z` }), " ") let panes = [{ id: "price", h: height }] + (has("rsi") ? [{ id: "rsi", h: 90 }] : []) + (has("macd") ? [{ id: "macd", h: 100 }] : []) // Where the crosshair is: the bar hovered, else the last in view. let focus = hover != null && hover < nb ? hover : null let shown = focus != null ? bars[focus] : (nb > 0 ? bars[nb - 1] : null) let before = (b) => b.i0 > 0 ? candles[b.i0 - 1].close : b.open let change = (b) => before(b) != 0 ? (b.close - before(b)) * 100 / before(b) : 0 let when = (b) => stock_chart_when(b.time) let last = n > 0 ? candles[-1] : null let last_up = n < 2 || last.close >= candles[n - 2].close let first_view = nb > 0 ? bars[0].open : 0 let view_change = nb > 0 && first_view != 0 ? (bars[nb - 1].close - first_view) * 100 / first_view : 0 let cross_price = at_y != null && focus != null && at_y * 100 >= 3 && at_y * 100 <= 79 ? sc.hi - (at_y * 100 - 3) * (sc.hi - sc.lo) / 76 : null let axis_width = max([6] + (sc.ticks | map((t) => len(tick(t)))) + [last != null ? len(price(last.close)) + 1 : 0]) let said = (b) => `${when(b)}: open ${price(b.open)}, high ${price(b.high)}, low ${price(b.low)}, close ${price(b.close)}, ${stock_chart_signed(change(b))}, volume ${stock_chart_short(b.volume)}` // Moving the view: to a first candle, by a factor of zoom around a point (0 to 1 across). let leave_range = () => { if picked != "" { picked = "" on_range && on_range("") } } let move_to = (first: int) => { let next = first < 0 ? 0 : (first > n - c ? n - c : first) if next != s { start = next leave_range() } } let zoom = (factor: num, fx: num) => { let anchor = s + fx * c let want = round(c * factor) let next = want < fewest ? fewest : (want > n ? n : want) if next != c { let first = round(anchor - fx * next) count = next start = first < 0 ? 0 : (first > n - next ? n - next : first) leave_range() } } let locate = (e) => { let r = e.target.closest(".ui-stk-panes").getBoundingClientRect() { fx: r.width > 0 ? (e.clientX - r.left) / r.width : 0.5, py: (e.clientY - r.top) / height, width: r.width } } <div class={ui_cls(["ui-stk", class])} style={`--ui-stk-h: ${height}px`}> <div.ui-stk-head> <h3.ui-stk-name>{label}</h3> {last != null && <span.ui-stk-last>{price(last.close)}</span>} {nb > 0 && <span class={`ui-stk-move ${side(view_change >= 0)}`}>{stock_chart_signed(view_change)}</span>} {nb > 0 && <span.ui-stk-span>{picked != "" && picked != "All" ? `past ${picked}` : "in view"} · {when(bars[0])} – {when(bars[nb - 1])}</span>} </div> <div.ui-stk-tools> <span.ui-stk-group role="group" aria-label="Range"> {for r in ranges => <button type="button" aria-pressed={picked == r.name ? "true" : "false"} disabled={window_of(r.name) == null} @click={() => { let wnd = window_of(r.name) if wnd != null { start = wnd.start count = wnd.count picked = r.name hover = null on_range && on_range(r.name) } }}>{r.name}</button>} </span> <span.ui-stk-group role="group" aria-label="Chart type"> {for k in kinds => <button type="button" aria-pressed={shape == k.id ? "true" : "false"} @click={() => { shape = k.id }}>{k.name}</button>} </span> <span.ui-stk-group.ui-stk-ind role="group" aria-label="Indicators"> {for t in studies => <button type="button" aria-pressed={has(t.id) ? "true" : "false"} style={`--c: var(--ui-stk-${t.hue})`} @click={() => { on = has(t.id) ? (on | filter((x) => x != t.id)) : on + [t.id] }}>{t.name}</button>} </span> </div> <div.ui-stk-chart> <div class={ui_cls(["ui-stk-panes", drag != null && "ui-stk-dragging"])} tabindex="0" role="application" aria-label={`${label}, ${nb > 0 ? `${when(bars[0])} to ${when(bars[nb - 1])}` : "no data"}`} aria-roledescription="chart" @pointermove={(e) => { // Read before the call below, which can pause the handler: a pointerup that // comes meanwhile must not cut this move short. let held = drag let p = locate(e) if held != null && p.width > 0 { move_to(held.from + round((held.x - e.clientX) * c / p.width)) } let j = floor(p.fx * nb) hover = nb == 0 ? null : (j < 0 ? 0 : (j >= nb ? nb - 1 : j)) at_y = p.py >= 0 && p.py <= 1 ? p.py : null keyed = false }} @pointerleave={() => { if drag == null { hover = null at_y = null } }} @pointerdown={(e) => { if e.button == 0 { // Set first: the handler can pause here, and a move that comes meanwhile // must find the drag begun. drag = { x: e.clientX, from: s } e.target.closest(".ui-stk-panes").setPointerCapture(e.pointerId) } }} @pointerup={() => { drag = null }} @pointercancel={() => { drag = null }} @wheel={(e) => { e.preventDefault() let p = locate(e) zoom(e.deltaY > 0 ? 1.25 : 0.8, p.fx < 0 ? 0 : (p.fx > 1 ? 1 : p.fx)) }} @keydown={(e) => { let k = e.key if k == "ArrowRight" || k == "ArrowLeft" { e.preventDefault() let step = k == "ArrowRight" ? 1 : -1 let next = focus == null ? nb - 1 : focus + step if next < 0 { move_to(s - per) next = 0 } else if next >= nb { move_to(s + per) next = nb - 1 } hover = next at_y = null keyed = true } else if k == "+" || k == "=" || k == "-" || k == "_" || k == "−" { e.preventDefault() zoom(k == "+" || k == "=" ? 0.8 : 1.25, focus != null && nb > 0 ? (focus + 0.5) / nb : 0.5) keyed = true } else if k == "Home" || k == "End" { e.preventDefault() move_to(k == "Home" ? 0 : n - c) hover = k == "Home" ? 0 : nb - 1 keyed = true } else if k == "Escape" { hover = null keyed = false } }}> {for p in panes => <div.ui-stk-pane data-pane={p.id} style={`--ph: ${p.h}px`}> {match p.id { "price" => [ for t in sc.ticks => <span.ui-stk-grid style={`top: ${pct(y(t))}%`}></span>, <svg.ui-stk-svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"> <path.ui-stk-vol.ui-stk-up d={vols(true)}/> <path.ui-stk-vol.ui-stk-down d={vols(false)}/> {has("bb") && [<path.ui-stk-band d={band()}/>, <path.ui-stk-bb d={line_in(at(bb.upper), y)} vector-effect="non-scaling-stroke"/>, <path.ui-stk-bb d={line_in(at(bb.lower), y)} vector-effect="non-scaling-stroke"/>, <path.ui-stk-bbmid d={line_in(at(bb.mid), y)} vector-effect="non-scaling-stroke"/>]} {shape == "candles" ? [ <path.ui-stk-wick.ui-stk-up d={wicks(true)} vector-effect="non-scaling-stroke"/>, <path.ui-stk-wick.ui-stk-down d={wicks(false)} vector-effect="non-scaling-stroke"/>, <path.ui-stk-body.ui-stk-up d={bodies(true)}/>, <path.ui-stk-body.ui-stk-down d={bodies(false)}/>, ] : [ shape == "area" && nb > 0 && <path.ui-stk-fill d={`${close_line} L${pct(x(nb - 1))},100 L${pct(x(0))},100 Z`}/>, <path.ui-stk-close d={close_line} vector-effect="non-scaling-stroke"/>, ]} {has("sma20") && <path.ui-stk-sma d={line_in(at(sma20), y)} vector-effect="non-scaling-stroke"/>} {has("ema50") && <path.ui-stk-ema d={line_in(at(ema50), y)} vector-effect="non-scaling-stroke"/>} </svg>, last != null && last.close >= sc.lo && last.close <= sc.hi && <span class={`ui-stk-lastline ${side(last_up)}`} style={`top: ${pct(y(last.close))}%`}></span>, cross_price != null && <span.ui-stk-hline style={`top: ${pct(at_y * 100)}%`}></span>, shown != null && <div.ui-stk-read aria-hidden="true"> <span.ui-stk-when>{when(shown)}{shown.i1 > shown.i0 ? ` (${shown.i1 - shown.i0 + 1} bars)` : ""}</span> <span>O <b>{price(shown.open)}</b></span> <span>H <b>{price(shown.high)}</b></span> <span>L <b>{price(shown.low)}</b></span> <span class={side(change(shown) >= 0)}>C <b>{price(shown.close)}</b> <b>{stock_chart_signed(change(shown))}</b></span> <span>Vol <b>{stock_chart_short(shown.volume)}</b></span> {has("sma20") && <span>SMA 20 <b>{price(sma20[shown.i1])}</b></span>} {has("ema50") && <span>EMA 50 <b>{price(ema50[shown.i1])}</b></span>} {has("bb") && <span>BB <b>{price(bb.lower[shown.i1])}</b> – <b>{price(bb.upper[shown.i1])}</b></span>} </div>, ] "rsi" => [ <span.ui-stk-pname>RSI 14 {shown != null && rsi14[shown.i1] != null ? stock_chart_num(rsi14[shown.i1], 1) : ""}</span>, <span.ui-stk-band-rsi style="top: 30%; height: 40%"></span>, <span.ui-stk-level style="top: 30%"></span>, <span.ui-stk-level style="top: 70%"></span>, <svg.ui-stk-svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"> <path.ui-stk-rsi d={line_in(at(rsi14), ri)} vector-effect="non-scaling-stroke"/> </svg>, ] _ => [ <span.ui-stk-pname>MACD 12 26 9 {shown != null && macd.macd[shown.i1] != null ? stock_chart_num(macd.macd[shown.i1], decimals + 2) : ""}</span>, <span.ui-stk-grid.ui-stk-zero style={`top: ${pct(my(0))}%`}></span>, <svg.ui-stk-svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"> <path.ui-stk-hist.ui-stk-up d={hists(true)}/> <path.ui-stk-hist.ui-stk-down d={hists(false)}/> <path.ui-stk-macd d={line_in(at(macd.macd), my)} vector-effect="non-scaling-stroke"/> <path.ui-stk-signal d={line_in(at(macd.signal), my)} vector-effect="non-scaling-stroke"/> </svg>, ] }} </div>} {focus != null && <span.ui-stk-vline style={`left: ${pct(x(focus))}%`}></span>} <span.ui-sr aria-live="polite">{keyed && shown != null ? said(shown) : ""}</span> </div> <div.ui-stk-axis style={`width: ${axis_width}ch`} aria-hidden="true"> {for p in panes => <div.ui-stk-ax style={`--ph: ${p.h}px`}> {match p.id { "price" => [ for t in sc.ticks => !(last != null && abs(y(t) - y(last.close)) * height < 1400) && <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>, last != null && last.close >= sc.lo && last.close <= sc.hi && <span class={`ui-stk-tag ${side(last_up)}`} style={`top: ${pct(y(last.close))}%`}>{price(last.close)}</span>, cross_price != null && <span.ui-stk-cross style={`top: ${pct(at_y * 100)}%`}>{price(cross_price)}</span>, ] "rsi" => for t in [70, 30] => <span style={`top: ${pct(ri(t))}%`}>{t}</span> _ => for t in msc.ticks => <span style={`top: ${pct(my(t))}%`}>{t < 0 ? "-" : ""}{stock_chart_num(abs(t))}</span> }} </div>} </div> <div.ui-stk-x aria-hidden="true"> {for t in stock_chart_ticks(bars | map((b) => b.time), 7) => !(focus != null && abs(x(t.i) - x(focus)) < 9) && <span class={ui_cls([x(t.i) < 4 && "ui-stk-first", x(t.i) > 96 && "ui-stk-end"])} style={`left: ${pct(x(t.i))}%`}>{t.text}</span>} {focus != null && <span class={ui_cls(["ui-stk-cross", x(focus) < 8 && "ui-stk-first", x(focus) > 92 && "ui-stk-end"])} style={`left: ${pct(x(focus))}%`}>{when(bars[focus])}</span>} </div> <span></span> </div> <p.ui-stk-help>Drag to pan and use the wheel to zoom; focused, <kbd>←</kbd> <kbd>→</kbd> move the crosshair, <kbd>+</kbd> <kbd>−</kbd> zoom, <kbd>Esc</kbd> lets go.</p> <table.ui-sr> <caption>{label}, the bars in view</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><th scope="col">Volume</th></tr></thead> <tbody> {for b in bars => <tr> <th scope="row">{when(b)}</th><td>{price(b.open)}</td><td>{price(b.high)}</td><td>{price(b.low)}</td> <td>{price(b.close)}</td><td>{stock_chart_num(b.volume, 0)}</td> </tr>} </tbody> </table> </div>}Related
MarketHeatmapA treemap of stocks sized by market value and coloured by their change, grouped by sector, with each one's details on hover.6 views · 7 downloadsPnlChartProfit and loss per day as green and red bars, with the running total and its drawdowns shaded, and the key figures above.6 views · 6 downloadsAreaChartFilled areas for one or more series, overlapping or stacked into a total, with a hover crosshair listing every value.6 views · 8 downloadsBollingerChartPrices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.7 views · 5 downloadsBrushChartA large chart of a range with an overview below, where a window dragged or moved by keys picks the range shown.6 views · 5 downloadsBulletChartMeasures as thin bars against a target mark over grey bands from poor to good, a row each, with the band named on hover.5 views · 7 downloads
Thread
Thread 0 posts
On StockChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.