acttrader-charts 1.3.0-beta.26 → 1.3.0-beta.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1523,6 +1523,1398 @@ The composite excludes trade levels (positions, SL/TP) — same rule as `chart.t
1523
1523
 
1524
1524
  ---
1525
1525
 
1526
+ ## Icons & Drawing Toolbar Options
1527
+
1528
+ ### Icons, Emojis & Stickers — `'icon'`
1529
+
1530
+ A glyph placed at a bar and price, behind `features.enableIconTools`. **One tool,
1531
+ not three**: emojis, stickers and icons are the same drawing with a different
1532
+ character, so the picker chooses the glyph and the glyph lives in `text` like any
1533
+ other annotation's content. They serialise, re-anchor and re-style through
1534
+ machinery that already exists rather than needing three parallel implementations.
1535
+
1536
+ ```ts
1537
+ const chart = new ChartEngine({
1538
+ container,
1539
+ features: { enableIconTools: true },
1540
+ });
1541
+ chart.setDrawingTool('icon'); // uses the last glyph picked
1542
+ ```
1543
+
1544
+ #### The picker
1545
+
1546
+ Opening the **Icons & Emojis** group gives a panel, not a list of rows: a row per
1547
+ glyph is unusable past a couple of dozen, and this is the one category whose
1548
+ items *are* the icon rather than being labelled by one.
1549
+
1550
+ ```
1551
+ ┌────────────────────────────────┐
1552
+ │ 😊 🐻 🍔 🏀 🚀 💡 ❤️ 🏳️ │ category tabs
1553
+ ├────────────────────────────────┤
1554
+ │ 😀 😃 😄 😁 😆 😅 🤣 😂 │
1555
+ │ 🙂 🙃 😉 😊 😇 🥰 😍 🤩 │ scrolling grid
1556
+ ├────────────────────────────────┤
1557
+ │ Emojis Stickers Icons │ kind tabs
1558
+ └────────────────────────────────┘
1559
+ ```
1560
+
1561
+ The three bottom tabs are not three copies of the same thing:
1562
+
1563
+ | Tab | Holds | Draws as |
1564
+ | --- | --- | --- |
1565
+ | **Emojis** | ~900 across the eight Unicode groups | System colour font, 22px |
1566
+ | **Stickers** | Trading-flavoured sets — bull & bear, reactions, money, warnings, awards | The same characters at 44px |
1567
+ | **Icons** | Monochrome arrows, shapes, marks, currency | Takes the drawing's colour |
1568
+
1569
+ That last row is why **Icons** is worth having at all. An emoji is a colour
1570
+ bitmap in the system font: it ignores `fillStyle`, so it can never be recoloured
1571
+ to match a palette or printed legibly in mono. Everything under Icons is a plain
1572
+ text character, and does tint.
1573
+
1574
+ The panel renders into the toolbar's existing flyout rather than adding a second
1575
+ floating layer, so it inherits that popover's positioning, outside-click
1576
+ dismissal and teardown. It remembers the tab and category per session — reopening
1577
+ a picker to find yourself back on the first page of smileys is the single most
1578
+ irritating thing a picker can do.
1579
+
1580
+ #### Resizing
1581
+
1582
+ A placed icon is selectable and **resizable by dragging any of its four corner
1583
+ handles**. Size is also in the style popover, which offers a colour swatch and a
1584
+ px picker rather than the default stroke/width/style — three controls that would
1585
+ do nothing to a glyph.
1586
+
1587
+ ```ts
1588
+ chart.getDrawings()
1589
+ .filter(d => d.type === 'icon')
1590
+ .forEach(d => { d.style = { ...d.style, fontSize: 64 }; });
1591
+ ```
1592
+
1593
+ **The size is in pixels, not price.** It does not rescale when the chart is
1594
+ zoomed. Deriving it from two data points would have fit the existing drag
1595
+ machinery with no new concepts, but a pin dropped on a bar would then become a
1596
+ billboard three zoom steps out and vanish three steps in — and the user would
1597
+ have to resize it again every time they changed the view. So `DrawingHandle`
1598
+ grew a `mode: 'size'`, and handles marked that way route to
1599
+ `IDrawing.resizeToPixel(px, py)` instead of the point writer.
1600
+
1601
+ `resizeToPixel` takes pixels because the size it sets is in pixels — converting
1602
+ through price and back would make the result depend on the zoom level it was
1603
+ dragged at. It measures against the anchor position cached during `render`,
1604
+ which is the only place a drawing is handed the scale.
1605
+
1606
+ The new size is the larger of the two axis distances, not the diagonal, so
1607
+ pulling straight out sideways grows the glyph as much as pulling corner to
1608
+ corner. A glyph is square; a user dragging one edge means "bigger", not "bigger
1609
+ by cos 45°". Clamped to 10–400px and rounded, since the value is shown in a px
1610
+ picker and round-trips through JSON.
1611
+
1612
+ Glyphs render through the system emoji font, so colour emoji stay colour on every
1613
+ platform that has them.
1614
+
1615
+ ### Drawing toolbar options
1616
+
1617
+ Five toggles in the toolbar's actions menu (⋯).
1618
+
1619
+ | Option | What it does |
1620
+ | --- | --- |
1621
+ | **Magnet Mode** | Drawing points snap to the nearest open/high/low/close of the bar under the cursor. |
1622
+ | **Keep Drawing Mode** | The tool stays armed after each drawing, so a series can be placed without returning to the toolbar. |
1623
+ | **Copy to All Charts** | New drawings are announced for replication across a layout. |
1624
+ | **Hide Drawings Toolbar** | Hides the toolbar at runtime; bring it back with `setDrawingToolbarVisible(true)`. |
1625
+ | **Zoom In** | In the Cursors group — drag a box and the chart zooms to it. |
1626
+
1627
+ ```ts
1628
+ const chart = new ChartEngine({
1629
+ container,
1630
+ magnetMode: true,
1631
+ keepDrawingMode: true,
1632
+ copyDrawingsToAllCharts: true,
1633
+ });
1634
+
1635
+ chart.setMagnetMode(false);
1636
+ chart.on('magnetModeChange', ({ enabled }) => persist(enabled));
1637
+ ```
1638
+
1639
+ **Magnet snaps within a pixel threshold, not always.** A trend line deliberately
1640
+ drawn through the middle of a range should stay there. The threshold is in
1641
+ pixels rather than price so it feels identical at every zoom — a price-space
1642
+ tolerance would snap from far away on a zoomed-out chart and never trigger on a
1643
+ zoomed-in one.
1644
+
1645
+ **Keep-drawing mode routes through one place.** There are four separate
1646
+ completion paths in the drawing manager; a mode that held for only some of them
1647
+ would look broken in exactly the cases a user reached for it.
1648
+
1649
+ **Copy-to-all is announced, not performed.** The chart emits `drawingCreated`
1650
+ with `copyToAll`, because only the host knows which other panes exist:
1651
+
1652
+ ```ts
1653
+ chart.on('drawingCreated', ({ drawing, copyToAll }) => {
1654
+ if (copyToAll) group.broadcastDrawing(drawing, thisPaneId);
1655
+ });
1656
+ ```
1657
+
1658
+ `ChartGroup.broadcastDrawing(drawing, fromPaneId?)` hands the payload to every
1659
+ other pane's `ChartEngine.addDrawing(drawing)`. Unlike `broadcastSymbol` there
1660
+ is no `LayoutSyncState` flag gating it — the toggle already lives on the chart
1661
+ the user drew on, and that is what `copyToAll` reports.
1662
+
1663
+ `addDrawing` **appends and is undoable**, where `setState` would replace the
1664
+ list and flatten the target pane's history. Each pane's copy also gets a fresh
1665
+ id, so dragging the mirrored drawing in one pane leaves the others alone.
1666
+
1667
+ **Zoom In is a drawing internally.** It reuses the two-point press-preview-release
1668
+ gesture rather than duplicating it as a bespoke drag mode, then applies the zoom
1669
+ and removes itself — so nothing is left on the chart and it never reaches
1670
+ `ChartState`. It sets *both* axes: zooming only time would leave the chosen
1671
+ prices no larger than before, which is not what dragging a box asks for.
1672
+
1673
+ ### Native wrapper parity
1674
+
1675
+ `'icon'` and `'zoomIn'` are `DrawingToolType` strings and need no wrapper change.
1676
+ The toolbar options are new bridge commands — see
1677
+ [charts-android](../charts-android) and [charts-ios](../charts-ios).
1678
+
1679
+ ---
1680
+
1681
+ ## Notes & Text
1682
+
1683
+ Six annotation tools, all single-click: place, then type.
1684
+
1685
+ | Tool | `DrawingToolType` | Form |
1686
+ | --- | --- | --- |
1687
+ | **Anchored Text** | `'anchoredText'` | Free text with a dot on the bar and price it refers to. |
1688
+ | **Note** | `'note'` | A compact page marker with the text beside it. |
1689
+ | **Pin** | `'pin'` | A teardrop whose **tip** sits exactly on the price. |
1690
+ | **Table** | `'table'` | Rows of text in a bordered grid. |
1691
+ | **Comment** | `'comment'` | A rounded speech bubble with its tail on the anchor. |
1692
+ | **Signpost** | `'signpost'` | A plaque on a post, rising clear of the candles. |
1693
+
1694
+ ```ts
1695
+ chart.setDrawingTool('signpost');
1696
+ ```
1697
+
1698
+ All six anchor to a bar and price like every other drawing, so they re-anchor
1699
+ correctly after a data reload and serialise into `ChartState` unchanged.
1700
+
1701
+ ### Editing
1702
+
1703
+ | | |
1704
+ | --- | --- |
1705
+ | **Place** | Click — the editor opens focused |
1706
+ | **Re-edit** | Select, then double-click |
1707
+ | **New line** | `Enter` (multi-line tools) |
1708
+ | **Commit** | `Ctrl`/`⌘`+`Enter`, or click away |
1709
+ | **Discard** | `Escape` |
1710
+
1711
+ **Anchored Text, Note, Table, Comment and Signpost get a multi-line editor**;
1712
+ Text, Callout, Anchored Note and Pin keep the single-line field where `Enter`
1713
+ commits, exactly as before. A Table is unusable without this — its rows *are*
1714
+ newlines, so `Enter` has to insert one.
1715
+
1716
+ The box grows with its content as you type, so adding rows is visible rather
1717
+ than feeling like it has a row limit.
1718
+
1719
+ ### Why these are separate tools rather than one with options
1720
+
1721
+ The shape *is* the choice. Someone reaching for a Pin wants a precise marker at
1722
+ a level; someone reaching for a Signpost wants a dated flag that does not cover
1723
+ the candles; someone reaching for a Table wants several values at once.
1724
+ Collapsing them into "text with a style dropdown" would make each one harder to
1725
+ reach than the thing it replaced.
1726
+
1727
+ ### Details worth knowing
1728
+
1729
+ **Table syntax** — newlines are rows and `|` separates columns, so a table is
1730
+ typed into the same single text field as everything else rather than needing a
1731
+ bespoke editor. The first row is tinted as a header; short rows are padded so the
1732
+ grid stays rectangular, and empty cells are kept (clearing a value is editing the
1733
+ table, not asking for the column to vanish).
1734
+
1735
+ **A new table arrives already a grid** — four rows by two columns, with the
1736
+ syntax visible in the editor:
1737
+
1738
+ ```
1739
+ Level | Price
1740
+ Entry |
1741
+ Stop |
1742
+ Target |
1743
+ ```
1744
+
1745
+ That is deliberate. An empty table rendered a single placeholder row and opened a
1746
+ blank editor, so nothing told you rows are newlines and columns are `|` — the
1747
+ tool looked as though it had no rows or columns at all. Overtyping a grid is also
1748
+ faster than building one from nothing.
1749
+
1750
+ **Pin anchors by its tip, not its centre.** A marker whose middle sits on the
1751
+ level is ambiguous about which price it means.
1752
+
1753
+ **Anchored Text vs Text** — plain `text` floats with no indication of what it
1754
+ refers to. Anchored Text draws its anchor, so a note about one specific candle
1755
+ still points at that candle after panning and zooming.
1756
+
1757
+ **Comment vs Callout** — `callout` is a square-cornered box with a straight
1758
+ pointer; `comment` is the softer rounded bubble. Both are kept because the
1759
+ difference is exactly what the user is choosing between.
1760
+
1761
+ Empty tools render a placeholder (`Text`, `Note`, `Event`…) rather than nothing,
1762
+ so a drawing placed but not yet typed into is still visible and selectable.
1763
+
1764
+ ### Native wrapper parity
1765
+
1766
+ All six are `DrawingToolType` strings, so `setDrawingTool('pin')` works on
1767
+ [charts-android](../charts-android) and [charts-ios](../charts-ios) with no
1768
+ wrapper change.
1769
+
1770
+ ---
1771
+
1772
+ ## Indicator Library
1773
+
1774
+ **78 studies added**, taking the library from 30 to 108.
1775
+
1776
+ ```ts
1777
+ chart.addIndicatorByName('TRIX');
1778
+ chart.addIndicatorByName('Vortex');
1779
+ ```
1780
+
1781
+ They appear in the Indicators flyout alongside the originals, grouped by the
1782
+ same categories, and each is configurable through the settings dialog.
1783
+
1784
+ ### How they are registered
1785
+
1786
+ The original thirty are registered by hand in three places — a preset list, a
1787
+ param-schema list, and `indicators.json`. Repeating that seventy-eight times
1788
+ would be seventy-eight chances to register an indicator in two places and not
1789
+ the third, which fails *silently*: the tool appears in the menu and does
1790
+ nothing, or computes and never appears.
1791
+
1792
+ So each new indicator is declared once in `indicators/registry.ts` and the three
1793
+ views are derived:
1794
+
1795
+ ```ts
1796
+ {
1797
+ shortName: 'TRIX', name: 'TRIX', type: 'Momentum',
1798
+ fields: [periodField(18), colorField('#00BFA5'), sourceField()],
1799
+ factory: (p) => new SimpleOscillator('TRIX', p.color, …),
1800
+ }
1801
+ ```
1802
+
1803
+ ### Shared foundations
1804
+
1805
+ | Module | What it holds |
1806
+ | --- | --- |
1807
+ | `indicators/math.ts` | `sma`, `ema`, `rma`, `wma`, `hma`, `vwma`, `alma`, `dema`, `tema`, `mcginley`, `lsma`, `stdev`, `standardError`, `correlation`, `atr`, … |
1808
+ | `indicators/base.ts` | `LineIndicatorBase`, `BandIndicatorBase`, `HistogramIndicatorBase` |
1809
+
1810
+ All exported, so a host can compute the same series off-chart.
1811
+
1812
+ Two conventions the maths holds to throughout:
1813
+
1814
+ - **`null` means undefined and propagates.** An EMA of an RSI is null wherever
1815
+ the RSI is. Returning `0` instead would draw a line through a period where the
1816
+ indicator does not exist, which reads as data and is not.
1817
+ - **`stdev` is the population form**, because Bollinger Bands and everything
1818
+ derived from them are defined that way; using `n−1` shifts every band visibly
1819
+ on short periods.
1820
+
1821
+ ### The comparative studies
1822
+
1823
+ **Correlation Coefficient**, **Correlation – Log**, **Ratio**, **Spread** and
1824
+ **Advance/Decline** are undefined against a single series — they need a second
1825
+ instrument. `IIndicator.compute` gained an optional second argument for this:
1826
+
1827
+ ```ts
1828
+ compute(data: OHLCVBar[], context?: IndicatorComputeContext): IndicatorResult[]
1829
+ ```
1830
+
1831
+ `context.compares` carries the bars for each symbol added through the Compare
1832
+ feature. The parameter is optional, so every existing indicator is unaffected.
1833
+
1834
+ **With no compare symbol added, these draw nothing.** Substituting a stand-in
1835
+ series — correlating price against its own average, say — would produce a
1836
+ confident-looking line answering a different question than the indicator's name
1837
+ promises.
1838
+
1839
+ ### Two things worth knowing
1840
+
1841
+ **Volume Profile Visible Range** distributes each bar's real volume across the
1842
+ price levels that bar actually traded through, and marks the Point of Control.
1843
+ The existing `anchoredVP` *drawing tool* is a different thing: its bar heights
1844
+ come from a hardcoded bell curve, not from the feed.
1845
+
1846
+ **`maxSubPanes` caps concurrent sub-pane indicators.** Most of these 78 live in
1847
+ their own pane, so adding many at once silently stops at the cap. Raise
1848
+ `maxSubPanes` if you need more on screen together.
1849
+
1850
+ ### Native wrapper parity
1851
+
1852
+ All 78 are reachable through the existing `addIndicator` bridge command by short
1853
+ name — no wrapper change:
1854
+
1855
+ ```kotlin
1856
+ chartView.addIndicatorByName("TRIX")
1857
+ ```
1858
+
1859
+ ### What was added
1860
+
1861
+ | Group | Studies |
1862
+ | --- | --- |
1863
+ | Moving averages (14) | ALMA, DEMA, TEMA, LSMA, McGinley, SMMA, VWMA, Hamming, Adaptive (KAMA), MA Channel, MA Double, MA Triple, MA Multiple, Guppy |
1864
+ | Momentum (23) | Accelerator, ASI, Balance of Power, CMO, Connors RSI, Coppock, DPO, Fisher, KST, Majority Rule, Momentum, Price Oscillator, Rank Correlation, Relative Vigor, SMI Ergodic, TRIX, TSI, Ultimate, Correlation, Correlation Log, + crosses |
1865
+ | Volatility (16) | %B, BB Width, Historical, Close-to-Close, Zero Trend, O-H-L-C, Volatility Index, StdDev, StdError, StdError Bands, Choppiness, Chop Zone, Chaikin Volatility, Mass Index, RVI, Envelopes |
1866
+ | Trend (12) | Aroon, DMI, Vortex, Trend Strength, LinReg Slope, Price Channel, Chande Kroll, 52W H/L, Alligator, Fractal, Zig Zag, MA/EMA crosses |
1867
+ | Volume (9) | Ease of Movement, Elder's Force, Klinger, Volume Oscillator, Net Volume, PVT, Chaikin Oscillator, Advance/Decline, VPVR |
1868
+ | Price transforms (4) | Average Price, Median Price, Typical Price, Ratio, Spread |
1869
+
1870
+ ---
1871
+
1872
+ ## Shapes & Arrows
1873
+
1874
+ ### Already shipped: the line arrow
1875
+
1876
+ **`'arrowMarker'`** is TradingView's Arrow — a line from tail to tip with a
1877
+ filled arrowhead. It was only ever *labelled* "Arrow Marker", which hid it. The
1878
+ label now reads **Arrow**.
1879
+
1880
+ The `DrawingToolType` string is unchanged, and labels are i18n-overridable
1881
+ through `labels.drawingToolbar.tools`, so nothing breaks — but it is a
1882
+ user-visible string change worth knowing about.
1883
+
1884
+ ### Arrow Mark Left / Right — `'arrowMarkLeft'`, `'arrowMarkRight'`
1885
+
1886
+ Single-click marks that point *at* a price level from the side — the horizontal
1887
+ siblings of `arrowUp` / `arrowDown`. Where those call out a move's direction,
1888
+ these call out a level: the arm runs along the price, so the mark reads as "this
1889
+ price, from the left" rather than "up from here".
1890
+
1891
+ Fixed pixel geometry, not price-space — a marker is chrome and should stay the
1892
+ same size whatever the axis is doing. The hit box covers the whole glyph, stem
1893
+ and head, so you grab what you can see.
1894
+
1895
+ ### Curve — `'curve'`
1896
+
1897
+ Two endpoints and a handle that bends the line between them: a **quadratic
1898
+ Bézier**, not a circular arc.
1899
+
1900
+ The existing `'arc'` tool fits a circle through three points, and a circle's
1901
+ curvature is constant — it cannot leave one endpoint steeply and arrive at the
1902
+ other gently. A Bézier can, which is what you want for sketching a rounding top
1903
+ or a decelerating trend.
1904
+
1905
+ One handle is exactly what a quadratic takes, so every position you can drag to
1906
+ maps to a curve, and every curve the tool can draw is reachable by dragging. A
1907
+ cubic would need a second handle to be fully controllable, or would hide half its
1908
+ shape. The handle and its guides are drawn only while selected.
1909
+
1910
+ ### Double Curve — `'doubleCurve'`
1911
+
1912
+ An S, from the same three clicks as a single Curve. The chord is split at its
1913
+ midpoint and each half gets its own quadratic, bent by equal and **opposite**
1914
+ offsets — so one handle controls both bends, and dragging across the chord
1915
+ inverts the S.
1916
+
1917
+ Deriving the second bend from the first, rather than asking for a fourth click,
1918
+ is the point: an S whose halves are set independently is just two curves. The
1919
+ shape only reads as one gesture if the halves stay symmetric, and making that
1920
+ automatic is cheaper for the user than making it their job.
1921
+
1922
+ ### Curve maths
1923
+
1924
+ Exported, so a host can reproduce the same geometry:
1925
+
1926
+ ```ts
1927
+ import { quadraticAt, controlFromOffset, offsetFromHandle } from 'acttrader-charts';
1928
+ ```
1929
+
1930
+ `controlFromOffset` and `offsetFromHandle` are **true inverses**, both speaking
1931
+ in *apex* distance — how far the curve itself lands from the chord, not how far
1932
+ the control point does. A quadratic's apex sits halfway between chord and
1933
+ control, so the conversion doubles in one direction and halves in the other.
1934
+
1935
+ ### Native wrapper parity
1936
+
1937
+ All four are `DrawingToolType` strings, so `setDrawingTool('doubleCurve')` works
1938
+ on [charts-android](../charts-android) and [charts-ios](../charts-ios) with no
1939
+ wrapper change.
1940
+
1941
+ ---
1942
+
1943
+ ## Projection & Measurement
1944
+
1945
+ ### Already shipped: Long Position / Short Position
1946
+
1947
+ `'longPosition'` and `'shortPosition'` exist today and are complete risk/reward
1948
+ tools: three prices (entry, target, stop) drawn as a green profit zone and a red
1949
+ risk zone, with the money and quantity the account's risk budget implies. You
1950
+ place entry and target; the stop is materialised from the default reward:risk, so
1951
+ the common case is two clicks rather than three.
1952
+
1953
+ They are gated behind `features.enableForecasting` and need `account` for money
1954
+ and quantity, and `instrument` for pips:
1955
+
1956
+ ```ts
1957
+ new ChartEngine({
1958
+ container,
1959
+ features: { enableForecasting: true },
1960
+ account: { equity: 50_000, riskPercent: 1 },
1961
+ });
1962
+ ```
1963
+
1964
+ Deliberately **not** Trade-From-Chart — `setLevels` draws real broker orders and
1965
+ committing one sends it. These are drawings; nothing here reaches the broker.
1966
+
1967
+ ### Anchored VWAP — `'anchoredVwap'`
1968
+
1969
+ Volume-weighted average price accumulated from a bar you pick: the average price
1970
+ everyone who traded since the anchor actually paid, which is what turns a swing
1971
+ high or an earnings gap into support.
1972
+
1973
+ **This is not the existing `'anchoredVP'`.** That one is Anchored Volume
1974
+ *Profile* — a horizontal histogram of volume by price level, and its bar heights
1975
+ come from a hardcoded bell curve rather than from the feed. Anchored VWAP is a
1976
+ single line computed from real volume.
1977
+
1978
+ Placed with one click, re-accumulating as data streams in. The series is
1979
+ exported:
1980
+
1981
+ ```ts
1982
+ import { anchoredVwapSeries } from 'acttrader-charts';
1983
+
1984
+ const vwap = anchoredVwapSeries(bars, anchorIndex); // number[] | null
1985
+ ```
1986
+
1987
+ Two deliberate behaviours:
1988
+
1989
+ - It uses the **(H+L+C)/3 typical price**, the convention VWAP is defined on —
1990
+ not the close, which would make it a volume-weighted close.
1991
+ - It returns **`null` when the span carries no volume**, and the chart draws no
1992
+ line. A VWAP without volume silently degrades into a running mean of typical
1993
+ prices: it looks like a VWAP and is not one. A volume-less feed gets no line
1994
+ rather than a plausible wrong one. The anchor dot is still drawn, so the
1995
+ drawing stays selectable and deletable.
1996
+
1997
+ ### Forecast — `'forecast'`
1998
+
1999
+ Three points: p1→p2 is what happened, p2→p3 is what you expect next. The base is
2000
+ solid, the forecast dashed, and a shaded cone opens from zero at p2 to half the
2001
+ base move's size at p3 — because a projection is exact where it starts and vague
2002
+ where it ends, and a bare line implies a precision no forecast has. A readout
2003
+ gives the projected change in price, percent and bars.
2004
+
2005
+ That cone is what separates it from the existing `'projection'` tool, which
2006
+ extrapolates a trend line to the right edge with no second leg you control and no
2007
+ statement of confidence.
2008
+
2009
+ ### Bars Pattern — `'barsPattern'`
2010
+
2011
+ Three points: p1 and p2 bracket the bars to copy, p3 anchors the copy. It draws
2012
+ real candles from the source bars, re-based by price **offset** rather than ratio
2013
+ — the tool is for asking "the same move, from here", not "a proportional move".
2014
+
2015
+ The copy is drawn at reduced alpha inside a dashed bounding box. Without that it
2016
+ renders in the chart's own candle colours and becomes indistinguishable from real
2017
+ data, which is worse than useless.
2018
+
2019
+ ### Native wrapper parity
2020
+
2021
+ All three new tools are `DrawingToolType` strings, so `setDrawingTool('forecast')`
2022
+ works on [charts-android](../charts-android) and [charts-ios](../charts-ios) with
2023
+ no wrapper change. Long/Short Position need `enableForecasting` in the native
2024
+ `init` payload, which both wrappers already expose.
2025
+
2026
+ ---
2027
+
2028
+ ## Patterns
2029
+
2030
+ ### XABCD Pattern — `'xabcdPattern'`
2031
+
2032
+ The free-form harmonic, and the only one that shows its working.
2033
+
2034
+ Every named harmonic in the library — Gartley, Bat, Butterfly, Crab, Shark,
2035
+ Cypher — draws the identical X→A→B→C→D zigzag and dashed XD line, differing only
2036
+ in colour. None of them measures or validates anything. XABCD assumes nothing
2037
+ instead: you place five pivots and it reports the ratios those pivots actually
2038
+ produce, so you can decide which harmonic the structure matches.
2039
+
2040
+ That measurement is the whole point. Without it this would be an existing tool
2041
+ under a new name.
2042
+
2043
+ | Ratio | Measured against | Why it matters |
2044
+ | --- | --- | --- |
2045
+ | `AB/XA` | the initial leg | the first retracement |
2046
+ | `BC/AB` | the AB leg | the counter-move |
2047
+ | `CD/BC` | the BC leg | the final leg's extension |
2048
+ | `AD/XA` | the whole XA move | what separates a Gartley (0.786) from a Butterfly (1.27) |
2049
+
2050
+ It also shades the XAB and BCD triangles, which is what makes the shape readable
2051
+ at a glance — the zigzag alone is just four lines.
2052
+
2053
+ The arithmetic is exported, so a host can auto-classify a pattern with the same
2054
+ numbers the chart draws:
2055
+
2056
+ ```ts
2057
+ import { xabcdRatios } from 'acttrader-charts';
2058
+
2059
+ const chips = xabcdRatios(drawing.points);
2060
+ // → [{ from: 1, to: 2, text: 'AB/XA 0.600' }, …]
2061
+ ```
2062
+
2063
+ A leg of zero length has no defined ratio against it, so that entry is **omitted**
2064
+ rather than printed as `Infinity` or `NaN`, and ratios for pivots you have not
2065
+ placed yet simply do not appear.
2066
+
2067
+ ### Elliott Triple Combo (WXYXZ) — `'elliottTripleCombo'`
2068
+
2069
+ The triple three: three corrective structures joined by two X waves, six points
2070
+ from an origin.
2071
+
2072
+ There are now three neighbouring tools and the distinction is exact:
2073
+
2074
+ | Tool | Points | Labels |
2075
+ | --- | --- | --- |
2076
+ | `'elliottWxy'` | 4 | the **double** three — W, X, Y |
2077
+ | `'elliottCombination'` | 7 | W, X, Y, X2, Z, X3, end |
2078
+ | `'elliottTripleCombo'` | 6 | 0, W, X, Y, X, Z |
2079
+
2080
+ You asked whether the existing Combination tool already covers this. It partly
2081
+ does — but it takes **seven** points where a triple three has six, and numbers its
2082
+ connectors `X2`/`X3` where Elliott notation leaves both as `X`. A trader counting
2083
+ a triple three places five waves after the origin, and a tool demanding six
2084
+ leaves them with a pivot to park somewhere meaningless. This tool matches the
2085
+ count and the notation; the Combination tool is unchanged.
2086
+
2087
+ Wave labels sit above a peak and below a trough, so they never land on the line
2088
+ that produced them.
2089
+
2090
+ ### Native wrapper parity
2091
+
2092
+ Both are new `DrawingToolType` strings, so `setDrawingTool('xabcdPattern')` works
2093
+ on [charts-android](../charts-android) and [charts-ios](../charts-ios) with no
2094
+ wrapper change.
2095
+
2096
+ ---
2097
+
2098
+ ## Fibonacci & Gann
2099
+
2100
+ Five tools added for TradingView parity. Each exists because it answers a
2101
+ question an existing tool in the same group cannot.
2102
+
2103
+ | Tool | `DrawingToolType` | Points | vs. its neighbour |
2104
+ | --- | --- | --- | --- |
2105
+ | **Trend-Based Fib Time** | `'trendBasedFibTime'` | 3 | Fib Timezone measures from its own origin. This projects one leg's *duration* onto a different starting bar. |
2106
+ | **Fib Speed Resistance Arcs** | `'fibSpeedResistanceArcs'` | 2 | The Fan projects straight lines, so implied support stays proportional forever. Arcs curve, so it decays with distance. |
2107
+ | **Fib Wedge** | `'fibWedge'` | 3 | Fib Circles sweep a full turn from one centre. A wedge is bounded by two user-drawn rays. |
2108
+ | **Pitchfan** | `'pitchfan'` | 3 | A Pitchfork's tines run parallel — a fixed channel. A Pitchfan's radiate, so they spread with distance. |
2109
+ | **Gann Square Fixed** | `'gannSquareFixed'` | 2 | The plain Gann Square stretches to the drag box, so its diagonal is only 45° at one zoom. |
2110
+
2111
+ ```ts
2112
+ chart.setDrawingTool('fibWedge');
2113
+ ```
2114
+
2115
+ ### Details that matter
2116
+
2117
+ **Trend-Based Fib Time** — p1→p2 is the base leg and p3 is where projection
2118
+ starts, so the verticals answer *"if this leg took N bars, when do the Fibonacci
2119
+ multiples of N fall from here?"*. Lines outside the viewport are skipped rather
2120
+ than stroked and clipped. It stays inert until the third point lands and when the
2121
+ base leg has zero duration — there is nothing to project in either case.
2122
+
2123
+ **Fib Speed Resistance Arcs** — drawn as **ellipses**, not circles, with the x and
2124
+ y radii taken separately from the trend's run and rise. A pixel-space circle
2125
+ would change shape the moment the price axis was rescaled, which makes the tool
2126
+ meaningless on a chart the user has zoomed. Only the half facing the trend is
2127
+ drawn; the half behind the origin would imply resistance before the move began.
2128
+
2129
+ **Fib Wedge** — the sweep always takes the short way round, so a wedge never
2130
+ flips to the reflex angle when an edge crosses the ±π seam. Hit testing checks
2131
+ the angular span first, so a click at a matching radius but outside the wedge
2132
+ correctly misses.
2133
+
2134
+ **Pitchfan** — rays are extended *forward* past the base, since that is the only
2135
+ part anyone trades off. Handle and base are dashed: construction, not signal.
2136
+
2137
+ **Gann Square Fixed** — takes the **longer** side of the drag and uses it for
2138
+ both, so the 45° diagonal really is 45°. That is the premise of Gann's angles and
2139
+ worthless if the shape skews with the axes. The cost is that the box no longer
2140
+ matches the drag exactly — "fixed" means fixed *proportion*, not fixed size.
2141
+
2142
+ ### Native wrapper parity
2143
+
2144
+ All five are new `DrawingToolType` strings, so `setDrawingTool('pitchfan')` works
2145
+ on [charts-android](../charts-android) and [charts-ios](../charts-ios) with no
2146
+ wrapper change.
2147
+
2148
+ ---
2149
+
2150
+ ## Cursors & Line Tools
2151
+
2152
+ ### Extending lines
2153
+
2154
+ Four tools that differ from a Trend Line only in where they stop being drawn —
2155
+ which is exactly why they exist as separate tools rather than a style option.
2156
+
2157
+ | Tool | `DrawingToolType` | What it does |
2158
+ | --- | --- | --- |
2159
+ | **Ray** | `'ray'` | Two anchors; continues past the second. |
2160
+ | **Extended Line** | `'extendedLine'` | Two anchors; continues past both. |
2161
+ | **Horizontal Ray** | `'horizontalRay'` | One click; a level that runs **forward only**. |
2162
+ | **Info Line** | `'infoLine'` | A trend line that permanently reports what it spans. |
2163
+
2164
+ ```ts
2165
+ chart.setDrawingTool('ray');
2166
+ ```
2167
+
2168
+ The extension is recomputed against the canvas every frame, so panning and
2169
+ zooming re-extend the line instead of dragging a stale endpoint around. Hit
2170
+ testing uses the *extended* segment, so the infinite part is clickable too — the
2171
+ user can grab what they can see. Anchor dots are drawn only at the anchors,
2172
+ never at the extended ends, because a dot at the canvas edge reads as a handle
2173
+ and there is nothing there to grab.
2174
+
2175
+ **Horizontal Ray vs Horizontal Line** is the one worth calling out: a level drawn
2176
+ from the swing high that formed it should not be painted back across the history
2177
+ that preceded it, because that implies support which did not yet exist.
2178
+
2179
+ **Info Line vs Ruler**: the Ruler is a transient measurement — drawn, read,
2180
+ discarded. An Info Line is a drawing you keep, so its readout travels with it and
2181
+ updates as either end is dragged. It reports price delta, percent, bar count,
2182
+ and — when `instrument` and bar timestamps are available — pips and calendar
2183
+ duration. Each is omitted rather than shown as a zero when it cannot be computed.
2184
+
2185
+ ### Cursors
2186
+
2187
+ Pointer behaviour over the plot, **independent of the drawing tool**: switching
2188
+ mode never cancels a drawing in progress, and arming a tool never changes the
2189
+ mode.
2190
+
2191
+ | `CursorMode` | Behaviour |
2192
+ | --- | --- |
2193
+ | `'cross'` | The default crosshair with its axis readouts. |
2194
+ | `'dot'` | A dot follows the pointer instead of two lines cutting across the candles. |
2195
+ | `'arrow'` | A plain pointer, no crosshair. |
2196
+ | `'demonstration'` | A laser trail that fades behind the pointer, for screen-sharing. Draws nothing permanent and creates no drawing. |
2197
+ | `'eraser'` | A click deletes the drawing under it. |
2198
+
2199
+ ```ts
2200
+ const chart = new ChartEngine({
2201
+ container,
2202
+ cursorMode: 'cross', // initial mode
2203
+ features: { enableCursorModes: true }, // adds the Cursors group to the toolbar
2204
+ });
2205
+
2206
+ chart.setCursorMode('eraser');
2207
+ chart.on('cursorModeChange', ({ mode }) => persist(mode));
2208
+ ```
2209
+
2210
+ `features.enableCursorModes` is a gate because it adds a **category** to a
2211
+ toolbar that hosts have laid out around its current contents. With it off the
2212
+ group is absent and the toolbar is exactly what it was — and the modes are still
2213
+ reachable from `setCursorMode()`, so a host can drive them from its own chrome
2214
+ without taking the group.
2215
+
2216
+ Two details worth knowing:
2217
+
2218
+ - **A drawing tool owns the cursor while armed.** It needs crosshair precision
2219
+ whatever the mode says; clearing the tool hands the cursor back to the mode.
2220
+ - **The eraser consumes every click**, hit or miss. A miss must not fall through
2221
+ to selecting or panning — that would be a surprising thing to do with an eraser
2222
+ in hand. It deletes via select-then-delete, so it shares the undo history with
2223
+ every other delete.
2224
+
2225
+ ### Value tooltip on long press
2226
+
2227
+ ```ts
2228
+ const chart = new ChartEngine({ container, valueTooltip: true });
2229
+ ```
2230
+
2231
+ A floating OHLCV readout beside a long press, on top of the crosshair that
2232
+ gesture already arms. The OHLC strip at the top of the chart carries the same
2233
+ numbers, but on a phone it is at the opposite end of the screen from the finger —
2234
+ the user has to look away from the thing they are pointing at.
2235
+
2236
+ It flips to the other side of the touch rather than running off an edge, never
2237
+ takes pointer events (the drag is still in progress), and omits volume rather
2238
+ than printing a zero when the feed carries none. Default `false`.
2239
+
2240
+ ### Native wrapper parity
2241
+
2242
+ The four line tools are new `DrawingToolType` strings, so `setDrawingTool('ray')`
2243
+ works on [charts-android](../charts-android) and [charts-ios](../charts-ios)
2244
+ unchanged.
2245
+
2246
+ | JS | Android | iOS |
2247
+ | --- | --- | --- |
2248
+ | `setCursorMode(m)` | `setCursorMode(m)` | `setCursorMode(_:)` |
2249
+ | `cursorModeChange` | `BridgeEvent.CursorModeChange` | `.cursorModeChange` |
2250
+ | `cursorMode` at init | `Init(cursorMode = "…")` | `cursorMode:` argument |
2251
+ | `valueTooltip` | `Init(valueTooltip = true)` | `valueTooltip:` argument |
2252
+ | `features.enableCursorModes` | `Init(enableCursorModes = true)` | `enableCursorModes:` argument |
2253
+
2254
+ ---
2255
+
2256
+ ## Chart Types
2257
+
2258
+ Thirteen time-based types plus five added for TradingView parity. The five split
2259
+ into two kinds, and the difference matters more than the list suggests.
2260
+
2261
+ ### HLC bars — `'hlc'`
2262
+
2263
+ An ordinary per-bar series: the high-low range with a close tick and no open
2264
+ stub. Halves the ink of an OHLC bar, which is the point — on a dense chart the
2265
+ closes stay legible where full bars smear. Colour still comes from open vs close.
2266
+
2267
+ ### Price-transform types — `'renko'`, `'linebreak'`, `'kagi'`, `'pointfigure'`
2268
+
2269
+ These are **not drawn per time bar.** A brick, block, kagi segment or P&F column
2270
+ forms when *price* moves far enough, which may take fifty bars or none at all.
2271
+ 365 daily candles might be 24 Renko bricks or 15 P&F columns.
2272
+
2273
+ So they are not renderers — the bar array itself is rebuilt. Each transform turns
2274
+ raw OHLCV into **synthetic** bars, one per visual element, which the rest of the
2275
+ chart then treats as ordinary bars. That is what keeps the feature small:
2276
+ viewport, zoom, price range, crosshair, the time axis, indicators and drawings
2277
+ all keep working unchanged. It also means **an indicator on a Renko chart is
2278
+ computed over the bricks**, not the underlying candles — same as TradingView.
2279
+
2280
+ Three consequences worth knowing:
2281
+
2282
+ - **The time axis is non-linear.** Labels show when each element *completed*.
2283
+ Several elements completing on one bar get timestamps one millisecond apart so
2284
+ drawing anchors and viewport restore keep working.
2285
+ - **Switching in or out refits the view**, because the old viewport indexes a
2286
+ different number of elements.
2287
+ - **Market price still comes off the raw feed.** Trade levels and P&L price
2288
+ against the real last close, never a brick top.
2289
+
2290
+ ### Parameters
2291
+
2292
+ ```ts
2293
+ const chart = new ChartEngine({
2294
+ container,
2295
+ series: 'renko',
2296
+ seriesOptions: {
2297
+ renko: { boxSize: { kind: 'atr', length: 14 } }, // the default
2298
+ lineBreak: { lines: 3 },
2299
+ kagi: { reversal: { kind: 'percent', percent: 2 } },
2300
+ pointFigure: { boxSize: { kind: 'fixed', size: 0.5 }, reversal: 3 },
2301
+ },
2302
+ });
2303
+
2304
+ chart.setSeriesOptions({ renko: { boxSize: { kind: 'fixed', size: 5 } } });
2305
+ ```
2306
+
2307
+ Every box/reversal size defaults to **ATR(14)**, not a fixed price, because a
2308
+ fixed box is meaningless until you know the instrument: ten points is noise on an
2309
+ index and a lifetime on a forex pair. `setSeriesOptions` merges, so retuning
2310
+ Renko leaves Kagi and P&F alone.
2311
+
2312
+ | `BoxSizeMode` | Meaning |
2313
+ | --- | --- |
2314
+ | `{ kind: 'atr', length }` | Wilder ATR over the last `length` bars. Default `14`. |
2315
+ | `{ kind: 'fixed', size }` | A fixed price distance. |
2316
+ | `{ kind: 'percent', percent }` | A percentage of the latest close. |
2317
+
2318
+ When a box cannot be sized — fewer than `length + 1` bars for the ATR, or a
2319
+ non-positive size — the transform returns **no elements** rather than guessing. A
2320
+ wrong box size does not look wrong; it looks like a different market.
2321
+
2322
+ ### The rules each type implements
2323
+
2324
+ | Type | Rule |
2325
+ | --- | --- |
2326
+ | **Renko** | A brick prints when price closes a full box past the last brick's edge. Reversals need **two** boxes, not one — that two-box rule is what filters the noise. `source: 'highlow'` lets a bar's extremes print bricks instead of its close. |
2327
+ | **Line Break** | A block prints when the close clears the extreme of the last `lines` blocks (default 3). No box size at all — the reversal distance is whatever recent blocks span, so it widens automatically in volatile stretches. |
2328
+ | **Kagi** | **Direction** flips on a retrace of the reversal amount. **Thickness** flips on a break of the prior shoulder or waist. These are independent: a line can reverse repeatedly without ever changing thickness. |
2329
+ | **Point & Figure** | Columns of X's and O's on a box grid, built from each bar's **high and low** — high first in an X column, low first in an O column, so a bar that both extended and reversed is read in the order it happened. A new column needs `reversal` boxes (default 3). |
2330
+
2331
+ ### Using the transforms off-chart
2332
+
2333
+ They are exported as pure functions, so a backtest or a server-side render gets
2334
+ byte-identical elements:
2335
+
2336
+ ```ts
2337
+ import { toRenko, toKagi, atrValue } from 'acttrader-charts';
2338
+
2339
+ const { bars, meta } = toRenko(ohlcv, { boxSize: { kind: 'fixed', size: 5 } });
2340
+ ```
2341
+
2342
+ Each returns `{ bars, meta }`. `meta[i]` carries what cannot be read off the
2343
+ synthetic bar: Kagi's `thick`, P&F's `boxSize`, and `sourceBars` / `startTime`
2344
+ for every type.
2345
+
2346
+ ### Native wrapper parity
2347
+
2348
+ The five types are new values of the same `series` string, so `setSeries('renko')`
2349
+ works on [charts-android](../charts-android) and [charts-ios](../charts-ios)
2350
+ unchanged. Parameters travel as a JSON object:
2351
+
2352
+ | JS | Android | iOS |
2353
+ | --- | --- | --- |
2354
+ | `seriesOptions` at init | `Init(seriesOptionsJson = "…")` | `seriesOptionsJson:` init argument |
2355
+ | `setSeriesOptions(o)` | `setSeriesOptions(json)` | `setSeriesOptions(_:)` |
2356
+
2357
+ ---
2358
+
2359
+ ## Top-Toolbar Features
2360
+
2361
+ Four opt-in features that bring the header in line with TradingView's: **indicator
2362
+ templates**, **saved chart layouts**, **Quick Search**, and **chart-settings
2363
+ templates with "Apply to all charts"**. Compare and Snapshot already shipped — see
2364
+ [Multi-Pane Layouts](#multi-pane-layouts) and the Compare section.
2365
+
2366
+ Two rules apply to all of them:
2367
+
2368
+ - **Nothing moves.** Every gate defaults to `false`, and with them off the chart's
2369
+ DOM is unchanged. When enabled, each feature extends a flyout, popover or dialog
2370
+ that already exists — no feature adds a header button (Quick Search can, but only
2371
+ if you ask for one).
2372
+ - **The library persists nothing.** Saving emits an event carrying the complete
2373
+ object; your app stores it and seeds it back. This is the same contract
2374
+ `stateChange` follows, and it is what lets Android and iOS reuse the feature
2375
+ without reimplementing storage twice.
2376
+
2377
+ ### Indicator templates
2378
+
2379
+ Named, reusable indicator sets, listed in a **Templates** block at the foot of the
2380
+ indicators flyout.
2381
+
2382
+ ```ts
2383
+ const chart = new ChartEngine({
2384
+ container,
2385
+ enableIndicatorTemplates: true,
2386
+ indicatorTemplates: loadFromStorage(), // what you saved last session
2387
+ });
2388
+
2389
+ chart.on('indicatorTemplateSaved', ({ template }) => persist(template));
2390
+ chart.on('indicatorTemplateDeleted', ({ id }) => forget(id));
2391
+ ```
2392
+
2393
+ | API | Does |
2394
+ | --- | --- |
2395
+ | `captureIndicatorTemplate(name)` | Saves the active indicators; emits `indicatorTemplateSaved`. Returns the template or `null`. |
2396
+ | `applyIndicatorTemplate(idOrName)` | Replaces the active indicators with the template's. Returns `false` if nothing matched. |
2397
+ | `deleteIndicatorTemplate(idOrName)` | Removes it; emits `indicatorTemplateDeleted`. |
2398
+ | `setIndicatorTemplates(list)` | Replaces the list the flyout shows. |
2399
+ | `getIndicatorTemplates()` | Reads it back (copies). |
2400
+
2401
+ Saving twice under one name yields two templates — ids are de-duplicated
2402
+ (`my-set`, `my-set-2`), which is what users expect from "Save as".
2403
+
2404
+ Rendered by the default `TopBar` and by `AdvancedToolbar`. The compact per-pane
2405
+ strip and the mobile header have no full indicators flyout, so they skip it.
2406
+
2407
+ ### Saved chart layouts
2408
+
2409
+ A **saved layout** is a grid preset *plus* every pane's full `ChartState` — symbol,
2410
+ timeframe, series, indicators, drawings, viewport, compares. That is the difference
2411
+ from the preset grid already in the layout popover, which only picks a *shape*.
2412
+
2413
+ Requires `enableMultipleLayouts`, since the section renders inside that popover.
2414
+
2415
+ ```ts
2416
+ const chart = new ChartEngine({
2417
+ container,
2418
+ enableMultipleLayouts: true,
2419
+ enableSavedLayouts: true,
2420
+ savedLayouts: loadFromStorage(),
2421
+ });
2422
+
2423
+ chart.on('layoutSaved', ({ layout }) => persist(layout));
2424
+ chart.on('layoutApplied', ({ layout }) => mountPanes(layout)); // multi-pane hosts
2425
+ ```
2426
+
2427
+ | API | Does |
2428
+ | --- | --- |
2429
+ | `captureSavedLayout(name, paneId?)` | Saves the preset + this chart's state. Single-engine: one pane. |
2430
+ | `applySavedLayout(idOrName, paneId?)` | Applies the preset (emits `layoutChange`) then this pane's state. |
2431
+ | `deleteSavedLayout(idOrName)` | Removes it; emits `layoutDeleted`. |
2432
+ | `setSavedLayouts(list)` / `getSavedLayouts()` | Replace / read the list. |
2433
+
2434
+ For a real multi-pane grid use `ChartGroup`, which sees every pane:
2435
+
2436
+ ```ts
2437
+ const layout = group.captureLayout('My Desk', chart.getLayoutPreset());
2438
+ persist(layout);
2439
+ for (const [, engine] of group.entries()) engine.setSavedLayouts(allLayouts);
2440
+
2441
+ // restoring: mount the grid for layout.presetId yourself, then
2442
+ const { applied, missing } = group.applyLayout(layout);
2443
+ ```
2444
+
2445
+ `applyLayout` skips panes that aren't registered yet and returns them in `missing`,
2446
+ so a host that mounts lazily can create them and call again. Registered panes that
2447
+ the layout doesn't mention are left alone rather than cleared.
2448
+
2449
+ ### Quick Search
2450
+
2451
+ A command palette over everything the chart can do: series types, timeframes,
2452
+ durations, indicators, drawing tools, saved templates and layouts, and the header
2453
+ actions.
2454
+
2455
+ ```ts
2456
+ const chart = new ChartEngine({ container, enableQuickSearch: true });
2457
+ ```
2458
+
2459
+ Opened with **Ctrl/⌘+K**, with **`/`**, or by `chart.openQuickSearch()`. It is
2460
+ keyboard-first and **adds no header button** — pass
2461
+ `quickSearch: { showButton: true }` if you want one. A bare `/` is ignored while
2462
+ the user is typing in any input, textarea, select, or contenteditable.
2463
+
2464
+ Add your own commands beside the built-ins:
2465
+
2466
+ ```ts
2467
+ chart.registerCommand({
2468
+ id: 'myapp.openTicket',
2469
+ label: 'Open order ticket',
2470
+ group: 'My App',
2471
+ keywords: ['buy', 'sell', 'trade'],
2472
+ run: () => openTicket(),
2473
+ });
2474
+
2475
+ chart.on('quickSearchCommand', ({ id }) => track(id));
2476
+ ```
2477
+
2478
+ Namespace your ids so a future built-in can't collide. `matchCommands`,
2479
+ `scoreCommand` and `groupCommands` are exported if you want the same ranking in
2480
+ your own UI: exact label beats prefix beats word-start beats substring, with
2481
+ keyword and group hits last, and ties keep their original order so the list never
2482
+ reshuffles unpredictably as the user types.
2483
+
2484
+ ### Chart-settings templates + Apply to all charts
2485
+
2486
+ Named snapshots of everything the Chart Settings dialog edits, plus a switch to
2487
+ push the current settings to every other chart. Both render in the dialog's
2488
+ footer — no new button anywhere.
2489
+
2490
+ ```ts
2491
+ const chart = new ChartEngine({
2492
+ container,
2493
+ enableSettingsTemplates: true,
2494
+ settingsTemplates: loadFromStorage(),
2495
+ });
2496
+
2497
+ chart.on('settingsTemplateSaved', ({ template }) => persist(template));
2498
+
2499
+ chart.on('chartSettingsApplied', ({ settings, applyToAll }) => {
2500
+ // This chart already applied them. Only the fan-out is yours:
2501
+ if (applyToAll) group.applySettingsToAll(settings, thisPaneId);
2502
+ });
2503
+ ```
2504
+
2505
+ | API | Does |
2506
+ | --- | --- |
2507
+ | `getChartSettings()` / `applyChartSettings(s)` | Read / apply a settings snapshot. Every route — dialog, template, apply-to-all, bridge — goes through `applyChartSettings`. |
2508
+ | `captureSettingsTemplate(name)` | Saves the current settings; emits `settingsTemplateSaved`. |
2509
+ | `applySettingsTemplate(idOrName)` | Applies a saved one. |
2510
+ | `deleteSettingsTemplate(idOrName)` | Removes it. |
2511
+ | `setSettingsTemplates(list)` / `getSettingsTemplates()` | Replace / read the list. |
2512
+ | `ChartGroup.applySettingsToAll(settings, fromPaneId?)` | Applies to every other pane. Not gated on a `sync` toggle — it is an explicit one-shot action, not continuous mirroring. |
2513
+
2514
+ ### Native wrapper parity
2515
+
2516
+ All four features, plus snapshot capture, are exposed by
2517
+ [charts-android](../charts-android) and [charts-ios](../charts-ios). Templates and
2518
+ layouts cross the bridge as opaque JSON strings: the app stores what the chart
2519
+ handed it and sends the same string back, so neither Kotlin nor Swift has to mirror
2520
+ a `ChartState` that only the chart interprets.
2521
+
2522
+ | JS | Android | iOS |
2523
+ | --- | --- | --- |
2524
+ | `enableIndicatorTemplates` | `Init(enableIndicatorTemplates = true)` | `HeaderFeatures(enableIndicatorTemplates: true)` |
2525
+ | `captureIndicatorTemplate(name)` | `captureIndicatorTemplate(name)` | `captureIndicatorTemplate(_:)` |
2526
+ | `enableSavedLayouts` | `Init(enableSavedLayouts = true)` | `HeaderFeatures(enableSavedLayouts: true)` |
2527
+ | `enableQuickSearch` / `openQuickSearch()` | `openQuickSearch()` | `openQuickSearch()` |
2528
+ | `enableSettingsTemplates` | `Init(enableSettingsTemplates = true)` | `HeaderFeatures(enableSettingsTemplates: true)` |
2529
+ | `chartSettingsApplied` event | `BridgeEvent.ChartSettingsApplied` | `.chartSettingsApplied` |
2530
+ | `chart.toDataUrl()` | `requestSnapshot()` → `BridgeEvent.Snapshot` | `requestSnapshot()` → `.snapshot` |
2531
+
2532
+ On mobile there is no Ctrl/⌘+K, so Quick Search is opened with the
2533
+ `openQuickSearch` command from your own toolbar.
2534
+
2535
+ ---
2536
+
2537
+ ## Chart Settings
2538
+
2539
+ Six settings TradingView has and ActCharts did not, behind
2540
+ `features.enableChartSettings`. The gate adds three tabs — **Status Line**,
2541
+ **Scales**, **Canvas** — to the Chart Settings dialog and a bar-colouring choice
2542
+ to Appearance. Every one of them is also a `ChartConfig` option and a
2543
+ `ChartEngine` method, so a host can drive them from its own chrome and leave the
2544
+ dialog as it was.
2545
+
2546
+ ```ts
2547
+ const chart = new ChartEngine({
2548
+ container,
2549
+ features: { enableChartSettings: true },
2550
+ statusLine: { barChange: true },
2551
+ scales: { highLowLabels: true, pricePrecision: 4, timezone: 'Asia/Tokyo' },
2552
+ canvas: { gridVertical: false, watermarkVisible: true },
2553
+ barColorSource: 'previousClose',
2554
+ });
2555
+ ```
2556
+
2557
+ ### Status line
2558
+
2559
+ What the legend above the chart shows.
2560
+
2561
+ | Field | Default | Shows |
2562
+ | --- | --- | --- |
2563
+ | `symbol` | `true` | The instrument name and its stream dot |
2564
+ | `ohlc` | `true` | O / H / L / C of the hovered bar |
2565
+ | `barChange` | **`false`** | Absolute and percent change, e.g. `+1.68 (+0.96%)` |
2566
+ | `volume` | `true` | V, when the feed carries it |
2567
+
2568
+ `barChange` is the one default of `false`: it is a new readout, and turning it on
2569
+ by itself would change a legend hosts have laid out around. **Change is measured
2570
+ against the previous bar's close**, which is what "the change" means on every
2571
+ quote screen — against this bar's open it would only restate the candle's colour.
2572
+
2573
+ ### Scales and lines
2574
+
2575
+ ```ts
2576
+ chart.setScalesSettings({ lastPriceLabel: false, highLowLabels: true });
2577
+ ```
2578
+
2579
+ | Field | Default | Effect |
2580
+ | --- | --- | --- |
2581
+ | `lastPriceLabel` | `true` | The live price tag and its dashed line |
2582
+ | `bidLabel` / `askLabel` | follows config | The bid / ask tags |
2583
+ | `highLowLabels` | `false` | Tags at the extremes of the **visible** range |
2584
+ | `timeAxisCountdown` | follows `showCandleCountdown` | Countdown on the time axis |
2585
+ | `pricePrecision` | `null` | Decimal places; null infers from the feed |
2586
+ | `timezone` | `'UTC'` | IANA name for axis, crosshair and tooltips |
2587
+
2588
+ Bid and ask fall through to the `showBidLine` / `showAskLine` config they
2589
+ duplicate, so the dialog and the existing runtime setters stay **one switch**
2590
+ rather than two that can disagree.
2591
+
2592
+ High/low are measured over what is on screen, not the whole dataset: a tag for a
2593
+ high six months out of view marks a line the user cannot see and cannot scroll
2594
+ to without losing it.
2595
+
2596
+ ### Price precision
2597
+
2598
+ ```ts
2599
+ chart.setPricePrecision(4); // pin
2600
+ chart.setPricePrecision(null); // back to inferring from the feed
2601
+ ```
2602
+
2603
+ One accessor feeds all eleven places the chart writes a price — axis, legend,
2604
+ tooltips, crosshair, drawings, trade levels — so an override cannot apply to
2605
+ some and not others. A price axis reading `1.2345` beside a tooltip reading
2606
+ `1.23` is worse than either choice on its own.
2607
+
2608
+ **It deliberately does not move pip size.** A pip is a property of the contract;
2609
+ asking the axis for fewer decimals is a display choice, and the Ruler must keep
2610
+ saying what a move is actually worth. Pip size still comes from `InstrumentSpec`
2611
+ and, failing that, from the precision *inferred* from the data.
2612
+
2613
+ ### Timezone
2614
+
2615
+ `setTimezone(tz)` already existed; it is now in the dialog, carried in
2616
+ `ChartSettingsSnapshot`, and emits `timezoneChange`. `'local'` resolves to the
2617
+ device zone immediately, so `getTimezone()` always returns a real IANA name.
2618
+
2619
+ ### Canvas
2620
+
2621
+ ```ts
2622
+ chart.setCanvasOptions({ gridVertical: false, watermarkVisible: true, watermarkSize: 128 });
2623
+ ```
2624
+
2625
+ | Field | Default |
2626
+ | --- | --- |
2627
+ | `gridHorizontal` / `gridVertical` | `true` |
2628
+ | `watermarkVisible` | `false` |
2629
+ | `watermarkText` | `''` — empty uses the current symbol |
2630
+ | `watermarkSize` / `watermarkOpacity` | `72` / `0.06` |
2631
+ | `crosshairStyle` | `'dashed'` |
2632
+ | `crosshairWidth` | `1` |
2633
+
2634
+ Colours stay in `setCanvasColors`, which was already plumbed per theme;
2635
+ `gridVertical` and `watermark` are new entries in `CanvasColorSettings` and fall
2636
+ back to `grid` and `axisText`.
2637
+
2638
+ **The watermark draws above the grid and below everything else.** Painted after
2639
+ the series it would tint the candles, and the point of it is to sit behind them.
2640
+ Crosshair dash lengths scale with the width, or a 4px dash on a 4px line reads as
2641
+ a solid bar.
2642
+
2643
+ ### Colour bars based on previous close
2644
+
2645
+ ```ts
2646
+ chart.setBarColorSource('previousClose');
2647
+ ```
2648
+
2649
+ Whether a bar is "up" against its own open (the default) or against the previous
2650
+ bar's close. The two disagree constantly — a gap-up bar that sells off all
2651
+ session is up on one rule and down on the other — which is why it is a setting
2652
+ and not a fix.
2653
+
2654
+ Applies to candles, hollow candles, bars, HLC bars, columns **and the volume
2655
+ pane together**: they are one reading of the same market, and a volume pane
2656
+ disagreeing with the candles above it is a bug, not a setting. Heikin Ashi,
2657
+ Renko, Kagi and Point & Figure are unaffected — direction is intrinsic to what
2658
+ those transforms compute.
2659
+
2660
+ Eight series each carried their own `close >= open`; they now share
2661
+ `computeUpFlags()`, which the engine calls once per frame. It takes the bar
2662
+ *before* the visible slice, because the renderers only ever see what is on
2663
+ screen — without it the leftmost bar would have nothing to compare against and
2664
+ would flip colour as the user panned it into view. At the very start of the data,
2665
+ where no earlier bar exists, that one bar falls back to open-vs-close.
2666
+
2667
+ ### Persistence
2668
+
2669
+ All six ride in `ChartSettingsSnapshot`, so they are covered by settings
2670
+ templates and "Apply to all charts" for free. **Absent is meaningful**: a
2671
+ template saved before these fields existed applies as "change nothing", not as
2672
+ "reset everything to its default" — otherwise re-applying an old template would
2673
+ silently rearrange the chart.
2674
+
2675
+
2676
+ ## Bottom Bar & Price Scale
2677
+
2678
+ Five TradingView controls, behind `features.enableScaleControls`. The gate adds
2679
+ a cluster to the left of the duration buttons — **Go to date**, a **timezone**
2680
+ selector, and **%**, **log**, **auto** — and needs `showBottomBar: true`, since
2681
+ the duration bar is off by default. Each is also a `ChartEngine` method, so a
2682
+ host can drive them from its own chrome and leave the bar as it was.
2683
+
2684
+ ```
2685
+ ┌──────────────────────────────────────────────────────────────────────┐
2686
+ │ 📅 │ UTC ⌄ │ % log auto 1D 5D 1M 3M 6M 1Y 5Y All │
2687
+ └──────────────────────────────────────────────────────────────────────┘
2688
+ ```
2689
+
2690
+ All five are ordinary buttons styled like the duration buttons beside them, and
2691
+ the two that need input open a themed popover above the bar. **No native
2692
+ `<input type="date">` or `<select>`**: both take their colours from the
2693
+ operating system, so in a dark chart they rendered as pale boxes with a browser
2694
+ calendar glyph — the one part of the bar that could not be themed. The menus
2695
+ open *upward*, since the bar is the last row on screen.
2696
+
2697
+ ```ts
2698
+ const chart = new ChartEngine({
2699
+ container,
2700
+ showBottomBar: true,
2701
+ features: { enableScaleControls: true },
2702
+ priceScaleMode: 'log',
2703
+ autoScale: true,
2704
+ });
2705
+ ```
2706
+
2707
+ ### Go to a specific date
2708
+
2709
+ ```ts
2710
+ chart.goToDate('2024-03-01'); // ISO, a Date, or unix ms
2711
+ ```
2712
+
2713
+ The calendar button opens a date field and a **Go**; the method behind it
2714
+ centres the **nearest** bar and returns its index, or `null` when there is no
2715
+ data. Nearest rather than exact because the date a user types is rarely a bar —
2716
+ it falls on a weekend, a holiday, or between two intraday bars — and refusing to
2717
+ move is a worse answer than landing beside it. Binary search, so it costs
2718
+ nothing on a long history. Emits `goToDate` with the bar it settled on.
2719
+
2720
+ ### Timezone selector
2721
+
2722
+ The bottom-bar button shows the current zone's short name and opens a checked
2723
+ list. It writes through `setTimezone()`, the same path the Scales tab and the
2724
+ config option use, so all three stay in step. `'local'` resolves to
2725
+ the device zone immediately — `getTimezone()` always returns a real IANA name.
2726
+
2727
+ ### Price scale modes
2728
+
2729
+ ```ts
2730
+ chart.setPriceScaleMode('log'); // 'normal' | 'log' | 'percent'
2731
+ ```
2732
+
2733
+ **One choice, not three flags.** Log and percent are different answers to the
2734
+ same question, and a chart in both at once has no meaning. Clicking the active
2735
+ button returns to `'normal'`, so the pair works as a toggle rather than a trap.
2736
+
2737
+ #### Logarithmic
2738
+
2739
+ Equal ratios get equal height: a move from 10 to 20 occupies the same space as
2740
+ one from 100 to 200. On a series running 1 → 1000 the pixel gaps between the
2741
+ decade marks go from **49.8px and 497.3px** linear to **156.4px and 156.4px**
2742
+ log, and an exponential series renders as a straight line.
2743
+
2744
+ The mode is carried on the **`PriceRange`**, not on the `ScaleManager`:
2745
+
2746
+ ```ts
2747
+ interface PriceRange { min: number; max: number; log?: boolean }
2748
+ ```
2749
+
2750
+ One manager serves the price pane and every sub-pane, and an RSI drawn on a log
2751
+ axis would be nonsense. Every price→pixel call already passes a range, so putting
2752
+ the flag there made series, drawings, indicators, the crosshair and hit-testing
2753
+ all log-aware **without changing a single call site**.
2754
+
2755
+ **Log needs a positive range.** `isLogRange()` falls back to linear when `min <= 0`
2756
+ rather than refusing to draw, so an instrument that trades through zero quietly
2757
+ keeps working. A manual zoom-out that drags the displayed minimum below zero is
2758
+ clamped instead, because falling back there would make the chart snap between two
2759
+ mappings in the middle of a drag.
2760
+
2761
+ **Ticks are the 1/2/5 × 10ⁿ series per decade**, thinned by *pixel* distance
2762
+ rather than by value — a decade occupies the same height whether it runs 1→10 or
2763
+ 1000→10000. A range inside a single decade falls back to the linear generator,
2764
+ since over a span that narrow the two mappings are within a pixel of each other
2765
+ and linear ticks read better.
2766
+
2767
+ Label precision is computed **per tick** from its own neighbours. One precision
2768
+ taken from the first pair would render `500` as `500.0` and `0.2` as `0.2` in the
2769
+ same column.
2770
+
2771
+ #### Percent
2772
+
2773
+ Every price re-expressed as percent change from the first visible bar's close,
2774
+ with `+0.00%` emphasised on the axis. It reuses the transform compare mode
2775
+ already had, with no second series to combine against, so panning re-bases the
2776
+ way TradingView's percent scale does rather than pinning to the start of history.
2777
+
2778
+ ### Auto scale
2779
+
2780
+ ```ts
2781
+ chart.setAutoScale(false); // freeze the Y range
2782
+ ```
2783
+
2784
+ Switching it off freezes whatever is on screen *now*, so the chart does not jump
2785
+ at the moment the user asks it to stop moving. A manual axis drag still wins —
2786
+ turning auto off should not also take away the ability to move the axis by hand.
2787
+
2788
+ This is separate from the implicit manual mode a price-axis drag already
2789
+ produced: that one is entered by dragging and left by resetting, while this is a
2790
+ switch the user holds across pans and timeframe changes.
2791
+
2792
+
2793
+ ## Side Panels
2794
+
2795
+ A right-hand docked panel with two tabs, behind `features.enableSidePanels`.
2796
+
2797
+ ```
2798
+ ┌──────────────────────────┐
2799
+ │ Data Window │ Objects │✕ │
2800
+ ├──────────────────────────┤
2801
+ │ Date 12 Aug 2026│
2802
+ │ Open 149.73│
2803
+ │ Close 148.56│
2804
+ │ Change -1.17 (-0.78%) │
2805
+ │ MA(20) 150.56│
2806
+ │ MACD(12, 26, 9) │
2807
+ │ MACD 1.22│
2808
+ │ Signal 1.84│
2809
+ │ Histogram -0.62│
2810
+ └──────────────────────────┘
2811
+ ```
2812
+
2813
+ ```ts
2814
+ const chart = new ChartEngine({
2815
+ container,
2816
+ features: { enableSidePanels: true },
2817
+ });
2818
+
2819
+ chart.setSidePanelTab('objects');
2820
+ chart.setSidePanelVisible(false); // the ✕ does this too
2821
+ ```
2822
+
2823
+ The panel is **docked, not floating**: both tabs are read while looking at the
2824
+ chart, and a floating window would cover the candles the numbers describe.
2825
+
2826
+ **Closing it collapses it to an icon rail, it does not hide it.**
2827
+
2828
+ ```
2829
+ expanded (232px) collapsed (30px)
2830
+ ┌──────────────────┐ ┌──┐
2831
+ │ Data │ Objects │✕│ │ ▦ │ ← Data Window
2832
+ ├──────────────────┤ │ ◈ │ ← Objects
2833
+ │ … │ │ │
2834
+ └──────────────────┘ └──┘
2835
+ ```
2836
+
2837
+ A panel that vanished completely would leave nothing to click to get it back,
2838
+ stranding the feature behind a command palette. The rail keeps 30px and gives
2839
+ the chart the other 202px; the `ResizeObserver` already watching `canvasWrap`
2840
+ resizes the canvases for free. Each rail icon opens straight onto *its* tab
2841
+ rather than reopening whatever was last showing, and the last-opened one stays
2842
+ marked so reopening is a single click on the icon already highlighted.
2843
+
2844
+ `setSidePanelVisible(false)` is the same operation as the ✕ — expanded versus
2845
+ rail. `features.enableSidePanels` is what removes the panel entirely.
2846
+
2847
+ ### Data Window
2848
+
2849
+ The bar under the cursor — or the last bar when the cursor is off the plot —
2850
+ followed by one block per visible indicator, refreshed from the render pass so
2851
+ it coalesces with the animation frame instead of firing on every pointer event.
2852
+
2853
+ Values come from the engine's existing per-instance result cache, which is a
2854
+ full-length array indexed by bar, so nothing is recomputed to fill the panel.
2855
+ Only indicators that are actually being recomputed are listed: a hidden or
2856
+ timeframe-locked instance holds a stale cache and showing it would present old
2857
+ numbers as current.
2858
+
2859
+ **Indicator fields are named by what they are.** `IndicatorResult` has five
2860
+ fixed slots — `value`, `upper`, `lower`, `signal`, `histogram` — and indicators
2861
+ do not agree on what they mean. MACD puts its signal line in `upper` and its
2862
+ histogram in `lower`, where Bollinger puts bands in both, so labelling the slots
2863
+ generically would print `Upper 1.84` for a MACD signal. A small table names the
2864
+ ones that differ; everything else falls back.
2865
+
2866
+ A single-line indicator renders as one `MA(20): 150.56` row rather than a
2867
+ heading with one orphan beneath it. One still warming up shows a dash, because
2868
+ omitting the block would read as the indicator having been removed.
2869
+
2870
+ Rows use tabular figures so the value column does not jitter as digits change
2871
+ under a moving crosshair, and the panel **reconciles rather than rebuilds** —
2872
+ it keys on the row labels and, when they are unchanged, writes only the text.
2873
+
2874
+ ### Object Tree
2875
+
2876
+ Everything on the chart, grouped, with per-row show/hide, lock and remove.
2877
+ Clicking a drawing row selects it; clicking an indicator row opens its settings.
2878
+ Empty groups are omitted rather than shown as a zero.
2879
+
2880
+ Drawing names come from `labels.drawingToolbar.tools` — the toolbar's own label
2881
+ map, so the tree is localised by the same override a host already supplies. A
2882
+ text tool shows its first line beside the type name, clipped, since a Table or
2883
+ Note carries several and the rest would push every other row off the panel.
2884
+
2885
+ Indicators get **no lock control**: there is nothing to lock. The panel reads an
2886
+ absent `locked` as exactly that rather than needing a flag.
2887
+
2888
+ ### New per-object primitives
2889
+
2890
+ The manager only had bulk operations, so these are new:
2891
+
2892
+ ```ts
2893
+ chart.setDrawingVisible(id, false);
2894
+ chart.setDrawingLocked(id, true);
2895
+ chart.deleteDrawing(id); // not "select it, then delete"
2896
+ chart.selectDrawing(id); // null clears
2897
+ chart.toggleIndicatorVisibility(instanceId);
2898
+ ```
2899
+
2900
+ `deleteDrawing` is separate from `deleteSelected()` on purpose: selecting fires
2901
+ `onSelectionChange`, which opens the style popover on a drawing that is about to
2902
+ disappear. Hiding or locking the selected drawing deselects it, the way the bulk
2903
+ operations already did — a selection box floating over nothing, with a popover
2904
+ attached, is worse than losing the selection. All three take an undo snapshot.
2905
+
2906
+ ### Reopening
2907
+
2908
+ Click either rail icon. Three Quick Search commands do the same from the
2909
+ keyboard: **Open Data Window**, **Open Object Tree**, **Toggle side panel**.
2910
+
2911
+ ### Rendering the data yourself
2912
+
2913
+ `buildDataWindow()` and `buildObjectTree()` are exported as pure functions, and
2914
+ `SidePanel` takes their output. A host can render the same rows in its own
2915
+ chrome, or feed the panel from somewhere other than the chart.
2916
+
2917
+
1526
2918
  ## Custom Indicator
1527
2919
 
1528
2920
  Implement `IIndicator` and pass it to `addIndicator()`: