acttrader-charts 1.3.0-beta.13 → 1.3.0-beta.15

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`).
@@ -171,6 +174,8 @@ dealingChart.setTickClosePriceSource('bid');
171
174
  dealingChart.on('priceSourceChange', ({ source }) => saveTradeSetting(source));
172
175
  ```
173
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
+
174
179
  ### Compare symbols
175
180
 
176
181
  Overlay one or more comparison instruments on the main chart, normalized to
@@ -348,6 +353,8 @@ const chart = new ChartEngine({
348
353
  container,
349
354
  features: { orderLineTimeDrag: true }, // host enables this only for the gated broker
350
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')
351
358
  });
352
359
 
353
360
  chart.setLevels(
@@ -365,6 +372,8 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
365
372
  - **Snap:** controlled by `orderLineDragSnap` (default `true`); set `false` to keep the exact released time.
366
373
  - **Escape / touch-cancel:** reverts the badge to its starting candle without emitting `orderLineMoved`.
367
374
  - **Stays put after release:** the chart holds the dropped position across `setLevels` refreshes until you echo the new `timestamp` back (auto-released on match).
375
+ - **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.
376
+ - **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.
368
377
  - A level **without** `timestamp` renders exactly as before (badge centered), so existing consumers are unaffected.
369
378
 
370
379
  ---
@@ -405,7 +414,7 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
405
414
  | `momentumMaxVelocity` | | `6.0` | Max launch velocity (px/ms) — caps hard-flick speed |
406
415
  | `targetCandleWidth` | | `10` | Target px width per candle for auto-calculating initial bar count |
407
416
  | `durationTimeframeMap` | | *(see below)* | Override duration → timeframe pairings |
408
- | `dataLoader` | | — | `(params) => Promise<OHLCVBar[]>` auto-called on load / change |
417
+ | `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'`) |
409
418
  | `compareDataLoader` | | — | `({ symbol, start, end, interval }) => Promise<OHLCVBar[]>` — fetches bars for a compare symbol. Set this to enable the library-owned Compare flow. |
410
419
  | `initialCompares` | | — | Symbols to auto-add as compares once the initial primary range is loaded |
411
420
  | `maxCompares` | | `8` | Maximum concurrent compare symbols. Adding beyond emits `compareError` |
@@ -414,7 +423,8 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
414
423
  | `labels` | | `DEFAULT_LABELS` | Deep-partial string overrides for i18n/translation |
415
424
  | `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) |
416
425
  | `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)` |
417
- | `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 live candle source and emits `priceSourceChange`; sync it programmatically with `chart.setTickClosePriceSource(source)`. Hidden when unset/empty. Default header layout only |
426
+ | `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 |
427
+ | `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 |
418
428
  | `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 |
419
429
  | `showAskLine` | | unset | Show the Ask price line (dashed line + axis tag) independently. Unset: legacy `showBidAskLines` behavior. Toggle at runtime with `chart.setShowAskLine(show)` |
420
430
  | `showBidLine` | | unset | Show the Bid price line (dashed line + axis tag) independently. Unset: legacy `showBidAskLines` behavior. Toggle at runtime with `chart.setShowBidLine(show)` |
@@ -432,6 +442,8 @@ chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, data }) => {
432
442
  | `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 |
433
443
  | `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 |
434
444
  | `orderLineDragSnap` | | `true` | When horizontal dragging is enabled, snap the badge to the nearest candle on release. Set `false` to keep the exact released time |
445
+ | `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 |
446
+ | `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` |
435
447
  | `levelClusteringEnabled` | | `true` | Enable trade-level fan-out clustering; overlapping levels group into expandable badges |
436
448
  | `clusterThresholdDistance` | | `20` | Pixel proximity threshold for clustering (only when `levelClusteringEnabled` is `true`) |
437
449
  | `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 |
@@ -1181,7 +1193,7 @@ chart.on('durationChange', ({ duration, timeframe }) => {});
1181
1193
  chart.on('seriesChange', ({ series }) => {});
1182
1194
  chart.on('priceSourceChange', ({ source }) => {}); // header BID/ASK/LTP dropdown pick
1183
1195
  chart.on('streamStatus', ({ status }) => {}); // 'connected' | 'reconnecting' | 'disconnected'
1184
- chart.on('dataLoaded', ({ timeframe, interval, start, end }) => {});
1196
+ chart.on('dataLoaded', ({ timeframe, interval, start, end, priceSource }) => {});
1185
1197
  chart.on('newBar', ({ completedBar, openingBar, intervalMs }) => {});
1186
1198
  chart.on('indicatorAdded', ({ instanceId, shortName, params }) => {}); // a study instance was added — keep instanceId to remove it later
1187
1199
  chart.on('indicatorRemoved', ({ instanceId, shortName }) => {});
@@ -1212,8 +1224,8 @@ chart.on('tradeLevelBracketDrag', ({ label, bracketType, newPrice, data }) => {}
1212
1224
  chart.on('orderLineMoveStart', ({ label, fromTimestamp, fromBarIndex, isFullscreen }) => {});
1213
1225
  chart.on('orderLineMoving', ({ label, toTimestamp, toBarIndex, isFullscreen }) => {}); // fires on every move
1214
1226
  chart.on('orderLineMoved', ({ label, fromTimestamp, toTimestamp, fromBarIndex, toBarIndex, data, isFullscreen }) => {
1215
- // Persist the new time anchor for this order. Echo `timestamp: toTimestamp` back in your
1216
- // next setLevels() payload so the badge stays put across refreshes.
1227
+ // The chart already remembers the drop in localStorage (orderLineAnchorPersistence, default on).
1228
+ // Hook here only if you also want to persist the new time anchor server-side.
1217
1229
  });
1218
1230
 
1219
1231
  // Fires after addLevelBracket() auto-places a bracket, delivering the computed price back to the caller
@@ -603,6 +603,15 @@ interface DataLoaderParams {
603
603
  start: Date;
604
604
  end: Date;
605
605
  interval: string;
606
+ /**
607
+ * Which price the chart is currently building candles from
608
+ * (`tickClosePriceSource`: `'bid'`, `'ask'` or `'ltp'`). Forward it to your
609
+ * history API (e.g. as a query parameter) so the fetched bars match the
610
+ * source the user picked in the header dropdown — the engine re-runs the
611
+ * loader with the new value whenever the source changes. Always set by the
612
+ * engine; optional only so callers that build params by hand keep compiling.
613
+ */
614
+ priceSource?: 'bid' | 'ask' | 'ltp';
606
615
  }
607
616
  /**
608
617
  * Params passed to `compareDataLoader`. Mirrors `DataLoaderParams` but also
@@ -920,6 +929,14 @@ interface ChartConfig {
920
929
  * Rendered by the default header layout.
921
930
  */
922
931
  priceSourceSelector?: Array<'bid' | 'ask' | 'ltp'>;
932
+ /**
933
+ * Re-run `dataLoader` whenever the candle price source changes (a header
934
+ * dropdown pick or `setTickClosePriceSource()`), passing the new source as
935
+ * `params.priceSource`, so the historical candles are rebuilt from the
936
+ * selected price and not just the live one. Set `false` to keep the old
937
+ * behavior where only live ticks follow the new source. Default: `true`.
938
+ */
939
+ reloadOnPriceSourceChange?: boolean;
923
940
  /**
924
941
  * When the combined count of open positions and pending orders exceeds this
925
942
  * number, the position render style auto-defaults to `'dot'` (compact).
@@ -1295,6 +1312,26 @@ interface ChartConfig {
1295
1312
  * keeps the exact (interpolated) time it was released at. Default: `true`.
1296
1313
  */
1297
1314
  orderLineDragSnap?: boolean;
1315
+ /**
1316
+ * Remember where each horizontally-dragged order-line badge was dropped in
1317
+ * `localStorage` (keyed by the level's label, i.e. your order/trade id) so it comes back
1318
+ * to the same candle after a page reload — even when the next `setLevels()` payload
1319
+ * carries the order's original `timestamp`. A persisted anchor takes precedence over
1320
+ * the level's `timestamp` until the label is dragged again. Bounded to the 500 most
1321
+ * recently dragged labels. Only relevant when `features.orderLineTimeDrag` is enabled.
1322
+ * Default: `true`.
1323
+ */
1324
+ orderLineAnchorPersistence?: boolean;
1325
+ /**
1326
+ * Where a `timeDraggable` level's badge sits before the user has dragged it:
1327
+ * - `'timestamp'` (default): over the candle at the level's `timestamp`, as before.
1328
+ * - `'center'`: at the chart's horizontal center, ignoring the level's `timestamp`, so a
1329
+ * freshly placed market order arrives mid-chart instead of pinned to the latest candle.
1330
+ * Once the user drags the badge it is anchored to (and persisted at) the dropped candle.
1331
+ * Levels that are not `timeDraggable` always honor their `timestamp`.
1332
+ * Only relevant when `features.orderLineTimeDrag` is enabled.
1333
+ */
1334
+ orderLineDefaultAnchor?: 'timestamp' | 'center';
1298
1335
  }
1299
1336
  interface IndicatorResult {
1300
1337
  index: number;
@@ -1569,7 +1606,9 @@ type ChartEventMap = {
1569
1606
  };
1570
1607
  /** Emitted when the user picks a price source (BID / ASK / LTP) from the
1571
1608
  * header dropdown (`priceSourceSelector`). Not emitted for programmatic
1572
- * `setTickClosePriceSource()` calls. */
1609
+ * `setTickClosePriceSource()` calls. By the time this fires the engine has
1610
+ * already switched the live source and (unless `reloadOnPriceSourceChange`
1611
+ * is `false`) started re-fetching history via `dataLoader`. */
1573
1612
  priceSourceChange: {
1574
1613
  source: 'bid' | 'ask' | 'ltp';
1575
1614
  };
@@ -1583,6 +1622,8 @@ type ChartEventMap = {
1583
1622
  interval: string;
1584
1623
  start: Date;
1585
1624
  end: Date;
1625
+ /** Candle price source the bars were requested with (see `DataLoaderParams.priceSource`). */
1626
+ priceSource: 'bid' | 'ask' | 'ltp';
1586
1627
  };
1587
1628
  /** Emitted when the user clicks the × button on a trade, position, or bracket (SL/TP) level. */
1588
1629
  tradeLevelClose: {
@@ -603,6 +603,15 @@ interface DataLoaderParams {
603
603
  start: Date;
604
604
  end: Date;
605
605
  interval: string;
606
+ /**
607
+ * Which price the chart is currently building candles from
608
+ * (`tickClosePriceSource`: `'bid'`, `'ask'` or `'ltp'`). Forward it to your
609
+ * history API (e.g. as a query parameter) so the fetched bars match the
610
+ * source the user picked in the header dropdown — the engine re-runs the
611
+ * loader with the new value whenever the source changes. Always set by the
612
+ * engine; optional only so callers that build params by hand keep compiling.
613
+ */
614
+ priceSource?: 'bid' | 'ask' | 'ltp';
606
615
  }
607
616
  /**
608
617
  * Params passed to `compareDataLoader`. Mirrors `DataLoaderParams` but also
@@ -920,6 +929,14 @@ interface ChartConfig {
920
929
  * Rendered by the default header layout.
921
930
  */
922
931
  priceSourceSelector?: Array<'bid' | 'ask' | 'ltp'>;
932
+ /**
933
+ * Re-run `dataLoader` whenever the candle price source changes (a header
934
+ * dropdown pick or `setTickClosePriceSource()`), passing the new source as
935
+ * `params.priceSource`, so the historical candles are rebuilt from the
936
+ * selected price and not just the live one. Set `false` to keep the old
937
+ * behavior where only live ticks follow the new source. Default: `true`.
938
+ */
939
+ reloadOnPriceSourceChange?: boolean;
923
940
  /**
924
941
  * When the combined count of open positions and pending orders exceeds this
925
942
  * number, the position render style auto-defaults to `'dot'` (compact).
@@ -1295,6 +1312,26 @@ interface ChartConfig {
1295
1312
  * keeps the exact (interpolated) time it was released at. Default: `true`.
1296
1313
  */
1297
1314
  orderLineDragSnap?: boolean;
1315
+ /**
1316
+ * Remember where each horizontally-dragged order-line badge was dropped in
1317
+ * `localStorage` (keyed by the level's label, i.e. your order/trade id) so it comes back
1318
+ * to the same candle after a page reload — even when the next `setLevels()` payload
1319
+ * carries the order's original `timestamp`. A persisted anchor takes precedence over
1320
+ * the level's `timestamp` until the label is dragged again. Bounded to the 500 most
1321
+ * recently dragged labels. Only relevant when `features.orderLineTimeDrag` is enabled.
1322
+ * Default: `true`.
1323
+ */
1324
+ orderLineAnchorPersistence?: boolean;
1325
+ /**
1326
+ * Where a `timeDraggable` level's badge sits before the user has dragged it:
1327
+ * - `'timestamp'` (default): over the candle at the level's `timestamp`, as before.
1328
+ * - `'center'`: at the chart's horizontal center, ignoring the level's `timestamp`, so a
1329
+ * freshly placed market order arrives mid-chart instead of pinned to the latest candle.
1330
+ * Once the user drags the badge it is anchored to (and persisted at) the dropped candle.
1331
+ * Levels that are not `timeDraggable` always honor their `timestamp`.
1332
+ * Only relevant when `features.orderLineTimeDrag` is enabled.
1333
+ */
1334
+ orderLineDefaultAnchor?: 'timestamp' | 'center';
1298
1335
  }
1299
1336
  interface IndicatorResult {
1300
1337
  index: number;
@@ -1569,7 +1606,9 @@ type ChartEventMap = {
1569
1606
  };
1570
1607
  /** Emitted when the user picks a price source (BID / ASK / LTP) from the
1571
1608
  * header dropdown (`priceSourceSelector`). Not emitted for programmatic
1572
- * `setTickClosePriceSource()` calls. */
1609
+ * `setTickClosePriceSource()` calls. By the time this fires the engine has
1610
+ * already switched the live source and (unless `reloadOnPriceSourceChange`
1611
+ * is `false`) started re-fetching history via `dataLoader`. */
1573
1612
  priceSourceChange: {
1574
1613
  source: 'bid' | 'ask' | 'ltp';
1575
1614
  };
@@ -1583,6 +1622,8 @@ type ChartEventMap = {
1583
1622
  interval: string;
1584
1623
  start: Date;
1585
1624
  end: Date;
1625
+ /** Candle price source the bars were requested with (see `DataLoaderParams.priceSource`). */
1626
+ priceSource: 'bid' | 'ask' | 'ltp';
1586
1627
  };
1587
1628
  /** Emitted when the user clicks the × button on a trade, position, or bracket (SL/TP) level. */
1588
1629
  tradeLevelClose: {
package/dist/index.cjs CHANGED
@@ -16837,7 +16837,7 @@ var IndicatorSettingsDialog = class {
16837
16837
  input.max = String(field.max);
16838
16838
  input.step = String(field.step);
16839
16839
  input.value = String(currentVal);
16840
- input.enterKeyHint = "done";
16840
+ input.enterKeyHint = "go";
16841
16841
  Object.assign(input.style, {
16842
16842
  width: "80px",
16843
16843
  background: this.theme.background,
@@ -18424,7 +18424,7 @@ var DrawingStylePopover = class {
18424
18424
  input.step = "0.001";
18425
18425
  input.value = current;
18426
18426
  input.title = "Level ratio \u2014 press Enter to apply";
18427
- input.enterKeyHint = "done";
18427
+ input.enterKeyHint = "go";
18428
18428
  Object.assign(input.style, {
18429
18429
  width: "58px",
18430
18430
  padding: "2px 4px",
@@ -18686,7 +18686,7 @@ var DraftQtyFlyout = class {
18686
18686
  makeInput() {
18687
18687
  const input = document.createElement("input");
18688
18688
  input.type = "number";
18689
- input.enterKeyHint = "done";
18689
+ input.enterKeyHint = "go";
18690
18690
  Object.assign(input.style, {
18691
18691
  width: "64px",
18692
18692
  background: "transparent",
@@ -19788,6 +19788,101 @@ var OrderLineTimeDragHandler = class _OrderLineTimeDragHandler {
19788
19788
  }
19789
19789
  };
19790
19790
 
19791
+ // src/interaction/OrderLineAnchorStore.ts
19792
+ var ORDER_LINE_ANCHORS_LS_KEY = "finchart.orderLineTimeAnchors";
19793
+ var ORDER_LINE_ANCHORS_MAX_ENTRIES = 500;
19794
+ var OrderLineAnchorStore = class {
19795
+ constructor(_storage, _key = ORDER_LINE_ANCHORS_LS_KEY, _maxEntries = ORDER_LINE_ANCHORS_MAX_ENTRIES) {
19796
+ this._storage = _storage;
19797
+ this._key = _key;
19798
+ this._maxEntries = _maxEntries;
19799
+ this._entries = /* @__PURE__ */ new Map();
19800
+ this._load();
19801
+ }
19802
+ /** The page's `localStorage`, or `null` when it is absent or access throws. */
19803
+ static browserStorage() {
19804
+ try {
19805
+ if (typeof localStorage === "undefined") return null;
19806
+ return localStorage;
19807
+ } catch {
19808
+ return null;
19809
+ }
19810
+ }
19811
+ /** Persisted unix-ms anchor for `label`, or `undefined` when none was saved. */
19812
+ get(label) {
19813
+ return this._entries.get(label)?.ts;
19814
+ }
19815
+ has(label) {
19816
+ return this._entries.has(label);
19817
+ }
19818
+ /** Number of persisted anchors. */
19819
+ get size() {
19820
+ return this._entries.size;
19821
+ }
19822
+ /**
19823
+ * Records the candle time `label`'s badge was dropped on and writes it through to storage.
19824
+ * Non-finite timestamps are ignored. `now` is the save time used for eviction ordering.
19825
+ */
19826
+ set(label, ts, now = Date.now()) {
19827
+ if (!label || !Number.isFinite(ts)) return;
19828
+ this._entries.set(label, { ts, at: now });
19829
+ this._evict();
19830
+ this._save();
19831
+ }
19832
+ /** Forgets `label`'s anchor. Returns whether one existed. */
19833
+ delete(label) {
19834
+ const had = this._entries.delete(label);
19835
+ if (had) this._save();
19836
+ return had;
19837
+ }
19838
+ /** Drops every persisted anchor (memory + storage). */
19839
+ clear() {
19840
+ this._entries.clear();
19841
+ try {
19842
+ this._storage?.removeItem(this._key);
19843
+ } catch {
19844
+ }
19845
+ }
19846
+ _evict() {
19847
+ if (this._entries.size <= this._maxEntries) return;
19848
+ const byAge = [...this._entries.entries()].sort((a, b) => a[1].at - b[1].at);
19849
+ const excess = this._entries.size - this._maxEntries;
19850
+ for (let i = 0; i < excess; i++) {
19851
+ const victim = byAge[i];
19852
+ if (victim) this._entries.delete(victim[0]);
19853
+ }
19854
+ }
19855
+ _load() {
19856
+ if (!this._storage) return;
19857
+ try {
19858
+ const raw = this._storage.getItem(this._key);
19859
+ if (!raw) return;
19860
+ const parsed = JSON.parse(raw);
19861
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
19862
+ for (const [label, value] of Object.entries(parsed)) {
19863
+ if (!value || typeof value !== "object") continue;
19864
+ const { ts, at } = value;
19865
+ if (typeof ts !== "number" || !Number.isFinite(ts)) continue;
19866
+ const savedAt = typeof at === "number" && Number.isFinite(at) ? at : 0;
19867
+ this._entries.set(label, { ts, at: savedAt });
19868
+ }
19869
+ this._evict();
19870
+ } catch {
19871
+ }
19872
+ }
19873
+ _save() {
19874
+ if (!this._storage) return;
19875
+ try {
19876
+ if (this._entries.size === 0) {
19877
+ this._storage.removeItem(this._key);
19878
+ return;
19879
+ }
19880
+ this._storage.setItem(this._key, JSON.stringify(Object.fromEntries(this._entries)));
19881
+ } catch {
19882
+ }
19883
+ }
19884
+ };
19885
+
19791
19886
  // src/indicators/sourceField.ts
19792
19887
  function getSource(bar, source) {
19793
19888
  return bar[source];
@@ -23504,6 +23599,8 @@ var _ChartEngine = class _ChartEngine {
23504
23599
  /** Last traded price from the most recent tick that carried one ('ltp' source mode). */
23505
23600
  this.liveLtp = null;
23506
23601
  this.tickClosePriceSource = "bid";
23602
+ /** Re-fetch history through `dataLoader` when the price source changes (`reloadOnPriceSourceChange`). */
23603
+ this._reloadOnPriceSourceChange = true;
23507
23604
  /** True while an async data fetch is in-flight; suppresses tick processing to
23508
23605
  * prevent stale ticks from corrupting newly-loaded bars. */
23509
23606
  this._suppressTicks = false;
@@ -23550,6 +23647,15 @@ var _ChartEngine = class _ChartEngine {
23550
23647
  * `timestamp` back via `setLevels` (auto-released on match). Survives PnL-only refreshes.
23551
23648
  */
23552
23649
  this._timeAnchorOverride = /* @__PURE__ */ new Map();
23650
+ /**
23651
+ * Time anchors persisted in localStorage (`orderLineAnchorPersistence`), keyed by level
23652
+ * label — the candle each badge was last dropped on. Restored at construction so a
23653
+ * horizontally-dragged order line comes back where the user left it after a reload.
23654
+ * Null when persistence is disabled or time-drag is off.
23655
+ */
23656
+ this._orderLineAnchorStore = null;
23657
+ /** Where an un-dragged `timeDraggable` badge sits (`orderLineDefaultAnchor`). */
23658
+ this._orderLineDefaultAnchor = "timestamp";
23553
23659
  this.tradeDragNewPrice = null;
23554
23660
  /** Derived SL preview price during an entry drag in follow-brackets mode. */
23555
23661
  this.tradeDragNewSLPrice = null;
@@ -24786,6 +24892,7 @@ var _ChartEngine = class _ChartEngine {
24786
24892
  this._targetViewport = { ...this.viewport };
24787
24893
  this.maxSubPanes = config.maxSubPanes ?? 3;
24788
24894
  this.tickClosePriceSource = config.tickClosePriceSource ?? "bid";
24895
+ this._reloadOnPriceSourceChange = config.reloadOnPriceSourceChange !== false;
24789
24896
  this.showLtpPrice = config.showLtpPrice;
24790
24897
  this.tradesThresholdForHorizontalLine = config.tradesThresholdForHorizontalLine ?? 2;
24791
24898
  if (config.positionRenderStyle === "line" || config.positionRenderStyle === "dot") {
@@ -24815,6 +24922,12 @@ var _ChartEngine = class _ChartEngine {
24815
24922
  this._features = config.features;
24816
24923
  this.drawingManager.setFreehandEnabled(config.features?.enableForecasting === true);
24817
24924
  this._orderLineDragSnap = config.orderLineDragSnap !== false;
24925
+ this._orderLineDefaultAnchor = config.orderLineDefaultAnchor === "center" ? "center" : "timestamp";
24926
+ if (this._orderTimeDragEnabled && config.orderLineAnchorPersistence !== false) {
24927
+ this._orderLineAnchorStore = new OrderLineAnchorStore(
24928
+ OrderLineAnchorStore.browserStorage()
24929
+ );
24930
+ }
24818
24931
  this.tradeLevelButtonScale = Math.min(Math.max(config.tradeLevelButtonScale ?? 1, 1), 3);
24819
24932
  this._bracketLabel = {
24820
24933
  mode: config.bracketLabelMode ?? "price",
@@ -24994,11 +25107,7 @@ var _ChartEngine = class _ChartEngine {
24994
25107
  showSnapshotBtn,
24995
25108
  config.priceSourceSelector ?? null,
24996
25109
  this.tickClosePriceSource,
24997
- (source) => {
24998
- this.tickClosePriceSource = source;
24999
- this.scheduleRender();
25000
- this.emitter.emit("priceSourceChange", { source });
25001
- }
25110
+ (source) => this._applyPriceSource(source, true)
25002
25111
  );
25003
25112
  this.advancedToolbar.setIndicatorOptions(indicatorOptions);
25004
25113
  this.wrapper.appendChild(this.advancedToolbar.el);
@@ -25045,11 +25154,7 @@ var _ChartEngine = class _ChartEngine {
25045
25154
  resolvedOneClickTradeToggle ?? null,
25046
25155
  config.priceSourceSelector ?? null,
25047
25156
  this.tickClosePriceSource,
25048
- (source) => {
25049
- this.tickClosePriceSource = source;
25050
- this.scheduleRender();
25051
- this.emitter.emit("priceSourceChange", { source });
25052
- }
25157
+ (source) => this._applyPriceSource(source, true)
25053
25158
  );
25054
25159
  this.topBar.setIndicatorOptions(indicatorOptions);
25055
25160
  this.wrapper.appendChild(this.topBar.el);
@@ -25171,7 +25276,7 @@ var _ChartEngine = class _ChartEngine {
25171
25276
  this.canvasWrap.appendChild(this.drawCanvas);
25172
25277
  this.textInputEl = document.createElement("input");
25173
25278
  this.textInputEl.type = "text";
25174
- this.textInputEl.enterKeyHint = "done";
25279
+ this.textInputEl.enterKeyHint = "go";
25175
25280
  Object.assign(this.textInputEl.style, {
25176
25281
  position: "absolute",
25177
25282
  display: "none",
@@ -25870,7 +25975,7 @@ var _ChartEngine = class _ChartEngine {
25870
25975
  const minBars = this.minInitialBars;
25871
25976
  const cap = this.maxLookbackMs;
25872
25977
  const attempt = (i, start) => {
25873
- loader({ start, end, interval }).then((bars) => {
25978
+ loader({ start, end, interval, priceSource: this.tickClosePriceSource }).then((bars) => {
25874
25979
  if (gen !== this._loadGeneration) return;
25875
25980
  const aggregated = this.maybeAggregate(bars);
25876
25981
  const reachedCap = end.getTime() - start.getTime() >= cap;
@@ -25882,7 +25987,8 @@ var _ChartEngine = class _ChartEngine {
25882
25987
  timeframe: this.timeframe,
25883
25988
  interval,
25884
25989
  start,
25885
- end
25990
+ end,
25991
+ priceSource: this.tickClosePriceSource
25886
25992
  });
25887
25993
  return;
25888
25994
  }
@@ -25917,7 +26023,7 @@ var _ChartEngine = class _ChartEngine {
25917
26023
  const minBars = this.minInitialBars;
25918
26024
  const cap = this.maxLookbackMs;
25919
26025
  const attempt = (i, start) => {
25920
- loader({ start, end, interval }).then((bars) => {
26026
+ loader({ start, end, interval, priceSource: this.tickClosePriceSource }).then((bars) => {
25921
26027
  if (gen !== this._loadGeneration) return;
25922
26028
  const aggregated = this.maybeAggregate(bars);
25923
26029
  const reachedCap = end.getTime() - start.getTime() >= cap;
@@ -27096,17 +27202,44 @@ var _ChartEngine = class _ChartEngine {
27096
27202
  this.streamAdapter = null;
27097
27203
  return this;
27098
27204
  }
27099
- /** Switch which price drives live candle close/high/low at runtime
27205
+ /** Switch which price drives candle close/high/low at runtime
27100
27206
  * (`'bid'`, `'ask'` or `'ltp'`). Also syncs the header price-source
27101
- * dropdown when `priceSourceSelector` is enabled. Programmatic calls do
27102
- * not emit `priceSourceChange` — that event is reserved for user picks
27103
- * from the dropdown. */
27207
+ * dropdown when `priceSourceSelector` is enabled, and unless
27208
+ * `reloadOnPriceSourceChange` is `false` — re-runs `dataLoader` with the
27209
+ * new `params.priceSource` so the loaded history is rebuilt from that
27210
+ * price too. Programmatic calls do not emit `priceSourceChange` — that
27211
+ * event is reserved for user picks from the dropdown. */
27104
27212
  setTickClosePriceSource(source) {
27213
+ this._applyPriceSource(source, false);
27214
+ return this;
27215
+ }
27216
+ /**
27217
+ * Applies a new candle price source. Shared by the header dropdown pick and
27218
+ * `setTickClosePriceSource()`.
27219
+ *
27220
+ * Switching the field alone only changes how the NEXT live ticks extend the
27221
+ * last candle; every bar already on screen was fetched with the previous
27222
+ * source, so the chart kept showing e.g. bid history under an "LTP" label.
27223
+ * When the source actually changes and a `dataLoader` is configured, the
27224
+ * whole window is therefore re-fetched (`triggerLoad('priceSource')`) — the
27225
+ * loader receives the source as `params.priceSource` to forward to its API.
27226
+ * The visible time window is restored after the reload so the user does not
27227
+ * lose their scroll/zoom position.
27228
+ */
27229
+ _applyPriceSource(source, fromUserPick) {
27230
+ const changed = source !== this.tickClosePriceSource;
27105
27231
  this.tickClosePriceSource = source;
27106
27232
  this.topBar?.setPriceSource(source);
27107
27233
  this.advancedToolbar?.setPriceSource(source);
27108
27234
  this.scheduleRender();
27109
- return this;
27235
+ if (changed && this._reloadOnPriceSourceChange && this.dataLoader && !this._isDestroyed) {
27236
+ const viewport = this.getViewportTimestamps();
27237
+ if (viewport) {
27238
+ this._pendingViewportRestore = { ...this._pendingViewportRestore ?? {}, viewport };
27239
+ }
27240
+ this.triggerLoad("priceSource");
27241
+ }
27242
+ if (fromUserPick) this.emitter.emit("priceSourceChange", { source });
27110
27243
  }
27111
27244
  /** Show/hide the LTP (last traded price) marker — dashed line + axis tag.
27112
27245
  * `true` always shows it when the feed supplies an LTP, `false` hides it
@@ -27271,7 +27404,7 @@ var _ChartEngine = class _ChartEngine {
27271
27404
  }
27272
27405
  const incomingTs = lvl.timestamp;
27273
27406
  const overrideTs = this._timeAnchorOverride.get(label);
27274
- if (incomingTs !== void 0 && overrideTs !== void 0 && Math.round(this.timestampToBarIndex(incomingTs)) === Math.round(this.timestampToBarIndex(overrideTs))) {
27407
+ if (this._honorsLevelTimestamp(lvl) && incomingTs !== void 0 && overrideTs !== void 0 && Math.round(this.timestampToBarIndex(incomingTs)) === Math.round(this.timestampToBarIndex(overrideTs))) {
27275
27408
  this._timeAnchorOverride.delete(label);
27276
27409
  }
27277
27410
  }
@@ -28289,8 +28422,7 @@ var _ChartEngine = class _ChartEngine {
28289
28422
  * Resolves the horizontal badge center X (canvas px) for a level during render, or `null`
28290
28423
  * to keep the legacy chart-centered placement. Priority:
28291
28424
  * 1. the live drag bar index, for the level currently being time-dragged;
28292
- * 2. a staged drop override (set on the last drag release);
28293
- * 3. the level's own `timestamp`.
28425
+ * 2. the resolved time anchor see `_resolvedTimeAnchor`.
28294
28426
  * Returns `null` when time-drag is disabled or the level has no time anchor.
28295
28427
  */
28296
28428
  _orderLineAnchorX(level, chartW) {
@@ -28298,10 +28430,34 @@ var _ChartEngine = class _ChartEngine {
28298
28430
  if (this.orderTimeDragHandler?.active && this.orderTimeDragHandler.dragLabel === level.label && this.tradeDragNewBarIndex !== null) {
28299
28431
  return this.scaleManager.xToPixel(this.tradeDragNewBarIndex, this.viewport, chartW);
28300
28432
  }
28301
- const ts = this._timeAnchorOverride.get(level.label) ?? level.timestamp;
28433
+ const ts = this._resolvedTimeAnchor(level);
28302
28434
  if (ts === void 0) return null;
28303
28435
  return this.scaleManager.xToPixel(this.timestampToBarIndex(ts), this.viewport, chartW);
28304
28436
  }
28437
+ /**
28438
+ * The unix-ms time anchor a level's badge should sit at (ignoring any live drag), or
28439
+ * `undefined` for the legacy chart-centered placement. Priority:
28440
+ * 1. a drop override staged this session (set on the last drag release);
28441
+ * 2. the anchor persisted in localStorage from an earlier session
28442
+ * (`orderLineAnchorPersistence`);
28443
+ * 3. the level's own `timestamp` — unless `orderLineDefaultAnchor: 'center'` and the
28444
+ * level is `timeDraggable`, in which case an un-dragged badge stays centered.
28445
+ */
28446
+ _resolvedTimeAnchor(level) {
28447
+ const override = this._timeAnchorOverride.get(level.label);
28448
+ if (override !== void 0) return override;
28449
+ const persisted = this._orderLineAnchorStore?.get(level.label);
28450
+ if (persisted !== void 0) return persisted;
28451
+ return this._honorsLevelTimestamp(level) ? level.timestamp : void 0;
28452
+ }
28453
+ /**
28454
+ * Whether a level's own `timestamp` positions its badge. False only for `timeDraggable`
28455
+ * levels under `orderLineDefaultAnchor: 'center'`, whose un-dragged badge is centered
28456
+ * regardless of `timestamp` (a dragged one is positioned by its override / persisted anchor).
28457
+ */
28458
+ _honorsLevelTimestamp(level) {
28459
+ return !(this._orderLineDefaultAnchor === "center" && level.timeDraggable === true);
28460
+ }
28305
28461
  /** Returns the effective entry price for a level, preferring any staged main drag. */
28306
28462
  _effectiveEntryPrice(label, fallback) {
28307
28463
  const changes = this.pendingLevelDrags.get(label);
@@ -28888,7 +29044,8 @@ var _ChartEngine = class _ChartEngine {
28888
29044
  const params = {
28889
29045
  start: new Date(allBars[0].time),
28890
29046
  end: new Date(allBars[allBars.length - 1].time),
28891
- interval: this.timeframe
29047
+ interval: this.timeframe,
29048
+ priceSource: this.tickClosePriceSource
28892
29049
  };
28893
29050
  if (this.compareManager.isActive()) {
28894
29051
  void this.compareManager.refetchAll(params).then(() => {
@@ -29850,7 +30007,7 @@ var _ChartEngine = class _ChartEngine {
29850
30007
  const rawAnchorX = this._orderLineAnchorX(level, chartW) ?? chartW / 2;
29851
30008
  const anchorX = Math.max(dragArea.x, Math.min(dragArea.x + dragArea.w, rawAnchorX));
29852
30009
  const startBarIndex = this.scaleManager.pixelToBarIndex(anchorX, this.viewport, chartW);
29853
- const startTimestamp = this._timeAnchorOverride.get(level.label) ?? level.timestamp ?? this.barIndexToTimestamp(startBarIndex) ?? 0;
30010
+ const startTimestamp = this._resolvedTimeAnchor(level) ?? this.barIndexToTimestamp(startBarIndex) ?? 0;
29854
30011
  this.orderTimeDragHandler.start(level, hitX ?? anchorX, anchorX, startBarIndex, startTimestamp);
29855
30012
  this.drawCanvas.style.cursor = "grabbing";
29856
30013
  this.emitter.emit("orderLineMoveStart", {
@@ -29953,6 +30110,7 @@ var _ChartEngine = class _ChartEngine {
29953
30110
  const fromBarIndex = Math.round(res.startBarIndex);
29954
30111
  if (toTs !== void 0 && res.newBarIndex !== fromBarIndex) {
29955
30112
  this._timeAnchorOverride.set(res.label, toTs);
30113
+ this._orderLineAnchorStore?.set(res.label, toTs);
29956
30114
  this.emitter.emit("orderLineMoved", {
29957
30115
  label: res.label,
29958
30116
  fromTimestamp: res.startTimestamp,
@@ -30076,7 +30234,12 @@ var _ChartEngine = class _ChartEngine {
30076
30234
  if (!this.dataLoader) return;
30077
30235
  if (this.isFetching) return;
30078
30236
  const interval = this.timeframeToInterval(this.getBaseInterval());
30079
- this.dataLoader({ start: new Date(fromTime), end: new Date(toTime), interval }).then((rawBars) => {
30237
+ this.dataLoader({
30238
+ start: new Date(fromTime),
30239
+ end: new Date(toTime),
30240
+ interval,
30241
+ priceSource: this.tickClosePriceSource
30242
+ }).then((rawBars) => {
30080
30243
  const bars = this.maybeAggregate(rawBars);
30081
30244
  if (bars.length === 0) return;
30082
30245
  this.backfillDrawingTimestamps();
@@ -30115,7 +30278,7 @@ var _ChartEngine = class _ChartEngine {
30115
30278
  const start = new Date(barTime);
30116
30279
  const end = new Date(barTime + intervalMs * 3);
30117
30280
  const interval = this.timeframeToInterval(this.getBaseInterval());
30118
- this.dataLoader({ start, end, interval }).then((rawBars) => {
30281
+ this.dataLoader({ start, end, interval, priceSource: this.tickClosePriceSource }).then((rawBars) => {
30119
30282
  const bars = this.maybeAggregate(rawBars);
30120
30283
  if (bars.length === 0) return;
30121
30284
  const all = this.dataStore.all;