acttrader-charts 1.3.0-beta.2 → 1.3.0-beta.20

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
@@ -58,12 +58,15 @@ chart.on('durationChange', ({ duration, timeframe }) => {
58
58
  ```ts
59
59
  const chart = new ChartEngine({
60
60
  container,
61
- dataLoader: async ({ start, end, interval }) => {
62
- const res = await fetch(`/api/bars?from=${start.getTime()}&to=${end.getTime()}&tf=${interval}`);
61
+ dataLoader: async ({ start, end, interval, priceSource }) => {
62
+ // `priceSource` ('bid' | 'ask' | 'ltp') is the candle source the chart is showing —
63
+ // pass it to your history API so the bars match (dealing feeds: LTP vs BID candles).
64
+ const res = await fetch(`/api/bars?from=${start.getTime()}&to=${end.getTime()}&tf=${interval}&source=${priceSource}`);
63
65
  return res.json(); // OHLCVBar[]
64
66
  },
65
67
  });
66
- // No manual loadData() needed — the engine calls dataLoader on start and on timeframe/duration changes.
68
+ // No manual loadData() needed — the engine calls dataLoader on start, on timeframe/duration
69
+ // changes, and (unless `reloadOnPriceSourceChange: false`) whenever the price source changes.
67
70
  ```
68
71
 
69
72
  If the first fetch returns fewer than `minInitialBars` (default `10`), the engine automatically widens the lookback window and calls `dataLoader` again — up to the `maxLookbackMs` ceiling (default 365 days). This keeps the chart useful on weekends, holidays, or for instruments that just listed. If retries still yield zero bars, the engine shows a **"No data available"** overlay (customise the text via `labels.chart.noData`).
@@ -152,6 +155,9 @@ const dealingChart = new ChartEngine({
152
155
  container,
153
156
  // "Show chart by" toggle: 'ltp' | 'ask' | 'bid' (candle close/high/low source)
154
157
  tickClosePriceSource: 'ltp',
158
+ // Header dropdown so the user can switch the source on the chart itself
159
+ // (rendered next to the timeframe dropdown; omit to hide):
160
+ priceSourceSelector: ['ltp', 'bid'],
155
161
  // Chart line checkboxes — each price line is independent:
156
162
  showAskLine: true, // Show AskLine
157
163
  showBidLine: true, // Show BidLine
@@ -161,8 +167,15 @@ const dealingChart = new ChartEngine({
161
167
  dealingChart.setShowAskLine(false); // hide the ask line
162
168
  dealingChart.setShowBidLine(true); // show the bid line
163
169
  dealingChart.setShowLtpPrice(true); // show the LTP line
170
+ // Switch the candle source programmatically (e.g. from a trade-settings page);
171
+ // the header dropdown label follows automatically:
172
+ dealingChart.setTickClosePriceSource('bid');
173
+ // A user pick from the dropdown emits `priceSourceChange` — persist it:
174
+ dealingChart.on('priceSourceChange', ({ source }) => saveTradeSetting(source));
164
175
  ```
165
176
 
177
+ **History follows the source.** Switching the source (dropdown pick or `setTickClosePriceSource()`) does not just change how live ticks extend the last candle — the engine re-runs your `dataLoader` with `params.priceSource` set to the new value and replaces the loaded bars, keeping the visible time window. Forward `priceSource` to your charting API (e.g. as a query parameter) so it returns LTP-built or BID-built candles accordingly; without it the API keeps serving the old source and the chart cannot change. Set `reloadOnPriceSourceChange: false` to opt out of the re-fetch.
178
+
166
179
  ### Compare symbols
167
180
 
168
181
  Overlay one or more comparison instruments on the main chart, normalized to
@@ -333,13 +346,15 @@ When `enableTrading` is on and live BID/ASK data is streaming, hovering / activa
333
346
 
334
347
  ### Horizontal (time-axis) order-line dragging
335
348
 
336
- Opt-in via `features.orderLineTimeDrag` (off by default; enable it per broker from the host app — e.g. only for Hankotrade users). When enabled, any level flagged `timeDraggable: true` can have its **info-box badge** dragged left/right to re-anchor the order to a different candle. The **price line stays locked** — only the time anchor moves. Grab the badge for horizontal drag (`grab` / `grabbing` cursor); the price line itself still drags vertically as before.
349
+ Opt-in via `features.orderLineTimeDrag` (off by default; enable it per broker from the host app — e.g. only for Hankotrade users). When enabled, any level flagged `timeDraggable: true` — open positions **and pending orders** alike — can have its **info-box badge** dragged left/right to re-anchor the order to a different candle. During a horizontal drag the **price line stays locked** — only the time anchor moves. A fixed-price position's badge grabs horizontally at once (`grab` / `grabbing` cursor). A pending order (or entry-editable position) keeps its price drag: its badge shows a `move` cursor and the **first movement decides the axis** — sideways moves the time anchor, up/down moves the entry price exactly as before, and the full-width line still drags vertically.
337
350
 
338
351
  ```ts
339
352
  const chart = new ChartEngine({
340
353
  container,
341
354
  features: { orderLineTimeDrag: true }, // host enables this only for the gated broker
342
355
  orderLineDragSnap: true, // snap to nearest candle on release (default)
356
+ orderLineAnchorPersistence: true, // remember dropped positions in localStorage (default)
357
+ orderLineDefaultAnchor: 'center', // un-dragged badges start mid-chart (default: 'timestamp')
343
358
  });
344
359
 
345
360
  chart.setLevels(
@@ -354,13 +369,101 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
354
369
  ```
355
370
 
356
371
  - **Bounds:** the badge is clamped to the visible viewport — it can't be dragged past the left/right edge.
372
+ - **Pending orders:** flag them `timeDraggable: true` too. The pill follows the drag direction — a sideways pull re-anchors the order in time (price locked, `orderLineMoved` on release), a vertical pull moves the entry price and its SL/TP brackets as usual (`tradeLevelDrag` / `tradeLevelEdit`). The axis is fixed after about 6px of movement and does not change mid-drag. Touch follows the same rule, and a tap that never moves still opens the edit form.
357
373
  - **Snap:** controlled by `orderLineDragSnap` (default `true`); set `false` to keep the exact released time.
358
374
  - **Escape / touch-cancel:** reverts the badge to its starting candle without emitting `orderLineMoved`.
359
375
  - **Stays put after release:** the chart holds the dropped position across `setLevels` refreshes until you echo the new `timestamp` back (auto-released on match).
376
+ - **Survives reloads:** with `orderLineAnchorPersistence` (default `true`) the dropped candle time is saved in `localStorage` under `finchart.orderLineTimeAnchors`, keyed by the level's label. After a page reload the badge returns to that candle even if your `setLevels()` payload still carries the order's original `timestamp` — the persisted anchor wins until the badge is dragged again. Capped at the 500 most recently dragged labels; set `false` if you persist anchors server-side instead.
377
+ - **New orders at the center:** with `orderLineDefaultAnchor: 'center'` a `timeDraggable` level that has never been dragged ignores its `timestamp` and renders mid-chart — so a freshly filled market order arrives in the middle of the chart rather than pinned to the latest candle at the right edge. Levels that are not `timeDraggable` still honor `timestamp`. Default `'timestamp'` keeps the previous behavior.
360
378
  - A level **without** `timestamp` renders exactly as before (badge centered), so existing consumers are unaffected.
361
379
 
362
380
  ---
363
381
 
382
+ ### Trade action bar — `features.tradeActionBar`
383
+
384
+ With the flag on, the floating trade button opens a segmented bar at the clicked
385
+ price instead of the two-row Buy/Sell popup:
386
+
387
+ ```
388
+ Alert | Pending | SL | TP | TSL | ×
389
+ ```
390
+
391
+ - No price readout — the axis already shows the price at that point.
392
+ - **Alert** calls `onAddAlert(price)`. The chip is only rendered when that callback is set.
393
+ - **Pending** opens a sub-menu just above the bar — an `Above · 1.17586` / `Below · …`
394
+ header, then the two orders that make sense at that price (`Buy Limit` / `Sell Stop`
395
+ below the market, `Sell Limit` / `Buy Stop` above), each with its `@ price`. Picking a
396
+ row creates the draft order exactly as before and closes both.
397
+ - **SL / TP / TSL** emit `bulkLevelsRequested` with `{ kind, price }` and close the
398
+ bar — the host opens its Bulk Levels dialog pre-focused on that kind. The chart
399
+ never calls a broker. The chips grey out, with a tooltip saying why, when the
400
+ symbol has no open positions or has buys and sells open at once — one stop or
401
+ target price cannot sit on the right side of the market for both directions.
402
+ - **×**, a click elsewhere, Escape, TFC off and `dismissAllUI()` all close it.
403
+
404
+ Chip labels and the × tooltip live under `labels.trade` (`actionAlert`, `actionPending`,
405
+ `actionSl`, `actionTp`, `actionTsl`, `actionCloseTitle`).
406
+
407
+ ### On-chart P/L pill — `features.pnlPill`
408
+
409
+ A floating summary of the open positions on the chart, for the trader who wants
410
+ the total without opening the positions panel:
411
+
412
+ ```
413
+ ⋮⋮ 3 POS +72.65 USD | ◎ ✕
414
+ ```
415
+
416
+ - **`3 POS`** — how many position levels are on the chart. Pending orders,
417
+ position-style entry orders (`entryPriceEditable`) and an unconfirmed draft
418
+ are not counted.
419
+ - **`+72.65 USD`** — the sum of the numeric `pnl` those levels carry, i.e. the
420
+ `pnlKey` column you pass to `setLevels`. The chart computes no money itself;
421
+ while no position has a `pnl` yet the pill shows `—`. Profit is drawn in the
422
+ buy colour, loss in the sell colour.
423
+ - **Click the body** (the grip, the count or the amount) to fan every position
424
+ badge out on the chart — clustered levels expand, and TFC is switched on if it
425
+ was off — with an accent ring on the pill while they are shown. Click again to
426
+ collapse.
427
+ - **◎** (the same target glyph as the Bulk Levels dialog header) emits
428
+ `bulkLevelsRequested` with `source: 'pill'` and a `null` price, so your Bulk Levels
429
+ dialog opens pre-scoped to every open position on the symbol. Tooltip:
430
+ "Set SL · TP · TSL on all N {symbol} positions". Greyed out while buys and sells are
431
+ both open ("Bulk levels need all N {symbol} positions in one direction").
432
+ - **✕** (amber) emits `pnlPillCloseAll` with every counted position. Your app
433
+ confirms and closes them; the chart never calls a broker.
434
+
435
+ The pill sits top-right of the plot, clear of the price axis. Drag it anywhere
436
+ inside the plot; double-click puts it back. It hides itself while the chart has
437
+ no positions and follows the chart theme. Every element sits on one shared row
438
+ height, and the grip and both buttons have fixed widths, so a long or negative
439
+ amount never shifts or squeezes them.
440
+
441
+ With the feature on, the header (advanced toolbar and default top bar) also gets a
442
+ **P/L** switch — wallet icon, `P/L`, the same 24px `rounded-sm` button tinted while the
443
+ pill is shown. It hides or shows the pill on the user's say-so, wins over the position
444
+ count, and emits `pnlPillToggle` so you can persist the choice; `pnlPill.visible` seeds
445
+ it and `setPnlPillVisible()` drives it from code.
446
+
447
+ ```ts
448
+ const chart = new ChartEngine({
449
+ container,
450
+ features: { pnlPill: true },
451
+ pnlPill: { currency: 'USD' }, // → "+72.65 USD"; or format: (pnl) => myMoney(pnl)
452
+ });
453
+
454
+ chart.setLevels(positions, 'TradeID', 'Price', 'position', 'pnl', 'pnlText');
455
+
456
+ chart.on('pnlPillCloseAll', async ({ count, pnl, data }) => {
457
+ if (!(await confirmDialog(`Close all ${count} positions?`))) return;
458
+ for (const row of data) closeTrade(row);
459
+ });
460
+ ```
461
+
462
+ Labels live under `labels.pnlPill`: `positionsAbbrev` (`'POS'`), `showPositionsTitle`,
463
+ `hidePositionsTitle`, `bulkLevelsTitle`, `closeAllTitle` — titles may
464
+ use `{symbol}` and `{count}`. The maths behind the pill is exported as
465
+ `summarisePositionLevels(levels, draftLabel?)` and `formatPnlPillValue(pnl, currency?)`.
466
+
364
467
  ## Default Configuration
365
468
 
366
469
  ### `ChartConfig`
@@ -375,9 +478,13 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
375
478
  | `hideHeader` | | `false` | Hide only the chart header (TopBar / AdvancedToolbar / CompactToolbar, per `headerLayout`). Bottom bar, left drawing tools, and on-canvas overlays remain on their own flags. Drive the chart from your own UI via `setTimeframe(tf)`, `setSeries(type)`, `addIndicatorByName(name)`, `removeIndicator(name)` |
376
479
  | `showDrawingTools` | | `true` | Show drawing toolbar and pencil button |
377
480
  | `showFullscreenButton` | | `true` | Show the fullscreen toggle button in the top bar. Set to `false` to hide it entirely. Mobile wrappers (Android / iOS) default this to `false` |
378
- | `timeframe` | | `"1D"` | Initial timeframe |
481
+ | `timeframe` | | `"1D"` | Initial timeframe. Values: `1m` `3m` `4m` `5m` `10m` `15m` `30m` `1h` `2h` `4h` `8h` `1D` `1W` `1M`; `3m` / `4m` / `10m` / `30m` are requested from `dataLoader` as `3min` / `4min` / `10min` / `30min`. If your API lacks them, point `aggregateFrom` at a finer interval it does serve, e.g. `{ '3m': '1m', '4m': '1m', '10m': '5m', '30m': '15m' }` — the chart merges the candles |
482
+ | `timeframes` | | classic set | Timeframes offered in the header, in order: the advanced row's pills, the classic dropdown, and the rest of the reference header's ⌄ menu after its six fixed pills. Omit to keep `1m 5m 15m 1h 4h 1D 1W` (row) / `… 30m 2h 8h 1M` (dropdown, ⌄), so the newer minute timeframes only appear for hosts that list them |
379
483
  | `duration` | | — | Initial active duration button |
380
484
  | `symbol` | | — | Symbol name shown in the top bar |
485
+ | `features` | | `{}` | Opt-in feature gates. See [Feature flags](#feature-flags) |
486
+ | `instrument` | | — | Contract specs for `symbol` — pip size, contract size, money conversion. See [`InstrumentSpec`](#instrumentspec) |
487
+ | `account` | | — | Account equity and per-trade risk used to size the position tools. See [`AccountSpec`](#accountspec) |
381
488
  | `isins` | | — | Symbol list for the picker modal |
382
489
  | `onIsinSelect` | | — | Called when user picks a symbol from the picker modal |
383
490
  | `padding` | | `{top:8,right:0,bottom:0,left:0}` | Canvas padding (px) |
@@ -394,7 +501,7 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
394
501
  | `momentumMaxVelocity` | | `6.0` | Max launch velocity (px/ms) — caps hard-flick speed |
395
502
  | `targetCandleWidth` | | `10` | Target px width per candle for auto-calculating initial bar count |
396
503
  | `durationTimeframeMap` | | *(see below)* | Override duration → timeframe pairings |
397
- | `dataLoader` | | — | `(params) => Promise<OHLCVBar[]>` auto-called on load / change |
504
+ | `dataLoader` | | — | `({ start, end, interval, priceSource }) => Promise<OHLCVBar[]>` auto-called on load, timeframe/duration change, and price-source change. `priceSource` is the candle source to fetch (`'bid'`, `'ask'` or `'ltp'`) |
398
505
  | `compareDataLoader` | | — | `({ symbol, start, end, interval }) => Promise<OHLCVBar[]>` — fetches bars for a compare symbol. Set this to enable the library-owned Compare flow. |
399
506
  | `initialCompares` | | — | Symbols to auto-add as compares once the initial primary range is loaded |
400
507
  | `maxCompares` | | `8` | Maximum concurrent compare symbols. Adding beyond emits `compareError` |
@@ -403,6 +510,8 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
403
510
  | `labels` | | `DEFAULT_LABELS` | Deep-partial string overrides for i18n/translation |
404
511
  | `tickClosePriceSource` | | `"bid"` | Which price drives live tick close/high/low: `"bid"`, `"ask"`, or `"ltp"` (build candles from the last traded price — exchange/dealing feeds; ticks without a valid LTP fall back to the bid) |
405
512
  | `showLtpPrice` | | unset | Show the LTP marker (dashed price line + axis tag). Unset: shown only in `"ltp"` mode. `true`: always shown when the feed supplies an LTP. `false`: hidden even in `"ltp"` mode (candles still build from the LTP). Toggle at runtime with `chart.setShowLtpPrice(show)` |
513
+ | `priceSourceSelector` | | unset | Show a price-source dropdown in the chart header listing the given sources, e.g. `["ltp", "bid"]` (dealing feeds). A user pick switches the candle source, re-fetches history via `dataLoader` (see `reloadOnPriceSourceChange`) and emits `priceSourceChange`; sync it programmatically with `chart.setTickClosePriceSource(source)`. Hidden when unset/empty. Default header layout only |
514
+ | `reloadOnPriceSourceChange` | | `true` | Re-run `dataLoader` with the new `params.priceSource` whenever the candle price source changes, so historical candles are rebuilt from the selected price (LTP vs BID) and not only the live one. The visible time window is preserved. Set `false` to keep only live ticks following the new source |
406
515
  | `showBidAskLines` | | `false` | **Deprecated** — show both bid and ask as dashed lines during a live stream. Prefer the per-line checks `showAskLine` / `showBidLine`, which override it when set |
407
516
  | `showAskLine` | | unset | Show the Ask price line (dashed line + axis tag) independently. Unset: legacy `showBidAskLines` behavior. Toggle at runtime with `chart.setShowAskLine(show)` |
408
517
  | `showBidLine` | | unset | Show the Bid price line (dashed line + axis tag) independently. Unset: legacy `showBidAskLines` behavior. Toggle at runtime with `chart.setShowBidLine(show)` |
@@ -412,14 +521,23 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
412
521
  | `showPriceAxisCountdown` | | `false` | Show candle countdown timer on the right price axis, just below the live price tag. Honours `candleCountdownTimeframes`. Toggleable from the Settings dialog (Appearance tab). |
413
522
  | `maxSubPanes` | | `3` | Max simultaneous oscillator sub-panes |
414
523
  | `tradeDisplayFilter` | | `"all"` | Which TFC levels are visible: `"all"` · `"positions"` · `"orders"` · `"none"` |
524
+ | `pnlPill` | | — | `{ currency?, format?, visible? }` for the on-chart P/L pill — currency code appended to the amount, or your own formatter. Only read when `features.pnlPill` is on |
415
525
  | `positionRenderStyle` | | auto | Force position render style: `"line"` or `"dot"` |
416
526
  | `hideLevelConfirmCancel` | | `false` | Hide on-canvas ✓/✗ confirm-cancel buttons for TFC level edits |
417
527
  | `deselectActiveOnOutsideClick` | | `false` | When `true`, clicking/tapping anywhere outside a selected trade level dismisses it (reverting any pending edits, mirroring ✗ Cancel). Default `false` keeps the level active so incidental clicks — price-axis resize, taps outside the QTY input — don't drop an in-progress edit. The level can still be dismissed via ✓/✗, tapping the level again, or `setLevels()` removing it |
418
528
  | `showTradeLevelsAlways` | | `false` | Always render SL/TP bracket lines + price pills, even when the parent level is not hovered or selected. The close (×) button stays hover-only so the chart isn't cluttered. Toggleable from the Settings dialog (Trading tab). Persisted in `localStorage`. |
419
529
  | `tradeLevelButtonScale` | | `1` | Multiplier for trade-level Confirm/Cancel/Edit/Close button radii and gaps. Scales visuals **and** hit/drag areas together — raise it on touch devices for larger tap targets. Clamped to `[1, 3]`. Also settable at runtime via `chart.setTradeLevelButtonScale(scale)` |
530
+ | `tradeLevelBadgeScale` | | `1` | Shrinks the on-chart position / order tag — font, padding, grip, ✎ × buttons and their hit areas together. `0.6` is about half the default area. Clamped `[0.5, 1]` |
531
+ | `tradeLevelBadgeAnchor` | | `"auto"` | `"left"` homes the tag at the plot's left edge like an axis label, whatever the open price or time anchor, so it never covers candles until the trader drags it. With `features.orderLineTimeDrag` the tag stays draggable along the time axis and the drop is remembered as before |
420
532
  | `tfcEnabled` | | `true` | Enable the TFC toggle button in the top bar. When `false`, TFC is completely disabled — the toggle button is hidden and all trade levels, draft orders, and the floating trade button are suppressed |
421
- | `features.orderLineTimeDrag` | | `false` | Enable **horizontal (time-axis) order-line dragging**. A level with `timeDraggable: true` can have its info-box badge dragged left/right to re-anchor the order to a different candle — **price stays locked**. Emits `orderLineMoved` on release. Off by default; intended to be enabled per broker (e.g. Hankotrade) by the host app |
533
+ | `crosshairEnabled` | | `true` | Draw the mouse crosshair. `false` hides it **and** the floating "place order at this price" button that rides on its horizontal line, ignores mirrored `setCrosshair()` calls and keeps the mobile long-press crosshair from arming. Trade levels, drawings and TFC are unaffected. Runtime: `chart.setCrosshairEnabled(enabled)` |
534
+ | `enableCrossHairHeader` | | `false` | Crosshair on/off switch in the header (advanced toolbar, default top bar and the compact per-pane strip): the reference `rounded-sm` button. The crosshair is on by default, so the icon starts tinted; a click hides the crosshair (and the trade button riding on it) and drops the icon to its plain state, the next click brings both back. Calls `setCrosshairEnabled()` and emits `crosshairToggle` so the host can persist the choice — see [Header crosshair switch](#header-crosshair-switch--enablecrosshairheader) |
535
+ | `enableCrosshairToggle` | | `false` | **Deprecated** — alias of `enableCrossHairHeader`; either flag renders the switch |
536
+ | `headerOverflowMenu` | | `false` | Advanced header only: the reference Hanko header. A 32px bar with six lowercase timeframe pills (`1m 5m 15m 1h 4h 1d`) and a ⌄ for the rest, then crosshair · P/L · Layout (grid icon) · ⋯ · fullscreen at 24px. Draw, Chart type, Indicators, Compare, Snapshot and Chart settings fold into the ⋯ menu; each keeps its behaviour |
537
+ | `features.orderLineTimeDrag` | | `false` | Enable **horizontal (time-axis) order-line dragging**. A level with `timeDraggable: true` (position or pending order) can have its info-box badge dragged left/right to re-anchor the order to a different candle — **price stays locked** while dragging sideways. Pending orders keep their vertical price drag: the badge's first movement picks the axis. Emits `orderLineMoved` on release. Off by default; intended to be enabled per broker (e.g. Hankotrade) by the host app |
422
538
  | `orderLineDragSnap` | | `true` | When horizontal dragging is enabled, snap the badge to the nearest candle on release. Set `false` to keep the exact released time |
539
+ | `orderLineAnchorPersistence` | | `true` | When horizontal dragging is enabled, remember where each badge was dropped in `localStorage` (key `finchart.orderLineTimeAnchors`, keyed by level label) so it comes back to the same candle after a reload. A persisted anchor overrides the level's `timestamp` until the badge is dragged again. Set `false` to persist anchors yourself |
540
+ | `orderLineDefaultAnchor` | | `'timestamp'` | Where an un-dragged `timeDraggable` badge sits: `'timestamp'` = over the candle at the level's `timestamp`; `'center'` = the chart's horizontal center (ignores `timestamp`), so a new market order lands mid-chart. Non-draggable levels always honor `timestamp` |
423
541
  | `levelClusteringEnabled` | | `true` | Enable trade-level fan-out clustering; overlapping levels group into expandable badges |
424
542
  | `clusterThresholdDistance` | | `20` | Pixel proximity threshold for clustering (only when `levelClusteringEnabled` is `true`) |
425
543
  | `hideSymbolAndTick` | | `false` | Hide the symbol name and tick-activity (streaming) dot in the top-left overlay. Does **not** affect the OHLC(V) strip — use `hideOHLCV` for that |
@@ -430,13 +548,120 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
430
548
  | `quantityFieldConfig` | | — | Constraints for the draft order QTY field (only relevant when `showQuantityField` is `true`). Object: `{ minLots?: number, maxLots?: number }`. `minLots` also sets the initial quantity and the step value for the flyout input (default: `1`). `maxLots` caps the input (default: `100`) |
431
549
  | `tradesThresholdForHorizontalLine` | | `2` | Level count above which render auto-switches to `"dot"` mode |
432
550
  | `timezone` | | `"UTC"` | IANA timezone string for time-axis and crosshair labels. `"UTC"` (default), `"local"` (browser/device timezone), or any IANA string (`"America/New_York"`, `"Europe/London"`, etc.) |
433
- | `canvasColors` | | — | Per-theme canvas background color overrides (persisted from Settings dialog) |
551
+ | `canvasColors` | | — | Per-theme colour overrides for the **canvas only** (persisted from the Settings dialog). The surface picks — `background`, `grid`, `axisText`, `axisBorder`, `crosshair` — repaint the chart canvas and leave the chart chrome (top/bottom/left bars, dialogs, popovers) on the theme from `themeOverrides`. Series picks (`candleUp`/`candleDown`/`wickUp`/`wickDown`/`borderUp`/`borderDown`/`volumeUp`/`volumeDown`) apply everywhere, so legends and indicator pills keep tracking them |
434
552
  | `aggregateFrom` | | — | Fetch finer-grained data and aggregate client-side per timeframe |
435
553
  | `onOrderSubmit` | | — | Called when user submits a trade via the floating button |
436
554
  | `onLevelEdit` | | — | Called when user confirms a TFC level edit |
437
555
  | `onLotsChange` | | — | Called when user changes qty on a draft order |
438
556
  | `onLevelDragEnd` | | — | *(deprecated — use `onLevelEdit`)* |
439
557
 
558
+ ### `InstrumentSpec`
559
+
560
+ The chart reads prices, never contract specs — so a tool that reports a distance
561
+ in **pips**, or converts one to money, has to be told how. Pass `instrument` at
562
+ construction and swap it with `setInstrument()` whenever the symbol changes.
563
+
564
+ ```ts
565
+ const chart = new ChartEngine({
566
+ container,
567
+ symbol: 'EURUSD',
568
+ instrument: {
569
+ pipSize: 0.0001, // 0.01 for JPY crosses
570
+ contractSize: 100_000, // units per lot
571
+ valuePerPoint: 1, // account-currency value of one price unit per unit
572
+ currencySymbol: '$',
573
+ },
574
+ });
575
+
576
+ chart.setSymbol('USDJPY').setInstrument({ pipSize: 0.01, contractSize: 100_000 });
577
+ ```
578
+
579
+ | Field | Default | Description |
580
+ |---|---|---|
581
+ | `pipSize` | inferred | Price distance counted as one pip |
582
+ | `contractSize` | `1` | Units per lot (`100` for XAUUSD, `100000` for most FX pairs) |
583
+ | `valuePerPoint` | `1` | Account-currency value of one price unit per contract unit |
584
+ | `currencySymbol` | `'$'` | Prefixed to money figures |
585
+
586
+ **Every field is optional and the whole block can be omitted** — tools fall back
587
+ to price-only readouts, so existing integrations keep working untouched.
588
+
589
+ When `pipSize` is absent it is inferred from how many decimals the feed quotes:
590
+ five-decimal (`1.08531`) and three-decimal (`151.234`) feeds carry fractional
591
+ pips, so the pip is the second-to-last digit; two- and four-decimal feeds quote
592
+ whole pips. **That convention is wrong for metals, indices and crypto** — pass
593
+ `pipSize` explicitly if pips matter.
594
+
595
+ The same fallbacks are exported for hosts that want to compute alongside the
596
+ chart:
597
+
598
+ ```ts
599
+ import { resolveInstrument, inferPipSize, toPips, toMoney } from 'acttrader-charts';
600
+ ```
601
+
602
+ > Per-position values still travel on the individual `TradeLevel`
603
+ > (`contractSize` / `valuePerPoint`). `InstrumentSpec` is the chart-wide default
604
+ > for tools that aren't attached to a position.
605
+
606
+ ### Feature flags
607
+
608
+ Newer tools ship behind `ChartConfig.features` so each host opts in. **Every flag
609
+ defaults to `false`** — a chart that sets none behaves exactly as it did before
610
+ the flag existed, including its toolbar contents.
611
+
612
+ ```ts
613
+ const chart = new ChartEngine({
614
+ container,
615
+ features: { enableForecasting: true },
616
+ });
617
+ ```
618
+
619
+ | Flag | Default | What it changes |
620
+ |---|---|---|
621
+ | `enableForecasting` | `false` | The reworked drawing tools, as one switch — see below |
622
+ | `orderLineTimeDrag` | `false` | Horizontal (time-axis) order-line dragging for positions and pending orders — see [Horizontal order-line dragging](#horizontal-time-axis-order-line-dragging) |
623
+ | `pnlPill` | `false` | Floating on-chart P/L pill — count and summed P/L of the positions on the chart — see [On-chart P/L pill](#on-chart-pl-pill--featurespnlpill) |
624
+ | `tradeActionBar` | `false` | The trade button opens a segmented `Alert · Pending · SL · TP · TSL · ×` bar at the clicked price instead of the two-row Buy/Sell popup — see [Trade action bar](#trade-action-bar--featurestradeactionbar) |
625
+
626
+ `enableForecasting` turns on three things together:
627
+
628
+ - **Long Position** / **Short Position** in a new **Forecasting** group in the
629
+ drawing toolbar. With the flag off the group is not rendered at all.
630
+ - **Brush** and **Highlighter** draw freehand — press, drag, release. Off, they
631
+ keep the click-per-point gesture ended by a double-click.
632
+ - **Ruler** reports percent, pips, calendar duration and volume. Off, it reports
633
+ the bar count and raw price delta as before.
634
+
635
+ They share one flag because they ship and roll back together: a broker either has
636
+ the reworked drawing tools or it doesn't.
637
+
638
+ Related config, independent of the flag: the position tools show quantity and
639
+ money only when [`account`](#accountspec) is supplied, and pips on both the
640
+ position tools and the ruler come from [`instrument`](#instrumentspec) — without
641
+ it, pip size is inferred from the feed's decimal count.
642
+
643
+ ### `AccountSpec`
644
+
645
+ What the **Long / Short position** tools size themselves against.
646
+
647
+ TradingView asks the user to type these because it has no broker connection. We
648
+ do have one — so pass real equity and keep it current.
649
+
650
+ ```ts
651
+ chart.setAccount({ size: 10_000, riskPercent: 1 });
652
+ ```
653
+
654
+ | Field | Default | Description |
655
+ |---|---|---|
656
+ | `size` | — | Account equity in the account currency |
657
+ | `riskPercent` | `1` | Percent of the account risked per trade |
658
+
659
+ Omit the block and the position tools still draw — price, percent, pips and
660
+ risk/reward all render; only the quantity and money amounts are left out.
661
+
662
+ > A sketch drawn against a stale balance reports the wrong quantity rather than
663
+ > failing visibly, so push `setAccount()` whenever equity moves.
664
+
440
665
  ### `OHLCVBar`
441
666
 
442
667
  | Field | Required | Type | Description |
@@ -477,7 +702,7 @@ import { DEFAULT_UI_CONFIG } from 'acttrader-charts';
477
702
 
478
703
  | Component key | Configurable properties |
479
704
  |---|---|
480
- | `drawingToolbar` | `iconBtnSize`, `iconFontSize`, `iconFontFamily`, `flyoutLabelFontSize`, `flyoutLabelFontFamily`, `shortcutFontSize`, `soonBadgeFontSize`, `flyoutHeadingFontSize`, `flyoutHeadingLetterSpacing`, `scrollBtnHeight`, `scrollBtnFontSize`, `barPadding`, `btnGap` |
705
+ | `drawingToolbar` | `iconBtnSize`, `iconFontSize`, `iconFontFamily`, `flyoutLabelFontSize`, `flyoutLabelFontFamily`, `shortcutFontSize`, `soonBadgeFontSize`, `flyoutHeadingFontSize`, `flyoutHeadingLetterSpacing`, `scrollBtnHeight`, `scrollBtnFontSize`, `barPadding`, `btnGap`, `modernIcons`, `actionsButtonIcon` (`'default'` \| `'lock'` — lock glyph on the Actions button, for hosts that relabel Lock All as "Save Charts") |
481
706
  | `topBar` | `height`, `dropBtnFontSize`, `dropBtnFontFamily`, `drawBtnSize`, `drawBtnIconFontSize`, `iconBtnSize`, `mobileIconBtnSize`, `mobileDrawBtnIconSize`, `flyoutRowFontSize`, `flyoutRowFontFamily`, `flyoutCategoryFontSize`, `flyoutCategoryLetterSpacing`, `flyoutCheckFontSize`, `streamDotSize` |
482
707
  | `bottomBar` | `height`, `btnFontSize`, `btnFontFamily` |
483
708
  | `priceAxis` | `fontSize`, `fontFamily` |
@@ -555,6 +780,11 @@ new ChartEngine({
555
780
  | `topBar.series` | `area` | `'Area'` | |
556
781
  | `topBar` | `indicatorsBtn` | `'Indicators'` | Indicators button (no active) |
557
782
  | `topBar` | `indicatorsBtnActive` | `'Indicators ({count})'` | `{count}` replaced at runtime |
783
+ | `topBar` | `disableCrosshairTitle` | `'Disable crosshair'` | Header crosshair switch tooltip while on (`enableCrossHairHeader`) |
784
+ | `topBar` | `enableCrosshairTitle` | `'Enable crosshair'` | Header crosshair switch tooltip while off |
785
+ | `topBar` | `pnlPillBtn` | `'P/L'` | Header P/L pill switch label (`features.pnlPill`) |
786
+ | `topBar` | `hidePnlPillTitle` | `'Hide P/L pill for {symbol}'` | P/L switch tooltip while the pill is shown |
787
+ | `topBar` | `showPnlPillTitle` | `'Show P/L pill for {symbol}'` | P/L switch tooltip while the pill is hidden |
558
788
  | `topBar` | `toggleDrawingTitle` | `'Toggle Drawing Tools'` | Toolbar toggle tooltip |
559
789
  | `topBar` | `toggleFullscreenTitle` | `'Toggle Fullscreen'` | Fullscreen button tooltip |
560
790
  | `topBar` | `exitFullscreenTitle` | `'Exit Fullscreen'` | |
@@ -597,6 +827,16 @@ new ChartEngine({
597
827
  | `trade` | `market` | `'Market'` | |
598
828
  | `trade` | `qty` | `'Qty'` | Draft order qty input label |
599
829
  | `trade` | `placeOrderTitle` | `'Place order at this price'` | Trade button tooltip |
830
+ | `trade` | `actionAlert` … `actionTsl` | `'Alert'`, `'Pending'`, `'SL'`, `'TP'`, `'TSL'` | Trade action bar chips (`features.tradeActionBar`) |
831
+ | `trade` | `actionCloseTitle` | `'Close'` | Trade action bar × tooltip |
832
+ | `trade` | `directionAbove`, `directionBelow` | `'Above'`, `'Below'` | Pending sub-menu header — side of the market the clicked price is on |
833
+ | `trade` | `actionBulkNoPositionsTitle`, `actionBulkMixedTitle` | `'No open {symbol} positions'`, `'Bulk levels need all {symbol} positions in one direction'` | SL / TP / TSL chips greyed out — why |
834
+ | `pnlPill` | `positionsAbbrev` | `'POS'` | On-chart P/L pill — word after the count (`3 POS`), shown uppercase |
835
+ | `pnlPill` | `showPositionsTitle` | `'Show positions on chart · drag handle to move · double-click to reset'` | Pill tooltip while collapsed — a body click fans the positions out |
836
+ | `pnlPill` | `hidePositionsTitle` | `'Hide positions on chart · drag handle to move · double-click to reset'` | Pill tooltip while fanned out |
837
+ | `pnlPill` | `bulkLevelsTitle` | `'Set SL · TP · TSL on all {count} {symbol} positions'` | ◎ tooltip — opens the host's Bulk Levels dialog |
838
+ | `pnlPill` | `bulkLevelsMixedTitle` | `'Bulk levels need all {count} {symbol} positions in one direction'` | ◎ tooltip while greyed out for mixed sides |
839
+ | `pnlPill` | `closeAllTitle` | `'Close all {symbol} positions'` | Amber ✕ tooltip; `{symbol}` and `{count}` are filled in |
600
840
  | `dialogs.settings` | `title` | `'Chart Settings'` | Dialog title |
601
841
  | `dialogs.settings` | `tfcSection` | `'Trade from Charts'` | Section heading |
602
842
  | `dialogs.settings` | `showLabel` | `'Show'` | Radio group label |
@@ -637,8 +877,8 @@ chart.setTheme('light');
637
877
  |---|---|---|
638
878
  | `background` | `string` | Canvas background color |
639
879
  | `grid` | `string` | Grid line color |
640
- | `axisText` | `string` | Axis tick label color |
641
- | `axisBorder` | `string` | Axis border / separator color |
880
+ | `axisText` | `string` | Axis tick label color. Also the default label colour for the chart chrome (dialog text, toolbar labels) — set it via `themeOverrides` to restyle both, or via the Settings dialog to restyle the canvas axis alone |
881
+ | `axisBorder` | `string` | Axis border / separator color. Also the default border colour for the chart chrome (dialog frames, toolbar dividers) — same split as `axisText` |
642
882
  | `crosshair` | `string` | Crosshair line color |
643
883
  | `tooltip` | `{background, text, border}` | Floating label colors |
644
884
  | `candle` | `{up, down, wickUp, wickDown}` | Candlestick colors |
@@ -673,7 +913,7 @@ chart.resetData(): this
673
913
 
674
914
  **Symbol switch pattern:**
675
915
  ```ts
676
- chart.setSymbol('GBPUSD').resetData();
916
+ chart.setSymbol('GBPUSD').setInstrument({ pipSize: 0.0001 }).resetData();
677
917
  // … fetch new bars …
678
918
  chart.loadData(newBars);
679
919
  ```
@@ -682,8 +922,15 @@ chart.loadData(newBars);
682
922
 
683
923
  ```ts
684
924
  chart.setSeries(series: SeriesType): this
925
+ chart.setInstrument(spec: InstrumentSpec | undefined): this // contract specs for pips / money readouts
926
+ chart.getInstrument(): ResolvedInstrument // specs with fallbacks applied
927
+ chart.setAccount(account: AccountSpec | undefined): this // equity + risk for the position tools
928
+ chart.getAccount(): AccountSpec | undefined
685
929
  chart.setTheme('dark' | 'light'): this
686
930
  chart.setTimeframe(tf: Timeframe): this // change timeframe and reload data
931
+ chart.setDuration(d: Duration, timeframe?: Timeframe): this // select a duration; pairs the timeframe and reloads
932
+ chart.getDuration(): Duration | null // currently selected duration
933
+ chart.setBracketLabelMode(mode, currencySymbol?): this // SL/TP pills: 'price' (default) or 'amount'
687
934
  chart.setThemeOverrides(overrides: ThemeOverrides): this // update per-theme color overrides at runtime
688
935
  chart.setTimezone(tz: string): this // change display timezone at runtime
689
936
  chart.setVolume(show: boolean): this
@@ -722,6 +969,7 @@ chart.cancelCurrentEdit(): this // cancel the active draft order or any in-pro
722
969
  chart.addLevelBracket(label: string, bracketType: 'sl' | 'tp'): this // auto-place a SL or TP bracket at a default offset; emits tradeLevelBracketActivated with the computed price
723
970
  chart.setDraftBracketPnl(bracketType: 'sl' | 'tp', pnlText: string | null): this // set estimated P&L text on the active bracket host — the draft order while drafting, or the currently selected existing pending order / position while modifying; pass null to clear
724
971
  chart.setTfcActive(enabled: boolean): this // toggle TFC on/off at runtime; hides/shows all trade levels, draft orders, and floating trade button; fires tfcToggle event
972
+ chart.setPnlPillVisible(visible: boolean): this // show/hide the on-chart P/L pill (features.pnlPill) — what the header's P/L switch does; emits pnlPillToggle
725
973
  ```
726
974
 
727
975
  > **Staging semantics for `updateLevelMainPrice` / `updateLevelBracket`.** Calls to these methods register the change in the chart's pending-edit buffer (the same buffer chart-initiated drags use), so the edit stays visible even when the host app keeps pushing fresh server state via `setLevels` (e.g. per-tick PnL refreshes). When the server echoes back the new price in a later `setLevels` call, the staged edit is auto-released (mobile, i.e. `hideLevelConfirmCancel: true`). If a panel closes without submitting, call `cancelLevelEdit(label)` or `cancelCurrentEdit()` to drop the staged edit — otherwise it will keep overriding server state on the chart.
@@ -730,8 +978,33 @@ chart.setTfcActive(enabled: boolean): this // toggle TFC on/off at runtime; hi
730
978
 
731
979
  ```ts
732
980
  chart.setOrderLots(lots: number): void // update default qty at runtime
981
+ chart.setCrosshairEnabled(enabled: boolean): this // off hides the crosshair AND this button — it rides on the crosshair line, so it has no place once the line is gone
733
982
  ```
734
983
 
984
+ The button follows the crosshair's horizontal line, so it is only ever shown while the
985
+ crosshair is (`crosshairEnabled`, default `true`). Hosts with a crosshair on/off control
986
+ should call `setCrosshairEnabled()` rather than painting the crosshair transparent — the
987
+ latter leaves the button riding an invisible line.
988
+
989
+ ### Header crosshair switch — `enableCrossHairHeader`
990
+
991
+ ```ts
992
+ const chart = new ChartEngine({
993
+ container,
994
+ enableCrossHairHeader: true, // crosshair icon in the header; the crosshair itself starts on
995
+ });
996
+ chart.on('crosshairToggle', ({ enabled }) => saveUserPref('crosshair', enabled));
997
+ ```
998
+
999
+ With the flag on, a crosshair icon sits in the header's right cluster (advanced toolbar and
1000
+ default top bar) or at the end of the compact per-pane strip (`headerLayout: 'compact'`). The chart crosshair is shown as usual on load and the icon is tinted. Clicking
1001
+ the icon hides the crosshair — and the floating trade button that rides on it — and the icon
1002
+ drops to its plain state; clicking again brings both back. Each click goes through
1003
+ `setCrosshairEnabled()` and emits `crosshairToggle`, so a host can persist the choice and seed
1004
+ it on the next load with `crosshairEnabled: false`. `enableCrosshairToggle` is the deprecated
1005
+ name of the same flag and still works. Native wrappers get the same switch via their
1006
+ `enableCrossHairHeader` init option, the `onCrosshairToggle` callback and `setCrosshairEnabled()`.
1007
+
735
1008
  ### Viewport
736
1009
 
737
1010
  ```ts
@@ -888,6 +1161,37 @@ Sub-pane heights are user-resizable by dragging the separator between panes.
888
1161
  | `flatChannel` | Flat Channel | 2 clicks |
889
1162
  | `disjointChannel` | Disjoint Channel | 3 clicks |
890
1163
 
1164
+ ### SL/TP bracket labels — price or amount
1165
+
1166
+ By default an SL/TP pill reads `SL 4159.00`. With `bracketLabelMode: 'amount'`
1167
+ it reads `SL -$290.80` — the money the position gains or loses if that bracket
1168
+ is hit, currency symbol in front.
1169
+
1170
+ The chart has no access to contract specs or the account currency, so each level
1171
+ supplies what the maths needs:
1172
+
1173
+ ```ts
1174
+ new ChartEngine({ container, bracketLabelMode: 'amount', currencySymbol: '$' });
1175
+
1176
+ chart.setLevels([{
1177
+ label: 'POS-1', price: 4173.54, side: 'buy', lots: 0.20,
1178
+ stopLossPrice: 4159.00, takeProfitPrice: 4183.00,
1179
+ contractSize: 100, // units per lot (100 for XAUUSD, 100000 for most FX)
1180
+ valuePerPoint: 1, // account-currency value of one price unit
1181
+ currencySymbol: '$', // optional per-level override
1182
+ }], 'label', 'price', 'position');
1183
+ // pills render: SL -$290.80 TP +$189.20
1184
+ ```
1185
+
1186
+ ```
1187
+ amount = (bracket − entry) × direction × lots × contractSize × valuePerPoint
1188
+ ```
1189
+
1190
+ `valuePerPoint` is where a quote → account currency conversion goes. A level
1191
+ missing `lots` or `contractSize` keeps showing its price, so a partial rollout
1192
+ degrades level by level rather than rendering `NaN`. Toggle at runtime with
1193
+ `chart.setBracketLabelMode('amount')`.
1194
+
891
1195
  ### Fibonacci & Gann
892
1196
 
893
1197
  `fibRetracement` · `fibExtension` · `fibChannel` · `fibTimezone` · `fibCircles` · `fibSpiral` · `fibFan` · `fibProjection` · `gannFan` · `gannSquare` · `gannBox`
@@ -920,10 +1224,78 @@ In a **per-drawing** style (`SerializedDrawing.style.levels`), a level may also
920
1224
 
921
1225
  `rectangle` · `rotatedRectangle` · `ellipse` · `triangle` · `circle` · `arc` · `polyline` · `path` · `brush` · `highlighter`
922
1226
 
1227
+ **`brush` and `highlighter` draw freehand** when `features.enableForecasting` is on
1228
+ (see [Feature flags](#feature-flags)): press, drag, release — the pointer
1229
+ path is sampled continuously and the finished stroke is thinned to the points
1230
+ that carry its shape. (`polyline` and `path` remain click-per-point, ended with a
1231
+ double-click.)
1232
+
923
1233
  ### Measurements & Annotations
924
1234
 
925
1235
  `priceRange` · `dateRange` · `datePriceRange` · `ruler` · `priceLabel` · `priceNote` · `priceProjection` · `projection` · `arrowUp` · `arrowDown` · `arrowMarker` · `text` · `callout` · `anchoredNote` · `flag` · `sineLine` · `regressionTrend` · `ghostFeed`
926
1236
 
1237
+ **`ruler`**, with `features.enableForecasting` on (see [Feature flags](#feature-flags)),
1238
+ draws a direction-tinted box over the span it measures and reports:
1239
+
1240
+ ```
1241
+ 0.00455 (0.80%) 45.5
1242
+ 43 bars, 9d 4h
1243
+ Vol 102.27K
1244
+ ```
1245
+
1246
+ Price delta, percent, and **pips**; bar count and calendar duration; volume
1247
+ traded across the span.
1248
+
1249
+ A pip figure is always shown — but it is only *correct* when the host supplies
1250
+ [`instrument.pipSize`](#instrumentspec). Without it the chart infers pip size
1251
+ from how many decimals the feed quotes, which follows the usual FX convention
1252
+ and is wrong for metals, indices and crypto.
1253
+
1254
+ Duration needs bar timestamps, and the volume line is omitted entirely when the
1255
+ bars carry no volume, rather than showing a `0` that would read as "no volume
1256
+ traded".
1257
+
1258
+ The box takes its colour from `candle.up` / `candle.down` so a measurement never
1259
+ disagrees with the candles underneath it; picking a colour in the style popover
1260
+ overrides that. All four corners resize.
1261
+
1262
+ With the flag off it reports the bar count and raw price delta, as it always has.
1263
+
1264
+ ### Forecasting
1265
+
1266
+ `longPosition` · `shortPosition`
1267
+
1268
+ > Requires `features.enableForecasting` — see [Feature flags](#feature-flags).
1269
+ > With the flag off the group is not rendered, on the desktop toolbar or in the
1270
+ > mobile tools sheet.
1271
+ >
1272
+ > `ghostFeed` and `projection` stay in **Advanced Tools** and are ungated — they
1273
+ > predate this group.
1274
+
1275
+ **`longPosition` / `shortPosition`** sketch a trade that hasn't been placed: a
1276
+ green profit zone from entry to target, a red risk zone from entry to stop, and
1277
+ live readouts.
1278
+
1279
+ ```
1280
+ Target: 0.00313 (0.549%) 31.3, Amount: 5622.13
1281
+ Open PnL: 0.00146, Qty: 11323
1282
+ Risk/reward ratio: 1.84
1283
+ Stop: 0.00170 (0.298%) 17.0, Amount: 2887.5
1284
+ ```
1285
+
1286
+ Two clicks place it — entry, then target — and the stop lands at a 2:1
1287
+ reward:risk for you to drag. All three prices have handles.
1288
+
1289
+ Quantity is sized so that hitting the stop costs exactly `riskPercent` of the
1290
+ account, which needs [`account`](#accountspec); the money amounts and pips also
1291
+ need [`instrument`](#instrumentspec). Without them the tool still draws and still
1292
+ reports price, percent and risk/reward.
1293
+
1294
+ > **This is a drawing, not an order.** Trade-From-Chart (`setLevels`) is what
1295
+ > puts real broker orders on the chart — see [Trade From Chart
1296
+ > (TFC)](#trade-from-chart-tfc--setlevels). Nothing on a position tool reaches
1297
+ > the broker.
1298
+
927
1299
  ### Volume Profile
928
1300
 
929
1301
  `volumeProfile` · `anchoredVP` · `fixedRangeVP`
@@ -932,6 +1304,10 @@ In a **per-drawing** style (`SerializedDrawing.style.levels`), a level may also
932
1304
 
933
1305
  `cyclicLines` · `timeCycles`
934
1306
 
1307
+ > `ghostFeed` and `projection` remain here and are ungated. TradingView files
1308
+ > them under Forecasting, but moving them would have reshuffled the toolbar for
1309
+ > hosts that never enabled the new group.
1310
+
935
1311
  ### Harmonic & Elliott Patterns
936
1312
 
937
1313
  `abcdPattern` · `headShoulders` · `trianglePattern` · `batPattern` · `butterflyPattern` · `crabPattern` · `gartleyPattern` · `cypherPattern` · `sharkPattern` · `threeDrives` · `elliottImpulse` · `elliottCorrective` · `elliottTriangle` · `elliottCombination` · `elliottWxy`
@@ -952,8 +1328,9 @@ chart.on('pan', ({ viewport }) => {});
952
1328
  chart.on('timeframeChange',({ timeframe }) => {});
953
1329
  chart.on('durationChange', ({ duration, timeframe }) => {});
954
1330
  chart.on('seriesChange', ({ series }) => {});
1331
+ chart.on('priceSourceChange', ({ source }) => {}); // header BID/ASK/LTP dropdown pick
955
1332
  chart.on('streamStatus', ({ status }) => {}); // 'connected' | 'reconnecting' | 'disconnected'
956
- chart.on('dataLoaded', ({ timeframe, interval, start, end }) => {});
1333
+ chart.on('dataLoaded', ({ timeframe, interval, start, end, priceSource }) => {});
957
1334
  chart.on('newBar', ({ completedBar, openingBar, intervalMs }) => {});
958
1335
  chart.on('indicatorAdded', ({ instanceId, shortName, params }) => {}); // a study instance was added — keep instanceId to remove it later
959
1336
  chart.on('indicatorRemoved', ({ instanceId, shortName }) => {});
@@ -976,6 +1353,10 @@ chart.on('tradeLevelEditCancelled', ({ label, type, isFullscreen }) => {}); // E
976
1353
  chart.on('draftInitiated', ({ side, price, orderType, isFullscreen }) => {}); // new draft order shown — open buy/sell form
977
1354
  chart.on('draftCancelled', ({ label, isFullscreen }) => {}); // draft order dismissed without confirming
978
1355
  chart.on('tfcToggle', ({ enabled }) => {}); // TFC toggled on or off via top bar button or setTfcActive()
1356
+ chart.on('crosshairToggle', ({ enabled }) => {}); // crosshair switched on or off — header switch (enableCrossHairHeader) or setCrosshairEnabled(); persist it here
1357
+ chart.on('pnlPillCloseAll', ({ count, pnl, labels, data, isFullscreen }) => {}); // the on-chart P/L pill's ✕ — close these positions (features.pnlPill)
1358
+ chart.on('pnlPillToggle', ({ visible }) => {}); // the header's P/L switch showed or hid the pill — persist it here (features.pnlPill)
1359
+ chart.on('bulkLevelsRequested', ({ kind, price, source, isFullscreen }) => {}); // action-bar chip (price) or the P/L pill's ◎ (price null) — open your Bulk Levels dialog for `kind`
979
1360
  chart.on('tradeLevelDragEnd', ({ label, type, newPrice, data }) => {}); // deprecated — use tradeLevelEdit
980
1361
  chart.on('tradeLevelBracketDrag', ({ label, bracketType, newPrice, data }) => {}); // deprecated
981
1362
 
@@ -984,8 +1365,8 @@ chart.on('tradeLevelBracketDrag', ({ label, bracketType, newPrice, data }) => {}
984
1365
  chart.on('orderLineMoveStart', ({ label, fromTimestamp, fromBarIndex, isFullscreen }) => {});
985
1366
  chart.on('orderLineMoving', ({ label, toTimestamp, toBarIndex, isFullscreen }) => {}); // fires on every move
986
1367
  chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, fromBarIndex, toBarIndex, data, isFullscreen }) => {
987
- // Persist the new time anchor for this order. Echo `timestamp: toTimestamp` back in your
988
- // next setLevels() payload so the badge stays put across refreshes.
1368
+ // The chart already remembers the drop in localStorage (orderLineAnchorPersistence, default on).
1369
+ // Hook here only if you also want to persist the new time anchor server-side.
989
1370
  });
990
1371
 
991
1372
  // Fires after addLevelBracket() auto-places a bracket, delivering the computed price back to the caller